skyouc
昨天 37467bf7d119ef9b599f1c19b869d046d730b7cb
修改优化
1. 波次生成修改优化
2. 出库库位查找 优化
3个文件已添加
1个文件已删除
15个文件已修改
801 ■■■■ 已修改文件
rsf-admin/src/i18n/en.js 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/i18n/zh.js 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/orders/wave/ItemToTaskModal.jsx 186 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/orders/wave/WaveList.jsx 44 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/task/TaskList.jsx 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/waveItem/WaveItemList.jsx 8 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/Test/MinimalCombinationSum.java 90 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/api/service/impl/MobileServiceImpl.java 7 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/manager/controller/WaveController.java 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/manager/entity/LocItem.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/manager/entity/Wave.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/manager/entity/WaveItem.java 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/manager/enums/AsnExceStatus.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/manager/service/WaveService.java 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/manager/service/impl/OutStockServiceImpl.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/manager/service/impl/TaskServiceImpl.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/manager/service/impl/WaveServiceImpl.java 118 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/manager/utils/GroupMergeUtil.java 154 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/manager/utils/OptimalAlgorithmUtil.java 132 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/i18n/en.js
@@ -745,6 +745,8 @@
                fieldsIndex: "fieldsIndex",
                anfme: "anfme",
                workQty: "workQty",
                qty: "Qty",
                stockLocs: "Stock Locs"
            },
            task: {
                taskCode: "TaskCode",
rsf-admin/src/i18n/zh.js
@@ -791,6 +791,8 @@
                fieldsIndex: "动态扩展",
                anfme: "数量",
                workQty: "执行数",
                qty: "完成数量",
                stockLocs: "库存位置"
            },
            task: {
                taskCode: "任务号",
rsf-admin/src/page/orders/wave/ItemToTaskModal.jsx
New file
@@ -0,0 +1,186 @@
import React, { useState, useRef, useEffect, useMemo, useCallback } from "react";
import {
    List,
    DatagridConfigurable,
    SearchInput,
    TopToolbar,
    SelectColumnsButton,
    EditButton,
    FilterButton,
    CreateButton,
    ExportButton,
    BulkDeleteButton,
    WrapperField,
    useRecordContext,
    useTranslate,
    useNotify,
    useListContext,
    FunctionField,
    TextField,
    NumberField,
    DateField,
    BooleanField,
    ReferenceField,
    TextInput,
    DateTimeInput,
    DateInput,
    SelectInput,
    NumberInput,
    ReferenceInput,
    ReferenceArrayInput,
    AutocompleteInput,
    DeleteButton,
    useRefresh,
    useRedirect,
    Button,
    useGetList,
    ListContextProvider,
    useList,
    Toolbar,
    SingleFieldList,
} from 'react-admin';
import { PAGE_DRAWER_WIDTH, OPERATE_MODE, DEFAULT_PAGE_SIZE } from '@/config/setting';
import { Box, Typography, Card, Stack, DialogContent, DialogTitle, DialogActions, Dialog } from '@mui/material';
import { styled } from '@mui/material/styles';
import DialogCloseButton from "../../components/DialogCloseButton";
import request from '@/utils/request';
import { filter } from "lodash";
import { height } from "@mui/system";
const StyledDatagrid = styled(DatagridConfigurable)(({ theme }) => ({
    '& .css-1vooibu-MuiSvgIcon-root': {
        height: '.9em'
    },
    '& .RaDatagrid-row': {
        cursor: 'auto'
    },
    '& .column-name': {
    },
    '& .opt': {
        width: 200
    },
}));
const ItemToTaskModal = (props) => {
    const { open, setOpen, record } = props;
    const translate = useTranslate();
    const [createDialog, setCreateDialog] = useState(false);
    const [drawerVal, setDrawerVal] = useState(false);
    const handleClose = (event, reason) => {
        if (reason !== "backdropClick") {
            setOpen(false);
        }
    };
    const { data, total, isPending, error, refetch, meta } = useGetList('/wave/locs/preview', { filter: { waveId: record?.id } });
    const listContext = useList({ data, isPending });
    return (
        <Box display="flex">
            <Dialog
                open={open}
                onClose={handleClose}
                aria-labelledby="form-dialog-title"
                aria-hidden
                fullWidth
                disableRestoreFocus
                maxWidth="lg"
            >
                <DialogTitle id="form-dialog-title" sx={{
                    position: 'sticky',
                    top: 0,
                    backgroundColor: 'background.paper',
                    zIndex: 1000
                }}>
                    {translate('toolbar.createTask')}
                    <Box sx={{ position: 'absolute', top: 8, right: 8, zIndex: 1001 }}>
                        <DialogCloseButton onClose={handleClose} />
                    </Box>
                </DialogTitle>
                <DialogContent sx={{ height: 400 }}>
                    <ListContextProvider
                        value={listContext}
                        sx={{
                            flexGrow: 1,
                            transition: (theme) =>
                                theme.transitions.create(['all'], {
                                    duration: theme.transitions.duration.enteringScreen,
                                }),
                            marginRight: !!drawerVal ? `${PAGE_DRAWER_WIDTH}px` : 0,
                        }}
                        title={"menu.waveItem"}
                        empty={false}
                        sort={{ field: "create_time", order: "desc" }}
                        actions={(
                            <TopToolbar>
                                <SelectColumnsButton preferenceKey='waveItem' />
                            </TopToolbar>
                        )}
                        perPage={DEFAULT_PAGE_SIZE}
                    >
                        <StyledDatagrid
                            preferenceKey='waveItem'
                            bulkActionButtons={false}
                            rowClick={(id, resource, record) => false}
                            expand={false}
                            expandSingle={false}
                            omit={['id', 'createTime', 'matnrId', 'waveId', 'batch', 'orderItemId', 'unit', 'batch', 'trackCode', 'fieldsIndex', 'createBy', 'memo']}
                        >
                            <NumberField source="id" />
                            <NumberField source="waveId" label="table.field.waveItem.waveId" />
                            <TextField source="waveCode" label="table.field.waveItem.waveCode" />
                            <TextField source="orderCode" label="table.field.waveItem.orderCode" />
                            {/* <TextField source="maktx" label="table.field.waveItem.matnrName" /> */}
                            <NumberField source="matnrId" label="table.field.waveItem.matnrId" />
                            <TextField source="matnrCode" label="table.field.waveItem.matnrCode" />
                            <TextField source="batch" label="table.field.waveItem.batch" />
                            <TextField source="splrBatch" label="table.field.waveItem.splrBatch" />
                            <NumberField source="orderItemId" label="table.field.waveItem.orderItemId" />
                            <TextField source="unit" label="table.field.waveItem.unit" />
                            <TextField source="trackCode" label="table.field.waveItem.trackCode" />
                            <TextField source="fieldsIndex" label="table.field.waveItem.fieldsIndex" />
                            <NumberField source="anfme" label="table.field.waveItem.anfme" />
                            <NumberField source="workQty" label="table.field.waveItem.workQty" />
                            <NumberField source="qty" label="table.field.waveItem.qty" />
                            <WrapperField cellClassName="opt" label="table.field.waveItem.stockLocs">
                                <ArrayField source="stockLocs" stockLocs="table.field.waveItem.stockLocs">
                                    <SingleFieldList linkType={false}>
                                        <ChilpField source="" size="small"></ChilpField>
                                    </SingleFieldList>
                                </ArrayField>
                                {/* <NumberField source="anfme" label="table.field.waveItem.stockLocs" /> */}
                                {/* <EditButton sx={{ padding: '1px', fontSize: '.75rem' }} />
                                <DeleteButton sx={{ padding: '1px', fontSize: '.75rem' }} mutationMode={OPERATE_MODE} /> */}
                            </WrapperField>
                        </StyledDatagrid>
                    </ListContextProvider>
                </DialogContent>
                <DialogActions>
                    <Toolbar sx={{ width: '100%', justifyContent: 'end' }} >
                        <GenerateTaskButton record={[record?.id]} />
                    </Toolbar>
                </DialogActions>
            </Dialog>
        </Box>
    )
}
export default ItemToTaskModal;
const GenerateTaskButton = (record) => {
    const refresh = useRefresh();
    const notify = useNotify();
    const redirect = useRedirect();
    const generateTask = async () => {
        const res = await request.post(`/wave/public/task`, { ids: record?.record });
        if (res?.data?.code === 200) {
            notify(res.data.msg);
            redirect("/task")
        } else {
            notify(res.data.msg);
        }
        refresh();
    }
    return (<Button variant="contained" label={"ra.action.save"} onClick={generateTask}></Button>)
}
rsf-admin/src/page/orders/wave/WaveList.jsx
@@ -48,6 +48,8 @@
import { PAGE_DRAWER_WIDTH, OPERATE_MODE, DEFAULT_PAGE_SIZE } from '@/config/setting';
import * as Common from '@/utils/common';
import PublicIcon from '@mui/icons-material/Public';
import ItemToTaskModal from "./ItemToTaskModal";
import ConfirmButton from "../../components/ConfirmButton";
const StyledDatagrid = styled(DatagridConfigurable)(({ theme }) => ({
@@ -60,7 +62,7 @@
    '& .column-name': {
    },
    '& .opt': {
        width: 200
        width: 260
    },
}));
@@ -68,7 +70,6 @@
    <SearchInput source="condition" alwaysOn />,
    <DateInput label='common.time.after' source="timeStart" alwaysOn />,
    <DateInput label='common.time.before' source="timeEnd" alwaysOn />,
    <TextInput source="code" label="table.field.wave.code" />,
    <SelectInput source="type" label="table.field.wave.type"
        choices={[
@@ -87,7 +88,6 @@
    <NumberInput source="anfme" label="table.field.wave.anfme" />,
    <NumberInput source="qty" label="table.field.wave.qty" />,
    <NumberInput source="orderNum" label="table.field.wave.orderNum" />,
    <TextInput label="common.field.memo" source="memo" />,
    <SelectInput
        label="common.field.status"
@@ -103,6 +103,8 @@
const WaveList = () => {
    const translate = useTranslate();
    const [createDialog, setCreateDialog] = useState(false);
    const [detailDialog, setDetailDialog] = useState(false);
    const [select, setSelectIds] = useState({});
    const [drawerVal, setDrawerVal] = useState(false);
    return (
@@ -130,9 +132,7 @@
            >
                <StyledDatagrid
                    preferenceKey='wave'
                    bulkActionButtons={
                        <PublicTaskButton />
                    }
                    bulkActionButtons={false}
                    rowClick={(id, resource, record) => false}
                    expand={false}
                    expandSingle={false}
@@ -152,11 +152,17 @@
                    <BooleanField source="statusBool" label="common.field.status" sortable={false} />
                    <TextField source="memo" label="common.field.memo" sortable={false} />
                    <WrapperField cellClassName="opt" label="common.field.opt">
                        <PublicTaskButton setSelectIds={setSelectIds} setDetailDialog={setDetailDialog} />
                        <EditButton sx={{ padding: '1px', fontSize: '.75rem' }} />
                        <DeleteButton sx={{ padding: '1px', fontSize: '.75rem' }} mutationMode={OPERATE_MODE} />
                    </WrapperField>
                </StyledDatagrid>
            </List>
            <ItemToTaskModal
                open={detailDialog}
                record={select}
                setOpen={setDetailDialog}
            />
            <WaveCreate
                open={createDialog}
                setOpen={setCreateDialog}
@@ -173,32 +179,16 @@
export default WaveList;
const PublicTaskButton = () => {
const PublicTaskButton = ({ setSelectIds, setDetailDialog }) => {
    const record = useRecordContext();
    const { selectedIds, onUnselectItems } = useListContext();
    const notify = useNotify();
    const refresh = useRefresh();
    const redirect = useRedirect();
    const pubClick = async (event) => {
        event.stopPropagation();
        console.log('=========>');
        onUnselectItems();
        const res = await request.post(`/wave/public/task`, { ids: selectedIds });
        if (res?.data?.code === 200) {
            notify(res.data.msg);
            redirect("/task")
        } else {
            notify(res.data.msg);
        setSelectIds(record);
        setDetailDialog(true);
        }
        refresh();
    }
    return (
        <Button
            onClick={pubClick}
            label={"toolbar.createTask"}
            startIcon={<PublicIcon />}
        />);
        <ConfirmButton label={"toolbar.createTask"} startIcon={<PublicIcon />} onConfirm={pubClick} />
    );
}
rsf-admin/src/page/task/TaskList.jsx
@@ -23,7 +23,6 @@
    DateInput,
    SelectInput,
    NumberInput,
    Button,
} from 'react-admin';
import { Box, Typography, Card, Stack, Drawer } from '@mui/material';
rsf-admin/src/page/waveItem/WaveItemList.jsx
@@ -60,9 +60,8 @@
const filters = [
    <SearchInput source="condition" alwaysOn />,
    <DateInput label='common.time.after' source="timeStart" alwaysOn />,
    <DateInput label='common.time.before' source="timeEnd" alwaysOn />,
    <DateInput label='common.time.after' source="timeStart" />,
    <DateInput label='common.time.before' source="timeEnd" />,
    <NumberInput source="waveId" label="table.field.waveItem.waveId" />,
    <TextInput source="waveCode" label="table.field.waveItem.waveCode" />,
    <NumberInput source="matnrId" label="table.field.waveItem.matnrId" />,
@@ -77,7 +76,6 @@
    <TextInput source="fieldsIndex" label="table.field.waveItem.fieldsIndex" />,
    <NumberInput source="anfme" label="table.field.waveItem.anfme" />,
    <NumberInput source="workQty" label="table.field.waveItem.workQty" />,
    <TextInput label="common.field.memo" source="memo" />,
    <SelectInput
        label="common.field.status"
@@ -92,7 +90,6 @@
const WaveItemList = () => {
    const translate = useTranslate();
    const [createDialog, setCreateDialog] = useState(false);
    const [drawerVal, setDrawerVal] = useState(false);
@@ -144,7 +141,6 @@
                    <TextField source="fieldsIndex" label="table.field.waveItem.fieldsIndex" />
                    <NumberField source="anfme" label="table.field.waveItem.anfme" />
                    <NumberField source="workQty" label="table.field.waveItem.workQty" />
                    <ReferenceField source="updateBy" label="common.field.updateBy" reference="user" link={false} sortable={false}>
                        <TextField source="nickname" />
                    </ReferenceField>
rsf-server/src/main/Test/MinimalCombinationSum.java
New file
@@ -0,0 +1,90 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class MinimalCombinationSum {
    private static List<Double> bestSolution;
    private static double target;
    private static double minDifference;
    private static final double EPSILON = 1e-10;
    public static List<Double> findBestCombination(Double[] candidates, double targetSum) {
        bestSolution = null;
        target = targetSum;
        minDifference = Double.MAX_VALUE;
        Arrays.sort(candidates);
        backtrack(candidates, 0, new ArrayList<>(), 0.0);
        // 如果没有精确解,返回最接近的近似解
        return bestSolution != null ? bestSolution : new ArrayList<>();
    }
    private static void backtrack(Double[] candidates, int start,
                                  List<Double> current, double currentSum) {
        // 计算当前和与目标的差值
        double difference = Math.abs(currentSum - target);
        // 如果找到更优解(差值更小或差值相同但组合更短)
        if (difference < minDifference - EPSILON ||
                (Math.abs(difference - minDifference) < EPSILON &&
                        (bestSolution == null || current.size() < bestSolution.size()))) {
            minDifference = difference;
            bestSolution = new ArrayList<>(current);
        }
        // 如果已经找到精确解,不需要继续搜索更长的组合
        if (minDifference < EPSILON) {
            return;
        }
        // 遍历候选数字
        for (int i = start; i < candidates.length; i++) {
            double num = candidates[i];
            // 剪枝:如果当前和已经远大于目标值,跳过
            if (currentSum + num > target + minDifference + EPSILON) {
                continue;
            }
            // 跳过重复数字
            if (i > start && Math.abs(candidates[i] - candidates[i - 1]) < EPSILON) {
                continue;
            }
            current.add(num);
            backtrack(candidates, i + 1, current, currentSum + num);
            current.remove(current.size() - 1);
        }
    }
    public static void main(String[] args) {
        Double[] candidates1 = {1.5, 2.3, 3.1, 4.7};
        double target1 = 3.8;
        System.out.println("候选数字: " + Arrays.toString(candidates1));
        System.out.println("目标值: " + target1);
        List<Double> result1 = findBestCombination(candidates1, target1);
        printResult(result1, target1);
//        Double[] candidates2 = {0.8, 1.2, 1.7, 2.5};
//        double target2 = 3.9;
//        System.out.println("\n候选数字: " + Arrays.toString(candidates2));
//        System.out.println("目标值: " + target2);
//        List<Double> result2 = findBestCombination(candidates2, target2);
//        printResult(result2, target2);
    }
    private static void printResult(List<Double> result, double target) {
        if (result.isEmpty()) {
            System.out.println("无解");
        } else {
            double sum = result.stream().mapToDouble(Double::doubleValue).sum();
            if (Math.abs(sum - target) < EPSILON) {
                System.out.printf("精确解: %s (和=%.2f)\n", result, sum);
            } else {
                System.out.printf("近似解: %s (和=%.2f, 与目标相差: %.2f)\n",
                        result, sum, sum - target);
            }
        }
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/api/service/impl/MobileServiceImpl.java
@@ -654,7 +654,9 @@
        itemList.forEach(asnOrderItem -> {
            LocItem item = new LocItem();
            BeanUtils.copyProperties(asnOrderItem, item);
            item.setId(loc.getId())
            item.setLocId(loc.getId())
                    .setId(null)
                    .setLocCode(loc.getCode())
                    .setOrderId(order.getId())
                    .setOrderItemId(asnOrderItem.getId())
                    .setWkType(Short.parseShort(order.getWkType()))
@@ -924,7 +926,8 @@
            }
            //获取当前库存信息
            LocItem stockItem = locItemService.getOne(new LambdaQueryWrapper<LocItem>()
                    .eq(LocItem::getOrderItemId, asnOrderItem.getId())
//                    .eq(LocItem::getOrderItemId, asnOrderItem.getId())
                            .eq(LocItem::getFieldsIndex, asnOrderItem.getFieldsIndex())
                    .eq(LocItem::getBatch, asnOrderItem.getBatch())
                    .eq(LocItem::getMatnrId, asnOrderItem.getMatnrId()));
            //SET 当前库存数量
rsf-server/src/main/java/com/vincent/rsf/server/manager/controller/WaveController.java
@@ -1,19 +1,23 @@
package com.vincent.rsf.server.manager.controller;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.vincent.rsf.framework.common.Cools;
import com.vincent.rsf.framework.common.R;
import com.vincent.rsf.framework.exception.CoolException;
import com.vincent.rsf.server.common.domain.PageResult;
import com.vincent.rsf.server.common.utils.ExcelUtil;
import com.vincent.rsf.server.common.annotation.OperationLog;
import com.vincent.rsf.server.common.domain.BaseParam;
import com.vincent.rsf.server.common.domain.KeyValVo;
import com.vincent.rsf.server.common.domain.PageParam;
import com.vincent.rsf.server.manager.entity.Wave;
import com.vincent.rsf.server.manager.entity.WaveItem;
import com.vincent.rsf.server.manager.service.WaveService;
import com.vincent.rsf.server.system.controller.BaseController;
import io.swagger.annotations.ApiOperation;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
@@ -108,6 +112,7 @@
    }
    @PreAuthorize("hasAuthority('manager:wave:update')")
    @ApiOperation("波次下发任务")
    @PostMapping("/wave/public/task")
@@ -118,4 +123,18 @@
        return waveService.publicTask(map);
    }
    @PreAuthorize("hasAuthority('manager:wave:list')")
    @ApiOperation("波次出库任务预览")
    @PostMapping("/wave/locs/preview/page")
    public R mergeWavePreview(@RequestBody Map<String, Object> map) {
        if (Cools.isEmpty(map.get("waveId")) || StringUtils.isBlank(map.get("waveId").toString())) {
            throw new CoolException("参数不能为空!!");
        }
        Long waveId = Long.parseLong(map.get("waveId").toString());
        List<WaveItem> waveItems = waveService.mergeWavePreview(waveId);
        PageResult<WaveItem> pageResult = new PageResult<>();
        pageResult.setRecords(waveItems).setTotal(Long.parseLong(waveItems.size() + ""));
        return R.ok().add(pageResult);
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/manager/entity/LocItem.java
@@ -43,6 +43,9 @@
    @ApiModelProperty(value= "主单ID")
    private Long locId;
    @ApiModelProperty("库位编码")
    private String locCode;
    /**
     * 单据ID
     */
@@ -106,6 +109,9 @@
    @ApiModelProperty("执行数量")
    private Double workQty;
    @ApiModelProperty("完成数量")
    private Double qty;
    /**
     * 库存批次
     */
rsf-server/src/main/java/com/vincent/rsf/server/manager/entity/Wave.java
@@ -43,6 +43,9 @@
    @ApiModelProperty(value= "波次号")
    private String code;
    @ApiModelProperty("执行数量")
    private Double workQty;
    /**
     * 波次类型 0: 手动  1: 自动  
     */
rsf-server/src/main/java/com/vincent/rsf/server/manager/entity/WaveItem.java
@@ -1,6 +1,7 @@
package com.vincent.rsf.server.manager.entity;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.baomidou.mybatisplus.annotation.*;
import java.text.SimpleDateFormat;
import java.util.Date;
@@ -9,10 +10,7 @@
import java.text.SimpleDateFormat;
import java.util.Date;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
@@ -172,6 +170,10 @@
    @ApiModelProperty(value= "修改人员")
    private Long updateBy;
    @ApiModelProperty("目标库位")
    @TableField(exist = false)
    private String stockLocs;
    /**
     * 备注
     */
rsf-server/src/main/java/com/vincent/rsf/server/manager/enums/AsnExceStatus.java
@@ -17,9 +17,7 @@
    ASN_EXCE_STATUS_TASK_CLOSE("4", "已关闭"),
    OUT_STOCK_STATUS_TASK_INIT("5", "初始化"),
    OUT_STOCK_STATUS_TASK_EXCE("6", "待处理"),
    OUT_STOCK_STATUS_TASK_WAVE("7", "生成波次"),
    OUT_STOCK_STATUS_TASK_WORKING("8", "作业中")
    ;
    AsnExceStatus(String val, String desc) {
rsf-server/src/main/java/com/vincent/rsf/server/manager/service/WaveService.java
@@ -3,7 +3,9 @@
import com.baomidou.mybatisplus.extension.service.IService;
import com.vincent.rsf.framework.common.R;
import com.vincent.rsf.server.manager.entity.Wave;
import com.vincent.rsf.server.manager.entity.WaveItem;
import java.util.List;
import java.util.Map;
public interface WaveService extends IService<Wave> {
@@ -16,4 +18,13 @@
     * @time 2025/4/25 16:24
     */
    R publicTask(Map<String, Object> map);
    /**
     * @author Ryan
     * @description 预览波次下发任务
     * @param
     * @return
     * @time 2025/4/27 11:08
     */
    List<WaveItem> mergeWavePreview(Long waveId);
}
rsf-server/src/main/java/com/vincent/rsf/server/manager/service/impl/OutStockServiceImpl.java
@@ -12,7 +12,7 @@
import com.vincent.rsf.server.manager.enums.WaveExceStatus;
import com.vincent.rsf.server.manager.mapper.AsnOrderMapper;
import com.vincent.rsf.server.manager.service.*;
import com.vincent.rsf.server.manager.utils.GroupMergeUtil;
import com.vincent.rsf.server.manager.utils.OptimalAlgorithmUtil;
import com.vincent.rsf.server.system.constant.SerialRuleCode;
import com.vincent.rsf.server.system.utils.SerialRuleUtils;
import org.apache.commons.lang3.StringUtils;
@@ -321,7 +321,7 @@
                    .setWaveCode(wave.getCode());
            items.add(item);
        });
        List<WaveItem> waveItems = GroupMergeUtil.groupAndMerge(items,
        List<WaveItem> waveItems = OptimalAlgorithmUtil.groupAndMerge(items,
                (p1, p2) -> new WaveItem(
                        p1.getWaveId(),
                        p1.getWaveCode(),
rsf-server/src/main/java/com/vincent/rsf/server/manager/service/impl/TaskServiceImpl.java
@@ -241,7 +241,7 @@
            Loc loc = locService.getOne(new LambdaQueryWrapper<Loc>().eq(Loc::getCode, task.getTargLoc()), false);
            LocItem item = new LocItem();
            BeanUtils.copyProperties(taskItem, item);
            item.setLocId(loc.getId()).setType(taskItem.getOrderType());
            item.setLocCode(loc.getCode()).setId(null).setLocId(loc.getId()).setType(taskItem.getOrderType());
            locItems.add(item);
        });
        if (!locItemService.saveBatch(locItems)) {
rsf-server/src/main/java/com/vincent/rsf/server/manager/service/impl/WaveServiceImpl.java
@@ -1,41 +1,53 @@
package com.vincent.rsf.server.manager.service.impl;
import com.alibaba.fastjson.JSONArray;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.vincent.rsf.framework.common.Cools;
import com.vincent.rsf.framework.common.R;
import com.vincent.rsf.framework.exception.CoolException;
import com.vincent.rsf.server.manager.entity.AsnOrder;
import com.vincent.rsf.server.manager.entity.AsnOrderItem;
import com.vincent.rsf.server.manager.entity.LocItem;
import com.vincent.rsf.server.manager.entity.WaveItem;
import com.vincent.rsf.server.manager.enums.AsnExceStatus;
import com.vincent.rsf.server.manager.enums.WaveExceStatus;
import com.vincent.rsf.server.manager.mapper.WaveMapper;
import com.vincent.rsf.server.manager.entity.Wave;
import com.vincent.rsf.server.manager.service.AsnOrderItemService;
import com.vincent.rsf.server.manager.service.AsnOrderService;
import com.vincent.rsf.server.manager.service.WaveService;
import com.vincent.rsf.server.manager.service.*;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.vincent.rsf.server.manager.utils.OptimalAlgorithmUtil;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
@Service("waveService")
public class WaveServiceImpl extends ServiceImpl<WaveMapper, Wave> implements WaveService {
    @Autowired
    private AsnOrderItemService asnOrderItemService;
    @Autowired
    private AsnOrderService asnOrderService;
    @Autowired
    private WaveItemService waveItemService;
    @Autowired
    private TaskService taskService;
    @Autowired
    private TaskItemService taskItemService;
    @Autowired
    private LocItemService locItemService;
    /**
     * @author Ryan
     * @description 波次任务下发
     * @param
     * @return
     * @author Ryan
     * @description 波次任务下发
     * @time 2025/4/25 16:24
     */
    @Override
@@ -49,16 +61,94 @@
        if (Objects.isNull(waves) || waves.isEmpty()) {
            throw new CoolException("波次数据不存在!!");
        }
        List<Long> list = waves.stream().map(Wave::getId).collect(Collectors.toList());
        List<WaveItem> waveItems = waveItemService.list(new LambdaQueryWrapper<WaveItem>().in(WaveItem::getWaveId, list));
        if (waveItems.isEmpty()) {
            throw new CoolException("波次明细不存在!!");
        }
        List<Long> orderIds = waveItems.stream().map(WaveItem::getOrderId).collect(Collectors.toList());
        List<AsnOrder> orders = asnOrderService.list(new LambdaQueryWrapper<AsnOrder>().eq(AsnOrder::getId, ids));
        asnOrderItemService.list(new LambdaQueryWrapper<AsnOrderItem>().eq(AsnOrderItem::getAsnId, ids));
        /**查询每条明细匹配的库位*/
        try {
            List<WaveItem> items = getLocs(waveItems);
        } catch (Exception e) {
            throw new CoolException("库位获取失败!!!");
        }
        //TODO 1. 根据波次明细生成出库任务
        // 2. 根据物料SKU寻找符合物料库位  {1. 根据物料编码,批次,动态字段 查询符合的库位,再根据库位中物料的数量选择最适合的库位 2. 判断当前订单是全拖出库还是拣料入库}
        // 3. 修改主单、波次执行数量
        // 4. 判断全仓出库或拣料出库
        List<AsnOrder> orders = asnOrderService.list(new LambdaQueryWrapper<AsnOrder>().in(AsnOrder::getId, orderIds));
        /**修改原出库单状态*/
        if (!asnOrderService.update(new LambdaQueryWrapper<AsnOrder>()
                .eq(AsnOrder::getExceStatus, AsnExceStatus.OUT_STOCK_STATUS_TASK_WORKING.val)
                .in(AsnOrder::getId, orders))) {
            throw new CoolException("出库单据状态修改失败!!");
        }
        if (!this.update(new LambdaUpdateWrapper<Wave>().set(Wave::getExceStatus, WaveExceStatus.WAVE_EXCE_STATUS_TASK).in(Wave::getId,ids))) {
            throw new CoolException("波次状态修改失败!!");
        }
        return R.ok();
    }
    /**
     * @param
     * @return
     * @author Ryan
     * @description 预览波次下发任务
     * @time 2025/4/27 11:09
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public List<WaveItem> mergeWavePreview(Long waveId) {
        Wave wave = this.getById(waveId);
        if (Objects.isNull(wave)) {
            throw new CoolException("波次不能存在!!");
        }
        List<WaveItem> waveItems = waveItemService.list(new LambdaQueryWrapper<WaveItem>().eq(WaveItem::getWaveId, waveId));
        if (waveItems.isEmpty()) {
            throw new CoolException("波次明细不存在!!");
        }
        List<WaveItem> itemPreview = null;
        try {
            itemPreview = getLocs(waveItems);
        } catch (Exception e) {
            throw new CoolException("库位获取失败!!!!");
        }
        return itemPreview;
    }
    /**
     * @param
     * @param waveItems
     * @return
     * @author Ryan
     * @description 根据物料编码,批次,动态字段 查询符合的库位,再根据库位中物料的数量选择最适合的库位
     * @time 2025/4/27 09:26
     */
    private synchronized List<WaveItem> getLocs(List<WaveItem> waveItems) throws Exception {
        //TODO  根据物料编码,批次,动态字段 查询符合的库位,再根据库位中物料的数量选择最适合的库位
        waveItems.forEach(waveItem -> {
            List<LocItem> locItems = locItemService.list(new QueryWrapper<LocItem>()
                    .select("id", "loc_id", "loc_code", "order_id", "SUM(anfme) anfme", "SUM(work_qty) work_qty", "splr_batch", "fields_index", "matnr_code")
                    .lambda()
                    .eq(LocItem::getMatnrCode, waveItem.getMatnrCode())
                    .eq(LocItem::getSplrBatch, waveItem.getSplrBatch())
                    .eq(StringUtils.isNotBlank(waveItem.getFieldsIndex()), LocItem::getFieldsIndex, waveItem.getFieldsIndex())
                    .groupBy(LocItem::getMatnrCode, LocItem::getSplrBatch, LocItem::getFieldsIndex, LocItem::getId));
            Double[] doubles = locItems.stream().map(LocItem::getAnfme).toArray(Double[]::new);
            List<Double> result = OptimalAlgorithmUtil.findBestCombination(doubles, waveItem.getAnfme());
            String locs = JSONArray.toJSONString(new ArrayList<>());
            if (!locItems.isEmpty()) {
                List<String> codes = locItems.stream().map(LocItem::getLocCode).collect(Collectors.toList());
                locs = JSONArray.toJSONString(codes);
            }
            waveItem.setStockLocs(locs);
        });
        return waveItems;
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/manager/utils/GroupMergeUtil.java
File was deleted
rsf-server/src/main/java/com/vincent/rsf/server/manager/utils/OptimalAlgorithmUtil.java
New file
@@ -0,0 +1,132 @@
package com.vincent.rsf.server.manager.utils;
import java.util.*;
import java.util.function.BinaryOperator;
import java.util.function.Function;
import java.util.stream.Collectors;
/**
 * @author Ryan
 * @description 根据对象多个属性合并处理
 * @param
 * @return
 * @time 2025/4/25 10:55
 */
public class OptimalAlgorithmUtil {
    private static List<Double> bestSolution;
    private static double target;
    private static double minDifference;
    private static final double EPSILON = 1e-10;
    /**
     * 根据多个属性分组合并对象列表
     *
     * @param list       待分组的对象列表
     * @param mergers    合并函数,定义如何合并同一组中的对象
     * @param keyGetters 分组属性的getter方法数组
     * @param <T>        对象类型
     * @param <K>        分组键类型
     * @return 分组合并后的对象列表
     */
    @SafeVarargs
    public static <T, K> List<T> groupAndMerge(
            List<T> list,
            BinaryOperator<T> mergers,
            Function<T, K>... keyGetters) {
        return new ArrayList<>(list.stream()
                .collect(Collectors.toMap(
                        item -> new GroupingKeys<>(Arrays.stream(keyGetters)
                                .map(getter -> getter.apply(item))
                                .toArray()),
                        Function.identity(),
                        mergers))
                .values());
    }
    /**
     * @author Ryan
     * @description 用于存储多个分组键的内部类
     * @param
     * @return
     * @time 2025/4/25 10:56
     */
    private static class GroupingKeys<K> {
        private final Object[] keys;
        private final int hashCode;
        public GroupingKeys(Object[] keys) {
            this.keys = keys;
            this.hashCode = Arrays.deepHashCode(keys);
        }
        @Override
        public boolean equals(Object o) {
            if (this == o) {
                return true;
            }
            if (o == null || getClass() != o.getClass()) {
                return false;
            }
            GroupingKeys<?> that = (GroupingKeys<?>) o;
            return Arrays.deepEquals(keys, that.keys);
        }
        @Override
        public int hashCode() {
            return hashCode;
        }
    }
    public static List<Double> findBestCombination(Double[] candidates, double targetSum) {
        bestSolution = null;
        target = targetSum;
        minDifference = Double.MAX_VALUE;
        Arrays.sort(candidates);
        backtrack(candidates, 0, new ArrayList<>(), 0.0);
        // 如果没有精确解,返回最接近的近似解
        return bestSolution != null ? bestSolution : new ArrayList<>();
    }
    private static void backtrack(Double[] candidates, int start,
                                  List<Double> current, double currentSum) {
        // 计算当前和与目标的差值
        double difference = Math.abs(currentSum - target);
        // 如果找到更优解(差值更小或差值相同但组合更短)
        if (difference < minDifference - EPSILON ||
                (Math.abs(difference - minDifference) < EPSILON &&
                        (bestSolution == null || current.size() < bestSolution.size()))) {
            minDifference = difference;
            bestSolution = new ArrayList<>(current);
        }
        // 如果已经找到精确解,不需要继续搜索更长的组合
        if (minDifference < EPSILON) {
            return;
        }
        // 遍历候选数字
        for (int i = start; i < candidates.length; i++) {
            double num = candidates[i];
            // 剪枝:如果当前和已经远大于目标值,跳过
            if (currentSum + num > target + minDifference + EPSILON) {
                continue;
            }
            // 跳过重复数字
            if (i > start && Math.abs(candidates[i] - candidates[i - 1]) < EPSILON) {
                continue;
            }
            current.add(num);
            backtrack(candidates, i + 1, current, currentSum + num);
            current.remove(current.size() - 1);
        }
    }
}