mirror of
https://gitee.com/yudaocode/yudao-boot-mini.git
synced 2026-10-06 14:13:14 +08:00
Merge branch 'master-jdk17' of https://gitee.com/zhijiantianya/ruoyi-vue-pro
# Conflicts: # pom.xml # yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/contract/CrmContractSaveReqVO.java # yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/product/CrmProductServiceImpl.java # yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/in/ErpPurchaseInSaveReqVO.java # yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/product/ErpProductServiceImpl.java # yudao-module-mall/yudao-module-product/src/main/java/cn/iocoder/yudao/module/product/controller/admin/spu/vo/ProductSkuSaveReqVO.java # yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/itemreceipt/MesWmItemReceiptDetailServiceImpl.java # yudao-module-system/src/test/java/cn/iocoder/yudao/module/system/service/user/AdminUserServiceImplTest.java
This commit is contained in:
@@ -0,0 +1,142 @@
|
||||
INSERT IGNORE INTO `fms_report_template` (`id`, `name`, `row_no`, `formula`, `remark`, `editable`, `sort`, `row_id`, `type`, `category`, `create_time`, `level`) VALUES
|
||||
(1, '流动资产:', 0, '[]', NULL, 0, 0, 0, 1, NULL, '2021-09-18 16:39:40', 1),
|
||||
(2, '流动负债:', 0, '[]', NULL, 0, 32, 0, 1, NULL, '2021-09-18 16:39:40', 1),
|
||||
(3, '货币资金', 1, '[{"operator":"+","rules":0,"subjectId":1331,"subjectName":"库存现金","subjectNumber":"1001"},{"operator":"+","rules":0,"subjectId":1332,"subjectName":"银行存款","subjectNumber":"1002"},{"operator":"+","rules":0,"subjectId":1333,"subjectName":"其他货币资金","subjectNumber":"1012"}]', NULL, 1, 1, 1, 1, NULL, '2021-09-18 16:39:40', 2),
|
||||
(4, '短期借款', 31, '[{"operator":"+","rules":0,"subjectId":1375,"subjectName":"短期借款","subjectNumber":"2001"}]', NULL, 1, 33, 1, 1, NULL, '2021-09-18 16:39:40', 2),
|
||||
(5, '短期投资', 2, '[{"operator":"+","rules":0,"subjectId":1334,"subjectName":"短期投资","subjectNumber":"1101"}]', NULL, 1, 2, 2, 1, NULL, '2021-09-18 16:39:40', 2),
|
||||
(6, '应付票据', 32, '[{"operator":"+","rules":0,"subjectId":1376,"subjectName":"应付票据","subjectNumber":"2201"}]', NULL, 1, 34, 2, 1, NULL, '2021-09-18 16:39:40', 2),
|
||||
(7, '应收票据', 3, '[{"operator":"+","rules":0,"subjectId":1339,"subjectName":"应收票据","subjectNumber":"1121"}]', NULL, 1, 3, 3, 1, NULL, '2021-09-18 16:39:40', 2),
|
||||
(8, '应付账款', 33, '[{"operator":"+","rules":4,"subjectId":1341,"subjectName":"预付账款","subjectNumber":"1123"},{"operator":"+","rules":4,"subjectId":1377,"subjectName":"应付账款","subjectNumber":"2202"}]', NULL, 1, 35, 3, 1, NULL, '2021-09-18 16:39:40', 2),
|
||||
(9, '应收账款', 4, '[{"operator":"+","rules":3,"subjectId":1340,"subjectName":"应收账款","subjectNumber":"1122"},{"operator":"+","rules":3,"subjectId":1378,"subjectName":"预收账款","subjectNumber":"2203"}]', NULL, 1, 4, 4, 1, NULL, '2021-09-18 16:39:40', 2),
|
||||
(10, '预收账款', 34, '[{"operator":"+","rules":4,"subjectId":1340,"subjectName":"应收账款","subjectNumber":"1122"},{"operator":"+","rules":4,"subjectId":1378,"subjectName":"预收账款","subjectNumber":"2203"}]', NULL, 1, 36, 4, 1, NULL, '2021-09-18 16:39:40', 2),
|
||||
(11, '预付账款', 5, '[{"operator":"+","rules":3,"subjectId":1341,"subjectName":"预付账款","subjectNumber":"1123"},{"operator":"+","rules":3,"subjectId":1377,"subjectName":"应付账款","subjectNumber":"2202"}]', NULL, 1, 5, 5, 1, NULL, '2021-09-18 16:39:40', 2),
|
||||
(12, '应付职工薪酬', 35, '[{"operator":"+","rules":0,"subjectId":1379,"subjectName":"应付职工薪酬","subjectNumber":"2211"}]', NULL, 1, 37, 5, 1, NULL, '2021-09-18 16:39:40', 2),
|
||||
(13, '应收股利', 6, '[{"operator":"+","rules":0,"subjectId":1342,"subjectName":"应收股利","subjectNumber":"1131"}]', NULL, 1, 6, 6, 1, NULL, '2021-09-18 16:39:40', 2),
|
||||
(14, '应交税费', 36, '[{"operator":"+","rules":0,"subjectId":1393,"subjectName":" 应交税费","subjectNumber":"2221"}]', NULL, 1, 38, 6, 1, NULL, '2021-09-18 16:39:40', 2),
|
||||
(15, '应收利息', 7, '[{"operator":"+","rules":0,"subjectId":1343,"subjectName":"应收利息","subjectNumber":"1132"}]', NULL, 1, 7, 7, 1, NULL, '2021-09-18 16:39:40', 2),
|
||||
(16, '应付利息', 37, '[{"operator":"+","rules":0,"subjectId":1412,"subjectName":"应付利息","subjectNumber":"2231"}]', NULL, 1, 39, 7, 1, NULL, '2021-09-18 16:39:40', 2),
|
||||
(17, '其他应收款', 8, '[{"operator":"+","rules":0,"subjectId":1344,"subjectName":"其他应收款","subjectNumber":"1221"}]', NULL, 1, 8, 8, 1, NULL, '2021-09-18 16:39:40', 2),
|
||||
(18, '应付利润', 38, '[{"operator":"+","rules":0,"subjectId":1413,"subjectName":"应付利润","subjectNumber":"2232"}]', NULL, 1, 40, 8, 1, NULL, '2021-09-18 16:39:41', 2),
|
||||
(19, '存货', 9, '[{"operator":"+","rules":0,"subjectId":1345,"subjectName":"材料采购","subjectNumber":"1401"},{"operator":"+","rules":0,"subjectId":1346,"subjectName":"在途物资","subjectNumber":"1402"},{"operator":"+","rules":0,"subjectId":1347,"subjectName":"原材料","subjectNumber":"1403"},{"operator":"+","rules":0,"subjectId":1348,"subjectName":"材料成本差异","subjectNumber":"1404"},{"operator":"+","rules":0,"subjectId":1349,"subjectName":"库存商品","subjectNumber":"1405"},{"operator":"-","rules":0,"subjectId":1350,"subjectName":"商品进销差价","subjectNumber":"1407"},{"operator":"+","rules":0,"subjectId":1351,"subjectName":"委托加工物资","subjectNumber":"1408"},{"operator":"+","rules":0,"subjectId":1352,"subjectName":"周转材料","subjectNumber":"1411"},{"operator":"+","rules":0,"subjectId":1353,"subjectName":"消耗性生物资产","subjectNumber":"1421"},{"operator":"+","rules":0,"subjectId":1437,"subjectName":"生产成本","subjectNumber":"4001"},{"operator":"+","rules":0,"subjectId":1440,"subjectName":"制造费用","subjectNumber":"4101"},{"operator":"+","rules":0,"subjectId":1442,"subjectName":"工程施工","subjectNumber":"4401"},{"operator":"+","rules":0,"subjectId":1443,"subjectName":"机械作业","subjectNumber":"4403"}]', NULL, 1, 9, 9, 1, NULL, '2021-09-18 16:39:41', 2),(20, '其他应付款', 39, '[{"operator":"+","rules":0,"subjectId":1414,"subjectName":"其他应付款","subjectNumber":"2241"}]', NULL, 1, 41, 9, 1, NULL, '2021-09-18 16:39:41', 2),
|
||||
(21, '其中:原材料', 10, '[{"operator":"+","rules":0,"subjectId":1347,"subjectName":"原材料","subjectNumber":"1403"}]', NULL, 1, 10, 10, 1, NULL, '2021-09-18 16:39:41', 2),
|
||||
(22, '其他流动负债', 40, '[{"operator":"+","rules":0,"subjectId":1396,"subjectName":"待转销项税额","subjectNumber":"222106"}]', NULL, 1, 42, 10, 1, NULL, '2021-09-18 16:39:41', 2),
|
||||
(23, '在产品', 11, '[{"operator":"+","rules":0,"subjectId":1437,"subjectName":"生产成本","subjectNumber":"4001"},{"operator":"+","rules":0,"subjectId":1440,"subjectName":"制造费用","subjectNumber":"4101"},{"operator":"+","rules":0,"subjectId":1442,"subjectName":"工程施工","subjectNumber":"4401"},{"operator":"+","rules":0,"subjectId":1443,"subjectName":"机械作业","subjectNumber":"4403"}]', NULL, 1, 11, 11, 1, NULL, '2021-09-18 16:39:41', 2),
|
||||
(24, '流动负债合计', 41, '["L31+L32+L33+L34+L35+L36+L37+L38+L39+L40"]', NULL, 0, 43, 11, 1, NULL, '2021-09-18 16:39:41', 1),
|
||||
(25, '库存商品', 12, '[{"operator":"+","rules":0,"subjectId":1349,"subjectName":"库存商品","subjectNumber":"1405"}]', NULL, 1, 12, 12, 1, NULL, '2021-09-18 16:39:41', 2),
|
||||
(26, '非流动负债:', 0, '[]', NULL, 0, 44, 12, 1, NULL, '2021-09-18 16:39:41', 1),
|
||||
(27, '周转材料', 13, '[{"operator":"+","rules":0,"subjectId":1352,"subjectName":"周转材料","subjectNumber":"1411"}]', NULL, 1, 13, 13, 1, NULL, '2021-09-18 16:39:41', 2),
|
||||
(28, '长期借款', 42, '[{"operator":"+","rules":0,"subjectId":1416,"subjectName":"长期借款","subjectNumber":"2501"}]', NULL, 1, 45, 13, 1, NULL, '2021-09-18 16:39:41', 2),
|
||||
(29, '其他流动资产', 14, '[{"operator":"+","rules":0,"subjectId":1394,"subjectName":"待抵扣进项税额","subjectNumber":"222104"},{"operator":"+","rules":0,"subjectId":1395,"subjectName":"待认证进项税额","subjectNumber":"222105"},{"operator":"+","rules":1,"subjectId":1392,"subjectName":"未交增值税","subjectNumber":"222102"},{"operator":"+","rules":0,"subjectId":1397,"subjectName":"增值税留抵税额","subjectNumber":"222107"}]', NULL, 1, 14, 14, 1, NULL, '2021-09-18 16:39:41', 2),
|
||||
(30, '长期应付款', 43, '[{"operator":"+","rules":0,"subjectId":1417,"subjectName":"长期应付款","subjectNumber":"2701"}]', NULL, 1, 46, 14, 1, NULL, '2021-09-18 16:39:41', 2),
|
||||
(31, '流动资产合计', 15, '["L1+L2+L3+L4+L5+L6+L7+L8+L9+L14"]', NULL, 0, 15, 15, 1, NULL, '2021-09-18 16:39:41', 1),
|
||||
(32, '递延收益', 44, '[{"operator":"+","rules":0,"subjectId":1415,"subjectName":"递延收益","subjectNumber":"2401"}]', NULL, 1, 47, 15, 1, NULL, '2021-09-18 16:39:41', 2),
|
||||
(33, '非流动资产:', 0, '[]', NULL, 0, 16, 16, 1, NULL, '2021-09-18 16:39:41', 1),
|
||||
(34, '其他非流动负债', 45, '[]', NULL, 0, 48, 16, 1, NULL, '2021-09-18 16:39:41', 2),
|
||||
(35, '长期债券投资', 16, '[{"operator":"+","rules":0,"subjectId":1354,"subjectName":"长期债券投资","subjectNumber":"1501"}]', NULL, 1, 17, 17, 1, NULL, '2021-09-18 16:39:41', 2),
|
||||
(36, '非流动负债合计', 46, '["L42+L43+L44+L45"]', NULL, 0, 49, 17, 1, NULL, '2021-09-18 16:39:41', 1),
|
||||
(37, '长期股权投资', 17, '[{"operator":"+","rules":0,"subjectId":1357,"subjectName":"长期股权投资","subjectNumber":"1511"}]', NULL, 1, 18, 18, 1, NULL, '2021-09-18 16:39:41', 2),
|
||||
(38, '负债合计', 47, '["L41+L46"]', NULL, 0, 50, 18, 1, NULL, '2021-09-18 16:39:41', 1),
|
||||
(39, '固定资产原价', 18, '[{"operator":"+","rules":0,"subjectId":1360,"subjectName":"固定资产","subjectNumber":"1601"}]', NULL, 1, 19, 19, 1, NULL, '2021-09-18 16:39:41', 2),
|
||||
(40, '减:累计折旧', 19, '[{"operator":"+","rules":0,"subjectId":1361,"subjectName":"累计折旧","subjectNumber":"1602"}]', NULL, 1, 20, 20, 1, NULL, '2021-09-18 16:39:41', 2),
|
||||
(41, '固定资产账面价值', 20, '["L18-L19"]', NULL, 0, 21, 21, 1, NULL, '2021-09-18 16:39:41', 2),
|
||||
(42, '在建工程', 21, '[{"operator":"+","rules":0,"subjectId":1362,"subjectName":"在建工程","subjectNumber":"1604"}]', NULL, 1, 22, 22, 1, NULL, '2021-09-18 16:39:41', 2),
|
||||
(43, '工程物资', 22, '[{"operator":"+","rules":0,"subjectId":1367,"subjectName":"工程物资","subjectNumber":"1605"}]', NULL, 1, 23, 23, 1, NULL, '2021-09-18 16:39:41', 2),
|
||||
(44, '固定资产清理', 23, '[{"operator":"+","rules":0,"subjectId":1368,"subjectName":"固定资产清理","subjectNumber":"1606"}]', NULL, 1, 24, 24, 1, NULL, '2021-09-18 16:39:41', 2),
|
||||
(45, '生产性生物资产', 24, '[{"operator":"+","rules":0,"subjectId":1369,"subjectName":"生产性生物资产","subjectNumber":"1621"},{"operator":"-","rules":0,"subjectId":1370,"subjectName":"生产性生物资产累计折旧","subjectNumber":"1622"}]', NULL, 1, 25, 25, 1, NULL, '2021-09-18 16:39:41', 2),(46, '所有者权益(或股东权益):', 0, '[]', NULL, 0, 51, 25, 1, NULL, '2021-09-18 16:39:41', 1),
|
||||
(47, '无形资产', 25, '[{"operator":"+","rules":0,"subjectId":1371,"subjectName":"无形资产","subjectNumber":"1701"},{"operator":"-","rules":0,"subjectId":1372,"subjectName":"累计摊销","subjectNumber":"1702"}]', NULL, 1, 26, 26, 1, NULL, '2021-09-18 16:39:41', 2),(48, '实收资本(或股本)', 48, '[{"operator":"+","rules":0,"subjectId":1418,"subjectName":"实收资本","subjectNumber":"3001"}]', NULL, 1, 52, 26, 1, NULL, '2021-09-18 16:39:41', 2),
|
||||
(49, '开发支出', 26, '[{"operator":"+","rules":0,"subjectId":1441,"subjectName":"研发支出","subjectNumber":"4301"}]', NULL, 1, 27, 27, 1, NULL, '2021-09-18 16:39:41', 2),
|
||||
(50, '资本公积', 49, '[{"operator":"+","rules":0,"subjectId":1419,"subjectName":"资本公积","subjectNumber":"3002"}]', NULL, 1, 53, 27, 1, NULL, '2021-09-18 16:39:41', 2),
|
||||
(51, '长期待摊费用', 27, '[{"operator":"+","rules":0,"subjectId":1373,"subjectName":"长期待摊费用","subjectNumber":"1801"}]', NULL, 1, 28, 28, 1, NULL, '2021-09-18 16:39:41', 2),
|
||||
(52, '盈余公积', 50, '[{"operator":"+","rules":0,"subjectId":1424,"subjectName":"盈余公积","subjectNumber":"3101"}]', NULL, 1, 54, 28, 1, NULL, '2021-09-18 16:39:41', 2),
|
||||
(53, '其他非流动资产', 28, '[{"operator":"+","rules":0,"subjectId":1374,"subjectName":"待处理财产损溢","subjectNumber":"1901"}]', NULL, 1, 29, 29, 1, NULL, '2021-09-18 16:39:41', 2),
|
||||
(54, '未分配利润', 51, '[{"operator":"+","rules":0,"subjectId":1428,"subjectName":"本年利润","subjectNumber":"3103"},{"operator":"+","rules":0,"subjectId":1429,"subjectName":"利润分配","subjectNumber":"3104"}]', NULL, 1, 55, 29, 1, NULL, '2021-09-18 16:39:41', 2),
|
||||
(55, '非流动资产合计', 29, '["L16+L17+L20+L21+L22+L23+L24+L25+L26+L27+L28"]', NULL, 0, 30, 30, 1, NULL, '2021-09-18 16:39:41', 1),
|
||||
(56, '所有者权益(或股东权益)合计', 52, '["L48+L49+L50+L51"]', NULL, 0, 56, 30, 1, NULL, '2021-09-18 16:39:41', 1),
|
||||
(57, '资产总计', 30, '["L15+L29"]', NULL, 0, 31, 31, 1, NULL, '2021-09-18 16:39:41', 1),
|
||||
(58, '负债和所有者权益(或股东权益)总计', 53, '["L47+L52"]', NULL, 0, 57, 31, 1, NULL, '2021-09-18 16:39:41', 1),
|
||||
(59, '一、营业收入', 1, '[{"operator":"+","rules":6,"subjectId":1444,"subjectName":"主营业务收入","subjectNumber":"5001"},{"operator":"+","rules":6,"subjectId":1445,"subjectName":"其他业务收入","subjectNumber":"5051"}]', NULL, 1, 0, NULL, 2, NULL, '2021-09-18 16:39:56', 1),
|
||||
(60, '减:营业成本', 2, '[{"operator":"+","rules":5,"subjectId":1453,"subjectName":"主营业务成本","subjectNumber":"5401"},{"operator":"+","rules":5,"subjectId":1454,"subjectName":"其他业务成本","subjectNumber":"5402"}]', NULL, 1, 1, NULL, 2, NULL, '2021-09-18 16:39:56', 2),
|
||||
(61, '税金及附加', 3, '[{"operator":"+","rules":5,"subjectId":1455,"subjectName":"税金及附加","subjectNumber":"5403"}]', NULL, 1, 2, NULL, 2, NULL, '2021-09-18 16:39:56', 2),
|
||||
(62, '其中:消费税', 4, '[{"operator":"+","rules":5,"subjectId":1456,"subjectName":"消费税","subjectNumber":"540301"}]', NULL, 1, 3, NULL, 2, NULL, '2021-09-18 16:39:56', 3),
|
||||
(63, '城市维护建设税', 6, '[{"operator":"+","rules":5,"subjectId":1457,"subjectName":"城市维护建设税","subjectNumber":"540303"}]', NULL, 1, 4, NULL, 2, NULL, '2021-09-18 16:39:56', 3),
|
||||
(64, '资源税', 7, '[{"operator":"+","rules":5,"subjectId":1458,"subjectName":"资源税","subjectNumber":"540304"}]', NULL, 1, 5, NULL, 2, NULL, '2021-09-18 16:39:56', 3),
|
||||
(65, '土地增值税', 8, '[{"operator":"+","rules":5,"subjectId":1459,"subjectName":"土地增值税","subjectNumber":"540305"}]', NULL, 1, 6, NULL, 2, NULL, '2021-09-18 16:39:56', 3),
|
||||
(66, '城镇土地使用税、房产税、车船税、印花税', 9, '[{"operator":"+","rules":5,"subjectId":1460,"subjectName":"城镇土地使用税","subjectNumber":"540306"},{"operator":"+","rules":5,"subjectId":1462,"subjectName":"车船税","subjectNumber":"540308"},{"operator":"+","rules":5,"subjectId":1461,"subjectName":"房产税","subjectNumber":"540307"},{"operator":"+","rules":5,"subjectId":1463,"subjectName":"印花税","subjectNumber":"540309"}]', NULL, 1, 7, NULL, 2, NULL, '2021-09-18 16:39:56', 3),
|
||||
(67, '教育费附加、矿产资源补偿费、排污费', 10, '[{"operator":"+","rules":5,"subjectId":1466,"subjectName":"排污费","subjectNumber":"540312"},{"operator":"+","rules":5,"subjectId":1465,"subjectName":"矿产资源补偿费","subjectNumber":"540311"},{"operator":"+","rules":5,"subjectId":1464,"subjectName":"教育费附加","subjectNumber":"540310"}]', NULL, 1, 8, NULL, 2, NULL, '2021-09-18 16:39:56', 3),
|
||||
(68, '销售费用', 11, '[{"operator":"+","rules":5,"subjectId":1468,"subjectName":"销售费用","subjectNumber":"5601"}]', NULL, 1, 9, NULL, 2, NULL, '2021-09-18 16:39:56', 2),
|
||||
(69, '其中:商品维修费', 12, '[{"operator":"+","rules":5,"subjectId":1483,"subjectName":"商品维修费","subjectNumber":"560115"}]', NULL, 1, 10, NULL, 2, NULL, '2021-09-18 16:39:56', 3),
|
||||
(70, '广告费和业务宣传费', 13, '[{"operator":"+","rules":5,"subjectId":1484,"subjectName":"广告和业务宣传费","subjectNumber":"560116"}]', NULL, 1, 11, NULL, 2, NULL, '2021-09-18 16:39:56', 3),
|
||||
(71, '管理费用', 14, '[{"operator":"+","rules":5,"subjectId":1486,"subjectName":"管理费用","subjectNumber":"5602"}]', NULL, 1, 12, NULL, 2, NULL, '2021-09-18 16:39:56', 2),
|
||||
(72, '其中:开办费', 15, '[{"operator":"+","rules":5,"subjectId":1499,"subjectName":"开办费","subjectNumber":"560213"}]', NULL, 1, 13, NULL, 2, NULL, '2021-09-18 16:39:56', 3),
|
||||
(73, '业务招待费', 16, '[{"operator":"+","rules":5,"subjectId":1491,"subjectName":"交际应酬费","subjectNumber":"560205"}]', NULL, 1, 14, NULL, 2, NULL, '2021-09-18 16:39:56', 3),
|
||||
(74, '研究费用', 17, '[{"operator":"+","rules":5,"subjectId":1501,"subjectName":"研究费用","subjectNumber":"560215"}]', NULL, 1, 15, NULL, 2, NULL, '2021-09-18 16:39:56', 3),
|
||||
(75, '财务费用', 18, '[{"operator":"+","rules":5,"subjectId":1503,"subjectName":"财务费用","subjectNumber":"5603"}]', NULL, 1, 16, NULL, 2, NULL, '2021-09-18 16:39:56', 2),
|
||||
(76, '其中:利息费用(收入以“-”号填列)', 19, '[{"operator":"+","rules":5,"subjectId":1505,"subjectName":"利息","subjectNumber":"560302"}]', NULL, 1, 17, NULL, 2, NULL, '2021-09-18 16:39:56', 3),
|
||||
(77, '加:投资收益(损失以“-”号填列)', 20, '[{"operator":"+","rules":6,"subjectId":1446,"subjectName":"投资收益","subjectNumber":"5111"}]', NULL, 1, 18, NULL, 2, NULL, '2021-09-18 16:39:56', 2),
|
||||
(78, '二、营业利润(亏损以“-”号填列)', 21, '["L1-L2-L3-L11-L14-L18+L20"]', NULL, 0, 19, NULL, 2, NULL, '2021-09-18 16:39:56', 1),(79, '加:营业外收入', 22, '[{"operator":"+","rules":6,"subjectId":1447,"subjectName":"营业外收入","subjectNumber":"5301"}]', NULL, 1, 20, NULL, 2, NULL, '2021-09-18 16:39:56', 2),
|
||||
(80, '其中:政府补助', 23, '[{"operator":"+","rules":6,"subjectId":1449,"subjectName":"政府补助","subjectNumber":"530102"}]', NULL, 1, 21, NULL, 2, NULL, '2021-09-18 16:39:56', 3),
|
||||
(81, '减:营业外支出', 24, '[{"operator":"+","rules":5,"subjectId":1508,"subjectName":"营业外支出","subjectNumber":"5711"}]', NULL, 1, 22, NULL, 2, NULL, '2021-09-18 16:39:56', 2),
|
||||
(82, '其中:坏账损失', 25, '[{"operator":"+","rules":5,"subjectId":1511,"subjectName":"坏账损失","subjectNumber":"571103"}]', NULL, 1, 23, NULL, 2, NULL, '2021-09-18 16:39:56', 3),
|
||||
(83, '无法收回的长期债券投资损失', 26, '[{"operator":"+","rules":5,"subjectId":1512,"subjectName":"无法收回的长期债券投资损失","subjectNumber":"571104"}]', NULL, 1, 24, NULL, 2, NULL, '2021-09-18 16:39:56', 3),
|
||||
(84, '无法收回的长期股权投资损失', 27, '[{"operator":"+","rules":5,"subjectId":1513,"subjectName":"无法收回的长期股权投资损失","subjectNumber":"571105"}]', NULL, 1, 25, NULL, 2, NULL, '2021-09-18 16:39:56', 3),
|
||||
(85, '自然灾害等不可抗力因素造成的损失', 28, '[{"operator":"+","rules":5,"subjectId":1514,"subjectName":"自然灾害等不可抗力造成的损失","subjectNumber":"571106"}]', NULL, 1, 26, NULL, 2, NULL, '2021-09-18 16:39:56', 3),
|
||||
(86, '税收滞纳金', 29, '[{"operator":"+","rules":5,"subjectId":1515,"subjectName":"税收滞纳金","subjectNumber":"571107"}]', NULL, 1, 27, NULL, 2, NULL, '2021-09-18 16:39:56', 3),
|
||||
(87, '三、利润总额(亏损总额以“-”号填列)', 30, '["L21+L22-L24"]', NULL, 0, 28, NULL, 2, NULL, '2021-09-18 16:39:56', 1),
|
||||
(88, '减:所得税费用', 31, '[{"operator":"+","rules":5,"subjectId":1519,"subjectName":"所得税费用","subjectNumber":"5801"}]', NULL, 1, 29, NULL, 2, NULL, '2021-09-18 16:39:56', 2),
|
||||
(89, '四、净利润(净亏损以“-”号填列)', 32, '["L30-L31"]', NULL, 0, 30, NULL, 2, NULL, '2021-09-18 16:39:56', 1);
|
||||
|
||||
INSERT IGNORE INTO `fms_report_template` (`id`, `name`, `row_no`, `formula`, `remark`, `editable`, `sort`, `row_id`, `type`, `category`, `create_time`, `level`) VALUES
|
||||
(90, '一、经营活动产生的现金流量:', 0, '[]', NULL, 0, 0, 0, 3, 1, '2021-09-18 16:39:59', 1),
|
||||
(91, '销售产成品、商品、提供劳务收到的现金', 1, '["IN1-((BA[3,2]-BA[3,1])+(BA[4,2]-BA[4,1])-(BA[34,2]-BA[34,1]))+EX5+EX11"]', NULL, 0, 1, 1, 3, 1, '2021-09-18 16:39:59', 2),
|
||||
(92, '收到其他与经营活动有关的现金', 2, '["((IN22-(BA[8,2]-BA[8,1])) < 0 ? 0 : (IN22-(BA[8,2]-BA[8,1])))"]', NULL, 0, 2, 2, 3, 1, '2021-09-18 16:39:59', 2),
|
||||
(93, '购买原材料、商品、接受劳务支付的现金', 3, '["IN2+(BA[9,2]-BA[9,1])-(BA[32,2]-BA[32,1])-(BA[33,2]-BA[33,1])+(BA[5,2]-BA[5,1])+EX6"]', NULL, 0, 3, 3, 3, 1, '2021-09-18 16:39:59', 2),
|
||||
(94, '支付的职工薪酬', 4, '["EX4"]', NULL, 0, 4, 4, 3, 1, '2021-09-18 16:39:59', 2),
|
||||
(95, '支付的税费', 5, '["EX12-(BA[36,2]-BA[36,1])"]', NULL, 0, 5, 5, 3, 1, '2021-09-18 16:39:59', 2),
|
||||
(96, '支付其他与经营活动有关的现金', 6, '["(((IN24+IN14+IN11+IN18+IN3+IN19)-(EX15+EX16+EX4+EX18+EX14+EX17+EX13+EX10+EX9+EX8-EX19)+(BA[14,2]-BA[14,1])-(BA[39,2]-BA[39,1]-(BA[35,2]-BA[35,1])-(BA[40,2]-BA[40,1]))) < 0 ? 0 : ((IN24+IN14+IN11+IN18+IN3+IN19)-(EX15+EX16+EX4+EX18+EX14+EX17+EX13+EX10+EX9+EX8-EX19)+(BA[14,2]-BA[14,1])-(BA[39,2]-BA[39,1]-(BA[35,2]-BA[35,1])-(BA[40,2]-BA[40,1]))))"]', NULL, 0, 6, 6, 3, 1, '2021-09-18 16:39:59', 2),
|
||||
(97, '经营活动产生的现金流量净额', 7, '["L1+L2-L3-L4-L5-L6"]', NULL, 0, 7, 7, 3, 1, '2021-09-18 16:39:59', 2),
|
||||
(98, '二、投资活动产生的现金流量:', 0, '[]', NULL, 0, 8, 8, 3, 1, '2021-09-18 16:39:59', 1),
|
||||
(99, '收回短期投资、长期债券投资和长期股权投资收到的现金', 8, '["((BA[2,2]-BA[2,1])+(BA[16,2]-BA[16,1])+(BA[17,2]-BA[17,1]))*(-1) < 0 ? 0 : ((BA[2,2]-BA[2,1])+(BA[16,2]-BA[16,1])+(BA[17,2]-BA[17,1]))*(-1)"]', NULL, 0, 9, 9, 3, 1, '2021-09-18 16:39:59', 2),
|
||||
(100, '取得投资收益收到的现金', 9, '["IN20-((BA[7,2]-BA[7,1])+(BA[6,2]-BA[6,1]))"]', NULL, 0, 10, 10, 3, 1, '2021-09-18 16:39:59', 2),
|
||||
(101, '处置固定资产、无形资产和其他非流动资产收回的现金净额', 10, '[]', NULL, 0, 11, 11, 3, 1, '2021-09-18 16:39:59', 2),
|
||||
(102, '短期投资、长期债券投资和长期股权投资支付的现金', 11, '["(((BA[2,2]-BA[2,1])+(BA[16,2]-BA[16,1])+(BA[17,2]-BA[17,1]))*(-1)) < 0 ? (((BA[2,2]-BA[2,1])+(BA[16,2]-BA[16,1])+(BA[17,2]-BA[17,1]))*(-1))*(-1) : 0"]', NULL, 0, 12, 12, 3, 1, '2021-09-18 16:39:59', 2),
|
||||
(103, '购建固定资产、无形资产和其他非流动资产支付的现金', 12, '["((BA[21,2]-BA[21,1])+(BA[20,2]-BA[20,1])+(BA[25,2]-BA[25,1])+(BA[28,2]-BA[28,1])+(BA[23,2]-BA[23,1])+(BA[22,2]-BA[22,1])+(BA[26,2]-BA[26,1])+(BA[27,2]-BA[27,1])+(BA[24,2]-BA[24,1]))+(EX15+EX16+EX18)+(EX13+EX17)"]', NULL, 0, 13, 13, 3, 1, '2021-09-18 16:39:59', 2),
|
||||
(104, '投资活动产生的现金流量净额', 13, '["L8+L9+L10-L11-L12"]', NULL, 0, 14, 14, 3, 1, '2021-09-18 16:39:59', 2),
|
||||
(105, '三、筹资活动产生的现金流量:', 0, '[]', NULL, 0, 15, 15, 3, 1, '2021-09-18 16:39:59', 1),
|
||||
(106, '取得借款收到的现金', 14, '["((BA[31,2]-BA[31,1])+(BA[42,2]-BA[42,1])+(BA[43,2]-BA[43,1])+(BA[44,2]-BA[44,1])+(BA[45,2]-BA[45,1])) < 0 ? 0 : ((BA[31,2]-BA[31,1])+(BA[42,2]-BA[42,1])+(BA[43,2]-BA[43,1])+(BA[44,2]-BA[44,1])+(BA[45,2]-BA[45,1]))"]', NULL, 0, 16, 16, 3, 1, '2021-09-18 16:39:59', 2),
|
||||
(107, '吸收投资者投资收到的现金', 15, '["(BA[48,2]-BA[48,1])+(BA[49,2]-BA[49,1])+(BA[50,2]-BA[50,1])"]', NULL, 0, 17, 17, 3, 1, '2021-09-18 16:39:59', 2),
|
||||
(108, '偿还借款本金支付的现金', 16, '["((BA[31,2]-BA[31,1])+(BA[42,2]-BA[42,1])+(BA[43,2]-BA[43,1])+(BA[44,2]-BA[44,1])+(BA[45,2]-BA[45,1])) < 0 ? ((BA[31,2]-BA[31,1])+(BA[42,2]-BA[42,1])+(BA[43,2]-BA[43,1])+(BA[44,2]-BA[44,1])+(BA[45,2]-BA[45,1]))*(-1) : 0"]', NULL, 0, 18, 18, 3, 1, '2021-09-18 16:39:59', 2),
|
||||
(109, '偿还借款利息支付的现金', 17, '["IN19-(BA[37,2]-BA[37,1])"]', NULL, 0, 19, 19, 3, 1, '2021-09-18 16:39:59', 2),
|
||||
(110, '分配利润支付的现金', 18, '["IN32-((BA[38,2]-BA[38,1])+(BA[51,2]-BA[51,1]))"]', NULL, 0, 20, 20, 3, 1, '2021-09-18 16:39:59', 2),
|
||||
(111, '筹资活动产生的现金流量净额', 19, '["L14+L15-L16-L17-L18"]', NULL, 0, 21, 21, 3, 1, '2021-09-18 16:39:59', 2),(112, '四、现金净增加额', 20, '["L7+L13+L19"]', NULL, 0, 22, 22, 3, 1, '2021-09-18 16:39:59', 1),
|
||||
(113, '加:期初现金余额', 21, '["BA[1,1]"]', NULL, 0, 23, 23, 3, 1, '2021-09-18 16:39:59', 2),
|
||||
(114, '五、期末现金余额', 22, '["L20+L21"]', NULL, 0, 24, 24, 3, 1, '2021-09-18 16:39:59', 1);
|
||||
|
||||
INSERT IGNORE INTO `fms_report_template` (`id`, `name`, `row_no`, `formula`, `remark`, `editable`, `sort`, `row_id`, `type`, `category`, `create_time`, `level`) VALUES
|
||||
(115, '支付给职工的工资', 1, '[{"operator":"+","rules":5,"subjectId":1379,"subjectName":"应付职工薪酬","subjectNumber":"2211"}]', NULL, 1, 0, NULL, 4, 1, '2021-09-18 16:40:02', 1),
|
||||
(116, '支付给职工的四金', 2, '[]', NULL, 1, 1, NULL, 4, 1, '2021-09-18 16:40:02', 1),
|
||||
(117, '支付给职工的其他福利费', 3, '[]', NULL, 1, 2, NULL, 4, 1, '2021-09-18 16:40:02', 1),
|
||||
(118, '支付给职工以及为职工支付的现金', 4, '["L1+L2+L3"]', '行 1、2、3 之和,参与现金流量表行 4 计算', 0, 3, NULL, 4, 1, '2021-09-18 16:40:02', 2),
|
||||
(119, '销项税额(小规模纳税人填本年累计应交增值税)', 5, '[{"operator":"+","rules":6,"subjectId":1388,"subjectName":"销项税额","subjectNumber":"22210107"}]', '参与现金流量表行 1 计算', 1, 4, NULL, 4, 1, '2021-09-18 16:40:02', 1),
|
||||
(120, '进项税额(小规模纳税人不填此项)', 6, '[{"operator":"+","rules":5,"subjectId":1382,"subjectName":"进项税额","subjectNumber":"22210101"}]', '参与现金流量表行 3 计算', 1, 5, NULL, 4, 1, '2021-09-18 16:40:02', 1),
|
||||
(121, '应交增值税', 7, '["L5-L6"]', '行 5、6 之差', 0, 6, NULL, 4, 1, '2021-09-18 16:40:02', 2),
|
||||
(122, '主营业务税金及附加', 8, '[{"operator":"+","rules":7,"subjectId":1455,"subjectName":"税金及附加","subjectNumber":"5403"}]', '参与现金流量表行 6 计算', 1, 7, NULL, 4, 1, '2021-09-18 16:40:02', 1),
|
||||
(123, '所得税', 9, '[{"operator":"+","rules":7,"subjectId":1519,"subjectName":"所得税费用","subjectNumber":"5801"}]', '参与现金流量表行 6 计算', 1, 8, NULL, 4, 1, '2021-09-18 16:40:02', 1),
|
||||
(124, '管理费用中列支的税金', 10, '[]', '参与现金流量表行 6 计算', 1, 9, NULL, 4, 1, '2021-09-18 16:40:02', 1),
|
||||
(125, '在其他业务支出中列支的税金', 11, '[]', '参与现金流量表行 1 计算', 1, 10, NULL, 4, 1, '2021-09-18 16:40:02', 1),
|
||||
(126, '应缴纳各项税金合计', 12, '["L7+L8+L9+L10+L11"]', '行 7、8、9、10、11 之和,参与现金流量表行 5 计算', 0, 11, NULL, 4, 1, '2021-09-18 16:40:02', 2),
|
||||
(127, '计提固定资产折旧', 13, '[{"operator":"+","rules":6,"subjectId":1370,"subjectName":"生产性生物资产累计折旧","subjectNumber":"1622"},{"operator":"+","rules":6,"subjectId":1361,"subjectName":"累计折旧","subjectNumber":"1602"}]', '参与现金流量表行 6、12 计算', 1, 12, NULL, 4, 1, '2021-09-18 16:40:02', 1),
|
||||
(128, '利息支出', 14, '[{"operator":"+","rules":7,"subjectId":1505,"subjectName":"利息","subjectNumber":"560302"}]', '参与现金流量表行 6 计算', 1, 13, NULL, 4, 1, '2021-09-18 16:40:02', 1),
|
||||
(129, '无形资产摊销', 15, '[]', '参与现金流量表行 6、12 计算', 1, 14, NULL, 4, 1, '2021-09-18 16:40:02', 1),
|
||||
(130, '长期待摊费用摊销', 16, '[]', '参与现金流量表行 6、12 计算', 1, 15, NULL, 4, 1, '2021-09-18 16:40:02', 1),
|
||||
(131, '计提的各项减值准备(仅反映本年与上年计提的差额)', 17, '[]', '参与现金流量表行 12 计算', 1, 16, NULL, 4, 1, '2021-09-18 16:40:02', 1),
|
||||
(132, '处置固定资产、无形资产和其他长期资产的损失(减:收益)', 18, '[]', '参与现金流量表行 6、12 计算', 1, 17, NULL, 4, 1, '2021-09-18 16:40:02', 1),
|
||||
(133, '汇兑收益(减:汇兑损失)', 19, '[]', '参与现金流量表行 6 计算', 1, 18, NULL, 4, 1, '2021-09-18 16:40:02', 1),
|
||||
(134, '不涉及现金收支的投资活动', 20, '[]', '参与现金流量表行 8 计算', 1, 19, NULL, 4, 1, '2021-09-18 16:40:02', 1),
|
||||
(135, '不涉及现金收支的筹资活动', 21, '[]', '参与现金流量表行 15 计算', 1, 20, NULL, 4, 1, '2021-09-18 16:40:02', 1);
|
||||
|
||||
-- 修正旧版默认公式;仅匹配原始公式,保留用户自定义内容
|
||||
UPDATE `fms_report_template` SET `formula` = '[{"operator":"+","rules":0,"subjectId":1345,"subjectName":"材料采购","subjectNumber":"1401"},{"operator":"+","rules":0,"subjectId":1346,"subjectName":"在途物资","subjectNumber":"1402"},{"operator":"+","rules":0,"subjectId":1347,"subjectName":"原材料","subjectNumber":"1403"},{"operator":"+","rules":0,"subjectId":1348,"subjectName":"材料成本差异","subjectNumber":"1404"},{"operator":"+","rules":0,"subjectId":1349,"subjectName":"库存商品","subjectNumber":"1405"},{"operator":"-","rules":0,"subjectId":1350,"subjectName":"商品进销差价","subjectNumber":"1407"},{"operator":"+","rules":0,"subjectId":1351,"subjectName":"委托加工物资","subjectNumber":"1408"},{"operator":"+","rules":0,"subjectId":1352,"subjectName":"周转材料","subjectNumber":"1411"},{"operator":"+","rules":0,"subjectId":1353,"subjectName":"消耗性生物资产","subjectNumber":"1421"},{"operator":"+","rules":0,"subjectId":1437,"subjectName":"生产成本","subjectNumber":"4001"},{"operator":"+","rules":0,"subjectId":1440,"subjectName":"制造费用","subjectNumber":"4101"},{"operator":"+","rules":0,"subjectId":1442,"subjectName":"工程施工","subjectNumber":"4401"},{"operator":"+","rules":0,"subjectId":1443,"subjectName":"机械作业","subjectNumber":"4403"}]' WHERE `id` = 19 AND `formula` = '[{"operator":"+","rules":0,"subjectId":1345,"subjectName":"材料采购","subjectNumber":"1401"},{"operator":"+","rules":0,"subjectId":1346,"subjectName":"在途物资","subjectNumber":"1402"},{"operator":"+","rules":0,"subjectId":1347,"subjectName":"原材料","subjectNumber":"1403"},{"operator":"+","rules":0,"subjectId":1348,"subjectName":"材料成本差异","subjectNumber":"1404"},{"operator":"+","rules":0,"subjectId":1349,"subjectName":"库存商品","subjectNumber":"1405"},{"operator":"+","rules":0,"subjectId":1350,"subjectName":"商品进销差价","subjectNumber":"1407"},{"operator":"+","rules":0,"subjectId":1351,"subjectName":"委托加工物资","subjectNumber":"1408"},{"operator":"+","rules":0,"subjectId":1352,"subjectName":"周转材料","subjectNumber":"1411"},{"operator":"+","rules":0,"subjectId":1353,"subjectName":"消耗性生物资产","subjectNumber":"1421"},{"operator":"+","rules":0,"subjectId":1437,"subjectName":"生产成本","subjectNumber":"4001"},{"operator":"+","rules":0,"subjectId":1440,"subjectName":"制造费用","subjectNumber":"4101"},{"operator":"+","rules":0,"subjectId":1442,"subjectName":"工程施工","subjectNumber":"4401"},{"operator":"+","rules":0,"subjectId":1443,"subjectName":"机械作业","subjectNumber":"4403"}]';
|
||||
UPDATE `fms_report_template` SET `formula` = '[{"operator":"+","rules":0,"subjectId":1369,"subjectName":"生产性生物资产","subjectNumber":"1621"},{"operator":"-","rules":0,"subjectId":1370,"subjectName":"生产性生物资产累计折旧","subjectNumber":"1622"}]' WHERE `id` = 45 AND `formula` = '[{"operator":"+","rules":0,"subjectId":1369,"subjectName":"生产性生物资产","subjectNumber":"1621"},{"operator":"+","rules":0,"subjectId":1370,"subjectName":"生产性生物资产累计折旧","subjectNumber":"1622"}]';
|
||||
UPDATE `fms_report_template` SET `formula` = '[{"operator":"+","rules":0,"subjectId":1371,"subjectName":"无形资产","subjectNumber":"1701"},{"operator":"-","rules":0,"subjectId":1372,"subjectName":"累计摊销","subjectNumber":"1702"}]' WHERE `id` = 47 AND `formula` = '[{"operator":"+","rules":0,"subjectId":1371,"subjectName":"无形资产","subjectNumber":"1701"},{"operator":"+","rules":0,"subjectId":1372,"subjectName":"累计摊销","subjectNumber":"1702"}]';
|
||||
UPDATE `fms_report_template` SET `formula` = '["L1-L2-L3-L11-L14-L18+L20"]' WHERE `id` = 78 AND `formula` = '["L1-L2-L3-L11-L14-L18"]';
|
||||
UPDATE `fms_report_template` SET `formula` = '["L14+L15-L16-L17-L18"]' WHERE `id` = 111 AND `formula` = '["L14+L15-L17-L18"]';
|
||||
File diff suppressed because it is too large
Load Diff
+18
@@ -7,6 +7,7 @@ import cn.idev.excel.converters.Converter;
|
||||
import cn.idev.excel.enums.CellDataTypeEnum;
|
||||
import cn.idev.excel.metadata.GlobalConfiguration;
|
||||
import cn.idev.excel.metadata.data.ReadCellData;
|
||||
import cn.idev.excel.metadata.data.WriteCellData;
|
||||
import cn.idev.excel.metadata.property.ExcelContentProperty;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@@ -43,4 +44,21 @@ public class AreaConvert implements Converter<Object> {
|
||||
return Convert.convert(fieldClazz, area.getId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public WriteCellData<String> convertToExcelData(Object object, ExcelContentProperty contentProperty,
|
||||
GlobalConfiguration globalConfiguration) {
|
||||
// 空时,返回空
|
||||
if (object == null) {
|
||||
return new WriteCellData<>("");
|
||||
}
|
||||
// 将地区编号转换成地区全路径(与 convertToJavaData 的 parseArea 对应,使用 "/" 分隔)
|
||||
Integer id = Convert.convert(Integer.class, object);
|
||||
String path = AreaUtils.format(id, "/");
|
||||
if (path == null) {
|
||||
log.error("[convertToExcelData][id({}) 转换不了]", id);
|
||||
return new WriteCellData<>("");
|
||||
}
|
||||
return new WriteCellData<>(path);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+6
@@ -133,6 +133,12 @@ public class SelectSheetWriteHandler implements SheetWriteHandler {
|
||||
DataValidationHelper helper = writeSheetHolder.getSheet().getDataValidationHelper(); // 需要设置下拉框的 sheet 页的数据验证助手
|
||||
Workbook workbook = writeWorkbookHolder.getWorkbook(); // 获得工作簿
|
||||
List<KeyValue<Integer, List<String>>> keyValues = convertList(selectMap.entrySet(), entry -> new KeyValue<>(entry.getKey(), entry.getValue()));
|
||||
// 对应 issue:https://github.com/YunaiV/ruoyi-vue-pro/pull/1217
|
||||
// 空字典/空选项源:跳过该列下拉,避免拼出 $F$1:$F$0 非法区间致 POI FormulaParseException(该列退化为普通文本列)
|
||||
keyValues.removeIf(item -> CollUtil.isEmpty(item.getValue()));
|
||||
if (CollUtil.isEmpty(keyValues)) {
|
||||
return; // 全部为空时连「字典sheet」都不创建
|
||||
}
|
||||
keyValues.sort(Comparator.comparing(item -> item.getValue().size())); // 升序不然创建下拉会报错
|
||||
|
||||
// 2. 创建数据字典的 sheet 页
|
||||
|
||||
+1
-1
@@ -13,7 +13,7 @@ import org.springframework.core.task.SimpleAsyncTaskExecutor;
|
||||
* 异步任务 Configuration
|
||||
*/
|
||||
@AutoConfiguration
|
||||
@EnableAsync
|
||||
@EnableAsync(proxyTargetClass = true) // proxyTargetClass=true:与 @EnableTransactionManagement(proxyTargetClass = true) 保持一致,否则会有 https://github.com/YunaiV/ruoyi-vue-pro/pull/1223 问题
|
||||
public class YudaoAsyncAutoConfiguration {
|
||||
|
||||
@Bean
|
||||
|
||||
+1
-1
@@ -13,7 +13,7 @@ import lombok.Data;
|
||||
* @author lesan
|
||||
*/
|
||||
@TableName(value = "ai_workflow", autoResultMap = true)
|
||||
@KeySequence("ai_workflow") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
|
||||
@KeySequence("ai_workflow_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
|
||||
@Data
|
||||
public class AiWorkflowDO extends BaseDO {
|
||||
|
||||
|
||||
+1
@@ -44,6 +44,7 @@ public interface ErrorCodeConstants {
|
||||
ErrorCode PROCESS_INSTANCE_HTTP_CALL_ERROR = new ErrorCode(1_009_004_006, "流程 Http 请求调用失败");
|
||||
ErrorCode PROCESS_INSTANCE_APPROVE_USER_SELECT_ASSIGNEES_NOT_CONFIG = new ErrorCode(1_009_004_007, "下一个任务({})的审批人未配置");
|
||||
ErrorCode PROCESS_INSTANCE_CANCEL_CHILD_FAIL_NOT_ALLOW = new ErrorCode(1_009_004_008, "子流程取消失败,子流程不允许取消");
|
||||
ErrorCode PROCESS_INSTANCE_START_FORM_FIELD_REQUIRED = new ErrorCode(1_009_004_009, "发起流程失败,表单字段({})为必填项,不能为空");
|
||||
|
||||
// ========== 流程任务 1-009-005-000 ==========
|
||||
ErrorCode TASK_OPERATE_FAIL_ASSIGN_NOT_SELF = new ErrorCode(1_009_005_001, "操作失败,原因:该任务的审批人不是你");
|
||||
|
||||
+42
@@ -22,6 +22,7 @@ import cn.iocoder.yudao.module.bpm.convert.task.BpmProcessInstanceConvert;
|
||||
import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmProcessDefinitionInfoDO;
|
||||
import cn.iocoder.yudao.module.bpm.dal.redis.BpmProcessIdRedisDAO;
|
||||
import cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants;
|
||||
import cn.iocoder.yudao.module.bpm.enums.definition.BpmModelFormTypeEnum;
|
||||
import cn.iocoder.yudao.module.bpm.enums.definition.BpmModelTypeEnum;
|
||||
import cn.iocoder.yudao.module.bpm.enums.definition.BpmSimpleModelNodeTypeEnum;
|
||||
import cn.iocoder.yudao.module.bpm.enums.task.BpmAttachmentTypeEnum;
|
||||
@@ -763,11 +764,52 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService
|
||||
// 获得流程定义
|
||||
ProcessDefinition definition = processDefinitionService
|
||||
.getProcessDefinition(createReqVO.getProcessDefinitionId());
|
||||
// 校验动态表单必填字段(前端校验可被接口直调绕过,后端必须兜底,否则必填缺失也会建成脏实例)
|
||||
validateDynamicFormRequiredFields(definition, createReqVO.getVariables());
|
||||
// 发起流程
|
||||
return createProcessInstance0(userId, definition, createReqVO.getVariables(), null,
|
||||
createReqVO.getStartUserSelectAssignees());
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验动态表单的必填字段
|
||||
*
|
||||
* @see <a href="https://github.com/YunaiV/ruoyi-vue-pro/pull/1245">相关 issue</a>
|
||||
*/
|
||||
private void validateDynamicFormRequiredFields(ProcessDefinition definition, Map<String, Object> variables) {
|
||||
BpmProcessDefinitionInfoDO processDefinitionInfo = processDefinitionService
|
||||
.getProcessDefinitionInfo(definition.getId());
|
||||
if (processDefinitionInfo == null
|
||||
|| !BpmModelFormTypeEnum.NORMAL.getType().equals(processDefinitionInfo.getFormType())
|
||||
|| CollUtil.isEmpty(processDefinitionInfo.getFormFields())) {
|
||||
return;
|
||||
}
|
||||
List<String> fieldStrs = processDefinitionInfo.getFormFields();
|
||||
// formFields 是 JSON 数组,每个元素本身又是 JSON 字符串;两种表单设计器并存:
|
||||
// 字段标识可能是 field 或 key,必填标记可能是数组 ["required"] 或对象 {"required": true}
|
||||
for (String fieldStr : fieldStrs) {
|
||||
Map<String, Object> field = JsonUtils.parseMap(fieldStr);
|
||||
boolean required = false;
|
||||
Object validate = field.get("validate");
|
||||
if (validate instanceof List) {
|
||||
required = ((List<?>) validate).contains("required");
|
||||
} else if (validate instanceof Map) {
|
||||
required = Boolean.TRUE.equals(((Map<?, ?>) validate).get("required"));
|
||||
}
|
||||
if (!required) {
|
||||
continue;
|
||||
}
|
||||
String fieldId = Convert.toStr(field.get("field"), Convert.toStr(field.get("key")));
|
||||
if (StrUtil.isEmpty(fieldId)) {
|
||||
continue;
|
||||
}
|
||||
Object value = variables == null ? null : variables.get(fieldId);
|
||||
if (value == null || (value instanceof String && StrUtil.isBlank((String) value))) {
|
||||
throw exception(PROCESS_INSTANCE_START_FORM_FIELD_REQUIRED, Convert.toStr(field.get("title"), fieldId));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@DataPermission(enable = false) // 关闭数据权限,避免查询不到用户数据。相关案例:https://gitee.com/zhijiantianya/yudao-cloud/issues/ID1UYA
|
||||
public String createProcessInstance(Long userId, @Valid BpmProcessInstanceCreateReqDTO createReqDTO) {
|
||||
|
||||
+1
-1
@@ -125,7 +125,7 @@ public class CrmBusinessRespVO {
|
||||
@Schema(description = "产品条码", requiredMode = Schema.RequiredMode.REQUIRED, example = "20529")
|
||||
private String productNo;
|
||||
@Schema(description = "产品单位", requiredMode = Schema.RequiredMode.REQUIRED, example = "李四")
|
||||
private Integer productUnit;
|
||||
private int productUnit;
|
||||
|
||||
@Schema(description = "产品单价", requiredMode = Schema.RequiredMode.REQUIRED, example = "123.00")
|
||||
private BigDecimal productPrice;
|
||||
|
||||
+1
-1
@@ -143,7 +143,7 @@ public class CrmContractRespVO {
|
||||
@Schema(description = "产品条码", requiredMode = Schema.RequiredMode.REQUIRED, example = "20529")
|
||||
private String productNo;
|
||||
@Schema(description = "产品单位", requiredMode = Schema.RequiredMode.REQUIRED, example = "李四")
|
||||
private Integer productUnit;
|
||||
private int productUnit;
|
||||
|
||||
@Schema(description = "产品单价", requiredMode = Schema.RequiredMode.REQUIRED, example = "123.00")
|
||||
private BigDecimal productPrice;
|
||||
|
||||
+3
-1
@@ -6,12 +6,13 @@ import cn.iocoder.yudao.module.crm.framework.operatelog.core.CrmCustomerParseFun
|
||||
import cn.iocoder.yudao.module.crm.framework.operatelog.core.SysAdminUserParseFunction;
|
||||
import com.mzt.logapi.starter.annotation.DiffLogField;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.DecimalMin;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
|
||||
import javax.validation.constraints.NotNull;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
@@ -67,6 +68,7 @@ public class CrmContractSaveReqVO {
|
||||
|
||||
@Schema(description = "合同金额", example = "5617")
|
||||
@DiffLogField(name = "合同金额")
|
||||
@DecimalMin(value = "0", message = "合同金额不能小于 0")
|
||||
private BigDecimal totalPrice;
|
||||
|
||||
@Schema(description = "客户签约人编号", example = "18546")
|
||||
|
||||
+1
-1
@@ -35,7 +35,7 @@ public class CrmProductRespVO implements VO {
|
||||
@Schema(description = "单位", example = "2")
|
||||
@ExcelProperty(value = "单位", converter = DictConvert.class)
|
||||
@DictFormat(DictTypeConstants.CRM_PRODUCT_UNIT)
|
||||
private Integer unit;
|
||||
private int unit;
|
||||
|
||||
@Schema(description = "价格, 单位:分", requiredMode = Schema.RequiredMode.REQUIRED, example = "8911")
|
||||
@ExcelProperty("价格,单位:分")
|
||||
|
||||
+1
-1
@@ -28,7 +28,7 @@ public class CrmProductSaveReqVO {
|
||||
|
||||
@Schema(description = "单位", example = "2")
|
||||
@DiffLogField(name = "单位", function = CrmProductUnitParseFunction.NAME)
|
||||
private Integer unit;
|
||||
private int unit;
|
||||
|
||||
@Schema(description = "价格", requiredMode = Schema.RequiredMode.REQUIRED, example = "8911")
|
||||
@NotNull(message = "价格不能为空")
|
||||
|
||||
+1
-1
@@ -43,7 +43,7 @@ public class CrmProductDO extends BaseDO {
|
||||
*
|
||||
* 字典 {@link DictTypeConstants#CRM_PRODUCT_UNIT}
|
||||
*/
|
||||
private Integer unit;
|
||||
private int unit;
|
||||
/**
|
||||
* 价格,单位:元
|
||||
*/
|
||||
|
||||
+2
-1
@@ -3,6 +3,7 @@ package cn.iocoder.yudao.module.crm.dal.mysql.receivable;
|
||||
import cn.hutool.core.date.LocalDateTimeUtil;
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageResult;
|
||||
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
|
||||
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
|
||||
import cn.iocoder.yudao.framework.mybatis.core.query.MPJLambdaWrapperX;
|
||||
import cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.plan.CrmReceivablePlanPageReqVO;
|
||||
import cn.iocoder.yudao.module.crm.dal.dataobject.contract.CrmContractDO;
|
||||
@@ -24,7 +25,7 @@ import java.util.Objects;
|
||||
public interface CrmReceivablePlanMapper extends BaseMapperX<CrmReceivablePlanDO> {
|
||||
|
||||
default CrmReceivablePlanDO selectMaxPeriodByContractId(Long contractId) {
|
||||
return selectOne(new MPJLambdaWrapperX<CrmReceivablePlanDO>()
|
||||
return selectOne(new LambdaQueryWrapperX<CrmReceivablePlanDO>()
|
||||
.eq(CrmReceivablePlanDO::getContractId, contractId)
|
||||
.orderByDesc(CrmReceivablePlanDO::getPeriod)
|
||||
.last("LIMIT 1"));
|
||||
|
||||
+2
@@ -81,6 +81,8 @@ public interface ErrorCodeConstants {
|
||||
ErrorCode PRODUCT_NOT_EXISTS = new ErrorCode(1_020_008_000, "产品不存在");
|
||||
ErrorCode PRODUCT_NO_EXISTS = new ErrorCode(1_020_008_001, "产品编号已存在");
|
||||
ErrorCode PRODUCT_NOT_ENABLE = new ErrorCode(1_020_008_002, "产品【{}】已禁用");
|
||||
ErrorCode PRODUCT_DELETE_FAIL_CONTRACT_EXISTS = new ErrorCode(1_020_008_003, "产品已被合同引用,无法删除");
|
||||
ErrorCode PRODUCT_DELETE_FAIL_BUSINESS_EXISTS = new ErrorCode(1_020_008_004, "产品已被商机引用,无法删除");
|
||||
|
||||
// ========== 产品分类 1_020_009_000 ==========
|
||||
ErrorCode PRODUCT_CATEGORY_NOT_EXISTS = new ErrorCode(1_020_009_000, "产品分类不存在");
|
||||
|
||||
+1
-1
@@ -144,7 +144,7 @@ public interface LogRecordConstants {
|
||||
String CRM_RECEIVABLE_CREATE_SUB_TYPE = "创建回款";
|
||||
String CRM_RECEIVABLE_CREATE_SUCCESS = "创建了合同【{getContractById{#receivable.contractId}}】的{{#period != null ? '【第'+ #period +'期】' : '编号为【'+ #receivable.no +'】的'}}回款";
|
||||
String CRM_RECEIVABLE_UPDATE_SUB_TYPE = "更新回款";
|
||||
String CRM_RECEIVABLE_UPDATE_SUCCESS = "更新了合同【{getContractById{#receivable.contractId}}】的{{#period != null ? '【第'+ #period +'期】' : '编号为【'+ #receivable.no +'】的'}}回款: {_DIFF{#updateReqVO}}";
|
||||
String CRM_RECEIVABLE_UPDATE_SUCCESS = "更新了合同【{getContractById{#oldReceivable.contractId}}】的{{#period != null ? '【第'+ #period +'期】' : '编号为【'+ #oldReceivable.no +'】的'}}回款: {_DIFF{#updateReqVO}}";
|
||||
String CRM_RECEIVABLE_DELETE_SUB_TYPE = "删除回款";
|
||||
String CRM_RECEIVABLE_DELETE_SUCCESS = "删除了合同【{getContractById{#receivable.contractId}}】的{{#period != null ? '【第'+ #period +'期】' : '编号为【'+ #receivable.no +'】的'}}回款";
|
||||
String CRM_RECEIVABLE_SUBMIT_SUB_TYPE = "提交回款审批";
|
||||
|
||||
+2
-1
@@ -25,7 +25,8 @@ public class CrmContractParseFunction implements IParseFunction {
|
||||
|
||||
@Override
|
||||
public boolean executeBefore() {
|
||||
return true; // 先转换值后对比
|
||||
// 可见 https://github.com/YunaiV/ruoyi-vue-pro/pull/1247 描述
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+8
@@ -45,6 +45,14 @@ public class CrmPermissionAspect {
|
||||
|
||||
@Before("@annotation(crmPermission)")
|
||||
public void doBefore(JoinPoint joinPoint, CrmPermission crmPermission) {
|
||||
// 对应 issue:https://github.com/YunaiV/ruoyi-vue-pro/pull/1221
|
||||
// 0. 系统调用(定时任务/异步线程等,无登录用户)不走数据权限校验,直接放行。
|
||||
// 数据权限是“按登录用户”的访问控制;/admin-api 外部请求必然有登录用户,
|
||||
// 故登录用户为 null 只可能是系统内部调用(如公海自动掉落定时任务),属可信进程。
|
||||
// 若在此校验,isCrmAdmin() 会以 null 用户查缓存,抛 Null key 异常导致业务失败。
|
||||
if (getUserId() == null) {
|
||||
return;
|
||||
}
|
||||
// 1.1 获取相关属性值
|
||||
Map<String, Object> expressionValues = parseExpressions(joinPoint, crmPermission);
|
||||
Integer bizType = StrUtil.isEmpty(crmPermission.bizTypeValue()) ?
|
||||
|
||||
+8
@@ -164,6 +164,14 @@ public interface CrmBusinessService {
|
||||
*/
|
||||
Long getBusinessCountByCustomerId(Long customerId);
|
||||
|
||||
/**
|
||||
* 获得引用指定产品的商机产品数量
|
||||
*
|
||||
* @param productId 产品编号
|
||||
* @return 数量
|
||||
*/
|
||||
Long getBusinessProductCountByProductId(Long productId);
|
||||
|
||||
/**
|
||||
* 获得使用指定商机状态组的商机数量
|
||||
*
|
||||
|
||||
+5
@@ -375,6 +375,11 @@ public class CrmBusinessServiceImpl implements CrmBusinessService {
|
||||
return businessMapper.selectCount(CrmBusinessDO::getCustomerId, customerId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long getBusinessProductCountByProductId(Long productId) {
|
||||
return businessProductMapper.selectCount(CrmBusinessProductDO::getProductId, productId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long getBusinessCountByStatusTypeId(Long statusTypeId) {
|
||||
return businessMapper.selectCountByStatusTypeId(statusTypeId);
|
||||
|
||||
+8
@@ -177,6 +177,14 @@ public interface CrmContractService {
|
||||
*/
|
||||
List<CrmContractProductDO> getContractProductListByContractId(Long contactId);
|
||||
|
||||
/**
|
||||
* 获得引用指定产品的合同产品数量
|
||||
*
|
||||
* @param productId 产品编号
|
||||
* @return 数量
|
||||
*/
|
||||
Long getContractProductCountByProductId(Long productId);
|
||||
|
||||
/**
|
||||
* 获得待审核合同数量
|
||||
*
|
||||
|
||||
+9
@@ -217,6 +217,10 @@ public class CrmContractServiceImpl implements CrmContractService {
|
||||
|
||||
private void calculateTotalPrice(CrmContractDO contract, List<CrmContractProductDO> contractProducts) {
|
||||
contract.setTotalProductPrice(getSumValue(contractProducts, CrmContractProductDO::getTotalPrice, BigDecimal::add, BigDecimal.ZERO));
|
||||
// 无产品明细的合同,合同金额以传入值为准(支持手工填写合同金额),不做重算覆盖
|
||||
if (CollUtil.isEmpty(contractProducts)) {
|
||||
return;
|
||||
}
|
||||
BigDecimal discountPrice = MoneyUtils.priceMultiplyPercent(contract.getTotalProductPrice(), contract.getDiscountPercent());
|
||||
contract.setTotalPrice(contract.getTotalProductPrice().subtract(discountPrice));
|
||||
}
|
||||
@@ -394,6 +398,11 @@ public class CrmContractServiceImpl implements CrmContractService {
|
||||
return contractProductMapper.selectListByContractId(contactId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long getContractProductCountByProductId(Long productId) {
|
||||
return contractProductMapper.selectCount(CrmContractProductDO::getProductId, productId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long getAuditContractCount(Long userId) {
|
||||
return contractMapper.selectCountByAudit(userId);
|
||||
|
||||
+18
-1
@@ -12,17 +12,20 @@ import cn.iocoder.yudao.module.crm.enums.common.CrmBizTypeEnum;
|
||||
import cn.iocoder.yudao.module.crm.enums.permission.CrmPermissionLevelEnum;
|
||||
import cn.iocoder.yudao.module.crm.enums.product.CrmProductStatusEnum;
|
||||
import cn.iocoder.yudao.module.crm.framework.permission.core.annotations.CrmPermission;
|
||||
import cn.iocoder.yudao.module.crm.service.business.CrmBusinessService;
|
||||
import cn.iocoder.yudao.module.crm.service.contract.CrmContractService;
|
||||
import cn.iocoder.yudao.module.crm.service.permission.CrmPermissionService;
|
||||
import cn.iocoder.yudao.module.crm.service.permission.bo.CrmPermissionCreateReqBO;
|
||||
import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
|
||||
import com.mzt.logapi.context.LogRecordContext;
|
||||
import com.mzt.logapi.service.impl.DiffParseFunction;
|
||||
import com.mzt.logapi.starter.annotation.LogRecord;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
@@ -49,6 +52,12 @@ public class CrmProductServiceImpl implements CrmProductService {
|
||||
@Resource
|
||||
private CrmProductCategoryService productCategoryService;
|
||||
@Resource
|
||||
@Lazy // 延迟加载,避免循环依赖
|
||||
private CrmContractService contractService;
|
||||
@Resource
|
||||
@Lazy // 延迟加载,避免循环依赖
|
||||
private CrmBusinessService businessService;
|
||||
@Resource
|
||||
private CrmPermissionService permissionService;
|
||||
|
||||
@Resource
|
||||
@@ -128,6 +137,14 @@ public class CrmProductServiceImpl implements CrmProductService {
|
||||
public void deleteProduct(Long id) {
|
||||
// 校验存在
|
||||
validateProductExists(id);
|
||||
// 校验未被合同/商机引用,避免删除后产生悬空引用
|
||||
if (contractService.getContractProductCountByProductId(id) > 0) {
|
||||
throw exception(PRODUCT_DELETE_FAIL_CONTRACT_EXISTS);
|
||||
}
|
||||
if (businessService.getBusinessProductCountByProductId(id) > 0) {
|
||||
throw exception(PRODUCT_DELETE_FAIL_BUSINESS_EXISTS);
|
||||
}
|
||||
|
||||
// 删除
|
||||
productMapper.deleteById(id);
|
||||
}
|
||||
|
||||
+1
-1
@@ -116,7 +116,7 @@ public class CrmReceivablePlanServiceImpl implements CrmReceivablePlanService {
|
||||
}
|
||||
// 校验合同存在
|
||||
if (reqVO.getContractId() != null) {
|
||||
CrmContractDO contract = contractService.getContract(reqVO.getContractId());
|
||||
CrmContractDO contract = contractService.validateContract(reqVO.getContractId());
|
||||
reqVO.setCustomerId(contract.getCustomerId());
|
||||
}
|
||||
}
|
||||
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
package cn.iocoder.yudao.module.crm.service.receivable;
|
||||
|
||||
import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
|
||||
import cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.plan.CrmReceivablePlanSaveReqVO;
|
||||
import cn.iocoder.yudao.module.crm.dal.dataobject.contract.CrmContractDO;
|
||||
import cn.iocoder.yudao.module.crm.dal.dataobject.receivable.CrmReceivablePlanDO;
|
||||
import cn.iocoder.yudao.module.crm.dal.mysql.receivable.CrmReceivablePlanMapper;
|
||||
import cn.iocoder.yudao.module.crm.service.contract.CrmContractService;
|
||||
import cn.iocoder.yudao.module.crm.service.permission.CrmPermissionService;
|
||||
import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
|
||||
import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException;
|
||||
import static cn.iocoder.yudao.module.crm.enums.ErrorCodeConstants.CONTRACT_NOT_EXISTS;
|
||||
import static org.mockito.ArgumentMatchers.argThat;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
public class CrmReceivablePlanServiceImplTest extends BaseMockitoUnitTest {
|
||||
|
||||
@InjectMocks
|
||||
private CrmReceivablePlanServiceImpl receivablePlanService;
|
||||
@Mock
|
||||
private CrmReceivablePlanMapper receivablePlanMapper;
|
||||
@Mock
|
||||
private CrmContractService contractService;
|
||||
@Mock
|
||||
private CrmPermissionService permissionService;
|
||||
@Mock
|
||||
private AdminUserApi adminUserApi;
|
||||
|
||||
@Test
|
||||
public void testCreateReceivablePlan_contractNotExists() {
|
||||
// 准备参数
|
||||
CrmReceivablePlanSaveReqVO reqVO = new CrmReceivablePlanSaveReqVO().setContractId(9L);
|
||||
// mock 合同不存在
|
||||
when(contractService.validateContract(9L)).thenThrow(exception(CONTRACT_NOT_EXISTS));
|
||||
|
||||
// 调用,并断言异常
|
||||
assertServiceException(() -> receivablePlanService.createReceivablePlan(reqVO), CONTRACT_NOT_EXISTS);
|
||||
// 断言未写入回款计划和数据权限
|
||||
verifyNoInteractions(receivablePlanMapper, permissionService);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCreateReceivablePlan_useContractCustomerId() {
|
||||
// 准备参数:入参的客户编号应以合同为准
|
||||
CrmReceivablePlanSaveReqVO reqVO = new CrmReceivablePlanSaveReqVO()
|
||||
.setContractId(9L).setCustomerId(999L);
|
||||
// mock 合同
|
||||
when(contractService.validateContract(9L)).thenReturn(new CrmContractDO().setId(9L).setCustomerId(20L));
|
||||
|
||||
// 调用
|
||||
receivablePlanService.createReceivablePlan(reqVO);
|
||||
// 断言
|
||||
verify(receivablePlanMapper).insert(argThat((CrmReceivablePlanDO row) ->
|
||||
Objects.equals(row.getCustomerId(), 20L) && Objects.equals(row.getPeriod(), 1)));
|
||||
}
|
||||
}
|
||||
+5
-1
@@ -1,9 +1,11 @@
|
||||
package cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.in;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.DecimalMin;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
|
||||
import javax.validation.constraints.NotNull;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
@@ -39,6 +41,7 @@ public class ErpPurchaseInSaveReqVO {
|
||||
private String remark;
|
||||
|
||||
@Schema(description = "入库清单列表")
|
||||
@Valid
|
||||
private List<Item> items;
|
||||
|
||||
@Data
|
||||
@@ -68,6 +71,7 @@ public class ErpPurchaseInSaveReqVO {
|
||||
|
||||
@Schema(description = "产品数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "100.00")
|
||||
@NotNull(message = "产品数量不能为空")
|
||||
@DecimalMin(value = "0", inclusive = false, message = "产品数量必须大于 0")
|
||||
private BigDecimal count;
|
||||
|
||||
@Schema(description = "税率,百分比", example = "99.88")
|
||||
|
||||
+8
-1
@@ -8,6 +8,7 @@ import cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.in.ErpPurchaseIn
|
||||
import cn.iocoder.yudao.module.erp.dal.dataobject.purchase.ErpPurchaseInDO;
|
||||
import cn.iocoder.yudao.module.erp.dal.dataobject.purchase.ErpPurchaseInItemDO;
|
||||
import cn.iocoder.yudao.module.erp.enums.ErpAuditStatus;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
@@ -67,4 +68,10 @@ public interface ErpPurchaseInMapper extends BaseMapperX<ErpPurchaseInDO> {
|
||||
return selectList(ErpPurchaseInDO::getOrderId, orderId);
|
||||
}
|
||||
|
||||
}
|
||||
default List<ErpPurchaseInDO> selectListByOrderIdAndStatus(Long orderId, Integer status) {
|
||||
return selectList(new LambdaQueryWrapper<ErpPurchaseInDO>()
|
||||
.eq(ErpPurchaseInDO::getOrderId, orderId)
|
||||
.eq(ErpPurchaseInDO::getStatus, status));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+6
@@ -130,6 +130,12 @@ ErrorCode PURCHASE_ORDER_ITEM_RETURN_FAIL_IN_EXCEED = new ErrorCode(1_030_101_00
|
||||
// ========== ERP 产品 1-030-500-000 ==========
|
||||
ErrorCode PRODUCT_NOT_EXISTS = new ErrorCode(1_030_500_000, "产品不存在");
|
||||
ErrorCode PRODUCT_NOT_ENABLE = new ErrorCode(1_030_500_001, "产品({})未启用");
|
||||
ErrorCode PRODUCT_DELETE_FAIL_PURCHASE_ORDER_EXISTS = new ErrorCode(1_030_500_002, "产品已被采购订单引用,无法删除");
|
||||
ErrorCode PRODUCT_DELETE_FAIL_PURCHASE_IN_EXISTS = new ErrorCode(1_030_500_003, "产品已被采购入库单引用,无法删除");
|
||||
ErrorCode PRODUCT_DELETE_FAIL_PURCHASE_RETURN_EXISTS = new ErrorCode(1_030_500_004, "产品已被采购退货单引用,无法删除");
|
||||
ErrorCode PRODUCT_DELETE_FAIL_SALE_ORDER_EXISTS = new ErrorCode(1_030_500_005, "产品已被销售订单引用,无法删除");
|
||||
ErrorCode PRODUCT_DELETE_FAIL_SALE_OUT_EXISTS = new ErrorCode(1_030_500_006, "产品已被销售出库单引用,无法删除");
|
||||
ErrorCode PRODUCT_DELETE_FAIL_SALE_RETURN_EXISTS = new ErrorCode(1_030_500_007, "产品已被销售退货单引用,无法删除");
|
||||
|
||||
// ========== ERP 产品分类 1-030-501-000 ==========
|
||||
ErrorCode PRODUCT_CATEGORY_NOT_EXISTS = new ErrorCode(1_030_501_000, "产品分类不存在");
|
||||
|
||||
+48
-9
@@ -12,20 +12,22 @@ import cn.iocoder.yudao.module.erp.dal.dataobject.product.ErpProductCategoryDO;
|
||||
import cn.iocoder.yudao.module.erp.dal.dataobject.product.ErpProductDO;
|
||||
import cn.iocoder.yudao.module.erp.dal.dataobject.product.ErpProductUnitDO;
|
||||
import cn.iocoder.yudao.module.erp.dal.mysql.product.ErpProductMapper;
|
||||
import cn.iocoder.yudao.module.erp.service.purchase.ErpPurchaseInService;
|
||||
import cn.iocoder.yudao.module.erp.service.purchase.ErpPurchaseOrderService;
|
||||
import cn.iocoder.yudao.module.erp.service.purchase.ErpPurchaseReturnService;
|
||||
import cn.iocoder.yudao.module.erp.service.sale.ErpSaleOrderService;
|
||||
import cn.iocoder.yudao.module.erp.service.sale.ErpSaleOutService;
|
||||
import cn.iocoder.yudao.module.erp.service.sale.ErpSaleReturnService;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.*;
|
||||
|
||||
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
|
||||
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap;
|
||||
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
|
||||
import static cn.iocoder.yudao.module.erp.enums.ErrorCodeConstants.PRODUCT_NOT_ENABLE;
|
||||
import static cn.iocoder.yudao.module.erp.enums.ErrorCodeConstants.PRODUCT_NOT_EXISTS;
|
||||
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*;
|
||||
import static cn.iocoder.yudao.module.erp.enums.ErrorCodeConstants.*;
|
||||
|
||||
/**
|
||||
* ERP 产品 Service 实现类
|
||||
@@ -43,6 +45,24 @@ public class ErpProductServiceImpl implements ErpProductService {
|
||||
private ErpProductCategoryService productCategoryService;
|
||||
@Resource
|
||||
private ErpProductUnitService productUnitService;
|
||||
@Resource
|
||||
@Lazy // 延迟加载,避免循环依赖
|
||||
private ErpPurchaseOrderService purchaseOrderService;
|
||||
@Resource
|
||||
@Lazy // 延迟加载,避免循环依赖
|
||||
private ErpPurchaseInService purchaseInService;
|
||||
@Resource
|
||||
@Lazy // 延迟加载,避免循环依赖
|
||||
private ErpPurchaseReturnService purchaseReturnService;
|
||||
@Resource
|
||||
@Lazy // 延迟加载,避免循环依赖
|
||||
private ErpSaleOrderService saleOrderService;
|
||||
@Resource
|
||||
@Lazy // 延迟加载,避免循环依赖
|
||||
private ErpSaleOutService saleOutService;
|
||||
@Resource
|
||||
@Lazy // 延迟加载,避免循环依赖
|
||||
private ErpSaleReturnService saleReturnService;
|
||||
|
||||
@Override
|
||||
public Long createProduct(ProductSaveReqVO createReqVO) {
|
||||
@@ -68,6 +88,25 @@ public class ErpProductServiceImpl implements ErpProductService {
|
||||
public void deleteProduct(Long id) {
|
||||
// 校验存在
|
||||
validateProductExists(id);
|
||||
// 校验未被任何业务单据明细引用,避免删除后产生悬空引用
|
||||
if (purchaseOrderService.getPurchaseOrderItemCountByProductId(id) > 0) {
|
||||
throw exception(PRODUCT_DELETE_FAIL_PURCHASE_ORDER_EXISTS);
|
||||
}
|
||||
if (purchaseInService.getPurchaseInItemCountByProductId(id) > 0) {
|
||||
throw exception(PRODUCT_DELETE_FAIL_PURCHASE_IN_EXISTS);
|
||||
}
|
||||
if (purchaseReturnService.getPurchaseReturnItemCountByProductId(id) > 0) {
|
||||
throw exception(PRODUCT_DELETE_FAIL_PURCHASE_RETURN_EXISTS);
|
||||
}
|
||||
if (saleOrderService.getSaleOrderItemCountByProductId(id) > 0) {
|
||||
throw exception(PRODUCT_DELETE_FAIL_SALE_ORDER_EXISTS);
|
||||
}
|
||||
if (saleOutService.getSaleOutItemCountByProductId(id) > 0) {
|
||||
throw exception(PRODUCT_DELETE_FAIL_SALE_OUT_EXISTS);
|
||||
}
|
||||
if (saleReturnService.getSaleReturnItemCountByProductId(id) > 0) {
|
||||
throw exception(PRODUCT_DELETE_FAIL_SALE_RETURN_EXISTS);
|
||||
}
|
||||
// 删除
|
||||
productMapper.deleteById(id);
|
||||
}
|
||||
|
||||
+9
-1
@@ -98,4 +98,12 @@ public interface ErpPurchaseInService {
|
||||
*/
|
||||
List<ErpPurchaseInItemDO> getPurchaseInItemListByInIds(Collection<Long> inIds);
|
||||
|
||||
}
|
||||
/**
|
||||
* 获得引用指定产品的采购入库项数量
|
||||
*
|
||||
* @param productId 产品编号
|
||||
* @return 数量
|
||||
*/
|
||||
Long getPurchaseInItemCountByProductId(Long productId);
|
||||
|
||||
}
|
||||
|
||||
+29
-6
@@ -11,6 +11,7 @@ import cn.iocoder.yudao.module.erp.dal.dataobject.product.ErpProductDO;
|
||||
import cn.iocoder.yudao.module.erp.dal.dataobject.purchase.ErpPurchaseInDO;
|
||||
import cn.iocoder.yudao.module.erp.dal.dataobject.purchase.ErpPurchaseInItemDO;
|
||||
import cn.iocoder.yudao.module.erp.dal.dataobject.purchase.ErpPurchaseOrderDO;
|
||||
import cn.iocoder.yudao.module.erp.dal.dataobject.purchase.ErpPurchaseOrderItemDO;
|
||||
import cn.iocoder.yudao.module.erp.dal.mysql.purchase.ErpPurchaseInItemMapper;
|
||||
import cn.iocoder.yudao.module.erp.dal.mysql.purchase.ErpPurchaseInMapper;
|
||||
import cn.iocoder.yudao.module.erp.dal.redis.no.ErpNoRedisDAO;
|
||||
@@ -144,13 +145,27 @@ public class ErpPurchaseInServiceImpl implements ErpPurchaseInService {
|
||||
}
|
||||
|
||||
private void updatePurchaseOrderInCount(Long orderId) {
|
||||
// 1.1 查询采购订单对应的采购入库单列表
|
||||
List<ErpPurchaseInDO> purchaseIns = purchaseInMapper.selectListByOrderId(orderId);
|
||||
// 1.2 查询对应的采购订单项的入库数量
|
||||
Map<Long, BigDecimal> returnCountMap = purchaseInItemMapper.selectOrderItemCountSumMapByInIds(
|
||||
convertList(purchaseIns, ErpPurchaseInDO::getId));
|
||||
// 1.1 查询已审核入库单,计算入库进度
|
||||
List<ErpPurchaseInDO> approvedIns = purchaseInMapper.selectListByOrderIdAndStatus(
|
||||
orderId, ErpAuditStatus.APPROVE.getStatus());
|
||||
Map<Long, BigDecimal> inCountMap = purchaseInItemMapper.selectOrderItemCountSumMapByInIds(
|
||||
convertList(approvedIns, ErpPurchaseInDO::getId));
|
||||
// 1.2 查询全部入库单,计算占用数量
|
||||
List<ErpPurchaseInDO> allIns = purchaseInMapper.selectListByOrderId(orderId);
|
||||
Map<Long, BigDecimal> allCountMap = purchaseInItemMapper.selectOrderItemCountSumMapByInIds(
|
||||
convertList(allIns, ErpPurchaseInDO::getId));
|
||||
// 1.3 校验全部入库数量未超过采购订单项数量
|
||||
List<ErpPurchaseOrderItemDO> orderItems = purchaseOrderService.getPurchaseOrderItemListByOrderId(orderId);
|
||||
for (ErpPurchaseOrderItemDO item : orderItems) {
|
||||
BigDecimal allCount = allCountMap.getOrDefault(item.getId(), BigDecimal.ZERO);
|
||||
if (allCount.compareTo(item.getCount()) > 0) {
|
||||
throw exception(PURCHASE_ORDER_ITEM_IN_FAIL_PRODUCT_EXCEED,
|
||||
productService.getProduct(item.getProductId()).getName(), item.getCount());
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 更新采购订单的入库数量
|
||||
purchaseOrderService.updatePurchaseOrderInCount(orderId, returnCountMap);
|
||||
purchaseOrderService.updatePurchaseOrderInCount(orderId, inCountMap);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -185,6 +200,9 @@ public class ErpPurchaseInServiceImpl implements ErpPurchaseInService {
|
||||
purchaseInItem.getProductId(), purchaseInItem.getWarehouseId(), count,
|
||||
bizType, purchaseInItem.getInId(), purchaseInItem.getId(), purchaseIn.getNo()));
|
||||
});
|
||||
|
||||
// 4. 更新采购订单的入库数量
|
||||
updatePurchaseOrderInCount(purchaseIn.getOrderId());
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -305,4 +323,9 @@ public class ErpPurchaseInServiceImpl implements ErpPurchaseInService {
|
||||
return purchaseInItemMapper.selectListByInIds(inIds);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long getPurchaseInItemCountByProductId(Long productId) {
|
||||
return purchaseInItemMapper.selectCount(ErpPurchaseInItemDO::getProductId, productId);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+9
-1
@@ -107,4 +107,12 @@ public interface ErpPurchaseOrderService {
|
||||
*/
|
||||
List<ErpPurchaseOrderItemDO> getPurchaseOrderItemListByOrderIds(Collection<Long> orderIds);
|
||||
|
||||
}
|
||||
/**
|
||||
* 获得引用指定产品的采购订单项数量
|
||||
*
|
||||
* @param productId 产品编号
|
||||
* @return 数量
|
||||
*/
|
||||
Long getPurchaseOrderItemCountByProductId(Long productId);
|
||||
|
||||
}
|
||||
|
||||
+5
@@ -292,4 +292,9 @@ public class ErpPurchaseOrderServiceImpl implements ErpPurchaseOrderService {
|
||||
return purchaseOrderItemMapper.selectListByOrderIds(orderIds);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long getPurchaseOrderItemCountByProductId(Long productId) {
|
||||
return purchaseOrderItemMapper.selectCount(ErpPurchaseOrderItemDO::getProductId, productId);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+9
-1
@@ -98,4 +98,12 @@ public interface ErpPurchaseReturnService {
|
||||
*/
|
||||
List<ErpPurchaseReturnItemDO> getPurchaseReturnItemListByReturnIds(Collection<Long> returnIds);
|
||||
|
||||
}
|
||||
/**
|
||||
* 获得引用指定产品的采购退货项数量
|
||||
*
|
||||
* @param productId 产品编号
|
||||
* @return 数量
|
||||
*/
|
||||
Long getPurchaseReturnItemCountByProductId(Long productId);
|
||||
|
||||
}
|
||||
|
||||
+5
@@ -301,4 +301,9 @@ public class ErpPurchaseReturnServiceImpl implements ErpPurchaseReturnService {
|
||||
return purchaseReturnItemMapper.selectListByReturnIds(returnIds);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long getPurchaseReturnItemCountByProductId(Long productId) {
|
||||
return purchaseReturnItemMapper.selectCount(ErpPurchaseReturnItemDO::getProductId, productId);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+9
-1
@@ -107,4 +107,12 @@ public interface ErpSaleOrderService {
|
||||
*/
|
||||
List<ErpSaleOrderItemDO> getSaleOrderItemListByOrderIds(Collection<Long> orderIds);
|
||||
|
||||
}
|
||||
/**
|
||||
* 获得引用指定产品的销售订单项数量
|
||||
*
|
||||
* @param productId 产品编号
|
||||
* @return 数量
|
||||
*/
|
||||
Long getSaleOrderItemCountByProductId(Long productId);
|
||||
|
||||
}
|
||||
|
||||
+5
@@ -304,4 +304,9 @@ public class ErpSaleOrderServiceImpl implements ErpSaleOrderService {
|
||||
return saleOrderItemMapper.selectListByOrderIds(orderIds);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long getSaleOrderItemCountByProductId(Long productId) {
|
||||
return saleOrderItemMapper.selectCount(ErpSaleOrderItemDO::getProductId, productId);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+9
-1
@@ -99,4 +99,12 @@ public interface ErpSaleOutService {
|
||||
List<ErpSaleOutItemDO> getSaleOutItemListByOutIds(Collection<Long> outIds);
|
||||
|
||||
|
||||
}
|
||||
/**
|
||||
* 获得引用指定产品的销售出库项数量
|
||||
*
|
||||
* @param productId 产品编号
|
||||
* @return 数量
|
||||
*/
|
||||
Long getSaleOutItemCountByProductId(Long productId);
|
||||
|
||||
}
|
||||
|
||||
+5
@@ -313,4 +313,9 @@ public class ErpSaleOutServiceImpl implements ErpSaleOutService {
|
||||
return saleOutItemMapper.selectListByOutIds(outIds);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long getSaleOutItemCountByProductId(Long productId) {
|
||||
return saleOutItemMapper.selectCount(ErpSaleOutItemDO::getProductId, productId);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+9
-1
@@ -98,4 +98,12 @@ public interface ErpSaleReturnService {
|
||||
*/
|
||||
List<ErpSaleReturnItemDO> getSaleReturnItemListByReturnIds(Collection<Long> returnIds);
|
||||
|
||||
}
|
||||
/**
|
||||
* 获得引用指定产品的销售退货项数量
|
||||
*
|
||||
* @param productId 产品编号
|
||||
* @return 数量
|
||||
*/
|
||||
Long getSaleReturnItemCountByProductId(Long productId);
|
||||
|
||||
}
|
||||
|
||||
+5
@@ -313,4 +313,9 @@ public class ErpSaleReturnServiceImpl implements ErpSaleReturnService {
|
||||
return saleReturnItemMapper.selectListByReturnIds(returnIds);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long getSaleReturnItemCountByProductId(Long productId) {
|
||||
return saleReturnItemMapper.selectCount(ErpSaleReturnItemDO::getProductId, productId);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+11
@@ -3,6 +3,7 @@ package cn.iocoder.yudao.module.fms.service.closing;
|
||||
import cn.iocoder.yudao.module.fms.controller.admin.closing.vo.FmsClosingOverviewRespVO;
|
||||
import cn.iocoder.yudao.module.fms.controller.admin.closing.vo.FmsClosingQueryReqVO;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.YearMonth;
|
||||
|
||||
@@ -57,6 +58,16 @@ public interface FmsClosingPeriodService {
|
||||
*/
|
||||
FmsClosingOverviewRespVO getClosingOverview(FmsClosingQueryReqVO queryReqVO, Long userId);
|
||||
|
||||
/**
|
||||
* 获得指定会计期间的损益结转余额(末级损益科目本期借贷差额绝对值之和)
|
||||
*
|
||||
* @param accountSetId 账套编号
|
||||
* @param month 会计期间
|
||||
* @param userId 用户编号
|
||||
* @return 损益余额
|
||||
*/
|
||||
BigDecimal getProfitLossBalance(Long accountSetId, String month, Long userId);
|
||||
|
||||
/**
|
||||
* 结账
|
||||
*
|
||||
|
||||
+5
@@ -198,6 +198,11 @@ public class FmsClosingPeriodServiceImpl implements FmsClosingPeriodService {
|
||||
.setCanClose(canClose);
|
||||
}
|
||||
|
||||
@Override
|
||||
public BigDecimal getProfitLossBalance(Long accountSetId, String month, Long userId) {
|
||||
return calculateProfitLossBalance(accountSetId, LocalDateTimeUtils.parseYearMonth(month), userId);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
@LogRecord(type = FMS_CLOSING_TYPE, subType = FMS_CLOSING_PERIOD_CLOSE_SUB_TYPE,
|
||||
|
||||
+10
@@ -53,6 +53,7 @@ import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionU
|
||||
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
|
||||
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap;
|
||||
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.filterList;
|
||||
import static cn.iocoder.yudao.module.fms.enums.ErrorCodeConstants.ACCOUNT_SET_NOT_INITIALIZED;
|
||||
import static cn.iocoder.yudao.module.fms.enums.ErrorCodeConstants.SUBJECT_CODE_DUPLICATE;
|
||||
import static cn.iocoder.yudao.module.fms.enums.ErrorCodeConstants.SUBJECT_CODE_RULE_INVALID;
|
||||
import static cn.iocoder.yudao.module.fms.enums.ErrorCodeConstants.SUBJECT_CURRENCY_INVALID;
|
||||
@@ -161,6 +162,9 @@ public class FmsSubjectServiceImpl implements FmsSubjectService {
|
||||
accountSetService.validateAccountSetWritePermission(createReqVO.getAccountSetId(), userId);
|
||||
// 1.2 查询财务参数
|
||||
FmsFinanceParameterDO financeParameter = financeParameterService.getFinanceParameter(createReqVO.getAccountSetId());
|
||||
if (financeParameter == null) {
|
||||
throw exception(ACCOUNT_SET_NOT_INITIALIZED);
|
||||
}
|
||||
// 1.3 校验科目编码唯一
|
||||
validateSubjectCodeUnique(null, createReqVO.getAccountSetId(), createReqVO.getCode());
|
||||
// 1.4 校验上级科目和科目编码规则
|
||||
@@ -209,6 +213,9 @@ public class FmsSubjectServiceImpl implements FmsSubjectService {
|
||||
// 1.2 查询财务参数
|
||||
FmsFinanceParameterDO financeParameter = financeParameterService.getFinanceParameter(
|
||||
updateReqVO.getAccountSetId());
|
||||
if (financeParameter == null) {
|
||||
throw exception(ACCOUNT_SET_NOT_INITIALIZED);
|
||||
}
|
||||
// 1.3 校验科目存在
|
||||
FmsSubjectDO subject = validateSubjectExists(updateReqVO.getAccountSetId(), updateReqVO.getId());
|
||||
if (ObjUtil.notEqual(subject.getParentId(), updateReqVO.getParentId())) {
|
||||
@@ -416,6 +423,9 @@ public class FmsSubjectServiceImpl implements FmsSubjectService {
|
||||
accountSetService.validateAccountSetWritePermission(accountSetId, userId);
|
||||
// 1.3 加载导入依赖
|
||||
FmsFinanceParameterDO financeParameter = financeParameterService.getFinanceParameter(accountSetId);
|
||||
if (financeParameter == null) {
|
||||
throw exception(ACCOUNT_SET_NOT_INITIALIZED);
|
||||
}
|
||||
Map<String, FmsSubjectDO> subjectMap = convertMap(
|
||||
subjectMapper.selectListByAccountSetIdAndType(accountSetId, null), FmsSubjectDO::getCode);
|
||||
Map<String, Long> auxiliaryTypeIdMap = auxiliaryTypeService.getAuxiliaryTypeIdMap(accountSetId);
|
||||
|
||||
+5
-8
@@ -7,8 +7,6 @@ import cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils;
|
||||
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
|
||||
import cn.iocoder.yudao.framework.common.util.number.NumberUtils;
|
||||
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
|
||||
import cn.iocoder.yudao.module.fms.controller.admin.closing.vo.FmsClosingOverviewRespVO;
|
||||
import cn.iocoder.yudao.module.fms.controller.admin.closing.vo.FmsClosingQueryReqVO;
|
||||
import cn.iocoder.yudao.module.fms.controller.admin.ledger.vo.subjectbalance.FmsLedgerSubjectBalanceRespVO;
|
||||
import cn.iocoder.yudao.module.fms.controller.admin.report.vo.balance.FmsBalanceSheetCheckRespVO;
|
||||
import cn.iocoder.yudao.module.fms.controller.admin.report.vo.balance.FmsBalanceSheetRowRespVO;
|
||||
@@ -292,7 +290,7 @@ public class FmsBalanceSheetServiceImpl implements FmsBalanceSheetService {
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断查询期间内各月的损益是否均已结转,逐月检查结账概览的损益余额
|
||||
* 判断查询期间内各月的损益是否均已结转
|
||||
*
|
||||
* @param listReqVO 查询条件
|
||||
* @param userId 用户编号
|
||||
@@ -302,12 +300,11 @@ public class FmsBalanceSheetServiceImpl implements FmsBalanceSheetService {
|
||||
YearMonth endMonth = LocalDateTimeUtils.parseYearMonth(listReqVO.getEndMonth());
|
||||
for (YearMonth currentMonth = LocalDateTimeUtils.parseYearMonth(listReqVO.getStartMonth());
|
||||
!currentMonth.isAfter(endMonth); currentMonth = currentMonth.plusMonths(1)) {
|
||||
FmsClosingQueryReqVO closingQueryReqVO = new FmsClosingQueryReqVO();
|
||||
closingQueryReqVO.setAccountSetId(listReqVO.getAccountSetId());
|
||||
closingQueryReqVO.setMonth(currentMonth.toString());
|
||||
FmsClosingOverviewRespVO overview = closingPeriodService.getClosingOverview(closingQueryReqVO, userId);
|
||||
// 直接查询损益余额,避免报表校验与结账概况循环调用
|
||||
BigDecimal profitLossBalance = closingPeriodService.getProfitLossBalance(
|
||||
listReqVO.getAccountSetId(), currentMonth.toString(), userId);
|
||||
// 当月损益余额不为零时,说明存在尚未结转的损益
|
||||
if (NumberUtils.zeroIfNull(overview.getProfitLossBalance()).signum() != 0) {
|
||||
if (NumberUtils.zeroIfNull(profitLossBalance).signum() != 0) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
+35
@@ -116,6 +116,41 @@ public class FmsSubjectServiceImplTest extends BaseDbUnitTest {
|
||||
assertTrue(subjects.get(1).getAuxiliaryTypeIds().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCreateSubject_accountSetNotInitialized() {
|
||||
// 准备参数
|
||||
FmsSubjectSaveReqVO reqVO = buildSubjectSaveReqVO(1L, "1001", "库存现金");
|
||||
|
||||
// 调用,并断言异常
|
||||
assertServiceException(() -> subjectService.createSubject(reqVO, 10L), ACCOUNT_SET_NOT_INITIALIZED);
|
||||
// 断言未写入科目
|
||||
assertEquals(0, subjectMapper.selectCount());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUpdateSubject_accountSetNotInitialized() {
|
||||
// 准备参数
|
||||
FmsSubjectSaveReqVO reqVO = buildSubjectSaveReqVO(1L, "1001", "库存现金");
|
||||
reqVO.setId(1L);
|
||||
|
||||
// 调用,并断言异常
|
||||
assertServiceException(() -> subjectService.updateSubject(reqVO, 10L), ACCOUNT_SET_NOT_INITIALIZED);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testImportSubjectList_accountSetNotInitialized() {
|
||||
// 准备参数
|
||||
FmsSubjectImportExcelVO importVO = new FmsSubjectImportExcelVO();
|
||||
importVO.setCode("1001");
|
||||
importVO.setName("库存现金");
|
||||
|
||||
// 调用,并断言异常
|
||||
assertServiceException(() -> subjectService.importSubjectList(1L, Collections.singletonList(importVO), 10L),
|
||||
ACCOUNT_SET_NOT_INITIALIZED);
|
||||
// 断言未写入科目
|
||||
assertEquals(0, subjectMapper.selectCount());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCreateSubject_success() {
|
||||
// 准备参数
|
||||
|
||||
+6
-5
@@ -21,7 +21,6 @@ import cn.iocoder.yudao.module.fms.dal.mysql.report.balance.FmsBalanceSheetConfi
|
||||
import cn.iocoder.yudao.module.fms.dal.mysql.report.balance.FmsBalanceSheetReportMapper;
|
||||
import cn.iocoder.yudao.module.fms.enums.config.FmsSubjectTypeEnum;
|
||||
import cn.iocoder.yudao.module.fms.enums.report.FmsReportTypeEnum;
|
||||
import cn.iocoder.yudao.module.fms.controller.admin.closing.vo.FmsClosingOverviewRespVO;
|
||||
import cn.iocoder.yudao.module.fms.service.closing.FmsClosingPeriodService;
|
||||
import cn.iocoder.yudao.module.fms.service.closing.FmsClosingSchemeService;
|
||||
import cn.iocoder.yudao.module.fms.service.closing.FmsClosingVoucherService;
|
||||
@@ -46,6 +45,7 @@ import java.util.List;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.when;
|
||||
@@ -206,8 +206,8 @@ public class FmsBalanceSheetServiceImplTest extends BaseDbUnitTest {
|
||||
FmsTrialBalanceRespVO trialBalance = new FmsTrialBalanceRespVO();
|
||||
trialBalance.setBalanced(false);
|
||||
when(initialBalanceService.getTrialBalance(1L, 10L)).thenReturn(trialBalance);
|
||||
when(closingPeriodService.getClosingOverview(any(), eq(10L)))
|
||||
.thenReturn(new FmsClosingOverviewRespVO().setProfitLossBalance(new BigDecimal("20.00")));
|
||||
when(closingPeriodService.getProfitLossBalance(1L, "2026-08", 10L))
|
||||
.thenReturn(new BigDecimal("20.00"));
|
||||
|
||||
// 调用
|
||||
FmsBalanceSheetCheckRespVO result = balanceSheetService.checkBalanceSheet(buildQueryReqVO(), 10L);
|
||||
@@ -241,14 +241,15 @@ public class FmsBalanceSheetServiceImplTest extends BaseDbUnitTest {
|
||||
.setChildren(Collections.emptyList())));
|
||||
when(initialBalanceService.getTrialBalance(1L, 10L))
|
||||
.thenReturn(new FmsTrialBalanceRespVO().setBalanced(true));
|
||||
when(closingPeriodService.getClosingOverview(any(), eq(10L)))
|
||||
.thenReturn(new FmsClosingOverviewRespVO().setProfitLossBalance(BigDecimal.ZERO));
|
||||
when(closingPeriodService.getProfitLossBalance(1L, "2026-08", 10L))
|
||||
.thenReturn(BigDecimal.ZERO);
|
||||
|
||||
// 调用
|
||||
FmsBalanceSheetCheckRespVO result = balanceSheetService.checkBalanceSheet(buildQueryReqVO(), 10L);
|
||||
|
||||
// 断言
|
||||
assertEquals(0, result.getUnmappedSubjects().size());
|
||||
assertTrue(result.getProfitLossTransferred());
|
||||
}
|
||||
|
||||
// ========== 随机对象 ==========
|
||||
|
||||
+381
@@ -0,0 +1,381 @@
|
||||
package cn.iocoder.yudao.module.fms.service.report;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
|
||||
import cn.iocoder.yudao.module.fms.controller.admin.ledger.vo.subjectbalance.FmsLedgerSubjectBalanceRespVO;
|
||||
import cn.iocoder.yudao.module.fms.controller.admin.report.vo.FmsReportItemRespVO;
|
||||
import cn.iocoder.yudao.module.fms.controller.admin.report.vo.FmsReportListReqVO;
|
||||
import cn.iocoder.yudao.module.fms.controller.admin.report.vo.balance.FmsBalanceSheetRowRespVO;
|
||||
import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsAccountSetDO;
|
||||
import cn.iocoder.yudao.module.fms.dal.dataobject.config.FmsSubjectDO;
|
||||
import cn.iocoder.yudao.module.fms.dal.dataobject.report.FmsReportTemplateDO;
|
||||
import cn.iocoder.yudao.module.fms.dal.mysql.report.FmsReportTemplateMapper;
|
||||
import cn.iocoder.yudao.module.fms.enums.report.FmsReportTypeEnum;
|
||||
import cn.iocoder.yudao.module.fms.service.closing.FmsClosingPeriodService;
|
||||
import cn.iocoder.yudao.module.fms.service.closing.FmsClosingSchemeService;
|
||||
import cn.iocoder.yudao.module.fms.service.closing.FmsClosingVoucherService;
|
||||
import cn.iocoder.yudao.module.fms.service.config.FmsAccountSetService;
|
||||
import cn.iocoder.yudao.module.fms.service.config.FmsFinanceParameterService;
|
||||
import cn.iocoder.yudao.module.fms.service.config.FmsInitialBalanceService;
|
||||
import cn.iocoder.yudao.module.fms.service.config.FmsSubjectService;
|
||||
import cn.iocoder.yudao.module.fms.service.ledger.FmsLedgerService;
|
||||
import cn.iocoder.yudao.module.fms.service.voucher.FmsVoucherService;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.core.io.FileSystemResource;
|
||||
import org.springframework.jdbc.datasource.init.ResourceDatabasePopulator;
|
||||
import org.springframework.test.context.bean.override.mockito.MockitoBean;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@Import({FmsBalanceSheetServiceImpl.class, FmsIncomeStatementServiceImpl.class,
|
||||
FmsCashFlowStatementServiceImpl.class, FmsReportCommonServiceImpl.class})
|
||||
public class FmsReportTemplateServiceTest extends BaseDbUnitTest {
|
||||
|
||||
@Resource
|
||||
private DataSource dataSource;
|
||||
@Resource
|
||||
private FmsReportTemplateMapper reportTemplateMapper;
|
||||
@Resource
|
||||
private FmsBalanceSheetServiceImpl balanceSheetService;
|
||||
@Resource
|
||||
private FmsIncomeStatementServiceImpl incomeStatementService;
|
||||
@Resource
|
||||
private FmsCashFlowStatementServiceImpl cashFlowStatementService;
|
||||
|
||||
@MockitoBean
|
||||
private FmsAccountSetService accountSetService;
|
||||
@MockitoBean
|
||||
private FmsSubjectService subjectService;
|
||||
@MockitoBean
|
||||
private FmsLedgerService ledgerService;
|
||||
@MockitoBean
|
||||
private FmsFinanceParameterService financeParameterService;
|
||||
@MockitoBean
|
||||
private FmsInitialBalanceService initialBalanceService;
|
||||
@MockitoBean
|
||||
private FmsClosingPeriodService closingPeriodService;
|
||||
@MockitoBean
|
||||
private FmsClosingSchemeService closingSchemeService;
|
||||
@MockitoBean
|
||||
private FmsClosingVoucherService closingVoucherService;
|
||||
@MockitoBean
|
||||
private FmsVoucherService voucherService;
|
||||
|
||||
private final List<FmsSubjectDO> subjects = new ArrayList<>();
|
||||
private final List<FmsLedgerSubjectBalanceRespVO> balances = new ArrayList<>();
|
||||
|
||||
@BeforeEach
|
||||
public void before() {
|
||||
// 初始化报表模板
|
||||
initializeTemplates();
|
||||
// mock 账套、科目和账簿余额
|
||||
FmsAccountSetDO accountSet = new FmsAccountSetDO().setId(1L).setInitialized(true)
|
||||
.setStartTime(LocalDateTime.of(2026, 1, 1, 0, 0));
|
||||
when(accountSetService.validateAccountSetReadPermission(1L, 10L)).thenReturn(accountSet);
|
||||
when(subjectService.getSubjectList(1L, null, 10L)).thenReturn(subjects);
|
||||
when(ledgerService.getSubjectBalanceList(any(), eq(10L))).thenReturn(balances);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInitializeTemplates_repeatExecution() {
|
||||
// mock 数据:模拟用户修改已有报表公式
|
||||
FmsReportTemplateDO template = CollUtil.findOne(
|
||||
reportTemplateMapper.selectListByType(FmsReportTypeEnum.INCOME_STATEMENT.getType()),
|
||||
item -> Integer.valueOf(21).equals(item.getRowNo()));
|
||||
reportTemplateMapper.updateById(new FmsReportTemplateDO().setId(template.getId()).setFormula("[\"L1\"]"));
|
||||
|
||||
// 调用
|
||||
initializeTemplates();
|
||||
|
||||
// 断言
|
||||
assertEquals(58, reportTemplateMapper.selectListByType(FmsReportTypeEnum.BALANCE_SHEET.getType()).size());
|
||||
assertEquals(31, reportTemplateMapper.selectListByType(FmsReportTypeEnum.INCOME_STATEMENT.getType()).size());
|
||||
assertEquals(25, reportTemplateMapper.selectListByType(FmsReportTypeEnum.CASH_FLOW_STATEMENT.getType()).size());
|
||||
assertEquals(21, reportTemplateMapper.selectListByType(FmsReportTypeEnum.CASH_FLOW_ADJUSTMENT.getType()).size());
|
||||
assertEquals(135, reportTemplateMapper.selectCount());
|
||||
assertEquals("[\"L1\"]", reportTemplateMapper.selectById(template.getId()).getFormula());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInitializeTemplates_preserveExistingFormulas() {
|
||||
// mock 数据:模拟旧版报表公式
|
||||
List<FmsReportTemplateDO> templates = new ArrayList<>();
|
||||
for (FmsReportTemplateDO template : reportTemplateMapper.selectList()) {
|
||||
String oldFormula;
|
||||
if (FmsReportTypeEnum.BALANCE_SHEET.getType().equals(template.getType()) && (template.getRowNo() == 9
|
||||
|| template.getRowNo() == 24 || template.getRowNo() == 25)) {
|
||||
oldFormula = template.getFormula().replace("\"operator\":\"-\"", "\"operator\":\"+\"");
|
||||
} else if (FmsReportTypeEnum.INCOME_STATEMENT.getType().equals(template.getType())
|
||||
&& template.getRowNo() == 21) {
|
||||
oldFormula = "[\"L1-L2-L3-L11-L14-L18\"]";
|
||||
} else if (FmsReportTypeEnum.CASH_FLOW_STATEMENT.getType().equals(template.getType())
|
||||
&& template.getRowNo() == 19) {
|
||||
oldFormula = "[\"L14+L15-L17-L18\"]";
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
templates.add(template);
|
||||
reportTemplateMapper.updateById(new FmsReportTemplateDO().setId(template.getId()).setFormula(oldFormula));
|
||||
}
|
||||
|
||||
// 调用
|
||||
initializeTemplates();
|
||||
|
||||
// 断言
|
||||
assertEquals(5, templates.size());
|
||||
for (FmsReportTemplateDO template : templates) {
|
||||
assertEquals(template.getFormula(), reportTemplateMapper.selectById(template.getId()).getFormula());
|
||||
}
|
||||
assertEquals(135, reportTemplateMapper.selectCount());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInitializeTemplates_preserveLegacySubjectIds() {
|
||||
// mock 数据:模拟使用历史科目编号的公式
|
||||
FmsReportTemplateDO template = CollUtil.findOne(
|
||||
reportTemplateMapper.selectListByType(FmsReportTypeEnum.BALANCE_SHEET.getType()),
|
||||
item -> Integer.valueOf(25).equals(item.getRowNo()));
|
||||
String oldFormula = "[{\"operator\":\"+\",\"rules\":0,\"subjectId\":1371,"
|
||||
+ "\"subjectName\":\"无形资产\",\"subjectNumber\":\"1701\"},"
|
||||
+ "{\"operator\":\"+\",\"rules\":0,\"subjectId\":1372,"
|
||||
+ "\"subjectName\":\"累计摊销\",\"subjectNumber\":\"1702\"}]";
|
||||
reportTemplateMapper.updateById(new FmsReportTemplateDO().setId(template.getId()).setFormula(oldFormula));
|
||||
|
||||
// 调用
|
||||
initializeTemplates();
|
||||
|
||||
// 断言
|
||||
assertEquals(template.getFormula(), reportTemplateMapper.selectById(template.getId()).getFormula());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetBalanceSheet_capitalContribution() {
|
||||
// mock 数据:投入资本 100 元
|
||||
mockSubjectBalance("1001", 1, 0, 100, 100, 0);
|
||||
mockSubjectBalance("3001", 2, 0, -100, 0, 100);
|
||||
|
||||
// 准备参数
|
||||
FmsReportListReqVO reqVO = buildQueryReqVO();
|
||||
|
||||
// 调用
|
||||
List<FmsBalanceSheetRowRespVO> balanceSheet = balanceSheetService.getBalanceSheet(reqVO, 10L);
|
||||
List<FmsReportItemRespVO> cashFlow = cashFlowStatementService.getCashFlowStatement(reqVO, 10L);
|
||||
|
||||
// 断言
|
||||
assertAssetAmount(balanceSheet, 30, 100);
|
||||
FmsBalanceSheetRowRespVO total = CollUtil.findOne(balanceSheet,
|
||||
row -> Integer.valueOf(53).equals(row.getLiabilityRowNo()));
|
||||
assertNotNull(total);
|
||||
assertAmount(100, total.getLiabilityClosingAmount());
|
||||
assertItemAmount(cashFlow, 15, 100);
|
||||
assertItemAmount(cashFlow, 22, 100);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetBalanceSheet_inventoryAndContraAccounts() {
|
||||
// mock 数据:存货、固定资产、无形资产及其备抵科目
|
||||
mockSubjectBalance("1403", 1, 0, 30, 30, 0);
|
||||
mockSubjectBalance("1407", 2, 0, -5, 0, 5);
|
||||
mockSubjectBalance("1621", 1, 0, 100, 100, 0);
|
||||
mockSubjectBalance("1622", 2, 0, -20, 0, 20);
|
||||
mockSubjectBalance("1701", 1, 0, 100, 100, 0);
|
||||
mockSubjectBalance("1702", 2, 0, -10, 0, 10);
|
||||
|
||||
// 准备参数
|
||||
FmsReportListReqVO reqVO = buildQueryReqVO();
|
||||
|
||||
// 调用
|
||||
List<FmsBalanceSheetRowRespVO> result = balanceSheetService.getBalanceSheet(reqVO, 10L);
|
||||
|
||||
// 断言
|
||||
assertAssetAmount(result, 9, 25);
|
||||
assertAssetAmount(result, 24, 80);
|
||||
assertAssetAmount(result, 25, 90);
|
||||
assertAssetAmount(result, 30, 195);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetCashFlowStatement_creditSale() {
|
||||
// mock 数据:赊销 100 元并结转利润,尚未收款
|
||||
mockSubjectBalance("1001", 1, 0, 0, 0, 0);
|
||||
mockSubjectBalance("1122", 1, 0, 100, 100, 0);
|
||||
mockSubjectBalance("5001", 2, 0, 0, 0, 100);
|
||||
mockSubjectBalance("3103", 2, 0, -100, 0, 100);
|
||||
|
||||
// 准备参数
|
||||
FmsReportListReqVO reqVO = buildQueryReqVO();
|
||||
|
||||
// 调用
|
||||
List<FmsReportItemRespVO> incomeItems = incomeStatementService.getIncomeStatement(reqVO, 10L);
|
||||
List<FmsReportItemRespVO> result = cashFlowStatementService.getCashFlowStatement(reqVO, 10L);
|
||||
|
||||
// 断言
|
||||
assertItemAmount(incomeItems, 1, 100);
|
||||
assertItemAmount(result, 1, 0);
|
||||
assertItemAmount(result, 6, 0);
|
||||
assertItemAmount(result, 20, 0);
|
||||
assertItemAmount(result, 22, 0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetCashFlowStatement_purchaseInventory() {
|
||||
// mock 数据:期初现金 100 元,支付 40 元购入原材料
|
||||
mockSubjectBalance("1001", 1, 100, 60, 0, 40);
|
||||
mockSubjectBalance("1403", 1, 0, 40, 40, 0);
|
||||
mockSubjectBalance("3001", 2, -100, -100, 0, 0);
|
||||
|
||||
// 准备参数
|
||||
FmsReportListReqVO reqVO = buildQueryReqVO();
|
||||
|
||||
// 调用
|
||||
List<FmsReportItemRespVO> result = cashFlowStatementService.getCashFlowStatement(reqVO, 10L);
|
||||
|
||||
// 断言
|
||||
assertItemAmount(result, 3, 40);
|
||||
assertItemAmount(result, 6, 0);
|
||||
assertItemAmount(result, 20, -40);
|
||||
assertItemAmount(result, 22, 60);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetCashFlowStatement_payWagesAndIncomeTax() {
|
||||
// mock 数据:计提并支付工资 30 元、所得税 10 元,期末应付款为零
|
||||
mockSubjectBalance("1001", 1, 100, 60, 0, 40);
|
||||
mockSubjectBalance("3001", 2, -100, -100, 0, 0);
|
||||
mockSubjectBalance("3103", 2, 0, 40, 40, 0);
|
||||
mockSubjectBalance("5602", 1, 0, 0, 30, 0);
|
||||
mockSubjectBalance("5801", 1, 0, 0, 10, 0);
|
||||
mockSubjectBalance("2211", 2, 0, 0, 30, 30);
|
||||
mockSubjectBalance("2221", 2, 0, 0, 10, 10);
|
||||
|
||||
// 准备参数
|
||||
FmsReportListReqVO reqVO = buildQueryReqVO();
|
||||
|
||||
// 调用
|
||||
List<FmsReportItemRespVO> result = cashFlowStatementService.getCashFlowStatement(reqVO, 10L);
|
||||
|
||||
// 断言
|
||||
assertItemAmount(result, 4, 30);
|
||||
assertItemAmount(result, 5, 10);
|
||||
assertItemAmount(result, 6, 0);
|
||||
assertItemAmount(result, 20, -40);
|
||||
assertItemAmount(result, 22, 60);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetCashFlowStatement_repayLoan() {
|
||||
// mock 数据:期初现金 100 元,偿还借款 30 元
|
||||
mockSubjectBalance("1001", 1, 100, 70, 0, 30);
|
||||
mockSubjectBalance("2001", 2, -100, -70, 30, 0);
|
||||
|
||||
// 准备参数
|
||||
FmsReportListReqVO reqVO = buildQueryReqVO();
|
||||
|
||||
// 调用
|
||||
List<FmsReportItemRespVO> result = cashFlowStatementService.getCashFlowStatement(reqVO, 10L);
|
||||
|
||||
// 断言
|
||||
assertItemAmount(result, 16, 30);
|
||||
assertItemAmount(result, 19, -30);
|
||||
assertItemAmount(result, 6, 0);
|
||||
assertItemAmount(result, 22, 70);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetIncomeStatement_investmentIncomeAndNonOperatingExpense() {
|
||||
// mock 数据:投资收益 20 元,营业外支出 5 元
|
||||
mockSubjectBalance("5111", 2, 0, -20, 0, 20);
|
||||
mockSubjectBalance("5711", 1, 0, 5, 5, 0);
|
||||
|
||||
// 准备参数
|
||||
FmsReportListReqVO reqVO = buildQueryReqVO();
|
||||
|
||||
// 调用
|
||||
List<FmsReportItemRespVO> result = incomeStatementService.getIncomeStatement(reqVO, 10L);
|
||||
|
||||
// 断言
|
||||
assertItemAmount(result, 20, 20);
|
||||
assertItemAmount(result, 21, 20);
|
||||
assertItemAmount(result, 24, 5);
|
||||
assertItemAmount(result, 32, 15);
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行报表模板升级脚本
|
||||
*/
|
||||
private void initializeTemplates() {
|
||||
new ResourceDatabasePopulator(new FileSystemResource("../sql/mysql/upgrade/20260926-fms-report-template.sql"))
|
||||
.execute(dataSource);
|
||||
}
|
||||
|
||||
/**
|
||||
* 模拟科目余额和发生额:余额借方为正、贷方为负,发生额已扣除损益结转数据
|
||||
*/
|
||||
private void mockSubjectBalance(String code, int direction, int opening, int closing, int debit, int credit) {
|
||||
Long id = Long.valueOf(code);
|
||||
subjects.add(new FmsSubjectDO().setId(id).setAccountSetId(1L).setCode(code).setName(code)
|
||||
.setLevel(1).setBalanceDirection(direction));
|
||||
balances.add(new FmsLedgerSubjectBalanceRespVO().setSubjectId(id).setSubjectCode(code)
|
||||
.setOpeningDebitAmount(BigDecimal.valueOf(Math.max(opening, 0)))
|
||||
.setOpeningCreditAmount(BigDecimal.valueOf(Math.max(-opening, 0)))
|
||||
.setEndingDebitAmount(BigDecimal.valueOf(Math.max(closing, 0)))
|
||||
.setEndingCreditAmount(BigDecimal.valueOf(Math.max(-closing, 0)))
|
||||
.setPeriodDebitAmount(BigDecimal.valueOf(debit)).setPeriodCreditAmount(BigDecimal.valueOf(credit))
|
||||
.setYearDebitAmount(BigDecimal.valueOf(debit)).setYearCreditAmount(BigDecimal.valueOf(credit))
|
||||
.setChildren(Collections.emptyList()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造 2026 年 8 月的报表查询参数
|
||||
*/
|
||||
private FmsReportListReqVO buildQueryReqVO() {
|
||||
FmsReportListReqVO reqVO = new FmsReportListReqVO();
|
||||
reqVO.setAccountSetId(1L);
|
||||
reqVO.setStartMonth("2026-08");
|
||||
reqVO.setEndMonth("2026-08");
|
||||
return reqVO;
|
||||
}
|
||||
|
||||
/**
|
||||
* 断言报表指定行的本期和本年金额
|
||||
*/
|
||||
private void assertItemAmount(List<FmsReportItemRespVO> rows, int rowNo, int expected) {
|
||||
FmsReportItemRespVO item = CollUtil.findOne(rows, row -> Integer.valueOf(rowNo).equals(row.getRowNo()));
|
||||
assertNotNull(item);
|
||||
assertAmount(expected, item.getCurrentAmount());
|
||||
assertAmount(expected, item.getYearAmount());
|
||||
}
|
||||
|
||||
/**
|
||||
* 断言资产负债表指定行的资产期末余额
|
||||
*/
|
||||
private void assertAssetAmount(List<FmsBalanceSheetRowRespVO> rows, int rowNo, int expected) {
|
||||
FmsBalanceSheetRowRespVO item = CollUtil.findOne(rows,
|
||||
row -> Integer.valueOf(rowNo).equals(row.getAssetRowNo()));
|
||||
assertNotNull(item);
|
||||
assertAmount(expected, item.getAssetClosingAmount());
|
||||
}
|
||||
|
||||
/**
|
||||
* 按数值比较金额,忽略 BigDecimal 的小数位数差异
|
||||
*/
|
||||
private void assertAmount(int expected, BigDecimal actual) {
|
||||
assertNotNull(actual);
|
||||
assertEquals(0, BigDecimal.valueOf(expected).compareTo(actual), "实际金额:" + actual);
|
||||
}
|
||||
|
||||
}
|
||||
+6
-1
@@ -106,7 +106,12 @@ public class HrmEmployeeFieldConfigServiceImpl implements HrmEmployeeFieldConfig
|
||||
public void validateEmployeeCreateFields(HrmEmployeeSaveReqVO reqVO, Integer entryStatus) {
|
||||
for (HrmEmployeeFieldConfigRespVO field : getEmployeeCreateFieldConfigList(entryStatus)) {
|
||||
if (Boolean.TRUE.equals(field.getVisible())
|
||||
|| HrmEmployeeCreateFieldEnum.CANDIDATE_ID.getName().equals(field.getName())) {
|
||||
|| HrmEmployeeCreateFieldEnum.CANDIDATE_ID.getName().equals(field.getName())
|
||||
// fix #1211:https://github.com/YunaiV/ruoyi-vue-pro/pull/1211
|
||||
// 候选人转员工(convert 来源,请求携带 candidateId):channelId 由服务端从候选人记录回填,
|
||||
// 与 candidateId 同属服务端赋值字段,默认隐藏时同样放行(渠道存在性仍由 createEmployee 校验兜底)
|
||||
|| (reqVO.getCandidateId() != null
|
||||
&& HrmEmployeeCreateFieldEnum.CHANNEL_ID.getName().equals(field.getName()))) {
|
||||
continue;
|
||||
}
|
||||
if (ObjectUtil.isNotEmpty(BeanUtil.getFieldValue(reqVO, field.getName()))) {
|
||||
|
||||
+23
-6
@@ -21,6 +21,7 @@ import com.google.common.util.concurrent.ListenableFuture;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
|
||||
@@ -48,6 +49,14 @@ import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.SENSITIVE_WORD
|
||||
@Slf4j
|
||||
public class ImSensitiveWordServiceImpl implements ImSensitiveWordService {
|
||||
|
||||
/**
|
||||
* 关闭多租户时的统一缓存键,仅用于缓存,不作为查询的租户编号
|
||||
*/
|
||||
private static final Long SINGLE_TENANT_CACHE_KEY = 0L;
|
||||
|
||||
@Value("${yudao.tenant.enable:true}")
|
||||
private boolean tenantEnable = true;
|
||||
|
||||
@Resource
|
||||
private ImSensitiveWordMapper sensitiveWordMapper;
|
||||
|
||||
@@ -70,7 +79,7 @@ public class ImSensitiveWordServiceImpl implements ImSensitiveWordService {
|
||||
}
|
||||
|
||||
/**
|
||||
* 租户 → SensitiveWordBs 实例的本地缓存
|
||||
* 租户 → SensitiveWordBs 实例的本地缓存;关闭多租户时共用一个缓存
|
||||
* <p>
|
||||
* 每分钟触发一次异步 reload,先读 max(update_time),没变就复用旧实例(避免 trie 重建),变了才重新读词库 + 重建。
|
||||
* 单实例 CRUD 后另外通过 {@link #invalidateSensitiveWordBsCaches()} 立即让本机失效,多实例靠定时刷新最长 1 分钟内收敛。
|
||||
@@ -87,8 +96,8 @@ public class ImSensitiveWordServiceImpl implements ImSensitiveWordService {
|
||||
|
||||
@Override
|
||||
public ListenableFuture<SensitiveWordBsCache> reload(Long tenantId, SensitiveWordBsCache oldValue) {
|
||||
// 异步刷新线程独立于业务线程,没有租户上下文;必须显式 TenantUtils.execute 设置,否则租户拦截器会按当前线程的空上下文拼 SQL
|
||||
return Futures.immediateFuture(TenantUtils.execute(tenantId, () -> {
|
||||
// 异步刷新时恢复租户上下文;关闭多租户时不设置租户编号
|
||||
return Futures.immediateFuture(TenantUtils.execute(tenantEnable ? tenantId : null, () -> {
|
||||
LocalDateTime currentMax = sensitiveWordMapper.selectMaxUpdateTime(tenantId);
|
||||
// 没变 → 复用旧实例,避免无谓地重建 trie
|
||||
if (Objects.equals(oldValue.getMaxUpdateTime(), currentMax)) {
|
||||
@@ -102,7 +111,7 @@ public class ImSensitiveWordServiceImpl implements ImSensitiveWordService {
|
||||
});
|
||||
|
||||
private SensitiveWordBsCache loadFresh(Long tenantId) {
|
||||
return TenantUtils.execute(tenantId, () -> {
|
||||
return TenantUtils.execute(tenantEnable ? tenantId : null, () -> {
|
||||
// 先取基线时间再读词库:反过来在两次查询之间出现的新插入会被漏感知
|
||||
LocalDateTime maxUpdateTime = sensitiveWordMapper.selectMaxUpdateTime(tenantId);
|
||||
List<ImSensitiveWordDO> words = sensitiveWordMapper.selectListByStatus(CommonStatusEnum.ENABLE.getStatus());
|
||||
@@ -122,9 +131,16 @@ public class ImSensitiveWordServiceImpl implements ImSensitiveWordService {
|
||||
/**
|
||||
* 强制让敏感词缓存失效,下次访问按最新 DB 重建
|
||||
* <p>
|
||||
* 有租户上下文:仅失效该租户。无租户上下文(如系统级 / 跨租户清理):兜底失效所有租户。
|
||||
* 关闭多租户时失效统一缓存;开启时仅失效当前租户,无租户上下文则失效所有租户。
|
||||
*/
|
||||
private void invalidateSensitiveWordBsCaches() {
|
||||
// 情况一:租户未开启的清理
|
||||
if (!tenantEnable) {
|
||||
sensitiveWordBsCaches.invalidate(SINGLE_TENANT_CACHE_KEY);
|
||||
return;
|
||||
}
|
||||
|
||||
// 情况二:租户已开启的清理
|
||||
Long tenantId = TenantContextHolder.getTenantId();
|
||||
if (tenantId != null) {
|
||||
sensitiveWordBsCaches.invalidate(tenantId);
|
||||
@@ -138,7 +154,8 @@ public class ImSensitiveWordServiceImpl implements ImSensitiveWordService {
|
||||
if (StrUtil.isBlank(text)) {
|
||||
return;
|
||||
}
|
||||
SensitiveWordBs bs = sensitiveWordBsCaches.getUnchecked(TenantContextHolder.getRequiredTenantId()).getBs();
|
||||
Long cacheKey = tenantEnable ? TenantContextHolder.getRequiredTenantId() : SINGLE_TENANT_CACHE_KEY;
|
||||
SensitiveWordBs bs = sensitiveWordBsCaches.getUnchecked(cacheKey).getBs();
|
||||
if (bs.contains(text)) {
|
||||
throw exception(MESSAGE_SENSITIVE_WORD_BLOCKED);
|
||||
}
|
||||
|
||||
+160
-65
@@ -2,24 +2,28 @@ package cn.iocoder.yudao.module.im.service.sensitiveword;
|
||||
|
||||
import cn.hutool.core.collection.ListUtil;
|
||||
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
|
||||
import cn.iocoder.yudao.framework.common.exception.ServiceException;
|
||||
import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder;
|
||||
import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
|
||||
import cn.iocoder.yudao.module.im.controller.admin.manager.sensitiveword.vo.ImSensitiveWordSaveReqVO;
|
||||
import cn.iocoder.yudao.module.im.dal.dataobject.sensitiveword.ImSensitiveWordDO;
|
||||
import cn.iocoder.yudao.module.im.dal.mysql.sensitiveword.ImSensitiveWordMapper;
|
||||
import com.google.common.cache.LoadingCache;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException;
|
||||
import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.MESSAGE_SENSITIVE_WORD_BLOCKED;
|
||||
import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.SENSITIVE_WORD_DUPLICATED;
|
||||
import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.SENSITIVE_WORD_NOT_EXISTS;
|
||||
import static org.awaitility.Awaitility.await;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
@@ -40,10 +44,9 @@ public class ImSensitiveWordServiceImplTest extends BaseMockitoUnitTest {
|
||||
|
||||
@BeforeEach
|
||||
public void setUp() {
|
||||
// 设置租户上下文;validateText 与 loadFresh 都依赖 TenantContextHolder
|
||||
// 设置租户上下文
|
||||
TenantContextHolder.setTenantId(TENANT_ID);
|
||||
// mock 启用敏感词列表;LoadingCache 在首次 validateText 时懒加载,无需主动 init
|
||||
// 用 lenient 避免 null / empty 等不触发 cache load 的用例报 UnnecessaryStubbing
|
||||
// mock 启用的敏感词,部分用例不触发缓存加载
|
||||
lenient().when(imSensitiveWordMapper.selectListByStatus(CommonStatusEnum.ENABLE.getStatus()))
|
||||
.thenReturn(ListUtil.of(
|
||||
ImSensitiveWordDO.builder().id(1L).word("badword")
|
||||
@@ -55,69 +58,165 @@ public class ImSensitiveWordServiceImplTest extends BaseMockitoUnitTest {
|
||||
|
||||
@AfterEach
|
||||
public void tearDown() {
|
||||
// 清理租户上下文,避免污染其它测试
|
||||
// 清理租户上下文
|
||||
TenantContextHolder.clear();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testValidateText_null() {
|
||||
// null 直接返回,不抛异常
|
||||
// 调用,并断言
|
||||
assertDoesNotThrow(() -> sensitiveWordService.validateText(null));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testValidateText_empty() {
|
||||
// 调用,并断言
|
||||
assertDoesNotThrow(() -> sensitiveWordService.validateText(""));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testValidateText_clean() {
|
||||
// 正常文本不应命中
|
||||
// 调用,并断言
|
||||
assertDoesNotThrow(() -> sensitiveWordService.validateText("hello world"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testValidateText_tenantDisabled() {
|
||||
// 准备上下文
|
||||
ReflectionTestUtils.setField(sensitiveWordService, "tenantEnable", false);
|
||||
TenantContextHolder.clear();
|
||||
|
||||
// 调用,并断言
|
||||
assertDoesNotThrow(() -> sensitiveWordService.validateText("hello world"));
|
||||
assertServiceException(() -> sensitiveWordService.validateText("this contains badword here"), MESSAGE_SENSITIVE_WORD_BLOCKED);
|
||||
assertNull(TenantContextHolder.getTenantId());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testValidateText_tenantEnabledWithoutTenantId() {
|
||||
// 准备上下文
|
||||
TenantContextHolder.clear();
|
||||
|
||||
// 调用,并断言异常
|
||||
assertThrows(NullPointerException.class, () -> sensitiveWordService.validateText("hello world"));
|
||||
// 断言
|
||||
verifyNoInteractions(imSensitiveWordMapper);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testValidateText_tenantIsolation() {
|
||||
// mock 不同租户的词库
|
||||
when(imSensitiveWordMapper.selectListByStatus(CommonStatusEnum.ENABLE.getStatus()))
|
||||
.thenAnswer(invocation -> List.of(ImSensitiveWordDO.builder()
|
||||
.word(TenantContextHolder.getRequiredTenantId().equals(TENANT_ID) ? "firstbad" : "secondbad")
|
||||
.build()));
|
||||
|
||||
// 调用,并断言当前租户的词库
|
||||
assertServiceException(() -> sensitiveWordService.validateText("firstbad"), MESSAGE_SENSITIVE_WORD_BLOCKED);
|
||||
assertDoesNotThrow(() -> sensitiveWordService.validateText("secondbad"));
|
||||
// 调用,并断言切换租户后的词库
|
||||
TenantContextHolder.setTenantId(2L);
|
||||
assertDoesNotThrow(() -> sensitiveWordService.validateText("firstbad"));
|
||||
assertServiceException(() -> sensitiveWordService.validateText("secondbad"), MESSAGE_SENSITIVE_WORD_BLOCKED);
|
||||
// 调用,并断言切回租户后复用缓存
|
||||
TenantContextHolder.setTenantId(TENANT_ID);
|
||||
assertServiceException(() -> sensitiveWordService.validateText("firstbad"), MESSAGE_SENSITIVE_WORD_BLOCKED);
|
||||
verify(imSensitiveWordMapper, times(2)).selectListByStatus(CommonStatusEnum.ENABLE.getStatus());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDeleteSensitiveWordList_tenantDisabled() {
|
||||
// 准备上下文
|
||||
ReflectionTestUtils.setField(sensitiveWordService, "tenantEnable", false);
|
||||
TenantContextHolder.clear();
|
||||
// 准备缓存
|
||||
sensitiveWordService.validateText("hello world");
|
||||
|
||||
// 调用,并断言携带租户编号时仍复用缓存
|
||||
TenantContextHolder.setTenantId(2L);
|
||||
sensitiveWordService.validateText("hello world");
|
||||
verify(imSensitiveWordMapper).selectListByStatus(CommonStatusEnum.ENABLE.getStatus());
|
||||
// mock 删除后的词库
|
||||
when(imSensitiveWordMapper.selectListByStatus(CommonStatusEnum.ENABLE.getStatus())).thenReturn(List.of());
|
||||
|
||||
// 调用
|
||||
sensitiveWordService.deleteSensitiveWordList(ListUtil.of(1L, 2L));
|
||||
|
||||
// 断言缓存已刷新,上下文不变
|
||||
assertDoesNotThrow(() -> sensitiveWordService.validateText("badword"));
|
||||
verify(imSensitiveWordMapper, times(2)).selectListByStatus(CommonStatusEnum.ENABLE.getStatus());
|
||||
assertEquals(2L, TenantContextHolder.getTenantId());
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
public void testValidateText_tenantDisabledRefresh() {
|
||||
// 准备上下文
|
||||
ReflectionTestUtils.setField(sensitiveWordService, "tenantEnable", false);
|
||||
TenantContextHolder.clear();
|
||||
// mock 初始更新时间
|
||||
when(imSensitiveWordMapper.selectMaxUpdateTime(0L)).thenReturn(LocalDateTime.of(2026, 9, 1, 0, 0));
|
||||
// 准备缓存
|
||||
sensitiveWordService.validateText("hello world");
|
||||
// mock 词库更新,并断言刷新线程无租户编号
|
||||
when(imSensitiveWordMapper.selectMaxUpdateTime(0L)).thenAnswer(invocation -> {
|
||||
assertNull(TenantContextHolder.getTenantId());
|
||||
return LocalDateTime.of(2026, 9, 2, 0, 0);
|
||||
});
|
||||
when(imSensitiveWordMapper.selectListByStatus(CommonStatusEnum.ENABLE.getStatus()))
|
||||
.thenReturn(List.of(ImSensitiveWordDO.builder().word("newbad").build()));
|
||||
|
||||
// 调用
|
||||
LoadingCache<Long, ?> cache = (LoadingCache<Long, ?>) ReflectionTestUtils.getField(
|
||||
sensitiveWordService, "sensitiveWordBsCaches");
|
||||
cache.refresh(0L);
|
||||
|
||||
// 断言缓存异步刷新
|
||||
await().atMost(Duration.ofSeconds(5)).untilAsserted(() ->
|
||||
assertServiceException(() -> sensitiveWordService.validateText("newbad"), MESSAGE_SENSITIVE_WORD_BLOCKED));
|
||||
assertDoesNotThrow(() -> sensitiveWordService.validateText("badword"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testValidateText_hitEnglish() {
|
||||
ServiceException exception = assertThrows(ServiceException.class,
|
||||
() -> sensitiveWordService.validateText("this contains badword here"));
|
||||
assertEquals(MESSAGE_SENSITIVE_WORD_BLOCKED.getCode(), exception.getCode());
|
||||
// 调用,并断言异常
|
||||
assertServiceException(() -> sensitiveWordService.validateText("this contains badword here"), MESSAGE_SENSITIVE_WORD_BLOCKED);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testValidateText_hitChinese() {
|
||||
ServiceException exception = assertThrows(ServiceException.class,
|
||||
() -> sensitiveWordService.validateText("这条消息里有违禁词哦"));
|
||||
assertEquals(MESSAGE_SENSITIVE_WORD_BLOCKED.getCode(), exception.getCode());
|
||||
// 调用,并断言异常
|
||||
assertServiceException(() -> sensitiveWordService.validateText("这条消息里有违禁词哦"), MESSAGE_SENSITIVE_WORD_BLOCKED);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testValidateText_lazyLoadsCacheOnFirstCall() {
|
||||
// 调用:首次 validateText 应触发 cache load
|
||||
// 调用
|
||||
sensitiveWordService.validateText("hello world");
|
||||
|
||||
// 断言:mapper 各调用 1 次(loadFresh 里先取 maxUpdateTime 再读词库)
|
||||
// 断言
|
||||
verify(imSensitiveWordMapper, times(1)).selectMaxUpdateTime(TENANT_ID);
|
||||
verify(imSensitiveWordMapper, times(1)).selectListByStatus(CommonStatusEnum.ENABLE.getStatus());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testValidateText_reusesCachedBsAcrossCalls() {
|
||||
// 调用:连续两次 validateText
|
||||
// 调用
|
||||
sensitiveWordService.validateText("hello world");
|
||||
sensitiveWordService.validateText("another text");
|
||||
|
||||
// 断言:第二次复用 cache,mapper 仍只被调用 1 次
|
||||
// 断言复用缓存
|
||||
verify(imSensitiveWordMapper, times(1)).selectMaxUpdateTime(TENANT_ID);
|
||||
verify(imSensitiveWordMapper, times(1)).selectListByStatus(CommonStatusEnum.ENABLE.getStatus());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCreateSensitiveWord_invalidatesCacheAndReloadsOnNextValidate() {
|
||||
// 准备:首次 validateText 触发 cache load(旧词库)
|
||||
// 准备缓存
|
||||
sensitiveWordService.validateText("hello world");
|
||||
verify(imSensitiveWordMapper, times(1)).selectListByStatus(CommonStatusEnum.ENABLE.getStatus());
|
||||
|
||||
// 准备:mapper 返回新词库(额外多一个 newbad),并让 createSensitiveWord 走通
|
||||
// mock 新增后的词库
|
||||
when(imSensitiveWordMapper.selectListByStatus(CommonStatusEnum.ENABLE.getStatus()))
|
||||
.thenReturn(ListUtil.of(
|
||||
ImSensitiveWordDO.builder().id(1L).word("badword")
|
||||
@@ -129,149 +228,145 @@ public class ImSensitiveWordServiceImplTest extends BaseMockitoUnitTest {
|
||||
));
|
||||
when(imSensitiveWordMapper.selectByWord("newbad")).thenReturn(null);
|
||||
|
||||
// 调用:新增敏感词,触发 invalidate
|
||||
// 准备参数
|
||||
ImSensitiveWordSaveReqVO reqVO = new ImSensitiveWordSaveReqVO();
|
||||
reqVO.setWord("newbad");
|
||||
reqVO.setStatus(CommonStatusEnum.ENABLE.getStatus());
|
||||
|
||||
// 调用
|
||||
sensitiveWordService.createSensitiveWord(reqVO);
|
||||
|
||||
// 调用:再次 validateText,应触发重新 load
|
||||
ServiceException exception = assertThrows(ServiceException.class,
|
||||
() -> sensitiveWordService.validateText("contains newbad here"));
|
||||
assertEquals(MESSAGE_SENSITIVE_WORD_BLOCKED.getCode(), exception.getCode());
|
||||
// 旧词依然命中
|
||||
assertThrows(ServiceException.class,
|
||||
() -> sensitiveWordService.validateText("contains badword here"));
|
||||
// 断言缓存已刷新
|
||||
assertServiceException(() -> sensitiveWordService.validateText("contains newbad here"), MESSAGE_SENSITIVE_WORD_BLOCKED);
|
||||
// 断言原有敏感词仍然生效
|
||||
assertServiceException(() -> sensitiveWordService.validateText("contains badword here"), MESSAGE_SENSITIVE_WORD_BLOCKED);
|
||||
|
||||
// 断言:selectListByStatus 共被调用 2 次(首次 load + invalidate 后 reload)
|
||||
// 断言词库重新加载
|
||||
verify(imSensitiveWordMapper, times(2)).selectListByStatus(CommonStatusEnum.ENABLE.getStatus());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCreateSensitiveWord_duplicateWord_throws() {
|
||||
// 准备:mock 已存在同名敏感词
|
||||
// mock 同名敏感词
|
||||
when(imSensitiveWordMapper.selectByWord("dup")).thenReturn(
|
||||
ImSensitiveWordDO.builder().id(99L).word("dup")
|
||||
.status(CommonStatusEnum.ENABLE.getStatus()).build());
|
||||
|
||||
// 调用 + 断言:重复敏感词抛 SENSITIVE_WORD_DUPLICATED
|
||||
// 准备参数
|
||||
ImSensitiveWordSaveReqVO reqVO = new ImSensitiveWordSaveReqVO();
|
||||
reqVO.setWord("dup");
|
||||
reqVO.setStatus(CommonStatusEnum.ENABLE.getStatus());
|
||||
ServiceException exception = assertThrows(ServiceException.class,
|
||||
() -> sensitiveWordService.createSensitiveWord(reqVO));
|
||||
assertEquals(SENSITIVE_WORD_DUPLICATED.getCode(), exception.getCode());
|
||||
|
||||
// 断言:未走到 insert
|
||||
// 调用,并断言异常
|
||||
assertServiceException(() -> sensitiveWordService.createSensitiveWord(reqVO), SENSITIVE_WORD_DUPLICATED, reqVO.getWord());
|
||||
|
||||
// 断言
|
||||
verify(imSensitiveWordMapper, never()).insert(any(ImSensitiveWordDO.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUpdateSensitiveWord_invalidatesCache() {
|
||||
// 准备:首次 validateText 触发 cache load
|
||||
// 准备缓存
|
||||
sensitiveWordService.validateText("hello world");
|
||||
verify(imSensitiveWordMapper, times(1)).selectListByStatus(CommonStatusEnum.ENABLE.getStatus());
|
||||
|
||||
// 准备:让 update 校验通过
|
||||
// mock 原敏感词
|
||||
when(imSensitiveWordMapper.selectById(1L)).thenReturn(
|
||||
ImSensitiveWordDO.builder().id(1L).word("badword")
|
||||
.status(CommonStatusEnum.ENABLE.getStatus()).build());
|
||||
when(imSensitiveWordMapper.selectByWord("updatedbad")).thenReturn(null);
|
||||
// 准备:reload 时返回新词库
|
||||
// mock 修改后的词库
|
||||
when(imSensitiveWordMapper.selectListByStatus(CommonStatusEnum.ENABLE.getStatus()))
|
||||
.thenReturn(ListUtil.of(
|
||||
ImSensitiveWordDO.builder().id(1L).word("updatedbad")
|
||||
.status(CommonStatusEnum.ENABLE.getStatus()).build()
|
||||
));
|
||||
|
||||
// 调用:更新敏感词,触发 invalidate
|
||||
// 准备参数
|
||||
ImSensitiveWordSaveReqVO reqVO = new ImSensitiveWordSaveReqVO();
|
||||
reqVO.setId(1L);
|
||||
reqVO.setWord("updatedbad");
|
||||
reqVO.setStatus(CommonStatusEnum.ENABLE.getStatus());
|
||||
|
||||
// 调用
|
||||
sensitiveWordService.updateSensitiveWord(reqVO);
|
||||
|
||||
// 调用 + 断言:再次 validateText 应使用新词库
|
||||
ServiceException exception = assertThrows(ServiceException.class,
|
||||
() -> sensitiveWordService.validateText("contains updatedbad here"));
|
||||
assertEquals(MESSAGE_SENSITIVE_WORD_BLOCKED.getCode(), exception.getCode());
|
||||
// 断言缓存已刷新
|
||||
assertServiceException(() -> sensitiveWordService.validateText("contains updatedbad here"), MESSAGE_SENSITIVE_WORD_BLOCKED);
|
||||
|
||||
// 断言:selectListByStatus 共被调用 2 次
|
||||
// 断言词库重新加载
|
||||
verify(imSensitiveWordMapper, times(2)).selectListByStatus(CommonStatusEnum.ENABLE.getStatus());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDeleteSensitiveWord_invalidatesCache() {
|
||||
// 准备:首次 validateText 触发 cache load
|
||||
// 准备缓存
|
||||
sensitiveWordService.validateText("hello world");
|
||||
verify(imSensitiveWordMapper, times(1)).selectListByStatus(CommonStatusEnum.ENABLE.getStatus());
|
||||
|
||||
// 准备:让 delete 校验通过
|
||||
// mock 原敏感词
|
||||
when(imSensitiveWordMapper.selectById(1L)).thenReturn(
|
||||
ImSensitiveWordDO.builder().id(1L).word("badword")
|
||||
.status(CommonStatusEnum.ENABLE.getStatus()).build());
|
||||
// 准备:reload 时返回剩余词库(只剩中文那条)
|
||||
// mock 删除后的词库
|
||||
when(imSensitiveWordMapper.selectListByStatus(CommonStatusEnum.ENABLE.getStatus()))
|
||||
.thenReturn(ListUtil.of(
|
||||
ImSensitiveWordDO.builder().id(2L).word("违禁词")
|
||||
.status(CommonStatusEnum.ENABLE.getStatus()).build()
|
||||
));
|
||||
|
||||
// 调用:删除 badword,触发 invalidate
|
||||
// 调用
|
||||
sensitiveWordService.deleteSensitiveWord(1L);
|
||||
|
||||
// 调用 + 断言:badword 已被删,不再命中
|
||||
// 断言缓存已刷新
|
||||
assertDoesNotThrow(() -> sensitiveWordService.validateText("contains badword here"));
|
||||
// 中文词依然在
|
||||
assertThrows(ServiceException.class,
|
||||
() -> sensitiveWordService.validateText("这条消息里有违禁词哦"));
|
||||
// 断言未删除的敏感词仍然生效
|
||||
assertServiceException(() -> sensitiveWordService.validateText("这条消息里有违禁词哦"), MESSAGE_SENSITIVE_WORD_BLOCKED);
|
||||
|
||||
// 断言:selectListByStatus 共被调用 2 次
|
||||
// 断言词库重新加载
|
||||
verify(imSensitiveWordMapper, times(2)).selectListByStatus(CommonStatusEnum.ENABLE.getStatus());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDeleteSensitiveWord_notExists_throws() {
|
||||
// 准备:selectById 返回 null
|
||||
// mock 敏感词不存在
|
||||
when(imSensitiveWordMapper.selectById(999L)).thenReturn(null);
|
||||
|
||||
// 调用 + 断言:抛 SENSITIVE_WORD_NOT_EXISTS
|
||||
ServiceException exception = assertThrows(ServiceException.class,
|
||||
() -> sensitiveWordService.deleteSensitiveWord(999L));
|
||||
assertEquals(SENSITIVE_WORD_NOT_EXISTS.getCode(), exception.getCode());
|
||||
// 调用,并断言异常
|
||||
assertServiceException(() -> sensitiveWordService.deleteSensitiveWord(999L), SENSITIVE_WORD_NOT_EXISTS);
|
||||
|
||||
// 断言:未走到 deleteById
|
||||
// 断言
|
||||
verify(imSensitiveWordMapper, never()).deleteById(anyLong());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDeleteSensitiveWordList_invalidatesCache() {
|
||||
// 准备:首次 validateText 触发 cache load
|
||||
// 准备缓存
|
||||
sensitiveWordService.validateText("hello world");
|
||||
verify(imSensitiveWordMapper, times(1)).selectListByStatus(CommonStatusEnum.ENABLE.getStatus());
|
||||
|
||||
// 准备:reload 时返回空词库
|
||||
// mock 删除后的词库
|
||||
when(imSensitiveWordMapper.selectListByStatus(CommonStatusEnum.ENABLE.getStatus()))
|
||||
.thenReturn(ListUtil.of());
|
||||
|
||||
// 调用:批量删除,触发 invalidate
|
||||
// 调用
|
||||
sensitiveWordService.deleteSensitiveWordList(ListUtil.of(1L, 2L));
|
||||
|
||||
// 调用 + 断言:所有词都不再命中
|
||||
// 断言缓存已刷新
|
||||
assertDoesNotThrow(() -> sensitiveWordService.validateText("contains badword here"));
|
||||
assertDoesNotThrow(() -> sensitiveWordService.validateText("这条消息里有违禁词哦"));
|
||||
|
||||
// 断言:deleteByIds 被调用 1 次;selectListByStatus 共 2 次
|
||||
// 断言
|
||||
verify(imSensitiveWordMapper, times(1)).deleteByIds(ListUtil.of(1L, 2L));
|
||||
verify(imSensitiveWordMapper, times(2)).selectListByStatus(CommonStatusEnum.ENABLE.getStatus());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDeleteSensitiveWordList_emptyIds_skip() {
|
||||
// 调用:空列表直接返回
|
||||
// 调用
|
||||
sensitiveWordService.deleteSensitiveWordList(ListUtil.of());
|
||||
|
||||
// 断言:mapper 不被调用
|
||||
// 断言
|
||||
verify(imSensitiveWordMapper, never()).deleteByIds(anyList());
|
||||
}
|
||||
|
||||
|
||||
+9
-5
@@ -1,12 +1,11 @@
|
||||
package cn.iocoder.yudao.module.product.controller.admin.spu.vo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import jakarta.validation.constraints.Min;
|
||||
import jakarta.validation.constraints.NotEmpty;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.*;
|
||||
|
||||
import javax.validation.constraints.NotEmpty;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import java.util.List;
|
||||
|
||||
@Schema(description = "管理后台 - 商品 SKU 创建/更新 Request VO")
|
||||
@@ -19,12 +18,15 @@ public class ProductSkuSaveReqVO {
|
||||
|
||||
@Schema(description = "销售价格,单位:分", requiredMode = Schema.RequiredMode.REQUIRED, example = "1999")
|
||||
@NotNull(message = "销售价格,单位:分不能为空")
|
||||
@Min(value = 0, message = "销售价格,单位:分不能小于 0")
|
||||
private Integer price;
|
||||
|
||||
@Schema(description = "市场价", example = "2999")
|
||||
@Min(value = 0, message = "市场价不能小于 0")
|
||||
private Integer marketPrice;
|
||||
|
||||
@Schema(description = "成本价", example = "19")
|
||||
@Min(value = 0, message = "成本价不能小于 0")
|
||||
private Integer costPrice;
|
||||
|
||||
@Schema(description = "条形码", example = "15156165456")
|
||||
@@ -45,9 +47,11 @@ public class ProductSkuSaveReqVO {
|
||||
private Double volume;
|
||||
|
||||
@Schema(description = "一级分销的佣金,单位:分", example = "199")
|
||||
@Min(value = 0, message = "一级分销的佣金,单位:分不能小于 0")
|
||||
private Integer firstBrokeragePrice;
|
||||
|
||||
@Schema(description = "二级分销的佣金,单位:分", example = "19")
|
||||
@Min(value = 0, message = "二级分销的佣金,单位:分不能小于 0")
|
||||
private Integer secondBrokeragePrice;
|
||||
|
||||
@Schema(description = "属性数组")
|
||||
|
||||
+4
-3
@@ -76,14 +76,15 @@ public interface ProductSpuMapper extends BaseMapperX<ProductSpuDO> {
|
||||
query.eq(ProductSpuDO::getStatus, ProductSpuStatusEnum.ENABLE.getStatus());
|
||||
|
||||
// 排序逻辑
|
||||
Boolean sortAsc = pageReqVO.getSortAsc() != null ? pageReqVO.getSortAsc() : Boolean.TRUE;
|
||||
if (Objects.equals(pageReqVO.getSortField(), AppProductSpuPageReqVO.SORT_FIELD_SALES_COUNT)) {
|
||||
query.last(String.format(" ORDER BY (sales_count + virtual_sales_count) %s, sort DESC, id DESC",
|
||||
pageReqVO.getSortAsc() ? "ASC" : "DESC"));
|
||||
sortAsc ? "ASC" : "DESC"));
|
||||
} else if (Objects.equals(pageReqVO.getSortField(), AppProductSpuPageReqVO.SORT_FIELD_PRICE)) {
|
||||
query.orderBy(true, pageReqVO.getSortAsc(), ProductSpuDO::getPrice)
|
||||
query.orderBy(true, sortAsc, ProductSpuDO::getPrice)
|
||||
.orderByDesc(ProductSpuDO::getSort).orderByDesc(ProductSpuDO::getId);
|
||||
} else if (Objects.equals(pageReqVO.getSortField(), AppProductSpuPageReqVO.SORT_FIELD_CREATE_TIME)) {
|
||||
query.orderBy(true, pageReqVO.getSortAsc(), ProductSpuDO::getCreateTime)
|
||||
query.orderBy(true, sortAsc, ProductSpuDO::getCreateTime)
|
||||
.orderByDesc(ProductSpuDO::getSort).orderByDesc(ProductSpuDO::getId);
|
||||
} else {
|
||||
query.orderByDesc(ProductSpuDO::getSort).orderByDesc(ProductSpuDO::getId);
|
||||
|
||||
+2
@@ -16,6 +16,7 @@ public interface ErrorCodeConstants {
|
||||
ErrorCode CATEGORY_EXISTS_CHILDREN = new ErrorCode(1_008_001_003, "存在子分类,无法删除");
|
||||
ErrorCode CATEGORY_DISABLED = new ErrorCode(1_008_001_004, "商品分类({})已禁用,无法使用");
|
||||
ErrorCode CATEGORY_HAVE_BIND_SPU = new ErrorCode(1_008_001_005, "类别下存在商品,无法删除");
|
||||
ErrorCode CATEGORY_PARENT_IS_SELF = new ErrorCode(1_008_001_006, "父分类不能设置为分类自身");
|
||||
|
||||
// ========== 商品品牌相关编号 1-008-002-000 ==========
|
||||
ErrorCode BRAND_NOT_EXISTS = new ErrorCode(1_008_002_000, "品牌不存在");
|
||||
@@ -44,6 +45,7 @@ public interface ErrorCodeConstants {
|
||||
ErrorCode SPU_ATTR_NUMBERS_MUST_BE_EQUALS = new ErrorCode(1_008_006_002, "一个 SPU 下的每个 SKU,其属性项必须一致");
|
||||
ErrorCode SPU_SKU_NOT_DUPLICATE = new ErrorCode(1_008_006_003, "一个 SPU 下的每个 SKU,必须不重复");
|
||||
ErrorCode SKU_STOCK_NOT_ENOUGH = new ErrorCode(1_008_006_004, "商品 SKU 库存不足");
|
||||
ErrorCode SKU_PROPERTIES_EMPTY = new ErrorCode(1_008_006_005, "多规格商品必须从规格列表选择属性组合,SKU 属性不能为空");
|
||||
|
||||
// ========== 商品 评价 1-008-007-000 ==========
|
||||
ErrorCode COMMENT_NOT_EXISTS = new ErrorCode(1_008_007_000, "商品评价不存在");
|
||||
|
||||
+4
@@ -55,6 +55,10 @@ public class ProductCategoryServiceImpl implements ProductCategoryService {
|
||||
public void updateCategory(ProductCategorySaveReqVO updateReqVO) {
|
||||
// 校验分类是否存在
|
||||
validateProductCategoryExists(updateReqVO.getId());
|
||||
// 校验父分类不能是分类自身(环检测:父级设成自己会形成循环层级,后续按父链遍历将死循环/脏数据)
|
||||
if (Objects.equals(updateReqVO.getParentId(), updateReqVO.getId())) {
|
||||
throw exception(CATEGORY_PARENT_IS_SELF);
|
||||
}
|
||||
// 校验父分类存在
|
||||
validateParentProductCategory(updateReqVO.getParentId());
|
||||
|
||||
|
||||
+3
-2
@@ -104,11 +104,12 @@ public class ProductSkuServiceImpl implements ProductSkuService {
|
||||
|
||||
// 1、校验属性项存在
|
||||
Set<Long> propertyIds = skus.stream().filter(p -> p.getProperties() != null)
|
||||
// 遍历多个 Property 属性
|
||||
.flatMap(p -> p.getProperties().stream())
|
||||
// 将每个 Property 转换成对应的 propertyId,最后形成集合
|
||||
.map(ProductSkuSaveReqVO.Property::getPropertyId)
|
||||
.collect(Collectors.toSet());
|
||||
if (CollUtil.isEmpty(propertyIds)) {
|
||||
throw exception(SKU_PROPERTIES_EMPTY);
|
||||
}
|
||||
List<ProductPropertyDO> propertyList = productPropertyService.getPropertyList(propertyIds);
|
||||
if (propertyList.size() != propertyIds.size()) {
|
||||
throw exception(PROPERTY_NOT_EXISTS);
|
||||
|
||||
+2
-1
@@ -32,7 +32,8 @@ public interface CombinationActivityMapper extends BaseMapperX<CombinationActivi
|
||||
|
||||
default PageResult<CombinationActivityDO> selectPage(PageParam pageParam, Integer status) {
|
||||
return selectPage(pageParam, new LambdaQueryWrapperX<CombinationActivityDO>()
|
||||
.eq(CombinationActivityDO::getStatus, status));
|
||||
.eq(CombinationActivityDO::getStatus, status)
|
||||
.orderByDesc(CombinationActivityDO::getId));
|
||||
}
|
||||
|
||||
default CombinationActivityDO selectBySpuIdAndStatusAndNow(Long spuId, Integer status) {
|
||||
|
||||
+4
-2
@@ -45,11 +45,13 @@ public interface BrokerageRecordService {
|
||||
/**
|
||||
* 增加佣金【多级分佣】
|
||||
*
|
||||
* @param userId 会员编号
|
||||
* @param userId 会员编号(下单人)
|
||||
* @param brokerageUserId 订单推广人
|
||||
* @param bizType 业务类型
|
||||
* @param list 请求参数列表
|
||||
*/
|
||||
void addBrokerage(Long userId, BrokerageRecordBizTypeEnum bizType, @Valid List<BrokerageAddReqBO> list);
|
||||
void addBrokerage(Long userId, Long brokerageUserId, BrokerageRecordBizTypeEnum bizType,
|
||||
@Valid List<BrokerageAddReqBO> list);
|
||||
|
||||
/**
|
||||
* 增加佣金【只针对自己】
|
||||
|
||||
+5
-3
@@ -74,16 +74,18 @@ public class BrokerageRecordServiceImpl implements BrokerageRecordService {
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void addBrokerage(Long userId, BrokerageRecordBizTypeEnum bizType, List<BrokerageAddReqBO> list) {
|
||||
public void addBrokerage(Long userId, Long brokerageUserId, BrokerageRecordBizTypeEnum bizType,
|
||||
List<BrokerageAddReqBO> list) {
|
||||
TradeConfigDO memberConfig = tradeConfigService.getTradeConfig();
|
||||
// 0 未启用分销功能
|
||||
if (memberConfig == null || !BooleanUtil.isTrue(memberConfig.getBrokerageEnabled())) {
|
||||
log.error("[addBrokerage][增加佣金失败:brokerageEnabled 未配置,userId({}) bizType({}) list({})", userId, bizType, list);
|
||||
log.error("[addBrokerage][增加佣金失败:brokerageEnabled 未配置,userId({}) brokerageUserId({}) bizType({}) list({})",
|
||||
userId, brokerageUserId, bizType, list);
|
||||
return;
|
||||
}
|
||||
|
||||
// 1.1 获得一级推广人
|
||||
BrokerageUserDO firstUser = brokerageUserService.getBindBrokerageUser(userId);
|
||||
BrokerageUserDO firstUser = brokerageUserService.getBrokerageUser(brokerageUserId);
|
||||
if (firstUser == null || !BooleanUtil.isTrue(firstUser.getBrokerageEnabled())) {
|
||||
return;
|
||||
}
|
||||
|
||||
+4
-3
@@ -58,7 +58,7 @@ public class TradeBrokerageOrderHandler implements TradeOrderHandler {
|
||||
if (order.getBrokerageUserId() == null) {
|
||||
return;
|
||||
}
|
||||
addBrokerage(order.getUserId(), orderItems);
|
||||
addBrokerage(order.getUserId(), order.getBrokerageUserId(), orderItems);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -97,9 +97,10 @@ public class TradeBrokerageOrderHandler implements TradeOrderHandler {
|
||||
* 2. 支付 or 下单成功时,创建分销记录(冻结),确认收货解冻或者 n 天后解冻
|
||||
*
|
||||
* @param userId 用户编号
|
||||
* @param brokerageUserId 订单推广人
|
||||
* @param orderItems 订单项
|
||||
*/
|
||||
protected void addBrokerage(Long userId, List<TradeOrderItemDO> orderItems) {
|
||||
protected void addBrokerage(Long userId, Long brokerageUserId, List<TradeOrderItemDO> orderItems) {
|
||||
MemberUserRespDTO user = memberUserApi.getUser(userId);
|
||||
Assert.notNull(user);
|
||||
Map<Long, ProductSpuRespDTO> spusMap = productSpuApi.getSpuMap(convertList(orderItems, TradeOrderItemDO::getSpuId));
|
||||
@@ -113,7 +114,7 @@ public class TradeBrokerageOrderHandler implements TradeOrderHandler {
|
||||
Assert.notNull(sku);
|
||||
return TradeOrderConvert.INSTANCE.convert(user, item, spu, sku);
|
||||
});
|
||||
brokerageRecordService.addBrokerage(userId, BrokerageRecordBizTypeEnum.ORDER, addList);
|
||||
brokerageRecordService.addBrokerage(userId, brokerageUserId, BrokerageRecordBizTypeEnum.ORDER, addList);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+24
@@ -11,10 +11,12 @@ import cn.iocoder.yudao.module.product.api.spu.dto.ProductSpuRespDTO;
|
||||
import cn.iocoder.yudao.module.trade.controller.admin.brokerage.vo.record.BrokerageRecordPageReqVO;
|
||||
import cn.iocoder.yudao.module.trade.controller.app.brokerage.vo.record.AppBrokerageProductPriceRespVO;
|
||||
import cn.iocoder.yudao.module.trade.dal.dataobject.brokerage.BrokerageRecordDO;
|
||||
import cn.iocoder.yudao.module.trade.dal.dataobject.brokerage.BrokerageUserDO;
|
||||
import cn.iocoder.yudao.module.trade.dal.dataobject.config.TradeConfigDO;
|
||||
import cn.iocoder.yudao.module.trade.dal.mysql.brokerage.BrokerageRecordMapper;
|
||||
import cn.iocoder.yudao.module.trade.enums.brokerage.BrokerageRecordBizTypeEnum;
|
||||
import cn.iocoder.yudao.module.trade.enums.brokerage.BrokerageRecordStatusEnum;
|
||||
import cn.iocoder.yudao.module.trade.service.brokerage.bo.BrokerageAddReqBO;
|
||||
import cn.iocoder.yudao.module.trade.service.config.TradeConfigService;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.boot.test.mock.mockito.MockBean;
|
||||
@@ -22,6 +24,7 @@ import org.springframework.context.annotation.Import;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.math.RoundingMode;
|
||||
import java.util.List;
|
||||
|
||||
import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.buildBetweenTime;
|
||||
import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.buildTime;
|
||||
@@ -30,6 +33,7 @@ import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertPojoEq
|
||||
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
/**
|
||||
@@ -96,6 +100,26 @@ public class BrokerageRecordServiceImplTest extends BaseDbUnitTest {
|
||||
assertPojoEquals(dbBrokerageRecord, pageResult.getList().get(0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAddBrokerage_usesOrderBrokerageUser() {
|
||||
TradeConfigDO tradeConfig = new TradeConfigDO();
|
||||
tradeConfig.setBrokerageEnabled(true);
|
||||
tradeConfig.setBrokerageFirstPercent(10);
|
||||
when(tradeConfigService.getTradeConfig()).thenReturn(tradeConfig);
|
||||
BrokerageUserDO orderBrokerageUser = new BrokerageUserDO().setId(200L).setBrokerageEnabled(true)
|
||||
.setBrokeragePrice(0);
|
||||
when(brokerageUserService.getBrokerageUser(200L)).thenReturn(orderBrokerageUser);
|
||||
|
||||
brokerageRecordService.addBrokerage(100L, 200L, BrokerageRecordBizTypeEnum.ORDER,
|
||||
ListUtil.of(new BrokerageAddReqBO("1", 1000, null, null, 100L, "订单商品")));
|
||||
|
||||
List<BrokerageRecordDO> records = brokerageRecordMapper.selectList(null);
|
||||
assertEquals(1, records.size());
|
||||
assertEquals(200L, records.get(0).getUserId());
|
||||
assertEquals(100, records.get(0).getPrice());
|
||||
verify(brokerageUserService).updateUserPrice(200L, 100);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCalculatePrice_useFixedPrice() {
|
||||
// mock 数据
|
||||
|
||||
+1
-1
@@ -10,11 +10,11 @@ import cn.iocoder.yudao.module.mes.service.wm.materialstock.MesWmMaterialStockSe
|
||||
import cn.iocoder.yudao.module.mes.service.wm.warehouse.MesWmWarehouseAreaService;
|
||||
import cn.iocoder.yudao.module.mes.service.md.item.MesMdItemService;
|
||||
import cn.iocoder.yudao.module.mes.service.wm.batch.MesWmBatchService;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
+24
@@ -96,6 +96,30 @@ public class MesCalTeamShiftServiceImplTest extends BaseDbUnitTest {
|
||||
assertRecord(records.get(8), LocalDateTime.of(2026, 4, 3, 0, 0), 3, 101L, 13L);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGenerateTeamShiftRecords_singleWithoutRotation() {
|
||||
Long planId = 3002L;
|
||||
LocalDateTime start = LocalDateTime.of(2026, 9, 21, 0, 0);
|
||||
when(planService.getPlan(planId)).thenReturn(MesCalPlanDO.builder()
|
||||
.id(planId).code("SINGLE-001").name("单白班")
|
||||
.calendarType(1).startDate(start).endDate(start.plusDays(1))
|
||||
.shiftType(MesCalShiftTypeEnum.SINGLE.getType())
|
||||
.shiftMethod(null).shiftCount(null).build());
|
||||
when(planShiftService.getPlanShiftListByPlanId(planId)).thenReturn(ListUtil.of(
|
||||
MesCalPlanShiftDO.builder().id(11L).planId(planId).sort(1).name("白班")
|
||||
.startTime("08:00").endTime("16:00").build()));
|
||||
when(planTeamService.getPlanTeamListByPlanId(planId)).thenReturn(ListUtil.of(
|
||||
MesCalPlanTeamDO.builder().id(21L).planId(planId).teamId(101L).build()));
|
||||
|
||||
teamShiftService.generateTeamShiftRecords(planId);
|
||||
|
||||
List<MesCalTeamShiftDO> records = teamShiftMapper.selectListByPlanId(planId);
|
||||
records.sort(Comparator.comparing(MesCalTeamShiftDO::getDay));
|
||||
assertEquals(2, records.size());
|
||||
assertRecord(records.get(0), start, 1, 101L, 11L);
|
||||
assertRecord(records.get(1), start.plusDays(1), 1, 101L, 11L);
|
||||
}
|
||||
|
||||
private static void assertRecord(MesCalTeamShiftDO actual, LocalDateTime day, Integer sort,
|
||||
Long teamId, Long shiftId) {
|
||||
assertEquals(day, actual.getDay());
|
||||
|
||||
+2
@@ -46,6 +46,8 @@ public interface ErrorCodeConstants {
|
||||
ErrorCode USER_IMPORT_INIT_PASSWORD = new ErrorCode(1_002_003_009, "初始密码不能为空");
|
||||
ErrorCode USER_MOBILE_NOT_EXISTS = new ErrorCode(1_002_003_010, "该手机号尚未注册");
|
||||
ErrorCode USER_REGISTER_DISABLED = new ErrorCode(1_002_003_011, "注册功能已关闭");
|
||||
ErrorCode USER_ADMIN_NOT_ALLOW_DISABLE = new ErrorCode(1_002_003_012, "超级管理员或租户管理员账号不能禁用");
|
||||
ErrorCode USER_ADMIN_NOT_ALLOW_DELETE = new ErrorCode(1_002_003_013, "超级管理员或租户管理员账号不能删除");
|
||||
|
||||
// ========== 部门模块 1-002-004-000 ==========
|
||||
ErrorCode DEPT_NAME_DUPLICATE = new ErrorCode(1_002_004_000, "已经存在该名字的部门");
|
||||
|
||||
+46
-6
@@ -6,10 +6,12 @@ import cn.hutool.core.util.ObjUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
|
||||
import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
|
||||
import cn.iocoder.yudao.framework.common.exception.ErrorCode;
|
||||
import cn.iocoder.yudao.framework.common.exception.ServiceException;
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageResult;
|
||||
import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
|
||||
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
|
||||
import cn.iocoder.yudao.framework.common.util.object.ObjectUtils;
|
||||
import cn.iocoder.yudao.framework.common.util.validation.ValidationUtils;
|
||||
import cn.iocoder.yudao.framework.datapermission.core.util.DataPermissionUtils;
|
||||
import cn.iocoder.yudao.module.infra.api.config.ConfigApi;
|
||||
@@ -22,14 +24,17 @@ import cn.iocoder.yudao.module.system.controller.admin.user.vo.user.UserPageReqV
|
||||
import cn.iocoder.yudao.module.system.controller.admin.user.vo.user.UserSaveReqVO;
|
||||
import cn.iocoder.yudao.module.system.dal.dataobject.dept.DeptDO;
|
||||
import cn.iocoder.yudao.module.system.dal.dataobject.dept.UserPostDO;
|
||||
import cn.iocoder.yudao.module.system.dal.dataobject.permission.RoleDO;
|
||||
import cn.iocoder.yudao.module.system.dal.dataobject.user.AdminUserDO;
|
||||
import cn.iocoder.yudao.module.system.dal.mysql.dept.UserPostMapper;
|
||||
import cn.iocoder.yudao.module.system.dal.mysql.user.AdminUserMapper;
|
||||
import cn.iocoder.yudao.module.system.enums.permission.RoleCodeEnum;
|
||||
import cn.iocoder.yudao.module.system.mq.producer.user.AdminUserProducer;
|
||||
import cn.iocoder.yudao.module.system.service.dept.DeptService;
|
||||
import cn.iocoder.yudao.module.system.service.dept.PostService;
|
||||
import cn.iocoder.yudao.module.system.service.oauth2.OAuth2TokenService;
|
||||
import cn.iocoder.yudao.module.system.service.permission.PermissionService;
|
||||
import cn.iocoder.yudao.module.system.service.permission.RoleService;
|
||||
import cn.iocoder.yudao.module.system.service.tenant.TenantService;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import com.mzt.logapi.context.LogRecordContext;
|
||||
@@ -75,6 +80,8 @@ public class AdminUserServiceImpl implements AdminUserService {
|
||||
@Resource
|
||||
private PermissionService permissionService;
|
||||
@Resource
|
||||
private RoleService roleService;
|
||||
@Resource
|
||||
private PasswordEncoder passwordEncoder;
|
||||
@Resource
|
||||
@Lazy // 延迟,避免循环依赖报错
|
||||
@@ -250,15 +257,20 @@ public class AdminUserServiceImpl implements AdminUserService {
|
||||
|
||||
@Override
|
||||
public void updateUserStatus(Long id, Integer status) {
|
||||
// 校验用户存在
|
||||
// 1.1 校验用户存在
|
||||
validateUserExists(id);
|
||||
// 更新状态
|
||||
// 1.2 管理员不能禁用
|
||||
if (CommonStatusEnum.isDisable(status)) {
|
||||
validateUserNotAdmin(id, USER_ADMIN_NOT_ALLOW_DISABLE);
|
||||
}
|
||||
|
||||
// 2. 更新状态
|
||||
AdminUserDO updateObj = new AdminUserDO();
|
||||
updateObj.setId(id);
|
||||
updateObj.setStatus(status);
|
||||
userMapper.updateById(updateObj);
|
||||
|
||||
// 如果是禁用用户,则删除其 Token 信息
|
||||
// 3. 如果是禁用用户,则删除其 Token 信息
|
||||
if (CommonStatusEnum.isDisable(status)) {
|
||||
oauth2TokenService.removeAccessToken(id, UserTypeEnum.ADMIN.getValue());
|
||||
}
|
||||
@@ -269,8 +281,10 @@ public class AdminUserServiceImpl implements AdminUserService {
|
||||
@LogRecord(type = SYSTEM_USER_TYPE, subType = SYSTEM_USER_DELETE_SUB_TYPE, bizNo = "{{#id}}",
|
||||
success = SYSTEM_USER_DELETE_SUCCESS)
|
||||
public void deleteUser(Long id) {
|
||||
// 1. 校验用户存在
|
||||
// 1.1 校验用户存在
|
||||
AdminUserDO user = validateUserExists(id);
|
||||
// 1.2 管理员不能删除
|
||||
validateUserNotAdmin(id, USER_ADMIN_NOT_ALLOW_DELETE);
|
||||
|
||||
// 2.1 删除用户
|
||||
userMapper.deleteById(id);
|
||||
@@ -286,16 +300,32 @@ public class AdminUserServiceImpl implements AdminUserService {
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void deleteUserList(List<Long> ids) {
|
||||
// 1. 批量删除用户
|
||||
// 1. 校验不能删除管理员
|
||||
ids.forEach(id -> validateUserNotAdmin(id, USER_ADMIN_NOT_ALLOW_DELETE));
|
||||
|
||||
// 2. 批量删除用户
|
||||
userMapper.deleteByIds(ids);
|
||||
|
||||
// 2. 批量删除用户关联数据
|
||||
// 3. 批量删除用户关联数据
|
||||
ids.forEach(id -> {
|
||||
permissionService.processUserDeleted(id);
|
||||
userPostMapper.deleteByUserId(id);
|
||||
});
|
||||
}
|
||||
|
||||
private void validateUserNotAdmin(Long id, ErrorCode errorCode) {
|
||||
Set<Long> roleIds = permissionService.getUserRoleIdListByUserId(id);
|
||||
if (CollUtil.isEmpty(roleIds)) {
|
||||
return;
|
||||
}
|
||||
for (RoleDO role : roleService.getRoleList(roleIds)) {
|
||||
if (ObjectUtils.equalsAny(role.getCode(),
|
||||
RoleCodeEnum.SUPER_ADMIN.getCode(), RoleCodeEnum.TENANT_ADMIN.getCode())) {
|
||||
throw exception(errorCode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public AdminUserDO getUserByUsername(String username) {
|
||||
return userMapper.selectByUsername(username);
|
||||
@@ -562,6 +592,16 @@ public class AdminUserServiceImpl implements AdminUserService {
|
||||
respVO.getFailureUsernames().put(importUser.getUsername(), USER_USERNAME_EXISTS.getMsg());
|
||||
return;
|
||||
}
|
||||
// 2.2.3 校验管理员不能禁用
|
||||
if (CommonStatusEnum.isDisable(importUser.getStatus())) {
|
||||
try {
|
||||
validateUserNotAdmin(existUser.getId(), USER_ADMIN_NOT_ALLOW_DISABLE);
|
||||
} catch (ServiceException ex) {
|
||||
respVO.getFailureUsernames().put(importUser.getUsername(), ex.getMessage());
|
||||
return;
|
||||
}
|
||||
}
|
||||
// 2.2.4 更新用户
|
||||
AdminUserDO updateUser = BeanUtils.toBean(importUser, AdminUserDO.class);
|
||||
updateUser.setId(existUser.getId());
|
||||
userMapper.updateById(updateUser);
|
||||
|
||||
+105
-11
@@ -19,25 +19,28 @@ import cn.iocoder.yudao.module.system.controller.admin.user.vo.user.UserSaveReqV
|
||||
import cn.iocoder.yudao.module.system.dal.dataobject.dept.DeptDO;
|
||||
import cn.iocoder.yudao.module.system.dal.dataobject.dept.PostDO;
|
||||
import cn.iocoder.yudao.module.system.dal.dataobject.dept.UserPostDO;
|
||||
import cn.iocoder.yudao.module.system.dal.dataobject.permission.RoleDO;
|
||||
import cn.iocoder.yudao.module.system.dal.dataobject.tenant.TenantDO;
|
||||
import cn.iocoder.yudao.module.system.dal.dataobject.user.AdminUserDO;
|
||||
import cn.iocoder.yudao.module.system.dal.mysql.dept.UserPostMapper;
|
||||
import cn.iocoder.yudao.module.system.dal.mysql.user.AdminUserMapper;
|
||||
import cn.iocoder.yudao.module.system.enums.common.SexEnum;
|
||||
import cn.iocoder.yudao.module.system.enums.permission.RoleCodeEnum;
|
||||
import cn.iocoder.yudao.module.system.mq.producer.user.AdminUserProducer;
|
||||
import cn.iocoder.yudao.module.system.service.dept.DeptService;
|
||||
import cn.iocoder.yudao.module.system.service.dept.PostService;
|
||||
import cn.iocoder.yudao.module.system.service.oauth2.OAuth2TokenService;
|
||||
import cn.iocoder.yudao.module.system.service.permission.PermissionService;
|
||||
import cn.iocoder.yudao.module.system.service.permission.RoleService;
|
||||
import cn.iocoder.yudao.module.system.service.tenant.TenantService;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.stubbing.Answer;
|
||||
import org.springframework.boot.test.mock.mockito.MockBean;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.test.context.bean.override.mockito.MockitoBean;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@@ -71,23 +74,25 @@ public class AdminUserServiceImplTest extends BaseDbUnitTest {
|
||||
@Resource
|
||||
private UserPostMapper userPostMapper;
|
||||
|
||||
@MockBean
|
||||
@MockitoBean
|
||||
private DeptService deptService;
|
||||
@MockBean
|
||||
@MockitoBean
|
||||
private PostService postService;
|
||||
@MockBean
|
||||
@MockitoBean
|
||||
private PermissionService permissionService;
|
||||
@MockBean
|
||||
@MockitoBean
|
||||
private RoleService roleService;
|
||||
@MockitoBean
|
||||
private PasswordEncoder passwordEncoder;
|
||||
@MockBean
|
||||
@MockitoBean
|
||||
private TenantService tenantService;
|
||||
@MockBean
|
||||
@MockitoBean
|
||||
private FileApi fileApi;
|
||||
@MockBean
|
||||
@MockitoBean
|
||||
private ConfigApi configApi;
|
||||
@MockBean
|
||||
@MockitoBean
|
||||
private OAuth2TokenService oauth2TokenService;
|
||||
@MockBean
|
||||
@MockitoBean
|
||||
private AdminUserProducer adminUserProducer;
|
||||
|
||||
@BeforeEach
|
||||
@@ -289,6 +294,35 @@ public class AdminUserServiceImplTest extends BaseDbUnitTest {
|
||||
verify(oauth2TokenService).removeAccessToken(userId, UserTypeEnum.ADMIN.getValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUpdateUserStatus_adminCannotDisable() {
|
||||
for (RoleCodeEnum roleCode : new RoleCodeEnum[]{RoleCodeEnum.SUPER_ADMIN, RoleCodeEnum.TENANT_ADMIN}) {
|
||||
// mock 数据
|
||||
AdminUserDO dbUser = randomAdminUserDO(o -> o.setStatus(CommonStatusEnum.ENABLE.getStatus()));
|
||||
userMapper.insert(dbUser);
|
||||
mockUserRole(dbUser.getId(), roleCode);
|
||||
|
||||
// 调用,并断言异常
|
||||
assertServiceException(() -> userService.updateUserStatus(dbUser.getId(), CommonStatusEnum.DISABLE.getStatus()),
|
||||
USER_ADMIN_NOT_ALLOW_DISABLE);
|
||||
assertEquals(CommonStatusEnum.ENABLE.getStatus(), userMapper.selectById(dbUser.getId()).getStatus());
|
||||
}
|
||||
verify(oauth2TokenService, never()).removeAccessToken(anyLong(), anyInt());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUpdateUserStatus_adminCanEnable() {
|
||||
// mock 数据
|
||||
AdminUserDO dbUser = randomAdminUserDO(o -> o.setStatus(CommonStatusEnum.DISABLE.getStatus()));
|
||||
userMapper.insert(dbUser);
|
||||
mockUserRole(dbUser.getId(), RoleCodeEnum.TENANT_ADMIN);
|
||||
|
||||
// 调用
|
||||
userService.updateUserStatus(dbUser.getId(), CommonStatusEnum.ENABLE.getStatus());
|
||||
// 断言
|
||||
assertEquals(CommonStatusEnum.ENABLE.getStatus(), userMapper.selectById(dbUser.getId()).getStatus());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDeleteUser_success(){
|
||||
// mock 数据
|
||||
@@ -305,6 +339,43 @@ public class AdminUserServiceImplTest extends BaseDbUnitTest {
|
||||
verify(permissionService, times(1)).processUserDeleted(eq(userId));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDeleteUser_adminCannotDelete() {
|
||||
for (RoleCodeEnum roleCode : new RoleCodeEnum[]{RoleCodeEnum.SUPER_ADMIN, RoleCodeEnum.TENANT_ADMIN}) {
|
||||
// mock 数据
|
||||
AdminUserDO dbUser = randomAdminUserDO();
|
||||
userMapper.insert(dbUser);
|
||||
mockUserRole(dbUser.getId(), roleCode);
|
||||
|
||||
// 调用,并断言异常
|
||||
assertServiceException(() -> userService.deleteUser(dbUser.getId()), USER_ADMIN_NOT_ALLOW_DELETE);
|
||||
assertNotNull(userMapper.selectById(dbUser.getId()));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDeleteUserList_adminCannotDelete() {
|
||||
// mock 数据
|
||||
AdminUserDO ordinaryUser = randomAdminUserDO();
|
||||
userMapper.insert(ordinaryUser);
|
||||
AdminUserDO adminUser = randomAdminUserDO();
|
||||
userMapper.insert(adminUser);
|
||||
mockUserRole(adminUser.getId(), RoleCodeEnum.TENANT_ADMIN);
|
||||
|
||||
// 调用,并断言整批未删除
|
||||
assertServiceException(() -> userService.deleteUserList(List.of(ordinaryUser.getId(), adminUser.getId())),
|
||||
USER_ADMIN_NOT_ALLOW_DELETE);
|
||||
assertNotNull(userMapper.selectById(ordinaryUser.getId()));
|
||||
assertNotNull(userMapper.selectById(adminUser.getId()));
|
||||
}
|
||||
|
||||
private void mockUserRole(Long userId, RoleCodeEnum roleCode) {
|
||||
Long roleId = 100L;
|
||||
when(permissionService.getUserRoleIdListByUserId(userId)).thenReturn(asSet(roleId));
|
||||
when(roleService.getRoleList(asSet(roleId))).thenReturn(singletonList(
|
||||
randomPojo(RoleDO.class, role -> role.setCode(roleCode.getCode()))));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetUserByUsername() {
|
||||
// mock 数据
|
||||
@@ -532,6 +603,29 @@ public class AdminUserServiceImplTest extends BaseDbUnitTest {
|
||||
assertEquals(0, respVO.getFailureUsernames().size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testImportUserList_adminCannotDisable() {
|
||||
// mock 数据
|
||||
AdminUserDO dbUser = randomAdminUserDO(o -> o.setStatus(CommonStatusEnum.ENABLE.getStatus()));
|
||||
userMapper.insert(dbUser);
|
||||
mockUserRole(dbUser.getId(), RoleCodeEnum.TENANT_ADMIN);
|
||||
// 准备参数
|
||||
UserImportExcelVO importUser = randomPojo(UserImportExcelVO.class, o -> {
|
||||
o.setUsername(dbUser.getUsername());
|
||||
o.setStatus(CommonStatusEnum.DISABLE.getStatus());
|
||||
o.setSex(randomEle(SexEnum.values()).getSex());
|
||||
o.setEmail(randomEmail());
|
||||
o.setMobile(randomMobile());
|
||||
});
|
||||
|
||||
// 调用
|
||||
UserImportRespVO respVO = userService.importUserList(singletonList(importUser), true);
|
||||
// 断言
|
||||
assertEquals(USER_ADMIN_NOT_ALLOW_DISABLE.getMsg(), respVO.getFailureUsernames().get(importUser.getUsername()));
|
||||
assertTrue(respVO.getUpdateUsernames().isEmpty());
|
||||
assertEquals(CommonStatusEnum.ENABLE.getStatus(), userMapper.selectById(dbUser.getId()).getStatus());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testValidateUserExists_notExists() {
|
||||
assertServiceException(() -> userService.validateUserExists(randomLongId()), USER_NOT_EXISTS);
|
||||
|
||||
Reference in New Issue
Block a user