初始化

master
lecjy 2023-11-14 21:20:14 +08:00
commit 75582c2d4c
2311 changed files with 606105 additions and 0 deletions

354
.all-contributorsrc Normal file
View File

@ -0,0 +1,354 @@
{
"files": [
"README.md",
"CONTRIBUTING.md"
],
"imageSize": 50,
"contributorsPerLine": 9,
"commit": false,
"contributors": [
{
"login": "mukaiu",
"name": "Mukaiu",
"avatar_url": "https://avatars.githubusercontent.com/u/7746790?v=4",
"profile": "https://github.com/mukaiu",
"contributions": [
"code",
"infra"
]
},
{
"login": "hailiang-wang",
"name": "Hai Liang W.",
"avatar_url": "https://avatars.githubusercontent.com/u/3538629?v=4",
"profile": "https://www.linkedin.com/in/hai-liang-wang/",
"contributions": [
"plugin",
"financial"
]
},
{
"login": "shih945",
"name": "SHIH",
"avatar_url": "https://avatars.githubusercontent.com/u/29646781?v=4",
"profile": "https://github.com/shih945",
"contributions": [
"code"
]
},
{
"login": "luruiGit",
"name": "luruiGit",
"avatar_url": "https://avatars.githubusercontent.com/u/49265205?v=4",
"profile": "https://github.com/luruiGit",
"contributions": [
"code"
]
},
{
"login": "enze5088",
"name": "Enze",
"avatar_url": "https://avatars.githubusercontent.com/u/14285786?v=4",
"profile": "http://enze5088.github.io",
"contributions": [
"code"
]
},
{
"login": "DevDengChao",
"name": "邓超",
"avatar_url": "https://avatars.githubusercontent.com/u/16363180?v=4",
"profile": "https://blog.dengchao.fun",
"contributions": [
"code"
]
},
{
"login": "Happy5",
"name": "Happy5",
"avatar_url": "https://avatars.githubusercontent.com/u/53087368?v=4",
"profile": "https://github.com/Happy5",
"contributions": [
"ideas"
]
},
{
"login": "kylezhang",
"name": "kyle",
"avatar_url": "https://avatars.githubusercontent.com/u/3679798?v=4",
"profile": "https://www.csdn.net",
"contributions": [
"code",
"talk"
]
},
{
"login": "xianliwang",
"name": "xianliwang",
"avatar_url": "https://avatars.githubusercontent.com/u/52594347?v=4",
"profile": "https://github.com/xianliwang",
"contributions": [
"video",
"test"
]
},
{
"login": "lihang2016",
"name": "lihang2016",
"avatar_url": "https://avatars.githubusercontent.com/u/23203931?v=4",
"profile": "https://github.com/lihang2016",
"contributions": [
"ideas"
]
},
{
"login": "live-in-the-moment",
"name": "live-in-the-moment",
"avatar_url": "https://avatars.githubusercontent.com/u/62800943?v=4",
"profile": "https://github.com/live-in-the-moment",
"contributions": [
"ideas",
"bug",
"test"
]
},
{
"login": "ArioWei",
"name": "ArioWei",
"avatar_url": "https://avatars.githubusercontent.com/u/41034256?v=4",
"profile": "https://github.com/ArioWei",
"contributions": [
"test"
]
},
{
"login": "youkefu",
"name": "优客服",
"avatar_url": "https://avatars.githubusercontent.com/u/48078408?v=4",
"profile": "http://www.youkefu.cn",
"contributions": [
"code",
"test",
"business",
"design"
]
},
{
"login": "lecjy",
"name": "lecjy",
"avatar_url": "https://avatars.githubusercontent.com/u/9280760?v=4",
"profile": "https://github.com/lecjy",
"contributions": [
"ideas",
"talk",
"mentoring",
"maintenance",
"code"
]
},
{
"login": "xl111",
"name": "徐。。",
"avatar_url": "https://avatars.githubusercontent.com/u/64338718?v=4",
"profile": "https://github.com/xl111",
"contributions": [
"code"
]
},
{
"login": "viaco2ove",
"name": "viaco2ove",
"avatar_url": "https://avatars.githubusercontent.com/u/8044837?v=4",
"profile": "https://github.com/viaco2ove",
"contributions": [
"code"
]
},
{
"login": "understanding",
"name": "understanding",
"avatar_url": "https://avatars.githubusercontent.com/u/2801277?v=4",
"profile": "https://github.com/understanding",
"contributions": [
"test"
]
},
{
"login": "MQPearth",
"name": "MQPearth",
"avatar_url": "https://avatars.githubusercontent.com/u/32632796?v=4",
"profile": "https://github.com/MQPearth",
"contributions": [
"test"
]
},
{
"login": "SkorpiosL",
"name": "SkorpiosL",
"avatar_url": "https://avatars.githubusercontent.com/u/32902343?v=4",
"profile": "https://github.com/SkorpiosL",
"contributions": [
"test"
]
},
{
"login": "always-China",
"name": "hua",
"avatar_url": "https://avatars.githubusercontent.com/u/49581101?v=4",
"profile": "https://github.com/always-China",
"contributions": [
"code"
]
},
{
"login": "wq11123",
"name": "wq11123",
"avatar_url": "https://avatars.githubusercontent.com/u/40993206?v=4",
"profile": "https://github.com/wq11123",
"contributions": [
"test",
"video",
"ideas"
]
},
{
"login": "MouMouQQ",
"name": "MouMouQQ",
"avatar_url": "https://avatars.githubusercontent.com/u/101631131?v=4",
"profile": "https://github.com/MouMouQQ",
"contributions": [
"ideas",
"test"
]
},
{
"login": "tigerun",
"name": "Tigerun",
"avatar_url": "https://avatars.githubusercontent.com/u/17540364?v=4",
"profile": "https://github.com/tigerun",
"contributions": [
"ideas"
]
},
{
"login": "yangbailiang",
"name": "yangbailiang",
"avatar_url": "https://avatars.githubusercontent.com/u/50096675?v=4",
"profile": "https://github.com/yangbailiang",
"contributions": [
"bug",
"test"
]
},
{
"login": "lokywang",
"name": "lokywang",
"avatar_url": "https://avatars.githubusercontent.com/u/28672424?v=4",
"profile": "https://github.com/lokywang",
"contributions": [
"ideas"
]
},
{
"login": "jichoucc",
"name": "jichoucc",
"avatar_url": "https://avatars.githubusercontent.com/u/87190214?v=4",
"profile": "https://github.com/jichoucc",
"contributions": [
"bug",
"test"
]
},
{
"login": "wuyongyin",
"name": "wuyongyin",
"avatar_url": "https://avatars.githubusercontent.com/u/20410234?v=4",
"profile": "https://github.com/wuyongyin",
"contributions": [
"ideas"
]
},
{
"login": "wangdayan",
"name": "Claire",
"avatar_url": "https://avatars.githubusercontent.com/u/62323175?v=4",
"profile": "https://github.com/wangdayan",
"contributions": [
"test"
]
},
{
"login": "zc1813400107",
"name": "super",
"avatar_url": "https://avatars.githubusercontent.com/u/46372405?v=4",
"profile": "https://github.com/zc1813400107",
"contributions": [
"code",
"doc"
]
},
{
"login": "xiaobo9",
"name": "xiaobo9",
"avatar_url": "https://avatars.githubusercontent.com/u/1284376?v=4",
"profile": "https://github.com/xiaobo9",
"contributions": [
"code"
]
},
{
"login": "zhangchanglong",
"name": "zhangchanglong",
"avatar_url": "https://avatars.githubusercontent.com/u/3481828?v=4",
"profile": "https://github.com/zhangchanglong",
"contributions": [
"eventOrganizing"
]
},
{
"login": "SAMZONG",
"name": "Samzong Lu",
"avatar_url": "https://avatars.githubusercontent.com/u/13782141?v=4",
"profile": "https://samzong.me",
"contributions": [
"eventOrganizing",
"projectManagement",
"design"
]
},
{
"login": "halfray",
"name": "halfray",
"avatar_url": "https://avatars.githubusercontent.com/u/8181982?v=4",
"profile": "https://github.com/halfray",
"contributions": [
"bug"
]
},
{
"login": "kely33",
"name": "kely33",
"avatar_url": "https://avatars.githubusercontent.com/u/134681303?v=4",
"profile": "https://github.com/kely33",
"contributions": [
"bug"
]
},
{
"login": "zjpzjp",
"name": "websir",
"avatar_url": "https://avatars.githubusercontent.com/u/11382248?v=4",
"profile": "https://github.com/zjpzjp",
"contributions": [
"code"
]
}
],
"projectName": "cskefu",
"projectOwner": "cskefu",
"repoType": "github",
"repoHost": "https://github.com",
"skipCi": true,
"commitConvention": "angular",
"commitType": "docs"
}

