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`;
CREATE TABLE `member_user` (
`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 '手机号',
`password` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' 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 '更新时间',
`deleted` bit(1) NOT NULL DEFAULT b'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 = '会员用户';
-- ----------------------------
@@ -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
*/
private Long id;
/**
* 用户 Code
*/
private String userCode;
/**
* 用户昵称
*/
@@ -19,6 +19,11 @@ public interface ChatApi {
*/
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_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 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();
@@ -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)")
private Double distance;
@Schema(description = "是否在线")
private Boolean onLine;
@Data
public static class MemberPhoto {
@@ -28,7 +28,9 @@ import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import javax.annotation.security.PermitAll;
import javax.validation.Valid;
import javax.validation.constraints.AssertFalse;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
@@ -69,6 +71,13 @@ public class MemberUserController {
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")
@Operation(summary = "更新会员用户等级")
@PreAuthorize("@ss.hasPermission('member:user:update-level')")
@@ -19,6 +19,9 @@ public class MemberUserRespVO extends MemberUserBaseVO {
@Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "23788")
private Long id;
@Schema(description = "用户 Code,6 位数字和大小写字母组合", example = "aB3xY9")
private String userCode;
@Schema(description = "注册 IP", requiredMode = Schema.RequiredMode.REQUIRED, example = "127.0.0.1")
private String registerIp;
@@ -21,6 +21,10 @@ public class AppMemberUserInfoRespVO {
@Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private Long id;
@Schema(description = "用户 Code,6 位数字和大小写字母组合", requiredMode = Schema.RequiredMode.REQUIRED,
example = "aB3xY9")
private String userCode;
@Schema(description = "用户昵称", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋艿")
private String nickname;
@@ -43,6 +43,11 @@ public class MemberUserDO extends TenantBaseDO {
@TableId
private Long id;
/**
* 用户 Code,6 位数字和大小写字母组合,全局唯一且大小写敏感
*/
private String userCode;
/**
* 手机
*/
@@ -26,6 +26,24 @@ public interface MemberUserMapper extends BaseMapperX<MemberUserDO> {
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) {
return selectList(new LambdaQueryWrapperX<MemberUserDO>()
.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.client.service.ChatApi;
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.TxMapGeocoderRespVO;
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);
// 为RecentPageResVo设置Member信息
convertRecentPageResVoSetupMember(resultList, isVip);
// 远程查询用户是否在线
convertRecentPageResVoSetupIsOnline(resultList);
// 如果是`新人``女生`模糊`头像`和`用户名`
List<MemberRecentSceneEnum> dimList = Arrays.asList(MemberRecentSceneEnum.SQUARE_NEW_PERSON, MemberRecentSceneEnum.SQUARE_FEMALE);
// 查询用户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信息
*/
@@ -60,6 +60,13 @@ public interface MemberUserService {
*/
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.util.object.BeanUtils;
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.vo.chat.user.ChatCreateOrUpdateUserReqVO;
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 com.google.common.annotations.VisibleForTesting;
import lombok.extern.slf4j.Slf4j;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -61,6 +63,12 @@ import static cn.iocoder.yudao.module.member.enums.MemberErrorCode.*;
@Slf4j
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
private MemberUserMapper memberUserMapper;
@Resource
@@ -132,6 +140,7 @@ public class MemberUserServiceImpl implements MemberUserService {
user.setNickname("用户" + RandomUtil.randomNumbers(6));
}
memberUserMapper.insert(user);
assignUserCode(user);
// 发送 MQ 消息:用户创建
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
@@ -145,6 +154,55 @@ public class MemberUserServiceImpl implements MemberUserService {
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
public void updateUserLogin(Long id, String loginIp) {
memberUserMapper.updateById(new MemberUserDO().setId(id)
@@ -1,6 +1,7 @@
CREATE TABLE IF NOT EXISTS "member_user"
(
"id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY COMMENT '编号',
"user_code" varchar(6) NULL COMMENT '用户 Code',
"nickname" varchar(30) NOT NULL DEFAULT '' COMMENT '用户昵称',
"name" varchar(30) 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 '更新时间',
"deleted" bit(1) NOT NULL DEFAULT '0' COMMENT '是否删除',
"tenant_id" bigint not null default '0',
PRIMARY KEY ("id")
PRIMARY KEY ("id"),
UNIQUE ("user_code")
) COMMENT '会员表';
CREATE TABLE IF NOT EXISTS "member_address" (
@@ -234,6 +234,7 @@ pf4j:
chat:
base-url: http://im-platform.htysapp.com
# base-url: http://127.0.0.1:8888
app-key: 4H2HkBLygMsxaQKO
app-secret: JIizm84av3TK5xaQdsng7X5mqLPjbeQ0