335 Commits

Author SHA1 Message Date
mzl92 43ea23307d Merge branch 'master' into develop 2026-03-16 14:37:54 +08:00
18338781771 4ec72f7772 Merge branch 'feature/stock' into develop 2026-03-03 17:44:18 +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 a16429ff3a 物品归还 2026-02-04 09:28:54 +08:00
mzl 59750d52f6 Merge branch 'master' into develop 2026-02-03 11:15:04 +08:00
18338781771 bce47e81f8 Merge branch 'hotfix/customer' into develop 2026-02-03 10:51:30 +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
mzl92 278dda4f7a Merge branch 'master' into develop 2026-01-22 10:15:19 +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 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
mzl92 725710abcb ts 2025-12-09 07:33:04 +00:00
mzl92 399eaa2b5c 物品领用 2025-12-05 09:22:53 +00:00
mzl92 253616b2bb Merge branch 'master' into develop 2025-12-02 18:02:21 +08:00
mzl92 beabfd96fb Merge branch 'master' into develop 2025-12-02 17:34:12 +08:00
mzl92 cc93979e9a Merge branch 'master' into develop 2025-12-02 17:30:49 +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 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
18338781771 c4a2b39edd Merge branch 'feature/transmit' into develop 2025-12-01 18:16:02 +08:00
18338781771 45169572b5 Merge branch 'feature/transmit' into develop 2025-12-01 17:51:25 +08:00
18338781771 631d433b7e Merge branch 'feature/transmit' into develop 2025-11-29 17:50:21 +08:00
18338781771 9fbe094a30 Merge branch 'feature/transmit' into develop 2025-11-29 16:14:20 +08:00
18338781771 1ba1d5763f Merge branch 'feature/transmit' into develop 2025-11-29 15:55:56 +08:00
18338781771 5086344742 Merge branch 'feature/transmit' into develop 2025-11-29 15:51:42 +08:00
18338781771 63cca12a8d Merge branch 'feature/transmit' into develop 2025-11-29 15:46:27 +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
mzl92 00bd5ada53 ts 2025-11-28 17:37: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
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 20188521fd ts 2025-11-13 11:45:38 +08:00
mzl92 1d4224bf4a Merge branch 'master' into develop 2025-11-13 11:42:34 +08:00
mzl92 8b8336437a Merge branch 'master' into develop 2025-11-13 10:06:30 +08:00
mzl92 c157b3ee12 更新类库 2025-11-12 11:35: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 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 f73bad8082 Merge branch 'develop' into feature/sku_crawl 2025-11-04 13:11:27 +08:00
mzl92 be4dcb4dd4 删除增加过滤 2025-11-04 13:10:46 +08:00
mzl92 1eccabb806 Finish mzl_dev 2025-11-04 10:33:40 +08:00
mzl92 013fc83362 Merge branch 'develop' into feature/mzl_dev 2025-11-04 10:32:22 +08:00
mzl92 42bf32a9a5 修正编辑后的备货单绕过文件上传 2025-11-04 10:31:45 +08:00
mzl92 2f8f7dccf9 调整备货单功能 2025-11-03 18:07:56 +08:00
18338781771 49e7709ae3 Merge branch 'feature/fin' into develop 2025-11-01 14:59:46 +08:00
18338781771 5b77b8ed83 待入账 2025-11-01 14:59:41 +08:00
18338781771 a98769fb4c Merge branch 'feature/fin' into develop 2025-11-01 14:44:58 +08:00
18338781771 56ad551707 收汇凭证倒计时 2025-11-01 14:44:47 +08:00
18338781771 2711ed8f6a Merge branch 'feature/fin' into develop 2025-11-01 14:14:01 +08:00
18338781771 f1d5fae122 入账记录汇率 2025-11-01 14:13:55 +08:00
18338781771 4f965f8b5f Merge branch 'feature/fin' into develop 2025-11-01 14:02:21 +08:00
18338781771 4dbd72acce 待入账与入账实际汇率 2025-11-01 14:02:15 +08:00
18338781771 0c789b7a1a Merge branch 'feature/fin' into develop
# Conflicts:
#	app/common/service/FinReceiptService.php
2025-11-01 13:37:39 +08:00
18338781771 8674063e6e 付款记录作废 2025-11-01 13:36:18 +08:00
18338781771 1094bc406f Revert "作废"
This reverts commit 139d3509f4.
2025-11-01 13:22:45 +08:00
18338781771 596313ed5a Merge branch 'feature/fin' into develop 2025-11-01 13:20:38 +08:00
18338781771 74ac8941ea 统计 2025-11-01 13:20:33 +08:00
18338781771 6493bdebb1 待入账 2025-11-01 11:34:49 +08:00
18338781771 07a3d66079 Merge branch 'feature/fin' into develop 2025-11-01 11:32:16 +08:00
18338781771 18eb3e7c5d 统计 2025-11-01 11:32:10 +08:00
18338781771 262add20f8 Merge branch 'feature/fin' into develop
# Conflicts:
#	app/common/service/FinReceiptService.php
2025-11-01 09:47:56 +08:00
18338781771 139d3509f4 作废 2025-11-01 09:45:41 +08:00
18338781771 f7faf79809 待入账列表 2025-11-01 08:56:48 +08:00
18338781771 29ba35f9b6 收付款统计 2025-10-31 16:22:28 +08:00
18338781771 0e38f0f9bb Merge branch 'feature/fin' into develop 2025-10-31 10:17:52 +08:00
18338781771 43fe263df1 备货单编号 2025-10-31 10:17:41 +08:00
mzl92 58a38d1463 Finish mzl_dev 2025-10-31 09:47:58 +08:00
mzl92 821f455e47 Merge branch 'develop' into feature/mzl_dev 2025-10-31 09:47:48 +08:00
mzl92 24805257c7 调整内勤仪表盘曲线查询 2025-10-31 09:46:57 +08:00
18338781771 b4ce4fe672 Merge branch 'feature/fin' into develop 2025-10-30 18:49:33 +08:00
18338781771 206eff87e2 附件 2025-10-30 18:49:28 +08:00
18338781771 2e85b46a3d Merge branch 'feature/fin' into develop 2025-10-30 18:37:46 +08:00
18338781771 72fa638cf5 排序优化 2025-10-30 18:37:40 +08:00
18338781771 5f25ac9895 Merge branch 'feature/fin' into develop 2025-10-30 18:12:34 +08:00
18338781771 ad67a88d0f 水单多文件 2025-10-30 18:11:50 +08:00
mzl92 4c3f8c9cf7 Finish mzl_dev 2025-10-30 17:55:32 +08:00
mzl92 3f8dbf97e4 Merge branch 'develop' into feature/mzl_dev 2025-10-30 17:55:27 +08:00
mzl92 e9636513ec 临时处理仪表盘显示 2025-10-30 17:55:08 +08:00
18338781771 9f167ad526 Merge branch 'feature/fin' into develop 2025-10-30 17:54:43 +08:00
18338781771 d737281283 水单多文件 2025-10-30 17:54:38 +08:00
18338781771 695c398962 Merge branch 'feature/fin' into develop 2025-10-30 17:38:46 +08:00
18338781771 0110e393ce 付款单关联备货单优化 2025-10-30 17:38:40 +08:00
18338781771 ea0d5b14bc Merge branch 'feature/fin' into develop 2025-10-30 17:21:20 +08:00
18338781771 a3819bcba7 入账资料附件格式处理 2025-10-30 17:21:10 +08:00
mzl92 3d6aa5f365 Finish mzl_dev 2025-10-30 17:10:06 +08:00
mzl92 79d0ae2cbd Merge branch 'develop' into feature/mzl_dev 2025-10-30 17:10:00 +08:00
mzl92 659f6d4866 ts 2025-10-30 17:09:41 +08:00
mzl92 1b4914f7b7 Finish mzl_dev 2025-10-30 16:52:26 +08:00
mzl92 c8cdc5ba00 Merge branch 'develop' into feature/mzl_dev 2025-10-30 16:52:18 +08:00
mzl92 94e9d6eb7e xg 2025-10-30 16:51:58 +08:00
mzl92 3d1a4ceeb5 Finish mzl_dev 2025-10-30 16:43:10 +08:00
mzl92 7db7e652de Merge branch 'develop' into feature/mzl_dev 2025-10-30 16:43:01 +08:00
mzl92 9fbe77a3a9 修改认领 2025-10-30 16:42:42 +08:00
18338781771 4c63c1b669 Merge branch 'feature/fin' into develop 2025-10-30 16:29:59 +08:00
18338781771 f64324fba5 超时未上传入账资料筛选 2025-10-30 16:29:54 +08:00
18338781771 499b0894b7 已认领筛选 2025-10-30 15:24:05 +08:00
18338781771 88ebef4fc3 Merge branch 'feature/fin' into develop 2025-10-30 14:58:40 +08:00
18338781771 15df6ad9a3 超时未认领筛选 2025-10-30 14:58:29 +08:00
mzl92 02fe911a44 Finish mzl_dev 2025-10-30 14:40:43 +08:00
mzl92 5facb1abba Merge branch 'develop' into feature/mzl_dev 2025-10-30 14:40:35 +08:00
mzl92 077edc5fd6 Merge branch 'feature/mzl_dev' into develop
# Conflicts:
#	config/route.php
2025-10-30 14:40:24 +08:00
mzl92 ef0c236130 Merge branch 'develop' into feature/mzl_dev
# Conflicts:
#	config/route.php
2025-10-30 14:39:35 +08:00
mzl92 8c3a445a39 Merge branch 'develop' into feature/mzl_dev
# Conflicts:
#	config/route.php
2025-10-30 14:39:10 +08:00
mzl92 a7d623656d 内勤仪表盘 2025-10-30 14:37:21 +08:00
18338781771 f004d99802 Merge branch 'feature/fin' into develop 2025-10-30 14:27:11 +08:00
18338781771 ea3d511c4d 未入账金额 2025-10-30 14:26:59 +08:00
18338781771 25f8017b3c 关键词抓取车型后创建 2025-10-30 14:15:18 +08:00
18338781771 59f474a037 Merge branch 'feature/fin' into develop 2025-10-30 13:35:59 +08:00
18338781771 672258e594 收款统计 2025-10-30 13:35:53 +08:00
18338781771 9f944ae92c 匹配排序优化 2025-10-30 09:52:20 +08:00
18338781771 c2d291bed4 Merge branch 'feature/fin' into develop 2025-10-29 18:38:42 +08:00
18338781771 f3b1c9bae4 付款记录排序 2025-10-29 18:38:37 +08:00
18338781771 346d67c084 超过24小时邮件通知提交入账资料 2025-10-29 18:05:23 +08:00
18338781771 eacdb93be2 Merge branch 'feature/fin' into develop 2025-10-29 15:58:29 +08:00
18338781771 cf45acbf0f 入账资料时间 2025-10-29 15:58:23 +08:00
18338781771 b235747a9a Merge branch 'feature/fin' into develop 2025-10-29 15:48:09 +08:00
18338781771 b440c03241 超时未认领筛选 2025-10-29 15:47:51 +08:00
18338781771 2b2e821779 Merge branch 'feature/fin' into develop 2025-10-29 14:23:53 +08:00
18338781771 07c0b2a2f3 收付款 2025-10-29 14:14:40 +08:00
18338781771 6baee549c0 Merge branch 'hotfix/series' into develop 2025-10-29 11:48:18 +08:00
18338781771 8a1b2f2369 系统设置 2025-10-28 17:05:23 +08:00
18338781771 2127473290 系统设置 2025-10-28 16:59:51 +08:00
18338781771 77dad7daed 模糊抓取车型 2025-10-28 16:19:37 +08:00
18338781771 38effed458 Merge branch 'feature/fin' into develop 2025-10-27 14:52:10 +08:00
18338781771 1bd2eee8db 入账时间 2025-10-27 14:52:04 +08:00
18338781771 36c7d19319 Merge branch 'feature/fin' into develop 2025-10-27 11:57:06 +08:00
18338781771 19cb169abd 历史记录 2025-10-27 11:57:00 +08:00
18338781771 a85fa8f072 Merge branch 'feature/fin' into develop 2025-10-27 10:57:54 +08:00
18338781771 83d76b16b4 确认入账 2025-10-27 10:57:47 +08:00
18338781771 2f9f483661 Merge branch 'feature/fin' into develop
# Conflicts:
#	app/controller/IndexController.php
#	config/route.php
2025-10-27 10:23:52 +08:00
18338781771 7fd28461fe Merge branch 'feature/audit' into develop 2025-10-24 16:32:34 +08:00
mzl92 5cc67b78f1 Finish mzl_dev 2025-10-24 15:47:18 +08:00
mzl92 770664e70f Merge branch 'develop' into feature/mzl_dev 2025-10-24 15:47:12 +08:00
mzl92 6bd4ed9ec3 ts备货单 2025-10-24 15:46:20 +08:00
18338781771 211fe575b8 Merge branch 'feature/audit' into develop 2025-10-24 15:25:44 +08:00
18338781771 18786591b1 Merge branch 'feature/audit' into develop 2025-10-24 15:02:01 +08:00
mzl92 1c3585b079 Finish mzl_dev 2025-10-24 11:04:35 +08:00
mzl92 8db33a40a4 Merge branch 'develop' into feature/mzl_dev 2025-10-24 11:04:27 +08:00
mzl92 c1ab40031d 调整采购单 2025-10-24 11:02:54 +08:00
mzl92 2c8efc18f9 Finish mzl_dev 2025-10-24 10:44:12 +08:00
mzl92 03ab55bd63 Merge branch 'develop' into feature/mzl_dev 2025-10-24 10:44:00 +08:00
mzl92 4326bcd68b 调整备货单 2025-10-24 10:43:36 +08:00
18338781771 7036e3b849 排序规则调整 2025-10-23 17:09:40 +08:00
18338781771 0f505d5c56 收款记录联查付款单 2025-10-23 16:25:02 +08:00
18338781771 33efdc8534 Merge branch 'hotfix/port' into develop 2025-10-23 14:19:43 +08:00
18338781771 6644cc1c58 收款记录 2025-10-23 11:27:24 +08:00
mzl92 797256e707 Finish mzl_dev 2025-10-22 16:16:01 +08:00
mzl92 448d151106 Merge branch 'develop' into feature/mzl_dev 2025-10-22 16:15:55 +08:00
mzl92 7486803468 调整pdf模板文件 2025-10-22 16:15:08 +08:00
18338781771 4c1ac68a2b Merge branch 'hotfix/sku' into develop 2025-10-22 10:45:24 +08:00
18338781771 1cb5a1d5b2 Merge branch 'hotfix/sku' into develop 2025-10-22 10:35:50 +08:00
mzl92 55850aef81 Finish mzl_dev 2025-10-21 19:54:42 +08:00
mzl92 13ab4b7868 根据10.17沟通调整 2025-10-21 19:53:21 +08:00
mzl92 67ef59f9fd Merge branch 'master' into develop 2025-10-21 15:11:45 +08:00
mzl92 07f9a4306f Merge branch 'master' into develop 2025-10-21 14:46:50 +08:00
mzl92 f17646238e Merge branch 'master' into develop 2025-10-21 14:06:24 +08:00
mzl92 03d010ec88 Finish mzl_dev 2025-10-21 13:41:40 +08:00
mzl92 13d24d041d Merge branch 'develop' into feature/mzl_dev 2025-10-21 13:41:27 +08:00
mzl92 4c94392ef6 ts 2025-10-21 13:35:17 +08:00
mzl92 5c6978c803 Merge branch 'master' into develop 2025-10-16 09:15:53 +08:00
18338781771 73ed4a413c 付款账号名 2025-10-09 10:24:13 +08:00
18338781771 d05106f9df 收款记录 2025-10-09 10:16:21 +08:00
18338781771 90cce108b0 收款记录创建与修改人 2025-09-28 10:10:15 +08:00
18338781771 a3a6cfefc0 收款附件优化 2025-09-28 09:56:01 +08:00
18338781771 40dfd99f4e 关联查询优化 2025-09-28 09:47:09 +08:00
18338781771 ae6cd4ec78 变更历史 2025-09-26 16:43:09 +08:00
18338781771 ec2502cd58 收款 2025-09-25 17:48:31 +08:00
18338781771 e0b11718a7 收款记录关联备货单 2025-09-24 17:55:16 +08:00
18338781771 82e4435a2c 收汇附件名称 2025-09-24 17:49:14 +08:00
18338781771 435d3ce677 付款记录 2025-09-24 15:23:47 +08:00
18338781771 abc1142e88 绑定付款记录 2025-09-24 10:16:34 +08:00
18338781771 54f83679ea 收款记录编辑 2025-09-23 18:38:02 +08:00
18338781771 db10ece6b7 付款详情-备货单 2025-09-23 18:30:13 +08:00
18338781771 870bca5e57 付款详情-备货单 2025-09-23 18:26:17 +08:00
18338781771 1d6caad7b0 付款申请参数结构修改 2025-09-23 17:33:09 +08:00
18338781771 e6d3cc3f04 付款申请查询相关字段名 2025-09-23 16:45:06 +08:00
18338781771 4717bb01ca 付款与收款 2025-09-23 15:14:08 +08:00
18338781771 36d1d333b1 付款与收款 2025-09-23 15:13:56 +08:00
74 changed files with 9689 additions and 1964 deletions
+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());
}
}
}
+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);
}
}
+18 -1
View File
@@ -4,6 +4,7 @@ namespace app\common\controller;
use app\common\service\AuthService; use app\common\service\AuthService;
use app\common\validate\UserValidate; use app\common\validate\UserValidate;
use Exception;
use support\Request; use support\Request;
class AuthController extends BaseController class AuthController extends BaseController
@@ -31,7 +32,8 @@ class AuthController extends BaseController
public function feishuScope(Request $request) public function feishuScope(Request $request)
{ {
try { 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); return $this->success($result);
} catch (\Throwable $e) { } catch (\Throwable $e) {
return $this->error($e->getMessage()); return $this->error($e->getMessage());
@@ -112,4 +114,19 @@ class AuthController extends BaseController
// (new AuthService())->blockToken($token, $systemCode); // (new AuthService())->blockToken($token, $systemCode);
// return $this->success([], '已强制停用该token'); // 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());
}
}
} }
@@ -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,
]);
}
}
+10 -1
View File
@@ -196,7 +196,15 @@ class AuthLogic
{ {
$certificate = $this->feishuAppInfo($data['corp_abbr'], $data['system_code']); $certificate = $this->feishuAppInfo($data['corp_abbr'], $data['system_code']);
$feishu = new Feishu($certificate['app_id'], $certificate['app_secret'], $certificate['abbr']); $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']); $userInfo = $feishu->userInfo($userAccessToken['access_token']);
if ($userInfo['code'] != 0) { if ($userInfo['code'] != 0) {
@@ -298,6 +306,7 @@ class AuthLogic
'app_id' => $fsAppInfo->app_id, 'app_id' => $fsAppInfo->app_id,
'app_secret' => $fsAppInfo->app_secret, 'app_secret' => $fsAppInfo->app_secret,
'r_url' => $fsAppInfo->r_url, 'r_url' => $fsAppInfo->r_url,
'm_url' => $fsAppInfo->m_url,
'scope' => $fsAppInfo->scopes, 'scope' => $fsAppInfo->scopes,
'state' => $fsAppInfo->state, 'state' => $fsAppInfo->state,
'abbr' => $corpAbbr, 'abbr' => $corpAbbr,
+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;
}
+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'];
/** /**
* 不允许自动填充模型字段 * 不允许自动填充模型字段
+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');
}
}
+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(); $logic = new AuthLogic();
$certificate = $logic->feishuAppInfo($corpAbbr, $systemCcode); $certificate = $logic->feishuAppInfo($corpAbbr, $systemCcode);
$feishu = new Feishu($certificate['app_id'], $certificate['app_secret'], $certificate['abbr']); $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); (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;
}
} }
+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;
}
}
}
+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 = [ $arr = [
'app_id' => $certificate['app_id'], 'app_id' => $certificate['app_id'],
'redirect_uri' => $certificate['r_url'], 'redirect_uri' => $mobile == 1 ? $certificate['m_url'] : $certificate['r_url'],
'scope' => $certificate['scope'], 'scope' => $certificate['scope'],
'state' => $certificate['state'] 'state' => $certificate['state']
]; ];
@@ -225,4 +225,101 @@ class Feishu
// ]; // ];
// return $this->requestResult($url); // 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);
}
}
@@ -0,0 +1,410 @@
<?php
namespace app\common\utils\approval\traits;
use support\Db;
use support\exception\BusinessException;
trait ApprovalResolverTrait
{
/** 解析审批人(role/user/initiator_leader/field_user/dept_head/...) */
private function resolveApprovers(array $step, object $instance, bool $avoidSelf = true): array
{
$resolved = [];
$formData = json_decode($instance->form_data ?? '{}', true) ?: [];
$confs = $step['approvers'] ?? [];
foreach ($confs as $cfg) {
$typ = $cfg['type'] ?? '';
$val = $cfg['value'] ?? null;
$ids = [];
switch ($typ) {
case 'role':
$sys = isset($cfg['system_id']) ? (int)$cfg['system_id'] : null;
$includeGlobal = array_key_exists('include_global', $cfg) ? (bool)$cfg['include_global'] : true;
$ids = $this->getUserIdsByRole($val, $sys, true, $includeGlobal);
break;
case 'user':
$ids = [(int)$val];
break;
case 'initiator_leader':
$leaderId = $this->getLeaderIdByUserId((int)$instance->initiator_id);
if ($leaderId) $ids = [(int)$leaderId];
break;
case 'field_user':
if (isset($formData[$val])) $ids = [(int)$formData[$val]];
break;
case 'dept_head':
$head = $this->getDeptHeadId((int)$val);
if ($head) $ids = [(int)$head];
break;
case 'group_members': {
// $gid = (int)($cfg['group_id'] ?? 0);
$gid = (int)($cfg['value'] ?? 0);
if ($gid <= 0) throw new BusinessException('group_members 需要 group_id');
$ids = $this->getGroupMemberIds($gid); // A 方案
break;
}
case 'scenario_leader_of_initiator': {
$formData = json_decode($instance->form_data ?? '{}', true) ?: [];
// $scenario = $cfg['scenario'] ?? ($formData[$cfg['scenario_field']] ?? null);
$scenario = $val ?? ($formData[$cfg['scenario_field']] ?? null);
if (!$scenario) throw new BusinessException('缺少业务场景(scenario 或 scenario_field)');
$mustBelong = array_key_exists('must_belong', $cfg) ? (bool)$cfg['must_belong'] : true;
$preferPrimary = array_key_exists('prefer_primary', $cfg) ? (bool)$cfg['prefer_primary'] : true;
$ids = $this->getLeadersByScenarioForInitiator(
(int)$instance->initiator_id,
(string)$scenario,
$mustBelong,
$preferPrimary
); // B 方案(按场景)
break;
}
default:
throw new BusinessException("不支持的审批人类型: {$typ}");
}
$resolved = array_merge($resolved, $ids);
}
$resolved = array_unique(array_filter(array_map('intval', $resolved)));
if ($avoidSelf) {
// $resolved = array_values(array_filter($resolved, fn($id) => $id !== (int)$instance->initiator_id));
$resolved = array_values($resolved);
}
if (empty($resolved)) {
throw new BusinessException("未解析到任何审批人");
}
return $resolved;
}
/** 通知抄送人 */
private function notifyCCs(array $step, object $instance, string $action = 'pending'): void
{
$ccConfig = $step['cc'] ?? [];
if (empty($ccConfig)) return;
$formData = json_decode($instance->form_data ?? '{}', true) ?: [];
$uids = [];
foreach ($ccConfig as $cc) {
$typ = $cc['type'] ?? '';
$val = $cc['value'] ?? null;
switch ($typ) {
case 'role':
$uids = array_merge($uids, $this->getUserIdsByRole($val, null, true, true));
break;
case 'user': $uids[] = (int)$val; break;
case 'initiator_leader':
$leaderId = $this->getLeaderIdByUserId((int)$instance->initiator_id);
if ($leaderId) $uids[] = (int)$leaderId;
break;
case 'field_user':
if (isset($formData[$val])) $uids[] = (int)$formData[$val];
break;
case 'dept_head':
$head = $this->getDeptHeadId((int)$val);
if ($head) $uids[] = (int)$head;
break;
case 'group_members': {
$gid = (int)($val ?? 0);
if ($gid <= 0) throw new BusinessException('group_members 需要 group_id');
$uids = $this->getGroupMemberIds($gid); // A 方案
break;
}
case 'scenario_leader_of_initiator': {
$formData = json_decode($instance->form_data ?? '{}', true) ?: [];
$scenario = $val ?? ($formData[$cc['scenario_field']] ?? null);
if (!$scenario) throw new BusinessException('缺少业务场景(scenario 或 scenario_field)');
$mustBelong = array_key_exists('must_belong', $cc) ? (bool)$cc['must_belong'] : true;
$preferPrimary = array_key_exists('prefer_primary', $cc) ? (bool)$cc['prefer_primary'] : true;
$uids = $this->getLeadersByScenarioForInitiator(
(int)$instance->initiator_id,
(string)$scenario,
$mustBelong,
$preferPrimary
); // B 方案(按场景)
break;
}
default: break;
}
}
$uids = array_unique(array_filter(array_map('intval', $uids)));
if (empty($uids)) return;
$now = date('Y-m-d H:i:s');
foreach ($uids as $uid) {
$exists = Db::table('approval_ccs')
->where('instance_id', $instance->id)
->where('user_id', $uid)
->count() > 0;
if (!$exists) {
Db::table('approval_ccs')->insert([
'instance_id' => $instance->id,
'user_id' => $uid,
'notified_at' => $now,
'created_at' => $now,
'updated_at' => $now,
]);
}
}
$this->sendNotification($uids, $instance, $action, true);
}
/** 条件求值 */
private function evaluateCondition(?string $condition, array $formData): bool
{
if (!$condition) return true;
$pattern = '/^(\w+)\s*(==|!=|>|<|>=|<=)\s*(.+)$/';
if (!preg_match($pattern, $condition, $m)) {
throw new BusinessException("条件格式错误: {$condition}");
}
$field = $m[1]; $op = $m[2]; $value = trim($m[3], "\"'");
$actual = $formData[$field] ?? null;
if (is_numeric($value) && is_numeric($actual)) { $value=(float)$value; $actual=(float)$actual; }
return match ($op) {
'==' => $actual == $value,
'!=' => $actual != $value,
'>' => $actual > $value,
'<' => $actual < $value,
'>=' => $actual >= $value,
'<=' => $actual <= $value,
default => throw new BusinessException("不支持的操作符: {$op}"),
};
}
/** 获取下一个有效步骤索引 */
private function getNextStepIndex(array $steps, array $formData, int $currentStepIndex): ?int
{
for ($i = $currentStepIndex + 1; $i < count($steps); $i++) {
$s = $steps[$i];
if (!isset($s['condition']) || $this->evaluateCondition($s['condition'], $formData)) {
return $i;
}
}
return null;
}
/** 聚合本步骤结论 */
private function getStepOutcome(int $instanceId, int $round, int $stepIndex, string $type): string
{
$records = Db::table('approval_records')
->where('instance_id', $instanceId)
->where('round', $round) //
->where('step_index', $stepIndex)
->get();
$pending = $approved = $rejected = $timeout = $skipped = 0;
foreach ($records as $r) {
switch ($r->status) {
case 'pending': $pending++; break;
case 'approved': $approved++; break;
case 'rejected': $rejected++; break;
case 'timeout': $timeout++; break;
case 'skipped': $skipped++; break;
}
}
$type = strtolower($type);
if ($type === 'any') {
if ($rejected > 0) return 'reject';
if ($approved > 0) return 'approve';
// 全为 skipped/timeout 且无 pending/rejected ⇒ 通过
if ($pending === 0 && $rejected === 0 && $approved === 0) return 'approve';
return 'pending';
}
if ($rejected > 0) return 'reject';
if ($pending > 0) return 'pending';
return 'approve';
}
/** 关闭本步其余待办为 skipped */
private function closeOtherPending(int $instanceId, int $round, int $stepIndex, array $keepApproverIds = []): void
{
$now = date('Y-m-d H:i:s');
$q = Db::table('approval_records')
->where('instance_id', $instanceId)
->where('round', $round) //
->where('step_index', $stepIndex)
->where('status', 'pending');
if (!empty($keepApproverIds)) $q->whereNotIn('approver_id', $keepApproverIds);
$q->update(['status'=>'skipped','approved_at'=>$now,'updated_at'=>$now]);
}
/** 创建待办(去重) */
private function insertApprovalRecord(object $instance, int $stepIndex, int $approverId): void
{
$round = (int)($instance->round ?? 1);
$now = date('Y-m-d H:i:s');
$exists = Db::table('approval_records')
->where('instance_id', (int)$instance->id)
->where('round', $round) // 限当前轮
->where('step_index', $stepIndex)
->where('approver_id', $approverId)
->where('status', 'pending')
->exists();
if ($exists) return;
Db::table('approval_records')->insert([
'instance_id' => (int)$instance->id,
'approver_id' => $approverId,
'step_index' => $stepIndex,
'round' => $round, //
'action' => null,
'status' => 'pending',
'comment' => null,
'approved_at' => null,
'created_at' => $now,
'updated_at' => $now,
]);
}
/** 取“某实例、某轮次、某步骤”里当前 pending 的审批人 */
private function getPendingApproverIds(int $instanceId, int $round, int $stepIndex): array
{
$ids = Db::table('approval_records')
->where('instance_id', $instanceId)
->where('round', $round)
->where('step_index', $stepIndex)
->where('status', 'pending')
->pluck('approver_id')->toArray();
return array_values(array_unique(array_map('intval', $ids)));
}
/** 从角色取用户ID */
private function getUserIdsByRole($roleIdOrName, ?int $systemId = null, bool $onlyActive = true, bool $includeGlobal = true): array
{
if (is_int($roleIdOrName) || (is_string($roleIdOrName) && ctype_digit($roleIdOrName))) {
$roleId = (int)$roleIdOrName;
$needJoin = $onlyActive || !is_null($systemId) || $includeGlobal === false;
if (!$needJoin) {
$rows = Db::table('user_roles')->where('role_id', $roleId)->select('user_id')->get();
} else {
$q = Db::table('user_roles as ur')
->join('roles as r', 'ur.role_id', '=', 'r.id')
->where('ur.role_id', $roleId)
->select('ur.user_id');
if ($onlyActive) $q->where('r.status', 1);
if (!is_null($systemId)) {
$includeGlobal ? $q->whereIn('r.system_id', [0, $systemId]) : $q->where('r.system_id', $systemId);
}
$rows = $q->get();
}
} else {
$name = (string)$roleIdOrName;
$q = Db::table('user_roles as ur')
->join('roles as r', 'ur.role_id', '=', 'r.id')
->where('r.name', $name)
->select('ur.user_id');
if ($onlyActive) $q->where('r.status', 1);
if (!is_null($systemId)) {
$includeGlobal ? $q->whereIn('r.system_id', [0, $systemId]) : $q->where('r.system_id', $systemId);
}
$rows = $q->get();
}
$ids = [];
foreach ($rows as $row) $ids[] = (int)$row->user_id;
return array_values(array_unique($ids));
}
private function getLeaderIdByUserId(int $userId): ?int
{
$id = Db::table('user')->where('id', $userId)->value('report_to');
return $id ? (int)$id : null;
}
private function getDeptHeadId(int $deptId): ?int
{
$id = Db::table('departments')->where('id', $deptId)->value('head_user_id');
return $id ? (int)$id : null;
}
/** A) 取组内全部成员 */
private function getGroupMemberIds(int $groupId): array
{
$rows = Db::table('user_group_members')
->where('group_id', $groupId)
->where('status', 1)
->pluck('user_id')->toArray();
return array_values(array_unique(array_map('intval', $rows)));
}
/** B) 按“场景”在发起人所在的组里找负责人 */
private function getLeadersByScenarioForInitiator(
int $initiatorId,
string $scenarioCode,
bool $mustBelong = true,
bool $preferPrimary = true
): array {
// 找出“该场景下发起人所在的所有组”
$groups = Db::table('user_group as g')
->join('user_group_members as m', 'm.group_id', '=', 'g.id')
->where('g.scenario_code', $scenarioCode)
->where('g.status', 1)
->where('m.user_id', $initiatorId)
->where('m.status', 1)
->orderByDesc('m.is_primary') // 主归属优先
->orderByDesc('g.sort') // 其次按组排序
->select('g.id', 'm.is_primary')
->get()->toArray();
if (empty($groups)) {
if ($mustBelong) {
throw new BusinessException('发起人在该业务场景下未加入任何分组');
}
// 不强制属于:允许只按“该场景下任一组”的负责人(不推荐)
$groups = Db::table('user_group as g')
->where('g.scenario_code', $scenarioCode)
->where('g.status', 1)
->orderByDesc('g.sort')
->select('g.id', Db::raw('0 as is_primary'))
->get()->toArray();
if (empty($groups)) {
throw new BusinessException('该业务场景下未配置任何分组');
}
}
// 选择一个“目标组”
$targetGroupId = null;
if ($preferPrimary) {
foreach ($groups as $g) {
if ((int)$g->is_primary === 1) { $targetGroupId = (int)$g->id; break; }
}
}
if (!$targetGroupId) {
$targetGroupId = (int)$groups[0]->id; // 用排序第一的组
}
// 取负责人(可支持多负责人)
$leaders = Db::table('user_group_members')
->where('group_id', $targetGroupId)
->where('is_leader', 1)
->where('status', 1)
->pluck('user_id')->toArray();
$ids = array_values(array_unique(array_map('intval', $leaders)));
if (empty($ids)) throw new BusinessException('目标分组未配置负责人');
return $ids;
}
public function StetApprovers(array $step, object $instance, bool $avoidSelf = true){
return$this->resolveApprovers($step, $instance, $avoidSelf);
}
}
@@ -0,0 +1,130 @@
<?php
namespace app\common\utils\approval\traits;
use support\Db;
use support\exception\BusinessException;
trait ApprovalTransferTrait
{
/** 查看某个实例的转交流水(按时间升序) */
public function listTransfers(int $instanceId): array
{
$rows = Db::table('approval_transfers')
->where('instance_id', $instanceId)
->orderBy('id', 'asc')
->get()
->toArray();
return array_map(function($r){
return [
'id' => (int)$r->id,
'instance_id' => (int)$r->instance_id,
'step_index' => (int)$r->step_index,
'record_id' => (int)$r->record_id,
'from_user_id' => (int)$r->from_user_id,
'to_user_id' => (int)$r->to_user_id,
'reason' => $r->reason,
'created_at' => $r->created_at,
];
}, $rows);
}
/** 转交当前待办给他人 */
public function transferApproval(
int $instanceId,
int $stepIndex,
int $fromUserId,
int $toUserId,
string $reason = '',
?string $userNameColumn = null
): array {
if ($fromUserId === $toUserId) {
return ['status' => 'noop', 'msg' => '目标与来源相同,无需转交'];
}
return \support\Db::transaction(function () use ($instanceId, $stepIndex, $fromUserId, $toUserId, $reason, $userNameColumn) {
// 锁实例,避免并发推进
$instance = Db::table('approval_instances')->where('id', $instanceId)->lockForUpdate()->first();
if (!$instance) throw new BusinessException('审批实例未找到');
if ($instance->status !== 'processing') throw new BusinessException('流程非进行中,无法转交');
// 锁定来源人的 pending 记录
$record = Db::table('approval_records')
->where('instance_id', $instanceId)
->where('round', (int)$instance->round)
->where('step_index', $stepIndex)
->where('approver_id', $fromUserId)
->where('status', 'pending')
->lockForUpdate()
->first();
if (!$record) throw new BusinessException('未找到可转交的待办或已处理');
$now = date('Y-m-d H:i:s');
// 若目标已有 pending:合并(本记录置为 skipped)
$target = Db::table('approval_records')
->where('instance_id', $instanceId)
->where('round', (int)$instance->round)
->where('step_index', $stepIndex)
->where('approver_id', $toUserId)
->where('status', 'pending')
->lockForUpdate()
->first();
if ($target) {
Db::table('approval_records')->where('id', $record->id)->update([
'status' => 'skipped',
'approved_at' => $now,
'updated_at' => $now,
'comment' => trim(($record->comment ?? '')."\n[转交给 {$toUserId},目标已有待办,已合并]"),
]);
$effectiveRecordId = (int)$target->id;
} else {
// 原地替换 approver_id
Db::table('approval_records')->where('id', $record->id)->update([
'approver_id' => $toUserId,
'updated_at' => $now,
'comment' => trim(($record->comment ?? '')."\n[自 {$fromUserId} 转交]"),
]);
$effectiveRecordId = (int)$record->id;
}
// 组织“转交来源/原因”文案(优先姓名)
$fromLabel = (string)$fromUserId;
$toLabel = (string)$toUserId;
if ($userNameColumn) {
$fromName = Db::table('user')->where('id', $fromUserId)->value($userNameColumn);
$toName = Db::table('user')->where('id', $toUserId)->value($userNameColumn);
if ($fromName) $fromLabel = $fromName;
if ($toName) $toLabel = $toName;
}
$note = "【转交】{$fromLabel} → {$toLabel}";
$r = trim($reason);
if ($r !== '') $note .= ",原因:{$r}";
// 通知被转交人(在内容里追加 note)
$this->sendNotification([$toUserId], (object)[
'id' => (int)$instance->id,
'flow_type' => (string)$instance->flow_type,
'biz_type' => (string)$instance->biz_type,
'biz_id' => (string)$instance->biz_id,
'initiator_id' => (int)$instance->initiator_id,
'form_data' => $instance->form_data,
], 'pending', false, ['note' => $note]);
// 记转交流水
Db::table('approval_transfers')->insert([
'instance_id' => (int)$instanceId,
'step_index' => (int)$stepIndex,
'record_id' => $effectiveRecordId,
'from_user_id' => (int)$fromUserId,
'to_user_id' => (int)$toUserId,
'reason' => $reason,
'created_at' => $now,
]);
$this->pushTodoChanged([$fromUserId, $toUserId]);
return ['status' => 'ok', 'record_id' => $effectiveRecordId];
});
}
}
+25
View File
@@ -0,0 +1,25 @@
<?php
namespace app\common\validate;
use think\Validate;
class InvItemValidate extends Validate
{
// 规则
protected $rule = [
];
// 提示
protected $message = [
];
// 场景
protected $scene = [
'update' => [
],
'qrCode' => [
],
];
}
@@ -0,0 +1,29 @@
<?php
namespace app\common\validate;
use think\Validate;
class InvRequisitionApproveValidate extends Validate
{
// 规则
protected $rule = [
];
// 提示
protected $message = [
];
// 场景
protected $scene = [
'store' => [
],
'update' => [
]
];
}
@@ -0,0 +1,29 @@
<?php
namespace app\common\validate;
use think\Validate;
class InvRequisitionAttachmentValidate extends Validate
{
// 规则
protected $rule = [
];
// 提示
protected $message = [
];
// 场景
protected $scene = [
'store' => [
],
'update' => [
]
];
}
@@ -0,0 +1,29 @@
<?php
namespace app\common\validate;
use think\Validate;
class InvRequisitionItemReturnAttachmentValidate extends Validate
{
// 规则
protected $rule = [
];
// 提示
protected $message = [
];
// 场景
protected $scene = [
'store' => [
],
'update' => [
]
];
}
@@ -0,0 +1,29 @@
<?php
namespace app\common\validate;
use think\Validate;
class InvRequisitionItemReturnValidate extends Validate
{
// 规则
protected $rule = [
];
// 提示
protected $message = [
];
// 场景
protected $scene = [
'store' => [
],
'update' => [
]
];
}
@@ -0,0 +1,29 @@
<?php
namespace app\common\validate;
use think\Validate;
class InvRequisitionItemValidate extends Validate
{
// 规则
protected $rule = [
];
// 提示
protected $message = [
];
// 场景
protected $scene = [
'store' => [
],
'update' => [
]
];
}
@@ -0,0 +1,29 @@
<?php
namespace app\common\validate;
use think\Validate;
class InvRequisitionReturnValidate extends Validate
{
// 规则
protected $rule = [
];
// 提示
protected $message = [
];
// 场景
protected $scene = [
'store' => [
],
'update' => [
]
];
}
@@ -0,0 +1,90 @@
<?php
namespace app\common\validate;
use think\Validate;
class InvRequisitionValidate extends Validate
{
// 规则
protected $rule = [
'applicant_id' => 'number',
'borrow_reason' => 'require|max:255',
'use_start_date' => 'require|date',
'use_end_date' => 'date',
'items' => 'require|array',
'items.*.item_id' => 'require|number',
'items.*.request_qty' => 'require|number|gt:0',
// 扫码领取规则
'item_id' => 'require|integer|gt:0',
'qty' => 'require|integer|gt:0',
// 管理员代领规则
'target_user_id' => 'integer|gt:0',
'target_user_ids' => 'array',
'target_user_ids.*' => 'integer|gt:0',
];
// 提示
protected $message = [
'applicant_id.number' => '申请人格式错误',
'borrow_reason.require' => '领用事由不能为空',
'borrow_reason.max' => '领用事由最多不能超过255个字符',
'use_start_date.require' => '使用开始日期不能为空',
'use_start_date.date' => '使用开始日期格式错误',
'use_end_date.date' => '预计归还日期格式错误',
'items.require' => '申请物品明细不能为空',
'items.array' => '申请物品明细格式错误',
'items.*.item_id.require' => '物品ID不能为空',
'items.*.item_id.number' => '物品ID格式错误',
'items.*.request_qty.require' => '申请数量不能为空',
'items.*.request_qty.number' => '申请数量格式错误',
'items.*.request_qty.gt' => '申请数量必须大于0',
'item_id.require' => '物品ID不能为空',
'item_id.integer' => '物品ID必须为整数',
'item_id.gt' => '物品ID必须大于0',
'qty.require' => '领取数量不能为空',
'qty.integer' => '领取数量必须为整数',
'qty.gt' => '领取数量必须大于0',
'target_user_id.require' => '代领用户不能为空',
];
// 场景
protected $scene = [
'store' => [
'applicant_id',
'borrow_reason',
'use_start_date',
'use_end_date',
'items'
],
'update' => [
'applicant_id',
'borrow_reason',
'use_start_date',
'use_end_date',
'items'
],
'apply' => [
'applicant_id',
'borrow_reason',
'use_start_date',
'use_end_date',
'items'
],
'qrClaim' => [
'item_id',
'qty',
],
'proxyClaim' => [
'item_id',
'qty',
'target_user_id',
'target_user_ids',
'borrow_reason',
],
];
}
@@ -0,0 +1,55 @@
<?php
namespace app\mobile\controller;
use app\common\controller\BaseController;
use app\common\model\Corp;
use app\common\model\User;
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');
return $data;
}
}
@@ -0,0 +1,193 @@
<?php
namespace app\mobile\controller;
use app\common\controller\BaseController;
use app\common\model\Corp;
use app\common\model\InvItem;
use app\common\model\InvRequisitionApprove;
use app\common\model\InvRequisitionApproveAttachment;
use app\common\model\InvRequisitionAttachment;
use app\common\model\InvRequisitionItem;
use app\common\service\OssService;
use app\common\traits\crud\CrudControllerTrait;
use app\common\service\InvRequisitionService;
use support\Request;
class InvRequisitionController extends BaseController
{
use CrudControllerTrait;
protected $service;
public function __construct()
{
$this->service = new InvRequisitionService();
}
/**
* 提交物品申请
*
* @param Request $request
* @return mixed
*/
public function apply(Request $request)
{
try {
// 验证申请数据
$data = $this->validate($this->service->getValidator(), $request->post(), 'apply');
// 处理申请逻辑
$result = $this->service->serviceStore($request, $data, 'apply');
return $this->success($result, '申请提交成功');
} catch (\Throwable $e) {
return $this->error($e->getMessage());
}
}
/**
* 获取列表
*
* @param Request $request 请求对象
* @return mixed 返回分页数据及总数
*/
public function index(Request $request)
{
try {
$param = $request->all();
$applicant_id = $request->get('applicant_id');
$raw = $this->service->serviceList($request);
$uid = $request->user->uid;
$data = $this->formatindex($raw,$applicant_id,$uid,$param);
// $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,$param){
$records = $p->getCollection()
->map(fn($item) => $this->indexFormat($item, $applicant_id,$uid))
->toArray();
if(isset($param['phase']) && $param['phase'] == 'pending'){
foreach ($records as $key => $record) {
if($record['action']==1 || $record['action']==2)
unset($records[$key]);
}
$records = array_values($records);
}
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']) && is_object($data)){
$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)
{
$oss = new OssService();
if (isset($data['id'])){
$data['approve'] = InvRequisitionApprove::where('requisition_id', $data['id'])->get()->toArray();
foreach ($data['approve'] as $key => $item){
$attachment = InvRequisitionApproveAttachment::where('approve_id', $item['id'])->get();
foreach ($attachment as $k => $v){
$attachment[$k] = $v->toArray();
$attachment[$k]['file_path_pull'] = $oss->generatePrivateUrl($v['file_path']);
}
$data['approve'][$key]['attachment'] = $attachment;
}
$approve_id = !empty($data['approve'])?array_pop($data['approve'])['id']:null;
$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();
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 qrClaim(Request $request)
{
try {
$data = $this->service->qrClaim($request, $request->post());
return $this->success($data, '领取成功');
} catch (\Throwable $e) {
return $this->error($e->getMessage());
}
}
/**
* 我的扫码/代领记录
*
* @param Request $request
* @return mixed
*/
public function myQrClaims(Request $request)
{
try {
// 只查询当前用户的扫码记录,通过设置 applicant_id=self 实现
$request->setGet('applicant_id','self');
$data = $this->service->qrClaimsList($request);
return $this->success($data);
} catch (\Throwable $e) {
return $this->error($e->getMessage());
}
}
}
@@ -0,0 +1,180 @@
<?php
namespace app\mobile\controller;
use app\common\controller\BaseController;
use app\common\model\Corp;
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,37 @@
<?php
namespace app\mobile\controller;
use app\common\controller\BaseController;
use app\common\model\InvRequisitionItem;
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['id'])) {
$data['item'] = InvRequisitionItem::where('requisition_id', $data['id'])->get();
}
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;
}
}
-10
View File
@@ -12,16 +12,6 @@ class CheckWeb
public function onWorkerStart() public function onWorkerStart()
{ {
// if (getenv('ENVIRONMENT') === 'master') {
// new Crontab('0 */10 * * * *', function(){
// $result = $this->tocurl('https://magicbridgeauto.com');
// if (!preg_match('/magicbridgeauto/', $result['body'])) {
// flg("【magicbridgeauto】疑似被篡改");
// }
// return true;
// });
// }
// 每秒执行:队列二手车数据处理 // 每秒执行:队列二手车数据处理
new Crontab('* * * * * *', function(){ new Crontab('* * * * * *', function(){
$service = new \app\common\utils\crawl\service\UsedCarCrawl(); $service = new \app\common\utils\crawl\service\UsedCarCrawl();
+2 -1
View File
@@ -49,7 +49,8 @@
"mpdf/mpdf": "^8.2", "mpdf/mpdf": "^8.2",
"jaeger/querylist": "^4.4", "jaeger/querylist": "^4.4",
"zoujingli/ip2region": "^3.0", "zoujingli/ip2region": "^3.0",
"webman/log": "^2.1" "webman/log": "^2.1",
"endroid/qr-code": "^6.0"
}, },
"suggest": { "suggest": {
"ext-event": "For better performance. " "ext-event": "For better performance. "
Generated
+154 -1
View File
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "bf987c3cd5510fc0637d6b64d460dde1", "content-hash": "28a80d57e5dbc1da167b66780b6dfd00",
"packages": [ "packages": [
{ {
"name": "aliyuncs/oss-sdk-php", "name": "aliyuncs/oss-sdk-php",
@@ -51,6 +51,55 @@
}, },
"time": "2024-10-28T10:41:12+00:00" "time": "2024-10-28T10:41:12+00:00"
}, },
{
"name": "bacon/bacon-qr-code",
"version": "v3.0.3",
"dist": {
"type": "zip",
"url": "https://mirrors.cloud.tencent.com/repository/composer/bacon/bacon-qr-code/v3.0.3/bacon-bacon-qr-code-v3.0.3.zip",
"reference": "36a1cb2b81493fa5b82e50bf8068bf84d1542563",
"shasum": ""
},
"require": {
"dasprid/enum": "^1.0.3",
"ext-iconv": "*",
"php": "^8.1"
},
"require-dev": {
"phly/keep-a-changelog": "^2.12",
"phpunit/phpunit": "^10.5.11 || ^11.0.4",
"spatie/phpunit-snapshot-assertions": "^5.1.5",
"spatie/pixelmatch-php": "^1.2.0",
"squizlabs/php_codesniffer": "^3.9"
},
"suggest": {
"ext-imagick": "to generate QR code images"
},
"type": "library",
"autoload": {
"psr-4": {
"BaconQrCode\\": "src/"
}
},
"license": [
"BSD-2-Clause"
],
"authors": [
{
"name": "Ben Scholzen 'DASPRiD'",
"email": "mail@dasprids.de",
"homepage": "https://dasprids.de/",
"role": "Developer"
}
],
"description": "BaconQrCode is a QR code generator for PHP.",
"homepage": "https://github.com/Bacon/BaconQrCode",
"support": {
"issues": "https://github.com/Bacon/BaconQrCode/issues",
"source": "https://github.com/Bacon/BaconQrCode/tree/v3.0.3"
},
"time": "2025-11-19T17:15:36+00:00"
},
{ {
"name": "brick/math", "name": "brick/math",
"version": "0.14.1", "version": "0.14.1",
@@ -259,6 +308,50 @@
], ],
"time": "2024-11-12T16:29:46+00:00" "time": "2024-11-12T16:29:46+00:00"
}, },
{
"name": "dasprid/enum",
"version": "1.0.7",
"dist": {
"type": "zip",
"url": "https://mirrors.cloud.tencent.com/repository/composer/dasprid/enum/1.0.7/dasprid-enum-1.0.7.zip",
"reference": "b5874fa9ed0043116c72162ec7f4fb50e02e7cce",
"shasum": ""
},
"require": {
"php": ">=7.1 <9.0"
},
"require-dev": {
"phpunit/phpunit": "^7 || ^8 || ^9 || ^10 || ^11",
"squizlabs/php_codesniffer": "*"
},
"type": "library",
"autoload": {
"psr-4": {
"DASPRiD\\Enum\\": "src/"
}
},
"license": [
"BSD-2-Clause"
],
"authors": [
{
"name": "Ben Scholzen 'DASPRiD'",
"email": "mail@dasprids.de",
"homepage": "https://dasprids.de/",
"role": "Developer"
}
],
"description": "PHP 7.1 enum implementation",
"keywords": [
"enum",
"map"
],
"support": {
"issues": "https://github.com/DASPRiD/Enum/issues",
"source": "https://github.com/DASPRiD/Enum/tree/1.0.7"
},
"time": "2025-09-16T12:23:56+00:00"
},
{ {
"name": "doctrine/inflector", "name": "doctrine/inflector",
"version": "2.1.0", "version": "2.1.0",
@@ -349,6 +442,66 @@
], ],
"time": "2025-08-10T19:31:58+00:00" "time": "2025-08-10T19:31:58+00:00"
}, },
{
"name": "endroid/qr-code",
"version": "6.0.9",
"dist": {
"type": "zip",
"url": "https://mirrors.cloud.tencent.com/repository/composer/endroid/qr-code/6.0.9/endroid-qr-code-6.0.9.zip",
"reference": "21e888e8597440b2205e2e5c484b6c8e556bcd1a",
"shasum": ""
},
"require": {
"bacon/bacon-qr-code": "^3.0",
"php": "^8.2"
},
"require-dev": {
"endroid/quality": "dev-main",
"ext-gd": "*",
"khanamiryan/qrcode-detector-decoder": "^2.0.2",
"setasign/fpdf": "^1.8.2"
},
"suggest": {
"ext-gd": "Enables you to write PNG images",
"khanamiryan/qrcode-detector-decoder": "Enables you to use the image validator",
"roave/security-advisories": "Makes sure package versions with known security issues are not installed",
"setasign/fpdf": "Enables you to use the PDF writer"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "6.x-dev"
}
},
"autoload": {
"psr-4": {
"Endroid\\QrCode\\": "src/"
}
},
"license": [
"MIT"
],
"authors": [
{
"name": "Jeroen van den Enden",
"email": "info@endroid.nl"
}
],
"description": "Endroid QR Code",
"homepage": "https://github.com/endroid/qr-code",
"keywords": [
"code",
"endroid",
"php",
"qr",
"qrcode"
],
"support": {
"issues": "https://github.com/endroid/qr-code/issues",
"source": "https://github.com/endroid/qr-code/tree/6.0.9"
},
"time": "2025-07-13T19:59:45+00:00"
},
{ {
"name": "firebase/php-jwt", "name": "firebase/php-jwt",
"version": "v6.11.1", "version": "v6.11.1",
+82 -1
View File
@@ -29,6 +29,11 @@ use Webman\Route;
Route::any('/c', [app\controller\IndexController::class, 'createResource']); Route::any('/c', [app\controller\IndexController::class, 'createResource']);
Route::any('/perm', [app\controller\IndexController::class, 'perm']); Route::any('/perm', [app\controller\IndexController::class, 'perm']);
// 系统间认证接口
Route::group('/api/system', function () {
Route::any('/auth', [app\common\controller\SystemController::class, 'auth']);
});
// 飞书 // 飞书
Route::any('/feishu/event', [app\common\controller\NotifyController::class, 'feishuEvent']); Route::any('/feishu/event', [app\common\controller\NotifyController::class, 'feishuEvent']);
// 飞书回调 // 飞书回调
@@ -51,7 +56,7 @@ Route::group('/common', function () {
Route::any('/register', [app\common\controller\AuthController::class, 'register']); Route::any('/register', [app\common\controller\AuthController::class, 'register']);
Route::any('/feishu/scope', [app\common\controller\AuthController::class, 'feishuScope']); Route::any('/feishu/scope', [app\common\controller\AuthController::class, 'feishuScope']);
Route::any('/refresh/token', [app\common\controller\AuthController::class, 'refresh']); Route::any('/refresh/token', [app\common\controller\AuthController::class, 'refresh']);
Route::any('/feishu/jsapi', [app\common\controller\AuthController::class, 'getJsApi']);
// // 调试 // // 调试
Route::any('/feishu/auth', [app\common\controller\AuthController::class, 'feishuAuth']); Route::any('/feishu/auth', [app\common\controller\AuthController::class, 'feishuAuth']);
@@ -518,6 +523,46 @@ Route::group('/admin', function () {
Route::any('/destroy', [app\admin\controller\UserDashboardRuleController::class, 'destroy']); Route::any('/destroy', [app\admin\controller\UserDashboardRuleController::class, 'destroy']);
}); });
}); });
// 物品领用
Route::group('/inv', function () {
Route::any('/item/list', [app\admin\controller\InvItemController::class, 'index']);
Route::any('/item/store', [app\admin\controller\InvItemController::class, 'store']);
Route::any('/item/update', [app\admin\controller\InvItemController::class, 'update']);
Route::any('/item/show', [app\admin\controller\InvItemController::class, 'show']);
Route::any('/item/destroy', [app\admin\controller\InvItemController::class, 'destroy']);
Route::any('/item/qr-code', [app\admin\controller\InvItemController::class, 'generateQrCode']);
Route::any('/item/qr-code/delete', [app\admin\controller\InvItemController::class, 'deleteQrCode']);
Route::group('/requisition', function () {
Route::any('/list', [app\admin\controller\InvRequisitionController::class, 'index']);
Route::any('/show', [app\admin\controller\InvRequisitionController::class, 'show']);
Route::any('/approve', [app\admin\controller\InvRequisitionController::class, 'approve']);
});
// 物品归还
Route::group('/requisition/return', function () {
Route::any('/store', [app\common\controller\InvRequisitionItemReturnController::class, 'store']);
Route::any('/update', [app\common\controller\InvRequisitionItemReturnController::class, 'update']);
Route::any('/list', [app\common\controller\InvRequisitionItemReturnController::class, 'index']);
Route::any('/show', [app\common\controller\InvRequisitionItemReturnController::class, 'show']);
Route::any('/confirm', [app\common\controller\InvRequisitionItemReturnController::class, 'returnConfirm']);
// Route::any('/confirmItem', [app\common\controller\InvRequisitionItemReturnController::class, 'returnItemConfirm']);
});
});
// 库存变化记录
Route::group('/stock-log', function () {
Route::any('/list', [app\admin\controller\InvStockLogController::class, 'list']);
});
Route::group('/requisition', function () {
// Route::any('/list', [app\admin\controller\InvRequisitionController::class, 'index']);
// Route::any('/show', [app\admin\controller\InvRequisitionController::class, 'show']);
// Route::any('/approve', [app\admin\controller\InvRequisitionController::class, 'approve']);
Route::any('/proxy-claim', [app\admin\controller\InvRequisitionController::class, 'proxyClaim']);
Route::any('/qr-claims', [app\admin\controller\InvRequisitionController::class, 'qrClaims']);
});
}); });
@@ -637,4 +682,40 @@ Route::group('/crm', function () {
Route::any('/update', [app\common\controller\VehModificationController::class, 'update']); Route::any('/update', [app\common\controller\VehModificationController::class, 'update']);
Route::any('/destroy', [app\common\controller\VehModificationController::class, 'destroy']); Route::any('/destroy', [app\common\controller\VehModificationController::class, 'destroy']);
}); });
});
// 移动端
Route::group('/mobile', function () {
// 物品领用
Route::group('/inv', function () {
//物品下拉
Route::any('/item/preset', [app\mobile\controller\InvItemController::class, 'preset']);
// 物品领用申请
// Route::any('/requisition/list', [app\mobile\controller\InvRequisitionController::class, 'index']);
// Route::any('/requisition/show', [app\mobile\controller\InvRequisitionController::class, 'show']);
Route::any('/requisition/apply', [app\mobile\controller\InvRequisitionController::class, 'apply']);
Route::any('/requisition/update', [app\mobile\controller\InvRequisitionController::class, 'update']);
Route::group('/requisition', function () {
Route::any('/list', [app\mobile\controller\InvRequisitionController::class, 'index']);
Route::any('/show', [app\mobile\controller\InvRequisitionController::class, 'show']);
// Route::any('/approve', [app\mobile\controller\InvRequisitionController::class, 'approve']);
Route::any('/qr-claim', [app\mobile\controller\InvRequisitionController::class, 'qrClaim']);
Route::any('/my-qr-claims', [app\mobile\controller\InvRequisitionController::class, 'myQrClaims']);
});
// 物品归还
Route::group('/requisition/return', function () {
Route::any('/store', [app\mobile\controller\InvRequisitionItemReturnController::class, 'store']);
Route::any('/update', [app\mobile\controller\InvRequisitionItemReturnController::class, 'update']);
Route::any('/list', [app\mobile\controller\InvRequisitionItemReturnController::class, 'index']);
Route::any('/show', [app\mobile\controller\InvRequisitionItemReturnController::class, 'show']);
Route::any('/confirm', [app\mobile\controller\InvRequisitionItemReturnController::class, 'returnConfirm']);
// Route::any('/confirmItem', [app\mobile\controller\InvRequisitionItemReturnController::class, 'returnItemConfirm']);
});
});
}); });
+12
View File
@@ -0,0 +1,12 @@
<?php
return [
// 共享密钥(双方约定同一个字符串)
'shared_secret' => env('SYSTEM_AUTH_SHARED_SECRET', 'your-shared-secret-string'),
// 签名有效期(秒),默认10分钟
'signature_expire' => 600,
// Token 有效期(秒)
'token_expire' => 86400,
];
+94
View File
@@ -0,0 +1,94 @@
CREATE TABLE `approval_flows` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(255) NOT NULL COMMENT '流程名称',
`type` varchar(100) NOT NULL COMMENT '业务类型标识,如 leave, reimbursement',
`steps` json NOT NULL COMMENT '审批步骤配置',
`is_active` tinyint(1) NOT NULL DEFAULT '1' COMMENT '是否启用',
`timeout_hours` int(11) NOT NULL DEFAULT '0' COMMENT '实例级超时时间(小时),0为不超时',
`timeout_action` varchar(20) NOT NULL DEFAULT 'auto_reject' COMMENT 'auto_approve | auto_reject',
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_flow_type` (`type`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb4 COMMENT='审批流程定义';
CREATE TABLE `approval_instances` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`flow_type` varchar(100) NOT NULL COMMENT '关联 approval_flows.type',
`biz_id` varchar(255) NOT NULL COMMENT '业务ID',
`biz_type` varchar(100) NOT NULL COMMENT '业务标识/表名',
`form_data` json NOT NULL COMMENT '提交表单快照',
`initiator_id` bigint(20) unsigned NOT NULL COMMENT '发起人',
`status` varchar(20) NOT NULL DEFAULT 'processing' COMMENT 'processing|approved|rejected|revoked|timed_out',
`current_step` int(11) NOT NULL DEFAULT '0' COMMENT '当前步骤索引',
`round` int(11) NOT NULL DEFAULT '1' COMMENT '当前轮次:1=首次,重提+1',
`finished_at` timestamp NULL DEFAULT NULL,
`deadline_at` timestamp NULL DEFAULT NULL,
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `idx_flow_type` (`flow_type`),
KEY `idx_biz` (`biz_type`,`biz_id`),
KEY `idx_initiator` (`initiator_id`),
KEY `idx_status_updated` (`status`,`updated_at`)
) ENGINE=InnoDB AUTO_INCREMENT=68 DEFAULT CHARSET=utf8mb4 COMMENT='审批实例';
CREATE TABLE `approval_records` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`instance_id` bigint(20) unsigned NOT NULL COMMENT 'approval_instances.id',
`approver_id` bigint(20) unsigned NOT NULL COMMENT '审批人',
`step_index` int(11) NOT NULL COMMENT '步骤索引',
`round` int(11) NOT NULL DEFAULT '1' COMMENT '所属轮次',
`action` varchar(20) DEFAULT NULL COMMENT 'approve|reject,待办时为NULL',
`status` varchar(20) NOT NULL DEFAULT 'pending' COMMENT 'pending|approved|rejected|skipped|timeout',
`comment` text COMMENT '审批意见',
`approved_at` timestamp NULL DEFAULT NULL COMMENT '审批时间/关闭时间',
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_instance_step_approver` (`instance_id`,`step_index`,`approver_id`),
KEY `idx_instance_step_status` (`instance_id`,`step_index`,`status`),
KEY `idx_approver` (`approver_id`),
KEY `idx_instance_step_round` (`instance_id`,`step_index`,`round`)
) ENGINE=InnoDB AUTO_INCREMENT=105 DEFAULT CHARSET=utf8mb4 COMMENT='审批记录';
CREATE TABLE `approval_transfers` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`instance_id` bigint(20) unsigned NOT NULL,
`step_index` int(11) NOT NULL,
`record_id` bigint(20) unsigned NOT NULL COMMENT '转交后仍然有效的记录ID(若合并则为目标记录ID)',
`from_user_id` bigint(20) unsigned NOT NULL,
`to_user_id` bigint(20) unsigned NOT NULL,
`reason` text,
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `idx_record` (`record_id`),
KEY `idx_instance_step` (`instance_id`,`step_index`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='审批转交日志';
CREATE TABLE `approval_ccs` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`instance_id` bigint(20) unsigned NOT NULL,
`user_id` bigint(20) unsigned NOT NULL,
`notified_at` timestamp NULL DEFAULT NULL,
`read_at` timestamp NULL DEFAULT NULL,
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_instance_user` (`instance_id`,`user_id`),
KEY `idx_user` (`user_id`),
KEY `idx_instance` (`instance_id`)
) ENGINE=InnoDB AUTO_INCREMENT=9 DEFAULT CHARSET=utf8mb4 COMMENT='审批抄送';
CREATE TABLE `approval_discussions` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`instance_id` bigint(20) unsigned NOT NULL,
`user_id` bigint(20) unsigned NOT NULL,
`is_internal` tinyint(1) NOT NULL DEFAULT '0' COMMENT '0=公开,1=仅审批人可见',
`content` text NOT NULL,
`attachments` json DEFAULT NULL,
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `idx_instance_id` (`instance_id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COMMENT='审批讨论';
+174
View File
@@ -0,0 +1,174 @@
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- 1. 物品主数据表:增加 company_id
DROP TABLE IF EXISTS inv_item;
CREATE TABLE inv_item (
id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID',
company_id BIGINT(20) UNSIGNED DEFAULT NULL COMMENT '物品所属公司ID',
item_code VARCHAR(64) NOT NULL COMMENT '物品编号(公司内部编码)',
item_name VARCHAR(128) NOT NULL COMMENT '物品名称',
category_id BIGINT(20) UNSIGNED DEFAULT NULL COMMENT '物品分类ID(可选,指向分类表)',
spec_model VARCHAR(128) DEFAULT NULL COMMENT '规格型号',
unit VARCHAR(32) DEFAULT '件' COMMENT '计量单位,如 件/盒/包',
is_consumable TINYINT(1) DEFAULT 1 COMMENT '是否耗材:1耗材(不需归还),0非耗材(需归还)',
stock_qty INT(11) DEFAULT 0 COMMENT '当前库存数量',
manage_user_id BIGINT(20) UNSIGNED DEFAULT NULL COMMENT '物品管理员用户ID',
location VARCHAR(255) DEFAULT NULL COMMENT '存放位置说明',
remark VARCHAR(255) DEFAULT NULL COMMENT '备注',
created_at INT(11) NOT NULL DEFAULT 0 COMMENT '创建时间时间戳',
updated_at INT(11) NOT NULL DEFAULT 0 COMMENT '更新时间时间戳',
PRIMARY KEY (id),
UNIQUE KEY uk_item_code (item_code),
KEY idx_company (company_id),
KEY idx_category (category_id),
KEY idx_manage_user (manage_user_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='物品主数据表';
-- 2. 物品领用/借用申请单(主表)
DROP TABLE IF EXISTS inv_requisition;
CREATE TABLE inv_requisition (
id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID',
order_no VARCHAR(64) NOT NULL COMMENT '申请单号',
applicant_company_id BIGINT(20) UNSIGNED DEFAULT NULL COMMENT '申请人所在公司ID(可选)',
dept_id BIGINT(20) UNSIGNED DEFAULT NULL COMMENT '申请人部门ID',
applicant_id BIGINT(20) UNSIGNED NOT NULL COMMENT '申请人用户ID',
applicant_name VARCHAR(64) DEFAULT NULL COMMENT '申请人姓名快照',
leader_id BIGINT(20) UNSIGNED DEFAULT NULL COMMENT '直属上级用户ID',
manager_id BIGINT(20) UNSIGNED DEFAULT NULL COMMENT '物品管理员用户ID(主负责人)',
borrow_reason VARCHAR(255) NOT NULL COMMENT '领用/借用事由',
use_start_date DATE NOT NULL COMMENT '借用/预计使用开始日期',
use_end_date DATE DEFAULT NULL COMMENT '预计归还日期(耗材可为空)',
-- 0草稿 10待直属上级审批 20直属上级通过待物品管理员审批
-- 30全部通过 40已驳回 50已取消
status TINYINT(3) NOT NULL DEFAULT 10 COMMENT '审批状态',
is_returned TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否已全部归还(针对需要归还的物品):0否 1是',
remark VARCHAR(255) DEFAULT NULL COMMENT '备注',
created_at INT(11) NOT NULL DEFAULT 0 COMMENT '创建时间时间戳',
updated_at INT(11) NOT NULL DEFAULT 0 COMMENT '更新时间时间戳',
PRIMARY KEY (id),
UNIQUE KEY uk_order_no (order_no),
KEY idx_applicant (applicant_id),
KEY idx_status (status)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='物品领用/借用申请单';
-- 3. 申请单明细表:支持部分、多次归还
DROP TABLE IF EXISTS inv_requisition_item;
CREATE TABLE inv_requisition_item (
id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID',
requisition_id BIGINT(20) UNSIGNED NOT NULL COMMENT '申请单ID',
item_id BIGINT(20) UNSIGNED NOT NULL COMMENT '物品ID',
item_name_snap VARCHAR(128) DEFAULT NULL COMMENT '物品名称快照',
spec_model_snap VARCHAR(128) DEFAULT NULL COMMENT '规格型号快照',
unit_snap VARCHAR(32) DEFAULT NULL COMMENT '单位快照',
is_consumable_snap TINYINT(1) DEFAULT NULL COMMENT '是否耗材快照:1耗材 0非耗材',
request_qty INT(11) NOT NULL DEFAULT 1 COMMENT '申请数量',
approved_qty INT(11) DEFAULT NULL COMMENT '实批数量(可与申请数量不同)',
returned_qty INT(11) NOT NULL DEFAULT 0 COMMENT '已归还数量(累加)',
-- 对于非耗材:returned_qty >= approved_qty 时置为1
is_finished TINYINT(1) NOT NULL DEFAULT 0 COMMENT '该物品是否已完成归还:0否 1是',
remark VARCHAR(255) DEFAULT NULL COMMENT '行备注',
created_at INT(11) NOT NULL DEFAULT 0 COMMENT '创建时间时间戳',
updated_at INT(11) NOT NULL DEFAULT 0 COMMENT '更新时间时间戳',
PRIMARY KEY (id),
KEY idx_req (requisition_id),
KEY idx_item (item_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='物品领用申请明细表';
-- 4. 审批记录表(同上一版,略微无改动)
DROP TABLE IF EXISTS inv_requisition_approve;
CREATE TABLE inv_requisition_approve (
id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID',
requisition_id BIGINT(20) UNSIGNED NOT NULL COMMENT '申请单ID',
node_type VARCHAR(32) NOT NULL COMMENT '审批节点类型:leader=直属上级,manager=物品管理员,other=其他',
approver_id BIGINT(20) UNSIGNED NOT NULL COMMENT '审批人用户ID',
approver_name VARCHAR(64) DEFAULT NULL COMMENT '审批人姓名快照',
action TINYINT(3) NOT NULL COMMENT '审批动作:1通过 2驳回',
comment VARCHAR(255) DEFAULT NULL COMMENT '审批意见',
action_at INT(11) NOT NULL DEFAULT 0 COMMENT '审批时间时间戳',
created_at INT(11) NOT NULL DEFAULT 0 COMMENT '创建时间时间戳',
updated_at INT(11) NOT NULL DEFAULT 0 COMMENT '更新时间时间戳',
PRIMARY KEY (id),
KEY idx_req (requisition_id),
KEY idx_node (node_type),
KEY idx_approver (approver_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='物品领用申请审批记录表';
-- 5. 申请单附件表(申请时上传)
DROP TABLE IF EXISTS inv_requisition_attachment;
CREATE TABLE inv_requisition_attachment (
id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID',
requisition_id BIGINT(20) UNSIGNED NOT NULL COMMENT '申请单ID',
file_name VARCHAR(255) NOT NULL COMMENT '文件原始名称',
file_path VARCHAR(255) NOT NULL COMMENT '文件存储路径/URL',
file_size INT(11) DEFAULT NULL COMMENT '文件大小(字节)',
created_at INT(11) NOT NULL DEFAULT 0 COMMENT '创建时间时间戳',
updated_at INT(11) NOT NULL DEFAULT 0 COMMENT '更新时间时间戳',
PRIMARY KEY (id),
KEY idx_req (requisition_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='物品领用申请附件表';
/* ================= 归 还 相 关 表 ================= */
-- 6. 归还记录表:一次归还动作(可归还一个明细的一部分)
DROP TABLE IF EXISTS inv_requisition_item_return;
CREATE TABLE inv_requisition_item_return (
id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID',
requisition_id BIGINT(20) UNSIGNED NOT NULL COMMENT '申请单ID',
requisition_item_id BIGINT(20) UNSIGNED NOT NULL COMMENT '申请单明细ID',
item_id BIGINT(20) UNSIGNED NOT NULL COMMENT '物品ID',
return_qty INT(11) NOT NULL DEFAULT 0 COMMENT '本次归还数量',
handler_id BIGINT(20) UNSIGNED NOT NULL COMMENT '办理归还的人(通常是物品管理员)',
handler_name VARCHAR(64) DEFAULT NULL COMMENT '办理人姓名快照',
return_status TINYINT(3) NOT NULL DEFAULT 1 COMMENT '归还状态:1正常归还 2损坏 3其他',
remark VARCHAR(255) DEFAULT NULL COMMENT '归还备注',
return_at INT(11) NOT NULL DEFAULT 0 COMMENT '归还时间时间戳',
created_at INT(11) NOT NULL DEFAULT 0 COMMENT '创建时间时间戳',
updated_at INT(11) NOT NULL DEFAULT 0 COMMENT '更新时间时间戳',
PRIMARY KEY (id),
KEY idx_req (requisition_id),
KEY idx_req_item (requisition_item_id),
KEY idx_item (item_id),
KEY idx_handler (handler_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='物品归还记录表(支持多次、部分归还)';
-- 7. 归还附件表:归还时拍照留底
DROP TABLE IF EXISTS inv_requisition_item_return_attachment;
CREATE TABLE inv_requisition_item_return_attachment (
id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID',
return_id BIGINT(20) UNSIGNED NOT NULL COMMENT '归还记录ID',
file_name VARCHAR(255) NOT NULL COMMENT '文件原始名称',
file_path VARCHAR(255) NOT NULL COMMENT '文件存储路径/URL(通常为照片)',
file_size INT(11) DEFAULT NULL COMMENT '文件大小(字节)',
created_at INT(11) NOT NULL DEFAULT 0 COMMENT '创建时间时间戳',
updated_at INT(11) NOT NULL DEFAULT 0 COMMENT '更新时间时间戳',
PRIMARY KEY (id),
KEY idx_return (return_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='物品归还记录附件表(归还拍照留底)';
SET FOREIGN_KEY_CHECKS = 1;