445 Commits
Author SHA1 Message Date
c-chengc1 fdefa440bb feat:h5首页logo调整为南京审计大学logo 2026-11-06 09:16:33 +08:00
c-chengc1 4b364f593a feat:企业微信对接 2026-11-06 08:49:17 +08:00
c-chengc1 3c6ef5b25a Merge remote-tracking branch 'origin/master' 2026-10-27 12:00:17 +08:00
c-chengc1 0d755b8888 fix:移除无效代码 2026-10-27 12:00:05 +08:00
c-chengc1 ee218c9632 更新 README.md 2026-10-27 11:57:06 +08:00
c-chengc1 07e42bc98c Merge remote master into local project 2026-10-27 11:45:15 +08:00
c-zhouhf1 55163b9fec 体育活动成绩整改 2026-10-26 15:55:22 +08:00
程诚 75045b3544 fix:工会管理表单搜索人员缓存问题修复;会员变更查询数据序号异常问题修复 2026-10-26 07:49:31 +08:00
c-zhouhf1 398dddc370 体育活动成绩统计,协会经费 2026-10-23 16:13:35 +08:00
c-zhouhf1 2ac7b0838a 不提交yaml 2026-10-23 09:32:57 +08:00
c-zhangr1 1b42561341 Merge remote-tracking branch 'origin/master' 2026-10-22 14:16:20 +08:00
c-zhangr1 cb1687a5fc 人员数据治理调整 2026-10-22 14:16:01 +08:00
c-chengc1 5671737c9e feat:web首页优化,用户关联角色过多会撑高上方信息区域 2026-10-21 16:49:02 +08:00
c-chengc1 47e7a71310 fix:1、我的学习增加返回按钮 2026-10-20 16:25:44 +08:00
c-chengc1 ef9a6eddd5 Merge remote-tracking branch 'origin/master' 2026-10-20 14:02:59 +08:00
c-chengc1 f09a2faf1f fix:1、登录密码校验注释;2、工会报销-我的报销查询数据范围修复;3、工会报销数据接口权限范围和菜单权限范围保持一致;4、工会管理表格高度固定内部滚动 2026-10-20 14:02:47 +08:00
c-zhangr1 23fc1433e1 Merge remote-tracking branch 'origin/master' 2026-10-20 11:57:08 +08:00
c-zhangr1 5b4fbe8495 拉取人员接口:给人员类型为“教职工”的人员,人员属性赋值为“新增'教职工'”,人员类型为“劳务派遣人员”的人员,人员属性赋值为“劳务派遣人员” 2026-10-20 11:56:51 +08:00
c-chengc1 e0e7dcfa05 feat:体检模块图片圆角样式 2026-10-17 17:06:50 +08:00
c-chengc1 ed2919f4e5 feat:会员入会-增加已有申请进行中判断逻辑 2026-10-17 16:13:21 +08:00
c-chengc1 010d56e348 feat:工会报销-社团活动附件说明不再复用文体活动的附件说明 2026-10-17 15:33:18 +08:00
c-chengc1 15c39aad69 Merge remote-tracking branch 'origin/master' into feature_经费管理整改 2026-10-17 14:18:42 +08:00
c-chengc1 3ffa1454b1 feat:优化社团入会 2026-10-17 14:17:41 +08:00
c-chengc1 453a8d24f1 feat:h5会员入会是否已是会员校验、操作栏按钮取消隐藏;h5体检模块:体检项目、我的体检界面卡片样式优化; 2026-10-16 17:22:03 +08:00
c-zhouhf1 76a6e298a7 增加验证码登录,验证码使用的是腾讯云的,验证码登录入口可以在系统配置里关闭
修改登录时使用算术改为滑块
2026-10-16 17:10:50 +08:00
c-chengc1 143d9eb0ce feat:h5首页工会动态模块恢复左右滑动样式 2026-10-16 15:21:07 +08:00
c-chengc1 c1dfe9b7a1 feat:工会报销:1、报销申请增加协会报销,增加社团活动;2、协会报销关联经费管理-社团预算;3、文体活动、日常活动报销,去除附件必填校验;4、活动增加报销金额,报销金额小于发票金额;5、报销审核、汇总,表格中增加流水号字段,所属单位隐藏 2026-10-16 14:30:51 +08:00
c-chengc1 4ea3ff8ac4 feat:慰问打印去掉二维码,加上流水号(按年月日编号自动编号,例如20261009001,后三位“001”按申请时间自动递增) 2026-10-16 09:00:51 +08:00
c-zhangr1 38d60b17cf Merge branch 'feature_会员管理整改' 2026-10-15 09:28:00 +08:00
c-zhangr1 b3c07b8bb5 待确认人员记录:导入变更调整 2026-10-15 09:24:52 +08:00
c-zhouhf1 c19ee12f4c 提案问题整改 2026-10-14 17:12:55 +08:00
c-zhouhf1 3e7fc55a40 幸福工惠整改 2026-10-14 15:23:42 +08:00
c-zhouhf1 1be063f818 品牌活动整改
幸福工惠移植
2026-10-14 14:33:11 +08:00
c-zhangr1 4c05dc8311 体检统计报错 2026-10-14 09:26:37 +08:00
c-zhangr1 79ce184be3 会员管理整改 2026-10-14 09:16:43 +08:00
c-zhouhf1 122f8bebb2 工会导出整改 2026-10-13 11:12:45 +08:00
c-zhouhf1 ca5776b41f Merge branch 'master' of http://gitea.dd3skj.com/c-zhouhf1/zhgh_cug 2026-10-12 18:31:38 +08:00
c-zhouhf1 4440420d90 医疗补助算法整改 2026-10-12 18:31:14 +08:00
c-zhangr1 8a581cddbc 1 2026-10-12 18:12:33 +08:00
c-zhangr1 93bb026555 Merge branch 'feature_人员数据治理迁移' 2026-10-12 18:11:17 +08:00
c-zhangr1 e539f6af36 人员数据治理模块迁移1.0 2026-10-12 15:31:53 +08:00
c-zhangr1 9ffdbd21ad 荣誉模块数据看板查询报错 2026-10-10 11:03:55 +08:00
c-zhangr1 8fd7eeac11 人员数据治理模块迁移(暂提交) 2026-10-10 10:13:28 +08:00
zhouhefeng c829cbf725 王总电脑修改代码 2026-10-08 09:43:38 +08:00
c-zhouhf1 ce75b40656 医疗补助算法整改 2026-09-30 10:17:32 +08:00
c-zhouhf1 043251a55d 工会报销导出修改 2026-09-29 17:41:55 +08:00
c-zhangr1 387e223e3e 人员更新字段调整 2026-09-29 17:07:48 +08:00
c-zhangr1 1475b3bef7 Merge remote-tracking branch 'origin/master' 2026-09-29 15:37:47 +08:00
c-zhangr1 52bfd3578a 待确认人员功能整改 2026-09-29 15:35:03 +08:00
c-zhouhf1 81e0dc770f 工会报销导出添加二维码 2026-09-29 15:10:12 +08:00
c-zhouhf1 5df7e26908 会员角色没有,以及基金会员申请看不到管理办法 2026-09-29 14:53:29 +08:00
c-zhouhf1 d94a6ed014 工会报销导出以及发票填报修改 2026-09-29 14:16:00 +08:00
zhouhefeng 5c0f593c08 Merge remote-tracking branch 'origin/master' 2026-09-29 10:07:19 +08:00
zhouhefeng 8c431ff665 commit 2026-09-29 10:06:50 +08:00
c-chengc1 f70f1b6177 Merge remote-tracking branch 'origin/feature_样式优化' 2026-09-29 10:02:35 +08:00
c-chengc1 7c7920b378 feat:首页样式优化,增加应用-我的收藏 2026-09-29 10:00:40 +08:00
c-zhangr1 97d005a8fd 待确认人员去掉人员类型控制,只需要确认会员数据即可 2026-09-28 20:37:21 +08:00
c-zhangr1 cda39d43c4 待确认人员去掉人员类型控制,只需要确认会员数据即可 2026-09-28 15:32:46 +08:00
c-hongqw1 fec7be2983 Merge remote-tracking branch 'origin/master' 2026-09-28 11:38:16 +08:00
c-hongqw1 c3fd832496 地大bug整改,工会报销:入住医院必填;校工会审核默认通过改成已付款成功;余额字段隐藏,只有系统管理员、校工会管理员可以看的。费用报销:隐藏分工会的经费类型 2026-09-28 11:35:46 +08:00
c-zhouhf1 e9e536ccff 修改三级单位 2026-09-28 10:41:33 +08:00
c-zhouhf1 75740de31e 修改三级单位
修改流程中心屏蔽开启功能
2026-09-28 10:06:42 +08:00
c-zhouhf1 d1f38603b4 会员信息修改 2026-09-24 16:02:34 +08:00
c-zhouhf1 177b614cd0 修改js引用本地路径 2026-09-23 21:17:42 +08:00
c-zhouhf1 76198c4c33 Merge remote-tracking branch 'origin/main' 2026-09-22 17:23:02 +08:00
c-zhangr1 6e44f62ee1 分工会预算统计列表修改 2026-09-22 11:29:38 +08:00
c-zhouhf1 390723639a 基础配置修改 2026-09-18 17:10:31 +08:00
c-zhouhf1 89ed1af284 工会报销支付信息修改 2026-09-18 14:39:24 +08:00
c-zhouhf1 5fa9c4c368 Merge remote-tracking branch 'origin/fix_bug整改' 2026-09-16 13:53:20 +08:00
c-zhouhf1 19d6cebeb0 对接学校OA待办 2026-09-16 13:51:50 +08:00
c-hongqw1 80774fdb0c 地大bug整改,提案数据看板列表增加字段,是否重点提案;会员数据看板增加年同比,修复会员人员占比数字没有显示; 2026-09-15 10:53:01 +08:00
c-zhouhf1 72b2413c00 Merge remote-tracking branch 'origin/fix_漏洞修复' 2026-09-14 19:33:57 +08:00
c-chenxs1 da13b19b57 Merge branch 'feature_积分商城_对账' into release_20260914 2026-09-14 08:45:27 +08:00
c-chengc1 c3ea84bfad fix:20260904代码扫描漏洞问题修复 2026-09-11 16:28:57 +08:00
c-chengc1 61b8819057 fix:20260904代码扫描漏洞问题修复 2026-09-11 14:04:30 +08:00
c-chengc1 01921f7fcf fix:20260904代码扫描漏洞问题修复 2026-09-09 10:55:31 +08:00
c-chengc1 465a3257b5 fix:20260904代码扫描漏洞问题修复 2026-09-09 10:25:53 +08:00
c-chenxs1 e78d75c516 feat: 积分商城-对账-发票-优化4 2026-09-08 20:11:56 +08:00
c-chenxs1 ecb5c8f40a feat: 积分商城-对账-发票-优化3 2026-09-08 15:09:42 +08:00
c-chenxs1 7b877255fb feat: 积分商城-对账-发票-优化2 2026-09-08 13:40:12 +08:00
c-chengc1 45410cb0a3 Merge remote-tracking branch 'origin/feature_工会报销UI优化' into feature_工会报销UI优化 2026-09-04 16:16:09 +08:00
c-hongqw1 6ef45e5403 fix:1、提案查看的流程节点,增加排序
2、场地预约关闭按钮无法关闭的bug修复
2026-09-04 16:15:55 +08:00
c-hongqw1 4235deede7 地大bug整改 2026-09-04 16:07:12 +08:00
c-chenxs1 9159f8912d feat: 积分商城-对账-发票-优化 2026-09-03 18:19:03 +08:00
c-chengc1 43ae24dceb Merge remote-tracking branch 'origin/feature_程诚_0831问题修复' into release_20260831 2026-09-02 16:27:12 +08:00
c-chengc1 cccad1a852 fix:职代会pc端修复投票纪录无法正常显示工会、代表团 2026-09-02 16:26:38 +08:00
c-chengc1 0b3fef4d71 Merge remote-tracking branch 'origin/feature_工会报销UI优化' into release_20260831 2026-09-02 11:43:11 +08:00
c-hongqw1 0b44f36c83 手机端社团、工会报销bug整改 2026-09-02 10:39:56 +08:00
c-chengc1 2de6820c1a Merge remote-tracking branch 'origin/feature_周何峰' into release_20260831 2026-09-02 09:20:52 +08:00
c-zhouhf1 478b70c2d1 整改微信小程序登录
整改测试提案UI
整改测试作品征集UI
2026-09-01 18:00:36 +08:00
c-chenxs1 72dfff50a6 feat: 积分商城-对账优化3 2026-09-01 16:58:29 +08:00
c-chenxs1 43d0d973c9 feat: 积分商城-对账优化2 2026-09-01 16:25:59 +08:00
c-chenxs1 d2cb60f5cf feat: 积分商城-对账优化 2026-09-01 16:07:41 +08:00
c-chengc1 d305317458 Merge branch 'feature_程诚_0831问题修复' into release_20260831 2026-09-01 16:00:47 +08:00
c-chengc1 982065c424 feat:职工福利PC端系统默认选择改为复选框组件;职代会PC端投票统计结果显示不正确问题修复;体育活动h5分工会报名权限提示;品牌活动h5联系人字段超长样式适配修复; 2026-09-01 16:00:11 +08:00
c-chengc1 a92f75730d feat:职工福利ui优化,我的福利支持修改福利 2026-09-01 15:04:19 +08:00
c-chenxs1 c84b387765 Merge branch 'feature_教代会代表证' into release_20260831 2026-09-01 13:58:50 +08:00
c-chenxs1 0db7e59275 feat: 会议选座逻辑优化2 2026-09-01 13:52:34 +08:00
c-chenxs1 258c2c987c feat: 会议选座逻辑优化 2026-09-01 13:50:18 +08:00
c-chengc1 6b0b6bf0ad Merge remote-tracking branch 'origin/feature_会员模块UI优化' into release_20260831 2026-09-01 10:56:05 +08:00
c-zhangr1 458aeeb6c8 fix_会员模块UI测试整改:页面卡片标题栏优化,查看、审核按钮需要从右向左抽屉式打开 2026-09-01 10:40:36 +08:00
c-chengc1 3bfbb52470 Merge branch 'feature_程诚_0831问题修复' into release_20260831 2026-09-01 10:34:54 +08:00
c-chengc1 b46a665322 feat:职工福利-选择列表过滤非套餐数据,返回栏样式统一,图片缺失占位样式 2026-09-01 10:34:13 +08:00
c-chengc1 dc4a733af5 Merge remote-tracking branch 'origin/feature_疗休养模块UI优化' into release_20260831 2026-08-31 18:05:55 +08:00
c-chengc1 e8ce6eef83 Merge remote-tracking branch 'origin/release_20260831' into release_20260831 2026-08-31 18:05:01 +08:00
c-chengc1 8b3bc19757 feat:职代会文件类型筛选去除图片、压缩包;文件预览支持ppt;查看已投票样式优化; 2026-08-31 18:04:14 +08:00
c-chenxs1 5e5ac081d2 Merge remote-tracking branch 'origin/feature_教代会代表证' into release_20260831 2026-08-31 17:51:50 +08:00
c-chenxs1 4c57dda909 feat: 会议选座样式优化-4 2026-08-31 17:51:01 +08:00
c-chengc1 bbf2bb798d feat:新版菜单图标更新脚本 2026-08-31 17:16:33 +08:00
c-chengc1 b3234fa64b feat:职代会h5查询切换届次列表去除过滤开启状态 2026-08-31 17:16:12 +08:00
c-zhangr1 75357eed1f 移动端疗休养模块UI优化 2026-08-31 16:38:27 +08:00
c-chengc1 3f741ebab6 Merge remote-tracking branch 'origin/release_20260831' into release_20260831 2026-08-31 15:30:03 +08:00
c-zhouhf1 8a0714a8e3 Merge branch 'release_20260831' into feature_周何峰 2026-08-31 15:20:19 +08:00
c-zhouhf1 2341b136ca 品牌活动报名统计增加校工会活动管理员看全部活动
不是超级管理员加不了角色
2026-08-31 15:20:00 +08:00
c-zhouhf1 bf40155fb9 品牌活动报名统计增加校工会活动管理员看全部活动
不是超级管理员加不了角色
2026-08-31 15:18:29 +08:00
c-chengc1 120f0bd046 Merge remote-tracking branch 'origin/feature_教代会代表证' into release_20260831 2026-08-31 13:36:12 +08:00
c-chenxs1 f6834bb969 feat: 会议选座样式优化-3 2026-08-31 12:03:53 +08:00
c-chenxs1 a21007bada feat: 会议选座样式优化-2 2026-08-31 11:54:17 +08:00
c-chengc1 5f20d703e4 Merge remote-tracking branch 'origin/feature_提案&困难帮扶&作品征集UI优化' into release_20260831 2026-08-31 11:14:20 +08:00
c-chengc1 f7b41ee709 Merge remote-tracking branch 'origin/feature_工会报销UI优化' into release_20260831 2026-08-31 11:14:13 +08:00
c-chengc1 0d5223e6c7 Merge remote-tracking branch 'origin/feature_教代会代表证' into release_20260831 2026-08-31 09:07:09 +08:00
c-chengc1 f89b0f643d feat:代表证卡套样式优化;消息中心、待办中心样式优化 2026-08-30 17:59:02 +08:00
c-chenxs1 c4713ae117 feat: 会议选座样式优化-图片 2026-08-30 17:53:01 +08:00
c-chenxs1 199a96130c feat: 会议选座样式优化 2026-08-30 17:52:43 +08:00
c-hongqw1 24c0957f59 地大工会报销手机端UI优化 2026-08-30 17:50:00 +08:00
c-zhouhf1 58921d9bb2 整改提案
整改慰问增加骨架屏
整改作品征集
整改困难帮扶
2026-08-30 17:04:37 +08:00
c-chengc1 5f73c6a7a3 Merge remote-tracking branch 'origin/feature_教代会代表证' into release_20260831 2026-08-30 16:03:12 +08:00
c-chenxs1 d6a9f9aeef feat: 会议选座、代表阅览-sql 2026-08-30 16:02:50 +08:00
c-chengc1 0d944da8b5 Merge remote-tracking branch 'origin/feature_教代会代表证' into release_20260831 2026-08-30 15:53:38 +08:00
c-chenxs1 6124e33edb feat: 会议选座、代表阅览2 2026-08-30 15:49:13 +08:00
c-chenxs1 b0182731bd feat: 会议选座、代表阅览 2026-08-30 15:49:07 +08:00
c-chengc1 479074d684 Merge remote-tracking branch 'origin/feature_会员模块UI优化' into release_20260831 2026-08-30 14:41:59 +08:00
c-chengc1 81912befab Merge remote-tracking branch 'origin/feature_教代会代表证' into release_20260831 2026-08-30 14:40:14 +08:00
c-chengc1 4efbfe40ac Merge remote-tracking branch 'origin/feature_职代会UI优化' into release_20260831 2026-08-30 14:39:27 +08:00
c-chengc1 6573043dae Merge remote-tracking branch 'origin/feature_社团组织管理UI优化' into release_20260831 2026-08-30 14:38:18 +08:00
c-chengc1 e41b1966b6 Merge remote-tracking branch 'origin/feature_品牌工作室' into release_20260831 2026-08-30 14:37:47 +08:00
c-chengc1 b026020ae8 Merge remote-tracking branch 'origin/dev-mall' into release_20260831 2026-08-30 14:37:18 +08:00
c-chengc1 7ae67a46a5 feat:职代会ui优化 2026-08-30 14:34:42 +08:00
c-zhangr1 4db67dfccf 移动端人员管理模块UI优化 2026-08-30 13:58:47 +08:00
= 760bc1a33c 手机端社团UI优化 2026-08-30 11:53:51 +08:00
= bc8f23a61d 手机端社团UI优化 2026-08-30 11:11:29 +08:00
c-chenxs1 ec54e3fc0d feat: 代表团凭证展示优化 2026-08-29 17:40:45 +08:00
c-chenxs1 b1cd738a53 Merge remote-tracking branch 'refs/remotes/origin/feature_主分支合并UI优化' into feature_教代会代表证
# Conflicts:
#	src/main/java/com/budwk/app/sys/controller/SysConfController.java
2026-08-29 16:27:12 +08:00
c-chenxs1 f2fb656912 feat: 代表团凭证 2026-08-29 16:23:09 +08:00
c-chenxs1 97d94bc702 feat: 品牌工作室菜单脚本提交 2026-08-29 10:27:31 +08:00
c-chenxs1 31f4e5c4a6 feat: 积分商城菜单脚本提交 2026-08-29 10:25:51 +08:00
c-chengc1 35ea26b750 Merge remote-tracking branch 'origin/feature_职代会' into feature_职代会UI优化
# Conflicts:
#	src/main/resources/views/layouts/platform_h5.html
2026-08-29 10:16:55 +08:00
c-chengc1 7d2bf71d36 Merge remote-tracking branch 'origin/feature_活动电子档案' into release_20260829 2026-08-29 09:50:22 +08:00
c-duxc1 17e8379903 活动电子档案代码重发; 2026-08-29 09:39:34 +08:00
c-duxc1 fae24dfecd Merge branch 'main' into feature_活动电子档案
# Conflicts:
#	src/main/resources/views/platform/zhgh/archive/docType/index.html
#	src/main/resources/views/platform/zhgh/archive/filing/index.html
#	src/main/resources/views/platform/zhgh/archive/project/index.html
2026-08-29 09:29:50 +08:00
c-chengc1 51c38e4f30 Merge branch 'feature_品牌工作室' into release_20260829 2026-08-29 09:19:17 +08:00
c-chengc1 60c3183d29 Merge remote-tracking branch 'refs/remotes/origin/dev-mall' into release_20260829
# Conflicts:
#	src/main/java/com/budwk/app/sys/controller/v4/SysV4MsgController.java
#	src/main/resources/views/platform/zhghh5/sys/home/msg.js
2026-08-29 09:12:07 +08:00
c-chenxs1 67bcbfc737 feat: 品牌工作室脚本提交 2026-08-29 09:08:14 +08:00
c-chengc1 c8461e6be5 Merge remote-tracking branch 'origin/feature_主分支合并UI优化' into release_20260829 2026-08-29 08:54:49 +08:00
c-chengc1 b5e7a26fdd Merge remote-tracking branch 'origin/feature_重大事项' into release_20260829 2026-08-29 08:54:06 +08:00
c-chengc1 cac1453547 Merge remote-tracking branch 'origin/feature_活动电子档案' into release_20260829
# Conflicts:
#	src/main/resources/views/platform/zhgh/archive/docType/index.html
#	src/main/resources/views/platform/zhgh/archive/filing/index.html
#	src/main/resources/views/platform/zhgh/archive/project/index.html
2026-08-29 08:53:45 +08:00
c-chengc1 c9f8e852b2 Merge remote-tracking branch 'refs/remotes/origin/feature_品牌工作室' into release_20260829 2026-08-29 08:52:42 +08:00
c-chengc1 ef68070afd Merge branch 'refs/heads/feature_职代会' into release_20260829
# Conflicts:
#	src/main/resources/views/layouts/platform_h5.html
2026-08-29 08:48:32 +08:00
c-chengc1 58218ad9c4 feat:首页推荐服务按钮布局适配(换行) 2026-08-29 08:45:39 +08:00
c-zhouhf1 37f874f31d 问卷整改 2026-08-28 17:00:42 +08:00
c-chengc1 c8f80c8d5a Merge branch 'refs/heads/feature_主分支合并UI优化' 2026-08-26 15:02:18 +08:00
c-chengc1 ccc9b518da feat:职工福利ui优化 2026-08-26 15:01:10 +08:00
c-zhouhf1 013833bc58 我的签字自动回显 2026-08-26 14:43:35 +08:00
c-zhouhf1 0a0840c5d2 Merge branch 'main' of https://dd3skj.picp.vip/JyuHsin/zhgh_cug_v4 2026-08-26 13:57:48 +08:00
c-zhouhf1 7509a9467c 移植杭医版生日祝福 2026-08-26 13:57:37 +08:00
c-zhangr1 58273be5c9 Merge remote-tracking branch 'origin/main' 2026-08-26 13:55:59 +08:00
c-zhangr1 1d5061e39a change 2026-08-26 13:55:37 +08:00
c-zhangr1 368c0b2ec8 change 2026-08-26 13:55:08 +08:00
c-chengc1 49c34cf226 feat:文化活动表单样式优化 2026-08-26 13:51:31 +08:00
c-chengc1 0dd3a66a90 Merge remote-tracking branch 'origin/feature_主分支合并UI优化' into feature_主分支合并UI优化 2026-08-26 13:50:26 +08:00
c-chengc1 cc2b3b8ab5 feat:职工福利ui优化 2026-08-26 13:50:12 +08:00
c-zhouhf1 e57b712134 整改慰问UI 2026-08-26 08:51:33 +08:00
c-chengc1 6678ca6a5a feat:首页普惠信息插画优化 2026-08-25 16:25:17 +08:00
c-chengc1 8e3752dd2d feat:首页普惠信息插画优化 2026-08-25 16:23:08 +08:00
c-chengc1 159f081f3f feat:应用中心底部导航栏遮挡问题修复 2026-08-25 16:19:36 +08:00
c-zhouhf1 b81c59dc86 整改提案UI 2026-08-25 15:58:05 +08:00
c-zhangr1 9503b5aa73 change 2026-08-25 13:50:47 +08:00
c-chengc1 e83f1317a4 Merge branch 'refs/heads/feature_界面UI优化' into feature_主分支合并UI优化
# Conflicts:
#	src/main/resources/views/platform/zhghh5/democratic/proposal/query/comprehensive/index.html
#	src/main/resources/views/platform/zhghh5/democratic/proposal/transact/commissioner/index.html
#	src/main/resources/views/platform/zhghh5/democratic/proposal/transact/delegation/index.html
#	src/main/resources/views/platform/zhghh5/democratic/proposal/transact/feedbackEvaluation/index.html
#	src/main/resources/views/platform/zhghh5/democratic/proposal/transact/mine/index.html
#	src/main/resources/views/platform/zhghh5/democratic/proposal/transact/schoolLeaderApproval/index.html
#	src/main/resources/views/platform/zhghh5/democratic/proposal/transact/seconded/index.html
#	src/main/resources/views/platform/zhghh5/democratic/proposal/transact/suggestion/index.html
#	src/main/resources/views/platform/zhghh5/democratic/proposal/transact/unitReply/index.html
2026-08-25 10:17:05 +08:00
c-chengc1 34726c2b11 Merge remote-tracking branch 'origin/feature_界面UI优化' into feature_界面UI优化 2026-08-25 10:09:17 +08:00
c-chengc1 707325c86e feat:亲子活动UI优化,首页按钮布局优化、插画优化 2026-08-25 10:08:57 +08:00
c-zhouhf1 6815fb7f38 整改提案UI 2026-08-24 18:07:41 +08:00
c-zhouhf1 0fed9f3fc8 整改福利UI 2026-08-24 14:06:21 +08:00
c-zhangr1 33252a7225 Merge remote-tracking branch 'origin/main' 2026-08-21 15:05:34 +08:00
c-zhangr1 06893cf3e9 change 2026-08-21 14:58:15 +08:00
= 52d28a0d6d Merge branch 'main' of https://dd3skj.picp.vip/JyuHsin/zhgh_cug_v4 2026-08-21 08:39:48 +08:00
= 6dc7c09786 commit 2026-08-21 08:39:18 +08:00
c-zhouhf1 fac819eb00 整改提案查看样式 2026-08-20 17:48:53 +08:00
c-zhangr1 b556fdacc5 Merge remote-tracking branch 'origin/main' 2026-08-20 17:26:55 +08:00
c-zhangr1 24cc40c698 change 2026-08-20 17:26:14 +08:00
= 05a7d3320e commit 2026-08-20 16:46:13 +08:00
c-zhouhf1 4803c7320e commit 2026-08-20 16:41:26 +08:00
= e07ed664f1 commit 2026-08-20 15:43:53 +08:00
c-zhangr1 840f7e2a59 Merge remote-tracking branch 'origin/main' 2026-08-20 10:55:34 +08:00
c-zhangr1 7622e414b6 change 2026-08-20 10:55:26 +08:00
c-zhangr1 34fe53caf9 change 2026-08-20 10:55:04 +08:00
c-zhouhf1 de46a2404d commit 2026-08-20 09:59:14 +08:00
= 681d8c6e81 Merge branch 'main' of https://dd3skj.picp.vip/JyuHsin/zhgh_cug_v4 2026-08-20 08:40:44 +08:00
= 9d6e6eb189 commit 2026-08-20 08:39:10 +08:00
c-zhouhf1 a359a01790 commit 2026-08-20 08:35:36 +08:00
c-chenxs1 19bbd61436 feat: 品牌工作室迁移完成 2026-08-19 18:04:39 +08:00
c-zhouhf1 db33fd0bbd commit 2026-08-19 17:59:41 +08:00
c-chengc1 2b44463d93 feat:体育活动、文化活动、品牌活动、系统金刚区 UI优化 2026-08-19 17:20:28 +08:00
= c6d0f5b7d4 commit 2026-08-19 16:58:43 +08:00
c-zhangr1 c6609dcb0b Merge remote-tracking branch 'origin/main' 2026-08-19 16:43:47 +08:00
c-zhangr1 d2321929c2 change 2026-08-19 16:43:18 +08:00
c-zhouhf1 f6389f2b67 Merge branch 'main' of https://dd3skj.picp.vip/JyuHsin/zhgh_cug_v4 2026-08-19 16:38:19 +08:00
c-zhouhf1 6db19f6e9d commit 2026-08-19 16:38:07 +08:00
c-chenxs1 d38d48a147 Merge branch 'main' into feature_品牌工作室 2026-08-19 09:25:28 +08:00
c-chenxs1 1b17c3434b feat: 品牌工作室初始化提交 2026-08-19 09:17:12 +08:00
= 55cd8c86fa commit 2026-08-18 15:17:32 +08:00
c-chengc1 bc783a4beb feat:系统UI优化初版 2026-08-17 17:10:33 +08:00
c-zhangr1 ac5a2d902e change 2026-08-17 11:01:48 +08:00
c-zhouhf1 e4485d7b02 commit 2026-08-13 08:39:26 +08:00
c-chenxs1 1096ea245f feat: 初始化gitignore和readme 2026-08-10 10:48:36 +08:00
c-chengc1 4cf4e45ea2 feat:系统UI优化初版 2026-08-07 17:08:19 +08:00
= ec900e5b4d commit 2026-08-05 10:27:25 +08:00
= d5f4543493 Merge branch 'main' of https://dd3skj.picp.vip/JyuHsin/zhgh_cug_v4 2026-08-05 08:43:28 +08:00
= de5f8dc9ba commit 2026-08-05 08:35:17 +08:00
c-zhouhf1 6e3279e485 commit 2026-08-04 11:04:15 +08:00
c-chengc1 956a76ac63 Merge remote-tracking branch 'refs/remotes/origin/feature_风采墙' into feature_界面UI优化 2026-08-04 09:07:29 +08:00
c-chengc1 98bca06eea feat:UI优化首页初版 - 插图优化 2026-08-04 09:06:45 +08:00
c-chengc1 f79570cf6b feat:UI优化首页初版 2026-08-04 08:46:47 +08:00
c-chenxs1 44cf66c570 feat: 积分商城-完成 2026-07-31 19:07:59 +08:00
c-chengc1 29f305d677 feat:职代会h5端UI优化调整 2026-07-31 17:28:32 +08:00
c-duxc1 a27075237d web端-风采墙-统计分析: 饼状体和柱状图初始化异常修复; 2026-07-31 17:04:00 +08:00
c-duxc1 27505e238a web端-新增风采墙模块(工会信息传播与文化展示): 风采展示轮播图样式调整; 2026-07-31 16:56:50 +08:00
c-duxc1 e00fdc0c78 web端-新增风采墙模块(工会信息传播与文化展示): 补充所属工会;统计,内容展示等样式调整; 2026-07-31 16:50:17 +08:00
c-chengc1 c910752e2e Merge remote-tracking branch 'origin/feature_职代会' into feature_职代会 2026-07-31 11:06:59 +08:00
c-chengc1 5477355f64 feat:职代会h5端文件预览兼容、增加切换届次功能 2026-07-31 11:06:45 +08:00
c-chenxs1 9be904bde0 feat: 优化 2026-07-31 10:42:00 +08:00
c-duxc1 11ae8175c1 web端-职代会: 暂时注释投票管理-会议投票'上传资料'按钮,避免文件修改时数据库文件累加至会议资料表; 2026-07-31 10:39:57 +08:00
c-duxc1 b3406f8b00 web端-新增风采墙模块(工会信息传播与文化展示): 修复栏目管理下级栏目无法打开等问题; 2026-07-30 16:59:50 +08:00
c-chenxs1 5de4d6f9f4 feat: 已实现订单基本逻辑,到完成,却订单售后 2026-07-30 15:55:00 +08:00
c-duxc1 5043cee747 web端-新增风采墙模块(工会信息传播与文化展示); 2026-07-30 15:42:06 +08:00
c-duxc1 e450b1a7e9 web端-活动电子档案: 页面样式调整(弹窗加宽至65%左右); 2026-07-30 14:34:02 +08:00
c-duxc1 cc71822aae web端-职代会管理: 页面样式调整(弹窗加宽至65%左右); 2026-07-30 14:07:18 +08:00
c-chenxs1 1cbacf987d feat: 积分商城-基本实现拆单、确认收货2 2026-07-29 18:22:05 +08:00
= ff3e8075b5 commit 2026-07-29 16:50:05 +08:00
c-duxc1 058f6b2563 web端-职代会管理-投票管理: 页面样式调整; 2026-07-29 16:08:05 +08:00
c-duxc1 bff78c6ced web端职代会管理: 修复congress_materials职代会资料文件的fileType,fileSize字段为空的问题; 2026-07-29 15:56:03 +08:00
c-chenxs1 d613a41854 feat: 积分商城-基本实现拆单、确认收货 2026-07-29 15:51:26 +08:00
c-duxc1 8bff6f8ce9 新增web端职代会管理(职代会届次取值teacher_congress_session表数据); 2026-07-29 14:57:41 +08:00
= e8ed1d93f9 commit 2026-07-29 13:44:45 +08:00
c-chenxs1 51a951d863 feat: 积分商城-补充 2026-07-29 10:11:25 +08:00
c-duxc1 a93cbc9704 Revert "新增活动电子档案模块;"
This reverts commit a8a4052c45.
2026-07-28 17:42:37 +08:00
c-duxc1 d90f87e453 Revert "新增活动电子档案模块;"
This reverts commit b2aef12c84.
2026-07-28 17:42:23 +08:00
c-chengc1 704bc278fa feat:浦发项目模块迁移-职代会h5端初始化 2026-07-28 17:27:29 +08:00
c-duxc1 b2aef12c84 新增活动电子档案模块; 2026-07-28 16:55:02 +08:00
c-duxc1 a8a4052c45 新增活动电子档案模块; 2026-07-28 15:59:45 +08:00
c-chengc1 5df36acc22 feat:浦发项目模块迁移-重大事项初始化 2026-07-28 10:54:28 +08:00
c-chenxs1 0eefc705a1 feat: h5页面-积分商城-外部服务接口 2026-07-27 15:36:26 +08:00
c-chenxs1 a2b769aeff feat: h5页面-积分商城首页优化 2026-07-27 15:09:52 +08:00
c-chenxs1 f054226683 feat: h5页面 2026-07-27 14:06:19 +08:00
c-chenxs1 a1047368fd feat: 对账记录2 2026-07-27 13:30:55 +08:00
c-chenxs1 a1a7527ca8 feat: 对账记录 2026-07-27 11:37:31 +08:00
c-chenxs1 5bbfba353d feat: 发票管理 2026-07-27 11:34:31 +08:00
c-chenxs1 2d8a31a3cc feat: 对账管理 2026-07-27 11:33:43 +08:00
c-chenxs1 d4e7e4282b feat: 消息推送 2026-07-27 11:17:15 +08:00
c-chenxs1 784350bb42 feat: 积分发放模块 2026-07-27 11:03:19 +08:00
c-chenxs1 dd7c35eb50 feat: 用户积分管理 2026-07-27 11:03:04 +08:00
c-chenxs1 a6a2471111 feat: web端初步完成 2026-07-27 08:41:32 +08:00
c-chenxs1 08e6d08668 初始化gitignore 2026-07-24 17:08:12 +08:00
c-chenxs1 6d63e502ff 初始化数据库 2026-07-24 16:57:00 +08:00
c-zhouhf1 785b9a4b80 commit 2026-07-23 18:30:30 +08:00
c-zhouhf1 86ab0142da Merge branch 'main' of https://dd3skj.picp.vip/JyuHsin/zhgh_cug_v4 2026-07-22 13:55:54 +08:00
c-zhouhf1 6b4a3c0525 commit 2026-07-22 13:52:20 +08:00
c-zhangr1 f81544e67f change 2026-07-17 10:56:54 +08:00
c-zhangr1 0936d61450 Merge remote-tracking branch 'origin/main' 2026-07-16 15:01:56 +08:00
c-zhangr1 65831a02dd change 2026-07-16 14:59:53 +08:00
zhouhefeng 730b0efebb Merge branch 'main' of https://dd3skj.picp.vip/JyuHsin/zhgh_cug_v4
 Conflicts:
	src/main/java/com/budwk/app/zhgh/democratic/teachercongress/institution/controller/TeacherCongressInstitutionController.java
	src/main/resources/views/platform/zhgh/democratic/teachercongress/institution/basicTable.js
	src/main/resources/views/platform/zhgh/democratic/teachercongress/institution/index.html
