Compare commits

..

133 Commits

Author SHA1 Message Date
2a671e2459 docs: 添加合并仓库说明(目录结构/启动方式/MeshSafe对接) 2026-11-01 20:34:14 +08:00
e43a521345 chore: 导入前端 senmesh-zc-legacy(main分支)至 frontend/ 2026-11-01 20:34:02 +08:00
20b2dd7009 chore: 导入后端 s-file(alita-bjy-uygur, f分支)至 backend/ 2026-11-01 20:34:00 +08:00
bf63096bb2 feat: 文件安全检测接入(列表检测列+悬停详情浮窗,不通过禁用分享传输,文件夹确认后仅分享通过内容,管理端复核/重新检测,检测中自动轮询,开关关闭隐藏检测列) 2026-11-01 20:29:42 +08:00
4c7dd207d0 feat: 对接MeshSafe文件安全检测(上传/解包自动送检+轮询回写,不通过禁止分享传输,文件夹确认后仅分享通过内容,人工复核/重新检测,meshsafe.enabled总开关) 2026-11-01 20:29:31 +08:00
c5a262b3dd chore: 新增 ARM64 Dockerfile 2026-11-01 16:49:56 +08:00
45b62ba131 feat: 多机构用户登录时选择机构(600临时token二次登录);设置密码页增加警示文案(Alert新增warning样式) 2026-10-27 15:18:17 +08:00
733dcb9fc0 fix: 机构管理员可修改本机构用户信息(modify_org_user_info双档权限+机构范围校验),禁改超管;isSuperior空值防护 2026-10-27 15:18:10 +08:00
78a1e6c60a feat: 用户管理新增搜索条件(用户名/姓名/启用状态/状态) 2026-10-27 13:39:53 +08:00
fcf0e6454f chore: 开发代理切换到本地后端(localhost:8081) 2026-10-27 13:30:55 +08:00
ef2dab823f feat: 管理员修改用户姓名、批量创建机构用户接口(单条失败不影响其他,返回逐条结果) 2026-10-27 13:29:14 +08:00
6ccc8524fa fix: 创建机构用户时未配置 user.default.password 导致 rawPassword cannot be null,补默认密码兜底 2026-10-18 22:07:21 +08:00
301aca54f8 fix: 重置密码接口补权限校验(超管全部/机构管理员限本机构),禁止重置超管与已注销账号 2026-09-16 17:18:51 +08:00
c305ca946e feat: 用户启停/注销、机构账号查询、手机号查记录、机构上传配置
1. 用户管理:新增启用(user/enable)/禁用(user/disable)/注销(user/admin_cancel)接口,
   平台超管可操作全部用户,机构管理员限本机构;注销带分布式锁、禁自己/超管守卫
2. 禁用/注销立即踢下线:Redis黑名单(user:disabled:{uid}) + JWT过滤器拦截,
   TTL与token有效期一致
3. 系统管理员查看各机构账号:GET user/page/org_account(机构/账号/手机号等筛选)
4. 按手机号查上传/发送/接收记录:GET user_file/record/page_by_phone,
   发送/接收方向按部署侧 file.transfer.location 自动映射
5. 机构上传配置:新表 x_upload_config + upload_config/info|modify|check 接口,
   上传总开关/后缀白名单/单文件大小限制,网盘落库时强制校验
6. 加固:UserInfoDTO.setPassword 缺省密码未配置时不再抛 rawPassword cannot be null
7. 新增 x_upload_config 建表脚本(upgrade-20260915-release.sql 为上线版)、
   生产部署说明、前端接口文档×2、运维脚本 sfile-ctl.sh、AGENTS.md
2026-09-15 12:14:27 +08:00
ran.shang
54b7e9a555 fix user 2026-09-09 10:05:23 +08:00
ran.shang
4bd6b57b4f fix 2026-04-13 11:12:59 +08:00
ran.shang
1f52669b52 fix 2026-04-08 09:26:57 +08:00
ran.shang
52356bdc2e fix ld 2026-03-27 18:28:05 +08:00
ran.shang
cb29084f97 rm 2026-03-18 17:18:06 +08:00
ran.shang
e025b918d9 fix anon 2026-03-04 16:54:26 +08:00
ran.shang
bb9cd2cf52 fix 2026-02-02 18:16:03 +08:00
ran.shang
673a211619 fix 2026-01-24 23:22:21 +08:00
ran.shang
d5cb399e8c fix 2026-01-16 10:35:27 +08:00
管理员
6b79c943ef 更新 'src/main/resources/Dockerfile' 2026-01-13 20:37:09 +08:00
管理员
6c005cdd32 fix 2026-01-13 19:37:55 +08:00
ran.shang
c16db9a1c4 fix 2026-01-12 14:10:55 +08:00
ran.shang
7ab02fe5a1 fix 2026-01-10 21:46:51 +08:00
ran.shang
9ee48d941c fix 2026-01-10 18:54:42 +08:00
ran.shang
d22300ac6a fix 2026-01-09 18:34:18 +08:00
ran.shang
91cbc7e37a fix 2026-01-08 18:10:29 +08:00
ran.shang
dccec2b341 fix 2026-01-08 15:38:10 +08:00
ran.shang
5405012faa 扫描 2026-01-08 14:33:33 +08:00
ran.shang
2b4458dc1b 优化+riskrule 2026-01-06 16:37:39 +08:00
ran.shang
ea6d25ca97 兼容+功能 2026-01-05 16:35:46 +08:00
ran.shang
e99b717e48 file 2025-12-31 18:10:10 +08:00
ran.shang
d8cecbe135 fix redis 2025-12-29 17:19:31 +08:00
ran.shang
ee95dfa2c2 init 2025-12-29 16:06:48 +08:00
ran.shang
b853edcfde init sql 2025-12-29 16:01:39 +08:00
ran.shang
be2e15b68a init 2025-12-23 17:40:32 +08:00
czy
083eb8bfe2 # 优化岗位查询 2025-11-06 17:19:33 +08:00
czy
8b50207e2f # 优化岗位查询权限 2025-11-06 16:06:40 +08:00
czy
d5245c1ce7 # 岗位管理:支持新建人才集团机构查看全平台岗位数据 2025-10-23 10:42:57 +08:00
ran.shang
d07a90eb5e 薪资面议 2025-09-15 11:12:46 +08:00
ran.shang
26184c1a80 薪资范围查询,在岗位工资上下限之间 2025-09-01 16:16:55 +08:00
ran.shang
11d581aaf0 file download range 2025-08-27 18:02:40 +08:00
ran.shang
59ef112929 file video ai idea 2025-08-27 17:15:06 +08:00
ran.shang
7c62035839 ADD LOG 2025-08-27 14:49:14 +08:00
ran.shang
e33f5c1953 fix file download buffer 2025-08-27 14:21:44 +08:00
ran.shang
cb83b3a0ca fix file ClientAbortException 2025-08-27 10:45:29 +08:00
ran.shang
1b056e51d2 产品要移除指定代码 2025-08-26 16:14:08 +08:00
ran.shang
1168ad4c85 岗位类型全称相关性搜索 2025-08-26 15:06:56 +08:00
ran.shang
6e5a55a0eb add log 2025-08-26 13:19:19 +08:00
ran.shang
bd2475a5e2 add log 2025-08-26 11:41:38 +08:00
ran.shang
8cbfd5aacd fix sms default 无 2025-08-25 14:04:47 +08:00
ran.shang
c324a8ef33 fix sms null value 2025-08-25 13:46:40 +08:00
ran.shang
cd1a3c2e16 fix 2025-08-25 13:11:33 +08:00
ran.shang
6570610a78 fix 2025-08-25 13:01:18 +08:00
ran.shang
74f54dcbc2 anon ai 2025-08-25 10:56:39 +08:00
guixinman
9e3237f94e 岗位查询 薪资字符串修改 2025-08-25 10:14:30 +08:00
ran.shang
0ddb3b4529 fix sms key 2025-08-25 09:25:40 +08:00
ran.shang
f572ee9714 job sendsms 2025-08-24 19:29:01 +08:00
ran.shang
5effd9bcf4 xf 维语转语音 2025-08-22 15:52:49 +08:00
ran.shang
cc8c388925 fix 短信签名 2025-08-22 11:23:46 +08:00
ran.shang
c3e30e8a44 user position nameOr 2025-08-22 09:41:54 +08:00
ran.shang
cc3819e2f3 fix 2025-08-21 11:36:25 +08:00
ran.shang
ab1b8a2560 第三方服务配置更改 2025-08-15 18:17:05 +08:00
ran.shang
305e57238b fix file delete 2025-08-15 14:56:08 +08:00
ran.shang
bff0f19500 fix xf oss read bug 2025-08-15 14:17:10 +08:00
ran.shang
8f44d802db fix properties 2025-08-15 10:04:55 +08:00
ran.shang
dd72bcb099 fix xf fail close 2025-08-15 10:03:33 +08:00
ran.shang
7feebc4a35 fix file max 2025-08-14 13:20:35 +08:00
ran.shang
3bb05d6de8 prod 配置 2025-08-12 17:54:12 +08:00
ran.shang
bf59ce0a10 ucard login 2025-08-12 10:41:06 +08:00
ran.shang
4526fc1cf8 show 2025-08-06 11:15:25 +08:00
ran.shang
2d80d9ac7c change mysql 2025-08-06 10:42:34 +08:00
guixinman
2d43a94f99 用户类型判断 2025-08-04 17:56:09 +08:00
guixinman
0b75af1555 岗位、岗位地址、企业新增 增加用户类型判断 2025-07-31 17:26:23 +08:00
guixinman
b43563546a 分页查询增加用户类型判断 2025-07-31 17:14:11 +08:00
guixinman
b318fbd3d1 分页查询增加用户类型判断 2025-07-31 16:59:20 +08:00
guixinman
8ae137192b 岗位地址list改造 2025-07-25 15:21:46 +08:00
ran.shang
73e8dcabb2 Revert "fix"
This reverts commit 1ed120da86.
2025-07-24 11:21:54 +08:00
guixinman
ce2a0d70c4 org统计 2025-07-21 15:02:37 +08:00
ran.shang
1ed120da86 fix 2025-07-17 19:17:21 +08:00
ran.shang
cc56007101 fix 2025-07-17 19:07:12 +08:00
guixinman
e9cc47853e 供应商代码修改 2025-07-17 14:32:34 +08:00
ran.shang
77cf213e49 chat 2025-07-17 14:01:36 +08:00
ran.shang
706bbf6d79 fix 2025-07-07 11:19:01 +08:00
ran.shang
cd3aaea259 add ai chat deepseek 2025-07-07 10:51:42 +08:00
ran.shang
c6a46b62cc fix xf chat param type 2025-07-03 15:17:39 +08:00
ran.shang
6cbb4526c8 失败不停程序 2025-07-03 13:56:18 +08:00
ran.shang
a033ad1f35 xf 听写用pcm 2025-07-03 10:33:18 +08:00
ran.shang
7784ba724c log/ali oss 2025-07-02 10:41:38 +08:00
guixinman
a107acdd16 匿名企业信息 2025-06-27 15:59:56 +08:00
guixinman
0f039ab54f 匿名企业信息 2025-06-27 15:23:19 +08:00
guixinman
552091540b 企业审核 2025-06-27 14:11:02 +08:00
ran.shang
161076e755 freemarker 默认值 2025-06-26 18:30:09 +08:00
guixinman
b622782415 企业审核 2025-06-26 18:25:47 +08:00
guixinman
5a17819fa4 企业审核 2025-06-26 18:22:39 +08:00
ran.shang
d10b7ca6d2 fix 消息通知接收方 2025-06-26 17:58:41 +08:00
ran.shang
881f196ef1 通知站内信 2025-06-26 17:36:59 +08:00
ran.shang
8a77c9179b xf实时语音转写 2025-06-25 18:11:39 +08:00
guixinman
dd968cb6d4 岗位地址搜索 2025-06-25 16:01:17 +08:00
ran.shang
25f48013fc fix xf chat key 2025-06-24 16:48:28 +08:00
ran.shang
6def8d1023 xf 翻译 2025-06-24 16:24:57 +08:00
ran.shang
2649ec853f 站内信通知 2025-06-20 14:51:25 +08:00
guixinman
676abc2631 埋点job 注释掉 2025-06-20 13:41:14 +08:00
guixinman
1ea55fbc51 岗位导入 2025-06-20 11:47:39 +08:00
guixinman
158aae51f4 岗位导入 2025-06-20 11:31:27 +08:00
guixinman
711dbd4d5d 招聘管理增加简历查询 2025-06-20 10:27:37 +08:00
guixinman
464fe71c7b 招聘管理增加简历查询 2025-06-20 10:14:21 +08:00
ran.shang
9199580155 实名mock次数 2025-06-19 18:12:50 +08:00
guixinman
88ffe7921e 配置管理anon 2025-06-19 11:18:01 +08:00
guixinman
c625dbc7e1 招聘管理导出 2025-06-19 10:51:47 +08:00
guixinman
ea679bc441 Merge remote-tracking branch 'origin/master' 2025-06-18 17:31:55 +08:00
guixinman
6aeaf6759a 岗位导出 2025-06-18 17:31:49 +08:00
ran.shang
7648f29ca3 fix idcard msg 2025-06-18 17:08:46 +08:00
guixinman
a2353f38f0 appVersion匿名接口 2025-06-18 16:45:38 +08:00
guixinman
0d603cf8ae appVersion匿名接口 2025-06-18 16:42:20 +08:00
guixinman
f141051f78 岗位复制相册和视频 2025-06-18 15:59:34 +08:00
guixinman
0191784845 岗位复制相册和视频 2025-06-18 15:58:39 +08:00
guixinman
fee5c6afb1 岗位复制相册和视频 2025-06-18 15:56:57 +08:00
guixinman
a318f4ba9e 岗位复制相册和视频 2025-06-18 15:19:29 +08:00
guixinman
28a8d944cc Merge remote-tracking branch 'origin/master' 2025-06-18 14:36:30 +08:00
guixinman
98bde9cf6e 岗位工作地址导出 2025-06-18 14:36:24 +08:00
ran.shang
e9d69a251b fix 手机号登录 2025-06-18 14:12:01 +08:00
guixinman
2c7fa11ea3 机构创建修改增加校验 2025-06-18 09:49:48 +08:00
ran.shang
94a5f85c65 ali idcard verify 2025-06-17 18:39:28 +08:00
ran.shang
f4a4fb9c76 fix redis index 2025-06-17 15:33:08 +08:00
ran.shang
17dad44d37 fix 系统标识 2025-06-16 15:25:40 +08:00
ran.shang
0d04167bca 阿里oss 2025-06-16 14:53:21 +08:00
guixinman
06f091967d 报名记录表增加tag 2025-06-11 13:44:08 +08:00
ran.shang
02f71a3167 fix 2025-06-11 09:48:02 +08:00
ran.shang
7e38b8d971 init 2025-06-11 09:42:13 +08:00
2181 changed files with 102352 additions and 342 deletions

View File

