初始化
commit
75582c2d4c
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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: 提供春松客服定制化开发、机器人客服平台等
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
---
|
||||
reviewers : cskefu/reviewers
|
||||
---
|
||||
|
||||
<!--- 在标题中简略说明问题 -->
|
||||
|
||||
## 描述
|
||||
<!--- 详细的描述变更 -->
|
||||
|
||||
### 关联 Issue #
|
||||
|
||||
## 解决的问题
|
||||
|
||||
<!--- 为什么变更是必要的? -->
|
||||
<!--- 如果这个PR解决了其他Issue,添加链接 -->
|
||||
|
||||
## 测试情况
|
||||
|
||||
<!--- 详细介绍怎么测试变更了 -->
|
||||
<!--- 介绍测试环境 -->
|
||||
<!--- 变更对其他代码的影响 -->
|
||||
|
||||
## 截屏
|
||||
|
||||
## 变更的类型
|
||||
|
||||
<!--- 变更有哪些特点,添加 `x` 到下面的对应项目中: -->
|
||||
|
||||
- [ ] 解决 Bug
|
||||
- [ ] 新功能(不影响其他功能)
|
||||
- [ ] 对其他功能有影响
|
||||
|
||||
## 检查
|
||||
|
||||
<!--- 检查下面,各项,添加 `x` 到下面的对应项目中: -->
|
||||
|
||||
- [ ] 我的变更和代码规范一致
|
||||
- [ ] 我的变更需要更新文档
|
||||
- [ ] 我已经更新了对应的文档
|
||||
- [ ] 我增加的代码有单元测试
|
||||
- [ ] 所有单元测试都能通过
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
---
|
||||
reviewers : cskefu/reviewers
|
||||
---
|
||||
|
||||
### Requirements for Contributing Documentation
|
||||
|
||||
## 变更说明
|
||||
|
||||
### 关联 Issue #
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
---
|
||||
reviewers : cskefu/reviewers
|
||||
---
|
||||
|
||||
### Requirements for Contributing a Performance Improvement
|
||||
|
||||
## 性能提升
|
||||
|
||||
### 关联 Issue #
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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/
|
||||
|
|
@ -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`) -->
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
# description
|
||||
|
||||
## parent #
|
||||
|
||||
# solution
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
# description
|
||||
|
||||
# Others
|
||||
|
||||
企业聊天机器人/产品需求汇总
|
||||
https://wiki.chatopera.com/pages/viewpage.action?pageId=4686818
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
# 描述
|
||||
|
||||
## 关联问题
|
||||
|
||||
<!-- BUG Issue 链接 -->
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
# 描述
|
||||
|
||||
## 新功能
|
||||
|
||||
<!-- 提交说明 -->
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
# 描述
|
||||
|
||||
## 关联问题
|
||||
|
||||
<!-- BUG Issue 链接 -->
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
# 描述
|
||||
|
||||
## 更新日志
|
||||
|
||||
<!-- 新版本 -->
|
||||
|
|
@ -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
|
||||
|
|
@ -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)。
|
||||
|
|
@ -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) 中。
|
||||
|
|
@ -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.
|
||||
|
|
@ -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>
|
||||
|
||||
# 春松客服
|
||||
|
||||
[](https://github.com/cskefu/cskefu/stargazers) [](https://github.com/cskefu/cskefu/network/members) [](https://www.cskefu.com/licenses/v1.html "开源许可协议") [](https://github.com/cskefu/cskefu/issues) [](https://github.com/cskefu/cskefu/issues?q=is%3Aissue+is%3Aclosed) [](https://hub.docker.com/r/chatopera/contact-center/) <!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
|
||||
[](#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 云服务快速让聊天机器人上线!
|
||||
上线机器人客服的两个方式:1)Chatopera 云服务,按量付费,提供每日免费额度;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) 查看二维码。
|
||||
|
||||
### 微信群
|
||||
|
||||
春松客服用户和开发者交流群。
|
||||
|
||||

|
||||
|
||||
### 微信公众号
|
||||
|
||||
及时获得产品更新、活动分享等信息,关注春松客服公众号。
|
||||
|
||||

|
||||
|
||||
## 鸣谢
|
||||
|
||||
[Amazon AWS 赞助春松客服服务器资源 5W RMB(2021 年度)](https://aws.amazon.com)
|
||||
|
||||
[IBM Cloud 赞助春松客服服务器资源 12W US Dollar(2019 年度)](https://cloud.ibm.com/)
|
||||
|
||||
[QingCloud 赞助春松客服服务器资源 1W RMB(2018 年度)](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)
|
||||
|
||||

|
||||
|
|
@ -0,0 +1,8 @@
|
|||
app/target
|
||||
!app/target/*.war.original
|
||||
!app/target/*.war
|
||||
!app/target/*.jar.original
|
||||
!app/target/*.jar
|
||||
logs/
|
||||
tmp/
|
||||
data/
|
||||
|
|
@ -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/
|
||||
|
|
@ -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"]
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
# Chatopera Contact Center
|
||||
|
||||
前三代呼叫中心均是以电话为主要的服务渠道。在 2000 年,伴随着互联网以及移动通信的发展与普及,将电子邮件、互联网、手机短信等渠道接入呼叫中心,成为第四代呼叫中心的标志。第四代呼叫中心也称为多媒体呼叫中心或联络中心(Contact Center)。它相对传统呼叫中心来说接入渠道丰富,同时引入了多渠道接入与多渠道统一排队等概念。
|
||||
|
||||
## 文档
|
||||
|
||||
<https://docs.chatopera.com/>
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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/
|
||||
|
|
@ -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>
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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());
|
||||
|
||||
/**
|
||||
* 发送消息给坐席
|
||||
* 如果没有AgentService或该AgentService没有坐席或AgentService在排队中,则不发送
|
||||
*/
|
||||
if (ctx.getAgentService() != null && (!ctx.isNoagent()) && !StringUtils.equals(
|
||||
MainContext.AgentUserStatusEnum.INQUENE.toString(),
|
||||
ctx.getAgentService().getStatus())) {
|
||||
// 通知消息到坐席
|
||||
MainContext.getPeerSyncIM().send(MainContext.ReceiverType.AGENT,
|
||||
MainContext.ChannelType.WEBIM,
|
||||
ctx.getAppid(),
|
||||
MainContext.MessageType.NEW,
|
||||
ctx.getAgentService().getAgentno(),
|
||||
ctx, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送消息给访客
|
||||
*/
|
||||
Message outMessage = new Message();
|
||||
outMessage.setAgentUser(ctx.getAgentUser());
|
||||
outMessage.setMessage(ctx.getMessage());
|
||||
outMessage.setMessageType(MainContext.MessageType.MESSAGE.toString());
|
||||
outMessage.setCalltype(MainContext.CallType.IN.toString());
|
||||
outMessage.setCreatetime(MainUtils.dateFormate.format(new Date()));
|
||||
outMessage.setNoagent(ctx.isNoagent());
|
||||
if (ctx.getAgentService() != null) {
|
||||
outMessage.setAgentserviceid(ctx.getAgentService().getId());
|
||||
}
|
||||
|
||||
MainContext.getPeerSyncIM().send(MainContext.ReceiverType.VISITOR,
|
||||
MainContext.ChannelType.toValue(ctx.getChannelType()),
|
||||
ctx.getAppid(),
|
||||
MainContext.MessageType.NEW, ctx.getOnlineUserId(), outMessage, true);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 邀请访客进入当前对话,如果当前操作的 坐席是已就绪状态,则直接加入到当前坐席的
|
||||
* 对话列表中,如果未登录,则分配给其他坐席
|
||||
*
|
||||
* @param agentno
|
||||
* @param agentUser
|
||||
* @throws Exception
|
||||
*/
|
||||
public void assignVisitorAsInvite(
|
||||
final String agentno,
|
||||
final AgentUser agentUser
|
||||
) throws Exception {
|
||||
final AgentStatus agentStatus = cache.findOneAgentStatusByAgentno(agentno);
|
||||
pickupAgentUserInQueue(agentUser, agentStatus);
|
||||
}
|
||||
|
||||
/**
|
||||
* 为坐席批量分配用户
|
||||
*
|
||||
* @param agentno
|
||||
*/
|
||||
public void assignVisitors(String agentno) {
|
||||
logger.info("[assignVisitors] agentno {}", agentno);
|
||||
// 获得目标坐席的状态
|
||||
AgentStatus agentStatus = SerializeUtil.deserialize(
|
||||
redisCommand.getHashKV(RedisKey.getAgentStatusReadyHashKey(), agentno));
|
||||
|
||||
if (agentStatus == null) {
|
||||
logger.warn("[assignVisitors] can not find AgentStatus for agentno {}", agentno);
|
||||
return;
|
||||
}
|
||||
logger.info("[assignVisitors] agentStatus id {}, status {}, service {}/{}, skills {}, busy {}",
|
||||
agentStatus.getId(), agentStatus.getStatus(), agentStatus.getUsers(), agentStatus.getMaxusers(),
|
||||
HashMapUtils.concatKeys(agentStatus.getSkills(), "|"), agentStatus.isBusy());
|
||||
|
||||
if ((!StringUtils.equals(
|
||||
MainContext.AgentStatusEnum.READY.toString(), agentStatus.getStatus())) || agentStatus.isBusy()) {
|
||||
// 该坐席处于非就绪状态,或该坐席处于置忙
|
||||
// 不分配坐席
|
||||
return;
|
||||
}
|
||||
|
||||
// 获得所有待服务访客的列表
|
||||
final Map<String, AgentUser> pendingAgentUsers = cache.getAgentUsersInQue();
|
||||
|
||||
// 本次批量分配访客数目
|
||||
Map<String, Integer> assigned = new HashMap<>();
|
||||
int currentAssigned = cache.getInservAgentUsersSizeByAgentno(
|
||||
agentStatus.getAgentno());
|
||||
|
||||
logger.info(
|
||||
"[assignVisitors] agentno {}, name {}, current assigned {}, batch size in queue {}",
|
||||
agentStatus.getAgentno(),
|
||||
agentStatus.getUsername(), currentAssigned, pendingAgentUsers.size());
|
||||
|
||||
for (Map.Entry<String, AgentUser> entry : pendingAgentUsers.entrySet()) {
|
||||
AgentUser agentUser = entry.getValue();
|
||||
boolean process = false;
|
||||
|
||||
if ((StringUtils.equals(agentUser.getAgentno(), agentno))) {
|
||||
// 待服务的访客指定了该坐席
|
||||
process = true;
|
||||
} else if (agentStatus != null &&
|
||||
agentStatus.getSkills() != null &&
|
||||
agentStatus.getSkills().size() > 0) {
|
||||
// 目标坐席有状态,并且坐席属于某技能组
|
||||
if ((StringUtils.isBlank(agentUser.getAgentno()) &&
|
||||
StringUtils.isBlank(agentUser.getSkill()))) {
|
||||
// 待服务的访客还没有指定坐席,并且也没有绑定技能组
|
||||
process = true;
|
||||
} else if (StringUtils.isBlank(agentUser.getAgentno()) &&
|
||||
agentStatus.getSkills().containsKey(agentUser.getSkill())) {
|
||||
// 待服务的访客还没有指定坐席,并且指定的技能组和该坐席的技能组一致
|
||||
process = true;
|
||||
}
|
||||
} else if (StringUtils.isBlank(agentUser.getAgentno()) &&
|
||||
StringUtils.isBlank(agentUser.getSkill())) {
|
||||
// 目标坐席没有状态,或该目标坐席有状态但是没有属于任何一个技能组
|
||||
// 待服务访客没有指定坐席,并且没有指定技能组
|
||||
process = true;
|
||||
}
|
||||
|
||||
if (!process) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 坐席未达到最大咨询访客数量,并且单次批量分配小于坐席就绪时分配最大访客数量(initMaxuser)
|
||||
final SessionConfig sessionConfig = acdPolicyService.initSessionConfig(agentUser.getSkill());
|
||||
if ((ACDServiceRouter.getAcdPolicyService().getAgentUsersBySkill(agentStatus, agentUser.getSkill()) < sessionConfig.getMaxuser()) && (assigned.getOrDefault(agentUser.getSkill(), 0) < sessionConfig.getInitmaxuser())) {
|
||||
assigned.merge(agentUser.getSkill(), 1, Integer::sum);
|
||||
pickupAgentUserInQueue(agentUser, agentStatus);
|
||||
} else {
|
||||
logger.info(
|
||||
"[assignVisitors] agentno {} reach the max users limit {}/{} or batch assign limit {}/{}",
|
||||
agentno,
|
||||
(currentAssigned + assigned.getOrDefault(agentUser.getSkill(), 0)),
|
||||
sessionConfig.getMaxuser(), assigned, sessionConfig.getInitmaxuser());
|
||||
break;
|
||||
}
|
||||
}
|
||||
agentStatusProxy.broadcastAgentsStatus("agent", "success", agentno);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从队列中选择访客进行会话
|
||||
*
|
||||
* @param agentUser
|
||||
* @param agentStatus
|
||||
* @return
|
||||
*/
|
||||
public AgentService pickupAgentUserInQueue(final AgentUser agentUser, final AgentStatus agentStatus) {
|
||||
// 从排队队列移除
|
||||
cache.deleteAgentUserInqueByAgentUserId(agentUser.getUserid());
|
||||
AgentService agentService = null;
|
||||
// 下面开始处理其加入到服务中的队列
|
||||
try {
|
||||
agentService = resolveAgentService(
|
||||
agentStatus, agentUser, false);
|
||||
|
||||
// 处理完成得到 agentService
|
||||
Message outMessage = new Message();
|
||||
outMessage.setMessage(acdMessageHelper.getSuccessMessage(
|
||||
agentService,
|
||||
agentUser.getChanneltype()));
|
||||
outMessage.setMessageType(MainContext.MediaType.TEXT.toString());
|
||||
outMessage.setCalltype(MainContext.CallType.IN.toString());
|
||||
outMessage.setCreatetime(MainUtils.dateFormate.format(new Date()));
|
||||
|
||||
if (StringUtils.isNotBlank(agentUser.getUserid())) {
|
||||
outMessage.setAgentUser(agentUser);
|
||||
outMessage.setChannelMessage(agentUser);
|
||||
|
||||
// 向访客推送消息
|
||||
peerSyncIM.send(
|
||||
MainContext.ReceiverType.VISITOR,
|
||||
MainContext.ChannelType.toValue(agentUser.getChanneltype()), agentUser.getAppid(),
|
||||
MainContext.MessageType.STATUS, agentUser.getUserid(), outMessage, true
|
||||
);
|
||||
|
||||
// 向坐席推送消息
|
||||
peerSyncIM.send(MainContext.ReceiverType.AGENT, MainContext.ChannelType.WEBIM,
|
||||
agentUser.getAppid(),
|
||||
MainContext.MessageType.NEW, agentUser.getAgentno(), outMessage, true);
|
||||
|
||||
// 通知更新在线数据
|
||||
agentStatusProxy.broadcastAgentsStatus("agent", "pickup", agentStatus.getAgentno());
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
logger.warn("[assignVisitors] fail to process service", ex);
|
||||
}
|
||||
return agentService;
|
||||
}
|
||||
|
||||
/**
|
||||
* 访客服务结束
|
||||
*
|
||||
* @param agentUser
|
||||
* @throws Exception
|
||||
*/
|
||||
public void finishAgentService(final AgentUser agentUser) {
|
||||
if (agentUser != null) {
|
||||
/**
|
||||
* 设置AgentUser
|
||||
*/
|
||||
// 获得坐席状态
|
||||
AgentStatus agentStatus = null;
|
||||
if (StringUtils.equals(MainContext.AgentUserStatusEnum.INSERVICE.toString(), agentUser.getStatus()) &&
|
||||
agentUser.getAgentno() != null) {
|
||||
agentStatus = cache.findOneAgentStatusByAgentno(agentUser.getAgentno());
|
||||
}
|
||||
|
||||
// 设置新AgentUser的状态
|
||||
agentUser.setStatus(MainContext.AgentUserStatusEnum.END.toString());
|
||||
if (agentUser.getServicetime() != null) {
|
||||
agentUser.setSessiontimes(System.currentTimeMillis() - agentUser.getServicetime().getTime());
|
||||
}
|
||||
|
||||
// 从缓存中删除agentUser缓存
|
||||
agentUserRes.save(agentUser);
|
||||
|
||||
final SessionConfig sessionConfig = acdPolicyService.initSessionConfig(agentUser.getSkill());
|
||||
|
||||
/**
|
||||
* 坐席服务
|
||||
*/
|
||||
AgentService service = null;
|
||||
if (StringUtils.isNotBlank(agentUser.getAgentserviceid())) {
|
||||
service = agentServiceRes.findById(agentUser.getAgentserviceid()).orElse(null);
|
||||
} else if (agentStatus != null) {
|
||||
// 该访客没有和坐席对话,因此没有 AgentService
|
||||
// 当做留言处理,创建一个新的 AgentService
|
||||
service = resolveAgentService(agentStatus, agentUser, true);
|
||||
}
|
||||
|
||||
if (service != null) {
|
||||
service.setStatus(MainContext.AgentUserStatusEnum.END.toString());
|
||||
service.setEndtime(new Date());
|
||||
if (service.getServicetime() != null) {
|
||||
service.setSessiontimes(System.currentTimeMillis() - service.getServicetime().getTime());
|
||||
}
|
||||
|
||||
final AgentUserTask agentUserTask = agentUserTaskRes.findById(agentUser.getId()).orElse(null);
|
||||
if (agentUserTask != null) {
|
||||
service.setAgentreplyinterval(agentUserTask.getAgentreplyinterval());
|
||||
service.setAgentreplytime(agentUserTask.getAgentreplytime());
|
||||
service.setAvgreplyinterval(agentUserTask.getAvgreplyinterval());
|
||||
service.setAvgreplytime(agentUserTask.getAvgreplytime());
|
||||
|
||||
service.setUserasks(agentUserTask.getUserasks());
|
||||
service.setAgentreplys(agentUserTask.getAgentreplys());
|
||||
|
||||
// 开启了质检,并且是有效对话
|
||||
if (sessionConfig.isQuality()) {
|
||||
// 未分配质检任务
|
||||
service.setQualitystatus(MainContext.QualityStatusEnum.NODIS.toString());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 启用了质检任务,开启质检
|
||||
*/
|
||||
if ((!sessionConfig.isQuality()) || service.getUserasks() == 0) {
|
||||
// 未开启质检 或无效对话无需质检
|
||||
service.setQualitystatus(MainContext.QualityStatusEnum.NO.toString());
|
||||
}
|
||||
agentServiceRes.save(service);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新AgentStatus
|
||||
*/
|
||||
if (agentStatus != null) {
|
||||
agentStatus.setUsers(
|
||||
cache.getInservAgentUsersSizeByAgentno(agentStatus.getAgentno()));
|
||||
agentStatusRes.save(agentStatus);
|
||||
}
|
||||
|
||||
Message outMessage = new Message();
|
||||
|
||||
/**
|
||||
* 发送到访客端的通知
|
||||
*/
|
||||
switch (MainContext.ChannelType.toValue(agentUser.getChanneltype())) {
|
||||
case WEBIM:
|
||||
// WebIM 发送对话结束事件
|
||||
// 向访客发送消息
|
||||
outMessage.setAgentStatus(agentStatus);
|
||||
outMessage.setMessage(acdMessageHelper.getServiceFinishMessage(agentUser.getChanneltype(), agentUser.getSkill()));
|
||||
outMessage.setMessageType(MainContext.AgentUserStatusEnum.END.toString());
|
||||
outMessage.setCalltype(MainContext.CallType.IN.toString());
|
||||
outMessage.setCreatetime(MainUtils.dateFormate.format(new Date()));
|
||||
outMessage.setAgentUser(agentUser);
|
||||
|
||||
// 向访客发送消息
|
||||
peerSyncIM.send(
|
||||
MainContext.ReceiverType.VISITOR,
|
||||
MainContext.ChannelType.toValue(agentUser.getChanneltype()), agentUser.getAppid(),
|
||||
MainContext.MessageType.STATUS, agentUser.getUserid(), outMessage, true
|
||||
);
|
||||
|
||||
if (agentStatus != null) {
|
||||
// 坐席在线,通知结束会话
|
||||
outMessage.setChannelMessage(agentUser);
|
||||
outMessage.setAgentUser(agentUser);
|
||||
peerSyncIM.send(MainContext.ReceiverType.AGENT, MainContext.ChannelType.WEBIM,
|
||||
agentUser.getAppid(),
|
||||
MainContext.MessageType.END, agentUser.getAgentno(), outMessage, true);
|
||||
}
|
||||
break;
|
||||
case PHONE:
|
||||
// 语音渠道,强制发送
|
||||
logger.info(
|
||||
"[finishAgentService] send notify to callout channel agentno {}", agentUser.getAgentno());
|
||||
NettyClients.getInstance().sendCalloutEventMessage(
|
||||
agentUser.getAgentno(), MainContext.MessageType.END.toString(), agentUser);
|
||||
break;
|
||||
case MESSENGER:
|
||||
outMessage.setAgentStatus(agentStatus);
|
||||
outMessage.setMessage(acdMessageHelper.getServiceFinishMessage(agentUser.getChanneltype(), agentUser.getSkill()));
|
||||
outMessage.setMessageType(MainContext.AgentUserStatusEnum.END.toString());
|
||||
outMessage.setCalltype(MainContext.CallType.IN.toString());
|
||||
outMessage.setCreatetime(MainUtils.dateFormate.format(new Date()));
|
||||
outMessage.setAgentUser(agentUser);
|
||||
|
||||
// 向访客发送消息
|
||||
peerSyncIM.send(
|
||||
MainContext.ReceiverType.VISITOR,
|
||||
MainContext.ChannelType.toValue(agentUser.getChanneltype()), agentUser.getAppid(),
|
||||
MainContext.MessageType.STATUS, agentUser.getUserid(), outMessage, true
|
||||
);
|
||||
|
||||
if (agentStatus != null) {
|
||||
// 坐席在线,通知结束会话
|
||||
outMessage.setChannelMessage(agentUser);
|
||||
outMessage.setAgentUser(agentUser);
|
||||
peerSyncIM.send(MainContext.ReceiverType.AGENT, MainContext.ChannelType.MESSENGER,
|
||||
agentUser.getAppid(),
|
||||
MainContext.MessageType.END, agentUser.getAgentno(), outMessage, true);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
logger.info(
|
||||
"[finishAgentService] ignore notify agent service end for channel {}, agent user id {}",
|
||||
agentUser.getChanneltype(), agentUser.getId());
|
||||
}
|
||||
|
||||
// 更新访客的状态为可以接收邀请
|
||||
final PassportWebIMUser passportWebIMUser = onlineUserRes.findOneByUserid(
|
||||
agentUser.getUserid());
|
||||
if (passportWebIMUser != null) {
|
||||
passportWebIMUser.setInvitestatus(MainContext.OnlineUserInviteStatus.DEFAULT.toString());
|
||||
onlineUserRes.save(passportWebIMUser);
|
||||
logger.info(
|
||||
"[finishAgentService] onlineUser id {}, status {}, invite status {}", passportWebIMUser.getId(),
|
||||
passportWebIMUser.getStatus(), passportWebIMUser.getInvitestatus());
|
||||
}
|
||||
|
||||
// 当前访客服务已经结束,为坐席寻找新访客
|
||||
if (agentStatus != null) {
|
||||
if ((ACDServiceRouter.getAcdPolicyService().getAgentUsersBySkill(agentStatus, agentUser.getSkill()) - 1) < sessionConfig.getMaxuser()) {
|
||||
assignVisitors(agentStatus.getAgentno());
|
||||
}
|
||||
}
|
||||
agentStatusProxy.broadcastAgentsStatus(
|
||||
"end", "success", agentUser != null ? agentUser.getId() : null);
|
||||
} else {
|
||||
logger.info("[finishAgentService] invalid agent user, should not be null");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 删除AgentUser
|
||||
* 包括数据库记录及缓存信息
|
||||
*
|
||||
* @param agentUser
|
||||
* @return
|
||||
*/
|
||||
public void finishAgentUser(final AgentUser agentUser) throws CSKefuException {
|
||||
logger.info("[finishAgentUser] userId {}", agentUser.getUserid());
|
||||
|
||||
if (agentUser == null || agentUser.getId() == null) {
|
||||
throw new CSKefuException("Invalid agentUser info");
|
||||
}
|
||||
|
||||
if (!StringUtils.equals(MainContext.AgentUserStatusEnum.END.toString(), agentUser.getStatus())) {
|
||||
/**
|
||||
* 未结束聊天,先结束对话,然后删除记录
|
||||
*/
|
||||
// 删除缓存
|
||||
finishAgentService(agentUser);
|
||||
}
|
||||
|
||||
// 删除数据库里的AgentUser记录
|
||||
agentUserRes.delete(agentUser);
|
||||
}
|
||||
|
||||
/**
|
||||
* 为agentUser生成对应的AgentService
|
||||
* 使用场景:
|
||||
* 1. 在AgentUser服务结束并且还没有对应的AgentService
|
||||
* 2. 在新服务开始,安排坐席
|
||||
*
|
||||
* @param agentStatus 坐席状态
|
||||
* @param agentUser 坐席访客会话
|
||||
* @param finished 结束服务
|
||||
* @return
|
||||
*/
|
||||
public AgentService resolveAgentService(
|
||||
AgentStatus agentStatus,
|
||||
final AgentUser agentUser,
|
||||
final boolean finished) {
|
||||
|
||||
AgentService agentService = new AgentService();
|
||||
if (StringUtils.isNotBlank(agentUser.getAgentserviceid())) {
|
||||
AgentService existAgentService = agentServiceRes.findById(agentUser.getAgentserviceid()).orElse(null);
|
||||
if (existAgentService != null) {
|
||||
agentService = existAgentService;
|
||||
} else {
|
||||
agentService.setId(agentUser.getAgentserviceid());
|
||||
}
|
||||
}
|
||||
final Date now = new Date();
|
||||
// 批量复制属性
|
||||
MainUtils.copyProperties(agentUser, agentService);
|
||||
agentService.setChanneltype(agentUser.getChanneltype());
|
||||
agentService.setSessionid(agentUser.getSessionid());
|
||||
|
||||
// 此处为何设置loginDate为现在
|
||||
agentUser.setLogindate(now);
|
||||
PassportWebIMUser passportWebIMUser = onlineUserRes.findOneByUserid(agentUser.getUserid());
|
||||
|
||||
if (finished == true) {
|
||||
// 服务结束
|
||||
agentUser.setStatus(MainContext.AgentUserStatusEnum.END.toString());
|
||||
agentService.setStatus(MainContext.AgentUserStatusEnum.END.toString());
|
||||
agentService.setSessiontype(MainContext.AgentUserStatusEnum.END.toString());
|
||||
if (agentStatus == null) {
|
||||
// 没有满足条件的坐席,留言
|
||||
agentService.setLeavemsg(true);
|
||||
agentService.setLeavemsgstatus(MainContext.LeaveMsgStatus.NOTPROCESS.toString()); //未处理的留言
|
||||
}
|
||||
|
||||
if (passportWebIMUser != null) {
|
||||
// 更新OnlineUser对象,变更为默认状态,可以接受邀请
|
||||
passportWebIMUser.setInvitestatus(MainContext.OnlineUserInviteStatus.DEFAULT.toString());
|
||||
}
|
||||
} else if (agentStatus != null) {
|
||||
agentService.setAgent(agentStatus.getAgentno());
|
||||
agentService.setSkill(agentUser.getSkill());
|
||||
agentUser.setStatus(MainContext.AgentUserStatusEnum.INSERVICE.toString());
|
||||
agentService.setStatus(MainContext.AgentUserStatusEnum.INSERVICE.toString());
|
||||
agentService.setSessiontype(MainContext.AgentUserStatusEnum.INSERVICE.toString());
|
||||
// 设置坐席名字
|
||||
agentService.setAgentno(agentStatus.getUserid());
|
||||
agentService.setAgentusername(agentStatus.getUsername());
|
||||
} else {
|
||||
// 不是服务结束,但是没有满足条件的坐席
|
||||
// 加入到排队中
|
||||
agentUser.setStatus(MainContext.AgentUserStatusEnum.INQUENE.toString());
|
||||
agentService.setStatus(MainContext.AgentUserStatusEnum.INQUENE.toString());
|
||||
agentService.setSessiontype(MainContext.AgentUserStatusEnum.INQUENE.toString());
|
||||
}
|
||||
|
||||
if (finished || agentStatus != null) {
|
||||
agentService.setAgentuserid(agentUser.getId());
|
||||
agentService.setInitiator(MainContext.ChatInitiatorType.USER.toString());
|
||||
|
||||
long waittingtime = 0;
|
||||
if (agentUser.getWaittingtimestart() != null) {
|
||||
waittingtime = System.currentTimeMillis() - agentUser.getWaittingtimestart().getTime();
|
||||
} else {
|
||||
if (agentUser.getCreatetime() != null) {
|
||||
waittingtime = System.currentTimeMillis() - agentUser.getCreatetime().getTime();
|
||||
}
|
||||
}
|
||||
|
||||
agentUser.setWaittingtime((int) waittingtime);
|
||||
agentUser.setServicetime(now);
|
||||
agentService.setOwner(agentUser.getOwner());
|
||||
agentService.setTimes(0);
|
||||
|
||||
final User agent = userRes.findById(agentService.getAgentno()).orElse(null);
|
||||
agentUser.setAgentname(agent.getUname());
|
||||
agentUser.setAgentno(agentService.getAgentno());
|
||||
|
||||
if (StringUtils.isNotBlank(agentUser.getName())) {
|
||||
agentService.setName(agentUser.getName());
|
||||
}
|
||||
if (StringUtils.isNotBlank(agentUser.getPhone())) {
|
||||
agentService.setPhone(agentUser.getPhone());
|
||||
}
|
||||
if (StringUtils.isNotBlank(agentUser.getEmail())) {
|
||||
agentService.setEmail(agentUser.getEmail());
|
||||
}
|
||||
if (StringUtils.isNotBlank(agentUser.getResion())) {
|
||||
agentService.setResion(agentUser.getResion());
|
||||
}
|
||||
|
||||
if (StringUtils.isNotBlank(agentUser.getSkill())) {
|
||||
agentService.setAgentskill(agentUser.getSkill());
|
||||
}
|
||||
|
||||
agentService.setServicetime(now);
|
||||
|
||||
if (agentUser.getCreatetime() != null) {
|
||||
agentService.setWaittingtime((int) (System.currentTimeMillis() - agentUser.getCreatetime().getTime()));
|
||||
agentUser.setWaittingtime(agentService.getWaittingtime());
|
||||
}
|
||||
if (passportWebIMUser != null) {
|
||||
agentService.setOsname(passportWebIMUser.getOpersystem());
|
||||
agentService.setBrowser(passportWebIMUser.getBrowser());
|
||||
// 记录onlineUser的id
|
||||
agentService.setDataid(passportWebIMUser.getId());
|
||||
}
|
||||
|
||||
agentService.setLogindate(agentUser.getCreatetime());
|
||||
agentServiceRes.save(agentService);
|
||||
|
||||
agentUser.setAgentserviceid(agentService.getId());
|
||||
agentUser.setLastgetmessage(now);
|
||||
agentUser.setLastmessage(now);
|
||||
}
|
||||
|
||||
agentService.setDataid(agentUser.getId());
|
||||
|
||||
/**
|
||||
* 分配成功以后, 将用户和坐席的对应关系放入到缓存
|
||||
* 将 AgentUser 放入到当前坐席的服务队列
|
||||
*/
|
||||
agentUserRes.save(agentUser);
|
||||
|
||||
/**
|
||||
* 更新OnlineUser对象,变更为服务中,不可邀请
|
||||
*/
|
||||
if (passportWebIMUser != null && !finished) {
|
||||
passportWebIMUser.setInvitestatus(MainContext.OnlineUserInviteStatus.INSERV.toString());
|
||||
onlineUserRes.save(passportWebIMUser);
|
||||
}
|
||||
|
||||
// 更新坐席服务人数,坐席更新时间到缓存
|
||||
if (agentStatus != null) {
|
||||
agentUserProxy.updateAgentStatus(agentStatus);
|
||||
}
|
||||
return agentService;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
||||
/**
|
||||
* 1)选择坐席,确定AgentService
|
||||
*/
|
||||
pipleline.use(acdVisServiceMw);
|
||||
|
||||
/**
|
||||
* 1)根据策略筛选坐席
|
||||
*/
|
||||
pipleline.use(acdVisAllocatorMw);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void enqueue(final ACDComposeContext ctx) {
|
||||
try {
|
||||
pipleline.handle(ctx);
|
||||
} catch (Compose4jRuntimeException e) {
|
||||
logger.error("[enqueueVisitor] error", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dequeue(ACDComposeContext ctx) {
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
||||
|
||||
/**
|
||||
* 通过 AgentUser获得ComposeContext
|
||||
*
|
||||
* @param agentUser
|
||||
* @param isInvite
|
||||
* @param initiator
|
||||
* @return
|
||||
*/
|
||||
public ACDComposeContext getComposeContextWithAgentUser(final AgentUser agentUser, final boolean isInvite, final String initiator) {
|
||||
ACDComposeContext ctx = new ACDComposeContext();
|
||||
ctx.setOnlineUserId(agentUser.getUserid());
|
||||
ctx.setOnlineUserNickname(agentUser.getNickname());
|
||||
ctx.setOrganid(agentUser.getSkill());
|
||||
ctx.setChannelType(agentUser.getChanneltype());
|
||||
ctx.setAgentno(agentUser.getAgentno());
|
||||
ctx.setBrowser(agentUser.getBrowser());
|
||||
ctx.setOsname(agentUser.getOsname());
|
||||
ctx.setAppid(agentUser.getAppid());
|
||||
ctx.setTitle(agentUser.getTitle());
|
||||
ctx.setSessionid(agentUser.getSessionid());
|
||||
ctx.setUrl(agentUser.getUrl());
|
||||
ctx.setOwnerid(agentUser.getOwner());
|
||||
|
||||
if (StringUtils.isNotBlank(agentUser.getIpaddr())) {
|
||||
ctx.setIp(agentUser.getIpaddr());
|
||||
// TODO set IP Data
|
||||
ctx.setIpdata(IPTools.getInstance().findGeography(agentUser.getIpaddr()));
|
||||
}
|
||||
|
||||
ctx.setInvite(isInvite);
|
||||
ctx.setInitiator(initiator);
|
||||
|
||||
return ctx;
|
||||
}
|
||||
|
||||
/**
|
||||
* 通知消息内容:分配到坐席
|
||||
*
|
||||
* @param agentService
|
||||
* @param channel
|
||||
* @return
|
||||
*/
|
||||
public String getSuccessMessage(AgentService agentService, String channel) {
|
||||
String queneTip = "<span id='agentno'>" + agentService.getAgentusername() + "</span>";
|
||||
if (!MainContext.ChannelType.WEBIM.toString().equals(channel)) {
|
||||
queneTip = agentService.getAgentusername();
|
||||
}
|
||||
SessionConfig sessionConfig = acdPolicyService.initSessionConfig(agentService.getSkill());
|
||||
String successMsg = "坐席分配成功," + queneTip + "为您服务。";
|
||||
if (StringUtils.isNotBlank(sessionConfig.getSuccessmsg())) {
|
||||
successMsg = sessionConfig.getSuccessmsg().replaceAll("\\{agent\\}", queneTip);
|
||||
}
|
||||
return successMsg;
|
||||
}
|
||||
|
||||
/**
|
||||
* 通知消息内容:和坐席断开
|
||||
*
|
||||
* @param channel
|
||||
* @return
|
||||
*/
|
||||
public String getServiceFinishMessage(String channel, String organid) {
|
||||
SessionConfig sessionConfig = acdPolicyService.initSessionConfig(organid);
|
||||
String queneTip = "坐席已断开和您的对话";
|
||||
if (StringUtils.isNotBlank(sessionConfig.getFinessmsg())) {
|
||||
queneTip = sessionConfig.getFinessmsg();
|
||||
}
|
||||
return queneTip;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 通知消息内容:和坐席断开,刷新页面
|
||||
*
|
||||
* @param channel
|
||||
* @return
|
||||
*/
|
||||
public String getServiceOffMessage(String channel, String organid) {
|
||||
SessionConfig sessionConfig = acdPolicyService.initSessionConfig(organid);
|
||||
String queneTip = "坐席已断开和您的对话,刷新页面为您分配新的坐席";
|
||||
if (StringUtils.isNotBlank(sessionConfig.getFinessmsg())) {
|
||||
queneTip = sessionConfig.getFinessmsg();
|
||||
}
|
||||
return queneTip;
|
||||
}
|
||||
|
||||
public String getNoAgentMessage(int queneIndex, String channel, String organid) {
|
||||
if (queneIndex < 0) {
|
||||
queneIndex = 0;
|
||||
}
|
||||
String queneTip = "<span id='queneindex'>" + queneIndex + "</span>";
|
||||
if (!MainContext.ChannelType.WEBIM.toString().equals(channel)) {
|
||||
queneTip = String.valueOf(queneIndex);
|
||||
}
|
||||
SessionConfig sessionConfig = acdPolicyService.initSessionConfig(organid);
|
||||
String noAgentTipMsg = "坐席全忙,已进入等待队列,您也可以在其他时间再来咨询。";
|
||||
if (StringUtils.isNotBlank(sessionConfig.getNoagentmsg())) {
|
||||
noAgentTipMsg = sessionConfig.getNoagentmsg().replaceAll("\\{num\\}", queneTip);
|
||||
}
|
||||
return noAgentTipMsg;
|
||||
}
|
||||
|
||||
public String getQueneMessage(int queneIndex, String channel, String organid) {
|
||||
|
||||
String queneTip = "<span id='queneindex'>" + queneIndex + "</span>";
|
||||
if (!MainContext.ChannelType.WEBIM.toString().equals(channel)) {
|
||||
queneTip = String.valueOf(queneIndex);
|
||||
}
|
||||
SessionConfig sessionConfig = acdPolicyService.initSessionConfig(organid);
|
||||
String agentBusyTipMsg = "正在排队,请稍候,在您之前,还有 " + queneTip + " 位等待用户。";
|
||||
if (StringUtils.isNotBlank(sessionConfig.getAgentbusymsg())) {
|
||||
agentBusyTipMsg = sessionConfig.getAgentbusymsg().replaceAll("\\{num\\}", queneTip);
|
||||
}
|
||||
return agentBusyTipMsg;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 构建WebIM分发的Context
|
||||
*
|
||||
* @param onlineUserId
|
||||
* @param nickname
|
||||
* @param session
|
||||
* @param appid
|
||||
* @param ip
|
||||
* @param osname
|
||||
* @param browser
|
||||
* @param headimg
|
||||
* @param ipdata
|
||||
* @param channel
|
||||
* @param skill
|
||||
* @param agent
|
||||
* @param title
|
||||
* @param url
|
||||
* @param traceid
|
||||
* @param ownerid
|
||||
* @param isInvite
|
||||
* @param initiator
|
||||
* @return
|
||||
*/
|
||||
public static ACDComposeContext getWebIMComposeContext(
|
||||
final String onlineUserId,
|
||||
final String nickname,
|
||||
final String session,
|
||||
final String appid,
|
||||
final String ip,
|
||||
final String osname,
|
||||
final String browser,
|
||||
final String headimg,
|
||||
final IP ipdata,
|
||||
final String channel,
|
||||
final String skill,
|
||||
final String agent,
|
||||
final String title,
|
||||
final String url,
|
||||
final String traceid,
|
||||
final String ownerid,
|
||||
final boolean isInvite,
|
||||
final String initiator) {
|
||||
logger.info(
|
||||
"[enqueueVisitor] user {}, appid {}, agent {}, skill {}, nickname {}, initiator {}, isInvite {}",
|
||||
onlineUserId,
|
||||
appid,
|
||||
agent,
|
||||
skill,
|
||||
nickname, initiator, isInvite);
|
||||
|
||||
// 坐席服务请求,分配 坐席
|
||||
final ACDComposeContext ctx = new ACDComposeContext();
|
||||
ctx.setOnlineUserId(onlineUserId);
|
||||
ctx.setOnlineUserNickname(nickname);
|
||||
ctx.setOrganid(skill);
|
||||
ctx.setChannelType(channel);
|
||||
ctx.setAgentno(agent);
|
||||
ctx.setBrowser(browser);
|
||||
ctx.setOsname(osname);
|
||||
ctx.setAppid(appid);
|
||||
ctx.setTitle(title);
|
||||
ctx.setSessionid(session);
|
||||
ctx.setUrl(url);
|
||||
ctx.setOnlineUserHeadimgUrl(headimg);
|
||||
ctx.setTraceid(traceid);
|
||||
ctx.setOwnerid(ownerid);
|
||||
ctx.setInitiator(initiator);
|
||||
ctx.setIpdata(ipdata);
|
||||
ctx.setIp(ip);
|
||||
ctx.setInvite(isInvite);
|
||||
return ctx;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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 新创建的AgentUser不需要设置Status和Agentno
|
||||
* 因为两个值在后面会检查,如果存在则不会申请新的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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
@ -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 {
|
||||
}
|
||||
}
|
||||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
|
@ -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 {
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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 Token的Redis连接
|
||||
*/
|
||||
public class AuthRedisTemplate extends RedisTemplate<String, String> {
|
||||
public AuthRedisTemplate() {
|
||||
RedisSerializer<String> stringSerializer = new StringRedisSerializer();
|
||||
this.setKeySerializer(stringSerializer);
|
||||
this.setValueSerializer(stringSerializer);
|
||||
this.setHashKeySerializer(stringSerializer);
|
||||
this.setHashValueSerializer(stringSerializer);
|
||||
}
|
||||
|
||||
public AuthRedisTemplate(RedisConnectionFactory connectionFactory) {
|
||||
this();
|
||||
this.setConnectionFactory(connectionFactory);
|
||||
this.afterPropertiesSet();
|
||||
}
|
||||
|
||||
protected RedisConnection preProcessConnection(RedisConnection connection, boolean existingConnection) {
|
||||
return new DefaultStringRedisConnection(connection);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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));
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据租户ID和认证Auth获得一个登录用户
|
||||
*
|
||||
* @param token
|
||||
* @return
|
||||
*/
|
||||
public User retrieve(final String token) {
|
||||
String serialized = redisValOps.get(resolveTokenKey(token));
|
||||
if (StringUtils.isNotBlank(serialized)) {
|
||||
return (User) SerializeUtil.deserialize(serialized);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 登出已经登录的系统用户
|
||||
*
|
||||
* @param token
|
||||
*/
|
||||
public void delete(final String token) {
|
||||
authRedisTemplate.delete(resolveTokenKey(token));
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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"))) ;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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));
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据ID和租户ID获得CallCenterAgent
|
||||
*
|
||||
* @param id
|
||||
* @return
|
||||
*/
|
||||
public CallCenterAgent findOneCallCenterAgentById(final String id) {
|
||||
String serialized = redisCommand.getHashKV(RedisKey.getCallCenterAgentHashKey(), id);
|
||||
if (StringUtils.isNotBlank(serialized)) {
|
||||
return (CallCenterAgent) SerializeUtil.deserialize(serialized);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除CallCenterAgent
|
||||
*
|
||||
* @param id
|
||||
*/
|
||||
public void deleteCallCenterAgentById(final String id) {
|
||||
redisCommand.delHashKV(RedisKey.getCallCenterAgentHashKey(), id);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 根据租户ID获得所有的CallCenterAgent
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public Map<String, CallCenterAgent> findAllCallCenterAgents() {
|
||||
Map<String, String> map = redisCommand.getHash(RedisKey.getCallCenterAgentHashKey());
|
||||
Map<String, CallCenterAgent> result = new HashMap<>();
|
||||
|
||||
for (Map.Entry<String, String> entry : map.entrySet()) {
|
||||
result.put(entry.getKey(), SerializeUtil.deserialize(entry.getValue()));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 访客黑名单
|
||||
*/
|
||||
// 将访客放在租户的黑名单中
|
||||
public void putBlackEntity(final BlackEntity blackEntity) {
|
||||
redisCommand.setHashKV(
|
||||
RedisKey.getBlackEntityKey(), blackEntity.getUserid(), SerializeUtil.serialize(blackEntity));
|
||||
}
|
||||
|
||||
// 通过指定的访客和租户查找黑名单
|
||||
public Optional<BlackEntity> findOneBlackEntityByUserId(final String userid) {
|
||||
String ser = redisCommand.getHashKV(RedisKey.getBlackEntityKey(), userid);
|
||||
if (StringUtils.isBlank(ser)) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
return Optional.ofNullable(SerializeUtil.deserialize(ser));
|
||||
}
|
||||
|
||||
// 将一个访客从黑名单中移除
|
||||
public void deleteBlackEntityByUserId(final String userid) {
|
||||
redisCommand.delHashKV(RedisKey.getBlackEntityKey(), userid);
|
||||
}
|
||||
|
||||
// 指定的访客是否在租户的黑名单中
|
||||
public boolean existBlackEntityByUserId(final String userid) {
|
||||
return redisCommand.hasHashKV(RedisKey.getBlackEntityKey(), userid);
|
||||
}
|
||||
|
||||
// 根据租户ID获得所有访客的黑名单
|
||||
public Map<String, BlackEntity> findAllBlackEntity() {
|
||||
Map<String, BlackEntity> result = new HashMap<>();
|
||||
for (Map.Entry<String, String> entry : redisCommand.getHash(
|
||||
RedisKey.getBlackEntityKey()).entrySet()) {
|
||||
result.put(entry.getKey(), SerializeUtil.deserialize(entry.getValue()));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
/*****************************
|
||||
* Job 相关
|
||||
*****************************/
|
||||
public void putJobById(final String jobId, final JobDetail job) {
|
||||
redisCommand.setHashKV(RedisKey.getJobHashKey(), jobId, SerializeUtil.serialize(job));
|
||||
}
|
||||
|
||||
public JobDetail findOneJobById(final String jobId) {
|
||||
String serialized = redisCommand.getHashKV(RedisKey.getJobHashKey(), jobId);
|
||||
|
||||
if (StringUtils.isNotBlank(serialized)) {
|
||||
return (JobDetail) SerializeUtil.deserialize(serialized);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public boolean existJobById(final String jobId) {
|
||||
return redisCommand.hasHashKV(RedisKey.getJobHashKey(), jobId);
|
||||
}
|
||||
|
||||
public void deleteJobByJobId(final String jobId) {
|
||||
redisCommand.delHashKV(RedisKey.getJobHashKey(), jobId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 系统词典相关
|
||||
*/
|
||||
// 存储根词典
|
||||
public void putSysDic(final String id, final SysDic sysDic) {
|
||||
redisCommand.setHashKV(RedisKey.getSysDicHashKey(), id, SerializeUtil.serialize(sysDic));
|
||||
}
|
||||
|
||||
// 将指定租户的系统词典清空
|
||||
public void eraseSysDic() {
|
||||
redisCommand.delete(RedisKey.getSysDicHashKey());
|
||||
}
|
||||
|
||||
// 存储词典子项
|
||||
public void putSysDic(final String code, final List<SysDic> sysDics) {
|
||||
redisCommand.setHashKV(RedisKey.getSysDicHashKey(), code, SerializeUtil.serialize(sysDics));
|
||||
}
|
||||
|
||||
// 获得词典的子项列表
|
||||
public List<SysDic> getSysDicItemsByCode(final String code) {
|
||||
String serialized = redisCommand.getHashKV(RedisKey.getSysDicHashKey(), code);
|
||||
if (serialized != null) {
|
||||
return (List<SysDic>) SerializeUtil.deserialize(serialized);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// 获得词典子项
|
||||
public SysDic findOneSysDicByCode(final String code) {
|
||||
String serialized = redisCommand.getHashKV(RedisKey.getSysDicHashKey(), code);
|
||||
|
||||
if (StringUtils.isBlank(serialized)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (SysDic) SerializeUtil.deserialize(serialized);
|
||||
}
|
||||
|
||||
// 获得词典
|
||||
public SysDic findOneSysDicById(final String id) {
|
||||
String serialized = redisCommand.getHashKV(RedisKey.getSysDicHashKey(), id);
|
||||
|
||||
if (StringUtils.isBlank(serialized)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (SysDic) SerializeUtil.deserialize(serialized);
|
||||
}
|
||||
|
||||
// 批量存储
|
||||
public void putSysDic(List<SysDic> vals) {
|
||||
Map<String, String> map = new HashMap<>();
|
||||
for (final SysDic dic : vals) {
|
||||
map.put(dic.getId(), SerializeUtil.serialize(dic));
|
||||
}
|
||||
redisCommand.hmset(RedisKey.getSysDicHashKey(), map);
|
||||
}
|
||||
|
||||
public void deleteSysDicById(final String id) {
|
||||
redisCommand.delHashKV(RedisKey.getSysDicHashKey(), id);
|
||||
}
|
||||
|
||||
public boolean existSysDicById(final String id) {
|
||||
return redisCommand.hasHashKV(RedisKey.getSysDicHashKey(), id);
|
||||
}
|
||||
|
||||
/**
|
||||
* System 相关
|
||||
*/
|
||||
public <T extends Serializable> void putSystemById(final String id, final T obj) {
|
||||
redisCommand.setHashKV(RedisKey.getSystemHashKey(), id, SerializeUtil.serialize(obj));
|
||||
}
|
||||
|
||||
public <T extends Serializable> void putSystemListById(final String id, final List<T> obj) {
|
||||
redisCommand.setHashKV(RedisKey.getSystemHashKey(), id, SerializeUtil.serialize(obj));
|
||||
}
|
||||
|
||||
public <TK, TV extends Serializable> void putSystemMapById(final String id, final Map<TK, TV> obj) {
|
||||
redisCommand.setHashKV(RedisKey.getSystemHashKey(), id, SerializeUtil.serialize(obj));
|
||||
}
|
||||
|
||||
public boolean existSystemById(final String id) {
|
||||
return redisCommand.hasHashKV(RedisKey.getSystemHashKey(), id);
|
||||
}
|
||||
|
||||
public void deleteSystembyId(final String id) {
|
||||
redisCommand.delHashKV(RedisKey.getSystemHashKey(), id);
|
||||
}
|
||||
|
||||
public <T extends Serializable> T findOneSystemById(final String id) {
|
||||
String serialized = redisCommand.getHashKV(RedisKey.getSystemHashKey(), id);
|
||||
if (StringUtils.isNotBlank(serialized)) {
|
||||
return (T) SerializeUtil.deserialize(serialized);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public <T extends Serializable> List<T> findOneSystemListById(final String id) {
|
||||
String serialized = redisCommand.getHashKV(RedisKey.getSystemHashKey(), id);
|
||||
if (StringUtils.isNotBlank(serialized)) {
|
||||
return (List<T>) SerializeUtil.deserialize(serialized);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public <TK, TV extends Serializable> Map<TK, TV> findOneSystemMapById(final String id) {
|
||||
String serialized = redisCommand.getHashKV(RedisKey.getSystemHashKey(), id);
|
||||
if (StringUtils.isNotBlank(serialized)) {
|
||||
return (Map<TK, TV>) SerializeUtil.deserialize(serialized);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// 获得系统cache的列表大小
|
||||
public int getSystemSize() {
|
||||
return redisCommand.getHashSize(RedisKey.getSystemHashKey());
|
||||
}
|
||||
|
||||
/**************************
|
||||
* Session Config 相关
|
||||
**************************/
|
||||
|
||||
public void putSessionConfig(final SessionConfig sessionConfig, String organid) {
|
||||
redisCommand.put(RedisKey.getSessionConfig(organid), SerializeUtil.serialize(sessionConfig));
|
||||
}
|
||||
|
||||
public SessionConfig findOneSessionConfig(String organid) {
|
||||
String serialized = redisCommand.get(RedisKey.getSessionConfig(organid));
|
||||
if (StringUtils.isNotBlank(serialized)) {
|
||||
return (SessionConfig) SerializeUtil.deserialize(serialized);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public void deleteSessionConfig(String organid) {
|
||||
redisCommand.delete(RedisKey.getSessionConfig(organid));
|
||||
}
|
||||
|
||||
public boolean existSessionConfig(String organid) {
|
||||
return redisCommand.exists(RedisKey.getSessionConfig(organid));
|
||||
}
|
||||
|
||||
public void putSessionConfigList(final List<SessionConfig> lis) {
|
||||
redisCommand.put(RedisKey.getSessionConfigList(), SerializeUtil.serialize(lis));
|
||||
}
|
||||
|
||||
public List<SessionConfig> findOneSessionConfigList() {
|
||||
String serialized = redisCommand.get(RedisKey.getSessionConfigList());
|
||||
if (StringUtils.isNotBlank(serialized)) {
|
||||
return (List<SessionConfig>) SerializeUtil.deserialize(serialized);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public void deleteSessionConfigList() {
|
||||
redisCommand.delete(RedisKey.getSessionConfigList());
|
||||
}
|
||||
|
||||
public boolean existSessionConfigList() {
|
||||
return redisCommand.exists(RedisKey.getSessionConfigList());
|
||||
}
|
||||
|
||||
/******************************************
|
||||
* Customer Chats Audit 相关
|
||||
******************************************/
|
||||
public void putAgentUserAudit(final AgentUserAudit audit) throws CSKefuCacheException {
|
||||
if (StringUtils.isBlank(audit.getAgentUserId())) {
|
||||
throw new CSKefuCacheException("agentUserId is required.");
|
||||
}
|
||||
redisCommand.setHashKV(
|
||||
RedisKey.getCustomerChatsAuditKey(), audit.getAgentUserId(), SerializeUtil.serialize(audit));
|
||||
}
|
||||
|
||||
public void deleteAgentUserAuditById(final String agentUserId) {
|
||||
redisCommand.delHashKV(RedisKey.getCustomerChatsAuditKey(), agentUserId);
|
||||
}
|
||||
|
||||
public Optional<AgentUserAudit> findOneAgentUserAuditById(final String agentUserId) {
|
||||
logger.info("[findOneAgentUserAuditById] agentUserId {}", agentUserId);
|
||||
String serialized = redisCommand.getHashKV(RedisKey.getCustomerChatsAuditKey(), agentUserId);
|
||||
if (StringUtils.isBlank(serialized)) {
|
||||
return Optional.empty();
|
||||
}
|
||||
return Optional.ofNullable((AgentUserAudit) SerializeUtil.deserialize(serialized));
|
||||
}
|
||||
|
||||
public boolean existAgentUserAuditById(final String agentUserId) {
|
||||
return redisCommand.hasHashKV(RedisKey.getCustomerChatsAuditKey(), agentUserId);
|
||||
}
|
||||
|
||||
|
||||
/******************************************
|
||||
* User Session 相关
|
||||
******************************************/
|
||||
/**
|
||||
* 存入user的session,存储这组信息是为了让客户的账号只能在一个浏览器内登录使用
|
||||
* 如果一个用户账号在多个浏览器使用,则登出之前的登录,只保留最后一个登录正常使用
|
||||
*
|
||||
* @param agentno
|
||||
* @param sessionId
|
||||
*/
|
||||
public void putUserSessionByAgentnoAndSessionId(final String agentno, final String sessionId) {
|
||||
redisCommand.setHashKV(RedisKey.getUserSessionKey(), agentno, sessionId);
|
||||
}
|
||||
|
||||
public boolean existUserSessionByAgentno(final String agentno) {
|
||||
return redisCommand.hasHashKV(RedisKey.getUserSessionKey(), agentno);
|
||||
}
|
||||
|
||||
public String findOneSessionIdByAgentno(final String agentno) {
|
||||
return redisCommand.getHashKV(RedisKey.getUserSessionKey(), agentno);
|
||||
}
|
||||
|
||||
public void deleteUserSessionByAgentno(final String agentno) {
|
||||
redisCommand.delHashKV(RedisKey.getUserSessionKey(), agentno);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
||||
/**
|
||||
* 使用StringRedisTemplate而不是RedisTemplate解决序列化问题
|
||||
* https://stackoverflow.com/questions/13215024/weird-redis-key-with-spring-data-jedis
|
||||
*/
|
||||
@Autowired
|
||||
private StringRedisTemplate redis;
|
||||
|
||||
@PostConstruct
|
||||
private void init() {
|
||||
redisListOps = redis.opsForList();
|
||||
redisHashOps = redis.opsForHash();
|
||||
redisValOps = redis.opsForValue();
|
||||
redisSetOps = redis.opsForSet();
|
||||
}
|
||||
|
||||
|
||||
/*****************************
|
||||
* String 相关
|
||||
*****************************/
|
||||
|
||||
/**
|
||||
* 设置一个KEY
|
||||
*
|
||||
* @param key
|
||||
* @param serialized
|
||||
*/
|
||||
public void put(final String key, final String serialized) {
|
||||
boolean result = true;
|
||||
redisValOps.set(key, serialized);
|
||||
}
|
||||
|
||||
public String get(final String key) {
|
||||
return redisValOps.get(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除一个KEY
|
||||
*
|
||||
* @param key
|
||||
*/
|
||||
public void delete(final String key) {
|
||||
redis.delete(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置一个KEY的过期时间
|
||||
*
|
||||
* @param key
|
||||
* @param seconds
|
||||
*/
|
||||
public void expire(final String key, final long seconds) {
|
||||
redis.expire(key, seconds, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获得一个KEY的过期时间
|
||||
*
|
||||
* @param key
|
||||
* @return
|
||||
*/
|
||||
public long ttl(final String key) {
|
||||
return redis.getExpire(key);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 是否存在一个KEY
|
||||
*
|
||||
* @param key
|
||||
* @return
|
||||
*/
|
||||
public boolean exists(final String key) {
|
||||
return redis.hasKey(key);
|
||||
}
|
||||
|
||||
|
||||
/*****************************
|
||||
* List 相关
|
||||
*****************************/
|
||||
|
||||
|
||||
/**
|
||||
* 在列表右侧追加
|
||||
*
|
||||
* @param key
|
||||
* @param val
|
||||
* @return
|
||||
*/
|
||||
public long appendList(final String key, final String val) {
|
||||
return redisListOps.rightPush(key, val);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获得一个列表的所有元素
|
||||
*
|
||||
* @param key
|
||||
* @return
|
||||
*/
|
||||
public List<String> getList(final String key) {
|
||||
return redisListOps.range(key, 0, redisListOps.size(key));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获得一个类标的长度
|
||||
*
|
||||
* @param key
|
||||
* @return
|
||||
*/
|
||||
public long listSize(final String key) {
|
||||
return redisListOps.size(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all value = val in key
|
||||
*
|
||||
* @param key
|
||||
* @param val
|
||||
*/
|
||||
public void listRemove(final String key, final String val) {
|
||||
redisListOps.remove(key, 0, val);
|
||||
}
|
||||
|
||||
|
||||
/*****************************
|
||||
* Hash 相关
|
||||
*****************************/
|
||||
|
||||
/**
|
||||
* 获得一个Hash的列表长度
|
||||
*
|
||||
* @param hashKey
|
||||
* @return
|
||||
*/
|
||||
public int getHashSize(final String hashKey) {
|
||||
return Math.toIntExact(redisHashOps.size(hashKey));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获得多个Hash的全部成员
|
||||
*
|
||||
* @param keys
|
||||
* @return
|
||||
*/
|
||||
public Map<String, String> getAllMembersInMultiHash(final List<String> keys) {
|
||||
return redis.execute((RedisCallback<Map<String, String>>) con -> {
|
||||
Map<String, String> ans = new HashMap<>();
|
||||
for (String key : keys) {
|
||||
Map<byte[], byte[]> result = con.hGetAll(key.getBytes());
|
||||
if (!CollectionUtils.isEmpty(result)) {
|
||||
for (Map.Entry<byte[], byte[]> entry : result.entrySet()) {
|
||||
ans.put(new String(entry.getKey()), new String(entry.getValue()));
|
||||
}
|
||||
}
|
||||
}
|
||||
return ans;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获得一个Hash中所有的值
|
||||
* https://juejin.im/post/5c1399a7f265da61764ac526
|
||||
*
|
||||
* @param hashKey
|
||||
* @return
|
||||
*/
|
||||
public Map<String, String> getHash(final String hashKey) {
|
||||
return redis.execute((RedisCallback<Map<String, String>>) con -> {
|
||||
Map<byte[], byte[]> result = con.hGetAll(hashKey.getBytes());
|
||||
if (CollectionUtils.isEmpty(result)) {
|
||||
return new HashMap<>(0);
|
||||
}
|
||||
|
||||
Map<String, String> ans = new HashMap<>(result.size());
|
||||
for (Map.Entry<byte[], byte[]> entry : result.entrySet()) {
|
||||
ans.put(new String(entry.getKey()), new String(entry.getValue()));
|
||||
}
|
||||
return ans;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置Hash Map KV
|
||||
*
|
||||
* @param hashKey
|
||||
* @param childKey
|
||||
* @param childVal
|
||||
*/
|
||||
public void setHashKV(final String hashKey, final String childKey, final String childVal) {
|
||||
redisHashOps.put(hashKey, childKey, childVal);
|
||||
}
|
||||
|
||||
/**
|
||||
* 指定的Hash实存存在键
|
||||
*
|
||||
* @param key
|
||||
* @param childKey
|
||||
* @return
|
||||
*/
|
||||
public boolean hasHashKV(String key, String childKey) {
|
||||
return redisHashOps.hasKey(key, childKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获得Hash Map KV的值
|
||||
*
|
||||
* @param hashKey
|
||||
* @param childKey
|
||||
* @return
|
||||
*/
|
||||
public String getHashKV(final String hashKey, final String childKey) {
|
||||
return redisHashOps.get(hashKey, childKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除Hash Map KV的值
|
||||
*
|
||||
* @param hashKey
|
||||
* @param childKey
|
||||
*/
|
||||
public void delHashKV(final String hashKey, final String childKey) {
|
||||
redisHashOps.delete(hashKey, childKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* HashSet
|
||||
* https://www.cnblogs.com/hongdada/p/9141125.html
|
||||
* 寻求使用 hmset 提升大量HASH KEY的存储 https://redis.io/commands/hmset
|
||||
* TODO 查看 putAll源代码确定是用hmset
|
||||
*
|
||||
* @param key 键
|
||||
* @param map 对应多个键值
|
||||
*/
|
||||
public void hmset(final String key, final Map<String, String> map) {
|
||||
try {
|
||||
redisHashOps.putAll(key, map);
|
||||
} catch (Exception e) {
|
||||
logger.error("hmset bad things happen", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/*****************************
|
||||
* Set 相关
|
||||
*****************************/
|
||||
|
||||
public void insertSetVal(final String key, final String val) {
|
||||
redisSetOps.add(key, val);
|
||||
}
|
||||
|
||||
public void removeSetVal(final String key, final String val) {
|
||||
redisSetOps.remove(key, val);
|
||||
}
|
||||
|
||||
public int getSetSize(final String key) {
|
||||
return Math.toIntExact(redisSetOps.size(key));
|
||||
}
|
||||
|
||||
public List<String> getSet(final String key) {
|
||||
Set<String> s = redisSetOps.members(key);
|
||||
|
||||
if (CollectionUtils.isEmpty(s)) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
return new ArrayList<>(s);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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";
|
||||
|
||||
/*********************
|
||||
*
|
||||
* 以下为Redis的常用KEY管理
|
||||
*
|
||||
*********************/
|
||||
|
||||
// AGENT STATUS 相关
|
||||
|
||||
/**
|
||||
* 获得坐席列表指定字符串状态的KEY
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public static String getAgentStatusHashKeyByStatusStr(final String status) {
|
||||
StringBuffer sb = new StringBuffer();
|
||||
sb.append("agent:status:");
|
||||
sb.append(status);
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 就绪的客服列表KEY
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public static String getAgentStatusReadyHashKey() {
|
||||
return getAgentStatusHashKeyByStatusStr(MainContext.AgentStatusEnum.READY.toString());
|
||||
}
|
||||
|
||||
/**
|
||||
* 未就绪的坐席
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public static String getAgentStatusNotReadyHashKey() {
|
||||
return getAgentStatusHashKeyByStatusStr(MainContext.AgentStatusEnum.NOTREADY.toString());
|
||||
}
|
||||
|
||||
|
||||
// AGENT USER 相关
|
||||
|
||||
/**
|
||||
* 获得坐席访客关联列表指定字符串状态的KEY
|
||||
*
|
||||
* @param status
|
||||
* @return
|
||||
*/
|
||||
public static String getAgentUserHashKeyByStatusStr(final String status) {
|
||||
StringBuffer sb = new StringBuffer();
|
||||
sb.append("agent:user:");
|
||||
sb.append(status);
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 排队中的访客KEY
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public static String getAgentUserInQueHashKey() {
|
||||
return getAgentUserHashKeyByStatusStr(MainContext.AgentUserStatusEnum.INQUENE.toString());
|
||||
}
|
||||
|
||||
/**
|
||||
* 服务中的访客
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public static String getAgentUserInServHashKey() {
|
||||
return getAgentUserHashKeyByStatusStr(MainContext.AgentUserStatusEnum.INSERVICE.toString());
|
||||
}
|
||||
|
||||
/**
|
||||
* 结束服务的访客
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public static String getAgentUserEndHashKey() {
|
||||
return getAgentUserHashKeyByStatusStr(MainContext.AgentUserStatusEnum.END.toString());
|
||||
}
|
||||
|
||||
/**
|
||||
* 获得一个坐席的服务中的访客列表KEY
|
||||
*/
|
||||
public static String getInServAgentUsersByAgentno(final String agentno) {
|
||||
StringBuffer sb = new StringBuffer();
|
||||
sb.append("agent:");
|
||||
sb.append(agentno);
|
||||
sb.append(":inserv");
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
|
||||
// Customer Chats Audit
|
||||
|
||||
/**
|
||||
* 存储AgentUser监控信息的存储Hash的KEY
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public static String getCustomerChatsAuditKey() {
|
||||
StringBuffer sb = new StringBuffer();
|
||||
sb.append("audit:customerchats");
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
|
||||
// ONLINE USER 相关
|
||||
|
||||
/**
|
||||
* 获得在线访客列表
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public static String getOnlineUserHashKey() {
|
||||
StringBuffer sb = new StringBuffer();
|
||||
sb.append("visitor:online");
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
// LOGIN USER 相关
|
||||
|
||||
/**
|
||||
* 已经登录的系统用户的API Auth Token
|
||||
* 包括管理员,坐席等,访客不在该列
|
||||
* 在该列表中的用户代表在线的系统用户,通过浏览器或API访问了系统
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public static String getApiTokenBearerKeyWithValue(final String token) {
|
||||
StringBuffer sb = new StringBuffer();
|
||||
sb.append("api:token:bearer:");
|
||||
sb.append(token);
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* CallCenter Agent 相关
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public static String getCallCenterAgentHashKey() {
|
||||
StringBuffer sb = new StringBuffer();
|
||||
sb.append("callcenter:agent");
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Job 相关
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public static String getJobHashKey() {
|
||||
StringBuffer sb = new StringBuffer();
|
||||
sb.append("job");
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* System 相关
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public static String getSystemHashKey() {
|
||||
StringBuffer sb = new StringBuffer();
|
||||
sb.append("system");
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 系统词典
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public static String getSysDicHashKey() {
|
||||
StringBuffer sb = new StringBuffer();
|
||||
sb.append("sysdic");
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 坐席会话配置相关
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public static String getSessionConfigList() {
|
||||
StringBuffer sb = new StringBuffer();
|
||||
sb.append("session:config:list");
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
public static String getSessionConfig(String organid) {
|
||||
StringBuffer sb = new StringBuffer();
|
||||
sb.append(organid);
|
||||
sb.append(":session:config");
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* SocketIO连接相关
|
||||
*/
|
||||
public static String getWebIMAgentSocketIOByAgentno(final String agentno) {
|
||||
StringBuffer sb = new StringBuffer();
|
||||
sb.append("agent:socketio:");
|
||||
sb.append(agentno);
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* CousultInvite 相关
|
||||
*/
|
||||
public static String getConsultInvites() {
|
||||
StringBuffer sb = new StringBuffer();
|
||||
sb.append("consultinvite");
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 和访客黑名单相关
|
||||
*/
|
||||
public static String getBlackEntityKey() {
|
||||
StringBuffer sb = new StringBuffer();
|
||||
sb.append("visitor:blacklist");
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 系统登录用户的会话Session信息
|
||||
*/
|
||||
public static String getUserSessionKey() {
|
||||
StringBuffer sb = new StringBuffer();
|
||||
sb.append("user:session");
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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.");
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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
Loading…
Reference in New Issue