42
.circleci/config.yml Normal file
View File

@ -0,0 +1,42 @@
# Use the latest 2.1 version of CircleCI pipeline process engine.
# See: https://circleci.com/docs/configuration-reference
version: 2.1
# Define a job to be invoked later in a workflow.
# See: https://circleci.com/docs/configuration-reference/#jobs
jobs:
# Below is the definition of your job to build and test your app, you can rename and customize it as you want.
package-build-push:
# These next lines define a Docker executor: https://circleci.com/docs/executor-types/
# You can specify an image from Dockerhub or use one of our Convenience Images from CircleCI's Developer Hub.
# 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
# Add steps to the job
# See: https://circleci.com/docs/configuration-reference/#steps
steps:
# Checkout the code as the first step.
- checkout
- setup_remote_docker
- run:
name: Login DockerHub
command: |
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
# Invoke jobs via workflows
# See: https://circleci.com/docs/configuration-reference/#workflows
workflows:
dockerize: # This is the name of the workflow, feel free to change it to better match your workflow.
# Inside the workflow, you define the jobs you want to run.
jobs:
- package-build-push:
filters:
branches:
only: develop

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

@ -0,0 +1,41 @@
---
reviewers : cskefu/reviewers
---
<!--- 在标题中简略说明问题 -->
## 描述
<!--- 详细的描述变更 -->
### 关联 Issue #
## 解决的问题
<!--- 为什么变更是必要的? -->
<!--- 如果这个PR解决了其他Issue添加链接 -->
## 测试情况
<!--- 详细介绍怎么测试变更了 -->
<!--- 介绍测试环境 -->
<!--- 变更对其他代码的影响 -->
## 截屏
## 变更的类型
<!--- 变更有哪些特点,添加 `x` 到下面的对应项目中: -->
- [ ] 解决 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 #

18
.github/CODEOWNERS vendored Normal file
View File

