插单修改

parent a84189b4
...@@ -418,6 +418,58 @@ public class ResourceGanttController { ...@@ -418,6 +418,58 @@ public class ResourceGanttController {
return R.ok("插单成功"); return R.ok("插单成功");
} }
@PostMapping("/orderInsertAuto")
@Operation(
summary = "自动插单",
description = "创建新工单并按基准时间+冻结期自动排程。若锚点被占用,则占位工单及后续工单后移;若前面有空挡,新工单自动前移到设备最早可用时间",
requestBody = @io.swagger.v3.oas.annotations.parameters.RequestBody(
description = "自动插单参数",
required = true,
content = @io.swagger.v3.oas.annotations.media.Content(
mediaType = "application/json",
examples = {
@io.swagger.v3.oas.annotations.media.ExampleObject(
name = "自动插单示例",
summary = "基础示例",
value = "{\n" +
" \"sceneId\": \"B571EF6682DB463AB2977B1055A74112\",\n" +
" \"newOrder\": {\n" +
" \"orderCode\": \"AUTO_20260326_001\",\n" +
" \"materialId\": \"d5d0dd08-cfb5-2b45-85e9-aba470895423\",\n" +
" \"quantity\": 610000\n" +
" }\n" +
"}"
),
@io.swagger.v3.oas.annotations.media.ExampleObject(
name = "大批量示例",
summary = "大批量订单",
value = "{\n" +
" \"sceneId\": \"B571EF6682DB463AB2977B1055A74112\",\n" +
" \"newOrder\": {\n" +
" \"orderCode\": \"AUTO_20260326_002\",\n" +
" \"materialId\": \"cbd0dd08-49b1-0146-83bd-ec6185316c16\",\n" +
" \"quantity\": 1200000\n" +
" }\n" +
"}"
)
}
)
)
)
public R<String> insertOrderAuto(@RequestBody Map<String, Object> params) {
log.info("insertOrderAuto 请求参数: {}", params);
String sceneId = ParamValidator.getString(params, "sceneId", "场景ID");
@SuppressWarnings("unchecked")
Map<String, Object> newOrderData = (Map<String, Object>) params.get("newOrder");
ParamValidator.validateSceneExists(sceneService, sceneId);
planResultService.InsertOrderAuto(sceneId, newOrderData);
return R.ok("自动插单成功");
}
@PostMapping("/ordermerge") @PostMapping("/ordermerge")
@Operation(summary = "订单合并", description = "订单合并") @Operation(summary = "订单合并", description = "订单合并")
public R<String> orderMerge(@RequestBody Map<String, Object> params) { public R<String> orderMerge(@RequestBody Map<String, Object> params) {
......
...@@ -12,6 +12,7 @@ import org.apache.commons.lang3.StringUtils; ...@@ -12,6 +12,7 @@ import org.apache.commons.lang3.StringUtils;
import org.springframework.context.annotation.ScopeMetadata; import org.springframework.context.annotation.ScopeMetadata;
import javax.xml.transform.Result; import javax.xml.transform.Result;
import java.math.BigDecimal;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.*; import java.util.*;
import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CopyOnWriteArrayList;
...@@ -1186,6 +1187,369 @@ if(targetOp.getSequence()>1) { ...@@ -1186,6 +1187,369 @@ if(targetOp.getSequence()>1) {
globalParam.setIsCheckSf(originalIsCheckSf); globalParam.setIsCheckSf(originalIsCheckSf);
} }
public void InsertOrderAuto(Chromosome chromosome, String newOrderId,
ProdLaunchOrder newLaunchOrder, List<ProdProcessExec> newProcessExecs,
List<ProdEquipment> newProdEquipments,
LocalDateTime anchorTime, GlobalParam globalParam) {
List<Order> orders = chromosome.getOrders();
List<GroupResult> OperatRels = chromosome.getOperatRel();
Order newOrder = new Order();
newOrder.setOrderId(newOrderId);
newOrder.setOrderCode(newLaunchOrder.getOrderCode());
newOrder.setQuantity(newLaunchOrder.getQuantity());
newOrder.setPriority(newLaunchOrder.getOrderPriority() != null ? newLaunchOrder.getOrderPriority() : 1);
newOrder.setMaterialId(newLaunchOrder.getMaterialId());
newOrder.setMaterialCode(newLaunchOrder.getMaterialCode());
newOrder.setMaterialName(newLaunchOrder.getMaterialName());
newOrder.setStartDate(newLaunchOrder.getStartDate());
newOrder.setDueDate(newLaunchOrder.getEndDate());
newOrder.setRoutingId(newLaunchOrder.getRoutingId());
newOrder.setRoutingCode(newLaunchOrder.getRoutingCode());
newOrder.setSerie(newLaunchOrder.getSerie());
int maxOrderId = orders.stream().mapToInt(Order::getId).max().orElse(0) + 1;
newOrder.setId(maxOrderId);
orders.add(newOrder);
int maxGroupId = OperatRels.size();
int newGroupId = maxGroupId + 1;
List<Entry> newEntrys = new ArrayList<>();
List<String> newIdList = new ArrayList<>();
List<String> newChildIdList = new ArrayList<>();
for (ProdProcessExec processExec : newProcessExecs) {
Entry newEntry = new Entry();
newEntry.setExecId(processExec.getExecId());
newEntry.setOrderId(newOrderId);
newEntry.setOrderCode(newLaunchOrder.getOrderCode());
newEntry.setGroupId(newGroupId);
newEntry.setQuantity(newLaunchOrder.getQuantity());
newEntry.setSequence(processExec.getTaskSeq() != null ? processExec.getTaskSeq().intValue() : 1);
newEntry.setTaskSeq(processExec.getTaskSeq());
newEntry.setState(2);
newEntry.setRoutingDetailId(processExec.getRoutingDetailId());
newEntry.setRoutingId(processExec.getRoutingId());
newEntry.setRoutingCode(processExec.getRoutingCode());
newEntry.setRoutingName(processExec.getRoutingName());
newEntry.setRoutingDetailName(processExec.getRoutingDetailName());
newEntry.setProductId(newLaunchOrder.getMaterialId());
newEntry.setProductCode(newLaunchOrder.getMaterialCode());
newEntry.setProductName(newLaunchOrder.getMaterialName());
if (processExec.getDepartmentId() != null) {
newEntry.setDepartmentId(processExec.getDepartmentId());
}
newEntry.setIsInterrupt(processExec.getCanInterrupt());
// 这里保留资源组ID(设备类型)
newEntry.setEquipTypeID(processExec.getMachineId());
if (processExec.getRuntime() != null) {
newEntry.setRuntime(processExec.getRuntime());
}
if (processExec.getSingleOut() != null) {
newEntry.setSingleOut(processExec.getSingleOut());
}
newEntry.setPriority(newLaunchOrder.getOrderPriority() != null ? newLaunchOrder.getOrderPriority() : 1);
if (processExec.getSetupTime() != null) {
newEntry.setSetupTime(processExec.getSetupTime());
}
newEntry.setChangeLineTime(processExec.getChangeLineTime());
newEntry.setConstTime(processExec.getConstTime());
newEntry.setPreTime(processExec.getPreprocessingTime());
newEntry.setTeardownTime(processExec.getPostprocessingTime());
newEntry.setEquipTypeName(processExec.getEquipTypeName());
newEntry.setEquipTypeCode(processExec.getEquipTypeCode());
// 与InitEntrys对齐:machineOptions来源于ProdEquipment(execId -> equipId)
List<MachineOption> machineOptions = newProdEquipments.stream()
.filter(pe -> pe.getExecId() != null && pe.getExecId().equals(processExec.getExecId()))
.map(pe -> {
MachineOption mo = new MachineOption();
mo.setMachineId(pe.getEquipId()); // 真实设备ID
mo.setRuntime(pe.getRuntime() != null ? pe.getRuntime() : BigDecimal.ZERO);
mo.setSingleOut(pe.getSingleOut() != null ? pe.getSingleOut() : BigDecimal.ONE);
mo.setEquipCode(pe.getEquipCode());
mo.setEquipName(pe.getEquipName());
mo.setResourceCode(pe.getResourceCode());
mo.setProcessingTime(pe.getSpeed() != null ? pe.getSpeed() : 0d);
return mo;
})
.collect(Collectors.toList());
if (machineOptions.isEmpty()) {
throw new RuntimeException("自动插单失败:工序[" + processExec.getExecId() + "]没有可用设备");
}
newEntry.setMachineOptions(machineOptions);
newEntry.setSelectMachineID(null); // 后面按bestSeq回填
newEntrys.add(newEntry);
}
for (int i = 0; i < newEntrys.size(); i++) {
Entry entry = newEntrys.get(i);
if (i == 0) {
newIdList.add(entry.getExecId());
newChildIdList.add(i + 1 < newEntrys.size() ? newEntrys.get(i + 1).getExecId() : "");
} else if (i == newEntrys.size() - 1) {
newIdList.add(entry.getExecId());
newChildIdList.add("");
} else {
newIdList.add(entry.getExecId());
newChildIdList.add(newEntrys.get(i + 1).getExecId());
}
}
OperatRels = IdGroupingWithDualSerial.addNewDataWithIsolatedGroup(OperatRels, newIdList, newChildIdList);
chromosome.setOperatRel(new CopyOnWriteArrayList<>(OperatRels));
int globalOpId = chromosome.getGlobalOpList().stream()
.mapToInt(GlobalOperationInfo::getGlobalOpId)
.max()
.orElse(0) + 1;
GroupResult newGroupResult = OperatRels.get(OperatRels.size() - 1);
List<NodeInfo> nodeInfoList = newGroupResult.getNodeInfoList();
Entry firstEntry = null;
for (NodeInfo nodeInfo : nodeInfoList) {
Entry entry = newEntrys.stream()
.filter(t -> t.getExecId().equals(nodeInfo.getOriginalId()))
.findFirst()
.orElse(null);
if (entry != null) {
entry.setId(nodeInfo.getGlobalSerial());
entry.setGroupId(newGroupId);
entry.setSequence(nodeInfo.getGroupSerial());
if (nodeInfo.getNewParentIds() != null) {
List<OperationDependency> dependencies = new ArrayList<>();
for (int id : nodeInfo.getNewParentIds()) {
OperationDependency od = new OperationDependency();
od.setPrevOperationId(id);
dependencies.add(od);
}
entry.setPrevEntryIds(dependencies);
}
if (nodeInfo.getNewChildIds() != null) {
List<OperationDependency> dependencies = new ArrayList<>();
for (int id : nodeInfo.getNewChildIds()) {
OperationDependency od = new OperationDependency();
od.setNextOperationId(id);
dependencies.add(od);
}
entry.setNextEntryIds(dependencies);
}
chromosome.getAllOperations().add(entry);
GlobalOperationInfo info = new GlobalOperationInfo();
info.setGlobalOpId(globalOpId);
info.setGroupId(entry.getGroupId());
info.setSequence(entry.getSequence());
info.setOp(entry);
chromosome.getGlobalOpList().add(info);
int bestSeq = calcBestMachineSeq(entry, chromosome, anchorTime);
chromosome.getMachineSelection().add(bestSeq);
entry.setSelectMachineID(entry.getMachineOptions().get(bestSeq - 1).getMachineId());
if (entry.getSequence() == 1) {
firstEntry = entry;
}
globalOpId++;
}
}
for (int i = 0; i < newEntrys.size(); i++) {
chromosome.getOperationSequencing().add(newGroupId);
}
int firstOperationId = firstEntry != null ? firstEntry.getId() : -1;
boolean originalIsCheckSf = globalParam.isIsCheckSf();
globalParam.setIsCheckSf(false);
// 第一次解码
if (anchorTime != null && firstEntry != null) {
firstEntry.setDesignatedStartTime(anchorTime);
}
redecode(chromosome, chromosome.getBaseTime(), globalParam);
// 第二次调整:锚点占位后推 + 新工单前移
if (anchorTime != null && firstOperationId > 0) {
GAScheduleResult firstResult = chromosome.getResult().stream()
.filter(r -> r.getOperationId() == firstOperationId)
.findFirst()
.orElse(null);
if (firstResult != null) {
long machineId = firstResult.getMachineId();
int anchorSeconds = (int) java.time.temporal.ChronoUnit.SECONDS.between(chromosome.getBaseTime(), anchorTime);
int firstDuration = Math.max(1, firstResult.getEndTime() - firstResult.getStartTime());
List<GAScheduleResult> machineOldResults = chromosome.getResult().stream()
.filter(r -> r.getMachineId() == machineId)
.filter(r -> r.getGroupId() != firstResult.getGroupId())
.sorted(Comparator.comparingInt(GAScheduleResult::getStartTime))
.collect(Collectors.toList());
GAScheduleResult pivot = machineOldResults.stream()
.filter(r -> r.getStartTime() <= anchorSeconds && r.getEndTime() > anchorSeconds)
.findFirst()
.orElse(null);
if (pivot == null) {
pivot = machineOldResults.stream()
.filter(r -> r.getStartTime() >= anchorSeconds)
.findFirst()
.orElse(null);
}
GAScheduleResult prev = machineOldResults.stream()
.filter(r -> r.getEndTime() <= anchorSeconds)
.reduce((a, b) -> b)
.orElse(null);
int newDesignatedStart;
if (pivot != null) {
if (pivot.getStartTime() <= anchorSeconds && pivot.getEndTime() > anchorSeconds) {
newDesignatedStart = pivot.getStartTime();
} else {
newDesignatedStart = prev != null ? prev.getEndTime() : 0;
}
} else {
newDesignatedStart = prev != null ? prev.getEndTime() : 0;
}
if (newDesignatedStart < 0) {
newDesignatedStart = 0;
}
Entry firstEntryRef = chromosome.getAllOperations().stream()
.filter(e -> e.getId() == firstOperationId)
.findFirst()
.orElse(null);
if (firstEntryRef != null) {
firstEntryRef.setDesignatedStartTime(chromosome.getBaseTime().plusSeconds(newDesignatedStart));
}
GAScheduleResult firstLock = chromosome.getResult().stream()
.filter(r -> r.getOperationId() == firstOperationId)
.findFirst()
.orElse(null);
if (firstLock != null) {
firstLock.setDesignatedStartTime(newDesignatedStart);
firstLock.setLockStartTime(1);
}
if (pivot != null) {
int pivotStart = pivot.getStartTime();
List<GAScheduleResult> needShift = machineOldResults.stream()
.filter(r -> r.getStartTime() >= pivotStart)
.collect(Collectors.toList());
for (GAScheduleResult oldResult : needShift) {
oldResult.setDesignatedStartTime(oldResult.getStartTime() + firstDuration);
oldResult.setLockStartTime(1);
}
}
redecode(chromosome, chromosome.getBaseTime(), globalParam);
}
}
globalParam.setIsCheckSf(originalIsCheckSf);
}
private int calcBestMachineSeq(Entry entry, Chromosome chromosome, LocalDateTime anchorTime) {
List<MachineOption> rawOptions = entry.getMachineOptions();
if (rawOptions == null || rawOptions.isEmpty()) {
throw new RuntimeException("工序无可选设备: " + entry.getExecId());
}
// 只保留当前排产设备池中的设备
Set<Long> availableMachineIds = chromosome.getMachines().stream()
.map(Machine::getId)
.collect(Collectors.toSet());
List<MachineOption> options = rawOptions.stream()
.filter(o -> availableMachineIds.contains(o.getMachineId()))
.collect(Collectors.toList());
if (options.isEmpty()) {
throw new RuntimeException("工序无可用设备(不在当前排产设备池): " + entry.getExecId());
}
Map<Long, Integer> machineEnd = chromosome.getResult().stream()
.collect(Collectors.groupingBy(
GAScheduleResult::getMachineId,
Collectors.collectingAndThen(
Collectors.maxBy(Comparator.comparingInt(GAScheduleResult::getEndTime)),
o -> o.map(GAScheduleResult::getEndTime).orElse(0)
)
));
int anchorSec = 0;
if (anchorTime != null && chromosome.getBaseTime() != null) {
anchorSec = (int) java.time.temporal.ChronoUnit.SECONDS.between(chromosome.getBaseTime(), anchorTime);
if (anchorSec < 0) {
anchorSec = 0;
}
}
int bestIdx = 0;
double bestScore = Double.MAX_VALUE;
for (int i = 0; i < options.size(); i++) {
MachineOption op = options.get(i);
int ready = Math.max(anchorSec, machineEnd.getOrDefault(op.getMachineId(), 0));
double duration;
if (entry.getConstTime() == 1) {
duration = Math.max(0d, op.getProcessingTime());
} else {
double runtime = op.getRuntime() != null ? op.getRuntime().doubleValue() : 0d;
double singleOut = op.getSingleOut() != null ? op.getSingleOut().doubleValue() : 1d;
if (singleOut <= 0) {
singleOut = 1d;
}
duration = runtime / singleOut * entry.getQuantity();
}
double score = ready + duration;
if (score < bestScore) {
bestScore = score;
bestIdx = i;
}
}
// 注意:这里返回的是在rawOptions中的序号(machineSelection要求按entry.machineOptions顺序)
Long bestMachineId = options.get(bestIdx).getMachineId();
int rawIndex = IntStream.range(0, rawOptions.size())
.filter(i -> rawOptions.get(i).getMachineId() == bestMachineId)
.findFirst()
.orElse(0);
return rawIndex + 1;
}
public void MergeOrder(Chromosome chromosome, String sourceorderId,String targetorderId, GlobalParam globalParam) { public void MergeOrder(Chromosome chromosome, String sourceorderId,String targetorderId, GlobalParam globalParam) {
List<Entry> allOperations = chromosome.getAllOperations(); List<Entry> allOperations = chromosome.getAllOperations();
List<GlobalOperationInfo> globalOpList= chromosome.getGlobalOpList(); List<GlobalOperationInfo> globalOpList= chromosome.getGlobalOpList();
......
...@@ -1340,74 +1340,110 @@ public class PlanResultService { ...@@ -1340,74 +1340,110 @@ public class PlanResultService {
return releasedCount; return releasedCount;
} }
public Chromosome InsertOrderAuto(String sceneId, Map<String, Object> newOrderData) {
private long countMachineAvailabilitySegments(Chromosome chromosome, Long machineId) { if (newOrderData == null) {
if (chromosome == null || machineId == null || chromosome.getInitMachines() == null) { throw new RuntimeException("newOrder 不能为空");
return 0;
} }
return chromosome.getInitMachines().stream() String orderCode = String.valueOf(newOrderData.get("orderCode"));
.filter(Objects::nonNull) String materialId = String.valueOf(newOrderData.get("materialId"));
.filter(m -> m.getId() == machineId) Object qtyObj = newOrderData.get("quantity");
.findFirst() if (orderCode == null || orderCode.trim().isEmpty()) {
.map(m -> m.getAvailability() == null ? 0L : (long) m.getAvailability().size()) throw new RuntimeException("orderCode 不能为空");
.orElse(0L); }
} if (materialId == null || materialId.trim().isEmpty()) {
throw new RuntimeException("materialId 不能为空");
}
if (qtyObj == null) {
throw new RuntimeException("quantity 不能为空");
}
Double quantity = Double.valueOf(String.valueOf(qtyObj));
private long countMachineUsedMaintenanceSegments(Chromosome chromosome, Long machineId) { // 1. 创建新订单(沿用现有创建逻辑)
if (chromosome == null || machineId == null || chromosome.getInitMachines() == null) { R<String> insertResp = lanuchService.insertOrder(sceneId, orderCode, materialId, null, null, 1, quantity);
return 0; String insertMsg = insertResp != null ? insertResp.getData() : null;
if (insertMsg == null || insertMsg.trim().isEmpty()) {
throw new RuntimeException("创建订单失败:未返回订单ID");
} }
return chromosome.getInitMachines().stream() String newOrderId = insertMsg;
.filter(Objects::nonNull) if (insertMsg.contains("订单ID:")) {
.filter(m -> m.getId() == machineId) newOrderId = insertMsg.substring(insertMsg.indexOf("订单ID:") + 5).trim();
.findFirst() }
.map(m -> m.getAvailability() == null ? 0L : m.getAvailability().stream()
.filter(Objects::nonNull)
.filter(seg -> seg.getType() == SegmentType.MAINTENANCE && seg.isUsed())
.count())
.orElse(0L);
}
private long countMachineUsedSegments(Chromosome chromosome, Long machineId, SegmentType type) { // 2. 读取场景排产结果
if (chromosome == null || machineId == null || chromosome.getInitMachines() == null) { Chromosome chromosome = _sceneService.loadChromosomeFromFile(sceneId);
return 0; if (chromosome == null) {
throw new RuntimeException("场景不存在或排产结果未初始化");
} }
return chromosome.getInitMachines().stream() // 3. 查询新订单及其工序
.filter(Objects::nonNull) ProdLaunchOrder newLaunchOrder = _prodLaunchOrderService.lambdaQuery()
.filter(m -> m.getId() == machineId) .eq(ProdLaunchOrder::getSceneId, sceneId)
.findFirst() .eq(ProdLaunchOrder::getOrderId, newOrderId)
.map(m -> m.getAvailability() == null ? 0L : m.getAvailability().stream() .one();
.filter(Objects::nonNull) if (newLaunchOrder == null) {
.filter(TimeSegment::isUsed) throw new RuntimeException("新订单不存在:" + newOrderId);
.filter(seg -> type == null || seg.getType() == type) }
.count())
.orElse(0L);
}
private String machineSegmentSnapshot(Chromosome chromosome, Long machineId, int limit) { List<ProdProcessExec> newProcessExecs = _prodProcessExecService.lambdaQuery()
if (chromosome == null || machineId == null || chromosome.getInitMachines() == null) { .eq(ProdProcessExec::getSceneId, sceneId)
return "[]"; .eq(ProdProcessExec::getOrderId, newOrderId)
.orderByAsc(ProdProcessExec::getTaskSeq)
.list();
if (newProcessExecs == null || newProcessExecs.isEmpty()) {
throw new RuntimeException("新订单没有工序:" + newOrderId);
} }
Machine machine = chromosome.getInitMachines().stream() // 3.1 查询新工序对应设备(与InitEntrys对齐:execId -> prod_equipment)
List<String> execIds = newProcessExecs.stream()
.map(ProdProcessExec::getExecId)
.filter(Objects::nonNull) .filter(Objects::nonNull)
.filter(m -> m.getId() == machineId) .distinct()
.findFirst() .collect(Collectors.toList());
.orElse(null);
List<ProdEquipment> newProdEquipments = execIds.isEmpty()
? new ArrayList<>()
: _prodEquipmentService.lambdaQuery()
.eq(ProdEquipment::getSceneId, sceneId)
.in(ProdEquipment::getExecId, execIds)
.list();
if (machine == null || machine.getAvailability() == null || machine.getAvailability().isEmpty()) { if (newProdEquipments == null || newProdEquipments.isEmpty()) {
return "[]"; throw new RuntimeException("自动插单失败:新工单未生成可选设备,请检查PROD_EQUIPMENT");
} }
return machine.getAvailability().stream() // 4. 计算锚点时间:基准时间 + 冻结期
.filter(Objects::nonNull) LocalDateTime baseTime = chromosome.getBaseTime();
.sorted(Comparator.comparing(TimeSegment::getStart)) LambdaQueryWrapper<ApsTimeConfig> queryWrapper = new LambdaQueryWrapper<>();
.limit(Math.max(limit, 1)) ApsTimeConfig apsTimeConfig = apsTimeConfigMapper.selectOne(queryWrapper);
.map(seg -> String.format("%s|used=%s|%s~%s", seg.getType(), seg.isUsed(), seg.getStart(), seg.getEnd())) long freezeSeconds = 0L;
.collect(Collectors.joining("; ")); if (apsTimeConfig != null && apsTimeConfig.getFreezeDate() != null) {
freezeSeconds = apsTimeConfig.getFreezeDate().longValue();
}
if (apsTimeConfig != null && apsTimeConfig.getBaseTime() != null) {
baseTime = apsTimeConfig.getBaseTime();
}
LocalDateTime anchorTime = baseTime.plusSeconds(Math.max(freezeSeconds, 0L));
// 5. 自动插单(占位后推 + 空挡前移)
GlobalParam globalParam = InitGlobalParam();
ScheduleOperationService scheduleOperation = new ScheduleOperationService(materialRequirementService, this);
scheduleOperation.InsertOrderAuto(
chromosome,
newOrderId,
newLaunchOrder,
newProcessExecs,
newProdEquipments,
anchorTime,
globalParam
);
// 6. 保存
WriteScheduleSummary(chromosome);
_sceneService.saveChromosomeToFile(chromosome, sceneId);
return chromosome;
} }
public Chromosome Move(String SceneId,List<Integer> opId, LocalDateTime newStartTime, public Chromosome Move(String SceneId,List<Integer> opId, LocalDateTime newStartTime,
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment