196 Commits
Author SHA1 Message Date
王奎兴 3081460143 Merge branch 'dev' into dev_sfjk_0827 2026-09-05 10:23:32 +08:00
王奎兴 6b3912aae9 发30 2026-09-04 13:59:37 +08:00
王奎兴 12e43080b8 发31 2026-09-04 13:55:07 +08:00
王奎兴 77e0c77fbf 发31 2026-09-04 12:21:17 +08:00
王奎兴 03978d23d7 发30 2026-09-04 12:17:28 +08:00
王奎兴 ee7dbfcfb9 发30 2026-09-04 11:55:45 +08:00
王奎兴 94d05cca9c 发31 2026-09-04 11:52:53 +08:00
王奎兴 9fd3d34610 逻辑删除查询条件; 2026-09-04 11:51:10 +08:00
王奎兴 edb7a5343d 逻辑删除查询条件; 2026-09-04 11:37:06 +08:00
rcx 762c49cd0f 发 测试 2026-09-04 11:10:20 +08:00
rcx 1b8511c092 feat(oms): 出库/入库业务单导入支持多页签——每个工作表独立生成一张单据
一、行为变更(ReservationStockInOrderImpl)
- 出库导入(parseExcelFile)/入库导入(parseStockInExcel):不再固定读单一页签,枚举Excel全部工作表,每个页签独立解析为一张单据(5个页签=5张单5个单号)
- 全部回滚策略:任一页签校验失败整体不落库,报错按页签聚合列出(页签[x]:原因;页签[y]:原因)
- 空页签(无货品/客户明细)自动跳过;单页签文件行为与原先完全一致
- 出库单号改到落库阶段逐张生成:多页签下校验阶段统一生成会因全部未入库而取到相同单号
- 入库模板OrderNO增加本文件内跨页签查重:各页签都未入库时库查重互相不可见,防两页签同OrderNO重复落库
- 页签解析异常message为空时兜底用异常类名,避免报错显示null
- 方法级@Transactional(rollbackFor=Exception.class):顺带修复旧入库导入因this自调用导致事务代理失效的问题

二、接口与返回结构(IReservationStockInOrderService / ReservationStockInOrderApi)
- 两个导入方法返回类型改为List<SheetImportResultDTO>(每页签一条:页签名/单号/物料行数/数量合计/解析数据)
- /parseExcel与/parse-excel返回:result=第一个页签解析数据(兼容老前端)、results=全部页签结果、msg=成功导入N张单:单号1、单号2…
- 入库接口不传sheetName为多页签模式,显式传页签名保持单页签老行为
2026-09-04 10:55:18 +08:00
rcx ce3bb036fa 发 测试 2026-09-04 10:50:48 +08:00
rcx 71515e74f5 Merge branch 'refs/heads/dev' into dev-ty1.2 2026-09-04 10:48:46 +08:00
rcx 7d3c229b9a feat(oms): 新增出仓委托单,支持新增/编辑/审核/下发WMS/完成入库
- 订单状态五态:1待审核(新增默认,驳回单编辑后自动回此态) 2已审核(下发后保持,用下发状态区分) 3已驳回 4进行中(预留) 5已入库
- 下发走WMS出库通知单链路(stockOutOrInInsert),返回强校验防吞错;完成入库限已审核+已下发
- 订单类型未传默认普通出库;明细新增单价unitPrice;列表支持invoiceNo/settlementCurrency查询,时间参数只传年月日
- 必填:客户/业务员/仓库/商品名称/申报数量;编辑删除限待审核/已驳回且未下发
- 附达梦建表脚本oms_out_entrust_order(主表+明细表,最终版)
2026-09-04 10:47:29 +08:00
rcx a549ac1507 发 31 2026-09-03 18:32:25 +08:00
rcx bf3f0a2bfe 发 30 2026-09-03 18:25:37 +08:00
rcx 35b026c8bd Merge branch 'refs/heads/dev-ty1.2' into dev 2026-09-03 18:23:32 +08:00
rcx 0eb47793a8 fix(库存): OMS/WMS库存查询入库时间/更新时间只保留年月日,去掉时分秒
- 入库时间(createTime)/更新时间(updateTime)继承自全系统基类 BaseVOEntity,默认序列化带时分秒;库存查询导出复用 /list 接口,前端取 JSON 值直接进 Excel,导出后无法在 Excel 里修改格式
- 两个库存 PO 覆盖 getCreateTime/getUpdateTime 并收窄 @JsonFormat 为 yyyy-MM-dd,仅影响 OMS/WMS 库存查询的显示与导出,不改动 BaseVOEntity 全局行为
- 页面列表/详情回显同步变为纯年月日;排序仍按原始时间值,逻辑不变
2026-09-03 18:00:52 +08:00
rcx fee891dad0 fix(oms): 下发出库单增加幂等拦截,已存在有效执行单时禁止重复下发
问题:单据下发过并取消后重新下发(或重复点下发),issueOrder 会再生成新执行单并原样复制业务明细uniqueId插入,撞执行明细表唯一约束(如CONS134279709)导致下发失败。
修复:issueOrder 生成执行单前先查同业务单号下是否已存在有效执行单(del_flag=1),存在则提示「已下发,请勿重复下发;如需重新下发请先取消下发」。
配套DB变更(单独由DBA执行):执行明细表唯一键CONS134279709改为函数唯一索引,仅对DEL_FLAG=1的行约束UNIQUE_ID唯一,软删行不再占用唯一键。
2026-09-03 17:44:16 +08:00
rcx 34496de969 发测试 2026-09-03 16:04:04 +08:00
rcx 7a3021f82a feat(wms,oms):出库排名统计改造为聚合排名——商品按物料合并/线路按线路+物料合并,按出库数量降序
- 商品tab:一行=一个货品(跨出库单汇总),返回 货号/货名/件数/毛重(吨)/体积(CBM)/退保总额(占位null),忽略线路筛选参数

- 线路tab:一行=线路×货品(按线路+物料编码合并),额外返回 线路编码/线路,仅统计带线路的单(排除line_code为空的历史单);商品/线路筛选参数均生效

- 单据级字段(种类/仓单编号/入库单号/客户编号/收货人/出仓日期)合并后无单值可对应,不再返回

- 件数=SUM(CHECK_QUANTITY)复核确认的实际出库量(与入库排名实际上架量SHELVES_QUANTITY对称),按件数降序,编码次之

- 合计行:件数/毛重吨/体积 全量合计;商品tab含totalOrderCount(去重出库单数),线路tab含totalOrderCount+totalLineCount(去重线路数)

- 数据源不变:WMS查本库出库表,OMS跨库查NGWL_TEST_WMS同名表,两边数字一致;接口路径/传参不变
2026-09-03 16:01:17 +08:00
rcx c9c3062789 feat(wms,oms):入库排名统计改为按物料编码合并(一行=一个物料在区间内全部已入库仓单的合计):返回列裁剪为物料编码/货名/规格型号/单位/件数(实际上架量)/毛重(吨)/投保总额(货品档案保额取MAX不跨仓单累加)/体积;仓单编号/客户编号/存货人/入仓日期为单据级字段单值失真,移出返回列仅保留为查询条件;HAVING过滤0件行;合计行投保总额=各物料保额之和(每物料计一次),totalOrderCount=涉及仓单数;OMS/WMS两端口径数字一致;查询参数与返回结构外层不变 2026-09-03 15:57:44 +08:00
rcx 4ad31a870a fix(oms): 物料解析防张冠李戴+导入三类校验合并报错,客户编码只认USER_MEMBER_CODE
一、出库业务单导入(ReservationStockInOrderImpl)
- 物料不存在/客户不存在/批次库存不足三类校验全部收集完成后统一报错,不再分阶段中断
- materialCache 取用统一 trim,防 Excel 编号带空格导致校验被跳过

二、物料三级解析防张冠李戴(MaterialBaseInfoResolveService)
- baseId 按主键查到的档案与明细编码不一致时不再信任 baseId,转库存ID反查/编码+组织兜底,两级落空才退回按ID档案,命中打WARN日志
- 背景:前端把库存ID传进 materialBaseInfoId,撞上编码=名称的脏档案主键后整行物料被覆盖成错误物料

三、客户编码口径(reservationStockOutOrder/ReservationStockOutOrderMapper.xml)
- queryCustomerByCode 只认 USER.USER_MEMBER_CODE,不再放行 USER_SHIPPER.CUSTOMER_NC_CODE

四、明细查询补字段(reserveStockOutOrder/ReserveStockOutOrderMapper.xml)
- resultMap 补 status/issue_status 映射,detailsList 查询补查 a.status, a.issue_status
2026-09-03 15:52:18 +08:00
秦鸿展 9447604c96 31 2026-09-03 12:37:33 +08:00
秦鸿展 1577eddc7b 30 2026-09-03 11:49:38 +08:00
秦鸿展 f5300fdd5d BI查询YMS月台管理列表查询开发接口 2026-09-03 11:39:13 +08:00
秦鸿展 3880755df0 BI查询YMS月台管理列表查询开发接口 2026-09-03 11:35:33 +08:00
秦鸿展 28bf58e8d1 BI查询YMS月台管理列表查询开发接口 2026-09-03 11:28:22 +08:00
秦鸿展 3a5af634dd 30 2026-09-03 00:29:18 +08:00
秦鸿展 4f45a73505 已登录账号APP菜单问题 2026-09-03 00:26:45 +08:00
秦鸿展 b5f710cd33 31 2026-09-02 20:49:09 +08:00
秦鸿展 bfffc6bbf1 App游客时正常展示消息页面 2026-09-02 20:46:31 +08:00
秦鸿展 a8815ade02 App游客时正常展示页面 2026-09-02 20:28:59 +08:00
rcx 57312753ed 发 30 2026-09-02 19:52:47 +08:00
rcx 03ce38ed9b 发 30 2026-09-02 19:46:44 +08:00
rcx b09b07978f fix(oms): 出库导入批次分配优化:批次号优先排序、移除归属校验、库存不足统一报错
c1f483f0 出库导入指定仓库功能的迭代调整:

1. 移除物料仓库归属前置校验(countInventoryByWarehouse):物料档案全仓共享、
   不属于任何仓库,仓库约束收敛到批次分配环节——分不到当前仓批次即报库存不足
2. getkcId 增加分配排序:同物料同仓多条可用库存时,有批次号的优先、空批次
   沉底、同组按库存ID(入库先后);此前无排序随机取第一条,会分到无批次号的
   历史脏数据记录
3. 明细仓库编码/名称兜底:部分历史库存记录只存仓库ID未存编码/名称,按ID
   反查仓库表补齐,修复明细仓库只显示ID不显示名称
4. 库存校验改收集式:全部物料批次比较完成后统一报出不足物料清单,原为逐行
   分配时立即抛错,首个不足即中断、后续物料不参与比较
2026-09-02 19:43:48 +08:00
rcx 87dae51a15 fix(bms): 发票列表兼容pageNum/pageNo参数名,修复翻页始终返回第一页;排序补id兜底防翻页重复 2026-09-02 19:31:01 +08:00
rcx 49ab26a8e9 fix(bms): 发票列表条数显示为0,注册MyBatis-Plus分页拦截器使Page生效 2026-09-02 18:33:50 +08:00
rcx c1f483f0a0 feat(oms): 出库导入支持指定仓库,校验物料库存归属并按仓过滤批次库存
背景:物料档案全仓共享,此前导入不区分仓库,批次库存"查到哪条算哪条",单据可能取到其他仓库的库存且主表仓库为空。

1. /parseExcel 新增可选参数 warehouseId(前端随文件提交当前页面仓库;不传保持原有行为)
2. 新增 countInventoryByWarehouse 归属校验:物料在指定仓库无库存记录时统一报错"物料[xx]在当前仓库[名]没有库存记录,无法从该仓库出库"
3. getkcId 批次库存查询增加仓库过滤,明细批次/仓库/入库单号均取当前仓库
4. 导入单主表回填当前仓库 warehouseId/Code/Name(此前导入单仓库为空)
5. 出库导入两个入口(reservationStockInOrderApi/executionStockInOrderApi 的 parseExcel)同步支持
6. 库存不足报错补充物料编码,便于定位
2026-09-02 18:32:12 +08:00
rcx 6d7e42bcde 发 31 2026-09-02 16:55:19 +08:00
rcx 61572c536a 发 30 2026-09-02 16:48:35 +08:00
rcx 572b25ee88 feat(oms): 运输业务单列表新增预计起运时间范围查询条件(layDateStart/layDateEnd) 2026-09-02 16:16:46 +08:00
rcx 3576d734af fix(oms): 出库业务单导入发票编号不落库,补齐繁简归一化「發/額」映射
模板列头「發票編號/金額/發票張數」归一化时映射表缺「發→发」「額→额」,發票編號列识别失败(invoiceNoCol=-1),客户配送明细落库时发票编号为空、查看不回显,金额列与发票张数同样受影响。补齐映射并在关键列缺失时输出告警日志;历史已导入单据需重新导入。
2026-09-02 15:43:07 +08:00
rcx 028af3dd2f fix(wms,oms):出库排名件数改为复核确认的实际出库量(CHECK_QUANTITY),排序同步调整
- 件数取数由明细 PIECE_COUNT(计划/申报件数)改为 SUM(CHECK_QUANTITY) 复核确认的实际出库量,与入库排名的实际上架量 SHELVES_QUANTITY 取数逻辑对称(已验证现有已出库数据 222/222 全覆盖)

- 排序第一键由出库数量(outbound_quantity,下发时的计划复制值)改为件数(实际出库量)降序,出仓日期/单号次之

- 接口路径/传参/返回字段结构均不变,前端无感;其余为注释同步
2026-09-02 15:41:51 +08:00
rcx 3af78d5d7b feat(wms,oms):出库排名统计改造为仓单×货品明细列表,按出库数量降序排名
- 字段:种类/仓单编号/入库单号/客户编号/收货人/货名/线路/件数/毛重(吨)/体积(CBM)/退保总额(占位null,数据来源待确认)/出仓日期

- 商品/线路各一套接口,路径名不变;商品接口忽略线路筛选参数,线路接口忽略商品筛选参数;含合计行(件数/毛重吨/体积/仓单数)与Excel导出

- 统计口径不变:出库单创建时间区间+已出库状态(9,12,13)+明细level=1主行;排序按出库数量(销售量)降序,件数/出仓日期/单号次之

- 数据源:WMS查本库出库表;OMS跨库查NGWL_TEST_WMS同名表(OMS库出库单状态不流转,跨库与WMS端数字一致)

- 删除原按商品/线路聚合的排名接口(销售量/交易数/金额字段)
2026-09-02 15:25:51 +08:00
rcx e821619a96 fix(库存): 修复OMS/WMS库存查询过期日期不回显
根因:业务只在OMS填写饮用日期,且下发DTO无过期日期字段,饮用日期→过期日期的转换仅存在于收货单生成瞬间,后续环节全部丢失。

- InMaterialDetail(DTO) 补充 expiryDate 字段,打通OMS→WMS传递
- 预约单/执行单下发前:过期日期为空时用饮用日期补齐(治本)
- WMS收货批次属性回写:恢复饮用日期回写并兜底转换为过期日期(加固)
- WMS上架确认:过期日期为空时从收货明细兜底补齐(加固)
2026-09-02 15:05:29 +08:00
rcx 434348f972 fix(oms,wms): 出库单下发WMS失败被静默吞掉,修复WMS出库拣货不显示OMS单据
- OMS建单/下发前校验明细数量>0且已关联库存
- OMS检查WMS自动分配/生成拣货单返回值,失败即报错回滚
- WMS自动分配各类失败场景抛带单号的明确异常,替代NPE
- WMS接单幂等保护,失败重试不产生重复数据
2026-09-02 14:32:13 +08:00
rcx 72cb1d8c07 feat(wms): 库存列表接口新增隐藏可用数量为0的参数
/materialInventoryApi/list 新增可选参数 excludeZeroAllocationQuantity,为 true 时排除可用数量=0 的库存行;与已有 excludeZeroInventoryQuantity 组成两个独立开关,不传时保持原有行为。配合盘点-库存选择页隐藏数量为0物料及出库选库存隐藏0库存需求。
2026-09-02 14:04:15 +08:00
rcx f5a7c8c1f3 发 31 2026-09-02 00:12:59 +08:00
rcx 3989d75f05 发 30 2026-09-01 23:50:34 +08:00
rcx 471b515a45 Merge branch 'refs/heads/dev-ty1.2' into dev
# Conflicts:
#	mhd-api/mhd-api-system/src/main/java/com/mhd/system/api/BmsServiceFeign.java
#	mhd-api/mhd-api-system/src/main/java/com/mhd/system/api/OmsServiceFeign.java
#	mhd-api/mhd-api-system/src/main/java/com/mhd/system/api/WlhyServiceFeign.java
#	mhd-api/mhd-api-system/src/main/java/com/mhd/system/api/WmsServiceFeign.java
#	mhd-api/mhd-api-system/src/main/java/com/mhd/system/api/YmsServiceFeign.java
#	mhd-common/mhd-common-core/src/main/java/com/mhd/common/core/feign/ThirdPartyServiceFeign.java
#	mhd_oms/src/main/java/com/mhd/oms/infrastructure/feign/ThirdPartyServiceFeign.java
#	mhd_oms/src/main/resources/bootstrap.yml
2026-09-01 23:49:08 +08:00
rcx 19e897cae0 发 30 2026-09-01 23:02:30 +08:00
rcx 91bdb99ec2 发 31 2026-09-01 22:50:50 +08:00
rcx 004580e252 发 30 2026-09-01 22:36:10 +08:00
rcx da3c58a366 fix(wms,oms):入库排名统计口径修复:件数改为实际上架量SUM(SHELVES_QUANTITY)(原PIECE_COUNT为建档申报值不随收货上架变化);OMS端数据源跨库改查WMS入库表NGWL_TEST_WMS.stock_in_order/in_material_detail(OMS执行单状态不回传且上架量回填代码未启用,与出库排名1300a4e1跨库修法一致),状态恢复为已入库(5);入仓日期为空兜底建档时间;OMS与WMS两端数字一致;接口路径/参数/返回结构不变 2026-09-01 21:36:33 +08:00
赵辉 1cec762930 user30 2026-09-01 17:35:21 +08:00
赵辉 4deaeaa89d user30 2026-09-01 17:22:26 +08:00
赵辉 1208e02fe7 yms租户同步 2026-09-01 17:14:23 +08:00
秦鸿展 77fb71dab4 30 2026-09-01 16:32:33 +08:00
秦鸿展 cd41c616bb 31 2026-09-01 16:30:23 +08:00
rcx a4f27ab643 feat(oms,wms):库存查询导出新增isExport标识与融合排序 /list 新增可选参数 isExport 标识导出请求(导出显示商品饮用有效期/物料分类/进货日期由查询结果直接返回);isExport=true 时所有查询维度(OMS 6条/WMS 8条)统一启用导出融合排序:到期日倒序空值最后(保留原 orderByExpiryDate 第一优先级,融合不取代)→同到期日内按物料编码升序→0库存靠后→更新时间倒序→MAX(库存ID)兜底保证分页稳定;各语句挂载公共片段 exportOrderByMaterialCode,不传 isExport 保持原有排序;OMS Controller 在 isExport=true 时跳过 Java 按更新时间重排避免覆盖 SQL 导出排序 2026-09-01 16:29:41 +08:00
秦鸿展 c174ea8742 单号编辑变化问题,以及组织查询问题 2026-09-01 15:15:53 +08:00
秦鸿展 2bbd6784c4 31 2026-09-01 14:14:51 +08:00
秦鸿展 923afda21c 大屏月台接口查询修改 2026-09-01 14:09:23 +08:00
rcx bb3ca932d2 feat(oms):出入库导入存在性校验改为收集后统一提示(物料+客户不存在的编号一条消息带全)
调整2d1e4b59引入的校验流程(入库parseStockInExcel/出库parseExcelFile统一):
1. 第一步:物料编号存在性校验——逐编号查库,全部不存在的收集到missingMaterials,不立即报错
2. 第二步:客户编号存在性校验——逐编号查库,全部不存在的收集到missingCustomers,不立即报错
3. 第三步:两步全部完成后统一抛出提示,避免修完物料问题重导一次才暴露客户问题:
   只缺物料:"物料[A、B]不存在,请先维护物料信息,维护完成后重新导入"
   只缺客户:"客户[C]不存在,请先维护客户信息,维护完成后重新导入"
   都缺:"物料[A、B]不存在,请先维护物料信息;客户[C、D]不存在,请先维护客户信息,维护完成后重新导入"
4. 存在性全部通过后才继续多货主/多客户编号/客户与货主一致性等校验及建单;
   入库"多个客户编号"校验相应移到存在性之后,多个编号中缺哪个报哪个
5. 入库客户查询结果存入customerCache复用,一致性校验不再二次查库

漏填编号(空白行)仍即时报错,不属于存在性问题,行为不变
2026-09-01 09:53:51 +08:00
rcx a588264849 fix(wms,oms):入库排名统计查询恒为空修复:OMS执行入库单状态不回传(全停status=1)无法按已入库过滤,改为仅排除已取消(6)/已关闭(7)(与入库明细查询页口径一致);入仓日期为空的历史单筛选与展示均兜底建档时间create_time(与出库明细统计口径一致);WMS仍保持仅已入库(5) 2026-09-01 09:24:20 +08:00
rcx 1300a4e1f8 fix(oms):出库排名OMS端数据源改为跨库查WMS出库表,修复查询结果恒为空
OMS库出库单状态从不流转(预约/执行单全部status=1,无"已出库"数据),原SQL按执行出库单status=9过滤导致查不出任何数据;改为跨库取WMS出库表NGWL_TEST_WMS.stock_out_order/out_material_detail(与OMS库存查询跨库join material_base_info同一模式),口径与WMS端/outboundRankApi完全一致:已出库及之后状态(9,12,13)+明细level=1主行+按出库单创建时间筛选,两边页面数字一致;接口路径/参数/返回结构均不变,其余文件仅更新注释中的数据源说明
2026-09-01 09:10:59 +08:00
rcx bf96f5f005 feat(wms,oms):库存管理新增入库排名统计(按仓单+货品汇总,件数降序,与出库排名tab配对);还原出库排名第一版源码
入库排名统计(新增):WMS /inboundRankApi、OMS /executionInboundRankApi 的 queryMaterialRankList 分页+合计、exportMaterialRank 导出;口径:仅已入库(status=5),明细仅level=1主行,日期区间按入仓日期warehouse_date筛选(必填);一行=仓单内某货品合计(GROUP BY 仓单+货品 ORDER BY 件数DESC);字段:入仓日期/仓单编号/客户编号/存货人/货名/件数/毛重(吨=KG/1000)/投保总额(货品档案保额,组内MAX防重复累加)/体积(CBM)+货号/规格/单位;合计行:totalPieceCount/totalGrossWeightTon/totalInsureAmount/totalVolume/totalOrderCount;登录上下文:日期必填校验+topOrganizationId租户隔离,WMS另注入关联仓库warehouseId。出库排名统计(还原第一版):/outboundRankApi、/executionOutboundRankApi,按货品/线路汇总排名
2026-08-31 17:47:09 +08:00
rcx a983ae639e fix(wms,oms):物料/过期日期查询补查入库日期(create_time)与更新时间,分组页签入库日期显示为空 2026-08-31 15:59:54 +08:00
rcx a74afd837c feat(oms):入库业务单新增页面新增按物料编号查询库存批次最近5条过期日期接口 /reservationMaterialInventoryApi/queryRecentExpiryDates/{materialCode},按物料编号关联 NGWL_TEST_WMS.material_base_info 查 reservation_material_inventory 库存批次,返回过期日期+批次号(date_format 按天去重、到期日倒序、limit 5),供前端下拉回显(也可点日期图标手动选择);非平台租户按登录用户一级组织 top_organization_id 过滤(与 /list 一致);沿用 Api→ApplicationService→DomainService→Facade→Impl→Mapper 分层,新增响应对象 MaterialInventoryExpiryBatchPO(expiryDate/batchNumber) 2026-08-31 15:22:59 +08:00
rcx bc3afbacb1 feat(wms,oms):物料分类:库存列表返回增加 materialClassifyCode/Name 显式映射
(ListTypeHandler,数据关联 material_base_info 带出)
2026-08-31 13:58:02 +08:00
rcx 5ba6a3b73d 发测试 2026-08-31 13:43:11 +08:00
rcx da84b7bb69 feat(wms,oms):库存管理-库存查询新增进货日期、入库日期查询条件,新增进货日期字段贯通收货-上架-库存全链路
需求:OMS/WMS 库存管理-库存查询页增加查询条件:物料分类(后端已支持,无需改动)、
进货日期(新增字段)、入库日期。

一、DDL(sql/20260831_wms_oms_add_purchase_date.sql)
    WMS 收货明细、上架明细、物料库存及 OMS 预留库存共 4 张表新增 PURCHASE_DATE DATE 列,
    附批次明细配置模板、执行前检查与回滚语句;须先执行 DDL 再发布服务。

二、进货日期录入与落库链路(收货→上架→WMS库存→OMS库存)
    1、录入入口为收货批次属性「进货日期」标签(批次规则配置,标签须一字不差),
       ReceiptMaterialDetailImpl 两处标签解析新增分支;
    2、收货明细→上架明细:StockReceiptOrderDomainService 6 处日期拷贝点补 setPurchaseDate(含子项);
    3、上架落库:stockIn/sj 赋值写入 material_inventory;
    4、同步 OMS:MaterialInventoryOmsParamDO 新增字段经 Feign omsEdit 传递,
       写入 reservation_material_inventory;
    5、库存行合并键:批次规则勾选后进货日期参与分行(getParam 映射 + selectListByWhere 全等匹配),
       OMS omsEdit 行匹配同步按进货日期区分,避免不同进货日期误合并到同一行。

三、查询条件(两模块 /list 接口)
    1、进货日期:purchaseDate(单日精确)+ purchaseDateStart/purchaseDateEnd(区间,End 含当天);
    2、入库日期:WMS 新增 createTimeStart/createTimeEnd(按库存行创建时间过滤,与 OMS 现有口径一致);
    3、列表/导出返回新增 purchaseDate 字段,分组查询取 MAX(purchase_date)。

说明:历史库存进货日期为空,按进货日期查询查不到存量数据(无来源可回填),区间查询会排除空值行。
2026-08-31 13:41:19 +08:00
rcx 2d1e4b5912 feat(oms):出入库导入按编号自动匹配名称并校验客户/物料存在性,模板列位改为动态识别
需求:①入库业务单导入模板只填物料编号+客户编号,物料名称/客户名称按编号自动匹配库中数据
②③出库导入校验客户/物料是否已存在,不存在提示"进行维护" ④库中查询结果为空(确无数据)时报错

一、入库导入(ReservationStockInOrderImpl.parseStockInExcel):
1. 物料逐编号查库,缺失料号收集后一次性报错"物料[xx]不存在,请先维护物料信息后再导入"
2. 明细新增客户编号:去重后查库,为空报错"客户[编号]不存在,请先维护客户信息后再导入";
   查到则以该客户为单据货主并自动匹配名称;与物料归属货主不一致时拦截并提示两者编号
3. 明细回填库中物料名称;未填客户编号时维持原"货主取自物料"逻辑,兼容老模板

二、出库导入(parseExcelFile):
1. 物料/客户校验前移到落库前(原为先落库后查客户,报错会留半截数据)
2. 物料查不到收集后报错维护提示;客户查不到报错维护提示,客户ID/名称改取库中匹配值
  (替换仅返回id且查不到时静默存null的queryShipperId)
3. 產品編號/客户編号漏填的行直接报错提示补全

三、模板解析改为按列头文字动态定位列(繁简体归一化),列/行顺序调整不影响解析,
   识别不到时回落老模板固定列位,老模板行为不变:
1. StockInExcelListener(入库):以產品編號为锚点识别列头行,新增客戶編號列支持
2. DynamicDeliveryExcelListener(出库):左右分栏各自锚点识别;送货日期/线号/车次/
   发票张数/小计/纪录合共改按标签文字定位,行号列号不再写死

四、Mapper:ReservationStockOutOrderMapper新增queryCustomerByCode(按组织查询,
   USER_MEMBER_CODE或USER_SHIPPER.CUSTOMER_NC_CODE任一命中),删除queryShipperId
2026-08-31 11:15:41 +08:00
rcx 2781f0d8d9 feat(oms):库存查询导出新增库存为0是否导出参数 /reservationMaterialInventoryApi/list 新增可选参数 excludeZeroInventoryQuantity,前端"库存为0是否导出"选"是"时传 true,SQL 以 HAVING IFNULL(SUM(inventory_quantity),0)>0 排除汇总库存为0的数据;所有查询维度(all/hz/wl/kw/wlkw/wlExpiry)共6条查询在 GROUP BY 后统一挂载公共片段 excludeZeroInventoryHaving,与 sort_order 对0库存的定义保持一致;不传参数时 SQL 不拼接条件,行为与原来完全一致 2026-08-31 10:17:53 +08:00
王奎兴 8a9bbb3cef 本地配置 2026-08-31 09:33:58 +08:00
rcx 155f1866b4 feat(system):库区管理新增库区面积合计查询接口
新增 GET /storageSectionApi/sumStorageRegion,与库区列表 /list 相同筛选条件及组织数据权限,
SQL 端 IFNULL(SUM(storage_region),0) 返回面积合计(平方米),无数据返回0;
抽取 queryList 数据权限为 applyQueryDataPermission 供列表与合计共用,/list 行为不变。
2026-08-28 18:01:44 +08:00
王奎兴 d4446e8482 发30 2026-08-28 17:35:43 +08:00
秦鸿展 254a804a17 30 2026-08-28 17:10:05 +08:00
秦鸿展 4c4b8f4e29 大屏月台接口只查询预约日期是当天的 2026-08-28 17:06:28 +08:00
zhaoqing 06cf5ce861 fix(oms):明细表修改 2026-08-28 16:22:28 +08:00
rcx 7a01315ca8 feat(sys):导出字段优化与前端显示保持一致 2026-08-28 16:17:48 +08:00
rcx d0773a40d2 feat(transport):基础数据-运营数据-车辆管理增加导出功能
新增 /vehicle/export 导出接口(GET/POST,兼容前端 download 工具),
复用列表查询条件与数据权限,不分页导出全量数据为 xlsx;
新增 VehicleExportVO 导出对象(40列,状态字段转中文)。
2026-08-28 16:12:56 +08:00
rcx 6349181cc3 feat(sys):费用科目导出增加新vo对象
新增 ExpenseAccountExportVO 导出对象(27列业务字段)
2026-08-28 09:20:11 +08:00
rcx e95feb5c24 Merge branch 'refs/heads/dev' into dev-ty1.2 2026-08-28 09:15:39 +08:00
rcx afa502eeaf feat(system):仓库管理-库位管理新增复制功能
需求:库位管理列表增加复制功能,快速复用已有库位配置新建库位。
实现(仿照bms复制账单copyBill的回显式复制):
一、新增接口 POST /storageLocationApi/copyStorageLocation(StorageLocationApi):
    id判空+try-catch兜底,异常返回"复制库单失败:xxx"
二、StorageLocationApplicationService新增copyStorageLocation:
    取原库位PO后清空唯一性字段(storageLocationId不清空会误走修改分支覆盖原库位、
    storageLocationCode同组织同仓库唯一须用户重填、locationBarCode物理唯一)、
    清空审计字段(提交时Assembler按登录人重写)、状态重置初始值
    (status=1空/openingUp=1启用/nullify=1未作废);其余业务字段保留回显
三、前端拿PO回显到新增页,用户改编码后走现有edit接口(id为空即新增)提交,
    天然复用编码唯一性校验、仓库/库区/库位组回填、库位组计数+1,Domain层零改动
2026-08-28 09:14:03 +08:00
rcx 627376da07 feat(sys):费用科目增加复制、导出功能
1、新增 POST /expenseAccountApi/copy 复制接口(回显式,清空主键及科目编码)
2、新增 GET/POST /expenseAccountApi/export 导出接口(按列表查询条件+数据权限全量导出)
3、新增 ExpenseAccountExportVO 导出对象(27列业务字段)
2026-08-28 09:13:10 +08:00
rcx 5127123236 发31 2026-08-27 16:38:33 +08:00
rcx 7b0d08a132 发30 2026-08-27 16:33:43 +08:00
zhaoqing ccdbbda165 Merge remote-tracking branch 'origin/dev' into dev-ty1.2 2026-08-27 14:27:16 +08:00
zhaoqing e3058d8eb1 fix(oms):明细表修改 2026-08-27 14:24:58 +08:00
rcx 5e3a0d86d0 发测试 2026-08-27 14:21:23 +08:00
rcx 76d95c1b2d feat(oms):入库业务单导入模板OrderNO改为只作业务单号,去掉格式校验并按业务单号查重
需求:导入模板表格OrderNO值回显到字段"业务单号";"入库业务单号"仍由系统
原本逻辑生成;业务单号重复不允许导入并提示"业务单号已存在";模板未填单号
保持现有系统逻辑;业务单号去掉规则校验。
一、导入流程(ReservationStockInOrderImpl.parseStockInExcel):
   1. 模板OrderNO只写入业务单号(business_order_no),不再同时作为入库业务单号
   2. 删除validateTemplateOrderNo格式规则校验(R+年月日+3位序号),模板单号不再限格式
   3. 查重由按in_order_number跨两表改为按business_order_no查入库业务单表,
      Mapper新增existsByBusinessOrderNo;重复时报"业务单号已存在:xxx,请修改
      模板中的OrderNO后重新导入"
   4. 入库业务单号(in_order_number)统一由insertDomain按原逻辑生成
      (R+年月日+当日累加,跨reservation/reserve两表取号防撞)
二、insertDomain业务单号赋值改为仅在为空时默认=入库单号,导入带模板单号时不覆盖;
   模板未填单号保持现有逻辑。页面手工新增(DomainService.insert)与客户预约下发
   (ReserveStockInOrderDomainService)为独立链路,不受影响。