@ -0,0 +1,18 @@
# https://github.com/cskefu/cskefu/issues/758
# defaults
* @lecjy
# Order is important; the last matching pattern takes the most
# precedence. When someone opens a pull request that only
# modifies JS files, only @js-owner and not the global
# owner(s) will be requested for a review.
*.js @lecjy
*.ts @lecjy
*.pug @lecjy
*.java @lecjy
*.sql @lecjy
pom.xml @hailiang-wang
docs/* @cskefu/reviewers
README* @cskefu/reviewers

19
.github/workflows/compile.yml vendored Normal file
View File

@ -0,0 +1,19 @@
name: Compile Checks on PRs
on:
pull_request:
branches: [master, develop]
types: [opened, synchronize, reopened]
jobs:
mvn-compile:
runs-on: [self-hosted]
steps:
- uses: actions/checkout@v3
with:
ref: ${{ github.event.pull_request.head.sha }}
- run: |
cd $GITHUB_WORKSPACE && pwd
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

25
.gitignore vendored Normal file
View File

@ -0,0 +1,25 @@
*.swp
*.swo
*.sublime-*
*.pyc
jmeter.log
__pycache__
tmp/
node_modules/
sftp-config.json
.DS_Store
*.iml
*.ipr
*.iws
*.idea
~$*.xls*
~$*.ppt*
~$*.doc*
backups/
.env
build.gradle
.vscode/
nohup.out
docker-compose.dev.yml
docker-compose.custom.yml
private/

View File

@ -0,0 +1,37 @@
<!--- Provide a general summary of the issue in the Title above -->
## 预期行为
<!--- Tell us what should happen -->
## 实际行为
<!--- Tell us what happens instead of the expected behavior -->
## 解决方案
<!--- Not obligatory, but suggest a fix/reason for the bug, -->
## 重现步骤
<!--- Provide a link to a live example, or an unambiguous set of steps to -->
<!--- reproduce this bug. Include code to reproduce, if relevant -->
### 第一步
### 第二步
### 第三步
### 第四步
## 环境
<!--- How has this issue affected you? What are you trying to accomplish? -->
<!--- Providing context helps us come up with a solution that is most useful in the real world -->
<!--- Provide a general summary of the issue in the Title above -->
### 版本
<!--- Git commit hash (`git rev-parse HEAD`) -->

View File

@ -0,0 +1,5 @@
# description
## parent #
# solution

View File

@ -0,0 +1,6 @@
# description
# Others
企业聊天机器人/产品需求汇总
https://wiki.chatopera.com/pages/viewpage.action?pageId=4686818

View File

@ -0,0 +1,5 @@
# 描述
## 关联问题
<!-- BUG Issue 链接 -->

View File

@ -0,0 +1,5 @@
# 描述
## 新功能
<!-- 提交说明 -->

View File

@ -0,0 +1,5 @@
# 描述
## 关联问题
<!-- BUG Issue 链接 -->

View File

@ -0,0 +1,5 @@
# 描述
## 更新日志
<!-- 新版本 -->

42
CHANGELOG.md Normal file
View File

@ -0,0 +1,42 @@
各版本发布更新情况描述。
# 8.0.0
- 更新春松客服应用的开源许可证,使用[春松许可证, v1.0](https://www.cskefu.com/2023/06/25/chunsong-public-license-1-0)
- 更新 MySQL 数据库表结构,大量优化 Table 定义,去掉冗余字段,大幅度提升性能
- 使用 Java 17 API & SDK利用 JDK 新功能,大幅度提升性能
- 从 Springboot 1.5.x 升级到 Springboot 3.x 版本,大幅度提升性能
- 去掉了对中间件服务 Elasticsearch 的依赖,大幅度提升性能
- 去掉了冗余的第三方代码,提升编译性能,减少潜在风险
- 修补若干安全漏洞,如 [#735](https://github.com/cskefu/cskefu/issues/735), [#435](https://github.com/cskefu/cskefu/issues/435), [#177](https://github.com/cskefu/cskefu/issues/177)
- Fixed [#476](https://github.com/cskefu/cskefu/issues/476) every SQL execution slowly on first run
# 7.0.1
- SQL 数据库升级脚本v6 到 v7 的 Rolling Upgrade 脚本
# 7.0.0
在该版本中,前端开发的效率比之前提高了 10倍整个春松客服的前端得到了彻底的重构数十万行代码被重写使用 PugJS 重构 Freemarker 相关,达到彻底替换的目的。
现在,开源社区的开发者们,可以基于 v7 来定制您的客服系统了!
[Blog](https://chatopera.blog.csdn.net/article/details/113786505)
[Commits](https://github.com/cskefu/cskefu/issues/406)
# 6.0.0
重要提示:本次升级未提供 v5 到 v6 的自动脚本 migration 或 rolling upgrade请搭建春松客服新实例因为本次调整设计到数据库无法设定默认值所以没有这部分脚本。切记不要在生产环境直接从 v5 升级到 v6。
- 发布全新组织机构管理模块,支持一个超级管理员和多管理员,每个管理员管理其所在组织机构的用户、坐席、对话、渠道等
- 基于新的组织机构管理隔离数据,优化资源权限
# 5.1.1
- 兼容最新的 Chatopera 机器人平台(<https://bot.chatopera.com>
# 5.0.0
- 优化服务启动速度,重写核心代码
- Fix Bugs

681
CODE_OF_CONDUCT.md Normal file
View File

@ -0,0 +1,681 @@
# Code of Conduct
[Java](#Java)
[C++](https://google.github.io/styleguide/cppguide.html)
[JavaScript](https://google.github.io/styleguide/jsguide.html)
[Python](https://google.github.io/styleguide/pyguide.html)
# Java
1. [前言](#Intro)
1. [源文件基础](#SFBasic)
1. [源文件结构](#SFStruct)
1. [格式](#Format)
1. [命名约定](#Naming)
1. [编程实践](#Practice)
1. [Javadoc](#Javadoc)
1. [IDE 插件](#JavaIDEPlugin)
1. [Java 命名规范版权声明](#JavaCopyright)
## <a id="Intro">前言</a>
这份文档是Google Java编程风格规范的完整定义。当且仅当一个Java源文件符合此文档中的规则
我们才认为它符合Google的Java编程风格。
与其它的编程风格指南一样,这里所讨论的不仅仅是编码格式美不美观的问题,
同时也讨论一些约定及编码标准。然而,这份文档主要侧重于我们所普遍遵循的规则,
对于那些不是明确强制要求的,我们尽量避免提供意见。
### 1.1 术语说明
在本文档中,除非另有说明:
1. 术语class可表示一个普通类枚举类接口或是annotation类型(`@interface`)
1. 术语comment只用来指代实现的注释(implementation comments)我们不使用“documentation comments”一词而是用Javadoc。
其他的术语说明会偶尔在后面的文档出现。
### 1.2 指南说明
本文档中的示例代码并不作为规范。也就是说虽然示例代码是遵循Google编程风格但并不意味着这是展现这些代码的唯一方式。
示例中的格式选择不应该被强制定为规则。
## <a id="SFBasic">源文件基础</a>
### 2.1 文件名
源文件以其最顶层的类名来命名,大小写敏感,文件扩展名为`.java`。
### 2.2 文件编码UTF-8
源文件编码格式为UTF-8。
### 2.3 特殊字符
#### 2.3.1 空白字符
除了行结束符序列ASCII水平空格字符(0x20即空格)是源文件中唯一允许出现的空白字符,这意味着:
1. 所有其它字符串中的空白字符都要进行转义。
1. 制表符不用于缩进。
#### 2.3.2 特殊转义序列
对于具有特殊[转义序列](http://zh.wikipedia.org/wiki/%E8%BD%AC%E4%B9%89%E5%BA%8F%E5%88%97)的任何字符(\b, \t, \n, \f, \r, \", \'及\\),我们使用它的转义序列,而不是相应的八进制(比如`\012`)或Unicode(比如`\u000a`)转义。
#### 2.3.3 非ASCII字符
对于剩余的非ASCII字符是使用实际的Unicode字符(比如∞)还是使用等价的Unicode转义符(比如\u221e),取决于哪个能让代码更易于阅读和理解。
> Tip: 在使用Unicode转义符或是一些实际的Unicode字符时建议做些注释给出解释这有助于别人阅读和理解。
例如:
```java
String unitAbbrev = "μs"; | 赞,即使没有注释也非常清晰
String unitAbbrev = "\u03bcs"; // "μs" | 允许,但没有理由要这样做
String unitAbbrev = "\u03bcs"; // Greek letter mu, "s" | 允许,但这样做显得笨拙还容易出错
String unitAbbrev = "\u03bcs"; | 很糟,读者根本看不出这是什么
return '\ufeff' + content; // byte order mark | Good对于非打印字符使用转义并在必要时写上注释
```
> Tip: 永远不要由于害怕某些程序可能无法正确处理非ASCII字符而让你的代码可读性变差。当程序无法正确处理非ASCII字符时它自然无法正确运行
你就会去fix这些问题的了。(言下之意就是大胆去用非ASCII字符如果真的有需要的话)
## <a id="SFStruct">源文件结构</a>
一个源文件包含(按顺序地)
1. 许可证或版权信息(如有需要)
1. package语句
1. import语句
1. 一个顶级类(**只有一个**)
以上每个部分之间用一个空行隔开。
### 3.1 许可证或版权信息
如果一个文件包含许可证或版权信息,那么它应当被放在文件最前面。
### 3.2 package语句
package语句不换行列限制(4.4节)并不适用于package语句。(即package语句写在一行里)
### 3.3 import语句
#### 3.3.1 import不要使用通配符
不要出现类似这样的import语句`import java.util.*;`
#### 3.3.2 不要换行
import语句不换行列限制(4.4节)并不适用于import语句。(每个import语句独立成行)
#### 3.3.3 顺序和间距
import语句可分为以下几组按照这个顺序每组由一个空行分隔
1. 所有的静态导入独立成组
1. `com.google` imports(仅当这个源文件是在`com.google`包下)
1. 第三方的包。每个顶级包为一组字典序。例如android, com, junit, org, sun
1. `java` imports
1. `javax` imports
组内不空行,按字典序排列。
### 3.4 类声明
#### 3.4.1 只有一个顶级类声明
每个顶级类都在一个与它同名的源文件中(当然,还包含`.java`后缀)。
例外:`package-info.java`,该文件中可没有`package-info`类。
#### 3.4.2 类成员顺序
类的成员顺序对易学性有很大的影响,但这也不存在唯一的通用法则。不同的类对成员的排序可能是不同的。
最重要的一点,每个类应该以某种逻辑去排序它的成员,维护者应该要能解释这种排序逻辑。比如,
新的方法不能总是习惯性地添加到类的结尾,因为这样就是按时间顺序而非某种逻辑来排序的。
##### 3.4.2.1 重载:永不分离
当一个类有多个构造函数,或是多个同名方法,这些函数/方法应该按顺序出现在一起,中间不要放进其它函数/方法。
## <a id="Format">格式</a>
**术语说明**:块状结构(block-like construct)指的是一个类,方法或构造函数的主体。需要注意的是,数组初始化中的初始值可被选择性地视为块状结构(4.8.3.1节)。
### 4.1 大括号
#### 4.1.1 使用大括号(即使是可选的)
大括号与`if, else, for, do, while`语句一起使用,即使只有一条语句(或是空),也应该把大括号写上。
#### 4.1.2 非空块K & R 风格
对于非空块和块状结构大括号遵循Kernighan和Ritchie风格
([Egyptian brackets](http://www.codinghorror.com/blog/2012/07/new-programming-jargon.html)):
* 左大括号前不换行
* 左大括号后换行
* 右大括号前换行
* 如果右大括号是一个语句、函数体或类的终止,则右大括号后换行; 否则不换行。例如如果右大括号后面是else或逗号则不换行。
示例:
```java
return new MyClass() {
@Override public void method() {
if (condition()) {
try {
something();
} catch (ProblemException e) {
recover();
}
}
}
};
```
4.8.1节给出了enum类的一些例外。
#### 4.1.3 空块:可以用简洁版本
一个空的块状结构里什么也不包含,大括号可以简洁地写成`{}`,不需要换行。例外:如果它是一个多块语句的一部分(if/else 或 try/catch/finally)
,即使大括号内没内容,右大括号也要换行。
示例:
```java
void doNothing() {}
```
### 4.2 块缩进2个空格
每当开始一个新的块缩进增加2个空格当块结束时缩进返回先前的缩进级别。缩进级别适用于代码和注释。(见4.1.2节中的代码示例)
### 4.3 一行一个语句
每个语句后要换行。
### 4.4 列限制80或100
一个项目可以选择一行80个字符或100个字符的列限制除了下述例外任何一行如果超过这个字符数限制必须自动换行。
例外:
1. 不可能满足列限制的行(例如Javadoc中的一个长URL或是一个长的JSNI方法参考)。
1. `package`和`import`语句(见3.2节和3.3节)。
1. 注释中那些可能被剪切并粘贴到shell中的命令行。
### 4.5 自动换行
**术语说明**:一般情况下,一行长代码为了避免超出列限制(80或100个字符)而被分为多行,我们称之为自动换行(line-wrapping)。
我们并没有全面,确定性的准则来决定在每一种情况下如何自动换行。很多时候,对于同一段代码会有好几种有效的自动换行方式。
> Tip: 提取方法或局部变量可以在不换行的情况下解决代码过长的问题(是合理缩短命名长度吧)
#### 4.5.1 从哪里断开
自动换行的基本准则是:更倾向于在更高的语法级别处断开。
1. 如果在`非赋值运算符`处断开,那么在该符号前断开(比如+,它将位于下一行)。注意这一点与Google其它语言的编程风格不同(如C++和JavaScript)。
这条规则也适用于以下“类运算符”符号:点分隔符(.),类型界限中的&`<T extends Foo & Bar>`)catch块中的管道符号(`catch (FooException | BarException e`)
1. 如果在`赋值运算符`处断开,通常的做法是在该符号后断开(比如=,它与前面的内容留在同一行)。这条规则也适用于`foreach`语句中的分号。
1. 方法名或构造函数名与左括号留在同一行。
1. 逗号(,)与其前面的内容留在同一行。
#### 4.5.2 自动换行时缩进至少+4个空格
自动换行时第一行后的每一行至少比第一行多缩进4个空格(注意制表符不用于缩进。见2.3.1节)。
当存在连续自动换行时缩进可能会多缩进不只4个空格(语法元素存在多级时)。一般而言,两个连续行使用相同的缩进当且仅当它们开始于同级语法元素。
第4.6.3水平对齐一节中指出,不鼓励使用可变数目的空格来对齐前面行的符号。
### 4.6 空白
#### 4.6.1 垂直空白
以下情况需要使用一个空行:
1. 类内连续的成员之间:字段,构造函数,方法,嵌套类,静态初始化块,实例初始化块。
- **例外**:两个连续字段之间的空行是可选的,用于字段的空行主要用来对字段进行逻辑分组。
1. 在函数体内,语句的逻辑分组间使用空行。
1. 类内的第一个成员前或最后一个成员后的空行是可选的(既不鼓励也不反对这样做,视个人喜好而定)。
1. 要满足本文档中其他节的空行要求(比如3.3节import语句)
多个连续的空行是允许的,但没有必要这样做(我们也不鼓励这样做)。
#### 4.6.2 水平空白
除了语言需求和其它规则并且除了文字注释和Javadoc用到单个空格单个ASCII空格也出现在以下几个地方
1. 分隔任何保留字与紧随其后的左括号(`(`)(如`if, for catch`等)。
1. 分隔任何保留字与其前面的右大括号(`}`)(如`else, catch`)。
1. 在任何左大括号前(`{`),两个例外:
- `@SomeAnnotation({a, b})`(不使用空格)。
- `String[][] x = {{"foo"}};`(大括号间没有空格见下面的Note)。
1. 在任何二元或三元运算符的两侧。这也适用于以下“类运算符”符号:
- 类型界限中的&(`<T extends Foo & Bar>`)。
- catch块中的管道符号(`catch (FooException | BarException e`)。
- `foreach`语句中的分号。
1. 在`, : ;`及右括号(`)`)后
1. 如果在一条语句后做注释,则双斜杠(//)两边都要空格。这里可以允许多个空格,但没有必要。
1. 类型和变量之间List<String> list。
1. 数组初始化中,大括号内的空格是可选的,即`new int[] {5, 6}`和`new int[] { 5, 6 }`都是可以的。
> Note这个规则并不要求或禁止一行的开关或结尾需要额外的空格只对内部空格做要求。
#### 4.6.3 水平对齐:不做要求
**术语说明**:水平对齐指的是通过增加可变数量的空格来使某一行的字符与上一行的相应字符对齐。
这是允许的(而且在不少地方可以看到这样的代码)但Google编程风格对此不做要求。即使对于已经使用水平对齐的代码我们也不需要去保持这种风格。
以下示例先展示未对齐的代码,然后是对齐的代码:
```java
private int x; // this is fine
private Color color; // this too
private int x; // permitted, but future edits
private Color color; // may leave it unaligned
```
> Tip对齐可增加代码可读性但它为日后的维护带来问题。考虑未来某个时候我们需要修改一堆对齐的代码中的一行。
这可能导致原本很漂亮的对齐代码变得错位。很可能它会提示你调整周围代码的空白来使这一堆代码重新水平对齐(比如程序员想保持这种水平对齐的风格)
这就会让你做许多的无用功增加了reviewer的工作并且可能导致更多的合并冲突。
### 4.7 用小括号来限定组:推荐
除非作者和reviewer都认为去掉小括号也不会使代码被误解或是去掉小括号能让代码更易于阅读否则我们不应该去掉小括号。
我们没有理由假设读者能记住整个Java运算符优先级表。
### 4.8 具体结构
#### 4.8.1 枚举类
枚举常量间用逗号隔开,换行可选。
没有方法和文档的枚举类可写成数组初始化的格式:
```java
private enum Suit { CLUBS, HEARTS, SPADES, DIAMONDS }
```
由于枚举类也是一个类,因此所有适用于其它类的格式规则也适用于枚举类。
#### 4.8.2 变量声明
##### 4.8.2.1 每次只声明一个变量
不要使用组合声明,比如`int a, b;`。
##### 4.8.2.2 需要时才声明,并尽快进行初始化
不要在一个代码块的开头把局部变量一次性都声明了(这是c语言的做法),而是在第一次需要使用它时才声明。
局部变量在声明时最好就进行初始化,或者声明后尽快进行初始化。
#### 4.8.3 数组
##### 4.8.3.1 数组初始化:可写成块状结构
数组初始化可以写成块状结构比如下面的写法都是OK的
```java
new int[] {
0, 1, 2, 3
}
new int[] {
0,
1,
2,
3
}
new int[] {
0, 1,
2, 3
}
new int[]{0, 1, 2, 3}
```
##### 4.8.3.2 非C风格的数组声明
中括号是类型的一部分:`String[] args` 而非`String args[]`。
#### 4.8.4 switch语句
**术语说明**switch块的大括号内是一个或多个语句组。每个语句组包含一个或多个switch标签(`case FOO:`或`default:`),后面跟着一条或多条语句。
##### 4.8.4.1 缩进
与其它块状结构一致switch块中的内容缩进为2个空格。
每个switch标签后新起一行再缩进2个空格写下一条或多条语句。
##### 4.8.4.2 Fall-through注释
在一个switch块内每个语句组要么通过`break, continue, return`或抛出异常来终止,要么通过一条注释来说明程序将继续执行到下一个语句组,
任何能表达这个意思的注释都是OK的(典型的是用`// fall through`)。这个特殊的注释并不需要在最后一个语句组(一般是`default`)中出现。示例:
```java
switch (input) {
case 1:
case 2:
prepareOneOrTwo();
// fall through
case 3:
handleOneTwoOrThree();
break;
default:
handleLargeNumber(input);
}
```
##### 4.8.4.3 default的情况要写出来
每个switch语句都包含一个`default`语句组,即使它什么代码也不包含。
#### 4.8.5 注解(Annotations)
注解紧跟在文档块后面,应用于类、方法和构造函数,一个注解独占一行。这些换行不属于自动换行(第4.5节,自动换行),因此缩进级别不变。例如:
```java
@Override
@Nullable
public String getNameIfPresent() { ... }
```
**例外**:单个的注解可以和签名的第一行出现在同一行。例如:
```java
@Override public int hashCode() { ... }
```
应用于字段的注解紧随文档块出现,应用于字段的多个注解允许与字段出现在同一行。例如:
```java
@Partial @Mock DataLoader loader;
```
参数和局部变量注解没有特定规则。
#### 4.8.6 注释
##### 4.8.6.1 块注释风格
块注释与其周围的代码在同一缩进级别。它们可以是`/* ... */`风格,也可以是`// ...`风格。对于多行的`/* ... */`注释,后续行必须从`*`开始,
并且与前一行的`*`对齐。以下示例注释都是OK的。
```java
/*
* This is // And so /* Or you can
* okay. // is this. * even do this. */
*/
```
注释不要封闭在由星号或其它字符绘制的框架里。
> Tip在写多行注释时如果你希望在必要时能重新换行(即注释像段落风格一样),那么使用 `/* ... */`
#### 4.8.7 Modifiers
类和成员的modifiers如果存在则按Java语言规范中推荐的顺序出现。
```java
public protected private abstract static final transient volatile synchronized native strictfp
```
## <a id="Naming">命名约定</a>
### 5.1 对所有标识符都通用的规则
标识符只能使用ASCII字母和数字因此每个有效的标识符名称都能匹配正则表达式`\w+`。
在Google其它编程语言风格中使用的特殊前缀或后缀如`name_`, `mName`, `s_name`和`kName`在Java编程风格中都不再使用。
### 5.2 标识符类型的规则
#### 5.2.1 包名
包名全部小写,连续的单词只是简单地连接起来,不使用下划线。
#### 5.2.2 类名
类名都以`UpperCamelCase`风格编写。
类名通常是名词或名词短语,接口名称有时可能是形容词或形容词短语。现在还没有特定的规则或行之有效的约定来命名注解类型。
测试类的命名以它要测试的类的名称开始,以`Test`结束。例如,`HashTest`或`HashIntegrationTest`。
#### 5.2.3 方法名
方法名都以`lowerCamelCase`风格编写。
方法名通常是动词或动词短语。
下划线可能出现在JUnit测试方法名称中用以分隔名称的逻辑组件。一个典型的模式是`test<MethodUnderTest>_<state>`,例如`testPop_emptyStack`。
并不存在唯一正确的方式来命名测试方法。
#### 5.2.4 常量名
常量名命名模式为`CONSTANT_CASE`,全部字母大写,用下划线分隔单词。那,到底什么算是一个常量?
每个常量都是一个静态final字段但不是所有静态final字段都是常量。在决定一个字段是否是一个常量时
考虑它是否真的感觉像是一个常量。例如,如果任何一个该实例的观测状态是可变的,则它几乎肯定不会是一个常量。
只是永远不`打算`改变对象一般是不够的,它要真的一直不变才能将它示为常量。
```java
// Constants
static final int NUMBER = 5;
static final ImmutableList<String> NAMES = ImmutableList.of("Ed", "Ann");
static final Joiner COMMA_JOINER = Joiner.on(','); // because Joiner is immutable
static final SomeMutableType[] EMPTY_ARRAY = {};
enum SomeEnum { ENUM_CONSTANT }
// Not constants
static String nonFinal = "non-final";
final String nonStatic = "non-static";
static final Set<String> mutableCollection = new HashSet<String>();
static final ImmutableSet<SomeMutableType> mutableElements = ImmutableSet.of(mutable);
static final Logger logger = Logger.getLogger(MyClass.getName());
static final String[] nonEmptyArray = {"these", "can", "change"};
```
这些名字通常是名词或名词短语。
#### 5.2.5 非常量字段名
非常量字段名以`lowerCamelCase`风格编写。
这些名字通常是名词或名词短语。
#### 5.2.6 参数名
参数名以`lowerCamelCase`风格编写。
参数应该避免用单个字符命名。
#### 5.2.7 局部变量名
局部变量名以`lowerCamelCase`风格编写,比起其它类型的名称,局部变量名可以有更为宽松的缩写。
虽然缩写更宽松,但还是要避免用单字符进行命名,除了临时变量和循环变量。
即使局部变量是final和不可改变的也不应该把它示为常量自然也不能用常量的规则去命名它。
#### 5.2.8 类型变量名
类型变量可用以下两种风格之一进行命名:
* 单个的大写字母,后面可以跟一个数字(如E, T, X, T2)。
* 以类命名方式(5.2.2节)后面加个大写的T(如RequestT, FooBarT)。
### 5.3 驼峰式命名法(CamelCase)
[驼峰式命名法](http://zh.wikipedia.org/wiki/%E9%A7%9D%E5%B3%B0%E5%BC%8F%E5%A4%A7%E5%B0%8F%E5%AF%AB)分大驼峰式命名法(`UpperCamelCase`)和小驼峰式命名法(`lowerCamelCase`)。
有时,我们有不只一种合理的方式将一个英语词组转换成驼峰形式,如缩略语或不寻常的结构(例如"IPv6"或"iOS")。Google指定了以下的转换方案。
名字从`散文形式`(prose form)开始:
1. 把短语转换为纯ASCII码并且移除任何单引号。例如"Müller's algorithm"将变成"Muellers algorithm"。
1. 把这个结果切分成单词,在空格或其它标点符号(通常是连字符)处分割开。
- 推荐:如果某个单词已经有了常用的驼峰表示形式,按它的组成将它分割开(如"AdWords"将分割成"ad words")。
需要注意的是"iOS"并不是一个真正的驼峰表示形式,因此该推荐对它并不适用。
1. 现在将所有字母都小写(包括缩写),然后将单词的第一个字母大写:
- 每个单词的第一个字母都大写,来得到大驼峰式命名。
- 除了第一个单词,每个单词的第一个字母都大写,来得到小驼峰式命名。
1. 最后将所有的单词连接起来得到一个标识符。
示例:
Prose form Correct Incorrect
------------------------------------------------------------------
"XML HTTP request" XmlHttpRequest XMLHTTPRequest
"new customer ID" newCustomerId newCustomerID
"inner stopwatch" innerStopwatch innerStopWatch
"supports IPv6 on iOS?" supportsIpv6OnIos supportsIPv6OnIOS
"YouTube importer" YouTubeImporter
YoutubeImporter*
加星号处表示可以,但不推荐。
> Note在英语中某些带有连字符的单词形式不唯一。例如"nonempty"和"non-empty"都是正确的,因此方法名`checkNonempty`和`checkNonEmpty`也都是正确的。
## <a id="Practice">编程实践</a>
### 6.1 @Override:能用则用
只要是合法的,就把`@Override`注解给用上。
### 6.2 捕获的异常:不能忽视
除了下面的例子,对捕获的异常不做响应是极少正确的。(典型的响应方式是打印日志,或者如果它被认为是不可能的,则把它当作一个`AssertionError`重新抛出。)
如果它确实是不需要在catch块中做任何响应需要做注释加以说明(如下面的例子)。
```java
try {
int i = Integer.parseInt(response);
return handleNumericResponse(i);
} catch (NumberFormatException ok) {
// it's not numeric; that's fine, just continue
}
return handleTextResponse(response);
```
**例外**:在测试中,如果一个捕获的异常被命名为`expected`,则它可以被不加注释地忽略。下面是一种非常常见的情形,用以确保所测试的方法会抛出一个期望中的异常,
因此在这里就没有必要加注释。
```java
try {
emptyStack.pop();
fail();
} catch (NoSuchElementException expected) {
}
```
### 6.3 静态成员:使用类进行调用
使用类名调用静态的类成员,而不是具体某个对象或表达式。
```java
Foo aFoo = ...;
Foo.aStaticMethod(); // good
aFoo.aStaticMethod(); // bad
somethingThatYieldsAFoo().aStaticMethod(); // very bad
```
### 6.4 Finalizers: 禁用
极少会去重写`Object.finalize`。
> Tip不要使用finalize。如果你非要使用它请先仔细阅读和理解 [Effective Java](http://books.google.com/books?isbn=8131726592) 第7条款“Avoid Finalizers”然后不要使用它。
## <a id="Javadoc">Javadoc</a>
### 7.1 格式
#### 7.1.1 一般形式
Javadoc块的基本格式如下所示
```java
/**
* Multiple lines of Javadoc text are written here,
* wrapped normally...
*/
public int method(String p1) { ... }
```
或者是以下单行形式:
```java
/** An especially short bit of Javadoc. */
```
基本格式总是OK的。当整个Javadoc块能容纳于一行时(且没有Javadoc标记@XXX),可以使用单行形式。
#### 7.1.2 段落
空行(即,只包含最左侧星号的行)会出现在段落之间和Javadoc标记(@XXX)之前(如果有的话)。
除了第一个段落,每个段落第一个单词前都有标签`<p>`,并且它和第一个单词间没有空格。
#### 7.1.3 Javadoc标记
标准的Javadoc标记按以下顺序出现`@param`, `@return`, `@throws`, `@deprecated`, 前面这4种标记如果出现描述都不能为空。
当描述无法在一行中容纳连续行需要至少再缩进4个空格。
### 7.2 摘要片段
每个类或成员的Javadoc以一个简短的摘要片段开始。这个片段是非常重要的在某些情况下它是唯一出现的文本比如在类和方法索引中。
这只是一个小片段,可以是一个名词短语或动词短语,但不是一个完整的句子。它不会以`A {@code Foo} is a...`或`This method returns...`开头,
它也不会是一个完整的祈使句,如`Save the record...`。然而,由于开头大写及被加了标点,它看起来就像是个完整的句子。
> Tip一个常见的错误是把简单的Javadoc写成`/** @return the customer ID */`,这是不正确的。它应该写成`/** Returns the customer ID. */`。
### 7.3 哪里需要使用Javadoc
至少在每个public类及它的每个public和protected成员处使用Javadoc以下是一些例外
#### 7.3.1 例外:不言自明的方法
对于简单明显的方法如`getFoo`Javadoc是可选的(即,是可以不写的)。这种情况下除了写“Returns the foo”确实也没有什么值得写了。
单元测试类中的测试方法可能是不言自明的最常见例子了,我们通常可以从这些方法的描述性命名中知道它是干什么的,因此不需要额外的文档说明。
> Tip如果有一些相关信息是需要读者了解的那么以上的例外不应作为忽视这些信息的理由。例如对于方法名`getCanonicalName`
就不应该忽视文档说明,因为读者很可能不知道词语`canonical name`指的是什么。
#### 7.3.2 例外:重写
如果一个方法重写了超类中的方法那么Javadoc并非必需的。
#### 7.3.3 可选的Javadoc
对于包外不可见的类和方法如有需要也是要使用Javadoc的。如果一个注释是用来定义一个类方法字段的整体目的或行为
那么这个注释应该写成Javadoc这样更统一更友好。
## <a id="JavaIDEPlugin">IDE 插件</a>
### 8.1 IntelliJ IDEA
自动给出优化建议
https://github.com/alibaba/p3c/tree/master/idea-plugin
## <a id="JavaCopyright">Java 命名规范版权声明</a>
春松客服开源社区在原文内容上有调整;原文翻译自[Google Java Style](http://google-styleguide.googlecode.com/svn/trunk/javaguide.html)
译者[@Hawstein](http://weibo.com/hawstein)。

26
CONTRIBUTING.md Normal file
View File

@ -0,0 +1,26 @@
# Contributing
## 提交反馈
在[春松客服 Issues](https://gitee.com/cskefu/cskefu/issues)中,先搜索是否有重复的。然后进行补充或新建 Issue。
## 提交代码
从零开始,一步步介绍:
[https://docs.cskefu.com/docs/osc/contribution](https://docs.cskefu.com/docs/osc/contribution)
## 提交文档
春松客服文档中心的项目,也是开源的,地址在:
[https://gitee.com/cskefu/docs](https://gitee.com/cskefu/docs)
春松客服文档 Markdown 文件路径:
[https://gitee.com/cskefu/docs/tree/main/docs](https://gitee.com/cskefu/docs/tree/main/docs)
更新文档:
1提交 PR 到[春松客服文档中心 Git 仓库](https://gitee.com/cskefu/docs/tree/main),一步到位。
2提交 Issue 到[春松客服 Issues](https://gitee.com/cskefu/cskefu/issues/new) 并撰写文档内容,使用该方案则需要后续其他协作者提交到 [春松客服文档中心 Git 仓库](https://gitee.com/cskefu/docs) 中。

13
LICENSE Normal file
View File

@ -0,0 +1,13 @@
Copyright 2023 Beijing Huaxia Chunsong Technology Co., Ltd. <https://www.chatopera.com>
Licensed under the Chunsong Public License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
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.

271
README.md Normal file
View File

@ -0,0 +1,271 @@
<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-35-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> <a href="#mentoring-lecjy" title="Mentoring">🧑‍🏫</a> <a href="#maintenance-lecjy" title="Maintenance">🚧</a> <a href="https://github.com/cskefu/cskefu/commits?author=lecjy" title="Code">💻</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/zjpzjp"><img src="https://avatars.githubusercontent.com/u/11382248?v=4?s=50" width="50px;" alt="websir"/><br /><sub><b>websir</b></sub></a><br /><a href="https://github.com/cskefu/cskefu/commits?author=zjpzjp" title="Code">💻</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="./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://store.chatopera.com/product/cskfdjt19)
- [掌握春松客服前端框架 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

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

23
contact-center/.gitignore vendored Normal file
View File

@ -0,0 +1,23 @@
*.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/

30
contact-center/Dockerfile Normal file
View File

@ -0,0 +1,30 @@
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"]

7
contact-center/README.md Normal file
View File

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

View File

@ -0,0 +1,40 @@
#! /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

@ -0,0 +1,19 @@
#! /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 compile
# 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

@ -0,0 +1,30 @@
#! /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

@ -0,0 +1,50 @@
#! /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

@ -0,0 +1,15 @@
#! /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

@ -0,0 +1,13 @@
#! /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

@ -0,0 +1,13 @@
#! /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

@ -0,0 +1,47 @@
#! /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

@ -0,0 +1,19 @@
#! /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

@ -0,0 +1,21 @@
#! /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
PACKAGE_VERSION=`git rev-parse --short HEAD`
docker push $registryPrefix$imagename:$PACKAGE_VERSION
docker push $registryPrefix$imagename:develop

View File

@ -0,0 +1,13 @@
#! /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

@ -0,0 +1,52 @@
#! /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

@ -0,0 +1,14 @@
#! /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

@ -0,0 +1,12 @@
#! /bin/bash
###########################################
#
###########################################
# constants
baseDir=$(cd `dirname "$0"`;pwd)
# functions
# main
[ -z "${BASH_SOURCE[0]}" -o "${BASH_SOURCE[0]}" = "$0" ] || return
rm -rf ~/.m2/repository/com/cskefu/cc

11
contact-center/app/.gitignore vendored Normal file
View File

@ -0,0 +1,11 @@
# 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/

142
contact-center/app/pom.xml Normal file
View File

@ -0,0 +1,142 @@
<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>
<version>3.1.3</version>
<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

@ -0,0 +1,141 @@
/*
* 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

@ -0,0 +1,116 @@
/*
* 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

@ -0,0 +1,649 @@
/*
* 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());
/**
*
* AgentServiceAgentServiceAgentService
*/
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);
}
/**
* agentUserAgentService
* 使
* 1. AgentUserAgentService
* 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

@ -0,0 +1,84 @@
/*
* 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

@ -0,0 +1,415 @@
/*
* 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 org.springframework.util.CollectionUtils;
import java.util.*;
import java.util.stream.Collectors;
/**
*
*/
@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;
@Autowired
private OrganRepository organRepository;
/**
* 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) {
List<Organ> list = organRepository.findAll();
if (CollectionUtils.isEmpty(list)) {
return new SessionConfig();
} else {
Map<String, String> map = list.stream().collect(Collectors.toMap(item -> item.getId(), item -> item.getParent()));
List<SessionConfig> configList = sessionConfigRes.findAll();
if (CollectionUtils.isEmpty(configList)) {
return new SessionConfig();
} else {
Map<String, SessionConfig> skillMap = configList.stream().collect(Collectors.toMap(item -> item.getSkill(), item -> item));
if (map.get(organid) == null || skillMap.get(map.get(organid)) == null) {
return new SessionConfig();
}
return skillMap.get(map.get(organid));
}
}
} 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

@ -0,0 +1,59 @@
/*
* 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

@ -0,0 +1,68 @@
/*
* 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

@ -0,0 +1,107 @@
/*
* 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);
/**
* 1AgentService
*/
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

@ -0,0 +1,194 @@
/*
* 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

@ -0,0 +1,309 @@
/*
* 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

@ -0,0 +1,228 @@
/*
* 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;
/**
* AgentUserComposeContext
*
* @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;
}
/**
* WebIMContext
*
* @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

@ -0,0 +1,28 @@
/*
* 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

@ -0,0 +1,67 @@
/*
* 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

@ -0,0 +1,86 @@
/*
* 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

@ -0,0 +1,257 @@
/*
* 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.model.ExecuteResult;
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 com.cskefu.cc.proxy.LicenseProxy;
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;
@Autowired
private LicenseProxy licenseProxy;
/**
* 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 AgentUserStatusAgentno
* Agent
*/
AgentUser p = new AgentUser(
ctx.getOnlineUserId(),
ctx.getChannelType(),
ctx.getOnlineUserId(),
ctx.getOnlineUserNickname(),
ctx.getAppid());
// 执行计费逻辑
ExecuteResult writeDownResult = licenseProxy.writeDownAgentUserUsageInStore(p);
if (writeDownResult.getRc() != ExecuteResult.RC_SUCC) {
// 配额操作失败,提示座席
p.setLicenseVerifiedPass(false);
p.setLicenseBillingMsg(writeDownResult.getMsg());
}
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

@ -0,0 +1,79 @@
/*
* 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

@ -0,0 +1,79 @@
/*
* 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

@ -0,0 +1,110 @@
/*
* 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

@ -0,0 +1,49 @@
/*
* 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

@ -0,0 +1,71 @@
/*
* 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

@ -0,0 +1,63 @@
/*
* 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

@ -0,0 +1,111 @@
/*
* 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

@ -0,0 +1,74 @@
/*
* 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

@ -0,0 +1,118 @@
/*
* 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

@ -0,0 +1,48 @@
/*
* 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

@ -0,0 +1,144 @@
/*
* 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.exception.BillingResourceException;
import com.cskefu.cc.model.AgentUser;
import com.cskefu.cc.proxy.AgentAuditProxy;
import com.cskefu.cc.proxy.LicenseProxy;
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.aspectj.lang.annotation.Before;
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;
@Autowired
private LicenseProxy licenseProxy;
@Before("execution(* com.cskefu.cc.persistence.repository.AgentUserRepository.save(..))")
public void beforeSave(final JoinPoint joinPoint) {
final AgentUser agentUser = (AgentUser) joinPoint.getArgs()[0];
if (StringUtils.isBlank(agentUser.getId())) {
logger.info("[beforeSave] agentUser id is blank");
if (StringUtils.isNotBlank(agentUser.getOpttype()) && StringUtils.equals(MainContext.OptType.CHATBOT.toString(), agentUser.getOpttype())) {
// 机器人座席支持的对话,跳过计数
agentUser.setLicenseVerifiedPass(true);
return;
}
// 计数加一
try {
licenseProxy.increResourceUsageInMetaKv(MainContext.BillingResource.AGENGUSER, 1);
} catch (BillingResourceException e) {
logger.error("[beforeSave] error", e.toString());
}
}
}
@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

@ -0,0 +1,50 @@
/*
* 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

@ -0,0 +1,51 @@
/*
* 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.aspect;
import com.cskefu.cc.basic.MainContext;
import com.cskefu.cc.exception.BillingQuotaException;
import com.cskefu.cc.exception.BillingResourceException;
import com.cskefu.cc.model.Channel;
import com.cskefu.cc.model.User;
import com.cskefu.cc.proxy.LicenseProxy;
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 ChannelAspect {
private final static Logger logger = LoggerFactory.getLogger(ChannelAspect.class);
@Autowired
private LicenseProxy licenseProxy;
@Before("execution(* com.cskefu.cc.persistence.repository.ChannelRepository.save(..))")
public void beforeSave(final JoinPoint joinPoint) throws BillingResourceException, BillingQuotaException {
final Channel channel = (Channel) joinPoint.getArgs()[0];
logger.info("[beforeSave] before channel id {}, type {}", channel.getId(), channel.getType());
if (StringUtils.isBlank(channel.getId())) {
// create new Channel
if (StringUtils.equals(channel.getType(), MainContext.ChannelType.WEBIM.toString())) {
// create new WEBIM channel
licenseProxy.writeDownResourceUsageInStore(MainContext.BillingResource.CHANNELWEBIM, 1);
}
} else {
// update existed Channel
}
}
}

View File

@ -0,0 +1,50 @@
/*
* 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.aspect;
import com.cskefu.cc.basic.MainContext;
import com.cskefu.cc.basic.MainUtils;
import com.cskefu.cc.exception.BillingQuotaException;
import com.cskefu.cc.exception.BillingResourceException;
import com.cskefu.cc.model.Contacts;
import com.cskefu.cc.model.User;
import com.cskefu.cc.proxy.LicenseProxy;
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 ContactsAspect {
private final static Logger logger = LoggerFactory.getLogger(ContactsAspect.class);
@Autowired
private LicenseProxy licenseProxy;
@Before("execution(* com.cskefu.cc.persistence.repository.ContactsRepository.save(..))")
public void beforeSave(final JoinPoint joinPoint) throws BillingResourceException, BillingQuotaException {
final Contacts contacts = (Contacts) joinPoint.getArgs()[0];
logger.info("[save] before contacts id {}", contacts.getId());
if (StringUtils.isBlank(contacts.getId())) {
// 执行配额扣除
licenseProxy.writeDownResourceUsageInStore(MainContext.BillingResource.CONTACT, 1);
contacts.setId(MainUtils.getUUID());
} else {
// update existed user
}
}
}

View File

@ -0,0 +1,58 @@
/*
* 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

@ -0,0 +1,47 @@
/*
* 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.aspect;
import com.cskefu.cc.basic.MainContext;
import com.cskefu.cc.exception.BillingQuotaException;
import com.cskefu.cc.exception.BillingResourceException;
import com.cskefu.cc.model.Channel;
import com.cskefu.cc.model.Organ;
import com.cskefu.cc.proxy.LicenseProxy;
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 OrganAspect {
private final static Logger logger = LoggerFactory.getLogger(OrganAspect.class);
@Autowired
private LicenseProxy licenseProxy;
@Before("execution(* com.cskefu.cc.persistence.repository.OrganRepository.save(..))")
public void beforeSave(final JoinPoint joinPoint) throws BillingResourceException, BillingQuotaException {
final Organ organ = (Organ) joinPoint.getArgs()[0];
logger.info("[beforeSave] before organ id {}", organ.getId());
if (StringUtils.isBlank(organ.getId())) {
// create new organ
licenseProxy.writeDownResourceUsageInStore(MainContext.BillingResource.ORGAN, 1);
} else {
// update existed Channel
}
}
}

View File

@ -0,0 +1,93 @@
/*
* 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

@ -0,0 +1,47 @@
/*
* 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.aspect;
import com.cskefu.cc.basic.MainContext;
import com.cskefu.cc.exception.BillingQuotaException;
import com.cskefu.cc.exception.BillingResourceException;
import com.cskefu.cc.model.User;
import com.cskefu.cc.proxy.LicenseProxy;
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 UserAspect {
private final static Logger logger = LoggerFactory.getLogger(UserAspect.class);
@Autowired
private LicenseProxy licenseProxy;
@Before("execution(* com.cskefu.cc.persistence.repository.UserRepository.save(..))")
public void beforeSave(final JoinPoint joinPoint) throws BillingResourceException, BillingQuotaException {
final User user = (User) joinPoint.getArgs()[0];
logger.info("[save] before user id {}", user.getId());
if (StringUtils.isBlank(user.getId())) {
// 执行配额扣除
licenseProxy.writeDownResourceUsageInStore(MainContext.BillingResource.USER, 1);
} else {
// update existed user
}
}
}

View File

@ -0,0 +1,242 @@
/*
* 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";
/**
* License
*/
public static final String PRODUCT_ID_CSKEFU001 = "cskefu001";
public static final String LICENSE_SERVER_INST_ID = "SERVERINSTID";
public static final String LICENSE_SERVICE_NAME = "SERVICENAME";
public static final String LICENSE_SERVICE_NAME_PREFIX = "春松客服";
public static final String LICENSEIDS = "LICENSEIDS";
public static final String METAKV_DATATYPE_STRING = "string";
public static final String METAKV_DATATYPE_INT = "int";
public static final String SHORTID = "shortId";
public static final String LICENSES = "licenses";
public static final String ADDDATE = "addDate";
public static final String LICENSE = "license";
public static final String UPDATETIME = "updateTime";
public static final String STATUS = "status";
public static final String PRODUCT = "product";
public static final String LICENSESTOREPROVIDER = "licenseStoreProvider";
public static final String USER = "user";
public static final String RESOURCES_USAGE_KEY_PREFIX = "RESOURCES_USAGE";
public static final String NEW_USER_SUCCESS = "new_user_success";
public static final String PRODUCT_ID = "productId";
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,20 @@
/**
* 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

@ -0,0 +1,45 @@
/*
* 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

@ -0,0 +1,44 @@
/*
* 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 TokenRedis
*/
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

@ -0,0 +1,32 @@
/**
* 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

@ -0,0 +1,132 @@
/*
* 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));
}
/**
* IDAuth
*
* @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

@ -0,0 +1,34 @@
/*
* 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

@ -0,0 +1,36 @@
/*
* 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

@ -0,0 +1,97 @@
/*
* 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

@ -0,0 +1,71 @@
/*
* 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

@ -0,0 +1,112 @@
/*
* 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

@ -0,0 +1,822 @@
/*
* 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));
}
/**
* IDIDCallCenterAgent
*
* @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);
}
/**
* IDCallCenterAgent
*
* @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
******************************************/
/**
* usersession使
* 使使
*
* @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

@ -0,0 +1,304 @@
/*
* 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;
/**
* 使StringRedisTemplateRedisTemplate
* 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 putAllhmset
*
* @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

@ -0,0 +1,258 @@
/*
* 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";
/*********************
*
* RedisKEY
*
*********************/
// 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
/**
* AgentUserHashKEY
*
* @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

@ -0,0 +1,43 @@
/*
* 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

@ -0,0 +1,127 @@
/*
* 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

@ -0,0 +1,156 @@
/*
* 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 com.cskefu.cc.proxy.LicenseProxy;
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());
}
// 初始化 ServerInstId
LicenseProxy licenseProxy = event.getApplicationContext().getBean(LicenseProxy.class);
licenseProxy.checkOnStartup();
} 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

@ -0,0 +1,48 @@
/*
* 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

@ -0,0 +1,37 @@
/*
* 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
}
}

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