Java Spring框架:单表的查询、新增、修改、删除、导入、导出

一、 前端:

index.vue

html 复制代码
<template>
  <div class="replenish-config flex flex-col">
    <WmsHeader
      ref="searchRef"
      :formConfig="searchConfig"
      :labelCol="{ span: 8 }"
      :searchNum="4"
      :searchPopoverStyle="{ width: '640px' }"
      :wrapperCol="{ span: 16 }"
      @onQuery="handleSearch"
      @onReset="handleReset"
    >
      <template #btnArea>
        <tips-icon icon="import" type="primary" @click="openImportDrawer">{{ $t('common.import') }}</tips-icon>
        <tips-icon icon="export" type="primary" @click="exportExcel">{{ $t('common.export') }}</tips-icon>
      </template>
    </WmsHeader>

    <base-table ref="tableRef" :autoWidth="false" :columns="columns" is-serial-number :query-api="QueryData" :query-args="queryArgs">
      <template #columns="{ column, currentRecord, text }">
        <template v-if="column?.key !== 'index'">
          <display-user v-if="column?.key === 'creator'" :value="currentRecord.creator" />
          <span v-else-if="column?.key === 'tj0Notifier'" class="multiple-user">
            <user-info-popover
              v-for="item in String(currentRecord.tj0Notifier || '')
                .split(',')
                .filter(Boolean)"
              :key="item"
              :user-id="item"
            />
          </span>
          <span v-else-if="column?.key === 'tj0AutoCreateWeek'">{{ getExecutionWeekLabel(text) }}</span>
          <n-tooltip v-else placement="topLeft">
            {{ text }}
            <template #title>
              <span style="white-space: pre-wrap">{{ text }}</span>
            </template>
          </n-tooltip>
        </template>
      </template>
    </base-table>

    <menu-boll :btnList="btnList" @btnclickEmit="menuClick" />

    <GvDrawer :title="drawerTitle" :type="formStatus" :visible="formVisible" @close="closeFormDrawer" @confirm="confirmForm">
      <template #form>
        <base-form ref="formRef" :config="formConfig" :model="formModel" :status="formStatus" />
      </template>
    </GvDrawer>

    <GvDrawer
      :confirmBtnText="$t('common.import')"
      :loading="importLoading"
      :title="$t('common.import')"
      :visible="importVisible"
      @close="closeImportDrawer"
      @confirm="confirmImport"
    >
      <template #form>
        <ImportTemplate ref="importTemplateRef" handUpload @change="handleImportChange" @downTemplate="downloadTemplate" />
        <p v-if="importErrorMsg" class="text-red mt-16">{{ $t('common.importError') }}: {{ importErrorMsg }}</p>
      </template>
    </GvDrawer>
  </div>
</template>

<script setup lang="ts">
import ImportTemplate from '@/components/ImportTemplate.vue';
import { Add, DeleteByIds, DownloadTemp, ExportExcel, ImportExcel, QueryData, Update } from '@/api/wms/replenishConfig';
import type { BaseFormExpose } from '@/types/baseForm.d';
import type { BaseTableExpose, QueryArgs, QueryCondition } from '@/types/baseTable';
import { fileTools } from '@/utils/fileTools';
import WmsHeader from '@/views/wms/components/WmsHeader.vue';
import { DeleteOutlined, EditOutlined, PlusOutlined, ReloadOutlined } from '@nancal-icon/icons-vue';
import { Modal, message } from 'n-designv3';
import { getExecutionWeekLabel, type ReplenishConfig } from './data';
import { ExtendConfig } from './extend';

const { columns, searchConfig, formConfig } = ExtendConfig;
const tableRef = ref<BaseTableExpose>();
const searchRef = ref();
const formRef = ref<BaseFormExpose>();
const importTemplateRef = ref();

/** 当前新增或编辑的表单数据。 */
const formModel = ref<Partial<ReplenishConfig>>({});
/** 表单抽屉状态;编辑时必须携带 objId。 */
const formStatus = ref<'add' | 'edit'>('add');
const formVisible = ref(false);
const importVisible = ref(false);
const importLoading = ref(false);
const importErrorMsg = ref('');
/** 上传组件返回的原始 Excel 文件。 */
const importFile = ref<any>(null);
const drawerTitle = computed(() => (formStatus.value === 'add' ? '新建驻地库自动补库配置' : '修改驻地库自动补库配置'));

/** 列表的默认分页、排序和筛选条件。 */
const queryArgs: QueryArgs = reactive({
  attrSet: [],
  condition: [],
  sorts: [{ name: 'createAt', sort: 'desc' }],
  page: { pageNo: 1, pageSize: 15 },
});

const btnList = computed(() => [
  { type: 'create', icon: PlusOutlined, name: '新建' },
  { type: 'edit', icon: EditOutlined, name: '修改' },
  { type: 'delete', icon: DeleteOutlined, name: '删除' },
  { type: 'refresh', icon: ReloadOutlined, name: '刷新' },
]);

/** 返回空表单模型,防止上次编辑数据残留到新增表单。 */
const defaultFormModel = (): Partial<ReplenishConfig> => ({});

/** 重新请求分页数据。 */
const queryList = () => tableRef.value?.refreshList();

/** 将搜索组件条件同步至列表,并回到第一页。 */
const handleSearch = (_model: Record<string, any>, condition: QueryCondition[] = []) => {
  queryArgs.page!.pageNo = 1;
  tableRef.value?.setQueryArgs({}, true);
  tableRef.value?.setQueryArgs(condition);
  queryList();
};

/** 清空搜索、勾选状态并恢复默认分页。 */
const handleReset = () => {
  tableRef.value?.setSelectedRows([]);
  tableRef.value?.setQueryArgs({}, true);
  queryArgs.page!.pageNo = 1;
  queryList();
};