2026-08-27 14:10:38 +08:00
秦鸿展 e4b29858a4 南岐GPS三方接口对接 2026-08-27 13:58:10 +08:00
rcx f1dec2298d 发 31 2026-08-26 18:04:44 +08:00
rcx 7b52764ae1 发 30 2026-08-26 17:52:23 +08:00
rcx 1006860698 Merge remote-tracking branch 'origin/dev' into dev-ty1.2 2026-08-26 17:32:40 +08:00
王奎兴 1e00681289 恢复
xxl-job
2026-08-26 17:28:50 +08:00
rcx 353300b643 feat(wms):出入库导入增加料号与规格型号/SKU码一致性校验
- 按料号查物料档案后逐项比对,任何一项不符直接报对应错误:规格型号/SKU码必须与档案完全一致(trim后比较)
- Excel留空而档案有值、或Excel填值而档案为空,均视为不一致报错(提示Excel:空/档案:空)
- SKU码以档案条码(bar_code)为比对基准;出库料号已填写但货主下查不到时直接报错,不再被SKU码/商品名称静默兜底(与入库对齐
2026-08-26 16:56:27 +08:00
rcx 9eb2f347f7 发测试 2026-08-26 15:55:03 +08:00
rcx 182438e1dc fix(system): 费用科目getInfoByOrgAndCode计费单位中英文均为null时过滤,避免返回null-null 2026-08-26 15:49:58 +08:00
rcx 0be73e9bfe fix(oms): 出入库联合列表(inAndOutList)查询条件修复与补全
1. 修复 ReflectionException:mapper XML <if> 引用 inOrOut/orderNumber 但 DO 无对应字段,
   DO/DTO 新增 inOrOut、orderNumber 查询字段(BeanUtils 同名自动拷贝,前端参数直达 mapper)
2. inOrOut 排除空串与 'all'(前端"全部"约定值),避免拼出 ="all" 查空
3. orderNumber 排除空串并改为 like 模糊匹配(与出库单列表单号搜索口径一致)
4. status 新增精确筛选(0 为有效条件值,按前端约定直传,"全部"时不传)
2026-08-26 15:49:38 +08:00
秦鸿展 5e369bdd99 31 2026-08-26 14:23:36 +08:00
王奎兴 a5a222a78e Merge branch 'dev' into dev-ty1.2
# Conflicts:
#	mhd-modules/mhd-system/src/main/resources/bootstrap.yml
2026-08-26 14:11:45 +08:00
王奎兴 b1840c8965 Merge remote-tracking branch 'origin/dev' into dev 2026-08-26 14:08:38 +08:00
王奎兴 02a0b7aab6 xxl-job 2026-08-26 14:08:24 +08:00
rcx 42ab5160ed 发30 2026-08-26 14:03:42 +08:00
秦鸿展 df66287455 31 2026-08-26 12:55:44 +08:00
秦鸿展 e2ce186aba 30 2026-08-26 12:49:16 +08:00
秦鸿展 9be0de0ae7 园区月台预约车辆显示修改 2026-08-26 12:48:06 +08:00
rcx 4410162f39 发测试 2026-08-26 10:03:46 +08:00
rcx 3060fcd6a4 feat(product):新增附件原始文件名字段SQL并去掉上传错的文件 2026-08-26 09:53:19 +08:00
rcx 7d8e1c3872 feat(wms):复核详情增加合计数;库存分配新增通宇专用可分配库存查询(物料+箱号/卡板+Sheet Ref匹配
1. 复核详情(getInfo/getInfoReviewOrderNumber)头表回填totalReviewedQty/totalPlanQty/materialKindCount,
   口径与列表queryList聚合一致(计划量=Σifnull(material_plan_qty,pending+reviewed))
2. 新增 GET /materialInventoryApi/assignableList,按通宇账号分流(TongyuOrgUtil:组织ID=2830或组织名含"通宇"):
   - 通宇:启用新匹配。双模式入参——传outOrderId+materialDetailId(须成对)则匹配值全部后端自取出库单/明细;
     不传则按平铺参数(materialBaseInfoId/materialCode+boxPalletNo+sheetRefNo,多值串自动拆token OR-LIKE)。
     固定条件:del_flag=1且可分配数量(allocation_quantity)>0,不分页全量返回,不套用用户组织数据权限
   - 非通宇:保持原/list行为,内部直调list();进原查询前清除materialDetailId(新接口专用参数,防泄入
     原查询的a.material_detail_id=上架单明细ID条件导致查空)
3. autoAssign自动分配同步分流:通宇走token匹配,非通宇保持原eq精确匹配不变
4. 新增util/BatchNoTokenUtil(批次属性多值拆分)、util/TongyuOrgUtil;库存批次属性回填抽为
   fillBatchAttributes供/list与可分配查询复用,原逻辑行为不变
2026-08-26 09:30:01 +08:00
秦鸿展 59249be9b0 31 2026-08-26 00:06:41 +08:00
秦鸿展 8fe5e079dd 新增月台预约楼层车辆查询 2026-08-26 00:01:40 +08:00
秦鸿展 d1d55ce52a 30 2026-08-25 23:00:55 +08:00
秦鸿展 b12517e5b5 30 2026-08-25 22:28:57 +08:00
秦鸿展 09d24217b5 统计业务类型 2026-08-25 22:25:12 +08:00
秦鸿展 3c2f252347 统计业务类型 2026-08-25 22:23:18 +08:00
秦鸿展 940e4c56b0 31 2026-08-25 20:24:10 +08:00
秦鸿展 09ab103d1d 30 2026-08-25 20:18:31 +08:00
秦鸿展 8dcb2334d5 BI月台数据改造 2026-08-25 20:11:52 +08:00
rcx 7a5e124f98 fix(oms): 入库模板导入优化异常提示信息 2026-08-25 17:51:54 +08:00
rcx c5a2508a36 发测试 2026-08-25 16:29:15 +08:00
rcx 7353021d92 feat(oms):入库模板导入支持模板自带业务单号,重复拦截并按R+年月日规则生成
1. 模板OrderNO解析兼容无标签新模板(E1直接为单号值)
2. 导入单号优先模板自带:先校验R+年月日+3位序号格式(含年月日有效性),
   再跨reservation/reserve两表查重,重复提示"入库业务单号已存在"
3. 模板无单号时按R+年月日+当日累加生成(insertDomain不再无条件覆盖已设置单号,清理无效的仓库规则生成死代码)
2026-08-25 16:11:19 +08:00
rcx 735cf9af9f feat(oms):入库导入模板饮用日期解析修复,入库单列表联查带出饮用有效日期
1. 入库/执行入库Excel解析监听器:扩充日期格式支持(yyyy年M月d日、d/M/yyyy、M/d/yyyy、dd.MM.yyyy等16种),
   改为严格解析防止宽松解析产生公元1~99年脏数据,新增Excel日期序列号兜底及年份合理性校验,
   解析失败抛出带行号的明确提示而非静默置空
2. parse-excel接口异常时返回具体错误信息(原为空body 500)
3. 入库单列表queryList联查明细表取最早饮用日期(MIN drink_date),PO及resultMap补drinkDate映射
2026-08-25 15:07:04 +08:00
秦鸿展 f2f3fb9c19 30 2026-08-25 11:09:23 +08:00
秦鸿展 a62ed7c60f 31 2026-08-25 11:05:06 +08:00
王奎兴 4168f921f6 查询nc日志排序修改; 2026-08-25 10:52:28 +08:00
王奎兴 1916152775 查询委托方去除组织条件; 2026-08-25 10:41:58 +08:00
rcx 83f4cce451 feat(oms):App端出入库明细跳详情补入库按单号查询,预约单修改限待提交状态
需求:入库/出库明细列表点击跳详情;待提交预约单可修改(暂存/提交均走/edit,与PC共用链路)
1.ReserveStockInOrderApi新增getInfoByInOrderNumber按单号查详情:
  入库明细行只带inOrderNumber(明细表无in_order_id列),对齐出库侧getInfoOutOrderNumber;
  del_flag=1过滤已删单+getOne不抛单号重复
2.ReserveStockInOrderMapper.xml MaterialDetailResult显式映射in_order_number,
  明细行单号不再依赖驼峰自动映射开关(出库侧:363本就有,入库侧补齐)
3.入库/出库DomainService.update加状态闸门:仅status=1(已创建)且issueStatus!=3(未确认)允许修改,
  防已确认下发/已流转单据被改写(仓码确认只置issueStatus=3不改status,原校验缺此维度必然漏防);
  明细为空改报'请添加货物明细'(原getMaterialDetailList().size()空指针500);出库update补@Transactional
2026-08-24 18:12:50 +08:00
王奎兴 f5f792d3f8 发31 2026-08-24 17:03:55 +08:00
王奎兴 dbd61c0519 发30 2026-08-24 16:58:26 +08:00
王奎兴 cb5d933e0a 渗透测试修改; 2026-08-24 16:53:20 +08:00
王奎兴 5298e9d4e3 发30 2026-08-22 19:35:56 +08:00
王奎兴 06f61d7957 发31 2026-08-22 19:32:52 +08:00
王奎兴 4567f00858 发测试 2026-08-22 18:46:36 +08:00
rcx e8328b0159 Merge remote-tracking branch 'origin/dev' into dev 2026-08-22 18:39:12 +08:00
rcx bb3dfaed4a fix(oms):出库业务单线路/车次同步运输业务单,补齐触发入口与拆单时序
出库业务单与运输业务单为两张独立表,仅靠发票号经配送明细关联,
此前无字段同步逻辑,出库单填写的线路/车次不会流到运输业务单。

1. ReservationStockOutOrderDomainService新增syncLineInfoToBusinessOrder:
   按关联链 出库单号->reservation_delivery_details_link.invoiceNumber
   ->business_delivery_details_link.invoiceNumber 定位关联业务单,
   以出库业务单为源头覆盖lineCode/lineName/shuttleNo。
2. 触发入口三个:审核通过(auditState)/下发(issueOrder)/执行并下发
   (executeAndIssue);取消下发不触发。下发与执行并下发不经过审核
   动作,只挂审核入口会漏同步。
3. 同步目标同时覆盖orderId(未拆单业务单/拆单后主单)与subOrderId
   (拆单子单):仅同步subOrderId时,出库先审核而运输单尚未拆单
   (subOrderId为空)会落空,且主单无值导致后续拆单子单拷贝不到。
4. 同步失败仅记error日志,不阻断审核/下发主流程
   (状态更新与同步非同一事务,关联数据异常不应导致接口报错)。

边界:运输业务单手工创建且未录配送明细时无关联链不同步,需页面自行
填写;关联依赖发票号两侧一致,与既有退货回写为同一关联假设。
2026-08-22 18:38:39 +08:00
王奎兴 0eb2f323c7 发31 2026-08-22 18:29:36 +08:00
王奎兴 a35030e2f5 登录报三方异常处理; 2026-08-22 18:27:56 +08:00
王奎兴 d1b1b27ab0 发30 2026-08-22 17:46:47 +08:00
王奎兴 4a6a2798e8 发31 2026-08-22 17:36:27 +08:00
rcx 9068fa0e40 feat(wms):WMS出库单增加线路车次字段,App出库单详情显示线路/车次
背景:App出库单详情调wmsApi/stockOutOrderApi/getInfo,数据在WMS
stock_out_order表,该表未加线路/车次字段导致不回显。

1. mhd-api StockOutOrder(OMS下发Feign DTO)加lineCode/lineName/shuttleNo,
   OMS执行单下发为copyProperties同名拷贝,自动透传,OMS代码零改动。
2. WMS stock_out_order表加三列(脚本追加);entity/PO加字段,
   Feign接收按JSON字段名对齐落库;Mapper XML resultMap与显式列清单补列,
   getInfo(getById自动)与列表均可返回。
3. 部署需重新打包mhd_oms与mhd_wms(OMS依赖的mhd-api jar变更)。
   存量已下发WMS的老单无值,OMS/WMS跨库无法批量刷数,新下发单自动带。
2026-08-22 17:23:48 +08:00
rcx 99cb355640 发测试 2026-08-22 16:50:12 +08:00
rcx c9520c65be feat(wms):上架默认库位填充限定谷丰组织(id=2842)生效 2026-08-22 16:38:18 +08:00
rcx 2a4e39f755 Merge remote-tracking branch 'origin/dev-ty1.2' into dev-ty1.2 2026-08-22 14:10:07 +08:00
rcx a08d8a7fd5 feat(oms):出库导入校验送貨線號/車次并回填线路名称
1. 出库导入(预约/执行两侧)增加validateLineAndShuttle校验:
   送貨線號/車次缺失直接报错;线路须在WMS线路管理存在(route_code精确匹配);
   车次须在该线路车次清单(shuttle_json)内;校验通过回填lineName,
   线路/车次一并落lineCode/lineName/shuttleNo。
2. WmsServiceFeign新增listLineManagement(复用WMS /lineManagementApi/listByApp)
   供OMS跨库查线路,附降级实现;顺带移除FeignClient写死的本机url,恢复Nacos服务发现。
3. 执行侧DynamicDeliveryExcelListener(关键字版)补送貨線號/車次独立成行的解析,
   原实现仅支持与送貨日期同行取值,该模板为分行布局导致解析不到。
2026-08-22 14:08:22 +08:00
rcx ab7219e600 feat(wms):上架管理PC上传页默认填充指定上架库位(维他奶仓→2层冷凍A區A01),仅回显不落库 2026-08-22 14:06:56 +08:00
王奎兴 1b3b1a6394 Merge branch 'dev' into dev-ty1.2
# Conflicts:
#	mhd_wms/src/main/resources/bootstrap.yml
2026-08-22 13:59:22 +08:00
王奎兴 072a4f7d1f 退货验证发票号是否重复;维他奶导入单号添加校验; 2026-08-22 11:17:42 +08:00
王奎兴 63a19fbbcd 退货入库单编辑后才显示数量; 2026-08-22 10:26:54 +08:00
rcx 5fc491f612 fix(oms,wms):执行单重复下发致WMS收货明细翻倍修复,executeAndIssue获取仓库信息失败修复
一、收货明细翻倍(executionStockInOrderApi/issueOrder下发后,WMS收货管理详情明细×2):
根因:执行单下发接口无防重 + WMS接收端无幂等。executeAndIssue已自动下发过的执行单
仍可被手动再次下发(issueTime已置但无校验),重复推送使WMS插入同号入库主单+第二份
in_material_detail,再生成第二张同号收货单(收货单号"RV-"+入库单号确定性生成),
收货管理详情按单号查明细即双倍。四层防御:
1. OMS执行单issueOrder补防重:issueTime非空拒绝下发(与executeAndIssue对齐),
   inOrderIds去重防同请求重复推送
2. WMS omsOrderAdd主单幂等:同单号有效单(del_flag=1)已存在则跳过插入
3. WMS omsOrderAddDetail明细幂等:按oms_in_material_detail判重,已存在行不再插入;
   未传ID的行保持原插入行为
4. WMS autoAuditOmsInOrder取单补orderByAsc(in_order_id)消除同号取单随机性;
   auditApprove生成收货单前按收货单号查重过滤,已生成过的单不再重复生成
二、executeAndIssue报"获取仓库信息失败":
1. 明细行warehouseId=0时兜底改用主表仓库(与下发issueOrder兜底逻辑对齐),
   不再拿无效仓库ID调system服务
2. executeAndIssue/issueOrder两处报错增强为带仓库ID与错误信息,便于定位
存量已翻倍数据(重复入库单/明细/收货单)需另行软删清理,本次仅防新增
2026-08-21 18:21:03 +08:00
rcx b209d8cb6d fix(oms):出库明细materialBaseInfoId自动纠正,杜绝脏物料ID落库
问题:客户端推单把materialInventoryId传进materialBaseInfoId字段(或漏传为0),
出库明细存脏ID,编辑/保存报"物料基础信息不存在"(如DCCK034存了库存ID477/444,
真实物料ID应为233/231)。预约端校验原先静默跳过,脏数据持续入库(7-15起200+行)。

一、新增MaterialBaseInfoResolveService三级解析:
    baseId直查 → 库存ID反查纠正 → 物料编码+组织兜底,全部失败返回null
二、三处setMaterialDetailInfo改为纠正+回填(预约/业务/执行出库单):
    查不到抛错带定位信息(baseId/库存ID/物料编码/单号);
    纠正后的ID随保存落库,旧脏单重新保存即自愈;
    预约端由静默跳过改为抛错
三、两处pushOutOrderDetail推送入口(预约/业务)加纠正,
    反查不到拒绝保存,脏数据不再入库

注:存量脏数据修复SQL另行手动执行;下发复制链(预约→业务→执行)不走校验,
2026-08-21 18:20:22 +08:00
rcx 06aae196c7 发测试 2026-08-21 16:59:56 +08:00
rcx e91c5801eb 发测试 2026-08-21 15:54:46 +08:00
rcx 8704df738c Merge remote-tracking branch 'origin/dev' into dev-ty1.2 2026-08-21 15:47:44 +08:00
rcx 000092952f feat(oms,wms):出库/运输单据增加线路车次字段,线路管理支持车次清单维护
1. OMS四张表(business_document_order/execute_order/
   reservation_stock_out_order/execution_stock_out_order)加
   LINE_CODE/LINE_NAME/SHUTTLE_NO三列,entity/DO/DTO/PO同步加字段,
   出库两链路Mapper XML补列与resultMap。
   线路编码/名称冗余存line_management的route_code/route_name
   (两库不互通无法join);SHUTTLE_NO为字符串(如4或31-4),
   前端拼"编码-车次"显示(31-4车次);不复用route(存tms_route.id)
   和train_no(入库链路占用)。
   下发链路(预约单->执行单,业务单->执行单)同名拷贝自动透传。
2. WMS line_management加SHUTTLE_JSON列(车次序号JSON数组),
   四层加字段+XML;DTO加反序列化器兼容前端传数组,
   PO加序列化器返回原样输出数组。
3. LineManagementImpl.update组织/仓库名称空串兜底,
   修复编辑线路后组织名称被刷成空串、列表查不到的问题。
4. 新增建表脚本sql/20260820_oms_outbound_transport_add_line_shuttle.sql(达梦语法)。
2026-08-21 15:28:50 +08:00
rcx 22f2b66ffa feat(oms,wms):库存导出按到期日分组排序并合并同物料同到期日库存(仅wlExpiry查询生效)
需求:库存查询导出按商品到期日分组归类、相同到期日相邻显示,相同物料相同
到期日合并库存。仅在 queryRule=wlExpiry(物料+到期日汇总查询)导出时生效,
列表显示不变;同时回退 8fedb6d6 对列表默认排序的改动。

一、列表默认查询还原(回退 8fedb6d6):OMS queryList 恢复 order by a.update_time desc,
    Api 恢复 update_time 内存重排(空值最后),两侧列表排序与改动前一致。
二、导出排序(OMS/WMS queryListByWlExpiry):/list 新增可选参数 orderByExpiryDate,
    仅 queryRule=wlExpiry 时生效:
    1. wlExpiry + orderByExpiryDate=true(导出):到期日倒序(日期近的在前)、空值最后、
       0库存靠后、同到期日按更新时间倒序;排序在SQL层,跨页/全量导出分组不被打散;
       OMS Api 传参时跳过 update_time 内存重排,避免覆盖SQL排序
    2. 仅 wlExpiry 不带参数:排序保持原样(sort_order+更新时间倒序)
    3. 其他查询方式:不受影响,参数被忽略
三、合并:wlExpiry 查询 GROUP BY 物料+到期日+货主(货主隔离),相同物料相同到期日
    合并一行,库存/可用/冻结数量及净重/毛重/体积/面积 SUM 汇总(复用现成查询,未改)
四、前端导出:/list + queryRule=wlExpiry + orderByExpiryDate=true + 当前查询条件(不分页)
2026-08-21 15:13:28 +08:00
rcx 3aec384ba0 fix(wms):出库委托单导入出仓日期丢失、升列与单价回填修复
- HeaderListener:parseDate升级为parseDateSmart,兼容Excel日期序列号(20000~60000≈1954~2064年,
  POI DateUtil.getJavaDate转换)及yyyy/M/d、yyyy.M.d、yyyy年M月d日等多格式文本;
  出仓日期/完成时间/下单日期三处调用点同步切换;解析失败记入dateParseErrors不再静默
- ExcelParseService:导入校验汇总dateParseErrors,日期单元格有值但格式不识别时阻断导入并
  明确提示(为空仍放行,与原行为一致);单价回填从料号分支移至料号/SKU/名称三分支统一回填块,
  补齐SKU、商品名称匹配场景
- ItemListener:升列新增normalizeNumeric归一化,去千分位逗号、空值统一存null、纯数字去尾零,
  落库值恒为"纯数字或null",前端合计不再产生NaN/拼接
2026-08-21 14:56:12 +08:00
rcx 27f3fceb6e feat(wms):出入库导入增加料号与规格型号/SKU码一致性校验
- 料号命中物料档案后:规格型号/SKU码必须与档案一致,Excel留空且档案有值同样报错(提示Excel:空);档案值为空则跳过该项
- SKU以档案条码(bar_code)为基准;错误行一次性汇总提示,拒绝导入
- 出库料号已填写但货主下查不到时直接报错,不再被SKU码/商品名称静默兜底(与入库行为对齐)
2026-08-21 14:42:25 +08:00
王奎兴 3e5ed28d69 发30 2026-08-20 18:14:37 +08:00
王奎兴 78d40d35be 发31 2026-08-20 18:13:30 +08:00
rcx 8fedb6d6ed feat(oms):库存查询/导出按商品到期日分组排序 2026-08-20 17:54:23 +08:00
王奎兴 b8de1c00eb 发31 2026-08-20 17:41:09 +08:00
王奎兴 bf080609fe 发30 2026-08-20 17:39:44 +08:00
王奎兴 b415ed6fa0 出库预约业务类型查询; 2026-08-20 17:34:43 +08:00
王奎兴 694ed699ec 单号生成规则包含已删除; 2026-08-20 17:26:56 +08:00
王奎兴 0b26692a34 发31 2026-08-20 15:35:31 +08:00
王奎兴 c2af421c7f 发30 2026-08-20 15:31:40 +08:00
王奎兴 57f87baf85 发30 2026-08-20 15:30:59 +08:00
王奎兴 e23edadde6 发30 2026-08-20 14:59:53 +08:00
王奎兴 530254e000 发31 2026-08-20 14:57:14 +08:00
rcx 33d085f9b9 fix(oms):入库链路计划数量翻倍修复
明细batchUpdate软删条件缺陷(入/出库共6个明细实现类):软删旧明细要求本次提交
   至少一行走更新分支,"删光旧物料全部换新物料"时提交列表全为新增行,旧行漏软删
   (del_flag仍=1),下发后执行单计划数量=旧+新叠加。改为库内有明细即对账软删
2026-08-20 14:45:13 +08:00
王奎兴 21fcc24e2e 发31 2026-08-20 14:20:07 +08:00
王奎兴 64350f565b 列表查询id不对; 2026-08-20 14:16:23 +08:00
rcx ff2b1965f7 fix(oms):入库业务单单号重复与入库链路计划数量翻倍修复
一、单号重复:业务单与客户预约两套取号计数器互不可见致撞号。
取号统一为跨两表MAX+1+存在性校验,保存前校验预取号占用,下发不再换号,Mapper新增existsByOrderNumber。
二、计划数量翻倍:编辑明细为软删(del_flag=2留存),下发查明细未过滤del_flag,
软删行被复制进下游单据并计入头表数量(执行单数量≈业务单×2+修改差值)。
1. 预约→业务单下发、执行单推WMS:明细查询补del_flag=1过滤
2. executeAndIssue(执行并下发):补防重复下发校验(issueTime非空拒绝)
3. pushInOrder/pushInOrderDetail:加幂等,防Feign重试插重复单
2026-08-20 14:06:50 +08:00
王奎兴 7a769071e4 发30 2026-08-20 10:48:34 +08:00
王奎兴 b05f994e74 发31 2026-08-20 10:35:34 +08:00
rcx ef13d890be fix(oms):入库执行单列表单号搜索查不到,businessNumber死条件改单号模糊互查;库存查询货主过滤移回主表并修正委托方误判
- 入库执行单列表:businessNumber条件原为BUSINESS_NUMBER精确等值,该列无写入恒空致全条件查询0条,改为业务单号/入库单号OR模糊互查

- 入库执行单列表:inOrderNumber精确等值改模糊like;删除重复的createByName条件;补businessInOrderNumber过滤(业务单号/入库单号互查)
- 库存查询:货主过滤(shipperId/Code/Name/shipperInfo)从物料表b移回库存主表a,避免LEFT JOIN退化INNER JOIN丢行
- 库存查询:委托方判定由组织层级改为UserPo.shipperEnterpriseName,避免二级组织运营人员(2841/2842)被误判强制按本人userId过滤
2026-08-20 10:17:16 +08:00
rcx cc943d164b fix(wms):入库单导入表头字段错位、备注丢失
- 取值向右查找时误把下一个标签/选项说明当值(承运方←Tel:、生产商←客户名称:、监管方式←送货地址:、送货地址←查验选项),现遇疑似标签即停止查找
- 备注:xxx同格书写为全角冒号,原仅按半角:拆分导致备注丢失,现全角/半角兼容
- 表头列遍历上界改用getLastCellNum,防空单元格漏扫尾部列"
2026-08-20 10:06:50 +08:00
rcx 77420e9617 feat(sys):合同附件增加原始文件名存储字段,配合上传v2回显原名称
需求背景:合同管理-上传的文件需显示原始文件名(原显示 LOCAL:D:\upload\{uuid}.pdf 之类存储键)。
附件此前仅存 fileKey,原始文件名在上传落盘时已被 UUID 重命名丢失,回显无原名可用。
改动内容:
1. contract_manage 表新增 4 个附件原始文件名字段(见 sql/20260818_contract_attachment_original_name.sql):
   contract_original_name    合同正本附件原始文件名
   integrity_agreement_name  廉政协议附件原始文件名
   security_protocol_name    安全协议附件原始文件名
   other_attachments_name    其他附件原始文件名(多文件与 other_attachments_url 同分隔符、同顺序)
2. ContractManage / ContractManagePO / ContractManageDO / ContractManageDTO
   四个对象同步新增对应 4 个 name 字段,保存、详情、列表全链路透传。
   Assembler 与 ContractManageImpl 为 BeanUtils 同名拷贝、Mapper XML 为 SELECT a.*,均无需改动。"
 "配套依赖:
- mhd-product 新增 /menu/upload/v2 返回 { fileKey, originalName }(另一提交)
- 前端上传后成对保存 fileKey 与 originalName,回显显示 name 字段
兼容说明:
- 存量合同 name 字段为空,前端回退显示 fileKey 末段(uuid 名),属预期兼容行为
- 纯新增字段,旧代码不读写,不影响既有接口
2026-08-19 18:08:59 +08:00
rcx fa51580985 feat(product):附件上传v2返回原始文件名,下载接口支持inline预览与中文文件名 2026-08-19 18:04:57 +08:00
rcx 362f1b5e2f fix(bms):科目编码重复致新增账单失败,税率查询按编码+名称取唯一;复制账单改生成新计费流水
- getTaxRate 增加 SUBJECT_NAME 匹配,费用科目编码重复(如1003同时有运输处理费/报关费3)时不再抛 TooManyResultsException,查不到税率兜底0
- addBill、开票/NC推送共4处调用点同步调整;计费流水号为空时只处理自身明细,防空指针
- copyBill 明细清空计费流水id并按原流水映射生成新JFLS流水号,防止复制提交后把原账单的流水改挂到新账单(原账单明细丢失)
- insertAndReturnId 币种空值兜底CNY(与addCost对齐)
2026-08-19 18:02:23 +08:00
rcx b859aa99d3 fix(oms):入库业务单导入件数未回显计划数量
- 导入模版去掉Qty(cases)列(5列布局),饮用日期列位前移
- 明细计划数量由Qty箱数改为"件数"取值,件数同步落库PIECE_COUNT用于列表回显
- 物料在当前登录组织下不存在时返回明确提示,替代货主列表取值越界导致的500
2026-08-19 17:59:19 +08:00
rcx 83f05dd11d feat(bms):新增费用统计分析报表,科目按模板类别归并、仓储拆散租/固定出租两行、行级占营收比,含34列导出与智能分析归因 2026-08-19 15:36:53 +08:00
rcx dd0bf32c35 feat(gateway):登录验证码改为纯数字,字符源限定0-9
需要将网关yml文件中 math 改成 char
security:
  captcha:
    enabled: true
    type: char   # math=算术题,char=纯数字
2026-08-19 14:28:45 +08:00
rcx 7ed12c9405 feat(bms):应收账单添加费用币种空值兜底CNY、费用明细增加流水备注保存与显示 2026-08-19 11:02:52 +08:00
rcx 72e445ea16 fix(oms):出库添加货物查询不到商品,库存查询改租户+货主维度并出库列表补货主旁路
1.ReservationMaterialInventoryApplicationService.queryList原对非南光组织用户强制organization_id=用户组织且top=2827,委托方(如仓码2841货主)库存被滤空;改为按一级组织(租户)过滤,委托方自动叠加登录货主shipperId查自己的货,库存数据已核实在库
2.ReserveStockOutOrderApplicationService.queryList/detailsList增加货主直查旁路(与入库queryList对齐),委托方按shipperId查询不被组织过滤拦截
2026-08-18 18:09:53 +08:00
rcx f50a3e690a fix(oms):出库预约落库回填仓库/组织/货主,状态默认已创建
与入库侧同款修复:insert按仓库档案回填组织/一级组织/仓库编码名称(兜底2841),委托方回填shipperId/customerId,status默认1已创建(配合App订单状态只读);补fastjson import
2026-08-18 18:09:41 +08:00
rcx 8692b38671 fix(oms):入库预约App提交后各端不显示,落库回填仓库/组织/货主并支持货主维度查询
根因:App提交的预约单organization_id/shipper_id落库为空、warehouse_code全空,列表按组织与仓码过滤必然查不到
1.ReserveStockInOrderDomainService.insert:
  -新增明细非空校验,App不传明细时返回'请添加货物明细'而非NPE
  -按warehouseId查仓库档案回填组织(仓码2841/谷丰2842按仓库归属)/一级组织(2827)/仓库编码名称,仓库缺失时兜底登录人一级组织+2841
  -委托方(有货主档案)未传shipper时回填登录人userId至shipperId/customerId
  -订单状态默认'新增'/status默认1(配合App端订单状态改只读)
2.ReserveStockInOrderApplicationService:
  -detailsList增加货主直查旁路(对齐queryList既有逻辑),委托方按shipperId查询不再被组织过滤拦截
  -inAndOutList原无任何数据权限,委托方登录强制注入本人shipperId,防止传错组织(如通宇2830)导致列表滤空并防越权
2026-08-18 18:09:34 +08:00
rcx f77d89fa7f fix(thirdParty):短信验证码发送失败误包成成功响应,失败原因取result并返回error
SmsApi.sms发送失败分支原返回AjaxResult.success(发送失败)且getMessage()取的是默认文案,现改为AjaxResult.error并携带乐信通返回的失败原因,App端可感知发送失败原因
注:收不到验证码的主因(网关白名单/租户域名映射)为Nacos与DB配置项,不在代码仓库
2026-08-18 18:09:25 +08:00
rcx 0ff327a071 fix(auth):App注册接口空实现,补全手机号+短信验证码注册链路
1.AppTokenController.register改为透传完整LoginDTO
2.TokenLoginApplicationService.register原为TODO空壳(仅格式校验直接返回成功),现补全:注册短信验证码校验(SMS_REGISTER redis)→域名解析一级组织→按手机号查重→复用registerAddUser创建用户(含默认角色/YMS同步)→注册成功清验证码并记录日志
3.配套网关白名单(/auth/app/register,/auth/app/getOrganizationByPath/**)需在Nacos发布,不入库
2026-08-18 18:09:18 +08:00
rcx 6661c016e8 fix(auth):密码登录账号/密码错误无提示,登录配置读取与全局异常兜底
1.SysPasswordService.validate登录重试配置(login/maxRetryCount,lockTime)读取增加try/catch与空值保护,三方服务异常或配置缺失时降级默认值(错5次锁30分钟)并warn日志,不再NPE/NumberFormatException导致msg为空
2.GlobalExceptionHandler的RuntimeException/Exception分支对e.getMessage()为空兜底为'系统繁忙,请稍后重试',保证App端始终能拿到可展示的错误信息
2026-08-18 18:08:49 +08:00
rcx bca8b5fbed fix(oms):入库执行单业务单号不随下发复制,执行并下发入口补充有效运输业务单回填 2026-08-18 11:36:21 +08:00
rcx e489015e3e fix(oms):下发过滤已删除明细,执行单不再残留已取消运输业务单号 2026-08-18 10:19:40 +08:00
327 changed files with 14278 additions and 1122 deletions
@@ -18,7 +18,7 @@ import java.util.Set;
/**
* bms财务结算系统feign调用接口
*/
@FeignClient(contextId = "BmsService", value = ServiceNameConstants.BMS_SERVICE, url = "http://10.33.0.109:8019",fallbackFactory = RemoteBmsFeignFallbackFactory.class, configuration = FeignAutoConfiguration.class)
@FeignClient(contextId = "BmsService", value = ServiceNameConstants.BMS_SERVICE, url = "http://10.102.192.3:8012",fallbackFactory = RemoteBmsFeignFallbackFactory.class, configuration = FeignAutoConfiguration.class)
public interface BmsServiceFeign {
@@ -16,7 +16,7 @@ import org.springframework.web.bind.annotation.RequestBody;
import java.util.List;
import java.util.concurrent.CompletableFuture;
@FeignClient(contextId = "OmsService", value = ServiceNameConstants.OMS_SERVICE, url = "http://10.33.0.99:8017",fallbackFactory = RemoteOmsFeignFallbackFactory.class, configuration = FeignAutoConfiguration.class)
@FeignClient(contextId = "OmsService", value = ServiceNameConstants.OMS_SERVICE,url = "http://10.102.192.4:8017",fallbackFactory = RemoteOmsFeignFallbackFactory.class, configuration = FeignAutoConfiguration.class)
public interface OmsServiceFeign {
@@ -237,6 +237,12 @@ public interface SystemServiceFeign {
@GetMapping("/warehouseApi/getInfo/{warehouseId}")
public AjaxResult getWarehouseInfoByWarehouseId(@PathVariable("warehouseId") Long warehouseId);
/**
* 根据warehouseId获取仓库及下库区及库位树(与前端 warehouseApi/getWarehouseChildren 一致,用于PC上架页默认库位回显)
*/
@GetMapping("/warehouseApi/getWarehouseChildren")
public AjaxResult getWarehouseChildren(@RequestParam("warehouseId") Long warehouseId);
/**
* 根据storageSectionId获取对应库区信息
*/
@@ -26,7 +26,7 @@ import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
@FeignClient(contextId = "commonWlhyServiceFeign",value = ServiceNameConstants.WLHY_SERVICE,url = "http://10.33.0.129:8014",configuration = FeignAutoConfiguration.class)
@FeignClient(contextId = "commonWlhyServiceFeign",value = ServiceNameConstants.WLHY_SERVICE,url = "http://10.102.192.30:8014",configuration = FeignAutoConfiguration.class)
public interface WlhyServiceFeign {
/**
@@ -21,7 +21,7 @@ import java.util.Map;
* @description: TODO
* @date 2024/5/10 8:58
**/
@FeignClient(contextId = "remoteWmsServiceFeign",value = ServiceNameConstants.WMS_SERVICE, url = "http://10.33.0.109:8016",fallbackFactory = RemoteWmsFallbackFactory.class, configuration = FeignAutoConfiguration.class)
@FeignClient(contextId = "remoteWmsServiceFeign",value = ServiceNameConstants.WMS_SERVICE, url = "http://10.102.192.3:8016",fallbackFactory = RemoteWmsFallbackFactory.class, configuration = FeignAutoConfiguration.class)
public interface WmsServiceFeign {
/**
@@ -151,4 +151,12 @@ public interface WmsServiceFeign {
@ApiOperation("OMS批量签名-同步WMS入/出库单签名字段")
@PostMapping("/stockSignatureApi/batchSign")
public AjaxResult batchSignWms(@RequestBody SignatureBatchFeignDTO dto);
/**
* OMS查询WMS线路管理列表(按线路编码模糊查,OMS侧自行精确匹配),
* 出库导入时校验送貨線號/車次并回填线路名称
*/
@ApiOperation("OMS查询WMS线路管理列表")
@GetMapping("/lineManagementApi/listByApp")
public AjaxResult listLineManagement(@RequestParam("routeCode") String routeCode);
}
@@ -0,0 +1,23 @@
package com.mhd.system.api;
import com.mhd.common.core.constant.ServiceNameConstants;
import com.mhd.common.core.domain.entity.R;
import com.mhd.common.core.domain.dto.YmsSysUserDTO;
import com.mhd.system.api.domain.YmsLoginUser;
import com.mhd.system.api.factory.RemoteBmsFeignFallbackFactory;
import com.mhd.system.api.factory.RemoteXxlJobFeignFallbackFactory;
import com.mhd.system.api.feign.FeignAutoConfiguration;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
@FeignClient(contextId = "XxlJobService", value = ServiceNameConstants.XXL_JOB_SERVICE,url = "http://127.0.0.1:8020",fallbackFactory = RemoteXxlJobFeignFallbackFactory.class, configuration = FeignAutoConfiguration.class)
public interface XxlJobServiceFeign {
@GetMapping("/user/openFeignTest")
public R<Map<String, Object>> openFeignTest(@RequestParam("param") String string);
}
@@ -9,7 +9,7 @@ import com.mhd.system.api.feign.FeignAutoConfiguration;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.*;
@FeignClient(contextId = "YmsService", value = ServiceNameConstants.YMS_SERVICE,url = "http://10.33.0.99:8018",fallbackFactory = RemoteBmsFeignFallbackFactory.class, configuration = FeignAutoConfiguration.class)
@FeignClient(contextId = "YmsService", value = ServiceNameConstants.YMS_SERVICE,url = "http://10.102.192.4:8018",fallbackFactory = RemoteBmsFeignFallbackFactory.class, configuration = FeignAutoConfiguration.class)
public interface YmsServiceFeign {
/**
@@ -375,6 +375,11 @@ public class InMaterialDetail extends BaseVOEntity {
@DateTimeFormat(pattern="yyyy-MM-dd")
private Date drinkDate;
@ApiModelProperty("过期日期")
@JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
@DateTimeFormat(pattern = "yyyy-MM-dd")
private Date expiryDate;
@ApiModelProperty("来货验收是否合格;1 合格,0 不合格")
@Excel(name = "来货验收是否合格;1 合格,0 不合格")
private String arrivalAcceptQualified;
@@ -83,6 +83,8 @@ public class MaterialInventoryOmsParamDO extends BaseVOEntity {
private Date productionDate;
//过期日期
private Date expiryDate;
//进货日期
private Date purchaseDate;
private String executioOrderNumber;
@@ -405,4 +405,13 @@ public class StockOutOrder extends BaseVOEntity {
@Excel(name = "车次")
private Integer trainNo;
@ApiModelProperty("线路编码(取自WMS线路管理)")
private String lineCode;
@ApiModelProperty("线路名称(冗余存储)")
private String lineName;
@ApiModelProperty("车次序号(显示: 线路编码-车次, 如31-4)")
private String shuttleNo;
}
@@ -0,0 +1,30 @@
package com.mhd.system.api.domain;
import lombok.Data;
import java.io.Serializable;
import java.util.List;
/**
* 仓库联动树节点(Feign侧)
* 镜像system端WarehouseLinkagePO/StorageSectionLinkagePO/StorageLocationLinkagePO三级结构:仓库→库区→库位
*
* @author rcx
* @date 2026-08-22
*/
@Data
public class WarehouseLinkageFeignPO implements Serializable {
private static final long serialVersionUID = 1L;
/** 仓库id / 库区id(storageSectionId) / 库位id(storageLocationId) */
private Long id;
/** warehouseCode / storageCode / storageLocationCode */
private String code;
/** warehouseName / storageName / storageLocationName */
private String name;
/** 下级节点:仓库下为库区列表,库区下为库位列表,库位无children */
private List<WarehouseLinkageFeignPO> children;
}
@@ -20,6 +20,7 @@ import org.springframework.cloud.openfeign.FallbackFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import java.util.List;
@@ -184,6 +185,11 @@ public class RemoteSystemFeignFallbackFactory implements FallbackFactory<SystemS
return AjaxResult.error("根据仓库ID获取仓库信息失败");
}
@Override
public AjaxResult getWarehouseChildren(@RequestParam("warehouseId") Long warehouseId) {
return AjaxResult.error("根据仓库ID获取仓库及下库区及库位树失败");
}
@Override
public AjaxResult getStorageSectionInfoByStorageSectionId(@PathVariable("storageSectionId") Long storageSectionId) {
return AjaxResult.error("根据库区ID获取库区信息失败");
@@ -125,6 +125,12 @@ public class RemoteWmsFallbackFactory implements FallbackFactory<WmsServiceFeign
log.error("WMS批量签名调用失败");
return AjaxResult.error("WMS批量签名调用失败:" + cause.getMessage());
}
@Override
public AjaxResult listLineManagement(String routeCode) {
log.error("OMS查询WMS线路管理失败, routeCode:{}", routeCode);
return AjaxResult.error("WMS线路管理查询失败:" + cause.getMessage());
}
};
}
}
@@ -0,0 +1,44 @@
package com.mhd.system.api.factory;
import com.mhd.common.core.domain.dto.BusinessDataPushDTO;
import com.mhd.common.core.domain.entity.R;
import com.mhd.common.core.domain.po.OrganizationPo;
import com.mhd.common.core.web.domain.AjaxResult;
import com.mhd.system.api.BmsServiceFeign;
import com.mhd.system.api.ProductServiceFeign;
import com.mhd.system.api.XxlJobServiceFeign;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.openfeign.FallbackFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.RequestBody;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* @Description
* @Author zg
* @Version 1.0
* @Date 2023/11/3 14:37
*/
@Component
public class RemoteXxlJobFeignFallbackFactory implements FallbackFactory<XxlJobServiceFeign> {
private static final Logger log = LoggerFactory.getLogger(RemoteXxlJobFeignFallbackFactory.class);
@Override
public XxlJobServiceFeign create(Throwable throwable) {
log.error("系统服务调用失败:{}", throwable.getMessage());
return new XxlJobServiceFeign() {
@Override
public R<Map<String, Object>> openFeignTest(String string) {
return null;
}
};
}
}
@@ -179,34 +179,67 @@ public class TokenLoginApplicationService {
}
/**
* 注册
* App注册(手机号 + 注册短信验证码 + 密码)
*/
public void register(String username, String password) {
// 用户名或密码为空 错误
if (StringUtils.isAnyBlank(username, password)) {
throw new ServiceException("用户/密码必须填写");
public void register(LoginDTO loginDTO) {
String userPhone = loginDTO.getUserPhone(); // LoginDTO.java:27-28
String verificationCode = loginDTO.getVerificationCode(); // LoginDTO.java:30-31
String password = loginDTO.getPassword();
// 1. 基础校验
if (StringUtils.isAnyBlank(userPhone, verificationCode, password)) {
throw new ServiceException("手机号、验证码、密码必须填写");
}
if (username.length() < UserConstants.USERNAME_MIN_LENGTH
|| username.length() > UserConstants.USERNAME_MAX_LENGTH) {
throw new ServiceException("账户长度必须在2到20个字符之间");
}
if (password.length() < UserConstants.PASSWORD_MIN_LENGTH
|| password.length() > UserConstants.PASSWORD_MAX_LENGTH) {
throw new ServiceException("密码长度必须在5到20个字符之间");
if (!RegexUtil.isPhoneLegal(userPhone)) {
throw new DigitalLogisticsException(UserError.USER_PHONE_REG_ERROR);
}
// 注册用户信息
SysUser sysUser = new SysUser();
sysUser.setUserName(username);
sysUser.setNickName(username);
sysUser.setPassword(SecurityUtils.encryptPassword(password));
//TODO app注册没写
/*R<?> registerResult = userServiceFeign.registerUserInfo(sysUser, SecurityConstants.INNER);
// 2. 校验注册短信验证码(发送时写入 redis,见 SmsApplicationService.java:112, :165
String key = CacheConstants.SMS_REGISTER + userPhone; // "sms_register:" CacheConstants.java:51
Object cache = redisService.getCacheObject(key);
if (cache == null || !ObjectUtil.equal(String.valueOf(cache), verificationCode)) {
recordLogininfor(userPhone, Constants.LOGIN_FAIL, "注册验证码错误", 1);
throw new ServiceException("验证码错误或已过期");
}
// 3. 域名 → 一级组织(与 loginByVerificationCode :103-110 一致)
String path = loginDTO.getPath();
AjaxResult topOrganizationIdR = organizationServiceFeign.getOrganizationByPath(path);
if (ObjectUtil.isNull(topOrganizationIdR) || ObjectUtil.isNull(topOrganizationIdR.get("data"))) {
throw new ServiceException("组织不存在,请检查登录域名");
}
SysTenantsPo sysTenantsPo = JSON.parseObject(JSON.toJSONString(topOrganizationIdR.get("data")), SysTenantsPo.class);
Long topOrganizationId = sysTenantsPo.getOrganizationId();
Long selectedOrgId = loginDTO.getOrganizationId(); // LoginDTO.java:37-38App 选择组织时传
Long organizationId = (selectedOrgId != null) ? selectedOrgId : topOrganizationId;
// 4. 手机号已注册则直接提示(UserMapper.xml:199 按 user_phone+top_organization_id 查)
UserDTO query = new UserDTO();
query.setUserPhone(userPhone);
query.setTopOrganizationId(topOrganizationId);
R<LoginUser> exist = userServiceFeign.getUserByTopOrganizationUserPhone(query, SecurityConstants.INNER);
if (exist != null && exist.getData() != null && exist.getData().getUserPo() != null) {
throw new ServiceException("该手机号已注册,请直接登录");
}
// 5. 创建用户(复用验证码登录自动注册的同一链路:
// auth feign UserServiceFeign.java:47-48 → user-center UserAPI.java:332-336
// → UserApplicationService.registerAddUser:1820-1927,内部已处理默认角色/YMS同步)
UserDTO userDTO = new UserDTO();
userDTO.setUserPhone(userPhone);
userDTO.setUserPassword(SecurityUtils.encryptPassword(password));
userDTO.setTopOrganizationId(topOrganizationId);
userDTO.setOrganizationId(organizationId);
userDTO.setOrganizationName(StringUtils.isNotBlank(loginDTO.getOrganizationName())
? loginDTO.getOrganizationName() : sysTenantsPo.getOrganizationName());
R<LoginUser> registerResult = userServiceFeign.registerAddUser(userDTO, SecurityConstants.INNER);
if (R.FAIL == registerResult.getCode()) {
throw new ServiceException(registerResult.getMsg());
}*/
// recordLogininfor(username, Constants.REGISTER, "注册成功");
}
// 6. 注册成功后清掉验证码并记日志
redisService.deleteObject(key);
recordLogininfor(userPhone, Constants.REGISTER, "注册成功", 2);
}
/**
@@ -72,9 +72,9 @@ public class AppTokenController extends BaseController {
@PostMapping("register")
public R<?> register(@RequestBody LoginDTO loginDTO) {
// 用户注册
tokenLoginApplicationService.register(loginDTO.getUsername(), loginDTO.getPassword());
return R.ok();
// 用户注册(手机号+短信验证码+密码)
tokenLoginApplicationService.register(loginDTO);
return R.ok("注册成功");
}
/**
@@ -28,6 +28,7 @@ import com.mhd.common.redis.service.RedisService;
import com.mhd.common.security.utils.password.PasswordUtil;
import com.mhd.system.api.RemoteLogService;
import com.mhd.system.api.domain.SysLogininforPo;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@@ -39,6 +40,7 @@ import javax.annotation.Resource;
* @description: 登录密码方法
* @date 2023/12/26 15:33
**/
@Slf4j
@Component
public class SysPasswordService
{
@@ -76,29 +78,31 @@ public class SysPasswordService
if (retryCount == null) {
retryCount = 0;
}
//TODO 从三方配置表中取值
SysDepartInterfaceInfoDTO sysDepartInterfaceInfoDTO = new SysDepartInterfaceInfoDTO();
sysDepartInterfaceInfoDTO.setTopOrganizationId(userPo.getTopOrganizationId());
sysDepartInterfaceInfoDTO.setInterfaceType("login");
sysDepartInterfaceInfoDTO.setInterfaceTypeKey("maxRetryCount");
List<SysDepartInterfaceInfoPo> sysDepartInterfaceInfoPos = thirdPartyServiceFeign.queryListByFeign(sysDepartInterfaceInfoDTO);
if (sysDepartInterfaceInfoPos.isEmpty()){
String errMsg = "缺少登录信息配置,请联系管理员";
recordLogininfor(userPo, Constants.LOGIN_FAIL, errMsg, 1);
throw new ServiceException(errMsg);
//TODO 从三方配置表中取值(失败时用默认值,保证登录错误提示可用)
int maxRetryCount = 5; // 默认:密码错5次锁定
long lockTime = 30L; // 默认:锁定30分钟
try {
SysDepartInterfaceInfoDTO dto = new SysDepartInterfaceInfoDTO();
dto.setTopOrganizationId(userPo.getTopOrganizationId());
dto.setInterfaceType("login");
dto.setInterfaceTypeKey("maxRetryCount");
List<SysDepartInterfaceInfoPo> pos = thirdPartyServiceFeign.queryListByFeign(dto);
if (pos != null && !pos.isEmpty()) {
maxRetryCount = Integer.parseInt(pos.get(0).getInterfaceTypeValue());
} else {
log.warn("组织[{}]缺少登录配置 login/maxRetryCount,使用默认值{}", userPo.getTopOrganizationId(), maxRetryCount);
}
dto.setInterfaceTypeKey("lockTime");
pos = thirdPartyServiceFeign.queryListByFeign(dto);
if (pos != null && !pos.isEmpty()) {
lockTime = Long.parseLong(pos.get(0).getInterfaceTypeValue());
} else {
log.warn("组织[{}]缺少登录配置 login/lockTime,使用默认值{}", userPo.getTopOrganizationId(), lockTime);
}
} catch (Exception e) {
log.warn("读取登录配置失败,使用默认重试策略", e);
}
SysDepartInterfaceInfoPo sysDepartInterfaceInfoPo = sysDepartInterfaceInfoPos.get(0);
Integer maxRetryCount = Integer.valueOf(sysDepartInterfaceInfoPo.getInterfaceTypeValue());
sysDepartInterfaceInfoDTO.setInterfaceTypeKey("lockTime");
sysDepartInterfaceInfoPos = thirdPartyServiceFeign.queryListByFeign(sysDepartInterfaceInfoDTO);
if (sysDepartInterfaceInfoPos.isEmpty()){
String errMsg = "缺少登录信息配置,请联系管理员";
recordLogininfor(userPo, Constants.LOGIN_FAIL, errMsg, 1);
throw new ServiceException(errMsg);
}
sysDepartInterfaceInfoPo = sysDepartInterfaceInfoPos.get(0);
Long lockTime = Long.valueOf(sysDepartInterfaceInfoPo.getInterfaceTypeValue());
if (retryCount >= maxRetryCount){
if (retryCount >= maxRetryCount) {
String errMsg = String.format("密码输入错误%s次,帐户锁定%s分钟", maxRetryCount, lockTime);
recordLogininfor(userPo, Constants.LOGIN_FAIL, errMsg, 1);
throw new ServiceException(errMsg);
+4 -4
View File
@@ -12,10 +12,10 @@ spring:
cloud:
nacos:
discovery:
# server-addr: 127.0.0.1:8848
server-addr: 127.0.0.1:8848
# 线上测试环境配置 容器名+端口号
# server-addr: 10.33.0.129:6010
server-addr: 10.102.192.31:6848
# server-addr: 10.102.192.31:6848
username: nacos
password: manhuoda@2023
#线上正式环境
@@ -23,8 +23,8 @@ spring:
# username: nacos
# password: manhuoda@2023
config:
# server-addr: 127.0.0.1:8848
server-addr: 10.102.192.31:6848
server-addr: 127.0.0.1:8848
# server-addr: 10.102.192.31:6848
# 测试环境配置 容器名+端口号
# server-addr: 10.33.0.129:6010
# 线上测试环境配置 容器名+端口号
@@ -4,6 +4,9 @@ import com.mhd.bi.domain.biPlatformRealData.repository.po.RealPlatformVehiclePO;
import com.mhd.bi.domain.biPlatformRealData.repository.po.RealPlatformOverviewPO;
import com.mhd.bi.domain.biPlatformRealData.repository.po.RealVehicleStatusPO;
import com.mhd.bi.domain.biPlatformRealData.repository.po.RealTaskLoadPO;
import com.mhd.bi.domain.biPlatformRealData.repository.po.RealBusinessTypeStatPO;
import com.mhd.bi.domain.biPlatformRealData.repository.po.RealPlatformFloorStatPO;
import com.mhd.bi.domain.biPlatformRealData.repository.po.RealPlatformInfoPO;
import java.util.List;
@@ -31,4 +34,13 @@ public interface BiPlatformRealDataMapper {
/** 任务负荷(今日 24 小时) */
List<RealTaskLoadPO> queryTaskLoadToday();
/** 今日预约-业务类型统计(按 businessName 分组统计今日预约数量) */
List<RealBusinessTypeStatPO> queryTodayBusinessTypeStats();
/** 月台监控-按楼层统计月台车辆(楼层、月台数量、今日预约车辆、当前作业车辆) */
List<RealPlatformFloorStatPO> queryPlatformFloorStats();
/** 月台管理列表(YMS qms_window_info,含机构/鹤位/状态/楼层/UUID 等完整信息) */
List<RealPlatformInfoPO> queryPlatformInfoList();
}
@@ -0,0 +1,20 @@
package com.mhd.bi.domain.biPlatformRealData.repository.po;
import lombok.Data;
import java.io.Serializable;
/**
* 今日预约-业务类型统计(真实数据,来自 TEST_NGWL_YMS.QMS_MAIN_BUSINESS
* 按业务类型分组统计今日预约数量
*/
@Data
public class RealBusinessTypeStatPO implements Serializable {
private static final long serialVersionUID = 1L;
/** 业务类型名称(qms_main_business.businessName */
private String businessName;
/** 今日预约数量 */
private Long count;
}
@@ -0,0 +1,26 @@
package com.mhd.bi.domain.biPlatformRealData.repository.po;
import lombok.Data;
import java.io.Serializable;
/**
* 月台监控-月台车辆统计(按楼层分组)
* 数据来源:TEST_NGWL_YMS.QMS_WINDOW_INFO(楼层+月台)与 QMS_MAIN_BUSINESS(当日预约/作业)
*/
@Data
public class RealPlatformFloorStatPO implements Serializable {
private static final long serialVersionUID = 1L;
/** 楼层(从叫号月台 WINDOW_NAME 解析,如 S1-01 -> F1 */
private String floorNo;
/** 该楼层的月台数量 */
private Long platformCount;
/** 今日预约车辆数(按 WINDOW_NAME 关联楼层) */
private Long todayReservedVehicles;
/** 当前作业车辆数(FLOW_STATUS IN 5,6,7,8,9,10 */
private Long operatingVehicles;
}
@@ -0,0 +1,59 @@
package com.mhd.bi.domain.biPlatformRealData.repository.po;
import lombok.Data;
import java.io.Serializable;
/**
* 月台管理列表(真实数据,来自 TEST_NGWL_YMS.QMS_WINDOW_INFO
* 对应 YMS qms_window_info 月台/鹤位窗口表
*/
@Data
public class RealPlatformInfoPO implements Serializable {
private static final long serialVersionUID = 1L;
/** 主键 */
private Long id;
/** 机构代码(org_no */
private String orgNo;
/** 机构名称(dept_name */
private String deptName;
/** 鹤位号/月台号(window_number */
private String windowNumber;
/** 鹤位名称/月台名称(window_name */
private String windowName;
/** 优先级别(priority_set */
private String prioritySet;
/** 鹤位状态/月台状态(running_status */
private String runningStatus;
/** uuid */
private String uuid;
/** 鹤位屏名字集合/绑定月台屏(berr_names */
private String berrNames;
/** 最新业务状态(new_flow_status */
private String newFlowStatus;
/** 月台图片(guide_images */
private String guideImages;
/** 月台楼层(platform_floor */
private String platformFloor;
/** 作业状态 0-空闲 1-占用(job_status */
private Integer jobStatus;
/** 创建人(create_by */
private String createBy;
/** 创建时间(create_time */
private String createTime;
}
@@ -1,22 +1,31 @@
package com.mhd.bi.domain.biPlatformRealData.service;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.mhd.bi.domain.biPlatformRealData.repository.mapper.BiPlatformRealDataMapper;
import com.mhd.bi.domain.biPlatformRealData.repository.po.RealPlatformVehiclePO;
import com.mhd.bi.domain.biPlatformRealData.repository.po.RealPlatformOverviewPO;
import com.mhd.bi.domain.biPlatformRealData.repository.po.RealVehicleStatusPO;
import com.mhd.bi.domain.biPlatformRealData.repository.po.RealTaskLoadPO;
import com.mhd.bi.domain.biPlatformRealData.repository.po.RealBusinessTypeStatPO;
import com.mhd.bi.domain.biPlatformRealData.repository.po.RealPlatformFloorStatPO;
import com.mhd.bi.domain.biPlatformRealData.repository.po.RealPlatformInfoPO;
import com.mhd.bi.domain.biPlatformSurveillance.repository.mapper.BiPlatformSurveillanceReportMapper;
import com.mhd.bi.domain.biSnapshotSync.repository.mapper.BiSnapshotWriteMapper;
import com.mhd.bi.domain.biSnapshotSync.support.BiSnapshotWeeklyRandomDataBuilder;
import com.mhd.bi.interfaces.facadeApi.biReport.vo.platform.PlatformSurveillanceVO;
import com.mhd.bi.interfaces.facadeApi.biReport.vo.platform.PlatformVehicleItemVO;
import com.mhd.bi.interfaces.facadeApi.biReport.vo.platform.TaskLoadItemVO;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Random;
/**
@@ -30,6 +39,96 @@ public class BiPlatformRealDataService {
@Resource
private BiPlatformRealDataMapper biPlatformRealDataMapper;
@Resource
private BiSnapshotWriteMapper biSnapshotWriteMapper;
@Resource
private BiPlatformSurveillanceReportMapper biPlatformSurveillanceReportMapper;
@Resource
private ObjectMapper objectMapper;
private static final String REMARK_REALTIME = "realtime platform-surveillance";
/**
* 实时组装月台监测数据;仅当真实业务数据(YMS 库查出的字段)发生变化时,
* 才覆盖更新 BI_PLATFORM_SURVEILLANCE_SNAPSHOT 快照表最新一条后返回。
* 写死字段变化不触发更新;表中尚无记录时插入一条兜底。
*/
public PlatformSurveillanceVO buildRealTimePlatformSurveillanceAndPersist() {
PlatformSurveillanceVO vo = buildRealTimePlatformSurveillance();
try {
String fullJson = objectMapper.writeValueAsString(vo);
String fingerprint = realDataFingerprint(vo);
String latestJson = biPlatformSurveillanceReportMapper.selectLatestPayloadJson();
if (StringUtils.isNotBlank(latestJson)) {
PlatformSurveillanceVO latestVo = objectMapper.readValue(latestJson.trim(), PlatformSurveillanceVO.class);
if (Objects.equals(fingerprint, realDataFingerprint(latestVo))) {
log.info("BI 月台监测真实业务数据未变化,跳过落库");
return vo;
}
}
int rows = biSnapshotWriteMapper.updateLatestPlatformSurveillanceSnapshot(fullJson, REMARK_REALTIME);
if (rows < 1) {
// 表中无最新记录可更新,插入一条兜底
biSnapshotWriteMapper.insertPlatformSurveillanceSnapshot(fullJson, REMARK_REALTIME);
}
log.info("BI 月台监测实时数据已落库(覆盖最新, 数据变化), affectedRows={}", rows);
} catch (Exception e) {
log.error("BI 月台监测实时数据落库失败", e);
}
return vo;
}
/**
* 提取月台监测的「真实业务数据」指纹(仅含 YMS 库查出的字段),用于判断业务数据是否变化。
* 写死/随机字段不纳入指纹,故其变化不会触发更新。
*/
private String realDataFingerprint(PlatformSurveillanceVO vo) throws Exception {
Map<String, Object> fp = new LinkedHashMap<>();
if (vo.getPlatformOverview() != null) {
fp.put("totalPlatforms", vo.getPlatformOverview().getTotalPlatforms());
fp.put("operatingPlatforms", vo.getPlatformOverview().getOperatingPlatforms());
fp.put("todayTasks", vo.getPlatformOverview().getTodayTasks());
}
if (vo.getVehicleStatus() != null) {
fp.put("todayReservedVehicles", vo.getVehicleStatus().getTodayReservedVehicles());
fp.put("notCheckedIn", vo.getVehicleStatus().getNotCheckedIn());
fp.put("operatingVehicles", vo.getVehicleStatus().getOperatingVehicles());
fp.put("completedVehicles", vo.getVehicleStatus().getCompletedVehicles());
}
if (vo.getMonthlyTaskStatistics() != null) {
fp.put("taskTotal", vo.getMonthlyTaskStatistics().getTaskTotal());
}
if (vo.getVehicles() != null) {
List<Map<String, Object>> vehicles = new ArrayList<>();
for (PlatformVehicleItemVO v : vo.getVehicles()) {
Map<String, Object> m = new LinkedHashMap<>();
m.put("appointmentTime", v.getAppointmentTime());
m.put("workFloor", v.getWorkFloor());
m.put("plateNumber", v.getPlateNumber());
m.put("status", v.getStatus());
m.put("platformId", v.getPlatformId());
m.put("platformDuration", v.getPlatformDuration());
vehicles.add(m);
}
fp.put("vehicles", vehicles);
}
if (vo.getTaskLoadStatistics() != null) {
List<Map<String, Object>> loads = new ArrayList<>();
for (TaskLoadItemVO item : vo.getTaskLoadStatistics()) {
Map<String, Object> m = new LinkedHashMap<>();
m.put("time", item.getTime());
m.put("taskCount", item.getTaskCount());
loads.add(m);
}
fp.put("taskLoadStatistics", loads);
}
return objectMapper.writeValueAsString(fp);
}
public List<RealPlatformVehiclePO> queryVehicles() {
try {
return biPlatformRealDataMapper.queryVehicles();
@@ -107,14 +206,56 @@ public class BiPlatformRealDataService {
}
/**
* 实时组装月台监测数据:真实字段查 YMS 库,写死字段保留静态值
* 今日预约-业务类型统计:按业务类型分组统计今日预约数量
* 例:今日预约航空打板 3 条 → 返回 {businessName:"航空打板", count:3}
*/
public List<RealBusinessTypeStatPO> queryTodayBusinessTypeStats() {
try {
return biPlatformRealDataMapper.queryTodayBusinessTypeStats();
} catch (Exception e) {
log.error("【YMS】统计今日业务类型失败: {}", e.getMessage(), e);
return java.util.Collections.emptyList();
}
}
/**
* 月台监控-按楼层统计月台车辆:返回 {floorNo, platformCount, todayReservedVehicles, operatingVehicles}
* 实时查 YMS 库,不落库。
*/
public List<RealPlatformFloorStatPO> queryPlatformFloorStats() {
try {
return biPlatformRealDataMapper.queryPlatformFloorStats();
} catch (Exception e) {
log.error("【YMS】月台监控按楼层统计失败: {}", e.getMessage(), e);
return java.util.Collections.emptyList();
}
}
/**
* 月台管理列表:实时查 YMS qms_window_info,返回月台/鹤位窗口完整信息,不落库。
*/
public List<RealPlatformInfoPO> queryPlatformInfoList() {
try {
return biPlatformRealDataMapper.queryPlatformInfoList();
} catch (Exception e) {
log.error("【YMS】查询月台管理列表失败: {}", e.getMessage(), e);
return java.util.Collections.emptyList();
}
}
/**
* 实时组装月台监测数据:原则「有真实数据源用真实,无真实数据源用写死模拟」。
* 有真实数据源的字段(vehicles / platformOverview / vehicleStatus / taskLoad / taskTotal
* 无条件用 YMS 真实查询覆盖,真实为空即为空,不用模板兜底;
* 无真实数据源的字段(效率统计、趋势、告警、任务类型等)保留模板写死值。
* 每次调用实时查库,不经过快照表。
*/
public PlatformSurveillanceVO buildRealTimePlatformSurveillance() {
Random r = new Random();
// 固定种子生成写死模板:仅用于没有真实数据源的字段
Random r = new Random(20260825L);
PlatformSurveillanceVO template = BiSnapshotWeeklyRandomDataBuilder.platformSurveillance(r);
// 作业车辆列表
// 作业车辆列表:真实数据源,无条件覆盖(真实为空则返回空列表,不用模板模拟)
List<RealPlatformVehiclePO> realVehicles = queryVehicles();
List<PlatformVehicleItemVO> vehicleVos = new ArrayList<>();
for (RealPlatformVehiclePO rv : realVehicles) {
@@ -127,52 +268,47 @@ public class BiPlatformRealDataService {
v.setPlatformDuration(rv.getPlatformDuration() == null ? 0 : rv.getPlatformDuration().intValue());
vehicleVos.add(v);
}
if (!vehicleVos.isEmpty()) {
template.setVehicles(vehicleVos);
}
template.setVehicles(vehicleVos);
// 月台实时概览
RealPlatformOverviewPO ov = queryPlatformOverview();
// 月台实时概览:真实数据源,无条件覆盖
if (template.getPlatformOverview() != null) {
template.getPlatformOverview().setTotalPlatforms(ov.getTotalPlatforms().intValue());
template.getPlatformOverview().setOperatingPlatforms(ov.getOperatingPlatforms().intValue());
RealPlatformOverviewPO ov = queryPlatformOverview();
int total = ov.getTotalPlatforms().intValue();
int occ = ov.getOperatingPlatforms().intValue();
double rate = total > 0 ? Math.round(((double) occ / total) * 100.0 * 100.0) / 100.0 : 0d;
template.getPlatformOverview().setTotalPlatforms(total);
template.getPlatformOverview().setOperatingPlatforms(occ);
template.getPlatformOverview().setCurrentOccupancyRate(rate);
}
long todayTasks = countTodayTasks();
if (template.getPlatformOverview() != null) {
template.getPlatformOverview().setTodayTasks((int) todayTasks);
template.getPlatformOverview().setTodayTasks((int) countTodayTasks());
}
// 作业车辆状态统计
RealVehicleStatusPO vs = queryVehicleStatusToday();
// 作业车辆状态统计:真实数据源,无条件覆盖
if (template.getVehicleStatus() != null) {
RealVehicleStatusPO vs = queryVehicleStatusToday();
template.getVehicleStatus().setTodayReservedVehicles(vs.getTodayReservedVehicles().intValue());
template.getVehicleStatus().setNotCheckedIn(vs.getNotCheckedIn().intValue());
template.getVehicleStatus().setOperatingVehicles(vs.getOperatingVehicles().intValue());
template.getVehicleStatus().setCompletedVehicles(vs.getCompletedVehicles().intValue());
}
// 本月任务统计 taskTotal
long monthTasks = countMonthTasks();
// 本月任务统计taskTotal 有真实数据源无条件覆盖;YoY 等无真实数据源保留模板写死值
if (template.getMonthlyTaskStatistics() != null) {
template.getMonthlyTaskStatistics().setTaskTotal((int) monthTasks);
template.getMonthlyTaskStatistics().setTaskTotal((int) countMonthTasks());
}
// 任务负荷(今日 24 小时)
// 任务负荷:有真实数据源;真实为空则清空不用模板模拟,有数据则按真实小时覆盖
List<RealTaskLoadPO> loads = queryTaskLoadToday();
if (!loads.isEmpty() && template.getTaskLoadStatistics() != null) {
if (loads.isEmpty()) {
template.setTaskLoadStatistics(new ArrayList<>());
} else if (template.getTaskLoadStatistics() != null) {
Map<Integer, Long> hourMap = new HashMap<>();
for (RealTaskLoadPO l : loads) {
hourMap.put(l.getHourOfDay(), l.getTaskCount());
}
for (TaskLoadItemVO item : template.getTaskLoadStatistics()) {
Integer hour = parseHourFromLabel(item.getTime());
if (hour != null) {
Long n = hourMap.get(hour);
if (n != null) item.setTaskCount(n.intValue());
if (hour != null && hourMap.containsKey(hour)) {
item.setTaskCount(hourMap.get(hour).intValue());
}
}
}
@@ -13,6 +13,8 @@ public interface BiSnapshotWriteMapper {
int insertPlatformSurveillanceSnapshot(@Param("payloadJson") String payloadJson, @Param("remark") String remark);
int updateLatestPlatformSurveillanceSnapshot(@Param("payloadJson") String payloadJson, @Param("remark") String remark);
int insertOverallOperationSnapshot(@Param("payloadJson") String payloadJson, @Param("remark") String remark);
int insertWarehouseOperationZoneSnapshot(@Param("payloadJson") String payloadJson, @Param("remark") String remark);
@@ -150,7 +150,8 @@ public class BiReportSnapshotSyncService {
/** TODO 业务落地:部分字段改为真实数据,其余仍写死。 */
protected PlatformSurveillanceVO buildPlatformSurveillanceSnapshot() {
Random r = new Random();
// 固定种子,保证写死字段每次生成值一致(真实字段随后实时覆盖)
Random r = new Random(20260825L);
// 写死数据复用原 builder
PlatformSurveillanceVO template = BiSnapshotWeeklyRandomDataBuilder.platformSurveillance(r);
@@ -4,6 +4,9 @@ import com.mhd.bi.domain.biSnapshotSync.service.BiReportSnapshotSyncService;
import com.mhd.bi.domain.biComprehensive.service.BiComprehensiveReportService;
import com.mhd.bi.domain.biPlatformSurveillance.service.BiPlatformSurveillanceReportService;
import com.mhd.bi.domain.biPlatformRealData.service.BiPlatformRealDataService;
import com.mhd.bi.domain.biPlatformRealData.repository.po.RealBusinessTypeStatPO;
import com.mhd.bi.domain.biPlatformRealData.repository.po.RealPlatformFloorStatPO;
import com.mhd.bi.domain.biPlatformRealData.repository.po.RealPlatformInfoPO;
import com.mhd.bi.domain.biOverallOperation.service.BiOverallOperationReportService;
import com.mhd.bi.domain.biHkMacaoServiceZone.service.BiHkMacaoServiceZoneReportService;
import com.mhd.bi.domain.biTransportOperationZone.service.BiTransportOperationZoneReportService;
@@ -30,6 +33,8 @@ import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
/**
* BI 报表接口
*/
@@ -96,12 +101,42 @@ public class BiReportApi extends BaseController {
}
/**
* 月台监测指标查询(实时查 YMS 库,真实字段 + 写死字段)
* 月台监测指标查询(实时查 YMS 库,真实字段 + 写死字段),并落库一条到 BI_PLATFORM_SURVEILLANCE_SNAPSHOT
*/
@ApiOperation(value = "月台监测指标查询", notes = "每次调用实时查询 YMS 库组装;部分字段为真实业务数据,部分为写死数据")
@ApiOperation(value = "月台监测指标查询", notes = "每次调用实时查询 YMS 库组装并写入快照表;部分字段为真实业务数据,部分为写死数据")
@GetMapping("/platformSurveillance")
public BiApiResult<PlatformSurveillanceVO> platformSurveillance() {
PlatformSurveillanceVO data = biPlatformRealDataService.buildRealTimePlatformSurveillance();
PlatformSurveillanceVO data = biPlatformRealDataService.buildRealTimePlatformSurveillanceAndPersist();
return BiApiResult.ok(data);
}
/**
* 今日预约-业务类型统计(实时查 YMS 库,不落库)
*/
@ApiOperation(value = "今日预约业务类型统计", notes = "按业务类型分组统计今日预约数量,实时查询 YMS 库,不落库;例:航空打板 3 条")
@GetMapping("/businessTypeStats")
public BiApiResult<List<RealBusinessTypeStatPO>> businessTypeStats() {
List<RealBusinessTypeStatPO> data = biPlatformRealDataService.queryTodayBusinessTypeStats();
return BiApiResult.ok(data);
}
/**
* 月台监控-按楼层统计月台车辆(实时查 YMS 库,不落库)
*/
@ApiOperation(value = "月台车辆统计(按楼层)", notes = "按楼层统计月台数量、今日预约车辆、当前作业车辆,实时查询 YMS 库,不落库")
@GetMapping("/platformFloorStats")
public BiApiResult<List<RealPlatformFloorStatPO>> platformFloorStats() {
List<RealPlatformFloorStatPO> data = biPlatformRealDataService.queryPlatformFloorStats();
return BiApiResult.ok(data);
}
/**
* 月台管理列表(实时查 YMS qms_window_info,不落库)
*/
@ApiOperation(value = "月台管理列表", notes = "实时查询 YMS 库 qms_window_info,返回月台/鹤位窗口列表(机构、月台号、月台名称、楼层、绑定屏、UUID、状态等),不落库")
@GetMapping("/platformInfoList")
public BiApiResult<List<RealPlatformInfoPO>> platformInfoList() {
List<RealPlatformInfoPO> data = biPlatformRealDataService.queryPlatformInfoList();
return BiApiResult.ok(data);
}
//园区物流大屏接口----------------------------------------------------------------------
+5 -5
View File
@@ -16,17 +16,17 @@ spring:
# 线上测试环境配置 容器名+端口号
# server-addr: 10.33.0.129:6010
# server-addr: 10.102.192.5:6848
username: nacos
password: manhuoda@2023
#线上正式环境
server-addr: 10.102.192.30:6848
# username: nacos
# password: manhuoda@2023
#线上正式环境
server-addr: 10.102.192.31:6848
username: nacos
password: manhuoda@2023
config:
# server-addr: 127.0.0.1:8848
# 线上测试环境配置 容器名+端口号
# server-addr: 10.33.0.129:6010
server-addr: 10.102.192.30:6848
server-addr: 10.102.192.31:6848
username: nacos
password: manhuoda@2023
#线上正式环境
@@ -4,7 +4,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.mhd.bi.domain.biPlatformRealData.repository.mapper.BiPlatformRealDataMapper">
<!-- 作业车辆状态列表(vehicles):
<!-- 作业车辆状态列表(vehicles):仅查询预约日期是今天(TARGET_DATE)的车辆
plateNumber <- qms_main_business.CAR_NO
status <- qms_main_business.FLOW_STATUS
platformId <- qms_main_business.WINDOW_NAME(叫号月台,主业务表直接有)
@@ -27,6 +27,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
FROM NGWL_TEST_YMS.QMS_MAIN_BUSINESS mb
LEFT JOIN NGWL_TEST_YMS.QMS_WINDOW_INFO wi
ON wi.WINDOW_NAME = mb.WINDOW_NAME
WHERE mb.TARGET_DATE IS NOT NULL
AND TRUNC(mb.TARGET_DATE) = TRUNC(SYSDATE)
ORDER BY mb.CREATE_TIME DESC
</select>
@@ -46,11 +48,12 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
AND TRUNC(ENTRY_TIME) = TRUNC(SYSDATE)
</select>
<!-- 车辆状态统计:今日的统计(按创建时间 / 流程状态)
<!-- 车辆状态统计:今日的统计(按创建时间 CREATE_TIME / 流程状态)
todayReservedVehicles 改为按预约日期 TARGET_DATE(其它 3 项保持 CREATE_TIME
FLOW_STATUS: 1已预约 2已签到 3已叫号 4已过号 5已进场 6已称重 7已装车 8二次称重 9已结算 10已还卡 11已出场 12已取消 -->
<select id="queryVehicleStatusToday" resultType="com.mhd.bi.domain.biPlatformRealData.repository.po.RealVehicleStatusPO">
SELECT
SUM(CASE WHEN TRUNC(CREATE_TIME) = TRUNC(SYSDATE) THEN 1 ELSE 0 END) AS todayReservedVehicles,
SUM(CASE WHEN TRUNC(TARGET_DATE) = TRUNC(SYSDATE) THEN 1 ELSE 0 END) AS todayReservedVehicles,
SUM(CASE WHEN FLOW_STATUS = '1' AND TRUNC(CREATE_TIME) = TRUNC(SYSDATE) THEN 1 ELSE 0 END) AS notCheckedIn,
SUM(CASE WHEN FLOW_STATUS IN ('5','6','7','8','9','10') AND TRUNC(CREATE_TIME) = TRUNC(SYSDATE) THEN 1 ELSE 0 END) AS operatingVehicles,
SUM(CASE WHEN FLOW_STATUS = '11' AND TRUNC(CREATE_TIME) = TRUNC(SYSDATE) THEN 1 ELSE 0 END) AS completedVehicles
@@ -65,6 +68,40 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
AND TO_CHAR(ENTRY_TIME, 'YYYY-MM') = TO_CHAR(SYSDATE, 'YYYY-MM')
</select>
<!-- 今日预约-业务名称统计:按 BUSINESS_NAME(业务名称)分组统计今日预约数量,按预约日期 TARGET_DATE 过滤 -->
<select id="queryTodayBusinessTypeStats" resultType="com.mhd.bi.domain.biPlatformRealData.repository.po.RealBusinessTypeStatPO">
SELECT
BUSINESS_NAME AS businessName,
COUNT(1) AS count
FROM NGWL_TEST_YMS.QMS_MAIN_BUSINESS
WHERE BUSINESS_NAME IS NOT NULL
AND TRUNC(TARGET_DATE) = TRUNC(SYSDATE)
GROUP BY BUSINESS_NAME
ORDER BY COUNT(1) DESC
</select>
<!-- 月台监控-按楼层统计:楼层从叫号月台 WINDOW_NAME 解析(如 S1-01 / N1-02 -> F1),不依赖字母前缀,只取 - 前面那段的数字
platformCount 来自 QMS_WINDOW_INFOtodayReservedVehicles/operatingVehicles 来自 QMS_MAIN_BUSINESS(按 TARGET_DATE=今天) -->
<select id="queryPlatformFloorStats" resultType="com.mhd.bi.domain.biPlatformRealData.repository.po.RealPlatformFloorStatPO">
SELECT
'F' || SUBSTR(W.WINDOW_NAME, INSTR(W.WINDOW_NAME,'-')-1, 1) AS floorNo,
COUNT(DISTINCT W.WINDOW_NAME) AS platformCount,
COUNT(DISTINCT CASE
WHEN TRUNC(mb.TARGET_DATE) = TRUNC(SYSDATE) THEN mb.CAR_NO
END) AS todayReservedVehicles,
COUNT(DISTINCT CASE
WHEN TRUNC(mb.TARGET_DATE) = TRUNC(SYSDATE)
AND mb.FLOW_STATUS IN ('5','6','7','8','9','10')
THEN mb.CAR_NO
END) AS operatingVehicles
FROM NGWL_TEST_YMS.QMS_WINDOW_INFO W
LEFT JOIN NGWL_TEST_YMS.QMS_MAIN_BUSINESS mb
ON mb.WINDOW_NAME = W.WINDOW_NAME
WHERE W.WINDOW_NAME LIKE '%-%'
GROUP BY SUBSTR(W.WINDOW_NAME, INSTR(W.WINDOW_NAME,'-')-1, 1)
ORDER BY 1 DESC
</select>
<!-- 任务负荷(今日 24 小时时段):按 EXTRACT(HOUR FROM ENTRY_TIME) 统计 -->
<select id="queryTaskLoadToday" resultType="com.mhd.bi.domain.biPlatformRealData.repository.po.RealTaskLoadPO">
SELECT
@@ -76,4 +113,34 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
GROUP BY EXTRACT(HOUR FROM ENTRY_TIME)
ORDER BY EXTRACT(HOUR FROM ENTRY_TIME)
</select>
<!-- 月台管理列表:查询 YMS qms_window_info 月台/鹤位窗口
机构名称 deptName <- sys_dept.DEPT_NAMEd.dept_id = a.ORG_NO 关联)
鹤位屏名称 berrNames <- qms_window_berr(WINDOW_ID/BERR_ID) join qms_berr_config(ID/BERR_NAME)WM_CONCAT 拼接
其余列来自 qms_window_info 本身 -->
<select id="queryPlatformInfoList" resultType="com.mhd.bi.domain.biPlatformRealData.repository.po.RealPlatformInfoPO">
SELECT
a.ID AS id,
a.ORG_NO AS orgNo,
d.DEPT_NAME AS deptName,
a.WINDOW_NUMBER AS windowNumber,
a.WINDOW_NAME AS windowName,
a.PRIORITY_SET AS prioritySet,
a.RUNNING_STATUS AS runningStatus,
a.CREATE_BY AS createBy,
TO_CHAR(a.CREATE_TIME, 'YYYY-MM-DD HH24:MI:SS') AS createTime,
a.UUID AS uuid,
a.GUIDE_IMAGES AS guideImages,
a.PLATFORM_FLOOR AS platformFloor,
a.JOB_STATUS AS jobStatus,
(
SELECT WM_CONCAT(qbc.BERR_NAME)
FROM NGWL_TEST_YMS.QMS_WINDOW_BERR qwb
LEFT JOIN NGWL_TEST_YMS.QMS_BERR_CONFIG qbc ON qbc.ID = qwb.BERR_ID
WHERE qwb.WINDOW_ID = a.ID
) AS berrNames
FROM NGWL_TEST_YMS.QMS_WINDOW_INFO a
LEFT JOIN NGWL_TEST_YMS.SYS_DEPT d ON d.DEPT_ID = a.ORG_NO
ORDER BY a.PLATFORM_FLOOR, a.WINDOW_NUMBER
</select>
</mapper>
@@ -18,6 +18,15 @@
values (#{payloadJson}, SYSDATE, SYSDATE, 1, #{remark})
</insert>
<update id="updateLatestPlatformSurveillanceSnapshot">
update NGWL_TEST_BI.BI_PLATFORM_SURVEILLANCE_SNAPSHOT
set PAYLOAD_JSON = #{payloadJson},
UPDATE_TIME = SYSDATE,
REMARK = #{remark}
where DEL_FLAG = 1
and ID = (select max(t.ID) from NGWL_TEST_BI.BI_PLATFORM_SURVEILLANCE_SNAPSHOT t where t.DEL_FLAG = 1)
</update>
<insert id="insertOverallOperationSnapshot">
insert into NGWL_TEST_BI.BI_OVERALL_OPERATION_SNAPSHOT (PAYLOAD_JSON, CREATE_TIME, UPDATE_TIME, DEL_FLAG, REMARK)
values (#{payloadJson}, SYSDATE, SYSDATE, 1, #{remark})
@@ -66,6 +66,9 @@ public class ServiceNameConstants
//YMS系统
public static final String YMS_SERVICE = "mhd-yms-service";
//YMS系统
public static final String XXL_JOB_SERVICE = "xxl-job-service";
}
@@ -16,7 +16,8 @@ public enum RoleEnum {
STORE_ADMIN("店铺管理员", "store_admin"),
OWNER_CARGO("货主", "ownerCargo"),
SUPPLIER("供应商", "supplier"),
CUSTOMER("客户", "customer");
CUSTOMER("客户", "customer"),
TENANT("租户", "tenant");
private String name;
private String code;
@@ -26,7 +26,7 @@ import java.io.InputStream;
import java.util.List;
import java.util.Map;
@FeignClient(value = "mhd-third-party-service", configuration = FeignAutoConfiguration.class)
@FeignClient(value = "mhd-third-party-service",url = "http://10.102.192.30:8012",configuration = FeignAutoConfiguration.class)
public interface ThirdPartyServiceFeign {
//查询当前组织所有三方接口信息
@@ -88,7 +88,8 @@ public class GlobalExceptionHandler
{
String requestURI = request.getRequestURI();
log.error("请求地址'{}',发生未知异常.", requestURI, e);
return AjaxResult.error(e.getMessage());
String msg = StringUtils.isEmpty(e.getMessage()) ? "系统繁忙,请稍后重试" : e.getMessage();
return AjaxResult.error(msg);
}
/**
@@ -99,7 +100,8 @@ public class GlobalExceptionHandler
{
String requestURI = request.getRequestURI();
log.error("请求地址'{}',发生系统异常.", requestURI, e);
return AjaxResult.error(e.getMessage());
String msg = StringUtils.isEmpty(e.getMessage()) ? "系统繁忙,请稍后重试" : e.getMessage();
return AjaxResult.error(msg);
}
/**
@@ -34,6 +34,8 @@ public class CaptchaConfig
properties.setProperty(KAPTCHA_SESSION_CONFIG_KEY, "kaptchaCode");
// 验证码文本字符长度 默认为5
properties.setProperty(KAPTCHA_TEXTPRODUCER_CHAR_LENGTH, "4");
// 验证码文本字符源 默认为abcde2345678gfynmnpqr 改为纯数字
properties.setProperty(KAPTCHA_TEXTPRODUCER_CHAR_STRING, "0123456789");
// 验证码文本字体样式 默认为new Font("Arial", 1, fontSize), new Font("Courier", 1, fontSize)
properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_NAMES, "Arial,Courier");
// 图片样式 水纹com.google.code.kaptcha.impl.WaterRipple 鱼眼com.google.code.kaptcha.impl.FishEyeGimpy 阴影com.google.code.kaptcha.impl.ShadowGimpy
+6 -13
View File
@@ -14,23 +14,16 @@ spring:
nacos:
discovery:
server-addr: 127.0.0.1:8848
# 线上测试环境配置 容器名+端口号
# server-addr: 10.102.192.31:6848
# server-addr: 10.102.192.30:6848
# server-addr: 10.33.0.129:6010
#线上正式环境
# server-addr: 127.0.0.1:8848
# server-addr: 10.102.192.31:6848
# username: nacos
# password: manhuoda@2023
username: nacos
password: manhuoda@2023
config:
server-addr: 127.0.0.1:8848
# 线上测试环境配置 容器名+端口号
# server-addr: 10.33.0.129:6010
#线上正式环境
# server-addr: 127.0.0.1:8848
# server-addr: 10.102.192.31:6848
# username: nacos
# password: manhuoda@2023
# server-addr: 10.102.192.30:6848
username: nacos
password: manhuoda@2023
group: DEFAULT_GROUP # 默认分组就是DEFAULT_GROUP,如果使用默认分组可以不配置
file-extension: yml #默认properties
# 共享配置
@@ -278,7 +278,7 @@ public class LeaseApplicationService {
* 货主租赁明细
* 定时任务:每天凌晨1点执行,计算前一天的货主租赁明细
*/
@Scheduled(cron = "0 0 1 * * ?")
// @Scheduled(cron = "0 0 1 * * ?")
public void shipperLeaseDetails() {
log.info("开始执行货主租赁明细定时任务");
// 计算前一天的日期,将时间设置为当天的0点0分0秒,只保留日期部分
@@ -461,7 +461,8 @@ public class MessageManageApplicationService
//获取当前登录人
LoginUser loginUser = SecurityUtils.getLoginUser();
if(null == loginUser){
throw new ServiceException("获取登录人失败");
//未登录(游客):返回空消息中心结构,保证小程序界面正常展示而不报错
return buildEmptyAppNoticeList();
}
noticeMessageLoggingDo.setUserId(loginUser.getUserid());
//获取每种类型的最新一条
@@ -522,6 +523,27 @@ public class MessageManageApplicationService
return result;
}
/**
* 未登录(游客)时返回空的消息中心结构,避免前端页面因无登录上下文而报错
* @return NoticeMessageLoggingAppListPo
*/
private NoticeMessageLoggingAppListPo buildEmptyAppNoticeList() {
NoticeMessageLoggingAppListPo result = new NoticeMessageLoggingAppListPo();
NoticeMessageLoggingAppListPo.CommonNotice serviceNotice = new NoticeMessageLoggingAppListPo.CommonNotice();
serviceNotice.setUnreadNum("0");
NoticeMessageLoggingAppListPo.CommonNotice activityNotice = new NoticeMessageLoggingAppListPo.CommonNotice();
activityNotice.setUnreadNum("0");
NoticeMessageLoggingAppListPo.CommonNotice logisticsNotice = new NoticeMessageLoggingAppListPo.CommonNotice();
logisticsNotice.setUnreadNum("0");
NoticeMessageLoggingAppListPo.CommonNotice systemNotice = new NoticeMessageLoggingAppListPo.CommonNotice();
systemNotice.setUnreadNum("0");
result.setServiceNotice(serviceNotice);
result.setActivityNotice(activityNotice);
result.setLogisticsNotice(logisticsNotice);
result.setSystemNotice(systemNotice);
return result;
}
/**
* APP根据类型获取消息通知
* @param noticeMessageLoggingDo
@@ -13,6 +13,7 @@ import com.mhd.basic.application.service.contractManage.ContractManageApplicatio
import com.mhd.basic.domain.expenseAccount.service.ExpenseAccountDomainService;
import com.mhd.basic.infrastructure.feign.vo.SysDictDataVo;
import com.mhd.basic.interfaces.dto.expenseAccount.ExpenseAccountDTO;
import com.mhd.basic.interfaces.dto.expenseAccount.ExpenseAccountExportVO;
import com.mhd.basic.interfaces.dto.expenseAccount.SearchExpenseAccountDTO;
import com.mhd.common.core.enums.DictCode;
import com.mhd.common.core.exception.ServiceException;
@@ -27,6 +28,7 @@ import com.mhd.system.api.SystemServiceFeign;
import com.mhd.system.api.model.LoginUser;
import com.mhd.common.core.domain.po.UserPo;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@@ -378,6 +380,55 @@ public class ExpenseAccountApplicationService {
return expenseAccountDomainService.getInfo(expenseAccountId);
}
/**
* 复制费用科目-获取原科目数据供前端回显
* <p>前端拿到数据后回显到新增弹窗,用户填写新科目编码后通过 /edit 接口(id 为空走新增)提交保存。
* 清空科目编码是为了避免直接复制出重复编码(费用科目编码无唯一性校验)。</p>
*/
public ExpenseAccountPO copy(ExpenseAccountDTO expenseAccountDTO) {
if (expenseAccountDTO == null || expenseAccountDTO.getExpenseAccountId() == null) {
throw new ServiceException("原费用科目id不能为空");
}
// 获取原费用科目信息
ExpenseAccountPO original = expenseAccountDomainService.getInfo(expenseAccountDTO.getExpenseAccountId());
if (null == original) {
throw new ServiceException("原费用科目信息未找到");
}
// 清空主键,前端提交时走新增
original.setExpenseAccountId(null);
// 清空科目编码,强制用户输入新编码,避免编码重复
original.setSubjectCode(null);
// 清空审计字段,新增时由 ExpenseAccountAssembler 重新生成
original.setCreateBy(null);
original.setCreateByName(null);
original.setCreateTime(null);
original.setUpdateBy(null);
original.setUpdateByName(null);
original.setUpdateTime(null);
original.setDelFlag(null);
// 清空非表字段
original.setUnit(null);
original.setExpenseAccountPOList(null);
return original;
}
/**
* 构建费用科目导出数据(不分页全量,按列表页查询条件 + 数据权限过滤)
*/
public List<ExpenseAccountExportVO> buildExportList(ExpenseAccountDO expenseAccountDO) {
List<ExpenseAccountExportVO> exportList = new ArrayList<>();
List<ExpenseAccountPO> list = this.queryList(expenseAccountDO);
if (list == null || list.isEmpty()) {
return exportList;
}
for (ExpenseAccountPO po : list) {
ExpenseAccountExportVO vo = new ExpenseAccountExportVO();
BeanUtils.copyProperties(po, vo);
exportList.add(vo);
}
return exportList;
}
/**
* 修改状态
* @param expenseAccountDTO
@@ -594,22 +645,28 @@ public class ExpenseAccountApplicationService {
// unit.add(expenseAccountPO.getBillingUnit2() + "-" + expenseAccountPO.getBillingUnitEn2());
// expenseAccountPO.setUnit(unit);
// }
// 组装计费单位列表(数组)
// 组装计费单位列表(数组),中文和英文都为null时过滤掉,避免出现"null-null"
List<Map<String, String>> unit = new ArrayList<>();
if (expenseAccountPO != null) {
Map<String, String> unit1 = new HashMap<>();
unit1.put("unit", expenseAccountPO.getBillingUnit() + "-" + expenseAccountPO.getBillingUnitEn());
unit.add(unit1);
Map<String, String> unit2 = new HashMap<>();
unit2.put("unit", expenseAccountPO.getBillingUnit2() + "-" + expenseAccountPO.getBillingUnitEn2());
unit.add(unit2);
addUnitIfPresent(unit, expenseAccountPO.getBillingUnit(), expenseAccountPO.getBillingUnitEn());
addUnitIfPresent(unit, expenseAccountPO.getBillingUnit2(), expenseAccountPO.getBillingUnitEn2());
expenseAccountPO.setUnit(unit);
}
return expenseAccountPO;
}
/**
* 组装计费单位,中文和英文都为null时不加入列表
*/
private void addUnitIfPresent(List<Map<String, String>> unit, String billingUnit, String billingUnitEn) {
if (billingUnit == null && billingUnitEn == null) {
return;
}
Map<String, String> unitMap = new HashMap<>();
unitMap.put("unit", billingUnit + "-" + billingUnitEn);
unit.add(unitMap);
}
public List<QueryExpenseAccountPO> selectExpenseAccount(SearchExpenseAccountDO searchExpenseAccountDO){
setDataPermissionForSearch(searchExpenseAccountDO);
return expenseAccountDomainService.selectExpenseAccount(searchExpenseAccountDO);
@@ -6,6 +6,7 @@ import com.alibaba.fastjson.JSON;
import com.mhd.basic.domain.storageLocation.repository.po.StorageLocationPO;
import com.mhd.basic.domain.storageLocation.repository.todo.StorageLocationDO;
import com.mhd.basic.domain.storageLocation.service.StorageLocationDomainService;
import com.mhd.basic.interfaces.dto.storageLocation.StorageLocationDTO;
import com.mhd.basic.domain.storageSection.repository.todo.StorageSectionDO;
import com.mhd.basic.infrastructure.feign.vo.SysDictDataVo;
import com.mhd.common.core.domain.po.UserPo;
@@ -207,6 +208,39 @@ public class StorageLocationApplicationService {
return storageLocationDomainService.getInfo(storageLocationId);
}
/**
* 复制库位-获取原库位数据供前端回显
* 前端拿到数据后跳转到新增库位页面,用户修改编码后通过edit接口提交新增
*/
public StorageLocationPO copyStorageLocation(StorageLocationDTO storageLocationDTO) {
if (storageLocationDTO.getStorageLocationId() == null) {
throw new ServiceException("原库位id不能为空");
}
// 获取原库位信息
StorageLocationPO originalStorageLocation = storageLocationDomainService.getInfo(storageLocationDTO.getStorageLocationId());
if (null == originalStorageLocation) {
throw new ServiceException("原库位信息未找到");
}
// 清空唯一性字段:库位id不清空会导致提交后走修改分支覆盖原库位;
// 库位编码同组织同仓库唯一,须由用户重新填写;条码为物理唯一标识,一并清空
originalStorageLocation.setStorageLocationId(null);
originalStorageLocation.setStorageLocationCode(null);
originalStorageLocation.setLocationBarCode(null);
// 状态重置为初始值:新库位无库存(空)、启用、未作废
originalStorageLocation.setStatus(1);
originalStorageLocation.setOpeningUp(1);
originalStorageLocation.setNullify(1);
// 清空审计字段,提交新增时由Assembler按当前登录人重新生成
originalStorageLocation.setCreateBy(null);
originalStorageLocation.setCreateByName(null);
originalStorageLocation.setCreateTime(null);
originalStorageLocation.setUpdateBy(null);
originalStorageLocation.setUpdateByName(null);
originalStorageLocation.setUpdateTime(null);
// 其余业务字段(仓库/库区/库位组、分类/种类/混放策略、限制、长宽高、坐标、备注)保留回显
return originalStorageLocation;
}
public int getStorageLocationCount(StorageLocationDO storageLocationDO)
{
return storageLocationDomainService.getStorageLocationCount(storageLocationDO);
@@ -21,6 +21,7 @@ import com.mhd.system.service.ISysDictTypeService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.util.List;
/**
* 库区ApplicationService
@@ -42,6 +43,27 @@ public class StorageSectionApplicationService {
*/
public List<StorageSectionPO> queryList(StorageSectionDO storageSectionDO) {
//设置查询数据权限
applyQueryDataPermission(storageSectionDO);
return storageSectionDomainService.queryList(storageSectionDO);
}
/**
* 库区面积合计:与 /list 相同的数据权限和筛选条件,SQL 直接 SUM(库区面积),
* 保证合计数与列表中各行库区面积之和一致
*/
public BigDecimal sumStorageRegion(StorageSectionDO storageSectionDO) {
//与 /list 共用同一数据权限,保证合计与列表口径一致
applyQueryDataPermission(storageSectionDO);
return storageSectionDomainService.sumStorageRegion(storageSectionDO);
}
/**
* @description 设置查询数据权限(queryList 与 sumStorageRegion 共用)
* @author ZhouGY
* @param storageSectionDO
*/
private void applyQueryDataPermission(StorageSectionDO storageSectionDO) {
LoginUser loginUser = SecurityUtils.getLoginUser();
if (loginUser != null && loginUser.getUserPo() != null) {
Long userOrganizationId = loginUser.getUserPo().getOrganizationId();
@@ -91,7 +113,6 @@ public class StorageSectionApplicationService {
}
}
}
return storageSectionDomainService.queryList(storageSectionDO);
}
@@ -126,6 +126,18 @@ public class ContractManage extends BaseVOEntity{
@Excel(name = "其他附件地址")
private String otherAttachmentsUrl;
@ApiModelProperty("合同正本附件原始文件名")
private String contractOriginalName;
@ApiModelProperty("廉政协议附件原始文件名")
private String integrityAgreementName;
@ApiModelProperty("安全协议附件原始文件名")
private String securityProtocolName;
@ApiModelProperty("其他附件原始文件名(多文件时与 otherAttachmentsUrl 同分隔符、同顺序)")
private String otherAttachmentsName;
@ApiModelProperty("我司签订人")
@Excel(name = "我司签订人")
private String ourSinged;
@@ -124,6 +124,18 @@ public class ContractManagePO extends BaseVOEntity{
@Excel(name = "其他附件地址")
private String otherAttachmentsUrl;
@ApiModelProperty("合同正本附件原始文件名")
private String contractOriginalName;
@ApiModelProperty("廉政协议附件原始文件名")
private String integrityAgreementName;
@ApiModelProperty("安全协议附件原始文件名")
private String securityProtocolName;
@ApiModelProperty("其他附件原始文件名(多文件时与 otherAttachmentsUrl 同分隔符、同顺序)")
private String otherAttachmentsName;
@ApiModelProperty("我司签订人")
@Excel(name = "我司签订人")
private String ourSinged;
@@ -121,6 +121,18 @@ public class ContractManageDO extends BaseVOEntity{
@Excel(name = "其他附件地址")
private String otherAttachmentsUrl;
@ApiModelProperty("合同正本附件原始文件名")
private String contractOriginalName;
@ApiModelProperty("廉政协议附件原始文件名")
private String integrityAgreementName;
@ApiModelProperty("安全协议附件原始文件名")
private String securityProtocolName;
@ApiModelProperty("其他附件原始文件名(多文件时与 otherAttachmentsUrl 同分隔符、同顺序)")
private String otherAttachmentsName;
@ApiModelProperty("我司签订人")
@Excel(name = "我司签订人")
private String ourSinged;
@@ -5,6 +5,7 @@ import com.mhd.basic.domain.storageSection.entity.StorageSection;
import com.mhd.basic.domain.storageSection.repository.po.StorageSectionPO;
import com.mhd.basic.domain.storageSection.repository.todo.StorageSectionDO;
import java.math.BigDecimal;
import java.util.List;
/**
@@ -20,6 +21,11 @@ public interface IStorageSectionService extends IService<StorageSection>
*/
public List<StorageSectionPO> queryList(StorageSectionDO storageSectionDO);
/**
* 库区面积合计 queryList 相同筛选条件
*/
public BigDecimal sumStorageRegion(StorageSectionDO storageSectionDO);
/**
* 新增库区
*/
@@ -1,5 +1,6 @@
package com.mhd.basic.domain.storageSection.repository.mapper;
import java.math.BigDecimal;
import java.util.List;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.mhd.basic.domain.storageSection.entity.StorageSection;
@@ -19,4 +20,9 @@ public interface StorageSectionMapper extends BaseMapper<StorageSection>
*/
public List<StorageSectionPO> queryList(StorageSectionDO storageSectionDO);
/**
* 库区面积合计 queryList 相同筛选条件
*/
public BigDecimal sumStorageRegion(StorageSectionDO storageSectionDO);
}
@@ -10,6 +10,7 @@ import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
@@ -33,6 +34,15 @@ public class StorageSectionImpl extends ServiceImpl<StorageSectionMapper, Storag
return storageSectionMapper.queryList(storageSectionDO);
}
/**
* 库区面积合计 queryList 相同筛选条件
*/
@Override
public BigDecimal sumStorageRegion(StorageSectionDO storageSectionDO)
{
return storageSectionMapper.sumStorageRegion(storageSectionDO);
}
/**
* 新增库区
*/
@@ -21,6 +21,7 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
@@ -53,6 +54,13 @@ public class StorageSectionDomainService {
return storageSectionService.queryList(storageSectionDO);
}
/**
* 库区面积合计 queryList 相同筛选条件
*/
public BigDecimal sumStorageRegion(StorageSectionDO storageSectionDO) {
return storageSectionService.sumStorageRegion(storageSectionDO);
}
/**
* 新增库区
@@ -121,6 +121,18 @@ public class ContractManageDTO extends BaseVOEntity{
@Excel(name = "其他附件地址")
private String otherAttachmentsUrl;
@ApiModelProperty("合同正本附件原始文件名")
private String contractOriginalName;
@ApiModelProperty("廉政协议附件原始文件名")
private String integrityAgreementName;
@ApiModelProperty("安全协议附件原始文件名")
private String securityProtocolName;
@ApiModelProperty("其他附件原始文件名(多文件时与 otherAttachmentsUrl 同分隔符、同顺序)")
private String otherAttachmentsName;
@ApiModelProperty("我司签订人")
@Excel(name = "我司签订人")
private String ourSinged;
@@ -0,0 +1,69 @@
package com.mhd.basic.interfaces.dto.expenseAccount;
import com.mhd.common.core.annotation.Excel;
import lombok.Data;
import java.math.BigDecimal;
import java.util.Date;
/**
* 费用科目导出对象 expense_account
*
* @author rcx
* @date 2026-08-28
*/
@Data
public class ExpenseAccountExportVO {
/** 费用类别(关联服务项名称) */
@Excel(name = "费用类别")
private String serviceItemsName;
@Excel(name = "上级科目")
private String upperSubject;
@Excel(name = "费用代码")
private String subjectCode;
@Excel(name = "费用名称")
private String subjectName;
@Excel(name = "税率")
private BigDecimal taxRate;
@Excel(name = "创建订单固定显示", readConverterExp = "0=否,1=是")
private Integer fixedOrderDisplay;
@Excel(name = "入库显示", readConverterExp = "0=否,1=是")
private Integer inOrderDisplay;
@Excel(name = "出库显示", readConverterExp = "0=否,1=是")
private Integer outOrderDisplay;
@Excel(name = "自有车辆固定显示", readConverterExp = "1=是,2=否")
private Integer ownStatus;
@Excel(name = "社会车辆固定显示", readConverterExp = "1=是,2=否")
private Integer societyStatus;
@Excel(name = "备注信息")
private String remark;
@Excel(name = "状态", readConverterExp = "1=启用,2=停用")
private Integer status;
@Excel(name = "创建人")
private String createByName;
@Excel(name = "创建时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
private Date createTime;
@Excel(name = "更新人")
private String updateByName;
@Excel(name = "更新时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
private Date updateTime;
@Excel(name = "所属组织")
private String organizationName;
}
@@ -1,5 +1,7 @@
package com.mhd.basic.interfaces.facade.expenseAccount;
import java.io.IOException;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.List;
@@ -13,9 +15,11 @@ import com.mhd.basic.domain.expenseAccount.repository.todo.SearchExpenseAccountD
import com.mhd.basic.domain.sysMultistageDict.repository.po.SysMultistageDictPo;
import com.mhd.basic.interfaces.assember.expenseAccount.ExpenseAccountAssembler;
import com.mhd.basic.interfaces.dto.expenseAccount.ExpenseAccountDTO;
import com.mhd.basic.interfaces.dto.expenseAccount.ExpenseAccountExportVO;
import com.mhd.basic.interfaces.dto.expenseAccount.SearchExpenseAccountDTO;
import com.mhd.common.core.constant.HttpStatus;
import com.mhd.common.core.domain.entity.R;
import com.mhd.common.core.utils.poi.ExcelUtil;
import com.sun.org.apache.bcel.internal.generic.NEW;
import io.swagger.annotations.ApiOperation;
import org.apache.commons.lang.StringUtils;
@@ -26,6 +30,7 @@ import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletResponse;
import com.mhd.common.core.web.controller.BaseController;
import com.mhd.common.core.web.domain.AjaxResult;
import com.mhd.common.core.web.page.TableDataInfo;
@@ -336,5 +341,45 @@ public class ExpenseAccountApi extends BaseController{
return AjaxResult.success(expenseAccountPO.getUnit());
}
/**
* 复制费用科目
* 返回原科目数据已清空主键科目编码等字段供前端回显到新增弹窗用户填写新科目编码后通过 /edit 接口提交新增
*/
@ApiOperation("复制费用科目-获取原科目数据用于回显")
@PostMapping("/copy")
public AjaxResult copy(@RequestBody ExpenseAccountDTO expenseAccountDTO)
{
try {
ExpenseAccountPO expenseAccountPO = expenseAccountApplicationService.copy(expenseAccountDTO);
return AjaxResult.success(expenseAccountPO);
}catch (Exception e){
return AjaxResult.error(e.getMessage());
}
}
/**
* 导出费用科目
* 按列表页当前查询条件 + 数据权限过滤不分页全量导出
*/
@ApiOperation("导出费用科目")
@RequestMapping(value = "/export", method = {RequestMethod.GET, RequestMethod.POST},
produces = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
public void export(ExpenseAccountDTO expenseAccountDTO, HttpServletResponse response) throws IOException
{
//转换实体不用Assembler避免其自动填充createByName/updateByName干扰查询条件/list一致
ExpenseAccountDO expenseAccountDO = new ExpenseAccountDO();
BeanUtils.copyProperties(expenseAccountDTO, expenseAccountDO);
//查询全量不分页并转换为导出对象
List<ExpenseAccountExportVO> list = expenseAccountApplicationService.buildExportList(expenseAccountDO);
//设置下载头防止浏览器乱码
String fileName = URLEncoder.encode("费用科目导出", "UTF-8").replaceAll("\\+", "%20");
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setCharacterEncoding("utf-8");
response.setHeader("Content-disposition", "attachment;filename=" + fileName + ".xlsx");
ExcelUtil<ExpenseAccountExportVO> util = new ExcelUtil<>(ExpenseAccountExportVO.class);
util.exportExcel(response, list, "费用科目");
}
}
@@ -19,6 +19,7 @@ import com.mhd.basic.domain.storageLocation.repository.todo.StorageLocationDO;
import com.mhd.basic.domain.storageLocation.repository.po.StorageLocationPO;
import com.mhd.basic.application.service.storageLocation.StorageLocationApplicationService;
import com.mhd.basic.interfaces.assember.storageLocation.StorageLocationAssembler;
import com.mhd.common.core.exception.ServiceException;
import javax.annotation.Resource;
import com.mhd.common.core.web.controller.BaseController;
import com.mhd.common.core.web.domain.AjaxResult;
@@ -127,6 +128,24 @@ public class StorageLocationApi extends BaseController{
{
return AjaxResult.success(storageLocationApplicationService.getInfo(storageLocationId));
}
/**
* 复制库位-获取原库位数据用于回显
*/
@ApiOperation("复制库位-获取原库位数据用于回显")
@PostMapping("/copyStorageLocation")
public AjaxResult copyStorageLocation(@RequestBody StorageLocationDTO storageLocationDTO)
{
if(storageLocationDTO.getStorageLocationId() == null){
throw new ServiceException("原库位id不能为空");
}
try {
StorageLocationPO result = storageLocationApplicationService.copyStorageLocation(storageLocationDTO);
return AjaxResult.success(result);
}catch (Exception e){
return AjaxResult.error("复制库位失败:"+e.getMessage());
}
}
@ApiOperation("获取库位总数")
@PostMapping(value = "/getStorageLocationCount")
public AjaxResult getStorageLocationCount(@RequestBody StorageLocationDTO storageLocationDTO)
@@ -55,6 +55,18 @@ public class StorageSectionApi extends BaseController{
return getDataTable(list);
}
/**
* 库区面积合计 /list 相同的筛选条件返回库区面积总和
*/
@ApiOperation("查询库区面积合计(与库区列表相同筛选条件,SUM(库区面积))")
@GetMapping("/sumStorageRegion")
public AjaxResult sumStorageRegion(StorageSectionDTO storageSectionDTO)
{
//转换实体
StorageSectionDO storageSectionDO = storageSectionAssembler.toDO(storageSectionDTO);
return AjaxResult.success(storageSectionApplicationService.sumStorageRegion(storageSectionDO));
}
/**
* 分页查询库区列表
*/
@@ -40,14 +40,14 @@ public class SysConfigController extends BaseController
/**
* 获取参数配置列表
*/
@RequiresPermissions("system:config:list")
@GetMapping("/list")
public TableDataInfo list(SysConfig config)
{
startPage();
List<SysConfig> list = configService.selectConfigList(config);
return getDataTable(list);
}
// @RequiresPermissions("system:config:list")
// @GetMapping("/list")
// public TableDataInfo list(SysConfig config)
// {
// startPage();
// List<SysConfig> list = configService.selectConfigList(config);
// return getDataTable(list);
// }
@Log(title = "参数管理", businessType = BusinessType.EXPORT)
@RequiresPermissions("system:config:export")
@@ -71,11 +71,11 @@ public class SysConfigController extends BaseController
/**
* 根据参数键名查询参数值
*/
@GetMapping(value = "/configKey/{configKey}")
public AjaxResult getConfigKey(@PathVariable String configKey)
{
return AjaxResult.success(configService.selectConfigByKey(configKey));
}
// @GetMapping(value = "/configKey/{configKey}")
// public AjaxResult getConfigKey(@PathVariable String configKey)
// {
// return AjaxResult.success(configService.selectConfigByKey(configKey));
// }
/**
* 新增参数配置
@@ -85,6 +85,9 @@ public class SysUserController extends BaseController
}
}
List<SysUser> list = userService.selectUserList(user);
for (SysUser sysUser : list) {
sysUser.setPassword("******");
}
return getDataTable(list);
}
@@ -14,16 +14,16 @@ spring:
nacos:
discovery:
# server-addr: 127.0.0.1:8848
server-addr: 10.33.0.129:6010
# server-addr: 10.102.192.30:6848
# server-addr: 10.33.0.129:6010
server-addr: 10.102.192.31:6848
username: nacos
password: manhuoda@2023
#线上正式环境
# server-addr: 10.102.192.105:6848
config:
# server-addr: 127.0.0.1:8848
server-addr: 10.33.0.129:6010
# server-addr: 10.102.192.30:6848
# server-addr: 10.33.0.129:6010
server-addr: 10.102.192.31:6848
username: nacos
password: manhuoda@2023
#线上正式环境
@@ -102,4 +102,12 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<include refid="selectStorageSectionPo1"/>
order by a.storage_name asc, a.create_time desc
</select>
<!-- 库区面积合计:与 queryList 相同筛选条件,SUM(库区面积),无数据时返回 0 -->
<select id="sumStorageRegion" parameterType="com.mhd.basic.domain.storageSection.repository.todo.StorageSectionDO" resultType="java.math.BigDecimal">
select IFNULL(SUM(a.storage_region), 0)
from storage_section a
LEFT JOIN warehouse w ON a.warehouse_id = w.warehouse_id AND w.del_flag = 1
<include refid="selectStorageSectionPo1"/>
</select>
</mapper>
@@ -129,8 +129,14 @@ public class CommonApplicationService {
* @param userDO
*/
public void checkAccountInfo(UserDO userDO) {
//获取当前登录用户的最高组织
Long topOrganizationId = SecurityUtils.getLoginUser().getUserPo().getTopOrganizationId();
//获取当前登录用户的最高组织远程Feign调用无loginUser时从userDO取
Long topOrganizationId;
LoginUser loginUser = SecurityUtils.getLoginUser();
if (ObjectUtil.isNotNull(loginUser)) {
topOrganizationId = loginUser.getUserPo().getTopOrganizationId();
} else {
topOrganizationId = userDO.getTopOrganizationId();
}
checkUserInfo(topOrganizationId, userDO);
}
@@ -735,14 +735,20 @@ public class UserApplicationService {
*/
public void addUserInfo(UserDO userDO) {
LoginUser loginUser = SecurityUtils.getLoginUser();
if (ObjectUtil.isNull(loginUser)) {
throw new DigitalLogisticsException(UserError.USER_NULL);
UserPo loginUserPo = null;
if (ObjectUtil.isNotNull(loginUser)) {
loginUserPo = loginUser.getUserPo();
}
UserPo loginUserPo = loginUser.getUserPo();
CheckDO checkDO = userFactory.getCheckDo(userDO);
//用户密码传值
if (StringUtils.isNotEmpty(userDO.getUserPassword())) {
String userPassword = AESUtil.decrypt(userDO.getUserPassword());
String userPassword;
if (ObjectUtil.isNotNull(loginUser)) {
userPassword = AESUtil.decrypt(userDO.getUserPassword());
} else {
// Feign调用传入的是明文密码无需AES解密
userPassword = userDO.getUserPassword();
}
checkDO.setUserPassword(userPassword);
}
//用户信息格式校验
@@ -750,9 +756,15 @@ public class UserApplicationService {
String salt = OConvertUtils.randomGen(8);
userDO.setUserSalt(salt);
regCheck(checkDO);
//用户密码传值
//用户密码加密入库
if (StringUtils.isNotEmpty(userDO.getUserPassword())) {
String userPassword = AESUtil.decrypt(userDO.getUserPassword());
String userPassword;
if (ObjectUtil.isNotNull(loginUser)) {
userPassword = AESUtil.decrypt(userDO.getUserPassword());
} else {
// Feign调用传入的是明文密码无需AES解密
userPassword = userDO.getUserPassword();
}
//加密算法PBEWithMD5AndDES
String passwordEncode = PasswordUtil.encrypt(userDO.getUserAccount(), userPassword, salt);
userDO.setUserPassword(passwordEncode);
@@ -783,11 +795,13 @@ public class UserApplicationService {
String idCardDes = DesensitizedUtil.desensitizedIdNumber(idCard);
userDO.setUserIdcardNumberDes(idCardDes);
}
userDO.setCreateBy(loginUserPo.getUserId());
userDO.setCreateByName(loginUserPo.getUserName());
if (loginUserPo.getUserAccountType() != null && loginUserPo.getUserAccountType() == 5) {
userDO.setCreateBy(loginUserPo.getCreateBy());
userDO.setCreateByName(loginUserPo.getCreateByName());
if (ObjectUtil.isNotNull(loginUserPo)) {
userDO.setCreateBy(loginUserPo.getUserId());
userDO.setCreateByName(loginUserPo.getUserName());
if (loginUserPo.getUserAccountType() != null && loginUserPo.getUserAccountType() == 5) {
userDO.setCreateBy(loginUserPo.getCreateBy());
userDO.setCreateByName(loginUserPo.getCreateByName());
}
}
userDO.setCreateTime(new Date());
//用户状态0-无状态1-正常2-锁定
@@ -1379,6 +1393,66 @@ public class UserApplicationService {
}
}
/**
* 远程Feign调用新增用户无token仅操作USER表基本字段
* 调用方需传入organizationIduserAccountuserPassword明文userName
*/
@Transactional(rollbackFor = Exception.class)
public UserPo addUserByFeign(UserDO userDO) {
// 1. 获取一级组织
AjaxResult organizationInfo = productServiceFeign.getTopOrganization(userDO.getOrganizationId());
String code = organizationInfo.get("code").toString();
if (!"200".equals(code)) {
throw new DigitalLogisticsException(UserError.USER_ACCOUNT_REG_ERROR);
}
String data = organizationInfo.get("data").toString();
if (ObjectUtil.isNotNull(data)) {
userDO.setTopOrganizationId(Long.parseLong(data));
}
// 2. 获取组织名称
AjaxResult orgInfoResult = productServiceFeign.getInfo(userDO.getOrganizationId());
if ("200".equals(orgInfoResult.get("code").toString())) {
Object orgData = orgInfoResult.get("data");
if (ObjectUtil.isNotNull(orgData)) {
OrganizationPo organizationPo = BeanUtil.toBean(orgData, OrganizationPo.class);
if (ObjectUtil.isNotNull(organizationPo)) {
userDO.setOrganizationName(organizationPo.getOrganizationName());
}
}
}
// 3. 密码加密调用方传入明文无需AES解密
if (StringUtils.isNotEmpty(userDO.getUserPassword())) {
String salt = OConvertUtils.randomGen(8);
userDO.setUserSalt(salt);
String passwordEncode = PasswordUtil.encrypt(userDO.getUserAccount(), userDO.getUserPassword(), salt);
userDO.setUserPassword(passwordEncode);
}
// 4. 根据userId判断新增还是更新
if (ObjectUtil.isNotNull(userDO.getUserId())) {
// 有userId更新已有用户
UserEntity updateEntity = new UserEntity();
BeanUtils.copyProperties(userDO, updateEntity);
userMapper.updateById(updateEntity);
UserPo userPo = userMapper.selectByUserId(userDO.getUserId());
return userPo;
} else {
// 无userId新增用户
userDO.setCreateTime(new Date());
userDO.setUserStatus(1);
userDO.setUserAuthStatus(1);
userDO.setDelFlag(1);
userDO.setRoleCode(RoleEnum.MUST.getCode());
userDO.setRoleName(RoleEnum.MUST.getName());
if (ObjectUtil.isNull(userDO.getUserAccount()) && ObjectUtil.isNotNull(userDO.getUserPhone())) {
userDO.setUserAccount(userDO.getUserPhone());
}
UserEntity userEntity = userDomainService.addUser(userDO);
UserPo userPo = new UserPo();
BeanUtils.copyProperties(userEntity, userPo);
return userPo;
}
}
/**
* @Description 新增用户
* @Author Alex
@@ -1388,18 +1462,31 @@ public class UserApplicationService {
public UserPo addUserYG(UserDO userDO) {
LoginUser loginUser = SecurityUtils.getLoginUser();
String password = userDO.getPlainPassword();
if (ObjectUtil.isNull(loginUser)) {
throw new DigitalLogisticsException(UserError.TIMEOUT);
// 远程Feign调用时可能无loginUser优先从loginUser取组织信息否则使用userDO自身携带的数据
if (ObjectUtil.isNotNull(loginUser)) {
userDO.setOrganizationName(loginUser.getUserPo().getOrganizationName());
} else if (ObjectUtil.isNotNull(userDO.getOrganizationId()) && ObjectUtil.isNull(userDO.getTopOrganizationId())) {
// 无loginUser时提前获取一级组织ID供后续checkAccountInfo使用
AjaxResult topOrgResult = productServiceFeign.getTopOrganization(userDO.getOrganizationId());
if ("200".equals(topOrgResult.get("code").toString())) {
String topOrgData = topOrgResult.get("data").toString();
if (ObjectUtil.isNotNull(topOrgData)) {
userDO.setTopOrganizationId(Long.parseLong(topOrgData));
}
}
}
userDO.setOrganizationName(loginUser.getUserPo().getOrganizationName());
//2023年3月24日 bug修改 增加登录账号/手机号唯一性校验
commonApplicationService.checkAccountInfo(userDO);
if (ObjectUtil.isNotNull(userDO.getUserId())) {
return updateUser(userDO);
} else {
if (ObjectUtil.isNull(userDO.getOrganizationId())) {
userDO.setOrganizationId(loginUser.getUserPo().getOrganizationId());
userDO.setOrganizationName(loginUser.getUserPo().getOrganizationName());
if (ObjectUtil.isNotNull(loginUser)) {
userDO.setOrganizationId(loginUser.getUserPo().getOrganizationId());
userDO.setOrganizationName(loginUser.getUserPo().getOrganizationName());
} else {
throw new DigitalLogisticsException(UserError.TIMEOUT);
}
} else {
// 如果前端传递了 organizationId根据 organizationId 查询组织信息获取正确的组织名称
AjaxResult orgInfoResult = productServiceFeign.getInfo(userDO.getOrganizationId());
@@ -1441,7 +1528,14 @@ public class UserApplicationService {
//组装新增用户信息
addUserInfo(userDO);
// 远程Feign调用无loginUser时强制分配默认普通角色不走管理员
if (ObjectUtil.isNull(loginUser)) {
userDO.setRoleCode(RoleEnum.TENANT.getCode());
userDO.setRoleName(RoleEnum.TENANT.getName());
userDO.setUserAccountType(null);
}
UserEntity userEntity = userDomainService.addUser(userDO);
if (ObjectUtil.isNotNull(loginUser)) {
if (isCustomerManagementPresetRole(userEntity.getRoleCode())) {
// 保持插入结果不再改 role_code / user_role
} else if ((StringUtils.isEmpty(userEntity.getRoleCode()) || !RoleEnum.PLAT_ADMIN.getCode().equals(userEntity.getRoleCode())) &&
@@ -1480,6 +1574,22 @@ public class UserApplicationService {
userDomainService.updateUser(userDO);
}
}
} // end of loginUser != null check
else {
RoleDo roleDo = new RoleDo();
roleDo.setRoleCode(userEntity.getRoleCode());
roleDo.setOrganizationId(userEntity.getOrganizationId());
RoleEntity roleEntity = roleDomainService.selectByOrganizationIdAndRoleCode(roleDo);
if (roleEntity != null) {
UserRoleDo userRoleDo = new UserRoleDo();
userRoleDo.setUserId(userEntity.getUserId());
userRoleDo.setRoleId(roleEntity.getRoleId());
userRoleDomainService.addUserRole(userRoleDo);
userDO.setRoleId(roleEntity.getRoleId());
userDO.setUserId(userEntity.getUserId());
userDomainService.updateUser(userDO);
}
}
BeanUtils.copyProperties(userEntity, userPo);
//同步网货平台员工信息
try {
@@ -1492,13 +1602,15 @@ public class UserApplicationService {
// throw e;
}
//获取组织信息
Long organizationId = loginUser.getUserPo().getOrganizationId();
Long organizationId = ObjectUtil.isNotNull(loginUser) ? loginUser.getUserPo().getOrganizationId() : userDO.getOrganizationId();
Integer isQueue = userMapper.getOrdIsQueue(organizationId);
if (isQueue==1) {
//同步YMS员工信息
try {
userEntity.setUserPassword(password);
if (StringUtils.isNotEmpty(password)) {
userEntity.setUserPassword(password);
}
ymsDomainService.addOrEditYmsUser(userEntity);
} catch (Exception e) {
// 同步YMS失败时记录日志但不影响用户保存
@@ -3428,6 +3540,24 @@ public class UserApplicationService {
if (!ObjectUtil.equal(String.valueOf(object), verificationCode) && !ObjectUtil.equal(verificationCode, SmsConstants.universalCode)) {
throw new ServiceException("验证码错误");
}*/
String oldUserPassword = userDO.getOldUserPassword();
if (oldUserPassword==null||oldUserPassword=="") {
throw new ServiceException("旧密码不能为空");
}
String userPassword1 = userPo.getUserPassword();
String oldDecrypt = AESUtil.decrypt(oldUserPassword);
String nowDecrypt = AESUtil.decrypt(userPassword1);
if (!oldDecrypt.equals(nowDecrypt)) {
throw new ServiceException("旧密码错误");
}
String newUserPassword = userDO.getUserPassword();
// 校验新密码复杂度必须同时包含大写字母小写字母数字和特殊符号
if (StringUtils.isNotEmpty(newUserPassword)) {
String decryptedNewPassword = AESUtil.decrypt(newUserPassword);
if (!decryptedNewPassword.matches("^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[^a-zA-Z0-9]).+$")) {
throw new ServiceException("新密码必须同时包含大写字母、小写字母、数字和特殊符号");
}
}
//用户密码传值
if (StringUtils.isNotEmpty(userDO.getUserPassword())) {
String userPassword = AESUtil.decrypt(userDO.getUserPassword());
@@ -3444,25 +3574,25 @@ public class UserApplicationService {
userDomainService.updateUser(userDO);
}
}else {
String userPassword1 = userDO.getUserPassword();
for (Long id : userDO.getUserIds()) {
UserPo userPo1 = userDomainService.selectByUserId(id);
//用户密码传值
if (StringUtils.isNotEmpty(userDO.getUserPassword())) {
String userPassword = AESUtil.decrypt(userPassword1);
// 生成随机8位数盐
String salt = OConvertUtils.randomGen(8);
userDO.setUserSalt(salt);
//加密算法PBEWithMD5AndDES
String passwordEncode = PasswordUtil.encrypt(userPo1.getUserAccount(), userPassword, salt);
userDO.setUserPassword(passwordEncode);
}
if (ObjectUtil.isNotNull(userPo1)) {
userDO.setUserId(userPo1.getUserId());
userDomainService.updateUser(userDO);
}
}
// String userPassword1 = userDO.getUserPassword();
// for (Long id : userDO.getUserIds()) {
// UserPo userPo1 = userDomainService.selectByUserId(id);
// //用户密码传值
// if (StringUtils.isNotEmpty(userDO.getUserPassword())) {
// String userPassword = AESUtil.decrypt(userPassword1);
// // 生成随机8位数盐
// String salt = OConvertUtils.randomGen(8);
// userDO.setUserSalt(salt);
// //加密算法PBEWithMD5AndDES
// String passwordEncode = PasswordUtil.encrypt(userPo1.getUserAccount(), userPassword, salt);
// userDO.setUserPassword(passwordEncode);
// }
//
// if (ObjectUtil.isNotNull(userPo1)) {
// userDO.setUserId(userPo1.getUserId());
// userDomainService.updateUser(userDO);
// }
// }
}
return true;
}
@@ -3874,6 +4004,18 @@ public class UserApplicationService {
//新支付密码
String payPassword = userDTO.getUserPayPassword();
String userPayPassword = userPo.getUserPayPassword();
String nowPayPassword = AESUtil.decrypt(userPayPassword);
String oldUserPassword = userDTO.getOldUserPayPassword();
if(oldUserPassword == null || "".equals(oldUserPassword)) {
throw new ServiceException("旧密码不能为空");
}
String oldPayPassword = AESUtil.decrypt(oldUserPassword);
if(!nowPayPassword.equals(oldPayPassword)) {
throw new ServiceException("旧密码不正确");
}
//验证码登录校验验证码
String key = CacheConstants.SMS_PAY_CHANGE + userPhone;
Object object = redisService.getCacheObject(key) == null ? "" : redisService.getCacheObject(key);
@@ -22,12 +22,12 @@ public class UserRoleFactory {
UserRoleEntity userRoleEntity = new UserRoleEntity();
BeanUtils.copyProperties(userRoleDo,userRoleEntity, IgnoreNullUtil.getNullPropertyNames(userRoleDo));
LoginUser loginUser = SecurityUtils.getLoginUser();
if (ObjectUtil.isNull(loginUser)) {
throw new DigitalLogisticsException(UserError.TIMEOUT);
String userName = null;
Long userId = null;
if (ObjectUtil.isNotNull(loginUser)) {
userName = loginUser.getUsername();
userId = loginUser.getUserid();
}
String userName = loginUser.getUsername();
// 获取登录人id
Long userId = loginUser.getUserid();
if(userRoleDo.getUserRoleId() != null){
if(userId != null){
userRoleEntity.setUpdateBy(userId);
@@ -49,6 +49,8 @@ public class UserDO extends BaseVOEntity {
private String userMemberCode;
@ApiModelProperty(name = "登录密码")
private String userPassword;
@ApiModelProperty(name = "老登录密码")
private String oldUserPassword;
@ApiModelProperty(name = "确认登录密码")
private String copyUserPassword;
@ApiModelProperty(name = "")
@@ -12,6 +12,7 @@ import com.mhd.user.domain.userAggregate.repository.todo.UserDO;
import com.mhd.common.core.domain.po.app.UserAppPo;
import com.mhd.common.core.annotation.fieldsetvalue.NeedSetValueMethod;
import com.mhd.common.security.utils.SecurityUtils;
import com.mhd.system.api.model.LoginUser;
import com.mhd.user.infrastructure.util.annotation.DataPermissions;
import com.mhd.user.interfaces.dto.queryDTO.UserDocumentWarningQueryDTO;
import com.mhd.user.interfaces.dto.updateDTO.UpdateHasDefaultAddressDTO;
@@ -50,9 +51,12 @@ public class UserDomainService {
*/
public UserEntity updateUser(UserDO userDO) {
UserEntity userEntity = userFactory.getUserEntity(userDO);
userEntity.setUpdateBy(SecurityUtils.getLoginUser().getUserid());
LoginUser loginUser = SecurityUtils.getLoginUser();
if (ObjectUtil.isNotNull(loginUser)) {
userEntity.setUpdateBy(loginUser.getUserid());
userEntity.setUpdateByName(loginUser.getUsername());
}
userEntity.setUpdateTime(new Date());
userEntity.setUpdateByName(SecurityUtils.getLoginUser().getUsername());
userRepositoryInterface.updateUser(userEntity);
return userEntity;
}
@@ -49,6 +49,8 @@ public class UserDTO extends BaseVOEntity {
private String userSalt;
@ApiModelProperty(name = "支付密码")
private String userPayPassword;
@ApiModelProperty(name = "旧支付密码")
private String oldUserPayPassword;
@ApiModelProperty(name = "身份证号")
private String userIdcardNumber;
@ApiModelProperty(name = "身份证号加密")
@@ -283,6 +283,22 @@ public class UserAPI extends BaseController {
return R.ok(sysUserVo, "操作成功");
}
/**
* 远程Feign调用新增用户无token仅操作USER表基本字段
*/
@ApiOperation("远程Feign修改用户(无token)")
@PostMapping("/addUserByFeign")
public R<LoginUser> addUserByFeign(@RequestBody UserDTO userDTO) {
LoginUser sysUserVo = new LoginUser();
UserDO userDO = userAssember.toUserDO(userDTO);
UserPo userPo = userApplicationService.addUserByFeign(userDO);
if (ObjectUtil.isNotNull(userPo)) {
sysUserVo.setUserid(userPo.getUserId());
}
sysUserVo.setUserPo(userPo);
return R.ok(sysUserVo, "操作成功");
}
/**
* @Description 新增合作商
* @Author Alex
@@ -19,14 +19,12 @@ spring:
nacos:
discovery:
# server-addr: 127.0.0.1:8848
# 线上测试环境配置 容器名+端口号
# server-addr: 10.33.0.129:6010
server-addr: 10.102.192.31:6848
username: nacos
password: manhuoda@2023
config:
# server-addr: 127.0.0.1:8848
# 线上测试环境配置 容器名+端口号
# server-addr: 10.33.0.129:6010
server-addr: 10.102.192.31:6848
username: nacos
@@ -472,7 +472,11 @@ public class BillManageApplicationService {
BeanUtils.copyProperties(billDetailDTO, billingStatementDTO);
String firstSubjectCode = billDetailDTO.getFirstSubjectCode();
Double taxRate = billDetailMapper.getTaxRate(firstSubjectCode);
// 科目编码可能重复按编码+名称取唯一税率科目删除/停用时兜底0避免空指针
Double taxRate = billDetailMapper.getTaxRate(firstSubjectCode, billDetailDTO.getFirstSubjectName());
if (taxRate == null) {
taxRate = 0.0;
}
BigDecimal amount = billingStatementDTO.getBillingAmount();
BigDecimal taxAmount = amount.divide(BigDecimal.ONE.add(new BigDecimal(taxRate).divide(BigDecimal.valueOf(100))),10, BigDecimal.ROUND_HALF_UP).multiply(new BigDecimal(taxRate).divide(BigDecimal.valueOf(100))).setScale(2, BigDecimal.ROUND_HALF_UP);
BigDecimal taxFreeFee = amount.subtract(taxAmount);
@@ -484,16 +488,26 @@ public class BillManageApplicationService {
billingStatementDTO.setBillManageId(billManageDO.getBillManageId());
billingStatementDTO.setBillingState(3);
Long billingStatementId = billingStatementApplicationService.insertAndReturnId(billingStatementDTO);
//遍历billDetailDTOList集合将计费流水号相同的赋值计费流水id
billDetailDTOList.stream().filter(item -> item.getBillingFlow().equals(billDetailDTO.getBillingFlow())).forEach(item -> {
item.setBillingStatementId(billingStatementId);
item.setBillType(billManageDTO.getBillType());
item.setBillManageId(billManageDO.getBillManageId());
item.setBillNumber(billNumber);
item.setInvoiceItem(item.getFirstSubjectCode());
item.setInvoiceItemName(item.getFirstSubjectName());
item.setFeeType(item.getServiceItemsName());
});
//遍历billDetailDTOList集合将计费流水号相同的赋值计费流水id流水号为空时只处理自身
if (StringUtils.isNotBlank(billDetailDTO.getBillingFlow())) {
billDetailDTOList.stream().filter(item -> billDetailDTO.getBillingFlow().equals(item.getBillingFlow())).forEach(item -> {
item.setBillingStatementId(billingStatementId);
item.setBillType(billManageDTO.getBillType());
item.setBillManageId(billManageDO.getBillManageId());
item.setBillNumber(billNumber);
item.setInvoiceItem(item.getFirstSubjectCode());
item.setInvoiceItemName(item.getFirstSubjectName());
item.setFeeType(item.getServiceItemsName());
});
} else {
billDetailDTO.setBillingStatementId(billingStatementId);
billDetailDTO.setBillType(billManageDTO.getBillType());
billDetailDTO.setBillManageId(billManageDO.getBillManageId());
billDetailDTO.setBillNumber(billNumber);
billDetailDTO.setInvoiceItem(billDetailDTO.getFirstSubjectCode());
billDetailDTO.setInvoiceItemName(billDetailDTO.getFirstSubjectName());
billDetailDTO.setFeeType(billDetailDTO.getServiceItemsName());
}
}
//同步更新已有计费流水新增账单时可修改已有费用
List<BillDetailDTO> billDetailDTOListHasBillingStatementId = billDetailDTOList.stream()
@@ -514,7 +528,11 @@ public class BillManageApplicationService {
BigDecimal allTaxFreeFee = BigDecimal.ZERO;
for (BillDetailDTO item : billDetailDTOList) {
String firstSubjectCode = item.getFirstSubjectCode();
Double taxRate = billDetailMapper.getTaxRate(firstSubjectCode);
// 同上编码+名称取唯一税率查不到兜底0
Double taxRate = billDetailMapper.getTaxRate(firstSubjectCode, item.getFirstSubjectName());
if (taxRate == null) {
taxRate = 0.0;
}
BigDecimal amount = item.getBillingAmount();
BigDecimal taxAmount = amount.divide(BigDecimal.ONE.add(new BigDecimal(taxRate).divide(BigDecimal.valueOf(100))),10, BigDecimal.ROUND_HALF_UP).multiply(new BigDecimal(taxRate).divide(BigDecimal.valueOf(100))).setScale(2, BigDecimal.ROUND_HALF_UP);
BigDecimal taxFreeFee = amount.subtract(taxAmount);
@@ -584,11 +602,20 @@ public class BillManageApplicationService {
originalBill.setInvoiceMakeTime(null);
originalBill.setNcId(null);
originalBill.setSkNcId(null);
// 明细清空id
// 明细清空id同时清空计费流水id并按"原流水→新流水"映射生成新流水号
// 否则提交addBill会走"更新已有流水"分支把原账单的流水改挂到新账单上原账单明细丢失
Map<String, String> flowMapping = new HashMap<>();
for (BillDetailPO detail : originalDetails) {
detail.setBillDetailId(null);
detail.setBillManageId(null);
detail.setBillNumber(null);
detail.setBillingStatementId(null);
String oldFlow = detail.getBillingFlow();
if (StringUtils.isNotBlank(oldFlow)) {
detail.setBillingFlow(flowMapping.computeIfAbsent(oldFlow, k -> OrderSequence.getOrderCode("JFLS")));
} else {
detail.setBillingFlow(null);
}
}
originalBill.setBillDetailPOList(originalDetails);
return originalBill;
@@ -739,7 +766,7 @@ public class BillManageApplicationService {
}
public List<NcLog> getNcLog(Long id, Long type) {
return ncLogMapper.selectList(new LambdaQueryWrapper<NcLog>().eq(NcLog::getOrderId, id).eq(NcLog::getType, type).orderByDesc(NcLog::getSendTime));
return ncLogMapper.selectList(new LambdaQueryWrapper<NcLog>().eq(NcLog::getOrderId, id).eq(NcLog::getType, type).orderByDesc(NcLog::getNcLogId));
}
/**
@@ -815,6 +842,7 @@ public class BillManageApplicationService {
billingStatementDO.setUpdateBy(loginUser.getUserid());
billingStatementDO.setUpdateByName(loginUser.getUserPo().getUserName());
billingStatementDO.setUpdateTime(new Date());
billingStatementDO.setBillingRemark(detailDTO.getBillingRemark());
billingStatementApplicationService.update(billingStatementDO);
}
Boolean flag = billDetailDomainService.update(billDetailDO);
@@ -568,6 +568,10 @@ public class BillingStatementApplicationService {
handleDate(billingStatementDO);
billingStatementDO.setDataSources(2);
// 币种为空时兜底CNY与addCost一致NC推送时该值作pk_currtype空值会推送失败
if (StringUtils.isEmpty(billingStatementDO.getSettlementCurrency())) {
billingStatementDO.setSettlementCurrency("CNY");
}
if(StringUtils.isEmpty(billingStatementDO.getBillingFlow())){
billingStatementDO.setBillingFlow(OrderSequence.getOrderCode("JFLS"));
}
@@ -593,6 +597,10 @@ public class BillingStatementApplicationService {
BillingStatementPO billingStatementPO = new BillingStatementPO();
//处理数据
handleDate(billingStatementDO);
// 币种为空时兜底CNY编码与前端默认值一致NC推送时该值原样作pk_currtype须在copyProperties之前赋值PO才能带上
if (StringUtils.isEmpty(billingStatementDO.getSettlementCurrency())) {
billingStatementDO.setSettlementCurrency("CNY");
}
BeanUtils.copyProperties(billingStatementDO,billingStatementPO);
billingStatementPO.setBillingTotalAmount(billingStatementDO.getBillingAmount());
billingStatementPO.setDataSources(2);
@@ -0,0 +1,155 @@
package com.mhd.bms.application.server.feeAnalysisReport;
import cn.hutool.core.util.ObjectUtil;
import com.mhd.bms.application.server.feeAnalysisReport.FeeAnalysisReportApplicationService.AuthScope;
import com.mhd.bms.domain.feeAnalysisReport.service.FeeAnalysisReportDomainService;
import com.mhd.bms.domain.feeAnalysisReport.service.support.FeeAnalysisExcelExporter;
import com.mhd.bms.domain.feeAnalysisReport.service.support.IntelligentAnalysisEngine;
import com.mhd.bms.interfaces.dto.feeAnalysisReport.FeeAnalysisReportQueryDTO;
import com.mhd.bms.interfaces.dto.feeAnalysisReport.FeeAnalysisReportResultDTO;
import com.mhd.bms.interfaces.dto.feeAnalysisReport.IntelligentAnalysisResultDTO;
import com.mhd.common.core.exception.ServiceException;
import com.mhd.common.core.exception.digitalLogisticsException.DigitalLogisticsException;
import com.mhd.common.core.exception.digitalLogisticsException.UserError;
import com.mhd.common.security.utils.SecurityUtils;
import com.mhd.system.api.model.LoginUser;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.net.URLEncoder;
import java.time.LocalDate;
import java.util.List;
import java.util.Objects;
/**
* 费用统计分析报表ApplicationService
* 职责登录校验多租户/组织数据范围注入参数校验与默认值编排领域服务
*
* @author rcx
* @date 2026-08-17
*/
@Service
@Slf4j
public class FeeAnalysisReportApplicationService {
/** 南光特权组织ID:可查全部组织数据(与 BillManageApplicationService 口径一致) */
private static final Long SPECIAL_ORG_NAN_GUANG = 2827L;
/** 总部一级组织ID:1 可看全部,其余租户强制隔离 */
private static final Long HQ_TOP_ORG_ID = 1L;
@Autowired
private FeeAnalysisReportDomainService feeAnalysisReportDomainService;
@Autowired
private IntelligentAnalysisEngine intelligentAnalysisEngine;
@Autowired
private FeeAnalysisExcelExporter feeAnalysisExcelExporter;
/** 报表查询(主入口,导出与智能分析复用) */
public FeeAnalysisReportResultDTO query(FeeAnalysisReportQueryDTO queryDTO) {
AuthScope scope = fillAuthScope(queryDTO);
int year = normalizeYear(queryDTO.getYear());
Integer type = normalizeType(queryDTO.getAccountExpenseType());
return feeAnalysisReportDomainService.buildReport(year, type, scope.topOrganizationId, scope.organizationId);
}
/** 智能分析:先取同一份数据(保证口径一致),再交给规则引擎 */
public IntelligentAnalysisResultDTO intelligentAnalysis(FeeAnalysisReportQueryDTO queryDTO) {
FeeAnalysisReportResultDTO report = query(queryDTO);
AuthScope scope = fillAuthScope(queryDTO);
return intelligentAnalysisEngine.analyze(report, report.getYear(),
report.getAccountExpenseType(), scope.topOrganizationId, scope.organizationId);
}
/** 导出:与 /query 完全同源数据;响应头用实际生效年份拼文件名(仿 BillManageApi.exportReceivableList */
public void export(FeeAnalysisReportQueryDTO queryDTO, HttpServletResponse response) throws IOException {
FeeAnalysisReportResultDTO report = query(queryDTO);
String fileName = URLEncoder.encode("费用统计分析报表" + report.getYear(), "UTF-8")
.replaceAll("\\+", "%20");
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setCharacterEncoding("utf-8");
response.setHeader("Content-disposition", "attachment;filename=" + fileName + ".xlsx");
feeAnalysisExcelExporter.export(response, report);
}
/** 年份下拉:库中有费用的年份(降序) */
public List<String> selectYears(FeeAnalysisReportQueryDTO queryDTO) {
AuthScope scope = fillAuthScope(queryDTO);
return feeAnalysisReportDomainService.selectDistinctYears(
normalizeType(queryDTO.getAccountExpenseType()), scope.topOrganizationId);
}
// ------------------------------------------------------------------
// 安全闸门登录校验 + 多租户隔离 + 2827 特例分支解析见本节末尾表格
// ------------------------------------------------------------------
/**
* 决定"当前用户能看哪个数据范围"结果通过 AuthScope 传给领域服务
* 为什么必须在后端做费用是财务敏感数据隔离条件从登录态注入
* 前端传什么都无法越权传别人的组织会被强制改回本人组织
*/
private AuthScope fillAuthScope(FeeAnalysisReportQueryDTO queryDTO) {
// 登录校验照抄 ReportFormApplicationService:84-87
LoginUser loginUser = SecurityUtils.getLoginUser();
if (ObjectUtil.isNull(loginUser) || ObjectUtil.isNull(loginUser.getUserPo())) {
throw new DigitalLogisticsException(UserError.TIMEOUT);
}
Long userOrganizationId = loginUser.getUserPo().getOrganizationId();
Long frontendOrganizationId = queryDTO.getOrganizationId(); // 前端传的组织可能越权下面裁决
AuthScope scope = new AuthScope();
if (Objects.equals(userOrganizationId, SPECIAL_ORG_NAN_GUANG)) {
// 南光组织2827特权账号可查全部组织照抄 BillManageApplicationService:97-107
if (frontendOrganizationId != null) {
scope.organizationId = frontendOrganizationId; // 前端指定了组织 按指定的查
} else {
scope.organizationId = null; // 没指定 查全部
}
scope.topOrganizationId = null; // 明确置 null不用一级组织条件
} else {
// 其他组织防越权裁决照抄 BillManageApplicationService:108-123
if (frontendOrganizationId != null && userOrganizationId != null
&& !frontendOrganizationId.equals(userOrganizationId)) {
// 前端传了别人的组织 强制改回本人组织
scope.organizationId = userOrganizationId;
} else {
scope.organizationId = frontendOrganizationId != null
? frontendOrganizationId : userOrganizationId;
}
// 组织条件组合照抄 BillManageApplicationService:124-133
if (scope.organizationId != null) {
scope.topOrganizationId = SPECIAL_ORG_NAN_GUANG; // 与账单口径一致有组织条件时 top 固定 2827
} else {
// 无组织条件 回退一级组织隔离非总部租户只能看自己租户总部(1)看全部
Long top = loginUser.getUserPo().getTopOrganizationId();
scope.topOrganizationId = (top != null && !top.equals(HQ_TOP_ORG_ID)) ? top : null;
}
}
return scope;
}
// ------------------------------------------------------------------
// 参数默认值与校验
// ------------------------------------------------------------------
private int normalizeYear(Integer year) {
int y = (year == null) ? LocalDate.now().getYear() : year;
if (y < 1900 || y > 2100) {
throw new ServiceException("年份参数不合法");
}
return y;
}
private Integer normalizeType(Integer accountExpenseType) {
return accountExpenseType == null ? 1 : accountExpenseType;
}
/** 数据范围值对象:两个字段都为 null 表示不过滤(查全部) */
public static class AuthScope {
public Long topOrganizationId;
public Long organizationId;
}
}
@@ -0,0 +1,22 @@
package com.mhd.bms.config;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.PaginationInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* MyBatis-Plus 分页插件配置
* 不注册该拦截器时Mapper 里传入的 Page 不生效SQL 不分页total 恒为 0
*/
@Configuration
public class MybatisPlusConfig {
@Bean
public PaginationInterceptor paginationInterceptor() {
PaginationInterceptor paginationInterceptor = new PaginationInterceptor();
// 达梦数据库不设置也会根据 jdbc:dm:// 自动识别这里显式指定更稳妥
paginationInterceptor.setDialectType(DbType.DM.getDb());
return paginationInterceptor;
}
}
@@ -21,5 +21,5 @@ public interface BillDetailMapper extends BaseMapper<BillDetail>
List<BillDetail> getDetailsByBillManageIdAndGroup(@Param("billManageId") Long billManageId);
Double getTaxRate(@Param("code") String code);
Double getTaxRate(@Param("code") String code, @Param("name") String name);
}
@@ -45,5 +45,5 @@ public interface BillManageMapper extends BaseMapper<BillManage>
String getOrgCodeNc(@Param("organizationId") Long organizationId);
Long getShipperId(@Param("userId") Long userId, @Param("organizationId") Long organizationId);
Long getShipperId(@Param("userId") Long userId);
}
@@ -211,7 +211,11 @@ public class BillManageImpl extends ServiceImpl<BillManageMapper, BillManage> im
BigDecimal allTaxFreeFee = BigDecimal.ZERO;
for (BillDetail item : billDetails) {
String firstSubjectCode = item.getInvoiceItem();
Double taxRate = billDetailMapper.getTaxRate(firstSubjectCode);
// 科目编码可能重复按编码+名称取唯一税率查不到兜底0避免空指针
Double taxRate = billDetailMapper.getTaxRate(firstSubjectCode, item.getInvoiceItemName());
if (taxRate == null) {
taxRate = 0.0;
}
BigDecimal amount = item.getBillingAmount();
BigDecimal taxAmount = amount.divide(BigDecimal.ONE.add(new BigDecimal(taxRate).divide(BigDecimal.valueOf(100))),10, BigDecimal.ROUND_HALF_UP).multiply(new BigDecimal(taxRate).divide(BigDecimal.valueOf(100))).setScale(2, BigDecimal.ROUND_HALF_UP);
BigDecimal taxFreeFee = amount.subtract(taxAmount);
@@ -265,7 +269,7 @@ public class BillManageImpl extends ServiceImpl<BillManageMapper, BillManage> im
throw new ServiceException("结算对象未找到!");
}
Long settlementEntityId = settlementCustomers.getSettlementEntityId();
Long shipperId = billManageMapper.getShipperId(settlementEntityId, organizationId);
Long shipperId = billManageMapper.getShipperId(settlementEntityId);
if (shipperId==null||shipperId==0){
throw new ServiceException("结算对象未找到!");
}
@@ -853,7 +857,11 @@ public class BillManageImpl extends ServiceImpl<BillManageMapper, BillManage> im
BigDecimal allTaxFreeFee = BigDecimal.ZERO;
for (BillDetail item : billDetails) {
String firstSubjectCode = item.getInvoiceItem();
Double taxRate = billDetailMapper.getTaxRate(firstSubjectCode);
// 科目编码可能重复按编码+名称取唯一税率查不到兜底0避免空指针
Double taxRate = billDetailMapper.getTaxRate(firstSubjectCode, item.getInvoiceItemName());
if (taxRate == null) {
taxRate = 0.0;
}
BigDecimal amount = item.getBillingAmount();
BigDecimal taxAmount = amount.divide(BigDecimal.ONE.add(new BigDecimal(taxRate).divide(BigDecimal.valueOf(100))),10, BigDecimal.ROUND_HALF_UP).multiply(new BigDecimal(taxRate).divide(BigDecimal.valueOf(100))).setScale(2, BigDecimal.ROUND_HALF_UP);
BigDecimal taxFreeFee = amount.subtract(taxAmount);
@@ -932,7 +940,7 @@ public class BillManageImpl extends ServiceImpl<BillManageMapper, BillManage> im
throw new ServiceException("结算对象未找到!");
}
Long settlementEntityId = settlementCustomers.getSettlementEntityId();
Long shipperId = billManageMapper.getShipperId(settlementEntityId, organizationId);
Long shipperId = billManageMapper.getShipperId(settlementEntityId);
if (shipperId==null||shipperId==0){
throw new ServiceException("结算对象未找到!");
}
@@ -140,5 +140,7 @@ public class BillDetailPO extends BaseVOEntity{
private BigDecimal billingNum;
@ApiModelProperty(value = "计费单价")
private BigDecimal billingUnitPrice;
@ApiModelProperty(value = "流水备注")
private String billingRemark;
}
@@ -0,0 +1,32 @@
package com.mhd.bms.domain.feeAnalysisReport.repository.mapper;
import com.mhd.bms.domain.feeAnalysisReport.repository.po.FeeSubjectMonthStatPO;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* 费用统计分析报表Mapper
*
* @author rcx
* @date 2026-08-17
*/
public interface FeeAnalysisReportMapper {
/** 按科目(一级+二级)×年月汇总费用金额(跨两个年度,区间为左闭右开 [beginDate, endDate) */
List<FeeSubjectMonthStatPO> sumBySubjectAndMonth(@Param("beginDate") String beginDate,
@Param("endDate") String endDate,
@Param("accountExpenseType") Integer accountExpenseType,
@Param("topOrganizationId") Long topOrganizationId,
@Param("organizationId") Long organizationId);
/** 智能分析-按科目×客户×年月分解(两期区间,客户归因用) */
List<FeeSubjectMonthStatPO> sumBySubjectAndCustomer(@Param("beginDate") String beginDate,
@Param("endDate") String endDate,
@Param("accountExpenseType") Integer accountExpenseType,
@Param("topOrganizationId") Long topOrganizationId,
@Param("organizationId") Long organizationId);
/** 年份下拉(降序) */
List<String> selectDistinctYears(@Param("accountExpenseType") Integer accountExpenseType,
@Param("topOrganizationId") Long topOrganizationId);
}
@@ -0,0 +1,28 @@
package com.mhd.bms.domain.feeAnalysisReport.repository.po;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.math.BigDecimal;
/** 费用统计-科目×年月汇总结果对象 */
@Data
@ApiModel("费用统计科目月度汇总对象")
public class FeeSubjectMonthStatPO {
@ApiModelProperty("一级科目code")
private String firstSubjectCode;
@ApiModelProperty("一级科目名称")
private String firstSubjectName;
@ApiModelProperty("二级科目code")
private String secondSubjectCode;
@ApiModelProperty("二级科目名称")
private String secondSubjectName;
@ApiModelProperty("统计月份 yyyy-MM")
private String statMonth;
@ApiModelProperty("客户名称(客户归因查询时使用)")
private String customerName;
@ApiModelProperty("金额合计(元)")
private BigDecimal totalAmount;
@ApiModelProperty("推送模式:SCATTERED=散租推送 / FIXED=整租推送或无法判定(由 document_form_json 判定,仅仓租类使用)")
private String pushMode;
}
@@ -0,0 +1,297 @@
package com.mhd.bms.domain.feeAnalysisReport.service;
import cn.hutool.core.util.StrUtil;
import com.mhd.bms.domain.feeAnalysisReport.repository.mapper.FeeAnalysisReportMapper;
import com.mhd.bms.domain.feeAnalysisReport.repository.po.FeeSubjectMonthStatPO;
import com.mhd.bms.domain.feeAnalysisReport.service.support.FeeAnalysisCalculator;
import com.mhd.bms.domain.feeAnalysisReport.service.support.FeeAnalysisCalculator.YearQuarter;
import com.mhd.bms.domain.feeAnalysisReport.service.support.FeeSubjectCategoryResolver;
import com.mhd.bms.interfaces.dto.feeAnalysisReport.FeeAnalysisReportResultDTO;
import com.mhd.bms.interfaces.dto.feeAnalysisReport.FeeAnalysisRowDTO;
import com.mhd.bms.interfaces.dto.feeAnalysisReport.FeeQuarterDTO;
import com.mhd.bms.interfaces.dto.feeAnalysisReport.MonthAmountDTO;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.time.YearMonth;
import java.util.*;
import java.util.stream.Collectors;
/**
* 费用统计分析报表领域服务行组装归组/排序/每行计算/合计行/占营收比
*
* @author rcx
* @date 2026-08-17
*/
@Service
@Slf4j
public class FeeAnalysisReportDomainService {
/** 合计行行名 */
private static final String TOTAL_ROW_NAME = "合计";
@Autowired
private FeeAnalysisReportMapper feeAnalysisReportMapper;
/**
* 构建整张报表六步逐步解释见 §6.3
*
* @param year 统计年份 Y
* @param accountExpenseType 1-收入 2-支出
* @param topOrganizationId 一级组织null=不过滤总部/南光特例
* @param organizationId 组织null=不过滤
*/
public FeeAnalysisReportResultDTO buildReport(int year, Integer accountExpenseType,
Long topOrganizationId, Long organizationId) {
// ========== 步骤0一次 SQL 取两年数据 [Y-1-01-01, Y+1-01-01) ==========
// 为什么要两年Q1 环比基期=上年Q4各季度同比基期=上年同季§1.3 规则3/4
String beginDate = (year - 1) + "-01-01";
String endDate = (year + 1) + "-01-01";
List<FeeSubjectMonthStatPO> stats = feeAnalysisReportMapper.sumBySubjectAndMonth(
beginDate, endDate, accountExpenseType, topOrganizationId, organizationId);
log.info("费用统计取数完成 year={}, type={}, 明细组数={}", year, accountExpenseType, stats.size());
// ========== 步骤1按行键(类别|模板二级文案)归组 Map<行键, Map<年月, 金额[]>> ==========
Map<String, Map<YearMonth, BigDecimal>> rowKeyMap = groupByRowKey(stats);
// 行键 科目信息取首条出现的记录用于行编码/名称与排序
Map<String, FeeSubjectMonthStatPO> subjectInfoMap = collectSubjectInfo(stats);
// ========== 步骤2行排序 类别模板顺序 仓储内散租/固定出租 当年累计()降序 ==========
Map<String, BigDecimal> currentYearTotalMap = calcCurrentYearTotal(rowKeyMap, year);
List<String> orderedRowKeys = sortRowKeys(rowKeyMap.keySet(), subjectInfoMap, currentYearTotalMap);
// ========== 步骤3逐行组装月列/季度块/累计 ==========
List<FeeAnalysisRowDTO> rows = new ArrayList<>(orderedRowKeys.size());
for (int i = 0; i < orderedRowKeys.size(); i++) {
String rowKey = orderedRowKeys.get(i);
FeeSubjectMonthStatPO subject = subjectInfoMap.get(rowKey);
rows.add(assembleRow(subject, rowKeyMap.get(rowKey), year, i + 1));
}
// ========== 步骤4合计行各月全科目求和后用与明细行完全相同的方法计算 ==========
Map<YearMonth, BigDecimal> totalMonthMap = mergeAllRows(rowKeyMap);
FeeAnalysisRowDTO totalRow = assembleRow(null, totalMonthMap, year, null);
// ========== 步骤5占营收比 每行自己的年累计 / 当年总累计v1.4 行级口径 ==========
fillRevenueRatio(rows, totalRow);
FeeAnalysisReportResultDTO result = new FeeAnalysisReportResultDTO();
result.setYear(year);
result.setAccountExpenseType(accountExpenseType);
result.setRemark(""); // 左下角"说明"本期固定空串需求规则2
result.setRows(rows);
result.setTotalRow(totalRow);
return result;
}
/** 年份下拉(透传 Mapper */
public List<String> selectDistinctYears(Integer accountExpenseType, Long topOrganizationId) {
return feeAnalysisReportMapper.selectDistinctYears(accountExpenseType, topOrganizationId);
}
// ------------------------------------------------------------------
// 步骤1归组
// ------------------------------------------------------------------
/**
* 行键 = 报表一级(类别) + "|" + 报表二级(具体费用项)
* 类别由 FeeSubjectCategoryResolver 按模板映射平级数据"一级科目名→类别"科目名降为二级
* 双编码同名科目2003/TYLSCCSNCZ按名称归并仍生效真实两级数据自动透传
*/
private String buildRowKey(FeeSubjectMonthStatPO po) {
return FeeSubjectCategoryResolver.firstOf(po) + "|" + FeeSubjectCategoryResolver.secondOf(po);
}
private Map<String, Map<YearMonth, BigDecimal>> groupByRowKey(List<FeeSubjectMonthStatPO> stats) {
Map<String, Map<YearMonth, BigDecimal>> rowKeyMap = new LinkedHashMap<>();
for (FeeSubjectMonthStatPO po : stats) {
String rowKey = buildRowKey(po);
BigDecimal amount = po.getTotalAmount() == null ? BigDecimal.ZERO : po.getTotalAmount();
// merge同名科目双编码或科目年中改名的金额累加到同一行不覆盖
rowKeyMap.computeIfAbsent(rowKey, k -> new HashMap<>(24))
.merge(YearMonth.parse(po.getStatMonth()), amount, BigDecimal::add);
}
return rowKeyMap;
}
private Map<String, FeeSubjectMonthStatPO> collectSubjectInfo(List<FeeSubjectMonthStatPO> stats) {
Map<String, FeeSubjectMonthStatPO> infoMap = new HashMap<>();
for (FeeSubjectMonthStatPO po : stats) {
infoMap.putIfAbsent(buildRowKey(po), po);
}
return infoMap;
}
// ------------------------------------------------------------------
// 步骤2排序
// ------------------------------------------------------------------
/** 每行当年(1-12月)累计金额(元),用于组内降序排序 */
private Map<String, BigDecimal> calcCurrentYearTotal(Map<String, Map<YearMonth, BigDecimal>> rowKeyMap,
int year) {
Map<String, BigDecimal> map = new HashMap<>();
rowKeyMap.forEach((rowKey, monthMap) -> map.put(rowKey, yearSumYuan(monthMap, year)));
return map;
}
private List<String> sortRowKeys(Set<String> rowKeys,
Map<String, FeeSubjectMonthStatPO> subjectInfoMap,
Map<String, BigDecimal> currentYearTotalMap) {
return rowKeys.stream()
.sorted(Comparator
// 第一排序键一级类别按模板固定顺序仓储配送增值物业其他未知类别沉底
.comparingInt((String k) -> FeeSubjectCategoryResolver.categoryOrder(
FeeSubjectCategoryResolver.firstOf(subjectInfoMap.get(k))))
// 仓储收入内散租固定出租模板顺序其他类别单行该键并列
.thenComparingInt(k -> FeeSubjectCategoryResolver.modeOrder(
FeeSubjectCategoryResolver.secondOf(subjectInfoMap.get(k))))
// 同类别内当年累计降序
.thenComparing(k -> currentYearTotalMap.getOrDefault(k, BigDecimal.ZERO),
Comparator.reverseOrder()))
.collect(Collectors.toList());
}
// ------------------------------------------------------------------
// 步骤3/4单行组装明细行与合计行共用subject=null 表示合计行
// ------------------------------------------------------------------
private FeeAnalysisRowDTO assembleRow(FeeSubjectMonthStatPO subject,
Map<YearMonth, BigDecimal> monthMap,
int year, Integer seq) {
FeeAnalysisRowDTO row = new FeeAnalysisRowDTO();
row.setSeq(seq);
boolean isTotalRow = (subject == null);
row.setTotalRow(isTotalRow);
if (isTotalRow) {
row.setSubjectName(TOTAL_ROW_NAME);
} else {
// 模板两级展示一级=类别二级=具体费用项
if (StrUtil.isNotBlank(subject.getSecondSubjectName())) {
// 真两级数据原样透传
row.setFirstSubjectCode(subject.getFirstSubjectCode());
row.setFirstSubjectName(subject.getFirstSubjectName());
row.setSecondSubjectCode(subject.getSecondSubjectCode());
row.setSecondSubjectName(subject.getSecondSubjectName());
} else {
// 平级数据一级=类别二级=模板文案仓储按模式拆两行其他类别各一行
row.setFirstSubjectCode(null);
row.setFirstSubjectName(FeeSubjectCategoryResolver.firstOf(subject));
String secondName = FeeSubjectCategoryResolver.secondOf(subject);
// 模板文案行由多科目合并而来编码无意义置 null科目行保留真实编码
row.setSecondSubjectCode(FeeSubjectCategoryResolver.isTemplateSecond(secondName)
? null : subject.getFirstSubjectCode());
row.setSecondSubjectName(secondName);
}
String firstName = StrUtil.nullToEmpty(row.getFirstSubjectName());
String secondName = StrUtil.nullToEmpty(row.getSecondSubjectName());
row.setSubjectName(StrUtil.isBlank(secondName) ? firstName : firstName + "-" + secondName);
}
// --- 月度列12 个月单位元无任何记录 null前端显""§1.4 空月规则---
List<MonthAmountDTO> months = new ArrayList<>(12);
for (int m = 1; m <= 12; m++) {
MonthAmountDTO mo = new MonthAmountDTO();
mo.setMonth(m);
mo.setAmount(monthMap.get(YearMonth.of(year, m)));
months.add(mo);
}
row.setMonths(months);
// --- 季度块4 金额万元基期列始终输出数值无数据=0.00---
List<FeeQuarterDTO> quarters = new ArrayList<>(4);
for (int q = 1; q <= 4; q++) {
quarters.add(buildQuarter(monthMap, year, q));
}
row.setQuarters(quarters);
// --- 累计列当年 12 个月求和() 万元需求规则5按费用科目=行汇总---
row.setAnnualTotal(FeeAnalysisCalculator.toWan(yearSumYuan(monthMap, year)));
return row;
}
/** 单个季度块:本季(万元) + 环比基期(万元) + 同比基期(万元) + 环比% + 同比% */
private FeeQuarterDTO buildQuarter(Map<YearMonth, BigDecimal> monthMap, int year, int quarter) {
FeeQuarterDTO fq = new FeeQuarterDTO();
fq.setQuarter(quarter);
// 本季金额3 个月求和() 万元舍入比率以万元舍入值为基数§1.4
BigDecimal current = FeeAnalysisCalculator.toWan(quarterSumYuan(monthMap, year, quarter));
// 环比基期Q1上年Q4Q2-Q4本年上一季
YearQuarter prev = FeeAnalysisCalculator.prevQuarter(year, quarter);
BigDecimal prevAmount = FeeAnalysisCalculator.toWan(quarterSumYuan(monthMap, prev.year, prev.quarter));
// 同比基期上年同季
YearQuarter yoy = FeeAnalysisCalculator.yoyQuarter(year, quarter);
BigDecimal yoyAmount = FeeAnalysisCalculator.toWan(quarterSumYuan(monthMap, yoy.year, yoy.quarter));
fq.setCurrentAmount(current);
fq.setPrevQuarterAmount(prevAmount);
fq.setYoyBaseAmount(yoyAmount);
fq.setQoqRate(FeeAnalysisCalculator.growthRate(current, prevAmount));
fq.setYoyRate(FeeAnalysisCalculator.growthRate(current, yoyAmount));
return fq;
}
/** 某(y,q)季度 3 个月金额求和(元);无记录的月份按 0 计(季度列始终输出数值) */
private BigDecimal quarterSumYuan(Map<YearMonth, BigDecimal> monthMap, int year, int quarter) {
BigDecimal sum = BigDecimal.ZERO;
int firstMonth = FeeAnalysisCalculator.firstMonthOfQuarter(quarter);
for (int m = firstMonth; m < firstMonth + 3; m++) {
BigDecimal v = monthMap.get(YearMonth.of(year, m));
if (v != null) {
sum = sum.add(v);
}
}
return sum;
}
/** 当年 1-12 月金额求和(元),无记录按 0 */
private BigDecimal yearSumYuan(Map<YearMonth, BigDecimal> monthMap, int year) {
BigDecimal sum = BigDecimal.ZERO;
for (int m = 1; m <= 12; m++) {
BigDecimal v = monthMap.get(YearMonth.of(year, m));
if (v != null) {
sum = sum.add(v);
}
}
return sum;
}
// ------------------------------------------------------------------
// 步骤4合计行数据源 全部行键的月份金额叠加
// ------------------------------------------------------------------
private Map<YearMonth, BigDecimal> mergeAllRows(Map<String, Map<YearMonth, BigDecimal>> rowKeyMap) {
Map<YearMonth, BigDecimal> totalMonthMap = new HashMap<>(24);
// 只有真实存在记录的月份才会进入 totalMonthMap
// 因此合计行的月列同样满足"无任何记录→null"规则
rowKeyMap.forEach((rowKey, monthMap) ->
monthMap.forEach((ym, amount) ->
totalMonthMap.merge(ym, amount, BigDecimal::add)));
return totalMonthMap;
}
// ------------------------------------------------------------------
// 步骤5占营收比需求规则6
// ------------------------------------------------------------------
/**
* 占营收比 = 每个明细行自己的当年累计万元 / 当年总累计万元 × 100
* - 口径变更2026-08-18原按一级类别汇总散租/固定出租同值 97.76%沿用原型 88.15% 的类别口径
* 仓储收入按计费模式拆成两行后无区分度业务确认改为行级占比
* - 分母 = 合计行 annualTotal(万元)总累计为 0 全部 null
* - 合计行固定 100.00
*/
private void fillRevenueRatio(List<FeeAnalysisRowDTO> rows, FeeAnalysisRowDTO totalRow) {
for (FeeAnalysisRowDTO row : rows) {
row.setRevenueRatio(
FeeAnalysisCalculator.ratioOf(row.getAnnualTotal(), totalRow.getAnnualTotal()));
}
// 合计行总累计>0 100.00否则 null与明细行除零规则一致
totalRow.setRevenueRatio(
totalRow.getAnnualTotal().compareTo(BigDecimal.ZERO) > 0
? new BigDecimal("100.00") : null);
}
}
@@ -0,0 +1,105 @@
package com.mhd.bms.domain.feeAnalysisReport.service.support;
import java.math.BigDecimal;
import java.math.RoundingMode;
/**
* 费用统计分析计算器万元换算 / 季度归属 / 环比同比基期切换 / 增长率
*
* 口径依据 §1.4
* - 月度列单位元季度/累计列单位万元/10000 四舍五入 2
* - 比率以万元舍入值为基数计算保留 2 位小数的百分数数值
* - 基期=0 且本期=0 0.00基期=0 且本期0 null前端显""基期<0 null
*/
public final class FeeAnalysisCalculator {
private static final BigDecimal HUNDRED = new BigDecimal("100");
private static final BigDecimal WAN = new BigDecimal("10000");
private FeeAnalysisCalculator() {
}
/** 元 → 万元(四舍五入2位);null 视为 0。季度列/基期列/累计列/比率基数统一使用 */
public static BigDecimal toWan(BigDecimal yuan) {
if (yuan == null) {
return BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP);
}
return yuan.divide(WAN, 2, RoundingMode.HALF_UP);
}
/** 季度归属:month(1-12) → quarter(1-4)。Q1=1-3月,Q2=4-6月,Q3=7-9月,Q4=10-12月 */
public static int quarterOf(int month) {
return (month - 1) / 3 + 1;
}
/** 季度首月:q1→1, q2→4, q3→7, q4→10 */
public static int firstMonthOfQuarter(int quarter) {
return (quarter - 1) * 3 + 1;
}
/**
* 环比基期需求规则3Q1 上年第 4 季度Q2/Q3/Q4 本年上一季度
*/
public static YearQuarter prevQuarter(int year, int quarter) {
return quarter == 1 ? new YearQuarter(year - 1, 4)
: new YearQuarter(year, quarter - 1);
}
/**
* 同比基期需求规则4上年同季度
*/
public static YearQuarter yoyQuarter(int year, int quarter) {
return new YearQuarter(year - 1, quarter);
}
/**
* 增长率(%) = (本期-基期)/基期 × 100保留2位
* 三条边界§1.4 除零规则
* 基期 = 0 且本期 = 0 0.00
* 基期 = 0 且本期 0 null无法计算前端/导出显示""对应原型 #DIV/0!
* 基期 < 0 null负基期比率无业务意义
*/
public static BigDecimal growthRate(BigDecimal current, BigDecimal base) {
if (base == null || base.compareTo(BigDecimal.ZERO) == 0) {
return isZero(current) ? zero2() : null;
}
if (base.compareTo(BigDecimal.ZERO) < 0) {
return null;
}
BigDecimal cur = current == null ? BigDecimal.ZERO : current;
return cur.subtract(base)
.divide(base, 6, RoundingMode.HALF_UP)
.multiply(HUNDRED)
.setScale(2, RoundingMode.HALF_UP);
}
/** 占比(%) = part / total × 100,保留2位;total 为 null 或 0 → null(分母为零) */
public static BigDecimal ratioOf(BigDecimal part, BigDecimal total) {
if (total == null || total.compareTo(BigDecimal.ZERO) == 0) {
return null;
}
BigDecimal p = part == null ? BigDecimal.ZERO : part;
return p.divide(total, 6, RoundingMode.HALF_UP)
.multiply(HUNDRED)
.setScale(2, RoundingMode.HALF_UP);
}
private static boolean isZero(BigDecimal v) {
return v == null || v.compareTo(BigDecimal.ZERO) == 0;
}
private static BigDecimal zero2() {
return BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP);
}
/** 年份+季度 值对象(基期表示) */
public static class YearQuarter {
public final int year;
public final int quarter;
public YearQuarter(int year, int quarter) {
this.year = year;
this.quarter = quarter;
}
}
}
@@ -0,0 +1,325 @@
package com.mhd.bms.domain.feeAnalysisReport.service.support;
import com.mhd.bms.interfaces.dto.feeAnalysisReport.FeeAnalysisReportResultDTO;
import com.mhd.bms.interfaces.dto.feeAnalysisReport.FeeAnalysisRowDTO;
import com.mhd.bms.interfaces.dto.feeAnalysisReport.FeeQuarterDTO;
import com.mhd.bms.interfaces.dto.feeAnalysisReport.MonthAmountDTO;
import lombok.extern.slf4j.Slf4j;
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.ss.util.CellRangeAddress;
import org.apache.poi.xssf.streaming.SXSSFSheet;
import org.apache.poi.xssf.streaming.SXSSFWorkbook;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.math.BigDecimal;
@Component
@Slf4j
public class FeeAnalysisExcelExporter {
private static final String[] CN_QUARTER = {"", "", "", ""};
private static final String DASH = "";
public void export(HttpServletResponse response, FeeAnalysisReportResultDTO data) throws IOException {
SXSSFWorkbook wb = new SXSSFWorkbook(500);
try {
SXSSFSheet sheet = wb.createSheet(data.getYear() + "年费用统计分析报表");
// ---- 样式准备 ----
CellStyle titleStyle = createTitleStyle(wb);
CellStyle headerNormalStyle = createHeaderStyle(wb, IndexedColors.AUTOMATIC.getIndex());
CellStyle headerRedStyle = createHeaderStyle(wb, IndexedColors.RED.getIndex());
CellStyle bannerStyle = createBannerStyle(wb);
CellStyle monthStyle = numberStyle(wb, false);
CellStyle wanStyle = numberStyle(wb, false);
CellStyle rateStyle = rateStyle(wb, false);
CellStyle textStyle = textStyle(wb, false);
CellStyle[] boldStyles = {
numberStyle(wb, true),
numberStyle(wb, true),
rateStyle(wb, true),
textStyle(wb, true)
};
int year = data.getYear();
int lastCol = 33; // AH0-based 34 A序号 + B一级项目 + C二级项目 + 31 个数据列
// ================= 1. 表头对照模板 xlsx 精确布局 =================
// Row 0 (第1行): 左上标题 "{year}年营业收入明细表"
Row row0 = sheet.createRow(0);
row0.setHeightInPoints(15.6f);
Cell titleCell = row0.createCell(1);
titleCell.setCellValue(year + "年营业收入明细表");
titleCell.setCellStyle(titleStyle);
// Row 1 (第2行): 留空占位行匹配模板行高
Row row1 = sheet.createRow(1);
row1.setHeightInPoints(15.6f);
// Row 2 & Row 3 (第34行): 表头区域
Row row2 = sheet.createRow(2);
Row row3 = sheet.createRow(3);
row2.setHeightInPoints(25.05f);
row3.setHeightInPoints(25.05f);
// A3:A4 序号合并 | B3:C4 项目合并
createMergedCell(sheet, row2, 2, 3, 0, 0, "序号", headerNormalStyle);
createMergedCell(sheet, row2, 2, 3, 1, 2, "项目", headerNormalStyle);
// D3:AH3 横幅标题 "营业收入构成明细表(HKD"
createMergedCell(sheet, row2, 2, 2, 3, lastCol, "营业收入构成明细表(HKD", bannerStyle);
// Row 3 (第4行): D~AH 具体列头
int col = 3;
for (int q = 1; q <= 4; q++) {
for (int m = (q - 1) * 3 + 1; m <= q * 3; m++) {
createCellWithStyle(row3, col++, m + "", headerNormalStyle);
}
createCellWithStyle(row3, col++, year + "年第" + CN_QUARTER[q - 1] + "季度(万)", headerRedStyle);
if (q == 1) { // Q1 块带有上年 Q4 环比基期列
createCellWithStyle(row3, col++, (year - 1) + "年第四季度(万)", headerRedStyle);
}
createCellWithStyle(row3, col++, (year - 1) + "年第" + CN_QUARTER[q - 1] + "季度(万)", headerRedStyle);
createCellWithStyle(row3, col++, "" + CN_QUARTER[q - 1] + "季度环比", headerNormalStyle);
createCellWithStyle(row3, col++, "" + CN_QUARTER[q - 1] + "季度同比", headerNormalStyle);
}
createCellWithStyle(row3, col++, "累计(万)", headerNormalStyle);
createCellWithStyle(row3, col, "占营收比(%", headerNormalStyle);
// 列宽与冻结窗格冻结前3列 + 前4表头行 D5 单元格
sheet.setColumnWidth(0, (int) (6.33 * 256)); // A列 序号
sheet.setColumnWidth(1, 14 * 256); // B列 一级项目
sheet.setColumnWidth(2, 15 * 256); // C列 二级项目
for (int c = 3; c <= lastCol; c++) {
sheet.setColumnWidth(c, 13 * 256);
}
sheet.createFreezePane(3, 4); // 关键修正冻结到 D5
// ================= 2. 数据行 =================
int rowIdx = 4; // 从第 5 (Row 4) 开始填入数据
if (data.getRows() != null) {
for (FeeAnalysisRowDTO row : data.getRows()) {
Row excelRow = sheet.createRow(rowIdx++);
excelRow.setHeightInPoints(16.95f);
writeDataRow(excelRow, row, false, monthStyle, wanStyle, rateStyle, textStyle, null);
}
}
// ================= 3. 合计行 =================
if (data.getTotalRow() != null) {
Row totalExcelRow = sheet.createRow(rowIdx);
totalExcelRow.setHeightInPoints(16.95f);
// 合计行 A:C 合并
sheet.addMergedRegion(new CellRangeAddress(rowIdx, rowIdx, 0, 2));
writeDataRow(totalExcelRow, data.getTotalRow(), true, monthStyle, wanStyle, rateStyle, textStyle, boldStyles);
}
wb.write(response.getOutputStream());
} finally {
wb.dispose();
}
}
/** 写入单元格数据 */
private void writeDataRow(Row row, FeeAnalysisRowDTO data, boolean bold,
CellStyle monthStyle, CellStyle wanStyle, CellStyle rateStyle,
CellStyle textStyle, CellStyle[] boldStyles) {
int c;
if (Boolean.TRUE.equals(data.getTotalRow())) {
// 合计行A~C 已合并只写入"合计"数据列从 D(下标 3) 开始
writeText(row, 0, data.getSubjectName() != null ? data.getSubjectName() : "合计", pick(bold, boldStyles, 3, textStyle));
// 为被合并的 B, C 列补充空单元格以挂载边框样式
createCellWithStyle(row, 1, "", pick(bold, boldStyles, 3, textStyle));
createCellWithStyle(row, 2, "", pick(bold, boldStyles, 3, textStyle));
c = 3;
} else {
// 明细行
writeText(row, 0, data.getSeq() == null ? DASH : String.valueOf(data.getSeq()), pick(bold, boldStyles, 3, textStyle));
writeText(row, 1, blankToDash(data.getFirstSubjectName()), pick(bold, boldStyles, 3, textStyle));
writeText(row, 2, blankToDash(data.getSecondSubjectName()), pick(bold, boldStyles, 3, textStyle));
c = 3;
}
for (int q = 1; q <= 4; q++) {
FeeQuarterDTO fq = (data.getQuarters() != null && data.getQuarters().size() >= q) ? data.getQuarters().get(q - 1) : new FeeQuarterDTO();
for (int m = (q - 1) * 3; m < (q - 1) * 3 + 3; m++) {
MonthAmountDTO mo = (data.getMonths() != null && data.getMonths().size() > m) ? data.getMonths().get(m) : new MonthAmountDTO();
c = writeAmount(row, c, mo.getAmount(), pick(bold, boldStyles, 0, monthStyle));
}
c = writeAmount(row, c, fq.getCurrentAmount(), pick(bold, boldStyles, 1, wanStyle));
if (q == 1) {
c = writeAmount(row, c, fq.getPrevQuarterAmount(), pick(bold, boldStyles, 1, wanStyle));
}
c = writeAmount(row, c, fq.getYoyBaseAmount(), pick(bold, boldStyles, 1, wanStyle));
c = writeAmount(row, c, fq.getQoqRate(), pick(bold, boldStyles, 2, rateStyle));
c = writeAmount(row, c, fq.getYoyRate(), pick(bold, boldStyles, 2, rateStyle));
}
c = writeAmount(row, c, data.getAnnualTotal(), pick(bold, boldStyles, 1, wanStyle));
writeAmount(row, c, data.getRevenueRatio(), pick(bold, boldStyles, 2, rateStyle));
}
private void createMergedCell(SXSSFSheet sheet, Row row, int firstRow, int lastRow, int firstCol, int lastCol, String text, CellStyle style) {
if (firstRow != lastRow || firstCol != lastCol) {
sheet.addMergedRegion(new CellRangeAddress(firstRow, lastRow, firstCol, lastCol));
}
for (int r = firstRow; r <= lastRow; r++) {
Row currRow = sheet.getRow(r);
if (currRow == null) {
currRow = sheet.createRow(r);
}
for (int c = firstCol; c <= lastCol; c++) {
Cell cell = currRow.createCell(c);
cell.setCellStyle(style);
if (r == firstRow && c == firstCol) {
cell.setCellValue(text);
}
}
}
}
private void createCellWithStyle(Row row, int col, String text, CellStyle style) {
Cell cell = row.createCell(col);
cell.setCellValue(text);
cell.setCellStyle(style);
}
private int writeAmount(Row row, int col, BigDecimal val, CellStyle style) {
Cell cell = row.createCell(col);
if (val == null) {
cell.setCellValue(DASH);
} else {
cell.setCellValue(val.doubleValue());
}
cell.setCellStyle(style);
return col + 1;
}
private String blankToDash(String val) {
return val == null || val.trim().isEmpty() ? DASH : val;
}
private int writeText(Row row, int col, String val, CellStyle style) {
Cell cell = row.createCell(col);
cell.setCellValue(val == null ? DASH : val);
cell.setCellStyle(style);
return col + 1;
}
private CellStyle pick(boolean bold, CellStyle[] boldStyles, int idx, CellStyle normal) {
return bold ? boldStyles[idx] : normal;
}
// ---------------- 样式定义 ----------------
private CellStyle createTitleStyle(Workbook wb) {
CellStyle s = wb.createCellStyle();
Font f = wb.createFont();
f.setFontName("等线");
f.setFontHeightInPoints((short) 12);
f.setBold(true);
s.setFont(f);
s.setAlignment(HorizontalAlignment.LEFT);
s.setVerticalAlignment(VerticalAlignment.CENTER);
return s;
}
private CellStyle createBannerStyle(Workbook wb) {
CellStyle s = wb.createCellStyle();
Font f = wb.createFont();
f.setFontName("等线");
f.setFontHeightInPoints((short) 18);
f.setBold(true);
s.setFont(f);
s.setAlignment(HorizontalAlignment.CENTER);
s.setVerticalAlignment(VerticalAlignment.CENTER);
s.setFillForegroundColor(IndexedColors.LIGHT_YELLOW.getIndex());
s.setFillPattern(FillPatternType.SOLID_FOREGROUND);
applyBorders(s);
return s;
}
private CellStyle createHeaderStyle(Workbook wb, short fontColor) {
CellStyle s = wb.createCellStyle();
Font f = wb.createFont();
f.setFontName("等线");
f.setFontHeightInPoints((short) 9);
f.setBold(true);
f.setColor(fontColor);
s.setFont(f);
s.setAlignment(HorizontalAlignment.CENTER);
s.setVerticalAlignment(VerticalAlignment.CENTER);
s.setWrapText(true);
s.setFillForegroundColor(IndexedColors.LIGHT_YELLOW.getIndex());
s.setFillPattern(FillPatternType.SOLID_FOREGROUND);
applyBorders(s);
return s;
}
private CellStyle numberStyle(Workbook wb, boolean bold) {
CellStyle s = wb.createCellStyle();
s.setAlignment(HorizontalAlignment.RIGHT);
s.setVerticalAlignment(VerticalAlignment.CENTER);
s.setDataFormat(wb.createDataFormat().getFormat("#,##0.00"));
applyBorders(s);
Font f = wb.createFont();
f.setFontName("等线");
f.setFontHeightInPoints((short) 11);
if (bold) {
f.setBold(true);
}
s.setFont(f);
return s;
}
private CellStyle rateStyle(Workbook wb, boolean bold) {
CellStyle s = wb.createCellStyle();
s.setAlignment(HorizontalAlignment.RIGHT);
s.setVerticalAlignment(VerticalAlignment.CENTER);
s.setDataFormat(wb.createDataFormat().getFormat("0.00\"%\""));
applyBorders(s);
Font f = wb.createFont();
f.setFontName("等线");
f.setFontHeightInPoints((short) 11);
if (bold) {
f.setBold(true);
}
s.setFont(f);
return s;
}
private CellStyle textStyle(Workbook wb, boolean bold) {
CellStyle s = wb.createCellStyle();
s.setAlignment(HorizontalAlignment.CENTER);
s.setVerticalAlignment(VerticalAlignment.CENTER);
applyBorders(s);
Font f = wb.createFont();
f.setFontName("等线");
f.setFontHeightInPoints((short) 11);
if (bold) {
f.setBold(true);
}
s.setFont(f);
return s;
}
private void applyBorders(CellStyle s) {
s.setBorderTop(BorderStyle.THIN);
s.setBorderBottom(BorderStyle.THIN);
s.setBorderLeft(BorderStyle.THIN);
s.setBorderRight(BorderStyle.THIN);
}
}
@@ -0,0 +1,130 @@
package com.mhd.bms.domain.feeAnalysisReport.service.support;
import cn.hutool.core.util.StrUtil;
import com.mhd.bms.domain.feeAnalysisReport.repository.po.FeeSubjectMonthStatPO;
import java.util.*;
/**
* 费用科目 报表一级类别 映射器
* 模板新建 Microsoft Excel 工作表.xlsx"项目"两级结构仓储收入/配送收入/增值服务收入/物业收入/其他
* 当前 billing_statement 为平级科目一级=具体费用项二级为空报表展示时由本类映射出类别
* 未来主数据两级化后二级科目非空自动透传真实一级本映射无需删除
*
* 映射键用科目名称而非编码规避 2003/TYLSCCSNCZ 双编码同名科目拆行问题
* 业务确认2026-08-18装卸费进出仓操作费暂归"增值服务收入"
*/
public final class FeeSubjectCategoryResolver {
public static final String CATEGORY_CC = "仓储收入";
public static final String CATEGORY_PS = "配送收入";
public static final String CATEGORY_ZZ = "增值服务收入";
public static final String CATEGORY_WY = "物业收入";
public static final String CATEGORY_QT = "其他";
/** 仓储收入二级(计费模式,模板口径):散租 / 固定出租 */
public static final String SECOND_SCATTERED = "散租";
public static final String SECOND_FIXED = "固定出租";
/** 配送/增值/物业/其他的模板固定二级文案(该类别所有科目合并为一行) */
public static final String SECOND_PS = "运输";
public static final String SECOND_ZZ = "贴标签/报关/海运、杂费";
public static final String SECOND_WY = "水/电费/物业";
/** 归"散租"的科目(业务确认:仅散租仓租;其余仓租科目全部归固定出租) */
private static final Set<String> SCATTERED_SUBJECTS = new HashSet<>(Collections.singletonList("散租仓租"));
/** 类别展示顺序(模板顺序),未知类别沉底 */
private static final List<String> CATEGORY_ORDER = Collections.unmodifiableList(
Arrays.asList(CATEGORY_CC, CATEGORY_PS, CATEGORY_ZZ, CATEGORY_WY, CATEGORY_QT));
/** 科目名 → 一级类别 */
private static final Map<String, String> NAME_TO_CATEGORY = new HashMap<>();
static {
// 仓储收入
map(CATEGORY_CC, "恒温仓仓租", "普通仓室内仓租", "临时仓储室内仓租",
"室外空地仓租", "散租仓租", "整租仓租");
// 配送收入
map(CATEGORY_PS, "运费");
// 增值服务收入
map(CATEGORY_ZZ, "装卸费", "进出仓操作费");
// 物业收入
map(CATEGORY_WY, "水费", "电费");
// 其余科目未命中兜底归"其他"无需登记
}
private static void map(String category, String... subjectNames) {
for (String n : subjectNames) {
NAME_TO_CATEGORY.put(n, category);
}
}
private FeeSubjectCategoryResolver() {
}
/**
* 报表一级类别
* - 数据本身已两级二级科目名非空 透传真实一级名
* - 平级数据当前现状 科目名查映射未命中归"其他"
*/
public static String firstOf(String firstSubjectName, String secondSubjectName) {
if (StrUtil.isNotBlank(secondSubjectName)) {
return StrUtil.nullToEmpty(firstSubjectName);
}
String name = StrUtil.trimToEmpty(firstSubjectName);
return name.isEmpty() ? CATEGORY_QT : NAME_TO_CATEGORY.getOrDefault(name, CATEGORY_QT);
}
public static String firstOf(FeeSubjectMonthStatPO po) {
return firstOf(po.getFirstSubjectName(), po.getSecondSubjectName());
}
/**
* 报表二级模板字面文案
* - 真两级数据 透传真实二级名
* - 仓储收入 计费模式pushMode=SCATTERED"散租"FIXED"固定出租"
* 无模式防御按科目名兜底仅散租仓租散租
* - 配送/增值/物业/其他 固定文案一行类别下所有科目金额合并
* - 完全未知的科目 "其他"
*/
public static String secondOf(FeeSubjectMonthStatPO po) {
if (StrUtil.isNotBlank(po.getSecondSubjectName())) {
return StrUtil.trimToEmpty(po.getSecondSubjectName());
}
String name = StrUtil.trimToEmpty(po.getFirstSubjectName());
String category = NAME_TO_CATEGORY.get(name);
if (CATEGORY_CC.equals(category)) {
if ("SCATTERED".equals(po.getPushMode())) {
return SECOND_SCATTERED;
}
if ("FIXED".equals(po.getPushMode())) {
return SECOND_FIXED;
}
return SCATTERED_SUBJECTS.contains(name) ? SECOND_SCATTERED : SECOND_FIXED;
}
if (CATEGORY_PS.equals(category)) return SECOND_PS;
if (CATEGORY_ZZ.equals(category)) return SECOND_ZZ;
if (CATEGORY_WY.equals(category)) return SECOND_WY;
return CATEGORY_QT; // 未映射科目统一并入"其他"一行
}
/** 仓储收入内二级排序(模板顺序:散租在前);非仓储类别返回 2(并列,落回金额降序) */
public static int modeOrder(String secondName) {
if (SECOND_SCATTERED.equals(secondName)) return 0;
if (SECOND_FIXED.equals(secondName)) return 1;
return 2;
}
/** 是否模板固定二级文案(行由多科目合并而来,行编码无意义) */
private static final Set<String> TEMPLATE_SECOND_NAMES = new HashSet<>(Arrays.asList(
SECOND_SCATTERED, SECOND_FIXED, SECOND_PS, SECOND_ZZ, SECOND_WY, CATEGORY_QT));
public static boolean isTemplateSecond(String secondName) {
return TEMPLATE_SECOND_NAMES.contains(StrUtil.nullToEmpty(secondName));
}
/** 类别展示顺序号;未知类别返回 99 沉底 */
public static int categoryOrder(String category) {
int idx = CATEGORY_ORDER.indexOf(StrUtil.nullToEmpty(category));
return idx < 0 ? 99 : idx;
}
}
@@ -0,0 +1,469 @@
package com.mhd.bms.domain.feeAnalysisReport.service.support;
import com.mhd.bms.domain.feeAnalysisReport.repository.mapper.FeeAnalysisReportMapper;
import com.mhd.bms.domain.feeAnalysisReport.repository.po.FeeSubjectMonthStatPO;
import com.mhd.bms.domain.feeAnalysisReport.service.support.FeeAnalysisCalculator.YearQuarter;
import com.mhd.bms.interfaces.dto.feeAnalysisReport.FeeAnalysisReportResultDTO;
import com.mhd.bms.interfaces.dto.feeAnalysisReport.FeeAnalysisRowDTO;
import com.mhd.bms.interfaces.dto.feeAnalysisReport.FeeQuarterDTO;
import com.mhd.bms.interfaces.dto.feeAnalysisReport.IntelligentAnalysisResultDTO;
import com.mhd.bms.interfaces.dto.feeAnalysisReport.MonthAnalysisDTO;
import com.mhd.bms.interfaces.dto.feeAnalysisReport.QuarterAnalysisDTO;
import com.mhd.bms.interfaces.dto.feeAnalysisReport.ReasonFactorDTO;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.YearMonth;
import java.util.*;
import java.util.stream.Collectors;
/**
* 智能分析规则引擎季度同比/环比归因 + 月度环比归因 + 年度汇总
*
* @author rcx
* @date 2026-08-17
*/
@Component
@Slf4j
public class IntelligentAnalysisEngine {
/** 触发阈值:|环比/同比| >= 5% 的科目行才生成分析 */
private static final BigDecimal RATE_THRESHOLD = new BigDecimal("5");
/** 科目归因取贡献绝对值 TOP5;客户归因 TOP3 */
private static final int SUBJECT_TOP_N = 5;
private static final int CUSTOMER_TOP_N = 3;
private static final BigDecimal HUNDRED = new BigDecimal("100");
@Autowired
private FeeAnalysisReportMapper feeAnalysisReportMapper;
/**
* @param report 已构建的报表结果复用 /query 数据保证口径一致
* @param year / accountExpenseType / topOrganizationId / organizationId 与报表查询同参
*/
public IntelligentAnalysisResultDTO analyze(FeeAnalysisReportResultDTO report, int year,
Integer accountExpenseType,
Long topOrganizationId, Long organizationId) {
int y = report.getYear();
// 引擎自带一份数据月度分析的1月基期=上年12月客户归因需要月粒度明细报表DTO里没带
List<FeeSubjectMonthStatPO> stats = feeAnalysisReportMapper.sumBySubjectAndMonth(
(y - 1) + "-01-01", (y + 1) + "-01-01",
accountExpenseType, topOrganizationId, organizationId);
// 科目行名 (年月 金额元)totalMap = 全科目合计
Map<String, Map<YearMonth, BigDecimal>> subjectMap = new LinkedHashMap<>();
Map<YearMonth, BigDecimal> totalMap = new HashMap<>();
for (FeeSubjectMonthStatPO po : stats) {
String name = buildSubjectName(po);
BigDecimal amt = po.getTotalAmount() == null ? BigDecimal.ZERO : po.getTotalAmount();
subjectMap.computeIfAbsent(name, k -> new HashMap<>())
.merge(YearMonth.parse(po.getStatMonth()), amt, BigDecimal::add);
totalMap.merge(YearMonth.parse(po.getStatMonth()), amt, BigDecimal::add);
}
IntelligentAnalysisResultDTO result = new IntelligentAnalysisResultDTO();
result.setYear(y);
result.setQuarterAnalyses(analyzeQuarters(y, report, subjectMap, totalMap,
accountExpenseType, topOrganizationId, organizationId));
result.setMonthAnalyses(analyzeMonths(y, subjectMap, totalMap));
result.setAnnualSummary(buildAnnualSummary(y, totalMap));
return result;
}
// ---------------- 季度分析合计行必生成 + 显著科目行 ----------------
private List<QuarterAnalysisDTO> analyzeQuarters(int y, FeeAnalysisReportResultDTO report,
Map<String, Map<YearMonth, BigDecimal>> subjectMap,
Map<YearMonth, BigDecimal> totalMap,
Integer type, Long topOrgId, Long orgId) {
List<QuarterAnalysisDTO> list = new ArrayList<>();
for (int q = 1; q <= 4; q++) {
// 每季度两个维度QOQ 环比 / YOY 同比合计行必生成
list.add(analyzeQuarterTotal(y, q, "QOQ", totalMap, subjectMap, type, topOrgId, orgId));
list.add(analyzeQuarterTotal(y, q, "YOY", totalMap, subjectMap, type, topOrgId, orgId));
// 显著科目行||>=5%
for (FeeAnalysisRowDTO row : report.getRows()) {
BigDecimal qoq = rateOfRow(row, q, "QOQ");
BigDecimal yoy = rateOfRow(row, q, "YOY");
if (needAnalyze(qoq)) list.add(analyzeSubjectRow(y, q, "QOQ", qoq, row, type, topOrgId, orgId));
if (needAnalyze(yoy)) list.add(analyzeSubjectRow(y, q, "YOY", yoy, row, type, topOrgId, orgId));
}
}
return list;
}
private boolean needAnalyze(BigDecimal rate) {
return rate != null && rate.abs().compareTo(RATE_THRESHOLD) >= 0;
}
private BigDecimal rateOfRow(FeeAnalysisRowDTO row, int quarter, String rateType) {
return "QOQ".equals(rateType)
? row.getQuarters().get(quarter - 1).getQoqRate()
: row.getQuarters().get(quarter - 1).getYoyRate();
}
/** 合计行季度归因:科目TOP5贡献 + 客户TOP3贡献 + 模板文案 */
private QuarterAnalysisDTO analyzeQuarterTotal(int y, int q, String rateType,
Map<YearMonth, BigDecimal> totalMap,
Map<String, Map<YearMonth, BigDecimal>> subjectMap,
Integer type, Long topOrgId, Long orgId) {
YearQuarter cur = new YearQuarter(y, q);
YearQuarter base = "QOQ".equals(rateType)
? FeeAnalysisCalculator.prevQuarter(y, q) : FeeAnalysisCalculator.yoyQuarter(y, q);
BigDecimal current = FeeAnalysisCalculator.toWan(quarterSum(totalMap, cur));
BigDecimal baseAmount = FeeAnalysisCalculator.toWan(quarterSum(totalMap, base));
BigDecimal rate = FeeAnalysisCalculator.growthRate(current, baseAmount);
BigDecimal delta = current.subtract(baseAmount);
// 科目因子每科目两期金额差 贡献度 |delta| 降序 TOP5
Map<String, BigDecimal> curByFactor = new HashMap<>();
Map<String, BigDecimal> baseByFactor = new HashMap<>();
subjectMap.forEach((name, mm) -> {
curByFactor.put(name, quarterSum(mm, cur));
baseByFactor.put(name, quarterSum(mm, base));
});
List<ReasonFactorDTO> reasons = decompose(curByFactor, baseByFactor, delta, true);
// 客户因子仅合计行一次SQL取两期区间按客户拆季度金额
List<ReasonFactorDTO> customerReasons = customerTopN(cur, base, type, topOrgId, orgId, delta);
QuarterAnalysisDTO dto = new QuarterAnalysisDTO();
dto.setQuarter(q);
dto.setScope("TOTAL");
dto.setSubjectName("合计");
dto.setRateType(rateType);
dto.setCurrentAmount(current);
dto.setBaseAmount(baseAmount);
dto.setRate(rate);
dto.setDeltaAmount(delta);
dto.setLevel(levelOf(rate, baseAmount, current));
dto.setReasons(reasons);
dto.setCustomerReasons(customerReasons);
dto.setSummary(buildQuarterTotalSummary(y, q, rateType, current, baseAmount, rate, delta,
reasons, customerReasons));
return dto;
}
/**
* 科目行季度归因因子 = 该科目下的客户谁带动了该科目增减
* 金额直接取报表行 FeeQuarterDTO万元口径 /query 完全一致
*/
private QuarterAnalysisDTO analyzeSubjectRow(int y, int q, String rateType, BigDecimal rate,
FeeAnalysisRowDTO row,
Integer type, Long topOrgId, Long orgId) {
YearQuarter cur = new YearQuarter(y, q);
YearQuarter base = "QOQ".equals(rateType)
? FeeAnalysisCalculator.prevQuarter(y, q) : FeeAnalysisCalculator.yoyQuarter(y, q);
FeeQuarterDTO quarterDto = row.getQuarters().get(q - 1);
BigDecimal current = quarterDto.getCurrentAmount();
BigDecimal baseAmt = "QOQ".equals(rateType)
? quarterDto.getPrevQuarterAmount() : quarterDto.getYoyBaseAmount();
BigDecimal delta = current.subtract(baseAmt);
String level = levelOf(rate, baseAmt, current);
List<ReasonFactorDTO> reasons =
customerTopN(cur, base, type, topOrgId, orgId, delta, row.getSubjectName());
QuarterAnalysisDTO dto = new QuarterAnalysisDTO();
dto.setQuarter(q);
dto.setScope("SUBJECT");
dto.setSubjectName(row.getSubjectName());
dto.setRateType(rateType);
dto.setCurrentAmount(current);
dto.setBaseAmount(baseAmt);
dto.setRate(rate);
dto.setDeltaAmount(delta);
dto.setLevel(level);
dto.setReasons(reasons);
dto.setSummary(String.format(
"%d年第%d季度【%s】收入%s万元,%s%s,主要贡献客户:%s。",
y, q, row.getSubjectName(), current.toPlainString(),
"QOQ".equals(rateType) ? (q == 1 ? "环比(对上年第4季度)" : "环比") : "同比",
changeDesc(level, rate, delta),
joinTopFactors(dto.getReasons())));
return dto;
}
// ---------------- 月度分析合计行月环比1月基期=上年12月 ----------------
private List<MonthAnalysisDTO> analyzeMonths(int y,
Map<String, Map<YearMonth, BigDecimal>> subjectMap,
Map<YearMonth, BigDecimal> totalMap) {
List<MonthAnalysisDTO> list = new ArrayList<>();
for (int m = 1; m <= 12; m++) {
YearMonth curYm = YearMonth.of(y, m);
YearMonth prevYm = (m == 1) ? YearMonth.of(y - 1, 12) : YearMonth.of(y, m - 1);
BigDecimal cur = totalMap.get(curYm);
BigDecimal prev = totalMap.get(prevYm);
if (cur == null && prev == null) {
continue; // 本月与上月都无记录不生成
}
BigDecimal curV = cur == null ? BigDecimal.ZERO : cur;
BigDecimal prevV = prev == null ? BigDecimal.ZERO : prev;
BigDecimal momRate = FeeAnalysisCalculator.growthRate(curV, prevV);
BigDecimal delta = curV.subtract(prevV);
Map<String, BigDecimal> curBy = new HashMap<>();
Map<String, BigDecimal> baseBy = new HashMap<>();
subjectMap.forEach((name, mm) -> {
curBy.put(name, mm.getOrDefault(curYm, BigDecimal.ZERO));
baseBy.put(name, mm.getOrDefault(prevYm, BigDecimal.ZERO));
});
List<ReasonFactorDTO> reasons = decompose(curBy, baseBy, delta, false);
MonthAnalysisDTO dto = new MonthAnalysisDTO();
dto.setMonth(m);
dto.setScope("TOTAL");
dto.setCurrentAmount(curV);
dto.setPrevMonthAmount(prevV);
dto.setMomRate(momRate);
String level = levelOf(momRate, prevV, curV);
dto.setLevel(level);
dto.setReasons(reasons);
ReasonFactorDTO top = reasons.isEmpty() ? null : reasons.get(0);
String change = "新增".equals(level)
? "新增(上月无数据)"
: String.format("%s%s", level,
momRate == null ? "" : String.format("%.2f%%", momRate.abs()));
dto.setSummary(String.format(
"%d月收入合计%s元,环比%s月%s,主要由于【%s】%s%s元%s。",
m, curV.toPlainString(), prevYm.getMonthValue(), change,
top == null ? "" : top.getFactor(),
top == null || top.getDeltaAmount().signum() >= 0 ? "增加" : "减少",
top == null ? "0" : top.getDeltaAmount().abs().toPlainString(),
(top == null || top.getContribution() == null) ? ""
: String.format("(贡献%.2f%%", top.getContribution())));
list.add(dto);
}
return list;
}
// ---------------- 年度汇总 ----------------
private String buildAnnualSummary(int y, Map<YearMonth, BigDecimal> totalMap) {
int maxMonth = 0;
BigDecimal annual = BigDecimal.ZERO;
for (int m = 1; m <= 12; m++) {
BigDecimal v = totalMap.get(YearMonth.of(y, m));
if (v != null) {
maxMonth = m;
annual = annual.add(v);
}
}
if (maxMonth == 0) {
return y + "年暂无收入数据。";
}
// 上年同期累计1~maxMonth保证同比可比
BigDecimal lastYearSamePeriod = BigDecimal.ZERO;
for (int m = 1; m <= maxMonth; m++) {
BigDecimal v = totalMap.get(YearMonth.of(y - 1, m));
if (v != null) {
lastYearSamePeriod = lastYearSamePeriod.add(v);
}
}
BigDecimal yoy = FeeAnalysisCalculator.growthRate(
FeeAnalysisCalculator.toWan(annual), FeeAnalysisCalculator.toWan(lastYearSamePeriod));
return String.format("截至%d月,%d年累计收入%s万元;同比%s。",
maxMonth, y, FeeAnalysisCalculator.toWan(annual).toPlainString(),
yoy == null ? "无同期数据"
: String.format("%s%.2f%%", yoy.signum() >= 0 ? "增长" : "下降", yoy.abs()));
}
// ---------------- 通用贡献度分解 / 等级 / 客户归因 ----------------
/**
* 贡献度分解factorDelta totalDelta 必须同口径后再相除
* isWan=true季度分析时分子先按万元舍入与分母万元一致 修复"元/万元"混算导致贡献度放大 10000 倍的问题
* 舍入后为 0 的因子不输出避免"增加0.00万元,贡献xx%"的噪音行
* totalDelta 0 null contribution 输出 null |factorDelta| 降序取 TOP N
*/
private List<ReasonFactorDTO> decompose(Map<String, BigDecimal> curBy, Map<String, BigDecimal> baseBy,
BigDecimal totalDelta, boolean isWan) {
BigDecimal total = totalDelta == null ? BigDecimal.ZERO : totalDelta;
List<ReasonFactorDTO> list = new ArrayList<>();
for (String factor : curBy.keySet()) {
BigDecimal c = curBy.getOrDefault(factor, BigDecimal.ZERO);
BigDecimal b = baseBy.getOrDefault(factor, BigDecimal.ZERO);
BigDecimal d = isWan ? FeeAnalysisCalculator.toWan(c.subtract(b)) : c.subtract(b);
if (d.signum() == 0) {
continue;
}
ReasonFactorDTO r = new ReasonFactorDTO();
r.setFactor(factor);
r.setDeltaAmount(d);
r.setContribution(total.signum() == 0 ? null
: d.divide(total, 6, RoundingMode.HALF_UP)
.multiply(HUNDRED).setScale(2, RoundingMode.HALF_UP));
r.setDescription(String.format("%s收入%s%s%s%s",
factor, d.signum() >= 0 ? "增加" : "减少",
d.abs().toPlainString(), isWan ? "万元" : "",
r.getContribution() == null ? ""
: String.format(",对整体变动贡献%s%%%s", r.getContribution().toPlainString(),
d.signum() == total.signum() ? "正向推动" : "反向抵消")));
list.add(r);
}
return list.stream()
.sorted(Comparator.comparing((ReasonFactorDTO r) -> r.getDeltaAmount().abs()).reversed())
.limit(SUBJECT_TOP_N)
.collect(Collectors.toList());
}
/** 客户归因(合计行):全部科目的客户 TOP3 */
private List<ReasonFactorDTO> customerTopN(YearQuarter cur, YearQuarter base,
Integer type, Long topOrgId, Long orgId,
BigDecimal totalDelta) {
return customerTopN(cur, base, type, topOrgId, orgId, totalDelta, null);
}
/**
* 客户归因一次 SQL 取两期月份并集区间按客户分别汇总两期金额失败降级为空不影响主分析
* subjectNameFilter 非空时仅统计该科目下的客户科目行归因否则统计全部科目合计行归因
* 区间修正early "年/季更早"的一期 原实现只比较年份Q2~Q4 同年环比时基期季度被排除在区间外
* 导致客户基期恒为 0delta 恒等于本期金额
*/
private List<ReasonFactorDTO> customerTopN(YearQuarter cur, YearQuarter base,
Integer type, Long topOrgId, Long orgId,
BigDecimal totalDelta, String subjectNameFilter) {
try {
YearQuarter early = isBefore(base, cur) ? base : cur;
YearQuarter late = isBefore(base, cur) ? cur : base;
String begin = String.format("%d-%02d-01", early.year,
FeeAnalysisCalculator.firstMonthOfQuarter(early.quarter));
String end = late.quarter == 4 ? (late.year + 1) + "-01-01"
: String.format("%d-%02d-01", late.year,
FeeAnalysisCalculator.firstMonthOfQuarter(late.quarter + 1));
List<FeeSubjectMonthStatPO> rows = feeAnalysisReportMapper.sumBySubjectAndCustomer(
begin, end, type, topOrgId, orgId);
Map<String, BigDecimal> curByCust = new HashMap<>();
Map<String, BigDecimal> baseByCust = new HashMap<>();
for (FeeSubjectMonthStatPO po : rows) {
if (subjectNameFilter != null && !subjectNameFilter.equals(buildSubjectName(po))) {
continue;
}
YearMonth ym = YearMonth.parse(po.getStatMonth());
BigDecimal amt = po.getTotalAmount() == null ? BigDecimal.ZERO : po.getTotalAmount();
String cust = po.getCustomerName() == null ? "未知客户" : po.getCustomerName();
if (ym.getYear() == cur.year
&& FeeAnalysisCalculator.quarterOf(ym.getMonthValue()) == cur.quarter) {
curByCust.merge(cust, amt, BigDecimal::add);
} else if (ym.getYear() == base.year
&& FeeAnalysisCalculator.quarterOf(ym.getMonthValue()) == base.quarter) {
baseByCust.merge(cust, amt, BigDecimal::add);
}
}
return decompose(curByCust, baseByCust, totalDelta, true).stream()
.limit(CUSTOMER_TOP_N).collect(Collectors.toList());
} catch (Exception e) {
log.warn("客户归因查询失败,跳过(不影响主分析): {}", e.getMessage());
return Collections.emptyList();
}
}
private boolean isBefore(YearQuarter a, YearQuarter b) {
return a.year < b.year || (a.year == b.year && a.quarter < b.quarter);
}
/**
* 等级判定§7.2 规则
* 率非空50 大幅增长 / 5 增长 / -50 大幅下降 / -5 下降 / 其余 持平
* 率为 null基期<0 数据异常基期=0 且本期0 新增上期无数据比率不可算不再误标"持平"
*/
private String levelOf(BigDecimal rate, BigDecimal base, BigDecimal current) {
if (rate == null) {
if (base != null && base.signum() < 0) {
return "数据异常";
}
return (current != null && current.signum() != 0) ? "新增" : "持平";
}
if (rate.compareTo(new BigDecimal("50")) >= 0) return "大幅增长";
if (rate.compareTo(RATE_THRESHOLD) >= 0) return "增长";
if (rate.compareTo(new BigDecimal("-50")) <= 0) return "大幅下降";
if (rate.compareTo(RATE_THRESHOLD.negate()) <= 0) return "下降";
return "持平";
}
/** 变动描述:常规"等级+比率+增减额";新增(上期无数据)与基期为负两种特殊口径单独措辞 */
private String changeDesc(String level, BigDecimal rate, BigDecimal delta) {
if ("新增".equals(level)) {
return String.format("新增(上期无数据,%s%s万元)",
delta.signum() >= 0 ? "增加" : "减少", delta.abs().toPlainString());
}
if ("数据异常".equals(level)) {
return "数据异常(基期为负,比率无意义)";
}
return String.format("%s%s%s%s万元)", level,
rate == null ? "" : String.format("%.2f%%", rate.abs()),
delta.signum() >= 0 ? "增加" : "减少", delta.abs().toPlainString());
}
private String buildQuarterTotalSummary(int y, int q, String rateType,
BigDecimal current, BigDecimal base, BigDecimal rate, BigDecimal delta,
List<ReasonFactorDTO> reasons,
List<ReasonFactorDTO> customerReasons) {
String baseDesc = "YOY".equals(rateType) ? "同比"
: (q == 1 ? "环比(对上年第4季度)" : "环比");
StringBuilder sb = new StringBuilder();
sb.append(String.format("%d年第%d季度收入合计%s万元,%s%s",
y, q, current.toPlainString(), baseDesc,
changeDesc(levelOf(rate, base, current), rate, delta)));
if (reasons != null && !reasons.isEmpty()) {
ReasonFactorDTO top = reasons.get(0);
sb.append(String.format(",主要由【%s】驱动(%s%s万元,贡献%s%%)",
top.getFactor(),
top.getDeltaAmount().signum() >= 0 ? "" : "",
top.getDeltaAmount().abs().toPlainString(),
top.getContribution() == null ? "" : top.getContribution().abs().toPlainString()));
}
if (customerReasons != null && !customerReasons.isEmpty()) {
ReasonFactorDTO c = customerReasons.get(0);
sb.append(String.format(";其中客户【%s】%s%s万元",
c.getFactor(), c.getDeltaAmount().signum() >= 0 ? "增加" : "减少",
c.getDeltaAmount().abs().toPlainString()));
}
sb.append("");
return sb.toString();
}
private String joinTopFactors(List<ReasonFactorDTO> reasons) {
if (reasons == null || reasons.isEmpty()) {
return "无显著变动客户";
}
return reasons.stream()
.limit(2)
.map(r -> String.format("%s(%s%%)",
r.getFactor(), r.getContribution() == null ? "" : r.getContribution().toPlainString()))
.collect(Collectors.joining(""));
}
/**
* 科目行名 FeeAnalysisReportDomainService 报表行 subjectName 一致客户归因过滤依赖相等
* 二级非空 "一级-二级"平级数据 "类别-科目名" FeeSubjectCategoryResolver 映射
*/
private String buildSubjectName(FeeSubjectMonthStatPO po) {
String first = po.getFirstSubjectName() == null ? "" : po.getFirstSubjectName().trim();
String second = po.getSecondSubjectName() == null ? "" : po.getSecondSubjectName().trim();
if (first.isEmpty() && second.isEmpty()) {
return "";
}
if (!second.isEmpty()) {
return first + "-" + second;
}
return FeeSubjectCategoryResolver.firstOf(po) + "-" + FeeSubjectCategoryResolver.secondOf(po);
}
private BigDecimal quarterSum(Map<YearMonth, BigDecimal> monthMap, YearQuarter yq) {
BigDecimal sum = BigDecimal.ZERO;
int firstMonth = FeeAnalysisCalculator.firstMonthOfQuarter(yq.quarter);
for (int m = firstMonth; m < firstMonth + 3; m++) {
BigDecimal v = monthMap.get(YearMonth.of(yq.year, m));
if (v != null) {
sum = sum.add(v);
}
}
return sum;
}
}
@@ -145,4 +145,6 @@ public class BillDetailDTO extends BaseVOEntity{
private BigDecimal billingNum;
@ApiModelProperty(value = "计费单价")
private BigDecimal billingUnitPrice;
@ApiModelProperty(value = "流水备注")
private String billingRemark;
}
@@ -0,0 +1,24 @@
// ---------- FeeAnalysisReportQueryDTO.java ----------
package com.mhd.bms.interfaces.dto.feeAnalysisReport;
import com.mhd.common.core.web.domain.BaseVOEntity;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
/** 费用统计分析报表查询入参 */
@Data
public class FeeAnalysisReportQueryDTO extends BaseVOEntity {
private static final long serialVersionUID = 1L;
@ApiModelProperty("统计年份,默认当前年(1900~2100)")
private Integer year;
@ApiModelProperty("收支方向:1-收入(默认) 2-支出")
private Integer accountExpenseType;
@ApiModelProperty("组织ID(可选;越权传值将被后端强制纠正,见 §6.5)")
private Long organizationId;
@ApiModelProperty("一级组织ID(后端从登录态注入,前端传值无效)")
private Long topOrganizationId;
}
@@ -0,0 +1,21 @@
// ---------- FeeAnalysisReportResultDTO.java ----------
package com.mhd.bms.interfaces.dto.feeAnalysisReport;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.util.List;
/** 报表整体出参 */
@Data
public class FeeAnalysisReportResultDTO {
@ApiModelProperty("统计年份")
private Integer year;
@ApiModelProperty("收支方向 1收入 2支出")
private Integer accountExpenseType;
@ApiModelProperty("左下角说明(本期固定空串)")
private String remark;
@ApiModelProperty("明细行(费用科目行)")
private List<FeeAnalysisRowDTO> rows;
@ApiModelProperty("合计行")
private FeeAnalysisRowDTO totalRow;
}
@@ -0,0 +1,34 @@
// ---------- FeeAnalysisRowDTO.java ----------
package com.mhd.bms.interfaces.dto.feeAnalysisReport;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.math.BigDecimal;
import java.util.List;
/** 报表行(明细行与合计行同构) */
@Data
public class FeeAnalysisRowDTO {
@ApiModelProperty("序号(合计行为 null")
private Integer seq;
@ApiModelProperty("一级科目code")
private String firstSubjectCode;
@ApiModelProperty("一级科目名称(=费用类别)")
private String firstSubjectName;
@ApiModelProperty("二级科目code")
private String secondSubjectCode;
@ApiModelProperty("二级科目名称")
private String secondSubjectName;
@ApiModelProperty("行名:一级-二级(二级空时=一级名;合计行=“合计”)")
private String subjectName;
@ApiModelProperty("12个月金额,单位元;null=该月无记录(显示“—”)")
private List<MonthAmountDTO> months;
@ApiModelProperty("4个季度块")
private List<FeeQuarterDTO> quarters;
@ApiModelProperty("累计(万元)")
private BigDecimal annualTotal;
@ApiModelProperty("占营收比(%);null=分母为0")
private BigDecimal revenueRatio;
@ApiModelProperty("是否合计行(导出加粗用)")
private Boolean totalRow;
}
@@ -0,0 +1,23 @@
// ---------- FeeQuarterDTO.java ----------
package com.mhd.bms.interfaces.dto.feeAnalysisReport;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.math.BigDecimal;
/** 季度块(金额单位万元,比率单位%) */
@Data
public class FeeQuarterDTO {
@ApiModelProperty("季度 1-4")
private Integer quarter;
@ApiModelProperty("本季总额(万元)")
private BigDecimal currentAmount;
@ApiModelProperty("环比基期(万元):Q1=上年Q4,Q2-Q4=本年上一季")
private BigDecimal prevQuarterAmount;
@ApiModelProperty("同比基期=上年同季(万元)")
private BigDecimal yoyBaseAmount;
@ApiModelProperty("环比%null=基期为0或负")
private BigDecimal qoqRate;
@ApiModelProperty("同比%null=基期为0或负")
private BigDecimal yoyRate;
}
@@ -0,0 +1,19 @@
// ---------- IntelligentAnalysisResultDTO.java ----------
package com.mhd.bms.interfaces.dto.feeAnalysisReport;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.util.List;
/** 智能分析出参 */
@Data
public class IntelligentAnalysisResultDTO {
@ApiModelProperty("年份")
private Integer year;
@ApiModelProperty("季度归因列表(合计行必生成 + |率|>=5%科目行,各含环比/同比)")
private List<QuarterAnalysisDTO> quarterAnalyses;
@ApiModelProperty("月度归因列表(合计行月环比,1月基期=上年12月)")
private List<MonthAnalysisDTO> monthAnalyses;
@ApiModelProperty("年度汇总文案")
private String annualSummary;
}
@@ -0,0 +1,15 @@
// ---------- MonthAmountDTO.java ----------
package com.mhd.bms.interfaces.dto.feeAnalysisReport;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.math.BigDecimal;
/** 月度单元格 */
@Data
public class MonthAmountDTO {
@ApiModelProperty("月份 1-12")
private Integer month;
@ApiModelProperty("金额(元);null=无记录")
private BigDecimal amount;
}
@@ -0,0 +1,28 @@
// ---------- MonthAnalysisDTO.java ----------
package com.mhd.bms.interfaces.dto.feeAnalysisReport;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.math.BigDecimal;
import java.util.List;
/** 月度归因项(单位:元,与月列一致) */
@Data
public class MonthAnalysisDTO {
@ApiModelProperty("月份 1-12")
private Integer month;
@ApiModelProperty("TOTAL=合计行")
private String scope;
@ApiModelProperty("本月金额(元)")
private BigDecimal currentAmount;
@ApiModelProperty("上月金额(元);1月=上年12月")
private BigDecimal prevMonthAmount;
@ApiModelProperty("环比%(元基数)")
private BigDecimal momRate;
@ApiModelProperty("等级")
private String level;
@ApiModelProperty("科目贡献因子 TOP5")
private List<ReasonFactorDTO> reasons;
@ApiModelProperty("智能分析结论文案")
private String summary;
}
@@ -0,0 +1,36 @@
// ---------- QuarterAnalysisDTO.java ----------
package com.mhd.bms.interfaces.dto.feeAnalysisReport;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.math.BigDecimal;
import java.util.List;
/** 季度归因项 */
@Data
public class QuarterAnalysisDTO {
@ApiModelProperty("季度 1-4")
private Integer quarter;
@ApiModelProperty("TOTAL=合计行 / SUBJECT=科目行")
private String scope;
@ApiModelProperty("科目名(合计行=“合计”)")
private String subjectName;
@ApiModelProperty("QOQ=环比 / YOY=同比")
private String rateType;
@ApiModelProperty("本期(万元)")
private BigDecimal currentAmount;
@ApiModelProperty("基期(万元)")
private BigDecimal baseAmount;
@ApiModelProperty("比率(%")
private BigDecimal rate;
@ApiModelProperty("等级:大幅增长/增长/持平/下降/大幅下降")
private String level;
@ApiModelProperty("增减额(万元)")
private BigDecimal deltaAmount;
@ApiModelProperty("科目贡献因子 TOP5")
private List<ReasonFactorDTO> reasons;
@ApiModelProperty("客户贡献因子 TOP3(仅合计行)")
private List<ReasonFactorDTO> customerReasons;
@ApiModelProperty("智能分析结论文案")
private String summary;
}
@@ -0,0 +1,19 @@
// ---------- ReasonFactorDTO.java ----------
package com.mhd.bms.interfaces.dto.feeAnalysisReport;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.math.BigDecimal;
/** 贡献因子 */
@Data
public class ReasonFactorDTO {
@ApiModelProperty("因子名(科目行名/客户名)")
private String factor;
@ApiModelProperty("变动额(季度分析=万元 / 月度分析=元)")
private BigDecimal deltaAmount;
@ApiModelProperty("贡献度(%=变动额/合计变动额×100);null=合计变动为0")
private BigDecimal contribution;
@ApiModelProperty("单因子描述文案")
private String description;
}
@@ -52,11 +52,14 @@ public class BmsInvoiceListApi extends BaseController {
@ApiOperation(value = "销项发票表-分页列表查询", notes = "销项发票表-分页列表查询")
@RequestMapping(value = "/queryList")
public Result<IPage<BmsInvoiceListVO>> queryList(BmsInvoiceListQueryDTO bmsInvoiceListQueryDTO,
@RequestParam(name="pageNo", defaultValue="1") Integer pageNo,
@RequestParam(name="pageNo", required=false) Integer pageNo,
@RequestParam(name="pageNum", required=false) Integer pageNum,
@RequestParam(name="pageSize", defaultValue="10") Integer pageSize,
HttpServletRequest req) {
// 前端分页参数名不统一pageNum / pageNo两个都兼容优先 pageNo
int currentPage = pageNo != null ? pageNo : (pageNum != null ? pageNum : 1);
Result<IPage<BmsInvoiceListVO>> result = new Result<IPage<BmsInvoiceListVO>>();
Page<BmsInvoiceListVO> pageList = new Page<BmsInvoiceListVO>(pageNo,pageSize);
Page<BmsInvoiceListVO> pageList = new Page<BmsInvoiceListVO>(currentPage,pageSize);
pageList = bmsInvoiceListService.queryList(pageList, bmsInvoiceListQueryDTO);
result.setResult(pageList);
result.setSuccess(true);
@@ -0,0 +1,56 @@
package com.mhd.bms.interfaces.facadeApi;
import com.mhd.bms.application.server.feeAnalysisReport.FeeAnalysisReportApplicationService;
import com.mhd.bms.interfaces.dto.feeAnalysisReport.FeeAnalysisReportQueryDTO;
import com.mhd.common.core.web.controller.BaseController;
import com.mhd.common.core.web.domain.AjaxResult;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* 费用统计分析报表Api
*
* @author rcx
* @date 2026-08-17
*/
@Api(tags = "费用统计分析报表")
@RestController
@RequestMapping("/feeAnalysisReportApi")
public class FeeAnalysisReportApi extends BaseController {
@Autowired
private FeeAnalysisReportApplicationService feeAnalysisReportApplicationService;
/** 费用统计分析报表查询(年份可选,默认当前年) */
@ApiOperation("费用统计分析报表查询")
@PostMapping("/query")
public AjaxResult query(@RequestBody FeeAnalysisReportQueryDTO queryDTO) {
return AjaxResult.success("操作成功", feeAnalysisReportApplicationService.query(queryDTO));
}
/** 智能分析:季度同比/环比增减原因 + 月度增减原因 */
@ApiOperation("费用统计智能分析")
@GetMapping("/intelligentAnalysis")
public AjaxResult intelligentAnalysis(FeeAnalysisReportQueryDTO queryDTO) {
return AjaxResult.success("操作成功", feeAnalysisReportApplicationService.intelligentAnalysis(queryDTO));
}
/** 导出(响应流输出 xlsx;响应头/文件名在 ApplicationService 里设置,仿 BillManageApi.exportReceivableList */
@ApiOperation("导出费用统计分析报表")
@GetMapping(value = "/export", produces = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
public void export(FeeAnalysisReportQueryDTO queryDTO, HttpServletResponse response) throws IOException {
feeAnalysisReportApplicationService.export(queryDTO, response);
}
/** 年份下拉 */
@ApiOperation("费用统计年份列表")
@GetMapping("/years")
public AjaxResult years(FeeAnalysisReportQueryDTO queryDTO) {
return AjaxResult.success("操作成功", feeAnalysisReportApplicationService.selectYears(queryDTO));
}
}
+4 -4
View File
@@ -18,15 +18,15 @@ spring:
discovery:
# server-addr: 127.0.0.1:8848
# 线上测试环境配置 容器名+端口号
# server-addr: 10.33.0.129:6010
server-addr: 10.102.192.30:6848
server-addr: 10.33.0.129:6010
# server-addr: 10.102.192.30:6848
username: nacos
password: manhuoda@2023
config:
# server-addr: 127.0.0.1:8848
# 线上测试环境配置 容器名+端口号
# server-addr: 10.33.0.129:6010
server-addr: 10.102.192.30:6848
server-addr: 10.33.0.129:6010
# server-addr: 10.102.192.30:6848
username: nacos
password: manhuoda@2023
group: DEFAULT_GROUP # 默认分组就是DEFAULT_GROUP,如果使用默认分组可以不配置
@@ -18,7 +18,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
bs.first_subject_code,
bs.first_subject_name,
bs.second_subject_code,
bs.second_subject_name
bs.second_subject_name,
bs.billing_remark
FROM
bill_detail bd
LEFT JOIN billing_statement bs ON bd.billing_statement_id = bs.billing_statement_id
@@ -47,7 +49,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
AND bill_manage_id = #{billManageId} GROUP BY INVOICE_ITEM,INVOICE_ITEM_NAME,FEE_TYPE
</select>
<select id="getTaxRate" resultType="java.lang.Double" parameterType="java.lang.String">
<!-- 科目编码存在重复(主数据未做唯一校验,如1003同时有"运输处理费/报关费3"),按编码+名称取唯一一条 -->
<select id="getTaxRate" resultType="java.lang.Double">
SELECT
rate
FROM
@@ -55,7 +58,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
WHERE
del_flag = 1
AND SUBJECT_CODE = #{code}
AND SUBJECT_NAME = #{name}
</select>
</mapper>
@@ -170,7 +170,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
</select>
<select id="getShipperId" resultType="java.lang.Long">
SELECT SHIPPER_ID FROM NGWL_TEST_USER."USER_SHIPPER" where DEL_FLAG = '1' and user_id = #{userId} and organization_id = #{organizationId} limit 1
SELECT SHIPPER_ID FROM NGWL_TEST_USER."USER_SHIPPER" where DEL_FLAG = '1' and user_id = #{userId} limit 1
</select>
</mapper>
@@ -10,7 +10,7 @@
where a.is_delete = 0
<include refid="common_where"></include>
group by a.id
order by a.create_time desc
order by a.create_time desc, a.id desc
</select>
<select id="getById" parameterType="java.lang.String" resultType="com.mhd.bms.domain.bmsInvoiceList.repository.vo.BmsInvoiceListDetailsVO">
select
@@ -0,0 +1,77 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.mhd.bms.domain.feeAnalysisReport.repository.mapper.FeeAnalysisReportMapper">
<!-- 公共统计口径:软删/已审核已出账/未红冲/收支方向/组织(组织条件由 §6.5 fillAuthScope 决定是否传入) -->
<sql id="common_stat_where">
bs.DEL_FLAG = 1
AND bs.BILLING_STATE IN (2, 3)
AND bs.RED_FLUSH_STATE = 1
AND bs.ACCOUNT_EXPENSE_TYPE = #{accountExpenseType}
<if test="topOrganizationId != null">
AND bs.TOP_ORGANIZATION_ID = #{topOrganizationId}
</if>
<if test="organizationId != null">
AND bs.ORGANIZATION_ID = #{organizationId}
</if>
</sql>
<!-- 科目×年月汇总:统计区间跨两个年度(本年+上年),季度组装在Java侧完成 -->
<select id="sumBySubjectAndMonth"
resultType="com.mhd.bms.domain.feeAnalysisReport.repository.po.FeeSubjectMonthStatPO">
SELECT
bs.FIRST_SUBJECT_CODE AS firstSubjectCode,
bs.FIRST_SUBJECT_NAME AS firstSubjectName,
bs.SECOND_SUBJECT_CODE AS secondSubjectCode,
bs.SECOND_SUBJECT_NAME AS secondSubjectName,
SUBSTR(TO_CHAR(bs.BUSINESS_DATE, 'YYYY-MM'), 1, 7) AS statMonth,
SUM(bs.BILLING_AMOUNT) AS totalAmount,
CASE WHEN bd.DOCUMENT_FORM_JSON LIKE '%"lease_area":0,%' THEN 'SCATTERED' ELSE 'FIXED' END AS pushMode
FROM BILLING_STATEMENT bs
LEFT JOIN BUSINESS_DOCUMENT bd ON bd.BUSINESS_DOCUMENT_ID = bs.BUSINESS_DOCUMENT_ID
WHERE <include refid="common_stat_where"/>
AND bs.BUSINESS_DATE &gt;= TO_DATE(#{beginDate}, 'YYYY-MM-DD')
AND bs.BUSINESS_DATE &lt; TO_DATE(#{endDate}, 'YYYY-MM-DD')
GROUP BY bs.FIRST_SUBJECT_CODE, bs.FIRST_SUBJECT_NAME,
bs.SECOND_SUBJECT_CODE, bs.SECOND_SUBJECT_NAME,
CASE WHEN bd.DOCUMENT_FORM_JSON LIKE '%"lease_area":0,%' THEN 'SCATTERED' ELSE 'FIXED' END,
SUBSTR(TO_CHAR(bs.BUSINESS_DATE, 'YYYY-MM'), 1, 7)
</select>
<!-- 智能分析:科目×客户×年月,WHERE同上(传入区间覆盖"本期季度+基期季度"两个月窗),用于客户归因 -->
<select id="sumBySubjectAndCustomer"
resultType="com.mhd.bms.domain.feeAnalysisReport.repository.po.FeeSubjectMonthStatPO">
SELECT
bs.FIRST_SUBJECT_CODE AS firstSubjectCode,
bs.FIRST_SUBJECT_NAME AS firstSubjectName,
bs.SECOND_SUBJECT_CODE AS secondSubjectCode,
bs.SECOND_SUBJECT_NAME AS secondSubjectName,
bs.SETTLEMENT_ENTITY AS customerName,
SUBSTR(TO_CHAR(bs.BUSINESS_DATE, 'YYYY-MM'), 1, 7) AS statMonth,
SUM(bs.BILLING_AMOUNT) AS totalAmount,
CASE WHEN bd.DOCUMENT_FORM_JSON LIKE '%"lease_area":0,%' THEN 'SCATTERED' ELSE 'FIXED' END AS pushMode
FROM BILLING_STATEMENT bs
LEFT JOIN BUSINESS_DOCUMENT bd ON bd.BUSINESS_DOCUMENT_ID = bs.BUSINESS_DOCUMENT_ID
WHERE <include refid="common_stat_where"/>
AND bs.BUSINESS_DATE &gt;= TO_DATE(#{beginDate}, 'YYYY-MM-DD')
AND bs.BUSINESS_DATE &lt; TO_DATE(#{endDate}, 'YYYY-MM-DD')
GROUP BY bs.FIRST_SUBJECT_CODE, bs.FIRST_SUBJECT_NAME,
bs.SECOND_SUBJECT_CODE, bs.SECOND_SUBJECT_NAME,
CASE WHEN bd.DOCUMENT_FORM_JSON LIKE '%"lease_area":0,%' THEN 'SCATTERED' ELSE 'FIXED' END,
SUBSTR(TO_CHAR(bs.BUSINESS_DATE, 'YYYY-MM'), 1, 7)
</select>
<!-- 年份下拉(降序) -->
<select id="selectDistinctYears" resultType="java.lang.String">
SELECT DISTINCT SUBSTR(TO_CHAR(bs.BUSINESS_DATE, 'YYYY'), 1, 4)
FROM BILLING_STATEMENT bs
WHERE bs.DEL_FLAG = 1
AND bs.BILLING_STATE IN (2, 3)
AND bs.RED_FLUSH_STATE = 1
AND bs.ACCOUNT_EXPENSE_TYPE = #{accountExpenseType}
<if test="topOrganizationId != null">
AND bs.TOP_ORGANIZATION_ID = #{topOrganizationId}
</if>
ORDER BY 1 DESC
</select>
</mapper>
+4 -6
View File
@@ -18,17 +18,15 @@ spring:
cloud:
nacos:
discovery:
# server-addr: 127.0.0.1:8848
# 线上测试环境配置 容器名+端口号
server-addr: 127.0.0.1:8848
# server-addr: 10.33.0.129:6010
server-addr: 10.102.192.30:6848
# server-addr: 10.102.192.30:6848
username: nacos
password: manhuoda@2023
config:
# server-addr: 127.0.0.1:8848
# 线上测试环境配置 容器名+端口号
server-addr: 127.0.0.1:8848
# server-addr: 10.33.0.129:6010
server-addr: 10.102.192.30:6848
# server-addr: 10.102.192.30:6848
username: nacos
password: manhuoda@2023
#线上正式环境
@@ -103,10 +103,14 @@ public class BusinessDocumentOrderApplicationService {
public List<BusinessDocumentOrderPO> queryList(BusinessDocumentOrderDO businessDocumentOrderDO) {
LoginUser loginUser = SecurityUtils.getLoginUser();
if (loginUser != null){
if (loginUser != null && loginUser.getUserPo() != null){
// 前端未传组织ID时查询当前登录组织的数据
if (businessDocumentOrderDO.getOrganizationId() == null){
businessDocumentOrderDO.setOrganizationId(loginUser.getUserPo().getOrganizationId());
}
Long topOrganizationId = loginUser.getUserPo().getTopOrganizationId();
if (topOrganizationId != null && topOrganizationId != 1){
businessDocumentOrderDO.setTopOrganizationId(loginUser.getUserPo().getTopOrganizationId());
businessDocumentOrderDO.setTopOrganizationId(topOrganizationId);
}
}
return businessDocumentOrderDomainService.queryList(businessDocumentOrderDO);
@@ -0,0 +1,390 @@
package com.mhd.oms.application.service.outEntrustOrder;
import cn.hutool.core.date.DateUtil;
import cn.hutool.json.JSONUtil;
import com.mhd.common.core.exception.ServiceException;
import com.mhd.common.core.web.domain.AjaxResult;
import com.mhd.common.security.utils.SecurityUtils;
import com.mhd.oms.constant.OutEntrustOrderConstant;
import com.mhd.oms.domain.outEntrustOrder.repository.po.OutEntrustOrderDetailPO;
import com.mhd.oms.domain.outEntrustOrder.repository.po.OutEntrustOrderPO;
import com.mhd.oms.domain.outEntrustOrder.repository.todo.OutEntrustOrderDO;
import com.mhd.oms.domain.outEntrustOrder.repository.todo.OutEntrustOrderDetailDO;
import com.mhd.oms.domain.outEntrustOrder.service.OutEntrustOrderDomainService;
import com.mhd.system.api.SystemServiceFeign;
import com.mhd.system.api.WmsServiceFeign;
import com.mhd.system.api.domain.NoticeMaterialDetailDTO;
import com.mhd.system.api.domain.NoticeOrderDTO;
import com.mhd.system.api.domain.WarehouseFeignPO;
import com.mhd.system.api.model.LoginUser;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
/**
* 出仓委托单ApplicationService
*
* @author rcx
* @date 2026-09-02
*/
@Service
@Slf4j
public class OutEntrustOrderApplicationService {
@Resource
private OutEntrustOrderDomainService outEntrustOrderDomainService;
@Resource
private SystemServiceFeign systemServiceFeign;
@Resource
private WmsServiceFeign wmsServiceFeign;
/**
* 分页查询出仓委托单列表(不做默认状态过滤,显示全部数据)
*/
public List<OutEntrustOrderPO> queryList(OutEntrustOrderDO outEntrustOrderDO) {
LoginUser loginUser = SecurityUtils.getLoginUser();
if (loginUser != null) {
Long topOrganizationId = loginUser.getUserPo().getTopOrganizationId();
if (topOrganizationId != null && topOrganizationId != 1) {
outEntrustOrderDO.setTopOrganizationId(topOrganizationId);
}
}
//结束时间只传年月日(解析为当天00:00:00),归一为当天最后一秒,保证查询包含结束日当天
if (outEntrustOrderDO.getEndCreateTime() != null) {
outEntrustOrderDO.setEndCreateTime(DateUtil.endOfDay(outEntrustOrderDO.getEndCreateTime()));
}
List<OutEntrustOrderPO> orderList = outEntrustOrderDomainService.queryList(outEntrustOrderDO);
fillInvoiceNo(orderList);
return orderList;
}
/**
* 列表回填发票号(明细发票号去重逗号拼接,供列表列展示)
*/
private void fillInvoiceNo(List<OutEntrustOrderPO> orderList) {
if (orderList == null || orderList.isEmpty()) {
return;
}
List<String> entrustNumbers = orderList.stream()
.map(OutEntrustOrderPO::getEntrustNumber).collect(Collectors.toList());
Map<String, String> invoiceNoMap = outEntrustOrderDomainService.getDetailByEntrustNumbers(entrustNumbers).stream()
.filter(detail -> detail.getInvoiceNo() != null && !detail.getInvoiceNo().trim().isEmpty())
.collect(Collectors.toMap(OutEntrustOrderDetailPO::getEntrustNumber, detail -> detail.getInvoiceNo().trim(),
(existing, replacement) -> existing.contains(replacement) ? existing
: existing + "," + replacement));
for (OutEntrustOrderPO order : orderList) {
order.setInvoiceNo(invoiceNoMap.get(order.getEntrustNumber()));
}
}
/**
* 新增出仓委托单(必填客户业务员仓库商品名称申报数量)
*/
public Boolean insert(OutEntrustOrderDO outEntrustOrderDO) {
//必填项校验
validateRequired(outEntrustOrderDO);
//订单类型未维护时默认普通出库(落库即生效,列表可见)
setDefaultOrderType(outEntrustOrderDO);
//仓库信息回填(校验仓库存在)
setWarehouseInfo(outEntrustOrderDO);
//组织信息填充
setOrganizationInfo(outEntrustOrderDO);
//汇总申报总数量
outEntrustOrderDO.setDeclareTotalQuantity(sumDeclareQuantity(outEntrustOrderDO.getMaterialDetailList()));
return outEntrustOrderDomainService.insert(outEntrustOrderDO);
}
/**
* 修改出仓委托单(仅待审核/已驳回且未下发可修改已驳回单编辑后状态重置为待审核)
*/
public Boolean update(OutEntrustOrderDO outEntrustOrderDO) {
//必填项校验
validateRequired(outEntrustOrderDO);
//状态类字段由审核/下发/完成入库接口推进编辑不允许改动(置null后updateById跳过这些列防前端误传/绕过审核改状态)
outEntrustOrderDO.setOrderStatus(null);
outEntrustOrderDO.setIssueStatus(null);
outEntrustOrderDO.setAuditRemark(null);
//订单类型未维护时默认普通出库(落库即生效,列表可见)
setDefaultOrderType(outEntrustOrderDO);
//仓库信息回填(校验仓库存在)
setWarehouseInfo(outEntrustOrderDO);
//明细全量替换时明细创建信息随主单更新人
if (outEntrustOrderDO.getCreateBy() == null) {
outEntrustOrderDO.setCreateBy(outEntrustOrderDO.getUpdateBy());
outEntrustOrderDO.setCreateByName(outEntrustOrderDO.getUpdateByName());
outEntrustOrderDO.setCreateTime(outEntrustOrderDO.getUpdateTime());
}
//汇总申报总数量
outEntrustOrderDO.setDeclareTotalQuantity(sumDeclareQuantity(outEntrustOrderDO.getMaterialDetailList()));
return outEntrustOrderDomainService.update(outEntrustOrderDO);
}
/**
* 订单类型未维护时默认普通出库(与下发兜底保持一致,保存即落库)
*/
private void setDefaultOrderType(OutEntrustOrderDO outEntrustOrderDO) {
if (outEntrustOrderDO.getOrderTypeCode() == null || outEntrustOrderDO.getOrderTypeCode().trim().isEmpty()) {
outEntrustOrderDO.setOrderTypeCode(OutEntrustOrderConstant.WMS_ORDER_TYPE_CODE);
outEntrustOrderDO.setOrderTypeName(OutEntrustOrderConstant.WMS_ORDER_TYPE_NAME);
}
}
/**
* 获取出仓委托单详情(含明细)
*/
public OutEntrustOrderPO getInfo(Long id) {
return outEntrustOrderDomainService.getInfo(id);
}
/**
* 批量删除出仓委托单(仅待审核/已驳回且未下发可删除)
*/
public Boolean delete(Long[] ids) {
if (ids == null || ids.length == 0) {
throw new ServiceException("传递的ID值缺失或为空");
}
return outEntrustOrderDomainService.delete(ids);
}
/**
* 批量审核待审核/已驳回 已审核(2)或已驳回(3)
*
* @param dto.idList 委托单ID集合
* @param dto.orderStatus 审核结果2-审核通过 3-驳回
* @param dto.auditRemark 审核备注(驳回原因选填)
*/
public Boolean audit(OutEntrustOrderDO outEntrustOrderDO) {
List<Long> ids = outEntrustOrderDO.getIdList();
if (ids == null || ids.isEmpty()) {
throw new ServiceException("传递的ID值缺失或为空");
}
Integer targetStatus = outEntrustOrderDO.getOrderStatus();
if (!OutEntrustOrderConstant.ORDER_STATUS_AUDITED.equals(targetStatus)
&& !OutEntrustOrderConstant.ORDER_STATUS_REJECTED.equals(targetStatus)) {
throw new ServiceException("审核结果参数错误,只能为已审核或已驳回");
}
//只有待审核/已驳回的委托单才可以审核
List<OutEntrustOrderPO> orderList = outEntrustOrderDomainService.queryByIds(ids);
List<Long> unableAuditIds = orderList.stream()
.filter(order -> !OutEntrustOrderConstant.ORDER_STATUS_PENDING_AUDIT.equals(order.getOrderStatus())
&& !OutEntrustOrderConstant.ORDER_STATUS_REJECTED.equals(order.getOrderStatus()))
.map(OutEntrustOrderPO::getId).collect(Collectors.toList());
if (!unableAuditIds.isEmpty()) {
throw new ServiceException("选中数据中存在进行中/已审核/已入库的委托单,只有待审核或已驳回的委托单方可审核");
}
LoginUser loginUser = SecurityUtils.getLoginUser();
Long auditBy = loginUser == null ? null : loginUser.getUserid();
String auditByName = loginUser == null ? null : loginUser.getUsername();
return outEntrustOrderDomainService.audit(ids, targetStatus, auditBy, auditByName, outEntrustOrderDO.getAuditRemark());
}
/**
* 批量下发已审核的委托单下发到WMS(生成出库通知单)下发后订单状态保持已审核仅置下发状态为已下发
*/
@Transactional(rollbackFor = Exception.class)
public Boolean issueOrder(List<Long> ids) {
if (ids == null || ids.isEmpty()) {
throw new ServiceException("传递的ID值缺失或为空");
}
//只有已审核且未下发的委托单才可以下发
List<OutEntrustOrderPO> orderList = outEntrustOrderDomainService.queryByIds(ids);
for (OutEntrustOrderPO order : orderList) {
if (!OutEntrustOrderConstant.ORDER_STATUS_AUDITED.equals(order.getOrderStatus())) {
throw new ServiceException(String.format("委托单[%s]未审核,已审核的委托单方可下发", order.getEntrustNumber()));
}
if (!OutEntrustOrderConstant.ISSUE_STATUS_1.equals(order.getIssueStatus())) {
throw new ServiceException(String.format("委托单[%s]已下发,不允许重复下发", order.getEntrustNumber()));
}
}
LoginUser loginUser = SecurityUtils.getLoginUser();
Long issueBy = loginUser == null ? null : loginUser.getUserid();
String issueByName = loginUser == null ? null : loginUser.getUsername();
for (OutEntrustOrderPO order : orderList) {
//组装出库通知单
NoticeOrderDTO noticeOrderDTO = createNoticeOrderInfo(order);
//推送给WMS
AjaxResult ajaxResult = wmsServiceFeign.stockOutOrInInsert(noticeOrderDTO);
//WMS返回成功才允许置为已下发避免下发失败被静默吞掉
if (ajaxResult == null || !"200".equals(String.valueOf(ajaxResult.get("code")))) {
String errorMsg = ajaxResult == null ? "WMS服务调用失败" : String.valueOf(ajaxResult.get("msg"));
log.error("出仓委托单[{}]下发WMS失败:{}", order.getEntrustNumber(), errorMsg);
throw new ServiceException(String.format("委托单[%s]下发WMS失败:%s", order.getEntrustNumber(), errorMsg));
}
log.info("出仓委托单[{}]下发WMS成功", order.getEntrustNumber());
}
return outEntrustOrderDomainService.issue(ids, issueBy, issueByName);
}
/**
* 批量完成已审核(已下发) 已入库(OMS人工推进,WMS出库完成后由人工确认)
*/
public Boolean finish(List<Long> ids) {
if (ids == null || ids.isEmpty()) {
throw new ServiceException("传递的ID值缺失或为空");
}
//只有已审核且已下发的委托单才可以置为已入库(兼容存量:旧版本曾自动置为进行中的也允许)
List<OutEntrustOrderPO> orderList = outEntrustOrderDomainService.queryByIds(ids);
List<Long> unableFinishIds = orderList.stream()
.filter(order -> !(OutEntrustOrderConstant.ORDER_STATUS_IN_PROGRESS.equals(order.getOrderStatus())
|| (OutEntrustOrderConstant.ORDER_STATUS_AUDITED.equals(order.getOrderStatus())
&& OutEntrustOrderConstant.ISSUE_STATUS_2.equals(order.getIssueStatus()))))
.map(OutEntrustOrderPO::getId).collect(Collectors.toList());
if (!unableFinishIds.isEmpty()) {
throw new ServiceException("选中数据中存在待审核/已驳回或未下发的委托单,只有已审核且已下发的委托单方可置为已入库");
}
LoginUser loginUser = SecurityUtils.getLoginUser();
Long updateBy = loginUser == null ? null : loginUser.getUserid();
String updateByName = loginUser == null ? null : loginUser.getUsername();
return outEntrustOrderDomainService.finish(ids, updateBy, updateByName);
}
/**
* 组装出库通知单信息(客户=货主)
*/
private NoticeOrderDTO createNoticeOrderInfo(OutEntrustOrderPO order) {
NoticeOrderDTO noticeOrderDTO = new NoticeOrderDTO();
noticeOrderDTO.setType(OutEntrustOrderConstant.NOTICE_TYPE_OUT);
//客户订单号记录委托单号便于WMS侧追溯
noticeOrderDTO.setCustomerOrderNumber(order.getEntrustNumber());
noticeOrderDTO.setWarehouseId(order.getWarehouseId());
//出仓委托单的客户即WMS货主货主为物料档案/库存作业的归属
noticeOrderDTO.setShipperId(order.getShipperId());
noticeOrderDTO.setShipperName(order.getShipperName());
noticeOrderDTO.setCustomerId(order.getShipperId());
noticeOrderDTO.setCustomerName(order.getShipperName());
//订单类型未维护时默认普通出库(WMS侧按出库单据类型字典翻译)
noticeOrderDTO.setOrderTypeCode(order.getOrderTypeCode() == null
? OutEntrustOrderConstant.WMS_ORDER_TYPE_CODE : order.getOrderTypeCode());
noticeOrderDTO.setOrderTypeName(order.getOrderTypeName());
noticeOrderDTO.setExpectTime(order.getExpectTime() == null ? new Date() : order.getExpectTime());
noticeOrderDTO.setNullify(2);
//出库通知单仅有限几个业务字段业务员及委托/报关关键信息拼入备注留存(超长截断)
noticeOrderDTO.setRemark(buildNoticeRemark(order));
//物料明细申报数量作为通知单计划数量
List<OutEntrustOrderDetailPO> detailList = outEntrustOrderDomainService.getDetailByEntrustNumber(order.getEntrustNumber());
if (detailList == null || detailList.isEmpty()) {
throw new ServiceException(String.format("委托单[%s]缺少商品明细,无法下发", order.getEntrustNumber()));
}
List<NoticeMaterialDetailDTO> materialDetailList = new ArrayList<>();
for (OutEntrustOrderDetailPO detail : detailList) {
NoticeMaterialDetailDTO materialDetail = new NoticeMaterialDetailDTO();
materialDetail.setMaterialBaseInfoId(detail.getMaterialBaseInfoId());
materialDetail.setMaterialCode(detail.getMaterialCode());
//物料名称档案名优先,无档案时用委托单品名
materialDetail.setMaterialName(detail.getMaterialName() != null && !detail.getMaterialName().trim().isEmpty()
? detail.getMaterialName() : detail.getCommodityName());
materialDetail.setBarCode(detail.getBarCode());
materialDetail.setQuantity(detail.getDeclareQuantity());
materialDetailList.add(materialDetail);
}
noticeOrderDTO.setMaterialDetailList(materialDetailList);
return noticeOrderDTO;
}
/**
* 出库通知单备注拼入业务员及委托/报关关键信息(通知单无对应字段)
*/
private String buildNoticeRemark(OutEntrustOrderPO order) {
StringBuilder remark = new StringBuilder();
if (order.getRemark() != null && !order.getRemark().trim().isEmpty()) {
remark.append(order.getRemark().trim());
}
appendIfPresent(remark, "业务员", order.getSalesmanName());
appendIfPresent(remark, "委托编号", order.getEntrustNo());
appendIfPresent(remark, "柜号", order.getContainerNo());
appendIfPresent(remark, "承运商", order.getCarrierName() != null ? order.getCarrierName() : order.getCarrier());
appendIfPresent(remark, "To", order.getToReceiver());
appendIfPresent(remark, "联系人", order.getContactPerson());
appendIfPresent(remark, "电话", order.getTel());
appendIfPresent(remark, "申报地关区", order.getDeclareCustoms());
appendIfPresent(remark, "进出境关别", order.getEntryExitCustoms());
//备注列长度保护
return remark.length() > 480 ? remark.substring(0, 480) : remark.toString();
}
private void appendIfPresent(StringBuilder remark, String label, String value) {
if (value != null && !value.trim().isEmpty()) {
remark.append(remark.length() > 0 ? "" : "").append(label).append("").append(value.trim());
}
}
/**
* 必填项校验客户业务员仓库商品名称申报数量其余字段不强制
*/
private void validateRequired(OutEntrustOrderDO outEntrustOrderDO) {
if (outEntrustOrderDO.getShipperId() == null) {
throw new ServiceException("客户不能为空");
}
if (outEntrustOrderDO.getSalesmanId() == null || outEntrustOrderDO.getSalesmanId().trim().isEmpty()) {
throw new ServiceException("业务员不能为空");
}
if (outEntrustOrderDO.getWarehouseId() == null) {
throw new ServiceException("仓库不能为空");
}
if (outEntrustOrderDO.getMaterialDetailList() == null || outEntrustOrderDO.getMaterialDetailList().isEmpty()) {
throw new ServiceException("商品明细不能为空");
}
int index = 1;
for (OutEntrustOrderDetailDO detail : outEntrustOrderDO.getMaterialDetailList()) {
//商品名称物料档案名/委托单品名至少其一
boolean nameBlank = (detail.getMaterialName() == null || detail.getMaterialName().trim().isEmpty())
&& (detail.getCommodityName() == null || detail.getCommodityName().trim().isEmpty());
if (nameBlank) {
throw new ServiceException(String.format("商品明细第%d行商品名称不能为空", index));
}
if (detail.getDeclareQuantity() == null || detail.getDeclareQuantity().compareTo(BigDecimal.ZERO) <= 0) {
throw new ServiceException(String.format("商品明细第%d行申报数量必须大于0", index));
}
index++;
}
}
/**
* 仓库信息校验并回填编码/名称
*/
private void setWarehouseInfo(OutEntrustOrderDO outEntrustOrderDO) {
AjaxResult ajaxResult = systemServiceFeign.getWarehouseInfoByWarehouseId(outEntrustOrderDO.getWarehouseId());
if (ajaxResult == null || !"200".equals(String.valueOf(ajaxResult.get("code")))) {
throw new ServiceException("获取仓库信息失败,请检查仓库是否有效");
}
WarehouseFeignPO warehouseFeignPO = JSONUtil.toBean(JSONUtil.toJsonStr(ajaxResult.get("data")), WarehouseFeignPO.class);
if (warehouseFeignPO == null || Objects.isNull(warehouseFeignPO.getWarehouseId())) {
throw new ServiceException("仓库不存在,请重新选择");
}
outEntrustOrderDO.setWarehouseCode(warehouseFeignPO.getWarehouseCode());
outEntrustOrderDO.setWarehouseName(warehouseFeignPO.getWarehouseName());
}
/**
* 组织信息填充
*/
private void setOrganizationInfo(OutEntrustOrderDO outEntrustOrderDO) {
LoginUser loginUser = SecurityUtils.getLoginUser();
if (loginUser != null && loginUser.getUserPo() != null) {
outEntrustOrderDO.setOrganizationId(loginUser.getUserPo().getOrganizationId());
outEntrustOrderDO.setOrganizationName(loginUser.getUserPo().getOrganizationName());
outEntrustOrderDO.setTopOrganizationId(loginUser.getUserPo().getTopOrganizationId());
}
}
/**
* 汇总明细申报数量
*/
private BigDecimal sumDeclareQuantity(List<OutEntrustOrderDetailDO> detailList) {
return detailList.stream()
.map(OutEntrustOrderDetailDO::getDeclareQuantity)
.reduce(BigDecimal.ZERO, BigDecimal::add);
}
}
@@ -0,0 +1,37 @@
package com.mhd.oms.constant;
/**
* 出仓委托单常量
*
* @author rcx
* @date 2026-09-02
*/
public interface OutEntrustOrderConstant {
/** 出仓委托单号前缀 */
String OUT_ENTRUST_NO_PREFIX = "CKWT";
/** 订单状态:1-待审核(新增默认值;已驳回单重新编辑后也回到此状态) */
Integer ORDER_STATUS_PENDING_AUDIT = 1;
/** 订单状态:2-已审核 */
Integer ORDER_STATUS_AUDITED = 2;
/** 订单状态:3-已驳回 */
Integer ORDER_STATUS_REJECTED = 3;
/** 订单状态:4-进行中(已下发WMS) */
Integer ORDER_STATUS_IN_PROGRESS = 4;
/** 订单状态:5-已入库 */
Integer ORDER_STATUS_STORED = 5;
/** 下发状态:1-未下发 */
Integer ISSUE_STATUS_1 = 1;
/** 下发状态:2-已下发 */
Integer ISSUE_STATUS_2 = 2;
/** 下发WMS出库通知单-订单类型编码 */
String WMS_ORDER_TYPE_CODE = "pu_tong_chu_ku";
/** 下发WMS出库通知单-订单类型名称 */
String WMS_ORDER_TYPE_NAME = "普通出库";
/** 出库通知单类型(NoticeOrderDTO.type1-入库通知单 2-出库通知单) */
Integer NOTICE_TYPE_OUT = 2;
}

Some files were not shown because too many files have changed in this diff Show More