| | |
| | | import request from "../utils/request"; |
| | | import * as Common from "../utils/common"; |
| | | |
| | | // 出库历史单与入库历史单共用 asnOrderLog 接口,仅前端 resource 不同 |
| | | const getApiResource = (resource) => (resource === "outStockOrderLog" ? "asnOrderLog" : resource); |
| | | |
| | | const MyDataProvider = { |
| | | // *** https://marmelab.com/react-admin/DataProviderWriting.html *** |
| | | |
| | | // get a list of records based on sort, filter, and pagination |
| | | getList: async (resource, params) => { |
| | | // console.log("getList", resource, params); |
| | | const apiResource = getApiResource(resource); |
| | | const _params = Common.integrateParams(params); |
| | | const res = await request.post(resource + "/page", _params); |
| | | const res = await request.post(apiResource + "/page", _params); |
| | | const { code, msg, data } = res.data; |
| | | if (code === 200) { |
| | | return Promise.resolve({ |
| | |
| | | |
| | | // get a single record by id |
| | | getOne: async (resource, params) => { |
| | | // console.log("getOne", resource, params); |
| | | const res = await request.get(resource + "/" + params.id); |
| | | const apiResource = getApiResource(resource); |
| | | const res = await request.get(apiResource + "/" + params.id); |
| | | const { code, msg, data } = res.data; |
| | | if (code === 200) { |
| | | return Promise.resolve({ |
| | |
| | | |
| | | // get a list of records based on an array of ids |
| | | getMany: async (resource, params) => { |
| | | |
| | | if (resource === "user") { |
| | | await new Promise((r) => setTimeout(r, 1000)); |
| | | } |
| | | const res = await request.post(resource + "/many/" + params.ids); |
| | | const apiResource = getApiResource(resource); |
| | | const res = await request.post(apiResource + "/many/" + params.ids); |
| | | const { code, msg, data } = res.data; |
| | | if (code === 200) { |
| | | return Promise.resolve({ |
| | |
| | | |
| | | // create a record |
| | | create: async (resource, params) => { |
| | | const res = await request.post(resource + "/save", params?.data); |
| | | const apiResource = getApiResource(resource); |
| | | const res = await request.post(apiResource + "/save", params?.data); |
| | | const { code, msg, data } = res.data; |
| | | if (code === 200) { |
| | | return Promise.resolve({ |
| | |
| | | |
| | | // update a record based on a patch |
| | | update: async (resource, params) => { |
| | | const res = await request.post(resource + "/update", { |
| | | const apiResource = getApiResource(resource); |
| | | const res = await request.post(apiResource + "/update", { |
| | | id: params.id, |
| | | ...params.data, |
| | | }); |
| | |
| | | |
| | | // update a list of records based on an array of ids and a common patch |
| | | updateMany: async (resource, params) => { |
| | | console.log("updateMany", resource, params); |
| | | const apiResource = getApiResource(resource); |
| | | const res = await request.post( |
| | | resource + "/update/many", |
| | | apiResource + "/update/many", |
| | | params.ids.map((id) => ({ id, ...params.data })), |
| | | ); |
| | | const { code, msg, data } = res.data; |
| | |
| | | |
| | | // delete a record by id |
| | | delete: async (resource, params) => { |
| | | console.log("delete", resource, params); |
| | | const res = await request.post(resource + "/remove/" + [params.id]); |
| | | const apiResource = getApiResource(resource); |
| | | const res = await request.post(apiResource + "/remove/" + [params.id]); |
| | | const { code, msg, data } = res.data; |
| | | if (code === 200) { |
| | | return Promise.resolve({ |
| | |
| | | |
| | | // delete a list of records based on an array of ids |
| | | deleteMany: async (resource, params) => { |
| | | console.log("deleteMany", resource, params); |
| | | const res = await request.post(resource + "/remove/" + params?.ids); |
| | | const apiResource = getApiResource(resource); |
| | | const res = await request.post(apiResource + "/remove/" + params?.ids); |
| | | const { code, msg, data } = res.data; |
| | | if (code === 200) { |
| | | return Promise.resolve({ |
| | |
| | | |
| | | // export excel from all data |
| | | export: async (resource, params) => { |
| | | const apiResource = getApiResource(resource); |
| | | const _params = Common.integrateParams(params); |
| | | try { |
| | | const res = await request.post(`${resource}/export`, _params, { |
| | | const res = await request.post(`${apiResource}/export`, _params, { |
| | | responseType: "blob", |
| | | }); |
| | | return res; |
| | |
| | | asnOrder: 'AsnOrder', |
| | | asnOrderItem: 'AsnOrderItem', |
| | | asnOrderLog: 'asnOrderLog', |
| | | outStockOrderLog: 'Outbound Order Log', |
| | | asnOrderItemLog: 'asnOrderItemLog', |
| | | purchase: 'Purchase', |
| | | purchaseItem: 'PurchaseItem', |
| | |
| | | const customChineseMessages = { |
| | | ...chineseMessages, |
| | | hello: '你好世界', |
| | | resources: { |
| | | config: { name: '配置参数' }, |
| | | }, |
| | | common: { |
| | | response: { |
| | | success: "操作成功", |
| | |
| | | companys: '往来企业', |
| | | serialRuleItem: '编码规则子表', |
| | | serialRule: '编码规则', |
| | | asnOrder: '收货通知单', |
| | | asnOrder: '入库通知单', |
| | | asnOrderItem: '收货明细', |
| | | asnOrderLog: '收货历史单', |
| | | asnOrderLog: '入库历史单', |
| | | outStockOrderLog: '出库历史单', |
| | | asnOrderItemLog: '收货历史明细', |
| | | purchase: 'PO单', |
| | | purchaseItem: 'PO单明细', |
| | |
| | | logs: '日志', |
| | | permissions: '权限管理', |
| | | delivery: 'DO单', |
| | | outStock: '出库单', |
| | | outStock: '出库通知单', |
| | | outStockItem: '出库单明细', |
| | | inStockPoces: '入库管理', |
| | | outStockPoces: '出库管理', |
| | |
| | | maxWeight: "最大重量", |
| | | }, |
| | | asnOrder: { |
| | | code: "ASN单号", |
| | | code: "WMS单号", |
| | | poCode: "PO编码", |
| | | poId: "PO标识", |
| | | type: "单据类型", |
| | |
| | | selectWave: '波次规则', |
| | | }, |
| | | ra: { |
| | | boolean: { |
| | | true: '正常', |
| | | false: '禁用', |
| | | }, |
| | | message: { |
| | | delete_title: '删除 %{name} #%{id}', |
| | | delete_content: '您确定要删除此项吗?', |
| | | }, |
| | | action: { |
| | | search: '搜索', |
| | | add_filter: '过滤条件', |
| | |
| | | } else { |
| | | if (node.component) { |
| | | // RCS测试:指向独立页路由,可单独打开一整页(无侧边栏/标签栏) |
| | | const to = node.component === 'rcsTest' ? '/rcsTest-page' : node.component; |
| | | const to = node.component === 'rcsTest' ? '/rcsTest-page' : `/${node.component}`; |
| | | return ( |
| | | <MenuItemLink |
| | | key={node.id} |
| | |
| | | import waitPakin from "./waitPakin"; |
| | | import waitPakinLog from "./histories/waitPakinLog"; |
| | | import asnOrderLog from "./histories/asnOrderLog"; |
| | | import outStockOrderLog from "./histories/outStockOrderLog"; |
| | | import task from "./task"; |
| | | import taskLog from "./histories/taskLog"; |
| | | import stock from "./orders/stock"; |
| | |
| | | import inStatisticItem from './statistics/inStockItem'; |
| | | import statisticCount from './statistics/stockStatisticNum'; |
| | | import rcsTest from './rcsTest'; |
| | | import openApiApp from './system/openApiApp'; |
| | | |
| | | const ResourceContent = (node) => { |
| | | switch (node.component) { |
| | |
| | | return asnOrder; |
| | | case "asnOrderLog": |
| | | return asnOrderLog; |
| | | case "outStockOrderLog": |
| | | return outStockOrderLog; |
| | | case "purchase": |
| | | return purchase; |
| | | case "fields": |
| | |
| | | return statisticCount; |
| | | case "rcsTest": |
| | | return rcsTest; |
| | | case "openApiApp": |
| | | return openApiApp; |
| | | default: |
| | | return { |
| | | list: ListGuesser, |
| | |
| | | <TextInput source="trackCode" label="table.field.asnOrderItemLog.trackCode" />, |
| | | <TextInput source="barcode" label="table.field.asnOrderItemLog.barcode" />, |
| | | <TextInput source="packName" label="table.field.asnOrderItemLog.packName" />, |
| | | <SelectInput source="ntyStatus" label="table.field.asnOrderItemLog.ntyStatus" |
| | | choices={[ |
| | | { id: 0, name: ' 未上报' }, |
| | | { id: 1, name: ' 已上报' }, |
| | | ]} |
| | | />, |
| | | // <SelectInput source="ntyStatus" label="table.field.asnOrderItemLog.ntyStatus" |
| | | // choices={[ |
| | | // { id: 0, name: ' 未上报' }, |
| | | // { id: 1, name: ' 已上报' }, |
| | | // ]} |
| | | // />, |
| | | |
| | | <TextInput label="common.field.memo" source="memo" />, |
| | | <SelectInput |
| | |
| | | />, |
| | | ] |
| | | |
| | | const AsnOrderItemLogList = () => { |
| | | /** |
| | | * @param {Object} props |
| | | * @param {number} [props.logId] - 入库历史单主键,传入时只显示该单的明细(用于详情页) |
| | | */ |
| | | const AsnOrderItemLogList = ({ logId: logIdProp }) => { |
| | | const translate = useTranslate(); |
| | | const [createDialog, setCreateDialog] = useState(false); |
| | | const [drawerVal, setDrawerVal] = useState(false); |
| | | const recodeId = useGetRecordId(); |
| | | const recordId = useGetRecordId(); |
| | | const logId = logIdProp != null ? logIdProp : recordId; |
| | | |
| | | return ( |
| | | <Box display="flex"> |
| | |
| | | title={"menu.asnOrderItemLog"} |
| | | empty={false} |
| | | filters={filters} |
| | | filter={{ logId: recodeId }} |
| | | filter={{ logId }} |
| | | sort={{ field: "create_time", order: "desc" }} |
| | | actions={( |
| | | <TopToolbar> |
| | |
| | | <TextField source="qrcode" label="table.field.asnOrderItemLog.qrcode" /> |
| | | <TextField source="trackCode" label="table.field.asnOrderItemLog.trackCode" /> |
| | | <TextField source="packName" label="table.field.asnOrderItemLog.packName" /> |
| | | <TextField source="ntyStatus$" label="table.field.asnOrderItemLog.ntyStatus" sortable={false} /> |
| | | {/*<TextField source="ntyStatus$" label="table.field.asnOrderItemLog.ntyStatus" sortable={false} />*/} |
| | | <TextField source="updateBy$" label="common.field.updateBy" /> |
| | | <TextField source="createBy$" label="common.field.createBy" /> |
| | | <DateField source="createTime" label="common.field.createTime" showTime /> |
| | |
| | | ]} |
| | | /> |
| | | </Grid> |
| | | {/* 质检上报状态 |
| | | <Grid item xs={6} display="flex" gap={1}> |
| | | <SelectInput |
| | | label="table.field.asnOrderLog.ntyStatus" |
| | |
| | | ]} |
| | | /> |
| | | </Grid> |
| | | */} |
| | | |
| | | <Grid item xs={6} display="flex" gap={1}> |
| | | <StatusSelectInput /> |
| | |
| | | readOnly |
| | | source="arrTime" |
| | | /> |
| | | {/* 质检上报状态 |
| | | <SelectInput |
| | | label="table.field.asnOrderLog.ntyStatus" |
| | | source="ntyStatus" |
| | |
| | | ]} |
| | | validate={required()} |
| | | /> |
| | | */} |
| | | </Stack> |
| | | </Grid> |
| | | </Grid> |
| | |
| | | import React, { useState, useRef, useEffect, useMemo, useCallback } from "react"; |
| | | import { useNavigate } from 'react-router-dom'; |
| | | 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, |
| | | Button, |
| | | useRecordSelection, |
| | | useRefresh, |
| | | } from 'react-admin'; |
| | | import { PAGE_DRAWER_WIDTH, OPERATE_MODE, DEFAULT_PAGE_SIZE } from '@/config/setting'; |
| | | import DictionarySelect from "../../components/DictionarySelect"; |
| | | import MyCreateButton from "../../components/MyCreateButton"; |
| | | import MyExportButton from '../../components/MyExportButton'; |
| | | import { Box, Typography, Card, Stack } from '@mui/material'; |
| | | import ConfirmButton from '../../components/ConfirmButton'; |
| | | import PageDrawer from "../../components/PageDrawer"; |
| | | import AsnOrderLogCreate from "./AsnOrderLogCreate"; |
| | | import CachedIcon from '@mui/icons-material/Cached'; |
| | | import EmptyData from "../../components/EmptyData"; |
| | | import AsnOrderLogPanel from "./AsnOrderLogPanel"; |
| | | import { styled } from '@mui/material/styles'; |
| | | import * as Common from '@/utils/common'; |
| | | import request from '@/utils/request'; |
| | | import React from "react"; |
| | | import AsnOrderLogListBase from "./AsnOrderLogListBase"; |
| | | |
| | | |
| | | const StyledDatagrid = styled(DatagridConfigurable)(({ theme }) => ({ |
| | | '& .css-1vooibu-MuiSvgIcon-root': { |
| | | height: '.9em' |
| | | }, |
| | | '& .RaDatagrid-row': { |
| | | cursor: 'auto' |
| | | }, |
| | | '& .column-name': { |
| | | }, |
| | | '& .opt': { |
| | | width: 150 |
| | | }, |
| | | '& .MuiTableCell-root': { |
| | | whiteSpace: 'nowrap', |
| | | overflow: 'visible', |
| | | textOverflow: 'unset' |
| | | } |
| | | })); |
| | | |
| | | |
| | | |
| | | const AsnOrderLogList = () => { |
| | | const translate = useTranslate(); |
| | | const [createDialog, setCreateDialog] = useState(false); |
| | | const [drawerVal, setDrawerVal] = useState(false); |
| | | const dicts = JSON.parse(localStorage.getItem('sys_dicts'))?.filter(dict => (dict.dictTypeCode == 'sys_order_type')) || []; |
| | | |
| | | const filters = [ |
| | | <SearchInput source="condition" alwaysOn />, |
| | | <TextInput source="code" label="table.field.asnOrderLog.code" />, |
| | | <TextInput source="poCode" label="table.field.asnOrderLog.poCode" />, |
| | | <NumberInput source="poId" label="table.field.asnOrderLog.poId" />, |
| | | // <TextInput source="type" label="table.field.asnOrderLog.type" />, |
| | | // <TextInput source="wkType" label="table.field.asnOrderLog.wkType" />, |
| | | <NumberInput source="anfme" label="table.field.asnOrderLog.anfme" />, |
| | | <NumberInput source="qty" label="table.field.asnOrderLog.qty" />, |
| | | <TextInput source="logisNo" label="table.field.asnOrderLog.logisNo" />, |
| | | <DateInput source="arrTime" label="table.field.asnOrderLog.arrTime" />, |
| | | // <SelectInput source="ntyStatus" label="table.field.asnOrderLog.ntyStatus" |
| | | // choices={[ |
| | | // { id: 0, name: ' 未上报' }, |
| | | // { id: 1, name: ' 已上报' }, |
| | | // { id: 2, name: ' 部分上报' }, |
| | | // ]} |
| | | // />, |
| | | <AutocompleteInput |
| | | choices={dicts} |
| | | optionText="label" |
| | | label="table.field.asnOrder.type" |
| | | source="type" |
| | | // defaultValue="in" |
| | | optionValue="value" |
| | | parse={v => v} |
| | | alwaysOn |
| | | />, |
| | | <ReferenceInput source="wkType" reference="dictData" filter={{ dictTypeCode: 'sys_business_type', group: "1" }} label="table.field.asnOrder.wkType" alwaysOn> |
| | | <AutocompleteInput label="table.field.asnOrder.wkType" optionValue="value" /> |
| | | </ReferenceInput>, |
| | | <DictionarySelect |
| | | label='table.field.asnOrder.exceStatus' |
| | | name="exceStatus" |
| | | group="1" |
| | | dictTypeCode="sys_asn_exce_status" |
| | | alwaysOn |
| | | />, |
| | | ] |
| | | |
| | | return ( |
| | | <Box display="flex"> |
| | | <List |
| | | sx={{ |
| | | flexGrow: 1, |
| | | transition: (theme) => |
| | | theme.transitions.create(['all'], { |
| | | duration: theme.transitions.duration.enteringScreen, |
| | | }), |
| | | marginRight: drawerVal ? `${PAGE_DRAWER_WIDTH}px` : 0, |
| | | }} |
| | | title={"menu.asnOrderLog"} |
| | | empty={false} |
| | | filters={filters} |
| | | sort={{ field: "create_time", order: "desc" }} |
| | | actions={( |
| | | <TopToolbar> |
| | | <FilterButton /> |
| | | <SelectColumnsButton preferenceKey='asnOrderLog' /> |
| | | {/* <MyExportButton /> */} |
| | | </TopToolbar> |
| | | )} |
| | | perPage={DEFAULT_PAGE_SIZE} |
| | | > |
| | | <StyledDatagrid |
| | | preferenceKey='asnOrderLog' |
| | | bulkActionButtons={false} |
| | | rowClick={'edit'} |
| | | expand={false} |
| | | expandSingle={true} |
| | | omit={['id', 'createTime', 'createBy', 'memo', 'logisNo', 'poId', 'rleStatus$', 'statusBool', 'createBy$']} |
| | | > |
| | | <NumberField source="id" /> |
| | | <TextField source="code" label="table.field.asnOrderLog.code" /> |
| | | <TextField source="poCode" label="table.field.asnOrderLog.poCode" /> |
| | | <NumberField source="poId" label="table.field.asnOrderLog.poId" /> |
| | | <TextField source="type$" label="table.field.asnOrderLog.type" /> |
| | | <TextField source="wkType$" label="table.field.asnOrderLog.wkType" /> |
| | | <NumberField source="anfme" label="table.field.asnOrderLog.anfme" /> |
| | | <NumberField source="qty" label="table.field.asnOrderLog.qty" /> |
| | | <TextField source="logisNo" label="table.field.asnOrderLog.logisNo" /> |
| | | <DateField source="arrTime" label="table.field.asnOrderLog.arrTime" showTime /> |
| | | <TextField source="rleStatus$" label="table.field.asnOrderLog.rleStatus" sortable={false} /> |
| | | <TextField source="ntyStatus$" label="table.field.asnOrderLog.ntyStatus" sortable={false} /> |
| | | <TextField source="updateBy$" label="common.field.updateBy" /> |
| | | <DateField source="updateTime" label="common.field.updateTime" showTime /> |
| | | <TextField source="createBy$" label="common.field.createBy" /> |
| | | <DateField source="createTime" label="common.field.createTime" showTime /> |
| | | <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"> |
| | | <ContinueButton /> |
| | | </WrapperField> |
| | | </StyledDatagrid> |
| | | </List> |
| | | <AsnOrderLogCreate |
| | | open={createDialog} |
| | | setOpen={setCreateDialog} |
| | | /> |
| | | <PageDrawer |
| | | title='AsnOrderLog Detail' |
| | | drawerVal={drawerVal} |
| | | setDrawerVal={setDrawerVal} |
| | | > |
| | | </PageDrawer> |
| | | </Box> |
| | | ) |
| | | /** 入库历史单列表:固定 type=in,请求后端 asnOrderLog 接口 */ |
| | | export default function AsnOrderLogList() { |
| | | return <AsnOrderLogListBase typeFilter="in" listTitle="menu.asnOrderLog" />; |
| | | } |
| | | |
| | | export default AsnOrderLogList; |
| | | |
| | | |
| | | const ContinueButton = () => { |
| | | const refresh = useRefresh(); |
| | | const record = useRecordContext(); |
| | | const notify = useNotify(); |
| | | const continueReceipt = async () => { |
| | | const { data: { code, data, msg } } = await request.post(`/asnOrderLog/continue/${record.id}`); |
| | | if (code === 200) { |
| | | notify(msg); |
| | | } else { |
| | | notify(msg); |
| | | } |
| | | refresh(); |
| | | } |
| | | |
| | | return ( |
| | | record.type == 'in' ? <ConfirmButton label={"toolbar.continue"} startIcon={<CachedIcon />} onConfirm={continueReceipt} /> : <></> |
| | | ) |
| | | } |
| New file |
| | |
| | | import React, { useState } from "react"; |
| | | import { |
| | | List, |
| | | DatagridConfigurable, |
| | | SearchInput, |
| | | TopToolbar, |
| | | SelectColumnsButton, |
| | | ShowButton, |
| | | FilterButton, |
| | | WrapperField, |
| | | TextField, |
| | | NumberField, |
| | | DateField, |
| | | BooleanField, |
| | | TextInput, |
| | | DateInput, |
| | | NumberInput, |
| | | ReferenceInput, |
| | | AutocompleteInput, |
| | | } from 'react-admin'; |
| | | import { PAGE_DRAWER_WIDTH, DEFAULT_PAGE_SIZE } from '@/config/setting'; |
| | | import DictionarySelect from "../../components/DictionarySelect"; |
| | | import { Box } from '@mui/material'; |
| | | import PageDrawer from "../../components/PageDrawer"; |
| | | import AsnOrderLogCreate from "./AsnOrderLogCreate"; |
| | | import { styled } from '@mui/material/styles'; |
| | | |
| | | const StyledDatagrid = styled(DatagridConfigurable)(({ theme }) => ({ |
| | | '& .css-1vooibu-MuiSvgIcon-root': { height: '.9em' }, |
| | | '& .RaDatagrid-row': { cursor: 'auto' }, |
| | | '& .opt': { width: 150 }, |
| | | '& .MuiTableCell-root': { whiteSpace: 'nowrap', overflow: 'visible', textOverflow: 'unset' } |
| | | })); |
| | | |
| | | /** |
| | | * 入库/出库历史单列表共用骨架,仅 type 与标题由外部传入。 |
| | | * 入库历史单:typeFilter="in", listTitle="menu.asnOrderLog" |
| | | * 出库历史单:typeFilter="out", listTitle="menu.outStockOrderLog" |
| | | * 后端接口均为 asnOrderLog(出库由 dataProvider 映射),参数 filter.type 不同。 |
| | | */ |
| | | export default function AsnOrderLogListBase({ typeFilter, listTitle }) { |
| | | const [createDialog, setCreateDialog] = useState(false); |
| | | const [drawerVal, setDrawerVal] = useState(false); |
| | | |
| | | const filters = [ |
| | | <SearchInput source="condition" alwaysOn />, |
| | | <TextInput source="code" label="table.field.asnOrderLog.code" />, |
| | | <TextInput source="poCode" label="table.field.asnOrderLog.poCode" />, |
| | | <NumberInput source="poId" label="table.field.asnOrderLog.poId" />, |
| | | <NumberInput source="anfme" label="table.field.asnOrderLog.anfme" />, |
| | | <NumberInput source="qty" label="table.field.asnOrderLog.qty" />, |
| | | <TextInput source="logisNo" label="table.field.asnOrderLog.logisNo" />, |
| | | <DateInput source="arrTime" label="table.field.asnOrderLog.arrTime" />, |
| | | <ReferenceInput source="wkType" reference="dictData" filter={{ dictTypeCode: 'sys_business_type', group: "1" }} label="table.field.asnOrder.wkType" alwaysOn> |
| | | <AutocompleteInput label="table.field.asnOrder.wkType" optionValue="value" /> |
| | | </ReferenceInput>, |
| | | <DictionarySelect |
| | | label='table.field.asnOrder.exceStatus' |
| | | name="exceStatus" |
| | | group="1" |
| | | dictTypeCode="sys_asn_exce_status" |
| | | alwaysOn |
| | | />, |
| | | ]; |
| | | |
| | | return ( |
| | | <Box display="flex"> |
| | | <List |
| | | key={`orderLog-list-${typeFilter}`} |
| | | sx={{ |
| | | flexGrow: 1, |
| | | transition: (theme) => theme.transitions.create(['all'], { duration: theme.transitions.duration.enteringScreen }), |
| | | marginRight: drawerVal ? `${PAGE_DRAWER_WIDTH}px` : 0, |
| | | }} |
| | | title={listTitle} |
| | | empty={false} |
| | | filters={filters} |
| | | filterDefaultValues={{ type: typeFilter }} |
| | | filter={{ type: typeFilter }} |
| | | sort={{ field: "create_time", order: "desc" }} |
| | | actions={( |
| | | <TopToolbar> |
| | | <FilterButton /> |
| | | <SelectColumnsButton preferenceKey='asnOrderLog' /> |
| | | </TopToolbar> |
| | | )} |
| | | perPage={DEFAULT_PAGE_SIZE} |
| | | > |
| | | <StyledDatagrid |
| | | preferenceKey='asnOrderLog' |
| | | bulkActionButtons={false} |
| | | rowClick={false} |
| | | expand={false} |
| | | expandSingle={true} |
| | | omit={['id', 'createTime', 'createBy', 'memo', 'logisNo', 'poId', 'rleStatus$', 'statusBool', 'createBy$']} |
| | | > |
| | | <NumberField source="id" /> |
| | | <TextField source="code" label="table.field.asnOrderLog.code" /> |
| | | <TextField source="poCode" label="table.field.asnOrderLog.poCode" /> |
| | | <NumberField source="poId" label="table.field.asnOrderLog.poId" /> |
| | | <TextField source="type$" label="table.field.asnOrderLog.type" /> |
| | | <TextField source="wkType$" label="table.field.asnOrderLog.wkType" /> |
| | | <NumberField source="anfme" label="table.field.asnOrderLog.anfme" /> |
| | | <NumberField source="qty" label="table.field.asnOrderLog.qty" /> |
| | | <TextField source="logisNo" label="table.field.asnOrderLog.logisNo" /> |
| | | <DateField source="arrTime" label="table.field.asnOrderLog.arrTime" showTime /> |
| | | <TextField source="rleStatus$" label="table.field.asnOrderLog.rleStatus" sortable={false} /> |
| | | <TextField source="updateBy$" label="common.field.updateBy" /> |
| | | <DateField source="updateTime" label="common.field.updateTime" showTime /> |
| | | <TextField source="createBy$" label="common.field.createBy" /> |
| | | <DateField source="createTime" label="common.field.createTime" showTime /> |
| | | <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"> |
| | | <ShowButton label="toolbar.detail" /> |
| | | </WrapperField> |
| | | </StyledDatagrid> |
| | | </List> |
| | | <AsnOrderLogCreate open={createDialog} setOpen={setCreateDialog} /> |
| | | <PageDrawer title='AsnOrderLog Detail' drawerVal={drawerVal} setDrawerVal={setDrawerVal} /> |
| | | </Box> |
| | | ); |
| | | } |
| | |
| | | field: 'packName', |
| | | headerName: translate('table.field.asnOrderItemLog.packName') |
| | | }, |
| | | /* 质检上报状态 |
| | | { |
| | | field: 'ntyStatus$', |
| | | headerName: translate('table.field.asnOrderItemLog.ntyStatus') |
| | | }] |
| | | } |
| | | */] |
| | | |
| | | const maktxChange = (value) => { |
| | | setMaktx(value) |
| | |
| | | import { BooleanField, DateField, NumberField, ReferenceField, Show, SimpleShowLayout, TextField ,DateInput, |
| | | SelectInput,required,useTranslate, |
| | | useRecordContext,} from 'react-admin'; |
| | | import { Stack, Grid, Box, Typography, Card } from '@mui/material'; |
| | | import { EDIT_MODE, REFERENCE_INPUT_PAGESIZE } from '@/config/setting'; |
| | | import EditBaseAside from "../../components/EditBaseAside"; |
| | | import CustomerTopToolBar from "../../components/EditTopToolBar"; |
| | | import AsnOrderItemLogList from "./AsnOrderItemLogList" |
| | | import { Stack, Grid, Box, Typography, Card } from '@mui/material'; |
| | | import { EDIT_MODE, REFERENCE_INPUT_PAGESIZE } from '@/config/setting'; |
| | | import EditBaseAside from "../../components/EditBaseAside"; |
| | | import CustomerTopToolBar from "../../components/EditTopToolBar"; |
| | | import AsnOrderItemLogList from "./AsnOrderItemLogList"; |
| | | |
| | | const AsnOrderLogDetailWithItems = () => { |
| | | const record = useRecordContext(); |
| | | const translate = useTranslate(); |
| | | if (!record?.id) return null; |
| | | return ( |
| | | <> |
| | | <Grid item xs={24} md={16} sx={{ marginTop: '1em', width: '100%' }}> |
| | | <Typography variant="h6" gutterBottom> |
| | | {translate('common.edit.title.common')} |
| | | </Typography> |
| | | </Grid> |
| | | <AsnOrderItemLogList logId={record.id} /> |
| | | </> |
| | | ); |
| | | }; |
| | | |
| | | const Aa = () =>{ |
| | | const translate = useTranslate(); |
| | |
| | | <SimpleShowLayout |
| | | shouldUnregister |
| | | warnWhenUnsavedChanges |
| | | |
| | | mode="onTouched" |
| | | defaultValues={{}} |
| | | > |
| | |
| | | <DateField source="arrTime" label="type" showTime/> |
| | | </Box> |
| | | </Grid> |
| | | <Grid item display="flex" gap={1} minWidth={150}> |
| | | {/* 质检上报状态 |
| | | <Grid item display="flex" gap={1} minWidth={150}> |
| | | <Box flexGrow={1}> |
| | | <Typography variant="body2" sx={{fontSize: 20}}> |
| | | {translate('table.field.asnOrderLog.ntyStatus')} |
| | | </Typography> |
| | | <TextField source="ntyStatus$" label="type"/> |
| | | </Box> |
| | | </Grid> |
| | | </Grid> |
| | | */} |
| | | </Stack> |
| | | </Grid> |
| | | </Grid> |
| | | |
| | | <AsnOrderLogDetailWithItems /> |
| | | </SimpleShowLayout> |
| | | </Show> |
| | | <Grid item xs={24} md={16} sx={{ marginTop: '1em' }}> |
| | | <Typography variant="h6" gutterBottom > |
| | | {translate('common.edit.title.common')} |
| | | </Typography> |
| | | </Grid> |
| | | <AsnOrderItemLogList /> |
| | | </> |
| | | |
| | | ); |
| | | } |
| | | |
| New file |
| | |
| | | import React from "react"; |
| | | import AsnOrderLogListBase from "../asnOrderLog/AsnOrderLogListBase"; |
| | | |
| | | /** 出库历史单列表:固定 type=out,请求后端 asnOrderLog 接口(dataProvider 映射 outStockOrderLog→asnOrderLog) */ |
| | | export default function OutStockOrderLogList() { |
| | | return <AsnOrderLogListBase typeFilter="out" listTitle="menu.outStockOrderLog" />; |
| | | } |
| New file |
| | |
| | | import React from "react"; |
| | | import OutStockOrderLogList from "./OutStockOrderLogList"; |
| | | import AsnOrderLogEdit from "../asnOrderLog/AsnOrderLogEdit"; |
| | | import AsnorderlogShow from "../asnOrderLog/AsnOrderLogShow"; |
| | | |
| | | export default { |
| | | list: OutStockOrderLogList, |
| | | edit: AsnOrderLogEdit, |
| | | show: AsnorderlogShow, |
| | | recordRepresentation: (record) => `${record.id}`, |
| | | }; |
| New file |
| | |
| | | import React from "react"; |
| | | import { |
| | | CreateBase, |
| | | useTranslate, |
| | | TextInput, |
| | | NumberInput, |
| | | SaveButton, |
| | | SelectInput, |
| | | Toolbar, |
| | | Form, |
| | | required, |
| | | useNotify, |
| | | } from 'react-admin'; |
| | | import { Dialog, DialogContent, DialogTitle, Grid, Box } from '@mui/material'; |
| | | import DialogCloseButton from "@/page/components/DialogCloseButton"; |
| | | |
| | | const OpenApiAppCreate = (props) => { |
| | | const { open, setOpen } = props; |
| | | const notify = useNotify(); |
| | | |
| | | const handleClose = (event, reason) => { |
| | | if (reason !== "backdropClick") setOpen(false); |
| | | }; |
| | | |
| | | const handleSuccess = () => { |
| | | setOpen(false); |
| | | notify('新增成功'); |
| | | }; |
| | | |
| | | const handleError = (error) => { |
| | | notify(error?.message || '新增失败', { type: 'error' }); |
| | | }; |
| | | |
| | | return ( |
| | | <CreateBase |
| | | resource="openApiApp" |
| | | record={{}} |
| | | mutationOptions={{ onSuccess: handleSuccess, onError: handleError }} |
| | | > |
| | | <Dialog |
| | | open={open} |
| | | onClose={handleClose} |
| | | fullWidth |
| | | disableRestoreFocus |
| | | maxWidth="sm" |
| | | > |
| | | <Form> |
| | | <DialogTitle sx={{ position: 'sticky', top: 0, backgroundColor: 'background.paper', zIndex: 1000 }}> |
| | | 新增应用 |
| | | <Box sx={{ position: 'absolute', top: 8, right: 8, zIndex: 1001 }}> |
| | | <DialogCloseButton onClose={handleClose} /> |
| | | </Box> |
| | | </DialogTitle> |
| | | <DialogContent sx={{ mt: 2 }}> |
| | | <Grid container rowSpacing={2} columnSpacing={2}> |
| | | <Grid item xs={12}> |
| | | <TextInput label="应用ID" source="id" validate={required()} fullWidth /> |
| | | </Grid> |
| | | <Grid item xs={12}> |
| | | <TextInput label="应用密钥" source="screct" validate={required()} fullWidth /> |
| | | </Grid> |
| | | <Grid item xs={12}> |
| | | <TextInput label="应用名称" source="name" fullWidth /> |
| | | </Grid> |
| | | <Grid item xs={12}> |
| | | <TextInput label="应用URL" source="url" fullWidth /> |
| | | </Grid> |
| | | <Grid item xs={12}> |
| | | <SelectInput |
| | | label="启用状态" |
| | | source="enable" |
| | | choices={[ |
| | | { id: 1, name: '启用' }, |
| | | { id: 0, name: '未启用' }, |
| | | ]} |
| | | defaultValue={1} |
| | | fullWidth |
| | | /> |
| | | </Grid> |
| | | </Grid> |
| | | </DialogContent> |
| | | <Box sx={{ position: 'sticky', bottom: 0, backgroundColor: 'background.paper', zIndex: 1000, p: 2, display: 'flex', justifyContent: 'flex-end' }}> |
| | | <Toolbar><SaveButton /></Toolbar> |
| | | </Box> |
| | | </Form> |
| | | </Dialog> |
| | | </CreateBase> |
| | | ); |
| | | }; |
| | | |
| | | export default OpenApiAppCreate; |
| New file |
| | |
| | | import React from "react"; |
| | | import { |
| | | Edit, |
| | | SimpleForm, |
| | | TextInput, |
| | | SaveButton, |
| | | SelectInput, |
| | | Toolbar, |
| | | required, |
| | | DeleteButton, |
| | | } from 'react-admin'; |
| | | import { Grid, Stack } from '@mui/material'; |
| | | import { EDIT_MODE } from '@/config/setting'; |
| | | import EditBaseAside from "@/page/components/EditBaseAside"; |
| | | import CustomerTopToolBar from "@/page/components/EditTopToolBar"; |
| | | |
| | | const FormToolbar = () => ( |
| | | <Toolbar sx={{ justifyContent: 'space-between' }}> |
| | | <SaveButton /> |
| | | <DeleteButton mutationMode="optimistic" /> |
| | | </Toolbar> |
| | | ); |
| | | |
| | | const OpenApiAppEdit = () => { |
| | | return ( |
| | | <Edit |
| | | resource="openApiApp" |
| | | redirect="list" |
| | | mutationMode={EDIT_MODE} |
| | | actions={<CustomerTopToolBar />} |
| | | aside={<EditBaseAside />} |
| | | > |
| | | <SimpleForm |
| | | toolbar={<FormToolbar />} |
| | | defaultValues={{ enable: 1 }} |
| | | > |
| | | <Grid container width={{ xs: '100%', xl: '80%' }} rowSpacing={3} columnSpacing={3}> |
| | | <Grid item xs={12} md={8}> |
| | | <Stack spacing={2}> |
| | | <TextInput label="应用ID" source="id" validate={required()} disabled /> |
| | | <TextInput label="应用密钥" source="screct" validate={required()} fullWidth /> |
| | | <TextInput label="应用名称" source="name" fullWidth /> |
| | | <TextInput label="应用URL" source="url" fullWidth /> |
| | | <SelectInput |
| | | label="启用状态" |
| | | source="enable" |
| | | choices={[ |
| | | { id: 1, name: '启用' }, |
| | | { id: 0, name: '未启用' }, |
| | | ]} |
| | | fullWidth |
| | | /> |
| | | </Stack> |
| | | </Grid> |
| | | </Grid> |
| | | </SimpleForm> |
| | | </Edit> |
| | | ); |
| | | }; |
| | | |
| | | export default OpenApiAppEdit; |
| New file |
| | |
| | | import React, { useState } from "react"; |
| | | import { |
| | | List, |
| | | DatagridConfigurable, |
| | | SearchInput, |
| | | TopToolbar, |
| | | SelectColumnsButton, |
| | | EditButton, |
| | | FilterButton, |
| | | TextField, |
| | | TextInput, |
| | | FunctionField, |
| | | SelectInput, |
| | | WrapperField, |
| | | DeleteButton, |
| | | } from 'react-admin'; |
| | | import { Box } from '@mui/material'; |
| | | import { styled } from '@mui/material/styles'; |
| | | import OpenApiAppCreate from "./OpenApiAppCreate"; |
| | | import EmptyData from "@/page/components/EmptyData"; |
| | | import MyCreateButton from "@/page/components/MyCreateButton"; |
| | | import { OPERATE_MODE, DEFAULT_PAGE_SIZE } from '@/config/setting'; |
| | | |
| | | const StyledDatagrid = styled(DatagridConfigurable)(() => ({ |
| | | '& .RaDatagrid-row': { cursor: 'auto' }, |
| | | '& .opt': { width: 140 }, |
| | | })); |
| | | |
| | | const filters = [ |
| | | <SearchInput source="condition" alwaysOn />, |
| | | <TextInput source="id" label="应用ID" />, |
| | | <TextInput source="name" label="应用名称" />, |
| | | <SelectInput |
| | | label="启用状态" |
| | | source="enable" |
| | | choices={[ |
| | | { id: 1, name: '启用' }, |
| | | { id: 0, name: '未启用' }, |
| | | ]} |
| | | />, |
| | | ]; |
| | | |
| | | const OpenApiAppList = () => { |
| | | const [createDialog, setCreateDialog] = useState(false); |
| | | |
| | | return ( |
| | | <Box display="flex"> |
| | | <List |
| | | title="应用管理" |
| | | empty={<EmptyData onClick={() => setCreateDialog(true)} />} |
| | | filters={filters} |
| | | sort={{ field: "id", order: "asc" }} |
| | | actions={( |
| | | <TopToolbar> |
| | | <FilterButton /> |
| | | <MyCreateButton onClick={() => setCreateDialog(true)} /> |
| | | <SelectColumnsButton preferenceKey="openApiApp" /> |
| | | </TopToolbar> |
| | | )} |
| | | perPage={DEFAULT_PAGE_SIZE} |
| | | > |
| | | <StyledDatagrid |
| | | preferenceKey="openApiApp" |
| | | bulkActionButtons={() => <DeleteButton mutationMode={OPERATE_MODE} />} |
| | | rowClick={false} |
| | | omit={['tenantId']} |
| | | > |
| | | <TextField source="id" label="应用ID" /> |
| | | <TextField source="name" label="应用名称" /> |
| | | <TextField source="screct" label="应用密钥" /> |
| | | <TextField source="url" label="应用URL" /> |
| | | <FunctionField source="enable" label="启用" render={(r) => (r.enable === 1 ? '启用' : '未启用')} /> |
| | | <WrapperField cellClassName="opt" label="操作"> |
| | | <EditButton sx={{ padding: '1px', fontSize: '.75rem' }} /> |
| | | <DeleteButton sx={{ padding: '1px', fontSize: '.75rem' }} mutationMode={OPERATE_MODE} /> |
| | | </WrapperField> |
| | | </StyledDatagrid> |
| | | </List> |
| | | <OpenApiAppCreate open={createDialog} setOpen={setCreateDialog} /> |
| | | </Box> |
| | | ); |
| | | }; |
| | | |
| | | export default OpenApiAppList; |
| New file |
| | |
| | | import React from "react"; |
| | | import { ShowGuesser } from "react-admin"; |
| | | import OpenApiAppList from "./OpenApiAppList"; |
| | | import OpenApiAppEdit from "./OpenApiAppEdit"; |
| | | |
| | | export default { |
| | | list: OpenApiAppList, |
| | | edit: OpenApiAppEdit, |
| | | show: ShowGuesser, |
| | | recordRepresentation: (record) => record?.name || record?.id || '', |
| | | }; |
| | |
| | | <artifactId>rsf-common</artifactId> |
| | | <version>1.0.0</version> |
| | | </dependency> |
| | | <!-- OpenFeign:转发调用立库 WMS 接口 --> |
| | | <dependency> |
| | | <groupId>org.springframework.cloud</groupId> |
| | | <artifactId>spring-cloud-starter-openfeign</artifactId> |
| | | </dependency> |
| | | <!-- 熔断器:Feign 调用失败时触发 Fallback,在 Feign 内统一返回错误响应 --> |
| | | <dependency> |
| | | <groupId>org.springframework.cloud</groupId> |
| | | <artifactId>spring-cloud-starter-circuitbreaker-reactor-resilience4j</artifactId> |
| | | </dependency> |
| | | <!-- JWT:Token 生成与校验(/erp 认证) --> |
| | | <dependency> |
| | | <groupId>io.jsonwebtoken</groupId> |
| | | <artifactId>jjwt-api</artifactId> |
| | | <version>0.11.5</version> |
| | | </dependency> |
| | | <dependency> |
| | | <groupId>io.jsonwebtoken</groupId> |
| | | <artifactId>jjwt-impl</artifactId> |
| | | <version>0.11.5</version> |
| | | <scope>runtime</scope> |
| | | </dependency> |
| | | <dependency> |
| | | <groupId>io.jsonwebtoken</groupId> |
| | | <artifactId>jjwt-jackson</artifactId> |
| | | <version>0.11.5</version> |
| | | <scope>runtime</scope> |
| | | </dependency> |
| | | <!-- BCrypt:getToken 时校验 appSecret 用哈希比对 --> |
| | | <dependency> |
| | | <groupId>org.springframework.security</groupId> |
| | | <artifactId>spring-security-crypto</artifactId> |
| | | </dependency> |
| | | </dependencies> |
| | | <build> |
| | | <finalName>rsf-open-api</finalName> |
| | |
| | | import org.springframework.boot.autoconfigure.SpringBootApplication; |
| | | import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration; |
| | | import org.springframework.boot.autoconfigure.security.servlet.UserDetailsServiceAutoConfiguration; |
| | | import org.springframework.cloud.openfeign.EnableFeignClients; |
| | | |
| | | @SpringBootApplication(exclude = {SecurityAutoConfiguration.class, UserDetailsServiceAutoConfiguration.class }) |
| | | @EnableFeignClients(basePackages = "com.vincent.rsf.openApi.feign") |
| | | public class OpenApi { |
| | | public static void main(String[] args) { |
| | | SpringApplication.run(OpenApi.class, args); |
| New file |
| | |
| | | package com.vincent.rsf.openApi.config; |
| | | |
| | | import com.vincent.rsf.openApi.security.filter.AppIdAuthenticationFilter; |
| | | import org.springframework.boot.web.servlet.FilterRegistrationBean; |
| | | import org.springframework.context.annotation.Bean; |
| | | import org.springframework.context.annotation.Configuration; |
| | | |
| | | import javax.annotation.Resource; |
| | | |
| | | /** |
| | | * API 安全配置 认证过滤器 |
| | | */ |
| | | @Configuration |
| | | public class ApiSecurityConfig { |
| | | |
| | | @Resource |
| | | private AppIdAuthenticationFilter appIdAuthenticationFilter; |
| | | |
| | | @Bean |
| | | public FilterRegistrationBean<AppIdAuthenticationFilter> apiAuthenticationFilter() { |
| | | FilterRegistrationBean<AppIdAuthenticationFilter> registrationBean = new FilterRegistrationBean<>(); |
| | | registrationBean.setFilter(appIdAuthenticationFilter); |
| | | registrationBean.addUrlPatterns("/api/*", "/erp/*", "/cloudwms/*", "/mes/*", "/agv/*"); |
| | | registrationBean.setName("apiAuthenticationFilter"); |
| | | registrationBean.setOrder(1); |
| | | return registrationBean; |
| | | } |
| | | } |
| New file |
| | |
| | | package com.vincent.rsf.openApi.config; |
| | | |
| | | import org.springframework.context.annotation.Bean; |
| | | import org.springframework.context.annotation.Configuration; |
| | | import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; |
| | | import org.springframework.security.crypto.password.PasswordEncoder; |
| | | |
| | | @Configuration |
| | | public class CryptoConfig { |
| | | |
| | | @Bean |
| | | public PasswordEncoder passwordEncoder() { |
| | | return new BCryptPasswordEncoder(); |
| | | } |
| | | } |
| New file |
| | |
| | | package com.vincent.rsf.openApi.config; |
| | | |
| | | import org.slf4j.Logger; |
| | | import org.slf4j.LoggerFactory; |
| | | import org.springframework.boot.web.error.ErrorAttributeOptions; |
| | | import org.springframework.boot.web.servlet.error.DefaultErrorAttributes; |
| | | import org.springframework.stereotype.Component; |
| | | import org.springframework.web.context.request.WebRequest; |
| | | |
| | | import java.util.Map; |
| | | |
| | | /** |
| | | * 自定义错误属性:响应体中不返回 path(仅打印日志);保证业务异常信息写入 message 返回给前端。 |
| | | */ |
| | | @Component |
| | | public class CustomErrorAttributes extends DefaultErrorAttributes { |
| | | |
| | | private static final Logger log = LoggerFactory.getLogger(CustomErrorAttributes.class); |
| | | private static final String KEY_PATH = "path"; |
| | | private static final String KEY_MESSAGE = "message"; |
| | | |
| | | @Override |
| | | public Map<String, Object> getErrorAttributes(WebRequest webRequest, ErrorAttributeOptions options) { |
| | | Map<String, Object> attrs = super.getErrorAttributes(webRequest, options); |
| | | Object path = attrs.remove(KEY_PATH); |
| | | if (path != null) { |
| | | log.warn("Error path: {}", path); |
| | | } |
| | | // 保证业务异常信息返回给前端(如 CoolException) |
| | | Throwable error = getError(webRequest); |
| | | if (error != null && error.getMessage() != null && !error.getMessage().isEmpty()) { |
| | | attrs.put(KEY_MESSAGE, error.getMessage()); |
| | | } |
| | | return attrs; |
| | | } |
| | | } |
| New file |
| | |
| | | package com.vincent.rsf.openApi.config; |
| | | |
| | | import feign.RequestInterceptor; |
| | | import feign.RequestTemplate; |
| | | import org.springframework.context.annotation.Bean; |
| | | import org.springframework.context.annotation.Configuration; |
| | | |
| | | /** |
| | | * Feign 请求配置,与原有 RestTemplate 转发保持一致的请求头 |
| | | */ |
| | | @Configuration |
| | | public class FeignConfig { |
| | | |
| | | @Bean |
| | | public RequestInterceptor wmsApiVersionInterceptor() { |
| | | return (RequestTemplate template) -> template.header("api-version", "v2.0"); |
| | | } |
| | | } |
| New file |
| | |
| | | package com.vincent.rsf.openApi.config; |
| | | |
| | | import com.vincent.rsf.framework.exception.CoolException; |
| | | import com.vincent.rsf.openApi.entity.dto.CommonResponse; |
| | | import com.vincent.rsf.openApi.entity.dto.ResultData; |
| | | import org.slf4j.Logger; |
| | | import org.slf4j.LoggerFactory; |
| | | import org.springframework.http.HttpStatus; |
| | | import org.springframework.http.ResponseEntity; |
| | | import org.springframework.web.bind.annotation.ExceptionHandler; |
| | | import org.springframework.web.bind.annotation.RestControllerAdvice; |
| | | |
| | | /** |
| | | * 全局异常处理,返回值符合 8.2.3:code、msg、data(含 result:SUCCESS/FAIL)。 |
| | | */ |
| | | @RestControllerAdvice |
| | | public class GlobalExceptionHandler { |
| | | |
| | | private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class); |
| | | |
| | | @ExceptionHandler(CoolException.class) |
| | | public ResponseEntity<CommonResponse> handleCoolException(CoolException e) { |
| | | log.warn("业务异常: {}", e.getMessage()); |
| | | CommonResponse r = CommonResponse.error(e.getMessage()); |
| | | return ResponseEntity.status(HttpStatus.OK).body(r); |
| | | } |
| | | |
| | | @ExceptionHandler(Exception.class) |
| | | public ResponseEntity<CommonResponse> handleException(Exception e) { |
| | | log.error("系统异常", e); |
| | | String msg = e.getMessage() != null ? e.getMessage() : "系统异常"; |
| | | CommonResponse r = new CommonResponse(); |
| | | r.setCode(500); |
| | | r.setMsg(msg); |
| | | r.setData(ResultData.fail()); |
| | | return ResponseEntity.status(HttpStatus.OK).body(r); |
| | | } |
| | | } |
| | |
| | | public void addInterceptors(InterceptorRegistry registry) { |
| | | registry.addInterceptor(getAsyncHandlerInterceptor()) |
| | | .addPathPatterns("/**") |
| | | .excludePathPatterns("/swagger-resources/**", "/webjars/**","/erp/**", "/v2/**","/v3/**","/doc.html/**", "/swagger-ui.html/**"); |
| | | .excludePathPatterns("/swagger-resources/**", "/webjars/**", "/cloudwms/**", "/erp/**", "/v2/**", "/v3/**", "/doc.html/**", "/swagger-ui.html/**"); |
| | | } |
| | | |
| | | |
| New file |
| | |
| | | package com.vincent.rsf.openApi.controller; |
| | | |
| | | import com.vincent.rsf.framework.common.Cools; |
| | | import com.vincent.rsf.openApi.entity.constant.Constants; |
| | | import com.vincent.rsf.openApi.entity.dto.CommonResponse; |
| | | import com.vincent.rsf.openApi.entity.params.GetTokenParam; |
| | | import com.vincent.rsf.openApi.security.service.AppAuthService; |
| | | import com.vincent.rsf.openApi.security.utils.TokenUtils; |
| | | import io.swagger.annotations.Api; |
| | | import io.swagger.annotations.ApiOperation; |
| | | import org.springframework.beans.factory.annotation.Autowired; |
| | | import org.springframework.web.bind.annotation.PostMapping; |
| | | import org.springframework.web.bind.annotation.RequestBody; |
| | | import org.springframework.web.bind.annotation.RestController; |
| | | |
| | | @RestController |
| | | @Api(tags = "应用认证管理") |
| | | public class AuthController { |
| | | |
| | | @Autowired |
| | | private AppAuthService appAuthService; |
| | | |
| | | @ApiOperation("获取App认证Token") |
| | | @PostMapping("/getToken") |
| | | public CommonResponse getToken(@RequestBody GetTokenParam param) { |
| | | String appId = param != null ? param.getAppId() : null; |
| | | String appSecret = param != null ? param.getAppSecret() : null; |
| | | if (Cools.isEmpty(appId, appSecret)) { |
| | | return CommonResponse.error("AppId和AppSecret不能为空"); |
| | | } |
| | | if (!appAuthService.validateApp(appId, appSecret)) { |
| | | return CommonResponse.error("AppId或AppSecret无效"); |
| | | } |
| | | String token = Constants.TOKEN_PREFIX + TokenUtils.generateToken(appId, appSecret); |
| | | return CommonResponse.ok().setMsg("获取Token成功").setData(token); |
| | | } |
| | | } |
| | |
| | | import java.util.Objects; |
| | | |
| | | @RestController |
| | | @RequestMapping("/erp") |
| | | @RequestMapping({"/erp","/cloudwms"}) |
| | | @Api("ERP接口对接") |
| | | public class WmsErpController { |
| | | |
| | |
| | | |
| | | |
| | | /** |
| | | * 订单修改 |
| | | * @param params |
| | | * @return |
| | | * 入/出库通知单下发(对接协议 8.3):新增/修改/取消。operateType 1新增 2修改 3取消,不传或 1/2 时有则更新、无则新增;3 时按取消。 |
| | | * @param params 单据参数(含 orderNo、orderItems、operateType 等,见对接文档) |
| | | * @return 操作结果 |
| | | */ |
| | | @ApiOperation("单据修改") |
| | | @PostMapping("/order/upadte") |
| | | public CommonResponse modifyOrderDtel(@RequestBody ErpOpParams params) { |
| | | if (Objects.isNull(params)) { |
| | | throw new CoolException("参数不能为空!!"); |
| | | } |
| | | return wmsErpService.updateOrderDetl(params); |
| | | } |
| | | |
| | | |
| | | /** |
| | | * 订单新增 |
| | | * @param params |
| | | * @return |
| | | */ |
| | | @ApiOperation("新增单据") |
| | | @ApiOperation("新增单据(兼容修改、取消)") |
| | | @PostMapping("/order/add") |
| | | public CommonResponse orderAdd(@RequestBody ErpOpParams params) { |
| | | if (Objects.isNull(params)) { |
| | | throw new CoolException("参数不能为空!!"); |
| | | } |
| | | return wmsErpService.updateOrderDetl(params); |
| | | return wmsErpService.addOrUpdateOrder(params); |
| | | } |
| | | |
| | | |
| | | /** |
| | | * 删除订单 |
| | | * @param params |
| | | * @return |
| | | * 取消订单/取消单据。与 /order/add 传 operateType=3 的取消逻辑一致,均转发立库 sync/orders/delete。 |
| | | * @param params 至少包含 orderNo,可选 orderItems |
| | | */ |
| | | @ApiOperation("删除订单") |
| | | @PostMapping("/order/del") |
| | | public CommonResponse orderDel(@RequestBody ErpOpParams params) { |
| | | @ApiOperation("取消订单") |
| | | @PostMapping({"/order/cancel", "/order/del"}) |
| | | public CommonResponse orderCancel(@RequestBody ErpOpParams params) { |
| | | if (Objects.isNull(params)) { |
| | | throw new CoolException("参数不能为空!!"); |
| | | } |
| | | return wmsErpService.orderDel(params); |
| | | return wmsErpService.orderCancel(params); |
| | | } |
| | | |
| | | @ApiOperation("基础物料信息更新") |
| | | /** |
| | | * 物料基础信息同步(对接协议 8.2) |
| | | * 支持 operateType:1新增 2修改 3禁用 4启用;请求体支持协议字段 matNr/makTx 与 matnr/maktx。 |
| | | */ |
| | | @ApiOperation("基础物料信息同步(支持 operateType、matNr/makTx)") |
| | | @PostMapping("/mat/sync/auth/v1") |
| | | public CommonResponse syncMatnrs(@RequestBody ErpMatnrParms parms) { |
| | | if (Objects.isNull(parms)) { |
| | |
| | | } |
| | | |
| | | |
| | | @ApiOperation("订单信息上报") |
| | | @PostMapping("/report/order") |
| | | public CommonResponse reportOrders(@RequestBody ReportParams params) { |
| | | if (Objects.isNull(params)) { |
| | | throw new CoolException("参数不能为空!!"); |
| | | } |
| | | return wmsErpService.reportOrders(params); |
| | | // @ApiOperation("订单信息上报") |
| | | // @PostMapping("/report/order") |
| | | // public CommonResponse reportOrders(@RequestBody ReportParams params) { |
| | | // if (Objects.isNull(params)) { |
| | | // throw new CoolException("参数不能为空!!"); |
| | | // } |
| | | // return wmsErpService.reportOrders(params); |
| | | // } |
| | | // |
| | | // @ApiOperation("盘点差异修改") |
| | | // @PostMapping("/check/locitem/update") |
| | | // public CommonResponse reportCheck(@RequestBody ReportParams params) { |
| | | // return wmsErpService.reportCheck(params); |
| | | // } |
| | | |
| | | @ApiOperation("库位信息查询") |
| | | @PostMapping("/query/locs/detls") |
| | | public CommonResponse queryLocsDetls(@RequestBody Map<String, Object> params) { |
| | | return wmsErpService.queryLocsDetls(params); |
| | | } |
| | | |
| | | @ApiOperation("盘点差异修改") |
| | | @PostMapping("/check/locitem/update") |
| | | public CommonResponse reportCheck(@RequestBody ReportParams params) { |
| | | return wmsErpService.reportCheck(params); |
| | | @ApiOperation("库存明细查询(对接协议8.4)") |
| | | @PostMapping("/inventory/details") |
| | | public CommonResponse inventoryDetails(@RequestBody(required = false) Map<String, Object> params) { |
| | | return wmsErpService.inventoryDetails(params); |
| | | } |
| | | |
| | | @ApiOperation("库存汇总查询(对接协议8.5)") |
| | | @PostMapping("/inventory/summary") |
| | | public CommonResponse inventorySummary(@RequestBody(required = false) Map<String, Object> params) { |
| | | return wmsErpService.inventorySummary(params); |
| | | } |
| | | |
| | | } |
| New file |
| | |
| | | package com.vincent.rsf.openApi.controller.platform; |
| | | |
| | | import com.baomidou.mybatisplus.extension.plugins.pagination.Page; |
| | | import com.vincent.rsf.openApi.entity.app.App; |
| | | import com.vincent.rsf.openApi.entity.dto.CommonResponse; |
| | | import com.vincent.rsf.openApi.service.AppService; |
| | | import io.swagger.annotations.Api; |
| | | import io.swagger.annotations.ApiOperation; |
| | | import org.springframework.beans.factory.annotation.Autowired; |
| | | import org.springframework.web.bind.annotation.*; |
| | | |
| | | import java.util.List; |
| | | |
| | | @RestController |
| | | @RequestMapping("/api/app") |
| | | @Api(tags = "应用管理") |
| | | public class AppController { |
| | | |
| | | @Autowired |
| | | private AppService appService; |
| | | |
| | | @ApiOperation("分页查询应用列表") |
| | | @GetMapping("/page") |
| | | public CommonResponse page(@RequestParam(defaultValue = "1") Integer current, |
| | | @RequestParam(defaultValue = "10") Integer size) { |
| | | Page<App> page = appService.page(new Page<>(current, size)); |
| | | return CommonResponse.ok().setData(page); |
| | | } |
| | | |
| | | @ApiOperation("查询所有应用") |
| | | @GetMapping("/list") |
| | | public CommonResponse list() { |
| | | List<App> list = appService.list(); |
| | | return CommonResponse.ok().setData(list); |
| | | } |
| | | |
| | | @ApiOperation("根据ID查询应用") |
| | | @GetMapping("/{id}") |
| | | public CommonResponse getById(@PathVariable String id) { |
| | | App app = appService.getById(id); |
| | | return CommonResponse.ok().setData(app); |
| | | } |
| | | |
| | | @ApiOperation("新增应用") |
| | | @PostMapping |
| | | public CommonResponse save(@RequestBody App app) { |
| | | if (appService.save(app)) { |
| | | return CommonResponse.ok().setMsg("新增成功"); |
| | | } |
| | | return CommonResponse.error("新增失败"); |
| | | } |
| | | |
| | | @ApiOperation("更新应用") |
| | | @PutMapping |
| | | public CommonResponse update(@RequestBody App app) { |
| | | if (appService.updateById(app)) { |
| | | return CommonResponse.ok().setMsg("更新成功"); |
| | | } |
| | | return CommonResponse.error("更新失败"); |
| | | } |
| | | |
| | | @ApiOperation("删除应用") |
| | | @DeleteMapping("/{id}") |
| | | public CommonResponse delete(@PathVariable String id) { |
| | | if (appService.removeById(id)) { |
| | | return CommonResponse.ok().setMsg("删除成功"); |
| | | } |
| | | return CommonResponse.error("删除失败"); |
| | | } |
| | | |
| | | @ApiOperation("批量删除应用") |
| | | @DeleteMapping("/batch") |
| | | public CommonResponse deleteBatch(@RequestBody List<String> ids) { |
| | | if (appService.removeByIds(ids)) { |
| | | return CommonResponse.ok().setMsg("批量删除成功"); |
| | | } |
| | | return CommonResponse.error("批量删除失败"); |
| | | } |
| | | } |
| New file |
| | |
| | | package com.vincent.rsf.openApi.entity.app; |
| | | |
| | | import com.baomidou.mybatisplus.annotation.TableId; |
| | | import com.baomidou.mybatisplus.annotation.TableName; |
| | | import io.swagger.annotations.ApiModelProperty; |
| | | import lombok.Data; |
| | | |
| | | import java.io.Serializable; |
| | | |
| | | @Data |
| | | @TableName("open_api_app") |
| | | public class App implements Serializable { |
| | | |
| | | private static final long serialVersionUID = 1L; |
| | | |
| | | @ApiModelProperty("appId") |
| | | @TableId(value = "id") |
| | | private String id; |
| | | |
| | | @ApiModelProperty("appSecret") |
| | | private String screct; |
| | | |
| | | @ApiModelProperty("appName") |
| | | private String name; |
| | | |
| | | @ApiModelProperty("appUrl") |
| | | private String url; |
| | | |
| | | @ApiModelProperty("是否启用 0未启用 1启用") |
| | | private Integer enable; |
| | | |
| | | @ApiModelProperty("租户id") |
| | | private Long tenantId; |
| | | } |
| | |
| | | public static final String TOKEN_TYPE = "Bearer"; |
| | | |
| | | /** |
| | | * Authorization 头中的 Token 前缀 |
| | | */ |
| | | public static final String TOKEN_PREFIX = "Bearer "; |
| | | |
| | | /** |
| | | * HTTP 头 Authorization 的 key(小写,请求头不区分大小写) |
| | | */ |
| | | public static final String HEADER_AUTHORIZATION = "Authorization"; |
| | | |
| | | /** |
| | | * 请求属性:认证通过后写入的 appId |
| | | */ |
| | | public static final String REQUEST_ATTR_APP_ID = "appId"; |
| | | |
| | | /** |
| | | * 库存出库 |
| | | */ |
| | | public static final String TASK_TYPE_OUT_STOCK = "outStock"; |
| | |
| | | //订单信息修改/添加 |
| | | public static String MODIFY_ORDER_DETLS = "/rsf-server/order/sync/orders/update"; |
| | | |
| | | //删除单据信息 |
| | | // 取消单据(立库侧 /order/sync/orders/delete 实现取消逻辑) |
| | | public static String ORDER_DEL = "/rsf-server/order/sync/orders/delete"; |
| | | |
| | | //获取出入库流水 |
| | |
| | | //订单完成回写 |
| | | public static String REPORT_ORDER_CALLBACK = "/C3Api?SysCode=WMS"; |
| | | |
| | | //库位信息查询(云仓只调 open-api,由 open-api 转发立库) |
| | | public static String QUERY_LOCS_DETLS = "/rsf-server/erp/query/locs/detls"; |
| | | |
| | | //调拨单信息查询 |
| | | public static String QUERY_TRANSFER = "/rsf-server/erp/query/transfer"; |
| | | |
| | | //物料分类列表查询 |
| | | public static String QUERY_MATNR_GROUP = "/rsf-server/erp/query/matnr/group"; |
| | | |
| | | // ========== 单据同步(立库提供,云仓调 open-api 转发) ========== |
| | | /** 采购单同步 */ |
| | | public static String ORDER_SYNC_PURCHASE = "/rsf-server/order/sync/purchase"; |
| | | /** 出库通知单(DO单)同步 */ |
| | | public static String ORDER_SYNC_DELIVERY = "/rsf-server/order/sync/delivery"; |
| | | /** 收货通知单/单据同步 */ |
| | | public static String ORDER_SYNC_CHECKS = "/rsf-server/order/sync/checks"; |
| | | /** 调拨单同步 */ |
| | | public static String ORDER_SYNC_TRANSFERS = "/rsf-server/order/sync/transfers"; |
| | | /** 库存调整单同步 */ |
| | | public static String ORDER_SYNC_REVISES = "/rsf-server/order/sync/revises"; |
| | | /** 质检单上报 */ |
| | | public static String ORDER_SYNC_QLY_INSPECT = "/rsf-server/order/sync/qlyInspect"; |
| | | /** 盘点差异单同步 */ |
| | | public static String ORDER_SYNC_CHECK_RESULT = "/rsf-server/order/sync/check/result"; |
| | | |
| | | // ========== 基础数据同步(立库提供) ========== |
| | | /** 基础物料信息同步(批量) */ |
| | | public static String BASE_SYNC_MATNRS = "/rsf-server/base/sync/base/matnrs"; |
| | | /** 库位信息同步 */ |
| | | public static String BASE_SYNC_LOCS = "/rsf-server/base/sync/locs"; |
| | | /** 物料分组信息同步 */ |
| | | public static String BASE_SYNC_MAT_GROUPS = "/rsf-server/base/sync/matGroups"; |
| | | /** 库区数据同步 */ |
| | | public static String BASE_SYNC_WAREHOUSE_AREAS = "/rsf-server/base/sync/warehouse/areas"; |
| | | /** 仓库数据同步 */ |
| | | public static String BASE_SYNC_WAREHOUSE = "/rsf-server/base/sync/warehouse"; |
| | | /** 企业信息同步 */ |
| | | public static String BASE_SYNC_COMPANIES = "/rsf-server/base/sync/companies"; |
| | | |
| | | /** 对接协议 8.4 库存明细查询 */ |
| | | public static String INVENTORY_DETAILS = "/rsf-server/erp/inventory/details"; |
| | | /** 对接协议 8.5 库存汇总查询 */ |
| | | public static String INVENTORY_SUMMARY = "/rsf-server/erp/inventory/summary"; |
| | | |
| | | } |
| | |
| | | @ApiModelProperty("响应结果") |
| | | private Object data; |
| | | |
| | | public static CommonResponse ok() { |
| | | CommonResponse r = new CommonResponse(); |
| | | r.setCode(200); |
| | | r.setMsg("操作成功"); |
| | | return r; |
| | | } |
| | | |
| | | /** 8.2.3 格式:成功且 data 仅含 result */ |
| | | public static CommonResponse okWithResult() { |
| | | return ok().setData(ResultData.success()); |
| | | } |
| | | |
| | | public static CommonResponse error(String msg) { |
| | | CommonResponse r = new CommonResponse(); |
| | | r.setCode(500); |
| | | r.setMsg(msg != null ? msg : "操作失败"); |
| | | r.setData(ResultData.fail()); |
| | | return r; |
| | | } |
| | | |
| | | } |
| New file |
| | |
| | | package com.vincent.rsf.openApi.entity.dto; |
| | | |
| | | import io.swagger.annotations.ApiModel; |
| | | import io.swagger.annotations.ApiModelProperty; |
| | | import lombok.AllArgsConstructor; |
| | | import lombok.Data; |
| | | import lombok.NoArgsConstructor; |
| | | |
| | | /** |
| | | * 8.2.3 返回值 data 数据模型:执行结果 SUCCESS/FAIL |
| | | */ |
| | | @Data |
| | | @NoArgsConstructor |
| | | @AllArgsConstructor |
| | | @ApiModel(value = "ResultData", description = "执行结果") |
| | | public class ResultData { |
| | | |
| | | @ApiModelProperty(value = "执行结果:SUCCESS 成功;FAIL 失败", example = "SUCCESS") |
| | | private String result; |
| | | |
| | | public static final String SUCCESS = "SUCCESS"; |
| | | public static final String FAIL = "FAIL"; |
| | | |
| | | public static ResultData success() { |
| | | return new ResultData(SUCCESS); |
| | | } |
| | | |
| | | public static ResultData fail() { |
| | | return new ResultData(FAIL); |
| | | } |
| | | } |
| | |
| | | package com.vincent.rsf.openApi.entity.params; |
| | | |
| | | |
| | | import com.fasterxml.jackson.annotation.JsonAlias; |
| | | import io.swagger.annotations.ApiModelProperty; |
| | | import lombok.Data; |
| | | import lombok.experimental.Accessors; |
| | | |
| | | /** |
| | | * 物料基础信息同步入参(对接协议 8.2) |
| | | * 协议字段 matNr/makTx 与 matnr/maktx 均可接收。 |
| | | */ |
| | | @Data |
| | | @Accessors(chain = true) |
| | | public class ErpMatnrParms { |
| | | |
| | | @ApiModelProperty("物料名称") |
| | | private String maktx; |
| | | @ApiModelProperty(value = "操作类型:1新增 2修改 3禁用 4启用", example = "1") |
| | | private Integer operateType; |
| | | |
| | | @ApiModelProperty("物料编码*") |
| | | @ApiModelProperty(value = "物料编码*(协议字段 matNr 同义)") |
| | | @JsonAlias("matNr") |
| | | private String matnr; |
| | | |
| | | @ApiModelProperty(value = "物料名称(协议字段 makTx 同义)") |
| | | @JsonAlias("makTx") |
| | | private String maktx; |
| | | |
| | | @ApiModelProperty("物料分组") |
| | | private String groupName; |
| | | |
| | |
| | | package com.vincent.rsf.openApi.entity.params; |
| | | |
| | | |
| | | import com.fasterxml.jackson.annotation.JsonAlias; |
| | | import io.swagger.annotations.ApiModel; |
| | | import io.swagger.annotations.ApiModelProperty; |
| | | import lombok.Data; |
| | |
| | | |
| | | import java.util.List; |
| | | |
| | | /** |
| | | * 入/出库通知单下发(对接协议 8.3)请求参数。 |
| | | * 以 8.3 文档字段为主,其他旧字段尽量不用。 |
| | | */ |
| | | @Data |
| | | @Accessors(chain = true) |
| | | @ApiModel(value = "ErpOpParams", description = "ERP操作请求参数") |
| | | @ApiModel(value = "ErpOpParams", description = "8.3 入/出库通知单下发参数") |
| | | public class ErpOpParams { |
| | | |
| | | /** |
| | | * 单号 |
| | | */ |
| | | @ApiModelProperty("订单号") |
| | | @ApiModelProperty(value = "订单编码", required = true) |
| | | private String orderNo; |
| | | |
| | | @ApiModelProperty("业务类型") |
| | | @ApiModelProperty(value = "单据内码,唯一标识,若没有可补充订单编码", required = true) |
| | | private String orderInternalCode; |
| | | |
| | | @ApiModelProperty(value = "订单类型:1 出库单;2 入库单;3 调拨单", required = true) |
| | | private Integer orderType; |
| | | |
| | | @ApiModelProperty(value = "业务类型,如:采购入库单、销售出库单、调拨申请单等", required = true) |
| | | private String wkType; |
| | | |
| | | @ApiModelProperty("订单类型") |
| | | private String type; |
| | | @ApiModelProperty(value = "业务日期,时间戳精确到秒", required = true) |
| | | private Long businessTime; |
| | | |
| | | @ApiModelProperty("数量") |
| | | private Double anfme; |
| | | @ApiModelProperty(value = "创建日期,时间戳精确到秒", required = true) |
| | | private Long createTime; |
| | | |
| | | @ApiModelProperty("执行状态") |
| | | private Short exceStatus; |
| | | |
| | | @ApiModelProperty("订单明细") |
| | | @ApiModelProperty(value = "订单明细", required = true) |
| | | private List<WmsOrderItemParam> orderItems; |
| | | |
| | | @ApiModelProperty("入/出库接驳站点,需要则补充") |
| | | private String stationId; |
| | | |
| | | @ApiModelProperty("操作类型:1 新增(默认);2 修改;3 取消") |
| | | private Integer operateType; |
| | | |
| | | /** 兼容旧字段:与 orderInternalCode 二选一 */ |
| | | @JsonAlias("orderId") |
| | | @ApiModelProperty(hidden = true) |
| | | private Long orderId; |
| | | } |
| New file |
| | |
| | | package com.vincent.rsf.openApi.entity.params; |
| | | |
| | | import io.swagger.annotations.ApiModel; |
| | | import io.swagger.annotations.ApiModelProperty; |
| | | import lombok.Data; |
| | | import lombok.experimental.Accessors; |
| | | |
| | | /** |
| | | * 对接协议 8.1 获取Token 请求参数 |
| | | */ |
| | | @Data |
| | | @Accessors(chain = true) |
| | | @ApiModel(value = "GetTokenParam", description = "获取Token") |
| | | public class GetTokenParam { |
| | | |
| | | @ApiModelProperty(value = "应用编码,立库WMS线下分配", required = true) |
| | | private String appId; |
| | | |
| | | @ApiModelProperty(value = "应用秘钥,立库WMS线下分配", required = true) |
| | | private String appSecret; |
| | | } |
| | |
| | | package com.vincent.rsf.openApi.entity.params; |
| | | |
| | | |
| | | import com.fasterxml.jackson.annotation.JsonAlias; |
| | | import io.swagger.annotations.ApiModel; |
| | | import io.swagger.annotations.ApiModelProperty; |
| | | import lombok.Data; |
| | | import lombok.experimental.Accessors; |
| | | |
| | | /** |
| | | * 8.3 订单明细,以文档字段为主。 |
| | | */ |
| | | @Data |
| | | @Accessors(chain = true) |
| | | @ApiModel(value = "WmsOrderItemParam", description = "订单明细参数") |
| | | @ApiModel(value = "WmsOrderItemParam", description = "8.3 订单明细") |
| | | public class WmsOrderItemParam { |
| | | |
| | | @ApiModelProperty("物料编码") |
| | | private String matnr; |
| | | @ApiModelProperty(value = "行内码,唯一标识", required = true) |
| | | private String lineId; |
| | | |
| | | @ApiModelProperty(value = "物料编码", required = true) |
| | | @JsonAlias("matnr") |
| | | private String matNr; |
| | | |
| | | @ApiModelProperty("物料名称") |
| | | private String maktx; |
| | | @JsonAlias("maktx") |
| | | private String makTx; |
| | | |
| | | @ApiModelProperty("客单号") |
| | | private String platOrderCode; |
| | | |
| | | @ApiModelProperty("平台标识(行号)") |
| | | private String platItemId; |
| | | |
| | | @ApiModelProperty("工单号") |
| | | private String platWorkCode; |
| | | |
| | | @ApiModelProperty("项目号") |
| | | private String projectCode; |
| | | |
| | | @ApiModelProperty("现金票号") |
| | | private String crushNo; |
| | | @ApiModelProperty(value = "数量,若有小数默认保留2位", required = true) |
| | | @JsonAlias("qty") |
| | | private String anfme; |
| | | |
| | | @ApiModelProperty("规格") |
| | | private String spec; |
| | |
| | | @ApiModelProperty("型号") |
| | | private String model; |
| | | |
| | | @ApiModelProperty("数量") |
| | | private Double anfme; |
| | | |
| | | @ApiModelProperty("库存单位") |
| | | @ApiModelProperty("单位") |
| | | private String unit; |
| | | |
| | | @ApiModelProperty("批次") |
| | | @ApiModelProperty("批次号") |
| | | private String batch; |
| | | |
| | | @ApiModelProperty("已收数量") |
| | | private Double qty; |
| | | @ApiModelProperty("托盘码,出库单时可指定该托盘出库") |
| | | private String palletId; |
| | | |
| | | @ApiModelProperty("条形码") |
| | | private String barcode; |
| | | @ApiModelProperty("计划跟踪号") |
| | | private String planNo; |
| | | |
| | | @ApiModelProperty("建议入库仓库") |
| | | private String targetWareHouseId; |
| | | |
| | | @ApiModelProperty("建议出库仓库") |
| | | private String sourceWareHouseId; |
| | | } |
| New file |
| | |
| | | package com.vincent.rsf.openApi.feign.erp; |
| | | |
| | | import com.vincent.rsf.openApi.entity.params.ReportParams; |
| | | import org.springframework.cloud.openfeign.FeignClient; |
| | | import org.springframework.http.MediaType; |
| | | import org.springframework.web.bind.annotation.PostMapping; |
| | | import org.springframework.web.bind.annotation.RequestBody; |
| | | |
| | | import java.util.Map; |
| | | |
| | | /** |
| | | * 调用 ERP/云仓 上报接口的 OpenFeign 客户端(订单完成回写、盘点差异单修改)。 |
| | | * 可选方式,当前仍使用 HttpEntity(RestTemplate);启用时在 WmsErpServiceImpl 中切换。 |
| | | */ |
| | | @FeignClient( |
| | | name = "erp-report", |
| | | url = "${platform.erp.host:http://127.0.0.1}:${platform.erp.port:8080}" |
| | | ) |
| | | public interface ErpReportFeignClient { |
| | | |
| | | /** 订单完成回写 / 盘点差异单修改:同一 ERP 回调地址 */ |
| | | @PostMapping(value = "/C3Api?SysCode=WMS", consumes = MediaType.APPLICATION_JSON_VALUE) |
| | | Map<String, Object> report(@RequestBody ReportParams params); |
| | | } |
| New file |
| | |
| | | package com.vincent.rsf.openApi.feign.wms; |
| | | |
| | | import com.vincent.rsf.openApi.entity.params.ErpMatnrParms; |
| | | import com.vincent.rsf.openApi.entity.params.ErpOpParams; |
| | | import com.vincent.rsf.openApi.feign.wms.fallback.WmsServerFeignClientFallbackFactory; |
| | | import org.springframework.cloud.openfeign.FeignClient; |
| | | import org.springframework.web.bind.annotation.PostMapping; |
| | | import org.springframework.web.bind.annotation.RequestBody; |
| | | |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | |
| | | /** |
| | | * WMS Server(立库)Feign 客户端 |
| | | * 用于 open-api 转发调用 rsf-server 的接口,云仓只调 open-api,由本 Feign 转发至立库。 |
| | | * |
| | | * url 从 application 中读取 platform.wms.host 与 platform.wms.port; |
| | | * 同机部署时可配为本地地址,分开部署时配 server 实际地址。 |
| | | */ |
| | | @FeignClient( |
| | | name = "wms-server", |
| | | url = "${platform.wms.host:http://127.0.0.1}:${platform.wms.port:8086}", |
| | | path = "", |
| | | fallbackFactory = WmsServerFeignClientFallbackFactory.class |
| | | ) |
| | | public interface WmsServerFeignClient { |
| | | |
| | | /** 订单信息及明细查询 */ |
| | | @PostMapping("/rsf-server/erp/query/order") |
| | | Map<String, Object> queryOrderAndDetls(@RequestBody ErpOpParams params); |
| | | |
| | | /** 订单信息修改/添加 */ |
| | | @PostMapping("/rsf-server/order/sync/orders/update") |
| | | Map<String, Object> updateOrderDetls(@RequestBody List<Map<String, Object>> body); |
| | | |
| | | /** 删除/取消单据(服务端接收 List<SyncOrderParams>) */ |
| | | @PostMapping("/rsf-server/order/sync/orders/delete") |
| | | Map<String, Object> orderDel(@RequestBody List<Map<String, Object>> body); |
| | | |
| | | /** 物料信息同步 */ |
| | | @PostMapping("/rsf-server/base/mat/sync/auth/v1") |
| | | Map<String, Object> syncMatnrs(@RequestBody ErpMatnrParms params); |
| | | |
| | | /** 库位信息查询 */ |
| | | @PostMapping("/rsf-server/erp/query/locs/detls") |
| | | Map<String, Object> queryLocsDetls(@RequestBody Map<String, Object> params); |
| | | |
| | | /** 库存明细查询(对接协议 8.4) */ |
| | | @PostMapping("/rsf-server/erp/inventory/details") |
| | | Map<String, Object> inventoryDetails(@RequestBody Map<String, Object> params); |
| | | |
| | | /** 库存汇总查询(对接协议 8.5) */ |
| | | @PostMapping("/rsf-server/erp/inventory/summary") |
| | | Map<String, Object> inventorySummary(@RequestBody Map<String, Object> params); |
| | | } |
| New file |
| | |
| | | package com.vincent.rsf.openApi.feign.wms.fallback; |
| | | |
| | | import com.vincent.rsf.framework.common.R; |
| | | import com.vincent.rsf.openApi.entity.params.ErpMatnrParms; |
| | | import com.vincent.rsf.openApi.entity.params.ErpOpParams; |
| | | import com.vincent.rsf.openApi.feign.wms.WmsServerFeignClient; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import org.springframework.stereotype.Component; |
| | | |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | |
| | | /** |
| | | * WMS Server Feign 客户端降级处理,在 Feign 内统一返回错误响应(不抛异常)。 |
| | | * 由 WmsServerFeignClientFallbackFactory 创建并传入异常 cause。 |
| | | */ |
| | | @Slf4j |
| | | @Component |
| | | public class WmsServerFeignClientFallback implements WmsServerFeignClient { |
| | | |
| | | /** 触发降级时的异常,由 FallbackFactory 传入;无 cause 时为 null */ |
| | | private final Throwable cause; |
| | | |
| | | public WmsServerFeignClientFallback() { |
| | | this.cause = null; |
| | | } |
| | | |
| | | public WmsServerFeignClientFallback(Throwable cause) { |
| | | this.cause = cause; |
| | | } |
| | | |
| | | private Map<String, Object> errorResponse() { |
| | | return R.error(filterErrorMessage(cause)); |
| | | } |
| | | |
| | | /** |
| | | * 过滤错误消息中的URL,只保留错误类型 |
| | | * @param throwable 异常对象(可选) |
| | | * @return 过滤后的完整错误消息(包含"查询失败:"前缀) |
| | | */ |
| | | public static String filterErrorMessage(Throwable throwable) { |
| | | if (throwable == null) { |
| | | return "查询失败:服务调用失败,请稍后重试"; |
| | | } |
| | | return filterErrorMessage(throwable.getMessage()); |
| | | } |
| | | |
| | | /** |
| | | * 过滤错误消息中的URL,只保留错误类型 |
| | | * @param errorMessage 错误消息字符串(可选) |
| | | * @return 过滤后的完整错误消息(包含"查询失败:"前缀) |
| | | */ |
| | | public static String filterErrorMessage(String errorMessage) { |
| | | if (errorMessage == null || errorMessage.isEmpty()) { |
| | | return "查询失败:未知错误"; |
| | | } |
| | | |
| | | String filteredMessage = errorMessage; |
| | | |
| | | // 如果包含"executing",说明是HTTP请求错误,去掉URL部分 |
| | | if (filteredMessage.contains("executing")) { |
| | | int executingIndex = filteredMessage.indexOf("executing"); |
| | | if (executingIndex > 0) { |
| | | // 提取"executing"之前的部分(如"Read timed out") |
| | | filteredMessage = filteredMessage.substring(0, executingIndex).trim(); |
| | | } else { |
| | | // 如果"executing"在开头,使用默认错误消息 |
| | | filteredMessage = "请求超时"; |
| | | } |
| | | } |
| | | // 如果包含"http://"或"https://",也尝试去掉URL部分 |
| | | else if (filteredMessage.contains("http://") || filteredMessage.contains("https://")) { |
| | | // 使用正则表达式去掉URL |
| | | filteredMessage = filteredMessage.replaceAll("https?://[^\\s]+", "").trim(); |
| | | if (filteredMessage.isEmpty()) { |
| | | filteredMessage = "请求失败"; |
| | | } |
| | | } |
| | | |
| | | // 如果过滤后的消息为空,使用默认错误消息 |
| | | if (filteredMessage.isEmpty()) { |
| | | filteredMessage = "未知错误"; |
| | | } |
| | | |
| | | // 返回包含"查询失败:"前缀的完整错误消息 |
| | | return "查询失败:" + filteredMessage; |
| | | } |
| | | |
| | | @Override |
| | | public Map<String, Object> queryOrderAndDetls(ErpOpParams params) { |
| | | log.error("调用立库WMS Server订单信息查询接口失败,触发降级", cause); |
| | | return errorResponse(); |
| | | } |
| | | |
| | | @Override |
| | | public Map<String, Object> updateOrderDetls(List<Map<String, Object>> body) { |
| | | log.error("调用立库WMS Server订单修改接口失败,触发降级", cause); |
| | | return errorResponse(); |
| | | } |
| | | |
| | | @Override |
| | | public Map<String, Object> orderDel(List<Map<String, Object>> body) { |
| | | log.error("调用立库WMS Server取消单据接口失败,触发降级", cause); |
| | | return errorResponse(); |
| | | } |
| | | |
| | | @Override |
| | | public Map<String, Object> syncMatnrs(ErpMatnrParms params) { |
| | | log.error("调用立库WMS Server物料信息同步接口失败,触发降级", cause); |
| | | return errorResponse(); |
| | | } |
| | | |
| | | @Override |
| | | public Map<String, Object> queryLocsDetls(Map<String, Object> params) { |
| | | log.error("调用立库WMS Server库位信息查询接口失败,触发降级", cause); |
| | | return errorResponse(); |
| | | } |
| | | |
| | | @Override |
| | | public Map<String, Object> inventoryDetails(Map<String, Object> params) { |
| | | log.error("调用立库WMS Server库存明细查询接口失败,触发降级", cause); |
| | | return errorResponse(); |
| | | } |
| | | |
| | | @Override |
| | | public Map<String, Object> inventorySummary(Map<String, Object> params) { |
| | | log.error("调用立库WMS Server库存汇总查询接口失败,触发降级", cause); |
| | | return errorResponse(); |
| | | } |
| | | } |
| New file |
| | |
| | | package com.vincent.rsf.openApi.feign.wms.fallback; |
| | | |
| | | import com.vincent.rsf.openApi.feign.wms.WmsServerFeignClient; |
| | | import org.springframework.cloud.openfeign.FallbackFactory; |
| | | import org.springframework.stereotype.Component; |
| | | |
| | | /** |
| | | * Feign 调用失败时创建带异常信息的 Fallback,在 Feign 内统一返回错误响应。 |
| | | */ |
| | | @Component |
| | | public class WmsServerFeignClientFallbackFactory implements FallbackFactory<WmsServerFeignClient> { |
| | | |
| | | @Override |
| | | public WmsServerFeignClient create(Throwable cause) { |
| | | return new WmsServerFeignClientFallback(cause); |
| | | } |
| | | } |
| New file |
| | |
| | | package com.vincent.rsf.openApi.mapper; |
| | | |
| | | import com.baomidou.mybatisplus.core.mapper.BaseMapper; |
| | | import com.vincent.rsf.openApi.entity.app.App; |
| | | import org.apache.ibatis.annotations.Mapper; |
| | | import org.springframework.stereotype.Repository; |
| | | |
| | | @Mapper |
| | | @Repository |
| | | public interface AppMapper extends BaseMapper<App> { |
| | | } |
| New file |
| | |
| | | package com.vincent.rsf.openApi.security.filter; |
| | | |
| | | import com.vincent.rsf.openApi.entity.constant.Constants; |
| | | import com.vincent.rsf.openApi.security.service.AppAuthService; |
| | | import com.vincent.rsf.openApi.security.utils.TokenUtils; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import org.springframework.core.annotation.Order; |
| | | import org.springframework.stereotype.Component; |
| | | import org.springframework.util.StringUtils; |
| | | import org.springframework.web.filter.OncePerRequestFilter; |
| | | |
| | | import javax.annotation.Resource; |
| | | import javax.servlet.FilterChain; |
| | | import javax.servlet.ServletException; |
| | | import javax.servlet.http.HttpServletRequest; |
| | | import javax.servlet.http.HttpServletResponse; |
| | | import java.io.IOException; |
| | | import java.io.PrintWriter; |
| | | |
| | | /** |
| | | * AppId/Token 认证过滤器 |
| | | */ |
| | | @Slf4j |
| | | @Component |
| | | @Order(1) |
| | | public class AppIdAuthenticationFilter extends OncePerRequestFilter { |
| | | |
| | | @Resource |
| | | private AppAuthService appAuthService; |
| | | |
| | | @Override |
| | | protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) |
| | | throws ServletException, IOException { |
| | | |
| | | String requestURI = request.getRequestURI(); |
| | | if (isAuthRequest(requestURI)) { |
| | | filterChain.doFilter(request, response); |
| | | return; |
| | | } |
| | | |
| | | String authHeader = request.getHeader(Constants.HEADER_AUTHORIZATION); |
| | | if (authHeader != null) { |
| | | String token = TokenUtils.extractTokenFromHeader(authHeader); |
| | | if (token != null && TokenUtils.validateTokenTime(token)) { |
| | | String tokenAppId = TokenUtils.getAppIdFromToken(token); |
| | | String tokenAppSecret = TokenUtils.getSecretFromToken(token); |
| | | if (!StringUtils.hasText(tokenAppId) || !StringUtils.hasText(tokenAppSecret) |
| | | || !appAuthService.validateApp(tokenAppId, tokenAppSecret)) { |
| | | log.warn("Token验证失败"); |
| | | sendErrorResponse(response, Constants.UNAUTHENTICATED_CODE, "认证失败,请提供有效的Token"); |
| | | return; |
| | | } |
| | | request.setAttribute(Constants.REQUEST_ATTR_APP_ID, tokenAppId); |
| | | } else { |
| | | log.warn("Token验证失败或缺失"); |
| | | sendErrorResponse(response, Constants.UNAUTHENTICATED_CODE, "认证失败,请提供有效的Token"); |
| | | return; |
| | | } |
| | | } else { |
| | | log.warn("缺少Token认证信息"); |
| | | sendErrorResponse(response, Constants.UNAUTHENTICATED_CODE, "认证失败,请提供有效的Token"); |
| | | return; |
| | | } |
| | | |
| | | filterChain.doFilter(request, response); |
| | | } |
| | | |
| | | private void sendErrorResponse(HttpServletResponse response, int code, String message) throws IOException { |
| | | response.setStatus(code); |
| | | response.setContentType("application/json;charset=UTF-8"); |
| | | PrintWriter writer = response.getWriter(); |
| | | writer.write("{\"code\": " + code + ", \"msg\": \"" + message + "\", \"data\": null}"); |
| | | writer.flush(); |
| | | } |
| | | |
| | | private boolean isAuthRequest(String requestURI) { |
| | | return requestURI != null && requestURI.contains("/getToken"); |
| | | } |
| | | |
| | | @Override |
| | | protected boolean shouldNotFilter(HttpServletRequest request) { |
| | | String requestURI = request.getRequestURI(); |
| | | return requestURI == null |
| | | || requestURI.contains("/auth/") |
| | | || requestURI.contains("/public/") |
| | | || requestURI.contains("/doc.html") |
| | | || requestURI.contains("/swagger") |
| | | || requestURI.contains("/webjars") |
| | | || requestURI.contains("/v2/api-docs") |
| | | || requestURI.contains("/v3/api-docs"); |
| | | } |
| | | } |
| New file |
| | |
| | | package com.vincent.rsf.openApi.security.service; |
| | | |
| | | import com.vincent.rsf.openApi.entity.app.App; |
| | | import com.vincent.rsf.openApi.service.AppService; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import org.springframework.security.crypto.password.PasswordEncoder; |
| | | import org.springframework.stereotype.Service; |
| | | |
| | | import javax.annotation.Resource; |
| | | |
| | | @Slf4j |
| | | @Service |
| | | public class AppAuthService { |
| | | |
| | | @Resource |
| | | private AppService appService; |
| | | @Resource |
| | | private PasswordEncoder passwordEncoder; |
| | | |
| | | public boolean validateApp(String appId, String appSecret) { |
| | | if (appId == null || appSecret == null) { |
| | | return false; |
| | | } |
| | | try { |
| | | App app = appService.getById(appId); |
| | | if (app == null) { |
| | | return false; |
| | | } |
| | | if (app.getEnable() != null && app.getEnable() != 1) { |
| | | return false; |
| | | } |
| | | String stored = app.getScrect(); |
| | | if (stored == null) { |
| | | return false; |
| | | } |
| | | // 存的是 BCrypt 哈希则用 matches,否则兼容明文 |
| | | if (stored.startsWith("$2")) { |
| | | return passwordEncoder.matches(appSecret, stored); |
| | | } |
| | | return appSecret.equals(stored); |
| | | } catch (Exception e) { |
| | | log.error("validateApp异常 appId={}", appId, e); |
| | | return false; |
| | | } |
| | | } |
| | | |
| | | public App getAppInfo(String appId) { |
| | | if (appId == null) { |
| | | return null; |
| | | } |
| | | try { |
| | | return appService.getById(appId); |
| | | } catch (Exception e) { |
| | | log.error("getAppInfo失败 appId={}", appId, e); |
| | | return null; |
| | | } |
| | | } |
| | | } |
| New file |
| | |
| | | package com.vincent.rsf.openApi.security.utils; |
| | | |
| | | import com.vincent.rsf.openApi.entity.constant.Constants; |
| | | import io.jsonwebtoken.Claims; |
| | | import io.jsonwebtoken.JwtException; |
| | | import io.jsonwebtoken.Jwts; |
| | | import io.jsonwebtoken.SignatureAlgorithm; |
| | | import io.jsonwebtoken.security.Keys; |
| | | import org.slf4j.Logger; |
| | | import org.slf4j.LoggerFactory; |
| | | |
| | | import javax.crypto.SecretKey; |
| | | import java.util.Date; |
| | | import java.util.HashMap; |
| | | import java.util.Map; |
| | | |
| | | /** |
| | | * JWT Token 工具类 |
| | | */ |
| | | public class TokenUtils { |
| | | private static final Logger log = LoggerFactory.getLogger(TokenUtils.class); |
| | | |
| | | private static final SecretKey SECRET_KEY = Keys.secretKeyFor(SignatureAlgorithm.HS256); |
| | | private static final long TOKEN_EXPIRATION = 60 * 60 * 1000L; |
| | | |
| | | public static String generateToken(Map<String, Object> claims) { |
| | | long now = System.currentTimeMillis(); |
| | | Date expiration = new Date(now + TOKEN_EXPIRATION); |
| | | return Jwts.builder() |
| | | .setClaims(claims) |
| | | .setExpiration(expiration) |
| | | .signWith(SECRET_KEY, SignatureAlgorithm.HS256) |
| | | .compact(); |
| | | } |
| | | |
| | | public static String generateToken(String appId, String appSecret) { |
| | | Map<String, Object> claims = new HashMap<>(); |
| | | claims.put("appId", appId); |
| | | claims.put("appSecret", appSecret); |
| | | claims.put("created", System.currentTimeMillis()); |
| | | return generateToken(claims); |
| | | } |
| | | |
| | | public static Claims parseToken(String token) { |
| | | try { |
| | | return Jwts.parserBuilder() |
| | | .setSigningKey(SECRET_KEY) |
| | | .build() |
| | | .parseClaimsJws(token) |
| | | .getBody(); |
| | | } catch (JwtException e) { |
| | | log.error("解析Token失败: {}", e.getMessage()); |
| | | return null; |
| | | } |
| | | } |
| | | |
| | | public static boolean validateTokenTime(String token) { |
| | | try { |
| | | Claims claims = parseToken(token); |
| | | if (claims == null) { |
| | | return false; |
| | | } |
| | | Date expiration = claims.getExpiration(); |
| | | return expiration != null && expiration.after(new Date()); |
| | | } catch (JwtException e) { |
| | | log.error("验证Token失败: {}", e.getMessage()); |
| | | return false; |
| | | } |
| | | } |
| | | |
| | | public static String getAppIdFromToken(String token) { |
| | | Claims claims = parseToken(token); |
| | | return claims != null ? (String) claims.get("appId") : null; |
| | | } |
| | | |
| | | public static String getSecretFromToken(String token) { |
| | | Claims claims = parseToken(token); |
| | | return claims != null ? (String) claims.get("appSecret") : null; |
| | | } |
| | | |
| | | public static String extractTokenFromHeader(String authHeader) { |
| | | if (authHeader != null && authHeader.startsWith(Constants.TOKEN_PREFIX)) { |
| | | return authHeader.substring(Constants.TOKEN_PREFIX.length()).trim(); |
| | | } |
| | | return null; |
| | | } |
| | | } |
| New file |
| | |
| | | package com.vincent.rsf.openApi.service; |
| | | |
| | | import com.baomidou.mybatisplus.extension.service.IService; |
| | | import com.vincent.rsf.openApi.entity.app.App; |
| | | |
| | | public interface AppService extends IService<App> { |
| | | } |
| New file |
| | |
| | | package com.vincent.rsf.openApi.service; |
| | | |
| | | /** |
| | | * 对接协议 8.1 Token 签发与校验 |
| | | */ |
| | | public interface TokenService { |
| | | |
| | | /** |
| | | * 校验 appId+appSecret 并签发 token,有效期 1 小时 |
| | | * @param appId 应用编码 |
| | | * @param appSecret 应用秘钥 |
| | | * @return token 字符串,失败返回 null |
| | | */ |
| | | String issueToken(String appId, String appSecret); |
| | | |
| | | /** |
| | | * 校验 token 是否有效 |
| | | */ |
| | | boolean validateToken(String token); |
| | | } |
| | |
| | | |
| | | CommonResponse getOrderInfo(ErpOpParams params); |
| | | |
| | | CommonResponse updateOrderDetl(ErpOpParams params); |
| | | /** 新增单据(兼容修改):入/出库通知单下发,有则更新、无则新增 */ |
| | | CommonResponse addOrUpdateOrder(ErpOpParams params); |
| | | |
| | | CommonResponse orderDel(ErpOpParams params); |
| | | /** 取消订单/取消单据:符合取消条件时执行取消逻辑 */ |
| | | CommonResponse orderCancel(ErpOpParams params); |
| | | |
| | | CommonResponse syncMatnrs(ErpMatnrParms parms); |
| | | |
| | | CommonResponse reportOrders(ReportParams params); |
| | | |
| | | CommonResponse reportCheck(ReportParams params); |
| | | |
| | | /** 库位信息查询(转发立库) */ |
| | | CommonResponse queryLocsDetls(Map<String, Object> params); |
| | | |
| | | /** |
| | | * 通用转发:将云仓请求原样转发至立库 WMS(接口提供方:立库) |
| | | * @param wmsPath 立库路径常量,见 WmsConstant |
| | | * @param body 请求体,可为 List 或 Map/对象,null 时按空 body 转发 |
| | | */ |
| | | CommonResponse forwardToWms(String wmsPath, Object body); |
| | | |
| | | /** 对接协议 8.4 库存明细查询 */ |
| | | CommonResponse inventoryDetails(Map<String, Object> params); |
| | | |
| | | /** 对接协议 8.5 库存汇总查询 */ |
| | | CommonResponse inventorySummary(Map<String, Object> params); |
| | | } |
| New file |
| | |
| | | package com.vincent.rsf.openApi.service.impl; |
| | | |
| | | import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; |
| | | import com.vincent.rsf.openApi.entity.app.App; |
| | | import com.vincent.rsf.openApi.mapper.AppMapper; |
| | | import com.vincent.rsf.openApi.service.AppService; |
| | | import org.springframework.stereotype.Service; |
| | | |
| | | @Service |
| | | public class AppServiceImpl extends ServiceImpl<AppMapper, App> implements AppService { |
| | | } |
| New file |
| | |
| | | package com.vincent.rsf.openApi.service.impl; |
| | | |
| | | import com.vincent.rsf.openApi.security.service.AppAuthService; |
| | | import com.vincent.rsf.openApi.service.TokenService; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import org.springframework.beans.factory.annotation.Autowired; |
| | | import org.springframework.stereotype.Service; |
| | | import org.springframework.util.StringUtils; |
| | | |
| | | import java.util.Map; |
| | | import java.util.UUID; |
| | | import java.util.concurrent.ConcurrentHashMap; |
| | | |
| | | @Slf4j |
| | | @Service |
| | | public class TokenServiceImpl implements TokenService { |
| | | |
| | | private static final long EXPIRE_MS = 60 * 60 * 1000L; |
| | | |
| | | @Autowired |
| | | private AppAuthService appAuthService; |
| | | |
| | | private final Map<String, Long> tokenExpire = new ConcurrentHashMap<>(); |
| | | |
| | | @Override |
| | | public String issueToken(String appId, String appSecret) { |
| | | if (!StringUtils.hasText(appId) || !StringUtils.hasText(appSecret)) { |
| | | return null; |
| | | } |
| | | if (!appAuthService.validateApp(appId, appSecret)) { |
| | | return null; |
| | | } |
| | | String token = UUID.randomUUID().toString().replace("-", ""); |
| | | tokenExpire.put(token, System.currentTimeMillis() + EXPIRE_MS); |
| | | evictExpired(); |
| | | return token; |
| | | } |
| | | |
| | | @Override |
| | | public boolean validateToken(String token) { |
| | | if (!StringUtils.hasText(token)) { |
| | | return false; |
| | | } |
| | | Long expire = tokenExpire.get(token); |
| | | if (expire == null || System.currentTimeMillis() > expire) { |
| | | tokenExpire.remove(token); |
| | | return false; |
| | | } |
| | | return true; |
| | | } |
| | | |
| | | private void evictExpired() { |
| | | long now = System.currentTimeMillis(); |
| | | tokenExpire.entrySet().removeIf(e -> e.getValue() < now); |
| | | } |
| | | } |
| | |
| | | import com.vincent.rsf.openApi.entity.constant.WmsConstant; |
| | | import com.vincent.rsf.openApi.entity.dto.CommonResponse; |
| | | import com.vincent.rsf.openApi.entity.dto.ErpCommonResponse; |
| | | import com.vincent.rsf.openApi.entity.dto.ResultData; |
| | | import com.vincent.rsf.openApi.entity.dto.OrderDto; |
| | | import com.vincent.rsf.openApi.entity.params.ErpMatnrParms; |
| | | import com.vincent.rsf.openApi.entity.params.ErpOpParams; |
| | | import com.vincent.rsf.openApi.entity.params.ReportParams; |
| | | import com.vincent.rsf.openApi.entity.params.WmsOrderItemParam; |
| | | import com.vincent.rsf.openApi.feign.wms.WmsServerFeignClient; |
| | | import com.vincent.rsf.openApi.service.WmsErpService; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import org.springframework.beans.factory.annotation.Autowired; |
| | |
| | | import org.springframework.web.client.RestTemplate; |
| | | |
| | | import java.util.*; |
| | | import java.util.stream.Collectors; |
| | | |
| | | @Slf4j |
| | | @Service("WmsErpService") |
| | |
| | | @Autowired |
| | | private RestTemplate restTemplate; |
| | | |
| | | @Autowired |
| | | private WmsServerFeignClient wmsServerFeignClient; |
| | | |
| | | /** |
| | | * 可选:改用 OpenFeign 调用 ERP 上报(订单完成回写、盘点差异修改)时启用。 |
| | | * 1)增加 import:com.vincent.rsf.openApi.feign.erp.ErpReportFeignClient; |
| | | * 2)取消下面两行注释,注入 ErpReportFeignClient; |
| | | * 3)在 reportOrders、reportCheck 中注释掉本方法内整段 HttpEntity/restTemplate 请求,改为使用下面注释中的 erpReportFeignClient.report(params) 及响应解析逻辑。 |
| | | */ |
| | | // @Autowired |
| | | // private ErpReportFeignClient erpReportFeignClient; |
| | | |
| | | /** |
| | | * 将 Feign 返回的 Map(或 R)转为 CommonResponse,符合 8.2.3:code、msg、data(含 result)。 |
| | | * 无法解析或非成功(code!=200)时直接 throw CoolException,不返回错误体。 |
| | | */ |
| | | private CommonResponse mapToCommonResponse(Map<String, Object> map) { |
| | | if (map == null) { |
| | | throw new CoolException("请求失败"); |
| | | } |
| | | Object c = map.get("code"); |
| | | int code = c instanceof Number ? ((Number) c).intValue() : 500; |
| | | if (code != 200) { |
| | | String msg = map.get("msg") != null ? map.get("msg").toString() : "请求失败"; |
| | | throw new CoolException(msg); |
| | | } |
| | | CommonResponse r = new CommonResponse(); |
| | | r.setCode(200); |
| | | r.setMsg(map.get("msg") != null ? map.get("msg").toString() : "操作成功"); |
| | | Object rawData = map.get("data"); |
| | | if (rawData == null) { |
| | | r.setData(ResultData.success()); |
| | | } else { |
| | | Map<String, Object> dataModel = new LinkedHashMap<>(); |
| | | dataModel.put("result", ResultData.SUCCESS); |
| | | dataModel.put("data", rawData); |
| | | r.setData(dataModel); |
| | | } |
| | | return r; |
| | | } |
| | | |
| | | /** |
| | | * 获取订单明细 |
| | | * |
| | |
| | | if (Objects.isNull(params.getOrderNo()) || params.getOrderNo().isEmpty()) { |
| | | throw new CoolException("订单号不能为空!!"); |
| | | } |
| | | /**WMS基础配置链接*/ |
| | | String rcsUrl = wmsApi.getHost() + ":" + wmsApi.getPort() + WmsConstant.QUERY_ORDER_AND_DETLS; |
| | | log.info("查询订单信息及状态: {}, 请求参数: {}", rcsUrl, JSONObject.toJSONString(params)); |
| | | HttpHeaders headers = new HttpHeaders(); |
| | | headers.add("Content-Type", "application/json"); |
| | | headers.add("api-version", "v2.0"); |
| | | HttpEntity httpEntity = new HttpEntity(params, headers); |
| | | ResponseEntity<String> exchange = restTemplate.exchange(rcsUrl, HttpMethod.POST, httpEntity, String.class); |
| | | log.info("查询响应结果: {}", exchange); |
| | | if (Objects.isNull(exchange.getBody())) { |
| | | throw new CoolException("查询失败!!"); |
| | | } else { |
| | | ObjectMapper objectMapper = new ObjectMapper(); |
| | | objectMapper.coercionConfigDefaults().setCoercion(CoercionInputShape.EmptyString, CoercionAction.AsEmpty); |
| | | try { |
| | | CommonResponse result = objectMapper.readValue(exchange.getBody(), CommonResponse.class); |
| | | if (result.getCode() == 200) { |
| | | JSONObject object = JSONObject.parseObject(JSONObject.toJSONString(result.getData())); |
| | | OrderDto dto = new OrderDto(); |
| | | dto.setOrderNo(object.getString("code")) |
| | | .setAnfme(object.getDouble("anfme")) |
| | | .setType(object.getString("type")) |
| | | .setWkType(object.getString("wkType")) |
| | | .setQty(object.getDouble("qty")) |
| | | .setPoCode(object.getString("poCode")) |
| | | .setExceStatus(object.getShort("exceStatus")) |
| | | .setWorkQty(object.getDouble("workQty")); |
| | | result.setData(dto); |
| | | return result; |
| | | } else { |
| | | return result; |
| | | // throw new CoolException("查询失败!!"); |
| | | } |
| | | } catch (JsonProcessingException e) { |
| | | throw new CoolException(e.getMessage()); |
| | | log.info("查询订单信息及状态,请求参数: {}", JSONObject.toJSONString(params)); |
| | | Map<String, Object> res = wmsServerFeignClient.queryOrderAndDetls(params); |
| | | CommonResponse result = mapToCommonResponse(res); |
| | | if (result.getCode() == 200 && result.getData() instanceof Map) { |
| | | @SuppressWarnings("unchecked") |
| | | Map<String, Object> dataModel = (Map<String, Object>) result.getData(); |
| | | Object inner = dataModel.get("data"); |
| | | if (inner != null) { |
| | | JSONObject object = JSONObject.parseObject(JSONObject.toJSONString(inner)); |
| | | OrderDto dto = new OrderDto(); |
| | | dto.setOrderNo(object.getString("code")) |
| | | .setAnfme(object.getDouble("anfme")) |
| | | .setType(object.getString("type")) |
| | | .setWkType(object.getString("wkType")) |
| | | .setQty(object.getDouble("qty")) |
| | | .setPoCode(object.getString("poCode")) |
| | | .setExceStatus(object.getShort("exceStatus")) |
| | | .setWorkQty(object.getDouble("workQty")); |
| | | Map<String, Object> wrap = new LinkedHashMap<>(); |
| | | wrap.put("result", ResultData.SUCCESS); |
| | | wrap.put("data", dto); |
| | | result.setData(wrap); |
| | | } |
| | | } |
| | | return result; |
| | | } |
| | | |
| | | /** |
| | | * 新增单据(兼容修改、取消):8.3 入/出库通知单下发。operateType=3 时按取消处理。 |
| | | * 以 8.3 文档字段为主,转发立库时映射为服务端 SyncOrderParams 字段。 |
| | | */ |
| | | @Override |
| | | public CommonResponse addOrUpdateOrder(ErpOpParams params) { |
| | | if (Objects.isNull(params.getOrderNo()) || params.getOrderNo().isEmpty()) { |
| | | throw new CoolException("订单号不能为空!!"); |
| | | } |
| | | if (Integer.valueOf(3).equals(params.getOperateType())) { |
| | | log.info("order/add 收到 operateType=3,走统一取消逻辑: {}", params.getOrderNo()); |
| | | return doCancel(params); |
| | | } |
| | | Map<String, Object> mapParams = toServerOrderMap(params); |
| | | List<Map<String, Object>> maps = Collections.singletonList(mapParams); |
| | | log.info("新增/修改单据,请求参数: {}", JSONArray.toJSONString(maps)); |
| | | Map<String, Object> res = wmsServerFeignClient.updateOrderDetls(maps); |
| | | CommonResponse r = mapToCommonResponse(res); |
| | | // 8.3.3:data 仅含 result,不返回业务载荷 |
| | | r.setData(ResultData.success()); |
| | | return r; |
| | | } |
| | | |
| | | /** 8.3 参数转为立库 SyncOrderParams 结构(orderNo/type/wkType/anfme/arrTime/orderItems 等) */ |
| | | private Map<String, Object> toServerOrderMap(ErpOpParams params) { |
| | | Map<String, Object> m = new HashMap<>(); |
| | | m.put("orderNo", params.getOrderNo()); |
| | | m.put("wkType", params.getWkType()); |
| | | m.put("type", params.getOrderType() != null ? String.valueOf(params.getOrderType()) : null); |
| | | m.put("orderId", params.getOrderId()); |
| | | double anfmeSum = 0; |
| | | if (params.getOrderItems() != null) { |
| | | List<Map<String, Object>> items = params.getOrderItems().stream() |
| | | .map(this::toServerOrderItemMap) |
| | | .collect(Collectors.toList()); |
| | | m.put("orderItems", items); |
| | | for (WmsOrderItemParam item : params.getOrderItems()) { |
| | | anfmeSum += parseAnfme(item.getAnfme()); |
| | | } |
| | | } else { |
| | | m.put("orderItems", Collections.emptyList()); |
| | | } |
| | | m.put("anfme", anfmeSum); |
| | | if (params.getBusinessTime() != null) { |
| | | m.put("arrTime", new Date(params.getBusinessTime() * 1000)); |
| | | } else if (params.getCreateTime() != null) { |
| | | m.put("arrTime", new Date(params.getCreateTime() * 1000)); |
| | | } |
| | | return m; |
| | | } |
| | | |
| | | private Map<String, Object> toServerOrderItemMap(WmsOrderItemParam item) { |
| | | Map<String, Object> m = new HashMap<>(); |
| | | m.put("matnr", item.getMatNr()); |
| | | m.put("maktx", item.getMakTx()); |
| | | m.put("platItemId", item.getLineId()); |
| | | m.put("anfme", parseAnfme(item.getAnfme())); |
| | | m.put("spec", item.getSpec()); |
| | | m.put("model", item.getModel()); |
| | | m.put("unit", item.getUnit()); |
| | | m.put("batch", item.getBatch()); |
| | | return m; |
| | | } |
| | | |
| | | private static double parseAnfme(String anfme) { |
| | | if (anfme == null || anfme.trim().isEmpty()) { |
| | | return 0; |
| | | } |
| | | try { |
| | | return Double.parseDouble(anfme.trim()); |
| | | } catch (NumberFormatException e) { |
| | | return 0; |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 订单修改 |
| | | * |
| | | * @param params |
| | | * @return |
| | | * 取消订单/取消单据。与 /order/add(operateType=3)共用同一套取消逻辑,转发立库 sync/orders/delete。 |
| | | */ |
| | | @Override |
| | | public CommonResponse updateOrderDetl(ErpOpParams params) { |
| | | public CommonResponse orderCancel(ErpOpParams params) { |
| | | if (Objects.isNull(params.getOrderNo()) || params.getOrderNo().isEmpty()) { |
| | | throw new CoolException("订单号不能为空!!"); |
| | | } |
| | | /**WMS基础配置链接*/ |
| | | String wmsUrl = wmsApi.getHost() + ":" + wmsApi.getPort() + WmsConstant.MODIFY_ORDER_DETLS; |
| | | HttpHeaders headers = new HttpHeaders(); |
| | | headers.add("Content-Type", "application/json"); |
| | | headers.add("api-version", "v2.0"); |
| | | |
| | | List<Map<String, Object>> maps = new ArrayList<>(); |
| | | Map<String, Object> mapParams = new HashMap<>(); |
| | | mapParams.put("orderNo", params.getOrderNo()); |
| | | mapParams.put("anfme", params.getAnfme()); |
| | | mapParams.put("type", params.getType()); |
| | | mapParams.put("wkType", params.getWkType()); |
| | | mapParams.put("exceStatus", params.getExceStatus()); |
| | | mapParams.put("orderItems", params.getOrderItems()); |
| | | maps.add(mapParams); |
| | | log.info("修改订单信息及状态: {}, 请求参数: {}", wmsUrl, JSONArray.toJSONString(maps)); |
| | | HttpEntity<List<Map<String, Object>>> httpEntity = new HttpEntity<>(maps, headers); |
| | | ResponseEntity<String> exchange = restTemplate.exchange(wmsUrl, HttpMethod.POST, httpEntity, String.class); |
| | | log.info("订单修改返回结果: {}", exchange); |
| | | if (Objects.isNull(exchange.getBody())) { |
| | | throw new CoolException("查询失败!!"); |
| | | } else { |
| | | ObjectMapper objectMapper = new ObjectMapper(); |
| | | objectMapper.coercionConfigDefaults().setCoercion(CoercionInputShape.EmptyString, CoercionAction.AsEmpty); |
| | | try { |
| | | CommonResponse result = objectMapper.readValue(exchange.getBody(), CommonResponse.class); |
| | | if (result.getCode() == 200) { |
| | | // JSONObject object = JSONObject.parseObject(JSONObject.toJSONString(result.getData())); |
| | | return result; |
| | | } else { |
| | | return result; |
| | | // throw new CoolException("查询失败!!"); |
| | | } |
| | | } catch (JsonProcessingException e) { |
| | | throw new CoolException(e.getMessage()); |
| | | } |
| | | } |
| | | return doCancel(params); |
| | | } |
| | | |
| | | /** |
| | | * 删除单据 |
| | | * |
| | | * @param params |
| | | * @return |
| | | */ |
| | | @Override |
| | | public CommonResponse orderDel(ErpOpParams params) { |
| | | if (Objects.isNull(params.getOrderNo()) || params.getOrderNo().isEmpty()) { |
| | | throw new CoolException("订单号不能为空!!"); |
| | | } |
| | | /**WMS基础配置链接*/ |
| | | String rcsUrl = wmsApi.getHost() + ":" + wmsApi.getPort() + WmsConstant.ORDER_DEL; |
| | | log.info("查询订单信息及状态: {}, 请求参数: {}", rcsUrl, JSONObject.toJSONString(params)); |
| | | HttpHeaders headers = new HttpHeaders(); |
| | | headers.add("Content-Type", "application/json"); |
| | | headers.add("api-version", "v2.0"); |
| | | HttpEntity httpEntity = new HttpEntity(params, headers); |
| | | ResponseEntity<String> exchange = restTemplate.exchange(rcsUrl, HttpMethod.POST, httpEntity, String.class); |
| | | log.info("查询响应结果: {}", exchange); |
| | | if (Objects.isNull(exchange.getBody())) { |
| | | throw new CoolException("查询失败!!"); |
| | | } else { |
| | | ObjectMapper objectMapper = new ObjectMapper(); |
| | | objectMapper.coercionConfigDefaults().setCoercion(CoercionInputShape.EmptyString, CoercionAction.AsEmpty); |
| | | try { |
| | | CommonResponse result = objectMapper.readValue(exchange.getBody(), CommonResponse.class); |
| | | if (result.getCode() == 200) { |
| | | return result; |
| | | } else { |
| | | throw new CoolException("查询失败!!"); |
| | | } |
| | | } catch (JsonProcessingException e) { |
| | | throw new CoolException(e.getMessage()); |
| | | } |
| | | } |
| | | /** 统一取消逻辑:/order/add(operateType=3) 与 /order/cancel、/order/del 均走此方法;8.3.3 data 仅含 result */ |
| | | private CommonResponse doCancel(ErpOpParams params) { |
| | | log.info("取消单据,请求参数: {}", JSONObject.toJSONString(params)); |
| | | Map<String, Object> one = new HashMap<>(); |
| | | one.put("orderNo", params.getOrderNo()); |
| | | one.put("orderItems", params.getOrderItems() != null ? params.getOrderItems().stream() |
| | | .map(this::toServerOrderItemMap) |
| | | .collect(Collectors.toList()) : Collections.emptyList()); |
| | | Map<String, Object> res = wmsServerFeignClient.orderDel(Collections.singletonList(one)); |
| | | CommonResponse r = mapToCommonResponse(res); |
| | | r.setData(ResultData.success()); |
| | | return r; |
| | | } |
| | | |
| | | /** |
| | |
| | | if (Objects.isNull(params.getMaktx())) { |
| | | throw new CoolException("物料名称不能为空!!"); |
| | | } |
| | | /**WMS基础配置链接*/ |
| | | String rcsUrl = wmsApi.getHost() + ":" + wmsApi.getPort() + WmsConstant.UPDATE_MATNR_INFO; |
| | | log.info("物料修改:{}, 请求参数: {}", rcsUrl, JSONObject.toJSONString(params)); |
| | | HttpHeaders headers = new HttpHeaders(); |
| | | headers.add("Content-Type", "application/json"); |
| | | headers.add("api-version", "v2.0"); |
| | | HttpEntity httpEntity = new HttpEntity(params, headers); |
| | | ResponseEntity<String> exchange = restTemplate.exchange(rcsUrl, HttpMethod.POST, httpEntity, String.class); |
| | | log.info("修改结果: {}", exchange); |
| | | if (Objects.isNull(exchange.getBody())) { |
| | | throw new CoolException("修改失败!!"); |
| | | } else { |
| | | ObjectMapper objectMapper = new ObjectMapper(); |
| | | objectMapper.coercionConfigDefaults().setCoercion(CoercionInputShape.EmptyString, CoercionAction.AsEmpty); |
| | | try { |
| | | CommonResponse result = objectMapper.readValue(exchange.getBody(), CommonResponse.class); |
| | | if (result.getCode() == 200) { |
| | | return result; |
| | | } else { |
| | | throw new CoolException("修改失败!!"); |
| | | } |
| | | } catch (JsonProcessingException e) { |
| | | throw new CoolException(e.getMessage()); |
| | | } |
| | | } |
| | | log.info("物料修改,请求参数: {}", JSONObject.toJSONString(params)); |
| | | Map<String, Object> res = wmsServerFeignClient.syncMatnrs(params); |
| | | return mapToCommonResponse(res); |
| | | } |
| | | |
| | | /** |
| | |
| | | throw new CoolException("上传失败!!"); |
| | | } |
| | | } |
| | | // Map<String, Object> res = erpReportFeignClient.report(params); |
| | | // if (res == null) throw new CoolException("上传失败!!"); |
| | | // Object c = res.get("code"); int code = c instanceof Number ? ((Number) c).intValue() : 500; |
| | | // if (code != 200) throw new CoolException("上传失败!!"); |
| | | // CommonResponse commonResponse = new CommonResponse(); |
| | | // commonResponse.setCode(200).setMsg(String.valueOf(res.get("msg"))).setData(res.get("data")); |
| | | // return commonResponse; |
| | | } |
| | | |
| | | /** |
| | |
| | | throw new CoolException("修改失败!!"); |
| | | } |
| | | } |
| | | // Map<String, Object> res = erpReportFeignClient.report(params); |
| | | // if (res == null) throw new CoolException("修改失败!!"); |
| | | // Object c = res.get("code"); int code = c instanceof Number ? ((Number) c).intValue() : 500; |
| | | // if (code != 200) throw new CoolException("修改失败!!"); |
| | | // CommonResponse commonResponse = new CommonResponse(); |
| | | // commonResponse.setCode(200).setMsg(String.valueOf(res.get("msg"))).setData(res.get("data")); |
| | | // return commonResponse; |
| | | } |
| | | |
| | | @Override |
| | | public CommonResponse queryLocsDetls(Map<String, Object> params) { |
| | | Map<String, Object> p = params == null ? new HashMap<>() : params; |
| | | log.info("库位信息查询,请求参数: {}", JSONObject.toJSONString(p)); |
| | | return mapToCommonResponse(wmsServerFeignClient.queryLocsDetls(p)); |
| | | } |
| | | |
| | | /** 8.4 库存明细查询:返回值 data 为对象数组,不包 result 外层 */ |
| | | @Override |
| | | public CommonResponse inventoryDetails(Map<String, Object> params) { |
| | | Map<String, Object> p = params == null ? new HashMap<>() : params; |
| | | log.info("库存明细查询,请求参数: {}", JSONObject.toJSONString(p)); |
| | | CommonResponse r = mapToCommonResponse(wmsServerFeignClient.inventoryDetails(p)); |
| | | unwrapDataToArray(r); |
| | | return r; |
| | | } |
| | | |
| | | /** 8.5 库存汇总查询:返回值 data 为对象数组,不包 result 外层 */ |
| | | @Override |
| | | public CommonResponse inventorySummary(Map<String, Object> params) { |
| | | Map<String, Object> p = params == null ? new HashMap<>() : params; |
| | | log.info("库存汇总查询,请求参数: {}", JSONObject.toJSONString(p)); |
| | | CommonResponse r = mapToCommonResponse(wmsServerFeignClient.inventorySummary(p)); |
| | | unwrapDataToArray(r); |
| | | return r; |
| | | } |
| | | |
| | | /** 8.4/8.5 规范:data 为对象数组,将 { result, data: array } 改为 data = array */ |
| | | private void unwrapDataToArray(CommonResponse r) { |
| | | if (r.getData() instanceof Map) { |
| | | @SuppressWarnings("unchecked") |
| | | Map<String, Object> dataModel = (Map<String, Object>) r.getData(); |
| | | Object inner = dataModel.get("data"); |
| | | if (inner != null) { |
| | | r.setData(inner); |
| | | } |
| | | } |
| | | } |
| | | |
| | | @Override |
| | | public CommonResponse forwardToWms(String wmsPath, Object body) { |
| | | String url = wmsApi.getHost() + ":" + wmsApi.getPort() + wmsPath; |
| | | Object payload = body != null ? body : new HashMap<String, Object>(); |
| | | log.info("转发请求: {}, 请求体长度: {}", url, payload instanceof List ? ((List<?>) payload).size() : 1); |
| | | return postToWms(url, payload); |
| | | } |
| | | |
| | | /** 统一转发并解析为 CommonResponse */ |
| | | private CommonResponse postToWms(String url, Object body) { |
| | | HttpHeaders headers = new HttpHeaders(); |
| | | headers.add("Content-Type", "application/json"); |
| | | headers.add("api-version", "v2.0"); |
| | | HttpEntity<Object> httpEntity = new HttpEntity<>(body, headers); |
| | | ResponseEntity<String> exchange = restTemplate.exchange(url, HttpMethod.POST, httpEntity, String.class); |
| | | if (Objects.isNull(exchange.getBody())) { |
| | | throw new CoolException("请求失败!!"); |
| | | } |
| | | ObjectMapper objectMapper = new ObjectMapper(); |
| | | objectMapper.coercionConfigDefaults().setCoercion(CoercionInputShape.EmptyString, CoercionAction.AsEmpty); |
| | | try { |
| | | return objectMapper.readValue(exchange.getBody(), CommonResponse.class); |
| | | } catch (JsonProcessingException e) { |
| | | throw new CoolException("解析响应失败:" + e.getMessage()); |
| | | } |
| | | } |
| | | |
| | | } |
| | |
| | | spring: |
| | | application: |
| | | name: @pom.artifactId@ |
| | | cloud: |
| | | openfeign: |
| | | circuitbreaker: |
| | | enabled: true # Feign 调用失败时走 Fallback,在 Feign 内统一返回错误 |
| | | mvc: |
| | | static-path-pattern: /** |
| | | path match: |
| | |
| | | datasource: |
| | | driver-class-name: com.mysql.cj.jdbc.Driver |
| | | url: jdbc:mysql://127.0.0.1:3306/rsf_jdxaj?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai |
| | | # url: jdbc:mysql://127.0.0.1:3306/jdxajwms?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai |
| | | username: root |
| | | password: 12345 |
| | | type: com.alibaba.druid.pool.DruidDataSource |
| | |
| | | port: 8086 |
| | | erp: |
| | | #链接 |
| | | host: http://www.itsdg.cn |
| | | host: http://127.0.0.1 |
| | | #端口 |
| | | port: 3741 |
| | |
| | | host: http://127.0.0.1 |
| | | port: 8085 |
| | | erp: |
| | | host: http://www.itsdg.cn |
| | | host: http://127.0.0.1 |
| | | port: 3741 |
| | |
| | | :banner: false |
| | | db-config: |
| | | id-type: auto |
| | | logic-delete-value: 1 |
| | | logic-not-delete-value: 0 |
| | | logic-delete-value: 1 #删除状态 |
| | | logic-not-delete-value: 0 #正常状态 |
| | | |
| | | logging: |
| | | file: |
| | |
| | | <groupId>org.springframework.boot</groupId> |
| | | <artifactId>spring-boot-starter-security</artifactId> |
| | | </dependency> |
| | | <dependency> |
| | | <groupId>org.springframework.cloud</groupId> |
| | | <artifactId>spring-cloud-starter-openfeign</artifactId> |
| | | </dependency> |
| | | <!-- 熔断器:Feign 调用失败时触发 Fallback,在 Feign 内统一返回错误响应 --> |
| | | <dependency> |
| | | <groupId>org.springframework.cloud</groupId> |
| | | <artifactId>spring-cloud-starter-circuitbreaker-reactor-resilience4j</artifactId> |
| | | </dependency> |
| | | <dependency> |
| | | <groupId>org.springframework.boot</groupId> |
| | | <artifactId>spring-boot-starter-test</artifactId> |
| | | <scope>test</scope> |
| | | </dependency> |
| | | <dependency> |
| | | <groupId>org.springframework.security</groupId> |
| | | <artifactId>spring-security-test</artifactId> |
| | | <scope>test</scope> |
| | | </dependency> |
| | | </dependencies> |
| | | |
| | | <build> |
| | |
| | | |
| | | import org.springframework.boot.SpringApplication; |
| | | import org.springframework.boot.autoconfigure.SpringBootApplication; |
| | | import org.springframework.cloud.openfeign.EnableFeignClients; |
| | | import org.springframework.context.annotation.ComponentScan; |
| | | |
| | | @SpringBootApplication |
| | | @ComponentScan(basePackages = {"com.vincent.rsf.server", "com.vincent.rsf.common"}) |
| | | @EnableFeignClients(basePackages = "com.vincent.rsf.server.api.feign") |
| | | public class ServerBoot { |
| | | |
| | | public static void main(String[] args) { |
| | |
| | | */ |
| | | private String prePath; |
| | | |
| | | /** |
| | | * 云仓地址 |
| | | */ |
| | | private String baseUrl; |
| | | |
| | | @Data |
| | | @Configuration |
| | | @ConfigurationProperties(prefix = "platform.erp.api") |
| | | public class ApiInfo { |
| | | /** |
| | | * 一键上报质检接口 |
| | | */ |
| | | /** 一键上报质检接口 */ |
| | | private String notifyInspect; |
| | | /** 9.1 入/出库结果上报(立库侧请求云仓) */ |
| | | private String inOutResultPath = "/api/report/inOutResult"; |
| | | /** 9.2 库存调整主动上报(立库侧请求云仓) */ |
| | | private String inventoryAdjustPath = "/api/report/inventoryAdjust"; |
| | | /** 物料基础信息同步(立库侧请求云仓) */ |
| | | private String matSyncPath = "/api/mat/sync"; |
| | | } |
| | | |
| | | @Data |
| New file |
| | |
| | | package com.vincent.rsf.server.api.controller; |
| | | |
| | | import com.vincent.rsf.server.api.controller.erp.params.InOutResultReportParam; |
| | | import com.vincent.rsf.server.api.controller.erp.params.InventoryAdjustReportParam; |
| | | import io.swagger.annotations.Api; |
| | | import io.swagger.annotations.ApiOperation; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import org.springframework.http.MediaType; |
| | | 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.RestController; |
| | | |
| | | import java.util.HashMap; |
| | | import java.util.Map; |
| | | |
| | | /** |
| | | * 云仓WMS 模拟接口(对接协议 9.1、9.2、物料同步)。 |
| | | * 云仓未提供真实 URL 时,可将 platform.erp.base-url 指向本机该服务(如 http://127.0.0.1:8086/rsf-server), |
| | | * 立库上报请求会打到本接口并返回模拟成功。 |
| | | */ |
| | | @Slf4j |
| | | @RestController |
| | | @RequestMapping("/api") |
| | | @Api(value = "云仓模拟接口", tags = "云仓模拟(无真实云仓URL时使用)") |
| | | public class CloudWmsMockController { |
| | | |
| | | private static Map<String, Object> successResponse() { |
| | | Map<String, Object> data = new HashMap<>(); |
| | | data.put("result", "SUCCESS"); |
| | | Map<String, Object> map = new HashMap<>(); |
| | | map.put("code", 200); |
| | | map.put("msg", ""); |
| | | map.put("data", data); |
| | | return map; |
| | | } |
| | | |
| | | /** 9.1 入/出库结果上报 - 模拟 */ |
| | | @ApiOperation("入/出库结果上报(模拟)") |
| | | @PostMapping(value = "/report/inOutResult", consumes = MediaType.APPLICATION_JSON_VALUE) |
| | | public Map<String, Object> mockInOutResult(@RequestBody InOutResultReportParam body) { |
| | | log.info("云仓模拟-入/出库结果上报,orderNo={},locId={},matNr={}", |
| | | body != null ? body.getOrderNo() : null, |
| | | body != null ? body.getLocId() : null, |
| | | body != null ? body.getMatNr() : null); |
| | | return successResponse(); |
| | | } |
| | | |
| | | /** 9.2 库存调整主动上报 - 模拟 */ |
| | | @ApiOperation("库存调整主动上报(模拟)") |
| | | @PostMapping(value = "/report/inventoryAdjust", consumes = MediaType.APPLICATION_JSON_VALUE) |
| | | public Map<String, Object> mockInventoryAdjust(@RequestBody InventoryAdjustReportParam body) { |
| | | log.info("云仓模拟-库存调整上报,changeType={},wareHouseId={},matNr={}", |
| | | body != null ? body.getChangeType() : null, |
| | | body != null ? body.getWareHouseId() : null, |
| | | body != null ? body.getMatNr() : null); |
| | | return successResponse(); |
| | | } |
| | | |
| | | /** 物料基础信息同步 - 模拟 */ |
| | | @ApiOperation("物料同步(模拟)") |
| | | @PostMapping(value = "/mat/sync", consumes = MediaType.APPLICATION_JSON_VALUE) |
| | | public Map<String, Object> mockMatSync(@RequestBody Object body) { |
| | | log.info("云仓模拟-物料同步,body={}", body != null ? body.toString() : null); |
| | | return successResponse(); |
| | | } |
| | | } |
| | |
| | | return receiveMsgService.queryTransfer(queryParams); |
| | | } |
| | | |
| | | /** |
| | | * 对接协议 8.4 库存明细查询 |
| | | */ |
| | | @PostMapping("/inventory/details") |
| | | @ApiOperation(value = "库存明细查询") |
| | | public R inventoryDetails(@RequestBody(required = false) InventoryDetailsParam param) { |
| | | return receiveMsgService.inventoryDetails(param != null ? param : new InventoryDetailsParam()); |
| | | } |
| | | |
| | | /** |
| | | * 对接协议 8.5 库存汇总查询 |
| | | */ |
| | | @PostMapping("/inventory/summary") |
| | | @ApiOperation(value = "库存汇总查询") |
| | | public R inventorySummary(@RequestBody(required = false) InventorySummaryParam param) { |
| | | return receiveMsgService.inventorySummary(param != null ? param : new InventorySummaryParam()); |
| | | } |
| | | |
| | | } |
| | |
| | | package com.vincent.rsf.server.api.controller.erp.params; |
| | | |
| | | |
| | | import com.fasterxml.jackson.annotation.JsonAlias; |
| | | import io.swagger.annotations.ApiModelProperty; |
| | | import lombok.experimental.Accessors; |
| | | import lombok.Data; |
| | | import lombok.experimental.Accessors; |
| | | |
| | | /** |
| | | * 物料基础信息同步入参(对接协议 8.2) |
| | | * 协议字段 matNr/makTx 与 matnr/maktx 均可接收。 |
| | | */ |
| | | @Data |
| | | @Accessors(chain = true) |
| | | public class BaseMatParms { |
| | | |
| | | @ApiModelProperty("物料名称") |
| | | private String maktx; |
| | | @ApiModelProperty(value = "操作类型:1新增 2修改 3禁用 4启用", example = "1") |
| | | private Integer operateType; |
| | | |
| | | @ApiModelProperty("物料编码*") |
| | | @ApiModelProperty(value = "物料编码*(协议字段 matNr 同义)") |
| | | @JsonAlias("matNr") |
| | | private String matnr; |
| | | |
| | | @ApiModelProperty(value = "物料名称(协议字段 makTx 同义)") |
| | | @JsonAlias("makTx") |
| | | private String maktx; |
| | | |
| | | @ApiModelProperty("物料分组") |
| | | private String groupName; |
| | | |
| New file |
| | |
| | | package com.vincent.rsf.server.api.controller.erp.params; |
| | | |
| | | import com.fasterxml.jackson.core.JsonParser; |
| | | import com.fasterxml.jackson.core.JsonToken; |
| | | import com.fasterxml.jackson.databind.DeserializationContext; |
| | | import com.fasterxml.jackson.databind.JsonDeserializer; |
| | | |
| | | import java.io.IOException; |
| | | import java.text.SimpleDateFormat; |
| | | import java.util.Date; |
| | | import java.util.TimeZone; |
| | | |
| | | /** |
| | | * 支持时间戳(秒/毫秒)与多种字符串格式的 Date 反序列化。 |
| | | * 字符串支持:ISO-8601(如 2024-03-03T08:00:00.000+00:00)、yyyy-MM-dd HH:mm:ss,精确到秒。 |
| | | */ |
| | | public class FlexibleDateDeserializer extends JsonDeserializer<Date> { |
| | | |
| | | private static final long TIMESTAMP_MS_THRESHOLD = 10_000_000_000L; // 约 1970-04-26 起为毫秒 |
| | | |
| | | private static final String PATTERN_SECONDS = "yyyy-MM-dd HH:mm:ss"; |
| | | private static final TimeZone DEFAULT_TZ = TimeZone.getTimeZone("GMT+8"); |
| | | |
| | | @Override |
| | | public Date deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { |
| | | JsonToken t = p.getCurrentToken(); |
| | | if (t == JsonToken.VALUE_NUMBER_INT || t == JsonToken.VALUE_NUMBER_FLOAT) { |
| | | long v = p.getLongValue(); |
| | | long ms = v < TIMESTAMP_MS_THRESHOLD ? v * 1000 : v; |
| | | return new Date(ms); |
| | | } |
| | | if (t == JsonToken.VALUE_STRING) { |
| | | String s = p.getText().trim(); |
| | | if (s.isEmpty()) { |
| | | return null; |
| | | } |
| | | // 1) 尝试纯数字字符串(秒或毫秒) |
| | | try { |
| | | long v = Long.parseLong(s); |
| | | long ms = v < TIMESTAMP_MS_THRESHOLD ? v * 1000 : v; |
| | | return new Date(ms); |
| | | } catch (NumberFormatException ignored) { |
| | | } |
| | | // 2) ISO-8601(含 T 和时区) |
| | | if (s.contains("T")) { |
| | | try { |
| | | return java.util.Date.from(java.time.Instant.parse(s)); |
| | | } catch (Exception ignored) { |
| | | } |
| | | } |
| | | // 3) yyyy-MM-dd HH:mm:ss |
| | | try { |
| | | SimpleDateFormat sdf = new SimpleDateFormat(PATTERN_SECONDS); |
| | | sdf.setTimeZone(DEFAULT_TZ); |
| | | sdf.setLenient(false); |
| | | return sdf.parse(s); |
| | | } catch (Exception ignored) { |
| | | } |
| | | // 4) 仅日期 yyyy-MM-dd |
| | | if (s.length() == 10 && s.charAt(4) == '-' && s.charAt(7) == '-') { |
| | | try { |
| | | SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); |
| | | sdf.setTimeZone(DEFAULT_TZ); |
| | | sdf.setLenient(false); |
| | | return sdf.parse(s); |
| | | } catch (Exception ignored) { |
| | | } |
| | | } |
| | | throw new IOException("Cannot parse date: " + s + ", support: timestamp(seconds/ms), ISO-8601, yyyy-MM-dd HH:mm:ss"); |
| | | } |
| | | if (t == JsonToken.VALUE_NULL) { |
| | | return null; |
| | | } |
| | | return (Date) ctxt.handleUnexpectedToken(Date.class, p); |
| | | } |
| | | } |
| New file |
| | |
| | | package com.vincent.rsf.server.api.controller.erp.params; |
| | | |
| | | import io.swagger.annotations.ApiModel; |
| | | import io.swagger.annotations.ApiModelProperty; |
| | | import lombok.Data; |
| | | import lombok.experimental.Accessors; |
| | | |
| | | /** |
| | | * 对接协议 9.1 入/出库结果上报 - 请求体(立库WMS 直接请求 云仓WMS) |
| | | */ |
| | | @Data |
| | | @Accessors(chain = true) |
| | | @ApiModel(value = "InOutResultReportParam", description = "入/出库结果上报") |
| | | public class InOutResultReportParam { |
| | | |
| | | @ApiModelProperty(value = "订单编码", required = true) |
| | | private String orderNo; |
| | | |
| | | @ApiModelProperty("计划跟踪号") |
| | | private String planNo; |
| | | |
| | | @ApiModelProperty("行内码") |
| | | private String lineId; |
| | | |
| | | @ApiModelProperty(value = "仓库编码", required = true) |
| | | private String wareHouseId; |
| | | |
| | | @ApiModelProperty(value = "库位号", required = true) |
| | | private String locId; |
| | | |
| | | @ApiModelProperty(value = "物料编码", required = true) |
| | | private String matNr; |
| | | |
| | | @ApiModelProperty(value = "本次出/入数量", required = true) |
| | | private String qty; |
| | | |
| | | @ApiModelProperty("托盘号") |
| | | private String palletId; |
| | | |
| | | @ApiModelProperty("批次") |
| | | private String batch; |
| | | } |
| New file |
| | |
| | | package com.vincent.rsf.server.api.controller.erp.params; |
| | | |
| | | import io.swagger.annotations.ApiModel; |
| | | import io.swagger.annotations.ApiModelProperty; |
| | | import lombok.Data; |
| | | import lombok.experimental.Accessors; |
| | | |
| | | /** |
| | | * 对接协议 9.2 库存调整主动上报 - 请求体 |
| | | * 立库侧调用云仓通知:立库WMS 主动调整库存后向云仓WMS 上报。 |
| | | */ |
| | | @Data |
| | | @Accessors(chain = true) |
| | | @ApiModel(value = "InventoryAdjustReportParam", description = "库存调整主动上报") |
| | | public class InventoryAdjustReportParam { |
| | | |
| | | @ApiModelProperty(value = "调整类型:1 入库;2 出库;3 移库", required = true) |
| | | private Integer changeType; |
| | | |
| | | @ApiModelProperty(value = "仓库编码", required = true) |
| | | private String wareHouseId; |
| | | |
| | | @ApiModelProperty("源库位号") |
| | | private String sourceLocId; |
| | | |
| | | @ApiModelProperty("目标库位号(移库时有)") |
| | | private String targetLocId; |
| | | |
| | | @ApiModelProperty(value = "物料编码", required = true) |
| | | private String matNr; |
| | | |
| | | @ApiModelProperty(value = "调整数量", required = true) |
| | | private String qty; |
| | | |
| | | @ApiModelProperty("托盘号") |
| | | private String palletId; |
| | | } |
| New file |
| | |
| | | package com.vincent.rsf.server.api.controller.erp.params; |
| | | |
| | | import io.swagger.annotations.ApiModel; |
| | | import io.swagger.annotations.ApiModelProperty; |
| | | import lombok.Data; |
| | | import lombok.experimental.Accessors; |
| | | |
| | | import java.io.Serializable; |
| | | |
| | | /** |
| | | * 对接协议 8.4 库存明细查询 请求参数 |
| | | */ |
| | | @Data |
| | | @Accessors(chain = true) |
| | | @ApiModel(value = "InventoryDetailsParam", description = "库存明细查询") |
| | | public class InventoryDetailsParam implements Serializable { |
| | | |
| | | @ApiModelProperty("仓库编码") |
| | | private String wareHouseId; |
| | | |
| | | @ApiModelProperty("库位编码") |
| | | private String locId; |
| | | |
| | | @ApiModelProperty("物料编码") |
| | | private String matNr; |
| | | |
| | | @ApiModelProperty("订单号/工单号") |
| | | private String orderNo; |
| | | |
| | | @ApiModelProperty("计划跟踪号") |
| | | private String planNo; |
| | | |
| | | @ApiModelProperty("批次号") |
| | | private String batch; |
| | | |
| | | @ApiModelProperty("物料组") |
| | | private String matGroup; |
| | | } |
| New file |
| | |
| | | package com.vincent.rsf.server.api.controller.erp.params; |
| | | |
| | | import io.swagger.annotations.ApiModel; |
| | | import io.swagger.annotations.ApiModelProperty; |
| | | import lombok.Data; |
| | | import lombok.experimental.Accessors; |
| | | |
| | | import java.io.Serializable; |
| | | |
| | | /** |
| | | * 对接协议 8.5 库存汇总查询 请求参数 |
| | | */ |
| | | @Data |
| | | @Accessors(chain = true) |
| | | @ApiModel(value = "InventorySummaryParam", description = "库存汇总查询") |
| | | public class InventorySummaryParam implements Serializable { |
| | | |
| | | @ApiModelProperty("仓库编码") |
| | | private String wareHouseId; |
| | | |
| | | @ApiModelProperty("物料编码,多个以英文逗号分隔") |
| | | private String matNr; |
| | | } |
| | |
| | | package com.vincent.rsf.server.api.controller.erp.params; |
| | | |
| | | import com.fasterxml.jackson.annotation.JsonFormat; |
| | | import com.fasterxml.jackson.databind.annotation.JsonDeserialize; |
| | | import io.swagger.annotations.ApiModel; |
| | | import io.swagger.annotations.ApiModelProperty; |
| | | import lombok.Data; |
| | |
| | | @ApiModelProperty("数量") |
| | | private Double anfme; |
| | | |
| | | @DateTimeFormat(pattern = "yyyy-MM-dd HH:ss:mm") |
| | | @JsonFormat(pattern = "yyyy-MM-dd HH:ss:mm") |
| | | @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") |
| | | @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") |
| | | @JsonDeserialize(using = FlexibleDateDeserializer.class) |
| | | private Date arrTime; |
| | | |
| | | @ApiModelProperty("单据明细信息") |
| New file |
| | |
| | | package com.vincent.rsf.server.api.feign; |
| | | |
| | | import com.vincent.rsf.server.api.controller.erp.params.InOutResultReportParam; |
| | | import com.vincent.rsf.server.api.controller.erp.params.InventoryAdjustReportParam; |
| | | import com.vincent.rsf.server.api.feign.fallback.CloudWmsErpFeignClientFallbackFactory; |
| | | import org.springframework.cloud.openfeign.FeignClient; |
| | | import org.springframework.http.MediaType; |
| | | import org.springframework.web.bind.annotation.PostMapping; |
| | | import org.springframework.web.bind.annotation.RequestBody; |
| | | |
| | | import java.util.Map; |
| | | |
| | | /** |
| | | * 立库侧通过 OpenFeign 调用云仓WMS:入/出库结果上报(9.1)、库存调整上报(9.2)、物料同步。 |
| | | * 使用 platform.erp.base-url 作为根地址;失败时走 Fallback,统一返回错误响应(不抛异常)。 |
| | | */ |
| | | @FeignClient( |
| | | name = "cloudWmsErp", |
| | | url = "${platform.erp.base-url:http://127.0.0.1:8080}", |
| | | fallbackFactory = CloudWmsErpFeignClientFallbackFactory.class |
| | | ) |
| | | public interface CloudWmsErpFeignClient { |
| | | |
| | | /** 9.1 入/出库结果上报 */ |
| | | @PostMapping(value = "/api/report/inOutResult", consumes = MediaType.APPLICATION_JSON_VALUE) |
| | | Map<String, Object> reportInOutResult(@RequestBody InOutResultReportParam body); |
| | | |
| | | /** 9.2 库存调整主动上报 */ |
| | | @PostMapping(value = "/api/report/inventoryAdjust", consumes = MediaType.APPLICATION_JSON_VALUE) |
| | | Map<String, Object> reportInventoryAdjust(@RequestBody InventoryAdjustReportParam body); |
| | | |
| | | /** 物料基础信息同步 */ |
| | | @PostMapping(value = "/api/mat/sync", consumes = MediaType.APPLICATION_JSON_VALUE) |
| | | Map<String, Object> syncMatnrs(@RequestBody Object body); |
| | | } |
| New file |
| | |
| | | package com.vincent.rsf.server.api.feign.fallback; |
| | | |
| | | import com.vincent.rsf.server.api.controller.erp.params.InOutResultReportParam; |
| | | import com.vincent.rsf.server.api.controller.erp.params.InventoryAdjustReportParam; |
| | | import com.vincent.rsf.server.api.feign.CloudWmsErpFeignClient; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import org.springframework.stereotype.Component; |
| | | |
| | | import java.util.HashMap; |
| | | import java.util.Map; |
| | | |
| | | /** |
| | | * 云仓WMS Feign 客户端降级处理,在 Feign 内统一返回错误响应(不抛异常)。 |
| | | * 由 CloudWmsErpFeignClientFallbackFactory 创建并传入异常 cause。 |
| | | */ |
| | | @Slf4j |
| | | @Component |
| | | public class CloudWmsErpFeignClientFallback implements CloudWmsErpFeignClient { |
| | | |
| | | private final Throwable cause; |
| | | |
| | | public CloudWmsErpFeignClientFallback() { |
| | | this.cause = null; |
| | | } |
| | | |
| | | public CloudWmsErpFeignClientFallback(Throwable cause) { |
| | | this.cause = cause; |
| | | } |
| | | |
| | | private Map<String, Object> errorResponse() { |
| | | return resultMap(500, filterErrorMessage(cause), dataFail()); |
| | | } |
| | | |
| | | private static Map<String, Object> dataFail() { |
| | | Map<String, Object> data = new HashMap<>(); |
| | | data.put("result", "FAIL"); |
| | | return data; |
| | | } |
| | | |
| | | private static Map<String, Object> resultMap(int code, String msg, Map<String, Object> data) { |
| | | Map<String, Object> map = new HashMap<>(); |
| | | map.put("code", code); |
| | | map.put("msg", msg); |
| | | map.put("data", data); |
| | | return map; |
| | | } |
| | | |
| | | /** |
| | | * 过滤错误消息中的 URL,只保留错误类型 |
| | | */ |
| | | public static String filterErrorMessage(Throwable throwable) { |
| | | if (throwable == null) { |
| | | return "请求失败:服务调用失败,请稍后重试"; |
| | | } |
| | | return filterErrorMessage(throwable.getMessage()); |
| | | } |
| | | |
| | | public static String filterErrorMessage(String errorMessage) { |
| | | if (errorMessage == null || errorMessage.isEmpty()) { |
| | | return "请求失败:未知错误"; |
| | | } |
| | | String filteredMessage = errorMessage; |
| | | if (filteredMessage.contains("executing")) { |
| | | int i = filteredMessage.indexOf("executing"); |
| | | filteredMessage = i > 0 ? filteredMessage.substring(0, i).trim() : "请求超时"; |
| | | } else if (filteredMessage.contains("http://") || filteredMessage.contains("https://")) { |
| | | filteredMessage = filteredMessage.replaceAll("https?://[^\\s]+", "").trim(); |
| | | if (filteredMessage.isEmpty()) { |
| | | filteredMessage = "请求失败"; |
| | | } |
| | | } |
| | | if (filteredMessage.isEmpty()) { |
| | | filteredMessage = "未知错误"; |
| | | } |
| | | return "请求失败:" + filteredMessage; |
| | | } |
| | | |
| | | @Override |
| | | public Map<String, Object> reportInOutResult(InOutResultReportParam body) { |
| | | log.error("调用云仓WMS 入/出库结果上报接口失败,触发降级", cause); |
| | | return errorResponse(); |
| | | } |
| | | |
| | | @Override |
| | | public Map<String, Object> reportInventoryAdjust(InventoryAdjustReportParam body) { |
| | | log.error("调用云仓WMS 库存调整上报接口失败,触发降级", cause); |
| | | return errorResponse(); |
| | | } |
| | | |
| | | @Override |
| | | public Map<String, Object> syncMatnrs(Object body) { |
| | | log.error("调用云仓WMS 物料同步接口失败,触发降级", cause); |
| | | return errorResponse(); |
| | | } |
| | | } |
| New file |
| | |
| | | package com.vincent.rsf.server.api.feign.fallback; |
| | | |
| | | import com.vincent.rsf.server.api.feign.CloudWmsErpFeignClient; |
| | | import org.springframework.cloud.openfeign.FallbackFactory; |
| | | import org.springframework.stereotype.Component; |
| | | |
| | | /** |
| | | * Feign 调用云仓失败时创建带异常信息的 Fallback,在 Feign 内统一返回错误响应。 |
| | | */ |
| | | @Component |
| | | public class CloudWmsErpFeignClientFallbackFactory implements FallbackFactory<CloudWmsErpFeignClient> { |
| | | |
| | | @Override |
| | | public CloudWmsErpFeignClient create(Throwable cause) { |
| | | return new CloudWmsErpFeignClientFallback(cause); |
| | | } |
| | | } |
| New file |
| | |
| | | package com.vincent.rsf.server.api.service; |
| | | |
| | | import com.vincent.rsf.server.api.controller.erp.params.InOutResultReportParam; |
| | | import com.vincent.rsf.server.api.controller.erp.params.InventoryAdjustReportParam; |
| | | |
| | | import java.util.Map; |
| | | |
| | | /** |
| | | * 立库侧请求云仓WMS:上报、物料同步等 |
| | | */ |
| | | public interface CloudWmsReportService { |
| | | |
| | | /** |
| | | * 物料基础信息同步(立库侧请求云仓) |
| | | * @param body 物料数据,可为单条或列表,具体结构以云仓接口为准 |
| | | * @return 云仓返回结构 Map:code, msg, data |
| | | */ |
| | | Map<String, Object> syncMatnrsToCloud(Object body); |
| | | |
| | | /** |
| | | * 9.1 入/出库结果上报 |
| | | * @param param 上报参数 |
| | | * @return 云仓返回结构 Map:code, msg, data(data.result 为 SUCCESS/FAIL) |
| | | */ |
| | | Map<String, Object> reportInOutResult(InOutResultReportParam param); |
| | | |
| | | /** |
| | | * 9.2 库存调整主动上报(立库侧调用云仓通知) |
| | | * @param param 上报参数 |
| | | * @return 云仓返回结构 Map:code, msg, data(data.result 为 SUCCESS/FAIL) |
| | | */ |
| | | Map<String, Object> reportInventoryAdjust(InventoryAdjustReportParam param); |
| | | } |
| | |
| | | * @return |
| | | */ |
| | | R matUpdate(BaseMatParms baseMatParms); |
| | | |
| | | /** |
| | | * 对接协议 8.4 库存明细查询 |
| | | */ |
| | | R inventoryDetails(InventoryDetailsParam param); |
| | | |
| | | /** |
| | | * 对接协议 8.5 库存汇总查询 |
| | | */ |
| | | R inventorySummary(InventorySummaryParam param); |
| | | } |
| New file |
| | |
| | | package com.vincent.rsf.server.api.service.impl; |
| | | |
| | | import com.vincent.rsf.server.api.config.RemotesInfoProperties; |
| | | import com.vincent.rsf.server.api.controller.erp.params.InOutResultReportParam; |
| | | import com.vincent.rsf.server.api.controller.erp.params.InventoryAdjustReportParam; |
| | | import com.vincent.rsf.server.api.feign.CloudWmsErpFeignClient; |
| | | import com.vincent.rsf.server.api.service.CloudWmsReportService; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import org.springframework.beans.factory.annotation.Autowired; |
| | | import org.springframework.stereotype.Service; |
| | | |
| | | import java.util.HashMap; |
| | | import java.util.Map; |
| | | |
| | | /** |
| | | * 立库侧请求云仓:入/出库结果上报(9.1)、库存调整主动上报(9.2)、物料基础信息同步。 |
| | | * 使用 OpenFeign 调用;可选 HttpEntity(RestTemplate) 方式已注释保留。 |
| | | */ |
| | | @Slf4j |
| | | @Service |
| | | public class CloudWmsReportServiceImpl implements CloudWmsReportService { |
| | | |
| | | @Autowired |
| | | private RemotesInfoProperties erpApi; |
| | | |
| | | @Autowired |
| | | private RemotesInfoProperties.ApiInfo erpApiInfo; |
| | | |
| | | @Autowired |
| | | private CloudWmsErpFeignClient cloudWmsErpFeignClient; |
| | | |
| | | /** |
| | | * 可选:改用 HttpEntity(RestTemplate) 调用云仓时启用。 |
| | | */ |
| | | // @Autowired |
| | | // private RestTemplate restTemplate; |
| | | |
| | | @Override |
| | | public Map<String, Object> syncMatnrsToCloud(Object body) { |
| | | if (!isCloudWmsConfigured()) { |
| | | log.warn("ErpApi(云仓WMS) 未配置 host,跳过物料基础信息同步"); |
| | | return stubSuccess("云仓地址未配置,未实际同步"); |
| | | } |
| | | return cloudWmsErpFeignClient.syncMatnrs(body != null ? body : new HashMap<>()); |
| | | } |
| | | |
| | | @Override |
| | | public Map<String, Object> reportInOutResult(InOutResultReportParam param) { |
| | | if (param == null) { |
| | | return resultMap(400, "参数不能为空", null); |
| | | } |
| | | if (!isCloudWmsConfigured()) { |
| | | log.warn("ErpApi(云仓WMS) 未配置 host,跳过 9.1 入/出库结果上报,订单:{}", param.getOrderNo()); |
| | | return stubSuccess("云仓地址未配置,未实际上报"); |
| | | } |
| | | return cloudWmsErpFeignClient.reportInOutResult(param); |
| | | } |
| | | |
| | | @Override |
| | | public Map<String, Object> reportInventoryAdjust(InventoryAdjustReportParam param) { |
| | | if (param == null) { |
| | | return resultMap(400, "参数不能为空", null); |
| | | } |
| | | if (!isCloudWmsConfigured()) { |
| | | log.warn("ErpApi(云仓WMS) 未配置 host,跳过 9.2 库存调整上报,物料:{}", param.getMatNr()); |
| | | return stubSuccess("云仓地址未配置,未实际上报"); |
| | | } |
| | | return cloudWmsErpFeignClient.reportInventoryAdjust(param); |
| | | } |
| | | |
| | | private boolean isCloudWmsConfigured() { |
| | | String host = erpApi.getHost(); |
| | | return host != null && !host.trim().isEmpty(); |
| | | } |
| | | |
| | | private Map<String, Object> stubSuccess(String msg) { |
| | | Map<String, Object> data = new HashMap<>(); |
| | | data.put("result", "SUCCESS"); |
| | | return resultMap(200, msg, data); |
| | | } |
| | | |
| | | private Map<String, Object> resultMap(int code, String msg, Map<String, Object> data) { |
| | | Map<String, Object> map = new HashMap<>(); |
| | | map.put("code", code); |
| | | map.put("msg", msg); |
| | | map.put("data", data); |
| | | return map; |
| | | } |
| | | |
| | | // ========== 可选:HttpEntity(RestTemplate) 方式(当前未使用) ========== |
| | | // 启用步骤:1)取消上方 restTemplate 的 @Autowired 注入; |
| | | // 2)取消下面整段注释,恢复 buildUrl、postToCloudWms、parseResponse 方法及 OBJECT_MAPPER; |
| | | // 3)在 syncMatnrsToCloud/reportInOutResult/reportInventoryAdjust 中改为:String url = buildUrl(erpApiInfo.getXxxPath()); if (url == null) return stubSuccess(...); return postToCloudWms(url, body); |
| | | // |
| | | // private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); |
| | | // private String buildUrl(String path) { ... } |
| | | // private Map<String, Object> postToCloudWms(String url, Object body) { HttpHeaders headers = ...; HttpEntity<Object> entity = new HttpEntity<>(body, headers); ResponseEntity<String> response = restTemplate.exchange(url, HttpMethod.POST, entity, String.class); return parseResponse(response.getBody()); } |
| | | // private Map<String, Object> parseResponse(String json) { ... } |
| | | } |
| | |
| | | || !Objects.isNull(fieldIndex) || !Cools.isEmpty(matnrCode) || !Cools.isEmpty(asnCode); |
| | | |
| | | if (!hasValidCondition) { |
| | | throw new CoolException("请至少输入一个查询条件:物料编码、ASN单号、跟踪码、批次或票号"); |
| | | throw new CoolException("请至少输入一个查询条件:物料编码、WMS单号、批号");/*、跟踪码、批次或票号*/ |
| | | } |
| | | |
| | | // 如果扫描物料编码且ASN单号为空,直接从物料信息表获取,不查询收货区 |
| | |
| | | import com.baomidou.mybatisplus.core.metadata.IPage; |
| | | import com.baomidou.mybatisplus.extension.plugins.pagination.Page; |
| | | import com.fasterxml.jackson.databind.ObjectMapper; |
| | | 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.api.controller.erp.params.*; |
| | |
| | | private DictDataService dictDataService; |
| | | @Autowired |
| | | private DictTypeService dictTypeService; |
| | | @Autowired |
| | | private LocItemService locItemService; |
| | | |
| | | |
| | | /** |
| | |
| | | } |
| | | |
| | | /** |
| | | * 基础物料信息变更 |
| | | * @param baseMatParms |
| | | * @return |
| | | * 基础物料信息变更(对接协议 8.2) |
| | | * operateType:1新增 2修改 3禁用 4启用;不传或 1/2 时按有则更新、无则新增。 |
| | | */ |
| | | @Override |
| | | @Transactional(rollbackFor = Exception.class) |
| | |
| | | if (StringUtils.isBlank(baseMatParms.getMatnr())) { |
| | | throw new CoolException("物料编码不能为空!!"); |
| | | } |
| | | Integer operateType = baseMatParms.getOperateType(); |
| | | // 3 禁用 / 4 启用:仅更新状态(status 1 正常 0 冻结) |
| | | if (Integer.valueOf(3).equals(operateType) || Integer.valueOf(4).equals(operateType)) { |
| | | Matnr matnr = matnrService.getOne(new LambdaQueryWrapper<Matnr>().eq(Matnr::getCode, baseMatParms.getMatnr())); |
| | | if (matnr == null) { |
| | | throw new CoolException("物料不存在,无法执行禁用/启用!!"); |
| | | } |
| | | int status = Integer.valueOf(4).equals(operateType) ? 1 : 0; // 4 启用=1 正常,3 禁用=0 冻结 |
| | | matnr.setStatus(status); |
| | | if (!matnrService.updateById(matnr)) { |
| | | throw new CoolException(operateType == 4 ? "物料启用失败!!" : "物料禁用失败!!"); |
| | | } |
| | | return R.ok(); |
| | | } |
| | | // 1 新增 / 2 修改 / 不传:有则更新、无则新增 |
| | | Matnr matnr = matnrService.getOne(new LambdaQueryWrapper<Matnr>().eq(Matnr::getCode, baseMatParms.getMatnr())); |
| | | if (Objects.isNull(matnr)) { |
| | | Matnr matnr1 = new Matnr(); |
| | |
| | | |
| | | return R.ok(); |
| | | } |
| | | |
| | | @Override |
| | | public R inventoryDetails(InventoryDetailsParam param) { |
| | | LambdaQueryWrapper<LocItem> wrapper = new LambdaQueryWrapper<>(); |
| | | wrapper.eq(LocItem::getDeleted, 0); |
| | | if (!Cools.isEmpty(param.getLocId())) { |
| | | wrapper.eq(LocItem::getLocCode, param.getLocId()); |
| | | } |
| | | if (!Cools.isEmpty(param.getMatNr())) { |
| | | wrapper.eq(LocItem::getMatnrCode, param.getMatNr()); |
| | | } |
| | | if (!Cools.isEmpty(param.getBatch())) { |
| | | wrapper.eq(LocItem::getBatch, param.getBatch()); |
| | | } |
| | | if (!Cools.isEmpty(param.getOrderNo())) { |
| | | wrapper.and(w -> w.eq(LocItem::getPlatOrderCode, param.getOrderNo()).or().eq(LocItem::getPlatWorkCode, param.getOrderNo())); |
| | | } |
| | | if (!Cools.isEmpty(param.getPlanNo())) { |
| | | wrapper.eq(LocItem::getPlatWorkCode, param.getPlanNo()); |
| | | } |
| | | if (!Cools.isEmpty(param.getWareHouseId())) { |
| | | Warehouse wh = warehouseService.getOne(new LambdaQueryWrapper<Warehouse>().eq(Warehouse::getCode, param.getWareHouseId())); |
| | | if (wh != null) { |
| | | List<Loc> locs = locService.list(new LambdaQueryWrapper<Loc>().eq(Loc::getWarehouseId, wh.getId())); |
| | | if (!locs.isEmpty()) { |
| | | wrapper.in(LocItem::getLocId, locs.stream().map(Loc::getId).collect(Collectors.toList())); |
| | | } else { |
| | | return R.ok().add(Collections.emptyList()); |
| | | } |
| | | } else { |
| | | return R.ok().add(Collections.emptyList()); |
| | | } |
| | | } |
| | | List<LocItem> list = locItemService.list(wrapper); |
| | | List<Map<String, Object>> result = new ArrayList<>(); |
| | | for (LocItem item : list) { |
| | | Map<String, Object> row = new LinkedHashMap<>(); |
| | | row.put("locId", item.getLocCode()); |
| | | Loc loc = locService.getById(item.getLocId()); |
| | | if (loc != null && loc.getWarehouseId() != null) { |
| | | Warehouse w = warehouseService.getById(loc.getWarehouseId()); |
| | | row.put("wareHouseId", w != null ? w.getCode() : null); |
| | | row.put("wareHouseName", w != null ? w.getName() : null); |
| | | } else { |
| | | row.put("wareHouseId", null); |
| | | row.put("wareHouseName", null); |
| | | } |
| | | row.put("palletId", item.getTrackCode()); |
| | | row.put("matNr", item.getMatnrCode()); |
| | | row.put("makTx", item.getMaktx()); |
| | | row.put("anfme", item.getAnfme() != null ? item.getAnfme() : 0); |
| | | row.put("unit", item.getUnit()); |
| | | row.put("status", item.getStatus() != null ? item.getStatus() : 1); |
| | | row.put("orderType", item.getWkType()); |
| | | row.put("orderNo", item.getPlatOrderCode()); |
| | | row.put("planNo", item.getPlatWorkCode()); |
| | | row.put("batch", item.getBatch()); |
| | | result.add(row); |
| | | } |
| | | return R.ok().add(result); |
| | | } |
| | | |
| | | @Override |
| | | public R inventorySummary(InventorySummaryParam param) { |
| | | LambdaQueryWrapper<LocItem> wrapper = new LambdaQueryWrapper<>(); |
| | | wrapper.eq(LocItem::getDeleted, 0).select(LocItem::getLocId, LocItem::getMatnrCode, LocItem::getMaktx, LocItem::getAnfme, LocItem::getUnit); |
| | | if (!Cools.isEmpty(param.getWareHouseId())) { |
| | | Warehouse wh = warehouseService.getOne(new LambdaQueryWrapper<Warehouse>().eq(Warehouse::getCode, param.getWareHouseId())); |
| | | if (wh != null) { |
| | | List<Loc> locs = locService.list(new LambdaQueryWrapper<Loc>().eq(Loc::getWarehouseId, wh.getId())); |
| | | if (!locs.isEmpty()) { |
| | | wrapper.in(LocItem::getLocId, locs.stream().map(Loc::getId).collect(Collectors.toList())); |
| | | } else { |
| | | return R.ok().add(Collections.emptyList()); |
| | | } |
| | | } else { |
| | | return R.ok().add(Collections.emptyList()); |
| | | } |
| | | } |
| | | if (!Cools.isEmpty(param.getMatNr())) { |
| | | List<String> matNrs = Arrays.asList(param.getMatNr().split(",")); |
| | | wrapper.in(LocItem::getMatnrCode, matNrs.stream().map(String::trim).collect(Collectors.toList())); |
| | | } |
| | | List<LocItem> list = locItemService.list(wrapper); |
| | | Map<String, Map<String, Object>> sumMap = new LinkedHashMap<>(); |
| | | for (LocItem item : list) { |
| | | Loc loc = locService.getById(item.getLocId()); |
| | | String whId = null; |
| | | String whName = null; |
| | | if (loc != null && loc.getWarehouseId() != null) { |
| | | Warehouse w = warehouseService.getById(loc.getWarehouseId()); |
| | | whId = w != null ? w.getCode() : null; |
| | | whName = w != null ? w.getName() : null; |
| | | } |
| | | String key = (whId != null ? whId : "") + "|" + (item.getMatnrCode() != null ? item.getMatnrCode() : ""); |
| | | final String finalWhId = whId; |
| | | final String finalWhName = whName; |
| | | sumMap.compute(key, (k, v) -> { |
| | | if (v == null) { |
| | | v = new LinkedHashMap<>(); |
| | | v.put("wareHouseId", finalWhId); |
| | | v.put("wareHouseName", finalWhName); |
| | | v.put("matNr", item.getMatnrCode()); |
| | | v.put("matTx", item.getMaktx()); |
| | | v.put("anfme", (item.getAnfme() != null ? item.getAnfme() : 0)); |
| | | v.put("unit", item.getUnit()); |
| | | } else { |
| | | v.put("anfme", ((Number) v.get("anfme")).doubleValue() + (item.getAnfme() != null ? item.getAnfme() : 0)); |
| | | } |
| | | return v; |
| | | }); |
| | | } |
| | | return R.ok().add(new ArrayList<>(sumMap.values())); |
| | | } |
| | | } |
| New file |
| | |
| | | package com.vincent.rsf.server.manager.controller; |
| | | |
| | | import com.vincent.rsf.framework.common.R; |
| | | import com.vincent.rsf.server.manager.schedules.AsnOrderLogSchedule; |
| | | import io.swagger.annotations.Api; |
| | | import io.swagger.annotations.ApiOperation; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import org.springframework.http.HttpStatus; |
| | | import org.springframework.http.ResponseEntity; |
| | | import org.springframework.web.bind.annotation.PostMapping; |
| | | import org.springframework.web.bind.annotation.RequestMapping; |
| | | import org.springframework.web.bind.annotation.RestController; |
| | | |
| | | import javax.servlet.http.HttpServletRequest; |
| | | import java.util.Arrays; |
| | | import java.util.List; |
| | | |
| | | /** |
| | | * 定时任务手动触发接口,仅允许本机请求(127.0.0.1 / ::1)。 |
| | | */ |
| | | @Slf4j |
| | | @RestController |
| | | @RequestMapping("/schedule") |
| | | @Api(value = "定时任务触发", tags = "定时任务手动触发(仅本地)") |
| | | public class ScheduleTriggerController { |
| | | |
| | | private static final List<String> LOCALHOST_IPS = Arrays.asList("127.0.0.1", "0:0:0:0:0:0:0:1", "::1"); |
| | | |
| | | private final AsnOrderLogSchedule asnOrderLogSchedule; |
| | | |
| | | public ScheduleTriggerController(AsnOrderLogSchedule asnOrderLogSchedule) { |
| | | this.asnOrderLogSchedule = asnOrderLogSchedule; |
| | | } |
| | | |
| | | private static boolean isLocalRequest(HttpServletRequest request) { |
| | | String remote = request.getRemoteAddr(); |
| | | if (remote != null && LOCALHOST_IPS.contains(remote)) { |
| | | return true; |
| | | } |
| | | String forwarded = request.getHeader("X-Forwarded-For"); |
| | | if (forwarded != null && !forwarded.isEmpty()) { |
| | | remote = forwarded.split(",")[0].trim(); |
| | | } |
| | | return remote != null && LOCALHOST_IPS.contains(remote); |
| | | } |
| | | |
| | | @ApiOperation("手动执行入库转历史(InStockToLog),仅允许本地请求") |
| | | @PostMapping("/trigger/inStockToLog") |
| | | public ResponseEntity<R> triggerInStockToLog(HttpServletRequest request) { |
| | | if (!isLocalRequest(request)) { |
| | | log.warn("拒绝非本地请求触发 InStockToLog,remote={}", request.getRemoteAddr()); |
| | | return ResponseEntity.status(HttpStatus.FORBIDDEN).body(R.error("仅允许本地请求")); |
| | | } |
| | | try { |
| | | asnOrderLogSchedule.InStockToLog(); |
| | | return ResponseEntity.ok(R.ok("执行完成")); |
| | | } catch (Exception e) { |
| | | log.error("InStockToLog 执行失败", e); |
| | | return ResponseEntity.ok(R.error(e.getMessage())); |
| | | } |
| | | } |
| | | |
| | | @ApiOperation("手动执行物理删除上上个月之前已逻辑删除的原单及明细,仅允许本地请求") |
| | | @PostMapping("/trigger/physicalDeleteLogicDeletedOrders") |
| | | public ResponseEntity<R> triggerPhysicalDeleteLogicDeletedOrders(HttpServletRequest request) { |
| | | if (!isLocalRequest(request)) { |
| | | log.warn("拒绝非本地请求触发 physicalDeleteLogicDeletedOrders,remote={}", request.getRemoteAddr()); |
| | | return ResponseEntity.status(HttpStatus.FORBIDDEN).body(R.error("仅允许本地请求")); |
| | | } |
| | | try { |
| | | asnOrderLogSchedule.physicalDeleteLogicDeletedOrders(); |
| | | return ResponseEntity.ok(R.ok("执行完成")); |
| | | } catch (Exception e) { |
| | | log.error("physicalDeleteLogicDeletedOrders 执行失败", e); |
| | | return ResponseEntity.ok(R.error(e.getMessage())); |
| | | } |
| | | } |
| | | } |
| | |
| | | @ApiModelProperty(value= "秀点单ID") |
| | | private Long orderId; |
| | | |
| | | @ApiModelProperty(value= "ASN单号") |
| | | @ApiModelProperty(value= "WMS单号") |
| | | private String orderCode; |
| | | |
| | | @ApiModelProperty(value= "物料标识") |
| | |
| | | private Long asnId; |
| | | |
| | | /** |
| | | * ASN单号 |
| | | * WMS单号 |
| | | */ |
| | | @ApiModelProperty(value= "ASN单号") |
| | | @ApiModelProperty(value= "WMS单号") |
| | | private String asnCode; |
| | | |
| | | /** |
| New file |
| | |
| | | package com.vincent.rsf.server.manager.entity; |
| | | |
| | | 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 com.fasterxml.jackson.annotation.JsonFormat; |
| | | import io.swagger.annotations.ApiModel; |
| | | import io.swagger.annotations.ApiModelProperty; |
| | | import lombok.Data; |
| | | import lombok.experimental.Accessors; |
| | | import org.springframework.format.annotation.DateTimeFormat; |
| | | |
| | | import java.io.Serializable; |
| | | import java.util.Date; |
| | | |
| | | /** 云仓上报待办记录 */ |
| | | @Data |
| | | @Accessors(chain = true) |
| | | @TableName("man_cloud_wms_notify_log") |
| | | @ApiModel(value = "CloudWmsNotifyLog", description = "云仓上报待办记录") |
| | | public class CloudWmsNotifyLog implements Serializable { |
| | | |
| | | private static final long serialVersionUID = 1L; |
| | | |
| | | public static final String REPORT_TYPE_IN_OUT_RESULT = "IN_OUT_RESULT"; |
| | | public static final String REPORT_TYPE_INVENTORY_ADJUST = "INVENTORY_ADJUST"; |
| | | |
| | | /** 通知状态:待通知 */ |
| | | public static final int NOTIFY_STATUS_PENDING = 0; |
| | | /** 通知状态:已成功 */ |
| | | public static final int NOTIFY_STATUS_SUCCESS = 1; |
| | | /** 通知状态:失败(含超过重试次数) */ |
| | | public static final int NOTIFY_STATUS_FAIL = 2; |
| | | |
| | | @ApiModelProperty("主键") |
| | | @TableId(value = "id", type = IdType.AUTO) |
| | | private Long id; |
| | | |
| | | @ApiModelProperty("上报类型:IN_OUT_RESULT-入出库结果,INVENTORY_ADJUST-库存调整") |
| | | private String reportType; |
| | | |
| | | @ApiModelProperty("请求体 JSON") |
| | | private String requestBody; |
| | | |
| | | @ApiModelProperty("是否已通知到云仓:0 待通知 1 成功 2 失败") |
| | | private Integer notifyStatus; |
| | | |
| | | @ApiModelProperty("已通知次数(重试累计)") |
| | | private Integer retryCount; |
| | | |
| | | @ApiModelProperty("最大重试次数") |
| | | private Integer maxRetryCount; |
| | | |
| | | @ApiModelProperty("重试间隔秒数") |
| | | private Integer retryIntervalSeconds; |
| | | |
| | | @ApiModelProperty("最近一次请求体(重试时可能与原 requestBody 一致)") |
| | | private String lastRequestBody; |
| | | |
| | | @ApiModelProperty("最近一次返回结果 JSON") |
| | | private String lastResponseBody; |
| | | |
| | | @ApiModelProperty("最近一次请求时间") |
| | | @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") |
| | | @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") |
| | | private Date lastNotifyTime; |
| | | |
| | | @ApiModelProperty("业务关联(如 taskId、reviseLogId,便于排查)") |
| | | private String bizRef; |
| | | |
| | | @ApiModelProperty("租户") |
| | | private Integer tenantId; |
| | | |
| | | @ApiModelProperty("是否删除 0 否 1 是") |
| | | @TableLogic |
| | | private Integer deleted; |
| | | |
| | | @ApiModelProperty("创建时间") |
| | | @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") |
| | | @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") |
| | | private Date createTime; |
| | | |
| | | @ApiModelProperty("更新时间") |
| | | @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") |
| | | @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") |
| | | private Date updateTime; |
| | | } |
| | |
| | | private Double workQty; |
| | | |
| | | /** |
| | | * ASN单号 |
| | | * WMS单号 |
| | | */ |
| | | @ApiModelProperty(value= "ASN单号") |
| | | @ApiModelProperty(value= "WMS单号") |
| | | private String orderCode; |
| | | |
| | | /** |
| | |
| | | /** |
| | | * 编号 |
| | | */ |
| | | @Excel(name = "*ASN单号") |
| | | @ApiModelProperty(value = "*ASN单号") |
| | | @Excel(name = "*WMS单号") |
| | | @ApiModelProperty(value = "*WMS单号") |
| | | @ExcelComment(value = "code", example = "ASN5945272236") |
| | | private String code; |
| | | |
| | |
| | | /** |
| | | * 编号 |
| | | */ |
| | | @Excel(name = "*ASN单号") |
| | | @ApiModelProperty(value = "*ASN单号") |
| | | @Excel(name = "*WMS单号") |
| | | @ApiModelProperty(value = "*WMS单号") |
| | | @ExcelComment(value = "code", example = "ASN5945272236") |
| | | private String code; |
| | | |
| | |
| | | import org.apache.ibatis.annotations.Param; |
| | | import org.springframework.stereotype.Repository; |
| | | |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | |
| | | @Mapper |
| | |
| | | |
| | | WkOrderItem resultById(@Param(Constants.WRAPPER) LambdaQueryWrapper<WkOrderItem> buildWrapper); |
| | | |
| | | /** 按订单 id 物理删除已逻辑删除的明细 */ |
| | | int physicalDeleteByOrderIds(@Param("orderIds") List<Long> orderIds); |
| | | |
| | | } |
| | |
| | | import org.apache.ibatis.annotations.Param; |
| | | import org.springframework.stereotype.Repository; |
| | | |
| | | import java.util.Date; |
| | | import java.util.List; |
| | | |
| | | @Mapper |
| | |
| | | DashboardDto getDashbord(@Param("type") String type, @Param("taskType") String taskType); |
| | | |
| | | List<StockTransItemDto> getStockTrand(@Param(Constants.WRAPPER) LambdaQueryWrapper<StockStatistic> queryWrapper); |
| | | |
| | | /** 查询在指定时间之前被逻辑删除的订单 id(用于物理清理,补删历史) */ |
| | | List<Long> selectLogicDeletedOrderIdsBefore(@Param("before") Date before); |
| | | |
| | | /** 按 id 物理删除已逻辑删除的订单 */ |
| | | int physicalDeleteByIds(@Param("ids") List<Long> ids); |
| | | } |
| New file |
| | |
| | | package com.vincent.rsf.server.manager.mapper; |
| | | |
| | | import com.baomidou.mybatisplus.core.mapper.BaseMapper; |
| | | import com.vincent.rsf.server.manager.entity.CloudWmsNotifyLog; |
| | | import org.apache.ibatis.annotations.Mapper; |
| | | import org.springframework.stereotype.Repository; |
| | | |
| | | @Mapper |
| | | @Repository |
| | | public interface CloudWmsNotifyLogMapper extends BaseMapper<CloudWmsNotifyLog> { |
| | | } |
| | |
| | | import com.vincent.rsf.server.common.utils.FieldsUtils; |
| | | import com.vincent.rsf.server.manager.entity.*; |
| | | import com.vincent.rsf.server.manager.enums.*; |
| | | import com.vincent.rsf.server.manager.mapper.AsnOrderItemMapper; |
| | | import com.vincent.rsf.server.manager.mapper.AsnOrderMapper; |
| | | import com.vincent.rsf.server.manager.service.*; |
| | | import com.vincent.rsf.server.manager.service.impl.StockItemServiceImpl; |
| | | import com.vincent.rsf.server.manager.service.impl.StockServiceImpl; |
| | |
| | | import org.springframework.stereotype.Component; |
| | | import org.springframework.transaction.annotation.Transactional; |
| | | |
| | | import java.time.LocalDate; |
| | | import java.time.ZoneId; |
| | | import java.util.*; |
| | | import java.util.stream.Collectors; |
| | | |
| | |
| | | @Autowired |
| | | private ReportMsgService reportMsgService; |
| | | |
| | | @Autowired |
| | | private AsnOrderMapper asnOrderMapper; |
| | | @Autowired |
| | | private AsnOrderItemMapper asnOrderItemMapper; |
| | | |
| | | /** |
| | | * @param |
| | |
| | | * @description 删除已完成订单加入Log表 |
| | | * @time 2025/3/19 19:09 |
| | | */ |
| | | @Scheduled(cron = "0 0 2 1 * ?") |
| | | @Scheduled(cron = "0 0 5 * * ?") |
| | | @Transactional(rollbackFor = Exception.class) |
| | | public void InStockToLog() { |
| | | List<WkOrder> wkOrders = asnOrderService.list(new LambdaQueryWrapper<WkOrder>() |
| | |
| | | * @description 出库单完成后,状态修改 |
| | | * @time 2025/6/16 08:35 |
| | | */ |
| | | @Scheduled(cron = "0/15 * * * * ? ") |
| | | @Scheduled(cron = "0/25 * * * * ? ") |
| | | // @Scheduled(cron = "0 0 2 1 * ?") |
| | | @Transactional(rollbackFor = Exception.class) |
| | | public void outStockComplete() { |
| | |
| | | } |
| | | // if (order.getType().equals(OrderType.ORDER_OUT.type) && order.getReportOnce() >= 4) { |
| | | AsnOrderLog one = asnOrderLogService.getOne(new LambdaQueryWrapper<AsnOrderLog>().eq(AsnOrderLog::getCode, order.getCode()), false); |
| | | AsnOrderLog orderLog; |
| | | if (Objects.isNull(one)) { |
| | | AsnOrderLog orderLog = new AsnOrderLog(); |
| | | orderLog = new AsnOrderLog(); |
| | | if (type.equals(OrderType.ORDER_OUT.type)) { |
| | | order.setExceStatus(AsnExceStatus.ASN_EXCE_STATUS_TASK_DONE.val); |
| | | order.setQty(order.getWorkQty()); |
| | |
| | | BeanUtils.copyProperties(order, orderLog); |
| | | orderLog.setId(null); |
| | | orderLog.setAsnId(order.getId()); |
| | | |
| | | if (!asnOrderLogService.save(orderLog)) { |
| | | throw new CoolException("主单历史档添加失败!!"); |
| | | } |
| | | |
| | | List<AsnOrderItemLog> logs = new ArrayList<>(); |
| | | List<WkOrderItem> items = asnOrderItemService.list(new LambdaQueryWrapper<WkOrderItem>() |
| | | .eq(WkOrderItem::getOrderId, order.getId())); |
| | | items.forEach(item -> { |
| | | AsnOrderItemLog itemLog = new AsnOrderItemLog(); |
| | | BeanUtils.copyProperties(item, itemLog); |
| | | itemLog.setAsnItemId(itemLog.getId()) |
| | | .setId(null) |
| | | .setMatnrId(item.getMatnrId()) |
| | | .setLogId(orderLog.getId()) |
| | | .setAsnId(item.getOrderId()); |
| | | logs.add(itemLog); |
| | | }); |
| | | |
| | | if (!asnOrderItemLogService.saveBatch(logs)) { |
| | | throw new CoolException("单据明细历史档保存失败!!"); |
| | | } else { |
| | | if (type.equals(OrderType.ORDER_OUT.type)) { |
| | | order.setExceStatus(AsnExceStatus.ASN_EXCE_STATUS_TASK_DONE.val); |
| | | order.setQty(order.getWorkQty()); |
| | | } |
| | | long existingLogId = one.getId(); |
| | | BeanUtils.copyProperties(order, one); |
| | | one.setId(existingLogId); |
| | | one.setAsnId(order.getId()); |
| | | if (!asnOrderLogService.updateById(one)) { |
| | | throw new CoolException("主单历史档更新失败!!"); |
| | | } |
| | | orderLog = one; |
| | | asnOrderItemLogService.remove(new LambdaQueryWrapper<AsnOrderItemLog>().eq(AsnOrderItemLog::getLogId, existingLogId)); |
| | | } |
| | | |
| | | List<AsnOrderItemLog> logs = new ArrayList<>(); |
| | | List<WkOrderItem> items = asnOrderItemService.list(new LambdaQueryWrapper<WkOrderItem>() |
| | | .eq(WkOrderItem::getOrderId, order.getId())); |
| | | items.forEach(item -> { |
| | | AsnOrderItemLog itemLog = new AsnOrderItemLog(); |
| | | BeanUtils.copyProperties(item, itemLog); |
| | | itemLog.setAsnItemId(item.getId()) |
| | | .setId(null) |
| | | .setMatnrId(item.getMatnrId()) |
| | | .setLogId(orderLog.getId()) |
| | | .setAsnId(item.getOrderId()); |
| | | logs.add(itemLog); |
| | | }); |
| | | if (!asnOrderItemLogService.saveBatch(logs)) { |
| | | throw new CoolException("单据明细历史档保存失败!!"); |
| | | } |
| | | |
| | | //更新PO/DO单执行状态 |
| | | if (type.equals(OrderType.ORDER_IN.type)) { |
| | |
| | | .set(Transfer::getExceStatus, AsnExceStatus.ASN_EXCE_STATUS_TASK_DONE.val))) { |
| | | throw new CoolException("调拔单状态修改失败!!"); |
| | | } |
| | | return; |
| | | removeOriginalOrderAndItems(order); |
| | | continue; |
| | | } else { |
| | | if (!Objects.isNull(order.getPoId())) { |
| | | purchaseService.update(new LambdaUpdateWrapper<Purchase>() |
| | |
| | | throw new CoolException("单据状态更新失败!!"); |
| | | } |
| | | //如果为调拔单据保留 |
| | | return; |
| | | removeOriginalOrderAndItems(order); |
| | | continue; |
| | | } else { |
| | | if (!Objects.isNull(order.getPoId())) { |
| | | deliveryService.update(new LambdaUpdateWrapper<Delivery>() |
| | |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | // if (!asnOrderItemService.remove(new LambdaQueryWrapper<WkOrderItem>() |
| | | // .eq(WkOrderItem::getOrderId, order.getId()))) { |
| | | // throw new CoolException("原单据明细删除失败!!"); |
| | | // } |
| | | // if (!this.asnOrderService.removeById(order.getId())) { |
| | | // throw new CoolException("原单据删除失败!!"); |
| | | // } |
| | | // } |
| | | // 转入历史后删除原单及明细 |
| | | removeOriginalOrderAndItems(order); |
| | | } |
| | | } |
| | | |
| | | /** 删除原入库/出库通知单及明细(转入历史后调用) */ |
| | | private void removeOriginalOrderAndItems(WkOrder order) { |
| | | if (!asnOrderItemService.remove(new LambdaQueryWrapper<WkOrderItem>().eq(WkOrderItem::getOrderId, order.getId()))) { |
| | | throw new CoolException("原单据明细删除失败!!"); |
| | | } |
| | | if (!asnOrderService.removeById(order.getId())) { |
| | | throw new CoolException("原单据删除失败!!"); |
| | | } |
| | | } |
| | | |
| | | /** 每月1号凌晨执行:物理删除上上个月之前已被逻辑删除的入库/出库通知单及明细 */ |
| | | @Scheduled(cron = "0 0 0 1 * ?") |
| | | @Transactional(rollbackFor = Exception.class) |
| | | public void physicalDeleteLogicDeletedOrders() { |
| | | LocalDate startOfTwoMonthsAgo = LocalDate.now().minusMonths(2).withDayOfMonth(1); |
| | | Date before = Date.from(startOfTwoMonthsAgo.atStartOfDay(ZoneId.systemDefault()).toInstant()); |
| | | List<Long> ids = asnOrderMapper.selectLogicDeletedOrderIdsBefore(before); |
| | | if (ids == null || ids.isEmpty()) { |
| | | return; |
| | | } |
| | | final int batchSize = 500; |
| | | for (int i = 0; i < ids.size(); i += batchSize) { |
| | | int to = Math.min(i + batchSize, ids.size()); |
| | | List<Long> batch = ids.subList(i, to); |
| | | asnOrderItemMapper.physicalDeleteByOrderIds(batch); |
| | | asnOrderMapper.physicalDeleteByIds(batch); |
| | | } |
| | | log.info("物理删除上上个月之前已逻辑删除的原单及明细,订单数:{}", ids.size()); |
| | | } |
| | | } |
| New file |
| | |
| | | package com.vincent.rsf.server.manager.schedules; |
| | | |
| | | import com.fasterxml.jackson.core.JsonProcessingException; |
| | | import com.fasterxml.jackson.databind.ObjectMapper; |
| | | import com.vincent.rsf.server.api.controller.erp.params.InOutResultReportParam; |
| | | import com.vincent.rsf.server.api.controller.erp.params.InventoryAdjustReportParam; |
| | | import com.vincent.rsf.server.api.service.CloudWmsReportService; |
| | | import com.vincent.rsf.server.manager.entity.CloudWmsNotifyLog; |
| | | import com.vincent.rsf.server.manager.service.CloudWmsNotifyLogService; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import org.springframework.beans.factory.annotation.Autowired; |
| | | import org.springframework.scheduling.annotation.Scheduled; |
| | | import org.springframework.stereotype.Component; |
| | | |
| | | import java.util.Date; |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | |
| | | /** 云仓上报定时任务 */ |
| | | @Slf4j |
| | | @Component |
| | | public class CloudWmsNotifySchedule { |
| | | |
| | | private static final int BATCH_LIMIT = 50; |
| | | |
| | | @Autowired |
| | | private CloudWmsNotifyLogService cloudWmsNotifyLogService; |
| | | @Autowired |
| | | private CloudWmsReportService cloudWmsReportService; |
| | | @Autowired |
| | | private ObjectMapper objectMapper; |
| | | |
| | | @Scheduled(cron = "0/30 * * * * ?") |
| | | public void syncCloudWmsNotify() { |
| | | List<CloudWmsNotifyLog> pending = cloudWmsNotifyLogService.listPending(BATCH_LIMIT, 999); |
| | | if (pending.isEmpty()) { |
| | | return; |
| | | } |
| | | long nowMs = System.currentTimeMillis(); |
| | | for (CloudWmsNotifyLog logRecord : pending) { |
| | | try { |
| | | Integer maxRetry = logRecord.getMaxRetryCount(); |
| | | Integer intervalSeconds = logRecord.getRetryIntervalSeconds(); |
| | | if (maxRetry == null || intervalSeconds == null || intervalSeconds <= 0) { |
| | | continue; |
| | | } |
| | | if (logRecord.getRetryCount() != null && logRecord.getRetryCount() >= maxRetry) { |
| | | continue; |
| | | } |
| | | if (logRecord.getLastNotifyTime() != null) { |
| | | long elapsed = (nowMs - logRecord.getLastNotifyTime().getTime()) / 1000; |
| | | if (elapsed < intervalSeconds) { |
| | | continue; |
| | | } |
| | | } |
| | | processOne(logRecord); |
| | | } catch (Exception e) { |
| | | log.warn("云仓上报定时任务处理单条异常,id={},bizRef={}:{}", logRecord.getId(), logRecord.getBizRef(), e.getMessage()); |
| | | } |
| | | } |
| | | } |
| | | |
| | | private void processOne(CloudWmsNotifyLog logRecord) { |
| | | String reportType = logRecord.getReportType(); |
| | | String requestBody = logRecord.getRequestBody(); |
| | | Date now = new Date(); |
| | | int nextRetry = (logRecord.getRetryCount() == null ? 0 : logRecord.getRetryCount()) + 1; |
| | | int effectiveMaxRetry = logRecord.getMaxRetryCount(); |
| | | |
| | | try { |
| | | if (cloudWmsNotifyLogService.getReportTypeInOutResult().equals(reportType)) { |
| | | InOutResultReportParam param = objectMapper.readValue(requestBody, InOutResultReportParam.class); |
| | | Map<String, Object> res = cloudWmsReportService.reportInOutResult(param); |
| | | updateAfterNotify(logRecord, requestBody, res, nextRetry, now, effectiveMaxRetry); |
| | | } else if (cloudWmsNotifyLogService.getReportTypeInventoryAdjust().equals(reportType)) { |
| | | InventoryAdjustReportParam param = objectMapper.readValue(requestBody, InventoryAdjustReportParam.class); |
| | | Map<String, Object> res = cloudWmsReportService.reportInventoryAdjust(param); |
| | | updateAfterNotify(logRecord, requestBody, res, nextRetry, now, effectiveMaxRetry); |
| | | } else { |
| | | log.warn("未知上报类型,id={},reportType={}", logRecord.getId(), reportType); |
| | | return; |
| | | } |
| | | } catch (JsonProcessingException e) { |
| | | log.warn("云仓上报请求体反序列化失败,id={}:{}", logRecord.getId(), e.getMessage()); |
| | | setFailResult(logRecord, requestBody, "反序列化失败: " + e.getMessage(), nextRetry, now, effectiveMaxRetry); |
| | | } catch (Exception e) { |
| | | log.warn("云仓上报请求失败,id={},bizRef={}:{}", logRecord.getId(), logRecord.getBizRef(), e.getMessage()); |
| | | setFailResult(logRecord, requestBody, "请求异常: " + e.getMessage(), nextRetry, now, effectiveMaxRetry); |
| | | } |
| | | } |
| | | |
| | | private void updateAfterNotify(CloudWmsNotifyLog logRecord, String requestBody, Map<String, Object> res, int nextRetry, Date now, int effectiveMaxRetry) { |
| | | String responseJson; |
| | | try { |
| | | responseJson = res != null ? objectMapper.writeValueAsString(res) : "null"; |
| | | } catch (JsonProcessingException e) { |
| | | responseJson = String.valueOf(res); |
| | | } |
| | | Object codeObj = res != null ? res.get("code") : null; |
| | | boolean success = Integer.valueOf(200).equals(codeObj); |
| | | int status = success ? cloudWmsNotifyLogService.getNotifyStatusSuccess() : cloudWmsNotifyLogService.getNotifyStatusPending(); |
| | | if (!success && nextRetry >= effectiveMaxRetry) { |
| | | status = cloudWmsNotifyLogService.getNotifyStatusFail(); |
| | | } |
| | | logRecord.setLastRequestBody(requestBody); |
| | | logRecord.setLastResponseBody(responseJson); |
| | | logRecord.setLastNotifyTime(now); |
| | | logRecord.setRetryCount(nextRetry); |
| | | logRecord.setNotifyStatus(status); |
| | | logRecord.setUpdateTime(now); |
| | | cloudWmsNotifyLogService.updateById(logRecord); |
| | | } |
| | | |
| | | private void setFailResult(CloudWmsNotifyLog logRecord, String requestBody, String errorMsg, int nextRetry, Date now, int effectiveMaxRetry) { |
| | | logRecord.setLastRequestBody(requestBody); |
| | | logRecord.setLastResponseBody(errorMsg); |
| | | logRecord.setLastNotifyTime(now); |
| | | logRecord.setRetryCount(nextRetry); |
| | | logRecord.setNotifyStatus(nextRetry >= effectiveMaxRetry ? cloudWmsNotifyLogService.getNotifyStatusFail() : cloudWmsNotifyLogService.getNotifyStatusPending()); |
| | | logRecord.setUpdateTime(now); |
| | | cloudWmsNotifyLogService.updateById(logRecord); |
| | | } |
| | | } |
| | |
| | | if (!Boolean.parseBoolean(allowChang.getVal())) { |
| | | if (order.getAnfme().compareTo(order.getQty()) == 0) { |
| | | order.setExceStatus(AsnExceStatus.OUT_STOCK_STATUS_TASK_DONE.val); |
| | | if (order.getQty() == null || order.getQty().compareTo(0.0) == 0) { |
| | | order.setQty(order.getWorkQty() != null ? order.getWorkQty() : 0.0); |
| | | } |
| | | if (!asnOrderService.updateById(order)) { |
| | | logger.error("出库单更新状态失败。订单ID:{},订单编码:{}", order.getId(), order.getCode()); |
| | | } |
| | |
| | | } else { |
| | | if (order.getAnfme().compareTo(order.getQty()) <= 0) { |
| | | order.setExceStatus(AsnExceStatus.OUT_STOCK_STATUS_TASK_DONE.val); |
| | | if (order.getQty() == null || order.getQty().compareTo(0.0) == 0) { |
| | | order.setQty(order.getWorkQty() != null ? order.getWorkQty() : 0.0); |
| | | } |
| | | if (!asnOrderService.updateById(order)) { |
| | | logger.error("出库单更新状态失败。订单ID:{},订单编码:{}", order.getId(), order.getCode()); |
| | | } |
| New file |
| | |
| | | package com.vincent.rsf.server.manager.service; |
| | | |
| | | import com.baomidou.mybatisplus.extension.service.IService; |
| | | import com.vincent.rsf.server.manager.entity.CloudWmsNotifyLog; |
| | | |
| | | import java.util.List; |
| | | |
| | | /** 云仓上报待办 */ |
| | | public interface CloudWmsNotifyLogService extends IService<CloudWmsNotifyLog> { |
| | | |
| | | List<CloudWmsNotifyLog> listPending(int limit, int maxRetry); |
| | | |
| | | void fillFromConfig(CloudWmsNotifyLog log); |
| | | |
| | | /** 上报类型:入出库结果(系统配置优先,缺省 IN_OUT_RESULT) */ |
| | | String getReportTypeInOutResult(); |
| | | |
| | | /** 上报类型:库存调整(系统配置优先,缺省 INVENTORY_ADJUST) */ |
| | | String getReportTypeInventoryAdjust(); |
| | | |
| | | /** 通知状态:待通知(系统配置优先,缺省 0) */ |
| | | int getNotifyStatusPending(); |
| | | |
| | | /** 通知状态:已成功(系统配置优先,缺省 1) */ |
| | | int getNotifyStatusSuccess(); |
| | | |
| | | /** 通知状态:失败(系统配置优先,缺省 2) */ |
| | | int getNotifyStatusFail(); |
| | | } |
| | |
| | | // throw new CoolException("收货数量不能为零!!"); |
| | | // } |
| | | WkOrder order = this.getById(asrder.getId()); |
| | | AsnOrderLog orderLog = new AsnOrderLog(); |
| | | // order.setExceStatus(AsnExceStatus.ASN_EXCE_STATUS_TASK_DONE.val); |
| | | BeanUtils.copyProperties(order, orderLog); |
| | | orderLog.setId(null); |
| | | orderLog.setAsnId(order.getId()); |
| | | |
| | | // if (!this.saveOrUpdate(order)) { |
| | | // throw new CoolException("状态修改失败!!"); |
| | | // } |
| | | // orderLog.setExceStatus(AsnExceStatus.ASN_EXCE_STATUS_TASK_CLOSE.val); |
| | | if (!asnOrderLogService.save(orderLog)) { |
| | | throw new CoolException("主单历史档添加失败!!"); |
| | | AsnOrderLog one = asnOrderLogService.getOne(new LambdaQueryWrapper<AsnOrderLog>().eq(AsnOrderLog::getCode, order.getCode()), false); |
| | | AsnOrderLog orderLog; |
| | | if (Objects.isNull(one)) { |
| | | orderLog = new AsnOrderLog(); |
| | | BeanUtils.copyProperties(order, orderLog); |
| | | orderLog.setId(null); |
| | | orderLog.setAsnId(order.getId()); |
| | | if (!asnOrderLogService.save(orderLog)) { |
| | | throw new CoolException("主单历史档添加失败!!"); |
| | | } |
| | | } else { |
| | | long existingLogId = one.getId(); |
| | | BeanUtils.copyProperties(order, one); |
| | | one.setId(existingLogId); |
| | | one.setAsnId(order.getId()); |
| | | if (!asnOrderLogService.updateById(one)) { |
| | | throw new CoolException("主单历史档更新失败!!"); |
| | | } |
| | | orderLog = one; |
| | | asnOrderItemLogService.remove(new LambdaQueryWrapper<AsnOrderItemLog>().eq(AsnOrderItemLog::getLogId, existingLogId)); |
| | | } |
| | | List<AsnOrderItemLog> logs = new ArrayList<>(); |
| | | List<WkOrderItem> items = asnOrderItemService.list(new LambdaQueryWrapper<WkOrderItem>().eq(WkOrderItem::getOrderId, order.getId())); |
| | | items.forEach(item -> { |
| | | AsnOrderItemLog itemLog = new AsnOrderItemLog(); |
| | | BeanUtils.copyProperties(item, itemLog); |
| | | itemLog.setAsnItemId(itemLog.getId()) |
| | | itemLog.setAsnItemId(item.getId()) |
| | | .setId(null) |
| | | .setLogId(orderLog.getId()) |
| | | .setAsnId(item.getOrderId()); |
| | | logs.add(itemLog); |
| | | }); |
| | | |
| | | if (!asnOrderItemLogService.saveBatch(logs)) { |
| | | throw new CoolException("通知单明细历史档保存失败!!"); |
| | | } |
| New file |
| | |
| | | package com.vincent.rsf.server.manager.service.impl; |
| | | |
| | | import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; |
| | | import com.baomidou.mybatisplus.extension.plugins.pagination.Page; |
| | | import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; |
| | | import com.vincent.rsf.server.manager.entity.CloudWmsNotifyLog; |
| | | import com.vincent.rsf.server.manager.mapper.CloudWmsNotifyLogMapper; |
| | | import com.vincent.rsf.server.manager.service.CloudWmsNotifyLogService; |
| | | import com.vincent.rsf.server.system.constant.GlobalConfigCode; |
| | | import com.vincent.rsf.server.system.entity.Config; |
| | | import com.vincent.rsf.server.system.service.ConfigService; |
| | | import org.springframework.beans.factory.annotation.Autowired; |
| | | import org.springframework.stereotype.Service; |
| | | |
| | | import java.util.List; |
| | | |
| | | @Service |
| | | public class CloudWmsNotifyLogServiceImpl extends ServiceImpl<CloudWmsNotifyLogMapper, CloudWmsNotifyLog> implements CloudWmsNotifyLogService { |
| | | |
| | | @Autowired |
| | | private ConfigService configService; |
| | | |
| | | @Override |
| | | public List<CloudWmsNotifyLog> listPending(int limit, int maxRetry) { |
| | | Page<CloudWmsNotifyLog> page = new Page<>(1, Math.max(1, limit)); |
| | | LambdaQueryWrapper<CloudWmsNotifyLog> wrapper = new LambdaQueryWrapper<CloudWmsNotifyLog>() |
| | | .eq(CloudWmsNotifyLog::getNotifyStatus, getNotifyStatusPending()) |
| | | .lt(CloudWmsNotifyLog::getRetryCount, maxRetry) |
| | | .orderByAsc(CloudWmsNotifyLog::getId); |
| | | return page(page, wrapper).getRecords(); |
| | | } |
| | | |
| | | @Override |
| | | public String getReportTypeInOutResult() { |
| | | return getConfigString(GlobalConfigCode.CLOUD_WMS_REPORT_TYPE_IN_OUT_RESULT, CloudWmsNotifyLog.REPORT_TYPE_IN_OUT_RESULT); |
| | | } |
| | | |
| | | @Override |
| | | public String getReportTypeInventoryAdjust() { |
| | | return getConfigString(GlobalConfigCode.CLOUD_WMS_REPORT_TYPE_INVENTORY_ADJUST, CloudWmsNotifyLog.REPORT_TYPE_INVENTORY_ADJUST); |
| | | } |
| | | |
| | | @Override |
| | | public int getNotifyStatusPending() { |
| | | return getConfigInt(GlobalConfigCode.CLOUD_WMS_NOTIFY_STATUS_PENDING, CloudWmsNotifyLog.NOTIFY_STATUS_PENDING); |
| | | } |
| | | |
| | | @Override |
| | | public int getNotifyStatusSuccess() { |
| | | return getConfigInt(GlobalConfigCode.CLOUD_WMS_NOTIFY_STATUS_SUCCESS, CloudWmsNotifyLog.NOTIFY_STATUS_SUCCESS); |
| | | } |
| | | |
| | | @Override |
| | | public int getNotifyStatusFail() { |
| | | return getConfigInt(GlobalConfigCode.CLOUD_WMS_NOTIFY_STATUS_FAIL, CloudWmsNotifyLog.NOTIFY_STATUS_FAIL); |
| | | } |
| | | |
| | | private String getConfigString(String flag, String defaultVal) { |
| | | Config c = configService.getOne(new LambdaQueryWrapper<Config>().eq(Config::getFlag, flag).last("LIMIT 1")); |
| | | if (c != null && c.getVal() != null && !c.getVal().isEmpty()) { |
| | | return c.getVal().trim(); |
| | | } |
| | | return defaultVal; |
| | | } |
| | | |
| | | private int getConfigInt(String flag, int defaultVal) { |
| | | Integer v = getConfigInt(flag); |
| | | return v != null ? v : defaultVal; |
| | | } |
| | | |
| | | @Override |
| | | public void fillFromConfig(CloudWmsNotifyLog log) { |
| | | Integer maxRetry = getConfigInt(GlobalConfigCode.CLOUD_WMS_NOTIFY_MAX_RETRY); |
| | | Integer interval = getConfigInt(GlobalConfigCode.CLOUD_WMS_NOTIFY_RETRY_INTERVAL_SECONDS); |
| | | log.setMaxRetryCount(maxRetry); |
| | | log.setRetryIntervalSeconds(interval); |
| | | } |
| | | |
| | | /** 返回 null 表示未配置或解析失败 */ |
| | | private Integer getConfigInt(String flag) { |
| | | try { |
| | | Config c = configService.getOne(new LambdaQueryWrapper<Config>().eq(Config::getFlag, flag).last("LIMIT 1")); |
| | | if (c != null && c.getVal() != null && !c.getVal().isEmpty()) { |
| | | return Integer.parseInt(c.getVal().trim()); |
| | | } |
| | | } catch (Exception ignored) { |
| | | } |
| | | return null; |
| | | } |
| | | } |
| | |
| | | return R.error("出库单不存在!!"); |
| | | } |
| | | order.setExceStatus(AsnExceStatus.OUT_STOCK_STATUS_TASK_DONE.val); |
| | | if (order.getQty() == null || order.getQty().compareTo(0.0) == 0) { |
| | | order.setQty(order.getWorkQty() != null ? order.getWorkQty() : 0.0); |
| | | } |
| | | if (!this.updateById(order)) { |
| | | throw new CoolException("完成出库单失败!!"); |
| | | } |
| | |
| | | import com.baomidou.mybatisplus.core.toolkit.StringUtils; |
| | | import com.vincent.rsf.framework.common.R; |
| | | import com.vincent.rsf.framework.exception.CoolException; |
| | | import com.fasterxml.jackson.databind.ObjectMapper; |
| | | import com.vincent.rsf.server.api.controller.erp.params.InventoryAdjustReportParam; |
| | | import com.vincent.rsf.server.manager.controller.params.ReviseLogParams; |
| | | import com.vincent.rsf.server.manager.entity.*; |
| | | import com.vincent.rsf.server.manager.enums.AsnExceStatus; |
| | |
| | | import com.vincent.rsf.server.manager.mapper.ReviseLogMapper; |
| | | import com.vincent.rsf.server.manager.service.*; |
| | | import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import org.springframework.beans.BeanUtils; |
| | | import org.springframework.beans.factory.annotation.Autowired; |
| | | import org.springframework.stereotype.Service; |
| | |
| | | import java.util.Set; |
| | | import java.util.stream.Collectors; |
| | | |
| | | @Slf4j |
| | | @Service("reviseLogService") |
| | | public class ReviseLogServiceImpl extends ServiceImpl<ReviseLogMapper, ReviseLog> implements ReviseLogService { |
| | | |
| | |
| | | |
| | | @Autowired |
| | | private OutStockItemService outStockItemService; |
| | | |
| | | @Autowired |
| | | private WarehouseService warehouseService; |
| | | |
| | | @Autowired |
| | | private CloudWmsNotifyLogService cloudWmsNotifyLogService; |
| | | |
| | | @Autowired |
| | | private ObjectMapper objectMapper; |
| | | |
| | | /** |
| | | * 库存调整单明细添加 |
| | |
| | | // 删除原库位的库存明细(如果存在) |
| | | locItemService.remove(new LambdaQueryWrapper<LocItem>().eq(LocItem::getLocId, loc.getId())); |
| | | |
| | | final Loc sourceLoc = loc; |
| | | Loc finalLoc = loc; |
| | | reviseItems.forEach(logItem -> { |
| | | LocItem locDetl = new LocItem(); |
| | |
| | | if (!locItemService.save(locDetl)) { |
| | | throw new CoolException("库存明细保存失败!!"); |
| | | } |
| | | // 9.2 库存调整主动上报待办 |
| | | try { |
| | | String wareHouseId = null; |
| | | if (finalLoc.getWarehouseId() != null) { |
| | | Warehouse wh = warehouseService.getById(finalLoc.getWarehouseId()); |
| | | if (wh != null) { |
| | | wareHouseId = wh.getCode(); |
| | | } |
| | | } |
| | | if (wareHouseId != null && logItem.getMatnrCode() != null && sourceLoc != null) { |
| | | InventoryAdjustReportParam param = new InventoryAdjustReportParam() |
| | | .setChangeType(3) // 3 移库 |
| | | .setWareHouseId(wareHouseId) |
| | | .setSourceLocId(sourceLoc.getCode()) |
| | | .setTargetLocId(finalLoc.getCode()) |
| | | .setMatNr(logItem.getMatnrCode()) |
| | | .setQty(logItem.getReviseQty() != null ? String.valueOf(logItem.getReviseQty()) : "0"); |
| | | String requestBody = objectMapper.writeValueAsString(param); |
| | | Date now = new Date(); |
| | | CloudWmsNotifyLog notifyLog = new CloudWmsNotifyLog() |
| | | .setReportType(cloudWmsNotifyLogService.getReportTypeInventoryAdjust()) |
| | | .setRequestBody(requestBody) |
| | | .setNotifyStatus(cloudWmsNotifyLogService.getNotifyStatusPending()) |
| | | .setRetryCount(0) |
| | | .setBizRef("reviseId=" + revise.getId() + ",reviseLogItemId=" + logItem.getId()) |
| | | .setCreateTime(now) |
| | | .setUpdateTime(now); |
| | | cloudWmsNotifyLogService.fillFromConfig(notifyLog); |
| | | cloudWmsNotifyLogService.save(notifyLog); |
| | | } |
| | | } catch (Exception e) { |
| | | log.warn("库存调整上报待办落库失败(不影响库存保存),matNr={}:{}", logItem.getMatnrCode(), e.getMessage()); |
| | | } |
| | | |
| | | // 为库存调整产生的库存创建对应的WkOrderItem |
| | | // 遍历所有未完成的出库单,检查是否需要这些物料 |
| | |
| | | import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; |
| | | import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; |
| | | import com.fasterxml.jackson.core.JsonProcessingException; |
| | | import com.fasterxml.jackson.core.JsonProcessingException; |
| | | import com.fasterxml.jackson.databind.ObjectMapper; |
| | | import com.fasterxml.jackson.databind.cfg.CoercionAction; |
| | | import com.fasterxml.jackson.databind.cfg.CoercionInputShape; |
| | | import com.vincent.rsf.framework.common.Cools; |
| | | import com.vincent.rsf.framework.common.DateUtils; |
| | | import com.vincent.rsf.server.api.config.RemotesInfoProperties; |
| | | import com.vincent.rsf.server.api.controller.erp.params.InOutResultReportParam; |
| | | import com.vincent.rsf.server.api.controller.erp.params.TaskInParam; |
| | | import com.vincent.rsf.server.api.entity.CommonResponse; |
| | | import com.vincent.rsf.server.api.entity.constant.RcsConstant; |
| | |
| | | import com.vincent.rsf.framework.exception.CoolException; |
| | | import com.vincent.rsf.server.api.utils.LocUtils; |
| | | import com.vincent.rsf.server.manager.controller.params.GenerateTaskParams; |
| | | import com.vincent.rsf.server.manager.entity.CloudWmsNotifyLog; |
| | | import com.vincent.rsf.server.manager.entity.*; |
| | | import com.vincent.rsf.server.manager.mapper.TaskMapper; |
| | | import com.vincent.rsf.server.manager.service.*; |
| | |
| | | private RestTemplate restTemplate; |
| | | @Autowired |
| | | private RemotesInfoProperties.RcsApi rcsApi; |
| | | @Autowired |
| | | private CloudWmsNotifyLogService cloudWmsNotifyLogService; |
| | | @Autowired |
| | | private WarehouseService warehouseService; |
| | | |
| | | @Override |
| | | @Transactional(rollbackFor = Exception.class) |
| | |
| | | .set(Task::getTaskStatus, TaskStsType.WAVE_SEED.id))) { |
| | | throw new CoolException("库存状态更新失败!!"); |
| | | } |
| | | // 9.1 入/出库结果上报:出库完成后通知云仓 |
| | | reportInOutResultToCloud(task, loc, taskItems, null, false); |
| | | |
| | | // if (task.getTaskType().equals(TaskType.TASK_TYPE_PICK_AGAIN_OUT.type) || task.getTaskType().equals(TaskType.TASK_TYPE_CHECK_OUT.type)) { |
| | | // if (!this.update(new LambdaUpdateWrapper<Task>() |
| | |
| | | if (!this.update(new LambdaUpdateWrapper<Task>().eq(Task::getId, task.getId()).set(Task::getTaskStatus, TaskStsType.UPDATED_IN.id))) { |
| | | throw new CoolException("任务状态修改失败!!"); |
| | | } |
| | | // 9.1 入/出库结果上报:入库完成后通知云仓 |
| | | reportInOutResultToCloud(task, loc, taskItems, pkinItemIds, true); |
| | | } |
| | | |
| | | /** |
| | |
| | | } |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 9.1 入/出库结果上报待办 |
| | | * @param isInbound true 入库完成,false 出库完成 |
| | | * @param pkinItemIds 入库时组托明细 ID 集合,用于查 asnCode 作为 orderNo;出库时传 null,用 taskItem.platOrderCode |
| | | */ |
| | | private void reportInOutResultToCloud(Task task, Loc loc, List<TaskItem> taskItems, Set<Long> pkinItemIds, boolean isInbound) { |
| | | try { |
| | | String locId = isInbound ? task.getTargLoc() : task.getOrgLoc(); |
| | | String wareHouseId = null; |
| | | if (loc.getWarehouseId() != null) { |
| | | Warehouse wh = warehouseService.getById(loc.getWarehouseId()); |
| | | if (wh != null) { |
| | | wareHouseId = wh.getCode(); |
| | | } |
| | | } |
| | | if (wareHouseId == null) { |
| | | log.warn("入/出库结果上报待办跳过:仓库编码为空,taskId={}", task.getId()); |
| | | return; |
| | | } |
| | | Map<Long, String> sourceToOrderNo = new HashMap<>(); |
| | | if (isInbound && pkinItemIds != null && !pkinItemIds.isEmpty()) { |
| | | List<WaitPakinItem> pakinItems = waitPakinItemService.list(new LambdaQueryWrapper<WaitPakinItem>().in(WaitPakinItem::getId, pkinItemIds)); |
| | | for (WaitPakinItem p : pakinItems) { |
| | | if (p.getAsnCode() != null) { |
| | | sourceToOrderNo.put(p.getId(), p.getAsnCode()); |
| | | } |
| | | } |
| | | } |
| | | ObjectMapper om = new ObjectMapper(); |
| | | Date now = new Date(); |
| | | for (TaskItem item : taskItems) { |
| | | String orderNo = isInbound ? sourceToOrderNo.get(item.getSource()) : (item.getPlatOrderCode() != null ? item.getPlatOrderCode() : item.getPlatWorkCode()); |
| | | if (orderNo == null || item.getMatnrCode() == null) { |
| | | continue; |
| | | } |
| | | InOutResultReportParam param = new InOutResultReportParam() |
| | | .setOrderNo(orderNo) |
| | | .setPlanNo(item.getPlatWorkCode()) |
| | | .setLineId(item.getPlatItemId()) |
| | | .setWareHouseId(wareHouseId) |
| | | .setLocId(locId) |
| | | .setMatNr(item.getMatnrCode()) |
| | | .setQty(item.getAnfme() != null ? String.valueOf(item.getAnfme()) : "0") |
| | | .setBatch(item.getBatch()); |
| | | try { |
| | | String requestBody = om.writeValueAsString(param); |
| | | CloudWmsNotifyLog notifyLog = new CloudWmsNotifyLog() |
| | | .setReportType(cloudWmsNotifyLogService.getReportTypeInOutResult()) |
| | | .setRequestBody(requestBody) |
| | | .setNotifyStatus(cloudWmsNotifyLogService.getNotifyStatusPending()) |
| | | .setRetryCount(0) |
| | | .setBizRef("taskId=" + task.getId() + ",orderNo=" + orderNo) |
| | | .setCreateTime(now) |
| | | .setUpdateTime(now); |
| | | cloudWmsNotifyLogService.fillFromConfig(notifyLog); |
| | | cloudWmsNotifyLogService.save(notifyLog); |
| | | } catch (JsonProcessingException e) { |
| | | log.warn("入/出库结果上报待办落库失败(不影响主流程),taskId={},orderNo={}:{}", task.getId(), orderNo, e.getMessage()); |
| | | } |
| | | } |
| | | } catch (Exception e) { |
| | | log.warn("入/出库结果上报待办失败,taskId={},isInbound={}:{}", task.getId(), isInbound, e.getMessage()); |
| | | } |
| | | } |
| | | } |
| | |
| | | |
| | | public final static String ALLOW_PUB_TASK = "AllowPubTask"; |
| | | |
| | | /** 云仓上报最大重试次数 */ |
| | | public final static String CLOUD_WMS_NOTIFY_MAX_RETRY = "CLOUD_WMS_NOTIFY_MAX_RETRY"; |
| | | /** 云仓上报重试间隔秒数 */ |
| | | public final static String CLOUD_WMS_NOTIFY_RETRY_INTERVAL_SECONDS = "CLOUD_WMS_NOTIFY_RETRY_INTERVAL_SECONDS"; |
| | | /** 云仓上报类型:入出库结果 */ |
| | | public final static String CLOUD_WMS_REPORT_TYPE_IN_OUT_RESULT = "CLOUD_WMS_REPORT_TYPE_IN_OUT_RESULT"; |
| | | /** 云仓上报类型:库存调整 */ |
| | | public final static String CLOUD_WMS_REPORT_TYPE_INVENTORY_ADJUST = "CLOUD_WMS_REPORT_TYPE_INVENTORY_ADJUST"; |
| | | /** 云仓通知状态:待通知 */ |
| | | public final static String CLOUD_WMS_NOTIFY_STATUS_PENDING = "CLOUD_WMS_NOTIFY_STATUS_PENDING"; |
| | | /** 云仓通知状态:已成功 */ |
| | | public final static String CLOUD_WMS_NOTIFY_STATUS_SUCCESS = "CLOUD_WMS_NOTIFY_STATUS_SUCCESS"; |
| | | /** 云仓通知状态:失败 */ |
| | | public final static String CLOUD_WMS_NOTIFY_STATUS_FAIL = "CLOUD_WMS_NOTIFY_STATUS_FAIL"; |
| | | |
| | | } |
| New file |
| | |
| | | package com.vincent.rsf.server.system.controller; |
| | | |
| | | import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; |
| | | import com.baomidou.mybatisplus.extension.plugins.pagination.Page; |
| | | import com.vincent.rsf.framework.common.R; |
| | | import com.vincent.rsf.server.common.domain.BaseParam; |
| | | import com.vincent.rsf.server.common.domain.PageParam; |
| | | import com.vincent.rsf.server.system.entity.OpenApiApp; |
| | | import com.vincent.rsf.server.system.service.OpenApiAppService; |
| | | import org.springframework.beans.factory.annotation.Autowired; |
| | | import org.springframework.security.access.prepost.PreAuthorize; |
| | | import org.springframework.web.bind.annotation.*; |
| | | |
| | | import java.util.Arrays; |
| | | import java.util.Map; |
| | | |
| | | @RestController |
| | | public class OpenApiAppController extends BaseController { |
| | | |
| | | @Autowired |
| | | private OpenApiAppService openApiAppService; |
| | | |
| | | @PreAuthorize("hasAuthority('system:openApiApp:list')") |
| | | @PostMapping("/openApiApp/page") |
| | | public R page(@RequestBody Map<String, Object> map) { |
| | | BaseParam baseParam = buildParam(map, BaseParam.class); |
| | | PageParam<OpenApiApp, BaseParam> pageParam = new PageParam<>(baseParam, OpenApiApp.class); |
| | | LambdaQueryWrapper<OpenApiApp> wrapper = new LambdaQueryWrapper<>(); |
| | | wrapper.orderByDesc(OpenApiApp::getId); |
| | | Page<OpenApiApp> page = openApiAppService.page(pageParam, wrapper); |
| | | return R.ok().add(page); |
| | | } |
| | | |
| | | @PreAuthorize("hasAuthority('system:openApiApp:list')") |
| | | @PostMapping("/openApiApp/list") |
| | | public R list(@RequestBody Map<String, Object> map) { |
| | | return R.ok().add(openApiAppService.list(new LambdaQueryWrapper<OpenApiApp>().orderByDesc(OpenApiApp::getId))); |
| | | } |
| | | |
| | | @PreAuthorize("hasAuthority('system:openApiApp:list')") |
| | | @PostMapping("/openApiApp/many/{ids}") |
| | | public R many(@PathVariable String[] ids) { |
| | | return R.ok().add(openApiAppService.listByIds(Arrays.asList(ids))); |
| | | } |
| | | |
| | | @PreAuthorize("hasAuthority('system:openApiApp:list')") |
| | | @GetMapping("/openApiApp/{id}") |
| | | public R get(@PathVariable String id) { |
| | | return R.ok().add(openApiAppService.getById(id)); |
| | | } |
| | | |
| | | @PreAuthorize("hasAuthority('system:openApiApp:save')") |
| | | @PostMapping("/openApiApp/save") |
| | | public R save(@RequestBody OpenApiApp app) { |
| | | if (openApiAppService.save(app)) { |
| | | return R.ok("Save Success").add(app); |
| | | } |
| | | return R.error("Save Fail"); |
| | | } |
| | | |
| | | @PreAuthorize("hasAuthority('system:openApiApp:update')") |
| | | @PostMapping("/openApiApp/update") |
| | | public R update(@RequestBody OpenApiApp app) { |
| | | if (openApiAppService.updateById(app)) { |
| | | return R.ok("Update Success").add(app); |
| | | } |
| | | return R.error("Update Fail"); |
| | | } |
| | | |
| | | @PreAuthorize("hasAuthority('system:openApiApp:remove')") |
| | | @PostMapping("/openApiApp/remove/{ids}") |
| | | public R remove(@PathVariable String[] ids) { |
| | | if (openApiAppService.removeByIds(Arrays.asList(ids))) { |
| | | return R.ok("Remove Success"); |
| | | } |
| | | return R.error("Remove Fail"); |
| | | } |
| | | } |
| New file |
| | |
| | | package com.vincent.rsf.server.system.entity; |
| | | |
| | | import com.baomidou.mybatisplus.annotation.TableId; |
| | | import com.baomidou.mybatisplus.annotation.TableName; |
| | | import io.swagger.annotations.ApiModelProperty; |
| | | import lombok.Data; |
| | | |
| | | import java.io.Serializable; |
| | | |
| | | @Data |
| | | @TableName("open_api_app") |
| | | public class OpenApiApp implements Serializable { |
| | | |
| | | private static final long serialVersionUID = 1L; |
| | | |
| | | @ApiModelProperty("应用ID") |
| | | @TableId(value = "id") |
| | | private String id; |
| | | |
| | | @ApiModelProperty("应用密钥") |
| | | private String screct; |
| | | |
| | | @ApiModelProperty("应用名称") |
| | | private String name; |
| | | |
| | | @ApiModelProperty("应用URL") |
| | | private String url; |
| | | |
| | | @ApiModelProperty("是否启用 0未启用 1启用") |
| | | private Integer enable; |
| | | |
| | | @ApiModelProperty("租户id") |
| | | private Long tenantId; |
| | | } |
| New file |
| | |
| | | package com.vincent.rsf.server.system.mapper; |
| | | |
| | | import com.baomidou.mybatisplus.core.mapper.BaseMapper; |
| | | import com.vincent.rsf.server.system.entity.OpenApiApp; |
| | | import org.apache.ibatis.annotations.Mapper; |
| | | import org.springframework.stereotype.Repository; |
| | | |
| | | @Mapper |
| | | @Repository |
| | | public interface OpenApiAppMapper extends BaseMapper<OpenApiApp> { |
| | | } |
| New file |
| | |
| | | package com.vincent.rsf.server.system.service; |
| | | |
| | | import com.baomidou.mybatisplus.extension.service.IService; |
| | | import com.vincent.rsf.server.system.entity.OpenApiApp; |
| | | |
| | | public interface OpenApiAppService extends IService<OpenApiApp> { |
| | | } |
| New file |
| | |
| | | package com.vincent.rsf.server.system.service.impl; |
| | | |
| | | import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; |
| | | import com.vincent.rsf.server.system.entity.OpenApiApp; |
| | | import com.vincent.rsf.server.system.mapper.OpenApiAppMapper; |
| | | import com.vincent.rsf.server.system.service.OpenApiAppService; |
| | | import org.springframework.security.crypto.password.PasswordEncoder; |
| | | import org.springframework.stereotype.Service; |
| | | |
| | | import javax.annotation.Resource; |
| | | |
| | | @Service |
| | | public class OpenApiAppServiceImpl extends ServiceImpl<OpenApiAppMapper, OpenApiApp> implements OpenApiAppService { |
| | | |
| | | @Resource |
| | | private PasswordEncoder passwordEncoder; |
| | | |
| | | @Override |
| | | public boolean save(OpenApiApp entity) { |
| | | encodeScrectIfPlain(entity); |
| | | return super.save(entity); |
| | | } |
| | | |
| | | @Override |
| | | public boolean updateById(OpenApiApp entity) { |
| | | encodeScrectIfPlain(entity); |
| | | return super.updateById(entity); |
| | | } |
| | | |
| | | /** 若为明文则改为 BCrypt 再存库,便于 getToken 时用 BCrypt 校验 */ |
| | | private void encodeScrectIfPlain(OpenApiApp entity) { |
| | | if (entity == null) { |
| | | return; |
| | | } |
| | | String s = entity.getScrect(); |
| | | if (s == null || s.isEmpty() || s.startsWith("$2")) { |
| | | return; |
| | | } |
| | | entity.setScrect(passwordEncoder.encode(s)); |
| | | } |
| | | } |
| | |
| | | spring: |
| | | application: |
| | | name: @pom.artifactId@ |
| | | cloud: |
| | | openfeign: |
| | | circuitbreaker: |
| | | enabled: true # Feign 调用失败时走 Fallback,在 Feign 内统一返回错误响应 |
| | | mvc: |
| | | static-path-pattern: /** |
| | | path match: |
| | |
| | | driver-class-name: com.mysql.cj.jdbc.Driver |
| | | username: root |
| | | url: jdbc:mysql://127.0.0.1:3306/rsf_jdxaj?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai |
| | | # url: jdbc:mysql://127.0.0.1:3306/jdxajwms?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai |
| | | password: 12345 |
| | | type: com.alibaba.druid.pool.DruidDataSource |
| | | druid: |
| | |
| | | #端口号 |
| | | port: 8080 |
| | | #接品链接前缀 |
| | | pre-path: rsf-server |
| | | #接口明细 |
| | | pre-path: "" |
| | | # Feign 调用云仓时的根地址。云仓未提供 URL 时可用本机模拟:http://127.0.0.1:8086/rsf-server(本服务提供的 CloudWmsMockController) |
| | | base-url: http://127.0.0.1:8086/rsf-server |
| | | #接口明细(立库侧请求云仓时使用的路径) |
| | | api: |
| | | #质检上报接口 |
| | | notify-inspect: /report/inspect |
| | | in-out-result-path: /api/report/inOutResult |
| | | inventory-adjust-path: /api/report/inventoryAdjust |
| | | mat-sync-path: /api/mat/sync |
| | | rcs: |
| | | #链接 |
| | | host: http://10.10.10.200 |
| | | #端口 |
| | | port: 8088 |
| | | |
| | | #仓库功能参数配置 |
| | | stock: |
| | | #是否允许打印货物标签, 默认允许打印,也可由供应商提供标签 |
| | |
| | | ) t |
| | | ${ew.customSqlSegment} |
| | | </select> |
| | | |
| | | <delete id="physicalDeleteByOrderIds"> |
| | | DELETE FROM man_asn_order_item |
| | | WHERE deleted = 1 |
| | | AND order_id IN |
| | | <foreach collection="orderIds" item="id" open="(" separator="," close=")">#{id}</foreach> |
| | | </delete> |
| | | </mapper> |
| | |
| | | GROUP BY |
| | | `day_time`, task_type |
| | | ) t |
| | | ${ew.customSqlSegment} |
| | | ${ew.customSqlSegment} |
| | | </select> |
| | | |
| | | <select id="selectLogicDeletedOrderIdsBefore" resultType="long"> |
| | | SELECT id FROM man_asn_order |
| | | WHERE deleted = 1 |
| | | AND update_time < #{before} |
| | | </select> |
| | | |
| | | <delete id="physicalDeleteByIds"> |
| | | DELETE FROM man_asn_order |
| | | WHERE deleted = 1 |
| | | AND id IN |
| | | <foreach collection="ids" item="id" open="(" separator="," close=")">#{id}</foreach> |
| | | </delete> |
| | | </mapper> |
| New file |
| | |
| | | package com.vincent.rsf.server.common.security; |
| | | |
| | | import org.junit.jupiter.api.Test; |
| | | import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; |
| | | |
| | | /** |
| | | * 字符串加密与验证。 |
| | | * |
| | | */ |
| | | class SecurityDemoControllerTest { |
| | | |
| | | @Test |
| | | void encryptAndVerify() { |
| | | BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(); |
| | | String raw = "wms001"; |
| | | |
| | | // 加密 |
| | | String encoded = encoder.encode(raw); |
| | | System.out.println("密文: " + encoded); |
| | | |
| | | // 验证:原文与加密结果匹配 |
| | | boolean ok = encoder.matches(raw, encoded); |
| | | System.out.println("加密后验证: " + ok); |
| | | |
| | | // 验证:错误原文不匹配 |
| | | boolean bad = encoder.matches("wrong", encoded); |
| | | System.out.println("错误原文验证: " + bad); |
| | | } |
| | | } |
| New file |
| | |
| | | INSERT INTO `sys_config` (`uuid`, `name`, `flag`, `type`, `val`, `content`, `status`, `deleted`, `tenant_id`, `create_by`, `create_time`, `update_by`, `update_time`, `memo`) |
| | | VALUES |
| | | (UPPER(UUID()), '云仓上报类型-入出库结果', 'CLOUD_WMS_REPORT_TYPE_IN_OUT_RESULT', 3, 'IN_OUT_RESULT', '入出库结果上报类型标识', 1, 0, 1, NULL, NOW(), NULL, NOW(), '与 man_cloud_wms_notify_log.report_type 对应'), |
| | | (UPPER(UUID()), '云仓上报类型-库存调整', 'CLOUD_WMS_REPORT_TYPE_INVENTORY_ADJUST', 3, 'INVENTORY_ADJUST', '库存调整上报类型标识', 1, 0, 1, NULL, NOW(), NULL, NOW(), '与 man_cloud_wms_notify_log.report_type 对应'), |
| | | (UPPER(UUID()), '云仓通知状态-待通知', 'CLOUD_WMS_NOTIFY_STATUS_PENDING', 2, '0', '待通知', 1, 0, 1, NULL, NOW(), NULL, NOW(), '与 man_cloud_wms_notify_log.notify_status 对应'), |
| | | (UPPER(UUID()), '云仓通知状态-已成功', 'CLOUD_WMS_NOTIFY_STATUS_SUCCESS', 2, '1', '已成功', 1, 0, 1, NULL, NOW(), NULL, NOW(), '与 man_cloud_wms_notify_log.notify_status 对应'), |
| | | (UPPER(UUID()), '云仓通知状态-失败', 'CLOUD_WMS_NOTIFY_STATUS_FAIL', 2, '2', '失败(含超过重试次数)', 1, 0, 1, NULL, NOW(), NULL, NOW(), '与 man_cloud_wms_notify_log.notify_status 对应'); |
| New file |
| | |
| | | -- 云仓上报待办表:业务事务内只落库,由定时任务异步请求云仓并更新通知结果 |
| | | -- 9.1 入出库结果上报、9.2 库存调整主动上报 |
| | | SET NAMES utf8mb4; |
| | | |
| | | DROP TABLE IF EXISTS `man_cloud_wms_notify_log`; |
| | | CREATE TABLE `man_cloud_wms_notify_log` ( |
| | | `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键', |
| | | `report_type` varchar(32) NOT NULL COMMENT '上报类型:IN_OUT_RESULT-入出库结果,INVENTORY_ADJUST-库存调整', |
| | | `request_body` text COMMENT '请求体JSON(与协议一致,供定时任务重放)', |
| | | `notify_status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '是否已通知到云仓:0待通知 1成功 2失败', |
| | | `retry_count` int(11) NOT NULL DEFAULT '0' COMMENT '已通知次数(重试累计)', |
| | | `max_retry_count` int(11) DEFAULT NULL COMMENT '最大重试次数(为空则用系统配置)', |
| | | `retry_interval_seconds` int(11) DEFAULT NULL COMMENT '重试频率/间隔秒数(为空则用系统配置)', |
| | | `last_request_body` text COMMENT '最近一次请求体', |
| | | `last_response_body` text COMMENT '最近一次返回结果JSON', |
| | | `last_notify_time` datetime DEFAULT NULL COMMENT '最近一次请求时间', |
| | | `biz_ref` varchar(255) DEFAULT NULL COMMENT '业务关联(如taskId、reviseLogId)', |
| | | `tenant_id` int(11) DEFAULT NULL COMMENT '租户', |
| | | `deleted` tinyint(4) NOT NULL DEFAULT '0' COMMENT '是否删除 0否 1是', |
| | | `create_time` datetime DEFAULT NULL COMMENT '创建时间', |
| | | `update_time` datetime DEFAULT NULL COMMENT '更新时间', |
| | | PRIMARY KEY (`id`), |
| | | KEY `idx_notify_status_retry` (`notify_status`, `retry_count`), |
| | | KEY `idx_create_time` (`create_time`) |
| | | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='云仓上报待办记录'; |
| New file |
| | |
| | | -- 8.1 Token 鉴权:应用表,getToken 时用 appId+appSecret 在此表校验 |
| | | SET NAMES utf8mb4; |
| | | |
| | | DROP TABLE IF EXISTS `open_api_app`; |
| | | CREATE TABLE `open_api_app` ( |
| | | `id` varchar(64) NOT NULL COMMENT 'appId', |
| | | `screct` varchar(255) NOT NULL COMMENT 'appSecret', |
| | | `name` varchar(128) DEFAULT NULL COMMENT '应用名称', |
| | | `url` varchar(512) DEFAULT NULL COMMENT '应用URL', |
| | | `enable` tinyint(4) NOT NULL DEFAULT '1' COMMENT '是否启用 0未启用 1启用', |
| | | `tenant_id` bigint(20) DEFAULT NULL COMMENT '租户id', |
| | | PRIMARY KEY (`id`) |
| | | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='开放接口应用'; |
| | | |
| | | -- 示例数据(可选,用于云仓对接) |
| | | -- INSERT INTO `open_api_app` (`id`, `screct`, `name`, `url`, `enable`, `tenant_id`) VALUES ('cloud_wms', 'your_secret', '云仓WMS', NULL, 1, NULL); |
| New file |
| | |
| | | -- 应用管理菜单(开放接口 Token 鉴权应用,需在 sys_menu 已有数据之后执行) |
| | | -- 父菜单 id=47,子菜单 48-51;parent_id=1 表示挂在「系统」下 |
| | | INSERT INTO `sys_menu` (`id`, `name`, `parent_id`, `parent_name`, `path`, `path_name`, `route`, `component`, `brief`, `code`, `type`, `authority`, `icon`, `sort`, `meta`, `tenant_id`, `status`, `deleted`, `create_time`, `create_by`, `update_time`, `update_by`, `memo`) VALUES (47, '应用管理', 1, 'menu.system', '1', 'menu.system', '/system/openApiApp', 'openApiApp', NULL, NULL, 0, NULL, 'Apps', 11, NULL, 1, 1, 0, NULL, NULL, NULL, NULL, NULL); |
| | | INSERT INTO `sys_menu` (`id`, `name`, `parent_id`, `parent_name`, `path`, `path_name`, `route`, `component`, `brief`, `code`, `type`, `authority`, `icon`, `sort`, `meta`, `tenant_id`, `status`, `deleted`, `create_time`, `create_by`, `update_time`, `update_by`, `memo`) VALUES (48, 'Query App', 47, NULL, '1,47', NULL, NULL, NULL, NULL, NULL, 1, 'system:openApiApp:list', NULL, 0, NULL, 1, 1, 0, NULL, NULL, NULL, NULL, NULL); |
| | | INSERT INTO `sys_menu` (`id`, `name`, `parent_id`, `parent_name`, `path`, `path_name`, `route`, `component`, `brief`, `code`, `type`, `authority`, `icon`, `sort`, `meta`, `tenant_id`, `status`, `deleted`, `create_time`, `create_by`, `update_time`, `update_by`, `memo`) VALUES (49, 'Create App', 47, NULL, '1,47', NULL, NULL, NULL, NULL, NULL, 1, 'system:openApiApp:save', NULL, 1, NULL, 1, 1, 0, NULL, NULL, NULL, NULL, NULL); |
| | | INSERT INTO `sys_menu` (`id`, `name`, `parent_id`, `parent_name`, `path`, `path_name`, `route`, `component`, `brief`, `code`, `type`, `authority`, `icon`, `sort`, `meta`, `tenant_id`, `status`, `deleted`, `create_time`, `create_by`, `update_time`, `update_by`, `memo`) VALUES (50, 'Update App', 47, NULL, '1,47', NULL, NULL, NULL, NULL, NULL, 1, 'system:openApiApp:update', NULL, 2, NULL, 1, 1, 0, NULL, NULL, NULL, NULL, NULL); |
| | | INSERT INTO `sys_menu` (`id`, `name`, `parent_id`, `parent_name`, `path`, `path_name`, `route`, `component`, `brief`, `code`, `type`, `authority`, `icon`, `sort`, `meta`, `tenant_id`, `status`, `deleted`, `create_time`, `create_by`, `update_time`, `update_by`, `memo`) VALUES (51, 'Delete App', 47, NULL, '1,47', NULL, NULL, NULL, NULL, NULL, 1, 'system:openApiApp:remove', NULL, 3, NULL, 1, 1, 0, NULL, NULL, NULL, NULL, NULL); |
| New file |
| | |
| | | -- 出库历史单菜单:与「入库历史单」同级,共用 asnOrderLog 接口,仅前端 resource=outStockOrderLog、固定 type=out |
| | | -- 执行前需已存在 component='asnOrderLog' 的菜单,否则本 INSERT 不会插入任何行 |
| | | INSERT INTO `sys_menu` (`id`, `name`, `parent_id`, `parent_name`, `path`, `path_name`, `route`, `component`, `brief`, `code`, `type`, `authority`, `icon`, `sort`, `meta`, `tenant_id`, `status`, `deleted`, `create_time`, `create_by`, `update_time`, `update_by`, `memo`) |
| | | SELECT 52, 'menu.outStockOrderLog', m.parent_id, m.parent_name, CONCAT(IFNULL(m.path,''), ',52'), 'menu.outStockOrderLog', '/histories/outStockOrderLog', 'outStockOrderLog', NULL, NULL, 0, NULL, 'Outbox', 2, NULL, 1, 1, 0, NULL, NULL, NULL, NULL, NULL |
| | | FROM `sys_menu` m WHERE m.component = 'asnOrderLog' LIMIT 1; |
| | | |
| | | -- 出库历史单列表权限(与入库历史单共用 manager:asnOrderLog:list,有该权限即可访问两个菜单) |
| | | INSERT INTO `sys_menu` (`id`, `name`, `parent_id`, `parent_name`, `path`, `path_name`, `route`, `component`, `brief`, `code`, `type`, `authority`, `icon`, `sort`, `meta`, `tenant_id`, `status`, `deleted`, `create_time`, `create_by`, `update_time`, `update_by`, `memo`) |
| | | SELECT 53, 'Query OutStockOrderLog', 52, NULL, CONCAT(IFNULL(m.path,''), ',53'), NULL, NULL, NULL, NULL, NULL, 1, 'manager:asnOrderLog:list', NULL, 0, NULL, 1, 1, 0, NULL, NULL, NULL, NULL, NULL |
| | | FROM `sys_menu` m WHERE m.id = 52 LIMIT 1; |