From f07386f4aa7e5f01cbbf99784ad7a109b69af280 Mon Sep 17 00:00:00 2001 From: wuweihua <88888888> Date: Thu, 22 Oct 2026 14:46:52 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E6=A8=A1=E7=89=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../system/SysFileTemplateController.java | 101 ++++++ .../bms/system/domain/SysFileTemplate.java | 40 +++ .../system/mapper/SysFileTemplateMapper.java | 19 ++ .../service/ISysFileTemplateService.java | 27 ++ .../impl/BmsBizAttachmentServiceImpl.java | 49 ++- .../impl/SysFileTemplateServiceImpl.java | 226 +++++++++++++ .../mapper/system/SysFileTemplateMapper.xml | 85 +++++ bms-back/sql/system/sys_file_template.sql | 124 ++++++++ .../sql/system/sys_file_template_menu_fix.sql | 46 +++ .../sys_file_template_type_no_default.sql | 3 + bms-front/src/api/system/fileTemplate.js | 83 +++++ .../components/ExcelImportDialog/index.vue | 21 +- .../basic/components/CatalogPriceCreate.vue | 14 +- bms-front/src/views/price/billing/bill.vue | 13 +- .../set/components/create/StepDetails.vue | 4 + .../src/views/system/fileTemplate/index.vue | 300 ++++++++++++++++++ 16 files changed, 1129 insertions(+), 26 deletions(-) create mode 100644 bms-back/bms-admin/src/main/java/com/bms/web/controller/system/SysFileTemplateController.java create mode 100644 bms-back/bms-system/src/main/java/com/bms/system/domain/SysFileTemplate.java create mode 100644 bms-back/bms-system/src/main/java/com/bms/system/mapper/SysFileTemplateMapper.java create mode 100644 bms-back/bms-system/src/main/java/com/bms/system/service/ISysFileTemplateService.java create mode 100644 bms-back/bms-system/src/main/java/com/bms/system/service/impl/SysFileTemplateServiceImpl.java create mode 100644 bms-back/bms-system/src/main/resources/mapper/system/SysFileTemplateMapper.xml create mode 100644 bms-back/sql/system/sys_file_template.sql create mode 100644 bms-back/sql/system/sys_file_template_menu_fix.sql create mode 100644 bms-back/sql/system/sys_file_template_type_no_default.sql create mode 100644 bms-front/src/api/system/fileTemplate.js create mode 100644 bms-front/src/views/system/fileTemplate/index.vue diff --git a/bms-back/bms-admin/src/main/java/com/bms/web/controller/system/SysFileTemplateController.java b/bms-back/bms-admin/src/main/java/com/bms/web/controller/system/SysFileTemplateController.java new file mode 100644 index 00000000..70064c0c --- /dev/null +++ b/bms-back/bms-admin/src/main/java/com/bms/web/controller/system/SysFileTemplateController.java @@ -0,0 +1,101 @@ +package com.bms.web.controller.system; + +import java.util.List; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +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 com.bms.common.annotation.Log; +import com.bms.common.core.controller.BaseController; +import com.bms.common.core.domain.AjaxResult; +import com.bms.common.core.page.TableDataInfo; +import com.bms.common.enums.BusinessType; +import com.bms.system.domain.SysFileTemplate; +import com.bms.system.service.ISysFileTemplateService; + +/** System file template CRUD + OSS upload/download. */ +@RestController +@RequestMapping("/system/fileTemplate") +public class SysFileTemplateController extends BaseController +{ + @Autowired + private ISysFileTemplateService fileTemplateService; + + @PreAuthorize("@ss.hasPermi('system:fileTemplate:list')") + @GetMapping("/list") + public TableDataInfo list(SysFileTemplate query) + { + startPage(); + List list = fileTemplateService.selectSysFileTemplateList(query); + return getDataTable(list); + } + + @PreAuthorize("@ss.hasPermi('system:fileTemplate:query')") + @GetMapping("/{templateId}") + public AjaxResult getInfo(@PathVariable Long templateId) + { + return success(fileTemplateService.selectSysFileTemplateById(templateId)); + } + + /** Meta by file code (authenticated; used by import dialogs). */ + @GetMapping("/code/{fileCode}") + public AjaxResult getByCode(@PathVariable String fileCode) + { + return success(fileTemplateService.selectByFileCode(fileCode)); + } + + @PreAuthorize("@ss.hasPermi('system:fileTemplate:add')") + @Log(title = "File template", businessType = BusinessType.INSERT) + @PostMapping + public AjaxResult add(@Validated @RequestBody SysFileTemplate row) + { + row.setCreateBy(getUsername()); + fileTemplateService.insertSysFileTemplate(row); + return success(row); + } + + @PreAuthorize("@ss.hasPermi('system:fileTemplate:edit')") + @Log(title = "File template", businessType = BusinessType.UPDATE) + @PutMapping + public AjaxResult edit(@Validated @RequestBody SysFileTemplate row) + { + row.setUpdateBy(getUsername()); + return toAjax(fileTemplateService.updateSysFileTemplate(row)); + } + + @PreAuthorize("@ss.hasPermi('system:fileTemplate:remove')") + @Log(title = "File template", businessType = BusinessType.DELETE) + @DeleteMapping("/{templateIds}") + public AjaxResult remove(@PathVariable Long[] templateIds) + { + return toAjax(fileTemplateService.deleteSysFileTemplateByIds(templateIds)); + } + + @PreAuthorize("@ss.hasPermi('system:fileTemplate:upload')") + @Log(title = "File template upload", businessType = BusinessType.UPDATE) + @PostMapping("/upload") + public AjaxResult upload(@RequestParam("templateId") Long templateId, + @RequestParam("file") MultipartFile file) throws Exception + { + return success(fileTemplateService.uploadFile(templateId, file, getUsername())); + } + + /** + * Download by file_code. Login required; no admin perm so import pages can use it. + */ + @GetMapping("/download/{fileCode}") + public void download(@PathVariable String fileCode, HttpServletResponse response) throws Exception + { + fileTemplateService.downloadByFileCode(fileCode, response); + } +} diff --git a/bms-back/bms-system/src/main/java/com/bms/system/domain/SysFileTemplate.java b/bms-back/bms-system/src/main/java/com/bms/system/domain/SysFileTemplate.java new file mode 100644 index 00000000..b57dfcb0 --- /dev/null +++ b/bms-back/bms-system/src/main/java/com/bms/system/domain/SysFileTemplate.java @@ -0,0 +1,40 @@ +package com.bms.system.domain; + +import com.bms.common.core.domain.BaseEntity; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** System file template sys_file_template (OSS). */ +@Data +@EqualsAndHashCode(callSuper = true) +public class SysFileTemplate extends BaseEntity +{ + private static final long serialVersionUID = 1L; + + public static final String TYPE_SALES = "SALES"; + public static final String TYPE_REDLINE = "REDLINE"; + public static final String TYPE_PRODUCT = "PRODUCT"; + public static final String TYPE_CHARGE_WEIGHT = "CHARGE_WEIGHT"; + + public static final String CODE_PRODUCT_ALL = "PRODUCT_ALL"; + public static final String CODE_PRODUCT_COUNTRY = "PRODUCT_COUNTRY"; + public static final String CODE_REDLINE_ALL = "REDLINE_ALL"; + public static final String CODE_REDLINE_COUNTRY = "REDLINE_COUNTRY"; + public static final String CODE_SALES = "SALES"; + public static final String CODE_CHARGE_WEIGHT = "CHARGE_WEIGHT"; + + private Long templateId; + /** Dict: sys_file_template_type */ + private String templateType; + /** Unique business code */ + private String fileCode; + private String templateName; + private String fileName; + /** oss://bucket/key */ + private String filePath; + private Long fileSize; + private String contentType; + private String fileExtension; + /** 0 normal / 1 disable */ + private String status; +} diff --git a/bms-back/bms-system/src/main/java/com/bms/system/mapper/SysFileTemplateMapper.java b/bms-back/bms-system/src/main/java/com/bms/system/mapper/SysFileTemplateMapper.java new file mode 100644 index 00000000..61d03f90 --- /dev/null +++ b/bms-back/bms-system/src/main/java/com/bms/system/mapper/SysFileTemplateMapper.java @@ -0,0 +1,19 @@ +package com.bms.system.mapper; + +import java.util.List; +import com.bms.system.domain.SysFileTemplate; + +public interface SysFileTemplateMapper +{ + SysFileTemplate selectSysFileTemplateById(Long templateId); + + SysFileTemplate selectByFileCode(String fileCode); + + List selectSysFileTemplateList(SysFileTemplate query); + + int insertSysFileTemplate(SysFileTemplate row); + + int updateSysFileTemplate(SysFileTemplate row); + + int deleteSysFileTemplateByIds(Long[] templateIds); +} diff --git a/bms-back/bms-system/src/main/java/com/bms/system/service/ISysFileTemplateService.java b/bms-back/bms-system/src/main/java/com/bms/system/service/ISysFileTemplateService.java new file mode 100644 index 00000000..d90acf65 --- /dev/null +++ b/bms-back/bms-system/src/main/java/com/bms/system/service/ISysFileTemplateService.java @@ -0,0 +1,27 @@ +package com.bms.system.service; + +import java.util.List; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.web.multipart.MultipartFile; +import com.bms.system.domain.SysFileTemplate; + +public interface ISysFileTemplateService +{ + SysFileTemplate selectSysFileTemplateById(Long templateId); + + SysFileTemplate selectByFileCode(String fileCode); + + List selectSysFileTemplateList(SysFileTemplate query); + + int insertSysFileTemplate(SysFileTemplate row); + + int updateSysFileTemplate(SysFileTemplate row); + + int deleteSysFileTemplateByIds(Long[] templateIds); + + /** Upload / replace Excel file on OSS for an existing template row. */ + SysFileTemplate uploadFile(Long templateId, MultipartFile file, String operName) throws Exception; + + /** Download by file_code (enabled templates only). */ + void downloadByFileCode(String fileCode, HttpServletResponse response) throws Exception; +} diff --git a/bms-back/bms-system/src/main/java/com/bms/system/service/impl/BmsBizAttachmentServiceImpl.java b/bms-back/bms-system/src/main/java/com/bms/system/service/impl/BmsBizAttachmentServiceImpl.java index 3175a58b..2f9b75ec 100644 --- a/bms-back/bms-system/src/main/java/com/bms/system/service/impl/BmsBizAttachmentServiceImpl.java +++ b/bms-back/bms-system/src/main/java/com/bms/system/service/impl/BmsBizAttachmentServiceImpl.java @@ -1,32 +1,34 @@ package com.bms.system.service.impl; -import java.io.File; -import java.io.FileOutputStream; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.Collections; import java.util.List; import java.util.Locale; import org.apache.commons.io.FilenameUtils; -import org.apache.commons.io.IOUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; -import com.bms.common.config.BmsConfig; import com.bms.common.exception.ServiceException; import com.bms.common.utils.DateUtils; import com.bms.common.utils.StringUtils; -import com.bms.common.utils.file.FileUploadUtils; +import com.bms.common.utils.file.OssStorage; import com.bms.common.utils.uuid.IdUtils; import com.bms.system.domain.BmsBizAttachment; import com.bms.system.mapper.BmsBizAttachmentMapper; import com.bms.system.service.IBmsBizAttachmentService; +/** Business attachments: always store on OSS (file_path = oss://bucket/key). */ @Service public class BmsBizAttachmentServiceImpl implements IBmsBizAttachmentService { - private static final String SUB_DIR = "biz/attachment"; + private static final String OBJECT_DIR = "biz/attachment"; @Autowired private BmsBizAttachmentMapper attachmentMapper; + @Autowired + private OssStorage ossStorage; + @Override public BmsBizAttachment selectBmsBizAttachmentById(Long attachmentId) { @@ -59,29 +61,30 @@ public class BmsBizAttachmentServiceImpl implements IBmsBizAttachmentService ext = "bin"; } ext = ext.toLowerCase(Locale.ROOT); + String contentType = resolveContentType(ext); + String typeSeg = bizType.trim().toLowerCase(Locale.ROOT).replaceAll("[^a-z0-9_-]", "_"); + String objectKey = OBJECT_DIR + "/" + typeSeg + "/" + + DateUtils.datePath() + "/" + IdUtils.fastSimpleUUID() + "." + ext; - String uploadDir = BmsConfig.getUploadPath() + File.separator + SUB_DIR; - String pathName = DateUtils.datePath() + "/" + IdUtils.fastSimpleUUID() + "." + ext; - File dest = FileUploadUtils.getAbsoluteFile(uploadDir, pathName); - FileOutputStream fos = null; + Path temp = Files.createTempFile("biz-attach-", "." + ext); + String storageKey; try { - fos = new FileOutputStream(dest); - fos.write(bytes); + Files.write(temp, bytes); + storageKey = ossStorage.upload(temp, objectKey, contentType); } finally { - IOUtils.close(fos); + try { Files.deleteIfExists(temp); } catch (Exception ignore) { } } - String storedPath = FileUploadUtils.getPathFileName(uploadDir, pathName); BmsBizAttachment row = new BmsBizAttachment(); row.setBizType(bizType.trim()); row.setBizId(bizId); row.setFileName(originalName); - row.setFilePath(storedPath); + row.setFilePath(storageKey); row.setFileSize(Long.valueOf(bytes.length)); - row.setContentType(resolveContentType(ext)); + row.setContentType(contentType); row.setFileExtension(ext); row.setRemark(remark); row.setCreateBy(operName); @@ -99,6 +102,18 @@ public class BmsBizAttachmentServiceImpl implements IBmsBizAttachmentService { return "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"; } - return ""; + if ("pdf".equals(ext)) + { + return "application/pdf"; + } + if ("png".equals(ext)) + { + return "image/png"; + } + if ("jpg".equals(ext) || "jpeg".equals(ext)) + { + return "image/jpeg"; + } + return "application/octet-stream"; } } diff --git a/bms-back/bms-system/src/main/java/com/bms/system/service/impl/SysFileTemplateServiceImpl.java b/bms-back/bms-system/src/main/java/com/bms/system/service/impl/SysFileTemplateServiceImpl.java new file mode 100644 index 00000000..f68ecdb7 --- /dev/null +++ b/bms-back/bms-system/src/main/java/com/bms/system/service/impl/SysFileTemplateServiceImpl.java @@ -0,0 +1,226 @@ +package com.bms.system.service.impl; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.Locale; +import jakarta.servlet.http.HttpServletResponse; +import org.apache.commons.io.FilenameUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; +import com.bms.common.exception.ServiceException; +import com.bms.common.utils.DateUtils; +import com.bms.common.utils.StringUtils; +import com.bms.common.utils.file.FileUtils; +import com.bms.common.utils.file.OssStorage; +import com.bms.common.utils.uuid.IdUtils; +import com.bms.system.domain.SysFileTemplate; +import com.bms.system.mapper.SysFileTemplateMapper; +import com.bms.system.service.ISysFileTemplateService; + +@Service +public class SysFileTemplateServiceImpl implements ISysFileTemplateService +{ + private static final String OBJECT_DIR = "system/fileTemplate"; + + @Autowired + private SysFileTemplateMapper templateMapper; + + @Autowired + private OssStorage ossStorage; + + @Override + public SysFileTemplate selectSysFileTemplateById(Long templateId) + { + return templateMapper.selectSysFileTemplateById(templateId); + } + + @Override + public SysFileTemplate selectByFileCode(String fileCode) + { + if (StringUtils.isEmpty(fileCode)) + { + return null; + } + return templateMapper.selectByFileCode(fileCode.trim()); + } + + @Override + public List selectSysFileTemplateList(SysFileTemplate query) + { + return templateMapper.selectSysFileTemplateList(query); + } + + @Override + public int insertSysFileTemplate(SysFileTemplate row) + { + normalize(row); + assertFileCodeUnique(row.getFileCode(), null); + return templateMapper.insertSysFileTemplate(row); + } + + @Override + public int updateSysFileTemplate(SysFileTemplate row) + { + normalize(row); + assertFileCodeUnique(row.getFileCode(), row.getTemplateId()); + return templateMapper.updateSysFileTemplate(row); + } + + @Override + public int deleteSysFileTemplateByIds(Long[] templateIds) + { + if (templateIds == null || templateIds.length == 0) + { + return 0; + } + for (Long id : templateIds) + { + SysFileTemplate old = templateMapper.selectSysFileTemplateById(id); + if (old != null && StringUtils.isNotEmpty(old.getFilePath()) && old.getFilePath().startsWith("oss://")) + { + try { ossStorage.delete(old.getFilePath()); } catch (Exception ignore) { } + } + } + return templateMapper.deleteSysFileTemplateByIds(templateIds); + } + + @Override + public SysFileTemplate uploadFile(Long templateId, MultipartFile file, String operName) throws Exception + { + if (templateId == null) + { + throw new ServiceException("templateId required"); + } + if (file == null || file.isEmpty()) + { + throw new ServiceException("file empty"); + } + SysFileTemplate row = templateMapper.selectSysFileTemplateById(templateId); + if (row == null) + { + throw new ServiceException("template not found"); + } + String originalName = file.getOriginalFilename(); + if (StringUtils.isEmpty(originalName)) + { + originalName = "template.xlsx"; + } + String ext = FilenameUtils.getExtension(originalName); + if (StringUtils.isEmpty(ext)) + { + ext = "xlsx"; + } + ext = ext.toLowerCase(Locale.ROOT); + if (!"xlsx".equals(ext) && !"xls".equals(ext)) + { + throw new ServiceException("only xlsx/xls allowed"); + } + String contentType = "xlsx".equals(ext) + ? "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" + : "application/vnd.ms-excel"; + String typeSeg = StringUtils.isEmpty(row.getTemplateType()) ? "misc" + : row.getTemplateType().trim().toLowerCase(Locale.ROOT); + String objectKey = OBJECT_DIR + "/" + typeSeg + "/" + + DateUtils.datePath() + "/" + IdUtils.fastSimpleUUID() + "." + ext; + + Path temp = Files.createTempFile("sys-file-tpl-", "." + ext); + String storageKey; + try + { + file.transferTo(temp); + storageKey = ossStorage.upload(temp, objectKey, contentType); + } + finally + { + try { Files.deleteIfExists(temp); } catch (Exception ignore) { } + } + + String oldPath = row.getFilePath(); + SysFileTemplate upd = new SysFileTemplate(); + upd.setTemplateId(templateId); + upd.setFileName(originalName); + upd.setFilePath(storageKey); + upd.setFileSize(Long.valueOf(file.getSize())); + upd.setContentType(contentType); + upd.setFileExtension(ext); + upd.setUpdateBy(operName); + templateMapper.updateSysFileTemplate(upd); + + if (StringUtils.isNotEmpty(oldPath) && oldPath.startsWith("oss://") && !oldPath.equals(storageKey)) + { + try { ossStorage.delete(oldPath); } catch (Exception ignore) { } + } + return templateMapper.selectSysFileTemplateById(templateId); + } + + @Override + public void downloadByFileCode(String fileCode, HttpServletResponse response) throws Exception + { + SysFileTemplate row = selectByFileCode(fileCode); + if (row == null) + { + throw new ServiceException("template not found"); + } + if (!"0".equals(StringUtils.nvl(row.getStatus(), "0"))) + { + throw new ServiceException("template disabled"); + } + if (StringUtils.isEmpty(row.getFilePath())) + { + throw new ServiceException("template file not uploaded"); + } + String name = StringUtils.isNotEmpty(row.getFileName()) ? row.getFileName() + : (row.getFileCode() + ".xlsx"); + response.setContentType(StringUtils.isNotEmpty(row.getContentType()) + ? row.getContentType() + : "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); + FileUtils.setAttachmentResponseHeader(response, name); + if (row.getFilePath().startsWith("oss://")) + { + ossStorage.download(row.getFilePath(), response.getOutputStream()); + return; + } + // Legacy local path fallback + if (!FileUtils.checkAllowDownload(row.getFilePath())) + { + throw new ServiceException("template path illegal"); + } + String local = com.bms.common.config.BmsConfig.getProfile() + FileUtils.stripPrefix(row.getFilePath()); + FileUtils.writeBytes(local, response.getOutputStream()); + } + + private void normalize(SysFileTemplate row) + { + if (row == null) + { + return; + } + if (StringUtils.isNotEmpty(row.getTemplateType())) + { + row.setTemplateType(row.getTemplateType().trim().toUpperCase(Locale.ROOT)); + } + if (StringUtils.isNotEmpty(row.getFileCode())) + { + row.setFileCode(row.getFileCode().trim().toUpperCase(Locale.ROOT)); + } + if (StringUtils.isNotEmpty(row.getTemplateName())) + { + row.setTemplateName(row.getTemplateName().trim()); + } + } + + private void assertFileCodeUnique(String fileCode, Long excludeId) + { + if (StringUtils.isEmpty(fileCode)) + { + throw new ServiceException("fileCode required"); + } + SysFileTemplate exist = templateMapper.selectByFileCode(fileCode.trim()); + if (exist != null && (excludeId == null || !excludeId.equals(exist.getTemplateId()))) + { + throw new ServiceException("fileCode already exists"); + } + } +} diff --git a/bms-back/bms-system/src/main/resources/mapper/system/SysFileTemplateMapper.xml b/bms-back/bms-system/src/main/resources/mapper/system/SysFileTemplateMapper.xml new file mode 100644 index 00000000..da3aa97b --- /dev/null +++ b/bms-back/bms-system/src/main/resources/mapper/system/SysFileTemplateMapper.xml @@ -0,0 +1,85 @@ + + + + + + + + + + + + + + + + + + + + + + + + select template_id, template_type, file_code, template_name, file_name, file_path, + file_size, content_type, file_extension, status, remark, + create_by, create_time, update_by, update_time + from sys_file_template + + + + + + + + + + insert into sys_file_template ( + template_type, file_code, template_name, file_name, file_path, file_size, + content_type, file_extension, status, remark, create_by, create_time + ) values ( + #{templateType}, #{fileCode}, #{templateName}, #{fileName}, #{filePath}, #{fileSize}, + #{contentType}, #{fileExtension}, ifnull(#{status}, '0'), #{remark}, #{createBy}, sysdate() + ) + + + + update sys_file_template + + template_type = #{templateType}, + file_code = #{fileCode}, + template_name = #{templateName}, + file_name = #{fileName}, + file_path = #{filePath}, + file_size = #{fileSize}, + content_type = #{contentType}, + file_extension = #{fileExtension}, + status = #{status}, + remark = #{remark}, + update_by = #{updateBy}, + update_time = sysdate() + + where template_id = #{templateId} + + + + delete from sys_file_template where template_id in + #{id} + + diff --git a/bms-back/sql/system/sys_file_template.sql b/bms-back/sql/system/sys_file_template.sql new file mode 100644 index 00000000..e217d426 --- /dev/null +++ b/bms-back/sql/system/sys_file_template.sql @@ -0,0 +1,124 @@ +-- File template management (system) +SET NAMES utf8mb4; + +-- Dict: template type +INSERT INTO sys_dict_type (dict_name, dict_type, status, create_by, create_time, remark) +SELECT N'模板类型', 'sys_file_template_type', '0', 'admin', sysdate(), N'系统文件模板类型' +FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_dict_type WHERE dict_type = 'sys_file_template_type'); + +INSERT INTO sys_dict_data (dict_sort, dict_label, dict_value, dict_type, css_class, list_class, is_default, status, create_by, create_time, remark) +SELECT 1, N'销售价', 'SALES', 'sys_file_template_type', '', 'primary', 'N', '0', 'admin', sysdate(), NULL +FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_dict_data WHERE dict_type = 'sys_file_template_type' AND dict_value = 'SALES'); + +INSERT INTO sys_dict_data (dict_sort, dict_label, dict_value, dict_type, css_class, list_class, is_default, status, create_by, create_time, remark) +SELECT 2, N'红线价', 'REDLINE', 'sys_file_template_type', '', 'warning', 'N', '0', 'admin', sysdate(), NULL +FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_dict_data WHERE dict_type = 'sys_file_template_type' AND dict_value = 'REDLINE'); + +INSERT INTO sys_dict_data (dict_sort, dict_label, dict_value, dict_type, css_class, list_class, is_default, status, create_by, create_time, remark) +SELECT 3, N'产品价', 'PRODUCT', 'sys_file_template_type', '', 'success', 'N', '0', 'admin', sysdate(), NULL +FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_dict_data WHERE dict_type = 'sys_file_template_type' AND dict_value = 'PRODUCT'); + +INSERT INTO sys_dict_data (dict_sort, dict_label, dict_value, dict_type, css_class, list_class, is_default, status, create_by, create_time, remark) +SELECT 4, N'修改计费重模板', 'CHARGE_WEIGHT', 'sys_file_template_type', '', 'info', 'N', '0', 'admin', sysdate(), NULL +FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_dict_data WHERE dict_type = 'sys_file_template_type' AND dict_value = 'CHARGE_WEIGHT'); + +CREATE TABLE IF NOT EXISTS sys_file_template ( + template_id bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键', + template_type varchar(32) NOT NULL COMMENT '类型(字典 sys_file_template_type)', + file_code varchar(64) NOT NULL COMMENT '文件代码(业务唯一)', + template_name varchar(100) NOT NULL COMMENT '模板名称', + file_name varchar(255) DEFAULT NULL COMMENT '原始文件名', + file_path varchar(500) DEFAULT NULL COMMENT '存储路径 oss://bucket/key', + file_size bigint(20) DEFAULT NULL COMMENT '文件大小字节', + content_type varchar(128) DEFAULT NULL COMMENT 'Content-Type', + file_extension varchar(16) DEFAULT NULL COMMENT '扩展名', + status char(1) NOT NULL DEFAULT '0' COMMENT '状态 0正常 1停用', + remark varchar(500) DEFAULT NULL COMMENT '备注', + create_by varchar(64) DEFAULT '' COMMENT '创建者', + create_time datetime DEFAULT NULL COMMENT '创建时间', + update_by varchar(64) DEFAULT '' COMMENT '更新者', + update_time datetime DEFAULT NULL COMMENT '更新时间', + PRIMARY KEY (template_id), + UNIQUE KEY uk_sft_file_code (file_code), + KEY idx_sft_type (template_type) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='系统文件模板'; + +INSERT INTO sys_file_template (template_type, file_code, template_name, status, remark, create_by, create_time) +SELECT 'PRODUCT', 'PRODUCT_ALL', '产品价-多国导入模板', '0', + '产品 Excel 导入(多国/目录 sheet)', 'admin', sysdate() + FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_file_template WHERE file_code = 'PRODUCT_ALL'); + +INSERT INTO sys_file_template (template_type, file_code, template_name, status, remark, create_by, create_time) +SELECT 'PRODUCT', 'PRODUCT_COUNTRY', '产品价-单国导入模板', '0', + '产品 Excel 导入(单国)', 'admin', sysdate() + FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_file_template WHERE file_code = 'PRODUCT_COUNTRY'); + +INSERT INTO sys_file_template (template_type, file_code, template_name, status, remark, create_by, create_time) +SELECT 'REDLINE', 'REDLINE_ALL', '红线价-多国导入模板', '0', + '红线 Excel 导入(多国)', 'admin', sysdate() + FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_file_template WHERE file_code = 'REDLINE_ALL'); + +INSERT INTO sys_file_template (template_type, file_code, template_name, status, remark, create_by, create_time) +SELECT 'REDLINE', 'REDLINE_COUNTRY', '红线价-单国导入模板', '0', + '红线 Excel 导入(单国)', 'admin', sysdate() + FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_file_template WHERE file_code = 'REDLINE_COUNTRY'); + +INSERT INTO sys_file_template (template_type, file_code, template_name, status, remark, create_by, create_time) +SELECT 'SALES', 'SALES_ALL', '销售价-多国导入模板', '0', + '套餐/销售价 Excel 导入(多国)', 'admin', sysdate() + FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_file_template WHERE file_code = 'SALES_ALL'); + +INSERT INTO sys_file_template (template_type, file_code, template_name, status, remark, create_by, create_time) +SELECT 'SALES', 'SALES_COUNTRY', '销售价-单国导入模板', '0', + '套餐/销售价 Excel 导入(单国)', 'admin', sysdate() + FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_file_template WHERE file_code = 'SALES_COUNTRY'); + +INSERT INTO sys_file_template (template_type, file_code, template_name, status, remark, create_by, create_time) +SELECT 'CHARGE_WEIGHT', 'CHARGE_WEIGHT', '修改计费重导入模板', '0', + '计费列表批量修改计费重(空模板)', 'admin', sysdate() + FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_file_template WHERE file_code = 'CHARGE_WEIGHT'); + +-- Menu under system (parent_id=1) +INSERT INTO sys_menu ( + menu_id, menu_name, parent_id, order_num, path, component, query, route_name, + is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, remark +) +SELECT 2460, N'模板管理', 1, 12, + 'fileTemplate', 'system/fileTemplate/index', NULL, 'FileTemplate', + 1, 0, 'C', '0', '0', 'system:fileTemplate:list', 'documentation', 'admin', sysdate(), + N'公共导入/下载 Excel 模板(OSS)' + FROM DUAL + WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE menu_id = 2460 OR perms = 'system:fileTemplate:list'); + +UPDATE sys_menu SET parent_id = 1, order_num = 12, visible = '0', status = '0' + WHERE menu_id = 2460 AND (parent_id IS NULL OR parent_id <> 1); + +INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time) +SELECT 2461, N'查询', 2460, 1, '', NULL, 1, 0, 'F', '0', '0', 'system:fileTemplate:query', '#', 'admin', sysdate() + FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE menu_id = 2461); + +INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time) +SELECT 2462, N'新增', 2460, 2, '', NULL, 1, 0, 'F', '0', '0', 'system:fileTemplate:add', '#', 'admin', sysdate() + FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE menu_id = 2462); + +INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time) +SELECT 2463, N'修改', 2460, 3, '', NULL, 1, 0, 'F', '0', '0', 'system:fileTemplate:edit', '#', 'admin', sysdate() + FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE menu_id = 2463); + +INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time) +SELECT 2464, N'删除', 2460, 4, '', NULL, 1, 0, 'F', '0', '0', 'system:fileTemplate:remove', '#', 'admin', sysdate() + FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE menu_id = 2464); + +INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time) +SELECT 2465, N'上传', 2460, 5, '', NULL, 1, 0, 'F', '0', '0', 'system:fileTemplate:upload', '#', 'admin', sysdate() + FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE menu_id = 2465); + +INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time) +SELECT 2466, N'下载', 2460, 6, '', NULL, 1, 0, 'F', '0', '0', 'system:fileTemplate:download', '#', 'admin', sysdate() + FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE menu_id = 2466); + +INSERT INTO sys_role_menu (role_id, menu_id) +SELECT 1, m.menu_id + FROM (SELECT 2460 AS menu_id UNION SELECT 2461 UNION SELECT 2462 UNION SELECT 2463 + UNION SELECT 2464 UNION SELECT 2465 UNION SELECT 2466) m + WHERE NOT EXISTS (SELECT 1 FROM sys_role_menu rm WHERE rm.role_id = 1 AND rm.menu_id = m.menu_id); diff --git a/bms-back/sql/system/sys_file_template_menu_fix.sql b/bms-back/sql/system/sys_file_template_menu_fix.sql new file mode 100644 index 00000000..e444f99f --- /dev/null +++ b/bms-back/sql/system/sys_file_template_menu_fix.sql @@ -0,0 +1,46 @@ +SET NAMES utf8mb4; + +INSERT INTO sys_menu ( + menu_id, menu_name, parent_id, order_num, path, component, query, route_name, + is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, remark +) +SELECT 2460, N'模板管理', 1, 12, + 'fileTemplate', 'system/fileTemplate/index', NULL, 'FileTemplate', + 1, 0, 'C', '0', '0', 'system:fileTemplate:list', 'documentation', 'admin', sysdate(), + N'公共导入/下载 Excel 模板' + FROM DUAL + WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE menu_id = 2460 OR perms = 'system:fileTemplate:list'); + +UPDATE sys_menu + SET parent_id = 1, order_num = 12, path = 'fileTemplate', + component = 'system/fileTemplate/index', visible = '0', status = '0', + perms = 'system:fileTemplate:list', menu_type = 'C' + WHERE menu_id = 2460; + +INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time) +SELECT 2461, N'查询', 2460, 1, '#', '', 1, 0, 'F', '0', '0', 'system:fileTemplate:query', '#', 'admin', sysdate() + FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE menu_id = 2461); +INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time) +SELECT 2462, N'新增', 2460, 2, '#', '', 1, 0, 'F', '0', '0', 'system:fileTemplate:add', '#', 'admin', sysdate() + FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE menu_id = 2462); +INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time) +SELECT 2463, N'修改', 2460, 3, '#', '', 1, 0, 'F', '0', '0', 'system:fileTemplate:edit', '#', 'admin', sysdate() + FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE menu_id = 2463); +INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time) +SELECT 2464, N'删除', 2460, 4, '#', '', 1, 0, 'F', '0', '0', 'system:fileTemplate:remove', '#', 'admin', sysdate() + FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE menu_id = 2464); +INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time) +SELECT 2465, N'上传', 2460, 5, '#', '', 1, 0, 'F', '0', '0', 'system:fileTemplate:upload', '#', 'admin', sysdate() + FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE menu_id = 2465); +INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time) +SELECT 2466, N'下载', 2460, 6, '#', '', 1, 0, 'F', '0', '0', 'system:fileTemplate:download', '#', 'admin', sysdate() + FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE menu_id = 2466); + +INSERT INTO sys_role_menu (role_id, menu_id) +SELECT 1, m.menu_id + FROM (SELECT 2460 AS menu_id UNION SELECT 2461 UNION SELECT 2462 UNION SELECT 2463 + UNION SELECT 2464 UNION SELECT 2465 UNION SELECT 2466) m + WHERE NOT EXISTS (SELECT 1 FROM sys_role_menu rm WHERE rm.role_id = 1 AND rm.menu_id = m.menu_id); + +SELECT menu_id, menu_name, parent_id, path, component, visible, status, perms + FROM sys_menu WHERE menu_id BETWEEN 2460 AND 2466; \ No newline at end of file diff --git a/bms-back/sql/system/sys_file_template_type_no_default.sql b/bms-back/sql/system/sys_file_template_type_no_default.sql new file mode 100644 index 00000000..3a98be93 --- /dev/null +++ b/bms-back/sql/system/sys_file_template_type_no_default.sql @@ -0,0 +1,3 @@ +SET NAMES utf8mb4; +UPDATE sys_dict_data SET is_default = 'N' + WHERE dict_type = 'sys_file_template_type' AND dict_value = 'PRODUCT' AND is_default = 'Y'; \ No newline at end of file diff --git a/bms-front/src/api/system/fileTemplate.js b/bms-front/src/api/system/fileTemplate.js new file mode 100644 index 00000000..e41fdee2 --- /dev/null +++ b/bms-front/src/api/system/fileTemplate.js @@ -0,0 +1,83 @@ +import request from '@/utils/request' +import { saveAs } from 'file-saver' +import { blobValidate } from '@/utils/bms' +import errorCode from '@/utils/errorCode' +import { ElMessage } from 'element-plus' + +export function listFileTemplate(query) { + return request({ + url: '/system/fileTemplate/list', + method: 'get', + params: query + }) +} + +export function getFileTemplate(templateId) { + return request({ + url: '/system/fileTemplate/' + templateId, + method: 'get' + }) +} + +export function getFileTemplateByCode(fileCode) { + return request({ + url: '/system/fileTemplate/code/' + encodeURIComponent(fileCode), + method: 'get' + }) +} + +export function addFileTemplate(data) { + return request({ + url: '/system/fileTemplate', + method: 'post', + data + }) +} + +export function updateFileTemplate(data) { + return request({ + url: '/system/fileTemplate', + method: 'put', + data + }) +} + +export function delFileTemplate(templateIds) { + return request({ + url: '/system/fileTemplate/' + templateIds, + method: 'delete' + }) +} + +export function uploadFileTemplate(templateId, file) { + const form = new FormData() + form.append('templateId', templateId) + form.append('file', file) + return request({ + url: '/system/fileTemplate/upload', + method: 'post', + data: form, + headers: { 'Content-Type': 'multipart/form-data', repeatSubmit: false }, + timeout: 120000 + }) +} + +/** Download template by file_code (shared by import dialogs). */ +export async function downloadFileTemplate(fileCode, fallbackName) { + const code = String(fileCode || '').trim() + if (!code) throw new Error('fileCode required') + const data = await request({ + url: '/system/fileTemplate/download/' + encodeURIComponent(code), + method: 'get', + responseType: 'blob', + timeout: 120000 + }) + if (!blobValidate(data)) { + const text = await data.text() + const rsp = JSON.parse(text) + throw new Error(errorCode[rsp.code] || rsp.msg || errorCode.default || 'download failed') + } + const name = (fallbackName || code) + '.xlsx' + saveAs(new Blob([data]), name) + return true +} diff --git a/bms-front/src/components/ExcelImportDialog/index.vue b/bms-front/src/components/ExcelImportDialog/index.vue index 81eee8fc..503fe00b 100644 --- a/bms-front/src/components/ExcelImportDialog/index.vue +++ b/bms-front/src/components/ExcelImportDialog/index.vue @@ -198,6 +198,11 @@ const props = defineProps({ type: Function, default: null }, + // System template file_code (sys_file_template); preferred over static templateUrl + templateFileCode: { + type: String, + default: '' + }, // 确定按钮文字 submitText: { type: String, @@ -229,7 +234,9 @@ const uploadUrl = computed(() => { return import.meta.env.VITE_APP_BASE_API + props.action + '?' + query.toString() }) -const hasTemplate = computed(() => !!(props.templateDownloadHandler || props.templateUrl || props.templateAction)) +const hasTemplate = computed(() => !!( + props.templateDownloadHandler || props.templateFileCode || props.templateUrl || props.templateAction +)) // 打开对话框(供父组件通过 ref 调用) function open() { @@ -254,7 +261,7 @@ function handleClosed() { selectedFiles.value = [] } -// 下载模板:自定义 handler > 静态文件 > 接口 proxy.download +// Download: custom handler > system fileCode (OSS) > static url > proxy.download async function handleDownloadTemplate() { if (typeof props.templateDownloadHandler === 'function') { try { @@ -265,6 +272,16 @@ async function handleDownloadTemplate() { } return } + if (props.templateFileCode) { + try { + const { downloadFileTemplate } = await import('@/api/system/fileTemplate') + await downloadFileTemplate(props.templateFileCode, props.templateFileName) + } catch (e) { + const tip = (e && e.message) ? e.message : '下载模板失败' + proxy.$modal.msgError(tip) + } + return + } if (props.templateUrl) { const link = document.createElement('a') link.href = props.templateUrl diff --git a/bms-front/src/views/price/basic/components/CatalogPriceCreate.vue b/bms-front/src/views/price/basic/components/CatalogPriceCreate.vue index a77078b4..e38799bd 100644 --- a/bms-front/src/views/price/basic/components/CatalogPriceCreate.vue +++ b/bms-front/src/views/price/basic/components/CatalogPriceCreate.vue @@ -103,7 +103,7 @@ :title="titleAll" width="560px" :show-update-support="false" - template-url="/templates/product/product-import-all-template.xlsx" + :template-file-code="tplCodeAll" :template-file-name="tplAll" :import-desc="allImportDesc" :file-limit="5" @@ -117,7 +117,7 @@ :show-update-support="false" :before-submit="validateCountryImport" :import-desc="countryImportDesc" - template-url="/templates/product/product-import-country-template.xlsx" + :template-file-code="tplCodeCountry" :template-file-name="tplCtry" :file-limit="5" :submit-handler="runImportCountry" @@ -207,8 +207,14 @@ const labCountry = '目的国家' const labPickCountry = '请选择国家' const labImporting = '正在导入' const labSaving = '正在保存' -const tplAll = '产品多国家价表导入模板' -const tplCtry = '产品按国家导入模板' +const tplAll = computed(() => (props.kind === 'product' + ? '产品多国家价表导入模板' + : '红线价多国导入模板')) +const tplCtry = computed(() => (props.kind === 'product' + ? '产品按国家导入模板' + : '红线价单国导入模板')) +const tplCodeAll = computed(() => (props.kind === 'product' ? 'PRODUCT_ALL' : 'REDLINE_ALL')) +const tplCodeCountry = computed(() => (props.kind === 'product' ? 'PRODUCT_COUNTRY' : 'REDLINE_COUNTRY')) const titleAll = props.kind === 'product' ? '产品 Excel 导入' : '红线价 Excel 导入' const titleCtry = '按国家导入' const headerTitle = props.kind === 'product' ? '编辑产品价格' : '编辑红线价' diff --git a/bms-front/src/views/price/billing/bill.vue b/bms-front/src/views/price/billing/bill.vue index 31c587f6..a757ceb8 100644 --- a/bms-front/src/views/price/billing/bill.vue +++ b/bms-front/src/views/price/billing/bill.vue @@ -170,6 +170,7 @@ title="批量修改计费重" width="520px" action="/price/billing/bill/chargeWeight/importData" + template-file-code="CHARGE_WEIGHT" template-file-name="批量修改计费重导入模板" :show-update-support="false" :import-desc="chargeWeightImportDesc" @@ -641,10 +642,16 @@ function buildChargeWeightTemplateQuery() { async function downloadChargeWeightImportTemplate() { const payload = buildChargeWeightTemplateQuery() - if (payload.fillByFilter && !hasBatchFeeQuery(tableRef.value?.getQueryParams?.() || {})) { - throw new Error('请先设置列表筛选条件') + // Checked: backend fills orderNos from list filters. Unchecked: OSS template CHARGE_WEIGHT. + if (payload.fillByFilter) { + if (!hasBatchFeeQuery(tableRef.value?.getQueryParams?.() || {})) { + throw new Error('请先设置列表筛选条件') + } + await downloadChargeWeightTemplate(payload) + return } - await downloadChargeWeightTemplate(payload) + const { downloadFileTemplate } = await import('@/api/system/fileTemplate') + await downloadFileTemplate('CHARGE_WEIGHT', '批量修改计费重导入模板') } function openChargeWeightImportAttach() { diff --git a/bms-front/src/views/price/set/components/create/StepDetails.vue b/bms-front/src/views/price/set/components/create/StepDetails.vue index 06b76e15..c5faf801 100644 --- a/bms-front/src/views/price/set/components/create/StepDetails.vue +++ b/bms-front/src/views/price/set/components/create/StepDetails.vue @@ -144,6 +144,8 @@ :show-update-support="false" :import-desc="importDesc" :file-limit="5" + template-file-code="SALES_ALL" + template-file-name="销售价多国导入模板" :submit-handler="runImportAsync" @success="onImportSuccess" /> @@ -155,6 +157,8 @@ :show-update-support="false" :import-desc="countryImportDesc" :file-limit="5" + template-file-code="SALES_COUNTRY" + template-file-name="销售价单国导入模板" :before-submit="validateCountryImport" :submit-handler="runCountryImportAsync" @success="onImportSuccess" diff --git a/bms-front/src/views/system/fileTemplate/index.vue b/bms-front/src/views/system/fileTemplate/index.vue new file mode 100644 index 00000000..301b27b6 --- /dev/null +++ b/bms-front/src/views/system/fileTemplate/index.vue @@ -0,0 +1,300 @@ + + + + +