/** 打开新增或编辑抽屉。 */
const openFormDrawer = (status: 'add' | 'edit') => {
  formStatus.value = status;
  formVisible.value = true;
};

/** 初始化新增表单。 */
const create = () => {
  formModel.value = defaultFormModel();
  openFormDrawer('add');
};

/**
 * 将后端以英文逗号保存的通知人员编号转换为多选组件数组值。
 *
 * @param notifier 后端保存的人员编号字符串。
 */
const splitNotifier = (notifier?: string) =>
  notifier
    ?.split(',')
    .map(item => item.trim())
    .filter(Boolean) || [];

/** 读取单个勾选行并初始化编辑表单。 */
const edit = () => {
  const rows = tableRef.value?.getSelectedRows() || [];
  if (!rows.length) return message.warn('请选择需要修改的数据');
  if (rows.length > 1) return message.warn('一次只能修改一条数据');
  formModel.value = {
    ...rows[0],
    tj0Notifier: splitNotifier(rows[0].tj0Notifier),
  };
  openFormDrawer('edit');
};

/** 关闭表单并清理数据。 */
const closeFormDrawer = () => {
  formVisible.value = false;
  formModel.value = defaultFormModel();
};

/** 校验表单后调用新增或修改接口,并刷新列表。 */
const confirmForm = async () => {
  try {
    await formRef.value?.validate();
    const api = formStatus.value === 'add' ? Add : Update;
    const params = {
      ...formModel.value,
      // 多选人员组件返回编号数组,提交给后端时按英文逗号拼接。
      tj0Notifier: Array.isArray(formModel.value.tj0Notifier) ? formModel.value.tj0Notifier.join(',') : formModel.value.tj0Notifier,
    };
    const response: any = await api(params);
    if (response.code === '0') {
      message.success('操作成功');
      closeFormDrawer();
      queryList();
    }
  } catch (_error) {
    // 表单校验不通过时保持抽屉打开。
  }
};

/** 确认后批量删除已勾选的补库配置。 */
const remove = () => {
  const rows = tableRef.value?.getSelectedRows() || [];
  if (!rows.length) return message.warn('请选择需要删除的数据');
  Modal.confirm({
    content: `确定删除选中的 ${rows.length} 条数据吗?`,
    okText: '确定',
    cancelText: '取消',
    async onOk() {
      const response: any = await DeleteByIds(rows.map((row: ReplenishConfig) => row.objId!));
      if (response.code === '0') {
        message.success('删除成功');
        queryList();
      }
    },
  });
};

/** 重置搜索表单后重新加载列表。 */
const refresh = () => {
  searchRef.value?.reset();
  handleReset();
};

/** 导出当前筛选结果;增大页大小以获取全部匹配记录。 */
const exportExcel = async () => {
  const params = JSON.parse(JSON.stringify(queryArgs));
  params.page.pageSize = 60000;
  const response = await ExportExcel(params);
  fileTools.downloadXlsx(response, '驻地库自动补库配置.xlsx');
};

/** 打开 Excel 导入抽屉。 */
const openImportDrawer = () => {
  importVisible.value = true;
};

/** 下载与当前字段顺序一致的导入模板。 */
const downloadTemplate = async () => {
  const response = await DownloadTemp();
  fileTools.downloadXlsx(response, '驻地库自动补库配置导入模板.xlsx');
};

/** 兼容上传组件不同版本的文件对象结构。 */
const getUploadFile = (file: any) => file?.originFileObj || file?.file || file;

/** 缓存用户选择的 Excel 文件,并在删除文件时清空缓存。 */
const handleImportChange = (info: any) => {
  importErrorMsg.value = '';
  if (info.file?.status === 'removed') {
    importFile.value = null;
    return;
  }
  importFile.value = getUploadFile(info.file);
};

/** 上传 Excel;后端按源库存地点和目标库存地点组合覆盖重复行。 */
const confirmImport = async () => {
  if (!importFile.value?.uid && !importFile.value?.name && !(importFile.value instanceof Blob)) {
    return message.warn('请选择需要导入的文件');
  }
  const formData = new FormData();
  formData.append('file', importFile.value);
  try {
    importLoading.value = true;
    const response: any = await ImportExcel(formData, { showMsg: false, timeout: 1000 * 60 * 60 });
    if (response.code === '0') {
      message.success('导入成功');
      closeImportDrawer();
      queryList();
    } else {
      importErrorMsg.value = response.msg;
    }
  } finally {
    importLoading.value = false;
  }
};

/** 清除上传控件状态并关闭导入抽屉。 */
const closeImportDrawer = () => {
  importTemplateRef.value?.clearFile();
  importFile.value = null;
  importErrorMsg.value = '';
  importVisible.value = false;
};

/** 分发底部操作栏事件。 */
const menuClick = (status: string) => {
  if (status === 'create') return create();
  if (status === 'edit') return edit();
  if (status === 'delete') return remove();
  if (status === 'refresh') return refresh();
};

onMounted(queryList);
defineExpose({ queryList });
</script>

<style scoped lang="less">
.replenish-config {
  min-height: 100%;
}
</style>

Data.ts

typescript 复制代码
 import type { FormConfig, HeaderConfig } from '@/types/baseForm';
import type { BaseTableColumn } from '@/types/baseTable';
import { getServerPath } from '@/api/basePath';
import { $t } from '@/lang';

/** 用户查询接口所在服务。 */
const serverPath = getServerPath();

/** 自动执行周下拉选项:界面显示中文,提交并保存数字编码。 */
const executionWeekOptions = [
  { label: '周一', value: '1' },
  { label: '周二', value: '2' },
  { label: '周三', value: '3' },
  { label: '周四', value: '4' },
  { label: '周五', value: '5' },
  { label: '周六', value: '6' },
  { label: '周日', value: '7' },
];

