1
0
mirror of https://github.com/chatopera/cosin.git synced 2025-06-16 18:30:03 +08:00

Merge branch 'develop' into feature/964

This commit is contained in:
lecjy 2023-11-13 19:42:56 +08:00 committed by GitHub
commit acca35dd51
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
97 changed files with 3177 additions and 961 deletions

View File

@ -0,0 +1,30 @@
name: 求助
description: 开发环境搭建、功能咨询和使用问题等
labels: ["help-wanted"]
assignees:
- zhangchanglong2021
body:
- type: markdown
attributes:
value: "## 描述"
- type: textarea
id: content1
attributes:
label: "详细描述问题后优先处理解决! 截图、错误日志等"
value: |
1. 针对某功能,需要提供详细描述文档
2. xxx
3. xxx
...
validations:
required: true
- type: textarea
id: content2
attributes:
label: "代码版本"
value: |
1. Git commit hash (`git rev-parse HEAD`),进入代码库并执行
2. v7, v8, v9 ...
validations:
required: true

View File

@ -0,0 +1,30 @@
name: 软件缺陷
description: 报告软件缺陷
labels: ["bug"]
assignees:
- kaifuny
body:
- type: markdown
attributes:
value: "## 描述"
- type: textarea
id: content1
attributes:
label: "详细描述问题后优先处理解决! 截图、错误日志等"
value: |
1. 如何重现
2. xxx
3. xxx
...
validations:
required: true
- type: textarea
id: content2
attributes:
label: "代码版本"
value: |
1. Git commit hash (`git rev-parse HEAD`),进入代码库并执行
2. v7, v8, v9 ...
validations:
required: true

View File

@ -0,0 +1,30 @@
name: 需求
description: 增加新需求、反馈建议
labels: ["requirement"]
assignees:
- kaifuny
body:
- type: markdown
attributes:
value: "## 描述"
- type: textarea
id: content1
attributes:
label: "详细描述需求"
value: |
1. xxx 模块需要支持 xxx 功能
2. xxx
3. xxx
...
validations:
required: true
- type: textarea
id: content2
attributes:
label: "代码版本"
value: |
1. Git commit hash (`git rev-parse HEAD`),进入代码库并执行
2. v7, v8, v9 ...
validations:
required: true

View File

@ -0,0 +1,30 @@
name: 性能优化
description: 瓶颈分析、性能优化建议和安全漏洞等
labels: ["profiling"]
assignees:
- lecjy
body:
- type: markdown
attributes:
value: "## 描述"
- type: textarea
id: content1
attributes:
label: "详细描述需求"
value: |
1. xxx 模块需要支持 xxx 功能
2. xxx
3. xxx
...
validations:
required: true
- type: textarea
id: content2
attributes:
label: "代码版本"
value: |
1. Git commit hash (`git rev-parse HEAD`),进入代码库并执行
2. v7, v8, v9 ...
validations:
required: true

View File

@ -0,0 +1,35 @@
name: 用户故事
description: 用户故事
title: "[us] "
labels: ["userstory"]
assignees:
- kaifuny
body:
- type: markdown
attributes:
value: "## 描述"
- type: textarea
id: content1
attributes:
label: "详细描述需求"
value: |
## 用户主体
## 用户故事
## 交互流程
## 交互细节
...
validations:
required: true
- type: textarea
id: content2
attributes:
label: "代码版本"
value: |
1. Git commit hash (`git rev-parse HEAD`),进入代码库并执行
2. v7, v8, v9 ...
validations:
required: true

View File

@ -0,0 +1,11 @@
blank_issues_enabled: false
contact_links:
- name: 文档中心
url: https://docs.cskefu.com/
about: 提供春松客服使用指南、教程、基本功能使用、介绍和常见问题解答
- name: 春松客服大讲堂
url: https://gitee.com/cskefu/cskefu-djt/blob/main/README.md
about: 提供春松客服定制化开发技能课程
- name: 商务洽谈
url: https://www.chatopera.com/price.html
about: 提供春松客服定制化开发、机器人客服平台等

