暂存提交
This commit is contained in:
@@ -0,0 +1,14 @@
|
||||
package com.baiyue.firstleg;
|
||||
|
||||
import org.mybatis.spring.annotation.MapperScan;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
|
||||
/**
|
||||
* 头程履约微服务启动入口,负责装配履约任务组件并扫描 MyBatis-Plus Mapper。
|
||||
*/
|
||||
@MapperScan("com.baiyue.firstleg.mapper")
|
||||
@SpringBootApplication(scanBasePackages = "com.baiyue")
|
||||
public class BaiyueFirstlegApplication {
|
||||
public static void main(String[] args) { SpringApplication.run(BaiyueFirstlegApplication.class, args); }
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.baiyue.firstleg.controller;
|
||||
|
||||
import com.baiyue.common.core.api.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 头程履约服务状态检查接口,供网关、部署平台或运维探针确认服务可用性。
|
||||
*/
|
||||
@Tag(
|
||||
name = "First-leg service status / 头程服务状态",
|
||||
description = "First-leg service availability API. / 头程服务可用性接口。"
|
||||
)
|
||||
@RestController
|
||||
@RequestMapping("/api/platform/v1/first-leg")
|
||||
public class FirstlegHealthController {
|
||||
@Operation(
|
||||
summary = "Get service status / 查询服务状态",
|
||||
description = "Returns the logical status of firstleg-service. / 返回 firstleg-service 的逻辑运行状态。"
|
||||
)
|
||||
@io.swagger.v3.oas.annotations.responses.ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "Service is available / 服务可用"
|
||||
)
|
||||
@GetMapping("/status")
|
||||
public ApiResponse<Map<String, String>> status() {
|
||||
return ApiResponse.success(Map.of("service", "firstleg-service", "status", "UP"));
|
||||
}
|
||||
}
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
package com.baiyue.firstleg.controller;
|
||||
|
||||
import com.baiyue.common.audit.BusinessAudit;
|
||||
import com.baiyue.common.core.api.ApiResponse;
|
||||
import com.baiyue.common.core.api.ResultCode;
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.common.security.AuthenticatedUser;
|
||||
import com.baiyue.firstleg.dto.AssignRequest;
|
||||
import com.baiyue.firstleg.service.FulfillmentTaskService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponses;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.validation.Valid;
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
/**
|
||||
* 平台端头程履约任务指派接口,在目标租户内完成任务指派或改派并记录指派历史。
|
||||
*/
|
||||
@Tag(
|
||||
name = "Platform first-leg assignment / 平台头程指派",
|
||||
description = "Platform APIs for assigning tenant-scoped first-leg fulfillment tasks. / 平台端按租户指派头程履约任务的接口。"
|
||||
)
|
||||
@RestController
|
||||
@RequestMapping("/api/platform/v1/first-leg/tasks")
|
||||
public class PlatformFulfillmentAssignmentController {
|
||||
@Resource
|
||||
private FulfillmentTaskService service;
|
||||
|
||||
@BusinessAudit(
|
||||
event = "FIRSTLEG_ASSIGN",
|
||||
resourceType = "FIRSTLEG_TASK",
|
||||
resourceId = "#taskId",
|
||||
description = "Assign first-leg task / 指派头程任务"
|
||||
)
|
||||
@Operation(
|
||||
summary = "Assign fulfillment task / 指派履约任务",
|
||||
description = "Assigns or reassigns an open first-leg task in the target tenant and records assignment history. / 指派或改派目标租户下的未关闭头程任务,并记录指派历史。"
|
||||
)
|
||||
@ApiResponses({
|
||||
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "Assignment succeeded / 指派成功"),
|
||||
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "400", description = "Invalid tenant or request / 租户或请求参数无效"),
|
||||
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "403", description = "Platform access required / 需要平台端权限"),
|
||||
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "Task not found in target tenant / 目标租户下任务不存在"),
|
||||
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "409", description = "Task is closed or concurrently updated / 任务已关闭或发生并发更新")
|
||||
})
|
||||
@PostMapping("/{taskId}/assignment")
|
||||
public ApiResponse<Void> assign(
|
||||
@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@Parameter(description = "Target tenant ID / 目标租户 ID", example = "10001", required = true)
|
||||
@RequestParam long tenantId,
|
||||
@Parameter(description = "First-leg fulfillment task ID / 头程履约任务 ID", example = "190000000000000101", required = true)
|
||||
@PathVariable long taskId,
|
||||
@Valid @RequestBody AssignRequest request) {
|
||||
requirePlatform(user);
|
||||
requireTenant(tenantId);
|
||||
service.assign(
|
||||
tenantId,
|
||||
user.userId(),
|
||||
taskId,
|
||||
request.assigneeId(),
|
||||
request.assigneeName(),
|
||||
request.reason()
|
||||
);
|
||||
return ApiResponse.success(null);
|
||||
}
|
||||
|
||||
private static void requirePlatform(AuthenticatedUser user) {
|
||||
if (user == null || !"PLATFORM".equals(user.appCode())) {
|
||||
throw new BizException(ResultCode.FORBIDDEN,
|
||||
"Platform application required / 仅允许平台端访问");
|
||||
}
|
||||
}
|
||||
|
||||
private static void requireTenant(long tenantId) {
|
||||
if (tenantId <= 0) {
|
||||
throw new BizException(ResultCode.BAD_REQUEST,
|
||||
"Target tenant is required / 必须指定有效目标租户");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.baiyue.firstleg.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Size;
|
||||
|
||||
/**
|
||||
* 头程履约任务指派请求,承载被指派人标识、显示名称和指派原因。
|
||||
*/
|
||||
@Schema(name = "FirstlegAssignRequest", description = "First-leg task assignment request / 头程任务指派请求")
|
||||
public record AssignRequest(
|
||||
@Schema(description = "Assignee user ID / 被指派人用户 ID", example = "20001", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
@NotNull Long assigneeId,
|
||||
@Schema(description = "Assignee display name / 被指派人显示名称", example = "Zhang San / 张三", maxLength = 64, requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
@NotBlank @Size(max = 64) String assigneeName,
|
||||
@Schema(description = "Assignment reason / 指派原因", example = "Responsible for booking and export customs declaration / 负责订舱及出口报关", maxLength = 500)
|
||||
@Size(max = 500) String reason
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package com.baiyue.firstleg.entity;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 头程任务指派历史持久化实体。
|
||||
*
|
||||
* <p>对应表 {@code fl_assignment_history},记录租户内任务每次指派或改派的
|
||||
* 前后负责人、原因、操作人和时间。</p>
|
||||
*/
|
||||
@Schema(name = "FirstlegAssignmentHistory", description = "First-leg task assignment history / 头程任务指派历史")
|
||||
@Getter
|
||||
@Setter
|
||||
@TableName("fl_assignment_history")
|
||||
public class AssignmentHistoryEntity {
|
||||
@Schema(description = "History ID / 历史记录 ID", example = "190000000000000111")
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
private Long id;
|
||||
@Schema(description = "Tenant ID / 租户 ID", example = "10001")
|
||||
private Long tenantId;
|
||||
@Schema(description = "Task ID / 任务 ID", example = "190000000000000101")
|
||||
private Long taskId;
|
||||
@Schema(description = "Previous assignee ID / 原被指派人 ID", example = "20000")
|
||||
private Long previousAssigneeId;
|
||||
@Schema(description = "New assignee ID / 新被指派人 ID", example = "20001")
|
||||
private Long assigneeId;
|
||||
@Schema(description = "New assignee name / 新被指派人名称", example = "Zhang San / 张三")
|
||||
private String assigneeName;
|
||||
@Schema(description = "Assignment reason / 指派原因", example = "Responsible for booking / 负责订舱")
|
||||
private String reason;
|
||||
@Schema(description = "Operator user ID / 操作人用户 ID", example = "90001")
|
||||
private Long operatorId;
|
||||
@Schema(description = "Operation time / 操作时间", example = "2026-07-28T15:30:00")
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package com.baiyue.firstleg.entity;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.baiyue.common.core.model.BaseEntity;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 头程履约任务持久化实体。
|
||||
*
|
||||
* <p>对应表 {@code fl_fulfillment_task},保存由订单驱动的租户履约任务、
|
||||
* 订单快照、负责人及处理时间节点。</p>
|
||||
*/
|
||||
@Schema(name = "FirstlegFulfillmentTask", description = "First-leg fulfillment task / 头程履约任务")
|
||||
@Getter
|
||||
@Setter
|
||||
@TableName("fl_fulfillment_task")
|
||||
public class FulfillmentTaskEntity extends BaseEntity {
|
||||
@Schema(description = "Task ID / 任务 ID", example = "190000000000000101")
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
private Long id;
|
||||
@Schema(description = "Tenant ID / 租户 ID", example = "10001")
|
||||
private Long tenantId;
|
||||
@Schema(description = "Source order ID, logical reference only / 来源订单 ID,仅逻辑引用", example = "190000000000000001")
|
||||
private Long orderId;
|
||||
@Schema(description = "Source order number / 来源订单号", example = "FL202607280001")
|
||||
private String orderNo;
|
||||
@Schema(description = "Order snapshot JSON / 订单快照 JSON", example = "{\"orderNo\":\"FL202607280001\"}")
|
||||
private String orderSnapshotJson;
|
||||
@Schema(
|
||||
description = "Task status / 任务状态",
|
||||
example = "ASSIGNED",
|
||||
allowableValues = {"PENDING_ASSIGNMENT", "ASSIGNED", "PROCESSING", "COMPLETED", "CANCELLED"}
|
||||
)
|
||||
private String status;
|
||||
@Schema(description = "Assignee user ID / 被指派人用户 ID", example = "20001")
|
||||
private Long assigneeId;
|
||||
@Schema(description = "Assignee display name / 被指派人显示名称", example = "Zhang San / 张三")
|
||||
private String assigneeName;
|
||||
@Schema(description = "Assignment time / 指派时间", example = "2026-07-28T15:30:00")
|
||||
private LocalDateTime assignedAt;
|
||||
@Schema(description = "Processing start time / 开始处理时间", example = "2026-07-28T16:00:00")
|
||||
private LocalDateTime startedAt;
|
||||
@Schema(description = "Completion time / 完成时间", example = "2026-07-29T12:00:00")
|
||||
private LocalDateTime completedAt;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
package com.baiyue.firstleg.mapper; import com.baiyue.firstleg.entity.AssignmentHistoryEntity; import com.baomidou.mybatisplus.core.mapper.BaseMapper; /**
|
||||
* 头程任务指派历史 Mapper,提供 {@code fl_assignment_history} 的 MyBatis-Plus 基础数据访问能力。
|
||||
*/
|
||||
public interface AssignmentHistoryMapper extends BaseMapper<AssignmentHistoryEntity>{}
|
||||
@@ -0,0 +1,4 @@
|
||||
package com.baiyue.firstleg.mapper; import com.baiyue.firstleg.entity.FulfillmentTaskEntity; import com.baomidou.mybatisplus.core.mapper.BaseMapper; /**
|
||||
* 头程履约任务 Mapper,提供 {@code fl_fulfillment_task} 的 MyBatis-Plus 基础数据访问能力。
|
||||
*/
|
||||
public interface FulfillmentTaskMapper extends BaseMapper<FulfillmentTaskEntity>{}
|
||||
@@ -0,0 +1,36 @@
|
||||
package com.baiyue.firstleg.repository;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baiyue.firstleg.entity.AssignmentHistoryEntity;
|
||||
import com.baiyue.firstleg.entity.FulfillmentTaskEntity;
|
||||
import com.baiyue.firstleg.mapper.AssignmentHistoryMapper;
|
||||
import com.baiyue.firstleg.mapper.FulfillmentTaskMapper;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
/**
|
||||
* 头程履约任务仓储,按租户查询和更新任务,并持久化每次指派历史。
|
||||
*/
|
||||
@Repository
|
||||
public class FulfillmentTaskRepository {
|
||||
private final FulfillmentTaskMapper tasks;
|
||||
private final AssignmentHistoryMapper histories;
|
||||
|
||||
public FulfillmentTaskRepository(FulfillmentTaskMapper tasks, AssignmentHistoryMapper histories) {
|
||||
this.tasks = tasks;
|
||||
this.histories = histories;
|
||||
}
|
||||
|
||||
public FulfillmentTaskEntity findOwned(long tenantId, long taskId) {
|
||||
return tasks.selectOne(new LambdaQueryWrapper<FulfillmentTaskEntity>()
|
||||
.eq(FulfillmentTaskEntity::getTenantId, tenantId)
|
||||
.eq(FulfillmentTaskEntity::getId, taskId));
|
||||
}
|
||||
|
||||
public boolean update(FulfillmentTaskEntity task) {
|
||||
return tasks.updateById(task) == 1;
|
||||
}
|
||||
|
||||
public void insertHistory(AssignmentHistoryEntity history) {
|
||||
histories.insert(history);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package com.baiyue.firstleg.service;
|
||||
import jakarta.annotation.Resource;
|
||||
|
||||
import com.baiyue.common.core.api.ResultCode;
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.firstleg.entity.AssignmentHistoryEntity;
|
||||
import com.baiyue.firstleg.entity.FulfillmentTaskEntity;
|
||||
import com.baiyue.firstleg.repository.FulfillmentTaskRepository;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 头程履约任务应用服务,在事务内校验任务状态、更新负责人并记录指派历史。
|
||||
*/
|
||||
@Service
|
||||
public class FulfillmentTaskService {
|
||||
@Resource
|
||||
private FulfillmentTaskRepository repository;
|
||||
|
||||
|
||||
@Transactional
|
||||
public void assign(long tenantId, long operatorId, long taskId,
|
||||
long assigneeId, String assigneeName, String reason) {
|
||||
FulfillmentTaskEntity task = repository.findOwned(tenantId, taskId);
|
||||
if (task == null) {
|
||||
throw new BizException(ResultCode.NOT_FOUND,
|
||||
"Fulfillment task not found / 头程履约任务不存在");
|
||||
}
|
||||
if ("COMPLETED".equals(task.getStatus()) || "CANCELLED".equals(task.getStatus())) {
|
||||
throw new BizException(ResultCode.CONFLICT,
|
||||
"Closed task cannot be assigned / 已关闭任务不能再次指派");
|
||||
}
|
||||
|
||||
Long previousAssigneeId = task.getAssigneeId();
|
||||
task.setAssigneeId(assigneeId);
|
||||
task.setAssigneeName(assigneeName);
|
||||
task.setAssignedAt(LocalDateTime.now());
|
||||
task.setStatus("ASSIGNED");
|
||||
task.setUpdatedBy(operatorId);
|
||||
if (!repository.update(task)) {
|
||||
throw new BizException(ResultCode.CONFLICT,
|
||||
"Concurrent task update, please retry / 任务已被并发更新,请重试");
|
||||
}
|
||||
|
||||
AssignmentHistoryEntity history = new AssignmentHistoryEntity();
|
||||
history.setTenantId(tenantId);
|
||||
history.setTaskId(taskId);
|
||||
history.setPreviousAssigneeId(previousAssigneeId);
|
||||
history.setAssigneeId(assigneeId);
|
||||
history.setAssigneeName(assigneeName);
|
||||
history.setReason(reason);
|
||||
history.setOperatorId(operatorId);
|
||||
history.setCreatedAt(LocalDateTime.now());
|
||||
repository.insertHistory(history);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user