fix:bug修改

This commit is contained in:
dzc committed 2026-07-30 20:20:06 +08:00
1 parent c0fa58d2bc
commit b220dc7e09
37 files changed
+1115 -110

No files matched your search

+13 -4
View File
@@ -6,21 +6,30 @@ CREATE TABLE `member_recent`
(
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '收件地址编号',
`member_id` bigint NOT NULL COMMENT '用户编号',
`cover_pic` varchar(256) NOT NULL COMMENT '主图',
`describe` varchar(256) NOT NULL COMMENT '描述',
`cover_pic` varchar(2048) DEFAULT NULL COMMENT '主图',
`cover_video` varchar(2048) DEFAULT NULL COMMENT '视频',
`description` varchar(1000) NOT NULL COMMENT '描述',
`issue_long` decimal(10, 8) NOT NULL COMMENT '发布-经度',
`issue_lat` decimal(10, 8) NOT NULL COMMENT '发布-纬度',
`issue_province` varchar(64) DEFAULT NULL COMMENT '发布-省',
`issue_city` varchar(64) DEFAULT NULL COMMENT '发布-市',
`issue_district` varchar(64) DEFAULT NULL COMMENT '发布-区',
`issue_address` varchar(256) DEFAULT NULL COMMENT '发布-详细地址',
`visible` tinyint(1) NOT NULL DEFAULT 1 COMMENT '是否展示:0-隐藏,1-显示',
`audit_status` tinyint NOT NULL DEFAULT 0 COMMENT '审核状态:0-审核中,1-审核通过,2-审核不通过',
`audit_source` varchar(16) DEFAULT NULL COMMENT '审核来源:THIRD_PARTY-第三方,MANUAL-人工,HISTORY-历史数据',
`audit_reason` varchar(512) DEFAULT NULL COMMENT '审核原因',
`audit_time` datetime DEFAULT NULL COMMENT '审核时间',
`audit_user_id` bigint DEFAULT NULL COMMENT '人工审核用户编号',
`creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '创建者',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '更新者',
`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,
KEY `idx_member_recent_audit_status_create_time` (`audit_status`, `create_time`),
KEY `idx_member_recent_visible_audit_create_time` (`visible`, `audit_status`, `create_time`)
) ENGINE = InnoDB AUTO_INCREMENT = 32 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_bin COMMENT = '朋友圈';
-- ----------------------------
@@ -318,4 +327,4 @@ CREATE TABLE `pay_wallet_tx_auth`
`deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
`tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 32 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_bin COMMENT = '提现金额审核';
) ENGINE = InnoDB AUTO_INCREMENT = 32 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_bin COMMENT = '提现金额审核';
@@ -0,0 +1,19 @@
-- 动态审核字段。历史动态默认通过,避免上线后从公开列表消失。
ALTER TABLE `member_recent`
ADD COLUMN `audit_status` tinyint NULL DEFAULT 1 COMMENT '审核状态:0-审核中,1-审核通过,2-审核不通过' AFTER `issue_address`,
ADD COLUMN `audit_source` varchar(16) NULL COMMENT '审核来源:THIRD_PARTY-第三方,MANUAL-人工,HISTORY-历史数据' AFTER `audit_status`,
ADD COLUMN `audit_reason` varchar(512) NULL COMMENT '审核原因' AFTER `audit_source`,
ADD COLUMN `audit_time` datetime NULL COMMENT '审核时间' AFTER `audit_reason`,
ADD COLUMN `audit_user_id` bigint NULL COMMENT '人工审核用户编号' AFTER `audit_time`;
UPDATE `member_recent`
SET `audit_source` = 'HISTORY',
`audit_reason` = '历史数据默认通过',
`audit_time` = COALESCE(`update_time`, `create_time`)
WHERE `audit_status` = 1;
ALTER TABLE `member_recent`
MODIFY COLUMN `audit_status` tinyint NOT NULL DEFAULT 0 COMMENT '审核状态:0-审核中,1-审核通过,2-审核不通过';
CREATE INDEX `idx_member_recent_audit_status_create_time`
ON `member_recent` (`audit_status`, `create_time`);
@@ -0,0 +1,6 @@
-- 动态紧急下架字段。历史动态默认继续展示。
ALTER TABLE `member_recent`
ADD COLUMN `visible` tinyint(1) NOT NULL DEFAULT 1 COMMENT '是否展示:0-隐藏,1-显示' AFTER `issue_address`;
CREATE INDEX `idx_member_recent_visible_audit_create_time`
ON `member_recent` (`visible`, `audit_status`, `create_time`);
@@ -22,6 +22,7 @@ public interface MemberErrorCode {
ErrorCode AUTH_LOGIN_USER_DISABLED = new ErrorCode(1_004_003_001, "登录失败,账号被禁用");
ErrorCode AUTH_SOCIAL_USER_NOT_FOUND = new ErrorCode(1_004_003_005, "登录失败,解析不到三方登录信息");
ErrorCode AUTH_MOBILE_USED = new ErrorCode(1_004_003_007, "手机号已经被使用");
ErrorCode AUTH_MINI_PROGRAM_LOGIN_CODE_INVALID = new ErrorCode(1_004_003_008, "小程序登录凭证已失效,请重新发起支付");
// ========== 用户收件地址 1-004-004-000 ==========
ErrorCode ADDRESS_NOT_EXISTS = new ErrorCode(1_004_004_000, "用户收件地址不存在");
@@ -89,4 +90,5 @@ public interface MemberErrorCode {
//========== 聊天相关 1-004-018-000 ==========
ErrorCode CHAT_USER_COUNT_EXCEED = new ErrorCode(1_004_018_000, "当天免费聊天人数已用完");
ErrorCode CHAT_MSG_COUNT_EXCEED = new ErrorCode(1_004_018_001, "当天免费聊天次数已用完");
}
ErrorCode CHAT_TOKEN_CREATE_FAILED = new ErrorCode(1_004_018_002, "聊天登录凭证签发失败,请稍后重试");
}
@@ -0,0 +1,20 @@
package cn.iocoder.yudao.module.member.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 动态审核来源
*/
@Getter
@AllArgsConstructor
public enum MemberRecentAuditSourceEnum {
THIRD_PARTY("THIRD_PARTY", "第三方审核"),
MANUAL("MANUAL", "人工审核"),
HISTORY("HISTORY", "历史数据");
private final String source;
private final String description;
}
@@ -0,0 +1,26 @@
package cn.iocoder.yudao.module.member.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.Arrays;
/**
* 动态审核状态
*/
@Getter
@AllArgsConstructor
public enum MemberRecentAuditStatusEnum {
PENDING(0, "审核中"),
APPROVED(1, "审核通过"),
REJECTED(2, "审核不通过");
private final Integer status;
private final String description;
public static boolean isManualAuditStatus(Integer status) {
return Arrays.asList(APPROVED.status, REJECTED.status).contains(status);
}
}
@@ -282,12 +282,14 @@ public class ThirdAuthServiceImpl implements ThirdAuthService {
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
log.error("|视频审核-同步|,线程被中断videoUrl:{}error:{}", videoUrl, e.getMessage(), e);
resultVo.setIsSuccess(true);
resultVo.setIsSuccess(false);
resultVo.setFirstErrorMsg("视频审核任务被中断,请人工审核");
return resultVo;
} catch (Exception e) {
e.printStackTrace();
log.error("|视频审核-同步|,意外的错误videoUrl:{}error:{}", videoUrl, e.getMessage());
resultVo.setIsSuccess(true);
resultVo.setIsSuccess(false);
resultVo.setFirstErrorMsg("视频审核服务异常,请人工审核");
return resultVo;
}
}
@@ -13,4 +13,7 @@ public class ChatUserOnLineStatusRespVo {
@Schema(description = "是否在线")
private Boolean online;
@Schema(description = "最后活跃时间,毫秒时间戳")
private Long lastActiveTime;
}
@@ -21,6 +21,7 @@ import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
@@ -65,6 +66,22 @@ public class RecentController {
return success(true);
}
@PutMapping("/audit")
@Operation(summary = "人工审核动态")
@PreAuthorize("@ss.hasPermission('member:recent:update')")
public CommonResult<Boolean> auditRecent(@Valid @RequestBody RecentAuditReqVO auditReqVO) {
recentService.auditRecent(auditReqVO, getLoginUserId());
return success(true);
}
@PutMapping("/visible")
@Operation(summary = "更新动态展示状态")
@PreAuthorize("@ss.hasPermission('member:recent:update')")
public CommonResult<Boolean> updateRecentVisible(@Valid @RequestBody RecentVisibleReqVO visibleReqVO) {
recentService.updateRecentVisible(visibleReqVO);
return success(true);
}
@GetMapping("/get")
@Operation(summary = "获得朋友圈")
@Parameter(name = "id", description = "编号", required = true, example = "1024")
@@ -93,4 +110,4 @@ public class RecentController {
ExcelUtils.write(response, "朋友圈.xls", "数据", RecentRespVO.class, BeanUtils.toBean(list, RecentRespVO.class));
}
}
}
@@ -0,0 +1,25 @@
package cn.iocoder.yudao.module.member.controller.admin.recent.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
@Schema(description = "管理后台 - 动态人工审核 Request VO")
@Data
public class RecentAuditReqVO {
@Schema(description = "动态编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
@NotNull(message = "动态编号不能为空")
private Long id;
@Schema(description = "审核状态:1-通过,2-不通过", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
@NotNull(message = "审核状态不能为空")
private Integer auditStatus;
@Schema(description = "审核原因")
@Size(max = 512, message = "审核原因不能超过 512 个字符")
private String auditReason;
}
@@ -73,6 +73,12 @@ public class RecentPageReqVO extends PageParam {
@Schema(description = "是否是VIP")
private Boolean isVip;
@Schema(description = "是否展示")
private Boolean visible;
@Schema(description = "审核状态:0-审核中,1-审核通过,2-审核不通过")
private Integer auditStatus;
/**
* 用户tag相关
*/
@@ -88,4 +94,4 @@ public class RecentPageReqVO extends PageParam {
@Schema(description = "当前经度")
private Double currentLng;
}
}
@@ -34,7 +34,7 @@ public class RecentRespVO {
@Schema(description = "描述", requiredMode = Schema.RequiredMode.REQUIRED)
@ExcelProperty("描述")
private String describe;
private String description;
@Schema(description = "发布-经度", requiredMode = Schema.RequiredMode.REQUIRED)
@ExcelProperty("发布-经度")
@@ -60,8 +60,31 @@ public class RecentRespVO {
@ExcelProperty("发布-详细地址")
private String issueAddress;
@Schema(description = "是否展示")
@ExcelProperty("展示状态")
private Boolean visible;
@Schema(description = "审核状态:0-审核中,1-审核通过,2-审核不通过")
@ExcelProperty("审核状态")
private Integer auditStatus;
@Schema(description = "审核来源")
@ExcelProperty("审核来源")
private String auditSource;
@Schema(description = "审核原因")
@ExcelProperty("审核原因")
private String auditReason;
@Schema(description = "审核时间")
@ExcelProperty("审核时间")
private LocalDateTime auditTime;
@Schema(description = "人工审核用户编号")
private Long auditUserId;
@Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
@ExcelProperty("创建时间")
private LocalDateTime createTime;
}
}
@@ -0,0 +1,20 @@
package cn.iocoder.yudao.module.member.controller.admin.recent.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import javax.validation.constraints.NotNull;
@Schema(description = "管理后台 - 动态展示状态更新 Request VO")
@Data
public class RecentVisibleReqVO {
@Schema(description = "动态编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
@NotNull(message = "动态编号不能为空")
private Long id;
@Schema(description = "是否展示", requiredMode = Schema.RequiredMode.REQUIRED, example = "false")
@NotNull(message = "是否展示不能为空")
private Boolean visible;
}
@@ -69,6 +69,26 @@ public class AppAuthController {
return success(authService.refreshToken(refreshToken));
}
@PostMapping("/mini-program-login-code")
@Operation(summary = "创建 H5 跳转微信小程序的一次性登录码")
public CommonResult<String> createMiniProgramLoginCode() {
return success(authService.createMiniProgramLoginCode(getLoginUserId()));
}
@PostMapping("/mini-program-login")
@Operation(summary = "使用一次性登录码登录微信小程序")
@Parameter(name = "code", description = "一次性登录码", required = true)
@PermitAll
public CommonResult<AppAuthLoginRespVO> loginByMiniProgramCode(@RequestParam("code") String code) {
return success(authService.loginByMiniProgramCode(code));
}
@PostMapping("/chat-token")
@Operation(summary = "使用当前登录态重新签发聊天令牌")
public CommonResult<AppAuthChatTokenRespVO> reissueChatToken() {
return success(new AppAuthChatTokenRespVO(authService.reissueChatToken(getLoginUserId())));
}
// ========== 短信登录相关 ==========
@PostMapping("/sms-register")
@@ -0,0 +1,17 @@
package cn.iocoder.yudao.module.member.controller.app.auth.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Schema(description = "用户 APP - 聊天令牌重新签发 Response VO")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class AppAuthChatTokenRespVO {
@Schema(description = "聊天令牌", requiredMode = Schema.RequiredMode.REQUIRED)
private AppAuthLoginRespVO.ChatToken chatToken;
}
@@ -151,6 +151,17 @@ public class AppRecentController {
return success(recentService.dynamicPage(dto));
}
@PostMapping("/update-and-resubmit")
@Operation(summary = "修改并重新提交朋友圈审核")
public CommonResult<Boolean> updateAndResubmitRecent(@Valid @RequestBody AppRecentUpdateReqVO dto) {
if (ObjectUtil.isAllEmpty(dto.getCoverVideo(), dto.getCoverPic())) {
throw new RuntimeException("图片、视频必须上传其中之一");
}
dto.setMemberId(getLoginUserId());
recentService.updateAndResubmitRecent(BeanUtils.toBean(dto, RecentSaveReqVO.class), getLoginUserId());
return success(true);
}
@PostMapping("/evaluate-page")
@Operation(summary = "我的评价")
public CommonResult<PageResult<cn.iocoder.yudao.module.member.controller.admin.recent.vo.OperationRespVO>> evaluatePage(@Valid @RequestBody AppRecentLikeReqVo dto) {
@@ -183,4 +194,4 @@ public class AppRecentController {
return success(recentService.total(getLoginUserId()));
}
}
}
@@ -13,4 +13,7 @@ public class AppRecentLikeReqVo extends PageParam {
@Schema(description = "操作id")
private Long operationId;
@Schema(description = "审核状态:0-审核中,1-审核通过,2-审核不通过")
private Integer auditStatus;
}
@@ -0,0 +1,18 @@
package cn.iocoder.yudao.module.member.controller.app.recent.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import javax.validation.constraints.NotNull;
@Schema(description = "用户 APP - 修改并重新提交朋友圈 Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
public class AppRecentUpdateReqVO extends AppRecentSaveReqVO {
@Schema(description = "朋友圈编号", requiredMode = Schema.RequiredMode.REQUIRED)
@NotNull(message = "朋友圈编号不能为空")
private Long id;
}
@@ -2,7 +2,7 @@ package cn.iocoder.yudao.module.member.dal.dataobject.recent;
import lombok.*;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import com.baomidou.mybatisplus.annotation.*;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
@@ -78,4 +78,37 @@ public class RecentDO extends BaseDO {
*/
private String issueAddress;
}
/**
* 是否展示
*
* true: 显示
* false: 隐藏
*/
private Boolean visible;
/**
* 审核状态:0-审核中,1-审核通过,2-审核不通过
*/
private Integer auditStatus;
/**
* 审核来源:THIRD_PARTY-第三方,MANUAL-人工,HISTORY-历史数据
*/
private String auditSource;
/**
* 审核原因
*/
private String auditReason;
/**
* 审核时间
*/
private LocalDateTime auditTime;
/**
* 人工审核用户编号
*/
private Long auditUserId;
}
@@ -11,6 +11,7 @@ import cn.iocoder.yudao.module.member.dal.dataobject.user.MemberUserDO;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import org.apache.ibatis.annotations.Mapper;
import java.time.LocalDateTime;
import java.util.List;
import java.util.stream.Collectors;
@@ -44,6 +45,14 @@ public interface MemberUserMapper extends BaseMapperX<MemberUserDO> {
.or().eq(MemberUserDO::getUserCode, "")));
}
default int updateAppRefreshDateIfEarlier(Long id, LocalDateTime appRefreshDate) {
return update(null, new LambdaUpdateWrapper<MemberUserDO>()
.set(MemberUserDO::getAppRefreshDate, appRefreshDate)
.eq(MemberUserDO::getId, id)
.and(wrapper -> wrapper.isNull(MemberUserDO::getAppRefreshDate)
.or().lt(MemberUserDO::getAppRefreshDate, appRefreshDate)));
}
default List<MemberUserDO> selectListByNicknameLike(String nickname) {
return selectList(new LambdaQueryWrapperX<MemberUserDO>()
.likeIfPresent(MemberUserDO::getNickname, nickname));
@@ -9,4 +9,6 @@ public interface RedisKeyConstants {
String MEMBER_COM_AUTH_CALLBACK = "member:com:auth:callback:%s";
String MEMBER_AUTH_MINI_PROGRAM_LOGIN_CODE = "member:auth:mini-program-login-code:%s";
}
@@ -0,0 +1,41 @@
package cn.iocoder.yudao.module.member.dal.redis.auth;
import cn.hutool.core.util.IdUtil;
import cn.iocoder.yudao.module.member.dal.redis.RedisKeyConstants;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Repository;
import javax.annotation.Resource;
import java.time.Duration;
/**
* H5 跳转微信小程序时使用的一次性登录码 Redis DAO。
*/
@Repository
public class MemberAuthLoginCodeRedisDAO {
private static final Duration LOGIN_CODE_TTL = Duration.ofMinutes(5);
@Resource
private StringRedisTemplate stringRedisTemplate;
public String create(Long userId) {
String code = IdUtil.fastSimpleUUID();
stringRedisTemplate.opsForValue().set(formatKey(code), userId.toString(), LOGIN_CODE_TTL);
return code;
}
public Long consume(String code) {
String key = formatKey(code);
String userId = stringRedisTemplate.opsForValue().get(key);
if (userId == null || !Boolean.TRUE.equals(stringRedisTemplate.delete(key))) {
return null;
}
return Long.valueOf(userId);
}
private static String formatKey(String code) {
return String.format(RedisKeyConstants.MEMBER_AUTH_MINI_PROGRAM_LOGIN_CODE, code);
}
}
@@ -85,4 +85,28 @@ public interface MemberAuthService {
* @return 登录结果
*/
AppAuthLoginRespVO refreshToken(String refreshToken);
/**
* 创建 H5 跳转微信小程序时使用的一次性登录码。
*
* @param userId 用户编号
* @return 一次性登录码
*/
String createMiniProgramLoginCode(Long userId);
/**
* 使用一次性登录码登录微信小程序。
*
* @param code 一次性登录码
* @return 登录结果
*/
AppAuthLoginRespVO loginByMiniProgramCode(String code);
/**
* 使用当前主站登录态重新签发聊天令牌。
*
* @param userId 用户编号
* @return 聊天令牌
*/
AppAuthLoginRespVO.ChatToken reissueChatToken(Long userId);
}
@@ -15,6 +15,7 @@ import cn.iocoder.yudao.module.member.controller.admin.user.vo.MemberUserUpdateR
import cn.iocoder.yudao.module.member.controller.app.auth.vo.*;
import cn.iocoder.yudao.module.member.convert.auth.AuthConvert;
import cn.iocoder.yudao.module.member.dal.dataobject.user.MemberUserDO;
import cn.iocoder.yudao.module.member.dal.redis.auth.MemberAuthLoginCodeRedisDAO;
import cn.iocoder.yudao.module.member.service.user.MemberRecentCfgService;
import cn.iocoder.yudao.module.member.service.user.MemberRecentTagService;
import cn.iocoder.yudao.module.member.service.user.MemberUserService;
@@ -75,6 +76,8 @@ public class MemberAuthServiceImpl implements MemberAuthService {
private OAuth2TokenApi oauth2TokenApi;
@Resource
private ChatApi chatApi;
@Resource
private MemberAuthLoginCodeRedisDAO authLoginCodeRedisDAO;
@Override
public AppAuthLoginRespVO login(AppAuthLoginReqVO reqVO) {
@@ -344,6 +347,47 @@ public class MemberAuthServiceImpl implements MemberAuthService {
return AuthConvert.INSTANCE.convert(accessTokenDO, null);
}
@Override
public String createMiniProgramLoginCode(Long userId) {
return authLoginCodeRedisDAO.create(userId);
}
@Override
public AppAuthLoginRespVO loginByMiniProgramCode(String code) {
Long userId = authLoginCodeRedisDAO.consume(code);
if (userId == null) {
throw exception(AUTH_MINI_PROGRAM_LOGIN_CODE_INVALID);
}
MemberUserDO user = userService.getUser(userId);
if (user == null) {
throw exception(USER_NOT_EXISTS);
}
if (CommonStatusEnum.isDisable(user.getStatus())) {
createLoginLog(user.getId(), user.getMobile(), LoginLogTypeEnum.LOGIN_SOCIAL, LoginResultEnum.USER_DISABLED);
throw exception(AUTH_LOGIN_USER_DISABLED);
}
return createTokenAfterLoginSuccess(user, user.getMobile(), LoginLogTypeEnum.LOGIN_SOCIAL, null);
}
@Override
public AppAuthLoginRespVO.ChatToken reissueChatToken(Long userId) {
if (userService.getUser(userId) == null) {
throw exception(USER_NOT_EXISTS);
}
if (!userService.syncChatUser(userId)) {
throw exception(CHAT_TOKEN_CREATE_FAILED);
}
ChatLoginReqVO loginReqVO = new ChatLoginReqVO();
loginReqVO.setTerminal(1);
loginReqVO.setUserId(userId);
ChatLoginRespVO loginRespVO = chatApi.login(loginReqVO);
if (ObjectUtil.isEmpty(loginRespVO)) {
throw exception(CHAT_TOKEN_CREATE_FAILED);
}
return BeanUtils.toBean(loginRespVO, AppAuthLoginRespVO.ChatToken.class);
}
private void createLogoutLog(Long userId) {
LoginLogCreateReqDTO reqDTO = new LoginLogCreateReqDTO();
reqDTO.setLogType(LoginLogTypeEnum.LOGOUT_SELF.getType());
@@ -0,0 +1,15 @@
package cn.iocoder.yudao.module.member.service.recent;
/**
* 动态第三方审核 Service
*/
public interface RecentAuditService {
/**
* 异步审核动态内容。
*
* @param recentId 动态编号
*/
void auditRecentAsync(Long recentId);
}
@@ -0,0 +1,113 @@
package cn.iocoder.yudao.module.member.service.recent;
import cn.hutool.core.util.ObjectUtil;
import cn.iocoder.yudao.module.member.api.thirdauth.ThirdAuthVo;
import cn.iocoder.yudao.module.member.client.service.ThirdAuthService;
import cn.iocoder.yudao.module.member.dal.dataobject.recent.RecentDO;
import cn.iocoder.yudao.module.member.dal.mysql.recent.RecentMapper;
import cn.iocoder.yudao.module.member.enums.MemberRecentAuditSourceEnum;
import cn.iocoder.yudao.module.member.enums.MemberRecentAuditStatusEnum;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.function.Function;
import java.util.stream.Collectors;
/**
* 动态第三方审核 Service 实现
*/
@Service
@Slf4j
public class RecentAuditServiceImpl implements RecentAuditService {
private static final int AUDIT_REASON_MAX_LENGTH = 512;
@Resource
private RecentMapper recentMapper;
@Resource
private ThirdAuthService thirdAuthService;
@Override
@Async
public void auditRecentAsync(Long recentId) {
try {
RecentDO recent = recentMapper.selectById(recentId);
if (recent == null || !ObjectUtil.equals(recent.getAuditStatus(),
MemberRecentAuditStatusEnum.PENDING.getStatus())) {
return;
}
List<CompletableFuture<String>> auditTasks = new ArrayList<>();
if (StringUtils.isNotBlank(recent.getDescription())) {
auditTasks.add(auditContent("文本", recent.getDescription(), thirdAuthService::textAuditSync));
}
addMediaAuditTasks(auditTasks, "图片", recent.getCoverPic(), thirdAuthService::imageAuditSync);
addMediaAuditTasks(auditTasks, "视频", recent.getCoverVideo(), thirdAuthService::videoAuditSync);
List<String> failureReasons = auditTasks.stream()
.map(CompletableFuture::join)
.filter(StringUtils::isNotBlank)
.collect(Collectors.toList());
updateThirdPartyAuditResult(recentId, failureReasons);
} catch (Exception ex) {
// 保持审核中,后台仍可人工处理,避免第三方异常导致内容被自动放行。
log.error("|动态异步审核|审核任务异常 recentId:{}", recentId, ex);
}
}
private void addMediaAuditTasks(List<CompletableFuture<String>> tasks, String contentType, String content,
Function<String, ThirdAuthVo> auditFunction) {
if (StringUtils.isBlank(content)) {
return;
}
for (String item : StringUtils.split(content, ',')) {
String url = StringUtils.trim(item);
if (StringUtils.isNotBlank(url)) {
tasks.add(auditContent(contentType, url, auditFunction));
}
}
}
private CompletableFuture<String> auditContent(String contentType, String content,
Function<String, ThirdAuthVo> auditFunction) {
return CompletableFuture.supplyAsync(() -> {
ThirdAuthVo result = auditFunction.apply(content);
if (result == null) {
return contentType + "审核服务无返回结果";
}
if (!result.isError()) {
return null;
}
String reason = StringUtils.defaultIfBlank(result.getFirstErrorMsg(), result.getErrorMsg());
return contentType + ":" + StringUtils.defaultIfBlank(reason, "审核不通过");
});
}
private void updateThirdPartyAuditResult(Long recentId, List<String> failureReasons) {
boolean approved = failureReasons.isEmpty();
String auditReason = approved ? null : StringUtils.left(String.join(";", failureReasons),
AUDIT_REASON_MAX_LENGTH);
LambdaUpdateWrapper<RecentDO> updateWrapper = new LambdaUpdateWrapper<RecentDO>()
.eq(RecentDO::getId, recentId)
.eq(RecentDO::getAuditStatus, MemberRecentAuditStatusEnum.PENDING.getStatus())
.set(RecentDO::getAuditStatus, approved ? MemberRecentAuditStatusEnum.APPROVED.getStatus()
: MemberRecentAuditStatusEnum.REJECTED.getStatus())
.set(RecentDO::getAuditSource, MemberRecentAuditSourceEnum.THIRD_PARTY.getSource())
.set(RecentDO::getAuditReason, auditReason)
.set(RecentDO::getAuditTime, LocalDateTime.now())
.set(RecentDO::getAuditUserId, null);
int updateCount = recentMapper.update(null, updateWrapper);
if (updateCount == 0) {
log.info("|动态异步审核|动态已被人工处理,忽略第三方结果 recentId:{}", recentId);
}
}
}
@@ -39,6 +39,16 @@ public interface RecentService {
*/
void deleteRecent(Long id);
/**
* 人工审核动态
*/
void auditRecent(@Valid RecentAuditReqVO auditReqVO, Long auditUserId);
/**
* 更新动态展示状态
*/
void updateRecentVisible(@Valid RecentVisibleReqVO visibleReqVO);
/**
* 朋友圈操作
*/
@@ -99,6 +109,11 @@ public interface RecentService {
*/
PageResult<RecentPageResVo> dynamicPage(@Valid AppRecentLikeReqVo dto);
/**
* 修改并重新提交朋友圈审核
*/
void updateAndResubmitRecent(@Valid RecentSaveReqVO updateReqVO, Long memberId);
/**
* 我的评价
*/
@@ -123,4 +138,4 @@ public interface RecentService {
* 朋友圈我喜欢的、喜欢我的等总数统计
*/
OperationTotalGroupRecentRespVO total(Long userId);
}
}
@@ -7,11 +7,8 @@ import cn.hutool.core.date.LocalDateTimeUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.iocoder.yudao.framework.common.util.number.NumberUtils;
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.*;
@@ -24,13 +21,14 @@ import cn.iocoder.yudao.module.member.dal.dataobject.user.MemberRecentTagDO;
import cn.iocoder.yudao.module.member.dal.dataobject.user.MemberUserDO;
import cn.iocoder.yudao.module.member.dal.mysql.recent.OperationMapper;
import cn.iocoder.yudao.module.member.enums.MemberErrorCode;
import cn.iocoder.yudao.module.member.enums.MemberRecentAuditSourceEnum;
import cn.iocoder.yudao.module.member.enums.MemberRecentAuditStatusEnum;
import cn.iocoder.yudao.module.member.enums.MemberRecentOperateEnum;
import cn.iocoder.yudao.module.member.enums.MemberRecentSceneEnum;
import cn.iocoder.yudao.module.member.enums.tag.MemberRecentTagEnum;
import cn.iocoder.yudao.module.member.service.config.MemberConfigService;
import cn.iocoder.yudao.module.member.service.location.LocationBO;
import cn.iocoder.yudao.module.member.service.location.LocationService;
import cn.iocoder.yudao.module.member.client.service.ThirdAuthService;
import cn.iocoder.yudao.module.member.service.opentype.OpenTypeService;
import cn.iocoder.yudao.module.member.service.photo.BO.MemberPhotoTotalExtBO;
import cn.iocoder.yudao.module.member.service.photo.PhotoService;
@@ -53,6 +51,8 @@ import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionSynchronization;
import org.springframework.transaction.support.TransactionSynchronizationManager;
import org.springframework.validation.annotation.Validated;
import cn.iocoder.yudao.module.member.dal.dataobject.recent.RecentDO;
@@ -65,8 +65,6 @@ import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDateTime;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.function.Function;
import java.util.stream.Collectors;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
@@ -91,8 +89,6 @@ public class RecentServiceImpl implements RecentService {
@Resource
private LocationService locationService;
@Resource
private ThirdAuthService thirdAuthService;
@Resource
private PhotoService photoService;
@Resource
private OperationService operationService;
@@ -102,6 +98,8 @@ public class RecentServiceImpl implements RecentService {
private MemberRecentCfgService memberRecentCfgService;
@Resource
private OpenTypeService openTypeService;
@Resource
private RecentAuditService recentAuditService;
@Resource
private TxMapApi txMapApi;
@@ -119,75 +117,53 @@ public class RecentServiceImpl implements RecentService {
if (ObjectUtil.isEmpty(user)) throw exception(MemberErrorCode.USER_NOT_EXISTS);
// 判断是否为会员
// 判断是否有位置信息
if (!ObjectUtil.isAllNotEmpty(vo.getIssueProvince(), vo.getIssueCity(), vo.getIssueDistrict(), vo.getIssueAddress())) {
if (ObjectUtil.isAllNotEmpty(vo.getIssueLat(), vo.getIssueLong())) {
TxMapGeocoderRespVO geocoder = txMapApi.geocoder(TxMapGeocoderReqVO.builder().location(vo.getIssueLat() + "," + vo.getIssueLong()).build());
if (ObjectUtil.isNotEmpty(geocoder) && ObjectUtil.isNotEmpty(geocoder.getAddress_component())) {
vo.setIssueProvince(geocoder.getAddress_component().getProvince());
vo.setIssueCity(geocoder.getAddress_component().getCity());
vo.setIssueDistrict(geocoder.getAddress_component().getDistrict());
vo.setIssueAddress(geocoder.getAddress_component().getStreet());
if (ObjectUtil.isNotEmpty(geocoder.getFormatted_addresses()) && ObjectUtil.isNotEmpty(geocoder.getFormatted_addresses().getRecommend())) {
vo.setIssueAddress(geocoder.getFormatted_addresses().getRecommend());
}
}
}
}
fillIssueAddress(vo);
// 插入
RecentDO recent = BeanUtils.toBean(vo, RecentDO.class);
recent.setVisible(true);
recent.setAuditStatus(MemberRecentAuditStatusEnum.PENDING.getStatus());
recentMapper.insert(recent);
// 文字和图片违规处理
CompletableFuture<ThirdAuthVo> textFuture = CompletableFuture.supplyAsync(
() -> auditCommaSeparatedContent(vo.getDescription(), thirdAuthService::textAuditSync)
);
CompletableFuture<ThirdAuthVo> imageFuture = CompletableFuture.supplyAsync(
() -> auditCommaSeparatedContent(vo.getCoverPic(), thirdAuthService::imageAuditSync)
);
try {
ThirdAuthVo textRes = textFuture.get();
ThirdAuthVo imageRes = imageFuture.get();
// 处理文字
if (ObjectUtil.isEmpty(textRes)) {
throw exception(MemberErrorCode.CHECK_TEXT_NOT_PASS, "未知错误");
}
if (textRes.isError() && ObjectUtil.isNotEmpty(textRes.getFirstErrorMsg())) {
throw exception(MemberErrorCode.CHECK_TEXT_NOT_PASS, textRes.getFirstErrorMsg());
}
// 处理图片
if (ObjectUtil.isEmpty(imageRes)) {
throw exception(MemberErrorCode.CHECK_IMAGE_NOT_PASS, "未知错误");
}
if (imageRes.isError() && ObjectUtil.isNotEmpty(imageRes.getFirstErrorMsg())) {
throw exception(MemberErrorCode.CHECK_IMAGE_NOT_PASS, imageRes.getFirstErrorMsg());
}
} catch (Exception e) {
log.error("|创建朋友圈|,意外的错误error:{}", e.getMessage());
throw exception(MemberErrorCode.EMPTY, e.getMessage());
}
triggerAuditAfterCommit(recent.getId());
// 返回
return recent.getId();
}
private ThirdAuthVo auditCommaSeparatedContent(String content, Function<String, ThirdAuthVo> auditFunction) {
if (StringUtils.isBlank(content)) {
return new ThirdAuthVo();
private void fillIssueAddress(RecentSaveReqVO vo) {
if (ObjectUtil.isAllNotEmpty(vo.getIssueProvince(), vo.getIssueCity(), vo.getIssueDistrict(), vo.getIssueAddress())
|| !ObjectUtil.isAllNotEmpty(vo.getIssueLat(), vo.getIssueLong())) {
return;
}
for (String item : StringUtils.split(content, ',')) {
String auditContent = StringUtils.trim(item);
if (StringUtils.isBlank(auditContent)) {
continue;
}
ThirdAuthVo result = auditFunction.apply(auditContent);
if (ObjectUtil.isEmpty(result) || result.isError()) {
return result;
}
TxMapGeocoderRespVO geocoder = txMapApi.geocoder(TxMapGeocoderReqVO.builder()
.location(vo.getIssueLat() + "," + vo.getIssueLong()).build());
if (ObjectUtil.isEmpty(geocoder) || ObjectUtil.isEmpty(geocoder.getAddress_component())) {
return;
}
return new ThirdAuthVo();
vo.setIssueProvince(geocoder.getAddress_component().getProvince());
vo.setIssueCity(geocoder.getAddress_component().getCity());
vo.setIssueDistrict(geocoder.getAddress_component().getDistrict());
vo.setIssueAddress(geocoder.getAddress_component().getStreet());
if (ObjectUtil.isNotEmpty(geocoder.getFormatted_addresses())
&& ObjectUtil.isNotEmpty(geocoder.getFormatted_addresses().getRecommend())) {
vo.setIssueAddress(geocoder.getFormatted_addresses().getRecommend());
}
}
private void triggerAuditAfterCommit(Long recentId) {
if (!TransactionSynchronizationManager.isSynchronizationActive()) {
recentAuditService.auditRecentAsync(recentId);
return;
}
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
@Override
public void afterCommit() {
recentAuditService.auditRecentAsync(recentId);
}
});
}
/**
@@ -208,9 +184,13 @@ public class RecentServiceImpl implements RecentService {
*/
@Override
public RecentPageResVo getByRecentId(Long recentId, Long loginUserId) {
validateRecentExists(recentId);
// 查询朋友圈
RecentDO recent = recentMapper.selectById(recentId);
if (recent == null || (!ObjectUtil.equals(recent.getMemberId(), loginUserId)
&& (!ObjectUtil.equals(recent.getAuditStatus(), MemberRecentAuditStatusEnum.APPROVED.getStatus())
|| !Boolean.TRUE.equals(recent.getVisible())))) {
throw exception(MemberErrorCode.RECENT_NOT_EXISTS);
}
RecentPageResVo result = BeanUtil.toBean(recent, RecentPageResVo.class);
LocationBO currentUserLastLocation = locationService.getCurrentUserLastLocation();
@@ -248,6 +228,35 @@ public class RecentServiceImpl implements RecentService {
recentMapper.deleteById(id);
}
@Override
public void auditRecent(RecentAuditReqVO auditReqVO, Long auditUserId) {
validateRecentExists(auditReqVO.getId());
if (!MemberRecentAuditStatusEnum.isManualAuditStatus(auditReqVO.getAuditStatus())) {
throw exception(MemberErrorCode.EMPTY, "人工审核状态只能是通过或不通过");
}
if (ObjectUtil.equals(auditReqVO.getAuditStatus(), MemberRecentAuditStatusEnum.REJECTED.getStatus())
&& StringUtils.isBlank(auditReqVO.getAuditReason())) {
throw exception(MemberErrorCode.EMPTY, "审核不通过时必须填写原因");
}
LambdaUpdateWrapper<RecentDO> updateWrapper = new LambdaUpdateWrapper<RecentDO>()
.eq(RecentDO::getId, auditReqVO.getId())
.set(RecentDO::getAuditStatus, auditReqVO.getAuditStatus())
.set(RecentDO::getAuditSource, MemberRecentAuditSourceEnum.MANUAL.getSource())
.set(RecentDO::getAuditReason, StringUtils.trimToNull(auditReqVO.getAuditReason()))
.set(RecentDO::getAuditTime, LocalDateTime.now())
.set(RecentDO::getAuditUserId, auditUserId);
recentMapper.update(null, updateWrapper);
}
@Override
public void updateRecentVisible(RecentVisibleReqVO visibleReqVO) {
validateRecentExists(visibleReqVO.getId());
recentMapper.updateById(new RecentDO()
.setId(visibleReqVO.getId())
.setVisible(visibleReqVO.getVisible()));
}
/**
* 朋友圈操作
*/
@@ -445,6 +454,8 @@ public class RecentServiceImpl implements RecentService {
MemberRecentCfgDO operationCfg = memberRecentCfgService.getOrCreate(operationUser.getId());
RecentPageReqVO resultQuery = BeanUtil.toBean(bean, RecentPageReqVO.class);
resultQuery.setSceneBizType(bean.getScene().getType());
resultQuery.setAuditStatus(MemberRecentAuditStatusEnum.APPROVED.getStatus());
resultQuery.setVisible(true);
quickBeforeQuery(operationUser, operationCfg, resultQuery);
IPage<RecentPageResVo> recentPageList = recentMapper.pageJoin(new Page(bean.getPageNo(), bean.getPageSize()), resultQuery);
@@ -492,8 +503,6 @@ 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
@@ -610,6 +619,7 @@ public class RecentServiceImpl implements RecentService {
RecentPageReqVO dtoQuery = new RecentPageReqVO();
dtoQuery.setMemberId(dto.getOperationMemberId());
dtoQuery.setAuditStatus(dto.getAuditStatus());
IPage<RecentPageResVo> pageList = recentMapper.pageJoin(new Page(dto.getPageNo(), dto.getPageSize()), dtoQuery);
if (ObjectUtil.isEmpty(pageList)) return new PageResult<>();
resultList = BeanUtil.copyToList(pageList.getRecords(), RecentPageResVo.class);
@@ -618,6 +628,73 @@ public class RecentServiceImpl implements RecentService {
return new PageResult<RecentPageResVo>(resultList, pageList.getTotal());
}
@Override
@Transactional
public void updateAndResubmitRecent(RecentSaveReqVO updateReqVO, Long memberId) {
RecentDO recent = recentMapper.selectById(updateReqVO.getId());
if (recent == null || !ObjectUtil.equals(recent.getMemberId(), memberId)) {
throw exception(MemberErrorCode.RECENT_NOT_EXISTS);
}
if (!ObjectUtil.equals(recent.getAuditStatus(), MemberRecentAuditStatusEnum.REJECTED.getStatus())) {
throw exception(MemberErrorCode.EMPTY, "仅审核不通过的动态可以修改后重新提交");
}
String description = StringUtils.trimToEmpty(updateReqVO.getDescription());
String coverPic = normalizeMediaUrls(updateReqVO.getCoverPic());
String coverVideo = normalizeMediaUrls(updateReqVO.getCoverVideo());
if (StringUtils.isBlank(description)) {
throw exception(MemberErrorCode.EMPTY, "描述不能为空");
}
if (coverPic == null && coverVideo == null) {
throw exception(MemberErrorCode.EMPTY, "图片、视频必须上传其中之一");
}
boolean contentChanged = !Objects.equals(StringUtils.trimToEmpty(recent.getDescription()), description)
|| !Objects.equals(normalizeMediaUrls(recent.getCoverPic()), coverPic)
|| !Objects.equals(normalizeMediaUrls(recent.getCoverVideo()), coverVideo);
if (!contentChanged) {
throw exception(MemberErrorCode.EMPTY, "请先修改动态内容再重新发布");
}
updateReqVO.setDescription(description);
updateReqVO.setCoverPic(coverPic);
updateReqVO.setCoverVideo(coverVideo);
fillIssueAddress(updateReqVO);
int updated = recentMapper.update(null, new LambdaUpdateWrapper<RecentDO>()
.eq(RecentDO::getId, updateReqVO.getId())
.eq(RecentDO::getMemberId, memberId)
.eq(RecentDO::getAuditStatus, MemberRecentAuditStatusEnum.REJECTED.getStatus())
.set(RecentDO::getDescription, updateReqVO.getDescription())
.set(RecentDO::getCoverPic, updateReqVO.getCoverPic())
.set(RecentDO::getCoverVideo, updateReqVO.getCoverVideo())
.set(RecentDO::getIssueLong, updateReqVO.getIssueLong())
.set(RecentDO::getIssueLat, updateReqVO.getIssueLat())
.set(RecentDO::getIssueProvince, updateReqVO.getIssueProvince())
.set(RecentDO::getIssueCity, updateReqVO.getIssueCity())
.set(RecentDO::getIssueDistrict, updateReqVO.getIssueDistrict())
.set(RecentDO::getIssueAddress, updateReqVO.getIssueAddress())
.set(RecentDO::getAuditStatus, MemberRecentAuditStatusEnum.PENDING.getStatus())
.set(RecentDO::getAuditSource, null)
.set(RecentDO::getAuditReason, null)
.set(RecentDO::getAuditTime, null)
.set(RecentDO::getAuditUserId, null));
if (updated == 0) {
throw exception(MemberErrorCode.EMPTY, "动态状态已变化,请刷新后重试");
}
triggerAuditAfterCommit(updateReqVO.getId());
}
private String normalizeMediaUrls(String content) {
if (StringUtils.isBlank(content)) {
return null;
}
String normalized = Arrays.stream(StringUtils.split(content, ','))
.map(StringUtils::trim)
.filter(StringUtils::isNotBlank)
.collect(Collectors.joining(","));
return StringUtils.trimToNull(normalized);
}
/**
* 我的评价
*/
@@ -786,28 +863,7 @@ public class RecentServiceImpl implements RecentService {
member.setWechatNum(member.getwechatMaskString());
}
entity.setMember(member);
}
}
/**
* 为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());
}
entity.setOnLine(member == null ? null : member.getOnline());
}
}
@@ -838,6 +894,7 @@ public class RecentServiceImpl implements RecentService {
member.setWechatNum(member.getwechatMaskString());
}
detail.setMember(member);
detail.setOnLine(member == null ? null : member.getOnline());
}
// 为RecentPageResVo设置userOperationStatus信息
@@ -47,9 +47,12 @@ public class MemberExtBO {
@Schema(description = "纬度")
private Double memberLng;
@Schema(description = "最近登录时间")
@Schema(description = "最后在线时间")
private LocalDateTime appRefreshDate;
@Schema(description = "是否在线")
private Boolean online;
public String getwechatMaskString() {
String str = this.wechatNum;
@@ -12,6 +12,8 @@ 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.client.vo.chat.user.ChatUserOnLineStatusReqVo;
import cn.iocoder.yudao.module.member.client.vo.chat.user.ChatUserOnLineStatusRespVo;
import cn.iocoder.yudao.module.member.controller.admin.user.vo.MemberUserPageReqVO;
import cn.iocoder.yudao.module.member.controller.admin.user.vo.MemberUserUpdateReqVO;
import cn.iocoder.yudao.module.member.controller.app.user.vo.*;
@@ -45,10 +47,14 @@ import org.springframework.transaction.support.TransactionSynchronizationManager
import javax.annotation.Resource;
import javax.validation.Valid;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
@@ -231,7 +237,7 @@ public class MemberUserServiceImpl implements MemberUserService {
return ListUtil.empty();
}
List<MemberUserDO> memberUserDOS = memberUserMapper.selectBatchIds(ids);
return memberUserDOS.stream().map(member -> {
List<MemberExtBO> memberList = memberUserDOS.stream().map(member -> {
MemberExtBO entity = new MemberExtBO();
entity.setMemberId(member.getId());
entity.setMemberAvatar(member.getAvatar());
@@ -248,6 +254,45 @@ public class MemberUserServiceImpl implements MemberUserService {
entity.setWechatNum(member.getWechatNum());
return entity;
}).collect(Collectors.toList());
syncChatActivity(memberList);
return memberList;
}
private void syncChatActivity(List<MemberExtBO> memberList) {
if (CollUtil.isEmpty(memberList)) {
return;
}
ChatUserOnLineStatusReqVo reqVO = new ChatUserOnLineStatusReqVo();
reqVO.setUserIdList(memberList.stream().map(MemberExtBO::getMemberId).collect(Collectors.toList()));
List<ChatUserOnLineStatusRespVo> statusList = chatApi.getUserOnlineStatusById(reqVO);
if (CollUtil.isEmpty(statusList)) {
return;
}
Map<Long, ChatUserOnLineStatusRespVo> statusMap = statusList.stream()
.filter(status -> status.getId() != null)
.collect(Collectors.toMap(ChatUserOnLineStatusRespVo::getId, Function.identity(), (first, second) -> second));
for (MemberExtBO member : memberList) {
ChatUserOnLineStatusRespVo status = statusMap.get(member.getMemberId());
if (status == null) {
continue;
}
member.setOnline(status.getOnline());
if (status.getLastActiveTime() == null || status.getLastActiveTime() <= 0) {
continue;
}
LocalDateTime lastActiveDate = LocalDateTime.ofInstant(
Instant.ofEpochMilli(status.getLastActiveTime()), ZoneId.systemDefault());
if (member.getAppRefreshDate() != null && !lastActiveDate.isAfter(member.getAppRefreshDate())) {
continue;
}
member.setAppRefreshDate(lastActiveDate);
try {
memberUserMapper.updateAppRefreshDateIfEarlier(member.getMemberId(), lastActiveDate);
} catch (Exception e) {
log.warn("回写用户最后在线时间失败,userId:{},lastActiveDate:{}",
member.getMemberId(), lastActiveDate, e);
}
}
}
@Override
@@ -13,6 +13,12 @@
a.issue_city,
a.issue_district,
a.issue_address,
a.visible,
a.audit_status,
a.audit_source,
a.audit_reason,
a.audit_time,
a.audit_user_id,
a.creator,
a.create_time,
a.updater,
@@ -41,8 +47,14 @@
<if test="dto.issueAddress != null">
and a.issue_address like concat('%', #{dto.issueAddress}, '%')
</if>
<if test="dto.auditStatus != null">
and a.audit_status = #{dto.auditStatus}
</if>
<if test="dto.visible != null">
and a.visible = #{dto.visible}
</if>
<if test="dto.createTime != null and dto.createTime.length == 2">
and a.createTime between #{dto.createTime[0]} and #{dto.createTime[1]}
and a.create_time between #{dto.createTime[0]} and #{dto.createTime[1]}
</if>
<if test="dto.excludeUserIdList != null and dto.excludeUserIdList.size() != 0">
and a.member_id not in
@@ -71,6 +83,9 @@
<if test="dto.memberSex != null">
and u.sex = #{dto.memberSex}
</if>
<if test="dto.memberNickname != null and dto.memberNickname != ''.toString()">
and u.nickname like concat('%', #{dto.memberNickname}, '%')
</if>
<if test="dto.memberRecentTagList != null and dto.memberRecentTagList.size() != 0">
and exists (select 1
from member_recent_tag t
@@ -0,0 +1,68 @@
package cn.iocoder.yudao.module.member.service.auth;
import cn.iocoder.yudao.framework.common.exception.ServiceException;
import cn.iocoder.yudao.module.member.client.service.ChatApi;
import cn.iocoder.yudao.module.member.client.vo.chat.user.ChatLoginReqVO;
import cn.iocoder.yudao.module.member.client.vo.chat.user.ChatLoginRespVO;
import cn.iocoder.yudao.module.member.controller.app.auth.vo.AppAuthLoginRespVO;
import cn.iocoder.yudao.module.member.service.user.MemberUserService;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import static cn.iocoder.yudao.module.member.enums.MemberErrorCode.CHAT_TOKEN_CREATE_FAILED;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class MemberAuthServiceImplChatTokenTest {
@InjectMocks
private MemberAuthServiceImpl authService;
@Mock
private MemberUserService userService;
@Mock
private ChatApi chatApi;
@Test
void reissueChatToken_success() {
Long userId = 100L;
when(userService.getUser(userId)).thenReturn(new cn.iocoder.yudao.module.member.dal.dataobject.user.MemberUserDO());
when(userService.syncChatUser(userId)).thenReturn(true);
ChatLoginRespVO chatLoginRespVO = new ChatLoginRespVO();
chatLoginRespVO.setAccessToken("new-access-token");
chatLoginRespVO.setAccessTokenExpiresIn(3600);
chatLoginRespVO.setRefreshToken("new-refresh-token");
chatLoginRespVO.setRefreshTokenExpiresIn(7200);
when(chatApi.login(org.mockito.ArgumentMatchers.any(ChatLoginReqVO.class))).thenReturn(chatLoginRespVO);
AppAuthLoginRespVO.ChatToken result = authService.reissueChatToken(userId);
assertEquals("new-access-token", result.getAccessToken());
assertEquals("new-refresh-token", result.getRefreshToken());
ArgumentCaptor<ChatLoginReqVO> requestCaptor = ArgumentCaptor.forClass(ChatLoginReqVO.class);
verify(chatApi).login(requestCaptor.capture());
assertEquals(userId, requestCaptor.getValue().getUserId());
assertEquals(1, requestCaptor.getValue().getTerminal());
}
@Test
void reissueChatToken_syncFailed() {
Long userId = 100L;
when(userService.getUser(userId)).thenReturn(new cn.iocoder.yudao.module.member.dal.dataobject.user.MemberUserDO());
when(userService.syncChatUser(userId)).thenReturn(false);
ServiceException exception = assertThrows(ServiceException.class,
() -> authService.reissueChatToken(userId));
assertEquals(CHAT_TOKEN_CREATE_FAILED.getCode(), exception.getCode());
}
}
@@ -0,0 +1,97 @@
package cn.iocoder.yudao.module.member.service.auth;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.common.exception.ServiceException;
import cn.iocoder.yudao.module.member.controller.app.auth.vo.AppAuthLoginRespVO;
import cn.iocoder.yudao.module.member.dal.dataobject.user.MemberUserDO;
import cn.iocoder.yudao.module.member.dal.redis.auth.MemberAuthLoginCodeRedisDAO;
import cn.iocoder.yudao.module.member.service.user.MemberUserService;
import cn.iocoder.yudao.module.system.api.logger.LoginLogApi;
import cn.iocoder.yudao.module.system.api.oauth2.OAuth2TokenApi;
import cn.iocoder.yudao.module.system.api.oauth2.dto.OAuth2AccessTokenCreateReqDTO;
import cn.iocoder.yudao.module.system.api.oauth2.dto.OAuth2AccessTokenRespDTO;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import java.time.LocalDateTime;
import static cn.iocoder.yudao.module.member.enums.MemberErrorCode.AUTH_MINI_PROGRAM_LOGIN_CODE_INVALID;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class MemberAuthServiceImplMiniProgramLoginTest {
@InjectMocks
private MemberAuthServiceImpl authService;
@Mock
private MemberAuthLoginCodeRedisDAO authLoginCodeRedisDAO;
@Mock
private MemberUserService userService;
@Mock
private LoginLogApi loginLogApi;
@Mock
private OAuth2TokenApi oauth2TokenApi;
@AfterEach
void tearDown() {
RequestContextHolder.resetRequestAttributes();
}
@Test
void createMiniProgramLoginCode_success() {
when(authLoginCodeRedisDAO.create(100L)).thenReturn("login-code");
assertEquals("login-code", authService.createMiniProgramLoginCode(100L));
}
@Test
void loginByMiniProgramCode_invalidCode() {
when(authLoginCodeRedisDAO.consume("invalid-code")).thenReturn(null);
ServiceException exception = assertThrows(ServiceException.class,
() -> authService.loginByMiniProgramCode("invalid-code"));
assertEquals(AUTH_MINI_PROGRAM_LOGIN_CODE_INVALID.getCode(), exception.getCode());
}
@Test
void loginByMiniProgramCode_success() {
MockHttpServletRequest request = new MockHttpServletRequest();
request.setRemoteAddr("127.0.0.1");
RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(request));
MemberUserDO user = new MemberUserDO()
.setId(100L)
.setMobile("13800138000")
.setStatus(CommonStatusEnum.ENABLE.getStatus());
when(authLoginCodeRedisDAO.consume("login-code")).thenReturn(user.getId());
when(userService.getUser(user.getId())).thenReturn(user);
OAuth2AccessTokenRespDTO token = new OAuth2AccessTokenRespDTO()
.setUserId(user.getId())
.setAccessToken("access-token")
.setRefreshToken("refresh-token")
.setExpiresTime(LocalDateTime.now().plusDays(1));
when(oauth2TokenApi.createAccessToken(any(OAuth2AccessTokenCreateReqDTO.class))).thenReturn(token);
AppAuthLoginRespVO result = authService.loginByMiniProgramCode("login-code");
assertEquals(user.getId(), result.getUserId());
assertEquals("access-token", result.getAccessToken());
assertEquals("refresh-token", result.getRefreshToken());
verify(authLoginCodeRedisDAO).consume("login-code");
}
}
@@ -0,0 +1,91 @@
package cn.iocoder.yudao.module.member.service.recent;
import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
import cn.iocoder.yudao.module.member.controller.admin.recent.vo.RecentSaveReqVO;
import cn.iocoder.yudao.module.member.dal.dataobject.recent.RecentDO;
import cn.iocoder.yudao.module.member.dal.mysql.recent.RecentMapper;
import cn.iocoder.yudao.module.member.enums.MemberRecentAuditStatusEnum;
import com.baomidou.mybatisplus.core.MybatisConfiguration;
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
import org.apache.ibatis.builder.MapperBuilderAssistant;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException;
import static cn.iocoder.yudao.module.member.enums.MemberErrorCode.EMPTY;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.isNull;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
class RecentServiceImplTest extends BaseMockitoUnitTest {
private static final Long RECENT_ID = 1L;
private static final Long MEMBER_ID = 2L;
@InjectMocks
private RecentServiceImpl recentService;
@Mock
private RecentMapper recentMapper;
@Mock
private RecentAuditService recentAuditService;
@BeforeAll
static void initTableInfo() {
TableInfoHelper.initTableInfo(new MapperBuilderAssistant(new MybatisConfiguration(), ""), RecentDO.class);
}
@Test
void updateAndResubmitRecentRejectsUnchangedContent() {
when(recentMapper.selectById(RECENT_ID)).thenReturn(createRejectedRecent());
RecentSaveReqVO reqVO = createUpdateReqVO("原动态内容");
assertServiceException(() -> recentService.updateAndResubmitRecent(reqVO, MEMBER_ID),
EMPTY, "请先修改动态内容再重新发布");
verify(recentMapper, never()).update(isNull(), any());
verify(recentAuditService, never()).auditRecentAsync(any());
}
@Test
void updateAndResubmitRecentUpdatesContentAndTriggersAudit() {
when(recentMapper.selectById(RECENT_ID)).thenReturn(createRejectedRecent());
when(recentMapper.update(isNull(), any())).thenReturn(1);
RecentSaveReqVO reqVO = createUpdateReqVO("修改后的动态内容");
recentService.updateAndResubmitRecent(reqVO, MEMBER_ID);
verify(recentMapper).update(isNull(), any());
verify(recentAuditService).auditRecentAsync(RECENT_ID);
}
private RecentDO createRejectedRecent() {
return RecentDO.builder()
.id(RECENT_ID)
.memberId(MEMBER_ID)
.description("原动态内容")
.coverPic("https://example.com/image.jpg")
.auditStatus(MemberRecentAuditStatusEnum.REJECTED.getStatus())
.build();
}
private RecentSaveReqVO createUpdateReqVO(String description) {
RecentSaveReqVO reqVO = new RecentSaveReqVO();
reqVO.setId(RECENT_ID);
reqVO.setMemberId(MEMBER_ID);
reqVO.setDescription(description);
reqVO.setCoverPic("https://example.com/image.jpg");
reqVO.setIssueLong(121.47D);
reqVO.setIssueLat(31.23D);
reqVO.setIssueProvince("上海市");
reqVO.setIssueCity("上海市");
reqVO.setIssueDistrict("浦东新区");
reqVO.setIssueAddress("测试地址");
return reqVO;
}
}
@@ -0,0 +1,83 @@
package cn.iocoder.yudao.module.member.service.user;
import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
import cn.iocoder.yudao.module.member.client.service.ChatApi;
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.dal.dataobject.user.MemberUserDO;
import cn.iocoder.yudao.module.member.dal.mysql.user.MemberUserMapper;
import cn.iocoder.yudao.module.member.service.user.BO.MemberExtBO;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.Collections;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
class MemberUserServiceImplChatActivityTest extends BaseMockitoUnitTest {
private static final Long USER_ID = 100L;
@InjectMocks
private MemberUserServiceImpl memberUserService;
@Mock
private MemberUserMapper memberUserMapper;
@Mock
private ChatApi chatApi;
@Test
void getUserExtListUsesNewerChatActivity() {
LocalDateTime databaseTime = LocalDateTime.of(2026, 7, 30, 10, 0);
LocalDateTime chatTime = databaseTime.plusMinutes(5);
when(memberUserMapper.selectBatchIds(Collections.singletonList(USER_ID)))
.thenReturn(Collections.singletonList(new MemberUserDO().setId(USER_ID).setAppRefreshDate(databaseTime)));
ChatUserOnLineStatusRespVo status = createStatus(true, chatTime);
when(chatApi.getUserOnlineStatusById(any())).thenReturn(Collections.singletonList(status));
List<MemberExtBO> result = memberUserService.getUserExtList(Collections.singletonList(USER_ID));
assertEquals(1, result.size());
assertTrue(result.get(0).getOnline());
assertEquals(chatTime, result.get(0).getAppRefreshDate());
verify(memberUserMapper).updateAppRefreshDateIfEarlier(USER_ID, chatTime);
ArgumentCaptor<ChatUserOnLineStatusReqVo> requestCaptor = ArgumentCaptor.forClass(ChatUserOnLineStatusReqVo.class);
verify(chatApi).getUserOnlineStatusById(requestCaptor.capture());
assertEquals(Collections.singletonList(USER_ID), requestCaptor.getValue().getUserIdList());
}
@Test
void getUserExtListKeepsNewerDatabaseActivity() {
LocalDateTime databaseTime = LocalDateTime.of(2026, 7, 30, 10, 5);
LocalDateTime chatTime = databaseTime.minusMinutes(5);
when(memberUserMapper.selectBatchIds(Collections.singletonList(USER_ID)))
.thenReturn(Collections.singletonList(new MemberUserDO().setId(USER_ID).setAppRefreshDate(databaseTime)));
when(chatApi.getUserOnlineStatusById(any()))
.thenReturn(Collections.singletonList(createStatus(false, chatTime)));
List<MemberExtBO> result = memberUserService.getUserExtList(Collections.singletonList(USER_ID));
assertEquals(databaseTime, result.get(0).getAppRefreshDate());
verify(memberUserMapper, never()).updateAppRefreshDateIfEarlier(eq(USER_ID), any());
}
private ChatUserOnLineStatusRespVo createStatus(boolean online, LocalDateTime lastActiveTime) {
ChatUserOnLineStatusRespVo status = new ChatUserOnLineStatusRespVo();
status.setId(USER_ID);
status.setOnline(online);
status.setLastActiveTime(lastActiveTime.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli());
return status;
}
}
@@ -277,7 +277,7 @@ public class SocialClientServiceImpl implements SocialClientService {
// 正式版本-release
// 体验版-trial
// 开发版-develop
WxMaGenerateSchemeRequest.JumpWxa.newBuilder().path(path).query(json).envVersion("release").build()
WxMaGenerateSchemeRequest.JumpWxa.newBuilder().path(path).query(json).envVersion("trial").build()
)
.expireTime(expireTime)
.expireType(0)
@@ -304,7 +304,8 @@ public class SocialClientServiceImpl implements SocialClientService {
ObjUtil.defaultIfNull(reqVO.getWidth(), SocialWxQrcodeReqDTO.WIDTH),
ObjUtil.defaultIfNull(reqVO.getAutoColor(), SocialWxQrcodeReqDTO.AUTO_COLOR),
null,
ObjUtil.defaultIfNull(reqVO.getHyaline(), SocialWxQrcodeReqDTO.HYALINE));
ObjUtil.defaultIfNull(reqVO.getHyaline(), SocialWxQrcodeReqDTO.HYALINE)
);
} catch (WxErrorException e) {
log.error("[getWxQrcode][reqVO({}) 获得小程序码失败]", reqVO, e);
throw exception(SOCIAL_CLIENT_WEIXIN_MINI_APP_QRCODE_ERROR);
@@ -180,6 +180,8 @@ yudao:
transfer-notify-url: https://baac9a029ea84f36ba28c023fed4fc5f.hd.takin.cc/admin-api/pay/notify/transfer # 支付渠道的【转账】回调地址
demo: false # 开启演示模式
tencent-lbs-key: TVDBZ-TDILD-4ON4B-PFDZA-RNLKH-VVF6E # QQ 地图的密钥 https://lbs.qq.com/service/staticV2/staticGuide/staticDoc
wxa-code:
env-version: trial # 小程序版本: 正式版为 "release";体验版为 "trial";开发版为 "develop"
justauth:
enabled: true