View File

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

View File

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

View File

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

View File

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

View File

@ -1,19 +0,0 @@
name: gitee
on:
- push
- delete
jobs:
sync:
runs-on: ubuntu-latest
name: Git Repo Sync
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- uses: wangchucheng/git-repo-sync@v0.1.0
with:
target-url: https://gitee.com/cskefu/cskefu.git
target-username: ${{ secrets.USERNAME }}
target-token: ${{ secrets.ACCESS_TOKEN }}

View File

@ -1,10 +1,8 @@
# Contributing
成为春松客服贡献者
## 提交反馈
在[春松客服 GitHub Issues](https://github.com/cskefu/cskefu/issues)中,先搜索是否有重复的。然后进行补充或新建 Issue。
在[春松客服 Issues](https://gitee.com/cskefu/cskefu/issues)中,先搜索是否有重复的。然后进行补充或新建 Issue。
## 提交代码
@ -15,25 +13,14 @@
春松客服文档中心的项目,也是开源的,地址在:
[https://github.com/cskefu/cskefu-docs](https://github.com/cskefu/cskefu-docs)
[https://gitee.com/cskefu/docs](https://gitee.com/cskefu/docs)
春松客服文档 Markdown 文件路径:
[https://github.com/cskefu/cskefu-docs/tree/main/docs](https://github.com/cskefu/cskefu-docs/tree/main/docs)
[https://gitee.com/cskefu/docs/tree/main/docs](https://gitee.com/cskefu/docs/tree/main/docs)
更新文档:
1提交 PR 到[春松客服文档中心 GitHub 仓库](https://github.com/cskefu/cskefu-docs/tree/main/docs),一步到位。
1提交 PR 到[春松客服文档中心 Git 仓库](https://gitee.com/cskefu/docs/tree/main),一步到位。
2提交 Issue 到[春松客服 Issues](https://github.com/cskefu/cskefu/issues/new) 并撰写文档内容,使用该方案则需要后续其他协作者提交到 [春松客服文档中心 GitHub 仓库](https://github.com/cskefu/cskefu-docs) 中。
## 期待您成为春松客服贡献者
不管是何种贡献,只有大小之分,而无本质区别。
一起贡献,一起好!
没有贡献,都不好!
团结是共赢,分裂是全输。
![image](./public/assets/screenshot-20220323-163051.jpg)
2提交 Issue 到[春松客服 Issues](https://gitee.com/cskefu/cskefu/issues/new) 并撰写文档内容,使用该方案则需要后续其他协作者提交到 [春松客服文档中心 Git 仓库](https://gitee.com/cskefu/docs) 中。

View File

@ -31,18 +31,6 @@
新版本介绍:[观看春松客服 v8 新版本发布会 @ 2023-07-01](https://www.cskefu.com/2023/07/03/community-conf/)
## 媒体报道
<img src="./public/assets/cskefu-gpv-2.png" height = "220" div align=right />
- [春松客服:通过开源加云原生模式,大规模交付智能客服系统](https://www.cskefu.com/2022/04/11/cskefu-opensource-plus-cloud-model/)
- [春松客服荣获 GVP 企业级开源项目认证](http://www.ctiforum.com/news/guonei/578988.html)
- [Chatopera 王海良:做好开源客服系统 | OpenTEKr 专访](https://www.bilibili.com/video/BV1qF411p7hW)
---
## 开发者列表 ✨
:evergreen_tree: 春松客服是开源的智能客服系统,于 2018 年 9 月由 [Chatopera](https://www.chatopera.com) 发布,在开源社区协作中优化和完善,春松客服属于[春松客服开源社区](https://github.com/cskefu/cskefu#%E6%98%A5%E6%9D%BE%E5%AE%A2%E6%9C%8D%E5%BC%80%E6%BA%90%E7%A4%BE%E5%8C%BA)。
@ -134,27 +122,35 @@
<p align="center">
<b>欢迎页</b><br>
<img src="https://static-public.chatopera.com/assets/images/cskefu/cskefu-screen-1.jpg" width="900">
<img src="./public/assets/cskefu-screen-1.jpg" width="900">
</p>
### 坐席工作台
<details>
<summary>展开查看更多产品截图</summary>
<p>
登录演示环境,查看更多产品能力:[https://demo.cskefu.com/](https://demo.cskefu.com/)
<p align="center">
<b>坐席工作台</b><br>
<img src="./public/assets/44915582-eb8d2c80-ad65-11e8-8876-86c8b5bb5cc7.png" width="900">
</p>
| **登录账号** | **密码** | **角色** |
| ------------ | --------- | -------------- |
| admin | admin1234 | 系统超级管理员 |
| zhangsan | agent1234 | 客服坐席人员 |
<p align="center">
<b>坐席监控</b><br>
<img src="./public/assets/44915711-432b9800-ad66-11e8-899b-1ea02244925d.png" width="900">
</p>
### 网页端访客示例
<p align="center">
<b>集成客服机器人</b><br>
<img src="./public/assets/51080565-4b82df00-1719-11e9-8cc4-dbbec0459224.png" width="900">
</p>
[https://demo.cskefu.com/testclient.html](http://demo.cskefu.com/testclient.html)
<p align="center">
<b>客服机器人应答</b><br>
<img src="./public/assets/51080567-50479300-1719-11e9-85d8-d209370c9d10.png" width="900">
</p>
- 登录张三后可接待访客,否则显示没有客服人员在线
### 机器人客服示例
[https://oh-my.cskefu.com/im/text/0nhckh.html](https://oh-my.cskefu.com/im/text/0nhckh.html)
</p>
</details>
## 快速开始
@ -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)

View File

@ -88,6 +88,7 @@
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>3.1.3</version>
<executions>
<execution>
<goals>

View File

@ -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<Organ> list = organRepository.findAll();
if (CollectionUtils.isEmpty(list)) {
return new SessionConfig();
} else {
Map<String, String> map = list.stream().collect(Collectors.toMap(item -> item.getId(), item -> item.getParent()));
List<SessionConfig> configList = sessionConfigRes.findAll();
if (CollectionUtils.isEmpty(configList)) {
return new SessionConfig();
} else {
Map<String, SessionConfig> skillMap = configList.stream().collect(Collectors.toMap(item -> item.getSkill(), item -> item));
if (map.get(organid) == null || skillMap.get(map.get(organid)) == null) {
return new SessionConfig();
}
return skillMap.get(map.get(organid));
}
}
} else {
cache.putSessionConfig(sessionConfig, organid);
}
}
return sessionConfig;
}

View File

@ -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<ACDComposeContext> {
@Autowired
private ACDMessageHelper acdMessageHelper;
@Autowired
private LicenseProxy licenseProxy;
/**
* 设置AgentUser基本信息
*
@ -87,6 +92,16 @@ public class ACDVisBodyParserMw implements Middleware<ACDComposeContext> {
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;
});

View File

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

View File

@ -0,0 +1,51 @@
/*
* Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd.
* <https://www.chatopera.com>, Licensed under the Chunsong Public
* License, Version 1.0 (the "License"), https://docs.cskefu.com/licenses/v1.html
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.cskefu.cc.aspect;
import com.cskefu.cc.basic.MainContext;
import com.cskefu.cc.exception.BillingQuotaException;
import com.cskefu.cc.exception.BillingResourceException;
import com.cskefu.cc.model.Channel;
import com.cskefu.cc.model.User;
import com.cskefu.cc.proxy.LicenseProxy;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class ChannelAspect {
private final static Logger logger = LoggerFactory.getLogger(ChannelAspect.class);
@Autowired
private LicenseProxy licenseProxy;
@Before("execution(* com.cskefu.cc.persistence.repository.ChannelRepository.save(..))")
public void beforeSave(final JoinPoint joinPoint) throws BillingResourceException, BillingQuotaException {
final Channel channel = (Channel) joinPoint.getArgs()[0];
logger.info("[beforeSave] before channel id {}, type {}", channel.getId(), channel.getType());
if (StringUtils.isBlank(channel.getId())) {
// create new Channel
if (StringUtils.equals(channel.getType(), MainContext.ChannelType.WEBIM.toString())) {
// create new WEBIM channel
licenseProxy.writeDownResourceUsageInStore(MainContext.BillingResource.CHANNELWEBIM, 1);
}
} else {
// update existed Channel
}
}
}

View File

@ -0,0 +1,50 @@
/*
* Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd.
* <https://www.chatopera.com>, Licensed under the Chunsong Public
* License, Version 1.0 (the "License"), https://docs.cskefu.com/licenses/v1.html
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.cskefu.cc.aspect;
import com.cskefu.cc.basic.MainContext;
import com.cskefu.cc.basic.MainUtils;
import com.cskefu.cc.exception.BillingQuotaException;
import com.cskefu.cc.exception.BillingResourceException;
import com.cskefu.cc.model.Contacts;
import com.cskefu.cc.model.User;
import com.cskefu.cc.proxy.LicenseProxy;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class ContactsAspect {
private final static Logger logger = LoggerFactory.getLogger(ContactsAspect.class);
@Autowired
private LicenseProxy licenseProxy;
@Before("execution(* com.cskefu.cc.persistence.repository.ContactsRepository.save(..))")
public void beforeSave(final JoinPoint joinPoint) throws BillingResourceException, BillingQuotaException {
final Contacts contacts = (Contacts) joinPoint.getArgs()[0];
logger.info("[save] before contacts id {}", contacts.getId());
if (StringUtils.isBlank(contacts.getId())) {
// 执行配额扣除
licenseProxy.writeDownResourceUsageInStore(MainContext.BillingResource.CONTACT, 1);
contacts.setId(MainUtils.getUUID());
} else {
// update existed user
}
}
}

View File

@ -0,0 +1,47 @@
/*
* Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd.
* <https://www.chatopera.com>, Licensed under the Chunsong Public
* License, Version 1.0 (the "License"), https://docs.cskefu.com/licenses/v1.html
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.cskefu.cc.aspect;
import com.cskefu.cc.basic.MainContext;
import com.cskefu.cc.exception.BillingQuotaException;
import com.cskefu.cc.exception.BillingResourceException;
import com.cskefu.cc.model.Channel;
import com.cskefu.cc.model.Organ;
import com.cskefu.cc.proxy.LicenseProxy;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class OrganAspect {
private final static Logger logger = LoggerFactory.getLogger(OrganAspect.class);
@Autowired
private LicenseProxy licenseProxy;
@Before("execution(* com.cskefu.cc.persistence.repository.OrganRepository.save(..))")
public void beforeSave(final JoinPoint joinPoint) throws BillingResourceException, BillingQuotaException {
final Organ organ = (Organ) joinPoint.getArgs()[0];
logger.info("[beforeSave] before organ id {}", organ.getId());
if (StringUtils.isBlank(organ.getId())) {
// create new organ
licenseProxy.writeDownResourceUsageInStore(MainContext.BillingResource.ORGAN, 1);
} else {
// update existed Channel
}
}
}

View File

@ -0,0 +1,47 @@
/*
* Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd.
* <https://www.chatopera.com>, Licensed under the Chunsong Public
* License, Version 1.0 (the "License"), https://docs.cskefu.com/licenses/v1.html
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.cskefu.cc.aspect;
import com.cskefu.cc.basic.MainContext;
import com.cskefu.cc.exception.BillingQuotaException;
import com.cskefu.cc.exception.BillingResourceException;
import com.cskefu.cc.model.User;
import com.cskefu.cc.proxy.LicenseProxy;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class UserAspect {
private final static Logger logger = LoggerFactory.getLogger(UserAspect.class);
@Autowired
private LicenseProxy licenseProxy;
@Before("execution(* com.cskefu.cc.persistence.repository.UserRepository.save(..))")
public void beforeSave(final JoinPoint joinPoint) throws BillingResourceException, BillingQuotaException {
final User user = (User) joinPoint.getArgs()[0];
logger.info("[save] before user id {}", user.getId());
if (StringUtils.isBlank(user.getId())) {
// 执行配额扣除
licenseProxy.writeDownResourceUsageInStore(MainContext.BillingResource.USER, 1);
} else {
// update existed user
}
}
}

View File

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

View File

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

View File

@ -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<ContextRe
private static final Logger logger = LoggerFactory.getLogger(AppCtxRefreshEventListener.class);
private void setupSysdicCacheAndExtras(final ContextRefreshedEvent event, final String cacheSetupStrategy, final Cache cache, final SysDicRepository sysDicRes, final BlackListRepository blackListRes) {
if (!StringUtils.equalsIgnoreCase(cacheSetupStrategy, Constants.cache_setup_strategy_skip)) {
@ -138,6 +138,9 @@ public class AppCtxRefreshEventListener implements ApplicationListener<ContextRe
logger.info("[Plugins] registered plugin id {}, class {}", p.getPluginId(), p.getClass().getName());
}
// 初始化 ServerInstId
LicenseProxy licenseProxy = event.getApplicationContext().getBean(LicenseProxy.class);
licenseProxy.checkOnStartup();
} else {
logger.info("[onApplicationEvent] bypass, initialization has been done already.");
}

View File

@ -0,0 +1,46 @@
/*
* Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd.
* <https://www.chatopera.com>, Licensed under the Chunsong Public
* License, Version 1.0 (the "License"), https://docs.cskefu.com/licenses/v1.html
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* Copyright (C) 2019-2022 Chatopera Inc, <https://www.chatopera.com>,
* Licensed under the Apache License, Version 2.0,
* http://www.apache.org/licenses/LICENSE-2.0
*/
package com.cskefu.cc.config;
import com.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;
}
}

View File

@ -0,0 +1,231 @@
/*
* Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd.
* <https://www.chatopera.com>, Licensed under the Chunsong Public
* License, Version 1.0 (the "License"), https://docs.cskefu.com/licenses/v1.html
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.cskefu.cc.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<JSONObject> 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<JSONObject> 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<JSONObject> 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"));
}
}
}

View File

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

View File

@ -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,13 +99,22 @@ 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())) {
try {
channel.setSnsid(Base62.encode(channel.getBaseURL()).toLowerCase());
int count = snsAccountRes.countBySnsid(channel.getSnsid());
if (count == 0) {
@ -129,6 +144,16 @@ public class ChannelController extends Handler {
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);
}
}
}
return request(super.createView("redirect:/admin/im/index.html?status=" + status));
}

View File

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

View File

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

View File

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

View File

@ -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<AgentUser> agentUserList = agentUserRepository.findByUserid(userid);
List<AgentUser> 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);

View File

@ -16,6 +16,7 @@ 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> contacts = contactsRes.findByCreaterAndSharesAndDatastatus(logined.getId(),
logined.getId(),
Page<Contacts> 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> contacts = contactsRes.findByCreaterAndSharesAndDatastatus(logined.getId(),
logined.getId(),
Page<Contacts> 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> contacts = contactsRes.findByCreaterAndSharesAndDatastatus(logined.getId(),
logined.getId(),
Page<Contacts> 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<Contacts> contactsList = contactsRes.findByCreaterAndSharesAndDatastatus(
logined.getId(), logined.getId(), false, PageRequest.of(super.getP(request), super.getPs(request)));
Iterable<Contacts> 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<Map<String, Object>> values = new ArrayList<>();
@ -512,8 +518,8 @@ public class ContactsController extends Handler {
map.put("ckind", ckind);
}
Iterable<Contacts> contactsList = contactsRes.findByCreaterAndSharesAndDatastatus(
logined.getId(), logined.getId(), false, PageRequest.of(super.getP(request), super.getPs(request)));
Iterable<Contacts> 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<Map<String, Object>> 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<Contacts> contactsList = contactsRes.findByCreaterAndSharesAndDatastatus(
logined.getId(), logined.getId(), false,
Page<Contacts> contactsList = contactsRes.findByCreaterAndSharesInAndDatastatus(
logined.getId(), Arrays.asList(logined.getId(),"all"), false,
PageRequest.of(super.getP(request), super.getPs(request)));
map.addAttribute("contactsList", contactsList);

View File

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

View File

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

View File

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

View File

@ -130,9 +130,10 @@ public class OnlineUserController extends Handler {
}
agentUserContactsRes.findOneByUserid(
userid).ifPresent(p -> {
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) {

View File

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

View File

@ -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<Contacts> contactsList = contactsRes.findByCreaterAndSharesAndDatastatus(super.getUser(request).getId(), super.getUser(request).getId(), false, PageRequest.of(0, 10));
Page<Contacts> 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()) {

View File

@ -0,0 +1,44 @@
/*
* Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd.
* <https://www.chatopera.com>, Licensed under the Chunsong Public
* License, Version 1.0 (the "License"), https://docs.cskefu.com/licenses/v1.html
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* Copyright (C) 2019-Jun. 2023 Chatopera Inc, <https://www.chatopera.com>,
* Licensed under the Apache License, Version 2.0,
* http://www.apache.org/licenses/LICENSE-2.0
*/
package com.cskefu.cc.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);
}
}

View File

@ -0,0 +1,20 @@
/*
* Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd.
* <https://www.chatopera.com>, Licensed under the Chunsong Public
* License, Version 1.0 (the "License"), https://docs.cskefu.com/licenses/v1.html
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* Copyright (C) 2019-Jun. 2023 Chatopera Inc, <https://www.chatopera.com>,
* Licensed under the Apache License, Version 2.0,
* http://www.apache.org/licenses/LICENSE-2.0
*/
package com.cskefu.cc.exception;
public class BillingResourceException extends Exception{
public BillingResourceException(final String s){
super(s);
}
}

View File

@ -0,0 +1,20 @@
/*
* Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd.
* <https://www.chatopera.com>, Licensed under the Chunsong Public
* License, Version 1.0 (the "License"), https://docs.cskefu.com/licenses/v1.html
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* Copyright (C) 2019-Jun. 2023 Chatopera Inc, <https://www.chatopera.com>,
* Licensed under the Apache License, Version 2.0,
* http://www.apache.org/licenses/LICENSE-2.0
*/
package com.cskefu.cc.exception;
public class LicenseNotFoundException extends Exception{
public LicenseNotFoundException(final String s){
super(s);
}
}

View File

@ -0,0 +1,20 @@
/*
* Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd.
* <https://www.chatopera.com>, Licensed under the Chunsong Public
* License, Version 1.0 (the "License"), https://docs.cskefu.com/licenses/v1.html
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* Copyright (C) 2019-Jun. 2023 Chatopera Inc, <https://www.chatopera.com>,
* Licensed under the Apache License, Version 2.0,
* http://www.apache.org/licenses/LICENSE-2.0
*/
package com.cskefu.cc.exception;
public class MetaKvInvalidKeyException extends Exception{
public MetaKvInvalidKeyException(final String s){
super(s);
}
}

View File

@ -0,0 +1,20 @@
/*
* Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd.
* <https://www.chatopera.com>, Licensed under the Chunsong Public
* License, Version 1.0 (the "License"), https://docs.cskefu.com/licenses/v1.html
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* Copyright (C) 2019-Jun. 2023 Chatopera Inc, <https://www.chatopera.com>,
* Licensed under the Apache License, Version 2.0,
* http://www.apache.org/licenses/LICENSE-2.0
*/
package com.cskefu.cc.exception;
public class MetaKvNotExistException extends Exception{
public MetaKvNotExistException(final String s){
super(s);
}
}

View File

@ -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<AgentUser> {
@Transient
private boolean tip = false;
@Transient
private boolean agentTip = false;
@ -119,11 +121,25 @@ public class AgentUser implements Serializable, Comparable<AgentUser> {
@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<AgentUser> {
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;
}
}

View File

@ -0,0 +1,91 @@
/*
* Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd.
* <https://www.chatopera.com>, Licensed under the Chunsong Public
* License, Version 1.0 (the "License"), https://docs.cskefu.com/licenses/v1.html
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.cskefu.cc.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;
}
}

View File

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

View File

@ -69,26 +69,26 @@ public interface AgentUserRepository extends JpaRepository<AgentUser, String> {
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<AgentUser> 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<AgentUser> 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")

View File

@ -40,7 +40,7 @@ public interface ContactsRepository extends JpaRepository<Contacts, String> {
@Query(nativeQuery = true, value = "SELECT * FROM uk_contacts WHERE id = ?1")
Optional<Contacts> findOneById(final String id);
Page<Contacts> findByCreaterAndSharesAndDatastatus(String id, String shares, boolean datastatus, Pageable pageRequest);
Page<Contacts> findByCreaterAndSharesInAndDatastatus(String id, Collection<String> shares, boolean datastatus, Pageable pageRequest);
/**
* 根据条件返回联系人符合一下条件之一

View File

@ -0,0 +1,22 @@
/*
* Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd.
* <https://www.chatopera.com>, Licensed under the Chunsong Public
* License, Version 1.0 (the "License"), https://docs.cskefu.com/licenses/v1.html
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.cskefu.cc.persistence.repository;
import com.cskefu.cc.model.MetaKv;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
public interface MetaKvRepository extends JpaRepository<MetaKv, String> {
Optional<MetaKv> findFirstByMetakey(final String p1);
}

View File

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

View File

@ -0,0 +1,565 @@
/*
* Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd.
* <https://www.chatopera.com>, Licensed under the Chunsong Public
* License, Version 1.0 (the "License"), https://docs.cskefu.com/licenses/v1.html
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.cskefu.cc.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<MainContext.BillingResource, Integer> 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<MetaKv> 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<MetaKv> 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<MetaKv> 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<MetaKv> 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<JSONObject> getLicensesInStore() throws InvalidResponseException {
List<JSONObject> result = new ArrayList<>();
try {
JSONArray ja = new JSONArray((retrieveMetaKv(Constants.LICENSEIDS).getMetavalue()));
HashMap<String, String> addDates = new HashMap<>();
List<String> 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<String> 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<String, LICSTATUS> statuses = quotaWdClient.getLicenseStatus(licenseShortId);
if (statuses.size() == 1) {
for (final Map.Entry<String, LICSTATUS> 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;
}
}

View File

@ -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,10 +82,11 @@ 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()));
}
@ -94,7 +98,18 @@ public class UserProxy {
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);

View File

@ -15,8 +15,13 @@
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;
@ -60,4 +65,35 @@ public class Base62 {
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;
}
}

View File

@ -16,10 +16,17 @@ package com.cskefu.cc.util;
import org.apache.commons.beanutils.converters.DateTimeConverter;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
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() {
}
@ -51,4 +58,17 @@ public class DateConverter extends DateTimeConverter {
}
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);
}
}

View File

@ -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 final static String NA = "N/A";
public String formatDate(String pattern, Date value) {
try {
if (value == null) {
return "";
return NA;
}
SimpleDateFormat format = new SimpleDateFormat(pattern);
return format.format(value);
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<String> 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 "";
}
}
}

View File

@ -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
@ -172,13 +179,3 @@ 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

View File

@ -11,6 +11,11 @@
* Licensed under the Apache License, Version 2.0
* http://www.apache.org/licenses/LICENSE-2.0
*/
/**
* 处理系统用户的创建的返回值
* @param responsecode
* @param cb
*/
function processUserAddOrUpdateResult(responsecode, cb) {
switch (responsecode) {
case 'username_exist':
@ -56,5 +61,95 @@ function processUserAddOrUpdateResult(responsecode, cb){
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");
}
}
}

View File

@ -18,7 +18,10 @@ 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 = io(protocol + '://' + hostname + ':' + port + "/im/agent?userid=" + userid + "&session=" + session + "&admin=" + adminuser, {
transports: ['websocket'],
upgrade: false
});
socket.on('connect', function () {
console.log("[IM] 连接初始化成功");
//请求服务端记录 当前用户在线事件
@ -30,8 +33,8 @@ $(document).ready(function(){
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();
@ -49,11 +52,15 @@ $(document).ready(function(){
"</div>");
}
}
if($('#multiMediaDialogWin').length > 0 && multiMediaDialogWin != null && multiMediaDialogWin.$ &&multiMediaDialogWin.$('#agentusers').length > 0){
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();
$('#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) {

View File

@ -343,6 +343,7 @@ 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) {
@ -356,9 +357,9 @@ var Proxy = {
} 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) {
@ -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);
});

View File

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

View File

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

View File

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

View File

@ -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') 接口管理
| &ndash;&gt;
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'));

View File

@ -0,0 +1,31 @@
//- Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd.
//- <https://www.chatopera.com>, Licensed under the Chunsong Public
//- License, Version 1.0 (the "License"), https://docs.cskefu.com/licenses/v1.html
//- Unless required by applicable law or agreed to in writing, software
//- distributed under the License is distributed on an "AS IS" BASIS,
//- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//- See the License for the specific language governing permissions and
//- limitations under the License.
.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();
});

View File

@ -0,0 +1,100 @@
//- Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd.
//- <https://www.chatopera.com>, Licensed under the Chunsong Public
//- License, Version 1.0 (the "License"), https://docs.cskefu.com/licenses/v1.html
//- Unless required by applicable law or agreed to in writing, software
//- distributed under the License is distributed on an "AS IS" BASIS,
//- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//- See the License for the specific language governing permissions and
//- limitations under the License.
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 &#xe654;
| 导入
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") &#xe60b;
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 &#xe642;
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;") &#x1006;
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;
});

View File

@ -0,0 +1,59 @@
//- Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd.
//- <https://www.chatopera.com>, Licensed under the Chunsong Public
//- License, Version 1.0 (the "License"), https://docs.cskefu.com/licenses/v1.html
//- Unless required by applicable law or agreed to in writing, software
//- distributed under the License is distributed on an "AS IS" BASIS,
//- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//- See the License for the specific language governing permissions and
//- limitations under the License.
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") &#xe60b;
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 &nbsp;&nbsp;
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 &nbsp;&nbsp;
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");
}

View File

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

View File

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

View File

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

View File

@ -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 基本信息

View File

@ -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 基本信息

View File

@ -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 基本信息

View File

@ -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 基本信息

View File

@ -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 联系人地址:

View File

@ -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 基本信息

View File

@ -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 基本信息

View File

@ -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 基本信息

View File

@ -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 &#xe606;
| 聊天
else
a.disabled(href="#", onclick="unreachableDialogWinByContactid('" + contacts.id + "')")
i.layui-icon &#xe60f;
| 聊天
a(href="/apps/contacts/detail.pug?id=" + (contacts.id ? contacts.id : ""), style="margin-left:10px;")
i.layui-icon &#xe60a;
| 详情
@ -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);
});
});

View File

@ -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 基本信息

View File

@ -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 基本信息

View File

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

View File

@ -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/utils.js"></script>
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

View File

@ -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', '');
-- ----------------------------

View File

@ -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='系统内置元数据';

View File

@ -400,12 +400,19 @@
<artifactId>compose4j</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>com.chatopera.bot</groupId>
<artifactId>sdk</artifactId>
<version>3.5.1</version>
</dependency>
<dependency>
<groupId>com.chatopera.store</groupId>
<artifactId>store-sdk</artifactId>
<version>1.1-SNAPSHOT</version>
</dependency>
<!-- Required for Java 11 https://github.com/cskefu/cskefu/issues/714 -->
<dependency>
<groupId>javax.xml.bind</groupId>

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 320 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 192 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 315 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 840 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

2
public/pr1st.md Normal file
View File

@ -0,0 +1,2 @@
# 第一个 PR 改动文件,新手任务,添加一行:昵称 @ 日期e.g.
Hai Liang W. @ 2023-09-11

View File

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