金昌市网站建设_网站建设公司_VS Code_seo优化
2025/12/19 17:11:14 网站建设 项目流程

这份文档完成于11月中旬,前面一个月左右基本都是写项目

一些代码是参考以及询问某些佬的写法,本苦手尤为感谢大佬们的帮助

项目无前端代码demo

下面就是正片了,主要还是个人存档使用,可能其他人参考不到,见谅

第一次大项目总结

包括学习的一些代码方法以及对项目的总结

茼蒿(车万)进销存(笑)

首先,这是一个前后端分离多语言开发(java与c++)的微服务架构项目,

项目介绍

《茼蒿进销存》是一款面向中小企业的高效、省心、高性价比的在线进销存(purchasing, sales, inventory)。旨在打通企业从采购、库存、销售等等到财务核算的核心业务流程,通过提供精准、实时、可视化的数据洞察,是促进企业发展的重要组成部分,是企业经营管理中的重要环节。

项目特点:操作简单、上网就能查库存、下销售单、采购管理、库存管理、库存管理/仓库管理等, 一应俱全;库存集中管理,管理员可以给不同的人员分配不同的数据权限和功能权限;智能补货, 保证库存充足,价格记忆,避免报价混乱,一键成本重算,解决多批次产品库存成本不同的问题;

作为一个相对比较符合市面上的web项目,以下是本次项目使用的架构:SpringCloud Alibaba微服务生态 + Vue3 + Docker(容器化)+ Redis(缓存)+ RocketMQ(消息代理)

个人任务

配置nacos服务注册中心与服务配置信息与线上服务器部署

首页界面的数据获取还有登录界面的数据获取等等(见下面)

-------------------

首页模块

总的来说都是crud,每个数据块都是获取来自库中的数据,大多数据都是使用dto的类进行返回,贴一个自己写的

@Data
@AllArgsConstructor
@ApiModel("首页-数据概览-收款单统计模型")
public class ImyCountsDTO {/*** 统计展示的日期*/@ApiModelProperty(value = "日期",example = "2025-10-18")LocalDate date;/**每日所有收款的总金额*/@ApiModelProperty(value = "每日所有收款的总金额",example = "114514.00000")BigDecimal value;
}

首页后端控制使用一个总的汇总信息的方法,调用各个展示的部分

最后前端从这个总和的dto中进行渲染

Login模块

包括菜单展示,获取用户,授权刷新退出登录5个方法,有点难度

菜单模块

本人负责的模块,最为熟悉,本质还是crud,之后对查询的数据先排序后换作树形结构

关于树形结构,返回数据的dto中包含了chidren节点的类

具体如下

public class MenuTreeVO{@ApiModelProperty(value = "序号", example = "001")private String id;@ApiModelProperty(value = "菜单名称", example = "主页")private String name;@ApiModelProperty(value = "路由路径", example = "/home")private String href;@ApiModelProperty(value = "菜单类型", example = "false")private String hasReport;@ApiModelProperty(value = "报表页面的路径", example = "/purchase-booking-report")private String reportHref;@ApiModelProperty(value = "节点包含的子节点")private List<MenuTreeVO> children;public MenuTreeVO(String id, String name, String href, String hasReport) {this.id = id;this.name = name;this.href = href;this.hasReport = hasReport;this.children = new ArrayList<MenuTreeVO>();}public void addChild(MenuTreeVO m){this.children.add(m);}
}

最后这个MenuTreeVo中会装载对应的子节点

