diff --git a/sql/mysql/recent/member_recent.sql b/sql/mysql/recent/member_recent.sql index 7b73811..a1731f3 100644 --- a/sql/mysql/recent/member_recent.sql +++ b/sql/mysql/recent/member_recent.sql @@ -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 = '提现金额审核'; \ No newline at end of file +) ENGINE = InnoDB AUTO_INCREMENT = 32 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_bin COMMENT = '提现金额审核'; diff --git a/sql/mysql/recent/member_recent_add_audit.sql b/sql/mysql/recent/member_recent_add_audit.sql new file mode 100644 index 0000000..d709b94 --- /dev/null +++ b/sql/mysql/recent/member_recent_add_audit.sql @@ -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`); diff --git a/sql/mysql/recent/member_recent_add_visible.sql b/sql/mysql/recent/member_recent_add_visible.sql new file mode 100644 index 0000000..bcdacb3 --- /dev/null +++ b/sql/mysql/recent/member_recent_add_visible.sql @@ -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`); diff --git a/yudao-module-member/yudao-module-member-api/src/main/java/cn/iocoder/yudao/module/member/enums/MemberErrorCode.java b/yudao-module-member/yudao-module-member-api/src/main/java/cn/iocoder/yudao/module/member/enums/MemberErrorCode.java index 6a5dfef..013d824 100644 --- a/yudao-module-member/yudao-module-member-api/src/main/java/cn/iocoder/yudao/module/member/enums/MemberErrorCode.java +++ b/yudao-module-member/yudao-module-member-api/src/main/java/cn/iocoder/yudao/module/member/enums/MemberErrorCode.java @@ -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, "当天免费聊天次数已用完"); -} \ No newline at end of file + ErrorCode CHAT_TOKEN_CREATE_FAILED = new ErrorCode(1_004_018_002, "聊天登录凭证签发失败,请稍后重试"); +} diff --git a/yudao-module-member/yudao-module-member-api/src/main/java/cn/iocoder/yudao/module/member/enums/MemberRecentAuditSourceEnum.java b/yudao-module-member/yudao-module-member-api/src/main/java/cn/iocoder/yudao/module/member/enums/MemberRecentAuditSourceEnum.java new file mode 100644 index 0000000..c8c1cb4 --- /dev/null +++ b/yudao-module-member/yudao-module-member-api/src/main/java/cn/iocoder/yudao/module/member/enums/MemberRecentAuditSourceEnum.java @@ -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; + +} diff --git a/yudao-module-member/yudao-module-member-api/src/main/java/cn/iocoder/yudao/module/member/enums/MemberRecentAuditStatusEnum.java b/yudao-module-member/yudao-module-member-api/src/main/java/cn/iocoder/yudao/module/member/enums/MemberRecentAuditStatusEnum.java new file mode 100644 index 0000000..ab3909c --- /dev/null +++ b/yudao-module-member/yudao-module-member-api/src/main/java/cn/iocoder/yudao/module/member/enums/MemberRecentAuditStatusEnum.java @@ -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); + } + +} diff --git a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/client/service/ThirdAuthServiceImpl.java b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/client/service/ThirdAuthServiceImpl.java index 61edd7f..be07a33 100644 --- a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/client/service/ThirdAuthServiceImpl.java +++ b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/client/service/ThirdAuthServiceImpl.java @@ -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; } } diff --git a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/client/vo/chat/user/ChatUserOnLineStatusRespVo.java b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/client/vo/chat/user/ChatUserOnLineStatusRespVo.java index 23da407..795032f 100644 --- a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/client/vo/chat/user/ChatUserOnLineStatusRespVo.java +++ b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/client/vo/chat/user/ChatUserOnLineStatusRespVo.java @@ -13,4 +13,7 @@ public class ChatUserOnLineStatusRespVo { @Schema(description = "是否在线") private Boolean online; + @Schema(description = "最后活跃时间,毫秒时间戳") + private Long lastActiveTime; + } diff --git a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/admin/recent/RecentController.java b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/admin/recent/RecentController.java index 8fe5413..e84ac01 100644 --- a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/admin/recent/RecentController.java +++ b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/admin/recent/RecentController.java @@ -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 auditRecent(@Valid @RequestBody RecentAuditReqVO auditReqVO) { + recentService.auditRecent(auditReqVO, getLoginUserId()); + return success(true); + } + + @PutMapping("/visible") + @Operation(summary = "更新动态展示状态") + @PreAuthorize("@ss.hasPermission('member:recent:update')") + public CommonResult 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)); } -} \ No newline at end of file +} diff --git a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/admin/recent/vo/RecentAuditReqVO.java b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/admin/recent/vo/RecentAuditReqVO.java new file mode 100644 index 0000000..82c3b83 --- /dev/null +++ b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/admin/recent/vo/RecentAuditReqVO.java @@ -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; + +} diff --git a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/admin/recent/vo/RecentPageReqVO.java b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/admin/recent/vo/RecentPageReqVO.java index 812e8db..f2889a6 100644 --- a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/admin/recent/vo/RecentPageReqVO.java +++ b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/admin/recent/vo/RecentPageReqVO.java @@ -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; -} \ No newline at end of file +} diff --git a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/admin/recent/vo/RecentRespVO.java b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/admin/recent/vo/RecentRespVO.java index 419127b..cd230d7 100644 --- a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/admin/recent/vo/RecentRespVO.java +++ b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/admin/recent/vo/RecentRespVO.java @@ -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; -} \ No newline at end of file +} diff --git a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/admin/recent/vo/RecentVisibleReqVO.java b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/admin/recent/vo/RecentVisibleReqVO.java new file mode 100644 index 0000000..bd930fc --- /dev/null +++ b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/admin/recent/vo/RecentVisibleReqVO.java @@ -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; + +} diff --git a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/app/auth/AppAuthController.java b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/app/auth/AppAuthController.java index 88d065e..338a4cd 100644 --- a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/app/auth/AppAuthController.java +++ b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/app/auth/AppAuthController.java @@ -69,6 +69,26 @@ public class AppAuthController { return success(authService.refreshToken(refreshToken)); } + @PostMapping("/mini-program-login-code") + @Operation(summary = "创建 H5 跳转微信小程序的一次性登录码") + public CommonResult createMiniProgramLoginCode() { + return success(authService.createMiniProgramLoginCode(getLoginUserId())); + } + + @PostMapping("/mini-program-login") + @Operation(summary = "使用一次性登录码登录微信小程序") + @Parameter(name = "code", description = "一次性登录码", required = true) + @PermitAll + public CommonResult loginByMiniProgramCode(@RequestParam("code") String code) { + return success(authService.loginByMiniProgramCode(code)); + } + + @PostMapping("/chat-token") + @Operation(summary = "使用当前登录态重新签发聊天令牌") + public CommonResult reissueChatToken() { + return success(new AppAuthChatTokenRespVO(authService.reissueChatToken(getLoginUserId()))); + } + // ========== 短信登录相关 ========== @PostMapping("/sms-register") diff --git a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/app/auth/vo/AppAuthChatTokenRespVO.java b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/app/auth/vo/AppAuthChatTokenRespVO.java new file mode 100644 index 0000000..4d23522 --- /dev/null +++ b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/app/auth/vo/AppAuthChatTokenRespVO.java @@ -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; + +} diff --git a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/app/recent/AppRecentController.java b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/app/recent/AppRecentController.java index 8e97750..6ad54eb 100644 --- a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/app/recent/AppRecentController.java +++ b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/app/recent/AppRecentController.java @@ -151,6 +151,17 @@ public class AppRecentController { return success(recentService.dynamicPage(dto)); } + @PostMapping("/update-and-resubmit") + @Operation(summary = "修改并重新提交朋友圈审核") + public CommonResult 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> evaluatePage(@Valid @RequestBody AppRecentLikeReqVo dto) { @@ -183,4 +194,4 @@ public class AppRecentController { return success(recentService.total(getLoginUserId())); } -} \ No newline at end of file +} diff --git a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/app/recent/vo/AppRecentLikeReqVo.java b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/app/recent/vo/AppRecentLikeReqVo.java index bde1799..a6ff12c 100644 --- a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/app/recent/vo/AppRecentLikeReqVo.java +++ b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/app/recent/vo/AppRecentLikeReqVo.java @@ -13,4 +13,7 @@ public class AppRecentLikeReqVo extends PageParam { @Schema(description = "操作id") private Long operationId; + @Schema(description = "审核状态:0-审核中,1-审核通过,2-审核不通过") + private Integer auditStatus; + } diff --git a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/app/recent/vo/AppRecentUpdateReqVO.java b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/app/recent/vo/AppRecentUpdateReqVO.java new file mode 100644 index 0000000..a00fac0 --- /dev/null +++ b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/app/recent/vo/AppRecentUpdateReqVO.java @@ -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; + +} diff --git a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/dal/dataobject/recent/RecentDO.java b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/dal/dataobject/recent/RecentDO.java index dce702a..0aa6cc6 100644 --- a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/dal/dataobject/recent/RecentDO.java +++ b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/dal/dataobject/recent/RecentDO.java @@ -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; -} \ No newline at end of file + /** + * 是否展示 + * + * 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; + +} diff --git a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/dal/mysql/user/MemberUserMapper.java b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/dal/mysql/user/MemberUserMapper.java index 5722d48..812593a 100644 --- a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/dal/mysql/user/MemberUserMapper.java +++ b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/dal/mysql/user/MemberUserMapper.java @@ -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 { .or().eq(MemberUserDO::getUserCode, ""))); } + default int updateAppRefreshDateIfEarlier(Long id, LocalDateTime appRefreshDate) { + return update(null, new LambdaUpdateWrapper() + .set(MemberUserDO::getAppRefreshDate, appRefreshDate) + .eq(MemberUserDO::getId, id) + .and(wrapper -> wrapper.isNull(MemberUserDO::getAppRefreshDate) + .or().lt(MemberUserDO::getAppRefreshDate, appRefreshDate))); + } + default List selectListByNicknameLike(String nickname) { return selectList(new LambdaQueryWrapperX() .likeIfPresent(MemberUserDO::getNickname, nickname)); diff --git a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/dal/redis/RedisKeyConstants.java b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/dal/redis/RedisKeyConstants.java index 491368d..faaeae0 100644 --- a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/dal/redis/RedisKeyConstants.java +++ b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/dal/redis/RedisKeyConstants.java @@ -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"; + } diff --git a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/dal/redis/auth/MemberAuthLoginCodeRedisDAO.java b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/dal/redis/auth/MemberAuthLoginCodeRedisDAO.java new file mode 100644 index 0000000..c04e8aa --- /dev/null +++ b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/dal/redis/auth/MemberAuthLoginCodeRedisDAO.java @@ -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); + } + +} diff --git a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/service/auth/MemberAuthService.java b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/service/auth/MemberAuthService.java index 97a890d..bf051be 100644 --- a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/service/auth/MemberAuthService.java +++ b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/service/auth/MemberAuthService.java @@ -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); } diff --git a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/service/auth/MemberAuthServiceImpl.java b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/service/auth/MemberAuthServiceImpl.java index 129b325..5032724 100644 --- a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/service/auth/MemberAuthServiceImpl.java +++ b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/service/auth/MemberAuthServiceImpl.java @@ -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()); diff --git a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/service/recent/RecentAuditService.java b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/service/recent/RecentAuditService.java new file mode 100644 index 0000000..ef585af --- /dev/null +++ b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/service/recent/RecentAuditService.java @@ -0,0 +1,15 @@ +package cn.iocoder.yudao.module.member.service.recent; + +/** + * 动态第三方审核 Service + */ +public interface RecentAuditService { + + /** + * 异步审核动态内容。 + * + * @param recentId 动态编号 + */ + void auditRecentAsync(Long recentId); + +} diff --git a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/service/recent/RecentAuditServiceImpl.java b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/service/recent/RecentAuditServiceImpl.java new file mode 100644 index 0000000..3ba25ca --- /dev/null +++ b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/service/recent/RecentAuditServiceImpl.java @@ -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> 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 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> tasks, String contentType, String content, + Function 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 auditContent(String contentType, String content, + Function 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 failureReasons) { + boolean approved = failureReasons.isEmpty(); + String auditReason = approved ? null : StringUtils.left(String.join(";", failureReasons), + AUDIT_REASON_MAX_LENGTH); + LambdaUpdateWrapper updateWrapper = new LambdaUpdateWrapper() + .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); + } + } + +} diff --git a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/service/recent/RecentService.java b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/service/recent/RecentService.java index f5c3ebc..f8eddb7 100644 --- a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/service/recent/RecentService.java +++ b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/service/recent/RecentService.java @@ -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 dynamicPage(@Valid AppRecentLikeReqVo dto); + /** + * 修改并重新提交朋友圈审核 + */ + void updateAndResubmitRecent(@Valid RecentSaveReqVO updateReqVO, Long memberId); + /** * 我的评价 */ @@ -123,4 +138,4 @@ public interface RecentService { * 朋友圈我喜欢的、喜欢我的等总数统计 */ OperationTotalGroupRecentRespVO total(Long userId); -} \ No newline at end of file +} diff --git a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/service/recent/RecentServiceImpl.java b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/service/recent/RecentServiceImpl.java index 78d5717..161eea6 100644 --- a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/service/recent/RecentServiceImpl.java +++ b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/service/recent/RecentServiceImpl.java @@ -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 textFuture = CompletableFuture.supplyAsync( - () -> auditCommaSeparatedContent(vo.getDescription(), thirdAuthService::textAuditSync) - ); - CompletableFuture 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 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 updateWrapper = new LambdaUpdateWrapper() + .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 recentPageList = recentMapper.pageJoin(new Page(bean.getPageNo(), bean.getPageSize()), resultQuery); @@ -492,8 +503,6 @@ public class RecentServiceImpl implements RecentService { List resultList = BeanUtils.toBean(recentPageList.getRecords(), RecentPageResVo.class); // 为RecentPageResVo设置Member信息 convertRecentPageResVoSetupMember(resultList, isVip); - // 远程查询用户是否在线 - convertRecentPageResVoSetupIsOnline(resultList); // 如果是`新人``女生`模糊`头像`和`用户名` List 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 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(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() + .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 list) { - if (ObjectUtil.isEmpty(list)) return; - List memberIds = list.stream().map(RecentPageResVo::getMemberId).collect(Collectors.toList()); - - ChatUserOnLineStatusReqVo dto = new ChatUserOnLineStatusReqVo(); - dto.setUserIdList(memberIds); - - List userOnlineList = chatApi.getUserOnlineStatusById(dto); - if (ObjectUtil.isEmpty(userOnlineList)) return; - Map 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信息 diff --git a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/service/user/BO/MemberExtBO.java b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/service/user/BO/MemberExtBO.java index ba68242..885a6be 100644 --- a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/service/user/BO/MemberExtBO.java +++ b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/service/user/BO/MemberExtBO.java @@ -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; diff --git a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/service/user/MemberUserServiceImpl.java b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/service/user/MemberUserServiceImpl.java index 93a2329..f90399c 100644 --- a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/service/user/MemberUserServiceImpl.java +++ b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/service/user/MemberUserServiceImpl.java @@ -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 memberUserDOS = memberUserMapper.selectBatchIds(ids); - return memberUserDOS.stream().map(member -> { + List 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 memberList) { + if (CollUtil.isEmpty(memberList)) { + return; + } + ChatUserOnLineStatusReqVo reqVO = new ChatUserOnLineStatusReqVo(); + reqVO.setUserIdList(memberList.stream().map(MemberExtBO::getMemberId).collect(Collectors.toList())); + List statusList = chatApi.getUserOnlineStatusById(reqVO); + if (CollUtil.isEmpty(statusList)) { + return; + } + Map 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 diff --git a/yudao-module-member/yudao-module-member-biz/src/main/resources/mapper/RecentMapper.xml b/yudao-module-member/yudao-module-member-biz/src/main/resources/mapper/RecentMapper.xml index ec77819..7c862ee 100644 --- a/yudao-module-member/yudao-module-member-biz/src/main/resources/mapper/RecentMapper.xml +++ b/yudao-module-member/yudao-module-member-biz/src/main/resources/mapper/RecentMapper.xml @@ -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 @@ and a.issue_address like concat('%', #{dto.issueAddress}, '%') + + and a.audit_status = #{dto.auditStatus} + + + and a.visible = #{dto.visible} + - and a.createTime between #{dto.createTime[0]} and #{dto.createTime[1]} + and a.create_time between #{dto.createTime[0]} and #{dto.createTime[1]} and a.member_id not in @@ -71,6 +83,9 @@ and u.sex = #{dto.memberSex} + + and u.nickname like concat('%', #{dto.memberNickname}, '%') + and exists (select 1 from member_recent_tag t diff --git a/yudao-module-member/yudao-module-member-biz/src/test/java/cn/iocoder/yudao/module/member/service/auth/MemberAuthServiceImplChatTokenTest.java b/yudao-module-member/yudao-module-member-biz/src/test/java/cn/iocoder/yudao/module/member/service/auth/MemberAuthServiceImplChatTokenTest.java new file mode 100644 index 0000000..307c166 --- /dev/null +++ b/yudao-module-member/yudao-module-member-biz/src/test/java/cn/iocoder/yudao/module/member/service/auth/MemberAuthServiceImplChatTokenTest.java @@ -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 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()); + } + +} diff --git a/yudao-module-member/yudao-module-member-biz/src/test/java/cn/iocoder/yudao/module/member/service/auth/MemberAuthServiceImplMiniProgramLoginTest.java b/yudao-module-member/yudao-module-member-biz/src/test/java/cn/iocoder/yudao/module/member/service/auth/MemberAuthServiceImplMiniProgramLoginTest.java new file mode 100644 index 0000000..faa3d52 --- /dev/null +++ b/yudao-module-member/yudao-module-member-biz/src/test/java/cn/iocoder/yudao/module/member/service/auth/MemberAuthServiceImplMiniProgramLoginTest.java @@ -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"); + } + +} diff --git a/yudao-module-member/yudao-module-member-biz/src/test/java/cn/iocoder/yudao/module/member/service/recent/RecentServiceImplTest.java b/yudao-module-member/yudao-module-member-biz/src/test/java/cn/iocoder/yudao/module/member/service/recent/RecentServiceImplTest.java new file mode 100644 index 0000000..c6fc7d5 --- /dev/null +++ b/yudao-module-member/yudao-module-member-biz/src/test/java/cn/iocoder/yudao/module/member/service/recent/RecentServiceImplTest.java @@ -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; + } + +} diff --git a/yudao-module-member/yudao-module-member-biz/src/test/java/cn/iocoder/yudao/module/member/service/user/MemberUserServiceImplChatActivityTest.java b/yudao-module-member/yudao-module-member-biz/src/test/java/cn/iocoder/yudao/module/member/service/user/MemberUserServiceImplChatActivityTest.java new file mode 100644 index 0000000..647b30d --- /dev/null +++ b/yudao-module-member/yudao-module-member-biz/src/test/java/cn/iocoder/yudao/module/member/service/user/MemberUserServiceImplChatActivityTest.java @@ -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 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 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 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; + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/social/SocialClientServiceImpl.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/social/SocialClientServiceImpl.java index 1cf43c3..5a07695 100644 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/social/SocialClientServiceImpl.java +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/social/SocialClientServiceImpl.java @@ -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); diff --git a/yudao-server/src/main/resources/application-dev.yaml b/yudao-server/src/main/resources/application-dev.yaml index b8c4bee..bced330 100644 --- a/yudao-server/src/main/resources/application-dev.yaml +++ b/yudao-server/src/main/resources/application-dev.yaml @@ -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