优化
This commit is contained in:
@@ -5,6 +5,7 @@
|
|||||||
- 本文件适用于 `baiyue-firstleg/` Maven 多模块仓库,包含国内头程与海外区域服务;进入具体服务后继续读取该服务的 `AGENTS.md`。
|
- 本文件适用于 `baiyue-firstleg/` Maven 多模块仓库,包含国内头程与海外区域服务;进入具体服务后继续读取该服务的 `AGENTS.md`。
|
||||||
- 本层只负责:服务边界、跨服务契约、数据库部署总入口、阶段路由和通用验证。Controller、Service、DTO、SQL 细节以模块级指引为准。
|
- 本层只负责:服务边界、跨服务契约、数据库部署总入口、阶段路由和通用验证。Controller、Service、DTO、SQL 细节以模块级指引为准。
|
||||||
- 普通任务先读本文件和目标模块指引;聚合构建/架构问题再读 `README.md`、`pom.xml`、`docs/头程项目目录结构与服务说明.md`。
|
- 普通任务先读本文件和目标模块指引;聚合构建/架构问题再读 `README.md`、`pom.xml`、`docs/头程项目目录结构与服务说明.md`。
|
||||||
|
- 平台/客户层级与权限公共语义见 `../docs/平台客户层级与权限契约.md`;国内/海外 Token、双区域 API 和跨区事件公共契约见 `../docs/跨区域认证与服务通信契约.md`。本文件只保留后端执行所需摘要。
|
||||||
|
|
||||||
## 不可突破的产品与数据边界
|
## 不可突破的产品与数据边界
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
- 负责登录、注册、账号凭据、Access/Refresh Token、退出、密码和账号归属范围。
|
- 负责登录、注册、账号凭据、Access/Refresh Token、退出、密码和账号归属范围。
|
||||||
- 客户主档、成员、角色权限归 `iam/`;Auth 通过 IAM 客户端获取授权快照,不跨库写 IAM。
|
- 客户主档、成员、角色权限归 `iam/`;Auth 通过 IAM 客户端获取授权快照,不跨库写 IAM。
|
||||||
|
- 修改国内/海外 Token、Audience、JWKS 或跨区域身份边界前,先读 `../../docs/跨区域认证与服务通信契约.md`。
|
||||||
- `tenant_id=0` 为平台作用域,`tenant_id>0` 为客户 ID;不得引入租户管理或切换。
|
- `tenant_id=0` 为平台作用域,`tenant_id>0` 为客户 ID;不得引入租户管理或切换。
|
||||||
|
|
||||||
## 模块入口
|
## 模块入口
|
||||||
|
|||||||
@@ -11,6 +11,10 @@ public record TokenResponse(
|
|||||||
@Schema(description = "JWT access token / JWT 访问令牌", format = "password") String accessToken,
|
@Schema(description = "JWT access token / JWT 访问令牌", format = "password") String accessToken,
|
||||||
@Schema(description = "Access token lifetime in seconds / 访问令牌有效秒数", example = "1800") long expiresIn,
|
@Schema(description = "Access token lifetime in seconds / 访问令牌有效秒数", example = "1800") long expiresIn,
|
||||||
@Schema(description = "Refresh token / 刷新令牌", format = "password") String refreshToken,
|
@Schema(description = "Refresh token / 刷新令牌", format = "password") String refreshToken,
|
||||||
@Schema(description = "Refresh token lifetime in seconds / 刷新令牌有效秒数", example = "604800") long refreshExpiresIn
|
@Schema(description = "Refresh token lifetime in seconds / 刷新令牌有效秒数", example = "604800") long refreshExpiresIn,
|
||||||
|
@Schema(description = "Short-lived overseas API access token; null only when overseas JWT is disabled / 海外 API 短时访问令牌;仅海外 JWT 未启用时为空",
|
||||||
|
format = "password", nullable = true) String overseasAccessToken,
|
||||||
|
@Schema(description = "Overseas access token lifetime in seconds; zero only when overseas JWT is disabled / 海外访问令牌有效秒数;仅海外 JWT 未启用时为 0",
|
||||||
|
example = "900", minimum = "0") long overseasExpiresIn
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import org.springframework.security.oauth2.jwt.JwtEncoderParameters;
|
|||||||
import org.springframework.security.oauth2.jwt.JwsHeader;
|
import org.springframework.security.oauth2.jwt.JwsHeader;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
import org.springframework.beans.factory.ObjectProvider;
|
||||||
|
|
||||||
import java.security.SecureRandom;
|
import java.security.SecureRandom;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
@@ -49,6 +50,8 @@ public class AuthService {
|
|||||||
private AuthorizationLoader authorizationLoader;
|
private AuthorizationLoader authorizationLoader;
|
||||||
@Resource
|
@Resource
|
||||||
private TokenSessionStore tokenSessionStore;
|
private TokenSessionStore tokenSessionStore;
|
||||||
|
@Resource
|
||||||
|
private ObjectProvider<OverseasTokenService> overseasTokens;
|
||||||
|
|
||||||
private final SecureRandom secureRandom = new SecureRandom();
|
private final SecureRandom secureRandom = new SecureRandom();
|
||||||
|
|
||||||
@@ -134,8 +137,13 @@ public class AuthService {
|
|||||||
String rawRefresh = Base64.getUrlEncoder().withoutPadding().encodeToString(random);
|
String rawRefresh = Base64.getUrlEncoder().withoutPadding().encodeToString(random);
|
||||||
tokenSessionStore.store(jwtId, account.getId(), authorization.getRoles(), authorization.getPermissions(), rawRefresh,
|
tokenSessionStore.store(jwtId, account.getId(), authorization.getRoles(), authorization.getPermissions(), rawRefresh,
|
||||||
properties.accessTokenTtl(), properties.refreshTokenTtl());
|
properties.accessTokenTtl(), properties.refreshTokenTtl());
|
||||||
|
OverseasTokenService service = overseasTokens.getIfAvailable();
|
||||||
|
OverseasTokenService.OverseasTokenResponse overseas = service == null
|
||||||
|
? null : service.issue(account, authorization);
|
||||||
return new TokenResponse("Bearer", accessToken, properties.accessTokenTtl().toSeconds(),
|
return new TokenResponse("Bearer", accessToken, properties.accessTokenTtl().toSeconds(),
|
||||||
rawRefresh, properties.refreshTokenTtl().toSeconds());
|
rawRefresh, properties.refreshTokenTtl().toSeconds(),
|
||||||
|
overseas == null ? null : overseas.accessToken(),
|
||||||
|
overseas == null ? 0L : overseas.expiresIn());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
|
|||||||
@@ -36,16 +36,22 @@ public class OverseasTokenService {
|
|||||||
throw new BizException(ResultCode.UNAUTHORIZED, "账号不可用");
|
throw new BizException(ResultCode.UNAUTHORIZED, "账号不可用");
|
||||||
}
|
}
|
||||||
AuthorizationSnapshot authorization = authorizationLoader.load(account);
|
AuthorizationSnapshot authorization = authorizationLoader.load(account);
|
||||||
|
return issue(account, authorization);
|
||||||
|
}
|
||||||
|
|
||||||
|
public OverseasTokenResponse issue(AuthAccount account, AuthorizationSnapshot authorization) {
|
||||||
Instant issuedAt = Instant.now();
|
Instant issuedAt = Instant.now();
|
||||||
Instant expiresAt = issuedAt.plus(properties.accessTokenTtl());
|
Instant expiresAt = issuedAt.plus(properties.accessTokenTtl());
|
||||||
JwtClaimsSet claims = JwtClaimsSet.builder()
|
JwtClaimsSet claims = JwtClaimsSet.builder()
|
||||||
.issuer(properties.issuer()).audience(List.of(properties.audience()))
|
.issuer(properties.issuer()).audience(List.of(properties.audience()))
|
||||||
.subject(account.getId().toString()).id(UUID.randomUUID().toString())
|
.subject(account.getId().toString()).id(UUID.randomUUID().toString())
|
||||||
.issuedAt(issuedAt).expiresAt(expiresAt)
|
.issuedAt(issuedAt).expiresAt(expiresAt)
|
||||||
.claim("userId", account.getId()).claim("tenantId", account.getDefaultTenantId())
|
.claim("user_id", account.getId()).claim("tenant_id", account.getDefaultTenantId())
|
||||||
.claim("appCode", account.getDefaultAppCode()).claim("roles", authorization.getRoles())
|
.claim("membership_id", authorization.getMembershipId())
|
||||||
|
.claim("app_code", account.getDefaultAppCode()).claim("client_type", authorization.getClientType())
|
||||||
|
.claim("roles", authorization.getRoles())
|
||||||
.claim("authorities", authorization.getPermissions())
|
.claim("authorities", authorization.getPermissions())
|
||||||
.claim("permissionVersion", authorization.getPermissionVersion() == null ? 0L : authorization.getPermissionVersion())
|
.claim("permission_version", authorization.getPermissionVersion() == null ? 0L : authorization.getPermissionVersion())
|
||||||
.build();
|
.build();
|
||||||
String token = encoder.encode(JwtEncoderParameters.from(
|
String token = encoder.encode(JwtEncoderParameters.from(
|
||||||
JwsHeader.with(SignatureAlgorithm.RS256).keyId(properties.currentKid()).build(), claims)).getTokenValue();
|
JwsHeader.with(SignatureAlgorithm.RS256).keyId(properties.currentKid()).build(), claims)).getTokenValue();
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ class AuthServiceSmallJwtTest {
|
|||||||
@MockBean private JwtEncoder jwtEncoder;
|
@MockBean private JwtEncoder jwtEncoder;
|
||||||
@MockBean private AuthorizationLoader authorizationLoader;
|
@MockBean private AuthorizationLoader authorizationLoader;
|
||||||
@MockBean private TokenSessionStore tokenSessionStore;
|
@MockBean private TokenSessionStore tokenSessionStore;
|
||||||
|
@MockBean private OverseasTokenService overseasTokenService;
|
||||||
@Resource private AuthService service;
|
@Resource private AuthService service;
|
||||||
|
|
||||||
@Configuration(proxyBeanMethods = false)
|
@Configuration(proxyBeanMethods = false)
|
||||||
@@ -59,7 +60,7 @@ class AuthServiceSmallJwtTest {
|
|||||||
account.setDefaultAppCode("PLATFORM");
|
account.setDefaultAppCode("PLATFORM");
|
||||||
account.setTokenVersion(1);
|
account.setTokenVersion(1);
|
||||||
AuthorizationSnapshot snapshot = new AuthorizationSnapshot(42L, 0L, 84L, "PLATFORM",
|
AuthorizationSnapshot snapshot = new AuthorizationSnapshot(42L, 0L, 84L, "PLATFORM",
|
||||||
"WEB", Set.of("PLATFORM_ADMIN"), Set.of("platform:order:list"));
|
"WEB", Set.of("PLATFORM_ADMIN"), Set.of("platform:order:list"), 1L);
|
||||||
Jwt encoded = org.mockito.Mockito.mock(Jwt.class);
|
Jwt encoded = org.mockito.Mockito.mock(Jwt.class);
|
||||||
|
|
||||||
when(repository.findByUsername("operator")).thenReturn(account);
|
when(repository.findByUsername("operator")).thenReturn(account);
|
||||||
@@ -67,8 +68,10 @@ class AuthServiceSmallJwtTest {
|
|||||||
when(authorizationLoader.load(account)).thenReturn(snapshot);
|
when(authorizationLoader.load(account)).thenReturn(snapshot);
|
||||||
when(jwtEncoder.encode(any())).thenReturn(encoded);
|
when(jwtEncoder.encode(any())).thenReturn(encoded);
|
||||||
when(encoded.getTokenValue()).thenReturn("small-token");
|
when(encoded.getTokenValue()).thenReturn("small-token");
|
||||||
|
when(overseasTokenService.issue(account, snapshot)).thenReturn(
|
||||||
|
new OverseasTokenService.OverseasTokenResponse("Bearer", "overseas-token", 900));
|
||||||
|
|
||||||
service.login(new LoginRequest("operator", "secret"));
|
var response = service.login(new LoginRequest("operator", "secret"));
|
||||||
|
|
||||||
ArgumentCaptor<JwtEncoderParameters> jwt = ArgumentCaptor.forClass(JwtEncoderParameters.class);
|
ArgumentCaptor<JwtEncoderParameters> jwt = ArgumentCaptor.forClass(JwtEncoderParameters.class);
|
||||||
verify(jwtEncoder).encode(jwt.capture());
|
verify(jwtEncoder).encode(jwt.capture());
|
||||||
@@ -88,6 +91,42 @@ class AuthServiceSmallJwtTest {
|
|||||||
org.mockito.ArgumentMatchers.eq(Set.of("platform:order:list")), any(),
|
org.mockito.ArgumentMatchers.eq(Set.of("platform:order:list")), any(),
|
||||||
org.mockito.ArgumentMatchers.eq(Duration.ofMinutes(30)),
|
org.mockito.ArgumentMatchers.eq(Duration.ofMinutes(30)),
|
||||||
org.mockito.ArgumentMatchers.eq(Duration.ofDays(7)));
|
org.mockito.ArgumentMatchers.eq(Duration.ofDays(7)));
|
||||||
|
verify(overseasTokenService).issue(account, snapshot);
|
||||||
|
assertThat(response.overseasAccessToken()).isEqualTo("overseas-token");
|
||||||
|
assertThat(response.overseasExpiresIn()).isEqualTo(900);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void refreshRotatesDomesticAndOverseasTokensFromOneAuthorizationSnapshot() {
|
||||||
|
AuthAccount account = new AuthAccount();
|
||||||
|
account.setId(52L);
|
||||||
|
account.setUsername("merchant");
|
||||||
|
account.setAccountStatus("ACTIVE");
|
||||||
|
account.setDefaultTenantId(10001L);
|
||||||
|
account.setOperatorTenantId(10001L);
|
||||||
|
account.setDefaultMembershipId(94L);
|
||||||
|
account.setDefaultAppCode("MERCHANT");
|
||||||
|
account.setTokenVersion(3);
|
||||||
|
AuthorizationSnapshot snapshot = new AuthorizationSnapshot(52L, 10001L, 94L, "MERCHANT",
|
||||||
|
"WEB", Set.of("MERCHANT_ADMIN"), Set.of("merchant:order:list"), 3L);
|
||||||
|
Jwt encoded = org.mockito.Mockito.mock(Jwt.class);
|
||||||
|
|
||||||
|
when(tokenSessionStore.consume("refresh-old")).thenReturn(
|
||||||
|
new TokenSessionStore.RefreshSession(52L, "refresh-session"));
|
||||||
|
when(repository.findById(52L)).thenReturn(account);
|
||||||
|
when(authorizationLoader.load(account)).thenReturn(snapshot);
|
||||||
|
when(jwtEncoder.encode(any())).thenReturn(encoded);
|
||||||
|
when(encoded.getTokenValue()).thenReturn("domestic-refreshed");
|
||||||
|
when(overseasTokenService.issue(account, snapshot)).thenReturn(
|
||||||
|
new OverseasTokenService.OverseasTokenResponse("Bearer", "overseas-refreshed", 900));
|
||||||
|
|
||||||
|
var response = service.refresh("refresh-old");
|
||||||
|
|
||||||
|
assertThat(response.accessToken()).isEqualTo("domestic-refreshed");
|
||||||
|
assertThat(response.overseasAccessToken()).isEqualTo("overseas-refreshed");
|
||||||
|
assertThat(response.overseasExpiresIn()).isEqualTo(900);
|
||||||
|
verify(authorizationLoader).load(account);
|
||||||
|
verify(overseasTokenService).issue(account, snapshot);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -102,7 +141,7 @@ class AuthServiceSmallJwtTest {
|
|||||||
account.setDefaultAppCode("MERCHANT");
|
account.setDefaultAppCode("MERCHANT");
|
||||||
account.setTokenVersion(2);
|
account.setTokenVersion(2);
|
||||||
AuthorizationSnapshot snapshot = new AuthorizationSnapshot(43L, 10001L, 85L, "MERCHANT",
|
AuthorizationSnapshot snapshot = new AuthorizationSnapshot(43L, 10001L, 85L, "MERCHANT",
|
||||||
"WEB", Set.of("MERCHANT_OWNER"), Set.of("merchant:order:list"));
|
"WEB", Set.of("MERCHANT_OWNER"), Set.of("merchant:order:list"), 2L);
|
||||||
Jwt encoded = org.mockito.Mockito.mock(Jwt.class);
|
Jwt encoded = org.mockito.Mockito.mock(Jwt.class);
|
||||||
TokenSessionStore.ImpersonationTicket ticket = new TokenSessionStore.ImpersonationTicket(
|
TokenSessionStore.ImpersonationTicket ticket = new TokenSessionStore.ImpersonationTicket(
|
||||||
"ticket-1", 43L, 10001L, 85L, 42L, 84L, "support");
|
"ticket-1", 43L, 10001L, 85L, 42L, 84L, "support");
|
||||||
|
|||||||
@@ -64,11 +64,13 @@ class OverseasTokenServiceTest {
|
|||||||
assertThat(parameters.getJwsHeader().getKeyId()).isEqualTo("current-kid");
|
assertThat(parameters.getJwsHeader().getKeyId()).isEqualTo("current-kid");
|
||||||
assertThat(parameters.getClaims().getAudience()).containsExactly("lastleg-api");
|
assertThat(parameters.getClaims().getAudience()).containsExactly("lastleg-api");
|
||||||
assertThat(parameters.getClaims().getClaims())
|
assertThat(parameters.getClaims().getClaims())
|
||||||
.containsEntry("userId", 42L).containsEntry("tenantId", 100L)
|
.containsEntry("user_id", 42L).containsEntry("tenant_id", 100L)
|
||||||
.containsEntry("appCode", "MERCHANT").containsEntry("permissionVersion", 7L)
|
.containsEntry("membership_id", 84L)
|
||||||
|
.containsEntry("app_code", "MERCHANT").containsEntry("client_type", "WEB")
|
||||||
|
.containsEntry("permission_version", 7L)
|
||||||
.containsEntry("roles", Set.of("MERCHANT_ADMIN"))
|
.containsEntry("roles", Set.of("MERCHANT_ADMIN"))
|
||||||
.containsEntry("authorities", Set.of("merchant:order:list"))
|
.containsEntry("authorities", Set.of("merchant:order:list"))
|
||||||
.doesNotContainKeys("user_id", "tenant_id", "app_code");
|
.doesNotContainKeys("userId", "tenantId", "appCode", "permissionVersion");
|
||||||
assertThat(response.expiresIn()).isEqualTo(900);
|
assertThat(response.expiresIn()).isEqualTo(900);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -751,7 +751,7 @@ CREATE TABLE IF NOT EXISTS wh_warehouse (
|
|||||||
tenant_id BIGINT UNSIGNED NOT NULL COMMENT '平台公共仓库固定使用 tenant_id=0',
|
tenant_id BIGINT UNSIGNED NOT NULL COMMENT '平台公共仓库固定使用 tenant_id=0',
|
||||||
warehouse_code VARCHAR(50) NOT NULL COMMENT '平台内唯一仓库编码',
|
warehouse_code VARCHAR(50) NOT NULL COMMENT '平台内唯一仓库编码',
|
||||||
warehouse_name VARCHAR(120) NOT NULL COMMENT '仓库名称',
|
warehouse_name VARCHAR(120) NOT NULL COMMENT '仓库名称',
|
||||||
warehouse_type VARCHAR(32) NOT NULL DEFAULT 'DOMESTIC' COMMENT '公共仓库类型:DOMESTIC、FBA、OVERSEAS',
|
warehouse_type VARCHAR(32) NOT NULL DEFAULT 'DOMESTIC' COMMENT '公共仓库类型:DOMESTIC国内仓、FBA亚马逊仓、OVERSEAS普通海外仓、OVERSEAS_PARTNER海外合作仓',
|
||||||
country_code CHAR(2) NOT NULL COMMENT '所在国家二字码',
|
country_code CHAR(2) NOT NULL COMMENT '所在国家二字码',
|
||||||
timezone VARCHAR(64) NOT NULL COMMENT '仓库所在地UTC偏移量',
|
timezone VARCHAR(64) NOT NULL COMMENT '仓库所在地UTC偏移量',
|
||||||
contact_name VARCHAR(64) DEFAULT NULL COMMENT '仓库联系人',
|
contact_name VARCHAR(64) DEFAULT NULL COMMENT '仓库联系人',
|
||||||
@@ -9636,21 +9636,28 @@ BEGIN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='港务船期历史不可删除';
|
|||||||
DELIMITER ;
|
DELIMITER ;
|
||||||
|
|
||||||
USE baiyue_iam;
|
USE baiyue_iam;
|
||||||
|
DELETE role_permission FROM iam_role_permission role_permission
|
||||||
|
JOIN iam_permission permission ON permission.id=role_permission.permission_id
|
||||||
|
WHERE permission.tenant_id=0
|
||||||
|
AND permission.permission_code='platform:firstleg:port-schedule:view';
|
||||||
|
DELETE FROM iam_permission
|
||||||
|
WHERE tenant_id=0 AND permission_code='platform:firstleg:port-schedule:view';
|
||||||
|
|
||||||
DROP PROCEDURE IF EXISTS assert_port_schedule_permission_ids_available;
|
DROP PROCEDURE IF EXISTS assert_port_schedule_permission_ids_available;
|
||||||
DELIMITER $$
|
DELIMITER $$
|
||||||
CREATE PROCEDURE assert_port_schedule_permission_ids_available()
|
CREATE PROCEDURE assert_port_schedule_permission_ids_available()
|
||||||
BEGIN
|
BEGIN
|
||||||
IF EXISTS (
|
IF EXISTS (
|
||||||
SELECT 1 FROM iam_permission
|
SELECT 1 FROM iam_permission
|
||||||
WHERE (id BETWEEN 5102180 AND 5102183
|
WHERE (id IN (5102180,5102182,5102183)
|
||||||
AND permission_code <> CASE id
|
AND permission_code <> CASE id
|
||||||
WHEN 5102180 THEN 'platform:firstleg:port-schedule:list'
|
WHEN 5102180 THEN 'platform:firstleg:port-schedule:list'
|
||||||
WHEN 5102181 THEN 'platform:firstleg:port-schedule:view'
|
|
||||||
WHEN 5102182 THEN 'platform:firstleg:port-schedule:create'
|
WHEN 5102182 THEN 'platform:firstleg:port-schedule:create'
|
||||||
WHEN 5102183 THEN 'platform:firstleg:port-schedule:edit'
|
WHEN 5102183 THEN 'platform:firstleg:port-schedule:edit'
|
||||||
END)
|
END)
|
||||||
|
OR id=5102181
|
||||||
OR (permission_code LIKE 'platform:firstleg:port-schedule:%'
|
OR (permission_code LIKE 'platform:firstleg:port-schedule:%'
|
||||||
AND (tenant_id <> 0 OR app_code <> 'PLATFORM' OR id NOT BETWEEN 5102180 AND 5102183))
|
AND (tenant_id <> 0 OR app_code <> 'PLATFORM' OR id NOT IN (5102180,5102182,5102183)))
|
||||||
) THEN
|
) THEN
|
||||||
SIGNAL SQLSTATE '45000'
|
SIGNAL SQLSTATE '45000'
|
||||||
SET MESSAGE_TEXT = '三期港务船期权限迁移失败:5102180-5102183 ID 或权限码被其他语义占用';
|
SET MESSAGE_TEXT = '三期港务船期权限迁移失败:5102180-5102183 ID 或权限码被其他语义占用';
|
||||||
@@ -9665,7 +9672,6 @@ INSERT INTO iam_permission
|
|||||||
route_path,http_method,sort_no,status,built_in,deleted)
|
route_path,http_method,sort_no,status,built_in,deleted)
|
||||||
VALUES
|
VALUES
|
||||||
(5102180,0,'PLATFORM','MENU',5102000,'platform:firstleg:port-schedule:list','港务船期','/first-leg/port-schedules','GET',120,'ACTIVE',1,0),
|
(5102180,0,'PLATFORM','MENU',5102000,'platform:firstleg:port-schedule:list','港务船期','/first-leg/port-schedules','GET',120,'ACTIVE',1,0),
|
||||||
(5102181,0,'PLATFORM','BUTTON',5102180,'platform:firstleg:port-schedule:view','查看港务船期',NULL,'GET',10,'ACTIVE',1,0),
|
|
||||||
(5102182,0,'PLATFORM','BUTTON',5102180,'platform:firstleg:port-schedule:create','新增港务船期',NULL,'POST',20,'ACTIVE',1,0),
|
(5102182,0,'PLATFORM','BUTTON',5102180,'platform:firstleg:port-schedule:create','新增港务船期',NULL,'POST',20,'ACTIVE',1,0),
|
||||||
(5102183,0,'PLATFORM','BUTTON',5102180,'platform:firstleg:port-schedule:edit','更新港务船期',NULL,'PUT',30,'ACTIVE',1,0)
|
(5102183,0,'PLATFORM','BUTTON',5102180,'platform:firstleg:port-schedule:edit','更新港务船期',NULL,'PUT',30,'ACTIVE',1,0)
|
||||||
ON DUPLICATE KEY UPDATE tenant_id=0,app_code='PLATFORM',permission_type=VALUES(permission_type),
|
ON DUPLICATE KEY UPDATE tenant_id=0,app_code='PLATFORM',permission_type=VALUES(permission_type),
|
||||||
@@ -10375,22 +10381,29 @@ FOR EACH ROW BEGIN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='船司查询历史
|
|||||||
DELIMITER ;
|
DELIMITER ;
|
||||||
|
|
||||||
USE baiyue_iam;
|
USE baiyue_iam;
|
||||||
|
DELETE role_permission FROM iam_role_permission role_permission
|
||||||
|
JOIN iam_permission permission ON permission.id=role_permission.permission_id
|
||||||
|
WHERE permission.tenant_id=0
|
||||||
|
AND permission.permission_code='platform:firstleg:port-schedule-query:view';
|
||||||
|
DELETE FROM iam_permission
|
||||||
|
WHERE tenant_id=0 AND permission_code='platform:firstleg:port-schedule-query:view';
|
||||||
|
|
||||||
DROP PROCEDURE IF EXISTS assert_carrier_schedule_query_permission_ids;
|
DROP PROCEDURE IF EXISTS assert_carrier_schedule_query_permission_ids;
|
||||||
DELIMITER $$
|
DELIMITER $$
|
||||||
CREATE PROCEDURE assert_carrier_schedule_query_permission_ids()
|
CREATE PROCEDURE assert_carrier_schedule_query_permission_ids()
|
||||||
BEGIN
|
BEGIN
|
||||||
IF EXISTS (
|
IF EXISTS (
|
||||||
SELECT 1 FROM iam_permission
|
SELECT 1 FROM iam_permission
|
||||||
WHERE (id BETWEEN 5102184 AND 5102188
|
WHERE (id IN (5102184,5102186,5102187,5102188)
|
||||||
AND permission_code <> CASE id
|
AND permission_code <> CASE id
|
||||||
WHEN 5102184 THEN 'platform:firstleg:port-schedule-query:list'
|
WHEN 5102184 THEN 'platform:firstleg:port-schedule-query:list'
|
||||||
WHEN 5102185 THEN 'platform:firstleg:port-schedule-query:view'
|
|
||||||
WHEN 5102186 THEN 'platform:firstleg:port-schedule-query:create'
|
WHEN 5102186 THEN 'platform:firstleg:port-schedule-query:create'
|
||||||
WHEN 5102187 THEN 'platform:firstleg:port-schedule-query:attempt'
|
WHEN 5102187 THEN 'platform:firstleg:port-schedule-query:attempt'
|
||||||
WHEN 5102188 THEN 'platform:firstleg:port-schedule-query:manual'
|
WHEN 5102188 THEN 'platform:firstleg:port-schedule-query:manual'
|
||||||
END)
|
END)
|
||||||
|
OR id=5102185
|
||||||
OR (permission_code LIKE 'platform:firstleg:port-schedule-query:%'
|
OR (permission_code LIKE 'platform:firstleg:port-schedule-query:%'
|
||||||
AND (tenant_id<>0 OR app_code<>'PLATFORM' OR id NOT BETWEEN 5102184 AND 5102188))
|
AND (tenant_id<>0 OR app_code<>'PLATFORM' OR id NOT IN (5102184,5102186,5102187,5102188)))
|
||||||
) THEN
|
) THEN
|
||||||
SIGNAL SQLSTATE '45000'
|
SIGNAL SQLSTATE '45000'
|
||||||
SET MESSAGE_TEXT='三期船司查询权限迁移失败:5102184-5102188 ID或权限码被占用';
|
SET MESSAGE_TEXT='三期船司查询权限迁移失败:5102184-5102188 ID或权限码被占用';
|
||||||
@@ -10405,7 +10418,6 @@ INSERT INTO iam_permission
|
|||||||
route_path,http_method,sort_no,status,built_in,deleted)
|
route_path,http_method,sort_no,status,built_in,deleted)
|
||||||
VALUES
|
VALUES
|
||||||
(5102184,0,'PLATFORM','BUTTON',5102180,'platform:firstleg:port-schedule-query:list','查询船司任务',NULL,'GET',40,'ACTIVE',1,0),
|
(5102184,0,'PLATFORM','BUTTON',5102180,'platform:firstleg:port-schedule-query:list','查询船司任务',NULL,'GET',40,'ACTIVE',1,0),
|
||||||
(5102185,0,'PLATFORM','BUTTON',5102180,'platform:firstleg:port-schedule-query:view','查看船司任务',NULL,'GET',50,'ACTIVE',1,0),
|
|
||||||
(5102186,0,'PLATFORM','BUTTON',5102180,'platform:firstleg:port-schedule-query:create','创建船司任务',NULL,'POST',60,'ACTIVE',1,0),
|
(5102186,0,'PLATFORM','BUTTON',5102180,'platform:firstleg:port-schedule-query:create','创建船司任务',NULL,'POST',60,'ACTIVE',1,0),
|
||||||
(5102187,0,'PLATFORM','BUTTON',5102180,'platform:firstleg:port-schedule-query:attempt','执行船司查询',NULL,'POST',70,'ACTIVE',1,0),
|
(5102187,0,'PLATFORM','BUTTON',5102180,'platform:firstleg:port-schedule-query:attempt','执行船司查询',NULL,'POST',70,'ACTIVE',1,0),
|
||||||
(5102188,0,'PLATFORM','BUTTON',5102180,'platform:firstleg:port-schedule-query:manual','录入人工结果',NULL,'POST',80,'ACTIVE',1,0)
|
(5102188,0,'PLATFORM','BUTTON',5102180,'platform:firstleg:port-schedule-query:manual','录入人工结果',NULL,'POST',80,'ACTIVE',1,0)
|
||||||
@@ -10433,7 +10445,7 @@ BEGIN
|
|||||||
END IF;
|
END IF;
|
||||||
IF (SELECT COUNT(*) FROM baiyue_iam.iam_permission
|
IF (SELECT COUNT(*) FROM baiyue_iam.iam_permission
|
||||||
WHERE tenant_id=0 AND id BETWEEN 5102184 AND 5102188
|
WHERE tenant_id=0 AND id BETWEEN 5102184 AND 5102188
|
||||||
AND permission_code LIKE 'platform:firstleg:port-schedule-query:%') <> 5 THEN
|
AND permission_code LIKE 'platform:firstleg:port-schedule-query:%') <> 4 THEN
|
||||||
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='船司查询任务权限未完整创建';
|
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='船司查询任务权限未完整创建';
|
||||||
END IF;
|
END IF;
|
||||||
IF (SELECT COUNT(*)
|
IF (SELECT COUNT(*)
|
||||||
@@ -10442,7 +10454,7 @@ BEGIN
|
|||||||
JOIN baiyue_iam.iam_permission permission ON permission.id=role_permission.permission_id
|
JOIN baiyue_iam.iam_permission permission ON permission.id=role_permission.permission_id
|
||||||
WHERE role.tenant_id=0 AND role.role_code IN ('PLATFORM_ADMIN','ORDER_OPERATOR')
|
WHERE role.tenant_id=0 AND role.role_code IN ('PLATFORM_ADMIN','ORDER_OPERATOR')
|
||||||
AND permission.tenant_id=0
|
AND permission.tenant_id=0
|
||||||
AND permission.permission_code LIKE 'platform:firstleg:port-schedule-query:%') <> 10 THEN
|
AND permission.permission_code LIKE 'platform:firstleg:port-schedule-query:%') <> 8 THEN
|
||||||
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='船司查询任务角色授权未完整创建';
|
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='船司查询任务角色授权未完整创建';
|
||||||
END IF;
|
END IF;
|
||||||
END$$
|
END$$
|
||||||
@@ -14221,7 +14233,7 @@ CALL queue_column_comment('baiyue_warehouse', 'wh_warehouse', 'updated_by', '最
|
|||||||
CALL queue_column_comment('baiyue_warehouse', 'wh_warehouse', 'version', '乐观锁版本号');
|
CALL queue_column_comment('baiyue_warehouse', 'wh_warehouse', 'version', '乐观锁版本号');
|
||||||
CALL queue_column_comment('baiyue_warehouse', 'wh_warehouse', 'warehouse_code', '平台内唯一仓库编码');
|
CALL queue_column_comment('baiyue_warehouse', 'wh_warehouse', 'warehouse_code', '平台内唯一仓库编码');
|
||||||
CALL queue_column_comment('baiyue_warehouse', 'wh_warehouse', 'warehouse_name', '仓库名称');
|
CALL queue_column_comment('baiyue_warehouse', 'wh_warehouse', 'warehouse_name', '仓库名称');
|
||||||
CALL queue_column_comment('baiyue_warehouse', 'wh_warehouse', 'warehouse_type', '公共仓库类型:DOMESTIC、FBA、OVERSEAS');
|
CALL queue_column_comment('baiyue_warehouse', 'wh_warehouse', 'warehouse_type', '公共仓库类型:DOMESTIC国内仓、FBA亚马逊仓、OVERSEAS普通海外仓、OVERSEAS_PARTNER海外合作仓');
|
||||||
CALL queue_column_comment('baiyue_warehouse', 'wh_warehouse_address', 'address_line1', '主地址行');
|
CALL queue_column_comment('baiyue_warehouse', 'wh_warehouse_address', 'address_line1', '主地址行');
|
||||||
CALL queue_column_comment('baiyue_warehouse', 'wh_warehouse_address', 'address_line2', '补充地址行');
|
CALL queue_column_comment('baiyue_warehouse', 'wh_warehouse_address', 'address_line2', '补充地址行');
|
||||||
CALL queue_column_comment('baiyue_warehouse', 'wh_warehouse_address', 'address_type', '地址类型,例如OPERATING或RETURN');
|
CALL queue_column_comment('baiyue_warehouse', 'wh_warehouse_address', 'address_type', '地址类型,例如OPERATING或RETURN');
|
||||||
|
|||||||
+3
-3
@@ -41,7 +41,7 @@ public class PlatformCarrierScheduleQueryController {
|
|||||||
|
|
||||||
@Operation(summary = "查询六家船公司合规适配策略")
|
@Operation(summary = "查询六家船公司合规适配策略")
|
||||||
@GetMapping("/carriers")
|
@GetMapping("/carriers")
|
||||||
@PreAuthorize("hasAuthority('platform:firstleg:port-schedule-query:view')")
|
@PreAuthorize("hasAuthority('platform:firstleg:port-schedule-query:list')")
|
||||||
public ApiResponse<List<CarrierDescriptor>> carriers(@AuthenticationPrincipal AuthenticatedUser user) {
|
public ApiResponse<List<CarrierDescriptor>> carriers(@AuthenticationPrincipal AuthenticatedUser user) {
|
||||||
FirstlegScopeGuard.platform(user);
|
FirstlegScopeGuard.platform(user);
|
||||||
return ApiResponse.success(service.carriers());
|
return ApiResponse.success(service.carriers());
|
||||||
@@ -57,7 +57,7 @@ public class PlatformCarrierScheduleQueryController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/{queryId}")
|
@GetMapping("/{queryId}")
|
||||||
@PreAuthorize("hasAuthority('platform:firstleg:port-schedule-query:view')")
|
@PreAuthorize("hasAuthority('platform:firstleg:port-schedule-query:list')")
|
||||||
public ApiResponse<Detail> detail(@AuthenticationPrincipal AuthenticatedUser user,
|
public ApiResponse<Detail> detail(@AuthenticationPrincipal AuthenticatedUser user,
|
||||||
@PathVariable @Positive long queryId) {
|
@PathVariable @Positive long queryId) {
|
||||||
FirstlegScopeGuard.platform(user, service.customerId(queryId));
|
FirstlegScopeGuard.platform(user, service.customerId(queryId));
|
||||||
@@ -65,7 +65,7 @@ public class PlatformCarrierScheduleQueryController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/{queryId}/history")
|
@GetMapping("/{queryId}/history")
|
||||||
@PreAuthorize("hasAuthority('platform:firstleg:port-schedule-query:view')")
|
@PreAuthorize("hasAuthority('platform:firstleg:port-schedule-query:list')")
|
||||||
public ApiResponse<List<CarrierScheduleQueryHistoryEntity>> history(
|
public ApiResponse<List<CarrierScheduleQueryHistoryEntity>> history(
|
||||||
@AuthenticationPrincipal AuthenticatedUser user, @PathVariable @Positive long queryId) {
|
@AuthenticationPrincipal AuthenticatedUser user, @PathVariable @Positive long queryId) {
|
||||||
FirstlegScopeGuard.platform(user, service.customerId(queryId));
|
FirstlegScopeGuard.platform(user, service.customerId(queryId));
|
||||||
|
|||||||
+2
-2
@@ -47,7 +47,7 @@ public class PlatformPortScheduleController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/{portScheduleId}")
|
@GetMapping("/{portScheduleId}")
|
||||||
@PreAuthorize("hasAuthority('platform:firstleg:port-schedule:view')")
|
@PreAuthorize("hasAuthority('platform:firstleg:port-schedule:list')")
|
||||||
public ApiResponse<Detail> detail(@AuthenticationPrincipal AuthenticatedUser user,
|
public ApiResponse<Detail> detail(@AuthenticationPrincipal AuthenticatedUser user,
|
||||||
@PathVariable @Positive long portScheduleId) {
|
@PathVariable @Positive long portScheduleId) {
|
||||||
FirstlegScopeGuard.platform(user);
|
FirstlegScopeGuard.platform(user);
|
||||||
@@ -55,7 +55,7 @@ public class PlatformPortScheduleController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/{portScheduleId}/history")
|
@GetMapping("/{portScheduleId}/history")
|
||||||
@PreAuthorize("hasAuthority('platform:firstleg:port-schedule:view')")
|
@PreAuthorize("hasAuthority('platform:firstleg:port-schedule:list')")
|
||||||
public ApiResponse<List<PortScheduleHistoryEntity>> history(@AuthenticationPrincipal AuthenticatedUser user,
|
public ApiResponse<List<PortScheduleHistoryEntity>> history(@AuthenticationPrincipal AuthenticatedUser user,
|
||||||
@PathVariable @Positive long portScheduleId) {
|
@PathVariable @Positive long portScheduleId) {
|
||||||
FirstlegScopeGuard.platform(user);
|
FirstlegScopeGuard.platform(user);
|
||||||
|
|||||||
@@ -150,22 +150,29 @@ FOR EACH ROW BEGIN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='船司查询历史
|
|||||||
DELIMITER ;
|
DELIMITER ;
|
||||||
|
|
||||||
USE baiyue_iam;
|
USE baiyue_iam;
|
||||||
|
DELETE role_permission FROM iam_role_permission role_permission
|
||||||
|
JOIN iam_permission permission ON permission.id=role_permission.permission_id
|
||||||
|
WHERE permission.tenant_id=0
|
||||||
|
AND permission.permission_code='platform:firstleg:port-schedule-query:view';
|
||||||
|
DELETE FROM iam_permission
|
||||||
|
WHERE tenant_id=0 AND permission_code='platform:firstleg:port-schedule-query:view';
|
||||||
|
|
||||||
DROP PROCEDURE IF EXISTS assert_carrier_schedule_query_permission_ids;
|
DROP PROCEDURE IF EXISTS assert_carrier_schedule_query_permission_ids;
|
||||||
DELIMITER $$
|
DELIMITER $$
|
||||||
CREATE PROCEDURE assert_carrier_schedule_query_permission_ids()
|
CREATE PROCEDURE assert_carrier_schedule_query_permission_ids()
|
||||||
BEGIN
|
BEGIN
|
||||||
IF EXISTS (
|
IF EXISTS (
|
||||||
SELECT 1 FROM iam_permission
|
SELECT 1 FROM iam_permission
|
||||||
WHERE (id BETWEEN 5102184 AND 5102188
|
WHERE (id IN (5102184,5102186,5102187,5102188)
|
||||||
AND permission_code <> CASE id
|
AND permission_code <> CASE id
|
||||||
WHEN 5102184 THEN 'platform:firstleg:port-schedule-query:list'
|
WHEN 5102184 THEN 'platform:firstleg:port-schedule-query:list'
|
||||||
WHEN 5102185 THEN 'platform:firstleg:port-schedule-query:view'
|
|
||||||
WHEN 5102186 THEN 'platform:firstleg:port-schedule-query:create'
|
WHEN 5102186 THEN 'platform:firstleg:port-schedule-query:create'
|
||||||
WHEN 5102187 THEN 'platform:firstleg:port-schedule-query:attempt'
|
WHEN 5102187 THEN 'platform:firstleg:port-schedule-query:attempt'
|
||||||
WHEN 5102188 THEN 'platform:firstleg:port-schedule-query:manual'
|
WHEN 5102188 THEN 'platform:firstleg:port-schedule-query:manual'
|
||||||
END)
|
END)
|
||||||
|
OR id=5102185
|
||||||
OR (permission_code LIKE 'platform:firstleg:port-schedule-query:%'
|
OR (permission_code LIKE 'platform:firstleg:port-schedule-query:%'
|
||||||
AND (tenant_id<>0 OR app_code<>'PLATFORM' OR id NOT BETWEEN 5102184 AND 5102188))
|
AND (tenant_id<>0 OR app_code<>'PLATFORM' OR id NOT IN (5102184,5102186,5102187,5102188)))
|
||||||
) THEN
|
) THEN
|
||||||
SIGNAL SQLSTATE '45000'
|
SIGNAL SQLSTATE '45000'
|
||||||
SET MESSAGE_TEXT='三期船司查询权限迁移失败:5102184-5102188 ID或权限码被占用';
|
SET MESSAGE_TEXT='三期船司查询权限迁移失败:5102184-5102188 ID或权限码被占用';
|
||||||
@@ -180,7 +187,6 @@ INSERT INTO iam_permission
|
|||||||
route_path,http_method,sort_no,status,built_in,deleted)
|
route_path,http_method,sort_no,status,built_in,deleted)
|
||||||
VALUES
|
VALUES
|
||||||
(5102184,0,'PLATFORM','BUTTON',5102180,'platform:firstleg:port-schedule-query:list','查询船司任务',NULL,'GET',40,'ACTIVE',1,0),
|
(5102184,0,'PLATFORM','BUTTON',5102180,'platform:firstleg:port-schedule-query:list','查询船司任务',NULL,'GET',40,'ACTIVE',1,0),
|
||||||
(5102185,0,'PLATFORM','BUTTON',5102180,'platform:firstleg:port-schedule-query:view','查看船司任务',NULL,'GET',50,'ACTIVE',1,0),
|
|
||||||
(5102186,0,'PLATFORM','BUTTON',5102180,'platform:firstleg:port-schedule-query:create','创建船司任务',NULL,'POST',60,'ACTIVE',1,0),
|
(5102186,0,'PLATFORM','BUTTON',5102180,'platform:firstleg:port-schedule-query:create','创建船司任务',NULL,'POST',60,'ACTIVE',1,0),
|
||||||
(5102187,0,'PLATFORM','BUTTON',5102180,'platform:firstleg:port-schedule-query:attempt','执行船司查询',NULL,'POST',70,'ACTIVE',1,0),
|
(5102187,0,'PLATFORM','BUTTON',5102180,'platform:firstleg:port-schedule-query:attempt','执行船司查询',NULL,'POST',70,'ACTIVE',1,0),
|
||||||
(5102188,0,'PLATFORM','BUTTON',5102180,'platform:firstleg:port-schedule-query:manual','录入人工结果',NULL,'POST',80,'ACTIVE',1,0)
|
(5102188,0,'PLATFORM','BUTTON',5102180,'platform:firstleg:port-schedule-query:manual','录入人工结果',NULL,'POST',80,'ACTIVE',1,0)
|
||||||
@@ -208,7 +214,7 @@ BEGIN
|
|||||||
END IF;
|
END IF;
|
||||||
IF (SELECT COUNT(*) FROM baiyue_iam.iam_permission
|
IF (SELECT COUNT(*) FROM baiyue_iam.iam_permission
|
||||||
WHERE tenant_id=0 AND id BETWEEN 5102184 AND 5102188
|
WHERE tenant_id=0 AND id BETWEEN 5102184 AND 5102188
|
||||||
AND permission_code LIKE 'platform:firstleg:port-schedule-query:%') <> 5 THEN
|
AND permission_code LIKE 'platform:firstleg:port-schedule-query:%') <> 4 THEN
|
||||||
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='船司查询任务权限未完整创建';
|
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='船司查询任务权限未完整创建';
|
||||||
END IF;
|
END IF;
|
||||||
IF (SELECT COUNT(*)
|
IF (SELECT COUNT(*)
|
||||||
@@ -217,7 +223,7 @@ BEGIN
|
|||||||
JOIN baiyue_iam.iam_permission permission ON permission.id=role_permission.permission_id
|
JOIN baiyue_iam.iam_permission permission ON permission.id=role_permission.permission_id
|
||||||
WHERE role.tenant_id=0 AND role.role_code IN ('PLATFORM_ADMIN','ORDER_OPERATOR')
|
WHERE role.tenant_id=0 AND role.role_code IN ('PLATFORM_ADMIN','ORDER_OPERATOR')
|
||||||
AND permission.tenant_id=0
|
AND permission.tenant_id=0
|
||||||
AND permission.permission_code LIKE 'platform:firstleg:port-schedule-query:%') <> 10 THEN
|
AND permission.permission_code LIKE 'platform:firstleg:port-schedule-query:%') <> 8 THEN
|
||||||
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='船司查询任务角色授权未完整创建';
|
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='船司查询任务角色授权未完整创建';
|
||||||
END IF;
|
END IF;
|
||||||
END$$
|
END$$
|
||||||
|
|||||||
@@ -122,21 +122,28 @@ BEGIN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='港务船期历史不可删除';
|
|||||||
DELIMITER ;
|
DELIMITER ;
|
||||||
|
|
||||||
USE baiyue_iam;
|
USE baiyue_iam;
|
||||||
|
DELETE role_permission FROM iam_role_permission role_permission
|
||||||
|
JOIN iam_permission permission ON permission.id=role_permission.permission_id
|
||||||
|
WHERE permission.tenant_id=0
|
||||||
|
AND permission.permission_code='platform:firstleg:port-schedule:view';
|
||||||
|
DELETE FROM iam_permission
|
||||||
|
WHERE tenant_id=0 AND permission_code='platform:firstleg:port-schedule:view';
|
||||||
|
|
||||||
DROP PROCEDURE IF EXISTS assert_port_schedule_permission_ids_available;
|
DROP PROCEDURE IF EXISTS assert_port_schedule_permission_ids_available;
|
||||||
DELIMITER $$
|
DELIMITER $$
|
||||||
CREATE PROCEDURE assert_port_schedule_permission_ids_available()
|
CREATE PROCEDURE assert_port_schedule_permission_ids_available()
|
||||||
BEGIN
|
BEGIN
|
||||||
IF EXISTS (
|
IF EXISTS (
|
||||||
SELECT 1 FROM iam_permission
|
SELECT 1 FROM iam_permission
|
||||||
WHERE (id BETWEEN 5102180 AND 5102183
|
WHERE (id IN (5102180,5102182,5102183)
|
||||||
AND permission_code <> CASE id
|
AND permission_code <> CASE id
|
||||||
WHEN 5102180 THEN 'platform:firstleg:port-schedule:list'
|
WHEN 5102180 THEN 'platform:firstleg:port-schedule:list'
|
||||||
WHEN 5102181 THEN 'platform:firstleg:port-schedule:view'
|
|
||||||
WHEN 5102182 THEN 'platform:firstleg:port-schedule:create'
|
WHEN 5102182 THEN 'platform:firstleg:port-schedule:create'
|
||||||
WHEN 5102183 THEN 'platform:firstleg:port-schedule:edit'
|
WHEN 5102183 THEN 'platform:firstleg:port-schedule:edit'
|
||||||
END)
|
END)
|
||||||
|
OR id=5102181
|
||||||
OR (permission_code LIKE 'platform:firstleg:port-schedule:%'
|
OR (permission_code LIKE 'platform:firstleg:port-schedule:%'
|
||||||
AND (tenant_id <> 0 OR app_code <> 'PLATFORM' OR id NOT BETWEEN 5102180 AND 5102183))
|
AND (tenant_id <> 0 OR app_code <> 'PLATFORM' OR id NOT IN (5102180,5102182,5102183)))
|
||||||
) THEN
|
) THEN
|
||||||
SIGNAL SQLSTATE '45000'
|
SIGNAL SQLSTATE '45000'
|
||||||
SET MESSAGE_TEXT = '三期港务船期权限迁移失败:5102180-5102183 ID 或权限码被其他语义占用';
|
SET MESSAGE_TEXT = '三期港务船期权限迁移失败:5102180-5102183 ID 或权限码被其他语义占用';
|
||||||
@@ -151,7 +158,6 @@ INSERT INTO iam_permission
|
|||||||
route_path,http_method,sort_no,status,built_in,deleted)
|
route_path,http_method,sort_no,status,built_in,deleted)
|
||||||
VALUES
|
VALUES
|
||||||
(5102180,0,'PLATFORM','MENU',5102000,'platform:firstleg:port-schedule:list','港务船期','/first-leg/port-schedules','GET',120,'ACTIVE',1,0),
|
(5102180,0,'PLATFORM','MENU',5102000,'platform:firstleg:port-schedule:list','港务船期','/first-leg/port-schedules','GET',120,'ACTIVE',1,0),
|
||||||
(5102181,0,'PLATFORM','BUTTON',5102180,'platform:firstleg:port-schedule:view','查看港务船期',NULL,'GET',10,'ACTIVE',1,0),
|
|
||||||
(5102182,0,'PLATFORM','BUTTON',5102180,'platform:firstleg:port-schedule:create','新增港务船期',NULL,'POST',20,'ACTIVE',1,0),
|
(5102182,0,'PLATFORM','BUTTON',5102180,'platform:firstleg:port-schedule:create','新增港务船期',NULL,'POST',20,'ACTIVE',1,0),
|
||||||
(5102183,0,'PLATFORM','BUTTON',5102180,'platform:firstleg:port-schedule:edit','更新港务船期',NULL,'PUT',30,'ACTIVE',1,0)
|
(5102183,0,'PLATFORM','BUTTON',5102180,'platform:firstleg:port-schedule:edit','更新港务船期',NULL,'PUT',30,'ACTIVE',1,0)
|
||||||
ON DUPLICATE KEY UPDATE tenant_id=0,app_code='PLATFORM',permission_type=VALUES(permission_type),
|
ON DUPLICATE KEY UPDATE tenant_id=0,app_code='PLATFORM',permission_type=VALUES(permission_type),
|
||||||
|
|||||||
@@ -7,11 +7,13 @@ paths:
|
|||||||
/api/platform/v1/carrier-schedule-queries/carriers:
|
/api/platform/v1/carrier-schedule-queries/carriers:
|
||||||
get:
|
get:
|
||||||
summary: List official carrier sources and compliant access modes
|
summary: List official carrier sources and compliant access modes
|
||||||
|
x-permission: platform:firstleg:port-schedule-query:list
|
||||||
responses:
|
responses:
|
||||||
'200': {description: MATSON, SML, ZIM, COSCO, EMC and HEDE descriptors}
|
'200': {description: MATSON, SML, ZIM, COSCO, EMC and HEDE descriptors}
|
||||||
/api/platform/v1/carrier-schedule-queries:
|
/api/platform/v1/carrier-schedule-queries:
|
||||||
get:
|
get:
|
||||||
summary: Query carrier schedule tasks
|
summary: Query carrier schedule tasks
|
||||||
|
x-permission: platform:firstleg:port-schedule-query:list
|
||||||
parameters:
|
parameters:
|
||||||
- {name: customerId, in: query, schema: {type: integer, format: int64, minimum: 1}}
|
- {name: customerId, in: query, schema: {type: integer, format: int64, minimum: 1}}
|
||||||
- {name: oceanBookingId, in: query, schema: {type: integer, format: int64, minimum: 1}}
|
- {name: oceanBookingId, in: query, schema: {type: integer, format: int64, minimum: 1}}
|
||||||
@@ -26,6 +28,7 @@ paths:
|
|||||||
'200': {description: Query tasks with source, freshness and rate-limit fields}
|
'200': {description: Query tasks with source, freshness and rate-limit fields}
|
||||||
post:
|
post:
|
||||||
summary: Create a query task linked to a real booking and allocated container
|
summary: Create a query task linked to a real booking and allocated container
|
||||||
|
x-permission: platform:firstleg:port-schedule-query:create
|
||||||
parameters: [{$ref: '#/components/parameters/IdempotencyKey'}]
|
parameters: [{$ref: '#/components/parameters/IdempotencyKey'}]
|
||||||
requestBody:
|
requestBody:
|
||||||
required: true
|
required: true
|
||||||
@@ -38,18 +41,21 @@ paths:
|
|||||||
parameters: [{$ref: '#/components/parameters/QueryId'}]
|
parameters: [{$ref: '#/components/parameters/QueryId'}]
|
||||||
get:
|
get:
|
||||||
summary: Get task, immutable attempts and history
|
summary: Get task, immutable attempts and history
|
||||||
|
x-permission: platform:firstleg:port-schedule-query:list
|
||||||
responses:
|
responses:
|
||||||
'200': {description: Stable task detail}
|
'200': {description: Stable task detail}
|
||||||
/api/platform/v1/carrier-schedule-queries/{queryId}/history:
|
/api/platform/v1/carrier-schedule-queries/{queryId}/history:
|
||||||
parameters: [{$ref: '#/components/parameters/QueryId'}]
|
parameters: [{$ref: '#/components/parameters/QueryId'}]
|
||||||
get:
|
get:
|
||||||
summary: Get immutable task history
|
summary: Get immutable task history
|
||||||
|
x-permission: platform:firstleg:port-schedule-query:list
|
||||||
responses:
|
responses:
|
||||||
'200': {description: CREATE, ATTEMPT and MANUAL_RESULT history}
|
'200': {description: CREATE, ATTEMPT and MANUAL_RESULT history}
|
||||||
/api/platform/v1/carrier-schedule-queries/{queryId}/attempts:
|
/api/platform/v1/carrier-schedule-queries/{queryId}/attempts:
|
||||||
parameters: [{$ref: '#/components/parameters/QueryId'}]
|
parameters: [{$ref: '#/components/parameters/QueryId'}]
|
||||||
post:
|
post:
|
||||||
summary: Execute the compliant carrier strategy; never bypass captcha or dynamic pages
|
summary: Execute the compliant carrier strategy; never bypass captcha or dynamic pages
|
||||||
|
x-permission: platform:firstleg:port-schedule-query:attempt
|
||||||
parameters: [{$ref: '#/components/parameters/IdempotencyKey'}]
|
parameters: [{$ref: '#/components/parameters/IdempotencyKey'}]
|
||||||
requestBody:
|
requestBody:
|
||||||
required: true
|
required: true
|
||||||
@@ -62,6 +68,7 @@ paths:
|
|||||||
parameters: [{$ref: '#/components/parameters/QueryId'}]
|
parameters: [{$ref: '#/components/parameters/QueryId'}]
|
||||||
post:
|
post:
|
||||||
summary: Link a validated manual port-schedule observation without pretending automated success
|
summary: Link a validated manual port-schedule observation without pretending automated success
|
||||||
|
x-permission: platform:firstleg:port-schedule-query:manual
|
||||||
parameters: [{$ref: '#/components/parameters/IdempotencyKey'}]
|
parameters: [{$ref: '#/components/parameters/IdempotencyKey'}]
|
||||||
requestBody:
|
requestBody:
|
||||||
required: true
|
required: true
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ paths:
|
|||||||
/api/platform/v1/port-schedules:
|
/api/platform/v1/port-schedules:
|
||||||
get:
|
get:
|
||||||
summary: Query schedules by customer, ocean booking, allocated container or expiry
|
summary: Query schedules by customer, ocean booking, allocated container or expiry
|
||||||
|
x-permission: platform:firstleg:port-schedule:list
|
||||||
parameters:
|
parameters:
|
||||||
- {name: customerId, in: query, schema: {type: string, pattern: '^[1-9][0-9]*$'}}
|
- {name: customerId, in: query, schema: {type: string, pattern: '^[1-9][0-9]*$'}}
|
||||||
- {name: oceanBookingId, in: query, schema: {type: string, pattern: '^[1-9][0-9]*$'}}
|
- {name: oceanBookingId, in: query, schema: {type: string, pattern: '^[1-9][0-9]*$'}}
|
||||||
@@ -20,6 +21,7 @@ paths:
|
|||||||
description: HTTP 200 business envelope containing a page of schedule views
|
description: HTTP 200 business envelope containing a page of schedule views
|
||||||
post:
|
post:
|
||||||
summary: Create a source observation linked to a real ocean booking
|
summary: Create a source observation linked to a real ocean booking
|
||||||
|
x-permission: platform:firstleg:port-schedule:create
|
||||||
parameters: [{$ref: '#/components/parameters/IdempotencyKey'}]
|
parameters: [{$ref: '#/components/parameters/IdempotencyKey'}]
|
||||||
requestBody:
|
requestBody:
|
||||||
required: true
|
required: true
|
||||||
@@ -32,10 +34,12 @@ paths:
|
|||||||
parameters: [{$ref: '#/components/parameters/PortScheduleId'}]
|
parameters: [{$ref: '#/components/parameters/PortScheduleId'}]
|
||||||
get:
|
get:
|
||||||
summary: Get schedule and immutable history
|
summary: Get schedule and immutable history
|
||||||
|
x-permission: platform:firstleg:port-schedule:list
|
||||||
responses:
|
responses:
|
||||||
'200': {description: Schedule detail with computed expired flag}
|
'200': {description: Schedule detail with computed expired flag}
|
||||||
put:
|
put:
|
||||||
summary: Update source observation with optimistic locking
|
summary: Update source observation with optimistic locking
|
||||||
|
x-permission: platform:firstleg:port-schedule:edit
|
||||||
parameters: [{$ref: '#/components/parameters/IdempotencyKey'}]
|
parameters: [{$ref: '#/components/parameters/IdempotencyKey'}]
|
||||||
requestBody:
|
requestBody:
|
||||||
required: true
|
required: true
|
||||||
@@ -48,6 +52,7 @@ paths:
|
|||||||
parameters: [{$ref: '#/components/parameters/PortScheduleId'}]
|
parameters: [{$ref: '#/components/parameters/PortScheduleId'}]
|
||||||
get:
|
get:
|
||||||
summary: Query immutable source observation history
|
summary: Query immutable source observation history
|
||||||
|
x-permission: platform:firstleg:port-schedule:list
|
||||||
responses:
|
responses:
|
||||||
'200': {description: Ordered CREATE and UPDATE history records}
|
'200': {description: Ordered CREATE and UPDATE history records}
|
||||||
components:
|
components:
|
||||||
|
|||||||
+3
-3
@@ -17,10 +17,10 @@ class PlatformCarrierScheduleQueryControllerContractTest {
|
|||||||
assertThat(PlatformCarrierScheduleQueryController.class.getAnnotation(RequestMapping.class).value())
|
assertThat(PlatformCarrierScheduleQueryController.class.getAnnotation(RequestMapping.class).value())
|
||||||
.containsExactly("/api/platform/v1/carrier-schedule-queries");
|
.containsExactly("/api/platform/v1/carrier-schedule-queries");
|
||||||
Map<String, String> permissions = Map.of(
|
Map<String, String> permissions = Map.of(
|
||||||
"carriers", "platform:firstleg:port-schedule-query:view",
|
"carriers", "platform:firstleg:port-schedule-query:list",
|
||||||
"page", "platform:firstleg:port-schedule-query:list",
|
"page", "platform:firstleg:port-schedule-query:list",
|
||||||
"detail", "platform:firstleg:port-schedule-query:view",
|
"detail", "platform:firstleg:port-schedule-query:list",
|
||||||
"history", "platform:firstleg:port-schedule-query:view",
|
"history", "platform:firstleg:port-schedule-query:list",
|
||||||
"create", "platform:firstleg:port-schedule-query:create",
|
"create", "platform:firstleg:port-schedule-query:create",
|
||||||
"attempt", "platform:firstleg:port-schedule-query:attempt",
|
"attempt", "platform:firstleg:port-schedule-query:attempt",
|
||||||
"manualResult", "platform:firstleg:port-schedule-query:manual");
|
"manualResult", "platform:firstleg:port-schedule-query:manual");
|
||||||
|
|||||||
+2
-2
@@ -18,8 +18,8 @@ class PlatformPortScheduleControllerContractTest {
|
|||||||
.containsExactly("/api/platform/v1/port-schedules");
|
.containsExactly("/api/platform/v1/port-schedules");
|
||||||
Map<String, String> permissions = Map.of(
|
Map<String, String> permissions = Map.of(
|
||||||
"page", "platform:firstleg:port-schedule:list",
|
"page", "platform:firstleg:port-schedule:list",
|
||||||
"detail", "platform:firstleg:port-schedule:view",
|
"detail", "platform:firstleg:port-schedule:list",
|
||||||
"history", "platform:firstleg:port-schedule:view",
|
"history", "platform:firstleg:port-schedule:list",
|
||||||
"create", "platform:firstleg:port-schedule:create",
|
"create", "platform:firstleg:port-schedule:create",
|
||||||
"update", "platform:firstleg:port-schedule:edit");
|
"update", "platform:firstleg:port-schedule:edit");
|
||||||
permissions.forEach((name, permission) -> {
|
permissions.forEach((name, permission) -> {
|
||||||
|
|||||||
+4
-2
@@ -37,9 +37,11 @@ class PortScheduleSqlContractTest {
|
|||||||
@Test
|
@Test
|
||||||
void reservesDedicatedPlatformPermissionsForBothOperationalRoles() throws Exception {
|
void reservesDedicatedPlatformPermissionsForBothOperationalRoles() throws Exception {
|
||||||
assertThat(sql())
|
assertThat(sql())
|
||||||
.contains("5102180 AND 5102183")
|
.contains("id IN (5102180,5102182,5102183)")
|
||||||
.contains("platform:firstleg:port-schedule:list")
|
.contains("platform:firstleg:port-schedule:list")
|
||||||
.contains("platform:firstleg:port-schedule:view")
|
.contains("permission.permission_code='platform:firstleg:port-schedule:view'")
|
||||||
|
.contains("permission_code='platform:firstleg:port-schedule:view'")
|
||||||
|
.doesNotContain("'platform:firstleg:port-schedule:view','查看港务船期'")
|
||||||
.contains("platform:firstleg:port-schedule:create")
|
.contains("platform:firstleg:port-schedule:create")
|
||||||
.contains("platform:firstleg:port-schedule:edit")
|
.contains("platform:firstleg:port-schedule:edit")
|
||||||
.contains("role.role_code IN ('PLATFORM_ADMIN','ORDER_OPERATOR')");
|
.contains("role.role_code IN ('PLATFORM_ADMIN','ORDER_OPERATOR')");
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
- 负责海外 API 统一入口、路由、RS256/JWKS 验签、Audience 和必要 Claims 校验。
|
- 负责海外 API 统一入口、路由、RS256/JWKS 验签、Audience 和必要 Claims 校验。
|
||||||
- 不保存签名私钥,不依赖国内 Redis 会话,不在网关实现尾程业务逻辑。
|
- 不保存签名私钥,不依赖国内 Redis 会话,不在网关实现尾程业务逻辑。
|
||||||
|
- 修改海外 Token、Audience、JWKS、区域路由或服务身份时,先读 `../../docs/跨区域认证与服务通信契约.md`。
|
||||||
|
|
||||||
## 快速索引
|
## 快速索引
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
- 负责客户主档、成员、企业资料/审核、平台用户、角色权限、数据范围和授权快照。
|
- 负责客户主档、成员、企业资料/审核、平台用户、角色权限、数据范围和授权快照。
|
||||||
- 账号凭据/Token 属于 `auth/`,文件内容属于 `file/`;资质登记只接收 File 的内部元数据调用。
|
- 账号凭据/Token 属于 `auth/`,文件内容属于 `file/`;资质登记只接收 File 的内部元数据调用。
|
||||||
|
- 修改用户可见层级、菜单权限、列表/详情只读语义或权限资源前,先读 `../../docs/平台客户层级与权限契约.md`。
|
||||||
- 当前仅“平台—客户”两级:`iam_tenant.id=0` 为平台作用域,`id>0` 为客户主档;不得新增真实租户层。
|
- 当前仅“平台—客户”两级:`iam_tenant.id=0` 为平台作用域,`id>0` 为客户主档;不得新增真实租户层。
|
||||||
|
|
||||||
## 模块入口
|
## 模块入口
|
||||||
|
|||||||
+9
@@ -4,6 +4,7 @@ import com.baiyue.common.core.api.ApiResponse;
|
|||||||
import com.baiyue.common.core.api.ResultCode;
|
import com.baiyue.common.core.api.ResultCode;
|
||||||
import com.baiyue.common.core.exception.BizException;
|
import com.baiyue.common.core.exception.BizException;
|
||||||
import com.baiyue.warehouse.dto.WarehouseDetailResponse;
|
import com.baiyue.warehouse.dto.WarehouseDetailResponse;
|
||||||
|
import com.baiyue.warehouse.dto.OverseasPartnerWarehouseResponse;
|
||||||
import com.baiyue.warehouse.service.WarehouseApplicationService;
|
import com.baiyue.warehouse.service.WarehouseApplicationService;
|
||||||
import jakarta.annotation.Resource;
|
import jakarta.annotation.Resource;
|
||||||
import jakarta.annotation.PostConstruct;
|
import jakarta.annotation.PostConstruct;
|
||||||
@@ -11,6 +12,7 @@ import org.springframework.beans.factory.annotation.Value;
|
|||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.security.MessageDigest;
|
import java.security.MessageDigest;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/internal/warehouses")
|
@RequestMapping("/internal/warehouses")
|
||||||
@@ -39,6 +41,13 @@ public class InternalWarehouseController {
|
|||||||
return ApiResponse.success(service.activeDetail(PLATFORM_TENANT_ID, id));
|
return ApiResponse.success(service.activeDetail(PLATFORM_TENANT_ID, id));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GetMapping("/overseas-partners")
|
||||||
|
public ApiResponse<List<OverseasPartnerWarehouseResponse>> overseasPartners(
|
||||||
|
@RequestHeader(value="X-Internal-Service-Token", required=false) String token) {
|
||||||
|
verifyToken(token);
|
||||||
|
return ApiResponse.success(service.activeOverseasPartners());
|
||||||
|
}
|
||||||
|
|
||||||
private void verifyToken(String token) {
|
private void verifyToken(String token) {
|
||||||
byte[] supplied = token == null ? new byte[0] : token.getBytes(StandardCharsets.UTF_8);
|
byte[] supplied = token == null ? new byte[0] : token.getBytes(StandardCharsets.UTF_8);
|
||||||
if (!MessageDigest.isEqual(expected, supplied)) {
|
if (!MessageDigest.isEqual(expected, supplied)) {
|
||||||
|
|||||||
+17
@@ -0,0 +1,17 @@
|
|||||||
|
package com.baiyue.warehouse.dto;
|
||||||
|
|
||||||
|
import com.baiyue.warehouse.entity.WarehouseEntity;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
|
||||||
|
@Schema(name = "OverseasPartnerWarehouseResponse", description = "Active overseas partner warehouse option / 启用的海外合作仓候选")
|
||||||
|
public record OverseasPartnerWarehouseResponse(
|
||||||
|
@Schema(description = "Platform public warehouse ID / 平台公共仓库 ID") Long id,
|
||||||
|
@Schema(description = "Warehouse code / 仓库编码") String warehouseCode,
|
||||||
|
@Schema(description = "Warehouse name / 仓库名称") String warehouseName,
|
||||||
|
@Schema(description = "ISO two-letter country code / 国家二字码") String countryCode
|
||||||
|
) {
|
||||||
|
public static OverseasPartnerWarehouseResponse of(WarehouseEntity warehouse) {
|
||||||
|
return new OverseasPartnerWarehouseResponse(warehouse.getId(), warehouse.getWarehouseCode(),
|
||||||
|
warehouse.getWarehouseName(), warehouse.getCountryCode());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -29,8 +29,8 @@ public final class WarehouseRequests {
|
|||||||
@NotBlank @Size(max = 50) String warehouseCode,
|
@NotBlank @Size(max = 50) String warehouseCode,
|
||||||
@Schema(description = "Warehouse name / 仓库名称", example = "Los Angeles Main Warehouse / 洛杉矶主仓", maxLength = 120, requiredMode = Schema.RequiredMode.REQUIRED)
|
@Schema(description = "Warehouse name / 仓库名称", example = "Los Angeles Main Warehouse / 洛杉矶主仓", maxLength = 120, requiredMode = Schema.RequiredMode.REQUIRED)
|
||||||
@NotBlank @Size(max = 120) String warehouseName,
|
@NotBlank @Size(max = 120) String warehouseName,
|
||||||
@Schema(description = "Warehouse type; defaults to DOMESTIC / 仓库类型,默认 DOMESTIC", example = "FBA", allowableValues = {"DOMESTIC", "FBA", "OVERSEAS"}, defaultValue = "DOMESTIC")
|
@Schema(description = "Warehouse type; defaults to DOMESTIC / 仓库类型,默认 DOMESTIC", example = "OVERSEAS_PARTNER", allowableValues = {"DOMESTIC", "FBA", "OVERSEAS", "OVERSEAS_PARTNER"}, defaultValue = "DOMESTIC")
|
||||||
@Pattern(regexp = "DOMESTIC|FBA|OVERSEAS") String warehouseType,
|
@Pattern(regexp = "DOMESTIC|FBA|OVERSEAS|OVERSEAS_PARTNER") String warehouseType,
|
||||||
@Schema(description = "ISO 3166-1 alpha-2 country code; DOMESTIC defaults to CN / ISO 两位国家代码,DOMESTIC 默认 CN", example = "US", pattern = "[A-Z]{2}")
|
@Schema(description = "ISO 3166-1 alpha-2 country code; DOMESTIC defaults to CN / ISO 两位国家代码,DOMESTIC 默认 CN", example = "US", pattern = "[A-Z]{2}")
|
||||||
@Pattern(regexp = "[A-Z]{2}") String countryCode,
|
@Pattern(regexp = "[A-Z]{2}") String countryCode,
|
||||||
@Schema(description = "UTC offset; DOMESTIC defaults to UTC+8 / UTC 偏移量,DOMESTIC 默认 UTC+8", example = "UTC-8")
|
@Schema(description = "UTC offset; DOMESTIC defaults to UTC+8 / UTC 偏移量,DOMESTIC 默认 UTC+8", example = "UTC-8")
|
||||||
@@ -104,8 +104,8 @@ public final class WarehouseRequests {
|
|||||||
String countryCode,
|
String countryCode,
|
||||||
@Schema(description = "Enabled capability code filter / 已启用能力编码筛选", example = "STORAGE")
|
@Schema(description = "Enabled capability code filter / 已启用能力编码筛选", example = "STORAGE")
|
||||||
String capabilityCode,
|
String capabilityCode,
|
||||||
@Schema(description = "Warehouse type filter / 仓库类型筛选", example = "DOMESTIC", allowableValues = {"DOMESTIC", "FBA", "OVERSEAS"})
|
@Schema(description = "Warehouse type filter / 仓库类型筛选", example = "OVERSEAS_PARTNER", allowableValues = {"DOMESTIC", "FBA", "OVERSEAS", "OVERSEAS_PARTNER"})
|
||||||
@Pattern(regexp = "DOMESTIC|FBA|OVERSEAS") String warehouseType,
|
@Pattern(regexp = "DOMESTIC|FBA|OVERSEAS|OVERSEAS_PARTNER") String warehouseType,
|
||||||
@Schema(description = "Status filter / 状态筛选", example = "ACTIVE", allowableValues = {"ACTIVE", "INACTIVE"})
|
@Schema(description = "Status filter / 状态筛选", example = "ACTIVE", allowableValues = {"ACTIVE", "INACTIVE"})
|
||||||
@Pattern(regexp = "ACTIVE|INACTIVE") String status
|
@Pattern(regexp = "ACTIVE|INACTIVE") String status
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ public record WarehouseSummaryResponse(
|
|||||||
@Schema(description = "Platform public scope ID, fixed to 0 / 平台公共作用域,固定为 0") Long tenantId,
|
@Schema(description = "Platform public scope ID, fixed to 0 / 平台公共作用域,固定为 0") Long tenantId,
|
||||||
@Schema(description = "Warehouse code / 仓库编码") String warehouseCode,
|
@Schema(description = "Warehouse code / 仓库编码") String warehouseCode,
|
||||||
@Schema(description = "Warehouse name / 仓库名称") String warehouseName,
|
@Schema(description = "Warehouse name / 仓库名称") String warehouseName,
|
||||||
@Schema(description = "Warehouse type / 仓库类型", allowableValues = {"DOMESTIC", "FBA", "OVERSEAS"}) String warehouseType,
|
@Schema(description = "Warehouse type / 仓库类型", allowableValues = {"DOMESTIC", "FBA", "OVERSEAS", "OVERSEAS_PARTNER"}) String warehouseType,
|
||||||
@Schema(description = "ISO two-letter country code / 国家二字码") String countryCode,
|
@Schema(description = "ISO two-letter country code / 国家二字码") String countryCode,
|
||||||
@Schema(description = "UTC offset / UTC 偏移量") String timezone,
|
@Schema(description = "UTC offset / UTC 偏移量") String timezone,
|
||||||
@Schema(description = "Contact name / 联系人") String contactName,
|
@Schema(description = "Contact name / 联系人") String contactName,
|
||||||
|
|||||||
@@ -27,7 +27,8 @@ public class WarehouseEntity extends BaseEntity {
|
|||||||
private String warehouseCode;
|
private String warehouseCode;
|
||||||
@Schema(description = "Warehouse name / 仓库名称", example = "Los Angeles Main Warehouse / 洛杉矶主仓")
|
@Schema(description = "Warehouse name / 仓库名称", example = "Los Angeles Main Warehouse / 洛杉矶主仓")
|
||||||
private String warehouseName;
|
private String warehouseName;
|
||||||
@Schema(description = "Warehouse type / 仓库类型", example = "OVERSEAS")
|
@Schema(description = "Warehouse type / 仓库类型", example = "OVERSEAS_PARTNER",
|
||||||
|
allowableValues = {"DOMESTIC", "FBA", "OVERSEAS", "OVERSEAS_PARTNER"})
|
||||||
private String warehouseType;
|
private String warehouseType;
|
||||||
@Schema(description = "ISO two-letter country code / ISO 两位国家代码", example = "US")
|
@Schema(description = "ISO two-letter country code / ISO 两位国家代码", example = "US")
|
||||||
private String countryCode;
|
private String countryCode;
|
||||||
|
|||||||
@@ -59,6 +59,15 @@ public class WarehouseRepository {
|
|||||||
.eq(WarehouseEntity::getStatus, "ACTIVE"));
|
.eq(WarehouseEntity::getStatus, "ACTIVE"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public List<WarehouseEntity> findActiveByType(long tenantId, String warehouseType) {
|
||||||
|
return warehouses.selectList(new LambdaQueryWrapper<WarehouseEntity>()
|
||||||
|
.eq(WarehouseEntity::getTenantId, tenantId)
|
||||||
|
.eq(WarehouseEntity::getWarehouseType, warehouseType)
|
||||||
|
.eq(WarehouseEntity::getStatus, "ACTIVE")
|
||||||
|
.orderByAsc(WarehouseEntity::getWarehouseName)
|
||||||
|
.orderByAsc(WarehouseEntity::getId));
|
||||||
|
}
|
||||||
|
|
||||||
public boolean existsCode(long tenantId, String code, Long excludedId) {
|
public boolean existsCode(long tenantId, String code, Long excludedId) {
|
||||||
return warehouses.selectCount(new LambdaQueryWrapper<WarehouseEntity>()
|
return warehouses.selectCount(new LambdaQueryWrapper<WarehouseEntity>()
|
||||||
.eq(WarehouseEntity::getTenantId, tenantId)
|
.eq(WarehouseEntity::getTenantId, tenantId)
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import com.baiyue.common.core.api.ResultCode;
|
|||||||
import com.baiyue.common.core.exception.BizException;
|
import com.baiyue.common.core.exception.BizException;
|
||||||
import com.baiyue.warehouse.dto.WarehouseDetailResponse;
|
import com.baiyue.warehouse.dto.WarehouseDetailResponse;
|
||||||
import com.baiyue.warehouse.dto.WarehouseRequests;
|
import com.baiyue.warehouse.dto.WarehouseRequests;
|
||||||
|
import com.baiyue.warehouse.dto.OverseasPartnerWarehouseResponse;
|
||||||
import com.baiyue.warehouse.dto.WarehouseSummaryResponse;
|
import com.baiyue.warehouse.dto.WarehouseSummaryResponse;
|
||||||
import com.baiyue.warehouse.entity.ServiceCapabilityEntity;
|
import com.baiyue.warehouse.entity.ServiceCapabilityEntity;
|
||||||
import com.baiyue.warehouse.entity.WarehouseAddressEntity;
|
import com.baiyue.warehouse.entity.WarehouseAddressEntity;
|
||||||
@@ -86,6 +87,14 @@ public class WarehouseApplicationService {
|
|||||||
return repository.activePage(tenantId, query);
|
return repository.activePage(tenantId, query);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public List<OverseasPartnerWarehouseResponse> activeOverseasPartners() {
|
||||||
|
return repository.findActiveByType(WarehouseSaveNormalizer.PUBLIC_TENANT_ID,
|
||||||
|
WarehouseSaveNormalizer.TYPE_OVERSEAS_PARTNER)
|
||||||
|
.stream()
|
||||||
|
.map(OverseasPartnerWarehouseResponse::of)
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
public WarehouseDetailResponse activeDetail(long tenantId, long id) {
|
public WarehouseDetailResponse activeDetail(long tenantId, long id) {
|
||||||
WarehouseEntity warehouse = repository.findOwned(tenantId, id);
|
WarehouseEntity warehouse = repository.findOwned(tenantId, id);
|
||||||
if (warehouse == null) {
|
if (warehouse == null) {
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ public class WarehouseImportService {
|
|||||||
Row instruction = sheet.createRow(1);
|
Row instruction = sheet.createRow(1);
|
||||||
instruction.createCell(0).setCellValue("说明");
|
instruction.createCell(0).setCellValue("说明");
|
||||||
instruction.createCell(1).setCellValue(
|
instruction.createCell(1).setCellValue(
|
||||||
"默认仅新增;仓库类型默认 DOMESTIC,状态默认 ACTIVE;DOMESTIC 默认 CN/UTC+8;FBA 名称可不填并默认使用编码,多时区州必须显式填写时区;更新不覆盖联系人和备注");
|
"默认仅新增;仓库类型默认 DOMESTIC,可填 OVERSEAS_PARTNER 表示海外合作仓;状态默认 ACTIVE;DOMESTIC 默认 CN/UTC+8;FBA 名称可不填并默认使用编码,多时区州必须显式填写时区;更新不覆盖联系人和备注");
|
||||||
Row header = sheet.createRow(2);
|
Row header = sheet.createRow(2);
|
||||||
for (int i = 0; i < HEADERS.length; i++) {
|
for (int i = 0; i < HEADERS.length; i++) {
|
||||||
header.createCell(i).setCellValue(HEADERS[i]);
|
header.createCell(i).setCellValue(HEADERS[i]);
|
||||||
@@ -202,8 +202,8 @@ public class WarehouseImportService {
|
|||||||
} else {
|
} else {
|
||||||
optionalLength(row, errors, "仓库名称", row.name(), 120);
|
optionalLength(row, errors, "仓库名称", row.name(), 120);
|
||||||
}
|
}
|
||||||
if (!row.type().isBlank() && !row.type().matches("(?i)DOMESTIC|FBA|OVERSEAS")) {
|
if (!row.type().isBlank() && !row.type().matches("(?i)DOMESTIC|FBA|OVERSEAS|OVERSEAS_PARTNER")) {
|
||||||
errors.add(new WarehouseImportError(row.row(), "仓库类型", "只能填写 DOMESTIC、FBA 或 OVERSEAS"));
|
errors.add(new WarehouseImportError(row.row(), "仓库类型", "只能填写 DOMESTIC、FBA、OVERSEAS 或 OVERSEAS_PARTNER"));
|
||||||
}
|
}
|
||||||
if (!row.countryCode().isBlank() && !row.countryCode().matches("(?i)[A-Z]{2}")) {
|
if (!row.countryCode().isBlank() && !row.countryCode().matches("(?i)[A-Z]{2}")) {
|
||||||
errors.add(new WarehouseImportError(row.row(), "国家二字码", "必须是两个英文字母"));
|
errors.add(new WarehouseImportError(row.row(), "国家二字码", "必须是两个英文字母"));
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ final class WarehouseSaveNormalizer {
|
|||||||
static final long PUBLIC_TENANT_ID = 0L;
|
static final long PUBLIC_TENANT_ID = 0L;
|
||||||
static final String TYPE_DOMESTIC = "DOMESTIC";
|
static final String TYPE_DOMESTIC = "DOMESTIC";
|
||||||
static final String TYPE_FBA = "FBA";
|
static final String TYPE_FBA = "FBA";
|
||||||
|
static final String TYPE_OVERSEAS = "OVERSEAS";
|
||||||
|
static final String TYPE_OVERSEAS_PARTNER = "OVERSEAS_PARTNER";
|
||||||
static final String STATUS_ACTIVE = "ACTIVE";
|
static final String STATUS_ACTIVE = "ACTIVE";
|
||||||
static final String DEFAULT_ADDRESS_TYPE = "WAREHOUSE";
|
static final String DEFAULT_ADDRESS_TYPE = "WAREHOUSE";
|
||||||
static final String DEFAULT_DOMESTIC_COUNTRY = "CN";
|
static final String DEFAULT_DOMESTIC_COUNTRY = "CN";
|
||||||
@@ -68,8 +70,9 @@ final class WarehouseSaveNormalizer {
|
|||||||
|
|
||||||
static NormalizedSaveRequest normalize(WarehouseRequests.SaveRequest request, long tenantId) {
|
static NormalizedSaveRequest normalize(WarehouseRequests.SaveRequest request, long tenantId) {
|
||||||
String warehouseType = upperOrDefault(request.warehouseType(), TYPE_DOMESTIC);
|
String warehouseType = upperOrDefault(request.warehouseType(), TYPE_DOMESTIC);
|
||||||
if (!TYPE_DOMESTIC.equals(warehouseType) && !TYPE_FBA.equals(warehouseType) && !"OVERSEAS".equals(warehouseType)) {
|
if (!Set.of(TYPE_DOMESTIC, TYPE_FBA, TYPE_OVERSEAS, TYPE_OVERSEAS_PARTNER).contains(warehouseType)) {
|
||||||
throw new BizException(ResultCode.VALIDATION_FAILED, "Warehouse type must be DOMESTIC, FBA or OVERSEAS / 仓库类型只能为 DOMESTIC、FBA 或 OVERSEAS");
|
throw new BizException(ResultCode.VALIDATION_FAILED,
|
||||||
|
"Warehouse type must be DOMESTIC, FBA, OVERSEAS or OVERSEAS_PARTNER / 仓库类型只能为 DOMESTIC、FBA、OVERSEAS 或 OVERSEAS_PARTNER");
|
||||||
}
|
}
|
||||||
String status = upperOrDefault(request.status(), STATUS_ACTIVE);
|
String status = upperOrDefault(request.status(), STATUS_ACTIVE);
|
||||||
if (!STATUS_ACTIVE.equals(status) && !"INACTIVE".equals(status)) {
|
if (!STATUS_ACTIVE.equals(status) && !"INACTIVE".equals(status)) {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ CREATE TABLE IF NOT EXISTS wh_warehouse (
|
|||||||
tenant_id BIGINT UNSIGNED NOT NULL COMMENT '平台公共仓库固定使用 tenant_id=0',
|
tenant_id BIGINT UNSIGNED NOT NULL COMMENT '平台公共仓库固定使用 tenant_id=0',
|
||||||
warehouse_code VARCHAR(50) NOT NULL COMMENT '平台内唯一仓库编码',
|
warehouse_code VARCHAR(50) NOT NULL COMMENT '平台内唯一仓库编码',
|
||||||
warehouse_name VARCHAR(120) NOT NULL COMMENT '仓库名称',
|
warehouse_name VARCHAR(120) NOT NULL COMMENT '仓库名称',
|
||||||
warehouse_type VARCHAR(32) NOT NULL DEFAULT 'DOMESTIC' COMMENT '公共仓库类型:DOMESTIC、FBA、OVERSEAS',
|
warehouse_type VARCHAR(32) NOT NULL DEFAULT 'DOMESTIC' COMMENT '公共仓库类型:DOMESTIC国内仓、FBA亚马逊仓、OVERSEAS普通海外仓、OVERSEAS_PARTNER海外合作仓',
|
||||||
country_code CHAR(2) NOT NULL COMMENT '所在国家二字码',
|
country_code CHAR(2) NOT NULL COMMENT '所在国家二字码',
|
||||||
timezone VARCHAR(64) NOT NULL COMMENT '仓库所在地UTC偏移量',
|
timezone VARCHAR(64) NOT NULL COMMENT '仓库所在地UTC偏移量',
|
||||||
contact_name VARCHAR(64) DEFAULT NULL COMMENT '仓库联系人',
|
contact_name VARCHAR(64) DEFAULT NULL COMMENT '仓库联系人',
|
||||||
|
|||||||
+12
@@ -3,6 +3,7 @@ package com.baiyue.warehouse.controller;
|
|||||||
import com.baiyue.common.core.api.ResultCode;
|
import com.baiyue.common.core.api.ResultCode;
|
||||||
import com.baiyue.common.core.exception.BizException;
|
import com.baiyue.common.core.exception.BizException;
|
||||||
import com.baiyue.warehouse.dto.WarehouseDetailResponse;
|
import com.baiyue.warehouse.dto.WarehouseDetailResponse;
|
||||||
|
import com.baiyue.warehouse.dto.OverseasPartnerWarehouseResponse;
|
||||||
import com.baiyue.warehouse.entity.WarehouseEntity;
|
import com.baiyue.warehouse.entity.WarehouseEntity;
|
||||||
import com.baiyue.warehouse.service.WarehouseApplicationService;
|
import com.baiyue.warehouse.service.WarehouseApplicationService;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
@@ -71,4 +72,15 @@ class InternalWarehouseControllerTest {
|
|||||||
|
|
||||||
verifyNoInteractions(service);
|
verifyNoInteractions(service);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void listsActiveOverseasPartnerOptionsForInternalCallers() {
|
||||||
|
var option = new OverseasPartnerWarehouseResponse(200002L, "US-PARTNER-LAX", "洛杉矶合作仓", "US");
|
||||||
|
when(service.activeOverseasPartners()).thenReturn(List.of(option));
|
||||||
|
|
||||||
|
var response = controller.overseasPartners(INTERNAL_TOKEN);
|
||||||
|
|
||||||
|
assertThat(response.data()).containsExactly(option);
|
||||||
|
verify(service).activeOverseasPartners();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+33
@@ -116,4 +116,37 @@ class WarehouseApplicationServiceTest {
|
|||||||
.isEqualTo(ResultCode.VALIDATION_FAILED);
|
.isEqualTo(ResultCode.VALIDATION_FAILED);
|
||||||
verify(repository, never()).insertWarehouse(any());
|
verify(repository, never()).insertWarehouse(any());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void createsOverseasPartnerWarehouseAsPlatformPublicMasterData() {
|
||||||
|
WarehouseRequests.SaveRequest request = new WarehouseRequests.SaveRequest(null, "US-PARTNER-LAX", "洛杉矶合作仓",
|
||||||
|
"OVERSEAS_PARTNER", "US", "UTC-8", "Alex", "+1-213-555-0100", null, null, null,
|
||||||
|
new WarehouseRequests.AddressRequest(null, "US", "CA", "Los Angeles", null,
|
||||||
|
"90001", "123 Partner Ave", null, null, null),
|
||||||
|
java.util.List.of(new WarehouseRequests.CapabilityRequest("STORAGE", "仓储", true, null, null)));
|
||||||
|
|
||||||
|
service.create(request);
|
||||||
|
|
||||||
|
var warehouse = org.mockito.ArgumentCaptor.forClass(WarehouseEntity.class);
|
||||||
|
verify(repository).insertWarehouse(warehouse.capture());
|
||||||
|
assertThat(warehouse.getValue().getTenantId()).isZero();
|
||||||
|
assertThat(warehouse.getValue().getWarehouseType()).isEqualTo("OVERSEAS_PARTNER");
|
||||||
|
assertThat(warehouse.getValue().getCountryCode()).isEqualTo("US");
|
||||||
|
assertThat(warehouse.getValue().getTimezone()).isEqualTo("UTC-8");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void exposesOnlyActiveOverseasPartnerWarehousesToInternalConsumers() {
|
||||||
|
WarehouseEntity partner = new WarehouseEntity();
|
||||||
|
partner.setId(2L);
|
||||||
|
partner.setWarehouseCode("US-PARTNER-LAX");
|
||||||
|
partner.setWarehouseName("洛杉矶合作仓");
|
||||||
|
partner.setCountryCode("US");
|
||||||
|
when(repository.findActiveByType(0L, "OVERSEAS_PARTNER")).thenReturn(java.util.List.of(partner));
|
||||||
|
|
||||||
|
assertThat(service.activeOverseasPartners())
|
||||||
|
.extracting("id", "warehouseCode", "warehouseName", "countryCode")
|
||||||
|
.containsExactly(tuple(2L, "US-PARTNER-LAX", "洛杉矶合作仓", "US"));
|
||||||
|
verify(repository).findActiveByType(0L, "OVERSEAS_PARTNER");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,6 +64,22 @@ class WarehouseImportServiceTest {
|
|||||||
assertThat(captor.getAllValues().get(1).warehouseName()).isEqualTo("FBA-LAX9");
|
assertThat(captor.getAllValues().get(1).warehouseName()).isEqualTo("FBA-LAX9");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("海外合作仓类型可通过批量导入进入公共仓库主数据")
|
||||||
|
void shouldImportOverseasPartnerWarehouse() throws Exception {
|
||||||
|
byte[] file = withRows(service.template(), new String[][] {
|
||||||
|
{"US-PARTNER-LAX", "洛杉矶合作仓", "OVERSEAS_PARTNER", "US", "CA", "Los Angeles", "", "123 Partner Ave", "90001", "UTC-8", "", "", "", "", ""}
|
||||||
|
});
|
||||||
|
|
||||||
|
WarehouseImportResult result = service.importFile(0L, "warehouses.xlsx", file, null);
|
||||||
|
|
||||||
|
assertThat(result.success()).isTrue();
|
||||||
|
var captor = org.mockito.ArgumentCaptor.forClass(WarehouseRequests.SaveRequest.class);
|
||||||
|
verify(warehouseService).create(captor.capture());
|
||||||
|
assertThat(captor.getValue().tenantId()).isZero();
|
||||||
|
assertThat(captor.getValue().warehouseType()).isEqualTo("OVERSEAS_PARTNER");
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("任一行错误时整批零写入")
|
@DisplayName("任一行错误时整批零写入")
|
||||||
void shouldRejectWholeBatchWhenAnyRowInvalid() throws Exception {
|
void shouldRejectWholeBatchWhenAnyRowInvalid() throws Exception {
|
||||||
|
|||||||
Reference in New Issue
Block a user