/** 将后端保存的执行周数字转换为页面显示的中文。 */
export const getExecutionWeekLabel = (value?: string | number) => executionWeekOptions.find(item => item.value === String(value))?.label || value || '';

/**
 * 驻地库自动补库配置页面数据模型。
 * objId 由后端生成;源库存地点和目标库存地点组合唯一。
 */
export interface ReplenishConfig {
  objId?: string | number;
  tj0SourceLocation?: string;
  tj0TargetLocation?: string;
  tj0AutoCreateTime?: string;
  tj0AutoCreateWeek?: string;
  remark?: string;
  /** 通知人员编号;表单中为编号数组,提交后以英文逗号分隔。 */
  tj0Notifier?: string | string[];
  creator?: string;
  creatorName?: string;
  createAt?: string;
}

/** 补库配置列表列定义。 */
export const columns = computed<BaseTableColumn[]>(() => [
  { title: '源库存地点', dataIndex: 'tj0SourceLocation', key: 'tj0SourceLocation', width: 140 },
  { title: '目标库存地点', dataIndex: 'tj0TargetLocation', key: 'tj0TargetLocation', width: 140 },
  { title: '自动执行时间', dataIndex: 'tj0AutoCreateTime', key: 'tj0AutoCreateTime', width: 150 },
  {
    title: '自动执行周',
    dataIndex: 'tj0AutoCreateWeek',
    key: 'tj0AutoCreateWeek',
    width: 130, 
    // customRender: 自定义渲染函数,将后端保存的执行周数字转换为中文显示的周名称
    // 周的序号转换为中文显示,例如:周一、周二、周三等
    customRender: ({ text }) => getExecutionWeekLabel(text),
  },
  {
    title: '通知人员',
    dataIndex: 'tj0Notifier',
    key: 'tj0Notifier',
    width: 300,
    /**
     * 使用框架内置的 multiple-user 表格渲染类型,而非第三方插件。
     * 它将 tj0Notifier 中以英文逗号分隔的人员编号逐个传给 user-info-popover;
     * user-info-popover 从已加载的用户树 userStore.userAllTree 中按 objId 匹配,展示人员姓名,
     * 鼠标悬停姓名时可显示部门、电话等用户信息。
     */
    type: 'multiple-user',
    attrs: {
      /**
       * 多名人员的姓名在同一行内以逗号横向排列;不设置或为 false 时,每名人员将以块级元素换行显示。
       * 该属性会透传给 user-info-popover 的 parallelOrNot 参数,仅影响列表显示样式,不影响后端保存值。
       */
      parallelOrNot: false,
    },
  },
  { title: '备注', dataIndex: 'remark', key: 'remark', width: 200 },
  { title: $t('common.creator'), dataIndex: 'creator', key: 'creator', width: 120, type: 'user' },
  { title: $t('common.createAt'), dataIndex: 'createAt', key: 'createAt', width: 150 },
]);

/** 补库配置列表搜索字段定义。 */
export const searchConfig = computed<HeaderConfig[]>(() => [
  {
    prop: 'tj0SourceLocation',
    label: '源库存地点',
    type: 'input',
    compare: 'EQUAL',
    attrs: { maxLength: 30, placeholder: $t('common.pleaseEnter') },
  },
  {
    prop: 'tj0TargetLocation',
    label: '目标库存地点',
    type: 'input',
    compare: 'EQUAL',
    attrs: { maxLength: 30, placeholder: $t('common.pleaseEnter') },
  },
  {
    prop: 'tj0AutoCreateWeek',
    label: '自动执行周',
    type: 'select',
    compare: 'EQUAL',
    options: executionWeekOptions,
    attrs: { placeholder: $t('common.pleaseSelect') },
  },
]);

/** 新增和修改补库配置的表单定义。 */
export const formConfig = computed<FormConfig[]>(() => [
  {
    prop: 'tj0SourceLocation',
    label: '源库存地点',
    type: 'input',
    attrs: { maxLength: 30, placeholder: '请输入源库存地点' },
    rules: { required: true, message: '请输入源库存地点', trigger: 'blur' },
  },
  {
    prop: 'tj0TargetLocation',
    label: '目标库存地点',
    type: 'input',
    attrs: { maxLength: 30, placeholder: '请输入目标库存地点' },
    rules: { required: true, message: '请输入目标库存地点', trigger: 'blur' },
  },
  {
    prop: 'tj0AutoCreateTime',
    label: '自动执行时间',
    type: 'input',
    attrs: { maxLength: 8, placeholder: '请输入 04:00 或 04:00:00' },
    rules: [
      { required: true, message: '请输入自动执行时间', trigger: 'blur' },
      {
        // 匹配 HH:mm 或 HH:mm:ss 格式
        // HH 为 00-23 之间的整数,mm 为 00-59 之间的整数,ss 为 00-59 之间的整数
        // 例如:04:00 或 04:00:00
        pattern: /^([01]\d|2[0-3]):[0-5]\d(?::[0-5]\d)?$/,
        message: '自动执行时间格式必须为 HH:mm 或 HH:mm:ss,例如 04:00 或 04:00:00',
        trigger: 'blur',
      },
    ],
  },
  {
    prop: 'tj0AutoCreateWeek',
    label: '自动执行周',
       // 自动执行周只能选择一个
    type: 'select',
    // executionWeekOptions: 1-7 之间的整数,分别对应周一至周日的执行周
    options: executionWeekOptions,
    attrs: { placeholder: '请选择自动执行周' },
    rules: { required: true, message: '请选择自动执行周', trigger: 'change' },
  },
  {
    prop: 'tj0Notifier',
    label: '通知人员',
    type: 'query-select',
    className: 'User',
    svrName: serverPath.mbd,
    attrs: {
      mode: 'multiple',
      maxLength: 300,
      placeholder: '请选择通知人员',
    },
  },
  {
    prop: 'remark',
    label: '备注',
    type: 'textarea',
    attrs: { maxLength: 150, placeholder: '请输入备注' },
  },
]);

