From aa1a9c9f237da14cd336756898fd29bacebe1d64 Mon Sep 17 00:00:00 2001 From: wells <123456@qq.com> Date: Sat, 8 Aug 2026 17:07:24 +0800 Subject: [PATCH] =?UTF-8?q?fix=20=E5=88=9B=E5=BB=BA=E7=BE=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/tailbet/controller/OpsController.java | 18 +++ .../com/tailbet/model/dto/CreateGroupDTO.java | 33 +++++ .../java/com/tailbet/model/entity/Group.java | 2 +- .../com/tailbet/service/IGroupService.java | 12 ++ .../service/impl/GroupServiceImpl.java | 122 ++++++++++++++++++ src/main/resources/sql/init.sql | 1 + 6 files changed, 187 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/tailbet/model/dto/CreateGroupDTO.java diff --git a/src/main/java/com/tailbet/controller/OpsController.java b/src/main/java/com/tailbet/controller/OpsController.java index 8c1b84d..2ca1b91 100644 --- a/src/main/java/com/tailbet/controller/OpsController.java +++ b/src/main/java/com/tailbet/controller/OpsController.java @@ -1,12 +1,15 @@ package com.tailbet.controller; +import com.tailbet.model.dto.CreateGroupDTO; import com.tailbet.model.dto.PointsDTO; import com.tailbet.model.dto.PullGroupDTO; +import com.tailbet.model.entity.Group; import com.tailbet.model.vo.R; import com.tailbet.model.vo.UserContext; import com.tailbet.service.IPointsService; import com.tailbet.service.IGroupService; import lombok.RequiredArgsConstructor; +import jakarta.validation.Valid; import org.springframework.web.bind.annotation.*; /** @@ -50,6 +53,21 @@ public class OpsController { } + /** + * 创建群(指定群ID、名称,群主为传入用户,并自动把机器人拉入群) + */ + @PostMapping("/group/create") + public R createGroup(@Valid @RequestBody CreateGroupDTO dto) { + if (!UserContext.isOps()) { + return R.fail("无权限:仅运营可操作"); + } + Long operatorId = UserContext.getUserId(); + Group group = groupService.createGroup(dto.getName(), + dto.getUserId(), dto.getRoundInterval(), operatorId); + return R.ok(group.getId()); + } + + /** * 拉入群 */ diff --git a/src/main/java/com/tailbet/model/dto/CreateGroupDTO.java b/src/main/java/com/tailbet/model/dto/CreateGroupDTO.java new file mode 100644 index 0000000..bf2a9bc --- /dev/null +++ b/src/main/java/com/tailbet/model/dto/CreateGroupDTO.java @@ -0,0 +1,33 @@ +package com.tailbet.model.dto; + +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +/** + * 创建群请求 + */ +@Data +public class CreateGroupDTO { + + /** + * 群名称 + */ + @NotBlank(message = "群名称不能为空") + private String name; + + /** + * 群主用户ID + */ + @NotNull(message = "群主用户ID不能为空") + private Long userId; + + /** + * 接注间隔(秒),不传则走库默认值60 + */ + @Min(value = 10, message = "接注间隔不能小于10秒") + @Max(value = 3600, message = "接注间隔不能大于3600秒") + private Integer roundInterval; +} \ No newline at end of file diff --git a/src/main/java/com/tailbet/model/entity/Group.java b/src/main/java/com/tailbet/model/entity/Group.java index 66ba88f..42d2eaf 100644 --- a/src/main/java/com/tailbet/model/entity/Group.java +++ b/src/main/java/com/tailbet/model/entity/Group.java @@ -10,7 +10,7 @@ import java.time.LocalDateTime; * 群表 */ @Data -@TableName("group") +@TableName("g_group") public class Group { @TableId(type = IdType.AUTO) diff --git a/src/main/java/com/tailbet/service/IGroupService.java b/src/main/java/com/tailbet/service/IGroupService.java index 23d2716..0697619 100644 --- a/src/main/java/com/tailbet/service/IGroupService.java +++ b/src/main/java/com/tailbet/service/IGroupService.java @@ -25,6 +25,18 @@ public interface IGroupService { */ List getUserGroups(Long userId); + /** + * 创建群(群ID自增生成,群主为传入用户,并自动把机器人拉入群) + *

机器人取 sys_config 中 cfg_key=robot_id 的配置值

+ * + * @param name 群名称 + * @param ownerUserId 群主用户ID + * @param roundInterval 接注间隔(秒),可为null走库默认值 + * @param operatorId 操作人用户ID + * @return 创建出的群 + */ + Group createGroup( String name, Long ownerUserId, Integer roundInterval, Long operatorId); + /** * 拉用户入群 */ diff --git a/src/main/java/com/tailbet/service/impl/GroupServiceImpl.java b/src/main/java/com/tailbet/service/impl/GroupServiceImpl.java index 71170a0..8c05862 100644 --- a/src/main/java/com/tailbet/service/impl/GroupServiceImpl.java +++ b/src/main/java/com/tailbet/service/impl/GroupServiceImpl.java @@ -2,9 +2,13 @@ package com.tailbet.service.impl; import com.tailbet.model.entity.Group; import com.tailbet.model.entity.GroupMember; +import com.tailbet.model.entity.SysConfig; +import com.tailbet.model.entity.User; import com.tailbet.mapper.GroupMapper; import com.tailbet.mapper.GroupMemberMapper; +import com.tailbet.mapper.SysConfigMapper; import com.tailbet.mapper.UserMapper; +import com.tailbet.openim.OpenIMApiClient; import com.tailbet.service.IGroupService; import com.tailbet.service.IAuditService; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; @@ -24,10 +28,17 @@ import java.util.List; @Service public class GroupServiceImpl implements IGroupService { + /** + * 系统配置键:机器人用户ID + */ + private static final String CFG_KEY_ROBOT_ID = "robot_id"; + private final GroupMapper groupMapper; private final GroupMemberMapper groupMemberMapper; private final UserMapper userMapper; + private final SysConfigMapper sysConfigMapper; private final IAuditService auditService; + private final OpenIMApiClient openIMApiClient; /** * 获取群信息 @@ -58,6 +69,117 @@ public class GroupServiceImpl implements IGroupService { .eq(Group::getStatus, 1)); } + /** + * 创建群(群ID自增生成,群主为传入用户,并自动把机器人拉入群) + *

+ * 机器人取 sys_config 中 cfg_key=robot_id 的配置值。 + * 先落本地库,再调 OpenIM 建群:OpenIM 失败会抛异常回滚本地事务, + * 保证不出现"本地有群、IM 无群"的脏数据。反向残留(IM 有群、本地回滚)可安全重试, + * 因为 {@link OpenIMApiClient#createGroup} 对已存在的群做了幂等跳过。 + *

+ */ + @Override + @Transactional + public Group createGroup( String name, Long ownerUserId, Integer roundInterval, Long operatorId) { + + if (name == null || name.isBlank()) { + throw new RuntimeException("群名称不能为空"); + } + if (ownerUserId == null) { + throw new RuntimeException("群主用户ID不能为空"); + } + + + + // 群主必须是已存在的用户 + User owner = userMapper.selectById(ownerUserId); + if (owner == null) { + throw new RuntimeException("群主用户不存在: " + ownerUserId); + } + + // 机器人账号(sys_config 中 cfg_key=robot_id) + Long botUserId = getRobotUserId(); + if (userMapper.selectById(botUserId) == null) { + throw new RuntimeException("机器人用户不存在: " + botUserId + ",请检查 sys_config.robot_id 配置"); + } + // 群主本人就是机器人时,只需建一条群主记录 + boolean ownerIsBot = botUserId.equals(ownerUserId); + + LocalDateTime now = LocalDateTime.now(); + + // 建群:id 由数据库自增生成 + // openim_group_id 为 NOT NULL 且要与自增 id 保持一致, + // 故先占位插入拿到 id,再回填。同一事务内,不存在中间态外泄。 + Group group = new Group(); + group.setName(name); + group.setOpenimGroupId(""); + group.setOwnerUserId(ownerUserId); + group.setRoundInterval(roundInterval); + group.setStatus(1); + group.setCreateTime(now); + group.setUpdateTime(now); + groupMapper.insert(group); + + Long groupId = group.getId(); + group.setOpenimGroupId(String.valueOf(groupId)); + groupMapper.updateById(group); + + // 群主入群 + groupMemberMapper.insert(buildMember(groupId, ownerUserId, ROLE_OWNER, now)); + + // 机器人入群 + if (!ownerIsBot) { + groupMemberMapper.insert(buildMember(groupId, botUserId, ROLE_NORMAL, now)); + } + + // OpenIM 侧建群:群主为 owner,机器人作为初始成员 + List memberUserIDs = ownerIsBot + ? List.of() + : List.of(String.valueOf(botUserId)); + openIMApiClient.createGroup(String.valueOf(groupId), name, String.valueOf(ownerUserId), memberUserIDs); + + // 审计日志 + auditService.log(groupId, null, operatorId, "CREATE_GROUP", + String.format("{\"name\":\"%s\",\"ownerUserId\":%d,\"botUserId\":%d}", + name.replace("\\", "\\\\").replace("\"", "\\\""), ownerUserId, botUserId)); + + log.info("创建群成功: groupId={}, name={}, ownerUserId={}, botUserId={}, operatorId={}", + groupId, name, ownerUserId, botUserId, operatorId); + return group; + } + + /** + * 读取机器人用户ID(sys_config 中 cfg_key=robot_id) + */ + private Long getRobotUserId() { + SysConfig config = sysConfigMapper.selectOne(new LambdaQueryWrapper() + .eq(SysConfig::getCfgKey, CFG_KEY_ROBOT_ID)); + + if (config == null || config.getCfgValue() == null || config.getCfgValue().isBlank()) { + throw new RuntimeException("系统配置缺失: sys_config 中未配置 " + CFG_KEY_ROBOT_ID); + } + + try { + return Long.parseLong(config.getCfgValue().trim()); + } catch (NumberFormatException e) { + throw new RuntimeException("系统配置 " + CFG_KEY_ROBOT_ID + " 不是合法的用户ID: " + config.getCfgValue()); + } + } + + /** + * 构造群成员记录 + */ + private GroupMember buildMember(Long groupId, Long userId, int role, LocalDateTime now) { + GroupMember member = new GroupMember(); + member.setGroupId(groupId); + member.setUserId(userId); + member.setRole(role); + member.setJoinTime(now); + member.setCreateTime(now); + member.setUpdateTime(now); + return member; + } + /** * 拉用户入群 */ diff --git a/src/main/resources/sql/init.sql b/src/main/resources/sql/init.sql index 2a1729b..1bea48d 100644 --- a/src/main/resources/sql/init.sql +++ b/src/main/resources/sql/init.sql @@ -221,6 +221,7 @@ INSERT INTO sys_config (cfg_key, cfg_value, remark) VALUES ('fake_daily_active_max', '20', '每日活跃假人最大数量'), ('win_profit_threshold', '500', '小赢利润阈值'), ('lose_profit_threshold', '-500', '小输利润阈值'); +INSERT INTO `guess_game`.`sys_config`(`id`, `cfg_key`, `cfg_value`, `remark`, `update_time`) VALUES (27, 'robot_id', '8', '机器人id', '2026-08-08 16:33:16'); -- red_packet 表增加预分配金额字段 ALTER TABLE red_packet ADD COLUMN share_amounts JSON COMMENT '预分配金额列表(JSON数组)';