This commit is contained in:
dzc committed 2026-07-30 11:42:04 +08:00
1 parent befc75593d
commit c0fa58d2bc
19 files changed
+219 -2

No files matched your search

+3 -1
View File
@@ -300,6 +300,7 @@ COMMIT;
DROP TABLE IF EXISTS `member_user`; DROP TABLE IF EXISTS `member_user`;
CREATE TABLE `member_user` ( CREATE TABLE `member_user` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '编号', `id` bigint NOT NULL AUTO_INCREMENT COMMENT '编号',
`user_code` varchar(6) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL DEFAULT NULL COMMENT '用户 Code,大小写敏感',
`mobile` varchar(11) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '手机号', `mobile` varchar(11) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '手机号',
`password` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '密码', `password` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '密码',
`status` tinyint NOT NULL COMMENT '状态', `status` tinyint NOT NULL COMMENT '状态',
@@ -325,7 +326,8 @@ CREATE TABLE `member_user` (
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除', `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
`tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号', `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
PRIMARY KEY (`id`) USING BTREE PRIMARY KEY (`id`) USING BTREE,
UNIQUE KEY `uk_user_code` (`user_code`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 286 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '会员用户'; ) ENGINE = InnoDB AUTO_INCREMENT = 286 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '会员用户';
-- ---------------------------- -- ----------------------------
@@ -0,0 +1,6 @@
-- 先执行本脚本增加字段和大小写敏感的唯一索引。
-- 再调用 PUT /admin-api/member/user/fill-missing-user-codes,为所有租户的历史用户补齐 Code。
ALTER TABLE `member_user`
ADD COLUMN `user_code` varchar(6) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL DEFAULT NULL
COMMENT '用户 Code,大小写敏感' AFTER `id`,
ADD UNIQUE KEY `uk_user_code` (`user_code`) USING BTREE;
@@ -17,6 +17,10 @@ public class MemberUserRespDTO {
* 用户ID * 用户ID
*/ */
private Long id; private Long id;
/**
* 用户 Code
*/
private String userCode;
/** /**
* 用户昵称 * 用户昵称
*/ */
@@ -19,6 +19,11 @@ public interface ChatApi {
*/ */
ChatLoginRespVO login(@Valid ChatLoginReqVO dto); ChatLoginRespVO login(@Valid ChatLoginReqVO dto);
/**
* 查询用户在线状态
*/
List<ChatUserOnLineStatusRespVo> getUserOnlineStatusById(@Valid ChatUserOnLineStatusReqVo dto);
/** /**
* 创建或者更新聊天用户 * 创建或者更新聊天用户
*/ */
@@ -62,6 +62,38 @@ public class ChatApiImpl implements ChatApi {
} }
} }
/**
* 查询用户在线状态
*/
@Override
public List<ChatUserOnLineStatusRespVo> getUserOnlineStatusById(ChatUserOnLineStatusReqVo dto) {
ChatApiTool.API api = setApiConfig(ChatApiTool.GET_USER_ONLINE_STATUS_BY_ID);
try {
String body = JSONUtil.toJsonStr(dto);
String res = HttpRequest.post(api.getFullPath()).header(apiCountersign(body)).body(body).execute().body();
log.debug("查询用户在线状态,res:{}", res);
if (ObjectUtil.isEmpty(res)) {
log.error("查询用户在线状态,res为空");
return null;
}
ChatBaseRespVO baseRes = JSONUtil.toBean(res, ChatBaseRespVO.class);
if (ObjectUtil.isEmpty(baseRes) || !baseRes.isSuccess()) {
log.error("查询用户在线状态,返回错误");
return null;
}
List<ChatUserOnLineStatusRespVo> vo = JSONUtil.toList(baseRes.getData(), ChatUserOnLineStatusRespVo.class);
if (ObjectUtil.isEmpty(vo)) {
log.error("查询用户在线状态,返回json解析错误");
return null;
}
return vo;
} catch (Exception e) {
e.printStackTrace();
log.error("查询用户在线状态,意外的错误", e);
return null;
}
}
/** /**
* 创建或者更新聊天用户 * 创建或者更新聊天用户
*/ */
@@ -21,6 +21,7 @@ public class ChatApiTool {
public static final API USER_LOGIN = API.builder().summary("用户登录").url("/third/user/login").method(HttpMethod.POST).build(); public static final API USER_LOGIN = API.builder().summary("用户登录").url("/third/user/login").method(HttpMethod.POST).build();
public static final API USER_GET = API.builder().summary("根据userId查询用户详情").url("/third/user/getUserByUserId").method(HttpMethod.POST).build(); public static final API USER_GET = API.builder().summary("根据userId查询用户详情").url("/third/user/getUserByUserId").method(HttpMethod.POST).build();
public static final API USER_SAVE_OR_UPDATE = API.builder().summary("创建或者更新用户").url("/third/user/createOrUpdate").method(HttpMethod.POST).build(); public static final API USER_SAVE_OR_UPDATE = API.builder().summary("创建或者更新用户").url("/third/user/createOrUpdate").method(HttpMethod.POST).build();
public static final API GET_USER_ONLINE_STATUS_BY_ID = API.builder().summary("查询用户在线状态").url("/third/user/getUserOnlineStatusById").method(HttpMethod.POST).build();
// 朋友圈 // 朋友圈
public static final API FRIEND_ADD = API.builder().summary("添加好友").url("/third/friend/add").method(HttpMethod.POST).build(); public static final API FRIEND_ADD = API.builder().summary("添加好友").url("/third/friend/add").method(HttpMethod.POST).build();
@@ -0,0 +1,15 @@
package cn.iocoder.yudao.module.member.client.vo.chat.user;
import lombok.Data;
import java.util.List;
@Data
public class ChatUserOnLineStatusReqVo {
/**
* 聊天用户ids
*/
private List<Long> userIdList;
}
@@ -0,0 +1,16 @@
package cn.iocoder.yudao.module.member.client.vo.chat.user;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
@Schema(description = "chat用户发送消息resp")
public class ChatUserOnLineStatusRespVo {
@Schema(description = "用户id")
private Long id;
@Schema(description = "是否在线")
private Boolean online;
}
@@ -43,6 +43,9 @@ public class RecentPageResVo extends RecentDO {
@Schema(description = "距离(km)") @Schema(description = "距离(km)")
private Double distance; private Double distance;
@Schema(description = "是否在线")
private Boolean onLine;
@Data @Data
public static class MemberPhoto { public static class MemberPhoto {
@@ -28,7 +28,9 @@ import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource; import javax.annotation.Resource;
import javax.annotation.security.PermitAll;
import javax.validation.Valid; import javax.validation.Valid;
import javax.validation.constraints.AssertFalse;
import java.util.Collection; import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
@@ -69,6 +71,13 @@ public class MemberUserController {
return success(true); return success(true);
} }
@GetMapping("/fill-missing-user-codes")
@Operation(summary = "为历史会员补齐用户 Code")
@PreAuthorize("@ss.hasPermission('member:user:update')")
public CommonResult<Integer> fillMissingUserCodes() {
return success(memberUserService.fillMissingUserCodes());
}
@PutMapping("/update-level") @PutMapping("/update-level")
@Operation(summary = "更新会员用户等级") @Operation(summary = "更新会员用户等级")
@PreAuthorize("@ss.hasPermission('member:user:update-level')") @PreAuthorize("@ss.hasPermission('member:user:update-level')")
@@ -19,6 +19,9 @@ public class MemberUserRespVO extends MemberUserBaseVO {
@Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "23788") @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "23788")
private Long id; private Long id;
@Schema(description = "用户 Code,6 位数字和大小写字母组合", example = "aB3xY9")
private String userCode;
@Schema(description = "注册 IP", requiredMode = Schema.RequiredMode.REQUIRED, example = "127.0.0.1") @Schema(description = "注册 IP", requiredMode = Schema.RequiredMode.REQUIRED, example = "127.0.0.1")
private String registerIp; private String registerIp;
@@ -21,6 +21,10 @@ public class AppMemberUserInfoRespVO {
@Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") @Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private Long id; private Long id;
@Schema(description = "用户 Code,6 位数字和大小写字母组合", requiredMode = Schema.RequiredMode.REQUIRED,
example = "aB3xY9")
private String userCode;
@Schema(description = "用户昵称", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋艿") @Schema(description = "用户昵称", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋艿")
private String nickname; private String nickname;
@@ -43,6 +43,11 @@ public class MemberUserDO extends TenantBaseDO {
@TableId @TableId
private Long id; private Long id;
/**
* 用户 Code,6 位数字和大小写字母组合,全局唯一且大小写敏感
*/
private String userCode;
/** /**
* 手机 * 手机
*/ */
@@ -26,6 +26,24 @@ public interface MemberUserMapper extends BaseMapperX<MemberUserDO> {
return selectOne(MemberUserDO::getMobile, mobile); return selectOne(MemberUserDO::getMobile, mobile);
} }
default List<Long> selectIdListByEmptyUserCode(int limit) {
return selectList(new LambdaQueryWrapperX<MemberUserDO>()
.select(MemberUserDO::getId)
.and(wrapper -> wrapper.isNull(MemberUserDO::getUserCode)
.or().eq(MemberUserDO::getUserCode, ""))
.orderByAsc(MemberUserDO::getId)
.last("LIMIT " + limit))
.stream().map(MemberUserDO::getId).collect(Collectors.toList());
}
default int updateUserCodeIfEmpty(Long id, String userCode) {
return update(null, new LambdaUpdateWrapper<MemberUserDO>()
.set(MemberUserDO::getUserCode, userCode)
.eq(MemberUserDO::getId, id)
.and(wrapper -> wrapper.isNull(MemberUserDO::getUserCode)
.or().eq(MemberUserDO::getUserCode, "")));
}
default List<MemberUserDO> selectListByNicknameLike(String nickname) { default List<MemberUserDO> selectListByNicknameLike(String nickname) {
return selectList(new LambdaQueryWrapperX<MemberUserDO>() return selectList(new LambdaQueryWrapperX<MemberUserDO>()
.likeIfPresent(MemberUserDO::getNickname, nickname)); .likeIfPresent(MemberUserDO::getNickname, nickname));
@@ -10,6 +10,8 @@ import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.member.api.thirdauth.ThirdAuthVo; import cn.iocoder.yudao.module.member.api.thirdauth.ThirdAuthVo;
import cn.iocoder.yudao.module.member.client.service.ChatApi; import cn.iocoder.yudao.module.member.client.service.ChatApi;
import cn.iocoder.yudao.module.member.client.service.TxMapApi; import cn.iocoder.yudao.module.member.client.service.TxMapApi;
import cn.iocoder.yudao.module.member.client.vo.chat.user.ChatUserOnLineStatusReqVo;
import cn.iocoder.yudao.module.member.client.vo.chat.user.ChatUserOnLineStatusRespVo;
import cn.iocoder.yudao.module.member.client.vo.txmap.geocoder.TxMapGeocoderReqVO; import cn.iocoder.yudao.module.member.client.vo.txmap.geocoder.TxMapGeocoderReqVO;
import cn.iocoder.yudao.module.member.client.vo.txmap.geocoder.TxMapGeocoderRespVO; import cn.iocoder.yudao.module.member.client.vo.txmap.geocoder.TxMapGeocoderRespVO;
import cn.iocoder.yudao.module.member.controller.admin.recent.vo.*; import cn.iocoder.yudao.module.member.controller.admin.recent.vo.*;
@@ -490,6 +492,8 @@ public class RecentServiceImpl implements RecentService {
List<RecentPageResVo> resultList = BeanUtils.toBean(recentPageList.getRecords(), RecentPageResVo.class); List<RecentPageResVo> resultList = BeanUtils.toBean(recentPageList.getRecords(), RecentPageResVo.class);
// 为RecentPageResVo设置Member信息 // 为RecentPageResVo设置Member信息
convertRecentPageResVoSetupMember(resultList, isVip); convertRecentPageResVoSetupMember(resultList, isVip);
// 远程查询用户是否在线
convertRecentPageResVoSetupIsOnline(resultList);
// 如果是`新人``女生`模糊`头像`和`用户名` // 如果是`新人``女生`模糊`头像`和`用户名`
List<MemberRecentSceneEnum> dimList = Arrays.asList(MemberRecentSceneEnum.SQUARE_NEW_PERSON, MemberRecentSceneEnum.SQUARE_FEMALE); List<MemberRecentSceneEnum> dimList = Arrays.asList(MemberRecentSceneEnum.SQUARE_NEW_PERSON, MemberRecentSceneEnum.SQUARE_FEMALE);
// 查询用户tag // 查询用户tag
@@ -785,6 +789,28 @@ public class RecentServiceImpl implements RecentService {
} }
} }
/**
* 为RecentPageResVo设置isOnline信息
*/
private void convertRecentPageResVoSetupIsOnline(List<RecentPageResVo> list) {
if (ObjectUtil.isEmpty(list)) return;
List<Long> memberIds = list.stream().map(RecentPageResVo::getMemberId).collect(Collectors.toList());
ChatUserOnLineStatusReqVo dto = new ChatUserOnLineStatusReqVo();
dto.setUserIdList(memberIds);
List<ChatUserOnLineStatusRespVo> userOnlineList = chatApi.getUserOnlineStatusById(dto);
if (ObjectUtil.isEmpty(userOnlineList)) return;
Map<Long, ChatUserOnLineStatusRespVo> userOnlineMap = IterUtil.toMap(userOnlineList, ChatUserOnLineStatusRespVo::getId);
for (RecentPageResVo entity : list) {
ChatUserOnLineStatusRespVo userOnLineStatus = userOnlineMap.get(entity.getMemberId());
if(ObjectUtil.isNotEmpty(userOnLineStatus)){
entity.setOnLine(userOnLineStatus.getOnline());
}
}
}
/** /**
* 为RecentPageResVo设置OperationTotal信息 * 为RecentPageResVo设置OperationTotal信息
*/ */
@@ -60,6 +60,13 @@ public interface MemberUserService {
*/ */
MemberUserDO createUser(String nickname, String avtar, String registerIp, Integer terminal); MemberUserDO createUser(String nickname, String avtar, String registerIp, Integer terminal);
/**
* 为所有租户中尚无用户 Code 的历史用户补齐 Code
*
* @return 本次补齐的用户数量
*/
int fillMissingUserCodes();
/** /**
* 更新用户的最后登陆信息 * 更新用户的最后登陆信息
* *
@@ -9,6 +9,7 @@ import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils; import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnore;
import cn.iocoder.yudao.module.member.client.service.ChatApi; import cn.iocoder.yudao.module.member.client.service.ChatApi;
import cn.iocoder.yudao.module.member.client.vo.chat.user.ChatCreateOrUpdateUserReqVO; import cn.iocoder.yudao.module.member.client.vo.chat.user.ChatCreateOrUpdateUserReqVO;
import cn.iocoder.yudao.module.member.controller.admin.user.vo.MemberUserPageReqVO; import cn.iocoder.yudao.module.member.controller.admin.user.vo.MemberUserPageReqVO;
@@ -35,6 +36,7 @@ import cn.iocoder.yudao.module.system.enums.sms.SmsSceneEnum;
import cn.iocoder.yudao.module.trade.api.brokerage.TradeBrokerageUserApi; import cn.iocoder.yudao.module.trade.api.brokerage.TradeBrokerageUserApi;
import com.google.common.annotations.VisibleForTesting; import com.google.common.annotations.VisibleForTesting;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
@@ -61,6 +63,12 @@ import static cn.iocoder.yudao.module.member.enums.MemberErrorCode.*;
@Slf4j @Slf4j
public class MemberUserServiceImpl implements MemberUserService { public class MemberUserServiceImpl implements MemberUserService {
private static final String USER_CODE_CHARACTERS =
"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
private static final int USER_CODE_LENGTH = 6;
private static final int USER_CODE_GENERATE_MAX_RETRY = 20;
private static final int USER_CODE_FILL_BATCH_SIZE = 1000;
@Resource @Resource
private MemberUserMapper memberUserMapper; private MemberUserMapper memberUserMapper;
@Resource @Resource
@@ -132,6 +140,7 @@ public class MemberUserServiceImpl implements MemberUserService {
user.setNickname("用户" + RandomUtil.randomNumbers(6)); user.setNickname("用户" + RandomUtil.randomNumbers(6));
} }
memberUserMapper.insert(user); memberUserMapper.insert(user);
assignUserCode(user);
// 发送 MQ 消息:用户创建 // 发送 MQ 消息:用户创建
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() { TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
@@ -145,6 +154,55 @@ public class MemberUserServiceImpl implements MemberUserService {
return user; return user;
} }
@Override
@TenantIgnore
public int fillMissingUserCodes() {
int fillCount = 0;
while (true) {
List<Long> userIds = memberUserMapper.selectIdListByEmptyUserCode(USER_CODE_FILL_BATCH_SIZE);
if (CollUtil.isEmpty(userIds)) {
return fillCount;
}
for (Long userId : userIds) {
MemberUserDO user = new MemberUserDO().setId(userId);
if (assignUserCode(user)) {
fillCount++;
}
}
}
}
/**
* 通过数据库唯一索引解决并发碰撞;更新方式只修改 user_code,便于安全重试。
*/
private boolean assignUserCode(MemberUserDO user) {
for (int i = 0; i < USER_CODE_GENERATE_MAX_RETRY; i++) {
String userCode = generateUserCode();
try {
if (memberUserMapper.updateUserCodeIfEmpty(user.getId(), userCode) > 0) {
user.setUserCode(userCode);
return true;
}
} catch (DuplicateKeyException ex) {
log.debug("生成会员用户 Code 发生碰撞,准备重试,userId={}", user.getId());
continue;
}
MemberUserDO currentUser = memberUserMapper.selectById(user.getId());
if (currentUser == null) {
return false;
}
if (StrUtil.isNotBlank(currentUser.getUserCode())) {
user.setUserCode(currentUser.getUserCode());
return false;
}
}
throw new IllegalStateException("生成会员用户 Code 失败,请稍后重试,userId=" + user.getId());
}
private String generateUserCode() {
return RandomUtil.randomString(USER_CODE_CHARACTERS, USER_CODE_LENGTH);
}
@Override @Override
public void updateUserLogin(Long id, String loginIp) { public void updateUserLogin(Long id, String loginIp) {
memberUserMapper.updateById(new MemberUserDO().setId(id) memberUserMapper.updateById(new MemberUserDO().setId(id)
@@ -1,6 +1,7 @@
CREATE TABLE IF NOT EXISTS "member_user" CREATE TABLE IF NOT EXISTS "member_user"
( (
"id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY COMMENT '编号', "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY COMMENT '编号',
"user_code" varchar(6) NULL COMMENT '用户 Code',
"nickname" varchar(30) NOT NULL DEFAULT '' COMMENT '用户昵称', "nickname" varchar(30) NOT NULL DEFAULT '' COMMENT '用户昵称',
"name" varchar(30) NULL COMMENT '真实名字', "name" varchar(30) NULL COMMENT '真实名字',
sex tinyint null comment '性别', sex tinyint null comment '性别',
@@ -25,7 +26,8 @@ CREATE TABLE IF NOT EXISTS "member_user"
"update_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', "update_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
"deleted" bit(1) NOT NULL DEFAULT '0' COMMENT '是否删除', "deleted" bit(1) NOT NULL DEFAULT '0' COMMENT '是否删除',
"tenant_id" bigint not null default '0', "tenant_id" bigint not null default '0',
PRIMARY KEY ("id") PRIMARY KEY ("id"),
UNIQUE ("user_code")
) COMMENT '会员表'; ) COMMENT '会员表';
CREATE TABLE IF NOT EXISTS "member_address" ( CREATE TABLE IF NOT EXISTS "member_address" (
@@ -234,6 +234,7 @@ pf4j:
chat: chat:
base-url: http://im-platform.htysapp.com base-url: http://im-platform.htysapp.com
# base-url: http://127.0.0.1:8888
app-key: 4H2HkBLygMsxaQKO app-key: 4H2HkBLygMsxaQKO
app-secret: JIizm84av3TK5xaQdsng7X5mqLPjbeQ0 app-secret: JIizm84av3TK5xaQdsng7X5mqLPjbeQ0