/** 页面默认配置,可由 extend.ts 按项目需要覆盖。 */
export const BaseConfig = { columns, searchConfig, formConfig };

API.ts

typescript 复制代码
import * as paths from '@/api/basePath';
import { getCommonApi } from '../common';
import { http, type CustomConfig } from '@/utils/request';
import type { QueryArgs } from '@/types/baseTable';

/** WMS 服务请求前缀。 */
const serverPath = paths.default.wms;

/** 驻地库自动补库配置后端控制器路径。 */
export const className = '/ReplenishConfig';

/**
 * 驻地库自动补库配置公共 CRUD 接口。
 * 包含分页查询、新增、修改、详情和删除。
 */
export const { queryPage: QueryData, add: Add, update: Update, detail: Detail, delete: Delete } = getCommonApi(serverPath, className);

/**
 * 按对象 ID 批量删除补库配置。
 *
 * @param params 选中记录的 objId 数组。
 */
export const DeleteByIds = (params: Array<string | number>) => http.post(`${serverPath}${className}/deleteByIds`, params);

/**
 * 按当前筛选条件导出补库配置 Excel。
 *
 * @param params 列表查询及排序条件。
 */
export const ExportExcel = (params?: QueryArgs) => http.post(`${serverPath}${className}/exportExcel`, params, false, 'blob');

/**
 * 导入补库配置 Excel;后端按源库存地点和目标库存地点组合覆盖已有行。
 *
 * @param params 包含 file 字段的 FormData。
 * @param customConfig 请求超时等自定义配置。
 */
export const ImportExcel = (params?: FormData, customConfig?: CustomConfig) =>
  http.post(`${serverPath}${className}/importExcel`, params || new FormData(), undefined, 'json', customConfig);

/** 下载补库配置 Excel 导入模板。 */
export const DownloadTemp = () => http.get(`${serverPath}${className}/downloadTemp`, {}, false, 'blob');

二、后端

2.1 API 接口控制器:ReplenishConfigController.java

java 复制代码
 package com.nancal.wms.controller;

import com.nancal.base.bean.page.PageQueryReq;
import com.nancal.base.bean.page.PageResult;
import com.nancal.base.bean.page.Result;
import com.nancal.base.service.BaseService;
import com.nancal.base.web.controller.AbstractController;
import com.nancal.wms.bean.entity.Tj0ReplenishConfig;
import com.nancal.wms.service.Tj0ReplenishConfigService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * 功能描述: 驻地库自动补库配置控制器。
 * 继承框架公共接口,提供补库配置的查询、新增、修改和删除功能。
 *
 * @author Bingo
 * @date 2026/09/18
 */
@Slf4j
@RestController
@RequestMapping("/ReplenishConfig")
public class ReplenishConfigController extends AbstractController<Tj0ReplenishConfig> {

    @Resource
    private Tj0ReplenishConfigService tj0ReplenishConfigService;

    @Resource
    private HttpServletResponse response;

    /**
     * 功能描述: 返回补库配置服务,复用框架公共增删改查接口。
     */
    @Override
    public BaseService<Tj0ReplenishConfig> getService() {
        return tj0ReplenishConfigService;
    }

    /**
     * 功能描述: 返回补库配置实体类型,供公共查询接口构建条件。
     */
    @Override
    public Class<Tj0ReplenishConfig> getEntityClass() {
        return Tj0ReplenishConfig.class;
    }

    /**
     * 功能描述: 导出当前查询结果。
     */
    @PostMapping("/exportExcel")
    public void exportExcel(@RequestBody PageQueryReq<Tj0ReplenishConfig> req) throws IOException {
        PageResult<Tj0ReplenishConfig> pageResult = this.queryPage(req);
        this.tj0ReplenishConfigService.exportExcel(pageResult.getData(), response);
    }

    /**
     * 功能描述: 下载补库配置导入模板。
     */
    @GetMapping("/downloadTemp")
    public void downloadTemp() throws IOException {
        this.tj0ReplenishConfigService.downloadTemp(response);
    }

    /**
     * 功能描述: 导入补库配置数据。
     */
    @PostMapping("/importExcel")
    public Result importExcel(@RequestParam("file") MultipartFile file) throws IOException {
        return this.tj0ReplenishConfigService.importExcel(file);
    }
}

2.1.1 继承类 AbstractController.java

java 复制代码
 //
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package com.nancal.base.web.controller;

import com.nancal.base.bean.entity.DtRoot;
import com.nancal.base.bean.exceptions.ParamServiceException;
import com.nancal.base.bean.page.EntityPageQueryReq;
import com.nancal.base.bean.page.EntityPageQueryResp;
import com.nancal.base.bean.page.PageQueryCovert;
import com.nancal.base.bean.page.PageQueryReq;
import com.nancal.base.bean.page.PageResult;
import com.nancal.base.bean.page.QueryReq;
import com.nancal.base.bean.page.Result;
import com.nancal.base.bean.query.CompareType;
import com.nancal.base.bean.query.Condition;
import com.nancal.base.service.BaseService;
import com.nancal.base.util.PreconditionUtil;
import java.util.List;
import org.apache.commons.collections4.CollectionUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;

public abstract class AbstractController<T extends DtRoot> {
    public abstract BaseService<T> getService();

    public abstract Class<T> getEntityClass();