@@ -1,73 +1,28 @@
# React + TypeScript + Vite # senmesh-zc(信创文件安全传输系统 · 合并仓库)
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. 光闸内外网文件摆渡系统,前后端合并仓库:
Currently, two official plugins are available: | 目录 | 项目 | 说明 | 技术栈 |
|---|---|---|---|
| `backend/` | s-file(原 alita-bjy-uygur) | 服务端:文件传输/网盘/机构用户管理/MeshSafe 安全检测对接 | Java 8 + Spring Boot 2.4.2 + MyBatis-Plus + PG/DM8 |
| `frontend/` | senmesh-zc-legacy | Web 前端:我的文件/分享/内外网传输/管理端 | React 19 + Vite + TanStack Router/Query + shadcn/ui |
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh > 两端保留原仓库完整提交历史(子目录导入),`git log --follow` 可追溯。
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
## React Compiler ## 本地启动
The React Compiler is currently not compatible with SWC. See [this issue](https://github.com/vitejs/vite-plugin-react/issues/428) for tracking the progress. ```bash
# 后端(需 JDK8,本地联调配置见 backend/docker-test/application-local.yml)
cd backend && mvn spring-boot:run
## Expanding the ESLint configuration # 前端(dev 代理 /api -> localhost:8081)
cd frontend && bun install && bun dev
If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
```js
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
// Other configs...
// Remove tseslint.configs.recommended and replace with this
tseslint.configs.recommendedTypeChecked,
// Alternatively, use this for stricter rules
tseslint.configs.strictTypeChecked,
// Optionally, add this for stylistic rules
tseslint.configs.stylisticTypeChecked,
// Other configs...
],
languageOptions: {
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
},
// other options...
},
},
])
``` ```
You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules: ## 文件安全检测(MeshSafe)
```js 独立检测服务,上传/传输接收解包后自动送检,检测不通过的文件禁止分享与传输(文件夹可确认跳过,仅传通过内容);支持人工复核与重新检测。
// eslint.config.js
import reactX from 'eslint-plugin-react-x'
import reactDom from 'eslint-plugin-react-dom'
export default defineConfig([ - 后端配置:`backend/docker-test/application.yml` 的 `meshsafe:` 段(`enabled` 总开关)
globalIgnores(['dist']), - 接口文档:`backend/docs/接口文档-文件安全检测.md`
{ - 数据库脚本:`backend/db/`(init 全量 + upgrade 增量,双写 MySQL/PG)
files: ['**/*.{ts,tsx}'],
extends: [
// Other configs...
// Enable lint rules for React
reactX.configs['recommended-typescript'],
// Enable lint rules for React DOM
reactDom.configs.recommended,
],
languageOptions: {
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
},
// other options...
},
},
])
```

34
backend/.gitignore vendored Normal file
View File

@@ -0,0 +1,34 @@
HELP.md
target/
!.mvn/wrapper/maven-wrapper.jar
!**/src/main/**/target/
!**/src/test/**/target/
### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
build/
!**/src/main/**/build/
!**/src/test/**/build/
### VS Code ###
.vscode/.DS_Store
.commandcode/

74
backend/AGENTS.md Normal file
View File

@@ -0,0 +1,74 @@
# AGENTS.md — s-file(alita-bjy-uygur)编码代理指南
信创文件传输/网盘后端服务:内网与外网之间做文件安全传输("内穿外/外传内"),附带个人网盘、分享、机构与用户管理、通知等能力。单 Maven 模块,Spring Boot 可执行 jar,Docker 部署(`src/main/resources/Dockerfile`)。
## 技术栈(务必匹配,勿升级)
- **Java 8** + Spring Boot **2.4.2**(父 POM 管理)
- ORM:**MyBatis-Plus 3.4.0**(注解/BaseMapper)+ 自研 `com.senmesh.tkmybatis` 封装 + PageHelper 1.4.6 + 自定义分页拦截器
- 数据库:**达梦 DM8**(信创主力,另有 MySQL/PG 驱动),Druid 连接池,SQL 需多库兼容
- 安全:Spring Security + 自研 JWT(`com.senmesh.security`),Redisson 分布式锁,RabbitMQ
- 文档:springfox swagger2 2.7.0;Lombok + MapStruct 1.2.0
## 常用命令
```bash
# 编译(需 JDK 8,例如本机 Zulu 1.8)
export JAVA_HOME=/Users/leijiaping/Library/Java/JavaVirtualMachines/azul-1.8.0_452/Contents/Home
mvn compile -DskipTests
# 测试目录基本为空(仅 PwdTest),项目无自动化测试,验证靠编译 + 自查
```
**已知依赖问题(重要)**:商业依赖 `e-iceblue:spire.pdf:3.7.4` 在阿里云/e-iceblue 官方仓库均不存在(官方仓库只有 9.x+)。本地仓库 `~/.m2/repository/e-iceblue/spire.pdf/3.7.4/` 现已安装一个**手工编译级 stub jar**(仅为 `PdfUtils.java` 引用的类签名生成),使 `mvn package` 可以通过;**代价是运行 PDF 盖章功能(`PdfUtils.pdfStampPerPage`)会失败**。正式发布前需从公司私服获取真实 jar 覆盖。全量快速编译验证可跳过 PdfUtils.java(详见下方命令)。
```bash
mvn dependency:build-classpath -Dmdep.outputFile=/tmp/cp.txt # 需联网
$JAVA_HOME/bin/javac -encoding UTF-8 -cp "$(cat /tmp/cp.txt)" \
-sourcepath src/main/java -d /tmp/v \
$(find src/main/java -name "*.java" ! -name "PdfUtils.java")
```
## 包结构地图(`com.senmesh`)
| 包 | 职责 |
|---|---|
| `usercenter` | 用户中心:`user`(用户/机构用户/注销)、`org`、`authority`(角色/权限)、`notice`(通知)、`auth` 等,子模块按「controller/service/impl/dto/entity/dao/mapper(XML)/constant/enums」分目录 |
| `sfile` | 文件业务:`UserFile`(上传/传输/分享记录,核心表 `x_user_file`)、`TransferUserFile`、`ShareUserFile`、`TransferFileMonitorJob`(定时扫描传输目录) |
| `component/file` | 物理文件上传组件(`xs_file_record`、`/file/upload`、`FileProperties`) |
| `common` | `AlitaResult`、`PageQueryDTO`、`BaseEntity`、`UserContextHolder`、异常体系 |
| `security` | JWT 过滤器、登录、`UserInfoDTO` |
| `frame/redis` | Redisson 封装:`RedissonUtils`、`@AutoUnLock`、`LockDTO` |
| `mybatis` / `mybatisplus` / `tkmybatis` | 分页拦截器、`@MapperScan`、`MyMapper<T>` 基类 |
| `mq`、`codesetting`、`config` 等 | 其他支撑模块 |
**注意**:源码树里有同包覆写的框架类(`org/springframework/...`、`com/senmesh/swagger`),改动前先确认是否为刻意覆写,不要删除。
## 新增一个接口的标准动作
1. **DTO**:查询 DTO 继承 `PageQueryDTO<XxxDTO>`(自带 pageNo/pageSize/needPage/pageResult);出参 DTO 继承 `BaseDTO`(用 `@SuperBuilder`);时间字段统一 `Date` + `@DateTimeFormat` + `@JsonFormat(pattern="yyyy-MM-dd HH:mm:ss", timezone="GMT+8")`。
2. **Entity**:继承 `BaseEntity`(带机构的继承 `OrgBaseEntity`),`@TableName` 注解,逻辑删除字段 `deleted`。
3. **Mapper**:接口继承 `MyMapper<T>`(已有 `saveEntity`/`updateEntitySelective`/`getById`/`deleteEntity`,自动填充 createBy/updateBy),放 `**/dao/`;**自定义 SQL 的 XML 放同名 java 源码目录的 `mapper/` 下**(pom 已配置打包 java 目录下 `**/*.xml`),XML 中参数一律 `@Param("param")` + `param.xxx` 引用。
4. **Service**:接口 + `impl`。权限校验用命令式断言(项目无 @PreAuthorize):
- 平台超管:`Assert.isTrue(UserContextHolder.isSuperior(), "当前用户无操作权限")`,注意字段拼写就是 `supeiror`(历史笔误,勿"修复")
- 机构管理员:`UserContextHolder.isAdministrator()`;平台机构:`checkPlatformOrg()`
5. **Controller**:返回一律 `AlitaResult<T>`(`AlitaResult.success(data)`);`@Api(tags=...)` + `@ApiOperation`;分页接口返回 `AlitaResult<Page<T>>`,模式:Service 把 queryDTO 传给 mapper 即自动分页,Controller 返回 `queryDTO.getPageResult()`。查询类用 GET + query 参数绑定,操作类用 POST + `@Validated @RequestBody`。
6. **URL 前缀有语义**:`anon/**` 匿名(清单在配置 `security.anonymous`)、`unac/**` 已登录未激活可用、`authed/**` 已实名;普通前缀 = 登录即可。
## 关键业务概念
- **用户**(`uc_user`):`status` 1待激活/2已激活/3已注销 + `enabled` 启用开关 + 逻辑删除 `deleted`,三者独立。登录侧实时校验 `enabled=TRUE and status!=3`,禁用/注销后用户下次请求即失效。注销有两套:自助申请流(`UserCancelService`,`uc_user_cancel` 表)与管理员直接注销(`user/admin_cancel`)。同一手机号可对应多个账号(企业+个人)。
- **机构**:`xs_org`(`parent_id` 树)+ `uc_org_user` 中间表(含 `administrator` 标志,一个用户可在多个机构,登录时选定 orgId)。**用户表没有 org_id**。平台机构固定 `orgId=1`(`CommonConstants.PLATFORM_ORG_ID`)。
- **文件记录**(`x_user_file`):上传/发送/接收/分享全在这一张表,靠 `root_type` 区分:`1`普通(上传)、`2`内穿外、`3`外传内、`4`我的分享;`file_status`:1正常/2失效/11解析中/12待审核/13传输中/14失败。表里只有 `user_id`,无手机号,按手机号查需 JOIN `uc_user.phone`。
- **传输方向映射**(`file.transfer.location`:1内网/2外网):发送记录 = 内网端 rootType 2 / 外网端 rootType 3;接收记录取反。手机号只在传输 ZIP 包内 `transferMetadata.json`,接收端解包时靠它反查本地用户。
## 陷阱清单
- 只能写 **Java 8** 语法(无 `var`/`Set.of`/record/switch 表达式;集合用 guava `Sets.newHashSet` 等也行)。
- SQL 要兼容 DM8:布尔字面量用 `TRUE/FALSE`(bit 列),时间用 `now()`;避免 MySQL 专属函数;分页依赖拦截器,XML 里不要手写 limit。
- XML 放错位置不会被打包——必须放 java 源码目录(与现有 `UserMapper.xml`、`UserFileMapper.xml` 同级模式)。
- 驼峰映射由 MyBatis-Plus 默认开启,XML 直接 `SELECT u.real_name` 即可映射 `realName`;列别名规避同名冲突(如 `u.type AS user_type`)。
- 注销/禁用等敏感操作要防并发:参考 `userCancel` 的 `RedissonUtils.tryLockAutomatically(LockDTO...)` + `@AutoUnLock` 模式;禁止注销/禁用自己与超管。
- 数据库初始化脚本在 `db/`(`init-table.sql` MySQL 版、`init-table-postgres-full.sql` PG 版),改表结构需同步两个版本;增量变更另建 `db/upgrade-YYYYMMDD-*.sql`(现有:`upgrade-20260914-upload-config.sql`),历史上有过 DDL 只加脚本、不改 init 的情况,新环境安装依赖 init 脚本,**改表务必 init 与 upgrade 双写**。
- 给前端的接口变更请同步维护 `docs/` 下的接口文档(现有:`接口文档-用户管理与记录查询.md`、`接口文档-机构上传配置.md`)。
- `.commandcode/` 为本地工具产物,勿提交。

2
backend/README.md Normal file
View File

@@ -0,0 +1,2 @@
# demo

46
backend/db/init-data.sql Normal file
View File

@@ -0,0 +1,46 @@
-- 系统uc_platform
INSERT INTO uc_platform
(platform_code, name, full_name, short_name, business, deleted, create_time)
VALUES(1, '文档系统', 's-file', 'file', true, FALSE, now());
-- 平台机构/平台机构管理员
INSERT INTO xs_org
(org_name, org_code, type, address_code, address_name, description, quality, enable, activated, parent_id, credit_code, trade, deleted, create_by, create_time, update_by, update_time, proxy_by, activation_code, phone, query_children, org_logo, registered_address, company_size, business_license, recruit_position_type)
VALUES('senmesh', 'O001', '1', '440232', NULL, NULL, false, TRUE, TRUE, NULL, '', NULL, FALSE, 0, now(), 1, now(), null, NULL, null, NULL, null, '1234', '6', null, '1');
INSERT INTO uc_user
(username, password, type, supeiror, security_code, nickname, avatar, phone, phone_binding, real_name, card_no, authenticated, status, email, address, education, init_user_id, enabled, account_non_expired, account_non_locked, credentials_non_expired, deleted, create_by, create_time, update_by, update_time, proxy_by, gender, birthday)
VALUES('admin', '$2a$10$0N3iaxx2sYv0hXvYzS4.6.yriL.DfjRTjED7Y4vkqxzbxi2dNpBou', 1, TRUE, '$2a$10$mGA1jdBzq8micEr02KiJ9OiYQz1fc/PSVG0hN0EYUuxhZp8EI1M2q', '平台管理员', '', '13888888888', TRUE, '平台管理员', NULL, FALSE, 2, null, NULL, NULL, NULL, TRUE, TRUE, TRUE, TRUE, FALSE, 0, now(), 1, NULL, NULL, NULL, NULL);
INSERT INTO uc_org_user
(user_id, org_id, administrator, deleted, create_by, create_time, update_by, update_time, proxy_by)
VALUES(1, 1, TRUE, FALSE, 0, now(), NULL, NULL, NULL);
-- 平台uc_org_relation
INSERT INTO uc_org_relation
(org_id, org_type, parent_id, path, platform_code, platform_name, deleted, create_by, create_time, update_by, update_time, proxy_by)
VALUES(1, 1, 0, '1', 1, NULL, FALSE, NULL, NULL, NULL, NULL, NULL);
-- ----------------------------
-- Records of x_file_category
-- ----------------------------
INSERT INTO x_file_category (file_category, file_suffix, deleted, create_by, create_time) VALUES
('video', 'mp4', FALSE, 0, NOW()),
('video', 'mkv', FALSE, 0, NOW()),
('video', 'avi', FALSE, 0, NOW()),
('video', 'mov', FALSE, 0, NOW()),
('video', 'wmv', FALSE, 0, NOW()),
('video', 'flv', FALSE, 0, NOW()),
('audio', 'mp3', FALSE, 0, NOW()),
('audio', 'wav', FALSE, 0, NOW()),
('audio', 'flac', FALSE, 0, NOW()),
('audio', 'aac', FALSE, 0, NOW()),
('document', 'txt', FALSE, 0, NOW()),
('document', 'docx', FALSE, 0, NOW()),
('document', 'pdf', FALSE, 0, NOW()),
('document', 'epub', FALSE, 0, NOW()),
('other', 'zip', FALSE, 0, NOW()),
('other', 'rar', FALSE, 0, NOW()),
('other', '7z', FALSE, 0, NOW()),
('other', 'jpg', FALSE, 0, NOW()),
('other', 'png', FALSE, 0, NOW()),
('other', 'gif', FALSE, 0, NOW()),
('other', 'tiff', FALSE, 0, NOW());

File diff suppressed because it is too large Load Diff

1302
backend/db/init-table.sql Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,62 @@
-- =============================================================
-- 升级脚本:机构上传配置(文件上传类型/大小限制)
-- 日期:2026-09-14
-- 说明:新增 x_upload_config 表(一行对应一个机构)。
-- 机构管理员可配置本机构允许上传的文件后缀白名单、
-- 单文件大小上限、上传总开关;未配置的机构默认允许全部。
-- 拦截点:网盘上传落库(normal_user_file/create)。
-- 执行方式:MySQL/达梦(DM8 MySQL兼容模式)执行第 1 节;
-- PostgreSQL 执行第 2 节。
-- =============================================================
-- -------------------------------------------------------------
-- 1. MySQL / 达梦 DM8
-- -------------------------------------------------------------
CREATE TABLE `x_upload_config` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`org_id` bigint(20) DEFAULT NULL COMMENT '机构id',
`upload_enabled` bit(1) DEFAULT b'1' COMMENT '是否允许上传',
`allow_all_type` bit(1) DEFAULT b'1' COMMENT '是否允许全部类型(false时按allowed_suffixes白名单校验)',
`allowed_suffixes` varchar(1024) DEFAULT NULL COMMENT '允许的文件后缀(逗号分隔,小写),allow_all_type=false时生效',
`max_file_size` bigint(20) DEFAULT NULL COMMENT '单文件大小上限(MB),空或0不限制',
`deleted` bit(1) DEFAULT b'0' COMMENT '是否删除',
`create_by` bigint(20) DEFAULT NULL COMMENT '创建人',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_by` bigint(20) DEFAULT NULL COMMENT '修改人',
`update_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
`proxy_by` bigint(20) DEFAULT NULL COMMENT '代理人',
PRIMARY KEY (`id`) USING BTREE,
UNIQUE KEY `uk_org_id` (`org_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='机构上传配置';
-- -------------------------------------------------------------
-- 2. PostgreSQL
-- -------------------------------------------------------------
CREATE TABLE x_upload_config (
id BIGSERIAL PRIMARY KEY,
org_id BIGINT DEFAULT NULL,
upload_enabled BOOLEAN DEFAULT TRUE,
allow_all_type BOOLEAN DEFAULT TRUE,
allowed_suffixes VARCHAR(1024) DEFAULT NULL,
max_file_size BIGINT DEFAULT NULL,
deleted BOOLEAN DEFAULT FALSE,
create_by BIGINT DEFAULT NULL,
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
update_by BIGINT DEFAULT NULL,
update_time TIMESTAMP DEFAULT NULL,
proxy_by BIGINT DEFAULT NULL,
CONSTRAINT uk_upload_config_org_id UNIQUE (org_id)
);
COMMENT ON TABLE x_upload_config IS '机构上传配置';
COMMENT ON COLUMN x_upload_config.id IS '主键ID';
COMMENT ON COLUMN x_upload_config.org_id IS '机构id';
COMMENT ON COLUMN x_upload_config.upload_enabled IS '是否允许上传';
COMMENT ON COLUMN x_upload_config.allow_all_type IS '是否允许全部类型(false时按allowed_suffixes白名单校验)';
COMMENT ON COLUMN x_upload_config.allowed_suffixes IS '允许的文件后缀(逗号分隔,小写),allow_all_type=false时生效';
COMMENT ON COLUMN x_upload_config.max_file_size IS '单文件大小上限(MB),空或0不限制';
COMMENT ON COLUMN x_upload_config.deleted IS '是否删除';
COMMENT ON COLUMN x_upload_config.create_by IS '创建人';
COMMENT ON COLUMN x_upload_config.create_time IS '创建时间';
COMMENT ON COLUMN x_upload_config.update_by IS '修改人';
COMMENT ON COLUMN x_upload_config.update_time IS '修改时间';
COMMENT ON COLUMN x_upload_config.proxy_by IS '代理人';

View File

@@ -0,0 +1,90 @@
-- =====================================================================
-- s-file 上线数据库脚本(2026-09-15 版本)
-- 适用场景:已有生产库升级(老版本 → 当前版本)
--
-- 本次版本(用户启停/注销、机构账号查询、手机号查记录、机构上传配置)
-- 相对老版本的数据库变更只有一项:
-- 新增表 x_upload_config(机构上传配置)
-- 其余功能(启停/注销/查询)全部复用已有表字段,无需任何变更。
--
-- 【执行说明】
-- * 生产主力库为达梦 DM8(MySQL 兼容模式)→ 只执行【第一部分】
-- * 若目标库是 PostgreSQL → 只执行【第二部分】
-- * 执行前建议备份;执行后核对【第三部分】的验证 SQL
-- * 可重复执行(幂等):表已存在时再执行会报"已存在"错误,属正常,
-- 说明该脚本已执行过,无需处理
-- =====================================================================
-- =====================================================================
-- 第一部分:达梦 DM8 / MySQL
-- =====================================================================
CREATE TABLE x_upload_config (
id BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
org_id BIGINT DEFAULT NULL COMMENT '机构id',
upload_enabled BIT(1) DEFAULT b'1' COMMENT '是否允许上传',
allow_all_type BIT(1) DEFAULT b'1' COMMENT '是否允许全部类型(false时按allowed_suffixes白名单校验)',
allowed_suffixes VARCHAR(1024) DEFAULT NULL COMMENT '允许的文件后缀(逗号分隔,小写),allow_all_type=false时生效',
max_file_size BIGINT DEFAULT NULL COMMENT '单文件大小上限(MB),空或0不限制',
deleted BIT(1) DEFAULT b'0' COMMENT '是否删除',
create_by BIGINT DEFAULT NULL COMMENT '创建人',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
update_by BIGINT DEFAULT NULL COMMENT '修改人',
update_time DATETIME DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
proxy_by BIGINT DEFAULT NULL COMMENT '代理人',
PRIMARY KEY (id) USING BTREE,
UNIQUE KEY uk_org_id (org_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='机构上传配置';
-- 说明:不需要初始化数据。
-- 未配置的机构 = 默认策略(允许上传全部类型、不限大小),
-- 由各机构管理员在管理后台"上传配置"页面自行配置后落库。
-- =====================================================================
-- 第二部分:PostgreSQL
-- =====================================================================
-- CREATE TABLE x_upload_config (
-- id BIGSERIAL PRIMARY KEY,
-- org_id BIGINT DEFAULT NULL,
-- upload_enabled BOOLEAN DEFAULT TRUE,
-- allow_all_type BOOLEAN DEFAULT TRUE,
-- allowed_suffixes VARCHAR(1024) DEFAULT NULL,
-- max_file_size BIGINT DEFAULT NULL,
-- deleted BOOLEAN DEFAULT FALSE,
-- create_by BIGINT DEFAULT NULL,
-- create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
-- update_by BIGINT DEFAULT NULL,
-- update_time TIMESTAMP DEFAULT NULL,
-- proxy_by BIGINT DEFAULT NULL,
-- CONSTRAINT uk_upload_config_org_id UNIQUE (org_id)
-- );
-- COMMENT ON TABLE x_upload_config IS '机构上传配置';
-- COMMENT ON COLUMN x_upload_config.id IS '主键ID';
-- COMMENT ON COLUMN x_upload_config.org_id IS '机构id';
-- COMMENT ON COLUMN x_upload_config.upload_enabled IS '是否允许上传';
-- COMMENT ON COLUMN x_upload_config.allow_all_type IS '是否允许全部类型(false时按allowed_suffixes白名单校验)';
-- COMMENT ON COLUMN x_upload_config.allowed_suffixes IS '允许的文件后缀(逗号分隔,小写),allow_all_type=false时生效';
-- COMMENT ON COLUMN x_upload_config.max_file_size IS '单文件大小上限(MB),空或0不限制';
-- COMMENT ON COLUMN x_upload_config.deleted IS '是否删除';
-- COMMENT ON COLUMN x_upload_config.create_by IS '创建人';
-- COMMENT ON COLUMN x_upload_config.create_time IS '创建时间';
-- COMMENT ON COLUMN x_upload_config.update_by IS '修改人';
-- COMMENT ON COLUMN x_upload_config.update_time IS '修改时间';
-- COMMENT ON COLUMN x_upload_config.proxy_by IS '代理人';
-- =====================================================================
-- 第三部分:执行后验证(达梦/MySQL 语法)
-- =====================================================================
-- 1) 表存在且为空(0 行正常)
-- SELECT COUNT(*) FROM x_upload_config;
-- 2) 唯一约束存在(应返回 uk_org_id)
-- SELECT INDEX_NAME FROM USER_INDEXES WHERE TABLE_NAME = 'X_UPLOAD_CONFIG';
-- 3) (可选)回滚脚本 —— 如需回滚上线:
-- DROP TABLE x_upload_config;

View File

@@ -0,0 +1,71 @@
-- =============================================================
-- 升级脚本:MeshSafe 文件安全检测对接
-- 日期:2026-11-01
-- 说明:新增 x_file_security 表(按文件编码维度记录安全检测结果)。
-- 文件上传/传输接收解包后自动提交 MeshSafe 检测,
-- 检测结论(通过/不通过)回写本表;同一文件允许多条检测
-- 记录(人工复核、重新检测均新增记录),业务取最新一条生效。
-- 检测不通过的文件禁止分享与跨网传输(下载预览不受限)。
-- 执行方式:MySQL/达梦(DM8 MySQL兼容模式)执行第 1 节;
-- PostgreSQL 执行第 2 节。
-- =============================================================
-- -------------------------------------------------------------
-- 1. MySQL / 达梦 DM8
-- -------------------------------------------------------------
CREATE TABLE `x_file_security` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`file_code` varchar(32) DEFAULT NULL COMMENT '文件编码',
`task_id` varchar(64) DEFAULT NULL COMMENT 'MeshSafe检测任务ID',
`scan_status` varchar(2) DEFAULT NULL COMMENT '检测状态:1检测中/2通过/3不通过/4检测失败',
`action` varchar(32) DEFAULT NULL COMMENT 'MeshSafe处置动作:pass/manual_review/reject',
`reason` varchar(1024) DEFAULT NULL COMMENT '检测结论说明',
`result_json` text DEFAULT NULL COMMENT '检测完整结果JSON',
`review_comment` varchar(512) DEFAULT NULL COMMENT '人工复核说明(人工改判时有值)',
`deleted` bit(1) DEFAULT b'0' COMMENT '是否删除',
`create_by` bigint(20) DEFAULT NULL COMMENT '创建人',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_by` bigint(20) DEFAULT NULL COMMENT '修改人',
`update_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
`proxy_by` bigint(20) DEFAULT NULL COMMENT '代理人',
PRIMARY KEY (`id`) USING BTREE,
KEY `idx_file_security_file_code` (`file_code`) USING BTREE,
KEY `idx_file_security_scan_status` (`scan_status`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='文件安全检测记录';
-- -------------------------------------------------------------
-- 2. PostgreSQL
-- -------------------------------------------------------------
CREATE TABLE x_file_security (
id BIGSERIAL PRIMARY KEY,
file_code VARCHAR(32) DEFAULT NULL,
task_id VARCHAR(64) DEFAULT NULL,
scan_status VARCHAR(2) DEFAULT NULL,
action VARCHAR(32) DEFAULT NULL,
reason VARCHAR(1024) DEFAULT NULL,
result_json TEXT DEFAULT NULL,
review_comment VARCHAR(512) DEFAULT NULL,
deleted BOOLEAN DEFAULT FALSE,
create_by BIGINT DEFAULT NULL,
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
update_by BIGINT DEFAULT NULL,
update_time TIMESTAMP DEFAULT NULL,
proxy_by BIGINT DEFAULT NULL
);
CREATE INDEX idx_file_security_file_code ON x_file_security(file_code);
CREATE INDEX idx_file_security_scan_status ON x_file_security(scan_status);
COMMENT ON TABLE x_file_security IS '文件安全检测记录';
COMMENT ON COLUMN x_file_security.id IS '主键ID';
COMMENT ON COLUMN x_file_security.file_code IS '文件编码';
COMMENT ON COLUMN x_file_security.task_id IS 'MeshSafe检测任务ID';
COMMENT ON COLUMN x_file_security.scan_status IS '检测状态:1检测中/2通过/3不通过/4检测失败';
COMMENT ON COLUMN x_file_security.action IS 'MeshSafe处置动作:pass/manual_review/reject';
COMMENT ON COLUMN x_file_security.reason IS '检测结论说明';
COMMENT ON COLUMN x_file_security.result_json IS '检测完整结果JSON';
COMMENT ON COLUMN x_file_security.review_comment IS '人工复核说明(人工改判时有值)';
COMMENT ON COLUMN x_file_security.deleted IS '是否删除';
COMMENT ON COLUMN x_file_security.create_by IS '创建人';
COMMENT ON COLUMN x_file_security.create_time IS '创建时间';
COMMENT ON COLUMN x_file_security.update_by IS '修改人';
COMMENT ON COLUMN x_file_security.update_time IS '修改时间';
COMMENT ON COLUMN x_file_security.proxy_by IS '代理人';

View File

@@ -0,0 +1,118 @@
server:
port: 8081
spring:
application:
name: s-file
datasource:
url: jdbc:postgresql://127.0.0.1:54330/s_file
driver-class-name: org.postgresql.Driver
username: sfile
password: sfile123
druid:
initial-size: 2
min-idle: 2
max-active: 10
validation-query: SELECT 1
redis:
host: 127.0.0.1
port: 63800
timeout: 5000
lettuce:
pool:
max-active: 8
max-idle: 4
rabbitmq:
host: 127.0.0.1
port: 56730
username: sfile
password: sfile123
servlet:
multipart:
max-file-size: 500MB
max-request-size: 500MB
main:
allow-bean-definition-overriding: true
mybatis-plus:
mapper-locations: classpath*:com/senmesh/**/mapper/*.xml
# 平台编码(init-data 中 uc_platform.platform_code=1)
system:
code: 1
# 发号器雪花 workerId(键格式: code.snowflake.workid.<本机IP>,方括号与裸键双写兼容)
"[code.snowflake.workid.192.168.71.85]": 1
"[code.snowflake.workid.172.27.160.1]": 1
"[code.snowflake.workid.172.28.16.1]": 1
"[code.snowflake.workid.127.0.0.1]": 1
code.snowflake.workid.192.168.71.85: 1
code.snowflake.workid.172.27.160.1: 1
code.snowflake.workid.172.28.16.1: 1
code.snowflake.workid.127.0.0.1: 1
security:
jwt:
enable: true
secret: sfile-local-test-secret-key-please-change-0123456789abcdef
expiration: 86400000
header: Authorization
ignoring: /error,/favicon.ico
login-page: /login
superior-only: false
use-local: true
allow-if-all-abstain-decisions: true
oauth2:
enable: false
not-logs: ""
# 新用户初始密码
user:
default:
password: Qaz123567!@#
file:
upload-dir: D:/zcodedata/sfile/upload
storage:
type: local
transfer:
# 内网侧
location: "1"
send-dir: D:/zcodedata/sfile/transfer/send
receive-dir: D:/zcodedata/sfile/transfer/receive
unpack-risk-check: false
dify:
risk:
rule:
api-key: test-rule-key
scan:
api-key: test-scan-key
# MeshSafe 文件安全检测(对接本地 MeshSafe venv 实例,端口 48443)
# enabled=false 完全关闭检测:不送检/不拦截/预检返回空/前端隐藏检测列
meshsafe:
enabled: true
base-url: http://127.0.0.1:48443
client-id: test-client
secret: test-secret
channel: sfile-local
timeout-seconds: 120
fail-open: true
poll-timeout-minutes: 60
review-sync-to-meshsafe: true
poll:
cron: "0 */10 * * * ?"
rabbitmq:
custom:
topic-exchange-name: s-file-local-topic
delayed-exchange-name: s-file-local-delayed
broadcast-id: s-file-local-broadcast
swagger:
enable: true
logging:
level:
com.senmesh: info

View File

