微信号问题修改
This commit is contained in:
1 parent
d473d5d04a
commit
b029b08cee
16 files changed
+749
-14
No files matched your search
@@ -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` 配置与表结构整理 |
|
||||
@@ -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='会员互动消息';
|
||||
@@ -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='会员推送设备';
|
||||
+19
@@ -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;
|
||||
|
||||
}
|
||||
+1
-2
@@ -19,8 +19,7 @@ public class CreateWechatAuthReqVo {
|
||||
@NotEmpty(message = "微信号不能为空")
|
||||
private String wechatNumber;
|
||||
|
||||
@Schema(description = "微信二维码")
|
||||
@NotEmpty(message = "微信二维码不能为空")
|
||||
@Schema(description = "微信二维码(选填)")
|
||||
private String wechatQrcode;
|
||||
|
||||
}
|
||||
+51
@@ -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<AppInteractionSummaryRespVO> getSummary() {
|
||||
return success(interactionService.getSummary(getLoginUserId()));
|
||||
}
|
||||
|
||||
@GetMapping("/page")
|
||||
@Operation(summary = "互动消息分页")
|
||||
public CommonResult<PageResult<AppInteractionRespVO>> getPage(@Valid PageParam pageParam) {
|
||||
return success(interactionService.getPage(getLoginUserId(), pageParam));
|
||||
}
|
||||
|
||||
@PostMapping("/read-all")
|
||||
@Operation(summary = "全部标为已读")
|
||||
public CommonResult<Boolean> readAll() {
|
||||
interactionService.readAll(getLoginUserId());
|
||||
return success(true);
|
||||
}
|
||||
|
||||
}
|
||||
+36
@@ -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;
|
||||
|
||||
}
|
||||
+21
@@ -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;
|
||||
|
||||
}
|
||||
+50
@@ -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;
|
||||
|
||||
}
|
||||
+40
@@ -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<MemberInteractionDO> {
|
||||
|
||||
default PageResult<MemberInteractionDO> selectPageByReceiver(Long receiverId, PageParam pageParam) {
|
||||
return selectPage(pageParam, new LambdaQueryWrapperX<MemberInteractionDO>()
|
||||
.eq(MemberInteractionDO::getReceiverId, receiverId)
|
||||
.orderByDesc(MemberInteractionDO::getId));
|
||||
}
|
||||
|
||||
default Long selectUnreadCount(Long receiverId) {
|
||||
return selectCount(new LambdaQueryWrapperX<MemberInteractionDO>()
|
||||
.eq(MemberInteractionDO::getReceiverId, receiverId)
|
||||
.eq(MemberInteractionDO::getReadStatus, false));
|
||||
}
|
||||
|
||||
default MemberInteractionDO selectLatest(Long receiverId) {
|
||||
return selectOne(new LambdaQueryWrapperX<MemberInteractionDO>()
|
||||
.eq(MemberInteractionDO::getReceiverId, receiverId)
|
||||
.orderByDesc(MemberInteractionDO::getId)
|
||||
.last("LIMIT 1"));
|
||||
}
|
||||
|
||||
default int markAllRead(Long receiverId) {
|
||||
return update(null, new LambdaUpdateWrapper<MemberInteractionDO>()
|
||||
.set(MemberInteractionDO::getReadStatus, true)
|
||||
.eq(MemberInteractionDO::getReceiverId, receiverId)
|
||||
.eq(MemberInteractionDO::getReadStatus, false));
|
||||
}
|
||||
|
||||
}
|
||||
+18
@@ -27,6 +27,24 @@ public interface MemberUserMapper extends BaseMapperX<MemberUserDO> {
|
||||
return selectOne(MemberUserDO::getMobile, mobile);
|
||||
}
|
||||
|
||||
/**
|
||||
* 按微信号查其他用户(用于唯一性校验,排除指定会员)
|
||||
*/
|
||||
default MemberUserDO selectByWechatNumExcludeId(String wechatNum, Long excludeMemberId) {
|
||||
return selectOne(new LambdaQueryWrapperX<MemberUserDO>()
|
||||
.eq(MemberUserDO::getWechatNum, wechatNum)
|
||||
.neIfPresent(MemberUserDO::getId, excludeMemberId));
|
||||
}
|
||||
|
||||
/**
|
||||
* 按微信号查其他用户(用于唯一性校验,排除本人)
|
||||
*/
|
||||
default MemberUserDO selectByWechatNumExcludeId(String wechatNum, Long excludeMemberId) {
|
||||
return selectOne(new LambdaQueryWrapperX<MemberUserDO>()
|
||||
.eq(MemberUserDO::getWechatNum, wechatNum)
|
||||
.ne(excludeMemberId != null, MemberUserDO::getId, excludeMemberId));
|
||||
}
|
||||
|
||||
default List<Long> selectIdListByEmptyUserCode(int limit) {
|
||||
return selectList(new LambdaQueryWrapperX<MemberUserDO>()
|
||||
.select(MemberUserDO::getId)
|
||||
|
||||
+18
-12
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
+22
@@ -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<AppInteractionRespVO> getPage(Long receiverId, PageParam pageParam);
|
||||
|
||||
void readAll(Long receiverId);
|
||||
|
||||
}
|
||||
+126
@@ -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<AppInteractionRespVO> 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<AppInteractionRespVO> getPage(Long receiverId, PageParam pageParam) {
|
||||
PageResult<MemberInteractionDO> page = interactionMapper.selectPageByReceiver(receiverId, pageParam);
|
||||
if (ObjectUtil.isEmpty(page.getList())) {
|
||||
return PageResult.empty(page.getTotal());
|
||||
}
|
||||
List<AppInteractionRespVO> list = fillActors(page.getList());
|
||||
return new PageResult<>(list, page.getTotal());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void readAll(Long receiverId) {
|
||||
interactionMapper.markAllRead(receiverId);
|
||||
}
|
||||
|
||||
private List<AppInteractionRespVO> fillActors(List<MemberInteractionDO> rows) {
|
||||
if (ObjectUtil.isEmpty(rows)) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
List<Long> actorIds = rows.stream().map(MemberInteractionDO::getActorId).distinct().collect(Collectors.toList());
|
||||
List<MemberExtBO> members = memberUserService.getUserExtList(actorIds);
|
||||
Map<Long, MemberExtBO> 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();
|
||||
}
|
||||
|
||||
}
|
||||
+11
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
+17
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user