1
0
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:
lecjy 2023-10-17 21:10:05 +08:00
parent 19badce8fb
commit 91bcca21f6
2390 changed files with 6338 additions and 602592 deletions

View File

@ -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

View 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

View 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

View 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

View 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

View 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

View 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: 提供春松客服定制化开发、机器人客服平台等

View File

@ -1,13 +1,21 @@
---
reviewers : cskefu/reviewers
---
<!--- 在标题中简略说明问题 -->
## 描述
<!--- 详细的描述变更 -->
### 关联 Issue #
## 解决的问题
<!--- 为什么变更是必要的? -->
<!--- 如果这个PR解决了其他Issue添加链接 -->
## 测试情况
<!--- 详细介绍怎么测试变更了 -->
<!--- 介绍测试环境 -->
<!--- 变更对其他代码的影响 -->
@ -15,13 +23,17 @@
## 截屏
## 变更的类型
<!--- 变更有哪些特点,添加 `x` 到下面的对应项目中: -->
- [ ] 解决Bug
- [ ] 解决 Bug
- [ ] 新功能(不影响其他功能)
- [ ] 对其他功能有影响
## 检查:
## 检查
<!--- 检查下面,各项,添加 `x` 到下面的对应项目中: -->
- [ ] 我的变更和代码规范一致
- [ ] 我的变更需要更新文档
- [ ] 我已经更新了对应的文档

View File

@ -0,0 +1,9 @@
---
reviewers : cskefu/reviewers
---
### Requirements for Contributing Documentation
## 变更说明
### 关联 Issue #

View File

@ -0,0 +1,9 @@
---
reviewers : cskefu/reviewers
---
### Requirements for Contributing a Performance Improvement
## 性能提升
### 关联 Issue #

8
.github/CODEOWNERS vendored
View File

@ -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

View File

@ -1,12 +0,0 @@
# 描述
## 现在行为
## 预期行为
# 解决方案
# 环境
* 代码版本:
Git commit hash (`git rev-parse HEAD`)

View File

@ -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

View File

@ -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
View File

@ -25,3 +25,5 @@ docker-compose.custom.yml
private/
!cskefu-frontend/.vscode
.tool-versions
smart-doc/
target/

View File

