330 Commits

Author SHA1 Message Date
mzl92 43ea23307d Merge branch 'master' into develop 2026-03-16 14:37:54 +08:00
mzl92 929bae6f13 Finish Hotfix-1 2026-03-16 14:37:54 +08:00
mzl92 a72b30a699 调整合塑询单分配人 2026-03-16 14:37:39 +08:00
18338781771 4ec72f7772 Merge branch 'feature/stock' into develop 2026-03-03 17:44:18 +08:00
18338781771 75a59a7197 优化 2026-03-03 17:44:10 +08:00
18338781771 51a553e1a4 Merge branch 'feature/xsm' into develop 2026-03-03 16:09:03 +08:00
18338781771 46666d1c2f 签名 2026-03-03 16:08:53 +08:00
18338781771 5a94a74590 签名算法 2026-03-03 11:44:20 +08:00
18338781771 88338b60fa 系统认证接口 2026-03-03 10:37:52 +08:00
18338781771 fda46cd187 Merge branch 'feature/xsm' into develop 2026-03-03 09:46:19 +08:00
18338781771 f16feca4f4 系统认证接口 2026-03-03 09:45:46 +08:00
18338781771 6e458e3751 Merge branch 'feature/inv' into develop 2026-02-27 10:45:22 +08:00
18338781771 f2451a48bb 筛选 2026-02-27 10:45:11 +08:00
18338781771 c88d2f20bc Merge branch 'feature/inv' into develop 2026-02-26 17:32:12 +08:00
18338781771 a7bc73f0d3 1 2026-02-26 17:32:03 +08:00
18338781771 4787e21491 Merge branch 'feature/inv' into develop 2026-02-26 17:21:35 +08:00
18338781771 d5a1b11b35 jsapi 2026-02-26 17:21:22 +08:00
18338781771 530a8123ba Merge branch 'feature/inv' into develop 2026-02-26 16:11:50 +08:00
18338781771 acdabea76e jsapi 2026-02-26 16:11:40 +08:00
18338781771 f33f969166 jsapi 2026-02-26 12:02:06 +08:00
18338781771 499611c700 Merge branch 'feature/inv' into develop 2026-02-26 11:07:43 +08:00
18338781771 114b1fbf92 jsApi 2026-02-26 11:06:31 +08:00
18338781771 a1c2b36d92 Merge branch 'feature/inv' into develop 2026-02-26 10:46:27 +08:00
18338781771 f7fa768da1 jsApi 2026-02-26 10:46:16 +08:00
18338781771 51344ac1fc Merge branch 'feature/inv' into develop 2026-02-26 09:33:54 +08:00
18338781771 7ac5bcd516 待审核筛选 2026-02-26 09:33:43 +08:00
18338781771 67069576b7 Merge branch 'feature/inv' into develop 2026-02-26 09:26:16 +08:00
18338781771 1951e2333c 待审核筛选 2026-02-26 09:26:05 +08:00
18338781771 19eab96bdc Merge branch 'feature/inv' into develop 2026-02-25 17:10:25 +08:00
18338781771 ae9dd498af 关联审批记录 2026-02-25 17:10:02 +08:00
18338781771 766b717a5a Merge branch 'feature/inv' into develop 2026-02-25 15:58:57 +08:00
18338781771 f2524dec22 物品领用 2026-02-25 15:58:46 +08:00
18338781771 89a824f1b8 Merge branch 'feature/inv' into develop 2026-02-25 15:50:12 +08:00
18338781771 5a3a1cf0de 1 2026-02-25 15:50:00 +08:00
18338781771 9210dcb312 Merge branch 'feature/inv' into develop 2026-02-25 15:03:53 +08:00
18338781771 0e88fdf1d1 1 2026-02-25 15:03:43 +08:00
18338781771 6c9adce32a Merge branch 'feature/inv' into develop 2026-02-25 14:46:43 +08:00
18338781771 73ce99edfb 物品修改 2026-02-25 14:46:34 +08:00
18338781771 3168885701 Merge branch 'feature/inv' into develop 2026-02-25 14:12:57 +08:00
18338781771 30e04b3d4c 扫码领取数量限制 2026-02-25 14:12:47 +08:00
18338781771 ddbec81d04 Merge branch 'feature/inv' into develop 2026-02-25 13:57:29 +08:00
18338781771 63605accc0 库存修改 2026-02-25 13:57:05 +08:00
18338781771 9f78530185 Merge branch 'feature/inv' into develop 2026-02-25 13:33:21 +08:00
18338781771 1c26828569 库存修改 2026-02-25 13:33:11 +08:00
18338781771 ef2bd1df55 Merge branch 'feature/inv' into develop 2026-02-25 13:21:09 +08:00
18338781771 1d88ef932a 库存修改 2026-02-25 13:13:36 +08:00
18338781771 5120f4bf4f Merge branch 'feature/inv' into develop 2026-02-25 10:41:39 +08:00
18338781771 1251f78c6b 扫码领取数量限制 2026-02-25 10:40:39 +08:00
18338781771 f44c8611d1 Merge branch 'feature/inv' into develop 2026-02-25 09:36:02 +08:00
18338781771 e829e406e1 扫码地址调整 2026-02-25 09:35:51 +08:00
18338781771 f28da4c4b9 Merge branch 'feature/inv' into develop 2026-02-24 17:36:03 +08:00
18338781771 407e573c0b 检测是否飞书扫码 2026-02-24 17:35:58 +08:00
18338781771 c2e2a20e35 Merge branch 'feature/inv' into develop 2026-02-24 16:54:46 +08:00
18338781771 8d83309daf 物品列表 2026-02-24 16:54:39 +08:00
18338781771 a8f1af9995 Merge branch 'feature/inv' into develop 2026-02-24 16:46:49 +08:00
18338781771 fb8f87dd5f 物品创建校验修复 2026-02-24 16:46:34 +08:00
18338781771 d59c390674 Merge branch 'feature/inv' into develop 2026-02-24 16:16:00 +08:00
18338781771 144fa72075 编辑数量时记录 2026-02-24 16:15:49 +08:00
18338781771 b4445abc3a Merge branch 'feature/inv' into develop 2026-02-24 14:19:24 +08:00
18338781771 cf7d8a9f5b 代领多人物品 2026-02-24 14:19:17 +08:00
18338781771 976945378e 初始库存变化 2026-02-24 13:55:48 +08:00
18338781771 d02555df0a Merge branch 'feature/inv' into develop 2026-02-24 13:30:33 +08:00
18338781771 a9aabfa454 二维码 2026-02-24 13:30:27 +08:00
18338781771 0067a7508f Merge branch 'feature/inv' into develop 2026-02-24 11:28:21 +08:00
18338781771 cd692594e4 物品扫码领用与库存 2026-02-24 11:28:12 +08:00
18338781771 6028ad3b6a Merge branch 'feature/inv' into develop 2026-02-12 15:33:30 +08:00
18338781771 7098f30af2 确认归还 2026-02-12 15:33:23 +08:00
18338781771 119e725b17 Merge branch 'feature/inv' into develop 2026-02-11 15:43:48 +08:00
18338781771 28287e7a7c 归还记录审核人 2026-02-11 15:43:38 +08:00
18338781771 3e16776d65 Merge branch 'feature/inv' into develop 2026-02-11 15:26:39 +08:00
18338781771 244b874751 归还记录审核人 2026-02-11 15:26:29 +08:00
18338781771 e4fb1fda2c Merge branch 'feature/inv' into develop 2026-02-11 15:10:39 +08:00
18338781771 9b55bcce79 归还记录审核人 2026-02-11 14:58:37 +08:00
18338781771 466519378e Merge branch 'feature/inv' into develop 2026-02-11 13:42:58 +08:00
18338781771 b721860d4a 审核列表筛选 2026-02-11 13:41:54 +08:00
18338781771 6a0fbd01fd Merge branch 'feature/inv' into develop 2026-02-11 10:48:15 +08:00
18338781771 d3d55df4c9 附件 2026-02-11 10:48:06 +08:00
18338781771 39e673ea1a Merge branch 'feature/inv' into develop 2026-02-10 15:27:22 +08:00
18338781771 41d968b369 归还记录过滤 2026-02-10 15:27:17 +08:00
18338781771 01f49f35b2 Merge branch 'feature/inv' into develop 2026-02-10 15:02:22 +08:00
18338781771 20857453d2 查询当前人是否已审核 2026-02-10 15:02:16 +08:00
18338781771 f57b0ca1f0 Merge branch 'feature/inv' into develop 2026-02-10 13:58:02 +08:00
18338781771 452182f0b2 审核附件 2026-02-10 13:57:52 +08:00
18338781771 90a431015b Merge branch 'feature/inv' into develop 2026-02-10 10:20:18 +08:00
18338781771 6469e05176 当前人审核状态 2026-02-10 10:20:09 +08:00
18338781771 698f532ced Merge branch 'feature/inv' into develop 2026-02-10 10:09:45 +08:00
18338781771 3da11b4b37 日期处理 2026-02-10 10:09:39 +08:00
18338781771 af90824edd Merge branch 'feature/inv' into develop 2026-02-10 10:03:34 +08:00
18338781771 f6c8271729 日期处理 2026-02-10 10:03:28 +08:00
18338781771 ad5c053a5e Merge branch 'feature/inv' into develop 2026-02-10 09:51:18 +08:00
18338781771 40bc3a894d 审批过的审批状态处理 2026-02-10 09:51:11 +08:00
18338781771 c72b128872 Merge branch 'feature/inv' into develop 2026-02-09 14:08:19 +08:00
18338781771 fde98ea5b9 审核详情 2026-02-09 14:08:08 +08:00
18338781771 5f605f5a63 Merge branch 'feature/inv' into develop 2026-02-09 11:37:58 +08:00
18338781771 4894b25722 时间处理 2026-02-09 11:37:24 +08:00
18338781771 92776b39d4 Merge branch 'feature/inv' into develop 2026-02-09 11:00:46 +08:00
18338781771 537b22c6fb 日期处理 2026-02-09 11:00:41 +08:00
18338781771 1dfc183cac Merge branch 'feature/inv' into develop 2026-02-09 10:40:56 +08:00
18338781771 25b79d6f63 物品领用 2026-02-09 10:40:50 +08:00
18338781771 359a8f403c Merge branch 'feature/inv' into develop 2026-02-06 15:51:53 +08:00
18338781771 3d33a502d9 1 2026-02-06 15:51:48 +08:00
18338781771 eeab4934cd Merge branch 'feature/inv' into develop 2026-02-06 15:47:22 +08:00
18338781771 74ca1a5d8c 归还列表 2026-02-06 15:47:14 +08:00
18338781771 488d510ad0 Merge branch 'feature/inv' into develop 2026-02-06 15:35:05 +08:00
18338781771 d719ee5ea2 审核权限 2026-02-06 15:34:01 +08:00
18338781771 04a3c85bc1 Merge branch 'feature/inv' into develop 2026-02-06 14:30:19 +08:00
18338781771 d3ed9c05c6 附件路径 2026-02-06 14:29:45 +08:00
18338781771 8105200154 Merge branch 'feature/inv' into develop 2026-02-06 14:02:14 +08:00
18338781771 070cb87d17 归还附件 2026-02-06 14:02:05 +08:00
18338781771 032b19a4ce Merge branch 'feature/inv' into develop 2026-02-06 13:24:28 +08:00
18338781771 320211cd21 1 2026-02-06 13:24:17 +08:00
18338781771 a2218dc517 Merge branch 'feature/inv' into develop 2026-02-06 13:05:05 +08:00
18338781771 7b1837ed20 归还详情与编辑 2026-02-06 13:04:51 +08:00
18338781771 828c8f1d60 Merge branch 'feature/inv' into develop 2026-02-05 15:47:02 +08:00
18338781771 b831775490 归还详情 2026-02-05 15:46:40 +08:00
18338781771 d58c54da76 Merge branch 'feature/inv' into develop 2026-02-05 14:57:16 +08:00
18338781771 cdb889137f 归还物品信息 2026-02-05 14:57:09 +08:00
18338781771 e3b38ee117 Merge branch 'feature/inv' into develop 2026-02-05 14:42:08 +08:00
18338781771 4464c86788 归还 2026-02-05 14:41:58 +08:00
18338781771 750c2fc642 Merge branch 'feature/inv' into develop 2026-02-05 13:40:45 +08:00
18338781771 a521cfcaf4 借用状态 2026-02-05 13:40:38 +08:00
18338781771 385f380914 Merge branch 'feature/inv' into develop 2026-02-05 13:18:36 +08:00
18338781771 e84aba0a49 借用单筛选 2026-02-05 13:18:29 +08:00
18338781771 f67f9ddef9 Merge branch 'feature/inv' into develop 2026-02-05 11:38:55 +08:00
18338781771 a39c18b714 审批 2026-02-05 11:38:49 +08:00
18338781771 44dc6c06ee Merge branch 'feature/inv' into develop 2026-02-05 11:06:14 +08:00
18338781771 757689573b 审批id 2026-02-05 11:06:05 +08:00
18338781771 be44d1dda2 Merge branch 'feature/inv' into develop 2026-02-05 10:48:33 +08:00
18338781771 28e5afe62a 物品管理员需是同一人 2026-02-05 10:48:26 +08:00
18338781771 e922e78ae2 Merge branch 'feature/inv' into develop 2026-02-05 10:22:17 +08:00
18338781771 7bf2249ce6 移动端登录 2026-02-05 10:22:10 +08:00
18338781771 f0a92cafda Merge branch 'feature/inv' into develop
# Conflicts:
#	app/process/CheckWeb.php
2026-02-05 09:56:37 +08:00
18338781771 9686e52484 1 2026-02-05 09:55:25 +08:00
18338781771 c896bef9a6 Merge branch 'feature/inv' into develop
# Conflicts:
#	app/controller/IndexController.php
#	composer.lock
#	config/route.php
2026-02-05 09:51:21 +08:00
18338781771 a8cab8c51c 领用耗材 2026-02-05 09:33:27 +08:00
18338781771 e4f0d107ec Merge branch 'feature/crawl' into develop 2026-02-05 09:17:16 +08:00
18338781771 30f3e211af 抓取品牌 2026-02-05 09:16:24 +08:00
18338781771 eee09316df 抓取车型信息 2026-02-04 17:35:08 +08:00
18338781771 a16429ff3a 物品归还 2026-02-04 09:28:54 +08:00
mzl 59750d52f6 Merge branch 'master' into develop 2026-02-03 11:15:04 +08:00
mzl 2da3804953 Finish Hotfix-1 2026-02-03 11:15:03 +08:00
mzl eae699cd27 ts 2026-02-03 11:14:45 +08:00
18338781771 bce47e81f8 Merge branch 'hotfix/customer' into develop 2026-02-03 10:51:30 +08:00
18338781771 4ea4204ff4 渠道调整 2026-02-03 10:51:08 +08:00
18338781771 6496a40679 物品归还 2026-02-02 15:25:16 +08:00
18338781771 ab3bffe7cb 借用 2026-02-02 13:17:11 +08:00
18338781771 a8d1309349 1 2026-01-30 10:46:54 +08:00
18338781771 80d0196c3d Merge branch 'hotfix/stockn' into develop 2026-01-22 17:23:16 +08:00
18338781771 67a39917f0 过滤未提交的 2026-01-22 17:20:29 +08:00
mzl92 278dda4f7a Merge branch 'master' into develop 2026-01-22 10:15:19 +08:00
mzl92 b6851cdef0 Finish Hotfix-1 2026-01-22 10:15:19 +08:00
mzl92 733996f346 修改询盘自动配置 2026-01-22 10:15:05 +08:00
18338781771 607de6c70a 超管 2026-01-20 15:41:24 +08:00
18338781771 e7c5e17592 内勤可以看自己组员的备货单 2026-01-20 15:30:24 +08:00
mzl92 2ec89a880a Finish Hotfix-1 2026-01-08 15:08:44 +08:00
mzl92 dffaac5b07 ts 2026-01-08 15:08:35 +08:00
mzl92 907be759a0 Finish Hotfix-1 2026-01-08 15:04:47 +08:00
mzl92 f7b94da39f ts 2026-01-08 15:04:39 +08:00
mzl92 c6fada47fc Finish Hotfix-1 2026-01-08 15:02:05 +08:00
mzl92 b09e695ca7 ts 2026-01-08 15:01:51 +08:00
mzl92 de39108efa Finish Hotfix-1 2026-01-08 14:49:54 +08:00
mzl92 7dceb4b828 ts 2026-01-08 14:49:44 +08:00
mzl92 d056ba5f6f Finish Hotfix-1 2026-01-08 09:14:34 +08:00
mzl92 99551bba7f 移除加纳询盘独立分配 2026-01-08 09:14:17 +08:00
18338781771 e408f7e20e 物品领用 2026-01-07 17:38:06 +08:00
18338781771 7cafb61797 物品领用审核 2026-01-07 17:23:03 +08:00
18338781771 5a7a6a45d4 借用详情 2026-01-07 13:38:47 +08:00
18338781771 5841b342de 借用明细 2026-01-07 11:32:13 +08:00
18338781771 63011a0351 借用列表 2026-01-07 11:07:43 +08:00
18338781771 2b529a27c8 物品借用审批列表 2026-01-07 10:57:28 +08:00
18338781771 cab15c4fab 借用列表与详情 2026-01-07 10:49:33 +08:00
18338781771 139acd9e70 产品规格不必传处理 2026-01-07 09:42:08 +08:00
18338781771 c38a53f053 物品下拉 2026-01-05 10:42:08 +08:00
18338781771 07a40bf688 物品相关信息 2026-01-05 10:18:27 +08:00
18338781771 5e06b1737c Merge branch 'hotfix/email' 2026-01-05 09:44:33 +08:00
18338781771 b43335c0d9 邮件清洗查询状态过滤 2026-01-05 09:44:26 +08:00
18338781771 c2e402722d 登录 2026-01-04 17:23:10 +08:00
18338781771 de9484f570 移动端回调 2026-01-04 16:42:19 +08:00
18338781771 ca757e67dd 移动端回调 2026-01-04 16:27:20 +08:00
18338781771 ac6bfcf16c Merge branch 'hotfix/email' 2026-01-04 16:13:55 +08:00
18338781771 9f74934f37 邮件 2026-01-04 16:13:21 +08:00
18338781771 9776df8289 Merge branch 'hotfix/email' 2026-01-04 15:50:50 +08:00
18338781771 6178084612 邮件 2026-01-04 15:50:43 +08:00
18338781771 d58c33e758 Merge branch 'hotfix/email' 2026-01-04 14:40:54 +08:00
18338781771 24e45f93db 邮件 2026-01-04 14:40:43 +08:00
18338781771 530d72071f Merge branch 'hotfix/email' 2026-01-04 13:39:59 +08:00
18338781771 8382a47a8c 邮件处理 2026-01-04 13:32:15 +08:00
18338781771 f83e578bed Merge branch 'hotfix/email' 2026-01-04 13:26:12 +08:00
18338781771 2c00a35b6a 发布人 2026-01-04 13:26:00 +08:00
18338781771 cd597b9605 Merge branch 'hotfix/email' 2026-01-04 11:46:28 +08:00
18338781771 259a7d2ae6 判断询盘 2026-01-04 11:46:04 +08:00
18338781771 10d3701ab0 Merge branch 'hotfix/email' 2026-01-04 11:02:59 +08:00
18338781771 279139e18f 1 2026-01-04 11:01:29 +08:00
mzl92 4d957e8f43 Finish Hotfix-1 2026-01-04 10:48:03 +08:00
mzl92 57251718b4 调整询盘分配人员 2026-01-04 10:47:46 +08:00
18338781771 0bcd9bf029 多邮箱处理 2025-12-26 14:12:54 +08:00
18338781771 927f1d9c28 邮件分配 2025-12-26 14:04:42 +08:00
mzl92 05096bc5ed Finish Hotfix-1 2025-12-25 16:11:52 +08:00
mzl92 ece685fbe5 ts 2025-12-25 16:09:50 +08:00
mzl92 8072388da2 ts 2025-12-25 16:07:33 +08:00
mzl92 f2b7d40b17 ts 2025-12-25 15:59:07 +08:00
18338781771 e514129ff6 业务主管备货单只看自己的 2025-12-24 14:02:52 +08:00
18338781771 a951fc39b3 业务主管只看自己的备货单 2025-12-24 13:41:14 +08:00
mzl92 7c12ab54e1 Finish Hotfix-1 2025-12-23 14:44:51 +08:00
mzl92 d81fdae406 备货单审批增加兜底检查 2025-12-23 14:44:43 +08:00
mzl92 1e70bbe0ad Finish Hotfix-1 2025-12-23 14:36:06 +08:00
mzl92 861b8b216f 增加备货单修改兜底检查 2025-12-23 14:35:57 +08:00
mzl92 5b2590bff8 Finish Hotfix-1 2025-12-22 16:22:33 +08:00
mzl92 7d1ea71e94 ts 2025-12-22 16:22:20 +08:00
mzl92 d171ad58c7 Finish Hotfix-1 2025-12-18 15:10:56 +08:00
mzl92 31a610664f inquiry ts 2025-12-18 15:10:43 +08:00
mzl92 8f60c79b7c Finish Hotfix-1 2025-12-16 14:22:29 +08:00
mzl92 5aadcac7e3 ts 2025-12-16 14:22:21 +08:00
mzl92 ca3455b219 Finish Hotfix-1 2025-12-16 13:44:47 +08:00
mzl92 56f8657886 ts 2025-12-16 13:44:38 +08:00
mzl92 c29c4941fc Merge branch 'develop' 2025-12-15 16:06:51 +08:00
mzl92 e960dd9b36 Finish Hotfix-1 2025-12-12 18:37:11 +08:00
mzl92 cd622addec ts 2025-12-12 18:37:03 +08:00
mzl92 da3794db50 Finish Hotfix-1 2025-12-12 18:11:53 +08:00
mzl92 8fab10b972 ts 2025-12-12 18:11:44 +08:00
mzl92 eea13a2a58 Merge branch 'develop'
# Conflicts:
#	app/command/InquiryImportFbCommand.php
#	app/inquiry/utils/FbLeadHeaderMapper.php
2025-12-12 17:52:20 +08:00
mzl92 1d0b51d598 Finish Hotfix-1 2025-12-11 15:32:43 +08:00
mzl92 f0af3a0cdb ts 2025-12-11 15:32:36 +08:00
mzl92 45a77ba12e Finish Hotfix-1 2025-12-11 15:23:13 +08:00
mzl92 85a79193a6 ts 2025-12-11 15:23:02 +08:00
mzl92 38deea3b0d Merge branch 'develop' 2025-12-11 15:07:55 +08:00
mzl92 b37cdc6834 Finish Hotfix-1 2025-12-10 20:26:35 +08:00
mzl92 b60df7f5cf ts 2025-12-10 20:26:21 +08:00
mzl92 628429af66 Merge branch 'develop' 2025-12-10 20:18:46 +08:00
18338781771 2a6d517a74 无报价时价格处理 2025-12-10 13:39:08 +08:00
18338781771 f58b0ee986 无报价时价格处理 2025-12-10 13:34:55 +08:00
18338781771 1207352e5a 日志 2025-12-10 13:12:02 +08:00
18338781771 14967f28c2 车价 2025-12-10 12:11:31 +08:00
mzl92 725710abcb ts 2025-12-09 07:33:04 +00:00
18338781771 4ff2c2493b 文件大小限制调整 2025-12-09 11:51:22 +08:00
18338781771 b391608d87 内勤备货单查询 2025-12-09 11:12:36 +08:00
18338781771 0ace4a7b0b 内勤备货单查询 2025-12-09 11:08:51 +08:00
18338781771 7258d9890e 内勤备货单查询 2025-12-09 10:54:58 +08:00
18338781771 8c3fbd4d9a 内勤备货单查询 2025-12-09 10:48:59 +08:00
18338781771 a33b109ddb 内勤备货单查询 2025-12-08 15:01:02 +08:00
18338781771 4bcc6c53f7 备货单与审批数据过滤 2025-12-08 13:42:20 +08:00
18338781771 32aa7170a5 备货单与审批数据过滤 2025-12-08 13:24:37 +08:00
18338781771 59994d1f5a 备货单与审批数据过滤 2025-12-08 13:22:37 +08:00
18338781771 a0a71161d3 备货单与审批数据过滤 2025-12-08 13:19:16 +08:00
mzl92 399eaa2b5c 物品领用 2025-12-05 09:22:53 +00:00
18338781771 06a264ff21 登录后首页 2025-12-05 16:27:22 +08:00
18338781771 82673cdcfa 仪表盘路由 2025-12-05 10:34:34 +08:00
18338781771 b4b4f6cd5f 仪表盘路由 2025-12-05 10:28:36 +08:00
18338781771 143dde08ce 仪表板路由 2025-12-04 18:11:45 +08:00
18338781771 2ffd053723 仪表盘路由 2025-12-04 18:10:09 +08:00
18338781771 d238b47aa8 仪表盘路由 2025-12-04 18:08:26 +08:00
18338781771 5c3f8679ce 仪表盘路由规则 2025-12-04 17:54:40 +08:00
18338781771 3520f10572 直属上级与内勤 2025-12-04 15:06:34 +08:00
18338781771 1858d3e586 返回角色id 2025-12-04 14:34:38 +08:00
mzl92 253616b2bb Merge branch 'master' into develop 2025-12-02 18:02:21 +08:00
mzl92 c0a5904c12 Finish Hotfix-1 2025-12-02 18:02:20 +08:00
mzl92 e2f79598b2 调整邮件转发移除不参与分配人员 2025-12-02 18:02:12 +08:00
18338781771 75a333b756 取消日志记录 2025-12-02 17:53:45 +08:00
mzl92 beabfd96fb Merge branch 'master' into develop 2025-12-02 17:34:12 +08:00
mzl92 0d979934be Finish Hotfix-1 2025-12-02 17:34:12 +08:00
mzl92 0153f40127 调整依赖包 2025-12-02 17:34:02 +08:00
mzl92 cc93979e9a Merge branch 'master' into develop 2025-12-02 17:30:49 +08:00
mzl92 fb5b523264 Finish Hotfix-1 2025-12-02 17:30:48 +08:00
mzl92 54c23a26b7 修改依赖包 2025-12-02 17:30:39 +08:00
mzl92 f336d1c4af Finish mzl_dev 2025-12-02 14:17:45 +08:00
mzl92 35836edbfa Merge branch 'develop' into feature/mzl_dev 2025-12-02 14:17:33 +08:00
mzl92 32a7752f8b Merge branch 'develop' into feature/mzl_dev
# Conflicts:
#	.gitignore
#	composer.lock
2025-12-02 14:17:25 +08:00
mzl92 f182684378 Merge branch 'master' into develop 2025-12-02 14:15:09 +08:00
mzl92 bbb3f2f70e ts 2025-12-02 14:14:25 +08:00
mzl92 d53f18181a ts 2025-12-02 14:13:48 +08:00
mzl92 75e31a7e9f Merge branch 'master' into develop 2025-12-02 10:00:34 +08:00
mzl92 707fd275c1 Finish Hotfix-1 2025-12-02 10:00:33 +08:00
mzl92 80d458a8b0 修改邮件转发接收人规则 2025-12-02 01:58:54 +00:00
18338781771 c4a2b39edd Merge branch 'feature/transmit' into develop 2025-12-01 18:16:02 +08:00
18338781771 fe78500ef0 转发 2025-12-01 18:15:53 +08:00
18338781771 45169572b5 Merge branch 'feature/transmit' into develop 2025-12-01 17:51:25 +08:00
18338781771 db7c8a650b 询单转发 2025-12-01 17:51:16 +08:00
18338781771 bf74d9b917 询单转发 2025-12-01 17:48:44 +08:00
18338781771 631d433b7e Merge branch 'feature/transmit' into develop 2025-11-29 17:50:21 +08:00
18338781771 05e39d2d65 询单转发 2025-11-29 17:50:08 +08:00
18338781771 9fbe094a30 Merge branch 'feature/transmit' into develop 2025-11-29 16:14:20 +08:00
18338781771 af8577064d 记录寻盘 2025-11-29 16:13:53 +08:00
18338781771 1ba1d5763f Merge branch 'feature/transmit' into develop 2025-11-29 15:55:56 +08:00
18338781771 22ba33125a 询单 2025-11-29 15:55:48 +08:00
18338781771 5086344742 Merge branch 'feature/transmit' into develop 2025-11-29 15:51:42 +08:00
18338781771 6997947717 询单 2025-11-29 15:51:34 +08:00
18338781771 63cca12a8d Merge branch 'feature/transmit' into develop 2025-11-29 15:46:27 +08:00
18338781771 1e37560301 询单转发 2025-11-29 15:46:11 +08:00
18338781771 eb4813b77b 询单转发 2025-11-29 15:44:23 +08:00
18338781771 f664653eb9 Merge branch 'feature/transmit' into develop 2025-11-29 15:37:06 +08:00
18338781771 8a94de610d 询盘转发 2025-11-29 15:36:53 +08:00
mzl92 00bd5ada53 ts 2025-11-28 17:37:10 +08:00
18338781771 6bdcc73ee8 模糊抓取优化 2025-11-28 16:12:10 +08:00
mzl92 2c295cd631 tz 2025-11-26 15:51:05 +08:00
mzl92 134505be8c 修改爬虫服务 2025-11-25 10:55:56 +08:00
mzl92 79d19a53e6 二手车信息爬虫服务 2025-11-24 18:29:11 +08:00
18338781771 fa22cfe132 Merge branch 'feature/crm' 2025-11-18 11:00:32 +08:00
18338781771 6096f7ee01 删除原因 2025-11-17 14:39:52 +08:00
18338781771 cb1b208190 移除原因 2025-11-17 10:05:29 +08:00
18338781771 3d96152ea9 品牌关键词抓取 2025-11-14 15:25:08 +08:00
18338781771 cd0d850f89 关键词抓取 2025-11-14 14:11:54 +08:00
mzl92 1d9fbb9032 Merge branch 'develop' into feature/mzl_dev 2025-11-13 11:50:32 +08:00
mzl92 76bd46a0d7 Merge branch 'master' into develop 2025-11-13 11:47:16 +08:00
mzl92 515a16a633 Finish Hotfix-1 2025-11-13 11:47:16 +08:00
mzl92 f0abdca45a ts 2025-11-13 11:47:07 +08:00
mzl92 20188521fd ts 2025-11-13 11:45:38 +08:00
mzl92 00ab988469 Finish Hotfix-1 2025-11-13 11:42:34 +08:00
mzl92 1d4224bf4a Merge branch 'master' into develop 2025-11-13 11:42:34 +08:00
mzl92 d388b6ace9 ts 2025-11-13 11:42:22 +08:00
mzl92 f388ad41d6 Finish Hotfix-1 2025-11-13 10:06:30 +08:00
mzl92 8b8336437a Merge branch 'master' into develop 2025-11-13 10:06:30 +08:00
mzl92 8b3778e83e 调整 2025-11-13 10:06:19 +08:00
mzl92 c157b3ee12 更新类库 2025-11-12 11:35:22 +08:00
mzl92 80fbf6c35a Merge branch 'develop'
# Conflicts:
#	config/route.php
2025-11-11 16:46:22 +08:00
mzl92 2d7456b74c Finish mzl_dev 2025-11-11 16:44:13 +08:00
mzl92 8740918785 Merge branch 'develop' into feature/mzl_dev 2025-11-11 16:44:00 +08:00
mzl92 d515bc07fe 备货单调整 2025-11-11 08:38:32 +00:00
mzl92 39820844f7 修改忽略 2025-11-07 16:34:57 +08:00
mzl92 6ab4505e17 对账 2025-11-07 15:28:59 +08:00
mzl92 6c59fdef10 Merge branch 'develop'
# Conflicts:
#	app/common/service/StockOrdersService.php
2025-11-04 15:43:42 +08:00
mzl92 663d29dc61 Finish mzl_dev 2025-11-04 15:13:41 +08:00
mzl92 862a87d66f Merge branch 'develop' into feature/mzl_dev 2025-11-04 15:13:35 +08:00
mzl92 32452dc29a 调整汽车数据采集 2025-11-04 15:13:19 +08:00
mzl92 f19ec4edc4 Finish mzl_dev 2025-11-04 13:11:54 +08:00
mzl92 7e90b05dde Merge branch 'develop' into feature/mzl_dev 2025-11-04 13:11:44 +08:00
mzl92 29dd6a2954 Finish sku_crawl 2025-11-04 13:11:34 +08:00
mzl92 be4dcb4dd4 删除增加过滤 2025-11-04 13:10:46 +08:00
mzl92 aabd0f4634 Merge branch 'develop'
# Conflicts:
#	app/admin/controller/dashboard/FinStatisticsController.php
#	app/common/controller/FinPaymentApplyController.php
#	app/common/controller/FinReceiptController.php
#	app/common/logic/FinReceiptLogic.php
#	app/common/model/FinPaymentApply.php
#	app/common/model/FinReceipt.php
#	app/common/service/FinPaymentApplyService.php
#	app/common/service/FinReceiptService.php
#	app/common/service/StockOrdersService.php
#	config/route.php
2025-11-04 10:36:52 +08:00
mzl92 359755fdf1 Merge branch 'release' 2025-11-03 18:16:51 +08:00
18338781771 a0935c4332 暂停抓取车辆信息 2025-10-31 16:33:02 +08:00
161 changed files with 18793 additions and 2676 deletions
+6
View File
@@ -8,3 +8,9 @@
/tests/.phpunit.result.cache
/public/uploads
/webman
/start.php
/windows.php
/bootstrap.php
/composer.lock
.env_copy
/public/excel
@@ -0,0 +1,33 @@
<?php
namespace app\admin\controller;
use app\common\controller\BaseController;
use app\common\traits\crud\CrudControllerTrait;
use app\common\service\FinanceReconDubaiFilesService;
class FinanceReconDubaiFilesController extends BaseController
{
use CrudControllerTrait;
protected $service;
public function __construct()
{
$this->service = new FinanceReconDubaiFilesService();
}
/**
* 列表页数据格式化
*
* @param array $data
* @return array
*/
// protected function indexFormat($data)
// {
// if (isset($data['created_at']))
// $data['created_at'] = date('Y-m-d H:i:s', $data['created_at']);
// if (isset($data['updated_at']))
// $data['updated_at'] = date('Y-m-d H:i:s', $data['updated_at']);
// return $data;
// }
}
@@ -0,0 +1,33 @@
<?php
namespace app\admin\controller;
use app\common\controller\BaseController;
use app\common\traits\crud\CrudControllerTrait;
use app\common\service\InquiryTransmitConfigService;
class InquiryTransmitConfigController extends BaseController
{
use CrudControllerTrait;
protected $service;
public function __construct()
{
$this->service = new InquiryTransmitConfigService();
}
/**
* 列表页数据格式化
*
* @param array $data
* @return array
*/
// protected function indexFormat($data)
// {
// if (isset($data['created_at']))
// $data['created_at'] = date('Y-m-d H:i:s', $data['created_at']);
// if (isset($data['updated_at']))
// $data['updated_at'] = date('Y-m-d H:i:s', $data['updated_at']);
// return $data;
// }
}
@@ -0,0 +1,280 @@
<?php
namespace app\admin\controller;
use app\common\controller\BaseController;
use app\common\model\InquiryTransmit;
use app\common\model\InquiryTransmitConfig;
use app\common\traits\crud\CrudControllerTrait;
use app\common\service\InquiryTransmitService;
use support\Log;
use support\Request;
class InquiryTransmitController extends BaseController
{
use CrudControllerTrait;
protected $noNeedLogin = ['form'];
protected $noNeedHeaderData = ['form'];
protected $service;
public function __construct()
{
$this->service = new InquiryTransmitService();
}
/**
* 列表页数据格式化
*
* @param array $data
* @return array
*/
// protected function indexFormat($data)
// {
// if (isset($data['created_at']))
// $data['created_at'] = date('Y-m-d H:i:s', $data['created_at']);
// if (isset($data['updated_at']))
// $data['updated_at'] = date('Y-m-d H:i:s', $data['updated_at']);
// return $data;
// }
// }
public function form(Request $request)
{
try {
$params = $request->all();
$url = $request->header('referer');
$domain = parse_url($url, PHP_URL_HOST);
$transmitConfig = InquiryTransmitConfig::where('domain', $domain)->first();
if ($transmitConfig) {
$mailto = $transmitConfig->mailto;
$fromDomain = $domain;
} else {
$mailto = '1696136552@qq.com';
$fromDomain = 'referer Failed to obtain';
}
if ($request->method() == 'GET') {
$config = [
'iLang' => 'en',
'Act' => 'Inquiry',
'post_url' => getenv('MAIN_DOMAIN').'/inquiry/transmit/form'.'?m=Data&a=save',
'mailto' => $mailto,
'fromDomain' => $fromDomain,
'fromPath' => parse_url($params['fromlink'], PHP_URL_PATH),
'm' => $params['m'],
'a' => $params['a'],
'tid' => $params['tid'],
'cid' => $params['cid'],
'version' => $params['version'],
'fromlink' => $params['fromlink']
];
$html = match ($params['tid']) {
'en-5-LP-pnx' => $this->formHmtl($config),
'en-3-P-vdx' => $this->formHmtl2($config)
};
return $this->success(['html' => $html], 'success');
// return json([
// 'code' => 200,
// 'data' => [
// 'html' => $html
// ],
// 'message' => 'success'
// ]);
}
if ($request->method() == 'POST') {
$ip = $request->getRealIp($safe_mode=true);
$edata = [
'mailto' => $params['mailto'],
'Date' => date('Y-m-d H:i:s', time()),
'Email' => $params['userEMail'],
'Name' => $params['userName'],
'Tel' => $params['userTel'],
'Company' => '',
'Country' => $params['uCountry'],
'Form Url' => $params['fromlink'],
'Message' => $params['Message'],
'Ip' => $ip,
'IpLocation' => ip2region($ip).'(仅供参考)',
'iLang' => $params['iLang'],
'mailto' => $params['mailto'],
'navigator_language' => $params['navigator_language'],
'navigator_platform' => $params['navigator_platform'],
'screen_width' => $params['screen_width'],
'screen_height' => $params['screen_height'],
'uCountry' => $params['uCountry']
];
$this->service->serviceStore($request, $edata, $domain);
$this->emailHtml($edata);
return $this->success([], 'Thank you for your message! We will contact with you soon.');
// return json([
// 'code' => 200,
// 'data' => [],
// 'message' => 'Thank you for your message! We will contact with you soon.'
// ]);
}
} catch (\Throwable $th) {
Log::info('Data:'.date('Y-m-d H:i:s').':'.$th->getMessage().'|'.$th->getLine().'|'.$th->getFile());
}
}
public function formHmtl($config)
{
// https://v1.xzgoogle.com/form.php?m=Temp&a=getTpl&tid=en-5-LP-pnx&cid=760&version=v4&fromlink=https://magicbridgeauto.com/contact-us/
$html = '<form class="xz-form-x xz-form-pnx" action="'.$config['post_url'].'"> <style> .xz-form-pnx { --c-theme: #BB0010; --c-border: #f0f0f0; --c-bg: #fff; --c-label: #333; --c-font: #666; --c-green: #00B160; --size: 1.6rem; --gap: 1.25em; --h: 3em; --lh: 1.8em; --pd: .4em .625em; --br: 4px; --btn-w: 18.75em; --btn-jc: center; --lines: 4; } .xz-form-pnx { font-size: var(--size); } .xz-form-pnx .xz-form-infos { display: grid; grid-template-columns: 1fr 1fr; grid-gap: var(--gap); margin-bottom: var(--gap); } .xz-form-pnx .xz-form-info, .xz-form-pnx .xz-form-msg { border: solid 1px var(--c-border); background: var(--c-bg); padding: var(--pd); display: flex; align-items: center; border-radius: var(--br); box-sizing: border-box; } .xz-form-pnx .xz-form-info { height: var(--h); } .xz-form-pnx .xz-form-msg { align-items: flex-start; } .xz-form-pnx .xz-form-input, .xz-form-pnx .xz-form-textarea { width: 100%; border: none; font-size: var(--size); line-height: var(--lh); outline: 0; padding: 0; margin-left: 1em; color: var(--c-font); } .xz-form-pnx .xz-form-textarea { height: calc(var(--lh) * var(--lines)); min-height: var(--h); font-family: inherit; resize: none; } .xz-form-pnx .xz-form-label { display: block; color: var(--c-label); line-height: var(--lh); white-space: nowrap; } .xz-form-pnx .xz-form-label.required:after { content: "*"; margin-left: .625em; color: var(--c-theme); } .xz-form-pnx .xz-form-btns { margin-top: var(--gap); display: flex; justify-content: var(--btn-jc); } .xz-form-pnx .xz-form-button { font-size: var(--size); border-radius: var(--br); display: inline-flex; justify-content: center; align-items: center; width: var(--btn-w); height: var(--h); background: var(--c-theme); color: #fff; border: none; transition: all .3s; display: flex; justify-content: center; align-items: center; gap: 1em; } .xz-form-pnx .xz-form-button path { fill: #fff; } .xz-form-pnx .xz-form-button:hover { filter: brightness(1.1); } .xz-form-pnx .xz-form-button:active { transform: scale(.98); } @media screen and (max-width: 500px){ .xz-form-pnx { --gap: .8em; } .xz-form-pnx .xz-form-infos { grid-template-columns: 1fr; } .xz-form-pnx .xz-form-button { width: 100%; } } .xzform_o { position: relative; } .xz-form-pnx .xz-form-message.success { position: absolute; top: 50%; left: 50%; transform: translate(-50%,-50%); z-index: 9; max-width: 75%; display: flex; text-align: center; align-items: center; padding: 4em; background: rgba(255,255,255,.8); color: var(--c-green); font-size: 1.125em; line-height: 1.8em; font-weight: 500; box-shadow: 0 .5rem 3rem -.5rem rgba(0,0,0,.3); } </style> <input type="hidden" name="iLang" value="'.$config['iLang'].'"> <input type="hidden" name="Act" value="'.$config['Act'].'"> <input type="hidden" name="mailto" value="'.$config['mailto'].'"> <input type="hidden" name="customerId" value="'.$config['cid'].'"> <input type="hidden" name="fromDomain" value="'.$config['fromDomain'].'"> <input type="hidden" name="fromlink" value="'.$config['fromlink'].'"> <input type="hidden" name="fromPath" value="'.$config['fromPath'].'"> <input type="hidden" name="navigator.language"> <input type="hidden" name="navigator.platform"> <input type="hidden" name="screen.width"> <input type="hidden" name="screen.height"> <div class="xz-form-infos"> <div class="xz-form-info"> <label class="xz-form-label" for="userName-pnx">Name</label> <input type="text" name="userName" id="userName-pnx" maxlength="80" class="xz-form-input" required> </div> <div class="xz-form-info"> <label class="xz-form-label" for="uCountry-pnx">Country/Region</label> <input type="text" name="uCountry" id="uCountry-pnx" maxlength="80" class="xz-form-input" required> </div> <div class="xz-form-info"> <label class="xz-form-label" for="userEMail-pnx">Email</label> <input type="email" name="userEMail" id="userEMail-pnx" maxlength="80" class="xz-form-input"> </div> <div class="xz-form-info"> <label class="xz-form-label" for="userTel-pnx">Mobile</label> <input type="tel" name="userTel" id="userTel-pnx" maxlength="80" class="xz-form-input" required placeholder="Or Whatsapp/Skype"> </div> </div> <div class="xz-form-msg"> <label class="xz-form-label" for="Message-pnx">Message</label> <textarea name="Message" id="Message-pnx" class="xz-form-textarea" required></textarea> </div> <div class="xz-form-btns"> <button class="xz-form-button"> Submit <svg t="1664258923484" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3462" width="20" height="20"> <path d="M871.04 89.770667L120.064 380.16a51.2 51.2 0 0 0-1.792 94.762667l303.36 130.56 131.072 303.957333a51.2 51.2 0 0 0 94.805333-1.877333l289.792-751.573334a51.2 51.2 0 0 0-66.261333-66.133333z m-41.130667 107.392l-231.978666 601.642666-97.962667-227.114666-3.584-7.338667a85.333333 85.333333 0 0 0-41.045333-37.248l-226.56-97.536 601.173333-232.405333z" p-id="3463"></path> </svg> </button> </div></form>';
return $html;
}
public function formHmtl2($config)
{
// https://v1.xzgoogle.com/form.php?m=Temp&a=getTpl&tid=en-3-P-vdx&cid=760&version=v4&fromlink=https://magicbridgeauto.com/news/
$html = '<form class="xz-form-x xz-form-vdx" action="'.$config['post_url'].'"> <style> .xz-form-vdx { --c-theme: #2C9F1C; --c-border: #f8f8f8; --c-bg: #f8f8f8; --c-font: #333; --c-green: #2C9F1C; --size: 1.6rem; --gap: .625em; --h: 3.5em; --lh: 1.25em; --pd: 1em; --br: .625em; --btn-w: 100%; --lines: 1; font-size: var(--size); } .xz-form-vdx .xz-form-infos { display: grid; grid-template-columns: 1fr; grid-gap: var(--gap); margin-bottom: var(--gap); } .xz-form-vdx .xz-form-info, .xz-form-vdx .xz-form-msg { border: solid 1px var(--c-border); background: var(--c-bg); padding: var(--pd); display: flex; align-items: center; border-radius: var(--br); box-sizing: border-box; } .xz-form-vdx .xz-form-info { height: var(--h); } .xz-form-vdx .xz-form-input, .xz-form-vdx .xz-form-textarea { width: 100%; border: none; font-size: var(--size); line-height: var(--lh); outline: 0; padding: 0; color: var(--c-font); background: transparent; } .xz-form-vdx .xz-form-textarea { height: calc(var(--lh) * var(--lines)); font-family: inherit; resize: none; } .xz-form-vdx .xz-form-btns { margin-top: var(--gap); display: flex; justify-content: var(--btn-jc); } .xz-form-vdx .xz-form-button { font-size: var(--size); border-radius: var(--br); display: inline-flex; justify-content: center; align-items: center; width: var(--btn-w); height: var(--h); background: var(--c-theme); color: #fff; border: none; transition: all .3s; display: flex; justify-content: center; align-items: center; } .xz-form-vdx .xz-form-button:hover { filter: brightness(1.1); } .xz-form-vdx .xz-form-button:active { transform: scale(.98); } .xzform_o { position: relative; } .xz-form-vdx .xz-form-message.success { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); z-index: 9; max-width: 90%; display: flex; text-align: center; align-items: center; padding: 2em; background: rgba(255, 255, 255, .8); color: var(--c-green); font-size: 1em; line-height: 1.5em; box-shadow: 0 .5rem 3rem -.5rem rgba(0, 0, 0, .3); } </style> <input type="hidden" name="iLang" value="'.$config['iLang'].'"> <input type="hidden" name="Act" value="'.$config['Act'].'"> <input type="hidden" name="mailto" value="'.$config['mailto'].'"> <input type="hidden" name="customerId" value="'.$config['cid'].'"> <input type="hidden" name="fromDomain" value="'.$config['fromDomain'].'"> <input type="hidden" name="fromlink" value="'.$config['fromlink'].'"> <input type="hidden" name="fromPath" value="'.$config['fromPath'].'"> <input type="hidden" name="navigator.language"> <input type="hidden" name="navigator.platform"> <input type="hidden" name="screen.width"> <input type="hidden" name="screen.height"> <div class="xz-form-infos"> <div class="xz-form-info"> <input type="text" name="userName" id="userName-vdx" maxlength="80" class="xz-form-input" placeholder="Name"> </div> <div class="xz-form-info"> <input type="email" name="userEMail" id="userEMail-vdx" maxlength="80" class="xz-form-input" placeholder="Email"> </div> </div> <div class="xz-form-msg"> <textarea name="Message" id="Message-vdx" class="xz-form-textarea" placeholder="What can we do for you? *" required></textarea> </div> <div class="xz-form-btns"> <button class="xz-form-button"> SUBMIT <svg t="1690535811615" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5056" width="1em" height="1em" fill="#fff"> <path d="M851.2 166.4c-12.8-6.4-25.6-6.4-32 0l-640 320C166.4 486.4 160 499.2 160 512s6.4 25.6 12.8 25.6l153.6 96c12.8 6.4 25.6 6.4 38.4-6.4L704 320l12.8 6.4-307.2 326.4c-6.4 6.4-6.4 12.8-6.4 19.2l0 140.8c0 12.8 6.4 25.6 19.2 32 12.8 6.4 25.6 0 32-6.4l76.8-76.8 153.6 102.4c6.4 6.4 12.8 6.4 19.2 6.4 6.4 0 6.4 0 12.8 0 12.8-6.4 19.2-12.8 19.2-25.6l128-640C864 185.6 864 172.8 851.2 166.4z" p-id="5057"></path> </svg> </button> </div></form>';
return $html;
}
public function emailHtml($data)
{
$html = '<!doctype html>
<html lang="zh-CN">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>Lead Preview</title>
<meta name="x-apple-disable-message-reformatting">
</head>
<body style="margin:0;padding:0;background:#f6f7f9;">
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%" style="background:#f6f7f9;">
<tr>
<td align="center" style="padding:24px 12px;">
<!-- container -->
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="600" style="width:600px; max-width:100%; background:#ffffff; border:1px solid #e5e7eb; border-radius:8px;">
<!-- header -->
<tr>
<td style="padding:16px 20px; border-bottom:1px solid #e5e7eb; font:700 18px/1.4 Arial, Helvetica, sans-serif; color:#111;">
Preview
</td>
</tr>
<!-- Preview section -->
<tr>
<td style="padding:18px 20px;">
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0">
<!-- 一行:左 label / 右 value -->
<tr>
<td style="width:140px; padding:6px 0; font:700 14px Arial, Helvetica, sans-serif; color:#111;">Date:</td>
<td style="padding:6px 0; font:14px Arial, Helvetica, sans-serif; color:#222;">'.$data['Date'].'</td>
</tr>
<tr>
<td style="width:140px; padding:6px 0; font:700 14px Arial, Helvetica, sans-serif; color:#111;">Email:</td>
<td style="padding:6px 0; font:14px Arial, Helvetica, sans-serif; color:#222;">
<a href="mailto:muhseen2222@yaho.com" style="color:#2563eb; text-decoration:none;">'.$data['Email'].'</a>
</td>
</tr>
<tr>
<td style="width:140px; padding:6px 0; font:700 14px Arial, Helvetica, sans-serif; color:#111;">Name:</td>
<td style="padding:6px 0; font:14px Arial, Helvetica, sans-serif; color:#222;" dir="auto">'.$data['Name'].'</td>
</tr>
<tr>
<td style="width:140px; padding:6px 0; font:700 14px Arial, Helvetica, sans-serif; color:#111;">Tel:</td>
<td style="padding:6px 0; font:14px Arial, Helvetica, sans-serif; color:#222;">'.$data['Tel'].'</td>
</tr>
<tr>
<td style="width:140px; padding:6px 0; font:700 14px Arial, Helvetica, sans-serif; color:#111;">Company:</td>
<td style="padding:6px 0; font:14px Arial, Helvetica, sans-serif; color:#666;"></td>
</tr>
<tr>
<td style="width:140px; padding:6px 0; font:700 14px Arial, Helvetica, sans-serif; color:#111;">Country:</td>
<td style="padding:6px 0; font:14px Arial, Helvetica, sans-serif; color:#222;" dir="auto">'.$data['Country'].'</td>
</tr>
<tr>
<td style="width:140px; padding:6px 0; font:700 14px Arial, Helvetica, sans-serif; color:#111;">Form Url:</td>
<td style="padding:6px 0; font:14px Arial, Helvetica, sans-serif; color:#222; word-break:break-word;">
<a href="'.$data['Form Url'].'" style="color:#2563eb; text-decoration:none;">'.$data['Form Url'].'</a>
</td>
</tr>
<tr>
<td style="width:140px; padding:6px 0; font:700 14px Arial, Helvetica, sans-serif; color:#111;">Message:</td>
<td style="padding:6px 0; font:14px Arial, Helvetica, sans-serif; color:#222;" dir="auto">'.$data['Message'].'</td>
</tr>
<tr>
<td style="width:140px; padding:6px 0; font:700 14px Arial, Helvetica, sans-serif; color:#111;">Ip:</td>
<td style="padding:6px 0; font:14px Arial, Helvetica, sans-serif; color:#222;">'.$data['Ip'].'</td>
</tr>
<tr>
<td style="width:140px; padding:6px 0; font:700 14px Arial, Helvetica, sans-serif; color:#111;">IpLocation:</td>
<td style="padding:6px 0; font:14px Arial, Helvetica, sans-serif; color:#222;">'.$data['IpLocation'].'</td>
</tr>
</table>
</td>
</tr>
<!-- divider + More -->
<tr>
<td style="padding:0 20px;">
<div style="height:1px; background:#e5e7eb; line-height:1px; font-size:0;">&nbsp;</div>
</td>
</tr>
<tr>
<td style="padding:14px 20px 0; font:700 18px Arial, Helvetica, sans-serif; color:#111;">More</td>
</tr>
<tr>
<td style="padding:6px 20px 18px;">
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0">
<tr>
<td style="width:140px; padding:6px 0; font:700 14px Arial, Helvetica, sans-serif; color:#111;">iLang:</td>
<td style="padding:6px 0; font:14px Arial, Helvetica, sans-serif; color:#222;">'.$data['iLang'].'</td>
</tr>
<tr>
<td style="width:140px; padding:6px 0; font:700 14px Arial, Helvetica, sans-serif; color:#111;">mailto:</td>
<td style="padding:6px 0; font:14px Arial, Helvetica, sans-serif; color:#222;">
<a href="mailto:info@borderlesscar.com" style="color:#2563eb; text-decoration:none;">'.$data['mailto'].'</a>
</td>
</tr>
<tr>
<td style="width:140px; padding:6px 0; font:700 14px Arial, Helvetica, sans-serif; color:#111;">navigator_language:</td>
<td style="padding:6px 0; font:14px Arial, Helvetica, sans-serif; color:#222;">'.$data['navigator_language'].'</td>
</tr>
<tr>
<td style="width:140px; padding:6px 0; font:700 14px Arial, Helvetica, sans-serif; color:#111;">navigator_platform:</td>
<td style="padding:6px 0; font:14px Arial, Helvetica, sans-serif; color:#222;">'.$data['navigator_platform'].'</td>
</tr>
<tr>
<td style="width:140px; padding:6px 0; font:700 14px Arial, Helvetica, sans-serif; color:#111;">screen_width:</td>
<td style="padding:6px 0; font:14px Arial, Helvetica, sans-serif; color:#222;">'.$data['screen_width'].'</td>
</tr>
<tr>
<td style="width:140px; padding:6px 0; font:700 14px Arial, Helvetica, sans-serif; color:#111;">screen_height:</td>
<td style="padding:6px 0; font:14px Arial, Helvetica, sans-serif; color:#222;">'.$data['screen_height'].'</td>
</tr>
<tr>
<td style="width:140px; padding:6px 0; font:700 14px Arial, Helvetica, sans-serif; color:#111;">uCountry:</td>
<td style="padding:6px 0; font:14px Arial, Helvetica, sans-serif; color:#222;" dir="auto">'.$data['uCountry'].'</td>
</tr>
</table>
</td>
</tr>
</table>
<!-- /container -->
</td>
</tr>
</table>
</body>
</html>';
// $email = new Email();
//return $email->send($data['mailto'], 'Inquiry_Of_'.$data['Name'], $html);
return \app\common\utils\email\MailSend::send($data['mailto'], 'Inquiry_Of_'.$data['Name'], $html);
}
}
+139
View File
@@ -0,0 +1,139 @@
<?php
namespace app\admin\controller;
use app\common\controller\BaseController;
use app\common\model\Corp;
use app\common\model\User;
use app\common\service\OssService;
use app\common\traits\crud\CrudControllerTrait;
use app\common\service\InvItemService;
use support\Request;
class InvItemController extends BaseController
{
use CrudControllerTrait;
protected $service;
public function __construct()
{
$this->service = new InvItemService();
}
public function preset(Request $request){
try {
$res = $this->service->getList();
$result = [];
foreach ($res as $value) {
$value['company_name'] = Corp::where('id', $value['company_id'])->value('name');
$value['manage_user_name'] = User::where('id', $value['manage_user_id'])->value('name');
$result[] = $value;
}
return $this->success($result, '获取物品列表');
} catch (\Throwable $e) {
return $this->error($e->getMessage());
}
}
/**
* 列表页数据格式化
*
* @param array $data
* @return array
*/
protected function indexFormat($data)
{
if (isset($data['created_at']))
$data['created_at'] = date('Y-m-d H:i:s', $data['created_at']);
if (isset($data['updated_at']))
$data['updated_at'] = date('Y-m-d H:i:s', $data['updated_at']);
if (isset($data['company_id']))
$data['company_name'] = Corp::where('id', $data['company_id'])->value('name');
if (isset($data['manage_user_id']))
$data['manage_user_name'] = User::where('id', $data['manage_user_id'])->value('name');
if (!empty($data['qr_code']))
$data['qr_code_pull'] = (new OssService)->generatePrivateUrl($data['qr_code']);;
return $data;
}
/**
* 生成物品二维码
*
* @param Request $request
* @return mixed
*/
public function generateQrCode(Request $request)
{
try {
$id = $request->post('id');
$num = $request->post('num');
if (!$id) {
return $this->error('物品ID不能为空');
}
$item = \app\common\model\InvItem::find($id);
if (!$item) {
return $this->error('物品不存在');
}
// 获取应用访问地址
$appUrl = 'https://'.$request->host();
$qrCodeService = new \app\common\service\QrCodeService();
$content = $qrCodeService->generateItemQrCode($id, $appUrl);
$fileName = 'qr_item_' . $id . '_' . time() . '.png';
$result = $qrCodeService->generateAndUploadQrCode($id, $content, $fileName);
// 更新物品记录
$item->qr_code = $result['path'];
//$item->qr_code_num = !empty($num)?$num:0;
$item->qr_code_url = $result['url'];
$item->save();
return $this->success([
'qr_code_url' => $result['url'],
'content' => $content,
], '二维码生成成功');
} catch (\Throwable $e) {
return $this->error($e->getMessage());
}
}
/**
* 删除物品二维码
*
* @param Request $request
* @return mixed
*/
public function deleteQrCode(Request $request)
{
try {
$id = $request->post('id');
if (!$id) {
return $this->error('物品ID不能为空');
}
$item = \app\common\model\InvItem::find($id);
if (!$item) {
return $this->error('物品不存在');
}
// 删除 OSS 文件
if ($item->qr_code) {
$qrCodeService = new \app\common\service\QrCodeService();
$qrCodeService->deleteQrCode($item->qr_code);
}
// 清空物品记录中的二维码字段
$item->qr_code = '';
//$item->qr_code_num = 0;
$item->qr_code_url = '';
$item->save();
return $this->success([], '二维码删除成功');
} catch (\Throwable $e) {
return $this->error($e->getMessage());
}
}
}
@@ -0,0 +1,180 @@
<?php
namespace app\admin\controller;
use app\common\controller\BaseController;
use app\common\model\InvRequisitionApprove;
use app\common\model\InvRequisitionAttachment;
use app\common\model\InvRequisitionItem;
use app\common\service\OssService;
use app\common\traits\crud\CrudControllerTrait;
use app\admin\service\InvRequisitionService;
use support\Request;
class InvRequisitionController extends BaseController
{
use CrudControllerTrait;
protected $service;
public function __construct()
{
$this->service = new InvRequisitionService();
}
/**
* 审核
*
* @param Request $request
* @return \support\Response
*/
public function approve(Request $request)
{
try {
$data = $this->service->serviceApprove($request);
return $this->success($data);
} catch (\Throwable $e) {
return $this->error($e->getMessage());
}
}
/**
* 获取列表
*
* @param Request $request 请求对象
* @return mixed 返回分页数据及总数
*/
public function index(Request $request)
{
try {
$applicant_id = $request->get('applicant_id');
$uid = $request->user->uid;
$raw = $this->service->serviceList($request);
$data = $this->formatindex($raw,$applicant_id,$uid);
// $data = $this->transformData($raw, method_exists($this, 'indexFormat')
// ? [$this, 'indexFormat']
// : null);
return $this->success($data);
} catch (\Throwable $e) {
return $this->error($e->getMessage());
}
}
public function formatindex($p,$applicant_id,$uid){
$records = $p->getCollection()
->map(fn($item) => $this->indexFormat($item, $applicant_id,$uid))
->toArray();
return [
'records' => $records,
'total' => $p->total(),
'current' => $p->currentPage(),
'size' => $p->perPage(),
];
}
/**
* 列表页数据格式化
*
* @param array $data
* @return array
*/
protected function indexFormat($data, $applicant_id, $uid)
{
if (isset($data['id'])){
//$data['approve'] = InvRequisitionApprove::where('requisition_id', $data['id'])->get();
$data = $data->toArray();
}
if (isset($data['created_at'])){
$data['created_at'] = is_int($data['created_at'])?$data['created_at']:strtotime($data['created_at']);
$data['created_at'] = date('Y-m-d H:i:s', $data['created_at']);
}
if (isset($data['updated_at'])){
$data['updated_at'] = is_int($data['updated_at'])?$data['updated_at']:strtotime($data['updated_at']);
$data['updated_at'] = date('Y-m-d H:i:s', $data['updated_at']);
}
if (isset($data['id'])){
$data['item'] = InvRequisitionItem::where('requisition_id', $data['id'])->get();
}
if($applicant_id != 'self' && isset($data['id'])){ //不是查询自己数据时查询是否已审核
$approve = InvRequisitionApprove::where('requisition_id', $data['id'])
->where('approver_id', $uid)
->first();
$data['action'] = $approve['action']??3;//查询人审核状态:0待审核 1通过 2驳回 3非审核人
}
return $data;
}
/**
* 列表页数据格式化
*
* @param array $data
* @return array
*/
protected function showFormat($data)
{
if (isset($data['id'])){
$data['approve'] = InvRequisitionApprove::where('requisition_id', $data['id'])->orderBy('id')->get()->toArray();
$approve_id = 0;
foreach ($data['approve'] as $key => $value) {
if (isset($value['created_at'])){
$value['created_at'] = is_int($value['created_at'])?$value['created_at']:strtotime($value['created_at']);
$data['approve'][$key]['created_at'] = date('Y-m-d H:i:s', $value['created_at']);
}
if (isset($value['updated_at'])){
$value['updated_at'] = is_int($value['updated_at'])?$value['updated_at']:strtotime($value['updated_at']);
$data['approve'][$key]['updated_at'] = date('Y-m-d H:i:s', $value['updated_at']);
}
$data['approve'][$key]['action_at'] = date('Y-m-d H:i:s', $data['approve'][$key]['action_at']);
$approve_id = $value['id'];
}
$data['approve_id'] = $approve_id;
}
if (isset($data['created_at']))
$data['created_at'] = date('Y-m-d H:i:s', $data['created_at']);
if (isset($data['updated_at']))
$data['updated_at'] = date('Y-m-d H:i:s', $data['updated_at']);
if (isset($data['id'])){
$data['item'] = InvRequisitionItem::where('requisition_id', $data['id'])->get();
$data['attachment'] = InvRequisitionAttachment::where('requisition_id', $data['id'])->get();
$oss = new OssService();
foreach ($data['attachment'] as $key => $value) {
$data['attachment'][$key]['file_path_pull'] = $oss->generatePrivateUrl($value['file_path']);
}
}
return $data;
}
/**
* 管理员代领
*
* @param Request $request
* @return mixed
*/
public function proxyClaim(Request $request)
{
try {
$data = $this->service->proxyClaim($request, $request->post());
return $this->success($data, '代领成功');
} catch (\Throwable $e) {
return $this->error($e->getMessage());
}
}
/**
* 扫码/代领记录查询
*
* @param Request $request
* @return mixed
*/
public function qrClaims(Request $request)
{
try {
$data = $this->service->qrClaimsList($request);
return $this->success($data);
} catch (\Throwable $e) {
return $this->error($e->getMessage());
}
}
}
@@ -0,0 +1,45 @@
<?php
namespace app\admin\controller;
use app\common\controller\BaseController;
use app\common\traits\crud\CrudControllerTrait;
use app\common\service\InvRequisitionReturnService;
use support\Request;
class InvRequisitionReturnController extends BaseController
{
use CrudControllerTrait;
protected $service;
public function __construct()
{
$this->service = new InvRequisitionReturnService();
}
// /**
// * 列表页数据格式化
// *
// * @param array $data
// * @return array
// */
// protected function indexFormat($data)
// {
// if (isset($data['created_at']))
// $data['created_at'] = date('Y-m-d H:i:s', $data['created_at']);
// if (isset($data['updated_at']))
// $data['updated_at'] = date('Y-m-d H:i:s', $data['updated_at']);
// return $data;
// }
public function returnConfirm(Request $request){
try {
// 处理申请逻辑
$result = $this->service->serviceReturnConfirm($request);
return $this->success($result, '申请提交成功');
} catch (\Throwable $e) {
return $this->error($e->getMessage());
}
}
}
@@ -0,0 +1,32 @@
<?php
namespace app\admin\controller;
use app\common\controller\BaseController;
use app\common\service\InvStockLogService;
use support\Request;
class InvStockLogController extends BaseController
{
protected $service;
public function __construct()
{
$this->service = new InvStockLogService();
}
/**
* 库存变化记录列表
*
* @param Request $request
* @return mixed
*/
public function list(Request $request)
{
try {
$data = $this->service->getList($request->get());
return $this->success($data);
} catch (\Throwable $e) {
return $this->error($e->getMessage());
}
}
}
@@ -0,0 +1,47 @@
<?php
namespace app\admin\controller;
use app\common\controller\BaseController;
use app\common\model\Resource;
use app\common\model\Role;
use app\common\model\User;
use app\common\model\UserGroup;
use app\common\traits\crud\CrudControllerTrait;
use app\common\service\UserDashboardRuleService;
use Couchbase\Group;
class UserDashboardRuleController extends BaseController
{
use CrudControllerTrait;
protected $service;
public function __construct()
{
$this->service = new UserDashboardRuleService();
}
/**
* 列表页数据格式化
*
* @param array $data
* @return array
*/
protected function indexFormat($data)
{
if (isset($data['created_at']))
$data['created_at'] = date('Y-m-d H:i:s', $data['created_at']);
if (isset($data['updated_at']))
$data['updated_at'] = date('Y-m-d H:i:s', $data['updated_at']);
if (isset($data['resources_id']))
$data['resources'] = Resource::where(['id' => $data['resources_id']])->first();
if (isset($data['rule_type']) && $data['rule_type'] == 'role'){
$data['related_rt'] = Role::where(['id' => $data['related']])->value('name');
}elseif (isset($data['rule_type']) && $data['rule_type'] == 'user'){
$data['related_rt'] = User::where(['id' => $data['related']])->value('name');
}elseif (isset($data['rule_type']) && $data['rule_type'] == 'group'){
$data['related_rt'] = UserGroup::where(['id' => $data['related']])->value('title');
}
return $data;
}
}
+153 -19
View File
@@ -254,10 +254,13 @@ class BrandSeriesService
// 验证系列是否存在
$series = BrandSeries::where('id', $series_id)->first();
if (!$series) {
throw new \Exception("系列ID {$series_id} 不存在");
throw new \Exception("车辆系列未收录,联系管理员处理");
}
// 获取品牌信息
$brand = Brand::where('id', $series->brand_id)->first();
if (!$brand) {
throw new \Exception("车辆品牌未收录,联系管理员处理");
}
// 保存车型数据
$createdCount = 0;
@@ -325,38 +328,64 @@ class BrandSeriesService
// 执行抓取
$crawlService = new CrawlService();
//懂车帝模糊搜索返回车系地址
//懂车帝模糊搜索返回车系地址|单个车系id抓取
$href = $crawlService->modelSearch($keyword);
$seriesId = $this->extractSeriesId($href);
$seriesId = $this->extractSeriesId($href); //懂车帝车系id
if (empty($seriesId)) {
// if (empty($seriesId)) { //没有抓取到懂车帝系列id时,按关键词为品牌名抓取所有的系列名
// $crawlSeries = $crawlService->brandSeries($keyword);
// }
//按关键词为品牌名抓取所有的系列名
$crawlSeries = $crawlService->brandSeries($keyword);
if (empty($seriesId) && empty($crawlSeries)) {
throw new \Exception('抓取失败,未找到该系列车型');
}
$brandSerieses = [];
if(empty($seriesId) && !empty($crawlSeries) || count($crawlSeries)>1){ //关键词为品牌名时抓取所有车系的懂车帝id
$href = $crawlService->modelSearch($crawlSeries[0]);
$seriesId = $this->extractSeriesId($href); //懂车帝车系id
//抓取品牌与系列
$brandSeries = $crawlService->crawlBrandSeries($seriesId);
foreach ($crawlSeries as $item) {
// $brandSerieses[] = $crawlService->brandSeries($keyword);
$brandSerieses[] = ['brand' => $brandSeries['brand'], 'series' => $item];
}
}elseif (!empty($seriesId) && empty($crawlSeries)){
//抓取品牌与系列
$brandSerieses[] = $crawlService->crawlBrandSeries($seriesId);
}
$result = [];
try {
foreach ($brandSerieses as $brandSeries) {
$item = [];
// 验证品牌是否存在
$brand = Brand::where('brand_name', $brandSeries['brand'])->first();
// 获取品牌信息
$series = BrandSeries::where('series_name', $brandSeries['series'])->first();
$result['brand'] = ['brand_name' => $brandSeries['brand'], 'exists' => $brand?1:0];
$item['brand'] = ['brand_name' => $brandSeries['brand'], 'exists' => $brand ? 1 : 0];
if ($brand) {
$result['brand']['id'] = $brand['id'];
$item['brand']['id'] = $brand['id'];
}
$result['series'] = ['series_name' => $brandSeries['series'], 'exists' => $series?1:0];
$item['series'] = ['series_name' => $brandSeries['series'], 'exists' => $series ? 1 : 0];
if ($series) {
$result['series']['id'] = $series['id'];
if(empty($result['brand']['id'])){
$result['brand']['id'] = $series['brand_id'];
$item['series']['id'] = $series['id'];
if (empty($item['brand']['id'])) {
$item['brand']['id'] = $series['brand_id'];
}
}
try {
//获取车型数据
$result['model'] = $this->crawlSeriesModel($seriesId);
return $result;
$item['model'] = [];
if (count($brandSerieses)<2) {
$item['model'] = $this->crawlSeriesModel($seriesId);
}
$result[] = $item;
}
return ['is_series' => (count($brandSerieses)<2)?1:0, 'data' => $result];
} catch (\Exception $e) {
throw new \Exception($e->getMessage());
}
@@ -369,21 +398,39 @@ class BrandSeriesService
*/
public function crawlKeywordCreate(Request $request)
{
$series_id = $request->input('series_id');
$brand_id = $request->input('brand_id');
$brand_name = $request->input('brand_name');
$brand_code = $request->input('brand_code');
$series_id = $request->input('series_id');
$series_name = $request->input('series_name');
$models = $request->input('models');
if(empty($brand_id)){
if(empty($brand_id) && empty($brand_name)){
throw new \Exception("请选择品牌");
}
if(empty($series_id) && empty($series_name)){
throw new \Exception("请选择车系");
}
// 获取品牌信息
if (empty($brand_id)) {
$brand = Brand::where('id', $brand_id)->where('brand_name', $brand_name)->first();
if (empty($brand)) { //如果没有该车系则创建
$brandService = new \app\admin\service\BrandService();
$brandData = [
'brand_code' => $brandService->generateUniqueCode(),
'brand_name' => $brand_name,
];
$brand = CrudUtil::create(Brand::class, $brandData);
}
$brand_id = $brand->id;
}else{
$brand = Brand::where('id', $brand_id)->first();
}
if (empty($series_id)) {
$series = BrandSeries::where('brand_id', $brand_id)->where('series_name', $series_name)->first();
if (empty($brandseries)) { //如果没有该车系则创建
if (empty($series)) { //如果没有该车系则创建
$seriesService = new \app\admin\service\BrandSeriesService();
$seriesData = [
'brand_id' => $brand_id,
@@ -396,8 +443,6 @@ class BrandSeriesService
}else{
$series = BrandSeries::where('id', $series_id)->first();
}
// 获取品牌信息
$brand = Brand::where('id', $series->brand_id)->first();
// 保存车型数据
$createdCount = 0;
@@ -452,6 +497,91 @@ class BrandSeriesService
return $msg;
}
/**
* 抓取
*/
public function crawl(Request $request): array
{
$keyword = $request->get('keyword');
if(filter_var($keyword, FILTER_VALIDATE_URL)){
return $this->crawlUrl($request);
}else{
return $this->crawlKeyword($request);
}
}
/**
* 抓取车型数据(通过接口)
* @param Request $request
* @return array
* @throws \Exception
*/
public function crawlUrl(Request $request): array
{
//$url = $request->get('url');
$url = $request->get('keyword');
// 执行抓取
$crawlService = new CrawlService();
//懂车帝提取车系id
$seriesId = $this->get_series_from_url($url); //懂车帝车系id
if (empty($seriesId)) {
throw new \Exception('抓取失败,未找到该系列车型');
}
$brandSerieses = [];
//抓取品牌与系列
$brandSeries = $crawlService->crawlBrandSeries($seriesId);
$brandSerieses[] = ['brand' => $brandSeries['brand'], 'series' => $brandSeries['series']];
$result = [];
try {
foreach ($brandSerieses as $brandSeries) {
$item = [];
// 验证品牌是否存在
$brand = Brand::where('brand_name', $brandSeries['brand'])->first();
// 获取品牌信息
$series = BrandSeries::where('series_name', $brandSeries['series'])->first();
$item['brand'] = ['brand_name' => $brandSeries['brand'], 'exists' => $brand ? 1 : 0];
if ($brand) {
$item['brand']['id'] = $brand['id'];
}
$item['series'] = ['series_name' => $brandSeries['series'], 'exists' => $series ? 1 : 0];
if ($series) {
$item['series']['id'] = $series['id'];
if (empty($item['brand']['id'])) {
$item['brand']['id'] = $series['brand_id'];
}
}
//获取车型数据
$item['model'] = [];
if (count($brandSerieses)<2) {
$item['model'] = $this->crawlSeriesModel($seriesId);
}
$result[] = $item;
}
return ['is_series' => (count($brandSerieses)<2)?1:0, 'data' => $result];
} catch (\Exception $e) {
throw new \Exception($e->getMessage());
}
}
public function get_series_from_url($url) {
// 使用正则表达式匹配 "https://www.dongchedi.com/auto/series/{数字}"
$pattern = '/^https:\/\/www\.dongchedi\.com\/auto\/series\/(\d+)$/';
if (preg_match($pattern, $url, $matches)) {
// 提取数字部分作为 series ID
$series_id = $matches[1];
return $series_id; // 返回系列ID
} else {
throw new \Exception("URL格式不匹配");
}
}
/**
* 获取车系下车型数据
* @return array
@@ -489,7 +619,11 @@ class BrandSeriesService
}
$official_price = 0;
if(!isset($model["info"]['official_price'])){
if(str_contains($model["info"]['official_price_str'],'万')){
$official_price = (int)(str_replace("万", "", $model["info"]['official_price_str']) * 10000) ?? 0;
}else{
$official_price = 0;
}
}else{
$official_price = $model["info"]['official_price'] * 10000;
}
+33
View File
@@ -151,4 +151,37 @@ class BrandService
$msg .= (!empty($fail))?',失败' . $fail . '条':'';
return $msg;
}
/**
* 生成唯一的7位颜色编码
* 格式:C + (O/I) + 5位字母数字组合
*
* @param int $type 类型
* @return string 7位唯一编码
*/
public function generateUniqueCode()
{
$chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ';
$maxAttempts = 10; // 防止无限循环
for ($attempt = 0; $attempt < $maxAttempts; $attempt++) {
// 生成后5位随机字符
$suffix = '';
for ($i = 0; $i < 8; $i++) {
$suffix .= $chars[rand(0, strlen($chars) - 1)];
}
$code = $suffix;
// 检查是否已存在
$existing = Brand::where('brand_code', $code)->first();
if (!$existing) {
return $code;
}
}
// 如果1000次尝试后仍然重复,使用时间戳确保唯一性
$timestamp = substr(time(), -6);
return $suffix . $timestamp;
}
}
+120
View File
@@ -0,0 +1,120 @@
<?php
namespace app\admin\service;
use app\common\validate\InvRequisitionValidate;
use app\common\traits\crud\CrudServiceTrait;
use app\common\model\InvRequisition;
use support\Request;
class InvRequisitionService
{
use CrudServiceTrait;
/**
* 指定当前要操作的模型
*/
protected string $modelClass = InvRequisition::class;
/**
* 验证器
*/
protected $validator = InvRequisitionValidate::class;
/**
* 获取验证器
*/
public function getValidator()
{
return new $this->validator;
}
/**
* 构造 CrudUtil 查询配置
*/
protected function buildQueryConfig(Request $request, string $scene = 'default'): array
{
$p = $request->all();
$config = [];
$config['model'] = $this->modelClass;
switch ($scene) {
case 'default':
//$config['where']['status'] = 1;
$config['order']['id'] = 'desc';
break;
case 'index':
if(isset($p['status']) && $p['status']){
$config['where']['status'] = $p['status'];
}
if (isset($p['applicant_id']) && $p['applicant_id'] == 'self') { //自己的领用申请
$config['where']['applicant_id'] = $request->user->uid;
} elseif (isset($p['applicant_id']) && $p['applicant_id']) { //指定人员的申请
$config['where']['applicant_id'] = $p['applicant_id'];
} else { //审批人对应的记录
$config['where']['approval.approver_id'] = $request->user->uid;
$config['where']['approval.action'] = 0;
}
if(isset($p['phase']) && $p['phase'] == 'approve'){//借用审批中
$config['where']['status'] = ['in',[10,11,13]];
}elseif(isset($p['phase']) && $p['phase'] == 'repaid'){//已全部归还
$config['where']['status'] = 17;
}elseif(isset($p['phase']) && $p['phase'] == 'unreturned'){ //未归还
$config['where']['status'] = ['in',[12,14,15,16,18]];
}
$config['order']['id'] = 'desc';
break;
case 'show':
$config['where']['id'] = $p['id'];
//$config['where']['status'] = 1;
break;
case 'store':
break;
case 'update':
$config['where']['id'] = $p['id'];
break;
case 'destroy':
$config['where']['id'] = $p['id'];
break;
}
return $config;
}
/**
* 审批
* @param Request $request
* @return mixed|string[]|null
*/
public function serviceApprove(Request $request)
{
$param = $request->all();
$attachment = $param['attachment']??[];
//$appro = new \app\common\utils\approval\service\ApprovalFlowService();
$appro = new \app\common\service\InvRequisitionApproveService();
//requisition_id:申请id
//approve_id:审核步骤id
//action:审批动作:1通过 2驳回
//comment:审批意见
return $appro->approve($param['requisition_id'], $param['approve_id'], $request->user->uid, $param['action'], $param['comment'], $attachment);
}
/**
* 管理员代领
* 调用 common 服务层方法
*/
public function proxyClaim(Request $request, array $data): array
{
return (new \app\common\service\InvRequisitionService())->proxyClaim($request, $data);
}
/**
* 扫码/代领记录查询
* 调用 common 服务层方法
*/
public function qrClaimsList(Request $request): array
{
return (new \app\common\service\InvRequisitionService())->qrClaimsList($request);
}
}
+32
View File
@@ -0,0 +1,32 @@
<?php
namespace app\command;
use app\common\model\User;
use app\common\model\UserSn;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Helper\Table;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use support\Db;
use app\common\utils\email\Emails;
/**
* 批量处理用户编号(也用于调试)
*/
class FinanceRecoCommand extends Command
{
protected static $defaultName = 'finance-reco';
protected static $defaultDescription = '';
protected function execute(InputInterface $input, OutputInterface $output): int
{
// $tmp = (new \app\common\service\OssService)->generatePrivateUrl('fdesk_test/20251104/20251104_财务.xlsx');;
$service = new \app\common\service\FinanceReconDubaiFilesService();
$result = $service->dImport('fdesk_test/20251104/20251104_迪拜.xlsx');
return self::SUCCESS;
}
}
+161
View File
@@ -0,0 +1,161 @@
<?php
namespace app\command;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use support\Db;
use support\Redis;
use support\exception\BusinessException;
use Webman\RedisQueue\Redis as RedisQueueRedis;
/**
* 轮询分配 inquiry_log 里的询盘给业务员,并推送到 CRM 队列
*/
class InquiryAssignInquiryCommand extends Command
{
protected static $defaultName = 'iq-assign';
protected static $defaultDescription = '轮询分配询盘给业务员,并推送到 CRM 队列';
/**
* 参与轮询的业务员 ID 列表(请按你想要的顺序填写)
* 比如:销售1=186,销售2=233...
*/
protected const SALES_USER_IDS = [
63,
64,
67,
93,
108,
132,
143,
145,
157,
160,
168,
172,
173,
191,
193,
194,
195,
196,
197
];
/**
* 轮询分配的分组 key(同一分组共用一个轮询指针)
*/
protected const ROUND_ROBIN_GROUP_KEY = 'fb_inquiry';
/**
* 每次脚本最多分配多少条
*/
protected const BATCH_LIMIT = 300;
protected function execute(InputInterface $input, OutputInterface $output): int
{
// 业务员列表检查
$userIds = array_values(array_filter(
array_map('intval', static::SALES_USER_IDS),
fn($v) => $v > 0
));
if (empty($userIds)) {
throw new BusinessException('SALES_USER_IDS 业务员列表不能为空,请先在命令中配置.');
}
// 取出“待分配”的询盘(你可以根据自己系统调整条件)
// 当前策略:status = 0 且 recipient = 0 的视为未分配
$inquiries = Db::table('inquiry_log')
->where('enter_user_id', 186)
->where('channel', 10)
->where('status', 0)
->where('recipient', 0)
->where('id', '>', 34847)
->orderBy('id')
->limit(static::BATCH_LIMIT)
->get();
if ($inquiries->isEmpty()) {
$output->writeln('<comment>没有需要分配的询盘(status=0 且 recipient=0)</comment>');
return self::SUCCESS;
}
$now = time();
$count = 0;
foreach ($inquiries as $row) {
$inquiryId = (int)$row->id;
sleep(1);
// 轮询取出一个业务员 ID
$recipientUserId = $this->nextRecipientId($userIds, static::ROUND_ROBIN_GROUP_KEY);
// 对每一条记录单独事务,避免单条异常拖垮全部
Db::transaction(function () use ($inquiryId, $recipientUserId, $now, $output, &$count) {
// 1)更新 inquiry_log
$affected = Db::table('inquiry_log')
->where('id', $inquiryId)
->update([
'recipient' => $recipientUserId,
'status' => 2,
'r_time' => $now,
't_time' => $now,
'updated_at' => $now,
]);
if ($affected === 0) {
// 被其他进程抢先更新也有可能,这里只打印提示不中断整个批次
$output->writeln("<comment>询盘 {$inquiryId} 未更新(可能已被处理)</comment>");
return;
}
// 2)构造队列 payload(后续 CRM 消费时至少知道 ID)
$res = [
'id' => $inquiryId,
'recipient' => $recipientUserId,
'dispatched_at' => $now,
'source' => 'fb_leads_inquiry',
];
// 3)推送到 CRM 队列
RedisQueueRedis::send('inquiry-to-crm-new', $res);
$count++;
$output->writeln("<info>已分配询盘 {$inquiryId} 给业务员 {$recipientUserId},并推送到队列</info>");
});
}
$output->writeln("<info>本次共分配成功 {$count} 条询盘</info>");
return self::SUCCESS;
}
/**
* 轮询获取“下一位业务员 ID”
*
* @param int[] $userIds
* @param string $groupKey
* @return int
* @throws BusinessException
*/
protected function nextRecipientId(array $userIds, string $groupKey = 'default'): int
{
$userIds = array_values(array_filter(array_map('intval', $userIds), fn($v) => $v > 0));
if (empty($userIds)) {
throw new BusinessException('轮询分配失败:业务员列表为空');
}
$count = count($userIds);
$key = 'inquiry:assign:round_robin:' . $groupKey;
// Redis INCR 是原子操作,适合并发下使用
$val = Redis::incr($key); // 第一次会从 0 变成 1
// 换算为从 0 开始的下标
$index = ($val - 1) % $count;
return $userIds[$index];
}
}
+164
View File
@@ -0,0 +1,164 @@
<?php
namespace app\command;
use app\inquiry\utils\PhoneCountryHelper;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use support\Db;
/**
* 将 fb_leads 数据清洗后导入 inquiry_log
*/
class InquiryFbLeadCommand extends Command
{
protected static $defaultName = 'iq-fb-to';
protected static $defaultDescription = '将 fb_leads 数据清洗后导入 inquiry_log';
// 这些常量根据你实际业务改
protected const DEFAULT_ENTER_USER_ID = 186;
protected const DEFAULT_CHANNEL = 10;
protected function execute(InputInterface $input, OutputInterface $output): int
{
// 只取未同步的记录
$leads = Db::table('fb_leads')
->where('sync_status', 0)
->get();
if ($leads->isEmpty()) {
$output->writeln('<comment>没有需要同步的 fb_leads(sync_status = 0)</comment>');
return self::SUCCESS;
}
$now = time();
Db::transaction(function () use ($leads, $now, $output) {
$insert = [];
$ids = [];
foreach ($leads as $lead) {
$leadArr = (array)$lead;
$insert[] = $this->mapLeadToInquiry($leadArr, $now);
// 假设 fb_leads 的主键字段名为 id
$ids[] = $leadArr['id'];
}
if ($insert) {
// 1) 写入 inquiry_log
Db::table('inquiry_log')->insert($insert);
// 2) 回写 fb_leads 的同步状态
Db::table('fb_leads')
->whereIn('id', $ids)
->update([
'sync_status' => 1,
'updated_at' => $now,
]);
$output->writeln('<info>已导入 ' . count($insert) . ' 条数据到 inquiry_log,并更新 fb_leads.sync_status = 1</info>');
}
});
return self::SUCCESS;
}
/**
* 将一条 fb_leads 记录映射为 inquiry_log 记录
*/
protected function mapLeadToInquiry(array $lead, int $now): array
{
// 联系方式
$email = (string)($lead['email'] ?? '');
$tel = (string)($lead['phone'] ?? '');
$wa = $tel; // 先用电话号码当 WhatsApp
// 根据电话或 WhatsApp 区号识别国家
$countryId = PhoneCountryHelper::detectCountryId($tel ?: $wa);
// 所有默认值按你的要求设置
$data = [
'order_no' => $this->orderNum(),
'enter_user_id' => static::DEFAULT_ENTER_USER_ID,
'recipient' => 0,
'quality' => 2,
'channel' => static::DEFAULT_CHANNEL,
'email' => $email,
'tel' => $tel,
'whatsapp' => $wa,
'country' => $countryId ?: 0,
'contacts' => (string)($lead['full_name'] ?? ''),
'content' => $this->buildContentHtml($lead),
'status' => 0,
'r_time' => 0,
't_time' => 0,
'sync_crm' => 0,
'created_at' => $now,
'updated_at' => $now,
];
return $data;
}
/**
* 生成订单号
*/
protected function orderNum(): string
{
return 'SN' . date('Ymd') . str_pad(mt_rand(1, 99999), 5, '0', STR_PAD_LEFT);
}
/**
* 根据 fb_leads 的字段拼出富文本内容
*/
protected function buildContentHtml(array $lead): string
{
$e = function ($v) {
return htmlspecialchars((string)$v, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
};
$lines = [];
if (!empty($lead['car_model'])) {
$lines[] = '<p><strong>意向车型:</strong>' . $e($lead['car_model']) . '</p>';
}
if (!empty($lead['car_quantity']) || !empty($lead['car_quantity_text'])) {
$qty = $lead['car_quantity'] ?? $lead['car_quantity_text'] ?? '';
$lines[] = '<p><strong>意向数量:</strong>' . $e($qty) . '</p>';
}
if (!empty($lead['favorite_color'])) {
$lines[] = '<p><strong>意向颜色:</strong>' . $e($lead['favorite_color']) . '</p>';
}
if (!empty($lead['usage_type'])) {
$lines[] = '<p><strong>用途:</strong>' . $e($lead['usage_type']) . '</p>';
}
if (!empty($lead['seat_preference'])) {
$lines[] = '<p><strong>座位数偏好:</strong>' . $e($lead['seat_preference']) . '</p>';
}
$lines[] = '<hr />';
$lines[] = '<p><strong>广告与表单信息:</strong></p>';
$lines[] = '<p>平台:' . $e($lead['platform'] ?? '') . '</p>';
$lines[] = '<p>Campaign:' . $e($lead['campaign_name'] ?? '') . ' (#' . $e($lead['campaign_id'] ?? '') . ')</p>';
$lines[] = '<p>Adset:' . $e($lead['adset_name'] ?? '') . ' (#' . $e($lead['adset_id'] ?? '') . ')</p>';
$lines[] = '<p>Ad:' . $e($lead['ad_name'] ?? '') . ' (#' . $e($lead['ad_id'] ?? '') . ')</p>';
$lines[] = '<p>Form:' . $e($lead['form_name'] ?? '') . ' (#' . $e($lead['form_id'] ?? '') . ')</p>';
$lines[] = '<hr />';
$lines[] = '<p><strong>Meta 信息:</strong></p>';
$lines[] = '<p>Lead ID:' . $e($lead['fb_lead_id'] ?? '') . '</p>';
$lines[] = '<p>FB 创建时间原始值:' . $e($lead['fb_created_time'] ?? '') . '</p>';
if (!empty($lead['inbox_url'])) {
$url = $e($lead['inbox_url']);
$lines[] = '<p>Inbox 链接:<a href="' . $url . '" target="_blank">' . $url . '</a></p>';
}
return implode("\n", $lines);
}
}
+259
View File
@@ -0,0 +1,259 @@
<?php
namespace app\command;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use support\Db;
use PhpOffice\PhpSpreadsheet\IOFactory;
use app\inquiry\utils\FbLeadHeaderMapper;
use support\exception\BusinessException;
/**
* 从Excel文件导入fb_leads
*/
class InquiryImportFbCommand extends Command
{
protected static $defaultName = 'iq-fb';
protected static $defaultDescription = '导入 Facebook Leads Excel 数据';
protected function configure(): void
{
$this
->addOption(
'file',
null,
InputOption::VALUE_OPTIONAL,
'要导入的单个 Excel 文件名(相对 public/excel 的路径,或绝对路径)'
)
->addOption(
'dir',
null,
InputOption::VALUE_OPTIONAL,
'Excel 目录(相对 public/ 的路径)',
'excel' // 默认就是 public/excel
);
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$fileOption = $input->getOption('file');
$dirOption = $input->getOption('dir') ?? 'excel';
// 基准目录:public/{$dirOption}
$baseDir = rtrim(public_path($dirOption), DIRECTORY_SEPARATOR);
if ($fileOption) {
// 如果传的是绝对路径,直接用;否则认为是相对 baseDir 的文件名
if ($this->isAbsolutePath($fileOption)) {
$filePath = $fileOption;
} else {
$filePath = $baseDir . DIRECTORY_SEPARATOR . $fileOption;
}
$output->writeln("<info>开始导入单个文件:</info> {$filePath}");
$this->importOneFile($filePath, $output);
} else {
// 不指定 file 时,默认导入目录下所有 .xlsx
if (!is_dir($baseDir)) {
throw new BusinessException("目录不存在:{$baseDir}");
}
$pattern = $baseDir . DIRECTORY_SEPARATOR . '*.xlsx';
$files = glob($pattern);
if (empty($files)) {
$output->writeln("<comment>目录 {$baseDir} 下没有找到 .xlsx 文件</comment>");
return self::SUCCESS;
}
$output->writeln("<info>开始批量导入目录:</info> {$baseDir}");
foreach ($files as $filePath) {
$output->writeln(" -> 文件:{$filePath}");
try {
$this->importOneFile($filePath, $output);
$output->writeln(" <info>导入成功</info>");
} catch (\Throwable $e) {
// 某个文件失败,不影响后面的文件
$output->writeln(" <error>导入失败:{$e->getMessage()}</error>");
}
}
}
return self::SUCCESS;
}
/**
* 导入单个 Excel 文件
*/
protected function importOneFile(string $filePath, OutputInterface $output): void
{
if (!is_file($filePath)) {
throw new BusinessException("文件不存在:{$filePath}");
}
$spreadsheet = IOFactory::load($filePath);
$sheet = $spreadsheet->getActiveSheet();
$rows = $sheet->toArray(null, true, true, true);
if (empty($rows)) {
throw new BusinessException('Excel 内容为空:' . $filePath);
}
// 取第一行表头
$headerRow = array_shift($rows);
// 表头映射器
$mapper = new FbLeadHeaderMapper();
// 如果有未知表头,这里会直接抛异常中断当前文件
$colToField = $mapper->buildColumnFieldMap($headerRow);
$insertData = [];
$now = time();
foreach ($rows as $row) {
$item = [];
foreach ($colToField as $colIndex => $field) {
$raw = $row[$colIndex] ?? null;
switch ($field) {
// 1) is_organic: Excel 里是 true/false,这里转成 0/1
case 'is_organic':
$item['is_organic'] = $this->normalizeIsOrganic($raw);
break;
// 2) fb_created_time: 统一转成 Y-m-d H:i:s
case 'fb_created_time':
$item['fb_created_time'] = $this->normalizeFbCreatedTime($raw);
break;
default:
if (is_string($raw)) {
$raw = trim($raw);
}
$item[$field] = $raw;
break;
}
}
// 补充公共字段
$item['source_file'] = basename($filePath);
$item['created_at'] = $now;
$item['updated_at'] = $now;
// 整行都空就跳过
$allNull = true;
foreach ($item as $v) {
if ($v !== null && $v !== '') {
$allNull = false;
break;
}
}
if ($allNull) {
continue;
}
$insertData[] = $item;
}
if ($insertData) {
Db::table('fb_leads')->insert($insertData);
} else {
$output->writeln(" <comment>文件 {$filePath} 没有有效数据行</comment>");
}
}
/**
* 将 Excel 里的 is_organic 值转换为 0/1
*/
protected function normalizeIsOrganic($value): int
{
if ($value === null) {
return 0;
}
$v = strtolower(trim((string)$value));
// 认为这些是 “true”
$trueValues = ['1', 'true', 'yes', 'y'];
return in_array($v, $trueValues, true) ? 1 : 0;
}
/**
* 统一处理 fb_created_time
* Excel 目前是类似 '12/8/25',这里尽量转成 'Y-m-d H:i:s'
* 解析失败时返回 null
*/
protected function normalizeFbCreatedTime($value): ?string
{
if ($value === null || $value === '') {
return null;
}
if ($value instanceof \DateTimeInterface) {
return $value->format('Y-m-d H:i:s');
}
$v = trim((string)$value);
$formats = [
'Y-m-d H:i:s',
'Y-m-d',
'n/j/y', // 12/8/25
'n/j/Y',
'd/m/Y',
'd.m.Y',
];
foreach ($formats as $fmt) {
$dt = \DateTime::createFromFormat($fmt, $v);
if ($dt !== false) {
return $dt->format('Y-m-d H:i:s');
}
}
// 纯数字当时间戳试一下
if (is_numeric($v) && strlen($v) >= 9 && strlen($v) <= 11) {
$ts = (int)$v;
if ($ts > 0) {
return date('Y-m-d H:i:s', $ts);
}
}
return null;
}
/**
* 判断是否为绝对路径(简单兼容 Windows / Linux)
*/
protected function isAbsolutePath(string $path): bool
{
// Windows: X:\ 或 \\server\share
if (preg_match('/^[a-zA-Z]:\\\\/', $path) || str_starts_with($path, '\\\\')) {
return true;
}
// *nix: / 开头
return str_starts_with($path, '/');
}
}
/*
php webman tb-db
# 等价于 php webman tb-db --dir=excel
# 会遍历 public/excel/*.xlsx 逐个导入
# 相对路径(相对 public/excel)
php webman tb-db --file="BYD tang_Leads_2025-12-04_2025-12-09.xlsx"
# 或给绝对路径
php webman tb-db --file="/www/wwwroot/xxx/public/excel/BYD tang_Leads_2025-12-04_2025-12-09.xlsx"
*/
+43
View File
@@ -0,0 +1,43 @@
<?php
namespace app\command;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Helper\Table;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use support\Db;
use Webman\RedisQueue\Redis as RedisQueueRedis;
/**
* 询盘补发
*/
class InquiryReissueCommand extends Command
{
protected static $defaultName = 'iq-bf';
protected static $defaultDescription = '';
protected function execute(InputInterface $input, OutputInterface $output): int
{
$inquiries = Db::table('inquiry_log')
->where('sync_crm', 0)
->where('id', '>', 33785)
->orderBy('id')
->get();
foreach ($inquiries as $row) {
$inquiryId = (int)$row->id;
var_dump('补发ID:'.$inquiryId);
sleep(1);
$res = [
'id' => $inquiryId
];
RedisQueueRedis::send('inquiry-to-crm-new', $res);
}
return self::SUCCESS;
}
}
+29
View File
@@ -0,0 +1,29 @@
<?php
namespace app\command;
use app\common\model\User;
use app\common\model\UserSn;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Helper\Table;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use support\Db;
use app\common\utils\email\Emails;
/**
* 批量处理用户编号(也用于调试)
*/
class VehMoneyCommand extends Command
{
protected static $defaultName = 'veh-money';
protected static $defaultDescription = '';
protected function execute(InputInterface $input, OutputInterface $output): int
{
return self::SUCCESS;
}
}
+18 -1
View File
@@ -4,6 +4,7 @@ namespace app\common\controller;
use app\common\service\AuthService;
use app\common\validate\UserValidate;
use Exception;
use support\Request;
class AuthController extends BaseController
@@ -31,7 +32,8 @@ class AuthController extends BaseController
public function feishuScope(Request $request)
{
try {
$result = (new AuthService())->feishuAuthorize($request->corp_abbr, $request->system_code);
$is_mob = $request->get('is_mob', 0);
$result = (new AuthService())->feishuAuthorize($request->corp_abbr, $request->system_code, $is_mob);
return $this->success($result);
} catch (\Throwable $e) {
return $this->error($e->getMessage());
@@ -112,4 +114,19 @@ class AuthController extends BaseController
// (new AuthService())->blockToken($token, $systemCode);
// return $this->success([], '已强制停用该token');
// }
public function getJsApi(Request $request)
{
try {
$url = $request->get('url');
if(empty($url)){
throw new Exception("url不能为空");
}
$result = (new AuthService())->getJsApiConfig($request->corp_abbr, $request->system_code,$url);
return $this->success($result);
} catch (\Throwable $e) {
return $this->error($e->getMessage());
}
}
}
@@ -101,6 +101,42 @@ class BrandSeriesController extends BaseController
}
}
/**
* URL抓取品牌、车系、车型
* @param Request $request
* @return \support\Response
*/
public function crawlUrl(Request $request){
try {
$result = $this->service->crawlUrl($request);
if($result){
return $this->success($result,'抓取成功');
}else{
return $this->error('抓取任务失败');
}
} catch (\Exception $e) {
return $this->error($e->getMessage());
}
}
/**
* 抓取品牌、车系、车型
* @param Request $request
* @return \support\Response
*/
public function crawl(Request $request){
try {
$result = $this->service->crawl($request);
if($result){
return $this->success($result,'抓取成功');
}else{
return $this->error('抓取任务失败');
}
} catch (\Exception $e) {
return $this->error($e->getMessage());
}
}
/**
* 保存抓取的系列车型
* @param Request $request
@@ -0,0 +1,33 @@
<?php
namespace app\common\controller;
use app\common\controller\BaseController;
use app\common\traits\crud\CrudControllerTrait;
use app\common\service\InvRequisitionApproveService;
class InvRequisitionApproveController extends BaseController
{
use CrudControllerTrait;
protected $service;
public function __construct()
{
$this->service = new InvRequisitionApproveService();
}
/**
* 列表页数据格式化
*
* @param array $data
* @return array
*/
// protected function indexFormat($data)
// {
// if (isset($data['created_at']))
// $data['created_at'] = date('Y-m-d H:i:s', $data['created_at']);
// if (isset($data['updated_at']))
// $data['updated_at'] = date('Y-m-d H:i:s', $data['updated_at']);
// return $data;
// }
}
@@ -0,0 +1,33 @@
<?php
namespace app\common\controller;
use app\common\controller\BaseController;
use app\common\traits\crud\CrudControllerTrait;
use app\common\service\InvRequisitionAttachmentService;
class InvRequisitionAttachmentController extends BaseController
{
use CrudControllerTrait;
protected $service;
public function __construct()
{
$this->service = new InvRequisitionAttachmentService();
}
/**
* 列表页数据格式化
*
* @param array $data
* @return array
*/
// protected function indexFormat($data)
// {
// if (isset($data['created_at']))
// $data['created_at'] = date('Y-m-d H:i:s', $data['created_at']);
// if (isset($data['updated_at']))
// $data['updated_at'] = date('Y-m-d H:i:s', $data['updated_at']);
// return $data;
// }
}
@@ -0,0 +1,33 @@
<?php
namespace app\common\controller;
use app\common\controller\BaseController;
use app\common\traits\crud\CrudControllerTrait;
use app\common\service\InvRequisitionItemService;
class InvRequisitionItemController extends BaseController
{
use CrudControllerTrait;
protected $service;
public function __construct()
{
$this->service = new InvRequisitionItemService();
}
/**
* 列表页数据格式化
*
* @param array $data
* @return array
*/
// protected function indexFormat($data)
// {
// if (isset($data['created_at']))
// $data['created_at'] = date('Y-m-d H:i:s', $data['created_at']);
// if (isset($data['updated_at']))
// $data['updated_at'] = date('Y-m-d H:i:s', $data['updated_at']);
// return $data;
// }
}
@@ -0,0 +1,33 @@
<?php
namespace app\common\controller;
use app\common\controller\BaseController;
use app\common\traits\crud\CrudControllerTrait;
use app\common\service\InvRequisitionItemReturnAttachmentService;
class InvRequisitionItemReturnAttachmentController extends BaseController
{
use CrudControllerTrait;
protected $service;
public function __construct()
{
$this->service = new InvRequisitionItemReturnAttachmentService();
}
/**
* 列表页数据格式化
*
* @param array $data
* @return array
*/
// protected function indexFormat($data)
// {
// if (isset($data['created_at']))
// $data['created_at'] = date('Y-m-d H:i:s', $data['created_at']);
// if (isset($data['updated_at']))
// $data['updated_at'] = date('Y-m-d H:i:s', $data['updated_at']);
// return $data;
// }
}
@@ -0,0 +1,181 @@
<?php
namespace app\common\controller;
use app\common\controller\BaseController;
use app\common\model\InvItem;
use app\common\model\InvRequisitionItem;
use app\common\model\InvRequisitionItemReturn;
use app\common\model\InvRequisitionItemReturnAttachment;
use app\common\service\OssService;
use app\common\traits\crud\CrudControllerTrait;
use app\common\service\InvRequisitionItemReturnService;
use support\Request;
class InvRequisitionItemReturnController extends BaseController
{
use CrudControllerTrait;
protected $service;
public function __construct()
{
$this->service = new InvRequisitionItemReturnService();
}
/**
* 获取列表
*
* @param Request $request 请求对象
* @return mixed 返回分页数据及总数
*/
public function index(Request $request)
{
try {
//$request->setGet('$request');
$raw = $this->service->serviceList($request);
$data = $this->transformData($raw, method_exists($this, 'indexFormat')
? [$this, 'indexFormat']
: null);
return $this->success($data);
} catch (\Throwable $e) {
return $this->error($e->getMessage());
}
}
/**
* 列表页数据格式化
*
* @param array $data
* @return array
*/
protected function indexFormat($data)
{
if(isset($data['id'])){
$data['return_item'] = InvRequisitionItemReturn::where('return_id', $data['id'])->get();
foreach ($data['return_item'] as $key => $value) {
$data['return_item'][$key]['item'] = InvItem::where('id', $value['item_id'])->get();
}
}
if (isset($data['return_at']))
$data['return_at'] = date('Y-m-d H:i:s', $data['return_at']);
if (isset($data['created_at']))
$data['created_at'] = date('Y-m-d H:i:s', $data['created_at']);
if (isset($data['updated_at']))
$data['updated_at'] = date('Y-m-d H:i:s', $data['updated_at']);
return $data;
}
/**
* 获取详情
*
* @param Request $request 请求对象
* @param int|string $id 资源 ID
* @return mixed 返回资源详情数据
*/
public function show(Request $request)
{
try {
$raw = $this->service->serviceShow($request);
$data = $this->transformData($raw, method_exists($this, 'showFormat')
? [$this, 'showFormat']
: null);
return $this->success($data);
} catch (\Throwable $e) {
return $this->error($e->getMessage());
}
}
/**
* 列表页数据格式化
*
* @param array $data
* @return array
*/
protected function showFormat($data)
{
foreach ($data['item_return'] as $key => $item){
$data['item_return'][$key]['requisition_item'] = InvRequisitionItem::where('id', $item['requisition_item_id'])->first();
}
if (isset($data['return_at']))
$data['return_at'] = date('Y-m-d H:i:s', $data['return_at']);
if (isset($data['id'])){
//归还附件
$data['return_attachment'] = InvRequisitionItemReturnAttachment::where('return_id', $data['id'])->get();
$ossservice = new OssService();
foreach ($data['return_attachment'] as $item){
$item['file_path_pull'] = $ossservice->generatePrivateUrl($item['file_path']);
}
//归还审核附件
$data['return_approve_attachment'] = InvRequisitionItemReturnAttachment::where('return_id', $data['id'])->get();
foreach ($data['return_approve_attachment'] as $item){
$item['file_path_pull'] = $ossservice->generatePrivateUrl($item['file_path']);
}
}
/*if (isset($data['created_at']))
$data['created_at'] = date('Y-m-d H:i:s', $data['created_at']);
if (isset($data['updated_at']))
$data['updated_at'] = date('Y-m-d H:i:s', $data['updated_at']);*/
return $data;
}
/**
* 更新
*
* @param Request $request 请求对象
* @return mixed 返回更新成功的数据或信息
*/
public function update(Request $request)
{
try {
$data = $this->validate($this->service->getValidator(), $request->post(), 'update');
$res = $this->service->serviceUpdate($request, $data);
return $this->success([], '更新成功');
} catch (\Throwable $e) {
return $this->error($e->getMessage());
}
}
/**
* 物品归还确认
*
* @param Request $request
* @return mixed
*/
public function returnItemConfirm(Request $request)
{
try {
// 验证申请数据
$data = $this->validate($this->service->getValidator(), $request->post(), 'returnConfirm');
// 处理申请逻辑
$result = $this->service->serviceReturnItemConfirm($request, $data, 'returnConfirm');
return $this->success($result, '申请提交成功');
} catch (\Throwable $e) {
return $this->error($e->getMessage());
}
}
/**
* 物品归还确认
*
* @param Request $request
* @return mixed
*/
public function returnConfirm(Request $request)
{
try {
// 验证申请数据
$data = $this->validate($this->service->getValidator(), $request->post(), 'returnConfirm');
// 处理申请逻辑
$result = $this->service->serviceReturnConfirm($request, $data, 'returnConfirm');
return $this->success($result, '申请提交成功');
} catch (\Throwable $e) {
return $this->error($e->getMessage());
}
}
}
@@ -0,0 +1,33 @@
<?php
namespace app\common\controller;
use app\common\controller\BaseController;
use app\common\traits\crud\CrudControllerTrait;
use app\common\service\InvRequisitionReturnService;
class InvRequisitionReturnController extends BaseController
{
use CrudControllerTrait;
protected $service;
public function __construct()
{
$this->service = new InvRequisitionReturnService();
}
/**
* 列表页数据格式化
*
* @param array $data
* @return array
*/
// protected function indexFormat($data)
// {
// if (isset($data['created_at']))
// $data['created_at'] = date('Y-m-d H:i:s', $data['created_at']);
// if (isset($data['updated_at']))
// $data['updated_at'] = date('Y-m-d H:i:s', $data['updated_at']);
// return $data;
// }
}
+211
View File
@@ -0,0 +1,211 @@
<?php
namespace app\common\controller;
use app\common\traits\ResponseTrait;
use support\Request;
use support\Response;
use app\common\utils\SystemAuth;
use app\common\utils\Jwt;
use app\common\model\User;
use app\common\model\Systems;
use support\Redis;
class SystemController
{
use ResponseTrait;
protected $noNeedLogin = ['auth'];
protected $noNeedHeaderData = ['auth'];
/**
* 系统间认证接口
* POST /api/system/auth
*/
public function auth(Request $request): Response
{
$userId = $request->post('user_id');
$timestamp = (int)$request->post('timestamp');
$signature = $request->post('signature');
$systemCode = $request->post('system_code');
// 参数验证
if (empty($userId) || empty($timestamp) || empty($signature)) {
return $this->error('参数不完整', 400);
}
// 获取配置
$secret = config('system_auth.shared_secret');
$expire = config('system_auth.signature_expire', 600);
// 验证时间戳(10分钟有效期)
if (!SystemAuth::verifyTimestamp($timestamp, $expire)) {
return $this->error('请求已过期', 401);
}
// 验证签名
if (!SystemAuth::verifySignature($userId, $timestamp, $signature, $secret)) {
return $this->error('签名验证失败', 401);
}
// 查找用户
$user = User::find($userId);
if (!$user) {
return $this->error('用户不存在', 404);
}
// 验证系统(如果提供了 system_code)
$system = null;
if ($systemCode) {
$system = Systems::where('code', $systemCode)->first();
if (!$system) {
return $this->error('系统不存在', 404);
}
}
// 获取 corp_abbr(优先从请求参数获取,否则通过用户 corp_id 查询)
$corpAbbr = $request->post('corp_abbr');
if (!$corpAbbr && $user->corp_id) {
$corp = \app\common\model\Corp::find($user->corp_id);
$corpAbbr = $corp ? $corp->abbr : '';
}
// 构建 JWT payload
$payload = $this->buildJwtPayload($user, $system);
// 生成 access_token
$jwt = new Jwt();
$token = $jwt->generateToken($payload);
// 生成 refresh_token
$refreshToken = $this->generateRefreshToken($user->id, $systemCode ?? '');
// 获取 home_url
$homeUrl = $system ? $this->getHome($system->id, $payload) : '';
// 记录 token 到 Redis 和 MySQL
$this->recordUserToken($user->id, $token, $systemCode ?? '');
// 构建用户信息(去除敏感信息)
$userInfo = [
'id' => $user->id,
'sn' => $user->sn ?? '',
'name' => $user->name ?? '',
'avatar' => $user->avatar ?? '',
'phone' => $user->phone ?? '',
'email' => $user->email ?? '',
'sex' => $user->sex ?? '',
'position' => $user->position ?? '',
'status' => $user->status ?? 1,
'corp_id' => $user->corp_id,
];
return $this->success([
'access_token' => $token,
'refresh_token' => $refreshToken,
'expires_in' => config('jwt.jwt_expiration'),
'system_code' => $systemCode ?? '',
'corp_abbr' => $corpAbbr ?? '',
'login_type' => 'system',
'home_url' => $homeUrl,
'user_info' => $userInfo,
], '认证成功');
}
/**
* 构建 JWT payload
*/
private function buildJwtPayload(User $user, ?Systems $system): array
{
$now = time();
$exp = $now + config('jwt.jwt_expiration');
$system_ids = $user->systems ? $user->systems->pluck('id')->toArray() : [];
$role_ids = $user->roles ? $user->roles->pluck('id')->toArray() : [];
$payload = [
'sub' => 'user_' . $user->id,
'aud' => $system->code,
'iat' => $now,
'exp' => $exp,
'nbf' => $now,
'typ' => 'access',
'uid' => $user->id,
'uname' => $user->name,
'role_ids' => $role_ids,
'system_ids' => $system_ids,
// 'user_id' => $user->id,
// 'sn' => $user->sn ?? '',
// 'name' => $user->name ?? '',
];
if ($system) {
$payload['system_code'] = $system->code;
$payload['system_id'] = $system->id;
}
return $payload;
}
/**
* 生成 refresh_token
*/
private function generateRefreshToken(int $userId, string $systemCode): string
{
$refreshToken = bin2hex(random_bytes(32));
// 存储到 Redis
$key = "refresh_token:{$refreshToken}";
$expire = config('jwt.jwt_refresh_token_expiration');
Redis::setex($key, $expire, json_encode([
'user_id' => $userId,
'system_code' => $systemCode,
]));
return $refreshToken;
}
/**
* 获取用户首页地址
*/
private function getHome(int $systemId, array $payload): string
{
// 简单实现,可以根据实际需求调整
return '';
}
/**
* 记录用户token到 Redis 和 MySQL
*/
private function recordUserToken(int $userId, string $token, string $systemCode): void
{
$request = request();
$ip = $request ? $request->getRealIp() : '';
$ua = $request ? $request->header('user-agent', '') : '';
$now = time();
$expireAt = $now + config('jwt.jwt_expiration');
// Redis Set 用户token集合
Redis::sAdd("user_tokens:{$userId}:{$systemCode}", $token);
Redis::expire("user_tokens:{$userId}:{$systemCode}", config('jwt.jwt_expiration'));
// Redis Hash token元信息
$meta = [
'system_code' => $systemCode,
'login_ip' => $ip,
'user_agent' => $ua,
'login_time' => $now,
'expired_at' => $expireAt,
];
Redis::hMSet("user_token_meta:{$token}", $meta);
Redis::expire("user_token_meta:{$token}", config('jwt.jwt_expiration'));
// MySQL 持久化
\app\common\model\UserTokenLog::create([
'user_id' => $userId,
'token' => $token,
'system_code' => $systemCode,
'login_ip' => $ip,
'user_agent' => $ua,
'expired_at' => $expireAt,
]);
}
}
+4 -4
View File
@@ -91,8 +91,8 @@ class UploadController
$mimes = $ossservice->getMimeTypes(['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'txt', 'zip', 'rar']);
$result = Storage::adapter('oss')
->path('x_server/' . $sceneUrl . date('Ymd'))
->size(1024*1024*5)
->path(config('oss.oss_main_dir') . $sceneUrl . date('Ymd'))
->size(1024*1024*50)
->extYes($mimes)
->upload($file);
@@ -132,8 +132,8 @@ class UploadController
}
$result = Storage::adapter('oss')
->path('x_server/' . $sceneUrl . date('Ymd'))
->size(1024*1024*5)
->path(config('oss.oss_main_dir') . $sceneUrl . date('Ymd'))
->size(1024*1024*50)
->extYes(['image/jpeg','image/png'])
->extNo(['image/gif'])
->upload($file);
@@ -0,0 +1,33 @@
<?php
namespace app\common\controller;
use app\common\controller\BaseController;
use app\common\traits\crud\CrudControllerTrait;
use app\common\service\UsedcarCrawlResultsService;
class UsedcarCrawlResultsController extends BaseController
{
use CrudControllerTrait;
protected $service;
public function __construct()
{
$this->service = new UsedcarCrawlResultsService();
}
/**
* 列表页数据格式化
*
* @param array $data
* @return array
*/
// protected function indexFormat($data)
// {
// if (isset($data['created_at']))
// $data['created_at'] = date('Y-m-d H:i:s', $data['created_at']);
// if (isset($data['updated_at']))
// $data['updated_at'] = date('Y-m-d H:i:s', $data['updated_at']);
// return $data;
// }
}
@@ -0,0 +1,33 @@
<?php
namespace app\common\controller;
use app\common\controller\BaseController;
use app\common\traits\crud\CrudControllerTrait;
use app\common\service\UsedcarCrawlResultsImagesService;
class UsedcarCrawlResultsImagesController extends BaseController
{
use CrudControllerTrait;
protected $service;
public function __construct()
{
$this->service = new UsedcarCrawlResultsImagesService();
}
/**
* 列表页数据格式化
*
* @param array $data
* @return array
*/
// protected function indexFormat($data)
// {
// if (isset($data['created_at']))
// $data['created_at'] = date('Y-m-d H:i:s', $data['created_at']);
// if (isset($data['updated_at']))
// $data['updated_at'] = date('Y-m-d H:i:s', $data['updated_at']);
// return $data;
// }
}
@@ -47,6 +47,7 @@ class VehVehicleSkuController extends BaseController
public function store(Request $request)
{
return $this->error('请优先抓取车辆信息,如抓取不到相应车型请联系管理员,暂停手动录入车辆信息');
try {
$data = $this->validate($this->service->getValidator(), $request->post(), 'store');
$res = $this->service->serviceStore($request, $data);
+31 -15
View File
@@ -196,7 +196,15 @@ class AuthLogic
{
$certificate = $this->feishuAppInfo($data['corp_abbr'], $data['system_code']);
$feishu = new Feishu($certificate['app_id'], $certificate['app_secret'], $certificate['abbr']);
$userAccessToken = $feishu->userAccessToken($data['feishu_code'], $certificate['r_url']);
//判断移动端或pc端回调地址
if(!empty($data['is_mob']) && $data['is_mob'] == 1){
$redirect_uri = $certificate['m_url'];
}else{
$redirect_uri = $certificate['r_url'];
}
$userAccessToken = $feishu->userAccessToken($data['feishu_code'], $redirect_uri);
$userInfo = $feishu->userInfo($userAccessToken['access_token']);
if ($userInfo['code'] != 0) {
@@ -298,6 +306,7 @@ class AuthLogic
'app_id' => $fsAppInfo->app_id,
'app_secret' => $fsAppInfo->app_secret,
'r_url' => $fsAppInfo->r_url,
'm_url' => $fsAppInfo->m_url,
'scope' => $fsAppInfo->scopes,
'state' => $fsAppInfo->state,
'abbr' => $corpAbbr,
@@ -539,23 +548,30 @@ class AuthLogic
public function getHome($systemId, $data)
{
// 2025.10.30 临时处理 ====== start
if ($systemId == 1) {
if (in_array(1, $data['role_ids'])) {
return '/dashboard/console';
}
if (count($data['role_ids']) == 1 && in_array(5, $data['role_ids'])) {
return '/dashboard/console/backoffice';
}
if (count($data['role_ids']) == 1 && in_array(6, $data['role_ids'])) {
return '/dashboard/console/finance';
}
}
// 2025.10.30 临时处理 ====== end
// // 2025.10.30 临时处理 ====== start
// if ($systemId == 1) {
// if (in_array(1, $data['role_ids'])) {
// return '/dashboard/console';
// }
// if (count($data['role_ids']) == 1 && in_array(5, $data['role_ids'])) {
// return '/dashboard/console/backoffice';
// }
// if (count($data['role_ids']) == 1 && in_array(6, $data['role_ids'])) {
// return '/dashboard/console/finance';
// }
// }
// // 2025.10.30 临时处理 ====== end
//
// $info = Resource::where('system_id', $systemId)
// ->where('is_home', 1)
// ->first();
$resources_id = (new UserDashboardRuleLogic)->getUserDashboard($data['uid']);
if(!empty($resources_id)){
$info = Resource::where('system_id', $systemId)
->where('is_home', 1)
->where('id', $resources_id)
->first();
}
if (!$info) {
return '/dashboard/console';
@@ -0,0 +1,17 @@
<?php
namespace app\common\logic;
use app\common\traits\crud\CrudLogicTrait;
use app\common\model\FinanceReconDubaiFiles;
class FinanceReconDubaiFilesLogic
{
use CrudLogicTrait;
/**
* 指定当前要操作的模型
*/
protected string $modelClass = FinanceReconDubaiFiles::class;
}
@@ -0,0 +1,17 @@
<?php
namespace app\common\logic;
use app\common\traits\crud\CrudLogicTrait;
use app\common\model\InquiryTransmitConfig;
class InquiryTransmitConfigLogic
{
use CrudLogicTrait;
/**
* 指定当前要操作的模型
*/
protected string $modelClass = InquiryTransmitConfig::class;
}
+17
View File
@@ -0,0 +1,17 @@
<?php
namespace app\common\logic;
use app\common\traits\crud\CrudLogicTrait;
use app\common\model\InquiryTransmit;
class InquiryTransmitLogic
{
use CrudLogicTrait;
/**
* 指定当前要操作的模型
*/
protected string $modelClass = InquiryTransmit::class;
}
+22
View File
@@ -0,0 +1,22 @@
<?php
namespace app\common\logic;
use app\common\traits\crud\CrudLogicTrait;
use app\common\model\InvItem;
class InvItemLogic
{
use CrudLogicTrait;
/**
* 指定当前要操作的模型
*/
protected string $modelClass = InvItem::class;
public function getList()
{
return InvItem::where('status',1)->orderBy('sort','desc')->get();
}
}
@@ -0,0 +1,17 @@
<?php
namespace app\common\logic;
use app\common\traits\crud\CrudLogicTrait;
use app\common\model\InvRequisitionApprove;
class InvRequisitionApproveLogic
{
use CrudLogicTrait;
/**
* 指定当前要操作的模型
*/
protected string $modelClass = InvRequisitionApprove::class;
}
@@ -0,0 +1,17 @@
<?php
namespace app\common\logic;
use app\common\traits\crud\CrudLogicTrait;
use app\common\model\InvRequisitionAttachment;
class InvRequisitionAttachmentLogic
{
use CrudLogicTrait;
/**
* 指定当前要操作的模型
*/
protected string $modelClass = InvRequisitionAttachment::class;
}
@@ -0,0 +1,17 @@
<?php
namespace app\common\logic;
use app\common\traits\crud\CrudLogicTrait;
use app\common\model\InvRequisitionItem;
class InvRequisitionItemLogic
{
use CrudLogicTrait;
/**
* 指定当前要操作的模型
*/
protected string $modelClass = InvRequisitionItem::class;
}
@@ -0,0 +1,17 @@
<?php
namespace app\common\logic;
use app\common\traits\crud\CrudLogicTrait;
use app\common\model\InvRequisitionItemReturnAttachment;
class InvRequisitionItemReturnAttachmentLogic
{
use CrudLogicTrait;
/**
* 指定当前要操作的模型
*/
protected string $modelClass = InvRequisitionItemReturnAttachment::class;
}
@@ -0,0 +1,17 @@
<?php
namespace app\common\logic;
use app\common\traits\crud\CrudLogicTrait;
use app\common\model\InvRequisitionItemReturn;
class InvRequisitionItemReturnLogic
{
use CrudLogicTrait;
/**
* 指定当前要操作的模型
*/
protected string $modelClass = InvRequisitionItemReturn::class;
}
+17
View File
@@ -0,0 +1,17 @@
<?php
namespace app\common\logic;
use app\common\traits\crud\CrudLogicTrait;
use app\common\model\InvRequisition;
class InvRequisitionLogic
{
use CrudLogicTrait;
/**
* 指定当前要操作的模型
*/
protected string $modelClass = InvRequisition::class;
}
@@ -0,0 +1,17 @@
<?php
namespace app\common\logic;
use app\common\traits\crud\CrudLogicTrait;
use app\common\model\InvRequisitionReturn;
class InvRequisitionReturnLogic
{
use CrudLogicTrait;
/**
* 指定当前要操作的模型
*/
protected string $modelClass = InvRequisitionReturn::class;
}
+7
View File
@@ -23,6 +23,13 @@ class PermLogic
}
$all = collect();
//获取仪表盘
$resources_id = (new UserDashboardRuleLogic)->getUserDashboard($userId);
if(!empty($resources_id)){
$resources = Resource::where('id', $resources_id)->get();
$all = $all->merge($resources);
}
// 遍历用户的所有角色,合并每个角色所拥有的资源
foreach ($user->roles as $role) {
$all = $all->merge(
@@ -0,0 +1,17 @@
<?php
namespace app\common\logic;
use app\common\traits\crud\CrudLogicTrait;
use app\common\model\UsedcarCrawlResultsImages;
class UsedcarCrawlResultsImagesLogic
{
use CrudLogicTrait;
/**
* 指定当前要操作的模型
*/
protected string $modelClass = UsedcarCrawlResultsImages::class;
}
@@ -0,0 +1,17 @@
<?php
namespace app\common\logic;
use app\common\traits\crud\CrudLogicTrait;
use app\common\model\UsedcarCrawlResults;
class UsedcarCrawlResultsLogic
{
use CrudLogicTrait;
/**
* 指定当前要操作的模型
*/
protected string $modelClass = UsedcarCrawlResults::class;
}
@@ -0,0 +1,47 @@
<?php
namespace app\common\logic;
use app\common\model\User;
use app\common\model\UserGroup;
use app\common\model\UserGroupMembers;
use app\common\traits\crud\CrudLogicTrait;
use app\common\model\UserDashboardRule;
class UserDashboardRuleLogic
{
use CrudLogicTrait;
/**
* 指定当前要操作的模型
*/
protected string $modelClass = UserDashboardRule::class;
public function getUserDashboard($userId)
{
$user = User::with('reportToInfo')->find($userId);;
//获取用户所在分组
$ugm = UserGroupMembers::where('user_id', $userId)->where('status', 1)->get();
$group_id = isset($ugm->group_id)?$ugm->group_id:0;
$roleRule = UserDashboardRule::orWhere(function ($query) use ($user) {
$query->whereIn('related', $user->roles->pluck('id'))
->where('rule_type', 'role');
})
->orWhere(function ($query) use ($userId) {
$query->where('related', $userId)
->where('rule_type', 'user');
})
->orWhere(function ($query) use ($group_id) {
$query->where('related', $group_id)
->where('rule_type', 'group');
})
->orderBy('weight','desc')
->orderBy('rule_type','desc')
->first();
$resources_id = $roleRule?$roleRule->resources_id:null;
return $resources_id;
}
}
+1 -1
View File
@@ -40,7 +40,7 @@ class CorpFeishuApp extends Model
/**
* 允许自动填充模型字段
*/
protected $fillable = ['id', 'corp_id', 'system_id', 'name', 'tenant_key', 'app_id', 'app_secret', 'base_uri', 'r_url', 'scopes', 'state', 'sort', 'status', 'created_at', 'updated_at'];
protected $fillable = ['id', 'corp_id', 'system_id', 'name', 'tenant_key', 'app_id', 'app_secret', 'base_uri', 'm_url', 'r_url', 'scopes', 'state', 'sort', 'status', 'created_at', 'updated_at'];
/**
* 不允许自动填充模型字段
+49
View File
@@ -0,0 +1,49 @@
<?php
namespace app\common\model;
use support\Model;
class EmailMailbox extends Model
{
/**
* The connection name for the model.
*
* @var string|null
*/
protected $connection = 'mysql';
/**
* The table associated with the model.
*
* @var string
*/
protected $table = 'email_mailbox';
/**
* The primary key associated with the table.
*
* @var string
*/
protected $primaryKey = 'id';
/**
* Indicates if the model should be timestamped.
*
* @var bool
*/
public $timestamps = false;
protected $dateFormat = 'U';
/**
* 允许自动填充模型字段
*/
protected $fillable = ['id', 'channel_id', 'corp_id', 'email', 'imap_host', 'imap_port', 'imap_flags', 'mailbox', 'username', 'password', 'status', 'last_uid', 'last_fetch_at', 'remark'];
/**
* 不允许自动填充模型字段
*/
protected $guarded = [];
}
+49
View File
@@ -0,0 +1,49 @@
<?php
namespace app\common\model;
use support\Model;
class EmailPull extends Model
{
/**
* The connection name for the model.
*
* @var string|null
*/
protected $connection = 'mysql';
/**
* The table associated with the model.
*
* @var string
*/
protected $table = 'email_pull';
/**
* The primary key associated with the table.
*
* @var string
*/
protected $primaryKey = 'id';
/**
* Indicates if the model should be timestamped.
*
* @var bool
*/
public $timestamps = true;
protected $dateFormat = 'U';
/**
* 允许自动填充模型字段
*/
protected $fillable = ['id', 'email', 'uid', 'subject', 'from', 'date', 'body', 'status', 'created_at', 'updated_at'];
/**
* 不允许自动填充模型字段
*/
protected $guarded = [];
}
+49
View File
@@ -0,0 +1,49 @@
<?php
namespace app\common\model;
use support\Model;
class EmailPullParsed extends Model
{
/**
* The connection name for the model.
*
* @var string|null
*/
protected $connection = 'mysql';
/**
* The table associated with the model.
*
* @var string
*/
protected $table = 'email_pull_parsed';
/**
* The primary key associated with the table.
*
* @var string
*/
protected $primaryKey = 'id';
/**
* Indicates if the model should be timestamped.
*
* @var bool
*/
public $timestamps = true;
// protected $dateFormat = 'U';
/**
* 允许自动填充模型字段
*/
protected $fillable = ['id', 'email_id', 'date', 'email', 'name', 'tel', 'company', 'country', 'form_url', 'message', 'ip', 'iplocation', 'ilang', 'mailto', 'navigator_language', 'navigator_platform', 'screen_width', 'screen_height', 'ucountry', 'status', 'created_at', 'updated_at'];
/**
* 不允许自动填充模型字段
*/
protected $guarded = [];
}
+49
View File
@@ -0,0 +1,49 @@
<?php
namespace app\common\model;
use support\Model;
class EmailPullUids extends Model
{
/**
* The connection name for the model.
*
* @var string|null
*/
protected $connection = 'mysql';
/**
* The table associated with the model.
*
* @var string
*/
protected $table = 'email_pull_uids';
/**
* The primary key associated with the table.
*
* @var string
*/
protected $primaryKey = 'id';
/**
* Indicates if the model should be timestamped.
*
* @var bool
*/
public $timestamps = false;
protected $dateFormat = 'U';
/**
* 允许自动填充模型字段
*/
protected $fillable = ['uid', 'fetched_at', 'status', 'processed_at', 'email'];
/**
* 不允许自动填充模型字段
*/
protected $guarded = [];
}
@@ -0,0 +1,49 @@
<?php
namespace app\common\model;
use support\Model;
class FinanceReconDubaiFiles extends Model
{
/**
* The connection name for the model.
*
* @var string|null
*/
protected $connection = 'mysql';
/**
* The table associated with the model.
*
* @var string
*/
protected $table = 'finance_recon_dubai_files';
/**
* The primary key associated with the table.
*
* @var string
*/
protected $primaryKey = 'id';
/**
* Indicates if the model should be timestamped.
*
* @var bool
*/
public $timestamps = true;
protected $dateFormat = 'U';
/**
* 允许自动填充模型字段
*/
protected $fillable = ['id', 'caiwu_file_name', 'caiwu_file_url', 'dubai_file_name', 'dubai_file_url', 'created_by', 'created_at', 'updated_at'];
/**
* 不允许自动填充模型字段
*/
protected $guarded = [];
}
+49
View File
@@ -0,0 +1,49 @@
<?php
namespace app\common\model;
use support\Model;
class InquiryTransmit extends Model
{
/**
* The connection name for the model.
*
* @var string|null
*/
protected $connection = 'mysql';
/**
* The table associated with the model.
*
* @var string
*/
protected $table = 'inquiry_transmit';
/**
* The primary key associated with the table.
*
* @var string
*/
protected $primaryKey = 'id';
/**
* Indicates if the model should be timestamped.
*
* @var bool
*/
public $timestamps = true;
protected $dateFormat = 'U';
/**
* 允许自动填充模型字段
*/
protected $fillable = ['id', 'date', 'email', 'name', 'tel', 'company', 'country', 'domain', 'form_url', 'message', 'ip', 'ip_location', 'ilang', 'mailto', 'navigator_language', 'navigator_platform', 'screen_width', 'screen_height', 'uCountry', 'created_at', 'updated_at'];
/**
* 不允许自动填充模型字段
*/
protected $guarded = [];
}
@@ -0,0 +1,49 @@
<?php
namespace app\common\model;
use support\Model;
class InquiryTransmitConfig extends Model
{
/**
* The connection name for the model.
*
* @var string|null
*/
protected $connection = 'mysql';
/**
* The table associated with the model.
*
* @var string
*/
protected $table = 'inquiry_transmit_config';
/**
* The primary key associated with the table.
*
* @var string
*/
protected $primaryKey = 'id';
/**
* Indicates if the model should be timestamped.
*
* @var bool
*/
public $timestamps = true;
protected $dateFormat = 'U';
/**
* 允许自动填充模型字段
*/
protected $fillable = ['id', 'domain', 'mailto', 'lang', 'status', 'remark', 'created_at', 'updated_at'];
/**
* 不允许自动填充模型字段
*/
protected $guarded = [];
}
+53
View File
@@ -0,0 +1,53 @@
<?php
namespace app\common\model;
use support\Model;
class InvItem extends Model
{
/**
* The connection name for the model.
*
* @var string|null
*/
protected $connection = 'mysql';
/**
* The table associated with the model.
*
* @var string
*/
protected $table = 'inv_item';
/**
* The primary key associated with the table.
*
* @var string
*/
protected $primaryKey = 'id';
/**
* Indicates if the model should be timestamped.
*
* @var bool
*/
public $timestamps = true;
protected $dateFormat = 'U';
/**
* 允许自动填充模型字段
*/
protected $fillable = ['id', 'company_id', 'item_code', 'item_name',
'category_id', 'spec_model', 'unit', 'is_consumable', 'allow_qr_claim', 'qr_code', 'qr_code_num', 'qr_code_url',
'stock_qty', 'manage_user_id', 'location', 'remark',
'sort', 'status',
'created_at', 'updated_at'];
/**
* 不允许自动填充模型字段
*/
protected $guarded = [];
}
+81
View File
@@ -0,0 +1,81 @@
<?php
namespace app\common\model;
use support\Model;
class InvRequisition extends Model
{
/**
* The connection name for the model.
*
* @var string|null
*/
protected $connection = 'mysql';
/**
* The table associated with the model.
*
* @var string
*/
protected $table = 'inv_requisition';
/**
* The primary key associated with the table.
*
* @var string
*/
protected $primaryKey = 'id';
/**
* Indicates if the model should be timestamped.
*
* @var bool
*/
public $timestamps = true;
protected $dateFormat = 'U';
/**
* 允许自动填充模型字段
*/
protected $fillable = ['id', 'order_no', 'applicant_company_id', 'dept_id', 'applicant_id', 'applicant_name', 'leader_id', 'manager_id', 'borrow_reason', 'use_start_date', 'use_end_date', 'status', 'claim_type', 'qr_claim_time', 'proxy_claim_user_id', 'proxy_claim_user_name', 'is_returned', 'remark', 'created_at', 'updated_at'];
/**
* 不允许自动填充模型字段
*/
protected $guarded = [];
// 领取方式常量
const CLAIM_TYPE_NORMAL = 1; // 正常申请
const CLAIM_TYPE_QR = 2; // 扫码领取
const CLAIM_TYPE_PROXY = 3; // 管理员代领
/**
* 领取方式文本
*/
public static function getClaimTypeText($type)
{
return [
self::CLAIM_TYPE_NORMAL => '正常申请',
self::CLAIM_TYPE_QR => '扫码领取',
self::CLAIM_TYPE_PROXY => '管理员代领',
][$type] ?? '未知';
}
public function approval()
{
return $this->hasMany('app\common\model\InvRequisitionApprove',
'requisition_id', 'id');
}
/**
* 领用明细关联
*/
public function items()
{
return $this->hasMany('app\common\model\InvRequisitionItem',
'requisition_id', 'id');
}
}
@@ -0,0 +1,55 @@
<?php
namespace app\common\model;
use support\Model;
class InvRequisitionApprove extends Model
{
/**
* The connection name for the model.
*
* @var string|null
*/
protected $connection = 'mysql';
/**
* The table associated with the model.
*
* @var string
*/
protected $table = 'inv_requisition_approve';
/**
* The primary key associated with the table.
*
* @var string
*/
protected $primaryKey = 'id';
/**
* Indicates if the model should be timestamped.
*
* @var bool
*/
public $timestamps = true;
protected $dateFormat = 'U';
/**
* 允许自动填充模型字段
*/
protected $fillable = ['id', 'requisition_id', 'node_type', 'approver_id', 'approver_name', 'action', 'comment', 'action_at', 'created_at', 'updated_at'];
/**
* 不允许自动填充模型字段
*/
protected $guarded = [];
public function requisition()
{
return $this->belongsTo('app\common\model\InvRequisition',
'requisition_id','id');
}
}
@@ -0,0 +1,49 @@
<?php
namespace app\common\model;
use support\Model;
class InvRequisitionApproveAttachment extends Model
{
/**
* The connection name for the model.
*
* @var string|null
*/
protected $connection = 'mysql';
/**
* The table associated with the model.
*
* @var string
*/
protected $table = 'inv_requisition_approve_attachment';
/**
* The primary key associated with the table.
*
* @var string
*/
protected $primaryKey = 'id';
/**
* Indicates if the model should be timestamped.
*
* @var bool
*/
public $timestamps = true;
protected $dateFormat = 'U';
/**
* 允许自动填充模型字段
*/
protected $fillable = ['id', 'requisition_id', 'approve_id', 'file_name', 'file_path', 'file_size', 'created_at', 'updated_at'];
/**
* 不允许自动填充模型字段
*/
protected $guarded = [];
}
@@ -0,0 +1,49 @@
<?php
namespace app\common\model;
use support\Model;
class InvRequisitionAttachment extends Model
{
/**
* The connection name for the model.
*
* @var string|null
*/
protected $connection = 'mysql';
/**
* The table associated with the model.
*
* @var string
*/
protected $table = 'inv_requisition_attachment';
/**
* The primary key associated with the table.
*
* @var string
*/
protected $primaryKey = 'id';
/**
* Indicates if the model should be timestamped.
*
* @var bool
*/
public $timestamps = true;
protected $dateFormat = 'U';
/**
* 允许自动填充模型字段
*/
protected $fillable = ['id', 'requisition_id', 'file_name', 'file_path', 'file_size', 'created_at', 'updated_at'];
/**
* 不允许自动填充模型字段
*/
protected $guarded = [];
}
+57
View File
@@ -0,0 +1,57 @@
<?php
namespace app\common\model;
use support\Model;
class InvRequisitionItem extends Model
{
/**
* The connection name for the model.
*
* @var string|null
*/
protected $connection = 'mysql';
/**
* The table associated with the model.
*
* @var string
*/
protected $table = 'inv_requisition_item';
/**
* The primary key associated with the table.
*
* @var string
*/
protected $primaryKey = 'id';
/**
* Indicates if the model should be timestamped.
*
* @var bool
*/
public $timestamps = true;
protected $dateFormat = 'U';
/**
* 允许自动填充模型字段
*/
protected $fillable = ['id', 'requisition_id', 'item_id', 'item_name_snap', 'spec_model_snap', 'unit_snap', 'is_consumable_snap', 'request_qty', 'approved_qty', 'returned_qty', 'is_finished', 'remark', 'created_at', 'updated_at'];
/**
* 不允许自动填充模型字段
*/
protected $guarded = [];
/**
* 关联领用申请主表
*/
public function requisition()
{
return $this->belongsTo('app\common\model\InvRequisition', 'requisition_id', 'id');
}
}
@@ -0,0 +1,49 @@
<?php
namespace app\common\model;
use support\Model;
class InvRequisitionItemReturn extends Model
{
/**
* The connection name for the model.
*
* @var string|null
*/
protected $connection = 'mysql';
/**
* The table associated with the model.
*
* @var string
*/
protected $table = 'inv_requisition_item_return';
/**
* The primary key associated with the table.
*
* @var string
*/
protected $primaryKey = 'id';
/**
* Indicates if the model should be timestamped.
*
* @var bool
*/
public $timestamps = true;
protected $dateFormat = 'U';
/**
* 允许自动填充模型字段
*/
protected $fillable = ['id', 'return_id', 'requisition_id', 'requisition_item_id', 'item_id', 'apply_user_id', 'apply_user_name', 'return_qty', 'status', 'handler_id', 'handler_name', 'return_status', 'apply_remark', 'handle_remark', 'return_at', 'created_at', 'updated_at'];
/**
* 不允许自动填充模型字段
*/
protected $guarded = [];
}
@@ -0,0 +1,49 @@
<?php
namespace app\common\model;
use support\Model;
class InvRequisitionItemReturnAttachment extends Model
{
/**
* The connection name for the model.
*
* @var string|null
*/
protected $connection = 'mysql';
/**
* The table associated with the model.
*
* @var string
*/
protected $table = 'inv_requisition_item_return_attachment';
/**
* The primary key associated with the table.
*
* @var string
*/
protected $primaryKey = 'id';
/**
* Indicates if the model should be timestamped.
*
* @var bool
*/
public $timestamps = true;
protected $dateFormat = 'U';
/**
* 允许自动填充模型字段
*/
protected $fillable = ['id', 'return_id', 'file_name', 'file_path', 'file_size', 'created_at', 'updated_at'];
/**
* 不允许自动填充模型字段
*/
protected $guarded = [];
}
+49
View File
@@ -0,0 +1,49 @@
<?php
namespace app\common\model;
use support\Model;
class InvRequisitionReturn extends Model
{
/**
* The connection name for the model.
*
* @var string|null
*/
protected $connection = 'mysql';
/**
* The table associated with the model.
*
* @var string
*/
protected $table = 'inv_requisition_return';
/**
* The primary key associated with the table.
*
* @var string
*/
protected $primaryKey = 'id';
/**
* Indicates if the model should be timestamped.
*
* @var bool
*/
public $timestamps = true;
protected $dateFormat = 'U';
/**
* 允许自动填充模型字段
*/
protected $fillable = ['id', 'order_no', 'requisition_id', 'applicant_id', 'applicant_name', 'apply_remark', 'handler_id', 'handler_name', 'status', 'handle_remark', 'return_at', 'created_at', 'updated_at'];
/**
* 不允许自动填充模型字段
*/
protected $guarded = [];
}
@@ -0,0 +1,49 @@
<?php
namespace app\common\model;
use support\Model;
class InvRequisitionReturnApproveAttachment extends Model
{
/**
* The connection name for the model.
*
* @var string|null
*/
protected $connection = 'mysql';
/**
* The table associated with the model.
*
* @var string
*/
protected $table = 'inv_requisition_return_approve_attachment';
/**
* The primary key associated with the table.
*
* @var string
*/
protected $primaryKey = 'id';
/**
* Indicates if the model should be timestamped.
*
* @var bool
*/
public $timestamps = true;
protected $dateFormat = 'U';
/**
* 允许自动填充模型字段
*/
protected $fillable = [];
/**
* 不允许自动填充模型字段
*/
protected $guarded = [];
}
+88
View File
@@ -0,0 +1,88 @@
<?php
namespace app\common\model;
use support\Model;
/**
* 物品库存变化记录模型
*/
class InvStockLog extends Model
{
protected $table = 'inv_stock_log';
protected $fillable = [
'company_id',
'item_id',
'item_name_snap',
'change_type',
'change_qty',
'before_qty',
'after_qty',
'biz_type',
'biz_no',
'operator_id',
'operator_name',
'remark',
'updated_at',
'created_at',
];
/**
* Indicates if the model should be timestamped.
*
* @var bool
*/
public $timestamps = true;
protected $dateFormat = 'U';
public const CHANGE_TYPE_IN = 1; // 入库
public const CHANGE_TYPE_OUT_CLAIM = 2; // 出库(领用)
public const CHANGE_TYPE_OUT_LOSS = 3; // 出库(损耗)
public const CHANGE_TYPE_IN_RETURN = 4; // 入库(归还)
public const BIZ_TYPE_PURCHASE = 1; // 采购入库
public const BIZ_TYPE_QR_CLAIM = 2; // 扫码领取
public const BIZ_TYPE_PROXY_CLAIM = 3; // 管理员代领
public const BIZ_TYPE_NORMAL_CLAIM = 4; // 正常领用(审批后)
public const BIZ_TYPE_RETURN = 5; // 归还入库
public const BIZ_TYPE_LOSS = 6; // 损耗出库
/**
* 变动类型文本
*/
public static function getChangeTypeText($type)
{
return [
self::CHANGE_TYPE_IN => '入库',
self::CHANGE_TYPE_OUT_CLAIM => '出库(领用)',
self::CHANGE_TYPE_OUT_LOSS => '出库(损耗)',
self::CHANGE_TYPE_IN_RETURN => '入库(归还)',
][$type] ?? '未知';
}
/**
* 业务类型文本
*/
public static function getBizTypeText($type)
{
return [
self::BIZ_TYPE_PURCHASE => '采购入库',
self::BIZ_TYPE_QR_CLAIM => '扫码领取',
self::BIZ_TYPE_PROXY_CLAIM => '管理员代领',
self::BIZ_TYPE_NORMAL_CLAIM => '正常领用',
self::BIZ_TYPE_RETURN => '归还入库',
self::BIZ_TYPE_LOSS => '损耗出库',
][$type] ?? '未知';
}
/**
* 关联物品
*/
public function item()
{
return $this->belongsTo(InvItem::class, 'item_id', 'id');
}
}
+49
View File
@@ -0,0 +1,49 @@
<?php
namespace app\common\model;
use support\Model;
class UsedcarCrawlResults extends Model
{
/**
* The connection name for the model.
*
* @var string|null
*/
protected $connection = 'mysql';
/**
* The table associated with the model.
*
* @var string
*/
protected $table = 'usedcar_crawl_results';
/**
* The primary key associated with the table.
*
* @var string
*/
protected $primaryKey = 'id';
/**
* Indicates if the model should be timestamped.
*
* @var bool
*/
public $timestamps = true;
protected $dateFormat = 'U';
/**
* 允许自动填充模型字段
*/
protected $fillable = ['id', 'used_car_id', 'car_id', 'url', 'car_id_url', 'fuel_form', 'classify_energy', 'brand_name', 'series_name', 'car_name', 'year', 'title', 'price', 'new_price', 'price_difference', 'usage_mileage', 'condition_desc', 'images', 'used_detail', 'config_detail', 'format_shop_info', 'format_car_info', 'created_at', 'updated_at'];
/**
* 不允许自动填充模型字段
*/
protected $guarded = [];
}
@@ -0,0 +1,49 @@
<?php
namespace app\common\model;
use support\Model;
class UsedcarCrawlResultsImages extends Model
{
/**
* The connection name for the model.
*
* @var string|null
*/
protected $connection = 'mysql';
/**
* The table associated with the model.
*
* @var string
*/
protected $table = 'usedcar_crawl_results_images';
/**
* The primary key associated with the table.
*
* @var string
*/
protected $primaryKey = 'id';
/**
* Indicates if the model should be timestamped.
*
* @var bool
*/
public $timestamps = true;
protected $dateFormat = 'U';
/**
* 允许自动填充模型字段
*/
protected $fillable = ['id', 'uc_id', 'used_car_id', 'original_name', 'file_path', 'created_at', 'updated_at'];
/**
* 不允许自动填充模型字段
*/
protected $guarded = [];
}
+49
View File
@@ -0,0 +1,49 @@
<?php
namespace app\common\model;
use support\Model;
class UserDashboardRule extends Model
{
/**
* The connection name for the model.
*
* @var string|null
*/
protected $connection = 'mysql';
/**
* The table associated with the model.
*
* @var string
*/
protected $table = 'user_dashboard_rule';
/**
* The primary key associated with the table.
*
* @var string
*/
protected $primaryKey = 'id';
/**
* Indicates if the model should be timestamped.
*
* @var bool
*/
public $timestamps = true;
protected $dateFormat = 'U';
/**
* 允许自动填充模型字段
*/
protected $fillable = [];
/**
* 不允许自动填充模型字段
*/
protected $guarded = [];
}
+50 -2
View File
@@ -2,6 +2,8 @@
namespace app\common\service;
use app\common\model\User;
use app\common\model\UserGroupMembers;
use app\common\validate\ApprovalRecordsValidate;
use app\common\traits\crud\CrudServiceTrait;
use app\common\model\ApprovalRecords;
@@ -56,17 +58,63 @@ class ApprovalRecordsService
case 'todo':
if ($p['scene'] === 'todo') {
if (!in_array(1, $request->user->role_ids)) {
// if (!in_array(1, $request->user->role_ids)) {
// $config['where']['approver_id'] = $request->user->uid;
// }
$roles_id = array_intersect([1,6], $request->user->role_ids);
if(count($roles_id) == 0){
if(in_array(4, $request->user->role_ids)){
$report = User::where('report_to',$request->user->uid)->pluck('id')->toArray();
$report = array_merge($report, [$request->user->uid]);
$config['where']['approver_id'] = ['in',$report];
} elseif (in_array(5, $request->user->role_ids)){ //内勤
$userGroupMember = UserGroupMembers::where('user_id',$request->user->uid)
->where('is_leader',1)
->first();
if($userGroupMember){
$report = UserGroupMembers::where('group_id',$userGroupMember->group_id)
->pluck('user_id')
->toArray();
$report = array_merge($report, [$request->user->uid]);
$config['where']['approver_id'] = ['in',$report];
}else{ //内勤没有负责的组时
$config['where']['approver_id'] = $request->user->uid;
}
} else {
$config['where']['approver_id'] = $request->user->uid;
}
}
$config['where']['status'] = 'pending';
$config['where']['instance.status'] = 'processing';
}
if ($p['scene'] === 'history') {
if (!in_array(1, $request->user->role_ids)) {
// if (!in_array(1, $request->user->role_ids) && !in_array(5, $request->user->role_ids)) {
// $config['where']['approver_id'] = $request->user->uid;
// }
$roles_id = array_intersect([1,6], $request->user->role_ids);
if(count($roles_id) == 0){
if(in_array(4, $request->user->role_ids)){
$report = User::where('report_to',$request->user->uid)->pluck('id')->toArray();
$report = array_merge($report, [$request->user->uid]);
$config['where']['approver_id'] = ['in',$report];
} elseif (in_array(5, $request->user->role_ids)){ //内勤
$userGroupMember = UserGroupMembers::where('user_id',$request->user->uid)
->where('is_leader',1)
->first();
if($userGroupMember){
$report = UserGroupMembers::where('group_id',$userGroupMember->group_id)
->pluck('user_id')
->toArray();
$report = array_merge($report, [$request->user->uid]);
$config['where']['approver_id'] = ['in',$report];
}else{ //内勤没有负责的组时
$config['where']['approver_id'] = $request->user->uid;
}
} else {
$config['where']['approver_id'] = $request->user->uid;
}
}
$config['where']['status'] = ['!=', 'pending'];
}
+18 -2
View File
@@ -13,12 +13,12 @@ class AuthService
/**
* 飞书授权
*/
public function feishuAuthorize(string $corpAbbr, string $systemCcode)
public function feishuAuthorize(string $corpAbbr, string $systemCcode, $mobile)
{
$logic = new AuthLogic();
$certificate = $logic->feishuAppInfo($corpAbbr, $systemCcode);
$feishu = new Feishu($certificate['app_id'], $certificate['app_secret'], $certificate['abbr']);
return ['auth_url' => $feishu->authorize($certificate)];
return ['auth_url' => $feishu->authorize($certificate, $mobile)];
}
/**
@@ -86,4 +86,20 @@ class AuthService
{
(new AuthLogic())->blockToken($token, $systemCode);
}
/**
* 获取jsapi配置
* @param string $corpAbbr
* @param string $systemCcode
* @param string $url 当前页面完整 URL(不包含 # 后面的内容)
* @return array
*/
public function getJsApiConfig(string $corpAbbr, string $systemCcode, string $url)
{
$logic = new AuthLogic();
$certificate = $logic->feishuAppInfo($corpAbbr, $systemCcode);
$feishu = new Feishu($certificate['app_id'], $certificate['app_secret'], $certificate['abbr']);
$jsApi = $feishu->getJsApiConfig($url);
return $jsApi;
}
}
+1 -1
View File
@@ -9,7 +9,7 @@ class CrawlService
{
public function brandSeries($brand)
{
$url = "https://www.dongchedi.com/search?currTab=6&keyword=".$brand;
$url = "https://www.dongchedi.com/search?currTab=6&keyword=".$brand.'&from=search_tab_er&api_param={"en_qc":+0}';
$ql = QueryList::get($url );
$series = [];
$ql->find('div.card-name')->texts()->map(function($item) use(&$series) {
@@ -0,0 +1,290 @@
<?php
namespace app\common\service;
use app\common\validate\FinanceReconDubaiFilesValidate;
use app\common\traits\crud\CrudServiceTrait;
use app\common\model\FinanceReconDubaiFiles;
use app\common\utils\excel\ExcelImportTrait;
use support\Request;
/**
* FinanceReconDubaiFilesService
*
* - 提供两类导入:
* 1) M项目对账明细(sheet: M项目对账明细)
* 2) 迪拜两表合并(sheets: Stock in Jebel Ali / Under shipping)
*
* - 公共导入流程复用 ExcelImportTrait
* - OSS 读取通过你的 OssService->generatePrivateUrl() 完成
*/
class FinanceReconDubaiFilesService
{
use CrudServiceTrait;
use ExcelImportTrait; // 复用通用导入方法
/** 指定当前要操作的模型(用于 CrudServiceTrait) */
protected string $modelClass = FinanceReconDubaiFiles::class;
/** 验证器 */
protected $validator = FinanceReconDubaiFilesValidate::class;
/** 获取验证器实例 */
public function getValidator()
{
return new $this->validator;
}
/**
* 写入前置:将上传组件的结构转为 *_url / *_name,并记录创建人
*/
public function beforeServiceStore($data, Request $request)
{
$data['created_by'] = $request->user->uid ?? 0;
$data['caiwu_file_url'] = $data['caiwu_file']['url'] ?? '';
$data['caiwu_file_name'] = $data['caiwu_file']['name'] ?? '';
$data['dubai_file_url'] = $data['dubai_file']['url'] ?? '';
$data['dubai_file_name'] = $data['dubai_file']['name'] ?? '';
return $data;
}
/* ============================================================
* 一、M项目对账明细 导入配置(sheet: M项目对账明细)
* ============================================================ */
/** 中文表头 => 目标字段名 */
protected array $headerMapM = [
'车架号' => 'vin',
'开票价' => 'invoice_price',
'实际采购价' => 'actual_purchase_price',
'开票价和实际采购价差额' => 'invoice_vs_purchase_diff',
'车源中介费' => 'source_broker_fee',
'配件' => 'parts_fee',
'停车费' => 'parking_fee',
'保险费' => 'insurance_fee',
'车辆手续费' => 'vehicle_service_fee',
'检测费' => 'inspection_fee',
'购置税' => 'purchase_tax',
'汽车其他费用' => 'other_vehicle_fee',
'转移待出口' => 'transfer_to_export',
'国际运费' => 'freight_international',
'国内运费1' => 'freight_domestic_1',
'国内运费2' => 'freight_domestic_2',
'报关费及港杂费' => 'customs_port_fee',
'注销费' => 'cancellation_fee',
'车船税' => 'vehicle_vessel_tax',
'其他财务费用' => 'other_finance_fee',
'汽车小额杂费' => 'minor_vehicle_misc',
'上牌费' => 'registration_fee',
'其他保险费' => 'other_insurance_fee',
'油补' => 'fuel_subsidy',
'快递费' => 'express_fee',
'国内成本合计' => 'domestic_cost_total',
'退税额' => 'tax_refund_amount',
'cif杰贝阿里成本价' => 'cif_jebel_ali_cost',
'范经理发车看板CIF' => 'fan_manager_cif',
'财务-范经理 差额' => 'finance_vs_fan_diff',
];
/** 需要转为 decimal 的目标字段 */
protected array $decimalFieldsM = [
'invoice_price','actual_purchase_price','invoice_vs_purchase_diff',
'source_broker_fee','parts_fee','parking_fee','insurance_fee','vehicle_service_fee','inspection_fee',
'purchase_tax','other_vehicle_fee','transfer_to_export',
'freight_international','freight_domestic_1','freight_domestic_2','customs_port_fee',
'cancellation_fee','vehicle_vessel_tax','other_finance_fee','minor_vehicle_misc',
'registration_fee','other_insurance_fee','fuel_subsidy','express_fee',
'domestic_cost_total','tax_refund_amount','cif_jebel_ali_cost','fan_manager_cif','finance_vs_fan_diff',
];
/**
* M项目对账明细导入
*
* @param string $file 可为“签名 URL”或“OSS 对象键”
* @param bool $isUrl 若传入的是已签名 URL,请设 true;若是对象键,设 false
* @param bool $insertToDb 是否立即入库
*
* 返回:['sheet','total_rows','skipped','errors','to_import','inserted','preview_3',('error'?)]
*/
public function mImport(string $file, bool $isUrl = true, bool $insertToDb = true): array
{
$sheet = 'M项目对账明细';
// 允许传对象键:当 isUrl=false 且非 http(s) 时,自动用 OssService 签名
if ($isUrl === false && !preg_match('#^https?://#i', $file)) {
$file = $this->ossSignedUrl($file);
$isUrl = true;
}
$rows = $this->readSheetRows($file, $sheet, $isUrl, headerRow: 1);
$mapped = [];
$skipped = 0;
$errors = 0;
foreach ($rows as $row) {
try {
if ($this->shouldSkipByVinAliases($row, ['车架号'])) { $skipped++; continue; }
$rec = $this->mapAndNormalizeRow($row, $this->headerMapM, $this->decimalFieldsM);
$this->stamp($rec);
$mapped[] = $rec;
} catch (\Throwable $e) {
// 单行异常不阻断全局导入
$errors++; continue;
}
}
$result = [
'sheet' => $sheet,
'total_rows' => count($rows),
'skipped' => $skipped,
'errors' => $errors,
'to_import' => count($mapped),
];
if ($insertToDb) {
// 注意表名:按你的业务这里写入 finance_recon_m_detail
$ret = $this->batchInsert('finance_recon_m_detail', $mapped);
$result['inserted'] = $ret['inserted'];
if ($ret['error']) $result['error'] = 'Database insert error: ' . $ret['error'];
} else {
$result['inserted'] = 0;
}
$result['preview_3'] = array_slice($mapped, 0, 3);
return $result;
}
/** M项目对账明细:从 OSS 对象键导入(语义化便捷入口) */
public function mImportFromOssKey(string $objectKey, bool $insertToDb = true): array
{
$signedUrl = $this->ossSignedUrl($objectKey);
return $this->mImport($signedUrl, true, $insertToDb);
}
/** M项目对账明细:从已签名 URL 导入(语义化便捷入口) */
public function mImportFromSignedUrl(string $signedUrl, bool $insertToDb = true): array
{
return $this->mImport($signedUrl, true, $insertToDb);
}
/* ============================================================
* 二、迪拜两表合并 导入配置(sheets: Stock in Jebel Ali / Under shipping)
* ============================================================ */
/** 表头映射(已兼容不一致列名:VIN NO/车架号、国际运费和杂费/SHIPPING COST) */
protected array $headerMapDubai = [
'VIN NO' => 'vin', '车架号' => 'vin',
'开票价' => 'invoice_price',
'中介费' => 'broker_fee',
'购置税' => 'purchase_tax',
'退税' => 'tax_refund',
'不含税车价' => 'price_ex_vat',
'费用' => 'fee_total',
'国内运费' => 'domestic_freight',
'FCA成本价' => 'fca_cost',
'FCA成本价汇率' => 'fca_fx',
'国际运费和杂费' => 'shipping_cost',
'SHIPPING COST' => 'shipping_cost',
'运费汇率' => 'shipping_fx',
'CIF 杰贝阿里成本价' => 'cif_jebel_ali_cost',
'CIF RMB' => 'cif_rmb', // Under shipping 没有该列,将在逻辑中推导
];
/** 需要转为 decimal 的目标字段(汇率字段使用 DECIMAL(18,6) 建表) */
protected array $decimalFieldsDubai = [
'invoice_price','broker_fee','purchase_tax','tax_refund','price_ex_vat',
'fee_total','domestic_freight','fca_cost','fca_fx',
'shipping_cost','shipping_fx','cif_jebel_ali_cost','cif_rmb',
];
/**
* 迪拜两表合并导入(Stock in Jebel Ali / Under shipping)
*
* 缺失值规则:
* - Under shipping 无 “CIF RMB”,尝试:cif_rmb = cif_jebel_ali_cost × shipping_fx
* 若运费汇率缺失则用 fca_fx 兜底,都没有则置 NULL。
*
* 返回:['sheets','total_rows','skipped','to_import','inserted','preview_5',('error'?)]
*/
public function dImport(string $file, bool $isUrl = false, bool $insertToDb = true): array
{
// 允许传对象键:当 isUrl=false 且非 http(s) 时,自动用 OssService 签名
if ($isUrl === false && !preg_match('#^https?://#i', $file)) {
$file = $this->ossSignedUrl($file);
$isUrl = true;
}
$sheets = ['Stock in Jebel Ali', 'Under shipping'];
$parsed = [];
// 逐 Sheet 读取
foreach ($sheets as $sheet) {
$parsed[$sheet] = $this->readSheetRows($file, $sheet, $isUrl, headerRow: 1);
}
// 行处理与合并
$rowsAll = [];
$skipped = 0;
foreach ($sheets as $sheet) {
foreach ($parsed[$sheet] as $row) {
if ($this->shouldSkipByVinAliases($row, ['VIN NO','车架号'])) { $skipped++; continue; }
// 映射 + 数值规范化
$rec = $this->mapAndNormalizeRow($row, $this->headerMapDubai, $this->decimalFieldsDubai);
// Under shipping 无 CIF RMB:尝试用汇率推导
if (empty($rec['cif_rmb'])) {
$cif = $rec['cif_jebel_ali_cost'] ?? null;
$fx = $rec['shipping_fx'] ?? null;
if ($cif !== null && $fx !== null) {
$rec['cif_rmb'] = round($cif * $fx, 2);
} elseif ($cif !== null && !empty($rec['fca_fx'])) {
$rec['cif_rmb'] = round($cif * (float)$rec['fca_fx'], 2);
} else {
$rec['cif_rmb'] = null;
}
}
// 统一 VIN 与来源
$rec['vin'] = $this->pickFirstNonEmpty($row, ['VIN NO','车架号']);
$rec['source_sheet'] = $sheet;
$this->stamp($rec);
$rowsAll[] = $rec;
}
}
// 结果与入库
$result = [
'sheets' => $sheets,
'total_rows' => array_sum(array_map('count', $parsed)),
'skipped' => $skipped,
'to_import' => count($rowsAll),
];
if ($insertToDb) {
// 注意表名:按你的业务这里写入 finance_recon_d_detail
$ret = $this->batchInsert('finance_recon_d_detail', $rowsAll);
$result['inserted'] = $ret['inserted'];
if ($ret['error']) $result['error'] = 'Database insert error: ' . $ret['error'];
} else {
$result['inserted'] = 0;
}
$result['preview_5'] = array_slice($rowsAll, 0, 5);
return $result;
}
/** 迪拜两表:从 OSS 对象键导入(语义化便捷入口) */
public function dImportFromOssKey(string $objectKey, bool $insertToDb = true): array
{
$signedUrl = $this->ossSignedUrl($objectKey);
return $this->dImport($signedUrl, true, $insertToDb);
}
/** 迪拜两表:从已签名 URL 导入(语义化便捷入口) */
public function dImportFromSignedUrl(string $signedUrl, bool $insertToDb = true): array
{
return $this->dImport($signedUrl, true, $insertToDb);
}
}
@@ -0,0 +1,67 @@
<?php
namespace app\common\service;
use app\common\validate\InquiryTransmitConfigValidate;
use app\common\traits\crud\CrudServiceTrait;
use app\common\model\InquiryTransmitConfig;
use support\Request;
class InquiryTransmitConfigService
{
use CrudServiceTrait;
/**
* 指定当前要操作的模型
*/
protected string $modelClass = InquiryTransmitConfig::class;
/**
* 验证器
*/
protected $validator = InquiryTransmitConfigValidate::class;
/**
* 获取验证器
*/
public function getValidator()
{
return new $this->validator;
}
/**
* 构造 CrudUtil 查询配置
*/
protected function buildQueryConfig(Request $request, string $scene = 'default'): array
{
$p = $request->all();
$config = [];
$config['model'] = $this->modelClass;
switch ($scene) {
case 'default':
$config['where']['status'] = 1;
$config['order']['id'] = 'desc';
break;
case 'index':
$config['where']['status'] = 1;
$config['order']['id'] = 'desc';
break;
case 'show':
$config['where']['id'] = $p['id'];
$config['where']['status'] = 1;
break;
case 'store':
break;
case 'update':
$config['where']['id'] = $p['id'];
break;
case 'destroy':
$config['where']['id'] = $p['id'];
break;
}
return $config;
}
}
@@ -0,0 +1,92 @@
<?php
namespace app\common\service;
use app\common\validate\InquiryTransmitValidate;
use app\common\traits\crud\CrudServiceTrait;
use app\common\model\InquiryTransmit;
use support\Request;
class InquiryTransmitService
{
use CrudServiceTrait;
/**
* 指定当前要操作的模型
*/
protected string $modelClass = InquiryTransmit::class;
/**
* 验证器
*/
protected $validator = InquiryTransmitValidate::class;
/**
* 获取验证器
*/
public function getValidator()
{
return new $this->validator;
}
/** 创建 */
public function serviceStore(Request $request, array $data = [], string $domain = '')
{
$data['email'] = $data['Email'];
$data['name'] = $data['Name'];
$data['tel'] = $data['Tel'];
$data['company'] = $data['Company'];
$data['country'] = $data['Country'];
$data['form_Url'] = $data['Form Url'];
$data['message'] = $data['Message'];
$data['ip_location'] = $data['IpLocation'];
$data['ip'] = $data['Ip'];
$data['domain'] = $domain;
return InquiryTransmit::create($data);
}
/**
* 构造 CrudUtil 查询配置
*/
protected function buildQueryConfig(Request $request, string $scene = 'default'): array
{
$p = $request->all();
$config = [];
$config['model'] = $this->modelClass;
switch ($scene) {
case 'default':
$config['order']['id'] = 'desc';
break;
case 'index':
// 域名
if (isset($p['domain']) && !empty($p['domain'])) {
$config['where']['domain'] = $p['domain'];
}
// 关键字
if (isset($p['keyword']) && $p['keyword']) {
$k = '%' . $p['keyword'] . '%';
foreach (['email', 'name', 'tel', 'country', 'message', 'form_url'] as $col) {
$config['orWhere'][] = [$col, 'like', $k];
}
}
$config['order']['id'] = 'desc';
break;
case 'show':
$config['where']['id'] = $p['id'];
break;
case 'store':
break;
case 'update':
$config['where']['id'] = $p['id'];
break;
case 'destroy':
$config['where']['id'] = $p['id'];
break;
}
return $config;
}
}
+320
View File
@@ -0,0 +1,320 @@
<?php
namespace app\common\service;
use app\common\logic\InvItemLogic;
use app\common\model\User;
use app\common\validate\InvItemValidate;
use app\common\traits\crud\CrudServiceTrait;
use app\common\model\InvItem;
use support\Redis;
use support\Request;
use support\exception\BusinessException;
class InvItemService
{
use CrudServiceTrait;
/**
* 指定当前要操作的模型
*/
protected string $modelClass = InvItem::class;
/**
* 验证器
*/
protected $validator = InvItemValidate::class;
protected $logic;
public function __construct()
{
$this->logic = (new InvItemLogic());
}
/**
* 获取验证器
*/
public function getValidator()
{
return new $this->validator;
}
/**
* 构造 CrudUtil 查询配置
*/
protected function buildQueryConfig(Request $request, string $scene = 'default'): array
{
$p = $request->all();
$config = [];
$config['model'] = $this->modelClass;
switch ($scene) {
case 'default':
$config['where']['status'] = 1;
$config['order']['sort'] = 'desc';
$config['order']['id'] = 'desc';
break;
case 'index':
$config['where']['status'] = 1;
// 关键字
if (isset($p['keyword']) && $p['keyword']) {
$k = '%' . $p['keyword'] . '%';
foreach (['item_name'] as $col) {
$config['orWhere'][] = [$col, 'like', $k];
}
}
$config['order']['sort'] = 'desc';
$config['order']['id'] = 'desc';
break;
case 'show':
$config['where']['id'] = $p['id'];
$config['where']['status'] = 1;
break;
case 'store':
break;
case 'update':
$config['where']['id'] = $p['id'];
break;
case 'destroy':
$config['where']['id'] = $p['id'];
break;
}
return $config;
}
/**
* 生成 8-12 位的物品编码(默认10位)
*
* @param int $length 编码长度,默认为10(范围 8-12)
* @return string 返回大写的字母数字组合编码
*/
private function generateItemCode($length = 10)
{
// 排除易混淆字符:0, O, o, 1, I, l
$chars = '23456789ABCDEFGHJKLMNPQRSTUVWXYZ';
$code = '';
$maxIndex = strlen($chars) - 1;
for ($i = 0; $i < $length; $i++) {
$code .= $chars[mt_rand(0, $maxIndex)];
}
return $code;
}
public function beforeServiceStore($data, Request $request): array
{
$data['item_code'] = $this->generateItemCode();
$data['category_id'] = 0;
$data['spec_model'] = '';
$data['unit'] = '件';
$data['stock_qty'] = $data['stock_qty']??1;
$data['location'] = '';
Redis::set('inv_store_u_stock', $request->user->uid, 'EX', 30);
return $data;
}
public function afterServiceStore($res)
{
$uid = redis::get('inv_store_u_stock');
$name = $uid ? User::where('id', $uid)->value('name') : '';
Redis::del('inv_store_u_stock');
// 记录库存变化日志
$this->recordStockLog([
'item_id' => $res['id'],
'item_name_snap' => $res['item_name'],
'change_qty' => $res['stock_qty'],
'before_qty' => 0,
'after_qty' => $res['stock_qty'],
'biz_type' => 1,
'biz_no' => '',
'operator_id' => $uid,
'operator_name' => $name,
'remark' => '初始数量',
], $res['stock_qty']);
}
public function beforeServiceUpdate($data, Request $request)
{
$uid = $request->user->uid;
$name = $uid ? User::where('id', $uid)->value('name') : '';
$item = InvItem::where('id', $data['id'])->first();
$change_qty = $data['stock_qty'] - $item['stock_qty'];
// 记录库存变化日志
$this->recordStockLog([
'item_id' => $data['id'],
'item_name_snap' => $data['item_name'],
'change_qty' => $change_qty,
'before_qty' => $item['stock_qty'],
'after_qty' => $data['stock_qty'],
'biz_type' => 1,
'biz_no' => '',
'operator_id' => $uid,
'operator_name' => $name,
'remark' => '管理员修改数量',
], $change_qty);
return $data;
}
/**
* 变动物品库存数量
*
* @param int $itemId 物品ID
* @param int $changeQty 变动数量:>0 入库,<0 出库
* @param bool $allowNegative 是否允许库存变成负数,默认不允许
* @param array $options 额外选项(用于记录日志)
* - change_type: 变动类型
* - biz_type: 业务类型
* - biz_no: 单据号
* - operator_id: 操作人ID
* - operator_name: 操作人姓名
* - remark: 备注
* @return InvItem 最新的物品记录
* @throws BusinessException
*/
public function changeStock(int $itemId, int $changeQty, bool $allowNegative = false, array $options = []): InvItem
{
if ($changeQty === 0) {
throw new BusinessException('库存变动数量不能为 0');
}
// 获取变动前的库存(用于日志)
$itemBefore = InvItem::query()->find($itemId);
if (!$itemBefore) {
throw new BusinessException('物品不存在');
}
$beforeQty = $itemBefore->stock_qty;
$now = time();
// 构建查询
$query = InvItem::query()->where('id', $itemId);
// 如果不允许负数,并且是出库场景(变动为负)
if (! $allowNegative && $changeQty < 0) {
// 要保证:当前 stock_qty >= 出库数量
$query->where('stock_qty', '>=', -$changeQty);
}
// 使用 increment 实现:UPDATE inv_item SET stock_qty = stock_qty + ?, updated_at = ? WHERE ...
$affected = $query->increment('stock_qty', $changeQty, [
'updated_at' => $now,
]);
if ($affected === 0) {
// 更新不到行:
// 1) 物品不存在
// 2) 或者库存不足(不允许负数时)
throw new BusinessException('库存不足,无法完成库存变动');
}
// 再查一次最新数据返回(在外层事务中也会保持一致)
/** @var InvItem $item */
$item = InvItem::query()->find($itemId);
// 记录库存变化日志
$this->recordStockLog([
'item_id' => $itemId,
'item_name_snap' => $item->item_name,
'change_qty' => $changeQty,
'before_qty' => $beforeQty,
'after_qty' => $item->stock_qty,
'change_type' => $options['change_type'] ?? null,
'biz_type' => $options['biz_type'] ?? null,
'biz_no' => $options['biz_no'] ?? null,
'operator_id' => $options['operator_id'] ?? null,
'operator_name' => $options['operator_name'] ?? null,
'remark' => $options['remark'] ?? null,
], $changeQty);
return $item;
}
/**
* 出库(减库存,默认不允许负数)
*
* @param int $itemId 物品ID
* @param int $qty 出库数量
* @param bool $allowNegative 是否允许负数
* @param array $options 日志选项(同 changeStock)
* @return InvItem
* @throws BusinessException
*/
public function stockOut(int $itemId, int $qty, bool $allowNegative = false, array $options = []): InvItem
{
if ($qty <= 0) {
throw new BusinessException('出库数量必须大于 0');
}
// 确定变动类型:2-出库(领用)或 3-出库(损耗)
$changeType = $options['change_type'] ?? \app\common\model\InvStockLog::CHANGE_TYPE_OUT_CLAIM;
return $this->changeStock($itemId, -$qty, $allowNegative, array_merge($options, [
'change_type' => $changeType,
'remark' => $options['remark'] ?? '领用出库',
]));
}
/**
* 入库(加库存)
*
* @param int $itemId 物品ID
* @param int $qty 入库数量
* @param array $options 日志选项(同 changeStock)
* @return InvItem
* @throws BusinessException
*/
public function stockIn(int $itemId, int $qty, array $options = []): InvItem
{
if ($qty <= 0) {
throw new BusinessException('入库数量必须大于 0');
}
// 确定变动类型:1-入库 或 4-入库(归还)
$changeType = $options['change_type'] ?? \app\common\model\InvStockLog::CHANGE_TYPE_IN;
return $this->changeStock($itemId, $qty, true, array_merge($options, [
'change_type' => $changeType,
'remark' => $options['remark'] ?? '入库',
]));
}
/**
* 记录库存变化日志
*
* @param array $logData
* @param int $changeQty 变动数量
* @return void
*/
private function recordStockLog(array $logData, int $changeQty): void
{
try {
// 根据变动数量确定变动类型(如果未指定)
if (!isset($logData['change_type'])) {
if ($changeQty > 0) {
$logData['change_type'] = \app\common\model\InvStockLog::CHANGE_TYPE_IN;
} else {
$logData['change_type'] = \app\common\model\InvStockLog::CHANGE_TYPE_OUT_CLAIM;
}
}
(new InvStockLogService())->record($logData);
} catch (\Throwable $e) {
// 日志记录失败不影响主业务,仅记录错误
error_log('库存日志记录失败: ' . $e->getMessage());
}
}
/**
* 获取车型list
*/
public function getList()
{
return $this->logic->getList();
}
}
@@ -0,0 +1,271 @@
<?php
namespace app\common\service;
use app\common\model\InvItem;
use app\common\model\User;
use app\common\validate\InvRequisitionApproveValidate;
use app\common\traits\crud\CrudServiceTrait;
use app\common\model\InvRequisitionApprove;
use support\Db;
use support\exception\BusinessException;
class InvRequisitionApproveService
{
use CrudServiceTrait;
/**
* 指定当前要操作的模型
*/
protected string $modelClass = InvRequisitionApprove::class;
/**
* 验证器
*/
protected $validator = InvRequisitionApproveValidate::class;
/**
* 获取验证器
*/
public function getValidator()
{
return new $this->validator;
}
/**
* @param string $flowType
* @param string $bizType
* @param string $bizId
* @param array $formData ['manage_user_id'=>23]//物品管理员
* @param int $initiatorId //申请人id
* @return array
*/
public function start(string $flowType, string $bizType, string $bizId, array $formData, int $initiatorId): array
{
return Db::transaction(function () use ($flowType, $bizType, $bizId, $formData, $initiatorId) {
$flow = Db::table('approval_flows')->where('type', $flowType)->where('is_active', 1)->first();
if (!$flow) throw new BusinessException("审批流程未找到或已停用: {$flowType}");
$steps = json_decode($flow->steps, true);
if (!is_array($steps) || empty($steps)) throw new BusinessException("流程步骤配置错误");
// 找首个有效步骤索引(考虑条件)
$firstIndex = $this->getNextStepIndex($steps, $formData, -1);
if ($firstIndex === null) throw new BusinessException("未找到可执行的审批步骤");
$firstStep = $steps[$firstIndex];
//$now = date('Y-m-d H:i:s');
$now = time();
$service = new \app\common\utils\approval\service\ApprovalFlowService();
// 解析首步审批人(支持 role_id 或 roles.name)
$instance = (object)[
'initiator_id' => $initiatorId,
'form_data'=>json_encode($formData, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)
];
$approverIds = $service->StetApprovers($firstStep, $instance, true);
if(empty($approverIds)){
throw new BusinessException('未解析到任何审批人');
}
$uid = array_shift($approverIds);
$approver = User::where('id', $uid)->first();
$approveId = Db::table('inv_requisition_approve')->insertGetId([
'requisition_id' => $bizId, //申请单ID
'node_type' => $bizType,
'approver_id' => $approver->id,
'approver_name' => $approver->name,
'action' => 0,
'comment' => '',
'created_at' => $now,
'updated_at' => $now,
]);
// foreach ($approverIds as $uid) {
// $this->insertApprovalRecord($instance, $firstIndex, $uid);
// }
return ['instance_id' => $approveId];
});
}
/** 执行审批 */
public function approve(int $requisitionId, int $approverId, int $uid, string $action, string $comment = '', array $attachment = [])
{
if (!in_array($action, ['1','2'], true)) { //审批动作:1通过 2驳回
throw new BusinessException("无效的审批操作");
}
return Db::transaction(function () use ($requisitionId, $approverId, $uid, $action, $comment, $attachment) {
// $instance = Db::table('approval_instances')->where('id', $instanceId)->lockForUpdate()->first();
$requisition = Db::table('inv_requisition')->where('id', $requisitionId)->lockForUpdate()->first();
if (!$requisition) throw new BusinessException("审批实例未找到");
if ($requisition->status !== 10 && $requisition->status !== 11) {
return ['status' => 'noop', 'msg' => '实例未处理'];
}
//审批流程
$flow = Db::table('approval_flows')->where('type', 'inv_requisition')->first();
if (!$flow) throw new BusinessException("审批流程定义未找到");
$steps = json_decode($flow->steps, true);
//$idx = (int)$requisition->current_step;
//$step = $steps[$idx] ?? null;
//if (!$step) throw new BusinessException("当前步骤配置错误");
// 找到该用户在本步的 pending 记录(加锁)
$pendingRecord = Db::table('inv_requisition_approve')
->where('requisition_id', $requisitionId)
->where('id', $approverId)
->where('action', 0)
->lockForUpdate()
->first();
if (!$pendingRecord) {
// 已处理过?
$anyRecord = Db::table('inv_requisition_approve')
->where('requisition_id', $requisitionId)
->where('id', $approverId)
->first();
if ($anyRecord && $anyRecord->action > 0) {
return ['status' => 'noop', 'msg' => '已处理'];
}
// 步骤已推进
// if ((int)$requisition->current_step !== $idx) {
// return ['status' => 'noop', 'msg' => '非当前审批'];
// }
return ['status' => 'noop', 'msg' => '无待处理记录'];
} else if ($pendingRecord->approver_id != $uid) { //检查当前用户是否为审核人
return ['status' => 'noop', 'msg' => '无审核权限'];
}
$now = time();
// 1) 更新当前人的审批记录
Db::table('inv_requisition_approve')->where('id', $pendingRecord->id)->update([
'action' => $action,
'comment' => $comment,
'action_at' => $now,
'updated_at' => $now,
]);
foreach ($attachment as $attac) {
Db::table('inv_requisition_approve_attachment')->insertGetId([
'requisition_id' => $requisitionId,
'approve_id' => $approverId,
'file_name' => $attac['original_name'],
'file_path' => $attac['file_path'],
'created_at' => $now,
]);
}
// 2) 已通过步骤与总步数相等则结束审核流程
$pass = Db::table('inv_requisition_approve')
->where('requisition_id', $requisitionId)
->where('action', 1)
->count();
if($action == 1){
if(count($steps) == $pass){
//判断是否都是消耗品,是否需要归还
$requisitionItem = Db::table('inv_requisition_item')
->where('requisition_id', $requisition->id)
->where('is_consumable_snap', 0)
->get();
if($requisitionItem){
Db::table('inv_requisition')->where('id', $requisition->id)->update(['status'=>12]);
}else{
Db::table('inv_requisition')->where('id', $requisition->id)->update(['status'=>19]);
}
//更改库存
$requisitionItem = Db::table('inv_requisition_item')
->where('requisition_id', $requisition->id)
->get();
foreach ($requisitionItem as $item) {
//$invItem = InvItem::where('id', $item->item_id)->first();
(new InvItemService())->stockOut($item->item_id,$item->approved_qty);
}
// $requisition->status = 12;
// $requisition->save();
return ['status' => 'complete', 'msg' => '已通过'];
}
}
// 3) 拒绝 => 结束
if($action == 2){
Db::table('inv_requisition_approve')->where('id', $pendingRecord->id)->update([
'action' => $action,
'comment' => $comment,
'approved_at' => $now,
'updated_at' => $now,
]);
//if(count($steps) == $pass){
Db::table('inv_requisition')->where('id', $requisition->id)->update(['status'=>13]);
return ['status' => 'complete', 'msg' => '拒绝'];
//}
}
// 4) 通过 => 进入下一步
Db::table('inv_requisition')->where('id', $requisitionId)->update(['status'=>11]);
$requisitionItem = Db::table('inv_requisition_item')->where('requisition_id', $requisitionId)->first();
$item_id = Db::table('inv_item')->where('id', $requisitionItem->item_id)->first();
$formData = ['manage_user_id'=>$item_id->manage_user_id];
$nextIndex = $pass;
$nextStep = $steps[$nextIndex];
$service = new \app\common\utils\approval\service\ApprovalFlowService();
$instance = (object)[
'initiator_id' => $requisition->applicant_id, //申请人id
'form_data'=>json_encode($formData, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)
];
$approverIds = $service->StetApprovers($nextStep, $instance, true);
// if($approverIds == "manage_user_id"){ //物品管理人审核步骤查询管理人
// $requisitionItem = Db::table('inv_requisition_item')->where('id', $requisitionId)->first();
// $invItem = Db::table('inv_requisition_item')->where('id', $requisitionItem->item_id)->first();
//
// if(empty($invItem)){
// return ['status' => 'noop', 'msg' => '未查询到物品管理人'];
// }
// $approverIds = [$invItem->manage_user_id];
// }
$approver = User::where('id', array_pop($approverIds))->first();
Db::table('inv_requisition_approve')->insertGetId([
'requisition_id' => $requisitionId, //申请单ID
'node_type' => $nextStep['approvers'][0]['value'],
'approver_id' => $approver->id,
'approver_name' => $approver->name,
'action' => 0,
'comment' => '',
'created_at' => $now,
'updated_at' => $now,
]);;
return ['status' => 'complete', 'msg' => '已通过'];
});
}
// public function insertApprovalRecord()
// {
// }
/** 获取下一个有效步骤索引 */
private function getNextStepIndex(array $steps, array $formData, int $currentStepIndex): ?int
{
for ($i = $currentStepIndex + 1; $i < count($steps); $i++) {
$s = $steps[$i];
if (empty($s['condition'])) {
return $i;
}
}
return null;
}
}
@@ -0,0 +1,31 @@
<?php
namespace app\common\service;
use app\common\validate\InvRequisitionAttachmentValidate;
use app\common\traits\crud\CrudServiceTrait;
use app\common\model\InvRequisitionAttachment;
class InvRequisitionAttachmentService
{
use CrudServiceTrait;
/**
* 指定当前要操作的模型
*/
protected string $modelClass = InvRequisitionAttachment::class;
/**
* 验证器
*/
protected $validator = InvRequisitionAttachmentValidate::class;
/**
* 获取验证器
*/
public function getValidator()
{
return new $this->validator;
}
}
@@ -0,0 +1,31 @@
<?php
namespace app\common\service;
use app\common\validate\InvRequisitionItemReturnAttachmentValidate;
use app\common\traits\crud\CrudServiceTrait;
use app\common\model\InvRequisitionItemReturnAttachment;
class InvRequisitionItemReturnAttachmentService
{
use CrudServiceTrait;
/**
* 指定当前要操作的模型
*/
protected string $modelClass = InvRequisitionItemReturnAttachment::class;
/**
* 验证器
*/
protected $validator = InvRequisitionItemReturnAttachmentValidate::class;
/**
* 获取验证器
*/
public function getValidator()
{
return new $this->validator;
}
}
@@ -0,0 +1,789 @@
<?php
namespace app\common\service;
use app\common\model\InvItem;
use app\common\model\InvRequisition;
use app\common\model\InvRequisitionItem;
use app\common\model\InvRequisitionReturn;
use app\common\model\InvStockLog;
use app\common\model\User;
use app\common\service\UserService;
use app\common\validate\InvRequisitionItemReturnValidate;
use app\common\traits\crud\CrudServiceTrait;
use app\common\model\InvRequisitionItemReturn;
use support\Request;
use support\Db;
use support\exception\BusinessException;
use Illuminate\Database\Eloquent\Model;
use app\common\model\InvRequisitionItemReturnAttachment;
use function Symfony\Component\String\u;
class InvRequisitionItemReturnService
{
use CrudServiceTrait;
/**
* 指定当前要操作的模型
*/
protected string $modelClass = InvRequisitionItemReturn::class;
/**
* 验证器
*/
protected $validator = InvRequisitionItemReturnValidate::class;
/**
* 获取验证器
*/
public function getValidator()
{
return new $this->validator;
}
/**
* 将二维数组按指定 key 重组为关联数组,该 key 的值作为新数组的键,
* 其余元素组成子数组作为值。
*
* @param array $array 原始二维数组
* @param string $keyField 用作新键的字段名(如 'id')
* @return array 重组后的数组
*/
private function groupArrayByKey($array, $keyField = 'id')
{
$result = [];
foreach ($array as $item) {
if (!isset($item[$keyField])) {
continue; // 跳过没有该 key 的项
}
$key = $item[$keyField];
// 移除用作键的字段
unset($item[$keyField]);
$result[$key] = $item;
}
return $result;
}
/**
* 创建物品归还申请前置处理
*
* @param array $data 归还申请数据
* @param Request $request 请求对象
* @return array 处理后的数据
*/
public function beforeServiceStore($data, Request $request): array
{
try {
// // 模拟数据
// $data = [
// [
// 'requisition_item_id' => 9,
// 'return_qty' => 1,
// 'apply_remark' => '申请备注',
// 'attachment' => [
// [
// 'original_name' => '附件1.jpg',
// 'file_path' => '/uploads/2023/07/01/attachment1.jpg'
// ],
// [
// 'original_name' => '附件2.jpg',
// 'file_path' => '/uploads/2023/07/01/attachment2.jpg'
// ]
// ]
// ]
// ];
// 获取所有申请项ID
$reqItemIds = array_column($data['items'], 'requisition_item_id');
// 参数校验
if (empty($reqItemIds)) {
throw new BusinessException('未提供有效的申请项ID');
}
// 将申请项按 ID 重组为关联数组
$newReqItems = $this->groupArrayByKey($data['items'], 'requisition_item_id');
$data['return_at'] = is_int($data['return_at']) ? $data['return_at']: strtotime($data['return_at']);
// 获取申请项明细
$reqItemsArr = InvRequisitionItem::whereIn('id', $reqItemIds)->get()->toArray();
// 检查是否获取到有效数据
if (empty($reqItemsArr)) {
throw new BusinessException('未找到对应的申请项');
}
// 获取用户信息
$userService = new UserService();
$user = $userService->getUserInfo($request->user->uid, '');
$data['applicant_id'] = $user['user']['id'];
$data['applicant_name'] = $user['user']['name'];
//$reqItemId = $data['requisition_id'];
$arr = [];
foreach ($reqItemsArr as $reqItem) {
$reqItemId = $reqItem['id'];
// 检查是否有对应的新申请项数据
if (!isset($newReqItems[$reqItemId])) {
continue;
}
$item = InvItem::where('id', $reqItem['item_id'])->first();
$manage = User::where('id',$item['manage_user_id'])->first();
$arr[] = [
'requisition_id' => $data['requisition_id'],
'requisition_item_id' => $reqItem['id'],
'item_id' => $reqItem['item_id'],
'apply_user_id' => $user['user']['id'],
'apply_user_name' => $user['user']['name'],
'return_qty' => $newReqItems[$reqItemId]['return_qty'] ?: '1',
'apply_remark' => $data['apply_remark'] ?? '',
'handler_id' => $item['manage_user_id'] ?? 0,
'handler_name' => $manage['name'] ?? '',
//'attachment' => $newReqItems[$reqItemId]['attachment'],
//'return_at' => time(),
'return_at' => $data['return_at'],
];
$data['handler_id'] = $item['manage_user_id'] ?? 0;
$data['handler_name'] = $manage['name'] ?? '';
}
$data['items'] =$arr;
return $data;
} catch (BusinessException $e) {
throw $e;
} catch (\Throwable $th) {
throw new BusinessException('创建物品归还申请失败:' . $th->getMessage());
}
}
/**
* 处理物品申请
*
* @param Request $request
* @param array $data
* @return array
*/
public function serviceStore(Request $request, array $data, string $scene = '')
{
try {
// 关联表增加事务处理
return Db::transaction(function () use ($request, $data) {
$data = $this->preProcess('Store', $data, $request);
//$dl = $this->getDataLimitFromRequest($request);
$items = $data['items'];
$attachment = $data['attachment'];
unset($data['items']);
unset($data['attachment']);
$data['order_no'] = $this->generateOrderNo();
$res = InvRequisitionReturn::create($data);
$created = [];
foreach ($items as $item) {
//$created = $this->logicCreate($item, $dl);
$item['return_id'] = $res->id;
$item['requisition_id'] = $data['requisition_id'];
$item['return_at'] = $data['return_at'];
$item['apply_remark'] = $data['apply_remark'];
$created = $this->logicCreate($item);
}
$this->processRequisitionItemReturnAttachment($res, $attachment);
InvRequisition::where('id',$request->post('requisition_id'))->update(
['status' => 14] //审批状态:10默认、11审批中、12已审批(待归还)、13拒绝、14申请归还、15归还审核中、16部分归还、17已全部归还、18已拒绝、19已审批(无需归还)
);
$this->postProcess('Store', $created);
return $res;
});
} catch (\Throwable $e) {
throw new BusinessException("创建物品申请失败:" . $e->getMessage().$e->getTraceAsString());
}
}
/**
* 更新物品归还申请前置处理
*
* @param array $data 归还申请数据
* @param Request $request 请求对象
* @return array 处理后的数据
*/
public function beforeServiceUpdate($data, Request $request): array
{
// // 模拟数据
// $data = [
// 'id' => 1,
// 'requisition_item_id' => 9,
// 'return_qty' => 1,
// 'apply_remark' => '申请备注22',
// 'attachment' => [
// [
// 'original_name' => '附件1.jpg',
// 'file_path' => '/uploads/2023/07/01/attachment1.jpg'
// ],
// [
// 'original_name' => '附件2.jpg',
// 'file_path' => '/uploads/2023/07/01/attachment2.jpg'
// ],
// [
// 'original_name' => '附件3.jpg',
// 'file_path' => '/uploads/2023/07/01/attachment3.jpg'
// ]
// ]
// ];
try {
$approved = InvRequisitionItemReturn::where('return_id',$data['id'])
->where('status','>',0)
->count();
if ($approved > 0) {
throw new BusinessException('已审核禁止编辑');
}
// 获取所有申请项ID
$reqItemIds = array_column($data['items'], 'requisition_item_id');
// 参数校验
if (empty($reqItemIds)) {
throw new BusinessException('未提供有效的申请项ID');
}
// 将申请项按 ID 重组为关联数组
$newReqItems = $this->groupArrayByKey($data['items'], 'requisition_item_id');
$data['return_at'] = is_int($data['return_at']) ? $data['return_at']: strtotime($data['return_at']);
// 获取申请项明细
$reqItemsArr = InvRequisitionItem::whereIn('id', $reqItemIds)->get()->toArray();
// 检查是否获取到有效数据
if (empty($reqItemsArr)) {
throw new BusinessException('未找到对应的申请项');
}
// 获取用户信息
$userService = new UserService();
$user = $userService->getUserInfo($request->user->uid, '');
$data['applicant_id'] = $user['user']['id'];
$data['applicant_name'] = $user['user']['name'];
//$reqItemId = $data['requisition_id'];
$arr = [];
foreach ($reqItemsArr as $reqItem) {
$reqItemId = $reqItem['id'];
// 检查是否有对应的新申请项数据
if (!isset($newReqItems[$reqItemId])) {
continue;
}
$arr[] = [
'requisition_id' => $data['requisition_id'],
'requisition_item_id' => $reqItem['id'],
'item_id' => $reqItem['item_id'],
'apply_user_id' => $user['user']['id'],
'apply_user_name' => $user['user']['name'],
'return_qty' => $newReqItems[$reqItemId]['return_qty'] ?: '1',
'apply_remark' => $data['apply_remark'] ?? '',
//'attachment' => $newReqItems[$reqItemId]['attachment'],
//'return_at' => time(),
'return_at' => $data['return_at'],
];
}
$data['items'] =$arr;
return $data;
} catch (BusinessException $e) {
throw $e;
} catch (\Throwable $th) {
throw new BusinessException('创建物品归还申请失败:' . $th->getMessage());
}
}
/**
* 更新物品归还申请
*
* @param Request $request
* @param array $data
* @return array
*/
public function serviceUpdate(Request $request, array $data, string $scene = '')
{
try {
// 关联表增加事务处理
return Db::transaction(function () use ($request, $data) {
$data = $this->preProcess('Update', $data, $request);
//$config = $this->buildQueryConfig($request, 'update');
$attachment = $data['attachment'];
unset($data['attachment']);
$items = $data['items'];
unset($data['items']);
$config = [];
$config['model'] = InvRequisitionReturn::class;
$config['where']['id'] = $request->post('id');
$returnService = new InvRequisitionReturnService();
$returnService->logicUpdateBy($config, $data);
InvRequisitionItemReturn::where('return_id',$data['id'])->delete();
foreach ($items as $item) {
$item['return_id'] = $data['id'];
$item['requisition_id'] = $data['requisition_id'];
$item['return_at'] = $data['return_at'];
$item['apply_remark'] = $data['apply_remark'];
$created = $this->logicCreate($item);
//$this->logicUpdateBy($config, $data);
}
// 获取更新后的完整记录
$updated = $returnService->logicShow($config);
// 处理附件更新
$this->processRequisitionItemReturnAttachment($updated, $attachment, true);
return $this->postProcess('Update', $updated);
});
} catch (\Throwable $e) {
throw new BusinessException("更新物品归还申请失败:" . $e->getMessage().$e->getLine().$e->getFile().$e->getTraceAsString());
}
}
/**
* 处理物品申请附件
*
* @param Model $created
* @param array $items
* @param bool $isUpdate
* @return void
*/
private function processRequisitionItemReturnAttachment(Model $created, array $attachment, bool $isUpdate = false): void
{
if ($isUpdate) {
$this->clearRequisitionItemReturnAttachment($created->id);
}
foreach ($attachment as $file) {
$attachmentData = [
'return_id' => $created->id,
'file_name' => $file['original_name'],
'file_path' => $file['file_path']
];
InvRequisitionItemReturnAttachment::create($attachmentData);
}
}
/**
* 清空物品申请附件
*
* @param int $returnId
* @return void
*/
private function clearRequisitionItemReturnAttachment(int $returnId): void
{
InvRequisitionItemReturnAttachment::where('return_id', $returnId)->delete();
}
/**
* 物品领用归还确认
*
* @param Request $request
* @param array $data
* @param string $scene
* @return array|bool
* @throws BusinessException
*/
public function serviceReturnItemConfirm(Request $request, array $data, string $scene = 'returnConfirm')
{
try {
$attachment = $request->post('attachment') ?? [];
unset($data['attachment']);
// 归还确认是业务顶层方法,这里统一使用事务
return Db::transaction(function () use ($request, $data, $scene, $attachment) {
// 1. 当前操作人信息
$userInfo = (new UserService())->getUserInfo($request->user->uid, '');
if (empty($userInfo['user']['id']) || empty($userInfo['user']['name'])) {
throw new BusinessException('无法获取当前用户信息');
}
$handlerId = (int)$userInfo['user']['id'];
$handlerName = (string)$userInfo['user']['name'];
$data['handler_id'] = $handlerId;
$data['handler_name'] = $handlerName;
// 确认状态:1=通过,2=拒绝 (按你现在用法)
$status = isset($data['status']) ? (int)$data['status'] : null;
if (!in_array($status, [1, 2], true)) {
throw new BusinessException('非法的归还处理状态');
}
// 2. 构建查询配置,定位到这条归还记录
$config = $this->buildQueryConfig($request, $scene);
// 更新时不允许前端覆盖主键
unset($data['requisition_item_return_id']);
// 3. 获取现有记录
/** @var \app\model\InvRequisitionItemReturn|null $existingRecord */
$existingRecord = $this->logicShow($config);
if (!$existingRecord) {
throw new BusinessException('未找到对应的归还申请记录');
}
// 已处理校验:只要归还记录的 status 已经是「通过或拒绝」,就不能再处理
$existingStatus = (int)($existingRecord->status ?? 0);
if (in_array($existingStatus, [1, 2], true) || !empty($existingRecord->handler_id)) {
throw new BusinessException('该归还申请已经被处理过了');
}
// 4. 更新归还记录(状态 + 审核人 + 备注等)
$updateResult = $this->logicUpdateBy($config, $data);
$resultItem = $this->logicShow($config);
InvRequisitionReturn::where('id',$resultItem['return_id'])->update(['status' => $data['status']]);
foreach ($attachment as $attac) {
Db::table('inv_requisition_return_approve_attachment')->insertGetId([
'requisition_id' => $resultItem['requisition_id'],
'return_id' => $resultItem['return_id'],
'file_name' => $attac['original_name'],
'file_path' => $attac['file_path'],
'created_at' => time(),
]);
}
if ($updateResult === 0) {
throw new BusinessException('更新归还申请失败,可能记录不存在或已被处理');
}
//InvRequisitionReturn::where('return_id', $data['id'])->update(['status' => $status]);
// 5. 若是拒绝,直接返回(不改库存、不改已归还数量)
if ($status === 2) {
// 拒绝:这里你返回 true 或者返回更新后的记录都可以
return true;
}
// 6. 审核通过:继续处理领用明细 & 库存 & 领用单状态
// 注意:requisition_item_id、item_id、return_qty 等字段在本次确认流程中不会被修改,
// 所以可以直接使用 $existingRecord 中的值(都是只读字段)
$requisitionItemId = (int)$existingRecord->requisition_item_id;
$itemId = (int)$existingRecord->item_id;
$returnQty = (int)$existingRecord->return_qty;
$requisitionId = (int)$existingRecord->requisition_id;
if ($requisitionItemId <= 0 || $itemId <= 0 || $returnQty <= 0 || $requisitionId <= 0) {
throw new BusinessException('归还申请记录缺少必要字段,无法处理');
}
// 6.1 处理领用明细
/** @var \app\model\InvRequisitionItem|null $item */
$item = InvRequisitionItem::find($requisitionItemId);
if (!$item) {
throw new BusinessException('未找到对应的领用明细记录');
}
$approvedQty = (int)($item->approved_qty ?? 0);
$returnedQty = (int)($item->returned_qty ?? 0);
if ($approvedQty <= 0) {
throw new BusinessException('该物品尚未审批出库,无法归还');
}
if ($returnedQty + $returnQty > $approvedQty) {
throw new BusinessException('归还数量超过已借数量,无法确认');
}
$item->returned_qty = $returnedQty + $returnQty;
if ($item->returned_qty >= $approvedQty) {
$item->is_finished = 1;
}
$item->updated_at = time();
if ($item->save() === false) {
throw new BusinessException('更新领用明细失败');
}
// 6.2 处理库存(入库)
(new InvItemService())->stockIn($itemId, $returnQty);
// 6.3 检查整张单是否已全部归还(只看非耗材)
$notFinishedCount = InvRequisitionItem::query()
->where('requisition_id', $requisitionId)
->where('is_consumable_snap', 0)
->where('is_finished', 0)
->count();
if ($notFinishedCount === 0) {
// 全部非耗材都已归还
$affected = InvRequisition::where('id', $requisitionId)->update([
'is_returned' => 1,
'status' => 17,
'updated_at' => time(),
]);
// Eloquent::update 返回的是影响行数,0 表示未更新到任何行
if ($affected === 0) {
throw new BusinessException('更新领用单状态失败,可能记录不存在');
}
}else{
// 非耗材未全部归还
$affected = InvRequisition::where('id', $requisitionId)->update([
'status' => 16,
'updated_at' => time(),
]);
}
// 7. 返回处理结果(保持你原来的 postProcess 习惯)
return $this->postProcess(ucfirst($scene), $existingRecord);
});
} catch (BusinessException $e) {
throw $e;
} catch (\Throwable $e) {
throw new BusinessException("确认归还物品失败:" . $e->getMessage());
}
}
/**
* 物品领用归还确认
*
* @param Request $request
* @param array $data
* @param string $scene
* @return array|bool
* @throws BusinessException
*/
public function serviceReturnConfirm(Request $request, array $data, string $scene = 'returnConfirm')
{
try {
if(empty($data['return_id'])) {
$data['return_id'] = $data['requisition_item_return_id'];
}
$attachment = $request->post('attachment') ?? [];
unset($data['attachment']);
// 归还确认是业务顶层方法,这里统一使用事务
return Db::transaction(function () use ($request, $data, $scene, $attachment) {
// 1. 当前操作人信息
$userInfo = (new UserService())->getUserInfo($request->user->uid, '');
if (empty($userInfo['user']['id']) || empty($userInfo['user']['name'])) {
throw new BusinessException('无法获取当前用户信息');
}
$handlerId = (int)$userInfo['user']['id'];
$handlerName = (string)$userInfo['user']['name'];
$data['handler_id'] = $handlerId;
$data['handler_name'] = $handlerName;
// 确认状态:1=通过,2=拒绝
$status = isset($data['status']) ? (int)$data['status'] : null;
if (!in_array($status, [1, 2], true)) {
throw new BusinessException('非法的归还处理状态');
}
// 2. 构建查询配置,定位到这条归还记录
$returnService = new InvRequisitionReturnService();
$return = InvRequisitionReturn::where('id', $data['return_id'])->first();
if (!$return) {
throw new BusinessException('未找到对应的领用归还记录');
}
$requisitionId = (int)$return->requisition_id;
$existingStatus = (int)($return->status ?? 0);
if (in_array($existingStatus, [1, 2], true)) {
throw new BusinessException('该归还申请已经被处理过了');
}
$return->status = $status;
$return->save();
if($status == 1){
//更改库存
$returnItem = Db::table('inv_requisition_item_return')
->where('return_id', $return->id)
->get();
foreach ($returnItem as $item) {
//$invItem = InvItem::where('id', $item->item_id)->first();
(new InvItemService())->stockIn($item->item_id,$item->return_qty,['change_type'=>InvStockLog::CHANGE_TYPE_IN_RETURN,
'biz_type'=>InvStockLog::BIZ_TYPE_RETURN,]);
}
}
// 保存归还审核附件
foreach ($attachment as $attac) {
Db::table('inv_requisition_return_approve_attachment')->insertGetId([
'requisition_id' => $requisitionId,
'return_id' => $data['return_id'],
'file_name' => $attac['original_name'],
'file_path' => $attac['file_path'],
'created_at' => time(),
]);
}
//查询归还明细
$itemReturns = InvRequisitionItemReturn::where('return_id', $data['return_id'])->get();
foreach ($itemReturns as $itemReturn) {
$itemReturn->status = $status;
$itemReturn->handle_remark = $data['handle_remark'];
$itemReturn->save();
// 5. 若是拒绝,直接跳出(不改库存、不改已归还数量)
if ($status === 2) {
// 拒绝:这里你返回 true 或者返回更新后的记录都可以
continue;
}
// 6. 审核通过:继续处理领用明细 & 库存 & 领用单状态
// 注意:requisition_item_id、item_id、return_qty 等字段在本次确认流程中不会被修改,
// 所以可以直接使用 $existingRecord 中的值(都是只读字段)
$requisitionItemId = (int)$itemReturn->requisition_item_id;
$itemId = (int)$itemReturn->item_id;
$returnQty = (int)$itemReturn->return_qty;
if ($requisitionItemId <= 0 || $itemId <= 0 || $returnQty <= 0 || $requisitionId <= 0) {
throw new BusinessException('归还申请记录缺少必要字段,无法处理');
}
// 6.2 处理领用明细
/** @var \app\model\InvRequisitionItem|null $item */
$item = InvRequisitionItem::find($requisitionItemId);
if (!$item) {
throw new BusinessException('未找到对应的领用明细记录');
}
$approvedQty = (int)($item->approved_qty ?? 0);
$returnedQty = (int)($item->returned_qty ?? 0);
if ($approvedQty <= 0) {
throw new BusinessException('该物品尚未审批出库,无法归还');
}
if ($returnedQty + $returnQty > $approvedQty) {
throw new BusinessException('归还数量超过已借数量,无法确认');
}
$item->returned_qty = $returnedQty + $returnQty;
if ($item->returned_qty >= $approvedQty) {
$item->is_finished = 1;
}
$item->updated_at = time();
if ($item->save() === false) {
throw new BusinessException('更新领用明细失败');
}
// 6.3 处理库存(入库)
(new InvItemService())->stockIn($itemId, $returnQty);
}
// 5. 若是拒绝,直接返回(不修改领用申请状态)
if ($status === 2) {
// 拒绝:这里你返回 true 或者返回更新后的记录都可以
return $return;
}
// 6.3 检查整张单是否已全部归还(只看非耗材)
$notFinishedCount = InvRequisitionItem::query()
->where('requisition_id', $requisitionId)
->where('is_consumable_snap', 0)
->where('is_finished', 0)
->count();
if ($notFinishedCount === 0) {
// 全部非耗材都已归还
$affected = InvRequisition::where('id', $requisitionId)->update([
'is_returned' => 1,
'status' => 17,
'updated_at' => time(),
]);
// Eloquent::update 返回的是影响行数,0 表示未更新到任何行
if ($affected === 0) {
throw new BusinessException('更新领用单状态失败,可能记录不存在');
}
}else{
// 非耗材未全部归还
$affected = InvRequisition::where('id', $requisitionId)->update([
'status' => 16,
'updated_at' => time(),
]);
}
// 7. 返回处理结果(保持你原来的 postProcess 习惯)
//return $this->postProcess(ucfirst($scene), $existingRecord);
return $return;
});
} catch (BusinessException $e) {
throw $e;
} catch (\Throwable $e) {
throw new BusinessException("确认归还物品失败:" . $e->getMessage());
}
}
/**
* 构建查询配置
*
* @param Request $request
* @param string $scene
* @return array
*/
protected function buildQueryConfig(Request $request, string $scene = 'default'): array
{
$p = $request->all();
$config = [];
$config['model'] = $this->modelClass;
switch ($scene) {
case 'returnConfirm':
$config['where']['id'] = $p['requisition_item_return_id'];
break;
case 'index':
// if(isset($p['status']) && $p['status']!=''){
// $config['where']['status'] = $p['status'];
// }
// if(isset($p['handler_id']) && $p['handler_id']=='self'){
// $config['where']['applicant_id'] = $request->user->uid;
// }else{
// $config['where']['handler_id'] = $request->user->uid;
// }
// $config['order']['id'] = 'desc';
break;
case 'show':
$config['where']['id'] = $p['id'];
break;
case 'store':
break;
case 'update':
$config['where']['id'] = $p['id'];
break;
case 'destroy':
$config['where']['id'] = $p['id'];
break;
}
return $config;
}
/** 分页列表 */
public function serviceList(Request $request, string $scene = 'index')
{
$InvRequisitionReturn = new InvRequisitionReturnService();
return $InvRequisitionReturn->serviceList($request, $scene);
}
/** 查看单条 */
public function serviceShow(Request $request, string $scene = 'show')
{
$id = $request->get('id');
$return = InvRequisitionReturn::where('id', $id)->first();
$return['item_return'] = InvRequisitionItemReturn::where('return_id', $return['id'])->orderBy('id', 'desc')->get();
return $return;
}
/**
* 生成申请单号
*
* @return string
*/
private function generateOrderNo(): string
{
return 'RR' . date('Ymd') . strtoupper(substr(uniqid(), -6));
}
}
@@ -0,0 +1,31 @@
<?php
namespace app\common\service;
use app\common\validate\InvRequisitionItemValidate;
use app\common\traits\crud\CrudServiceTrait;
use app\common\model\InvRequisitionItem;
class InvRequisitionItemService
{
use CrudServiceTrait;
/**
* 指定当前要操作的模型
*/
protected string $modelClass = InvRequisitionItem::class;
/**
* 验证器
*/
protected $validator = InvRequisitionItemValidate::class;
/**
* 获取验证器
*/
public function getValidator()
{
return new $this->validator;
}
}
@@ -0,0 +1,93 @@
<?php
namespace app\common\service;
use app\common\validate\InvRequisitionReturnValidate;
use app\common\traits\crud\CrudServiceTrait;
use app\common\model\InvRequisitionReturn;
use support\exception\BusinessException;
use support\Request;
class InvRequisitionReturnService
{
use CrudServiceTrait;
/**
* 指定当前要操作的模型
*/
protected string $modelClass = InvRequisitionReturn::class;
/**
* 验证器
*/
protected $validator = InvRequisitionReturnValidate::class;
/**
* 获取验证器
*/
public function getValidator()
{
return new $this->validator;
}
public function serviceReturnConfirm(Request$request)
{
$status = $request->post('status');
if(in_array($status,[1,2])){
throw new BusinessException("状态不正确");
}
$requisitionReturn = InvRequisitionReturn::where(['id' => $request['id']])->first();
if (!$requisitionReturn && $requisitionReturn->handler_id == $request->user->uid) {
$requisitionReturn->status = $status;
return $requisitionReturn->save();
}else{
throw new BusinessException("审核示例不正确或非物品管理员");
}
}
/**
* 构建查询配置
*
* @param Request $request
* @param string $scene
* @return array
*/
protected function buildQueryConfig(Request $request, string $scene = 'default'): array
{
$p = $request->all();
$config = [];
$config['model'] = $this->modelClass;
switch ($scene) {
case 'returnConfirm':
$config['where']['id'] = $p['requisition_item_return_id'];
break;
case 'index':
if(isset($p['status']) && $p['status']!=''){
$config['where']['status'] = $p['status'];
}
if (isset($p['handler_id']) && $p['handler_id'] == 'self') {
$config['where']['applicant_id'] = $request->user->uid;
} else {
$config['where']['handler_id'] = $request->user->uid;
}
$config['order']['id'] = 'desc';
break;
case 'show':
$config['where']['id'] = $p['id'];
break;
case 'store':
break;
case 'update':
$config['where']['id'] = $p['id'];
break;
case 'destroy':
$config['where']['id'] = $p['id'];
break;
}
return $config;
}
}
@@ -0,0 +1,629 @@
<?php
namespace app\common\service;
use app\common\model\InvItem;
use app\common\model\InvStockLog;
use app\common\validate\InvRequisitionValidate;
use app\common\traits\crud\CrudServiceTrait;
use app\common\model\InvRequisition;
use app\common\model\InvRequisitionAttachment;
use app\common\model\InvRequisitionItem;
use Illuminate\Database\Eloquent\Model;
use support\Request;
use support\Db;
use support\exception\BusinessException;
class InvRequisitionService
{
use CrudServiceTrait;
/**
* 指定当前要操作的模型
*/
protected string $modelClass = InvRequisition::class;
/**
* 验证器
*/
protected $validator = InvRequisitionValidate::class;
/**
* 获取验证器
*/
public function getValidator()
{
return new $this->validator;
}
/**
* 构造 CrudUtil 查询配置
*/
protected function buildQueryConfig(Request $request, string $scene = 'default'): array
{
$p = $request->all();
$config = [];
$config['model'] = $this->modelClass;
switch ($scene) {
case 'default':
$config['where']['status'] = 1;
$config['order']['sort'] = 'desc';
$config['order']['id'] = 'desc';
break;
case 'index':
if(isset($p['status']) && $p['status']){
$config['where']['status'] = $p['status'];
}
if (isset($p['applicant_id']) && $p['applicant_id'] == 'self') { //自己的领用申请
$config['where']['applicant_id'] = $request->user->uid;
} elseif (isset($p['applicant_id']) && $p['applicant_id']) { //指定人员的申请
$config['where']['applicant_id'] = $p['applicant_id'];
}
/* else { //审批人对应的记录
$config['with'] = ['approval'];
$config['where']['approval.approver_id'] = $request->user->uid;
//$config['where']['approval.action'] = 0;
}*/
if(isset($p['phase']) && $p['phase'] == 'approve'){//借用审批中
$config['where']['status'] = ['in',[10,11]];
if(isset($p['scene']) && $p['scene'] == 'approval'){
$config['with'] = ['approval'];
$config['where']['approval.action'] = 0;
}
}elseif(isset($p['phase']) && $p['phase'] == 'repaid'){//已全部归还
$config['where']['status'] = 17;
}elseif(isset($p['phase']) && $p['phase'] == 'unreturned'){ //未归还
$config['where']['status'] = ['in',[11,12,13,14,15,16,18,19]];
}
//审核列表
elseif(isset($p['phase']) && $p['phase'] == 'pending'){ //待审核
$config['where']['status'] = ['in',[10,11]];
$config['with'] = ['approval'];
$config['where']['approval.approver_id'] = $request->user->uid;
$config['where']['approval.action'] = 0;
}elseif(isset($p['phase']) && $p['phase'] == 'approved'){ //已通过
$config['where']['status'] = ['in',[11,12,14,15,16,17,18,19]];
$config['with'] = ['approval'];
$config['where']['approval.approver_id'] = $request->user->uid;
$config['where']['approval.action'] = [">",0];
}elseif(isset($p['phase']) && $p['phase'] == 'refuse'){ //拒绝,未通过
$config['where']['status'] = ['in',[13]];
}
$config['order']['id'] = 'desc';
break;
case 'show':
$config['where']['id'] = $p['id'];
break;
case 'store':
break;
case 'update':
$config['where']['id'] = $p['id'];
break;
case 'destroy':
$config['where']['id'] = $p['id'];
break;
}
return $config;
}
/**
* 创建物品申请 前置
*
* @param array $data
* @param Request $request
* @return array
*/
public function beforeServiceStore($data, Request $request): array
{
$user = (new UserService())->getUserInfo($request->user->uid, '');
if (empty($user['user']['id']) || empty($user['user']['name'])) {
throw new BusinessException('无法获取当前用户信息');
}
$data['order_no'] = $this->generateOrderNo();
$data['applicant_id'] = (int)$user['user']['id'];
$data['applicant_name'] = (string)$user['user']['name'];
$data['leader_id'] = $user['user']['report_to'];
$data['status'] = 10;
//查询物品管理人是否是一个
if(count($data['items'])>=2){
$manage_user = [];
foreach ($data['items'] as $item) {
$manage_user[] = InvItem::where('id',$item['item_id'])->value('manage_user_id');
}
$manage_user = array_unique($manage_user);
if(count($manage_user)>1){
throw new BusinessException("物品管理员不是同一人");
}
}
return $data;
}
/**
* 处理物品申请
*
* @param Request $request
* @param array $data
* @return array
*/
public function serviceStore(Request $request, array $data, string $scene = '')
{
try {
// 关联表增加事务处理
return Db::transaction(function () use ($request, $data) {
$data = $this->preProcess('Store', $data, $request);
$dl = $this->getDataLimitFromRequest($request);
$created = $this->logicCreate($data, $dl);
// 处理物品申请项
$this->processRequisitionItems($created, $data['items']);
// 处理物品申请附件
$this->processRequisitionAttachmentItems($created, $data['attachment']);
$res = $this->postProcess('Store', $created);
$manage_user_id = InvItem::where('id',$data['items'][0]['item_id'])->value('manage_user_id');
$approval = [
'id' => $res['id'], //申请单id
'applicant_id' => $res['applicant_id'], //申请人id
'manage_user_id' => $manage_user_id,
];
$this->startApproval($approval);
return $res;
});
} catch (\Throwable $e) {
throw new BusinessException("创建物品申请失败:".$e->getMessage());
}
}
/**
* 启动审批
*/
public function startApproval($requisition)
{
// 启动审批
//$appro = new \app\common\utils\approval\service\ApprovalFlowService();
$appro = new \app\common\service\InvRequisitionApproveService();
$formData = [
'manage_user_id' => $requisition['manage_user_id'],
];
return $appro->start('inv_requisition', 'inv_requisition', $requisition['id'], $formData, $requisition['applicant_id']);
}
/**
* 更新物品申请
*
* @param Request $request
* @param array $data
* @return array
*/
public function serviceUpdate(Request $request, array $data, string $scene = '')
{
try {
// 关联表增加事务处理
return Db::transaction(function () use ($request, $data) {
$data = $this->preProcess('Update', $data, $request);
$config = $this->buildQueryConfig($request, 'update');
// 获取申请项/附件
$items = $data['items'];
$attachment = $data['attachment'];
unset($data['items']);
unset($data['attachment']);
// 执行更新操作
$this->logicUpdateBy($config, $data);
// 获取更新后的完整记录
$updated = $this->logicShow($config);
// 重新创建新的申请项
$this->processRequisitionItems((object)$updated, $items, true);
// 重新创建新的附件
$this->processRequisitionAttachmentItems((object)$updated, $attachment, true);
return $this->postProcess('Update', $updated);
});
} catch (\Throwable $e) {
throw new BusinessException("更新物品申请失败:" . $e->getMessage()."|".$e->getLine()."|".$e->getFile());
}
}
/**
* 处理物品申请项
*
* @param Model $created
* @param array $items
* @return void
*/
private function processRequisitionItems(Model $created, array $items, bool $isUpdate = false): void
{
if ($isUpdate) {
$this->clearRequisitionItems($created->id);
}
foreach ($items as $item) {
$invitem = InvItem::where('id',$item)->first();
$itemData = [
'requisition_id' => $created->id,
'item_id' => $item['item_id'],
'item_name_snap' => $item['item_name_snap'], // 修改:使用 snap 字段
'spec_model_snap' => $item['spec_model_snap']??'', // 修改:使用 snap 字段
'unit_snap' => $item['unit_snap']??'', // 修改:使用 snap 字段
'is_consumable_snap' => $item['is_consumable_snap']?? ($invitem['is_consumable_snap']??0), // 修改:使用 snap 字段
'request_qty' => (int) $item['request_qty'] ?? 1, // 动态获取数量,转为整型
'approved_qty' => (int) $item['request_qty'] ?? 1, // 初始审批数量等于申请数量
'remark' => $item['remark'] ?? ''
];
InvRequisitionItem::create($itemData);
}
}
/**
* 处理物品申请项
*
* @param Model $created
* @param array $items
* @return void
*/
private function processRequisitionAttachmentItems(Model $created, array $attachment, bool $isUpdate = false): void
{
if ($isUpdate) {
$this->clearRequisitionAttachmentItems($created->id);
}
foreach ($attachment as $file) {
$attachmentData = [
'requisition_id' => $created->id,
'file_name' => $file['original_name'],
'file_path' => $file['file_path']
];
InvRequisitionAttachment::create($attachmentData);
}
}
/**
* 删除申请项
*
* @param int $requisitionId
* @return void
*/
public function clearRequisitionItems(int $requisitionId): void
{
InvRequisitionItem::where('requisition_id', $requisitionId)->delete();
}
/**
* 删除申请项
*
* @param int $requisitionId
* @return void
*/
public function clearRequisitionAttachmentItems(int $requisitionId): void
{
InvRequisitionAttachment::where('requisition_id', $requisitionId)->delete();
}
/**
* 生成申请单号
*
* @return string
*/
private function generateOrderNo(): string
{
return 'RQ' . date('Ymd') . strtoupper(substr(uniqid(), -6));
}
/**
* 扫码领取物品
*
* @param Request $request
* @param array $data
* @return array
* @throws BusinessException
*/
public function qrClaim(Request $request, array $data): array
{
return Db::transaction(function () use ($request, $data) {
$itemId = (int)($data['item_id'] ?? 0);
$qty = (int)($data['qty'] ?? 1);
if ($itemId <= 0) {
throw new BusinessException('物品ID无效');
}
if ($qty <= 0) {
throw new BusinessException('领取数量必须大于0');
}
// 获取物品信息
$item = InvItem::find($itemId);
if (!$item) {
throw new BusinessException('物品不存在');
}
// 检查是否允许扫码领取
if (!$item->allow_qr_claim) {
throw new BusinessException('该物品不允许扫码领取');
}
// 获取用户信息
$user = (new UserService())->getUserInfo($request->user->uid, '');
$applicantId = (int)$user['user']['id'];
$applicantName = (string)$user['user']['name'];
// 检查扫码领取数量限制
if ($item->qr_code_num > 0) {
// 查询该用户已扫码领取该物品的数量
$claimedQty = InvRequisitionItem::whereHas('requisition', function ($q) use ($applicantId) {
$q->where('applicant_id', $applicantId)
->where('claim_type', InvRequisition::CLAIM_TYPE_QR);
})->where('item_id', $itemId)
->sum('approved_qty');
if (($claimedQty + $qty) > $item->qr_code_num) {
throw new BusinessException("扫码领取数量超限,每人最多可领取 {$item->qr_code_num} {$item->unit},已领取 {$claimedQty} {$item->unit}");
}
}
// 检查库存
if ($item->stock_qty < $qty) {
throw new BusinessException('库存不足,当前库存: ' . $item->stock_qty);
}
// 创建领用申请记录(直接完成)
$requisition = InvRequisition::create([
'order_no' => $this->generateOrderNo(),
'applicant_company_id' => $user['user']['company_id'] ?? 0,
'dept_id' => $user['user']['dept_id'] ?? 0,
'applicant_id' => $applicantId,
'applicant_name' => $applicantName,
'leader_id' => $user['user']['report_to'] ?? 0,
'manager_id' => $item->manage_user_id,
'borrow_reason' => '扫码领取',
'use_start_date' => date('Y-m-d', time()),
'use_end_date' => date('Y-m-d', time()),
'status' => 19, // 已审批(无需归还)
'claim_type' => InvRequisition::CLAIM_TYPE_QR,
'qr_claim_time' => time(),
'remark' => $data['remark'] ?? '扫码领取',
]);
// 创建领用明细
InvRequisitionItem::create([
'requisition_id' => $requisition->id,
'item_id' => $itemId,
'item_name_snap' => $item->item_name,
'spec_model_snap' => $item->spec_model,
'unit_snap' => $item->unit,
'is_consumable_snap' => $item->is_consumable,
'request_qty' => $qty,
'approved_qty' => $qty,
'remark' => '扫码领取',
]);
// 扣减库存
(new InvItemService())->stockOut($itemId, $qty, false, [
'biz_type' => InvStockLog::BIZ_TYPE_QR_CLAIM,
'biz_no' => $requisition->order_no,
'operator_id' => $applicantId,
'operator_name' => $applicantName,
'remark' => '扫码领取',
]);
return $requisition->toArray();
});
}
/**
* 管理员代领(支持多人)
*
* @param Request $request
* @param array $data
* @return array
* @throws BusinessException
*/
public function proxyClaim(Request $request, array $data): array
{
return Db::transaction(function () use ($request, $data) {
$itemId = (int)($data['item_id'] ?? 0);
$qty = (int)($data['qty'] ?? 1);
$targetUserIds = $data['target_user_id'];
if ($itemId <= 0) {
throw new BusinessException('物品ID无效');
}
if ($qty <= 0) {
throw new BusinessException('领取数量必须大于0');
}
if (empty($targetUserIds)) {
throw new BusinessException('请选择代领用户');
}
// 获取物品信息
$item = InvItem::find($itemId);
if (!$item) {
throw new BusinessException('物品不存在');
}
$targetUserIds = explode(',', $targetUserIds);
// 获取操作人(管理员)信息
$proxyUser = (new UserService())->getUserInfo($request->user->uid, '');
$proxyUserId = (int)$proxyUser['user']['id'];
$proxyUserName = (string)$proxyUser['user']['name'];
// 验证操作人是否是物品管理员
if ($item->manage_user_id != $proxyUserId) {
throw new BusinessException('您不是该物品的管理员,无权代领');
}
// 计算总需求量
$totalQty = $qty * count($targetUserIds);
// 检查库存
if ($item->stock_qty < $totalQty) {
throw new BusinessException('库存不足,当前库存: ' . $item->stock_qty . ',需要: ' . $totalQty);
}
$createdRequisitions = [];
$remark = $data['remark'] ?? '管理员代领';
// 为每个目标用户创建领用记录
foreach ($targetUserIds as $targetUserId) {
$targetUserId = (int)$targetUserId;
if ($targetUserId <= 0) {
continue;
}
// 获取目标用户信息
$targetUser = (new UserService())->getUserInfo($targetUserId, '');
if (!$targetUser['user']) {
throw new BusinessException('用户ID ' . $targetUserId . ' 不存在');
}
// 创建领用申请记录(直接完成)
$requisition = InvRequisition::create([
'order_no' => $this->generateOrderNo(),
'applicant_company_id' => $targetUser['user']['company_id'] ?? 0,
'dept_id' => $targetUser['user']['dept_id'] ?? 0,
'applicant_id' => $targetUserId,
'applicant_name' => $targetUser['user']['name'],
'leader_id' => $targetUser['user']['report_to'] ?? 0,
'manager_id' => $item->manage_user_id,
'borrow_reason' => $data['borrow_reason'] ?? '管理员代领',
'use_start_date' => date('Y-m-d', time()),
'use_end_date' => date('Y-m-d', time()),
'status' => 19, // 已审批(无需归还)
'claim_type' => InvRequisition::CLAIM_TYPE_PROXY,
'proxy_claim_user_id' => $proxyUserId,
'proxy_claim_user_name' => $proxyUserName,
'remark' => $remark,
]);
// 创建领用明细
InvRequisitionItem::create([
'requisition_id' => $requisition->id,
'item_id' => $itemId,
'item_name_snap' => $item->item_name,
'spec_model_snap' => $item->spec_model,
'unit_snap' => $item->unit,
'is_consumable_snap' => $item->is_consumable,
'request_qty' => $qty,
'approved_qty' => $qty,
'remark' => $remark,
]);
$createdRequisitions[] = $requisition->toArray();
}
// 一次性扣减总库存
(new InvItemService())->stockOut($itemId, $totalQty, false, [
'biz_type' => InvStockLog::BIZ_TYPE_PROXY_CLAIM,
'biz_no' => 'PROXY_' . time(),
'operator_id' => $proxyUserId,
'operator_name' => $proxyUserName,
'remark' => '管理员代领(' . count($targetUserIds) . '人×' . $qty . ')',
]);
return [
'created_count' => count($createdRequisitions),
'requisitions' => $createdRequisitions,
'total_qty' => $totalQty,
];
});
}
/**
* 查询扫码/代领记录列表
*
* @param Request $request
* @return array
*/
public function qrClaimsList(Request $request): array
{
$query = InvRequisition::query()->with(['items']);
// 只查询扫码领取和代领记录
$query->whereIn('claim_type', [
InvRequisition::CLAIM_TYPE_QR,
InvRequisition::CLAIM_TYPE_PROXY
]);
// 按申请人筛选(移动端"我的记录"功能)
if ($request->get('applicant_id') === 'self') {
$query->where('applicant_id', $request->user->uid);
} elseif (!empty($request->get('applicant_id'))) {
$query->where('applicant_id', $request->get('applicant_id'));
}
// 按领取方式筛选
if (!empty($request->get('claim_type'))) {
$query->where('claim_type', $request->get('claim_type'));
}
// 按代领操作人筛选
if (!empty($request->get('proxy_user_id'))) {
$query->where('proxy_claim_user_id', $request->get('proxy_user_id'));
}
// 按物品ID筛选
if (!empty($request->get('item_id'))) {
$query->whereHas('items', function ($q) use ($request) {
$q->where('item_id', $request->get('item_id'));
});
}
// 时间范围筛选
if (!empty($request->get('start_time'))) {
$query->where('created_at', '>=', strtotime($request->get('start_time')));
}
if (!empty($request->get('end_time'))) {
$query->where('created_at', '<=', strtotime($request->get('end_time')));
}
// 排序
$query->orderBy('id', 'desc');
// 分页
$perPage = $request->get('size', 20);
$page = $request->get('current', 1);
$result = $query->paginate($perPage, ['*'], 'page', $page);
return [
'records' => $result->getCollection()->map(function ($req) {
return [
'id' => $req->id,
'order_no' => $req->order_no,
'applicant_id' => $req->applicant_id,
'applicant_name' => $req->applicant_name,
'claim_type' => $req->claim_type,
'claim_type_text' => InvRequisition::getClaimTypeText($req->claim_type),
'qr_claim_time' => $req->qr_claim_time ? date('Y-m-d H:i:s', $req->qr_claim_time) : null,
'proxy_claim_user_id' => $req->proxy_claim_user_id,
'proxy_claim_user_name' => $req->proxy_claim_user_name,
'status' => $req->status,
'borrow_reason' => $req->borrow_reason,
'remark' => $req->remark,
'created_at' => $req->created_at ? $req->created_at->format('Y-m-d H:i:s') : null,
'items' => $req->items->map(function ($item) {
return [
'item_id' => $item->item_id,
'item_name_snap' => $item->item_name_snap,
'approved_qty' => $item->approved_qty,
];
})->toArray(),
];
})->toArray(),
'total' => $result->total(),
'current' => $result->currentPage(),
'size' => $result->perPage(),
];
}
}
+119
View File
@@ -0,0 +1,119 @@
<?php
namespace app\common\service;
use app\common\model\InvStockLog;
use support\exception\BusinessException;
/**
* 物品库存变化记录服务
*/
class InvStockLogService
{
/**
* 记录库存变化
*
* @param array $data 日志数据
* @return InvStockLog
* @throws BusinessException
*/
public function record(array $data): InvStockLog
{
$required = ['item_id', 'change_type', 'change_qty', 'before_qty', 'after_qty'];
foreach ($required as $field) {
if (!isset($data[$field])) {
throw new BusinessException("库存日志缺少必需字段: {$field}");
}
}
return InvStockLog::create([
'company_id' => $data['company_id'] ?? 0,
'item_id' => $data['item_id'],
'item_name_snap' => $data['item_name_snap'] ?? null,
'change_type' => $data['change_type'],
'change_qty' => $data['change_qty'],
'before_qty' => $data['before_qty'],
'after_qty' => $data['after_qty'],
'biz_type' => $data['biz_type'] ?? null,
'biz_no' => $data['biz_no'] ?? null,
'operator_id' => $data['operator_id'] ?? null,
'operator_name' => $data['operator_name'] ?? null,
'remark' => $data['remark'] ?? null,
'updated_at' => time(),
'created_at' => time(),
]);
}
/**
* 查询库存变化记录列表
*
* @param array $params 查询参数
* @return array
*/
public function getList(array $params): array
{
$query = InvStockLog::query()->with(['item']);
// 物品ID筛选
if (!empty($params['item_id'])) {
$query->where('item_id', $params['item_id']);
}
// 变动类型筛选
if (!empty($params['change_type'])) {
$query->where('change_type', $params['change_type']);
}
// 业务类型筛选
if (!empty($params['biz_type'])) {
$query->where('biz_type', $params['biz_type']);
}
// 单据号筛选
if (!empty($params['biz_no'])) {
$query->where('biz_no', 'like', '%' . $params['biz_no'] . '%');
}
// 时间范围筛选
if (!empty($params['start_time'])) {
$query->where('created_at', '>=', strtotime($params['start_time']));
}
if (!empty($params['end_time'])) {
$query->where('created_at', '<=', strtotime($params['end_time']));
}
// 排序
$query->orderBy('id', 'desc');
// 分页
$perPage = $params['size'] ?? 20;
$page = $params['current'] ?? 1;
$result = $query->paginate($perPage, ['*'], 'page', $page);
return [
'records' => $result->getCollection()->map(function ($log) {
return [
'id' => $log->id,
'item_id' => $log->item_id,
'item_name' => $log->item_name_snap,
'change_type' => $log->change_type,
'change_type_text' => InvStockLog::getChangeTypeText($log->change_type),
'change_qty' => $log->change_qty,
'before_qty' => $log->before_qty,
'after_qty' => $log->after_qty,
'biz_type' => $log->biz_type,
'biz_type_text' => $log->biz_type ? InvStockLog::getBizTypeText($log->biz_type) : null,
'biz_no' => $log->biz_no,
'operator_id' => $log->operator_id,
'operator_name' => $log->operator_name,
'remark' => $log->remark,
'created_at' => date('Y-m-d H:i:s', is_int($log->created_at)?$log->created_at:strtotime($log->created_at)),
];
})->toArray(),
'total' => $result->total(),
'current' => $result->currentPage(),
'size' => $result->perPage(),
];
}
}
+187
View File
@@ -0,0 +1,187 @@
<?php
namespace app\common\service;
use support\exception\BusinessException;
/**
* 二维码生成服务
*/
class QrCodeService
{
/**
* 生成物品二维码
*
* @param int $itemId 物品ID
* @param string $appUrl 应用访问地址
* @return string 二维码内容URL
*/
public function generateItemQrCode(int $itemId, string $appUrl): string
{
//return rtrim($appUrl, '/') . '/mobile/inv/claim/' . $itemId;
return "https://mweb-fdesk.hnxinyouhe.com/pages/scan/external-scan?id=" . $itemId;
}
/**
* 生成二维码图片并上传到 OSS
*
* @param int $itemId 物品ID
* @param string $content 二维码内容
* @param string $fileName 文件名
* @return array 返回OSS路径和完整URL
* @throws BusinessException
*/
public function generateAndUploadQrCode(int $itemId, string $content, string $fileName): array
{
// 使用 endroid/qr-code 库生成二维码
// 如果未安装,使用 API 方式生成
$tempPath = $this->generateQrCodeImage($content, $fileName);
if (!$tempPath) {
throw new BusinessException('二维码生成失败');
}
try {
// 上传到 OSS
$ossService = new OssService();
$ossPath = 'qr/' . $fileName;
$ossUrl = $ossService->uploadImage($tempPath, $ossPath);
// 删除临时文件
if (file_exists($tempPath)) {
unlink($tempPath);
}
return [
'path' => $ossPath,
'url' => $ossUrl,
];
} catch (\Throwable $e) {
// 删除临时文件
if (file_exists($tempPath)) {
unlink($tempPath);
}
throw new BusinessException('二维码上传失败:' . $e->getMessage());
}
}
/**
* 生成二维码图片(使用 QR Server API 或本地库)
*
* @param string $content 二维码内容
* @param string $fileName 文件名
* @return string 临时文件路径,失败返回 false
*/
private function generateQrCodeImage(string $content, string $fileName)
{
$tempDir = runtime_path() . '/qr_temp/';
if (!is_dir($tempDir)) {
mkdir($tempDir, 0755, true);
}
$tempPath = $tempDir . $fileName;
// 方式1: 使用 endroid/qr-code 库(推荐)
if (class_exists('\Endroid\QrCode\QrCode')) {
return $this->generateWithEndroid($content, $tempPath);
}
// 方式2: 使用在线 API(备用方案)
return $this->generateWithApi($content, $tempPath);
}
/**
* 使用 endroid/qr-code 生成二维码
*/
private function generateWithEndroid(string $content, string $tempPath): string
{
try {
// 5.x 版本使用静态工厂方法
if (method_exists(\Endroid\QrCode\QrCode::class, 'create')) {
$qrCode = \Endroid\QrCode\QrCode::create($content)
->setSize(300)
->setMargin(10);
} else {
// 4.x 及以下版本
$qrCode = new \Endroid\QrCode\QrCode($content);
if (method_exists($qrCode, 'setSize')) {
$qrCode->setSize(300);
}
if (method_exists($qrCode, 'setMargin')) {
$qrCode->setMargin(10);
}
}
// 获取 Writer
if (class_exists('\Endroid\QrCode\Writer\PngWriter')) {
$writer = new \Endroid\QrCode\Writer\PngWriter();
} else {
// 旧版本可能使用不同的命名空间
$writer = new \Endroid\QrCode\Writer();
}
$result = $writer->write($qrCode);
// 保存文件
if (method_exists($result, 'getString')) {
file_put_contents($tempPath, $result->getString());
} else {
// 更旧版本可能直接返回字符串
file_put_contents($tempPath, $result);
}
return $tempPath;
} catch (\Throwable $e) {
// 如果 endroid 库使用失败,回退到在线 API
error_log('endroid/qr-code 生成失败: ' . $e->getMessage() . ',使用在线 API 备用方案');
return $this->generateWithApi($content, $tempPath);
}
}
/**
* 使用在线 API 生成二维码(备用方案)
*/
private function generateWithApi(string $content, string $tempPath): string
{
$apiUrl = 'https://api.qrserver.com/v1/create-qr-code/?';
$params = [
'size' => '300x300',
'data' => $content,
'margin' => '10',
];
$url = $apiUrl . http_build_query($params);
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 30);
$imageData = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode !== 200 || !$imageData) {
return false;
}
file_put_contents($tempPath, $imageData);
return $tempPath;
}
/**
* 删除 OSS 上的二维码文件
*
* @param string $ossPath OSS路径
* @return bool
*/
public function deleteQrCode(string $ossPath): bool
{
try {
$ossService = new OssService();
return $ossService->deleteFile($ossPath);
} catch (\Throwable $e) {
return false;
}
}
}
+3
View File
@@ -47,6 +47,9 @@ class ResourceService
$config['order']['id'] = 'asc';
break;
case 'menuList':
if(isset($p['is_home']) && $p['is_home']==1){
$config['where']['is_home'] = 1;
}
$config['where']['status'] = 1;
$config['where']['type'] = ['!=', 'permission'];
$config['order']['sort'] = 'asc';
+46 -20
View File
@@ -227,6 +227,8 @@ class StockOrdersService
if (!$order->save()) {
flg('备货单【'.$order->order_no.'】创建终版成功,但更新原单失败');
}
StockOrders::where('object_id', $res->object_id)->where('is_final', 1)->where('approval_status', 3)->delete();
}
// 启动审批
if ($res->approval_status == 1) {
@@ -354,6 +356,10 @@ class StockOrdersService
$config = $this->buildQueryConfig($request, 'approval');
$item = $this->logicShow($config);
if (!$item || $item->approval_status != 0) {
throw new BusinessException('该单据已提交审批或单子不存在');
}
// 检查备货单是否包含必需的文件
$this->validateRequiredFilesForApproval($item);
@@ -454,6 +460,12 @@ class StockOrdersService
// 检查必填文件是否已上传
// $this->validateRequiredFiles($data);
$param = $request->all();
$info = StockOrders::find($param['id']);
if (!$info || $info->approval_status != 0) {
throw new BusinessException('该单不存在或状态异常,请关闭或刷新页面后重试~');
}
$data['biz_date'] = strtotime($data['biz_date']);
$data['eta_ship_date'] = strtotime($data['eta_ship_date']);
$data['latest_arrival_date'] = strtotime($data['latest_arrival_date']);
@@ -655,7 +667,7 @@ class StockOrdersService
*/
private function generateVehicleUniqueId(array $veh): string
{
$keysToRemove = ['vin', 'remark', 'modify_list', 'order_id'];
$keysToRemove = ['remark', 'modify_list', 'order_id'];
$array = array_diff_key($veh, array_flip($keysToRemove));
$uniqueIdStr = json_encode($array);
return md5($uniqueIdStr);
@@ -884,6 +896,7 @@ class StockOrdersService
'num' => 1,
'mileage' => $v->mileage,
'remark' => $v->remark,
'vin' => $v->vin,
] + $moneyValues;
}
return ['list' => $vehList];
@@ -925,25 +938,26 @@ class StockOrdersService
{
$vehicleData['num'] += 1;
$fieldList = [
'fob_transport_fee',
'car_price',
'ocean_freight',
'commission',
'retention_price',
'port_misc_fee',
'total_price',
'clearance_price',
'profit'
];
// ** 11.11 remove 价格*数量
// $fieldList = [
// 'fob_transport_fee',
// 'car_price',
// 'ocean_freight',
// 'commission',
// 'retention_price',
// 'port_misc_fee',
// 'total_price',
// 'clearance_price',
// 'profit'
// ];
foreach ($vehicleData as $field => $value) {
if (in_array($field, $fieldList)) {
$amount = $vehicleData[$field]['amount'] * $vehicleData['num'];
$currencySymbol = SysCurrency::where('currency_code', $vehicleData[$field]['currency'])->value('currency_symbol');
$vehicleData[$field]['currency_info'] = $currencySymbol . number_format($amount, 2, '.', ',');
}
}
// foreach ($vehicleData as $field => $value) {
// if (in_array($field, $fieldList)) {
// $amount = $vehicleData[$field]['amount'] * $vehicleData['num'];
// $currencySymbol = SysCurrency::where('currency_code', $vehicleData[$field]['currency'])->value('currency_symbol');
// $vehicleData[$field]['currency_info'] = $currencySymbol . number_format($amount, 2, '.', ',');
// }
// }
}
/**
@@ -1114,5 +1128,17 @@ class StockOrdersService
}
public function serviceDestroy(Request $request, string $scene = 'destroy')
{
$config = $this->buildQueryConfig($request, $scene);
$item = $this->logicShow($config);
if (!$item) {
throw new BusinessException('数据不存在');
}
if ($item->approval_status == 1 || $item->approval_status == 2) {
throw new BusinessException('审核中或者已审核的订单不能删除');
}
$deleted = $this->logicDeleteBy($config);
return $this->postProcess(ucfirst($scene), $deleted);
}
}
+1 -1
View File
@@ -50,7 +50,7 @@ class UploadService
$size = filesize($filePath) ?: 0;
$sceneUrl = $scene ? ($scene . '/') : '';
$dir = 'x_server/' . $sceneUrl . date('Ymd') . '/';
$dir = config('oss.oss_main_dir') . $sceneUrl . date('Ymd') . '/';
// 使用传入的 original_name 命名(缺少扩展名时补上推断的扩展名),并做简单安全处理
$safeBaseName = preg_replace('/[\\\\\/]+/', '_', $originalName);
if ($extension && strtolower(pathinfo($safeBaseName, PATHINFO_EXTENSION)) !== strtolower($extension)) {
@@ -0,0 +1,31 @@
<?php
namespace app\common\service;
use app\common\validate\UsedcarCrawlResultsImagesValidate;
use app\common\traits\crud\CrudServiceTrait;
use app\common\model\UsedcarCrawlResultsImages;
class UsedcarCrawlResultsImagesService
{
use CrudServiceTrait;
/**
* 指定当前要操作的模型
*/
protected string $modelClass = UsedcarCrawlResultsImages::class;
/**
* 验证器
*/
protected $validator = UsedcarCrawlResultsImagesValidate::class;
/**
* 获取验证器
*/
public function getValidator()
{
return new $this->validator;
}
}
@@ -0,0 +1,31 @@
<?php
namespace app\common\service;
use app\common\validate\UsedcarCrawlResultsValidate;
use app\common\traits\crud\CrudServiceTrait;
use app\common\model\UsedcarCrawlResults;
class UsedcarCrawlResultsService
{
use CrudServiceTrait;
/**
* 指定当前要操作的模型
*/
protected string $modelClass = UsedcarCrawlResults::class;
/**
* 验证器
*/
protected $validator = UsedcarCrawlResultsValidate::class;
/**
* 获取验证器
*/
public function getValidator()
{
return new $this->validator;
}
}
@@ -0,0 +1,78 @@
<?php
namespace app\common\service;
use app\common\validate\UserDashboardRuleValidate;
use app\common\traits\crud\CrudServiceTrait;
use app\common\model\UserDashboardRule;
use support\Request;
class UserDashboardRuleService
{
use CrudServiceTrait;
/**
* 指定当前要操作的模型
*/
protected string $modelClass = UserDashboardRule::class;
/**
* 验证器
*/
protected $validator = UserDashboardRuleValidate::class;
/**
* 获取验证器
*/
public function getValidator()
{
return new $this->validator;
}
/**
* 构造 CrudUtil 查询配置
*/
protected function buildQueryConfig(Request $request, string $scene = 'default'): array
{
$p = $request->all();
$config = [];
$config['model'] = $this->modelClass;
switch ($scene) {
case 'default':
$config['where']['status'] = 1;
$config['order']['id'] = 'desc';
break;
case 'index':
$config['where']['status'] = 1;
$config['order']['id'] = 'desc';
break;
case 'show':
$config['where']['id'] = $p['id'];
$config['where']['status'] = 1;
break;
case 'store':
break;
case 'update':
$config['where']['id'] = $p['id'];
break;
case 'destroy':
$config['where']['id'] = $p['id'];
break;
}
return $config;
}
/**
* 添加前置处理
*/
public function beforeServiceStore($data, Request $request)
{
if(!isset($data['weight']) || ($data['weight'] == null || $data['weight'] == '')){
throw new \Exception("权重不能为空");
}
return $data;
}
}
+32 -7
View File
@@ -41,6 +41,27 @@ class UserService
// $menuTree = $permService->getMenuTree($uid, $systemCode);
// $permissionCodes = $permService->getPermissionCodes($uid, $systemCode);
$corp_name = Corp::where('abbr', $user->corp_id)->value('name');
// 查找直属上级 10.13 add
$reportToInfo = [];
if ($user->reportToInfo) {
$reportToInfo = [
'id' => $user->reportToInfo->id,
'name' => $user->reportToInfo->name
];
}
// 查找绑定内勤 10.13 add
$officeClerkInfo = [];
$ugm = UserGroupMembers::where('user_id', $uid)->where('status', 1)->first();
if ($ugm) {
$lugm = UserGroupMembers::where('group_id', $ugm->group_id)->where('is_leader', 1)->first();
if ($lugm) {
$officeClerkInfo['id'] = $lugm->user_id;
$officeClerkInfo['name'] = User::where('id', $lugm->user_id)->value('name');
}
}
return [
'user' => [
'id' => $user->id,
@@ -50,6 +71,7 @@ class UserService
'email' => $user->email,
'status' => $user->status,
'roles' => $user->roles->pluck('name'),
'roles_id' => $user->roles->pluck('id'),
'sex' => $user->sex,
'sn' => $user->sn,
'corp_id' => $user->corp_id,
@@ -57,6 +79,8 @@ class UserService
'report_to' => $user->report_to,
'sign_img' => $user->sign_img,
'sign_img_url' => (!empty($user->sign_img))?(new \app\common\service\OssService)->generatePrivateUrl($user->sign_img):'',
'report_to_info' => $reportToInfo,
'office_clerk_info' => $officeClerkInfo
],
// 'menu' => $menuTree,
// 'permissions' => $permissionCodes
@@ -159,14 +183,10 @@ class UserService
/** 分页列表 */
public function serviceTeam(Request $request)
{
if (method_exists($this, 'complexQuery')) {
$result = $this->complexQuery($request->get());
} else {
$config = $this->buildQueryConfig($request, 'team');
$limit = (int)$request->get('size', 15);
$page = (int)$request->get('current', 1);
$result = $this->logicList($config, $limit, $page);
}
foreach ($result as &$data){
$data['customer_count'] = Customer::where('user_id', $data['id'])->count();
@@ -219,9 +239,14 @@ class UserService
$config['where']['status'] = $p['status'];
}
$config['dataLimit']['type'] = 'relation';
$config['dataLimit']['userId'] = $request->user->uid;
$config['dataLimit']['userField'] = 'report_to';
$config['with']= ['reportToInfo'];
if (!in_array(1, $request->user->role_ids)) {
$config['where']['reportToInfo.id'] = $request->user->uid;
}
// $config['dataLimit']['type'] = 'relation';
// $config['dataLimit']['userId'] = $request->user->uid;
// $config['dataLimit']['userField'] = 'report_to';
break;
}
+99 -2
View File
@@ -91,11 +91,11 @@ class Feishu
/**
* 授权
*/
public function authorize($certificate, $url = 'authen/v1/authorize')
public function authorize($certificate,$mobile, $url = 'authen/v1/authorize')
{
$arr = [
'app_id' => $certificate['app_id'],
'redirect_uri' => $certificate['r_url'],
'redirect_uri' => $mobile == 1 ? $certificate['m_url'] : $certificate['r_url'],
'scope' => $certificate['scope'],
'state' => $certificate['state']
];
@@ -225,4 +225,101 @@ class Feishu
// ];
// return $this->requestResult($url);
// }
/**
* 获取 JSAPI 配置(签名)
* @param string $url 当前页面完整 URL(不包含 # 后面的内容)
* @return array
*/
public function getJsApiConfig($url)
{
// 1. 获取 tenant_access_token
//$accessToken = $this->getTenantAccessToken();
$accessToken = $this->tenantAccessToken();
// 2. 获取 jsapi_ticket
$ticket = $this->getJsapiTicket($accessToken);
// 3. 生成签名参数
$timestamp = (int) round(microtime(true) * 1000);
$nonceStr = $this->generateNonceStr();
// 4. 生成签名
$signature = $this->generateSignature($ticket, $nonceStr, $timestamp, $url);
return [
'appId' => $this->appId,
'timestamp' => $timestamp,
'nonceStr' => $nonceStr,
'signature' => $signature,
'url' => $url
];
}
/**
* 获取 jsapi_ticket
* @param string $accessToken
* @return string
*/
private function getJsapiTicket($accessToken)
{
// 检查缓存
$cachedTicket = Cache::get($this->abbr.'-feishu:jsapi_ticket');
// $cachedTicket = $this->getCachedData('feishu:jsapi_ticket');
if ($cachedTicket) {
return $cachedTicket;
}
// 使用 requestResult 方法发起请求,而不是 httpGet
$this->options["headers"] = [
"Authorization" => "Bearer " . $accessToken,
"Content-Type" => "application/json; charset=utf-8"
];
$result = $this->requestResult('jssdk/ticket/get', 'GET');
if (!$result) {
throw new BusinessException('获取 jsapi_ticket 失败: ' . $this->error());
}
$ticket = $result['data']['ticket'];
$expire = $result['data']['expire_in'] - 300; // 提前5分钟过期
// 缓存 ticket
Cache::set($this->abbr.'-feishu:jsapi_ticket', $ticket, $expire);
// $this->setCachedData('feishu:jsapi_ticket', $ticket, $expire);
return $ticket;
}
/**
* 生成签名
* @param string $ticket
* @param string $nonceStr
* @param int $timestamp
* @param string $url
* @return string
*/
private function generateSignature($ticket, $nonceStr, $timestamp, $url)
{
// 拼接待签名字符串(注意顺序)
$signStr = "jsapi_ticket={$ticket}&noncestr={$nonceStr}&timestamp={$timestamp}&url={$url}";
return sha1($signStr);
}
/**
* 生成随机字符串
* @param int $length
* @return string
*/
private function generateNonceStr($length = 16)
{
$chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
$str = '';
for ($i = 0; $i < $length; $i++) {
$str .= substr($chars, mt_rand(0, strlen($chars) - 1), 1);
}
return $str;
}
}
+46
View File
@@ -0,0 +1,46 @@
<?php
namespace app\common\utils;
class SystemAuth
{
/**
* 生成签名
* @param string $userId 用户ID
* @param int $timestamp Unix时间戳
* @param string $secret 共享密钥
* @return string
*/
public static function generateSignature(string $userId, int $timestamp, string $secret): string
{
$data = $userId . $timestamp;
return hash_hmac('md5', $data, $secret);
}
/**
* 验证签名
* @param string $userId 用户ID
* @param int $timestamp Unix时间戳
* @param string $receivedSignature 接收到的签名
* @param string $secret 共享密钥
* @return bool
*/
public static function verifySignature(string $userId, int $timestamp, string $receivedSignature, string $secret): bool
{
$expectedSignature = self::generateSignature($userId, $timestamp, $secret);
return hash_equals($expectedSignature, $receivedSignature);
}
/**
* 验证时间戳是否在有效期内
* @param int $timestamp Unix时间戳
* @param int $maxSeconds 有效期(秒)
* @return bool
*/
public static function verifyTimestamp(int $timestamp, int $maxSeconds = 600): bool
{
$now = time();
$diff = abs($now - $timestamp);
return $diff <= $maxSeconds;
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,151 @@
<?php
namespace app\common\utils\approval\traits;
use support\Db;
use support\exception\BusinessException;
trait ApprovalDiscussionTrait
{
/** 是否该实例参与者 */
private function isParticipantOfInstance(int $instanceId, int $userId): bool
{
$inst = Db::table('approval_instances')->where('id', $instanceId)->first();
if (!$inst) return false;
if ((int)$inst->initiator_id === $userId) return true;
$inRecords = Db::table('approval_records')
->where('instance_id', $instanceId)->where('approver_id', $userId)->exists();
if ($inRecords) return true;
$inCc = Db::table('approval_ccs')
->where('instance_id', $instanceId)->where('user_id', $userId)->exists();
return $inCc;
}
/** 是否该实例任一步的审批人 */
private function isApproverOfInstance(int $instanceId, int $userId): bool
{
return Db::table('approval_records')
->where('instance_id', $instanceId)
->where('approver_id', $userId)
->exists();
}
/** 获取讨论受众 */
private function getDiscussionAudienceUserIds(int $instanceId, bool $approversOnly, int $excludeUserId): array
{
$uids = [];
if (!$approversOnly) {
$inst = Db::table('approval_instances')->where('id', $instanceId)->first();
if ($inst) $uids[] = (int)$inst->initiator_id;
$ccs = Db::table('approval_ccs')->where('instance_id', $instanceId)->pluck('user_id')->toArray();
foreach ($ccs as $u) $uids[] = (int)$u;
}
$approvers = Db::table('approval_records')
->where('instance_id', $instanceId)
->distinct()->pluck('approver_id')->toArray();
foreach ($approvers as $u) $uids[] = (int)$u;
$uids = array_values(array_unique(array_filter($uids, fn($u) => $u !== $excludeUserId)));
return $uids;
}
/** 发表讨论 */
public function postDiscussion(
int $instanceId,
int $userId,
string $content,
bool $isInternal = false,
array $attachments = [],
?string $userNameColumn = null
): array {
$content = trim($content);
if ($content === '') throw new BusinessException('讨论内容不能为空');
if (!$this->isParticipantOfInstance($instanceId, $userId)) {
throw new BusinessException('无权参与该实例的讨论');
}
if ($isInternal && !$this->isApproverOfInstance($instanceId, $userId)) {
throw new BusinessException('仅审批人可发布内部讨论');
}
$now = date('Y-m-d H:i:s');
$id = Db::table('approval_discussions')->insertGetId([
'instance_id' => $instanceId,
'user_id' => $userId,
'is_internal' => $isInternal ? 1 : 0,
'content' => $content,
'attachments' => empty($attachments) ? null : json_encode($attachments, JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES),
'created_at' => $now,
'updated_at' => $now,
]);
// 通知其他人
$inst = Db::table('approval_instances')->where('id', $instanceId)->first();
if ($inst) {
$audience = $this->getDiscussionAudienceUserIds($instanceId, $isInternal, $userId);
// 发言人姓名(可选)
$speaker = (string)$userId;
if ($userNameColumn) {
$name = Db::table('user')->where('id', $userId)->value($userNameColumn);
if ($name) $speaker = $name;
}
$snippet = function_exists('mb_strimwidth') ? mb_strimwidth($content, 0, 80, '...', 'UTF-8') : $content;
$extra = ['note' => "【讨论】{$speaker}:{$snippet}"];
$this->sendNotification(
$audience,
(object)[
'id' => (int)$inst->id,
'flow_type' => (string)$inst->flow_type,
'initiator_id' => (int)$inst->initiator_id,
'form_data' => $inst->form_data,
'biz_type' => (string)$inst->biz_type,
'biz_id' => (string)$inst->biz_id,
],
'pending',
false,
$extra
);
}
return [
'id' => (int)$id,
'instance_id' => $instanceId,
'user_id' => $userId,
'is_internal' => (bool)$isInternal,
'content' => $content,
'attachments' => $attachments,
'created_at' => $now,
];
}
/** 获取讨论列表 */
public function listDiscussions(int $instanceId, ?int $afterId = null, int $limit = 50, ?string $userNameColumn = null): array
{
$q = Db::table('approval_discussions as d')->where('d.instance_id', $instanceId);
if ($afterId) $q->where('d.id', '>', $afterId);
$q->orderBy('d.id', 'asc')->limit(max(1, min(200, $limit)));
if ($userNameColumn) {
$rows = $q->leftJoin('user as u', 'd.user_id', '=', 'u.id')
->select('d.*', Db::raw("u.`{$userNameColumn}` as user_name"))
->get()->toArray();
} else {
$rows = $q->select('d.*')->get()->toArray();
}
return array_map(function($r){
return [
'id' => (int)$r->id,
'instance_id' => (int)$r->instance_id,
'user_id' => (int)$r->user_id,
'is_internal' => (bool)$r->is_internal,
'content' => $r->content,
'attachments' => $r->attachments ? json_decode($r->attachments, true) : [],
'created_at' => $r->created_at,
'updated_at' => $r->updated_at,
'user_name' => property_exists($r, 'user_name') ? $r->user_name : null,
];
}, $rows);
}
}
@@ -0,0 +1,592 @@
<?php
namespace app\common\utils\approval\traits;
use support\Db;
use support\Log;
use support\exception\BusinessException;
trait ApprovalEngineTrait
{
/** 启动审批流程 */
public function start(string $flowType, string $bizType, string $bizId, array $formData, int $initiatorId): array
{
return Db::transaction(function () use ($flowType, $bizType, $bizId, $formData, $initiatorId) {
$flow = Db::table('approval_flows')->where('type', $flowType)->where('is_active', 1)->first();
if (!$flow) throw new BusinessException("审批流程未找到或已停用: {$flowType}");
$steps = json_decode($flow->steps, true);
if (!is_array($steps) || empty($steps)) throw new BusinessException("流程步骤配置错误");
// 找首个有效步骤索引(考虑条件)
$firstIndex = $this->getNextStepIndex($steps, $formData, -1);
if ($firstIndex === null) throw new BusinessException("未找到可执行的审批步骤");
$firstStep = $steps[$firstIndex];
$now = date('Y-m-d H:i:s');
$deadlineAt = null;
if (($flow->timeout_hours ?? 0) > 0) {
$deadlineAt = date('Y-m-d H:i:s', time() + $flow->timeout_hours * 3600);
}
$instanceId = Db::table('approval_instances')->insertGetId([
'flow_type' => $flowType,
'biz_type' => $bizType,
'biz_id' => $bizId,
'form_data' => json_encode($formData, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
'initiator_id' => $initiatorId,
'status' => 'processing',
'current_step' => $firstIndex,
'round' => 1,
'deadline_at' => $deadlineAt,
'created_at' => $now,
'updated_at' => $now,
]);
$instance = (object)[
'id' => $instanceId,
'flow_type' => $flowType,
'form_data' => json_encode($formData, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
'initiator_id' => $initiatorId,
'current_step' => $firstIndex,
'status' => 'processing',
'flow_type' => $flowType,
'biz_type' => $bizType,
'biz_id' => $bizId,
'round' => 1,
];
// 解析首步审批人(支持 role_id 或 roles.name)
$approverIds = $this->resolveApprovers($firstStep, $instance, true);
foreach ($approverIds as $uid) {
$this->insertApprovalRecord($instance, $firstIndex, $uid);
}
$extra = [];
$submitNote = $formData['submit_note'] ?? '';
if (is_string($submitNote) && $submitNote !== '') {
$extra['note'] = $submitNote;
}
$this->sendNotification($approverIds, $instance, 'pending', false, $extra);
// $this->notifyCCs($firstStep, $instance, 'pending');
$this->pushTodoChanged($approverIds);
return ['instance_id' => $instanceId];
});
}
/** 执行审批(幂等友好) */
public function approve(int $instanceId, int $approverId, string $action, string $comment = '')
{
if (!in_array($action, ['approve','reject'], true)) {
throw new BusinessException("无效的审批操作");
}
return Db::transaction(function () use ($instanceId, $approverId, $action, $comment) {
$instance = Db::table('approval_instances')->where('id', $instanceId)->lockForUpdate()->first();
if (!$instance) throw new BusinessException("审批实例未找到");
if ($instance->status !== 'processing') {
return ['status' => 'noop', 'msg' => '实例未处理'];
}
$flow = Db::table('approval_flows')->where('type', $instance->flow_type)->first();
if (!$flow) throw new BusinessException("审批流程定义未找到");
$steps = json_decode($flow->steps, true);
$idx = (int)$instance->current_step;
$round = (int)$instance->round;
$step = $steps[$idx] ?? null;
if (!$step) throw new BusinessException("当前步骤配置错误");
// 找到该用户在本步的 pending 记录(加锁)
$pendingRecord = Db::table('approval_records')
->where('instance_id', $instanceId)
->where('round', $round)
->where('approver_id', $approverId)
->where('step_index', $idx)
->where('status', 'pending')
->lockForUpdate()
->first();
if (!$pendingRecord) {
// 已处理过?
$anyRecord = Db::table('approval_records')
->where('instance_id', $instanceId)
->where('round', $round)
->where('approver_id', $approverId)
->where('step_index', $idx)
->first();
if ($anyRecord && $anyRecord->status !== 'pending') {
return ['status' => 'noop', 'msg' => '已处理'];
}
// 步骤已推进
if ((int)$instance->current_step !== $idx) {
return ['status' => 'noop', 'msg' => '非当前审批'];
}
return ['status' => 'noop', 'msg' => '无待处理记录'];
}
$now = date('Y-m-d H:i:s');
// 1) 更新当前人的审批记录
Db::table('approval_records')->where('id', $pendingRecord->id)->update([
'action' => $action,
'status' => ($action === 'approve') ? 'approved' : 'rejected',
'comment' => $comment,
'approved_at' => $now,
'updated_at' => $now,
]);
// 异步审批回调 9.23 add
$approvalcallback = [
'biz_id' => $instance->biz_id,
'flow_type' => $instance->flow_type,
'instance_id' => $instance->id,
'initiator_id' => $instance->initiator_id,
'status' => $action
];
\Webman\RedisQueue\Redis::send('approval-callback', $approvalcallback);
// 2) 聚合本步结论(any / all)
$typeRaw = strtolower($step['type'] ?? 'all');
$type = in_array($typeRaw, ['any','all']) ? $typeRaw : 'all';
$outcome = $this->getStepOutcome($instanceId, $round, $idx, $type); // 'pending' | 'approve' | 'reject'
if ($outcome === 'pending') {
$this->pushTodoChanged([$approverId]);
return ['status' => 'step_pending', 'msg' => '审批待定'];
}
$otherPendingUids = $this->getPendingApproverIds($instanceId, $round, $idx);
// 3) 有结论:关闭本步其余 pending
$this->closeOtherPending($instanceId, $round, $idx, [$approverId]);
// 这些人 pending→skipped,待办数减少
$this->pushTodoChanged($otherPendingUids);
// 4) 拒绝 => 结束
if ($outcome === 'reject') {
Db::table('approval_instances')->where('id', $instanceId)->update([
'status' => 'rejected',
'finished_at' => $now,
'updated_at' => $now,
]);
$this->onFlowFinalized($instance, 'rejected');
$this->sendNotification([$instance->initiator_id], $instance, 'reject', false);
$this->pushTodoChanged([$approverId]);
return ['status' => 'completed', 'msg' => '拒绝'];
}
// 5) 通过 => 进入下一步或结束
$formData = json_decode($instance->form_data, true) ?: [];
$nextIndex = $this->getNextStepIndex($steps, $formData, $idx);
if ($nextIndex === null) {
$finalStatus = $this->normalizeFinalStatus((string)$instance->flow_type, 'approved');
$notifyAction = ($finalStatus === 'voided') ? 'voided' : 'approve';
Db::table('approval_instances')->where('id', $instanceId)->update([
'status' => $finalStatus,
'finished_at' => $now,
'updated_at' => $now,
]);
$this->onFlowFinalized($instance, $finalStatus);
// 新增:在流程最终完成时才发本步骤的抄送
if (!empty($step['cc'])) {
$this->notifyCCs($step, $instance, $notifyAction);
}
$this->sendNotification([$instance->initiator_id], $instance, $notifyAction, false);
$this->pushTodoChanged([$approverId]);
return ['status' => 'completed', 'msg' => ($finalStatus === 'voided' ? '已作废' : '同意')];
}
// 乐观推进 current_step
$affected = Db::table('approval_instances')
->where('id', $instanceId)
->where('current_step', $idx)
->where('status', 'processing')
->update(['current_step' => $nextIndex, 'updated_at' => $now]);
if (!$affected) {
return ['status' => 'noop', 'msg' => '并发进展'];
}
// 生成下一步待办 + 通知
$nextStep = $steps[$nextIndex];
$nextApprovers = $this->resolveApprovers($nextStep, $instance, true);
foreach ($nextApprovers as $uid) {
$this->insertApprovalRecord($instance, $nextIndex, $uid);
}
$this->sendNotification($nextApprovers, $instance, 'pending', false);
// $this->notifyCCs($nextStep, $instance, 'pending');
$this->pushTodoChanged(array_merge([$approverId], $nextApprovers));
return ['status' => 'next_step', 'msg' => '进行下一步:'.$nextStep['name'] ?? ('Step '.$nextIndex)];
});
}
/** 撤回(仅发起人在无已处理记录前可撤回) */
public function revoke(int $instanceId, int $initiatorId, string $reason = '')
{
return Db::transaction(function () use ($instanceId, $initiatorId, $reason) {
$instance = Db::table('approval_instances')->where('id', $instanceId)->lockForUpdate()->first();
if (!$instance) throw new BusinessException('审批实例未找到');
if ((int)$instance->initiator_id !== (int)$initiatorId) throw new BusinessException('无权撤回');
if ($instance->status !== 'processing') throw new BusinessException('流程已结束或不可撤回');
$handled = Db::table('approval_records')
->where('instance_id', $instanceId)
->whereIn('status', ['approved','rejected','timeout','skipped'])
->count() > 0;
if ($handled) throw new BusinessException('当前进度不可撤回');
$now = date('Y-m-d H:i:s');
Db::table('approval_instances')->where('id', $instanceId)->update([
'status' => 'revoked',
'finished_at' => $now,
'updated_at' => $now,
]);
$this->onFlowFinalized($instance, 'revoked');
$pendingUids = Db::table('approval_records')
->where('instance_id', $instanceId)
->where('status', 'pending')
->pluck('approver_id')->toArray();
Db::table('approval_records')
->where('instance_id', $instanceId)
->where('status', 'pending')
->update(['status' => 'skipped', 'approved_at' => $now, 'updated_at' => $now]);
$this->sendNotification([$initiatorId], $instance, 'revoked', false);
$this->pushTodoChanged($pendingUids);
return ['status' => 'completed', 'result' => 'revoked'];
});
}
/** 定时任务:处理超时(实例级) */
public function handleTimeouts()
{
$now = date('Y-m-d H:i:s');
$expired = Db::table('approval_instances')
->where('status', 'processing')
->whereNotNull('deadline_at')
->where('deadline_at', '<=', $now)
->get();
foreach ($expired as $instance) {
Db::transaction(function () use ($instance, $now) {
$flow = Db::table('approval_flows')->where('type', $instance->flow_type)->first();
if (!$flow) return;
$final = ($flow->timeout_action === 'auto_approve') ? 'approved' : 'rejected';
$final = $this->normalizeFinalStatus((string)$instance->flow_type, $final);
$finalAction = ($final === 'voided') ? 'voided' : (($final === 'approved') ? 'approve' : 'reject');
Db::table('approval_instances')->where('id', $instance->id)->update([
'status' => $final,
'finished_at' => $now,
'updated_at' => $now,
]);
$this->onFlowFinalized((object)$instance, $final);
$pendingUids = Db::table('approval_records')
->where('instance_id', $instance->id)
->where('round', (int)$instance->round)
->where('step_index', $instance->current_step)
->where('status', 'pending')
->pluck('approver_id')->toArray();
Db::table('approval_records')
->where('instance_id', $instance->id)
->where('round', (int)$instance->round)
->where('step_index', $instance->current_step)
->where('status', 'pending')
->update(['status' => 'timeout', 'approved_at' => $now, 'updated_at' => $now]);
$this->notifyInitiator($instance, $finalAction);
$this->pushTodoChanged($pendingUids);
});
}
}
/** 重提交(仅对 status=rejected 的实例;支持申诉说明) */
public function resubmit(int $instanceId, int $initiatorId, array $patchData = [], string $appealText = ''): array
{
return Db::transaction(function () use ($instanceId, $initiatorId, $patchData, $appealText) {
$instance = Db::table('approval_instances')->where('id', $instanceId)->lockForUpdate()->first();
if (!$instance) throw new BusinessException('审批实例未找到');
if ((int)$instance->initiator_id !== (int)$initiatorId) throw new BusinessException('仅发起人可重提');
if ($instance->status !== 'rejected') throw new BusinessException('仅被驳回的实例可重提');
$flow = Db::table('approval_flows')->where('type', $instance->flow_type)->first();
if (!$flow) throw new BusinessException('审批流程定义未找到');
$steps = json_decode($flow->steps, true) ?: [];
$formData = json_decode($instance->form_data ?? '{}', true) ?: [];
// 合并改动 & 打上重提标记
$formData = array_merge($formData, $patchData);
$formData['resubmitted'] = 1;
$appealText = trim((string)$appealText);
if ($appealText !== '') {
$formData['appeal_last'] = $appealText;
}
// 计算重提后首个命中的步骤
$firstIndex = $this->getNextStepIndex($steps, $formData, -1);
if ($firstIndex === null) throw new BusinessException('重提后未找到可执行的步骤');
$now = date('Y-m-d H:i:s');
$deadlineAt = null;
if (($flow->timeout_hours ?? 0) > 0) {
$deadlineAt = date('Y-m-d H:i:s', time() + $flow->timeout_hours * 3600);
}
$oldPendingUids = Db::table('approval_records')
->where('instance_id', $instanceId)
->where('status', 'pending')
->pluck('approver_id')->toArray();
// 清理可能遗留的 pending
Db::table('approval_records')
->where('instance_id', $instanceId)
->where('status', 'pending')
->update(['status'=>'skipped','approved_at'=>$now,'updated_at'=>$now]);
// 轮次 +1
$newRound = ((int)$instance->round) + 1;
// 重新开启实例
Db::table('approval_instances')->where('id', $instanceId)->update([
'status' => 'processing',
'current_step' => $firstIndex,
'round' => $newRound,
'form_data' => json_encode($formData, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
'finished_at' => null,
'deadline_at' => $deadlineAt,
'updated_at' => $now,
]);
// 创建待办 + 通知
$firstStep = $steps[$firstIndex];
$instanceLite = (object)[
'id' => (int)$instanceId,
'flow_type' => (string)$instance->flow_type,
'form_data' => json_encode($formData, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
'initiator_id' => (int)$instance->initiator_id,
'biz_type' => (string)$instance->biz_type,
'biz_id' => (string)$instance->biz_id,
'round' => $newRound,
];
$approvers = $this->resolveApprovers($firstStep, $instanceLite, true);
if (empty($approvers)) throw new BusinessException('重提后首步未解析到审批人');
foreach ($approvers as $uid) {
$this->insertApprovalRecord($instanceLite, $firstIndex, (int)$uid);
}
$extra = $appealText !== '' ? ['appeal' => $appealText] : [];
$this->sendNotification($approvers, $instanceLite, 'pending', false, $extra);
$this->notifyCCs($firstStep, $instanceLite, 'pending');
$this->pushTodoChanged(array_merge($oldPendingUids, $approvers));
return [
'status' => 'resubmitted',
'instance_id' => (int)$instanceId,
'current_step' => $firstIndex,
'step_name' => $firstStep['name'] ?? ('Step '.$firstIndex),
];
});
}
/** 新增:发起“作废审批” */
public function startNullify(
string $voidFlowType,
string $bizType,
string $bizId,
array $formData,
int $initiatorId
): array {
return Db::transaction(function () use ($voidFlowType, $bizType, $bizId, $formData, $initiatorId) {
// 1) 流程定义校验
$flow = Db::table('approval_flows')->where('type', $voidFlowType)->where('is_active', 1)->first();
if (!$flow) throw new BusinessException("作废流程未找到或已停用: {$voidFlowType}");
$steps = json_decode($flow->steps, true);
if (!is_array($steps) || empty($steps)) throw new BusinessException("作废流程步骤配置错误");
// 2) 必须存在“已通过”的最新有效实例(非作废流程)
$latestApproved = Db::table('approval_instances')
->where('biz_type', $bizType)
->where('biz_id', $bizId)
->where('flow_type', '!=', $voidFlowType)
->where('status', 'approved')
->orderByDesc('id')
->first();
if (!$latestApproved) {
throw new BusinessException('仅已通过的单据可发起作废');
}
// 3) 不允许并行的作废流程
$voidProcessing = Db::table('approval_instances')
->where('flow_type', $voidFlowType)
->where('biz_id', $bizId)
->where('status', 'processing')
->exists();
if ($voidProcessing) {
throw new BusinessException('已存在进行中的作废审批');
}
// A. 基于历史“作废流程已通过”的实例拦截
$voidApproved = Db::table('approval_instances')
->where('flow_type', $voidFlowType)
->where('biz_id', $bizId)
->whereIn('status', ['approved','voided'])
->exists();
if ($voidApproved) {
throw new BusinessException('该单据已作废,无需重复作废');
}
// B.(可选)直接查业务表状态拦截(若你用 approval_status=4 表示已作废)
/*
$orderVoided = \app\common\model\StockOrders::where('id', $bizId)
->where('approval_status', 4)
->exists();
if ($orderVoided) {
throw new BusinessException('该单据已作废,无需重复作废');
}
*/
// 4) 计算首步
// 注:作废流程可以有独立条件;若不需要条件,直接第一步
$formData = array_merge([
'_op' => 'nullify',
'_nullify_reason' => $formData['nullify_reason'] ?? ($formData['reason'] ?? ''),
'_nullify_of_instance_id' => (int)$latestApproved->id,
], $formData);
$firstIndex = $this->getNextStepIndex($steps, $formData, -1);
if ($firstIndex === null) throw new BusinessException("作废流程未命中任何审批步骤");
$firstStep = $steps[$firstIndex];
$now = date('Y-m-d H:i:s');
$deadlineAt = null;
if (($flow->timeout_hours ?? 0) > 0) {
$deadlineAt = date('Y-m-d H:i:s', time() + $flow->timeout_hours * 3600);
}
// 5) 新建作废实例(round 固定从 1 开始)
$instanceId = Db::table('approval_instances')->insertGetId([
'flow_type' => $voidFlowType,
'biz_type' => $bizType,
'biz_id' => $bizId,
'form_data' => json_encode($formData, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
'initiator_id' => $initiatorId,
'status' => 'processing',
'current_step' => $firstIndex,
'round' => 1, //
'deadline_at' => $deadlineAt,
'created_at' => $now,
'updated_at' => $now,
]);
$instance = (object)[
'id' => $instanceId,
'flow_type' => $voidFlowType,
'biz_type' => $bizType,
'biz_id' => $bizId,
'form_data' => json_encode($formData, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
'initiator_id' => $initiatorId,
'current_step' => $firstIndex,
'status' => 'processing',
'round' => 1, //
];
// 6) 生成首步待办 + 通知(带上作废原因)
$approverIds = $this->resolveApprovers($firstStep, $instance, true);
foreach ($approverIds as $uid) {
$this->insertApprovalRecord($instance, $firstIndex, $uid);
}
$extra = [];
if (!empty($formData['_nullify_reason'])) {
$extra['note'] = '【作废原因】'.$formData['_nullify_reason'];
}
$this->sendNotification($approverIds, $instance, 'pending', false, $extra);
$this->notifyCCs($firstStep, $instance, 'pending');
$this->pushTodoChanged($approverIds);
return ['instance_id' => $instanceId];
});
}
// 流程完结后的业务回调(approved|rejected|revoked|timeout?)
private function onFlowFinalized(object $instance, string $final): void
{
try {
switch ($instance->flow_type) {
case 'stock_orders': {
$order = \app\common\model\StockOrders::find($instance->biz_id);
if (!$order) {
throw new BusinessException('订单不存在');
}
if ($final === 'approved') {
$order->approval_status = 2;
$order->save();
} elseif ($final === 'rejected') {
$order->approval_status = 3;
$order->save();
} elseif ($final === 'revoked') {
$order->approval_status = 0;
$order->save();
}
break;
}
case 'stock_orders_nullify':
if ($final === 'approved') {
$final = 'voided';
Db::table('approval_instances')->where('id', $instance->id)->update([
'status' => 'voided',
]);
}
if (in_array($final, ['voided','approved'], true)) {
$order = \app\common\model\StockOrders::find($instance->biz_id);
if ($order && $order->approval_status !== 4) {
$order->approval_status = 4;
$order->save();
}
}
break;
default:
// 非作废流程,无需处理
break;
}
} catch (\Throwable $e) {
Log::error('onFlowFinalized error: '.$e->getMessage());
}
}
// 是否作废流程
private function isNullifyFlow(string $flowType): bool
{
$t = strtolower(trim($flowType));
return str_ends_with($t, '_nullify') || in_array($t, [
// 所有“作废”类流程的类型
'stock_orders_nullify',
], true);
}
// 归一化最终状态
private function normalizeFinalStatus(string $flowType, string $final): string
{
if ($this->isNullifyFlow($flowType) && $final === 'approved') {
return 'voided';
}
return $final;
}
}
@@ -0,0 +1,164 @@
<?php
namespace app\common\utils\approval\traits;
use support\Db;
use support\Log;
trait ApprovalNotificationTrait
{
/** 发送通知 */
private function sendNotification(array $userIds, object $instance, string $action, bool $isCC, array $extra = []): void
{
$map = [
'pending' => '待审批',
'approve' => '已通过',
'reject' => '已拒绝',
'revoked' => '已撤回',
'voided' => '已作废',
];
$prefix = $isCC ? '【抄送】' : '【审批】';
$text = $map[$action] ?? $action;
// 业务标题/内容
$approvalType = array_flip(\app\common\service\SysDictService::pluckKeyValueByCode('approval_type'));
$flowTxt = $approvalType[$instance->flow_type] ?? (string)$instance->flow_type;
$title = $prefix.'审批通知';
$content = "{$prefix}你有一条审批 {$text}:类型 {$flowTxt},业务标识 {$instance->biz_id}";
switch ($instance->flow_type) {
case 'stock_orders':
// 备货单
$stockOrder = \app\common\model\StockOrders::find($instance->biz_id);
if ($stockOrder) {
$title = $prefix."备货单:{$stockOrder->order_no}";
$flowTxt = $approvalType[$instance->flow_type] ?? $instance->flow_type;
$content = "{$prefix}你有一条审批 {$text}:类型 {$flowTxt},业务单号 {$stockOrder->order_no}";
}
break;
case 'stock_orders_nullify':
// 备货单作废
$stockOrder = \app\common\model\StockOrders::find($instance->biz_id);
if ($stockOrder) {
$title = $prefix."备货单作废:{$stockOrder->order_no}";
$flowTxt = $approvalType[$instance->flow_type] ?? '备货单作废';
$content = "{$prefix}你有一条审批 {$text}:类型 {$flowTxt},业务单号 {$stockOrder->order_no}";
// 把作废原因附带展示更清晰(也可依赖 $extra['note'])
$fd = json_decode($instance->form_data ?? '{}', true) ?: [];
if (!empty($fd['_nullify_reason'])) {
$content .= ';原因:'.(function_exists('mb_strimwidth') ? mb_strimwidth($fd['_nullify_reason'], 0, 120, '...', 'UTF-8') : $fd['_nullify_reason']);
}
}
break;
case 'purchase_orders':
// 采购单
$purchaseOrder = \app\common\model\PurchaseOrders::find($instance->biz_id);
if ($purchaseOrder) {
$title = $prefix."采购单:{$purchaseOrder->order_no}";
$flowTxt = $approvalType[$instance->flow_type] ?? $instance->flow_type;
$content = "{$prefix}你有一条审批 {$text}:类型 {$flowTxt},业务单号 {$purchaseOrder->order_no}";
}
break;
case 'purchase_order':
// 采购单
break;
case 'inv_requisition':
// 备货单
$invRequisition = \app\common\model\InvRequisition::find($instance->biz_id);
if ($invRequisition) {
$title = $prefix."物品借用:{$invRequisition->order_no}";
$flowTxt = $approvalType[$instance->flow_type] ?? $instance->flow_type;
$content = "{$prefix}你有一条审批 {$text}:类型 {$flowTxt},业务单号 {$invRequisition->order_no}";
}
break;
}
// 附加说明(提交说明/申诉/讨论摘要)
$append = $extra['appeal'] ?? ($extra['note'] ?? null);
if ($append) {
if (function_exists('mb_strimwidth')) {
$append = mb_strimwidth($append, 0, 120, '...', 'UTF-8');
}
$content .= ";说明:{$append}";
}
noticePush([
'title' => $title,
'content' => $content,
'recipient' => $userIds,
'email' => 1,
]);
}
private function notifyInitiator(object $instance, string $result): void
{
$this->sendNotification([(int)$instance->initiator_id], $instance, $result, false);
}
/** WebSocket:把一组用户的“待办数量”推送给前端(旧版) */
private function pushTodoChanged22(array $userIds): void
{
$userIds = array_values(array_unique(array_map('intval', $userIds)));
if (empty($userIds)) return;
// 统计这些用户当前的待办数量(以 approval_records.status = 'pending' 为准)
$rows = Db::table('approval_records')
->whereIn('approver_id', $userIds)
->where('status', 'pending')
->groupBy('approver_id')
->select('approver_id', Db::raw('COUNT(*) as cnt'))
->get();
// 默认都置 0,再填充真实值
$payload = array_fill_keys($userIds, 0);
foreach ($rows as $r) {
$payload[(int)$r->approver_id] = (int)$r->cnt;
}
try {
foreach ($payload as $uid => $cnt) {
$data = [
'event' => 'approval.todo.counts',
'user_id' => $uid,
'num' => $cnt,
];
\Webman\RedisQueue\Redis::send('ws-msg', $data);
}
} catch (\Throwable $e) {
Log::error('ws todo push error: '.$e->getMessage());
}
}
/** WebSocket:把一组用户的“待办数量”推送给前端(统一口径) */
private function pushTodoChanged(array $userIds): void
{
$userIds = array_values(array_unique(array_map('intval', $userIds)));
if (empty($userIds)) return;
// 只统计:实例进行中 + 当前轮次 + 当前步骤 的 pending
$rows = Db::table('approval_records as ar')
->join('approval_instances as ai', 'ai.id', '=', 'ar.instance_id')
->whereIn('ar.approver_id', $userIds)
->where('ar.status', 'pending')
->where('ai.status', 'processing')
->whereRaw('ar.round = ai.round')
->whereRaw('ar.step_index = ai.current_step')
->groupBy('ar.approver_id')
->select('ar.approver_id', Db::raw('COUNT(*) as cnt'))
->get();
// 默认置 0,再填充真实值
$payload = array_fill_keys($userIds, 0);
foreach ($rows as $r) {
$payload[(int)$r->approver_id] = (int)$r->cnt;
}
try {
foreach ($payload as $uid => $cnt) {
(new \app\common\utils\WsEvents())->todoCounts($uid, $cnt);
}
} catch (\Throwable $e) {
Log::error('ws todo push error: '.$e->getMessage());
}
}
}
@@ -0,0 +1,475 @@
<?php
namespace app\common\utils\approval\traits;
use support\Db;
use support\exception\BusinessException;
trait ApprovalQueryTrait
{
/** 流程图数据 */
public function getFlowDiagramData(int $instanceId): array
{
$instance = Db::table('approval_instances')->find($instanceId);
if (!$instance) return [];
$flow = Db::table('approval_flows')->where('type', $instance->flow_type)->first();
if (!$flow) return [];
$steps = json_decode($flow->steps, true) ?: [];
$records = Db::table('approval_records')
->where('instance_id', $instanceId)
->where('round', (int)$instance->round)
->orderBy('step_index')->orderByRaw('COALESCE(approved_at, created_at) asc')
->get()
->groupBy('step_index');
$formData = json_decode($instance->form_data, true) ?: [];
$data = [];
foreach ($steps as $i => $step) {
$actions = ($records->get($i) ?? collect())->toArray();
if (!empty($actions)) {
$cntApproved = 0; $cntRejected = 0; $cntPending = 0;
foreach ($actions as $a) {
if ($a->status === 'approved') $cntApproved++;
elseif ($a->status === 'rejected') $cntRejected++;
elseif ($a->status === 'pending') $cntPending++;
// skipped/timeout 不阻断完结显示
}
if ($cntRejected > 0) $status = 'rejected';
elseif ($cntPending > 0) $status = 'processing';
elseif ($cntApproved > 0) $status = 'approved';
else $status = 'approved'; // 全是 skipped/timeout 也视为已过
} else {
if ($i < (int)$instance->current_step) {
if (isset($step['condition']) && !$this->evaluateCondition($step['condition'], $formData)) {
$status = 'skipped';
} else {
$status = 'approved';
}
} elseif ($i === (int)$instance->current_step && $instance->status === 'processing') {
$status = 'processing';
} else {
$status = 'pending';
}
}
$data[] = [
'index' => $i,
'name' => $step['name'] ?? ('Step '.$i),
'type' => $step['type'] ?? 'all',
'status' => $status,
'actions' => $actions,
];
}
return $data;
}
/** 查询某个审批实例的审批记录 */
public function getInstanceRecords(int $instanceId, ?string $userNameColumn = null): array
{
// 1) 实例 + 流程步骤
$instance = Db::table('approval_instances')->where('id', $instanceId)->first();
if (!$instance) {
throw new BusinessException('审批实例未找到');
}
$flow = Db::table('approval_flows')->where('type', $instance->flow_type)->first();
if (!$flow) {
throw new BusinessException('审批流程定义未找到');
}
$steps = json_decode($flow->steps, true) ?: [];
// 2) 记录查询(可选带出审批人姓名)
if ($userNameColumn) {
// 注意:确保 user 表存在该列
$records = Db::table('approval_records as ar')
->leftJoin('user as u', 'ar.approver_id', '=', 'u.id')
->where('ar.instance_id', $instanceId)
->orderBy('ar.step_index')
->orderByRaw('COALESCE(ar.approved_at, ar.created_at) asc')
->select(
'ar.*',
Db::raw("u.`{$userNameColumn}` as approver_name")
)
->get()
->toArray();
} else {
$records = Db::table('approval_records as ar')
->where('ar.instance_id', $instanceId)
->orderBy('ar.step_index')
->orderByRaw('COALESCE(ar.approved_at, ar.created_at) asc')
->select('ar.*')
->get()
->toArray();
}
// 3) 附加步骤名/类型;统计汇总
$summary = ['approved'=>0,'rejected'=>0,'pending'=>0,'skipped'=>0,'timeout'=>0,'total'=>0];
foreach ($records as &$r) {
$idx = (int)$r->step_index;
$meta = $steps[$idx] ?? [];
$r->step_name = $meta['name'] ?? ('Step '.$idx);
$r->step_type = $meta['type'] ?? 'all';
if (isset($summary[$r->status])) {
$summary[$r->status]++;
}
$summary['total']++;
}
unset($r);
return [
'instance' => [
'id' => (int)$instance->id,
'flow_type' => $instance->flow_type,
'biz_type' => $instance->biz_type,
'biz_id' => $instance->biz_id,
'status' => $instance->status,
'current_step' => (int)$instance->current_step,
'created_at' => $instance->created_at,
'updated_at' => $instance->updated_at,
'finished_at' => $instance->finished_at,
],
'steps' => array_map(function($s){
return [
'name' => $s['name'] ?? '',
'type' => $s['type'] ?? 'all',
'condition' => $s['condition'] ?? null,
];
}, $steps),
'records' => array_map(function($r){
// 转成数组(Illuminate 返回的是 stdClass)
return [
'id' => (int)$r->id,
'instance_id' => (int)$r->instance_id,
'approver_id' => (int)$r->approver_id,
'step_index' => (int)$r->step_index,
'action' => $r->action,
'status' => $r->status,
'comment' => $r->comment,
'approved_at' => $r->approved_at,
'created_at' => $r->created_at,
'updated_at' => $r->updated_at,
'approver_name'=> property_exists($r, 'approver_name') ? $r->approver_name : null,
'step_name' => $r->step_name,
'step_type' => $r->step_type,
];
}, $records),
'summary' => $summary,
];
}
/** 简单时间线 */
public function getSimpleTimeline(int $instanceId, ?string $userNameColumn = null, string $initiatorPlacement = 'prepend'): array
{
// 基础数据
$instance = Db::table('approval_instances')->where('id', $instanceId)->first();
if (!$instance) throw new BusinessException('审批实例未找到');
$flow = Db::table('approval_flows')->where('type', $instance->flow_type)->first();
if (!$flow) throw new BusinessException('审批流程定义未找到');
$steps = json_decode($flow->steps, true) ?: [];
$formData = json_decode($instance->form_data ?? '{}', true) ?: [];
$curStep = (int)$instance->current_step;
$round = (int)$instance->round;
// 申请人
$initiatorId = (int)$instance->initiator_id;
$initiatorName = null;
if ($userNameColumn) {
$initiatorName = Db::table('user')->where('id', $initiatorId)->value($userNameColumn);
}
// 审批记录(仅当前轮次)
$records = Db::table('approval_records as ar')
->where('ar.instance_id', $instanceId)
->where('ar.round', $round)
->orderBy('ar.step_index')
->orderByRaw('COALESCE(ar.approved_at, ar.created_at) asc')
->select('ar.*')
->get()
->toArray();
// 一次性拉取姓名,避免 N+1
$nameMap = [];
if ($userNameColumn && !empty($records)) {
$uids = [];
foreach ($records as $r) $uids[] = (int)$r->approver_id;
$uids = array_values(array_unique($uids));
if (!empty($uids)) {
$pairs = Db::table('user')->whereIn('id', $uids)->pluck($userNameColumn, 'id')->toArray();
foreach ($pairs as $uid => $name) $nameMap[(int)$uid] = $name;
}
}
// 记录分组
$byStep = [];
foreach ($records as $r) {
$idx = (int)$r->step_index;
$byStep[$idx][] = $r;
}
$timeline = [];
// 可选:发起申请块
if ($initiatorPlacement === 'prepend') {
$appliedAt = $instance->created_at ?: null;
$timeline[] = [
'index' => -1,
'name' => '发起申请',
'type' => '-',
'status' => 'submitted',
'started_at' => $appliedAt,
'finished_at' => $appliedAt,
'duration_seconds' => 0,
'initiator_id' => $initiatorId,
'initiator_name' => $initiatorName,
];
}
// 每一步
foreach ($steps as $i => $meta) {
$name = $meta['name'] ?? ('Step '.$i);
$typeRaw = strtolower($meta['type'] ?? 'all');
$type = in_array($typeRaw, ['any','all']) ? $typeRaw : 'all';
$condStr = $meta['condition'] ?? null;
$condOk = $condStr ? $this->evaluateCondition($condStr, $formData) : true;
$recs = $byStep[$i] ?? [];
$hasRecs = !empty($recs);
$status = 'pending';
$startedAt = null;
$finishedAt = null;
$durationSec = null;
// 每一步的“审批意见”(只展示有内容的 comment)
$opinions = []; // [{approver_id, approver_name?, action, status, comment, time}...]
if ($hasRecs) {
// 统计时间
$minCreated = null;
$minApproveAt = null; $maxApproveAt = null;
$minRejectAt = null;
$maxTimeoutAt = null;
$cnt = ['pending'=>0,'approved'=>0,'rejected'=>0,'timeout'=>0];
foreach ($recs as $rr) {
// 收集“审批意见”(非空 comment 都算)
$cmt = trim((string)($rr->comment ?? ''));
if ($cmt !== '') {
$opinions[] = [
'approver_id' => (int)$rr->approver_id,
'approver_name' => $userNameColumn ? ($nameMap[(int)$rr->approver_id] ?? null) : null,
'action' => $rr->action, // approve | reject | null
'status' => $rr->status, // approved|rejected|pending|skipped|timeout
'comment' => $cmt,
'time' => $rr->approved_at ?: $rr->created_at,
];
}
if ($rr->created_at && (!$minCreated || $rr->created_at < $minCreated)) $minCreated = $rr->created_at;
if ($rr->approved_at) {
if ($rr->status === 'approved') {
if (!$minApproveAt || $rr->approved_at < $minApproveAt) $minApproveAt = $rr->approved_at;
if (!$maxApproveAt || $rr->approved_at > $maxApproveAt) $maxApproveAt = $rr->approved_at;
} elseif ($rr->status === 'rejected') {
if (!$minRejectAt || $rr->approved_at < $minRejectAt) $minRejectAt = $rr->approved_at;
} elseif ($rr->status === 'timeout') {
if (!$maxTimeoutAt || $rr->approved_at > $maxTimeoutAt) $maxTimeoutAt = $rr->approved_at;
}
}
if (isset($cnt[$rr->status])) $cnt[$rr->status]++;
}
// 时间升序排序 opinions
if (!empty($opinions)) {
usort($opinions, function($a, $b) {
return strcmp($a['time'] ?? '', $b['time'] ?? '');
});
}
$startedAt = $minCreated;
// 总结果(注意:这里按你现在的签名调用,含 round)
$outcome = $this->getStepOutcome($instanceId, $round, $i, $type); // 'pending'|'approve'|'reject'
if ($outcome === 'reject') {
$status = 'rejected';
$finishedAt = $minRejectAt;
} elseif ($outcome === 'approve') {
$status = 'approved';
$finishedAt = ($type === 'any') ? $minApproveAt : $maxApproveAt;
} else {
if ($cnt['timeout'] > 0 && $cnt['approved'] === 0 && $cnt['rejected'] === 0 && $cnt['pending'] === 0) {
$status = 'timeout';
$finishedAt = $maxTimeoutAt;
} else {
$status = 'processing';
}
}
if ($startedAt && $finishedAt) {
$durationSec = max(0, strtotime($finishedAt) - strtotime($startedAt));
}
} else {
// 无记录:根据位置与条件展示
if ($i < $curStep) {
$status = $condOk ? 'approved' : 'skipped';
} elseif ($i === $curStep && $instance->status === 'processing') {
$status = 'processing';
} else {
$status = $condOk ? 'pending' : 'skipped';
}
}
$row = [
'index' => (int)$i,
'name' => $name,
'type' => $type,
'status' => $status,
'started_at' => $startedAt,
'finished_at' => $finishedAt,
'duration_seconds' => $durationSec,
'opinions' => $opinions, // 这就是每个审批意见
];
// 把申请人信息并到第一步(可选)
if ($initiatorPlacement === 'attach' && empty($timeline)) {
$row['initiator_id'] = $initiatorId;
$row['initiator_name'] = $initiatorName;
}
$timeline[] = $row;
}
return $timeline;
}
/** 按业务ID获取“最新进展”的极简状态 */
public function getBriefStatusByBiz(string $bizId, ?string $bizType = null, ?string $userNameColumn = null): array
{
// 1) 最新实例
$q = Db::table('approval_instances')->where('biz_id', $bizId);
if ($bizType !== null && $bizType !== '') {
$q->where('biz_type', $bizType);
}
$instance = $q->orderByDesc('id')->first();
if (!$instance) {
// throw new BusinessException('未找到对应的审批实例');
return [];
}
$base = [
'instance_id' => (int)$instance->id,
'flow_type' => $instance->flow_type,
'biz_type' => $instance->biz_type,
'biz_id' => $instance->biz_id,
'status' => $instance->status,
'finished_at' => $instance->finished_at,
'updated_at' => $instance->updated_at,
];
// 2) 已完结:直接返回
if ($instance->status !== 'processing') {
return $base;
}
// 3) 进行中:取当前步骤 + 当前 pending 审批者
$flow = Db::table('approval_flows')->where('type', $instance->flow_type)->first();
if (!$flow) {
// 容错:没有流程定义
return $base;
}
$steps = json_decode($flow->steps, true) ?: [];
$idx = (int)$instance->current_step;
$round = (int)$instance->round;
$meta = $steps[$idx] ?? [];
$typeRaw = strtolower($meta['type'] ?? 'all');
$type = in_array($typeRaw, ['any','all']) ? $typeRaw : 'all';
// 当前步的所有记录
$recsQ = Db::table('approval_records')->where('instance_id', $instance->id)->where('step_index', $idx);
// 待处理人(只列出 pending)
if ($userNameColumn) {
$pendingRows = Db::table('approval_records as ar')
->leftJoin('user as u', 'ar.approver_id', '=', 'u.id')
->where('ar.instance_id', $instance->id)
->where('ar.round', $round)
->where('ar.step_index', $idx)
->where('ar.status', 'pending')
->select('ar.approver_id', Db::raw("u.`{$userNameColumn}` as approver_name"))
->get()
->toArray();
} else {
$pendingRows = Db::table('approval_records')
->where('instance_id', $instance->id)
->where('round', $round)
->where('step_index', $idx)
->where('status', 'pending')
->select('approver_id')
->get()
->toArray();
}
$pendingApprovers = [];
foreach ($pendingRows as $r) {
$row = ['approver_id' => (int)$r->approver_id];
if ($userNameColumn && property_exists($r, 'approver_name')) {
$row['approver_name'] = $r->approver_name;
}
$pendingApprovers[] = $row;
}
// 当前步的聚合状态(进行中通常返回 processing;若全被超时任务置为 timeout,则返回 timeout)
// 先快速统计是否纯 timeout
$stats = Db::table('approval_records')
->where('instance_id', $instance->id)
->where('round', $round)
->where('step_index', $idx)
->selectRaw("SUM(CASE WHEN status='pending' THEN 1 ELSE 0 END) as c_pending")
->selectRaw("SUM(CASE WHEN status='approved' THEN 1 ELSE 0 END) as c_approved")
->selectRaw("SUM(CASE WHEN status='rejected' THEN 1 ELSE 0 END) as c_rejected")
->selectRaw("SUM(CASE WHEN status='timeout' THEN 1 ELSE 0 END) as c_timeout")
->first();
$stepStatus = 'processing';
if ($stats && (int)$stats->c_timeout > 0 && (int)$stats->c_pending === 0 && (int)$stats->c_approved === 0 && (int)$stats->c_rejected === 0) {
$stepStatus = 'timeout';
}
$base['step'] = [
'index' => $idx,
'name' => $meta['name'] ?? ('Step '.$idx),
'type' => $type, // any|all
'status' => $stepStatus, // processing|timeout
'pending_approvers' => $pendingApprovers, // 当前正在等待的审批者
];
return $base;
}
/** 登录/首屏初始化:获取当前用户的待办总数 */
public function getInitTodoCount(int $userId): int
{
$userId = (int)$userId;
if ($userId <= 0) {
throw new BusinessException('无效的用户ID');
}
$row = Db::table('approval_records as ar')
->join('approval_instances as ai', 'ai.id', '=', 'ar.instance_id')
->where('ar.approver_id', $userId)
->where('ar.status', 'pending')
->where('ai.status', 'processing')
// 只统计实例“当前轮次、当前步骤”的待办,避免历史残留干扰
->whereRaw('ar.round = ai.round')
->whereRaw('ar.step_index = ai.current_step')
->selectRaw('COUNT(*) as c')
->first();
return (int)($row->c ?? 0);
}
}

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