    @GetMapping({"/detail"})
    public Result<T> detail(@RequestParam("objId") Long objId) {
        PreconditionUtil.checkParamTrue(this.extDetailPre(objId));
        T entity = this.getService().detail(this.getEntityClass(), objId);
        this.extDetailPost(entity);
        return Result.success(entity);
    }

    @GetMapping({"/detailDisplayFile"})
    public Result<T> detailDisplayFile(@RequestParam("objId") Long objId) {
        PreconditionUtil.checkParamTrue(this.extDetailPre(objId));
        T entity = this.getService().detailDisplayFile(this.getEntityClass(), objId);
        this.extDetailPost(entity);
        return Result.success(entity);
    }

    @GetMapping({"/detailByCode"})
    public Result<T> detailByCode(@RequestParam("code") String code) {
        PreconditionUtil.checkParamTrue(this.extDetailByCodePre(code));
        QueryReq<T> req = new QueryReq();
        req.setEntityClass(this.getEntityClass());
        req.setCondition(List.of(new Condition("code", CompareType.EQUAL, code)));
        EntityPageQueryReq<T> entityPageReq = PageQueryCovert.buildEntityQueryReq(req);
        Result<List<T>> ret = this.getService().query(entityPageReq);
        List<T> datas = (List)ret.getData();
        if (CollectionUtils.size(datas) > 1) {
            PreconditionUtil.checkTrue(CollectionUtils.size(datas) > 1, new ParamServiceException());
        } else if (CollectionUtils.size(datas) == 1) {
            T data = (T)(datas.get(0));
            this.extDetailByCodePost(data);
            return Result.success(data);
        }

        return Result.success();
    }

    @PostMapping({"/add"})
    public Result<T> add(@RequestBody T entity) {
        this.extAddPre(entity);
        T save = this.getService().save(entity);
        entity.setObjId(save.getObjId());
        this.extAddPost(entity);
        this.processResultPost(save);
        return Result.success(save);
    }

    @PostMapping({"/addProcessFile"})
    public Result<T> addProcessFile(@RequestBody T entity) {
        this.extAddPre(entity);
        T save = this.getService().addProcessFile(entity);
        this.extAddPost(entity);
        this.processResultPost(save);
        return Result.success(save);
    }

    public void processResultPost(T save) {
    }

    @PostMapping({"/addUpdate"})
    public Result<List<T>> addUpdate(@RequestBody List<T> entitys) {
        this.extAddUpdatePre(entitys);
        List<T> saves = this.getService().addUpdate(entitys);
        this.extAddUpdatePost(entitys);
        this.extAddUpdateResultPost(saves);
        return Result.success(saves);
    }

    @PostMapping({"/update"})
    public Result<?> update(@RequestBody T entity) {
        this.extUpdatePre(entity);
        this.getService().update(entity);
        this.extUpdatePost(entity);
        return Result.success();
    }

    @PostMapping({"/updateProcessFile"})
    public Result<T> updateProcessFile(@RequestBody T entity) {
        this.extUpdatePre(entity);
        this.getService().updateProcessFile(entity);
        this.extUpdatePost(entity);
        return Result.success(entity);
    }

    @GetMapping({"/delete"})
    public Result<?> deleteById(@RequestParam("objId") Long objId) {
        PreconditionUtil.checkParamTrue(this.extDeleteByIdPre(objId));
        this.getService().deleteById(this.getEntityClass(), objId);
        this.extDeleteByIdPost(objId);
        return Result.success();
    }

    @PostMapping({"/deleteByIds"})
    public Result<?> deleteByIds(@RequestBody List<Long> ids) {
        PreconditionUtil.checkParamTrue(this.extDeleteByIdsPre(ids));
        this.getService().deleteByIds(this.getEntityClass(), ids);
        this.extDeleteByIdsPost(ids);
        return Result.success();
    }

    @PostMapping({"/delProcessFileByIds"})
    public Result<?> delProcessFileByIds(@RequestBody List<Long> ids) {
        PreconditionUtil.checkParamTrue(this.extDeleteByIdsPre(ids));
        this.getService().delProcessFileByIds(this.getEntityClass(), ids);
        this.extDeleteByIdsPost(ids);
        return Result.success();
    }

    public boolean extDeleteByIdsPost(List<Long> ids) {
        return true;
    }

    public boolean extDeleteByIdsPre(List<Long> ids) {
        return true;
    }

    @PostMapping({"/queryPage"})
    public PageResult<T> queryPage(@RequestBody PageQueryReq<T> req) {
        PreconditionUtil.checkParamTrue(this.extQueryPagePre(req));
        req.setEntityClass(this.getEntityClass());
        EntityPageQueryReq<T> entityPageReq = PageQueryCovert.buildEntityQueryReq(req);
        EntityPageQueryResp<T> pageResp = this.getService().queryPage(entityPageReq);
        this.extQueryPagePost(pageResp);
        return new PageResult(pageResp.getTotal(), pageResp.getData());
    }

    @PostMapping({"/queryPageDisplayFile"})
    public PageResult<T> queryPageDisplayFile(@RequestBody PageQueryReq<T> req) {
        PreconditionUtil.checkParamTrue(this.extQueryPagePre(req));
        req.setEntityClass(this.getEntityClass());
        EntityPageQueryResp<T> pageResp = this.getService().queryPageDisplayFile(PageQueryCovert.buildEntityQueryReq(req));
        this.extQueryPagePost(pageResp);
        return new PageResult(pageResp.getTotal(), pageResp.getData());
    }

    @GetMapping({"/findTree"})
    public PageResult<T> findTree(@RequestParam(value = "preNodeId",required = false) Long preNodeId) {
        EntityPageQueryResp<T> pageResp = this.getService().findTree(this.getEntityClass(), preNodeId);
        this.extFindTreePost(pageResp);
        return new PageResult(pageResp.getTotal(), pageResp.getData());
    }

