diff --git a/.gitee/ISSUE_TEMPLATE/01_help.yml b/.gitee/ISSUE_TEMPLATE/01_help.yml new file mode 100644 index 00000000..8cc7ac72 --- /dev/null +++ b/.gitee/ISSUE_TEMPLATE/01_help.yml @@ -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 \ No newline at end of file diff --git a/.gitee/ISSUE_TEMPLATE/02_bug.yml b/.gitee/ISSUE_TEMPLATE/02_bug.yml new file mode 100644 index 00000000..1452d318 --- /dev/null +++ b/.gitee/ISSUE_TEMPLATE/02_bug.yml @@ -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 \ No newline at end of file diff --git a/.gitee/ISSUE_TEMPLATE/03_requirement.yml b/.gitee/ISSUE_TEMPLATE/03_requirement.yml new file mode 100644 index 00000000..abb0c4f5 --- /dev/null +++ b/.gitee/ISSUE_TEMPLATE/03_requirement.yml @@ -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 \ No newline at end of file diff --git a/.gitee/ISSUE_TEMPLATE/04_profiling.yml b/.gitee/ISSUE_TEMPLATE/04_profiling.yml new file mode 100644 index 00000000..44647c5f --- /dev/null +++ b/.gitee/ISSUE_TEMPLATE/04_profiling.yml @@ -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 \ No newline at end of file diff --git a/.gitee/ISSUE_TEMPLATE/05_userstory.yml b/.gitee/ISSUE_TEMPLATE/05_userstory.yml new file mode 100644 index 00000000..7e63130d --- /dev/null +++ b/.gitee/ISSUE_TEMPLATE/05_userstory.yml @@ -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 \ No newline at end of file diff --git a/.gitee/ISSUE_TEMPLATE/config.yaml b/.gitee/ISSUE_TEMPLATE/config.yaml new file mode 100644 index 00000000..80c0b2ab --- /dev/null +++ b/.gitee/ISSUE_TEMPLATE/config.yaml @@ -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: 提供春松客服定制化开发、机器人客服平台等 diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.gitee/PULL_REQUEST_TEMPLATE/bug_fix.md similarity index 89% rename from .github/PULL_REQUEST_TEMPLATE.md rename to .gitee/PULL_REQUEST_TEMPLATE/bug_fix.md index 76b95f03..fe4becfd 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.gitee/PULL_REQUEST_TEMPLATE/bug_fix.md @@ -1,13 +1,21 @@ +--- +reviewers : cskefu/reviewers +--- + ## 描述 +### 关联 Issue # + ## 解决的问题 + ## 测试情况 + @@ -15,13 +23,17 @@ ## 截屏 ## 变更的类型 + -- [ ] 解决Bug + +- [ ] 解决 Bug - [ ] 新功能(不影响其他功能) - [ ] 对其他功能有影响 -## 检查: +## 检查 + + - [ ] 我的变更和代码规范一致 - [ ] 我的变更需要更新文档 - [ ] 我已经更新了对应的文档 diff --git a/.gitee/PULL_REQUEST_TEMPLATE/documentation.md b/.gitee/PULL_REQUEST_TEMPLATE/documentation.md new file mode 100644 index 00000000..ee0ce348 --- /dev/null +++ b/.gitee/PULL_REQUEST_TEMPLATE/documentation.md @@ -0,0 +1,9 @@ +--- +reviewers : cskefu/reviewers +--- + +### Requirements for Contributing Documentation + +## 变更说明 + +### 关联 Issue # diff --git a/.gitee/PULL_REQUEST_TEMPLATE/performance_improvement.md b/.gitee/PULL_REQUEST_TEMPLATE/performance_improvement.md new file mode 100644 index 00000000..7f50fb0e --- /dev/null +++ b/.gitee/PULL_REQUEST_TEMPLATE/performance_improvement.md @@ -0,0 +1,9 @@ +--- +reviewers : cskefu/reviewers +--- + +### Requirements for Contributing a Performance Improvement + +## 性能提升 + +### 关联 Issue # diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md deleted file mode 100644 index 3add9bf1..00000000 --- a/.github/ISSUE_TEMPLATE.md +++ /dev/null @@ -1,12 +0,0 @@ -# 描述 - -## 现在行为 - -## 预期行为 - -# 解决方案 - -# 环境 - -* 代码版本: -Git commit hash (`git rev-parse HEAD`) diff --git a/.github/workflows/gitee.yml b/.github/workflows/gitee.yml deleted file mode 100644 index f316dd61..00000000 --- a/.github/workflows/gitee.yml +++ /dev/null @@ -1,19 +0,0 @@ -name: gitee - -on: - - push - - delete - -jobs: - sync: - runs-on: ubuntu-latest - name: Git Repo Sync - steps: - - uses: actions/checkout@v3 - with: - fetch-depth: 0 - - uses: wangchucheng/git-repo-sync@v0.1.0 - with: - target-url: https://gitee.com/cskefu/cskefu.git - target-username: ${{ secrets.USERNAME }} - target-token: ${{ secrets.ACCESS_TOKEN }} \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f5736fbd..f5ff8b0c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,10 +1,8 @@ # Contributing -成为春松客服贡献者 - ## 提交反馈 -在[春松客服 GitHub Issues](https://github.com/cskefu/cskefu/issues)中,先搜索是否有重复的。然后进行补充或新建 Issue。 +在[春松客服 Issues](https://gitee.com/cskefu/cskefu/issues)中,先搜索是否有重复的。然后进行补充或新建 Issue。 ## 提交代码 @@ -15,25 +13,14 @@ 春松客服文档中心的项目,也是开源的,地址在: -[https://github.com/cskefu/cskefu-docs](https://github.com/cskefu/cskefu-docs) +[https://gitee.com/cskefu/docs](https://gitee.com/cskefu/docs) 春松客服文档 Markdown 文件路径: -[https://github.com/cskefu/cskefu-docs/tree/main/docs](https://github.com/cskefu/cskefu-docs/tree/main/docs) +[https://gitee.com/cskefu/docs/tree/main/docs](https://gitee.com/cskefu/docs/tree/main/docs) 更新文档: -1)提交 PR 到[春松客服文档中心 GitHub 仓库](https://github.com/cskefu/cskefu-docs/tree/main/docs),一步到位。 +1)提交 PR 到[春松客服文档中心 Git 仓库](https://gitee.com/cskefu/docs/tree/main),一步到位。 -2)提交 Issue 到[春松客服 Issues](https://github.com/cskefu/cskefu/issues/new), 并撰写文档内容,使用该方案则需要后续其他协作者提交到 [春松客服文档中心 GitHub 仓库](https://github.com/cskefu/cskefu-docs) 中。 - -## 期待您成为春松客服贡献者 - -不管是何种贡献,只有大小之分,而无本质区别。 - -一起贡献,一起好! -没有贡献,都不好! - -团结是共赢,分裂是全输。 - -![image](./public/assets/screenshot-20220323-163051.jpg) +2)提交 Issue 到[春松客服 Issues](https://gitee.com/cskefu/cskefu/issues/new), 并撰写文档内容,使用该方案则需要后续其他协作者提交到 [春松客服文档中心 Git 仓库](https://gitee.com/cskefu/docs) 中。 diff --git a/README.md b/README.md index ba3a8f6e..2145d3b9 100644 --- a/README.md +++ b/README.md @@ -31,18 +31,6 @@ 新版本介绍:[观看春松客服 v8 新版本发布会 @ 2023-07-01](https://www.cskefu.com/2023/07/03/community-conf/) -## 媒体报道 - - - -- [春松客服:通过开源加云原生模式,大规模交付智能客服系统](https://www.cskefu.com/2022/04/11/cskefu-opensource-plus-cloud-model/) - -- [春松客服荣获 GVP 企业级开源项目认证](http://www.ctiforum.com/news/guonei/578988.html) - -- [Chatopera 王海良:做好开源客服系统 | OpenTEKr 专访](https://www.bilibili.com/video/BV1qF411p7hW) - ---- - ## 开发者列表 ✨ :evergreen_tree: 春松客服是开源的智能客服系统,于 2018 年 9 月由 [Chatopera](https://www.chatopera.com) 发布,在开源社区协作中优化和完善,春松客服属于[春松客服开源社区](https://github.com/cskefu/cskefu#%E6%98%A5%E6%9D%BE%E5%AE%A2%E6%9C%8D%E5%BC%80%E6%BA%90%E7%A4%BE%E5%8C%BA)。 @@ -134,27 +122,35 @@

欢迎页
- +

-### 坐席工作台 +
+展开查看更多产品截图 +

