Compare commits
196
Commits
dev_820
...
dev_sfjk_0827
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3081460143 | ||
|
|
6b3912aae9 | ||
|
|
12e43080b8 | ||
|
|
77e0c77fbf | ||
|
|
03978d23d7 | ||
|
|
ee7dbfcfb9 | ||
|
|
94d05cca9c | ||
|
|
9fd3d34610 | ||
|
|
edb7a5343d | ||
|
|
762c49cd0f | ||
|
|
1b8511c092 | ||
|
|
ce3bb036fa | ||
|
|
71515e74f5 | ||
|
|
7d3c229b9a | ||
|
|
a549ac1507 | ||
|
|
bf3f0a2bfe | ||
|
|
35b026c8bd | ||
|
|
0eb47793a8 | ||
|
|
fee891dad0 | ||
|
|
34496de969 | ||
|
|
7a3021f82a | ||
|
|
c9c3062789 | ||
|
|
4ad31a870a | ||
|
|
9447604c96 | ||
|
|
1577eddc7b | ||
|
|
f5300fdd5d | ||
|
|
3880755df0 | ||
|
|
28bf58e8d1 | ||
|
|
3a5af634dd | ||
|
|
4f45a73505 | ||
|
|
b5f710cd33 | ||
|
|
bfffc6bbf1 | ||
|
|
a8815ade02 | ||
|
|
57312753ed | ||
|
|
03ce38ed9b | ||
|
|
b09b07978f | ||
|
|
87dae51a15 | ||
|
|
49ab26a8e9 | ||
|
|
c1f483f0a0 | ||
|
|
6d7e42bcde | ||
|
|
61572c536a | ||
|
|
572b25ee88 | ||
|
|
3576d734af | ||
|
|
028af3dd2f | ||
|
|
3af78d5d7b | ||
|
|
e821619a96 | ||
|
|
434348f972 | ||
|
|
72cb1d8c07 | ||
|
|
f5a7c8c1f3 | ||
|
|
3989d75f05 | ||
|
|
471b515a45 | ||
|
|
19e897cae0 | ||
|
|
91bdb99ec2 | ||
|
|
004580e252 | ||
|
|
da3c58a366 | ||
|
|
1cec762930 | ||
|
|
4deaeaa89d | ||
|
|
1208e02fe7 | ||
|
|
77fb71dab4 | ||
|
|
cd41c616bb | ||
|
|
a4f27ab643 | ||
|
|
c174ea8742 | ||
|
|
2bbd6784c4 | ||
|
|
923afda21c | ||
|
|
bb3ca932d2 | ||
|
|
a588264849 | ||
|
|
1300a4e1f8 | ||
|
|
bf96f5f005 | ||
|
|
a983ae639e | ||
|
|
a74afd837c | ||
|
|
bc3afbacb1 | ||
|
|
5ba6a3b73d | ||
|
|
da84b7bb69 | ||
|
|
2d1e4b5912 | ||
|
|
2781f0d8d9 | ||
|
|
8a9bbb3cef | ||
|
|
155f1866b4 | ||
|
|
d4446e8482 | ||
|
|
254a804a17 | ||
|
|
4c4b8f4e29 | ||
|
|
06cf5ce861 | ||
|
|
7a01315ca8 | ||
|
|
d0773a40d2 | ||
|
|
6349181cc3 | ||
|
|
e95feb5c24 | ||
|
|
afa502eeaf | ||
|
|
627376da07 | ||
|
|
5127123236 | ||
|
|
7b0d08a132 | ||
|
|
ccdbbda165 | ||
|
|
e3058d8eb1 | ||
|
|
5e3a0d86d0 | ||
|
|
76d95c1b2d | ||
|
|
e4b29858a4 | ||
|
|
f1dec2298d | ||
|
|
7b52764ae1 | ||
|
|
1006860698 | ||
|
|
1e00681289 | ||
|
|
353300b643 | ||
|
|
9eb2f347f7 | ||
|
|
182438e1dc | ||
|
|
0be73e9bfe | ||
|
|
5e369bdd99 | ||
|
|
a5a222a78e | ||
|
|
b1840c8965 | ||
|
|
02a0b7aab6 | ||
|
|
42ab5160ed | ||
|
|
df66287455 | ||
|
|
e2ce186aba | ||
|
|
9be0de0ae7 | ||
|
|
4410162f39 | ||
|
|
3060fcd6a4 | ||
|
|
7d8e1c3872 | ||
|
|
59249be9b0 | ||
|
|
8fe5e079dd | ||
|
|
d1d55ce52a | ||
|
|
b12517e5b5 | ||
|
|
09d24217b5 | ||
|
|
3c2f252347 | ||
|
|
940e4c56b0 | ||
|
|
09ab103d1d | ||
|
|
8dcb2334d5 | ||
|
|
7a5e124f98 | ||
|
|
c5a2508a36 | ||
|
|
7353021d92 | ||
|
|
735cf9af9f | ||
|
|
f2f3fb9c19 | ||
|
|
a62ed7c60f | ||
|
|
4168f921f6 | ||
|
|
1916152775 | ||
|
|
83f4cce451 | ||
|
|
f5f792d3f8 | ||
|
|
dbd61c0519 | ||
|
|
cb5d933e0a | ||
|
|
5298e9d4e3 | ||
|
|
06f61d7957 | ||
|
|
4567f00858 | ||
|
|
e8328b0159 | ||
|
|
bb3dfaed4a | ||
|
|
0eb2f323c7 | ||
|
|
a35030e2f5 | ||
|
|
d1b1b27ab0 | ||
|
|
4a6a2798e8 | ||
|
|
9068fa0e40 | ||
|
|
99cb355640 | ||
|
|
c9520c65be | ||
|
|
2a4e39f755 | ||
|
|
a08d8a7fd5 | ||
|
|
ab7219e600 | ||
|
|
1b3b1a6394 | ||
|
|
072a4f7d1f | ||
|
|
63a19fbbcd | ||
|
|
5fc491f612 | ||
|
|
b209d8cb6d | ||
|
|
06aae196c7 | ||
|
|
e91c5801eb | ||
|
|
8704df738c | ||
|
|
000092952f | ||
|
|
22f2b66ffa | ||
|
|
3aec384ba0 | ||
|
|
27f3fceb6e | ||
|
|
3e5ed28d69 | ||
|
|
78d40d35be | ||
|
|
8fedb6d6ed | ||
|
|
b8de1c00eb | ||
|
|
bf080609fe | ||
|
|
b415ed6fa0 | ||
|
|
694ed699ec | ||
|
|
0b26692a34 | ||
|
|
c2af421c7f | ||
|
|
57f87baf85 | ||
|
|
e23edadde6 | ||
|
|
530254e000 | ||
|
|
33d085f9b9 | ||
|
|
21fcc24e2e | ||
|
|
64350f565b | ||
|
|
ff2b1965f7 | ||
|
|
7a769071e4 | ||
|
|
b05f994e74 | ||
|
|
ef13d890be | ||
|
|
cc943d164b | ||
|
|
77420e9617 | ||
|
|
fa51580985 | ||
|
|
362f1b5e2f | ||
|
|
b859aa99d3 | ||
|
|
83f05dd11d | ||
|
|
dd0bf32c35 | ||
|
|
7ed12c9405 | ||
|
|
72e445ea16 | ||
|
|
f50a3e690a | ||
|
|
8692b38671 | ||
|
|
f77d89fa7f | ||
|
|
0ff327a071 | ||
|
|
6661c016e8 | ||
|
|
bca8b5fbed | ||
|
|
e489015e3e |
@@ -18,7 +18,7 @@ import java.util.Set;
|
|||||||
/**
|
/**
|
||||||
* bms财务结算系统feign调用接口
|
* 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 {
|
public interface BmsServiceFeign {
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import org.springframework.web.bind.annotation.RequestBody;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.concurrent.CompletableFuture;
|
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 {
|
public interface OmsServiceFeign {
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -237,6 +237,12 @@ public interface SystemServiceFeign {
|
|||||||
@GetMapping("/warehouseApi/getInfo/{warehouseId}")
|
@GetMapping("/warehouseApi/getInfo/{warehouseId}")
|
||||||
public AjaxResult getWarehouseInfoByWarehouseId(@PathVariable("warehouseId") Long warehouseId);
|
public AjaxResult getWarehouseInfoByWarehouseId(@PathVariable("warehouseId") Long warehouseId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据warehouseId获取仓库及下库区及库位树(与前端 warehouseApi/getWarehouseChildren 一致,用于PC上架页默认库位回显)
|
||||||
|
*/
|
||||||
|
@GetMapping("/warehouseApi/getWarehouseChildren")
|
||||||
|
public AjaxResult getWarehouseChildren(@RequestParam("warehouseId") Long warehouseId);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 根据storageSectionId获取对应库区信息
|
* 根据storageSectionId获取对应库区信息
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ import org.springframework.web.bind.annotation.*;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
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 {
|
public interface WlhyServiceFeign {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import java.util.Map;
|
|||||||
* @description: TODO
|
* @description: TODO
|
||||||
* @date 2024/5/10 8:58
|
* @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 {
|
public interface WmsServiceFeign {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -151,4 +151,12 @@ public interface WmsServiceFeign {
|
|||||||
@ApiOperation("OMS批量签名-同步WMS入/出库单签名字段")
|
@ApiOperation("OMS批量签名-同步WMS入/出库单签名字段")
|
||||||
@PostMapping("/stockSignatureApi/batchSign")
|
@PostMapping("/stockSignatureApi/batchSign")
|
||||||
public AjaxResult batchSignWms(@RequestBody SignatureBatchFeignDTO dto);
|
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.cloud.openfeign.FeignClient;
|
||||||
import org.springframework.web.bind.annotation.*;
|
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 {
|
public interface YmsServiceFeign {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -375,6 +375,11 @@ public class InMaterialDetail extends BaseVOEntity {
|
|||||||
@DateTimeFormat(pattern="yyyy-MM-dd")
|
@DateTimeFormat(pattern="yyyy-MM-dd")
|
||||||
private Date drinkDate;
|
private Date drinkDate;
|
||||||
|
|
||||||
|
@ApiModelProperty("过期日期")
|
||||||
|
@JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
|
||||||
|
@DateTimeFormat(pattern = "yyyy-MM-dd")
|
||||||
|
private Date expiryDate;
|
||||||
|
|
||||||
@ApiModelProperty("来货验收是否合格;1 合格,0 不合格")
|
@ApiModelProperty("来货验收是否合格;1 合格,0 不合格")
|
||||||
@Excel(name = "来货验收是否合格;1 合格,0 不合格")
|
@Excel(name = "来货验收是否合格;1 合格,0 不合格")
|
||||||
private String arrivalAcceptQualified;
|
private String arrivalAcceptQualified;
|
||||||
|
|||||||
+2
@@ -83,6 +83,8 @@ public class MaterialInventoryOmsParamDO extends BaseVOEntity {
|
|||||||
private Date productionDate;
|
private Date productionDate;
|
||||||
//过期日期
|
//过期日期
|
||||||
private Date expiryDate;
|
private Date expiryDate;
|
||||||
|
//进货日期
|
||||||
|
private Date purchaseDate;
|
||||||
|
|
||||||
private String executioOrderNumber;
|
private String executioOrderNumber;
|
||||||
|
|
||||||
|
|||||||
@@ -405,4 +405,13 @@ public class StockOutOrder extends BaseVOEntity {
|
|||||||
@Excel(name = "车次")
|
@Excel(name = "车次")
|
||||||
private Integer trainNo;
|
private Integer trainNo;
|
||||||
|
|
||||||
|
@ApiModelProperty("线路编码(取自WMS线路管理)")
|
||||||
|
private String lineCode;
|
||||||
|
|
||||||
|
@ApiModelProperty("线路名称(冗余存储)")
|
||||||
|
private String lineName;
|
||||||
|
|
||||||
|
@ApiModelProperty("车次序号(显示: 线路编码-车次, 如31-4)")
|
||||||
|
private String shuttleNo;
|
||||||
|
|
||||||
}
|
}
|
||||||
+30
@@ -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;
|
||||||
|
}
|
||||||
+6
@@ -20,6 +20,7 @@ import org.springframework.cloud.openfeign.FallbackFactory;
|
|||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
import org.springframework.web.bind.annotation.PathVariable;
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
import org.springframework.web.bind.annotation.RequestBody;
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@@ -184,6 +185,11 @@ public class RemoteSystemFeignFallbackFactory implements FallbackFactory<SystemS
|
|||||||
return AjaxResult.error("根据仓库ID获取仓库信息失败");
|
return AjaxResult.error("根据仓库ID获取仓库信息失败");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AjaxResult getWarehouseChildren(@RequestParam("warehouseId") Long warehouseId) {
|
||||||
|
return AjaxResult.error("根据仓库ID获取仓库及下库区及库位树失败");
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public AjaxResult getStorageSectionInfoByStorageSectionId(@PathVariable("storageSectionId") Long storageSectionId) {
|
public AjaxResult getStorageSectionInfoByStorageSectionId(@PathVariable("storageSectionId") Long storageSectionId) {
|
||||||
return AjaxResult.error("根据库区ID获取库区信息失败");
|
return AjaxResult.error("根据库区ID获取库区信息失败");
|
||||||
|
|||||||
+6
@@ -125,6 +125,12 @@ public class RemoteWmsFallbackFactory implements FallbackFactory<WmsServiceFeign
|
|||||||
log.error("WMS批量签名调用失败");
|
log.error("WMS批量签名调用失败");
|
||||||
return AjaxResult.error("WMS批量签名调用失败:" + cause.getMessage());
|
return AjaxResult.error("WMS批量签名调用失败:" + cause.getMessage());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AjaxResult listLineManagement(String routeCode) {
|
||||||
|
log.error("OMS查询WMS线路管理失败, routeCode:{}", routeCode);
|
||||||
|
return AjaxResult.error("WMS线路管理查询失败:" + cause.getMessage());
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+44
@@ -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;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
+54
-21
@@ -179,34 +179,67 @@ public class TokenLoginApplicationService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 注册
|
* App注册(手机号 + 注册短信验证码 + 密码)
|
||||||
*/
|
*/
|
||||||
public void register(String username, String password) {
|
public void register(LoginDTO loginDTO) {
|
||||||
// 用户名或密码为空 错误
|
String userPhone = loginDTO.getUserPhone(); // LoginDTO.java:27-28
|
||||||
if (StringUtils.isAnyBlank(username, password)) {
|
String verificationCode = loginDTO.getVerificationCode(); // LoginDTO.java:30-31
|
||||||
throw new ServiceException("用户/密码必须填写");
|
String password = loginDTO.getPassword();
|
||||||
|
|
||||||
|
// 1. 基础校验
|
||||||
|
if (StringUtils.isAnyBlank(userPhone, verificationCode, password)) {
|
||||||
|
throw new ServiceException("手机号、验证码、密码必须填写");
|
||||||
}
|
}
|
||||||
if (username.length() < UserConstants.USERNAME_MIN_LENGTH
|
if (!RegexUtil.isPhoneLegal(userPhone)) {
|
||||||
|| username.length() > UserConstants.USERNAME_MAX_LENGTH) {
|
throw new DigitalLogisticsException(UserError.USER_PHONE_REG_ERROR);
|
||||||
throw new ServiceException("账户长度必须在2到20个字符之间");
|
|
||||||
}
|
|
||||||
if (password.length() < UserConstants.PASSWORD_MIN_LENGTH
|
|
||||||
|| password.length() > UserConstants.PASSWORD_MAX_LENGTH) {
|
|
||||||
throw new ServiceException("密码长度必须在5到20个字符之间");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 注册用户信息
|
// 2. 校验注册短信验证码(发送时写入 redis,见 SmsApplicationService.java:112, :165)
|
||||||
SysUser sysUser = new SysUser();
|
String key = CacheConstants.SMS_REGISTER + userPhone; // "sms_register:" CacheConstants.java:51
|
||||||
sysUser.setUserName(username);
|
Object cache = redisService.getCacheObject(key);
|
||||||
sysUser.setNickName(username);
|
if (cache == null || !ObjectUtil.equal(String.valueOf(cache), verificationCode)) {
|
||||||
sysUser.setPassword(SecurityUtils.encryptPassword(password));
|
recordLogininfor(userPhone, Constants.LOGIN_FAIL, "注册验证码错误", 1);
|
||||||
//TODO app注册没写
|
throw new ServiceException("验证码错误或已过期");
|
||||||
/*R<?> registerResult = userServiceFeign.registerUserInfo(sysUser, SecurityConstants.INNER);
|
}
|
||||||
|
|
||||||
|
// 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-38,App 选择组织时传
|
||||||
|
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()) {
|
if (R.FAIL == registerResult.getCode()) {
|
||||||
throw new ServiceException(registerResult.getMsg());
|
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")
|
@PostMapping("register")
|
||||||
public R<?> register(@RequestBody LoginDTO loginDTO) {
|
public R<?> register(@RequestBody LoginDTO loginDTO) {
|
||||||
// 用户注册
|
// 用户注册(手机号+短信验证码+密码)
|
||||||
tokenLoginApplicationService.register(loginDTO.getUsername(), loginDTO.getPassword());
|
tokenLoginApplicationService.register(loginDTO);
|
||||||
return R.ok();
|
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.common.security.utils.password.PasswordUtil;
|
||||||
import com.mhd.system.api.RemoteLogService;
|
import com.mhd.system.api.RemoteLogService;
|
||||||
import com.mhd.system.api.domain.SysLogininforPo;
|
import com.mhd.system.api.domain.SysLogininforPo;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
@@ -39,6 +40,7 @@ import javax.annotation.Resource;
|
|||||||
* @description: 登录密码方法
|
* @description: 登录密码方法
|
||||||
* @date 2023/12/26 15:33
|
* @date 2023/12/26 15:33
|
||||||
**/
|
**/
|
||||||
|
@Slf4j
|
||||||
@Component
|
@Component
|
||||||
public class SysPasswordService
|
public class SysPasswordService
|
||||||
{
|
{
|
||||||
@@ -76,29 +78,31 @@ public class SysPasswordService
|
|||||||
if (retryCount == null) {
|
if (retryCount == null) {
|
||||||
retryCount = 0;
|
retryCount = 0;
|
||||||
}
|
}
|
||||||
//TODO 从三方配置表中取值
|
//TODO 从三方配置表中取值(失败时用默认值,保证登录错误提示可用)
|
||||||
SysDepartInterfaceInfoDTO sysDepartInterfaceInfoDTO = new SysDepartInterfaceInfoDTO();
|
int maxRetryCount = 5; // 默认:密码错5次锁定
|
||||||
sysDepartInterfaceInfoDTO.setTopOrganizationId(userPo.getTopOrganizationId());
|
long lockTime = 30L; // 默认:锁定30分钟
|
||||||
sysDepartInterfaceInfoDTO.setInterfaceType("login");
|
try {
|
||||||
sysDepartInterfaceInfoDTO.setInterfaceTypeKey("maxRetryCount");
|
SysDepartInterfaceInfoDTO dto = new SysDepartInterfaceInfoDTO();
|
||||||
List<SysDepartInterfaceInfoPo> sysDepartInterfaceInfoPos = thirdPartyServiceFeign.queryListByFeign(sysDepartInterfaceInfoDTO);
|
dto.setTopOrganizationId(userPo.getTopOrganizationId());
|
||||||
if (sysDepartInterfaceInfoPos.isEmpty()){
|
dto.setInterfaceType("login");
|
||||||
String errMsg = "缺少登录信息配置,请联系管理员";
|
dto.setInterfaceTypeKey("maxRetryCount");
|
||||||
recordLogininfor(userPo, Constants.LOGIN_FAIL, errMsg, 1);
|
List<SysDepartInterfaceInfoPo> pos = thirdPartyServiceFeign.queryListByFeign(dto);
|
||||||
throw new ServiceException(errMsg);
|
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);
|
if (retryCount >= maxRetryCount) {
|
||||||
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){
|
|
||||||
String errMsg = String.format("密码输入错误%s次,帐户锁定%s分钟", maxRetryCount, lockTime);
|
String errMsg = String.format("密码输入错误%s次,帐户锁定%s分钟", maxRetryCount, lockTime);
|
||||||
recordLogininfor(userPo, Constants.LOGIN_FAIL, errMsg, 1);
|
recordLogininfor(userPo, Constants.LOGIN_FAIL, errMsg, 1);
|
||||||
throw new ServiceException(errMsg);
|
throw new ServiceException(errMsg);
|
||||||
|
|||||||
@@ -12,10 +12,10 @@ spring:
|
|||||||
cloud:
|
cloud:
|
||||||
nacos:
|
nacos:
|
||||||
discovery:
|
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.33.0.129:6010
|
||||||
server-addr: 10.102.192.31:6848
|
# server-addr: 10.102.192.31:6848
|
||||||
username: nacos
|
username: nacos
|
||||||
password: manhuoda@2023
|
password: manhuoda@2023
|
||||||
#线上正式环境
|
#线上正式环境
|
||||||
@@ -23,8 +23,8 @@ spring:
|
|||||||
# username: nacos
|
# username: nacos
|
||||||
# password: manhuoda@2023
|
# password: manhuoda@2023
|
||||||
config:
|
config:
|
||||||
# server-addr: 127.0.0.1:8848
|
server-addr: 127.0.0.1:8848
|
||||||
server-addr: 10.102.192.31:6848
|
# server-addr: 10.102.192.31:6848
|
||||||
# 测试环境配置 容器名+端口号
|
# 测试环境配置 容器名+端口号
|
||||||
# server-addr: 10.33.0.129:6010
|
# server-addr: 10.33.0.129:6010
|
||||||
# 线上测试环境配置 容器名+端口号
|
# 线上测试环境配置 容器名+端口号
|
||||||
@@ -52,4 +52,4 @@ sso:
|
|||||||
AwIDAQAB
|
AwIDAQAB
|
||||||
-----END PUBLIC KEY-----
|
-----END PUBLIC KEY-----
|
||||||
# SSO登录成功后跳转的前端页面地址
|
# SSO登录成功后跳转的前端页面地址
|
||||||
redirect-url: "https://diglog.namkwong.com.mo/main_app/sso"
|
redirect-url: "https://diglog.namkwong.com.mo/main_app/sso"
|
||||||
+12
@@ -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.RealPlatformOverviewPO;
|
||||||
import com.mhd.bi.domain.biPlatformRealData.repository.po.RealVehicleStatusPO;
|
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.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;
|
import java.util.List;
|
||||||
|
|
||||||
@@ -31,4 +34,13 @@ public interface BiPlatformRealDataMapper {
|
|||||||
|
|
||||||
/** 任务负荷(今日 24 小时) */
|
/** 任务负荷(今日 24 小时) */
|
||||||
List<RealTaskLoadPO> queryTaskLoadToday();
|
List<RealTaskLoadPO> queryTaskLoadToday();
|
||||||
|
|
||||||
|
/** 今日预约-业务类型统计(按 businessName 分组统计今日预约数量) */
|
||||||
|
List<RealBusinessTypeStatPO> queryTodayBusinessTypeStats();
|
||||||
|
|
||||||
|
/** 月台监控-按楼层统计月台车辆(楼层、月台数量、今日预约车辆、当前作业车辆) */
|
||||||
|
List<RealPlatformFloorStatPO> queryPlatformFloorStats();
|
||||||
|
|
||||||
|
/** 月台管理列表(YMS qms_window_info,含机构/鹤位/状态/楼层/UUID 等完整信息) */
|
||||||
|
List<RealPlatformInfoPO> queryPlatformInfoList();
|
||||||
}
|
}
|
||||||
+20
@@ -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;
|
||||||
|
}
|
||||||
+26
@@ -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;
|
||||||
|
}
|
||||||
+59
@@ -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;
|
||||||
|
}
|
||||||
+160
-24
@@ -1,22 +1,31 @@
|
|||||||
package com.mhd.bi.domain.biPlatformRealData.service;
|
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.mapper.BiPlatformRealDataMapper;
|
||||||
import com.mhd.bi.domain.biPlatformRealData.repository.po.RealPlatformVehiclePO;
|
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.RealPlatformOverviewPO;
|
||||||
import com.mhd.bi.domain.biPlatformRealData.repository.po.RealVehicleStatusPO;
|
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.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.domain.biSnapshotSync.support.BiSnapshotWeeklyRandomDataBuilder;
|
||||||
import com.mhd.bi.interfaces.facadeApi.biReport.vo.platform.PlatformSurveillanceVO;
|
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.PlatformVehicleItemVO;
|
||||||
import com.mhd.bi.interfaces.facadeApi.biReport.vo.platform.TaskLoadItemVO;
|
import com.mhd.bi.interfaces.facadeApi.biReport.vo.platform.TaskLoadItemVO;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import javax.annotation.Resource;
|
import javax.annotation.Resource;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
import java.util.Random;
|
import java.util.Random;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -30,6 +39,96 @@ public class BiPlatformRealDataService {
|
|||||||
@Resource
|
@Resource
|
||||||
private BiPlatformRealDataMapper biPlatformRealDataMapper;
|
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() {
|
public List<RealPlatformVehiclePO> queryVehicles() {
|
||||||
try {
|
try {
|
||||||
return biPlatformRealDataMapper.queryVehicles();
|
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() {
|
public PlatformSurveillanceVO buildRealTimePlatformSurveillance() {
|
||||||
Random r = new Random();
|
// 固定种子生成写死模板:仅用于没有真实数据源的字段
|
||||||
|
Random r = new Random(20260825L);
|
||||||
PlatformSurveillanceVO template = BiSnapshotWeeklyRandomDataBuilder.platformSurveillance(r);
|
PlatformSurveillanceVO template = BiSnapshotWeeklyRandomDataBuilder.platformSurveillance(r);
|
||||||
|
|
||||||
// 作业车辆列表
|
// 作业车辆列表:真实数据源,无条件覆盖(真实为空则返回空列表,不用模板模拟)
|
||||||
List<RealPlatformVehiclePO> realVehicles = queryVehicles();
|
List<RealPlatformVehiclePO> realVehicles = queryVehicles();
|
||||||
List<PlatformVehicleItemVO> vehicleVos = new ArrayList<>();
|
List<PlatformVehicleItemVO> vehicleVos = new ArrayList<>();
|
||||||
for (RealPlatformVehiclePO rv : realVehicles) {
|
for (RealPlatformVehiclePO rv : realVehicles) {
|
||||||
@@ -127,52 +268,47 @@ public class BiPlatformRealDataService {
|
|||||||
v.setPlatformDuration(rv.getPlatformDuration() == null ? 0 : rv.getPlatformDuration().intValue());
|
v.setPlatformDuration(rv.getPlatformDuration() == null ? 0 : rv.getPlatformDuration().intValue());
|
||||||
vehicleVos.add(v);
|
vehicleVos.add(v);
|
||||||
}
|
}
|
||||||
if (!vehicleVos.isEmpty()) {
|
template.setVehicles(vehicleVos);
|
||||||
template.setVehicles(vehicleVos);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 月台实时概览
|
// 月台实时概览:真实数据源,无条件覆盖
|
||||||
RealPlatformOverviewPO ov = queryPlatformOverview();
|
|
||||||
if (template.getPlatformOverview() != null) {
|
if (template.getPlatformOverview() != null) {
|
||||||
template.getPlatformOverview().setTotalPlatforms(ov.getTotalPlatforms().intValue());
|
RealPlatformOverviewPO ov = queryPlatformOverview();
|
||||||
template.getPlatformOverview().setOperatingPlatforms(ov.getOperatingPlatforms().intValue());
|
|
||||||
int total = ov.getTotalPlatforms().intValue();
|
int total = ov.getTotalPlatforms().intValue();
|
||||||
int occ = ov.getOperatingPlatforms().intValue();
|
int occ = ov.getOperatingPlatforms().intValue();
|
||||||
double rate = total > 0 ? Math.round(((double) occ / total) * 100.0 * 100.0) / 100.0 : 0d;
|
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);
|
template.getPlatformOverview().setCurrentOccupancyRate(rate);
|
||||||
}
|
template.getPlatformOverview().setTodayTasks((int) countTodayTasks());
|
||||||
long todayTasks = countTodayTasks();
|
|
||||||
if (template.getPlatformOverview() != null) {
|
|
||||||
template.getPlatformOverview().setTodayTasks((int) todayTasks);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 作业车辆状态统计
|
// 作业车辆状态统计:真实数据源,无条件覆盖
|
||||||
RealVehicleStatusPO vs = queryVehicleStatusToday();
|
|
||||||
if (template.getVehicleStatus() != null) {
|
if (template.getVehicleStatus() != null) {
|
||||||
|
RealVehicleStatusPO vs = queryVehicleStatusToday();
|
||||||
template.getVehicleStatus().setTodayReservedVehicles(vs.getTodayReservedVehicles().intValue());
|
template.getVehicleStatus().setTodayReservedVehicles(vs.getTodayReservedVehicles().intValue());
|
||||||
template.getVehicleStatus().setNotCheckedIn(vs.getNotCheckedIn().intValue());
|
template.getVehicleStatus().setNotCheckedIn(vs.getNotCheckedIn().intValue());
|
||||||
template.getVehicleStatus().setOperatingVehicles(vs.getOperatingVehicles().intValue());
|
template.getVehicleStatus().setOperatingVehicles(vs.getOperatingVehicles().intValue());
|
||||||
template.getVehicleStatus().setCompletedVehicles(vs.getCompletedVehicles().intValue());
|
template.getVehicleStatus().setCompletedVehicles(vs.getCompletedVehicles().intValue());
|
||||||
}
|
}
|
||||||
|
|
||||||
// 本月任务统计 taskTotal
|
// 本月任务统计:taskTotal 有真实数据源无条件覆盖;YoY 等无真实数据源保留模板写死值
|
||||||
long monthTasks = countMonthTasks();
|
|
||||||
if (template.getMonthlyTaskStatistics() != null) {
|
if (template.getMonthlyTaskStatistics() != null) {
|
||||||
template.getMonthlyTaskStatistics().setTaskTotal((int) monthTasks);
|
template.getMonthlyTaskStatistics().setTaskTotal((int) countMonthTasks());
|
||||||
}
|
}
|
||||||
|
|
||||||
// 任务负荷(今日 24 小时)
|
// 任务负荷:有真实数据源;真实为空则清空不用模板模拟,有数据则按真实小时覆盖
|
||||||
List<RealTaskLoadPO> loads = queryTaskLoadToday();
|
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<>();
|
Map<Integer, Long> hourMap = new HashMap<>();
|
||||||
for (RealTaskLoadPO l : loads) {
|
for (RealTaskLoadPO l : loads) {
|
||||||
hourMap.put(l.getHourOfDay(), l.getTaskCount());
|
hourMap.put(l.getHourOfDay(), l.getTaskCount());
|
||||||
}
|
}
|
||||||
for (TaskLoadItemVO item : template.getTaskLoadStatistics()) {
|
for (TaskLoadItemVO item : template.getTaskLoadStatistics()) {
|
||||||
Integer hour = parseHourFromLabel(item.getTime());
|
Integer hour = parseHourFromLabel(item.getTime());
|
||||||
if (hour != null) {
|
if (hour != null && hourMap.containsKey(hour)) {
|
||||||
Long n = hourMap.get(hour);
|
item.setTaskCount(hourMap.get(hour).intValue());
|
||||||
if (n != null) item.setTaskCount(n.intValue());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+2
@@ -13,6 +13,8 @@ public interface BiSnapshotWriteMapper {
|
|||||||
|
|
||||||
int insertPlatformSurveillanceSnapshot(@Param("payloadJson") String payloadJson, @Param("remark") String remark);
|
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 insertOverallOperationSnapshot(@Param("payloadJson") String payloadJson, @Param("remark") String remark);
|
||||||
|
|
||||||
int insertWarehouseOperationZoneSnapshot(@Param("payloadJson") String payloadJson, @Param("remark") String remark);
|
int insertWarehouseOperationZoneSnapshot(@Param("payloadJson") String payloadJson, @Param("remark") String remark);
|
||||||
|
|||||||
+2
-1
@@ -150,7 +150,8 @@ public class BiReportSnapshotSyncService {
|
|||||||
|
|
||||||
/** TODO 业务落地:部分字段改为真实数据,其余仍写死。 */
|
/** TODO 业务落地:部分字段改为真实数据,其余仍写死。 */
|
||||||
protected PlatformSurveillanceVO buildPlatformSurveillanceSnapshot() {
|
protected PlatformSurveillanceVO buildPlatformSurveillanceSnapshot() {
|
||||||
Random r = new Random();
|
// 固定种子,保证写死字段每次生成值一致(真实字段随后实时覆盖)
|
||||||
|
Random r = new Random(20260825L);
|
||||||
// 写死数据复用原 builder
|
// 写死数据复用原 builder
|
||||||
PlatformSurveillanceVO template = BiSnapshotWeeklyRandomDataBuilder.platformSurveillance(r);
|
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.biComprehensive.service.BiComprehensiveReportService;
|
||||||
import com.mhd.bi.domain.biPlatformSurveillance.service.BiPlatformSurveillanceReportService;
|
import com.mhd.bi.domain.biPlatformSurveillance.service.BiPlatformSurveillanceReportService;
|
||||||
import com.mhd.bi.domain.biPlatformRealData.service.BiPlatformRealDataService;
|
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.biOverallOperation.service.BiOverallOperationReportService;
|
||||||
import com.mhd.bi.domain.biHkMacaoServiceZone.service.BiHkMacaoServiceZoneReportService;
|
import com.mhd.bi.domain.biHkMacaoServiceZone.service.BiHkMacaoServiceZoneReportService;
|
||||||
import com.mhd.bi.domain.biTransportOperationZone.service.BiTransportOperationZoneReportService;
|
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.RequestParam;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* BI 报表接口
|
* 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")
|
@GetMapping("/platformSurveillance")
|
||||||
public BiApiResult<PlatformSurveillanceVO> 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);
|
return BiApiResult.ok(data);
|
||||||
}
|
}
|
||||||
//园区物流大屏接口----------------------------------------------------------------------
|
//园区物流大屏接口----------------------------------------------------------------------
|
||||||
|
|||||||
@@ -16,17 +16,17 @@ spring:
|
|||||||
# 线上测试环境配置 容器名+端口号
|
# 线上测试环境配置 容器名+端口号
|
||||||
# server-addr: 10.33.0.129:6010
|
# server-addr: 10.33.0.129:6010
|
||||||
# server-addr: 10.102.192.5:6848
|
# server-addr: 10.102.192.5:6848
|
||||||
username: nacos
|
|
||||||
password: manhuoda@2023
|
|
||||||
#线上正式环境
|
|
||||||
server-addr: 10.102.192.30:6848
|
|
||||||
# username: nacos
|
# username: nacos
|
||||||
# password: manhuoda@2023
|
# password: manhuoda@2023
|
||||||
|
#线上正式环境
|
||||||
|
server-addr: 10.102.192.31:6848
|
||||||
|
username: nacos
|
||||||
|
password: manhuoda@2023
|
||||||
config:
|
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.33.0.129:6010
|
||||||
server-addr: 10.102.192.30:6848
|
server-addr: 10.102.192.31:6848
|
||||||
username: nacos
|
username: nacos
|
||||||
password: manhuoda@2023
|
password: manhuoda@2023
|
||||||
#线上正式环境
|
#线上正式环境
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||||||
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||||
<mapper namespace="com.mhd.bi.domain.biPlatformRealData.repository.mapper.BiPlatformRealDataMapper">
|
<mapper namespace="com.mhd.bi.domain.biPlatformRealData.repository.mapper.BiPlatformRealDataMapper">
|
||||||
|
|
||||||
<!-- 作业车辆状态列表(vehicles):
|
<!-- 作业车辆状态列表(vehicles):仅查询预约日期是今天(TARGET_DATE)的车辆
|
||||||
plateNumber <- qms_main_business.CAR_NO
|
plateNumber <- qms_main_business.CAR_NO
|
||||||
status <- qms_main_business.FLOW_STATUS
|
status <- qms_main_business.FLOW_STATUS
|
||||||
platformId <- qms_main_business.WINDOW_NAME(叫号月台,主业务表直接有)
|
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
|
FROM NGWL_TEST_YMS.QMS_MAIN_BUSINESS mb
|
||||||
LEFT JOIN NGWL_TEST_YMS.QMS_WINDOW_INFO wi
|
LEFT JOIN NGWL_TEST_YMS.QMS_WINDOW_INFO wi
|
||||||
ON wi.WINDOW_NAME = mb.WINDOW_NAME
|
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
|
ORDER BY mb.CREATE_TIME DESC
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
@@ -46,11 +48,12 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||||||
AND TRUNC(ENTRY_TIME) = TRUNC(SYSDATE)
|
AND TRUNC(ENTRY_TIME) = TRUNC(SYSDATE)
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<!-- 车辆状态统计:今日的统计(按创建时间 / 流程状态)
|
<!-- 车辆状态统计:今日的统计(按创建时间 CREATE_TIME / 流程状态)
|
||||||
|
todayReservedVehicles 改为按预约日期 TARGET_DATE(其它 3 项保持 CREATE_TIME)
|
||||||
FLOW_STATUS: 1已预约 2已签到 3已叫号 4已过号 5已进场 6已称重 7已装车 8二次称重 9已结算 10已还卡 11已出场 12已取消 -->
|
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 id="queryVehicleStatusToday" resultType="com.mhd.bi.domain.biPlatformRealData.repository.po.RealVehicleStatusPO">
|
||||||
SELECT
|
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 = '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 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
|
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')
|
AND TO_CHAR(ENTRY_TIME, 'YYYY-MM') = TO_CHAR(SYSDATE, 'YYYY-MM')
|
||||||
</select>
|
</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_INFO;todayReservedVehicles/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) 统计 -->
|
<!-- 任务负荷(今日 24 小时时段):按 EXTRACT(HOUR FROM ENTRY_TIME) 统计 -->
|
||||||
<select id="queryTaskLoadToday" resultType="com.mhd.bi.domain.biPlatformRealData.repository.po.RealTaskLoadPO">
|
<select id="queryTaskLoadToday" resultType="com.mhd.bi.domain.biPlatformRealData.repository.po.RealTaskLoadPO">
|
||||||
SELECT
|
SELECT
|
||||||
@@ -76,4 +113,34 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||||||
GROUP BY EXTRACT(HOUR FROM ENTRY_TIME)
|
GROUP BY EXTRACT(HOUR FROM ENTRY_TIME)
|
||||||
ORDER BY EXTRACT(HOUR FROM ENTRY_TIME)
|
ORDER BY EXTRACT(HOUR FROM ENTRY_TIME)
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
|
<!-- 月台管理列表:查询 YMS qms_window_info 月台/鹤位窗口
|
||||||
|
机构名称 deptName <- sys_dept.DEPT_NAME(d.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>
|
</mapper>
|
||||||
@@ -18,6 +18,15 @@
|
|||||||
values (#{payloadJson}, SYSDATE, SYSDATE, 1, #{remark})
|
values (#{payloadJson}, SYSDATE, SYSDATE, 1, #{remark})
|
||||||
</insert>
|
</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 id="insertOverallOperationSnapshot">
|
||||||
insert into NGWL_TEST_BI.BI_OVERALL_OPERATION_SNAPSHOT (PAYLOAD_JSON, CREATE_TIME, UPDATE_TIME, DEL_FLAG, REMARK)
|
insert into NGWL_TEST_BI.BI_OVERALL_OPERATION_SNAPSHOT (PAYLOAD_JSON, CREATE_TIME, UPDATE_TIME, DEL_FLAG, REMARK)
|
||||||
values (#{payloadJson}, SYSDATE, SYSDATE, 1, #{remark})
|
values (#{payloadJson}, SYSDATE, SYSDATE, 1, #{remark})
|
||||||
|
|||||||
+3
@@ -66,6 +66,9 @@ public class ServiceNameConstants
|
|||||||
//YMS系统
|
//YMS系统
|
||||||
public static final String YMS_SERVICE = "mhd-yms-service";
|
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"),
|
STORE_ADMIN("店铺管理员", "store_admin"),
|
||||||
OWNER_CARGO("货主", "ownerCargo"),
|
OWNER_CARGO("货主", "ownerCargo"),
|
||||||
SUPPLIER("供应商", "supplier"),
|
SUPPLIER("供应商", "supplier"),
|
||||||
CUSTOMER("客户", "customer");
|
CUSTOMER("客户", "customer"),
|
||||||
|
TENANT("租户", "tenant");
|
||||||
|
|
||||||
private String name;
|
private String name;
|
||||||
private String code;
|
private String code;
|
||||||
|
|||||||
+1
-1
@@ -26,7 +26,7 @@ import java.io.InputStream;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
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 {
|
public interface ThirdPartyServiceFeign {
|
||||||
|
|
||||||
//查询当前组织所有三方接口信息
|
//查询当前组织所有三方接口信息
|
||||||
|
|||||||
+4
-2
@@ -88,7 +88,8 @@ public class GlobalExceptionHandler
|
|||||||
{
|
{
|
||||||
String requestURI = request.getRequestURI();
|
String requestURI = request.getRequestURI();
|
||||||
log.error("请求地址'{}',发生未知异常.", requestURI, e);
|
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();
|
String requestURI = request.getRequestURI();
|
||||||
log.error("请求地址'{}',发生系统异常.", requestURI, e);
|
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");
|
properties.setProperty(KAPTCHA_SESSION_CONFIG_KEY, "kaptchaCode");
|
||||||
// 验证码文本字符长度 默认为5
|
// 验证码文本字符长度 默认为5
|
||||||
properties.setProperty(KAPTCHA_TEXTPRODUCER_CHAR_LENGTH, "4");
|
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)
|
// 验证码文本字体样式 默认为new Font("Arial", 1, fontSize), new Font("Courier", 1, fontSize)
|
||||||
properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_NAMES, "Arial,Courier");
|
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
|
// 图片样式 水纹com.google.code.kaptcha.impl.WaterRipple 鱼眼com.google.code.kaptcha.impl.FishEyeGimpy 阴影com.google.code.kaptcha.impl.ShadowGimpy
|
||||||
|
|||||||
@@ -14,23 +14,16 @@ spring:
|
|||||||
nacos:
|
nacos:
|
||||||
discovery:
|
discovery:
|
||||||
server-addr: 127.0.0.1:8848
|
server-addr: 127.0.0.1:8848
|
||||||
# 线上测试环境配置 容器名+端口号
|
# server-addr: 10.102.192.30:6848
|
||||||
# server-addr: 10.102.192.31:6848
|
|
||||||
# server-addr: 10.33.0.129:6010
|
# server-addr: 10.33.0.129:6010
|
||||||
#线上正式环境
|
username: nacos
|
||||||
# server-addr: 127.0.0.1:8848
|
password: manhuoda@2023
|
||||||
# server-addr: 10.102.192.31:6848
|
|
||||||
# username: nacos
|
|
||||||
# password: manhuoda@2023
|
|
||||||
config:
|
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.33.0.129:6010
|
||||||
#线上正式环境
|
# server-addr: 10.102.192.30:6848
|
||||||
# server-addr: 127.0.0.1:8848
|
username: nacos
|
||||||
# server-addr: 10.102.192.31:6848
|
password: manhuoda@2023
|
||||||
# username: nacos
|
|
||||||
# password: manhuoda@2023
|
|
||||||
group: DEFAULT_GROUP # 默认分组就是DEFAULT_GROUP,如果使用默认分组可以不配置
|
group: DEFAULT_GROUP # 默认分组就是DEFAULT_GROUP,如果使用默认分组可以不配置
|
||||||
file-extension: yml #默认properties
|
file-extension: yml #默认properties
|
||||||
# 共享配置
|
# 共享配置
|
||||||
|
|||||||
+1
-1
@@ -278,7 +278,7 @@ public class LeaseApplicationService {
|
|||||||
* 货主租赁明细
|
* 货主租赁明细
|
||||||
* 定时任务:每天凌晨1点执行,计算前一天的货主租赁明细
|
* 定时任务:每天凌晨1点执行,计算前一天的货主租赁明细
|
||||||
*/
|
*/
|
||||||
@Scheduled(cron = "0 0 1 * * ?")
|
// @Scheduled(cron = "0 0 1 * * ?")
|
||||||
public void shipperLeaseDetails() {
|
public void shipperLeaseDetails() {
|
||||||
log.info("开始执行货主租赁明细定时任务");
|
log.info("开始执行货主租赁明细定时任务");
|
||||||
// 计算前一天的日期,将时间设置为当天的0点0分0秒,只保留日期部分
|
// 计算前一天的日期,将时间设置为当天的0点0分0秒,只保留日期部分
|
||||||
|
|||||||
+23
-1
@@ -461,7 +461,8 @@ public class MessageManageApplicationService
|
|||||||
//获取当前登录人
|
//获取当前登录人
|
||||||
LoginUser loginUser = SecurityUtils.getLoginUser();
|
LoginUser loginUser = SecurityUtils.getLoginUser();
|
||||||
if(null == loginUser){
|
if(null == loginUser){
|
||||||
throw new ServiceException("获取登录人失败");
|
//未登录(游客):返回空消息中心结构,保证小程序界面正常展示而不报错
|
||||||
|
return buildEmptyAppNoticeList();
|
||||||
}
|
}
|
||||||
noticeMessageLoggingDo.setUserId(loginUser.getUserid());
|
noticeMessageLoggingDo.setUserId(loginUser.getUserid());
|
||||||
//获取每种类型的最新一条
|
//获取每种类型的最新一条
|
||||||
@@ -522,6 +523,27 @@ public class MessageManageApplicationService
|
|||||||
return result;
|
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根据类型获取消息通知
|
* APP根据类型获取消息通知
|
||||||
* @param noticeMessageLoggingDo
|
* @param noticeMessageLoggingDo
|
||||||
|
|||||||
+66
-9
@@ -13,6 +13,7 @@ import com.mhd.basic.application.service.contractManage.ContractManageApplicatio
|
|||||||
import com.mhd.basic.domain.expenseAccount.service.ExpenseAccountDomainService;
|
import com.mhd.basic.domain.expenseAccount.service.ExpenseAccountDomainService;
|
||||||
import com.mhd.basic.infrastructure.feign.vo.SysDictDataVo;
|
import com.mhd.basic.infrastructure.feign.vo.SysDictDataVo;
|
||||||
import com.mhd.basic.interfaces.dto.expenseAccount.ExpenseAccountDTO;
|
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.basic.interfaces.dto.expenseAccount.SearchExpenseAccountDTO;
|
||||||
import com.mhd.common.core.enums.DictCode;
|
import com.mhd.common.core.enums.DictCode;
|
||||||
import com.mhd.common.core.exception.ServiceException;
|
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.system.api.model.LoginUser;
|
||||||
import com.mhd.common.core.domain.po.UserPo;
|
import com.mhd.common.core.domain.po.UserPo;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.beans.BeanUtils;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
@@ -378,6 +380,55 @@ public class ExpenseAccountApplicationService {
|
|||||||
return expenseAccountDomainService.getInfo(expenseAccountId);
|
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
|
* @param expenseAccountDTO
|
||||||
@@ -594,22 +645,28 @@ public class ExpenseAccountApplicationService {
|
|||||||
// unit.add(expenseAccountPO.getBillingUnit2() + "-" + expenseAccountPO.getBillingUnitEn2());
|
// unit.add(expenseAccountPO.getBillingUnit2() + "-" + expenseAccountPO.getBillingUnitEn2());
|
||||||
// expenseAccountPO.setUnit(unit);
|
// expenseAccountPO.setUnit(unit);
|
||||||
// }
|
// }
|
||||||
// 组装计费单位列表(数组)
|
// 组装计费单位列表(数组),中文和英文都为null时过滤掉,避免出现"null-null"
|
||||||
List<Map<String, String>> unit = new ArrayList<>();
|
List<Map<String, String>> unit = new ArrayList<>();
|
||||||
if (expenseAccountPO != null) {
|
if (expenseAccountPO != null) {
|
||||||
Map<String, String> unit1 = new HashMap<>();
|
addUnitIfPresent(unit, expenseAccountPO.getBillingUnit(), expenseAccountPO.getBillingUnitEn());
|
||||||
unit1.put("unit", expenseAccountPO.getBillingUnit() + "-" + expenseAccountPO.getBillingUnitEn());
|
addUnitIfPresent(unit, expenseAccountPO.getBillingUnit2(), expenseAccountPO.getBillingUnitEn2());
|
||||||
unit.add(unit1);
|
|
||||||
|
|
||||||
Map<String, String> unit2 = new HashMap<>();
|
|
||||||
unit2.put("unit", expenseAccountPO.getBillingUnit2() + "-" + expenseAccountPO.getBillingUnitEn2());
|
|
||||||
unit.add(unit2);
|
|
||||||
|
|
||||||
expenseAccountPO.setUnit(unit);
|
expenseAccountPO.setUnit(unit);
|
||||||
}
|
}
|
||||||
return expenseAccountPO;
|
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){
|
public List<QueryExpenseAccountPO> selectExpenseAccount(SearchExpenseAccountDO searchExpenseAccountDO){
|
||||||
setDataPermissionForSearch(searchExpenseAccountDO);
|
setDataPermissionForSearch(searchExpenseAccountDO);
|
||||||
return expenseAccountDomainService.selectExpenseAccount(searchExpenseAccountDO);
|
return expenseAccountDomainService.selectExpenseAccount(searchExpenseAccountDO);
|
||||||
|
|||||||
+34
@@ -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.po.StorageLocationPO;
|
||||||
import com.mhd.basic.domain.storageLocation.repository.todo.StorageLocationDO;
|
import com.mhd.basic.domain.storageLocation.repository.todo.StorageLocationDO;
|
||||||
import com.mhd.basic.domain.storageLocation.service.StorageLocationDomainService;
|
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.domain.storageSection.repository.todo.StorageSectionDO;
|
||||||
import com.mhd.basic.infrastructure.feign.vo.SysDictDataVo;
|
import com.mhd.basic.infrastructure.feign.vo.SysDictDataVo;
|
||||||
import com.mhd.common.core.domain.po.UserPo;
|
import com.mhd.common.core.domain.po.UserPo;
|
||||||
@@ -207,6 +208,39 @@ public class StorageLocationApplicationService {
|
|||||||
return storageLocationDomainService.getInfo(storageLocationId);
|
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)
|
public int getStorageLocationCount(StorageLocationDO storageLocationDO)
|
||||||
{
|
{
|
||||||
return storageLocationDomainService.getStorageLocationCount(storageLocationDO);
|
return storageLocationDomainService.getStorageLocationCount(storageLocationDO);
|
||||||
|
|||||||
+23
-2
@@ -21,6 +21,7 @@ import com.mhd.system.service.ISysDictTypeService;
|
|||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
import java.math.BigDecimal;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
/**
|
/**
|
||||||
* 库区ApplicationService
|
* 库区ApplicationService
|
||||||
@@ -42,11 +43,32 @@ public class StorageSectionApplicationService {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
public List<StorageSectionPO> queryList(StorageSectionDO storageSectionDO) {
|
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();
|
LoginUser loginUser = SecurityUtils.getLoginUser();
|
||||||
if (loginUser != null && loginUser.getUserPo() != null) {
|
if (loginUser != null && loginUser.getUserPo() != null) {
|
||||||
Long userOrganizationId = loginUser.getUserPo().getOrganizationId();
|
Long userOrganizationId = loginUser.getUserPo().getOrganizationId();
|
||||||
Long frontendOrganizationId = storageSectionDO.getOrganizationId(); // 前端传递的 organizationId
|
Long frontendOrganizationId = storageSectionDO.getOrganizationId(); // 前端传递的 organizationId
|
||||||
|
|
||||||
// 数据权限:根据 organizationId 来设置查询权限
|
// 数据权限:根据 organizationId 来设置查询权限
|
||||||
// 当前登录用户的 organizationId 为 2827 时,可查全部组织数据;其他组织只能查自己
|
// 当前登录用户的 organizationId 为 2827 时,可查全部组织数据;其他组织只能查自己
|
||||||
if (userOrganizationId != null && userOrganizationId == 2827L) {
|
if (userOrganizationId != null && userOrganizationId == 2827L) {
|
||||||
@@ -91,7 +113,6 @@ public class StorageSectionApplicationService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return storageSectionDomainService.queryList(storageSectionDO);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+12
@@ -126,6 +126,18 @@ public class ContractManage extends BaseVOEntity{
|
|||||||
@Excel(name = "其他附件地址")
|
@Excel(name = "其他附件地址")
|
||||||
private String otherAttachmentsUrl;
|
private String otherAttachmentsUrl;
|
||||||
|
|
||||||
|
@ApiModelProperty("合同正本附件原始文件名")
|
||||||
|
private String contractOriginalName;
|
||||||
|
|
||||||
|
@ApiModelProperty("廉政协议附件原始文件名")
|
||||||
|
private String integrityAgreementName;
|
||||||
|
|
||||||
|
@ApiModelProperty("安全协议附件原始文件名")
|
||||||
|
private String securityProtocolName;
|
||||||
|
|
||||||
|
@ApiModelProperty("其他附件原始文件名(多文件时与 otherAttachmentsUrl 同分隔符、同顺序)")
|
||||||
|
private String otherAttachmentsName;
|
||||||
|
|
||||||
@ApiModelProperty("我司签订人")
|
@ApiModelProperty("我司签订人")
|
||||||
@Excel(name = "我司签订人")
|
@Excel(name = "我司签订人")
|
||||||
private String ourSinged;
|
private String ourSinged;
|
||||||
|
|||||||
+12
@@ -124,6 +124,18 @@ public class ContractManagePO extends BaseVOEntity{
|
|||||||
@Excel(name = "其他附件地址")
|
@Excel(name = "其他附件地址")
|
||||||
private String otherAttachmentsUrl;
|
private String otherAttachmentsUrl;
|
||||||
|
|
||||||
|
@ApiModelProperty("合同正本附件原始文件名")
|
||||||
|
private String contractOriginalName;
|
||||||
|
|
||||||
|
@ApiModelProperty("廉政协议附件原始文件名")
|
||||||
|
private String integrityAgreementName;
|
||||||
|
|
||||||
|
@ApiModelProperty("安全协议附件原始文件名")
|
||||||
|
private String securityProtocolName;
|
||||||
|
|
||||||
|
@ApiModelProperty("其他附件原始文件名(多文件时与 otherAttachmentsUrl 同分隔符、同顺序)")
|
||||||
|
private String otherAttachmentsName;
|
||||||
|
|
||||||
@ApiModelProperty("我司签订人")
|
@ApiModelProperty("我司签订人")
|
||||||
@Excel(name = "我司签订人")
|
@Excel(name = "我司签订人")
|
||||||
private String ourSinged;
|
private String ourSinged;
|
||||||
|
|||||||
+12
@@ -121,6 +121,18 @@ public class ContractManageDO extends BaseVOEntity{
|
|||||||
@Excel(name = "其他附件地址")
|
@Excel(name = "其他附件地址")
|
||||||
private String otherAttachmentsUrl;
|
private String otherAttachmentsUrl;
|
||||||
|
|
||||||
|
@ApiModelProperty("合同正本附件原始文件名")
|
||||||
|
private String contractOriginalName;
|
||||||
|
|
||||||
|
@ApiModelProperty("廉政协议附件原始文件名")
|
||||||
|
private String integrityAgreementName;
|
||||||
|
|
||||||
|
@ApiModelProperty("安全协议附件原始文件名")
|
||||||
|
private String securityProtocolName;
|
||||||
|
|
||||||
|
@ApiModelProperty("其他附件原始文件名(多文件时与 otherAttachmentsUrl 同分隔符、同顺序)")
|
||||||
|
private String otherAttachmentsName;
|
||||||
|
|
||||||
@ApiModelProperty("我司签订人")
|
@ApiModelProperty("我司签订人")
|
||||||
@Excel(name = "我司签订人")
|
@Excel(name = "我司签订人")
|
||||||
private String ourSinged;
|
private String ourSinged;
|
||||||
|
|||||||
+6
@@ -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.po.StorageSectionPO;
|
||||||
import com.mhd.basic.domain.storageSection.repository.todo.StorageSectionDO;
|
import com.mhd.basic.domain.storageSection.repository.todo.StorageSectionDO;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -20,6 +21,11 @@ public interface IStorageSectionService extends IService<StorageSection>
|
|||||||
*/
|
*/
|
||||||
public List<StorageSectionPO> queryList(StorageSectionDO storageSectionDO);
|
public List<StorageSectionPO> queryList(StorageSectionDO storageSectionDO);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 库区面积合计(与 queryList 相同筛选条件)
|
||||||
|
*/
|
||||||
|
public BigDecimal sumStorageRegion(StorageSectionDO storageSectionDO);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 新增库区
|
* 新增库区
|
||||||
*/
|
*/
|
||||||
|
|||||||
+6
@@ -1,5 +1,6 @@
|
|||||||
package com.mhd.basic.domain.storageSection.repository.mapper;
|
package com.mhd.basic.domain.storageSection.repository.mapper;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
import com.mhd.basic.domain.storageSection.entity.StorageSection;
|
import com.mhd.basic.domain.storageSection.entity.StorageSection;
|
||||||
@@ -19,4 +20,9 @@ public interface StorageSectionMapper extends BaseMapper<StorageSection>
|
|||||||
*/
|
*/
|
||||||
public List<StorageSectionPO> queryList(StorageSectionDO storageSectionDO);
|
public List<StorageSectionPO> queryList(StorageSectionDO storageSectionDO);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 库区面积合计(与 queryList 相同筛选条件)
|
||||||
|
*/
|
||||||
|
public BigDecimal sumStorageRegion(StorageSectionDO storageSectionDO);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+10
@@ -10,6 +10,7 @@ import org.springframework.beans.BeanUtils;
|
|||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@@ -33,6 +34,15 @@ public class StorageSectionImpl extends ServiceImpl<StorageSectionMapper, Storag
|
|||||||
return storageSectionMapper.queryList(storageSectionDO);
|
return storageSectionMapper.queryList(storageSectionDO);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 库区面积合计(与 queryList 相同筛选条件)
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public BigDecimal sumStorageRegion(StorageSectionDO storageSectionDO)
|
||||||
|
{
|
||||||
|
return storageSectionMapper.sumStorageRegion(storageSectionDO);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 新增库区
|
* 新增库区
|
||||||
*/
|
*/
|
||||||
|
|||||||
+8
@@ -21,6 +21,7 @@ import lombok.extern.slf4j.Slf4j;
|
|||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -53,6 +54,13 @@ public class StorageSectionDomainService {
|
|||||||
return storageSectionService.queryList(storageSectionDO);
|
return storageSectionService.queryList(storageSectionDO);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 库区面积合计(与 queryList 相同筛选条件)
|
||||||
|
*/
|
||||||
|
public BigDecimal sumStorageRegion(StorageSectionDO storageSectionDO) {
|
||||||
|
return storageSectionService.sumStorageRegion(storageSectionDO);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 新增库区
|
* 新增库区
|
||||||
|
|||||||
+12
@@ -121,6 +121,18 @@ public class ContractManageDTO extends BaseVOEntity{
|
|||||||
@Excel(name = "其他附件地址")
|
@Excel(name = "其他附件地址")
|
||||||
private String otherAttachmentsUrl;
|
private String otherAttachmentsUrl;
|
||||||
|
|
||||||
|
@ApiModelProperty("合同正本附件原始文件名")
|
||||||
|
private String contractOriginalName;
|
||||||
|
|
||||||
|
@ApiModelProperty("廉政协议附件原始文件名")
|
||||||
|
private String integrityAgreementName;
|
||||||
|
|
||||||
|
@ApiModelProperty("安全协议附件原始文件名")
|
||||||
|
private String securityProtocolName;
|
||||||
|
|
||||||
|
@ApiModelProperty("其他附件原始文件名(多文件时与 otherAttachmentsUrl 同分隔符、同顺序)")
|
||||||
|
private String otherAttachmentsName;
|
||||||
|
|
||||||
@ApiModelProperty("我司签订人")
|
@ApiModelProperty("我司签订人")
|
||||||
@Excel(name = "我司签订人")
|
@Excel(name = "我司签订人")
|
||||||
private String ourSinged;
|
private String ourSinged;
|
||||||
|
|||||||
+69
@@ -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;
|
||||||
|
}
|
||||||
+45
@@ -1,5 +1,7 @@
|
|||||||
package com.mhd.basic.interfaces.facade.expenseAccount;
|
package com.mhd.basic.interfaces.facade.expenseAccount;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.URLEncoder;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
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.domain.sysMultistageDict.repository.po.SysMultistageDictPo;
|
||||||
import com.mhd.basic.interfaces.assember.expenseAccount.ExpenseAccountAssembler;
|
import com.mhd.basic.interfaces.assember.expenseAccount.ExpenseAccountAssembler;
|
||||||
import com.mhd.basic.interfaces.dto.expenseAccount.ExpenseAccountDTO;
|
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.basic.interfaces.dto.expenseAccount.SearchExpenseAccountDTO;
|
||||||
import com.mhd.common.core.constant.HttpStatus;
|
import com.mhd.common.core.constant.HttpStatus;
|
||||||
import com.mhd.common.core.domain.entity.R;
|
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 com.sun.org.apache.bcel.internal.generic.NEW;
|
||||||
import io.swagger.annotations.ApiOperation;
|
import io.swagger.annotations.ApiOperation;
|
||||||
import org.apache.commons.lang.StringUtils;
|
import org.apache.commons.lang.StringUtils;
|
||||||
@@ -26,6 +30,7 @@ import org.springframework.http.ResponseEntity;
|
|||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
import javax.annotation.Resource;
|
import javax.annotation.Resource;
|
||||||
|
import javax.servlet.http.HttpServletResponse;
|
||||||
import com.mhd.common.core.web.controller.BaseController;
|
import com.mhd.common.core.web.controller.BaseController;
|
||||||
import com.mhd.common.core.web.domain.AjaxResult;
|
import com.mhd.common.core.web.domain.AjaxResult;
|
||||||
import com.mhd.common.core.web.page.TableDataInfo;
|
import com.mhd.common.core.web.page.TableDataInfo;
|
||||||
@@ -336,5 +341,45 @@ public class ExpenseAccountApi extends BaseController{
|
|||||||
return AjaxResult.success(expenseAccountPO.getUnit());
|
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
@@ -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.domain.storageLocation.repository.po.StorageLocationPO;
|
||||||
import com.mhd.basic.application.service.storageLocation.StorageLocationApplicationService;
|
import com.mhd.basic.application.service.storageLocation.StorageLocationApplicationService;
|
||||||
import com.mhd.basic.interfaces.assember.storageLocation.StorageLocationAssembler;
|
import com.mhd.basic.interfaces.assember.storageLocation.StorageLocationAssembler;
|
||||||
|
import com.mhd.common.core.exception.ServiceException;
|
||||||
import javax.annotation.Resource;
|
import javax.annotation.Resource;
|
||||||
import com.mhd.common.core.web.controller.BaseController;
|
import com.mhd.common.core.web.controller.BaseController;
|
||||||
import com.mhd.common.core.web.domain.AjaxResult;
|
import com.mhd.common.core.web.domain.AjaxResult;
|
||||||
@@ -127,6 +128,24 @@ public class StorageLocationApi extends BaseController{
|
|||||||
{
|
{
|
||||||
return AjaxResult.success(storageLocationApplicationService.getInfo(storageLocationId));
|
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("获取库位总数")
|
@ApiOperation("获取库位总数")
|
||||||
@PostMapping(value = "/getStorageLocationCount")
|
@PostMapping(value = "/getStorageLocationCount")
|
||||||
public AjaxResult getStorageLocationCount(@RequestBody StorageLocationDTO storageLocationDTO)
|
public AjaxResult getStorageLocationCount(@RequestBody StorageLocationDTO storageLocationDTO)
|
||||||
|
|||||||
+12
@@ -55,6 +55,18 @@ public class StorageSectionApi extends BaseController{
|
|||||||
return getDataTable(list);
|
return getDataTable(list);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 库区面积合计:与 /list 相同的筛选条件,返回库区面积总和
|
||||||
|
*/
|
||||||
|
@ApiOperation("查询库区面积合计(与库区列表相同筛选条件,SUM(库区面积))")
|
||||||
|
@GetMapping("/sumStorageRegion")
|
||||||
|
public AjaxResult sumStorageRegion(StorageSectionDTO storageSectionDTO)
|
||||||
|
{
|
||||||
|
//转换实体
|
||||||
|
StorageSectionDO storageSectionDO = storageSectionAssembler.toDO(storageSectionDTO);
|
||||||
|
return AjaxResult.success(storageSectionApplicationService.sumStorageRegion(storageSectionDO));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 分页查询库区列表
|
* 分页查询库区列表
|
||||||
*/
|
*/
|
||||||
|
|||||||
+14
-14
@@ -40,14 +40,14 @@ public class SysConfigController extends BaseController
|
|||||||
/**
|
/**
|
||||||
* 获取参数配置列表
|
* 获取参数配置列表
|
||||||
*/
|
*/
|
||||||
@RequiresPermissions("system:config:list")
|
// @RequiresPermissions("system:config:list")
|
||||||
@GetMapping("/list")
|
// @GetMapping("/list")
|
||||||
public TableDataInfo list(SysConfig config)
|
// public TableDataInfo list(SysConfig config)
|
||||||
{
|
// {
|
||||||
startPage();
|
// startPage();
|
||||||
List<SysConfig> list = configService.selectConfigList(config);
|
// List<SysConfig> list = configService.selectConfigList(config);
|
||||||
return getDataTable(list);
|
// return getDataTable(list);
|
||||||
}
|
// }
|
||||||
|
|
||||||
@Log(title = "参数管理", businessType = BusinessType.EXPORT)
|
@Log(title = "参数管理", businessType = BusinessType.EXPORT)
|
||||||
@RequiresPermissions("system:config:export")
|
@RequiresPermissions("system:config:export")
|
||||||
@@ -71,11 +71,11 @@ public class SysConfigController extends BaseController
|
|||||||
/**
|
/**
|
||||||
* 根据参数键名查询参数值
|
* 根据参数键名查询参数值
|
||||||
*/
|
*/
|
||||||
@GetMapping(value = "/configKey/{configKey}")
|
// @GetMapping(value = "/configKey/{configKey}")
|
||||||
public AjaxResult getConfigKey(@PathVariable String configKey)
|
// public AjaxResult getConfigKey(@PathVariable String configKey)
|
||||||
{
|
// {
|
||||||
return AjaxResult.success(configService.selectConfigByKey(configKey));
|
// return AjaxResult.success(configService.selectConfigByKey(configKey));
|
||||||
}
|
// }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 新增参数配置
|
* 新增参数配置
|
||||||
@@ -132,4 +132,4 @@ public class SysConfigController extends BaseController
|
|||||||
configService.resetConfigCache();
|
configService.resetConfigCache();
|
||||||
return AjaxResult.success();
|
return AjaxResult.success();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+4
-1
@@ -85,6 +85,9 @@ public class SysUserController extends BaseController
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
List<SysUser> list = userService.selectUserList(user);
|
List<SysUser> list = userService.selectUserList(user);
|
||||||
|
for (SysUser sysUser : list) {
|
||||||
|
sysUser.setPassword("******");
|
||||||
|
}
|
||||||
return getDataTable(list);
|
return getDataTable(list);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -323,4 +326,4 @@ public class SysUserController extends BaseController
|
|||||||
userService.insertUserAuth(userId, roleIds);
|
userService.insertUserAuth(userId, roleIds);
|
||||||
return success();
|
return success();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -14,16 +14,16 @@ spring:
|
|||||||
nacos:
|
nacos:
|
||||||
discovery:
|
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.33.0.129:6010
|
||||||
# server-addr: 10.102.192.30:6848
|
server-addr: 10.102.192.31:6848
|
||||||
username: nacos
|
username: nacos
|
||||||
password: manhuoda@2023
|
password: manhuoda@2023
|
||||||
#线上正式环境
|
#线上正式环境
|
||||||
# server-addr: 10.102.192.105:6848
|
# server-addr: 10.102.192.105:6848
|
||||||
config:
|
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.33.0.129:6010
|
||||||
# server-addr: 10.102.192.30:6848
|
server-addr: 10.102.192.31:6848
|
||||||
username: nacos
|
username: nacos
|
||||||
password: manhuoda@2023
|
password: manhuoda@2023
|
||||||
#线上正式环境
|
#线上正式环境
|
||||||
|
|||||||
+8
@@ -102,4 +102,12 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||||||
<include refid="selectStorageSectionPo1"/>
|
<include refid="selectStorageSectionPo1"/>
|
||||||
order by a.storage_name asc, a.create_time desc
|
order by a.storage_name asc, a.create_time desc
|
||||||
</select>
|
</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>
|
</mapper>
|
||||||
+8
-2
@@ -129,8 +129,14 @@ public class CommonApplicationService {
|
|||||||
* @param userDO
|
* @param userDO
|
||||||
*/
|
*/
|
||||||
public void checkAccountInfo(UserDO userDO) {
|
public void checkAccountInfo(UserDO userDO) {
|
||||||
//获取当前登录用户的最高组织
|
//获取当前登录用户的最高组织,远程Feign调用无loginUser时从userDO取
|
||||||
Long topOrganizationId = SecurityUtils.getLoginUser().getUserPo().getTopOrganizationId();
|
Long topOrganizationId;
|
||||||
|
LoginUser loginUser = SecurityUtils.getLoginUser();
|
||||||
|
if (ObjectUtil.isNotNull(loginUser)) {
|
||||||
|
topOrganizationId = loginUser.getUserPo().getTopOrganizationId();
|
||||||
|
} else {
|
||||||
|
topOrganizationId = userDO.getTopOrganizationId();
|
||||||
|
}
|
||||||
|
|
||||||
checkUserInfo(topOrganizationId, userDO);
|
checkUserInfo(topOrganizationId, userDO);
|
||||||
}
|
}
|
||||||
|
|||||||
+179
-37
@@ -735,14 +735,20 @@ public class UserApplicationService {
|
|||||||
*/
|
*/
|
||||||
public void addUserInfo(UserDO userDO) {
|
public void addUserInfo(UserDO userDO) {
|
||||||
LoginUser loginUser = SecurityUtils.getLoginUser();
|
LoginUser loginUser = SecurityUtils.getLoginUser();
|
||||||
if (ObjectUtil.isNull(loginUser)) {
|
UserPo loginUserPo = null;
|
||||||
throw new DigitalLogisticsException(UserError.USER_NULL);
|
if (ObjectUtil.isNotNull(loginUser)) {
|
||||||
|
loginUserPo = loginUser.getUserPo();
|
||||||
}
|
}
|
||||||
UserPo loginUserPo = loginUser.getUserPo();
|
|
||||||
CheckDO checkDO = userFactory.getCheckDo(userDO);
|
CheckDO checkDO = userFactory.getCheckDo(userDO);
|
||||||
//用户密码传值
|
//用户密码传值
|
||||||
if (StringUtils.isNotEmpty(userDO.getUserPassword())) {
|
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);
|
checkDO.setUserPassword(userPassword);
|
||||||
}
|
}
|
||||||
//用户信息格式校验
|
//用户信息格式校验
|
||||||
@@ -750,9 +756,15 @@ public class UserApplicationService {
|
|||||||
String salt = OConvertUtils.randomGen(8);
|
String salt = OConvertUtils.randomGen(8);
|
||||||
userDO.setUserSalt(salt);
|
userDO.setUserSalt(salt);
|
||||||
regCheck(checkDO);
|
regCheck(checkDO);
|
||||||
//用户密码传值
|
//用户密码加密入库
|
||||||
if (StringUtils.isNotEmpty(userDO.getUserPassword())) {
|
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,
|
//加密算法,PBEWithMD5AndDES,
|
||||||
String passwordEncode = PasswordUtil.encrypt(userDO.getUserAccount(), userPassword, salt);
|
String passwordEncode = PasswordUtil.encrypt(userDO.getUserAccount(), userPassword, salt);
|
||||||
userDO.setUserPassword(passwordEncode);
|
userDO.setUserPassword(passwordEncode);
|
||||||
@@ -783,11 +795,13 @@ public class UserApplicationService {
|
|||||||
String idCardDes = DesensitizedUtil.desensitizedIdNumber(idCard);
|
String idCardDes = DesensitizedUtil.desensitizedIdNumber(idCard);
|
||||||
userDO.setUserIdcardNumberDes(idCardDes);
|
userDO.setUserIdcardNumberDes(idCardDes);
|
||||||
}
|
}
|
||||||
userDO.setCreateBy(loginUserPo.getUserId());
|
if (ObjectUtil.isNotNull(loginUserPo)) {
|
||||||
userDO.setCreateByName(loginUserPo.getUserName());
|
userDO.setCreateBy(loginUserPo.getUserId());
|
||||||
if (loginUserPo.getUserAccountType() != null && loginUserPo.getUserAccountType() == 5) {
|
userDO.setCreateByName(loginUserPo.getUserName());
|
||||||
userDO.setCreateBy(loginUserPo.getCreateBy());
|
if (loginUserPo.getUserAccountType() != null && loginUserPo.getUserAccountType() == 5) {
|
||||||
userDO.setCreateByName(loginUserPo.getCreateByName());
|
userDO.setCreateBy(loginUserPo.getCreateBy());
|
||||||
|
userDO.setCreateByName(loginUserPo.getCreateByName());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
userDO.setCreateTime(new Date());
|
userDO.setCreateTime(new Date());
|
||||||
//用户状态:0-无状态,1-正常,2-锁定
|
//用户状态:0-无状态,1-正常,2-锁定
|
||||||
@@ -1379,6 +1393,66 @@ public class UserApplicationService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 远程Feign调用新增用户(无token),仅操作USER表基本字段
|
||||||
|
* 调用方需传入:organizationId、userAccount、userPassword(明文)、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 新增用户
|
* @Description 新增用户
|
||||||
* @Author Alex
|
* @Author Alex
|
||||||
@@ -1388,18 +1462,31 @@ public class UserApplicationService {
|
|||||||
public UserPo addUserYG(UserDO userDO) {
|
public UserPo addUserYG(UserDO userDO) {
|
||||||
LoginUser loginUser = SecurityUtils.getLoginUser();
|
LoginUser loginUser = SecurityUtils.getLoginUser();
|
||||||
String password = userDO.getPlainPassword();
|
String password = userDO.getPlainPassword();
|
||||||
if (ObjectUtil.isNull(loginUser)) {
|
// 远程Feign调用时可能无loginUser,优先从loginUser取组织信息,否则使用userDO自身携带的数据
|
||||||
throw new DigitalLogisticsException(UserError.TIMEOUT);
|
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修改 增加登录账号/手机号唯一性校验
|
//2023年3月24日 bug修改 增加登录账号/手机号唯一性校验
|
||||||
commonApplicationService.checkAccountInfo(userDO);
|
commonApplicationService.checkAccountInfo(userDO);
|
||||||
if (ObjectUtil.isNotNull(userDO.getUserId())) {
|
if (ObjectUtil.isNotNull(userDO.getUserId())) {
|
||||||
return updateUser(userDO);
|
return updateUser(userDO);
|
||||||
} else {
|
} else {
|
||||||
if (ObjectUtil.isNull(userDO.getOrganizationId())) {
|
if (ObjectUtil.isNull(userDO.getOrganizationId())) {
|
||||||
userDO.setOrganizationId(loginUser.getUserPo().getOrganizationId());
|
if (ObjectUtil.isNotNull(loginUser)) {
|
||||||
userDO.setOrganizationName(loginUser.getUserPo().getOrganizationName());
|
userDO.setOrganizationId(loginUser.getUserPo().getOrganizationId());
|
||||||
|
userDO.setOrganizationName(loginUser.getUserPo().getOrganizationName());
|
||||||
|
} else {
|
||||||
|
throw new DigitalLogisticsException(UserError.TIMEOUT);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// 如果前端传递了 organizationId,根据 organizationId 查询组织信息,获取正确的组织名称
|
// 如果前端传递了 organizationId,根据 organizationId 查询组织信息,获取正确的组织名称
|
||||||
AjaxResult orgInfoResult = productServiceFeign.getInfo(userDO.getOrganizationId());
|
AjaxResult orgInfoResult = productServiceFeign.getInfo(userDO.getOrganizationId());
|
||||||
@@ -1441,7 +1528,14 @@ public class UserApplicationService {
|
|||||||
|
|
||||||
//组装新增用户信息
|
//组装新增用户信息
|
||||||
addUserInfo(userDO);
|
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);
|
UserEntity userEntity = userDomainService.addUser(userDO);
|
||||||
|
if (ObjectUtil.isNotNull(loginUser)) {
|
||||||
if (isCustomerManagementPresetRole(userEntity.getRoleCode())) {
|
if (isCustomerManagementPresetRole(userEntity.getRoleCode())) {
|
||||||
// 保持插入结果,不再改 role_code / user_role
|
// 保持插入结果,不再改 role_code / user_role
|
||||||
} else if ((StringUtils.isEmpty(userEntity.getRoleCode()) || !RoleEnum.PLAT_ADMIN.getCode().equals(userEntity.getRoleCode())) &&
|
} else if ((StringUtils.isEmpty(userEntity.getRoleCode()) || !RoleEnum.PLAT_ADMIN.getCode().equals(userEntity.getRoleCode())) &&
|
||||||
@@ -1480,6 +1574,22 @@ public class UserApplicationService {
|
|||||||
userDomainService.updateUser(userDO);
|
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);
|
BeanUtils.copyProperties(userEntity, userPo);
|
||||||
//同步网货平台员工信息
|
//同步网货平台员工信息
|
||||||
try {
|
try {
|
||||||
@@ -1492,13 +1602,15 @@ public class UserApplicationService {
|
|||||||
// throw e;
|
// throw e;
|
||||||
}
|
}
|
||||||
//获取组织信息
|
//获取组织信息
|
||||||
Long organizationId = loginUser.getUserPo().getOrganizationId();
|
Long organizationId = ObjectUtil.isNotNull(loginUser) ? loginUser.getUserPo().getOrganizationId() : userDO.getOrganizationId();
|
||||||
Integer isQueue = userMapper.getOrdIsQueue(organizationId);
|
Integer isQueue = userMapper.getOrdIsQueue(organizationId);
|
||||||
|
|
||||||
if (isQueue==1) {
|
if (isQueue==1) {
|
||||||
//同步YMS员工信息
|
//同步YMS员工信息
|
||||||
try {
|
try {
|
||||||
userEntity.setUserPassword(password);
|
if (StringUtils.isNotEmpty(password)) {
|
||||||
|
userEntity.setUserPassword(password);
|
||||||
|
}
|
||||||
ymsDomainService.addOrEditYmsUser(userEntity);
|
ymsDomainService.addOrEditYmsUser(userEntity);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
// 同步YMS失败时,记录日志但不影响用户保存
|
// 同步YMS失败时,记录日志但不影响用户保存
|
||||||
@@ -3428,6 +3540,24 @@ public class UserApplicationService {
|
|||||||
if (!ObjectUtil.equal(String.valueOf(object), verificationCode) && !ObjectUtil.equal(verificationCode, SmsConstants.universalCode)) {
|
if (!ObjectUtil.equal(String.valueOf(object), verificationCode) && !ObjectUtil.equal(verificationCode, SmsConstants.universalCode)) {
|
||||||
throw new ServiceException("验证码错误");
|
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())) {
|
if (StringUtils.isNotEmpty(userDO.getUserPassword())) {
|
||||||
String userPassword = AESUtil.decrypt(userDO.getUserPassword());
|
String userPassword = AESUtil.decrypt(userDO.getUserPassword());
|
||||||
@@ -3444,25 +3574,25 @@ public class UserApplicationService {
|
|||||||
userDomainService.updateUser(userDO);
|
userDomainService.updateUser(userDO);
|
||||||
}
|
}
|
||||||
}else {
|
}else {
|
||||||
String userPassword1 = userDO.getUserPassword();
|
// String userPassword1 = userDO.getUserPassword();
|
||||||
for (Long id : userDO.getUserIds()) {
|
// for (Long id : userDO.getUserIds()) {
|
||||||
UserPo userPo1 = userDomainService.selectByUserId(id);
|
// UserPo userPo1 = userDomainService.selectByUserId(id);
|
||||||
//用户密码传值
|
// //用户密码传值
|
||||||
if (StringUtils.isNotEmpty(userDO.getUserPassword())) {
|
// if (StringUtils.isNotEmpty(userDO.getUserPassword())) {
|
||||||
String userPassword = AESUtil.decrypt(userPassword1);
|
// String userPassword = AESUtil.decrypt(userPassword1);
|
||||||
// 生成随机8位数盐
|
// // 生成随机8位数盐
|
||||||
String salt = OConvertUtils.randomGen(8);
|
// String salt = OConvertUtils.randomGen(8);
|
||||||
userDO.setUserSalt(salt);
|
// userDO.setUserSalt(salt);
|
||||||
//加密算法,PBEWithMD5AndDES,
|
// //加密算法,PBEWithMD5AndDES,
|
||||||
String passwordEncode = PasswordUtil.encrypt(userPo1.getUserAccount(), userPassword, salt);
|
// String passwordEncode = PasswordUtil.encrypt(userPo1.getUserAccount(), userPassword, salt);
|
||||||
userDO.setUserPassword(passwordEncode);
|
// userDO.setUserPassword(passwordEncode);
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
if (ObjectUtil.isNotNull(userPo1)) {
|
// if (ObjectUtil.isNotNull(userPo1)) {
|
||||||
userDO.setUserId(userPo1.getUserId());
|
// userDO.setUserId(userPo1.getUserId());
|
||||||
userDomainService.updateUser(userDO);
|
// userDomainService.updateUser(userDO);
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -3874,6 +4004,18 @@ public class UserApplicationService {
|
|||||||
//新支付密码
|
//新支付密码
|
||||||
String payPassword = userDTO.getUserPayPassword();
|
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;
|
String key = CacheConstants.SMS_PAY_CHANGE + userPhone;
|
||||||
Object object = redisService.getCacheObject(key) == null ? "" : redisService.getCacheObject(key);
|
Object object = redisService.getCacheObject(key) == null ? "" : redisService.getCacheObject(key);
|
||||||
|
|||||||
+5
-5
@@ -22,12 +22,12 @@ public class UserRoleFactory {
|
|||||||
UserRoleEntity userRoleEntity = new UserRoleEntity();
|
UserRoleEntity userRoleEntity = new UserRoleEntity();
|
||||||
BeanUtils.copyProperties(userRoleDo,userRoleEntity, IgnoreNullUtil.getNullPropertyNames(userRoleDo));
|
BeanUtils.copyProperties(userRoleDo,userRoleEntity, IgnoreNullUtil.getNullPropertyNames(userRoleDo));
|
||||||
LoginUser loginUser = SecurityUtils.getLoginUser();
|
LoginUser loginUser = SecurityUtils.getLoginUser();
|
||||||
if (ObjectUtil.isNull(loginUser)) {
|
String userName = null;
|
||||||
throw new DigitalLogisticsException(UserError.TIMEOUT);
|
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(userRoleDo.getUserRoleId() != null){
|
||||||
if(userId != null){
|
if(userId != null){
|
||||||
userRoleEntity.setUpdateBy(userId);
|
userRoleEntity.setUpdateBy(userId);
|
||||||
|
|||||||
+2
@@ -49,6 +49,8 @@ public class UserDO extends BaseVOEntity {
|
|||||||
private String userMemberCode;
|
private String userMemberCode;
|
||||||
@ApiModelProperty(name = "登录密码")
|
@ApiModelProperty(name = "登录密码")
|
||||||
private String userPassword;
|
private String userPassword;
|
||||||
|
@ApiModelProperty(name = "老登录密码")
|
||||||
|
private String oldUserPassword;
|
||||||
@ApiModelProperty(name = "确认登录密码")
|
@ApiModelProperty(name = "确认登录密码")
|
||||||
private String copyUserPassword;
|
private String copyUserPassword;
|
||||||
@ApiModelProperty(name = "盐")
|
@ApiModelProperty(name = "盐")
|
||||||
|
|||||||
+6
-2
@@ -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.domain.po.app.UserAppPo;
|
||||||
import com.mhd.common.core.annotation.fieldsetvalue.NeedSetValueMethod;
|
import com.mhd.common.core.annotation.fieldsetvalue.NeedSetValueMethod;
|
||||||
import com.mhd.common.security.utils.SecurityUtils;
|
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.infrastructure.util.annotation.DataPermissions;
|
||||||
import com.mhd.user.interfaces.dto.queryDTO.UserDocumentWarningQueryDTO;
|
import com.mhd.user.interfaces.dto.queryDTO.UserDocumentWarningQueryDTO;
|
||||||
import com.mhd.user.interfaces.dto.updateDTO.UpdateHasDefaultAddressDTO;
|
import com.mhd.user.interfaces.dto.updateDTO.UpdateHasDefaultAddressDTO;
|
||||||
@@ -50,9 +51,12 @@ public class UserDomainService {
|
|||||||
*/
|
*/
|
||||||
public UserEntity updateUser(UserDO userDO) {
|
public UserEntity updateUser(UserDO userDO) {
|
||||||
UserEntity userEntity = userFactory.getUserEntity(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.setUpdateTime(new Date());
|
||||||
userEntity.setUpdateByName(SecurityUtils.getLoginUser().getUsername());
|
|
||||||
userRepositoryInterface.updateUser(userEntity);
|
userRepositoryInterface.updateUser(userEntity);
|
||||||
return userEntity;
|
return userEntity;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,6 +49,8 @@ public class UserDTO extends BaseVOEntity {
|
|||||||
private String userSalt;
|
private String userSalt;
|
||||||
@ApiModelProperty(name = "支付密码")
|
@ApiModelProperty(name = "支付密码")
|
||||||
private String userPayPassword;
|
private String userPayPassword;
|
||||||
|
@ApiModelProperty(name = "旧支付密码")
|
||||||
|
private String oldUserPayPassword;
|
||||||
@ApiModelProperty(name = "身份证号")
|
@ApiModelProperty(name = "身份证号")
|
||||||
private String userIdcardNumber;
|
private String userIdcardNumber;
|
||||||
@ApiModelProperty(name = "身份证号加密")
|
@ApiModelProperty(name = "身份证号加密")
|
||||||
|
|||||||
@@ -283,6 +283,22 @@ public class UserAPI extends BaseController {
|
|||||||
return R.ok(sysUserVo, "操作成功");
|
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 新增合作商
|
* @Description 新增合作商
|
||||||
* @Author Alex
|
* @Author Alex
|
||||||
|
|||||||
@@ -19,14 +19,12 @@ spring:
|
|||||||
nacos:
|
nacos:
|
||||||
discovery:
|
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.33.0.129:6010
|
||||||
server-addr: 10.102.192.31:6848
|
server-addr: 10.102.192.31:6848
|
||||||
username: nacos
|
username: nacos
|
||||||
password: manhuoda@2023
|
password: manhuoda@2023
|
||||||
config:
|
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.33.0.129:6010
|
||||||
server-addr: 10.102.192.31:6848
|
server-addr: 10.102.192.31:6848
|
||||||
username: nacos
|
username: nacos
|
||||||
|
|||||||
+42
-14
@@ -472,7 +472,11 @@ public class BillManageApplicationService {
|
|||||||
BeanUtils.copyProperties(billDetailDTO, billingStatementDTO);
|
BeanUtils.copyProperties(billDetailDTO, billingStatementDTO);
|
||||||
|
|
||||||
String firstSubjectCode = billDetailDTO.getFirstSubjectCode();
|
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 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 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);
|
BigDecimal taxFreeFee = amount.subtract(taxAmount);
|
||||||
@@ -484,16 +488,26 @@ public class BillManageApplicationService {
|
|||||||
billingStatementDTO.setBillManageId(billManageDO.getBillManageId());
|
billingStatementDTO.setBillManageId(billManageDO.getBillManageId());
|
||||||
billingStatementDTO.setBillingState(3);
|
billingStatementDTO.setBillingState(3);
|
||||||
Long billingStatementId = billingStatementApplicationService.insertAndReturnId(billingStatementDTO);
|
Long billingStatementId = billingStatementApplicationService.insertAndReturnId(billingStatementDTO);
|
||||||
//遍历billDetailDTOList集合,将计费流水号相同的,赋值计费流水id
|
//遍历billDetailDTOList集合,将计费流水号相同的,赋值计费流水id(流水号为空时只处理自身)
|
||||||
billDetailDTOList.stream().filter(item -> item.getBillingFlow().equals(billDetailDTO.getBillingFlow())).forEach(item -> {
|
if (StringUtils.isNotBlank(billDetailDTO.getBillingFlow())) {
|
||||||
item.setBillingStatementId(billingStatementId);
|
billDetailDTOList.stream().filter(item -> billDetailDTO.getBillingFlow().equals(item.getBillingFlow())).forEach(item -> {
|
||||||
item.setBillType(billManageDTO.getBillType());
|
item.setBillingStatementId(billingStatementId);
|
||||||
item.setBillManageId(billManageDO.getBillManageId());
|
item.setBillType(billManageDTO.getBillType());
|
||||||
item.setBillNumber(billNumber);
|
item.setBillManageId(billManageDO.getBillManageId());
|
||||||
item.setInvoiceItem(item.getFirstSubjectCode());
|
item.setBillNumber(billNumber);
|
||||||
item.setInvoiceItemName(item.getFirstSubjectName());
|
item.setInvoiceItem(item.getFirstSubjectCode());
|
||||||
item.setFeeType(item.getServiceItemsName());
|
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()
|
List<BillDetailDTO> billDetailDTOListHasBillingStatementId = billDetailDTOList.stream()
|
||||||
@@ -514,7 +528,11 @@ public class BillManageApplicationService {
|
|||||||
BigDecimal allTaxFreeFee = BigDecimal.ZERO;
|
BigDecimal allTaxFreeFee = BigDecimal.ZERO;
|
||||||
for (BillDetailDTO item : billDetailDTOList) {
|
for (BillDetailDTO item : billDetailDTOList) {
|
||||||
String firstSubjectCode = item.getFirstSubjectCode();
|
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 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 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);
|
BigDecimal taxFreeFee = amount.subtract(taxAmount);
|
||||||
@@ -584,11 +602,20 @@ public class BillManageApplicationService {
|
|||||||
originalBill.setInvoiceMakeTime(null);
|
originalBill.setInvoiceMakeTime(null);
|
||||||
originalBill.setNcId(null);
|
originalBill.setNcId(null);
|
||||||
originalBill.setSkNcId(null);
|
originalBill.setSkNcId(null);
|
||||||
// 明细也清空id
|
// 明细清空id;同时清空计费流水id并按"原流水→新流水"映射生成新流水号,
|
||||||
|
// 否则提交addBill会走"更新已有流水"分支,把原账单的流水改挂到新账单上(原账单明细丢失)
|
||||||
|
Map<String, String> flowMapping = new HashMap<>();
|
||||||
for (BillDetailPO detail : originalDetails) {
|
for (BillDetailPO detail : originalDetails) {
|
||||||
detail.setBillDetailId(null);
|
detail.setBillDetailId(null);
|
||||||
detail.setBillManageId(null);
|
detail.setBillManageId(null);
|
||||||
detail.setBillNumber(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);
|
originalBill.setBillDetailPOList(originalDetails);
|
||||||
return originalBill;
|
return originalBill;
|
||||||
@@ -739,7 +766,7 @@ public class BillManageApplicationService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public List<NcLog> getNcLog(Long id, Long type) {
|
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.setUpdateBy(loginUser.getUserid());
|
||||||
billingStatementDO.setUpdateByName(loginUser.getUserPo().getUserName());
|
billingStatementDO.setUpdateByName(loginUser.getUserPo().getUserName());
|
||||||
billingStatementDO.setUpdateTime(new Date());
|
billingStatementDO.setUpdateTime(new Date());
|
||||||
|
billingStatementDO.setBillingRemark(detailDTO.getBillingRemark());
|
||||||
billingStatementApplicationService.update(billingStatementDO);
|
billingStatementApplicationService.update(billingStatementDO);
|
||||||
}
|
}
|
||||||
Boolean flag = billDetailDomainService.update(billDetailDO);
|
Boolean flag = billDetailDomainService.update(billDetailDO);
|
||||||
|
|||||||
+8
@@ -568,6 +568,10 @@ public class BillingStatementApplicationService {
|
|||||||
handleDate(billingStatementDO);
|
handleDate(billingStatementDO);
|
||||||
|
|
||||||
billingStatementDO.setDataSources(2);
|
billingStatementDO.setDataSources(2);
|
||||||
|
// 币种为空时兜底CNY(与addCost一致;NC推送时该值作pk_currtype,空值会推送失败)
|
||||||
|
if (StringUtils.isEmpty(billingStatementDO.getSettlementCurrency())) {
|
||||||
|
billingStatementDO.setSettlementCurrency("CNY");
|
||||||
|
}
|
||||||
if(StringUtils.isEmpty(billingStatementDO.getBillingFlow())){
|
if(StringUtils.isEmpty(billingStatementDO.getBillingFlow())){
|
||||||
billingStatementDO.setBillingFlow(OrderSequence.getOrderCode("JFLS"));
|
billingStatementDO.setBillingFlow(OrderSequence.getOrderCode("JFLS"));
|
||||||
}
|
}
|
||||||
@@ -593,6 +597,10 @@ public class BillingStatementApplicationService {
|
|||||||
BillingStatementPO billingStatementPO = new BillingStatementPO();
|
BillingStatementPO billingStatementPO = new BillingStatementPO();
|
||||||
//处理数据
|
//处理数据
|
||||||
handleDate(billingStatementDO);
|
handleDate(billingStatementDO);
|
||||||
|
// 币种为空时兜底CNY(编码与前端默认值一致;NC推送时该值原样作pk_currtype。须在copyProperties之前赋值,PO才能带上)
|
||||||
|
if (StringUtils.isEmpty(billingStatementDO.getSettlementCurrency())) {
|
||||||
|
billingStatementDO.setSettlementCurrency("CNY");
|
||||||
|
}
|
||||||
BeanUtils.copyProperties(billingStatementDO,billingStatementPO);
|
BeanUtils.copyProperties(billingStatementDO,billingStatementPO);
|
||||||
billingStatementPO.setBillingTotalAmount(billingStatementDO.getBillingAmount());
|
billingStatementPO.setBillingTotalAmount(billingStatementDO.getBillingAmount());
|
||||||
billingStatementPO.setDataSources(2);
|
billingStatementPO.setDataSources(2);
|
||||||
|
|||||||
+155
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
-1
@@ -21,5 +21,5 @@ public interface BillDetailMapper extends BaseMapper<BillDetail>
|
|||||||
|
|
||||||
List<BillDetail> getDetailsByBillManageIdAndGroup(@Param("billManageId") Long billManageId);
|
List<BillDetail> getDetailsByBillManageIdAndGroup(@Param("billManageId") Long billManageId);
|
||||||
|
|
||||||
Double getTaxRate(@Param("code") String code);
|
Double getTaxRate(@Param("code") String code, @Param("name") String name);
|
||||||
}
|
}
|
||||||
+1
-1
@@ -45,5 +45,5 @@ public interface BillManageMapper extends BaseMapper<BillManage>
|
|||||||
|
|
||||||
String getOrgCodeNc(@Param("organizationId") Long organizationId);
|
String getOrgCodeNc(@Param("organizationId") Long organizationId);
|
||||||
|
|
||||||
Long getShipperId(@Param("userId") Long userId, @Param("organizationId") Long organizationId);
|
Long getShipperId(@Param("userId") Long userId);
|
||||||
}
|
}
|
||||||
+12
-4
@@ -211,7 +211,11 @@ public class BillManageImpl extends ServiceImpl<BillManageMapper, BillManage> im
|
|||||||
BigDecimal allTaxFreeFee = BigDecimal.ZERO;
|
BigDecimal allTaxFreeFee = BigDecimal.ZERO;
|
||||||
for (BillDetail item : billDetails) {
|
for (BillDetail item : billDetails) {
|
||||||
String firstSubjectCode = item.getInvoiceItem();
|
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 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 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);
|
BigDecimal taxFreeFee = amount.subtract(taxAmount);
|
||||||
@@ -265,7 +269,7 @@ public class BillManageImpl extends ServiceImpl<BillManageMapper, BillManage> im
|
|||||||
throw new ServiceException("结算对象未找到!");
|
throw new ServiceException("结算对象未找到!");
|
||||||
}
|
}
|
||||||
Long settlementEntityId = settlementCustomers.getSettlementEntityId();
|
Long settlementEntityId = settlementCustomers.getSettlementEntityId();
|
||||||
Long shipperId = billManageMapper.getShipperId(settlementEntityId, organizationId);
|
Long shipperId = billManageMapper.getShipperId(settlementEntityId);
|
||||||
if (shipperId==null||shipperId==0){
|
if (shipperId==null||shipperId==0){
|
||||||
throw new ServiceException("结算对象未找到!");
|
throw new ServiceException("结算对象未找到!");
|
||||||
}
|
}
|
||||||
@@ -853,7 +857,11 @@ public class BillManageImpl extends ServiceImpl<BillManageMapper, BillManage> im
|
|||||||
BigDecimal allTaxFreeFee = BigDecimal.ZERO;
|
BigDecimal allTaxFreeFee = BigDecimal.ZERO;
|
||||||
for (BillDetail item : billDetails) {
|
for (BillDetail item : billDetails) {
|
||||||
String firstSubjectCode = item.getInvoiceItem();
|
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 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 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);
|
BigDecimal taxFreeFee = amount.subtract(taxAmount);
|
||||||
@@ -932,7 +940,7 @@ public class BillManageImpl extends ServiceImpl<BillManageMapper, BillManage> im
|
|||||||
throw new ServiceException("结算对象未找到!");
|
throw new ServiceException("结算对象未找到!");
|
||||||
}
|
}
|
||||||
Long settlementEntityId = settlementCustomers.getSettlementEntityId();
|
Long settlementEntityId = settlementCustomers.getSettlementEntityId();
|
||||||
Long shipperId = billManageMapper.getShipperId(settlementEntityId, organizationId);
|
Long shipperId = billManageMapper.getShipperId(settlementEntityId);
|
||||||
if (shipperId==null||shipperId==0){
|
if (shipperId==null||shipperId==0){
|
||||||
throw new ServiceException("结算对象未找到!");
|
throw new ServiceException("结算对象未找到!");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -140,5 +140,7 @@ public class BillDetailPO extends BaseVOEntity{
|
|||||||
private BigDecimal billingNum;
|
private BigDecimal billingNum;
|
||||||
@ApiModelProperty(value = "计费单价")
|
@ApiModelProperty(value = "计费单价")
|
||||||
private BigDecimal billingUnitPrice;
|
private BigDecimal billingUnitPrice;
|
||||||
|
@ApiModelProperty(value = "流水备注")
|
||||||
|
private String billingRemark;
|
||||||
|
|
||||||
}
|
}
|
||||||
+32
@@ -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);
|
||||||
|
}
|
||||||
+28
@@ -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;
|
||||||
|
}
|
||||||
+297
@@ -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→上年Q4,Q2-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);
|
||||||
|
}
|
||||||
|
}
|
||||||
+105
@@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 环比基期(需求规则3):Q1 → 上年第 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+325
@@ -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; // AH(0-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 (第3、4行): 表头区域
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
+130
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
+469
@@ -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 同年环比时基期季度被排除在区间外,
|
||||||
|
* 导致客户基期恒为 0、delta 恒等于本期金额。
|
||||||
|
*/
|
||||||
|
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;
|
private BigDecimal billingNum;
|
||||||
@ApiModelProperty(value = "计费单价")
|
@ApiModelProperty(value = "计费单价")
|
||||||
private BigDecimal billingUnitPrice;
|
private BigDecimal billingUnitPrice;
|
||||||
|
@ApiModelProperty(value = "流水备注")
|
||||||
|
private String billingRemark;
|
||||||
}
|
}
|
||||||
+24
@@ -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;
|
||||||
|
}
|
||||||
+21
@@ -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;
|
||||||
|
}
|
||||||
+34
@@ -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;
|
||||||
|
}
|
||||||
+19
@@ -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;
|
||||||
|
}
|
||||||
+15
@@ -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;
|
||||||
|
}
|
||||||
+28
@@ -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;
|
||||||
|
}
|
||||||
+36
@@ -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;
|
||||||
|
}
|
||||||
+19
@@ -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 = "销项发票表-分页列表查询")
|
@ApiOperation(value = "销项发票表-分页列表查询", notes = "销项发票表-分页列表查询")
|
||||||
@RequestMapping(value = "/queryList")
|
@RequestMapping(value = "/queryList")
|
||||||
public Result<IPage<BmsInvoiceListVO>> queryList(BmsInvoiceListQueryDTO bmsInvoiceListQueryDTO,
|
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,
|
@RequestParam(name="pageSize", defaultValue="10") Integer pageSize,
|
||||||
HttpServletRequest req) {
|
HttpServletRequest req) {
|
||||||
|
// 前端分页参数名不统一(pageNum / pageNo),两个都兼容,优先 pageNo
|
||||||
|
int currentPage = pageNo != null ? pageNo : (pageNum != null ? pageNum : 1);
|
||||||
Result<IPage<BmsInvoiceListVO>> result = new Result<IPage<BmsInvoiceListVO>>();
|
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);
|
pageList = bmsInvoiceListService.queryList(pageList, bmsInvoiceListQueryDTO);
|
||||||
result.setResult(pageList);
|
result.setResult(pageList);
|
||||||
result.setSuccess(true);
|
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));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,15 +18,15 @@ spring:
|
|||||||
discovery:
|
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.33.0.129:6010
|
||||||
server-addr: 10.102.192.30:6848
|
# server-addr: 10.102.192.30:6848
|
||||||
username: nacos
|
username: nacos
|
||||||
password: manhuoda@2023
|
password: manhuoda@2023
|
||||||
config:
|
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.33.0.129:6010
|
||||||
server-addr: 10.102.192.30:6848
|
# server-addr: 10.102.192.30:6848
|
||||||
username: nacos
|
username: nacos
|
||||||
password: manhuoda@2023
|
password: manhuoda@2023
|
||||||
group: DEFAULT_GROUP # 默认分组就是DEFAULT_GROUP,如果使用默认分组可以不配置
|
group: DEFAULT_GROUP # 默认分组就是DEFAULT_GROUP,如果使用默认分组可以不配置
|
||||||
|
|||||||
@@ -18,7 +18,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||||||
bs.first_subject_code,
|
bs.first_subject_code,
|
||||||
bs.first_subject_name,
|
bs.first_subject_name,
|
||||||
bs.second_subject_code,
|
bs.second_subject_code,
|
||||||
bs.second_subject_name
|
bs.second_subject_name,
|
||||||
|
bs.billing_remark
|
||||||
|
|
||||||
FROM
|
FROM
|
||||||
bill_detail bd
|
bill_detail bd
|
||||||
LEFT JOIN billing_statement bs ON bd.billing_statement_id = bs.billing_statement_id
|
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
|
AND bill_manage_id = #{billManageId} GROUP BY INVOICE_ITEM,INVOICE_ITEM_NAME,FEE_TYPE
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<select id="getTaxRate" resultType="java.lang.Double" parameterType="java.lang.String">
|
<!-- 科目编码存在重复(主数据未做唯一校验,如1003同时有"运输处理费/报关费3"),按编码+名称取唯一一条 -->
|
||||||
|
<select id="getTaxRate" resultType="java.lang.Double">
|
||||||
SELECT
|
SELECT
|
||||||
rate
|
rate
|
||||||
FROM
|
FROM
|
||||||
@@ -55,7 +58,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||||||
WHERE
|
WHERE
|
||||||
del_flag = 1
|
del_flag = 1
|
||||||
AND SUBJECT_CODE = #{code}
|
AND SUBJECT_CODE = #{code}
|
||||||
|
AND SUBJECT_NAME = #{name}
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
|
|
||||||
</mapper>
|
</mapper>
|
||||||
@@ -170,7 +170,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||||||
</select>
|
</select>
|
||||||
|
|
||||||
<select id="getShipperId" resultType="java.lang.Long">
|
<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>
|
</select>
|
||||||
|
|
||||||
</mapper>
|
</mapper>
|
||||||
@@ -10,7 +10,7 @@
|
|||||||
where a.is_delete = 0
|
where a.is_delete = 0
|
||||||
<include refid="common_where"></include>
|
<include refid="common_where"></include>
|
||||||
group by a.id
|
group by a.id
|
||||||
order by a.create_time desc
|
order by a.create_time desc, a.id desc
|
||||||
</select>
|
</select>
|
||||||
<select id="getById" parameterType="java.lang.String" resultType="com.mhd.bms.domain.bmsInvoiceList.repository.vo.BmsInvoiceListDetailsVO">
|
<select id="getById" parameterType="java.lang.String" resultType="com.mhd.bms.domain.bmsInvoiceList.repository.vo.BmsInvoiceListDetailsVO">
|
||||||
select
|
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 >= TO_DATE(#{beginDate}, 'YYYY-MM-DD')
|
||||||
|
AND bs.BUSINESS_DATE < 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 >= TO_DATE(#{beginDate}, 'YYYY-MM-DD')
|
||||||
|
AND bs.BUSINESS_DATE < 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>
|
||||||
@@ -18,17 +18,15 @@ spring:
|
|||||||
cloud:
|
cloud:
|
||||||
nacos:
|
nacos:
|
||||||
discovery:
|
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.33.0.129:6010
|
||||||
server-addr: 10.102.192.30:6848
|
# server-addr: 10.102.192.30:6848
|
||||||
username: nacos
|
username: nacos
|
||||||
password: manhuoda@2023
|
password: manhuoda@2023
|
||||||
config:
|
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.33.0.129:6010
|
||||||
server-addr: 10.102.192.30:6848
|
# server-addr: 10.102.192.30:6848
|
||||||
username: nacos
|
username: nacos
|
||||||
password: manhuoda@2023
|
password: manhuoda@2023
|
||||||
#线上正式环境
|
#线上正式环境
|
||||||
|
|||||||
+6
-2
@@ -103,10 +103,14 @@ public class BusinessDocumentOrderApplicationService {
|
|||||||
|
|
||||||
public List<BusinessDocumentOrderPO> queryList(BusinessDocumentOrderDO businessDocumentOrderDO) {
|
public List<BusinessDocumentOrderPO> queryList(BusinessDocumentOrderDO businessDocumentOrderDO) {
|
||||||
LoginUser loginUser = SecurityUtils.getLoginUser();
|
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();
|
Long topOrganizationId = loginUser.getUserPo().getTopOrganizationId();
|
||||||
if (topOrganizationId != null && topOrganizationId != 1){
|
if (topOrganizationId != null && topOrganizationId != 1){
|
||||||
businessDocumentOrderDO.setTopOrganizationId(loginUser.getUserPo().getTopOrganizationId());
|
businessDocumentOrderDO.setTopOrganizationId(topOrganizationId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return businessDocumentOrderDomainService.queryList(businessDocumentOrderDO);
|
return businessDocumentOrderDomainService.queryList(businessDocumentOrderDO);
|
||||||
|
|||||||
+390
@@ -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.type:1-入库通知单 2-出库通知单) */
|
||||||
|
Integer NOTICE_TYPE_OUT = 2;
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user