暂存提交

This commit is contained in:
2026-07-30 18:01:36 +08:00
commit 2f4e159356
368 changed files with 20370 additions and 0 deletions
@@ -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"));
}
}
@@ -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);
}
}