@@ -0,0 +1,108 @@
server:
port: 8080
spring:
application:
name: s-file
datasource:
url: jdbc:postgresql://sfile-pg:5432/s_file
driver-class-name: org.postgresql.Driver
username: postgres
password: sfile123
druid:
initial-size: 2
min-idle: 2
max-active: 10
validation-query: SELECT 1
redis:
host: sfile-redis
port: 6379
timeout: 5000
lettuce:
pool:
max-active: 8
max-idle: 4
rabbitmq:
host: sfile-mq
port: 5672
username: guest
password: guest
servlet:
multipart:
max-file-size: 500MB
max-request-size: 500MB
main:
allow-bean-definition-overriding: true
mybatis-plus:
mapper-locations: classpath*:com/senmesh/**/mapper/*.xml
# 平台编码(init-data 中 uc_platform.platform_code=1)
system:
code: 1
# 发号器雪花 workerId(键格式: code.snowflake.workid.<容器IP>)
"[code.snowflake.workid.172.20.0.10]": 1
security:
jwt:
enable: true
secret: sfile-functional-test-secret-key-please-change-0123456789abcdef
expiration: 86400000
header: Authorization
ignoring: /error,/favicon.ico
login-page: /login
superior-only: false
use-local: true
allow-if-all-abstain-decisions: true
oauth2:
enable: false
not-logs: ""
# 新用户初始密码
user:
default:
password: Qaz123567!@#
file:
upload-dir: /data/upload
storage:
type: local
transfer:
# 内网侧
location: "1"
send-dir: /data/transfer/send
receive-dir: /data/transfer/receive
unpack-risk-check: false
dify:
risk:
rule:
api-key: test-rule-key
scan:
api-key: test-scan-key
# MeshSafe 文件安全检测(上传/接收解包后自动送检,不通过禁止分享传输)
meshsafe:
enabled: false
base-url: https://meshsafe.internal
client-id: s-file
secret: change-me
channel: default
timeout-seconds: 120
fail-open: true
poll-timeout-minutes: 60
review-sync-to-meshsafe: true
rabbitmq:
custom:
topic-exchange-name: s-file-test-topic
delayed-exchange-name: s-file-test-delayed
broadcast-id: s-file-test-broadcast
swagger:
enable: false
logging:
level:
com.senmesh: info

View File

@@ -0,0 +1,171 @@
# 前端接口文档 —— 文件安全检测(MeshSafe 对接)
> 版本:v1.0(2026-11-01)
> 适用后端:s-file(alita-bjy-uygur)
> Swagger 分组:「文件安全检测」
## 功能说明
对接独立文件安全检测服务 **MeshSafe**:文件上传(网盘上传)与跨网传输接收解包后,自动提交 MeshSafe 检测,检测结论回写到本系统并标记在文件上。
核心规则:
- **检测不通过(误报可复核)**:文件**只能查看/下载/预览**,**禁止分享与跨网传输**;管理员可人工复核修改检测结果(需填写复核说明),复核通过后即可正常分享/传输;
- **文件夹部分文件不通过**:分享/传输弹窗内提示未通过的文件清单,用户确认后**仅分享/传输检测通过的内容**(请求带 `onlyPassed=true`,后端排除不通过/检测中的文件);不确认则整体拦截;
- **检测中**:禁止分享与传输(检测通常秒级完成,稍后重试即可);
- **检测失败**:默认不阻断业务(仅标记"检测失败",可重新检测);`meshsafe.fail-open=false` 时检测失败同样禁止分享/传输;
- **未检测**:不阻断(历史文件/开关未开启期间上传的文件);
- 同一文件允许多条检测记录(重新检测、人工复核均新增记录),**列表与校验均取最新一条**;
- 人工复核会尽力回写 MeshSafe 的人工复核接口(失败仅告警,不影响本地生效)。
检测状态(`securityStatus`)取值:
| 值 | 含义 | 能否分享/传输 |
|---|---|---|
| 空 | 未检测 | 能 |
| 1 | 检测中 | 否 |
| 2 | 通过 | 能 |
| 3 | 不通过 | 否 |
| 4 | 检测失败 | 默认能(fail-open=false 时否) |
## 通用约定
与既有接口一致:登录态调用;统一返回 `AlitaResult<T>`(`code="200"` 且 `success=true` 成功;业务失败 `success=false`,`msg` 为提示文案)。
**总开关**:`meshsafe.enabled=false` 时检测功能整体关闭——上传/解包不送检、分享/传输不拦截、预检返回空、复核与重新检测接口报"文件安全检测未开启";前端通过 `GET /file_security/enabled` 获取开关状态,关闭时列表隐藏"安全检测"列。
## 文件列表新增字段
`GET /user_file/page`、`GET /user_file/list`、`GET /user_file/record/page_by_phone` 等返回的文件记录新增以下字段(前端据此展示与禁用操作):
| 字段 | 类型 | 说明 |
|---|---|---|
| securityStatus | String | 安全检测状态:1检测中/2通过/3不通过/4检测失败,null 为未检测 |
| securityAction | String | MeshSafe 处置动作:pass/manual_review/reject |
| securityReason | String | 检测结论说明(可直接展示给用户,人工复核时为复核说明) |
| securityReviewComment | String | 人工复核说明(人工改判时有值) |
---
## 1. 查询文件最新检测结果
- **URL**:`GET /file_security/detail`
- **权限**:所有登录用户
- **参数**:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| fileCode | String | 是 | 文件编码 |
- **返回**:`AlitaResult<FileSecurityDTO>`,无检测记录时 `data` 为 null
**FileSecurityDTO 字段**
| 字段 | 类型 | 说明 |
|---|---|---|
| fileCode | String | 文件编码 |
| taskId | String | MeshSafe 检测任务 ID |
| scanStatus | String | 检测状态:1检测中/2通过/3不通过/4检测失败 |
| action | String | MeshSafe 处置动作:pass/manual_review/reject |
| reason | String | 检测结论说明 |
| resultJson | String | 检测完整结果 JSON(原始响应) |
| reviewComment | String | 人工复核说明(人工改判时有值) |
| createTime | String | 记录时间 yyyy-MM-dd HH:mm:ss |
## 2. 人工复核修改检测结果
- **URL**:`POST /file_security/review`
- **权限**:平台超管(全部)/ 机构管理员(仅本机构用户的文件)
- **Content-Type**:`application/json`
- **参数**:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| fileCode | String | 是 | 文件编码 |
| decision | String | 是 | 复核结论:`pass` 通过 / `reject` 不通过 |
| comment | String | 是 | 复核说明(审计要求,必填),例如"经人工查验为误报" |
- **返回**:`AlitaResult<FileSecurityDTO>`(本次复核生成的最新检测记录)
- **行为**:新增一条检测记录留痕并立即生效;若该文件此前有 MeshSafe 任务 ID,会尽力回写 MeshSafe 人工复核接口(失败仅告警)
**错误提示**:无检测记录、复核结论不合法、无操作权限、非本机构文件时返回 `success=false` 并附 `msg`。
## 3. 分享/传输预检
- **URL**:`GET /file_security/share_check`
- **权限**:所有登录用户(校验本人文件)
- **参数**:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| userFileId | Long | 是 | 用户文件ID(普通文件/文件夹) |
- **返回**:`AlitaResult<FileSecurityShareCheckDTO>`
**FileSecurityShareCheckDTO 字段**
| 字段 | 类型 | 说明 |
|---|---|---|
| fileType | String | 1文件/2文件夹 |
| totalFileCount | Integer | 目录内文件总数(不含目录) |
| blockedCount | Integer | 检测不通过数量 |
| scanningCount | Integer | 检测中数量 |
| failedCount | Integer | 检测失败数量 |
| blockedFileNames | List&lt;String&gt; | 检测不通过文件名 |
| scanningFileNames | List&lt;String&gt; | 检测中文件名 |
| failedFileNames | List&lt;String&gt; | 检测失败文件名 |
| excludeFileIds | List&lt;Long&gt; | 确认跳过时需排除的用户文件ID |
**前端用法**:文件夹分享/传输前调用,`excludeFileIds` 非空时弹窗展示文件清单,用户确认后分享/传输请求带 `onlyPassed=true`。
## 4. 分享/传输的 onlyPassed 参数
`POST /share_user_file/create`、`POST /transfer_user_file/create` 请求体新增可选字段:
| 参数 | 类型 | 说明 |
|---|---|---|
| onlyPassed | Boolean | 文件夹内存在检测未通过文件且用户已确认跳过时传 `true`,仅复制/传输检测通过的内容;缺省或 `false` 时维持整体拦截 |
规则:单文件检测不通过 → 无论该参数一律拦截;文件夹 + `onlyPassed=true` → 排除不通过/检测中文件;文件夹内全部被拦截 → 报错「文件夹内没有检测通过的文件,无法分享/传输」。
## 5. 重新提交安全检测
- **URL**:`POST /file_security/rescan`
- **权限**:平台超管(全部)/ 机构管理员(仅本机构用户的文件)
- **参数**(query):
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| fileCode | String | 是 | 文件编码 |
- **返回**:`AlitaResult<Boolean>`,受理成功返回 `true`(异步执行,结果通过列表/详情查询获取)
- **行为**:新增一条"检测中"记录并异步提交 MeshSafe;该文件正在检测中时拒绝重复提交
---
## 后端对接配置(运维)
```yaml
meshsafe:
enabled: true # 总开关,false 时完全不送检、不校验
base-url: https://meshsafe.internal
client-id: s-file # MeshSafe 分配的调用方标识
secret: "***" # MeshSafe 分配的密钥(请勿泄露)
channel: default # 业务通道标识(仅用于 MeshSafe 日志追溯)
timeout-seconds: 120 # 单次请求超时
fail-open: true # 检测服务异常时是否放行(true仅标记检测失败)
poll-timeout-minutes: 60 # 检测中记录超时时长,超时标记检测失败
review-sync-to-meshsafe: true # 人工复核是否回写 MeshSafe
poll:
cron: "0 */30 * * * ?" # 轮询未完成检测任务的定时任务(默认30秒)
```
数据表:`x_file_security`(见 `db/upgrade-20261101-meshsafe-file-security.sql`)。
送检触发点:
1. **网盘上传**:`POST /normal_user_file/create` 落库后异步送检;
2. **跨网传输接收**:传输解包定时任务创建文件记录后逐个异步送检(保护"外传内"方向);
3. **手动重新检测**:上述 `/file_security/rescan`。
分享/传输入口强校验:`POST /share_user_file/create`、`POST /transfer_user_file/create` 均校验文件及子文件的最新检测状态,不通过/检测中直接拒绝,`msg` 会带上具体文件名与原因。

View File

@@ -0,0 +1,181 @@
# 前端接口文档 —— 机构上传配置(文件上传类型/大小限制)
> 版本:v1.0(2026-09-14)
> 适用后端:s-file(alita-bjy-uygur)
> Swagger 分组:「机构上传配置」
## 功能说明
机构管理员可以为本机构配置网盘上传策略:
- **上传总开关**:关闭后本机构用户不能上传任何文件到网盘;
- **类型白名单**:可限制只允许上传指定后缀的文件(如 `doc、docx、pdf、jpg`);
- **单文件大小上限**:单位 MB,超过即拒绝。
规则约定:
- 配置**按机构隔离**,作用于当前登录机构,管理的是本机构用户的**网盘上传**(`normal_user_file/create` 落库时强制校验);
- **未配置过的机构 = 默认策略**:允许上传、允许全部类型、不限大小(对现有用户完全兼容);
- 个人用户(无登录机构)不受机构配置限制;
- 后缀不区分大小写,配置时可带可不带前导点(`PDF`、`.pdf`、`pdf` 等价),服务端统一转小写存储;
- 文件夹创建不受类型/大小校验影响;
- 文件**传输(发送/接收)与分享不受此配置影响**,该配置只管上传。
## 通用约定
与既有接口一致:登录态调用;统一返回 `AlitaResult<T>`(`code="200"` 且 `success=true` 成功;业务失败 `success=false`,`msg` 为提示文案);时间格式 `yyyy-MM-dd HH:mm:ss`。
---
## 1. 查询当前机构的上传配置
- **URL**:`GET /upload_config/info`
- **权限**:所有登录用户(前端可在上传前读取并据此过滤可选文件类型)
- **参数**:无(按当前登录机构查询)
- **返回**:`AlitaResult<UploadConfigDTO>`
**UploadConfigDTO 字段**
| 字段 | 类型 | 说明 |
|---|---|---|
| orgId | Long | 机构 id |
| uploadEnabled | Boolean | 是否允许上传 |
| allowAllType | Boolean | 是否允许全部类型 |
| allowedSuffixes | List&lt;String&gt; | 允许的后缀(小写、不含点),`allowAllType=false` 时有值,否则为 null |
| maxFileSize | Long | 单文件大小上限(MB),null 或 0 表示不限制 |
| configured | Boolean | **机构是否已配置过**。`false` 表示当前是默认策略(允许上传全部类型、不限大小),前端此时可不限制文件选择器 |
**返回示例(已配置)**
```json
{
"code": "200",
"success": true,
"data": {
"orgId": 12,
"uploadEnabled": true,
"allowAllType": false,
"allowedSuffixes": ["doc", "docx", "pdf", "jpg", "png"],
"maxFileSize": 200,
"configured": true
}
}
```
**返回示例(未配置过)**
```json
{
"code": "200",
"success": true,
"data": {
"orgId": 12,
"uploadEnabled": true,
"allowAllType": true,
"allowedSuffixes": null,
"maxFileSize": null,
"configured": false
}
}
```
---
## 2. 保存机构的上传配置
Upsert:该机构无配置则新建,有则更新。
- **URL**:`POST /upload_config/modify`
- **权限**:**机构管理员** 或 平台超管(其他用户返回「当前用户无操作权限」)
- **Body**(application/json)
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| uploadEnabled | Boolean | 是 | 是否允许上传 |
| allowAllType | Boolean | 是 | 是否允许全部类型 |
| allowedSuffixes | List&lt;String&gt; | 条件必填 | `allowAllType=false` 时必填且不能为空;后缀可带可不带点,自动小写去重 |
| maxFileSize | Long | 否 | 单文件上限(MB),null 或 0 不限制;传负数报错 |
**校验失败提示**
| 场景 | msg |
|---|---|
| 非机构管理员/超管 | 当前用户无操作权限 |
| 白名单为空 | 未允许全部类型时,允许的文件后缀不能为空 |
| maxFileSize ≤ 0 | 单文件大小上限必须大于0 |
| 并发修改 | 上传配置正在修改,请稍后重试 |
**请求示例(只允许文档和图片,单个最大 200MB)**
```json
POST /upload_config/modify
{
"uploadEnabled": true,
"allowAllType": false,
"allowedSuffixes": ["doc", "docx", "xls", "xlsx", "pdf", "jpg", "png"],
"maxFileSize": 200
}
```
**请求示例(禁止本机构上传)**
```json
POST /upload_config/modify
{
"uploadEnabled": false,
"allowAllType": true
}
```
**返回示例**
```json
{ "code": "200", "success": true, "data": true }
```
---
## 3. 上传预检(可选,提升体验)
按文件名/大小对当前机构配置做一次预检,用于**选择文件后、真正上传前**给出友好提示,避免白传一遍物理文件才被拒绝。
- **URL**:`GET /upload_config/check`
- **权限**:所有登录用户
- **参数**(URL query)
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| fileName | String | 是 | 完整文件名(含后缀) |
| fileSize | Long | 否 | 文件大小(字节),不传则跳过大小校验 |
- **返回**:通过时 `{"code":"200","success":true,"data":true}`;不通过时 `success=false`,`msg` 说明原因(与真实上传被拦截时的文案一致)。
**不通过返回示例**
```json
{
"code": "700",
"success": false,
"msg": "仅允许上传后缀为 doc、docx、pdf、jpg、png 的文件",
"data": null
}
```
---
## 4. 上传被拦截时的服务端行为
用户上传最终在网盘创建记录接口(`POST /normal_user_file/create`)被强制校验,不通过时接口返回 `success=false`,`msg` 为以下之一:
| 场景 | msg |
|---|---|
| 机构关闭上传 | 当前机构已禁止上传文件,请联系机构管理员 |
| 后缀不在白名单 | 仅允许上传后缀为 doc、docx、pdf 的文件(动态列出) |
| 机构白名单为空(异常配置) | 当前机构未配置可上传的文件类型,请联系机构管理员 |
| 超过大小上限 | 上传文件过大,单文件最大不得超过200MB(动态数值) |
**前端建议**:
1. 上传页加载时调 `GET /upload_config/info`:`uploadEnabled=false` 时直接隐藏/禁用上传入口;`allowAllType=false` 时设置文件选择器的 accept 过滤;
2. 选择文件后可先调 `GET /upload_config/check` 预检(可选);
3. 真实上传两步(`/file/upload` → `/normal_user_file/create`)中第二步失败时直接 toast `msg`。

View File

@@ -0,0 +1,498 @@
# 前端接口文档 —— 用户管理增强 & 机构账号查询 & 按手机号查询文件记录
> 版本:v1.2(2026-10-27,新增 1.4 修改用户姓名、1.5 批量创建机构用户)
> 适用后端:s-file(alita-bjy-uygur)
> Swagger 分组:「用户」「用户文件」
---
## 0. 通用约定
### 0.1 请求与认证
- 所有接口均需**登录后调用**,认证 token 按现有方式携带(header 或 cookie,与系统其他接口一致)。
- 新增接口均要求**平台管理员(超管)身份**(`uc_user.supeiror = true`),非管理员调用统一返回失败,提示「当前用户无操作权限」。
### 0.2 统一返回结构 `AlitaResult<T>`
```json
{
"requestId": "xxx",
"code": "200",
"success": true,
"msg": null,
"data": { ... }
}
```
| 字段 | 说明 |
|---|---|
| code | `"200"` 成功;业务失败时 success=false,msg 为错误提示(如「不能禁用当前登录账号」) |
| success | 是否成功 |
| data | 业务数据,失败时为 null |
### 0.3 分页请求与返回
分页查询接口通过 **URL query 参数** 传递分页信息,返回结构 `Page<T>`:
```
pageNo 第几页,默认 1
pageSize 页宽,默认 20,上限 1000
needPage 是否分页,默认 true(无需传)
```
```json
{
"pageNo": 1,
"pageSize": 20,
"pageCount": 5,
"totalCount": 88,
"entityList": [ ... ]
}
```
### 0.4 通用枚举(前后端共用)
**用户状态 `status`**
| 值 | 含义 |
|---|---|
| 1 | 待激活 |
| 2 | 已激活 |
| 3 | 已注销 |
**是否启用 `enabled`**:`true` 启用,`false` 禁用。
**文件状态 `fileStatus`**
| 值 | 含义 |
|---|---|
| 1 | 正常 |
| 2 | 失效 |
| 11 | 解析中 |
| 12 | 待审核 |
| 13 | 传输中 |
| 14 | 失败 |
**记录类型 `recordType`**(本文档 3.4 节专用)
| 值 | 含义 |
|---|---|
| 1 | 上传记录 |
| 2 | 发送记录(本端发起文件传输) |
| 3 | 接收记录(本端接收文件传输) |
> 发送/接收与部署侧有关,但**前端无需关心**:传入 `recordType` 后,后端按部署配置(内网/外网)自动映射到对应的 `root_type`;返回列表中每条记录也已回填 `recordType` / `recordTypeDesc`。
---
## 1. 用户管理:启用 / 禁用 / 注销
### 1.1 启用用户
将被禁用的账号重新启用(幂等:已启用的账号重复调用仍返回成功)。
- **URL**:`POST /user/enable`
- **权限**:平台管理员
- **Body**(application/json)
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| userId | Long | 是 | 目标用户 id |
- **返回**:`AlitaResult<Boolean>`,data 为 `true`
- **失败场景**:用户不存在;账号已注销(提示「已注销账号不可启用」);非管理员
**请求示例**
```json
POST /user/enable
{ "userId": 10086 }
```
**返回示例**
```json
{ "code": "200", "success": true, "data": true }
```
### 1.2 禁用用户
禁用账号,禁用后该用户无法登录/使用系统(后端登录态校验会实时读取启用状态,被禁用用户下次请求即被拒绝)。幂等:已禁用的账号重复调用仍返回成功。
- **URL**:`POST /user/disable`
- **权限**:平台管理员
- **Body**(application/json)
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| userId | Long | 是 | 目标用户 id |
- **返回**:`AlitaResult<Boolean>`,data 为 `true`
- **失败场景**:
- 用户不存在
- `userId` 为当前登录人(提示「不能禁用当前登录账号」)
- 目标是超级管理员(提示「不能禁用超级管理员账号」)
- 目标已注销(提示「已注销账号无需禁用」)
**请求示例**
```json
POST /user/disable
{ "userId": 10086 }
```
### 1.3 注销用户(管理员直接注销)
管理员将账号直接置为**已注销**(status=3)并同步禁用(enabled=false),**不进入注销申请审批流程**。注销后账号不可恢复、不可登录。幂等:已注销账号重复调用返回成功。
- **URL**:`POST /user/admin_cancel`
- **权限**:平台管理员
- **Body**(application/json)
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| userId | Long | 是 | 目标用户 id |
- **返回**:`AlitaResult<Void>`,data 为 null
- **失败场景**:
- 用户不存在
- `userId` 为当前登录人(提示「不能注销当前登录账号」)
- 目标是超级管理员(提示「不能注销超级管理员账号」)
- 并发重复操作(提示「该用户正在注销,请勿重复操作」)
**请求示例**
```json
POST /user/admin_cancel
{ "userId": 10086 }
```
> **与既有接口的区别**:既有 `POST /user/cancel` 为机构内注销(限定当前登录机构、且要求目标账号启用中),供原「注销申请/同意」流程使用,保持不变;本接口供系统管理员在用户管理页面跨机构直接注销。
### 1.4 修改用户姓名
修改用户账号的真实姓名(`real_name`)。平台超管可修改全部用户;机构管理员仅可修改本机构用户(同一账号属于多个机构时,改的是账号本身,各机构下展示同步生效)。
- **URL**:`POST /user/modify_real_name`
- **权限**:平台超管(全部用户)/ 机构管理员(限本机构用户)
- **Body**(application/json)
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| userId | Long | 是 | 目标用户 id |
| realName | String | 是 | 新姓名,2-30 位,仅支持中文或英文姓名 |
- **返回**:`AlitaResult<Boolean>`,data 为 `true`
- **失败场景**:
- 用户不存在
- 目标已注销(提示「已注销账号不可修改姓名」)
- 姓名格式错误(提示「姓名格式错误!(长度2-30位,仅支持中文或英文姓名)」)
- 与原姓名一致(提示「姓名与原姓名一致,无需修改!」)
- 机构管理员操作非本机构用户(提示「只能操作本机构的用户」)
**请求示例**
```json
POST /user/modify_real_name
{ "userId": 10086, "realName": "张三" }
```
**返回示例**
```json
{ "code": "200", "success": true, "data": true }
```
> **与既有接口的区别**:既有 `POST /user/modify_org_user_info` 为超管编辑企业用户完整信息(账号名/姓名/启用状态一起校验更新);本接口仅更新姓名,供用户管理页与机构账号页的「修改姓名」入口使用。
### 1.5 批量创建机构用户
一次性提交多条「手机号 + 姓名」创建机构用户。**单条失败不影响其他条目**(每条独立事务),返回与入参**顺序一一对应**的逐条结果,前端据此展示失败项。
- **URL**:`POST /user/batch_create_org_user`
- **权限**:登录用户(机构取当前登录机构,与单条创建 `POST /user/create_org_user` 一致)
- **Body**(application/json,对象数组,单次最多 **500** 条)
数组元素字段:
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| phone | String | 是 | 手机号 |
| realName | String | 否 | 姓名 |
- **返回**:`AlitaResult<List<OrgUserCreateResultDTO>>`
**OrgUserCreateResultDTO 字段**
| 字段 | 类型 | 说明 |
|---|---|---|
| phone | String | 手机号(回显入参) |
| realName | String | 姓名(回显入参) |
| success | Boolean | 是否创建成功 |
| userId | Long | 用户 id,失败时为 null |
| msg | String | 失败原因,成功时为 null |
**请求示例**
```json
POST /user/batch_create_org_user
[
{ "phone": "13800000001", "realName": "张三" },
{ "phone": "13800000002", "realName": "李四" }
]
```
**返回示例**
```json
{
"code": "200",
"success": true,
"data": [
{ "phone": "13800000001", "realName": "张三", "success": true, "userId": 1001, "msg": null },
{ "phone": "13800000002", "realName": "李四", "success": false, "userId": null, "msg": "手机号已存在,不能重复添加" }
]
}
```
- **失败场景**(整体请求失败):列表为空;超过 500 条(提示「单次批量创建不能超过500条」)。单条业务失败(手机号格式不正确、手机号已存在等)不使请求失败,体现在对应条目的 `success/msg` 中。
---
## 2. 系统管理员:查看各机构下的账号信息
### 2.1 分页查询机构账号
按机构维度查看账号信息。一行 = 一个机构下的一个账号(同一账号属于多个机构时会出现多行)。不传 `orgId` 时查询全部机构,传 `orgId` 时查看指定机构。
- **URL**:`GET /user/page/org_account`
- **权限**:平台管理员
- **参数**(URL query)
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| orgId | Long | 否 | 机构 id,不传查全部机构 |
| orgNameLike | String | 否 | 机构名称,模糊匹配 |
| usernameLike | String | 否 | 账号,模糊匹配 |
| realNameLike | String | 否 | 真实姓名,模糊匹配 |
| phoneLike | String | 否 | 手机号,模糊匹配 |
| enabled | Boolean | 否 | 是否启用:true/false |
| status | Integer | 否 | 用户状态:1 待激活 / 2 已激活 / 3 已注销 |
| createTimeStart | String | 否 | 创建时间起,格式 `yyyy-MM-dd HH:mm:ss` |
| createTimeEnd | String | 否 | 创建时间止,格式 `yyyy-MM-dd HH:mm:ss` |
| pageNo / pageSize | Integer | 否 | 分页参数,默认 1 / 20 |
- **返回**:`AlitaResult<Page<OrgAccountDTO>>`
**OrgAccountDTO 字段**
| 字段 | 类型 | 说明 |
|---|---|---|
| orgId | Long | 机构 id |
| orgName | String | 机构名称 |
| administrator | Boolean | 是否该机构的管理员 |
| userId | Long | 用户 id(可传给 1.1/1.2/1.3 接口做启停/注销操作) |
| username | String | 账号 |
| nickname | String | 昵称 |
| realName | String | 真实姓名 |
| phone | String | 手机号 |
| email | String | 邮箱 |
| userType | Integer | 用户类型:1 企业用户 / 2 个人用户 |
| enabled | Boolean | 是否启用 |
| status | Integer | 状态:1 待激活 / 2 已激活 / 3 已注销 |
| createTime | String | 创建时间 `yyyy-MM-dd HH:mm:ss` |
- **排序规则**:机构 id 升序,机构管理员排前面,同机构内按用户 id 倒序。
**请求示例**
```
GET /user/page/org_account?orgNameLike=教育&phoneLike=138&enabled=false&pageNo=1&pageSize=20
```
**返回示例**
```json
{
"code": "200",
"success": true,
"data": {
"pageNo": 1,
"pageSize": 20,
"pageCount": 1,
"totalCount": 2,
"entityList": [
{
"orgId": 12,
"orgName": "XX市教育局",
"administrator": true,
"userId": 1001,
"username": "admin_xx",
"nickname": "张三",
"realName": "张三",
"phone": "13800000001",
"email": "zhang@xx.gov.cn",
"userType": 1,
"enabled": false,
"status": 2,
"createTime": "2026-08-01 10:00:00"
},
{
"orgId": 12,
"orgName": "XX市教育局",
"administrator": false,
"userId": 1002,
"username": "user_xx01",
"nickname": "李四",
"realName": "李四",
"phone": "13800000002",
"email": null,
"userType": 1,
"enabled": true,
"status": 2,
"createTime": "2026-08-03 14:30:00"
}
]
}
}
```
---
## 3. 按手机号查询文件记录(上传 / 发送 / 接收)
### 3.1 分页查询
输入手机号,精确匹配该手机号名下(可能是多个账号)的全部文件记录,可按记录类型筛选。
- **URL**:`GET /user_file/record/page_by_phone`
- **权限**:平台管理员
- **参数**(URL query)
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| phone | String | **是** | 用户手机号,精确匹配,同一手机号关联的所有账号均会命中 |
| recordType | Integer | 否 | 1 上传记录 / 2 发送记录 / 3 接收记录,**不传查全部三类** |
| fileNameLike | String | 否 | 文件名,模糊匹配 |
| fileType | String | 否 | 文件类型:`1` 文件 / `2` 文件夹 |
| fileStatus | String | 否 | 文件状态:1 正常 / 2 失效 / 11 解析中 / 12 待审核 / 13 传输中 / 14 失败 |
| createTimeBegin | String | 否 | 记录时间起,格式 `yyyy-MM-dd HH:mm:ss` |
| createTimeEnd | String | 否 | 记录时间止,格式 `yyyy-MM-dd HH:mm:ss` |
| pageNo / pageSize | Integer | 否 | 分页参数,默认 1 / 20 |
- **返回**:`AlitaResult<Page<UserFileRecordDTO>>`
**UserFileRecordDTO 字段**(继承用户文件全部字段,以下为主要字段)
| 字段 | 类型 | 说明 |
|---|---|---|
| id | Long | 记录 id |
| phone | String | 归属手机号 |
| username | String | 归属账号 |
| realName | String | 归属真实姓名 |
| userType | Integer | 归属用户类型:1 企业用户 / 2 个人用户 |
| recordType | Integer | 记录类型:1 上传 / 2 发送 / 3 接收 |
| recordTypeDesc | String | 记录类型描述(「上传记录」「发送记录」「接收记录」) |
| userId | Long | 归属用户 id |
| rootType | String | 原始根类型:1 普通 / 2 内穿外 / 3 外传内(调试用,前端一般用 recordType 即可) |
| fileType | String | 文件大类:1 文件 / 2 文件夹 |
| fileName | String | 文件名 |
| fileCode | String | 物理文件编码 |
| fileSize | Long | 文件大小(字节) |
| fileStatus | String | 文件状态:1 正常 / 2 失效 / 11 解析中 / 12 待审核 / 13 传输中 / 14 失败 |
| createTime | String | 记录时间(上传/发送/接收发起时间)`yyyy-MM-dd HH:mm:ss` |
| endTime | String | 有效期(分享类记录使用),可能为 null |
| fileCategory | String | 文件种类(文档/图片/视频等,按后缀归类) |
| maxRiskLevel | String | 最高风险等级:1 重度 / 2 中度 / 3 轻度,可能为 null |
- **排序**:记录 id 倒序(即时间倒序)。
- **失败场景**:phone 为空;recordType 非法(提示「记录类型不合法,仅支持1上传/2发送/3接收」);非管理员。
**请求示例**
```
GET /user_file/record/page_by_phone?phone=13800000001&recordType=2&pageNo=1&pageSize=10
```
**返回示例**
```json
{
"code": "200",
"success": true,
"data": {
"pageNo": 1,
"pageSize": 10,
"pageCount": 1,
"totalCount": 2,
"entityList": [
{
"id": 9001,
"phone": "13800000001",
"username": "user_xx01",
"realName": "李四",
"userType": 1,
"recordType": 2,
"recordTypeDesc": "发送记录",
"userId": 1002,
"rootType": "2",
"fileType": "2",
"fileName": "2026年预算材料_20260912103000",
"fileCode": null,
"fileSize": null,
"fileStatus": "1",
"createTime": "2026-09-12 10:30:00",
"endTime": null,
"fileCategory": null,
"maxRiskLevel": null
},
{
"id": 8802,
"phone": "13800000001",
"username": "user_xx01",
"realName": "李四",
"userType": 1,
"recordType": 1,
"recordTypeDesc": "上传记录",
"userId": 1002,
"rootType": "1",
"fileType": "1",
"fileName": "工作总结.docx",
"fileCode": "fc_3a7d92",
"fileSize": 1048576,
"fileStatus": "1",
"createTime": "2026-09-10 09:12:00",
"endTime": null,
"fileCategory": "文档",
"maxRiskLevel": null
}
]
}
}
```
> **记录类型口径说明**(便于前端理解,无需处理):
> - 上传记录:用户在网盘中上传的文件/文件夹(rootType=1)。
> - 发送记录:用户发起的文件传输(文件夹整体外发,传输中状态为 13,完成后变 1,失败为 14)。
> - 接收记录:对端传来的文件在本端解包后归属到该手机号用户的记录(若开启风险审查,先为 12 待审核,通过后为 1)。
---
## 4. 错误处理建议
前端统一按 `success=false` 判定失败并 toast `msg`;特殊 code 沿用系统既有约定(`401` 未登录等,与现有接口一致)。
涉及本批接口的典型业务错误文案:
| 场景 | msg |
|---|---|
| 非平台管理员操作 | 当前用户无操作权限 |
| 启用已注销账号 | 已注销账号不可启用 |
| 禁用/注销自己 | 不能禁用当前登录账号 / 不能注销当前登录账号 |
| 禁用/注销超管 | 不能禁用超级管理员账号 / 不能注销超级管理员账号 |
| recordType 非法 | 记录类型不合法,仅支持1上传/2发送/3接收 |
| phone 为空 | 手机号不能为空 |
| 修改姓名格式错误 | 姓名格式错误!(长度2-30位,仅支持中文或英文姓名) |
| 修改姓名与原值相同 | 姓名与原姓名一致,无需修改! |
| 机构管理员越机构操作 | 只能操作本机构的用户 |

View File

@@ -0,0 +1,220 @@
# s-file 生产部署 / 上线说明
> 适用版本:2026-09-14(含用户启停/注销、机构账号查询、手机号查记录、机构上传配置、禁用踢下线黑名单)
> 构建产物:`target/s-file.jar`(Spring Boot 可执行 fat jar,需 JDK 8)
---
## 0. 上线前必须处理(阻断项)
### 0.1 替换 spire.pdf 占位 jar ⚠️
本机构开发机的 `~/.m2/repository/e-iceblue/spire.pdf/3.7.4/` 中目前安装的是**编译级 stub(空实现)jar**——它能保证编译打包通过,但 **PDF 盖章功能(`PdfUtils.pdfStampPerPage`)运行时必然失败**。
上线前必须:
1. 从公司私服获取真实的 `spire.pdf-3.7.4.jar`(阿里云与 e-iceblue 官方仓库均无此版本);
2. 覆盖 `~/.m2/repository/e-iceblue/spire.pdf/3.7.4/spire.pdf-3.7.4.jar`(pom 同步覆盖);
3. 重新 `mvn clean package -DskipTests`;
4. 验证:解包 jar 中 `com/spire/pdf/PdfDocument.class` 应为真实实现(stub 版仅 300~500 字节)。
> 若该业务暂时不用 PDF 盖章,可带 stub 上线,但需明确告知团队。
### 0.2 提供生产配置文件
**仓库内不含任何运行配置**(application.yml 是外置的)。启动时通过 `spring.config.additional-location` 挂载。生产模板如下(按实际环境修改):
```yaml
server:
port: 8080
spring:
application:
name: s-file
datasource:
# 生产为达梦 DM8;驱动 com.dameng.jdbc.driver(pom 已含依赖)
url: jdbc:dm://<DB_HOST>:5236/S_FILE
driver-class-name: com.dameng.jdbc.driver
username: <DB_USER>
password: <DB_PASSWORD>
druid:
initial-size: 5
min-idle: 5
max-active: 20
validation-query: SELECT 1
redis: # 必须可用:登录缓存 + 禁用/注销踢下线黑名单依赖 Redis
host: <REDIS_HOST>
port: 6379
timeout: 5000
rabbitmq:
host: <MQ_HOST>
port: 5672
username: <MQ_USER>
password: <MQ_PASSWORD>
servlet:
multipart:
max-file-size: 500MB # 按网盘需求调整
max-request-size: 500MB
main:
allow-bean-definition-overriding: true
mybatis-plus:
mapper-locations: classpath*:com/senmesh/**/mapper/*.xml # 必须有,否则自定义 XML 的接口全部报 Invalid bound statement
system:
code: 1
# 发号器雪花 workerId:键格式 code.snowflake.workid.<本机IP>
# 单机部署:直接写死本机 IP 的键;多机部署:每台机器一个键、值必须不同且 0~31
"[code.snowflake.workid.192.168.1.10]": 1
security:
jwt:
enable: true
secret: <上线前必须换成 64 字节以上随机串,勿用文档示例>
expiration: 86400 # token 有效期(秒)
header: Authorization
ignoring: /error,/favicon.ico,/v2/api-docs,/swagger-resources/**
login-page: /login
superior-only: false # true=仅平台机构用户可登录系统;按部署侧实际选择
use-local: true
allow-if-all-abstain-decisions: true
oauth2:
enable: false
anonymous: "" # 匿名白名单按需追加(逗号分隔,如 /file/upload)
user:
default:
password: <新用户初始密码>
file:
upload-dir: /data/upload # 容器/主机上需存在且可写
storage:
type: local # 阿里云 OSS 则改 ali 并补齐 oss 配置
transfer:
location: "1" # 内网侧 "1",外网侧 "2" —— 两侧必须相反!
send-dir: /data/transfer/send
receive-dir: /data/transfer/receive
unpack-risk-check: false
dify:
risk:
rule:
api-key: <真实key或占位>
scan:
api-key: <真实key或占位> # 必填(可为占位),否则启动失败
rabbitmq:
custom:
topic-exchange-name: s-file-topic
delayed-exchange-name: s-file-delayed
broadcast-id: s-file-broadcast
swagger:
enable: false # 生产必须 false
```
### 0.3 数据库变更
- **已有生产库**:执行 `db/upgrade-20260914-upload-config.sql`(达梦执行「1. MySQL / 达梦 DM8」一节,建 `x_upload_config` 表)。其余本次功能无表结构变更。
- **全新库**:先执行 `db/init-table.sql`(达梦按 MySQL 兼容模式)+ `db/init-data.sql`(平台、平台机构、admin 账号、文件种类字典)。
- 执行后验证:`select count(*) from x_upload_config;` 无报错即建表成功(0 行为正常,未配置的机构走默认策略)。
### 0.4 目录与权限
```
mkdir -p /data/upload /data/transfer/send /data/transfer/receive
chown -R <运行用户> /data
```
外网侧与内网侧通过 `file.transfer.location` 区分,`send-dir`/`receive-dir` 指向各自的摆渡目录(网闸/摆渡盘)。
---
## 1. 启动方式
### 1.1 裸机 / 虚拟机(JDK 8)
```bash
# 需 JDK 1.8(java -version 确认)
java -Xms1g -Xmx2g \
-Dspring.config.additional-location=file:/opt/sfile/config/application.yml \
-jar /opt/sfile/s-file.jar \
> /opt/sfile/logs/stdout.log 2>&1 &
```
多节点时按节点 IP 配置不同的 `code.snowflake.workid.<ip>`(可用启动参数 `-DCODE_SNOWFLAKE_WORKID_<IP大写>=<n>` 环境变量方式,`_` 映射 `.`)。
### 1.2 Docker
```bash
# 构建(context 内需 s-file.jar 与 Dockerfile,Dockerfile 在 src/main/resources/)
cp target/s-file.jar <build-context>/ && docker build -t s-file:<版本> <build-context>
docker run -d --name sfile-app --restart unless-stopped -p 8080:8080 \
-v /opt/sfile/config/application.yml:/config/application.yml:ro \
-v /data/upload:/data/upload \
-v /data/transfer/send:/data/transfer/send \
-v /data/transfer/receive:/data/transfer/receive \
-e SPRING_CONFIG_ADDITIONAL_LOCATION=file:/config/application.yml \
-e CODE_SNOWFLAKE_WORKID_<容器IP大写下划线>=1 \
s-file:<版本>
```
> 注意:基础镜像 `freshworks/oraclejdk8` 为 amd64,arm64 主机需 `--platform linux/amd64`;workerId 键里的 IP 用容器固定 IP(自定义网络 `--ip`),容器重建 IP 不变。
---
## 2. 前端发布
```bash
cd senmesh-zc-legacy
bun install
bun run build # 产出 dist/(.env.production 已带 VITE_APP_TITLE 引号修复,规避 Bun env 解析 bug)
```
- `dist/` 交由 nginx 托管;
- nginx 需将 `/api` 反代到后端并**去掉 `/api` 前缀**(与 vite dev 代理行为一致):
```nginx
location /api/ {
proxy_pass http://<后端IP>:8080/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
client_max_body_size 500m; # 与后端 multipart 上限匹配
}
```
- `.env.production` 当前值:`VITE_API_URL=`(空,走同域 /api)、`VITE_INIT_PASSWORD`(前端判断初始密码强制改密用)——如有调整需重新 build。
---
## 3. 本次版本功能与数据依赖清单(验收用)
| 功能 | 依赖 | 验收方法 |
|---|---|---|
| 用户启用/禁用/注销(管理员) | DB `uc_user.enabled/status`;**Redis**(禁用踢下线黑名单) | 禁用后该用户已登录 token 立即失效(302/401),重新登录被拒 |
| 机构管理员启停本机构用户 | 无额外依赖 | 机构管理员可启停本机构用户;跨机构返回「只能操作本机构的用户」 |
| 机构账号查询(超管) | 新增只读查询 | 管理后台-机构账号页正常出列表与筛选 |
| 按手机号查上传/发送/接收记录 | 无额外依赖 | 输入手机号出记录;发送/接收方向由部署侧 `file.transfer.location` 决定 |
| 机构上传配置 | 新表 `x_upload_config` | 管理后台-上传配置保存后,该机构网盘上传立即按策略拦截 |
| 禁用踢下线 | Redis key `user:disabled:{userId}` | `redis-cli keys 'user:disabled:*'` 可见禁用标记 |
---
## 4. 上线步骤建议
1. 生产库执行 0.3 升级 SQL(执行前备份);
2. 替换真实 spire.pdf jar 并重新打包(0.1),核对 jar 时间戳;
3. 准备 application.yml(0.2 模板),**核对 JWT secret 已更换、dify api-key 已填、swagger.enable=false**;
4. 创建 /data 目录,确定内外网 location;
5. 部署后端(1.1 或 1.2),观察启动日志出现 `Started SFileApplication`;
6. 构建并发布前端(2),nginx reload;
7. 验收第 3 节清单(至少:admin 登录、上传一个非白名单文件被拒、禁用一个测试用户并确认其会话失效、记录查询出数据);
8. 观察日志无 `ERROR`,Redis 中 `user:disabled:*` 与登录缓存键正常。
## 5. 回滚
- 后端:保留上一版 jar/镜像,停新启旧即可(本次无破坏性 DDL/DML,`x_upload_config` 留存不影响旧版本运行);
- 前端:nginx 指回上一版 dist 目录;
- 数据库:无需回滚(仅新增表)。

458
backend/pom.xml Normal file
View File

@@ -0,0 +1,458 @@
<?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/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.2</version>
<relativePath />
</parent>
<groupId>com.senmesh</groupId>
<artifactId>s-file</artifactId>
<version>0.0.1</version>
<packaging>jar</packaging>
<properties>
<org.mapstruct.version>1.2.0.Final</org.mapstruct.version>
<maven.compiler.source>8</maven.compiler.source> <!-- 匹配你的 JDK 版本 -->
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!--引入AOP依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>18.0</version>
<scope>compile</scope>
</dependency>
<!-- 工具包 -->
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.6</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.2.2</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId>
<version>4.2</version>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-jdk8</artifactId>
<version>${org.mapstruct.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${org.mapstruct.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>${org.mapstruct.version}</version>
</dependency>
<!-- 使用org.springframework.data.annotation下的注解 -->
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-commons</artifactId>
</dependency>
<!-- freemarker -->
<dependency>
<groupId>org.freemarker</groupId>
<artifactId>freemarker</artifactId>
</dependency>
<!-- swagger -->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.7.0</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.7.0</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>3.1.1</version>
</dependency>
<!-- 加解密BC -->
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk15on</artifactId>
<version>1.55</version>
</dependency>
<!-- 配置加密 -->
<dependency>
<groupId>com.github.ulisesbocchio</groupId>
<artifactId>jasypt-spring-boot-starter</artifactId>
<version>3.0.2</version>
</dependency>
<!--达梦数据库驱动 -->
<dependency>
<groupId>com.dameng</groupId>
<artifactId>Dm8JdbcDriver18</artifactId>
<version>8.1.1.49</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.4</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.0</version>
</dependency>
<!--mybatis -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.3</version>
</dependency>
<!--pagehelper -->
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>1.4.6</version>
</dependency>
<!-- rabbitmq -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<!-- redis -->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!--<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.15.6</version>
</dependency>-->
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
</dependency>
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-data-21</artifactId>
<version>3.11.0</version>
</dependency>
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.11.0</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.16</version>
</dependency>
<!-- pdfbox -->
<dependency>
<groupId>org.apache.pdfbox</groupId>
<artifactId>pdfbox</artifactId>
<version>2.0.20</version>
</dependency>
<dependency>
<groupId>org.apache.pdfbox</groupId>
<artifactId>fontbox</artifactId>
<version>2.0.20</version>
</dependency>
<!-- oss -->
<dependency>
<groupId>com.aliyun.oss</groupId>
<artifactId>aliyun-sdk-oss</artifactId>
<version>3.14.0</version>
</dependency>
<!-- Spire.Pdf -->
<dependency>
<groupId>e-iceblue</groupId>
<artifactId>spire.pdf</artifactId>
<version>3.7.4</version>
</dependency>
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>itextpdf</artifactId>
<version>5.5.11</version>
</dependency>
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>itext-asian</artifactId>
<version>5.2.0</version>
</dependency>
<!-- security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- oauth2 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
<dependency>
<artifactId>spring-boot-starter-websocket</artifactId>
<groupId>org.springframework.boot</groupId>
</dependency>
<!-- sdk -->
<!-- 阿里云sms -->
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>aliyun-java-sdk-core</artifactId>
<version>4.1.0</version>
</dependency>
<!-- 腾讯云sdk -->
<dependency>
<groupId>com.tencentcloudapi</groupId>
<artifactId>tencentcloud-sdk-java</artifactId>
<version>3.1.100</version>
</dependency>
<!-- 钉钉机器人 -->
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>alibaba-dingtalk-service-sdk</artifactId>
<version>1.0.1</version>
</dependency>
<!-- 百度sdk -->
<dependency>
<groupId>com.baidu.aip</groupId>
<artifactId>java-sdk</artifactId>
<version>4.15.3</version>
<exclusions>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- 敏感词检测 -->
<dependency>
<groupId>io.github.toolgood</groupId>
<artifactId>toolgood-words</artifactId>
<version>3.1.0.0</version>
</dependency>
<dependency>
<groupId>com.spatial4j</groupId>
<artifactId>spatial4j</artifactId>
<version>0.5</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.squareup.okhttp3/okhttp -->
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>4.10.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.squareup.okio/okio -->
<dependency>
<groupId>com.squareup.okio</groupId>
<artifactId>okio</artifactId>
<version>2.10.0</version>
</dependency>
<dependency>
<groupId>ws.schild</groupId>
<artifactId>jave-core</artifactId>
<version>3.4.0</version>
</dependency>
<!-- Linux 环境 -->
<dependency>
<groupId>ws.schild</groupId>
<artifactId>jave-nativebin-linux64</artifactId>
<version>3.4.0</version>
</dependency>
<!-- MacOS 环境 -->
<dependency>
<groupId>ws.schild</groupId>
<artifactId>jave-nativebin-osx64</artifactId>
<version>3.4.0</version>
</dependency>
<!-- Win64 环境 -->
<dependency>
<groupId>ws.schild</groupId>
<artifactId>jave-nativebin-win64</artifactId>
<version>3.4.0</version>
</dependency>
<!-- 讯飞-星火大模型相关 -->
<dependency>
<groupId>cn.xfyun</groupId>
<artifactId>websdk-java-spark</artifactId>
<version>2.1.3</version>
</dependency>
<!-- 讯飞-语音相关能力 -->
<dependency>
<groupId>cn.xfyun</groupId>
<artifactId>websdk-java-speech</artifactId>
<version>3.0.3</version>
</dependency>
<!-- 讯飞-自然语言处理 -->
<dependency>
<groupId>cn.xfyun</groupId>
<artifactId>websdk-java-nlp</artifactId>
<version>2.0.7</version>
</dependency>
</dependencies>
<build>
<finalName>${project.artifactId}</finalName>
<resources>
<resource>
<directory>src/main/resources</directory>
</resource>
<resource>
<directory>src/main/java</directory>
<includes>
<!--指定打包的文件格式 -->
<include>**/*.xml</include>
</includes>
</resource>
<resource>
<directory>src/main/resources</directory>
<filtering>true</filtering>
<includes>
<include>application.yml</include>
<include>bootstrap.yml</include>
<include>${project.artifactId}-${profile.active}.yaml</include>
</includes>
</resource>
</resources>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<!-- docker -->
<plugin>
<groupId>com.spotify</groupId>
<artifactId>docker-maven-plugin</artifactId>
<version>1.2.1</version>
<configuration>
<imageName>${project.build.finalName}:${project.version}</imageName>
<dockerDirectory>${project.build.outputDirectory}</dockerDirectory>
<buildArgs>
<JAR_FILE>${project.build.finalName}.jar</JAR_FILE>
</buildArgs>
<resources>
<resource>
<directory>${project.build.directory}</directory>
<include>${project.build.finalName}.jar</include>
</resource>
</resources>
<dockerHost>${docker.host}</dockerHost>
<dockerCertPath>${docker.certPath}</dockerCertPath>
</configuration>
</plugin>
</plugins>
</build>
</project>

