mirror of
https://gitcode.com/ageerle/ruoyi-ai.git
synced 2026-11-11 21:40:41 +00:00
fix: preserve application Jackson settings for tenant requests
This commit is contained in:
47
docs/tenant-management.md
Normal file
47
docs/tenant-management.md
Normal file
@@ -0,0 +1,47 @@
|
||||
# 新增租户接口
|
||||
|
||||
开启 `tenant.enable: true` 后,超级管理员可通过 `POST /system/tenant` 创建租户。请求需要登录凭证和 `system:tenant:add` 权限;使用反向代理时,请按部署配置添加接口前缀。
|
||||
|
||||
## 请求参数
|
||||
|
||||
请求使用 `Content-Type: application/json`。
|
||||
|
||||
| 参数 | 说明 |
|
||||
| --- | --- |
|
||||
| `companyName` | 企业名称,必填且不能与已有租户重复。 |
|
||||
| `contactUserName` | 联系人,必填。 |
|
||||
| `contactPhone` | 联系电话,必填。 |
|
||||
| `username` | 租户初始管理员用户名,必填。 |
|
||||
| `password` | 租户初始管理员密码,必填。 |
|
||||
| `packageId` | 已存在的租户套餐 ID,必填。建议使用字符串传输,避免浏览器对大整数的精度损失。 |
|
||||
| `expireTime` | 过期时间,使用 `yyyy-MM-dd HH:mm:ss`,例如 `2027-09-11 00:00:00`;不传或传 `null` 表示不限制有效期。未带时区的时间按后端运行时区解释。 |
|
||||
| `accountCount` | 可创建的用户数量,`-1` 表示不限制。 |
|
||||
|
||||
## 请求示例
|
||||
|
||||
请将套餐 ID 替换为当前系统中的有效套餐,并设置实际使用的管理员账号和密码。
|
||||
|
||||
```json
|
||||
{
|
||||
"companyName": "示例企业",
|
||||
"contactUserName": "联系人",
|
||||
"contactPhone": "13800000000",
|
||||
"username": "tenant_admin",
|
||||
"password": "ChangeMe_123456!",
|
||||
"packageId": "2018611998196109314",
|
||||
"expireTime": "2027-09-11 00:00:00",
|
||||
"accountCount": -1
|
||||
}
|
||||
```
|
||||
|
||||
## 日期配置
|
||||
|
||||
后端默认日期格式配置位于 `ruoyi-admin/src/main/resources/application.yml`:
|
||||
|
||||
```yaml
|
||||
spring:
|
||||
jackson:
|
||||
date-format: yyyy-MM-dd HH:mm:ss
|
||||
```
|
||||
|
||||
扩展应用的 `ObjectMapper` 时,使用 Spring 注入的 `Jackson2ObjectMapperBuilder`,以保留全局日期格式、自定义 Jackson 模块和构建器配置。
|
||||
@@ -0,0 +1,143 @@
|
||||
package org.ruoyi.config;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.junit.jupiter.api.Tag;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.ruoyi.common.json.config.JacksonConfig;
|
||||
import org.ruoyi.system.domain.bo.SysTenantBo;
|
||||
import org.ruoyi.workflow.config.BeanConfig;
|
||||
import org.springframework.boot.autoconfigure.AutoConfigurations;
|
||||
import org.springframework.boot.autoconfigure.http.HttpMessageConvertersAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration;
|
||||
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.converter.HttpMessageNotReadableException;
|
||||
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
|
||||
import org.springframework.mock.http.MockHttpInputMessage;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneId;
|
||||
import java.util.Date;
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertSame;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
@Tag("dev")
|
||||
class TenantRequestJacksonTest {
|
||||
|
||||
private static final String TENANT_REQUEST = """
|
||||
{
|
||||
"contactPhone": "13700000000",
|
||||
"expireTime": "2027-09-11 00:00:00",
|
||||
"accountCount": -1,
|
||||
"companyName": "第一集",
|
||||
"contactUserName": "刘备",
|
||||
"username": "user",
|
||||
"password": "123456",
|
||||
"packageId": "2018611998196109314"
|
||||
}
|
||||
""";
|
||||
|
||||
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
|
||||
.withConfiguration(AutoConfigurations.of(JacksonConfig.class, JacksonAutoConfiguration.class,
|
||||
HttpMessageConvertersAutoConfiguration.class))
|
||||
.withUserConfiguration(BeanConfig.class);
|
||||
|
||||
@Test
|
||||
void readsTenantRequestWithApplicationDateFormat() {
|
||||
contextRunner.withPropertyValues("spring.jackson.date-format=yyyy-MM-dd HH:mm:ss").run(context -> {
|
||||
MappingJackson2HttpMessageConverter converter = context.getBean(MappingJackson2HttpMessageConverter.class);
|
||||
assertSame(context.getBean(ObjectMapper.class), converter.getObjectMapper());
|
||||
|
||||
SysTenantBo tenant = readTenant(converter, TENANT_REQUEST);
|
||||
|
||||
assertEquals(expirationDate(), tenant.getExpireTime());
|
||||
assertEquals(2018611998196109314L, tenant.getPackageId());
|
||||
assertEquals(-1L, tenant.getAccountCount());
|
||||
assertEquals("第一集", tenant.getCompanyName());
|
||||
assertEquals("user", tenant.getUsername());
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void keepsRegisteredDateDeserializerWithoutDateFormatProperty() {
|
||||
contextRunner.run(context -> {
|
||||
SysTenantBo tenant = readTenant(context.getBean(MappingJackson2HttpMessageConverter.class), TENANT_REQUEST);
|
||||
|
||||
assertEquals(expirationDate(), tenant.getExpireTime());
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void honorsConfiguredDateSerializationFormat() {
|
||||
contextRunner.withPropertyValues("spring.jackson.date-format=yyyy/MM/dd HH:mm:ss").run(context -> {
|
||||
ObjectMapper mapper = context.getBean(ObjectMapper.class);
|
||||
|
||||
String json = mapper.writeValueAsString(new DateValue(expirationDate()));
|
||||
|
||||
assertEquals("{\"expireTime\":\"2027/09/11 00:00:00\"}", json);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void preservesWorkflowJsonRoundTripAndNullOmission() {
|
||||
contextRunner.run(context -> {
|
||||
ObjectMapper mapper = context.getBean(ObjectMapper.class);
|
||||
WorkflowValue value = new WorkflowValue(2018611998196109314L,
|
||||
LocalDateTime.of(2027, 9, 11, 0, 0), Optional.of("workflow"), null);
|
||||
|
||||
String json = mapper.writeValueAsString(value);
|
||||
JsonNode tree = mapper.readTree(json);
|
||||
|
||||
assertTrue(tree.get("id").isTextual());
|
||||
assertEquals("2018611998196109314", tree.get("id").asText());
|
||||
assertEquals("2027-09-11 00:00:00", tree.get("createdAt").asText());
|
||||
assertEquals("workflow", tree.get("name").asText());
|
||||
assertFalse(tree.has("remark"));
|
||||
assertEquals(value, mapper.readValue(json, WorkflowValue.class));
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void acceptsNullExpirationDate() {
|
||||
contextRunner.run(context -> {
|
||||
String request = TENANT_REQUEST.replace("\"2027-09-11 00:00:00\"", "null");
|
||||
SysTenantBo tenant = readTenant(context.getBean(MappingJackson2HttpMessageConverter.class), request);
|
||||
|
||||
assertNull(tenant.getExpireTime());
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsInvalidExpirationDate() {
|
||||
contextRunner.run(context -> {
|
||||
String request = TENANT_REQUEST.replace("2027-09-11 00:00:00", "invalid-date");
|
||||
MappingJackson2HttpMessageConverter converter = context.getBean(MappingJackson2HttpMessageConverter.class);
|
||||
|
||||
assertThrows(HttpMessageNotReadableException.class, () -> readTenant(converter, request));
|
||||
});
|
||||
}
|
||||
|
||||
private static SysTenantBo readTenant(MappingJackson2HttpMessageConverter converter, String json) throws Exception {
|
||||
MockHttpInputMessage input = new MockHttpInputMessage(json.getBytes(StandardCharsets.UTF_8));
|
||||
input.getHeaders().setContentType(MediaType.APPLICATION_JSON);
|
||||
return (SysTenantBo) converter.read(SysTenantBo.class, input);
|
||||
}
|
||||
|
||||
private static Date expirationDate() {
|
||||
return Date.from(LocalDateTime.of(2027, 9, 11, 0, 0).atZone(ZoneId.systemDefault()).toInstant());
|
||||
}
|
||||
|
||||
private record DateValue(Date expireTime) {
|
||||
}
|
||||
|
||||
private record WorkflowValue(Long id, LocalDateTime createdAt, Optional<String> name, String remark) {
|
||||
}
|
||||
}
|
||||
@@ -39,9 +39,10 @@ public class BeanConfig {
|
||||
|
||||
@Bean
|
||||
@Primary
|
||||
public ObjectMapper objectMapper() {
|
||||
public ObjectMapper objectMapper(Jackson2ObjectMapperBuilder builder) {
|
||||
log.info("Configuration:create objectMapper");
|
||||
ObjectMapper objectMapper = new Jackson2ObjectMapperBuilder().createXmlMapper(false).build();
|
||||
// 使用容器中的构建器,保留全局日期格式、Module 和 Customizer 配置。
|
||||
ObjectMapper objectMapper = builder.createXmlMapper(false).build();
|
||||
objectMapper.registerModules(LocalDateTimeUtil.getSimpleModule(), new JavaTimeModule(), new Jdk8Module());
|
||||
//设置null值不参与序列化(字段不被显示)
|
||||
objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
|
||||
|
||||
Reference in New Issue
Block a user