-登录演示环境,查看更多产品能力:[https://demo.cskefu.com/](https://demo.cskefu.com/) +

+ 坐席工作台
+ +

-| **登录账号** | **密码** | **角色** | -| ------------ | --------- | -------------- | -| admin | admin1234 | 系统超级管理员 | -| zhangsan | agent1234 | 客服坐席人员 | +

+ 坐席监控
+ +

-### 网页端访客示例 +

+ 集成客服机器人
+ +

-[https://demo.cskefu.com/testclient.html](http://demo.cskefu.com/testclient.html) +

+ 客服机器人应答
+ +

-- 登录张三后可接待访客,否则显示没有客服人员在线 - -### 机器人客服示例 - -[https://oh-my.cskefu.com/im/text/0nhckh.html](https://oh-my.cskefu.com/im/text/0nhckh.html) +

+
## 快速开始 @@ -210,13 +206,11 @@ 在春松客服开源社区,我们建立关系、发现认同、合作共赢! - 了解春松客服采用的开源许可协议,参考[文档](https://www.cskefu.com/2023/06/25/chunsong-public-license-1-0/) -- 了解春松客服的开发计划,参考[文档](https://chatopera.github.io/cskefu.roadmap/) -- 加入开源社区运营,成为社区合伙人,参考[文档](https://mp.weixin.qq.com/s/TLE87YX4k097iOXnV4WVSw) -- 加入春松客服开源社区,参考[文档](https://www.cskefu.com/join-us/) +- 了解春松客服的开发计划,参考[文档](https://github.com/cskefu/cskefu/issues) +- 如何发布春松客服人物志,向社区介绍自己,参考[文档](https://www.cskefu.com/join-us/) - 如何提交反馈、文档,参考[文档](./CONTRIBUTING.md) +- 如何成为春松客服开发者,参考[文档](https://docs.cskefu.com/docs/osc/devonboard/) - 如何提交代码,参考[文档](https://docs.cskefu.com/docs/osc/contribution) -- 如何最新的春松客服开发进展:订阅[春松客服邮件列表](https://lists.cskefu.com/cgi-bin/mailman/listinfo/dev) -- 如何获得春松客服商业插件和服务,参考[文档](https://www.chatopera.com/price.html) 春松客服之所以开源,是基于这样一种信念:爱人也是爱己,利他也是利己。 因春松客服受益,而不回报开源社区的用户,我们不欢迎使用春松客服:我们开源并不是为了你们,你们是不被祝福的。 @@ -241,7 +235,7 @@ - [IDE 配置和使用之 VSCode](https://docs.cskefu.com/docs/osc/ide_vscode) - 定制开发技能 - [系统集成之 RestAPIs](https://docs.cskefu.com/docs/osc/restapi) - - [从零开始学习定制春松客服技能:春松客服大讲堂 PPT 课件及视频](https://github.com/cskefu/cskefu.djt) + - [从零开始学习定制春松客服技能:春松客服大讲堂 PPT 课件及视频](https://store.chatopera.com/product/cskfdjt19) - [掌握春松客服前端框架 Pugjs,介绍及使用注意事项](https://blog.csdn.net/samurais/article/details/114576611) - [提交代码](https://docs.cskefu.com/docs/osc/contribution) diff --git a/contact-center/app/pom.xml b/contact-center/app/pom.xml index 01da5fe9..df97d49d 100644 --- a/contact-center/app/pom.xml +++ b/contact-center/app/pom.xml @@ -88,6 +88,7 @@ org.springframework.boot spring-boot-maven-plugin + 3.1.3 diff --git a/contact-center/app/src/main/java/com/cskefu/cc/acd/ACDPolicyService.java b/contact-center/app/src/main/java/com/cskefu/cc/acd/ACDPolicyService.java index d0eb72d6..91daf55f 100644 --- a/contact-center/app/src/main/java/com/cskefu/cc/acd/ACDPolicyService.java +++ b/contact-center/app/src/main/java/com/cskefu/cc/acd/ACDPolicyService.java @@ -27,8 +27,10 @@ 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; /** * 坐席自动分配策略集 @@ -58,6 +60,8 @@ public class ACDPolicyService { @Autowired private OrganProxy organProxy; + @Autowired + private OrganRepository organRepository; /** * 载入坐席 ACD策略配置 * @@ -85,12 +89,26 @@ public class ACDPolicyService { if ((sessionConfig = cache.findOneSessionConfig(organid)) == null) { sessionConfig = sessionConfigRes.findBySkill(organid); if (sessionConfig == null) { - sessionConfig = new SessionConfig(); + List list = organRepository.findAll(); + if (CollectionUtils.isEmpty(list)) { + return new SessionConfig(); + } else { + Map map = list.stream().collect(Collectors.toMap(item -> item.getId(), item -> item.getParent())); + List configList = sessionConfigRes.findAll(); + if (CollectionUtils.isEmpty(configList)) { + return new SessionConfig(); + } else { + Map 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; } @@ -394,4 +412,4 @@ public class ACDPolicyService { return agentUserRes.countByAgentnoAndStatusAndSkill(agentStatus.getAgentno(), MainContext.AgentUserStatusEnum.INSERVICE.toString(), skill); } -} +} \ No newline at end of file diff --git a/contact-center/app/src/main/java/com/cskefu/cc/acd/middleware/visitor/ACDVisBodyParserMw.java b/contact-center/app/src/main/java/com/cskefu/cc/acd/middleware/visitor/ACDVisBodyParserMw.java index 7da261a2..9aae742a 100644 --- a/contact-center/app/src/main/java/com/cskefu/cc/acd/middleware/visitor/ACDVisBodyParserMw.java +++ b/contact-center/app/src/main/java/com/cskefu/cc/acd/middleware/visitor/ACDVisBodyParserMw.java @@ -1,14 +1,14 @@ -/* - * Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd. - * , Licensed under the Chunsong Public +/* + * Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd. + * , 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, , - * Licensed under the Apache License, Version 2.0, + * Copyright (C) 2019-2022 Chatopera Inc, , + * Licensed under the Apache License, Version 2.0, * http://www.apache.org/licenses/LICENSE-2.0 */ @@ -22,12 +22,14 @@ 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; @@ -62,6 +64,9 @@ public class ACDVisBodyParserMw implements Middleware { @Autowired private ACDMessageHelper acdMessageHelper; + @Autowired + private LicenseProxy licenseProxy; + /** * 设置AgentUser基本信息 * @@ -87,6 +92,16 @@ public class ACDVisBodyParserMw implements Middleware { 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; }); diff --git a/contact-center/app/src/main/java/com/cskefu/cc/aspect/AgentUserAspect.java b/contact-center/app/src/main/java/com/cskefu/cc/aspect/AgentUserAspect.java index e342ff09..8b5f57e5 100644 --- a/contact-center/app/src/main/java/com/cskefu/cc/aspect/AgentUserAspect.java +++ b/contact-center/app/src/main/java/com/cskefu/cc/aspect/AgentUserAspect.java @@ -1,14 +1,14 @@ -/* - * Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd. - * , Licensed under the Chunsong Public +/* + * Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd. + * , 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, , - * Licensed under the Apache License, Version 2.0, + * Copyright (C) 2019-2022 Chatopera Inc, , + * Licensed under the Apache License, Version 2.0, * http://www.apache.org/licenses/LICENSE-2.0 */ @@ -18,14 +18,17 @@ 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; @@ -51,6 +54,31 @@ public class AgentUserAspect { @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]; diff --git a/contact-center/app/src/main/java/com/cskefu/cc/aspect/ChannelAspect.java b/contact-center/app/src/main/java/com/cskefu/cc/aspect/ChannelAspect.java new file mode 100644 index 00000000..499c4701 --- /dev/null +++ b/contact-center/app/src/main/java/com/cskefu/cc/aspect/ChannelAspect.java @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd. + * , 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 + } + } +} diff --git a/contact-center/app/src/main/java/com/cskefu/cc/aspect/ContactsAspect.java b/contact-center/app/src/main/java/com/cskefu/cc/aspect/ContactsAspect.java new file mode 100644 index 00000000..8c7f3e6b --- /dev/null +++ b/contact-center/app/src/main/java/com/cskefu/cc/aspect/ContactsAspect.java @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd. + * , 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 + } + } +} diff --git a/contact-center/app/src/main/java/com/cskefu/cc/aspect/OrganAspect.java b/contact-center/app/src/main/java/com/cskefu/cc/aspect/OrganAspect.java new file mode 100644 index 00000000..69e1f4a7 --- /dev/null +++ b/contact-center/app/src/main/java/com/cskefu/cc/aspect/OrganAspect.java @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd. + * , 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 + } + } +} diff --git a/contact-center/app/src/main/java/com/cskefu/cc/aspect/UserAspect.java b/contact-center/app/src/main/java/com/cskefu/cc/aspect/UserAspect.java new file mode 100644 index 00000000..1f67cc90 --- /dev/null +++ b/contact-center/app/src/main/java/com/cskefu/cc/aspect/UserAspect.java @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd. + * , 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 + } + } +} diff --git a/contact-center/app/src/main/java/com/cskefu/cc/basic/Constants.java b/contact-center/app/src/main/java/com/cskefu/cc/basic/Constants.java index 6df47781..8fc7cdfe 100644 --- a/contact-center/app/src/main/java/com/cskefu/cc/basic/Constants.java +++ b/contact-center/app/src/main/java/com/cskefu/cc/basic/Constants.java @@ -1,14 +1,14 @@ -/* - * Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd. - * , Licensed under the Chunsong Public +/* + * Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd. + * , 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, , - * Licensed under the Apache License, Version 2.0, + * Copyright (C) 2019-2022 Chatopera Inc, , + * Licensed under the Apache License, Version 2.0, * http://www.apache.org/licenses/LICENSE-2.0 */ package com.cskefu.cc.basic; @@ -217,4 +217,26 @@ public class Constants { 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"; } diff --git a/contact-center/app/src/main/java/com/cskefu/cc/basic/MainContext.java b/contact-center/app/src/main/java/com/cskefu/cc/basic/MainContext.java index 753d6d74..8e3f25ce 100644 --- a/contact-center/app/src/main/java/com/cskefu/cc/basic/MainContext.java +++ b/contact-center/app/src/main/java/com/cskefu/cc/basic/MainContext.java @@ -1,15 +1,15 @@ /* - * Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd. - * , Licensed under the Chunsong Public + * Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd. + * , 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, , Licensed under the Apache License, Version 2.0, + * Copyright (C) 2018- Jun. 2023 Chatopera Inc, , 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, + * Copyright (C) 2017 优客服-多渠道客服系统, Licensed under the Apache License, Version 2.0, * http://www.apache.org/licenses/LICENSE-2.0 */ @@ -929,6 +929,31 @@ public class MainContext { } } + /** + * 计费资源 + */ + public enum BillingResource { + USER, // 系统用户 + AGENGUSER, // 访客会话 + CONTACT, // 联系人 + ORGAN, // 组织机构 + CHANNELWEBIM; // 网页渠道 + + @Override + public String toString() { + return super.toString().toLowerCase(); + } + + public static BillingResource toValue(final String str) { + for (final BillingResource item : values()) { + if (StringUtils.equalsIgnoreCase(item.toString(), str)) { + return item; + } + } + throw new IllegalArgumentException(); + } + } + public static void setApplicationContext(ApplicationContext context) { applicationContext = context; context.getBean(TerminateBean.class); diff --git a/contact-center/app/src/main/java/com/cskefu/cc/config/AppCtxRefreshEventListener.java b/contact-center/app/src/main/java/com/cskefu/cc/config/AppCtxRefreshEventListener.java index 234abb1a..51d4a6a8 100644 --- a/contact-center/app/src/main/java/com/cskefu/cc/config/AppCtxRefreshEventListener.java +++ b/contact-center/app/src/main/java/com/cskefu/cc/config/AppCtxRefreshEventListener.java @@ -24,6 +24,7 @@ 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; @@ -36,7 +37,6 @@ public class AppCtxRefreshEventListener implements ApplicationListener, 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, , + * Licensed under the Apache License, Version 2.0, + * http://www.apache.org/licenses/LICENSE-2.0 + */ +package com.cskefu.cc.config; + +import com.chatopera.store.sdk.QuotaWdClient; +import com.chatopera.store.sdk.exceptions.InvalidProviderException; +import com.cskefu.cc.basic.MainContext; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.SpringApplication; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class QuotaWdClientConfig { + + @Value("${license.store.provider}") + private String licenseStoreProvider; + + /** + * 证书商店服务客户端 + * + * @return + */ + @Bean + public QuotaWdClient quotaWdClient() throws InvalidProviderException { + if (StringUtils.isBlank(licenseStoreProvider)) { + System.out.println("[license] invalid license provider info, service is terminated."); + System.exit(1); + } + + QuotaWdClient quotaWdClient = new QuotaWdClient(); + return quotaWdClient; + } +} diff --git a/contact-center/app/src/main/java/com/cskefu/cc/controller/admin/LicenseController.java b/contact-center/app/src/main/java/com/cskefu/cc/controller/admin/LicenseController.java new file mode 100644 index 00000000..2c404e16 --- /dev/null +++ b/contact-center/app/src/main/java/com/cskefu/cc/controller/admin/LicenseController.java @@ -0,0 +1,231 @@ +/* + * Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd. + * , 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.controller.admin; + +import com.chatopera.store.sdk.exceptions.InvalidRequestException; +import com.chatopera.store.sdk.exceptions.InvalidResponseException; +import com.cskefu.cc.basic.Constants; +import com.cskefu.cc.controller.Handler; +import com.cskefu.cc.exception.LicenseNotFoundException; +import com.cskefu.cc.exception.MetaKvInvalidKeyException; +import com.cskefu.cc.model.User; +import com.cskefu.cc.proxy.LicenseProxy; +import com.cskefu.cc.util.Menu; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.validation.Valid; +import org.apache.commons.lang3.StringUtils; +import org.json.JSONArray; +import org.json.JSONObject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Controller; +import org.springframework.ui.ModelMap; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.servlet.ModelAndView; + +import java.util.Date; +import java.util.List; + +@Controller +@RequestMapping("/admin/license") +public class LicenseController extends Handler { + private final static Logger logger = LoggerFactory.getLogger(LicenseController.class); + + @Autowired + private LicenseProxy licenseProxy; + + @RequestMapping("/index") + @Menu(type = "admin", subtype = "licenseList") + public ModelAndView index(ModelMap map, HttpServletRequest request) { + User user = super.getUser(request); + if (user.isSuperadmin()) { + try { + List licenses = licenseProxy.getLicensesInStore(); + map.addAttribute(Constants.UPDATETIME, new Date()); + map.addAttribute(Constants.LICENSES, licenses); + map.addAttribute(Constants.LICENSESTOREPROVIDER, licenseProxy.getLicenseStoreProvider()); + + } catch (InvalidResponseException e) { + throw new RuntimeException(e); + } + return request(super.createView("/admin/license/index")); + } else { + return request(super.createView("/public/error")); + } + } + + @RequestMapping("/add") + @Menu(type = "admin", subtype = "licenseList") + public ModelAndView add(ModelMap map, HttpServletRequest request) { + User user = super.getUser(request); + if (user.isSuperadmin()) { + return request(super.createView("/admin/license/add")); + } else { + return request(super.createView("/public/error")); + } + } + + /** + * 保存新的证书 + * + * @param map + * @param request + * @param licenseShortId + * @return + */ + @RequestMapping("/save") + @Menu(type = "admin", subtype = "licenseList") + public ModelAndView save(ModelMap map, + HttpServletRequest request, + @Valid String licenseShortId) throws MetaKvInvalidKeyException, InvalidRequestException { + User user = super.getUser(request); + logger.info("[save] licenseShortId {}", licenseShortId); + String msg = ""; + + if (user.isSuperadmin()) { + try { + /** + * 验证证书可以添加 + */ + // 验证该证书不在当前证书列表中 + JSONArray currents = licenseProxy.getLicensesInMetakv(); + boolean isAddedBefore = false; + + for (int i = 0; i < currents.length(); i++) { + JSONObject item = (JSONObject) currents.get(i); + if (StringUtils.equals(item.getString(Constants.SHORTID), licenseShortId)) { + isAddedBefore = true; + break; + } + } + + if (isAddedBefore) { + msg = "already_added"; + return request(super.createView( + "redirect:/admin/license/index.html?msg=" + msg)); + } + + // 验证该证书存在 + licenseProxy.existLicenseInStore(licenseShortId); + + // 验证该证书的所属产品没有现在没有其它证书:同一个产品最多只有一个证书 + JSONObject licBasic = licenseProxy.getLicenseBasicsInStore(licenseShortId); + final String productId = licBasic.getJSONObject(Constants.PRODUCT).getString(Constants.SHORTID); + + boolean isProductAdded = false; + JSONArray addedLicenseBasicsFromStore = licenseProxy.getAddedLicenseBasicsInStore(); + + for (int i = 0; i < addedLicenseBasicsFromStore.length(); i++) { + JSONObject item = (JSONObject) addedLicenseBasicsFromStore.get(i); + if (StringUtils.equals(item.getJSONObject(Constants.PRODUCT).getString(Constants.SHORTID), productId)) { + isProductAdded = true; + break; + } + } + + if (isProductAdded) { + msg = "product_added_already"; + return request(super.createView( + "redirect:/admin/license/index.html?msg=" + msg)); + } + + /** + * 添加该证书 + */ + JSONObject licenseKvData = new JSONObject(); + licenseKvData.put(Constants.SHORTID, licenseShortId); + licenseKvData.put(Constants.ADDDATE, new Date()); + licenseKvData.put(Constants.PRODUCT_ID, productId); + currents.put(0, licenseKvData); + licenseProxy.createOrUpdateMetaKv(Constants.LICENSEIDS, currents.toString(), Constants.METAKV_DATATYPE_STRING); + + // 跳转回到证书列表 + List licenses = licenseProxy.getLicensesInStore(); + map.addAttribute(Constants.LICENSES, licenses); + map.addAttribute(Constants.UPDATETIME, new Date()); + map.addAttribute(Constants.LICENSESTOREPROVIDER, licenseProxy.getLicenseStoreProvider()); + + return request(super.createView("/admin/license/index")); + } catch (InvalidResponseException e) { + logger.warn("[save] error in getLicenseFromStore", e); + msg = "invalid_id"; + return request(super.createView( + "redirect:/admin/license/index.html?msg=" + msg)); + } catch (LicenseNotFoundException e) { + logger.warn("[save] error in getLicenseFromStore", e); + msg = "notfound_id"; + return request(super.createView( + "redirect:/admin/license/index.html?msg=" + msg)); + } + } else { + return request(super.createView("/public/error")); + } + } + + @RequestMapping("/delete/{licenseShortId}") + @Menu(type = "admin", subtype = "licenseList") + public ModelAndView delete(ModelMap map, + HttpServletRequest request, + @PathVariable String licenseShortId) throws MetaKvInvalidKeyException { + User user = super.getUser(request); + logger.info("[delete] licenseShortId {}", licenseShortId); + String msg = ""; + + if (user.isSuperadmin()) { + try { + JSONArray currents = licenseProxy.getLicensesInMetakv(); + JSONArray post = new JSONArray(); + for (int i = 0; i < currents.length(); i++) { + JSONObject item = (JSONObject) currents.get(i); + if (!StringUtils.equals(item.getString(Constants.SHORTID), licenseShortId)) { + post.put(item); + } + } + + /** + * 添加该证书 + */ + licenseProxy.createOrUpdateMetaKv(Constants.LICENSEIDS, post.toString(), Constants.METAKV_DATATYPE_STRING); + + // 跳转回到证书列表 + List licenses = licenseProxy.getLicensesInStore(); + map.addAttribute(Constants.LICENSES, licenses); + map.addAttribute("updateTime", new Date()); + map.addAttribute(Constants.LICENSESTOREPROVIDER, licenseProxy.getLicenseStoreProvider()); + + return request(super.createView("/admin/license/index")); + } catch (InvalidResponseException e) { + logger.warn("[save] error in getLicenseFromStore", e); + msg = "invalid_id"; + return request(super.createView( + "redirect:/admin/license/index.html?msg=" + msg)); + } + } else { + return request(super.createView("/public/error")); + } + } + + @RequestMapping("/instance") + @Menu(type = "admin", subtype = "licenseInst") + public ModelAndView getInstanceInfo(ModelMap map, HttpServletRequest request) { + User user = super.getUser(request); + if (user.isSuperadmin()) { + map.addAttribute(Constants.LICENSE_SERVICE_NAME, licenseProxy.resolveServicename()); + map.addAttribute(Constants.LICENSE_SERVER_INST_ID, licenseProxy.resolveServerinstId()); + map.addAttribute(Constants.LICENSESTOREPROVIDER, licenseProxy.getLicenseStoreProvider()); + return request(super.createView("/admin/license/instance")); + } else { + return request(super.createView("/public/error")); + } + } +} diff --git a/contact-center/app/src/main/java/com/cskefu/cc/controller/admin/OrganController.java b/contact-center/app/src/main/java/com/cskefu/cc/controller/admin/OrganController.java index dadf3216..d44d5087 100644 --- a/contact-center/app/src/main/java/com/cskefu/cc/controller/admin/OrganController.java +++ b/contact-center/app/src/main/java/com/cskefu/cc/controller/admin/OrganController.java @@ -1,15 +1,15 @@ /* - * Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd. - * , Licensed under the Chunsong Public + * Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd. + * , 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, , Licensed under the Apache License, Version 2.0, + * Copyright (C) 2018- Jun. 2023 Chatopera Inc, , 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, + * Copyright (C) 2017 优客服-多渠道客服系统, Licensed under the Apache License, Version 2.0, * http://www.apache.org/licenses/LICENSE-2.0 */ package com.cskefu.cc.controller.admin; @@ -17,6 +17,7 @@ package com.cskefu.cc.controller.admin; import com.cskefu.cc.basic.Constants; import com.cskefu.cc.cache.Cache; import com.cskefu.cc.controller.Handler; +import com.cskefu.cc.exception.BillingQuotaException; import com.cskefu.cc.model.*; import com.cskefu.cc.persistence.repository.*; import com.cskefu.cc.proxy.OrganProxy; @@ -34,6 +35,8 @@ import org.springframework.web.servlet.ModelAndView; import jakarta.servlet.http.HttpServletRequest; import jakarta.validation.Valid; + +import java.lang.reflect.UndeclaredThrowableException; import java.util.*; /** @@ -157,16 +160,26 @@ public class OrganController extends Handler { public ModelAndView save(HttpServletRequest request, @Valid Organ organ) { Organ tempOrgan = organRepository.findByName(organ.getName()); String msg = "admin_organ_new_success"; - String firstId = null; + String createdId = null; if (tempOrgan != null) { msg = "admin_organ_update_name_not"; // 分类名字重复 } else { - firstId = organ.getId(); - - organRepository.save(organ); + try { + organRepository.save(organ); + createdId = organ.getId(); + } catch (Exception e) { + if (e instanceof UndeclaredThrowableException) { + logger.error("[save] BillingQuotaException", e); + if (StringUtils.startsWith(e.getCause().getMessage(), BillingQuotaException.SUFFIX)) { + msg = e.getCause().getMessage(); + } + } else { + logger.error("[save] err", e); + } + } } return request(super.createView( - "redirect:/admin/organ/index.html?msg=" + msg + "&organ=" + firstId)); + "redirect:/admin/organ/index.html?msg=" + msg + "&organ=" + createdId)); } /** diff --git a/contact-center/app/src/main/java/com/cskefu/cc/controller/admin/channel/ChannelController.java b/contact-center/app/src/main/java/com/cskefu/cc/controller/admin/channel/ChannelController.java index 93c5ff0f..53a7cf18 100644 --- a/contact-center/app/src/main/java/com/cskefu/cc/controller/admin/channel/ChannelController.java +++ b/contact-center/app/src/main/java/com/cskefu/cc/controller/admin/channel/ChannelController.java @@ -1,15 +1,15 @@ /* - * Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd. - * , Licensed under the Chunsong Public + * Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd. + * , 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, , Licensed under the Apache License, Version 2.0, + * Copyright (C) 2018- Jun. 2023 Chatopera Inc, , 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, + * Copyright (C) 2017 优客服-多渠道客服系统, Licensed under the Apache License, Version 2.0, * http://www.apache.org/licenses/LICENSE-2.0 */ package com.cskefu.cc.controller.admin.channel; @@ -18,6 +18,7 @@ import com.cskefu.cc.basic.MainContext; import com.cskefu.cc.basic.MainUtils; import com.cskefu.cc.cache.Cache; import com.cskefu.cc.controller.Handler; +import com.cskefu.cc.exception.BillingQuotaException; import com.cskefu.cc.model.*; import com.cskefu.cc.persistence.repository.ConsultInviteRepository; import com.cskefu.cc.persistence.repository.OrganRepository; @@ -27,6 +28,8 @@ import com.cskefu.cc.proxy.OrganProxy; import com.cskefu.cc.util.Base62; import com.cskefu.cc.util.Menu; import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.PageRequest; import org.springframework.stereotype.Controller; @@ -37,6 +40,8 @@ import org.springframework.web.servlet.ModelAndView; import jakarta.servlet.http.HttpServletRequest; import jakarta.validation.Valid; + +import java.lang.reflect.UndeclaredThrowableException; import java.security.NoSuchAlgorithmException; import java.util.Date; import java.util.List; @@ -48,6 +53,7 @@ import java.util.Map; @Controller @RequestMapping("/admin/im") public class ChannelController extends Handler { + private final static Logger logger = LoggerFactory.getLogger(ChannelController.class); @Autowired private ChannelRepository snsAccountRes; @@ -93,40 +99,59 @@ public class ChannelController extends Handler { return request(super.createView("/admin/channel/im/add")); } + /** + * 创建新的网站渠道 + * + * @param request + * @param channel + * @return + * @throws NoSuchAlgorithmException + */ @RequestMapping("/save") - @Menu(type = "admin", subtype = "weixin") + @Menu(type = "admin", subtype = "im") public ModelAndView save(HttpServletRequest request, @Valid Channel channel) throws NoSuchAlgorithmException { Organ currentOrgan = super.getOrgan(request); String status = "new_webim_fail"; if (StringUtils.isNotBlank(channel.getBaseURL())) { - channel.setSnsid(Base62.encode(channel.getBaseURL()).toLowerCase()); - int count = snsAccountRes.countBySnsid(channel.getSnsid()); - if (count == 0) { - status = "new_webim_success"; - channel.setType(MainContext.ChannelType.WEBIM.toString()); - channel.setCreatetime(new Date()); - User curr = super.getUser(request); - channel.setCreater(curr.getId()); - channel.setOrgan(currentOrgan.getId()); + try { + channel.setSnsid(Base62.encode(channel.getBaseURL()).toLowerCase()); + int count = snsAccountRes.countBySnsid(channel.getSnsid()); + if (count == 0) { + status = "new_webim_success"; + channel.setType(MainContext.ChannelType.WEBIM.toString()); + channel.setCreatetime(new Date()); + User curr = super.getUser(request); + channel.setCreater(curr.getId()); + channel.setOrgan(currentOrgan.getId()); - snsAccountRes.save(channel); + snsAccountRes.save(channel); - /** - * 同时创建CousultInvite 记录 - */ - CousultInvite coultInvite = invite.findBySnsaccountid(channel.getSnsid()); - if (coultInvite == null) { - coultInvite = new CousultInvite(); - coultInvite.setSnsaccountid(channel.getSnsid()); - coultInvite.setCreate_time(new Date()); - coultInvite.setName(channel.getName()); - coultInvite.setOwner(channel.getCreater()); - coultInvite.setSkill(false); // 不启动技能组 - coultInvite.setConsult_skill_fixed(false); // 不绑定唯一技能组 - coultInvite.setAi(false); - coultInvite.setAifirst(false); - invite.save(coultInvite); + /** + * 同时创建CousultInvite 记录 + */ + CousultInvite coultInvite = invite.findBySnsaccountid(channel.getSnsid()); + if (coultInvite == null) { + coultInvite = new CousultInvite(); + coultInvite.setSnsaccountid(channel.getSnsid()); + coultInvite.setCreate_time(new Date()); + coultInvite.setName(channel.getName()); + coultInvite.setOwner(channel.getCreater()); + coultInvite.setSkill(false); // 不启动技能组 + coultInvite.setConsult_skill_fixed(false); // 不绑定唯一技能组 + coultInvite.setAi(false); + coultInvite.setAifirst(false); + invite.save(coultInvite); + } + } + } catch (Exception e) { + if (e instanceof UndeclaredThrowableException) { + logger.error("[save] BillingQuotaException", e); + if (StringUtils.startsWith(e.getCause().getMessage(), BillingQuotaException.SUFFIX)) { + status = e.getCause().getMessage(); + } + } else { + logger.error("[save] err", e); } } } diff --git a/contact-center/app/src/main/java/com/cskefu/cc/controller/admin/system/MetadataController.java b/contact-center/app/src/main/java/com/cskefu/cc/controller/admin/system/MetadataController.java index fba17dac..a9208a84 100644 --- a/contact-center/app/src/main/java/com/cskefu/cc/controller/admin/system/MetadataController.java +++ b/contact-center/app/src/main/java/com/cskefu/cc/controller/admin/system/MetadataController.java @@ -41,7 +41,7 @@ import jakarta.persistence.EntityManager; import jakarta.persistence.PersistenceContext; import jakarta.servlet.http.HttpServletRequest; import jakarta.validation.Valid; -import java.sql.Connection; + import java.sql.SQLException; import java.util.ArrayList; import java.util.Arrays; diff --git a/contact-center/app/src/main/java/com/cskefu/cc/controller/api/ApiContactsController.java b/contact-center/app/src/main/java/com/cskefu/cc/controller/api/ApiContactsController.java index dbcb38bc..b28e4df7 100644 --- a/contact-center/app/src/main/java/com/cskefu/cc/controller/api/ApiContactsController.java +++ b/contact-center/app/src/main/java/com/cskefu/cc/controller/api/ApiContactsController.java @@ -46,6 +46,8 @@ import org.springframework.web.bind.annotation.RestController; import jakarta.servlet.http.HttpServletRequest; import jakarta.validation.Valid; + +import java.util.Arrays; import java.util.Date; import java.util.List; import java.util.Optional; @@ -84,7 +86,7 @@ public class ApiContactsController extends Handler { if (!StringUtils.isBlank(creater)) { User user = super.getUser(request); - contactsList = contactsRepository.findByCreaterAndSharesAndDatastatus(user.getId(), "all", false, + contactsList = contactsRepository.findByCreaterAndSharesInAndDatastatus(user.getId(), Arrays.asList(user.getId(),"all"), false, PageRequest.of( super.getP(request), super.getPs(request))); diff --git a/contact-center/app/src/main/java/com/cskefu/cc/controller/api/ApiUserController.java b/contact-center/app/src/main/java/com/cskefu/cc/controller/api/ApiUserController.java index 86cfb791..08956ef1 100644 --- a/contact-center/app/src/main/java/com/cskefu/cc/controller/api/ApiUserController.java +++ b/contact-center/app/src/main/java/com/cskefu/cc/controller/api/ApiUserController.java @@ -1,15 +1,15 @@ /* - * Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd. - * , Licensed under the Chunsong Public + * Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd. + * , 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, , Licensed under the Apache License, Version 2.0, + * Copyright (C) 2018- Jun. 2023 Chatopera Inc, , 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, + * Copyright (C) 2017 优客服-多渠道客服系统, Licensed under the Apache License, Version 2.0, * http://www.apache.org/licenses/LICENSE-2.0 */ package com.cskefu.cc.controller.api; @@ -183,7 +183,9 @@ public class ApiUserController extends Handler { User user = userProxy.parseUserFromJson(payload); JsonObject resp = userProxy.createNewUser(user, parentOrgan); - if (StringUtils.isNotEmpty(roleId)) { + if (StringUtils.isNotEmpty(roleId) && + StringUtils.equals(resp.get(RestUtils.RESP_KEY_DATA).getAsString(), + Constants.NEW_USER_SUCCESS)) { Role role = roleRes.findById(roleId).orElse(null); UserRole userRole = new UserRole(); userRole.setUser(user); diff --git a/contact-center/app/src/main/java/com/cskefu/cc/controller/apps/AgentAuditController.java b/contact-center/app/src/main/java/com/cskefu/cc/controller/apps/AgentAuditController.java index e781cf0b..86c7e958 100644 --- a/contact-center/app/src/main/java/com/cskefu/cc/controller/apps/AgentAuditController.java +++ b/contact-center/app/src/main/java/com/cskefu/cc/controller/apps/AgentAuditController.java @@ -71,9 +71,6 @@ public class AgentAuditController extends Handler { @Autowired private UserRepository userRes; - @Autowired - private AgentUserRepository agentUserRepository; - @Autowired private ChatMessageRepository chatMessageRepository; @@ -245,7 +242,7 @@ public class AgentAuditController extends Handler { view.addObject( "agentUserList", agentUserRes.findByStatusAndAgentnoIsNot( MainContext.AgentUserStatusEnum.INSERVICE.toString(), logined.getId(), defaultSort)); - List agentUserList = agentUserRepository.findByUserid(userid); + List agentUserList = agentUserRes.findByUserid(userid); view.addObject( "curagentuser", agentUserList != null && agentUserList.size() > 0 ? agentUserList.get(0) : null); @@ -266,7 +263,7 @@ public class AgentAuditController extends Handler { } ModelAndView view = request(super.createView(mainagentuser)); final User logined = super.getUser(request); - AgentUser agentUser = agentUserRepository.findById(id).orElse(null); + AgentUser agentUser = agentUserRes.findById(id).orElse(null); if (agentUser != null) { view.addObject("curagentuser", agentUser); diff --git a/contact-center/app/src/main/java/com/cskefu/cc/controller/apps/AgentController.java b/contact-center/app/src/main/java/com/cskefu/cc/controller/apps/AgentController.java index dc4e0935..431bd2d0 100644 --- a/contact-center/app/src/main/java/com/cskefu/cc/controller/apps/AgentController.java +++ b/contact-center/app/src/main/java/com/cskefu/cc/controller/apps/AgentController.java @@ -1252,4 +1252,4 @@ return request(super.createView("redirect:/agent/index.html")); } - } + } \ No newline at end of file diff --git a/contact-center/app/src/main/java/com/cskefu/cc/controller/apps/ContactsController.java b/contact-center/app/src/main/java/com/cskefu/cc/controller/apps/ContactsController.java index ad9f4401..3bb55b2a 100644 --- a/contact-center/app/src/main/java/com/cskefu/cc/controller/apps/ContactsController.java +++ b/contact-center/app/src/main/java/com/cskefu/cc/controller/apps/ContactsController.java @@ -1,21 +1,22 @@ /* - * Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd. - * , Licensed under the Chunsong Public + * Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd. + * , 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, , Licensed under the Apache License, Version 2.0, + * Copyright (C) 2018- Jun. 2023 Chatopera Inc, , 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, + * Copyright (C) 2017 优客服-多渠道客服系统, Licensed under the Apache License, Version 2.0, * http://www.apache.org/licenses/LICENSE-2.0 */ package com.cskefu.cc.controller.apps; import com.cskefu.cc.basic.MainUtils; import com.cskefu.cc.controller.Handler; +import com.cskefu.cc.exception.BillingQuotaException; import com.cskefu.cc.exception.CSKefuException; import com.cskefu.cc.model.*; import com.cskefu.cc.persistence.repository.*; @@ -47,8 +48,10 @@ import org.springframework.web.servlet.ModelAndView; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import jakarta.validation.Valid; + import java.io.File; import java.io.IOException; +import java.lang.reflect.UndeclaredThrowableException; import java.text.SimpleDateFormat; import java.util.*; @@ -143,8 +146,8 @@ public class ContactsController extends Handler { map.put("ckind", ckind); } - Page contacts = contactsRes.findByCreaterAndSharesAndDatastatus(logined.getId(), - logined.getId(), + Page contacts = contactsRes.findByCreaterAndSharesInAndDatastatus(logined.getId(), + Arrays.asList(logined.getId(),"all"), false, PageRequest.of( super.getP(request), @@ -177,8 +180,8 @@ public class ContactsController extends Handler { map.put("ckind", ckind); } - Page contacts = contactsRes.findByCreaterAndSharesAndDatastatus(logined.getId(), - logined.getId(), + Page contacts = contactsRes.findByCreaterAndSharesInAndDatastatus(logined.getId(), + Arrays.asList(logined.getId(),"all"), false, PageRequest.of( super.getP(request), @@ -211,8 +214,8 @@ public class ContactsController extends Handler { map.put("ckind", ckind); } - Page contacts = contactsRes.findByCreaterAndSharesAndDatastatus(logined.getId(), - logined.getId(), + Page contacts = contactsRes.findByCreaterAndSharesInAndDatastatus(logined.getId(), + Arrays.asList(logined.getId(),"all"), false, PageRequest.of( super.getP(request), @@ -255,13 +258,11 @@ public class ContactsController extends Handler { @RequestParam(name = "idselflocation", required = false) String selflocation) { final User logined = super.getUser(request); Organ currentOrgan = super.getOrgan(request); - String skypeIDReplace = contactsProxy.sanitizeSkypeId(contacts.getSkypeid()); String msg = ""; - Contacts contact = contactsRes.findByskypeidAndDatastatus(skypeIDReplace, false); // 添加数据 - if (contacts.getSkypeid() != null && contact == null) { - logger.info("[save] 数据库没有相同skypeid"); + try { + contacts.setId(null); contacts.setCreater(logined.getId()); if (currentOrgan != null && StringUtils.isBlank(contacts.getOrgan())) { @@ -274,11 +275,16 @@ public class ContactsController extends Handler { } contactsRes.save(contacts); msg = "new_contacts_success"; - - return request(super.createView( - "redirect:/apps/contacts/index.html?ckind=" + contacts.getCkind() + "&msg=" + msg)); + } catch (Exception e) { + if (e instanceof UndeclaredThrowableException) { + logger.error("[save] BillingQuotaException", e); + if (StringUtils.startsWith(e.getCause().getMessage(), BillingQuotaException.SUFFIX)) { + msg = e.getCause().getMessage(); + } + } else { + logger.error("[save] err", e); + } } - msg = "new_contacts_fail"; return request(super.createView( "redirect:/apps/contacts/index.html?ckind=" + contacts.getCkind() + "&msg=" + msg)); } @@ -478,8 +484,8 @@ public class ContactsController extends Handler { map.put("ckind", ckind); } - Iterable contactsList = contactsRes.findByCreaterAndSharesAndDatastatus( - logined.getId(), logined.getId(), false, PageRequest.of(super.getP(request), super.getPs(request))); + Iterable contactsList = contactsRes.findByCreaterAndSharesInAndDatastatus( + logined.getId(), Arrays.asList(logined.getId(),"all"),false, PageRequest.of(super.getP(request), super.getPs(request))); MetadataTable table = metadataRes.findByTablename("uk_contacts"); List> values = new ArrayList<>(); @@ -512,8 +518,8 @@ public class ContactsController extends Handler { map.put("ckind", ckind); } - Iterable contactsList = contactsRes.findByCreaterAndSharesAndDatastatus( - logined.getId(), logined.getId(), false, PageRequest.of(super.getP(request), super.getPs(request))); + Iterable contactsList = contactsRes.findByCreaterAndSharesInAndDatastatus( + logined.getId(), Arrays.asList(logined.getId(),"all"), false, PageRequest.of(super.getP(request), super.getPs(request))); MetadataTable table = metadataRes.findByTablename("uk_contacts"); List> values = new ArrayList<>(); for (Contacts contacts : contactsList) { @@ -552,8 +558,8 @@ public class ContactsController extends Handler { if (StringUtils.isNotBlank(agentserviceid)) { AgentService service = agentServiceRes.findById(agentserviceid).orElse(null); } - Page contactsList = contactsRes.findByCreaterAndSharesAndDatastatus( - logined.getId(), logined.getId(), false, + Page contactsList = contactsRes.findByCreaterAndSharesInAndDatastatus( + logined.getId(), Arrays.asList(logined.getId(),"all"), false, PageRequest.of(super.getP(request), super.getPs(request))); map.addAttribute("contactsList", contactsList); diff --git a/contact-center/app/src/main/java/com/cskefu/cc/controller/apps/IMController.java b/contact-center/app/src/main/java/com/cskefu/cc/controller/apps/IMController.java index e6afc0a4..9ea62a55 100644 --- a/contact-center/app/src/main/java/com/cskefu/cc/controller/apps/IMController.java +++ b/contact-center/app/src/main/java/com/cskefu/cc/controller/apps/IMController.java @@ -117,7 +117,7 @@ public class IMController extends Handler { private LeaveMsgRepository leaveMsgRes; @Autowired - private AgentUserRepository agentUserRepository; + private AgentUserRepository agentUserRes; @Autowired private AttachmentRepository attachementRes; @@ -822,7 +822,7 @@ public class IMController extends Handler { Contacts contacts1 = contactsRes.findOneByWluidAndWlsidAndWlcidAndDatastatus( uid, sid, cid, false); if (contacts1 != null) { - agentUserRepository.findOneByUserid(userid).ifPresent(p -> { + agentUserRes.findOneByUserid(userid).ifPresent(p -> { // 关联AgentService的联系人 if (StringUtils.isNotBlank(p.getAgentserviceid())) { AgentService agentService = agentServiceRepository.findById(p.getAgentserviceid()).orElse(null); diff --git a/contact-center/app/src/main/java/com/cskefu/cc/controller/apps/service/AgentSummaryController.java b/contact-center/app/src/main/java/com/cskefu/cc/controller/apps/service/AgentSummaryController.java index 2231dea7..6bda5541 100644 --- a/contact-center/app/src/main/java/com/cskefu/cc/controller/apps/service/AgentSummaryController.java +++ b/contact-center/app/src/main/java/com/cskefu/cc/controller/apps/service/AgentSummaryController.java @@ -31,7 +31,6 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Sort; -import org.springframework.data.jpa.domain.Specification; import org.springframework.stereotype.Controller; import org.springframework.ui.ModelMap; import org.springframework.web.bind.annotation.RequestMapping; diff --git a/contact-center/app/src/main/java/com/cskefu/cc/controller/apps/service/ChatServiceController.java b/contact-center/app/src/main/java/com/cskefu/cc/controller/apps/service/ChatServiceController.java index f8b3d030..86bbdf66 100644 --- a/contact-center/app/src/main/java/com/cskefu/cc/controller/apps/service/ChatServiceController.java +++ b/contact-center/app/src/main/java/com/cskefu/cc/controller/apps/service/ChatServiceController.java @@ -77,9 +77,6 @@ public class ChatServiceController extends Handler { @Autowired private AgentStatusRepository agentStatusRepository; - @Autowired - private AgentUserRepository agentUserRepository; - @Autowired private LeaveMsgRepository leaveMsgRes; @@ -233,7 +230,7 @@ public class ChatServiceController extends Handler { if (agentUser != null) { agentUser.setAgentno(agentno); agentUser.setAgentname(targetAgent.getUname()); - agentUserRepository.save(agentUser); + agentUserRes.save(agentUser); if (MainContext.AgentUserStatusEnum.INSERVICE.toString().equals( agentUser.getStatus())) { // 转接 , 发送消息给 目标坐席 @@ -288,11 +285,11 @@ public class ChatServiceController extends Handler { } } } else { - agentUser = agentUserRepository.findById(agentService.getAgentuserid()).orElse(null); + agentUser = agentUserRes.findById(agentService.getAgentuserid()).orElse(null); if (agentUser != null) { agentUser.setAgentno(agentno); agentUser.setAgentname(targetAgent.getUname()); - agentUserRepository.save(agentUser); + agentUserRes.save(agentUser); } } @@ -317,7 +314,7 @@ public class ChatServiceController extends Handler { AgentService agentService = agentServiceRes.findById(id).orElse(null); if (agentService != null) { User user = super.getUser(request); - AgentUser agentUser = agentUserRepository.findById(agentService.getAgentuserid()).orElse(null); + AgentUser agentUser = agentUserRes.findById(agentService.getAgentuserid()).orElse(null); if (agentUser != null) { acdAgentService.finishAgentUser(agentUser); } diff --git a/contact-center/app/src/main/java/com/cskefu/cc/controller/apps/service/OnlineUserController.java b/contact-center/app/src/main/java/com/cskefu/cc/controller/apps/service/OnlineUserController.java index a16e4978..3bf73b4e 100644 --- a/contact-center/app/src/main/java/com/cskefu/cc/controller/apps/service/OnlineUserController.java +++ b/contact-center/app/src/main/java/com/cskefu/cc/controller/apps/service/OnlineUserController.java @@ -130,9 +130,10 @@ public class OnlineUserController extends Handler { } - agentUserContactsRes.findOneByUserid( - userid).ifPresent(p -> { - map.put("contacts", contactsRes.findById(p.getContactsid()).orElse(null)); + agentUserContactsRes.findOneByUserid(userid).ifPresent(p -> { + if (p.getContactsid() != null) { + map.put("contacts", contactsRes.findById(p.getContactsid()).orElse(null)); + } }); AgentService service = agentServiceRes.findById(agentservice).orElse(null); if (service != null) { diff --git a/contact-center/app/src/main/java/com/cskefu/cc/controller/apps/service/ProcessedSummaryController.java b/contact-center/app/src/main/java/com/cskefu/cc/controller/apps/service/ProcessedSummaryController.java index 51602e96..d436af3b 100644 --- a/contact-center/app/src/main/java/com/cskefu/cc/controller/apps/service/ProcessedSummaryController.java +++ b/contact-center/app/src/main/java/com/cskefu/cc/controller/apps/service/ProcessedSummaryController.java @@ -31,7 +31,6 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Sort; -import org.springframework.data.jpa.domain.Specification; import org.springframework.stereotype.Controller; import org.springframework.ui.ModelMap; import org.springframework.web.bind.annotation.RequestMapping; diff --git a/contact-center/app/src/main/java/com/cskefu/cc/controller/resource/ContactsResourceController.java b/contact-center/app/src/main/java/com/cskefu/cc/controller/resource/ContactsResourceController.java index 376e0c66..48a6f24c 100644 --- a/contact-center/app/src/main/java/com/cskefu/cc/controller/resource/ContactsResourceController.java +++ b/contact-center/app/src/main/java/com/cskefu/cc/controller/resource/ContactsResourceController.java @@ -31,6 +31,8 @@ import org.springframework.web.bind.annotation.ResponseBody; import jakarta.servlet.http.HttpServletRequest; import jakarta.validation.Valid; +import java.util.Arrays; + @Controller public class ContactsResourceController extends Handler { @@ -44,7 +46,7 @@ public class ContactsResourceController extends Handler { if (q == null) { q = ""; } - Page contactsList = contactsRes.findByCreaterAndSharesAndDatastatus(super.getUser(request).getId(), super.getUser(request).getId(), false, PageRequest.of(0, 10)); + Page contactsList = contactsRes.findByCreaterAndSharesInAndDatastatus(super.getUser(request).getId(), Arrays.asList(super.getUser(request).getId(),"all"),false, PageRequest.of(0, 10)); JSONArray result = new JSONArray(); for (Contacts contact : contactsList.getContent()) { diff --git a/contact-center/app/src/main/java/com/cskefu/cc/exception/BillingQuotaException.java b/contact-center/app/src/main/java/com/cskefu/cc/exception/BillingQuotaException.java new file mode 100644 index 00000000..77f91df5 --- /dev/null +++ b/contact-center/app/src/main/java/com/cskefu/cc/exception/BillingQuotaException.java @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd. + * , 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-Jun. 2023 Chatopera Inc, , + * Licensed under the Apache License, Version 2.0, + * http://www.apache.org/licenses/LICENSE-2.0 + */ +package com.cskefu.cc.exception; + +public class BillingQuotaException extends Exception { + + public final static String SUFFIX = "billingquotaexception."; + + // metakv 中没有找到可以支持配额的证书类型 + public final static String NO_LICENSE_FOUND = SUFFIX + "no_license_found"; + // 返回值异常,可能是网络连接不上,稍后再试 + public static final String RESPONSE_UNEXPECTED = SUFFIX + "response_unexpected"; + // 请求参数不合法 + public static final String INVALID_REQUEST_BODY = SUFFIX + "invalid_request_body"; + // 证书在证书商店不存在 + public static final String LICENSE_INVALID = SUFFIX + "license_invalid"; + // 证书关联的产品信息不合法 + public static final String PRODUCT_INVALID = SUFFIX + "product_invalid"; + // 证书失效或耗尽 + public static final String LICENSE_EXPIRED_OR_EXHAUSTED = SUFFIX + "license_expired_or_exhausted"; + // 证书关闭了对该 serverinst 的支持 + public static final String LICENSE_DISABLED_SERVERINST = SUFFIX + "license_disabled_serverinst"; + // 证书不支持配额回退 + public static final String LICENSE_UNSUPPORT_REFUND = SUFFIX + "license_unsupport_refund"; + // 证书配额余量不足,不能完成本次请求 + public static final String LICENSE_QUOTA_INADEQUATE = SUFFIX + "license_quota_inadequate"; + // 内部错误,不应该发生 + public static final String INTERNAL_ERROR = SUFFIX + "internal_error"; + + public BillingQuotaException(final String s) { + super(s); + } +} diff --git a/contact-center/app/src/main/java/com/cskefu/cc/exception/BillingResourceException.java b/contact-center/app/src/main/java/com/cskefu/cc/exception/BillingResourceException.java new file mode 100644 index 00000000..e7e38f30 --- /dev/null +++ b/contact-center/app/src/main/java/com/cskefu/cc/exception/BillingResourceException.java @@ -0,0 +1,20 @@ +/* + * Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd. + * , 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-Jun. 2023 Chatopera Inc, , + * Licensed under the Apache License, Version 2.0, + * http://www.apache.org/licenses/LICENSE-2.0 + */ +package com.cskefu.cc.exception; + +public class BillingResourceException extends Exception{ + public BillingResourceException(final String s){ + super(s); + } +} diff --git a/contact-center/app/src/main/java/com/cskefu/cc/exception/LicenseNotFoundException.java b/contact-center/app/src/main/java/com/cskefu/cc/exception/LicenseNotFoundException.java new file mode 100644 index 00000000..90da2580 --- /dev/null +++ b/contact-center/app/src/main/java/com/cskefu/cc/exception/LicenseNotFoundException.java @@ -0,0 +1,20 @@ +/* + * Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd. + * , 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-Jun. 2023 Chatopera Inc, , + * Licensed under the Apache License, Version 2.0, + * http://www.apache.org/licenses/LICENSE-2.0 + */ +package com.cskefu.cc.exception; + +public class LicenseNotFoundException extends Exception{ + public LicenseNotFoundException(final String s){ + super(s); + } +} diff --git a/contact-center/app/src/main/java/com/cskefu/cc/exception/MetaKvInvalidKeyException.java b/contact-center/app/src/main/java/com/cskefu/cc/exception/MetaKvInvalidKeyException.java new file mode 100644 index 00000000..fafbd11a --- /dev/null +++ b/contact-center/app/src/main/java/com/cskefu/cc/exception/MetaKvInvalidKeyException.java @@ -0,0 +1,20 @@ +/* + * Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd. + * , 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-Jun. 2023 Chatopera Inc, , + * Licensed under the Apache License, Version 2.0, + * http://www.apache.org/licenses/LICENSE-2.0 + */ +package com.cskefu.cc.exception; + +public class MetaKvInvalidKeyException extends Exception{ + public MetaKvInvalidKeyException(final String s){ + super(s); + } +} diff --git a/contact-center/app/src/main/java/com/cskefu/cc/exception/MetaKvNotExistException.java b/contact-center/app/src/main/java/com/cskefu/cc/exception/MetaKvNotExistException.java new file mode 100644 index 00000000..dd9e3e07 --- /dev/null +++ b/contact-center/app/src/main/java/com/cskefu/cc/exception/MetaKvNotExistException.java @@ -0,0 +1,20 @@ +/* + * Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd. + * , 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-Jun. 2023 Chatopera Inc, , + * Licensed under the Apache License, Version 2.0, + * http://www.apache.org/licenses/LICENSE-2.0 + */ +package com.cskefu.cc.exception; + +public class MetaKvNotExistException extends Exception{ + public MetaKvNotExistException(final String s){ + super(s); + } +} diff --git a/contact-center/app/src/main/java/com/cskefu/cc/model/AgentUser.java b/contact-center/app/src/main/java/com/cskefu/cc/model/AgentUser.java index c30ad35e..07371d25 100644 --- a/contact-center/app/src/main/java/com/cskefu/cc/model/AgentUser.java +++ b/contact-center/app/src/main/java/com/cskefu/cc/model/AgentUser.java @@ -1,15 +1,15 @@ /* - * Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd. - * , Licensed under the Chunsong Public + * Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd. + * , 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, , Licensed under the Apache License, Version 2.0, + * Copyright (C) 2018- Jun. 2023 Chatopera Inc, , 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, + * Copyright (C) 2017 优客服-多渠道客服系统, Licensed under the Apache License, Version 2.0, * http://www.apache.org/licenses/LICENSE-2.0 */ package com.cskefu.cc.model; @@ -19,6 +19,7 @@ import org.hibernate.annotations.GenericGenerator; import org.hibernate.annotations.Proxy; import jakarta.persistence.*; + import java.io.Serializable; import java.util.Date; @@ -105,6 +106,7 @@ public class AgentUser implements Serializable, Comparable { @Transient private boolean tip = false; + @Transient private boolean agentTip = false; @@ -119,11 +121,25 @@ public class AgentUser implements Serializable, Comparable { @Transient private boolean fromhis = false; + @Transient private boolean online = false; + @Transient private boolean disconnect = false; + /** + * 证书验证通过 + */ + @Transient + private boolean licenseVerifiedPass = true; + + /** + * 证书验证提示信息 + */ + @Transient + private String licenseBillingMsg; + public AgentUser() { } @@ -617,4 +633,22 @@ public class AgentUser implements Serializable, Comparable { public void setAgentname(String agentname) { this.agentname = agentname; } + + @Transient + public boolean isLicenseVerifiedPass() { + return licenseVerifiedPass; + } + + public void setLicenseVerifiedPass(boolean licenseVerifiedPass) { + this.licenseVerifiedPass = licenseVerifiedPass; + } + + @Transient + public String getLicenseBillingMsg() { + return licenseBillingMsg; + } + + public void setLicenseBillingMsg(String licenseBillingMsg) { + this.licenseBillingMsg = licenseBillingMsg; + } } diff --git a/contact-center/app/src/main/java/com/cskefu/cc/model/ExecuteResult.java b/contact-center/app/src/main/java/com/cskefu/cc/model/ExecuteResult.java new file mode 100644 index 00000000..adadc46d --- /dev/null +++ b/contact-center/app/src/main/java/com/cskefu/cc/model/ExecuteResult.java @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd. + * , 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.model; + +import org.json.JSONObject; + +import java.io.Serializable; + +public class ExecuteResult implements Serializable { + public final static int RC_SUCC = 0; + public final static int RC_ERR1 = 1; + public final static int RC_ERR2 = 2; + public final static int RC_ERR3 = 3; + public final static int RC_ERR4 = 4; + public final static int RC_ERR5 = 5; + public final static int RC_ERR6 = 6; + public final static int RC_ERR7 = 7; + public final static int RC_ERR8 = 8; + public final static int RC_ERR9 = 9; + + private int rc; // 0 for success, errors other + private String error; + private String msg; + private JSONObject data; + + + public ExecuteResult() { + + } + + public ExecuteResult(final int rc, final String msg) { + this.rc = rc; + this.msg = msg; + } + + public ExecuteResult(final int rc, final String msg, final String error) { + this.rc = rc; + this.msg = msg; + this.error = error; + } + + public ExecuteResult(final int rc, + final String msg, + final String error, + final JSONObject data) { + this.rc = rc; + this.msg = msg; + this.error = error; + this.data = data; + } + + public int getRc() { + return rc; + } + + public void setRc(int rc) { + this.rc = rc; + } + + public String getError() { + return error; + } + + public void setError(String error) { + this.error = error; + } + + public String getMsg() { + return msg; + } + + public void setMsg(String msg) { + this.msg = msg; + } + + public JSONObject getData() { + return data; + } + + public void setData(JSONObject data) { + this.data = data; + } +} diff --git a/contact-center/app/src/main/java/com/cskefu/cc/model/MetaKv.java b/contact-center/app/src/main/java/com/cskefu/cc/model/MetaKv.java new file mode 100644 index 00000000..0de0916e --- /dev/null +++ b/contact-center/app/src/main/java/com/cskefu/cc/model/MetaKv.java @@ -0,0 +1,77 @@ +package com.cskefu.cc.model; + +import jakarta.persistence.*; + +import java.util.Date; + +/** + * 存储元数据 + */ +@Entity +@Table(name = "cs_metakv") +@org.hibernate.annotations.Proxy(lazy = false) +public class MetaKv implements java.io.Serializable { + + @Id + private String metakey; + + private String metavalue; + + private String datatype; + + private String comment; + + @Temporal(TemporalType.TIMESTAMP) + private Date updatetime; + + @Temporal(TemporalType.TIMESTAMP) + private Date createtime; + + public String getMetakey() { + return metakey; + } + + public void setMetakey(String metakey) { + this.metakey = metakey; + } + + public String getMetavalue() { + return metavalue; + } + + public void setMetavalue(String metavalue) { + this.metavalue = metavalue; + } + + public String getDatatype() { + return datatype; + } + + public void setDatatype(String datatype) { + this.datatype = datatype; + } + + public String getComment() { + return comment; + } + + public void setComment(String comment) { + this.comment = comment; + } + + public Date getUpdatetime() { + return updatetime; + } + + public void setUpdatetime(Date updatetime) { + this.updatetime = updatetime; + } + + public Date getCreatetime() { + return createtime; + } + + public void setCreatetime(Date createtime) { + this.createtime = createtime; + } +} diff --git a/contact-center/app/src/main/java/com/cskefu/cc/persistence/repository/AgentUserRepository.java b/contact-center/app/src/main/java/com/cskefu/cc/persistence/repository/AgentUserRepository.java index 8ccc0a46..d5211362 100644 --- a/contact-center/app/src/main/java/com/cskefu/cc/persistence/repository/AgentUserRepository.java +++ b/contact-center/app/src/main/java/com/cskefu/cc/persistence/repository/AgentUserRepository.java @@ -69,26 +69,26 @@ public interface AgentUserRepository extends JpaRepository { AgentUser findOneByAgentnoAndStatus(String id, String status); - @Query(nativeQuery = true, value = "SELECT * FROM uk_agentuser AS u " + + @Query(nativeQuery = true, value = "SELECT u.* FROM uk_agentuser AS u " + "LEFT JOIN uk_agentuser_contacts AS c " + "ON u.userid = c.userid WHERE c.id = ?1 AND NOT u.status = ?2 LIMIT 1") AgentUser findOneByContactIdAndStatusNot(final String contactid, final String status); - @Query(nativeQuery = true, value = "SELECT * FROM uk_agentuser AS u " + + @Query(nativeQuery = true, value = "SELECT u.* FROM uk_agentuser AS u " + "LEFT JOIN uk_agentuser_contacts AS c " + "ON u.userid = c.userid WHERE c.contactsid = ?1 " + "AND c.channeltype = ?3 AND NOT u.status = ?2 " + "ORDER BY u.createtime DESC LIMIT 1") Optional findOneByContactIdAndStatusNotAndChanneltype(final String contactid, final String status, final String channeltype); - @Query(nativeQuery = true, value = "SELECT * FROM uk_agentuser AS u " + + @Query(nativeQuery = true, value = "SELECT u.* FROM uk_agentuser AS u " + "LEFT JOIN uk_agentuser_contacts AS c " + "ON u.userid = c.userid WHERE c.contactsid = ?1 " + "AND c.channeltype = ?2 " + "ORDER BY u.createtime DESC LIMIT 1") Optional findOneByContactIdAndChanneltype(final String contactid, final String channeltype); - @Query(nativeQuery = true, value = "SELECT * FROM uk_agentuser AS u " + + @Query(nativeQuery = true, value = "SELECT u.* FROM uk_agentuser AS u " + "WHERE u.userid = ?1 " + "AND u.channeltype = ?3 AND NOT u.status = ?2 " + "ORDER BY u.createtime DESC LIMIT 1") diff --git a/contact-center/app/src/main/java/com/cskefu/cc/persistence/repository/ContactsRepository.java b/contact-center/app/src/main/java/com/cskefu/cc/persistence/repository/ContactsRepository.java index dec00c22..1784968f 100644 --- a/contact-center/app/src/main/java/com/cskefu/cc/persistence/repository/ContactsRepository.java +++ b/contact-center/app/src/main/java/com/cskefu/cc/persistence/repository/ContactsRepository.java @@ -40,7 +40,7 @@ public interface ContactsRepository extends JpaRepository { @Query(nativeQuery = true, value = "SELECT * FROM uk_contacts WHERE id = ?1") Optional findOneById(final String id); - Page findByCreaterAndSharesAndDatastatus(String id, String shares, boolean datastatus, Pageable pageRequest); + Page findByCreaterAndSharesInAndDatastatus(String id, Collection shares, boolean datastatus, Pageable pageRequest); /** * 根据条件返回联系人,符合一下条件之一: diff --git a/contact-center/app/src/main/java/com/cskefu/cc/persistence/repository/MetaKvRepository.java b/contact-center/app/src/main/java/com/cskefu/cc/persistence/repository/MetaKvRepository.java new file mode 100644 index 00000000..f180c902 --- /dev/null +++ b/contact-center/app/src/main/java/com/cskefu/cc/persistence/repository/MetaKvRepository.java @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd. + * , 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.persistence.repository; + +import com.cskefu.cc.model.MetaKv; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface MetaKvRepository extends JpaRepository { + + Optional findFirstByMetakey(final String p1); + +} diff --git a/contact-center/app/src/main/java/com/cskefu/cc/persistence/repository/MetadataRepository.java b/contact-center/app/src/main/java/com/cskefu/cc/persistence/repository/MetadataRepository.java index 6deb23ad..58349d63 100644 --- a/contact-center/app/src/main/java/com/cskefu/cc/persistence/repository/MetadataRepository.java +++ b/contact-center/app/src/main/java/com/cskefu/cc/persistence/repository/MetadataRepository.java @@ -1,33 +1,33 @@ -/* - * Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd. - * , 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, , 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.persistence.repository; - -import com.cskefu.cc.model.MetadataTable; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; -import org.springframework.data.jpa.repository.JpaRepository; - -import java.util.List; - -public interface MetadataRepository extends JpaRepository{ - - MetadataTable findByTablename(String tablename); - - Page findAll(Pageable paramPageable); - - int countByTablename(String tableName) ; - - List findAll(); -} +/* + * Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd. + * , 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, , 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.persistence.repository; + +import com.cskefu.cc.model.MetadataTable; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface MetadataRepository extends JpaRepository{ + + MetadataTable findByTablename(String tablename); + + Page findAll(Pageable paramPageable); + + int countByTablename(String tableName) ; + + List findAll(); +} diff --git a/contact-center/app/src/main/java/com/cskefu/cc/plugins/chatbot/ChatbotEventHandler.java b/contact-center/app/src/main/java/com/cskefu/cc/plugins/chatbot/ChatbotEventHandler.java index ef54e3a2..1ef1de9d 100644 --- a/contact-center/app/src/main/java/com/cskefu/cc/plugins/chatbot/ChatbotEventHandler.java +++ b/contact-center/app/src/main/java/com/cskefu/cc/plugins/chatbot/ChatbotEventHandler.java @@ -1,371 +1,371 @@ -/* - * Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd. - * , 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-Jun. 2023 Chatopera Inc, , - * Licensed under the Apache License, Version 2.0, - * http://www.apache.org/licenses/LICENSE-2.0 - */ -package com.cskefu.cc.plugins.chatbot; - -import com.chatopera.bot.sdk.Response; -import com.corundumstudio.socketio.AckRequest; -import com.corundumstudio.socketio.SocketIOClient; -import com.corundumstudio.socketio.SocketIOServer; -import com.corundumstudio.socketio.annotation.OnConnect; -import com.corundumstudio.socketio.annotation.OnDisconnect; -import com.corundumstudio.socketio.annotation.OnEvent; -import com.cskefu.cc.acd.ACDServiceRouter; -import com.cskefu.cc.basic.Constants; -import com.cskefu.cc.basic.MainContext; -import com.cskefu.cc.basic.MainUtils; -import com.cskefu.cc.model.*; -import com.cskefu.cc.persistence.repository.AgentUserRepository; -import com.cskefu.cc.persistence.repository.ChatbotRepository; -import com.cskefu.cc.persistence.repository.PassportWebIMUserRepository; -import com.cskefu.cc.proxy.OnlineUserProxy; -import com.cskefu.cc.socketio.client.NettyClients; -import com.cskefu.cc.socketio.message.AgentStatusMessage; -import com.cskefu.cc.socketio.message.ChatMessage; -import com.cskefu.cc.socketio.message.Message; -import com.cskefu.cc.socketio.util.IMServiceUtils; -import com.cskefu.cc.util.IP; -import com.cskefu.cc.util.IPTools; -import org.apache.commons.lang3.StringUtils; -import org.json.JSONObject; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; - -import java.net.InetSocketAddress; -import java.util.Date; - -public class ChatbotEventHandler { - private static final Logger logger = LoggerFactory.getLogger(ChatbotEventHandler.class); - - protected final SocketIOServer server; - - private static AgentUserRepository agentUserRes; - private static PassportWebIMUserRepository onlineUserRes; - private static ChatbotRepository chatbotRes; - private static ChatbotProxy chatbotProxy; - - @Autowired - public ChatbotEventHandler(SocketIOServer server) { - this.server = server; - } - - @OnConnect - public void onConnect(SocketIOClient client) { - try { - String user = client.getHandshakeData().getSingleUrlParam("userid"); - String nickname = client.getHandshakeData().getSingleUrlParam("nickname"); - String session = MainUtils.getContextID(client.getHandshakeData().getSingleUrlParam("session")); - String appid = client.getHandshakeData().getSingleUrlParam("appid"); - String aiid = client.getHandshakeData().getSingleUrlParam("aiid"); - logger.info( - "[onConnect] userid {}, nickname {}, session {}, appid {}, aiid {}", user, nickname, session, appid, - aiid); - - client.set("aiid", aiid); - client.set("session", session); - client.set("userid", user); - client.set("appid", appid); - - Date now = new Date(); - - if (StringUtils.isNotBlank(user)) { - /** - * 加入到 缓存列表 - */ - NettyClients.getInstance().putChatbotEventClient(user, client); - CousultInvite invite = OnlineUserProxy.consult(appid); - - /** - * 更新坐席服务类型 - */ - IMServiceUtils.shiftOpsType(user, MainContext.OptType.CHATBOT); - - // send out tip - Message tip = new Message(); - tip.setMessage("您正在使用机器人客服!"); - tip.setMessageType(MainContext.MessageType.MESSAGE.toString()); - tip.setCalltype(MainContext.CallType.IN.toString()); - tip.setCreatetime(MainUtils.dateFormate.format(now)); - - client.sendEvent(MainContext.MessageType.STATUS.toString(), tip); - - // send out welcome message - if (invite != null) { - Chatbot chatbot = getChatbotRes().findById(invite.getAiid()).orElse(null); - com.chatopera.bot.sdk.Chatbot bot = new com.chatopera.bot.sdk.Chatbot( - chatbot.getClientId(), chatbot.getSecret(), chatbot.getBaseUrl()); - Response result = bot.command("GET", "/"); - - // 发送欢迎语 - if (result.getRc() == 0) { - JSONObject details = (JSONObject) result.getData(); - ChatMessage welcome = new ChatMessage(); - String welcomeTextMessage = details.getString("welcome"); - if (StringUtils.isNotBlank(welcomeTextMessage)) { - welcome.setCalltype(MainContext.CallType.OUT.toString()); - welcome.setAppid(appid); - welcome.setAiid(aiid); - welcome.setMessage(welcomeTextMessage); - welcome.setTouser(user); - welcome.setMsgtype(MainContext.MessageType.MESSAGE.toString()); - welcome.setUserid(user); - welcome.setUsername(invite.getAiname()); - welcome.setUpdatetime(System.currentTimeMillis()); - client.sendEvent(MainContext.MessageType.MESSAGE.toString(), welcome); - } - - // 发送常见问题列表 - JSONObject faqhotresp = bot.conversation(user, "__faq_hot_list"); - logger.info("faqhot {}", faqhotresp.toString()); - if (faqhotresp.getInt("rc") == 0) { - JSONObject faqhotdata = faqhotresp.getJSONObject("data"); - if ((!faqhotdata.getBoolean("logic_is_fallback")) && - faqhotdata.has("string") && - faqhotdata.has("params")) { - ChatMessage faqhotmsg = new ChatMessage(); - faqhotmsg.setCalltype(MainContext.CallType.OUT.toString()); - faqhotmsg.setAppid(appid); - faqhotmsg.setAiid(aiid); - faqhotmsg.setMessage(faqhotdata.getString("string")); - faqhotmsg.setExpmsg(faqhotdata.getJSONArray("params").toString()); - faqhotmsg.setTouser(user); - faqhotmsg.setMsgtype(MainContext.MessageType.MESSAGE.toString()); - faqhotmsg.setUserid(user); - faqhotmsg.setUsername(invite.getAiname()); - faqhotmsg.setUpdatetime(System.currentTimeMillis()); - client.sendEvent(MainContext.MessageType.MESSAGE.toString(), faqhotmsg); - } - } - } else if (result.getRc() == 999 || result.getRc() == 998) { - logger.error("[chat] chatbot agent response rc {}, error {}", result.getRc(), result.getError()); - } else { - logger.error("[chat] chatbot agent response rc {}, error {}", result.getRc(), result.getError()); - } - } - - InetSocketAddress address = (InetSocketAddress) client.getRemoteAddress(); - String ip = MainUtils.getIpAddr(client.getHandshakeData().getHttpHeaders(), address.getHostString()); - PassportWebIMUser passportWebIMUser = getOnlineUserRes().findById(user).orElse(null); - - if (passportWebIMUser == null) { - passportWebIMUser = new PassportWebIMUser(); - passportWebIMUser.setAppid(appid); - if (StringUtils.isNotBlank(nickname)) { - passportWebIMUser.setUsername(nickname); - } else { - passportWebIMUser.setUsername(Constants.GUEST_USER + "_" + MainUtils.genIDByKey(user)); - } - - passportWebIMUser.setSessionid(session); - passportWebIMUser.setOptype(MainContext.OptType.CHATBOT.toString()); - passportWebIMUser.setUserid(user); - passportWebIMUser.setId(user); - passportWebIMUser.setChannel(MainContext.ChannelType.WEBIM.toString()); - passportWebIMUser.setIp(ip); - passportWebIMUser.setUpdatetime(now); - passportWebIMUser.setLogintime(now); - passportWebIMUser.setCreatetime(now); - IP ipdata = IPTools.getInstance().findGeography(ip); - passportWebIMUser.setCity(ipdata.getCity()); - passportWebIMUser.setCountry(ipdata.getCountry()); - passportWebIMUser.setProvince(ipdata.getProvince()); - passportWebIMUser.setIsp(ipdata.getIsp()); - passportWebIMUser.setRegion(ipdata.getRegion()); - passportWebIMUser.setStatus(MainContext.OnlineUserStatusEnum.ONLINE.toString()); - } - - // 在线客服访客咨询记录 - AgentUser agentUser = new AgentUser( - passportWebIMUser.getId(), - MainContext.ChannelType.WEBIM.toString(), // callout - passportWebIMUser.getId(), - passportWebIMUser.getUsername(), - appid); - - agentUser.setServicetime(now); - agentUser.setCreatetime(now); - agentUser.setUpdatetime(now); - agentUser.setSessionid(session); - agentUser.setRegion(passportWebIMUser.getRegion()); - - // 聊天机器人处理的请求 - agentUser.setOpttype(MainContext.OptType.CHATBOT.toString()); - agentUser.setAgentno(aiid); // 聊天机器人ID - agentUser.setAgentname(invite != null ? invite.getAiname() : "机器人客服"); - agentUser.setCity(passportWebIMUser.getCity()); - agentUser.setProvince(passportWebIMUser.getProvince()); - agentUser.setCountry(passportWebIMUser.getCountry()); - AgentService agentService = ACDServiceRouter.getAcdChatbotService().processChatbotService( - invite != null ? invite.getAiname() : "机器人客服", agentUser); - agentUser.setAgentserviceid(agentService.getId()); - - // 标记为机器人坐席 - agentUser.setChatbotops(true); - - // 保存到MySQL - getAgentUserRes().save(agentUser); - getOnlineUserRes().save(passportWebIMUser); - } - } catch (Exception e) { - logger.info("[onConnect] error", e); - } - } - - // 添加 @OnDisconnect 事件,客户端断开连接时调用,刷新客户端信息 - @OnDisconnect - public void onDisconnect(SocketIOClient client) { - String user = client.getHandshakeData().getSingleUrlParam("userid"); - if (StringUtils.isNotBlank(user)) { - NettyClients.getInstance().removeChatbotEventClient( - user, MainUtils.getContextID(client.getSessionId().toString())); - PassportWebIMUser passportWebIMUser = MainContext.getCache().findOneOnlineUserByUserId(user); - - MainContext.getCache().findOneAgentUserByUserId(user).ifPresent(p -> { - ACDServiceRouter.getAcdChatbotService().processChatbotService(null, p); - - MainContext.getCache().deleteAgentUserByUserId(p); - MainContext.getCache().deleteOnlineUserById(user); - - p.setStatus(MainContext.AgentUserStatusEnum.END.toString()); - passportWebIMUser.setStatus(MainContext.OnlineUserStatusEnum.OFFLINE.toString()); - - getAgentUserRes().save(p); - getOnlineUserRes().save(passportWebIMUser); - }); - } - client.disconnect(); - } - - // 消息接收入口,网站有新用户接入对话 - @OnEvent(value = "new") - public void onEvent(SocketIOClient client, AckRequest request, Message data) { - - } - - // 消息接收入口,坐席状态更新 - @OnEvent(value = "agentstatus") - public void onEvent(SocketIOClient client, AckRequest request, AgentStatusMessage data) { - logger.info("[onEvent] agentstatus: ", data.getMessage()); - } - - // 消息接收入口,收发消息,用户向机器人发送消息 - @OnEvent(value = "message") - public void onEvent(SocketIOClient client, AckRequest request, ChatMessage data) { - String aiid = client.get("aiid"); - String user = client.get("userid"); - String sessionid = client.get("session"); - String appid = client.get("appid"); - logger.info( - "[onEvent] message: session {}, aiid {}, userid {}, dataType {}, appid {}", sessionid, aiid, - user, data.getType(), appid); - - // ignore event if dataType is not message. - if (!StringUtils.equals(data.getType(), Constants.IM_MESSAGE_TYPE_MESSAGE)) { - return; - } - - MainContext.getCache().findOneAgentUserByUserId(user).ifPresent(p -> { - /** - * 以下代码主要用于检查 访客端的字数限制 - */ - CousultInvite invite = OnlineUserProxy.consult(data.getAppid()); - // ignore event if no invite found. - if (invite == null) { - return; - } - - // ignore if Chatbot is turnoff. - if (!invite.isAi()) { - return; - } - - Date now = new Date(); - if (invite.getMaxwordsnum() > 0) { - if (StringUtils.isNotBlank(data.getMessage()) && data.getMessage().length() > invite.getMaxwordsnum()) { - data.setMessage(data.getMessage().substring(0, invite.getMaxwordsnum())); - } - } else if (StringUtils.isNotBlank(data.getMessage()) && data.getMessage().length() > 300) { - data.setMessage(data.getMessage().substring(0, 300)); - } - - data.setUsession(user); // 绑定唯一用户 - data.setSessionid(sessionid); - data.setMessage(MainUtils.processEmoti(data.getMessage())); // 处理表情 - data.setTouser(aiid); - data.setUsername(p.getUsername()); - data.setAiid(aiid); - data.setAgentserviceid(p.getAgentserviceid()); - data.setChannel(p.getChanneltype()); - data.setContextid(p.getAgentserviceid()); // 一定要设置 ContextID - data.setCalltype(MainContext.CallType.IN.toString()); - - // 保存并发送消息给访客 - getChatbotProxy().createTextMessage( - data, - MainContext.CallType.IN.toString()); - - // 更新访客咨询记录 - p.setUpdatetime(now); - p.setLastmessage(now); - p.setLastmsg(data.getMessage()); - getAgentUserRes().save(p); - - // 发送消息给Bot - getChatbotProxy().publishMessage(data, Constants.CHATBOT_EVENT_TYPE_CHAT); - }); - - - } - - /** - * Lazy load - * - * @return - */ - private AgentUserRepository getAgentUserRes() { - if (agentUserRes == null) { - agentUserRes = MainContext.getContext().getBean(AgentUserRepository.class); - } - - return agentUserRes; - } - - /** - * Lazy load - * - * @return - */ - private ChatbotProxy getChatbotProxy() { - if (chatbotProxy == null) { - chatbotProxy = MainContext.getContext().getBean(ChatbotProxy.class); - } - return chatbotProxy; - } - - private PassportWebIMUserRepository getOnlineUserRes() { - if (onlineUserRes == null) { - onlineUserRes = MainContext.getContext().getBean(PassportWebIMUserRepository.class); - } - - return onlineUserRes; - } - - private ChatbotRepository getChatbotRes() { - if (chatbotRes == null) { - chatbotRes = MainContext.getContext().getBean(ChatbotRepository.class); - } - - return chatbotRes; - } -} +/* + * Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd. + * , 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-Jun. 2023 Chatopera Inc, , + * Licensed under the Apache License, Version 2.0, + * http://www.apache.org/licenses/LICENSE-2.0 + */ +package com.cskefu.cc.plugins.chatbot; + +import com.chatopera.bot.sdk.Response; +import com.corundumstudio.socketio.AckRequest; +import com.corundumstudio.socketio.SocketIOClient; +import com.corundumstudio.socketio.SocketIOServer; +import com.corundumstudio.socketio.annotation.OnConnect; +import com.corundumstudio.socketio.annotation.OnDisconnect; +import com.corundumstudio.socketio.annotation.OnEvent; +import com.cskefu.cc.acd.ACDServiceRouter; +import com.cskefu.cc.basic.Constants; +import com.cskefu.cc.basic.MainContext; +import com.cskefu.cc.basic.MainUtils; +import com.cskefu.cc.model.*; +import com.cskefu.cc.persistence.repository.AgentUserRepository; +import com.cskefu.cc.persistence.repository.ChatbotRepository; +import com.cskefu.cc.persistence.repository.PassportWebIMUserRepository; +import com.cskefu.cc.proxy.OnlineUserProxy; +import com.cskefu.cc.socketio.client.NettyClients; +import com.cskefu.cc.socketio.message.AgentStatusMessage; +import com.cskefu.cc.socketio.message.ChatMessage; +import com.cskefu.cc.socketio.message.Message; +import com.cskefu.cc.socketio.util.IMServiceUtils; +import com.cskefu.cc.util.IP; +import com.cskefu.cc.util.IPTools; +import org.apache.commons.lang3.StringUtils; +import org.json.JSONObject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; + +import java.net.InetSocketAddress; +import java.util.Date; + +public class ChatbotEventHandler { + private static final Logger logger = LoggerFactory.getLogger(ChatbotEventHandler.class); + + protected final SocketIOServer server; + + private static AgentUserRepository agentUserRes; + private static PassportWebIMUserRepository onlineUserRes; + private static ChatbotRepository chatbotRes; + private static ChatbotProxy chatbotProxy; + + @Autowired + public ChatbotEventHandler(SocketIOServer server) { + this.server = server; + } + + @OnConnect + public void onConnect(SocketIOClient client) { + try { + String user = client.getHandshakeData().getSingleUrlParam("userid"); + String nickname = client.getHandshakeData().getSingleUrlParam("nickname"); + String session = MainUtils.getContextID(client.getHandshakeData().getSingleUrlParam("session")); + String appid = client.getHandshakeData().getSingleUrlParam("appid"); + String aiid = client.getHandshakeData().getSingleUrlParam("aiid"); + logger.info( + "[onConnect] userid {}, nickname {}, session {}, appid {}, aiid {}", user, nickname, session, appid, + aiid); + + client.set("aiid", aiid); + client.set("session", session); + client.set("userid", user); + client.set("appid", appid); + + Date now = new Date(); + + if (StringUtils.isNotBlank(user)) { + /** + * 加入到 缓存列表 + */ + NettyClients.getInstance().putChatbotEventClient(user, client); + CousultInvite invite = OnlineUserProxy.consult(appid); + + /** + * 更新坐席服务类型 + */ + IMServiceUtils.shiftOpsType(user, MainContext.OptType.CHATBOT); + + // send out tip + Message tip = new Message(); + tip.setMessage("您正在使用机器人客服!"); + tip.setMessageType(MainContext.MessageType.MESSAGE.toString()); + tip.setCalltype(MainContext.CallType.IN.toString()); + tip.setCreatetime(MainUtils.dateFormate.format(now)); + + client.sendEvent(MainContext.MessageType.STATUS.toString(), tip); + + // send out welcome message + if (invite != null) { + Chatbot chatbot = getChatbotRes().findById(invite.getAiid()).orElse(null); + com.chatopera.bot.sdk.Chatbot bot = new com.chatopera.bot.sdk.Chatbot( + chatbot.getClientId(), chatbot.getSecret(), chatbot.getBaseUrl()); + Response result = bot.command("GET", "/"); + + // 发送欢迎语 + if (result.getRc() == 0) { + JSONObject details = (JSONObject) result.getData(); + ChatMessage welcome = new ChatMessage(); + String welcomeTextMessage = details.getString("welcome"); + if (StringUtils.isNotBlank(welcomeTextMessage)) { + welcome.setCalltype(MainContext.CallType.OUT.toString()); + welcome.setAppid(appid); + welcome.setAiid(aiid); + welcome.setMessage(welcomeTextMessage); + welcome.setTouser(user); + welcome.setMsgtype(MainContext.MessageType.MESSAGE.toString()); + welcome.setUserid(user); + welcome.setUsername(invite.getAiname()); + welcome.setUpdatetime(System.currentTimeMillis()); + client.sendEvent(MainContext.MessageType.MESSAGE.toString(), welcome); + } + + // 发送常见问题列表 + JSONObject faqhotresp = bot.conversation(user, "__faq_hot_list"); + logger.info("faqhot {}", faqhotresp.toString()); + if (faqhotresp.getInt("rc") == 0) { + JSONObject faqhotdata = faqhotresp.getJSONObject("data"); + if ((!faqhotdata.getBoolean("logic_is_fallback")) && + faqhotdata.has("string") && + faqhotdata.has("params")) { + ChatMessage faqhotmsg = new ChatMessage(); + faqhotmsg.setCalltype(MainContext.CallType.OUT.toString()); + faqhotmsg.setAppid(appid); + faqhotmsg.setAiid(aiid); + faqhotmsg.setMessage(faqhotdata.getString("string")); + faqhotmsg.setExpmsg(faqhotdata.getJSONArray("params").toString()); + faqhotmsg.setTouser(user); + faqhotmsg.setMsgtype(MainContext.MessageType.MESSAGE.toString()); + faqhotmsg.setUserid(user); + faqhotmsg.setUsername(invite.getAiname()); + faqhotmsg.setUpdatetime(System.currentTimeMillis()); + client.sendEvent(MainContext.MessageType.MESSAGE.toString(), faqhotmsg); + } + } + } else if (result.getRc() == 999 || result.getRc() == 998) { + logger.error("[chat] chatbot agent response rc {}, error {}", result.getRc(), result.getError()); + } else { + logger.error("[chat] chatbot agent response rc {}, error {}", result.getRc(), result.getError()); + } + } + + InetSocketAddress address = (InetSocketAddress) client.getRemoteAddress(); + String ip = MainUtils.getIpAddr(client.getHandshakeData().getHttpHeaders(), address.getHostString()); + PassportWebIMUser passportWebIMUser = getOnlineUserRes().findById(user).orElse(null); + + if (passportWebIMUser == null) { + passportWebIMUser = new PassportWebIMUser(); + passportWebIMUser.setAppid(appid); + if (StringUtils.isNotBlank(nickname)) { + passportWebIMUser.setUsername(nickname); + } else { + passportWebIMUser.setUsername(Constants.GUEST_USER + "_" + MainUtils.genIDByKey(user)); + } + + passportWebIMUser.setSessionid(session); + passportWebIMUser.setOptype(MainContext.OptType.CHATBOT.toString()); + passportWebIMUser.setUserid(user); + passportWebIMUser.setId(user); + passportWebIMUser.setChannel(MainContext.ChannelType.WEBIM.toString()); + passportWebIMUser.setIp(ip); + passportWebIMUser.setUpdatetime(now); + passportWebIMUser.setLogintime(now); + passportWebIMUser.setCreatetime(now); + IP ipdata = IPTools.getInstance().findGeography(ip); + passportWebIMUser.setCity(ipdata.getCity()); + passportWebIMUser.setCountry(ipdata.getCountry()); + passportWebIMUser.setProvince(ipdata.getProvince()); + passportWebIMUser.setIsp(ipdata.getIsp()); + passportWebIMUser.setRegion(ipdata.getRegion()); + passportWebIMUser.setStatus(MainContext.OnlineUserStatusEnum.ONLINE.toString()); + } + + // 在线客服访客咨询记录 + AgentUser agentUser = new AgentUser( + passportWebIMUser.getId(), + MainContext.ChannelType.WEBIM.toString(), // callout + passportWebIMUser.getId(), + passportWebIMUser.getUsername(), + appid); + + agentUser.setServicetime(now); + agentUser.setCreatetime(now); + agentUser.setUpdatetime(now); + agentUser.setSessionid(session); + agentUser.setRegion(passportWebIMUser.getRegion()); + + // 聊天机器人处理的请求 + agentUser.setOpttype(MainContext.OptType.CHATBOT.toString()); + agentUser.setAgentno(aiid); // 聊天机器人ID + agentUser.setAgentname(invite != null ? invite.getAiname() : "机器人客服"); + agentUser.setCity(passportWebIMUser.getCity()); + agentUser.setProvince(passportWebIMUser.getProvince()); + agentUser.setCountry(passportWebIMUser.getCountry()); + AgentService agentService = ACDServiceRouter.getAcdChatbotService().processChatbotService( + invite != null ? invite.getAiname() : "机器人客服", agentUser); + agentUser.setAgentserviceid(agentService.getId()); + + // 标记为机器人坐席 + agentUser.setChatbotops(true); + + // 保存到MySQL + getAgentUserRes().save(agentUser); + getOnlineUserRes().save(passportWebIMUser); + } + } catch (Exception e) { + logger.info("[onConnect] error", e); + } + } + + // 添加 @OnDisconnect 事件,客户端断开连接时调用,刷新客户端信息 + @OnDisconnect + public void onDisconnect(SocketIOClient client) { + String user = client.getHandshakeData().getSingleUrlParam("userid"); + if (StringUtils.isNotBlank(user)) { + NettyClients.getInstance().removeChatbotEventClient( + user, MainUtils.getContextID(client.getSessionId().toString())); + PassportWebIMUser passportWebIMUser = MainContext.getCache().findOneOnlineUserByUserId(user); + + MainContext.getCache().findOneAgentUserByUserId(user).ifPresent(p -> { + ACDServiceRouter.getAcdChatbotService().processChatbotService(null, p); + + MainContext.getCache().deleteAgentUserByUserId(p); + MainContext.getCache().deleteOnlineUserById(user); + + p.setStatus(MainContext.AgentUserStatusEnum.END.toString()); + passportWebIMUser.setStatus(MainContext.OnlineUserStatusEnum.OFFLINE.toString()); + + getAgentUserRes().save(p); + getOnlineUserRes().save(passportWebIMUser); + }); + } + client.disconnect(); + } + + // 消息接收入口,网站有新用户接入对话 + @OnEvent(value = "new") + public void onEvent(SocketIOClient client, AckRequest request, Message data) { + + } + + // 消息接收入口,坐席状态更新 + @OnEvent(value = "agentstatus") + public void onEvent(SocketIOClient client, AckRequest request, AgentStatusMessage data) { + logger.info("[onEvent] agentstatus: ", data.getMessage()); + } + + // 消息接收入口,收发消息,用户向机器人发送消息 + @OnEvent(value = "message") + public void onEvent(SocketIOClient client, AckRequest request, ChatMessage data) { + String aiid = client.get("aiid"); + String user = client.get("userid"); + String sessionid = client.get("session"); + String appid = client.get("appid"); + logger.info( + "[onEvent] message: session {}, aiid {}, userid {}, dataType {}, appid {}", sessionid, aiid, + user, data.getType(), appid); + + // ignore event if dataType is not message. + if (!StringUtils.equals(data.getType(), Constants.IM_MESSAGE_TYPE_MESSAGE)) { + return; + } + + MainContext.getCache().findOneAgentUserByUserId(user).ifPresent(p -> { + /** + * 以下代码主要用于检查 访客端的字数限制 + */ + CousultInvite invite = OnlineUserProxy.consult(data.getAppid()); + // ignore event if no invite found. + if (invite == null) { + return; + } + + // ignore if Chatbot is turnoff. + if (!invite.isAi()) { + return; + } + + Date now = new Date(); + if (invite.getMaxwordsnum() > 0) { + if (StringUtils.isNotBlank(data.getMessage()) && data.getMessage().length() > invite.getMaxwordsnum()) { + data.setMessage(data.getMessage().substring(0, invite.getMaxwordsnum())); + } + } else if (StringUtils.isNotBlank(data.getMessage()) && data.getMessage().length() > 300) { + data.setMessage(data.getMessage().substring(0, 300)); + } + + data.setUsession(user); // 绑定唯一用户 + data.setSessionid(sessionid); + data.setMessage(MainUtils.processEmoti(data.getMessage())); // 处理表情 + data.setTouser(aiid); + data.setUsername(p.getUsername()); + data.setAiid(aiid); + data.setAgentserviceid(p.getAgentserviceid()); + data.setChannel(p.getChanneltype()); + data.setContextid(p.getAgentserviceid()); // 一定要设置 ContextID + data.setCalltype(MainContext.CallType.IN.toString()); + + // 保存并发送消息给访客 + getChatbotProxy().createTextMessage( + data, + MainContext.CallType.IN.toString()); + + // 更新访客咨询记录 + p.setUpdatetime(now); + p.setLastmessage(now); + p.setLastmsg(data.getMessage()); + getAgentUserRes().save(p); + + // 发送消息给Bot + getChatbotProxy().publishMessage(data, Constants.CHATBOT_EVENT_TYPE_CHAT); + }); + + + } + + /** + * Lazy load + * + * @return + */ + private AgentUserRepository getAgentUserRes() { + if (agentUserRes == null) { + agentUserRes = MainContext.getContext().getBean(AgentUserRepository.class); + } + + return agentUserRes; + } + + /** + * Lazy load + * + * @return + */ + private ChatbotProxy getChatbotProxy() { + if (chatbotProxy == null) { + chatbotProxy = MainContext.getContext().getBean(ChatbotProxy.class); + } + return chatbotProxy; + } + + private PassportWebIMUserRepository getOnlineUserRes() { + if (onlineUserRes == null) { + onlineUserRes = MainContext.getContext().getBean(PassportWebIMUserRepository.class); + } + + return onlineUserRes; + } + + private ChatbotRepository getChatbotRes() { + if (chatbotRes == null) { + chatbotRes = MainContext.getContext().getBean(ChatbotRepository.class); + } + + return chatbotRes; + } +} diff --git a/contact-center/app/src/main/java/com/cskefu/cc/proxy/AgentUserProxy.java b/contact-center/app/src/main/java/com/cskefu/cc/proxy/AgentUserProxy.java index 58df9ddb..589e6d9d 100644 --- a/contact-center/app/src/main/java/com/cskefu/cc/proxy/AgentUserProxy.java +++ b/contact-center/app/src/main/java/com/cskefu/cc/proxy/AgentUserProxy.java @@ -1,14 +1,14 @@ -/* - * Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd. - * , Licensed under the Chunsong Public +/* + * Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd. + * , 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, , - * Licensed under the Apache License, Version 2.0, + * Copyright (C) 2019-2022 Chatopera Inc, , + * Licensed under the Apache License, Version 2.0, * http://www.apache.org/licenses/LICENSE-2.0 */ package com.cskefu.cc.proxy; @@ -34,6 +34,7 @@ import org.springframework.web.servlet.ModelAndView; import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; + import java.io.IOException; import java.util.*; @@ -96,6 +97,8 @@ public class AgentUserProxy { @Lazy private PeerSyncIM peerSyncIM; + @Autowired + private LicenseProxy licenseProxy; /** * 与联系人主动聊天前查找获取AgentUser diff --git a/contact-center/app/src/main/java/com/cskefu/cc/proxy/LicenseProxy.java b/contact-center/app/src/main/java/com/cskefu/cc/proxy/LicenseProxy.java new file mode 100644 index 00000000..ba8bc358 --- /dev/null +++ b/contact-center/app/src/main/java/com/cskefu/cc/proxy/LicenseProxy.java @@ -0,0 +1,565 @@ +/* + * Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd. + * , 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.proxy; + +import com.chatopera.store.enums.LICSTATUS; +import com.chatopera.store.sdk.QuotaWdClient; +import com.chatopera.store.sdk.Response; +import com.chatopera.store.sdk.exceptions.InvalidRequestException; +import com.chatopera.store.sdk.exceptions.InvalidResponseException; +import com.cskefu.cc.basic.Constants; +import com.cskefu.cc.basic.MainContext; +import com.cskefu.cc.basic.MainUtils; +import com.cskefu.cc.exception.*; +import com.cskefu.cc.model.AgentUser; +import com.cskefu.cc.model.ExecuteResult; +import com.cskefu.cc.model.MetaKv; +import com.cskefu.cc.persistence.repository.MetaKvRepository; +import com.cskefu.cc.util.Base62; +import com.cskefu.cc.util.DateConverter; +import org.apache.commons.lang3.StringUtils; +import org.json.JSONArray; +import org.json.JSONObject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.SpringApplication; +import org.springframework.stereotype.Service; + +import java.text.ParseException; +import java.util.*; + +/** + * 证书服务 + */ +@Service +public class LicenseProxy { + private final static Logger logger = LoggerFactory.getLogger(LicenseProxy.class); + + @Autowired + private MetaKvRepository metaKvRes; + + @Autowired + private QuotaWdClient quotaWdClient; + + private static final Map BILLING_RES_QUOTA_MAPPINGS = new HashMap<>(); + + static { + BILLING_RES_QUOTA_MAPPINGS.put(MainContext.BillingResource.USER, 100); + BILLING_RES_QUOTA_MAPPINGS.put(MainContext.BillingResource.AGENGUSER, 1); + BILLING_RES_QUOTA_MAPPINGS.put(MainContext.BillingResource.CHANNELWEBIM, 100); + BILLING_RES_QUOTA_MAPPINGS.put(MainContext.BillingResource.CONTACT, 1); + BILLING_RES_QUOTA_MAPPINGS.put(MainContext.BillingResource.ORGAN, 10); + } + + /** + * 初始化 serverinstId + * serverinstId 作为服务唯一的实例ID + */ + public void checkOnStartup() { + /** + * Check service connection + */ + System.out.println("[license] license service URL " + quotaWdClient.getBaseUrl()); + try { + Response resp = quotaWdClient.ping(); + System.out.println("[license] license service ping successfully."); + + if (resp.getRc() != 0) { + throw new InvalidResponseException("Unexpected response from license service " + resp.toString()); + } + } catch (InvalidResponseException e) { + logger.error("[license] make sure this host machine could connect to " + quotaWdClient.getBaseUrl() + " during running."); + logger.error("[license] checkOnStartup could not connect to license service, CSKeFu instance is terminated.", e); + // Very serious event happens, just shutdown the instance + SpringApplication.exit(MainContext.getContext(), () -> 1); + } + + /** + * Init local data for License + */ + resolveServerinstId(); + resolveServicename(); + resolveLicenseIds(); + } + + /** + * 读取或初始化 serverinstId + * + * @return + */ + public String resolveServerinstId() { + Optional metaServerinstIdOpt = metaKvRes.findFirstByMetakey(Constants.LICENSE_SERVER_INST_ID); + if (metaServerinstIdOpt.isEmpty()) { + // 没有 serverinstId 信息,初始化 + final String serverinstId = Base62.generateShortId(); + createMetaKv(Constants.LICENSE_SERVER_INST_ID, serverinstId, Constants.METAKV_DATATYPE_STRING); + return serverinstId; + } + return metaServerinstIdOpt.get().getMetavalue(); + } + + /** + * 读取或初始化 licenseIds + */ + private void resolveLicenseIds() { + Optional metaLicensesOpt = metaKvRes.findFirstByMetakey(Constants.LICENSEIDS); + if (metaLicensesOpt.isEmpty()) { + // 没有 license 信息,初始化 + createMetaKv(Constants.LICENSEIDS, (new JSONArray()).toString(), Constants.METAKV_DATATYPE_STRING); + } + } + + /** + * 读取或初始化 serviceName + * + * @return + */ + public String resolveServicename() { + Optional metaServicenameOpt = metaKvRes.findFirstByMetakey(Constants.LICENSE_SERVICE_NAME); + if (metaServicenameOpt.isEmpty()) { + // 没有 Service Name 信息,初始化 + final String serviceName = generateLicenseServiceName(); + createMetaKv(Constants.LICENSE_SERVICE_NAME, serviceName, Constants.METAKV_DATATYPE_STRING); + return serviceName; + } + return metaServicenameOpt.get().getMetavalue(); + } + + /** + * 从 MetaKv 表中取得数据 MetaKv + * + * @param key + * @return + * @throws MetaKvNotExistException + */ + public MetaKv retrieveMetaKv(final String key) throws MetaKvNotExistException, MetaKvInvalidKeyException { + + if (StringUtils.isBlank(key)) { + throw new MetaKvInvalidKeyException("Key must not be empy"); + } + + Optional kvOpt = metaKvRes.findFirstByMetakey(key); + if (kvOpt.isEmpty()) { + throw new MetaKvNotExistException(key + " not exist"); + } else { + return kvOpt.get(); + } + } + + /** + * 创建或更新 MetaKv + * UpdateOnExist + * + * @param key + * @param value + * @param datatype + */ + public MetaKv createOrUpdateMetaKv(final String key, final String value, final String datatype) throws MetaKvInvalidKeyException { + try { + MetaKv kv = retrieveMetaKv(key); + kv.setMetavalue(value); + kv.setUpdatetime(new Date()); + metaKvRes.save(kv); + return kv; + } catch (MetaKvNotExistException e) { + return createMetaKv(key, value, datatype); + } + } + + + /** + * 建立 Metakv 数据 + * + * @param key + * @param value + * @param datatype + */ + public MetaKv createMetaKv(final String key, final String value, final String datatype) { + Date now = new Date(); + MetaKv metakv = new MetaKv(); + metakv.setCreatetime(now); + metakv.setUpdatetime(now); + metakv.setMetakey(key); + metakv.setMetavalue(value); + metakv.setDatatype(datatype); + + metaKvRes.save(metakv); + return metakv; + } + + /** + * 增加 MetaKv 中的 Key 的值,作为 Integer 做增量,不存在则初始化其值为 0,然后增量操作 + * + * @param key + * @param incrValue + */ + public MetaKv increValueInMetaKv(final String key, final int incrValue) { + try { + MetaKv kv = retrieveMetaKv(key); + int pre = Integer.parseInt(kv.getMetavalue()); + kv.setMetavalue(Integer.toString(pre + incrValue)); + kv.setUpdatetime(new Date()); + metaKvRes.save(kv); + return kv; + } catch (MetaKvNotExistException e) { + return createMetaKv(key, Integer.toString(incrValue), Constants.METAKV_DATATYPE_INT); + } catch (MetaKvInvalidKeyException e) { + throw new RuntimeException(e); + } + } + + + /** + * 生成随机字符串,作为服务名称 + * + * @return + */ + private String generateLicenseServiceName() { + StringBuffer sb = new StringBuffer(); + + sb.append(Constants.LICENSE_SERVICE_NAME_PREFIX); + sb.append(Base62.generatingRandomAlphanumericString(5)); + + return sb.toString(); + } + + /** + * 从数据库及证书商店获得证书列表信息 + * + * @return + */ + public List getLicensesInStore() throws InvalidResponseException { + List result = new ArrayList<>(); + + try { + JSONArray ja = new JSONArray((retrieveMetaKv(Constants.LICENSEIDS).getMetavalue())); + HashMap addDates = new HashMap<>(); + List licenseIds = new ArrayList<>(); + for (int i = 0; i < ja.length(); i++) { + JSONObject obj = ((JSONObject) ja.get(i)); + licenseIds.add(obj.getString(Constants.SHORTID)); + addDates.put(obj.getString(Constants.SHORTID), obj.getString(Constants.ADDDATE)); + } + + Response resp = null; + try { + resp = quotaWdClient.getLicenseBasics(licenseIds); + } catch (InvalidRequestException e) { + return result; + } + JSONArray data = (JSONArray) resp.getData(); + + for (int i = 0; i < data.length(); i++) { + JSONObject lic = (JSONObject) data.get(i); + if(StringUtils.equals(lic.getJSONObject(Constants.LICENSE).getString(Constants.STATUS), "notfound")){ + // fill in placeholders for notfound license + final JSONObject licenseJsonTmp = lic.getJSONObject(Constants.LICENSE); + licenseJsonTmp.put("effectivedateend", "N/A"); + licenseJsonTmp.put("quotaeffectiveremaining", "N/A"); + + JSONObject productJsonTmp = new JSONObject(); + productJsonTmp.put("shortId", "N/A"); + productJsonTmp.put("name", "N/A"); + lic.put("product", productJsonTmp); + + JSONObject userJsonTmp = new JSONObject(); + userJsonTmp.put("nickname", "N/A"); + lic.put("user", userJsonTmp); + +// lic.put(Constants.ADDDATE, null); + + result.add(lic); + continue; + } + + try { + Date addDate = DateConverter.parseCSTAsChinaTimezone(addDates.get(lic.getJSONObject(Constants.LICENSE).getString(Constants.SHORTID))); + lic.put(Constants.ADDDATE, addDate); + } catch (ParseException e) { + logger.info("[getLicensesFromStore] can not resolve add date"); + } + result.add(lic); + } + + } catch (MetaKvNotExistException e) { + logger.info("[getLicenses] no LICENSEIDS data in MySQL DB"); + } catch (MetaKvInvalidKeyException e) { + throw new RuntimeException(e); + } + + return result; + } + + /** + * 获得在 MetaKV 表中的 license 信息 + * + * @return JSONArray + */ + public JSONArray getLicensesInMetakv() { + try { + String value = retrieveMetaKv(Constants.LICENSEIDS).getMetavalue(); + return new JSONArray(value); + } catch (MetaKvNotExistException e) { + return new JSONArray(); + } catch (MetaKvInvalidKeyException e) { + return new JSONArray(); + } + } + + /** + * @param licenseShortId + * @return + * @throws InvalidResponseException + */ + public JSONObject getLicenseBasicsInStore(final String licenseShortId) throws InvalidResponseException, InvalidRequestException { + Response resp = quotaWdClient.getLicenseBasics(licenseShortId); + if (resp.getRc() == 0) { + JSONArray data = (JSONArray) resp.getData(); + if (data.length() != 1) + throw new InvalidResponseException("Unexpected data in Response."); + + return (JSONObject) (data).get(0); + } else { + throw new InvalidResponseException("Unexpected Response."); + } + + } + + /** + * 获得已经添加的证书在 Store 中的基本信息 + * + * @return + * @throws InvalidResponseException + */ + public JSONArray getAddedLicenseBasicsInStore() throws InvalidResponseException { + JSONArray arr = getLicensesInMetakv(); + List ids = new ArrayList<>(); + + for (int i = 0; i < arr.length(); i++) { + ids.add(((JSONObject) arr.get(i)).getString(Constants.SHORTID)); + } + + if (ids.size() > 0) { + Response resp = null; + try { + resp = quotaWdClient.getLicenseBasics(StringUtils.join(ids, ",")); + } catch (InvalidRequestException e) { + logger.error("[getAddedLicenseBasicsFromStore] InvalidRequestException", e); + } + if (resp.getRc() != 0) { + throw new InvalidResponseException("Invalid response, rc " + Integer.toString(resp.getRc())); + } + + return (JSONArray) resp.getData(); + } else { + logger.error("[license] getAddedLicenseBasicsFromStore - No license ids in metaKv"); + return new JSONArray(); + } + } + + + /** + * 验证证书存在 + * + * @param licenseShortId + * @return + */ + public LICSTATUS existLicenseInStore(final String licenseShortId) throws InvalidResponseException, LicenseNotFoundException, InvalidRequestException { + Map statuses = quotaWdClient.getLicenseStatus(licenseShortId); + + if (statuses.size() == 1) { + for (final Map.Entry entry : statuses.entrySet()) { + final LICSTATUS status = entry.getValue(); + + if (status == LICSTATUS.NOTFOUND) + throw new LicenseNotFoundException("LicenseId not found [" + licenseShortId + "]"); + + return status; + } + throw new InvalidResponseException("Unexpected response, internal error."); + } else { + throw new InvalidResponseException("Unexpected response, should contain one record."); + } + } + + public String getLicenseStoreProvider() { + return quotaWdClient.getBaseUrl(); + } + + /** + * 获得资源用量存储 + * + * @param resourceKey + * @return + */ + public String getResourceUsageKey(final String resourceKey) { + StringBuffer sb = new StringBuffer(); + sb.append(Constants.RESOURCES_USAGE_KEY_PREFIX); + sb.append("_"); + sb.append(StringUtils.toRootUpperCase(resourceKey)); + return sb.toString(); + } + + + /** + * 增加计费资源用量 + * + * @param billingResource + * @param consume + */ + public void increResourceUsageInMetaKv(final MainContext.BillingResource billingResource, int consume) throws BillingResourceException { + switch (billingResource) { + case USER: + case CONTACT: + case ORGAN: + case AGENGUSER: + case CHANNELWEBIM: + increValueInMetaKv(getResourceUsageKey(billingResource.toString()), consume); + break; + default: + throw new BillingResourceException("invalid_billing_resource_type"); + } + } + + /** + * 获取在 MetaKv 中资源的已经使用的计数 + * + * @param billingResource + * @return + */ + private int getResourceUsageInMetaKv(final MainContext.BillingResource billingResource) { + final String key = getResourceUsageKey(billingResource.toString()); + try { + MetaKv kv = retrieveMetaKv(key); + int pre = Integer.parseInt(kv.getMetavalue()); + return pre; + } catch (MetaKvNotExistException e) { + createMetaKv(key, Integer.toString(0), Constants.METAKV_DATATYPE_INT); + return 0; + } catch (MetaKvInvalidKeyException e) { + return 0; + } + } + + /** + * 获得春松客服 cskefu001 产品的证书标识 ID + * 春松客服证书基本类型 + * + * @return + */ + private String getLicenseIdAsCskefu001InMetaKv() throws BillingQuotaException { + JSONArray curr = getLicensesInMetakv(); + for (int i = 0; i < curr.length(); i++) { + JSONObject jo = (JSONObject) curr.get(i); + if (jo.has(Constants.PRODUCT_ID) && + StringUtils.equals(jo.getString(Constants.PRODUCT_ID), Constants.PRODUCT_ID_CSKEFU001)) { + return jo.getString(Constants.SHORTID); + } + } + throw new BillingQuotaException(BillingQuotaException.NO_LICENSE_FOUND); + } + + /** + * 执行配额变更操作 + * + * @param billingResource + * @param unitNum + * @return + */ + public void writeDownResourceUsageInStore(final MainContext.BillingResource billingResource, + int unitNum) throws BillingQuotaException, BillingResourceException { + + // 检查是否还在体验阶段 + if (billingResource == MainContext.BillingResource.CONTACT) { + int alreadyUsed = getResourceUsageInMetaKv(billingResource); + if (alreadyUsed <= 1) { + // 可以免费创建 1 个联系人 + return; + } + } + + // 请求操作配额 + String licenseId = getLicenseIdAsCskefu001InMetaKv(); + String serverinstId = resolveServerinstId(); + String servicename = resolveServicename(); + + try { + Response resp = quotaWdClient.write(licenseId, + serverinstId, servicename, unitNum * BILLING_RES_QUOTA_MAPPINGS.get(billingResource)); + // 识别操作是否完成,并处理 + if (resp.getRc() == 0) { + final JSONObject data = (JSONObject) resp.getData(); + + // 配额操作成功,执行计数 + increResourceUsageInMetaKv(billingResource, unitNum); + } else if (resp.getRc() == 1 || resp.getRc() == 2) { + throw new BillingQuotaException(BillingQuotaException.INVALID_REQUEST_BODY); + } else if (resp.getRc() == 3) { + // 证书商店中不存在 + throw new BillingQuotaException(BillingQuotaException.LICENSE_INVALID); + } else if (resp.getRc() == 4) { + // 证书商店中不存在该产品或产品类型无效 + throw new BillingQuotaException(BillingQuotaException.PRODUCT_INVALID); + } else if (resp.getRc() == 5) { + // 该证书不支持资源回退 + throw new BillingQuotaException(BillingQuotaException.LICENSE_UNSUPPORT_REFUND); + } else if (resp.getRc() == 6) { + // 证书失效或耗尽,不支持继续扣除配额 + throw new BillingQuotaException(BillingQuotaException.LICENSE_EXPIRED_OR_EXHAUSTED); + } else if (resp.getRc() == 7) { + // 该证书禁用了该 serverinstId + throw new BillingQuotaException(BillingQuotaException.LICENSE_DISABLED_SERVERINST); + } else if (resp.getRc() == 8) { + // 配额扣除额度超过该证书目前的剩余量 + throw new BillingQuotaException(BillingQuotaException.LICENSE_QUOTA_INADEQUATE); + } else { + // 未知情况 + logger.error("[writeDownResourceUsageInStore] resp data {}", resp.toString()); + throw new BillingQuotaException(BillingQuotaException.INTERNAL_ERROR); + } + } catch (InvalidResponseException e) { + // TODO 处理异常信息 + logger.error("[writeDownResourceUsageInStore] error ", e); + throw new BillingQuotaException(BillingQuotaException.RESPONSE_UNEXPECTED); + } + } + + /** + * 访客会话执行计费 + * + * @param agentUser + * @return + */ + public ExecuteResult writeDownAgentUserUsageInStore(final AgentUser agentUser) { + // 检查是否还在体验阶段 + ExecuteResult er = new ExecuteResult(); + int alreadyUsed = getResourceUsageInMetaKv(MainContext.BillingResource.AGENGUSER); + if (alreadyUsed <= 100) { + // 可以免费创建 100 个访客会话 + er.setRc(ExecuteResult.RC_SUCC); + return er; + } + + try { + writeDownResourceUsageInStore(MainContext.BillingResource.AGENGUSER, 1); + er.setRc(ExecuteResult.RC_SUCC); + } catch (BillingQuotaException e) { + er.setRc(ExecuteResult.RC_ERR1); + er.setMsg(e.getMessage()); + } catch (BillingResourceException e) { + er.setRc(ExecuteResult.RC_ERR2); + er.setMsg(e.getMessage()); + } + + return er; + } +} diff --git a/contact-center/app/src/main/java/com/cskefu/cc/proxy/UserProxy.java b/contact-center/app/src/main/java/com/cskefu/cc/proxy/UserProxy.java index 74309ac3..83f2f086 100644 --- a/contact-center/app/src/main/java/com/cskefu/cc/proxy/UserProxy.java +++ b/contact-center/app/src/main/java/com/cskefu/cc/proxy/UserProxy.java @@ -1,13 +1,13 @@ -/* - * Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd. - * , Licensed under the Chunsong Public +/* + * Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd. + * , 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. + * Copyright (C) 2019-2022 Chatopera Inc, All rights reserved. * */ @@ -16,6 +16,7 @@ package com.cskefu.cc.proxy; import com.cskefu.cc.basic.Constants; import com.cskefu.cc.basic.MainContext; import com.cskefu.cc.basic.MainUtils; +import com.cskefu.cc.exception.BillingQuotaException; import com.cskefu.cc.model.*; import com.cskefu.cc.persistence.repository.*; import com.cskefu.cc.util.restapi.RestUtils; @@ -29,6 +30,8 @@ import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Component; import jakarta.persistence.criteria.Predicate; + +import java.lang.reflect.UndeclaredThrowableException; import java.util.*; import java.util.stream.Collectors; @@ -79,22 +82,34 @@ public class UserProxy { public JsonObject createNewUser(final User user, Organ organ) { JsonObject result = new JsonObject(); String msg = validUser(user); - if (StringUtils.equalsIgnoreCase(msg, "new_user_success")) { + + if (StringUtils.equalsIgnoreCase(msg, Constants.NEW_USER_SUCCESS)) { // 此时 msg 是 new_user_success user.setSuperadmin(false); // 不支持创建第二个系统管理员 + try { + if (StringUtils.isNotBlank(user.getPassword())) { + user.setPassword(MainUtils.md5(user.getPassword())); + } + userRes.save(user); - if (StringUtils.isNotBlank(user.getPassword())) { - user.setPassword(MainUtils.md5(user.getPassword())); - } - userRes.save(user); - - if (organ != null) { - OrganUser ou = new OrganUser(); - ou.setUserid(user.getId()); - ou.setOrgan(organ.getId()); - organUserRes.save(ou); + if (organ != null) { + OrganUser ou = new OrganUser(); + ou.setUserid(user.getId()); + ou.setOrgan(organ.getId()); + organUserRes.save(ou); + } + } catch (Exception e) { + if (e instanceof UndeclaredThrowableException) { + logger.error("[createNewUser] BillingQuotaException", e); + if (StringUtils.startsWith(e.getCause().getMessage(), BillingQuotaException.SUFFIX)) { + msg = e.getCause().getMessage(); + } + } else { + logger.error("[createNewUser] err", e); + } } } + // 新账号未通过验证,返回创建失败信息msg result.addProperty(RestUtils.RESP_KEY_RC, RestUtils.RESP_RC_SUCC); result.addProperty(RestUtils.RESP_KEY_DATA, msg); diff --git a/contact-center/app/src/main/java/com/cskefu/cc/util/Base62.java b/contact-center/app/src/main/java/com/cskefu/cc/util/Base62.java index 597c9150..132fbbba 100644 --- a/contact-center/app/src/main/java/com/cskefu/cc/util/Base62.java +++ b/contact-center/app/src/main/java/com/cskefu/cc/util/Base62.java @@ -1,63 +1,99 @@ /* - * Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd. - * , Licensed under the Chunsong Public + * Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd. + * , 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, , Licensed under the Apache License, Version 2.0, + * Copyright (C) 2018- Jun. 2023 Chatopera Inc, , 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, + * Copyright (C) 2017 优客服-多渠道客服系统, Licensed under the Apache License, Version 2.0, * http://www.apache.org/licenses/LICENSE-2.0 */ package com.cskefu.cc.util; import org.apache.commons.codec.digest.DigestUtils; +import org.apache.commons.lang3.RandomStringUtils; import org.apache.commons.lang3.StringUtils; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Random; + public class Base62 { - private static final int BINARY = 0x2; + private static final int BINARY = 0x2; - private static final int NUMBER_61 = 0x0000003d; - - static final char[] DIGITS = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f', 'g', - 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'A', 'B', - 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', - 'X', 'Y', 'Z' }; + private static final int NUMBER_61 = 0x0000003d; + + static final char[] DIGITS = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f', 'g', + 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'A', 'B', + 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', + 'X', 'Y', 'Z'}; - public static String encode(long value){ - return encode(String.valueOf(value)).toLowerCase() ; - } - - public static String encode(String str){ - String md5Hex = DigestUtils.md5Hex(str); - // 6 digit binary can indicate 62 letter & number from 0-9a-zA-Z - int binaryLength = 6 * 6; - long binaryLengthFixer = Long.valueOf(StringUtils.repeat("1", binaryLength), BINARY); - for (int i = 0; i < 4;) { - String subString = StringUtils.substring(md5Hex, i * 8, (i + 1) * 8); - subString = Long.toBinaryString(Long.valueOf(subString, 16) & binaryLengthFixer); - subString = StringUtils.leftPad(subString, binaryLength, "0"); - StringBuilder sbBuilder = new StringBuilder(); - for (int j = 0; j < 6; j++) { - String subString2 = StringUtils.substring(subString, j * 6, (j + 1) * 6); - int charIndex = Integer.valueOf(subString2, BINARY) & NUMBER_61; - sbBuilder.append(DIGITS[charIndex]); - } - String shortUrl = sbBuilder.toString(); - if(shortUrl!=null){ - return shortUrl; - } - } - // if all 4 possibilities are already exists - return null; - } - + public static String encode(long value) { + return encode(String.valueOf(value)).toLowerCase(); + } + + public static String encode(String str) { + String md5Hex = DigestUtils.md5Hex(str); + // 6 digit binary can indicate 62 letter & number from 0-9a-zA-Z + int binaryLength = 6 * 6; + long binaryLengthFixer = Long.valueOf(StringUtils.repeat("1", binaryLength), BINARY); + for (int i = 0; i < 4; ) { + String subString = StringUtils.substring(md5Hex, i * 8, (i + 1) * 8); + subString = Long.toBinaryString(Long.valueOf(subString, 16) & binaryLengthFixer); + subString = StringUtils.leftPad(subString, binaryLength, "0"); + StringBuilder sbBuilder = new StringBuilder(); + for (int j = 0; j < 6; j++) { + String subString2 = StringUtils.substring(subString, j * 6, (j + 1) * 6); + int charIndex = Integer.valueOf(subString2, BINARY) & NUMBER_61; + sbBuilder.append(DIGITS[charIndex]); + } + String shortUrl = sbBuilder.toString(); + if (shortUrl != null) { + return shortUrl; + } + } + // if all 4 possibilities are already exists + return null; + } + @SuppressWarnings("unused") - private static void print(Object messagr){ - System.out.println(messagr); - } + private static void print(Object messagr) { + System.out.println(messagr); + } + + + /** + * 生成 16 位随机字符串作为 ID + * https://stackoverflow.com/questions/4267475/generating-8-character-only-uuids + */ + public static String generateShortId() { + SimpleDateFormat df = new SimpleDateFormat("yyMMdd"); + String randomStr = RandomStringUtils.randomAlphanumeric(10); + return df.format(new Date()) + randomStr; + } + + /** + * 生成随机字符串 + * + * @param targetStringLength + * @return + */ + public static String generatingRandomAlphanumericString(final int targetStringLength) { + int leftLimit = 48; // numeral '0' + int rightLimit = 122; // letter 'z' + Random random = new Random(); + + String generatedString = random.ints(leftLimit, rightLimit + 1) + .filter(i -> (i <= 57 || i >= 65) && (i <= 90 || i >= 97)) + .limit(targetStringLength) + .collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append) + .toString(); + + return generatedString; + } } \ No newline at end of file diff --git a/contact-center/app/src/main/java/com/cskefu/cc/util/DateConverter.java b/contact-center/app/src/main/java/com/cskefu/cc/util/DateConverter.java index 1a419fa4..8942a975 100644 --- a/contact-center/app/src/main/java/com/cskefu/cc/util/DateConverter.java +++ b/contact-center/app/src/main/java/com/cskefu/cc/util/DateConverter.java @@ -1,54 +1,74 @@ /* - * Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd. - * , Licensed under the Chunsong Public + * Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd. + * , 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, , Licensed under the Apache License, Version 2.0, + * Copyright (C) 2018- Jun. 2023 Chatopera Inc, , 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, + * Copyright (C) 2017 优客服-多渠道客服系统, Licensed under the Apache License, Version 2.0, * http://www.apache.org/licenses/LICENSE-2.0 */ package com.cskefu.cc.util; import org.apache.commons.beanutils.converters.DateTimeConverter; +import java.text.ParseException; +import java.text.SimpleDateFormat; import java.util.Date; - -public class DateConverter extends DateTimeConverter { - - public DateConverter() { - } - - public DateConverter(Object defaultValue) { - super(defaultValue); - } - - /* (non-Javadoc) - * @see org.apache.commons.beanutils.converters.AbstractConverter#getDefaultType() - */ - @SuppressWarnings("rawtypes") - protected Class getDefaultType() { - return Date.class; - } - - /* - * (non-Javadoc) - * @see org.apache.commons.beanutils.converters.DateTimeConverter#convertToType(java.lang.Class, java.lang.Object) - */ - @SuppressWarnings("rawtypes") - @Override - protected Object convertToType(Class arg0, Object arg1) throws Exception { - if (arg1 == null) { - return null; - } - String value = arg1.toString().trim(); - if (value.length() == 0) { - return null; - } - return super.convertToType(arg0, arg1); - } +import java.util.Locale; + +public class DateConverter extends DateTimeConverter { + + final public static String ZONE_ID_DEFAULT = "Asia/Shanghai"; + // format date string like `Wed Aug 30 16:30:23 CST 2023` to Date + public static SimpleDateFormat TIMEZONE_CHINA_FORMAT_DEFAULT = new SimpleDateFormat("EEE MMM dd HH:mm:ss zzz yyyy", Locale.US); + + public DateConverter() { + } + + public DateConverter(Object defaultValue) { + super(defaultValue); + } + + /* (non-Javadoc) + * @see org.apache.commons.beanutils.converters.AbstractConverter#getDefaultType() + */ + @SuppressWarnings("rawtypes") + protected Class getDefaultType() { + return Date.class; + } + + /* + * (non-Javadoc) + * @see org.apache.commons.beanutils.converters.DateTimeConverter#convertToType(java.lang.Class, java.lang.Object) + */ + @SuppressWarnings("rawtypes") + @Override + protected Object convertToType(Class arg0, Object arg1) throws Exception { + if (arg1 == null) { + return null; + } + String value = arg1.toString().trim(); + if (value.length() == 0) { + return null; + } + return super.convertToType(arg0, arg1); + } + + /** + * Java将CST的时间字符串转换成需要的日期格式字符串 + * https://blog.csdn.net/qq_44868502/article/details/103511505 + * (new Date()).toString() 与 String to Date 的转化 + * + * @param dstr + * @return + * @throws ParseException + */ + static public Date parseCSTAsChinaTimezone(final String dstr) throws ParseException { + return (Date) TIMEZONE_CHINA_FORMAT_DEFAULT.parse(dstr); + } } \ No newline at end of file diff --git a/contact-center/app/src/main/java/com/cskefu/cc/util/PugHelper.java b/contact-center/app/src/main/java/com/cskefu/cc/util/PugHelper.java index f0b1d424..93934acd 100644 --- a/contact-center/app/src/main/java/com/cskefu/cc/util/PugHelper.java +++ b/contact-center/app/src/main/java/com/cskefu/cc/util/PugHelper.java @@ -12,18 +12,34 @@ package com.cskefu.cc.util; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONArray; +import com.chatopera.store.enums.LICSTATUS; +import com.chatopera.store.exceptions.EnumValueException; +import org.apache.commons.lang3.StringUtils; import java.text.SimpleDateFormat; import java.util.*; public class PugHelper { - public String formatDate(String pattern, Date value) { - if (value == null) { - return ""; - } + public final static String NA = "N/A"; - SimpleDateFormat format = new SimpleDateFormat(pattern); - return format.format(value); + + public String formatDate(String pattern, Date value) { + try { + if (value == null) { + return NA; + } + + SimpleDateFormat format = new SimpleDateFormat(pattern); + String result = format.format(value); + + if (StringUtils.isBlank(result)) { + return NA; + } + return result; + } catch (Exception e) { + e.printStackTrace(); + } + return NA; } public String padRight(Object src, String ch) { @@ -42,8 +58,39 @@ public class PugHelper { return new String(charr); } + /** + * 在字符串中替换一些字符为 *, 起到混淆、加密、遮盖的敏感信息的目的 + * + * @param prev + * @return + */ + public String messupStringWithStars(final String prev) { + StringBuffer sb = new StringBuffer(); + + if (prev.length() >= 6) { + sb.append("***"); + int initial = prev.length() - 4; + for (int i = initial; i < prev.length(); i++) { + sb.append(prev.charAt(i)); + } + } else { // < 6 + if (prev.length() <= 2 && prev.length() > 0) { + return "***"; + } else { // 2 < length < 6 + sb.append("***"); + int initial = prev.length() - 2; + for (int i = initial; i < prev.length(); i++) { + sb.append(prev.charAt(i)); + } + } + } + + return sb.toString(); + } + /** * 将 String 转化为 JSONArray + * * @param str * @return */ @@ -72,4 +119,59 @@ public class PugHelper { Collections.reverse(result); return result; } + + /** + * 获得证书状态的中文 + * + * @param status + * @return + */ + public String getLicstatusInChinese(final String status) { + try { + LICSTATUS licstatus = LICSTATUS.toValue(status); + switch (licstatus) { + case NOTFOUND -> { + return "未找到"; + } + case EXHAUSTED -> { + return "配额耗尽"; + } + case INUSE -> { + return "使用中"; + } + case EXPIRED -> { + return "已过期"; + } + default -> { + return status; + } + } + } catch (EnumValueException e) { + return "未知"; + } + } + + /** + * 截取字符串,首先根据分隔符分隔,然后选取前 N 个,使用连接符连接返回 + * + * @param orignal + * @param splitBy + * @param firstN + * @param joinWith + * @return + */ + public String splitStringAndJoinWith(final String orignal, final String splitBy, final int firstN, final String joinWith) { + String[] splits = StringUtils.split(orignal, splitBy); + int n = Math.min(splits.length, firstN); + List joined = new ArrayList<>(); + for (int i = 0; i < n; i++) { + joined.add(splits[i]); + } + + if (joined.size() > 0) { + return StringUtils.join(joined, joinWith); + } else { + return ""; + } + } } diff --git a/contact-center/app/src/main/resources/application.properties b/contact-center/app/src/main/resources/application.properties index f39822a5..c7c352b7 100644 --- a/contact-center/app/src/main/resources/application.properties +++ b/contact-center/app/src/main/resources/application.properties @@ -76,6 +76,7 @@ spring.datasource.password=123456 spring.jpa.properties.hibernate.current_session_context_class=org.springframework.orm.hibernate5.SpringSessionContext spring.jpa.show-sql=false spring.jpa.properties.hibernate.format_sql=true +spring.jpa.properties.hibernate.allow_update_outside_transaction=true ############################################## # Cache @@ -152,13 +153,19 @@ cskefu.modules.cca=true cskefu.modules.entim=false cskefu.modules.report=true +############################################## +# Channels +############################################## +channel.skype.crm= # https://gitlab.chatopera.com/chatopera/cosinee/issues/838 cskefu.settings.webim.visitor-separate=false ############################################## -# Skype Channel +# License ############################################## -channel.skype.crm= +# License Service Provider URL +license.store.provider=https://store.chatopera.com + ############################################## # Telemetry @@ -171,14 +178,4 @@ telemetry.channel.webim.visitor=on extras.login.banner=off extras.login.chatbox=off extras.auth.super-admin.pass= -extras.log.request=off - -spring.jpa.properties.hibernate.allow_update_outside_transaction=true - -############################################## -# ssl -############################################## -# server.ssl.key-store=classpath:cskefu.jks -# server.ssl.key-alias=cskefu -# server.ssl.key-store-password=123456 -# server.http2.enabled=true +extras.log.request=off \ No newline at end of file diff --git a/contact-center/app/src/main/resources/static/js/CSKeFu_Admin.v1.js b/contact-center/app/src/main/resources/static/js/CSKeFu_Admin.v1.js index 553c3f9d..c0e5d766 100644 --- a/contact-center/app/src/main/resources/static/js/CSKeFu_Admin.v1.js +++ b/contact-center/app/src/main/resources/static/js/CSKeFu_Admin.v1.js @@ -11,50 +11,145 @@ * Licensed under the Apache License, Version 2.0 * http://www.apache.org/licenses/LICENSE-2.0 */ -function processUserAddOrUpdateResult(responsecode, cb){ +/** + * 处理系统用户的创建的返回值 + * @param responsecode + * @param cb + */ +function processUserAddOrUpdateResult(responsecode, cb) { switch (responsecode) { case 'username_exist': - layer.msg('用户名存在,请重新填写',{icon: 2, time: 3000}); + layer.msg('用户名存在,请重新填写', {icon: 2, time: 3000}); // 清空用户名 $('input[name="username"]').val(""); break; case 'email_exist': - layer.msg('邮件存在,请重新填写',{icon: 2, time: 3000}); + layer.msg('邮件存在,请重新填写', {icon: 2, time: 3000}); // 清空邮件 $('input[name="email"]').val(""); break; case 'mobile_exist': - layer.msg('手机存在,请重新填写',{icon: 2, time: 3000}); + layer.msg('手机存在,请重新填写', {icon: 2, time: 3000}); // 清空手机号 $('input[name="mobile"]').val(""); break; case 'sip_account_exist': - layer.msg('SIP地址已经存在,请重新填写',{icon: 2, time: 3000}); + layer.msg('SIP地址已经存在,请重新填写', {icon: 2, time: 3000}); // 清空SIP $('input[name="sipaccount"]').val(""); break; case 'extension_binded': - layer.msg('分机号已经被其他用户绑定',{icon: 2, time: 3000}); + layer.msg('分机号已经被其他用户绑定', {icon: 2, time: 3000}); $('input[name="extensionid"]').val(""); break; case 'extension_not_exist': - layer.msg('绑定分机不存在',{icon: 2, time: 3000}); + layer.msg('绑定分机不存在', {icon: 2, time: 3000}); $('input[name="extensionid"]').val(""); break; case 'pbxhost_not_exist': - layer.msg('指定的呼叫中心语音平台不存在',{icon: 2, time: 3000}); + layer.msg('指定的呼叫中心语音平台不存在', {icon: 2, time: 3000}); $('input[name="pbxhostid"]').val(""); break; case 't1': - layer.msg('当前用户坐席就绪或对话未结束,不能切换为非坐席',{icon: 2, time: 3000}); + layer.msg('当前用户坐席就绪或对话未结束,不能切换为非坐席', {icon: 2, time: 3000}); break; case 'new_user_success': - layer.msg('新用户创建成功',{icon: 1, time: 1000}); + layer.msg('新用户创建成功', {icon: 1, time: 1000}); cb(); break; case 'edit_user_success': - layer.msg('用户编辑成功',{icon: 1, time: 1000}); + layer.msg('用户编辑成功', {icon: 1, time: 1000}); cb(); break; + default: + handleGeneralCodeInQueryPathOrApiResp(responsecode, cb); + } +} + +/** + * 处理在 RedirectURL, API 中返回的 code 信息: status, msg, etc. + * code 为约定的返回值,通过下面的函数进行展示 + * @param code + * @param cb + */ +function handleGeneralCodeInQueryPathOrApiResp(code, cb) { + switch (code) { + case 'billingquotaexception.no_license_found': + layer.confirm('证书不存在,联系系统超级管理员导入。', { + title: '使用授权证书', icon: 2, btn: [ + '查看说明' + ], + btn1: function (index, layero, that) { + // 查看说明 + window.open("https://docs.cskefu.com/docs/licenses", "_blank"); + return false; + } + }); + if (cb && (typeof (x) === 'function')) { + cb() + } + break; + case 'billingquotaexception.response_unexpected': + layer.msg('【使用授权证书】证书商店返回异常,稍后再试。', {icon: 2, time: 5000}); + if (cb && (typeof (x) === 'function')) { + cb() + } + break; + case 'billingquotaexception.invalid_request_body': + layer.msg('【使用授权证书】请求证书商店参数不合法,请获取最新软件代码。', {icon: 2, time: 5000}); + if (cb && (typeof (x) === 'function')) { + cb() + } + break; + case 'billingquotaexception.license_invalid': + layer.msg('【使用授权证书】证书商店中不存在该证书,请联系系统超级管理员导入。', {icon: 2, time: 5000}); + if (cb && (typeof (x) === 'function')) { + cb() + } + break; + case 'billingquotaexception.product_invalid': + layer.msg('【使用授权证书】产品或产品款式不存在,请联系系统超级管理员导入新证书。', {icon: 2, time: 5000}); + if (cb && (typeof (x) === 'function')) { + cb() + } + break; + case 'billingquotaexception.license_expired_or_exhausted': + layer.msg('【使用授权证书】证书过期或耗尽,请升级证书或绑定新证书。', {icon: 2, time: 5000}); + if (cb && (typeof (x) === 'function')) { + cb() + } + break; + case 'billingquotaexception.license_disabled_serverinst': + layer.msg('【使用授权证书】证书商店禁用了本服务实例。', {icon: 2, time: 5000}); + if (cb && (typeof (x) === 'function')) { + cb() + } + break; + case 'billingquotaexception.license_unsupport_refund': + layer.msg('【使用授权证书】目前使用的证书不支持回退配额。', {icon: 2, time: 5000}); + if (cb && (typeof (x) === 'function')) { + cb() + } + break; + case 'billingquotaexception.license_quota_inadequate': + layer.msg('【使用授权证书】本次操作需要使用的配额资源超过证书中剩余配额,请联系系统超级管理员升级证书。', { + icon: 2, + time: 5000 + }); + if (cb && (typeof (x) === 'function')) { + cb() + } + break; + case 'billingquotaexception.internal_error': + layer.msg('【使用授权证书】系统错误,稍后再试。', {icon: 2, time: 5000}); + if (cb && (typeof (x) === 'function')) { + cb() + } + break; + default: + console.log("[handleGeneralCodeInQueryPathOrApiResp] none code matched", code); + if (cb && (typeof (x) === 'function')) { + cb("no_code_matched"); + } } } \ No newline at end of file diff --git a/contact-center/app/src/main/resources/static/js/CSKeFu_IM.v1.js b/contact-center/app/src/main/resources/static/js/CSKeFu_IM.v1.js index 24b22cca..e369db7c 100644 --- a/contact-center/app/src/main/resources/static/js/CSKeFu_IM.v1.js +++ b/contact-center/app/src/main/resources/static/js/CSKeFu_IM.v1.js @@ -11,196 +11,203 @@ * Licensed under the Apache License, Version 2.0 * http://www.apache.org/licenses/LICENSE-2.0 */ -var title = "春松客服-开源客服系统" ; -var socket , newuser = [] , newmessage = [] , ring = []; -newuser['mp3'] = '/images/new.mp3'; +var title = "春松客服-开源客服系统"; +var socket, newuser = [], newmessage = [], ring = []; +newuser['mp3'] = '/images/new.mp3'; newmessage['mp3'] = '/images/message.mp3'; ring['mp3'] = '/images/ring.mp3'; -$(document).ready(function(){ - var protocol = window.location.protocol.replace(/:/g,''); - socket = io(protocol+'://'+hostname+':'+port+"/im/agent?userid="+userid+"&session="+session+"&admin="+adminuser , {transports: ['websocket'], upgrade: false}); - socket.on('connect',function() { - console.log("[IM] 连接初始化成功"); - //请求服务端记录 当前用户在线事件 - }).on('disconnect',function() { - console.log("[IM] 连接已断开"); - //请求服务端记录,当前用户离线 +$(document).ready(function () { + var protocol = window.location.protocol.replace(/:/g, ''); + socket = io(protocol + '://' + hostname + ':' + port + "/im/agent?userid=" + userid + "&session=" + session + "&admin=" + adminuser, { + transports: ['websocket'], + upgrade: false + }); + socket.on('connect', function () { + console.log("[IM] 连接初始化成功"); + //请求服务端记录 当前用户在线事件 + }).on('disconnect', function () { + console.log("[IM] 连接已断开"); + //请求服务端记录,当前用户离线 }); - - socket.on('chatevent', function(data) { - // console.log(data.messageType + " ..... message:"+data.message); - }).on('task', function(data) { - - }).on('new', function(data) { - if($('#customerChatAudit').length > 0){ - if(customerChatAudit.$('#agentuser_' + data.userid).length > 0 && customerChatAudit.$("#chat_users li").length>1){ - customerChatAudit.$('#agentuser_' + data.userid).remove(); - customerChatAudit.$("#chat_users li:first-child a").click(); - }else{ - customerChatAudit.$('#ccaIndex').html("
\n" + - "
\n" + - "
\n" + - "
\n" + - " \n" + - "
还没有任何对话
\n" + - "
\n" + - "
\n" + - "
\n" + - "
"); - } - } - if($('#multiMediaDialogWin').length > 0 && multiMediaDialogWin != null && multiMediaDialogWin.$ &&multiMediaDialogWin.$('#agentusers').length > 0){ - multiMediaDialogWin.Proxy.newAgentUserService(data,"agent"); - }else{ - //来电弹屏 - $('#agentdesktop').attr('data-href' , '/agent/index.html?userid='+data.userid).click(); - WebIM.audioplayer('audioplane', newuser, false); // 播放 - } - }).on('status', function(data) { - $.post('/lazyAgentStatus').success(function(html){ - $('#agents_status').html(html); - }); - // $('#agents_status').html("服务中的人数:"+data.users+"人,当前排队人数:"+data.inquene+"人,在线坐席数:"+data.agents+"人,坐席忙:"+data.busy+"人"); - }).on('message', function(data) { - if($('#multiMediaDialogWin').length > 0 && multiMediaDialogWin != null && multiMediaDialogWin.$ && multiMediaDialogWin.$('#agentusers').length > 0){ - multiMediaDialogWin.Proxy.newAgentUserMessage(data,"agent"); - if(data.type == 'message'){ - WebIM.audioplayer('audioplane', newmessage, false); // 播放 - if(multiMediaDialogWin.isAisuggest && multiMediaDialogWin.isAisuggest == "true"){ - multiMediaDialogWin.Proxy.quickReply(data,"agent"); - } - } - }else{ - //来电弹屏 - $('#agentdesktop').attr('data-href' , '/agent/index.html?userid='+data.userid).click(); - } - }).on('workorder', function(data) { - }).on('transout', function(data){ - // TODO 坐席会话被转接出去 - if($('#multiMediaDialogWin').length > 0){ - if(multiMediaDialogWin.document.getElementById('agentusers') != null){ - multiMediaDialogWin.Proxy.transoutAgentUserService(data); - } - } - layer.msg("您与"+data.username+"的会话已被转接给"+data.agentname,{time:1500}) + socket.on('chatevent', function (data) { + // console.log(data.messageType + " ..... message:"+data.message); + }).on('task', function (data) { + }).on('new', function (data) { + console.log("new data ...", data); + if ($('#customerChatAudit').length > 0) { + if (customerChatAudit.$('#agentuser_' + data.userid).length > 0 && customerChatAudit.$("#chat_users li").length > 1) { + customerChatAudit.$('#agentuser_' + data.userid).remove(); + customerChatAudit.$("#chat_users li:first-child a").click(); + } else { + customerChatAudit.$('#ccaIndex').html("
\n" + + "
\n" + + "
\n" + + "
\n" + + " \n" + + "
还没有任何对话
\n" + + "
\n" + + "
\n" + + "
\n" + + "
"); + } + } - }).on('audit_message', function(data){ - // 会话监控:消息 - if($('#customerChatAudit').length > 0 && customerChatAudit != null && customerChatAudit.$ && customerChatAudit.$('#agentuserscca').length > 0){ - customerChatAudit.Proxy.newAgentUserMessage(data,"cca"); - if(data.type == 'message'){ - WebIM.audioplayer('audioplane', newmessage, false); // 播放 - if(customerChatAudit.isCcaAisuggest && customerChatAudit.isCcaAisuggest == "true"){ - customerChatAudit.Proxy.quickReply(data,"cca"); - } - } - } - }).on('audit_new', function(data){ - // 会话监控:新建 - if(skills.indexOf(data.skill)>-1 && $('#customerChatAudit').length > 0 && customerChatAudit != null && customerChatAudit.$){ - customerChatAudit.Proxy.newAgentUserService(data,"cca"); - if(data.type == 'message'){ - WebIM.audioplayer('audioplane', newmessage, false); // 播放 - } - } - }).on('audit_end', function(data){ - // 会话监控:结束 - if($('#customerChatAudit').length > 0){ - if(customerChatAudit.document.getElementById('agentuserscca') != null){ - customerChatAudit.Proxy.endAgentUserService(data); - } - } - }).on('end', function(data) { - console.warn(111111, data) - if($('#multiMediaDialogWin').length > 0){ - if(multiMediaDialogWin.document.getElementById('agentusers') != null){ - multiMediaDialogWin.Proxy.endAgentUserService(data); - } - }else{ - //来电弹屏 - $('#agentdesktop').attr('data-href', '/agent/index.html?userid='+data.userid).click(); - } - }).on('leave', function(data){ - top.layer.msg('当前会话已经过期,稍后将自动登出!',{icon: 1, time: 2000}); - setTimeout(function(){ - // 执行登出 - window.location.href = "/logout.html?code=2"; - }, 2000); - }); - /****每分钟执行一次,与服务器交互,保持会话****/ - setInterval(function(){ - WebIM.ping(); - } , 60000); + if ($('#multiMediaDialogWin').length > 0 && + multiMediaDialogWin != null && + multiMediaDialogWin.$ && + multiMediaDialogWin.$('#agentusers').length > 0) { + multiMediaDialogWin.Proxy.newAgentUserService(data, "agent"); + } else { + //来电弹屏 + $('#agentdesktop').attr('data-href', '/agent/index.html?userid=' + data.userid + '&licenseVerifiedPass=' + data.licenseVerifiedPass + '&licenseBillingMsg=' + data.licenseBillingMsg).click(); + WebIM.audioplayer('audioplane', newuser, false); // 播放 + } + }).on('status', function (data) { + $.post('/lazyAgentStatus').success(function (html) { + $('#agents_status').html(html); + }); + // $('#agents_status').html("服务中的人数:"+data.users+"人,当前排队人数:"+data.inquene+"人,在线坐席数:"+data.agents+"人,坐席忙:"+data.busy+"人"); + }).on('message', function (data) { + if ($('#multiMediaDialogWin').length > 0 && multiMediaDialogWin != null && multiMediaDialogWin.$ && multiMediaDialogWin.$('#agentusers').length > 0) { + multiMediaDialogWin.Proxy.newAgentUserMessage(data, "agent"); + if (data.type == 'message') { + WebIM.audioplayer('audioplane', newmessage, false); // 播放 + if (multiMediaDialogWin.isAisuggest && multiMediaDialogWin.isAisuggest == "true") { + multiMediaDialogWin.Proxy.quickReply(data, "agent"); + } + } + } else { + //来电弹屏 + $('#agentdesktop').attr('data-href', '/agent/index.html?userid=' + data.userid).click(); + } + }).on('workorder', function (data) { + + }).on('transout', function (data) { + // TODO 坐席会话被转接出去 + if ($('#multiMediaDialogWin').length > 0) { + if (multiMediaDialogWin.document.getElementById('agentusers') != null) { + multiMediaDialogWin.Proxy.transoutAgentUserService(data); + } + } + layer.msg("您与" + data.username + "的会话已被转接给" + data.agentname, {time: 1500}) + + }).on('audit_message', function (data) { + // 会话监控:消息 + if ($('#customerChatAudit').length > 0 && customerChatAudit != null && customerChatAudit.$ && customerChatAudit.$('#agentuserscca').length > 0) { + customerChatAudit.Proxy.newAgentUserMessage(data, "cca"); + if (data.type == 'message') { + WebIM.audioplayer('audioplane', newmessage, false); // 播放 + if (customerChatAudit.isCcaAisuggest && customerChatAudit.isCcaAisuggest == "true") { + customerChatAudit.Proxy.quickReply(data, "cca"); + } + } + } + }).on('audit_new', function (data) { + // 会话监控:新建 + if (skills.indexOf(data.skill) > -1 && $('#customerChatAudit').length > 0 && customerChatAudit != null && customerChatAudit.$) { + customerChatAudit.Proxy.newAgentUserService(data, "cca"); + if (data.type == 'message') { + WebIM.audioplayer('audioplane', newmessage, false); // 播放 + } + } + }).on('audit_end', function (data) { + // 会话监控:结束 + if ($('#customerChatAudit').length > 0) { + if (customerChatAudit.document.getElementById('agentuserscca') != null) { + customerChatAudit.Proxy.endAgentUserService(data); + } + } + }).on('end', function (data) { + console.warn(111111, data) + if ($('#multiMediaDialogWin').length > 0) { + if (multiMediaDialogWin.document.getElementById('agentusers') != null) { + multiMediaDialogWin.Proxy.endAgentUserService(data); + } + } else { + //来电弹屏 + $('#agentdesktop').attr('data-href', '/agent/index.html?userid=' + data.userid).click(); + } + }).on('leave', function (data) { + top.layer.msg('当前会话已经过期,稍后将自动登出!', {icon: 1, time: 2000}); + setTimeout(function () { + // 执行登出 + window.location.href = "/logout.html?code=2"; + }, 2000); + }); + /****每分钟执行一次,与服务器交互,保持会话****/ + setInterval(function () { + WebIM.ping(); + }, 60000); }); var WebIM = { - sendMessage:function(message , userid , appid , session , touser , agentstatus, agentuserid){ - WebIM.sendTypeMessage(message, userid, appid, session, touser, agentstatus, null , null,agentuserid) ; - }, - sendTypeMessage:function(message , userid , appid , session , touser , agentstatus , msgtype , attachmentid,agentuserid){ - socket.emit('message', { - appid : appid , - userid:userid, - sign:session, - touser:touser, - session: session, - username:agentstatus, - nickname:agentstatus, - message : message, - msgtype:msgtype, - attachmentid:attachmentid, - agentuser:agentuserid + sendMessage: function (message, userid, appid, session, touser, agentstatus, agentuserid) { + WebIM.sendTypeMessage(message, userid, appid, session, touser, agentstatus, null, null, agentuserid); + }, + sendTypeMessage: function (message, userid, appid, session, touser, agentstatus, msgtype, attachmentid, agentuserid) { + socket.emit('message', { + appid: appid, + userid: userid, + sign: session, + touser: touser, + session: session, + username: agentstatus, + nickname: agentstatus, + message: message, + msgtype: msgtype, + attachmentid: attachmentid, + agentuser: agentuserid }); - }, - // 发送会话监控干预消息 - sendIntervention: function(supervisorid, agentuserid, session, msgtype, content, extra){ - socket.emit('intervention', { - supervisorid: supervisorid, // 坐席监控人员ID,必须 - agentuserid: agentuserid, // 坐席访客会话ID,必须 - msgtype: msgtype, // 干预消息类型, 必须 - content: content, // 干预消息内容, 必须 - session: session, // 登录会话ID - extra: extra // 其它可选属性, 可选 - }); - }, - ping : function(){ - loadURL("/message/ping.html"); - console.log("[IM] heartbeat:" + new Date().getTime()); - }, - audioplayer:function(id, file, loop) { - var audioplayer = document.getElementById(id); - if (audioplayer != null) { - document.body.removeChild(audioplayer); - } + }, + // 发送会话监控干预消息 + sendIntervention: function (supervisorid, agentuserid, session, msgtype, content, extra) { + socket.emit('intervention', { + supervisorid: supervisorid, // 坐席监控人员ID,必须 + agentuserid: agentuserid, // 坐席访客会话ID,必须 + msgtype: msgtype, // 干预消息类型, 必须 + content: content, // 干预消息内容, 必须 + session: session, // 登录会话ID + extra: extra // 其它可选属性, 可选 + }); + }, + ping: function () { + loadURL("/message/ping.html"); + console.log("[IM] heartbeat:" + new Date().getTime()); + }, + audioplayer: function (id, file, loop) { + var audioplayer = document.getElementById(id); + if (audioplayer != null) { + document.body.removeChild(audioplayer); + } - if (typeof(file) != 'undefined') { - if (navigator.userAgent.indexOf("MSIE") > 0) { // IE - var player = document.createElement('bgsound'); - player.id = id; - player.src = file['mp3']; - player.setAttribute('autostart', 'true'); - if (loop) { - player.setAttribute('loop', 'infinite'); - } - document.body.appendChild(player); + if (typeof (file) != 'undefined') { + if (navigator.userAgent.indexOf("MSIE") > 0) { // IE + var player = document.createElement('bgsound'); + player.id = id; + player.src = file['mp3']; + player.setAttribute('autostart', 'true'); + if (loop) { + player.setAttribute('loop', 'infinite'); + } + document.body.appendChild(player); - } else { // Other FF Chome Safari Opera - var player = document.createElement('audio'); - player.id = id; - player.setAttribute('autoplay', 'autoplay'); - if (loop) { - player.setAttribute('loop', 'loop'); - } - document.body.appendChild(player); + } else { // Other FF Chome Safari Opera + var player = document.createElement('audio'); + player.id = id; + player.setAttribute('autoplay', 'autoplay'); + if (loop) { + player.setAttribute('loop', 'loop'); + } + document.body.appendChild(player); - var mp3 = document.createElement('source'); - mp3.src = file['mp3']; - mp3.type = 'audio/mpeg'; - player.appendChild(mp3); - } - } - } + var mp3 = document.createElement('source'); + mp3.src = file['mp3']; + mp3.type = 'audio/mpeg'; + player.appendChild(mp3); + } + } + } } diff --git a/contact-center/app/src/main/resources/static/js/cskefu.js b/contact-center/app/src/main/resources/static/js/cskefu.js index a0f17e02..0c01fc96 100644 --- a/contact-center/app/src/main/resources/static/js/cskefu.js +++ b/contact-center/app/src/main/resources/static/js/cskefu.js @@ -343,22 +343,23 @@ function newMessageScorllBottom(type, msgType) { var Proxy = { newAgentUserService: function (data, type) { + console.log("newAgentUserService data type", data, type) if ($('#tip_message_' + data.userid).length > 0) { var channel = data.channeltype if (channel) { if (channel === 'phone') { $('#tip_icon_phone_' + data.userid).attr("src", "/images/phone-ico.png"); } else { - $('#tip_icon_' +channel+ '_' + data.userid).removeClass('ukefu-channel-icon-end').addClass("ukefu-channel-icon"); + $('#tip_icon_' + channel + '_' + data.userid).removeClass('ukefu-channel-icon-end').addClass("ukefu-channel-icon"); } } $('#tip_message_' + data.userid).removeClass('bg-gray').addClass("bg-green").text('在线'); } else { if ($('.chat-list-item.active').length > 0) { var id = $('.chat-list-item.active').data('id'); - type == "agent" ? loadURL('/agent/agentusers.html?newuser=true&userid=' + id, '#agentusers') : loadURL('/apps/cca/agentusers.html?newuser=true&userid=' + id, '#agentuserscca'); + type == "agent" ? loadURL('/agent/agentusers.html?newuser=true&userid=' + id + '&licenseVerifiedPass=' + data.licenseVerifiedPass + '&licenseBillingMsg=' + data.licenseBillingMsg, '#agentusers') : loadURL('/apps/cca/agentusers.html?newuser=true&userid=' + id + "&licenseVerifiedPass=" + data.licenseVerifiedPass + "&licenseBillingMsg=" + data.licenseBillingMsg, '#agentuserscca'); } else { - type == "agent" ? location.href = "/agent/index.html?newuser=true" : location.href = "/apps/cca/index.html?newuser=true"; + type == "agent" ? location.href = "/agent/index.html?newuser=true&licenseVerifiedPass=" + data.licenseVerifiedPass + "&licenseBillingMsg=" + data.licenseBillingMsg : location.href = "/apps/cca/index.html?newuser=true&licenseVerifiedPass=" + data.licenseVerifiedPass + "&licenseBillingMsg=" + data.licenseBillingMsg; } } if (data.userid == cursession) { @@ -485,7 +486,7 @@ var Proxy = { if (channel === 'phone') { $('#tip_icon_phone_' + data.userid).attr("src", "/images/cde-ico-gray.png"); } else { - $('#tip_icon_'+ channel +'_' + data.userid).removeClass("ukefu-channel-icon").addClass('ukefu-channel-icon-end'); + $('#tip_icon_' + channel + '_' + data.userid).removeClass("ukefu-channel-icon").addClass('ukefu-channel-icon-end'); } } $('#tip_message_' + data.userid).removeClass("bg-green").addClass('bg-gray').text('离开'); @@ -504,7 +505,11 @@ var Proxy = { } }, tipMsgForm: function (href) { - top.layer.prompt({formType: 2, title: '请输入拉黑原因', area: ['300px', '50px']}, function (value, index, elem) { + top.layer.prompt({ + formType: 2, + title: '请输入拉黑原因', + area: ['300px', '50px'] + }, function (value, index, elem) { location.href = href + "&description=" + encodeURIComponent(value); top.layer.close(index); }); diff --git a/contact-center/app/src/main/resources/static/js/utils.js b/contact-center/app/src/main/resources/static/js/utils.js index 92ba115a..885427ab 100644 --- a/contact-center/app/src/main/resources/static/js/utils.js +++ b/contact-center/app/src/main/resources/static/js/utils.js @@ -312,6 +312,21 @@ var arrayListPrototype = { ArrayList.prototype = arrayListPrototype; +/** + * 复制值到系统粘贴板 + * https://www.freecodecamp.org/news/copy-text-to-clipboard-javascript/ + * @param val + */ +function copyValue2ClipboardOnOS(val, cb) { + navigator.clipboard.writeText(val).then(() => { + /* Resolved - text copied to clipboard successfully */ + if(cb && typeof cb === 'function') cb(); + },(err) => { + /* Rejected - text failed to copy to the clipboard */ + if(cb && typeof cb === 'function') cb(err || "Fail"); + }); +} + /*! Math.uuid.js (v1.4) diff --git a/contact-center/app/src/main/resources/templates/admin/channel/im/index.pug b/contact-center/app/src/main/resources/templates/admin/channel/im/index.pug index 6e47a370..1cc3695c 100644 --- a/contact-center/app/src/main/resources/templates/admin/channel/im/index.pug +++ b/contact-center/app/src/main/resources/templates/admin/channel/im/index.pug @@ -63,13 +63,13 @@ block content layui.use('layer', function () { var layer = layui.layer; - console.log(window.location.href) var status = '#{status}'; if (status == 'new_webim_success') layer.msg('网站添加成功', {icon: 1, time: 1000}) else if (status == 'new_webim_fail') layer.msg('网站添加失败', {icon: 2, time: 3000}) - + else + handleGeneralCodeInQueryPathOrApiResp(status); }); layui.use(['laypage', 'layer'], function () { var laypage = layui.laypage diff --git a/contact-center/app/src/main/resources/templates/admin/include/layout.pug b/contact-center/app/src/main/resources/templates/admin/include/layout.pug index 94d08136..6bd354e0 100644 --- a/contact-center/app/src/main/resources/templates/admin/include/layout.pug +++ b/contact-center/app/src/main/resources/templates/admin/include/layout.pug @@ -26,12 +26,14 @@ html(xmlns='http://www.w3.org/1999/xhtml', xmlns:th='http://www.thymeleaf.org', link(rel='stylesheet', href='/css/layui.css') link(rel='stylesheet', href='/res/css.html') link(rel='stylesheet', href='/css/flexboxgrid.min.css') + script(src='/js/utils.js') script(src='/js/jquery-1.10.2.min.js') script(src='/js/jquery.form.js') script(src='/js/ztree/jquery.ztree.all.min.js') script(src='/js/echarts.common.min.js') script(language='javascript', src='/js/theme/wonderland.js') script(src='/layui.js') + script(src="/js/CSKeFu_Admin.v1.js") script(src='/js/cskefu.js') if userExpTelemetry == 'on' script(src='https://www.googletagmanager.com/gtag/js?id=G-SBBX10RKTC' async) diff --git a/contact-center/app/src/main/resources/templates/admin/include/left.pug b/contact-center/app/src/main/resources/templates/admin/include/left.pug index 0b10bb6c..c8af840c 100644 --- a/contact-center/app/src/main/resources/templates/admin/include/left.pug +++ b/contact-center/app/src/main/resources/templates/admin/include/left.pug @@ -51,6 +51,13 @@ ul.layui-nav.layui-nav-tree(lay-filter='demo') dd(class={'layui-this': subtype == 'interf'}) a(href='/admin/weixin/interf.html') 接口管理 | –> + if user.superadmin + li.layui-nav-item.layui-nav-itemed + a.layui-nav-title(href='javascript:;') 人工智能 + dl.layui-nav-child + if models.contains("chatbot") && (user.roleAuthMap["A09"] || user.admin) + dd + a(href='javascript:void(0)',data-title="智能机器人",onclick="openChatbot()",data-href="/admin/system/chatbot/index.html",class="iframe_btn",data-id="chatbotIntegrationWin", data-type="tabAdd") 智能机器人 if user.superadmin li.layui-nav-item.layui-nav-itemed a.layui-nav-title(href='javascript:;') 系统设置 @@ -61,16 +68,20 @@ ul.layui-nav.layui-nav-tree(lay-filter='demo') a(href='/admin/sysdic/index.html') 字典管理 dd(class={'layui-this': subtype == 'metadata'}) a(href='/admin/metadata/index.html') 元数据 - if models.contains("chatbot") && (user.roleAuthMap["A09"] || user.admin) - dd - a(href='javascript:void(0)',data-title="智能机器人",onclick="openChatbot()",data-href="/admin/system/chatbot/index.html",class="iframe_btn",data-id="chatbotIntegrationWin", data-type="tabAdd") 智能机器人 dd(class={'layui-this': subtype == 'template'}) a(href='/admin/template/index.html') 系统模板 - dd(class={'layui-this': subtype == 'email'}) - a(href='/admin/email/index.html') 邮件通知设置 - dd(class={'layui-this': subtype == 'sms'}) - a(href='/admin/sms/index.html') 短信通知设置 - + //dd(class={'layui-this': subtype == 'email'}) + // a(href='/admin/email/index.html') 邮件通知设置 + //dd(class={'layui-this': subtype == 'sms'}) + // a(href='/admin/sms/index.html') 短信通知设置 + if user.superadmin + li.layui-nav-item.layui-nav-itemed + a.layui-nav-title(href='javascript:;') 使用授权 + dl.layui-nav-child + dd(class={'layui-this': subtype == 'licenseInst'}) + a(href='/admin/license/instance.html') 实例信息 + dd(class={'layui-this': subtype == 'licenseList'}) + a(href='/admin/license/index.html') 授权证书列表 script. function openChatbot() { window.parent.active.tabAdd($(".iframe_btn").data('href'), $(".iframe_btn").data('title'), $(".iframe_btn").data('id')); diff --git a/contact-center/app/src/main/resources/templates/admin/license/add.pug b/contact-center/app/src/main/resources/templates/admin/license/add.pug new file mode 100644 index 00000000..152451c1 --- /dev/null +++ b/contact-center/app/src/main/resources/templates/admin/license/add.pug @@ -0,0 +1,31 @@ +//- Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd. +//- , 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. +.uk-layui-form + form.layui-form(action='/admin/license/save.html', method='post') + .layui-form-item(style='margin-top:10px;') + .layui-inline + label.layui-form-label(style='width:150px;') 证书标识: + .layui-input-inline + input.layui-input(type='text', name='licenseShortId', required, lay-verify='required', autocomplete='off') + .layui-form-mid.layui-word-aux + font(color='red') * + .layui-form-button + .layui-button-block + button.layui-btn(lay-submit, lay-filter='formNewLicense') 立即提交 + button.layui-btn.layui-btn-original(type='reset') 重置 + +script. + layui.use('form', function () { + var form = layui.form(); + form.render(); //更新全部 + }); + layui.use('element', function () { + var element = layui.element(); + }); + diff --git a/contact-center/app/src/main/resources/templates/admin/license/index.pug b/contact-center/app/src/main/resources/templates/admin/license/index.pug new file mode 100644 index 00000000..f5d00ac9 --- /dev/null +++ b/contact-center/app/src/main/resources/templates/admin/license/index.pug @@ -0,0 +1,100 @@ +//- Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd. +//- , 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. +extends /admin/include/layout.pug +block content + .row: .col-lg-12 + h1.site-h1(style='background-color:#FFFFFF;') + | 使用授权证书列表 (#{licenses.size()}) + | ,更新时间 #{pugHelper.formatDate('yyyy-MM-dd HH:mm:ss', updateTime)} + span(style='float:right;') + .layui-btn-group.ukefu-btn-group + button.layui-btn.layui-btn-small(href='/admin/license/add.html', data-toggle='ajax', data-width='550', data-height='450', data-title='添加使用授权证书') + i.layui-icon  + | 导入 + button.layui-btn.layui-btn-small(onclick='location.reload()') + span 刷新 + button.layui-btn.layui-btn-warm.layui-btn-small(onclick='openLicenseStorePage()') + span 购买使用授权证书 + + .row(style='padding:5px;') + blockquote.layui-elem-quote.layui-quote-nm + i.layui-icon(style="color:gray")  + font(color="#999").layui-word-aux 春松客服使用授权证书是通过 Chatopera 证书商店(https://store.chatopera.com)分发的对【春松客服计费资源】进行管理的凭证,在使用春松客服的过程中,春松客服与 Chatopera 证书商店集成,完成证书购买、证书绑定、配额扣除、配额同步和开具发票等。 + + .col-lg-12 + table.layui-table(lay-skin='line') + colgroup + col(width='10%') + col(width='10%') + col(width='10%') + col(width='20%') + col(width='10%') + col(width='10%') + col(width='10%') + col(width='10%') + col(width='10%') + thead + tr + th 证书 ID + th 状态 + th 产品标识 + th 产品名称 + th 有效期截止 + th 配额剩余 + th 所属人昵称 + th 添加时间 + th(style='white-space:nowrap;', nowrap) 操作 + tbody + for item in licenses + tr + - var messupLicenseShortId = pugHelper.messupStringWithStars(item.license.shortId) + td= messupLicenseShortId + td= pugHelper.getLicstatusInChinese(item.license.status) + td= item.product.shortId + td= item.product.name + td= pugHelper.splitStringAndJoinWith(item.license.effectivedateend, " ", 1, "") + td= item.license.quotaeffectiveremaining + td= item.user.nickname + td= pugHelper.formatDate('yyyy-MM-dd', item.addDate) + td(style="white-space:nowrap;" nowrap="nowrap") + a(href="#", onclick="copyLicenseId2ClipboardOnOS('" + item.license.shortId + "');return false;") + i.layui-icon  + span 复制 ID + a(href="/admin/license/delete/" + item.license.shortId + ".html" style="margin-left:10px;" data-toggle="tip" title="请确认是否删除使用授权证书 " + messupLicenseShortId + "?") + i.layui-icon(style="color:red;") ဆ + span 删除 + .row(style='padding:5px;') + .col-lg-12#page(style='text-align:center;') + + script. + var msg = '#{msg}'; + if (msg == 'already_added') + top.layer.alert('已经添加,不需要再次执行。', {icon: 1}); + else if (msg == 'product_added_already') + top.layer.alert('同产品证书已经添加,不支持继续添加。', {icon: 2}); + else if (msg == 'invalid_id') + top.layer.alert('不合法的证书标识', {icon: 2}); + else if (msg == 'notfound_id') + top.layer.alert('不存在该证书信息', {icon: 2}); + + function copyLicenseId2ClipboardOnOS(val){ + copyValue2ClipboardOnOS(val, (err) => { + top.layer.msg('复制完成', {icon: 1, time: 2000, offset: 't'}); + }) + } + + function openLicenseStorePage() { + var licenseStoreProvider = "#{licenseStoreProvider}/product/cskefu001"; + window.open(licenseStoreProvider, "_blank"); + } + + layui.use(['laypage', 'layer'], function () { + var laypage = layui.laypage + , layer = layui.layer; + }); \ No newline at end of file diff --git a/contact-center/app/src/main/resources/templates/admin/license/instance.pug b/contact-center/app/src/main/resources/templates/admin/license/instance.pug new file mode 100644 index 00000000..4be914c0 --- /dev/null +++ b/contact-center/app/src/main/resources/templates/admin/license/instance.pug @@ -0,0 +1,59 @@ +//- Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd. +//- , 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. +extends /admin/include/layout.pug +block content + .row: .col-lg-12 + h1.site-h1(style='background-color:#FFFFFF;') + | 实例信息 + span(style='float:right;') + .layui-btn-group.ukefu-btn-group + button.layui-btn.layui-btn-warm.layui-btn-small(onclick='openLicenseStorePage()') + span 购买使用授权证书 + + .row(style='padding:5px;') + blockquote.layui-elem-quote.layui-quote-nm + i.layui-icon(style="color:gray")  + font(color="#999").layui-word-aux 春松客服实例信息,代表该春松客服软件运行的标识信息。在春松客服使用授权中,每个实例为一个可以绑定授权证书的单位。 + + .col-lg-12 + fieldset.layui-elem-field + legend(style='font-size:13px!important;') 实例 ID + span.layui-field-box #{SERVERINSTID} + span    + button(style='font-size:10px!important;' onclick='copyServerinstId2ClipboardOnOS("' +SERVERINSTID + '")') + span 复制 + + fieldset.layui-elem-field + legend(style='font-size:13px!important;') 实例名称 + span.layui-field-box #{SERVICENAME} + span    + button(style='font-size:10px!important;' onclick='copyServicename2ClipboardOnOS("' +SERVICENAME + '")') + span 复制 + script. + layui.use(['laypage', 'layer'], function () { + var laypage = layui.laypage + , layer = layui.layer; + }); + + function copyServerinstId2ClipboardOnOS(val){ + copyValue2ClipboardOnOS(val, (err) => { + top.layer.msg('复制完成', {icon: 1, time: 2000, offset: 't'}); + }) + } + + function copyServicename2ClipboardOnOS(val){ + copyValue2ClipboardOnOS(val, (err) => { + top.layer.msg('复制完成', {icon: 1, time: 2000, offset: 't'}); + }) + } + + function openLicenseStorePage() { + var licenseStoreProvider = "#{licenseStoreProvider}/product/cskefu001"; + window.open(licenseStoreProvider, "_blank"); + } \ No newline at end of file diff --git a/contact-center/app/src/main/resources/templates/admin/organ/index.pug b/contact-center/app/src/main/resources/templates/admin/organ/index.pug index d057b2f9..a0e0965d 100644 --- a/contact-center/app/src/main/resources/templates/admin/organ/index.pug +++ b/contact-center/app/src/main/resources/templates/admin/organ/index.pug @@ -189,6 +189,8 @@ block content layer.msg('修改无法完成,上级机构选择错误', {icon: 2, time: 2000}) } else if (msg == 'not_allow_remove_user') { layer.msg('用户只有一个组织,不允许移除', {icon: 2, time: 2000}) + } else { + handleGeneralCodeInQueryPathOrApiResp(msg); } }); diff --git a/contact-center/app/src/main/resources/templates/admin/user/index.pug b/contact-center/app/src/main/resources/templates/admin/user/index.pug index c6641ab6..47ff38ea 100644 --- a/contact-center/app/src/main/resources/templates/admin/user/index.pug +++ b/contact-center/app/src/main/resources/templates/admin/user/index.pug @@ -121,6 +121,8 @@ block content layer.msg('新用户创建成功',{icon: 1, time: 1000}) else if (msg == 'edit_user_success') layer.msg('用户编辑成功', {icon: 1, time: 1000}) + else if (msg == 'billingquotaexception.no_license_found') + layer.msg('证书不存在,联系系统超级管理员导入授权使用证书',{icon: 2, time: 3000}) }); layui.use(['laypage', 'layer'], function(){ var laypage = layui.laypage diff --git a/contact-center/app/src/main/resources/templates/apps/agent/agentusers.pug b/contact-center/app/src/main/resources/templates/apps/agent/agentusers.pug index a4aee649..d1868ff8 100644 --- a/contact-center/app/src/main/resources/templates/apps/agent/agentusers.pug +++ b/contact-center/app/src/main/resources/templates/apps/agent/agentusers.pug @@ -80,3 +80,11 @@ .last-msg small.ukefu-badge.bg-red(id="last_msg_" + agentuser.userid,style="#{(agentuser.tokenum == 0 || (curagentuser && curagentuser.id == agentuser.id)) ? 'display:none' : ''}") | #{agentuser.tokenum ? agentuser.tokenum : 0} +script(language="javascript"). + $(document).ready(function () { + var licenseVerifiedPass = #{licenseVerifiedPass}; + var licenseBillingMsg = '#{licenseBillingMsg}'; + if (licenseBillingMsg) { + handleGeneralCodeInQueryPathOrApiResp(licenseBillingMsg); + } + }); \ No newline at end of file diff --git a/contact-center/app/src/main/resources/templates/apps/agent/calloutcontact/add.pug b/contact-center/app/src/main/resources/templates/apps/agent/calloutcontact/add.pug index 52fb88e5..980b40e8 100644 --- a/contact-center/app/src/main/resources/templates/apps/agent/calloutcontact/add.pug +++ b/contact-center/app/src/main/resources/templates/apps/agent/calloutcontact/add.pug @@ -24,7 +24,6 @@ html h1.site-h1(style='background-color:#FFFFFF;') 新建联系人 form.layui-form(action='/agent/calloutcontact/save.html?agentuser=${curagentuser.id!\'\'}', method='post') input(hidden, name='calloutcontact') - input(type='hidden', name='shares', value='all') .layui-collapse .layui-colla-item h2.layui-colla-title 基本信息 diff --git a/contact-center/app/src/main/resources/templates/apps/agent/calloutcontact/edit.pug b/contact-center/app/src/main/resources/templates/apps/agent/calloutcontact/edit.pug index 69350948..ee892312 100644 --- a/contact-center/app/src/main/resources/templates/apps/agent/calloutcontact/edit.pug +++ b/contact-center/app/src/main/resources/templates/apps/agent/calloutcontact/edit.pug @@ -23,7 +23,6 @@ html h1.site-h1(style='background-color:#FFFFFF;') 编辑联系人 form.layui-form(action='/agent/calloutcontact/update.html', method='post') input(type='hidden', name='id', value='${contacts.id!\'\'}') - input(type='hidden', name='shares', value='all') .layui-collapse .layui-colla-item h2.layui-colla-title 基本信息 diff --git a/contact-center/app/src/main/resources/templates/apps/cca/calloutcontact/add.pug b/contact-center/app/src/main/resources/templates/apps/cca/calloutcontact/add.pug index c250265d..39ccad0e 100644 --- a/contact-center/app/src/main/resources/templates/apps/cca/calloutcontact/add.pug +++ b/contact-center/app/src/main/resources/templates/apps/cca/calloutcontact/add.pug @@ -23,7 +23,6 @@ html h1.site-h1(style='background-color:#FFFFFF;') 新建联系人 form.layui-form(action='/agent/calloutcontact/save.html?agentuser=${curagentuser.id!\'\'}', method='post') input(hidden, name='calloutcontact') - input(type='hidden', name='shares', value='all') .layui-collapse .layui-colla-item h2.layui-colla-title 基本信息 diff --git a/contact-center/app/src/main/resources/templates/apps/cca/calloutcontact/edit.pug b/contact-center/app/src/main/resources/templates/apps/cca/calloutcontact/edit.pug index 269b23d8..f7d4f1b3 100644 --- a/contact-center/app/src/main/resources/templates/apps/cca/calloutcontact/edit.pug +++ b/contact-center/app/src/main/resources/templates/apps/cca/calloutcontact/edit.pug @@ -23,7 +23,6 @@ html h1.site-h1(style='background-color:#FFFFFF;') 编辑联系人 form.layui-form(action='/agent/calloutcontact/update.html', method='post') input(type='hidden', name='id', value='${contacts.id!\'\'}') - input(type='hidden', name='shares', value='all') .layui-collapse .layui-colla-item h2.layui-colla-title 基本信息 diff --git a/contact-center/app/src/main/resources/templates/apps/contacts/add.pug b/contact-center/app/src/main/resources/templates/apps/contacts/add.pug index b4357527..44b37bc0 100644 --- a/contact-center/app/src/main/resources/templates/apps/contacts/add.pug +++ b/contact-center/app/src/main/resources/templates/apps/contacts/add.pug @@ -13,7 +13,6 @@ include /mixins/dic.mixin.pug .uk-layui-form form.layui-form(action='/apps/contacts/save.html', method='post') - input(type='hidden', name='shares', value='all') .layui-collapse .layui-colla-item h2.layui-colla-title 基本信息 @@ -79,11 +78,6 @@ include /mixins/dic.mixin.pug label.layui-form-label 电子邮件: .layui-input-inline(style='margin-left:5px;') input.layui-input(type='text', name='email', lay-verify='entemail', autocomplete='off') - .layui-form-item - .layui-inline - label.layui-form-label#contacts_skypeid(style='widht:80px;') Skype ID: - .layui-input-inline - input#skypeid.layui-input(type='text', name='skypeid', lay-verify='skypeid', autocomplete='off') .layui-form-item .layui-inline label.layui-form-label 联系人地址: diff --git a/contact-center/app/src/main/resources/templates/apps/contacts/edit.pug b/contact-center/app/src/main/resources/templates/apps/contacts/edit.pug index 272e9336..e489a671 100644 --- a/contact-center/app/src/main/resources/templates/apps/contacts/edit.pug +++ b/contact-center/app/src/main/resources/templates/apps/contacts/edit.pug @@ -21,7 +21,6 @@ include /mixins/dic.mixin.pug input(type='hidden', name='wlcompany_name', value=contacts.wlcompany_name) input(type='hidden', name='wlsid', value=contacts.wlsid) input(type='hidden', name='wlsystem_name', value=contacts.wlsystem_name) - input(type='hidden', name='shares', value='all') .layui-collapse .layui-colla-item h2.layui-colla-title 基本信息 diff --git a/contact-center/app/src/main/resources/templates/apps/contacts/embed/add.pug b/contact-center/app/src/main/resources/templates/apps/contacts/embed/add.pug index 54acad69..d717f2d3 100644 --- a/contact-center/app/src/main/resources/templates/apps/contacts/embed/add.pug +++ b/contact-center/app/src/main/resources/templates/apps/contacts/embed/add.pug @@ -13,7 +13,6 @@ include /mixins/dic.mixin.pug .uk-layui-form form.layui-form(action='/apps/contacts/embed/save.html', data-toggle='ajax-form', data-close='false', data-target='#mainajaxwin', method='post') input(type='hidden', name='agentserviceid', value=agentserviceid) - input(type='hidden', name='shares', value='all') .layui-collapse .layui-colla-item h2.layui-colla-title 基本信息 diff --git a/contact-center/app/src/main/resources/templates/apps/contacts/embed/edit.pug b/contact-center/app/src/main/resources/templates/apps/contacts/embed/edit.pug index 61c4f932..f05507e1 100644 --- a/contact-center/app/src/main/resources/templates/apps/contacts/embed/edit.pug +++ b/contact-center/app/src/main/resources/templates/apps/contacts/embed/edit.pug @@ -14,7 +14,6 @@ include /mixins/dic.mixin.pug form.layui-form(action='/apps/contacts/embed/update.html', data-toggle="ajax-form" data-close="false" data-target="#mainajaxwin" method="post") input(type='hidden', name='id', value=contacts.id) input(type='hidden', name='agentserviceid', value=agentserviceid) - input(type='hidden', name='shares', value='all') .layui-collapse .layui-colla-item h2.layui-colla-title 基本信息 diff --git a/contact-center/app/src/main/resources/templates/apps/contacts/index.pug b/contact-center/app/src/main/resources/templates/apps/contacts/index.pug index 827a4365..5c74889e 100644 --- a/contact-center/app/src/main/resources/templates/apps/contacts/index.pug +++ b/contact-center/app/src/main/resources/templates/apps/contacts/index.pug @@ -103,14 +103,6 @@ block content td #{contacts.ckind && uKeFuDic[contacts.ckind] ? uKeFuDic[contacts.ckind].name : ""} td #{contacts.user ? contacts.user.username : ""} td - if approachable.contains(contacts.id) - a(href="#", onclick="openDialogWinByContactid('" + contacts.id + "')") - i.layui-icon  - | 聊天 - else - a.disabled(href="#", onclick="unreachableDialogWinByContactid('" + contacts.id + "')") - i.layui-icon  - | 聊天 a(href="/apps/contacts/detail.pug?id=" + (contacts.id ? contacts.id : ""), style="margin-left:10px;") i.layui-icon  | 详情 @@ -186,6 +178,8 @@ block content layer.msg('联系人编辑成功', {icon: 1, time: 1500}) else if (msg && msg == 'edit_contacts_fail') layer.msg('联系人编辑失败,因为存在相同Skype ID', {icon: 2, time: 1500}) + else + handleGeneralCodeInQueryPathOrApiResp(msg); }); }); diff --git a/contact-center/app/src/main/resources/templates/apps/customer/add.pug b/contact-center/app/src/main/resources/templates/apps/customer/add.pug index f74aeea6..432f9172 100644 --- a/contact-center/app/src/main/resources/templates/apps/customer/add.pug +++ b/contact-center/app/src/main/resources/templates/apps/customer/add.pug @@ -14,7 +14,6 @@ include /mixins/dic.mixin.pug .uk-layui-form form.layui-form(action="/apps/customer/save.html", method="post") input(type="hidden", name="entcustomer.shares", value="all") - input(type="hidden", name="contacts.shares", value="all") .layui-collapse .layui-colla-item h2.layui-colla-title 基本信息 diff --git a/contact-center/app/src/main/resources/templates/apps/customer/edit.pug b/contact-center/app/src/main/resources/templates/apps/customer/edit.pug index d9d61792..f5bdf42d 100644 --- a/contact-center/app/src/main/resources/templates/apps/customer/edit.pug +++ b/contact-center/app/src/main/resources/templates/apps/customer/edit.pug @@ -16,7 +16,6 @@ include /mixins/dic.mixin.pug input(type="hidden", name="entcustomer.id", value=account.id) input(type="hidden", name="ekindId", value=ekindId) input(type="hidden", name="entcustomer.shares", value="all") - input(type="hidden", name="contacts.shares", value="all") .layui-collapse .layui-colla-item h2.layui-colla-title 基本信息 diff --git a/contact-center/app/src/main/resources/templates/apps/include/layout.pug b/contact-center/app/src/main/resources/templates/apps/include/layout.pug index a051e3fc..e22e5ec2 100644 --- a/contact-center/app/src/main/resources/templates/apps/include/layout.pug +++ b/contact-center/app/src/main/resources/templates/apps/include/layout.pug @@ -36,6 +36,7 @@ html(xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xm script(src="/js/select/js/i18n/zh-CN.js") script(src="/layui.js") script(src="/js/cskefu.js") + script(src="/js/CSKeFu_Admin.v1.js") script(type="text/javascript" src="/js/kindeditor/kindeditor.js") script(type="text/javascript" src="/js/kindeditor/lang/zh-CN.js") script(type="text/javascript" src="/js/kindeditor-suggest.js") diff --git a/contact-center/app/src/main/resources/templates/apps/index.pug b/contact-center/app/src/main/resources/templates/apps/index.pug index 2cfe8656..31d85103 100644 --- a/contact-center/app/src/main/resources/templates/apps/index.pug +++ b/contact-center/app/src/main/resources/templates/apps/index.pug @@ -38,8 +38,6 @@ html(xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xm script(src="/js/moment-timezone.js") script(src="/js/moment-timezone-with-data.js") script(src="/layui.js") - //- #894 和原生Map行为不一致 - //- script(src="/js/cskefu.js") script(src="/im/js/socket.io.js") script(src="/js/CSKeFu_IM.v1.js") @@ -81,7 +79,7 @@ html(xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xm else if (msg == 't1') layer.msg('当前用户坐席就绪或对话未结束,不能切换为非坐席', {icon: 2, time: 3000}) - if ('#{models.contains("entim")}') { + if (#{models.contains("entim")}) { var imDialogHelper = { open: function () { layinx = layer.open({ @@ -196,7 +194,7 @@ html(xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xm dd a(href="javascript:void(0)" onclick="showSystemBuildInfo()") 关于产品 dd - a(href="https://docs.cskefu.com/" target="_blank") 使用指南 + a(href="https://docs.cskefu.com/docs/licenses" target="_blank") 使用授权 dd a(href="https://github.com/cskefu/cskefu/issues" target="_blank") 反馈建议 dd diff --git a/contact-center/config/sql/002.mysql-create-schemas.sql b/contact-center/config/sql/002.mysql-create-schemas.sql index 350fa3ac..ecce3082 100644 --- a/contact-center/config/sql/002.mysql-create-schemas.sql +++ b/contact-center/config/sql/002.mysql-create-schemas.sql @@ -138,6 +138,24 @@ CREATE TABLE `cs_fb_otn_follow` ( -- Records of cs_fb_otn_follow -- ---------------------------- +-- ---------------------------- +-- Table structure for cs_metakv +-- ---------------------------- +DROP TABLE IF EXISTS `cs_metakv`; +CREATE TABLE `cs_metakv` ( + `metakey` varchar(512) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '元数据字段名,唯一标识', + `metavalue` text COLLATE utf8mb4_unicode_ci COMMENT '元数据值', + `createtime` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updatetime` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `datatype` varchar(256) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'string' COMMENT '数据类型', + `comment` varchar(1024) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '字段备注描述', + PRIMARY KEY (`metakey`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='系统内置元数据'; + +-- ---------------------------- +-- Records of cs_metakv +-- ---------------------------- + -- ---------------------------- -- Table structure for cs_organ_user -- ---------------------------- @@ -2211,7 +2229,6 @@ CREATE TABLE `uk_organ` ( -- Records of uk_organ -- ---------------------------- INSERT INTO `uk_organ` VALUES ('2c9e80867d65eb5c017d65f17ceb0019', '售前坐席A组', null, null, null, null, null, null, '4028a0866f9403f1016f9405a05d000e', '1', ''); -INSERT INTO `uk_organ` VALUES ('40288296874ae16101874ae4f2670016', '机器人平台', null, null, null, null, null, null, '4028a0866f9403f1016f9405a05d000e', '0', ''); INSERT INTO `uk_organ` VALUES ('4028a0866f9403f1016f9405a05d000e', '我的企业', null, null, null, null, 'cskefu', null, '0', '0', ''); -- ---------------------------- diff --git a/contact-center/config/sql/upgrade/001.add_table_cs_metakey.sql b/contact-center/config/sql/upgrade/001.add_table_cs_metakey.sql new file mode 100644 index 00000000..9a99355c --- /dev/null +++ b/contact-center/config/sql/upgrade/001.add_table_cs_metakey.sql @@ -0,0 +1,14 @@ +USE `cosinee`; +-- ----------------- +-- prepare variables +-- ----------------- + +CREATE TABLE IF NOT EXISTS `cs_metakey` ( + `metakey` varchar(512) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '元数据字段名,唯一标识', + `metavalue` text COLLATE utf8mb4_unicode_ci COMMENT '元数据值', + `createtime` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updatetime` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `datatype` varchar(256) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'string' COMMENT '数据类型', + `comment` varchar(1024) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '字段备注描述', + PRIMARY KEY (`metakey`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='系统内置元数据'; diff --git a/contact-center/root/pom.xml b/contact-center/root/pom.xml index 81dc1ff9..08b85210 100644 --- a/contact-center/root/pom.xml +++ b/contact-center/root/pom.xml @@ -400,11 +400,18 @@ compose4j 1.0.0 + com.chatopera.bot sdk 3.5.1 + + + com.chatopera.store + store-sdk + 1.1-SNAPSHOT + diff --git a/docker-compose.yml b/docker-compose.yml index fd2cde82..aab29b9b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -42,6 +42,7 @@ services: - BOT_THRESHOLD_FAQ_BEST_REPLY=${BOT_THRESHOLD_FAQ_BEST_REPLY:-0.9} - BOT_THRESHOLD_FAQ_SUGG_REPLY=${BOT_THRESHOLD_FAQ_SUGG_REPLY:-0.1} - CSKEFU_SETTINGS_WEBIM_VISITOR_SEPARATE=true + - LICENSE_STORE_PROVIDER=${LICENSE_STORE_PROVIDER:-https://store.chatopera.com} - TONGJI_BAIDU_SITEKEY=${TONGJI_BAIDU_SITEKEY:-placeholder} - EXTRAS_LOGIN_BANNER=${NOTICE_LOGIN_BANNER:-off} - EXTRAS_LOGIN_CHATBOX=${EXTRAS_LOGIN_CHATBOX:-off} diff --git a/public/assets/44915582-eb8d2c80-ad65-11e8-8876-86c8b5bb5cc7.png b/public/assets/44915582-eb8d2c80-ad65-11e8-8876-86c8b5bb5cc7.png new file mode 100644 index 00000000..32642168 Binary files /dev/null and b/public/assets/44915582-eb8d2c80-ad65-11e8-8876-86c8b5bb5cc7.png differ diff --git a/public/assets/44915711-432b9800-ad66-11e8-899b-1ea02244925d.png b/public/assets/44915711-432b9800-ad66-11e8-899b-1ea02244925d.png new file mode 100644 index 00000000..3b1323e4 Binary files /dev/null and b/public/assets/44915711-432b9800-ad66-11e8-899b-1ea02244925d.png differ diff --git a/public/assets/51080565-4b82df00-1719-11e9-8cc4-dbbec0459224.png b/public/assets/51080565-4b82df00-1719-11e9-8cc4-dbbec0459224.png new file mode 100644 index 00000000..c8713711 Binary files /dev/null and b/public/assets/51080565-4b82df00-1719-11e9-8cc4-dbbec0459224.png differ diff --git a/public/assets/51080567-50479300-1719-11e9-85d8-d209370c9d10.png b/public/assets/51080567-50479300-1719-11e9-85d8-d209370c9d10.png new file mode 100644 index 00000000..97cf521c Binary files /dev/null and b/public/assets/51080567-50479300-1719-11e9-85d8-d209370c9d10.png differ diff --git a/public/assets/cskefu-screen-1.jpg b/public/assets/cskefu-screen-1.jpg new file mode 100644 index 00000000..46625638 Binary files /dev/null and b/public/assets/cskefu-screen-1.jpg differ diff --git a/public/pr1st.md b/public/pr1st.md new file mode 100644 index 00000000..f7751186 --- /dev/null +++ b/public/pr1st.md @@ -0,0 +1,2 @@ +# 第一个 PR 改动文件,新手任务,添加一行:昵称 @ 日期,e.g. +Hai Liang W. @ 2023-09-11 \ No newline at end of file diff --git a/sample.env b/sample.env index e41e7d5d..fc40b8f4 100644 --- a/sample.env +++ b/sample.env @@ -30,6 +30,7 @@ CACHE_SETUP_STRATEGY=create_by_force BOT_THRESHOLD_FAQ_BEST_REPLY=0.8 BOT_THRESHOLD_FAQ_SUGG_REPLY=0.6 +LICENSE_STORE_PROVIDER=https://store.chatopera.com TONGJI_BAIDU_SITEKEY=placeholder EXTRAS_LOGIN_BANNER="" EXTRAS_LOGIN_CHATBOX=