diff --git a/.gitignore b/.gitignore index 750bbe7..bcc83d5 100644 --- a/.gitignore +++ b/.gitignore @@ -62,3 +62,5 @@ application-my.yaml /.claude/agents/backend-architect.md /tkdata-model-server-issues.md /CLAUDE.md +/.omc/ +/AGENTS.md diff --git a/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/operatelog/core/service/LogRecordServiceImpl.java b/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/operatelog/core/service/LogRecordServiceImpl.java index 798a7ea..7504acc 100644 --- a/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/operatelog/core/service/LogRecordServiceImpl.java +++ b/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/operatelog/core/service/LogRecordServiceImpl.java @@ -1,11 +1,13 @@ package cn.iocoder.yudao.framework.operatelog.core.service; import cn.iocoder.yudao.framework.common.biz.system.logger.OperateLogCommonApi; +import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; import cn.iocoder.yudao.framework.common.biz.system.logger.dto.OperateLogCreateReqDTO; import cn.iocoder.yudao.framework.common.util.monitor.TracerUtils; import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils; import cn.iocoder.yudao.framework.security.core.LoginUser; import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils; +import cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils; import com.mzt.logapi.beans.LogRecord; import com.mzt.logapi.service.ILogRecordService; import lombok.extern.slf4j.Slf4j; @@ -50,11 +52,16 @@ public class LogRecordServiceImpl implements ILogRecordService { private static void fillUserFields(OperateLogCreateReqDTO reqDTO) { // 使用 SecurityFrameworkUtils。因为要考虑,rpc、mq、job,它其实不是 web; LoginUser loginUser = SecurityFrameworkUtils.getLoginUser(); - if (loginUser == null) { + if (loginUser != null) { + reqDTO.setUserId(loginUser.getId()); + reqDTO.setUserType(loginUser.getUserType()); return; } - reqDTO.setUserId(loginUser.getId()); - reqDTO.setUserType(loginUser.getUserType()); + // 匿名请求场景(例如注册),尝试从 Request Attribute 获取用户信息 + Long loginUserId = WebFrameworkUtils.getLoginUserId(); + Integer loginUserType = WebFrameworkUtils.getLoginUserType(); + reqDTO.setUserId(loginUserId != null ? loginUserId : 0L); + reqDTO.setUserType(loginUserType != null ? loginUserType : UserTypeEnum.ADMIN.getValue()); } public static void fillModuleFields(OperateLogCreateReqDTO reqDTO, LogRecord logRecord) { @@ -88,4 +95,4 @@ public class LogRecordServiceImpl implements ILogRecordService { throw new UnsupportedOperationException("使用 OperateLogApi 进行操作日志的查询"); } -} \ No newline at end of file +} diff --git a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/tenant/TenantController.java b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/tenant/TenantController.java index b1245bf..4411ea3 100644 --- a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/tenant/TenantController.java +++ b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/tenant/TenantController.java @@ -77,6 +77,14 @@ public class TenantController { return success(tenantService.createTenant(createReqVO)); } + @PostMapping("/register") + @PermitAll + @TenantIgnore + @Operation(summary = "注册租户") + public CommonResult registerTenant(@Valid @RequestBody TenantRegisterReqVO registerReqVO) { + return success(tenantService.registerTenant(registerReqVO)); + } + @PutMapping("/update") @Operation(summary = "更新租户") @PreAuthorize("@ss.hasPermission('system:tenant:update')") diff --git a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/tenant/vo/tenant/TenantRegisterReqVO.java b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/tenant/vo/tenant/TenantRegisterReqVO.java new file mode 100644 index 0000000..b1474ea --- /dev/null +++ b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/tenant/vo/tenant/TenantRegisterReqVO.java @@ -0,0 +1,58 @@ +package cn.iocoder.yudao.module.system.controller.admin.tenant.vo.tenant; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import org.hibernate.validator.constraints.Length; + +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Pattern; +import javax.validation.constraints.Size; +import javax.validation.constraints.NotEmpty; + +@Schema(description = "管理后台 - 租户注册 Request VO") +@Data +public class TenantRegisterReqVO { + + @Schema(description = "租户名", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋道") + @NotNull(message = "租户名不能为空") + private String name; + + @Schema(description = "联系人", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋艿") + @NotNull(message = "联系人不能为空") + private String contactName; + + @Schema(description = "联系手机", example = "15601691300") + private String contactMobile; + + @Schema(description = "用户账号", requiredMode = Schema.RequiredMode.REQUIRED, example = "yudao") + @NotNull(message = "用户账号不能为空") + @Pattern(regexp = "^[a-zA-Z0-9]{4,30}$", message = "用户账号由 数字、字母 组成") + @Size(min = 4, max = 30, message = "用户账号长度为 4-30 个字符") + private String username; + + @Schema(description = "密码", requiredMode = Schema.RequiredMode.REQUIRED, example = "123456") + @NotNull(message = "密码不能为空") + @Length(min = 6, max = 16, message = "密码长度为 6-16 位") + private String password; + + @Schema(description = "Turnstile 令牌", requiredMode = Schema.RequiredMode.REQUIRED) + @NotEmpty(message = "Turnstile 令牌不能为空") + private String turnstileToken; + +// @Schema(description = "是否允许登录爬虫客户端", example = "0不允许,1允许") +// private Byte crawl; +// +// @Schema(description = "是否允许登录大哥客户端", example = "0不允许,1允许") +// private Byte bigBrother; +// +// @Schema(description = "能否登录 AI 聊天工具", example = "0不允许,1允许") +// private Byte aiChat; +// +// @Schema(description = "备注", example = "备注") +// private String remark; +// +// @Schema(description = "租户类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "代理/客户") +// @NotNull(message = "租户类型不能为空") +// private String tenantType; + +} diff --git a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/tenant/TenantService.java b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/tenant/TenantService.java index 3749144..875c841 100755 --- a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/tenant/TenantService.java +++ b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/tenant/TenantService.java @@ -3,6 +3,7 @@ package cn.iocoder.yudao.module.system.service.tenant; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder; import cn.iocoder.yudao.module.system.controller.admin.tenant.vo.tenant.TenantPageReqVO; +import cn.iocoder.yudao.module.system.controller.admin.tenant.vo.tenant.TenantRegisterReqVO; import cn.iocoder.yudao.module.system.controller.admin.tenant.vo.tenant.TenantRenewalReqVO; import cn.iocoder.yudao.module.system.controller.admin.tenant.vo.tenant.TenantSaveReqVO; import cn.iocoder.yudao.module.system.dal.dataobject.tenant.TenantDO; @@ -28,6 +29,14 @@ public interface TenantService { */ Long createTenant(@Valid TenantSaveReqVO createReqVO); + /** + * 注册租户(固定默认值) + * + * @param registerReqVO 注册信息 + * @return 编号 + */ + Long registerTenant(@Valid TenantRegisterReqVO registerReqVO); + /** * 更新租户 * diff --git a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/tenant/TenantServiceImpl.java b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/tenant/TenantServiceImpl.java index c29bd2a..175ba66 100755 --- a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/tenant/TenantServiceImpl.java +++ b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/tenant/TenantServiceImpl.java @@ -7,18 +7,24 @@ import cn.hutool.core.date.LocalDateTimeUtil; import cn.hutool.core.lang.Assert; import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.StrUtil; +import cn.hutool.http.HttpRequest; +import cn.hutool.http.HttpResponse; import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils; import cn.iocoder.yudao.framework.common.util.date.DateUtils; +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; import cn.iocoder.yudao.framework.common.util.object.BeanUtils; import cn.iocoder.yudao.framework.datapermission.core.annotation.DataPermission; import cn.iocoder.yudao.framework.tenant.config.TenantProperties; import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnore; import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder; import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils; +import cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils; import cn.iocoder.yudao.module.system.controller.admin.permission.vo.role.RoleSaveReqVO; import cn.iocoder.yudao.module.system.controller.admin.tenant.vo.tenant.TenantPageReqVO; +import cn.iocoder.yudao.module.system.controller.admin.tenant.vo.tenant.TenantRegisterReqVO; import cn.iocoder.yudao.module.system.controller.admin.tenant.vo.tenant.TenantRenewalReqVO; import cn.iocoder.yudao.module.system.controller.admin.tenant.vo.tenant.TenantSaveReqVO; import cn.iocoder.yudao.module.system.convert.tenant.TenantConvert; @@ -48,17 +54,22 @@ import cn.iocoder.yudao.module.system.service.user.AdminUserService; import com.baomidou.dynamic.datasource.annotation.DSTransactional; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.validation.annotation.Validated; import javax.annotation.Resource; +import javax.servlet.http.HttpServletRequest; import java.time.LocalDateTime; import java.time.temporal.ChronoUnit; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.Set; +import java.util.concurrent.atomic.AtomicReference; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.*; @@ -74,6 +85,12 @@ import static java.util.Collections.singleton; @Slf4j public class TenantServiceImpl implements TenantService { + private static final Long REGISTER_PACKAGE_ID = 999L; + private static final Integer REGISTER_ACCOUNT_COUNT = 99; + private static final LocalDateTime REGISTER_EXPIRE_TIME = LocalDateTime.of(2099, 12, 31, 23, 59, 59); + private static final String TenantType = "用户"; + private static final String TURNSTILE_VERIFY_FAILED_REASON = "Turnstile 校验失败"; + @SuppressWarnings("SpringJavaAutowiredFieldsWarningInspection") @Autowired(required = false) // 由于 yudao.tenant.enable 配置项,可以关闭多租户的功能,所以这里只能不强制注入 private TenantProperties tenantProperties; @@ -103,6 +120,18 @@ public class TenantServiceImpl implements TenantService { @Autowired private TenantBalanceService tenantBalanceService; + @Value("${yudao.turnstile.enable:false}") + private Boolean turnstileEnable; + + @Value("${yudao.turnstile.secret-key:}") + private String turnstileSecretKey; + + @Value("${yudao.turnstile.verify-url:https://challenges.cloudflare.com/turnstile/v0/siteverify}") + private String turnstileVerifyUrl; + + @Value("${yudao.turnstile.timeout-millis:3000}") + private Integer turnstileTimeoutMillis; + @Override public List getTenantIdList() { List tenants = tenantMapper.selectList(); @@ -208,6 +237,65 @@ public class TenantServiceImpl implements TenantService { } + @Override + @DSTransactional + @DataPermission(enable = false) + public Long registerTenant(TenantRegisterReqVO registerReqVO) { + verifyTurnstile(registerReqVO.getTurnstileToken()); + // 注册接口为匿名访问,需要补充操作人,避免 creator/updater 为空 + fillSystemOperatorIfAbsent(); + TenantSaveReqVO createReqVO = BeanUtils.toBean(registerReqVO, TenantSaveReqVO.class); + createReqVO.setStatus(CommonStatusEnum.ENABLE.getStatus()); + createReqVO.setTenantType(TenantType); + createReqVO.setWebsite(registerReqVO.getContactMobile()+ ".yolozs.com"); + createReqVO.setAccountCount(REGISTER_ACCOUNT_COUNT); + createReqVO.setPackageId(REGISTER_PACKAGE_ID); + createReqVO.setExpireTime(REGISTER_EXPIRE_TIME); + AtomicReference tenantIdRef = new AtomicReference<>(); + TenantUtils.execute(1L, () -> tenantIdRef.set(createTenant(createReqVO))); + return tenantIdRef.get(); + } + + private void fillSystemOperatorIfAbsent() { + HttpServletRequest request = WebFrameworkUtils.getRequest(); + if (request == null || WebFrameworkUtils.getLoginUserId(request) != null) { + return; + } + WebFrameworkUtils.setLoginUserId(request, 1L); + WebFrameworkUtils.setLoginUserType(request, UserTypeEnum.ADMIN.getValue()); + } + + private void verifyTurnstile(String turnstileToken) { + if (!Boolean.TRUE.equals(turnstileEnable)) { + return; + } + if (StrUtil.isBlank(turnstileSecretKey)) { + throw exception(AUTH_REGISTER_CAPTCHA_CODE_ERROR, TURNSTILE_VERIFY_FAILED_REASON + ":secret-key 未配置"); + } + Map verifyResp; + try (HttpResponse response = HttpRequest.post(turnstileVerifyUrl) + .timeout(turnstileTimeoutMillis) + .form(buildTurnstileForm(turnstileToken)) + .execute()) { + verifyResp = JsonUtils.parseObject(response.body(), Map.class); + } catch (Exception ex) { + log.error("[verifyTurnstile][Turnstile 请求失败]", ex); + throw exception(AUTH_REGISTER_CAPTCHA_CODE_ERROR, TURNSTILE_VERIFY_FAILED_REASON + ":request failed"); + } + if (verifyResp == null || !Boolean.TRUE.equals(verifyResp.get("success"))) { + Object errorCodes = verifyResp == null ? null : verifyResp.get("error-codes"); + throw exception(AUTH_REGISTER_CAPTCHA_CODE_ERROR, + TURNSTILE_VERIFY_FAILED_REASON + ":" + StrUtil.blankToDefault(StrUtil.toString(errorCodes), "unknown")); + } + } + + private Map buildTurnstileForm(String turnstileToken) { + Map form = new HashMap<>(); + form.put("secret", turnstileSecretKey); + form.put("response", turnstileToken); + return form; + } + @Override @DSTransactional // 多数据源,使用 @DSTransactional 保证本地事务,以及数据源的切换 @DataPermission(enable = false) // 参见 https://gitee.com/zhijiantianya/ruoyi-vue-pro/pulls/1154 说明 @@ -594,4 +682,4 @@ public class TenantServiceImpl implements TenantService { return tenantProperties == null || Boolean.FALSE.equals(tenantProperties.getEnable()); } -} \ No newline at end of file +} diff --git a/yudao-server/src/main/resources/application.yaml b/yudao-server/src/main/resources/application.yaml index c4ca279..8ebd762 100644 --- a/yudao-server/src/main/resources/application.yaml +++ b/yudao-server/src/main/resources/application.yaml @@ -98,7 +98,7 @@ aj: cache-number: 1000 # local 缓存的阈值,达到这个值,清除缓存 timing-clear: 180 # local定时清除过期缓存(单位秒),设置为0代表不执行 type: blockPuzzle # 验证码类型 default两种都实例化。 blockPuzzle 滑块拼图 clickWord 文字点选 - water-mark: 芋道源码 # 右下角水印文字(我的水印),可使用 https://tool.chinaz.com/tools/unicode.aspx 中文转 Unicode,Linux 可能需要转 unicode + water-mark: # 右下角水印文字(我的水印),可使用 https://tool.chinaz.com/tools/unicode.aspx 中文转 Unicode,Linux 可能需要转 unicode interference-options: 0 # 滑动干扰项(0/1/2) req-frequency-limit-enable: false # 接口请求次数一分钟限制是否开启 true|false req-get-lock-limit: 5 # 验证失败 5 次,get接口锁定 @@ -212,6 +212,11 @@ yudao: send-maximum-quantity-per-day: 10 begin-code: 9999 # 这里配置 9999 的原因是,测试方便。 end-code: 9999 # 这里配置 9999 的原因是,测试方便。 + turnstile: # Cloudflare Turnstile 服务端校验配置 + enable: true # 生产开启后,必须配置 secret-key + secret-key: "0x4AAAAAACYSAQ2xlao9D8LlyDRhB3n1BmM" + verify-url: https://challenges.cloudflare.com/turnstile/v0/siteverify + timeout-millis: 3000 debug: false # 插件配置 TODO 芋艿:【IOT】需要处理下 @@ -221,4 +226,4 @@ pf4j: md5: salt: (-FhqvXO,wMz -multiple-device-login: true \ No newline at end of file +multiple-device-login: true