diff --git a/contact-center/app/pom.xml b/contact-center/app/pom.xml index 01da5fe9..df97d49d 100644 --- a/contact-center/app/pom.xml +++ b/contact-center/app/pom.xml @@ -88,6 +88,7 @@ org.springframework.boot spring-boot-maven-plugin + 3.1.3 diff --git a/contact-center/app/src/main/java/com/cskefu/cc/acd/middleware/visitor/ACDVisBodyParserMw.java b/contact-center/app/src/main/java/com/cskefu/cc/acd/middleware/visitor/ACDVisBodyParserMw.java index 7da261a2..9aae742a 100644 --- a/contact-center/app/src/main/java/com/cskefu/cc/acd/middleware/visitor/ACDVisBodyParserMw.java +++ b/contact-center/app/src/main/java/com/cskefu/cc/acd/middleware/visitor/ACDVisBodyParserMw.java @@ -1,14 +1,14 @@ -/* - * Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd. - * , Licensed under the Chunsong Public +/* + * Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd. + * , Licensed under the Chunsong Public * License, Version 1.0 (the "License"), https://docs.cskefu.com/licenses/v1.html * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. - * Copyright (C) 2019-2022 Chatopera Inc, , - * Licensed under the Apache License, Version 2.0, + * Copyright (C) 2019-2022 Chatopera Inc, , + * Licensed under the Apache License, Version 2.0, * http://www.apache.org/licenses/LICENSE-2.0 */ @@ -22,12 +22,14 @@ import com.cskefu.cc.cache.Cache; import com.cskefu.cc.model.AgentUser; import com.cskefu.cc.model.AgentUserContacts; import com.cskefu.cc.model.Contacts; +import com.cskefu.cc.model.ExecuteResult; import com.cskefu.cc.persistence.repository.ContactsRepository; import com.cskefu.cc.persistence.repository.AgentUserContactsRepository; import com.cskefu.cc.proxy.AgentStatusProxy; import com.cskefu.cc.proxy.AgentUserProxy; import com.chatopera.compose4j.Functional; import com.chatopera.compose4j.Middleware; +import com.cskefu.cc.proxy.LicenseProxy; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -62,6 +64,9 @@ public class ACDVisBodyParserMw implements Middleware { @Autowired private ACDMessageHelper acdMessageHelper; + @Autowired + private LicenseProxy licenseProxy; + /** * 设置AgentUser基本信息 * @@ -87,6 +92,16 @@ public class ACDVisBodyParserMw implements Middleware { ctx.getOnlineUserId(), ctx.getOnlineUserNickname(), ctx.getAppid()); + + // 执行计费逻辑 + ExecuteResult writeDownResult = licenseProxy.writeDownAgentUserUsageInStore(p); + + if (writeDownResult.getRc() != ExecuteResult.RC_SUCC) { + // 配额操作失败,提示座席 + p.setLicenseVerifiedPass(false); + p.setLicenseBillingMsg(writeDownResult.getMsg()); + } + logger.info("[apply] create new agent user id {}", p.getId()); return p; }); diff --git a/contact-center/app/src/main/java/com/cskefu/cc/aspect/AgentUserAspect.java b/contact-center/app/src/main/java/com/cskefu/cc/aspect/AgentUserAspect.java index e342ff09..8b5f57e5 100644 --- a/contact-center/app/src/main/java/com/cskefu/cc/aspect/AgentUserAspect.java +++ b/contact-center/app/src/main/java/com/cskefu/cc/aspect/AgentUserAspect.java @@ -1,14 +1,14 @@ -/* - * Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd. - * , Licensed under the Chunsong Public +/* + * Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd. + * , Licensed under the Chunsong Public * License, Version 1.0 (the "License"), https://docs.cskefu.com/licenses/v1.html * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. - * Copyright (C) 2019-2022 Chatopera Inc, , - * Licensed under the Apache License, Version 2.0, + * Copyright (C) 2019-2022 Chatopera Inc, , + * Licensed under the Apache License, Version 2.0, * http://www.apache.org/licenses/LICENSE-2.0 */ @@ -18,14 +18,17 @@ import com.cskefu.cc.basic.MainContext; import com.cskefu.cc.cache.Cache; import com.cskefu.cc.cache.RedisCommand; import com.cskefu.cc.cache.RedisKey; +import com.cskefu.cc.exception.BillingResourceException; import com.cskefu.cc.model.AgentUser; import com.cskefu.cc.proxy.AgentAuditProxy; +import com.cskefu.cc.proxy.LicenseProxy; import org.apache.commons.lang3.StringUtils; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.After; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Before; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -51,6 +54,31 @@ public class AgentUserAspect { @Autowired private AgentAuditProxy agentAuditProxy; + @Autowired + private LicenseProxy licenseProxy; + + @Before("execution(* com.cskefu.cc.persistence.repository.AgentUserRepository.save(..))") + public void beforeSave(final JoinPoint joinPoint) { + final AgentUser agentUser = (AgentUser) joinPoint.getArgs()[0]; + + if (StringUtils.isBlank(agentUser.getId())) { + logger.info("[beforeSave] agentUser id is blank"); + + if (StringUtils.isNotBlank(agentUser.getOpttype()) && StringUtils.equals(MainContext.OptType.CHATBOT.toString(), agentUser.getOpttype())) { + // 机器人座席支持的对话,跳过计数 + agentUser.setLicenseVerifiedPass(true); + return; + } + + // 计数加一 + try { + licenseProxy.increResourceUsageInMetaKv(MainContext.BillingResource.AGENGUSER, 1); + } catch (BillingResourceException e) { + logger.error("[beforeSave] error", e.toString()); + } + } + } + @After("execution(* com.cskefu.cc.persistence.repository.AgentUserRepository.save(..))") public void save(final JoinPoint joinPoint) { final AgentUser agentUser = (AgentUser) joinPoint.getArgs()[0]; diff --git a/contact-center/app/src/main/java/com/cskefu/cc/aspect/ChannelAspect.java b/contact-center/app/src/main/java/com/cskefu/cc/aspect/ChannelAspect.java new file mode 100644 index 00000000..499c4701 --- /dev/null +++ b/contact-center/app/src/main/java/com/cskefu/cc/aspect/ChannelAspect.java @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd. + * , Licensed under the Chunsong Public + * License, Version 1.0 (the "License"), https://docs.cskefu.com/licenses/v1.html + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.cskefu.cc.aspect; + +import com.cskefu.cc.basic.MainContext; +import com.cskefu.cc.exception.BillingQuotaException; +import com.cskefu.cc.exception.BillingResourceException; +import com.cskefu.cc.model.Channel; +import com.cskefu.cc.model.User; +import com.cskefu.cc.proxy.LicenseProxy; +import org.apache.commons.lang3.StringUtils; +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Before; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +@Aspect +@Component +public class ChannelAspect { + + private final static Logger logger = LoggerFactory.getLogger(ChannelAspect.class); + + @Autowired + private LicenseProxy licenseProxy; + + @Before("execution(* com.cskefu.cc.persistence.repository.ChannelRepository.save(..))") + public void beforeSave(final JoinPoint joinPoint) throws BillingResourceException, BillingQuotaException { + final Channel channel = (Channel) joinPoint.getArgs()[0]; + logger.info("[beforeSave] before channel id {}, type {}", channel.getId(), channel.getType()); + if (StringUtils.isBlank(channel.getId())) { + // create new Channel + if (StringUtils.equals(channel.getType(), MainContext.ChannelType.WEBIM.toString())) { + // create new WEBIM channel + licenseProxy.writeDownResourceUsageInStore(MainContext.BillingResource.CHANNELWEBIM, 1); + } + } else { + // update existed Channel + } + } +} diff --git a/contact-center/app/src/main/java/com/cskefu/cc/aspect/ContactsAspect.java b/contact-center/app/src/main/java/com/cskefu/cc/aspect/ContactsAspect.java new file mode 100644 index 00000000..8c7f3e6b --- /dev/null +++ b/contact-center/app/src/main/java/com/cskefu/cc/aspect/ContactsAspect.java @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd. + * , Licensed under the Chunsong Public + * License, Version 1.0 (the "License"), https://docs.cskefu.com/licenses/v1.html + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.cskefu.cc.aspect; + +import com.cskefu.cc.basic.MainContext; +import com.cskefu.cc.basic.MainUtils; +import com.cskefu.cc.exception.BillingQuotaException; +import com.cskefu.cc.exception.BillingResourceException; +import com.cskefu.cc.model.Contacts; +import com.cskefu.cc.model.User; +import com.cskefu.cc.proxy.LicenseProxy; +import org.apache.commons.lang3.StringUtils; +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Before; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +@Aspect +@Component +public class ContactsAspect { + + private final static Logger logger = LoggerFactory.getLogger(ContactsAspect.class); + + @Autowired + private LicenseProxy licenseProxy; + + @Before("execution(* com.cskefu.cc.persistence.repository.ContactsRepository.save(..))") + public void beforeSave(final JoinPoint joinPoint) throws BillingResourceException, BillingQuotaException { + final Contacts contacts = (Contacts) joinPoint.getArgs()[0]; + logger.info("[save] before contacts id {}", contacts.getId()); + if (StringUtils.isBlank(contacts.getId())) { + // 执行配额扣除 + licenseProxy.writeDownResourceUsageInStore(MainContext.BillingResource.CONTACT, 1); + contacts.setId(MainUtils.getUUID()); + } else { + // update existed user + } + } +} diff --git a/contact-center/app/src/main/java/com/cskefu/cc/aspect/OrganAspect.java b/contact-center/app/src/main/java/com/cskefu/cc/aspect/OrganAspect.java new file mode 100644 index 00000000..69e1f4a7 --- /dev/null +++ b/contact-center/app/src/main/java/com/cskefu/cc/aspect/OrganAspect.java @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd. + * , Licensed under the Chunsong Public + * License, Version 1.0 (the "License"), https://docs.cskefu.com/licenses/v1.html + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.cskefu.cc.aspect; + +import com.cskefu.cc.basic.MainContext; +import com.cskefu.cc.exception.BillingQuotaException; +import com.cskefu.cc.exception.BillingResourceException; +import com.cskefu.cc.model.Channel; +import com.cskefu.cc.model.Organ; +import com.cskefu.cc.proxy.LicenseProxy; +import org.apache.commons.lang3.StringUtils; +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Before; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +@Aspect +@Component +public class OrganAspect { + private final static Logger logger = LoggerFactory.getLogger(OrganAspect.class); + + @Autowired + private LicenseProxy licenseProxy; + + @Before("execution(* com.cskefu.cc.persistence.repository.OrganRepository.save(..))") + public void beforeSave(final JoinPoint joinPoint) throws BillingResourceException, BillingQuotaException { + final Organ organ = (Organ) joinPoint.getArgs()[0]; + logger.info("[beforeSave] before organ id {}", organ.getId()); + if (StringUtils.isBlank(organ.getId())) { + // create new organ + licenseProxy.writeDownResourceUsageInStore(MainContext.BillingResource.ORGAN, 1); + } else { + // update existed Channel + } + } +} diff --git a/contact-center/app/src/main/java/com/cskefu/cc/aspect/UserAspect.java b/contact-center/app/src/main/java/com/cskefu/cc/aspect/UserAspect.java new file mode 100644 index 00000000..1f67cc90 --- /dev/null +++ b/contact-center/app/src/main/java/com/cskefu/cc/aspect/UserAspect.java @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd. + * , Licensed under the Chunsong Public + * License, Version 1.0 (the "License"), https://docs.cskefu.com/licenses/v1.html + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.cskefu.cc.aspect; + +import com.cskefu.cc.basic.MainContext; +import com.cskefu.cc.exception.BillingQuotaException; +import com.cskefu.cc.exception.BillingResourceException; +import com.cskefu.cc.model.User; +import com.cskefu.cc.proxy.LicenseProxy; +import org.apache.commons.lang3.StringUtils; +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Before; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +@Aspect +@Component +public class UserAspect { + + private final static Logger logger = LoggerFactory.getLogger(UserAspect.class); + + @Autowired + private LicenseProxy licenseProxy; + + @Before("execution(* com.cskefu.cc.persistence.repository.UserRepository.save(..))") + public void beforeSave(final JoinPoint joinPoint) throws BillingResourceException, BillingQuotaException { + final User user = (User) joinPoint.getArgs()[0]; + logger.info("[save] before user id {}", user.getId()); + if (StringUtils.isBlank(user.getId())) { + // 执行配额扣除 + licenseProxy.writeDownResourceUsageInStore(MainContext.BillingResource.USER, 1); + } else { + // update existed user + } + } +} diff --git a/contact-center/app/src/main/java/com/cskefu/cc/basic/Constants.java b/contact-center/app/src/main/java/com/cskefu/cc/basic/Constants.java index 6df47781..8fc7cdfe 100644 --- a/contact-center/app/src/main/java/com/cskefu/cc/basic/Constants.java +++ b/contact-center/app/src/main/java/com/cskefu/cc/basic/Constants.java @@ -1,14 +1,14 @@ -/* - * Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd. - * , Licensed under the Chunsong Public +/* + * Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd. + * , Licensed under the Chunsong Public * License, Version 1.0 (the "License"), https://docs.cskefu.com/licenses/v1.html * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. - * Copyright (C) 2019-2022 Chatopera Inc, , - * Licensed under the Apache License, Version 2.0, + * Copyright (C) 2019-2022 Chatopera Inc, , + * Licensed under the Apache License, Version 2.0, * http://www.apache.org/licenses/LICENSE-2.0 */ package com.cskefu.cc.basic; @@ -217,4 +217,26 @@ public class Constants { public static final String AUTH_TOKEN_TYPE_BEARER = "Bearer"; public static final String AUTH_TOKEN_TYPE_BASIC = "Basic"; + /** + * License + */ + public static final String PRODUCT_ID_CSKEFU001 = "cskefu001"; + public static final String LICENSE_SERVER_INST_ID = "SERVERINSTID"; + public static final String LICENSE_SERVICE_NAME = "SERVICENAME"; + public static final String LICENSE_SERVICE_NAME_PREFIX = "春松客服"; + public static final String LICENSEIDS = "LICENSEIDS"; + public static final String METAKV_DATATYPE_STRING = "string"; + public static final String METAKV_DATATYPE_INT = "int"; + public static final String SHORTID = "shortId"; + public static final String LICENSES = "licenses"; + public static final String ADDDATE = "addDate"; + public static final String LICENSE = "license"; + public static final String UPDATETIME = "updateTime"; + public static final String STATUS = "status"; + public static final String PRODUCT = "product"; + public static final String LICENSESTOREPROVIDER = "licenseStoreProvider"; + public static final String USER = "user"; + public static final String RESOURCES_USAGE_KEY_PREFIX = "RESOURCES_USAGE"; + public static final String NEW_USER_SUCCESS = "new_user_success"; + public static final String PRODUCT_ID = "productId"; } diff --git a/contact-center/app/src/main/java/com/cskefu/cc/basic/MainContext.java b/contact-center/app/src/main/java/com/cskefu/cc/basic/MainContext.java index 753d6d74..8e3f25ce 100644 --- a/contact-center/app/src/main/java/com/cskefu/cc/basic/MainContext.java +++ b/contact-center/app/src/main/java/com/cskefu/cc/basic/MainContext.java @@ -1,15 +1,15 @@ /* - * Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd. - * , Licensed under the Chunsong Public + * Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd. + * , Licensed under the Chunsong Public * License, Version 1.0 (the "License"), https://docs.cskefu.com/licenses/v1.html * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. - * Copyright (C) 2018- Jun. 2023 Chatopera Inc, , Licensed under the Apache License, Version 2.0, + * Copyright (C) 2018- Jun. 2023 Chatopera Inc, , Licensed under the Apache License, Version 2.0, * http://www.apache.org/licenses/LICENSE-2.0 - * Copyright (C) 2017 优客服-多渠道客服系统, Licensed under the Apache License, Version 2.0, + * Copyright (C) 2017 优客服-多渠道客服系统, Licensed under the Apache License, Version 2.0, * http://www.apache.org/licenses/LICENSE-2.0 */ @@ -929,6 +929,31 @@ public class MainContext { } } + /** + * 计费资源 + */ + public enum BillingResource { + USER, // 系统用户 + AGENGUSER, // 访客会话 + CONTACT, // 联系人 + ORGAN, // 组织机构 + CHANNELWEBIM; // 网页渠道 + + @Override + public String toString() { + return super.toString().toLowerCase(); + } + + public static BillingResource toValue(final String str) { + for (final BillingResource item : values()) { + if (StringUtils.equalsIgnoreCase(item.toString(), str)) { + return item; + } + } + throw new IllegalArgumentException(); + } + } + public static void setApplicationContext(ApplicationContext context) { applicationContext = context; context.getBean(TerminateBean.class); diff --git a/contact-center/app/src/main/java/com/cskefu/cc/config/AppCtxRefreshEventListener.java b/contact-center/app/src/main/java/com/cskefu/cc/config/AppCtxRefreshEventListener.java index 234abb1a..51d4a6a8 100644 --- a/contact-center/app/src/main/java/com/cskefu/cc/config/AppCtxRefreshEventListener.java +++ b/contact-center/app/src/main/java/com/cskefu/cc/config/AppCtxRefreshEventListener.java @@ -24,6 +24,7 @@ import com.cskefu.cc.model.BlackEntity; import com.cskefu.cc.model.SysDic; import com.cskefu.cc.model.SystemConfig; import com.cskefu.cc.persistence.repository.*; +import com.cskefu.cc.proxy.LicenseProxy; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -36,7 +37,6 @@ public class AppCtxRefreshEventListener implements ApplicationListener, Licensed under the Chunsong Public + * License, Version 1.0 (the "License"), https://docs.cskefu.com/licenses/v1.html + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * Copyright (C) 2019-2022 Chatopera Inc, , + * Licensed under the Apache License, Version 2.0, + * http://www.apache.org/licenses/LICENSE-2.0 + */ +package com.cskefu.cc.config; + +import com.chatopera.store.sdk.QuotaWdClient; +import com.chatopera.store.sdk.exceptions.InvalidProviderException; +import com.cskefu.cc.basic.MainContext; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.SpringApplication; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class QuotaWdClientConfig { + + @Value("${license.store.provider}") + private String licenseStoreProvider; + + /** + * 证书商店服务客户端 + * + * @return + */ + @Bean + public QuotaWdClient quotaWdClient() throws InvalidProviderException { + if (StringUtils.isBlank(licenseStoreProvider)) { + System.out.println("[license] invalid license provider info, service is terminated."); + System.exit(1); + } + + QuotaWdClient quotaWdClient = new QuotaWdClient(); + return quotaWdClient; + } +} diff --git a/contact-center/app/src/main/java/com/cskefu/cc/controller/admin/LicenseController.java b/contact-center/app/src/main/java/com/cskefu/cc/controller/admin/LicenseController.java new file mode 100644 index 00000000..2c404e16 --- /dev/null +++ b/contact-center/app/src/main/java/com/cskefu/cc/controller/admin/LicenseController.java @@ -0,0 +1,231 @@ +/* + * Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd. + * , Licensed under the Chunsong Public + * License, Version 1.0 (the "License"), https://docs.cskefu.com/licenses/v1.html + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.cskefu.cc.controller.admin; + +import com.chatopera.store.sdk.exceptions.InvalidRequestException; +import com.chatopera.store.sdk.exceptions.InvalidResponseException; +import com.cskefu.cc.basic.Constants; +import com.cskefu.cc.controller.Handler; +import com.cskefu.cc.exception.LicenseNotFoundException; +import com.cskefu.cc.exception.MetaKvInvalidKeyException; +import com.cskefu.cc.model.User; +import com.cskefu.cc.proxy.LicenseProxy; +import com.cskefu.cc.util.Menu; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.validation.Valid; +import org.apache.commons.lang3.StringUtils; +import org.json.JSONArray; +import org.json.JSONObject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Controller; +import org.springframework.ui.ModelMap; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.servlet.ModelAndView; + +import java.util.Date; +import java.util.List; + +@Controller +@RequestMapping("/admin/license") +public class LicenseController extends Handler { + private final static Logger logger = LoggerFactory.getLogger(LicenseController.class); + + @Autowired + private LicenseProxy licenseProxy; + + @RequestMapping("/index") + @Menu(type = "admin", subtype = "licenseList") + public ModelAndView index(ModelMap map, HttpServletRequest request) { + User user = super.getUser(request); + if (user.isSuperadmin()) { + try { + List licenses = licenseProxy.getLicensesInStore(); + map.addAttribute(Constants.UPDATETIME, new Date()); + map.addAttribute(Constants.LICENSES, licenses); + map.addAttribute(Constants.LICENSESTOREPROVIDER, licenseProxy.getLicenseStoreProvider()); + + } catch (InvalidResponseException e) { + throw new RuntimeException(e); + } + return request(super.createView("/admin/license/index")); + } else { + return request(super.createView("/public/error")); + } + } + + @RequestMapping("/add") + @Menu(type = "admin", subtype = "licenseList") + public ModelAndView add(ModelMap map, HttpServletRequest request) { + User user = super.getUser(request); + if (user.isSuperadmin()) { + return request(super.createView("/admin/license/add")); + } else { + return request(super.createView("/public/error")); + } + } + + /** + * 保存新的证书 + * + * @param map + * @param request + * @param licenseShortId + * @return + */ + @RequestMapping("/save") + @Menu(type = "admin", subtype = "licenseList") + public ModelAndView save(ModelMap map, + HttpServletRequest request, + @Valid String licenseShortId) throws MetaKvInvalidKeyException, InvalidRequestException { + User user = super.getUser(request); + logger.info("[save] licenseShortId {}", licenseShortId); + String msg = ""; + + if (user.isSuperadmin()) { + try { + /** + * 验证证书可以添加 + */ + // 验证该证书不在当前证书列表中 + JSONArray currents = licenseProxy.getLicensesInMetakv(); + boolean isAddedBefore = false; + + for (int i = 0; i < currents.length(); i++) { + JSONObject item = (JSONObject) currents.get(i); + if (StringUtils.equals(item.getString(Constants.SHORTID), licenseShortId)) { + isAddedBefore = true; + break; + } + } + + if (isAddedBefore) { + msg = "already_added"; + return request(super.createView( + "redirect:/admin/license/index.html?msg=" + msg)); + } + + // 验证该证书存在 + licenseProxy.existLicenseInStore(licenseShortId); + + // 验证该证书的所属产品没有现在没有其它证书:同一个产品最多只有一个证书 + JSONObject licBasic = licenseProxy.getLicenseBasicsInStore(licenseShortId); + final String productId = licBasic.getJSONObject(Constants.PRODUCT).getString(Constants.SHORTID); + + boolean isProductAdded = false; + JSONArray addedLicenseBasicsFromStore = licenseProxy.getAddedLicenseBasicsInStore(); + + for (int i = 0; i < addedLicenseBasicsFromStore.length(); i++) { + JSONObject item = (JSONObject) addedLicenseBasicsFromStore.get(i); + if (StringUtils.equals(item.getJSONObject(Constants.PRODUCT).getString(Constants.SHORTID), productId)) { + isProductAdded = true; + break; + } + } + + if (isProductAdded) { + msg = "product_added_already"; + return request(super.createView( + "redirect:/admin/license/index.html?msg=" + msg)); + } + + /** + * 添加该证书 + */ + JSONObject licenseKvData = new JSONObject(); + licenseKvData.put(Constants.SHORTID, licenseShortId); + licenseKvData.put(Constants.ADDDATE, new Date()); + licenseKvData.put(Constants.PRODUCT_ID, productId); + currents.put(0, licenseKvData); + licenseProxy.createOrUpdateMetaKv(Constants.LICENSEIDS, currents.toString(), Constants.METAKV_DATATYPE_STRING); + + // 跳转回到证书列表 + List licenses = licenseProxy.getLicensesInStore(); + map.addAttribute(Constants.LICENSES, licenses); + map.addAttribute(Constants.UPDATETIME, new Date()); + map.addAttribute(Constants.LICENSESTOREPROVIDER, licenseProxy.getLicenseStoreProvider()); + + return request(super.createView("/admin/license/index")); + } catch (InvalidResponseException e) { + logger.warn("[save] error in getLicenseFromStore", e); + msg = "invalid_id"; + return request(super.createView( + "redirect:/admin/license/index.html?msg=" + msg)); + } catch (LicenseNotFoundException e) { + logger.warn("[save] error in getLicenseFromStore", e); + msg = "notfound_id"; + return request(super.createView( + "redirect:/admin/license/index.html?msg=" + msg)); + } + } else { + return request(super.createView("/public/error")); + } + } + + @RequestMapping("/delete/{licenseShortId}") + @Menu(type = "admin", subtype = "licenseList") + public ModelAndView delete(ModelMap map, + HttpServletRequest request, + @PathVariable String licenseShortId) throws MetaKvInvalidKeyException { + User user = super.getUser(request); + logger.info("[delete] licenseShortId {}", licenseShortId); + String msg = ""; + + if (user.isSuperadmin()) { + try { + JSONArray currents = licenseProxy.getLicensesInMetakv(); + JSONArray post = new JSONArray(); + for (int i = 0; i < currents.length(); i++) { + JSONObject item = (JSONObject) currents.get(i); + if (!StringUtils.equals(item.getString(Constants.SHORTID), licenseShortId)) { + post.put(item); + } + } + + /** + * 添加该证书 + */ + licenseProxy.createOrUpdateMetaKv(Constants.LICENSEIDS, post.toString(), Constants.METAKV_DATATYPE_STRING); + + // 跳转回到证书列表 + List licenses = licenseProxy.getLicensesInStore(); + map.addAttribute(Constants.LICENSES, licenses); + map.addAttribute("updateTime", new Date()); + map.addAttribute(Constants.LICENSESTOREPROVIDER, licenseProxy.getLicenseStoreProvider()); + + return request(super.createView("/admin/license/index")); + } catch (InvalidResponseException e) { + logger.warn("[save] error in getLicenseFromStore", e); + msg = "invalid_id"; + return request(super.createView( + "redirect:/admin/license/index.html?msg=" + msg)); + } + } else { + return request(super.createView("/public/error")); + } + } + + @RequestMapping("/instance") + @Menu(type = "admin", subtype = "licenseInst") + public ModelAndView getInstanceInfo(ModelMap map, HttpServletRequest request) { + User user = super.getUser(request); + if (user.isSuperadmin()) { + map.addAttribute(Constants.LICENSE_SERVICE_NAME, licenseProxy.resolveServicename()); + map.addAttribute(Constants.LICENSE_SERVER_INST_ID, licenseProxy.resolveServerinstId()); + map.addAttribute(Constants.LICENSESTOREPROVIDER, licenseProxy.getLicenseStoreProvider()); + return request(super.createView("/admin/license/instance")); + } else { + return request(super.createView("/public/error")); + } + } +} diff --git a/contact-center/app/src/main/java/com/cskefu/cc/controller/admin/OrganController.java b/contact-center/app/src/main/java/com/cskefu/cc/controller/admin/OrganController.java index dadf3216..d44d5087 100644 --- a/contact-center/app/src/main/java/com/cskefu/cc/controller/admin/OrganController.java +++ b/contact-center/app/src/main/java/com/cskefu/cc/controller/admin/OrganController.java @@ -1,15 +1,15 @@ /* - * Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd. - * , Licensed under the Chunsong Public + * Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd. + * , Licensed under the Chunsong Public * License, Version 1.0 (the "License"), https://docs.cskefu.com/licenses/v1.html * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. - * Copyright (C) 2018- Jun. 2023 Chatopera Inc, , Licensed under the Apache License, Version 2.0, + * Copyright (C) 2018- Jun. 2023 Chatopera Inc, , Licensed under the Apache License, Version 2.0, * http://www.apache.org/licenses/LICENSE-2.0 - * Copyright (C) 2017 优客服-多渠道客服系统, Licensed under the Apache License, Version 2.0, + * Copyright (C) 2017 优客服-多渠道客服系统, Licensed under the Apache License, Version 2.0, * http://www.apache.org/licenses/LICENSE-2.0 */ package com.cskefu.cc.controller.admin; @@ -17,6 +17,7 @@ package com.cskefu.cc.controller.admin; import com.cskefu.cc.basic.Constants; import com.cskefu.cc.cache.Cache; import com.cskefu.cc.controller.Handler; +import com.cskefu.cc.exception.BillingQuotaException; import com.cskefu.cc.model.*; import com.cskefu.cc.persistence.repository.*; import com.cskefu.cc.proxy.OrganProxy; @@ -34,6 +35,8 @@ import org.springframework.web.servlet.ModelAndView; import jakarta.servlet.http.HttpServletRequest; import jakarta.validation.Valid; + +import java.lang.reflect.UndeclaredThrowableException; import java.util.*; /** @@ -157,16 +160,26 @@ public class OrganController extends Handler { public ModelAndView save(HttpServletRequest request, @Valid Organ organ) { Organ tempOrgan = organRepository.findByName(organ.getName()); String msg = "admin_organ_new_success"; - String firstId = null; + String createdId = null; if (tempOrgan != null) { msg = "admin_organ_update_name_not"; // 分类名字重复 } else { - firstId = organ.getId(); - - organRepository.save(organ); + try { + organRepository.save(organ); + createdId = organ.getId(); + } catch (Exception e) { + if (e instanceof UndeclaredThrowableException) { + logger.error("[save] BillingQuotaException", e); + if (StringUtils.startsWith(e.getCause().getMessage(), BillingQuotaException.SUFFIX)) { + msg = e.getCause().getMessage(); + } + } else { + logger.error("[save] err", e); + } + } } return request(super.createView( - "redirect:/admin/organ/index.html?msg=" + msg + "&organ=" + firstId)); + "redirect:/admin/organ/index.html?msg=" + msg + "&organ=" + createdId)); } /** diff --git a/contact-center/app/src/main/java/com/cskefu/cc/controller/admin/channel/ChannelController.java b/contact-center/app/src/main/java/com/cskefu/cc/controller/admin/channel/ChannelController.java index 93c5ff0f..53a7cf18 100644 --- a/contact-center/app/src/main/java/com/cskefu/cc/controller/admin/channel/ChannelController.java +++ b/contact-center/app/src/main/java/com/cskefu/cc/controller/admin/channel/ChannelController.java @@ -1,15 +1,15 @@ /* - * Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd. - * , Licensed under the Chunsong Public + * Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd. + * , Licensed under the Chunsong Public * License, Version 1.0 (the "License"), https://docs.cskefu.com/licenses/v1.html * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. - * Copyright (C) 2018- Jun. 2023 Chatopera Inc, , Licensed under the Apache License, Version 2.0, + * Copyright (C) 2018- Jun. 2023 Chatopera Inc, , Licensed under the Apache License, Version 2.0, * http://www.apache.org/licenses/LICENSE-2.0 - * Copyright (C) 2017 优客服-多渠道客服系统, Licensed under the Apache License, Version 2.0, + * Copyright (C) 2017 优客服-多渠道客服系统, Licensed under the Apache License, Version 2.0, * http://www.apache.org/licenses/LICENSE-2.0 */ package com.cskefu.cc.controller.admin.channel; @@ -18,6 +18,7 @@ import com.cskefu.cc.basic.MainContext; import com.cskefu.cc.basic.MainUtils; import com.cskefu.cc.cache.Cache; import com.cskefu.cc.controller.Handler; +import com.cskefu.cc.exception.BillingQuotaException; import com.cskefu.cc.model.*; import com.cskefu.cc.persistence.repository.ConsultInviteRepository; import com.cskefu.cc.persistence.repository.OrganRepository; @@ -27,6 +28,8 @@ import com.cskefu.cc.proxy.OrganProxy; import com.cskefu.cc.util.Base62; import com.cskefu.cc.util.Menu; import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.PageRequest; import org.springframework.stereotype.Controller; @@ -37,6 +40,8 @@ import org.springframework.web.servlet.ModelAndView; import jakarta.servlet.http.HttpServletRequest; import jakarta.validation.Valid; + +import java.lang.reflect.UndeclaredThrowableException; import java.security.NoSuchAlgorithmException; import java.util.Date; import java.util.List; @@ -48,6 +53,7 @@ import java.util.Map; @Controller @RequestMapping("/admin/im") public class ChannelController extends Handler { + private final static Logger logger = LoggerFactory.getLogger(ChannelController.class); @Autowired private ChannelRepository snsAccountRes; @@ -93,40 +99,59 @@ public class ChannelController extends Handler { return request(super.createView("/admin/channel/im/add")); } + /** + * 创建新的网站渠道 + * + * @param request + * @param channel + * @return + * @throws NoSuchAlgorithmException + */ @RequestMapping("/save") - @Menu(type = "admin", subtype = "weixin") + @Menu(type = "admin", subtype = "im") public ModelAndView save(HttpServletRequest request, @Valid Channel channel) throws NoSuchAlgorithmException { Organ currentOrgan = super.getOrgan(request); String status = "new_webim_fail"; if (StringUtils.isNotBlank(channel.getBaseURL())) { - channel.setSnsid(Base62.encode(channel.getBaseURL()).toLowerCase()); - int count = snsAccountRes.countBySnsid(channel.getSnsid()); - if (count == 0) { - status = "new_webim_success"; - channel.setType(MainContext.ChannelType.WEBIM.toString()); - channel.setCreatetime(new Date()); - User curr = super.getUser(request); - channel.setCreater(curr.getId()); - channel.setOrgan(currentOrgan.getId()); + try { + channel.setSnsid(Base62.encode(channel.getBaseURL()).toLowerCase()); + int count = snsAccountRes.countBySnsid(channel.getSnsid()); + if (count == 0) { + status = "new_webim_success"; + channel.setType(MainContext.ChannelType.WEBIM.toString()); + channel.setCreatetime(new Date()); + User curr = super.getUser(request); + channel.setCreater(curr.getId()); + channel.setOrgan(currentOrgan.getId()); - snsAccountRes.save(channel); + snsAccountRes.save(channel); - /** - * 同时创建CousultInvite 记录 - */ - CousultInvite coultInvite = invite.findBySnsaccountid(channel.getSnsid()); - if (coultInvite == null) { - coultInvite = new CousultInvite(); - coultInvite.setSnsaccountid(channel.getSnsid()); - coultInvite.setCreate_time(new Date()); - coultInvite.setName(channel.getName()); - coultInvite.setOwner(channel.getCreater()); - coultInvite.setSkill(false); // 不启动技能组 - coultInvite.setConsult_skill_fixed(false); // 不绑定唯一技能组 - coultInvite.setAi(false); - coultInvite.setAifirst(false); - invite.save(coultInvite); + /** + * 同时创建CousultInvite 记录 + */ + CousultInvite coultInvite = invite.findBySnsaccountid(channel.getSnsid()); + if (coultInvite == null) { + coultInvite = new CousultInvite(); + coultInvite.setSnsaccountid(channel.getSnsid()); + coultInvite.setCreate_time(new Date()); + coultInvite.setName(channel.getName()); + coultInvite.setOwner(channel.getCreater()); + coultInvite.setSkill(false); // 不启动技能组 + coultInvite.setConsult_skill_fixed(false); // 不绑定唯一技能组 + coultInvite.setAi(false); + coultInvite.setAifirst(false); + invite.save(coultInvite); + } + } + } catch (Exception e) { + if (e instanceof UndeclaredThrowableException) { + logger.error("[save] BillingQuotaException", e); + if (StringUtils.startsWith(e.getCause().getMessage(), BillingQuotaException.SUFFIX)) { + status = e.getCause().getMessage(); + } + } else { + logger.error("[save] err", e); } } } diff --git a/contact-center/app/src/main/java/com/cskefu/cc/controller/admin/system/MetadataController.java b/contact-center/app/src/main/java/com/cskefu/cc/controller/admin/system/MetadataController.java index fba17dac..a9208a84 100644 --- a/contact-center/app/src/main/java/com/cskefu/cc/controller/admin/system/MetadataController.java +++ b/contact-center/app/src/main/java/com/cskefu/cc/controller/admin/system/MetadataController.java @@ -41,7 +41,7 @@ import jakarta.persistence.EntityManager; import jakarta.persistence.PersistenceContext; import jakarta.servlet.http.HttpServletRequest; import jakarta.validation.Valid; -import java.sql.Connection; + import java.sql.SQLException; import java.util.ArrayList; import java.util.Arrays; diff --git a/contact-center/app/src/main/java/com/cskefu/cc/controller/api/ApiUserController.java b/contact-center/app/src/main/java/com/cskefu/cc/controller/api/ApiUserController.java index 86cfb791..08956ef1 100644 --- a/contact-center/app/src/main/java/com/cskefu/cc/controller/api/ApiUserController.java +++ b/contact-center/app/src/main/java/com/cskefu/cc/controller/api/ApiUserController.java @@ -1,15 +1,15 @@ /* - * Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd. - * , Licensed under the Chunsong Public + * Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd. + * , Licensed under the Chunsong Public * License, Version 1.0 (the "License"), https://docs.cskefu.com/licenses/v1.html * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. - * Copyright (C) 2018- Jun. 2023 Chatopera Inc, , Licensed under the Apache License, Version 2.0, + * Copyright (C) 2018- Jun. 2023 Chatopera Inc, , Licensed under the Apache License, Version 2.0, * http://www.apache.org/licenses/LICENSE-2.0 - * Copyright (C) 2017 优客服-多渠道客服系统, Licensed under the Apache License, Version 2.0, + * Copyright (C) 2017 优客服-多渠道客服系统, Licensed under the Apache License, Version 2.0, * http://www.apache.org/licenses/LICENSE-2.0 */ package com.cskefu.cc.controller.api; @@ -183,7 +183,9 @@ public class ApiUserController extends Handler { User user = userProxy.parseUserFromJson(payload); JsonObject resp = userProxy.createNewUser(user, parentOrgan); - if (StringUtils.isNotEmpty(roleId)) { + if (StringUtils.isNotEmpty(roleId) && + StringUtils.equals(resp.get(RestUtils.RESP_KEY_DATA).getAsString(), + Constants.NEW_USER_SUCCESS)) { Role role = roleRes.findById(roleId).orElse(null); UserRole userRole = new UserRole(); userRole.setUser(user); diff --git a/contact-center/app/src/main/java/com/cskefu/cc/controller/apps/AgentAuditController.java b/contact-center/app/src/main/java/com/cskefu/cc/controller/apps/AgentAuditController.java index e781cf0b..86c7e958 100644 --- a/contact-center/app/src/main/java/com/cskefu/cc/controller/apps/AgentAuditController.java +++ b/contact-center/app/src/main/java/com/cskefu/cc/controller/apps/AgentAuditController.java @@ -71,9 +71,6 @@ public class AgentAuditController extends Handler { @Autowired private UserRepository userRes; - @Autowired - private AgentUserRepository agentUserRepository; - @Autowired private ChatMessageRepository chatMessageRepository; @@ -245,7 +242,7 @@ public class AgentAuditController extends Handler { view.addObject( "agentUserList", agentUserRes.findByStatusAndAgentnoIsNot( MainContext.AgentUserStatusEnum.INSERVICE.toString(), logined.getId(), defaultSort)); - List agentUserList = agentUserRepository.findByUserid(userid); + List agentUserList = agentUserRes.findByUserid(userid); view.addObject( "curagentuser", agentUserList != null && agentUserList.size() > 0 ? agentUserList.get(0) : null); @@ -266,7 +263,7 @@ public class AgentAuditController extends Handler { } ModelAndView view = request(super.createView(mainagentuser)); final User logined = super.getUser(request); - AgentUser agentUser = agentUserRepository.findById(id).orElse(null); + AgentUser agentUser = agentUserRes.findById(id).orElse(null); if (agentUser != null) { view.addObject("curagentuser", agentUser); diff --git a/contact-center/app/src/main/java/com/cskefu/cc/controller/apps/AgentController.java b/contact-center/app/src/main/java/com/cskefu/cc/controller/apps/AgentController.java index 7beb6194..5011d63d 100644 --- a/contact-center/app/src/main/java/com/cskefu/cc/controller/apps/AgentController.java +++ b/contact-center/app/src/main/java/com/cskefu/cc/controller/apps/AgentController.java @@ -1,1255 +1,1264 @@ /* - * Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd. - * , Licensed under the Chunsong Public + * Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd. + * , Licensed under the Chunsong Public * License, Version 1.0 (the "License"), https://docs.cskefu.com/licenses/v1.html * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. - * Copyright (C) 2018-Jun. 2023 Chatopera Inc, , Licensed under the Apache License, Version 2.0, + * Copyright (C) 2018-Jun. 2023 Chatopera Inc, , Licensed under the Apache License, Version 2.0, * http://www.apache.org/licenses/LICENSE-2.0 - * Copyright (C) 2017 优客服-多渠道客服系统, Licensed under the Apache License, Version 2.0, + * Copyright (C) 2017 优客服-多渠道客服系统, Licensed under the Apache License, Version 2.0, * http://www.apache.org/licenses/LICENSE-2.0 */ - package com.cskefu.cc.controller.apps; - - import com.alibaba.fastjson.JSONObject; - import com.cskefu.cc.acd.ACDAgentService; - import com.cskefu.cc.acd.ACDWorkMonitor; - import com.cskefu.cc.activemq.BrokerPublisher; - import com.cskefu.cc.basic.Constants; - import com.cskefu.cc.basic.MainContext; - import com.cskefu.cc.basic.MainUtils; - import com.cskefu.cc.cache.Cache; - import com.cskefu.cc.controller.Handler; - import com.cskefu.cc.util.restapi.RestUtils; - import com.cskefu.cc.exception.CSKefuException; - import com.cskefu.cc.model.*; - import com.cskefu.cc.peer.PeerSyncIM; - import com.cskefu.cc.persistence.blob.JpaBlobHelper; - import com.cskefu.cc.persistence.interfaces.DataExchangeInterface; - import com.cskefu.cc.persistence.repository.*; - import com.cskefu.cc.proxy.*; - import com.cskefu.cc.socketio.message.ChatMessage; - import com.cskefu.cc.socketio.message.Message; - import com.cskefu.cc.util.Menu; - import com.cskefu.cc.util.PinYinTools; - import com.cskefu.cc.util.PropertiesEventUtil; - import org.apache.commons.lang3.StringUtils; - import org.slf4j.Logger; - import org.slf4j.LoggerFactory; - import org.springframework.beans.factory.annotation.Autowired; - import org.springframework.beans.factory.annotation.Value; - import org.springframework.context.annotation.Lazy; - import org.springframework.data.domain.Page; - import org.springframework.data.domain.PageRequest; - import org.springframework.data.domain.Sort; - import org.springframework.data.domain.Sort.Direction; - import org.springframework.http.HttpHeaders; - import org.springframework.http.HttpStatus; - import org.springframework.http.ResponseEntity; - import org.springframework.stereotype.Controller; - import org.springframework.ui.ModelMap; - import org.springframework.util.FileCopyUtils; - import org.springframework.web.bind.annotation.RequestMapping; - import org.springframework.web.bind.annotation.RequestParam; - import org.springframework.web.bind.annotation.ResponseBody; - import org.springframework.web.multipart.MultipartFile; - 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.text.ParseException; - import java.text.SimpleDateFormat; - import java.util.ArrayList; - import java.util.Date; - import java.util.List; - import java.util.Map; - - @Controller - @RequestMapping("/agent") - public class AgentController extends Handler { - - static final Logger logger = LoggerFactory.getLogger(AgentController.class); - - @Autowired - private ACDWorkMonitor acdWorkMonitor; - - @Autowired - private ACDAgentService acdAgentService; - - @Autowired - private ContactsRepository contactsRes; - - @Autowired - private PropertiesEventRepository propertiesEventRes; - - @Autowired - private AgentUserRepository agentUserRes; - - @Autowired - private AgentStatusRepository agentStatusRes; - - @Autowired - private AgentServiceRepository agentServiceRes; - - @Autowired - private PassportWebIMUserRepository onlineUserRes; - - @Autowired - private WeiXinUserRepository weiXinUserRes; - - @Autowired - private ServiceSummaryRepository serviceSummaryRes; - - @Autowired - private ChatMessageRepository chatMessageRes; - - @Autowired - private AgentProxy agentProxy; - - @Autowired - private TagRepository tagRes; - - @Autowired - private TagRelationRepository tagRelationRes; - - @Autowired - private AgentUserTaskRepository agentUserTaskRes; - - @Autowired - private UserRepository userRes; - - @Autowired - private StatusEventRepository statusEventRes; - - @Autowired - private AgentUserProxy agentUserProxy; - - @Autowired - private PbxHostRepository pbxHostRes; - - @Autowired - private AgentUserContactsRepository agentUserContactsRes; - - @Autowired - private StreamingFileRepository streamingFileRes; - - @Autowired - private JpaBlobHelper jpaBlobHelper; - - @Autowired - private BlackEntityProxy blackEntityProxy; - - @Autowired - private Cache cache; - - @Autowired - private AgentServiceProxy agentServiceProxy; - - @Value("${web.upload-path}") - private String webUploadPath; - - @Autowired - @Lazy - private PeerSyncIM peerSyncIM; - - @Autowired - private BrokerPublisher brokerPublisher; - - @Autowired - private AgentStatusProxy agentStatusProxy; - - @Autowired - private UserProxy userProxy; - - @Autowired - private OrganProxy organProxy; - - @Autowired - private OrganRepository organRes; - - @Autowired - private ChatbotRepository chatbotRes; - - /** - * 坐席从联系人列表进入坐席工作台和该联系人聊天 - * - * @param map - * @param request - * @param response - * @param sort - * @param channels 可立即触达的渠道 - * @param contactid - * @return - * @throws IOException - * @throws TemplateException - */ - @RequestMapping("/proactive") - @Menu(type = "apps", subtype = "agent") - public ModelAndView proactive( - ModelMap map, - HttpServletRequest request, - HttpServletResponse response, - @Valid String sort, - @Valid String channels, - @RequestParam(name = "contactid", required = false) String contactid) throws IOException, CSKefuException { - - if (StringUtils.isBlank(contactid)) { - logger.info("[chat] empty contactid, fast return error page."); - return request(super.createView("/public/error")); - } - - logger.info( - "[chat] contactid {}, channels {}", contactid, - channels); - - final User logined = super.getUser(request); - AgentUser agentUser = agentUserProxy.figureAgentUserBeforeChatWithContactInfo(channels, contactid, logined); - - if (agentUser != null) { - logger.info( - "[chat] resolved agentUser, figure view model data as index page, agentUserId {}, onlineUser Id {}, agentno {}, channel {}", - agentUser.getId(), agentUser.getUserid(), agentUser.getAgentno(), agentUser.getChanneltype()); - } else { - logger.info("[chat] can not resolve agentUser !!!"); - } - - // TODO 在agentUser没有得到的情况下,传回的前端信息增加提示,提示放在modelview中 - - // 处理原聊天数据 - ModelAndView view = request(super.createView("/apps/agent/index")); - agentUserProxy.buildIndexViewWithModels(view, map, request, response, sort, logined, agentUser); - return view; - } - - - /** - * 打开坐席工作台 - * - * @param map - * @param request - * @param response - * @param sort - * @return - * @throws IOException - * @throws TemplateException - */ - @RequestMapping("/index") - @Menu(type = "apps", subtype = "agent") - public ModelAndView index( - ModelMap map, - HttpServletRequest request, - HttpServletResponse response, - @Valid String sort) throws IOException { - final User logined = super.getUser(request); - ModelAndView view = request(super.createView("/apps/agent/index")); - agentUserProxy.buildIndexViewWithModels(view, map, request, response, sort, logined, null); - return view; - } - - @RequestMapping("/agentusers") - @Menu(type = "apps", subtype = "agent") - public ModelAndView agentusers(HttpServletRequest request, String userid) { - ModelAndView view = request(super.createView("/apps/agent/agentusers")); - User logined = super.getUser(request); - view.addObject( - "agentUserList", agentUserRes.findByAgentno(logined.getId(), - Sort.by(Direction.DESC, "status"))); - List agentUserList = agentUserRes.findByUserid(userid); - view.addObject( - "curagentuser", agentUserList != null && agentUserList.size() > 0 ? agentUserList.get(0) : null); - - return view; - } - - @RequestMapping("/agentuserpage") - @Menu(type = "apps", subtype = "agent") - public ModelAndView agentuserpage( - ModelMap map, - HttpServletRequest request, - String id, - Integer page, - Integer current) throws IOException { - String mainagentuserconter = "/apps/agent/mainagentuserconter"; - ModelAndView view = request(super.createView(mainagentuserconter)); - AgentUser agentUser = agentUserRes.findById(id).orElse(null); - if (agentUser != null) { - view.addObject("curagentuser", agentUser); - view.addObject( - "agentUserMessageList", this.chatMessageRes.findByusession(agentUser.getUserid(), current)); - } - return view; - } - - @RequestMapping("/agentuserLabel") - @Menu(type = "apps", subtype = "agent") - public ModelAndView agentuserLabel( - ModelMap map, - HttpServletRequest request, - String iconid) throws IOException { - String mainagentuserconter = "/apps/agent/mainagentuserconter"; - ModelAndView view = request(super.createView(mainagentuserconter)); - ChatMessage labelid = this.chatMessageRes.findById(iconid).orElse(null); - if (labelid != null) { - if (labelid.isIslabel() == false) { - labelid.setIslabel(true); - } else { - labelid.setIslabel(false); - } - chatMessageRes.save(labelid); - } - return view; - } - - @RequestMapping("/agentusersearch") - @Menu(type = "apps", subtype = "agent") - public ModelAndView agentusersearch( - ModelMap map, - HttpServletRequest request, - String id, - String search, - String condition - ) throws IOException { - String mainagentuserconter = "/apps/agent/mainagentusersearch"; - ModelAndView view = request(super.createView(mainagentuserconter)); - AgentUser agentUser = agentUserRes.findById(id).orElse(null); - - if (agentUser != null) { - Page agentUserMessageList = null; - if (condition.equals("label")) { - agentUserMessageList = this.chatMessageRes.findByislabel( - agentUser.getUserid(), search, PageRequest.of(0, 9999, Direction.DESC, "updatetime")); - } else { - agentUserMessageList = this.chatMessageRes.findByUsessionAndMessageContaining( - agentUser.getUserid(), search, PageRequest.of(0, 9999, Direction.DESC, "updatetime")); - } - view.addObject("agentUserMessageList", agentUserMessageList); - } - return view; - } - - - @RequestMapping("/agentusersearchdetails") - @Menu(type = "apps", subtype = "agent") - public ModelAndView agentusersearchdetails( - ModelMap map, - HttpServletRequest request, - String id, - String createtime, - String thisid) throws IOException, ParseException { - String mainagentuserconter = "/apps/agent/mainagentuserconter"; - ModelAndView view = request(super.createView(mainagentuserconter)); - AgentUser agentUser = agentUserRes.findById(id).orElse(null); - if (agentUser != null) { - SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); - Date date = formatter.parse(createtime); - view.addObject("agentusersearchdetails", thisid); - view.addObject( - "agentUserMessageList", this.chatMessageRes.findByCreatetime(agentUser.getUserid(), date)); - view.addObject( - "agentUserMessageListnum", this.chatMessageRes.countByUsessionAndCreatetimeGreaterThanEqual( - agentUser.getUserid(), date)); - } - return (view); - } - - @RequestMapping("/agentuser") - @Menu(type = "apps", subtype = "agent") - public ModelAndView agentuser( - ModelMap map, - HttpServletRequest request, - String id, - String channel) throws IOException { - // set default Value as WEBIM - String mainagentuser = "/apps/agent/mainagentuser"; - switch (MainContext.ChannelType.toValue(channel)) { - case MESSENGER: - mainagentuser = "/apps/agent/mainagentuser_messenger"; - break; - case PHONE: - mainagentuser = "/apps/agent/mainagentuser_callout"; - break; - case SKYPE: - mainagentuser = "/apps/agent/mainagentuser_skype"; - break; - } - - ModelAndView view = request(super.createView(mainagentuser)); - final User logined = super.getUser(request); - AgentUser agentUser = agentUserRes.findById(id).orElse(null); - - if (agentUser != null) { - view.addObject("curagentuser", agentUser); - - Chatbot c = chatbotRes.findBySnsAccountIdentifier(agentUser.getAppid()); - if (c != null) { - view.addObject("aisuggest", c.isAisuggest()); - } - - view.addObject("inviteData", OnlineUserProxy.consult(agentUser.getAppid())); - AgentUserTask agentUserTask = agentUserTaskRes.findById(id).orElse(null); - agentUserTask.setTokenum(0); - agentUserTaskRes.save(agentUserTask); - - if (StringUtils.isNotBlank(agentUser.getAgentserviceid())) { - List summarizes = this.serviceSummaryRes.findByAgentserviceid( - agentUser.getAgentserviceid()); - if (summarizes.size() > 0) { - view.addObject("summary", summarizes.get(0)); - } - } - - view.addObject( - "agentUserMessageList", - this.chatMessageRes.findByUsession(agentUser.getUserid(), - PageRequest.of(0, 20, Direction.DESC, - "updatetime"))); - AgentService agentService = null; - if (StringUtils.isNotBlank(agentUser.getAgentserviceid())) { - agentService = this.agentServiceRes.findById(agentUser.getAgentserviceid()).orElse(null); - view.addObject("curAgentService", agentService); - if (agentService != null) { - /** - * 获取关联数据 - */ - agentServiceProxy.processRelaData(logined.getId(), agentService, map); - } - } - if (MainContext.ChannelType.WEIXIN.toString().equals(agentUser.getChanneltype())) { - List passportWechatUserList = weiXinUserRes.findByOpenid(agentUser.getUserid()); - if (passportWechatUserList.size() > 0) { - PassportWechatUser passportWechatUser = passportWechatUserList.get(0); - view.addObject("weiXinUser", passportWechatUser); - } - } else if (MainContext.ChannelType.WEBIM.toString().equals(agentUser.getChanneltype())) { - PassportWebIMUser passportWebIMUser = onlineUserRes.findById(agentUser.getUserid()).orElse(null); - if (passportWebIMUser != null) { - if (passportWebIMUser.getLogintime() != null) { - if (MainContext.OnlineUserStatusEnum.OFFLINE.toString().equals(passportWebIMUser.getStatus())) { - passportWebIMUser.setBetweentime( - (int) (passportWebIMUser.getUpdatetime().getTime() - passportWebIMUser.getLogintime().getTime())); - } else { - passportWebIMUser.setBetweentime( - (int) (System.currentTimeMillis() - passportWebIMUser.getLogintime().getTime())); - } - } - view.addObject("onlineUser", passportWebIMUser); - } - } else if (MainContext.ChannelType.PHONE.toString().equals(agentUser.getChanneltype())) { - if (agentService != null && StringUtils.isNotBlank(agentService.getOwner())) { - StatusEvent statusEvent = this.statusEventRes.findById(agentService.getOwner()).orElse(null); - if (statusEvent != null) { - if (StringUtils.isNotBlank(statusEvent.getHostid())) { - view.addObject("pbxHost", pbxHostRes.findById(statusEvent.getHostid()).orElse(null)); - } - view.addObject("statusEvent", statusEvent); - } - } - } - - view.addObject("serviceCount", this.agentServiceRes - .countByUseridAndStatus(agentUser - .getUserid(), - MainContext.AgentUserStatusEnum.END - .toString())); - view.addObject("tagRelationList", tagRelationRes.findByUserid(agentUser.getUserid())); - } - - AgentService service = agentServiceRes.findById(agentUser.getAgentserviceid()).orElse(null); - if (service != null) { - view.addObject("tags", tagRes.findByTagtypeAndSkill(MainContext.ModelType.USER.toString(), service.getSkill())); - } - return view; - } - - @RequestMapping("/workorders/list") - @Menu(type = "apps", subtype = "workorderslist") - public ModelAndView workorderslist(HttpServletRequest request, String contactsid, ModelMap map) { - if (MainContext.hasModule(Constants.CSKEFU_MODULE_WORKORDERS) && StringUtils.isNotBlank(contactsid)) { - DataExchangeInterface dataExchange = (DataExchangeInterface) MainContext.getContext().getBean( - "workorders"); - if (dataExchange != null) { - map.addAttribute( - "workOrdersList", - dataExchange.getListDataById(contactsid, super.getUser(request).getId())); - } - map.addAttribute("contactsid", contactsid); - } - return request(super.createView("/apps/agent/workorders")); - } - - /** - * 设置为就绪,置闲 - * - * @param request - * @return - */ - @RequestMapping(value = "/ready") - @Menu(type = "apps", subtype = "agent") - public ModelAndView ready(HttpServletRequest request) { - final User logined = super.getUser(request); - final AgentStatus agentStatus = agentProxy.resolveAgentStatusByAgentno( - logined.getId(), logined.getSkills()); - - // 缓存就绪状态 - agentProxy.ready(logined, agentStatus, false); - - // 为该坐席分配访客 - acdAgentService.assignVisitors(agentStatus.getAgentno()); - acdWorkMonitor.recordAgentStatus(agentStatus.getAgentno(), - agentStatus.getUsername(), - agentStatus.getAgentno(), - logined.isAdmin(), // 0代表admin - agentStatus.getAgentno(), - MainContext.AgentStatusEnum.NOTREADY.toString(), - MainContext.AgentStatusEnum.READY.toString(), - MainContext.AgentWorkType.MEIDIACHAT.toString(), - null); - - return request(super.createView("/public/success")); - } - - /** - * 将一个已经就绪的坐席设置为未就绪的状态 - * 这个接口并不会重新分配坐席的现在的服务中的访客给其它坐席 - * - * @param request - * @return - */ - @RequestMapping(value = "/notready") - @Menu(type = "apps", subtype = "agent") - public ModelAndView notready(HttpServletRequest request) { - final User logined = super.getUser(request); - logger.info("[notready] set user {} as not ready", logined.getId()); - AgentStatus agentStatus = agentProxy.resolveAgentStatusByAgentno( - logined.getId(), logined.getSkills()); - - agentStatus.setBusy(false); - agentStatus.setUpdatetime(new Date()); - agentStatus.setStatus(MainContext.AgentStatusEnum.NOTREADY.toString()); - cache.putAgentStatus(agentStatus); - agentStatusRes.save(agentStatus); - - agentStatusProxy.broadcastAgentsStatus("agent", "notready", agentStatus.getAgentno()); - - acdWorkMonitor.recordAgentStatus(agentStatus.getAgentno(), - agentStatus.getUsername(), - agentStatus.getAgentno(), - logined.isAdmin(), // 0代表admin - agentStatus.getAgentno(), - MainContext.AgentStatusEnum.READY.toString(), - MainContext.AgentStatusEnum.NOTREADY.toString(), - MainContext.AgentWorkType.MEIDIACHAT.toString(), - null); - - return request(super.createView("/public/success")); - } - - /** - * 设置状态:就绪,置忙 - * - * @param request - * @return - */ - @RequestMapping(value = "/busy") - @Menu(type = "apps", subtype = "agent") - public ModelAndView busy(HttpServletRequest request) { - final User logined = super.getUser(request); - logger.info("[busy] set user {} as busy", logined.getId()); - AgentStatus agentStatus = agentProxy.resolveAgentStatusByAgentno( - logined.getId(), logined.getSkills()); - - agentStatus.setBusy(true); - acdWorkMonitor.recordAgentStatus( - agentStatus.getAgentno(), - agentStatus.getUsername(), - agentStatus.getAgentno(), - super.getUser(request).isAdmin(), - agentStatus.getAgentno(), - MainContext.AgentStatusEnum.NOTBUSY.toString(), - MainContext.AgentStatusEnum.BUSY.toString(), - MainContext.AgentWorkType.MEIDIACHAT.toString(), - agentStatus.getUpdatetime()); - agentStatus.setUpdatetime(new Date()); - cache.putAgentStatus(agentStatus); - agentStatusRes.save(agentStatus); - - agentStatusProxy.broadcastAgentsStatus("agent", "busy", logined.getId()); - - return request(super.createView("/public/success")); - } - - /** - * 设置状态:就绪,置闲 - * - * @param request - * @return - */ - @RequestMapping(value = "/notbusy") - @Menu(type = "apps", subtype = "agent") - public ModelAndView notbusy(HttpServletRequest request) { - final User logined = super.getUser(request); - // 组织结构和权限数据 - logger.info("[notbusy] set user {} as not busy", logined.getId()); - - AgentStatus agentStatus = agentProxy.resolveAgentStatusByAgentno( - logined.getId(), logined.getSkills()); - - // 设置为就绪,置闲 - agentStatus.setBusy(false); - agentStatus.setUpdatetime(new Date()); - agentStatus.setStatus(MainContext.AgentStatusEnum.READY.toString()); - - // 更新工作记录 - acdWorkMonitor.recordAgentStatus( - agentStatus.getAgentno(), - agentStatus.getUsername(), - agentStatus.getAgentno(), - super.getUser(request).isAdmin(), - agentStatus.getAgentno(), - MainContext.AgentStatusEnum.BUSY.toString(), - MainContext.AgentStatusEnum.NOTBUSY.toString(), - MainContext.AgentWorkType.MEIDIACHAT.toString(), - agentStatus.getUpdatetime()); - - // 更新数据库和缓存 - cache.putAgentStatus(agentStatus); - agentStatusRes.save(agentStatus); - - // 重新分配访客给坐席 - acdAgentService.assignVisitors(agentStatus.getAgentno()); - - return request(super.createView("/public/success")); - } - - @RequestMapping(value = "/clean") - @Menu(type = "apps", subtype = "clean") - public ModelAndView clean(HttpServletRequest request) throws Exception { - List agentUserList = agentUserRes.findByAgentnoAndStatus( - super.getUser(request).getId(), MainContext.AgentUserStatusEnum.END.toString()); - List agentServiceList = new ArrayList<>(); - for (AgentUser agentUser : agentUserList) { - if (agentUser != null && super.getUser(request).getId().equals(agentUser.getAgentno())) { - acdAgentService.finishAgentUser(agentUser); - AgentService agentService = agentServiceRes.findById(agentUser.getAgentserviceid()).orElse(null); - if (agentService != null) { - agentService.setStatus(MainContext.AgentUserStatusEnum.END.toString()); - agentServiceList.add(agentService); - } - } - } - agentServiceRes.saveAll(agentServiceList); - return request(super - .createView("redirect:/agent/index.html")); - } - - - /** - * 结束对话 - * 如果当前对话属于登录用户或登录用户为超级用户,则可以结束这个对话 - * - * @param request - * @param id - * @return - * @throws Exception - */ - @RequestMapping({"/end"}) - @Menu(type = "apps", subtype = "agent") - public ModelAndView end(HttpServletRequest request, @Valid String id) { - logger.info("[end] end id {}", id); - final User logined = super.getUser(request); - - final AgentUser agentUser = agentUserRes.findById(id).orElse(null); - - if (agentUser != null) { - if ((StringUtils.equals( - logined.getId(), agentUser.getAgentno()) || logined.isAdmin())) { - // 删除访客-坐席关联关系,包括缓存 - try { - acdAgentService.finishAgentUser(agentUser); - } catch (CSKefuException e) { - // 未能删除成功 - logger.error("[end]", e); - } - } else { - logger.info("[end] Permission not fulfill."); - } - } - - return request(super - .createView("redirect:/agent/index.html")); - } - - @RequestMapping({"/readmsg"}) - @Menu(type = "apps", subtype = "agent") - public ModelAndView readmsg(HttpServletRequest request, @Valid String userid) { - - AgentUserTask agentUserTask = agentUserTaskRes.findById(userid).orElse(null); - agentUserTask.setTokenum(0); - agentUserTaskRes.save(agentUserTask); - return request(super.createView("/public/success")); - } - - @RequestMapping({"/blacklist/add"}) - @Menu(type = "apps", subtype = "blacklist") - public ModelAndView blacklistadd(ModelMap map, HttpServletRequest request, @Valid String agentuserid, @Valid String agentserviceid, @Valid String userid) - throws Exception { - map.addAttribute("agentuserid", agentuserid); - map.addAttribute("agentserviceid", agentserviceid); - map.addAttribute("userid", userid); - map.addAttribute("agentUser", agentUserRes.findById(userid).orElse(null)); - return request(super.createView("/apps/agent/blacklistadd")); - } - - @RequestMapping({"/blacklist/save"}) - @Menu(type = "apps", subtype = "blacklist") - public ModelAndView blacklist( - HttpServletRequest request, - @Valid String agentuserid, - @Valid String agentserviceid, - @Valid String userid, - @Valid BlackEntity blackEntity) - throws Exception { - logger.info("[blacklist] userid {}", userid); - final User logined = super.getUser(request); - - if (StringUtils.isBlank(userid)) { - throw new CSKefuException("Invalid userid"); - } - /** - * 添加黑名单 - * 一定时间后触发函数 - */ - JSONObject payload = new JSONObject(); - - int timeSeconds = blackEntity.getControltime() * 3600; - payload.put("userId", userid); - ModelAndView view = end(request, agentuserid); - - // 更新或创建黑名单 - blackEntityProxy.updateOrCreateBlackEntity(blackEntity, logined, userid, agentserviceid, agentuserid); - - // 创建定时任务 取消拉黑 - brokerPublisher.send( - Constants.WEBIM_SOCKETIO_ONLINE_USER_BLACKLIST, payload.toJSONString(), false, timeSeconds); - - return view; - } - - @RequestMapping("/tagrelation") - @Menu(type = "apps", subtype = "tagrelation") - public ModelAndView tagrelation(ModelMap map, HttpServletRequest request, @Valid String userid, @Valid String tagid, @Valid String dataid) { - TagRelation tagRelation = tagRelationRes.findByUseridAndTagid(userid, tagid); - if (tagRelation == null) { - tagRelation = new TagRelation(); - tagRelation.setUserid(userid); - tagRelation.setTagid(tagid); - tagRelation.setDataid(dataid); - tagRelationRes.save(tagRelation); - } else { - tagRelationRes.delete(tagRelation); - } - return request(super.createView("/public/success")); - } - - /** - * 坐席聊天时发送图片和文件 - * - * @param map - * @param request - * @param multipart - * @param id - * @param paste 是否是粘贴到chatbox的图片事件,此时发送者还没有执行发送 - * @return - * @throws IOException - */ - @RequestMapping("/image/upload") - @Menu(type = "im", subtype = "image") - public ResponseEntity upload( - ModelMap map, - HttpServletRequest request, - @RequestParam(value = "imgFile", required = false) MultipartFile multipart, - @Valid String id, - @Valid boolean paste) throws IOException { - logger.info("[upload] image file, agentUser id {}, paste {}", id, paste); - final User logined = super.getUser(request); - - JSONObject result = new JSONObject(); - HttpHeaders headers = RestUtils.header(); - final AgentUser agentUser = agentUserRes.findById(id).orElse(null); - - if (multipart != null && multipart.getOriginalFilename().lastIndexOf(".") > 0) { - try { - StreamingFile sf = agentProxy.saveFileIntoMySQLBlob(logined, multipart); - // 发送通知 - if (!paste) { - agentProxy.sendFileMessageByAgent(logined, agentUser, multipart, sf); - } - result.put("error", 0); - result.put("url", sf.getFileUrl()); - } catch (CSKefuException e) { - result.put("error", 1); - result.put("message", "请选择文件"); - } - } else { - result.put("error", 1); - result.put("message", "请选择图片文件"); - } - return new ResponseEntity<>(result.toString(), headers, HttpStatus.OK); - } - - @RequestMapping("/message/image") - @Menu(type = "resouce", subtype = "image", access = true) - public ModelAndView messageimage(HttpServletResponse response, ModelMap map, @Valid String id, @Valid String t) throws IOException { - ChatMessage message = chatMessageRes.findById(id).orElse(null); - map.addAttribute("chatMessage", message); - map.addAttribute("agentUser", cache.findOneAgentUserByUserId(message.getUserid())); +package com.cskefu.cc.controller.apps; + +import com.alibaba.fastjson.JSONObject; +import com.cskefu.cc.acd.ACDAgentService; +import com.cskefu.cc.acd.ACDWorkMonitor; +import com.cskefu.cc.activemq.BrokerPublisher; +import com.cskefu.cc.basic.Constants; +import com.cskefu.cc.basic.MainContext; +import com.cskefu.cc.basic.MainUtils; +import com.cskefu.cc.cache.Cache; +import com.cskefu.cc.controller.Handler; +import com.cskefu.cc.util.restapi.RestUtils; +import com.cskefu.cc.exception.CSKefuException; +import com.cskefu.cc.model.*; +import com.cskefu.cc.peer.PeerSyncIM; +import com.cskefu.cc.persistence.blob.JpaBlobHelper; +import com.cskefu.cc.persistence.interfaces.DataExchangeInterface; +import com.cskefu.cc.persistence.repository.*; +import com.cskefu.cc.proxy.*; +import com.cskefu.cc.socketio.message.ChatMessage; +import com.cskefu.cc.socketio.message.Message; +import com.cskefu.cc.util.Menu; +import com.cskefu.cc.util.PinYinTools; +import com.cskefu.cc.util.PropertiesEventUtil; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Lazy; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; +import org.springframework.data.domain.Sort.Direction; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Controller; +import org.springframework.ui.ModelMap; +import org.springframework.util.FileCopyUtils; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.multipart.MultipartFile; +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.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Map; + +@Controller +@RequestMapping("/agent") +public class AgentController extends Handler { + + static final Logger logger = LoggerFactory.getLogger(AgentController.class); + + @Autowired + private ACDWorkMonitor acdWorkMonitor; + + @Autowired + private ACDAgentService acdAgentService; + + @Autowired + private ContactsRepository contactsRes; + + @Autowired + private PropertiesEventRepository propertiesEventRes; + + @Autowired + private AgentUserRepository agentUserRes; + + @Autowired + private AgentStatusRepository agentStatusRes; + + @Autowired + private AgentServiceRepository agentServiceRes; + + @Autowired + private PassportWebIMUserRepository onlineUserRes; + + @Autowired + private WeiXinUserRepository weiXinUserRes; + + @Autowired + private ServiceSummaryRepository serviceSummaryRes; + + @Autowired + private ChatMessageRepository chatMessageRes; + + @Autowired + private AgentProxy agentProxy; + + @Autowired + private TagRepository tagRes; + + @Autowired + private TagRelationRepository tagRelationRes; + + @Autowired + private AgentUserTaskRepository agentUserTaskRes; + + @Autowired + private UserRepository userRes; + + @Autowired + private StatusEventRepository statusEventRes; + + @Autowired + private AgentUserProxy agentUserProxy; + + @Autowired + private PbxHostRepository pbxHostRes; + + @Autowired + private AgentUserContactsRepository agentUserContactsRes; + + @Autowired + private StreamingFileRepository streamingFileRes; + + @Autowired + private JpaBlobHelper jpaBlobHelper; + + @Autowired + private BlackEntityProxy blackEntityProxy; + + @Autowired + private Cache cache; + + @Autowired + private AgentServiceProxy agentServiceProxy; + + @Value("${web.upload-path}") + private String webUploadPath; + + @Autowired + @Lazy + private PeerSyncIM peerSyncIM; + + @Autowired + private BrokerPublisher brokerPublisher; + + @Autowired + private AgentStatusProxy agentStatusProxy; + + @Autowired + private UserProxy userProxy; + + @Autowired + private OrganProxy organProxy; + + @Autowired + private OrganRepository organRes; + + @Autowired + private ChatbotRepository chatbotRes; + + /** + * 坐席从联系人列表进入坐席工作台和该联系人聊天 + * + * @param map + * @param request + * @param response + * @param sort + * @param channels 可立即触达的渠道 + * @param contactid + * @return + * @throws IOException + * @throws TemplateException + */ + @RequestMapping("/proactive") + @Menu(type = "apps", subtype = "agent") + public ModelAndView proactive( + ModelMap map, + HttpServletRequest request, + HttpServletResponse response, + @Valid String sort, + @Valid String channels, + @RequestParam(name = "contactid", required = false) String contactid) throws IOException, CSKefuException { + + if (StringUtils.isBlank(contactid)) { + logger.info("[chat] empty contactid, fast return error page."); + return request(super.createView("/public/error")); + } + + logger.info( + "[chat] contactid {}, channels {}", contactid, + channels); + + final User logined = super.getUser(request); + AgentUser agentUser = agentUserProxy.figureAgentUserBeforeChatWithContactInfo(channels, contactid, logined); + + if (agentUser != null) { + logger.info( + "[chat] resolved agentUser, figure view model data as index page, agentUserId {}, onlineUser Id {}, agentno {}, channel {}", + agentUser.getId(), agentUser.getUserid(), agentUser.getAgentno(), agentUser.getChanneltype()); + } else { + logger.info("[chat] can not resolve agentUser !!!"); + } + + // TODO 在agentUser没有得到的情况下,传回的前端信息增加提示,提示放在modelview中 + + // 处理原聊天数据 + ModelAndView view = request(super.createView("/apps/agent/index")); + agentUserProxy.buildIndexViewWithModels(view, map, request, response, sort, logined, agentUser); + return view; + } + + + /** + * 打开坐席工作台 + * + * @param map + * @param request + * @param response + * @param sort + * @return + * @throws IOException + * @throws TemplateException + */ + @RequestMapping("/index") + @Menu(type = "apps", subtype = "agent") + public ModelAndView index( + ModelMap map, + HttpServletRequest request, + HttpServletResponse response, + @Valid String sort, + boolean licenseVerifiedPass, + String licenseBillingMsg) throws IOException { + logger.info("[index] licenseVerifiedPass {}, licenseBillingMsg {}", licenseVerifiedPass, licenseBillingMsg); + final User logined = super.getUser(request); + ModelAndView view = request(super.createView("/apps/agent/index")); + agentUserProxy.buildIndexViewWithModels(view, map, request, response, sort, logined, null); + return view; + } + + @RequestMapping("/agentusers") + @Menu(type = "apps", subtype = "agent") + public ModelAndView agentusers(HttpServletRequest request, + String userid, + boolean licenseVerifiedPass, + String licenseBillingMsg) { + logger.info("[agentusers] userid {}, licenseVerifiedPass {}, licenseBillingMsg {}", userid, licenseVerifiedPass, licenseBillingMsg); + ModelAndView view = request(super.createView("/apps/agent/agentusers")); + User logined = super.getUser(request); + view.addObject( + "agentUserList", agentUserRes.findByAgentno(logined.getId(), + Sort.by(Direction.DESC, "status"))); + List agentUserList = agentUserRes.findByUserid(userid); + view.addObject( + "curagentuser", agentUserList != null && agentUserList.size() > 0 ? agentUserList.get(0) : null); + view.addObject("licenseVerifiedPass", licenseVerifiedPass); + view.addObject("licenseBillingMsg", licenseBillingMsg); + return view; + } + + @RequestMapping("/agentuserpage") + @Menu(type = "apps", subtype = "agent") + public ModelAndView agentuserpage( + ModelMap map, + HttpServletRequest request, + String id, + Integer page, + Integer current) throws IOException { + String mainagentuserconter = "/apps/agent/mainagentuserconter"; + ModelAndView view = request(super.createView(mainagentuserconter)); + AgentUser agentUser = agentUserRes.findById(id).orElse(null); + if (agentUser != null) { + view.addObject("curagentuser", agentUser); + view.addObject( + "agentUserMessageList", this.chatMessageRes.findByusession(agentUser.getUserid(), current)); + } + return view; + } + + @RequestMapping("/agentuserLabel") + @Menu(type = "apps", subtype = "agent") + public ModelAndView agentuserLabel( + ModelMap map, + HttpServletRequest request, + String iconid) throws IOException { + String mainagentuserconter = "/apps/agent/mainagentuserconter"; + ModelAndView view = request(super.createView(mainagentuserconter)); + ChatMessage labelid = this.chatMessageRes.findById(iconid).orElse(null); + if (labelid != null) { + if (labelid.isIslabel() == false) { + labelid.setIslabel(true); + } else { + labelid.setIslabel(false); + } + chatMessageRes.save(labelid); + } + return view; + } + + @RequestMapping("/agentusersearch") + @Menu(type = "apps", subtype = "agent") + public ModelAndView agentusersearch( + ModelMap map, + HttpServletRequest request, + String id, + String search, + String condition + ) throws IOException { + String mainagentuserconter = "/apps/agent/mainagentusersearch"; + ModelAndView view = request(super.createView(mainagentuserconter)); + AgentUser agentUser = agentUserRes.findById(id).orElse(null); + + if (agentUser != null) { + Page agentUserMessageList = null; + if (condition.equals("label")) { + agentUserMessageList = this.chatMessageRes.findByislabel( + agentUser.getUserid(), search, PageRequest.of(0, 9999, Direction.DESC, "updatetime")); + } else { + agentUserMessageList = this.chatMessageRes.findByUsessionAndMessageContaining( + agentUser.getUserid(), search, PageRequest.of(0, 9999, Direction.DESC, "updatetime")); + } + view.addObject("agentUserMessageList", agentUserMessageList); + } + return view; + } + + + @RequestMapping("/agentusersearchdetails") + @Menu(type = "apps", subtype = "agent") + public ModelAndView agentusersearchdetails( + ModelMap map, + HttpServletRequest request, + String id, + String createtime, + String thisid) throws IOException, ParseException { + String mainagentuserconter = "/apps/agent/mainagentuserconter"; + ModelAndView view = request(super.createView(mainagentuserconter)); + AgentUser agentUser = agentUserRes.findById(id).orElse(null); + if (agentUser != null) { + SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + Date date = formatter.parse(createtime); + view.addObject("agentusersearchdetails", thisid); + view.addObject( + "agentUserMessageList", this.chatMessageRes.findByCreatetime(agentUser.getUserid(), date)); + view.addObject( + "agentUserMessageListnum", this.chatMessageRes.countByUsessionAndCreatetimeGreaterThanEqual( + agentUser.getUserid(), date)); + } + return (view); + } + + @RequestMapping("/agentuser") + @Menu(type = "apps", subtype = "agent") + public ModelAndView agentuser( + ModelMap map, + HttpServletRequest request, + String id, + String channel) throws IOException { + // set default Value as WEBIM + String mainagentuser = "/apps/agent/mainagentuser"; + switch (MainContext.ChannelType.toValue(channel)) { + case MESSENGER: + mainagentuser = "/apps/agent/mainagentuser_messenger"; + break; + case PHONE: + mainagentuser = "/apps/agent/mainagentuser_callout"; + break; + case SKYPE: + mainagentuser = "/apps/agent/mainagentuser_skype"; + break; + } + + ModelAndView view = request(super.createView(mainagentuser)); + final User logined = super.getUser(request); + AgentUser agentUser = agentUserRes.findById(id).orElse(null); + + if (agentUser != null) { + view.addObject("curagentuser", agentUser); + + Chatbot c = chatbotRes.findBySnsAccountIdentifier(agentUser.getAppid()); + if (c != null) { + view.addObject("aisuggest", c.isAisuggest()); + } + + view.addObject("inviteData", OnlineUserProxy.consult(agentUser.getAppid())); + AgentUserTask agentUserTask = agentUserTaskRes.findById(id).orElse(null); + agentUserTask.setTokenum(0); + agentUserTaskRes.save(agentUserTask); + + if (StringUtils.isNotBlank(agentUser.getAgentserviceid())) { + List summarizes = this.serviceSummaryRes.findByAgentserviceid( + agentUser.getAgentserviceid()); + if (summarizes.size() > 0) { + view.addObject("summary", summarizes.get(0)); + } + } + + view.addObject( + "agentUserMessageList", + this.chatMessageRes.findByUsession(agentUser.getUserid(), + PageRequest.of(0, 20, Direction.DESC, + "updatetime"))); + AgentService agentService = null; + if (StringUtils.isNotBlank(agentUser.getAgentserviceid())) { + agentService = this.agentServiceRes.findById(agentUser.getAgentserviceid()).orElse(null); + view.addObject("curAgentService", agentService); + if (agentService != null) { + /** + * 获取关联数据 + */ + agentServiceProxy.processRelaData(logined.getId(), agentService, map); + } + } + if (MainContext.ChannelType.WEIXIN.toString().equals(agentUser.getChanneltype())) { + List passportWechatUserList = weiXinUserRes.findByOpenid(agentUser.getUserid()); + if (passportWechatUserList.size() > 0) { + PassportWechatUser passportWechatUser = passportWechatUserList.get(0); + view.addObject("weiXinUser", passportWechatUser); + } + } else if (MainContext.ChannelType.WEBIM.toString().equals(agentUser.getChanneltype())) { + PassportWebIMUser passportWebIMUser = onlineUserRes.findById(agentUser.getUserid()).orElse(null); + if (passportWebIMUser != null) { + if (passportWebIMUser.getLogintime() != null) { + if (MainContext.OnlineUserStatusEnum.OFFLINE.toString().equals(passportWebIMUser.getStatus())) { + passportWebIMUser.setBetweentime( + (int) (passportWebIMUser.getUpdatetime().getTime() - passportWebIMUser.getLogintime().getTime())); + } else { + passportWebIMUser.setBetweentime( + (int) (System.currentTimeMillis() - passportWebIMUser.getLogintime().getTime())); + } + } + view.addObject("onlineUser", passportWebIMUser); + } + } else if (MainContext.ChannelType.PHONE.toString().equals(agentUser.getChanneltype())) { + if (agentService != null && StringUtils.isNotBlank(agentService.getOwner())) { + StatusEvent statusEvent = this.statusEventRes.findById(agentService.getOwner()).orElse(null); + if (statusEvent != null) { + if (StringUtils.isNotBlank(statusEvent.getHostid())) { + view.addObject("pbxHost", pbxHostRes.findById(statusEvent.getHostid()).orElse(null)); + } + view.addObject("statusEvent", statusEvent); + } + } + } + + view.addObject("serviceCount", this.agentServiceRes + .countByUseridAndStatus(agentUser + .getUserid(), + MainContext.AgentUserStatusEnum.END + .toString())); + view.addObject("tagRelationList", tagRelationRes.findByUserid(agentUser.getUserid())); + } + + AgentService service = agentServiceRes.findById(agentUser.getAgentserviceid()).orElse(null); + if (service != null) { + view.addObject("tags", tagRes.findByTagtypeAndSkill(MainContext.ModelType.USER.toString(), service.getSkill())); + } + return view; + } + + @RequestMapping("/workorders/list") + @Menu(type = "apps", subtype = "workorderslist") + public ModelAndView workorderslist(HttpServletRequest request, String contactsid, ModelMap map) { + if (MainContext.hasModule(Constants.CSKEFU_MODULE_WORKORDERS) && StringUtils.isNotBlank(contactsid)) { + DataExchangeInterface dataExchange = (DataExchangeInterface) MainContext.getContext().getBean( + "workorders"); + if (dataExchange != null) { + map.addAttribute( + "workOrdersList", + dataExchange.getListDataById(contactsid, super.getUser(request).getId())); + } + map.addAttribute("contactsid", contactsid); + } + return request(super.createView("/apps/agent/workorders")); + } + + /** + * 设置为就绪,置闲 + * + * @param request + * @return + */ + @RequestMapping(value = "/ready") + @Menu(type = "apps", subtype = "agent") + public ModelAndView ready(HttpServletRequest request) { + final User logined = super.getUser(request); + final AgentStatus agentStatus = agentProxy.resolveAgentStatusByAgentno( + logined.getId(), logined.getSkills()); + + // 缓存就绪状态 + agentProxy.ready(logined, agentStatus, false); + + // 为该坐席分配访客 + acdAgentService.assignVisitors(agentStatus.getAgentno()); + acdWorkMonitor.recordAgentStatus(agentStatus.getAgentno(), + agentStatus.getUsername(), + agentStatus.getAgentno(), + logined.isAdmin(), // 0代表admin + agentStatus.getAgentno(), + MainContext.AgentStatusEnum.NOTREADY.toString(), + MainContext.AgentStatusEnum.READY.toString(), + MainContext.AgentWorkType.MEIDIACHAT.toString(), + null); + + return request(super.createView("/public/success")); + } + + /** + * 将一个已经就绪的坐席设置为未就绪的状态 + * 这个接口并不会重新分配坐席的现在的服务中的访客给其它坐席 + * + * @param request + * @return + */ + @RequestMapping(value = "/notready") + @Menu(type = "apps", subtype = "agent") + public ModelAndView notready(HttpServletRequest request) { + final User logined = super.getUser(request); + logger.info("[notready] set user {} as not ready", logined.getId()); + AgentStatus agentStatus = agentProxy.resolveAgentStatusByAgentno( + logined.getId(), logined.getSkills()); + + agentStatus.setBusy(false); + agentStatus.setUpdatetime(new Date()); + agentStatus.setStatus(MainContext.AgentStatusEnum.NOTREADY.toString()); + cache.putAgentStatus(agentStatus); + agentStatusRes.save(agentStatus); + + agentStatusProxy.broadcastAgentsStatus("agent", "notready", agentStatus.getAgentno()); + + acdWorkMonitor.recordAgentStatus(agentStatus.getAgentno(), + agentStatus.getUsername(), + agentStatus.getAgentno(), + logined.isAdmin(), // 0代表admin + agentStatus.getAgentno(), + MainContext.AgentStatusEnum.READY.toString(), + MainContext.AgentStatusEnum.NOTREADY.toString(), + MainContext.AgentWorkType.MEIDIACHAT.toString(), + null); + + return request(super.createView("/public/success")); + } + + /** + * 设置状态:就绪,置忙 + * + * @param request + * @return + */ + @RequestMapping(value = "/busy") + @Menu(type = "apps", subtype = "agent") + public ModelAndView busy(HttpServletRequest request) { + final User logined = super.getUser(request); + logger.info("[busy] set user {} as busy", logined.getId()); + AgentStatus agentStatus = agentProxy.resolveAgentStatusByAgentno( + logined.getId(), logined.getSkills()); + + agentStatus.setBusy(true); + acdWorkMonitor.recordAgentStatus( + agentStatus.getAgentno(), + agentStatus.getUsername(), + agentStatus.getAgentno(), + super.getUser(request).isAdmin(), + agentStatus.getAgentno(), + MainContext.AgentStatusEnum.NOTBUSY.toString(), + MainContext.AgentStatusEnum.BUSY.toString(), + MainContext.AgentWorkType.MEIDIACHAT.toString(), + agentStatus.getUpdatetime()); + agentStatus.setUpdatetime(new Date()); + cache.putAgentStatus(agentStatus); + agentStatusRes.save(agentStatus); + + agentStatusProxy.broadcastAgentsStatus("agent", "busy", logined.getId()); + + return request(super.createView("/public/success")); + } + + /** + * 设置状态:就绪,置闲 + * + * @param request + * @return + */ + @RequestMapping(value = "/notbusy") + @Menu(type = "apps", subtype = "agent") + public ModelAndView notbusy(HttpServletRequest request) { + final User logined = super.getUser(request); + // 组织结构和权限数据 + logger.info("[notbusy] set user {} as not busy", logined.getId()); + + AgentStatus agentStatus = agentProxy.resolveAgentStatusByAgentno( + logined.getId(), logined.getSkills()); + + // 设置为就绪,置闲 + agentStatus.setBusy(false); + agentStatus.setUpdatetime(new Date()); + agentStatus.setStatus(MainContext.AgentStatusEnum.READY.toString()); + + // 更新工作记录 + acdWorkMonitor.recordAgentStatus( + agentStatus.getAgentno(), + agentStatus.getUsername(), + agentStatus.getAgentno(), + super.getUser(request).isAdmin(), + agentStatus.getAgentno(), + MainContext.AgentStatusEnum.BUSY.toString(), + MainContext.AgentStatusEnum.NOTBUSY.toString(), + MainContext.AgentWorkType.MEIDIACHAT.toString(), + agentStatus.getUpdatetime()); + + // 更新数据库和缓存 + cache.putAgentStatus(agentStatus); + agentStatusRes.save(agentStatus); + + // 重新分配访客给坐席 + acdAgentService.assignVisitors(agentStatus.getAgentno()); + + return request(super.createView("/public/success")); + } + + @RequestMapping(value = "/clean") + @Menu(type = "apps", subtype = "clean") + public ModelAndView clean(HttpServletRequest request) throws Exception { + List agentUserList = agentUserRes.findByAgentnoAndStatus( + super.getUser(request).getId(), MainContext.AgentUserStatusEnum.END.toString()); + List agentServiceList = new ArrayList<>(); + for (AgentUser agentUser : agentUserList) { + if (agentUser != null && super.getUser(request).getId().equals(agentUser.getAgentno())) { + acdAgentService.finishAgentUser(agentUser); + AgentService agentService = agentServiceRes.findById(agentUser.getAgentserviceid()).orElse(null); + if (agentService != null) { + agentService.setStatus(MainContext.AgentUserStatusEnum.END.toString()); + agentServiceList.add(agentService); + } + } + } + agentServiceRes.saveAll(agentServiceList); + return request(super + .createView("redirect:/agent/index.html")); + } + + + /** + * 结束对话 + * 如果当前对话属于登录用户或登录用户为超级用户,则可以结束这个对话 + * + * @param request + * @param id + * @return + * @throws Exception + */ + @RequestMapping({"/end"}) + @Menu(type = "apps", subtype = "agent") + public ModelAndView end(HttpServletRequest request, @Valid String id) { + logger.info("[end] end id {}", id); + final User logined = super.getUser(request); + + final AgentUser agentUser = agentUserRes.findById(id).orElse(null); + + if (agentUser != null) { + if ((StringUtils.equals( + logined.getId(), agentUser.getAgentno()) || logined.isAdmin())) { + // 删除访客-坐席关联关系,包括缓存 + try { + acdAgentService.finishAgentUser(agentUser); + } catch (CSKefuException e) { + // 未能删除成功 + logger.error("[end]", e); + } + } else { + logger.info("[end] Permission not fulfill."); + } + } + + return request(super + .createView("redirect:/agent/index.html")); + } + + @RequestMapping({"/readmsg"}) + @Menu(type = "apps", subtype = "agent") + public ModelAndView readmsg(HttpServletRequest request, @Valid String userid) { + + AgentUserTask agentUserTask = agentUserTaskRes.findById(userid).orElse(null); + agentUserTask.setTokenum(0); + agentUserTaskRes.save(agentUserTask); + return request(super.createView("/public/success")); + } + + @RequestMapping({"/blacklist/add"}) + @Menu(type = "apps", subtype = "blacklist") + public ModelAndView blacklistadd(ModelMap map, HttpServletRequest request, @Valid String agentuserid, @Valid String agentserviceid, @Valid String userid) + throws Exception { + map.addAttribute("agentuserid", agentuserid); + map.addAttribute("agentserviceid", agentserviceid); + map.addAttribute("userid", userid); + map.addAttribute("agentUser", agentUserRes.findById(userid).orElse(null)); + return request(super.createView("/apps/agent/blacklistadd")); + } + + @RequestMapping({"/blacklist/save"}) + @Menu(type = "apps", subtype = "blacklist") + public ModelAndView blacklist( + HttpServletRequest request, + @Valid String agentuserid, + @Valid String agentserviceid, + @Valid String userid, + @Valid BlackEntity blackEntity) + throws Exception { + logger.info("[blacklist] userid {}", userid); + final User logined = super.getUser(request); + + if (StringUtils.isBlank(userid)) { + throw new CSKefuException("Invalid userid"); + } + /** + * 添加黑名单 + * 一定时间后触发函数 + */ + JSONObject payload = new JSONObject(); + + int timeSeconds = blackEntity.getControltime() * 3600; + payload.put("userId", userid); + ModelAndView view = end(request, agentuserid); + + // 更新或创建黑名单 + blackEntityProxy.updateOrCreateBlackEntity(blackEntity, logined, userid, agentserviceid, agentuserid); + + // 创建定时任务 取消拉黑 + brokerPublisher.send( + Constants.WEBIM_SOCKETIO_ONLINE_USER_BLACKLIST, payload.toJSONString(), false, timeSeconds); + + return view; + } + + @RequestMapping("/tagrelation") + @Menu(type = "apps", subtype = "tagrelation") + public ModelAndView tagrelation(ModelMap map, HttpServletRequest request, @Valid String userid, @Valid String tagid, @Valid String dataid) { + TagRelation tagRelation = tagRelationRes.findByUseridAndTagid(userid, tagid); + if (tagRelation == null) { + tagRelation = new TagRelation(); + tagRelation.setUserid(userid); + tagRelation.setTagid(tagid); + tagRelation.setDataid(dataid); + tagRelationRes.save(tagRelation); + } else { + tagRelationRes.delete(tagRelation); + } + return request(super.createView("/public/success")); + } + + /** + * 坐席聊天时发送图片和文件 + * + * @param map + * @param request + * @param multipart + * @param id + * @param paste 是否是粘贴到chatbox的图片事件,此时发送者还没有执行发送 + * @return + * @throws IOException + */ + @RequestMapping("/image/upload") + @Menu(type = "im", subtype = "image") + public ResponseEntity upload( + ModelMap map, + HttpServletRequest request, + @RequestParam(value = "imgFile", required = false) MultipartFile multipart, + @Valid String id, + @Valid boolean paste) throws IOException { + logger.info("[upload] image file, agentUser id {}, paste {}", id, paste); + final User logined = super.getUser(request); + + JSONObject result = new JSONObject(); + HttpHeaders headers = RestUtils.header(); + final AgentUser agentUser = agentUserRes.findById(id).orElse(null); + + if (multipart != null && multipart.getOriginalFilename().lastIndexOf(".") > 0) { + try { + StreamingFile sf = agentProxy.saveFileIntoMySQLBlob(logined, multipart); + // 发送通知 + if (!paste) { + agentProxy.sendFileMessageByAgent(logined, agentUser, multipart, sf); + } + result.put("error", 0); + result.put("url", sf.getFileUrl()); + } catch (CSKefuException e) { + result.put("error", 1); + result.put("message", "请选择文件"); + } + } else { + result.put("error", 1); + result.put("message", "请选择图片文件"); + } + return new ResponseEntity<>(result.toString(), headers, HttpStatus.OK); + } + + @RequestMapping("/message/image") + @Menu(type = "resouce", subtype = "image", access = true) + public ModelAndView messageimage(HttpServletResponse response, ModelMap map, @Valid String id, @Valid String t) throws IOException { + ChatMessage message = chatMessageRes.findById(id).orElse(null); + map.addAttribute("chatMessage", message); + map.addAttribute("agentUser", cache.findOneAgentUserByUserId(message.getUserid())); /*if(StringUtils.isNotBlank(t)){ map.addAttribute("t", t) ; }*/ - map.addAttribute("t", true); - return request(super.createView("/apps/agent/media/messageimage")); - } + map.addAttribute("t", true); + return request(super.createView("/apps/agent/media/messageimage")); + } - @RequestMapping("/message/image/upload") - @Menu(type = "im", subtype = "image") - public ModelAndView messageimage( - ModelMap map, - HttpServletRequest request, - @RequestParam(value = "image", required = false) MultipartFile image, - @Valid String id, - @Valid String userid, - @Valid String fileid) throws IOException { - logger.info("[messageimage] userid {}, chat message id {}, fileid {}", userid, id, fileid); - if (image != null && StringUtils.isNotBlank(fileid)) { - File tempFile = File.createTempFile(fileid, ".png"); - try { - // 创建临时图片文件 - if (!tempFile.getParentFile().exists()) { - tempFile.getParentFile().mkdirs(); - } - // 写入临时文件 - FileCopyUtils.copy(image.getBytes(), tempFile); - ChatMessage chatMessage = chatMessageRes.findById(id).orElse(null); - chatMessage.setCooperation(true); - chatMessageRes.save(chatMessage); + @RequestMapping("/message/image/upload") + @Menu(type = "im", subtype = "image") + public ModelAndView messageimage( + ModelMap map, + HttpServletRequest request, + @RequestParam(value = "image", required = false) MultipartFile image, + @Valid String id, + @Valid String userid, + @Valid String fileid) throws IOException { + logger.info("[messageimage] userid {}, chat message id {}, fileid {}", userid, id, fileid); + if (image != null && StringUtils.isNotBlank(fileid)) { + File tempFile = File.createTempFile(fileid, ".png"); + try { + // 创建临时图片文件 + if (!tempFile.getParentFile().exists()) { + tempFile.getParentFile().mkdirs(); + } + // 写入临时文件 + FileCopyUtils.copy(image.getBytes(), tempFile); + ChatMessage chatMessage = chatMessageRes.findById(id).orElse(null); + chatMessage.setCooperation(true); + chatMessageRes.save(chatMessage); - // 写入协作文件 - String fileName = "upload/" + fileid + "_cooperation"; - File imageFile = new File(webUploadPath, fileName); - MainUtils.scaleImage(imageFile, tempFile, 0.1F); + // 写入协作文件 + String fileName = "upload/" + fileid + "_cooperation"; + File imageFile = new File(webUploadPath, fileName); + MainUtils.scaleImage(imageFile, tempFile, 0.1F); - // 保存到数据库 - StreamingFile sf = streamingFileRes.findById(fileid).orElse(null); - if (sf != null) { - sf.setCooperation(jpaBlobHelper.createBlobWithFile(imageFile)); - streamingFileRes.save(sf); - } + // 保存到数据库 + StreamingFile sf = streamingFileRes.findById(fileid).orElse(null); + if (sf != null) { + sf.setCooperation(jpaBlobHelper.createBlobWithFile(imageFile)); + streamingFileRes.save(sf); + } - cache.findOneAgentUserByUserId( - chatMessage.getUserid()).ifPresent(p -> { - Message outMessage = new Message(); - outMessage.setMessage("/res/image.html?id=" + fileid + "&cooperation=true"); - outMessage.setFilename(imageFile.getName()); - outMessage.setAttachmentid(chatMessage.getAttachmentid()); - outMessage.setFilesize((int) imageFile.length()); - outMessage.setMessageType(MainContext.MediaType.ACTION.toString()); - outMessage.setCalltype(MainContext.CallType.INVITE.toString()); - outMessage.setCreatetime(Constants.DISPLAY_DATE_FORMATTER.format(new Date())); - outMessage.setAgentUser(p); + cache.findOneAgentUserByUserId( + chatMessage.getUserid()).ifPresent(p -> { + Message outMessage = new Message(); + outMessage.setMessage("/res/image.html?id=" + fileid + "&cooperation=true"); + outMessage.setFilename(imageFile.getName()); + outMessage.setAttachmentid(chatMessage.getAttachmentid()); + outMessage.setFilesize((int) imageFile.length()); + outMessage.setMessageType(MainContext.MediaType.ACTION.toString()); + outMessage.setCalltype(MainContext.CallType.INVITE.toString()); + outMessage.setCreatetime(Constants.DISPLAY_DATE_FORMATTER.format(new Date())); + outMessage.setAgentUser(p); - peerSyncIM.send( - MainContext.ReceiverType.VISITOR, - MainContext.ChannelType.toValue(p.getChanneltype()), - p.getAppid(), - MainContext.MessageType.MESSAGE, - p.getUserid(), - outMessage, - true); - }); - } finally { - if (tempFile.exists()) { - tempFile.delete(); - } - } - } - return request(super.createView("/public/success")); - } + peerSyncIM.send( + MainContext.ReceiverType.VISITOR, + MainContext.ChannelType.toValue(p.getChanneltype()), + p.getAppid(), + MainContext.MessageType.MESSAGE, + p.getUserid(), + outMessage, + true); + }); + } finally { + if (tempFile.exists()) { + tempFile.delete(); + } + } + } + return request(super.createView("/public/success")); + } - /** - * 坐席会话关联联系人 - * - * @param map - * @param request - * @param contactsid 联系人ID - * @param userid 访客ID - * @param agentserviceid 坐席服务ID - * @param agentuserid 坐席ID - * @return - */ - @RequestMapping(value = "/contacts") - @Menu(type = "apps", subtype = "contacts") - public ModelAndView contacts( - ModelMap map, - final HttpServletRequest request, - @Valid String contactsid, - @Valid String userid, - @Valid String agentserviceid, - @Valid String agentuserid) throws CSKefuException { - logger.info( - "[contacts] contactsid {}, userid {}, agentserviceid {}, agentuserid {}", contactsid, userid, - agentserviceid, agentuserid); + /** + * 坐席会话关联联系人 + * + * @param map + * @param request + * @param contactsid 联系人ID + * @param userid 访客ID + * @param agentserviceid 坐席服务ID + * @param agentuserid 坐席ID + * @return + */ + @RequestMapping(value = "/contacts") + @Menu(type = "apps", subtype = "contacts") + public ModelAndView contacts( + ModelMap map, + final HttpServletRequest request, + @Valid String contactsid, + @Valid String userid, + @Valid String agentserviceid, + @Valid String agentuserid) throws CSKefuException { + logger.info( + "[contacts] contactsid {}, userid {}, agentserviceid {}, agentuserid {}", contactsid, userid, + agentserviceid, agentuserid); - final User logined = super.getUser(request); + final User logined = super.getUser(request); - if (StringUtils.isNotBlank(userid) && StringUtils.isNotBlank(contactsid)) { + if (StringUtils.isNotBlank(userid) && StringUtils.isNotBlank(contactsid)) { - /** - * 获得联系人 - */ - Contacts contacts = contactsRes.findById(contactsid).orElse(null); - if (contacts != null) { - map.addAttribute("contacts", contacts); - } + /** + * 获得联系人 + */ + Contacts contacts = contactsRes.findById(contactsid).orElse(null); + if (contacts != null) { + map.addAttribute("contacts", contacts); + } - /** - * 在关联联系人后,更新AgentUser的显示的名字 - */ - AgentUser agentUser = agentUserRes.findById(agentuserid).orElse(null); - if (agentUser != null) { - agentUser.setUsername(contacts.getName()); - agentUser.setNickname(contacts.getName()); - agentUserRes.save(agentUser); - } + /** + * 在关联联系人后,更新AgentUser的显示的名字 + */ + AgentUser agentUser = agentUserRes.findById(agentuserid).orElse(null); + if (agentUser != null) { + agentUser.setUsername(contacts.getName()); + agentUser.setNickname(contacts.getName()); + agentUserRes.save(agentUser); + } - /** - * 更新OnlineUser - */ - PassportWebIMUser passportWebIMUser = onlineUserRes.findOneByUserid(userid); - if (passportWebIMUser != null) { - passportWebIMUser.setContactsid(contactsid); - passportWebIMUser.setUsername(contacts.getName()); - passportWebIMUser.setUpdateuser(logined.getUname()); - onlineUserRes.save(passportWebIMUser); - } + /** + * 更新OnlineUser + */ + PassportWebIMUser passportWebIMUser = onlineUserRes.findOneByUserid(userid); + if (passportWebIMUser != null) { + passportWebIMUser.setContactsid(contactsid); + passportWebIMUser.setUsername(contacts.getName()); + passportWebIMUser.setUpdateuser(logined.getUname()); + onlineUserRes.save(passportWebIMUser); + } - AgentService agentService = agentServiceRes.findById(agentserviceid).orElse(null); - if (agentService != null) { - agentService.setContactsid(contactsid); - agentService.setUsername(contacts.getName()); - agentServiceRes.save(agentService); + AgentService agentService = agentServiceRes.findById(agentserviceid).orElse(null); + if (agentService != null) { + agentService.setContactsid(contactsid); + agentService.setUsername(contacts.getName()); + agentServiceRes.save(agentService); - AgentUserContacts agentUserContacts = agentUserContactsRes.findOneByUserid( - userid).orElseGet(() -> { - AgentUserContacts p = new AgentUserContacts(); + AgentUserContacts agentUserContacts = agentUserContactsRes.findOneByUserid( + userid).orElseGet(() -> { + AgentUserContacts p = new AgentUserContacts(); - p.setUserid(userid); - p.setCreater(super.getUser(request).getId()); - p.setCreatetime(new Date()); - return p; - }); + p.setUserid(userid); + p.setCreater(super.getUser(request).getId()); + p.setCreatetime(new Date()); + return p; + }); - agentUserContacts.setContactsid(contactsid); - agentUserContacts.setAppid(agentService.getAppid()); - agentUserContacts.setChanneltype(agentService.getChanneltype()); + agentUserContacts.setContactsid(contactsid); + agentUserContacts.setAppid(agentService.getAppid()); + agentUserContacts.setChanneltype(agentService.getChanneltype()); - agentUserContactsRes.save(agentUserContacts); - } - } - return request(super.createView("/apps/agent/contacts")); - } + agentUserContactsRes.save(agentUserContacts); + } + } + return request(super.createView("/apps/agent/contacts")); + } - @RequestMapping(value = "/clean/associated") - @Menu(type = "apps", subtype = "cleanassociated") - public ModelAndView cleanAssociated(ModelMap map, HttpServletRequest request, final @RequestParam String currentAgentUserContactsId) { - String contactsid = null; - if (StringUtils.isNotEmpty(currentAgentUserContactsId)) { - AgentUserContacts agentUserContacts = agentUserContactsRes.findById(currentAgentUserContactsId).orElse(null); - if (agentUserContacts != null) { - agentUserContactsRes.delete(agentUserContacts); - } - } + @RequestMapping(value = "/clean/associated") + @Menu(type = "apps", subtype = "cleanassociated") + public ModelAndView cleanAssociated(ModelMap map, HttpServletRequest request, final @RequestParam String currentAgentUserContactsId) { + String contactsid = null; + if (StringUtils.isNotEmpty(currentAgentUserContactsId)) { + AgentUserContacts agentUserContacts = agentUserContactsRes.findById(currentAgentUserContactsId).orElse(null); + if (agentUserContacts != null) { + agentUserContactsRes.delete(agentUserContacts); + } + } - return request(super.createView("/apps/agent/contacts")); - } + return request(super.createView("/apps/agent/contacts")); + } - @ResponseBody - @RequestMapping(value = "/evaluation") - @Menu(type = "apps", subtype = "evaluation") - public String evaluation(HttpServletRequest request, @Valid String agentuserid) { - AgentUser agentUser = agentUserRes.findById(agentuserid).orElse(null); + @ResponseBody + @RequestMapping(value = "/evaluation") + @Menu(type = "apps", subtype = "evaluation") + public String evaluation(HttpServletRequest request, @Valid String agentuserid) { + AgentUser agentUser = agentUserRes.findById(agentuserid).orElse(null); - Message outMessage = new Message(); - outMessage.setChannelMessage(agentUser); - outMessage.setAgentUser(agentUser); + Message outMessage = new Message(); + outMessage.setChannelMessage(agentUser); + outMessage.setAgentUser(agentUser); - peerSyncIM.send( - MainContext.ReceiverType.VISITOR, - MainContext.ChannelType.toValue(agentUser.getChanneltype()), - agentUser.getAppid(), - MainContext.MessageType.SATISFACTION, - agentUser.getUserid(), - outMessage, - true); + peerSyncIM.send( + MainContext.ReceiverType.VISITOR, + MainContext.ChannelType.toValue(agentUser.getChanneltype()), + agentUser.getAppid(), + MainContext.MessageType.SATISFACTION, + agentUser.getUserid(), + outMessage, + true); - return "ok"; - } + return "ok"; + } - @RequestMapping(value = "/summary") - @Menu(type = "apps", subtype = "summary") - public ModelAndView summary( - ModelMap map, - HttpServletRequest request, - @Valid String userid, - @Valid String agentserviceid, - @Valid String agentuserid, - @Valid String channel) { - if (StringUtils.isNotBlank(userid) && StringUtils.isNotBlank(agentuserid)) { - AgentUser agentUser = this.agentUserRes.findById(agentuserid).orElse(null); - if (agentUser != null && StringUtils.isNotBlank(agentUser.getAgentserviceid())) { - List summaries = this.serviceSummaryRes.findByAgentserviceid( - agentUser.getAgentserviceid()); - if (summaries.size() > 0) { - map.addAttribute("summary", summaries.get(0)); - } - } - Organ currentOrgan = super.getOrgan(request); - if(null!=currentOrgan){ - map.addAttribute( - "tags", tagRes.findByTagtypeAndSkill( - MainContext.ModelType.CCSUMMARY.toString(), currentOrgan.getParent())); - } - - - map.addAttribute("userid", userid); - map.addAttribute("agentserviceid", agentserviceid); - map.addAttribute("agentuserid", agentuserid); - map.addAttribute("channel", channel); - } - return request(super.createView("/apps/agent/summary")); - } - - @RequestMapping(value = "/summary/save") - @Menu(type = "apps", subtype = "summarysave") - public ModelAndView summarysave( - ModelMap map, - HttpServletRequest request, - @Valid AgentServiceSummary summary, - @Valid String contactsid, - @Valid String userid, - @Valid String agentserviceid, - @Valid String agentuserid, - @Valid String channel) { - if (StringUtils.isNotBlank(userid) && StringUtils.isNotBlank(agentuserid)) { - summary.setCreater(super.getUser(request).getId()); - summary.setCreatetime(new Date()); - AgentService service = agentServiceRes.findById(agentserviceid).orElse(null); - summary.setAgent(service.getAgentno()); - summary.setAgentno(service.getAgentno()); - summary.setSkill(service.getSkill()); - summary.setUsername(service.getUsername()); - summary.setAgentusername(service.getAgentusername()); - summary.setChannel(service.getChanneltype()); - summary.setContactsid(contactsid); - summary.setLogindate(service.getLogindate()); - summary.setContactsid(service.getContactsid()); - summary.setEmail(service.getEmail()); - summary.setPhonenumber(service.getPhone()); - serviceSummaryRes.save(summary); - } - - return request(super.createView( - "redirect:/agent/agentuser.html?id=" + agentuserid + "&channel=" + channel)); - } - - /** - * 坐席转接窗口 - * - * @param map - * @param request - * @param userid - * @param agentserviceid - * @param agentuserid - * @return - */ - @RequestMapping(value = "/transfer") - @Menu(type = "apps", subtype = "transfer") - public ModelAndView transfer( - ModelMap map, - final HttpServletRequest request, - final @Valid String userid, - final @Valid String agentserviceid, - final @Valid String agentuserid) { - logger.info("[transfer] userId {}, agentUser {}", userid, agentuserid); - final User logined = super.getUser(request); - - Organ targetOrgan = super.getOrgan(request); - Map ownOrgans = organProxy.findAllOrganByParent(targetOrgan); - - if (StringUtils.isNotBlank(userid) && StringUtils.isNotBlank(agentuserid)) { - // 列出所有技能组 - List skillGroups = organRes.findByIdInAndSkill(ownOrgans.keySet(), true); - - // 选择当前用户的默认技能组 - AgentService agentService = agentServiceRes.findById(agentserviceid).orElse(null); - - String currentOrgan = agentService.getSkill(); - - if (StringUtils.isBlank(currentOrgan)) { - if (!skillGroups.isEmpty()) { - currentOrgan = skillGroups.get(0).getId(); - } - } - logger.info("[transfer] set current organ as {}", currentOrgan); - // 列出所有在线的坐席,排除本身 - List userids = new ArrayList<>(); - final Map agentStatusMap = cache.findAllReadyAgentStatus(); - - for (final String o : agentStatusMap.keySet()) { - if (!StringUtils.equals(o, logined.getId())) { - userids.add(o); - } - } - - logger.info("[transfer] get all userids except mine, {}", StringUtils.join(userids, "\t")); - - final List userList = userRes.findAllById(userids); - for (final User o : userList) { - o.setAgentStatus(agentStatusMap.get(o.getId())); - // find user's skills - userProxy.attachOrgansPropertiesForUser(o); - } - - map.addAttribute("userList", userList); - map.addAttribute("userid", userid); - map.addAttribute("agentserviceid", agentserviceid); - map.addAttribute("agentuserid", agentuserid); - map.addAttribute("skillGroups", skillGroups); - map.addAttribute("agentno", agentService.getAgentno()); - map.addAttribute("agentservice", this.agentServiceRes.findById(agentserviceid).orElse(null)); - map.addAttribute("currentorgan", currentOrgan); - } - - return request(super.createView("/apps/agent/transfer")); - } - - /** - * 查找一个组织机构中的在线坐席 - * - * @param map - * @param request - * @param organ - * @return - */ - @RequestMapping(value = "/transfer/agent") - @Menu(type = "apps", subtype = "transferagent") - public ModelAndView transferagent( - ModelMap map, - HttpServletRequest request, - @Valid String organ, - @Valid String agentid) { - final User logined = super.getUser(request); - if (StringUtils.isNotBlank(organ)) { - List userids = new ArrayList<>(); - - final Map agentStatusMap = cache.findAllReadyAgentStatus(); - - for (final String o : agentStatusMap.keySet()) { - if (!StringUtils.equals(o, agentid)) { - userids.add(o); - } - } - - final List userList = userRes.findAllById(userids); - for (final User o : userList) { - o.setAgentStatus(agentStatusMap.get(o.getId())); - // find user's skills - userProxy.attachOrgansPropertiesForUser(o); - } - map.addAttribute("userList", userList); - map.addAttribute("currentorgan", organ); - } - return request(super.createView("/apps/agent/transferagentlist")); - } + @RequestMapping(value = "/summary") + @Menu(type = "apps", subtype = "summary") + public ModelAndView summary( + ModelMap map, + HttpServletRequest request, + @Valid String userid, + @Valid String agentserviceid, + @Valid String agentuserid, + @Valid String channel) { + if (StringUtils.isNotBlank(userid) && StringUtils.isNotBlank(agentuserid)) { + AgentUser agentUser = this.agentUserRes.findById(agentuserid).orElse(null); + if (agentUser != null && StringUtils.isNotBlank(agentUser.getAgentserviceid())) { + List summaries = this.serviceSummaryRes.findByAgentserviceid( + agentUser.getAgentserviceid()); + if (summaries.size() > 0) { + map.addAttribute("summary", summaries.get(0)); + } + } + Organ currentOrgan = super.getOrgan(request); + if (null != currentOrgan) { + map.addAttribute( + "tags", tagRes.findByTagtypeAndSkill( + MainContext.ModelType.CCSUMMARY.toString(), currentOrgan.getParent())); + } - @RequestMapping("/calloutcontact/add") - @Menu(type = "apps", subtype = "calloutcontact", admin = true) - public ModelAndView add(ModelMap map, HttpServletRequest request, @Valid String ckind) { - map.addAttribute("ckind", ckind); - return request(super.createView("/apps/agent/calloutcontact/add")); - } + map.addAttribute("userid", userid); + map.addAttribute("agentserviceid", agentserviceid); + map.addAttribute("agentuserid", agentuserid); + map.addAttribute("channel", channel); + } + return request(super.createView("/apps/agent/summary")); + } - @RequestMapping(value = "/calloutcontact/save") - @Menu(type = "apps", subtype = "calloutcontact") - public ModelAndView calloutcontactsave( - ModelMap map, - HttpServletRequest request, - @RequestParam(value = "agentuser") String agentuser, - @Valid Contacts contacts) throws CSKefuException { - logger.info("[agent ctrl] calloutcontactsave agentuser [{}]", agentuser); - AgentUser au = agentUserRes.findById(agentuser).orElse(null); - if (au == null) { - throw new CSKefuException("不存在该服务记录"); - } + @RequestMapping(value = "/summary/save") + @Menu(type = "apps", subtype = "summarysave") + public ModelAndView summarysave( + ModelMap map, + HttpServletRequest request, + @Valid AgentServiceSummary summary, + @Valid String contactsid, + @Valid String userid, + @Valid String agentserviceid, + @Valid String agentuserid, + @Valid String channel) { + if (StringUtils.isNotBlank(userid) && StringUtils.isNotBlank(agentuserid)) { + summary.setCreater(super.getUser(request).getId()); + summary.setCreatetime(new Date()); + AgentService service = agentServiceRes.findById(agentserviceid).orElse(null); + summary.setAgent(service.getAgentno()); + summary.setAgentno(service.getAgentno()); + summary.setSkill(service.getSkill()); + summary.setUsername(service.getUsername()); + summary.setAgentusername(service.getAgentusername()); + summary.setChannel(service.getChanneltype()); + summary.setContactsid(contactsid); + summary.setLogindate(service.getLogindate()); + summary.setContactsid(service.getContactsid()); + summary.setEmail(service.getEmail()); + summary.setPhonenumber(service.getPhone()); + serviceSummaryRes.save(summary); + } - User logined = super.getUser(request); - contacts.setId(MainUtils.getUUID()); - contacts.setCreater(logined.getId()); - contacts.setPinyin(PinYinTools.getInstance().getFirstPinYin(contacts.getName())); - if (StringUtils.isBlank(contacts.getCusbirthday())) { - contacts.setCusbirthday(null); - } - contactsRes.save(contacts); + return request(super.createView( + "redirect:/agent/agentuser.html?id=" + agentuserid + "&channel=" + channel)); + } - AgentUserContacts auc = new AgentUserContacts(); - auc.setId(MainUtils.getUUID()); - auc.setUsername(au.getUsername()); - auc.setUserid(au.getUserid()); - auc.setContactsid(contacts.getId()); - auc.setChanneltype(au.getChanneltype()); - auc.setCreatetime(new Date()); - auc.setAppid(au.getAppid()); - auc.setCreater(logined.getId()); - agentUserContactsRes.save(auc); - return request(super.createView("redirect:/agent/index.html")); - } + /** + * 坐席转接窗口 + * + * @param map + * @param request + * @param userid + * @param agentserviceid + * @param agentuserid + * @return + */ + @RequestMapping(value = "/transfer") + @Menu(type = "apps", subtype = "transfer") + public ModelAndView transfer( + ModelMap map, + final HttpServletRequest request, + final @Valid String userid, + final @Valid String agentserviceid, + final @Valid String agentuserid) { + logger.info("[transfer] userId {}, agentUser {}", userid, agentuserid); + final User logined = super.getUser(request); - @RequestMapping("/calloutcontact/update") - @Menu(type = "apps", subtype = "calloutcontact") - public ModelAndView update(HttpServletRequest request, @Valid Contacts contacts) { - Contacts data = contactsRes.findById(contacts.getId()).orElse(null); - if (data != null) { - List events = PropertiesEventUtil.processPropertiesModify( - request, contacts, data, "id", "creater", "createtime", "updatetime"); //记录 数据变更 历史 - if (events.size() > 0) { - String modifyid = MainUtils.getUUID(); - Date modifytime = new Date(); - for (PropertiesEvent event : events) { - event.setDataid(contacts.getId()); - event.setCreater(super.getUser(request).getId()); - event.setModifyid(modifyid); - event.setCreatetime(modifytime); - propertiesEventRes.save(event); - } - } + Organ targetOrgan = super.getOrgan(request); + Map ownOrgans = organProxy.findAllOrganByParent(targetOrgan); - contacts.setCreater(data.getCreater()); - contacts.setCreatetime(data.getCreatetime()); - contacts.setPinyin(PinYinTools.getInstance().getFirstPinYin(contacts.getName())); - if (StringUtils.isBlank(contacts.getCusbirthday())) { - contacts.setCusbirthday(null); - } - contactsRes.save(contacts); - } + if (StringUtils.isNotBlank(userid) && StringUtils.isNotBlank(agentuserid)) { + // 列出所有技能组 + List skillGroups = organRes.findByIdInAndSkill(ownOrgans.keySet(), true); - return request(super.createView("redirect:/agent/index.html")); - } - } + // 选择当前用户的默认技能组 + AgentService agentService = agentServiceRes.findById(agentserviceid).orElse(null); + + String currentOrgan = agentService.getSkill(); + + if (StringUtils.isBlank(currentOrgan)) { + if (!skillGroups.isEmpty()) { + currentOrgan = skillGroups.get(0).getId(); + } + } + logger.info("[transfer] set current organ as {}", currentOrgan); + // 列出所有在线的坐席,排除本身 + List userids = new ArrayList<>(); + final Map agentStatusMap = cache.findAllReadyAgentStatus(); + + for (final String o : agentStatusMap.keySet()) { + if (!StringUtils.equals(o, logined.getId())) { + userids.add(o); + } + } + + logger.info("[transfer] get all userids except mine, {}", StringUtils.join(userids, "\t")); + + final List userList = userRes.findAllById(userids); + for (final User o : userList) { + o.setAgentStatus(agentStatusMap.get(o.getId())); + // find user's skills + userProxy.attachOrgansPropertiesForUser(o); + } + + map.addAttribute("userList", userList); + map.addAttribute("userid", userid); + map.addAttribute("agentserviceid", agentserviceid); + map.addAttribute("agentuserid", agentuserid); + map.addAttribute("skillGroups", skillGroups); + map.addAttribute("agentno", agentService.getAgentno()); + map.addAttribute("agentservice", this.agentServiceRes.findById(agentserviceid).orElse(null)); + map.addAttribute("currentorgan", currentOrgan); + } + + return request(super.createView("/apps/agent/transfer")); + } + + /** + * 查找一个组织机构中的在线坐席 + * + * @param map + * @param request + * @param organ + * @return + */ + @RequestMapping(value = "/transfer/agent") + @Menu(type = "apps", subtype = "transferagent") + public ModelAndView transferagent( + ModelMap map, + HttpServletRequest request, + @Valid String organ, + @Valid String agentid) { + final User logined = super.getUser(request); + if (StringUtils.isNotBlank(organ)) { + List userids = new ArrayList<>(); + + final Map agentStatusMap = cache.findAllReadyAgentStatus(); + + for (final String o : agentStatusMap.keySet()) { + if (!StringUtils.equals(o, agentid)) { + userids.add(o); + } + } + + final List userList = userRes.findAllById(userids); + for (final User o : userList) { + o.setAgentStatus(agentStatusMap.get(o.getId())); + // find user's skills + userProxy.attachOrgansPropertiesForUser(o); + } + map.addAttribute("userList", userList); + map.addAttribute("currentorgan", organ); + } + return request(super.createView("/apps/agent/transferagentlist")); + } + + + @RequestMapping("/calloutcontact/add") + @Menu(type = "apps", subtype = "calloutcontact", admin = true) + public ModelAndView add(ModelMap map, HttpServletRequest request, @Valid String ckind) { + map.addAttribute("ckind", ckind); + return request(super.createView("/apps/agent/calloutcontact/add")); + } + + @RequestMapping(value = "/calloutcontact/save") + @Menu(type = "apps", subtype = "calloutcontact") + public ModelAndView calloutcontactsave( + ModelMap map, + HttpServletRequest request, + @RequestParam(value = "agentuser") String agentuser, + @Valid Contacts contacts) throws CSKefuException { + logger.info("[agent ctrl] calloutcontactsave agentuser [{}]", agentuser); + AgentUser au = agentUserRes.findById(agentuser).orElse(null); + if (au == null) { + throw new CSKefuException("不存在该服务记录"); + } + + User logined = super.getUser(request); + contacts.setId(MainUtils.getUUID()); + contacts.setCreater(logined.getId()); + contacts.setPinyin(PinYinTools.getInstance().getFirstPinYin(contacts.getName())); + if (StringUtils.isBlank(contacts.getCusbirthday())) { + contacts.setCusbirthday(null); + } + contactsRes.save(contacts); + + AgentUserContacts auc = new AgentUserContacts(); + auc.setId(MainUtils.getUUID()); + auc.setUsername(au.getUsername()); + auc.setUserid(au.getUserid()); + auc.setContactsid(contacts.getId()); + auc.setChanneltype(au.getChanneltype()); + auc.setCreatetime(new Date()); + auc.setAppid(au.getAppid()); + auc.setCreater(logined.getId()); + agentUserContactsRes.save(auc); + return request(super.createView("redirect:/agent/index.html")); + } + + @RequestMapping("/calloutcontact/update") + @Menu(type = "apps", subtype = "calloutcontact") + public ModelAndView update(HttpServletRequest request, @Valid Contacts contacts) { + Contacts data = contactsRes.findById(contacts.getId()).orElse(null); + if (data != null) { + List events = PropertiesEventUtil.processPropertiesModify( + request, contacts, data, "id", "creater", "createtime", "updatetime"); //记录 数据变更 历史 + if (events.size() > 0) { + String modifyid = MainUtils.getUUID(); + Date modifytime = new Date(); + for (PropertiesEvent event : events) { + event.setDataid(contacts.getId()); + event.setCreater(super.getUser(request).getId()); + event.setModifyid(modifyid); + event.setCreatetime(modifytime); + propertiesEventRes.save(event); + } + } + + contacts.setCreater(data.getCreater()); + contacts.setCreatetime(data.getCreatetime()); + contacts.setPinyin(PinYinTools.getInstance().getFirstPinYin(contacts.getName())); + if (StringUtils.isBlank(contacts.getCusbirthday())) { + contacts.setCusbirthday(null); + } + contactsRes.save(contacts); + } + + return request(super.createView("redirect:/agent/index.html")); + } +} diff --git a/contact-center/app/src/main/java/com/cskefu/cc/controller/apps/ContactsController.java b/contact-center/app/src/main/java/com/cskefu/cc/controller/apps/ContactsController.java index ad9f4401..4b1c761d 100644 --- a/contact-center/app/src/main/java/com/cskefu/cc/controller/apps/ContactsController.java +++ b/contact-center/app/src/main/java/com/cskefu/cc/controller/apps/ContactsController.java @@ -1,21 +1,22 @@ /* - * Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd. - * , Licensed under the Chunsong Public + * Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd. + * , Licensed under the Chunsong Public * License, Version 1.0 (the "License"), https://docs.cskefu.com/licenses/v1.html * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. - * Copyright (C) 2018- Jun. 2023 Chatopera Inc, , Licensed under the Apache License, Version 2.0, + * Copyright (C) 2018- Jun. 2023 Chatopera Inc, , Licensed under the Apache License, Version 2.0, * http://www.apache.org/licenses/LICENSE-2.0 - * Copyright (C) 2017 优客服-多渠道客服系统, Licensed under the Apache License, Version 2.0, + * Copyright (C) 2017 优客服-多渠道客服系统, Licensed under the Apache License, Version 2.0, * http://www.apache.org/licenses/LICENSE-2.0 */ package com.cskefu.cc.controller.apps; import com.cskefu.cc.basic.MainUtils; import com.cskefu.cc.controller.Handler; +import com.cskefu.cc.exception.BillingQuotaException; import com.cskefu.cc.exception.CSKefuException; import com.cskefu.cc.model.*; import com.cskefu.cc.persistence.repository.*; @@ -47,8 +48,10 @@ import org.springframework.web.servlet.ModelAndView; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import jakarta.validation.Valid; + import java.io.File; import java.io.IOException; +import java.lang.reflect.UndeclaredThrowableException; import java.text.SimpleDateFormat; import java.util.*; @@ -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)); } diff --git a/contact-center/app/src/main/java/com/cskefu/cc/controller/apps/IMController.java b/contact-center/app/src/main/java/com/cskefu/cc/controller/apps/IMController.java index e6afc0a4..9ea62a55 100644 --- a/contact-center/app/src/main/java/com/cskefu/cc/controller/apps/IMController.java +++ b/contact-center/app/src/main/java/com/cskefu/cc/controller/apps/IMController.java @@ -117,7 +117,7 @@ public class IMController extends Handler { private LeaveMsgRepository leaveMsgRes; @Autowired - private AgentUserRepository agentUserRepository; + private AgentUserRepository agentUserRes; @Autowired private AttachmentRepository attachementRes; @@ -822,7 +822,7 @@ public class IMController extends Handler { Contacts contacts1 = contactsRes.findOneByWluidAndWlsidAndWlcidAndDatastatus( uid, sid, cid, false); if (contacts1 != null) { - agentUserRepository.findOneByUserid(userid).ifPresent(p -> { + agentUserRes.findOneByUserid(userid).ifPresent(p -> { // 关联AgentService的联系人 if (StringUtils.isNotBlank(p.getAgentserviceid())) { AgentService agentService = agentServiceRepository.findById(p.getAgentserviceid()).orElse(null); diff --git a/contact-center/app/src/main/java/com/cskefu/cc/controller/apps/service/AgentSummaryController.java b/contact-center/app/src/main/java/com/cskefu/cc/controller/apps/service/AgentSummaryController.java index 2231dea7..6bda5541 100644 --- a/contact-center/app/src/main/java/com/cskefu/cc/controller/apps/service/AgentSummaryController.java +++ b/contact-center/app/src/main/java/com/cskefu/cc/controller/apps/service/AgentSummaryController.java @@ -31,7 +31,6 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Sort; -import org.springframework.data.jpa.domain.Specification; import org.springframework.stereotype.Controller; import org.springframework.ui.ModelMap; import org.springframework.web.bind.annotation.RequestMapping; diff --git a/contact-center/app/src/main/java/com/cskefu/cc/controller/apps/service/ChatServiceController.java b/contact-center/app/src/main/java/com/cskefu/cc/controller/apps/service/ChatServiceController.java index f8b3d030..86bbdf66 100644 --- a/contact-center/app/src/main/java/com/cskefu/cc/controller/apps/service/ChatServiceController.java +++ b/contact-center/app/src/main/java/com/cskefu/cc/controller/apps/service/ChatServiceController.java @@ -77,9 +77,6 @@ public class ChatServiceController extends Handler { @Autowired private AgentStatusRepository agentStatusRepository; - @Autowired - private AgentUserRepository agentUserRepository; - @Autowired private LeaveMsgRepository leaveMsgRes; @@ -233,7 +230,7 @@ public class ChatServiceController extends Handler { if (agentUser != null) { agentUser.setAgentno(agentno); agentUser.setAgentname(targetAgent.getUname()); - agentUserRepository.save(agentUser); + agentUserRes.save(agentUser); if (MainContext.AgentUserStatusEnum.INSERVICE.toString().equals( agentUser.getStatus())) { // 转接 , 发送消息给 目标坐席 @@ -288,11 +285,11 @@ public class ChatServiceController extends Handler { } } } else { - agentUser = agentUserRepository.findById(agentService.getAgentuserid()).orElse(null); + agentUser = agentUserRes.findById(agentService.getAgentuserid()).orElse(null); if (agentUser != null) { agentUser.setAgentno(agentno); agentUser.setAgentname(targetAgent.getUname()); - agentUserRepository.save(agentUser); + agentUserRes.save(agentUser); } } @@ -317,7 +314,7 @@ public class ChatServiceController extends Handler { AgentService agentService = agentServiceRes.findById(id).orElse(null); if (agentService != null) { User user = super.getUser(request); - AgentUser agentUser = agentUserRepository.findById(agentService.getAgentuserid()).orElse(null); + AgentUser agentUser = agentUserRes.findById(agentService.getAgentuserid()).orElse(null); if (agentUser != null) { acdAgentService.finishAgentUser(agentUser); } diff --git a/contact-center/app/src/main/java/com/cskefu/cc/controller/apps/service/ProcessedSummaryController.java b/contact-center/app/src/main/java/com/cskefu/cc/controller/apps/service/ProcessedSummaryController.java index 51602e96..d436af3b 100644 --- a/contact-center/app/src/main/java/com/cskefu/cc/controller/apps/service/ProcessedSummaryController.java +++ b/contact-center/app/src/main/java/com/cskefu/cc/controller/apps/service/ProcessedSummaryController.java @@ -31,7 +31,6 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Sort; -import org.springframework.data.jpa.domain.Specification; import org.springframework.stereotype.Controller; import org.springframework.ui.ModelMap; import org.springframework.web.bind.annotation.RequestMapping; diff --git a/contact-center/app/src/main/java/com/cskefu/cc/exception/BillingQuotaException.java b/contact-center/app/src/main/java/com/cskefu/cc/exception/BillingQuotaException.java new file mode 100644 index 00000000..77f91df5 --- /dev/null +++ b/contact-center/app/src/main/java/com/cskefu/cc/exception/BillingQuotaException.java @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd. + * , Licensed under the Chunsong Public + * License, Version 1.0 (the "License"), https://docs.cskefu.com/licenses/v1.html + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * Copyright (C) 2019-Jun. 2023 Chatopera Inc, , + * Licensed under the Apache License, Version 2.0, + * http://www.apache.org/licenses/LICENSE-2.0 + */ +package com.cskefu.cc.exception; + +public class BillingQuotaException extends Exception { + + public final static String SUFFIX = "billingquotaexception."; + + // metakv 中没有找到可以支持配额的证书类型 + public final static String NO_LICENSE_FOUND = SUFFIX + "no_license_found"; + // 返回值异常,可能是网络连接不上,稍后再试 + public static final String RESPONSE_UNEXPECTED = SUFFIX + "response_unexpected"; + // 请求参数不合法 + public static final String INVALID_REQUEST_BODY = SUFFIX + "invalid_request_body"; + // 证书在证书商店不存在 + public static final String LICENSE_INVALID = SUFFIX + "license_invalid"; + // 证书关联的产品信息不合法 + public static final String PRODUCT_INVALID = SUFFIX + "product_invalid"; + // 证书失效或耗尽 + public static final String LICENSE_EXPIRED_OR_EXHAUSTED = SUFFIX + "license_expired_or_exhausted"; + // 证书关闭了对该 serverinst 的支持 + public static final String LICENSE_DISABLED_SERVERINST = SUFFIX + "license_disabled_serverinst"; + // 证书不支持配额回退 + public static final String LICENSE_UNSUPPORT_REFUND = SUFFIX + "license_unsupport_refund"; + // 证书配额余量不足,不能完成本次请求 + public static final String LICENSE_QUOTA_INADEQUATE = SUFFIX + "license_quota_inadequate"; + // 内部错误,不应该发生 + public static final String INTERNAL_ERROR = SUFFIX + "internal_error"; + + public BillingQuotaException(final String s) { + super(s); + } +} diff --git a/contact-center/app/src/main/java/com/cskefu/cc/exception/BillingResourceException.java b/contact-center/app/src/main/java/com/cskefu/cc/exception/BillingResourceException.java new file mode 100644 index 00000000..e7e38f30 --- /dev/null +++ b/contact-center/app/src/main/java/com/cskefu/cc/exception/BillingResourceException.java @@ -0,0 +1,20 @@ +/* + * Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd. + * , Licensed under the Chunsong Public + * License, Version 1.0 (the "License"), https://docs.cskefu.com/licenses/v1.html + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * Copyright (C) 2019-Jun. 2023 Chatopera Inc, , + * Licensed under the Apache License, Version 2.0, + * http://www.apache.org/licenses/LICENSE-2.0 + */ +package com.cskefu.cc.exception; + +public class BillingResourceException extends Exception{ + public BillingResourceException(final String s){ + super(s); + } +} diff --git a/contact-center/app/src/main/java/com/cskefu/cc/exception/LicenseNotFoundException.java b/contact-center/app/src/main/java/com/cskefu/cc/exception/LicenseNotFoundException.java new file mode 100644 index 00000000..90da2580 --- /dev/null +++ b/contact-center/app/src/main/java/com/cskefu/cc/exception/LicenseNotFoundException.java @@ -0,0 +1,20 @@ +/* + * Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd. + * , Licensed under the Chunsong Public + * License, Version 1.0 (the "License"), https://docs.cskefu.com/licenses/v1.html + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * Copyright (C) 2019-Jun. 2023 Chatopera Inc, , + * Licensed under the Apache License, Version 2.0, + * http://www.apache.org/licenses/LICENSE-2.0 + */ +package com.cskefu.cc.exception; + +public class LicenseNotFoundException extends Exception{ + public LicenseNotFoundException(final String s){ + super(s); + } +} diff --git a/contact-center/app/src/main/java/com/cskefu/cc/exception/MetaKvInvalidKeyException.java b/contact-center/app/src/main/java/com/cskefu/cc/exception/MetaKvInvalidKeyException.java new file mode 100644 index 00000000..fafbd11a --- /dev/null +++ b/contact-center/app/src/main/java/com/cskefu/cc/exception/MetaKvInvalidKeyException.java @@ -0,0 +1,20 @@ +/* + * Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd. + * , Licensed under the Chunsong Public + * License, Version 1.0 (the "License"), https://docs.cskefu.com/licenses/v1.html + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * Copyright (C) 2019-Jun. 2023 Chatopera Inc, , + * Licensed under the Apache License, Version 2.0, + * http://www.apache.org/licenses/LICENSE-2.0 + */ +package com.cskefu.cc.exception; + +public class MetaKvInvalidKeyException extends Exception{ + public MetaKvInvalidKeyException(final String s){ + super(s); + } +} diff --git a/contact-center/app/src/main/java/com/cskefu/cc/exception/MetaKvNotExistException.java b/contact-center/app/src/main/java/com/cskefu/cc/exception/MetaKvNotExistException.java new file mode 100644 index 00000000..dd9e3e07 --- /dev/null +++ b/contact-center/app/src/main/java/com/cskefu/cc/exception/MetaKvNotExistException.java @@ -0,0 +1,20 @@ +/* + * Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd. + * , Licensed under the Chunsong Public + * License, Version 1.0 (the "License"), https://docs.cskefu.com/licenses/v1.html + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * Copyright (C) 2019-Jun. 2023 Chatopera Inc, , + * Licensed under the Apache License, Version 2.0, + * http://www.apache.org/licenses/LICENSE-2.0 + */ +package com.cskefu.cc.exception; + +public class MetaKvNotExistException extends Exception{ + public MetaKvNotExistException(final String s){ + super(s); + } +} diff --git a/contact-center/app/src/main/java/com/cskefu/cc/model/AgentUser.java b/contact-center/app/src/main/java/com/cskefu/cc/model/AgentUser.java index c30ad35e..07371d25 100644 --- a/contact-center/app/src/main/java/com/cskefu/cc/model/AgentUser.java +++ b/contact-center/app/src/main/java/com/cskefu/cc/model/AgentUser.java @@ -1,15 +1,15 @@ /* - * Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd. - * , Licensed under the Chunsong Public + * Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd. + * , Licensed under the Chunsong Public * License, Version 1.0 (the "License"), https://docs.cskefu.com/licenses/v1.html * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. - * Copyright (C) 2018- Jun. 2023 Chatopera Inc, , Licensed under the Apache License, Version 2.0, + * Copyright (C) 2018- Jun. 2023 Chatopera Inc, , Licensed under the Apache License, Version 2.0, * http://www.apache.org/licenses/LICENSE-2.0 - * Copyright (C) 2017 优客服-多渠道客服系统, Licensed under the Apache License, Version 2.0, + * Copyright (C) 2017 优客服-多渠道客服系统, Licensed under the Apache License, Version 2.0, * http://www.apache.org/licenses/LICENSE-2.0 */ package com.cskefu.cc.model; @@ -19,6 +19,7 @@ import org.hibernate.annotations.GenericGenerator; import org.hibernate.annotations.Proxy; import jakarta.persistence.*; + import java.io.Serializable; import java.util.Date; @@ -105,6 +106,7 @@ public class AgentUser implements Serializable, Comparable { @Transient private boolean tip = false; + @Transient private boolean agentTip = false; @@ -119,11 +121,25 @@ public class AgentUser implements Serializable, Comparable { @Transient private boolean fromhis = false; + @Transient private boolean online = false; + @Transient private boolean disconnect = false; + /** + * 证书验证通过 + */ + @Transient + private boolean licenseVerifiedPass = true; + + /** + * 证书验证提示信息 + */ + @Transient + private String licenseBillingMsg; + public AgentUser() { } @@ -617,4 +633,22 @@ public class AgentUser implements Serializable, Comparable { public void setAgentname(String agentname) { this.agentname = agentname; } + + @Transient + public boolean isLicenseVerifiedPass() { + return licenseVerifiedPass; + } + + public void setLicenseVerifiedPass(boolean licenseVerifiedPass) { + this.licenseVerifiedPass = licenseVerifiedPass; + } + + @Transient + public String getLicenseBillingMsg() { + return licenseBillingMsg; + } + + public void setLicenseBillingMsg(String licenseBillingMsg) { + this.licenseBillingMsg = licenseBillingMsg; + } } diff --git a/contact-center/app/src/main/java/com/cskefu/cc/model/ExecuteResult.java b/contact-center/app/src/main/java/com/cskefu/cc/model/ExecuteResult.java new file mode 100644 index 00000000..adadc46d --- /dev/null +++ b/contact-center/app/src/main/java/com/cskefu/cc/model/ExecuteResult.java @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd. + * , Licensed under the Chunsong Public + * License, Version 1.0 (the "License"), https://docs.cskefu.com/licenses/v1.html + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.cskefu.cc.model; + +import org.json.JSONObject; + +import java.io.Serializable; + +public class ExecuteResult implements Serializable { + public final static int RC_SUCC = 0; + public final static int RC_ERR1 = 1; + public final static int RC_ERR2 = 2; + public final static int RC_ERR3 = 3; + public final static int RC_ERR4 = 4; + public final static int RC_ERR5 = 5; + public final static int RC_ERR6 = 6; + public final static int RC_ERR7 = 7; + public final static int RC_ERR8 = 8; + public final static int RC_ERR9 = 9; + + private int rc; // 0 for success, errors other + private String error; + private String msg; + private JSONObject data; + + + public ExecuteResult() { + + } + + public ExecuteResult(final int rc, final String msg) { + this.rc = rc; + this.msg = msg; + } + + public ExecuteResult(final int rc, final String msg, final String error) { + this.rc = rc; + this.msg = msg; + this.error = error; + } + + public ExecuteResult(final int rc, + final String msg, + final String error, + final JSONObject data) { + this.rc = rc; + this.msg = msg; + this.error = error; + this.data = data; + } + + public int getRc() { + return rc; + } + + public void setRc(int rc) { + this.rc = rc; + } + + public String getError() { + return error; + } + + public void setError(String error) { + this.error = error; + } + + public String getMsg() { + return msg; + } + + public void setMsg(String msg) { + this.msg = msg; + } + + public JSONObject getData() { + return data; + } + + public void setData(JSONObject data) { + this.data = data; + } +} diff --git a/contact-center/app/src/main/java/com/cskefu/cc/model/MetaKv.java b/contact-center/app/src/main/java/com/cskefu/cc/model/MetaKv.java new file mode 100644 index 00000000..0de0916e --- /dev/null +++ b/contact-center/app/src/main/java/com/cskefu/cc/model/MetaKv.java @@ -0,0 +1,77 @@ +package com.cskefu.cc.model; + +import jakarta.persistence.*; + +import java.util.Date; + +/** + * 存储元数据 + */ +@Entity +@Table(name = "cs_metakv") +@org.hibernate.annotations.Proxy(lazy = false) +public class MetaKv implements java.io.Serializable { + + @Id + private String metakey; + + private String metavalue; + + private String datatype; + + private String comment; + + @Temporal(TemporalType.TIMESTAMP) + private Date updatetime; + + @Temporal(TemporalType.TIMESTAMP) + private Date createtime; + + public String getMetakey() { + return metakey; + } + + public void setMetakey(String metakey) { + this.metakey = metakey; + } + + public String getMetavalue() { + return metavalue; + } + + public void setMetavalue(String metavalue) { + this.metavalue = metavalue; + } + + public String getDatatype() { + return datatype; + } + + public void setDatatype(String datatype) { + this.datatype = datatype; + } + + public String getComment() { + return comment; + } + + public void setComment(String comment) { + this.comment = comment; + } + + public Date getUpdatetime() { + return updatetime; + } + + public void setUpdatetime(Date updatetime) { + this.updatetime = updatetime; + } + + public Date getCreatetime() { + return createtime; + } + + public void setCreatetime(Date createtime) { + this.createtime = createtime; + } +} diff --git a/contact-center/app/src/main/java/com/cskefu/cc/persistence/repository/MetaKvRepository.java b/contact-center/app/src/main/java/com/cskefu/cc/persistence/repository/MetaKvRepository.java new file mode 100644 index 00000000..f180c902 --- /dev/null +++ b/contact-center/app/src/main/java/com/cskefu/cc/persistence/repository/MetaKvRepository.java @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd. + * , Licensed under the Chunsong Public + * License, Version 1.0 (the "License"), https://docs.cskefu.com/licenses/v1.html + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.cskefu.cc.persistence.repository; + +import com.cskefu.cc.model.MetaKv; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface MetaKvRepository extends JpaRepository { + + Optional findFirstByMetakey(final String p1); + +} diff --git a/contact-center/app/src/main/java/com/cskefu/cc/persistence/repository/MetadataRepository.java b/contact-center/app/src/main/java/com/cskefu/cc/persistence/repository/MetadataRepository.java index 6deb23ad..58349d63 100644 --- a/contact-center/app/src/main/java/com/cskefu/cc/persistence/repository/MetadataRepository.java +++ b/contact-center/app/src/main/java/com/cskefu/cc/persistence/repository/MetadataRepository.java @@ -1,33 +1,33 @@ -/* - * Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd. - * , Licensed under the Chunsong Public - * License, Version 1.0 (the "License"), https://docs.cskefu.com/licenses/v1.html - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * Copyright (C) 2018- Jun. 2023 Chatopera Inc, , Licensed under the Apache License, Version 2.0, - * http://www.apache.org/licenses/LICENSE-2.0 - * Copyright (C) 2017 优客服-多渠道客服系统, Licensed under the Apache License, Version 2.0, - * http://www.apache.org/licenses/LICENSE-2.0 - */ -package com.cskefu.cc.persistence.repository; - -import com.cskefu.cc.model.MetadataTable; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; -import org.springframework.data.jpa.repository.JpaRepository; - -import java.util.List; - -public interface MetadataRepository extends JpaRepository{ - - MetadataTable findByTablename(String tablename); - - Page findAll(Pageable paramPageable); - - int countByTablename(String tableName) ; - - List findAll(); -} +/* + * Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd. + * , Licensed under the Chunsong Public + * License, Version 1.0 (the "License"), https://docs.cskefu.com/licenses/v1.html + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * Copyright (C) 2018- Jun. 2023 Chatopera Inc, , Licensed under the Apache License, Version 2.0, + * http://www.apache.org/licenses/LICENSE-2.0 + * Copyright (C) 2017 优客服-多渠道客服系统, Licensed under the Apache License, Version 2.0, + * http://www.apache.org/licenses/LICENSE-2.0 + */ +package com.cskefu.cc.persistence.repository; + +import com.cskefu.cc.model.MetadataTable; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface MetadataRepository extends JpaRepository{ + + MetadataTable findByTablename(String tablename); + + Page findAll(Pageable paramPageable); + + int countByTablename(String tableName) ; + + List findAll(); +} diff --git a/contact-center/app/src/main/java/com/cskefu/cc/plugins/chatbot/ChatbotEventHandler.java b/contact-center/app/src/main/java/com/cskefu/cc/plugins/chatbot/ChatbotEventHandler.java index ef54e3a2..1ef1de9d 100644 --- a/contact-center/app/src/main/java/com/cskefu/cc/plugins/chatbot/ChatbotEventHandler.java +++ b/contact-center/app/src/main/java/com/cskefu/cc/plugins/chatbot/ChatbotEventHandler.java @@ -1,371 +1,371 @@ -/* - * Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd. - * , Licensed under the Chunsong Public - * License, Version 1.0 (the "License"), https://docs.cskefu.com/licenses/v1.html - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * Copyright (C) 2019-Jun. 2023 Chatopera Inc, , - * Licensed under the Apache License, Version 2.0, - * http://www.apache.org/licenses/LICENSE-2.0 - */ -package com.cskefu.cc.plugins.chatbot; - -import com.chatopera.bot.sdk.Response; -import com.corundumstudio.socketio.AckRequest; -import com.corundumstudio.socketio.SocketIOClient; -import com.corundumstudio.socketio.SocketIOServer; -import com.corundumstudio.socketio.annotation.OnConnect; -import com.corundumstudio.socketio.annotation.OnDisconnect; -import com.corundumstudio.socketio.annotation.OnEvent; -import com.cskefu.cc.acd.ACDServiceRouter; -import com.cskefu.cc.basic.Constants; -import com.cskefu.cc.basic.MainContext; -import com.cskefu.cc.basic.MainUtils; -import com.cskefu.cc.model.*; -import com.cskefu.cc.persistence.repository.AgentUserRepository; -import com.cskefu.cc.persistence.repository.ChatbotRepository; -import com.cskefu.cc.persistence.repository.PassportWebIMUserRepository; -import com.cskefu.cc.proxy.OnlineUserProxy; -import com.cskefu.cc.socketio.client.NettyClients; -import com.cskefu.cc.socketio.message.AgentStatusMessage; -import com.cskefu.cc.socketio.message.ChatMessage; -import com.cskefu.cc.socketio.message.Message; -import com.cskefu.cc.socketio.util.IMServiceUtils; -import com.cskefu.cc.util.IP; -import com.cskefu.cc.util.IPTools; -import org.apache.commons.lang3.StringUtils; -import org.json.JSONObject; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; - -import java.net.InetSocketAddress; -import java.util.Date; - -public class ChatbotEventHandler { - private static final Logger logger = LoggerFactory.getLogger(ChatbotEventHandler.class); - - protected final SocketIOServer server; - - private static AgentUserRepository agentUserRes; - private static PassportWebIMUserRepository onlineUserRes; - private static ChatbotRepository chatbotRes; - private static ChatbotProxy chatbotProxy; - - @Autowired - public ChatbotEventHandler(SocketIOServer server) { - this.server = server; - } - - @OnConnect - public void onConnect(SocketIOClient client) { - try { - String user = client.getHandshakeData().getSingleUrlParam("userid"); - String nickname = client.getHandshakeData().getSingleUrlParam("nickname"); - String session = MainUtils.getContextID(client.getHandshakeData().getSingleUrlParam("session")); - String appid = client.getHandshakeData().getSingleUrlParam("appid"); - String aiid = client.getHandshakeData().getSingleUrlParam("aiid"); - logger.info( - "[onConnect] userid {}, nickname {}, session {}, appid {}, aiid {}", user, nickname, session, appid, - aiid); - - client.set("aiid", aiid); - client.set("session", session); - client.set("userid", user); - client.set("appid", appid); - - Date now = new Date(); - - if (StringUtils.isNotBlank(user)) { - /** - * 加入到 缓存列表 - */ - NettyClients.getInstance().putChatbotEventClient(user, client); - CousultInvite invite = OnlineUserProxy.consult(appid); - - /** - * 更新坐席服务类型 - */ - IMServiceUtils.shiftOpsType(user, MainContext.OptType.CHATBOT); - - // send out tip - Message tip = new Message(); - tip.setMessage("您正在使用机器人客服!"); - tip.setMessageType(MainContext.MessageType.MESSAGE.toString()); - tip.setCalltype(MainContext.CallType.IN.toString()); - tip.setCreatetime(MainUtils.dateFormate.format(now)); - - client.sendEvent(MainContext.MessageType.STATUS.toString(), tip); - - // send out welcome message - if (invite != null) { - Chatbot chatbot = getChatbotRes().findById(invite.getAiid()).orElse(null); - com.chatopera.bot.sdk.Chatbot bot = new com.chatopera.bot.sdk.Chatbot( - chatbot.getClientId(), chatbot.getSecret(), chatbot.getBaseUrl()); - Response result = bot.command("GET", "/"); - - // 发送欢迎语 - if (result.getRc() == 0) { - JSONObject details = (JSONObject) result.getData(); - ChatMessage welcome = new ChatMessage(); - String welcomeTextMessage = details.getString("welcome"); - if (StringUtils.isNotBlank(welcomeTextMessage)) { - welcome.setCalltype(MainContext.CallType.OUT.toString()); - welcome.setAppid(appid); - welcome.setAiid(aiid); - welcome.setMessage(welcomeTextMessage); - welcome.setTouser(user); - welcome.setMsgtype(MainContext.MessageType.MESSAGE.toString()); - welcome.setUserid(user); - welcome.setUsername(invite.getAiname()); - welcome.setUpdatetime(System.currentTimeMillis()); - client.sendEvent(MainContext.MessageType.MESSAGE.toString(), welcome); - } - - // 发送常见问题列表 - JSONObject faqhotresp = bot.conversation(user, "__faq_hot_list"); - logger.info("faqhot {}", faqhotresp.toString()); - if (faqhotresp.getInt("rc") == 0) { - JSONObject faqhotdata = faqhotresp.getJSONObject("data"); - if ((!faqhotdata.getBoolean("logic_is_fallback")) && - faqhotdata.has("string") && - faqhotdata.has("params")) { - ChatMessage faqhotmsg = new ChatMessage(); - faqhotmsg.setCalltype(MainContext.CallType.OUT.toString()); - faqhotmsg.setAppid(appid); - faqhotmsg.setAiid(aiid); - faqhotmsg.setMessage(faqhotdata.getString("string")); - faqhotmsg.setExpmsg(faqhotdata.getJSONArray("params").toString()); - faqhotmsg.setTouser(user); - faqhotmsg.setMsgtype(MainContext.MessageType.MESSAGE.toString()); - faqhotmsg.setUserid(user); - faqhotmsg.setUsername(invite.getAiname()); - faqhotmsg.setUpdatetime(System.currentTimeMillis()); - client.sendEvent(MainContext.MessageType.MESSAGE.toString(), faqhotmsg); - } - } - } else if (result.getRc() == 999 || result.getRc() == 998) { - logger.error("[chat] chatbot agent response rc {}, error {}", result.getRc(), result.getError()); - } else { - logger.error("[chat] chatbot agent response rc {}, error {}", result.getRc(), result.getError()); - } - } - - InetSocketAddress address = (InetSocketAddress) client.getRemoteAddress(); - String ip = MainUtils.getIpAddr(client.getHandshakeData().getHttpHeaders(), address.getHostString()); - PassportWebIMUser passportWebIMUser = getOnlineUserRes().findById(user).orElse(null); - - if (passportWebIMUser == null) { - passportWebIMUser = new PassportWebIMUser(); - passportWebIMUser.setAppid(appid); - if (StringUtils.isNotBlank(nickname)) { - passportWebIMUser.setUsername(nickname); - } else { - passportWebIMUser.setUsername(Constants.GUEST_USER + "_" + MainUtils.genIDByKey(user)); - } - - passportWebIMUser.setSessionid(session); - passportWebIMUser.setOptype(MainContext.OptType.CHATBOT.toString()); - passportWebIMUser.setUserid(user); - passportWebIMUser.setId(user); - passportWebIMUser.setChannel(MainContext.ChannelType.WEBIM.toString()); - passportWebIMUser.setIp(ip); - passportWebIMUser.setUpdatetime(now); - passportWebIMUser.setLogintime(now); - passportWebIMUser.setCreatetime(now); - IP ipdata = IPTools.getInstance().findGeography(ip); - passportWebIMUser.setCity(ipdata.getCity()); - passportWebIMUser.setCountry(ipdata.getCountry()); - passportWebIMUser.setProvince(ipdata.getProvince()); - passportWebIMUser.setIsp(ipdata.getIsp()); - passportWebIMUser.setRegion(ipdata.getRegion()); - passportWebIMUser.setStatus(MainContext.OnlineUserStatusEnum.ONLINE.toString()); - } - - // 在线客服访客咨询记录 - AgentUser agentUser = new AgentUser( - passportWebIMUser.getId(), - MainContext.ChannelType.WEBIM.toString(), // callout - passportWebIMUser.getId(), - passportWebIMUser.getUsername(), - appid); - - agentUser.setServicetime(now); - agentUser.setCreatetime(now); - agentUser.setUpdatetime(now); - agentUser.setSessionid(session); - agentUser.setRegion(passportWebIMUser.getRegion()); - - // 聊天机器人处理的请求 - agentUser.setOpttype(MainContext.OptType.CHATBOT.toString()); - agentUser.setAgentno(aiid); // 聊天机器人ID - agentUser.setAgentname(invite != null ? invite.getAiname() : "机器人客服"); - agentUser.setCity(passportWebIMUser.getCity()); - agentUser.setProvince(passportWebIMUser.getProvince()); - agentUser.setCountry(passportWebIMUser.getCountry()); - AgentService agentService = ACDServiceRouter.getAcdChatbotService().processChatbotService( - invite != null ? invite.getAiname() : "机器人客服", agentUser); - agentUser.setAgentserviceid(agentService.getId()); - - // 标记为机器人坐席 - agentUser.setChatbotops(true); - - // 保存到MySQL - getAgentUserRes().save(agentUser); - getOnlineUserRes().save(passportWebIMUser); - } - } catch (Exception e) { - logger.info("[onConnect] error", e); - } - } - - // 添加 @OnDisconnect 事件,客户端断开连接时调用,刷新客户端信息 - @OnDisconnect - public void onDisconnect(SocketIOClient client) { - String user = client.getHandshakeData().getSingleUrlParam("userid"); - if (StringUtils.isNotBlank(user)) { - NettyClients.getInstance().removeChatbotEventClient( - user, MainUtils.getContextID(client.getSessionId().toString())); - PassportWebIMUser passportWebIMUser = MainContext.getCache().findOneOnlineUserByUserId(user); - - MainContext.getCache().findOneAgentUserByUserId(user).ifPresent(p -> { - ACDServiceRouter.getAcdChatbotService().processChatbotService(null, p); - - MainContext.getCache().deleteAgentUserByUserId(p); - MainContext.getCache().deleteOnlineUserById(user); - - p.setStatus(MainContext.AgentUserStatusEnum.END.toString()); - passportWebIMUser.setStatus(MainContext.OnlineUserStatusEnum.OFFLINE.toString()); - - getAgentUserRes().save(p); - getOnlineUserRes().save(passportWebIMUser); - }); - } - client.disconnect(); - } - - // 消息接收入口,网站有新用户接入对话 - @OnEvent(value = "new") - public void onEvent(SocketIOClient client, AckRequest request, Message data) { - - } - - // 消息接收入口,坐席状态更新 - @OnEvent(value = "agentstatus") - public void onEvent(SocketIOClient client, AckRequest request, AgentStatusMessage data) { - logger.info("[onEvent] agentstatus: ", data.getMessage()); - } - - // 消息接收入口,收发消息,用户向机器人发送消息 - @OnEvent(value = "message") - public void onEvent(SocketIOClient client, AckRequest request, ChatMessage data) { - String aiid = client.get("aiid"); - String user = client.get("userid"); - String sessionid = client.get("session"); - String appid = client.get("appid"); - logger.info( - "[onEvent] message: session {}, aiid {}, userid {}, dataType {}, appid {}", sessionid, aiid, - user, data.getType(), appid); - - // ignore event if dataType is not message. - if (!StringUtils.equals(data.getType(), Constants.IM_MESSAGE_TYPE_MESSAGE)) { - return; - } - - MainContext.getCache().findOneAgentUserByUserId(user).ifPresent(p -> { - /** - * 以下代码主要用于检查 访客端的字数限制 - */ - CousultInvite invite = OnlineUserProxy.consult(data.getAppid()); - // ignore event if no invite found. - if (invite == null) { - return; - } - - // ignore if Chatbot is turnoff. - if (!invite.isAi()) { - return; - } - - Date now = new Date(); - if (invite.getMaxwordsnum() > 0) { - if (StringUtils.isNotBlank(data.getMessage()) && data.getMessage().length() > invite.getMaxwordsnum()) { - data.setMessage(data.getMessage().substring(0, invite.getMaxwordsnum())); - } - } else if (StringUtils.isNotBlank(data.getMessage()) && data.getMessage().length() > 300) { - data.setMessage(data.getMessage().substring(0, 300)); - } - - data.setUsession(user); // 绑定唯一用户 - data.setSessionid(sessionid); - data.setMessage(MainUtils.processEmoti(data.getMessage())); // 处理表情 - data.setTouser(aiid); - data.setUsername(p.getUsername()); - data.setAiid(aiid); - data.setAgentserviceid(p.getAgentserviceid()); - data.setChannel(p.getChanneltype()); - data.setContextid(p.getAgentserviceid()); // 一定要设置 ContextID - data.setCalltype(MainContext.CallType.IN.toString()); - - // 保存并发送消息给访客 - getChatbotProxy().createTextMessage( - data, - MainContext.CallType.IN.toString()); - - // 更新访客咨询记录 - p.setUpdatetime(now); - p.setLastmessage(now); - p.setLastmsg(data.getMessage()); - getAgentUserRes().save(p); - - // 发送消息给Bot - getChatbotProxy().publishMessage(data, Constants.CHATBOT_EVENT_TYPE_CHAT); - }); - - - } - - /** - * Lazy load - * - * @return - */ - private AgentUserRepository getAgentUserRes() { - if (agentUserRes == null) { - agentUserRes = MainContext.getContext().getBean(AgentUserRepository.class); - } - - return agentUserRes; - } - - /** - * Lazy load - * - * @return - */ - private ChatbotProxy getChatbotProxy() { - if (chatbotProxy == null) { - chatbotProxy = MainContext.getContext().getBean(ChatbotProxy.class); - } - return chatbotProxy; - } - - private PassportWebIMUserRepository getOnlineUserRes() { - if (onlineUserRes == null) { - onlineUserRes = MainContext.getContext().getBean(PassportWebIMUserRepository.class); - } - - return onlineUserRes; - } - - private ChatbotRepository getChatbotRes() { - if (chatbotRes == null) { - chatbotRes = MainContext.getContext().getBean(ChatbotRepository.class); - } - - return chatbotRes; - } -} +/* + * Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd. + * , Licensed under the Chunsong Public + * License, Version 1.0 (the "License"), https://docs.cskefu.com/licenses/v1.html + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * Copyright (C) 2019-Jun. 2023 Chatopera Inc, , + * Licensed under the Apache License, Version 2.0, + * http://www.apache.org/licenses/LICENSE-2.0 + */ +package com.cskefu.cc.plugins.chatbot; + +import com.chatopera.bot.sdk.Response; +import com.corundumstudio.socketio.AckRequest; +import com.corundumstudio.socketio.SocketIOClient; +import com.corundumstudio.socketio.SocketIOServer; +import com.corundumstudio.socketio.annotation.OnConnect; +import com.corundumstudio.socketio.annotation.OnDisconnect; +import com.corundumstudio.socketio.annotation.OnEvent; +import com.cskefu.cc.acd.ACDServiceRouter; +import com.cskefu.cc.basic.Constants; +import com.cskefu.cc.basic.MainContext; +import com.cskefu.cc.basic.MainUtils; +import com.cskefu.cc.model.*; +import com.cskefu.cc.persistence.repository.AgentUserRepository; +import com.cskefu.cc.persistence.repository.ChatbotRepository; +import com.cskefu.cc.persistence.repository.PassportWebIMUserRepository; +import com.cskefu.cc.proxy.OnlineUserProxy; +import com.cskefu.cc.socketio.client.NettyClients; +import com.cskefu.cc.socketio.message.AgentStatusMessage; +import com.cskefu.cc.socketio.message.ChatMessage; +import com.cskefu.cc.socketio.message.Message; +import com.cskefu.cc.socketio.util.IMServiceUtils; +import com.cskefu.cc.util.IP; +import com.cskefu.cc.util.IPTools; +import org.apache.commons.lang3.StringUtils; +import org.json.JSONObject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; + +import java.net.InetSocketAddress; +import java.util.Date; + +public class ChatbotEventHandler { + private static final Logger logger = LoggerFactory.getLogger(ChatbotEventHandler.class); + + protected final SocketIOServer server; + + private static AgentUserRepository agentUserRes; + private static PassportWebIMUserRepository onlineUserRes; + private static ChatbotRepository chatbotRes; + private static ChatbotProxy chatbotProxy; + + @Autowired + public ChatbotEventHandler(SocketIOServer server) { + this.server = server; + } + + @OnConnect + public void onConnect(SocketIOClient client) { + try { + String user = client.getHandshakeData().getSingleUrlParam("userid"); + String nickname = client.getHandshakeData().getSingleUrlParam("nickname"); + String session = MainUtils.getContextID(client.getHandshakeData().getSingleUrlParam("session")); + String appid = client.getHandshakeData().getSingleUrlParam("appid"); + String aiid = client.getHandshakeData().getSingleUrlParam("aiid"); + logger.info( + "[onConnect] userid {}, nickname {}, session {}, appid {}, aiid {}", user, nickname, session, appid, + aiid); + + client.set("aiid", aiid); + client.set("session", session); + client.set("userid", user); + client.set("appid", appid); + + Date now = new Date(); + + if (StringUtils.isNotBlank(user)) { + /** + * 加入到 缓存列表 + */ + NettyClients.getInstance().putChatbotEventClient(user, client); + CousultInvite invite = OnlineUserProxy.consult(appid); + + /** + * 更新坐席服务类型 + */ + IMServiceUtils.shiftOpsType(user, MainContext.OptType.CHATBOT); + + // send out tip + Message tip = new Message(); + tip.setMessage("您正在使用机器人客服!"); + tip.setMessageType(MainContext.MessageType.MESSAGE.toString()); + tip.setCalltype(MainContext.CallType.IN.toString()); + tip.setCreatetime(MainUtils.dateFormate.format(now)); + + client.sendEvent(MainContext.MessageType.STATUS.toString(), tip); + + // send out welcome message + if (invite != null) { + Chatbot chatbot = getChatbotRes().findById(invite.getAiid()).orElse(null); + com.chatopera.bot.sdk.Chatbot bot = new com.chatopera.bot.sdk.Chatbot( + chatbot.getClientId(), chatbot.getSecret(), chatbot.getBaseUrl()); + Response result = bot.command("GET", "/"); + + // 发送欢迎语 + if (result.getRc() == 0) { + JSONObject details = (JSONObject) result.getData(); + ChatMessage welcome = new ChatMessage(); + String welcomeTextMessage = details.getString("welcome"); + if (StringUtils.isNotBlank(welcomeTextMessage)) { + welcome.setCalltype(MainContext.CallType.OUT.toString()); + welcome.setAppid(appid); + welcome.setAiid(aiid); + welcome.setMessage(welcomeTextMessage); + welcome.setTouser(user); + welcome.setMsgtype(MainContext.MessageType.MESSAGE.toString()); + welcome.setUserid(user); + welcome.setUsername(invite.getAiname()); + welcome.setUpdatetime(System.currentTimeMillis()); + client.sendEvent(MainContext.MessageType.MESSAGE.toString(), welcome); + } + + // 发送常见问题列表 + JSONObject faqhotresp = bot.conversation(user, "__faq_hot_list"); + logger.info("faqhot {}", faqhotresp.toString()); + if (faqhotresp.getInt("rc") == 0) { + JSONObject faqhotdata = faqhotresp.getJSONObject("data"); + if ((!faqhotdata.getBoolean("logic_is_fallback")) && + faqhotdata.has("string") && + faqhotdata.has("params")) { + ChatMessage faqhotmsg = new ChatMessage(); + faqhotmsg.setCalltype(MainContext.CallType.OUT.toString()); + faqhotmsg.setAppid(appid); + faqhotmsg.setAiid(aiid); + faqhotmsg.setMessage(faqhotdata.getString("string")); + faqhotmsg.setExpmsg(faqhotdata.getJSONArray("params").toString()); + faqhotmsg.setTouser(user); + faqhotmsg.setMsgtype(MainContext.MessageType.MESSAGE.toString()); + faqhotmsg.setUserid(user); + faqhotmsg.setUsername(invite.getAiname()); + faqhotmsg.setUpdatetime(System.currentTimeMillis()); + client.sendEvent(MainContext.MessageType.MESSAGE.toString(), faqhotmsg); + } + } + } else if (result.getRc() == 999 || result.getRc() == 998) { + logger.error("[chat] chatbot agent response rc {}, error {}", result.getRc(), result.getError()); + } else { + logger.error("[chat] chatbot agent response rc {}, error {}", result.getRc(), result.getError()); + } + } + + InetSocketAddress address = (InetSocketAddress) client.getRemoteAddress(); + String ip = MainUtils.getIpAddr(client.getHandshakeData().getHttpHeaders(), address.getHostString()); + PassportWebIMUser passportWebIMUser = getOnlineUserRes().findById(user).orElse(null); + + if (passportWebIMUser == null) { + passportWebIMUser = new PassportWebIMUser(); + passportWebIMUser.setAppid(appid); + if (StringUtils.isNotBlank(nickname)) { + passportWebIMUser.setUsername(nickname); + } else { + passportWebIMUser.setUsername(Constants.GUEST_USER + "_" + MainUtils.genIDByKey(user)); + } + + passportWebIMUser.setSessionid(session); + passportWebIMUser.setOptype(MainContext.OptType.CHATBOT.toString()); + passportWebIMUser.setUserid(user); + passportWebIMUser.setId(user); + passportWebIMUser.setChannel(MainContext.ChannelType.WEBIM.toString()); + passportWebIMUser.setIp(ip); + passportWebIMUser.setUpdatetime(now); + passportWebIMUser.setLogintime(now); + passportWebIMUser.setCreatetime(now); + IP ipdata = IPTools.getInstance().findGeography(ip); + passportWebIMUser.setCity(ipdata.getCity()); + passportWebIMUser.setCountry(ipdata.getCountry()); + passportWebIMUser.setProvince(ipdata.getProvince()); + passportWebIMUser.setIsp(ipdata.getIsp()); + passportWebIMUser.setRegion(ipdata.getRegion()); + passportWebIMUser.setStatus(MainContext.OnlineUserStatusEnum.ONLINE.toString()); + } + + // 在线客服访客咨询记录 + AgentUser agentUser = new AgentUser( + passportWebIMUser.getId(), + MainContext.ChannelType.WEBIM.toString(), // callout + passportWebIMUser.getId(), + passportWebIMUser.getUsername(), + appid); + + agentUser.setServicetime(now); + agentUser.setCreatetime(now); + agentUser.setUpdatetime(now); + agentUser.setSessionid(session); + agentUser.setRegion(passportWebIMUser.getRegion()); + + // 聊天机器人处理的请求 + agentUser.setOpttype(MainContext.OptType.CHATBOT.toString()); + agentUser.setAgentno(aiid); // 聊天机器人ID + agentUser.setAgentname(invite != null ? invite.getAiname() : "机器人客服"); + agentUser.setCity(passportWebIMUser.getCity()); + agentUser.setProvince(passportWebIMUser.getProvince()); + agentUser.setCountry(passportWebIMUser.getCountry()); + AgentService agentService = ACDServiceRouter.getAcdChatbotService().processChatbotService( + invite != null ? invite.getAiname() : "机器人客服", agentUser); + agentUser.setAgentserviceid(agentService.getId()); + + // 标记为机器人坐席 + agentUser.setChatbotops(true); + + // 保存到MySQL + getAgentUserRes().save(agentUser); + getOnlineUserRes().save(passportWebIMUser); + } + } catch (Exception e) { + logger.info("[onConnect] error", e); + } + } + + // 添加 @OnDisconnect 事件,客户端断开连接时调用,刷新客户端信息 + @OnDisconnect + public void onDisconnect(SocketIOClient client) { + String user = client.getHandshakeData().getSingleUrlParam("userid"); + if (StringUtils.isNotBlank(user)) { + NettyClients.getInstance().removeChatbotEventClient( + user, MainUtils.getContextID(client.getSessionId().toString())); + PassportWebIMUser passportWebIMUser = MainContext.getCache().findOneOnlineUserByUserId(user); + + MainContext.getCache().findOneAgentUserByUserId(user).ifPresent(p -> { + ACDServiceRouter.getAcdChatbotService().processChatbotService(null, p); + + MainContext.getCache().deleteAgentUserByUserId(p); + MainContext.getCache().deleteOnlineUserById(user); + + p.setStatus(MainContext.AgentUserStatusEnum.END.toString()); + passportWebIMUser.setStatus(MainContext.OnlineUserStatusEnum.OFFLINE.toString()); + + getAgentUserRes().save(p); + getOnlineUserRes().save(passportWebIMUser); + }); + } + client.disconnect(); + } + + // 消息接收入口,网站有新用户接入对话 + @OnEvent(value = "new") + public void onEvent(SocketIOClient client, AckRequest request, Message data) { + + } + + // 消息接收入口,坐席状态更新 + @OnEvent(value = "agentstatus") + public void onEvent(SocketIOClient client, AckRequest request, AgentStatusMessage data) { + logger.info("[onEvent] agentstatus: ", data.getMessage()); + } + + // 消息接收入口,收发消息,用户向机器人发送消息 + @OnEvent(value = "message") + public void onEvent(SocketIOClient client, AckRequest request, ChatMessage data) { + String aiid = client.get("aiid"); + String user = client.get("userid"); + String sessionid = client.get("session"); + String appid = client.get("appid"); + logger.info( + "[onEvent] message: session {}, aiid {}, userid {}, dataType {}, appid {}", sessionid, aiid, + user, data.getType(), appid); + + // ignore event if dataType is not message. + if (!StringUtils.equals(data.getType(), Constants.IM_MESSAGE_TYPE_MESSAGE)) { + return; + } + + MainContext.getCache().findOneAgentUserByUserId(user).ifPresent(p -> { + /** + * 以下代码主要用于检查 访客端的字数限制 + */ + CousultInvite invite = OnlineUserProxy.consult(data.getAppid()); + // ignore event if no invite found. + if (invite == null) { + return; + } + + // ignore if Chatbot is turnoff. + if (!invite.isAi()) { + return; + } + + Date now = new Date(); + if (invite.getMaxwordsnum() > 0) { + if (StringUtils.isNotBlank(data.getMessage()) && data.getMessage().length() > invite.getMaxwordsnum()) { + data.setMessage(data.getMessage().substring(0, invite.getMaxwordsnum())); + } + } else if (StringUtils.isNotBlank(data.getMessage()) && data.getMessage().length() > 300) { + data.setMessage(data.getMessage().substring(0, 300)); + } + + data.setUsession(user); // 绑定唯一用户 + data.setSessionid(sessionid); + data.setMessage(MainUtils.processEmoti(data.getMessage())); // 处理表情 + data.setTouser(aiid); + data.setUsername(p.getUsername()); + data.setAiid(aiid); + data.setAgentserviceid(p.getAgentserviceid()); + data.setChannel(p.getChanneltype()); + data.setContextid(p.getAgentserviceid()); // 一定要设置 ContextID + data.setCalltype(MainContext.CallType.IN.toString()); + + // 保存并发送消息给访客 + getChatbotProxy().createTextMessage( + data, + MainContext.CallType.IN.toString()); + + // 更新访客咨询记录 + p.setUpdatetime(now); + p.setLastmessage(now); + p.setLastmsg(data.getMessage()); + getAgentUserRes().save(p); + + // 发送消息给Bot + getChatbotProxy().publishMessage(data, Constants.CHATBOT_EVENT_TYPE_CHAT); + }); + + + } + + /** + * Lazy load + * + * @return + */ + private AgentUserRepository getAgentUserRes() { + if (agentUserRes == null) { + agentUserRes = MainContext.getContext().getBean(AgentUserRepository.class); + } + + return agentUserRes; + } + + /** + * Lazy load + * + * @return + */ + private ChatbotProxy getChatbotProxy() { + if (chatbotProxy == null) { + chatbotProxy = MainContext.getContext().getBean(ChatbotProxy.class); + } + return chatbotProxy; + } + + private PassportWebIMUserRepository getOnlineUserRes() { + if (onlineUserRes == null) { + onlineUserRes = MainContext.getContext().getBean(PassportWebIMUserRepository.class); + } + + return onlineUserRes; + } + + private ChatbotRepository getChatbotRes() { + if (chatbotRes == null) { + chatbotRes = MainContext.getContext().getBean(ChatbotRepository.class); + } + + return chatbotRes; + } +} diff --git a/contact-center/app/src/main/java/com/cskefu/cc/proxy/AgentUserProxy.java b/contact-center/app/src/main/java/com/cskefu/cc/proxy/AgentUserProxy.java index 58df9ddb..589e6d9d 100644 --- a/contact-center/app/src/main/java/com/cskefu/cc/proxy/AgentUserProxy.java +++ b/contact-center/app/src/main/java/com/cskefu/cc/proxy/AgentUserProxy.java @@ -1,14 +1,14 @@ -/* - * Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd. - * , Licensed under the Chunsong Public +/* + * Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd. + * , Licensed under the Chunsong Public * License, Version 1.0 (the "License"), https://docs.cskefu.com/licenses/v1.html * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. - * Copyright (C) 2019-2022 Chatopera Inc, , - * Licensed under the Apache License, Version 2.0, + * Copyright (C) 2019-2022 Chatopera Inc, , + * Licensed under the Apache License, Version 2.0, * http://www.apache.org/licenses/LICENSE-2.0 */ package com.cskefu.cc.proxy; @@ -34,6 +34,7 @@ import org.springframework.web.servlet.ModelAndView; import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; + import java.io.IOException; import java.util.*; @@ -96,6 +97,8 @@ public class AgentUserProxy { @Lazy private PeerSyncIM peerSyncIM; + @Autowired + private LicenseProxy licenseProxy; /** * 与联系人主动聊天前查找获取AgentUser diff --git a/contact-center/app/src/main/java/com/cskefu/cc/proxy/LicenseProxy.java b/contact-center/app/src/main/java/com/cskefu/cc/proxy/LicenseProxy.java new file mode 100644 index 00000000..ba8bc358 --- /dev/null +++ b/contact-center/app/src/main/java/com/cskefu/cc/proxy/LicenseProxy.java @@ -0,0 +1,565 @@ +/* + * Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd. + * , Licensed under the Chunsong Public + * License, Version 1.0 (the "License"), https://docs.cskefu.com/licenses/v1.html + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.cskefu.cc.proxy; + +import com.chatopera.store.enums.LICSTATUS; +import com.chatopera.store.sdk.QuotaWdClient; +import com.chatopera.store.sdk.Response; +import com.chatopera.store.sdk.exceptions.InvalidRequestException; +import com.chatopera.store.sdk.exceptions.InvalidResponseException; +import com.cskefu.cc.basic.Constants; +import com.cskefu.cc.basic.MainContext; +import com.cskefu.cc.basic.MainUtils; +import com.cskefu.cc.exception.*; +import com.cskefu.cc.model.AgentUser; +import com.cskefu.cc.model.ExecuteResult; +import com.cskefu.cc.model.MetaKv; +import com.cskefu.cc.persistence.repository.MetaKvRepository; +import com.cskefu.cc.util.Base62; +import com.cskefu.cc.util.DateConverter; +import org.apache.commons.lang3.StringUtils; +import org.json.JSONArray; +import org.json.JSONObject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.SpringApplication; +import org.springframework.stereotype.Service; + +import java.text.ParseException; +import java.util.*; + +/** + * 证书服务 + */ +@Service +public class LicenseProxy { + private final static Logger logger = LoggerFactory.getLogger(LicenseProxy.class); + + @Autowired + private MetaKvRepository metaKvRes; + + @Autowired + private QuotaWdClient quotaWdClient; + + private static final Map BILLING_RES_QUOTA_MAPPINGS = new HashMap<>(); + + static { + BILLING_RES_QUOTA_MAPPINGS.put(MainContext.BillingResource.USER, 100); + BILLING_RES_QUOTA_MAPPINGS.put(MainContext.BillingResource.AGENGUSER, 1); + BILLING_RES_QUOTA_MAPPINGS.put(MainContext.BillingResource.CHANNELWEBIM, 100); + BILLING_RES_QUOTA_MAPPINGS.put(MainContext.BillingResource.CONTACT, 1); + BILLING_RES_QUOTA_MAPPINGS.put(MainContext.BillingResource.ORGAN, 10); + } + + /** + * 初始化 serverinstId + * serverinstId 作为服务唯一的实例ID + */ + public void checkOnStartup() { + /** + * Check service connection + */ + System.out.println("[license] license service URL " + quotaWdClient.getBaseUrl()); + try { + Response resp = quotaWdClient.ping(); + System.out.println("[license] license service ping successfully."); + + if (resp.getRc() != 0) { + throw new InvalidResponseException("Unexpected response from license service " + resp.toString()); + } + } catch (InvalidResponseException e) { + logger.error("[license] make sure this host machine could connect to " + quotaWdClient.getBaseUrl() + " during running."); + logger.error("[license] checkOnStartup could not connect to license service, CSKeFu instance is terminated.", e); + // Very serious event happens, just shutdown the instance + SpringApplication.exit(MainContext.getContext(), () -> 1); + } + + /** + * Init local data for License + */ + resolveServerinstId(); + resolveServicename(); + resolveLicenseIds(); + } + + /** + * 读取或初始化 serverinstId + * + * @return + */ + public String resolveServerinstId() { + Optional metaServerinstIdOpt = metaKvRes.findFirstByMetakey(Constants.LICENSE_SERVER_INST_ID); + if (metaServerinstIdOpt.isEmpty()) { + // 没有 serverinstId 信息,初始化 + final String serverinstId = Base62.generateShortId(); + createMetaKv(Constants.LICENSE_SERVER_INST_ID, serverinstId, Constants.METAKV_DATATYPE_STRING); + return serverinstId; + } + return metaServerinstIdOpt.get().getMetavalue(); + } + + /** + * 读取或初始化 licenseIds + */ + private void resolveLicenseIds() { + Optional metaLicensesOpt = metaKvRes.findFirstByMetakey(Constants.LICENSEIDS); + if (metaLicensesOpt.isEmpty()) { + // 没有 license 信息,初始化 + createMetaKv(Constants.LICENSEIDS, (new JSONArray()).toString(), Constants.METAKV_DATATYPE_STRING); + } + } + + /** + * 读取或初始化 serviceName + * + * @return + */ + public String resolveServicename() { + Optional metaServicenameOpt = metaKvRes.findFirstByMetakey(Constants.LICENSE_SERVICE_NAME); + if (metaServicenameOpt.isEmpty()) { + // 没有 Service Name 信息,初始化 + final String serviceName = generateLicenseServiceName(); + createMetaKv(Constants.LICENSE_SERVICE_NAME, serviceName, Constants.METAKV_DATATYPE_STRING); + return serviceName; + } + return metaServicenameOpt.get().getMetavalue(); + } + + /** + * 从 MetaKv 表中取得数据 MetaKv + * + * @param key + * @return + * @throws MetaKvNotExistException + */ + public MetaKv retrieveMetaKv(final String key) throws MetaKvNotExistException, MetaKvInvalidKeyException { + + if (StringUtils.isBlank(key)) { + throw new MetaKvInvalidKeyException("Key must not be empy"); + } + + Optional kvOpt = metaKvRes.findFirstByMetakey(key); + if (kvOpt.isEmpty()) { + throw new MetaKvNotExistException(key + " not exist"); + } else { + return kvOpt.get(); + } + } + + /** + * 创建或更新 MetaKv + * UpdateOnExist + * + * @param key + * @param value + * @param datatype + */ + public MetaKv createOrUpdateMetaKv(final String key, final String value, final String datatype) throws MetaKvInvalidKeyException { + try { + MetaKv kv = retrieveMetaKv(key); + kv.setMetavalue(value); + kv.setUpdatetime(new Date()); + metaKvRes.save(kv); + return kv; + } catch (MetaKvNotExistException e) { + return createMetaKv(key, value, datatype); + } + } + + + /** + * 建立 Metakv 数据 + * + * @param key + * @param value + * @param datatype + */ + public MetaKv createMetaKv(final String key, final String value, final String datatype) { + Date now = new Date(); + MetaKv metakv = new MetaKv(); + metakv.setCreatetime(now); + metakv.setUpdatetime(now); + metakv.setMetakey(key); + metakv.setMetavalue(value); + metakv.setDatatype(datatype); + + metaKvRes.save(metakv); + return metakv; + } + + /** + * 增加 MetaKv 中的 Key 的值,作为 Integer 做增量,不存在则初始化其值为 0,然后增量操作 + * + * @param key + * @param incrValue + */ + public MetaKv increValueInMetaKv(final String key, final int incrValue) { + try { + MetaKv kv = retrieveMetaKv(key); + int pre = Integer.parseInt(kv.getMetavalue()); + kv.setMetavalue(Integer.toString(pre + incrValue)); + kv.setUpdatetime(new Date()); + metaKvRes.save(kv); + return kv; + } catch (MetaKvNotExistException e) { + return createMetaKv(key, Integer.toString(incrValue), Constants.METAKV_DATATYPE_INT); + } catch (MetaKvInvalidKeyException e) { + throw new RuntimeException(e); + } + } + + + /** + * 生成随机字符串,作为服务名称 + * + * @return + */ + private String generateLicenseServiceName() { + StringBuffer sb = new StringBuffer(); + + sb.append(Constants.LICENSE_SERVICE_NAME_PREFIX); + sb.append(Base62.generatingRandomAlphanumericString(5)); + + return sb.toString(); + } + + /** + * 从数据库及证书商店获得证书列表信息 + * + * @return + */ + public List getLicensesInStore() throws InvalidResponseException { + List result = new ArrayList<>(); + + try { + JSONArray ja = new JSONArray((retrieveMetaKv(Constants.LICENSEIDS).getMetavalue())); + HashMap addDates = new HashMap<>(); + List licenseIds = new ArrayList<>(); + for (int i = 0; i < ja.length(); i++) { + JSONObject obj = ((JSONObject) ja.get(i)); + licenseIds.add(obj.getString(Constants.SHORTID)); + addDates.put(obj.getString(Constants.SHORTID), obj.getString(Constants.ADDDATE)); + } + + Response resp = null; + try { + resp = quotaWdClient.getLicenseBasics(licenseIds); + } catch (InvalidRequestException e) { + return result; + } + JSONArray data = (JSONArray) resp.getData(); + + for (int i = 0; i < data.length(); i++) { + JSONObject lic = (JSONObject) data.get(i); + if(StringUtils.equals(lic.getJSONObject(Constants.LICENSE).getString(Constants.STATUS), "notfound")){ + // fill in placeholders for notfound license + final JSONObject licenseJsonTmp = lic.getJSONObject(Constants.LICENSE); + licenseJsonTmp.put("effectivedateend", "N/A"); + licenseJsonTmp.put("quotaeffectiveremaining", "N/A"); + + JSONObject productJsonTmp = new JSONObject(); + productJsonTmp.put("shortId", "N/A"); + productJsonTmp.put("name", "N/A"); + lic.put("product", productJsonTmp); + + JSONObject userJsonTmp = new JSONObject(); + userJsonTmp.put("nickname", "N/A"); + lic.put("user", userJsonTmp); + +// lic.put(Constants.ADDDATE, null); + + result.add(lic); + continue; + } + + try { + Date addDate = DateConverter.parseCSTAsChinaTimezone(addDates.get(lic.getJSONObject(Constants.LICENSE).getString(Constants.SHORTID))); + lic.put(Constants.ADDDATE, addDate); + } catch (ParseException e) { + logger.info("[getLicensesFromStore] can not resolve add date"); + } + result.add(lic); + } + + } catch (MetaKvNotExistException e) { + logger.info("[getLicenses] no LICENSEIDS data in MySQL DB"); + } catch (MetaKvInvalidKeyException e) { + throw new RuntimeException(e); + } + + return result; + } + + /** + * 获得在 MetaKV 表中的 license 信息 + * + * @return JSONArray + */ + public JSONArray getLicensesInMetakv() { + try { + String value = retrieveMetaKv(Constants.LICENSEIDS).getMetavalue(); + return new JSONArray(value); + } catch (MetaKvNotExistException e) { + return new JSONArray(); + } catch (MetaKvInvalidKeyException e) { + return new JSONArray(); + } + } + + /** + * @param licenseShortId + * @return + * @throws InvalidResponseException + */ + public JSONObject getLicenseBasicsInStore(final String licenseShortId) throws InvalidResponseException, InvalidRequestException { + Response resp = quotaWdClient.getLicenseBasics(licenseShortId); + if (resp.getRc() == 0) { + JSONArray data = (JSONArray) resp.getData(); + if (data.length() != 1) + throw new InvalidResponseException("Unexpected data in Response."); + + return (JSONObject) (data).get(0); + } else { + throw new InvalidResponseException("Unexpected Response."); + } + + } + + /** + * 获得已经添加的证书在 Store 中的基本信息 + * + * @return + * @throws InvalidResponseException + */ + public JSONArray getAddedLicenseBasicsInStore() throws InvalidResponseException { + JSONArray arr = getLicensesInMetakv(); + List ids = new ArrayList<>(); + + for (int i = 0; i < arr.length(); i++) { + ids.add(((JSONObject) arr.get(i)).getString(Constants.SHORTID)); + } + + if (ids.size() > 0) { + Response resp = null; + try { + resp = quotaWdClient.getLicenseBasics(StringUtils.join(ids, ",")); + } catch (InvalidRequestException e) { + logger.error("[getAddedLicenseBasicsFromStore] InvalidRequestException", e); + } + if (resp.getRc() != 0) { + throw new InvalidResponseException("Invalid response, rc " + Integer.toString(resp.getRc())); + } + + return (JSONArray) resp.getData(); + } else { + logger.error("[license] getAddedLicenseBasicsFromStore - No license ids in metaKv"); + return new JSONArray(); + } + } + + + /** + * 验证证书存在 + * + * @param licenseShortId + * @return + */ + public LICSTATUS existLicenseInStore(final String licenseShortId) throws InvalidResponseException, LicenseNotFoundException, InvalidRequestException { + Map statuses = quotaWdClient.getLicenseStatus(licenseShortId); + + if (statuses.size() == 1) { + for (final Map.Entry entry : statuses.entrySet()) { + final LICSTATUS status = entry.getValue(); + + if (status == LICSTATUS.NOTFOUND) + throw new LicenseNotFoundException("LicenseId not found [" + licenseShortId + "]"); + + return status; + } + throw new InvalidResponseException("Unexpected response, internal error."); + } else { + throw new InvalidResponseException("Unexpected response, should contain one record."); + } + } + + public String getLicenseStoreProvider() { + return quotaWdClient.getBaseUrl(); + } + + /** + * 获得资源用量存储 + * + * @param resourceKey + * @return + */ + public String getResourceUsageKey(final String resourceKey) { + StringBuffer sb = new StringBuffer(); + sb.append(Constants.RESOURCES_USAGE_KEY_PREFIX); + sb.append("_"); + sb.append(StringUtils.toRootUpperCase(resourceKey)); + return sb.toString(); + } + + + /** + * 增加计费资源用量 + * + * @param billingResource + * @param consume + */ + public void increResourceUsageInMetaKv(final MainContext.BillingResource billingResource, int consume) throws BillingResourceException { + switch (billingResource) { + case USER: + case CONTACT: + case ORGAN: + case AGENGUSER: + case CHANNELWEBIM: + increValueInMetaKv(getResourceUsageKey(billingResource.toString()), consume); + break; + default: + throw new BillingResourceException("invalid_billing_resource_type"); + } + } + + /** + * 获取在 MetaKv 中资源的已经使用的计数 + * + * @param billingResource + * @return + */ + private int getResourceUsageInMetaKv(final MainContext.BillingResource billingResource) { + final String key = getResourceUsageKey(billingResource.toString()); + try { + MetaKv kv = retrieveMetaKv(key); + int pre = Integer.parseInt(kv.getMetavalue()); + return pre; + } catch (MetaKvNotExistException e) { + createMetaKv(key, Integer.toString(0), Constants.METAKV_DATATYPE_INT); + return 0; + } catch (MetaKvInvalidKeyException e) { + return 0; + } + } + + /** + * 获得春松客服 cskefu001 产品的证书标识 ID + * 春松客服证书基本类型 + * + * @return + */ + private String getLicenseIdAsCskefu001InMetaKv() throws BillingQuotaException { + JSONArray curr = getLicensesInMetakv(); + for (int i = 0; i < curr.length(); i++) { + JSONObject jo = (JSONObject) curr.get(i); + if (jo.has(Constants.PRODUCT_ID) && + StringUtils.equals(jo.getString(Constants.PRODUCT_ID), Constants.PRODUCT_ID_CSKEFU001)) { + return jo.getString(Constants.SHORTID); + } + } + throw new BillingQuotaException(BillingQuotaException.NO_LICENSE_FOUND); + } + + /** + * 执行配额变更操作 + * + * @param billingResource + * @param unitNum + * @return + */ + public void writeDownResourceUsageInStore(final MainContext.BillingResource billingResource, + int unitNum) throws BillingQuotaException, BillingResourceException { + + // 检查是否还在体验阶段 + if (billingResource == MainContext.BillingResource.CONTACT) { + int alreadyUsed = getResourceUsageInMetaKv(billingResource); + if (alreadyUsed <= 1) { + // 可以免费创建 1 个联系人 + return; + } + } + + // 请求操作配额 + String licenseId = getLicenseIdAsCskefu001InMetaKv(); + String serverinstId = resolveServerinstId(); + String servicename = resolveServicename(); + + try { + Response resp = quotaWdClient.write(licenseId, + serverinstId, servicename, unitNum * BILLING_RES_QUOTA_MAPPINGS.get(billingResource)); + // 识别操作是否完成,并处理 + if (resp.getRc() == 0) { + final JSONObject data = (JSONObject) resp.getData(); + + // 配额操作成功,执行计数 + increResourceUsageInMetaKv(billingResource, unitNum); + } else if (resp.getRc() == 1 || resp.getRc() == 2) { + throw new BillingQuotaException(BillingQuotaException.INVALID_REQUEST_BODY); + } else if (resp.getRc() == 3) { + // 证书商店中不存在 + throw new BillingQuotaException(BillingQuotaException.LICENSE_INVALID); + } else if (resp.getRc() == 4) { + // 证书商店中不存在该产品或产品类型无效 + throw new BillingQuotaException(BillingQuotaException.PRODUCT_INVALID); + } else if (resp.getRc() == 5) { + // 该证书不支持资源回退 + throw new BillingQuotaException(BillingQuotaException.LICENSE_UNSUPPORT_REFUND); + } else if (resp.getRc() == 6) { + // 证书失效或耗尽,不支持继续扣除配额 + throw new BillingQuotaException(BillingQuotaException.LICENSE_EXPIRED_OR_EXHAUSTED); + } else if (resp.getRc() == 7) { + // 该证书禁用了该 serverinstId + throw new BillingQuotaException(BillingQuotaException.LICENSE_DISABLED_SERVERINST); + } else if (resp.getRc() == 8) { + // 配额扣除额度超过该证书目前的剩余量 + throw new BillingQuotaException(BillingQuotaException.LICENSE_QUOTA_INADEQUATE); + } else { + // 未知情况 + logger.error("[writeDownResourceUsageInStore] resp data {}", resp.toString()); + throw new BillingQuotaException(BillingQuotaException.INTERNAL_ERROR); + } + } catch (InvalidResponseException e) { + // TODO 处理异常信息 + logger.error("[writeDownResourceUsageInStore] error ", e); + throw new BillingQuotaException(BillingQuotaException.RESPONSE_UNEXPECTED); + } + } + + /** + * 访客会话执行计费 + * + * @param agentUser + * @return + */ + public ExecuteResult writeDownAgentUserUsageInStore(final AgentUser agentUser) { + // 检查是否还在体验阶段 + ExecuteResult er = new ExecuteResult(); + int alreadyUsed = getResourceUsageInMetaKv(MainContext.BillingResource.AGENGUSER); + if (alreadyUsed <= 100) { + // 可以免费创建 100 个访客会话 + er.setRc(ExecuteResult.RC_SUCC); + return er; + } + + try { + writeDownResourceUsageInStore(MainContext.BillingResource.AGENGUSER, 1); + er.setRc(ExecuteResult.RC_SUCC); + } catch (BillingQuotaException e) { + er.setRc(ExecuteResult.RC_ERR1); + er.setMsg(e.getMessage()); + } catch (BillingResourceException e) { + er.setRc(ExecuteResult.RC_ERR2); + er.setMsg(e.getMessage()); + } + + return er; + } +} diff --git a/contact-center/app/src/main/java/com/cskefu/cc/proxy/UserProxy.java b/contact-center/app/src/main/java/com/cskefu/cc/proxy/UserProxy.java index 74309ac3..83f2f086 100644 --- a/contact-center/app/src/main/java/com/cskefu/cc/proxy/UserProxy.java +++ b/contact-center/app/src/main/java/com/cskefu/cc/proxy/UserProxy.java @@ -1,13 +1,13 @@ -/* - * Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd. - * , Licensed under the Chunsong Public +/* + * Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd. + * , Licensed under the Chunsong Public * License, Version 1.0 (the "License"), https://docs.cskefu.com/licenses/v1.html * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. - * Copyright (C) 2019-2022 Chatopera Inc, All rights reserved. + * Copyright (C) 2019-2022 Chatopera Inc, All rights reserved. * */ @@ -16,6 +16,7 @@ package com.cskefu.cc.proxy; import com.cskefu.cc.basic.Constants; import com.cskefu.cc.basic.MainContext; import com.cskefu.cc.basic.MainUtils; +import com.cskefu.cc.exception.BillingQuotaException; import com.cskefu.cc.model.*; import com.cskefu.cc.persistence.repository.*; import com.cskefu.cc.util.restapi.RestUtils; @@ -29,6 +30,8 @@ import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Component; import jakarta.persistence.criteria.Predicate; + +import java.lang.reflect.UndeclaredThrowableException; import java.util.*; import java.util.stream.Collectors; @@ -79,22 +82,34 @@ public class UserProxy { public JsonObject createNewUser(final User user, Organ organ) { JsonObject result = new JsonObject(); String msg = validUser(user); - if (StringUtils.equalsIgnoreCase(msg, "new_user_success")) { + + if (StringUtils.equalsIgnoreCase(msg, Constants.NEW_USER_SUCCESS)) { // 此时 msg 是 new_user_success user.setSuperadmin(false); // 不支持创建第二个系统管理员 + try { + if (StringUtils.isNotBlank(user.getPassword())) { + user.setPassword(MainUtils.md5(user.getPassword())); + } + userRes.save(user); - if (StringUtils.isNotBlank(user.getPassword())) { - user.setPassword(MainUtils.md5(user.getPassword())); - } - userRes.save(user); - - if (organ != null) { - OrganUser ou = new OrganUser(); - ou.setUserid(user.getId()); - ou.setOrgan(organ.getId()); - organUserRes.save(ou); + if (organ != null) { + OrganUser ou = new OrganUser(); + ou.setUserid(user.getId()); + ou.setOrgan(organ.getId()); + organUserRes.save(ou); + } + } catch (Exception e) { + if (e instanceof UndeclaredThrowableException) { + logger.error("[createNewUser] BillingQuotaException", e); + if (StringUtils.startsWith(e.getCause().getMessage(), BillingQuotaException.SUFFIX)) { + msg = e.getCause().getMessage(); + } + } else { + logger.error("[createNewUser] err", e); + } } } + // 新账号未通过验证,返回创建失败信息msg result.addProperty(RestUtils.RESP_KEY_RC, RestUtils.RESP_RC_SUCC); result.addProperty(RestUtils.RESP_KEY_DATA, msg); diff --git a/contact-center/app/src/main/java/com/cskefu/cc/util/Base62.java b/contact-center/app/src/main/java/com/cskefu/cc/util/Base62.java index 597c9150..132fbbba 100644 --- a/contact-center/app/src/main/java/com/cskefu/cc/util/Base62.java +++ b/contact-center/app/src/main/java/com/cskefu/cc/util/Base62.java @@ -1,63 +1,99 @@ /* - * Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd. - * , Licensed under the Chunsong Public + * Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd. + * , Licensed under the Chunsong Public * License, Version 1.0 (the "License"), https://docs.cskefu.com/licenses/v1.html * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. - * Copyright (C) 2018- Jun. 2023 Chatopera Inc, , Licensed under the Apache License, Version 2.0, + * Copyright (C) 2018- Jun. 2023 Chatopera Inc, , Licensed under the Apache License, Version 2.0, * http://www.apache.org/licenses/LICENSE-2.0 - * Copyright (C) 2017 优客服-多渠道客服系统, Licensed under the Apache License, Version 2.0, + * Copyright (C) 2017 优客服-多渠道客服系统, Licensed under the Apache License, Version 2.0, * http://www.apache.org/licenses/LICENSE-2.0 */ package com.cskefu.cc.util; import org.apache.commons.codec.digest.DigestUtils; +import org.apache.commons.lang3.RandomStringUtils; import org.apache.commons.lang3.StringUtils; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Random; + public class Base62 { - private static final int BINARY = 0x2; + private static final int BINARY = 0x2; - private static final int NUMBER_61 = 0x0000003d; - - static final char[] DIGITS = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f', 'g', - 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'A', 'B', - 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', - 'X', 'Y', 'Z' }; + private static final int NUMBER_61 = 0x0000003d; + + static final char[] DIGITS = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f', 'g', + 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'A', 'B', + 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', + 'X', 'Y', 'Z'}; - public static String encode(long value){ - return encode(String.valueOf(value)).toLowerCase() ; - } - - public static String encode(String str){ - String md5Hex = DigestUtils.md5Hex(str); - // 6 digit binary can indicate 62 letter & number from 0-9a-zA-Z - int binaryLength = 6 * 6; - long binaryLengthFixer = Long.valueOf(StringUtils.repeat("1", binaryLength), BINARY); - for (int i = 0; i < 4;) { - String subString = StringUtils.substring(md5Hex, i * 8, (i + 1) * 8); - subString = Long.toBinaryString(Long.valueOf(subString, 16) & binaryLengthFixer); - subString = StringUtils.leftPad(subString, binaryLength, "0"); - StringBuilder sbBuilder = new StringBuilder(); - for (int j = 0; j < 6; j++) { - String subString2 = StringUtils.substring(subString, j * 6, (j + 1) * 6); - int charIndex = Integer.valueOf(subString2, BINARY) & NUMBER_61; - sbBuilder.append(DIGITS[charIndex]); - } - String shortUrl = sbBuilder.toString(); - if(shortUrl!=null){ - return shortUrl; - } - } - // if all 4 possibilities are already exists - return null; - } - + public static String encode(long value) { + return encode(String.valueOf(value)).toLowerCase(); + } + + public static String encode(String str) { + String md5Hex = DigestUtils.md5Hex(str); + // 6 digit binary can indicate 62 letter & number from 0-9a-zA-Z + int binaryLength = 6 * 6; + long binaryLengthFixer = Long.valueOf(StringUtils.repeat("1", binaryLength), BINARY); + for (int i = 0; i < 4; ) { + String subString = StringUtils.substring(md5Hex, i * 8, (i + 1) * 8); + subString = Long.toBinaryString(Long.valueOf(subString, 16) & binaryLengthFixer); + subString = StringUtils.leftPad(subString, binaryLength, "0"); + StringBuilder sbBuilder = new StringBuilder(); + for (int j = 0; j < 6; j++) { + String subString2 = StringUtils.substring(subString, j * 6, (j + 1) * 6); + int charIndex = Integer.valueOf(subString2, BINARY) & NUMBER_61; + sbBuilder.append(DIGITS[charIndex]); + } + String shortUrl = sbBuilder.toString(); + if (shortUrl != null) { + return shortUrl; + } + } + // if all 4 possibilities are already exists + return null; + } + @SuppressWarnings("unused") - private static void print(Object messagr){ - System.out.println(messagr); - } + private static void print(Object messagr) { + System.out.println(messagr); + } + + + /** + * 生成 16 位随机字符串作为 ID + * https://stackoverflow.com/questions/4267475/generating-8-character-only-uuids + */ + public static String generateShortId() { + SimpleDateFormat df = new SimpleDateFormat("yyMMdd"); + String randomStr = RandomStringUtils.randomAlphanumeric(10); + return df.format(new Date()) + randomStr; + } + + /** + * 生成随机字符串 + * + * @param targetStringLength + * @return + */ + public static String generatingRandomAlphanumericString(final int targetStringLength) { + int leftLimit = 48; // numeral '0' + int rightLimit = 122; // letter 'z' + Random random = new Random(); + + String generatedString = random.ints(leftLimit, rightLimit + 1) + .filter(i -> (i <= 57 || i >= 65) && (i <= 90 || i >= 97)) + .limit(targetStringLength) + .collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append) + .toString(); + + return generatedString; + } } \ No newline at end of file diff --git a/contact-center/app/src/main/java/com/cskefu/cc/util/DateConverter.java b/contact-center/app/src/main/java/com/cskefu/cc/util/DateConverter.java index 1a419fa4..8942a975 100644 --- a/contact-center/app/src/main/java/com/cskefu/cc/util/DateConverter.java +++ b/contact-center/app/src/main/java/com/cskefu/cc/util/DateConverter.java @@ -1,54 +1,74 @@ /* - * Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd. - * , Licensed under the Chunsong Public + * Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd. + * , Licensed under the Chunsong Public * License, Version 1.0 (the "License"), https://docs.cskefu.com/licenses/v1.html * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. - * Copyright (C) 2018- Jun. 2023 Chatopera Inc, , Licensed under the Apache License, Version 2.0, + * Copyright (C) 2018- Jun. 2023 Chatopera Inc, , Licensed under the Apache License, Version 2.0, * http://www.apache.org/licenses/LICENSE-2.0 - * Copyright (C) 2017 优客服-多渠道客服系统, Licensed under the Apache License, Version 2.0, + * Copyright (C) 2017 优客服-多渠道客服系统, Licensed under the Apache License, Version 2.0, * http://www.apache.org/licenses/LICENSE-2.0 */ package com.cskefu.cc.util; import org.apache.commons.beanutils.converters.DateTimeConverter; +import java.text.ParseException; +import java.text.SimpleDateFormat; import java.util.Date; - -public class DateConverter extends DateTimeConverter { - - public DateConverter() { - } - - public DateConverter(Object defaultValue) { - super(defaultValue); - } - - /* (non-Javadoc) - * @see org.apache.commons.beanutils.converters.AbstractConverter#getDefaultType() - */ - @SuppressWarnings("rawtypes") - protected Class getDefaultType() { - return Date.class; - } - - /* - * (non-Javadoc) - * @see org.apache.commons.beanutils.converters.DateTimeConverter#convertToType(java.lang.Class, java.lang.Object) - */ - @SuppressWarnings("rawtypes") - @Override - protected Object convertToType(Class arg0, Object arg1) throws Exception { - if (arg1 == null) { - return null; - } - String value = arg1.toString().trim(); - if (value.length() == 0) { - return null; - } - return super.convertToType(arg0, arg1); - } +import java.util.Locale; + +public class DateConverter extends DateTimeConverter { + + final public static String ZONE_ID_DEFAULT = "Asia/Shanghai"; + // format date string like `Wed Aug 30 16:30:23 CST 2023` to Date + public static SimpleDateFormat TIMEZONE_CHINA_FORMAT_DEFAULT = new SimpleDateFormat("EEE MMM dd HH:mm:ss zzz yyyy", Locale.US); + + public DateConverter() { + } + + public DateConverter(Object defaultValue) { + super(defaultValue); + } + + /* (non-Javadoc) + * @see org.apache.commons.beanutils.converters.AbstractConverter#getDefaultType() + */ + @SuppressWarnings("rawtypes") + protected Class getDefaultType() { + return Date.class; + } + + /* + * (non-Javadoc) + * @see org.apache.commons.beanutils.converters.DateTimeConverter#convertToType(java.lang.Class, java.lang.Object) + */ + @SuppressWarnings("rawtypes") + @Override + protected Object convertToType(Class arg0, Object arg1) throws Exception { + if (arg1 == null) { + return null; + } + String value = arg1.toString().trim(); + if (value.length() == 0) { + return null; + } + return super.convertToType(arg0, arg1); + } + + /** + * Java将CST的时间字符串转换成需要的日期格式字符串 + * https://blog.csdn.net/qq_44868502/article/details/103511505 + * (new Date()).toString() 与 String to Date 的转化 + * + * @param dstr + * @return + * @throws ParseException + */ + static public Date parseCSTAsChinaTimezone(final String dstr) throws ParseException { + return (Date) TIMEZONE_CHINA_FORMAT_DEFAULT.parse(dstr); + } } \ No newline at end of file diff --git a/contact-center/app/src/main/java/com/cskefu/cc/util/PugHelper.java b/contact-center/app/src/main/java/com/cskefu/cc/util/PugHelper.java index f0b1d424..93934acd 100644 --- a/contact-center/app/src/main/java/com/cskefu/cc/util/PugHelper.java +++ b/contact-center/app/src/main/java/com/cskefu/cc/util/PugHelper.java @@ -12,18 +12,34 @@ package com.cskefu.cc.util; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONArray; +import com.chatopera.store.enums.LICSTATUS; +import com.chatopera.store.exceptions.EnumValueException; +import org.apache.commons.lang3.StringUtils; import java.text.SimpleDateFormat; import java.util.*; public class PugHelper { - public String formatDate(String pattern, Date value) { - if (value == null) { - return ""; - } + public final static String NA = "N/A"; - SimpleDateFormat format = new SimpleDateFormat(pattern); - return format.format(value); + + public String formatDate(String pattern, Date value) { + try { + if (value == null) { + return NA; + } + + SimpleDateFormat format = new SimpleDateFormat(pattern); + String result = format.format(value); + + if (StringUtils.isBlank(result)) { + return NA; + } + return result; + } catch (Exception e) { + e.printStackTrace(); + } + return NA; } public String padRight(Object src, String ch) { @@ -42,8 +58,39 @@ public class PugHelper { return new String(charr); } + /** + * 在字符串中替换一些字符为 *, 起到混淆、加密、遮盖的敏感信息的目的 + * + * @param prev + * @return + */ + public String messupStringWithStars(final String prev) { + StringBuffer sb = new StringBuffer(); + + if (prev.length() >= 6) { + sb.append("***"); + int initial = prev.length() - 4; + for (int i = initial; i < prev.length(); i++) { + sb.append(prev.charAt(i)); + } + } else { // < 6 + if (prev.length() <= 2 && prev.length() > 0) { + return "***"; + } else { // 2 < length < 6 + sb.append("***"); + int initial = prev.length() - 2; + for (int i = initial; i < prev.length(); i++) { + sb.append(prev.charAt(i)); + } + } + } + + return sb.toString(); + } + /** * 将 String 转化为 JSONArray + * * @param str * @return */ @@ -72,4 +119,59 @@ public class PugHelper { Collections.reverse(result); return result; } + + /** + * 获得证书状态的中文 + * + * @param status + * @return + */ + public String getLicstatusInChinese(final String status) { + try { + LICSTATUS licstatus = LICSTATUS.toValue(status); + switch (licstatus) { + case NOTFOUND -> { + return "未找到"; + } + case EXHAUSTED -> { + return "配额耗尽"; + } + case INUSE -> { + return "使用中"; + } + case EXPIRED -> { + return "已过期"; + } + default -> { + return status; + } + } + } catch (EnumValueException e) { + return "未知"; + } + } + + /** + * 截取字符串,首先根据分隔符分隔,然后选取前 N 个,使用连接符连接返回 + * + * @param orignal + * @param splitBy + * @param firstN + * @param joinWith + * @return + */ + public String splitStringAndJoinWith(final String orignal, final String splitBy, final int firstN, final String joinWith) { + String[] splits = StringUtils.split(orignal, splitBy); + int n = Math.min(splits.length, firstN); + List joined = new ArrayList<>(); + for (int i = 0; i < n; i++) { + joined.add(splits[i]); + } + + if (joined.size() > 0) { + return StringUtils.join(joined, joinWith); + } else { + return ""; + } + } } diff --git a/contact-center/app/src/main/resources/application.properties b/contact-center/app/src/main/resources/application.properties index f39822a5..c7c352b7 100644 --- a/contact-center/app/src/main/resources/application.properties +++ b/contact-center/app/src/main/resources/application.properties @@ -76,6 +76,7 @@ spring.datasource.password=123456 spring.jpa.properties.hibernate.current_session_context_class=org.springframework.orm.hibernate5.SpringSessionContext spring.jpa.show-sql=false spring.jpa.properties.hibernate.format_sql=true +spring.jpa.properties.hibernate.allow_update_outside_transaction=true ############################################## # Cache @@ -152,13 +153,19 @@ cskefu.modules.cca=true cskefu.modules.entim=false cskefu.modules.report=true +############################################## +# Channels +############################################## +channel.skype.crm= # https://gitlab.chatopera.com/chatopera/cosinee/issues/838 cskefu.settings.webim.visitor-separate=false ############################################## -# Skype Channel +# License ############################################## -channel.skype.crm= +# License Service Provider URL +license.store.provider=https://store.chatopera.com + ############################################## # Telemetry @@ -171,14 +178,4 @@ telemetry.channel.webim.visitor=on extras.login.banner=off extras.login.chatbox=off extras.auth.super-admin.pass= -extras.log.request=off - -spring.jpa.properties.hibernate.allow_update_outside_transaction=true - -############################################## -# ssl -############################################## -# server.ssl.key-store=classpath:cskefu.jks -# server.ssl.key-alias=cskefu -# server.ssl.key-store-password=123456 -# server.http2.enabled=true +extras.log.request=off \ No newline at end of file diff --git a/contact-center/app/src/main/resources/static/js/CSKeFu_Admin.v1.js b/contact-center/app/src/main/resources/static/js/CSKeFu_Admin.v1.js index 553c3f9d..0aac693b 100644 --- a/contact-center/app/src/main/resources/static/js/CSKeFu_Admin.v1.js +++ b/contact-center/app/src/main/resources/static/js/CSKeFu_Admin.v1.js @@ -11,50 +11,136 @@ * Licensed under the Apache License, Version 2.0 * http://www.apache.org/licenses/LICENSE-2.0 */ -function processUserAddOrUpdateResult(responsecode, cb){ +/** + * 处理系统用户的创建的返回值 + * @param responsecode + * @param cb + */ +function processUserAddOrUpdateResult(responsecode, cb) { switch (responsecode) { case 'username_exist': - layer.msg('用户名存在,请重新填写',{icon: 2, time: 3000}); + layer.msg('用户名存在,请重新填写', {icon: 2, time: 3000}); // 清空用户名 $('input[name="username"]').val(""); break; case 'email_exist': - layer.msg('邮件存在,请重新填写',{icon: 2, time: 3000}); + layer.msg('邮件存在,请重新填写', {icon: 2, time: 3000}); // 清空邮件 $('input[name="email"]').val(""); break; case 'mobile_exist': - layer.msg('手机存在,请重新填写',{icon: 2, time: 3000}); + layer.msg('手机存在,请重新填写', {icon: 2, time: 3000}); // 清空手机号 $('input[name="mobile"]').val(""); break; case 'sip_account_exist': - layer.msg('SIP地址已经存在,请重新填写',{icon: 2, time: 3000}); + layer.msg('SIP地址已经存在,请重新填写', {icon: 2, time: 3000}); // 清空SIP $('input[name="sipaccount"]').val(""); break; case 'extension_binded': - layer.msg('分机号已经被其他用户绑定',{icon: 2, time: 3000}); + layer.msg('分机号已经被其他用户绑定', {icon: 2, time: 3000}); $('input[name="extensionid"]').val(""); break; case 'extension_not_exist': - layer.msg('绑定分机不存在',{icon: 2, time: 3000}); + layer.msg('绑定分机不存在', {icon: 2, time: 3000}); $('input[name="extensionid"]').val(""); break; case 'pbxhost_not_exist': - layer.msg('指定的呼叫中心语音平台不存在',{icon: 2, time: 3000}); + layer.msg('指定的呼叫中心语音平台不存在', {icon: 2, time: 3000}); $('input[name="pbxhostid"]').val(""); break; case 't1': - layer.msg('当前用户坐席就绪或对话未结束,不能切换为非坐席',{icon: 2, time: 3000}); + layer.msg('当前用户坐席就绪或对话未结束,不能切换为非坐席', {icon: 2, time: 3000}); break; case 'new_user_success': - layer.msg('新用户创建成功',{icon: 1, time: 1000}); + layer.msg('新用户创建成功', {icon: 1, time: 1000}); cb(); break; case 'edit_user_success': - layer.msg('用户编辑成功',{icon: 1, time: 1000}); + layer.msg('用户编辑成功', {icon: 1, time: 1000}); cb(); break; + default: + handleGeneralCodeInQueryPathOrApiResp(responsecode, cb); + } +} + +/** + * 处理在 RedirectURL, API 中返回的 code 信息: status, msg, etc. + * code 为约定的返回值,通过下面的函数进行展示 + * @param code + * @param cb + */ +function handleGeneralCodeInQueryPathOrApiResp(code, cb) { + switch (code) { + case 'billingquotaexception.no_license_found': + layer.msg('【使用授权证书】证书不存在,联系系统超级管理员导入。', {icon: 2, time: 5000}); + if (cb && (typeof (x) === 'function')) { + cb() + } + break; + case 'billingquotaexception.response_unexpected': + layer.msg('【使用授权证书】证书商店返回异常,稍后再试。', {icon: 2, time: 5000}); + if (cb && (typeof (x) === 'function')) { + cb() + } + break; + case 'billingquotaexception.invalid_request_body': + layer.msg('【使用授权证书】请求证书商店参数不合法,请获取最新软件代码。', {icon: 2, time: 5000}); + if (cb && (typeof (x) === 'function')) { + cb() + } + break; + case 'billingquotaexception.license_invalid': + layer.msg('【使用授权证书】证书商店中不存在该证书,请联系系统超级管理员导入。', {icon: 2, time: 5000}); + if (cb && (typeof (x) === 'function')) { + cb() + } + break; + case 'billingquotaexception.product_invalid': + layer.msg('【使用授权证书】产品或产品款式不存在,请联系系统超级管理员导入新证书。', {icon: 2, time: 5000}); + if (cb && (typeof (x) === 'function')) { + cb() + } + break; + case 'billingquotaexception.license_expired_or_exhausted': + layer.msg('【使用授权证书】证书过期或耗尽,请升级证书或绑定新证书。', {icon: 2, time: 5000}); + if (cb && (typeof (x) === 'function')) { + cb() + } + break; + case 'billingquotaexception.license_disabled_serverinst': + layer.msg('【使用授权证书】证书商店禁用了本服务实例。', {icon: 2, time: 5000}); + if (cb && (typeof (x) === 'function')) { + cb() + } + break; + case 'billingquotaexception.license_unsupport_refund': + layer.msg('【使用授权证书】目前使用的证书不支持回退配额。', {icon: 2, time: 5000}); + if (cb && (typeof (x) === 'function')) { + cb() + } + break; + case 'billingquotaexception.license_quota_inadequate': + layer.msg('【使用授权证书】本次操作需要使用的配额资源超过证书中剩余配额,请联系系统超级管理员升级证书。', { + icon: 2, + time: 5000 + }); + if (cb && (typeof (x) === 'function')) { + cb() + } + break; + case 'billingquotaexception.internal_error': + layer.msg('【使用授权证书】系统错误,稍后再试。', {icon: 2, time: 5000}); + if (cb && (typeof (x) === 'function')) { + cb() + } + break; + default: + console.log("[handleGeneralCodeInQueryPathOrApiResp] none code matched", code); + if (cb && (typeof (x) === 'function')) { + cb("no_code_matched"); + } } } \ No newline at end of file diff --git a/contact-center/app/src/main/resources/static/js/CSKeFu_IM.v1.js b/contact-center/app/src/main/resources/static/js/CSKeFu_IM.v1.js index 24b22cca..e369db7c 100644 --- a/contact-center/app/src/main/resources/static/js/CSKeFu_IM.v1.js +++ b/contact-center/app/src/main/resources/static/js/CSKeFu_IM.v1.js @@ -11,196 +11,203 @@ * Licensed under the Apache License, Version 2.0 * http://www.apache.org/licenses/LICENSE-2.0 */ -var title = "春松客服-开源客服系统" ; -var socket , newuser = [] , newmessage = [] , ring = []; -newuser['mp3'] = '/images/new.mp3'; +var title = "春松客服-开源客服系统"; +var socket, newuser = [], newmessage = [], ring = []; +newuser['mp3'] = '/images/new.mp3'; newmessage['mp3'] = '/images/message.mp3'; ring['mp3'] = '/images/ring.mp3'; -$(document).ready(function(){ - var protocol = window.location.protocol.replace(/:/g,''); - socket = io(protocol+'://'+hostname+':'+port+"/im/agent?userid="+userid+"&session="+session+"&admin="+adminuser , {transports: ['websocket'], upgrade: false}); - socket.on('connect',function() { - console.log("[IM] 连接初始化成功"); - //请求服务端记录 当前用户在线事件 - }).on('disconnect',function() { - console.log("[IM] 连接已断开"); - //请求服务端记录,当前用户离线 +$(document).ready(function () { + var protocol = window.location.protocol.replace(/:/g, ''); + socket = io(protocol + '://' + hostname + ':' + port + "/im/agent?userid=" + userid + "&session=" + session + "&admin=" + adminuser, { + transports: ['websocket'], + upgrade: false + }); + socket.on('connect', function () { + console.log("[IM] 连接初始化成功"); + //请求服务端记录 当前用户在线事件 + }).on('disconnect', function () { + console.log("[IM] 连接已断开"); + //请求服务端记录,当前用户离线 }); - - socket.on('chatevent', function(data) { - // console.log(data.messageType + " ..... message:"+data.message); - }).on('task', function(data) { - - }).on('new', function(data) { - if($('#customerChatAudit').length > 0){ - if(customerChatAudit.$('#agentuser_' + data.userid).length > 0 && customerChatAudit.$("#chat_users li").length>1){ - customerChatAudit.$('#agentuser_' + data.userid).remove(); - customerChatAudit.$("#chat_users li:first-child a").click(); - }else{ - customerChatAudit.$('#ccaIndex').html("
\n" + - "
\n" + - "
\n" + - "
\n" + - " \n" + - "
还没有任何对话
\n" + - "
\n" + - "
\n" + - "
\n" + - "
"); - } - } - if($('#multiMediaDialogWin').length > 0 && multiMediaDialogWin != null && multiMediaDialogWin.$ &&multiMediaDialogWin.$('#agentusers').length > 0){ - multiMediaDialogWin.Proxy.newAgentUserService(data,"agent"); - }else{ - //来电弹屏 - $('#agentdesktop').attr('data-href' , '/agent/index.html?userid='+data.userid).click(); - WebIM.audioplayer('audioplane', newuser, false); // 播放 - } - }).on('status', function(data) { - $.post('/lazyAgentStatus').success(function(html){ - $('#agents_status').html(html); - }); - // $('#agents_status').html("服务中的人数:"+data.users+"人,当前排队人数:"+data.inquene+"人,在线坐席数:"+data.agents+"人,坐席忙:"+data.busy+"人"); - }).on('message', function(data) { - if($('#multiMediaDialogWin').length > 0 && multiMediaDialogWin != null && multiMediaDialogWin.$ && multiMediaDialogWin.$('#agentusers').length > 0){ - multiMediaDialogWin.Proxy.newAgentUserMessage(data,"agent"); - if(data.type == 'message'){ - WebIM.audioplayer('audioplane', newmessage, false); // 播放 - if(multiMediaDialogWin.isAisuggest && multiMediaDialogWin.isAisuggest == "true"){ - multiMediaDialogWin.Proxy.quickReply(data,"agent"); - } - } - }else{ - //来电弹屏 - $('#agentdesktop').attr('data-href' , '/agent/index.html?userid='+data.userid).click(); - } - }).on('workorder', function(data) { - }).on('transout', function(data){ - // TODO 坐席会话被转接出去 - if($('#multiMediaDialogWin').length > 0){ - if(multiMediaDialogWin.document.getElementById('agentusers') != null){ - multiMediaDialogWin.Proxy.transoutAgentUserService(data); - } - } - layer.msg("您与"+data.username+"的会话已被转接给"+data.agentname,{time:1500}) + socket.on('chatevent', function (data) { + // console.log(data.messageType + " ..... message:"+data.message); + }).on('task', function (data) { + }).on('new', function (data) { + console.log("new data ...", data); + if ($('#customerChatAudit').length > 0) { + if (customerChatAudit.$('#agentuser_' + data.userid).length > 0 && customerChatAudit.$("#chat_users li").length > 1) { + customerChatAudit.$('#agentuser_' + data.userid).remove(); + customerChatAudit.$("#chat_users li:first-child a").click(); + } else { + customerChatAudit.$('#ccaIndex').html("
\n" + + "
\n" + + "
\n" + + "
\n" + + " \n" + + "
还没有任何对话
\n" + + "
\n" + + "
\n" + + "
\n" + + "
"); + } + } - }).on('audit_message', function(data){ - // 会话监控:消息 - if($('#customerChatAudit').length > 0 && customerChatAudit != null && customerChatAudit.$ && customerChatAudit.$('#agentuserscca').length > 0){ - customerChatAudit.Proxy.newAgentUserMessage(data,"cca"); - if(data.type == 'message'){ - WebIM.audioplayer('audioplane', newmessage, false); // 播放 - if(customerChatAudit.isCcaAisuggest && customerChatAudit.isCcaAisuggest == "true"){ - customerChatAudit.Proxy.quickReply(data,"cca"); - } - } - } - }).on('audit_new', function(data){ - // 会话监控:新建 - if(skills.indexOf(data.skill)>-1 && $('#customerChatAudit').length > 0 && customerChatAudit != null && customerChatAudit.$){ - customerChatAudit.Proxy.newAgentUserService(data,"cca"); - if(data.type == 'message'){ - WebIM.audioplayer('audioplane', newmessage, false); // 播放 - } - } - }).on('audit_end', function(data){ - // 会话监控:结束 - if($('#customerChatAudit').length > 0){ - if(customerChatAudit.document.getElementById('agentuserscca') != null){ - customerChatAudit.Proxy.endAgentUserService(data); - } - } - }).on('end', function(data) { - console.warn(111111, data) - if($('#multiMediaDialogWin').length > 0){ - if(multiMediaDialogWin.document.getElementById('agentusers') != null){ - multiMediaDialogWin.Proxy.endAgentUserService(data); - } - }else{ - //来电弹屏 - $('#agentdesktop').attr('data-href', '/agent/index.html?userid='+data.userid).click(); - } - }).on('leave', function(data){ - top.layer.msg('当前会话已经过期,稍后将自动登出!',{icon: 1, time: 2000}); - setTimeout(function(){ - // 执行登出 - window.location.href = "/logout.html?code=2"; - }, 2000); - }); - /****每分钟执行一次,与服务器交互,保持会话****/ - setInterval(function(){ - WebIM.ping(); - } , 60000); + if ($('#multiMediaDialogWin').length > 0 && + multiMediaDialogWin != null && + multiMediaDialogWin.$ && + multiMediaDialogWin.$('#agentusers').length > 0) { + multiMediaDialogWin.Proxy.newAgentUserService(data, "agent"); + } else { + //来电弹屏 + $('#agentdesktop').attr('data-href', '/agent/index.html?userid=' + data.userid + '&licenseVerifiedPass=' + data.licenseVerifiedPass + '&licenseBillingMsg=' + data.licenseBillingMsg).click(); + WebIM.audioplayer('audioplane', newuser, false); // 播放 + } + }).on('status', function (data) { + $.post('/lazyAgentStatus').success(function (html) { + $('#agents_status').html(html); + }); + // $('#agents_status').html("服务中的人数:"+data.users+"人,当前排队人数:"+data.inquene+"人,在线坐席数:"+data.agents+"人,坐席忙:"+data.busy+"人"); + }).on('message', function (data) { + if ($('#multiMediaDialogWin').length > 0 && multiMediaDialogWin != null && multiMediaDialogWin.$ && multiMediaDialogWin.$('#agentusers').length > 0) { + multiMediaDialogWin.Proxy.newAgentUserMessage(data, "agent"); + if (data.type == 'message') { + WebIM.audioplayer('audioplane', newmessage, false); // 播放 + if (multiMediaDialogWin.isAisuggest && multiMediaDialogWin.isAisuggest == "true") { + multiMediaDialogWin.Proxy.quickReply(data, "agent"); + } + } + } else { + //来电弹屏 + $('#agentdesktop').attr('data-href', '/agent/index.html?userid=' + data.userid).click(); + } + }).on('workorder', function (data) { + + }).on('transout', function (data) { + // TODO 坐席会话被转接出去 + if ($('#multiMediaDialogWin').length > 0) { + if (multiMediaDialogWin.document.getElementById('agentusers') != null) { + multiMediaDialogWin.Proxy.transoutAgentUserService(data); + } + } + layer.msg("您与" + data.username + "的会话已被转接给" + data.agentname, {time: 1500}) + + }).on('audit_message', function (data) { + // 会话监控:消息 + if ($('#customerChatAudit').length > 0 && customerChatAudit != null && customerChatAudit.$ && customerChatAudit.$('#agentuserscca').length > 0) { + customerChatAudit.Proxy.newAgentUserMessage(data, "cca"); + if (data.type == 'message') { + WebIM.audioplayer('audioplane', newmessage, false); // 播放 + if (customerChatAudit.isCcaAisuggest && customerChatAudit.isCcaAisuggest == "true") { + customerChatAudit.Proxy.quickReply(data, "cca"); + } + } + } + }).on('audit_new', function (data) { + // 会话监控:新建 + if (skills.indexOf(data.skill) > -1 && $('#customerChatAudit').length > 0 && customerChatAudit != null && customerChatAudit.$) { + customerChatAudit.Proxy.newAgentUserService(data, "cca"); + if (data.type == 'message') { + WebIM.audioplayer('audioplane', newmessage, false); // 播放 + } + } + }).on('audit_end', function (data) { + // 会话监控:结束 + if ($('#customerChatAudit').length > 0) { + if (customerChatAudit.document.getElementById('agentuserscca') != null) { + customerChatAudit.Proxy.endAgentUserService(data); + } + } + }).on('end', function (data) { + console.warn(111111, data) + if ($('#multiMediaDialogWin').length > 0) { + if (multiMediaDialogWin.document.getElementById('agentusers') != null) { + multiMediaDialogWin.Proxy.endAgentUserService(data); + } + } else { + //来电弹屏 + $('#agentdesktop').attr('data-href', '/agent/index.html?userid=' + data.userid).click(); + } + }).on('leave', function (data) { + top.layer.msg('当前会话已经过期,稍后将自动登出!', {icon: 1, time: 2000}); + setTimeout(function () { + // 执行登出 + window.location.href = "/logout.html?code=2"; + }, 2000); + }); + /****每分钟执行一次,与服务器交互,保持会话****/ + setInterval(function () { + WebIM.ping(); + }, 60000); }); var WebIM = { - sendMessage:function(message , userid , appid , session , touser , agentstatus, agentuserid){ - WebIM.sendTypeMessage(message, userid, appid, session, touser, agentstatus, null , null,agentuserid) ; - }, - sendTypeMessage:function(message , userid , appid , session , touser , agentstatus , msgtype , attachmentid,agentuserid){ - socket.emit('message', { - appid : appid , - userid:userid, - sign:session, - touser:touser, - session: session, - username:agentstatus, - nickname:agentstatus, - message : message, - msgtype:msgtype, - attachmentid:attachmentid, - agentuser:agentuserid + sendMessage: function (message, userid, appid, session, touser, agentstatus, agentuserid) { + WebIM.sendTypeMessage(message, userid, appid, session, touser, agentstatus, null, null, agentuserid); + }, + sendTypeMessage: function (message, userid, appid, session, touser, agentstatus, msgtype, attachmentid, agentuserid) { + socket.emit('message', { + appid: appid, + userid: userid, + sign: session, + touser: touser, + session: session, + username: agentstatus, + nickname: agentstatus, + message: message, + msgtype: msgtype, + attachmentid: attachmentid, + agentuser: agentuserid }); - }, - // 发送会话监控干预消息 - sendIntervention: function(supervisorid, agentuserid, session, msgtype, content, extra){ - socket.emit('intervention', { - supervisorid: supervisorid, // 坐席监控人员ID,必须 - agentuserid: agentuserid, // 坐席访客会话ID,必须 - msgtype: msgtype, // 干预消息类型, 必须 - content: content, // 干预消息内容, 必须 - session: session, // 登录会话ID - extra: extra // 其它可选属性, 可选 - }); - }, - ping : function(){ - loadURL("/message/ping.html"); - console.log("[IM] heartbeat:" + new Date().getTime()); - }, - audioplayer:function(id, file, loop) { - var audioplayer = document.getElementById(id); - if (audioplayer != null) { - document.body.removeChild(audioplayer); - } + }, + // 发送会话监控干预消息 + sendIntervention: function (supervisorid, agentuserid, session, msgtype, content, extra) { + socket.emit('intervention', { + supervisorid: supervisorid, // 坐席监控人员ID,必须 + agentuserid: agentuserid, // 坐席访客会话ID,必须 + msgtype: msgtype, // 干预消息类型, 必须 + content: content, // 干预消息内容, 必须 + session: session, // 登录会话ID + extra: extra // 其它可选属性, 可选 + }); + }, + ping: function () { + loadURL("/message/ping.html"); + console.log("[IM] heartbeat:" + new Date().getTime()); + }, + audioplayer: function (id, file, loop) { + var audioplayer = document.getElementById(id); + if (audioplayer != null) { + document.body.removeChild(audioplayer); + } - if (typeof(file) != 'undefined') { - if (navigator.userAgent.indexOf("MSIE") > 0) { // IE - var player = document.createElement('bgsound'); - player.id = id; - player.src = file['mp3']; - player.setAttribute('autostart', 'true'); - if (loop) { - player.setAttribute('loop', 'infinite'); - } - document.body.appendChild(player); + if (typeof (file) != 'undefined') { + if (navigator.userAgent.indexOf("MSIE") > 0) { // IE + var player = document.createElement('bgsound'); + player.id = id; + player.src = file['mp3']; + player.setAttribute('autostart', 'true'); + if (loop) { + player.setAttribute('loop', 'infinite'); + } + document.body.appendChild(player); - } else { // Other FF Chome Safari Opera - var player = document.createElement('audio'); - player.id = id; - player.setAttribute('autoplay', 'autoplay'); - if (loop) { - player.setAttribute('loop', 'loop'); - } - document.body.appendChild(player); + } else { // Other FF Chome Safari Opera + var player = document.createElement('audio'); + player.id = id; + player.setAttribute('autoplay', 'autoplay'); + if (loop) { + player.setAttribute('loop', 'loop'); + } + document.body.appendChild(player); - var mp3 = document.createElement('source'); - mp3.src = file['mp3']; - mp3.type = 'audio/mpeg'; - player.appendChild(mp3); - } - } - } + var mp3 = document.createElement('source'); + mp3.src = file['mp3']; + mp3.type = 'audio/mpeg'; + player.appendChild(mp3); + } + } + } } diff --git a/contact-center/app/src/main/resources/static/js/cskefu.js b/contact-center/app/src/main/resources/static/js/cskefu.js index a0f17e02..0c01fc96 100644 --- a/contact-center/app/src/main/resources/static/js/cskefu.js +++ b/contact-center/app/src/main/resources/static/js/cskefu.js @@ -343,22 +343,23 @@ function newMessageScorllBottom(type, msgType) { var Proxy = { newAgentUserService: function (data, type) { + console.log("newAgentUserService data type", data, type) if ($('#tip_message_' + data.userid).length > 0) { var channel = data.channeltype if (channel) { if (channel === 'phone') { $('#tip_icon_phone_' + data.userid).attr("src", "/images/phone-ico.png"); } else { - $('#tip_icon_' +channel+ '_' + data.userid).removeClass('ukefu-channel-icon-end').addClass("ukefu-channel-icon"); + $('#tip_icon_' + channel + '_' + data.userid).removeClass('ukefu-channel-icon-end').addClass("ukefu-channel-icon"); } } $('#tip_message_' + data.userid).removeClass('bg-gray').addClass("bg-green").text('在线'); } else { if ($('.chat-list-item.active').length > 0) { var id = $('.chat-list-item.active').data('id'); - type == "agent" ? loadURL('/agent/agentusers.html?newuser=true&userid=' + id, '#agentusers') : loadURL('/apps/cca/agentusers.html?newuser=true&userid=' + id, '#agentuserscca'); + type == "agent" ? loadURL('/agent/agentusers.html?newuser=true&userid=' + id + '&licenseVerifiedPass=' + data.licenseVerifiedPass + '&licenseBillingMsg=' + data.licenseBillingMsg, '#agentusers') : loadURL('/apps/cca/agentusers.html?newuser=true&userid=' + id + "&licenseVerifiedPass=" + data.licenseVerifiedPass + "&licenseBillingMsg=" + data.licenseBillingMsg, '#agentuserscca'); } else { - type == "agent" ? location.href = "/agent/index.html?newuser=true" : location.href = "/apps/cca/index.html?newuser=true"; + type == "agent" ? location.href = "/agent/index.html?newuser=true&licenseVerifiedPass=" + data.licenseVerifiedPass + "&licenseBillingMsg=" + data.licenseBillingMsg : location.href = "/apps/cca/index.html?newuser=true&licenseVerifiedPass=" + data.licenseVerifiedPass + "&licenseBillingMsg=" + data.licenseBillingMsg; } } if (data.userid == cursession) { @@ -485,7 +486,7 @@ var Proxy = { if (channel === 'phone') { $('#tip_icon_phone_' + data.userid).attr("src", "/images/cde-ico-gray.png"); } else { - $('#tip_icon_'+ channel +'_' + data.userid).removeClass("ukefu-channel-icon").addClass('ukefu-channel-icon-end'); + $('#tip_icon_' + channel + '_' + data.userid).removeClass("ukefu-channel-icon").addClass('ukefu-channel-icon-end'); } } $('#tip_message_' + data.userid).removeClass("bg-green").addClass('bg-gray').text('离开'); @@ -504,7 +505,11 @@ var Proxy = { } }, tipMsgForm: function (href) { - top.layer.prompt({formType: 2, title: '请输入拉黑原因', area: ['300px', '50px']}, function (value, index, elem) { + top.layer.prompt({ + formType: 2, + title: '请输入拉黑原因', + area: ['300px', '50px'] + }, function (value, index, elem) { location.href = href + "&description=" + encodeURIComponent(value); top.layer.close(index); }); diff --git a/contact-center/app/src/main/resources/static/js/utils.js b/contact-center/app/src/main/resources/static/js/utils.js index 92ba115a..885427ab 100644 --- a/contact-center/app/src/main/resources/static/js/utils.js +++ b/contact-center/app/src/main/resources/static/js/utils.js @@ -312,6 +312,21 @@ var arrayListPrototype = { ArrayList.prototype = arrayListPrototype; +/** + * 复制值到系统粘贴板 + * https://www.freecodecamp.org/news/copy-text-to-clipboard-javascript/ + * @param val + */ +function copyValue2ClipboardOnOS(val, cb) { + navigator.clipboard.writeText(val).then(() => { + /* Resolved - text copied to clipboard successfully */ + if(cb && typeof cb === 'function') cb(); + },(err) => { + /* Rejected - text failed to copy to the clipboard */ + if(cb && typeof cb === 'function') cb(err || "Fail"); + }); +} + /*! Math.uuid.js (v1.4) diff --git a/contact-center/app/src/main/resources/templates/admin/channel/im/index.pug b/contact-center/app/src/main/resources/templates/admin/channel/im/index.pug index 6e47a370..1cc3695c 100644 --- a/contact-center/app/src/main/resources/templates/admin/channel/im/index.pug +++ b/contact-center/app/src/main/resources/templates/admin/channel/im/index.pug @@ -63,13 +63,13 @@ block content layui.use('layer', function () { var layer = layui.layer; - console.log(window.location.href) var status = '#{status}'; if (status == 'new_webim_success') layer.msg('网站添加成功', {icon: 1, time: 1000}) else if (status == 'new_webim_fail') layer.msg('网站添加失败', {icon: 2, time: 3000}) - + else + handleGeneralCodeInQueryPathOrApiResp(status); }); layui.use(['laypage', 'layer'], function () { var laypage = layui.laypage diff --git a/contact-center/app/src/main/resources/templates/admin/include/layout.pug b/contact-center/app/src/main/resources/templates/admin/include/layout.pug index 94d08136..6bd354e0 100644 --- a/contact-center/app/src/main/resources/templates/admin/include/layout.pug +++ b/contact-center/app/src/main/resources/templates/admin/include/layout.pug @@ -26,12 +26,14 @@ html(xmlns='http://www.w3.org/1999/xhtml', xmlns:th='http://www.thymeleaf.org', link(rel='stylesheet', href='/css/layui.css') link(rel='stylesheet', href='/res/css.html') link(rel='stylesheet', href='/css/flexboxgrid.min.css') + script(src='/js/utils.js') script(src='/js/jquery-1.10.2.min.js') script(src='/js/jquery.form.js') script(src='/js/ztree/jquery.ztree.all.min.js') script(src='/js/echarts.common.min.js') script(language='javascript', src='/js/theme/wonderland.js') script(src='/layui.js') + script(src="/js/CSKeFu_Admin.v1.js") script(src='/js/cskefu.js') if userExpTelemetry == 'on' script(src='https://www.googletagmanager.com/gtag/js?id=G-SBBX10RKTC' async) diff --git a/contact-center/app/src/main/resources/templates/admin/include/left.pug b/contact-center/app/src/main/resources/templates/admin/include/left.pug index 0b10bb6c..c8af840c 100644 --- a/contact-center/app/src/main/resources/templates/admin/include/left.pug +++ b/contact-center/app/src/main/resources/templates/admin/include/left.pug @@ -51,6 +51,13 @@ ul.layui-nav.layui-nav-tree(lay-filter='demo') dd(class={'layui-this': subtype == 'interf'}) a(href='/admin/weixin/interf.html') 接口管理 | –> + if user.superadmin + li.layui-nav-item.layui-nav-itemed + a.layui-nav-title(href='javascript:;') 人工智能 + dl.layui-nav-child + if models.contains("chatbot") && (user.roleAuthMap["A09"] || user.admin) + dd + a(href='javascript:void(0)',data-title="智能机器人",onclick="openChatbot()",data-href="/admin/system/chatbot/index.html",class="iframe_btn",data-id="chatbotIntegrationWin", data-type="tabAdd") 智能机器人 if user.superadmin li.layui-nav-item.layui-nav-itemed a.layui-nav-title(href='javascript:;') 系统设置 @@ -61,16 +68,20 @@ ul.layui-nav.layui-nav-tree(lay-filter='demo') a(href='/admin/sysdic/index.html') 字典管理 dd(class={'layui-this': subtype == 'metadata'}) a(href='/admin/metadata/index.html') 元数据 - if models.contains("chatbot") && (user.roleAuthMap["A09"] || user.admin) - dd - a(href='javascript:void(0)',data-title="智能机器人",onclick="openChatbot()",data-href="/admin/system/chatbot/index.html",class="iframe_btn",data-id="chatbotIntegrationWin", data-type="tabAdd") 智能机器人 dd(class={'layui-this': subtype == 'template'}) a(href='/admin/template/index.html') 系统模板 - dd(class={'layui-this': subtype == 'email'}) - a(href='/admin/email/index.html') 邮件通知设置 - dd(class={'layui-this': subtype == 'sms'}) - a(href='/admin/sms/index.html') 短信通知设置 - + //dd(class={'layui-this': subtype == 'email'}) + // a(href='/admin/email/index.html') 邮件通知设置 + //dd(class={'layui-this': subtype == 'sms'}) + // a(href='/admin/sms/index.html') 短信通知设置 + if user.superadmin + li.layui-nav-item.layui-nav-itemed + a.layui-nav-title(href='javascript:;') 使用授权 + dl.layui-nav-child + dd(class={'layui-this': subtype == 'licenseInst'}) + a(href='/admin/license/instance.html') 实例信息 + dd(class={'layui-this': subtype == 'licenseList'}) + a(href='/admin/license/index.html') 授权证书列表 script. function openChatbot() { window.parent.active.tabAdd($(".iframe_btn").data('href'), $(".iframe_btn").data('title'), $(".iframe_btn").data('id')); diff --git a/contact-center/app/src/main/resources/templates/admin/license/add.pug b/contact-center/app/src/main/resources/templates/admin/license/add.pug new file mode 100644 index 00000000..152451c1 --- /dev/null +++ b/contact-center/app/src/main/resources/templates/admin/license/add.pug @@ -0,0 +1,31 @@ +//- Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd. +//- , Licensed under the Chunsong Public +//- License, Version 1.0 (the "License"), https://docs.cskefu.com/licenses/v1.html +//- Unless required by applicable law or agreed to in writing, software +//- distributed under the License is distributed on an "AS IS" BASIS, +//- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +//- See the License for the specific language governing permissions and +//- limitations under the License. +.uk-layui-form + form.layui-form(action='/admin/license/save.html', method='post') + .layui-form-item(style='margin-top:10px;') + .layui-inline + label.layui-form-label(style='width:150px;') 证书标识: + .layui-input-inline + input.layui-input(type='text', name='licenseShortId', required, lay-verify='required', autocomplete='off') + .layui-form-mid.layui-word-aux + font(color='red') * + .layui-form-button + .layui-button-block + button.layui-btn(lay-submit, lay-filter='formNewLicense') 立即提交 + button.layui-btn.layui-btn-original(type='reset') 重置 + +script. + layui.use('form', function () { + var form = layui.form(); + form.render(); //更新全部 + }); + layui.use('element', function () { + var element = layui.element(); + }); + diff --git a/contact-center/app/src/main/resources/templates/admin/license/index.pug b/contact-center/app/src/main/resources/templates/admin/license/index.pug new file mode 100644 index 00000000..d76f6f90 --- /dev/null +++ b/contact-center/app/src/main/resources/templates/admin/license/index.pug @@ -0,0 +1,100 @@ +//- Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd. +//- , Licensed under the Chunsong Public +//- License, Version 1.0 (the "License"), https://docs.cskefu.com/licenses/v1.html +//- Unless required by applicable law or agreed to in writing, software +//- distributed under the License is distributed on an "AS IS" BASIS, +//- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +//- See the License for the specific language governing permissions and +//- limitations under the License. +extends /admin/include/layout.pug +block content + .row: .col-lg-12 + h1.site-h1(style='background-color:#FFFFFF;') + | 使用授权证书列表 (#{licenses.size()}) + | ,更新时间 #{pugHelper.formatDate('yyyy-MM-dd HH:mm:ss', updateTime)} + span(style='float:right;') + .layui-btn-group.ukefu-btn-group + button.layui-btn.layui-btn-small(href='/admin/license/add.html', data-toggle='ajax', data-width='550', data-height='450', data-title='添加使用授权证书') + i.layui-icon  + | 导入 + button.layui-btn.layui-btn-small(onclick='location.reload()') + span 刷新 + button.layui-btn.layui-btn-small(onclick='openLicenseStorePage()') + span 打开证书商店 + + .row(style='padding:5px;') + blockquote.layui-elem-quote.layui-quote-nm + i.layui-icon(style="color:gray")  + font(color="#999").layui-word-aux 春松客服使用授权证书是通过 Chatopera 证书商店(https://store.chatopera.com)分发的对【春松客服计费资源】进行管理的凭证,在使用春松客服的过程中,春松客服与 Chatopera 证书商店集成,完成证书购买、证书绑定、配额扣除、配额同步和开具发票等。 + + .col-lg-12 + table.layui-table(lay-skin='line') + colgroup + col(width='10%') + col(width='10%') + col(width='10%') + col(width='20%') + col(width='10%') + col(width='10%') + col(width='10%') + col(width='10%') + col(width='10%') + thead + tr + th 证书 ID + th 状态 + th 产品标识 + th 产品名称 + th 有效期截止 + th 配额剩余 + th 所属人昵称 + th 添加时间 + th(style='white-space:nowrap;', nowrap) 操作 + tbody + for item in licenses + tr + - var messupLicenseShortId = pugHelper.messupStringWithStars(item.license.shortId) + td= messupLicenseShortId + td= pugHelper.getLicstatusInChinese(item.license.status) + td= item.product.shortId + td= item.product.name + td= pugHelper.splitStringAndJoinWith(item.license.effectivedateend, " ", 1, "") + td= item.license.quotaeffectiveremaining + td= item.user.nickname + td= pugHelper.formatDate('yyyy-MM-dd', item.addDate) + td(style="white-space:nowrap;" nowrap="nowrap") + a(href="#", onclick="copyLicenseId2ClipboardOnOS('" + item.license.shortId + "');return false;") + i.layui-icon  + span 复制 ID + a(href="/admin/license/delete/" + item.license.shortId + ".html" style="margin-left:10px;" data-toggle="tip" title="请确认是否删除使用授权证书 " + messupLicenseShortId + "?") + i.layui-icon(style="color:red;") ဆ + span 删除 + .row(style='padding:5px;') + .col-lg-12#page(style='text-align:center;') + + script. + var msg = '#{msg}'; + if (msg == 'already_added') + top.layer.alert('已经添加,不需要再次执行。', {icon: 1}); + else if (msg == 'product_added_already') + top.layer.alert('同产品证书已经添加,不支持继续添加。', {icon: 2}); + else if (msg == 'invalid_id') + top.layer.alert('不合法的证书标识', {icon: 2}); + else if (msg == 'notfound_id') + top.layer.alert('不存在该证书信息', {icon: 2}); + + function copyLicenseId2ClipboardOnOS(val){ + copyValue2ClipboardOnOS(val, (err) => { + top.layer.msg('复制完成', {icon: 1, time: 2000, offset: 't'}); + }) + } + + function openLicenseStorePage() { + var licenseStoreProvider = "#{licenseStoreProvider}/product/cskefu001"; + window.open(licenseStoreProvider, "_blank"); + } + + layui.use(['laypage', 'layer'], function () { + var laypage = layui.laypage + , layer = layui.layer; + }); \ No newline at end of file diff --git a/contact-center/app/src/main/resources/templates/admin/license/instance.pug b/contact-center/app/src/main/resources/templates/admin/license/instance.pug new file mode 100644 index 00000000..7f03ecd4 --- /dev/null +++ b/contact-center/app/src/main/resources/templates/admin/license/instance.pug @@ -0,0 +1,59 @@ +//- Copyright (C) 2023 Beijing Huaxia Chunsong Technology Co., Ltd. +//- , Licensed under the Chunsong Public +//- License, Version 1.0 (the "License"), https://docs.cskefu.com/licenses/v1.html +//- Unless required by applicable law or agreed to in writing, software +//- distributed under the License is distributed on an "AS IS" BASIS, +//- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +//- See the License for the specific language governing permissions and +//- limitations under the License. +extends /admin/include/layout.pug +block content + .row: .col-lg-12 + h1.site-h1(style='background-color:#FFFFFF;') + | 实例信息 + span(style='float:right;') + .layui-btn-group.ukefu-btn-group + button.layui-btn.layui-btn-small(onclick='openLicenseStorePage()') + span 打开证书商店 + + .row(style='padding:5px;') + blockquote.layui-elem-quote.layui-quote-nm + i.layui-icon(style="color:gray")  + font(color="#999").layui-word-aux balala ... + + .col-lg-12 + fieldset.layui-elem-field + legend(style='font-size:13px!important;') 实例 ID + span.layui-field-box #{SERVERINSTID} + span    + button(style='font-size:10px!important;' onclick='copyServerinstId2ClipboardOnOS("' +SERVERINSTID + '")') + span 复制 + + fieldset.layui-elem-field + legend(style='font-size:13px!important;') 实例名称 + span.layui-field-box #{SERVICENAME} + span    + button(style='font-size:10px!important;' onclick='copyServicename2ClipboardOnOS("' +SERVICENAME + '")') + span 复制 + script. + layui.use(['laypage', 'layer'], function () { + var laypage = layui.laypage + , layer = layui.layer; + }); + + function copyServerinstId2ClipboardOnOS(val){ + copyValue2ClipboardOnOS(val, (err) => { + top.layer.msg('复制完成', {icon: 1, time: 2000, offset: 't'}); + }) + } + + function copyServicename2ClipboardOnOS(val){ + copyValue2ClipboardOnOS(val, (err) => { + top.layer.msg('复制完成', {icon: 1, time: 2000, offset: 't'}); + }) + } + + function openLicenseStorePage() { + var licenseStoreProvider = "#{licenseStoreProvider}"; + window.open(licenseStoreProvider, "_blank"); + } \ No newline at end of file diff --git a/contact-center/app/src/main/resources/templates/admin/organ/index.pug b/contact-center/app/src/main/resources/templates/admin/organ/index.pug index d057b2f9..a0e0965d 100644 --- a/contact-center/app/src/main/resources/templates/admin/organ/index.pug +++ b/contact-center/app/src/main/resources/templates/admin/organ/index.pug @@ -189,6 +189,8 @@ block content layer.msg('修改无法完成,上级机构选择错误', {icon: 2, time: 2000}) } else if (msg == 'not_allow_remove_user') { layer.msg('用户只有一个组织,不允许移除', {icon: 2, time: 2000}) + } else { + handleGeneralCodeInQueryPathOrApiResp(msg); } }); diff --git a/contact-center/app/src/main/resources/templates/admin/user/index.pug b/contact-center/app/src/main/resources/templates/admin/user/index.pug index c6641ab6..47ff38ea 100644 --- a/contact-center/app/src/main/resources/templates/admin/user/index.pug +++ b/contact-center/app/src/main/resources/templates/admin/user/index.pug @@ -121,6 +121,8 @@ block content layer.msg('新用户创建成功',{icon: 1, time: 1000}) else if (msg == 'edit_user_success') layer.msg('用户编辑成功', {icon: 1, time: 1000}) + else if (msg == 'billingquotaexception.no_license_found') + layer.msg('证书不存在,联系系统超级管理员导入授权使用证书',{icon: 2, time: 3000}) }); layui.use(['laypage', 'layer'], function(){ var laypage = layui.laypage diff --git a/contact-center/app/src/main/resources/templates/apps/agent/agentusers.pug b/contact-center/app/src/main/resources/templates/apps/agent/agentusers.pug index a4aee649..d1868ff8 100644 --- a/contact-center/app/src/main/resources/templates/apps/agent/agentusers.pug +++ b/contact-center/app/src/main/resources/templates/apps/agent/agentusers.pug @@ -80,3 +80,11 @@ .last-msg small.ukefu-badge.bg-red(id="last_msg_" + agentuser.userid,style="#{(agentuser.tokenum == 0 || (curagentuser && curagentuser.id == agentuser.id)) ? 'display:none' : ''}") | #{agentuser.tokenum ? agentuser.tokenum : 0} +script(language="javascript"). + $(document).ready(function () { + var licenseVerifiedPass = #{licenseVerifiedPass}; + var licenseBillingMsg = '#{licenseBillingMsg}'; + if (licenseBillingMsg) { + handleGeneralCodeInQueryPathOrApiResp(licenseBillingMsg); + } + }); \ No newline at end of file diff --git a/contact-center/app/src/main/resources/templates/apps/contacts/add.pug b/contact-center/app/src/main/resources/templates/apps/contacts/add.pug index b4357527..51afbf63 100644 --- a/contact-center/app/src/main/resources/templates/apps/contacts/add.pug +++ b/contact-center/app/src/main/resources/templates/apps/contacts/add.pug @@ -79,11 +79,6 @@ include /mixins/dic.mixin.pug label.layui-form-label 电子邮件: .layui-input-inline(style='margin-left:5px;') input.layui-input(type='text', name='email', lay-verify='entemail', autocomplete='off') - .layui-form-item - .layui-inline - label.layui-form-label#contacts_skypeid(style='widht:80px;') Skype ID: - .layui-input-inline - input#skypeid.layui-input(type='text', name='skypeid', lay-verify='skypeid', autocomplete='off') .layui-form-item .layui-inline label.layui-form-label 联系人地址: diff --git a/contact-center/app/src/main/resources/templates/apps/contacts/index.pug b/contact-center/app/src/main/resources/templates/apps/contacts/index.pug index 827a4365..5c74889e 100644 --- a/contact-center/app/src/main/resources/templates/apps/contacts/index.pug +++ b/contact-center/app/src/main/resources/templates/apps/contacts/index.pug @@ -103,14 +103,6 @@ block content td #{contacts.ckind && uKeFuDic[contacts.ckind] ? uKeFuDic[contacts.ckind].name : ""} td #{contacts.user ? contacts.user.username : ""} td - if approachable.contains(contacts.id) - a(href="#", onclick="openDialogWinByContactid('" + contacts.id + "')") - i.layui-icon  - | 聊天 - else - a.disabled(href="#", onclick="unreachableDialogWinByContactid('" + contacts.id + "')") - i.layui-icon  - | 聊天 a(href="/apps/contacts/detail.pug?id=" + (contacts.id ? contacts.id : ""), style="margin-left:10px;") i.layui-icon  | 详情 @@ -186,6 +178,8 @@ block content layer.msg('联系人编辑成功', {icon: 1, time: 1500}) else if (msg && msg == 'edit_contacts_fail') layer.msg('联系人编辑失败,因为存在相同Skype ID', {icon: 2, time: 1500}) + else + handleGeneralCodeInQueryPathOrApiResp(msg); }); }); diff --git a/contact-center/app/src/main/resources/templates/apps/include/layout.pug b/contact-center/app/src/main/resources/templates/apps/include/layout.pug index a051e3fc..e22e5ec2 100644 --- a/contact-center/app/src/main/resources/templates/apps/include/layout.pug +++ b/contact-center/app/src/main/resources/templates/apps/include/layout.pug @@ -36,6 +36,7 @@ html(xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xm script(src="/js/select/js/i18n/zh-CN.js") script(src="/layui.js") script(src="/js/cskefu.js") + script(src="/js/CSKeFu_Admin.v1.js") script(type="text/javascript" src="/js/kindeditor/kindeditor.js") script(type="text/javascript" src="/js/kindeditor/lang/zh-CN.js") script(type="text/javascript" src="/js/kindeditor-suggest.js") diff --git a/contact-center/app/src/main/resources/templates/apps/index.pug b/contact-center/app/src/main/resources/templates/apps/index.pug index 2cfe8656..a7378bbb 100644 --- a/contact-center/app/src/main/resources/templates/apps/index.pug +++ b/contact-center/app/src/main/resources/templates/apps/index.pug @@ -38,8 +38,6 @@ html(xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xm script(src="/js/moment-timezone.js") script(src="/js/moment-timezone-with-data.js") script(src="/layui.js") - //- #894 和原生Map行为不一致 - //- script(src="/js/cskefu.js") script(src="/im/js/socket.io.js") script(src="/js/CSKeFu_IM.v1.js") diff --git a/contact-center/config/sql/002.mysql-create-schemas.sql b/contact-center/config/sql/002.mysql-create-schemas.sql index 350fa3ac..ecce3082 100644 --- a/contact-center/config/sql/002.mysql-create-schemas.sql +++ b/contact-center/config/sql/002.mysql-create-schemas.sql @@ -138,6 +138,24 @@ CREATE TABLE `cs_fb_otn_follow` ( -- Records of cs_fb_otn_follow -- ---------------------------- +-- ---------------------------- +-- Table structure for cs_metakv +-- ---------------------------- +DROP TABLE IF EXISTS `cs_metakv`; +CREATE TABLE `cs_metakv` ( + `metakey` varchar(512) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '元数据字段名,唯一标识', + `metavalue` text COLLATE utf8mb4_unicode_ci COMMENT '元数据值', + `createtime` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updatetime` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `datatype` varchar(256) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'string' COMMENT '数据类型', + `comment` varchar(1024) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '字段备注描述', + PRIMARY KEY (`metakey`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='系统内置元数据'; + +-- ---------------------------- +-- Records of cs_metakv +-- ---------------------------- + -- ---------------------------- -- Table structure for cs_organ_user -- ---------------------------- @@ -2211,7 +2229,6 @@ CREATE TABLE `uk_organ` ( -- Records of uk_organ -- ---------------------------- INSERT INTO `uk_organ` VALUES ('2c9e80867d65eb5c017d65f17ceb0019', '售前坐席A组', null, null, null, null, null, null, '4028a0866f9403f1016f9405a05d000e', '1', ''); -INSERT INTO `uk_organ` VALUES ('40288296874ae16101874ae4f2670016', '机器人平台', null, null, null, null, null, null, '4028a0866f9403f1016f9405a05d000e', '0', ''); INSERT INTO `uk_organ` VALUES ('4028a0866f9403f1016f9405a05d000e', '我的企业', null, null, null, null, 'cskefu', null, '0', '0', ''); -- ---------------------------- diff --git a/contact-center/config/sql/upgrade/001.add_table_cs_metakey.sql b/contact-center/config/sql/upgrade/001.add_table_cs_metakey.sql new file mode 100644 index 00000000..9a99355c --- /dev/null +++ b/contact-center/config/sql/upgrade/001.add_table_cs_metakey.sql @@ -0,0 +1,14 @@ +USE `cosinee`; +-- ----------------- +-- prepare variables +-- ----------------- + +CREATE TABLE IF NOT EXISTS `cs_metakey` ( + `metakey` varchar(512) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '元数据字段名,唯一标识', + `metavalue` text COLLATE utf8mb4_unicode_ci COMMENT '元数据值', + `createtime` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updatetime` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `datatype` varchar(256) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'string' COMMENT '数据类型', + `comment` varchar(1024) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '字段备注描述', + PRIMARY KEY (`metakey`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='系统内置元数据'; diff --git a/contact-center/root/pom.xml b/contact-center/root/pom.xml index 81dc1ff9..08b85210 100644 --- a/contact-center/root/pom.xml +++ b/contact-center/root/pom.xml @@ -400,11 +400,18 @@ compose4j 1.0.0 + com.chatopera.bot sdk 3.5.1 + + + com.chatopera.store + store-sdk + 1.1-SNAPSHOT + diff --git a/docker-compose.yml b/docker-compose.yml index fd2cde82..aab29b9b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -42,6 +42,7 @@ services: - BOT_THRESHOLD_FAQ_BEST_REPLY=${BOT_THRESHOLD_FAQ_BEST_REPLY:-0.9} - BOT_THRESHOLD_FAQ_SUGG_REPLY=${BOT_THRESHOLD_FAQ_SUGG_REPLY:-0.1} - CSKEFU_SETTINGS_WEBIM_VISITOR_SEPARATE=true + - LICENSE_STORE_PROVIDER=${LICENSE_STORE_PROVIDER:-https://store.chatopera.com} - TONGJI_BAIDU_SITEKEY=${TONGJI_BAIDU_SITEKEY:-placeholder} - EXTRAS_LOGIN_BANNER=${NOTICE_LOGIN_BANNER:-off} - EXTRAS_LOGIN_CHATBOX=${EXTRAS_LOGIN_CHATBOX:-off} diff --git a/sample.env b/sample.env index e41e7d5d..fc40b8f4 100644 --- a/sample.env +++ b/sample.env @@ -30,6 +30,7 @@ CACHE_SETUP_STRATEGY=create_by_force BOT_THRESHOLD_FAQ_BEST_REPLY=0.8 BOT_THRESHOLD_FAQ_SUGG_REPLY=0.6 +LICENSE_STORE_PROVIDER=https://store.chatopera.com TONGJI_BAIDU_SITEKEY=placeholder EXTRAS_LOGIN_BANNER="" EXTRAS_LOGIN_CHATBOX=