mirror of
https://github.com/chatopera/cosin.git
synced 2025-08-01 16:38:02 +08:00
Merge remote-tracking branch 'cskefu-backend' into v9
This commit is contained in:
parent
19badce8fb
commit
91bcca21f6
@ -12,7 +12,7 @@ jobs:
|
||||
# Be sure to update the Docker image tag below to openjdk version of your application.
|
||||
# A list of available CircleCI Docker Convenience Images are available here: https://circleci.com/developer/images/image/cimg/openjdk
|
||||
docker:
|
||||
- image: cimg/openjdk:17.0.7
|
||||
- image: cimg/openjdk:21
|
||||
# Add steps to the job
|
||||
# See: https://circleci.com/docs/configuration-reference/#steps
|
||||
steps:
|
||||
@ -25,10 +25,7 @@ jobs:
|
||||
echo "$DOCKERHUB_USERPASS" | docker login -u "$DOCKERHUB_USERNAME" --password-stdin
|
||||
- run:
|
||||
name: Build Contact Center Docker Image
|
||||
command: cd $CIRCLE_WORKING_DIRECTORY/contact-center && ./admin/build.sh
|
||||
- run:
|
||||
name: Push Contact Center Docker Image to DockerHub
|
||||
command: cd $CIRCLE_WORKING_DIRECTORY/contact-center && ./admin/push.sh
|
||||
command: cd $CIRCLE_WORKING_DIRECTORY/cskefu-backend && ../scripts/deploy.sh
|
||||
|
||||
# Invoke jobs via workflows
|
||||
# See: https://circleci.com/docs/configuration-reference/#workflows
|
||||
|
30
.gitee/ISSUE_TEMPLATE/01_help.yml
Normal file
30
.gitee/ISSUE_TEMPLATE/01_help.yml
Normal file
@ -0,0 +1,30 @@
|
||||
name: 求助
|
||||
description: 开发环境搭建、功能咨询和使用问题等
|
||||
labels: ["help-wanted"]
|
||||
assignees:
|
||||
- zhangchanglong2021
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: "## 描述"
|
||||
- type: textarea
|
||||
id: content1
|
||||
attributes:
|
||||
label: "详细描述问题后优先处理解决! 截图、错误日志等"
|
||||
value: |
|
||||
1. 针对某功能,需要提供详细描述文档
|
||||
2. xxx
|
||||
3. xxx
|
||||
...
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: content2
|
||||
attributes:
|
||||
label: "代码版本"
|
||||
value: |
|
||||
1. Git commit hash (`git rev-parse HEAD`),进入代码库并执行
|
||||
或
|
||||
2. v7, v8, v9 ...
|
||||
validations:
|
||||
required: true
|
30
.gitee/ISSUE_TEMPLATE/02_bug.yml
Normal file
30
.gitee/ISSUE_TEMPLATE/02_bug.yml
Normal file
@ -0,0 +1,30 @@
|
||||
name: 软件缺陷
|
||||
description: 报告软件缺陷
|
||||
labels: ["bug"]
|
||||
assignees:
|
||||
- kaifuny
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: "## 描述"
|
||||
- type: textarea
|
||||
id: content1
|
||||
attributes:
|
||||
label: "详细描述问题后优先处理解决! 截图、错误日志等"
|
||||
value: |
|
||||
1. 如何重现
|
||||
2. xxx
|
||||
3. xxx
|
||||
...
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: content2
|
||||
attributes:
|
||||
label: "代码版本"
|
||||
value: |
|
||||
1. Git commit hash (`git rev-parse HEAD`),进入代码库并执行
|
||||
或
|
||||
2. v7, v8, v9 ...
|
||||
validations:
|
||||
required: true
|
30
.gitee/ISSUE_TEMPLATE/03_requirement.yml
Normal file
30
.gitee/ISSUE_TEMPLATE/03_requirement.yml
Normal file
@ -0,0 +1,30 @@
|
||||
name: 需求
|
||||
description: 增加新需求、反馈建议
|
||||
labels: ["requirement"]
|
||||
assignees:
|
||||
- kaifuny
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: "## 描述"
|
||||
- type: textarea
|
||||
id: content1
|
||||
attributes:
|
||||
label: "详细描述需求"
|
||||
value: |
|
||||
1. xxx 模块需要支持 xxx 功能
|
||||
2. xxx
|
||||
3. xxx
|
||||
...
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: content2
|
||||
attributes:
|
||||
label: "代码版本"
|
||||
value: |
|
||||
1. Git commit hash (`git rev-parse HEAD`),进入代码库并执行
|
||||
或
|
||||
2. v7, v8, v9 ...
|
||||
validations:
|
||||
required: true
|
30
.gitee/ISSUE_TEMPLATE/04_profiling.yml
Normal file
30
.gitee/ISSUE_TEMPLATE/04_profiling.yml
Normal file
@ -0,0 +1,30 @@
|
||||
name: 性能优化
|
||||
description: 瓶颈分析、性能优化建议和安全漏洞等
|
||||
labels: ["profiling"]
|
||||
assignees:
|
||||
- lecjy
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: "## 描述"
|
||||
- type: textarea
|
||||
id: content1
|
||||
attributes:
|
||||
label: "详细描述需求"
|
||||
value: |
|
||||
1. xxx 模块需要支持 xxx 功能
|
||||
2. xxx
|
||||
3. xxx
|
||||
...
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: content2
|
||||
attributes:
|
||||
label: "代码版本"
|
||||
value: |
|
||||
1. Git commit hash (`git rev-parse HEAD`),进入代码库并执行
|
||||
或
|
||||
2. v7, v8, v9 ...
|
||||
validations:
|
||||
required: true
|
35
.gitee/ISSUE_TEMPLATE/05_userstory.yml
Normal file
35
.gitee/ISSUE_TEMPLATE/05_userstory.yml
Normal file
@ -0,0 +1,35 @@
|
||||
name: 用户故事
|
||||
description: 用户故事
|
||||
title: "[us] "
|
||||
labels: ["userstory"]
|
||||
assignees:
|
||||
- kaifuny
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: "## 描述"
|
||||
- type: textarea
|
||||
id: content1
|
||||
attributes:
|
||||
label: "详细描述需求"
|
||||
value: |
|
||||
## 用户主体
|
||||
|
||||
## 用户故事
|
||||
|
||||
## 交互流程
|
||||
|
||||
## 交互细节
|
||||
...
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: content2
|
||||
attributes:
|
||||
label: "代码版本"
|
||||
value: |
|
||||
1. Git commit hash (`git rev-parse HEAD`),进入代码库并执行
|
||||
或
|
||||
2. v7, v8, v9 ...
|
||||
validations:
|
||||
required: true
|
11
.gitee/ISSUE_TEMPLATE/config.yaml
Normal file
11
.gitee/ISSUE_TEMPLATE/config.yaml
Normal file
@ -0,0 +1,11 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: 文档中心
|
||||
url: https://docs.cskefu.com/
|
||||
about: 提供春松客服使用指南、教程、基本功能使用、介绍和常见问题解答
|
||||
- name: 春松客服大讲堂
|
||||
url: https://gitee.com/cskefu/cskefu-djt/blob/main/README.md
|
||||
about: 提供春松客服定制化开发技能课程
|
||||
- name: 商务洽谈
|
||||
url: https://www.chatopera.com/price.html
|
||||
about: 提供春松客服定制化开发、机器人客服平台等
|
@ -1,13 +1,21 @@
|
||||
---
|
||||
reviewers : cskefu/reviewers
|
||||
---
|
||||
|
||||
<!--- 在标题中简略说明问题 -->
|
||||
|
||||
## 描述
|
||||
<!--- 详细的描述变更 -->
|
||||
|
||||
### 关联 Issue #
|
||||
|
||||
## 解决的问题
|
||||
|
||||
<!--- 为什么变更是必要的? -->
|
||||
<!--- 如果这个PR解决了其他Issue,添加链接 -->
|
||||
|
||||
## 测试情况
|
||||
|
||||
<!--- 详细介绍怎么测试变更了 -->
|
||||
<!--- 介绍测试环境 -->
|
||||
<!--- 变更对其他代码的影响 -->
|
||||
@ -15,13 +23,17 @@
|
||||
## 截屏
|
||||
|
||||
## 变更的类型
|
||||
|
||||
<!--- 变更有哪些特点,添加 `x` 到下面的对应项目中: -->
|
||||
|
||||
- [ ] 解决 Bug
|
||||
- [ ] 新功能(不影响其他功能)
|
||||
- [ ] 对其他功能有影响
|
||||
|
||||
## 检查:
|
||||
## 检查
|
||||
|
||||
<!--- 检查下面,各项,添加 `x` 到下面的对应项目中: -->
|
||||
|
||||
- [ ] 我的变更和代码规范一致
|
||||
- [ ] 我的变更需要更新文档
|
||||
- [ ] 我已经更新了对应的文档
|
9
.gitee/PULL_REQUEST_TEMPLATE/documentation.md
Normal file
9
.gitee/PULL_REQUEST_TEMPLATE/documentation.md
Normal file
@ -0,0 +1,9 @@
|
||||
---
|
||||
reviewers : cskefu/reviewers
|
||||
---
|
||||
|
||||
### Requirements for Contributing Documentation
|
||||
|
||||
## 变更说明
|
||||
|
||||
### 关联 Issue #
|
9
.gitee/PULL_REQUEST_TEMPLATE/performance_improvement.md
Normal file
9
.gitee/PULL_REQUEST_TEMPLATE/performance_improvement.md
Normal file
@ -0,0 +1,9 @@
|
||||
---
|
||||
reviewers : cskefu/reviewers
|
||||
---
|
||||
|
||||
### Requirements for Contributing a Performance Improvement
|
||||
|
||||
## 性能提升
|
||||
|
||||
### 关联 Issue #
|
8
.github/CODEOWNERS
vendored
8
.github/CODEOWNERS
vendored
@ -1,7 +1,7 @@
|
||||
# https://github.com/cskefu/cskefu/issues/758
|
||||
|
||||
# defaults
|
||||
* @cskefu/reviewers
|
||||
* @lecjy
|
||||
|
||||
# Order is important; the last matching pattern takes the most
|
||||
# precedence. When someone opens a pull request that only
|
||||
@ -12,7 +12,7 @@
|
||||
*.pug @lecjy
|
||||
*.java @lecjy
|
||||
*.sql @lecjy
|
||||
pom.xml @lecjy
|
||||
pom.xml @hailiang-wang
|
||||
|
||||
docs/* @SAMZONG
|
||||
README* @SAMZONG
|
||||
docs/* @cskefu/reviewers
|
||||
README* @cskefu/reviewers
|
12
.github/ISSUE_TEMPLATE.md
vendored
12
.github/ISSUE_TEMPLATE.md
vendored
@ -1,12 +0,0 @@
|
||||
# 描述
|
||||
|
||||
## 现在行为
|
||||
|
||||
## 预期行为
|
||||
|
||||
# 解决方案
|
||||
|
||||
# 环境
|
||||
|
||||
* 代码版本:
|
||||
Git commit hash (`git rev-parse HEAD`)
|
3
.github/workflows/compile.yml
vendored
3
.github/workflows/compile.yml
vendored
@ -15,5 +15,4 @@ jobs:
|
||||
if [ ! -d .git ]; then git init; git config user.email "you@dummy.com"; git config user.name "dummy"; git add --all && git commit -q -m "Only fix mvn goals for github workflow"; fi
|
||||
if [ -f ~/.cskefu.rc ]; then source ~/.cskefu.rc; else echo "Not found ~/.cskefu.rc; find info with https://github.com/cskefu/cskefu/issues/688"; exit 1; fi
|
||||
java -version && mvn -version
|
||||
$GITHUB_WORKSPACE/public/plugins/scripts/install-all.sh
|
||||
cd $GITHUB_WORKSPACE/contact-center && ./admin/compile.sh
|
||||
cd $GITHUB_WORKSPACE/cskefu-backend && ../public/scripts/compile.sh
|
||||
|
19
.github/workflows/gitee.yml
vendored
19
.github/workflows/gitee.yml
vendored
@ -1,19 +0,0 @@
|
||||
name: gitee
|
||||
|
||||
on:
|
||||
- push
|
||||
- delete
|
||||
|
||||
jobs:
|
||||
sync:
|
||||
runs-on: ubuntu-latest
|
||||
name: Git Repo Sync
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: wangchucheng/git-repo-sync@v0.1.0
|
||||
with:
|
||||
target-url: https://gitee.com/cskefu/cskefu.git
|
||||
target-username: ${{ secrets.USERNAME }}
|
||||
target-token: ${{ secrets.ACCESS_TOKEN }}
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -25,3 +25,5 @@ docker-compose.custom.yml
|
||||
private/
|
||||
!cskefu-frontend/.vscode
|
||||
.tool-versions
|
||||
smart-doc/
|
||||
target/
|
@ -1,10 +1,8 @@
|
||||
# Contributing
|
||||
|
||||
成为春松客服贡献者
|
||||
|
||||
## 提交反馈
|
||||
|
||||
在[春松客服 GitHub Issues](https://github.com/cskefu/cskefu/issues)中,先搜索是否有重复的。然后进行补充或新建 Issue。
|
||||
在[春松客服 Issues](https://gitee.com/cskefu/cskefu/issues)中,先搜索是否有重复的。然后进行补充或新建 Issue。
|
||||
|
||||
## 提交代码
|
||||
|
||||
@ -15,25 +13,14 @@
|
||||
|
||||
春松客服文档中心的项目,也是开源的,地址在:
|
||||
|
||||
[https://github.com/cskefu/cskefu-docs](https://github.com/cskefu/cskefu-docs)
|
||||
[https://gitee.com/cskefu/docs](https://gitee.com/cskefu/docs)
|
||||
|
||||
春松客服文档 Markdown 文件路径:
|
||||
|
||||
[https://github.com/cskefu/cskefu-docs/tree/main/docs](https://github.com/cskefu/cskefu-docs/tree/main/docs)
|
||||
[https://gitee.com/cskefu/docs/tree/main/docs](https://gitee.com/cskefu/docs/tree/main/docs)
|
||||
|
||||
更新文档:
|
||||
|
||||
1)提交 PR 到[春松客服文档中心 GitHub 仓库](https://github.com/cskefu/cskefu-docs/tree/main/docs),一步到位。
|
||||
1)提交 PR 到[春松客服文档中心 Git 仓库](https://gitee.com/cskefu/docs/tree/main),一步到位。
|
||||
|
||||
2)提交 Issue 到[春松客服 Issues](https://github.com/cskefu/cskefu/issues/new), 并撰写文档内容,使用该方案则需要后续其他协作者提交到 [春松客服文档中心 GitHub 仓库](https://github.com/cskefu/cskefu-docs) 中。
|
||||
|
||||
## 期待您成为春松客服贡献者
|
||||
|
||||
不管是何种贡献,只有大小之分,而无本质区别。
|
||||
|
||||
一起贡献,一起好!
|
||||
没有贡献,都不好!
|
||||
|
||||
团结是共赢,分裂是全输。
|
||||
|
||||

|
||||
2)提交 Issue 到[春松客服 Issues](https://gitee.com/cskefu/cskefu/issues/new), 并撰写文档内容,使用该方案则需要后续其他协作者提交到 [春松客服文档中心 Git 仓库](https://gitee.com/cskefu/docs) 中。
|
||||
|
58
README.md
58
README.md
@ -31,18 +31,6 @@
|
||||
|
||||
新版本介绍:[观看春松客服 v8 新版本发布会 @ 2023-07-01](https://www.cskefu.com/2023/07/03/community-conf/)
|
||||
|
||||
## 媒体报道
|
||||
|
||||
<img src="./public/assets/cskefu-gpv-2.png" height = "220" div align=right />
|
||||
|
||||
- [春松客服:通过开源加云原生模式,大规模交付智能客服系统](https://www.cskefu.com/2022/04/11/cskefu-opensource-plus-cloud-model/)
|
||||
|
||||
- [春松客服荣获 GVP 企业级开源项目认证](http://www.ctiforum.com/news/guonei/578988.html)
|
||||
|
||||
- [Chatopera 王海良:做好开源客服系统 | OpenTEKr 专访](https://www.bilibili.com/video/BV1qF411p7hW)
|
||||
|
||||
---
|
||||
|
||||
## 开发者列表 ✨
|
||||
|
||||
:evergreen_tree: 春松客服是开源的智能客服系统,于 2018 年 9 月由 [Chatopera](https://www.chatopera.com) 发布,在开源社区协作中优化和完善,春松客服属于[春松客服开源社区](https://github.com/cskefu/cskefu#%E6%98%A5%E6%9D%BE%E5%AE%A2%E6%9C%8D%E5%BC%80%E6%BA%90%E7%A4%BE%E5%8C%BA)。
|
||||
@ -93,10 +81,12 @@
|
||||
<td align="center" valign="top" width="11.11%"><a href="https://samzong.me"><img src="https://avatars.githubusercontent.com/u/13782141?v=4?s=50" width="50px;" alt="Samzong Lu"/><br /><sub><b>Samzong Lu</b></sub></a><br /><a href="#eventOrganizing-SAMZONG" title="Event Organizing">📋</a> <a href="#projectManagement-SAMZONG" title="Project Management">📆</a> <a href="#design-SAMZONG" title="Design">🎨</a></td>
|
||||
<td align="center" valign="top" width="11.11%"><a href="https://github.com/halfray"><img src="https://avatars.githubusercontent.com/u/8181982?v=4?s=50" width="50px;" alt="halfray"/><br /><sub><b>halfray</b></sub></a><br /><a href="https://github.com/cskefu/cskefu/issues?q=author%3Ahalfray" title="Bug reports">🐛</a></td>
|
||||
<td align="center" valign="top" width="11.11%"><a href="https://github.com/kely33"><img src="https://avatars.githubusercontent.com/u/134681303?v=4?s=50" width="50px;" alt="kely33"/><br /><sub><b>kely33</b></sub></a><br /><a href="https://github.com/cskefu/cskefu/issues?q=author%3Akely33" title="Bug reports">🐛</a></td>
|
||||
<td align="center" valign="top" width="11.11%"><a href="https://github.com/lecjy"><img src="https://avatars.githubusercontent.com/u/9280760?v=4?s=50" width="50px;" alt="lecjy"/><br /><sub><b>lecjy</b></sub></a><br /></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
|
||||
<!-- markdownlint-restore -->
|
||||
<!-- prettier-ignore-end -->
|
||||
|
||||
@ -132,27 +122,35 @@
|
||||
|
||||
<p align="center">
|
||||
<b>欢迎页</b><br>
|
||||
<img src="https://static-public.chatopera.com/assets/images/cskefu/cskefu-screen-1.jpg" width="900">
|
||||
<img src="./public/assets/cskefu-screen-1.jpg" width="900">
|
||||
</p>
|
||||
|
||||
### 坐席工作台
|
||||
<details>
|
||||
<summary>展开查看更多产品截图</summary>
|
||||
<p>
|
||||
|
||||
登录演示环境,查看更多产品能力:[https://demo.cskefu.com/](https://demo.cskefu.com/)
|
||||
<p align="center">
|
||||
<b>坐席工作台</b><br>
|
||||
<img src="./public/assets/44915582-eb8d2c80-ad65-11e8-8876-86c8b5bb5cc7.png" width="900">
|
||||
</p>
|
||||
|
||||
| **登录账号** | **密码** | **角色** |
|
||||
| ------------ | --------- | -------------- |
|
||||
| admin | admin1234 | 系统超级管理员 |
|
||||
| zhangsan | agent1234 | 客服坐席人员 |
|
||||
<p align="center">
|
||||
<b>坐席监控</b><br>
|
||||
<img src="./public/assets/44915711-432b9800-ad66-11e8-899b-1ea02244925d.png" width="900">
|
||||
</p>
|
||||
|
||||
### 网页端访客示例
|
||||
<p align="center">
|
||||
<b>集成客服机器人</b><br>
|
||||
<img src="./public/assets/51080565-4b82df00-1719-11e9-8cc4-dbbec0459224.png" width="900">
|
||||
</p>
|
||||
|
||||
[https://demo.cskefu.com/testclient.html](http://demo.cskefu.com/testclient.html)
|
||||
<p align="center">
|
||||
<b>客服机器人应答</b><br>
|
||||
<img src="./public/assets/51080567-50479300-1719-11e9-85d8-d209370c9d10.png" width="900">
|
||||
</p>
|
||||
|
||||
- 登录张三后可接待访客,否则显示没有客服人员在线
|
||||
|
||||
### 机器人客服示例
|
||||
|
||||
[https://oh-my.cskefu.com/im/text/0nhckh.html](https://oh-my.cskefu.com/im/text/0nhckh.html)
|
||||
</p>
|
||||
</details>
|
||||
|
||||
## 快速开始
|
||||
|
||||
@ -208,13 +206,11 @@
|
||||
在春松客服开源社区,我们建立关系、发现认同、合作共赢!
|
||||
|
||||
- 了解春松客服采用的开源许可协议,参考[文档](https://www.cskefu.com/2023/06/25/chunsong-public-license-1-0/)
|
||||
- 了解春松客服的开发计划,参考[文档](https://chatopera.github.io/cskefu.roadmap/)
|
||||
- 加入开源社区运营,成为社区合伙人,参考[文档](https://mp.weixin.qq.com/s/TLE87YX4k097iOXnV4WVSw)
|
||||
- 加入春松客服开源社区,参考[文档](https://www.cskefu.com/join-us/)
|
||||
- 了解春松客服的开发计划,参考[文档](https://github.com/cskefu/cskefu/issues)
|
||||
- 如何发布春松客服人物志,向社区介绍自己,参考[文档](https://www.cskefu.com/join-us/)
|
||||
- 如何提交反馈、文档,参考[文档](./CONTRIBUTING.md)
|
||||
- 如何成为春松客服开发者,参考[文档](https://docs.cskefu.com/docs/osc/devonboard/)
|
||||
- 如何提交代码,参考[文档](https://docs.cskefu.com/docs/osc/contribution)
|
||||
- 如何最新的春松客服开发进展:订阅[春松客服邮件列表](https://lists.cskefu.com/cgi-bin/mailman/listinfo/dev)
|
||||
- 如何获得春松客服商业插件和服务,参考[文档](https://www.chatopera.com/price.html)
|
||||
|
||||
春松客服之所以开源,是基于这样一种信念:爱人也是爱己,利他也是利己。
|
||||
因春松客服受益,而不回报开源社区的用户,我们不欢迎使用春松客服:我们开源并不是为了你们,你们是不被祝福的。
|
||||
|
@ -1,8 +0,0 @@
|
||||
app/target
|
||||
!app/target/*.war.original
|
||||
!app/target/*.war
|
||||
!app/target/*.jar.original
|
||||
!app/target/*.jar
|
||||
logs/
|
||||
tmp/
|
||||
data/
|
23
contact-center/.gitignore
vendored
23
contact-center/.gitignore
vendored
@ -1,23 +0,0 @@
|
||||
*.swp
|
||||
*.swo
|
||||
*.sublime-*
|
||||
*.pyc
|
||||
jmeter.log
|
||||
__pycache__
|
||||
tmp/
|
||||
node_modules/
|
||||
sftp-config.json
|
||||
.DS_Store
|
||||
*.iml
|
||||
*.ipr
|
||||
*.iws
|
||||
*.idea
|
||||
~$*.xls*
|
||||
~$*.ppt*
|
||||
~$*.doc*
|
||||
admin/localrc
|
||||
app/target/
|
||||
app/.classpath
|
||||
app/.project
|
||||
app/.settings/
|
||||
logPath_IS_UNDEFINED/
|
@ -1,30 +0,0 @@
|
||||
FROM chatopera/java:17
|
||||
MAINTAINER Hai Liang Wang <hain@chatopera.com>
|
||||
|
||||
ARG DEBIAN_FRONTEND=noninteractive
|
||||
ARG VCS_REF
|
||||
ARG APPLICATION_CUSTOMER_ENTITY
|
||||
ARG APPLICATION_BUILD_DATESTR
|
||||
|
||||
ENV APPLICATION_CUSTOMER_ENTITY=$APPLICATION_CUSTOMER_ENTITY
|
||||
ENV APPLICATION_BUILD_DATESTR=$APPLICATION_BUILD_DATESTR
|
||||
|
||||
LABEL org.label-schema.vcs-ref=$VCS_REF \
|
||||
org.label-schema.vcs-url="https://www.cskefu.com"
|
||||
|
||||
# create dirs
|
||||
RUN /bin/bash -c "mkdir -p /{data,logs}"
|
||||
|
||||
# build WAR
|
||||
RUN mkdir -p /opt/cskefu
|
||||
COPY ./app/target/contact-center.war /opt/cskefu/contact-center.war
|
||||
COPY ./assets/mysql.setup.db.sh /opt/cskefu
|
||||
COPY ./assets/mysql.upgrade.db.sh /opt/cskefu
|
||||
COPY ./assets/utils.sh /opt/cskefu
|
||||
COPY ./assets/docker-entrypoint.sh /opt/cskefu
|
||||
RUN chmod +x /opt/cskefu/*.sh
|
||||
RUN touch /root/.cskefu.pep
|
||||
|
||||
WORKDIR /opt/cskefu
|
||||
EXPOSE 8030-8050
|
||||
CMD ["./docker-entrypoint.sh"]
|
@ -1,7 +0,0 @@
|
||||
# Chatopera Contact Center
|
||||
|
||||
前三代呼叫中心均是以电话为主要的服务渠道。在 2000 年,伴随着互联网以及移动通信的发展与普及,将电子邮件、互联网、手机短信等渠道接入呼叫中心,成为第四代呼叫中心的标志。第四代呼叫中心也称为多媒体呼叫中心或联络中心(Contact Center)。它相对传统呼叫中心来说接入渠道丰富,同时引入了多渠道接入与多渠道统一排队等概念。
|
||||
|
||||
## 文档
|
||||
|
||||
<https://docs.chatopera.com/>
|
@ -1,40 +0,0 @@
|
||||
#! /bin/bash
|
||||
###########################################
|
||||
#
|
||||
###########################################
|
||||
|
||||
# constants
|
||||
baseDir=$(cd `dirname "$0"`;pwd)
|
||||
appHome=$baseDir/..
|
||||
registryPrefix=
|
||||
imagename=cskefu/contact-center
|
||||
|
||||
# functions
|
||||
|
||||
# main
|
||||
[ -z "${BASH_SOURCE[0]}" -o "${BASH_SOURCE[0]}" = "$0" ] || return
|
||||
# build
|
||||
cd $appHome
|
||||
TIMESTAMP=`date "+%Y%m%d.%H%M%S"`
|
||||
PACKAGE_VERSION=`git rev-parse --short HEAD`
|
||||
APPLICATION_CUSTOMER_ENTITY=${APPLICATION_CUSTOMER_ENTITY:-"OpenSource Community"}
|
||||
|
||||
$baseDir/package.sh
|
||||
|
||||
if [ ! $? -eq 0 ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
set -x
|
||||
docker build --build-arg VCS_REF=$PACKAGE_VERSION \
|
||||
--build-arg APPLICATION_BUILD_DATESTR=$TIMESTAMP \
|
||||
--build-arg APPLICATION_CUSTOMER_ENTITY="$APPLICATION_CUSTOMER_ENTITY" \
|
||||
--no-cache \
|
||||
--force-rm=true --tag $registryPrefix$imagename:$PACKAGE_VERSION .
|
||||
|
||||
if [ $? -eq 0 ]; then
|
||||
docker tag $registryPrefix$imagename:$PACKAGE_VERSION $registryPrefix$imagename:develop
|
||||
else
|
||||
echo "Build contact-center failure."
|
||||
exit 1
|
||||
fi
|
@ -1,30 +0,0 @@
|
||||
#! /bin/bash
|
||||
###########################################
|
||||
# Create standalone SQL file to setup db
|
||||
###########################################
|
||||
|
||||
# constants
|
||||
baseDir=$(cd `dirname "$0"`;pwd)
|
||||
cwdDir=$PWD
|
||||
export PYTHONUNBUFFERED=1
|
||||
export PATH=/opt/miniconda3/envs/venv-py3/bin:$PATH
|
||||
export TS=$(date +%Y%m%d%H%M%S)
|
||||
export DATE=`date "+%Y%m%d"`
|
||||
export DATE_WITH_TIME=`date "+%Y%m%d-%H%M%S"` #add %3N as we want millisecond too
|
||||
|
||||
# functions
|
||||
|
||||
# main
|
||||
[ -z "${BASH_SOURCE[0]}" -o "${BASH_SOURCE[0]}" = "$0" ] || return
|
||||
|
||||
cd $baseDir/..
|
||||
|
||||
if [ ! -e tmp ]; then
|
||||
mkdir tmp
|
||||
fi
|
||||
|
||||
cat config/sql/001.mysql-create-db.sql > tmp/db-setup.sql
|
||||
echo "" >> tmp/db-setup.sql
|
||||
cat config/sql/002.mysql-create-schemas.sql >> tmp/db-setup.sql
|
||||
|
||||
echo "Setup Script created in" `pwd`/tmp/db-setup.sql
|
@ -1,50 +0,0 @@
|
||||
#! /bin/bash
|
||||
###########################################
|
||||
#
|
||||
###########################################
|
||||
|
||||
# constants
|
||||
baseDir=$(cd `dirname "$0"`;pwd)
|
||||
REPO_ID_SNP=chatopera-snapshots
|
||||
REPO_URL_SNP=https://nexus.chatopera.com/repository/maven-snapshots/
|
||||
REPO_ID_REL=chatopera-releases
|
||||
REPO_URL_REL=https://nexus.chatopera.com/repository/maven-releases/
|
||||
|
||||
# functions
|
||||
|
||||
# main
|
||||
[ -z "${BASH_SOURCE[0]}" -o "${BASH_SOURCE[0]}" = "$0" ] || return
|
||||
cd $baseDir/../app
|
||||
mvn clean jar:jar
|
||||
PACKAGE_VERSION=$(grep --max-count=1 '<version>' pom.xml | awk -F '>' '{ print $2 }' | awk -F '<' '{ print $1 }')
|
||||
if [[ $PACKAGE_VERSION == *SNAPSHOT ]]; then
|
||||
echo "Deploy as snapshot package ..."
|
||||
mvn deploy:deploy-file \
|
||||
-Dmaven.test.skip=true \
|
||||
-Dfile=./target/contact-center.jar \
|
||||
-DgroupId=com.cskefu.cc \
|
||||
-DartifactId=cc-core \
|
||||
-Dversion=$PACKAGE_VERSION \
|
||||
-Dpackaging=jar \
|
||||
-DgeneratePom=true \
|
||||
-DrepositoryId=$REPO_ID_SNP \
|
||||
-Durl=$REPO_URL_SNP
|
||||
if [ ! $? -eq 0 ]; then
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo "Deploy as release package ..."
|
||||
mvn deploy:deploy-file \
|
||||
-Dmaven.test.skip=true \
|
||||
-Dfile=./target/contact-center.jar \
|
||||
-DgroupId=com.cskefu.cc \
|
||||
-DartifactId=cc-core \
|
||||
-Dversion=$PACKAGE_VERSION \
|
||||
-Dpackaging=jar \
|
||||
-DgeneratePom=true \
|
||||
-DrepositoryId=$REPO_ID_REL \
|
||||
-Durl=$REPO_URL_REL
|
||||
if [ ! $? -eq 0 ]; then
|
||||
exit 1
|
||||
fi
|
||||
fi
|
@ -1,15 +0,0 @@
|
||||
#! /bin/bash
|
||||
###########################################
|
||||
#
|
||||
###########################################
|
||||
|
||||
# constants
|
||||
baseDir=$(cd `dirname "$0"`;pwd)
|
||||
# functions
|
||||
|
||||
# main
|
||||
[ -z "${BASH_SOURCE[0]}" -o "${BASH_SOURCE[0]}" = "$0" ] || return
|
||||
cd $baseDir/../app
|
||||
source .env
|
||||
mvn spring-boot:run
|
||||
#java -jar target/contact-center.war
|
@ -1,13 +0,0 @@
|
||||
#! /bin/bash
|
||||
###########################################
|
||||
#
|
||||
###########################################
|
||||
|
||||
# constants
|
||||
baseDir=$(cd `dirname "$0"`;pwd)
|
||||
# functions
|
||||
|
||||
# main
|
||||
[ -z "${BASH_SOURCE[0]}" -o "${BASH_SOURCE[0]}" = "$0" ] || return
|
||||
cd $baseDir/../app
|
||||
mvn eclipse:eclipse
|
@ -1,13 +0,0 @@
|
||||
#! /bin/bash
|
||||
###########################################
|
||||
#
|
||||
###########################################
|
||||
|
||||
# constants
|
||||
baseDir=$(cd `dirname "$0"`;pwd)
|
||||
# functions
|
||||
|
||||
# main
|
||||
[ -z "${BASH_SOURCE[0]}" -o "${BASH_SOURCE[0]}" = "$0" ] || return
|
||||
cd $baseDir/../app
|
||||
mvn idea:idea
|
@ -1,47 +0,0 @@
|
||||
#! /bin/bash
|
||||
###########################################
|
||||
#
|
||||
###########################################
|
||||
|
||||
# constants
|
||||
baseDir=$(cd `dirname "$0"`;pwd)
|
||||
SCRIPT_PATH=$0
|
||||
ts=`date +"%Y-%m-%d_%H-%M-%S"`
|
||||
buildDir=/tmp/cc-build-$ts
|
||||
# functions
|
||||
function print_usage(){
|
||||
echo "Install contact-center plugin: $SCRIPT_PATH contact-center_jar_path plugin_path output_path"
|
||||
}
|
||||
|
||||
# main
|
||||
[ -z "${BASH_SOURCE[0]}" -o "${BASH_SOURCE[0]}" = "$0" ] || return
|
||||
if [ "$#" -ne 4 ]; then
|
||||
CONTACT_CENTER=$1
|
||||
CC_PLUGIN=$2
|
||||
OUTPUT_PATH=$3
|
||||
if [ ! -f $1 ]; then
|
||||
echo "contact center jar file not exist."
|
||||
print_usage
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -f $2 ]; then
|
||||
echo "cc plugin jar file not exist."
|
||||
print_usage
|
||||
exit 2
|
||||
fi
|
||||
|
||||
# create jar
|
||||
rm -rf $buildDir
|
||||
mkdir $buildDir
|
||||
unzip $CONTACT_CENTER -d $buildDir
|
||||
cp $CC_PLUGIN $buildDir/BOOT-INF/lib
|
||||
cd $buildDir
|
||||
jar -cvfM0 $3 .
|
||||
echo "Created new jar file as" $OUTPUT_PATH "successfully."
|
||||
echo "Build done, delete buildDir" $buildDir "in 3 seconds ..."
|
||||
sleep 3
|
||||
rm -rf $buildDir
|
||||
else
|
||||
print_usage
|
||||
fi
|
@ -1,19 +0,0 @@
|
||||
#! /bin/bash
|
||||
###########################################
|
||||
#
|
||||
###########################################
|
||||
|
||||
# constants
|
||||
baseDir=$(cd `dirname "$0"`;pwd)
|
||||
# functions
|
||||
|
||||
# main
|
||||
[ -z "${BASH_SOURCE[0]}" -o "${BASH_SOURCE[0]}" = "$0" ] || return
|
||||
cd $baseDir/../app
|
||||
mvn -DskipTests clean package
|
||||
# take too long time with dev002 for uploading artifact, skip this operation
|
||||
# $baseDir/deploy.app.sh
|
||||
|
||||
if [ ! $? -eq 0 ]; then
|
||||
exit 1
|
||||
fi
|
@ -1,13 +0,0 @@
|
||||
#! /bin/bash
|
||||
###########################################
|
||||
#
|
||||
###########################################
|
||||
|
||||
# constants
|
||||
baseDir=$(cd `dirname "$0"`;pwd)
|
||||
# functions
|
||||
|
||||
# main
|
||||
[ -z "${BASH_SOURCE[0]}" -o "${BASH_SOURCE[0]}" = "$0" ] || return
|
||||
cd $baseDir/../root
|
||||
mvn deploy
|
@ -1,52 +0,0 @@
|
||||
#! /bin/bash
|
||||
###########################################
|
||||
#
|
||||
###########################################
|
||||
|
||||
# constants
|
||||
baseDir=$(cd `dirname "$0"`;pwd)
|
||||
appHome=$baseDir/..
|
||||
registryPrefix=
|
||||
imagename=cskefu/contact-center
|
||||
PACKAGE_VERSION=
|
||||
|
||||
# functions
|
||||
|
||||
# main
|
||||
[ -z "${BASH_SOURCE[0]}" -o "${BASH_SOURCE[0]}" = "$0" ] || return
|
||||
cd $appHome/
|
||||
|
||||
if [ -d ../private ]; then
|
||||
registryPrefix=dockerhub.qingcloud.com/
|
||||
fi
|
||||
|
||||
TIMESTAMP=`date "+%Y%m%d.%H%M%S"`
|
||||
PACKAGE_VERSION=`git rev-parse --short HEAD`
|
||||
|
||||
cd $baseDir
|
||||
docker run -it --rm \
|
||||
-p 9035:8035 \
|
||||
-p 9036:8036 \
|
||||
-v $PWD/data:/data \
|
||||
-v $PWD/logs:/logs \
|
||||
-e "JAVA_OPTS=-Xmx12288m -Xms2048m -XX:PermSize=256m -XX:MaxPermSize=1024m -Djava.net.preferIPv4Stack=true" \
|
||||
-e SERVER_PORT=8035 \
|
||||
-e SERVER_LOG_PATH=/logs \
|
||||
-e SERVER_LOG_LEVEL=INFO \
|
||||
-e WEB_UPLOAD_PATH=/data \
|
||||
-e SPRING_FREEMARKER_CACHE=true \
|
||||
-e SPRING_DATA_ELASTICSEARCH_PROPERTIES_PATH_DATA=/data \
|
||||
-e SPRING_DATASOURCE_DRIVER_CLASS_NAME=com.mysql.jdbc.Driver \
|
||||
-e "SPRING_DATASOURCE_URL=jdbc:mysql://mysql:8037/contactcenter?useUnicode=true&characterEncoding=UTF-8" \
|
||||
-e SPRING_DATASOURCE_USERNAME=root \
|
||||
-e SPRING_DATASOURCE_PASSWORD=123456 \
|
||||
-e MANAGEMENT_SECURITY_ENABLED=false \
|
||||
-e SPRING_REDIS_DATABASE=2 \
|
||||
-e SPRING_REDIS_HOST=redis \
|
||||
-e SPRING_REDIS_PORT=8041 \
|
||||
-e CSKEFU_CALLOUT_WATCH_INTERVAL=60000 \
|
||||
-e SPRING_DATA_ELASTICSEARCH_CLUSTER_NAME=elasticsearch \
|
||||
-e SPRING_DATA_ELASTICSEARCH_CLUSTER_NODES=elasticsearch:8040 \
|
||||
-e SPRING_DATA_ELASTICSEARCH_LOCAL=false \
|
||||
-e SPRING_DATA_ELASTICSEARCH_REPOSITORIES_ENABLED=true \
|
||||
$registryPrefix$imagename:$PACKAGE_VERSION
|
@ -1,14 +0,0 @@
|
||||
#! /bin/bash
|
||||
###########################################
|
||||
#
|
||||
###########################################
|
||||
|
||||
# constants
|
||||
baseDir=$(cd `dirname "$0"`;pwd)
|
||||
# functions
|
||||
|
||||
# main
|
||||
[ -z "${BASH_SOURCE[0]}" -o "${BASH_SOURCE[0]}" = "$0" ] || return
|
||||
cd $baseDir/../app
|
||||
set -x
|
||||
mvn -Dtest=com.cskefu.cc.proto.ProtoTest#testProto test
|
11
contact-center/app/.gitignore
vendored
11
contact-center/app/.gitignore
vendored
@ -1,11 +0,0 @@
|
||||
# dev profile
|
||||
src/main/resources/application-dev.properties
|
||||
|
||||
# ignore plugins: app views, classes
|
||||
src/main/resources/templates/apps/callout
|
||||
src/main/resources/templates/apps/callcenter
|
||||
src/main/java/com/cskefu/cc/plugins/botplt
|
||||
|
||||
# ignore logs
|
||||
logs/
|
||||
data/
|
@ -1,141 +0,0 @@
|
||||
<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>
|
||||
<groupId>com.cskefu.cc</groupId>
|
||||
<artifactId>contact-center</artifactId>
|
||||
<packaging>war</packaging>
|
||||
<name>cc-core</name>
|
||||
<description>春松客服:开源客服系统</description>
|
||||
<licenses>
|
||||
<license>
|
||||
<name>Chunsong Public License, version 1.0</name>
|
||||
<url>https://docs.cskefu.com/licenses/v1.html</url>
|
||||
<comments>
|
||||
Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd.
|
||||
</comments>
|
||||
</license>
|
||||
</licenses>
|
||||
<parent>
|
||||
<groupId>com.cskefu.cc</groupId>
|
||||
<artifactId>cc-root</artifactId>
|
||||
<version>8.0.0-SNAPSHOT</version>
|
||||
<!-- for Chatopera Nexus reference if file is available with latest version -->
|
||||
<!-- <relativePath/> -->
|
||||
<!-- for local reference if file is available with latest version -->
|
||||
<relativePath>../root/pom.xml</relativePath>
|
||||
</parent>
|
||||
<build>
|
||||
<finalName>contact-center</finalName>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>pl.project13.maven</groupId>
|
||||
<artifactId>git-commit-id-plugin</artifactId>
|
||||
<version>2.2.5</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>get-the-git-infos</id>
|
||||
<goals>
|
||||
<goal>revision</goal>
|
||||
</goals>
|
||||
<!-- *NOTE*: The default phase of revision is initialize, but in case you want to change it, you can do so by adding the phase here -->
|
||||
<phase>initialize</phase>
|
||||
</execution>
|
||||
<execution>
|
||||
<id>validate-the-git-infos</id>
|
||||
<goals>
|
||||
<goal>validateRevision</goal>
|
||||
</goals>
|
||||
<!-- *NOTE*: The default phase of validateRevision is verify, but in case you want to change it, you can do so by adding the phase here -->
|
||||
<phase>package</phase>
|
||||
</execution>
|
||||
</executions>
|
||||
<configuration>
|
||||
<excludeProperties>
|
||||
<excludeProperty>git.tags</excludeProperty>
|
||||
<excludeProperty>git.remote.*</excludeProperty>
|
||||
<excludeProperty>git.closest.*</excludeProperty>
|
||||
<excludeProperty>git.total.commit.count</excludeProperty>
|
||||
</excludeProperties>
|
||||
<dotGitDirectory>${project.basedir}/../../.git</dotGitDirectory>
|
||||
<generateGitPropertiesFilename>
|
||||
${project.build.outputDirectory}/git.properties
|
||||
</generateGitPropertiesFilename>
|
||||
<generateGitPropertiesFile>true</generateGitPropertiesFile>
|
||||
<prefix>git</prefix>
|
||||
<verbose>false</verbose>
|
||||
<injectAllReactorProjects>true</injectAllReactorProjects>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-war-plugin</artifactId>
|
||||
<configuration>
|
||||
<attachClasses>true</attachClasses>
|
||||
<warSourceExcludes>**/WEB-INF</warSourceExcludes>
|
||||
<packagingExcludes>**/WEB-INF,**/resources</packagingExcludes>
|
||||
<webResources>
|
||||
<resource>
|
||||
<directory>../config/sql/</directory>
|
||||
<includes>
|
||||
<include>**/*.sql</include>
|
||||
</includes>
|
||||
</resource>
|
||||
</webResources>
|
||||
</configuration>
|
||||
<version>3.3.2</version>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
<executions>
|
||||
<execution>
|
||||
<goals>
|
||||
<goal>repackage</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>springloaded</artifactId>
|
||||
<version>1.2.8.RELEASE</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-resources-plugin</artifactId>
|
||||
</plugin>
|
||||
</plugins>
|
||||
<defaultGoal>compile</defaultGoal>
|
||||
</build>
|
||||
<repositories>
|
||||
<repository>
|
||||
<id>chatopera</id>
|
||||
<name>Chatopera Inc.</name>
|
||||
<url>https://nexus.chatopera.com/repository/maven-public/</url>
|
||||
<releases>
|
||||
<enabled>true</enabled>
|
||||
</releases>
|
||||
<snapshots>
|
||||
<enabled>true</enabled>
|
||||
</snapshots>
|
||||
</repository>
|
||||
</repositories>
|
||||
<developers>
|
||||
<developer>
|
||||
<id>hain</id>
|
||||
<name>Hai Liang Wang</name>
|
||||
<email>hai@chatopera.com</email>
|
||||
<url>https://github.com/hailiang-wang</url>
|
||||
<organization>Chatopera Inc.</organization>
|
||||
<organizationUrl>https://www.chatopera.com</organizationUrl>
|
||||
<roles>
|
||||
<role>architect</role>
|
||||
<role>developer</role>
|
||||
</roles>
|
||||
<timezone>Asia/Shanghai</timezone>
|
||||
</developer>
|
||||
</developers>
|
||||
</project>
|
@ -1,141 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd.
|
||||
* <https://www.chatopera.com>, Licensed under the Chunsong Public
|
||||
* License, Version 1.0 (the "License"), https://docs.cskefu.com/licenses/v1.html
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* Copyright (C) 2018- Jun. 2023 Chatopera Inc, <https://www.chatopera.com>, Licensed under the Apache License, Version 2.0,
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Copyright (C) 2017 优客服-多渠道客服系统, Licensed under the Apache License, Version 2.0,
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*/
|
||||
package com.cskefu.cc;
|
||||
|
||||
import com.cskefu.cc.basic.Constants;
|
||||
import com.cskefu.cc.basic.MainContext;
|
||||
import com.cskefu.cc.config.AppCtxRefreshEventListener;
|
||||
import com.cskefu.cc.util.SystemEnvHelper;
|
||||
import com.cskefu.cc.util.mobile.MobileNumberUtils;
|
||||
import jakarta.servlet.MultipartConfigElement;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.Banner;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.boot.autoconfigure.jms.JmsPoolConnectionFactoryFactory;
|
||||
import org.springframework.boot.builder.SpringApplicationBuilder;
|
||||
import org.springframework.boot.web.server.ConfigurableWebServerFactory;
|
||||
import org.springframework.boot.web.server.ErrorPage;
|
||||
import org.springframework.boot.web.server.WebServerFactoryCustomizer;
|
||||
import org.springframework.boot.web.servlet.MultipartConfigFactory;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.transaction.annotation.EnableTransactionManagement;
|
||||
import org.springframework.util.unit.DataSize;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
@SpringBootApplication
|
||||
@EnableJpaRepositories("com.cskefu.cc.persistence.repository")
|
||||
@EnableTransactionManagement
|
||||
public class Application {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(Application.class);
|
||||
|
||||
@Value("${web.upload-path}")
|
||||
private String uploaddir;
|
||||
|
||||
@Value("${spring.servlet.multipart.max-file-size}")
|
||||
private Long multipartMaxUpload;
|
||||
|
||||
@Value("${spring.servlet.multipart.max-request-size}")
|
||||
private Long multipartMaxRequest;
|
||||
|
||||
/**
|
||||
* 加载模块
|
||||
*/
|
||||
static {
|
||||
// CRM模块
|
||||
if (StringUtils.equalsIgnoreCase(SystemEnvHelper.parseFromApplicationProps("cskefu.modules.contacts"), "true")) {
|
||||
MainContext.enableModule(Constants.CSKEFU_MODULE_CONTACTS);
|
||||
}
|
||||
// 会话监控模块 Customer Chats Audit
|
||||
if (StringUtils.equalsIgnoreCase(SystemEnvHelper.parseFromApplicationProps("cskefu.modules.cca"), "true")) {
|
||||
MainContext.enableModule(Constants.CSKEFU_MODULE_CCA);
|
||||
}
|
||||
|
||||
// 企业聊天模块
|
||||
if (StringUtils.equalsIgnoreCase(SystemEnvHelper.parseFromApplicationProps("cskefu.modules.entim"), "true")) {
|
||||
MainContext.enableModule(Constants.CSKEFU_MODULE_ENTIM);
|
||||
}
|
||||
|
||||
// 数据报表
|
||||
if (StringUtils.equalsIgnoreCase(SystemEnvHelper.parseFromApplicationProps("cskefu.modules.report"), "true")) {
|
||||
MainContext.enableModule(Constants.CSKEFU_MODULE_REPORT);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Init local resources
|
||||
*/
|
||||
protected static void serve(final String[] args) {
|
||||
try {
|
||||
// Tune druid params, https://github.com/cskefu/cskefu/issues/835
|
||||
System.setProperty("druid.mysql.usePingMethod", "false");
|
||||
|
||||
MobileNumberUtils.init();
|
||||
/************************
|
||||
* 该APP中加载多个配置文件
|
||||
* http://roufid.com/load-multiple-configuration-files-different-directories-spring-boot/
|
||||
************************/
|
||||
SpringApplication app = new SpringApplicationBuilder(Application.class)
|
||||
.properties("spring.config.name:application,git")
|
||||
.build();
|
||||
app.setBannerMode(Banner.Mode.CONSOLE);
|
||||
app.setAddCommandLineProperties(false);
|
||||
app.addListeners(new AppCtxRefreshEventListener());
|
||||
|
||||
MainContext.setApplicationContext(app.run(args));
|
||||
} catch (IOException e) {
|
||||
logger.error("Application Startup Error", e);
|
||||
System.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO lecjy
|
||||
@Bean
|
||||
public MultipartConfigElement multipartConfigElement() {
|
||||
MultipartConfigFactory factory = new MultipartConfigFactory();
|
||||
factory.setMaxFileSize(DataSize.ofMegabytes(multipartMaxUpload)); //KB,MB
|
||||
factory.setMaxRequestSize(DataSize.ofMegabytes(multipartMaxRequest));
|
||||
factory.setLocation(uploaddir);
|
||||
return factory.createMultipartConfig();
|
||||
}
|
||||
|
||||
// TODO lecjy
|
||||
@Bean
|
||||
public WebServerFactoryCustomizer<ConfigurableWebServerFactory> webServerFactoryCustomizer() {
|
||||
return factory -> {
|
||||
// 定义404错误页
|
||||
HttpStatus notFound = HttpStatus.NOT_FOUND;
|
||||
// 定义404错误页
|
||||
ErrorPage errorPage = new ErrorPage(notFound, "/error.html");
|
||||
// 追加错误页,替换springboot默认的错误页
|
||||
factory.addErrorPages(errorPage);
|
||||
};
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
try {
|
||||
Application.serve(args);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
@ -1,116 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd.
|
||||
* <https://www.chatopera.com>, Licensed under the Chunsong Public
|
||||
* License, Version 1.0 (the "License"), https://docs.cskefu.com/licenses/v1.html
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* Copyright (C) 2019-2022 Chatopera Inc, <https://www.chatopera.com>,
|
||||
* Licensed under the Apache License, Version 2.0,
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*/
|
||||
|
||||
package com.cskefu.cc.acd;
|
||||
|
||||
import com.cskefu.cc.acd.basic.ACDComposeContext;
|
||||
import com.cskefu.cc.acd.basic.ACDMessageHelper;
|
||||
import com.cskefu.cc.acd.basic.IACDDispatcher;
|
||||
import com.cskefu.cc.basic.MainContext;
|
||||
import com.cskefu.cc.cache.Cache;
|
||||
import com.cskefu.cc.cache.RedisCommand;
|
||||
import com.cskefu.cc.cache.RedisKey;
|
||||
import com.cskefu.cc.model.AgentStatus;
|
||||
import com.cskefu.cc.model.AgentUser;
|
||||
import com.cskefu.cc.persistence.repository.AgentStatusRepository;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
@Component
|
||||
public class ACDAgentDispatcher implements IACDDispatcher {
|
||||
private final static Logger logger = LoggerFactory.getLogger(ACDAgentDispatcher.class);
|
||||
|
||||
@Autowired
|
||||
private Cache cache;
|
||||
|
||||
@Autowired
|
||||
private AgentStatusRepository agentStatusRes;
|
||||
|
||||
@Autowired
|
||||
private RedisCommand redisCommand;
|
||||
|
||||
@Autowired
|
||||
private ACDVisitorDispatcher acdVisitorDispatcher;
|
||||
|
||||
@Autowired
|
||||
private ACDMessageHelper acdMessageHelper;
|
||||
|
||||
@Override
|
||||
public void enqueue(ACDComposeContext ctx) {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 撤退一个坐席
|
||||
* 1)将该坐席状态置为"非就绪"
|
||||
* 2) 将该坐席的访客重新分配给其它坐席
|
||||
*
|
||||
* @param ctx agentno为必填
|
||||
* @return 有没有成功将所有其服务的访客都分配出去
|
||||
*/
|
||||
@Override
|
||||
public void dequeue(final ACDComposeContext ctx) {
|
||||
// 先将该客服切换到非就绪状态
|
||||
final AgentStatus agentStatus = cache.findOneAgentStatusByAgentno(ctx.getAgentno());
|
||||
if (agentStatus != null) {
|
||||
agentStatus.setBusy(false);
|
||||
agentStatus.setUpdatetime(new Date());
|
||||
agentStatus.setStatus(MainContext.AgentStatusEnum.NOTREADY.toString());
|
||||
agentStatusRes.save(agentStatus);
|
||||
cache.putAgentStatus(agentStatus);
|
||||
}
|
||||
|
||||
// 然后将该坐席的访客分配给其它坐席
|
||||
// 获得该租户在线的客服的多少
|
||||
// TODO 对于agentUser的技能组过滤,在下面再逐个考虑?
|
||||
// 该信息同样也包括当前用户
|
||||
List<AgentUser> agentUsers = cache.findInservAgentUsersByAgentno(ctx.getAgentno());
|
||||
int sz = agentUsers.size();
|
||||
for (final AgentUser x : agentUsers) {
|
||||
try {
|
||||
// TODO 此处没有考虑遍历过程中,系统中坐席的服务访客的信息实际上是变化的
|
||||
// 可能会发生maxusers超过设置的情况,如果做很多检查,会带来一定一系统开销
|
||||
// 因为影响不大,放弃实时的检查
|
||||
ACDComposeContext y = acdMessageHelper.getComposeContextWithAgentUser(
|
||||
x, false, MainContext.ChatInitiatorType.USER.toString());
|
||||
acdVisitorDispatcher.enqueue(y);
|
||||
|
||||
// 因为重新分配该访客,将其从撤离的坐席中服务集合中删除
|
||||
// 此处类似于 Transfer
|
||||
redisCommand.removeSetVal(
|
||||
RedisKey.getInServAgentUsersByAgentno(ctx.getAgentno()), x.getUserid());
|
||||
sz--;
|
||||
} catch (Exception e) {
|
||||
logger.warn("[dequeue] throw error:", e);
|
||||
}
|
||||
}
|
||||
|
||||
if (sz == 0) {
|
||||
logger.info(
|
||||
"[dequeue] after re-allotAgent, the agentUsers size is {} for agentno {}", sz,
|
||||
ctx.getAgentno());
|
||||
} else {
|
||||
logger.warn(
|
||||
"[dequeue] after re-allotAgent, the agentUsers size is {} for agentno {}", sz,
|
||||
ctx.getAgentno());
|
||||
}
|
||||
|
||||
ctx.setResolved(sz == 0);
|
||||
}
|
||||
}
|
@ -1,649 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd.
|
||||
* <https://www.chatopera.com>, Licensed under the Chunsong Public
|
||||
* License, Version 1.0 (the "License"), https://docs.cskefu.com/licenses/v1.html
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* Copyright (C) 2019-2022 Chatopera Inc, <https://www.chatopera.com>,
|
||||
* Licensed under the Apache License, Version 2.0,
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*/
|
||||
|
||||
package com.cskefu.cc.acd;
|
||||
|
||||
import com.cskefu.cc.acd.basic.ACDComposeContext;
|
||||
import com.cskefu.cc.acd.basic.ACDMessageHelper;
|
||||
import com.cskefu.cc.basic.MainContext;
|
||||
import com.cskefu.cc.basic.MainUtils;
|
||||
import com.cskefu.cc.cache.Cache;
|
||||
import com.cskefu.cc.cache.RedisCommand;
|
||||
import com.cskefu.cc.cache.RedisKey;
|
||||
import com.cskefu.cc.exception.CSKefuException;
|
||||
import com.cskefu.cc.model.*;
|
||||
import com.cskefu.cc.peer.PeerSyncIM;
|
||||
import com.cskefu.cc.persistence.repository.*;
|
||||
import com.cskefu.cc.proxy.AgentStatusProxy;
|
||||
import com.cskefu.cc.proxy.AgentUserProxy;
|
||||
import com.cskefu.cc.socketio.client.NettyClients;
|
||||
import com.cskefu.cc.socketio.message.Message;
|
||||
import com.cskefu.cc.util.HashMapUtils;
|
||||
import com.cskefu.cc.util.SerializeUtil;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
@Component
|
||||
public class ACDAgentService {
|
||||
private final static Logger logger = LoggerFactory.getLogger(ACDAgentService.class);
|
||||
|
||||
@Autowired
|
||||
private RedisCommand redisCommand;
|
||||
|
||||
@Autowired
|
||||
private ACDMessageHelper acdMessageHelper;
|
||||
|
||||
@Autowired
|
||||
private AgentStatusProxy agentStatusProxy;
|
||||
|
||||
@Autowired
|
||||
private ACDPolicyService acdPolicyService;
|
||||
|
||||
@Autowired
|
||||
@Lazy
|
||||
private PeerSyncIM peerSyncIM;
|
||||
|
||||
@Autowired
|
||||
private Cache cache;
|
||||
|
||||
@Autowired
|
||||
private AgentUserRepository agentUserRes;
|
||||
|
||||
@Autowired
|
||||
private AgentServiceRepository agentServiceRes;
|
||||
|
||||
@Autowired
|
||||
private AgentUserTaskRepository agentUserTaskRes;
|
||||
|
||||
@Autowired
|
||||
private AgentStatusRepository agentStatusRes;
|
||||
|
||||
@Autowired
|
||||
private PassportWebIMUserRepository onlineUserRes;
|
||||
|
||||
@Autowired
|
||||
private UserRepository userRes;
|
||||
|
||||
@Autowired
|
||||
private AgentUserProxy agentUserProxy;
|
||||
|
||||
|
||||
/**
|
||||
* ACD结果通知
|
||||
*
|
||||
* @param ctx
|
||||
*/
|
||||
public void notifyAgentUserProcessResult(final ACDComposeContext ctx) {
|
||||
Objects.requireNonNull(ctx, "ctx can not be null");
|
||||
if (StringUtils.isBlank(ctx.getMessage())) {
|
||||
logger.info("[onConnect] can not find available agent for user {}", ctx.getOnlineUserId());
|
||||
return;
|
||||
}
|
||||
logger.info("[onConnect] find available agent for onlineUser id {}", ctx.getOnlineUserId());
|
||||
|
||||
/**
|
||||
* 发送消息给坐席
|
||||
* 如果没有AgentService或该AgentService没有坐席或AgentService在排队中,则不发送
|
||||
*/
|
||||
if (ctx.getAgentService() != null && (!ctx.isNoagent()) && !StringUtils.equals(
|
||||
MainContext.AgentUserStatusEnum.INQUENE.toString(),
|
||||
ctx.getAgentService().getStatus())) {
|
||||
// 通知消息到坐席
|
||||
MainContext.getPeerSyncIM().send(MainContext.ReceiverType.AGENT,
|
||||
MainContext.ChannelType.WEBIM,
|
||||
ctx.getAppid(),
|
||||
MainContext.MessageType.NEW,
|
||||
ctx.getAgentService().getAgentno(),
|
||||
ctx, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送消息给访客
|
||||
*/
|
||||
Message outMessage = new Message();
|
||||
outMessage.setAgentUser(ctx.getAgentUser());
|
||||
outMessage.setMessage(ctx.getMessage());
|
||||
outMessage.setMessageType(MainContext.MessageType.MESSAGE.toString());
|
||||
outMessage.setCalltype(MainContext.CallType.IN.toString());
|
||||
outMessage.setCreatetime(MainUtils.dateFormate.format(new Date()));
|
||||
outMessage.setNoagent(ctx.isNoagent());
|
||||
if (ctx.getAgentService() != null) {
|
||||
outMessage.setAgentserviceid(ctx.getAgentService().getId());
|
||||
}
|
||||
|
||||
MainContext.getPeerSyncIM().send(MainContext.ReceiverType.VISITOR,
|
||||
MainContext.ChannelType.toValue(ctx.getChannelType()),
|
||||
ctx.getAppid(),
|
||||
MainContext.MessageType.NEW, ctx.getOnlineUserId(), outMessage, true);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 邀请访客进入当前对话,如果当前操作的 坐席是已就绪状态,则直接加入到当前坐席的
|
||||
* 对话列表中,如果未登录,则分配给其他坐席
|
||||
*
|
||||
* @param agentno
|
||||
* @param agentUser
|
||||
* @throws Exception
|
||||
*/
|
||||
public void assignVisitorAsInvite(
|
||||
final String agentno,
|
||||
final AgentUser agentUser
|
||||
) throws Exception {
|
||||
final AgentStatus agentStatus = cache.findOneAgentStatusByAgentno(agentno);
|
||||
pickupAgentUserInQueue(agentUser, agentStatus);
|
||||
}
|
||||
|
||||
/**
|
||||
* 为坐席批量分配用户
|
||||
*
|
||||
* @param agentno
|
||||
*/
|
||||
public void assignVisitors(String agentno) {
|
||||
logger.info("[assignVisitors] agentno {}", agentno);
|
||||
// 获得目标坐席的状态
|
||||
AgentStatus agentStatus = SerializeUtil.deserialize(
|
||||
redisCommand.getHashKV(RedisKey.getAgentStatusReadyHashKey(), agentno));
|
||||
|
||||
if (agentStatus == null) {
|
||||
logger.warn("[assignVisitors] can not find AgentStatus for agentno {}", agentno);
|
||||
return;
|
||||
}
|
||||
logger.info("[assignVisitors] agentStatus id {}, status {}, service {}/{}, skills {}, busy {}",
|
||||
agentStatus.getId(), agentStatus.getStatus(), agentStatus.getUsers(), agentStatus.getMaxusers(),
|
||||
HashMapUtils.concatKeys(agentStatus.getSkills(), "|"), agentStatus.isBusy());
|
||||
|
||||
if ((!StringUtils.equals(
|
||||
MainContext.AgentStatusEnum.READY.toString(), agentStatus.getStatus())) || agentStatus.isBusy()) {
|
||||
// 该坐席处于非就绪状态,或该坐席处于置忙
|
||||
// 不分配坐席
|
||||
return;
|
||||
}
|
||||
|
||||
// 获得所有待服务访客的列表
|
||||
final Map<String, AgentUser> pendingAgentUsers = cache.getAgentUsersInQue();
|
||||
|
||||
// 本次批量分配访客数目
|
||||
Map<String, Integer> assigned = new HashMap<>();
|
||||
int currentAssigned = cache.getInservAgentUsersSizeByAgentno(
|
||||
agentStatus.getAgentno());
|
||||
|
||||
logger.info(
|
||||
"[assignVisitors] agentno {}, name {}, current assigned {}, batch size in queue {}",
|
||||
agentStatus.getAgentno(),
|
||||
agentStatus.getUsername(), currentAssigned, pendingAgentUsers.size());
|
||||
|
||||
for (Map.Entry<String, AgentUser> entry : pendingAgentUsers.entrySet()) {
|
||||
AgentUser agentUser = entry.getValue();
|
||||
boolean process = false;
|
||||
|
||||
if ((StringUtils.equals(agentUser.getAgentno(), agentno))) {
|
||||
// 待服务的访客指定了该坐席
|
||||
process = true;
|
||||
} else if (agentStatus != null &&
|
||||
agentStatus.getSkills() != null &&
|
||||
agentStatus.getSkills().size() > 0) {
|
||||
// 目标坐席有状态,并且坐席属于某技能组
|
||||
if ((StringUtils.isBlank(agentUser.getAgentno()) &&
|
||||
StringUtils.isBlank(agentUser.getSkill()))) {
|
||||
// 待服务的访客还没有指定坐席,并且也没有绑定技能组
|
||||
process = true;
|
||||
} else if (StringUtils.isBlank(agentUser.getAgentno()) &&
|
||||
agentStatus.getSkills().containsKey(agentUser.getSkill())) {
|
||||
// 待服务的访客还没有指定坐席,并且指定的技能组和该坐席的技能组一致
|
||||
process = true;
|
||||
}
|
||||
} else if (StringUtils.isBlank(agentUser.getAgentno()) &&
|
||||
StringUtils.isBlank(agentUser.getSkill())) {
|
||||
// 目标坐席没有状态,或该目标坐席有状态但是没有属于任何一个技能组
|
||||
// 待服务访客没有指定坐席,并且没有指定技能组
|
||||
process = true;
|
||||
}
|
||||
|
||||
if (!process) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 坐席未达到最大咨询访客数量,并且单次批量分配小于坐席就绪时分配最大访客数量(initMaxuser)
|
||||
final SessionConfig sessionConfig = acdPolicyService.initSessionConfig(agentUser.getSkill());
|
||||
if ((ACDServiceRouter.getAcdPolicyService().getAgentUsersBySkill(agentStatus, agentUser.getSkill()) < sessionConfig.getMaxuser()) && (assigned.getOrDefault(agentUser.getSkill(), 0) < sessionConfig.getInitmaxuser())) {
|
||||
assigned.merge(agentUser.getSkill(), 1, Integer::sum);
|
||||
pickupAgentUserInQueue(agentUser, agentStatus);
|
||||
} else {
|
||||
logger.info(
|
||||
"[assignVisitors] agentno {} reach the max users limit {}/{} or batch assign limit {}/{}",
|
||||
agentno,
|
||||
(currentAssigned + assigned.getOrDefault(agentUser.getSkill(), 0)),
|
||||
sessionConfig.getMaxuser(), assigned, sessionConfig.getInitmaxuser());
|
||||
break;
|
||||
}
|
||||
}
|
||||
agentStatusProxy.broadcastAgentsStatus("agent", "success", agentno);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从队列中选择访客进行会话
|
||||
*
|
||||
* @param agentUser
|
||||
* @param agentStatus
|
||||
* @return
|
||||
*/
|
||||
public AgentService pickupAgentUserInQueue(final AgentUser agentUser, final AgentStatus agentStatus) {
|
||||
// 从排队队列移除
|
||||
cache.deleteAgentUserInqueByAgentUserId(agentUser.getUserid());
|
||||
AgentService agentService = null;
|
||||
// 下面开始处理其加入到服务中的队列
|
||||
try {
|
||||
agentService = resolveAgentService(
|
||||
agentStatus, agentUser, false);
|
||||
|
||||
// 处理完成得到 agentService
|
||||
Message outMessage = new Message();
|
||||
outMessage.setMessage(acdMessageHelper.getSuccessMessage(
|
||||
agentService,
|
||||
agentUser.getChanneltype()));
|
||||
outMessage.setMessageType(MainContext.MediaType.TEXT.toString());
|
||||
outMessage.setCalltype(MainContext.CallType.IN.toString());
|
||||
outMessage.setCreatetime(MainUtils.dateFormate.format(new Date()));
|
||||
|
||||
if (StringUtils.isNotBlank(agentUser.getUserid())) {
|
||||
outMessage.setAgentUser(agentUser);
|
||||
outMessage.setChannelMessage(agentUser);
|
||||
|
||||
// 向访客推送消息
|
||||
peerSyncIM.send(
|
||||
MainContext.ReceiverType.VISITOR,
|
||||
MainContext.ChannelType.toValue(agentUser.getChanneltype()), agentUser.getAppid(),
|
||||
MainContext.MessageType.STATUS, agentUser.getUserid(), outMessage, true
|
||||
);
|
||||
|
||||
// 向坐席推送消息
|
||||
peerSyncIM.send(MainContext.ReceiverType.AGENT, MainContext.ChannelType.WEBIM,
|
||||
agentUser.getAppid(),
|
||||
MainContext.MessageType.NEW, agentUser.getAgentno(), outMessage, true);
|
||||
|
||||
// 通知更新在线数据
|
||||
agentStatusProxy.broadcastAgentsStatus("agent", "pickup", agentStatus.getAgentno());
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
logger.warn("[assignVisitors] fail to process service", ex);
|
||||
}
|
||||
return agentService;
|
||||
}
|
||||
|
||||
/**
|
||||
* 访客服务结束
|
||||
*
|
||||
* @param agentUser
|
||||
* @throws Exception
|
||||
*/
|
||||
public void finishAgentService(final AgentUser agentUser) {
|
||||
if (agentUser != null) {
|
||||
/**
|
||||
* 设置AgentUser
|
||||
*/
|
||||
// 获得坐席状态
|
||||
AgentStatus agentStatus = null;
|
||||
if (StringUtils.equals(MainContext.AgentUserStatusEnum.INSERVICE.toString(), agentUser.getStatus()) &&
|
||||
agentUser.getAgentno() != null) {
|
||||
agentStatus = cache.findOneAgentStatusByAgentno(agentUser.getAgentno());
|
||||
}
|
||||
|
||||
// 设置新AgentUser的状态
|
||||
agentUser.setStatus(MainContext.AgentUserStatusEnum.END.toString());
|
||||
if (agentUser.getServicetime() != null) {
|
||||
agentUser.setSessiontimes(System.currentTimeMillis() - agentUser.getServicetime().getTime());
|
||||
}
|
||||
|
||||
// 从缓存中删除agentUser缓存
|
||||
agentUserRes.save(agentUser);
|
||||
|
||||
final SessionConfig sessionConfig = acdPolicyService.initSessionConfig(agentUser.getSkill());
|
||||
|
||||
/**
|
||||
* 坐席服务
|
||||
*/
|
||||
AgentService service = null;
|
||||
if (StringUtils.isNotBlank(agentUser.getAgentserviceid())) {
|
||||
service = agentServiceRes.findById(agentUser.getAgentserviceid()).orElse(null);
|
||||
} else if (agentStatus != null) {
|
||||
// 该访客没有和坐席对话,因此没有 AgentService
|
||||
// 当做留言处理,创建一个新的 AgentService
|
||||
service = resolveAgentService(agentStatus, agentUser, true);
|
||||
}
|
||||
|
||||
if (service != null) {
|
||||
service.setStatus(MainContext.AgentUserStatusEnum.END.toString());
|
||||
service.setEndtime(new Date());
|
||||
if (service.getServicetime() != null) {
|
||||
service.setSessiontimes(System.currentTimeMillis() - service.getServicetime().getTime());
|
||||
}
|
||||
|
||||
final AgentUserTask agentUserTask = agentUserTaskRes.findById(agentUser.getId()).orElse(null);
|
||||
if (agentUserTask != null) {
|
||||
service.setAgentreplyinterval(agentUserTask.getAgentreplyinterval());
|
||||
service.setAgentreplytime(agentUserTask.getAgentreplytime());
|
||||
service.setAvgreplyinterval(agentUserTask.getAvgreplyinterval());
|
||||
service.setAvgreplytime(agentUserTask.getAvgreplytime());
|
||||
|
||||
service.setUserasks(agentUserTask.getUserasks());
|
||||
service.setAgentreplys(agentUserTask.getAgentreplys());
|
||||
|
||||
// 开启了质检,并且是有效对话
|
||||
if (sessionConfig.isQuality()) {
|
||||
// 未分配质检任务
|
||||
service.setQualitystatus(MainContext.QualityStatusEnum.NODIS.toString());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 启用了质检任务,开启质检
|
||||
*/
|
||||
if ((!sessionConfig.isQuality()) || service.getUserasks() == 0) {
|
||||
// 未开启质检 或无效对话无需质检
|
||||
service.setQualitystatus(MainContext.QualityStatusEnum.NO.toString());
|
||||
}
|
||||
agentServiceRes.save(service);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新AgentStatus
|
||||
*/
|
||||
if (agentStatus != null) {
|
||||
agentStatus.setUsers(
|
||||
cache.getInservAgentUsersSizeByAgentno(agentStatus.getAgentno()));
|
||||
agentStatusRes.save(agentStatus);
|
||||
}
|
||||
|
||||
Message outMessage = new Message();
|
||||
|
||||
/**
|
||||
* 发送到访客端的通知
|
||||
*/
|
||||
switch (MainContext.ChannelType.toValue(agentUser.getChanneltype())) {
|
||||
case WEBIM:
|
||||
// WebIM 发送对话结束事件
|
||||
// 向访客发送消息
|
||||
outMessage.setAgentStatus(agentStatus);
|
||||
outMessage.setMessage(acdMessageHelper.getServiceFinishMessage(agentUser.getChanneltype(), agentUser.getSkill()));
|
||||
outMessage.setMessageType(MainContext.AgentUserStatusEnum.END.toString());
|
||||
outMessage.setCalltype(MainContext.CallType.IN.toString());
|
||||
outMessage.setCreatetime(MainUtils.dateFormate.format(new Date()));
|
||||
outMessage.setAgentUser(agentUser);
|
||||
|
||||
// 向访客发送消息
|
||||
peerSyncIM.send(
|
||||
MainContext.ReceiverType.VISITOR,
|
||||
MainContext.ChannelType.toValue(agentUser.getChanneltype()), agentUser.getAppid(),
|
||||
MainContext.MessageType.STATUS, agentUser.getUserid(), outMessage, true
|
||||
);
|
||||
|
||||
if (agentStatus != null) {
|
||||
// 坐席在线,通知结束会话
|
||||
outMessage.setChannelMessage(agentUser);
|
||||
outMessage.setAgentUser(agentUser);
|
||||
peerSyncIM.send(MainContext.ReceiverType.AGENT, MainContext.ChannelType.WEBIM,
|
||||
agentUser.getAppid(),
|
||||
MainContext.MessageType.END, agentUser.getAgentno(), outMessage, true);
|
||||
}
|
||||
break;
|
||||
case PHONE:
|
||||
// 语音渠道,强制发送
|
||||
logger.info(
|
||||
"[finishAgentService] send notify to callout channel agentno {}", agentUser.getAgentno());
|
||||
NettyClients.getInstance().sendCalloutEventMessage(
|
||||
agentUser.getAgentno(), MainContext.MessageType.END.toString(), agentUser);
|
||||
break;
|
||||
case MESSENGER:
|
||||
outMessage.setAgentStatus(agentStatus);
|
||||
outMessage.setMessage(acdMessageHelper.getServiceFinishMessage(agentUser.getChanneltype(), agentUser.getSkill()));
|
||||
outMessage.setMessageType(MainContext.AgentUserStatusEnum.END.toString());
|
||||
outMessage.setCalltype(MainContext.CallType.IN.toString());
|
||||
outMessage.setCreatetime(MainUtils.dateFormate.format(new Date()));
|
||||
outMessage.setAgentUser(agentUser);
|
||||
|
||||
// 向访客发送消息
|
||||
peerSyncIM.send(
|
||||
MainContext.ReceiverType.VISITOR,
|
||||
MainContext.ChannelType.toValue(agentUser.getChanneltype()), agentUser.getAppid(),
|
||||
MainContext.MessageType.STATUS, agentUser.getUserid(), outMessage, true
|
||||
);
|
||||
|
||||
if (agentStatus != null) {
|
||||
// 坐席在线,通知结束会话
|
||||
outMessage.setChannelMessage(agentUser);
|
||||
outMessage.setAgentUser(agentUser);
|
||||
peerSyncIM.send(MainContext.ReceiverType.AGENT, MainContext.ChannelType.MESSENGER,
|
||||
agentUser.getAppid(),
|
||||
MainContext.MessageType.END, agentUser.getAgentno(), outMessage, true);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
logger.info(
|
||||
"[finishAgentService] ignore notify agent service end for channel {}, agent user id {}",
|
||||
agentUser.getChanneltype(), agentUser.getId());
|
||||
}
|
||||
|
||||
// 更新访客的状态为可以接收邀请
|
||||
final PassportWebIMUser passportWebIMUser = onlineUserRes.findOneByUserid(
|
||||
agentUser.getUserid());
|
||||
if (passportWebIMUser != null) {
|
||||
passportWebIMUser.setInvitestatus(MainContext.OnlineUserInviteStatus.DEFAULT.toString());
|
||||
onlineUserRes.save(passportWebIMUser);
|
||||
logger.info(
|
||||
"[finishAgentService] onlineUser id {}, status {}, invite status {}", passportWebIMUser.getId(),
|
||||
passportWebIMUser.getStatus(), passportWebIMUser.getInvitestatus());
|
||||
}
|
||||
|
||||
// 当前访客服务已经结束,为坐席寻找新访客
|
||||
if (agentStatus != null) {
|
||||
if ((ACDServiceRouter.getAcdPolicyService().getAgentUsersBySkill(agentStatus, agentUser.getSkill()) - 1) < sessionConfig.getMaxuser()) {
|
||||
assignVisitors(agentStatus.getAgentno());
|
||||
}
|
||||
}
|
||||
agentStatusProxy.broadcastAgentsStatus(
|
||||
"end", "success", agentUser != null ? agentUser.getId() : null);
|
||||
} else {
|
||||
logger.info("[finishAgentService] invalid agent user, should not be null");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 删除AgentUser
|
||||
* 包括数据库记录及缓存信息
|
||||
*
|
||||
* @param agentUser
|
||||
* @return
|
||||
*/
|
||||
public void finishAgentUser(final AgentUser agentUser) throws CSKefuException {
|
||||
logger.info("[finishAgentUser] userId {}", agentUser.getUserid());
|
||||
|
||||
if (agentUser == null || agentUser.getId() == null) {
|
||||
throw new CSKefuException("Invalid agentUser info");
|
||||
}
|
||||
|
||||
if (!StringUtils.equals(MainContext.AgentUserStatusEnum.END.toString(), agentUser.getStatus())) {
|
||||
/**
|
||||
* 未结束聊天,先结束对话,然后删除记录
|
||||
*/
|
||||
// 删除缓存
|
||||
finishAgentService(agentUser);
|
||||
}
|
||||
|
||||
// 删除数据库里的AgentUser记录
|
||||
agentUserRes.delete(agentUser);
|
||||
}
|
||||
|
||||
/**
|
||||
* 为agentUser生成对应的AgentService
|
||||
* 使用场景:
|
||||
* 1. 在AgentUser服务结束并且还没有对应的AgentService
|
||||
* 2. 在新服务开始,安排坐席
|
||||
*
|
||||
* @param agentStatus 坐席状态
|
||||
* @param agentUser 坐席访客会话
|
||||
* @param finished 结束服务
|
||||
* @return
|
||||
*/
|
||||
public AgentService resolveAgentService(
|
||||
AgentStatus agentStatus,
|
||||
final AgentUser agentUser,
|
||||
final boolean finished) {
|
||||
|
||||
AgentService agentService = new AgentService();
|
||||
if (StringUtils.isNotBlank(agentUser.getAgentserviceid())) {
|
||||
AgentService existAgentService = agentServiceRes.findById(agentUser.getAgentserviceid()).orElse(null);
|
||||
if (existAgentService != null) {
|
||||
agentService = existAgentService;
|
||||
} else {
|
||||
agentService.setId(agentUser.getAgentserviceid());
|
||||
}
|
||||
}
|
||||
final Date now = new Date();
|
||||
// 批量复制属性
|
||||
MainUtils.copyProperties(agentUser, agentService);
|
||||
agentService.setChanneltype(agentUser.getChanneltype());
|
||||
agentService.setSessionid(agentUser.getSessionid());
|
||||
|
||||
// 此处为何设置loginDate为现在
|
||||
agentUser.setLogindate(now);
|
||||
PassportWebIMUser passportWebIMUser = onlineUserRes.findOneByUserid(agentUser.getUserid());
|
||||
|
||||
if (finished == true) {
|
||||
// 服务结束
|
||||
agentUser.setStatus(MainContext.AgentUserStatusEnum.END.toString());
|
||||
agentService.setStatus(MainContext.AgentUserStatusEnum.END.toString());
|
||||
agentService.setSessiontype(MainContext.AgentUserStatusEnum.END.toString());
|
||||
if (agentStatus == null) {
|
||||
// 没有满足条件的坐席,留言
|
||||
agentService.setLeavemsg(true);
|
||||
agentService.setLeavemsgstatus(MainContext.LeaveMsgStatus.NOTPROCESS.toString()); //未处理的留言
|
||||
}
|
||||
|
||||
if (passportWebIMUser != null) {
|
||||
// 更新OnlineUser对象,变更为默认状态,可以接受邀请
|
||||
passportWebIMUser.setInvitestatus(MainContext.OnlineUserInviteStatus.DEFAULT.toString());
|
||||
}
|
||||
} else if (agentStatus != null) {
|
||||
agentService.setAgent(agentStatus.getAgentno());
|
||||
agentService.setSkill(agentUser.getSkill());
|
||||
agentUser.setStatus(MainContext.AgentUserStatusEnum.INSERVICE.toString());
|
||||
agentService.setStatus(MainContext.AgentUserStatusEnum.INSERVICE.toString());
|
||||
agentService.setSessiontype(MainContext.AgentUserStatusEnum.INSERVICE.toString());
|
||||
// 设置坐席名字
|
||||
agentService.setAgentno(agentStatus.getUserid());
|
||||
agentService.setAgentusername(agentStatus.getUsername());
|
||||
} else {
|
||||
// 不是服务结束,但是没有满足条件的坐席
|
||||
// 加入到排队中
|
||||
agentUser.setStatus(MainContext.AgentUserStatusEnum.INQUENE.toString());
|
||||
agentService.setStatus(MainContext.AgentUserStatusEnum.INQUENE.toString());
|
||||
agentService.setSessiontype(MainContext.AgentUserStatusEnum.INQUENE.toString());
|
||||
}
|
||||
|
||||
if (finished || agentStatus != null) {
|
||||
agentService.setAgentuserid(agentUser.getId());
|
||||
agentService.setInitiator(MainContext.ChatInitiatorType.USER.toString());
|
||||
|
||||
long waittingtime = 0;
|
||||
if (agentUser.getWaittingtimestart() != null) {
|
||||
waittingtime = System.currentTimeMillis() - agentUser.getWaittingtimestart().getTime();
|
||||
} else {
|
||||
if (agentUser.getCreatetime() != null) {
|
||||
waittingtime = System.currentTimeMillis() - agentUser.getCreatetime().getTime();
|
||||
}
|
||||
}
|
||||
|
||||
agentUser.setWaittingtime((int) waittingtime);
|
||||
agentUser.setServicetime(now);
|
||||
agentService.setOwner(agentUser.getOwner());
|
||||
agentService.setTimes(0);
|
||||
|
||||
final User agent = userRes.findById(agentService.getAgentno()).orElse(null);
|
||||
agentUser.setAgentname(agent.getUname());
|
||||
agentUser.setAgentno(agentService.getAgentno());
|
||||
|
||||
if (StringUtils.isNotBlank(agentUser.getName())) {
|
||||
agentService.setName(agentUser.getName());
|
||||
}
|
||||
if (StringUtils.isNotBlank(agentUser.getPhone())) {
|
||||
agentService.setPhone(agentUser.getPhone());
|
||||
}
|
||||
if (StringUtils.isNotBlank(agentUser.getEmail())) {
|
||||
agentService.setEmail(agentUser.getEmail());
|
||||
}
|
||||
if (StringUtils.isNotBlank(agentUser.getResion())) {
|
||||
agentService.setResion(agentUser.getResion());
|
||||
}
|
||||
|
||||
if (StringUtils.isNotBlank(agentUser.getSkill())) {
|
||||
agentService.setAgentskill(agentUser.getSkill());
|
||||
}
|
||||
|
||||
agentService.setServicetime(now);
|
||||
|
||||
if (agentUser.getCreatetime() != null) {
|
||||
agentService.setWaittingtime((int) (System.currentTimeMillis() - agentUser.getCreatetime().getTime()));
|
||||
agentUser.setWaittingtime(agentService.getWaittingtime());
|
||||
}
|
||||
if (passportWebIMUser != null) {
|
||||
agentService.setOsname(passportWebIMUser.getOpersystem());
|
||||
agentService.setBrowser(passportWebIMUser.getBrowser());
|
||||
// 记录onlineUser的id
|
||||
agentService.setDataid(passportWebIMUser.getId());
|
||||
}
|
||||
|
||||
agentService.setLogindate(agentUser.getCreatetime());
|
||||
agentServiceRes.save(agentService);
|
||||
|
||||
agentUser.setAgentserviceid(agentService.getId());
|
||||
agentUser.setLastgetmessage(now);
|
||||
agentUser.setLastmessage(now);
|
||||
}
|
||||
|
||||
agentService.setDataid(agentUser.getId());
|
||||
|
||||
/**
|
||||
* 分配成功以后, 将用户和坐席的对应关系放入到缓存
|
||||
* 将 AgentUser 放入到当前坐席的服务队列
|
||||
*/
|
||||
agentUserRes.save(agentUser);
|
||||
|
||||
/**
|
||||
* 更新OnlineUser对象,变更为服务中,不可邀请
|
||||
*/
|
||||
if (passportWebIMUser != null && !finished) {
|
||||
passportWebIMUser.setInvitestatus(MainContext.OnlineUserInviteStatus.INSERV.toString());
|
||||
onlineUserRes.save(passportWebIMUser);
|
||||
}
|
||||
|
||||
// 更新坐席服务人数,坐席更新时间到缓存
|
||||
if (agentStatus != null) {
|
||||
agentUserProxy.updateAgentStatus(agentStatus);
|
||||
}
|
||||
return agentService;
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -1,84 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd.
|
||||
* <https://www.chatopera.com>, Licensed under the Chunsong Public
|
||||
* License, Version 1.0 (the "License"), https://docs.cskefu.com/licenses/v1.html
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* Copyright (C) 2019-2022 Chatopera Inc, <https://www.chatopera.com>,
|
||||
* Licensed under the Apache License, Version 2.0,
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*/
|
||||
|
||||
package com.cskefu.cc.acd;
|
||||
|
||||
import com.cskefu.cc.basic.MainContext;
|
||||
import com.cskefu.cc.model.AgentService;
|
||||
import com.cskefu.cc.model.AgentUser;
|
||||
import com.cskefu.cc.persistence.repository.AgentServiceRepository;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
@Component
|
||||
public class ACDChatbotService {
|
||||
private final static Logger logger = LoggerFactory.getLogger(ACDChatbotService.class);
|
||||
|
||||
@Autowired
|
||||
private AgentServiceRepository agentServiceRes;
|
||||
|
||||
/**
|
||||
* 为访客分配机器人客服, ACD策略,此处 AgentStatus 是建议 的 坐席, 如果启用了 历史服务坐席 优先策略, 则会默认检查历史坐席是否空闲,如果空闲,则分配,如果不空闲,则 分配当前建议的坐席
|
||||
*
|
||||
* @param agentUser
|
||||
* @return
|
||||
* @throws Exception
|
||||
*/
|
||||
public AgentService processChatbotService(final String botName, final AgentUser agentUser) {
|
||||
AgentService agentService = new AgentService(); //放入缓存的对象
|
||||
Date now = new Date();
|
||||
if (StringUtils.isNotBlank(agentUser.getAgentserviceid())) {
|
||||
agentService = agentServiceRes.findById(agentUser.getAgentserviceid()).orElse(null);
|
||||
agentService.setEndtime(now);
|
||||
if (agentService.getServicetime() != null) {
|
||||
agentService.setSessiontimes(System.currentTimeMillis() - agentService.getServicetime().getTime());
|
||||
}
|
||||
agentService.setStatus(MainContext.AgentUserStatusEnum.END.toString());
|
||||
} else {
|
||||
agentService.setServicetime(now);
|
||||
agentService.setLogindate(now);
|
||||
agentService.setOwner(agentUser.getContextid());
|
||||
agentService.setSessionid(agentUser.getSessionid());
|
||||
agentService.setRegion(agentUser.getRegion());
|
||||
agentService.setUsername(agentUser.getUsername());
|
||||
agentService.setChanneltype(agentUser.getChanneltype());
|
||||
if (botName != null) {
|
||||
agentService.setAgentusername(botName);
|
||||
}
|
||||
|
||||
if (StringUtils.isNotBlank(agentUser.getContextid())) {
|
||||
agentService.setContextid(agentUser.getContextid());
|
||||
} else {
|
||||
agentService.setContextid(agentUser.getSessionid());
|
||||
}
|
||||
|
||||
agentService.setUserid(agentUser.getUserid());
|
||||
agentService.setAiid(agentUser.getAgentno());
|
||||
agentService.setAiservice(true);
|
||||
agentService.setStatus(MainContext.AgentUserStatusEnum.INSERVICE.toString());
|
||||
|
||||
agentService.setAppid(agentUser.getAppid());
|
||||
agentService.setLeavemsg(false);
|
||||
}
|
||||
|
||||
agentServiceRes.save(agentService);
|
||||
return agentService;
|
||||
}
|
||||
|
||||
}
|
@ -1,397 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd.
|
||||
* <https://www.chatopera.com>, Licensed under the Chunsong Public
|
||||
* License, Version 1.0 (the "License"), https://docs.cskefu.com/licenses/v1.html
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* Copyright (C) 2019-2022 Chatopera Inc, <https://www.chatopera.com>,
|
||||
* Licensed under the Apache License, Version 2.0,
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*/
|
||||
|
||||
package com.cskefu.cc.acd;
|
||||
|
||||
import com.cskefu.cc.basic.MainContext;
|
||||
import com.cskefu.cc.basic.MainUtils;
|
||||
import com.cskefu.cc.cache.Cache;
|
||||
import com.cskefu.cc.model.*;
|
||||
import com.cskefu.cc.persistence.repository.*;
|
||||
import com.cskefu.cc.proxy.OrganProxy;
|
||||
import com.cskefu.cc.util.HashMapUtils;
|
||||
import com.cskefu.cc.util.WebIMReport;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* 坐席自动分配策略集
|
||||
*/
|
||||
@Component
|
||||
public class ACDPolicyService {
|
||||
private final static Logger logger = LoggerFactory.getLogger(ACDPolicyService.class);
|
||||
|
||||
@Autowired
|
||||
private Cache cache;
|
||||
|
||||
@Autowired
|
||||
private UserRepository userRes;
|
||||
|
||||
@Autowired
|
||||
private SessionConfigRepository sessionConfigRes;
|
||||
|
||||
@Autowired
|
||||
private PassportWebIMUserRepository onlineUserRes;
|
||||
|
||||
@Autowired
|
||||
private AgentUserRepository agentUserRes;
|
||||
|
||||
@Autowired
|
||||
private ChannelRepository snsAccountRes;
|
||||
|
||||
@Autowired
|
||||
private OrganProxy organProxy;
|
||||
|
||||
/**
|
||||
* 载入坐席 ACD策略配置
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public List<SessionConfig> initSessionConfigList() {
|
||||
List<SessionConfig> sessionConfigList;
|
||||
if ((sessionConfigList = cache.findOneSessionConfigList()) == null) {
|
||||
sessionConfigList = sessionConfigRes.findAll();
|
||||
if (sessionConfigList != null && sessionConfigList.size() > 0) {
|
||||
cache.putSessionConfigList(sessionConfigList);
|
||||
}
|
||||
}
|
||||
return sessionConfigList;
|
||||
}
|
||||
|
||||
/**
|
||||
* 载入坐席 ACD策略配置
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public SessionConfig initSessionConfig(String organid) {
|
||||
SessionConfig sessionConfig;
|
||||
if ((sessionConfig = cache.findOneSessionConfig(organid)) == null) {
|
||||
sessionConfig = sessionConfigRes.findBySkill(organid);
|
||||
if (sessionConfig == null) {
|
||||
sessionConfig = new SessionConfig();
|
||||
} else {
|
||||
cache.putSessionConfig(sessionConfig, organid);
|
||||
}
|
||||
}
|
||||
|
||||
return sessionConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* 确定AgentStatus:空闲坐席优先
|
||||
*
|
||||
* @param agentStatuses
|
||||
* @return
|
||||
*/
|
||||
public AgentStatus decideAgentStatusWithIdleAgent(final List<AgentStatus> agentStatuses) {
|
||||
for (final AgentStatus o : agentStatuses) {
|
||||
if (o.getUsers() == 0) {
|
||||
logger.info("[decideAgentStatusWithIdleAgent] choose agentno {} by idle status.", o.getAgentno());
|
||||
return o;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 确定AgentStatus:坐席平均分配
|
||||
*
|
||||
* @param agentStatuses
|
||||
* @return
|
||||
*/
|
||||
public AgentStatus decideAgentStatusInAverage(final List<AgentStatus> agentStatuses) {
|
||||
// 查找最少人数的AgentStatus
|
||||
AgentStatus x = agentStatuses.stream().min(Comparator.comparingInt(AgentStatus::getUsers)).get();
|
||||
|
||||
if (x != null) {
|
||||
logger.info("[decideAgentStatusWithIdleAgent] choose agentno {} in average.", x.getAgentno());
|
||||
}
|
||||
|
||||
return x;
|
||||
}
|
||||
|
||||
/**
|
||||
* 过滤就绪坐席
|
||||
* 优先级: 1. 指定坐席;2. 指定技能组; 3. 租户所有的坐席
|
||||
*
|
||||
* @param agentUser
|
||||
* @return
|
||||
*/
|
||||
public List<AgentStatus> filterOutAvailableAgentStatus(
|
||||
final AgentUser agentUser,
|
||||
final SessionConfig sessionConfig) {
|
||||
logger.info(
|
||||
"[filterOutAvailableAgentStatus] pre-conditions: agentUser.agentno {}, skill {}, onlineUser {}",
|
||||
agentUser.getAgentno(), agentUser.getSkill(), agentUser.getUserid()
|
||||
);
|
||||
List<AgentStatus> agentStatuses = new ArrayList<>();
|
||||
Map<String, AgentStatus> map = cache.findAllReadyAgentStatus();
|
||||
|
||||
// DEBUG
|
||||
if (map.size() > 0) {
|
||||
StringBuffer sb = new StringBuffer();
|
||||
sb.append("[filterOutAvailableAgentStatus] ready agents online: \n");
|
||||
for (final Map.Entry<String, AgentStatus> f : map.entrySet()) {
|
||||
sb.append(
|
||||
String.format(" name %s, agentno %s, service %d/%d, status %s, busy %s, skills %s \n",
|
||||
f.getValue().getUsername(),
|
||||
f.getValue().getAgentno(), f.getValue().getUsers(), f.getValue().getMaxusers(),
|
||||
f.getValue().getStatus(), f.getValue().isBusy(),
|
||||
HashMapUtils.concatKeys(f.getValue().getSkills(), "|")));
|
||||
}
|
||||
logger.info(sb.toString());
|
||||
} else {
|
||||
logger.info("[filterOutAvailableAgentStatus] None ready agent found.");
|
||||
}
|
||||
|
||||
if (agentUser != null && StringUtils.isNotBlank(agentUser.getAgentno())) {
|
||||
User user = userRes.findById(agentUser.getAgentno()).orElse(null);
|
||||
if (user != null && !user.isSuperadmin()) {
|
||||
// 用户不为空,并且不是超级管理员
|
||||
// 指定坐席
|
||||
for (final Map.Entry<String, AgentStatus> entry : map.entrySet()) {
|
||||
// 被指定的坐席,不检查是否忙,是否达到最大接待数量
|
||||
if (StringUtils.equals(
|
||||
entry.getValue().getAgentno(), agentUser.getAgentno())) {
|
||||
agentStatuses.add(entry.getValue());
|
||||
logger.info(
|
||||
"[filterOutAvailableAgentStatus] <Agent> find ready agent {}, name {}, status {}, service {}/{}",
|
||||
entry.getValue().getAgentno(), entry.getValue().getUsername(), entry.getValue().getStatus(),
|
||||
entry.getValue().getUsers(),
|
||||
entry.getValue().getMaxusers());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 此处size是1或0
|
||||
if (agentStatuses.size() == 1) {
|
||||
logger.info("[filterOutAvailableAgentStatus] agent status list size: {}", agentStatuses.size());
|
||||
// 得到指定的坐席
|
||||
return filterOutAgentStatusBySkipSuperAdmin(agentStatuses);
|
||||
}
|
||||
|
||||
// Note 如果指定了坐席,但是该坐席却不是就绪的,那么就根据技能组或其它条件查找
|
||||
|
||||
/**
|
||||
* 指定坐席未查询到就绪的
|
||||
*/
|
||||
if (StringUtils.isNotBlank(agentUser.getSkill())) {
|
||||
// 指定技能组
|
||||
for (final Map.Entry<String, AgentStatus> entry : map.entrySet()) {
|
||||
if ((!entry.getValue().isBusy()) &&
|
||||
(getAgentUsersBySkill(entry.getValue(), agentUser.getSkill()) < sessionConfig.getMaxuser()) &&
|
||||
(entry.getValue().getSkills() != null &&
|
||||
entry.getValue().getSkills().containsKey(agentUser.getSkill()))) {
|
||||
logger.info(
|
||||
"[filterOutAvailableAgentStatus] <Skill#{}> find ready agent {}, name {}, status {}, service {}/{}, skills {}",
|
||||
agentUser.getSkill(),
|
||||
entry.getValue().getAgentno(), entry.getValue().getUsername(), entry.getValue().getStatus(),
|
||||
entry.getValue().getUsers(),
|
||||
entry.getValue().getMaxusers(),
|
||||
HashMapUtils.concatKeys(entry.getValue().getSkills(), "|"));
|
||||
agentStatuses.add(entry.getValue());
|
||||
} else {
|
||||
logger.info(
|
||||
"[filterOutAvailableAgentStatus] <Skill#{}> skip ready agent {}, name {}, status {}, service {}/{}, skills {}",
|
||||
agentUser.getSkill(),
|
||||
entry.getValue().getAgentno(), entry.getValue().getUsername(), entry.getValue().getStatus(),
|
||||
entry.getValue().getUsers(),
|
||||
entry.getValue().getMaxusers(),
|
||||
HashMapUtils.concatKeys(entry.getValue().getSkills(), "|"));
|
||||
}
|
||||
}
|
||||
// 如果绑定了技能组,立即返回该技能组的人
|
||||
// 这时候,如果该技能组没有人,也不按照其它条件查找
|
||||
logger.info("[filterOutAvailableAgentStatus] agent status list size: {}", agentStatuses.size());
|
||||
return filterOutAgentStatusBySkipSuperAdmin(agentStatuses);
|
||||
} else {
|
||||
/**
|
||||
* 在指定的坐席和技能组中未查到坐席
|
||||
* 接下来进行无差别查询
|
||||
*
|
||||
* TODO 指定技能组无用户,停止分配
|
||||
*/
|
||||
Channel channel = snsAccountRes.findBySnsid(agentUser.getAppid()).get();
|
||||
Map<String, Organ> allOrgan = organProxy.findAllOrganByParentId(channel.getOrgan());
|
||||
// allOrgan.keySet().retainAll
|
||||
|
||||
// 对于该租户的所有客服
|
||||
for (final Map.Entry<String, AgentStatus> entry : map.entrySet()) {
|
||||
Set<String> agentSkills = entry.getValue().getSkills().keySet();
|
||||
agentSkills.retainAll(allOrgan.keySet());
|
||||
|
||||
if ((!entry.getValue().isBusy()) && (entry.getValue().getUsers() < sessionConfig.getMaxuser()) && agentSkills.size() > 0) {
|
||||
agentStatuses.add(entry.getValue());
|
||||
logger.info(
|
||||
"[filterOutAvailableAgentStatus] <Redundance> find ready agent {}, agentname {}, status {}, service {}/{}, skills {}",
|
||||
entry.getValue().getAgentno(), entry.getValue().getUsername(), entry.getValue().getStatus(),
|
||||
entry.getValue().getUsers(),
|
||||
entry.getValue().getMaxusers(),
|
||||
HashMapUtils.concatKeys(entry.getValue().getSkills(), "|"));
|
||||
} else {
|
||||
logger.info(
|
||||
"[filterOutAvailableAgentStatus] <Redundance> skip ready agent {}, name {}, status {}, service {}/{}, skills {}",
|
||||
entry.getValue().getAgentno(), entry.getValue().getUsername(), entry.getValue().getStatus(),
|
||||
entry.getValue().getUsers(),
|
||||
entry.getValue().getMaxusers(),
|
||||
HashMapUtils.concatKeys(entry.getValue().getSkills(), "|"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
logger.info("[filterOutAvailableAgentStatus] agent status list size: {}", agentStatuses.size());
|
||||
return filterOutAgentStatusBySkipSuperAdmin(agentStatuses);
|
||||
}
|
||||
|
||||
/**
|
||||
* 过滤超级管理员
|
||||
*
|
||||
* @param agentStatuses
|
||||
* @return
|
||||
*/
|
||||
private List<AgentStatus> filterOutAgentStatusBySkipSuperAdmin(final List<AgentStatus> agentStatuses) {
|
||||
List<AgentStatus> result = new ArrayList<>();
|
||||
List<String> uids = new ArrayList<>();
|
||||
HashMap<String, User> userMap = new HashMap<>();
|
||||
|
||||
for (final AgentStatus as : agentStatuses) {
|
||||
if (StringUtils.isNotBlank(as.getUserid()))
|
||||
uids.add(as.getUserid());
|
||||
}
|
||||
|
||||
List<User> users = userRes.findByIdIn(uids);
|
||||
|
||||
for (final User u : users) {
|
||||
userMap.put(u.getId(), u);
|
||||
}
|
||||
|
||||
for (final AgentStatus as : agentStatuses) {
|
||||
if (userMap.containsKey(as.getUserid())) {
|
||||
if (!userMap.get(as.getUserid()).isSuperadmin())
|
||||
result.add(as);
|
||||
}
|
||||
}
|
||||
|
||||
logger.info("[filterOutAgentStatusBySkipSuperAdmin] agent status list size: {}", agentStatuses.size());
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 根据坐席配置的策略输出符合要求的AgentStatus,确定最终的坐席
|
||||
*
|
||||
* @param sessionConfig
|
||||
* @param agentStatuses
|
||||
* @return
|
||||
*/
|
||||
public AgentStatus filterOutAgentStatusWithPolicies(
|
||||
final SessionConfig sessionConfig,
|
||||
final List<AgentStatus> agentStatuses,
|
||||
final String onlineUserId,
|
||||
final boolean isInvite) {
|
||||
AgentStatus agentStatus = null;
|
||||
|
||||
// 过滤后没有就绪的满足条件的坐席
|
||||
if (agentStatuses.size() == 0) {
|
||||
return agentStatus;
|
||||
}
|
||||
|
||||
// 邀请功能
|
||||
if (isInvite) {
|
||||
logger.info("[filterOutAgentStatusWithPolicies] is invited onlineUser.");
|
||||
if (agentStatuses.size() == 1) {
|
||||
agentStatus = agentStatuses.get(0);
|
||||
// Note: 如何该邀请人离线了,恰巧只有一个其它就绪坐席,也会进入这种条件。
|
||||
logger.info(
|
||||
"[filterOutAgentStatusWithPolicies] resolve agent as the invitee {}.",
|
||||
agentStatus.getAgentno());
|
||||
}
|
||||
// 邀请功能,但是agentStatuses大小不是1,则进入后续决策
|
||||
}
|
||||
|
||||
// 启用历史坐席优先
|
||||
if ((agentStatus == null) && sessionConfig.isLastagent()) {
|
||||
logger.info("[filterOutAgentStatusWithPolicies] check agent against chat history.");
|
||||
// 启用了历史坐席优先 , 查找 历史服务坐席
|
||||
List<WebIMReport> webIMaggs = MainUtils.getWebIMDataAgg(
|
||||
onlineUserRes.findBySkillForDistinctAgent(sessionConfig.getSkill(), onlineUserId));
|
||||
for (WebIMReport report : webIMaggs) {
|
||||
for (final AgentStatus o : agentStatuses) {
|
||||
if (StringUtils.equals(
|
||||
o.getAgentno(), report.getData()) && getAgentUsersBySkill(o, sessionConfig.getSkill()) < sessionConfig.getMaxuser()) {
|
||||
agentStatus = o;
|
||||
logger.info(
|
||||
"[filterOutAgentStatusWithPolicies] choose agentno {} by chat history.",
|
||||
agentStatus.getAgentno());
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (agentStatus != null) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 新客服接入人工坐席分配策略
|
||||
if (agentStatus == null) {
|
||||
// 设置默认为空闲坐席优先
|
||||
if (StringUtils.isBlank(sessionConfig.getDistribution())) {
|
||||
sessionConfig.setDistribution("0");
|
||||
}
|
||||
|
||||
switch (sessionConfig.getDistribution()) {
|
||||
case "0":
|
||||
// 空闲坐席优先
|
||||
agentStatus = decideAgentStatusWithIdleAgent(agentStatuses);
|
||||
if (agentStatus == null) {
|
||||
// 如果没有空闲坐席,则按照平均分配
|
||||
agentStatus = decideAgentStatusInAverage(agentStatuses);
|
||||
}
|
||||
break;
|
||||
case "1":
|
||||
// 坐席平均分配
|
||||
agentStatus = decideAgentStatusInAverage(agentStatuses);
|
||||
break;
|
||||
default:
|
||||
logger.warn(
|
||||
"[filterOutAgentStatusWithPolicies] unexpected Distribution Strategy 【{}】",
|
||||
sessionConfig.getDistribution());
|
||||
}
|
||||
}
|
||||
|
||||
if (agentStatus != null) {
|
||||
logger.info(
|
||||
"[filterOutAgentStatusWithPolicies] final agentStatus {}, agentno {}", agentStatus.getId(),
|
||||
agentStatus.getAgentno());
|
||||
} else {
|
||||
logger.info("[filterOutAgentStatusWithPolicies] oops, no agent satisfy rules.");
|
||||
}
|
||||
|
||||
return agentStatus;
|
||||
}
|
||||
|
||||
public int getAgentUsersBySkill(AgentStatus agentStatus, String skill) {
|
||||
return agentUserRes.countByAgentnoAndStatusAndSkill(agentStatus.getAgentno(), MainContext.AgentUserStatusEnum.INSERVICE.toString(), skill);
|
||||
}
|
||||
|
||||
}
|
@ -1,59 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd.
|
||||
* <https://www.chatopera.com>, Licensed under the Chunsong Public
|
||||
* License, Version 1.0 (the "License"), https://docs.cskefu.com/licenses/v1.html
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* Copyright (C) 2019-2022 Chatopera Inc, <https://www.chatopera.com>,
|
||||
* Licensed under the Apache License, Version 2.0,
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*/
|
||||
|
||||
package com.cskefu.cc.acd;
|
||||
|
||||
import com.cskefu.cc.cache.Cache;
|
||||
import com.cskefu.cc.model.AgentUser;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
@Component
|
||||
public class ACDQueueService {
|
||||
private final static Logger logger = LoggerFactory.getLogger(ACDQueueService.class);
|
||||
|
||||
|
||||
@Autowired
|
||||
private Cache cache;
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public int getQueueIndex(String agent, String skill) {
|
||||
int queneUsers = 0;
|
||||
Map<String, AgentUser> map = cache.getAgentUsersInQue();
|
||||
|
||||
for (final Map.Entry<String, AgentUser> entry : map.entrySet()) {
|
||||
if (StringUtils.isNotBlank(skill)) {
|
||||
if (StringUtils.equals(entry.getValue().getSkill(), skill)) {
|
||||
queneUsers++;
|
||||
}
|
||||
continue;
|
||||
} else {
|
||||
if (StringUtils.isNotBlank(agent)) {
|
||||
if (StringUtils.equals(entry.getValue().getAgentno(), agent)) {
|
||||
queneUsers++;
|
||||
}
|
||||
continue;
|
||||
} else {
|
||||
queneUsers++;
|
||||
}
|
||||
}
|
||||
}
|
||||
return queneUsers;
|
||||
}
|
||||
}
|
@ -1,68 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd.
|
||||
* <https://www.chatopera.com>, Licensed under the Chunsong Public
|
||||
* License, Version 1.0 (the "License"), https://docs.cskefu.com/licenses/v1.html
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* Copyright (C) 2019-2022 Chatopera Inc, <https://www.chatopera.com>,
|
||||
* Licensed under the Apache License, Version 2.0,
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*/
|
||||
package com.cskefu.cc.acd;
|
||||
|
||||
import com.cskefu.cc.basic.MainContext;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Automatic Call Distribution Main Entry
|
||||
* ACD服务路由得到子服务
|
||||
*/
|
||||
public class ACDServiceRouter {
|
||||
private final static Logger logger = LoggerFactory.getLogger(ACDServiceRouter.class);
|
||||
|
||||
private static ACDChatbotService acdChatbotService;
|
||||
|
||||
// 坐席服务
|
||||
private static ACDAgentService acdAgentService;
|
||||
|
||||
private static ACDPolicyService acdPolicyService;
|
||||
|
||||
private static ACDWorkMonitor acdWorkMonitor;
|
||||
|
||||
|
||||
public static ACDPolicyService getAcdPolicyService() {
|
||||
if (acdPolicyService == null) {
|
||||
acdPolicyService = MainContext.getContext().getBean(ACDPolicyService.class);
|
||||
}
|
||||
|
||||
return acdPolicyService;
|
||||
}
|
||||
|
||||
public static ACDAgentService getAcdAgentService() {
|
||||
if (acdAgentService == null) {
|
||||
acdAgentService = MainContext.getContext().getBean(ACDAgentService.class);
|
||||
}
|
||||
|
||||
return acdAgentService;
|
||||
}
|
||||
|
||||
|
||||
public static ACDChatbotService getAcdChatbotService() {
|
||||
if (acdChatbotService == null) {
|
||||
acdChatbotService = MainContext.getContext().getBean(ACDChatbotService.class);
|
||||
}
|
||||
return acdChatbotService;
|
||||
}
|
||||
|
||||
public static ACDWorkMonitor getAcdWorkMonitor() {
|
||||
if (acdWorkMonitor == null) {
|
||||
acdWorkMonitor = MainContext.getContext().getBean(ACDWorkMonitor.class);
|
||||
}
|
||||
return acdWorkMonitor;
|
||||
|
||||
}
|
||||
}
|
@ -1,107 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd.
|
||||
* <https://www.chatopera.com>, Licensed under the Chunsong Public
|
||||
* License, Version 1.0 (the "License"), https://docs.cskefu.com/licenses/v1.html
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* Copyright (C) 2019-2022 Chatopera Inc, <https://www.chatopera.com>,
|
||||
* Licensed under the Apache License, Version 2.0,
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*/
|
||||
package com.cskefu.cc.acd;
|
||||
|
||||
import com.chatopera.compose4j.Composer;
|
||||
import com.chatopera.compose4j.exception.Compose4jRuntimeException;
|
||||
import com.cskefu.cc.acd.basic.ACDComposeContext;
|
||||
import com.cskefu.cc.acd.basic.IACDDispatcher;
|
||||
import com.cskefu.cc.acd.middleware.visitor.*;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import jakarta.annotation.PostConstruct;
|
||||
|
||||
/**
|
||||
* 处置访客分配
|
||||
*/
|
||||
@Component
|
||||
public class ACDVisitorDispatcher implements IACDDispatcher {
|
||||
private final static Logger logger = LoggerFactory.getLogger(ACDVisitorDispatcher.class);
|
||||
|
||||
/**
|
||||
* 为访客安排坐席
|
||||
*/
|
||||
private Composer<ACDComposeContext> pipleline;
|
||||
|
||||
@Autowired
|
||||
private ACDVisBodyParserMw acdVisBodyParserMw;
|
||||
|
||||
@Autowired
|
||||
private ACDVisBindingMw acdVisBindingMw;
|
||||
|
||||
@Autowired
|
||||
private ACDVisSessionCfgMw acdVisSessionCfgMw;
|
||||
|
||||
@Autowired
|
||||
private ACDVisServiceMw acdVisServiceMw;
|
||||
|
||||
@Autowired
|
||||
private ACDVisAllocatorMw acdVisAllocatorMw;
|
||||
|
||||
@PostConstruct
|
||||
private void setup() {
|
||||
logger.info("[setup] setup ACD Visitor Dispatch Service ...");
|
||||
buildPipeline();
|
||||
}
|
||||
|
||||
/**
|
||||
* 建立访客处理管道
|
||||
*/
|
||||
private void buildPipeline() {
|
||||
pipleline = new Composer<>();
|
||||
|
||||
/**
|
||||
* 1) 设置基本信息
|
||||
*/
|
||||
pipleline.use(acdVisBodyParserMw);
|
||||
|
||||
/**
|
||||
* 1) 绑定技能组或坐席(包括邀请时的坐席)
|
||||
*/
|
||||
pipleline.use(acdVisBindingMw);
|
||||
|
||||
/**
|
||||
* 1) 坐席配置:工作时间段,有无就绪在线坐席
|
||||
*
|
||||
*/
|
||||
pipleline.use(acdVisSessionCfgMw);
|
||||
|
||||
/**
|
||||
* 1)选择坐席,确定AgentService
|
||||
*/
|
||||
pipleline.use(acdVisServiceMw);
|
||||
|
||||
/**
|
||||
* 1)根据策略筛选坐席
|
||||
*/
|
||||
pipleline.use(acdVisAllocatorMw);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void enqueue(final ACDComposeContext ctx) {
|
||||
try {
|
||||
pipleline.handle(ctx);
|
||||
} catch (Compose4jRuntimeException e) {
|
||||
logger.error("[enqueueVisitor] error", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dequeue(ACDComposeContext ctx) {
|
||||
|
||||
}
|
||||
}
|
@ -1,194 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd.
|
||||
* <https://www.chatopera.com>, Licensed under the Chunsong Public
|
||||
* License, Version 1.0 (the "License"), https://docs.cskefu.com/licenses/v1.html
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* Copyright (C) 2019-2022 Chatopera Inc, <https://www.chatopera.com>,
|
||||
* Licensed under the Apache License, Version 2.0,
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*/
|
||||
package com.cskefu.cc.acd;
|
||||
|
||||
import com.cskefu.cc.basic.MainContext;
|
||||
import com.cskefu.cc.basic.MainUtils;
|
||||
import com.cskefu.cc.cache.Cache;
|
||||
import com.cskefu.cc.model.AgentReport;
|
||||
import com.cskefu.cc.model.AgentStatus;
|
||||
import com.cskefu.cc.model.Organ;
|
||||
import com.cskefu.cc.model.WorkMonitor;
|
||||
import com.cskefu.cc.persistence.repository.AgentServiceRepository;
|
||||
import com.cskefu.cc.persistence.repository.AgentUserRepository;
|
||||
import com.cskefu.cc.persistence.repository.WorkMonitorRepository;
|
||||
import com.cskefu.cc.proxy.OrganProxy;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Component
|
||||
public class ACDWorkMonitor {
|
||||
private final static Logger logger = LoggerFactory.getLogger(ACDWorkMonitor.class);
|
||||
|
||||
@Autowired
|
||||
private WorkMonitorRepository workMonitorRes;
|
||||
|
||||
@Autowired
|
||||
private Cache cache;
|
||||
|
||||
@Autowired
|
||||
private OrganProxy organProxy;
|
||||
|
||||
@Autowired
|
||||
private AgentServiceRepository agentServiceRes;
|
||||
|
||||
@Autowired
|
||||
private AgentUserRepository agentUserRes;
|
||||
|
||||
/**
|
||||
* 获得 当前服务状态
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@SuppressWarnings({"unchecked", "rawtypes"})
|
||||
public AgentReport getAgentReport() {
|
||||
return getAgentReport(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获得一个技能组的坐席状态
|
||||
*
|
||||
* @param organ
|
||||
* @return
|
||||
*/
|
||||
public AgentReport getAgentReport(String organ) {
|
||||
/**
|
||||
* 统计当前在线的坐席数量
|
||||
*/
|
||||
AgentReport report = new AgentReport();
|
||||
|
||||
Map<String, AgentStatus> readys = cache.getAgentStatusReady();
|
||||
|
||||
int readyNum = 0;
|
||||
int busyNum = 0;
|
||||
|
||||
for (Map.Entry<String, AgentStatus> entry : readys.entrySet()) {
|
||||
if (organ == null) {
|
||||
readyNum++;
|
||||
if (entry.getValue().isBusy()) {
|
||||
busyNum++;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (entry.getValue().getSkills() != null &&
|
||||
entry.getValue().getSkills().containsKey(organ)) {
|
||||
readyNum++;
|
||||
if (entry.getValue().isBusy()) {
|
||||
busyNum++;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
report.setAgents(readyNum);
|
||||
report.setBusy(busyNum);
|
||||
|
||||
/**
|
||||
* 统计当前服务中的用户数量
|
||||
*/
|
||||
|
||||
if (organ != null) {
|
||||
Organ currentOrgan = new Organ();
|
||||
currentOrgan.setId(organ);
|
||||
Map<String, Organ> organs = organProxy.findAllOrganByParent(currentOrgan);
|
||||
|
||||
report.setUsers(agentServiceRes.countByStatusAndAgentskillIn(MainContext.AgentUserStatusEnum.INSERVICE.toString(), organs.keySet()));
|
||||
report.setInquene(agentUserRes.countByStatusAndSkillIn(MainContext.AgentUserStatusEnum.INQUENE.toString(), organs.keySet()));
|
||||
} else {
|
||||
// 服务中
|
||||
report.setUsers(cache.getInservAgentUsersSize());
|
||||
// 等待中
|
||||
report.setInquene(cache.getInqueAgentUsersSize());
|
||||
}
|
||||
|
||||
// DEBUG
|
||||
logger.info(
|
||||
"[getAgentReport] organ {}, agents {}, busy {}, users {}, inqueue {}", organ,
|
||||
report.getAgents(), report.getBusy(), report.getUsers(), report.getInquene()
|
||||
);
|
||||
return report;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param agent 坐席
|
||||
* @param userid 用户ID
|
||||
* @param status 工作状态,也就是上一个状态
|
||||
* @param current 下一个工作状态
|
||||
* @param worktype 类型 : 语音OR 文本
|
||||
* @param lasttime
|
||||
*/
|
||||
public void recordAgentStatus(
|
||||
String agent,
|
||||
String username,
|
||||
String extno,
|
||||
boolean admin,
|
||||
String userid,
|
||||
String status,
|
||||
String current,
|
||||
String worktype,
|
||||
Date lasttime
|
||||
) {
|
||||
WorkMonitor workMonitor = new WorkMonitor();
|
||||
if (StringUtils.isNotBlank(agent) && StringUtils.isNotBlank(status)) {
|
||||
workMonitor.setAgent(agent);
|
||||
workMonitor.setAgentno(agent);
|
||||
workMonitor.setStatus(status);
|
||||
workMonitor.setAdmin(admin);
|
||||
workMonitor.setUsername(username);
|
||||
workMonitor.setExtno(extno);
|
||||
workMonitor.setWorktype(worktype);
|
||||
if (lasttime != null) {
|
||||
workMonitor.setDuration((int) (System.currentTimeMillis() - lasttime.getTime()) / 1000);
|
||||
}
|
||||
if (status.equals(MainContext.AgentStatusEnum.BUSY.toString())) {
|
||||
workMonitor.setBusy(true);
|
||||
}
|
||||
if (status.equals(MainContext.AgentStatusEnum.READY.toString())) {
|
||||
int count = workMonitorRes.countByAgentAndDatestrAndStatus(
|
||||
agent, MainUtils.simpleDateFormat.format(new Date()),
|
||||
MainContext.AgentStatusEnum.READY.toString()
|
||||
);
|
||||
if (count == 0) {
|
||||
workMonitor.setFirsttime(true);
|
||||
}
|
||||
}
|
||||
if (current.equals(MainContext.AgentStatusEnum.NOTREADY.toString())) {
|
||||
List<WorkMonitor> workMonitorList = workMonitorRes.findByAgentAndDatestrAndFirsttime(agent, MainUtils.simpleDateFormat.format(new Date()), true);
|
||||
if (workMonitorList.size() > 0) {
|
||||
WorkMonitor firstWorkMonitor = workMonitorList.get(0);
|
||||
if (firstWorkMonitor.getFirsttimes() == 0) {
|
||||
firstWorkMonitor.setFirsttimes(
|
||||
(int) (System.currentTimeMillis() - firstWorkMonitor.getCreatetime().getTime()));
|
||||
workMonitorRes.save(firstWorkMonitor);
|
||||
}
|
||||
}
|
||||
}
|
||||
workMonitor.setCreatetime(new Date());
|
||||
workMonitor.setDatestr(MainUtils.simpleDateFormat.format(new Date()));
|
||||
|
||||
workMonitor.setName(agent);
|
||||
workMonitor.setUserid(userid);
|
||||
|
||||
workMonitorRes.save(workMonitor);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -1,309 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd.
|
||||
* <https://www.chatopera.com>, Licensed under the Chunsong Public
|
||||
* License, Version 1.0 (the "License"), https://docs.cskefu.com/licenses/v1.html
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* Copyright (C) 2019-2022 Chatopera Inc, <https://www.chatopera.com>,
|
||||
* Licensed under the Apache License, Version 2.0,
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*/
|
||||
|
||||
package com.cskefu.cc.acd.basic;
|
||||
|
||||
import com.cskefu.cc.model.*;
|
||||
import com.cskefu.cc.socketio.message.Message;
|
||||
import com.cskefu.cc.util.IP;
|
||||
|
||||
public class ACDComposeContext extends Message {
|
||||
|
||||
// 技能组及渠道
|
||||
private String organid;
|
||||
private Organ organ;
|
||||
private String appid;
|
||||
private String channeltype;
|
||||
private Channel channel;
|
||||
private String sessionid;
|
||||
|
||||
// 策略
|
||||
private SessionConfig sessionConfig;
|
||||
// 坐席报告
|
||||
private AgentReport agentReport;
|
||||
|
||||
// 机器人客服
|
||||
private String aiid;
|
||||
private boolean isAi;
|
||||
|
||||
// 是否是邀请
|
||||
private boolean isInvite;
|
||||
|
||||
private User agent;
|
||||
private String agentno;
|
||||
private String agentUserId;
|
||||
|
||||
private String agentServiceId;
|
||||
|
||||
private AgentUser agentUser;
|
||||
|
||||
private AgentService agentService;
|
||||
|
||||
// 访客
|
||||
private String onlineUserId;
|
||||
private PassportWebIMUser passportWebIMUser;
|
||||
private String onlineUserNickname;
|
||||
private String onlineUserHeadimgUrl;
|
||||
|
||||
// 其它信息
|
||||
private IP ipdata;
|
||||
private String initiator;
|
||||
private String title;
|
||||
private String url;
|
||||
private String browser;
|
||||
private String osname;
|
||||
private String traceid;
|
||||
private String ownerid;
|
||||
private String ip;
|
||||
|
||||
public String getOrganid() {
|
||||
return organid;
|
||||
}
|
||||
|
||||
public void setOrganid(String organid) {
|
||||
this.organid = organid;
|
||||
}
|
||||
|
||||
public Organ getOrgan() {
|
||||
return organ;
|
||||
}
|
||||
|
||||
public void setOrgan(Organ organ) {
|
||||
this.organ = organ;
|
||||
}
|
||||
|
||||
public String getAppid() {
|
||||
return appid;
|
||||
}
|
||||
|
||||
public void setAppid(String appid) {
|
||||
this.appid = appid;
|
||||
}
|
||||
|
||||
public String getChannelType() {
|
||||
return channeltype;
|
||||
}
|
||||
|
||||
public void setChannelType(String channelType) {
|
||||
this.channeltype = channelType;
|
||||
}
|
||||
|
||||
public Channel getSnsAccount() {
|
||||
return channel;
|
||||
}
|
||||
|
||||
public void setSnsAccount(Channel channel) {
|
||||
this.channel = channel;
|
||||
}
|
||||
|
||||
public SessionConfig getSessionConfig() {
|
||||
return sessionConfig;
|
||||
}
|
||||
|
||||
public void setSessionConfig(SessionConfig sessionConfig) {
|
||||
this.sessionConfig = sessionConfig;
|
||||
}
|
||||
|
||||
public String getAiid() {
|
||||
return aiid;
|
||||
}
|
||||
|
||||
public void setAiid(String aiid) {
|
||||
this.aiid = aiid;
|
||||
}
|
||||
|
||||
public boolean isAi() {
|
||||
return isAi;
|
||||
}
|
||||
|
||||
public void setAi(boolean ai) {
|
||||
isAi = ai;
|
||||
}
|
||||
|
||||
public boolean isInvite() {
|
||||
return isInvite;
|
||||
}
|
||||
|
||||
public void setInvite(boolean invite) {
|
||||
isInvite = invite;
|
||||
}
|
||||
|
||||
public User getAgent() {
|
||||
return agent;
|
||||
}
|
||||
|
||||
public void setAgent(User agent) {
|
||||
this.agent = agent;
|
||||
}
|
||||
|
||||
public String getAgentno() {
|
||||
return agentno;
|
||||
}
|
||||
|
||||
public void setAgentno(String agentno) {
|
||||
this.agentno = agentno;
|
||||
}
|
||||
|
||||
public String getAgentUserId() {
|
||||
return agentUserId;
|
||||
}
|
||||
|
||||
public void setAgentUserId(String agentUserId) {
|
||||
this.agentUserId = agentUserId;
|
||||
}
|
||||
|
||||
public String getOnlineUserId() {
|
||||
return onlineUserId;
|
||||
}
|
||||
|
||||
public void setOnlineUserId(String onlineUserId) {
|
||||
this.onlineUserId = onlineUserId;
|
||||
}
|
||||
|
||||
public String getAgentServiceId() {
|
||||
return agentServiceId;
|
||||
}
|
||||
|
||||
public void setAgentServiceId(String agentServiceId) {
|
||||
this.agentServiceId = agentServiceId;
|
||||
}
|
||||
|
||||
public AgentUser getAgentUser() {
|
||||
return agentUser;
|
||||
}
|
||||
|
||||
public void setAgentUser(AgentUser agentUser) {
|
||||
this.agentUser = agentUser;
|
||||
}
|
||||
|
||||
public PassportWebIMUser getOnlineUser() {
|
||||
return passportWebIMUser;
|
||||
}
|
||||
|
||||
public void setOnlineUser(PassportWebIMUser passportWebIMUser) {
|
||||
this.passportWebIMUser = passportWebIMUser;
|
||||
}
|
||||
|
||||
public AgentService getAgentService() {
|
||||
return agentService;
|
||||
}
|
||||
|
||||
public void setAgentService(AgentService agentService) {
|
||||
this.agentService = agentService;
|
||||
}
|
||||
|
||||
public String getSessionid() {
|
||||
return sessionid;
|
||||
}
|
||||
|
||||
public void setSessionid(String sessionid) {
|
||||
this.sessionid = sessionid;
|
||||
}
|
||||
|
||||
public String getOnlineUserNickname() {
|
||||
return onlineUserNickname;
|
||||
}
|
||||
|
||||
public void setOnlineUserNickname(String onlineUserNickname) {
|
||||
this.onlineUserNickname = onlineUserNickname;
|
||||
}
|
||||
|
||||
public IP getIpdata() {
|
||||
return ipdata;
|
||||
}
|
||||
|
||||
public void setIpdata(IP ipdata) {
|
||||
this.ipdata = ipdata;
|
||||
}
|
||||
|
||||
public String getInitiator() {
|
||||
return initiator;
|
||||
}
|
||||
|
||||
public void setInitiator(String initiator) {
|
||||
this.initiator = initiator;
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
return title;
|
||||
}
|
||||
|
||||
public void setTitle(String title) {
|
||||
this.title = title;
|
||||
}
|
||||
|
||||
public String getUrl() {
|
||||
return url;
|
||||
}
|
||||
|
||||
public void setUrl(String url) {
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
public String getBrowser() {
|
||||
return browser;
|
||||
}
|
||||
|
||||
public void setBrowser(String browser) {
|
||||
this.browser = browser;
|
||||
}
|
||||
|
||||
public String getOsname() {
|
||||
return osname;
|
||||
}
|
||||
|
||||
public void setOsname(String osname) {
|
||||
this.osname = osname;
|
||||
}
|
||||
|
||||
public String getTraceid() {
|
||||
return traceid;
|
||||
}
|
||||
|
||||
public void setTraceid(String traceid) {
|
||||
this.traceid = traceid;
|
||||
}
|
||||
|
||||
public String getOwnerid() {
|
||||
return ownerid;
|
||||
}
|
||||
|
||||
public void setOwnerid(String ownerid) {
|
||||
this.ownerid = ownerid;
|
||||
}
|
||||
|
||||
public String getOnlineUserHeadimgUrl() {
|
||||
return onlineUserHeadimgUrl;
|
||||
}
|
||||
|
||||
public void setOnlineUserHeadimgUrl(String onlineUserHeadimgUrl) {
|
||||
this.onlineUserHeadimgUrl = onlineUserHeadimgUrl;
|
||||
}
|
||||
|
||||
public String getIp() {
|
||||
return ip;
|
||||
}
|
||||
|
||||
public void setIp(String ip) {
|
||||
this.ip = ip;
|
||||
}
|
||||
|
||||
public AgentReport getAgentReport() {
|
||||
return agentReport;
|
||||
}
|
||||
|
||||
public void setAgentReport(AgentReport agentReport) {
|
||||
this.agentReport = agentReport;
|
||||
}
|
||||
}
|
@ -1,228 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd.
|
||||
* <https://www.chatopera.com>, Licensed under the Chunsong Public
|
||||
* License, Version 1.0 (the "License"), https://docs.cskefu.com/licenses/v1.html
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* Copyright (C) 2019-2022 Chatopera Inc, <https://www.chatopera.com>,
|
||||
* Licensed under the Apache License, Version 2.0,
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*/
|
||||
|
||||
package com.cskefu.cc.acd.basic;
|
||||
|
||||
import com.cskefu.cc.acd.ACDPolicyService;
|
||||
import com.cskefu.cc.basic.MainContext;
|
||||
import com.cskefu.cc.model.AgentService;
|
||||
import com.cskefu.cc.model.AgentUser;
|
||||
import com.cskefu.cc.model.SessionConfig;
|
||||
import com.cskefu.cc.util.IP;
|
||||
import com.cskefu.cc.util.IPTools;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class ACDMessageHelper {
|
||||
private final static Logger logger = LoggerFactory.getLogger(ACDMessageHelper.class);
|
||||
|
||||
@Autowired
|
||||
private ACDPolicyService acdPolicyService;
|
||||
|
||||
|
||||
/**
|
||||
* 通过 AgentUser获得ComposeContext
|
||||
*
|
||||
* @param agentUser
|
||||
* @param isInvite
|
||||
* @param initiator
|
||||
* @return
|
||||
*/
|
||||
public ACDComposeContext getComposeContextWithAgentUser(final AgentUser agentUser, final boolean isInvite, final String initiator) {
|
||||
ACDComposeContext ctx = new ACDComposeContext();
|
||||
ctx.setOnlineUserId(agentUser.getUserid());
|
||||
ctx.setOnlineUserNickname(agentUser.getNickname());
|
||||
ctx.setOrganid(agentUser.getSkill());
|
||||
ctx.setChannelType(agentUser.getChanneltype());
|
||||
ctx.setAgentno(agentUser.getAgentno());
|
||||
ctx.setBrowser(agentUser.getBrowser());
|
||||
ctx.setOsname(agentUser.getOsname());
|
||||
ctx.setAppid(agentUser.getAppid());
|
||||
ctx.setTitle(agentUser.getTitle());
|
||||
ctx.setSessionid(agentUser.getSessionid());
|
||||
ctx.setUrl(agentUser.getUrl());
|
||||
ctx.setOwnerid(agentUser.getOwner());
|
||||
|
||||
if (StringUtils.isNotBlank(agentUser.getIpaddr())) {
|
||||
ctx.setIp(agentUser.getIpaddr());
|
||||
// TODO set IP Data
|
||||
ctx.setIpdata(IPTools.getInstance().findGeography(agentUser.getIpaddr()));
|
||||
}
|
||||
|
||||
ctx.setInvite(isInvite);
|
||||
ctx.setInitiator(initiator);
|
||||
|
||||
return ctx;
|
||||
}
|
||||
|
||||
/**
|
||||
* 通知消息内容:分配到坐席
|
||||
*
|
||||
* @param agentService
|
||||
* @param channel
|
||||
* @return
|
||||
*/
|
||||
public String getSuccessMessage(AgentService agentService, String channel) {
|
||||
String queneTip = "<span id='agentno'>" + agentService.getAgentusername() + "</span>";
|
||||
if (!MainContext.ChannelType.WEBIM.toString().equals(channel)) {
|
||||
queneTip = agentService.getAgentusername();
|
||||
}
|
||||
SessionConfig sessionConfig = acdPolicyService.initSessionConfig(agentService.getSkill());
|
||||
String successMsg = "坐席分配成功," + queneTip + "为您服务。";
|
||||
if (StringUtils.isNotBlank(sessionConfig.getSuccessmsg())) {
|
||||
successMsg = sessionConfig.getSuccessmsg().replaceAll("\\{agent\\}", queneTip);
|
||||
}
|
||||
return successMsg;
|
||||
}
|
||||
|
||||
/**
|
||||
* 通知消息内容:和坐席断开
|
||||
*
|
||||
* @param channel
|
||||
* @return
|
||||
*/
|
||||
public String getServiceFinishMessage(String channel, String organid) {
|
||||
SessionConfig sessionConfig = acdPolicyService.initSessionConfig(organid);
|
||||
String queneTip = "坐席已断开和您的对话";
|
||||
if (StringUtils.isNotBlank(sessionConfig.getFinessmsg())) {
|
||||
queneTip = sessionConfig.getFinessmsg();
|
||||
}
|
||||
return queneTip;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 通知消息内容:和坐席断开,刷新页面
|
||||
*
|
||||
* @param channel
|
||||
* @return
|
||||
*/
|
||||
public String getServiceOffMessage(String channel, String organid) {
|
||||
SessionConfig sessionConfig = acdPolicyService.initSessionConfig(organid);
|
||||
String queneTip = "坐席已断开和您的对话,刷新页面为您分配新的坐席";
|
||||
if (StringUtils.isNotBlank(sessionConfig.getFinessmsg())) {
|
||||
queneTip = sessionConfig.getFinessmsg();
|
||||
}
|
||||
return queneTip;
|
||||
}
|
||||
|
||||
public String getNoAgentMessage(int queneIndex, String channel, String organid) {
|
||||
if (queneIndex < 0) {
|
||||
queneIndex = 0;
|
||||
}
|
||||
String queneTip = "<span id='queneindex'>" + queneIndex + "</span>";
|
||||
if (!MainContext.ChannelType.WEBIM.toString().equals(channel)) {
|
||||
queneTip = String.valueOf(queneIndex);
|
||||
}
|
||||
SessionConfig sessionConfig = acdPolicyService.initSessionConfig(organid);
|
||||
String noAgentTipMsg = "坐席全忙,已进入等待队列,您也可以在其他时间再来咨询。";
|
||||
if (StringUtils.isNotBlank(sessionConfig.getNoagentmsg())) {
|
||||
noAgentTipMsg = sessionConfig.getNoagentmsg().replaceAll("\\{num\\}", queneTip);
|
||||
}
|
||||
return noAgentTipMsg;
|
||||
}
|
||||
|
||||
public String getQueneMessage(int queneIndex, String channel, String organid) {
|
||||
|
||||
String queneTip = "<span id='queneindex'>" + queneIndex + "</span>";
|
||||
if (!MainContext.ChannelType.WEBIM.toString().equals(channel)) {
|
||||
queneTip = String.valueOf(queneIndex);
|
||||
}
|
||||
SessionConfig sessionConfig = acdPolicyService.initSessionConfig(organid);
|
||||
String agentBusyTipMsg = "正在排队,请稍候,在您之前,还有 " + queneTip + " 位等待用户。";
|
||||
if (StringUtils.isNotBlank(sessionConfig.getAgentbusymsg())) {
|
||||
agentBusyTipMsg = sessionConfig.getAgentbusymsg().replaceAll("\\{num\\}", queneTip);
|
||||
}
|
||||
return agentBusyTipMsg;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 构建WebIM分发的Context
|
||||
*
|
||||
* @param onlineUserId
|
||||
* @param nickname
|
||||
* @param session
|
||||
* @param appid
|
||||
* @param ip
|
||||
* @param osname
|
||||
* @param browser
|
||||
* @param headimg
|
||||
* @param ipdata
|
||||
* @param channel
|
||||
* @param skill
|
||||
* @param agent
|
||||
* @param title
|
||||
* @param url
|
||||
* @param traceid
|
||||
* @param ownerid
|
||||
* @param isInvite
|
||||
* @param initiator
|
||||
* @return
|
||||
*/
|
||||
public static ACDComposeContext getWebIMComposeContext(
|
||||
final String onlineUserId,
|
||||
final String nickname,
|
||||
final String session,
|
||||
final String appid,
|
||||
final String ip,
|
||||
final String osname,
|
||||
final String browser,
|
||||
final String headimg,
|
||||
final IP ipdata,
|
||||
final String channel,
|
||||
final String skill,
|
||||
final String agent,
|
||||
final String title,
|
||||
final String url,
|
||||
final String traceid,
|
||||
final String ownerid,
|
||||
final boolean isInvite,
|
||||
final String initiator) {
|
||||
logger.info(
|
||||
"[enqueueVisitor] user {}, appid {}, agent {}, skill {}, nickname {}, initiator {}, isInvite {}",
|
||||
onlineUserId,
|
||||
appid,
|
||||
agent,
|
||||
skill,
|
||||
nickname, initiator, isInvite);
|
||||
|
||||
// 坐席服务请求,分配 坐席
|
||||
final ACDComposeContext ctx = new ACDComposeContext();
|
||||
ctx.setOnlineUserId(onlineUserId);
|
||||
ctx.setOnlineUserNickname(nickname);
|
||||
ctx.setOrganid(skill);
|
||||
ctx.setChannelType(channel);
|
||||
ctx.setAgentno(agent);
|
||||
ctx.setBrowser(browser);
|
||||
ctx.setOsname(osname);
|
||||
ctx.setAppid(appid);
|
||||
ctx.setTitle(title);
|
||||
ctx.setSessionid(session);
|
||||
ctx.setUrl(url);
|
||||
ctx.setOnlineUserHeadimgUrl(headimg);
|
||||
ctx.setTraceid(traceid);
|
||||
ctx.setOwnerid(ownerid);
|
||||
ctx.setInitiator(initiator);
|
||||
ctx.setIpdata(ipdata);
|
||||
ctx.setIp(ip);
|
||||
ctx.setInvite(isInvite);
|
||||
return ctx;
|
||||
}
|
||||
|
||||
}
|
@ -1,28 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd.
|
||||
* <https://www.chatopera.com>, Licensed under the Chunsong Public
|
||||
* License, Version 1.0 (the "License"), https://docs.cskefu.com/licenses/v1.html
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* Copyright (C) 2018- Jun. 2023 Chatopera Inc, <https://www.chatopera.com>, Licensed under the Apache License, Version 2.0,
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Copyright (C) 2017 优客服-多渠道客服系统, Licensed under the Apache License, Version 2.0,
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*/
|
||||
package com.cskefu.cc.acd.basic;
|
||||
|
||||
/**
|
||||
* 调度类抽象接口
|
||||
*/
|
||||
public interface IACDDispatcher {
|
||||
|
||||
// 一个目标对象入队
|
||||
void enqueue(final ACDComposeContext ctx);
|
||||
|
||||
// 一个目标对象出队
|
||||
void dequeue(final ACDComposeContext ctx);
|
||||
|
||||
}
|
@ -1,67 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd.
|
||||
* <https://www.chatopera.com>, Licensed under the Chunsong Public
|
||||
* License, Version 1.0 (the "License"), https://docs.cskefu.com/licenses/v1.html
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* Copyright (C) 2019-2022 Chatopera Inc, <https://www.chatopera.com>,
|
||||
* Licensed under the Apache License, Version 2.0,
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*/
|
||||
|
||||
package com.cskefu.cc.acd.middleware.visitor;
|
||||
|
||||
import com.cskefu.cc.acd.ACDAgentService;
|
||||
import com.cskefu.cc.acd.ACDPolicyService;
|
||||
import com.cskefu.cc.acd.basic.ACDComposeContext;
|
||||
import com.cskefu.cc.model.AgentService;
|
||||
import com.cskefu.cc.model.AgentStatus;
|
||||
import com.chatopera.compose4j.Functional;
|
||||
import com.chatopera.compose4j.Middleware;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Component
|
||||
public class ACDVisAllocatorMw implements Middleware<ACDComposeContext> {
|
||||
private final static Logger logger = LoggerFactory.getLogger(ACDVisAllocatorMw.class);
|
||||
|
||||
@Autowired
|
||||
private ACDAgentService acdAgentService;
|
||||
|
||||
@Autowired
|
||||
private ACDPolicyService acdPolicyService;
|
||||
|
||||
@Override
|
||||
public void apply(final ACDComposeContext ctx, final Functional next) {
|
||||
|
||||
/**
|
||||
* 查询条件,当前在线的 坐席,并且 未达到最大 服务人数的坐席
|
||||
*/
|
||||
final List<AgentStatus> agentStatuses = acdPolicyService.filterOutAvailableAgentStatus(
|
||||
ctx.getAgentUser(), ctx.getSessionConfig());
|
||||
|
||||
/**
|
||||
* 处理ACD 的 技能组请求和 坐席请求
|
||||
*/
|
||||
AgentStatus agentStatus = acdPolicyService.filterOutAgentStatusWithPolicies(
|
||||
ctx.getSessionConfig(), agentStatuses, ctx.getOnlineUserId(), ctx.isInvite());
|
||||
|
||||
AgentService agentService = null;
|
||||
try {
|
||||
agentService = acdAgentService.resolveAgentService(
|
||||
agentStatus, ctx.getAgentUser(), false);
|
||||
} catch (Exception ex) {
|
||||
logger.warn("[allotAgent] exception: ", ex);
|
||||
}
|
||||
|
||||
ctx.setAgentService(agentService);
|
||||
}
|
||||
|
||||
}
|
@ -1,86 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd.
|
||||
* <https://www.chatopera.com>, Licensed under the Chunsong Public
|
||||
* License, Version 1.0 (the "License"), https://docs.cskefu.com/licenses/v1.html
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* Copyright (C) 2019-2022 Chatopera Inc, <https://www.chatopera.com>,
|
||||
* Licensed under the Apache License, Version 2.0,
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*/
|
||||
|
||||
package com.cskefu.cc.acd.middleware.visitor;
|
||||
|
||||
import com.cskefu.cc.acd.basic.ACDComposeContext;
|
||||
import com.cskefu.cc.model.Organ;
|
||||
import com.cskefu.cc.model.User;
|
||||
import com.cskefu.cc.persistence.repository.OrganRepository;
|
||||
import com.cskefu.cc.persistence.repository.UserRepository;
|
||||
import com.chatopera.compose4j.Functional;
|
||||
import com.chatopera.compose4j.Middleware;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class ACDVisBindingMw implements Middleware<ACDComposeContext> {
|
||||
|
||||
private final static Logger logger = LoggerFactory.getLogger(ACDVisBindingMw.class);
|
||||
|
||||
@Autowired
|
||||
private UserRepository userRes;
|
||||
|
||||
@Autowired
|
||||
private OrganRepository organRes;
|
||||
|
||||
/**
|
||||
* 绑定技能组或坐席
|
||||
*
|
||||
* @param ctx
|
||||
* @param next
|
||||
*/
|
||||
@Override
|
||||
public void apply(final ACDComposeContext ctx, final Functional next) {
|
||||
/**
|
||||
* 访客新上线的请求
|
||||
*/
|
||||
/**
|
||||
* 技能组 和 坐席
|
||||
*/
|
||||
if (StringUtils.isNotBlank(ctx.getOrganid())) {
|
||||
logger.info("[apply] bind skill {}", ctx.getOrganid());
|
||||
// 绑定技能组
|
||||
Organ organ = organRes.findById(ctx.getOrganid()).orElse(null);
|
||||
if (organ != null) {
|
||||
ctx.getAgentUser().setSkill(organ.getId());
|
||||
ctx.setOrgan(organ);
|
||||
}
|
||||
} else {
|
||||
// 如果没有绑定技能组,则清除之前的标记
|
||||
ctx.getAgentUser().setSkill(null);
|
||||
}
|
||||
|
||||
if (StringUtils.isNotBlank(ctx.getAgentno()) && (!StringUtils.equalsIgnoreCase(ctx.getAgentno(), "null"))) {
|
||||
logger.info("[apply] bind agentno {}, isInvite {}", ctx.getAgentno(), ctx.isInvite());
|
||||
// 绑定坐席
|
||||
// 绑定坐席有可能是因为前端展示了技能组和坐席
|
||||
// 也有可能是坐席发送了邀请,该访客接收邀请
|
||||
ctx.getAgentUser().setAgentno(ctx.getAgentno());
|
||||
User agent = userRes.findById(ctx.getAgentno()).orElse(null);
|
||||
ctx.setAgent(agent);
|
||||
ctx.getAgentUser().setAgentname(agent.getUname());
|
||||
} else {
|
||||
// 如果没有绑定坐席,则清除之前的标记
|
||||
ctx.getAgentUser().setAgentno(null);
|
||||
ctx.getAgentUser().setAgentname(null);
|
||||
ctx.setAgent(null);
|
||||
}
|
||||
|
||||
next.apply();
|
||||
}
|
||||
}
|
@ -1,242 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd.
|
||||
* <https://www.chatopera.com>, Licensed under the Chunsong Public
|
||||
* License, Version 1.0 (the "License"), https://docs.cskefu.com/licenses/v1.html
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* Copyright (C) 2019-2022 Chatopera Inc, <https://www.chatopera.com>,
|
||||
* Licensed under the Apache License, Version 2.0,
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*/
|
||||
|
||||
package com.cskefu.cc.acd.middleware.visitor;
|
||||
|
||||
import com.cskefu.cc.acd.ACDQueueService;
|
||||
import com.cskefu.cc.acd.basic.ACDComposeContext;
|
||||
import com.cskefu.cc.acd.basic.ACDMessageHelper;
|
||||
import com.cskefu.cc.basic.MainContext;
|
||||
import com.cskefu.cc.cache.Cache;
|
||||
import com.cskefu.cc.model.AgentUser;
|
||||
import com.cskefu.cc.model.AgentUserContacts;
|
||||
import com.cskefu.cc.model.Contacts;
|
||||
import com.cskefu.cc.persistence.repository.ContactsRepository;
|
||||
import com.cskefu.cc.persistence.repository.AgentUserContactsRepository;
|
||||
import com.cskefu.cc.proxy.AgentStatusProxy;
|
||||
import com.cskefu.cc.proxy.AgentUserProxy;
|
||||
import com.chatopera.compose4j.Functional;
|
||||
import com.chatopera.compose4j.Middleware;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* Resolve AgentUser
|
||||
*/
|
||||
@Component
|
||||
public class ACDVisBodyParserMw implements Middleware<ACDComposeContext> {
|
||||
private final static Logger logger = LoggerFactory.getLogger(ACDVisBodyParserMw.class);
|
||||
|
||||
@Autowired
|
||||
private AgentUserContactsRepository agentUserContactsRes;
|
||||
|
||||
@Autowired
|
||||
private ContactsRepository contactsRes;
|
||||
|
||||
@Autowired
|
||||
private Cache cache;
|
||||
|
||||
@Autowired
|
||||
private AgentUserProxy agentUserProxy;
|
||||
|
||||
@Autowired
|
||||
private AgentStatusProxy agentStatusProxy;
|
||||
|
||||
@Autowired
|
||||
private ACDQueueService acdQueueService;
|
||||
|
||||
@Autowired
|
||||
private ACDMessageHelper acdMessageHelper;
|
||||
|
||||
/**
|
||||
* 设置AgentUser基本信息
|
||||
*
|
||||
* @param ctx
|
||||
* @param next
|
||||
*/
|
||||
@Override
|
||||
public void apply(final ACDComposeContext ctx, final Functional next) {
|
||||
|
||||
/**
|
||||
* NOTE AgentUser代表一次会话记录,在上一个会话结束,并且由坐席人员点击"清除"后,会从数据库中删除
|
||||
* 此处查询到的,可能是之前的会话。其状态需要验证,所以不一定是由TA来服务本次会话。
|
||||
*/
|
||||
AgentUser agentUser = cache.findOneAgentUserByUserId(ctx.getOnlineUserId()).orElseGet(
|
||||
() -> {
|
||||
/**
|
||||
* NOTE 新创建的AgentUser不需要设置Status和Agentno
|
||||
* 因为两个值在后面会检查,如果存在则不会申请新的Agent
|
||||
*/
|
||||
AgentUser p = new AgentUser(
|
||||
ctx.getOnlineUserId(),
|
||||
ctx.getChannelType(),
|
||||
ctx.getOnlineUserId(),
|
||||
ctx.getOnlineUserNickname(),
|
||||
ctx.getAppid());
|
||||
logger.info("[apply] create new agent user id {}", p.getId());
|
||||
return p;
|
||||
});
|
||||
|
||||
|
||||
logger.info("[apply] resolve agent user id {}", agentUser.getId());
|
||||
|
||||
agentUser.setUsername(resolveAgentUsername(agentUser, ctx.getOnlineUserNickname()));
|
||||
agentUser.setOsname(ctx.getOsname());
|
||||
agentUser.setBrowser(ctx.getBrowser());
|
||||
agentUser.setAppid(ctx.getAppid());
|
||||
agentUser.setSessionid(ctx.getSessionid());
|
||||
|
||||
if (ctx.getIpdata() != null) {
|
||||
logger.info("[apply] set IP data for agentUser {}", agentUser.getId());
|
||||
agentUser.setCountry(ctx.getIpdata().getCountry());
|
||||
agentUser.setProvince(ctx.getIpdata().getProvince());
|
||||
agentUser.setCity(ctx.getIpdata().getCity());
|
||||
if (StringUtils.isNotBlank(ctx.getIp())) {
|
||||
agentUser.setRegion(ctx.getIpdata().toString() + "[" + ctx.getIp() + "]");
|
||||
} else {
|
||||
agentUser.setRegion(ctx.getIpdata().toString());
|
||||
}
|
||||
}
|
||||
|
||||
agentUser.setOwner(ctx.getOwnerid()); // 智能IVR的 EventID
|
||||
agentUser.setHeadimgurl(ctx.getOnlineUserHeadimgUrl());
|
||||
agentUser.setTitle(ctx.getTitle());
|
||||
agentUser.setUrl(ctx.getUrl());
|
||||
agentUser.setTraceid(ctx.getTraceid());
|
||||
ctx.setAgentUser(agentUser);
|
||||
|
||||
next.apply();
|
||||
|
||||
/**
|
||||
* 发送通知
|
||||
*/
|
||||
if (ctx.getAgentService() != null && StringUtils.isNotBlank(ctx.getAgentService().getStatus())) {
|
||||
/**
|
||||
* 找到空闲坐席,如果未找到坐席,则将该用户放入到 排队队列
|
||||
*/
|
||||
switch (MainContext.AgentUserStatusEnum.toValue(ctx.getAgentService().getStatus())) {
|
||||
case INSERVICE:
|
||||
ctx.setMessage(
|
||||
acdMessageHelper.getSuccessMessage(
|
||||
ctx.getAgentService(),
|
||||
ctx.getChannelType()));
|
||||
|
||||
// TODO 判断 INSERVICE 时,agentService 对应的 agentUser
|
||||
logger.info(
|
||||
"[apply] agent service: agentno {}, \n agentuser id {} \n user {} \n channel {} \n status {} \n queue index {}",
|
||||
ctx.getAgentService().getAgentno(), ctx.getAgentService().getAgentuserid(),
|
||||
ctx.getAgentService().getUserid(),
|
||||
ctx.getAgentService().getChanneltype(),
|
||||
ctx.getAgentService().getStatus(),
|
||||
ctx.getAgentService().getQueneindex());
|
||||
|
||||
if (StringUtils.isNotBlank(ctx.getAgentService().getAgentuserid())) {
|
||||
agentUserProxy.findOne(ctx.getAgentService().getAgentuserid()).ifPresent(ctx::setAgentUser);
|
||||
}
|
||||
|
||||
// TODO 如果是 INSERVICE 那么 agentService.getAgentuserid 就一定不能为空?
|
||||
// // TODO 此处需要考虑 agentService.getAgentuserid 为空的情况
|
||||
// // 那么什么情况下,agentService.getAgentuserid为空?
|
||||
// if (StringUtils.isNotBlank(agentService.getAgentuserid())) {
|
||||
// logger.info("[handle] set Agent User with agentUser Id {}", agentService.getAgentuserid());
|
||||
// getAgentUserProxy().findOne(agentService.getAgentuserid()).ifPresent(p -> {
|
||||
// outMessage.setChannelMessage(p);
|
||||
// });
|
||||
// } else {
|
||||
// logger.info("[handle] agent user id is null.");
|
||||
// }
|
||||
|
||||
agentStatusProxy.broadcastAgentsStatus(
|
||||
"user", MainContext.AgentUserStatusEnum.INSERVICE.toString(),
|
||||
ctx.getAgentUser().getId());
|
||||
break;
|
||||
case INQUENE:
|
||||
// 处理结果:进入排队队列
|
||||
ctx.getAgentService().setQueneindex(
|
||||
acdQueueService.getQueueIndex(
|
||||
ctx.getAgentUser().getAgentno(), ctx.getAgentUser().getSkill()));
|
||||
|
||||
if (ctx.getAgentService().getQueneindex() > 0) {
|
||||
// 当前有坐席,要排队
|
||||
ctx.setMessage(acdMessageHelper.getQueneMessage(
|
||||
ctx.getAgentService().getQueneindex(),
|
||||
ctx.getAgentUser().getChanneltype(),
|
||||
ctx.getOrganid()));
|
||||
} else {
|
||||
// TODO 什么是否返回 noAgentMessage, 是否在是 INQUENE 时 getQueneindex == 0
|
||||
// 当前没有坐席,要留言
|
||||
ctx.setNoagent(true);
|
||||
ctx.setMessage(acdMessageHelper.getNoAgentMessage(
|
||||
ctx.getAgentService().getQueneindex(),
|
||||
ctx.getChannelType(),
|
||||
ctx.getOrganid()));
|
||||
}
|
||||
|
||||
agentStatusProxy.broadcastAgentsStatus("user", MainContext.AgentUserStatusEnum.INQUENE.toString(),
|
||||
ctx.getAgentUser().getId());
|
||||
|
||||
break;
|
||||
case END:
|
||||
logger.info("[handler] should not happen for new onlineUser service request.");
|
||||
default:
|
||||
}
|
||||
ctx.setChannelMessage(ctx.getAgentUser());
|
||||
} else {
|
||||
ctx.setNoagent(true);
|
||||
ctx.setMessage(acdMessageHelper.getNoAgentMessage(
|
||||
0,
|
||||
ctx.getChannelType(),
|
||||
ctx.getOrganid()));
|
||||
}
|
||||
|
||||
logger.info(
|
||||
"[apply] message text: {}, noagent {}", ctx.getMessage(), ctx.isNoagent());
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 确定该访客的名字,优先级
|
||||
* 1. 如果AgentUser username 与 nickName 不一致,则用 agentUser username
|
||||
* 2. 如果AgentUser username 与 nickName 一致,则查找 AgentUserContact对应的联系人
|
||||
* 2.1 如果联系人存在,则用联系人的名字
|
||||
* 2.2 如果联系人不存在,则使用 nickName
|
||||
* <p>
|
||||
* TODO 此处有一些问题:如果联系人更新了名字,那么么后面TA的会话用的还是旧的名字,
|
||||
* 所以,在更新联系人名字的时候,也应更新其对应的AgentUser里面的名字
|
||||
*
|
||||
* @param agentUser
|
||||
* @param nickname
|
||||
* @return
|
||||
*/
|
||||
private String resolveAgentUsername(final AgentUser agentUser, final String nickname) {
|
||||
if (!StringUtils.equals(agentUser.getUsername(), nickname)) {
|
||||
return agentUser.getUsername();
|
||||
}
|
||||
|
||||
// 查找会话联系人关联表
|
||||
AgentUserContacts agentUserContact = agentUserContactsRes.findOneByUserid(
|
||||
agentUser.getUserid()).orElse(null);
|
||||
if (agentUserContact != null) {
|
||||
Contacts contact = contactsRes.findOneById(agentUserContact.getContactsid()).orElseGet(null);
|
||||
if (contact != null) {
|
||||
return contact.getName();
|
||||
}
|
||||
}
|
||||
|
||||
return nickname;
|
||||
}
|
||||
}
|
@ -1,79 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd.
|
||||
* <https://www.chatopera.com>, Licensed under the Chunsong Public
|
||||
* License, Version 1.0 (the "License"), https://docs.cskefu.com/licenses/v1.html
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* Copyright (C) 2019-2022 Chatopera Inc, <https://www.chatopera.com>,
|
||||
* Licensed under the Apache License, Version 2.0,
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*/
|
||||
|
||||
package com.cskefu.cc.acd.middleware.visitor;
|
||||
|
||||
import com.cskefu.cc.acd.ACDQueueService;
|
||||
import com.cskefu.cc.acd.basic.ACDComposeContext;
|
||||
import com.cskefu.cc.acd.basic.ACDMessageHelper;
|
||||
import com.cskefu.cc.basic.MainContext;
|
||||
import com.chatopera.compose4j.Functional;
|
||||
import com.chatopera.compose4j.Middleware;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 寻找或为绑定服务访客的坐席,建立双方通话
|
||||
*/
|
||||
@Component
|
||||
public class ACDVisServiceMw implements Middleware<ACDComposeContext> {
|
||||
private final static Logger logger = LoggerFactory.getLogger(ACDVisServiceMw.class);
|
||||
|
||||
@Autowired
|
||||
private ACDQueueService acdQueueService;
|
||||
|
||||
@Autowired
|
||||
private ACDMessageHelper acdMessageHelper;
|
||||
|
||||
@Override
|
||||
public void apply(final ACDComposeContext ctx, final Functional next) {
|
||||
ctx.setMessageType(MainContext.MessageType.STATUS.toString());
|
||||
/**
|
||||
* 首先交由 IMR处理 MESSAGE指令 , 如果当前用户是在 坐席对话列表中, 则直接推送给坐席,如果不在,则执行 IMR
|
||||
*/
|
||||
if (StringUtils.isNotBlank(ctx.getAgentUser().getStatus())) {
|
||||
// 该AgentUser已经在数据库中
|
||||
switch (MainContext.AgentUserStatusEnum.toValue(ctx.getAgentUser().getStatus())) {
|
||||
case INQUENE:
|
||||
logger.info("[apply] agent user is in queue");
|
||||
int queueIndex = acdQueueService.getQueueIndex(
|
||||
ctx.getAgentUser().getAgentno(),
|
||||
ctx.getOrganid());
|
||||
ctx.setMessage(
|
||||
acdMessageHelper.getQueneMessage(
|
||||
queueIndex,
|
||||
ctx.getChannelType(),
|
||||
ctx.getOrganid()));
|
||||
break;
|
||||
case INSERVICE:
|
||||
// 该访客与坐席正在服务中,忽略新的连接
|
||||
logger.info(
|
||||
"[apply] agent user {} is in service, userid {}, agentno {}", ctx.getAgentUser().getId(),
|
||||
ctx.getAgentUser().getUserid(), ctx.getAgentUser().getAgentno());
|
||||
break;
|
||||
case END:
|
||||
logger.info("[apply] agent user is null or END");
|
||||
// 过滤坐席,获得 Agent Service
|
||||
next.apply();
|
||||
}
|
||||
} else {
|
||||
// 该AgentUser为新建
|
||||
// 过滤坐席,获得 Agent Service
|
||||
next.apply();
|
||||
}
|
||||
}
|
||||
}
|
@ -1,79 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd.
|
||||
* <https://www.chatopera.com>, Licensed under the Chunsong Public
|
||||
* License, Version 1.0 (the "License"), https://docs.cskefu.com/licenses/v1.html
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* Copyright (C) 2019-2022 Chatopera Inc, <https://www.chatopera.com>,
|
||||
* Licensed under the Apache License, Version 2.0,
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*/
|
||||
|
||||
package com.cskefu.cc.acd.middleware.visitor;
|
||||
|
||||
import com.cskefu.cc.acd.ACDPolicyService;
|
||||
import com.cskefu.cc.acd.ACDWorkMonitor;
|
||||
import com.cskefu.cc.acd.basic.ACDComposeContext;
|
||||
import com.cskefu.cc.basic.MainContext;
|
||||
import com.cskefu.cc.basic.MainUtils;
|
||||
import com.cskefu.cc.model.AgentReport;
|
||||
import com.cskefu.cc.model.SessionConfig;
|
||||
import com.chatopera.compose4j.Functional;
|
||||
import com.chatopera.compose4j.Middleware;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
@Component
|
||||
public class ACDVisSessionCfgMw implements Middleware<ACDComposeContext> {
|
||||
private final static Logger logger = LoggerFactory.getLogger(ACDVisSessionCfgMw.class);
|
||||
|
||||
@Autowired
|
||||
private ACDPolicyService acdPolicyService;
|
||||
|
||||
@Autowired
|
||||
private ACDWorkMonitor acdWorkMonitor;
|
||||
|
||||
@Override
|
||||
public void apply(final ACDComposeContext ctx, final Functional next) {
|
||||
SessionConfig sessionConfig = acdPolicyService.initSessionConfig(ctx.getOrganid());
|
||||
|
||||
ctx.setSessionConfig(sessionConfig);
|
||||
|
||||
// 查询就绪的坐席,如果指定技能组则按照技能组查询
|
||||
AgentReport report;
|
||||
if (StringUtils.isNotBlank(ctx.getOrganid())) {
|
||||
report = acdWorkMonitor.getAgentReport(ctx.getOrganid());
|
||||
} else {
|
||||
report = acdWorkMonitor.getAgentReport();
|
||||
}
|
||||
|
||||
ctx.setAgentReport(report);
|
||||
|
||||
// 不在工作时间段
|
||||
if (sessionConfig.isHourcheck() && !MainUtils.isInWorkingHours(sessionConfig.getWorkinghours())) {
|
||||
logger.info("[apply] not in working hours");
|
||||
ctx.setMessage(sessionConfig.getNotinwhmsg());
|
||||
} else if (report.getAgents() == 0) {
|
||||
// 没有就绪的坐席
|
||||
if (ctx.getChannelType().equals(MainContext.ChannelType.MESSENGER.toString())) {
|
||||
next.apply();
|
||||
} else {
|
||||
logger.info("[apply] find no agents, redirect to leave a message.");
|
||||
ctx.setNoagent(true);
|
||||
}
|
||||
} else {
|
||||
logger.info("[apply] find agents size {}, allocate agent in next.", report.getAgents());
|
||||
// 具备工作中的就绪坐席,进入筛选坐席
|
||||
next.apply();
|
||||
}
|
||||
}
|
||||
}
|
@ -1,110 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd.
|
||||
* <https://www.chatopera.com>, Licensed under the Chunsong Public
|
||||
* License, Version 1.0 (the "License"), https://docs.cskefu.com/licenses/v1.html
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* Copyright (C) 2019-2022 Chatopera Inc, <https://www.chatopera.com>,
|
||||
* Licensed under the Apache License, Version 2.0,
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*/
|
||||
|
||||
package com.cskefu.cc.activemq;
|
||||
|
||||
import com.cskefu.cc.basic.Constants;
|
||||
import com.cskefu.cc.cache.Cache;
|
||||
import com.cskefu.cc.exception.CSKefuException;
|
||||
import com.cskefu.cc.model.AgentUser;
|
||||
import com.cskefu.cc.model.AgentUserAudit;
|
||||
import com.cskefu.cc.persistence.repository.AgentUserRepository;
|
||||
import com.cskefu.cc.proxy.AgentAuditProxy;
|
||||
import com.cskefu.cc.socketio.client.NettyClients;
|
||||
import com.cskefu.cc.util.SerializeUtil;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.jms.annotation.JmsListener;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 会话监控
|
||||
*/
|
||||
@Component
|
||||
public class AgentAuditSubscription {
|
||||
private final static Logger logger = LoggerFactory.getLogger(AgentAuditSubscription.class);
|
||||
|
||||
@Autowired
|
||||
private Cache cache;
|
||||
|
||||
@Autowired
|
||||
private AgentAuditProxy agentAuditProxy;
|
||||
|
||||
@Autowired
|
||||
private AgentUserRepository agentUserRes;
|
||||
|
||||
|
||||
/**
|
||||
* 接收坐席会话监控消息
|
||||
*
|
||||
* @param msg
|
||||
*/
|
||||
@JmsListener(destination = Constants.AUDIT_AGENT_MESSAGE, containerFactory = "jmsListenerContainerTopic")
|
||||
public void onMessage(final String msg) {
|
||||
logger.info("[onMessage] payload {}", msg);
|
||||
try {
|
||||
final JsonObject json = new JsonParser().parse(msg).getAsJsonObject();
|
||||
|
||||
if (json.has("data") &&
|
||||
json.has("agentUserId") &&
|
||||
json.has("event") && json.has("agentno")) {
|
||||
|
||||
// 查找关联的会话监控信息
|
||||
final AgentUserAudit agentUserAudit = cache.findOneAgentUserAuditById(
|
||||
json.get("agentUserId").getAsString()).orElseGet(() -> {
|
||||
final AgentUser agentUser = agentUserRes.findById(json.get("agentUserId").getAsString()).orElse(null);
|
||||
if (agentUser != null) {
|
||||
return agentAuditProxy.updateAgentUserAudits(agentUser);
|
||||
} else {
|
||||
logger.warn(
|
||||
"[onMessage] can not find agent user by id {}", json.get("agentUserId").getAsString());
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
if (agentUserAudit != null) {
|
||||
final String agentno = json.get("agentno").getAsString();
|
||||
logger.info(
|
||||
"[onMessage] agentno {}, subscribers size {}, subscribers {}", agentno,
|
||||
agentUserAudit.getSubscribers().size(),
|
||||
StringUtils.join(agentUserAudit.getSubscribers().keySet(), "|"));
|
||||
|
||||
// 发送消息给坐席监控,不需要分布式,因为这条消息已经是从ActiveMQ使用Topic多机广播
|
||||
for (final String subscriber : agentUserAudit.getSubscribers().keySet()) {
|
||||
logger.info("[onMessage] process subscriber {}", subscriber);
|
||||
if (!StringUtils.equals(subscriber, agentno)) {
|
||||
logger.info("[onMessage] publish event to {}", subscriber);
|
||||
NettyClients.getInstance().publishAuditEventMessage(
|
||||
subscriber,
|
||||
json.get("event").getAsString(),
|
||||
SerializeUtil.deserialize(json.get("data").getAsString()));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
logger.warn(
|
||||
"[onMessage] can not resolve agent user audit object for agent user id {}",
|
||||
json.get("agentUserId").getAsString());
|
||||
}
|
||||
} else {
|
||||
throw new CSKefuException("Invalid payload.");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.error("[onMessage] error", e);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,49 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd.
|
||||
* <https://www.chatopera.com>, Licensed under the Chunsong Public
|
||||
* License, Version 1.0 (the "License"), https://docs.cskefu.com/licenses/v1.html
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* Copyright (C) 2019-2022 Chatopera Inc, <https://www.chatopera.com>,
|
||||
* Licensed under the Apache License, Version 2.0,
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*/
|
||||
package com.cskefu.cc.activemq;
|
||||
|
||||
import com.cskefu.cc.basic.Constants;
|
||||
import com.cskefu.cc.socketio.client.NettyClients;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.jms.annotation.JmsListener;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class AgentSessionSubscription {
|
||||
private final static Logger logger = LoggerFactory.getLogger(AgentSessionSubscription.class);
|
||||
|
||||
/**
|
||||
* 接收坐席会话监控消息
|
||||
*
|
||||
* @param msg
|
||||
*/
|
||||
@JmsListener(destination = Constants.MQ_TOPIC_WEB_SESSION_SSO, containerFactory = "jmsListenerContainerTopic")
|
||||
public void onMessage(final String msg) {
|
||||
logger.info("[onMessage] payload {}", msg);
|
||||
try {
|
||||
final JsonObject json = new JsonParser().parse(msg).getAsJsonObject();
|
||||
// 把登出消息通知给浏览器
|
||||
NettyClients.getInstance().publishLeaveEventMessage(
|
||||
json.get("agentno").getAsString(),
|
||||
json.get("expired").getAsString());
|
||||
} catch (Exception e) {
|
||||
logger.warn("[onMessage] error", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -1,71 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd.
|
||||
* <https://www.chatopera.com>, Licensed under the Chunsong Public
|
||||
* License, Version 1.0 (the "License"), https://docs.cskefu.com/licenses/v1.html
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* Copyright (C) 2019-2022 Chatopera Inc, All rights reserved.
|
||||
* <https://www.chatopera.com>
|
||||
*/
|
||||
package com.cskefu.cc.activemq;
|
||||
|
||||
import com.cskefu.cc.basic.Constants;
|
||||
import com.cskefu.cc.socketio.client.NettyClients;
|
||||
import com.cskefu.cc.util.SerializeUtil;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.jms.annotation.JmsListener;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* WebIM Agent
|
||||
*/
|
||||
@Component
|
||||
public class AgentSubscription {
|
||||
|
||||
private final static Logger logger = LoggerFactory.getLogger(AgentSubscription.class);
|
||||
|
||||
@Value("${application.node.id}")
|
||||
private String appNodeId;
|
||||
|
||||
@Autowired
|
||||
private BrokerPublisher brokerPublisher;
|
||||
|
||||
/**
|
||||
* Publish Message into ActiveMQ
|
||||
*
|
||||
* @param j
|
||||
*/
|
||||
public void publish(JsonObject j) {
|
||||
j.addProperty("node", appNodeId);
|
||||
brokerPublisher.send(Constants.INSTANT_MESSAGING_MQ_TOPIC_AGENT, j.toString(), true);
|
||||
}
|
||||
|
||||
@JmsListener(destination = Constants.INSTANT_MESSAGING_MQ_TOPIC_AGENT, containerFactory = "jmsListenerContainerTopic")
|
||||
public void onMessage(final String payload) {
|
||||
logger.info("[onMessage] payload {}", payload);
|
||||
JsonParser parser = new JsonParser();
|
||||
JsonObject j = parser.parse(payload).getAsJsonObject();
|
||||
logger.debug("[onMessage] message body {}", j.toString());
|
||||
try {
|
||||
if (!j.has("id")) {
|
||||
logger.warn("[onMessage] Invalid payload, id is null");
|
||||
return;
|
||||
}
|
||||
|
||||
NettyClients.getInstance().sendAgentEventMessage(
|
||||
j.get("id").getAsString(),
|
||||
j.get("event").getAsString(),
|
||||
SerializeUtil.deserialize(j.get("data").getAsString()));
|
||||
} catch (Exception e) {
|
||||
logger.error("onMessage", e);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,63 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd.
|
||||
* <https://www.chatopera.com>, Licensed under the Chunsong Public
|
||||
* License, Version 1.0 (the "License"), https://docs.cskefu.com/licenses/v1.html
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* Copyright (C) 2019-2022 Chatopera Inc, <https://www.chatopera.com>,
|
||||
* Licensed under the Apache License, Version 2.0,
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*/
|
||||
package com.cskefu.cc.activemq;
|
||||
|
||||
import com.alibaba.fastjson.JSON;
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import com.cskefu.cc.basic.Constants;
|
||||
import com.cskefu.cc.cache.Cache;
|
||||
import com.cskefu.cc.persistence.repository.BlackListRepository;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.jms.annotation.JmsListener;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 访客黑名单
|
||||
*/
|
||||
@Component
|
||||
public class BlackListEventSubscription {
|
||||
private final static Logger logger = LoggerFactory.getLogger(BlackListEventSubscription.class);
|
||||
|
||||
@Autowired
|
||||
private Cache cache;
|
||||
|
||||
@Autowired
|
||||
private BlackListRepository blackListRes;
|
||||
|
||||
/**
|
||||
* 拉黑访客到达拉黑时间后,从黑名单中移除
|
||||
*
|
||||
* @param payload
|
||||
*/
|
||||
@JmsListener(destination = Constants.WEBIM_SOCKETIO_ONLINE_USER_BLACKLIST, containerFactory = "jmsListenerContainerQueue")
|
||||
public void onMessage(final String payload) {
|
||||
logger.info("[onMessage] payload {}", payload);
|
||||
|
||||
try {
|
||||
final JSONObject json = JSON.parseObject(payload);
|
||||
final String userId = json.getString("userId");
|
||||
|
||||
if (StringUtils.isNotBlank(userId)) {
|
||||
cache.findOneBlackEntityByUserId(userId).ifPresent(blackListRes::delete);
|
||||
} else {
|
||||
logger.warn("[onMessage] error: invalid payload");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.error("[onMessage] error", e);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,111 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd.
|
||||
* <https://www.chatopera.com>, Licensed under the Chunsong Public
|
||||
* License, Version 1.0 (the "License"), https://docs.cskefu.com/licenses/v1.html
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* Copyright (C) 2019-2022 Chatopera Inc, All rights reserved.
|
||||
* <https://www.chatopera.com>
|
||||
*/
|
||||
|
||||
package com.cskefu.cc.activemq;
|
||||
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import org.apache.activemq.ScheduledMessage;
|
||||
import org.apache.activemq.command.ActiveMQTopic;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.jms.core.JmsMessagingTemplate;
|
||||
import org.springframework.jms.core.JmsTemplate;
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.messaging.core.MessagePostProcessor;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
@Component
|
||||
public class BrokerPublisher {
|
||||
|
||||
final static private Logger logger = LoggerFactory.getLogger(BrokerPublisher.class);
|
||||
|
||||
@Autowired
|
||||
private JmsTemplate jmsTemplate;
|
||||
|
||||
@PostConstruct
|
||||
public void setup() {
|
||||
logger.info("[ActiveMQ Publisher] setup successfully.");
|
||||
}
|
||||
|
||||
/**
|
||||
* 时延消息
|
||||
*
|
||||
* @param destination
|
||||
* @param payload
|
||||
* @param delay available by delayed seconds
|
||||
*/
|
||||
public void send(final String destination, final String payload, final boolean isTopic, final int delay) {
|
||||
try {
|
||||
if (isTopic) {
|
||||
jmsTemplate.convertAndSend(new ActiveMQTopic(destination), payload, m -> {
|
||||
m.setLongProperty(ScheduledMessage.AMQ_SCHEDULED_DELAY, 1000 * delay);
|
||||
return m;
|
||||
});
|
||||
} else {
|
||||
// 默认为Queue
|
||||
jmsTemplate.convertAndSend(destination, payload, m -> {
|
||||
m.setLongProperty(ScheduledMessage.AMQ_SCHEDULED_DELAY, 1000 * delay);
|
||||
return m;
|
||||
});
|
||||
}
|
||||
logger.debug("[send] send succ, dest {}, payload {}", destination, payload);
|
||||
} catch (Exception e) {
|
||||
logger.warn("[send] error happens.", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param destination
|
||||
* @param payload
|
||||
* @param isTopic
|
||||
*/
|
||||
public void send(final String destination, final String payload, boolean isTopic) {
|
||||
try {
|
||||
if (isTopic) {
|
||||
jmsTemplate.convertAndSend(new ActiveMQTopic(destination), payload);
|
||||
} else {
|
||||
// 默认为Queue
|
||||
jmsTemplate.convertAndSend(destination, payload);
|
||||
}
|
||||
logger.debug("[send] send succ, dest {}, payload {}", destination, payload);
|
||||
} catch (Exception e) {
|
||||
logger.warn("[send] error happens.", e);
|
||||
}
|
||||
}
|
||||
|
||||
public void send(final String destination, final String payload) {
|
||||
send(destination, payload, false);
|
||||
}
|
||||
|
||||
public void send(final String destination, final JSONObject payload) {
|
||||
send(destination, payload.toJSONString());
|
||||
}
|
||||
|
||||
public void send(final String destination, final org.json.JSONObject payload) {
|
||||
send(destination, payload.toString());
|
||||
}
|
||||
|
||||
public void send(final String destination, final Map<String, String> payload) {
|
||||
JSONObject obj = new JSONObject();
|
||||
|
||||
for (Map.Entry<String, String> entry : payload.entrySet()) {
|
||||
obj.put(entry.getKey(), entry.getValue());
|
||||
}
|
||||
|
||||
send(destination, obj.toJSONString());
|
||||
}
|
||||
}
|
@ -1,74 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd.
|
||||
* <https://www.chatopera.com>, Licensed under the Chunsong Public
|
||||
* License, Version 1.0 (the "License"), https://docs.cskefu.com/licenses/v1.html
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* Copyright (C) 2019-2022 Chatopera Inc, All rights reserved.
|
||||
* <https://www.chatopera.com>
|
||||
*/
|
||||
package com.cskefu.cc.activemq;
|
||||
|
||||
import com.cskefu.cc.basic.Constants;
|
||||
import com.cskefu.cc.socketio.client.NettyClients;
|
||||
import com.cskefu.cc.util.SerializeUtil;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.jms.annotation.JmsListener;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import jakarta.annotation.PostConstruct;
|
||||
|
||||
/**
|
||||
* IM OnlineUser
|
||||
*/
|
||||
@Component
|
||||
public class OnlineUserSubscription {
|
||||
|
||||
private final static Logger logger = LoggerFactory.getLogger(OnlineUserSubscription.class);
|
||||
|
||||
@Value("${application.node.id}")
|
||||
private String appNodeId;
|
||||
|
||||
|
||||
@Autowired
|
||||
private BrokerPublisher brokerPublisher;
|
||||
|
||||
@PostConstruct
|
||||
public void setup() {
|
||||
logger.info("ActiveMQ Subscription is setup successfully.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Publish Message into ActiveMQ
|
||||
*
|
||||
* @param j
|
||||
*/
|
||||
public void publish(final JsonObject j) {
|
||||
j.addProperty("node", appNodeId);
|
||||
brokerPublisher.send(Constants.INSTANT_MESSAGING_MQ_TOPIC_ONLINEUSER, j.toString(), true);
|
||||
|
||||
}
|
||||
|
||||
@JmsListener(destination = Constants.INSTANT_MESSAGING_MQ_TOPIC_ONLINEUSER, containerFactory = "jmsListenerContainerTopic")
|
||||
public void onMessage(final String payload){
|
||||
logger.info("[onMessage] payload {}", payload);
|
||||
JsonParser parser = new JsonParser();
|
||||
JsonObject j = parser.parse(payload).getAsJsonObject();
|
||||
logger.debug("[instant messaging] message body {}", j.toString());
|
||||
try {
|
||||
NettyClients.getInstance().publishIMEventMessage(j.get("id").getAsString(),
|
||||
j.get("event").getAsString(),
|
||||
SerializeUtil.deserialize(j.get("data").getAsString()));
|
||||
} catch (Exception e) {
|
||||
logger.error("onMessage", e);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,118 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd.
|
||||
* <https://www.chatopera.com>, Licensed under the Chunsong Public
|
||||
* License, Version 1.0 (the "License"), https://docs.cskefu.com/licenses/v1.html
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* Copyright (C) 2019-2022 Chatopera Inc, All rights reserved.
|
||||
* <https://www.chatopera.com>
|
||||
*/
|
||||
package com.cskefu.cc.activemq;
|
||||
|
||||
import com.cskefu.cc.acd.ACDAgentDispatcher;
|
||||
import com.cskefu.cc.acd.ACDWorkMonitor;
|
||||
import com.cskefu.cc.acd.basic.ACDComposeContext;
|
||||
import com.cskefu.cc.basic.Constants;
|
||||
import com.cskefu.cc.basic.MainContext;
|
||||
import com.cskefu.cc.cache.Cache;
|
||||
import com.cskefu.cc.model.AgentStatus;
|
||||
import com.cskefu.cc.persistence.repository.AgentStatusRepository;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.jms.annotation.JmsListener;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 处理SocketIO的离线事件
|
||||
*/
|
||||
@Component
|
||||
public class SocketioConnEventSubscription {
|
||||
|
||||
private final static Logger logger = LoggerFactory.getLogger(SocketioConnEventSubscription.class);
|
||||
|
||||
@Autowired
|
||||
private ACDAgentDispatcher acdAgentDispatcher;
|
||||
|
||||
@Autowired
|
||||
private ACDWorkMonitor acdWorkMonitor;
|
||||
|
||||
@Autowired
|
||||
private AgentStatusRepository agentStatusRes;
|
||||
|
||||
@Autowired
|
||||
private Cache cache;
|
||||
|
||||
@Value("${application.node.id}")
|
||||
private String appNodeId;
|
||||
|
||||
@PostConstruct
|
||||
public void setup() {
|
||||
logger.info("ActiveMQ Subscription is setup successfully.");
|
||||
}
|
||||
|
||||
@JmsListener(destination = Constants.WEBIM_SOCKETIO_AGENT_DISCONNECT, containerFactory = "jmsListenerContainerQueue")
|
||||
public void onMessage(final String payload) {
|
||||
logger.info("[onMessage] payload {}", payload);
|
||||
|
||||
try {
|
||||
JsonParser parser = new JsonParser();
|
||||
JsonObject j = parser.parse(payload).getAsJsonObject();
|
||||
if (j.has("userId") && j.has("isAdmin")) {
|
||||
final AgentStatus agentStatus = cache.findOneAgentStatusByAgentno(
|
||||
j.get("userId").getAsString());
|
||||
if (agentStatus != null && (!agentStatus.isConnected())) {
|
||||
/**
|
||||
* 处理该坐席为离线
|
||||
*/
|
||||
// 重分配坐席
|
||||
ACDComposeContext ctx = new ACDComposeContext();
|
||||
ctx.setAgentno(agentStatus.getAgentno());
|
||||
acdAgentDispatcher.dequeue(ctx);
|
||||
if (ctx.isResolved()) {
|
||||
logger.info("[onMessage] re-allotAgent for user's visitors successfully.");
|
||||
} else {
|
||||
logger.info("[onMessage] re-allotAgent, error happens.");
|
||||
}
|
||||
|
||||
// 更新数据库
|
||||
agentStatus.setBusy(false);
|
||||
agentStatus.setStatus(MainContext.AgentStatusEnum.OFFLINE.toString());
|
||||
agentStatus.setUpdatetime(new Date());
|
||||
|
||||
// 设置该坐席状态为离线
|
||||
cache.deleteAgentStatusByAgentno(agentStatus.getAgentno());
|
||||
agentStatusRes.save(agentStatus);
|
||||
|
||||
// 记录坐席工作日志
|
||||
acdWorkMonitor.recordAgentStatus(agentStatus.getAgentno(),
|
||||
agentStatus.getUsername(),
|
||||
agentStatus.getAgentno(),
|
||||
j.get("isAdmin").getAsBoolean(),
|
||||
agentStatus.getAgentno(),
|
||||
agentStatus.getStatus(),
|
||||
MainContext.AgentStatusEnum.OFFLINE.toString(),
|
||||
MainContext.AgentWorkType.MEIDIACHAT.toString(),
|
||||
null);
|
||||
} else if (agentStatus == null) {
|
||||
// 该坐席已经完成离线设置
|
||||
logger.info("[onMessage] agent is already offline, skip any further operations");
|
||||
} else {
|
||||
// 该坐席目前在线,忽略该延迟事件
|
||||
logger.info("[onMessage] agent is online now, ignore this message.");
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.error("onMessage", e);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,48 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd.
|
||||
* <https://www.chatopera.com>, Licensed under the Chunsong Public
|
||||
* License, Version 1.0 (the "License"), https://docs.cskefu.com/licenses/v1.html
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* Copyright (C) 2019-2022 Chatopera Inc, <https://www.chatopera.com>,
|
||||
* Licensed under the Apache License, Version 2.0,
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*/
|
||||
package com.cskefu.cc.aspect;
|
||||
|
||||
import com.cskefu.cc.cache.Cache;
|
||||
import com.cskefu.cc.model.AgentStatus;
|
||||
import org.aspectj.lang.JoinPoint;
|
||||
import org.aspectj.lang.annotation.After;
|
||||
import org.aspectj.lang.annotation.Aspect;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
@Aspect
|
||||
@Component
|
||||
public class AgentStatusAspect {
|
||||
private final static Logger logger = LoggerFactory.getLogger(AgentStatusAspect.class);
|
||||
|
||||
@Autowired
|
||||
private Cache cache;
|
||||
|
||||
@After("execution(* com.cskefu.cc.persistence.repository.AgentStatusRepository.save(..))")
|
||||
public void save(final JoinPoint joinPoint) {
|
||||
final AgentStatus agentStatus = (AgentStatus) joinPoint.getArgs()[0];
|
||||
cache.putAgentStatus(agentStatus);
|
||||
}
|
||||
|
||||
@After("execution(* com.cskefu.cc.persistence.repository.AgentStatusRepository.delete(..))")
|
||||
public void delete(final JoinPoint joinPoint) {
|
||||
final AgentStatus agentStatus = (AgentStatus) joinPoint.getArgs()[0];
|
||||
cache.deleteAgentStatusByAgentno(agentStatus.getAgentno());
|
||||
}
|
||||
}
|
@ -1,116 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd.
|
||||
* <https://www.chatopera.com>, Licensed under the Chunsong Public
|
||||
* License, Version 1.0 (the "License"), https://docs.cskefu.com/licenses/v1.html
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* Copyright (C) 2019-2022 Chatopera Inc, <https://www.chatopera.com>,
|
||||
* Licensed under the Apache License, Version 2.0,
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*/
|
||||
|
||||
package com.cskefu.cc.aspect;
|
||||
|
||||
import com.cskefu.cc.basic.MainContext;
|
||||
import com.cskefu.cc.cache.Cache;
|
||||
import com.cskefu.cc.cache.RedisCommand;
|
||||
import com.cskefu.cc.cache.RedisKey;
|
||||
import com.cskefu.cc.model.AgentUser;
|
||||
import com.cskefu.cc.proxy.AgentAuditProxy;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.aspectj.lang.JoinPoint;
|
||||
import org.aspectj.lang.ProceedingJoinPoint;
|
||||
import org.aspectj.lang.annotation.After;
|
||||
import org.aspectj.lang.annotation.Around;
|
||||
import org.aspectj.lang.annotation.Aspect;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
@Aspect
|
||||
@Component
|
||||
public class AgentUserAspect {
|
||||
|
||||
private final static Logger logger = LoggerFactory.getLogger(AgentUserAspect.class);
|
||||
|
||||
@Autowired
|
||||
private Cache cache;
|
||||
|
||||
@Autowired
|
||||
private RedisCommand redisCommand;
|
||||
|
||||
@Autowired
|
||||
private AgentAuditProxy agentAuditProxy;
|
||||
|
||||
@After("execution(* com.cskefu.cc.persistence.repository.AgentUserRepository.save(..))")
|
||||
public void save(final JoinPoint joinPoint) {
|
||||
final AgentUser agentUser = (AgentUser) joinPoint.getArgs()[0];
|
||||
logger.info(
|
||||
"[save] agentUser id {}, agentno {}, userId {}, status {}", agentUser.getId(), agentUser.getAgentno(),
|
||||
agentUser.getUserid(), agentUser.getStatus());
|
||||
|
||||
if (StringUtils.isBlank(agentUser.getId())
|
||||
|| StringUtils.isBlank(agentUser.getUserid())) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 更新坐席监控信息
|
||||
agentAuditProxy.updateAgentUserAudits(agentUser);
|
||||
|
||||
// 同步缓存
|
||||
cache.putAgentUser(agentUser);
|
||||
}
|
||||
|
||||
@After("execution(* com.cskefu.cc.persistence.repository.AgentUserRepository.delete(..))")
|
||||
public void delete(final JoinPoint joinPoint) {
|
||||
final AgentUser agentUser = (AgentUser) joinPoint.getArgs()[0];
|
||||
logger.info(
|
||||
"[delete] agentUser id {}, agentno {}, userId {}", agentUser.getId(), agentUser.getAgentno(),
|
||||
agentUser.getUserid());
|
||||
cache.deleteAgentUserAuditById(agentUser.getId());
|
||||
cache.deleteAgentUserByUserId(agentUser);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新内存中的坐席与其服务的访客的集合
|
||||
*
|
||||
* @param joinPoint
|
||||
* @return
|
||||
* @throws Throwable
|
||||
*/
|
||||
@Around("@annotation(com.cskefu.cc.aspect.AgentUserAspect.LinkAgentUser)")
|
||||
public Object LinkAgentUser(ProceedingJoinPoint joinPoint) throws Throwable {
|
||||
final AgentUser updated = (AgentUser) joinPoint.getArgs()[0];
|
||||
Object proceed = joinPoint.proceed(); // after things are done.
|
||||
logger.info(
|
||||
"[linkAgentUser] agentUser: status {}, userId {}, agentno {}", updated.getStatus(),
|
||||
updated.getUserid(), updated.getAgentno());
|
||||
if (StringUtils.equals(updated.getStatus(), MainContext.AgentUserStatusEnum.END.toString())) {
|
||||
// 从集合中删除
|
||||
redisCommand.removeSetVal(
|
||||
RedisKey.getInServAgentUsersByAgentno(updated.getAgentno()), updated.getUserid());
|
||||
} else if (StringUtils.equals(updated.getStatus(), MainContext.AgentUserStatusEnum.INSERVICE.toString())) {
|
||||
redisCommand.insertSetVal(
|
||||
RedisKey.getInServAgentUsersByAgentno(updated.getAgentno()), updated.getUserid());
|
||||
} else if (StringUtils.equals(updated.getStatus(), MainContext.AgentUserStatusEnum.INQUENE.toString())) {
|
||||
logger.info("[linkAgentUser] ignored inque agent user, haven't resolve one agent yet.");
|
||||
} else {
|
||||
logger.warn("[linkAgentUser] unexpected condition.");
|
||||
}
|
||||
return proceed;
|
||||
}
|
||||
|
||||
@Target(ElementType.METHOD)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface LinkAgentUser {
|
||||
}
|
||||
}
|
@ -1,50 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd.
|
||||
* <https://www.chatopera.com>, Licensed under the Chunsong Public
|
||||
* License, Version 1.0 (the "License"), https://docs.cskefu.com/licenses/v1.html
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* Copyright (C) 2019-2022 Chatopera Inc, <https://www.chatopera.com>,
|
||||
* Licensed under the Apache License, Version 2.0,
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*/
|
||||
|
||||
package com.cskefu.cc.aspect;
|
||||
|
||||
import com.cskefu.cc.cache.Cache;
|
||||
import com.cskefu.cc.model.BlackEntity;
|
||||
import org.aspectj.lang.JoinPoint;
|
||||
import org.aspectj.lang.annotation.After;
|
||||
import org.aspectj.lang.annotation.Aspect;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
|
||||
@Aspect
|
||||
@Component
|
||||
public class BlackEntityAspect {
|
||||
|
||||
private final static Logger logger = LoggerFactory.getLogger(BlackEntityAspect.class);
|
||||
|
||||
@Autowired
|
||||
private Cache cache;
|
||||
|
||||
@After("execution(* com.cskefu.cc.persistence.repository.BlackListRepository.save(..))")
|
||||
public void save(final JoinPoint joinPoint) {
|
||||
final BlackEntity blackEntity = (BlackEntity) joinPoint.getArgs()[0];
|
||||
logger.info("[save] blackEntity userId {}", blackEntity.getUserid());
|
||||
cache.putBlackEntity(blackEntity);
|
||||
}
|
||||
|
||||
@After("execution(* com.cskefu.cc.persistence.repository.BlackListRepository.delete(..))")
|
||||
public void delete(final JoinPoint joinPoint) {
|
||||
final BlackEntity blackEntity = (BlackEntity) joinPoint.getArgs()[0];
|
||||
logger.info("[delete] blackEntity userId {}", blackEntity.getUserid());
|
||||
cache.deleteBlackEntityByUserId(blackEntity.getUserid());
|
||||
}
|
||||
}
|
@ -1,58 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd.
|
||||
* <https://www.chatopera.com>, Licensed under the Chunsong Public
|
||||
* License, Version 1.0 (the "License"), https://docs.cskefu.com/licenses/v1.html
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* Copyright (C) 2019-2022 Chatopera Inc, <https://www.chatopera.com>,
|
||||
* Licensed under the Apache License, Version 2.0,
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*/
|
||||
package com.cskefu.cc.aspect;
|
||||
|
||||
import com.cskefu.cc.basic.MainContext;
|
||||
import com.cskefu.cc.cache.Cache;
|
||||
import com.cskefu.cc.model.PassportWebIMUser;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.aspectj.lang.JoinPoint;
|
||||
import org.aspectj.lang.annotation.Aspect;
|
||||
import org.aspectj.lang.annotation.Before;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Aspect
|
||||
@Component
|
||||
public class OnlineUserAspect {
|
||||
private final static Logger logger = LoggerFactory.getLogger(OnlineUserAspect.class);
|
||||
|
||||
@Autowired
|
||||
private Cache cache;
|
||||
|
||||
/**
|
||||
* 因为会定期从缓存序列化到数据库
|
||||
*
|
||||
* @param joinPoint
|
||||
*/
|
||||
@Before("execution(* com.cskefu.cc.persistence.repository.PassportWebIMUserRepository.save(..))")
|
||||
public void save(final JoinPoint joinPoint) {
|
||||
final PassportWebIMUser passportWebIMUser = (PassportWebIMUser) joinPoint.getArgs()[0];
|
||||
// logger.info(
|
||||
// "[save] put onlineUser id {}, status {}, invite status {}", onlineUser.getId(), onlineUser.getStatus(),
|
||||
// onlineUser.getInvitestatus());
|
||||
if (StringUtils.isNotBlank(passportWebIMUser.getStatus())) {
|
||||
switch (MainContext.OnlineUserStatusEnum.toValue(passportWebIMUser.getStatus())) {
|
||||
case OFFLINE:
|
||||
cache.deleteOnlineUserById(passportWebIMUser.getId());
|
||||
break;
|
||||
default:
|
||||
cache.putOnlineUser(passportWebIMUser);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -1,93 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd.
|
||||
* <https://www.chatopera.com>, Licensed under the Chunsong Public
|
||||
* License, Version 1.0 (the "License"), https://docs.cskefu.com/licenses/v1.html
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* Copyright (C) 2018- Jun. 2023 Chatopera Inc, <https://www.chatopera.com>, Licensed under the Apache License, Version 2.0,
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Copyright (C) 2017 优客服-多渠道客服系统, Licensed under the Apache License, Version 2.0,
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*/
|
||||
package com.cskefu.cc.aspect;
|
||||
|
||||
import com.cskefu.cc.persistence.hibernate.BaseService;
|
||||
import com.cskefu.cc.util.CskefuList;
|
||||
import org.aspectj.lang.ProceedingJoinPoint;
|
||||
import org.aspectj.lang.annotation.Around;
|
||||
import org.aspectj.lang.annotation.Aspect;
|
||||
import org.aspectj.lang.annotation.Pointcut;
|
||||
import org.hibernate.StaleStateException;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Aspect
|
||||
@Component
|
||||
public class SyncDatabaseAspect {
|
||||
private final static Logger logger = LoggerFactory.getLogger(SyncDatabaseAspect.class);
|
||||
|
||||
@Autowired
|
||||
private BaseService<?> dbDataRes;
|
||||
|
||||
/**
|
||||
* 定义拦截规则:拦截org.springframework.data.elasticsearch.repository。
|
||||
*/
|
||||
@Pointcut("execution(* org.springframework.data.elasticsearch.repository.*.save(*))")
|
||||
public void syncSaveEsData() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 定义拦截规则:拦截org.springframework.data.elasticsearch.repository。
|
||||
*/
|
||||
@Pointcut("execution(* org.springframework.data.elasticsearch.repository.*.delete(*))")
|
||||
public void syncDeleteEsData() {
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Around("syncSaveEsData()")
|
||||
public void syncSaveEsData(ProceedingJoinPoint pjp) throws Throwable {
|
||||
pjp.proceed();
|
||||
Object[] args = pjp.getArgs();
|
||||
if (args.length == 1) {
|
||||
Object data = args[0];
|
||||
if (data != null) {
|
||||
if (data instanceof CskefuList) {
|
||||
/** 只有一个地方用到,从ES同步数据到MySQL **/
|
||||
} else if (data instanceof List) {
|
||||
// TODO 批量建联系人操作会执行这段代码,此处会报错,但是批量更新可以通过
|
||||
dbDataRes.saveOrUpdateAll((List<Object>) data);
|
||||
} else {
|
||||
try {
|
||||
// 更新时,执行此代码,但是新建时会报错
|
||||
dbDataRes.saveOrUpdate(data);
|
||||
} catch (StaleStateException ex) {
|
||||
// 报错的情况下,执行此代码
|
||||
dbDataRes.save(data);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Around("syncDeleteEsData()")
|
||||
public void syncDeleteEsData(ProceedingJoinPoint pjp) throws Throwable {
|
||||
pjp.proceed();
|
||||
Object[] args = pjp.getArgs();
|
||||
if (args.length == 1) {
|
||||
Object data = args[0];
|
||||
if (data instanceof List) {
|
||||
dbDataRes.deleteAll((List<Object>) data);
|
||||
} else {
|
||||
dbDataRes.delete(data);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,220 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd.
|
||||
* <https://www.chatopera.com>, Licensed under the Chunsong Public
|
||||
* License, Version 1.0 (the "License"), https://docs.cskefu.com/licenses/v1.html
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* Copyright (C) 2019-2022 Chatopera Inc, <https://www.chatopera.com>,
|
||||
* Licensed under the Apache License, Version 2.0,
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*/
|
||||
package com.cskefu.cc.basic;
|
||||
|
||||
import java.text.DecimalFormat;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 常量
|
||||
*/
|
||||
public class Constants {
|
||||
|
||||
/**
|
||||
* 系统配置
|
||||
*/
|
||||
public static final String USER_SESSION_NAME = "user";
|
||||
public static final String ORGAN_SESSION_NAME = "organ";
|
||||
public static final String GUEST_USER = "guest";
|
||||
public static final String IM_USER_SESSION_NAME = "im_user";
|
||||
public static final String CSKEFU_SYSTEM_DIC = "com.dic.system.template";
|
||||
public static final String CSKEFU_SYSTEM_AUTH_DIC = "com.dic.auth.resource";
|
||||
public static final String CSKEFU_SYSTEM_AREA_DIC = "com.dic.address.area";
|
||||
public static final String CSKEFU_SYSTEM_ADPOS_DIC = "com.dic.adv.type";
|
||||
public static final String CSKEFU_SYSTEM_COMMENT_DIC = "com.dic.webim.comment";
|
||||
public static final String CSKEFU_SYSTEM_COMMENT_ITEM_DIC = "com.dic.webim.comment.item";
|
||||
public static final String CSKEFU_SYSTEM_DIS_AI = "ownerai";
|
||||
public static final String CSKEFU_SYSTEM_DIS_AGENT = "owneruser";
|
||||
public static final String CSKEFU_SYSTEM_ASSUSER = "assuser";
|
||||
public static final String CSKEFU_SYSTEM_DIS_ORGAN = "ownerdept";
|
||||
public static final String CSKEFU_SYSTEM_DIS_TIME = "distime";
|
||||
public static final String CSKEFU_SYSTEM_COOKIES_FLAG = "uk_flagid";
|
||||
public static final String CSKEFU_SYSTEM_NO_DAT = "NOTEXIST";
|
||||
public static final String CSKEFU_SYSTEM_SECFIELD = "cskefu_sec_field";
|
||||
|
||||
public static final String CSKEFU_SYSTEM_CALLCENTER = "callcenter";
|
||||
public static final String CSKEFU_SYSTEM_WORKORDEREMAIL = "workordermail";
|
||||
public static final String CSKEFU_SYSTEM_SMSEMAIL = "callcenter";
|
||||
public static final String CSKEFU_SYSTEM_AI_INPUT = "inputparam";
|
||||
public static final String CSKEFU_SYSTEM_AI_OUTPUT = "outputparam";
|
||||
|
||||
public static final String CSKEFU_SYSTEM_INFOACQ = "infoacq"; // 数据采集模式
|
||||
public static final String DEFAULT_TYPE = "default"; // 默认分类代码
|
||||
public static final String CACHE_SKILL = "cache_skill_"; // 技能组的缓存
|
||||
public static final String CACHE_AGENT = "cache_agent_"; // 坐席列表的缓存
|
||||
|
||||
public static final String CUBE_TITLE_MEASURE = "指标";
|
||||
|
||||
public static final String CSKEFU_SYSTEM_AREA = "cskefu_system_area";
|
||||
|
||||
public static final String CSKEFU_SYSTEM_ADV = "cskefu_system_adv"; // 系统广告位
|
||||
|
||||
public static final String SYSTEM_CACHE_CALLOUT_CONFIG = "callout_config";
|
||||
|
||||
/**
|
||||
* 分布式存储
|
||||
*/
|
||||
public final static String MINIO_BUCKET = "chatopera";
|
||||
|
||||
|
||||
/**
|
||||
* Channels
|
||||
*/
|
||||
public static final String CHANNEL_TYPE_WEBIM = "webim";
|
||||
public static final String CHANNEL_TYPE_MESSENGER = "messenger";
|
||||
public final static String IM_MESSAGE_TYPE_MESSAGE = "message";
|
||||
public final static String IM_MESSAGE_TYPE_WRITING = "writing";
|
||||
public final static String CHATBOT_EVENT_TYPE_CHAT = "chat";
|
||||
|
||||
/**
|
||||
* Messenger Channels
|
||||
*/
|
||||
public static final String MESSENGER_CHANNEL_ENABLED = "enabled";
|
||||
public static final String MESSENGER_CHANNEL_DISABLED = "disabled";
|
||||
|
||||
/**
|
||||
* Modules
|
||||
*/
|
||||
public final static String CSKEFU_MODULE_CALLOUT = "callout";
|
||||
public final static String CSKEFU_MODULE_CHATBOT = "chatbot";
|
||||
public final static String CSKEFU_MODULE_CONTACTS = "contacts";
|
||||
public final static String CSKEFU_MODULE_SKYPE = "skype";
|
||||
public final static String CSKEFU_MODULE_MESSENGER = "messenger";
|
||||
public final static String CSKEFU_MODULE_CCA = "cca";
|
||||
public final static String CSKEFU_MODULE_ENTIM = "entim";
|
||||
public final static String CSKEFU_MODULE_WORKORDERS = "workorders";
|
||||
public final static String CSKEFU_MODULE_CALLCENTER = "callcenter";
|
||||
public final static String CSKEFU_MODULE_REPORT = "report";
|
||||
|
||||
/**
|
||||
* Formatter
|
||||
*/
|
||||
// Date Formatter https://docs.oracle.com/javase/7/docs/api/java/text/SimpleDateFormat.html
|
||||
public final static SimpleDateFormat QUERY_DATE_FORMATTER = new SimpleDateFormat("yyyy-MM-dd");
|
||||
public final static SimpleDateFormat DISPLAY_DATE_FORMATTER = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
|
||||
public final static DecimalFormat DURATION_MINS_FORMATTER = new DecimalFormat("0.00");
|
||||
|
||||
/**
|
||||
* Instant Messaging Events
|
||||
*/
|
||||
public final static String INSTANT_MESSAGING_MQ_TOPIC_AGENT = "cskefu.webim.agent";
|
||||
// freeswitch 通知消息
|
||||
public final static String INSTANT_MESSAGING_MQ_QUEUE_PBX = "pbx.*.events";
|
||||
public final static String INSTANT_MESSAGING_MQ_TOPIC_ONLINEUSER = "cskefu.webim.onlineuser";
|
||||
public final static String WEBIM_SOCKETIO_AGENT_DISCONNECT = "cskefu.socketio.agent.disconnect";
|
||||
// 黑名单
|
||||
public final static String WEBIM_SOCKETIO_ONLINE_USER_BLACKLIST = "cskefu.im.onlineuser.blacklist";
|
||||
// 坐席socketio断开到判定为离线的时长
|
||||
public final static int WEBIM_SOCKETIO_AGENT_OFFLINE_THRESHOLD = 20;
|
||||
// 发送消息给访客: 接收来自路由的消息并判断渠道
|
||||
public final static String INSTANT_MESSAGING_MQ_TOPIC_VISITOR = "cskefu.outbound.visitor";
|
||||
|
||||
// 发送给聊天机器人并处理返回结果
|
||||
public final static String INSTANT_MESSAGING_MQ_QUEUE_CHATBOT = "cskefu.outbound.chatbot";
|
||||
public static final String AUDIT_AGENT_MESSAGE = "cskefu.agent.audit";
|
||||
// 机器人返回的结果数据来源为faq
|
||||
public final static String PROVIDER_FAQ = "faq";
|
||||
public final static String PROVIDER_FEEDBACK = "feedback";
|
||||
public final static String PROVIDER_FEEDBACK_EVAL_POSITIVE = "positive";
|
||||
public final static String PROVIDER_FEEDBACK_EVAL_NEGATIVE = "negative";
|
||||
|
||||
// Facebook OTN 发送
|
||||
public final static String INSTANT_MESSAGING_MQ_QUEUE_FACEBOOK_OTN = "cskefu.outbound.faceboot.otn";
|
||||
|
||||
/**
|
||||
* 登录用户的唯一登录会话管理
|
||||
*/
|
||||
// web session single sign on
|
||||
public final static String MQ_TOPIC_WEB_SESSION_SSO = "cskefu.agent.session.retired";
|
||||
|
||||
/**
|
||||
* Attachment File Type
|
||||
*/
|
||||
public final static String ATTACHMENT_TYPE_IMAGE = "image";
|
||||
public final static String ATTACHMENT_TYPE_FILE = "file";
|
||||
|
||||
/**
|
||||
* FreeSwitch Communication
|
||||
*/
|
||||
// callcenter
|
||||
public final static String ACTIVEMQ_QUEUE_SWITCH_SYNC = "cskefu.callcenter.switch.sync";
|
||||
|
||||
// callout
|
||||
public final static String FS_SIP_STATUS = "pbx:%s:sips"; // 查询SIP状态
|
||||
public final static String FS_CHANNEL_CC_TO_FS = "pbx/%s/execute"; // 发送外呼执行信号
|
||||
public final static String FS_DIALPLAN_STATUS = "pbx:%s:status"; // 外呼执行状态存储
|
||||
public final static String FS_DIALPLAN_TARGET = "pbx:%s:targets:%s"; // 外呼计划电话列表
|
||||
public final static String FS_BRIDGE_CONNECT = "callOutConnect";
|
||||
public final static String FS_LEG_ANSWER = "answer";
|
||||
public final static String FS_LEG_HANGUP = "hangup";
|
||||
public final static String FS_LEG_INCALL_ZH = "通话";
|
||||
public final static String FS_CALL_TYPE_CALLOUT = "callout";
|
||||
public final static Set<String> CALL_DIRECTION_TYPES = new HashSet<>(Arrays.asList(
|
||||
MainContext.CallType.OUT.toString(), MainContext.CallType.IN.toString()));
|
||||
public final static Set<String> CALL_SERVICE_STAUTS = new HashSet<>(Arrays.asList(MainContext.CallServiceStatus.INQUENE.toString(),
|
||||
MainContext.CallServiceStatus.RING.toString(),
|
||||
MainContext.CallServiceStatus.INCALL.toString(),
|
||||
MainContext.CallServiceStatus.BRIDGE.toString(),
|
||||
MainContext.CallServiceStatus.HOLD.toString(),
|
||||
MainContext.CallServiceStatus.HANGUP.toString(),
|
||||
MainContext.CallServiceStatus.OFFLINE.toString()));
|
||||
|
||||
/**
|
||||
* 缓存管理策略
|
||||
*/
|
||||
public final static String cache_setup_strategy_skip = "skip";
|
||||
|
||||
/**
|
||||
* Skype消息路由
|
||||
* TODO 待优化为Skype渠道,暂时使用常量
|
||||
*/
|
||||
public final static String CHANNEL_SKYPE_DEST = "skype.{0}.send";
|
||||
public final static String CHANNEL_SKYPE_RECV = "skype.*.rec";
|
||||
public static final String SKYPE_PAYLOAD_KEY_CONTENT = "content";
|
||||
public static final String SKYPE_PAYLOAD_KEY_SKYPEID = "skypeId";
|
||||
public static final String SKYPE_PAYLOAD_KEY_MSGTYPE = "msgType";
|
||||
|
||||
/**
|
||||
* skype接收图片类型
|
||||
*/
|
||||
public final static String SKYPE_MESSAGE_TEXT = "text";
|
||||
public final static String SKYPE_MESSAGE_PIC = "pic";
|
||||
public final static String SKYPE_MESSAGE_FILE = "file";
|
||||
|
||||
/**
|
||||
* 坐席邀请访客加入聊天的超时,如果访客过了这么长时间还没有接入
|
||||
* 就忽略该邀请,当前设置为 20 分钟,如果访客点击该邀请,则会随机分配坐席
|
||||
*/
|
||||
public final static int WEBIM_AGENT_INVITE_TIMEOUT = 20 * 60 * 1000;
|
||||
|
||||
|
||||
/**
|
||||
* 聊天机器人
|
||||
*/
|
||||
public static final HashSet<String> CHATBOT_VALID_LANGS = new HashSet<>(Arrays.asList("zh_CN", "en_US"));
|
||||
public static final String CHATBOT_CHATBOT_FIRST = "机器人客服优先";
|
||||
public static final String CHATBOT_HUMAN_FIRST = "人工客服优先";
|
||||
public static final String CHATBOT_CHATBOT_ONLY = "仅机器人客服";
|
||||
public static final HashSet<String> CHATBOT_VALID_WORKMODELS = new HashSet<>(Arrays.asList(CHATBOT_CHATBOT_FIRST, CHATBOT_HUMAN_FIRST, CHATBOT_CHATBOT_ONLY));
|
||||
|
||||
/**
|
||||
* AUTH
|
||||
*/
|
||||
public static final String AUTH_TOKEN_TYPE_BEARER = "Bearer";
|
||||
public static final String AUTH_TOKEN_TYPE_BASIC = "Basic";
|
||||
|
||||
}
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -1,20 +0,0 @@
|
||||
/**
|
||||
* Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd.
|
||||
* <https://www.chatopera.com>, Licensed under the Chunsong Public
|
||||
* License, Version 1.0 (the "License"), https://docs.cskefu.com/licenses/v1.html
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* Copyright Jun. 2023 Chatopera Inc. <https://www.chatopera.com>. All rights reserved.
|
||||
*/
|
||||
package com.cskefu.cc.basic;
|
||||
|
||||
import jakarta.annotation.PreDestroy;
|
||||
|
||||
public class TerminateBean {
|
||||
@PreDestroy
|
||||
public void onDestroy() throws Exception {
|
||||
}
|
||||
}
|
@ -1,45 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd.
|
||||
* <https://www.chatopera.com>, Licensed under the Chunsong Public
|
||||
* License, Version 1.0 (the "License"), https://docs.cskefu.com/licenses/v1.html
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* Copyright (C) 2018- Jun. 2023 Chatopera Inc, <https://www.chatopera.com>, Licensed under the Apache License, Version 2.0,
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Copyright (C) 2017 优客服-多渠道客服系统, Licensed under the Apache License, Version 2.0,
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*/
|
||||
package com.cskefu.cc.basic;
|
||||
|
||||
public class Viewport {
|
||||
private String page;
|
||||
private String template;
|
||||
|
||||
public Viewport(String template, String page) {
|
||||
this.template = template;
|
||||
this.page = page;
|
||||
}
|
||||
|
||||
public Viewport(String page) {
|
||||
this.page = page;
|
||||
}
|
||||
|
||||
public String getPage() {
|
||||
return page;
|
||||
}
|
||||
|
||||
public void setPage(String page) {
|
||||
this.page = page;
|
||||
}
|
||||
|
||||
public String getTemplate() {
|
||||
return template;
|
||||
}
|
||||
|
||||
public void setTemplate(String template) {
|
||||
this.template = template;
|
||||
}
|
||||
}
|
@ -1,44 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd.
|
||||
* <https://www.chatopera.com>, Licensed under the Chunsong Public
|
||||
* License, Version 1.0 (the "License"), https://docs.cskefu.com/licenses/v1.html
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* Copyright (C) 2019-2022 Chatopera Inc, <https://www.chatopera.com>,
|
||||
* Licensed under the Apache License, Version 2.0,
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*/
|
||||
package com.cskefu.cc.basic.auth;
|
||||
|
||||
import org.springframework.data.redis.connection.DefaultStringRedisConnection;
|
||||
import org.springframework.data.redis.connection.RedisConnection;
|
||||
import org.springframework.data.redis.connection.RedisConnectionFactory;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.data.redis.serializer.RedisSerializer;
|
||||
import org.springframework.data.redis.serializer.StringRedisSerializer;
|
||||
|
||||
/**
|
||||
* 存储Auth Token的Redis连接
|
||||
*/
|
||||
public class AuthRedisTemplate extends RedisTemplate<String, String> {
|
||||
public AuthRedisTemplate() {
|
||||
RedisSerializer<String> stringSerializer = new StringRedisSerializer();
|
||||
this.setKeySerializer(stringSerializer);
|
||||
this.setValueSerializer(stringSerializer);
|
||||
this.setHashKeySerializer(stringSerializer);
|
||||
this.setHashValueSerializer(stringSerializer);
|
||||
}
|
||||
|
||||
public AuthRedisTemplate(RedisConnectionFactory connectionFactory) {
|
||||
this();
|
||||
this.setConnectionFactory(connectionFactory);
|
||||
this.afterPropertiesSet();
|
||||
}
|
||||
|
||||
protected RedisConnection preProcessConnection(RedisConnection connection, boolean existingConnection) {
|
||||
return new DefaultStringRedisConnection(connection);
|
||||
}
|
||||
}
|
@ -1,32 +0,0 @@
|
||||
/**
|
||||
* Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd.
|
||||
* <https://www.chatopera.com>, Licensed under the Chunsong Public
|
||||
* License, Version 1.0 (the "License"), https://docs.cskefu.com/licenses/v1.html
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* Copyright Jun. 2023 Chatopera Inc. <https://www.chatopera.com>. All rights reserved.
|
||||
*/
|
||||
package com.cskefu.cc.basic.auth;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class BasicTokenMgr {
|
||||
final static Logger logger = LoggerFactory.getLogger(BasicTokenMgr.class);
|
||||
|
||||
/**
|
||||
* Generate basic token with username and password
|
||||
*
|
||||
* @param username
|
||||
* @param password
|
||||
* @return
|
||||
*/
|
||||
public String generate(final String username, final String password) {
|
||||
return null;
|
||||
}
|
||||
}
|
@ -1,132 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd.
|
||||
* <https://www.chatopera.com>, Licensed under the Chunsong Public
|
||||
* License, Version 1.0 (the "License"), https://docs.cskefu.com/licenses/v1.html
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* Copyright (C) 2019-2022 Chatopera Inc, <https://www.chatopera.com>,
|
||||
* Licensed under the Apache License, Version 2.0,
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*/
|
||||
package com.cskefu.cc.basic.auth;
|
||||
|
||||
import com.cskefu.cc.basic.Constants;
|
||||
import com.cskefu.cc.cache.RedisKey;
|
||||
import com.cskefu.cc.model.User;
|
||||
import com.cskefu.cc.util.SerializeUtil;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.data.redis.core.ValueOperations;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* 认证和授权的API Token管理
|
||||
*/
|
||||
@Component
|
||||
public class BearerTokenMgr {
|
||||
|
||||
private final static Logger logger = LoggerFactory.getLogger(BearerTokenMgr.class);
|
||||
|
||||
@Value("${server.session-timeout}")
|
||||
private int timeout;
|
||||
|
||||
@Autowired
|
||||
private AuthRedisTemplate authRedisTemplate;
|
||||
|
||||
private ValueOperations<String, String> redisValOps;
|
||||
|
||||
@PostConstruct
|
||||
private void init() {
|
||||
redisValOps = authRedisTemplate.opsForValue();
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove token with Bearer prefix
|
||||
*
|
||||
* @param token
|
||||
* @return
|
||||
*/
|
||||
private String trimToken(final String token) {
|
||||
if (token.startsWith(Constants.AUTH_TOKEN_TYPE_BEARER)) {
|
||||
return StringUtils.substring(token, 7);
|
||||
}
|
||||
return token;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置一个KEY的过期时间
|
||||
*
|
||||
* @param key
|
||||
* @param seconds
|
||||
*/
|
||||
private void expire(final String key, final long seconds) {
|
||||
authRedisTemplate.expire(key, seconds, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
private String resolveTokenKey(final String token) {
|
||||
return RedisKey.getApiTokenBearerKeyWithValue(trimToken(token));
|
||||
}
|
||||
|
||||
/**********************************
|
||||
* LOGIN USER API TOKEN 相关
|
||||
* 认证,授权,登录用户
|
||||
**********************************/
|
||||
|
||||
/**
|
||||
* @param token 授权的KEY
|
||||
* @param user 已经登录的用户
|
||||
*/
|
||||
public void update(final String token, final User user) {
|
||||
if (StringUtils.isNotBlank(token) && user != null) {
|
||||
String serialized = SerializeUtil.serialize(user);
|
||||
final String key = resolveTokenKey(token);
|
||||
redisValOps.set(key, serialized);
|
||||
expire(key, timeout);
|
||||
} else {
|
||||
logger.warn("[putLoginUserByAuth] error Invalid params.");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 判断一个Auth是否是有效的
|
||||
*
|
||||
* @param token
|
||||
* @return
|
||||
*/
|
||||
public boolean existToken(final String token) {
|
||||
return authRedisTemplate.hasKey(resolveTokenKey(token));
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据租户ID和认证Auth获得一个登录用户
|
||||
*
|
||||
* @param token
|
||||
* @return
|
||||
*/
|
||||
public User retrieve(final String token) {
|
||||
String serialized = redisValOps.get(resolveTokenKey(token));
|
||||
if (StringUtils.isNotBlank(serialized)) {
|
||||
return (User) SerializeUtil.deserialize(serialized);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 登出已经登录的系统用户
|
||||
*
|
||||
* @param token
|
||||
*/
|
||||
public void delete(final String token) {
|
||||
authRedisTemplate.delete(resolveTokenKey(token));
|
||||
}
|
||||
}
|
@ -1,34 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd.
|
||||
* <https://www.chatopera.com>, Licensed under the Chunsong Public
|
||||
* License, Version 1.0 (the "License"), https://docs.cskefu.com/licenses/v1.html
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.cskefu.cc.basic.plugins;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public abstract class AbstractPluginConfigurer implements IPluginConfigurer {
|
||||
|
||||
public abstract String getPluginId();
|
||||
|
||||
public abstract String getPluginName();
|
||||
|
||||
public abstract String getIOEventHandler();
|
||||
|
||||
public Map<String, String> getEnvironmentVariables() {
|
||||
Map<String, String> env = new HashMap<>();
|
||||
return env;
|
||||
}
|
||||
|
||||
public boolean isModule() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public abstract void setup();
|
||||
}
|
@ -1,36 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd.
|
||||
* <https://www.chatopera.com>, Licensed under the Chunsong Public
|
||||
* License, Version 1.0 (the "License"), https://docs.cskefu.com/licenses/v1.html
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* Copyright (C) 2019-2022 Chatopera Inc, <https://www.chatopera.com>,
|
||||
* Licensed under the Apache License, Version 2.0,
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*/
|
||||
package com.cskefu.cc.basic.plugins;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
public interface IPluginConfigurer {
|
||||
// 插件的ID:插件的标识,用于区别其它插件,由[a-z]组成,最大32位长度
|
||||
String getPluginId();
|
||||
|
||||
// 插件的名字:最少的概述插件
|
||||
String getPluginName();
|
||||
|
||||
// 即时通信接口
|
||||
String getIOEventHandler();
|
||||
|
||||
// 获得环境变量及默认值
|
||||
Map<String, String> getEnvironmentVariables();
|
||||
|
||||
// 是否是Module(在一级菜单有入口的插件)
|
||||
boolean isModule();
|
||||
|
||||
// 安装插件
|
||||
public void setup();
|
||||
}
|
@ -1,97 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd.
|
||||
* <https://www.chatopera.com>, Licensed under the Chunsong Public
|
||||
* License, Version 1.0 (the "License"), https://docs.cskefu.com/licenses/v1.html
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* Copyright (C) 2019-2022 Chatopera Inc, <https://www.chatopera.com>,
|
||||
* Licensed under the Apache License, Version 2.0,
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*/
|
||||
package com.cskefu.cc.basic.plugins;
|
||||
|
||||
import com.cskefu.cc.basic.MainContext;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* 插件注册表
|
||||
*/
|
||||
@Component
|
||||
public class PluginRegistry {
|
||||
/**
|
||||
* Plugins Entry
|
||||
*/
|
||||
public final static String PLUGIN_CHANNEL_MESSAGER_SUFFIX = "ChannelMessager";
|
||||
|
||||
public final static String PLUGIN_CHATBOT_MESSAGER_SUFFIX = "ChatbotMessager";
|
||||
|
||||
// 插件列表
|
||||
private final List<IPluginConfigurer> plugins = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* 添加插件
|
||||
*
|
||||
* @param plugin
|
||||
*/
|
||||
public void addPlugin(final IPluginConfigurer plugin) {
|
||||
for (final IPluginConfigurer x : plugins) {
|
||||
if (StringUtils.equalsIgnoreCase(x.getPluginId(), plugin.getPluginId())) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (StringUtils.isNotBlank(plugin.getPluginId())) {
|
||||
MainContext.enableModule(plugin.getPluginId());
|
||||
}
|
||||
|
||||
plugins.add(plugin);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获得插件列表
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public List<IPluginConfigurer> getPlugins() {
|
||||
return plugins;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获得一个插件
|
||||
*
|
||||
* @param pluginId
|
||||
* @return
|
||||
*/
|
||||
public Optional<IPluginConfigurer> getPlugin(final String pluginId) {
|
||||
IPluginConfigurer p = null;
|
||||
for (final IPluginConfigurer plugin : plugins) {
|
||||
if (StringUtils.equalsIgnoreCase(plugin.getPluginId(), pluginId)) {
|
||||
p = plugin;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return Optional.ofNullable(p);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除插件
|
||||
*
|
||||
* @param pluginId
|
||||
*/
|
||||
public void removePlugin(final String pluginId) {
|
||||
for (final IPluginConfigurer plugin : plugins) {
|
||||
if (StringUtils.equalsIgnoreCase(plugin.getPluginId(), pluginId)) {
|
||||
plugins.remove(plugin);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,71 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd.
|
||||
* <https://www.chatopera.com>, Licensed under the Chunsong Public
|
||||
* License, Version 1.0 (the "License"), https://docs.cskefu.com/licenses/v1.html
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* Copyright (C) 2018- Jun. 2023 Chatopera Inc, <https://www.chatopera.com>, Licensed under the Apache License, Version 2.0,
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Copyright (C) 2017 优客服-多渠道客服系统, Licensed under the Apache License, Version 2.0,
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*/
|
||||
package com.cskefu.cc.basic.resource;
|
||||
|
||||
import com.cskefu.cc.model.JobDetail;
|
||||
import com.cskefu.cc.util.es.UKDataBean;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public class OutputTextFormat {
|
||||
private String id ;
|
||||
private String title ;
|
||||
private String parent ;
|
||||
|
||||
private Map<String , Object> data = new HashMap<>();
|
||||
private JobDetail job ;
|
||||
private UKDataBean dataBean ;
|
||||
|
||||
public OutputTextFormat(JobDetail job){
|
||||
this.job = job ;
|
||||
}
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
public void setId(String id) {
|
||||
this.id = id;
|
||||
}
|
||||
public String getTitle() {
|
||||
return title;
|
||||
}
|
||||
public void setTitle(String title) {
|
||||
this.title = title;
|
||||
}
|
||||
public Map<String, Object> getData() {
|
||||
return data;
|
||||
}
|
||||
public void setData(Map<String, Object> data) {
|
||||
this.data = data;
|
||||
}
|
||||
public JobDetail getJob() {
|
||||
return job;
|
||||
}
|
||||
public void setJob(JobDetail job) {
|
||||
this.job = job;
|
||||
}
|
||||
public String getParent() {
|
||||
return parent;
|
||||
}
|
||||
public void setParent(String parent) {
|
||||
this.parent = parent;
|
||||
}
|
||||
public UKDataBean getDataBean() {
|
||||
return dataBean;
|
||||
}
|
||||
public void setDataBean(UKDataBean dataBean) {
|
||||
this.dataBean = dataBean;
|
||||
}
|
||||
}
|
@ -1,112 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd.
|
||||
* <https://www.chatopera.com>, Licensed under the Chunsong Public
|
||||
* License, Version 1.0 (the "License"), https://docs.cskefu.com/licenses/v1.html
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* Copyright (C) 2018- Jun. 2023 Chatopera Inc, <https://www.chatopera.com>, Licensed under the Apache License, Version 2.0,
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Copyright (C) 2017 优客服-多渠道客服系统, Licensed under the Apache License, Version 2.0,
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*/
|
||||
package com.cskefu.cc.basic.resource;
|
||||
|
||||
import com.cskefu.cc.basic.MainContext;
|
||||
import com.cskefu.cc.model.JobDetail;
|
||||
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* @author jaddy0302 Rivulet Resource.java 2010-3-6
|
||||
*
|
||||
*/
|
||||
public abstract class Resource {
|
||||
|
||||
public static Logger log = LoggerFactory.getLogger(Resource.class.getName()) ;
|
||||
|
||||
public abstract void begin() throws Exception;
|
||||
|
||||
|
||||
public abstract void end(boolean clear) throws Exception;
|
||||
/**
|
||||
* Re connection
|
||||
*/
|
||||
public abstract JobDetail getJob();
|
||||
|
||||
/**
|
||||
* Re connection
|
||||
*/
|
||||
public abstract void process(OutputTextFormat meta , JobDetail job)throws Exception;
|
||||
|
||||
/**
|
||||
* synchronized
|
||||
* Single-mode single-threaded access to records under a record
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public abstract OutputTextFormat next() throws Exception;
|
||||
|
||||
/**
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public abstract boolean isAvailable() ;
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public abstract OutputTextFormat getText(OutputTextFormat object) throws Exception;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public abstract void rmResource() ;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public abstract void updateTask()throws Exception ;
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* @param job
|
||||
* @return
|
||||
* @throws IllegalAccessException
|
||||
* @throws InstantiationException
|
||||
* @throws NoSuchMethodException
|
||||
* @throws SecurityException
|
||||
* @throws InvocationTargetException
|
||||
* @throws IllegalArgumentException
|
||||
*/
|
||||
public static Resource getResource(JobDetail job)
|
||||
throws Exception{
|
||||
return job != null
|
||||
&& MainContext.getResource(job.getTasktype()) != null ? (Resource) MainContext
|
||||
.getResource(job.getTasktype()).getConstructor(
|
||||
new Class[] { JobDetail.class }).newInstance(
|
||||
new Object[] { job })
|
||||
: null;
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Filter
|
||||
* @param file
|
||||
* @param netFile
|
||||
* @return
|
||||
*/
|
||||
public boolean val(String inputFile , String acceptDocType){
|
||||
String file = inputFile!=null ? inputFile.toLowerCase() :null ;
|
||||
return file!=null && acceptDocType!=null && ((acceptDocType.contains(file.substring(file.lastIndexOf(".") + 1)) || acceptDocType.contains("all"))) ;
|
||||
}
|
||||
|
||||
}
|
@ -1,822 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd.
|
||||
* <https://www.chatopera.com>, Licensed under the Chunsong Public
|
||||
* License, Version 1.0 (the "License"), https://docs.cskefu.com/licenses/v1.html
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* Copyright (C) 2019-2022 Chatopera Inc, <https://www.chatopera.com>,
|
||||
* Licensed under the Apache License, Version 2.0,
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*/
|
||||
package com.cskefu.cc.cache;
|
||||
|
||||
import com.cskefu.cc.aspect.AgentUserAspect;
|
||||
import com.cskefu.cc.basic.MainContext;
|
||||
import com.cskefu.cc.exception.CSKefuCacheException;
|
||||
import com.cskefu.cc.model.*;
|
||||
import com.cskefu.cc.persistence.repository.AgentUserRepository;
|
||||
import com.cskefu.cc.persistence.repository.PassportWebIMUserRepository;
|
||||
import com.cskefu.cc.util.SerializeUtil;
|
||||
import com.cskefu.cc.util.freeswitch.model.CallCenterAgent;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.*;
|
||||
|
||||
@Component
|
||||
public class Cache {
|
||||
|
||||
final static private Logger logger = LoggerFactory.getLogger(Cache.class);
|
||||
|
||||
@Autowired
|
||||
private PassportWebIMUserRepository onlineUserRes;
|
||||
|
||||
@Autowired
|
||||
private AgentUserRepository agentUserRes;
|
||||
|
||||
@Autowired
|
||||
private RedisCommand redisCommand;
|
||||
|
||||
/**
|
||||
* 获得就绪的坐席列表
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public Map<String, AgentStatus> getAgentStatusReady() {
|
||||
Map<String, String> agentStatuses = redisCommand.getHash(RedisKey.getAgentStatusReadyHashKey());
|
||||
return convertFromStringToAgentStatus(agentStatuses);
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过访客ID获得访客坐席关联关系
|
||||
*
|
||||
* @param userId
|
||||
* @return
|
||||
*/
|
||||
public Optional<AgentUser> findOneAgentUserByUserId(final String userId) {
|
||||
if (redisCommand.hasHashKV(RedisKey.getAgentUserInQueHashKey(), userId)) {
|
||||
// 排队等待中
|
||||
return Optional.ofNullable((AgentUser) SerializeUtil.deserialize(
|
||||
redisCommand.getHashKV(RedisKey.getAgentUserInQueHashKey(), userId)));
|
||||
} else if (redisCommand.hasHashKV(RedisKey.getAgentUserInServHashKey(), userId)) {
|
||||
// 服务中
|
||||
return Optional.ofNullable((AgentUser) SerializeUtil.deserialize(
|
||||
redisCommand.getHashKV(RedisKey.getAgentUserInServHashKey(), userId)));
|
||||
} else if (redisCommand.hasHashKV(RedisKey.getAgentUserEndHashKey(), userId)) {
|
||||
// 已经结束
|
||||
return Optional.ofNullable((AgentUser) SerializeUtil.deserialize(
|
||||
redisCommand.getHashKV(RedisKey.getAgentUserEndHashKey(), userId)));
|
||||
} else {
|
||||
// 缓存中没有找到,继续到数据库查找
|
||||
return agentUserRes.findOneByUserid(userId);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 返回排队中的客服列表
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public Map<String, AgentUser> getAgentUsersInQue() {
|
||||
Map<String, String> agentUsers = redisCommand.getHash(RedisKey.getAgentUserInQueHashKey());
|
||||
Map<String, AgentUser> map = new HashMap<>();
|
||||
for (final Map.Entry<String, String> entry : agentUsers.entrySet()) {
|
||||
final AgentUser obj = SerializeUtil.deserialize(entry.getValue());
|
||||
map.put(obj.getId(), obj);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将访客ID从服务中队列中删除
|
||||
* TODO 将访客对应的客服的服务列表变更
|
||||
*
|
||||
* @param userid
|
||||
*/
|
||||
public void deleteAgentUserInservByAgentUserId(final String userid) {
|
||||
redisCommand.delHashKV(RedisKey.getAgentUserInServHashKey(), userid);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 将访客ID从排队队列中删除
|
||||
*
|
||||
* @param userid
|
||||
*/
|
||||
public void deleteAgentUserInqueByAgentUserId(final String userid) {
|
||||
redisCommand.delHashKV(RedisKey.getAgentUserInQueHashKey(), userid);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获得一个坐席的状态
|
||||
*
|
||||
* @param agentno 坐席ID
|
||||
* @return
|
||||
*/
|
||||
public AgentStatus findOneAgentStatusByAgentno(final String agentno) {
|
||||
String status = getAgentStatusStatus(agentno);
|
||||
logger.debug("[findOneAgentStatusByAgentnoAndOrig] agentno {}, status {}", agentno, status);
|
||||
|
||||
// 缓存中没有该坐席状态,该坐席目前是离线的
|
||||
if (StringUtils.equals(status, MainContext.AgentStatusEnum.OFFLINE.toString())) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String val = redisCommand.getHashKV(RedisKey.getAgentStatusHashKeyByStatusStr(status), agentno);
|
||||
AgentStatus result = SerializeUtil.deserialize(val);
|
||||
logger.debug("[findOneAgentStatusByAgentnoAndOrig] result: username {}", result.getUsername());
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新坐席状态
|
||||
*
|
||||
* @param agentStatus
|
||||
*/
|
||||
public void putAgentStatus(AgentStatus agentStatus) {
|
||||
String pre = getAgentStatusStatus(agentStatus.getAgentno()); // 坐席前状态
|
||||
|
||||
if (StringUtils.equals(pre, MainContext.AgentStatusEnum.OFFLINE.toString())) {
|
||||
// 之前不存在,新建缓存
|
||||
if ((!StringUtils.equals(agentStatus.getStatus(), MainContext.AgentStatusEnum.OFFLINE.toString()))) {
|
||||
redisCommand.setHashKV(
|
||||
RedisKey.getAgentStatusHashKeyByStatusStr(agentStatus.getStatus()),
|
||||
agentStatus.getAgentno(), SerializeUtil.serialize(agentStatus));
|
||||
}
|
||||
return;
|
||||
} else {
|
||||
// 之前存在,与将要更新的状态一致
|
||||
if (StringUtils.equals(pre, agentStatus.getStatus())) {
|
||||
redisCommand.setHashKV(
|
||||
RedisKey.getAgentStatusHashKeyByStatusStr(pre), agentStatus.getAgentno(),
|
||||
SerializeUtil.serialize(agentStatus));
|
||||
return;
|
||||
} else {
|
||||
// 之前存在,而且与新状态不一致
|
||||
redisCommand.delHashKV(RedisKey.getAgentStatusHashKeyByStatusStr(pre), agentStatus.getAgentno());
|
||||
if (!StringUtils.equals(agentStatus.getStatus(), MainContext.AgentStatusEnum.OFFLINE.toString())) {
|
||||
redisCommand.setHashKV(
|
||||
RedisKey.getAgentStatusHashKeyByStatusStr(agentStatus.getStatus()),
|
||||
agentStatus.getAgentno(), SerializeUtil.serialize(agentStatus));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获得一个租户的就绪坐席状态
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public Map<String, AgentStatus> findAllReadyAgentStatus() {
|
||||
List<String> keys = new ArrayList<>();
|
||||
keys.add(RedisKey.getAgentStatusReadyHashKey());
|
||||
|
||||
Map<String, String> map = redisCommand.getAllMembersInMultiHash(keys);
|
||||
return convertFromStringToAgentStatus(map);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获得一个租户的所有坐席状态
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public Map<String, AgentStatus> findAllAgentStatus() {
|
||||
List<String> keys = new ArrayList<>();
|
||||
// TODO 增加支持更多状态
|
||||
keys.add(RedisKey.getAgentStatusReadyHashKey());
|
||||
keys.add(RedisKey.getAgentStatusNotReadyHashKey());
|
||||
|
||||
Map<String, String> map = redisCommand.getAllMembersInMultiHash(keys);
|
||||
return convertFromStringToAgentStatus(map);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Inline方法
|
||||
*/
|
||||
private static Map<String, AgentStatus> convertFromStringToAgentStatus(final Map<String, String> map) {
|
||||
Map<String, AgentStatus> result = new HashMap<>();
|
||||
for (Map.Entry<String, String> entry : map.entrySet()) {
|
||||
AgentStatus obj = SerializeUtil.deserialize(entry.getValue());
|
||||
result.put(entry.getKey(), obj);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Delete Agent Status
|
||||
*
|
||||
* @param agentno
|
||||
*/
|
||||
public void deleteAgentStatusByAgentno(final String agentno) {
|
||||
String status = getAgentStatusStatus(agentno);
|
||||
if (!StringUtils.equals(MainContext.AgentStatusEnum.OFFLINE.toString(), status)) {
|
||||
redisCommand.delHashKV(RedisKey.getAgentStatusHashKeyByStatusStr(status), agentno);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获得一个坐席的状态 agentStatus.status
|
||||
* 只返回大类状态
|
||||
*
|
||||
* @param agentno
|
||||
* @return
|
||||
*/
|
||||
private String getAgentStatusStatus(final String agentno) {
|
||||
// 首先判断这个坐席的状态是READY还是BUSY,再去更新
|
||||
if (redisCommand.hasHashKV(RedisKey.getAgentStatusReadyHashKey(), agentno)) {
|
||||
return MainContext.AgentStatusEnum.READY.toString();
|
||||
} else if (redisCommand.hasHashKV(RedisKey.getAgentStatusNotReadyHashKey(), agentno)) {
|
||||
return MainContext.AgentStatusEnum.NOTREADY.toString();
|
||||
} else {
|
||||
return MainContext.AgentStatusEnum.OFFLINE.toString();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获得技能组的坐席状态
|
||||
*
|
||||
* @param skill
|
||||
* @return
|
||||
*/
|
||||
public List<AgentStatus> getAgentStatusBySkill(final String skill) {
|
||||
Map<String, AgentStatus> map = findAllAgentStatus();
|
||||
List<AgentStatus> agentList = new ArrayList<>();
|
||||
|
||||
for (Map.Entry<String, AgentStatus> entry : map.entrySet()) {
|
||||
if (StringUtils.isNotBlank(skill)) {
|
||||
if (entry.getValue().getSkills() != null &&
|
||||
entry.getValue().getSkills().containsKey(skill)) {
|
||||
agentList.add(entry.getValue());
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
agentList.add(entry.getValue());
|
||||
}
|
||||
}
|
||||
return agentList;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获得指定租户的就绪的坐席个数
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public int getAgentStatusReadySize() {
|
||||
return Math.toIntExact(redisCommand.getHashSize(RedisKey.getAgentStatusReadyHashKey()));
|
||||
}
|
||||
|
||||
|
||||
/**************************
|
||||
* AgentUser相关
|
||||
**************************/
|
||||
|
||||
/**
|
||||
* 更新坐席访客关联关系
|
||||
* TODO 更新坐席的访客列表信息,增加新的访客信息
|
||||
* 包括:从等待到服务中;从等待、服务中到删除
|
||||
* 但是此处并不包含"转接"访客给其它坐席的情况,其它坐席的关联此处会完成。
|
||||
* 但是之前那个关联坐席的信息需要删除,要另行维护
|
||||
*
|
||||
* @param agentUser 最新的agentUser的状态
|
||||
*/
|
||||
@AgentUserAspect.LinkAgentUser
|
||||
public void putAgentUser(AgentUser agentUser) {
|
||||
if (redisCommand.hasHashKV(RedisKey.getAgentUserInServHashKey(), agentUser.getUserid())) {
|
||||
// 服务中
|
||||
if (!StringUtils.equals(
|
||||
agentUser.getStatus(),
|
||||
MainContext.AgentUserStatusEnum.INSERVICE.toString())) {
|
||||
// 删除旧记录
|
||||
redisCommand.delHashKV(RedisKey.getAgentUserInServHashKey(), agentUser.getUserid());
|
||||
}
|
||||
} else if (redisCommand.hasHashKV(RedisKey.getAgentUserInQueHashKey(), agentUser.getUserid())) {
|
||||
// 等待服务
|
||||
if (!StringUtils.equals(
|
||||
agentUser.getStatus(),
|
||||
MainContext.AgentUserStatusEnum.INQUENE.toString())) {
|
||||
// 删除旧记录
|
||||
redisCommand.delHashKV(RedisKey.getAgentUserInQueHashKey(), agentUser.getUserid());
|
||||
}
|
||||
}
|
||||
|
||||
// 更新新记录,忽略状态为END的agentUser,已结束的服务不加入缓存
|
||||
if (!StringUtils.equals(agentUser.getStatus(), MainContext.AgentUserStatusEnum.END.toString())) {
|
||||
redisCommand.setHashKV(
|
||||
RedisKey.getAgentUserHashKeyByStatusStr(agentUser.getStatus()), agentUser.getUserid(),
|
||||
SerializeUtil.serialize(agentUser));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获得一个客服服务中的访客列表
|
||||
*
|
||||
* @param agentno
|
||||
* @return
|
||||
*/
|
||||
public List<AgentUser> findInservAgentUsersByAgentno(final String agentno) {
|
||||
logger.info("[findInservAgentUsersByAgentno] agentno {}", agentno);
|
||||
List<AgentUser> result = new ArrayList<>();
|
||||
List<String> ids = redisCommand.getSet(RedisKey.getInServAgentUsersByAgentno(agentno));
|
||||
if (ids.size() == 0) { // no inserv agentUser
|
||||
return result;
|
||||
} else {
|
||||
result = agentUserRes.findAllByUserids(ids);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获得一个坐席服务中的访客数量
|
||||
*
|
||||
* @param agentno
|
||||
* @return
|
||||
*/
|
||||
public int getInservAgentUsersSizeByAgentno(final String agentno) {
|
||||
return Math.toIntExact(redisCommand.getSetSize(RedisKey.getInServAgentUsersByAgentno(agentno)));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获得服务中的访客的数量
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public int getInservAgentUsersSize() {
|
||||
return redisCommand.getHashSize(RedisKey.getAgentUserInServHashKey());
|
||||
}
|
||||
|
||||
/**
|
||||
* 获得等待中的访客的数量
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public int getInqueAgentUsersSize() {
|
||||
return redisCommand.getHashSize(RedisKey.getAgentUserInQueHashKey());
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete agentUser
|
||||
*
|
||||
* @param agentUser
|
||||
*/
|
||||
@AgentUserAspect.LinkAgentUser
|
||||
public void deleteAgentUserByUserId(final AgentUser agentUser) {
|
||||
if (redisCommand.hasHashKV(RedisKey.getAgentUserInQueHashKey(), agentUser.getUserid())) {
|
||||
// 排队等待中
|
||||
redisCommand.delHashKV(RedisKey.getAgentUserInQueHashKey(), agentUser.getUserid());
|
||||
} else if (redisCommand.hasHashKV(RedisKey.getAgentUserInServHashKey(), agentUser.getUserid())) {
|
||||
redisCommand.delHashKV(RedisKey.getAgentUserInServHashKey(), agentUser.getUserid());
|
||||
} else if (redisCommand.hasHashKV(RedisKey.getAgentUserEndHashKey(), agentUser.getUserid())) {
|
||||
redisCommand.delHashKV(RedisKey.getAgentUserEndHashKey(), agentUser.getUserid());
|
||||
} else {
|
||||
// TODO 考虑是否有其他状态保存
|
||||
}
|
||||
}
|
||||
|
||||
/***************************
|
||||
* CousultInvite 相关
|
||||
***************************/
|
||||
public void putConsultInvite(final CousultInvite cousultInvite) {
|
||||
redisCommand.setHashKV(
|
||||
RedisKey.getConsultInvites(), cousultInvite.getSnsaccountid(),
|
||||
SerializeUtil.serialize(cousultInvite));
|
||||
}
|
||||
|
||||
public CousultInvite findOneConsultInviteBySnsid(final String snsid) {
|
||||
String serialized = redisCommand.getHashKV(RedisKey.getConsultInvites(), snsid);
|
||||
if (StringUtils.isBlank(serialized)) {
|
||||
return null;
|
||||
} else {
|
||||
return (CousultInvite) SerializeUtil.deserialize(serialized);
|
||||
}
|
||||
}
|
||||
|
||||
public void deleteConsultInviteBySnsid(final String snsid) {
|
||||
redisCommand.delHashKV(RedisKey.getConsultInvites(), snsid);
|
||||
}
|
||||
|
||||
|
||||
/****************************
|
||||
* OnlineUser相关
|
||||
****************************/
|
||||
|
||||
/**
|
||||
* 更新 onlineUser
|
||||
*
|
||||
* @param passportWebIMUser
|
||||
*/
|
||||
public void putOnlineUser(final PassportWebIMUser passportWebIMUser) {
|
||||
// 此处onlineUser的id 与 onlineUser userId相同
|
||||
redisCommand.setHashKV(
|
||||
RedisKey.getOnlineUserHashKey(), passportWebIMUser.getId(), SerializeUtil.serialize(passportWebIMUser));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获得 onlineUser
|
||||
*
|
||||
* @param id
|
||||
* @return
|
||||
*/
|
||||
public PassportWebIMUser findOneOnlineUserByUserId(final String id) {
|
||||
String serialized = redisCommand.getHashKV(RedisKey.getOnlineUserHashKey(), id);
|
||||
if (StringUtils.isBlank(serialized)) {
|
||||
// query with MySQL
|
||||
return onlineUserRes.findOneByUserid(id);
|
||||
} else {
|
||||
return convertFromStringToOnlineUser(serialized);
|
||||
}
|
||||
}
|
||||
|
||||
private static PassportWebIMUser convertFromStringToOnlineUser(final String serialized) {
|
||||
PassportWebIMUser obj = SerializeUtil.deserialize(serialized);
|
||||
return obj;
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除 onlineUser
|
||||
*
|
||||
* @param id
|
||||
*/
|
||||
public void deleteOnlineUserById(final String id) {
|
||||
redisCommand.delHashKV(RedisKey.getOnlineUserHashKey(), id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据租户ID获得在线访客的列表大小
|
||||
*/
|
||||
public int getOnlineUserSize() {
|
||||
return redisCommand.getHashSize(RedisKey.getOnlineUserHashKey());
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 将在线访客从一个坐席的服务列表中删除
|
||||
*
|
||||
* @param userid
|
||||
* @param agentno
|
||||
*/
|
||||
public void deleteOnlineUserIdFromAgentStatusByUseridAndAgentno(final String userid, final String agentno) {
|
||||
redisCommand.removeSetVal(RedisKey.getInServAgentUsersByAgentno(agentno), userid);
|
||||
}
|
||||
|
||||
private Map<String, PassportWebIMUser> convertFromStringToOnlineUsers(final Map<String, String> map) {
|
||||
Map<String, PassportWebIMUser> result = new HashMap<>();
|
||||
for (Map.Entry<String, String> entry : map.entrySet()) {
|
||||
PassportWebIMUser x = SerializeUtil.deserialize(entry.getValue());
|
||||
result.put(entry.getKey(), x);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
/******************************
|
||||
* Callcenter Agent 相关
|
||||
******************************/
|
||||
|
||||
/**
|
||||
* 更新CallCenterAgent
|
||||
*
|
||||
* @param id
|
||||
* @param agent
|
||||
*/
|
||||
public void putCallCenterAgentById(final String id, final CallCenterAgent agent) {
|
||||
redisCommand.setHashKV(RedisKey.getCallCenterAgentHashKey(), id, SerializeUtil.serialize(agent));
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据ID和租户ID获得CallCenterAgent
|
||||
*
|
||||
* @param id
|
||||
* @return
|
||||
*/
|
||||
public CallCenterAgent findOneCallCenterAgentById(final String id) {
|
||||
String serialized = redisCommand.getHashKV(RedisKey.getCallCenterAgentHashKey(), id);
|
||||
if (StringUtils.isNotBlank(serialized)) {
|
||||
return (CallCenterAgent) SerializeUtil.deserialize(serialized);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除CallCenterAgent
|
||||
*
|
||||
* @param id
|
||||
*/
|
||||
public void deleteCallCenterAgentById(final String id) {
|
||||
redisCommand.delHashKV(RedisKey.getCallCenterAgentHashKey(), id);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 根据租户ID获得所有的CallCenterAgent
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public Map<String, CallCenterAgent> findAllCallCenterAgents() {
|
||||
Map<String, String> map = redisCommand.getHash(RedisKey.getCallCenterAgentHashKey());
|
||||
Map<String, CallCenterAgent> result = new HashMap<>();
|
||||
|
||||
for (Map.Entry<String, String> entry : map.entrySet()) {
|
||||
result.put(entry.getKey(), SerializeUtil.deserialize(entry.getValue()));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 访客黑名单
|
||||
*/
|
||||
// 将访客放在租户的黑名单中
|
||||
public void putBlackEntity(final BlackEntity blackEntity) {
|
||||
redisCommand.setHashKV(
|
||||
RedisKey.getBlackEntityKey(), blackEntity.getUserid(), SerializeUtil.serialize(blackEntity));
|
||||
}
|
||||
|
||||
// 通过指定的访客和租户查找黑名单
|
||||
public Optional<BlackEntity> findOneBlackEntityByUserId(final String userid) {
|
||||
String ser = redisCommand.getHashKV(RedisKey.getBlackEntityKey(), userid);
|
||||
if (StringUtils.isBlank(ser)) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
return Optional.ofNullable(SerializeUtil.deserialize(ser));
|
||||
}
|
||||
|
||||
// 将一个访客从黑名单中移除
|
||||
public void deleteBlackEntityByUserId(final String userid) {
|
||||
redisCommand.delHashKV(RedisKey.getBlackEntityKey(), userid);
|
||||
}
|
||||
|
||||
// 指定的访客是否在租户的黑名单中
|
||||
public boolean existBlackEntityByUserId(final String userid) {
|
||||
return redisCommand.hasHashKV(RedisKey.getBlackEntityKey(), userid);
|
||||
}
|
||||
|
||||
// 根据租户ID获得所有访客的黑名单
|
||||
public Map<String, BlackEntity> findAllBlackEntity() {
|
||||
Map<String, BlackEntity> result = new HashMap<>();
|
||||
for (Map.Entry<String, String> entry : redisCommand.getHash(
|
||||
RedisKey.getBlackEntityKey()).entrySet()) {
|
||||
result.put(entry.getKey(), SerializeUtil.deserialize(entry.getValue()));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
/*****************************
|
||||
* Job 相关
|
||||
*****************************/
|
||||
public void putJobById(final String jobId, final JobDetail job) {
|
||||
redisCommand.setHashKV(RedisKey.getJobHashKey(), jobId, SerializeUtil.serialize(job));
|
||||
}
|
||||
|
||||
public JobDetail findOneJobById(final String jobId) {
|
||||
String serialized = redisCommand.getHashKV(RedisKey.getJobHashKey(), jobId);
|
||||
|
||||
if (StringUtils.isNotBlank(serialized)) {
|
||||
return (JobDetail) SerializeUtil.deserialize(serialized);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public boolean existJobById(final String jobId) {
|
||||
return redisCommand.hasHashKV(RedisKey.getJobHashKey(), jobId);
|
||||
}
|
||||
|
||||
public void deleteJobByJobId(final String jobId) {
|
||||
redisCommand.delHashKV(RedisKey.getJobHashKey(), jobId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 系统词典相关
|
||||
*/
|
||||
// 存储根词典
|
||||
public void putSysDic(final String id, final SysDic sysDic) {
|
||||
redisCommand.setHashKV(RedisKey.getSysDicHashKey(), id, SerializeUtil.serialize(sysDic));
|
||||
}
|
||||
|
||||
// 将指定租户的系统词典清空
|
||||
public void eraseSysDic() {
|
||||
redisCommand.delete(RedisKey.getSysDicHashKey());
|
||||
}
|
||||
|
||||
// 存储词典子项
|
||||
public void putSysDic(final String code, final List<SysDic> sysDics) {
|
||||
redisCommand.setHashKV(RedisKey.getSysDicHashKey(), code, SerializeUtil.serialize(sysDics));
|
||||
}
|
||||
|
||||
// 获得词典的子项列表
|
||||
public List<SysDic> getSysDicItemsByCode(final String code) {
|
||||
String serialized = redisCommand.getHashKV(RedisKey.getSysDicHashKey(), code);
|
||||
if (serialized != null) {
|
||||
return (List<SysDic>) SerializeUtil.deserialize(serialized);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// 获得词典子项
|
||||
public SysDic findOneSysDicByCode(final String code) {
|
||||
String serialized = redisCommand.getHashKV(RedisKey.getSysDicHashKey(), code);
|
||||
|
||||
if (StringUtils.isBlank(serialized)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (SysDic) SerializeUtil.deserialize(serialized);
|
||||
}
|
||||
|
||||
// 获得词典
|
||||
public SysDic findOneSysDicById(final String id) {
|
||||
String serialized = redisCommand.getHashKV(RedisKey.getSysDicHashKey(), id);
|
||||
|
||||
if (StringUtils.isBlank(serialized)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (SysDic) SerializeUtil.deserialize(serialized);
|
||||
}
|
||||
|
||||
// 批量存储
|
||||
public void putSysDic(List<SysDic> vals) {
|
||||
Map<String, String> map = new HashMap<>();
|
||||
for (final SysDic dic : vals) {
|
||||
map.put(dic.getId(), SerializeUtil.serialize(dic));
|
||||
}
|
||||
redisCommand.hmset(RedisKey.getSysDicHashKey(), map);
|
||||
}
|
||||
|
||||
public void deleteSysDicById(final String id) {
|
||||
redisCommand.delHashKV(RedisKey.getSysDicHashKey(), id);
|
||||
}
|
||||
|
||||
public boolean existSysDicById(final String id) {
|
||||
return redisCommand.hasHashKV(RedisKey.getSysDicHashKey(), id);
|
||||
}
|
||||
|
||||
/**
|
||||
* System 相关
|
||||
*/
|
||||
public <T extends Serializable> void putSystemById(final String id, final T obj) {
|
||||
redisCommand.setHashKV(RedisKey.getSystemHashKey(), id, SerializeUtil.serialize(obj));
|
||||
}
|
||||
|
||||
public <T extends Serializable> void putSystemListById(final String id, final List<T> obj) {
|
||||
redisCommand.setHashKV(RedisKey.getSystemHashKey(), id, SerializeUtil.serialize(obj));
|
||||
}
|
||||
|
||||
public <TK, TV extends Serializable> void putSystemMapById(final String id, final Map<TK, TV> obj) {
|
||||
redisCommand.setHashKV(RedisKey.getSystemHashKey(), id, SerializeUtil.serialize(obj));
|
||||
}
|
||||
|
||||
public boolean existSystemById(final String id) {
|
||||
return redisCommand.hasHashKV(RedisKey.getSystemHashKey(), id);
|
||||
}
|
||||
|
||||
public void deleteSystembyId(final String id) {
|
||||
redisCommand.delHashKV(RedisKey.getSystemHashKey(), id);
|
||||
}
|
||||
|
||||
public <T extends Serializable> T findOneSystemById(final String id) {
|
||||
String serialized = redisCommand.getHashKV(RedisKey.getSystemHashKey(), id);
|
||||
if (StringUtils.isNotBlank(serialized)) {
|
||||
return (T) SerializeUtil.deserialize(serialized);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public <T extends Serializable> List<T> findOneSystemListById(final String id) {
|
||||
String serialized = redisCommand.getHashKV(RedisKey.getSystemHashKey(), id);
|
||||
if (StringUtils.isNotBlank(serialized)) {
|
||||
return (List<T>) SerializeUtil.deserialize(serialized);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public <TK, TV extends Serializable> Map<TK, TV> findOneSystemMapById(final String id) {
|
||||
String serialized = redisCommand.getHashKV(RedisKey.getSystemHashKey(), id);
|
||||
if (StringUtils.isNotBlank(serialized)) {
|
||||
return (Map<TK, TV>) SerializeUtil.deserialize(serialized);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// 获得系统cache的列表大小
|
||||
public int getSystemSize() {
|
||||
return redisCommand.getHashSize(RedisKey.getSystemHashKey());
|
||||
}
|
||||
|
||||
/**************************
|
||||
* Session Config 相关
|
||||
**************************/
|
||||
|
||||
public void putSessionConfig(final SessionConfig sessionConfig, String organid) {
|
||||
redisCommand.put(RedisKey.getSessionConfig(organid), SerializeUtil.serialize(sessionConfig));
|
||||
}
|
||||
|
||||
public SessionConfig findOneSessionConfig(String organid) {
|
||||
String serialized = redisCommand.get(RedisKey.getSessionConfig(organid));
|
||||
if (StringUtils.isNotBlank(serialized)) {
|
||||
return (SessionConfig) SerializeUtil.deserialize(serialized);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public void deleteSessionConfig(String organid) {
|
||||
redisCommand.delete(RedisKey.getSessionConfig(organid));
|
||||
}
|
||||
|
||||
public boolean existSessionConfig(String organid) {
|
||||
return redisCommand.exists(RedisKey.getSessionConfig(organid));
|
||||
}
|
||||
|
||||
public void putSessionConfigList(final List<SessionConfig> lis) {
|
||||
redisCommand.put(RedisKey.getSessionConfigList(), SerializeUtil.serialize(lis));
|
||||
}
|
||||
|
||||
public List<SessionConfig> findOneSessionConfigList() {
|
||||
String serialized = redisCommand.get(RedisKey.getSessionConfigList());
|
||||
if (StringUtils.isNotBlank(serialized)) {
|
||||
return (List<SessionConfig>) SerializeUtil.deserialize(serialized);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public void deleteSessionConfigList() {
|
||||
redisCommand.delete(RedisKey.getSessionConfigList());
|
||||
}
|
||||
|
||||
public boolean existSessionConfigList() {
|
||||
return redisCommand.exists(RedisKey.getSessionConfigList());
|
||||
}
|
||||
|
||||
/******************************************
|
||||
* Customer Chats Audit 相关
|
||||
******************************************/
|
||||
public void putAgentUserAudit(final AgentUserAudit audit) throws CSKefuCacheException {
|
||||
if (StringUtils.isBlank(audit.getAgentUserId())) {
|
||||
throw new CSKefuCacheException("agentUserId is required.");
|
||||
}
|
||||
redisCommand.setHashKV(
|
||||
RedisKey.getCustomerChatsAuditKey(), audit.getAgentUserId(), SerializeUtil.serialize(audit));
|
||||
}
|
||||
|
||||
public void deleteAgentUserAuditById(final String agentUserId) {
|
||||
redisCommand.delHashKV(RedisKey.getCustomerChatsAuditKey(), agentUserId);
|
||||
}
|
||||
|
||||
public Optional<AgentUserAudit> findOneAgentUserAuditById(final String agentUserId) {
|
||||
logger.info("[findOneAgentUserAuditById] agentUserId {}", agentUserId);
|
||||
String serialized = redisCommand.getHashKV(RedisKey.getCustomerChatsAuditKey(), agentUserId);
|
||||
if (StringUtils.isBlank(serialized)) {
|
||||
return Optional.empty();
|
||||
}
|
||||
return Optional.ofNullable((AgentUserAudit) SerializeUtil.deserialize(serialized));
|
||||
}
|
||||
|
||||
public boolean existAgentUserAuditById(final String agentUserId) {
|
||||
return redisCommand.hasHashKV(RedisKey.getCustomerChatsAuditKey(), agentUserId);
|
||||
}
|
||||
|
||||
|
||||
/******************************************
|
||||
* User Session 相关
|
||||
******************************************/
|
||||
/**
|
||||
* 存入user的session,存储这组信息是为了让客户的账号只能在一个浏览器内登录使用
|
||||
* 如果一个用户账号在多个浏览器使用,则登出之前的登录,只保留最后一个登录正常使用
|
||||
*
|
||||
* @param agentno
|
||||
* @param sessionId
|
||||
*/
|
||||
public void putUserSessionByAgentnoAndSessionId(final String agentno, final String sessionId) {
|
||||
redisCommand.setHashKV(RedisKey.getUserSessionKey(), agentno, sessionId);
|
||||
}
|
||||
|
||||
public boolean existUserSessionByAgentno(final String agentno) {
|
||||
return redisCommand.hasHashKV(RedisKey.getUserSessionKey(), agentno);
|
||||
}
|
||||
|
||||
public String findOneSessionIdByAgentno(final String agentno) {
|
||||
return redisCommand.getHashKV(RedisKey.getUserSessionKey(), agentno);
|
||||
}
|
||||
|
||||
public void deleteUserSessionByAgentno(final String agentno) {
|
||||
redisCommand.delHashKV(RedisKey.getUserSessionKey(), agentno);
|
||||
}
|
||||
|
||||
}
|
@ -1,304 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd.
|
||||
* <https://www.chatopera.com>, Licensed under the Chunsong Public
|
||||
* License, Version 1.0 (the "License"), https://docs.cskefu.com/licenses/v1.html
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* Copyright (C) 2019-2022 Chatopera Inc, <https://www.chatopera.com>,
|
||||
* Licensed under the Apache License, Version 2.0,
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*/
|
||||
package com.cskefu.cc.cache;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.data.redis.core.*;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
|
||||
@Component
|
||||
public class RedisCommand {
|
||||
|
||||
final static private Logger logger = LoggerFactory.getLogger(RedisCommand.class);
|
||||
|
||||
private ListOperations<String, String> redisListOps;
|
||||
private HashOperations<String, String, String> redisHashOps;
|
||||
private ValueOperations<String, String> redisValOps;
|
||||
private SetOperations<String, String> redisSetOps;
|
||||
|
||||
/**
|
||||
* 使用StringRedisTemplate而不是RedisTemplate解决序列化问题
|
||||
* https://stackoverflow.com/questions/13215024/weird-redis-key-with-spring-data-jedis
|
||||
*/
|
||||
@Autowired
|
||||
private StringRedisTemplate redis;
|
||||
|
||||
@PostConstruct
|
||||
private void init() {
|
||||
redisListOps = redis.opsForList();
|
||||
redisHashOps = redis.opsForHash();
|
||||
redisValOps = redis.opsForValue();
|
||||
redisSetOps = redis.opsForSet();
|
||||
}
|
||||
|
||||
|
||||
/*****************************
|
||||
* String 相关
|
||||
*****************************/
|
||||
|
||||
/**
|
||||
* 设置一个KEY
|
||||
*
|
||||
* @param key
|
||||
* @param serialized
|
||||
*/
|
||||
public void put(final String key, final String serialized) {
|
||||
boolean result = true;
|
||||
redisValOps.set(key, serialized);
|
||||
}
|
||||
|
||||
public String get(final String key) {
|
||||
return redisValOps.get(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除一个KEY
|
||||
*
|
||||
* @param key
|
||||
*/
|
||||
public void delete(final String key) {
|
||||
redis.delete(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置一个KEY的过期时间
|
||||
*
|
||||
* @param key
|
||||
* @param seconds
|
||||
*/
|
||||
public void expire(final String key, final long seconds) {
|
||||
redis.expire(key, seconds, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获得一个KEY的过期时间
|
||||
*
|
||||
* @param key
|
||||
* @return
|
||||
*/
|
||||
public long ttl(final String key) {
|
||||
return redis.getExpire(key);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 是否存在一个KEY
|
||||
*
|
||||
* @param key
|
||||
* @return
|
||||
*/
|
||||
public boolean exists(final String key) {
|
||||
return redis.hasKey(key);
|
||||
}
|
||||
|
||||
|
||||
/*****************************
|
||||
* List 相关
|
||||
*****************************/
|
||||
|
||||
|
||||
/**
|
||||
* 在列表右侧追加
|
||||
*
|
||||
* @param key
|
||||
* @param val
|
||||
* @return
|
||||
*/
|
||||
public long appendList(final String key, final String val) {
|
||||
return redisListOps.rightPush(key, val);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获得一个列表的所有元素
|
||||
*
|
||||
* @param key
|
||||
* @return
|
||||
*/
|
||||
public List<String> getList(final String key) {
|
||||
return redisListOps.range(key, 0, redisListOps.size(key));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获得一个类标的长度
|
||||
*
|
||||
* @param key
|
||||
* @return
|
||||
*/
|
||||
public long listSize(final String key) {
|
||||
return redisListOps.size(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all value = val in key
|
||||
*
|
||||
* @param key
|
||||
* @param val
|
||||
*/
|
||||
public void listRemove(final String key, final String val) {
|
||||
redisListOps.remove(key, 0, val);
|
||||
}
|
||||
|
||||
|
||||
/*****************************
|
||||
* Hash 相关
|
||||
*****************************/
|
||||
|
||||
/**
|
||||
* 获得一个Hash的列表长度
|
||||
*
|
||||
* @param hashKey
|
||||
* @return
|
||||
*/
|
||||
public int getHashSize(final String hashKey) {
|
||||
return Math.toIntExact(redisHashOps.size(hashKey));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获得多个Hash的全部成员
|
||||
*
|
||||
* @param keys
|
||||
* @return
|
||||
*/
|
||||
public Map<String, String> getAllMembersInMultiHash(final List<String> keys) {
|
||||
return redis.execute((RedisCallback<Map<String, String>>) con -> {
|
||||
Map<String, String> ans = new HashMap<>();
|
||||
for (String key : keys) {
|
||||
Map<byte[], byte[]> result = con.hGetAll(key.getBytes());
|
||||
if (!CollectionUtils.isEmpty(result)) {
|
||||
for (Map.Entry<byte[], byte[]> entry : result.entrySet()) {
|
||||
ans.put(new String(entry.getKey()), new String(entry.getValue()));
|
||||
}
|
||||
}
|
||||
}
|
||||
return ans;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获得一个Hash中所有的值
|
||||
* https://juejin.im/post/5c1399a7f265da61764ac526
|
||||
*
|
||||
* @param hashKey
|
||||
* @return
|
||||
*/
|
||||
public Map<String, String> getHash(final String hashKey) {
|
||||
return redis.execute((RedisCallback<Map<String, String>>) con -> {
|
||||
Map<byte[], byte[]> result = con.hGetAll(hashKey.getBytes());
|
||||
if (CollectionUtils.isEmpty(result)) {
|
||||
return new HashMap<>(0);
|
||||
}
|
||||
|
||||
Map<String, String> ans = new HashMap<>(result.size());
|
||||
for (Map.Entry<byte[], byte[]> entry : result.entrySet()) {
|
||||
ans.put(new String(entry.getKey()), new String(entry.getValue()));
|
||||
}
|
||||
return ans;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置Hash Map KV
|
||||
*
|
||||
* @param hashKey
|
||||
* @param childKey
|
||||
* @param childVal
|
||||
*/
|
||||
public void setHashKV(final String hashKey, final String childKey, final String childVal) {
|
||||
redisHashOps.put(hashKey, childKey, childVal);
|
||||
}
|
||||
|
||||
/**
|
||||
* 指定的Hash实存存在键
|
||||
*
|
||||
* @param key
|
||||
* @param childKey
|
||||
* @return
|
||||
*/
|
||||
public boolean hasHashKV(String key, String childKey) {
|
||||
return redisHashOps.hasKey(key, childKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获得Hash Map KV的值
|
||||
*
|
||||
* @param hashKey
|
||||
* @param childKey
|
||||
* @return
|
||||
*/
|
||||
public String getHashKV(final String hashKey, final String childKey) {
|
||||
return redisHashOps.get(hashKey, childKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除Hash Map KV的值
|
||||
*
|
||||
* @param hashKey
|
||||
* @param childKey
|
||||
*/
|
||||
public void delHashKV(final String hashKey, final String childKey) {
|
||||
redisHashOps.delete(hashKey, childKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* HashSet
|
||||
* https://www.cnblogs.com/hongdada/p/9141125.html
|
||||
* 寻求使用 hmset 提升大量HASH KEY的存储 https://redis.io/commands/hmset
|
||||
* TODO 查看 putAll源代码确定是用hmset
|
||||
*
|
||||
* @param key 键
|
||||
* @param map 对应多个键值
|
||||
*/
|
||||
public void hmset(final String key, final Map<String, String> map) {
|
||||
try {
|
||||
redisHashOps.putAll(key, map);
|
||||
} catch (Exception e) {
|
||||
logger.error("hmset bad things happen", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/*****************************
|
||||
* Set 相关
|
||||
*****************************/
|
||||
|
||||
public void insertSetVal(final String key, final String val) {
|
||||
redisSetOps.add(key, val);
|
||||
}
|
||||
|
||||
public void removeSetVal(final String key, final String val) {
|
||||
redisSetOps.remove(key, val);
|
||||
}
|
||||
|
||||
public int getSetSize(final String key) {
|
||||
return Math.toIntExact(redisSetOps.size(key));
|
||||
}
|
||||
|
||||
public List<String> getSet(final String key) {
|
||||
Set<String> s = redisSetOps.members(key);
|
||||
|
||||
if (CollectionUtils.isEmpty(s)) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
return new ArrayList<>(s);
|
||||
}
|
||||
|
||||
}
|
@ -1,258 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd.
|
||||
* <https://www.chatopera.com>, Licensed under the Chunsong Public
|
||||
* License, Version 1.0 (the "License"), https://docs.cskefu.com/licenses/v1.html
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* Copyright (C) 2019-2022 Chatopera Inc, <https://www.chatopera.com>,
|
||||
* Licensed under the Apache License, Version 2.0,
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*/
|
||||
package com.cskefu.cc.cache;
|
||||
|
||||
import com.cskefu.cc.basic.MainContext;
|
||||
|
||||
public class RedisKey {
|
||||
|
||||
public static final String CACHE_SESSIONS = "sso";
|
||||
|
||||
/*********************
|
||||
*
|
||||
* 以下为Redis的常用KEY管理
|
||||
*
|
||||
*********************/
|
||||
|
||||
// AGENT STATUS 相关
|
||||
|
||||
/**
|
||||
* 获得坐席列表指定字符串状态的KEY
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public static String getAgentStatusHashKeyByStatusStr(final String status) {
|
||||
StringBuffer sb = new StringBuffer();
|
||||
sb.append("agent:status:");
|
||||
sb.append(status);
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 就绪的客服列表KEY
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public static String getAgentStatusReadyHashKey() {
|
||||
return getAgentStatusHashKeyByStatusStr(MainContext.AgentStatusEnum.READY.toString());
|
||||
}
|
||||
|
||||
/**
|
||||
* 未就绪的坐席
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public static String getAgentStatusNotReadyHashKey() {
|
||||
return getAgentStatusHashKeyByStatusStr(MainContext.AgentStatusEnum.NOTREADY.toString());
|
||||
}
|
||||
|
||||
|
||||
// AGENT USER 相关
|
||||
|
||||
/**
|
||||
* 获得坐席访客关联列表指定字符串状态的KEY
|
||||
*
|
||||
* @param status
|
||||
* @return
|
||||
*/
|
||||
public static String getAgentUserHashKeyByStatusStr(final String status) {
|
||||
StringBuffer sb = new StringBuffer();
|
||||
sb.append("agent:user:");
|
||||
sb.append(status);
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 排队中的访客KEY
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public static String getAgentUserInQueHashKey() {
|
||||
return getAgentUserHashKeyByStatusStr(MainContext.AgentUserStatusEnum.INQUENE.toString());
|
||||
}
|
||||
|
||||
/**
|
||||
* 服务中的访客
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public static String getAgentUserInServHashKey() {
|
||||
return getAgentUserHashKeyByStatusStr(MainContext.AgentUserStatusEnum.INSERVICE.toString());
|
||||
}
|
||||
|
||||
/**
|
||||
* 结束服务的访客
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public static String getAgentUserEndHashKey() {
|
||||
return getAgentUserHashKeyByStatusStr(MainContext.AgentUserStatusEnum.END.toString());
|
||||
}
|
||||
|
||||
/**
|
||||
* 获得一个坐席的服务中的访客列表KEY
|
||||
*/
|
||||
public static String getInServAgentUsersByAgentno(final String agentno) {
|
||||
StringBuffer sb = new StringBuffer();
|
||||
sb.append("agent:");
|
||||
sb.append(agentno);
|
||||
sb.append(":inserv");
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
|
||||
// Customer Chats Audit
|
||||
|
||||
/**
|
||||
* 存储AgentUser监控信息的存储Hash的KEY
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public static String getCustomerChatsAuditKey() {
|
||||
StringBuffer sb = new StringBuffer();
|
||||
sb.append("audit:customerchats");
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
|
||||
// ONLINE USER 相关
|
||||
|
||||
/**
|
||||
* 获得在线访客列表
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public static String getOnlineUserHashKey() {
|
||||
StringBuffer sb = new StringBuffer();
|
||||
sb.append("visitor:online");
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
// LOGIN USER 相关
|
||||
|
||||
/**
|
||||
* 已经登录的系统用户的API Auth Token
|
||||
* 包括管理员,坐席等,访客不在该列
|
||||
* 在该列表中的用户代表在线的系统用户,通过浏览器或API访问了系统
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public static String getApiTokenBearerKeyWithValue(final String token) {
|
||||
StringBuffer sb = new StringBuffer();
|
||||
sb.append("api:token:bearer:");
|
||||
sb.append(token);
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* CallCenter Agent 相关
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public static String getCallCenterAgentHashKey() {
|
||||
StringBuffer sb = new StringBuffer();
|
||||
sb.append("callcenter:agent");
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Job 相关
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public static String getJobHashKey() {
|
||||
StringBuffer sb = new StringBuffer();
|
||||
sb.append("job");
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* System 相关
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public static String getSystemHashKey() {
|
||||
StringBuffer sb = new StringBuffer();
|
||||
sb.append("system");
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 系统词典
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public static String getSysDicHashKey() {
|
||||
StringBuffer sb = new StringBuffer();
|
||||
sb.append("sysdic");
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 坐席会话配置相关
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public static String getSessionConfigList() {
|
||||
StringBuffer sb = new StringBuffer();
|
||||
sb.append("session:config:list");
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
public static String getSessionConfig(String organid) {
|
||||
StringBuffer sb = new StringBuffer();
|
||||
sb.append(organid);
|
||||
sb.append(":session:config");
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* SocketIO连接相关
|
||||
*/
|
||||
public static String getWebIMAgentSocketIOByAgentno(final String agentno) {
|
||||
StringBuffer sb = new StringBuffer();
|
||||
sb.append("agent:socketio:");
|
||||
sb.append(agentno);
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* CousultInvite 相关
|
||||
*/
|
||||
public static String getConsultInvites() {
|
||||
StringBuffer sb = new StringBuffer();
|
||||
sb.append("consultinvite");
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 和访客黑名单相关
|
||||
*/
|
||||
public static String getBlackEntityKey() {
|
||||
StringBuffer sb = new StringBuffer();
|
||||
sb.append("visitor:blacklist");
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 系统登录用户的会话Session信息
|
||||
*/
|
||||
public static String getUserSessionKey() {
|
||||
StringBuffer sb = new StringBuffer();
|
||||
sb.append("user:session");
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
}
|
@ -1,43 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd.
|
||||
* <https://www.chatopera.com>, Licensed under the Chunsong Public
|
||||
* License, Version 1.0 (the "License"), https://docs.cskefu.com/licenses/v1.html
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* Copyright (C) 2019-2022 Chatopera Inc, All rights reserved.
|
||||
* <https://www.chatopera.com>
|
||||
*/
|
||||
package com.cskefu.cc.config;
|
||||
|
||||
import jakarta.jms.ConnectionFactory;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.jms.annotation.EnableJms;
|
||||
import org.springframework.jms.config.DefaultJmsListenerContainerFactory;
|
||||
import org.springframework.jms.config.JmsListenerContainerFactory;
|
||||
|
||||
@EnableJms
|
||||
@Configuration
|
||||
public class ActiveMQConfigure {
|
||||
|
||||
// topic模式的ListenerContainer
|
||||
@Bean
|
||||
@SuppressWarnings("SpringJavaAutowiringInspection")
|
||||
public JmsListenerContainerFactory<?> jmsListenerContainerTopic(ConnectionFactory connectionFactory) {
|
||||
DefaultJmsListenerContainerFactory factory = new DefaultJmsListenerContainerFactory();
|
||||
factory.setConnectionFactory(connectionFactory);
|
||||
factory.setPubSubDomain(true);
|
||||
return factory;
|
||||
}
|
||||
|
||||
// queue模式的ListenerContainer
|
||||
@Bean
|
||||
public JmsListenerContainerFactory<?> jmsListenerContainerQueue(ConnectionFactory connectionFactory) {
|
||||
DefaultJmsListenerContainerFactory bean = new DefaultJmsListenerContainerFactory();
|
||||
bean.setConnectionFactory(connectionFactory);
|
||||
return bean;
|
||||
}
|
||||
}
|
@ -1,127 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd.
|
||||
* <https://www.chatopera.com>, Licensed under the Chunsong Public
|
||||
* License, Version 1.0 (the "License"), https://docs.cskefu.com/licenses/v1.html
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* Copyright (C) 2018- Jun. 2023 Chatopera Inc, <https://www.chatopera.com>, Licensed under the Apache License, Version 2.0,
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Copyright (C) 2017 优客服-多渠道客服系统, Licensed under the Apache License, Version 2.0,
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*/
|
||||
package com.cskefu.cc.config;
|
||||
|
||||
import com.cskefu.cc.basic.MainContext;
|
||||
import com.cskefu.cc.basic.auth.BearerTokenMgr;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.security.web.util.matcher.RequestMatcher;
|
||||
|
||||
import jakarta.servlet.*;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
|
||||
import static com.cskefu.cc.basic.Constants.AUTH_TOKEN_TYPE_BASIC;
|
||||
import static com.cskefu.cc.basic.Constants.AUTH_TOKEN_TYPE_BEARER;
|
||||
|
||||
public class ApiRequestMatchingFilter implements Filter {
|
||||
private static final Logger logger = LoggerFactory.getLogger(ApiRequestMatchingFilter.class);
|
||||
|
||||
private final RequestMatcher[] ignoredRequests;
|
||||
private static BearerTokenMgr bearerTokenMgr;
|
||||
|
||||
|
||||
public ApiRequestMatchingFilter(RequestMatcher... matcher) {
|
||||
this.ignoredRequests = matcher;
|
||||
}
|
||||
|
||||
public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException {
|
||||
HttpServletRequest request = (HttpServletRequest) req;
|
||||
HttpServletResponse response = (HttpServletResponse) resp;
|
||||
|
||||
String method = request.getMethod();
|
||||
|
||||
if (StringUtils.isNotBlank(method) && method.equalsIgnoreCase("options")) {
|
||||
response.setHeader("Access-Control-Allow-Origin", "*");
|
||||
response.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE, PUT");
|
||||
response.setHeader("Access-Control-Max-Age", "3600");
|
||||
response.setHeader("Access-Control-Allow-Headers", "x-requested-with,accept,authorization,content-type");
|
||||
response.setHeader("X-Frame-Options", "SAMEORIGIN");
|
||||
response.setStatus(HttpStatus.ACCEPTED.value());
|
||||
} else {
|
||||
boolean matchAnyRoles = false;
|
||||
for (RequestMatcher anyRequest : ignoredRequests) {
|
||||
if (anyRequest.matches(request)) {
|
||||
matchAnyRoles = true;
|
||||
}
|
||||
}
|
||||
if (matchAnyRoles) {
|
||||
String authorization = request.getHeader("authorization");
|
||||
if (StringUtils.isBlank(authorization)) {
|
||||
authorization = request.getParameter("authorization");
|
||||
}
|
||||
|
||||
if (StringUtils.isNotBlank(authorization)) {
|
||||
// set the default value for backward compatibility as bear token bare metal
|
||||
String authorizationTrimed = authorization;
|
||||
String authorizationTokenType = AUTH_TOKEN_TYPE_BEARER;
|
||||
if (authorization.startsWith(String.format("%s ", AUTH_TOKEN_TYPE_BEARER))) {
|
||||
authorizationTrimed = StringUtils.substring(authorization, 7);
|
||||
authorizationTokenType = AUTH_TOKEN_TYPE_BEARER;
|
||||
} else if (authorization.startsWith(String.format("%s ", AUTH_TOKEN_TYPE_BASIC))) {
|
||||
authorizationTrimed = StringUtils.substring(authorization, 6);
|
||||
authorizationTokenType = AUTH_TOKEN_TYPE_BASIC;
|
||||
}
|
||||
|
||||
if (StringUtils.isNotBlank(authorizationTrimed)) {
|
||||
switch (authorizationTokenType) {
|
||||
case AUTH_TOKEN_TYPE_BEARER:
|
||||
if (getBearerTokenMgr().existToken(authorizationTrimed)) {
|
||||
chain.doFilter(req, resp);
|
||||
} else {
|
||||
response.sendRedirect("/auth/error");
|
||||
}
|
||||
break;
|
||||
case AUTH_TOKEN_TYPE_BASIC:
|
||||
// TODO
|
||||
response.sendRedirect("/auth/error");
|
||||
break;
|
||||
default:
|
||||
response.sendRedirect("/auth/error");
|
||||
}
|
||||
} else {
|
||||
response.sendRedirect("/auth/error");
|
||||
}
|
||||
} else {
|
||||
response.sendRedirect("/auth/error");
|
||||
}
|
||||
} else {
|
||||
chain.doFilter(req, resp);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroy() {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(FilterConfig arg0) throws ServletException {
|
||||
|
||||
}
|
||||
|
||||
private static BearerTokenMgr getBearerTokenMgr() {
|
||||
if (bearerTokenMgr == null) {
|
||||
bearerTokenMgr = MainContext.getContext().getBean(BearerTokenMgr.class);
|
||||
}
|
||||
return bearerTokenMgr;
|
||||
}
|
||||
|
||||
}
|
@ -1,153 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd.
|
||||
* <https://www.chatopera.com>, Licensed under the Chunsong Public
|
||||
* License, Version 1.0 (the "License"), https://docs.cskefu.com/licenses/v1.html
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* Copyright (C) 2018- Jun. 2023 Chatopera Inc, <https://www.chatopera.com>, Licensed under the Apache License, Version 2.0,
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Copyright (C) 2017 优客服-多渠道客服系统, Licensed under the Apache License, Version 2.0,
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*/
|
||||
package com.cskefu.cc.config;
|
||||
|
||||
import com.cskefu.cc.basic.Constants;
|
||||
import com.cskefu.cc.basic.MainContext;
|
||||
import com.cskefu.cc.basic.MainUtils;
|
||||
import com.cskefu.cc.basic.plugins.IPluginConfigurer;
|
||||
import com.cskefu.cc.basic.plugins.PluginRegistry;
|
||||
import com.cskefu.cc.cache.Cache;
|
||||
import com.cskefu.cc.model.BlackEntity;
|
||||
import com.cskefu.cc.model.SysDic;
|
||||
import com.cskefu.cc.model.SystemConfig;
|
||||
import com.cskefu.cc.persistence.repository.*;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.context.ApplicationListener;
|
||||
import org.springframework.context.event.ContextRefreshedEvent;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
public class AppCtxRefreshEventListener implements ApplicationListener<ContextRefreshedEvent> {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(AppCtxRefreshEventListener.class);
|
||||
|
||||
|
||||
private void setupSysdicCacheAndExtras(final ContextRefreshedEvent event, final String cacheSetupStrategy, final Cache cache, final SysDicRepository sysDicRes, final BlackListRepository blackListRes) {
|
||||
if (!StringUtils.equalsIgnoreCase(cacheSetupStrategy, Constants.cache_setup_strategy_skip)) {
|
||||
|
||||
/**************************
|
||||
* 加载系统到缓存
|
||||
* 加载系统词典大约只需要5s左右
|
||||
**************************/
|
||||
|
||||
// 首先将之前缓存清空,此处使用系统的默认租户信息
|
||||
cache.eraseSysDic();
|
||||
|
||||
List<SysDic> sysDicList = sysDicRes.findAll();
|
||||
Map<String, List<SysDic>> rootDictItems = new HashMap<>(); // 关联根词典及其子项
|
||||
Map<String, SysDic> rootDics = new HashMap<>();
|
||||
Set<String> parents = new HashSet<>();
|
||||
|
||||
// 获得所有根词典
|
||||
for (final SysDic dic : sysDicList) {
|
||||
if (StringUtils.equals(dic.getParentid(), "0")) {
|
||||
parents.add(dic.getId());
|
||||
rootDics.put(dic.getId(), dic);
|
||||
}
|
||||
}
|
||||
|
||||
// 向根词典中添加子项
|
||||
for (final SysDic dic : sysDicList) {
|
||||
if ((!StringUtils.equals(dic.getParentid(), "0")) &&
|
||||
parents.contains(dic.getDicid())) {
|
||||
// 不是根词典,并且包含在一个根词典内
|
||||
if (!rootDictItems.containsKey(dic.getDicid())) {
|
||||
rootDictItems.put(dic.getDicid(), new ArrayList<>());
|
||||
}
|
||||
rootDictItems.get(dic.getDicid()).add(dic);
|
||||
}
|
||||
}
|
||||
|
||||
// 更新缓存
|
||||
// TODO 集群时注意!!!
|
||||
// 此处为长时间的操作,如果在一个集群中,会操作共享内容,非常不可靠
|
||||
// 所以,当前代码不支持集群,需要解决启动上的这个问题!
|
||||
|
||||
// 存储根词典 TODO 此处只考虑了系统默认租户
|
||||
cache.putSysDic(new ArrayList<>(rootDics.values()));
|
||||
|
||||
for (final Map.Entry<String, List<SysDic>> entry : rootDictItems.entrySet()) {
|
||||
SysDic rootDic = rootDics.get(entry.getKey());
|
||||
// 打印根词典信息
|
||||
logger.debug("[onApplicationEvent] root dict: {}, code {}, name {}, item size {}", entry.getKey(), rootDics.get(entry.getKey()).getCode(), rootDics.get(entry.getKey()).getName(), entry.getValue().size());
|
||||
// 存储子项列表
|
||||
cache.putSysDic(rootDic.getCode(), entry.getValue());
|
||||
// 存储子项成员
|
||||
cache.putSysDic(entry.getValue());
|
||||
}
|
||||
|
||||
List<BlackEntity> blackList = blackListRes.findAll();
|
||||
for (final BlackEntity black : blackList) {
|
||||
if (StringUtils.isNotBlank(black.getUserid())) {
|
||||
if (black.getEndtime() == null || black.getEndtime().after(new Date())) {
|
||||
cache.putSystemById(black.getUserid(), black);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载系统全局配置
|
||||
*/
|
||||
SystemConfigRepository systemConfigRes = event.getApplicationContext().getBean(SystemConfigRepository.class);
|
||||
List<SystemConfig> configs = systemConfigRes.findAll();
|
||||
SystemConfig config = configs.size() > 0 ? configs.get(0) : null;
|
||||
if (config != null) {
|
||||
cache.putSystemById("systemConfig", config);
|
||||
}
|
||||
logger.warn("[StartedEventListener] setup Sysdicts in Redis done, strategy {}", cacheSetupStrategy);
|
||||
} else {
|
||||
logger.warn("[onApplicationEvent] skip initialize sysdicts.");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onApplicationEvent(ContextRefreshedEvent event) {
|
||||
if (MainContext.getContext() == null) {
|
||||
logger.info("[onApplicationEvent] set main context and initialize the Cache System.");
|
||||
MainContext.setApplicationContext(event.getApplicationContext());
|
||||
SysDicRepository sysDicRes = event.getApplicationContext().getBean(SysDicRepository.class);
|
||||
BlackListRepository blackListRes = event.getApplicationContext().getBean(BlackListRepository.class);
|
||||
Cache cache = event.getApplicationContext().getBean(Cache.class);
|
||||
String cacheSetupStrategy = event.getApplicationContext().getEnvironment().getProperty("cache.setup.strategy");
|
||||
|
||||
setupSysdicCacheAndExtras(event, cacheSetupStrategy, cache, sysDicRes, blackListRes);
|
||||
|
||||
MainUtils.initSystemArea();
|
||||
|
||||
MainUtils.initSystemSecField(event.getApplicationContext().getBean(TablePropertiesRepository.class));
|
||||
// MainUtils.initAdv();//初始化广告位
|
||||
|
||||
// 初始化插件
|
||||
PluginRegistry pluginRegistry = MainContext.getContext().getBean(PluginRegistry.class);
|
||||
for (final IPluginConfigurer p : pluginRegistry.getPlugins()) {
|
||||
logger.info("[Plugins] registered plugin id {}, class {}", p.getPluginId(), p.getClass().getName());
|
||||
}
|
||||
|
||||
} else {
|
||||
logger.info("[onApplicationEvent] bypass, initialization has been done already.");
|
||||
}
|
||||
|
||||
// Fix SQL init lazy load delay
|
||||
if (MainContext.getContext() != null) {
|
||||
UserRepository userRes = MainContext.getContext().getBean(UserRepository.class);
|
||||
userRes.findByUsername("admin").ifPresent((p) -> {
|
||||
logger.warn("[onApplicationEvent] inited JPA sql.");
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
@ -1,48 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd.
|
||||
* <https://www.chatopera.com>, Licensed under the Chunsong Public
|
||||
* License, Version 1.0 (the "License"), https://docs.cskefu.com/licenses/v1.html
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* Copyright (C) 2018- Jun. 2023 Chatopera Inc, <https://www.chatopera.com>, Licensed under the Apache License, Version 2.0,
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Copyright (C) 2017 优客服-多渠道客服系统, Licensed under the Apache License, Version 2.0,
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*/
|
||||
package com.cskefu.cc.config;
|
||||
|
||||
import com.cskefu.cc.proxy.UserProxy;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.context.event.ApplicationReadyEvent;
|
||||
import org.springframework.context.ApplicationListener;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class ApplicationStartupListener implements ApplicationListener<ApplicationReadyEvent> {
|
||||
|
||||
final private static Logger logger = LoggerFactory.getLogger(ApplicationStartupListener.class);
|
||||
|
||||
@Value("${extras.auth.super-admin.pass}")
|
||||
private String superAdminPass;
|
||||
|
||||
@Autowired
|
||||
private UserProxy userProxy;
|
||||
|
||||
@Override
|
||||
public void onApplicationEvent(final ApplicationReadyEvent event) {
|
||||
if (StringUtils.isNotBlank(superAdminPass)) {
|
||||
logger.warn("Reset Superadmin Password by ENV variable EXTRAS_AUTH_SUPER_ADMIN_PASS=********");
|
||||
if (!userProxy.resetAccountPasswordByUsername("admin", superAdminPass)) {
|
||||
logger.error("Reset Superadmin Password failure. Check 1) admin user do exist in DB with username admin.");
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
@ -1,37 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd.
|
||||
* <https://www.chatopera.com>, Licensed under the Chunsong Public
|
||||
* License, Version 1.0 (the "License"), https://docs.cskefu.com/licenses/v1.html
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* Copyright (C) 2018- Jun. 2023 Chatopera Inc, <https://www.chatopera.com>, Licensed under the Apache License, Version 2.0,
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Copyright (C) 2017 优客服-多渠道客服系统, Licensed under the Apache License, Version 2.0,
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*/
|
||||
package com.cskefu.cc.config;
|
||||
|
||||
import com.lmax.disruptor.ExceptionHandler;
|
||||
|
||||
public class CSKeFuExceptionHandler implements ExceptionHandler<Object>{
|
||||
|
||||
@Override
|
||||
public void handleEventException(Throwable ex, long arg1, Object arg2) {
|
||||
ex.printStackTrace();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleOnShutdownException(Throwable ex) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleOnStartException(Throwable ex) {
|
||||
// TODO Auto-generated method stub
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -1,70 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd.
|
||||
* <https://www.chatopera.com>, Licensed under the Chunsong Public
|
||||
* License, Version 1.0 (the "License"), https://docs.cskefu.com/licenses/v1.html
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* Copyright (C) 2018- Jun. 2023 Chatopera Inc, <https://www.chatopera.com>, Licensed under the Apache License, Version 2.0,
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Copyright (C) 2017 优客服-多渠道客服系统, Licensed under the Apache License, Version 2.0,
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*/
|
||||
package com.cskefu.cc.config;
|
||||
|
||||
import com.cskefu.cc.interceptor.*;
|
||||
import com.cskefu.cc.util.SystemEnvHelper;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.servlet.config.annotation.*;
|
||||
|
||||
@Configuration
|
||||
public class CSKeFuWebAppConfigurer implements WebMvcConfigurer {
|
||||
private final static Logger logger = LoggerFactory.getLogger(CSKeFuWebAppConfigurer.class);
|
||||
private final static String ENABLE_LOG_REQUEST = SystemEnvHelper.parseFromApplicationProps("extras.log.request");
|
||||
|
||||
/**
|
||||
* https://www.baeldung.com/spring-cors
|
||||
*
|
||||
* @param registry
|
||||
*/
|
||||
@Override
|
||||
public void addCorsMappings(CorsRegistry registry) {
|
||||
// enables CORS requests from any origin to any endpoint in the application.
|
||||
registry.addMapping("/**").allowedOrigins("*");
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("deprecation")
|
||||
public void configurePathMatch(PathMatchConfigurer configurer) {
|
||||
configurer.setUseSuffixPatternMatch(Boolean.TRUE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addResourceHandlers(ResourceHandlerRegistry registry) {
|
||||
registry.addResourceHandler("/**").addResourceLocations("classpath:/static/");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addInterceptors(InterceptorRegistry registry) {
|
||||
// 多个拦截器组成一个拦截器链
|
||||
// addPathPatterns 用于添加拦截规则
|
||||
// excludePathPatterns 用户排除拦截
|
||||
registry.addInterceptor(new UserExperiencePlanInterceptorHandler()).addPathPatterns("/**").excludePathPatterns("/im/**", "/res/image*", "/res/file*", "/cs/**", "/messenger/webhook/*");
|
||||
registry.addInterceptor(new UserInterceptorHandler()).addPathPatterns("/**").excludePathPatterns("/login.html", "/im/**", "/res/image*", "/res/file*", "/cs/**", "/messenger/webhook/*");
|
||||
registry.addInterceptor(new CrossInterceptorHandler()).addPathPatterns("/**");
|
||||
|
||||
if (StringUtils.equalsIgnoreCase(ENABLE_LOG_REQUEST, "on")) {
|
||||
logger.warn("Logging request into DB as in ENV: ENABLE_LOG_REQUEST=on");
|
||||
registry.addInterceptor(new RequestLogIntercreptorHandler()).addPathPatterns("/**");
|
||||
} else {
|
||||
logger.info("Disable Logging request into DB.");
|
||||
}
|
||||
|
||||
registry.addInterceptor(new ViewsInterceptorHandler()).addPathPatterns("/**");
|
||||
}
|
||||
}
|
@ -1,69 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd.
|
||||
* <https://www.chatopera.com>, Licensed under the Chunsong Public
|
||||
* License, Version 1.0 (the "License"), https://docs.cskefu.com/licenses/v1.html
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* Copyright (C) 2018- Jun. 2023 Chatopera Inc, <https://www.chatopera.com>, Licensed under the Apache License, Version 2.0,
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Copyright (C) 2017 优客服-多渠道客服系统, Licensed under the Apache License, Version 2.0,
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*/
|
||||
package com.cskefu.cc.config;
|
||||
|
||||
import com.cskefu.cc.basic.Constants;
|
||||
import com.cskefu.cc.model.User;
|
||||
import org.apache.catalina.connector.ClientAbortException;
|
||||
import org.springframework.security.web.util.matcher.RequestMatcher;
|
||||
|
||||
import jakarta.servlet.*;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
|
||||
public class DelegateRequestMatchingFilter implements Filter {
|
||||
private final RequestMatcher[] ignoredRequests;
|
||||
|
||||
public DelegateRequestMatchingFilter(RequestMatcher... matcher) {
|
||||
this.ignoredRequests = matcher;
|
||||
}
|
||||
|
||||
public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException {
|
||||
HttpServletRequest request = (HttpServletRequest) req;
|
||||
boolean matchAnyRoles = false;
|
||||
for (RequestMatcher anyRequest : ignoredRequests) {
|
||||
if (anyRequest.matches(request)) {
|
||||
matchAnyRoles = true;
|
||||
}
|
||||
}
|
||||
User user = (User) request.getSession().getAttribute(Constants.USER_SESSION_NAME);
|
||||
if (matchAnyRoles) {
|
||||
if (user != null && (user.isAdmin())) {
|
||||
chain.doFilter(req, resp);
|
||||
} else {
|
||||
// 重定向到 无权限执行操作的页面
|
||||
HttpServletResponse response = (HttpServletResponse) resp;
|
||||
response.sendRedirect("/?msg=security");
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
chain.doFilter(req, resp);
|
||||
} catch (ClientAbortException ex) {
|
||||
//Tomcat异常,不做处理
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroy() {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(FilterConfig arg0) throws ServletException {
|
||||
|
||||
}
|
||||
}
|
@ -1,62 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd.
|
||||
* <https://www.chatopera.com>, Licensed under the Chunsong Public
|
||||
* License, Version 1.0 (the "License"), https://docs.cskefu.com/licenses/v1.html
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* Copyright (C) 2018- Jun. 2023 Chatopera Inc, <https://www.chatopera.com>, Licensed under the Apache License, Version 2.0,
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Copyright (C) 2017 优客服-多渠道客服系统, Licensed under the Apache License, Version 2.0,
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*/
|
||||
package com.cskefu.cc.config;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
|
||||
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
|
||||
|
||||
/**
|
||||
* 线程池 , 作业调度平台
|
||||
* @author iceworld
|
||||
*
|
||||
*/
|
||||
@Configuration
|
||||
public class ExecutorConfig {
|
||||
private static final int CORE_POOL_SIZE = 7;
|
||||
private static final int MAX_POOL_SIZE = 100;
|
||||
|
||||
/**
|
||||
* 作业平台使用的线程池
|
||||
* @return
|
||||
*/
|
||||
@Bean(name = "webimTaskExecutor")
|
||||
public ThreadPoolTaskExecutor common() {
|
||||
|
||||
ThreadPoolTaskExecutor poolTaskExecutor = new ThreadPoolTaskExecutor();
|
||||
// 线程池维护线程的最少数量
|
||||
poolTaskExecutor.setCorePoolSize(CORE_POOL_SIZE);
|
||||
// 线程池维护线程的最大数量
|
||||
poolTaskExecutor.setMaxPoolSize(MAX_POOL_SIZE);
|
||||
// 线程池所使用的缓冲队列
|
||||
poolTaskExecutor.setQueueCapacity(200);
|
||||
// 线程池维护线程所允许的空闲时间
|
||||
poolTaskExecutor.setKeepAliveSeconds(30000);
|
||||
poolTaskExecutor.setWaitForTasksToCompleteOnShutdown(true);
|
||||
poolTaskExecutor.setThreadNamePrefix("cs-webim-task-");
|
||||
|
||||
return poolTaskExecutor;
|
||||
}
|
||||
|
||||
@Bean(name = "scheduleTaskExecutor")
|
||||
public ThreadPoolTaskScheduler schedule(){
|
||||
ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler();
|
||||
taskScheduler.setPoolSize(CORE_POOL_SIZE);
|
||||
taskScheduler.setWaitForTasksToCompleteOnShutdown(true);
|
||||
taskScheduler.setThreadNamePrefix("cs-schedule-");
|
||||
return taskScheduler;
|
||||
}
|
||||
}
|
@ -1,109 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd.
|
||||
* <https://www.chatopera.com>, Licensed under the Chunsong Public
|
||||
* License, Version 1.0 (the "License"), https://docs.cskefu.com/licenses/v1.html
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* Copyright (C) 2018- Jun. 2023 Chatopera Inc, <https://www.chatopera.com>, Licensed under the Apache License, Version 2.0,
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Copyright (C) 2017 优客服-多渠道客服系统, Licensed under the Apache License, Version 2.0,
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*/
|
||||
package com.cskefu.cc.config;
|
||||
|
||||
import com.corundumstudio.socketio.Configuration;
|
||||
import com.corundumstudio.socketio.SocketIOServer;
|
||||
import com.corundumstudio.socketio.annotation.SpringAnnotationScanner;
|
||||
import com.cskefu.cc.exception.InstantMessagingExceptionListener;
|
||||
import jakarta.annotation.PreDestroy;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.autoconfigure.web.ServerProperties;
|
||||
import org.springframework.boot.web.server.Ssl;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
|
||||
import java.io.InputStream;
|
||||
|
||||
@org.springframework.context.annotation.Configuration
|
||||
public class MessagingServerConfigure {
|
||||
@Value("${uk.im.server.host}")
|
||||
private String host;
|
||||
|
||||
@Value("${uk.im.server.port}")
|
||||
private Integer port;
|
||||
|
||||
@Value("${cs.im.server.ssl.port}")
|
||||
private Integer sslPort;
|
||||
|
||||
@Value("${uk.im.server.threads}")
|
||||
private String threads;
|
||||
|
||||
private SocketIOServer server;
|
||||
|
||||
@Bean(name = "webimport")
|
||||
public Integer getWebIMPort() {
|
||||
if (sslPort != null) {
|
||||
return sslPort;
|
||||
} else {
|
||||
return port;
|
||||
}
|
||||
}
|
||||
|
||||
@Autowired
|
||||
private ServerProperties serverProperties;
|
||||
|
||||
@Bean
|
||||
public SocketIOServer socketIOServer() {
|
||||
Configuration config = new Configuration();
|
||||
//解决对此重启服务时,netty端口被占用问题
|
||||
com.corundumstudio.socketio.SocketConfig tmpConfig = new com.corundumstudio.socketio.SocketConfig();
|
||||
tmpConfig.setReuseAddress(true);
|
||||
config.setSocketConfig(tmpConfig);
|
||||
|
||||
// config.setHostname(host);
|
||||
config.setPort(port);
|
||||
|
||||
// config.getSocketConfig().setReuseAddress(true);
|
||||
// config.setSocketConfig(new SocketConfig());
|
||||
// config.setOrigin("*");
|
||||
config.setExceptionListener(new InstantMessagingExceptionListener());
|
||||
|
||||
// config.setSSLProtocol("https");
|
||||
int workThreads = StringUtils.isNotBlank(threads) && threads.matches("[\\d]{1,6}") ? Integer.parseInt(threads) : 100;
|
||||
config.setWorkerThreads(workThreads);
|
||||
// config.setStoreFactory(new HazelcastStoreFactory());
|
||||
config.setAuthorizationListener(data -> true);
|
||||
config.getSocketConfig().setReuseAddress(true);
|
||||
config.getSocketConfig().setSoLinger(0);
|
||||
config.getSocketConfig().setTcpNoDelay(true);
|
||||
config.getSocketConfig().setTcpKeepAlive(true);
|
||||
|
||||
// ServerProperties serverProperties = applicationContext.getBean(ServerProperties.class);
|
||||
|
||||
Ssl ssl = serverProperties.getSsl();
|
||||
if (ssl != null) {
|
||||
String keyStore = ssl.getKeyStore();
|
||||
String keyStorePassword = ssl.getKeyStorePassword();
|
||||
if (StringUtils.isNotEmpty(keyStore) && StringUtils.isNotEmpty(keyStorePassword)) {
|
||||
InputStream keyStoreStream = this.getClass().getResourceAsStream("/" + keyStore.trim().split(":")[1]);
|
||||
config.setKeyStore(keyStoreStream);
|
||||
config.setKeyStorePassword(keyStorePassword);
|
||||
}
|
||||
}
|
||||
return server = new SocketIOServer(config);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public SpringAnnotationScanner springAnnotationScanner(SocketIOServer socketServer) {
|
||||
return new SpringAnnotationScanner(socketServer);
|
||||
}
|
||||
|
||||
@PreDestroy
|
||||
public void destory() {
|
||||
server.stop();
|
||||
}
|
||||
}
|
@ -1,56 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd.
|
||||
* <https://www.chatopera.com>, Licensed under the Chunsong Public
|
||||
* License, Version 1.0 (the "License"), https://docs.cskefu.com/licenses/v1.html
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* Copyright (C) 2019-2022 Chatopera Inc, <https://www.chatopera.com>,
|
||||
* Licensed under the Apache License, Version 2.0,
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*/
|
||||
package com.cskefu.cc.config;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import de.neuland.pug4j.PugConfiguration;
|
||||
import de.neuland.pug4j.spring.template.SpringTemplateLoader;
|
||||
import de.neuland.pug4j.spring.view.PugViewResolver;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.servlet.ViewResolver;
|
||||
|
||||
@Configuration
|
||||
public class PugConfig {
|
||||
@Value("${spring.pug4j.cache}")
|
||||
private Boolean pug4jCache;
|
||||
|
||||
@Value("${spring.pug4j.template-loader-path}")
|
||||
private String templatePath;
|
||||
@Bean
|
||||
public SpringTemplateLoader templateLoader() {
|
||||
SpringTemplateLoader templateLoader = new SpringTemplateLoader();
|
||||
templateLoader.setTemplateLoaderPath(templatePath);
|
||||
templateLoader.setEncoding("UTF-8");
|
||||
templateLoader.setSuffix(".pug");
|
||||
return templateLoader;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public PugConfiguration pugConfiguration() {
|
||||
PugConfiguration configuration = new PugConfiguration();
|
||||
configuration.setCaching(pug4jCache);
|
||||
configuration.setTemplateLoader(templateLoader());
|
||||
return configuration;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ViewResolver viewResolver() {
|
||||
PugViewResolver viewResolver = new PugCskefuViewResolver();
|
||||
viewResolver.setConfiguration(pugConfiguration());
|
||||
viewResolver.setOrder(0);
|
||||
viewResolver.setSuffix(".pug");
|
||||
return viewResolver;
|
||||
}
|
||||
}
|
@ -1,27 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd.
|
||||
* <https://www.chatopera.com>, Licensed under the Chunsong Public
|
||||
* License, Version 1.0 (the "License"), https://docs.cskefu.com/licenses/v1.html
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.cskefu.cc.config;
|
||||
|
||||
import de.neuland.pug4j.spring.view.PugView;
|
||||
import de.neuland.pug4j.spring.view.PugViewResolver;
|
||||
import org.springframework.web.servlet.view.AbstractUrlBasedView;
|
||||
|
||||
public class PugCskefuViewResolver extends PugViewResolver {
|
||||
@Override
|
||||
protected AbstractUrlBasedView buildView(String viewName) throws Exception {
|
||||
AbstractUrlBasedView view = super.buildView(viewName);
|
||||
if (viewName.startsWith("/resource/css")) {
|
||||
PugView pugView = (PugView) view;
|
||||
pugView.setContentType("text/css; charset=UTF-8");
|
||||
}
|
||||
return view;
|
||||
}
|
||||
}
|
@ -1,33 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd.
|
||||
* <https://www.chatopera.com>, Licensed under the Chunsong Public
|
||||
* License, Version 1.0 (the "License"), https://docs.cskefu.com/licenses/v1.html
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* Copyright (C) 2019-2022 Chatopera Inc, <https://www.chatopera.com>,
|
||||
* Licensed under the Apache License, Version 2.0,
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*/
|
||||
package com.cskefu.cc.config;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.data.redis.connection.RedisConnectionFactory;
|
||||
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
|
||||
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
@Configuration
|
||||
public class RedisConfigure {
|
||||
|
||||
@Bean
|
||||
RedisMessageListenerContainer redisContainer(RedisConnectionFactory redisConnectionFactory) {
|
||||
final RedisMessageListenerContainer container = new RedisMessageListenerContainer();
|
||||
container.setConnectionFactory(redisConnectionFactory);
|
||||
container.setTaskExecutor(Executors.newFixedThreadPool(100));
|
||||
return container;
|
||||
}
|
||||
}
|
@ -1,26 +0,0 @@
|
||||
/**
|
||||
* Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd.
|
||||
* <https://www.chatopera.com>, Licensed under the Chunsong Public
|
||||
* License, Version 1.0 (the "License"), https://docs.cskefu.com/licenses/v1.html
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* Copyright Jun. 2023 Chatopera Inc. <https://www.chatopera.com>. All rights reserved.
|
||||
*/
|
||||
package com.cskefu.cc.config;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import com.cskefu.cc.basic.TerminateBean;
|
||||
|
||||
@Configuration
|
||||
public class ShutdownConfig {
|
||||
|
||||
@Bean
|
||||
public TerminateBean getTerminateBean() {
|
||||
return new TerminateBean();
|
||||
}
|
||||
}
|
@ -1,56 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd.
|
||||
* <https://www.chatopera.com>, Licensed under the Chunsong Public
|
||||
* License, Version 1.0 (the "License"), https://docs.cskefu.com/licenses/v1.html
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* Copyright (C) 2018- Jun. 2023 Chatopera Inc, <https://www.chatopera.com>, Licensed under the Apache License, Version 2.0,
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Copyright (C) 2017 优客服-多渠道客服系统, Licensed under the Apache License, Version 2.0,
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*/
|
||||
package com.cskefu.cc.config;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.core.convert.converter.Converter;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
|
||||
public class StringToDateConverter implements Converter<String, Date> {
|
||||
private static final String dateFormat = "yyyy-MM-dd HH:mm:ss";
|
||||
private static final String shortDateFormat = "yyyy-MM-dd";
|
||||
|
||||
/**
|
||||
* @see org.springframework.core.convert.converter.Converter#convert(java.lang.Object)
|
||||
*/
|
||||
@Override
|
||||
public Date convert(String source) {
|
||||
if (StringUtils.isBlank(source)) {
|
||||
return null;
|
||||
}
|
||||
source = source.trim();
|
||||
try {
|
||||
if (source.contains("-")) {
|
||||
SimpleDateFormat formatter;
|
||||
if (source.contains(":")) {
|
||||
formatter = new SimpleDateFormat(dateFormat);
|
||||
} else {
|
||||
formatter = new SimpleDateFormat(shortDateFormat);
|
||||
}
|
||||
Date dtDate = formatter.parse(source);
|
||||
return dtDate;
|
||||
} else if (source.matches("^\\d+$")) {
|
||||
Long lDate = new Long(source);
|
||||
return new Date(lDate);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(String.format("parser %s to Date callOutFail", source));
|
||||
}
|
||||
throw new RuntimeException(String.format("parser %s to Date callOutFail", source));
|
||||
}
|
||||
|
||||
}
|
@ -1,53 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd.
|
||||
* <https://www.chatopera.com>, Licensed under the Chunsong Public
|
||||
* License, Version 1.0 (the "License"), https://docs.cskefu.com/licenses/v1.html
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* Copyright (C) 2018- Jun. 2023 Chatopera Inc, <https://www.chatopera.com>, Licensed under the Apache License, Version 2.0,
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Copyright (C) 2017 优客服-多渠道客服系统, Licensed under the Apache License, Version 2.0,
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*/
|
||||
package com.cskefu.cc.config;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.convert.support.GenericConversionService;
|
||||
import org.springframework.web.bind.support.ConfigurableWebBindingInitializer;
|
||||
import org.springframework.web.multipart.support.StandardServletMultipartResolver;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter;
|
||||
|
||||
import jakarta.annotation.PostConstruct;
|
||||
|
||||
@Configuration
|
||||
public class WebConfigBeans {
|
||||
@Autowired
|
||||
private RequestMappingHandlerAdapter handlerAdapter;
|
||||
|
||||
@Bean
|
||||
public StandardServletMultipartResolver multipartResolver() {
|
||||
return new StandardServletMultipartResolver();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 增加字符串转日期的功能
|
||||
*/
|
||||
@PostConstruct
|
||||
public void initEditableValidation() {
|
||||
|
||||
ConfigurableWebBindingInitializer initializer = (ConfigurableWebBindingInitializer) handlerAdapter
|
||||
.getWebBindingInitializer();
|
||||
if (initializer.getConversionService() != null) {
|
||||
GenericConversionService genericConversionService = (GenericConversionService) initializer
|
||||
.getConversionService();
|
||||
genericConversionService.addConverter(new StringToDateConverter());
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -1,106 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd.
|
||||
* <https://www.chatopera.com>, Licensed under the Chunsong Public
|
||||
* License, Version 1.0 (the "License"), https://docs.cskefu.com/licenses/v1.html
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* Copyright (C) 2018- Jun. 2023 Chatopera Inc, <https://www.chatopera.com>, Licensed under the Apache License, Version 2.0,
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Copyright (C) 2017 优客服-多渠道客服系统, Licensed under the Apache License, Version 2.0,
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*/
|
||||
package com.cskefu.cc.config;
|
||||
|
||||
import jakarta.servlet.Filter;
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.Cookie;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.config.Customizer;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.config.annotation.web.configurers.CsrfConfigurer;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
|
||||
import org.springframework.security.web.csrf.CsrfToken;
|
||||
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.RequestMatcher;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
import org.springframework.web.util.WebUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
public class WebSecurityConfig {
|
||||
|
||||
@Bean
|
||||
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||
http
|
||||
.addFilterAfter(tokenInfoTokenFilterSecurityInterceptor(), BasicAuthenticationFilter.class)
|
||||
.authorizeHttpRequests(authorize -> authorize.requestMatchers("/**").permitAll())
|
||||
.csrf().disable().headers().frameOptions().sameOrigin()
|
||||
// .addFilterAfter(csrfHeaderFilter(), BasicAuthenticationFilter.class) // TODO lecjy
|
||||
.and().addFilterAfter(apiTokenFilterSecurityInterceptor(), BasicAuthenticationFilter.class);
|
||||
http.headers().contentTypeOptions().disable();
|
||||
return http.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public Filter tokenInfoTokenFilterSecurityInterceptor() throws Exception {
|
||||
RequestMatcher autconfig = new AntPathRequestMatcher("/autoconfig/**");
|
||||
RequestMatcher configprops = new AntPathRequestMatcher("/configprops/**");
|
||||
RequestMatcher beans = new AntPathRequestMatcher("/beans/**");
|
||||
RequestMatcher dump = new AntPathRequestMatcher("/dump/**");
|
||||
RequestMatcher env = new AntPathRequestMatcher("/env/**");
|
||||
RequestMatcher info = new AntPathRequestMatcher("/info/**");
|
||||
RequestMatcher mappings = new AntPathRequestMatcher("/mappings/**");
|
||||
RequestMatcher trace = new AntPathRequestMatcher("/trace/**");
|
||||
RequestMatcher druid = new AntPathRequestMatcher("/druid/**");
|
||||
|
||||
/**
|
||||
* Bypass actuator api
|
||||
*/
|
||||
// RequestMatcher health = new AntPathRequestMatcher("/health/**");
|
||||
// RequestMatcher metrics = new AntPathRequestMatcher("/metrics/**");
|
||||
// return new DelegateRequestMatchingFilter(autconfig , configprops , beans , dump , env , health , info , mappings , metrics , trace, druid);
|
||||
return new DelegateRequestMatchingFilter(autconfig, configprops, beans, dump, env, mappings, trace, druid);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public Filter apiTokenFilterSecurityInterceptor() throws Exception {
|
||||
return new ApiRequestMatchingFilter(new AntPathRequestMatcher("/api/**"));
|
||||
}
|
||||
|
||||
private Filter csrfHeaderFilter() {
|
||||
return new OncePerRequestFilter() {
|
||||
|
||||
@Override
|
||||
protected void doFilterInternal(HttpServletRequest request,
|
||||
HttpServletResponse response,
|
||||
FilterChain filterChain) throws ServletException, IOException {
|
||||
|
||||
CsrfToken csrf = (CsrfToken) request.getAttribute(CsrfToken.class.getName());
|
||||
if (csrf != null) {
|
||||
Cookie cookie = WebUtils.getCookie(request, "XSRF-TOKEN");
|
||||
String token = csrf.getToken();
|
||||
if (cookie == null || token != null
|
||||
&& !token.equals(cookie.getValue())) {
|
||||
|
||||
// Token is being added to the XSRF-TOKEN cookie.
|
||||
cookie = new Cookie("XSRF-TOKEN", token);
|
||||
cookie.setPath("/");
|
||||
response.addCookie(cookie);
|
||||
}
|
||||
}
|
||||
filterChain.doFilter(request, response);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
@ -1,73 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd.
|
||||
* <https://www.chatopera.com>, Licensed under the Chunsong Public
|
||||
* License, Version 1.0 (the "License"), https://docs.cskefu.com/licenses/v1.html
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* Copyright (C) 2018- Jun. 2023 Chatopera Inc, <https://www.chatopera.com>, Licensed under the Apache License, Version 2.0,
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Copyright (C) 2017 优客服-多渠道客服系统, Licensed under the Apache License, Version 2.0,
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*/
|
||||
package com.cskefu.cc.config;
|
||||
|
||||
import org.apache.catalina.connector.Connector;
|
||||
import org.apache.coyote.ProtocolHandler;
|
||||
import org.apache.coyote.http11.Http11NioProtocol;
|
||||
import org.apache.tomcat.util.http.Rfc6265CookieProcessor;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.web.embedded.tomcat.TomcatConnectorCustomizer;
|
||||
import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
|
||||
@Configuration
|
||||
public class WebServerContainerConfigure {
|
||||
|
||||
@Value("${server.threads.max}")
|
||||
private Integer maxthread;
|
||||
|
||||
@Value("${server.connection.max}")
|
||||
private Integer maxconnections;
|
||||
|
||||
@Value("${web.upload-path}")
|
||||
private String path;
|
||||
|
||||
@Bean
|
||||
public TomcatServletWebServerFactory createEmbeddedServletContainerFactory() throws IOException, NoSuchAlgorithmException {
|
||||
TomcatServletWebServerFactory tomcatFactory = new TomcatServletWebServerFactory();
|
||||
tomcatFactory.addConnectorCustomizers(new CSKeFuTomcatConnectorCustomizer(maxthread, maxconnections));
|
||||
// Enable cookie value with space
|
||||
// https://stackoverflow.com/questions/38687210/error-with-cookie-value-when-adding-a-new-spring-session
|
||||
// TODO lecjy
|
||||
tomcatFactory.addContextCustomizers(context -> context.setCookieProcessor(new Rfc6265CookieProcessor()));
|
||||
return tomcatFactory;
|
||||
}
|
||||
|
||||
class CSKeFuTomcatConnectorCustomizer implements TomcatConnectorCustomizer {
|
||||
private final Integer maxthread;
|
||||
private final Integer maxconnection;
|
||||
|
||||
CSKeFuTomcatConnectorCustomizer(Integer maxthread, Integer maxconnection) {
|
||||
this.maxthread = maxthread;
|
||||
this.maxconnection = maxconnection;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void customize(Connector connector) {
|
||||
Http11NioProtocol protocol = (Http11NioProtocol) connector.getProtocolHandler();
|
||||
//设置最大连接数
|
||||
protocol.setMaxConnections(maxthread != null ? maxthread : 2000);
|
||||
//设置最大线程数
|
||||
protocol.setMaxThreads(maxconnection != null ? maxconnection : 2000);
|
||||
protocol.setConnectionTimeout(30000);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,121 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd.
|
||||
* <https://www.chatopera.com>, Licensed under the Chunsong Public
|
||||
* License, Version 1.0 (the "License"), https://docs.cskefu.com/licenses/v1.html
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* Copyright (C) 2019-2022 Chatopera Inc, <https://www.chatopera.com>,
|
||||
* Licensed under the Apache License, Version 2.0,
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*/
|
||||
package com.cskefu.cc.config;
|
||||
|
||||
import com.cskefu.cc.basic.auth.AuthRedisTemplate;
|
||||
import com.cskefu.cc.cache.RedisKey;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Primary;
|
||||
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.data.redis.serializer.StringRedisSerializer;
|
||||
import org.springframework.session.FlushMode;
|
||||
import org.springframework.session.data.redis.RedisSessionRepository;
|
||||
import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
|
||||
/**
|
||||
* maxInactiveIntervalInSeconds: 设置 Session 失效时间,
|
||||
* 使用 Redis Session 之后,原 Spring Boot 的 server.session.timeout 属性不再生效。
|
||||
* http://www.ityouknow.com/springboot/2016/03/06/spring-boot-redis.html
|
||||
* 86400 代表一天
|
||||
* maxInactiveIntervalInSeconds = 86400 * 30
|
||||
*/
|
||||
|
||||
@Configuration
|
||||
public class WebServerSessionConfigure {
|
||||
|
||||
/**
|
||||
* spring在多长时间后强制使redis中的session失效,默认是1800.(单位/秒)
|
||||
*/
|
||||
@Value("${server.session-timeout}")
|
||||
private long maxInactiveIntervalInSeconds;
|
||||
|
||||
@Value("${spring.data.redis.host}")
|
||||
private String host;
|
||||
|
||||
@Value("${spring.data.redis.port}")
|
||||
private int port;
|
||||
|
||||
@Value("${spring.data.redis.password}")
|
||||
private String pass;
|
||||
|
||||
@Value("${spring.redis.session.db}")
|
||||
private int sessionDb;
|
||||
|
||||
@Value("${spring.redis.token.db}")
|
||||
private int tokenDb;
|
||||
|
||||
@Value("${spring.data.redis.timeout}")
|
||||
private int timeout;
|
||||
|
||||
@Primary
|
||||
@Bean
|
||||
// TODO lecjy
|
||||
public RedisSessionRepository sessionRepository(RedisTemplate<String, Object> sessionRedisTemplate) {
|
||||
RedisSessionRepository sessionRepository = new RedisSessionRepository(sessionRedisTemplate);
|
||||
sessionRepository.setDefaultMaxInactiveInterval(Duration.ofSeconds(maxInactiveIntervalInSeconds));
|
||||
sessionRepository.setFlushMode(FlushMode.IMMEDIATE);
|
||||
sessionRepository.setRedisKeyNamespace(RedisKey.CACHE_SESSIONS);
|
||||
return sessionRepository;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public RedisTemplate<String, Object> sessionRedisTemplate() {
|
||||
JedisConnectionFactory factory = new JedisConnectionFactory();
|
||||
factory.setHostName(host);
|
||||
factory.setPort(port);
|
||||
factory.setDatabase(sessionDb);
|
||||
if (StringUtils.isNotBlank(pass)) {
|
||||
factory.setPassword(pass);
|
||||
}
|
||||
factory.setTimeout(timeout);
|
||||
factory.afterPropertiesSet();
|
||||
RedisTemplate<String, Object> template = new RedisTemplate<>();
|
||||
template.setKeySerializer(new StringRedisSerializer());
|
||||
template.setHashKeySerializer(new StringRedisSerializer());
|
||||
template.setConnectionFactory(factory);
|
||||
return template;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 存储AuthToken
|
||||
* @return
|
||||
*/
|
||||
@Bean
|
||||
public AuthRedisTemplate authRedisTemplate() {
|
||||
JedisConnectionFactory factory = new JedisConnectionFactory();
|
||||
factory.setHostName(host);
|
||||
factory.setPort(port);
|
||||
factory.setDatabase(tokenDb);
|
||||
if (StringUtils.isNotBlank(pass)) {
|
||||
factory.setPassword(pass);
|
||||
}
|
||||
factory.setTimeout(timeout);
|
||||
factory.afterPropertiesSet();
|
||||
|
||||
AuthRedisTemplate template = new AuthRedisTemplate();
|
||||
template.setKeySerializer(new StringRedisSerializer());
|
||||
template.setHashKeySerializer(new StringRedisSerializer());
|
||||
template.setConnectionFactory(factory);
|
||||
return template;
|
||||
}
|
||||
|
||||
}
|
@ -1,164 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd.
|
||||
* <https://www.chatopera.com>, Licensed under the Chunsong Public
|
||||
* License, Version 1.0 (the "License"), https://docs.cskefu.com/licenses/v1.html
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* Copyright (C) 2018- Jun. 2023 Chatopera Inc, <https://www.chatopera.com>, Licensed under the Apache License, Version 2.0,
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Copyright (C) 2017 优客服-多渠道客服系统, Licensed under the Apache License, Version 2.0,
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*/
|
||||
package com.cskefu.cc.controller;
|
||||
|
||||
import com.cskefu.cc.acd.ACDWorkMonitor;
|
||||
import com.cskefu.cc.basic.Constants;
|
||||
import com.cskefu.cc.basic.MainContext;
|
||||
import com.cskefu.cc.basic.MainUtils;
|
||||
import com.cskefu.cc.cache.Cache;
|
||||
import com.cskefu.cc.model.Organ;
|
||||
import com.cskefu.cc.model.PbxHost;
|
||||
import com.cskefu.cc.model.User;
|
||||
import com.cskefu.cc.persistence.repository.ExtensionRepository;
|
||||
import com.cskefu.cc.persistence.repository.OrganRepository;
|
||||
import com.cskefu.cc.persistence.repository.PbxHostRepository;
|
||||
import com.cskefu.cc.proxy.OrganProxy;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.ResponseBody;
|
||||
import org.springframework.web.servlet.ModelAndView;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.validation.Valid;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.TimeZone;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Controller
|
||||
public class ApplicationController extends Handler {
|
||||
private final static Logger logger = LoggerFactory.getLogger(ApplicationController.class);
|
||||
|
||||
@Autowired
|
||||
private ACDWorkMonitor acdWorkMonitor;
|
||||
|
||||
@Value("${cskefu.build.version}")
|
||||
private String appVersionNumber;
|
||||
|
||||
@Value("${git.commit.id.abbrev}")
|
||||
private String appVersionAbbrev;
|
||||
|
||||
@Value("${application.build.datestr}")
|
||||
private String appBuildDate;
|
||||
|
||||
@Value("${application.customer.entity}")
|
||||
private String appCustomerEntity;
|
||||
|
||||
@Autowired
|
||||
private Cache cache;
|
||||
|
||||
@Value("${tongji.baidu.sitekey}")
|
||||
private String tongjiBaiduSiteKey;
|
||||
|
||||
@Autowired
|
||||
private OrganProxy organProxy;
|
||||
|
||||
@Autowired
|
||||
private OrganRepository organRepository;
|
||||
|
||||
@Autowired
|
||||
private PbxHostRepository pbxHostRes;
|
||||
|
||||
@Autowired
|
||||
private ExtensionRepository extensionRes;
|
||||
|
||||
@RequestMapping("/")
|
||||
public ModelAndView admin(HttpServletRequest request) {
|
||||
// logger.info("[admin] path {} queryString {}", request.getPathInfo(),request.getQueryString());
|
||||
ModelAndView view = request(super.createView("/apps/index"));
|
||||
User logined = super.getUser(request);
|
||||
Organ currentOrgan = super.getOrgan(request);
|
||||
|
||||
TimeZone timezone = TimeZone.getDefault();
|
||||
|
||||
List<Organ> organs = organProxy.findOrganInIds(logined.getAffiliates());
|
||||
|
||||
view.addObject(
|
||||
"skills",
|
||||
organProxy.findAllOrganByParent(currentOrgan).keySet().stream().collect(Collectors.joining(","))
|
||||
);
|
||||
|
||||
view.addObject("agentStatusReport", acdWorkMonitor.getAgentReport(currentOrgan != null ? currentOrgan.getId() : null));
|
||||
view.addObject("istenantshare", false);
|
||||
view.addObject("timeDifference", timezone.getRawOffset());
|
||||
view.addObject("organList", organs);
|
||||
view.addObject("currentOrgan", super.getOrgan(request));
|
||||
|
||||
// 增加版本信息
|
||||
view.addObject("appBuildDate", appBuildDate);
|
||||
view.addObject("appVersionAbbrev", appVersionAbbrev);
|
||||
view.addObject("appVersionNumber", appVersionNumber);
|
||||
view.addObject("appCustomerEntity", appCustomerEntity);
|
||||
|
||||
// 在线坐席状态信息
|
||||
view.addObject("agentStatus", cache.findOneAgentStatusByAgentno(logined.getId()));
|
||||
|
||||
// 呼叫中心信息
|
||||
if (MainContext.hasModule(Constants.CSKEFU_MODULE_CALLCENTER) && logined.isCallcenter()) {
|
||||
extensionRes.findByAgentno(logined.getId()).ifPresent(ext -> {
|
||||
PbxHost one = pbxHostRes.findById(ext.getHostid()).orElse(null);
|
||||
Map<String, Object> webrtcData = new HashMap<>();
|
||||
webrtcData.put("callCenterWebrtcIP", one.getWebrtcaddress());
|
||||
webrtcData.put("callCenterWebRtcPort", one.getWebrtcport());
|
||||
webrtcData.put("callCenterExtensionNum", ext.getExtension());
|
||||
try {
|
||||
webrtcData.put("callCenterExtensionPassword", MainUtils.decryption(ext.getPassword()));
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
logger.error("[admin]", e);
|
||||
webrtcData.put("callCenterError", "Invalid data for callcenter agent.");
|
||||
}
|
||||
view.addObject("webrtc", webrtcData);
|
||||
});
|
||||
}
|
||||
|
||||
if (StringUtils.isNotBlank(tongjiBaiduSiteKey) && !StringUtils.equalsIgnoreCase(tongjiBaiduSiteKey, "placeholder")) {
|
||||
logger.info("tongjiBaiduSiteKey: {}", tongjiBaiduSiteKey);
|
||||
view.addObject("tongjiBaiduSiteKey", tongjiBaiduSiteKey);
|
||||
}
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
@RequestMapping("/setorgan")
|
||||
@ResponseBody
|
||||
public String setOrgan(HttpServletRequest request, @Valid String organ) {
|
||||
if (StringUtils.isNotBlank(organ)) {
|
||||
Organ currentOrgan = organRepository.findById(organ).orElse(null);
|
||||
if (currentOrgan != null) {
|
||||
request.getSession(true).setAttribute(Constants.ORGAN_SESSION_NAME, currentOrgan);
|
||||
}
|
||||
}
|
||||
|
||||
return "ok";
|
||||
}
|
||||
|
||||
@RequestMapping("/lazyAgentStatus")
|
||||
public ModelAndView lazyAgentStatus(HttpServletRequest request) {
|
||||
ModelAndView view = request(super.createView("/public/agentstatustext"));
|
||||
Organ currentOrgan = super.getOrgan(request);
|
||||
view.addObject("agentStatusReport", acdWorkMonitor.getAgentReport(currentOrgan != null ? currentOrgan.getId() : null));
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
}
|
@ -1,414 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd.
|
||||
* <https://www.chatopera.com>, Licensed under the Chunsong Public
|
||||
* License, Version 1.0 (the "License"), https://docs.cskefu.com/licenses/v1.html
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* Copyright (C) 2018- Jun. 2023 Chatopera Inc, <https://www.chatopera.com>, Licensed under the Apache License, Version 2.0,
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Copyright (C) 2017 优客服-多渠道客服系统, Licensed under the Apache License, Version 2.0,
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*/
|
||||
package com.cskefu.cc.controller;
|
||||
|
||||
import com.cskefu.cc.basic.Constants;
|
||||
import com.cskefu.cc.basic.MainUtils;
|
||||
import com.cskefu.cc.basic.Viewport;
|
||||
import com.cskefu.cc.basic.auth.BearerTokenMgr;
|
||||
import com.cskefu.cc.cache.Cache;
|
||||
import com.cskefu.cc.controller.api.QueryParams;
|
||||
import com.cskefu.cc.exception.CSKefuException;
|
||||
import com.cskefu.cc.model.Organ;
|
||||
import com.cskefu.cc.model.StreamingFile;
|
||||
import com.cskefu.cc.model.User;
|
||||
import com.cskefu.cc.persistence.blob.JpaBlobHelper;
|
||||
import com.cskefu.cc.persistence.repository.StreamingFileRepository;
|
||||
import com.cskefu.cc.proxy.OrganProxy;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.commons.codec.binary.Base64;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.SessionAttributes;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import org.springframework.web.servlet.ModelAndView;
|
||||
|
||||
import jakarta.servlet.http.Cookie;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import java.io.IOException;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static com.cskefu.cc.basic.Constants.AUTH_TOKEN_TYPE_BASIC;
|
||||
import static com.cskefu.cc.basic.Constants.AUTH_TOKEN_TYPE_BEARER;
|
||||
|
||||
@Controller
|
||||
@SessionAttributes
|
||||
public class Handler {
|
||||
private static final Logger logger = LoggerFactory.getLogger(Handler.class);
|
||||
|
||||
@Autowired
|
||||
private JpaBlobHelper jpaBlobHelper;
|
||||
|
||||
@Autowired
|
||||
private StreamingFileRepository streamingFileRes;
|
||||
|
||||
@Autowired
|
||||
private Cache cache;
|
||||
|
||||
@Autowired
|
||||
private BearerTokenMgr bearerTokenMgr;
|
||||
|
||||
@Autowired
|
||||
private OrganProxy organProxy;
|
||||
|
||||
public final static int PAGE_SIZE_BG = 1;
|
||||
public final static int PAGE_SIZE_TW = 20;
|
||||
public final static int PAGE_SIZE_FV = 50;
|
||||
public final static int PAGE_SIZE_HA = 100;
|
||||
|
||||
private long starttime = System.currentTimeMillis();
|
||||
|
||||
public User getUser(HttpServletRequest request) {
|
||||
User user = (User) request.getSession(true).getAttribute(Constants.USER_SESSION_NAME);
|
||||
if (user == null) {
|
||||
String authorization = request.getHeader("authorization");
|
||||
if (StringUtils.isBlank(authorization) && request.getCookies() != null) {
|
||||
for (Cookie cookie : request.getCookies()) {
|
||||
if (cookie.getName().equals("authorization")) {
|
||||
authorization = cookie.getValue();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// trim token
|
||||
if (StringUtils.isNotBlank(authorization)) {
|
||||
String authorizationTrimed = authorization;
|
||||
if (authorization.startsWith(String.format("%s ", AUTH_TOKEN_TYPE_BEARER))) {
|
||||
authorizationTrimed = StringUtils.substring(authorization, 7);
|
||||
if (StringUtils.isNotBlank(authorizationTrimed)) {
|
||||
user = bearerTokenMgr.retrieve(authorizationTrimed);
|
||||
}
|
||||
} else if (authorization.startsWith(String.format("%s ", AUTH_TOKEN_TYPE_BASIC))) {
|
||||
authorizationTrimed = StringUtils.substring(authorization, 6);
|
||||
// TODO https://gitlab.chatopera.com/chatopera/chatopera.bot/issues/1292
|
||||
// get user with basic token mgr
|
||||
}
|
||||
}
|
||||
|
||||
if (user == null) {
|
||||
user = new User();
|
||||
user.setId(MainUtils.getContextID(request.getSession().getId()));
|
||||
user.setUsername(Constants.GUEST_USER + "_" + MainUtils.genIDByKey(user.getId()));
|
||||
user.setSessionid(user.getId());
|
||||
}
|
||||
} else {
|
||||
user.setSessionid(MainUtils.getContextID(request.getSession().getId()));
|
||||
}
|
||||
return user;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获得登录账号的当前导航的组织机构
|
||||
*
|
||||
* @param request
|
||||
* @return
|
||||
*/
|
||||
public Organ getOrgan(HttpServletRequest request) {
|
||||
User user = getUser(request);
|
||||
if (user.getOrgans() != null) {
|
||||
|
||||
Organ organ = (Organ) request.getSession(true).getAttribute(Constants.ORGAN_SESSION_NAME);
|
||||
if (organ == null) {
|
||||
organ = organProxy.getDefault(user.getOrgans().values());
|
||||
|
||||
if (organ != null) {
|
||||
request.getSession(true).setAttribute(Constants.ORGAN_SESSION_NAME, organ);
|
||||
}
|
||||
}
|
||||
return organ;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获得该用户的组织机构及附属组织机构的数组
|
||||
*
|
||||
* @param user
|
||||
* @return
|
||||
*/
|
||||
public List<String> getMyAffiliatesFlat(final User user) {
|
||||
ArrayList<String> organIds = new ArrayList<>(user.getAffiliates());
|
||||
return organIds;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获得当前用户导航的组织机构和附属组织机构的信息
|
||||
*
|
||||
* @param user
|
||||
* @return
|
||||
*/
|
||||
public List<String> getMyCurrentAffiliatesFlat(final User user) {
|
||||
ArrayList<String> organIds = new ArrayList<>(user.getCurrOrganAffiliates());
|
||||
return organIds;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建ElasticSearch基于部门查询的Filter
|
||||
*
|
||||
* @param request
|
||||
* @return
|
||||
* @throws CSKefuException
|
||||
*/
|
||||
public boolean preCheckPermissions(final HttpServletRequest request)
|
||||
throws CSKefuException {
|
||||
// 组合部门条件
|
||||
User u = getUser(request);
|
||||
if (u == null) {
|
||||
throw new CSKefuException("[esOrganFilter] 未能获取到登录用户。");
|
||||
} else if (u.isAdmin()) {
|
||||
// 管理员, 查看任何数据
|
||||
return true;
|
||||
} else {
|
||||
// 用户在部门中,通过部门过滤数据
|
||||
// String[] values = u.getAffiliates().toArray(new
|
||||
// String[u.getAffiliates().size()]);
|
||||
// boolQueryBuilder.filter(termsQuery("organ", values));
|
||||
// 不对contacts进行过滤,普通用户也可以查看该租户的任何数据
|
||||
// return true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建或从HTTP会话中查找到访客的User对象,该对象不在数据库中,属于临时会话。
|
||||
* 这个User很可能是打开一个WebIM访客聊天控件,随机生成用户名,之后和Contact关联
|
||||
* 这个用户可能关联一个OnlineUser,如果开始给TA分配坐席
|
||||
*
|
||||
* @param request
|
||||
* @param userid
|
||||
* @param nickname
|
||||
* @return
|
||||
*/
|
||||
public User getIMUser(HttpServletRequest request, String userid, String nickname) {
|
||||
User user = (User) request.getSession(true).getAttribute(Constants.IM_USER_SESSION_NAME);
|
||||
if (user == null) {
|
||||
user = new User();
|
||||
if (StringUtils.isNotBlank(userid)) {
|
||||
user.setId(userid);
|
||||
} else {
|
||||
user.setId(MainUtils.getContextID(request.getSession().getId()));
|
||||
}
|
||||
if (StringUtils.isNotBlank(nickname)) {
|
||||
user.setUsername(nickname);
|
||||
} else {
|
||||
Map<String, String> sessionMessage = cache.findOneSystemMapById(request.getSession().getId());
|
||||
if (sessionMessage != null) {
|
||||
String struname = sessionMessage.get("username");
|
||||
String strcname = sessionMessage.get("company_name");
|
||||
|
||||
user.setUsername(struname + "@" + strcname);
|
||||
} else {
|
||||
user.setUsername(Constants.GUEST_USER + "_" + MainUtils.genIDByKey(user.getId()));
|
||||
}
|
||||
}
|
||||
user.setSessionid(user.getId());
|
||||
} else {
|
||||
user.setSessionid(MainUtils.getContextID(request.getSession().getId()));
|
||||
}
|
||||
return user;
|
||||
}
|
||||
|
||||
public User getIMUser(HttpServletRequest request, String userid, String nickname, String sessionid) {
|
||||
User user = (User) request.getSession(true).getAttribute(Constants.IM_USER_SESSION_NAME);
|
||||
if (user == null) {
|
||||
user = new User();
|
||||
if (StringUtils.isNotBlank(userid)) {
|
||||
user.setId(userid);
|
||||
} else {
|
||||
user.setId(MainUtils.getContextID(request.getSession().getId()));
|
||||
}
|
||||
if (StringUtils.isNotBlank(nickname)) {
|
||||
user.setUsername(nickname);
|
||||
} else {
|
||||
Map<String, String> sessionMessage = cache.findOneSystemMapById(sessionid);
|
||||
if (sessionMessage != null) {
|
||||
String struname = sessionMessage.get("username");
|
||||
String strcname = sessionMessage.get("company_name");
|
||||
|
||||
user.setUsername(struname + "@" + strcname);
|
||||
} else {
|
||||
user.setUsername(Constants.GUEST_USER + "_" + MainUtils.genIDByKey(user.getId()));
|
||||
}
|
||||
}
|
||||
user.setSessionid(user.getId());
|
||||
} else {
|
||||
user.setSessionid(MainUtils.getContextID(request.getSession().getId()));
|
||||
}
|
||||
return user;
|
||||
}
|
||||
|
||||
public void setUser(HttpServletRequest request, User user) {
|
||||
request.getSession(true).removeAttribute(Constants.USER_SESSION_NAME);
|
||||
request.getSession(true).setAttribute(Constants.USER_SESSION_NAME, user);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建系统监控的 模板页面
|
||||
*
|
||||
* @param page
|
||||
* @return
|
||||
*/
|
||||
public Viewport createViewIncludedByFreemarkerTplForAdmin(String page) {
|
||||
return new Viewport("/admin/include/tpl", page);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建系统监控的 模板页面
|
||||
*
|
||||
* @param page
|
||||
* @return
|
||||
*/
|
||||
public Viewport createViewIncludedByFreemarkerTpl(String page) {
|
||||
return new Viewport("/apps/include/tpl", page);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建系统监控的 模板页面
|
||||
*
|
||||
* @param page
|
||||
* @return
|
||||
*/
|
||||
public Viewport createViewIncludedByFreemarkerTplForEntIM(final String page) {
|
||||
return new Viewport("/apps/entim/include/tpl", page);
|
||||
}
|
||||
|
||||
public Viewport createView(final String page) {
|
||||
return new Viewport(page);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param data
|
||||
* @return
|
||||
*/
|
||||
public ModelAndView request(Viewport data) {
|
||||
return new ModelAndView(data.getTemplate() != null ? data.getTemplate() : data.getPage(), "data", data);
|
||||
}
|
||||
|
||||
public int getP(HttpServletRequest request) {
|
||||
int page = 0;
|
||||
String p = request.getParameter("p");
|
||||
if (StringUtils.isNotBlank(p) && p.matches("[\\d]*")) {
|
||||
page = Integer.parseInt(p);
|
||||
if (page > 0) {
|
||||
page = page - 1;
|
||||
}
|
||||
}
|
||||
return page;
|
||||
}
|
||||
|
||||
public int getPs(HttpServletRequest request) {
|
||||
int pagesize = PAGE_SIZE_TW;
|
||||
String ps = request.getParameter("ps");
|
||||
if (StringUtils.isNotBlank(ps) && ps.matches("[\\d]*")) {
|
||||
pagesize = Integer.parseInt(ps);
|
||||
}
|
||||
return pagesize;
|
||||
}
|
||||
|
||||
public int getP(QueryParams params) {
|
||||
int page = 0;
|
||||
if (params != null && StringUtils.isNotBlank(params.getP()) && params.getP().matches("[\\d]*")) {
|
||||
page = Integer.parseInt(params.getP());
|
||||
if (page > 0) {
|
||||
page = page - 1;
|
||||
}
|
||||
}
|
||||
return page;
|
||||
}
|
||||
|
||||
public int getPs(QueryParams params) {
|
||||
int pagesize = PAGE_SIZE_TW;
|
||||
if (params != null && StringUtils.isNotBlank(params.getPs()) && params.getPs().matches("[\\d]*")) {
|
||||
pagesize = Integer.parseInt(params.getPs());
|
||||
}
|
||||
return pagesize;
|
||||
}
|
||||
|
||||
public int get50Ps(HttpServletRequest request) {
|
||||
int pagesize = PAGE_SIZE_FV;
|
||||
String ps = request.getParameter("ps");
|
||||
if (StringUtils.isNotBlank(ps) && ps.matches("[\\d]*")) {
|
||||
pagesize = Integer.parseInt(ps);
|
||||
}
|
||||
return pagesize;
|
||||
}
|
||||
|
||||
public long getStarttime() {
|
||||
return starttime;
|
||||
}
|
||||
|
||||
public void setStarttime(long starttime) {
|
||||
this.starttime = starttime;
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用Blob保存文件
|
||||
*
|
||||
* @param multipart
|
||||
* @return id
|
||||
* @throws IOException
|
||||
*/
|
||||
public String saveImageFileWithMultipart(MultipartFile multipart) throws IOException {
|
||||
StreamingFile sf = new StreamingFile();
|
||||
final String fileid = MainUtils.getUUID();
|
||||
sf.setId(fileid);
|
||||
sf.setMime(multipart.getContentType());
|
||||
sf.setData(jpaBlobHelper.createBlob(multipart.getInputStream(), multipart.getSize()));
|
||||
sf.setName(multipart.getOriginalFilename());
|
||||
streamingFileRes.save(sf);
|
||||
return fileid;
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用Blob保存文件
|
||||
*
|
||||
* @param dataStr Data URL 图片数据
|
||||
* @return id
|
||||
* @throws IOException
|
||||
*/
|
||||
public String saveImageFileWithDataURL(String dataStr) throws IOException {
|
||||
String[] cell = dataStr.split(";");
|
||||
String mime = cell[0].substring(5);
|
||||
String base64Str = cell[1].substring(7);
|
||||
byte[] buf = Base64.decodeBase64(base64Str);
|
||||
|
||||
StreamingFile sf = new StreamingFile();
|
||||
final String fileid = MainUtils.getUUID();
|
||||
sf.setId(fileid);
|
||||
sf.setMime(mime);
|
||||
sf.setData(jpaBlobHelper.createBlob(new ByteArrayInputStream(buf),
|
||||
buf.length));
|
||||
sf.setName(fileid);
|
||||
streamingFileRes.save(sf);
|
||||
return fileid;
|
||||
}
|
||||
|
||||
public String getSchema(HttpServletRequest request) {
|
||||
String schema = request.getScheme();
|
||||
String headerProto = request.getHeader("X-Forwarded-Proto");
|
||||
if (StringUtils.isNotBlank(headerProto)) {
|
||||
schema = headerProto;
|
||||
}
|
||||
return schema;
|
||||
}
|
||||
|
||||
}
|
@ -1,417 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd.
|
||||
* <https://www.chatopera.com>, Licensed under the Chunsong Public
|
||||
* License, Version 1.0 (the "License"), https://docs.cskefu.com/licenses/v1.html
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* Copyright (C) 2018- Jun. 2023 Chatopera Inc, <https://www.chatopera.com>, Licensed under the Apache License, Version 2.0,
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Copyright (C) 2017 优客服-多渠道客服系统, Licensed under the Apache License, Version 2.0,
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*/
|
||||
package com.cskefu.cc.controller;
|
||||
|
||||
import com.cskefu.cc.acd.ACDWorkMonitor;
|
||||
import com.cskefu.cc.basic.Constants;
|
||||
import com.cskefu.cc.basic.MainContext;
|
||||
import com.cskefu.cc.basic.MainUtils;
|
||||
import com.cskefu.cc.basic.auth.BearerTokenMgr;
|
||||
import com.cskefu.cc.model.AgentStatus;
|
||||
import com.cskefu.cc.model.Organ;
|
||||
import com.cskefu.cc.model.SystemConfig;
|
||||
import com.cskefu.cc.model.User;
|
||||
import com.cskefu.cc.model.UserRole;
|
||||
import com.cskefu.cc.persistence.repository.UserRepository;
|
||||
import com.cskefu.cc.persistence.repository.UserRoleRepository;
|
||||
import com.cskefu.cc.proxy.AgentProxy;
|
||||
import com.cskefu.cc.proxy.AgentSessionProxy;
|
||||
import com.cskefu.cc.proxy.OrganProxy;
|
||||
import com.cskefu.cc.proxy.UserProxy;
|
||||
import com.cskefu.cc.util.Menu;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.jasypt.exceptions.EncryptionOperationNotPossibleException;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.RequestHeader;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMethod;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.servlet.ModelAndView;
|
||||
|
||||
import jakarta.servlet.http.Cookie;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import jakarta.validation.Valid;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author CSKefu
|
||||
* @version 1.0.1
|
||||
*/
|
||||
@Controller
|
||||
public class LoginController extends Handler {
|
||||
private final static Logger logger = LoggerFactory.getLogger(LoginController.class);
|
||||
|
||||
@Autowired
|
||||
private UserRepository userRepository;
|
||||
|
||||
@Autowired
|
||||
private UserRoleRepository userRoleRes;
|
||||
|
||||
@Autowired
|
||||
private BearerTokenMgr bearerTokenMgr;
|
||||
|
||||
@Autowired
|
||||
private AgentProxy agentProxy;
|
||||
|
||||
@Autowired
|
||||
private AgentSessionProxy agentSessionProxy;
|
||||
|
||||
@Autowired
|
||||
private UserProxy userProxy;
|
||||
|
||||
@Autowired
|
||||
private ACDWorkMonitor acdWorkMonitor;
|
||||
|
||||
@Value("${tongji.baidu.sitekey}")
|
||||
private String tongjiBaiduSiteKey;
|
||||
|
||||
@Value("${extras.login.banner}")
|
||||
private String extrasLoginBanner;
|
||||
|
||||
@Value("${extras.login.chatbox}")
|
||||
private String extrasLoginChatbox;
|
||||
|
||||
private void putViewExtras(final ModelAndView view) {
|
||||
if (StringUtils.isNotBlank(extrasLoginBanner) && !StringUtils.equalsIgnoreCase(extrasLoginBanner, "off")) {
|
||||
view.addObject("extrasLoginBanner", extrasLoginBanner);
|
||||
} else {
|
||||
view.addObject("extrasLoginBanner", "off");
|
||||
}
|
||||
|
||||
if (StringUtils.isNotBlank(extrasLoginChatbox) && !StringUtils.equalsIgnoreCase(extrasLoginChatbox, "off")) {
|
||||
view.addObject("extrasLoginChatbox", extrasLoginChatbox);
|
||||
} else {
|
||||
view.addObject("extrasLoginChatbox", "off");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 登录页面
|
||||
*
|
||||
* @param request
|
||||
* @param response
|
||||
* @param referer
|
||||
* @param msg
|
||||
* @return
|
||||
* @throws NoSuchAlgorithmException
|
||||
*/
|
||||
@RequestMapping(value = "/login", method = RequestMethod.GET)
|
||||
@Menu(type = "apps", subtype = "user", access = true)
|
||||
public ModelAndView login(HttpServletRequest request, HttpServletResponse response,
|
||||
@RequestHeader(value = "referer", required = false) String referer, @Valid String msg) {
|
||||
ModelAndView view = new ModelAndView("redirect:/");
|
||||
if (request.getSession(true).getAttribute(Constants.USER_SESSION_NAME) == null) {
|
||||
view = new ModelAndView("/login");
|
||||
if (StringUtils.isNotBlank(request.getParameter("referer"))) {
|
||||
referer = request.getParameter("referer");
|
||||
}
|
||||
if (StringUtils.isNotBlank(referer)) {
|
||||
view.addObject("referer", referer);
|
||||
}
|
||||
Cookie[] cookies = request.getCookies(); // 这样便可以获取一个cookie数组
|
||||
if (cookies != null) {
|
||||
for (Cookie cookie : cookies) {
|
||||
if (cookie != null && StringUtils.isNotBlank(cookie.getName()) && StringUtils.isNotBlank(
|
||||
cookie.getValue())) {
|
||||
if (cookie.getName().equals(Constants.CSKEFU_SYSTEM_COOKIES_FLAG)) {
|
||||
String flagid;
|
||||
try {
|
||||
flagid = MainUtils.decryption(cookie.getValue());
|
||||
if (StringUtils.isNotBlank(flagid)) {
|
||||
User user = userRepository.findById(flagid).orElse(null);
|
||||
if (user != null) {
|
||||
view = this.processLogin(request, user, referer);
|
||||
}
|
||||
}
|
||||
} catch (EncryptionOperationNotPossibleException e) {
|
||||
logger.error("[login] error:", e);
|
||||
view = request(super.createView("/public/clearcookie"));
|
||||
return view;
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
logger.error("[login] error:", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (StringUtils.isNotBlank(msg)) {
|
||||
view.addObject("msg", msg);
|
||||
}
|
||||
SystemConfig systemConfig = MainUtils.getSystemConfig();
|
||||
|
||||
// is Enable reg tenant
|
||||
// view.addObject("show", false);
|
||||
if (systemConfig != null) {
|
||||
view.addObject("systemConfig", systemConfig);
|
||||
}
|
||||
|
||||
if (StringUtils.isNotBlank(tongjiBaiduSiteKey)
|
||||
&& !StringUtils.equalsIgnoreCase(tongjiBaiduSiteKey, "placeholder")) {
|
||||
view.addObject("tongjiBaiduSiteKey", tongjiBaiduSiteKey);
|
||||
}
|
||||
|
||||
putViewExtras(view);
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交登录表单
|
||||
*
|
||||
* @param request
|
||||
* @param response
|
||||
* @param user
|
||||
* @param referer
|
||||
* @param sla
|
||||
* @return
|
||||
* @throws NoSuchAlgorithmException
|
||||
*/
|
||||
@RequestMapping(value = "/login", method = RequestMethod.POST)
|
||||
@Menu(type = "apps", subtype = "user", access = true)
|
||||
public ModelAndView login(
|
||||
final HttpServletRequest request,
|
||||
final HttpServletResponse response,
|
||||
@Valid User user,
|
||||
@Valid String referer,
|
||||
@Valid String sla) throws NoSuchAlgorithmException {
|
||||
ModelAndView view = new ModelAndView("redirect:/");
|
||||
if (request.getSession(true).getAttribute(Constants.USER_SESSION_NAME) == null) {
|
||||
if (user != null && user.getUsername() != null) {
|
||||
final User loginUser = userRepository.findByUsernameAndPasswordAndDatastatus(
|
||||
user.getUsername(), MainUtils.md5(user.getPassword()), false);
|
||||
if (loginUser != null && StringUtils.isNotBlank(loginUser.getId())) {
|
||||
view = this.processLogin(request, loginUser, referer);
|
||||
|
||||
// 自动登录
|
||||
if (StringUtils.equals("1", sla)) {
|
||||
Cookie flagid = new Cookie(
|
||||
Constants.CSKEFU_SYSTEM_COOKIES_FLAG, MainUtils.encryption(loginUser.getId()));
|
||||
flagid.setMaxAge(7 * 24 * 60 * 60);
|
||||
response.addCookie(flagid);
|
||||
}
|
||||
|
||||
// add authorization code for rest api
|
||||
String uuid = MainUtils.getUUID();
|
||||
String token = String.format("%s %s", Constants.AUTH_TOKEN_TYPE_BEARER, uuid);
|
||||
bearerTokenMgr.update(token, loginUser);
|
||||
userRepository.save(loginUser); // 更新登录状态到数据库
|
||||
response.addCookie((new Cookie("authorization", uuid)));
|
||||
|
||||
// 该登录用户是坐席,并且具有坐席对话的角色
|
||||
if ((loginUser.isAgent() &&
|
||||
loginUser.getRoleAuthMap().containsKey("A01") &&
|
||||
((boolean) loginUser.getRoleAuthMap().get("A01") == true))
|
||||
|| loginUser.isAdmin()) {
|
||||
try {
|
||||
/****************************************
|
||||
* 登录成功,设置该坐席为就绪状态(默认)
|
||||
****************************************/
|
||||
// https://gitlab.chatopera.com/chatopera/cosinee.w4l/issues/306
|
||||
final AgentStatus agentStatus = agentProxy.resolveAgentStatusByAgentno(
|
||||
loginUser.getId(), loginUser.getSkills());
|
||||
agentStatus.setBusy(false);
|
||||
agentProxy.ready(loginUser, agentStatus, false);
|
||||
|
||||
// 工作状态记录
|
||||
acdWorkMonitor.recordAgentStatus(agentStatus.getAgentno(),
|
||||
agentStatus.getUsername(),
|
||||
agentStatus.getAgentno(),
|
||||
user.isAdmin(), // 0代表admin
|
||||
agentStatus.getAgentno(),
|
||||
MainContext.AgentStatusEnum.OFFLINE.toString(),
|
||||
MainContext.AgentStatusEnum.READY.toString(),
|
||||
MainContext.AgentWorkType.MEIDIACHAT.toString(),
|
||||
null);
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("[login] set agent status", e);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
view = request(super.createView("/login"));
|
||||
if (StringUtils.isNotBlank(referer)) {
|
||||
view.addObject("referer", referer);
|
||||
}
|
||||
|
||||
putViewExtras(view);
|
||||
|
||||
view.addObject("msg", "0");
|
||||
}
|
||||
}
|
||||
}
|
||||
SystemConfig systemConfig = MainUtils.getSystemConfig();
|
||||
// is Enable reg tenant
|
||||
// view.addObject("show", false);
|
||||
|
||||
if (systemConfig != null) {
|
||||
view.addObject("systemConfig", systemConfig);
|
||||
}
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理登录事件
|
||||
*
|
||||
* @param request
|
||||
* @param loginUser
|
||||
* @param referer
|
||||
* @return
|
||||
*/
|
||||
private ModelAndView processLogin(final HttpServletRequest request, final User loginUser, String referer) {
|
||||
ModelAndView view = new ModelAndView();
|
||||
if (loginUser != null) {
|
||||
// 设置登录用户的状态
|
||||
loginUser.setLogin(true);
|
||||
// 更新redis session信息,用以支持sso
|
||||
agentSessionProxy.updateUserSession(
|
||||
loginUser.getId(), MainUtils.getContextID(request.getSession().getId()));
|
||||
loginUser.setSessionid(MainUtils.getContextID(request.getSession().getId()));
|
||||
|
||||
if (StringUtils.isNotBlank(referer)) {
|
||||
view = new ModelAndView("redirect:" + referer);
|
||||
} else {
|
||||
view = new ModelAndView("redirect:/");
|
||||
}
|
||||
|
||||
// 登录成功 判断是否进入多租户页面
|
||||
SystemConfig systemConfig = MainUtils.getSystemConfig();
|
||||
if (systemConfig != null && systemConfig.isEnabletneant() && systemConfig.isTenantconsole()
|
||||
&& !loginUser.isAdmin()) {
|
||||
view = new ModelAndView("redirect:/apps/tenant/index");
|
||||
}
|
||||
List<UserRole> userRoleList = userRoleRes.findByUser(loginUser);
|
||||
if (userRoleList != null && userRoleList.size() > 0) {
|
||||
for (UserRole userRole : userRoleList) {
|
||||
loginUser.getRoleList().add(userRole.getRole());
|
||||
}
|
||||
}
|
||||
|
||||
// 获取用户所在部门及附属部门的信息
|
||||
userProxy.attachOrgansPropertiesForUser(loginUser);
|
||||
|
||||
|
||||
Organ currentOrgan = super.getOrgan(request);
|
||||
userProxy.attachCurrentOrgansPropertiesForUser(loginUser, currentOrgan);
|
||||
|
||||
// 添加角色信息
|
||||
userProxy.attachRolesMap(loginUser, currentOrgan);
|
||||
|
||||
loginUser.setLastlogintime(new Date());
|
||||
if (StringUtils.isNotBlank(loginUser.getId())) {
|
||||
userRepository.save(loginUser);
|
||||
}
|
||||
|
||||
super.setUser(request, loginUser);
|
||||
}
|
||||
return view;
|
||||
}
|
||||
|
||||
/**
|
||||
* 登出用户
|
||||
* code代表登出的原因
|
||||
*
|
||||
* @param request
|
||||
* @param response
|
||||
* @param code 登出的代码
|
||||
* @return
|
||||
*/
|
||||
@RequestMapping("/logout")
|
||||
public String logout(HttpServletRequest request, HttpServletResponse response,
|
||||
@RequestParam(value = "code", required = false) String code) throws UnsupportedEncodingException {
|
||||
final User user = super.getUser(request);
|
||||
request.getSession().removeAttribute(Constants.USER_SESSION_NAME);
|
||||
request.getSession().invalidate();
|
||||
Cookie[] cookies = request.getCookies();
|
||||
if (cookies != null) {
|
||||
for (Cookie cookie : cookies) {
|
||||
if (cookie != null && StringUtils.isNotBlank(cookie.getName()) && StringUtils.isNotBlank(
|
||||
cookie.getValue())) {
|
||||
if (cookie.getName().equals(Constants.CSKEFU_SYSTEM_COOKIES_FLAG)) {
|
||||
cookie.setMaxAge(0);
|
||||
response.addCookie(cookie);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (StringUtils.isNotBlank(code)) {
|
||||
return "redirect:/?msg=" + code;
|
||||
}
|
||||
|
||||
return "redirect:/";
|
||||
}
|
||||
|
||||
@RequestMapping(value = "/register")
|
||||
@Menu(type = "apps", subtype = "user", access = true)
|
||||
public ModelAndView register(HttpServletRequest request, HttpServletResponse response, @Valid String msg) {
|
||||
ModelAndView view = request(super.createView("redirect:/"));
|
||||
if (request.getSession(true).getAttribute(Constants.USER_SESSION_NAME) == null) {
|
||||
view = request(super.createView("/register"));
|
||||
}
|
||||
if (StringUtils.isNotBlank(msg)) {
|
||||
view.addObject("msg", msg);
|
||||
}
|
||||
return view;
|
||||
}
|
||||
|
||||
@RequestMapping("/addAdmin")
|
||||
@Menu(type = "apps", subtype = "user", access = true)
|
||||
public ModelAndView addAdmin(HttpServletRequest request, HttpServletResponse response, @Valid User user) {
|
||||
String msg = "";
|
||||
msg = validUser(user);
|
||||
if (StringUtils.isNotBlank(msg)) {
|
||||
return request(super.createView("redirect:/register.html?msg=" + msg));
|
||||
} else {
|
||||
user.setUname(user.getUsername());
|
||||
user.setAdmin(true);
|
||||
if (StringUtils.isNotBlank(user.getPassword())) {
|
||||
user.setPassword(MainUtils.md5(user.getPassword()));
|
||||
}
|
||||
userRepository.save(user);
|
||||
}
|
||||
ModelAndView view = this.processLogin(request, user, "");
|
||||
return view;
|
||||
}
|
||||
|
||||
private String validUser(User user) {
|
||||
String msg = "";
|
||||
User tempUser = userRepository.findByUsernameAndDatastatus(user.getUsername(), false);
|
||||
if (tempUser != null) {
|
||||
msg = "username_exist";
|
||||
return msg;
|
||||
}
|
||||
tempUser = userRepository.findByEmailAndDatastatus(user.getEmail(), false);
|
||||
if (tempUser != null) {
|
||||
msg = "email_exist";
|
||||
return msg;
|
||||
}
|
||||
tempUser = userRepository.findByMobileAndDatastatus(user.getMobile(), false);
|
||||
if (tempUser != null) {
|
||||
msg = "mobile_exist";
|
||||
return msg;
|
||||
}
|
||||
return msg;
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user