209
backend/scripts/sfile-ctl.sh Executable file
View File

@@ -0,0 +1,209 @@
#!/usr/bin/env bash
# ==============================================================================
# s-file 一键部署/运维脚本(生产环境)
#
# 用法:
# ./sfile-ctl.sh start 启动服务
# ./sfile-ctl.sh stop 停止服务
# ./sfile-ctl.sh restart 重启服务
# ./sfile-ctl.sh status 查看状态
# ./sfile-ctl.sh deploy <jar路径> 发布新版 jar(自动备份旧版并重启)
# ./sfile-ctl.sh log [行数] 查看启动日志(默认 200 行)
# ./sfile-ctl.sh db-upgrade <sql路径> 在达梦库执行升级 SQL(需配置 disql)
# 上线用: db/upgrade-20260915-release.sql
#
# 部署目录结构(脚本首次 start 时自动创建,配置需手工放置):
# /opt/sfile/
# ├── s-file.jar 服务 jar
# ├── config/application.yml 生产配置(模板见 docs/生产部署说明.md)
# ├── backup/ 历史版本备份
# ├── logs/ 日志目录
# └── data/ 上传/摆渡目录(upload/transfer/send/transfer/receive)
# ==============================================================================
set -euo pipefail
# ------------------------- 环境变量(按服务器实际修改) -------------------------
APP_NAME="s-file"
INSTALL_DIR="${SFILE_INSTALL_DIR:-/opt/sfile}"
JAR_FILE="${INSTALL_DIR}/${APP_NAME}.jar"
CONFIG_FILE="${INSTALL_DIR}/config/application.yml"
LOG_DIR="${INSTALL_DIR}/logs"
BACKUP_DIR="${INSTALL_DIR}/backup"
STDOUT_LOG="${LOG_DIR}/stdout.log"
GC_LOG="${LOG_DIR}/gc.log"
# 运行用户(root 部署时可改为专用低权用户,如 sfile)
RUN_USER="${SFILE_RUN_USER:-$(whoami)}"
# JVM 参数:按机器内存调整 -Xmx;生产建议 2g 起
JAVA_OPTS="${SFILE_JAVA_OPTS:--Xms1g -Xmx2g -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=${LOG_DIR} -Xloggc:${GC_LOG} -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=5 -XX:GCLogFileSize=20M}"
# 健康检查地址与超时
HEALTH_URL="http://127.0.0.1:8080/user/info"
START_TIMEOUT=120
# 达梦 disql(db-upgrade 用;达梦容器则为 docker exec -i <容器> /opt/dmdbms/bin/disql ...)
DISQL_CMD="${SFILE_DISQL:-disql <DB_USER>/<DB_PASSWORD>@<DB_HOST>:5236}"
# -----------------------------------------------------------------------------
GREEN='\033[32m'; RED='\033[31m'; YELLOW='\033[33m'; NC='\033[0m'
info() { echo -e "${GREEN}[INFO]${NC} $*"; }
warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
err() { echo -e "${RED}[ERROR]${NC} $*"; }
get_pid() {
pgrep -f "java .*${JAR_FILE}" 2>/dev/null || true
}
require_env() {
[ -f "${JAR_FILE}" ] || { err "jar 不存在: ${JAR_FILE},先用 deploy 发布"; exit 1; }
[ -f "${CONFIG_FILE}" ] || { err "配置不存在: ${CONFIG_FILE}(模板见 docs/生产部署说明.md)"; exit 1; }
# 项目要求 JDK 8 运行(字节码 52.0),防护误用高版本 JDK
if command -v java >/dev/null 2>&1; then
local major
major=$(java -version 2>&1 | head -1 | sed -E 's/.*"([0-9]+)\..*"/\1/')
if [ "${major}" != "1" ] && [ "${major}" != "8" ]; then
err "检测到 java 版本为 ${major},本项目要求 JDK 8(请 export JAVA_HOME 指向 JDK8 并确认 java 命令)"
exit 1
fi
else
err "未找到 java 命令,请安装 JDK 8 并配置 PATH"; exit 1
fi
}
prepare_dirs() {
mkdir -p "${INSTALL_DIR}/config" "${LOG_DIR}" "${BACKUP_DIR}" \
"${INSTALL_DIR}/data/upload" \
"${INSTALL_DIR}/data/transfer/send" \
"${INSTALL_DIR}/data/transfer/receive"
}
start() {
require_env
local pid
pid=$(get_pid)
if [ -n "${pid}" ]; then
warn "${APP_NAME} 已在运行 (PID ${pid}),如需重启请先 stop"
exit 0
fi
prepare_dirs
info "启动 ${APP_NAME} ..."
# workerId 说明:多节点部署时每台机器配置不同的 code.snowflake.workid.<本机IP>,
# 最简单的方式是用环境变量(. 映射为 _),如:
# export CODE_SNOWFLAKE_WORKID_192_168_1_10=1
# shellcheck disable=SC2086
if [ "${RUN_USER}" = "$(whoami)" ]; then
(cd "${INSTALL_DIR}" && nohup java ${JAVA_OPTS} \
-Dspring.config.additional-location=file:${CONFIG_FILE} \
-jar ${JAR_FILE} >> ${STDOUT_LOG} 2>&1 & echo $! > "${INSTALL_DIR}/app.pid")
else
su -s /bin/bash "${RUN_USER}" -c "cd ${INSTALL_DIR} && nohup java ${JAVA_OPTS} \
-Dspring.config.additional-location=file:${CONFIG_FILE} \
-jar ${JAR_FILE} >> ${STDOUT_LOG} 2>&1 & echo \$! > ${INSTALL_DIR}/app.pid"
fi
# 启动健康检查轮询
info "等待服务就绪(最多 ${START_TIMEOUT}s)..."
local waited=0
while [ ${waited} -lt ${START_TIMEOUT} ]; do
if curl -sf -o /dev/null --max-time 3 "${HEALTH_URL}"; then
info "启动成功 (PID $(cat "${INSTALL_DIR}/app.pid")),耗时约 ${waited}s"
info "日志: tail -f ${STDOUT_LOG}"
return 0
fi
# 进程若已退出则立即报错,不干等
if [ -z "$(get_pid)" ]; then
err "进程已退出,最近日志:"
tail -30 "${STDOUT_LOG}"
exit 1
fi
sleep 3; waited=$((waited + 3))
done
err "${START_TIMEOUT}s 内未就绪,请检查日志: tail -100 ${STDOUT_LOG}"
exit 1
}
stop() {
local pid
pid=$(get_pid)
if [ -z "${pid}" ]; then
info "${APP_NAME} 未在运行"
return 0
fi
info "停止 ${APP_NAME} (PID ${pid}) ..."
kill "${pid}"
local waited=0
while kill -0 "${pid}" 2>/dev/null && [ ${waited} -lt 60 ]; do
sleep 2; waited=$((waited + 2))
done
if kill -0 "${pid}" 2>/dev/null; then
warn "60s 未退出,强制 kill -9"
kill -9 "${pid}" 2>/dev/null || true
fi
rm -f "${INSTALL_DIR}/app.pid"
info "已停止"
}
status() {
local pid
pid=$(get_pid)
if [ -z "${pid}" ]; then
info "${APP_NAME}: 未运行"
exit 1
fi
info "${APP_NAME}: 运行中 (PID ${pid})"
if curl -sf -o /dev/null --max-time 3 "${HEALTH_URL}"; then
info "健康检查: 正常"
else
warn "健康检查: 8080 未响应(可能仍在启动)"
fi
info "内存: $(ps -o rss= -p "${pid}" | awk '{printf "%.0fMB", $1/1024}')"
}
deploy() {
local new_jar="$1"
[ -f "${new_jar}" ] || { err "找不到 jar: ${new_jar}"; exit 1; }
prepare_dirs
local ts; ts=$(date +%Y%m%d-%H%M%S)
if [ -f "${JAR_FILE}" ]; then
# 运行中先停
if [ -n "$(get_pid)" ]; then
stop
fi
cp "${JAR_FILE}" "${BACKUP_DIR}/${APP_NAME}-${ts}.jar"
info "旧版本已备份: ${BACKUP_DIR}/${APP_NAME}-${ts}.jar"
fi
cp "${new_jar}" "${JAR_FILE}"
# 保留最近 5 个备份
ls -1t "${BACKUP_DIR}/${APP_NAME}-"*.jar 2>/dev/null | tail -n +6 | xargs -r rm -f
info "新版本已就位: ${JAR_FILE} ($(du -h "${JAR_FILE}" | cut -f1))"
start
}
db_upgrade() {
local sql_file="$1"
[ -f "${sql_file}" ] || { err "找不到 SQL: ${sql_file}"; exit 1; }
case "${DISQL_CMD}" in
*"<DB_USER>"*) err "请先在脚本头部或环境变量 SFILE_DISQL 配置真实 disql 连接串"; exit 1;;
esac
info "执行升级 SQL: ${sql_file}"
# 达梦 disql 执行 SQL 文件语法: disql 用户/密码@host:port \`文件路径\`
${DISQL_CMD} \`"${sql_file}"
info "执行完成,请核对上方输出无 ERROR 后再继续部署"
}
log() {
local lines="${1:-200}"
tail -n "${lines}" "${STDOUT_LOG}" 2>/dev/null || warn "暂无日志: ${STDOUT_LOG}"
}
case "${1:-}" in
start) start ;;
stop) stop ;;
restart) stop; start ;;
status) status ;;
deploy) [ $# -ge 2 ] || { err "用法: $0 deploy <jar路径>"; exit 1; }; deploy "$2" ;;
log) log "${2:-200}" ;;
db-upgrade) [ $# -ge 2 ] || { err "用法: $0 db-upgrade <sql路径>"; exit 1; }; db_upgrade "$2" ;;
*) sed -n '2,20p' "$0"; exit 1 ;;
esac

View File

@@ -0,0 +1,22 @@
package com.senmesh;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.scheduling.annotation.EnableScheduling;
/**
* @author sr on 2020-04-16
*/
@SpringBootApplication(scanBasePackages = {"com.senmesh"})
@EnableAspectJAutoProxy(proxyTargetClass = true, exposeProxy = true)
@EnableScheduling
public class SFileApplication {
public static void main(String[] args) {
SpringApplication.run(SFileApplication.class, args);
}
}

View File

@@ -0,0 +1,67 @@
package com.senmesh.codegenerator.config;
import org.apache.commons.lang3.StringUtils;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import com.senmesh.codegenerator.service.CodeService;
import com.senmesh.codegenerator.service.SnowflakeIdWorker;
import com.senmesh.codegenerator.service.generator.SnowFlakeCodeGenerator;
import com.senmesh.codegenerator.service.impl.CodeServiceImpl;
import com.senmesh.codegenerator.util.CodeUtils;
import com.senmesh.common.CommonConfiguration;
import com.senmesh.common.constant.CommonConstants;
import com.senmesh.common.util.EnvironmentUtils;
import com.senmesh.common.util.RequestUtils;
import lombok.extern.slf4j.Slf4j;
/**
* 发号器配置类
* @author sr on 2021-03-23
*/
//@Configuration
@AutoConfigureAfter(CommonConfiguration.class)
@Slf4j
public class CodeConfiguration {
public static final String PROP_WORID_PREFIX = "code.snowflake.workid";
/**
* 默认编号生成器
* @return
* @author sr on 2021-03-23
*/
@Bean
public SnowFlakeCodeGenerator snowFlakeCodeGenerator() {
// 机器id
String hostIp = RequestUtils.getHostIp();
log.info("hostIp:" + hostIp);
String workIdStr = EnvironmentUtils.getProperty(
String.join(CommonConstants.POINT, PROP_WORID_PREFIX, hostIp));
Long workId = 99l;
try {
if (StringUtils.isNoneBlank(workIdStr)) {
workId = Long.valueOf(workIdStr);
}
} catch (Exception e) {
}
SnowflakeIdWorker idWorker = new SnowflakeIdWorker(workId, 0);
return new SnowFlakeCodeGenerator(idWorker);
}
/**
* 编号service(根据类型分发不同的生成器)
* @return
* @author sr on 2021-03-23
*/
@Bean
public CodeService codeService() {
CodeService codeService = new CodeServiceImpl();
CodeUtils.setCodeService(codeService);
return codeService;
}
}

View File

@@ -0,0 +1,26 @@
package com.senmesh.codegenerator.constant;
/**
* 编号常量类
* @author sr on 2019-09-15
*/
public class CodeConstants {
/** 生成器类型:默认(不可用) **/
// public static final String GENERATOR_TYPE_DEFAULT = "DEFAULT";
public static final String GENERATOR_TYPE_SNOW_FLAKE = "SnowFlake";
/** id默认长度 **/
public static final int ID_DEFAULT_LENGTH = 5;
/** id默认补位字符 **/
public static final char ID_DEFAULT_PAD = '0';
/** id默认日期格式化 **/
public static final DateFormatterEnum ID_DEFAULT_FORMATTER = DateFormatterEnum.YEAR;
/** -----异常信息----- **/
public static final String CODE_GENERATE_ERROR = "编号生成失败";
/** -----异常信息----- **/
}

View File

@@ -0,0 +1,32 @@
package com.senmesh.codegenerator.constant;
import java.time.format.DateTimeFormatter;
/**
* 日期formatter枚举
* @author sr on 2019-12-03
*/
public enum DateFormatterEnum {
/** 年:yyyy **/
YEAR(DateTimeFormatter.ofPattern("yyyy")),
/** 年月:yyyyMM **/
YM(DateTimeFormatter.ofPattern("yyyyMM")),
/** 年月:yyMM **/
YM2(DateTimeFormatter.ofPattern("yyMM")),
/** 年月日:yyyyMMdd **/
YMD(DateTimeFormatter.ofPattern("yyyyMMdd")),
/** 年月日:yyMMdd **/
YMD2(DateTimeFormatter.ofPattern("yyMMdd"));
private DateTimeFormatter formatter;
private DateFormatterEnum(DateTimeFormatter formatter){
this.formatter = formatter;
}
public DateTimeFormatter getDateTimeFormatter() {
return this.formatter;
}
}

View File

@@ -0,0 +1,26 @@
package com.senmesh.codegenerator.exception;
/**
* 编号生成器异常
* @author sr on 2019-12-03
*/
public class CodeGeneratorException extends RuntimeException{
/** **/
private static final long serialVersionUID = 3558280270933232655L;
public CodeGeneratorException() {}
public CodeGeneratorException(String message) {
super(message);
}
public CodeGeneratorException(String message, Throwable cause) {
super(message, cause);
}
public CodeGeneratorException(Throwable cause) {
super(cause);
}
}

View File

@@ -0,0 +1,25 @@
package com.senmesh.codegenerator.pojo;
import com.senmesh.codegenerator.constant.CodeConstants;
import lombok.Builder;
import lombok.Data;
import lombok.Builder.Default;
/**
* 编码生成参数对象
* @author sr on 2019-12-05
*/
@Data
@Builder
public class CodeParam {
/** 生成器类型 **/
@Default
private String generatorType = CodeConstants.GENERATOR_TYPE_SNOW_FLAKE;
/** 编码类型 **/
private String type;
}

View File

@@ -0,0 +1,42 @@
package com.senmesh.codegenerator.service;
import java.util.List;
import com.senmesh.codegenerator.pojo.CodeParam;
/**
* 编号生成器 默认一套实现,如需重定义中间时间format和id长度,重写规则 重写规则,注意去发号器对应的数据库表(leaf_alloc)中添加对应type+时间(biz_tag)的记录
*
* @author sr on 2019-09-15
*/
public interface CodeGenerator<T extends CodeParam> {
/**
* 自定义特定业务类型,即前缀
*
* @return
* @author sr on 2019-09-19
*/
String getType();
/**
* 根据生成规则获取编号
*
* @param codeParam
* @return
* @author sr on 2020-07-13
*/
String getCode(T codeParam);
/**
* 批量根据生成规则获取编号
*
* @param codeParam
* @return
* @author sr on 2020-07-13
*/
List<String> batchCode(T codeParam, Integer batchNum);
}

View File

@@ -0,0 +1,33 @@
package com.senmesh.codegenerator.service;
import java.util.List;
import com.senmesh.codegenerator.pojo.CodeParam;
/**
* 编号service(根据类型分发不同的生成器)
*
* @author sr on 2019-09-15
*/
public interface CodeService {
/**
* 根据生成规则获取编号
*
* @param codeParam
* @return
* @author sr on 2020-07-13
*/
String getCode(CodeParam codeParam);
/**
* 批量根据生成规则获取编号
*
* @param codeParam
* @param batchNum 批次数(1-1000)
* @return
* @author sr on 2020-07-13
*/
List<String> batchCode(CodeParam codeParam, Integer batchNum);
}

View File

@@ -0,0 +1,160 @@
package com.senmesh.codegenerator.service;
/**
* Twitter_Snowflake<br>
* SnowFlake的结构如下(每部分用-分开):<br>
* 0 - 0000000000 0000000000 0000000000 0000000000 0 - 00000 - 00000 - 000000000000 <br>
* 1位标识,由于long基本类型在Java中是带符号的,最高位是符号位,正数是0,负数是1,所以id一般是正数,最高位是0<br>
* 41位时间戳(毫秒级),注意,41位时间戳不是存储当前时间的时间戳,而是存储时间戳的差值(当前时间戳 - 开始时间戳) 得到的值),这里的的开始时间戳,一般是我们的id生成器开始使用的时间,由我们程序来指定的(如下下面程序IdWorker类的startTime属性)。41位的时间戳,可以使用69年,年T = (1L << 41) / (1000L * 60 * 60 * 24 * 365) = 69<br>
* 10位的数据机器位,可以部署在1024个节点,包括5位datacenterId和5位workerId<br>
* 12位序列,毫秒内的计数,12位的计数顺序号支持每个节点每毫秒(同一机器,同一时间戳)产生4096个ID序号<br>
* 加起来刚好64位,为一个Long型。<br>
* SnowFlake的优点是,整体上按照时间自增排序,并且整个分布式系统内不会产生ID碰撞(由数据中心ID和机器ID作区分),并且效率较高,经测试,SnowFlake每秒能够产生26万ID左右。
*/
public class SnowflakeIdWorker {
// ==============================Fields===========================================
/** 开始时间戳 (2015-01-01) */
private final long twepoch = 1420041600000L;
/** 能容忍的系统时钟回拨时间(2秒) */
private final long timeOffset = 2000L;
/** 机器id所占的位数 */
private final long workerIdBits = 5L;
/** 数据标识id所占的位数 */
private final long datacenterIdBits = 5L;
/** 支持的最大机器id,结果是31 (这个移位算法可以很快的计算出几位二进制数所能表示的最大十进制数) */
private final long maxWorkerId = -1L ^ (-1L << workerIdBits);
/** 支持的最大数据标识id,结果是31 */
private final long maxDatacenterId = -1L ^ (-1L << datacenterIdBits);
/** 序列在id中占的位数 */
private final long sequenceBits = 12L;
/** 机器ID向左移12位 */
private final long workerIdShift = sequenceBits;
/** 数据标识id向左移17位(12+5) */
private final long datacenterIdShift = sequenceBits + workerIdBits;
/** 时间戳向左移22位(5+5+12) */
private final long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;
/** 生成序列的掩码,这里为4095 (0b111111111111=0xfff=4095) */
private final long sequenceMask = -1L ^ (-1L << sequenceBits);
/** 工作机器ID(0~31) */
private long workerId;
/** 数据中心ID(0~31) */
private long datacenterId;
/** 毫秒内序列(0~4095) */
private long sequence = 0L;
/** 上次生成ID的时间戳 */
private long lastTimestamp = -1L;
// ==============================Constructors=====================================
/**
* 构造函数
* @param workerId 工作ID (0~31)
* @param datacenterId 数据中心ID (0~31)
*/
public SnowflakeIdWorker(long workerId, long datacenterId) {
if (workerId > maxWorkerId || workerId < 0) {
throw new IllegalArgumentException(
String.format("worker Id can't be greater than %d or less than 0", maxWorkerId));
}
if (datacenterId > maxDatacenterId || datacenterId < 0) {
throw new IllegalArgumentException(
String.format("datacenter Id can't be greater than %d or less than 0", maxDatacenterId));
}
this.workerId = workerId;
this.datacenterId = datacenterId;
}
// ==============================Methods==========================================
/**
* 获得下一个ID (该方法是线程安全的)
* @return SnowflakeId
*/
public synchronized long nextId() {
long timestamp = timeGen();
// 当前时间戳小于上一次ID生成的时间戳
if (timestamp < lastTimestamp) {
long offset = lastTimestamp - timestamp;
// 系统时钟回退在容忍范围内,例如出现过闰秒
if (offset < timeOffset) {
timestamp = lastTimestamp;
}
// 系统时钟回退超出容忍范围,抛出异常
else {
throw new RuntimeException(String
.format("Clock moved backwards. Refusing to generate id for %d milliseconds", offset));
}
}
// 如果是同一时间生成的,则进行毫秒内序列
if (lastTimestamp == timestamp) {
sequence = (sequence + 1) & sequenceMask;
// 毫秒内序列溢出
if (sequence == 0) {
// 阻塞到下一个毫秒,获得新的时间戳
timestamp = tilNextMillis(lastTimestamp);
}
}
// 时间戳改变,毫秒内序列重置
else {
sequence = 0L;
}
// 上次生成ID的时间戳
lastTimestamp = timestamp;
// 移位并通过或运算拼到一起组成64位的ID
return ((timestamp - twepoch) << timestampLeftShift) //
| (datacenterId << datacenterIdShift) //
| (workerId << workerIdShift) //
| sequence;
}
/**
* 阻塞到下一个毫秒,直到获得新的时间戳
* @param lastTimestamp 上次生成ID的时间戳
* @return 当前时间戳
*/
protected long tilNextMillis(long lastTimestamp) {
long timestamp = timeGen();
while (timestamp <= lastTimestamp) {
timestamp = timeGen();
}
return timestamp;
}
/**
* 返回以毫秒为单位的当前时间
* @return 当前时间(毫秒)
*/
protected long timeGen() {
return System.currentTimeMillis();
}
// ==============================Test=============================================
/** 测试 */
public static void main(String[] args) {
System.out.println("1000011111010000000111110011100110011110000000000000000001100".length());
// SnowflakeIdWorker idWorker = new SnowflakeIdWorker(0, 0);
// for (int i = 0; i < 1000; i++) {
// long id = idWorker.nextId();
// System.out.println(Long.toBinaryString(id));
// System.out.println(id);
// }
}
}

View File

@@ -0,0 +1,47 @@
package com.senmesh.codegenerator.service.generator;
import java.util.List;
import com.google.common.collect.Lists;
import com.senmesh.codegenerator.constant.CodeConstants;
import com.senmesh.codegenerator.pojo.CodeParam;
import com.senmesh.codegenerator.service.CodeGenerator;
import com.senmesh.codegenerator.service.SnowflakeIdWorker;
import lombok.extern.slf4j.Slf4j;
/**
* 默认编号生成器
*
* @author sr on 2019-09-15
*/
@Slf4j
public class SnowFlakeCodeGenerator implements CodeGenerator<CodeParam> {
private SnowflakeIdWorker snowflakeIdWorker;
public SnowFlakeCodeGenerator(SnowflakeIdWorker snowflakeIdWorker) {
this.snowflakeIdWorker = snowflakeIdWorker;
}
@Override
public String getType() {
return CodeConstants.GENERATOR_TYPE_SNOW_FLAKE;
}
@Override
public String getCode(CodeParam codeParam) {
return codeParam.getType() + snowflakeIdWorker.nextId();
}
@Override
public List<String> batchCode(CodeParam codeParam, Integer batchNum) {
List<String> ids = Lists.newArrayListWithCapacity(batchNum);
for (int i = 0; i < batchNum; i++) {
ids.add(this.getCode(codeParam));
}
return ids;
}
}

View File

@@ -0,0 +1,48 @@
package com.senmesh.codegenerator.service.impl;
import java.util.List;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import com.google.common.collect.Maps;
import com.senmesh.codegenerator.constant.CodeConstants;
import com.senmesh.codegenerator.pojo.CodeParam;
import com.senmesh.codegenerator.service.CodeGenerator;
import com.senmesh.codegenerator.service.CodeService;
/**
* 编号service实现类
*
* @author sr on 2019-09-15
*/
public class CodeServiceImpl implements CodeService {
private Map<String, CodeGenerator<CodeParam>> codeGenerators = Maps.newHashMap();
@SuppressWarnings({"unchecked", "rawtypes"})
@Autowired
private void codeGenerateResolver(List<CodeGenerator> resolvers) {
resolvers.forEach(resolver -> codeGenerators.put(resolver.getType(), resolver));
}
private CodeGenerator<CodeParam> getResolver(String type) {
CodeGenerator<CodeParam> resolver = codeGenerators.get(type);
if (resolver == null) {
resolver = codeGenerators.get(CodeConstants.GENERATOR_TYPE_SNOW_FLAKE);
}
return resolver;
}
@Override
public String getCode(CodeParam codeParam) {
return getResolver(codeParam.getGeneratorType()).getCode(codeParam);
}
@Override
public List<String> batchCode(CodeParam codeParam, Integer batchNum) {
return getResolver(codeParam.getGeneratorType()).batchCode(codeParam, batchNum);
}
}

View File

@@ -0,0 +1,48 @@
package com.senmesh.codegenerator.util;
import java.util.List;
import com.senmesh.codegenerator.pojo.CodeParam;
import com.senmesh.codegenerator.service.CodeService;
import com.senmesh.component.constants.CodeTypeEnum;
/**
* 发号器工具类
* @author sr on 2021-03-23
*/
public abstract class CodeUtils {
private static CodeService codeService;
public static void setCodeService(CodeService codeService) {
CodeUtils.codeService = codeService;
}
public static String getCode(CodeTypeEnum codeTypeEnum) {
return codeService.getCode(codeTypeEnum.getCodeParam());
}
/**
* 根据生成规则获取编号
*
* @param codeParam
* @return
* @author sr on 2021-03-23
*/
public static String getCode(CodeParam codeParam) {
return codeService.getCode(codeParam);
}
/**
* 批量根据生成规则获取编号
*
* @param codeParam
* @param batchNum
* @return
* @author sr on 2021-03-23
*/
public static List<String> batchCode(CodeParam codeParam, Integer batchNum) {
return codeService.batchCode(codeParam, batchNum);
}
}

View File

@@ -0,0 +1,37 @@
package com.senmesh.codesetting.constant;
public interface CodeSettingConstants {
/**
* 编码配置数据类型
*/
public static final String CODE_SETTING_DATA_TYPE = "code_setting_data_type";
public class CodeSettingDataType {
public static final String STRING = "1";
public static final String NUMBER = "2";
public static final String DATE = "3";
public static final String BOOL = "4";
public static final String RADIO = "5";
public static final String CHECKBOX = "6";
public static final String FILE = "7";
}
/**
* 编码配置模块:机构配置
**/
public static final String CODE_SETTING_TYPE_CONFIG = "1";
/**
* 编码配置默认级别
**/
public static final String CODE_SETTING_LEVEL_DEFAULT = "1,2,3,4";
}

View File

@@ -0,0 +1,67 @@
package com.senmesh.codesetting.controller;
import com.senmesh.codesetting.dto.CodeSettingDTO;
import com.senmesh.codesetting.dto.CodeSettingOperatorDTO;
import com.senmesh.codesetting.dto.CodeSettingPageQueryDTO;
import com.senmesh.codesetting.dto.CodeSettingQueryDTO;
import com.senmesh.codesetting.service.CodeSettingService;
import com.senmesh.common.AlitaResult;
import com.senmesh.tkmybatis.pojo.Page;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* 编码表
*
* @author chenyun chenyun.home@outlook.com
* @since 1.0.0 2023-01-04
*/
@Api(tags = "编码表")
@RestController
@RequestMapping("code_setting")
public class CodeSettingController {
@Autowired
private CodeSettingService codeSettingService;
@PostMapping
@ApiOperation("编码表-创建")
public AlitaResult<CodeSettingDTO> create(@RequestBody CodeSettingOperatorDTO operatorDTO) {
return AlitaResult.success(codeSettingService.create(operatorDTO));
}
@PutMapping
@ApiOperation("编码表-修改")
public AlitaResult<CodeSettingDTO> modify(@RequestBody CodeSettingOperatorDTO operatorDTO) {
return AlitaResult.success(codeSettingService.modify(operatorDTO));
}
@DeleteMapping("{id}")
@ApiOperation("编码表-删除")
public AlitaResult<CodeSettingDTO> delete(@PathVariable("id") Long id) {
return AlitaResult.success(codeSettingService.delete(id));
}
@GetMapping("{id}")
@ApiOperation("编码表-查询单个")
public AlitaResult<CodeSettingDTO> findById(@PathVariable("id") Long id) {
return AlitaResult.success(codeSettingService.findById(id));
}
@GetMapping("list")
@ApiOperation("编码表-列表查询")
public AlitaResult<List<CodeSettingDTO>> list(CodeSettingQueryDTO queryDTO) {
return AlitaResult.success(codeSettingService.list(queryDTO));
}
@GetMapping("page")
@ApiOperation("编码表-分页查询")
public AlitaResult<Page<CodeSettingDTO>> page(CodeSettingPageQueryDTO queryDTO) {
codeSettingService.page(queryDTO);
return AlitaResult.success(queryDTO.getPageResult());
}
}

View File

@@ -0,0 +1,25 @@
package com.senmesh.codesetting.convert;
import com.senmesh.codesetting.dto.CodeSettingDTO;
import com.senmesh.codesetting.dto.CodeSettingOperatorDTO;
import com.senmesh.codesetting.entity.CodeSetting;
import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;
/**
* 编码表
*
* @author chenyun chenyun.home@outlook.com
* @since 1.0.0 2023-01-04
*/
@Mapper
public interface CodeSettingConvert {
CodeSettingConvert INSTANCE = Mappers.getMapper(CodeSettingConvert.class);
CodeSetting convert(CodeSettingDTO dto);
CodeSettingDTO convert(CodeSetting entity);
CodeSetting convert(CodeSettingOperatorDTO dto);
}

View File

@@ -0,0 +1,35 @@
package com.senmesh.codesetting.dao;
import com.senmesh.codesetting.dto.CodeSettingDTO;
import com.senmesh.codesetting.dto.CodeSettingPageQueryDTO;
import com.senmesh.codesetting.dto.CodeSettingQueryDTO;
import com.senmesh.codesetting.entity.CodeSetting;
import com.senmesh.tkmybatis.mybatis.MyMapper;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import java.util.List;
/**
* 编码表
*
* @author chenyun chenyun.home@outlook.com
* @since 1.0.0 2023-01-04
*/
@Mapper
public interface CodeSettingMapper extends MyMapper<CodeSetting> {
List<CodeSettingDTO> page(@Param("param") CodeSettingPageQueryDTO queryDTO);
List<CodeSettingDTO> list(@Param("param") CodeSettingQueryDTO queryDTO);
/**
* 根据类型查询
*
* @param type
* @return
*/
@Select("select * from xs_code_setting where type = #{type} and deleted = 0")
List<CodeSetting> queryByType(String type);
}

View File

@@ -0,0 +1,45 @@
package com.senmesh.codesetting.dto;
import com.senmesh.common.dto.BaseDTO;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.*;
import lombok.experimental.SuperBuilder;
/**
* 编码表
*
* @author chenyun chenyun.home@outlook.com
* @since 1.0.0 2023-01-04
*/
@ApiModel("编码表")
@Data
@SuperBuilder
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode(callSuper = true)
public class CodeSettingDTO extends BaseDTO {
private static final long serialVersionUID = 1L;
@ApiModelProperty("编码")
private String code;
@ApiModelProperty("名称")
private String name;
@ApiModelProperty("备注")
private String remark;
@ApiModelProperty("类型 模板编码、配置编码")
private String type;
@ApiModelProperty("数据类型")
private String dataType;
@ApiModelProperty("级别 平台、渠道、定制机构、普通机构")
private String level;
@ApiModelProperty("数据值")
private String dataFormat;
}

View File

@@ -0,0 +1,60 @@
package com.senmesh.codesetting.dto;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.hibernate.validator.constraints.Length;
import javax.validation.constraints.NotEmpty;
import java.io.Serializable;
/**
* 编码表
*
* @author chenyun chenyun.home@outlook.com
* @since 1.0.0 2023-01-04
*/
@ApiModel("编码表")
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class CodeSettingOperatorDTO implements Serializable {
private static final long serialVersionUID = 8836210011277521607L;
private Long id;
@ApiModelProperty("编码")
@NotEmpty(message = "配置项编码不能为空")
@Length(min = 1, max = 64, message = "配置项编码长度超限")
private String code;
@ApiModelProperty("名称")
@NotEmpty(message = "配置项名称不能为空")
@Length(min = 1, max = 128, message = "配置项名称长度超限")
private String name;
@ApiModelProperty("备注")
// @NotEmpty(message = "备注不能为空")
private String remark;
@ApiModelProperty("数据值")
private String dataFormat;
@ApiModelProperty("类型 模板编码、配置编码")
@NotEmpty(message = "配置类型不能为空")
private String type;
@ApiModelProperty("数据类型")
@NotEmpty(message = "配置参数类型不能为空")
private String dataType;
@ApiModelProperty("级别 平台、渠道、定制机构、普通机构")
@NotEmpty(message = "配置应用级别不能为空")
private String level;
}

View File

@@ -0,0 +1,77 @@
package com.senmesh.codesetting.dto;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.senmesh.common.annotation.trimAndUpper.TrimAndUpper;
import com.senmesh.common.dto.PageQueryDTO;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.*;
import org.springframework.format.annotation.DateTimeFormat;
import java.util.Date;
/**
* 编码表
*
* @author chenyun chenyun.home@outlook.com
* @since 1.0.0 2023-01-04
*/
@ApiModel("编码表")
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode(callSuper = true)
public class CodeSettingPageQueryDTO extends PageQueryDTO<CodeSettingDTO> {
private static final long serialVersionUID = 1L;
@ApiModelProperty("主键ID")
private Long id;
@ApiModelProperty("编码")
private String code;
@ApiModelProperty("编码 (模糊匹配)")
@TrimAndUpper(TrimAndUpper.ExecuteType.TRIM)
private String codeLike;
@ApiModelProperty("名称 (模糊匹配)")
@TrimAndUpper(TrimAndUpper.ExecuteType.TRIM)
private String nameLike;
@ApiModelProperty("备注")
private String remark;
@ApiModelProperty("类型 模板编码、配置编码")
@TrimAndUpper(TrimAndUpper.ExecuteType.TRIM)
private String type;
@ApiModelProperty("数据类型")
private String dataType;
@ApiModelProperty("级别 平台、渠道、定制机构、普通机构")
private String level;
@ApiModelProperty("创建时间起,格式 (yyyy-MM-dd HH:mm:ss)")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date createTimeStart;
@ApiModelProperty("创建时间止,格式 (yyyy-MM-dd HH:mm:ss)")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date createTimeEnd;
@ApiModelProperty("修改时间起,格式 (yyyy-MM-dd HH:mm:ss)")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date updateTimeStart;
@ApiModelProperty("修改时间止,格式 (yyyy-MM-dd HH:mm:ss)")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date updateTimeEnd;
}

View File

@@ -0,0 +1,74 @@
package com.senmesh.codesetting.dto;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.format.annotation.DateTimeFormat;
import java.io.Serializable;
import java.util.Date;
/**
* 编码表
*
* @author chenyun chenyun.home@outlook.com
* @since 1.0.0 2023-01-04
*/
@ApiModel("编码表")
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class CodeSettingQueryDTO implements Serializable {
private static final long serialVersionUID = 1L;
@ApiModelProperty("主键ID")
private Long id;
@ApiModelProperty("编码")
private String code;
@ApiModelProperty("编码 (模糊匹配)")
private String codeLike;
@ApiModelProperty("名称 (模糊匹配)")
private String nameLike;
@ApiModelProperty("备注")
private String remark;
@ApiModelProperty("类型 模板编码、配置编码")
private String type;
@ApiModelProperty("数据类型")
private String dataType;
@ApiModelProperty("级别 平台、渠道、定制机构、普通机构")
private String level;
@ApiModelProperty("创建时间起,格式 (yyyy-MM-dd HH:mm:ss)")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date createTimeStart;
@ApiModelProperty("创建时间止,格式 (yyyy-MM-dd HH:mm:ss)")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date createTimeEnd;
@ApiModelProperty("修改时间起,格式 (yyyy-MM-dd HH:mm:ss)")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date updateTimeStart;
@ApiModelProperty("修改时间止,格式 (yyyy-MM-dd HH:mm:ss)")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date updateTimeEnd;
}

View File

@@ -0,0 +1,56 @@
package com.senmesh.codesetting.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import com.senmesh.common.entity.BaseEntity;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
/**
* 编码表
*
* @author chenyun chenyun.home@outlook.com
* @since 1.0.0 2023-01-04
*/
@TableName("xs_code_setting")
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode(callSuper = true)
public class CodeSetting extends BaseEntity {
private static final long serialVersionUID = 1L;
/**
* 编码
*/
private String code;
/**
* 名称
*/
private String name;
/**
* 备注
*/
private String remark;
/**
* 类型 模板编码、配置编码
*/
private String type;
/**
* 数据类型
*/
private String dataType;
/**
* 数据格式编号
*/
private String dataFormat;
/**
* 级别 平台、渠道、定制机构、普通机构
*/
private String level;
}

View File

@@ -0,0 +1,60 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.senmesh.codesetting.dao.CodeSettingMapper">
<select id="list" resultType="com.senmesh.codesetting.dto.CodeSettingDTO">
SELECT t.*
<include refid="queryFromSql"></include>
</select>
<select id="page" resultType="com.senmesh.codesetting.dto.CodeSettingDTO">
SELECT t.*
<include refid="queryFromSql"></include>
</select>
<sql id="queryFromSql">
FROM
xs_code_setting t
WHERE t.deleted = FALSE
<if test='param.id != null'>
AND t.id = #{param.id}
</if>
<if test='param.code != null and param.code.trim() != ""'>
AND t.code = #{param.code}
</if>
<if test='param.codeLike != null and param.codeLike.trim() != ""'>
<bind name="codeLike" value="'%' + param.codeLike + '%'"/>
AND t.code like #{codeLike}
</if>
<if test='param.nameLike != null and param.nameLike.trim() != ""'>
<bind name="nameLike" value="'%' + param.nameLike + '%'"/>
AND t.name like #{nameLike}
</if>
<if test='param.remark != null and param.remark.trim() != ""'>
AND t.remark = #{param.remark}
</if>
<if test='param.type != null and param.type.trim() != ""'>
AND t.type = #{param.type}
</if>
<if test='param.dataType != null and param.dataType.trim() != ""'>
AND t.data_type = #{param.dataType}
</if>
<if test='param.level != null and param.level.trim() != ""'>
AND FIND_IN_SET(CONCAT(#{param.level},""),t.level)
</if>
<if test='param.createTimeStart != null'>
AND t.create_time &gt;= #{param.createTimeStart}
</if>
<if test='param.createTimeEnd != null'>
AND t.create_time &lt;= #{param.createTimeEnd}
</if>
<if test='param.updateTimeStart != null'>
AND t.update_time &gt;= #{param.updateTimeStart}
</if>
<if test='param.updateTimeEnd != null'>
AND t.update_time &lt;= #{param.updateTimeEnd}
</if>
ORDER BY t.id DESC
</sql>
</mapper>

View File

@@ -0,0 +1,95 @@
package com.senmesh.codesetting.service;
import com.senmesh.codesetting.dto.CodeSettingDTO;
import com.senmesh.codesetting.dto.CodeSettingOperatorDTO;
import com.senmesh.codesetting.dto.CodeSettingPageQueryDTO;
import com.senmesh.codesetting.dto.CodeSettingQueryDTO;
import org.springframework.validation.annotation.Validated;
import javax.validation.Valid;
import javax.validation.groups.Default;
import java.util.List;
/**
* 编码表
*
* @author chenyun chenyun.home@outlook.com
* @since 1.0.0 2023-01-04
*/
@Validated(Default.class)
public interface CodeSettingService {
/**
* 编码表 - 创建
*
* @param operatorDto 操作dto
* @return {@link CodeSettingDTO}
*/
CodeSettingDTO create(@Valid CodeSettingOperatorDTO operatorDto);
/**
* 校验编码是否存在
*
* @param code 代码
* @param codeSettingId 编码ID
* @return boolean true: 存在重复编码
*/
boolean checkCode(String code, Long codeSettingId);
/**
* 编码表 - 修改
*
* @param operatorDto 操作dto
* @return {@link CodeSettingDTO}
*/
CodeSettingDTO modify(@Valid CodeSettingOperatorDTO operatorDto);
/**
* 编码表 - 删除
*
* @param id id
* @return {@link CodeSettingDTO}
*/
CodeSettingDTO delete(Long id);
/**
* 编码表 - 根据ID查询单个
*
* @param id id
* @return {@link CodeSettingDTO}
*/
CodeSettingDTO findById(Long id);
/**
* 编码表 - 列表查询
*
* @param queryDTO 列表查询dto
* @return {@link List}<{@link CodeSettingDTO}>
*/
List<CodeSettingDTO> list(CodeSettingQueryDTO queryDTO);
/**
* 编码表 - 分页查询
*
* @param queryDTO 分页查询dto
* @return {@link CodeSettingDTO}
*/
List<CodeSettingDTO> page(CodeSettingPageQueryDTO queryDTO);
/**
* 根据type和code查询
*
* @param codeSettingType
* @param orgType
* @return
*/
List<com.senmesh.codesetting.dto.CodeSettingDTO> queryCode(String codeSettingType, String orgType);
/**
* 根据code查询
*
* @param code
* @return
*/
CodeSettingDTO getByCode(String code);
}

View File

@@ -0,0 +1,119 @@
package com.senmesh.codesetting.service.impl;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
import javax.annotation.Resource;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.ArrayUtils;
import org.springframework.stereotype.Service;
import com.senmesh.codesetting.convert.CodeSettingConvert;
import com.senmesh.codesetting.dao.CodeSettingMapper;
import com.senmesh.codesetting.dto.CodeSettingDTO;
import com.senmesh.codesetting.dto.CodeSettingOperatorDTO;
import com.senmesh.codesetting.dto.CodeSettingPageQueryDTO;
import com.senmesh.codesetting.dto.CodeSettingQueryDTO;
import com.senmesh.codesetting.entity.CodeSetting;
import com.senmesh.codesetting.service.CodeSettingService;
import com.senmesh.common.constant.CommonConstants;
import com.senmesh.common.exception.util.Assert;
import com.senmesh.common.util.UserContextHolder;
import com.senmesh.config.dao.ConfigMapper;
import lombok.extern.slf4j.Slf4j;
/**
* 编码表
*
* @author chenyun chenyun.home@outlook.com
* @since 1.0.0 2023-01-04
*/
@Slf4j
@Service
public class CodeSettingServiceImpl implements CodeSettingService {
@Resource
private CodeSettingMapper codeSettingMapper;
@Resource
private ConfigMapper configMapper;
@Override
public CodeSettingDTO create(CodeSettingOperatorDTO operatorDto) {
Assert.isTrue(!checkCode(operatorDto.getCode(), null), "配置项编码已存在");
CodeSetting codeSetting = CodeSettingConvert.INSTANCE.convert(operatorDto);
codeSettingMapper.saveEntity(codeSetting);
return CodeSettingConvert.INSTANCE.convert(codeSetting);
}
@Override
public boolean checkCode(String code, Long codeSettingId) {
return codeSettingMapper.existsEntity(CodeSetting.builder().code(code).build());
}
@Override
public CodeSettingDTO modify(CodeSettingOperatorDTO operatorDto) {
CodeSettingDTO codeSettingDto = this.findById(operatorDto.getId());
if (Objects.isNull(codeSettingDto)) {
return null;
}
Assert.isTrue(!checkCode(operatorDto.getCode(), operatorDto.getId()), "配置项编码已存在");
CodeSetting codeSetting = CodeSettingConvert.INSTANCE.convert(operatorDto);
codeSettingMapper.updateEntity(codeSetting);
return this.findById(operatorDto.getId());
}
@Override
public CodeSettingDTO delete(Long id) {
CodeSettingDTO codeSettingDto = this.findById(id);
if (Objects.isNull(codeSettingDto)) {
return null;
}
CodeSetting codeSetting = CodeSetting.builder().build();
codeSetting.setId(id);
codeSettingMapper.deleteEntity(codeSetting, UserContextHolder.getLoginUserId());
// 删除配置项的时候,同步删除配置项下的配置
configMapper.batchDelete(codeSettingDto.getCode(), UserContextHolder.getExistLoginUserId());
return codeSettingDto;
}
@Override
public CodeSettingDTO findById(Long id) {
CodeSetting codeSetting = codeSettingMapper.getById(id);
if (Objects.isNull(codeSetting)) {
return null;
}
return CodeSettingConvert.INSTANCE.convert(codeSetting);
}
@Override
public List<CodeSettingDTO> list(CodeSettingQueryDTO queryDTO) {
return codeSettingMapper.list(queryDTO);
}
@Override
public List<CodeSettingDTO> page(CodeSettingPageQueryDTO queryDTO) {
return codeSettingMapper.page(queryDTO);
}
@Override
public List<CodeSettingDTO> queryCode(String codeSettingType, String orgType) {
List<CodeSetting> codeSettings = codeSettingMapper.queryByType(codeSettingType);
return codeSettings.stream()
.filter(codeSetting -> ArrayUtils
.contains(codeSetting.getLevel().split(CommonConstants.COMMA), orgType))
.map(CodeSettingConvert.INSTANCE::convert).collect(Collectors.toList());
}
@Override
public CodeSettingDTO getByCode(String code) {
List<CodeSettingDTO> list = this.list(CodeSettingQueryDTO.builder().code(code).build());
if (CollectionUtils.isEmpty(list)) {
return null;
}
return list.get(0);
}
}

View File

@@ -0,0 +1,253 @@
package com.senmesh.common;
import java.io.Serializable;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.senmesh.sdk.common.constant.SdkConstants;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 用于 ajax 请求的响应工具类
*
* @author guhong
* @date 2019/8/19
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class AlitaResult<T> implements Serializable {
/** */
private static final long serialVersionUID = 5933105804003541576L;
/** 未登录 * */
public static final String UN_LOGIN_CODE = "401";
/** 无权限 * */
public static final String NO_AUTHORITY_CODE = "402";
/** 无查询信息 * */
public static final String NO_RESULT_CODE = "405";
/** 登录用户多机构 * */
public static final String DUPLICATE_ORG_CODE = "600";
/** 登录用户多用户 * */
public static final String DUPLICATE_USER_CODE = "601";
/** 用户未实名 * */
public static final String UN_AUTH_CODE = "602";
/** 用户更换token **/
public static final String CHANGE_TOKEN_CODE = "603";
/** 用户未绑定手机号 **/
public static final String UN_BINDING_PHONE = "604";
/** 登录用户相关异常 **/
public static final String LOGIN_ERROR_CODE = "666";
/** 系统异常 * */
public static final String SYSTEM_ERROR_CODE = "999";
/** 业务异常 **/
public static final String BUSINESS_ERROR_CODE = "700";
/** 校验异常 * */
public static final String VALIDATION_ERROR_CODE = "555";
/** 操作成功 * */
public static final String SUCCESS_CODE = "200";
public static final String CHANG_TOKEN_MSG = "token变更";
/** 链路id **/
private String requestId;
/** 响应信息 */
private String msg;
/** 响应code */
private String code;
/** 是否成功 */
private Boolean success;
/** 操作成功,响应数据 */
private T data;
/** 操作失败结果 **/
private Object errorData;
/** 临时json **/
@JsonIgnore
private String tempJson;
public AlitaResult(String code, String msg, boolean success, T data) {
this.msg = msg;
this.code = code;
this.success = success;
this.data = data;
}
public AlitaResult(String code, String msg, Serializable errorData) {
this.msg = msg;
this.code = code;
this.success = false;
this.errorData = errorData;
}
/**
* 创建一个成功的响应对象
*
* @return
*/
public static <T> AlitaResult<T> success() {
return success(null);
}
/**
* 创建一个成功的响应对象
*
* @param data
* @return
*/
public static <T> AlitaResult<T> success(T data) {
return new AlitaResult<T>(SUCCESS_CODE, "请求成功", true, data);
}
/**
* 创建一个成功的响应对象
*
* @param msg
* @return
*/
public static <T> AlitaResult<T> success(String msg, T data) {
return new AlitaResult<T>(SUCCESS_CODE, msg, true, data);
}
/**
* 创建一个失败的响应对象
*
* @param msg
* @return
*/
public static <T> AlitaResult<T> error(String msg) {
return new AlitaResult<T>(SYSTEM_ERROR_CODE, msg, false, null);
}
/**
* 创建一个失败的响应对象
*
* @param code
* @param msg
* @return
*/
public static <T> AlitaResult<T> error(String code, String msg) {
return new AlitaResult<T>(code, msg, false, null);
}
/**
* 创建一个失败的响应对象
*
* @param code
* @param msg
* @return
*/
public static <T> AlitaResult<T> error(String code, String msg, T data) {
return new AlitaResult<T>(code, msg, false, data);
}
/**
* 创建一个失败的响应对象
*
* @param msg
* @return
*/
public static <T> AlitaResult<T> error(String msg, T data) {
return new AlitaResult<T>(SYSTEM_ERROR_CODE, msg, false, data);
}
/**
* 创建业务异常对象
*
* @param <T>
* @param msg
* @return
* @author sr on 2020-07-02
*/
public static <T> AlitaResult<T> bizError(String msg) {
return new AlitaResult<T>(BUSINESS_ERROR_CODE, msg, false, null);
}
/**
* 创建一个未登录响应对象
*
* @return
*/
public static <T> AlitaResult<T> unLogin() {
return new AlitaResult<T>(UN_LOGIN_CODE, "请先登录", false, null);
}
/**
* 创建未实名响应对象
* @param <T>
* @return
* @author sr on 2021-06-30
*/
public static <T> AlitaResult<T> unAuth() {
return new AlitaResult<T>(UN_AUTH_CODE, "请先实名", false, null);
}
/**
* 创建未绑定手机号响应对象
* @param <T>
* @return
* @author sr on 2021-06-30
*/
public static <T> AlitaResult<T> unBinding() {
return new AlitaResult<T>(UN_BINDING_PHONE, "请先绑定手机号", false, null);
}
/**
* 创建一个未登录响应对象
*
* @param loginUrl 登录地址
* @return
* @author sr on 2019-12-29
*/
public static AlitaResult<String> unLogin(String loginUrl) {
return new AlitaResult<String>(UN_LOGIN_CODE, "请先登录", false, loginUrl);
}
/**
* 更换token
* @param <T>
* @param token
* @return
* @author sr on 2021-08-02
*/
public static <T> AlitaResult<T> changeToken(Serializable token) {
return new AlitaResult<T>(AlitaResult.CHANGE_TOKEN_CODE, CHANG_TOKEN_MSG, token);
}
/**
* 创建一个未登录响应对象
*
* @return
* @author sr on 2019-12-29
*/
public static <T> AlitaResult<T> noResult() {
return new AlitaResult<T>(NO_RESULT_CODE, "无查询信息", false, null);
}
/**
* 接口调用失败,未能拿到返回对象
*
* @param <T>
* @param msg
* @return
* @author sr on 2020-04-10
*/
public static <T> AlitaResult<T> sdkInvokeNotReachError(String msg) {
return new AlitaResult<T>(SdkConstants.SDK_INVOKE_NOT_REACH_CODE, msg, false, null);
}
@SuppressWarnings({"hiding", "unchecked"})
public <T> AlitaResult<T> setRequestId(String requestId){
this.requestId = requestId;
return (AlitaResult<T>) this;
}
public <T> AlitaResult<T> setTempJson(String tempJson){
this.tempJson = tempJson;
return (AlitaResult<T>) this;
}
}

View File

@@ -0,0 +1,110 @@
package com.senmesh.common;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.core.env.Environment;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.deser.std.StringDeserializer;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.senmesh.common.cache.DefaultCacheProvider;
import com.senmesh.common.cache.ICacheProvider;
import com.senmesh.common.log.ILogProvider;
import com.senmesh.common.log.provider.Slf4jLogProvider;
import com.senmesh.common.log.util.LogUtils;
import com.senmesh.common.service.IMockProvider;
import com.senmesh.common.service.IMockProvider.DefaultMockProvider;
import com.senmesh.common.util.EnvironmentUtils;
import com.senmesh.common.util.MockUtils;
import com.senmesh.component.file.service.IFileStorage;
import com.senmesh.component.file.service.storage.LocalFileStorage;
import com.senmesh.component.file.util.FileStorageUtils;
import lombok.var;
/**
* @author sr on 2020-07-24
*/
@Configuration
public class CommonConfiguration {
@Bean
EnvironmentUtils environmentUtils(Environment environment) {
return new EnvironmentUtils(environment);
}
@Bean
@ConditionalOnMissingBean(ICacheProvider.class)
@Order(100)
DefaultCacheProvider defauCacheProvider() {
return new DefaultCacheProvider();
}
@Bean
@Order(100)
@ConditionalOnMissingBean(ILogProvider.class)
Slf4jLogProvider slf4jLogProvider() {
return new Slf4jLogProvider();
}
@Bean
@Order(200)
LogUtils logUtils(ILogProvider iLogProvider) {
LogUtils logUtils = new LogUtils();
logUtils.setLogProvider(iLogProvider);
return logUtils;
}
@Bean
@ConditionalOnMissingBean(IFileStorage.class)
public LocalFileStorage localFileStorage() {
return new LocalFileStorage();
}
@Bean
public FileStorageUtils fileStorageUtils(IFileStorage fileStorage) {
return new FileStorageUtils(fileStorage);
}
@Bean
@ConditionalOnMissingBean(IMockProvider.class)
public IMockProvider defaultMockProvider() {
return new DefaultMockProvider();
}
@Bean
public MockUtils mockUtils(IMockProvider mockProvider) {
return new MockUtils(mockProvider);
}
/**
* requestbody string trim
* @return
* @author sr on 2024-04-19
*/
@Bean
public SimpleModule stringDeserializerModule() {
final var module = new SimpleModule();
module.addDeserializer(String.class, new StringDeserializer() {
@Override
public String deserialize(JsonParser p, DeserializationContext ctx) throws java.io.IOException {
final var value = super.deserialize(p, ctx);
if (value == null) {
return null;
}
final var trimmed = value.trim();
if (trimmed.isEmpty()) {
return null;
}
return trimmed;
}
});
return module;
}
}

View File

@@ -0,0 +1,26 @@
package com.senmesh.common;
import org.jasypt.encryption.StringEncryptor;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import com.senmesh.common.trace.DefaultTracer;
import com.senmesh.common.trace.ITracer;
import com.senmesh.common.util.JasyptUtils;
@Configuration
public class CoreConfiguration {
@Bean
JasyptUtils jasyptUtils(StringEncryptor stringEncryptor) {
return new JasyptUtils(stringEncryptor);
}
@Bean
@ConditionalOnMissingBean(value = ITracer.class)
ITracer defaultTracer() {
return new DefaultTracer();
}
}

View File

@@ -0,0 +1,24 @@
package com.senmesh.common.actuate.health.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 监控健康信息
*
* @author sr on 2020-07-08
* @param <T>
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class HealthDTO<T> {
private String status;
private T details;
}

View File

@@ -0,0 +1,14 @@
package com.senmesh.common.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface AlitaStopWatch {
}

View File

@@ -0,0 +1,56 @@
package com.senmesh.common.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import com.senmesh.common.constant.ProjectProfileEnum;
/**
* 校验并mock
*
* @author sr on 2020-05-20
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface CheckAndMock {
/**
* 属性前缀
*
* @return
* @author sr on 2020-05-20
*/
String propertyPrefix() default "";
/**
* 属性配置类
*
* @return
* @author sr on 2020-05-20
*/
Class<ICheckAndMockPropertity> checkAndMockPropertity() default ICheckAndMockPropertity.class;
/**
* mock实现类(需注入spring上下文,提供与原方法方法名/参数/返回对象一致的方法)
*
* @return
* @author sr on 2020-05-20
*/
Class<?> mockClass();
// String mockBeanId();
/**
* 指定环境直接mock
*
* @return
* @author sr on 2020-05-21
*/
ProjectProfileEnum[] mockIfInProfile() default {};
}

View File

@@ -0,0 +1,27 @@
package com.senmesh.common.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 校验定时任务是否开启
*
* @author sr on 2020-05-19
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface CheckJob {
/**
* 配置文件是否开启的key,明确指定为false时才关闭
*
* @return
* @author sr on 2020-05-19
*/
String value();
}

View File

@@ -0,0 +1,34 @@
package com.senmesh.common.annotation;
/**
* 需要校验后mock的配置
*
* @author sr on 2020-05-20
*/
public interface ICheckAndMockPropertity {
/**
* 是否开启
*
* @return
* @author sr on 2020-05-21
*/
Boolean getEnable();
/**
* 次数限制
*
* @return
* @author sr on 2020-05-21
*/
Long getNumLimit();
/**
* 属性前缀
*
* @return
* @author sr on 2020-05-21
*/
String getPropertityPrefix();
}

View File

@@ -0,0 +1,19 @@
package com.senmesh.common.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import com.fasterxml.jackson.annotation.JacksonAnnotationsInside;
/**
* 掩码
* @author sr on 2021-11-25
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.METHOD})
@JacksonAnnotationsInside
public @interface Mask {
}

View File

@@ -0,0 +1,32 @@
package com.senmesh.common.annotation;
import com.fasterxml.jackson.databind.introspect.Annotated;
import com.fasterxml.jackson.databind.introspect.AnnotatedMember;
import com.fasterxml.jackson.databind.introspect.JacksonAnnotationIntrospector;
/**
* @author sr on 2021-11-25
*/
public class MaskAnontationIntrospector extends JacksonAnnotationIntrospector {
private static final long serialVersionUID = 1L;
@Override
public Object findSerializer(Annotated a) {
Mask mask = a.getAnnotation(Mask.class);
if (mask != null) {
return MaskSerializer.class;
}
return super.findSerializer(a);
}
@Override
public boolean hasIgnoreMarker(AnnotatedMember m) {
// 直接不打印该字段,则置为true
/*if (m.hasAnnotation(Mask.class)) {
return true;
}*/
return super.hasIgnoreMarker(m);
}
}

View File

@@ -0,0 +1,37 @@
package com.senmesh.common.annotation;
import java.io.IOException;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.ser.std.StdSerializer;
/**
* @author sr on 2021-11-25
*/
public class MaskSerializer extends StdSerializer<Object> {
private static final long serialVersionUID = 1L;
private String mask = "***";
public MaskSerializer() {
super(Object.class);
}
public MaskSerializer(String mask, int count) {
this();
}
public MaskSerializer(MaskSerializer src) {
super(src);
}
@Override
public void serialize(Object s, JsonGenerator jsonGenerator,
SerializerProvider serializerProvider) throws IOException {
jsonGenerator.writeString(mask);
}
}

View File

@@ -0,0 +1,49 @@
package com.senmesh.common.annotation.aspect;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
import org.springframework.util.StopWatch;
import com.senmesh.common.annotation.AlitaStopWatch;
import com.senmesh.common.exception.util.DevopsUtils;
import com.senmesh.common.util.EnvironmentUtils;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@Aspect
@Component
public class AlitaStopWatchAspect {
public static final String DEVOPS_TIME_KEY = "devops.usetime";
/** 默认十分钟 **/
public static final Long DEFAULT_DEVOPS_TIME = 10 * 60L;
@Around("@annotation(alitaStopWatch)")
public Object around(ProceedingJoinPoint joinPoint, AlitaStopWatch alitaStopWatch) throws Throwable {
StopWatch stopWatch = new StopWatch();
stopWatch.start();
try {
return joinPoint.proceed();
} finally {
stopWatch.stop();
String useTimeStr = EnvironmentUtils.getProperty(DEVOPS_TIME_KEY);
Long maxUseTime = DEFAULT_DEVOPS_TIME;
try {
maxUseTime = Long.parseLong(useTimeStr);
} catch (Exception e) {
}
maxUseTime = 1000 * maxUseTime;
Long actualTime = stopWatch.getTotalTimeMillis();
String logMsg = joinPoint.getSignature().toShortString() + ",use(ms): " + actualTime;
if (actualTime > maxUseTime) {
DevopsUtils.deal(logMsg);
}
log.info(logMsg);
}
}
}

View File

@@ -0,0 +1,129 @@
package com.senmesh.common.annotation.aspect;
import com.senmesh.common.annotation.CheckAndMock;
import com.senmesh.common.annotation.ICheckAndMockPropertity;
import com.senmesh.common.cache.ICacheProvider;
import com.senmesh.common.constant.CommonConstants;
import com.senmesh.common.constant.ProjectProfileEnum;
import com.senmesh.common.util.SpringContextHolder;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.annotation.Order;
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
/**
* 校验并mock
*
* @author sr on 2020-05-19
*/
@Aspect
@Component
@Slf4j
@Order(200)
public class CheckAndMockAspect {
/** 是否开启属性 **/
public static final String ENABLE_KEY = "enable";
/** 次数限制属性 **/
public static final String NUM_LIMIT_KEY = "numLimit";
/** 默认次数 **/
public static final Long DEFALUT_NUM_LIMIT = 3L;
@Autowired
private Environment environment;
@Autowired
private ICacheProvider iCacheProvider;
@Around("@annotation(checkAndMock)")
public Object around(ProceedingJoinPoint joinPoint, CheckAndMock checkAndMock) throws Throwable {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
ProjectProfileEnum[] profileEnums = checkAndMock.mockIfInProfile();
Class<?> mockClass = checkAndMock.mockClass();
String currentProfile = environment.getProperty(CommonConstants.PROFILE_KEY);
Object[] args = joinPoint.getArgs();
// 只要配置环境mock,就不校验开关和次数限制
if (profileEnums.length > 0) {
for (ProjectProfileEnum profileEnum : profileEnums) {
if (profileEnum.getValue().equals(currentProfile)) {
return mock(mockClass, method, args);
}
}
return joinPoint.proceed();
}
// 获取属性配置
String propertyPrefix = checkAndMock.propertyPrefix();
boolean enable = true;
Long numLimit = null;
if (StringUtils.isNotBlank(propertyPrefix)) {
enable = BooleanUtils.toBoolean(
environment.getProperty(String.join(CommonConstants.POINT, propertyPrefix, ENABLE_KEY)));
try {
numLimit = enable ? Long.parseLong(environment
.getProperty(String.join(CommonConstants.POINT, propertyPrefix, NUM_LIMIT_KEY))) : 0L;
} catch (NumberFormatException e) {
log.info("{} numLimit not right, use default numLimit", propertyPrefix);
}
} else {
try {
ICheckAndMockPropertity properties =
SpringContextHolder.getBean(checkAndMock.checkAndMockPropertity());
enable = properties.getEnable();
numLimit = properties.getNumLimit();
propertyPrefix = properties.getPropertityPrefix();
} catch (Exception e) {
log.info("ICheckAndMockPropertity not found...");
enable = false;
}
}
// 开关关闭,都走mock
if (!enable) {
return mock(mockClass, method, args);
}
// 开关开启,生产无次数限制,本地测试根据配置/默认次数控制
boolean isProd = ProjectProfileEnum.PROD.getValue().equals(currentProfile);
if (isProd) {
return joinPoint.proceed();
}
numLimit = numLimit == null ? DEFALUT_NUM_LIMIT : numLimit;
String key = String.join(CommonConstants.POINT, propertyPrefix, NUM_LIMIT_KEY);
if (iCacheProvider.outLimit(key, numLimit)) {
// 超限,mock
return mock(mockClass, method, args);
}
// 次数限制
return joinPoint.proceed();
}
/**
* mock调用
*
* @param clazz
* @param method
* @param objects
* @return
* @throws Throwable
* @author sr on 2020-05-21
*/
private Object mock(Class<?> clazz, Method method, Object[] objects) throws Throwable {
Object mockBean = SpringContextHolder.getBean(clazz);
Method mockMethod = mockBean.getClass().getMethod(method.getName(), method.getParameterTypes());
return mockMethod.invoke(mockBean, objects);
}
}

View File

@@ -0,0 +1,46 @@
package com.senmesh.common.annotation.aspect;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Component;
import com.senmesh.common.annotation.CheckJob;
import com.senmesh.common.constant.CommonConstants;
import lombok.extern.slf4j.Slf4j;
/**
* 校验定时任务是否开启
*
* @author sr on 2020-05-19
*/
@Aspect
@Component
@Slf4j
public class CheckJobAspect {
/** 配置无效识别符(只有配置false才失效) **/
private String falseValue = "false";
/** 定时任务配置前缀 **/
public static final String JOB_PROPERTY_PREFIX = "job";
@Autowired
private Environment environment;
@Around("@annotation(checkJob)")
public Object around(ProceedingJoinPoint joinPoint, CheckJob checkJob) throws Throwable {
String propertityKey = checkJob.value();
boolean disable = falseValue.equals(environment
.getProperty(String.join(CommonConstants.POINT, JOB_PROPERTY_PREFIX, propertityKey)));
if (disable) {
log.info("job:{} not enable...", propertityKey);
return null;
}
return joinPoint.proceed();
}
}

View File

@@ -0,0 +1,31 @@
package com.senmesh.common.annotation.trimAndUpper;
import java.lang.annotation.*;
/**
* 用于字符串去空格以及大写
*
* @author Carlye
* @date 2022/02/16
*/
@Target(value = { ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface TrimAndUpper {
/**
* 执行类型
* ALL: (所有类型都包含)
* TRIM: (去两边的空格)
* REPLACE_ALL: (去所有的空格)
* UPPER_CASE: (转大写)
*
* @author Carlye
* @date 2022/02/16
*/
public static enum ExecuteType {
ALL, TRIM, REPLACE_ALL, UPPER_CASE;
}
ExecuteType value() default ExecuteType.ALL;
}

View File

@@ -0,0 +1,27 @@
package com.senmesh.common.annotation.trimAndUpper;
import com.fasterxml.jackson.databind.AnnotationIntrospector;
import com.fasterxml.jackson.databind.introspect.Annotated;
import com.fasterxml.jackson.databind.introspect.AnnotationIntrospectorPair;
/**
* @author sr on 2021-11-25
*/
public class TrimAndUpperAnontationIntrospector extends AnnotationIntrospectorPair {
private static final long serialVersionUID = -1755768596246088917L;
public TrimAndUpperAnontationIntrospector(AnnotationIntrospector p,
AnnotationIntrospector s) {
super(p, s);
}
@Override
public Object findDeserializer(Annotated a) {
TrimAndUpper trim = a.getAnnotation(TrimAndUpper.class);
if (trim != null) {
return TrimAndUpperDeserializer.class;
}
return super.findDeserializer(a);
}
}

View File

@@ -0,0 +1,59 @@
package com.senmesh.common.annotation.trimAndUpper;
import com.fasterxml.jackson.databind.AnnotationIntrospector;
import com.fasterxml.jackson.databind.DeserializationConfig;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.cfg.BaseSettings;
import com.fasterxml.jackson.databind.introspect.AnnotationIntrospectorPair;
import java.lang.reflect.Field;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.format.FormatterRegistry;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.util.ReflectionUtils;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter;
@Configuration
public class TrimAndUpperConfig {
@Autowired
public void addFormatters(FormatterRegistry registry) {
TrimAndUpperFormatterFactory trimAndUpperCaseFormatterFactory = new TrimAndUpperFormatterFactory();
registry.addFormatterForFieldAnnotation(trimAndUpperCaseFormatterFactory);
}
@Autowired
public void init(RequestMappingHandlerAdapter handlerAdapter){
for (HttpMessageConverter<?> messageConverter : handlerAdapter.getMessageConverters()) {
try {
if (messageConverter instanceof MappingJackson2HttpMessageConverter){
MappingJackson2HttpMessageConverter converter1 = (MappingJackson2HttpMessageConverter)messageConverter;
ObjectMapper objectMapper = converter1.getObjectMapper();
DeserializationConfig deserializationConfig = objectMapper.getDeserializationConfig();
Field _base = ReflectionUtils.findField(DeserializationConfig.class, "_base");
ReflectionUtils.makeAccessible(_base);
BaseSettings baseSettings = (BaseSettings)ReflectionUtils.getField(_base, deserializationConfig);
AnnotationIntrospector annotationIntrospector = baseSettings.getAnnotationIntrospector();
if (annotationIntrospector instanceof AnnotationIntrospectorPair){
AnnotationIntrospectorPair annotationIntrospectorOld = (AnnotationIntrospectorPair)baseSettings.getAnnotationIntrospector();
Field _primary = ReflectionUtils.findField(AnnotationIntrospectorPair.class, "_primary");
ReflectionUtils.makeAccessible(_primary);
AnnotationIntrospector _primaryObj = (AnnotationIntrospector)ReflectionUtils.getField(_primary, annotationIntrospectorOld);
Field _secondary = ReflectionUtils.findField(AnnotationIntrospectorPair.class, "_secondary");
ReflectionUtils.makeAccessible(_secondary);
AnnotationIntrospector _secondaryObj = (AnnotationIntrospector)ReflectionUtils.getField(_secondary, annotationIntrospectorOld);
converter1.getObjectMapper().setAnnotationIntrospector(new TrimAndUpperAnontationIntrospector(_primaryObj,_secondaryObj));
}
}
} catch (Exception e) {
//doNothing
}
}
}
}

View File

@@ -0,0 +1,66 @@
package com.senmesh.common.annotation.trimAndUpper;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.BeanProperty;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.deser.ContextualDeserializer;
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
import com.google.common.collect.Maps;
import com.senmesh.common.annotation.trimAndUpper.TrimAndUpper.ExecuteType;
import java.io.IOException;
import java.util.Map;
import java.util.Objects;
import org.apache.commons.lang3.StringUtils;
public class TrimAndUpperDeserializer extends StdDeserializer<String> implements ContextualDeserializer {
private static final Map<ExecuteType, TrimAndUpperDeserializer> INSTANCE_MAP = Maps.newHashMap();
private static final long serialVersionUID = 3463447204376923373L;
static {
for (ExecuteType value : ExecuteType.values()) {
INSTANCE_MAP.put(value,new TrimAndUpperDeserializer(value));
}
}
private ExecuteType executeType;
public TrimAndUpperDeserializer(){
super(String.class);
}
public TrimAndUpperDeserializer(ExecuteType executeType){
super(String.class);
this.executeType = executeType;
}
@Override
public String deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException, JsonProcessingException {
String s = jsonParser.getText();
if (!(StringUtils.isEmpty(s) || Objects.isNull(executeType))) {
if (ExecuteType.ALL == executeType || ExecuteType.TRIM == executeType) {
s = StringUtils.replace(StringUtils.trim(s)," ","");
}
if (ExecuteType.ALL == executeType || ExecuteType.TRIM == executeType) {
s = StringUtils.upperCase(s);
}
}
return s;
}
@Override
public JsonDeserializer<?> createContextual(DeserializationContext prov, BeanProperty property) throws JsonMappingException {
ExecuteType executeType = null;
TrimAndUpper ann = null;
if (property != null) {
ann = property.getAnnotation(TrimAndUpper.class);
}
if (ann != null) {
executeType = ann.value();
}
return INSTANCE_MAP.get(executeType);
}
}

View File

@@ -0,0 +1,77 @@
package com.senmesh.common.annotation.trimAndUpper;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.senmesh.common.annotation.trimAndUpper.TrimAndUpper.ExecuteType;
import org.apache.commons.lang3.StringUtils;
import org.springframework.format.AnnotationFormatterFactory;
import org.springframework.format.Formatter;
import org.springframework.format.Parser;
import org.springframework.format.Printer;
import java.text.ParseException;
import java.util.*;
public class TrimAndUpperFormatterFactory implements
AnnotationFormatterFactory<TrimAndUpper> {
private static final Map<ExecuteType, TrimAndUpperCaseFormatter> TRIMMER_FORMATTER_MAP;
static {
ExecuteType[] values = ExecuteType.values();
Map<ExecuteType, TrimAndUpperCaseFormatter> map = Maps.newHashMap();
for (ExecuteType type : values) {
map.put(type, new TrimAndUpperCaseFormatter(type));
}
TRIMMER_FORMATTER_MAP = Collections.unmodifiableMap(map);
}
@Override
public Set<Class<?>> getFieldTypes() {
return Sets.newHashSet(String.class);
}
@Override
public Printer<?> getPrinter(TrimAndUpper trimAndUpper, Class<?> aClass) {
return TRIMMER_FORMATTER_MAP.get(trimAndUpper.value());
}
@Override
public Parser<?> getParser(TrimAndUpper trimAndUpper, Class<?> aClass) {
return TRIMMER_FORMATTER_MAP.get(trimAndUpper.value());
}
private static class TrimAndUpperCaseFormatter implements Formatter<String> {
private TrimAndUpper.ExecuteType executeType;
public TrimAndUpperCaseFormatter(TrimAndUpper.ExecuteType executeType) {
this.executeType = executeType;
}
@Override
public String parse(String value, Locale locale) throws ParseException {
if (StringUtils.isEmpty(value) || Objects.isNull(executeType)) {
return value;
}
// 去所有空格
if (ExecuteType.ALL == executeType || ExecuteType.REPLACE_ALL == executeType) {
value = StringUtils.replace(value, " ", "");
}
// 转大写
if (ExecuteType.ALL == executeType || ExecuteType.UPPER_CASE == executeType) {
value = StringUtils.upperCase(value);
}
//去两边空格
if (ExecuteType.ALL == executeType || ExecuteType.TRIM == executeType) {
value = StringUtils.trim(value);
}
return value;
}
@Override
public String print(String value, Locale locale) {
return value;
}
}
}

View File

@@ -0,0 +1,31 @@
package com.senmesh.common.cache;
import java.util.Map;
import org.apache.commons.collections.MapUtils;
import com.google.common.collect.Maps;
/**
* @author sr on 2020-07-07
*/
public class DefaultCacheProvider implements ICacheProvider {
private Map<String, Long> numLimitMap = Maps.newConcurrentMap();
@Override
public boolean outLimit(final String key, Long maxNumLimit) {
Long currentNumLimit = null;
final Object lockKey = key.intern();
synchronized (lockKey) {
currentNumLimit = MapUtils.getLong(numLimitMap, key, 0L);
if (currentNumLimit >= maxNumLimit) {
return true;
}
currentNumLimit++;
numLimitMap.put(key, currentNumLimit);
}
return false;
}
}

View File

@@ -0,0 +1,18 @@
package com.senmesh.common.cache;
/**
* @author sr on 2020-07-07
*/
public interface ICacheProvider {
/**
* 是否超限
*
* @param key
* @param maxNumLimit
* @return
* @author sr on 2020-07-07
*/
boolean outLimit(String key, Long maxNumLimit);
}

View File

@@ -0,0 +1,32 @@
package com.senmesh.common.cache;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.core.annotation.Order;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
/**
* @author sr on 2020-07-07
*/
@Service
@ConditionalOnClass(RedisTemplate.class)
@Order(1)
public class RedisCacheProvier implements ICacheProvider{
@SuppressWarnings("rawtypes")
@Autowired(required = false)
private RedisTemplate redisTemplate;
@SuppressWarnings("unchecked")
@Override
public boolean outLimit(String key, Long maxNumLimit) {
Long currentNumLimit = redisTemplate.opsForValue().increment(key);
if (currentNumLimit > maxNumLimit) {
redisTemplate.opsForValue().decrement(key);
return true;
}
return false;
}
}

View File

@@ -0,0 +1,48 @@
package com.senmesh.common.constant;
/**
* 系统枚举
*
* @author sr on 2020-06-02
*/
public enum AuthStatusEnum {
/**
* 待认证
**/
NOT_AUTH(1, "待认证"),
/**
* 认证中
**/
AUTHING(2, "认证中"),
/**
* 认证成功
**/
AUTH_SUCCESS(3, "认证成功"),
/**
* 认证失败
**/
AUTH_FAIL(4, "认证失败");
/**
* 状态类型
*/
private Integer type;
/**
* 状态名称
*/
private String name;
AuthStatusEnum(Integer type, String name) {
this.type = type;
this.name = name;
}
public Integer getType() {
return type;
}
public String getName() {
return name;
}
}

View File

@@ -0,0 +1,12 @@
package com.senmesh.common.constant;
/**
* 缓存常量
*
* @author sr on 2020-04-03
*/
public interface CacheConstants {
String CACHE_NAME_DICT = "dict";
}

View File

@@ -0,0 +1,70 @@
package com.senmesh.common.constant;
/**
* 公共常量
* @author sr on 2019-12-03
*/
public interface CommonConstants {
/** _(下划线) **/
String UNDERLINE = "_";
/** # **/
String POUND = "#";
/** & **/
String AND = "&";
/** ,(逗号) **/
String COMMA = ",";
/** :(冒号) **/
String COLON = ":";
/** ;(分号) **/
String SEMICOLON = ";";
/** . **/
String POINT = ".";
/** = **/
String EQUAL = "=";
/** ? **/
String QUESTION = "?";
/** -(连字号) **/
String HYPHEN = "-";
/** ...(省略号) **/
String ELLIPSIS = "...";
/** 金额精度:两位 **/
int AMOUNT_SCALE = 2;
/** 金额小数位 **/
int AMOUNT_FRACTION = 2;
/** 金额整数位 **/
int AMOUNT_INTEGER = 14;
/** 项目环境配置key **/
String PROFILE_KEY = "spring.profiles.active";
/** 项目名称 **/
String APPLICATION_NAME_KEY = "spring.application.name";
String SYSTEM_CODE_KEY = "system.code";
/** security是否使用本地方法 **/
String USELOCAL_KEY = "security.useLocal";
/**
* 表字段去空格
*/
String TRIM_TABLE_FIELDS_ENBALE_KEY = "trim.table.fields";
/** 开关键 **/
String ENABLE_KEY = "enable";
String FALSE_STR = "false";
/** 配置值:是 **/
String CONFIG_TRUE = "1";
/** 配置值:否 **/
String CONFIG_FALSE = "0";
/** 平台机构id **/
Long PLATFORM_ORG_ID = 1L;
/** 用户禁用/注销登录态黑名单key前缀(存Redis,值存在期间该用户已发放的token视为无效) **/
String USER_DISABLED_KEY = "user:disabled:";
}

View File

@@ -0,0 +1,6 @@
package com.senmesh.common.constant;
public interface CommonMqConstants {
}

View File

@@ -0,0 +1,18 @@
package com.senmesh.common.constant;
/**
* @author Jerry.li
* @description Cookie相关常量
* @date 2020/5/11
*/
public class CookieConstants {
/**
* 登录Cookie名称前缀
*/
public static final String LOGIN_COOKIE_NAME_PREFIX = "xs_ck_";
/** 属性配置键:cookie域名 **/
public static final String PROP_KEY_COOKIE_DOMAIN = "cookie.domain";
}

View File

@@ -0,0 +1,21 @@
package com.senmesh.common.constant;
/**
* 异常错误信息
*
* @author on 2019-11-05
*/
public interface ExceptionMsgs {
String ENTITY_HAS_EXISTS = "数据已存在";
String ENTITY_NONE_EXISTS = "数据不存在";
String ENTITY_MULTIPLE_EXISTS = "数据存在多条";
String UPDATE_DATA_NOT_EXIST = "操作失败,请检查数据";
}

View File

@@ -0,0 +1,10 @@
package com.senmesh.common.constant;
public interface ExceptionRecordConstants {
/**
* 接口记录系统类型
*/
String SOURCE_HYG = "1";
String SOURCE_XINPAI = "2";
}

View File

@@ -0,0 +1,48 @@
package com.senmesh.common.constant;
/**
* 性别枚举
*
*/
public enum GenderEnum {
MALE(1, "男"),
FEMALE(2, "女"),
OTHER(3, "其它");
private final Integer type;
private final String desc;
GenderEnum(Integer type, String desc) {
this.type = type;
this.desc = desc;
}
public Integer getType() {
return type;
}
public String getDesc() {
return desc;
}
public static Integer formatFemale(Integer value){
// 通过sql 身份证号处理性别条件:0女,1男
if(GenderEnum.FEMALE.getType().equals(value)){
return 0;
}
return value;
}
public static String getVal(Integer val){
if(GenderEnum.MALE.getType().equals(val)){
return GenderEnum.MALE.desc;
} else if(GenderEnum.FEMALE.getType().equals(val)){
return GenderEnum.FEMALE.desc;
}
return GenderEnum.OTHER.desc;
}
}

View File

@@ -0,0 +1,30 @@
package com.senmesh.common.constant;
/**
* 项目环境枚举
* @author sr on 2020-05-20
*/
public enum ProjectProfileEnum {
/** 开发 **/
DEV("dev"),
/** 测试 **/
TEST("test"),
/** 测试 * */
UAT("uat"),
/** 生产 **/
PROD("prod"),
/** 福安 **/
FUAN("fuan");
private String value;
ProjectProfileEnum(String value){
this.value = value;
}
public String getValue() {
return this.value;
}
}

View File

@@ -0,0 +1,69 @@
package com.senmesh.common.constant;
import java.util.Arrays;
import java.util.Objects;
/**
* 交易账户类型常量
* @author sr on 2020-08-19
*/
public interface TradeAccountTypeConstants {
/** 交易账号类型:银行卡 **/
String TRADE_ACCOUNT_TYPE_BANK = TradeAccountTypeEnum.BANK.getType().toString();
/** 交易账号类型:子账号 **/
String TRADE_ACCOUNT_TYPE_SUB_ACCOUNT = TradeAccountTypeEnum.SUB_ACCOUNT.getType().toString();
/** 交易账号类型:支付宝账号 **/
String TRADE_ACCOUNT_TYPE_ALIPAY_ACCOUNT = TradeAccountTypeEnum.ALIPAY_ACCOUNT.getType().toString();
/** 交易账号类型:支付宝用户ID **/
String TRADE_ACCOUNT_TYPE_ALIPAY_USERID = TradeAccountTypeEnum.ALIPAY_USERID.getType().toString();
/** 交易账号类型:微信OPENID **/
String TRADE_ACCOUNT_TYPE_WECHAT_OPENID = TradeAccountTypeEnum.WECHAT_OPENID.getType().toString();
/** 交易账号类型:平台机构 **/
String TRADE_ACCOUNT_TYPE_PLATFORM_ORG = TradeAccountTypeEnum.PLATFORM_ORG.getType().toString();
/**
* 交易账号类型:其他
*/
String TRADE_ACCOUNT_TYPE_OTHER = TradeAccountTypeEnum.OTHER.getType().toString();
/**
* 帐户类型枚举
*
* @author Carlye
* @date 2021/08/17
*/
public enum TradeAccountTypeEnum{
BANK(1,"银行卡"),
SUB_ACCOUNT(2,"内部子账户"),
ALIPAY_ACCOUNT(3,"支付宝账号"),
ALIPAY_USERID(4,"支付宝用户ID"),
WECHAT_OPENID(5,"微信支付"),
PLATFORM_ORG(6,"平台机构"),
OTHER(99,"其他");
private final Integer type;
private final String desc;
TradeAccountTypeEnum(Integer type,String desc){
this.type = type;
this.desc = desc;
}
public Integer getType() {
return type;
}
public String getDesc() {
return desc;
}
public static TradeAccountTypeEnum find(Integer type){
if (Objects.isNull(type)){
return null;
}
return Arrays.stream(TradeAccountTypeEnum.values()).filter(e->e.getType().equals(type)).findFirst().orElse(null);
}
}
}

View File

@@ -0,0 +1,41 @@
package com.senmesh.common.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import com.senmesh.common.AlitaResult;
import com.senmesh.common.util.JsonUtils;
import com.senmesh.common.util.RequestUtils;
import com.senmesh.commonexport.service.ExportService;
import com.senmesh.usercenter.user.dto.UserDTO;
@RestController
public class TestController {
@Autowired
private ExportService exportService;
@GetMapping("anon/hostIp")
public AlitaResult<String> hostIp() {
return AlitaResult.success(RequestUtils.getHostIp());
}
@GetMapping("anon/trim")
public AlitaResult<String> gettrim(UserDTO userDTO) {
return AlitaResult.success(JsonUtils.toJsonString(userDTO));
}
@PostMapping("anon/trim")
public AlitaResult<String> posttrim(@RequestBody UserDTO userDTO) {
return AlitaResult.success(JsonUtils.toJsonString(userDTO));
}
}

View File

@@ -0,0 +1,56 @@
package com.senmesh.common.dto;
import java.io.Serializable;
import java.util.Date;
import javax.validation.constraints.NotNull;
import org.springframework.format.annotation.DateTimeFormat;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonFormat.Shape;
import com.senmesh.common.validation.Update;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.SuperBuilder;
/**
* 基础dto
*
* @author sr on 2019-12-07
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@SuperBuilder
public class BaseDTO implements Serializable,IBaseDTO {
/** **/
private static final long serialVersionUID = 3305990673475531178L;
@ApiModelProperty("id")
@NotNull(message = "id不能为空", groups = {Update.class})
private Long id;
@ApiModelProperty("是否逻辑删除(0否1是)")
private Boolean deleted;
@ApiModelProperty("创建人")
private Long createBy;
@ApiModelProperty("创建时间")
// requestparam
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
// requestbody+serial
@JsonFormat(shape = Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date createTime;
@ApiModelProperty("修改人")
private Long updateBy;
@ApiModelProperty("修改时间")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@JsonFormat(shape = Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date updateTime;
private Long proxyBy;
}

View File

@@ -0,0 +1,41 @@
package com.senmesh.common.dto;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.senmesh.common.util.UserContextHolder;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
/**
* 基础操作对象
*
* @author sr on 2020-08-26
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class BaseOperateDTO implements Serializable, IOperateDTO{
/** **/
private static final long serialVersionUID = -3075506470200622050L;
/** 操作机构ID **/
@JsonIgnore
private Long operateOrgId;
/** 操作用户ID **/
@JsonIgnore
private Long operateUserId;
public Long getOperateOrgId() {
return operateOrgId == null ? UserContextHolder.getLoginOrgId() : operateOrgId;
}
public Long getOperateUserId() {
return operateUserId == null ? UserContextHolder.getLoginUserId() : operateUserId;
}
}

View File

@@ -0,0 +1,13 @@
package com.senmesh.common.dto;
import java.io.Serializable;
/**
* @author sr on 2020-04-22
*/
public class ButtonDTO implements Serializable{
/** **/
private static final long serialVersionUID = -8606784819452806185L;
}

View File

@@ -0,0 +1,27 @@
package com.senmesh.common.dto;
import java.io.Serializable;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class CasTokenDTO implements Serializable{
/** **/
private static final long serialVersionUID = -6818144396647084889L;
private String token;
/** 当前token用户id **/
private Long userId;
/** 是否新注册 **/
private Boolean register;
}

View File

@@ -0,0 +1,37 @@
package com.senmesh.common.dto;
import java.io.Serializable;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Builder.Default;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* @author sr on 2020-04-22
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class ColumnDTO implements Serializable{
/** **/
private static final long serialVersionUID = 1L;
private static final int DEFAULT_WIDTH = 160;
/** 标题,如:总条数 **/
private String title;
/** 下标(同key),如:totalCount **/
private String dataIndex;
/** 属性,如:totalCount **/
private String key;
/** 宽度 **/
@Default
private int width = DEFAULT_WIDTH;
/** 对齐方式 **/
@Default
private String align = "center";
}

View File

@@ -0,0 +1,32 @@
package com.senmesh.common.dto;
import java.io.Serializable;
import java.util.List;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 展示定制的组件
*
* @author sr on 2020-04-22
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class DisplayedComponentDTO<T> implements Serializable{
/** **/
private static final long serialVersionUID = 1L;
/** 消息 **/
private MessageDTO message;
/** 列配置 **/
private List<ColumnDTO> columns;
/** 展示数据 **/
private List<T> data;
/** 按钮 **/
private List<ButtonDTO> buttons;
}

View File

@@ -0,0 +1,31 @@
package com.senmesh.common.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ExceptionRecordDTO implements Serializable {
/**
* 入参json
*/
private String paramJson;
/**
* 响应json
*/
private String resultJson;
/**
* 调用接口地址
*/
private String url;
/**
* 系统 1 慧用工、2、3、4待定
*/
private String sys;
}

View File

@@ -0,0 +1,27 @@
package com.senmesh.common.dto;
import java.io.Serializable;
import java.util.List;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@AllArgsConstructor
@NoArgsConstructor
@Data
@Builder
public class ExportHeadDetailInfoDTO implements Serializable {
private static final long serialVersionUID = 2399902503017038944L;
/**
* 表头
*/
private String head;
/**
* 下拉列表
*/
private List<String> explicitList;
}

View File

@@ -0,0 +1,59 @@
package com.senmesh.common.dto;
import org.apache.commons.compress.utils.Lists;
import java.io.Serializable;
import java.util.List;
import java.util.Objects;
import cn.hutool.core.date.DateUtil;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@AllArgsConstructor
@NoArgsConstructor
@Data
@Builder
public class ExportHeadInfoDTO implements Serializable {
private static final long serialVersionUID = -3123297330700623056L;
/**
* 文件名称
*/
private String fileName;
/**
* 表头
*/
private List<ExportHeadDetailInfoDTO> heads;
/**
* 数据列
*/
private List<List> datas;
public String getFileName() {
if (Objects.isNull(fileName)){
fileName = DateUtil.today() + "_动态模板";
}
return fileName;
}
public List<ExportHeadDetailInfoDTO> getHeads() {
if (Objects.isNull(heads)){
heads = Lists.newArrayList();
}
return heads;
}
public List<List> getDatas() {
if (Objects.isNull(datas)){
datas = Lists.newArrayList();
}
return datas;
}
}

View File

@@ -0,0 +1,12 @@
package com.senmesh.common.dto;
/**
* 基础对象
* @author sr on 2024-04-18
*/
public interface IBaseDTO {
Long getId();
}

View File

@@ -0,0 +1,23 @@
package com.senmesh.common.dto;
/**
* 展示信息
*
* @author sr on 2020-07-14
*/
public interface IDisplayed{
/**
* 是否展示
* @return
* @author sr on 2020-07-14
*/
Boolean getDisplayed();
/**
* 展示组件
* @return
* @author sr on 2020-07-14
*/
DisplayedComponentDTO<?> getDisplayedComponentDto();
}

View File

@@ -0,0 +1,26 @@
package com.senmesh.common.dto;
/**
* 操作对象
*
* @author sr on 2020-08-26
*/
public interface IOperateDTO {
/**
* 设置操作机构id
*
* @param orgId
* @author sr on 2020-08-31
*/
void setOperateOrgId(Long orgId);
/**
* 设置操作用户id
*
* @param userId
* @author sr on 2020-08-31
*/
void setOperateUserId(Long userId);
}

View File

@@ -0,0 +1,43 @@
package com.senmesh.common.dto;
import java.util.List;
/**
* 分页对象
*
* @author sr on 2020-08-26
*/
public interface IPage<T> {
/**
* 第几页
*
* @return
* @author sr on 2020-08-26
*/
Integer getPageNo();
/**
* 页宽
*
* @return
* @author sr on 2020-08-26
*/
Integer getPageSize();
/**
* 获取是否需要分页属性
*
* @return {@link Boolean}
*/
Boolean getNeedPage();
/**
* 设置分页结果
*
* @author sr on 2020-08-26
*/
void setPageResult(int totalCount, List<T> results);
}

View File

@@ -0,0 +1,28 @@
package com.senmesh.common.dto;
import java.io.Serializable;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.Builder.Default;
/**
* @author sr on 2020-04-22
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class MessageDTO implements Serializable{
/** **/
private static final long serialVersionUID = 3623225038987713421L;
/** 类型:success/info/warning/error **/
@Default
private String type = "info";
/** 内容 **/
private String content;
}

View File

@@ -0,0 +1,24 @@
package com.senmesh.common.dto;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import lombok.experimental.SuperBuilder;
/**
* 机构基础
*
* @author sr on 2019-12-22
*/
@Data
@EqualsAndHashCode(callSuper = true)
@NoArgsConstructor
@SuperBuilder
public class OrgBaseDTO extends BaseDTO {
/** **/
private static final long serialVersionUID = -2595043832455614748L;
private Long orgId;
}

View File

@@ -0,0 +1,98 @@
package com.senmesh.common.dto;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.senmesh.tkmybatis.pojo.Page;
import lombok.AllArgsConstructor;
import lombok.NoArgsConstructor;
import java.io.Serializable;
import java.util.List;
import java.util.Optional;
/**
* 分页查询对象
*
* @author sr on 2020-08-03
*/
@AllArgsConstructor
@NoArgsConstructor
public class PageQueryDTO<T> extends BaseOperateDTO implements Serializable, IPage<T> {
/**
*
**/
private static final long serialVersionUID = -6370476082571087349L;
/**
* 第几页
**/
private Integer pageNo = 1;
/**
* 页宽
**/
private Integer pageSize = 20;
/**
* 是否需要分页
**/
private Boolean needPage = Boolean.TRUE;
/**
* 分页结果
**/
@JsonIgnore
private Page<T> pageResult;
@Override
public Integer getPageNo() {
return Optional.ofNullable(pageNo).orElse(1);
}
@Override
public Integer getPageSize() {
return Optional.ofNullable(pageSize).orElse(20);
}
@Override
public Boolean getNeedPage() {
return Optional.ofNullable(this.needPage).orElse(Boolean.TRUE);
}
@Override
public void setPageResult(int totalCount, List<T> results) {
pageResult = new Page<T>(getPageNo(), getPageSize());
pageResult.setPageResult(totalCount, results);
}
public void setPageNo(Integer pageNo) {
this.pageNo = pageNo;
}
public void setPageSize(Integer pageSize) {
if (pageSize != null && pageSize >1000) {
this.pageSize = 1000;
} else {
this.pageSize = pageSize;
}
}
public void setNeedPage(Boolean needPage) {
this.needPage = needPage;
}
public Page<T> getPageResult() {
return pageResult;
}
public void setPageResult(Page<T> pageResult) {
this.pageResult = pageResult;
}
public PageQueryDTO<T> disablePage() {
this.setNeedPage(Boolean.FALSE);
return this;
}
public Integer getStartPage() {
return (getPageNo() - 1) * getPageSize();
}
}

View File

@@ -0,0 +1,156 @@
package com.senmesh.common.easyexcel;
import com.google.common.collect.Maps;
import com.alibaba.excel.metadata.Head;
import com.alibaba.excel.metadata.data.WriteCellData;
import com.alibaba.excel.write.handler.AbstractCellWriteHandler;
import com.alibaba.excel.write.metadata.holder.WriteSheetHolder;
import com.alibaba.excel.write.metadata.holder.WriteTableHolder;
import com.senmesh.common.dto.ExportHeadDetailInfoDTO;
import com.senmesh.common.dto.ExportHeadInfoDTO;
import org.apache.poi.hssf.util.HSSFColor;
import org.apache.poi.ss.usermodel.BorderStyle;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.DataValidation;
import org.apache.poi.ss.usermodel.DataValidationConstraint;
import org.apache.poi.ss.usermodel.DataValidationHelper;
import org.apache.poi.ss.usermodel.FillPatternType;
import org.apache.poi.ss.usermodel.Font;
import org.apache.poi.ss.usermodel.HorizontalAlignment;
import org.apache.poi.ss.usermodel.IndexedColors;
import org.apache.poi.ss.usermodel.Name;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.VerticalAlignment;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.ss.util.CellRangeAddressList;
import org.apache.poi.xssf.usermodel.XSSFCellStyle;
import org.apache.poi.xssf.usermodel.XSSFColor;
import org.apache.poi.xssf.usermodel.XSSFDataValidation;
import java.awt.*;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import cn.hutool.core.collection.CollUtil;
public class DynamicExportTemplateCellWriteHandler extends AbstractCellWriteHandler {
private Map<Integer, ExportHeadDetailInfoDTO> exportHeadIndexMap = null;
private Integer hiddenSheetIndex = 1;
@Override
public void afterCellDispose(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, List<WriteCellData<?>> cellDataList, Cell cell, Head head, Integer relativeRowIndex, Boolean isHead) {
super.afterCellDispose(writeSheetHolder, writeTableHolder, cellDataList, cell, head, relativeRowIndex, isHead);
int rowIndex = cell.getRowIndex();
int columnIndex = cell.getColumnIndex();
Workbook workbook = writeSheetHolder.getParentWriteWorkbookHolder().getWorkbook();
Sheet sheet = writeSheetHolder.getSheet();
if (rowIndex == 0) {
Row row = cell.getRow();
row.setHeight((short) (33 * 20));
ExportHeadInfoDTO exportInfo = (ExportHeadInfoDTO) ImportDataContext.get("exportInfo");
XSSFCellStyle cellStyle = (XSSFCellStyle) workbook.createCellStyle();
cellStyle.setBorderBottom(BorderStyle.THIN); //下边框
cellStyle.setBottomBorderColor(IndexedColors.BLACK.index);
cellStyle.setBorderLeft(BorderStyle.THIN);//左边框
cellStyle.setLeftBorderColor(IndexedColors.BLACK.index);
cellStyle.setBorderTop(BorderStyle.THIN);//上边框
cellStyle.setTopBorderColor(IndexedColors.BLACK.index);
cellStyle.setBorderRight(BorderStyle.THIN);//右边框
cellStyle.setRightBorderColor(IndexedColors.BLACK.index);
cellStyle.setAlignment(HorizontalAlignment.CENTER);
cellStyle.setVerticalAlignment(VerticalAlignment.CENTER);
Color color = new Color(91, 155, 213);
cellStyle.setFillForegroundColor(new XSSFColor(color,null));
cellStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND);
Font font = workbook.createFont();
cellStyle.setFont(font);
font.setFontName("Calibri");
font.setColor(HSSFColor.HSSFColorPredefined.WHITE.getIndex());
cell.setCellStyle(cellStyle);
if (Objects.isNull(exportHeadIndexMap)) {
List<ExportHeadDetailInfoDTO> heads = exportInfo.getHeads();
exportHeadIndexMap = Maps.newHashMap();
for (int i = 0; i < heads.size(); i++) {
ExportHeadDetailInfoDTO exportHead = heads.get(i);
exportHeadIndexMap.put(i, exportHead);
}
}
ExportHeadDetailInfoDTO exportHeadDetailInfoDTO = exportHeadIndexMap.get(columnIndex);
String name;
if (Objects.isNull(exportHeadDetailInfoDTO)) {
name = cell.getStringCellValue();
} else {
name = exportHeadDetailInfoDTO.getHead();
}
int length = ((name.getBytes(StandardCharsets.UTF_8).length + cell.toString().length()) * 3) / 5 + 5;
sheet.setColumnWidth(columnIndex, length * 290);
List<String> explicitList = Objects.isNull(exportHeadDetailInfoDTO) ? null : exportHeadDetailInfoDTO.getExplicitList();
// 设置下拉属性
// if (CollUtil.isNotEmpty(explicitList)) {
// DataValidationHelper helper = sheet.getDataValidationHelper();
// DataValidationConstraint constraint = helper.createExplicitListConstraint(explicitList.toArray(new String[]{}));
// //设置生效的起始行、终止行、起始列、终止列
// CellRangeAddressList cellAddressList = new CellRangeAddressList(1,
// 65535, columnIndex, columnIndex);
// DataValidation validation = helper.createValidation(constraint, cellAddressList);
// //适配xls和xlsx
// if (validation instanceof HSSFDataValidation) {
// validation.setSuppressDropDownArrow(false);
// } else {
// validation.setSuppressDropDownArrow(true);
// validation.setShowErrorBox(true);
// }
// sheet.addValidationData(validation);
// }
if (CollUtil.isNotEmpty(explicitList)){
String hiddenName = "hidden" + hiddenSheetIndex;
//1.创建隐藏的sheet页。 起个名字吧!叫"hidden"!
Sheet hiddenSheet = workbook.createSheet(hiddenName);
//2.循环赋值(为了防止下拉框的行数与隐藏域的行数相对应,将隐藏域加到结束行之后)
for (int i = 0; i < explicitList.size(); i++) {
hiddenSheet.createRow(65535 + i).createCell(columnIndex).setCellValue(explicitList.get(i));
}
Name category1Name = workbook.createName();
category1Name.setNameName(hiddenName);
//3 A1:A代表隐藏域创建第N列createCell(N)时。以A1列开始A行数据获取下拉数组
category1Name.setRefersToFormula(hiddenName + "!A1:A" + (explicitList.size() + 65535));
//
DataValidationHelper helper = sheet.getDataValidationHelper();
DataValidationConstraint constraint = helper.createFormulaListConstraint(hiddenName);
CellRangeAddressList addressList = new CellRangeAddressList(1, 65535, columnIndex, columnIndex);
DataValidation dataValidation = helper.createValidation(constraint, addressList);
if (dataValidation instanceof XSSFDataValidation) {
// 数据校验
dataValidation.setSuppressDropDownArrow(true);
dataValidation.setShowErrorBox(true);
} else {
dataValidation.setSuppressDropDownArrow(false);
}
// 作用在目标sheet上
sheet.addValidationData(dataValidation);
// 设置hiddenSheet隐藏
workbook.setSheetHidden(hiddenSheetIndex, true);
hiddenSheetIndex++;
}
}
}
}

View File

@@ -0,0 +1,77 @@
package com.senmesh.common.easyexcel;
import com.google.common.collect.Lists;
import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.ExcelWriter;
import com.senmesh.common.dto.ExportHeadDetailInfoDTO;
import com.senmesh.common.dto.ExportHeadInfoDTO;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import java.io.IOException;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
import javax.servlet.http.HttpServletResponse;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
public class ExcelUtil {
private ExcelUtil() {
}
/**
* 动态导出excel模板
*
* @param reqDto 点播dto
*/
public static void dynamicExportTemplate(ExportHeadInfoDTO reqDto) {
if (CollUtil.isEmpty(reqDto.getHeads())) {
return;
}
ImportDataContext.set("exportInfo", reqDto);
ServletRequestAttributes attr = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletResponse response = attr.getResponse();
if (Objects.isNull(response)) {
return;
}
String fileName = reqDto.getFileName() + ".xlsx";
response.setContentType("application/vnd.ms-excel;charset=UTF-8");
response.setCharacterEncoding("UTF-8");
String fileNamePrefix = StrUtil.subBefore(fileName, ".", true);
String fileNameSuffix = StrUtil.subAfter(fileName, ".", true);
try {
String fileNamePrefix2 = URLEncoder.encode(fileNamePrefix, "UTF-8").replaceAll("\\+", "%20");
response.setHeader("Content-Disposition", "attachment;" + fileNamePrefix2 + "." + fileNameSuffix);
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
throw new IllegalArgumentException("下载文件异常");
}
response.addHeader("Param", "no-cache");
response.addHeader("Cache-Control", "no-cache");
try (OutputStream out = response.getOutputStream();) {
ExcelWriter writer = EasyExcel.write(out).registerWriteHandler(new DynamicExportTemplateCellWriteHandler()).build();
List<List> objects = Lists.newArrayList();
objects.add(reqDto.getHeads().stream().map(ExportHeadDetailInfoDTO::getHead).collect(Collectors.toList()));
objects.addAll(reqDto.getDatas());
writer.write(objects, EasyExcel.writerSheet("sheet").build());
writer.finish();
} catch (IOException e) {
e.printStackTrace();
}
}
}

View File

@@ -0,0 +1,35 @@
package com.senmesh.common.easyexcel;
import com.google.common.collect.Maps;
import java.util.Map;
import java.util.Objects;
public class ImportDataContext {
public static final ThreadLocal<Map<String, Object>> THREAD_LOCAL_MAP = new ThreadLocal<>();
public static Object get(String key) {
Map<String, Object> stringObjectMap = THREAD_LOCAL_MAP.get();
if (Objects.isNull(stringObjectMap)) {
THREAD_LOCAL_MAP.set(Maps.newHashMap());
return null;
}
return stringObjectMap.get(key);
}
public static void set(String key,Object value) {
Map<String, Object> stringObjectMap = THREAD_LOCAL_MAP.get();
if (Objects.isNull(stringObjectMap)) {
THREAD_LOCAL_MAP.set(Maps.newHashMap());
}
THREAD_LOCAL_MAP.get().put(key,value);
}
public static void putAll(Map<String, Object> extraInfoMap){
Map<String, Object> stringObjectMap = THREAD_LOCAL_MAP.get();
if (Objects.isNull(stringObjectMap)) {
THREAD_LOCAL_MAP.set(Maps.newHashMap());
}
THREAD_LOCAL_MAP.get().putAll(extraInfoMap);
}
}

View File

@@ -0,0 +1,64 @@
package com.senmesh.common.entity;
import java.io.Serializable;
import java.util.Date;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.senmesh.common.dto.IBaseDTO;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.SuperBuilder;
@Data
@NoArgsConstructor
@AllArgsConstructor
@SuperBuilder
public abstract class BaseEntity implements Serializable, Cloneable, IBaseDTO {
private static final long serialVersionUID = -723338960662087915L;
/**
* 主键
*/
@TableId(value = "id", type = IdType.AUTO)
private Long id;
/**
* 是否逻辑删除(0否1是)
*/
@TableField(value = "deleted", fill = FieldFill.INSERT)
@Builder.Default
private Boolean deleted = Boolean.FALSE;
/**
* 创建人
*/
@TableField(value = "create_by", fill = FieldFill.INSERT)
private Long createBy;
/**
* 创建时间
*/
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
@TableField(value = "create_time", fill = FieldFill.INSERT)
private Date createTime;
/**
* 修改人
*/
@TableField(value = "update_by", fill = FieldFill.UPDATE)
private Long updateBy;
/**
* 修改时间
*/
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
@TableField(value = "update_time", fill = FieldFill.UPDATE)
private Date updateTime;
/**
* 代理人
*/
private Long proxyBy;
}

View File

@@ -0,0 +1,27 @@
package com.senmesh.common.entity;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import lombok.experimental.SuperBuilder;
/**
* 机构基础实体
* @author sr on 2019-10-19
*/
@Data
@EqualsAndHashCode(callSuper = true)
@NoArgsConstructor
@AllArgsConstructor
@SuperBuilder
public class OrgBaseEntity extends BaseEntity {
/** **/
private static final long serialVersionUID = 2559571969035017003L;
/** 机构id **/
private Long orgId;
}

View File

@@ -0,0 +1,30 @@
package com.senmesh.common.exception;
/**
* 项目所有自定义异常父类
*
* @author guhong
*/
public class AlitaException extends RuntimeException {
/**
*
*/
private static final long serialVersionUID = 3121871576733025786L;
public AlitaException() {}
public AlitaException(String message) {
super(message);
}
public AlitaException(String message, Throwable cause) {
super(message, cause);
}
public AlitaException(Throwable cause) {
super(cause);
}
}

View File

@@ -0,0 +1,30 @@
package com.senmesh.common.exception;
/**
* 业务异常
*
* @author guhong
* @date 2019/8/19
*/
public class BusinessException extends AlitaException {
/**
*
*/
private static final long serialVersionUID = -3097174055942519169L;
public BusinessException() {}
public BusinessException(String message) {
super(message);
}
public BusinessException(String message, Throwable cause) {
super(message, cause);
}
public BusinessException(Throwable cause) {
super(cause);
}
}

View File

@@ -0,0 +1,24 @@
package com.senmesh.common.exception;
import com.senmesh.common.AlitaResult;
import com.senmesh.common.dto.CasTokenDTO;
/**
* 更新token
* @author sr on 2021-07-05
*/
public class ChangeTokenException extends WithResultException{
/** **/
private static final long serialVersionUID = -8897274199463229506L;
public ChangeTokenException(CasTokenDTO casTokenDTO) {
super(AlitaResult.changeToken(casTokenDTO));
}
public CasTokenDTO getCasTokenDTO(){
return (CasTokenDTO) getResult().getErrorData();
}
}

View File

@@ -0,0 +1,24 @@
package com.senmesh.common.exception;
import com.senmesh.common.exception.util.DevopsUtils;
/**
* 运维业务异常
* @author sr on 2022-03-01
*/
public class DevopsException extends BusinessException{
/** **/
private static final long serialVersionUID = 1L;
public DevopsException(String message) {
super(message);
DevopsUtils.deal(message);
}
public DevopsException(String message, Throwable cause) {
super(message, cause);
DevopsUtils.deal(message);
}
}

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