    @PostMapping({"/query"})
    public Result<List<T>> query(@RequestBody QueryReq<T> req) {
        req.setEntityClass(this.getEntityClass());
        EntityPageQueryReq<T> entityPageReq = PageQueryCovert.buildEntityQueryReq(req);
        return this.getService().query(entityPageReq);
    }

    public void extAddUpdateResultPost(List<T> entitys) {
    }

    protected boolean extDetailByCodePre(String code) {
        return true;
    }

    protected void extDetailByCodePost(T entity) {
    }

    protected boolean extDetailPre(Long id) {
        return true;
    }

    protected void extDetailPost(T entity) {
    }

    protected void extAddPre(T entity) {
    }

    protected void extAddPost(T entity) {
    }

    protected void extAddUpdatePre(List<T> entitys) {
    }

    protected void extAddUpdatePost(List<T> entitys) {
    }

    protected void extUpdatePre(T entity) {
    }

    protected void extUpdatePost(T entity) {
    }

    protected boolean extDeleteByIdPre(Long id) {
        return true;
    }

    protected boolean extDeleteByIdPost(Long id) {
        return true;
    }

    protected boolean extQueryPagePre(PageQueryReq<T> req) {
        return true;
    }

    protected void extQueryPagePost(EntityPageQueryResp<?> resp) {
    }

    protected void extFindTreePost(EntityPageQueryResp<?> resp) {
    }
}

2.2 服务接口 Tj0ReplenishConfigService

java 复制代码
package com.nancal.wms.service;

import com.nancal.base.bean.page.Result;
import com.nancal.base.service.BaseService;
import com.nancal.wms.bean.entity.Tj0ReplenishConfig;
import org.springframework.web.multipart.MultipartFile;

import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.List;

/**
 * 功能描述: 驻地库自动补库配置服务接口。
 *
 * @author Bingo
 * @date 2026/09/18
 */
public interface Tj0ReplenishConfigService extends BaseService<Tj0ReplenishConfig> {

    /**
     * 功能描述: 导出驻地库自动补库配置。
     */
    void exportExcel(List<Tj0ReplenishConfig> list, HttpServletResponse response) throws IOException;

    /**
     * 功能描述: 下载驻地库自动补库配置导入模板。
     */
    void downloadTemp(HttpServletResponse response) throws IOException;

    /**
     * 功能描述: 导入驻地库自动补库配置。
     */
    Result importExcel(MultipartFile file) throws IOException;
}

2.3 服务实现 Tj0ReplenishConfigServiceImpl

java 复制代码
package com.nancal.wms.service.impl;

import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.util.StrUtil;
import com.nancal.base.bean.exceptions.ServiceException;
import com.nancal.base.bean.page.Result;
import com.nancal.base.bean.query.EntityQueryReq;
import com.nancal.base.service.AbstractService;
import com.nancal.base.service.repository.BaseRepository;
import com.nancal.qms.utils.ExcelUtil;
import com.nancal.wms.bean.common.Tj0ReplenishConfigExcel;
import com.nancal.wms.bean.entity.Tj0ReplenishConfig;
import com.nancal.wms.service.Tj0ReplenishConfigService;
import com.nancal.wms.service.repository.Tj0ReplenishConfigRepository;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.interceptor.TransactionAspectSupport;
import org.springframework.web.multipart.MultipartFile;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletResponse;
import javax.transaction.Transactional;
import java.io.IOException;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;

/**
 * 功能描述: 驻地库自动补库配置服务实现。
 *
 * @author Bingo
 * @date 2026/09/18
 */
@Service
@Transactional(rollbackOn = ServiceException.class)
public class Tj0ReplenishConfigServiceImpl extends AbstractService<Tj0ReplenishConfig> implements Tj0ReplenishConfigService {

    @Resource
    private Tj0ReplenishConfigRepository tj0ReplenishConfigRepository;

    /**
     * 功能描述: 返回补库配置仓储。
     */
    @Override
    public BaseRepository<Tj0ReplenishConfig> getBaseRepository() {
        return tj0ReplenishConfigRepository;
    }

    /**
     * 功能描述: 新增前校验源库存地点和目标库存地点组合唯一。
     */
    @Override
    protected boolean extSavePre(Tj0ReplenishConfig entity) {
        prepareEntity(entity);
        if (!findByLocations(entity.getTj0SourceLocation(), entity.getTj0TargetLocation()).isEmpty()) {
            throw new ServiceException("源库存地点和目标库存地点的补库配置已存在");
        }
        return super.extSavePre(entity);
    }

    /**
     * 功能描述: 修改前校验源库存地点和目标库存地点组合唯一。
     */
    @Override
    protected boolean extUpdatePre(Tj0ReplenishConfig entity) {
        prepareEntity(entity);
        List<Tj0ReplenishConfig> configs = findByLocations(entity.getTj0SourceLocation(), entity.getTj0TargetLocation());
        configs.removeIf(item -> Objects.equals(item.getObjId(), entity.getObjId()));
        if (!configs.isEmpty()) {
            throw new ServiceException("源库存地点和目标库存地点的补库配置已存在");
        }
        return super.extUpdatePre(entity);
    }