2026-07-15 11:49:49 +08:00
zhouhefeng 3425888c91 commit 2026-07-15 11:47:47 +08:00
= 73954cf4e7 commit 2026-07-15 11:13:08 +08:00
= 5c46d07f46 Merge remote-tracking branch 'origin/main' 2026-07-15 09:24:24 +08:00
= b974d84086 commit 2026-07-15 09:23:00 +08:00
c-zhouhf1 fb25ad45a8 commit 2026-07-14 18:05:53 +08:00
c-zhangr1 cbc0dcf67d change普惠疗休养 2026-07-14 17:41:39 +08:00
c-zhouhf1 33a9a3df05 Merge branch 'main' of https://dd3skj.picp.vip/JyuHsin/zhgh_cug_v4 2026-07-14 16:41:16 +08:00
c-zhouhf1 2168888ca7 commit 2026-07-14 16:40:42 +08:00
= 2f8158a658 Merge remote-tracking branch 'origin/main' 2026-07-14 16:36:44 +08:00
c-zhangr1 71558c3df8 Merge remote-tracking branch 'origin/main'
# Conflicts:
#	src/main/resources/views/platform/zhgh/democratic/proposal/transact/unitRead/index.html
2026-07-14 16:31:37 +08:00
c-zhangr1 68d8045179 1 2026-07-14 16:30:12 +08:00
= 5626139605 commit 2026-07-14 16:29:59 +08:00
c-zhouhf1 33687bbd66 Merge branch 'main' of https://dd3skj.picp.vip/JyuHsin/zhgh_cug_v4 2026-07-14 16:28:31 +08:00
c-zhouhf1 f39735f1f2 commit 2026-07-14 16:28:22 +08:00
c-zhangr1 924feefdeb Merge remote-tracking branch 'origin/main'
# Conflicts:
#	src/main/resources/db/menu/init_tour_menu.sql
#	src/main/resources/db/tour_setting_create.sql
#	src/main/resources/views/platform/zhgh/staffmanage/member/apply/common/commonQuery.js
#	src/main/resources/views/platform/zhgh/staffmanage/member/change/common/commonQuery.js
#	src/main/resources/views/platform/zhgh/staffmanage/member/info/common/commonQuery.js
2026-07-14 16:27:39 +08:00
c-zhangr1 c7503cd4cc 1 2026-07-14 16:24:26 +08:00
= c504830db3 commit 2026-07-14 16:20:40 +08:00
c-zhangr1 fe250baef8 1 2026-07-14 14:35:17 +08:00
c-zhangr1 93ef06cfe6 1 2026-07-14 14:34:34 +08:00
= 24e2d9e826 commit 2026-07-14 09:19:04 +08:00
zhouhefeng 9b4f4d57be Merge branch 'main' of https://dd3skj.picp.vip/JyuHsin/zhgh_cug_v4 2026-07-14 09:15:41 +08:00
zhouhefeng 8da7d2f3b4 Changes 2026-07-14 09:15:34 +08:00
zhouhefeng cadd4bbffe Changes 2026-07-14 09:12:00 +08:00
zhouhefeng 4475404f70 Changes 2026-07-10 08:42:57 +08:00
c-zhouhf1 783e2b1fb0 commit 2026-07-08 15:28:09 +08:00
c-zhouhf1 f7f239c68b commit 2026-07-08 09:51:11 +08:00
c-zhangr1 37a17972af Merge remote-tracking branch 'origin/main' 2026-07-08 09:26:37 +08:00
c-zhangr1 1a64de529e 1 2026-07-08 09:26:27 +08:00
c-zhouhf1 ac891ce87e commit 2026-07-08 09:16:21 +08:00
c-zhouhf1 e80ec00183 Merge branch 'main' of https://dd3skj.picp.vip/JyuHsin/zhgh_cug_v4 2026-07-07 15:16:11 +08:00
c-zhouhf1 cc5a944b47 commit 2026-07-07 15:15:26 +08:00
= 635038b25c commit 2026-07-07 10:24:37 +08:00
= 34627245ac commit 2026-07-03 17:25:27 +08:00
c-zhouhf1 6380842b34 commit 2026-07-03 17:10:35 +08:00
= 2e3c3e87da commit 2026-07-01 09:21:06 +08:00
c-zhouhf1 3f8f8f5f44 commit 2026-06-16 10:29:29 +08:00
c-zhouhf1 b3f02790fc Merge branch 'main' of https://dd3skj.picp.vip/JyuHsin/zhgh_cug_v4
# Conflicts:
#	src/main/resources/application-dev.yaml
2026-06-16 10:28:39 +08:00
c-zhouhf1 455e4ca117 commit 2026-06-16 10:28:26 +08:00
zhouhefeng 643c978692 Changes 2026-06-15 14:36:39 +08:00
zhouhefeng 601e1ac039 Changes 2026-06-15 14:20:03 +08:00
zhouhefeng 873213de53 Changes 2026-06-15 14:18:01 +08:00
zhouhefeng 449428b762 commit 2026-06-02 08:17:07 +08:00
zhouhefeng 268cbd6a49 Merge remote-tracking branch 'origin/main' 2026-06-01 11:04:03 +08:00
zhouhefeng 27f2b1670e Changes 2026-06-01 11:03:09 +08:00
c-zhouhf1 792ae3de1b commit 2026-06-01 09:23:07 +08:00
c-zhouhf1 eff5c662d2 Merge branch 'main' of https://dd3skj.picp.vip/JyuHsin/zhgh_cug_v4
# Conflicts:
#	src/main/java/com/budwk/app/sys/controller/SysConfController.java
#	src/main/resources/views/layouts/v4/home/index.html
2026-06-01 08:41:59 +08:00
c-zhouhf1 79e61f8d99 commit 2026-06-01 08:41:09 +08:00
zhouhefeng 8b47088953 commit 2026-06-01 08:36:26 +08:00
c-zhouhf1 f05e534e52 commit 2026-05-29 09:23:09 +08:00
c-zhouhf1 9d7fa18c53 commit 2026-05-29 09:21:40 +08:00
c-zhangr1 45515c2108 Merge remote-tracking branch 'origin/main' 2026-05-28 19:53:20 +08:00
c-zhangr1 dc82d5ed23 1 2026-05-28 19:53:02 +08:00
c-zhouhf1 02dde3fe3a Merge branch 'main' of https://dd3skj.picp.vip/JyuHsin/zhgh_cug_v4 2026-05-28 19:52:36 +08:00
c-zhouhf1 8915ace193 commit 2026-05-28 19:52:20 +08:00
zhouhefeng 7ee1c4fbb5 1 2026-05-28 18:31:36 +08:00
zhouhefeng daac940f3f Merge remote-tracking branch 'origin/main' 2026-05-20 09:04:22 +08:00
zhouhefeng e48f8956a5 Changes 2026-05-20 09:02:56 +08:00
zhouhefeng 3f76d2c6cb Changes 2026-05-20 08:54:05 +08:00
= 5fb4c261c8 Merge branch 'main' of https://dd3skj.picp.vip/JyuHsin/zhgh_cug_v4 2026-05-09 15:36:56 +08:00
= 9e014daddf commit 2026-05-09 15:33:13 +08:00
c-zhouhf1 4fc3b318ed commit 2026-05-09 15:04:03 +08:00
c-zhouhf1 f7a0c1e599 commit 2026-05-09 15:03:53 +08:00
= 943c15a7f4 commit 2026-05-09 13:41:24 +08:00
= 7e3fca6a7f commit 2026-05-08 20:08:14 +08:00
= 9d22c1c4e2 commit 2026-05-07 18:01:54 +08:00
c-zhouhf1 9123ed5f66 Merge branch 'main' of https://dd3skj.picp.vip/JyuHsin/zhgh_cug_v4 2026-05-07 10:27:55 +08:00
c-zhouhf1 63e2091cbb commit 2026-05-07 10:27:43 +08:00
c-zhangr1 93cc059740 Merge remote-tracking branch 'origin/main' 2026-05-07 09:43:47 +08:00
c-zhangr1 251abdc9f3 日常办公-场地预约:
已过的时间段应该不能选择
2026-05-07 09:43:26 +08:00
= d615098f50 Merge branch 'main' of https://dd3skj.picp.vip/JyuHsin/zhgh_cug_v4 2026-05-06 19:42:49 +08:00
c-zhangr1 9641e0183b Merge remote-tracking branch 'origin/main' 2026-05-06 19:42:22 +08:00
c-zhouhf1 927841f914 commit 2026-05-06 19:35:50 +08:00
c-zhangr1 afd9016ef8 1 2026-05-06 19:34:13 +08:00
= f101ab2098 commit 2026-05-06 19:34:07 +08:00
= 2956cb6622 commit 2026-05-06 15:47:01 +08:00
= 4bd10c5337 commit 2026-05-06 14:23:54 +08:00
= 5e666bdebf commit 2026-04-30 16:39:41 +08:00
= d7c07aba71 commit 2026-04-30 15:39:07 +08:00
= 466a67e0df Merge branch 'main' of https://dd3skj.picp.vip/JyuHsin/zhgh_cug_v4
# Conflicts:
#	src/main/resources/static/components/module/activity/UserScope.vue
2026-04-30 11:09:23 +08:00
zhouhefeng b3a84786c3 Changes 2026-04-30 10:51:59 +08:00
zhouhefeng 195688cc01 Merge branch 'main' of https://dd3skj.picp.vip/JyuHsin/zhgh_cug_v4 2026-04-30 10:03:51 +08:00
c-zhangr1 fd2e12ab58 Merge remote-tracking branch 'origin/main' 2026-04-30 09:23:12 +08:00
c-zhangr1 5c646ff039 1 2026-04-30 09:22:53 +08:00
= 6856d4fc2e Merge branch 'main' of https://dd3skj.picp.vip/JyuHsin/zhgh_cug_v4 2026-04-27 17:03:41 +08:00
= 896a6fe1ac commit 2026-04-27 17:03:11 +08:00
zhouhefeng 48892370c0 Changes 2026-04-27 08:42:06 +08:00
zhouhefeng dcbb1904c2 Merge branch 'main' of https://dd3skj.picp.vip/JyuHsin/zhgh_cug_v4 2026-04-27 08:39:59 +08:00
c-zhouhf1 17bc74a014 Merge branch 'main' of https://dd3skj.picp.vip/JyuHsin/zhgh_cug_v4 2026-04-25 18:20:33 +08:00
c-zhouhf1 568dc5e169 commit 2026-04-25 18:20:04 +08:00
c-zhouhf1 9edd627390 commit 2026-04-25 18:15:37 +08:00
c-zhouhf1 924f99dcdf commit 2026-04-25 18:13:53 +08:00
= 458721d2a9 commit 2026-04-25 18:13:42 +08:00
c-zhangr1 4189ec7834 1 2026-04-25 17:53:37 +08:00
c-zhangr1 aa51d082c1 Merge remote-tracking branch 'origin/main' 2026-04-25 16:39:26 +08:00
c-zhangr1 f454b6d6c3 1 2026-04-25 16:38:47 +08:00
c-zhangr1 e8e39c128c 1 2026-04-25 14:51:03 +08:00
c-zhangr1 9b92c29197 bug整改 2026-04-25 11:41:14 +08:00
c-zhouhf1 e3be4cbd6e commit 2026-04-25 08:53:03 +08:00
zhouhefeng 08ee03f802 Changes 2026-04-24 16:42:07 +08:00
zhouhefeng 693925a088 Merge branch 'main' of https://dd3skj.picp.vip/JyuHsin/zhgh_cug_v4 2026-04-24 16:39:14 +08:00
zhouhefeng 6e246854fb Changes 2026-04-24 16:39:05 +08:00
c-zhangr1 14a150a335 Merge remote-tracking branch 'origin/main' 2026-04-24 15:35:08 +08:00
= d63c8812d2 commit 2026-04-24 15:32:34 +08:00
= e6b4e9f96a Merge branch 'main' of https://dd3skj.picp.vip/JyuHsin/zhgh_cug_v4 2026-04-24 15:29:18 +08:00
= 9cb64ddef1 commit 2026-04-24 15:28:35 +08:00
c-zhouhf1 0e5f241cc7 提案整改 2026-04-24 15:26:47 +08:00
c-zhouhf1 79f9cf9123 提案整改 2026-04-24 15:26:30 +08:00
= 3207e0a594 commit 2026-04-24 14:27:55 +08:00
c-zhangr1 d6b4be6218 bug整改 2026-04-24 11:51:46 +08:00
c-zhangr1 8bd5c01c26 bug整改 2026-04-23 17:38:05 +08:00
@jyuhsin c2f93eb4b3 commit 2026-04-23 15:00:46 +08:00
c-zhangr1 473240441f 1 2026-04-23 10:37:18 +08:00
c-zhangr1 6e087d2b82 1 2026-04-22 20:05:01 +08:00
@jyuhsin f18d0ea0ba Merge branch 'main' of https://dd3skj.picp.vip/JyuHsin/zhgh_cug_v4 2026-04-22 19:05:31 +08:00
@jyuhsin caf9df2633 commit 2026-04-22 19:04:46 +08:00
= 40dc2b304f 协会整改 2026-04-21 15:56:41 +08:00
c-zhangr1 fef2608d78 Merge remote-tracking branch 'origin/main' 2026-04-21 14:28:55 +08:00
c-zhangr1 e68013d525 1 2026-04-21 14:02:46 +08:00
c-zhouhf1 7fb0dc304f 提案整改 2026-04-21 11:06:13 +08:00
c-zhouhf1 a02ba1abfd 提案整改 2026-04-21 11:06:00 +08:00
c-zhouhf1 a14bd95e44 提案整改 2026-04-21 11:03:56 +08:00
c-zhangr1 8df583f186 1 2026-04-21 08:53:52 +08:00
c-zhangr1 af18f33493 1 2026-04-16 14:51:52 +08:00
c-zhangr1 b67930563a Merge remote-tracking branch 'origin/main' 2026-04-15 14:07:51 +08:00
c-zhangr1 e1a99feb28 1 2026-04-15 14:07:30 +08:00
@jyuhsin 3f25c9e2e7 commit 2026-04-15 14:06:03 +08:00
= 909458a92c Merge branch 'main' of https://dd3skj.picp.vip/JyuHsin/zhgh_cug_v4 2026-04-11 16:42:03 +08:00
= c121492ce1 会员整改 2026-04-11 16:41:11 +08:00
c-zhouhf1 8e0f23d6af 提案整改 2026-04-10 09:04:58 +08:00
c-zhouhf1 476dc19590 提案整改 2026-04-09 14:55:19 +08:00
= 0902cd4613 commit 2026-04-07 10:41:17 +08:00
c-zhouhf1 fc1da7c37c Merge branch 'main' of https://dd3skj.picp.vip/JyuHsin/zhgh_cug_v4 2026-04-03 17:32:05 +08:00
c-zhouhf1 590733bc49 commit 2026-04-03 17:31:51 +08:00
= 95e3afb492 协会整改 2026-04-03 16:47:53 +08:00
c-zhouhf1 e179abf716 commit 2026-04-03 10:33:55 +08:00
c-zhouhf1 765c715d28 commit 2026-04-03 10:32:14 +08:00
c-zhouhf1 18236d8d1f Merge branch 'main' of https://dd3skj.picp.vip/JyuHsin/zhgh_cug_v4 2026-04-01 18:53:23 +08:00
c-zhouhf1 d799d11f69 commit 2026-04-01 18:53:08 +08:00
@jyuhsin 6adf949e3e commit 2026-03-31 16:55:35 +08:00
@jyuhsin dc71dc22d8 commit 2026-03-26 14:03:21 +08:00
Paidax c5fbf8576c git查看用户 2026-03-20 15:09:35 +08:00
Paidax ad326ec240 Merge remote-tracking branch 'origin/main' 2026-03-17 16:31:21 +08:00
= 94c0e7691e commit 2026-03-17 16:31:01 +08:00
Paidax 4fb1e137c4 git查看用户 2026-03-17 16:09:25 +08:00
c-zhouhf1 5340b78204 commit 2026-02-06 09:18:33 +08:00
= a71e169656 困难帮扶移植 2026-02-05 09:19:06 +08:00
c-zhouhf1 26486ef737 commit 2026-02-04 11:00:34 +08:00
c-zhouhf1 2eb28ee83a Merge remote-tracking branch 'origin/main' 2026-02-02 10:30:29 +08:00
c-zhouhf1 e76d8123cb commit 2026-02-02 10:30:15 +08:00
@jyuhsin 9c44686995 Merge branch 'main' of https://dd3skj.picp.vip/JyuHsin/zhgh_cug_v4 2026-01-30 15:09:10 +08:00
@jyuhsin 47bcbb381e commit 2026-01-30 15:09:00 +08:00
c-zhouhf1 1ab7c6c998 Merge branch 'main' of https://dd3skj.picp.vip/JyuHsin/zhgh_cug_v4 2026-01-29 18:43:00 +08:00
c-zhouhf1 76c3db3b7f commit 2026-01-29 18:42:50 +08:00
= d4426ba9a9 Merge branch 'main' of https://dd3skj.picp.vip/JyuHsin/zhgh_cug_v4 2026-01-28 11:46:09 +08:00
= e0fb1f8c76 困难帮扶移植 2026-01-28 11:45:46 +08:00
c-zhouhf1 c05f04d7d9 commit 2026-01-27 15:30:50 +08:00
c-zhouhf1 f271b8a084 commit 2026-01-27 15:29:58 +08:00
c-zhouhf1 9d19da8395 Merge branch 'main' of https://dd3skj.picp.vip/JyuHsin/zhgh_cug_v4
# Conflicts:
#	src/main/java/com/budwk/app/zhgh/staffbenefit/medicalMutualAid/medical/param/MedicalPageForm.java
2026-01-24 17:44:36 +08:00
c-zhouhf1 d53b63f6ac commit 2026-01-24 17:44:19 +08:00
@jyuhsin 4f14ab3258 Merge remote-tracking branch 'origin/main' 2026-01-24 17:38:23 +08:00
@jyuhsin 7637981eba commit 2026-01-24 17:37:29 +08:00
= bfb0c58057 医疗补助汇总 2026-01-24 17:32:52 +08:00
c-zhouhf1 1634c29758 Merge branch 'main' of https://dd3skj.picp.vip/JyuHsin/zhgh_cug_v4 2026-01-24 10:56:12 +08:00
c-zhouhf1 94c8045208 commit 2026-01-24 10:56:02 +08:00
= e0e6c462b6 工会报销 2026-01-24 09:30:37 +08:00
c-zhouhf1 45d020e5ed commit 2026-01-24 09:19:13 +08:00
@jyuhsin 6a07605b39 commit 2026-01-22 17:32:06 +08:00
@jyuhsin c50bcbf869 commit 2026-01-22 13:53:50 +08:00
c-zhouhf1 7eac8b00b5 commit 2026-01-21 10:44:02 +08:00
c-zhouhf1 c5ec2137e2 commit 2026-01-19 09:01:00 +08:00
c-zhouhf1 fdc1f203a4 commit 2026-01-16 10:27:20 +08:00
@jyuhsin 483719caa1 commit 2026-01-15 15:44:26 +08:00
@jyuhsin 7d566e20a7 commit 2026-01-15 09:26:22 +08:00
@jyuhsin 0a479ae299 commit 2026-01-15 08:56:42 +08:00
@jyuhsin 105c955abd first commit 2026-01-14 14:49:41 +08:00
5804 changed files with 1134984 additions and 22 deletions
+52 -21
View File
@@ -1,26 +1,57 @@
# ---> Java # 依赖与构建产物
# Compiled class file **/node_modules/
*.class **/dist/
**/build/
**/target/
**/out/
**/.next/
**/.nuxt/
**/coverage/
# Log file # 日志、缓存与临时文件
*.log **/*.log
**/*.tmp
**/*.temp
**/.cache/
**/.eslintcache
**/npm-debug.log*
**/yarn-debug.log*
**/yarn-error.log*
**/pnpm-debug.log*
# BlueJ files # IDE 与操作系统文件
*.ctxt .idea/
.vscode/
*.iml
.project
.classpath
.settings/
.DS_Store
Thumbs.db
# Mobile Tools for Java (J2ME) # 本地环境和密钥配置(保留示例文件)
.mtj.tmp/ **/.env
**/.env.local
**/.env.*.local
!**/.env.example
!**/.env.*.example
# Package Files # # 本地开发配置(含数据库、第三方平台密钥,不入库)
*.jar src/main/resources/application-dev.yaml
*.war src/main/resources/application-local.yaml
*.nar # 示例模板需要提交,供其他人参照填写
*.ear !src/main/resources/*.example.yaml
*.zip
*.tar.gz
*.rar
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
hs_err_pid*
replay_pid*
# Java 编译文件
**/*.class
.trae/.ignore
.trae/cache-components/SKILL.md
.trae/code-reviewer/SKILL.md
.trae/find-skills/SKILL.md
.trae/fix/SKILL.md
.trae/frontend-code-review/SKILL.md
.trae/frontend-design/SKILL.md
.trae/fullstack-developer/SKILL.md
.trae/pr-creator/SKILL.md
.trae/update-docs/SKILL.md
.trae/webapp-testing/SKILL.md
+4
View File
@@ -0,0 +1,4 @@
**/wangEditor/*.js
**/nprogress/*.js
**/assets/platform/plugins/**
**/views/*.js
+14
View File
@@ -0,0 +1,14 @@
{
"printWidth": 250,
"singleQuote": false,
"tabWidth": 4,
"semi": false,
"trailingComma": "none",
"bracketSpacing": true,
"arrowParens": "always",
"rangeStart": 0,
"htmlWhitespaceSensitivity": "ignore",
"vueIndentScriptAndStyle": false,
"endOfLine": "auto",
"useTabs": false
}
+7
View File
@@ -0,0 +1,7 @@
FROM openjdk:11.0.13-jdk
COPY ./target/mini.jar /app/
COPY ./target/classes/application.yaml /app/
ENV TZ Asia/Shanghai
WORKDIR /app
CMD ["java","-jar","-Dnutz.boot.configure.yaml.dir=/app","/app/mini.jar"]
+201
View File
@@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "{}"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright {yyyy} {name of copyright owner}
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+37 -1
View File
@@ -1,3 +1,39 @@
# zhgh_nau # zhgh_nau
南京审计大学智慧工会v4 智慧工会系统本地开发说明。
## 访问入口
PC 管理端:
```text
http://127.0.0.1/platform/login
```
H5 端:
```text
http://127.0.0.1/platform/h5
```
## 登录账号
```text
账号:superadmin
密码:以本地数据库或初始化数据为准
```
## 本地启动参数
如果使用 `application-dev.yaml`,IDEA 的 VM options 可配置:
```text
-Dnutz.profiles.active=dev --add-opens java.base/java.lang=ALL-UNNAMED
```
如果端口不是 80,需要在访问地址中补上端口号,例如:
```text
http://127.0.0.1:8080/platform/login
http://127.0.0.1:8080/platform/h5
```
+203
View File
@@ -0,0 +1,203 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.budwk</groupId>
<artifactId>mini</artifactId>
<version>5.6.0-plus</version>
<build>
<resources>
<resource>
<directory>src/main/resources</directory>
<includes>
<include>**/*</include>
</includes>
</resource>
</resources>
<finalName>${project.artifactId}</finalName>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>17</source>
<target>17</target>
<compilerArgs>
<arg>--add-opens</arg>
<arg>java.base/java.lang=ALL-UNNAMED</arg>
</compilerArgs>
<useIncrementalCompilation>false</useIncrementalCompilation>
</configuration>
</plugin>
<plugin>
<artifactId>maven-javadoc-plugin</artifactId>
<version>3.0.1</version>
<executions>
<execution>
<id>attach-javadocs</id>
<goals>
<goal>jar</goal>
</goals>
</execution>
</executions>
<configuration>
<encoding>UTF-8</encoding>
<charset>UTF-8</charset>
<docencoding>UTF-8</docencoding>
</configuration>
</plugin>
<plugin>
<artifactId>maven-source-plugin</artifactId>
<version>3.0.1</version>
<executions>
<execution>
<phase>compile</phase>
<goals>
<goal>jar</goal>
</goals>
</execution>
</executions>
<configuration>
<attach>true</attach>
</configuration>
</plugin>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.22.0</version>
<configuration>
<skip>true</skip>
</configuration>
</plugin>
<plugin>
<groupId>org.nutz.boot</groupId>
<artifactId>nutzboot-maven-plugin</artifactId>
<version>${nutzboot.version}</version>
<configuration>
<mainClass>com.budwk.app.MainLauncher</mainClass>
</configuration>
</plugin>
</plugins>
</build>
<repositories>
<repository>
<id>nutz</id>
<url>https://jfrog.nutz.cn/artifactory/libs-release</url>
</repository>
<repository>
<releases>
<enabled>false</enabled>
</releases>
<snapshots>
<updatePolicy>always</updatePolicy>
</snapshots>
<id>nutz-snapshots</id>
<url>https://jfrog.nutz.cn/artifactory/snapshots</url>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<releases>
<enabled>false</enabled>
</releases>
<snapshots>
<updatePolicy>always</updatePolicy>
</snapshots>
<id>nutz-snapshots</id>
<url>https://jfrog.nutz.cn/artifactory/snapshots</url>
</pluginRepository>
</pluginRepositories>
<dependencies>
<dependency>
<groupId>javax.xml</groupId>
<artifactId>jaxrpc</artifactId>
<version>1.1</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.nutz</groupId>
<artifactId>nutzboot-starter-test-junit4</artifactId>
<version>2.6.0-SNAPSHOT</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.1</version>
<scope>test</scope>
<exclusions>
<exclusion>
<artifactId>hamcrest-core</artifactId>
<groupId>org.hamcrest</groupId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax.websocket</groupId>
<artifactId>javax.websocket-api</artifactId>
<version>1.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-core</artifactId>
<version>${tomcat.version}</version>
</dependency>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-websocket</artifactId>
<version>${tomcat.version}</version>
</dependency>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-el</artifactId>
<version>${tomcat.version}</version>
</dependency>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-jasper</artifactId>
<version>${tomcat.version}</version>
</dependency>
<dependency>
<groupId>org.nutz</groupId>
<artifactId>nutzboot-parent</artifactId>
<version>${nutzboot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
<version>${jaxb-api.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
<properties>
<easypoi.version>4.5.0</easypoi.version>
<jsoup.veision>1.15.3</jsoup.veision>
<mysql-connector-java.version>8.0.28</mysql-connector-java.version>
<tomcat.version>9.0.121</tomcat.version>
<sa-token.version>1.25.0</sa-token.version>
<easy-captcha.version>1.6.2</easy-captcha.version>
<emoji-java.veision>4.0.0</emoji-java.veision>
<jaxb-api.version>2.3.1</jaxb-api.version>
<lombok.version>1.18.34</lombok.version>
<slf4j.version>2.0.8</slf4j.version>
<poitl.version>1.12.2</poitl.version>
<poi.version>4.1.2</poi.version>
<commons-lang3.version>3.8.1</commons-lang3.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<pinyin4j.veision>2.5.1</pinyin4j.veision>
<nutzboot.version>2.6.0-SNAPSHOT</nutzboot.version>
<logback.version>1.5.8</logback.version>
<nutz.version>1.r.70-SNAPSHOT</nutz.version>
<tencentcloud-sdk-java.version>4.0.11</tencentcloud-sdk-java.version>
</properties>
</project>
+597
View File
@@ -0,0 +1,597 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<packaging>jar</packaging>
<groupId>com.budwk</groupId>
<artifactId>mini</artifactId>
<version>5.6.0-plus</version>
<properties>
<nutzboot.version>2.6.0-SNAPSHOT</nutzboot.version>
<nutz.version>1.r.70-SNAPSHOT</nutz.version>
<tomcat.version>9.0.121</tomcat.version>
<mysql-connector-java.version>8.0.28</mysql-connector-java.version>
<jaxb-api.version>2.3.1</jaxb-api.version>
<slf4j.version>2.0.8</slf4j.version>
<logback.version>1.5.8</logback.version>
<sa-token.version>1.25.0</sa-token.version>
<easy-captcha.version>1.6.2</easy-captcha.version>
<commons-lang3.version>3.8.1</commons-lang3.version>
<emoji-java.veision>4.0.0</emoji-java.veision>
<jsoup.veision>1.15.3</jsoup.veision>
<pinyin4j.veision>2.5.1</pinyin4j.veision>
<poi.version>4.1.2</poi.version>
<tencentcloud-sdk-java.version>4.0.11</tencentcloud-sdk-java.version>
<poitl.version>1.12.2</poitl.version>
<easypoi.version>4.5.0</easypoi.version>
<lombok.version>1.18.34</lombok.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>com.github.javen205</groupId>
<artifactId>IJPay-All</artifactId>
<version>2.9.10</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>dashscope-sdk-java</artifactId>
<version>2.14.4</version>
<exclusions>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.alipay.sdk</groupId>
<artifactId>alipay-sdk-java</artifactId>
<version>4.35.37.ALL</version>
</dependency>
<dependency>
<groupId>org.apache.axis</groupId>
<artifactId>axis</artifactId>
<version>1.4</version>
</dependency>
<dependency>
<groupId>org.apache.axis</groupId>
<artifactId>axis-ant</artifactId>
<version>1.4</version>
</dependency>
<dependency>
<groupId>javax.xml</groupId>
<artifactId>jaxrpc</artifactId>
<version>1.1</version>
</dependency>
<dependency>
<groupId>javax.xml.soap</groupId>
<artifactId>javax.xml.soap-api</artifactId>
<version>1.4.0</version>
</dependency>
<dependency>
<groupId>commons-discovery</groupId>
<artifactId>commons-discovery</artifactId>
<version>0.2</version>
</dependency>
<dependency>
<groupId>wsdl4j</groupId>
<artifactId>wsdl4j</artifactId>
<version>1.6.3</version>
</dependency>
<dependency>
<groupId>com.antherd</groupId>
<artifactId>sm-crypto</artifactId>
<version>0.3.2</version>
</dependency>
<dependency>
<groupId>com.googlecode.aviator</groupId>
<artifactId>aviator</artifactId>
<version>5.3.3</version>
</dependency>
<dependency>
<groupId>com.google.zxing</groupId>
<artifactId>javase</artifactId>
<version>3.3.0</version>
</dependency>
<!-- minio文件sdk -->
<dependency>
<groupId>io.minio</groupId>
<artifactId>minio</artifactId>
<version>8.2.2</version>
</dependency>
<dependency>
<groupId>javax.activation</groupId>
<artifactId>activation</artifactId>
<version>1.1.1</version>
</dependency>
<dependency>
<groupId>org.apache.pdfbox</groupId>
<artifactId>pdfbox</artifactId>
<version>3.0.3</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>4.0.2</version>
</dependency>
<dependency>
<groupId>com.tencentcloudapi</groupId>
<artifactId>tencentcloud-sdk-java</artifactId>
<version>${tencentcloud-sdk-java.version}</version>
<exclusions>
<exclusion>
<groupId>com.squareup.okio</groupId>
<artifactId>okio</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</dependency>
<dependency>
<groupId>org.nutz</groupId>
<artifactId>nutzboot-core</artifactId>
<version>${nutzboot.version}</version>
</dependency>
<dependency>
<groupId>org.nutz</groupId>
<artifactId>nutzboot-starter-nutz-mvc</artifactId>
<version>${nutzboot.version}</version>
</dependency>
<dependency>
<groupId>org.nutz</groupId>
<artifactId>nutzboot-starter-tomcat</artifactId>
<version>${nutzboot.version}</version>
<exclusions>
<exclusion>
<groupId>org.gandon.tomcat</groupId>
<artifactId>juli-to-slf4j</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-websocket</artifactId>
</dependency>
<dependency>
<groupId>org.nutz</groupId>
<artifactId>nutz-plugins-websocket</artifactId>
</dependency>
<dependency>
<groupId>org.nutz</groupId>
<artifactId>nutzboot-starter-nutz-dao</artifactId>
<version>${nutzboot.version}</version>
</dependency>
<dependency>
<groupId>org.nutz</groupId>
<artifactId>nutzboot-starter-jdbc</artifactId>
<version>${nutzboot.version}</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql-connector-java.version}</version>
</dependency>
<!-- <dependency> -->
<!-- <groupId>com.oracle</groupId> -->
<!-- <artifactId>classes12</artifactId> -->
<!-- <version>10.2.0.2.0</version> -->
<!-- <scope>system</scope> -->
<!-- <systemPath>D://oracle/product/10.2.0/db_1/jdbc/lib/classes12.jar</systemPath> -->
<!-- </dependency> -->
<dependency>
<groupId>org.nutz</groupId>
<artifactId>nutzboot-starter-redis</artifactId>
<version>${nutzboot.version}</version>
</dependency>
<dependency>
<groupId>org.nutz</groupId>
<artifactId>nutzboot-starter-wkcache</artifactId>
<version>${nutzboot.version}</version>
</dependency>
<dependency>
<groupId>org.nutz</groupId>
<artifactId>nutzboot-starter-weixin</artifactId>
<version>${nutzboot.version}</version>
</dependency>
<dependency>
<groupId>org.nutz</groupId>
<artifactId>nutzboot-starter-ftp</artifactId>
<version>${nutzboot.version}</version>
</dependency>
<dependency>
<groupId>org.nutz</groupId>
<artifactId>nutzboot-starter-beetl</artifactId>
<version>${nutzboot.version}</version>
</dependency>
<dependency>
<groupId>org.nutz</groupId>
<artifactId>nutzboot-starter-quartz</artifactId>
<version>${nutzboot.version}</version>
</dependency>
<dependency>
<groupId>com.vdurmont</groupId>
<artifactId>emoji-java</artifactId>
<version>${emoji-java.veision}</version>
</dependency>
<!-- api token web server -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<!-- sa-token -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-core</artifactId>
<version>${sa-token.version}</version>
</dependency>
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-servlet</artifactId>
<version>${sa-token.version}</version>
</dependency>
<!-- cas客户端 -->
<dependency>
<groupId>org.jasig.cas.client</groupId>
<artifactId>cas-client-core</artifactId>
<version>3.2.2</version>
</dependency>
<dependency>
<groupId>net.unicon.cas</groupId>
<artifactId>cas-client-autoconfig-support</artifactId>
<version>2.3.0-GA</version>
</dependency>
<!-- html -->
<dependency>
<groupId>org.jsoup</groupId>
<artifactId>jsoup</artifactId>
<version>${jsoup.veision}</version>
</dependency>
<dependency>
<groupId>com.belerweb</groupId>
<artifactId>pinyin4j</artifactId>
<version>${pinyin4j.veision}</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>${commons-lang3.version}</version>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.17.0</version>
</dependency>
<dependency>
<groupId>org.nutz</groupId>
<artifactId>nutzboot-starter-test-junit4</artifactId>
<version>${nutzboot.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>${slf4j.version}</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-core</artifactId>
<version>${logback.version}</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>${logback.version}</version>
</dependency>
<dependency>
<groupId>com.github.whvcse</groupId>
<artifactId>easy-captcha</artifactId>
<version>${easy-captcha.version}</version>
</dependency>
<dependency>
<groupId>org.openjdk.nashorn</groupId>
<artifactId>nashorn-core</artifactId>
<version>15.3</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.26</version>
</dependency>
<!-- <dependency> -->
<!-- <groupId>org.nutz</groupId> -->
<!-- <artifactId>nutz-plugins-validation</artifactId> -->
<!-- <version>1.r.69.v20220215</version> -->
<!-- </dependency> -->
<!-- <dependency>-->
<!-- <groupId>org.hibernate.validator</groupId>-->
<!-- <artifactId>hibernate-validator</artifactId>-->
<!-- <version>6.0.8.Final</version>-->
<!-- </dependency>-->
<dependency>
<groupId>org.glassfish</groupId>
<artifactId>javax.el</artifactId>
<version>3.0.1-b09</version>
</dependency>
<dependency>
<groupId>org.jboss.logging</groupId>
<artifactId>jboss-logging-annotations</artifactId>
<version>2.1.0.Final</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.8.1</version>
</dependency>
<dependency>
<groupId>com.fasterxml.uuid</groupId>
<artifactId>java-uuid-generator</artifactId>
<version>3.1.3</version>
</dependency>
<!-- <dependency> -->
<!-- <groupId>org.dromara.warm</groupId> -->
<!-- <artifactId>warm-flow-core</artifactId> -->
<!-- <version>1.7.7</version> -->
<!-- </dependency> -->
<!-- <dependency> -->
<!-- <groupId>org.dromara.warm</groupId> -->
<!-- <artifactId>warm-flow-plugin-modes-solon</artifactId> -->
<!-- <version>1.7.7</version> -->
<!-- </dependency> -->
<!-- <dependency> -->
<!-- <groupId>org.dromara.warm</groupId> -->
<!-- <artifactId>warm-flow-mybatis-sb-starter</artifactId> -->
<!-- <version>1.7.7</version> -->
<!-- </dependency> -->
<dependency>
<groupId>org.dromara.warm</groupId>
<artifactId>warm-flow-plugin-ui-core</artifactId>
<version>1.7.7</version>
</dependency>
<dependency>
<groupId>com.deepoove</groupId>
<artifactId>poi-tl</artifactId>
<version>${poitl.version}</version>
</dependency>
<dependency>
<groupId>cn.afterturn</groupId>
<artifactId>easypoi-base</artifactId>
<version>${easypoi.version}</version>
<exclusions>
<exclusion>
<artifactId>commons-lang3</artifactId>
<groupId>org.apache.commons</groupId>
</exclusion>
<exclusion>
<artifactId>poi</artifactId>
<groupId>org.apache.poi</groupId>
</exclusion>
<exclusion>
<artifactId>poi-ooxml</artifactId>
<groupId>org.apache.poi</groupId>
</exclusion>
<exclusion>
<artifactId>poi-ooxml-schemas</artifactId>
<groupId>org.apache.poi</groupId>
</exclusion>
<exclusion>
<artifactId>slf4j-api</artifactId>
<groupId>org.slf4j</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-openapi2-spring-boot-starter</artifactId>
<version>4.4.0</version>
</dependency>
<dependency>
<groupId>io.github.draco1023</groupId>
<artifactId>poi-tl-ext</artifactId>
<version>0.4.18-poi5</version>
</dependency>
<dependency>
<groupId>com.luhuiguo</groupId>
<artifactId>aspose-words</artifactId>
<version>23.1</version>
</dependency>
<!-- xlsx或xls转pdf -->
<dependency>
<groupId>com.luhuiguo</groupId>
<artifactId>aspose-cells</artifactId>
<version>23.1</version>
</dependency>
<!-- ppt转pdf -->
<dependency>
<groupId>com.luhuiguo</groupId>
<artifactId>aspose-slides</artifactId>
<version>23.1</version>
</dependency>
<dependency>
<groupId>commons-dbutils</groupId>
<artifactId>commons-dbutils</artifactId>
<version>1.7</version>
<!-- 或使用最新版本 -->
</dependency>
<dependency>
<groupId>de.odysseus.juel</groupId>
<artifactId>juel</artifactId>
<version>2.1.3</version>
</dependency>
<!-- <dependency>-->
<!-- <groupId>org.bytedeco</groupId>-->
<!-- <artifactId>ffmpeg-platform</artifactId>-->
<!-- <version>7.1.1-1.5.12</version>-->
<!-- </dependency>-->
</dependencies>
<dependencyManagement>
<dependencies>
<!-- Servlet API 由 Tomcat 提供,防止旧 API 混入可执行 JAR。 -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax.websocket</groupId>
<artifactId>javax.websocket-api</artifactId>
<version>1.1</version>
<scope>provided</scope>
</dependency>
<!-- 显式覆盖导入的 NutzBoot BOM,避免回退到其默认 Tomcat 8.5。 -->
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-core</artifactId>
<version>${tomcat.version}</version>
</dependency>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-websocket</artifactId>
<version>${tomcat.version}</version>
</dependency>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-el</artifactId>
<version>${tomcat.version}</version>
</dependency>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-jasper</artifactId>
<version>${tomcat.version}</version>
</dependency>
<dependency>
<groupId>org.nutz</groupId>
<artifactId>nutzboot-parent</artifactId>
<version>${nutzboot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
<version>${jaxb-api.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<finalName>${project.artifactId}</finalName>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>17</source>
<target>17</target>
<compilerArgs>
<arg>--add-opens</arg>
<arg>java.base/java.lang=ALL-UNNAMED</arg>
</compilerArgs>
<useIncrementalCompilation>false</useIncrementalCompilation>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId>
<version>3.0.1</version>
<configuration>
<encoding>UTF-8</encoding>
<charset>UTF-8</charset>
<docencoding>UTF-8</docencoding>
</configuration>
<executions>
<execution>
<id>attach-javadocs</id>
<goals>
<goal>jar</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-source-plugin</artifactId>
<version>3.0.1</version>
<configuration>
<attach>true</attach>
</configuration>
<executions>
<execution>
<phase>compile</phase>
<goals>
<goal>jar</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.22.0</version>
<configuration>
<skip>true</skip>
</configuration>
</plugin>
<plugin>
<groupId>org.nutz.boot</groupId>
<artifactId>nutzboot-maven-plugin</artifactId>
<version>${nutzboot.version}</version>
<configuration>
<mainClass>com.budwk.app.MainLauncher</mainClass>
</configuration>
</plugin>
</plugins>
<resources>
<resource>
<directory>src/main/resources</directory>
<includes>
<include>**/*</include>
</includes>
</resource>
</resources>
</build>
<repositories>
<repository>
<id>nutz</id>
<url>https://jfrog.nutz.cn/artifactory/libs-release</url>
</repository>
<repository>
<id>nutz-snapshots</id>
<url>https://jfrog.nutz.cn/artifactory/snapshots</url>
<snapshots>
<enabled>true</enabled>
<updatePolicy>always</updatePolicy>
</snapshots>
<releases>
<enabled>false</enabled>
</releases>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>nutz-snapshots</id>
<url>https://jfrog.nutz.cn/artifactory/snapshots</url>
<snapshots>
<enabled>true</enabled>
<updatePolicy>always</updatePolicy>
</snapshots>
<releases>
<enabled>false</enabled>
</releases>
</pluginRepository>
</pluginRepositories>
</project>
@@ -0,0 +1,177 @@
package com.budwk.app;
import cn.dev33.satoken.SaManager;
import cn.dev33.satoken.config.SaTokenConfig;
import com.budwk.app.base.utils.AppIocUtil;
import com.budwk.app.flow.engine.FlowEngine;
import com.budwk.app.sys.services.SysTaskService;
import com.budwk.app.task.services.TaskPlatformService;
import com.budwk.app.web.commons.auth.satoken.SaTokenContextImpl;
import com.budwk.app.web.commons.auth.satoken.SaTokenDaoRedisImpl;
import com.budwk.app.web.commons.auth.satoken.StpInterfaceImpl;
import com.budwk.app.web.commons.base.Globals;
import com.budwk.app.web.commons.ext.pubsub.WebPubSub;
import com.budwk.app.web.commons.ext.handler.WkTomcatConfiguration;
import com.budwk.app.zhgh.dayofficework.message.service.GlobalMessageSendService;
import com.budwk.app.zhgh.club.service.ClubPayRecordService;
import com.budwk.app.zhgh.club.service.SysClubUserService;
import lombok.extern.slf4j.Slf4j;
import org.beetl.core.GroupTemplate;
import org.nutz.boot.NbApp;
import org.nutz.dao.Dao;
import org.nutz.dao.impl.FileSqlManager;
import org.nutz.dao.sql.Sql;
import org.nutz.dao.util.Daos;
import org.nutz.integration.jedis.JedisAgent;
import org.nutz.ioc.Ioc;
import org.nutz.ioc.impl.PropertiesProxy;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.lang.Mirror;
import org.nutz.mvc.Mvcs;
import org.nutz.mvc.annotation.*;
import org.quartz.Scheduler;
import javax.management.MBeanServer;
import javax.management.ObjectName;
import javax.servlet.ServletContext;
import java.lang.management.ManagementFactory;
import java.sql.Driver;
import java.sql.DriverManager;
import java.util.Enumeration;
import java.util.List;
@IocBean(create = "init", depose = "depose")
@Localization(value = "locales/", defaultLocalizationKey = "zh_CN")
@Encoding(input = "UTF-8", output = "UTF-8")
@ChainBy(args = "chain/mvc-chain.json")
@Slf4j
public class MainLauncher {
protected static final String PRE = "security.";
@Inject("refer:$ioc")
private Ioc ioc;
@Inject
private PropertiesProxy conf;
@Inject
private JedisAgent jedisAgent;
@Inject
private WebPubSub webPubSub;//注入一下为了初始化
@Inject
private GroupTemplate groupTemplate;
@Inject
private TaskPlatformService taskPlatformService;
@Inject
private SysTaskService sysTaskService;
@Inject
private Dao dao;
public static void main(String[] args) throws Exception {
NbApp nb = new NbApp().setArgs(args).setPrintProcDoc(true);
nb.getAppContext().setMainPackage("com.budwk");
nb.addListener(new WkTomcatConfiguration());
nb.run();
}
public static NbApp warMain(ServletContext sc) {
NbApp nb = new NbApp().setPrintProcDoc(true);
nb.getAppContext().setMainPackage("com.budwk");
return nb;
}
public void init() {
Mvcs.DISABLE_X_POWERED_BY = true;
Globals.AppBase = Mvcs.getServletContext().getContextPath();
Globals.AppRoot = Mvcs.getServletContext().getRealPath("/");
init_sys();
// 2026年10月3日 12:01:28;操作人:wxg:先升级会员表及补齐历史资料,再处理缴费快照和启动计划任务;不依赖 DEBUG。
SysClubUserService members = ioc.get(SysClubUserService.class, "sysClubUserServiceImpl");
members.upgradeMemberSchema();
members.backfillMemberSnapshots();
// 2026年10月3日 09:05:37;操作人:wxg:缴费表升级独立于 DEBUG,先完成字段检查和旧数据补齐再启动任务。
// 2026年10月3日 09:28:37;操作人:wxg:按实际注册的实现类 Bean 名称获取服务,避免接口默认名称不存在导致启动失败。
ClubPayRecordService payRecords = ioc.get(ClubPayRecordService.class, "clubPayRecordServiceImpl");
payRecords.upgradePayRecordSchema();
payRecords.backfillPayRecordSnapshots();
init_task();
init_auth();
ioc.get(Globals.class);
ioc.get(FlowEngine.class);
ioc.get(GlobalMessageSendService.class);
AppIocUtil.setIoc(ioc);
}
public void init_auth() {
SaTokenConfig saTokenConfig = conf.makeDeep(SaTokenConfig.class, PRE);
String tokenName = conf.get(PRE + "tokenName", "token");
// 注意这里的token默认超时时间与控制中心 websocket 超时时间保持一致
saTokenConfig.setTimeout(conf.getLong(PRE + "timeout", 86400));
saTokenConfig.setTokenName(tokenName);
saTokenConfig.setIsV(false);
SaManager.setConfig(saTokenConfig);
SaManager.setSaTokenContext(ioc.get(SaTokenContextImpl.class));
SaManager.setSaTokenDao(ioc.get(SaTokenDaoRedisImpl.class));
SaManager.setStpInterface(ioc.get(StpInterfaceImpl.class));
}
private void init_task() {
if (log.isDebugEnabled() && !dao.exists("sys_qrtz_triggers") && !dao.exists("sys_qrtz_triggers".toUpperCase())) {
//执行Quartz SQL脚本
String dbType = dao.getJdbcExpert().getDatabaseType();
log.debug("dbType:::" + dbType);
FileSqlManager fmq = new FileSqlManager("db/quartz/" + dbType.toLowerCase() + ".sql");
List<Sql> sqlListq = fmq.createCombo(fmq.keys());
Sql[] sqlsq = sqlListq.toArray(new Sql[sqlListq.size()]);
for (Sql sql : sqlsq) {
dao.execute(sql);
}
}
}
private void init_sys() {
if (log.isDebugEnabled()) {
//通过POJO类创建表结构
try {
Daos.createTablesInPackage(dao, "com.budwk", false);
//通过POJO类修改表结构
Daos.migration(dao, "com.budwk", true, false);
// Daos.migration(dao, "com.budwk", true, false, false);
} catch (Exception e) {
e.printStackTrace();
}
}
}
public void depose() {
// 非mysql数据库,或多webapp共享mysql驱动的话,以下语句删掉
try {
Mirror.me(Class.forName("com.mysql.jdbc.AbandonedConnectionCleanupThread")).invoke(null, "shutdown");
} catch (Throwable e) {
}
// 解决quartz有时候无法停止的问题
try {
ioc.get(Scheduler.class).shutdown(true);
} catch (Exception e) {
}
// 解决com.alibaba.druid.proxy.DruidDriver和com.mysql.jdbc.Driver在reload时报warning的问题
// 多webapp共享mysql驱动的话,以下语句删掉
Enumeration<Driver> en = DriverManager.getDrivers();
while (en.hasMoreElements()) {
try {
Driver driver = en.nextElement();
String className = driver.getClass().getName();
log.debug("deregisterDriver: " + className);
DriverManager.deregisterDriver(driver);
} catch (Exception e) {
}
}
try {
MBeanServer mbeanServer = ManagementFactory.getPlatformMBeanServer();
ObjectName objectName = new ObjectName("com.alibaba.druid:type=MockDriver");
if (mbeanServer.isRegistered(objectName)) mbeanServer.unregisterMBean(objectName);
objectName = new ObjectName("com.alibaba.druid:type=DruidDriver");
if (mbeanServer.isRegistered(objectName)) mbeanServer.unregisterMBean(objectName);
} catch (Exception ex) {
}
}
}
@@ -0,0 +1,22 @@
package com.budwk.app.base.annotation;
import java.lang.annotation.*;
/**
* 字典枚举类注解,方便收集做为字典使用的
*/
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface DictEnum{
/**
* 名称
*/
String name();
/**
* 唯一标识
*/
String key();
}
@@ -0,0 +1,23 @@
package com.budwk.app.base.annotation;
import java.lang.annotation.*;
/**
* 自定义注解防止表单重复提交
*
* @author ruoyi
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RepeatSubmit {
/**
* 间隔时间(ms),小于此时间视为重复提交
*/
public int interval() default 5000;
/**
* 提示消息
*/
public String message() default "请求过于频繁,请稍后再试";
}
@@ -0,0 +1,41 @@
package com.budwk.app.base.annotation;
import java.lang.annotation.*;
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface SLog {
String type() default "platform";
/**
* 标签
*
* @return
*/
String tag();
String msg() default "";
/**
* 是否记录传递参数
*
* @return 消息模板
*/
boolean param() default true;
/**
* 记录执行结果
*
* @return 消息模板
*/
boolean result() default true;
/**
* 是否异步执行,默认为true
*
* @return true, 如果需要异步执行
*/
boolean async() default true;
}
@@ -0,0 +1,52 @@
package com.budwk.app.base.config;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.nutz.ioc.impl.PropertiesProxy;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
import java.util.HashMap;
import java.util.Map;
/**
* @version 1.0
* @Author zzr
* @name:DataCenterProperties
* @Date 2025/9/15 15:34
* @注释 数据中心配置文件,存放接口地址和参数配置
*/
@Slf4j
@Data
@IocBean(create = "init")
public class DataCenterProperties {
@Inject
private PropertiesProxy conf;
private String tokenUrl;
private String key;
private String secret;
private Map<String, String> urls = new HashMap<>();
public void init() {
Map<String, String> all = conf.toMap();
tokenUrl = all.get("data-center.token-url");
key = all.get("data-center.key");
secret = all.get("data-center.secret");
extract(all, "data-center.urls.", urls);
}
/**
* 扫描配置 把 prefix 开头的 key 变成 Map
* @param prefix 配置前缀
* @param target 目标值
*/
private void extract(Map<String, String> all, String prefix, Map<String, String> target) {
all.forEach((k, v) -> {
if (k.startsWith(prefix)) {
target.put(k.substring(prefix.length()), v);
}
});
}
}
@@ -0,0 +1,125 @@
package com.budwk.app.base.config;
import lombok.extern.slf4j.Slf4j;
import org.nutz.dao.Dao;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.lang.Lang;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.List;
import java.util.concurrent.ThreadPoolExecutor;
/**
* @version 1.0
* @Author zzr
* @name:ThreadPoolConfig
* @Date 2024/9/2 10:18
* @注释
*/
@Slf4j
@IocBean
public class ThreadPoolConfig {
@Inject
private Dao dao;
/**
* 核心线程数(默认线程数)
*/
private static final int CORE_POOL_SIZE = 2 * Runtime.getRuntime().availableProcessors() + 1;
/**
* 最大线程数
*/
private static final int MAX_POOL_SIZE = 128;
/**
* 允许线程空闲时间(单位:默认为秒)
*/
private static final int KEEP_ALIVE_TIME = 5;
/**
* 任务的等待时间
*/
private static final int AWAIT_TERMINATION_TIME = 30;
/**
* 缓冲队列数
*/
private static final int QUEUE_CAPACITY = 1200;
/**
* 线程池名前缀
*/
private static final String THREAD_NAME_PREFIX = "dd3s-thread-pool";
/**
* bean的名称,默认为首字母小写的方法名
* spring管理的线程池,顶级父类也是Executor
*/
@IocBean(name = "executorService")
public ThreadPoolTaskExecutor executorService() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(CORE_POOL_SIZE);
executor.setMaxPoolSize(MAX_POOL_SIZE);
executor.setQueueCapacity(QUEUE_CAPACITY);
executor.setKeepAliveSeconds(KEEP_ALIVE_TIME);
executor.setThreadNamePrefix(THREAD_NAME_PREFIX);
executor.setAwaitTerminationSeconds(AWAIT_TERMINATION_TIME);
executor.setWaitForTasksToCompleteOnShutdown(true);
// 线程池对拒绝任务的处理策略
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.initialize();
return executor;
}
/**
* 异步执行
*
* @param list 泛型list,任意实体类集合
* @param batchSize 期望单次操作的数量,默认200
* @param mode insert插入 ; update更新 ; insertOrUpdate插入或更新
* @param isFastInsertOrUpdateIgnoreNull mode为新增此值为true代表fastInsert;mode为修改时此值为true代表updateIgnoreNull
* @param <T>
*/
public <T> void asyncExecute(List<T> list, Integer batchSize, String mode, boolean isFastInsertOrUpdateIgnoreNull) {
if (Lang.isEmpty(list)) {
return;
}
if (batchSize == null || batchSize <= 0) {
batchSize = 200;
}
for (int i = 0; i < list.size(); i += batchSize) {
final int end = Math.min(i + batchSize, list.size());
List<T> batch = list.subList(i, end);
executorService().execute(() -> {
try {
switch(mode) {
case "insert" -> {
if (isFastInsertOrUpdateIgnoreNull) {
dao.fastInsert(batch);
} else {
dao.insert(batch);
}
}
case "update" -> {
if (isFastInsertOrUpdateIgnoreNull) {
dao.updateIgnoreNull(batch);
} else {
dao.update(batch);
}
}
case "insertOrUpdate" -> dao.insertOrUpdate(batch);
}
} catch (Exception e) {
// 日志记录异常信息
System.err.println("Error occurred while inserting batch: " + e.getMessage());
e.printStackTrace();
}
});
}
}
}
@@ -0,0 +1,51 @@
package com.budwk.app.base.constant;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 流程常量
*/
@Getter
@AllArgsConstructor
public enum BpmProcessConstant {
WARMTH_CONDOLENCE("送温暖慰问"),
LOVE_ASSIST("爱心扶助基金"),
PROPOSAL("提案"),
SUGGESTION_TC("教代会意见建议"),
SUGGESTION_WC("工代会意见建议"),
GRASSROOTS_CONGRESS("基层双代会"),
GRASSROOTS_CONGRESS_MATERIALS("基层双代会会议资料"),
MEMBER_SINGLE_APPLY("会员入会申请"),
MEMBER_CHANGE("会员入会申请"),
MEMBER_BRANCH_UNION_CHANGE("会员分工会变更"),
CLUB_JOIN("社团入会申请"),
CLUB_EXAMINE("社团考核"),
CLUB_EVALUATE("社团评优"),
CLUB_REGISTER("社团注册"),
PERSON_EVALUATE("评优评先申请"),
ARTICLE("新闻投稿"),
BRAND_STUDIO("品牌工作室"),
BRANCH_UNION_WEIYUAN_AUTHORIZATION("分工会委员授权")
;
public final String description;
}
@@ -0,0 +1,17 @@
package com.budwk.app.base.constant;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
public enum ProcessBusinessConstant {
CONDOLENCE_APPLY("慰问");
/**
* 避免和枚举类的name冲突
*/
public final String roleName;
}
@@ -0,0 +1,88 @@
package com.budwk.app.base.constant;
/**
* @author wizzer@qq.com
*/
public class RedisConstant {
//项目统一前缀
public final static String PLATFORM_REDIS_PREFIX = "budwk5mini:";
public final static String PLATFORM_REDIS_WKCACHE_PREFIX = PLATFORM_REDIS_PREFIX + "wkcache:";
//聊天室
public final static String REDIS_KEY_WSROOM = PLATFORM_REDIS_PREFIX + "wsroom:";
public final static String REDIS_KEY_LOGIN_ADMIN_CAPTCHA = PLATFORM_REDIS_PREFIX + "admin:login:captcha:";
public final static String REDIS_KEY_ADMIN_PUBSUB = PLATFORM_REDIS_PREFIX + "admin:pubsub:";
//微信token
public final static String REDIS_KEY_WX_TOKEN = PLATFORM_REDIS_PREFIX + "wx:token:";
//企业微信token
public final static String REDIS_KEY_WX_WORK_TOKEN = PLATFORM_REDIS_PREFIX + "wxwork:token:";
//健步走小程序TOKEN
public final static String REDIS_KEY_WE_APP_ACCESS_TOKEN = PLATFORM_REDIS_PREFIX + "weapp:token:";
//图形验证码
public final static String REDIS_CAPTCHA_KEY = PLATFORM_REDIS_PREFIX + "platfrom:captcha:";
//短信验证码
public final static String REDIS_SMSCODE_KEY = PLATFORM_REDIS_PREFIX + "platfrom:smscode:";
public final static String REDIS_KEY_API_SIGN_DEPLOY_NONCE = PLATFORM_REDIS_PREFIX + "api:sign:deploy:nonce:";
public final static String REDIS_KEY_API_SIGN_OPEN_NONCE = PLATFORM_REDIS_PREFIX + "api:sign:open:nonce:";
/**
* Token 缓存前缀
*/
public static final String TOKEN = PLATFORM_REDIS_PREFIX + "token:";
/**
* 验证码前缀
*/
public static final String UCENTER_CAPTCHA = PLATFORM_REDIS_PREFIX + "ucenter:captcha:";
/**
* 短信验证码前缀
*/
public static final String UCENTER_SMSCODE = PLATFORM_REDIS_PREFIX + "ucenter:smscode:";
/** 拼图挑战与凭证按匿名会话隔离,频率键单独存放。 */
public static final String REDIS_SLIDER_CAPTCHA_KEY = PLATFORM_REDIS_PREFIX + "login:slider:";
public static final String REDIS_SLIDER_RATE_KEY = PLATFORM_REDIS_PREFIX + "login:slider-rate:";
/** 登录短信专用缓存前缀,避免验证码与其他业务场景交叉使用。 */
public static final String LOGIN_SMSCODE = PLATFORM_REDIS_PREFIX + "login:sms:";
/**
* 接口重复提交前缀
*/
public static final String REPEAT_SUBMIT_PREFIX = PLATFORM_REDIS_PREFIX + "repeat_submit:";
/**
* 签名前缀
*/
public static final String SIGNATURE_PREFIX = PLATFORM_REDIS_PREFIX + "sys:signature:";
/**
* 用户登录锁前缀
*/
public static final String USER_LOGIN_LOCK_PREFIX = PLATFORM_REDIS_PREFIX + "user:login:lock:";
/**
* 数据接口锁前缀
*/
public static final String DATE_CENTER_PREFIX = PLATFORM_REDIS_PREFIX + "datacenter:token:";
/**
* 数据接口锁前缀
*/
public static final String RSA_KEY_PREFIX = PLATFORM_REDIS_PREFIX + "rsa:private:";
/**
* 人员治理第三方数据查询结果缓存前缀。
*/
public static final String REDIS_KEY_PERSON_API_DATA_QUERY_RESULT =
PLATFORM_REDIS_PREFIX + "personGovernance:dataQuery:";
}
@@ -0,0 +1,116 @@
package com.budwk.app.base.constant;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 角色常量
*/
@Getter
@AllArgsConstructor
public enum
RoleConstant {
PUBLIC("公共角色"),
MEMBER("会员角色"),
SYSADMIN("系统管理员"),
UNION_COMMITTEE_MEMBER("工代会委员"),
SCHOOL_UNION_ADMIN("校工会管理员"),
SCHOOL_UNION_CHAIRMAN("校工会主席"),
SCHOOL_UNION_VICE_CHAIRMAN("校工会副主席"),
SCHOOL_UNION_WELFARE_ADMIN("校工会福利管理员"),
SCHOOL_UNION_MEMBER_ADMIN("校工会会员管理员"),
SCHOOL_UNION_TC_ADMIN("校工会教代会管理员"),
SCHOOL_UNION_WC_ADMIN("校工会工代会管理员"),
SCHOOL_UNION_DC_ADMIN("校工会民主管理员"),
SCHOOL_UNION_PROPOSAL_ADMIN("校工会提案管理员"),
SCHOOL_UNION_CLUB_ADMIN("校工会社团管理员"),
SCHOOL_UNION_ACTIVITY_ADMIN("校工会活动管理员"),
/** 品牌活动统计、人员调整管理员,数据范围按 trainSignUp 子活动负责人关系控制。 */
SCHOOL_UNION_TRAIN_SIGN_UP_ADMIN("校工会品牌活动管理员"),
SCHOOL_UNION_ARTICLE_CLUB_ADMIN("校工会社团新闻审批管理员"),
SCHOOL_UNION_ARTICLE_FGH_ADMIN("校工会分工会新闻审批管理员"),
SCHOOL_UNION_ARTICLE_ADMIN("校工会新闻审批管理员"),
SCHOOL_OUTLAY_ADMIN("校工会经费管理员"),
SCHOOL_PUBLICITY_AND_CULTURAL("校宣传与文体办公室负责人"),
BRANCH_UNION_ADMIN("分工会管理员"),
BRANCH_UNION_CHAIRMAN("分工会主席"),
BRANCH_UNION_VICE_CHAIRMAN("分工会副主席"),
BRANCH_UNION_OPERATOR("分工会操作员"),
BRANCH_UNION_GROUP_LEADER("工会小组组长"),
BRANCH_UNION_ARTICLE_WRITER("分工会新闻投稿员"),
BRANCH_UNION_WY("分工会委员"),
BRANCH_UNION_ZUZHI_WY("分工会组织委员"),
BRANCH_UNION_XUANCHUAN_WY("分工会宣传委员"),
BRANCH_UNION_NVGONG_WY("分工会女工委员"),
BRANCH_UNION_QINGNIAN_WY("分工会青年委员"),
BRANCH_UNION_WENTI_WY("分工会文体委员"),
BRANCH_UNION_SHENGGHUO_WY("分工会生活委员"),
BRANCH_UNION_TIAOJIE_WY("分工会调解委员"),
BRANCH_UNION_WENTI_SPORTS("分工会文体福利委员"),
UNIT_PARTY_SECRETARY("单位党委书记"),
TEACHER_CONGRESS_DELEGATE_FORMAL("教代会正式代表"),
TEACHER_CONGRESS_DELEGATE_ATTENDANCE("教代会列席代表"),
TEACHER_CONGRESS_DELEGATE_SPECIALLY_INVITE("教代会特邀代表"),
TEACHER_CONGRESS_DELEGATION_HEAD("教代会代表团团长"),
TEACHER_CONGRESS_VICE_DELEGATION_HEAD("教代会代表团副团长"),
TEACHER_CONGRESS_DELEGATION_CONTACT("教代会代表团联络人"),
TEACHER_CONGRESS_DELEGATION_QUALIFICATION("教代会代表资格审查员"),
TEACHER_CONGRESS_PREPARATION_LEADERSHIP_GROUP_DIRECTOR("筹备领导小组主任"),
TEACHER_CONGRESS_PREPARATION_LEADERSHIP_GROUP_MEMBER("筹备领导小组成员"),
PROPOSAL_COMMITTEE_DIRECTOR("提案委员会主任"),
PROPOSAL_COMMITTEE_DEPUTY_DIRECTOR("提案委员会副主任"),
PROPOSAL_BRANCH_SCHOOL_LEADER("提案分管校领导"),
PROPOSAL_UNIT_LEADER("提案承办单位领导"),
PROPOSAL_UNIT_PROXY("提案承办单位代理答复人"),
WORKER_CONGRESS_DELEGATE_FORMAL("工代会正式代表"),
WORKER_CONGRESS_DELEGATE_ATTENDANCE("工代会列席代表"),
WORKER_CONGRESS_DELEGATE_SPECIALLY_INVITE("工代会特邀代表"),
WORKER_CONGRESS_DELEGATION_HEAD("工代会代表团团长"),
SCHOOL_UNION_WC_AUDIT_ADMIN("基层双代会校工会批复人"),
SCHOOL_HOSPITAL_LEADER("校医院负责人"),
/*社团模块*/
CLUB_MEMBER("社团会员"),
CLUB_MANAGER("社团负责人"),
CLUB_PRESIDENT("社团会长"),
CLUB_VICE_PRESIDENT("社团副会长"),
CLUB_SECRETARY("社团秘书长"),
CLUB_VICE_SECRETARY("社团副秘书长"),
// SCHOOL_UNION_CLUB_ADMIN("校工会社团管理员"),
CLUB_AUDIT_LEADER("社团分管领导"),
CLUB_OPERATOR("社团操作员"),
/*大病基金会员*/
AID_FUND_MEMBER("大病基金会员"),
RETIREMENT_WORKPLACE("离退休工作处"),
/*爱心扶助基金*/
LOVE_ASSIST_WORK_GROUP_LEADER("爱心扶助小组组长"),
LOVE_ASSIST_WORK_GROUP_MEMBER("爱心扶助小组审批员"),
PSYCHOLOGY_DOCTOR("心理咨询师"),
LEGAL_DOCTOR("法律顾问"),
UNION_REIMBURSEMENT_MANAGER("分工会报销负责人"),
CLUB_REIMBURSEMENT_MANAGER("社团报销负责人"),
SCHOOL_REIMBURSEMENT_MANAGER("校工会报销负责人"),
;
/**
* 避免和枚举类的name冲突
*/
public final String roleName;
}
@@ -0,0 +1,44 @@
package com.budwk.app.base.dao;
import cn.hutool.core.util.StrUtil;
import com.budwk.app.base.param.PageForm;
import org.nutz.dao.Cnd;
import org.nutz.lang.Lang;
import org.nutz.lang.util.NutMap;
/**
* 条件构造辅助类。
* 当前健步走移植代码需要按页面查询条件动态追加字段查询和排序,空字段不应参与 SQL 条件。
*/
public class CndPlus extends Cnd {
private static final NutMap ORDER_MAP = NutMap.NEW().addv("ascending", "asc").addv("descending", "desc");
public static CndPlus create() {
return new CndPlus();
}
public CndPlus and(PageForm pageForm) {
if (pageForm != null && StrUtil.isAllNotBlank(pageForm.getSearchName(), pageForm.getSearchKeyword())) {
this.where().andLike(pageForm.getSearchName(), pageForm.getSearchKeyword());
}
if (pageForm != null && StrUtil.isAllNotBlank(pageForm.getPageOrderName(), pageForm.getPageOrderBy())) {
this.orderBy(pageForm.getPageOrderName(), ORDER_MAP.getString(pageForm.getPageOrderBy()));
}
return this;
}
/**
* 仅在字段名和值都有效时追加条件,避免可选筛选项为空时生成无效 SQL。
*
* @param name 字段名或表达式
* @param op 比较操作符
* @param value 比较值
* @return 当前条件对象
*/
public CndPlus andEX(String name, String op, Object value) {
if (StrUtil.isNotBlank(name) && !Lang.isEmpty(value)) {
this.and(name, op, value);
}
return this;
}
}
@@ -0,0 +1,15 @@
package com.budwk.app.base.enums;
/**
* 浏览器平台
*/
public enum BrowserPlatform {
PC_WEB,
H5_WEB,
WEI_XIN,
QI_YE_WEI_XIN;
}
@@ -0,0 +1,35 @@
package com.budwk.app.base.enums;
import cn.hutool.core.convert.Convert;
import java.util.Arrays;
import java.util.Optional;
public interface CodedEnum {
Integer getCode();
String getMessage();
public static <E extends Enum<?> & CodedEnum> Optional<E> codeOf(Class<E> enumClass, Object code) {
try {
return Arrays.stream(enumClass.getEnumConstants()).filter(e ->
e.getCode().equals(Convert.toInt(code))
|| e.name().equalsIgnoreCase(Convert.toStr(code))
|| e.getMessage().equalsIgnoreCase(Convert.toStr(code))
).findAny();
} catch (Exception e) {
return Optional.empty();
}
}
/**
* 获取数据类型,用于转换String或Integer
*
* @return
*/
default Class<?> getDataType() {
return Integer.class;
}
}
@@ -0,0 +1,22 @@
package com.budwk.app.base.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 登录类型
*/
@Getter
@AllArgsConstructor
public enum LoginType {
QI_YE_WECHAT("企业微信"),
WECHAT("微信"),
PC_LOCAL("PC本地"),
SMS_CODE("手机验证码"),
MOBILE_LOCAL("手机本地"),
CAS("CAS");
private final String value;
}
@@ -0,0 +1,21 @@
package com.budwk.app.base.enums;
/**
* 文件存储桶的权限策略枚举
*/
public enum MinIoFileBucketAuthEnum {
/**
* 私有的(仅有 owner 可以读写)
*/
PRIVATE,
/**
* 公有读,私有写( owner 可以读写, 其他客户可以读)
*/
PUBLIC_READ,
/**
* 公共读写(即所有人都可以读写,慎用)
*/
PUBLIC_READ_WRITE
}
@@ -0,0 +1,18 @@
package com.budwk.app.base.enums;
import lombok.Data;
import java.util.List;
/**
* @author mldong
* @date 2023/9/27
*/
@Data
//@ApiModel(value = "UpAndDownParam对象", description = "启用/禁用实体")
public class UpAndDownParam {
// @ApiModelProperty(value = "id集合")
private List<Object> ids;
// @ApiModelProperty(value = "操作类型")
private YesNoEnum opType;
}
@@ -0,0 +1,52 @@
package com.budwk.app.base.enums;
import com.budwk.app.base.annotation.DictEnum;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonValue;
/**
* yes_no
*
* @author mldong
*/
@DictEnum(key = "yes_no", name = "是否")
public enum YesNoEnum implements CodedEnum {
/**
* 是
*/
YES(1, "是"),
/**
* 否
*/
NO(0, "否");
private Integer code;
private String message;
/**
* 未删除
*/
public final static int Y = 1;
/**
* 已删除
*/
public final static int N = 0;
@JsonCreator
public static YesNoEnum forValue(Object value) {
return CodedEnum.codeOf(YesNoEnum.class, value).get();
}
YesNoEnum(int value, String name) {
this.code = value;
this.message = name;
}
@JsonValue
public Integer getCode() {
return code;
}
public String getMessage() {
return message;
}
}
@@ -0,0 +1,14 @@
package com.budwk.app.base.event.flow.jumptofirsttask;
/**
* @version 1.0
* @Author FKY
* @name:FlowJumpToFirstTaskNodeListener
* @Date 2025/12/20 14:10
* @注释
*/
public interface FlowJumpToFirstTaskNodeListener {
void receive(Long processTaskId);
}
@@ -0,0 +1,25 @@
package com.budwk.app.base.event.flow.jumptofirsttask;
import com.budwk.app.base.event.user.UserChangeEventListener;
import com.budwk.app.base.utils.AppIocUtil;
import org.nutz.ioc.Ioc;
import org.nutz.mvc.Mvcs;
/**
* @version 1.0
* @Author FKY
* @name:FlowJumpToFirstTaskNodeEvent
* @Date 2025/12/20 14:09
* @注释
*/
public class FlowJumpToFirstTaskNodePublisher {
public static void broadcast(Long processTaskId) {
String[] names = Mvcs.getIoc().getNamesByType(FlowJumpToFirstTaskNodeListener.class);
for (String listener : names) {
FlowJumpToFirstTaskNodeListener listenerBean = Mvcs.getIoc().get(FlowJumpToFirstTaskNodeListener.class, listener);
listenerBean.receive(processTaskId);
}
}
}
@@ -0,0 +1,13 @@
package com.budwk.app.base.event.role;
/**
* @version 1.0
* @Author zzr
* @name:RoleEventListener
* @Date 2025/9/3 17:13
* @注释
*/
public interface RoleEventListener {
void receive(RoleEventMsg message);
}
@@ -0,0 +1,68 @@
package com.budwk.app.base.event.role;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
/**
* @version 1.0
* @Author zzr
* @name:RoleEventMsg
* @Date 2025/9/3 17:14
* @注释
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class RoleEventMsg {
/**
* 用户id
*/
private List<String> userIds;
/**
* 角色code
*/
private String roleCode;
/**
* 单位id
*/
private String unitId;
/**
* 操作类型
*/
private Integer operationType;
/**
* 添加角色
*/
public static final int ADD_ROLE = 1;
/**
* 删除角色
*/
public static final int REMOVE_ROLE = 2;
/**
* 更新角色
*/
public static final int RENEW_ROLE = 3;
public RoleEventMsg(String unitId, String roleCode, Integer operationType){
this.unitId = unitId;
this.roleCode = roleCode;
this.operationType = operationType;
}
public RoleEventMsg(List<String> userIds, String roleCode, Integer operationType){
this.userIds = userIds;
this.roleCode = roleCode;
this.operationType = operationType;
}
}
@@ -0,0 +1,21 @@
package com.budwk.app.base.event.role;
import org.nutz.mvc.Mvcs;
/**
* @version 1.0
* @Author zzr
* @name:RoleEventPublisher
* @Date 2025/9/3 17:14
* @注释
*/
public class RoleEventPublisher {
public static void broadcast(RoleEventMsg event){
String[] names = Mvcs.getIoc().getNamesByType(RoleEventListener.class);
for (String name : names) {
RoleEventListener listener = Mvcs.getIoc().get(RoleEventListener.class, name);
listener.receive(event);
}
}
}
@@ -0,0 +1,17 @@
package com.budwk.app.base.event.user;
import com.budwk.app.zhgh.staffmanage.member.constant.MemberChangeType;
import lombok.Builder;
import lombok.Data;
@Data
@Builder
public class SysUserEvent {
//变更类型
private MemberChangeType memberChangeType;
//userId
private String userId;
}
@@ -0,0 +1,7 @@
package com.budwk.app.base.event.user;
public interface SysUserEventListener {
void onEvent(SysUserEvent event);
}
@@ -0,0 +1,14 @@
package com.budwk.app.base.event.user;
import org.nutz.mvc.Mvcs;
public class SysUserPublisher {
public static void notify(SysUserEvent event){
String[] names = Mvcs.getIoc().getNamesByType(SysUserEventListener.class);
for (String name : names) {
SysUserEventListener listener = Mvcs.getIoc().get(SysUserEventListener.class, name);
listener.onEvent(event);
}
}
}
@@ -0,0 +1,13 @@
package com.budwk.app.base.event.user;
/**
* @version 1.0
* @Author zzr
* @name:UserChangeEventListener
* @Date 2025/8/13 14:18
* @注释
*/
public interface UserChangeEventListener {
void receive(UserChangeMsg message);
}
@@ -0,0 +1,70 @@
package com.budwk.app.base.event.user;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
/**
* @version 1.0
* @Author zzr
* @name:UserChangeMsg
* @Date 2025/8/13 11:51
* @注释
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class UserChangeMsg {
/**
* 变更资源,用户Id
*/
private List<String> userIds;
/**
* 操作类型
*/
private Integer operationType;
/**
* 变更前的单位Id,当operationType值为1时,该字段有效
*/
private String sourceUnitId;
/**
* 变更后的单位Id,当operationType值为1时,该字段有效
*/
private String targetUnitId;
/**
* 单位变动
*/
public final static int UNIT_CHANGE_OPERATION = 1;
/**
* 退休
*/
public final static int RETIRE_OPERATION = 2;
/**
* 入会
*/
public final static int RESTORE_OPERATION = 3;
public UserChangeMsg(List<String> userIds, Integer operationType) {
super();
this.userIds = userIds;
this.operationType = operationType;
}
public UserChangeMsg(List<String> userIds, Integer operationType, String sourceUnitId) {
super();
this.userIds = userIds;
this.operationType = operationType;
this.sourceUnitId = sourceUnitId;
}
}
@@ -0,0 +1,25 @@
package com.budwk.app.base.event.user;
import com.budwk.app.base.utils.AppIocUtil;
import org.nutz.ioc.Ioc;
/**
* @version 1.0
* @Author zzr
* @name:UserChangePublisher
* @Date 2025/8/13 14:23
* @注释
*/
public class UserChangePublisher {
// 发送消息给所有订阅者
public static void broadcast(UserChangeMsg msg) {
Ioc ioc = AppIocUtil.get();
String[] names = ioc.getNamesByType(UserChangeEventListener.class);
for (String listener : names) {
UserChangeEventListener listenerBean = ioc.get(UserChangeEventListener.class, listener);
listenerBean.receive(msg);
}
}
}
@@ -0,0 +1,19 @@
package com.budwk.app.base.exception;
import cn.hutool.core.util.StrUtil;
/**
* @author wizzer@qq.com
*/
public class BaseException extends RuntimeException{
private static final long serialVersionUID = 7192152812384031563L;
public BaseException(String message) {
super(message);
}
public BaseException(String message, Object... arguments) {
super(StrUtil.format(message, arguments));
}
}
@@ -0,0 +1,18 @@
package com.budwk.app.base.exception;
import cn.hutool.core.util.StrUtil;
/**
* 方法参数校验异常
*/
public class MethodArgumentNotValidException extends RuntimeException{
public MethodArgumentNotValidException(String message) {
super(message);
}
public MethodArgumentNotValidException(String message, Object... arguments) {
super(StrUtil.format(message, arguments));
}
}
@@ -0,0 +1,8 @@
package com.budwk.app.base.exception;
public class UnknownAccountException extends BaseException{
public UnknownAccountException(String message) {
super(message);
}
}
@@ -0,0 +1,19 @@
package com.budwk.app.base.interceptor.repeatSubmit;
import com.budwk.app.base.annotation.RepeatSubmit;
import org.nutz.aop.MethodInterceptor;
import org.nutz.ioc.Ioc;
import org.nutz.ioc.aop.SimpleAopMaker;
import org.nutz.ioc.loader.annotation.IocBean;
import java.lang.reflect.Method;
import java.util.List;
@IocBean(name = "$aop_repeatSubmit")
public class RepeatSubmitAopConfiguration extends SimpleAopMaker<RepeatSubmit> {
@Override
public List<? extends MethodInterceptor> makeIt(RepeatSubmit repeatSubmit, Method method, Ioc ioc) {
return List.of(new RepeatSubmitAopInterceptor(ioc, repeatSubmit, method));
}
}
@@ -0,0 +1,51 @@
package com.budwk.app.base.interceptor.repeatSubmit;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.json.JSONUtil;
import com.budwk.app.base.annotation.RepeatSubmit;
import com.budwk.app.base.constant.RedisConstant;
import com.budwk.app.base.exception.BaseException;
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
import org.nutz.aop.InterceptorChain;
import org.nutz.aop.MethodInterceptor;
import org.nutz.integration.jedis.RedisService;
import org.nutz.ioc.Ioc;
import org.nutz.mvc.Mvcs;
import java.lang.reflect.Method;
public class RepeatSubmitAopInterceptor implements MethodInterceptor {
protected Ioc ioc;
public RepeatSubmitAopInterceptor(Ioc ioc, RepeatSubmit repeatSubmit, Method method) {
RedisService redisService = ioc.get(RedisService.class);
}
@Override
public void filter(InterceptorChain chain) throws Throwable {
Method method = chain.getCallingMethod();
RepeatSubmit annotation = method.getAnnotation(RepeatSubmit.class);
int interval = annotation.interval();
RedisService redisService = Mvcs.getIoc().get(RedisService.class);
String source = method.getDeclaringClass().getName() + "#" + method.getName();
Object[] args = chain.getArgs();
String parameterJson = "";
if (ObjectUtil.isNotEmpty(args)) {
parameterJson = JSONUtil.toJsonStr(args); // 序列化实际参数
}
String key = RedisConstant.REPEAT_SUBMIT_PREFIX + SecurityUtil.getUserId() + ":" + source;
Boolean exists = redisService.exists(key);
if (exists) {
// 判断
String oldParameterJson = redisService.get(key);
if (oldParameterJson != null && oldParameterJson.equalsIgnoreCase(parameterJson)) {
throw new BaseException(annotation.message());
}
} else {
redisService.setex(key, interval / 1000, parameterJson);
}
chain.doChain();
}
}
@@ -0,0 +1,34 @@
package com.budwk.app.base.interceptor.sLog;
import com.budwk.app.base.annotation.SLog;
import org.nutz.aop.MethodInterceptor;
import org.nutz.ioc.Ioc;
import org.nutz.ioc.aop.SimpleAopMaker;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.List;
/**
* Created by wizzer on 2016/6/22.
*/
@IocBean(name = "$aop_syslog")
public class SLogAopConfigration extends SimpleAopMaker<SLog> {
@Inject("refer:$ioc")
protected Ioc ioc;
public List<? extends MethodInterceptor> makeIt(SLog slog, Method method, Ioc ioc) {
return Arrays.asList(new SLogAopInterceptor(ioc, slog, method));
}
public String[] getName() {
return new String[0];
}
public boolean has(String name) {
return false;
}
}
@@ -0,0 +1,88 @@
package com.budwk.app.base.interceptor.sLog;
import com.budwk.app.base.annotation.SLog;
import org.nutz.aop.InterceptorChain;
import org.nutz.aop.MethodInterceptor;
import org.nutz.el.El;
import org.nutz.ioc.Ioc;
import org.nutz.lang.segment.CharSegment;
import org.nutz.log.Log;
import org.nutz.log.Logs;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
/**
* Created by wizzer on 2016/6/22.
*/
public class SLogAopInterceptor implements MethodInterceptor {
private static final Log log = Logs.get();
protected SLogService sLogService;
protected String source;
protected String type;
protected String tag;
protected CharSegment seg;
protected boolean param;
protected boolean result;
protected boolean async;
protected Map<String, El> els;
protected Ioc ioc;
public SLogAopInterceptor(Ioc ioc, SLog slog, Method method) {
this.seg = new CharSegment(slog.msg());
if (seg.hasKey()) {
els = new HashMap<String, El>();
for (String key : seg.keys()) {
els.put(key, new El(key));
}
}
this.param = slog.param();
this.result = slog.result();
this.ioc = ioc;
this.source = method.getDeclaringClass().getName() + "#" + method.getName();
this.tag = slog.tag();
SLog _s = method.getDeclaringClass().getAnnotation(SLog.class);
if (_s != null) {
this.tag = _s.tag() + "," + this.tag;
}
this.type = slog.type();
this.async = slog.async();
}
public void filter(InterceptorChain chain) throws Throwable {
try {
chain.doChain();
doLog("aop.after", seg, chain, null);
} catch (Throwable e) {
doLog("aop.error", seg, chain, e);
throw e;
}
}
protected void doLog(String t, CharSegment seg, InterceptorChain chain, Throwable e) {
if (sLogService == null)
sLogService = ioc.get(SLogService.class);
try {
sLogService.log(t,
type,
tag,
source,
seg,
els,
param,
result,
async,
chain.getArgs(),
chain.getReturn(),
chain.getCallingMethod(),
chain.getCallingObj(),
e);
} catch (Exception e1) {
log.debug("slog fail", e1);
}
}
}
@@ -0,0 +1,230 @@
package com.budwk.app.base.interceptor.sLog;
import com.budwk.app.sys.models.Sys_log;
import com.budwk.app.sys.services.SysLogService;
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
import org.nutz.Nutz;
import org.nutz.el.El;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.json.Json;
import org.nutz.lang.Lang;
import org.nutz.lang.segment.CharSegment;
import org.nutz.lang.util.ClassMetaReader;
import org.nutz.lang.util.Context;
import org.nutz.lang.util.MethodParamNamesScaner;
import org.nutz.log.Log;
import org.nutz.log.Logs;
import org.nutz.mvc.Mvcs;
import java.io.IOException;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
/**
* Created by wizzer on 2016/6/22.
*/
@IocBean(create = "init", depose = "close")
public class SLogService implements Runnable {
private static final Log log = Logs.get();
ExecutorService es;
LinkedBlockingQueue<Sys_log> queue;
@Inject
protected SysLogService sysLogService;
/**
* 异步插入日志
*
* @param syslog 日志对象
*/
public void async(Sys_log syslog) {
LinkedBlockingQueue<Sys_log> queue = this.queue;
if (queue != null)
try {
boolean re = queue.offer(syslog, 50, TimeUnit.MILLISECONDS);
if (!re) {
log.info("syslog queue is full, drop it ...");
}
} catch (InterruptedException e) {
}
}
/**
* 同步插入日志
*
* @param syslog 日志对象
*/
public void sync(Sys_log syslog) {
try {
sysLogService.fastInsertSysLog(syslog);
} catch (Throwable e) {
log.info("insert syslog sync fail", e);
}
}
public void run() {
while (true) {
LinkedBlockingQueue<Sys_log> queue = this.queue;
if (queue == null)
break;
try {
Sys_log sysLog = queue.poll(1, TimeUnit.SECONDS);
if (sysLog != null) {
sync(sysLog);
}
} catch (InterruptedException e) {
break;
}
}
}
/**
* 本方法通常由aop拦截器调用.
*
* @param t 日志类型
* @param tag 标签
* @param source 源码位置
* @param els 消息模板的EL表达式预处理表
* @param param 是否异步插入
* @param result 是否异步插入
* @param async 是否异步插入
* @param args 方法参数
* @param re 方法返回值
* @param method 方法实例
* @param obj 被拦截的对象
* @param e 异常对象
*/
public void log(String t, String type, String tag, String source, CharSegment seg,
Map<String, El> els, boolean param, boolean result,
boolean async,
Object[] args, Object re, Method method, Object obj,
Throwable e) {
String _msg = null;
if (seg.hasKey()) {
Context ctx = Lang.context();
List<String> names = null;
if (Nutz.majorVersion() == 1 && Nutz.minorVersion() < 60) {
Class<?> klass = obj.getClass();
if (klass.getName().endsWith("$$NUTZAOP"))
klass = klass.getSuperclass();
String key = klass.getName();
if (caches.containsKey(key))
names = caches.get(key).get(ClassMetaReader.getKey(method));
else {
try {
Map<String, List<String>> tmp = MethodParamNamesScaner.getParamNames(klass);
names = tmp.get(ClassMetaReader.getKey(method));
caches.put(key, tmp);
} catch (IOException e1) {
log.debug("error when reading param name");
}
}
} else {
names = MethodParamNamesScaner.getParamNames(method);
}
if (names != null) {
for (int i = 0; i < names.size() && i < args.length; i++) {
ctx.set(names.get(i), args[i]);
}
}
ctx.set("obj", obj);
ctx.set("args", args);
ctx.set("re", re);
ctx.set("return", re);
ctx.set("req", Mvcs.getReq());
ctx.set("resp", Mvcs.getResp());
Context _ctx = Lang.context();
for (String key : seg.keys()) {
_ctx.set(key, els.get(key).eval(ctx));
}
_msg = seg.render(_ctx).toString();
} else {
_msg = seg.getOrginalString();
}
String _param = "";
String _result = "";
if (param && args != null) {
try {
_param = Json.toJson(args);
} catch (Exception e1) {
_param = "传参不能转换为JSON格式";
}
}
if (result && re != null) {
try {
_result = Json.toJson(re);
} catch (Exception e1) {
_param = "返回对象不能转换为JSON格式";
}
}
log(type, tag, source, _msg, async, _param, _result);
}
public void log(String type, String tag, String source, String msg, boolean async, String param, String result) {
Sys_log slog = makeLog(type, tag, source, msg, param, result);
if (async)
async(slog);
else
sync(slog);
}
protected static Map<String, Map<String, List<String>>> caches = new HashMap<String, Map<String, List<String>>>();
public void init() {
queue = new LinkedBlockingQueue<Sys_log>();
int c = Runtime.getRuntime().availableProcessors();
es = Executors.newFixedThreadPool(c);
for (int i = 0; i < c; i++) {
es.submit(this);
}
}
public void close() throws InterruptedException {
queue = null; // 触发关闭
if (es != null && !es.isShutdown()) {
es.shutdown();
es.awaitTermination(5, TimeUnit.SECONDS);
}
}
public static Sys_log makeLog(String type, String tag, String source, String msg, String param, String result) {
Sys_log sysLog = new Sys_log();
if (type == null || tag == null) {
throw new RuntimeException("type/tag can't null");
}
if (source == null) {
StackTraceElement[] tmp = Thread.currentThread().getStackTrace();
if (tmp.length > 2) {
source = tmp[2].getClassName() + "#" + tmp[2].getMethodName();
} else {
source = "main";
}
}
sysLog.setType(type);
sysLog.setTag(tag);
sysLog.setSrc(source);
sysLog.setMsg(msg);
sysLog.setParam(param);
sysLog.setResult(result);
if (Mvcs.getReq() != null) {
sysLog.setIp(Lang.getIP(Mvcs.getReq()));
}
sysLog.setCreatedBy(SecurityUtil.getUserId());
sysLog.setDelFlag(false);
sysLog.setUsername(SecurityUtil.getUserUsername());
sysLog.setLoginname(SecurityUtil.getUserLoginname());
return sysLog;
}
}
@@ -0,0 +1,94 @@
package com.budwk.app.base.model;
import cn.hutool.json.JSONObject;
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.nutz.dao.entity.annotation.*;
import org.nutz.dao.interceptor.annotation.PrevInsert;
import java.util.Date;
@EqualsAndHashCode(callSuper = true)
@Table("audit")
@Comment("审核记录表")
@Data
public class Audit extends BaseModel {
@Column
@Name
@Comment("ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
@PrevInsert(uu32 = true)
private String id;
@Column
@Comment("审核人")
@ColDefine(type = ColType.VARCHAR, width = 32)
@PrevInsert(els = {@EL("$me.uid()")})
private String auditor;
@Column
@Comment("是否通过")
@ColDefine(type = ColType.BOOLEAN)
private Boolean auditPass;
@Column
@Comment("审核时间")
@Prev(els = {@EL("$me.nowDate()")})
private Date auditTime;
@Column
@Comment("审核意见")
@ColDefine(type = ColType.VARCHAR, width = 500)
private String auditOpinion;
@Column
@Comment("签字")
@ColDefine(type = ColType.TEXT)
private String auditSign;
@Column
@Comment("审核人姓名")
@ColDefine(type = ColType.VARCHAR, width = 50)
@PrevInsert(els = {@EL("$me.userName()")})
private String username;
@Column
@Comment("审核人工号")
@ColDefine(type = ColType.VARCHAR, width = 50)
@PrevInsert(els = {@EL("$me.loginName()")})
private String loginname;
@Column
@Comment("审核类型(1.通过 2.拒绝 3.退回)")
@ColDefine(type = ColType.INT)
private Integer auditType;
@Column
@Comment("扩展字段")
@ColDefine(type = ColType.MYSQL_JSON)
private JSONObject ext;
@Column
@Comment("父级ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String parentId;
public Date nowDate() {
return new Date();
}
public String uid() {
return SecurityUtil.getUserId();
}
public String userName() {
return SecurityUtil.getUserUsername();
}
public String loginName() {
return SecurityUtil.getUserLoginname();
}
}
@@ -0,0 +1,92 @@
package com.budwk.app.base.model;
import cn.dev33.satoken.stp.StpUtil;
import lombok.Data;
import org.nutz.dao.entity.annotation.*;
import org.nutz.dao.interceptor.annotation.PrevInsert;
import org.nutz.dao.interceptor.annotation.PrevUpdate;
import org.nutz.json.Json;
import org.nutz.json.JsonFormat;
import org.nutz.lang.Strings;
import java.io.Serializable;
/**
* Created by wizzer on 2016/6/21.
* Update by wizzer on 2021/8/30.
*/
@Data
public abstract class BaseModel implements Serializable {
private static final long serialVersionUID = 1L;
@Column
@Comment("创建人")
@PrevInsert(els = @EL("$me.createdByUid()"))
@ColDefine(type = ColType.VARCHAR, width = 32)
private String createdBy;
/**
* Long不要用ColDefine定义,兼容oracle/mysql,支持2038年以后的时间戳
* 13位时间戳哦,不再是11位
*/
@Column
@Comment("创建时间")
@PrevInsert(now = true)
private Long createdAt;
@Column
@Comment("修改人")
@PrevInsert(els = @EL("$me.updatedByUid()"))
@PrevUpdate(els = @EL("$me.updatedByUid()"))
@ColDefine(type = ColType.VARCHAR, width = 32)
private String updatedBy;
/**
* Long不要用ColDefine定义,兼容oracle/mysql,支持2038年以后的时间戳
* 13位时间戳哦,不再是11位
*/
@Column
@Comment("修改时间")
@PrevInsert(now = true)
@PrevUpdate(now = true)
private Long updatedAt;
@Column
@Comment("删除标记")
@PrevInsert(els = @EL("$me.flag()"))
@ColDefine(type = ColType.BOOLEAN)
private Boolean delFlag;
public String toJsonString() {
return Json.toJson(this, JsonFormat.compact());
}
public Boolean flag() {
return false;
}
public String createdByUid() {
String uid = getCreatedBy();
if (Strings.isNotBlank(uid)) {
return uid;
}
try {
return StpUtil.getLoginIdAsString();
} catch (Exception e) {
return "";
}
}
public String updatedByUid() {
String uid = getUpdatedBy();
if (Strings.isNotBlank(uid)) {
return uid;
}
try {
return StpUtil.getLoginIdAsString();
} catch (Exception e) {
return "";
}
}
}
@@ -0,0 +1,18 @@
package com.budwk.app.base.model;
import lombok.Data;
import java.util.List;
@Data
public class CustomFormField {
private String fieldName; // 字段名称
private String fieldType; // 字段类型(如 "text", "date", "file" 等)
private Boolean required; // 是否必填
private Integer maxLength; // 最大字符数(仅适用于文本类型)
private String dateRange; // 日期范围(仅适用于日期类型,格式为 "YYYY-MM-DD,YYYY-MM-DD")
private List<String> allowedFileTypes; // 允许的文件类型(仅适用于文件类型,如 ["pdf", "jpg"])
private Integer maxFiles; // 最大文件数量(仅适用于文件类型)
}
@@ -0,0 +1,25 @@
package com.budwk.app.base.model;
import lombok.Getter;
/**
* Excel导入错误信息
*/
public class ExcelImportError {
/**
* 行号
*/
private int row;
/**
* 错误信息
*/
@Getter
private String errMsg;
public void setErrInfo(String errMsg, int row) {
this.errMsg = errMsg;
this.row = row;
}
}
@@ -0,0 +1,29 @@
package com.budwk.app.base.model;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;
/**
* Excel导入结果
* @param <T>
*/
@Data
public class ExcelImportRes<T extends ExcelImportError> {
private int totalRecords = 0;
private int successCount = 0;
private int failedCount = 0;
private List<T> errorDetails = new ArrayList<>();
// 失败数+1
public void addFailedCount() {
failedCount++;
System.out.println(failedCount);
}
}
@@ -0,0 +1,46 @@
package com.budwk.app.base.page;
import org.nutz.dao.pager.Pager;
import java.io.Serializable;
/**
* 指定偏移量及大小的Pager, 这样就不会受限于原生Pager的offset=(pageNumber-1)*pageSize
*
* @author wendal
*/
public class OffsetPager extends Pager implements Serializable {
private static final long serialVersionUID = -1385308131663113162L;
protected int offset = -1;
protected OffsetPager() {
}
/**
* 构建一个指定偏移量及大小的Pager
*
* @param offset 偏移量
* @param size 数据大小
*/
public OffsetPager(int offset, int size) {
super();
this.offset = offset;
setPageSize(size);
}
/**
* 覆盖超类的计算得到的offset
*/
public int getOffset() {
if (offset > -1)
return offset;
return super.getOffset();
}
public void setOffset(int offset) {
this.offset = offset;
}
}
@@ -0,0 +1,57 @@
package com.budwk.app.base.page;
public interface Paginable {
/**
* 总记录数
*
* @return
*/
public int getTotalCount();
/**
* 总页数
*
* @return
*/
public int getTotalPage();
/**
* 每页记录数
*
* @return
*/
public int getPageSize();
/**
* 当前页号
*
* @return
*/
public int getPageNo();
/**
* 是否第一页
*
* @return
*/
public boolean isFirstPage();
/**
* 是否最后一页
*
* @return
*/
public boolean isLastPage();
/**
* 返回下页的页号
*/
public int getNextPage();
/**
* 返回上页的页号
*/
public int getPrePage();
}
@@ -0,0 +1,77 @@
package com.budwk.app.base.page;
import org.nutz.lang.Lang;
import java.util.List;
public class Pagination<T> extends SimplePage implements java.io.Serializable {
private static final long serialVersionUID = 1L;
public Pagination() {
}
/**
* 构造器
*
* @param pageNo 页码
* @param pageSize 每页几条数据
* @param totalCount 总共几条数据
*/
public Pagination(int pageNo, int pageSize, int totalCount) {
super(pageNo, pageSize, totalCount);
}
/**
* 构造器
*
* @param pageNo 页码
* @param pageSize 每页几条数据
* @param totalCount 总共几条数据
* @param list 分页内容
*/
public Pagination(int pageNo, int pageSize, int totalCount, List<T> list) {
super(pageNo, pageSize, totalCount);
this.list = list;
}
/**
* 第一条数据位置
*
* @return
*/
public int getFirstResult() {
return (pageNo - 1) * pageSize;
}
/**
* 当前页的数据
*/
private List<T> list;
/**
* 获得分页内容
*
* @return
*/
public List<T> getList() {
return list;
}
/**
* @param classOfT 列表容器內的元素类型
* @return
*/
public List<T> getList(Class<T> classOfT) {
return Lang.collection2list(list, classOfT);
}
/**
* 设置分页内容
*
* @param list
*/
public void setList(List<T> list) {
this.list = list;
}
}
@@ -0,0 +1,175 @@
package com.budwk.app.base.page;
import java.util.ArrayList;
import java.util.List;
public class SimplePage implements java.io.Serializable, Paginable {
private static final long serialVersionUID = 1L;
public static final int DEF_COUNT = 10;
private List<Integer> localArrayList = new ArrayList<Integer>();
public List<Integer> getSegment() {
return localArrayList;
}
/**
* 检查页码 checkPageNo
*
* @param pageNo
* @return if pageNo==null or pageNo 小于 1 then return 1 else return pageNo
*/
public static int cpn(Integer pageNo) {
return (pageNo == null || pageNo < 1) ? 1 : pageNo;
}
public SimplePage() {
}
/**
* 构造器
*
* @param pageNo 页码
* @param pageSize 每页几条数据
* @param totalCount 总共几条数据
*/
public SimplePage(int pageNo, int pageSize, int totalCount) {
setTotalCount(totalCount);
setPageSize(pageSize);
setPageNo(pageNo);
adjustPageNo();
int totalPages = getTotalPage();
minPage = minPage < 1 ? 1 : minPage;
maxPage = maxPage > totalPages ? totalPages : maxPage;
for (int i = minPage; i <= maxPage; i++) {
localArrayList.add(i);
}
}
/**
* 调整页码,使不超过最大页数
*/
public void adjustPageNo() {
if (pageNo == 1) {
return;
}
int tp = getTotalPage();
if (pageNo > tp) {
pageNo = tp;
}
}
/**
* 获得页码
*/
public int getPageNo() {
return pageNo;
}
/**
* 每页几条数据
*/
public int getPageSize() {
return pageSize;
}
/**
* 总共几条数据
*/
public int getTotalCount() {
return totalCount;
}
/**
* 总共几页
*/
public int getTotalPage() {
int totalPage = totalCount / pageSize;
if (totalPage == 0 || totalCount % pageSize != 0) {
totalPage++;
}
return totalPage;
}
/**
* 是否第一页
*/
public boolean isFirstPage() {
return pageNo <= 1;
}
/**
* 是否最后一页
*/
public boolean isLastPage() {
return pageNo >= getTotalPage();
}
/**
* 下一页页码
*/
public int getNextPage() {
if (isLastPage()) {
return pageNo;
} else {
return pageNo + 1;
}
}
/**
* 上一页页码
*/
public int getPrePage() {
if (isFirstPage()) {
return pageNo;
} else {
return pageNo - 1;
}
}
protected int totalCount = 0;
protected int pageSize = 20;
protected int pageNo = 1;
/**
* if totalCount 小于 0 then totalCount=0
*
* @param totalCount
*/
public void setTotalCount(int totalCount) {
if (totalCount < 0) {
this.totalCount = 0;
} else {
this.totalCount = totalCount;
}
}
/**
* if pageSize 小于 1 then pageSize=DEF_COUNT
*
* @param pageSize
*/
public void setPageSize(int pageSize) {
if (pageSize < 1) {
this.pageSize = DEF_COUNT;
} else {
this.pageSize = pageSize;
}
}
/**
* if pageNo 小于 1 then pageNo=1
*
* @param pageNo
*/
public void setPageNo(int pageNo) {
if (pageNo < 1) {
this.pageNo = 1;
} else {
this.pageNo = pageNo;
}
}
int minPage = pageNo - (int) Math.floor((pageSize - 1) / 2.0D);
int maxPage = pageNo + (int) Math.ceil((pageSize - 1) / 2.0D);
int totalPage = getTotalPage();
}
@@ -0,0 +1,47 @@
package com.budwk.app.base.page.datatable;
import java.io.Serializable;
/**
* Created by wizzer on 2016/6/27.
*/
public class DataTableColumn implements Serializable {
private static final long serialVersionUID = 1L;
protected String data;
protected String name;
protected boolean searchable;
protected boolean orderable;
public String getData() {
return data;
}
public void setData(String data) {
this.data = data;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public boolean isSearchable() {
return searchable;
}
public void setSearchable(boolean searchable) {
this.searchable = searchable;
}
public boolean isOrderable() {
return orderable;
}
public void setOrderable(boolean orderable) {
this.orderable = orderable;
}
}
@@ -0,0 +1,29 @@
package com.budwk.app.base.page.datatable;
import java.io.Serializable;
/**
* Created by wizzer on 2016/6/27.
*/
public class DataTableOrder implements Serializable {
private static final long serialVersionUID = 1L;
protected int column;
protected String dir;
public int getColumn() {
return column;
}
public void setColumn(int column) {
this.column = column;
}
public String getDir() {
return dir;
}
public void setDir(String dir) {
this.dir = dir;
}
}
@@ -0,0 +1,21 @@
package com.budwk.app.base.param;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
/**
* 单个条件
*/
@Data
public class Condition {
@ApiModelProperty("字段名")
private String field;
@ApiModelProperty("操作符: =, !=, >, <, >=, <=, LIKE, IN, NOT IN, IS NULL, IS NOT NULL")
private String operator;
@ApiModelProperty("字段值")
private Object value;
}
@@ -0,0 +1,24 @@
package com.budwk.app.base.param;
import com.budwk.app.sys.param.SysDataUserUpdateParam;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.util.List;
/**
* 条件组,支持嵌套的与或非逻辑
*/
@Data
public class ConditionGroup {
@ApiModelProperty("逻辑类型:AND, OR")
private String logic = "AND";
@ApiModelProperty("条件列表")
private List<Condition> conditions;
@ApiModelProperty("嵌套条件组")
private List<ConditionGroup> groups;
}
@@ -0,0 +1,14 @@
package com.budwk.app.base.param;
import lombok.Data;
/**
* 自定义导出表格列
*/
@Data
public class ExportTableColumns {
private String label;
private String prop;
}
@@ -0,0 +1,60 @@
package com.budwk.app.base.param;
import lombok.Data;
import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Positive;
/**
* @Digits注解 验证注解的元素值的整数位数和小数位数上限 ,并且类型为float,double,BigDecimal。
*/
@Data
public class PageForm {
@NotNull(message = "pageNumber不能为空")
@Min(value = 1, message = "pageNumber最小为1")
@Positive(message = "pageNumber必须是正数")
private Integer pageNumber;
@NotNull(message = "pageSize不能为空")
@Max(value = 200, message = "pageSize最大为50")
private Integer pageSize;
private String pageOrderName;
private String pageOrderBy;
private String searchName;
private String searchKeyword;
/*======================流程参数==============================*/
/**
* 是否审核
*/
private Boolean audit;
/**
* 业务ID
*/
private String bizId;
public PageForm defaultSort(String column, String order) {
this.setPageOrderName(column);
this.setPageOrderBy(order);
return this;
}
public PageForm defaultSortAsc(String column) {
return this.defaultSort(column, "ascending");
}
public PageForm defaultSortDesc(String column) {
return this.defaultSort(column, "descending");
}
}
@@ -0,0 +1,68 @@
package com.budwk.app.base.payment.alipay.controller;
import com.budwk.app.base.payment.alipay.service.AlipayService;
import lombok.extern.slf4j.Slf4j;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.mvc.annotation.At;
import org.nutz.mvc.annotation.Ok;
import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
/**
* @ClassName AlipayClientController
* @Description TODO
* @Author zhf
* @Date 2024/1/25 15:36
*/
@IocBean
@Slf4j
@Ok("json:full")
@At("/mobile/memberCost/AlipayClient")
public class AlipayClientController {
@Inject
private AlipayService alipayService;
@At("/tradePagePay")
public String tradePagePay(String money, String paymentProjectId) throws Exception {
log.info("统一收单下单并支付接口调用");
String formStr = alipayService.tradeCreate(money, paymentProjectId);
return formStr;
}
@At("/tradeNotify")
@Ok("re")
public String tradeNotify(HttpServletRequest request) {
try {
Map<String, String> params = new HashMap<String, String>();
Map<String, String[]> requestParams = request.getParameterMap();
for (Iterator<String> iter = requestParams.keySet().iterator(); iter.hasNext(); ) {
String name = (String) iter.next();
String[] values = (String[]) requestParams.get(name);
String valueStr = "";
for (int i = 0; i < values.length; i++) {
valueStr = (i == values.length - 1) ? valueStr + values[i]
: valueStr + values[i] + ",";
}
//乱码解决,这段代码在出现乱码时使用
valueStr = new String(valueStr.getBytes("ISO-8859-1"), "utf-8");
params.put(name, valueStr);
}
log.info("支付通知,正在执行,通知参数:{}");
return alipayService.tradeNotify(params);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
}
@@ -0,0 +1,22 @@
package com.budwk.app.base.payment.alipay.service;
/**
* @ClassName AlipayClientConfig
* @Description TODO
* @Author zhf
* @Date 2024/1/25 15:33
*/
public interface AlipayClientConfig {
String serverUrl = "https://openapi.alipay.com/gateway.do";
String appId = "2021004133670087";
String sellerId = "2088002638762620";
String privateKey = "MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCEJBWNlUIY/2Tave1OtjTgvA21GZ5tdvZZD3jFmixIFTkEdZaRUpXCvlKuZ9heoL1IQwpbuOErJ4dM2f2Ju/Oc7QtUOjEOX6CnElR73KK5OSY8d8f5822LUOgp2zg5FHI6C7olYmjN4Nw26G6BGyqQBmWs4/sKzghKelEaH9AqkR8DZ+yOvuYtIg5KEvVixsM27TMhd6/3BuNXDIiQDVH9ZeQVb9IKuynZ6hnUJS0T4MkS41qehS1u8LgplGU5yvrNuUNeQtybADsiGHDNmNkl5lVt0QNh+N/lo56//duf5PJwh8ApDB22eXptiS6p321PSrYowy5VeFpHC07Tref9AgMBAAECggEAPQddXPKegGUT82LcgwnsCg9QVV/rp6rVw/Q9VyYJZZFHsCOO0rdNq4+zKBGWAL5ELWepFQuiEwhCmN/lFQPVU5i3LsNhfL31uLPiiQIxtVILssmvdmVTJpDLaOlyYgprZxubcUCuHsfHbSSURNSDJ9mMg/kXLLpTYfJ0Gpiuz7lkrMuAxncdz88Sp2r0O9k2T+TcnHFpRXHs+C/QAaa15EF18ieDSBLPkY0ti1R8J91LGAzDrjwcP7pNlq4u7TasZ7djv47ZFwQ3yjWoo13d9ab0MBYeQx9Grf5N+JGNf33YLyulQ0IshcuwNyB+RYhyWGjNmjnJdytzKRMOYSpPgQKBgQC6iqMPX+LGunyI+3T9l/AwAVWRJXzEHPflNxDYfQbicB5awlRixxH5T1qyZ46KZb/pBt01qDULXy0bEObVFSjAn1wXPzC9Fwb+BtVZUroYBTKV3tOAMpVCz2AAC34OZwG/3X//sRtW9Sr0Dr+yCcx8ULMiFS1BuCZfhSNXA516NQKBgQC1V+rge1pYKGnjtWqML4qJy4FEIDI4fSpGIizxumYDMxdXoUdNKoG2NHQkznHU4fIHJPVvIN/Tm82mfj2rhH9eFuu1FB8TL2soe5nPgzSJ9AmP7rZ7KBo3/SczvZqUEpii8+PjHu+Gx95BmgTzOFMnYz8xzitb4IBGTEGxVzavqQKBgCL28XFkS6FsOwOB/n+6Bu84kM1P9X7c3yo7lBC//pZa5ZWlbyPEpH3SffGKr5EJWVtYSgZQrQCgqle0Z0/xU14ngJ9q3LvUoR7kMQE5HGB8OOsWhRY2ZPvM2vWnMc2JSS6pWeCXVcSK164/RXcdlxKGHc15oq360lZ9Ig6hQtcxAoGAIYijd2V4HGtEwYXvsQKtlLOcMNPfVZDXu2Zgd0bnzFmrTFoXjE569A2ZGKFQbXRmxClydV1X5pKeIdagYtI6l1SXu8F/yZVwNGk+1R/HHGeTSRl0hyuwRYA7upaqOg2CSkfdcUdsNblKAUmQ9sUIo3T336bnX7MRJW6+iyO8ihkCgYBG8vG1fVUwPmqlRK+QanHxicR1+UyjRAN+aW6dgJUNMlC+tmPeHj21+WGkDdKPdhinHziLihLmzbmpwWSDwuGMMQPNQm8gSjiaqy1tWcP6XKDxjpG5biTzwEyHNuCIY6odS4HTeDOrrU4LvN6PxirN8xKBIpq+A2cgpGo+Mn4x+g==";
String alipayPublicKey = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAjO/QUy+ujNBlToL6Kt8YjvTRGpEwmXjqc9pFe7GIORVkzBXP3F/UC/w5mPiwXhhyymXAe7bUwLANL/QMUZhvpjjCdLENtFWC9bd6hq3m2oHiTu0oicbzqCuaoQkzik2NybrNjuH+aYVi4RU0eNYpdewQoHHW+hbfxkSYUWmx7NF4SuG93edYlpc/G/ImdrweATLE0b6W9il4H/BxVUweCraPrispiOM8r6TN3fiIKUQRP15HKQo8Xsr4pMJzfi37I9aN8AThN/xBf6sual8IZZsH5hC3PS37LrYkXbUjt0dK5tqKl3SsZPjvvHGyz9b9JC225+0q09Y6B8/axOlihwIDAQAB";
/* String serverUrl = "https://openapi-sandbox.dl.alipaydev.com/gateway.do";
String appId = "9021000134641646";
String privateKey = "MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCBFTF8qFbK+NiqUbKf1/jWHL/s0hfUXK6cyKwZCEXkEMZH1Lu6kZQl3kQye9Fsasawxn1vLmKz+OKjMiE+TdTytHDu5ULdLlSHN3bY1qf91i8L+RJfmZpb1p+NQOk9zImhhSauuk+itMkTC3bTiwwAkg/utOxkNB7YaynpAb3evhXnlX3LUVTHaMnCQFeV7ithh7CDKx0LPlxOpOvxnkWDBR3nsdcfjDqYNDVHXnUxEk0KQV4Ik6fA40+Gq/HXTByJolCSAaTk0OaH2ULp1RZAklWLqhiNEhytTZAlJUx8cSm36mm0c4jhgSXNdRz0Yi1BlPY9yxQGStpH5ue3+EfvAgMBAAECggEAda/D4Zp9HysIJyi7SZ6XwWaO2TSnAzfI/yq6NUxkpE1i6cSbr7rgrjHaTYiz5DvpkOw3GnHZDarhvtaKlopRl0JKIJSnhI6kBG/eDE6Q9Q4c8LaSONULdaXU6+z9ehmMddW28eDtkDNxi8qM+MuyEqcYnZMYXzdh1ADIQicTXArxo+9fpvlKUMNxOTO5LoOf0DtJg/xE1CD9PtWVc+l2G8S3+C52+RhfZ214YQdxwayMlvJQlWzLTbfzJCp8w4F2up1nJdwp7EY8k8xDUngEnG6BWm8ztWUVTTg7qp/+SC9yMTD2dl86wDkJt1lTtwcKaWxHYh2ZIn8a8+icHFykaQKBgQDgBEWAmuj0pgpny5Q0GRsUYPq42SwpN70u4rmD5UYFCeDHaKb4U3kyvA1V+1Tp0XWzFi7GU855h1n8dnQX64J3Vm4GfhftWwTJgB5ibbY53cZVEoV5nZe0bOO4hxxMdNy3BKjPovkT0REPpCkVas/RRLAFCdnvi6vmDkFxvv9JewKBgQCTgx9NZZCqSzzCJYpLTDOCitooOQmR8kAlgBiDCcHeH2wNVMh5O2Dl/OyTGauKUJFWIOSD8FXVTrNUbOEmx58msnS/+Mc5SnbEka5ptVEDp73gf6ctB+eZAMdO1kLsBT3iKnuyNa7pRyR/D2A692RHOTQZIvRHE1N+kM1r+iBPHQKBgCZRVLocxUAjrp/2eJE4wiMIO5qmuyEJwbhlW6ot634tJF+3jOfpD1AYToeoh+u+wk0xvCp8Z1LuTfwbHtyAkj5rcEJ4Cwu0h+sXmRro/USEpMokcuRMioh4H7OOmM6TJrW/fEiX7dbv6eUtBUH+lTGMeDdEahzJXUvqoFMegnhFAoGAa2eflRUMflIFzT7UMPsszjIXog3VoASJHETmlBK4q+ipHhPLXCoV5Z+BCaI5Op5rFBEjEcYeWQ/Slvgn/7p9cQmi5FxZgqdrTrjcC2/Pqx+pqxqHOMx2nvroI8jyNTOj2VchdaKM1qHMOw+Q+55Bz3IhMuQ2hMHXm2IHkEUw3SkCgYBjcqcWbJcq26L8yYVpU2JFzOvu7iHuV99TpEvF9Ym3cAcSNuoR5kGXzGS0MadYY87cd4HK9HvmXQmthLY14buKYxq27ZFfNbK/Auz8vk0Wbrm1Ym4Z1yMqClQqxKdr4hV29ijn1nPProRqgnVsGH3EtFmNxDfilZoVzh9ERaZe7w==";
String alipayPublicKey = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArhElUiFMpxqFCs+8zDHa4OVuhdWgKUUGVKgWnfLktPgy9mDZkuGPKk295k8ktWQSUZsnFvT8ikJIKdGYfbSWzlXvCAn6lDDDlsjfOJWMQ6zLmb9xygCih2OjwN5xt9QFIIthnWq8sOXpWvqmoRpIFQHolxxmvvCmUZ/VwK6mwgwU/yVvVypruQ/FGrJA70LAcJ764spWAMy/pWIoIs5UewOlJa3TAcZQIqRnjupL2/sI/TpIWFWF90a/rahwLfBgZ83VH7MA4Tl9uFLc3aeNFLllDF4X7lRdzaHIMYgUUKte9yvlDy5Ihrt+ssjsgdRhqLHvFn00u/g7BCvO5ZmIUwIDAQAB";
*/
}
@@ -0,0 +1,15 @@
package com.budwk.app.base.payment.alipay.service;
import com.budwk.app.base.service.BaseService;
import java.util.Map;
public interface AlipayService extends BaseService {
/**
* 支付宝开放平台接收 request请求对象后
* @return
*/
String tradeCreate( String money, String paymentProjectId) throws Exception;
String tradeNotify(Map<String, String> params) ;
}
@@ -0,0 +1,140 @@
package com.budwk.app.base.payment.alipay.service.impl;
import com.alipay.api.AlipayApiException;
import com.alipay.api.AlipayConstants;
import com.alipay.api.DefaultAlipayClient;
import com.alipay.api.domain.AlipayTradeWapPayModel;
import com.alipay.api.internal.util.AlipaySignature;
import com.alipay.api.request.AlipayTradeWapPayRequest;
import com.alipay.api.response.AlipayTradeWapPayResponse;
import com.budwk.app.base.payment.alipay.service.AlipayClientConfig;
import com.budwk.app.base.payment.alipay.service.AlipayService;
import com.budwk.app.base.service.impl.BaseServiceImpl;
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
import com.budwk.app.zhgh.staffmanage.memberCost.model.MemberPaymentInfo;
import lombok.extern.slf4j.Slf4j;
import org.nutz.dao.Chain;
import org.nutz.dao.Cnd;
import org.nutz.dao.Dao;
import org.nutz.ioc.loader.annotation.IocBean;
import java.util.Date;
import java.util.Map;
/**
* @ClassName AlipayNewServiceImpl
* @Description TODO
* @Author zhf
* @Date 2024/1/25 15:38
*/
@Slf4j
@IocBean(args = {"refer:dao"})
public class AlipayNewServiceImpl extends BaseServiceImpl implements AlipayService {
public AlipayNewServiceImpl(Dao dao) {
super(dao);
}
DefaultAlipayClient alipayClient = new DefaultAlipayClient(AlipayClientConfig.serverUrl,
AlipayClientConfig.appId,
AlipayClientConfig.privateKey,
AlipayConstants.FORMAT_JSON,
AlipayConstants.CHARSET_UTF8,
AlipayClientConfig.alipayPublicKey,
AlipayConstants.SIGN_TYPE_RSA2);
@Override
public String tradeCreate(String money, String paymentProjectId) throws Exception {
String outTradeNo = String.valueOf(new Date().getTime());
AlipayTradeWapPayRequest request = new AlipayTradeWapPayRequest();
//支付宝公共参数
request.setReturnUrl("http://sansi.ltd/mobile/memberCost/AlipayClient/tradeNotify");
//面向对象封装业务参数
AlipayTradeWapPayModel model = new AlipayTradeWapPayModel();
model.setOutTradeNo(outTradeNo);
model.setTotalAmount(money);
model.setSubject("会员入会会费");
model.setProductCode("QUICK_WAP_WAY");
model.setSellerId(AlipayClientConfig.sellerId);
model.setTimeoutExpress("30m");
model.setQuitUrl("http://sansi.ltd/mobile/memberCost/list");
request.setBizModel(model);
//执行请求,调用支付宝
AlipayTradeWapPayResponse response = alipayClient.pageExecute(request, "POST");
if (response.isSuccess()) {
dao().update(MemberPaymentInfo.class, Chain.make("payId", outTradeNo), Cnd.where("paymentProjectId", "=", paymentProjectId)
.and("userId", "=", SecurityUtil.getUserId()));
log.info("调用成功,返回结果:[{}]", response.getBody());
return response.getBody();
} else {
log.info("调用失败!!");
}
return null;
}
@Override
public String tradeNotify(Map<String, String> params) {
String total_amount = params.get("total_amount");
String out_trade_no = params.get("out_trade_no");
String trade_no = params.get("trade_no");
//
String result = "failure";
try {
//异步通知验签
boolean signVerified = AlipaySignature.rsaCheckV1(params,
AlipayClientConfig.alipayPublicKey,
AlipayConstants.CHARSET_UTF8,
AlipayConstants.SIGN_TYPE_RSA2);
if (!signVerified) {
// TODO 验签失败则记录异常日志,并在response中返回failure.
log.error("支付成功,异步通知验签失败!");
return result;
}
log.info("支付成功,异步通知验签成功!");
//TODO 验签成功后,按照支付结果异步通知中的描述,对支付结果中的业务内容进行二次校验
//1.验证out_trade_no 是否为商家系统中创建的订单号
String outTradeNo = params.get("out_trade_no");
if (!outTradeNo.equals(out_trade_no)) {
log.error("订单号不一致");
return result;
}
//2.判断 total_amount 是否确实为该订单的实际金额
String totalAmount = params.get("total_amount");
if (!totalAmount.equals(total_amount)) {
log.error("订单的实际金额不一致");
return result;
}
//3.校验通知中的 seller_id是否为 out_trade_no 这笔单据的对应的操作方
String sellerId = params.get("seller_id");
if (!sellerId.equals(AlipayClientConfig.sellerId)) {
log.error("商家PID校验失败");
return result;
}
//4.验证 app_id 是否为该商家本身
String appId = params.get("app_id");
if (!appId.equals(AlipayClientConfig.appId)) {
log.error("app_id校验失败");
return result;
}
dao().update(MemberPaymentInfo.class,
Chain.make("isPayment", true)
.add("paymentMoney", total_amount)
.add("paymentTime", new Date())
.add("tradeNo", trade_no),
Cnd.where("userId", "=", SecurityUtil.getUserId()).and("payId", "=", out_trade_no));
//TODO 处理自身业务
result = "->:/mobile/memberCost/list/paySuccess";
} catch (AlipayApiException e) {
e.printStackTrace();
}
return result;
}
}
@@ -0,0 +1,24 @@
package com.budwk.app.base.payment.enums;
import com.budwk.app.base.annotation.DictEnum;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 退款状态枚举
*/
@Getter
@AllArgsConstructor
@DictEnum(key = "RefundStateEnum", name = "退款状态枚举")
public enum RefundStateEnum {
INITIATE_REFUND("发起退款"),
REFUNDING("退款中"),
REFUND_SUCCESS("退款成功"),
REFUND_FAILED("退款失败");
/**
* 避免和枚举类的name冲突
*/
public final String refundStateName;
}
@@ -0,0 +1,25 @@
package com.budwk.app.base.payment.enums;
import com.budwk.app.base.annotation.DictEnum;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 交易渠道枚举
*/
@Getter
@AllArgsConstructor
@DictEnum(key = "TradingChannelEnum", name = "交易渠道枚举")
public enum TradingChannelEnum {
QQ_PAY("QQ"),
WX_PAY("微信"),
JD_PAY("京东"),
ALI_PAY("支付宝"),
UNION_PAY("云闪付");
/**
* 避免和枚举类的name冲突
*/
public final String tradingChannelName;
}
@@ -0,0 +1,24 @@
package com.budwk.app.base.payment.enums;
import com.budwk.app.base.annotation.DictEnum;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 支付状态枚举
*/
@Getter
@AllArgsConstructor
@DictEnum(key = "TradingStateEnum", name = "支付状态枚举")
public enum TradingStateEnum {
WAITING_FOR_PAYMENT("等待支付"),
PAY_SUCCESS("支付成功"),
CANCEL_PAYMENT("取消支付");
/**
* 避免和枚举类的name冲突
*/
public final String tradingStateName;
}
@@ -0,0 +1,22 @@
package com.budwk.app.base.payment.enums;
import com.budwk.app.base.annotation.DictEnum;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 交易类型枚举
*/
@Getter
@AllArgsConstructor
@DictEnum(key = "TradingTypeEnum", name = "交易类型枚举")
public enum TradingTypeEnum {
PAYMENT("付款"),
REFUND("退款");
/**
* 避免和枚举类的name冲突
*/
public final String tradingTypeName;
}
@@ -0,0 +1,30 @@
package com.budwk.app.base.payment.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* @version 1.0
* @Author zzr
* @name:WxPayResultCode
* @Date 2025/3/10 10:27
* @注释 微信支付返回码
*/
@Getter
@AllArgsConstructor
public enum WxPayResultCode {
SUCCESS(200, "操作成功"),
APPID_MCHID_NOT_MATCH(400, "商户ID和服务号或企业微信号不匹配"),
INVALID_REQUEST(400, "无效请求"),
MCH_NOT_EXISTS(400, "商户号不存在"),
PARAM_ERROR(400, "参数错误"),
SIGN_ERROR(401, "签名错误"),
NO_AUTH(403, "商户无权限"),
OUT_TRADE_NO_USED(403, "商户订单号重复"),
FREQUENCY_LIMITED(429, "频率超限"),
SYSTEM_ERROR(500, "系统错误");
final int code;
final String msg;
}
@@ -0,0 +1,61 @@
package com.budwk.app.base.payment.model;
import com.budwk.app.base.model.BaseModel;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.nutz.dao.entity.annotation.*;
import org.nutz.dao.interceptor.annotation.PrevInsert;
import java.math.BigDecimal;
/**
* 退款记录表
*/
@Data
@Table
@Comment("退款记录表")
@EqualsAndHashCode(callSuper = true)
public class PaymentRefundRecord extends BaseModel {
@Column
@Name
@Comment("ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
@PrevInsert(uu32 = true)
private String id;
@Column
@Comment("关联系统交易订单表Id")
@ColDefine(type = ColType.VARCHAR,width = 32)
private String paymentTransactionId;
@Column
@Comment("本次退款订单号")
@ColDefine(type = ColType.VARCHAR,width = 128)
private String outRefundTradeNo;
@Column
@Comment("第三方支付的退款单号")
@ColDefine(type = ColType.VARCHAR,width = 128)
private String refundNo;
@Column
@Comment("退款状态")
@ColDefine(type = ColType.VARCHAR,width = 30)
private String refundState;
@Column
@Comment("本次退款金额")
@ColDefine(type = ColType.INT)
private Integer refundAmount;
@Column
@Comment("原订单金额")
@ColDefine(type = ColType.INT)
private Integer total;
@Column
@Comment("交易返回数据")
@ColDefine(type = ColType.MYSQL_JSON)
private String resultJson;
}
@@ -0,0 +1,131 @@
package com.budwk.app.base.payment.model;
import com.budwk.app.base.model.BaseModel;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.nutz.dao.entity.annotation.*;
import org.nutz.dao.interceptor.annotation.PrevInsert;
import java.math.BigDecimal;
/**
* 交易单表
*/
@Data
@Table
@Comment("交易订单表")
@EqualsAndHashCode(callSuper = true)
public class PaymentTransaction extends BaseModel {
@Column
@Name
@Comment("ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
@PrevInsert(uu32 = true)
private String id;
@Column
@Comment("商户号")
@ColDefine(type = ColType.VARCHAR,width = 128)
private String mchId;
@Column
@Comment("订单号,传递给交易系统,交易系统订单号")
@ColDefine(type = ColType.VARCHAR,width = 128)
private String outTradeNo;
@Column
@Comment("交易系统交易号")
@ColDefine(type = ColType.VARCHAR,width = 128)
private String transactionId;
@Column
@Comment("交易渠道")
@ColDefine(type = ColType.VARCHAR,width = 50)
private String tradingChannel;
@Column
@Comment("交易类型")
@ColDefine(type = ColType.VARCHAR,width = 30)
private String tradingType;
@Column
@Comment("交易状态")
@ColDefine(type = ColType.VARCHAR,width = 50)
private String tradingState;
@Column
@Comment("用户ID")
@ColDefine(type = ColType.VARCHAR,width = 32)
private String userId;
@Column
@Comment("用户工号")
@ColDefine(type = ColType.VARCHAR,width = 50)
private String loginName;
@Column
@Comment("用户姓名")
@ColDefine(type = ColType.VARCHAR,width = 50)
private String userName;
@Column
@Comment("工会id")
@ColDefine(type = ColType.VARCHAR,width = 32)
private String unionId;
@Column
@Comment("用户工会")
@ColDefine(type = ColType.VARCHAR,width = 50)
private String unionName;
@Column
@Comment("单位id")
@ColDefine(type = ColType.VARCHAR,width = 32)
private String unitId;
@Column
@Comment("用户单位")
@ColDefine(type = ColType.VARCHAR,width = 50)
private String unitName;
@Column
@Comment("交易金额")
@ColDefine(type = ColType.INT)
private Integer tradingAmount;
@Column
@Comment("是否退款")
@ColDefine(type = ColType.BOOLEAN)
private Boolean isRefund;
@Column
@Comment("退款金额")
@ColDefine(type = ColType.INT)
private Integer refundAmount;
@Column
@Comment("订单描述")
@ColDefine(type = ColType.VARCHAR,width = 128)
private String description;
@Column
@Comment("附加详细描述")
@ColDefine(type = ColType.TEXT)
private String attach;
@Column
@Comment("微信支付用户openId")
@ColDefine(type = ColType.VARCHAR,width = 50)
private String openId;
@Column
@Comment("交易返回数据")
@ColDefine(type = ColType.MYSQL_JSON)
private String resultJson;
@Column
@Comment("交易名单的主键id")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String memberPaymentInfoId;
}
@@ -0,0 +1,53 @@
package com.budwk.app.base.payment.wxpay.bean;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* @version 1.0
* @Author zzr
* @name:WxPayV3Bean
* @Date 2025/3/6 9:54
* @注释
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class WxPayV3Bean {
/**
* 服务号
* appid: wxf12fa6175ec7c75f
* app_secret: f65bbb41d3de4a93c8c93d8e2fd50369
*/
private String appId;
private String keyPath;
private String publicKeyPath;
private String certPath;
private String certP12Path;
private String platformCertPath;
private String mchId;
private String apiKey;
private String apiKey3;
private String domain;
private String publicKeyId;
@Override
public String toString() {
return "WxPayV3Bean{" +
"appId='" + appId + '\'' +
", keyPath='" + keyPath + '\'' +
", publicKeyPath='" + publicKeyPath + '\'' +
", certPath='" + certPath + '\'' +
", certP12Path='" + certP12Path + '\'' +
", platformCertPath='" + platformCertPath + '\'' +
", mchId='" + mchId + '\'' +
", apiKey='" + apiKey + '\'' +
", apiKey3='" + apiKey3 + '\'' +
", domain='" + domain + '\'' +
", publicKeyId='" + publicKeyId + '\'' +
'}';
}
}
@@ -0,0 +1,44 @@
package com.budwk.app.base.payment.wxpay.controller;
import com.budwk.app.base.payment.wxpay.service.WxPayCommonService;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.mvc.annotation.At;
import org.nutz.mvc.annotation.Ok;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* @version 1.0
* @Author zzr
* @name:WxPayCommonController
* @Date 2025/3/10 19:28
* @注释 微信支付回调
*/
@Slf4j
@IocBean
@Ok("json:full")
@At("/platform/wxPay")
public class WxPayCommonController {
@Inject
private WxPayCommonService wxPayCommonService;
@At
@Ok("void")
@ApiOperation("微信支付-付款回调")
public void payNotify(HttpServletRequest request, HttpServletResponse response){
wxPayCommonService.payNotify(request, response);
}
@At
@Ok("void")
public void refundNotify(HttpServletRequest request, HttpServletResponse response){
wxPayCommonService.refundNotify(request, response);
}
}
@@ -0,0 +1,25 @@
-----BEGIN CERTIFICATE-----
MIIEKDCCAxCgAwIBAgIUONYpzvxVfV/RrWnLGPlL6/nb/AcwDQYJKoZIhvcNAQEL
BQAwXjELMAkGA1UEBhMCQ04xEzARBgNVBAoTClRlbnBheS5jb20xHTAbBgNVBAsT
FFRlbnBheS5jb20gQ0EgQ2VudGVyMRswGQYDVQQDExJUZW5wYXkuY29tIFJvb3Qg
Q0EwHhcNMjUwMzA2MTQwNDAzWhcNMzAwMzA1MTQwNDAzWjCBgTETMBEGA1UEAwwK
MTY2NjkzMDk4MDEbMBkGA1UECgwS5b6u5L+h5ZWG5oi357O757ufMS0wKwYDVQQL
DCTmrabmsYnlnLDlpKfkuInmgJ3np5HmioDmnInpmZDlhazlj7gxCzAJBgNVBAYT
AkNOMREwDwYDVQQHDAhTaGVuWmhlbjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC
AQoCggEBALlDML+FJEW+m2hXp/qUH1cU6xlwFllLU4LjFusJkxwyzzcZO4nY0mOi
3woz8xIJipvUWj2Y413WYwMkfcJcRgghupZkfYwdQtSyPfUu7xQBUm8TLWGH88Re
kyyVABEB5T+ne6r3LesXS4IuZknm5N/iaa4z9mbbscGqF0GLvMUqZCRNyTkyfK9r
Bkvs/vspi+Jcjx9I7aU++h/vwzsGB6Bf/uB3kLMb+lj/i3xY20sdrrW8JD2sqONQ
NcRIKmNXSp4zsNy+d6kIKWE3P5609ZTta1ogkeGBNdGXMRNNy6vLIudQc7zny/9H
S4LxGsIrsYmZNhGljRng56X+PG3w1CUCAwEAAaOBuTCBtjAJBgNVHRMEAjAAMAsG
A1UdDwQEAwID+DCBmwYDVR0fBIGTMIGQMIGNoIGKoIGHhoGEaHR0cDovL2V2Y2Eu
aXRydXMuY29tLmNuL3B1YmxpYy9pdHJ1c2NybD9DQT0xQkQ0MjIwRTUwREJDMDRC
MDZBRDM5NzU0OTg0NkMwMUMzRThFQkQyJnNnPUhBQ0M0NzFCNjU0MjJFMTJCMjdB
OUQzM0E4N0FEMUNERjU5MjZFMTQwMzcxMA0GCSqGSIb3DQEBCwUAA4IBAQBQ5e8U
1pQ40gfRoZyO0s+QF16L8pAcVLVwHffo0mmJpIRB4bu8W5IJ9kKhksJ0CzZ0YEwB
Yv7/LTC1qpi7PE73nZ4Ytm9oZeH1Kk5KIlQS+2H3IwOpoV4wucMc34y6MVQJjC7E
u5OnglBPSAopwNP4p+Us9A3ah7Ks48XCw+LCEWpA/setJwo9C13Qq2AQm8qLpogK
pNBwm/oylxb8/kZK1lLPgcXd7geLDhl/Str+aOzBXyd7YSEMxS8zWiZhWozu6rCq
dg10QFKicPGDkDzNQwBz7cDXvlJXSWRDBK5r1li1YgX+QD5WJYPchATo5SlvF4Lr
5/V/dZzI7BJTinr1
-----END CERTIFICATE-----
@@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC5QzC/hSRFvpto
V6f6lB9XFOsZcBZZS1OC4xbrCZMcMs83GTuJ2NJjot8KM/MSCYqb1Fo9mONd1mMD
JH3CXEYIIbqWZH2MHULUsj31Lu8UAVJvEy1hh/PEXpMslQARAeU/p3uq9y3rF0uC
LmZJ5uTf4mmuM/Zm27HBqhdBi7zFKmQkTck5MnyvawZL7P77KYviXI8fSO2lPvof
78M7BgegX/7gd5CzG/pY/4t8WNtLHa61vCQ9rKjjUDXESCpjV0qeM7DcvnepCClh
Nz+etPWU7WtaIJHhgTXRlzETTcuryyLnUHO858v/R0uC8RrCK7GJmTYRpY0Z4Oel
/jxt8NQlAgMBAAECggEBAKGQqTi9Ii741Ysftsze1J4bDhS3HaZt/LyO0vPS+8qB
vRiuFrBWuNRPu/en6nl2fLz9al2Bsfep+dtJvar80c5Rq+lwZdUFx7y6XTIv5iwq
IvvfW5pM+Bfu6qbcopwybP0gUMd+nTsCq8UyNd6JUFy3lbZodqCpQ9JDRdjRxqSi
oMonve36Q9kJyDs0mS2F0w+Y9oYFtpE9WvGMBthwT7VfzWuDa+YC/Gh0RhT2V9OG
uDC8kxmfaIbk0IITDVFWVMMhScXiqmJL1II9yQWsCHH73dZS+dDxwwH0ObRD3kOx
hHptxpRzhtuNxLjCOZDGhneyvUHIftpqf4wdOgQ8iUECgYEA6TDo36s3XDFIs8SH
lpVYrhie0nmaNET50CtOc5C2M1OotrkMUrQTMoa4F263UloJfqweW9AKZX11wPp2
8h7af2sZsGawjuwv7mMthZJoi0fc5J2+z6BnVGgFo7uZbcs1Y/+B0tNrDKIHVrqb
Lywmh18iKDXrMiMDMRp8mtiNyU0CgYEAy2Im0oGb1rmAh9gPvQIENmNEgRsyIU4x
WPAFb5KO7jnseO9KrC9uiRSO5JuwZu/QNGlga2w77Gu7n+Q4HfSU2KFlNhR6pKjb
mqXzPq1quryzbtWay520VsRKz3F/Tfp1FUpIuZvj8x7FrL8z8vjbLkbW4p+Z7HE4
heYdmk2KCjkCgYBC6xU48pJtc3swk2pxjSQoucoCDoFxFIY39eWKhnGy4wQcg5gE
t8Z2jnCP0ObszAfDgTMpF9eT7l/3ZCj0LGR5gkuZ8q4iJadPmsh6d1xm89VxCc+z
F43vi/ZqWTCbcGA+9RjVXnRu6KSAgAZ+kAXyfRt5PJflCGUnFznA2fo+VQKBgG6s
n7Gno/UKBv5lPdM9mKs8A5g6A2KKWWIrYtnfJ/ZNAPiiRsFCImVoEYYnjqvFTkhn
G3f5cvZpfbU5jKwIX0MvOJNgO4kE9lPGgIKX/d9WNlJW3KB21h7tPER8X6lAZJxM
SxztzvKKyUkC1zMd5q/dDqxN64X16qs6mNtR3I9JAoGBAJbqQtg59Szi2m4jZ9Xo
0cKvP7+PSKgysdWsemTkU0ji1rcMpGRTtcUGRWlawLjRSInQS+xNdL6GVU5mHZWe
gNwXIXEpape4IJta0B1PYAhNcIvOSh5CzcUnVTeo6q0CTSVQPdsivlrObLKZ4uGQ
eewDw82Ej6YoSgczjf99ug0x
-----END PRIVATE KEY-----
@@ -0,0 +1,24 @@
-----BEGIN CERTIFICATE-----
MIIEFDCCAvygAwIBAgIUKN05Gm0Q9n9mLPPltUh8qjKKegQwDQYJKoZIhvcNAQEL
BQAwXjELMAkGA1UEBhMCQ04xEzARBgNVBAoTClRlbnBheS5jb20xHTAbBgNVBAsT
FFRlbnBheS5jb20gQ0EgQ2VudGVyMRswGQYDVQQDExJUZW5wYXkuY29tIFJvb3Qg
Q0EwHhcNMjUwMzA2MTQwNDAzWhcNMzAwMzA1MTQwNDAzWjBuMRgwFgYDVQQDDA9U
ZW5wYXkuY29tIHNpZ24xEzARBgNVBAoMClRlbnBheS5jb20xHTAbBgNVBAsMFFRl
bnBheS5jb20gQ0EgQ2VudGVyMQswCQYDVQQGEwJDTjERMA8GA1UEBwwIU2hlblpo
ZW4wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDbMenF0GNlcUYErPpj
Aj+WOuzRFz+PJcrtgkI81S0WDh8flMtq1A2nwKARNyPXx+UPd9it657nH5G8E9kL
Ea5u5rENpn+JZc4cAIvZqazteSAUg2pfQH9ewW+/CxNOWarjXPNRYa0OjZWf+5F8
ZBOT/x8sJ2+jou9lxKNe1skha+jzmiQ8m3B2vPXqR87qgL/fyQjjpLvBGpvearh/
hxTMBEfGeB4F6Fu52Fz3/VCWe38ubxwj8EMcRSPp4xsl2BTaRVMMT525Hul2N49C
vXCSVsCNt3ND8UDffXnb19Zgpir4KqpECTbfqcT7YX+U34P1Av8gcDl6rRm+7Wpj
PY4RAgMBAAGjgbkwgbYwCQYDVR0TBAIwADALBgNVHQ8EBAMCA/gwgZsGA1UdHwSB
kzCBkDCBjaCBiqCBh4aBhGh0dHA6Ly9ldmNhLml0cnVzLmNvbS5jbi9wdWJsaWMv
aXRydXNjcmw/Q0E9MUJENDIyMEU1MERCQzA0QjA2QUQzOTc1NDk4NDZDMDFDM0U4
RUJEMiZzZz1IQUNDNDcxQjY1NDIyRTEyQjI3QTlEMzNBODdBRDFDREY1OTI2RTE0
MDM3MTANBgkqhkiG9w0BAQsFAAOCAQEAUAvXCx55ZGmd3ATi28W4mirnUHQb2RAw
X5/0OC2SkuBdzxI5RbY4YAuugP6nQIAU8TXCANzno2oqZ6vmFZ4dBnJmLewVFMoM
qp18A4RWmnsGnmEIgMm6N1kB+RbM11oLCLdy2KK0SvmqDmTBFJyjyUGqEmRshnV/
QfKn4UUzJ4suDFAolGX4DyDIOtM0D+SCHwwxGI4yf2+nj4IB8XgmmOeschxoSvu9
T3E6BgQ271YVDwg+3r9CiFVI5JdyftvQagOEQmZJMPINEKgFn5LzrZJW7mH42+N+
i4RFz04wpGUsrSFDESh3JxYSNhq/HulpMLinntWxEBHFhpkjyyGDGg==
-----END CERTIFICATE-----
@@ -0,0 +1,71 @@
package com.budwk.app.base.payment.wxpay.service;
import com.budwk.app.base.payment.wxpay.vo.WxPayNeedParamVo;
import com.budwk.app.base.result.Result;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* @version 1.0
* @Author zzr
* @name:WxPayCommonService
* @Date 2025/3/6 11:40
* @注释
*/
public interface WxPayCommonService {
/**
* 二维码统一下单,返回支付二维码链接
* @param paramVo 需要传递的信息,请自定义付款金额Amount 和 付款描述Description,其他关键信息请勿修改
* 注意Amount的总价total是int类型,会自动乘以0.01,例如传递100元,会自动转换为1元
* @return
*/
Result nativePay(WxPayNeedParamVo paramVo);
/**
* JSAPI支付,返回支付参数
* @param paramVo 需要传递的信息,请自定义付款金额Amount 和 付款描述Description,其他关键信息请勿修改
* 注意Amount的总价total是int类型,会自动乘以0.01,例如传递100元,会自动转换为1元
* JsAPI支付,需要传递openId,请自行获取
* @return
*/
Result jsApiPay(WxPayNeedParamVo paramVo);
/**
* WAP支付,返回支付参数
* @param paramVo 需要传递的信息,请自定义付款金额Amount 和 付款描述Description,其他关键信息请勿修改
* 注意Amount的总价total是int类型,会自动乘以0.01,例如传递100元,会自动转换为1元
* @return
*/
Result wapPay(WxPayNeedParamVo paramVo);
/**
* 支付回调,验证是否支付成功
* @param request 请求
* @param response 响应
*/
void payNotify(HttpServletRequest request, HttpServletResponse response);
/**
* 退款
* @param transactionId 微信订单号
* @param outTradeNo 商户订单号
* @return
*/
Result refund(String transactionId, String outTradeNo);
/**
* 退款回调
* @param request 请求
* @param response 响应
*/
void refundNotify(HttpServletRequest request, HttpServletResponse response);
}
@@ -0,0 +1,612 @@
package com.budwk.app.base.payment.wxpay.service.impl;
import cn.hutool.core.date.DatePattern;
import cn.hutool.core.date.DateTime;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.ContentType;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.budwk.app.base.payment.enums.*;
import com.budwk.app.base.payment.model.PaymentRefundRecord;
import com.budwk.app.base.payment.model.PaymentTransaction;
import com.budwk.app.base.payment.wxpay.bean.WxPayV3Bean;
import com.budwk.app.base.payment.wxpay.service.WxPayCommonService;
import com.budwk.app.base.payment.wxpay.vo.WxPayNeedParamVo;
import com.budwk.app.base.result.Result;
import com.budwk.app.sys.models.Sys_user;
import com.budwk.app.sys.services.SysUserService;
import com.budwk.app.sys.views.View_user;
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
import com.budwk.app.zhgh.staffmanage.memberCost.model.MemberPaymentInfo;
import com.ijpay.core.IJPayHttpResponse;
import com.ijpay.core.enums.AuthTypeEnum;
import com.ijpay.core.enums.RequestMethodEnum;
import com.ijpay.core.kit.AesUtil;
import com.ijpay.core.kit.HttpKit;
import com.ijpay.core.kit.PayKit;
import com.ijpay.core.kit.WxPayKit;
import com.ijpay.wxpay.WxPayApi;
import com.ijpay.wxpay.enums.WxDomainEnum;
import com.ijpay.wxpay.enums.v3.BasePayApiEnum;
import com.ijpay.wxpay.model.v3.*;
import lombok.extern.slf4j.Slf4j;
import org.nutz.aop.interceptor.ioc.TransAop;
import org.nutz.dao.Chain;
import org.nutz.dao.Cnd;
import org.nutz.dao.Dao;
import org.nutz.ioc.aop.Aop;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.lang.random.R;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.ByteArrayInputStream;
import java.io.FileWriter;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.nio.charset.StandardCharsets;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* @version 1.0
* @Author zzr
* @name:WxPayCommonServiceImpl
* @Date 2025/3/6 11:41
* @注释
*/
@Slf4j
@IocBean
public class WxPayCommonServiceImpl implements WxPayCommonService {
// 判断下单接口成功返回码
private final static int OK = 200;
// 1666930980 商户号
// ww3eda050a1b5ad96e 企业微信
// wxf12fa6175ec7c75f 微信服务号
// 微信V3必要的支付参数,后期优化
private final static WxPayV3Bean WX_PAY_V3_BEAN = new WxPayV3Bean(
"wxf12fa6175ec7c75f",
"src/main/java/com/budwk/app/base/payment/wxpay/pem/apiclient_key.pem",
"src/main/java/com/budwk/app/base/payment/wxpay/pem/apiclient_key.pem",
"src/main/java/com/budwk/app/base/payment/wxpay/pem/apiclient_cert.pem",
"src/main/java/com/budwk/app/base/payment/wxpay/pem/apiclient_cert.pem",
"src/main/java/com/budwk/app/base/payment/wxpay/pem/wechatpay.pem",
"1666930980",
"",
"4ebd020883285d698c44ec50939c0967",
"https://schoolhd.top/platform",
"");
// 自定义参数,内容是统一的,但会被多次调用,直接声明
private String serialNo;
private String platSerialNo;
private final String payNotifyUrl = WX_PAY_V3_BEAN.getDomain().concat("/wxPay/payNotify");
private final String refundNotifyUrl = WX_PAY_V3_BEAN.getDomain().concat("/wxPay/refundNotify");
@Inject
private Dao dao;
@Inject
private SysUserService sysUserService;
/**
* 微信扫描二维码支付
* @param paramVo 需要传递的信息,请自定义付款金额Amount 和 付款描述Description,其他关键信息请勿修改
* 注意Amount的总价total是int类型,会自动乘以0.01,例如传递100元,会自动转换为1元
* @return
*/
@Override
@Aop(TransAop.READ_COMMITTED)
public Result nativePay(WxPayNeedParamVo paramVo) {
try {
// 商户订单号,存储当前用户的订单
String outTradeNo = PayKit.generateStr();
String timeExpire = getNowTimeExpire();
UnifiedOrderModel unifiedOrderModel = new UnifiedOrderModel()
.setAppid(WX_PAY_V3_BEAN.getAppId())
.setMchid(WX_PAY_V3_BEAN.getMchId())
// 描述信息
.setDescription(paramVo.getDescription())
.setOut_trade_no(outTradeNo)
.setTime_expire(timeExpire)
.setAttach(paramVo.getAttach())
.setNotify_url(payNotifyUrl)
// 订单金额
.setAmount(paramVo.getAmount());
log.info("二维码支付统一下单参数 {}", JSONUtil.toJsonStr(unifiedOrderModel));
IJPayHttpResponse response = WxPayApi.v3(
RequestMethodEnum.POST,
WxDomainEnum.CHINA.toString(),
BasePayApiEnum.NATIVE_PAY.toString(),
WX_PAY_V3_BEAN.getMchId(),
getSerialNumber(),
null,
WX_PAY_V3_BEAN.getKeyPath(),
JSONUtil.toJsonStr(unifiedOrderModel),
AuthTypeEnum.RSA.getCode()
);
log.info("统一下单响应 {}", response);
// 根据证书序列号查询对应的证书来验证签名结果
boolean verifySignature = WxPayKit.verifySignature(response, WX_PAY_V3_BEAN.getPlatformCertPath());
log.info("verifySignature: {}", verifySignature);
// 下单返回数据
String result = response.getBody();
JSONObject jsonObject = JSONUtil.parseObj(result);
if (response.getStatus() == OK) {
// 统一下单成功,向数据库插入订单记录
insertTradingOrder(paramVo, outTradeNo, result);
return Result.success("请在三分钟内完成支付,超时将自动取消订单").addData(result);
} else {
WxPayResultCode code = WxPayResultCode.valueOf(WxPayResultCode.class, jsonObject.getStr("code"));
return Result.error(code.getMsg());
}
} catch (Exception e) {
log.error("系统异常", e);
return Result.error(e.getMessage());
}
}
@Override
@Aop(TransAop.READ_COMMITTED)
public Result jsApiPay(WxPayNeedParamVo paramVo) {
try {
Sys_user user = dao.fetch(Sys_user.class, SecurityUtil.getUserId());
if (StrUtil.isBlank(user.getOpenId())) {
return Result.error("未获取到当前用户的openid,请重新登录");
}
// 商户订单号,存储当前用户的订单
String outTradeNo = PayKit.generateStr();
// 订单关闭时间
String timeExpire = getNowTimeExpire();
UnifiedOrderModel unifiedOrderModel = new UnifiedOrderModel()
.setAppid(WX_PAY_V3_BEAN.getAppId())
.setMchid(WX_PAY_V3_BEAN.getMchId())
.setDescription(paramVo.getDescription())
.setOut_trade_no(outTradeNo)
.setTime_expire(timeExpire)
.setAttach(paramVo.getAttach())
.setNotify_url(payNotifyUrl)
.setAmount(paramVo.getAmount())
.setPayer(new Payer().setOpenid(user.getOpenId()));
log.info("统一下单参数 {}", JSONUtil.toJsonStr(unifiedOrderModel));
IJPayHttpResponse response = WxPayApi.v3(
RequestMethodEnum.POST,
WxDomainEnum.CHINA.toString(),
BasePayApiEnum.JS_API_PAY.toString(),
WX_PAY_V3_BEAN.getMchId(),
getSerialNumber(),
null,
WX_PAY_V3_BEAN.getKeyPath(),
JSONUtil.toJsonStr(unifiedOrderModel)
);
log.info("统一下单响应 {}", response);
// 根据证书序列号查询对应的证书来验证签名结果
boolean verifySignature = WxPayKit.verifySignature(response, WX_PAY_V3_BEAN.getPlatformCertPath());
log.info("verifySignature: {}", verifySignature);
if (response.getStatus() == OK && verifySignature) {
String body = response.getBody();
JSONObject jsonObject = JSONUtil.parseObj(body);
String prepayId = jsonObject.getStr("prepay_id");
Map<String, String> map = WxPayKit.jsApiCreateSign(WX_PAY_V3_BEAN.getAppId(), prepayId, WX_PAY_V3_BEAN.getKeyPath());
log.info("唤起支付参数:{}", map);
// 唤起支付成功,向数据库插入订单参数
insertTradingOrder(paramVo, outTradeNo, body);
return Result.success().addData(JSONUtil.toJsonStr(map));
}
// return JSONUtil.toJsonStr(response);
return Result.error("唤起支付失败");
} catch (Exception e) {
log.error("系统异常", e);
return Result.error(e.getMessage());
}
}
@Override
@Aop(TransAop.READ_COMMITTED)
public Result wapPay(WxPayNeedParamVo paramVo) {
try {
// 商户订单号,存储当前用户的订单
String outTradeNo = PayKit.generateStr();
// 订单关闭时间
String timeExpire = getNowTimeExpire();
UnifiedOrderModel unifiedOrderModel = new UnifiedOrderModel()
.setAppid(WX_PAY_V3_BEAN.getAppId())
.setMchid(WX_PAY_V3_BEAN.getMchId())
.setDescription(paramVo.getDescription())
.setOut_trade_no(outTradeNo)
.setTime_expire(timeExpire)
.setAttach(paramVo.getAttach())
.setNotify_url(payNotifyUrl)
.setAmount(paramVo.getAmount())
.setScene_info(
new SceneInfo()
.setPayer_client_ip("").
setH5_info(new H5Info()
.setType("Wap")));
log.info("统一下单参数 {}", JSONUtil.toJsonStr(unifiedOrderModel));
IJPayHttpResponse response = WxPayApi.v3(
RequestMethodEnum.POST,
WxDomainEnum.CHINA.toString(),
BasePayApiEnum.H5_PAY.toString(),
WX_PAY_V3_BEAN.getMchId(),
getSerialNumber(),
null,
WX_PAY_V3_BEAN.getKeyPath(),
JSONUtil.toJsonStr(unifiedOrderModel)
);
log.info("统一下单响应 {}", response);
// 根据证书序列号查询对应的证书来验证签名结果
boolean verifySignature = WxPayKit.verifySignature(response, WX_PAY_V3_BEAN.getPlatformCertPath());
log.info("verifySignature: {}", verifySignature);
if (response.getStatus() == OK && verifySignature) {
String body = response.getBody();
insertTradingOrder(paramVo, outTradeNo, body);
return Result.success(JSONUtil.toJsonStr(body));
}
// return JSONUtil.toJsonStr(response);
return Result.error();
} catch (Exception e) {
log.error("系统异常", e);
return Result.error(e.getMessage());
}
}
/**
* 支付回调
* @param request
* @param response
*/
@Override
public void payNotify(HttpServletRequest request, HttpServletResponse response) {
Map<String, String> map = new HashMap<>(12);
try {
String timestamp = request.getHeader("Wechatpay-Timestamp");
String nonce = request.getHeader("Wechatpay-Nonce");
String serialNo = request.getHeader("Wechatpay-Serial");
String signature = request.getHeader("Wechatpay-Signature");
log.info("timestamp:{} nonce:{} serialNo:{} signature:{}", timestamp, nonce, serialNo, signature);
String result = HttpKit.readData(request);
log.info("支付通知密文 {}", result);
// String plainText = null;
// 需要通过证书序列号查找对应的证书,verifyNotify 中有验证证书的序列号
String plainText = WxPayKit.verifyNotify(serialNo, result, signature, nonce, timestamp,
WX_PAY_V3_BEAN.getApiKey3(), WX_PAY_V3_BEAN.getPlatformCertPath());
// 微信公钥验证签名并解密
// if (ObjectUtil.equals(serialNo, WX_PAY_V3_BEAN.getPublicKeyId())) {
// plainText = WxPayKit.verifyPublicKeyNotify(result, signature, nonce, timestamp,
// WX_PAY_V3_BEAN.getApiKey3(), WX_PAY_V3_BEAN.getPlatformCertPath());
// }
log.info("支付通知明文 {}", plainText);
if (StrUtil.isNotEmpty(plainText)) {
// 支付结果
JSONObject jsonResult = JSONUtil.parseObj(plainText);
// 获取缴费记录,并修改缴费结果
PaymentTransaction transaction = sysUserService.dao().fetch(PaymentTransaction.class, Cnd.where("outTradeNo", "=", jsonResult.getStr("out_trade_no")));
transaction.setTradingState(TradingStateEnum.PAY_SUCCESS.name());
transaction.setTransactionId(jsonResult.getStr("transaction_id"));
sysUserService.dao().update(transaction);
sysUserService.dao().update(MemberPaymentInfo.class,
Chain.make("isPayment", true).add("paymentTime", DateUtil.date())
.add("paymentMoney", transaction.getTradingAmount()),
Cnd.where("id", "=", transaction.getMemberPaymentInfoId())
);
response.setStatus(200);
map.put("code", "SUCCESS");
map.put("message", "SUCCESS");
} else {
response.setStatus(500);
map.put("code", "ERROR");
map.put("message", "签名错误");
log.error("签名错误");
}
// 支付回调,返回给微信,自己也接收不到结果,这里的返回没用
response.setHeader("Content-type", ContentType.JSON.toString());
response.getOutputStream().write(JSONUtil.toJsonStr(map).getBytes(StandardCharsets.UTF_8));
response.flushBuffer();
} catch (Exception e) {
log.error("系统异常", e);
}
}
/**
* 退款
* @param transactionId 微信订单号
* @param outTradeNo 商户订单号
* @return
*/
@Override
public Result refund(String transactionId, String outTradeNo) {
try {
// 查询商户订单
PaymentTransaction transaction = sysUserService.dao().fetch(PaymentTransaction.class, Cnd.where("outTradeNo", "=", outTradeNo));
Integer tradingAmount = transaction.getTradingAmount();
String outRefundNo = PayKit.generateStr();
log.info("商户退款单号: {}", outRefundNo);
// 没用
// List<RefundGoodsDetail> list = new ArrayList<>();
// RefundGoodsDetail refundGoodsDetail = new RefundGoodsDetail()
// .setMerchant_goods_id("123")
// .setGoods_name("测试")
// .setUnit_price(1)
// .setRefund_amount(1)
// .setRefund_quantity(1);
// list.add(refundGoodsDetail);
RefundModel refundModel = new RefundModel()
.setOut_refund_no(outRefundNo)
.setReason(transaction.getDescription() + "退款")
.setNotify_url(refundNotifyUrl)
.setAmount(new RefundAmount().setRefund(tradingAmount).setTotal(tradingAmount).setCurrency("CNY"));
// .setGoods_detail(list);
if (StrUtil.isNotEmpty(transactionId)) {
refundModel.setTransaction_id(transactionId);
}
if (StrUtil.isNotEmpty(outTradeNo)) {
refundModel.setOut_trade_no(outTradeNo);
}
log.info("退款参数 {}", JSONUtil.toJsonStr(refundModel));
IJPayHttpResponse response = WxPayApi.v3(
RequestMethodEnum.POST,
WxDomainEnum.CHINA.toString(),
BasePayApiEnum.REFUND.toString(),
WX_PAY_V3_BEAN.getMchId(),
getSerialNumber(),
null,
WX_PAY_V3_BEAN.getKeyPath(),
JSONUtil.toJsonStr(refundModel)
);
// 根据证书序列号查询对应的证书来验证签名结果
boolean verifySignature = WxPayKit.verifySignature(response, WX_PAY_V3_BEAN.getPlatformCertPath());
// 微信支付公钥验证签名
// boolean verifySignature = WxPayKit.verifyPublicKeySignature(response, WX_PAY_V3_BEAN.getPlatformCertPath());
log.info("verifySignature: {}", verifySignature);
log.info("退款响应 {}", response);
if (verifySignature) {
// 用户发起退款,存一个退款记录
PaymentRefundRecord record = new PaymentRefundRecord();
record.setId(R.UU32());
record.setPaymentTransactionId(transaction.getId());
record.setOutRefundTradeNo(outRefundNo);
record.setRefundNo(null);
record.setRefundState(RefundStateEnum.REFUNDING.name());
record.setRefundAmount(tradingAmount);
record.setTotal(tradingAmount);
record.setResultJson(response.getBody());
dao.insert(record);
dao.update(MemberPaymentInfo.class, Chain.make("isRefund", true)
.add("refundState", RefundStateEnum.REFUNDING.name()),
Cnd.where("id", "=", transaction.getMemberPaymentInfoId()));
// 返回退款下单结果
return Result.success();
}
return Result.error("退款失败");
} catch (Exception e) {
log.error("系统异常", e);
return Result.error(e.getMessage());
}
}
/**
* 退款回调
* @param request
* @param response
*/
@Override
@Aop(TransAop.READ_COMMITTED)
public void refundNotify(HttpServletRequest request, HttpServletResponse response) {
Map<String, String> map = new HashMap<>(12);
try {
String timestamp = request.getHeader("Wechatpay-Timestamp");
String nonce = request.getHeader("Wechatpay-Nonce");
String serialNo = request.getHeader("Wechatpay-Serial");
String signature = request.getHeader("Wechatpay-Signature");
log.info("退款通知 timestamp:{} nonce:{} serialNo:{} signature:{}", timestamp, nonce, serialNo, signature);
String result = HttpKit.readData(request);
log.info("退款通知密文 {}", result);
// String plainText = null;
// 需要通过证书序列号查找对应的证书,verifyNotify 中有验证证书的序列号
String plainText = WxPayKit.verifyNotify(serialNo, result, signature, nonce, timestamp,
WX_PAY_V3_BEAN.getApiKey3(), WX_PAY_V3_BEAN.getPlatformCertPath());
// 微信公钥验证签名并解密
// if (ObjectUtil.equals(serialNo, WX_PAY_V3_BEAN.getPublicKeyId())) {
// plainText = WxPayKit.verifyPublicKeyNotify(result, signature, nonce, timestamp,
// WX_PAY_V3_BEAN.getApiKey3(), WX_PAY_V3_BEAN.getPlatformCertPath());
// }
log.info("退款通知明文 {}", plainText);
if (StrUtil.isNotEmpty(plainText)) {
// 取出明文,修改退款的结果
JSONObject jsonObject = JSONUtil.parseObj(plainText);
// 查询出退款记录,存储关键信息并修改
PaymentRefundRecord record = dao.fetch(PaymentRefundRecord.class, Cnd.where("outRefundTradeNo", "=", jsonObject.getStr("out_refund_no")));
record.setRefundNo(jsonObject.getStr("refund_id"));
record.setRefundState(RefundStateEnum.REFUND_SUCCESS.name());
// 到此是退款成功了,还要去修改订单表的数据
PaymentTransaction transaction = dao.fetch(PaymentTransaction.class, record.getPaymentTransactionId());
dao.update(PaymentTransaction.class, Chain.make("isRefund", true)
.add("refundAmount", record.getRefundAmount()),
Cnd.where("id", "=", record.getPaymentTransactionId()));
dao.update(MemberPaymentInfo.class, Chain.make("isPayment", null).add("paymentMoney", null)
.add("refundState", RefundStateEnum.REFUND_SUCCESS.name()),
Cnd.where("id", "=", transaction.getMemberPaymentInfoId()));
dao.update(record);
response.setStatus(200);
map.put("code", "SUCCESS");
map.put("message", "SUCCESS");
} else {
response.setStatus(500);
map.put("code", "ERROR");
map.put("message", "签名错误");
}
// 没用
response.setHeader("Content-type", ContentType.JSON.toString());
response.getOutputStream().write(JSONUtil.toJsonStr(map).getBytes(StandardCharsets.UTF_8));
response.flushBuffer();
} catch (Exception e) {
log.error("系统异常", e);
}
}
/**
* 公共方法,获取证书序列号
* @return
*/
private String getSerialNumber() {
if (StrUtil.isEmpty(serialNo)) {
// 获取证书序列号
X509Certificate certificate = PayKit.getCertificate(WX_PAY_V3_BEAN.getCertPath());
if (null != certificate) {
serialNo = certificate.getSerialNumber().toString(16).toUpperCase();
// 提前两天检查证书是否有效
boolean isValid = PayKit.checkCertificateIsValid(certificate, WX_PAY_V3_BEAN.getMchId(), -2);
log.info("证书是否可用 {} 证书有效期为 {}", isValid, DateUtil.format(certificate.getNotAfter(), DatePattern.NORM_DATETIME_PATTERN));
}
// System.out.println("输出证书信息:\n" + certificate.toString());
// // 输出关键信息,截取部分并进行标记
// System.out.println("证书序列号:" + certificate.getSerialNumber().toString(16));
// System.out.println("版本号:" + certificate.getVersion());
// System.out.println("签发者:" + certificate.getIssuerDN());
// System.out.println("有效起始日期:" + certificate.getNotBefore());
// System.out.println("有效终止日期:" + certificate.getNotAfter());
// System.out.println("主体名:" + certificate.getSubjectDN());
// System.out.println("签名算法:" + certificate.getSigAlgName());
// System.out.println("签名:" + certificate.getSignature().toString());
}
System.out.println("serialNo:" + serialNo);
return serialNo;
}
/**
* 公共方法,获取平台证书序列号
* @return
*/
private String getPlatSerialNumber() {
if (StrUtil.isEmpty(platSerialNo)) {
// 获取平台证书序列号
X509Certificate certificate = PayKit.getCertificate(FileUtil.getInputStream(WX_PAY_V3_BEAN.getPlatformCertPath()));
platSerialNo = certificate.getSerialNumber().toString(16).toUpperCase();
}
System.out.println("platSerialNo:" + platSerialNo);
return platSerialNo;
}
/**
* 公共方法,保存平台证书
* @param associatedData
* @param nonce
* @param cipherText
* @param algorithm
* @param certPath
* @return
*/
private String savePlatformCert(String associatedData, String nonce, String cipherText, String algorithm, String certPath) {
try {
String key3 = WX_PAY_V3_BEAN.getApiKey3();
String publicKey;
if (StrUtil.equals(algorithm, AuthTypeEnum.SM2.getPlatformCertAlgorithm())) {
publicKey = PayKit.sm4DecryptToString(key3, cipherText, nonce, associatedData);
} else {
AesUtil aesUtil = new AesUtil(WX_PAY_V3_BEAN.getApiKey3().getBytes(StandardCharsets.UTF_8));
// 平台证书密文解密
// encrypt_certificate 中的 associated_data nonce ciphertext
publicKey = aesUtil.decryptToString(
associatedData.getBytes(StandardCharsets.UTF_8),
nonce.getBytes(StandardCharsets.UTF_8),
cipherText
);
}
if (StrUtil.isNotEmpty(publicKey)) {
// 保存证书
FileWriter writer = new FileWriter(certPath);
writer.write(publicKey);
// 获取平台证书序列号
X509Certificate certificate = PayKit.getCertificate(new ByteArrayInputStream(publicKey.getBytes()));
return certificate.getSerialNumber().toString(16).toUpperCase();
}
return "";
} catch (Exception e) {
log.error("保存平台证书异常", e);
return e.getMessage();
}
}
/**
* 公共方法,构建订单关闭时间,默认3分钟
* @return
*/
private String getNowTimeExpire() {
long currentTimeMillis = System.currentTimeMillis() + 1000 * 60 * 3;
// 将时间戳转换为 DateTime 对象
DateTime dateTime = DateUtil.date(currentTimeMillis);
return DateUtil.format(dateTime, "yyyy-MM-dd'T'HH:mm:ssXXX");
}
/**
* 公共方法,插入交易订单
* @param paramVo 交易必填参数Vo
* @param outTradeNo 商户交易号
* @param result 下单返回结构
*/
private void insertTradingOrder(WxPayNeedParamVo paramVo, String outTradeNo, String result){
View_user user = sysUserService.dao().fetch(View_user.class, Cnd.where("id", "=", SecurityUtil.getUserId()));
PaymentTransaction transaction = new PaymentTransaction();
transaction.setId(R.UU32());
transaction.setMchId(WX_PAY_V3_BEAN.getMchId());
transaction.setOutTradeNo(outTradeNo);
transaction.setTransactionId(null);
transaction.setTradingChannel(TradingChannelEnum.WX_PAY.name());
transaction.setTradingType(TradingTypeEnum.PAYMENT.name());
transaction.setUserId(user.getId());
transaction.setLoginName(user.getLoginname());
transaction.setUserName(user.getUsername());
transaction.setUnitId(user.getUnitId());
transaction.setUnitName(user.getUnitName());
transaction.setUnionId(user.getUnionId());
transaction.setUnionName(user.getUnionName());
transaction.setTradingState(TradingStateEnum.WAITING_FOR_PAYMENT.name());
// 获取订单金额
transaction.setTradingAmount(paramVo.getAmount().getTotal());
transaction.setIsRefund(false);
transaction.setRefundAmount(null);
transaction.setDescription(paramVo.getDescription());
transaction.setAttach(paramVo.getAttach());
transaction.setOpenId(null);
transaction.setResultJson(result);
transaction.setMemberPaymentInfoId(paramVo.getMemberPaymentInfoId());
sysUserService.dao().insert(transaction);
}
}
@@ -0,0 +1,132 @@
package com.budwk.app.base.payment.wxpay.util;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.HttpUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.budwk.app.base.constant.RedisConstant;
import lombok.extern.slf4j.Slf4j;
import org.nutz.http.Http;
import org.nutz.integration.jedis.RedisService;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.json.Json;
import org.nutz.lang.util.NutMap;
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
import java.net.URLEncoder;
import java.util.Base64;
/**
* @version 1.0
* @Author zzr
* @name:WeChatUtils
* @Date 2025/3/10 14:13
* @注释
*/
@Slf4j
@IocBean
public class WeChatUtils {
private static final String ALGORITHM = "AES";
private static final String KEY = "4eXvXq1ZJmZj8z5z6yXvXq1ZJmZj8z5z";
@Inject
private RedisService redisService;
public static final String APPID = "wxf12fa6175ec7c75f";
public static final String SECRET = "f65bbb41d3de4a93c8c93d8e2fd50369";
// 获取code
public final static String CODE_URL = "https://open.weixin.qq.com/connect/oauth2/authorize?appid=APPID&redirect_uri=REDIRECT_URI&response_type=code&scope=snsapi_userinfo&state=STATE#wechat_redirect";
//获取网页access_token的接口地址
public final static String WEB_ACCESS_TOKEN_URL = "https://api.weixin.qq.com/sns/oauth2/access_token?appid=APPID&secret=APPSECRET&code=CODE&grant_type=authorization_code";
//获取用户信息地址
public final static String USERINFO_URL = "https://api.weixin.qq.com/sns/userinfo?access_token=ACCESS_TOKEN&openid=OPENID&lang=zh_CN";
// 获取access_token的接口地址(GET) 限2000(次/天)
public final static String ACCESS_TOKEN_URL = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=APPID&secret=APPSECRET";
// 发送模板消息
public static final String SEND_MESSAGE_URL = "https://api.weixin.qq.com/cgi-bin/message/template/send?access_token=ACCESS_TOKEN";
/**
* 生成获取code的url
*
* @param redirectUrl
* @return
*/
public String getCodeUrl(String redirectUrl, String state) {
String requestUrl = null;
state = state == null ? "" : state;
try {
requestUrl = CODE_URL.replace("APPID", APPID).replace("REDIRECT_URI", URLEncoder.encode(redirectUrl, "UTF-8")).replace("STATE", state);
} catch (Exception e) {
e.printStackTrace();
}
return requestUrl;
}
/**
* 获取微信网页access_token 和 openid
*
* @param code
* @return
*/
public JSONObject getWebAccessTokenAndOpenid(String code) {
String requestUrl = WEB_ACCESS_TOKEN_URL.replace("APPID", APPID).replace("APPSECRET", SECRET).replace("CODE", code);
return JSONUtil.parseObj(HttpUtil.post(requestUrl, new JSONObject()));
}
/**
* 网页授权作用域为snsapi_userinfo,则此时开发者可以通过access_token和openid拉取用户信息了
* <a href="https://developers.weixin.qq.com/doc/offiaccount/OA_Web_Apps/Wechat_webpage_authorization.html">...</a>
*
* @param webAccessToken
* @param openid
* @return
*/
public JSONObject getUserInfo(String webAccessToken, String openid) {
String requestUrl = USERINFO_URL.replace("ACCESS_TOKEN", webAccessToken).replace("OPENID", openid);
return JSONUtil.parseObj(HttpUtil.post(requestUrl, new JSONObject()));
}
/**
* 获取access_token 先从Redis获取,如果没有再调微信获取并存Redis
*
* @return
*/
public String getAccessToken() {
String token = redisService.get(RedisConstant.REDIS_KEY_WX_TOKEN);
if (StrUtil.isNotBlank(token)) {
return token;
}
String accessTokenUrl = ACCESS_TOKEN_URL.formatted(APPID, SECRET);
String accessToken = Json.fromJson(NutMap.class, Http.get(accessTokenUrl).getContent()).getString("access_token");
redisService.setex(RedisConstant.REDIS_KEY_WX_TOKEN, 60 * 60 * 2, accessToken);
return accessToken;
}
public String encrypt(String data) throws Exception {
SecretKeySpec keySpec = new SecretKeySpec(KEY.getBytes(), ALGORITHM);
Cipher cipher = Cipher.getInstance(ALGORITHM);
cipher.init(Cipher.ENCRYPT_MODE, keySpec);
byte[] encryptedBytes = cipher.doFinal(data.getBytes());
return Base64.getEncoder().encodeToString(encryptedBytes);
}
public String decrypt(String encryptedData) throws Exception {
SecretKeySpec keySpec = new SecretKeySpec(KEY.getBytes(), ALGORITHM);
Cipher cipher = Cipher.getInstance(ALGORITHM);
cipher.init(Cipher.DECRYPT_MODE, keySpec);
byte[] decryptedBytes = cipher.doFinal(Base64.getDecoder().decode(encryptedData));
return new String(decryptedBytes);
}
}
@@ -0,0 +1,37 @@
package com.budwk.app.base.payment.wxpay.vo;
import com.ijpay.wxpay.model.v3.Amount;
import com.ijpay.wxpay.model.v3.Payer;
import lombok.Data;
/**
* @version 1.0
* @Author zzr
* @name:WxPayNeedParamVo
* @Date 2025/3/7 18:43
* @注释
*/
@Data
public class WxPayNeedParamVo {
/**
* 订单描述,会显示在支付界面的标题,最大长度127,必填
*/
private String description;
/**
* 附加数据,订单的详细描述信息,不必填
*/
private String attach;
/**
* 金额对象,对象中的 total 是金额,必填;
* 其余参数可不传递也可不填
*/
private Amount amount;
/**
* 会员支付信息ID,必填
*/
private String memberPaymentInfoId;
}
@@ -0,0 +1,20 @@
package com.budwk.app.base.portal;
import com.budwk.app.base.portal.model.PortalTodoResponse;
/** 成功发送记录的持久化服务,不负责再次请求门户。 */
public interface PortalTodoLogService {
/**
* 保存一次已确认成功的发送快照;使用当前业务事务,外层回滚时记录也可能回滚。
* @param requestId 本次调用的32位关联编号,对应日志主键
* @param requestUrl 实际发送接口地址,不传认证头
* @param requestBody 实际发送的完整JSON字符串
* @param responseBody 门户原始响应正文
* @param response 解析后的成功结果,含HTTP状态、业务码、提示、data及traceId
* @param requestTime 请求开始的毫秒时间戳
* @param elapsedMillis 接口耗时,不含数据库保存时间
* @return 无返回值;保存异常由调用端单独处理,不改变门户调用结果
*/
void recordSuccess(String requestId, String requestUrl, String requestBody, String responseBody,
PortalTodoResponse response, long requestTime, long elapsedMillis);
}
@@ -0,0 +1,26 @@
package com.budwk.app.base.portal;
import com.budwk.app.base.portal.model.PortalTodoRequest;
import com.budwk.app.base.portal.model.PortalTodoSendRequest;
import com.budwk.app.base.portal.model.PortalTodoResponse;
/** 信息门户待办接口,可在业务 service 中注入后独立调用。 */
public interface PortalTodoService {
/**
* @param request 业务名称、唯一 ID、单据编码、接收人登录账号及标题、正文、完整 PC 链接
* @return 调用结果;success 表示门户成功,skipped 表示开关拦截,msg/traceId 用于排查
*/
PortalTodoResponse sendTodo(PortalTodoSendRequest request);
/**
* @param request 与发送时完全一致的业务名称、唯一 ID、单据编码和接收人账号
* @return 调用结果;审核完成使用此方法,门户保留已办记录
*/
PortalTodoResponse doneTodo(PortalTodoRequest request);
/**
* @param request 与发送时完全一致的业务名称、唯一 ID、单据编码和接收人账号
* @return 调用结果;撤销或删除业务时使用,失败不会抛出网络异常阻断业务
*/
PortalTodoResponse removeTodo(PortalTodoRequest request);
}
@@ -0,0 +1,58 @@
# 门户待办调用
在业务 service 中注入 `PortalTodoService`。三个方法均返回 `PortalTodoResponse`,不会因门户 HTTP 失败主动抛出异常。
```java
@Inject
private PortalTodoService portalTodoService;
// 同一待办发送、完成、删除时保留下面四项定位数据。
PortalTodoSendRequest request = new PortalTodoSendRequest();
request.setEntityName("职工福利审核");
request.setEntityId(taskId);
request.setEntityNumber(businessNumber);
request.setTargets(List.of(receiverLoginName));
request.setSubject("职工福利审核待办");
request.setContent("您有一条职工福利申请需要审核。");
request.setLink(auditPageUrl);
request.setMobileLink(h5AuditPageUrl);
request.setCreator(senderLoginName);
request.setCreatorName(senderName);
PortalTodoResponse result = portalTodoService.sendTodo(request);
```
审核完成时调用 `portalTodoService.doneTodo(request)`;撤销或删除时调用 `portalTodoService.removeTodo(request)`。这两个操作也可单独构造 `PortalTodoRequest`,只设置四项定位数据,不需要标题和内容。
- `success=true`:门户接口返回成功,实际展示仍以门户为准。
- `skipped=true`:开发模式或开关关闭,没有发送请求。
- 两者均为 `false`:校验、网络或门户业务失败,使用 `msg`、`code`、`traceId` 排查。
- `data` 按门户原值返回,不当作 `notifyId` 使用。
## 启用配置
1. 部署配置设置 `portal.todo.base-url`、`portal.todo.username`、`portal.todo.password`。密码按原始字符填写,不编码、不转义增删。
2. 按当前项目约定,`Globals.sso` 必须为 `true`(来自 `cas.enable`)。
3. 在现有系统参数管理中配置 `AppPortalTodo=true` 并刷新系统配置;未配置默认关闭,与 `AppSms` 无关。
4. 设置正确的项目 `AppDomain`,供 wf 构建审核链接。
wf 会自动同步通用任务变化,业务代码无需再次发送同一 wf 待办。HTTP 超时不自动重试;本地事务无法撤销已成功的外部调用,需要根据日志人工补偿。
PC链接使用任务的 `formKey`,手机链接使用 `h5FormKey`,补齐 `AppDomain` 并携带编码后的 `taskId`、`bizId`、`taskKey`。未配置手机表单时记录提示并回退到 `/platform/h5` 手机首页。独立调用的 `mobileLink` 应传完整HTTP(S)地址,未传也回退到项目手机首页。修改只影响新发送的待办,不更新门户已有链接。
## 发送记录与控制台排查
成功的 `send` 请求写入 `portal_todo_log`,每次请求一条,保存实际发送的完整JSON、原始响应正文、业务标识、标题正文、发送人、接收人账号数组、链接、请求时间、耗时及返回状态。失败、跳过、已办和删除不写成功发送记录。
控制台搜索 `——————————————学校OA待办发送`,按“调用编号”关联“请求开始”和“请求结束”。结束日志包含成功/失败/跳过、HTTP状态、业务码、提示、traceId、耗时和响应正文;无响应的网络异常仍输出结束记录。日志不打印Basic凭证或认证头。
成功发送但保存记录失败时另输出“发送成功但记录入库失败”,门户返回结果保持成功,不自动重发。保存沿用业务事务,外层回滚时记录也可能回滚;控制台“已写入当前事务”不表示外层事务已提交。
表结构通过现有实体扫描机制管理:`MainLauncher.init_sys` 仅在DEBUG日志级别时自动建表。生产部署需确认该表已创建,本功能不生成或执行额外SQL,不提供后台查询页面。
## 验证说明
`PortalTodoServiceTest` 使用本地模拟 HTTP 服务;`FlowPortalTodoServiceTest` 使用内存 Dao 替身。两者不连接真实门户或业务数据库。
串并行会签、转办、撤回和流程删除仍需在测试部署中走完整业务流程验收。提案附议等脱离标准任务状态的业务特例未单独适配。
现有 `activate` 方法只恢复 `INTERRUPT` 状态任务,并不恢复 `PENDING`;本次保持该业务规则,仅为实际恢复的任务重发待办。
@@ -0,0 +1,56 @@
package com.budwk.app.base.portal.impl;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.budwk.app.base.portal.PortalTodoLogService;
import com.budwk.app.base.portal.model.PortalTodoLog;
import com.budwk.app.base.portal.model.PortalTodoResponse;
import org.nutz.aop.interceptor.ioc.TransAop;
import org.nutz.dao.Dao;
import org.nutz.ioc.aop.Aop;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
/** 记录实际请求报文的业务快照,不读取可变化的人员或流程数据。 */
@IocBean
public class PortalTodoLogServiceImpl implements PortalTodoLogService {
@Inject
private Dao dao;
@Override
@Aop(TransAop.READ_COMMITTED)
public void recordSuccess(String requestId, String requestUrl, String requestBody, String responseBody,
PortalTodoResponse response, long requestTime, long elapsedMillis) {
// 防止跳过或HTTP失败被误记为成功发送;多接收人作为一次请求保留原数组。
if (response == null || !response.isSuccess() || response.isSkipped()
|| response.getHttpStatus() == null || response.getHttpStatus() < 200
|| response.getHttpStatus() >= 300) return;
JSONObject payload = JSONUtil.parseObj(requestBody);
PortalTodoLog record = new PortalTodoLog();
record.setId(requestId);
record.setAppName(payload.getStr("appName"));
record.setModuleName(payload.getStr("moduleName"));
record.setEntityName(payload.getStr("entityName"));
record.setEntityId(payload.getStr("entityId"));
record.setEntityNumber(payload.getStr("entityNumber"));
record.setSubject(payload.getStr("subject"));
record.setContent(payload.getStr("content"));
record.setCreator(payload.getStr("creator"));
record.setCreatorName(payload.getStr("creatorName"));
record.setTargets(JSONUtil.toJsonStr(payload.getJSONArray("targets")));
record.setLink(payload.getStr("link"));
record.setMobileLink(payload.getStr("mobileLink"));
record.setRequestUrl(requestUrl);
record.setRequestBody(requestBody);
record.setResponseBody(responseBody);
record.setRequestTime(requestTime);
record.setElapsedMillis(elapsedMillis);
record.setHttpStatus(response.getHttpStatus());
record.setSuccess(response.isSuccess());
record.setCode(response.getCode());
record.setMsg(response.getMsg());
record.setData(response.getData());
record.setTraceId(response.getTraceId());
dao.insert(record);
}
}
@@ -0,0 +1,193 @@
package com.budwk.app.base.portal.impl;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.budwk.app.base.portal.PortalTodoService;
import com.budwk.app.base.portal.PortalTodoLogService;
import com.budwk.app.base.portal.model.*;
import com.budwk.app.web.commons.base.Globals;
import lombok.extern.slf4j.Slf4j;
import org.nutz.aop.interceptor.ioc.TransAop;
import org.nutz.ioc.aop.Aop;
import org.nutz.ioc.impl.PropertiesProxy;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
/** Basic 认证的门户 REST 客户端,所有操作共用环境拦截及结果处理。 */
@IocBean
@Slf4j
public class PortalTodoServiceImpl implements PortalTodoService {
private static final String LOG_PREFIX = "——————————————学校OA待办发送";
@Inject
private PropertiesProxy conf;
@Inject
private PortalTodoLogService portalTodoLogService;
@Override
@Aop(TransAop.READ_COMMITTED)
public PortalTodoResponse sendTodo(PortalTodoSendRequest request) {
return post("send", request);
}
@Override
@Aop(TransAop.READ_COMMITTED)
public PortalTodoResponse doneTodo(PortalTodoRequest request) {
return post("done", request);
}
@Override
@Aop(TransAop.READ_COMMITTED)
public PortalTodoResponse removeTodo(PortalTodoRequest request) {
return post("removeTodo", request);
}
/** 先拦截环境与开关,再校验参数并请求;禁止自动重试不确定是否已送达的发送。 */
private PortalTodoResponse post(String operation, PortalTodoRequest request) {
PortalTodoResponse result = new PortalTodoResponse();
String requestId = UUID.randomUUID().toString().replace("-", "");
long requestTime = System.currentTimeMillis();
long startedNanos = System.nanoTime();
String requestUrl = null;
String requestBody = null;
String responseBody = null;
String exceptionType = null;
HttpURLConnection connection = null;
try {
// 环境与功能开关均开启才调用门户,拦截早于参数及认证配置校验。
if (!Globals.sso || !Globals.MyConfig.getBoolean("AppPortalTodo", false)) {
result.setSkipped(true);
result.setCode("SKIPPED");
result.setMsg(!Globals.sso ? "开发模式不调用门户待办" : "门户待办开关未开启");
return result;
}
JSONObject payload = payload(operation, request);
requestBody = payload.toString();
String baseUrl = required(conf.get("portal.todo.base-url"), "门户地址未配置");
String username = required(conf.get("portal.todo.username"), "门户认证用户名未配置");
String password = required(conf.get("portal.todo.password"), "门户认证密码未配置");
requestUrl = StrUtil.removeSuffix(baseUrl, "/")
+ "/openapi/sys-notifybus/sysNotifyComponent/" + operation;
// 打印实际发送的同一份JSON,不输出Basic用户名、密码或Authorization头。
log.info("{}【请求开始】调用编号={},操作={},地址={},发送内容={}",
LOG_PREFIX, requestId, operation, requestUrl, requestBody);
connection = (HttpURLConnection) URI.create(requestUrl).toURL().openConnection();
connection.setInstanceFollowRedirects(false);
connection.setRequestMethod("POST");
connection.setDoOutput(true);
connection.setUseCaches(false);
connection.setConnectTimeout(conf.getInt("portal.todo.connect-timeout", 5000));
connection.setReadTimeout(conf.getInt("portal.todo.read-timeout", 10000));
connection.setRequestProperty("Content-Type", "application/json;charset=UTF-8");
connection.setRequestProperty("Authorization", "Basic " + Base64.getEncoder()
.encodeToString((username + ":" + password).getBytes(StandardCharsets.UTF_8)));
try (OutputStream out = connection.getOutputStream()) {
out.write(requestBody.getBytes(StandardCharsets.UTF_8));
}
int status = connection.getResponseCode();
result.setHttpStatus(status);
try (InputStream in = status >= 400 ? connection.getErrorStream() : connection.getInputStream()) {
// 先保留原始响应,非JSON、解析失败等情况也能在控制台排查。
responseBody = in == null ? "" : new String(in.readAllBytes(), StandardCharsets.UTF_8);
JSONObject json = JSONUtil.parseObj(responseBody);
result.setCode(json.getStr("code"));
result.setMsg(json.getStr("msg"));
result.setData(json.getStr("data"));
result.setTraceId(json.getStr("traceId"));
result.setSuccess(status >= 200 && status < 300 && Boolean.TRUE.equals(json.getBool("success")));
}
} catch (Exception e) {
result.setSuccess(false);
result.setCode("CLIENT_ERROR");
exceptionType = e.getClass().getSimpleName();
// 异常仅记录类型,不打印可能携带认证信息的异常原文。
result.setMsg(e instanceof IllegalArgumentException ? "门户参数、配置或响应格式无效" : "门户请求失败或超时");
} finally {
long elapsedMillis = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startedNanos);
// 将多行响应编码为单行JSON字符串,确保每条日志均可用统一前缀定位。
String outcome = result.isSkipped() ? "已跳过" : result.isSuccess() ? "成功" : "失败";
String format = "{}【请求结束-{}】调用编号={},操作={},业务ID={},HTTP状态={},耗时={}ms,code={},msg={},traceId={},异常类型={},返回内容={}";
Object[] values = {LOG_PREFIX, outcome, requestId, operation,
request == null ? null : request.getEntityId(), result.getHttpStatus(), elapsedMillis,
result.getCode(), JSONUtil.toJsonStr(result.getMsg()), result.getTraceId(), exceptionType,
JSONUtil.toJsonStr(responseBody)};
if (result.isSuccess() || result.isSkipped()) log.info(format, values);
else log.error(format, values);
if (connection != null) connection.disconnect();
// 仅send成功落库。日志保存异常不能改写门户成功结果,也不能触发再次发送。
if ("send".equals(operation) && result.isSuccess() && !result.isSkipped()) {
try {
portalTodoLogService.recordSuccess(requestId, requestUrl, requestBody, responseBody,
result, requestTime, elapsedMillis);
log.info("{}【记录已写入当前事务】调用编号={},traceId={}", LOG_PREFIX, requestId, result.getTraceId());
} catch (Exception e) {
log.error("{}【发送成功但记录入库失败】调用编号={},业务ID={},traceId={},异常类型={}",
LOG_PREFIX, requestId, request.getEntityId(), result.getTraceId(), e.getClass().getSimpleName());
}
}
}
return result;
}
/** 只发送接口使用的字段;删除和已办不得混入标题、正文等发送参数。 */
private JSONObject payload(String operation, PortalTodoRequest request) {
if (request == null) throw new IllegalArgumentException("请求不能为空");
if (request.getTargets() == null || request.getTargets().isEmpty()
|| request.getTargets().stream().anyMatch(StrUtil::isBlank)) {
throw new IllegalArgumentException("接收人账号不能为空");
}
JSONObject json = new JSONObject();
json.set("appName", "智慧工会平台");
json.set("moduleName", "智慧工会平台");
json.set("entityName", required(request.getEntityName(), "业务名称不能为空"));
json.set("entityId", required(request.getEntityId(), "业务ID不能为空"));
json.set("entityNumber", required(request.getEntityNumber(), "单据编码不能为空"));
json.set("targets", request.getTargets().stream().distinct().toList());
json.set("notifyType", "todo");
json.set("orgProperty", "fdLoginName");
json.set("todoType", 1);
json.set("async", false);
if ("send".equals(operation)) {
PortalTodoSendRequest send = (PortalTodoSendRequest) request;
json.set("subject", required(send.getSubject(), "标题不能为空"));
json.set("content", required(send.getContent(), "正文不能为空"));
String link = required(send.getLink(), "PC链接不能为空");
URI uri = URI.create(link);
if (!("http".equalsIgnoreCase(uri.getScheme()) || "https".equalsIgnoreCase(uri.getScheme()))
|| uri.getHost() == null) throw new IllegalArgumentException("PC链接必须为完整HTTP地址");
json.set("link", link);
// 独立调用可传完整H5地址;未提供时使用手机首页,避免门户打开空白页。
String mobileLink = send.getMobileLink();
if (StrUtil.isBlank(mobileLink)) {
mobileLink = URI.create(StrUtil.appendIfMissing(Globals.AppDomain, "/"))
.resolve("/platform/h5").toString();
}
URI mobileUri = URI.create(mobileLink);
if (!("http".equalsIgnoreCase(mobileUri.getScheme()) || "https".equalsIgnoreCase(mobileUri.getScheme()))
|| mobileUri.getHost() == null) throw new IllegalArgumentException("手机链接必须为完整HTTP地址");
json.set("mobileLink", mobileLink);
if (StrUtil.isNotBlank(send.getCreator())) json.set("creator", send.getCreator());
if (StrUtil.isNotBlank(send.getCreatorName())) json.set("creatorName", send.getCreatorName());
json.set("queryType", 1);
json.set("todoLevel", 3);
}
return json;
}
private String required(String value, String message) {
if (StrUtil.isBlank(value)) throw new IllegalArgumentException(message);
return value;
}
}
@@ -0,0 +1,99 @@
package com.budwk.app.base.portal.model;
import com.budwk.app.base.model.BaseModel;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.nutz.dao.entity.annotation.*;
/** 每次发送成功的请求保存一条快照,多个接收人仍归属于同一次请求。 */
@Data
@EqualsAndHashCode(callSuper = true)
@Table("portal_todo_log")
@TableMeta("{'mysql-charset':'utf8mb4'}")
@Comment("学校OA待办成功发送记录")
public class PortalTodoLog extends BaseModel {
@Name
@ColDefine(type = ColType.VARCHAR, width = 32)
@Comment("请求关联编号,与控制台调用编号一致")
private String id;
@Column
@Comment("来源系统")
private String appName;
@Column
@Comment("来源模块")
private String moduleName;
@Column
@Comment("来源业务名称")
private String entityName;
@Column
@Comment("业务记录或wf任务ID")
private String entityId;
@Column
@Comment("业务单据编码")
private String entityNumber;
@Column
@ColDefine(type = ColType.TEXT)
@Comment("待办标题")
private String subject;
@Column
@ColDefine(customType = "MEDIUMTEXT")
@Comment("待办正文")
private String content;
@Column
@Comment("发送人登录账号")
private String creator;
@Column
@Comment("发送人姓名")
private String creatorName;
@Column
@ColDefine(customType = "MEDIUMTEXT")
@Comment("接收人登录账号JSON数组,保留工号前导零")
private String targets;
@Column
@ColDefine(type = ColType.TEXT)
@Comment("PC跳转地址")
private String link;
@Column
@ColDefine(type = ColType.TEXT)
@Comment("移动端跳转地址")
private String mobileLink;
@Column
@ColDefine(type = ColType.TEXT)
@Comment("发送接口地址,不含认证信息")
private String requestUrl;
@Column
@ColDefine(customType = "MEDIUMTEXT")
@Comment("实际发送的完整JSON报文")
private String requestBody;
@Column
@ColDefine(customType = "MEDIUMTEXT")
@Comment("门户完整响应正文")
private String responseBody;
@Column
@Comment("请求开始时间,毫秒时间戳")
private Long requestTime;
@Column
@Comment("接口调用耗时,毫秒,不含日志入库")
private Long elapsedMillis;
@Column
@Comment("HTTP响应状态码")
private Integer httpStatus;
@Column
@Comment("门户是否返回成功,不代表接收人已查看")
private Boolean success;
@Column
@Comment("门户业务状态码")
private String code;
@Column
@ColDefine(type = ColType.TEXT)
@Comment("门户返回提示")
private String msg;
@Column
@ColDefine(customType = "MEDIUMTEXT")
@Comment("门户原样返回的数据,不推断为notifyId")
private String data;
@Column
@Comment("门户链路追踪编号")
private String traceId;
}
@@ -0,0 +1,17 @@
package com.budwk.app.base.portal.model;
import lombok.Data;
import java.util.List;
/** 门户待办定位参数;发送、已办和删除时必须使用同一组业务标识及人员账号。 */
@Data
public class PortalTodoRequest {
/** 来源业务名称,同一待办生命周期内不得更改。 */
private String entityName;
/** 业务记录或任务的唯一 ID,不能仅使用可重复的节点名称。 */
private String entityId;
/** 所属业务单据编码。 */
private String entityNumber;
/** 门户登录账号数组,保留工号前导零,不传本系统人员 ID。 */
private List<String> targets;
}
@@ -0,0 +1,18 @@
package com.budwk.app.base.portal.model;
import lombok.Data;
/** 门户调用结果;跳过不表示发送成功,网络异常时 httpStatus 为空。 */
@Data
public class PortalTodoResponse {
private boolean success;
private boolean skipped;
private Integer httpStatus;
/** 门户业务错误码,或本地 SKIPPED / CLIENT_ERROR。 */
private String code;
private String msg;
/** 门户原样返回的数据,不推断为 notifyId。 */
private String data;
/** 提供给门户管理员排查的链路标识。 */
private String traceId;
}
@@ -0,0 +1,22 @@
package com.budwk.app.base.portal.model;
import lombok.Data;
import lombok.EqualsAndHashCode;
/** 发送待办参数;继承的定位参数需由业务方保留,供后续已办、删除使用。 */
@Data
@EqualsAndHashCode(callSuper = true)
public class PortalTodoSendRequest extends PortalTodoRequest {
/** 待办标题。 */
private String subject;
/** 待办正文。 */
private String content;
/** 发送人门户登录账号,可不填。 */
private String creator;
/** 发送人姓名,可不填。 */
private String creatorName;
/** 可访问的 PC 业务页面完整地址,必填。 */
private String link;
/** 可访问的H5业务页面完整HTTP(S)地址;未传时回退到项目手机首页。 */
private String mobileLink;
}
@@ -0,0 +1,10 @@
package com.budwk.app.base.result;
/**
* @author wizzer@qq.com
*/
public interface IResultCode {
int getCode();
String getMsg();
}
@@ -0,0 +1,117 @@
package com.budwk.app.base.result;
import org.nutz.json.Json;
import org.nutz.json.JsonFormat;
import org.nutz.lang.Strings;
import org.nutz.mvc.Mvcs;
import java.io.Serializable;
/**
* Created by wizzer on 2016/12/21.
*/
public class Result implements Serializable {
private static final long serialVersionUID = 1L;
private int code;
private String msg;
private Object data;
private long time;
public Result() {
this.time = System.currentTimeMillis();
}
public static Result NEW() {
return new Result();
}
public Result addCode(int code) {
this.code = code;
return this;
}
public Result addMsg(String msg) {
if (Strings.isBlank(msg) || Mvcs.getActionContext() == null || Mvcs.getActionContext().getRequest() == null || Mvcs.getMessage(Mvcs.getActionContext().getRequest(), msg) == null) {
this.msg = Strings.sNull(msg);
} else {
this.msg = Mvcs.getMessage(Mvcs.getActionContext().getRequest(), msg);
}
return this;
}
public Result addData(Object data) {
this.data = data;
return this;
}
public Result(int code, String msg, Object data) {
this.code = code;
if (Strings.isBlank(msg) || Mvcs.getActionContext() == null || Mvcs.getActionContext().getRequest() == null || Mvcs.getMessage(Mvcs.getActionContext().getRequest(), msg) == null) {
this.msg = Strings.sNull(msg);
} else {
this.msg = Mvcs.getMessage(Mvcs.getActionContext().getRequest(), msg);
}
this.data = data;
this.time = System.currentTimeMillis();
}
public static Result success(String content) {
return new Result(0, content, null);
}
public static Result success(String content, Object data) {
return new Result(0, content, data);
}
public static Result success(Object data) {
return new Result(0, "system.success", data);
}
public boolean isSuccess() {
return this.code == ResultCode.SUCCESS.code;
}
public static Result error(IResultCode resultCode) {
return new Result(resultCode.getCode(), resultCode.getMsg(), null);
}
public static Result error(int code, String content) {
return new Result(code, content, null);
}
public static Result error(String content) {
return new Result(1, content, null);
}
public static Result success() {
return new Result(0, "system.success", null);
}
public static Result error() {
return new Result(1, "system.error", null);
}
public int getCode() {
return code;
}
public String getMsg() {
return msg;
}
public Object getData() {
return data;
}
public static Result condition(boolean flag) {
return flag ? success("system.error") : error("system.error");
}
public String toJsonString() {
return Json.toJson(this, JsonFormat.compact());
}
}
@@ -0,0 +1,42 @@
package com.budwk.app.base.result;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* @author wizzer@qq.com
*/
@Getter
@AllArgsConstructor
public enum ResultCode implements IResultCode {
SUCCESS(0, "操作成功"),
FAILURE(200400, "业务异常"),
NOT_FOUND(200404, "服务未找到"),
TOO_MANY_REQUESTS(200429, "请求次数过多"),
SERVER_ERROR(200500, "服务异常"),
IOC_ERROR(500000, "IOC对象加载异常"),
NULL_DATA_ERROR(500100, "数据不存在"),
HAVE_DATA_ERROR(500110, "数据已存在"),
PARAM_ERROR(500200, "参数错误"),
XSS_SQL_ERROR(500300, "传参被拦截"),
BLACKLIST_ERROR(500400, "IP黑名单"),
DAO_ERROR(500600, "DAO数据库查询错误"),
DEMO_ERROR(500700, "演示环境,限制操作"),
PARAM_VALID_ERROR(500800, "请求参数校验异常"),
USER_NOT_LOGIN(600098, "用户未登录"),
USER_NOT_ROLE(600099, "无此角色"),
USER_NOT_PERMISSION(600100, "无此权限"),
USER_NOT_FOUND(600101, "用户不存在"),
USER_DISABLED(600102, "用户被禁用"),
USER_LOCKED(600103, "用户已锁定"),
USER_NAME_ERROR(600104, "用户名错误"),
USER_PWD_ERROR(600105, "用户密码错误"),
USER_PWD_EXPIRED(600106, "用户密码过期"),
USER_VERIFY_ERROR(600107, "验证码错误"),
USER_LOGIN_FAIL(600108, "用户登录失败");
final int code;
final String msg;
}
@@ -0,0 +1,960 @@
package com.budwk.app.base.service;
import com.budwk.app.base.page.Pagination;
import com.budwk.app.base.page.datatable.DataTableColumn;
import com.budwk.app.base.page.datatable.DataTableOrder;
import com.budwk.app.base.param.PageForm;
import org.nutz.dao.*;
import org.nutz.dao.entity.Entity;
import org.nutz.dao.entity.Record;
import org.nutz.dao.pager.Pager;
import org.nutz.dao.sql.Sql;
import org.nutz.lang.util.NutMap;
import java.util.List;
import java.util.Map;
/**
* Created by wizzer on 2016/12/22.
*/
public interface BaseService<T> {
Dao dao();
/**
* 获取实体的Entity
*
* @return 实体的Entity
*/
Entity<T> getEntity();
/**
* 获取实体类型
*
* @return 实体类型
*/
Class<T> getEntityClass();
/**
* 统计符合条件的对象表条数
*
* @param cnd
* @return
*/
int count(Condition cnd);
/**
* 统计对象表条数
*
* @return
*/
int count();
/**
* 统计符合条件的记录条数
*
* @param tableName
* @param cnd
* @return
*/
int count(String tableName, Condition cnd);
/**
* 统计表记录条数
*
* @param tableName
* @return
*/
int count(String tableName);
/**
* 自定义SQL统计
*
* @param sql
* @return
*/
int count(Sql sql);
/**
* 通过数字型主键查询对象
*
* @param id
* @return
*/
T fetch(long id);
/**
* 自定义SQL查询VO对象
*
* @param sql
* @param clazz
* @param <C>
* @return 味大无需多盐
*/
<C> C fetchVO(Sql sql, Class<C> clazz);
/**
* 通过字符型主键查询对象
*
* @param id
* @return
*/
T fetch(String id);
/**
* 查询关联表
*
* @param obj 数据对象,可以是普通对象或集合,但不是类
* @param regex 为null查询全部,支持通配符 ^(a|b)$
* @return
*/
<T> T fetchLinks(T obj, String regex);
/**
* 查询关联表
*
* @param obj 数据对象,可以是普通对象或集合,但不是类
* @param regex 为null查询全部,支持通配符 ^(a|b)$
* @param cnd 关联字段的过滤(排序,条件语句,分页等)
* @return
*/
<T> T fetchLinks(T obj, String regex, Condition cnd);
/**
* 查出符合条件的第一条记录
*
* @param cnd 查询条件
* @return 实体, 如不存在则为null
*/
T fetch(Condition cnd);
/**
* 复合主键专用
*
* @param pks 键值
* @return 对象 T
*/
T fetchx(Object... pks);
/**
* 复合主键专用
*
* @param pks 键值
* @return 对象 T
*/
boolean exists(Object... pks);
/**
* 将一个对象插入到一个数据库
*
* @param obj 要被插入的对象
* 它可以是:
* 普通 POJO
* 集合
* 数组
* Map
* 注意:如果是集合,数组或者 Map,所有的对象必须类型相同,否则可能会出错
* @return 插入后的对象
*/
<T> T insert(T obj);
/**
* 将一个对象按FieldFilter过滤后,插入到一个数据源。
* <p>
* <code>dao.insert(pet, FieldFilter.create(Pet.class, FieldMatcher.create(false)));</code>
*
* @param obj 要被插入的对象
* @param filter 字段过滤器, 其中FieldMatcher.isIgnoreId生效
* @return 插入后的对象
* @see Dao#insert(Object)
*/
<T> T insert(T obj, FieldFilter filter);
/**
* 根据对象的主键(@Id/@Name/@Pk)先查询, 如果存在就更新, 不存在就插入
*
* @param obj 对象
* @return 原对象
*/
<T> T insertOrUpdate(T obj);
/**
* 根据对象的主键(@Id/@Name/@Pk)先查询, 如果存在就更新, 不存在就插入
*
* @param obj 对象
* @param insertFieldFilter 插入时的字段过滤, 可以是null
* @param updateFieldFilter 更新时的字段过滤,可以是null
* @return 原对象
*/
<T> T insertOrUpdate(T obj, FieldFilter insertFieldFilter, FieldFilter updateFieldFilter);
/**
* 自由的向一个数据表插入一条数据
*
* @param tableName 表名
* @param chain 数据名值链
*/
void insert(String tableName, Chain chain);
/**
* 快速插入一个对象,对象的 '@Prev' 以及 '@Next' 在这个函数里不起作用
*
* @param obj
* @return
*/
<T> T fastInsert(T obj);
/**
* 将对象插入数据库同时,也将符合一个正则表达式的所有关联字段关联的对象统统插入相应的数据库
* <p>
* 关于关联字段更多信息,请参看 '@One' | '@Many' | '@ManyMany' 更多的描述
*
* @param obj 数据对象
* @param regex 正则表达式,描述了什么样的关联字段将被关注。如果为 null,则表示全部的关联字段都会被插入
* @return 数据对象本身
* @see org.nutz.dao.entity.annotation.One
* @see org.nutz.dao.entity.annotation.Many
* @see org.nutz.dao.entity.annotation.ManyMany
*/
<T> T insertWith(T obj, String regex);
/**
* 根据一个正则表达式,仅将对象所有的关联字段插入到数据库中,并不包括对象本身
*
* @param obj 数据对象
* @param regex 正则表达式,描述了什么样的关联字段将被关注。如果为 null,则表示全部的关联字段都会被插入
* @return 数据对象本身
* @see org.nutz.dao.entity.annotation.One
* @see org.nutz.dao.entity.annotation.Many
* @see org.nutz.dao.entity.annotation.ManyMany
*/
<T> T insertLinks(T obj, String regex);
/**
* 将对象的一个或者多个,多对多的关联信息,插入数据表
*
* @param obj 对象
* @param regex 正则表达式,描述了那种多对多关联字段将被执行该操作
* @return 对象自身
* @see org.nutz.dao.entity.annotation.ManyMany
*/
<T> T insertRelation(T obj, String regex);
/**
* 更新数据
*
* @param obj
* @return
*/
int update(Object obj);
/**
* 更新数据忽略值为null的字段
*
* @param obj
* @return
*/
int updateIgnoreNull(Object obj);
/**
* 部分更新实体表
*
* @param chain
* @param cnd
* @return
*/
int update(Chain chain, Condition cnd);
/**
* 部分更新表
*
* @param tableName
* @param chain
* @param cnd
* @return
*/
int update(String tableName, Chain chain, Condition cnd);
/**
* 将对象更新的同时,也将符合一个正则表达式的所有关联字段关联的对象统统更新
* <p>
* 关于关联字段更多信息,请参看 '@One' | '@Many' | '@ManyMany' 更多的描述
*
* @param obj 数据对象
* @param regex 正则表达式,描述了什么样的关联字段将被关注。如果为 null,则表示全部的关联字段都会被更新
* @return 数据对象本身
* @see org.nutz.dao.entity.annotation.One
* @see org.nutz.dao.entity.annotation.Many
* @see org.nutz.dao.entity.annotation.ManyMany
*/
<T> T updateWith(T obj, String regex);
/**
* 根据一个正则表达式,仅更新对象所有的关联字段,并不包括对象本身
*
* @param obj 数据对象
* @param regex 正则表达式,描述了什么样的关联字段将被关注。如果为 null,则表示全部的关联字段都会被更新
* @return 数据对象本身
* @see org.nutz.dao.entity.annotation.One
* @see org.nutz.dao.entity.annotation.Many
* @see org.nutz.dao.entity.annotation.ManyMany
*/
<T> T updateLinks(T obj, String regex);
/**
* 多对多关联是通过一个中间表将两条数据表记录关联起来。
* <p>
* 而这个中间表可能还有其他的字段,比如描述关联的权重等
* <p>
* 这个操作可以让你一次更新某一个对象中多个多对多关联的数据
*
* @param classOfT 对象类型
* @param regex 正则表达式,描述了那种多对多关联字段将被执行该操作
* @param chain 针对中间关联表的名值链。
* @param cnd 针对中间关联表的 WHERE 条件
* @return 共有多少条数据被更新
* @see org.nutz.dao.entity.annotation.ManyMany
*/
int updateRelation(Class<?> classOfT, String regex, Chain chain, Condition cnd);
/**
* 基于版本的更新,版本不一样无法更新到数据
*
* @param obj 需要更新的对象, 必须有version属性
* @return 若更新成功, 大于0, 否则小于0
*/
int updateWithVersion(Object obj);
/**
* 基于版本的更新,版本不一样无法更新到数据
*
* @param obj 需要更新的对象, 必须有version属性
* @param filter 需要过滤的字段设置
* @return 若更新成功, 大于0, 否则小于0
*/
int updateWithVersion(Object obj, FieldFilter filter);
/**
* 乐观锁, 以特定字段的值作为限制条件,更新对象,并自增该字段.
* <p>
* 执行的sql如下:
* <p>
* <code>update t_user set age=30, city="广州", version=version+1 where name="wendal" and version=124;</code>
*
* @param obj 需要更新的对象, 必须带@Id/@Name/@Pk中的其中一种.
* @param fieldFilter 需要过滤的属性. 若设置了哪些字段不更新,那务必确保过滤掉fieldName的字段
* @param fieldName 参考字段的Java属性名.默认是"version",可以是任意数值字段
* @return 若更新成功, 返回值大于0, 否则小于等于0
*/
int updateAndIncrIfMatch(Object obj, FieldFilter fieldFilter, String fieldName);
/**
* 获取某个对象,最大的 ID 值,这个对象必须声明了 '@Id'
*
* @return
*/
int getMaxId();
/**
* 通过long主键删除数据
*
* @param id
* @return
*/
int delete(long id);
/**
* 通过int主键删除数据
*
* @param id
* @return
*/
int delete(int id);
/**
* 通过string主键删除数据
*
* @param id
* @return
*/
int delete(String id);
/**
* 批量删除
*
* @param ids
*/
void delete(Integer[] ids);
/**
* 批量删除
*
* @param ids
*/
void delete(Long[] ids);
/**
* 批量删除
*
* @param ids
*/
void delete(String[] ids);
/**
* 批量删除
*
* @param ids
*/
void delete(List<String> ids);
/**
* 清空表
*
* @return
*/
int clear();
/**
* 清空表
*
* @return
*/
int clear(String tableName);
/**
* 按条件清除一组数据
*
* @return
*/
int clear(Condition cnd);
/**
* 按条件清除一组数据
*
* @return
*/
int clear(String tableName, Condition cnd);
/**
* 伪删除
*
* @param id
* @return
*/
int vDelete(String id);
/**
* 批量伪删除
*
* @param ids
* @return
*/
int vDelete(String[] ids);
/**
* 批量伪删除
*
* @param ids
* @return
*/
int vDelete(List<String> ids);
/**
* 根据条件进行伪删除
*
* @param cnd
* @return
*/
int vDelete(Condition cnd);
/**
* 根据条件进行伪删除
*
* @param cnd
* @return
*/
int vDelete(String tableName, Condition cnd);
/**
* 通过LONG主键获取部分字段值
*
* @param fieldName
* @param id
* @return
*/
T getField(String fieldName, long id);
/**
* 通过INT主键获取部分字段值
*
* @param fieldName
* @param id
* @return
*/
T getField(String fieldName, int id);
/**
* 通过NAME主键获取部分字段值
*
* @param fieldName 支持通配符 ^(a|b)$
* @param name
* @return
*/
T getField(String fieldName, String name);
/**
* 通过条件获取部分字段值
*
* @param fieldName 支持通配符 ^(a|b)$
* @param cnd
* @return
*/
T getField(String fieldName, Condition cnd);
/**
* 查询获取部分字段
*
* @param fieldName 支持通配符 ^(a|b)$
* @param cnd
* @return
*/
List<T> query(String fieldName, Condition cnd);
/**
* 查询一组对象。你可以为这次查询设定条件
*
* @param cnd WHERE 条件。如果为 null,将获取全部数据,顺序为数据库原生顺序<br>
* 只有在调用这个函数的时候, cnd.limit 才会生效
* @return 对象列表
*/
List<T> query(Condition cnd);
/**
* 获取全部数据
*
* @return
*/
List<T> query();
/**
* @param cnd 查询条件
* @param linkName 关联字段,支持正则 ^(a|b)$
* @return
*/
List<T> query(Condition cnd, String linkName);
/**
* 获取表及关联表全部数据(支持子查询)
*
* @param cnd 查询条件
* @param linkName 关联字段,支持正则 ^(a|b)$
* @param linkCnd 关联条件
* @return
*/
List<T> query(Condition cnd, String linkName, Condition linkCnd);
/**
* 获取表及关联表全部数据
*
* @param linkName 关联字段,支持正则 ^(a|b)$
* @return
*/
List<T> query(String linkName);
/**
* 分页关联字段查询
*
* @param cnd 查询条件
* @param linkName 关联字段,支持正则 ^(a|b)$
* @param pager 分页对象
* @return
*/
List<T> query(Condition cnd, String linkName, Pager pager);
/**
* 分页关联字段查询(支持关联条件)
*
* @param cnd 查询条件
* @param linkName 关联字段,支持正则 ^(a|b)$
* @param linkCnd 关联条件
* @param pager 分页对象
* @return
*/
List<T> query(Condition cnd, String linkName, Condition linkCnd, Pager pager);
/**
* 分页查询
*
* @param cnd 查询条件
* @param pager 分页对象
* @return
*/
List<T> query(Condition cnd, Pager pager);
/**
* 查询获取NutMap对象
*
* @param keyColumnName 作为key的字段名
* @param valueColumnName 作为value的字段名
* @param cnd 查询条件
* @return
*/
NutMap query(String keyColumnName, String valueColumnName, Condition cnd);
/**
* 查询获取NutMap对象
*
* @param tableName 表名
* @param keyColumnName 作为key的字段名
* @param valueColumnName 作为value的字段名
* @param cnd 查询条件
* @return
*/
NutMap query(String tableName, String keyColumnName, String valueColumnName, Condition cnd);
/**
* 计算子节点TREEID
*
* @param tableName
* @param colName
* @param value
* @return
*/
String getSubPath(String tableName, String colName, String value);
/**
* 获取TREEID父级
*
* @param path
* @return
*/
String getParentPath(String path);
/**
* 执行一条自定义SQL
*
* @param sql
* @return
*/
Sql execute(Sql sql);
/**
* 自定义SQL返回Record记录集,Record是个MAP但不区分大小写
* 别返回Map对象,因为MySql和Oracle中字段名有大小写之分
*
* @param sql
* @return
*/
List<Record> list(Sql sql);
/**
* 自定义SQL返回NutMap记录集,区分大小写
*
* @param sql
* @return
*/
List<NutMap> listMap(Sql sql);
/**
* 自定义sql 返回自定义类型
*
* @param sql
* @return
*/
<C> List<C> listVO(Sql sql, Class<C> clazz);
/**
* 自定义分页sql 返回自定义类型
*
* @param sql
* @return
*/
<C> Pagination<C> listPageVO(PageForm pageForm, Sql sql, Class<C> clazz);
/**
* 自定义查询,并返回当前实体类对象
*
* @param sql
* @return
*/
List<T> listEntity(Sql sql);
/**
* 自定义sql获取map key-value
*
* @param sql
* @return
*/
Map getMap(Sql sql);
/**
* 自定义sql获取NutMap key-value
*
* @param sql
* @return
*/
NutMap getNutMap(Sql sql);
/**
* 分页查询
*
* @param pageNumber
* @param cnd
* @return
*/
Pagination listPage(Integer pageNumber, Condition cnd);
/**
* 分页查询
*
* @param pageNumber
* @param sql
* @return
*/
Pagination listPage(Integer pageNumber, Sql sql);
/**
* 分页查询(sql)
*
* @param pageNumber
* @param pageSize
* @param sql
* @return
*/
Pagination listPage(Integer pageNumber, int pageSize, Sql sql);
/**
* 分页查询
*
* @param pageNumber
* @param sql 查询语句
* @param countSql 统计语句
* @return
*/
Pagination listPage(Integer pageNumber, Sql sql, Sql countSql);
/**
* 分页查询
*
* @param pageNumber
* @param pageSize
* @param sql 查询语句
* @param countSql 统计语句
* @return
*/
Pagination listPage(Integer pageNumber, int pageSize, Sql sql, Sql countSql);
/**
* 分页查询
*
* @param pageNumber
* @param sql
* @return
*/
Pagination listPageMap(Integer pageNumber, Sql sql);
/**
* 分页查询(sql)
*
* @param pageNumber
* @param pageSize
* @param sql
* @return
*/
Pagination listPageMap(Integer pageNumber, int pageSize, Sql sql);
/**
* 分页查询
*
* @param pageNumber
* @param sql 查询语句
* @param countSql 统计语句
* @return
*/
Pagination listPageMap(Integer pageNumber, Sql sql, Sql countSql);
/**
* 分页查询
*
* @param pageNumber
* @param pageSize
* @param sql 查询语句
* @param countSql 统计语句
* @return
*/
Pagination listPageMap(Integer pageNumber, int pageSize, Sql sql, Sql countSql);
/**
* 分页查询
*
* @param pageNumber
* @param tableName
* @param cnd
* @return
*/
Pagination listPage(Integer pageNumber, String tableName, Condition cnd);
/**
* 分页查询(cnd)
*
* @param pageNumber
* @param pageSize
* @param cnd
* @return
*/
Pagination listPage(Integer pageNumber, int pageSize, Condition cnd);
/**
* 分页 只要你的class是实体类 就是标注了@Table 就可以不写service 直接用这个
*
* @param pageNumber
* @param pageSize
* @param clazz
* @param cnd
* @param <C>
* @return
*/
<C> Pagination<C> listPage(Integer pageNumber, int pageSize, Class<C> clazz, Condition cnd);
/**
* 分页查询,获取部分字段(cnd)
*
* @param pageNumber
* @param pageSize
* @param cnd
* @param fieldName 支持通配符 ^(a|b)$
* @return
*/
Pagination listPage(Integer pageNumber, int pageSize, Condition cnd, String fieldName);
/**
* 关联查询
*
* @param pageNumber
* @param pageSize
* @param cnd
* @param linkName 支持通配符 ^(a|b)$
* @return
*/
Pagination listPageLinks(Integer pageNumber, int pageSize, Condition cnd, String linkName);
/**
* 关联查询,带子查询条件
*
* @param pageNumber
* @param pageSize
* @param cnd
* @param linkName 支持通配符 ^(a|b)$
* @param subCnd 子查询条件
* @return
*/
Pagination listPageLinks(Integer pageNumber, int pageSize, Condition cnd, String linkName, Condition subCnd);
/**
* 分页查询(tabelName)
*
* @param pageNumber
* @param pageSize
* @param tableName
* @param cnd
* @return
*/
Pagination listPage(Integer pageNumber, int pageSize, String tableName, Condition cnd);
/**
* 分页查询并返回包含实体类内容的NutMap对象
*
* @param pageNumber
* @param cnd
* @return
*/
Pagination listPageMap(Integer pageNumber, Condition cnd);
/**
* 分页查询并返回包含实体类内容的NutMap对象
*
* @param pageNumber
* @param pageSize
* @param cnd
* @return
*/
Pagination listPageMap(Integer pageNumber, int pageSize, Condition cnd);
/**
* DataTable Page
*
* @param length 页大小
* @param start start
* @param draw draw
* @param orders 排序
* @param columns 字段
* @param cnd 查询条件
* @param linkName 关联查询 支持通配符 ^(a|b)$
* @return
*/
NutMap data(int length, int start, int draw, List<DataTableOrder> orders, List<DataTableColumn> columns, Cnd cnd, String linkName);
/**
* DataTable Page
*
* @param length 页大小
* @param start start
* @param draw draw
* @param orders 排序
* @param columns 字段
* @param cnd 查询条件
* @param linkName 关联查询 支持通配符 ^(a|b)$
* @param subCnd 关联查询条件
* @return
*/
NutMap data(int length, int start, int draw, List<DataTableOrder> orders, List<DataTableColumn> columns, Cnd cnd, String linkName, Cnd subCnd);
/**
* DataTable Page 自定义SQL
*
* @param length 页大小
* @param start start
* @param draw draw
* @param countSql 统计查询语句
* @param orderSql 结果查询语句
* @return
*/
NutMap data(int length, int start, int draw, Sql countSql, Sql orderSql);
/**
* DataTable Page 自定义SQL
*
* @param length 页大小
* @param start start
* @param draw draw
* @param countSql 统计查询语句
* @param orderSql 结果查询语句
* @param countOnly 统计查询语句是否只有count()
* @return
*/
NutMap data(int length, int start, int draw, Sql countSql, Sql orderSql, boolean countOnly);
/**
* DataTable Page
*
* @param length 页大小
* @param start start
* @param draw draw
* @param cnd 查询条件
* @param linkName 关联查询 支持通配符 ^(a|b)$
* @return
*/
NutMap data(int length, int start, int draw, Cnd cnd, String linkName);
/**
* 获取Map
* @param sql
* @return
*/
NutMap fetchMap(Sql sql);
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,56 @@
package com.budwk.app.base.sms;
import com.budwk.app.base.sms.model.MsgPlatformResponse;
import com.budwk.app.base.sms.model.MsgPlatformSmsRequest;
import com.budwk.app.base.sms.model.MsgPlatformWechatRequest;
import java.util.List;
public interface SmsService {
/**
* 发送短信消息。
*
* @param request 短信请求参数,需传接收人、发送人、模板ID或消息内容等字段
* @return 通讯平台返回结果,返回类型为 {@link MsgPlatformResponse}
*/
MsgPlatformResponse sendSms(MsgPlatformSmsRequest request);
/**
* 通过工号发送短信文本消息的便捷方法。
* controller 只需要传接收人工号,以及短信内容即可。
*
* @param account 接收人工号
* @param content 短信内容
* @return 通讯平台返回结果,返回类型为 {@link MsgPlatformResponse}
*/
MsgPlatformResponse sendSmsByAccount(String account, String content);
/**
* 通过手机号发送短信文本消息的便捷方法。
* controller 只需要传接收人手机号,以及短信内容即可。
*
* @param mobile 接收人手机号
* @param content 短信内容
* @return 通讯平台返回结果,返回类型为 {@link MsgPlatformResponse}
*/
MsgPlatformResponse sendSmsByMobile(String mobile, String content);
/**
* 发送微信消息。
*
* @param request 微信请求参数,需传接收人、发送人、消息类型、模板ID或消息内容等字段
* @return 通讯平台返回结果,返回类型为 {@link MsgPlatformResponse}
*/
MsgPlatformResponse sendWechat(MsgPlatformWechatRequest request);
/**
* 发送微信文本消息的便捷方法。
* controller 只需要传接收人工号或手机号,以及微信文本内容即可。
*
* @param accountOrMobile 接收人工号或手机号
* @param content 微信文本内容
* @return 通讯平台返回结果,返回类型为 {@link MsgPlatformResponse}
*/
MsgPlatformResponse sendWechatTextByAccountOrMobile(String accountOrMobile, String content);
}
@@ -0,0 +1,544 @@
package com.budwk.app.base.sms.impl;
import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.digest.DigestUtil;
import cn.hutool.http.HttpRequest;
import cn.hutool.http.HttpResponse;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.budwk.app.base.exception.BaseException;
import com.budwk.app.base.sms.SmsService;
import com.budwk.app.base.sms.model.*;
import com.budwk.app.web.commons.base.Globals;
import lombok.extern.slf4j.Slf4j;
import org.nutz.integration.jedis.RedisService;
import org.nutz.ioc.impl.PropertiesProxy;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.json.Json;
import org.nutz.lang.Lang;
import org.nutz.lang.Strings;
import org.nutz.lang.util.NutMap;
import org.nutz.log.Log;
import org.nutz.log.Logs;
import java.io.*;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.ArrayList;
import java.util.Base64;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* @ClassName SmsJshvcServiceImpl
* @Author JyuHsin
* @Date 2025/11/26 19:32
* @Version 1.0
* @Description TODO
*/
@Slf4j
@IocBean
public class SmsServiceImpl implements SmsService {
private static final Log log = Logs.get();
private static final String SMS_API_PATH = "/tp_mp/api/SmsService/saveSmsInfo";
private static final String WECHAT_API_PATH = "/tp_mp/api/WechatService/saveWechatInfo";
private static final int MAX_RECEIVER_COUNT = 500;
@Inject
private PropertiesProxy conf;
/**
* 发送短信消息。
*
* @param request 短信请求参数,需传接收人、发送人、模板ID或消息内容等字段
* @return 通讯平台返回结果,返回类型为 {@link MsgPlatformResponse}
*/
@Override
public MsgPlatformResponse sendSms(MsgPlatformSmsRequest request) {
validateCommonRequest(request);
JSONObject payload = buildCommonPayload(request, true);
return doPost(SMS_API_PATH, payload);
}
/**
* 通过工号发送短信文本消息的便捷方法。
* 该方法适合 controller 里只传“工号 + 内容”的简单场景,发送人信息从配置中读取。
*
* @param account 接收人工号
* @param content 短信内容
* @return 通讯平台返回结果,返回类型为 {@link MsgPlatformResponse}
*/
@Override
public MsgPlatformResponse sendSmsByAccount(String account, String content) {
MsgPlatformSmsRequest request = new MsgPlatformSmsRequest();
request.setInfo(content);
request.setRecipients(buildAccountRecipients(account));
fillDefaultSenderInfo(request);
return sendSms(request);
}
/**
* 通过手机号发送短信文本消息的便捷方法。
* 该方法适合 controller 里只传“手机号 + 内容”的简单场景,发送人信息从配置中读取。
*
* @param mobile 接收人手机号
* @param content 短信内容
* @return 通讯平台返回结果,返回类型为 {@link MsgPlatformResponse}
*/
@Override
public MsgPlatformResponse sendSmsByMobile(String mobile, String content) {
MsgPlatformSmsRequest request = new MsgPlatformSmsRequest();
request.setInfo(content);
request.setRecipients(buildMobileRecipients(mobile));
request.setCustomVar(Map.of("1","","2","","3",content));
fillDefaultSenderInfo(request);
return sendSms(request);
}
/**
* 发送微信消息。
*
* @param request 微信请求参数,需传接收人、发送人、消息类型、模板ID或消息内容等字段
* @return 通讯平台返回结果,返回类型为 {@link MsgPlatformResponse}
*/
@Override
public MsgPlatformResponse sendWechat(MsgPlatformWechatRequest request) {
validateCommonRequest(request);
if (Strings.isBlank(request.getWechatType())) {
request.setWechatType("text");
}
if ("news".equalsIgnoreCase(request.getWechatType()) && Strings.isBlank(request.getInfo())) {
if (request.getNewsItems() == null || request.getNewsItems().isEmpty()) {
throw new BaseException("微信图文消息必须传 info 或 newsItems");
}
request.setInfo(JSONUtil.toJsonStr(request.getNewsItems()));
}
JSONObject payload = buildCommonPayload(request, false);
payload.set("wechat_type", request.getWechatType());
return doPost(WECHAT_API_PATH, payload);
}
/**
* 发送微信文本消息的便捷方法。
* 该方法适合 controller 里只传“工号/手机号 + 内容”的简单场景,发送人信息从配置中读取。
*
* @param accountOrMobile 接收人工号或手机号
* @param content 微信文本内容
* @return 通讯平台返回结果,返回类型为 {@link MsgPlatformResponse}
*/
@Override
public MsgPlatformResponse sendWechatTextByAccountOrMobile(String accountOrMobile, String content) {
MsgPlatformWechatRequest request = new MsgPlatformWechatRequest();
request.setInfo(content);
request.setWechatType("text");
request.setRecipients(buildWechatRecipients(accountOrMobile));
fillDefaultSenderInfo(request);
return sendWechat(request);
}
/**
* 组装平台公共请求体。
*
* @param request 平台请求参数
* @param includeMobile 是否在 person_info 第 5 段写入手机号
* @return 返回发送给第三方平台的 JSON 对象
*/
private JSONObject buildCommonPayload(MsgPlatformBaseRequest request, boolean includeMobile) {
JSONObject payload = new JSONObject();
payload.set("tp_name", getRequiredConfig("msg.platform.tp-name", "请先配置 msg.platform.tp-name"));
payload.set("secret_key", getSecretKey());
payload.set("person_info", resolvePersonInfo(request, includeMobile));
payload.set("template_id", Strings.sNull(request.getTemplateId()));
payload.set("info", Strings.sNull(request.getInfo()));
payload.set("send_priority", Strings.isBlank(request.getSendPriority()) ? "3" : request.getSendPriority());
payload.set("send_user_id", Strings.sNull(request.getSendUserId()));
payload.set("send_user_name", Strings.sNull(request.getSendUserName()));
payload.set("send_time", Strings.sNull(request.getSendTime()));
payload.set("send_unit_id", Strings.sNull(request.getSendUnitId()));
payload.set("send_unit_name", Strings.sNull(request.getSendUnitName()));
payload.set("send_user_sign", Strings.sNull(request.getSendUserSign()));
payload.set("receipt_id", Strings.isBlank(request.getReceiptId()) ? "0" : request.getReceiptId());
if (request.getCustomVar() != null && !request.getCustomVar().isEmpty()) {
payload.set("custom_var", JSONUtil.toJsonStr(request.getCustomVar()));
}
return payload;
}
/**
* 校验公共请求参数,避免 controller 传错参数后才到第三方接口报错。
*
* @param request 平台请求参数
*/
private void validateCommonRequest(MsgPlatformBaseRequest request) {
if (request == null) {
throw new BaseException("请求参数不能为空");
}
if (Strings.isBlank(request.getPersonInfo()) && (request.getRecipients() == null || request.getRecipients().isEmpty())) {
throw new BaseException("personInfo 和 recipients 不能同时为空");
}
if (Strings.isBlank(request.getTemplateId()) && Strings.isBlank(request.getInfo())) {
throw new BaseException("templateId 和 info 至少需要传一个");
}
if (Strings.isBlank(request.getSendUserId())) {
throw new BaseException("sendUserId 不能为空");
}
if (Strings.isBlank(request.getSendUserName())) {
throw new BaseException("sendUserName 不能为空");
}
if (Strings.isBlank(request.getSendPriority())) {
request.setSendPriority("3");
}
if ("4".equals(request.getSendPriority()) && Strings.isBlank(request.getSendTime())) {
throw new BaseException("sendPriority=4 时必须传 sendTime");
}
validateReceiverCount(request.getPersonInfo(), request.getRecipients());
}
/**
* 解析 person_info。
* 已传原始 personInfo 时直接使用;否则根据 recipients 自动拼接平台要求的格式。
*
* @param request 平台请求参数
* @param includeMobile 是否在第 5 段写入手机号
* @return 平台要求的 person_info 字符串
*/
private String resolvePersonInfo(MsgPlatformBaseRequest request, boolean includeMobile) {
if (Strings.isNotBlank(request.getPersonInfo())) {
return request.getPersonInfo();
}
List<MsgPlatformRecipient> recipients = request.getRecipients();
if (recipients == null || recipients.isEmpty()) {
throw new BaseException("接收人列表不能为空");
}
return recipients.stream().map(recipient -> buildReceiverLine(recipient, includeMobile)).collect(Collectors.joining("^@^"));
}
/**
* 将单个接收人转成平台要求的分隔符格式。
*
* @param recipient 接收人对象
* @param includeMobile 是否在最后一段补手机号
* @return 单个接收人的 person_info 片段
*/
private String buildReceiverLine(MsgPlatformRecipient recipient, boolean includeMobile) {
if (recipient == null) {
throw new BaseException("接收人信息中存在空对象");
}
List<String> segments = new ArrayList<>(5);
segments.add(Strings.sNull(recipient.getName()));
segments.add(Strings.sNull(recipient.getAccount()));
segments.add(Strings.sNull(recipient.getUnitId()));
segments.add(Strings.sNull(recipient.getUnitName()));
if (includeMobile) {
segments.add(resolveSmsMobile(recipient));
} else {
segments.add("");
}
return String.join("|", segments);
}
/**
* 解析短信接收人手机号。
* 如果 controller 传的是手机号,则校验手机号格式;
* 如果传的是工号,则允许手机号为空,按“姓名|工号|部门ID|部门名称|”格式发送。
*
* @param recipient 接收人信息
* @return 短信 person_info 第 5 段内容
*/
private String resolveSmsMobile(MsgPlatformRecipient recipient) {
String mobile = recipient.getMobile();
if (Strings.isBlank(mobile)) {
if (Strings.isBlank(recipient.getAccount())) {
throw new BaseException("短信接收人手机号和工号不能同时为空");
}
return "";
}
if (!mobile.matches("^\\d{11}$")) {
throw new BaseException("短信接收人手机号必须为11位数字");
}
return mobile;
}
/**
* 校验接收人数量,避免超过第三方平台单次上限。
*
* @param personInfo 原始 person_info
* @param recipients 接收人列表
*/
private void validateReceiverCount(String personInfo, List<MsgPlatformRecipient> recipients) {
int receiverCount;
if (Strings.isNotBlank(personInfo)) {
receiverCount = personInfo.split("\\^@\\^", -1).length;
} else {
receiverCount = recipients == null ? 0 : recipients.size();
}
if (receiverCount > MAX_RECEIVER_COUNT) {
throw new BaseException("单次发送人数不能超过" + MAX_RECEIVER_COUNT + "人");
}
}
/**
* 构建工号发送专用接收人列表。
*
* @param account controller 传入的工号
* @return 仅包含一个接收人的列表
*/
private List<MsgPlatformRecipient> buildAccountRecipients(String account) {
if (Strings.isBlank(account)) {
throw new BaseException("接收人工号不能为空");
}
MsgPlatformRecipient recipient = new MsgPlatformRecipient();
recipient.setAccount(account);
List<MsgPlatformRecipient> recipients = new ArrayList<>(1);
recipients.add(recipient);
return recipients;
}
/**
* 构建手机号发送专用接收人列表。
*
* @param mobile controller 传入的手机号
* @return 仅包含一个接收人的列表
*/
private List<MsgPlatformRecipient> buildMobileRecipients(String mobile) {
if (Strings.isBlank(mobile)) {
throw new BaseException("接收人手机号不能为空");
}
if (!mobile.matches("^\\d{11}$")) {
throw new BaseException("接收人手机号必须为11位数字");
}
MsgPlatformRecipient recipient = new MsgPlatformRecipient();
recipient.setMobile(mobile);
List<MsgPlatformRecipient> recipients = new ArrayList<>(1);
recipients.add(recipient);
return recipients;
}
/**
* 构建微信便捷发送专用接收人列表。
*
* @param accountOrMobile controller 传入的企业微信成员ID、工号或手机号
* @return 仅包含一个接收人的列表
*/
private List<MsgPlatformRecipient> buildWechatRecipients(String accountOrMobile) {
if (Strings.isBlank(accountOrMobile)) {
throw new BaseException("微信接收人工号或手机号不能为空");
}
MsgPlatformRecipient recipient = new MsgPlatformRecipient();
recipient.setAccount(accountOrMobile);
List<MsgPlatformRecipient> recipients = new ArrayList<>(1);
recipients.add(recipient);
return recipients;
}
/**
* 为便捷发送方法补默认发送人信息。
* 这些字段从配置中读取,避免 controller 每次重复传固定参数。
*
* @param request 平台请求参数
*/
private void fillDefaultSenderInfo(MsgPlatformBaseRequest request) {
request.setSendUserId(getRequiredConfig("msg.platform.default-send-user-id", "请先配置 msg.platform.default-send-user-id"));
request.setSendUserName(getRequiredConfig("msg.platform.default-send-user-name", "请先配置 msg.platform.default-send-user-name"));
request.setSendUnitId(conf.get("msg.platform.default-send-unit-id", ""));
request.setSendUnitName(conf.get("msg.platform.default-send-unit-name", ""));
request.setSendUserSign(conf.get("msg.platform.default-send-user-sign", ""));
request.setTemplateId(conf.get("msg.platform.default-template-id", ""));
}
/**
* 调用地大通讯平台 REST 接口。
*
* @param apiPath 具体接口路径
* @param payload 发送报文
* @return 平台响应结果
*/
private MsgPlatformResponse doPost(String apiPath, JSONObject payload) {
if (!Globals.sso){
log.error("【log】开发模式不允许发送消息通知");
MsgPlatformResponse response = new MsgPlatformResponse();
response.setHttpStatus(200);
response.setCode(0);
response.setResult(true);
response.setMsg("开发模式不允许发送消息通知");
response.setRawBody(JSONUtil.toJsonStr(payload));
return response;
}
if (!Globals.MyConfig.getBoolean("AppSms", false)) {
log.error("【log】短信配置未开启");
MsgPlatformResponse response = new MsgPlatformResponse();
response.setHttpStatus(200);
response.setCode(0);
response.setResult(true);
response.setMsg("短信配置未开启");
response.setRawBody(JSONUtil.toJsonStr(payload));
return response;
}
if (!conf.getBoolean("msg.platform.enabled", false)) {
MsgPlatformResponse response = new MsgPlatformResponse();
response.setHttpStatus(200);
response.setCode(0);
response.setResult(true);
response.setMsg("通讯平台未启用,已跳过实际发送");
response.setRawBody(JSONUtil.toJsonStr(payload));
return response;
}
HttpURLConnection connection = null;
try {
String requestBody = JSONUtil.toJsonStr(payload);
log.error("发送内容:=================================================="+requestBody);
String requestUrl = buildRequestUrl(apiPath);
log.warnf("发送地址:=================================================="+requestUrl);
connection = (HttpURLConnection) new URL(requestUrl).openConnection();
connection.setRequestMethod("POST");
connection.setDoOutput(true);
connection.setDoInput(true);
connection.setUseCaches(false);
connection.setRequestProperty("Content-Type", "application/json;charset=UTF-8");
connection.setConnectTimeout(conf.getInt("msg.platform.connect-timeout", 5000));
connection.setReadTimeout(conf.getInt("msg.platform.read-timeout", 10000));
try (OutputStream outputStream = connection.getOutputStream()) {
outputStream.write(requestBody.getBytes(StandardCharsets.UTF_8));
outputStream.flush();
}
int httpStatus = connection.getResponseCode();
String responseBody = readResponseBody(connection, httpStatus);
MsgPlatformResponse response = parseResponse(httpStatus, responseBody);
log.warnf("接口调用返回结果=============================="+responseBody);
if (response.getCode() != null && response.getCode() != 0) {
log.warnf("通讯平台调用失败, httpStatus=%s, body=%s", httpStatus, responseBody);
}
return response;
} catch (IOException e) {
log.error("调用通讯平台接口异常", e);
throw new BaseException("调用通讯平台接口失败: " + e.getMessage());
} finally {
if (connection != null) {
connection.disconnect();
}
}
}
/**
* 构建完整请求地址,支持配置带或不带结尾斜杠的 base-url。
*
* @param apiPath 接口路径
* @return 完整请求地址
*/
private String buildRequestUrl(String apiPath) {
String baseUrl = getRequiredConfig("msg.platform.base-url", "请先配置 msg.platform.base-url");
if (baseUrl.endsWith("/")) {
return baseUrl.substring(0, baseUrl.length() - 1) + apiPath;
}
return baseUrl + apiPath;
}
/**
* 读取第三方接口响应报文。
*
* @param connection HttpURLConnection
* @param httpStatus HTTP 状态码
* @return 原始响应字符串
* @throws IOException 读取流失败时抛出
*/
private String readResponseBody(HttpURLConnection connection, int httpStatus) throws IOException {
InputStream inputStream = httpStatus >= 200 && httpStatus < 400 ? connection.getInputStream() : connection.getErrorStream();
if (inputStream == null) {
return "";
}
try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8))) {
StringBuilder builder = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
builder.append(line);
}
return builder.toString();
}
}
/**
* 解析平台响应体。
*
* @param httpStatus HTTP 状态码
* @param responseBody 原始响应字符串
* @return 解析后的平台响应对象
*/
private MsgPlatformResponse parseResponse(int httpStatus, String responseBody) {
MsgPlatformResponse response = new MsgPlatformResponse();
response.setHttpStatus(httpStatus);
response.setRawBody(responseBody);
if (Strings.isBlank(responseBody)) {
response.setCode(httpStatus);
response.setResult(false);
response.setMsg("通讯平台未返回响应内容");
return response;
}
if (!JSONUtil.isTypeJSON(responseBody)) {
response.setCode(httpStatus);
response.setResult(false);
response.setMsg("通讯平台返回内容不是合法JSON");
return response;
}
JSONObject jsonObject = JSONUtil.parseObj(responseBody);
response.setCode(jsonObject.getInt("code"));
response.setMsg(jsonObject.getStr("msg"));
response.setResult(jsonObject.getBool("result"));
response.setMsgId(jsonObject.getStr("msg_id"));
response.setRemainingQuota(jsonObject.getInt("remaining_quota"));
response.setDescription(jsonObject.getStr("description"));
return response;
}
/**
* 获取平台密钥。
* 优先读取已加密密钥;未配置时再根据原始密钥按“Base64 后 SHA”规则自动计算。
*
* @return 平台要求的 secret_key
*/
private String getSecretKey() {
String encryptedSecretKey = conf.get("msg.platform.encrypted-secret-key", "");
if (Strings.isNotBlank(encryptedSecretKey)) {
return encryptedSecretKey;
}
String rawSecret = getRequiredConfig("msg.platform.raw-secret-key", "请先配置 msg.platform.raw-secret-key 或 msg.platform.encrypted-secret-key");
return shaHex(rawSecret);
}
/**
* 读取必填配置项。
*
* @param key 配置 key
* @param errorMessage 配置缺失时抛出的异常信息
* @return 配置值
*/
private String getRequiredConfig(String key, String errorMessage) {
String value = conf.get(key, "");
if (Strings.isBlank(value)) {
throw new BaseException(errorMessage);
}
return value;
}
/**
* 生成 SHA 十六进制摘要。
*
* @param content 待加密内容
* @return SHA 十六进制字符串
*/
private String shaHex(String content) {
try {
MessageDigest messageDigest = MessageDigest.getInstance("SHA");
messageDigest.update(content.getBytes());
byte[] hash = messageDigest.digest();
return Base64.getEncoder().encodeToString(hash);
} catch (Exception e) {
throw new BaseException("生成通讯平台 secret_key 失败: " + e.getMessage());
}
}
}
@@ -0,0 +1,87 @@
package com.budwk.app.base.sms.model;
import lombok.Data;
import java.util.List;
import java.util.Map;
/**
* 通讯平台公共请求参数。
*/
@Data
public class MsgPlatformBaseRequest {
/**
* 接收人信息原始字符串。
* 如果 controller 已经按平台要求拼好了 person_info,可以直接传该字段;
* 否则可只传 recipients,由 service 自动组装。
*/
private String personInfo;
/**
* 接收人列表。
* 当 personInfo 为空时,service 会根据该列表自动拼接平台要求的 person_info 字符串。
*/
private List<MsgPlatformRecipient> recipients;
/**
* 模板ID。
* templateId 和 info 至少需要传一个,使用模板发送时优先传该字段。
*/
private String templateId;
/**
* 消息内容。
* templateId 和 info 至少需要传一个;不使用模板时,需要传完整消息内容。
*/
private String info;
/**
* 发送优先级。
* 3 表示立即发送,4 表示定时发送。
*/
private String sendPriority = "3";
/**
* 发送人账号。
*/
private String sendUserId;
/**
* 发送人姓名。
*/
private String sendUserName;
/**
* 模板变量。
* 使用模板发送时可传该字段,service 会自动转成平台要求的 JSON 字符串。
*/
private Map<String, String> customVar;
/**
* 定时发送时间,格式必须为 yyyy-MM-dd HH:mm:ss。
* 当 sendPriority=4 时该字段必填。
*/
private String sendTime;
/**
* 发送机构ID。
*/
private String sendUnitId;
/**
* 发送机构名称。
*/
private String sendUnitName;
/**
* 发送签名。
* 旧通用模板或特定签名场景可传该字段。
*/
private String sendUserSign;
/**
* 回执ID。
* 不需要回执时可保持默认值 0。
*/
private String receiptId = "0";
}
@@ -0,0 +1,29 @@
package com.budwk.app.base.sms.model;
import lombok.Data;
/**
* 微信图文消息单条数据。
*/
@Data
public class MsgPlatformNewsItem {
/**
* 图文标题。
*/
private String title;
/**
* 图文描述。
*/
private String description;
/**
* 点击跳转链接。
*/
private String url;
/**
* 图文封面图片链接。
*/
private String picurl;
}
@@ -0,0 +1,36 @@
package com.budwk.app.base.sms.model;
import lombok.Data;
/**
* 通讯平台接收人信息。
*/
@Data
public class MsgPlatformRecipient {
/**
* 接收人姓名,对应 person_info 中的第 1 段。
*/
private String name;
/**
* 接收人账号,对应 person_info 中的第 2 段。
* 短信场景一般传学工号;微信场景可传企业微信成员ID、学工号或手机号。
*/
private String account;
/**
* 接收人部门ID,对应 person_info 中的第 3 段。
*/
private String unitId;
/**
* 接收人部门名称,对应 person_info 中的第 4 段。
*/
private String unitName;
/**
* 接收人手机号,对应短信 person_info 中的第 5 段。
* 微信接口不会使用该字段,微信接口会自动补空串以保留分隔符格式。
*/
private String mobile;
}
@@ -0,0 +1,49 @@
package com.budwk.app.base.sms.model;
import lombok.Data;
/**
* 通讯平台响应结果。
*/
@Data
public class MsgPlatformResponse {
/**
* HTTP 状态码,便于 controller 定位网络层问题。
*/
private Integer httpStatus;
/**
* 平台返回业务状态码,0 表示成功。
*/
private Integer code;
/**
* 平台返回消息提示。
*/
private String msg;
/**
* 平台返回业务是否成功。
*/
private Boolean result;
/**
* 发送成功后的消息ID。
*/
private String msgId;
/**
* 短信剩余额度,仅短信接口返回。
*/
private Integer remainingQuota;
/**
* 平台失败描述。
*/
private String description;
/**
* 平台原始响应报文,排查问题时可直接查看。
*/
private String rawBody;
}
@@ -0,0 +1,7 @@
package com.budwk.app.base.sms.model;
/**
* 短信发送请求参数。
*/
public class MsgPlatformSmsRequest extends MsgPlatformBaseRequest {
}

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