暂存提交

This commit is contained in:
2026-07-31 14:57:09 +08:00
parent 7a19e2f096
commit f8804fbc52
53 changed files with 3771 additions and 0 deletions
@@ -0,0 +1,27 @@
package com.baiyue.firstleg.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.Size;
import jakarta.validation.constraints.Pattern;
/** 头程履约任务分页查询条件。 */
@Schema(name = "FulfillmentTaskQuery", description = "头程履约任务分页查询条件")
public record FulfillmentTaskQuery(
@Schema(description = "订单号关键字") @Size(max = 64) String keyword,
@Schema(description = "任务状态")
@Size(max = 24) @Pattern(regexp = "PENDING_ASSIGNMENT|ASSIGNED|PROCESSING|COMPLETED|CANCELLED") String status,
@Schema(description = "负责人用户ID") Long assigneeId,
@Schema(description = "页码", defaultValue = "1") @Min(1) long page,
@Schema(description = "每页条数", defaultValue = "20") @Min(1) @Max(200) long size
) {
public FulfillmentTaskQuery {
if (page == 0) {
page = 1;
}
if (size == 0) {
size = 20;
}
}
}
@@ -0,0 +1,14 @@
SET NAMES utf8mb4;
USE baiyue_firstleg;
-- 既有履约任务表补充分页查询索引;重复执行时不重复创建。
SET @firstleg_task_list_index_sql = IF(
EXISTS(SELECT 1 FROM information_schema.statistics
WHERE table_schema = DATABASE() AND table_name = 'fl_fulfillment_task'
AND index_name = 'idx_task_tenant_created'),
'SELECT 1',
'CREATE INDEX idx_task_tenant_created ON fl_fulfillment_task (tenant_id, created_at)'
);
PREPARE firstleg_task_list_index_stmt FROM @firstleg_task_list_index_sql;
EXECUTE firstleg_task_list_index_stmt;
DEALLOCATE PREPARE firstleg_task_list_index_stmt;
@@ -0,0 +1,106 @@
openapi: 3.0.3
info:
title: 柏悦头程履约 API
version: 1.1.0
security:
- bearerAuth: []
paths:
/api/platform/v1/first-leg/tasks:
get:
summary: 分页查询履约任务
description: 需要 platform:firstleg:task:list 权限
parameters:
- $ref: '#/components/parameters/TenantId'
- $ref: '#/components/parameters/Page'
- $ref: '#/components/parameters/Size'
- {name: keyword, in: query, schema: {type: string, maxLength: 64}}
- {name: status, in: query, schema: {type: string, enum: [PENDING_ASSIGNMENT, ASSIGNED, PROCESSING, COMPLETED, CANCELLED]}}
- {name: assigneeId, in: query, schema: {type: integer, format: int64}}
responses:
'200': {description: 查询成功}
'403': {description: 无查询权限}
/api/platform/v1/first-leg/tasks/{taskId}:
get:
summary: 查询履约任务详情
description: 需要 platform:firstleg:task:view 权限
parameters:
- $ref: '#/components/parameters/TenantId'
- $ref: '#/components/parameters/TaskId'
responses:
'200': {description: 查询成功}
'403': {description: 无查看权限}
'404': {description: 任务不存在}
/api/platform/v1/first-leg/tasks/by-order/{orderId}:
get:
summary: 按订单查询履约任务
description: 需要 platform:firstleg:task:view 权限
parameters:
- $ref: '#/components/parameters/TenantId'
- {name: orderId, in: path, required: true, schema: {type: integer, format: int64}}
responses:
'200': {description: 查询成功}
'403': {description: 无查看权限}
'404': {description: 任务不存在}
/api/platform/v1/first-leg/tasks/{taskId}/assignment-history:
get:
summary: 分页查询任务分配历史
description: 需要 platform:firstleg:assignment:history 权限
parameters:
- $ref: '#/components/parameters/TenantId'
- $ref: '#/components/parameters/TaskId'
- $ref: '#/components/parameters/Page'
- $ref: '#/components/parameters/Size'
responses:
'200': {description: 查询成功}
'403': {description: 无历史查看权限}
'404': {description: 任务不存在}
/api/platform/v1/first-leg/tasks/{taskId}/assignment:
post:
summary: 分配或改派履约任务
description: 需要 platform:firstleg:assignment:assign 权限
parameters:
- $ref: '#/components/parameters/TenantId'
- $ref: '#/components/parameters/TaskId'
requestBody:
required: true
content:
application/json:
schema: {$ref: '#/components/schemas/AssignRequest'}
responses:
'200': {description: 分配成功}
'403': {description: 无分配权限}
'404': {description: 任务不存在}
'409': {description: 任务已关闭或发生并发更新}
components:
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
parameters:
TenantId:
name: tenantId
in: query
required: true
schema: {type: integer, format: int64, minimum: 1}
TaskId:
name: taskId
in: path
required: true
schema: {type: integer, format: int64}
Page:
name: page
in: query
schema: {type: integer, format: int64, minimum: 1, default: 1}
Size:
name: size
in: query
schema: {type: integer, format: int64, minimum: 1, maximum: 200, default: 20}
schemas:
AssignRequest:
type: object
required: [assigneeId, assigneeName]
properties:
assigneeId: {type: integer, format: int64}
assigneeName: {type: string, maxLength: 64}
reason: {type: string, maxLength: 500}
@@ -0,0 +1,55 @@
package com.baiyue.firstleg.controller;
import com.baiyue.common.security.AuthenticatedUser;
import com.baiyue.firstleg.dto.AssignRequest;
import com.baiyue.firstleg.dto.FulfillmentTaskQuery;
import org.junit.jupiter.api.Test;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.core.io.ClassPathResource;
import java.lang.reflect.Method;
import java.nio.charset.StandardCharsets;
import static org.assertj.core.api.Assertions.assertThat;
class FulfillmentTaskControllerContractTest {
@Test
void 查询和分配接口声明对应权限码() throws Exception {
assertAuthority("page", "hasAuthority('platform:firstleg:task:list')",
AuthenticatedUser.class, long.class, FulfillmentTaskQuery.class);
assertAuthority("detail", "hasAuthority('platform:firstleg:task:view')",
AuthenticatedUser.class, long.class, long.class);
assertAuthority("byOrder", "hasAuthority('platform:firstleg:task:view')",
AuthenticatedUser.class, long.class, long.class);
assertAuthority("assignmentHistory", "hasAuthority('platform:firstleg:assignment:history')",
AuthenticatedUser.class, long.class, long.class, long.class, long.class);
assertAuthority("assign", "hasAuthority('platform:firstleg:assignment:assign')",
AuthenticatedUser.class, long.class, long.class, AssignRequest.class);
}
@Test
void 分页查询缺省值为第一页每页二十条() {
FulfillmentTaskQuery query = new FulfillmentTaskQuery(null, null, null, 0, 0);
assertThat(query.page()).isEqualTo(1);
assertThat(query.size()).isEqualTo(20);
}
@Test
void OpenAPI覆盖四类只读接口和对应权限() throws Exception {
String openapi = new ClassPathResource("openapi/firstleg-v1.yaml")
.getContentAsString(StandardCharsets.UTF_8);
assertThat(openapi)
.contains("/api/platform/v1/first-leg/tasks:")
.contains("/api/platform/v1/first-leg/tasks/{taskId}:")
.contains("/api/platform/v1/first-leg/tasks/by-order/{orderId}:")
.contains("/api/platform/v1/first-leg/tasks/{taskId}/assignment-history:")
.contains("platform:firstleg:task:list")
.contains("platform:firstleg:assignment:history");
}
private void assertAuthority(String methodName, String permission, Class<?>... parameterTypes)
throws NoSuchMethodException {
Method method = PlatformFulfillmentAssignmentController.class.getDeclaredMethod(methodName, parameterTypes);
assertThat(method.getAnnotation(PreAuthorize.class).value()).isEqualTo(permission);
}
}
@@ -0,0 +1,65 @@
package com.baiyue.firstleg.service;
import com.baiyue.common.core.api.PageResult;
import com.baiyue.common.core.api.ResultCode;
import com.baiyue.common.core.exception.BizException;
import com.baiyue.firstleg.dto.FulfillmentTaskQuery;
import com.baiyue.firstleg.entity.FulfillmentTaskEntity;
import com.baiyue.firstleg.repository.FulfillmentTaskRepository;
import org.junit.jupiter.api.Test;
import org.springframework.test.util.ReflectionTestUtils;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
class FulfillmentTaskServiceTest {
@Test
void 列表查询保留租户和分页条件() {
FulfillmentTaskRepository repository = mock(FulfillmentTaskRepository.class);
FulfillmentTaskQuery query = new FulfillmentTaskQuery(null, null, null, 1, 20);
PageResult<FulfillmentTaskEntity> expected = new PageResult<>(List.of(), 1, 20, 0, 0);
when(repository.page(10001L, query)).thenReturn(expected);
assertThat(service(repository).page(10001L, query)).isSameAs(expected);
verify(repository).page(10001L, query);
}
@Test
void 详情和按订单查询不存在时返回404() {
FulfillmentTaskRepository repository = mock(FulfillmentTaskRepository.class);
FulfillmentTaskService service = service(repository);
assertThatThrownBy(() -> service.detail(10001L, 1L))
.isInstanceOf(BizException.class)
.extracting(exception -> ((BizException) exception).getResultCode())
.isEqualTo(ResultCode.NOT_FOUND);
assertThatThrownBy(() -> service.byOrder(10001L, 2L))
.isInstanceOf(BizException.class)
.extracting(exception -> ((BizException) exception).getResultCode())
.isEqualTo(ResultCode.NOT_FOUND);
}
@Test
void 查询分配历史前先校验任务归属租户() {
FulfillmentTaskRepository repository = mock(FulfillmentTaskRepository.class);
FulfillmentTaskEntity task = new FulfillmentTaskEntity();
when(repository.findOwned(10001L, 1L)).thenReturn(task);
PageResult<com.baiyue.firstleg.entity.AssignmentHistoryEntity> expected =
new PageResult<>(List.of(), 1, 20, 0, 0);
when(repository.pageHistory(10001L, 1L, 1, 20)).thenReturn(expected);
assertThat(service(repository).assignmentHistory(10001L, 1L, 1, 20)).isSameAs(expected);
verify(repository).findOwned(10001L, 1L);
}
private FulfillmentTaskService service(FulfillmentTaskRepository repository) {
FulfillmentTaskService service = new FulfillmentTaskService();
ReflectionTestUtils.setField(service, "repository", repository);
return service;
}
}