    /**
     * 功能描述: 批量保存前校验源库存地点和目标库存地点组合唯一,覆盖导入场景。
     */
    @Override
    protected boolean extAddUpdatePre(List<Tj0ReplenishConfig> entityList) {
        if (CollectionUtil.isEmpty(entityList)) {
            return super.extAddUpdatePre(entityList);
        }
        Map<String, Integer> duplicateCheck = new LinkedHashMap<>();
        for (Tj0ReplenishConfig entity : entityList) {
            prepareEntity(entity);
            String locationKey = buildLocationKey(entity.getTj0SourceLocation(), entity.getTj0TargetLocation());
            duplicateCheck.merge(locationKey, 1, Integer::sum);
            if (duplicateCheck.get(locationKey) > 1) {
                throw new ServiceException("源库存地点和目标库存地点重复:" + locationKey);
            }
            List<Tj0ReplenishConfig> configs = findByLocations(entity.getTj0SourceLocation(), entity.getTj0TargetLocation());
            configs.removeIf(item -> Objects.equals(item.getObjId(), entity.getObjId()));
            if (!configs.isEmpty()) {
                throw new ServiceException("源库存地点和目标库存地点的补库配置已存在");
            }
        }
        return super.extAddUpdatePre(entityList);
    }

    /**
     * 功能描述: 保存前统一清理字段空格并校验必填库存地点。
     */
    private void prepareEntity(Tj0ReplenishConfig entity) {
        if (entity == null || StrUtil.isBlank(entity.getTj0SourceLocation())) {
            throw new ServiceException("源库存地点不能为空");
        }
        if (StrUtil.isBlank(entity.getTj0TargetLocation())) {
            throw new ServiceException("目标库存地点不能为空");
        }
        entity.setTj0SourceLocation(entity.getTj0SourceLocation().trim());
        entity.setTj0TargetLocation(entity.getTj0TargetLocation().trim());
        entity.setTj0AutoCreateTime(trimToNull(entity.getTj0AutoCreateTime()));
        entity.setTj0AutoCreateWeek(trimToNull(entity.getTj0AutoCreateWeek()));
        entity.setTj0Notifier(trimToNull(entity.getTj0Notifier()));
    }

    /**
     * 功能描述: 按源库存地点和目标库存地点查询已有配置。
     */
    private List<Tj0ReplenishConfig> findByLocations(String sourceLocation, String targetLocation) {
        return tj0ReplenishConfigRepository.find(
                EntityQueryReq.builder(Tj0ReplenishConfig.class)
                        .eq("tj0SourceLocation", sourceLocation)
                        .eq("tj0TargetLocation", targetLocation)
        );
    }

    /**
     * 功能描述: 生成库存地点组合唯一键。
     */
    private String buildLocationKey(String sourceLocation, String targetLocation) {
        return sourceLocation + " -> " + targetLocation;
    }

    /**
     * 功能描述: 将字符串去首尾空格后转为 null。
     */
    private String trimToNull(String value) {
        return StrUtil.isBlank(value) ? null : value.trim();
    }

    /**
     * 功能描述: 导出驻地库自动补库配置。
     */
    @Override
    public void exportExcel(List<Tj0ReplenishConfig> list, HttpServletResponse response) throws IOException {
        List<Tj0ReplenishConfigExcel> excelList = list.stream().map(this::toExcel).collect(Collectors.toList());
        ExcelUtil.export("驻地库自动补库配置", excelList, Tj0ReplenishConfigExcel.class, response);
    }

    /**
     * 功能描述: 下载驻地库自动补库配置导入模板。
     */
    @Override
    public void downloadTemp(HttpServletResponse response) throws IOException {
        ExcelUtil.export("驻地库自动补库配置导入模板", new ArrayList<>(), Tj0ReplenishConfigExcel.class, response);
    }

    /**
     * 功能描述: 导入驻地库自动补库配置,按源库存地点和目标库存地点新增或覆盖已有配置。
     */
    @Override
    public Result importExcel(MultipartFile file) throws IOException {
        if (file == null || file.isEmpty()) {
            return Result.error("空数据,无需导入");
        }
        String originalFilename = file.getOriginalFilename();
        if (StrUtil.isBlank(originalFilename) || !originalFilename.endsWith(".xlsx")) {
            return Result.error("请上传.xlsx格式的Excel文件");
        }
        try {
            List<Tj0ReplenishConfigExcel> rows = ExcelUtil.importExcel(file, Tj0ReplenishConfigExcel.class)
                    .stream()
                    .map(Tj0ReplenishConfigExcel.class::cast)
                    .collect(Collectors.toList());
            if (CollectionUtil.isEmpty(rows)) {
                return Result.error("空数据,无需导入");
            }
            Map<String, Integer> duplicateCheck = new LinkedHashMap<>();
            for (Tj0ReplenishConfigExcel row : rows) {
                if (row == null || StrUtil.isBlank(row.getTj0SourceLocation())) {
                    throw new ServiceException("源库存地点不能为空");
                }
                if (StrUtil.isBlank(row.getTj0TargetLocation())) {
                    throw new ServiceException("目标库存地点不能为空");
                }
                String locationKey = buildLocationKey(row.getTj0SourceLocation().trim(), row.getTj0TargetLocation().trim());
                duplicateCheck.merge(locationKey, 1, Integer::sum);
                if (duplicateCheck.get(locationKey) > 1) {
                    throw new ServiceException("Excel中源库存地点和目标库存地点重复:" + locationKey);
                }
            }

            List<Tj0ReplenishConfig> saveList = new ArrayList<>();
            for (Tj0ReplenishConfigExcel row : rows) {
                String sourceLocation = row.getTj0SourceLocation().trim();
                String targetLocation = row.getTj0TargetLocation().trim();
                List<Tj0ReplenishConfig> existed = findByLocations(sourceLocation, targetLocation);
                Tj0ReplenishConfig entity = CollectionUtil.isEmpty(existed) ? new Tj0ReplenishConfig() : existed.get(0);
                entity.setTj0SourceLocation(sourceLocation);
                entity.setTj0TargetLocation(targetLocation);
                entity.setTj0AutoCreateTime(trimToNull(row.getTj0AutoCreateTime()));
                entity.setTj0AutoCreateWeek(trimToNull(row.getTj0AutoCreateWeek()));
                entity.setTj0Notifier(trimToNull(row.getTj0Notifier()));
                saveList.add(entity);
            }
            this.addUpdate(saveList);
            return Result.success();
        } catch (Exception e) {
            TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
            return Result.error("导入Excel失败: " + e.getMessage());
        }
    }