@ -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) 中。
## 期待您成为春松客服贡献者
不管是何种贡献,只有大小之分,而无本质区别。
一起贡献,一起好!
没有贡献,都不好!
团结是共赢,分裂是全输。
![image](./public/assets/screenshot-20220323-163051.jpg)
2提交 Issue 到[春松客服 Issues](https://gitee.com/cskefu/cskefu/issues/new) 并撰写文档内容,使用该方案则需要后续其他协作者提交到 [春松客服文档中心 Git 仓库](https://gitee.com/cskefu/docs) 中。

548
README.md
View File

@ -1,276 +1,272 @@
<div align=right>
[主页](https://www.cskefu.com/) | [开源许可协议](https://docs.cskefu.com/licenses/v1.html) | [工单列表](https://github.com/cskefu/cskefu/issues) | [路线图](https://github.com/orgs/cskefu/projects/1)
</div>
# 春松客服
[![GitHub Stargazers](https://img.shields.io/github/stars/chatopera/cskefu.svg?style=social&label=Star&maxAge=2592000)](https://github.com/cskefu/cskefu/stargazers) [![GitHub Forks](https://img.shields.io/github/forks/chatopera/cskefu.svg?style=social&label=Fork&maxAge=2592000)](https://github.com/cskefu/cskefu/network/members) [![License](https://cdndownload2.chatopera.com/cskefu/licenses/chunsong1.0.svg)](https://www.cskefu.com/licenses/v1.html "开源许可协议") [![GitHub Issues](https://img.shields.io/github/issues/chatopera/cskefu.svg)](https://github.com/cskefu/cskefu/issues) [![GitHub Issues Closed](https://img.shields.io/github/issues-closed/chatopera/cskefu.svg)](https://github.com/cskefu/cskefu/issues?q=is%3Aissue+is%3Aclosed) [![docker](https://img.shields.io/docker/pulls/chatopera/contact-center.svg "Docker Pulls")](https://hub.docker.com/r/chatopera/contact-center/) <!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
[![All Contributors](https://img.shields.io/badge/all_contributors-34-orange.svg?style=flat-square)](#contributors-)
<!-- ALL-CONTRIBUTORS-BADGE:END -->
[https://www.cskefu.com](https://www.cskefu.com/)
| 版本 | 文档中心 | Git 分支 | 状态 |
| --- | --- | --- | --- |
| v8.x | [v8](https://docs.cskefu.com/docs/) | [GitHub](https://github.com/cskefu/cskefu/tree/develop) \| [Gitee](https://gitee.com/cskefu/cskefu/tree/develop/) | Active, 维护中 |
| v7.x | [v7](https://docs.cskefu.com/docs/v7/) | [GitHub](https://github.com/cskefu/cskefu/tree/v7) \| [Gitee](https://gitee.com/cskefu/cskefu/tree/v7/) | Sunset, 维护终止 |
:hearts: 春松客服的愿景:
- 公元 2032 年内1000 万企业上线开源客服系统
:innocent: 春松客服的承诺:
- 坚持基础功能开源,不发布垃圾
- 坚持持续优化
- 坚持商业友好授权
春松客服宣言视频: [Bilibili](https://www.bilibili.com/video/BV1hu411o76r/) | [YouTube](https://youtu.be/ILf3BWpq4Ns)
新版本介绍:[观看春松客服 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)。
<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
<!-- prettier-ignore-start -->
<!-- markdownlint-disable -->
<table>
<tbody>
<tr>
<td align="center" valign="top" width="11.11%"><a href="https://github.com/mukaiu"><img src="https://avatars.githubusercontent.com/u/7746790?v=4?s=50" width="50px;" alt="Mukaiu"/><br /><sub><b>Mukaiu</b></sub></a><br /><a href="https://github.com/cskefu/cskefu/commits?author=mukaiu" title="Code">💻</a> <a href="#infra-mukaiu" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td>
<td align="center" valign="top" width="11.11%"><a href="https://www.linkedin.com/in/hai-liang-wang/"><img src="https://avatars.githubusercontent.com/u/3538629?v=4?s=50" width="50px;" alt="Hai Liang W."/><br /><sub><b>Hai Liang W.</b></sub></a><br /><a href="#plugin-hailiang-wang" title="Plugin/utility libraries">🔌</a> <a href="#financial-hailiang-wang" title="Financial">💵</a></td>
<td align="center" valign="top" width="11.11%"><a href="https://github.com/shih945"><img src="https://avatars.githubusercontent.com/u/29646781?v=4?s=50" width="50px;" alt="SHIH"/><br /><sub><b>SHIH</b></sub></a><br /><a href="https://github.com/cskefu/cskefu/commits?author=shih945" title="Code">💻</a></td>
<td align="center" valign="top" width="11.11%"><a href="https://github.com/luruiGit"><img src="https://avatars.githubusercontent.com/u/49265205?v=4?s=50" width="50px;" alt="luruiGit"/><br /><sub><b>luruiGit</b></sub></a><br /><a href="https://github.com/cskefu/cskefu/commits?author=luruiGit" title="Code">💻</a></td>
<td align="center" valign="top" width="11.11%"><a href="http://enze5088.github.io"><img src="https://avatars.githubusercontent.com/u/14285786?v=4?s=50" width="50px;" alt="Enze"/><br /><sub><b>Enze</b></sub></a><br /><a href="https://github.com/cskefu/cskefu/commits?author=enze5088" title="Code">💻</a></td>
<td align="center" valign="top" width="11.11%"><a href="https://blog.dengchao.fun"><img src="https://avatars.githubusercontent.com/u/16363180?v=4?s=50" width="50px;" alt="邓超"/><br /><sub><b>邓超</b></sub></a><br /><a href="https://github.com/cskefu/cskefu/commits?author=DevDengChao" title="Code">💻</a></td>
<td align="center" valign="top" width="11.11%"><a href="https://github.com/Happy5"><img src="https://avatars.githubusercontent.com/u/53087368?v=4?s=50" width="50px;" alt="Happy5"/><br /><sub><b>Happy5</b></sub></a><br /><a href="#ideas-Happy5" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center" valign="top" width="11.11%"><a href="https://www.csdn.net"><img src="https://avatars.githubusercontent.com/u/3679798?v=4?s=50" width="50px;" alt="kyle"/><br /><sub><b>kyle</b></sub></a><br /><a href="https://github.com/cskefu/cskefu/commits?author=kylezhang" title="Code">💻</a> <a href="#talk-kylezhang" title="Talks">📢</a></td>
<td align="center" valign="top" width="11.11%"><a href="https://github.com/xianliwang"><img src="https://avatars.githubusercontent.com/u/52594347?v=4?s=50" width="50px;" alt="xianliwang"/><br /><sub><b>xianliwang</b></sub></a><br /><a href="#video-xianliwang" title="Videos">📹</a> <a href="https://github.com/cskefu/cskefu/commits?author=xianliwang" title="Tests">⚠️</a></td>
</tr>
<tr>
<td align="center" valign="top" width="11.11%"><a href="https://github.com/lihang2016"><img src="https://avatars.githubusercontent.com/u/23203931?v=4?s=50" width="50px;" alt="lihang2016"/><br /><sub><b>lihang2016</b></sub></a><br /><a href="#ideas-lihang2016" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center" valign="top" width="11.11%"><a href="https://github.com/live-in-the-moment"><img src="https://avatars.githubusercontent.com/u/62800943?v=4?s=50" width="50px;" alt="live-in-the-moment"/><br /><sub><b>live-in-the-moment</b></sub></a><br /><a href="#ideas-live-in-the-moment" title="Ideas, Planning, & Feedback">🤔</a> <a href="https://github.com/cskefu/cskefu/issues?q=author%3Alive-in-the-moment" title="Bug reports">🐛</a> <a href="https://github.com/cskefu/cskefu/commits?author=live-in-the-moment" title="Tests">⚠️</a></td>
<td align="center" valign="top" width="11.11%"><a href="https://github.com/ArioWei"><img src="https://avatars.githubusercontent.com/u/41034256?v=4?s=50" width="50px;" alt="ArioWei"/><br /><sub><b>ArioWei</b></sub></a><br /><a href="https://github.com/cskefu/cskefu/commits?author=ArioWei" title="Tests">⚠️</a></td>
<td align="center" valign="top" width="11.11%"><a href="http://www.youkefu.cn"><img src="https://avatars.githubusercontent.com/u/48078408?v=4?s=50" width="50px;" alt="优客服"/><br /><sub><b>优客服</b></sub></a><br /><a href="https://github.com/cskefu/cskefu/commits?author=youkefu" title="Code">💻</a> <a href="https://github.com/cskefu/cskefu/commits?author=youkefu" title="Tests">⚠️</a> <a href="#business-youkefu" title="Business development">💼</a> <a href="#design-youkefu" title="Design">🎨</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 /><a href="#ideas-lecjy" title="Ideas, Planning, & Feedback">🤔</a> <a href="#talk-lecjy" title="Talks">📢</a></td>
<td align="center" valign="top" width="11.11%"><a href="https://github.com/xl111"><img src="https://avatars.githubusercontent.com/u/64338718?v=4?s=50" width="50px;" alt="徐。。"/><br /><sub><b>徐。。</b></sub></a><br /><a href="https://github.com/cskefu/cskefu/commits?author=xl111" title="Code">💻</a></td>
<td align="center" valign="top" width="11.11%"><a href="https://github.com/viaco2ove"><img src="https://avatars.githubusercontent.com/u/8044837?v=4?s=50" width="50px;" alt="viaco2ove"/><br /><sub><b>viaco2ove</b></sub></a><br /><a href="https://github.com/cskefu/cskefu/commits?author=viaco2ove" title="Code">💻</a></td>
<td align="center" valign="top" width="11.11%"><a href="https://github.com/understanding"><img src="https://avatars.githubusercontent.com/u/2801277?v=4?s=50" width="50px;" alt="understanding"/><br /><sub><b>understanding</b></sub></a><br /><a href="https://github.com/cskefu/cskefu/commits?author=understanding" title="Tests">⚠️</a></td>
<td align="center" valign="top" width="11.11%"><a href="https://github.com/MQPearth"><img src="https://avatars.githubusercontent.com/u/32632796?v=4?s=50" width="50px;" alt="MQPearth"/><br /><sub><b>MQPearth</b></sub></a><br /><a href="https://github.com/cskefu/cskefu/commits?author=MQPearth" title="Tests">⚠️</a></td>
</tr>
<tr>
<td align="center" valign="top" width="11.11%"><a href="https://github.com/SkorpiosL"><img src="https://avatars.githubusercontent.com/u/32902343?v=4?s=50" width="50px;" alt="SkorpiosL"/><br /><sub><b>SkorpiosL</b></sub></a><br /><a href="https://github.com/cskefu/cskefu/commits?author=SkorpiosL" title="Tests">⚠️</a></td>
<td align="center" valign="top" width="11.11%"><a href="https://github.com/always-China"><img src="https://avatars.githubusercontent.com/u/49581101?v=4?s=50" width="50px;" alt="hua"/><br /><sub><b>hua</b></sub></a><br /><a href="https://github.com/cskefu/cskefu/commits?author=always-China" title="Code">💻</a></td>
<td align="center" valign="top" width="11.11%"><a href="https://github.com/wq11123"><img src="https://avatars.githubusercontent.com/u/40993206?v=4?s=50" width="50px;" alt="wq11123"/><br /><sub><b>wq11123</b></sub></a><br /><a href="https://github.com/cskefu/cskefu/commits?author=wq11123" title="Tests">⚠️</a> <a href="#video-wq11123" title="Videos">📹</a> <a href="#ideas-wq11123" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center" valign="top" width="11.11%"><a href="https://github.com/MouMouQQ"><img src="https://avatars.githubusercontent.com/u/101631131?v=4?s=50" width="50px;" alt="MouMouQQ"/><br /><sub><b>MouMouQQ</b></sub></a><br /><a href="#ideas-MouMouQQ" title="Ideas, Planning, & Feedback">🤔</a> <a href="https://github.com/cskefu/cskefu/commits?author=MouMouQQ" title="Tests">⚠️</a></td>
<td align="center" valign="top" width="11.11%"><a href="https://github.com/tigerun"><img src="https://avatars.githubusercontent.com/u/17540364?v=4?s=50" width="50px;" alt="Tigerun"/><br /><sub><b>Tigerun</b></sub></a><br /><a href="#ideas-tigerun" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center" valign="top" width="11.11%"><a href="https://github.com/yangbailiang"><img src="https://avatars.githubusercontent.com/u/50096675?v=4?s=50" width="50px;" alt="yangbailiang"/><br /><sub><b>yangbailiang</b></sub></a><br /><a href="https://github.com/cskefu/cskefu/issues?q=author%3Ayangbailiang" title="Bug reports">🐛</a> <a href="https://github.com/cskefu/cskefu/commits?author=yangbailiang" title="Tests">⚠️</a></td>
<td align="center" valign="top" width="11.11%"><a href="https://github.com/lokywang"><img src="https://avatars.githubusercontent.com/u/28672424?v=4?s=50" width="50px;" alt="lokywang"/><br /><sub><b>lokywang</b></sub></a><br /><a href="#ideas-lokywang" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center" valign="top" width="11.11%"><a href="https://github.com/jichoucc"><img src="https://avatars.githubusercontent.com/u/87190214?v=4?s=50" width="50px;" alt="jichoucc"/><br /><sub><b>jichoucc</b></sub></a><br /><a href="https://github.com/cskefu/cskefu/issues?q=author%3Ajichoucc" title="Bug reports">🐛</a> <a href="https://github.com/cskefu/cskefu/commits?author=jichoucc" title="Tests">⚠️</a></td>
<td align="center" valign="top" width="11.11%"><a href="https://github.com/wuyongyin"><img src="https://avatars.githubusercontent.com/u/20410234?v=4?s=50" width="50px;" alt="wuyongyin"/><br /><sub><b>wuyongyin</b></sub></a><br /><a href="#ideas-wuyongyin" title="Ideas, Planning, & Feedback">🤔</a></td>
</tr>
<tr>
<td align="center" valign="top" width="11.11%"><a href="https://github.com/wangdayan"><img src="https://avatars.githubusercontent.com/u/62323175?v=4?s=50" width="50px;" alt="Claire"/><br /><sub><b>Claire</b></sub></a><br /><a href="https://github.com/cskefu/cskefu/commits?author=wangdayan" title="Tests">⚠️</a></td>
<td align="center" valign="top" width="11.11%"><a href="https://github.com/zc1813400107"><img src="https://avatars.githubusercontent.com/u/46372405?v=4?s=50" width="50px;" alt="super"/><br /><sub><b>super</b></sub></a><br /><a href="https://github.com/cskefu/cskefu/commits?author=zc1813400107" title="Code">💻</a> <a href="https://github.com/cskefu/cskefu/commits?author=zc1813400107" title="Documentation">📖</a></td>
<td align="center" valign="top" width="11.11%"><a href="https://github.com/xiaobo9"><img src="https://avatars.githubusercontent.com/u/1284376?v=4?s=50" width="50px;" alt="xiaobo9"/><br /><sub><b>xiaobo9</b></sub></a><br /><a href="https://github.com/cskefu/cskefu/commits?author=xiaobo9" title="Code">💻</a></td>
<td align="center" valign="top" width="11.11%"><a href="https://github.com/zhangchanglong"><img src="https://avatars.githubusercontent.com/u/3481828?v=4?s=50" width="50px;" alt="zhangchanglong"/><br /><sub><b>zhangchanglong</b></sub></a><br /><a href="#eventOrganizing-zhangchanglong" title="Event Organizing">📋</a></td>
<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>
</tr>
</tbody>
</table>
<!-- markdownlint-restore -->
<!-- prettier-ignore-end -->
<!-- ALL-CONTRIBUTORS-LIST:END -->
## 功能介绍
<!-- <img src="./public/assets/cskefu-2021-08-22-8.22.09PM.jpg" width="900"> -->
春松客服提供的开源代码,即[CSKeFu](https://github.com/cskefu/cskefu),包含多个开箱即用的模块:
- 账号及组织机构管理:按组织、角色分配账号权限
- 坐席监控:设置坐席监控角色的人员可以看到并干预访客会话
- 联系人和客户管理CRM 模块,管理联系人和客户,细粒度维护客户信息,自定义标签和打标签,记录来往历史等
- 网页渠道组件:一分钟接入对话窗口,支持技能组、邀请和关联联系人等
- Facebook 渠道组件:快速接入 [Facebook Messenger](https://www.messenger.com/) 渠道,通过 Messenger 支持 Facebook 粉丝页、[Shopify](https://www.shopify.com/) 等海外社交、电商平台
- 坐席工作台:汇聚多渠道访客请求,坐席根据策略自动分配,自动弹屏,转接等
- 机器人客服:与[Chatopera 云服务](/products/chatbot-platform/index.html)集成
- 企业聊天:支持企业员工在春松客服系统中群聊和私聊
- 质检:历史会话、服务小结、服务反馈及相关报表
了解功能详细介绍,参考[文档中心](https://docs.cskefu.com/)。
## 产品演示
<p align="center">
<b>欢迎页</b><br>
<img src="https://static-public.chatopera.com/assets/images/cskefu/cskefu-screen-1.jpg" width="900">
</p>
### 坐席工作台
登录演示环境,查看更多产品能力:[https://demo.cskefu.com/](https://demo.cskefu.com/)
| **登录账号** | **密码** | **角色** |
| ------------ | --------- | -------------- |
| admin | admin1234 | 系统超级管理员 |
| zhangsan | agent1234 | 客服坐席人员 |
### 网页端访客示例
[https://demo.cskefu.com/testclient.html](http://demo.cskefu.com/testclient.html)
- 登录张三后可接待访客,否则显示没有客服人员在线
### 机器人客服示例
[https://oh-my.cskefu.com/im/text/0nhckh.html](https://oh-my.cskefu.com/im/text/0nhckh.html)
## 快速开始
### 春松客服用户使用指南
- 快速的了解和介绍春松客服
- 快速的查找和春松客服相关的材料
下载[《春松客服用户使用指南》](https://www.cskefu.com/moment/825.html/)。
### 安装部署
支持云原生环境,容器化一键部署,现在就使用春松客服!参考[《私有部署文档》](http://docs.cskefu.com/docs/deploy)。
### 系统初始化
部署后,进行系统初始化,为组织设定部门、权限、账号等,参考[《系统初始化文档》](https://docs.cskefu.com/docs/initialization)。
### 运维
备份、升级、回滚等运维工作,参考[《系统维护文档》](https://docs.cskefu.com/docs/osc/maintainence)。
### 运营使用指南
关于产品的具体使用说明,请参考[《春松客服文档》](https://docs.cskefu.com)。
### 立即上线机器人客服
超过 85% 的春松客服企业客户通过 Chatopera 云服务上线机器人客服7x24 小时在线,接待访客,辅助人工坐席,提升 10 倍工作效率。Chatopera 机器人平台包括知识库、多轮对话、意图识别和语音识别等组件,标准化聊天机器人开发。
- [集成 Chatopera 云服务](https://docs.cskefu.com/docs/work-chatbot/bot-agent)
- [设定知识库、对话技能:欢迎语、按钮、图文消息等](https://docs.cskefu.com/docs/work-chatbot/message-types)
<details>
<summary>展开查看更多机器人客服介绍</summary>
<p>
<p align="center">
<b>应用场景示例</b><br>
<img src="https://github.com/cskefu/cskefu/raw/develop/public/assets/screenshot-20210908-184522.png" width="800">
</p>
支持企业 OA 智能问答、HR 智能问答、智能客服和网络营销等场景。企业 IT 部门、业务部门借助 Chatopera 云服务快速让聊天机器人上线!
上线机器人客服的两个方式1Chatopera 云服务按量付费提供每日免费额度2私有部署。
</p>
</details>
## 春松客服开源社区
### 合作开源客服系统,共赢未来
在春松客服开源社区,我们建立关系、发现认同、合作共赢!
- 了解春松客服采用的开源许可协议,参考[文档](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/)
- 如何提交反馈、文档,参考[文档](./CONTRIBUTING.md)
- 如何提交代码,参考[文档](https://docs.cskefu.com/docs/osc/contribution)
- 如何最新的春松客服开发进展:订阅[春松客服邮件列表](https://lists.cskefu.com/cgi-bin/mailman/listinfo/dev)
- 如何获得春松客服商业插件和服务,参考[文档](https://www.chatopera.com/price.html)
春松客服之所以开源,是基于这样一种信念:爱人也是爱己,利他也是利己。
因春松客服受益,而不回报开源社区的用户,我们不欢迎使用春松客服:我们开源并不是为了你们,你们是不被祝福的。
严重违反社区理念,通报及拉黑声明:[拉黑 @vicviz](https://www.cskefu.com/violation-announcement-2022-04-24/)
### 工单
遇到任何软件使用的问题,先在[工单历史记录](https://github.com/cskefu/cskefu/issues)中查询。
如果没有找到相似问题,使用下面的链接创建新的工单 -
- [Help: 开发环境搭建、功能咨询和使用问题等](https://github.com/cskefu/cskefu/issues/new?assignees=hailiang-wang&labels=help-wanted&template=1_help.md&title=Title%3A+%E7%94%A8%E4%B8%80%E5%8F%A5%E8%AF%9D%E9%99%88%E8%BF%B0%E4%BA%8B%E6%83%85%EF%BC%8C%E4%BF%9D%E8%AF%81%E8%A8%80%E7%AE%80%E6%84%8F%E8%B5%85%EF%BC%8C%E6%AF%94%E5%A6%82%E9%97%AE%E9%A2%98%E7%AE%80%E8%BF%B0%E5%8F%8A+root+cause+%E6%97%A5%E5%BF%97%E8%AF%AD%E5%8F%A5%EF%BC%8C%E6%9B%B4%E5%AE%B9%E6%98%93%E8%8E%B7%E5%BE%97%E5%B8%AE%E5%8A%A9)
- [Bug: 提交软件缺陷](https://github.com/cskefu/cskefu/issues/new?assignees=hailiang-wang&labels=bug&template=2_bug_report.md&title=Title%3A+%E7%94%A8%E4%B8%80%E5%8F%A5%E8%AF%9D%E9%99%88%E8%BF%B0%E4%BA%8B%E6%83%85%EF%BC%8C%E4%BF%9D%E8%AF%81%E8%A8%80%E7%AE%80%E6%84%8F%E8%B5%85%EF%BC%8C%E6%AF%94%E5%A6%82%E9%97%AE%E9%A2%98%E7%AE%80%E8%BF%B0%E5%8F%8A+root+cause+%E6%97%A5%E5%BF%97%E8%AF%AD%E5%8F%A5%EF%BC%8C%E6%9B%B4%E5%AE%B9%E6%98%93%E8%8E%B7%E5%BE%97%E5%B8%AE%E5%8A%A9)
- [Requirement: 描述新需求、反馈建议](https://github.com/cskefu/cskefu/issues/new?assignees=hailiang-wang&labels=requirement&template=3_requirement.md&title=Title%3A+%E7%94%A8%E4%B8%80%E5%8F%A5%E8%AF%9D%E9%99%88%E8%BF%B0%E4%BA%8B%E6%83%85%EF%BC%8C%E4%BF%9D%E8%AF%81%E8%A8%80%E7%AE%80%E6%84%8F%E8%B5%85%EF%BC%8C%E6%AF%94%E5%A6%82%E9%97%AE%E9%A2%98%E7%AE%80%E8%BF%B0%E5%8F%8A+root+cause+%E6%97%A5%E5%BF%97%E8%AF%AD%E5%8F%A5%EF%BC%8C%E6%9B%B4%E5%AE%B9%E6%98%93%E8%8E%B7%E5%BE%97%E5%B8%AE%E5%8A%A9)
- [Profiling: 瓶颈分析、性能优化建议和安全漏洞等](https://github.com/cskefu/cskefu/issues/new?assignees=hailiang-wang&labels=profiling&template=4_profiling.md&title=Title%3A+%E7%94%A8%E4%B8%80%E5%8F%A5%E8%AF%9D%E9%99%88%E8%BF%B0%E4%BA%8B%E6%83%85%EF%BC%8C%E4%BF%9D%E8%AF%81%E8%A8%80%E7%AE%80%E6%84%8F%E8%B5%85%EF%BC%8C%E6%AF%94%E5%A6%82%E9%97%AE%E9%A2%98%E7%AE%80%E8%BF%B0%E5%8F%8A+root+cause+%E6%97%A5%E5%BF%97%E8%AF%AD%E5%8F%A5%EF%BC%8C%E6%9B%B4%E5%AE%B9%E6%98%93%E8%8E%B7%E5%BE%97%E5%B8%AE%E5%8A%A9)
### 开发者文档
- 开发环境搭建
- [安装依赖和启动数据库等](https://docs.cskefu.com/docs/osc/engineering)
- [IDE 配置和使用之 IntelliJ IDEA](https://docs.cskefu.com/docs/osc/ide_intelij_idea)
- [IDE 配置和使用之 VSCode](https://docs.cskefu.com/docs/osc/ide_vscode)
- 定制开发技能
- [系统集成之 RestAPIs](https://docs.cskefu.com/docs/osc/restapi)
- [从零开始学习定制春松客服技能:春松客服大讲堂 PPT 课件及视频](https://github.com/cskefu/cskefu.djt)
- [掌握春松客服前端框架 Pugjs介绍及使用注意事项](https://blog.csdn.net/samurais/article/details/114576611)
- [提交代码](https://docs.cskefu.com/docs/osc/contribution)
## 微信
* 如以下图片无法浏览,可能是网络原因,请打开 [Gitee](https://gitee.com/cskefu/cskefu#%E5%BE%AE%E4%BF%A1) 查看二维码。
### 微信群
春松客服用户和开发者交流群。
![春松客服微信群](./public/assets/cskefu_opensource_community_wx_qr.jpg)
### 微信公众号
及时获得产品更新、活动分享等信息,关注春松客服公众号。
![春松客服公众号](./public/assets/cskefu-wechat-gzh.jpg)
## 鸣谢
[Amazon AWS 赞助春松客服服务器资源 5W RMB2021 年度)](https://aws.amazon.com)
[IBM Cloud 赞助春松客服服务器资源 12W US Dollar2019 年度)](https://cloud.ibm.com/)
[QingCloud 赞助春松客服服务器资源 1W RMB2018 年度)](https://www.qingcloud.com/)
## 开源许可协议
Copyright 2023 <a href="https://www.chatopera.com/" target="_blank">Beijing Huaxia Chunsong Technology Co., Ltd.</a>
[Chunsong Public License, version 1.0](https://docs.cskefu.com/licenses/v1.html)
![image](./public/assets/screenshot-20220323-163051.jpg)
<div align=right>
[主页](https://www.cskefu.com/) | [开源许可协议](https://docs.cskefu.com/licenses/v1.html) | [工单列表](https://github.com/cskefu/cskefu/issues) | [路线图](https://github.com/orgs/cskefu/projects/1)
</div>
# 春松客服
[![GitHub Stargazers](https://img.shields.io/github/stars/chatopera/cskefu.svg?style=social&label=Star&maxAge=2592000)](https://github.com/cskefu/cskefu/stargazers) [![GitHub Forks](https://img.shields.io/github/forks/chatopera/cskefu.svg?style=social&label=Fork&maxAge=2592000)](https://github.com/cskefu/cskefu/network/members) [![License](https://cdndownload2.chatopera.com/cskefu/licenses/chunsong1.0.svg)](https://www.cskefu.com/licenses/v1.html "开源许可协议") [![GitHub Issues](https://img.shields.io/github/issues/chatopera/cskefu.svg)](https://github.com/cskefu/cskefu/issues) [![GitHub Issues Closed](https://img.shields.io/github/issues-closed/chatopera/cskefu.svg)](https://github.com/cskefu/cskefu/issues?q=is%3Aissue+is%3Aclosed) [![docker](https://img.shields.io/docker/pulls/chatopera/contact-center.svg "Docker Pulls")](https://hub.docker.com/r/chatopera/contact-center/) <!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
[![All Contributors](https://img.shields.io/badge/all_contributors-34-orange.svg?style=flat-square)](#contributors-)
<!-- ALL-CONTRIBUTORS-BADGE:END -->
[https://www.cskefu.com](https://www.cskefu.com/)
| 版本 | 文档中心 | Git 分支 | 状态 |
| --- | --- | --- | --- |
| v8.x | [v8](https://docs.cskefu.com/docs/) | [GitHub](https://github.com/cskefu/cskefu/tree/develop) \| [Gitee](https://gitee.com/cskefu/cskefu/tree/develop/) | Active, 维护中 |
| v7.x | [v7](https://docs.cskefu.com/docs/v7/) | [GitHub](https://github.com/cskefu/cskefu/tree/v7) \| [Gitee](https://gitee.com/cskefu/cskefu/tree/v7/) | Sunset, 维护终止 |
:hearts: 春松客服的愿景:
- 公元 2032 年内1000 万企业上线开源客服系统
:innocent: 春松客服的承诺:
- 坚持基础功能开源,不发布垃圾
- 坚持持续优化
- 坚持商业友好授权
春松客服宣言视频: [Bilibili](https://www.bilibili.com/video/BV1hu411o76r/) | [YouTube](https://youtu.be/ILf3BWpq4Ns)
新版本介绍:[观看春松客服 v8 新版本发布会 @ 2023-07-01](https://www.cskefu.com/2023/07/03/community-conf/)
## 开发者列表 ✨
: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)。
<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
<!-- prettier-ignore-start -->
<!-- markdownlint-disable -->
<table>
<tbody>
<tr>
<td align="center" valign="top" width="11.11%"><a href="https://github.com/mukaiu"><img src="https://avatars.githubusercontent.com/u/7746790?v=4?s=50" width="50px;" alt="Mukaiu"/><br /><sub><b>Mukaiu</b></sub></a><br /><a href="https://github.com/cskefu/cskefu/commits?author=mukaiu" title="Code">💻</a> <a href="#infra-mukaiu" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td>
<td align="center" valign="top" width="11.11%"><a href="https://www.linkedin.com/in/hai-liang-wang/"><img src="https://avatars.githubusercontent.com/u/3538629?v=4?s=50" width="50px;" alt="Hai Liang W."/><br /><sub><b>Hai Liang W.</b></sub></a><br /><a href="#plugin-hailiang-wang" title="Plugin/utility libraries">🔌</a> <a href="#financial-hailiang-wang" title="Financial">💵</a></td>
<td align="center" valign="top" width="11.11%"><a href="https://github.com/shih945"><img src="https://avatars.githubusercontent.com/u/29646781?v=4?s=50" width="50px;" alt="SHIH"/><br /><sub><b>SHIH</b></sub></a><br /><a href="https://github.com/cskefu/cskefu/commits?author=shih945" title="Code">💻</a></td>
<td align="center" valign="top" width="11.11%"><a href="https://github.com/luruiGit"><img src="https://avatars.githubusercontent.com/u/49265205?v=4?s=50" width="50px;" alt="luruiGit"/><br /><sub><b>luruiGit</b></sub></a><br /><a href="https://github.com/cskefu/cskefu/commits?author=luruiGit" title="Code">💻</a></td>
<td align="center" valign="top" width="11.11%"><a href="http://enze5088.github.io"><img src="https://avatars.githubusercontent.com/u/14285786?v=4?s=50" width="50px;" alt="Enze"/><br /><sub><b>Enze</b></sub></a><br /><a href="https://github.com/cskefu/cskefu/commits?author=enze5088" title="Code">💻</a></td>
<td align="center" valign="top" width="11.11%"><a href="https://blog.dengchao.fun"><img src="https://avatars.githubusercontent.com/u/16363180?v=4?s=50" width="50px;" alt="邓超"/><br /><sub><b>邓超</b></sub></a><br /><a href="https://github.com/cskefu/cskefu/commits?author=DevDengChao" title="Code">💻</a></td>
<td align="center" valign="top" width="11.11%"><a href="https://github.com/Happy5"><img src="https://avatars.githubusercontent.com/u/53087368?v=4?s=50" width="50px;" alt="Happy5"/><br /><sub><b>Happy5</b></sub></a><br /><a href="#ideas-Happy5" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center" valign="top" width="11.11%"><a href="https://www.csdn.net"><img src="https://avatars.githubusercontent.com/u/3679798?v=4?s=50" width="50px;" alt="kyle"/><br /><sub><b>kyle</b></sub></a><br /><a href="https://github.com/cskefu/cskefu/commits?author=kylezhang" title="Code">💻</a> <a href="#talk-kylezhang" title="Talks">📢</a></td>
<td align="center" valign="top" width="11.11%"><a href="https://github.com/xianliwang"><img src="https://avatars.githubusercontent.com/u/52594347?v=4?s=50" width="50px;" alt="xianliwang"/><br /><sub><b>xianliwang</b></sub></a><br /><a href="#video-xianliwang" title="Videos">📹</a> <a href="https://github.com/cskefu/cskefu/commits?author=xianliwang" title="Tests">⚠️</a></td>
</tr>
<tr>
<td align="center" valign="top" width="11.11%"><a href="https://github.com/lihang2016"><img src="https://avatars.githubusercontent.com/u/23203931?v=4?s=50" width="50px;" alt="lihang2016"/><br /><sub><b>lihang2016</b></sub></a><br /><a href="#ideas-lihang2016" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center" valign="top" width="11.11%"><a href="https://github.com/live-in-the-moment"><img src="https://avatars.githubusercontent.com/u/62800943?v=4?s=50" width="50px;" alt="live-in-the-moment"/><br /><sub><b>live-in-the-moment</b></sub></a><br /><a href="#ideas-live-in-the-moment" title="Ideas, Planning, & Feedback">🤔</a> <a href="https://github.com/cskefu/cskefu/issues?q=author%3Alive-in-the-moment" title="Bug reports">🐛</a> <a href="https://github.com/cskefu/cskefu/commits?author=live-in-the-moment" title="Tests">⚠️</a></td>
<td align="center" valign="top" width="11.11%"><a href="https://github.com/ArioWei"><img src="https://avatars.githubusercontent.com/u/41034256?v=4?s=50" width="50px;" alt="ArioWei"/><br /><sub><b>ArioWei</b></sub></a><br /><a href="https://github.com/cskefu/cskefu/commits?author=ArioWei" title="Tests">⚠️</a></td>
<td align="center" valign="top" width="11.11%"><a href="http://www.youkefu.cn"><img src="https://avatars.githubusercontent.com/u/48078408?v=4?s=50" width="50px;" alt="优客服"/><br /><sub><b>优客服</b></sub></a><br /><a href="https://github.com/cskefu/cskefu/commits?author=youkefu" title="Code">💻</a> <a href="https://github.com/cskefu/cskefu/commits?author=youkefu" title="Tests">⚠️</a> <a href="#business-youkefu" title="Business development">💼</a> <a href="#design-youkefu" title="Design">🎨</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 /><a href="#ideas-lecjy" title="Ideas, Planning, & Feedback">🤔</a> <a href="#talk-lecjy" title="Talks">📢</a></td>
<td align="center" valign="top" width="11.11%"><a href="https://github.com/xl111"><img src="https://avatars.githubusercontent.com/u/64338718?v=4?s=50" width="50px;" alt="徐。。"/><br /><sub><b>徐。。</b></sub></a><br /><a href="https://github.com/cskefu/cskefu/commits?author=xl111" title="Code">💻</a></td>
<td align="center" valign="top" width="11.11%"><a href="https://github.com/viaco2ove"><img src="https://avatars.githubusercontent.com/u/8044837?v=4?s=50" width="50px;" alt="viaco2ove"/><br /><sub><b>viaco2ove</b></sub></a><br /><a href="https://github.com/cskefu/cskefu/commits?author=viaco2ove" title="Code">💻</a></td>
<td align="center" valign="top" width="11.11%"><a href="https://github.com/understanding"><img src="https://avatars.githubusercontent.com/u/2801277?v=4?s=50" width="50px;" alt="understanding"/><br /><sub><b>understanding</b></sub></a><br /><a href="https://github.com/cskefu/cskefu/commits?author=understanding" title="Tests">⚠️</a></td>
<td align="center" valign="top" width="11.11%"><a href="https://github.com/MQPearth"><img src="https://avatars.githubusercontent.com/u/32632796?v=4?s=50" width="50px;" alt="MQPearth"/><br /><sub><b>MQPearth</b></sub></a><br /><a href="https://github.com/cskefu/cskefu/commits?author=MQPearth" title="Tests">⚠️</a></td>
</tr>
<tr>
<td align="center" valign="top" width="11.11%"><a href="https://github.com/SkorpiosL"><img src="https://avatars.githubusercontent.com/u/32902343?v=4?s=50" width="50px;" alt="SkorpiosL"/><br /><sub><b>SkorpiosL</b></sub></a><br /><a href="https://github.com/cskefu/cskefu/commits?author=SkorpiosL" title="Tests">⚠️</a></td>
<td align="center" valign="top" width="11.11%"><a href="https://github.com/always-China"><img src="https://avatars.githubusercontent.com/u/49581101?v=4?s=50" width="50px;" alt="hua"/><br /><sub><b>hua</b></sub></a><br /><a href="https://github.com/cskefu/cskefu/commits?author=always-China" title="Code">💻</a></td>
<td align="center" valign="top" width="11.11%"><a href="https://github.com/wq11123"><img src="https://avatars.githubusercontent.com/u/40993206?v=4?s=50" width="50px;" alt="wq11123"/><br /><sub><b>wq11123</b></sub></a><br /><a href="https://github.com/cskefu/cskefu/commits?author=wq11123" title="Tests">⚠️</a> <a href="#video-wq11123" title="Videos">📹</a> <a href="#ideas-wq11123" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center" valign="top" width="11.11%"><a href="https://github.com/MouMouQQ"><img src="https://avatars.githubusercontent.com/u/101631131?v=4?s=50" width="50px;" alt="MouMouQQ"/><br /><sub><b>MouMouQQ</b></sub></a><br /><a href="#ideas-MouMouQQ" title="Ideas, Planning, & Feedback">🤔</a> <a href="https://github.com/cskefu/cskefu/commits?author=MouMouQQ" title="Tests">⚠️</a></td>
<td align="center" valign="top" width="11.11%"><a href="https://github.com/tigerun"><img src="https://avatars.githubusercontent.com/u/17540364?v=4?s=50" width="50px;" alt="Tigerun"/><br /><sub><b>Tigerun</b></sub></a><br /><a href="#ideas-tigerun" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center" valign="top" width="11.11%"><a href="https://github.com/yangbailiang"><img src="https://avatars.githubusercontent.com/u/50096675?v=4?s=50" width="50px;" alt="yangbailiang"/><br /><sub><b>yangbailiang</b></sub></a><br /><a href="https://github.com/cskefu/cskefu/issues?q=author%3Ayangbailiang" title="Bug reports">🐛</a> <a href="https://github.com/cskefu/cskefu/commits?author=yangbailiang" title="Tests">⚠️</a></td>
<td align="center" valign="top" width="11.11%"><a href="https://github.com/lokywang"><img src="https://avatars.githubusercontent.com/u/28672424?v=4?s=50" width="50px;" alt="lokywang"/><br /><sub><b>lokywang</b></sub></a><br /><a href="#ideas-lokywang" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center" valign="top" width="11.11%"><a href="https://github.com/jichoucc"><img src="https://avatars.githubusercontent.com/u/87190214?v=4?s=50" width="50px;" alt="jichoucc"/><br /><sub><b>jichoucc</b></sub></a><br /><a href="https://github.com/cskefu/cskefu/issues?q=author%3Ajichoucc" title="Bug reports">🐛</a> <a href="https://github.com/cskefu/cskefu/commits?author=jichoucc" title="Tests">⚠️</a></td>
<td align="center" valign="top" width="11.11%"><a href="https://github.com/wuyongyin"><img src="https://avatars.githubusercontent.com/u/20410234?v=4?s=50" width="50px;" alt="wuyongyin"/><br /><sub><b>wuyongyin</b></sub></a><br /><a href="#ideas-wuyongyin" title="Ideas, Planning, & Feedback">🤔</a></td>
</tr>
<tr>
<td align="center" valign="top" width="11.11%"><a href="https://github.com/wangdayan"><img src="https://avatars.githubusercontent.com/u/62323175?v=4?s=50" width="50px;" alt="Claire"/><br /><sub><b>Claire</b></sub></a><br /><a href="https://github.com/cskefu/cskefu/commits?author=wangdayan" title="Tests">⚠️</a></td>
<td align="center" valign="top" width="11.11%"><a href="https://github.com/zc1813400107"><img src="https://avatars.githubusercontent.com/u/46372405?v=4?s=50" width="50px;" alt="super"/><br /><sub><b>super</b></sub></a><br /><a href="https://github.com/cskefu/cskefu/commits?author=zc1813400107" title="Code">💻</a> <a href="https://github.com/cskefu/cskefu/commits?author=zc1813400107" title="Documentation">📖</a></td>
<td align="center" valign="top" width="11.11%"><a href="https://github.com/xiaobo9"><img src="https://avatars.githubusercontent.com/u/1284376?v=4?s=50" width="50px;" alt="xiaobo9"/><br /><sub><b>xiaobo9</b></sub></a><br /><a href="https://github.com/cskefu/cskefu/commits?author=xiaobo9" title="Code">💻</a></td>
<td align="center" valign="top" width="11.11%"><a href="https://github.com/zhangchanglong"><img src="https://avatars.githubusercontent.com/u/3481828?v=4?s=50" width="50px;" alt="zhangchanglong"/><br /><sub><b>zhangchanglong</b></sub></a><br /><a href="#eventOrganizing-zhangchanglong" title="Event Organizing">📋</a></td>
<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 -->
<!-- ALL-CONTRIBUTORS-LIST:END -->
## 功能介绍
<!-- <img src="./public/assets/cskefu-2021-08-22-8.22.09PM.jpg" width="900"> -->
春松客服提供的开源代码,即[CSKeFu](https://github.com/cskefu/cskefu),包含多个开箱即用的模块:
- 账号及组织机构管理:按组织、角色分配账号权限
- 坐席监控:设置坐席监控角色的人员可以看到并干预访客会话
- 联系人和客户管理CRM 模块,管理联系人和客户,细粒度维护客户信息,自定义标签和打标签,记录来往历史等
- 网页渠道组件:一分钟接入对话窗口,支持技能组、邀请和关联联系人等
- Facebook 渠道组件:快速接入 [Facebook Messenger](https://www.messenger.com/) 渠道,通过 Messenger 支持 Facebook 粉丝页、[Shopify](https://www.shopify.com/) 等海外社交、电商平台
- 坐席工作台:汇聚多渠道访客请求,坐席根据策略自动分配,自动弹屏,转接等
- 机器人客服:与[Chatopera 云服务](/products/chatbot-platform/index.html)集成
- 企业聊天:支持企业员工在春松客服系统中群聊和私聊
- 质检:历史会话、服务小结、服务反馈及相关报表
了解功能详细介绍,参考[文档中心](https://docs.cskefu.com/)。
## 产品演示
<p align="center">
<b>欢迎页</b><br>
<img src="./public/assets/cskefu-screen-1.jpg" width="900">
</p>
<details>
<summary>展开查看更多产品截图</summary>
<p>
<p align="center">
<b>坐席工作台</b><br>
<img src="./public/assets/44915582-eb8d2c80-ad65-11e8-8876-86c8b5bb5cc7.png" width="900">
</p>
<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>
<p align="center">
<b>客服机器人应答</b><br>
<img src="./public/assets/51080567-50479300-1719-11e9-85d8-d209370c9d10.png" width="900">
</p>
</p>
</details>
## 快速开始
### 春松客服用户使用指南
- 快速的了解和介绍春松客服
- 快速的查找和春松客服相关的材料
下载[《春松客服用户使用指南》](https://www.cskefu.com/moment/825.html/)。
### 安装部署
支持云原生环境,容器化一键部署,现在就使用春松客服!参考[《私有部署文档》](http://docs.cskefu.com/docs/deploy)。
### 系统初始化
部署后,进行系统初始化,为组织设定部门、权限、账号等,参考[《系统初始化文档》](https://docs.cskefu.com/docs/initialization)。
### 运维
备份、升级、回滚等运维工作,参考[《系统维护文档》](https://docs.cskefu.com/docs/osc/maintainence)。
### 运营使用指南
关于产品的具体使用说明,请参考[《春松客服文档》](https://docs.cskefu.com)。
### 立即上线机器人客服
超过 85% 的春松客服企业客户通过 Chatopera 云服务上线机器人客服7x24 小时在线,接待访客,辅助人工坐席,提升 10 倍工作效率。Chatopera 机器人平台包括知识库、多轮对话、意图识别和语音识别等组件,标准化聊天机器人开发。
- [集成 Chatopera 云服务](https://docs.cskefu.com/docs/work-chatbot/bot-agent)
- [设定知识库、对话技能:欢迎语、按钮、图文消息等](https://docs.cskefu.com/docs/work-chatbot/message-types)
<details>
<summary>展开查看更多机器人客服介绍</summary>
<p>
<p align="center">
<b>应用场景示例</b><br>
<img src="https://github.com/cskefu/cskefu/raw/develop/public/assets/screenshot-20210908-184522.png" width="800">
</p>
支持企业 OA 智能问答、HR 智能问答、智能客服和网络营销等场景。企业 IT 部门、业务部门借助 Chatopera 云服务快速让聊天机器人上线!
上线机器人客服的两个方式1Chatopera 云服务按量付费提供每日免费额度2私有部署。
</p>
</details>
## 春松客服开源社区
### 合作开源客服系统,共赢未来
在春松客服开源社区,我们建立关系、发现认同、合作共赢!
- 了解春松客服采用的开源许可协议,参考[文档](https://www.cskefu.com/2023/06/25/chunsong-public-license-1-0/)
- 了解春松客服的开发计划,参考[文档](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)
春松客服之所以开源,是基于这样一种信念:爱人也是爱己,利他也是利己。
因春松客服受益,而不回报开源社区的用户,我们不欢迎使用春松客服:我们开源并不是为了你们,你们是不被祝福的。
严重违反社区理念,通报及拉黑声明:[拉黑 @vicviz](https://www.cskefu.com/violation-announcement-2022-04-24/)
### 工单
遇到任何软件使用的问题,先在[工单历史记录](https://github.com/cskefu/cskefu/issues)中查询。
如果没有找到相似问题,使用下面的链接创建新的工单 -
- [Help: 开发环境搭建、功能咨询和使用问题等](https://github.com/cskefu/cskefu/issues/new?assignees=hailiang-wang&labels=help-wanted&template=1_help.md&title=Title%3A+%E7%94%A8%E4%B8%80%E5%8F%A5%E8%AF%9D%E9%99%88%E8%BF%B0%E4%BA%8B%E6%83%85%EF%BC%8C%E4%BF%9D%E8%AF%81%E8%A8%80%E7%AE%80%E6%84%8F%E8%B5%85%EF%BC%8C%E6%AF%94%E5%A6%82%E9%97%AE%E9%A2%98%E7%AE%80%E8%BF%B0%E5%8F%8A+root+cause+%E6%97%A5%E5%BF%97%E8%AF%AD%E5%8F%A5%EF%BC%8C%E6%9B%B4%E5%AE%B9%E6%98%93%E8%8E%B7%E5%BE%97%E5%B8%AE%E5%8A%A9)
- [Bug: 提交软件缺陷](https://github.com/cskefu/cskefu/issues/new?assignees=hailiang-wang&labels=bug&template=2_bug_report.md&title=Title%3A+%E7%94%A8%E4%B8%80%E5%8F%A5%E8%AF%9D%E9%99%88%E8%BF%B0%E4%BA%8B%E6%83%85%EF%BC%8C%E4%BF%9D%E8%AF%81%E8%A8%80%E7%AE%80%E6%84%8F%E8%B5%85%EF%BC%8C%E6%AF%94%E5%A6%82%E9%97%AE%E9%A2%98%E7%AE%80%E8%BF%B0%E5%8F%8A+root+cause+%E6%97%A5%E5%BF%97%E8%AF%AD%E5%8F%A5%EF%BC%8C%E6%9B%B4%E5%AE%B9%E6%98%93%E8%8E%B7%E5%BE%97%E5%B8%AE%E5%8A%A9)
- [Requirement: 描述新需求、反馈建议](https://github.com/cskefu/cskefu/issues/new?assignees=hailiang-wang&labels=requirement&template=3_requirement.md&title=Title%3A+%E7%94%A8%E4%B8%80%E5%8F%A5%E8%AF%9D%E9%99%88%E8%BF%B0%E4%BA%8B%E6%83%85%EF%BC%8C%E4%BF%9D%E8%AF%81%E8%A8%80%E7%AE%80%E6%84%8F%E8%B5%85%EF%BC%8C%E6%AF%94%E5%A6%82%E9%97%AE%E9%A2%98%E7%AE%80%E8%BF%B0%E5%8F%8A+root+cause+%E6%97%A5%E5%BF%97%E8%AF%AD%E5%8F%A5%EF%BC%8C%E6%9B%B4%E5%AE%B9%E6%98%93%E8%8E%B7%E5%BE%97%E5%B8%AE%E5%8A%A9)
- [Profiling: 瓶颈分析、性能优化建议和安全漏洞等](https://github.com/cskefu/cskefu/issues/new?assignees=hailiang-wang&labels=profiling&template=4_profiling.md&title=Title%3A+%E7%94%A8%E4%B8%80%E5%8F%A5%E8%AF%9D%E9%99%88%E8%BF%B0%E4%BA%8B%E6%83%85%EF%BC%8C%E4%BF%9D%E8%AF%81%E8%A8%80%E7%AE%80%E6%84%8F%E8%B5%85%EF%BC%8C%E6%AF%94%E5%A6%82%E9%97%AE%E9%A2%98%E7%AE%80%E8%BF%B0%E5%8F%8A+root+cause+%E6%97%A5%E5%BF%97%E8%AF%AD%E5%8F%A5%EF%BC%8C%E6%9B%B4%E5%AE%B9%E6%98%93%E8%8E%B7%E5%BE%97%E5%B8%AE%E5%8A%A9)
### 开发者文档
- 开发环境搭建
- [安装依赖和启动数据库等](https://docs.cskefu.com/docs/osc/engineering)
- [IDE 配置和使用之 IntelliJ IDEA](https://docs.cskefu.com/docs/osc/ide_intelij_idea)
- [IDE 配置和使用之 VSCode](https://docs.cskefu.com/docs/osc/ide_vscode)
- 定制开发技能
- [系统集成之 RestAPIs](https://docs.cskefu.com/docs/osc/restapi)
- [从零开始学习定制春松客服技能:春松客服大讲堂 PPT 课件及视频](https://github.com/cskefu/cskefu.djt)
- [掌握春松客服前端框架 Pugjs介绍及使用注意事项](https://blog.csdn.net/samurais/article/details/114576611)
- [提交代码](https://docs.cskefu.com/docs/osc/contribution)
## 微信
* 如以下图片无法浏览,可能是网络原因,请打开 [Gitee](https://gitee.com/cskefu/cskefu#%E5%BE%AE%E4%BF%A1) 查看二维码。
### 微信群
春松客服用户和开发者交流群。
![春松客服微信群](./public/assets/cskefu_opensource_community_wx_qr.jpg)
### 微信公众号
及时获得产品更新、活动分享等信息,关注春松客服公众号。
![春松客服公众号](./public/assets/cskefu-wechat-gzh.jpg)
## 鸣谢
[Amazon AWS 赞助春松客服服务器资源 5W RMB2021 年度)](https://aws.amazon.com)
[IBM Cloud 赞助春松客服服务器资源 12W US Dollar2019 年度)](https://cloud.ibm.com/)
[QingCloud 赞助春松客服服务器资源 1W RMB2018 年度)](https://www.qingcloud.com/)
## 开源许可协议
Copyright 2023 <a href="https://www.chatopera.com/" target="_blank">Beijing Huaxia Chunsong Technology Co., Ltd.</a>
[Chunsong Public License, version 1.0](https://docs.cskefu.com/licenses/v1.html)
![image](./public/assets/screenshot-20220323-163051.jpg)

View File

@ -1,8 +0,0 @@
app/target
!app/target/*.war.original
!app/target/*.war
!app/target/*.jar.original
!app/target/*.jar
logs/
tmp/
data/

View File

@ -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/

View File

@ -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"]

View File

@ -1,7 +0,0 @@
# Chatopera Contact Center
前三代呼叫中心均是以电话为主要的服务渠道。在 2000 年伴随着互联网以及移动通信的发展与普及将电子邮件、互联网、手机短信等渠道接入呼叫中心成为第四代呼叫中心的标志。第四代呼叫中心也称为多媒体呼叫中心或联络中心Contact Center。它相对传统呼叫中心来说接入渠道丰富同时引入了多渠道接入与多渠道统一排队等概念。
## 文档
<https://docs.chatopera.com/>

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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/

View File

@ -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>

View File

@ -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();
}
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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) {
}
}

View File

@ -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);
}
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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);
}

View File

@ -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);
}
}

View File

@ -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();
}
}

View File

@ -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;
}
}

View File

@ -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();
}
}
}

View File

@ -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();
}
}
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}
}

View File

@ -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());
}
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}
}

View File

@ -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());
}
}

View File

@ -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 {
}
}

View File

@ -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());
}
}

View File

@ -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);
}
}
}
}

View File

@ -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);
}
}
}
}

View File

@ -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";
}

View File

@ -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 {
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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));
}
}

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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;
}
}
}
}

View File

@ -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;
}
}

View File

@ -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"))) ;
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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();
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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.");
});
}
}
}

View File

@ -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;
}
}

View File

@ -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
}
}

View File

@ -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("/**");
}
}

View File

@ -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 {
}
}

View File

@ -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;
}
}

View File

@ -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();
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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();
}
}

View File

@ -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));
}
}

View File

@ -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());
}
}
}

View File

@ -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);
}
};
}
}

View File

@ -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);
}
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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