diff --git a/docs/清用户数据操作文档.md b/docs/清用户数据操作文档.md new file mode 100644 index 0000000..ed47f05 --- /dev/null +++ b/docs/清用户数据操作文档.md @@ -0,0 +1,283 @@ +# 清库操作文档:清空全部用户数据(含 IM) + +> 适用环境:荷塘月色业务后端 `friend_back` + IM 系统 `chat-im` +> 目标:清除**所有会员用户**相关数据(资料、会话、钱包/分销用户侧数据、IM 好友与聊天等) +> **不是**「注销单个用户」;也不是清空系统配置/管理员账号。 + +--- + +## 1. 先搞清楚有两套系统 + +| 系统 | 代码目录 | MySQL 库 | Redis | 文件 | +|------|----------|----------|-------|------| +| 业务 API | `friend_back` | **`chat_v1`** | **db 7** | `infra_file` + 对应对象存储(七牛/MinIO 等) | +| IM | `chat-im`(`im-platform` / `im-server`) | **`im_platform`** | **db 10** | MinIO bucket **`im-platform`** | + +- 会员 ID 对齐:`member_user.id` == `im_user.id` +- `friend_back` 只通过 HTTP 调 IM(`chat.base-url`),**没有「删除用户」的第三方 API**,IM 必须在 `im_platform` 库直接清 +- 特殊 IM 账号(**不要删**): + - `-100` 系统消息通知号 + - `-200` 客服号 + +生产配置参考: + +- 业务:`friend_back/yudao-server/src/main/resources/application-prod.yaml`(`chat_v1`、Redis db7、`chat.base-url`) +- IM:`chat-im/im-platform/src/main/resources/application-prod.yml`(`im_platform`、Redis db10) + +--- + +## 2. 操作前检查清单 + +1. **确认环境**(测试 / 预发 / 生产),禁止在未备份的生产库直接执行。 +2. **全量备份** + ```bash + mysqldump -h... -u... -p chat_v1 > chat_v1_backup_$(date +%Y%m%d).sql + mysqldump -h... -u... -p im_platform > im_platform_backup_$(date +%Y%m%d).sql + # Redis:按运维规范导出 RDB/AOF(业务 db7、IM db10) + # 对象存储:按需快照或确认可丢弃用户上传文件 + ``` +3. **停写流量**(强烈建议) + - 停或摘流:`yudao-server`、`im-platform`、`im-server` + - 避免删一半又有新注册/新消息写入 +4. **合规确认** + - 支付订单、退款、提现流水是否允许物理删除(默认建议**保留或脱敏归档**,见第 5 节) +5. **现有后台没有「一键清会员」接口**,只能运维 SQL + Redis + 存储清理。 + +--- + +## 3. 建议操作顺序(总览) + +```text +① 停服务 +② 备份 MySQL / Redis / 文件 +③ 清业务库 chat_v1(用户相关表) +④ 清 IM 库 im_platform(保留 -100/-200) +⑤ 清 Redis db7、db10 +⑥ 清用户上传文件(可选) +⑦ 启服务并验收 +``` + +--- + +## 4. 业务库 `chat_v1`:清用户数据 + +> 执行前先 `USE chat_v1;` +> 下面按「先子表、后主表」;若表不存在可跳过。 +> 全量重置测试环境可用 `TRUNCATE`;生产更建议 `DELETE` + 条件(如按 `tenant_id`)。 + +### 4.1 会员扩展与行为(必清) + +```sql +-- 相册 / 位置 / 动态相关 +TRUNCATE TABLE member_photo; +TRUNCATE TABLE member_location; +TRUNCATE TABLE member_recent; +TRUNCATE TABLE member_operation; +TRUNCATE TABLE member_view_record; +TRUNCATE TABLE member_recent_tag; +TRUNCATE TABLE member_recent_cfg; + +-- 认证 / 反馈 / 开通记录 / 积分经验签到 +TRUNCATE TABLE member_com_auth; +TRUNCATE TABLE member_feedback; +TRUNCATE TABLE member_open_type_user; +TRUNCATE TABLE member_point_record; +TRUNCATE TABLE member_level_record; +TRUNCATE TABLE member_experience_record; +TRUNCATE TABLE member_sign_in_record; +TRUNCATE TABLE member_address; +``` + +### 4.2 登录态 / 社交绑定(会员侧) + +```sql +-- user_type = 1 表示会员(Member) +DELETE FROM system_oauth2_access_token WHERE user_type = 1; +DELETE FROM system_oauth2_refresh_token WHERE user_type = 1; +DELETE FROM system_oauth2_approve WHERE user_type = 1; +DELETE FROM system_oauth2_code WHERE user_type = 1; + +DELETE FROM system_social_user_bind WHERE user_type = 1; +-- 可选:清理已无绑定的 social_user 孤儿数据(按你们库实际字段调整) + +DELETE FROM system_login_log WHERE user_type = 1; +DELETE FROM system_notify_message WHERE user_type = 1; +-- 短信验证码可按需清空 +TRUNCATE TABLE system_sms_code; +``` + +### 4.3 分销用户数据(必清用户侧) + +```sql +TRUNCATE TABLE trade_brokerage_record; +TRUNCATE TABLE trade_brokerage_withdraw; +TRUNCATE TABLE trade_brokerage_code; +TRUNCATE TABLE trade_brokerage_user; +``` + +### 4.4 钱包用户数据(必清用户侧) + +```sql +TRUNCATE TABLE pay_wallet_transaction; +TRUNCATE TABLE pay_wallet_recharge; +TRUNCATE TABLE pay_wallet_user_transition; -- 若存在 +TRUNCATE TABLE pay_wallet_tx_auth; -- 若存在 +DELETE FROM pay_wallet WHERE user_type = 1; +``` + +### 4.5 商城用户行为(若启用 mall) + +```sql +TRUNCATE TABLE trade_cart; +TRUNCATE TABLE trade_order_log; +TRUNCATE TABLE trade_order_item; +TRUNCATE TABLE trade_after_sale_log; +TRUNCATE TABLE trade_after_sale; +TRUNCATE TABLE trade_order; + +TRUNCATE TABLE product_browse_history; +TRUNCATE TABLE product_favorite; +TRUNCATE TABLE product_comment; +-- 优惠券领取记录等带 user_id 的 promotion_* 表按实际启用情况清理 +``` + +### 4.6 最后删会员主表 + +```sql +TRUNCATE TABLE member_user; +-- 或:DELETE FROM member_user; +``` + +### 4.7 业务库「不要清」的配置表(示例) + +保留系统能继续跑的配置: + +- `member_config`、`member_level`、`member_tag`、`member_group` +- `member_sign_in_config`、`member_open_type`(会员套餐) +- `pay_app`、`pay_channel`、`pay_wallet_recharge_package` +- `trade_config` +- `system_oauth2_client`、`system_social_client`、短信模板/渠道 +- `system_users` 等**后台管理员**账号(除非连运营账号一起重置) + +--- + +## 5. 支付流水是否删除(单独决策) + +| 策略 | 处理 | +|------|------| +| **推荐(测试可删、生产慎删)** | 保留 `pay_order` / `pay_order_extension` / `pay_refund` / `pay_transfer` / `pay_notify_*`,仅清用户侧钱包与会员 | +| **测试环境彻底重置** | 可一并 `TRUNCATE` 上述支付流水表,并确认与渠道对账无冲突 | + +若生产必须销毁支付痕迹,需法务/财务确认后另出工单,不建议与「清用户」混为一谈。 + +--- + +## 6. IM 库 `im_platform`:清 IM 数据 + +> 执行前先 `USE im_platform;` +> 建表参考:`chat-im/db/im-platform.sql` + +```sql +-- 消息与关系 +TRUNCATE TABLE im_private_message; +TRUNCATE TABLE im_group_message; +TRUNCATE TABLE im_friend; +TRUNCATE TABLE im_group_member; +TRUNCATE TABLE im_group; +TRUNCATE TABLE im_file_info; + +-- 用户:保留系统号 +DELETE FROM im_user WHERE id NOT IN (-100, -200); + +-- 敏感词库一般保留 +-- TRUNCATE TABLE im_sensitive_word; -- 仅当要重置词库时 +``` + +**说明:** + +- 不要依赖 `friend_back` 的 ChatApi 删用户(接口不支持) +- 清完后会员重新登录会走同步逻辑(`syncChatUser`)在 IM 侧重建用户 + +--- + +## 7. Redis + +**务必指定 DB,禁止误 FLUSH 其它库。** + +```bash +# 业务 Redis(application-prod: database 7) +redis-cli -n 7 FLUSHDB + +# IM Redis(im-platform/im-server: database 10) +redis-cli -n 10 FLUSHDB +``` + +若不能整库清空,至少清理: + +- 业务:`oauth2_access_token:*`、会员相关 lock/cache、`social_auth_state:*` 等 +- IM:`im:user:*`、`im:cache:*`、`im:message:*`、`im:readed:*` 等 + +--- + +## 8. 文件存储(可选) + +| 来源 | 处理建议 | +|------|----------| +| 业务头像/相册/认证图 | 根据 `infra_file` + `member_user.avatar` / `member_photo` 对应对象存储清理;测试环境可按配置整桶策略清理 | +| IM 图片视频 | MinIO bucket `im-platform`;清 `im_file_info` 后按 path 删对象,或测试环境整桶清理 | + +生产环境删除对象存储前务必确认无合规留存要求。 + +--- + +## 9. 启动与验收 + +1. 启动 `im-platform` → `im-server` → `yudao-server` +2. 检查: + ```sql + SELECT COUNT(*) FROM chat_v1.member_user; -- 期望 0 + SELECT COUNT(*) FROM im_platform.im_user + WHERE id NOT IN (-100, -200); -- 期望 0 + SELECT id, user_name FROM im_platform.im_user; -- 应还能看到 -100/-200 + ``` +3. App 侧: + - 旧 token 应失效,需重新注册/登录 + - 登录后 IM 可正常建链、发消息 + - 后台管理账号仍可登录(若未清 `system_users`) +4. 配置仍在:会员套餐、支付渠道、分销开关、敏感词等 + +--- + +## 10. 回滚 + +仅能依赖操作前备份: + +```bash +mysql -h... -u... -p chat_v1 < chat_v1_backup_YYYYMMDD.sql +mysql -h... -u... -p im_platform < im_platform_backup_YYYYMMDD.sql +# 恢复 Redis / 文件快照后重启服务 +``` + +--- + +## 11. 常见问题 + +**Q:只清 `member_user` 行不行?** +不行。登录 token、IM 消息、钱包、分销、相册等都会残留,且可能脏数据关联异常。 + +**Q:清完业务库,IM 会不会自动空?** +不会。必须单独清 `im_platform`(和 Redis db10)。 + +**Q:有没有管理后台按钮?** +没有会员「注销/全量删除」接口;管理员删除只作用于后台 `system_users`。 + +**Q:测试环境最快怎么干?** +停服务 → 备份 → 按第 4~7 节 TRUNCATE/DELETE + `FLUSHDB` → 启服务。 + +--- + +## 12. 变更记录 + +| 日期 | 说明 | +|------|------| +| 2026-08-12 | 初版:基于当前 `friend_back` + `chat-im` 配置与表结构整理 | diff --git a/sql/mysql/member/member_interaction.sql b/sql/mysql/member/member_interaction.sql new file mode 100644 index 0000000..7699028 --- /dev/null +++ b/sql/mysql/member/member_interaction.sql @@ -0,0 +1,18 @@ +-- 会员互动消息(被查看 / 微信被解锁) +CREATE TABLE IF NOT EXISTS `member_interaction` ( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT '编号', + `receiver_id` bigint NOT NULL COMMENT '接收人(被查看 / 被解锁用户)', + `actor_id` bigint NOT NULL COMMENT '操作人', + `biz_type` varchar(32) NOT NULL COMMENT 'VIEWED / WECHAT_UNLOCKED', + `ref_id` bigint NULL DEFAULT NULL COMMENT '关联业务 id(访客记录 / 操作记录)', + `read_status` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否已读', + `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号', + `creator` varchar(64) NULL DEFAULT '' COMMENT '创建者', + `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updater` varchar(64) 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 '是否删除', + PRIMARY KEY (`id`) USING BTREE, + KEY `idx_receiver_read` (`receiver_id`, `read_status`) USING BTREE, + KEY `idx_receiver_time` (`receiver_id`, `id`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='会员互动消息'; diff --git a/sql/mysql/member/member_push_device.sql b/sql/mysql/member/member_push_device.sql new file mode 100644 index 0000000..2a181d4 --- /dev/null +++ b/sql/mysql/member/member_push_device.sql @@ -0,0 +1,18 @@ +-- 会员推送设备(极光 RegistrationID 绑定) +CREATE TABLE IF NOT EXISTS `member_push_device` ( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT '编号', + `member_id` bigint NOT NULL COMMENT '会员编号', + `registration_id` varchar(64) NOT NULL COMMENT '极光 RegistrationID', + `alias` varchar(64) NULL DEFAULT NULL COMMENT '极光别名,如 u{memberId}', + `platform` varchar(16) NOT NULL COMMENT 'ios / android', + `provider` varchar(16) NOT NULL DEFAULT 'jpush' COMMENT '推送服务商', + `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号', + `creator` varchar(64) NULL DEFAULT '' COMMENT '创建者', + `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updater` varchar(64) 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 '是否删除', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE KEY `uk_registration_id` (`registration_id`) USING BTREE, + KEY `idx_member_id` (`member_id`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='会员推送设备'; diff --git a/yudao-module-member/yudao-module-member-api/src/main/java/cn/iocoder/yudao/module/member/enums/interaction/MemberInteractionBizTypeEnum.java b/yudao-module-member/yudao-module-member-api/src/main/java/cn/iocoder/yudao/module/member/enums/interaction/MemberInteractionBizTypeEnum.java new file mode 100644 index 0000000..372b95b --- /dev/null +++ b/yudao-module-member/yudao-module-member-api/src/main/java/cn/iocoder/yudao/module/member/enums/interaction/MemberInteractionBizTypeEnum.java @@ -0,0 +1,19 @@ +package cn.iocoder.yudao.module.member.enums.interaction; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 互动消息类型 + */ +@Getter +@AllArgsConstructor +public enum MemberInteractionBizTypeEnum { + + VIEWED("VIEWED", "查看了你"), + WECHAT_UNLOCKED("WECHAT_UNLOCKED", "解锁了你的微信"); + + private final String type; + private final String contentSuffix; + +} diff --git a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/app/comauth/vo/CreateWechatAuthReqVo.java b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/app/comauth/vo/CreateWechatAuthReqVo.java index 0ebdf59..f25cfab 100644 --- a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/app/comauth/vo/CreateWechatAuthReqVo.java +++ b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/app/comauth/vo/CreateWechatAuthReqVo.java @@ -19,8 +19,7 @@ public class CreateWechatAuthReqVo { @NotEmpty(message = "微信号不能为空") private String wechatNumber; - @Schema(description = "微信二维码") - @NotEmpty(message = "微信二维码不能为空") + @Schema(description = "微信二维码(选填)") private String wechatQrcode; } diff --git a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/app/interaction/AppMemberInteractionController.java b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/app/interaction/AppMemberInteractionController.java new file mode 100644 index 0000000..3001b3c --- /dev/null +++ b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/app/interaction/AppMemberInteractionController.java @@ -0,0 +1,51 @@ +package cn.iocoder.yudao.module.member.controller.app.interaction; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.member.controller.app.interaction.vo.AppInteractionRespVO; +import cn.iocoder.yudao.module.member.controller.app.interaction.vo.AppInteractionSummaryRespVO; +import cn.iocoder.yudao.module.member.service.interaction.MemberInteractionService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import javax.annotation.Resource; +import javax.validation.Valid; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; + +@Tag(name = "用户 App - 互动消息") +@RestController +@RequestMapping("/member/interaction") +@Validated +public class AppMemberInteractionController { + + @Resource + private MemberInteractionService interactionService; + + @GetMapping("/summary") + @Operation(summary = "互动消息摘要(未读数 + 最近一条)") + public CommonResult getSummary() { + return success(interactionService.getSummary(getLoginUserId())); + } + + @GetMapping("/page") + @Operation(summary = "互动消息分页") + public CommonResult> getPage(@Valid PageParam pageParam) { + return success(interactionService.getPage(getLoginUserId(), pageParam)); + } + + @PostMapping("/read-all") + @Operation(summary = "全部标为已读") + public CommonResult readAll() { + interactionService.readAll(getLoginUserId()); + return success(true); + } + +} diff --git a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/app/interaction/vo/AppInteractionRespVO.java b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/app/interaction/vo/AppInteractionRespVO.java new file mode 100644 index 0000000..e8b4d15 --- /dev/null +++ b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/app/interaction/vo/AppInteractionRespVO.java @@ -0,0 +1,36 @@ +package cn.iocoder.yudao.module.member.controller.app.interaction.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +@Schema(description = "用户 App - 互动消息 Response VO") +@Data +public class AppInteractionRespVO { + + @Schema(description = "编号") + private Long id; + + @Schema(description = "类型:VIEWED / WECHAT_UNLOCKED") + private String bizType; + + @Schema(description = "文案,如「张三 查看了你」") + private String content; + + @Schema(description = "操作人编号") + private Long actorId; + + @Schema(description = "操作人头像") + private String actorAvatar; + + @Schema(description = "操作人昵称") + private String actorNickname; + + @Schema(description = "是否已读") + private Boolean readStatus; + + @Schema(description = "创建时间") + private LocalDateTime createTime; + +} diff --git a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/app/interaction/vo/AppInteractionSummaryRespVO.java b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/app/interaction/vo/AppInteractionSummaryRespVO.java new file mode 100644 index 0000000..93443a8 --- /dev/null +++ b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/app/interaction/vo/AppInteractionSummaryRespVO.java @@ -0,0 +1,21 @@ +package cn.iocoder.yudao.module.member.controller.app.interaction.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +@Schema(description = "用户 App - 互动消息摘要 Response VO") +@Data +public class AppInteractionSummaryRespVO { + + @Schema(description = "未读数") + private Long unreadCount; + + @Schema(description = "最近一条文案") + private String lastContent; + + @Schema(description = "最近一条时间") + private LocalDateTime lastTime; + +} diff --git a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/dal/dataobject/interaction/MemberInteractionDO.java b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/dal/dataobject/interaction/MemberInteractionDO.java new file mode 100644 index 0000000..0814fd4 --- /dev/null +++ b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/dal/dataobject/interaction/MemberInteractionDO.java @@ -0,0 +1,50 @@ +package cn.iocoder.yudao.module.member.dal.dataobject.interaction; + +import cn.iocoder.yudao.framework.tenant.core.db.TenantBaseDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.*; + +/** + * 会员互动消息 DO + */ +@TableName("member_interaction") +@KeySequence("member_interaction_seq") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class MemberInteractionDO extends TenantBaseDO { + + @TableId + private Long id; + + /** + * 接收人(被查看 / 被解锁) + */ + private Long receiverId; + + /** + * 操作人 + */ + private Long actorId; + + /** + * VIEWED / WECHAT_UNLOCKED + */ + private String bizType; + + /** + * 关联业务 id + */ + private Long refId; + + /** + * 是否已读 + */ + private Boolean readStatus; + +} diff --git a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/dal/mysql/interaction/MemberInteractionMapper.java b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/dal/mysql/interaction/MemberInteractionMapper.java new file mode 100644 index 0000000..c59afe5 --- /dev/null +++ b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/dal/mysql/interaction/MemberInteractionMapper.java @@ -0,0 +1,40 @@ +package cn.iocoder.yudao.module.member.dal.mysql.interaction; + +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.module.member.dal.dataobject.interaction.MemberInteractionDO; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface MemberInteractionMapper extends BaseMapperX { + + default PageResult selectPageByReceiver(Long receiverId, PageParam pageParam) { + return selectPage(pageParam, new LambdaQueryWrapperX() + .eq(MemberInteractionDO::getReceiverId, receiverId) + .orderByDesc(MemberInteractionDO::getId)); + } + + default Long selectUnreadCount(Long receiverId) { + return selectCount(new LambdaQueryWrapperX() + .eq(MemberInteractionDO::getReceiverId, receiverId) + .eq(MemberInteractionDO::getReadStatus, false)); + } + + default MemberInteractionDO selectLatest(Long receiverId) { + return selectOne(new LambdaQueryWrapperX() + .eq(MemberInteractionDO::getReceiverId, receiverId) + .orderByDesc(MemberInteractionDO::getId) + .last("LIMIT 1")); + } + + default int markAllRead(Long receiverId) { + return update(null, new LambdaUpdateWrapper() + .set(MemberInteractionDO::getReadStatus, true) + .eq(MemberInteractionDO::getReceiverId, receiverId) + .eq(MemberInteractionDO::getReadStatus, false)); + } + +} 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 812593a..fa4105d 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 @@ -27,6 +27,24 @@ public interface MemberUserMapper extends BaseMapperX { return selectOne(MemberUserDO::getMobile, mobile); } + /** + * 按微信号查其他用户(用于唯一性校验,排除指定会员) + */ + default MemberUserDO selectByWechatNumExcludeId(String wechatNum, Long excludeMemberId) { + return selectOne(new LambdaQueryWrapperX() + .eq(MemberUserDO::getWechatNum, wechatNum) + .neIfPresent(MemberUserDO::getId, excludeMemberId)); + } + + /** + * 按微信号查其他用户(用于唯一性校验,排除本人) + */ + default MemberUserDO selectByWechatNumExcludeId(String wechatNum, Long excludeMemberId) { + return selectOne(new LambdaQueryWrapperX() + .eq(MemberUserDO::getWechatNum, wechatNum) + .ne(excludeMemberId != null, MemberUserDO::getId, excludeMemberId)); + } + default List selectIdListByEmptyUserCode(int limit) { return selectList(new LambdaQueryWrapperX() .select(MemberUserDO::getId) diff --git a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/service/comauth/ComAuthServiceImpl.java b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/service/comauth/ComAuthServiceImpl.java index 2014083..ece879f 100644 --- a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/service/comauth/ComAuthServiceImpl.java +++ b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/service/comauth/ComAuthServiceImpl.java @@ -5,6 +5,7 @@ import cn.hutool.core.collection.IterUtil; import cn.hutool.core.date.DatePattern; import cn.hutool.core.date.LocalDateTimeUtil; import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; import cn.hutool.extra.spring.SpringUtil; import cn.hutool.json.JSONUtil; import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; @@ -149,34 +150,39 @@ public class ComAuthServiceImpl implements ComAuthService { } /** - * 创建微信认证 + * 创建微信认证:校验微信号未被他人占用后自动审核通过并落库,不再走人工审核。 */ @Override @Transactional(rollbackFor = Exception.class) public ComAuthDO createWechatAuth(CreateWechatAuthReqVo dto) { + String wechatNumber = StrUtil.trim(dto.getWechatNumber()); + if (StrUtil.isBlank(wechatNumber)) { + throw exception(MemberErrorCode.EMPTY, "微信号不能为空"); + } + dto.setWechatNumber(wechatNumber); + + // 查重:其他用户已占用则直接拒绝 + MemberUserDO occupied = memberUserMapper.selectByWechatNumExcludeId(wechatNumber, dto.getMemberId()); + if (ObjectUtil.isNotEmpty(occupied)) { + throw exception(MemberErrorCode.EMPTY, "请勿填写重复的微信号"); + } ComAuthDO.WechatAuthExtra extraObj = new ComAuthDO.WechatAuthExtra(); - extraObj.setWechatNumber(dto.getWechatNumber()); + extraObj.setWechatNumber(wechatNumber); extraObj.setWechatQrcode(dto.getWechatQrcode()); ComAuthDO saveObj = new ComAuthDO(); saveObj.setAuthMemberId(dto.getMemberId()); saveObj.setBizType(MemberComAuthEnum.WECHAT_NUMBER.getType()); - saveObj.setAuthStatus(MemberComAuthStatusEnum.WAIT_AUTH.getType()); + // 无重复则自动通过,不再进入人工审核队列 + saveObj.setAuthStatus(MemberComAuthStatusEnum.CHECK_PASS.getType()); + saveObj.setAuthTime(LocalDateTime.now()); saveObj.setAuthPic(dto.getWechatQrcode()); saveObj.setAuthExtraJson(JSONUtil.toJsonStr(extraObj)); authMapper.insert(saveObj); ComAuthDO detail = authMapper.selectById(saveObj.getId()); - - // 查询配置 - MemberConfigDO config = memberConfigService.getConfig(); - if (ObjectUtil.isNotEmpty(config) && ObjectUtil.isNotEmpty(config.getAutoWechatNumAuth())) { - if (config.getAutoWechatNumAuth()) { - getSelf().wechatVerify(detail); - } - } - + getSelf().wechatVerify(detail); return detail; } diff --git a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/service/interaction/MemberInteractionService.java b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/service/interaction/MemberInteractionService.java new file mode 100644 index 0000000..9635145 --- /dev/null +++ b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/service/interaction/MemberInteractionService.java @@ -0,0 +1,22 @@ +package cn.iocoder.yudao.module.member.service.interaction; + +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.member.controller.app.interaction.vo.AppInteractionRespVO; +import cn.iocoder.yudao.module.member.controller.app.interaction.vo.AppInteractionSummaryRespVO; +import cn.iocoder.yudao.module.member.enums.interaction.MemberInteractionBizTypeEnum; + +public interface MemberInteractionService { + + /** + * 写入一条互动消息(接收人=被操作方) + */ + void create(Long receiverId, Long actorId, MemberInteractionBizTypeEnum bizType, Long refId); + + AppInteractionSummaryRespVO getSummary(Long receiverId); + + PageResult getPage(Long receiverId, PageParam pageParam); + + void readAll(Long receiverId); + +} diff --git a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/service/interaction/MemberInteractionServiceImpl.java b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/service/interaction/MemberInteractionServiceImpl.java new file mode 100644 index 0000000..b7cb5b2 --- /dev/null +++ b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/service/interaction/MemberInteractionServiceImpl.java @@ -0,0 +1,126 @@ +package cn.iocoder.yudao.module.member.service.interaction; + +import cn.hutool.core.collection.IterUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.member.controller.app.interaction.vo.AppInteractionRespVO; +import cn.iocoder.yudao.module.member.controller.app.interaction.vo.AppInteractionSummaryRespVO; +import cn.iocoder.yudao.module.member.dal.dataobject.interaction.MemberInteractionDO; +import cn.iocoder.yudao.module.member.dal.mysql.interaction.MemberInteractionMapper; +import cn.iocoder.yudao.module.member.enums.interaction.MemberInteractionBizTypeEnum; +import cn.iocoder.yudao.module.member.service.user.BO.MemberExtBO; +import cn.iocoder.yudao.module.member.service.user.MemberUserService; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +@Service +@Validated +public class MemberInteractionServiceImpl implements MemberInteractionService { + + @Resource + private MemberInteractionMapper interactionMapper; + @Resource + private MemberUserService memberUserService; + + @Override + public void create(Long receiverId, Long actorId, MemberInteractionBizTypeEnum bizType, Long refId) { + if (receiverId == null || actorId == null || bizType == null) { + return; + } + if (ObjectUtil.equals(receiverId, actorId)) { + return; + } + MemberInteractionDO row = MemberInteractionDO.builder() + .receiverId(receiverId) + .actorId(actorId) + .bizType(bizType.getType()) + .refId(refId) + .readStatus(false) + .build(); + interactionMapper.insert(row); + } + + @Override + public AppInteractionSummaryRespVO getSummary(Long receiverId) { + AppInteractionSummaryRespVO vo = new AppInteractionSummaryRespVO(); + Long unread = interactionMapper.selectUnreadCount(receiverId); + vo.setUnreadCount(unread == null ? 0L : unread); + MemberInteractionDO latest = interactionMapper.selectLatest(receiverId); + if (latest == null) { + vo.setLastContent(""); + return vo; + } + List filled = fillActors(Collections.singletonList(latest)); + AppInteractionRespVO first = filled.isEmpty() ? null : filled.get(0); + vo.setLastContent(first != null ? first.getContent() : ""); + vo.setLastTime(latest.getCreateTime()); + return vo; + } + + @Override + public PageResult getPage(Long receiverId, PageParam pageParam) { + PageResult page = interactionMapper.selectPageByReceiver(receiverId, pageParam); + if (ObjectUtil.isEmpty(page.getList())) { + return PageResult.empty(page.getTotal()); + } + List list = fillActors(page.getList()); + return new PageResult<>(list, page.getTotal()); + } + + @Override + public void readAll(Long receiverId) { + interactionMapper.markAllRead(receiverId); + } + + private List fillActors(List rows) { + if (ObjectUtil.isEmpty(rows)) { + return Collections.emptyList(); + } + List actorIds = rows.stream().map(MemberInteractionDO::getActorId).distinct().collect(Collectors.toList()); + List members = memberUserService.getUserExtList(actorIds); + Map memberMap = IterUtil.toMap(members, MemberExtBO::getMemberId); + + return rows.stream().map(row -> { + AppInteractionRespVO vo = new AppInteractionRespVO(); + vo.setId(row.getId()); + vo.setBizType(row.getBizType()); + vo.setActorId(row.getActorId()); + vo.setReadStatus(Boolean.TRUE.equals(row.getReadStatus())); + vo.setCreateTime(row.getCreateTime()); + MemberExtBO actor = memberMap.get(row.getActorId()); + String nickname = "用户"; + if (actor != null) { + vo.setActorAvatar(actor.getMemberAvatar()); + if (StrUtil.isNotBlank(actor.getMemberNickname())) { + nickname = actor.getMemberNickname(); + } + vo.setActorNickname(nickname); + } else { + vo.setActorNickname(nickname); + } + vo.setContent(buildContent(nickname, row.getBizType())); + return vo; + }).collect(Collectors.toList()); + } + + private String buildContent(String nickname, String bizType) { + MemberInteractionBizTypeEnum type = Arrays.stream(MemberInteractionBizTypeEnum.values()) + .filter(e -> ObjectUtil.equals(e.getType(), bizType)) + .findFirst() + .orElse(null); + if (type == null) { + return nickname + " 与你产生了互动"; + } + return nickname + " " + type.getContentSuffix(); + } + +} 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 161eea6..6809428 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 @@ -25,8 +25,10 @@ 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.interaction.MemberInteractionBizTypeEnum; 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.interaction.MemberInteractionService; import cn.iocoder.yudao.module.member.service.location.LocationBO; import cn.iocoder.yudao.module.member.service.location.LocationService; import cn.iocoder.yudao.module.member.service.opentype.OpenTypeService; @@ -105,6 +107,8 @@ public class RecentServiceImpl implements RecentService { private TxMapApi txMapApi; @Resource private ChatApi chatApi; + @Resource + private MemberInteractionService memberInteractionService; /** * 创建朋友圈 @@ -402,6 +406,13 @@ public class RecentServiceImpl implements RecentService { operationObj.setBizType(MemberRecentOperateEnum.MEMBER_LOOK_WECHAT.getType()); operationObj.setTapTime(LocalDateTime.now()); operationMapper.insert(operationObj); + // 首次解锁:通知被解锁用户 + memberInteractionService.create( + memberId, + operateMemberId, + MemberInteractionBizTypeEnum.WECHAT_UNLOCKED, + operationObj.getId() + ); return memberWechat; } diff --git a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/service/recent/ViewRecordServiceImpl.java b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/service/recent/ViewRecordServiceImpl.java index 0b1d71a..5456f13 100644 --- a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/service/recent/ViewRecordServiceImpl.java +++ b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/service/recent/ViewRecordServiceImpl.java @@ -24,6 +24,8 @@ import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.util.object.BeanUtils; import cn.iocoder.yudao.module.member.dal.mysql.viewrecord.ViewRecordMapper; +import cn.iocoder.yudao.module.member.enums.interaction.MemberInteractionBizTypeEnum; +import cn.iocoder.yudao.module.member.service.interaction.MemberInteractionService; import java.util.List; import java.util.Map; @@ -42,6 +44,8 @@ public class ViewRecordServiceImpl implements ViewRecordService { private ViewRecordMapper viewRecordMapper; @Resource private MemberUserService memberUserService; + @Resource + private MemberInteractionService memberInteractionService; /** * 创建用户查看记录 @@ -56,6 +60,19 @@ public class ViewRecordServiceImpl implements ViewRecordService { viewRecord.setTitle("访问朋友圈"); } viewRecordMapper.insert(viewRecord); + + // 查看用户主页:给被查看方写入互动消息 + if (ObjectUtil.equals(viewRecord.getBizType(), MemberRecentViewRecordEnum.USER.getType()) + && viewRecord.getBizId() != null + && viewRecord.getMemberId() != null + && ObjectUtil.notEqual(viewRecord.getBizId(), viewRecord.getMemberId())) { + memberInteractionService.create( + viewRecord.getBizId(), + viewRecord.getMemberId(), + MemberInteractionBizTypeEnum.VIEWED, + viewRecord.getId() + ); + } return viewRecord.getId(); }