    /**
     * 功能描述: 将驻地库自动补库配置实体转换为 Excel 导出模型。
     */
    private Tj0ReplenishConfigExcel toExcel(Tj0ReplenishConfig entity) {
        Tj0ReplenishConfigExcel excel = new Tj0ReplenishConfigExcel();
        BeanUtils.copyProperties(entity, excel);
        excel.setCreator(StrUtil.isBlank(entity.getCreatorName()) ? entity.getCreator() : entity.getCreatorName());
        LocalDateTime createAt = entity.getCreateAt();
        if (createAt != null) {
            excel.setCreateAt(createAt.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
        }
        return excel;
    }
}

2.4 配置仓储 Tj0ReplenishConfigRepository

java 复制代码
package com.nancal.wms.service.repository;

import com.nancal.base.service.repository.BaseRepository;
import com.nancal.wms.bean.entity.Tj0ReplenishConfig;

/**
 * 功能描述: 驻地库自动补库配置仓储。
 *
 * @author Bingo
 * @date 2026/09/18
 */
public interface Tj0ReplenishConfigRepository extends BaseRepository<Tj0ReplenishConfig> {
}

2.5 仓储实现 Tj0ReplenishConfigRepositoryImpl

java 复制代码
package com.nancal.wms.infrastructure.dao.impl;

import com.nancal.base.infrastructure.repository.SimpleRepository;
import com.nancal.lz.engine.Engine;
import com.nancal.wms.bean.entity.Tj0ReplenishConfig;
import com.nancal.wms.service.repository.Tj0ReplenishConfigRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;

/**
 * 功能描述: 驻地库自动补库配置仓储实现。
 *
 * @author Bingo
 * @date 2026/09/18
 */
@Repository
public class Tj0ReplenishConfigRepositoryImpl implements SimpleRepository<Tj0ReplenishConfig>, Tj0ReplenishConfigRepository {

    @Autowired
    private Engine engine;

    /**
     * 功能描述: 返回框架数据引擎。
     */
    @Override
    public Engine getEngine() {
        return engine;
    }
}

2.6 实体类 Tj0ReplenishConfig

java 复制代码
 package com.nancal.wms.bean.entity;

import com.nancal.base.bean.entity.ManufacturingActivity;
import lombok.Data;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Inheritance;
import javax.persistence.InheritanceType;
import javax.persistence.Table;

/**
 * 功能描述: 驻地库自动补库配置实体。
 *
 * @author Bingo
 * @date 2026/09/18
 */
@Data
@Entity
@Table(name = "tj0_replenish_config")
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
public class Tj0ReplenishConfig extends ManufacturingActivity {

    /** 自动创建补库调拨单时间,如:04:00:00。 */
    @Column(name = "tj0_auto_create_time")
    private String tj0AutoCreateTime;

    /** 自动执行周,如:周一、周二。 */
    @Column(name = "tj0_auto_create_week")
    private String tj0AutoCreateWeek;

    /** 通知人员。 */
    @Column(name = "tj0_notifier")
    private String tj0Notifier;

    /** 源库存地点。 */
    @Column(name = "tj0_source_location")
    private String tj0SourceLocation;

    /** 目标库存地点,如:5104。 */
    @Column(name = "tj0_target_location")   
    private String tj0TargetLocation;
}

2.6.2 配置导入导出模型 Tj0ReplenishConfigExcel

java 复制代码
package com.nancal.wms.bean.common;

import com.alibaba.excel.annotation.ExcelProperty;
import lombok.Data;

/**
 * 功能描述: 驻地库自动补库配置导入导出模型。
 *
 * @author Bingo
 * @date 2026/09/18
 */
@Data
public class Tj0ReplenishConfigExcel {

    @ExcelProperty(value = "源库存地点", index = 0)
    private String tj0SourceLocation;

    @ExcelProperty(value = "目标库存地点", index = 1)
    private String tj0TargetLocation;

    @ExcelProperty(value = "自动创建补库调拨单时间", index = 2)
    private String tj0AutoCreateTime;

    @ExcelProperty(value = "自动执行周", index = 3)
    private String tj0AutoCreateWeek;

    @ExcelProperty(value = "通知人员", index = 4)
    private String tj0Notifier;

    @ExcelProperty(value = "创建人", index = 5)
    private String creator;

    @ExcelProperty(value = "创建时间", index = 6)
    private String createAt;
}
相关推荐
小羊没烦恼!1 天前
微服务化的基石——持续集成
java·大数据·word·powerpoint·.net
俊昭喜喜里1 天前
java中的继承和多态的区别
java
小羊没烦恼!1 天前
初探性能优化——2个月到4小时的性能提升
java·开发语言·windows·算法·c#
譕痕1 天前
JSONObject与JSONArray封装数据格式区别
java·json
胡写代码1 天前
别再前后端各写一套表单校验了
java·后端
小鱼能吃糖1 天前
缺陷修复总览 · mall电商项目:5类缺陷,1个病根,4个业务域
java·电商
此时不提桶,更待何时1 天前
01-06-A-JVM排查实战详解
java·jvm
vipxieliang1 天前
ValidX 在 DDD 领域驱动设计中的实践
java·spring boot
ba_pi1 天前
mysql 查询所有表名并授权
java
ProcessOn官方账号1 天前
java函数式编程--入门基础
java·编程·函数式编程