diff --git a/README2.md b/README2.md new file mode 100644 index 00000000..c13cdf93 --- /dev/null +++ b/README2.md @@ -0,0 +1,97 @@ +# Reddoor Micro-ERP System + +## 项目简介 (Project Overview) +本项目是一个基于微服务架构思想的单体多模块后端企业级应用程序(Micro-ERP),涵盖了**完善的供应链(商城)、物流管理及企业级业务处理**。该系统提供了从商品管理、采购、销售、库存到财务结算的完整闭环,同时集成了复杂的物流运输路由计算、场站管理以支撑线下业务。 + +该项目适合作为新零售、电商ERP、同城/干线物流等混合业务场景的后端基石。 + +--- + +## 技术栈 (Technology Stack) + +### 核心框架 +* **后端应用框架**: Spring Boot 2.5.14 +* **安全认证**: JWT (jjwt) + 自定义/Shiro方案 +* **持久层框架**: MyBatis-Plus 3.5.1 (含代码生成器) +* **API文档**: Knife4j / Swagger 2 + +### 数据与缓存 +* **关系型数据库**: MySQL 5.7+ (连接池: Druid) +* **缓存与分布式锁**: Redis (Redisson 3.17.2) + +### 云服务与第三方集成 +* **对象存储(OSS)**: 支持 MinIO, 阿里云 OSS, 腾讯云 COS, 七牛云 +* **消息通知**: 腾讯云短信服务 (Tencent Cloud SMS) +* **支付集成**: 支付宝 (Alipay SDK), Payment Spring Boot Starter +* **微信生态**: WxJava (支持小程序与微信公众号) + +### 工具与中间件 +* **工具箱**: Hutool, Guava (Gson), MapStruct, EasyPoi (Excel导入导出) +* **流程引擎**: Smart Flow (流程管理框架) +* **授权与安全**: TrueLicense (License许可验证), Bcprov (加密算法) +* **网络通信**: Smart Socket (AIO) + +### 测试环境 +* **单元/集成测试**: TestNG +* **UI/自动化测试**: Selenium +* **测试报告**: ExtentReports TestNG Adapter + +--- + +## 项目结构 (Project Structure) + +该工程采用 Maven 多模块 (Multi-Module) 结构,明确化了业务边界: + +```text +school (Root) + ├── hiver-admin # 后端管理系统的入口模块 (启动应用) + ├── hiver-core # 核心基础层:包含基础配置、物流实体与基础Dao (logistics, shopprint) + ├── hiver-modules # 业务模块层结构封装: + │ ├── hiver-app # 移动端/APP API (C端与普通工人端) + │ ├── hiver-base # 基础服务 (系统字典、通用业务或基础RBAC) + │ ├── hiver-file # 文件上传与对象存储统一管控 + │ ├── hiver-mall # 供应链商城与ERP业务 (核心业务模块) + │ ├── hiver-open # 开放平台/第三方对接API + │ ├── hiver-quartz # 分布式定时任务调度模块 + │ └── hiver-social # 社交与圈子功能等模块 + └── pom.xml # 全局依赖管理 +``` + +--- + +## 核心业务模块分析 (Business Models Analysis) + +### 1. 供应链与商城管理 (hiver-mall) +* **商品流**: 包括商品(Goods)、产品(Product)、分类(Category)、属性(Attribute)和品牌(Brand)的管理。 +* **交易流**: 提供针对客户(Customer)、供应商(Supplier)的管理,涵盖采购单(Purchase)、销售单(Sale)、订单管理(Order)、购物车(Cart)、退货退款(ReturnSale/ReturnCommission)。 +* **库存与财务**: 支持仓储管理(Stock/GoodsStock)、资金记录(DealingsRecord)、充值(Recharge)。 +* **店铺与终端**: 提供门店/网点(Shop, ShopArea, ShopTakeaway)与终端员工/工人的管理(Worker, WorkerAuth, UserClockIn)。 + +### 2. 物流与运输业务 (hiver-core logistics*) +深入集成了物流执行侧的实体: +* **路由与节点**: 路由(logisticsroute)、公司路由(logisticscompanyroute)、站点管理(logisticsstation)、中转站(logisticstransferstation)。 +* **业务操作**: 物流订单(logisticsorder)、装车日志(logisticsentruckinglog)、落车费规则计算(logisticslandingfeerules)。 + +### 3. 多端支持与扩展 (hiver-app / hiver-open) +* 提供给移动端(APP、小程序)的标准RESTful接口,支持微信生态。 +* 提供标准的Open API以便与外部ERP/WMS进行数据对接。 + +--- + +## 快速开始 (Getting Started) + +### 1. 环境准备 +* JDK 1.8 +* Maven 3.6+ +* MySQL 5.7+ +* Redis 6.x+ (安装并正常运行) + +### 2. 初始化项目 +1. **克隆代码**: 将本工程导入到 IDEA 中。 +2. **执行构建**: 在根目录下执行 `mvn clean install -Dmaven.test.skip=true` 解决所有依赖。 +3. **导入数据库**: 寻找相关的 `.sql` 脚本导入到 MySQL 数据库中(如有)。自行配置好 `application-dev.yml` 中的 Redis 和 MySQL 信息。 +4. **启动服务**: 找到 `hiver-admin` 模块中的 `HiverApplication.java` 启动类,直接启动即可运行后台 API 服务。 + +### 3. 注意事项 +* 如果提示 `lombok` 缺失,请在 IDE 中安装 Lombok 插件并开启 `Enable Annotation Processing`。 +* 支付配置与OSS/短信配置:若是进行本地开发调试,请在配置或者数据库字典中替换成对应的测试 KEY 和 Secret。 diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/controller/ProductController.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/controller/ProductController.java index 8e332d6c..19007f10 100644 --- a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/controller/ProductController.java +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/controller/ProductController.java @@ -18,6 +18,8 @@ import cc.hiver.mall.productpicture.service.ProductPictureService; import cc.hiver.mall.service.mybatis.ProductCategoryService; import cc.hiver.mall.service.mybatis.ProductGroupBuyPriceService; import cc.hiver.mall.service.mybatis.ProductService; +import cc.hiver.mall.service.ShopService; +import cc.hiver.mall.entity.Shop; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper; import com.baomidou.mybatisplus.core.metadata.IPage; @@ -52,6 +54,28 @@ public class ProductController { @Autowired private ProductGroupBuyPriceService productGroupBuyPriceService; + @Autowired + private ShopService shopService; + + private void triggerCacheRefreshByProductId(String productId) { + Product product = productService.getById(productId); + if (product != null && StringUtils.isNotEmpty(product.getShopId())) { + Shop shop = shopService.get(product.getShopId()); + if (shop != null) { + shopService.refreshShopCache(shop.getId(), shop.getRegionId()); + } + } + } + + private void triggerCacheRefreshByProductIds(String ids) { + if (StringUtils.isNotEmpty(ids)) { + String[] idArray = ids.split(","); + if (idArray.length > 0) { + triggerCacheRefreshByProductId(idArray[0]); + } + } + } + @RequestMapping(value = "/save", method = RequestMethod.POST) @ApiOperation("新增货品") public Result save(@RequestBody ProductVo productVo) { @@ -105,6 +129,7 @@ public class ProductController { productGroupBuyPriceService.saveBatch(productGroupBuyPrices); } if (result) { + triggerCacheRefreshByProductId(product.getId()); return ResultUtil.success("添加成功"); } else { return ResultUtil.error("添加失败"); @@ -167,6 +192,7 @@ public class ProductController { productGroupBuyPriceService.saveBatch(productGroupBuyPrices); } if (result) { + triggerCacheRefreshByProductId(productVo.getId()); return ResultUtil.success("修改成功"); } else { return ResultUtil.error("修改失败"); @@ -181,6 +207,7 @@ public class ProductController { updateWrapper.eq("id", id); final boolean result = productService.update(updateWrapper); if (result) { + triggerCacheRefreshByProductId(id); return ResultUtil.success("上架成功"); } else { return ResultUtil.error("上架失败"); @@ -203,6 +230,7 @@ public class ProductController { } final boolean result = productService.batchUp(ids); if (result) { + triggerCacheRefreshByProductIds(ids); return ResultUtil.success("上架成功"); } else { return ResultUtil.error("上架失败"); @@ -234,6 +262,7 @@ public class ProductController { updateWrapper.eq("id", id); final boolean result = productService.update(updateWrapper); if (result) { + triggerCacheRefreshByProductId(id); return ResultUtil.success("下架成功"); } else { return ResultUtil.error("下架失败"); @@ -248,6 +277,7 @@ public class ProductController { updateWrapper.eq("id", id); final boolean result = productService.update(updateWrapper); if (result) { + triggerCacheRefreshByProductId(id); return ResultUtil.success("设置成功"); } else { return ResultUtil.error("设置失败"); @@ -262,6 +292,7 @@ public class ProductController { updateWrapper.eq("id", id); final boolean result = productService.update(updateWrapper); if (result) { + triggerCacheRefreshByProductId(id); return ResultUtil.success("设置成功"); } else { return ResultUtil.error("设置失败"); @@ -284,6 +315,7 @@ public class ProductController { } final boolean result = productService.batchDown(ids); if (result) { + triggerCacheRefreshByProductIds(ids); return ResultUtil.success("下架成功"); } else { return ResultUtil.error("下架失败"); @@ -298,6 +330,7 @@ public class ProductController { updateWrapper.eq("id", id); final boolean result = productService.update(updateWrapper); if (result) { + triggerCacheRefreshByProductId(id); return ResultUtil.success("删除成功"); } else { return ResultUtil.error("删除失败"); @@ -319,6 +352,7 @@ public class ProductController { } final boolean result = productService.batchDeleteProduct(ids); if (result) { + triggerCacheRefreshByProductIds(ids); return ResultUtil.success("删除成功"); } else { return ResultUtil.error("删除失败"); diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/controller/ShopController.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/controller/ShopController.java index 69f88cd1..e108ec05 100644 --- a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/controller/ShopController.java +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/controller/ShopController.java @@ -43,6 +43,10 @@ import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import io.swagger.annotations.ApiParam; import lombok.extern.slf4j.Slf4j; +import cn.hutool.json.JSONUtil; +import cc.hiver.core.common.redis.RedisTemplateHelper; +import cc.hiver.mall.pojo.dto.ShopCacheDTO; +import org.springframework.data.domain.PageImpl; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Page; import org.springframework.web.bind.annotation.*; @@ -80,6 +84,9 @@ public class ShopController { @Autowired private InviteLogService inviteLogService; + @Autowired + private RedisTemplateHelper redisTemplateHelper; + @RequestMapping(value = "/getAll", method = RequestMethod.GET) @ApiOperation("获取全部数据") public Result> getAll() { @@ -178,6 +185,7 @@ public class ShopController { if (shop.getYearFee() != null && shop.getYearFee().compareTo(BigDecimal.ZERO) > 0) { inviteLogService.updateInviteLogIsOpen(shop.getId()); } + shopService.refreshShopCache(shop.getId(), shop.getRegionId()); return ResultUtil.success("编辑成功"); } @@ -188,6 +196,7 @@ public class ShopController { final Shop shop = shopService.findById(id); shop.setShopName(shopName); shopService.update(shop); + shopService.refreshShopCache(shop.getId(), shop.getRegionId()); return ResultUtil.success("编辑成功"); } @@ -200,6 +209,9 @@ public class ShopController { shopService.delete(id); // 删除店铺用户 shopUserService.deleteAllByShopId(id); + if (shop != null && shop.getRegionId() != null) { + shopService.removeShopCache(shop.getId(), shop.getRegionId()); + } } return ResultUtil.success("删除成功"); @@ -209,6 +221,62 @@ public class ShopController { @ApiOperation("多条件分页获取公司列表") public Result> getByCondition(Shop shop, PageVo pageVo) { + String regionId = shop.getRegionId(); + if (CharSequenceUtil.isNotBlank(regionId)) { + List values = redisTemplateHelper.hValues("SHOP_CACHE:" + regionId); + if (values != null && !values.isEmpty()) { + List allShops = new ArrayList<>(); + for (Object v : values) { + ShopCacheDTO dto = JSONUtil.toBean(v.toString(), ShopCacheDTO.class); + Shop s = dto.getShop(); + s.setProducts(dto.getProducts()); + if (CharSequenceUtil.isNotBlank(shop.getShopArea())) { + if (!shop.getShopArea().equals(s.getShopArea())) { + continue; + } + } + allShops.add(s); + } + + String sort = pageVo.getSort(); + String order = pageVo.getOrder(); + if (CharSequenceUtil.isNotBlank(sort)) { + allShops.sort((s1, s2) -> { + int compareResult = 0; + if ("saleCount".equals(sort)) { + Integer count1 = s1.getSaleCount(); + Integer count2 = s2.getSaleCount(); + Integer c1 = count1 == null ? 0 : count1; + Integer c2 = count2 == null ? 0 : count2; + compareResult = c1.compareTo(c2); + } else if ("shopScore".equals(sort)) { + BigDecimal score1 = s1.getShopScore() == null ? BigDecimal.ZERO : s1.getShopScore(); + BigDecimal score2 = s2.getShopScore() == null ? BigDecimal.ZERO : s2.getShopScore(); + compareResult = score1.compareTo(score2); + } + if ("desc".equalsIgnoreCase(order)) { + return -compareResult; + } + return compareResult; + }); + } + + int pageNumber = pageVo.getPageNumber() == null ? 1 : pageVo.getPageNumber(); + int pageSize = pageVo.getPageSize() == null ? 10 : pageVo.getPageSize(); + int fromIndex = (pageNumber - 1) * pageSize; + int toIndex = Math.min(fromIndex + pageSize, allShops.size()); + List pageList; + if (fromIndex >= allShops.size()) { + pageList = new ArrayList<>(); + } else { + pageList = allShops.subList(fromIndex, toIndex); + } + + Page pageResult = new PageImpl<>(pageList, PageUtil.initPage(pageVo), allShops.size()); + return new ResultUtil>().setData(pageResult); + } + } + final Page page = shopService.findByCondition(shop, PageUtil.initPage(pageVo)); final List shopIdList = new ArrayList<>(); @@ -223,7 +291,7 @@ public class ShopController { page.getContent().forEach(e -> { final List products = new ArrayList<>(); - if(productList.getRecords().size() > 0){ + if(productList.getRecords() != null && productList.getRecords().size() > 0){ productList.getRecords().forEach(productPageVO -> { if (e.getId().equals(productPageVO.getShopId())) { products.add(productPageVO); @@ -232,6 +300,9 @@ public class ShopController { } e.setProducts(products); + if (CharSequenceUtil.isNotBlank(regionId)) { + shopService.refreshShopCache(e.getId(), regionId); + } }); return new ResultUtil>().setData(page); } @@ -242,6 +313,7 @@ public class ShopController { final Shop shop = shopService.get(id); shop.setStatus(ShopConstant.SHOP_STATUS_NORMAL); shopService.update(shop); + shopService.refreshShopCache(shop.getId(), shop.getRegionId()); return ResultUtil.success("操作成功"); } @@ -251,7 +323,7 @@ public class ShopController { final Shop shop = shopService.get(id); shop.setStatus(ShopConstant.SHOP_STATUS_LOCK); shopService.update(shop); - + shopService.removeShopCache(shop.getId(), shop.getRegionId()); return ResultUtil.success("操作成功"); } @@ -360,6 +432,7 @@ public class ShopController { oldShop.setSubtitle(shop.getSubtitle()); } shopService.update(oldShop); + shopService.refreshShopCache(oldShop.getId(), oldShop.getRegionId()); return ResultUtil.success("更新成功"); } @@ -404,6 +477,7 @@ public class ShopController { final Shop shop = shopService.findById(shopId); shop.setShopIcon(shopIcon); shopService.update(shop); + shopService.refreshShopCache(shop.getId(), shop.getRegionId()); return ResultUtil.data(shop); } } diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/pojo/dto/ShopCacheDTO.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/pojo/dto/ShopCacheDTO.java new file mode 100644 index 00000000..39fd934c --- /dev/null +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/pojo/dto/ShopCacheDTO.java @@ -0,0 +1,31 @@ +package cc.hiver.mall.pojo.dto; + +import cc.hiver.mall.entity.Shop; +import cc.hiver.mall.entity.ShopTakeaway; +import cc.hiver.mall.pojo.vo.ProductPageVO; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import java.io.Serializable; +import java.util.List; + +/** + * 店铺Redis缓存传输对象 + * + * @author cc + */ +@Data +@ApiModel(value = "店铺Redis缓存对象") +public class ShopCacheDTO implements Serializable { + private static final long serialVersionUID = 1L; + + @ApiModelProperty(value = "店铺基础信息") + private Shop shop; + + @ApiModelProperty(value = "店铺外卖配送信息") + private ShopTakeaway shopTakeaway; + + @ApiModelProperty(value = "店铺推荐/可用商品列表") + private List products; +} diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/service/ShopService.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/service/ShopService.java index f79374a8..1f0f6ab1 100644 --- a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/service/ShopService.java +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/service/ShopService.java @@ -73,4 +73,20 @@ public interface ShopService extends HiverBaseService { List getShopSaleCounts(List shopIdList); IPage getShareList(ProductPageQuery productPageQuery); + + /** + * 刷新或写入店铺及其附属信息到Redis缓存 + * + * @param shopId 店铺ID + * @param regionId 区域ID + */ + void refreshShopCache(String shopId, String regionId); + + /** + * 从Redis缓存移除指定店铺 + * + * @param shopId 店铺ID + * @param regionId 区域ID + */ + void removeShopCache(String shopId, String regionId); } diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/serviceimpl/ShopServiceImpl.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/serviceimpl/ShopServiceImpl.java index ae07aa7f..d0997789 100644 --- a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/serviceimpl/ShopServiceImpl.java +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/serviceimpl/ShopServiceImpl.java @@ -36,6 +36,11 @@ import cc.hiver.mall.service.ShopService; import cc.hiver.mall.service.mybatis.ProductGroupBuyPriceService; import cc.hiver.mall.utils.DateUtil; import cn.hutool.core.text.CharSequenceUtil; +import cn.hutool.json.JSONUtil; +import cc.hiver.core.common.redis.RedisTemplateHelper; +import cc.hiver.mall.dao.mapper.ShopTakeawayMapper; +import cc.hiver.mall.entity.ShopTakeaway; +import cc.hiver.mall.pojo.dto.ShopCacheDTO; import com.baomidou.mybatisplus.core.metadata.IPage; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; @@ -87,6 +92,12 @@ public class ShopServiceImpl implements ShopService { @Autowired private ProductGroupBuyPriceService productGroupBuyPriceService; + @Autowired + private RedisTemplateHelper redisTemplateHelper; + + @Autowired + private ShopTakeawayMapper shopTakeawayMapper; + @Override public HiverBaseDao getRepository() { return shopDao; @@ -226,4 +237,44 @@ public class ShopServiceImpl implements ShopService { return page; } + @Override + public void refreshShopCache(String shopId, String regionId) { + if (CharSequenceUtil.isBlank(regionId)) { + return; + } + Shop shop = shopDao.getById(shopId); + if (shop == null) { + removeShopCache(shopId, regionId); + return; + } + + ShopCacheDTO cacheDTO = new ShopCacheDTO(); + cacheDTO.setShop(shop); + + ShopTakeaway takeaway = shopTakeawayMapper.selectByPrimaryKey(shopId); + cacheDTO.setShopTakeaway(takeaway); + + ProductPageQuery query = new ProductPageQuery(); + query.setPageNum(1); + query.setPageSize(200); + List shopIdList = new ArrayList<>(); + shopIdList.add(shopId); + query.setShopIdList(shopIdList); + query.setIsPush(1); // 推荐 + IPage productList = getShareList(query); + cacheDTO.setProducts(productList.getRecords()); + + String redisKey = "SHOP_CACHE:" + regionId; + redisTemplateHelper.hPut(redisKey, shopId, JSONUtil.toJsonStr(cacheDTO)); + } + + @Override + public void removeShopCache(String shopId, String regionId) { + if (CharSequenceUtil.isBlank(regionId)) { + return; + } + String redisKey = "SHOP_CACHE:" + regionId; + redisTemplateHelper.hDelete(redisKey, shopId); + } + } diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/serviceimpl/ShopTakeawayServiceImpl.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/serviceimpl/ShopTakeawayServiceImpl.java index ea799d6d..5fa6d576 100644 --- a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/serviceimpl/ShopTakeawayServiceImpl.java +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/serviceimpl/ShopTakeawayServiceImpl.java @@ -19,10 +19,12 @@ import cc.hiver.mall.dao.mapper.ShopTakeawayMapper; import cc.hiver.mall.entity.ShopTakeaway; import cc.hiver.mall.pojo.query.ShopTakeawayQuery; import cc.hiver.mall.service.ShopTakeawayService; +import cc.hiver.mall.service.ShopService; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -38,6 +40,11 @@ public class ShopTakeawayServiceImpl extends ServiceImpl