关于排序

    private void sortMenuTree(List<MenuTreeVO> menuTree, Map<String, Menu> originalMenus) {// 对当前层级菜单按 sort 排序menuTree.sort((m1, m2) -> {Menu menu1 = originalMenus.get(m1.getId());Menu menu2 = originalMenus.get(m2.getId());return menu1.getSort().compareTo(menu2.getSort());});// 递归对子菜单排序for (MenuTreeVO menu : menuTree) {if (menu.getChildren() != null && !menu.getChildren().isEmpty()) {sortMenuTree(menu.getChildren(), originalMenus);}}}

上半部分的landom表达式中将Menu中的sort这个值取出,之后m1对m2进行比较,要是m1大于m2就返回正数,否则负数,之后的结果就是正序

其他地方都比较好理解了

授权登录

基于Spring Security OAuth2,外加验证码验证

@Resourceprivate CaptchaBusinessService captchaBusinessService;@ApiOperation(value = "授权登录")@PostMapping("auth-login")@Overridepublic JsonVO<Oauth2TokenDTO> authLogin(LoginDTO loginDTO) {// TODO:未实现验证码验证,注意:接入验证码后需要加一个启动或关闭验证码验证功能的开关,此开关可以在Nacos配置中心中动态的调整// 验证码二次校验if (captchaEnabled){//校验LoginDTOif(StrUtil.isBlank(loginDTO.getCode())){JsonVO.create(null, ResultStatus.FAIL.getCode(), "验证码不能为空");}//校验验证码ResponseModel responseModel = captchaBusinessService.verification(loginDTO.getCode());if (!responseModel.isSuccess()) {// TODO: 可能错误码冲突return JsonVO.create(null,Integer.parseInt(responseModel.getRepCode()),responseModel.getRepMsg());}//验证码缓存删除 源码已经实现 详情见BlockPuzzleCaptchaServiceImpl//二次校验也是同理详情见BlockPuzzleCaptchaServiceImpl和DefaultCaptchaServiceImpl
//            redisTemplate.delete(RedisConstant.RUNNING_CAPTCHA + )}// 账号密码认证Map<String, String> params = new HashMap<>(5);params.put("grant_type", "password");//密码授权方式params.put("client_id", clientId);//客户端idparams.put("client_secret", clientPassword);//客户端密钥params.put("username", loginDTO.getUsername());params.put("password", loginDTO.getPassword());Oauth2Token oauth2Token = oAuthService.postAccessToken(params);// 认证失败if (oauth2Token.getErrorMsg() != null) {return JsonVO.create(null, ResultStatus.FAIL.getCode(), oauth2Token.getErrorMsg());}// TODO:未实现认证成功后如何实现注销凭证(如记录凭证到内存数据库)Oauth2TokenDTO tokenDTO = BeanUtil.toBean(oauth2Token, Oauth2TokenDTO.class);//转换对象//缓存token到白名单redisTemplate.opsForValue().set(RedisConstant.LOGOUT_TOKEN_PREFIX + tokenDTO.getToken(),RedisConstant.TOKEN_STATUS_ACTIVE,oauth2Token.getExpiresIn(),TimeUnit.SECONDS);// 响应认证成功数据return JsonVO.success(tokenDTO);}

总体逻辑就是通过CaptchaBusinessService(防刷,区别人机,保护等等)保证二次验证码通过,之后将信息传输到OAuthService中进行OAuth2令牌获取,之后如果认证成功缓存token到(redis实现)白名单(这里没有实现黑名单,默认不在白名单的就是在黑名单1)

补充:oauth2Token中的认证实际上就是判断是否有错误,否则就返回null(都null了,那我也没意见了)

刷新登录

    @ApiOperation(value = "刷新登录")@PostMapping("refresh-token")@Overridepublic JsonVO<Oauth2TokenDTO> refreshToken(RefreshTokenDTO refreshTokenDTO) {// TODO:未实现注销凭证验证// 注销凭证验证try {jwtComponent.defaultRsaVerify(refreshTokenDTO.getToken());}catch (Exception e){if (!(e instanceof JwtExpiredException))return JsonVO.create(null, ResultStatus.FAIL.getCode(),e.getMessage());}// 刷新凭证Map<String, String> params = new HashMap<>(4);params.put("grant_type", "refresh_token");params.put("client_id", clientId);params.put("client_secret", clientPassword);params.put("refresh_token", refreshTokenDTO.getRefreshToken());Oauth2Token oauth2Token = oAuthService.postAccessToken(params);// 刷新失败if (oauth2Token.getErrorMsg() != null) {return JsonVO.create(null, ResultStatus.FAIL.getCode(), oauth2Token.getErrorMsg());}Oauth2TokenDTO tokenDTO = BeanUtil.toBean(oauth2Token, Oauth2TokenDTO.class);// TODO:未实现刷新成功后如何刷新注销凭证(如删除与更新内存数据库)// 刷新注销凭证(如删除与更新内存数据库),更新accessTokenredisTemplate.opsForValue().getOperations().delete(RedisConstant.LOGOUT_TOKEN_PREFIX+refreshTokenDTO.getToken());redisTemplate.opsForValue().set(RedisConstant.LOGOUT_TOKEN_PREFIX+oauth2Token.getToken(),RedisConstant.TOKEN_STATUS_ACTIVE,oauth2Token.getExpiresIn(), TimeUnit.SECONDS);// 响应刷新成功数据return JsonVO.success(tokenDTO);}

通过传入一个token,首先先注销对应的旧的信息(jwt令牌),之后再跟重新登录一样再加入一个新的令牌(同样还是使用redis,实现就是除旧迎新)

获取用户

@ApiOperation(value = "获取当前用户")@GetMapping("current-user")@Overridepublic JsonVO<LoginVO> getCurrUser() {UserDTO currentUser;try {currentUser = userHolder.getCurrentUser();} catch (Exception e) {return JsonVO.create(null, ResultStatus.FAIL.getCode(), e.getMessage());}if (currentUser == null) {return JsonVO.fail(null);} else {// TODO:这里需要根据业务需求,重新实现LoginVO vo = new LoginVO();BeanUtil.copyProperties(currentUser, vo);return JsonVO.success(vo);}}

需要先登录之后才能获取,直接返回分装即可

这里可以;了解下如何获取用户

    @ResourceJwtComponent jwtComponent;/*** 从请求头中获取用户信息* @return 用户信息* @throws Exception 解析失败抛出异常*/@SuppressWarnings("MismatchedQueryAndUpdateOfCollection")public UserDTO getCurrentUser() throws Exception {// 从Header中获取用户信息ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();if (servletRequestAttributes == null) {return null;}HttpServletRequest request = servletRequestAttributes.getRequest();String userStr = request.getHeader("user");// 不是通过网关过来的,那么执行解析验证JWTif (userStr == null) {//从token中解析用户信息并设置到Header中去log.info("Authrization = "+ request.getHeader("Authorization"));String realToken = request.getHeader("Authorization").replace("Bearer ", "");userStr = jwtComponent.defaultRsaVerify(realToken);} else {userStr = UriEncoder.decode(userStr);}JSONObject userJsonObject = new JSONObject(userStr);// HARD_CODE 在没有办法使用token时候可以修改这里的代码伪造用户信息,注意伪造用户代码不要提交到仓库中/*userJsonObject = new JSONObject();userJsonObject.putOnce("id", 1);userJsonObject.putOnce("user_name", "王麻子");ArrayList<Object> roles = new ArrayList<>();roles.add("ROLE_ADMIN");userJsonObject.putOnce("authorities", roles);userJsonObject.putOnce("avatar","https://img95.699pic.com/photo/60112/3125.jpg_wh860.jpg")  ;
*/// FIXME: 如果要扩展用户信息,需要修改这里的代码
//  1.修改添加头像avatar 去除isenabled  -- ZGjie20return UserDTO.builder().id(Convert.toStr(userJsonObject.get("id"))).username(userJsonObject.getStr("user_name"))//.isEnabled(Convert.toByte(1)).avatar(Convert.toStr(userJsonObject.get("avatar"))).roles(Convert.toList(String.class, userJsonObject.get("authorities"))).frameId(Convert.toStr(userJsonObject.get("frameId"))).frameName(Convert.toStr(userJsonObject.get("frameName"))).build();}/*** 从请求头中获取当前请求的token* @return 没有获取到返回null*/public String getCurrentToken() {ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();if (servletRequestAttributes == null) {return null;}HttpServletRequest request = servletRequestAttributes.getRequest();String token = request.getHeader("Authorization");if (StringUtils.isEmpty(token)) {return null;}return token.replace("Bearer ", "");}

通过验证请求头携带的参数,再通过jwt令牌验证之后创建user对象,下面的token是一个道理

退出登录

@ApiOperation(value = "退出登录")@GetMapping("logout")@Overridepublic JsonVO<String> logout() {// 获取当前请求的tokenString token = userHolder.getCurrentToken();if (token != null){// TODO:登出逻辑,需要配合登录逻辑实现// 构造 Redis 中存储 token 的 keyString redisKey = RedisConstant.LOGOUT_TOKEN_PREFIX + token;// 删除 Redis 中的 token 记录redisTemplate.delete(redisKey);return JsonVO.success("退出成功");}return JsonVO.fail("获取凭证失败,退出失败");}

这里就使用到了获取token,之后删除redis中的数据就算退出登录了

系统配置

字典管理

这个部分包括两种查找,新增,修改,删除,新增

查找本质都是crud

    <resultMap id="dictDTOResultMap" type="com.zeroone.star.project.dto.j1.sysconfig.DictDTO"><id column="id" property="id"/><result column="tid" property="tid"/><result column="name" property="name"/><result column="value" property="value"/><result column="remark" property="remark"/><result column="type_name" property="type_name"/></resultMap><select id="selectByCondition" resultMap="dictDTOResultMap">SELECTd.id,d.tid,d.name,d.value,d.remark,dt.name AS type_nameFROMdict dLEFT JOINdict_type dt ON d.tid = dt.id<where><if test="tid != null and tid != ''">AND d.tid = #{tid}</if><if test="name != null and name != ''">AND d.name LIKE CONCAT('%', #{name}, '%')</if><if test="value != null and value != ''">AND d.value LIKE CONCAT('%', #{value}, '%')</if></where></select>

有一部分需要进行分页,可以使用mq中的分页对象进行分页

    public PageDTO<DictDTO> selectByCondition(DictQuery condition){Page<DictDTO> page = new Page<>(condition.getPageIndex(), condition.getPageSize());baseMapper.selectPageByCondition(page, condition);PageDTO<DictDTO> pageDTO = PageDTO.create(page);System.out.println(pageDTO);//测试没来得及删的,可以换logreturn pageDTO;}

剩下的crud挑点重要的说说

修改和新增中,需要验证表中是否存在已存在过的字典,根据id啥的来判断,这里不做阐述

模板管理

主要说下有关fastdfs的地方

首先要知道这个是干啥的:分布式文件系统的文件的存储管理等等

这个用删除模板来举例子

@Override
public int deleteTemplate(String id) {TmplImport tmplImport=tmplImportMapper.selectById(id);if(tmplImport==null){return 0;}try{String group=tmplImport.getSavePath().split(",")[0];String storageId=tmplImport.getSavePath().split(",")[1];FastDfsFileInfo info=FastDfsFileInfo.builder().group(group).storageId(storageId).build();fastDfsClientComponent.deleteFile(info);}catch (Exception e){e.printStackTrace();}return tmplImportMapper.deleteById(id);
}

这里的fastdfs都是分装过的

@Component
@EnableFastdfsClient
public class FastDfsClientComponent {@Resourceprivate FastdfsClientService remoteService;/*** 文件上传* @param fileName 文件全路径* @param extName  文件扩展名,不包含(.)* @return 上传结果信息* @throws Exception 存储失败异常*/public FastDfsFileInfo uploadFile(String fileName, String extName) throws Exception {String[] info = remoteService.autoUpload(FileUtils.readFileToByteArray(new File(fileName)), extName);if (info != null) {return FastDfsFileInfo.builder().group(info[0]).storageId(info[1]).build();}return null;}/*** 文件上传* @param fileName 文件全路径* @return 上传结果信息* @throws Exception 存储失败异常*/public FastDfsFileInfo uploadFile(String fileName) throws Exception {return uploadFile(fileName, null);}/*** 上传文件* @param fileContent 文件的内容,字节数组* @param extName     文件扩展名* @return 上传结果信息* @throws Exception 存储失败异常*/public FastDfsFileInfo uploadFile(byte[] fileContent, String extName) throws Exception {String[] info = remoteService.autoUpload(fileContent, extName);if (info != null) {return FastDfsFileInfo.builder().group(info[0]).storageId(info[1]).build();}return null;}/*** 上传文件* @param fileContent 件的内容,字节数组* @return 上传结果信息 [0]:服务器分组,[1]:服务器ID* @throws Exception 存储失败异常*/public FastDfsFileInfo uploadFile(byte[] fileContent) throws Exception {return uploadFile(fileContent, null);}/*** 文件下载* @param info 文件信息* @return 下载数据* @throws Exception 异常信息*/public byte[] downloadFile(FastDfsFileInfo info) throws Exception {return remoteService.download(info.getGroup(), info.getStorageId());}/*** 删除文件* @param info 文件信息* @return 删除结果 0表示删除成功* @throws Exception 异常信息*/public int deleteFile(FastDfsFileInfo info) throws Exception {return remoteService.delete(info.getGroup(), info.getStorageId());}/*** 解析成url地址* @param info      文件信息* @param urlPrefix 如:<a href="#">http://ip:port</a>* @param isToken   是否带防盗链* @return 获取失败返回null*/public String fetchUrl(FastDfsFileInfo info, String urlPrefix, boolean isToken) {try {if (isToken) {return remoteService.autoDownloadWithToken(info.getGroup(), info.getStorageId(), urlPrefix);} else {return remoteService.autoDownloadWithoutToken(info.getGroup(), info.getStorageId(), urlPrefix);}} catch (Exception e) {e.printStackTrace();}return null;}
}

根据打包好的fastdfs中的内容来哦写还是比较简单的嘛

菜单管理

获取菜单跟登录界面的获取菜单有点像,也是获取本个节点之后向下递归或者向上获取节点,这里忽略

注意一下获取菜单需要使用到redis进行缓存,也就是保存界面

到这里也算是说完了我们小组负责的部分了,还是比较简单的,但是为啥实际实现却如此困难呢(笑)


仓库管理

d03f1d9e98b0870bbe337adf29df3ecc

5a96f7caefff089b643d584800f1771f

库存查询

    <!--查询商品的基本信息--><select id="selectInventoryBaseList" resultType="com.zeroone.star.project.dto.j2.store.InventoryListDTO">SELECTg.id,g.name,g.threshold,g.stock,g.number,g.spec,g.brand,g.unit,g.code,g.`data` as remark,c.id as "categoryId",c.name as "categoryName",<!-- 使用子查询计算总库存 -->COALESCE((SELECT SUM(r2.nums)FROM room r2WHERE r2.goods = g.id<if test="query.warehouseId != null and query.warehouseId.size() > 0">AND r2.warehouse IN<foreach collection="query.warehouseId" item="id" open="(" close=")" separator=",">#{id}</foreach></if>), 0) as totalStockFROM goods gLEFT JOIN category c ON g.category = c.id<!--查询条件--><where><if test="query.goodsName != null and query.goodsName != ''">AND g.name LIKE CONCAT('%', #{query.goodsName},'%')</if><if test="query.goodsNumber != null and query.goodsNumber != ''">AND g.number LIKE CONCAT('%', #{query.goodsNumber},'%')</if><if test="query.goodsSpec != null and query.goodsSpec != ''">AND g.spec LIKE CONCAT('%',#{query.goodsSpec},'%')</if><if test="query.goodsCategoryIds != null and query.goodsCategoryIds.size() > 0">AND g.category IN<foreach collection="query.goodsCategoryIds" item="categoryId" open="(" close=")" separator=",">#{categoryId}</foreach></if><if test="query.goodsBrand != null and query.goodsBrand != ''">AND g.brand = #{query.goodsBrand}</if><if test="query.goodsCode != null and query.goodsCode != ''">AND g.code LIKE CONCAT('%',#{query.goodsCode},'%')</if><if test="query.goodsRemark != null and query.goodsRemark != ''">AND g.data LIKE CONCAT('%',#{query.goodsRemark},'%')</if></where>ORDER BY g.id DESC</select>

sql如上,这里要介绍个字段:COALESCE(a,b)中的字段如果为null就转换成b

导出库存数据excel

/*** <p>* 描述:EasyExcel操作组件* </p>* <p>版权:&copy;01星球</p>* <p>地址:01星球总部</p>* @author 阿伟学长* @version 1.0.0*/
@Component
public class EasyExcelComponent {/*** 定义每个页签存储的数据量*/private static final int MAX_COUNT_PER_SHEET = 5000;/*** 生成 Excel* @param path      Excel 存储路径* @param sheetName sheet名称* @param clazz     存储的数据类型* @param dataList  存储数据集合* @param <T>       生成元素实体类类型*/public <T> void generateExcel(String path, String sheetName, Class<T> clazz, List<T> dataList) {EasyExcel.write(path, clazz).sheet(sheetName).doWrite(dataList);}/*** 解析Excel* @param path  解析的Excel的路径* @param clazz 存储的数据类型* @param <T>   解析元素实体类类型* @return 解析后的数据集合*/public <T> List<T> parseExcel(String path, Class<T> clazz) {ExcelReadListener<T> listener = new ExcelReadListener<>();//sheet()方法表示读取所有的sheet//doRead() 表示表示执行读取动作EasyExcel.read(path, clazz, listener).sheet().doRead();return listener.getDataList();}/*** 导出到输出流* @param sheetName sheet名称* @param os        输出流* @param clazz     导出数据类型* @param dataList  导出的数据集* @param <T>       生成元素实体类类型* @throws IOException IO异常*/public <T> void export(String sheetName, OutputStream os, Class<T> clazz, List<T> dataList) throws IOException {ExcelWriterBuilder builder = EasyExcel.write(os, clazz);ExcelWriter writer = builder.build();//计算总页数int sheetCount = dataList.size() / MAX_COUNT_PER_SHEET;sheetCount = dataList.size() % MAX_COUNT_PER_SHEET == 0 ? sheetCount : sheetCount + 1;//循环构建分页for (int i = 0; i < sheetCount; i++) {//创建一个页签WriteSheet sheet = new WriteSheet();sheet.setSheetNo(i);sheet.setSheetName(sheetName + (i + 1));//设置数据起始位置int start = i * MAX_COUNT_PER_SHEET;int end = (i + 1) * MAX_COUNT_PER_SHEET;end = Math.min(end, dataList.size());//写入数据到页签writer.write(dataList.subList(start, end), sheet);}writer.finish();os.close();}
}

导出的具体代码

   /*** 导出库存列表数据Excel** @param query* @return*/@SneakyThrows@Overridepublic ResponseEntity<byte[]> getListExport(InventoryQuery query) {// 定义输出流ByteArrayOutputStream out = new ByteArrayOutputStream();// 设置查询参数以获取所有数据(不分页)InventoryQuery allDataQuery = new InventoryQuery();// 复制原始查询条件BeanUtils.copyProperties(query, allDataQuery);// 设置分页参数为获取全部数据allDataQuery.setPageIndex(1);allDataQuery.setPageSize(Integer.MAX_VALUE);List<InventoryListDTO> inventoryListDTOS = getInventoryList(allDataQuery).getRows();// 将数据转换为导出DTO列表List<InventoryListExcelDTO> exportList = new ArrayList<>();for (InventoryListDTO inventory : inventoryListDTOS) {List<WarehouseStockDTO> warehouses = inventory.getGoodsWarehouses();// 如果没有仓库数据,创建一条基本记录if (warehouses == null || warehouses.isEmpty()) {InventoryListExcelDTO exportDTO = new InventoryListExcelDTO();BeanUtils.copyProperties(inventory, exportDTO);exportDTO.setWarehouseName("无仓库数据");exportDTO.setStockNum(BigDecimal.ZERO);exportList.add(exportDTO);} else {// 为每个仓库创建一条记录for (WarehouseStockDTO warehouse : warehouses) {InventoryListExcelDTO exportDTO = new InventoryListExcelDTO();BeanUtils.copyProperties(inventory, exportDTO);BeanUtils.copyProperties(warehouse, exportDTO);exportDTO.setWarehouseName(warehouse.getWarehouseName());exportDTO.setStockNum(warehouse.getStockNum());exportList.add(exportDTO);}}}// 生成Excelexcel.export("库存列表", out, InventoryListExcelDTO.class, exportList);// 响应给前端HttpHeaders headers = new HttpHeaders();String filename = "库存列表" + DateTime.now().toString("yyyyMMddHHmmssS") + ".xlsx";try {// 对文件名进行URL编码,确保中文能正确显示filename = URLEncoder.encode(filename, "UTF-8");} catch (UnsupportedEncodingException e) {// 编码失败时使用默认文件名filename = "inventory_detail_" + DateTime.now().toString("yyyyMMddHHmmssS") + ".xlsx";}headers.setContentDispositionFormData("attachment", filename);headers.setContentType(MediaType.APPLICATION_OCTET_STREAM);ResponseEntity<byte[]> res = new ResponseEntity<>(out.toByteArray(), headers, HttpStatus.CREATED);out.close();log.info("库存列表数据已导出");return res;}

注意点:注意向前端发送的字符集编码问题,空文件问题,excel是根据页面进行创建的,每个页面中的数据内容大小是写死的

如果是返回其他数据也是同理

分组代码技巧

            // 6. 按商品ID、属性名称、批次号进行分组Map<String, Map<String, Map<String, List<BatchDocumentDTO>>>> batchGroupMap = allBatchDocuments.stream().collect(Collectors.groupingBy(BatchDocumentDTO::getGoodsId,Collectors.groupingBy(dto -> {// 修正无属性商品的处理if (dto.getAttrName() == null || dto.getAttrName().isEmpty()) {return "NO_ATTR";}return dto.getAttrName();},Collectors.groupingBy(BatchDocumentDTO::getBatchNumber))));

多数据分组,使用stream进行,解释下关于分组:

这里使用的是多级分组,参数为(当前级别分组键,下游收集器),如果就单层只是用一个分组键即可

最外层分组(按 goodsId):遍历所有 BatchDocumentDTO 对象,提取每个对象的 goodsId 作为第一级键,对相同 goodsId 的对象应用下游收集器

中间层分组(处理后的 attrName):,在每个 goodsId 分组内,再次按属性名分组,空值安全处理:null/空属性名统一转为 "NO_ATTR",对相同处理后的属性名应用下游收集器

最内层分组(按 batchNumber):在每个属性名分组内,按批次号进行最终分组

建造者模式

OtherInListInfoDTO otherInListInfoDTO = OtherInListInfoDTO.builder().goods(inventoryVerifyList.getGoodId()).attr(attrName).unit(good.getUnit()).warehouse(inventoryVerifyList.getWarehouseId()).price(good.getBuy()).nums(inventoryVerifyList.getInventoryDifference()).total(inventoryVerifyList.getInventoryDifference().multiply(good.getBuy())).build();

这样来创建一个对象挺好用的,看的比较清晰,毕竟就不需要写set地狱了(笑)

关于使用easyExcel

EasyExcel官方文档 - 基于Java的Excel处理工具 | Easy Excel 官网

可以作为对文档数据的输入输出的一种形式

通常我们会通过excel提前写一个模板来进行导入导出

这个部分的写法见下

写Excel | Easy Excel 官网

 /*** 导出库存盘点单(不需要参数,直接在数据库去查)** @param* @return*/@Overridepublic ByteArrayOutputStream exportInventoryVerifyExcel () throws IOException {try {// 查询数据List<InventoryVerifyListDTO> inventoryVerifyList = inventoryVerifyMapper.selectInventoryVerifyListDTO();log.info("库存盘点单导出数据:{}", inventoryVerifyList);List<InventoryVerifyListDTO> inventoryVerifyListS = new ArrayList<>();// 组装数据//查看对象有有无属性attrName,如果有则将其包装成一个InventoryVerifyListDTO对象,并将其添加到inventoryVerifyListS中for (int i = 0; i < inventoryVerifyList.size(); i++) {if (inventoryVerifyList.get(i).getAttrName() != null && !inventoryVerifyList.get(i).getAttrName().isEmpty()) {if (i != 0 && inventoryVerifyList.get(i).getName().equals(inventoryVerifyList.get(i - 1).getName())) {InventoryVerifyListDTO inventoryVerifyListDTO1 = new InventoryVerifyListDTO();inventoryVerifyListDTO1.setName(inventoryVerifyList.get(i).getAttrName());inventoryVerifyListS.add(inventoryVerifyListDTO1);} else {inventoryVerifyList.get(i).setAttrName(null);inventoryVerifyListS.add(inventoryVerifyList.get(i));}} else {inventoryVerifyListS.add(inventoryVerifyList.get(i));}}log.info("组装后的库存盘点单数据:{}", inventoryVerifyListS);// 1. 准备字节输出流(用于存储Excel文件的字节数据)ByteArrayOutputStream baos = new ByteArrayOutputStream();// 2. 读取Excel模板文件(位于resources/templates目录下)InputStream is = null;try {is = new ClassPathResource("templates/InventoryVerify.xlsx").getInputStream();} catch (IOException e) {throw new RuntimeException(e); // 模板读取失败则抛异常}// 3. 初始化EasyExcel写入器(基于模板)ExcelWriter workBook = EasyExcel.write(baos, InventoryVerifyListDTO.class).withTemplate(is) // 关联模板.build();// 4. 定义写入的sheetWriteSheet sheet = EasyExcel.writerSheet().build();// 6. 填充数据到模板(强制新增行,避免覆盖模板样式)FillConfig build = FillConfig.builder().forceNewRow(true).build();workBook.fill(inventoryVerifyListS, build, sheet); // 填充明细数据// 7. 完成写入并获取字节数组workBook.finish();// 8. 返回成功响应return baos;} catch (Exception e) {log.error("导出库存盘点单Excel失败", e);throw new IOException("导出库存盘点单Excel失败: " + e.getMessage(), e);}}

还有一个关于读入操作的代码

        ExcelReadListener<OtherInImportExcelDTO> listener = new ExcelReadListener<>();EasyExcel.read(is, OtherInImportExcelDTO.class, listener).sheet().headRowNumber(2).doRead();List<OtherInImportExcelDTO> dataList = listener.getDataList();
  • 创建监听器来收集解析的Excel数据

  • 从输入流is读取Excel,跳过前2行表头

  • 将每行数据映射为OtherInImportExcelDTO对象

  • 通过监听器获取所有解析的数据

就是通过监听器来获取信息

如果是web,用户需要下载关于excel之类的,后端需要准备

        workBook.fill(otherInEasyExportExcelDTOList, build, sheet);workBook.fill(map, build, sheet);workBook.finish();byte[] byteArray = baos.toByteArray();HttpHeaders headers = new HttpHeaders();String filename = "xxx"+ DateTime.now().toString("yyyyMMddHHmmssS") + ".xlsx";headers.setContentDispositionFormData("attachment", filename);headers.setContentType(MediaType.APPLICATION_OCTET_STREAM);//之后可以将这个文件直接传出,这里就按具体分析了return new ResponseEntity<>(byteArray, headers, HttpStatus.OK);

Excel 模板填充、生成文件并返回下载响应的完整流程

剩余的部分都是crud了

用模板化语言吹吹就行

系统参数模块

还是总结点比较有价值的代码

比较清爽的递归

    /*** 递归构建树形结构* @param allNodes 所有节点列表* @param parentId 父节点ID,根节点为null* @return 构建好的子树*/private List<FrameDTO> buildTree(List<FrameDTO> allNodes, String parentId) {return allNodes.stream()// 过滤出当前父节点的直接子节点.filter(node -> Objects.equals(node.getPid(), parentId))// 递归构建子树.peek(node -> {List<FrameDTO> children = buildTree(allNodes, node.getId());node.setChildren(children);node.setHasChildren(!children.isEmpty());})// 按sort字段排序.sorted(Comparator.comparing(FrameDTO::getSort)).collect(Collectors.toList());}

如果是我还是会这么写吧,问就是习惯(笑)

private List<FrameDTO> buildTree(List<FrameDTO> allNodes, String parentId) {List<FrameDTO> result = new ArrayList<>();// 找出当前父节点的所有直接子节点for (FrameDTO node : allNodes) {if (Objects.equals(node.getPid(), parentId)) {result.add(node);}}// 为每个节点递归构建子树for (FrameDTO node : result) {List<FrameDTO> children = buildTree(allNodes, node.getId());node.setChildren(children);node.setHasChildren(!children.isEmpty());}// 按sort字段排序result.sort(Comparator.comparing(FrameDTO::getSort));return result;
}

还有一个因人而异的玩意,我个人还是比较习惯xml形式的

    public PageDTO<CustomerDTO> listAll(CustomerQuery query) {// 构建分页查询对象Page<Customer> page = new Page<>(query.getPageIndex(), query.getPageSize());// 构建查询条件对象QueryWrapper<Customer> queryWrapper = new QueryWrapper<>();queryWrapper.like(!StringUtils.isEmpty(query.getName()), "name", query.getName());queryWrapper.like(!StringUtils.isEmpty(query.getNumber()), "number", query.getNumber());queryWrapper.like(!StringUtils.isEmpty(query.getCategory()), "category", query.getCategory());queryWrapper.like(!StringUtils.isEmpty(query.getGrade()), "grade", query.getGrade());queryWrapper.like(!StringUtils.isEmpty(query.getContactPerson()), "contacts", query.getContactPerson());queryWrapper.like(!StringUtils.isEmpty(query.getContactPhone()), "contacts", query.getContactPhone());queryWrapper.like(!StringUtils.isEmpty(query.getUser()), "user", query.getUser());queryWrapper.like(!StringUtils.isEmpty(query.getData()), "data", query.getData());//queryWrapper.orderBy(true, false, "IFNULL(`update_time`,`create_time`)");queryWrapper.orderBy(true, false, "id");// 分页查询Page<Customer> pageRes = baseMapper.selectPage(page, queryWrapper);System.out.println(pageRes);return PageDTO.create(pageRes, msCustomerMapper::customerToCustomerDTO);}

感觉还是xml形式比较容易写点?看个人习惯了

关于参数校验,springBoot中有提供一个

@Validated @RequestBody CustomerDTO customerDTO

@Validated就这个,可以提供比较基础的校验,当然还是自己写好点


(剩余模块就不是我参与了,略略略)

还能再加把劲点吧_

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询