Repository: wildfirechat/im-app_server
Branch: master
Commit: f06dc8460a5f
Files: 148
Total size: 341.2 KB
Directory structure:
gitextract_dfvped34/
├── .github/
│ └── workflows/
│ └── build.yml
├── .gitignore
├── LICENSE
├── README.md
├── aliyun_sms.md
├── build_release.sh
├── config/
│ ├── aliyun_sms.properties
│ ├── application.properties
│ ├── im.properties
│ └── tencent_sms.properties
├── deb/
│ └── control/
│ ├── control
│ ├── postinst
│ └── postrm
├── docker/
│ ├── Dockerfile
│ └── README.md
├── mvnw
├── mvnw.cmd
├── nginx/
│ └── appserver.conf
├── pom.xml
├── release_note.md
├── src/
│ ├── lib/
│ │ ├── DmDialect-for-hibernate5.4.jar
│ │ ├── DmJdbcDriver8.jar
│ │ ├── common-1.4.4.jar
│ │ └── sdk-1.4.4.jar
│ ├── main/
│ │ ├── java/
│ │ │ └── cn/
│ │ │ └── wildfirechat/
│ │ │ └── app/
│ │ │ ├── AppController.java
│ │ │ ├── Application.java
│ │ │ ├── AudioController.java
│ │ │ ├── ForbiddenException.java
│ │ │ ├── IMCallbackController.java
│ │ │ ├── IMConfig.java
│ │ │ ├── IMExceptionEventController.java
│ │ │ ├── RestResult.java
│ │ │ ├── Service.java
│ │ │ ├── ServiceImpl.java
│ │ │ ├── conference/
│ │ │ │ ├── ConferenceCleanupService.java
│ │ │ │ ├── ConferenceController.java
│ │ │ │ ├── ConferenceService.java
│ │ │ │ └── ConferenceServiceImpl.java
│ │ │ ├── jpa/
│ │ │ │ ├── Announcement.java
│ │ │ │ ├── AnnouncementRepository.java
│ │ │ │ ├── ConferenceEntity.java
│ │ │ │ ├── ConferenceEntityRepository.java
│ │ │ │ ├── ConferenceRecord.java
│ │ │ │ ├── ConferenceRecordRepository.java
│ │ │ │ ├── FavoriteItem.java
│ │ │ │ ├── FavoriteRepository.java
│ │ │ │ ├── PCSession.java
│ │ │ │ ├── PCSessionRepository.java
│ │ │ │ ├── Record.java
│ │ │ │ ├── RecordRepository.java
│ │ │ │ ├── ShiroSession.java
│ │ │ │ ├── ShiroSessionRepository.java
│ │ │ │ ├── SlideVerify.java
│ │ │ │ ├── SlideVerifyRepository.java
│ │ │ │ ├── UserConference.java
│ │ │ │ ├── UserConferenceQuota.java
│ │ │ │ ├── UserConferenceQuotaRepository.java
│ │ │ │ ├── UserConferenceRepository.java
│ │ │ │ ├── UserNameEntry.java
│ │ │ │ ├── UserNameRepository.java
│ │ │ │ ├── UserPassword.java
│ │ │ │ ├── UserPasswordRepository.java
│ │ │ │ ├── UserPrivateConferenceId.java
│ │ │ │ ├── UserPrivateConferenceIdRepository.java
│ │ │ │ ├── UserQuotaUsage.java
│ │ │ │ └── UserQuotaUsageRepository.java
│ │ │ ├── model/
│ │ │ │ └── ConferenceDTO.java
│ │ │ ├── pojo/
│ │ │ │ ├── CancelSessionRequest.java
│ │ │ │ ├── ChangeNameRequest.java
│ │ │ │ ├── ChangePasswordRequest.java
│ │ │ │ ├── ComplainRequest.java
│ │ │ │ ├── ConferenceInfo.java
│ │ │ │ ├── ConferenceInfoRequest.java
│ │ │ │ ├── ConferenceQuotaResponse.java
│ │ │ │ ├── ConfirmSessionRequest.java
│ │ │ │ ├── CreateSessionRequest.java
│ │ │ │ ├── DestroyRequest.java
│ │ │ │ ├── GroupAnnouncementPojo.java
│ │ │ │ ├── GroupIdPojo.java
│ │ │ │ ├── LoadFavoriteRequest.java
│ │ │ │ ├── LoadFavoriteResponse.java
│ │ │ │ ├── LoginResponse.java
│ │ │ │ ├── PhoneCodeLoginRequest.java
│ │ │ │ ├── PhoneCodeLoginRequestWithSlideVerify.java
│ │ │ │ ├── RecordingRequest.java
│ │ │ │ ├── ResetPasswordRequest.java
│ │ │ │ ├── SendCodeRequest.java
│ │ │ │ ├── SendCodeRequestWithSlideVerify.java
│ │ │ │ ├── SendDestroyCodeRequest.java
│ │ │ │ ├── SendMessageRequest.java
│ │ │ │ ├── SessionOutput.java
│ │ │ │ ├── SlideVerifyRequest.java
│ │ │ │ ├── SlideVerifyResponse.java
│ │ │ │ ├── UploadFileResponse.java
│ │ │ │ ├── UserIdNamePortraitPojo.java
│ │ │ │ ├── UserIdPojo.java
│ │ │ │ ├── UserPasswordLoginRequest.java
│ │ │ │ └── UserPasswordLoginRequestWithSlideVerify.java
│ │ │ ├── shiro/
│ │ │ │ ├── AuthDataSource.java
│ │ │ │ ├── CorsFilter.java
│ │ │ │ ├── DBSessionDao.java
│ │ │ │ ├── JsonAuthLoginFilter.java
│ │ │ │ ├── LdapMatcher.java
│ │ │ │ ├── LdapRealm.java
│ │ │ │ ├── LdapToken.java
│ │ │ │ ├── PhoneCodeRealm.java
│ │ │ │ ├── PhoneCodeToken.java
│ │ │ │ ├── ScanCodeRealm.java
│ │ │ │ ├── ShiroConfig.java
│ │ │ │ ├── ShiroSessionManager.java
│ │ │ │ ├── TokenAuthenticationToken.java
│ │ │ │ ├── TokenMatcher.java
│ │ │ │ └── UserPasswordRealm.java
│ │ │ ├── slide/
│ │ │ │ ├── SlideVerifyCleanupService.java
│ │ │ │ └── SlideVerifyService.java
│ │ │ ├── sms/
│ │ │ │ ├── AliyunSMSConfig.java
│ │ │ │ ├── SmsService.java
│ │ │ │ ├── SmsServiceImpl.java
│ │ │ │ └── TencentSMSConfig.java
│ │ │ └── tools/
│ │ │ ├── LdapUser.java
│ │ │ ├── LdapUtil.java
│ │ │ ├── NumericIdGenerator.java
│ │ │ ├── OrderedIdUserNameGenerator.java
│ │ │ ├── PhoneNumberUserNameGenerator.java
│ │ │ ├── RateLimiter.java
│ │ │ ├── ShortUUIDGenerator.java
│ │ │ ├── SpinLock.java
│ │ │ ├── UUIDUserNameGenerator.java
│ │ │ ├── UserNameGenerator.java
│ │ │ └── Utils.java
│ │ └── resources/
│ │ └── application.properties
│ └── test/
│ └── java/
│ └── cn/
│ └── wildfirechat/
│ └── app/
│ ├── ApplicationTests.java
│ ├── jpa/
│ │ ├── AnnouncementTest.java
│ │ ├── ConferenceEntityTest.java
│ │ ├── FavoriteItemTest.java
│ │ ├── PCSessionTest.java
│ │ ├── RecordTest.java
│ │ ├── ShiroSessionTest.java
│ │ ├── SlideVerifyRepositoryTest.java
│ │ ├── SlideVerifyTest.java
│ │ ├── UserConferenceTest.java
│ │ ├── UserNameEntryTest.java
│ │ ├── UserPasswordTest.java
│ │ └── UserPrivateConferenceIdTest.java
│ └── slide/
│ ├── SlideVerifyCleanupServiceTest.java
│ └── SlideVerifyServiceTest.java
└── systemd/
├── README.md
└── app-server.service
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/workflows/build.yml
================================================
# This workflow will build a Java project with Maven
# For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-maven
name: Java CI with Maven
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up JDK 1.8
uses: actions/setup-java@v1
with:
java-version: 1.8
- name: Build with Maven
run: mvn -B package --file pom.xml
================================================
FILE: .gitignore
================================================
target
.idea
appdata.mv.db
nohup.out
appdata.trace.db
avatar/
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2019 wildfirechat
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
1. ikidou/TypeBuilder
Copyright 2016 ikidou
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
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.
================================================
FILE: README.md
================================================
## 野火IM解决方案
野火IM是专业级即时通讯和实时音视频整体解决方案,由北京野火无限网络科技有限公司维护和支持。
主要特性有:私有部署安全可靠,性能强大,功能齐全,全平台支持,开源率高,部署运维简单,二次开发友好,方便与第三方系统对接或者嵌入现有系统中。详细情况请参考[在线文档](https://docs.wildfirechat.cn)。
主要包括一下项目:
| [GitHub仓库地址(主站)](https://github.com/wildfirechat) | [码云仓库地址(镜像)](https://gitee.com/wfchat) | 说明 | 备注 |
| ------------------------------------------------------------ | ----------------------------------------------------- | ----------------------------------------------------------------------------------------- | ---------------------------------------------- |
| [im-server](https://github.com/wildfirechat/im-server) | [im-server](https://gitee.com/wfchat/im-server) | IM Server | |
| [android-chat](https://github.com/wildfirechat/android-chat) | [android-chat](https://gitee.com/wfchat/android-chat) | 野火IM Android SDK源码和App源码 | 可以很方便地进行二次开发,或集成到现有应用当中 |
| [ios-chat](https://github.com/wildfirechat/ios-chat) | [ios-chat](https://gitee.com/wfchat/ios-chat) | 野火IM iOS SDK源码和App源码 | 可以很方便地进行二次开发,或集成到现有应用当中 |
| [pc-chat](https://github.com/wildfirechat/vue-pc-chat) | [pc-chat](https://gitee.com/wfchat/vue-pc-chat) | 基于[Electron](https://electronjs.org/)开发的PC 端 | |
| [web-chat](https://github.com/wildfirechat/vue-chat) | [web-chat](https://gitee.com/wfchat/vue-chat) | 野火IM Web 端, [体验地址](http://web.wildfirechat.cn) | |
| [wx-chat](https://github.com/wildfirechat/wx-chat) | [wx-chat](https://gitee.com/wfchat/wx-chat) | 小程序平台的Demo(支持微信、百度、阿里、字节、QQ 等小程序平台) | |
| [app server](https://github.com/wildfirechat/app_server) | [app server](https://gitee.com/wfchat/app_server) | 应用服务端 | |
| [robot_server](https://github.com/wildfirechat/robot_server) | [robot_server](https://gitee.com/wfchat/robot_server) | 机器人服务端 | |
| [push_server](https://github.com/wildfirechat/push_server) | [push_server](https://gitee.com/wfchat/push_server) | 推送服务器 | |
| [docs](https://github.com/wildfirechat/docs) | [docs](https://gitee.com/wfchat/docs) | 野火IM相关文档,包含设计、概念、开发、使用说明,[在线查看](https://docs.wildfirechat.cn/) | |
## 野火IM后端应用
作为野火IM的后端应用的演示,本工程具有如下功能:
1. 短信登陆和注册功能,用来演示登陆应用,获取token的场景.
2. PC端扫码登录的功能.
3. 群公告的获取和更新功能.
4. 客户端上传日志功能.
> 本工程为Demo工程,实际使用时需要把对应功能移植到您的应用服务中。如果需要直接使用,请按照后面的说明解决掉性能瓶颈问题。
#### 编译
```
mvn clean package
```
## 打包RPM格式
打包会生成Java包和deb安装包,如果需要rpm安装包,请在```pom.xml```中取消注释生成rpm包的plugin。另外还需要本地安装有rpm,在linux或者mac系统中很容易安装,在windows系统需要安装cygwin并安装rpm,具体信息请百度查询。
修改之后运行编译命令```mvn clean package```,rpm包生成在```target```目录下。
#### 短信资源
应用使用的是腾讯云短信功能,需要申请到```appid/appkey/templateId```这三个参数,并配置到```tencent_sms.properties```中去。用户也可以自行更换为自己喜欢的短信提供商。在没有短信供应商的情况下,为了测试可以使用```superCode```,设置好后,客户端可以直接使用```superCode```进行登陆。上线时一定要注意删掉```superCode```。
#### 修改配置
本演示服务有4个配置文件在工程的```config```目录下,分别是```application.properties```, ```im.properties```, ```aliyun_sms.properties```和```tencent_sms.properties```。请正确配置放到jar包所在的目录下的```config```目录下。
> ```application.properties```配置中的```sms.verdor```决定是使用那个短信服务商,1为腾讯短信,2为阿里云短信
#### 运行
在```target```目录找到```app-XXXX.jar```,把jar包和放置配置文件的```config```目录放到一起,然后执行下面命令:
```
java -jar app-XXXXX.jar
```
#### 性能瓶颈
本服务最早只提供获取token功能,后来逐渐增加了群公告/Shiro等功能,需要引入数据库。为了提高用户体验的便利性,引入了数据库[H2](http://www.h2database.com),让用户可以无需安装任何软件就可以直接运行(JRE还是需要的),另外shiro的session也存储在h2数据库中。提高了便利性的同时导致一方面性能有瓶颈,另外一方面也不能水平扩展和高可用。因此需要使用本工程上线必须修改2个地方。
1. 切换到MySQL,切换方法请参考 ```application.properties``` 文件中的描述。
2. 使用RedisSessionDao,详情请参考 https://www.baidu.com/s?wd=shiro+redis&tn=84053098_3_dg&ie=utf-8
3. 从0.53版本开始,应用服务改为无状态服务,可以集群部署。验证码和PC会话等信息都存放到数据库中,如果压力较大,可以二开引入redis缓存。
#### 版本兼容
+ 0.40版本引入了shiro功能,在升级本服务之前,需要确保客户端已经引入了本工程0.40版本发布时或之后的移动客户端。并且在升级之后,客户端需要退出重新登录一次以便保存session(退出登录时调用disconnect,需要使用false值,这样重新登录才能保留历史聊天记录,一定要在新版本中改成这样)。如果是旧版本或者没有重新登录,群公告和扫码登录功能将不可用。为了系统的安全性,建议升级。
+ 0.43版本把Web和PC登录的短轮询改为长轮询,如果应用服务升级需要对Web和PC进行对应修改。
+ 0.45.1 配置文件中添加了```wfc.all_client_support_ssl```开关,当升级到这个版本或之后时,需要配置文件中添加这个开关。
+ 0.51版本添加了token认证。可以同时支持token和cookies认证,客户端也做了对应修改,优先使用token。注意做好兼容。
+ 从0.53版本开始,所以数据都存储在数据库中,因此应用服务为无状态服务,可以部署多台应用服务做高可用和水平扩展。需要注意数据都是存储在数据库中,如果用户量较大或者业务量比较大,可以自己二开应用服务,添加redis缓存。
#### 使用LDAP统一认证
代码中添加了AD域登录的示例代码,可以在```application.properties```配置文件中打开ldap的配置并正确配置。需要ldap用户信息中,包含有电话号码。当登录时,先用电话号码查询到用户的dn,再用这个dn登录。如果遇到问题,请自己调试一下。调试时重点关注一下类```LdapMatcher```和```loginWithLdap```方法。
#### 修改其他登录方式
野火把登录功能从IM服务剥离,放到了应用服务中,目的是为了让客户更灵活的接入各种业务系统中进行登录。可以修改这个服务的任意代码,只要确保登录后从IM服务获取IM token返回给用户即可。
#### 注意事项
服务中对同一个IP的请求会有限频,默认是一个ip一小时可以请求200次,可以根据您的实际情况调整(搜索rateLimiter字符串就能找到)。如果使用了nginx做反向代理需要注意把用户真实ip传递过去(使用X-Real-IP或X-Forwarded-For),避免获取不到真实ip从而影响正常使用。
#### 使用到的开源代码
1. [TypeBuilder](https://github.com/ikidou/TypeBuilder) 一个用于生成泛型的简易Builder
#### LICENSE
UNDER MIT LICENSE. 详情见LICENSE文件
#### 使用阿里云短信
请参考说明[使用阿里云短信](./aliyun_sms.md)
================================================
FILE: aliyun_sms.md
================================================
# 阿里云短信功能说明
## 短信对接
1. 在[这里](https://usercenter.console.aliyun.com/#/manage/ak)申请阿里云***accessKeyId***和***accessSecret***
2. 开通短信服务,并申请短信签名和短信模版。注意申请短信签名和模版都是需要审核的,可以同时申请,以便节省您的时间
3. 修改```config```目录下的```aliyun_sms.properities```,填入上述四个参数。比如
```$xslt
alisms.accessKeyId=LTXXXXXXXXXXXXtW
alisms.accessSecret=4pXXXXXXXXXXXXXXXXXXXXXXXXXXXXyU
alisms.signName=野火IM
alisms.templateCode=SMS_170000000
```
4. 修改默认使用阿里云短信,在```application.properites```文件中修改```sms.vendor```为***2***
5. 运行测试。
> 上述几个参数如果不明白,可以参考[阿里云文档](https://help.aliyun.com/document_detail/55284.html?spm=a2c4e.11153987.0.0.5861aeecePRLPH)
## 迁移阿里云短信功能
指导如何把阿里云短信功能迁移到客户应用服务中
1. 引入jar包
```$xslt
com.aliyun
aliyun-java-sdk-core
4.1.0
```
2. 拷贝除了```Application.java```以外的所有源码到客户应用服务器.
3. 拷贝配置文件到客户应用服务,需要注意配置文件会依赖特定的路径,请放置正确的路径
================================================
FILE: build_release.sh
================================================
if [ $# -eq 0 ]; then
echo "Usage: sh build_release.sh version"
exit -1
fi
echo "build release $1"
mvn clean package
cd target
cp -af ../config ./
cp -af ../systemd ./
cp -af ../nginx ./
cp -af ../release_note.md ./
tar -czvf app-server-release-$1.tar.gz app-$1.jar config systemd release_note.md
cp app-server-release-$1.tar.gz app-server-release-latest.tar.gz
cp app-server-$1.rpm app-server-latest.rpm
cp app-server-$1.deb app-server-latest.deb
================================================
FILE: config/aliyun_sms.properties
================================================
alisms.accessKeyId=MTAI82gOTQQTuKtW
alisms.accessSecret=4p7HlgMTOQWHsX82IICabcea556677
alisms.signName=\u91CE\u706BIM
alisms.templateCode=SMS_170843232
================================================
FILE: config/application.properties
================================================
spring.message.encoding=UTF-8
server.port=8888
## 给服务添加统一的路径前缀,方便代理统一转换。
## 注意,如果这里改了,在客户端配置文件中修改APP_SERVER_ADDRESS,加上这个地址
#server.servlet.context-path=/wfapp_api
# 短信服务提供商,1是腾讯,2是阿里云
sms.verdor=1
# 在没有短信服务器时可以使用super code进行登录,上线时需要置为空(禁止超级验证码登录),或者改为较为严格的密码
# 但是不能直接把这一行直接删除,或者注释了
sms.super_code=66666
# json序列化时去掉为null的属性,避免iOS出现NSNull的问题
spring.jackson.default-property-inclusion=NON_NULL
# h2适合开发使用,上线时请切换到mysql。切换时把下面h2部分配置注释掉,打开mysql部署配置。
##*********************** h2 DB begin ***************************
spring.datasource.url=jdbc:h2:file:./appdata
spring.datasource.username=sa
spring.datasource.password=
spring.datasource.driver-class-name=org.h2.Driver
spring.jpa.hibernate.ddl-auto=update
##*********************** h2 DB end ******************************
# mysql默认配置
# mysql需要手动创建数据库,mysql命令行下执行 create database appdata; appdata可以换为别的库名,但注意不能使用IM服务器使用的数据库"wfchat",否则会引起冲突。
##*********************** mysql DB begin *************************
#spring.datasource.url=jdbc:mysql://localhost:3306/appdata?serverTimezone=UTC&allowPublicKeyRetrieval=true&useSSL=false
#spring.datasource.username=root
#spring.datasource.password=123456
#spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
#spring.jpa.database=mysql
#spring.jpa.hibernate.ddl-auto=update
## 遇到后面的报错时,请打开下面的注释:Storage engine MyISAM is disabled (Table creation is disallowed).
##spring.jpa.database-platform=org.hibernate.dialect.MySQL5InnoDBDialect
##*********************** mysql DB end ***************************
# 达梦数据库配置
# 达梦数据库需要手动创建数据库,使用达梦客户端工具创建数据库
##*********************** 达梦 DB begin *************************
#spring.datasource.url=jdbc:dm://localhost:5236?useUnicode=true&characterEncoding=utf-8
#spring.datasource.username=SYSDBA
#spring.datasource.password=SYSDBA001
#spring.datasource.driver-class-name=dm.jdbc.driver.DmDriver
#spring.jpa.hibernate.ddl-auto=update
#spring.jpa.show-sql=true
#spring.jpa.properties.hibernate.format_sql=true
#spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.DmDialect
##*********************** 达梦 DB end ***************************
# ldap登录的配置
## 是否开启ldap登录。开启后普通密码登录就失效了,只能用ldap登录。另外客户端代码也需要修改一下,去掉短信登录和修改密码等相关代码。
ldap.enable=false
ldap.admin_dn=cn=admin,dc=wildfirechat,dc=net
ldap.admin_password=123456@abc
ldap.ldap_url=ldap://192.168.1.48:389
ldap.search_base=dc=wildfirechat,dc=net
# PC快速登录兼容旧的版本。仅当已经有未支持PC快速登录的移动端上线了,需要兼容时打开此开关。
wfc.compat_pc_quick_login=false
# 用户上传协议日志存放目录,上线时请修改可用路径
logs.user_logs_path=/Users/imhao/wildfire_user_logs/
# *************************** 上线必看 *********************************
# demo工程为了方便大家运行测试,使用了数据库作为SessionDao的缓存,上线后,当用户较多时会是一个瓶颈,请在上线前切换成redis的缓存。
# 细节请参考 https://www.baidu.com/s?wd=shiro+redis&tn=84053098_3_dg&ie=utf-8
# 小程序不能播放amr格式的音频,需要将amr转化成mp3格式
# amr转mp3缓存目录,本目录会存储转换后的mp3文件,可以定时清理
wfc.audio.cache.dir=/data/wfc/audio/cache
# 是否支持SSL,如果所有客户端调用appserver都支持https,请把下面开关设置为true,否则为false。
# 如果为false,在Web端和wx端的appserve的群公告等功能将不可用。
# 详情请参考 https://www.baidu.com/s?wd=cookie+SameSite&ie=utf-8
wfc.all_client_support_ssl=false
## 是否添加用户默认密码。可以开启此配置,使用手机号码的后六位作为初始密码。首次登录之后必须修改密码。其他情况不用打开此开关。
## 用户设置密码时,不能设置为手机号码的后6位
wfc.default_user_password=false
## 是否禁止用户注册。默认为false(允许注册)。
## 当设置为true时,如果登录时用户不存在,不会自动创建用户,而是返回用户不存在的错误
wfc.user_register_forbidden=true
## iOS系统使用share extension来处理分享,客户端无法调用SDK发送消息和文件,只能通过应用服务来进行。
## 这里配置为了满足iOS设备在share extension中进行上传文件的需求。
## 存储使用类型,0使用内置文件服务器(这里无法使用),1使用七牛云存储,2使用阿里云对象存储,3野火私有对象存储,
## 4野火对象存储网关(当使用4时,需要处理 uploadMedia和putFavoriteItem方法),5腾讯云存储。
## 默认的七牛/阿里OSS/野火私有存储账户信息不可用,请按照下面说明配置
## https://docs.wildfirechat.cn/server/oss.html
media.server.media_type=1
## 滑动验证配置
## 是否强制开启滑动验证(true=强制要求滑动验证,false=可选滑动验证)
## true: 发送验证码和登录都强制要求滑动验证
## false: 发送验证码和登录时,如果客户端传了slideVerifyToken就验证,没传就不验证
slide.verify.force=false
# 使用这个目录作为临时目录,必须配置有效目录。
local.media.temp_storage=/Users/imhao/wildfire_upload_tmp/
## OSS配置,可以是七牛/阿里云OSS/野火私有OSS。
## 注意与IM服务的配置格式不太一样,这里是用"Key=Vaue"的格式,IM服务配置里是"Key Value",拷贝粘贴时要注意修改。
## 配置请参考IM服务
## 下面是七牛云的示例,如果是腾讯云或者阿里云,server_url应该是 cos.ap-nanjing.myqcloud.com 或 oss-cn-beijing.aliyuncs.com 这样。
media.server_url=http://up.qbox.me
media.access_key=tU3vdBK5BL5j4N7jI5N5uZgq_HQDo170w5C9Amnn
media.secret_key=YfQIJdgp5YGhwEw14vGpaD2HJZsuJldWtqens7i5
## bucket名字及Domain
media.bucket_general_name=media
media.bucket_general_domain=http://cdn.wildfirechat.cn
media.bucket_image_name=media
media.bucket_image_domain=http://cdn.wildfirechat.cn
media.bucket_voice_name=media
media.bucket_voice_domain=http://cdn.wildfirechat.cn
media.bucket_video_name=media
media.bucket_video_domain=http://cdn.wildfirechat.cn
media.bucket_file_name=media
media.bucket_file_domain=http://cdn.wildfirechat.cn
media.bucket_sticker_name=media
media.bucket_sticker_domain=http://cdn.wildfirechat.cn
media.bucket_moments_name=media
media.bucket_moments_domain=http://cdn.wildfirechat.cn
media.bucket_portrait_name=storage
media.bucket_portrait_domain=http://cdn2.wildfirechat.cn
media.bucket_favorite_name=storage
media.bucket_favorite_domain=http://cdn2.wildfirechat.cn
# 报警发送邮件配置
# 当IM服务异常时,会把异常信息推送到应用服务,由应用服务来给运维人员发送邮件,建议上线时调通次功能
spring.mail.host=smtp.wildfirechat.com
spring.mail.username=admin@wildfirechat.cn
# 注意有些邮件服务商会提供客户端授权码,不能用邮箱账户密码。
spring.mail.password=xxxxxxxx
spring.mail.port=465
spring.mail.protocol=smtp
spring.mail.default-encoding=UTF-8
spring.mail.test-connection=false
spring.mail.properties.mail.smtp.auth=true
spring.mail.properties.mail.smtp.ssl.enable=true
spring.mail.properties.mail.smtp.starttls.enable=true
spring.mail.properties.mail.smtp.starttls.required=true
spring.mail.properties.mail.imap.ssl.socketFactory.fallback=false
# 邮箱必须是有效邮箱,如果是无效邮箱可能会发送失败
spring.mail.to_lists=admin1@wildfirechat.cn,admin2@wildfirechat.cn,admin3@wildfirechat.cn
# 头像背景颜色可选列表,逗号分隔,中间不能有空格
avatar.bg.corlors=#D32F2F,#D81B60,#880E4F,#9C27B0,#6A1B9A,#4A148C,#AA00FF,#C51162,#673AB7,#311B92,#651FFF,#5C6BC0,#283593,#1A237E,#304FFE,#1976D2,#0D47A1,#2962FF,#0D47A1,#0277BD,#01579B
# 会议配额配置:默认会议额度(分钟),当用户没有单独配置配额时使用。0表示不限制
conference.default_quota_minutes=0
================================================
FILE: config/im.properties
================================================
im.admin_url=http://localhost:18080
#需要和im server里面配置的http.admin.secret_key一致
im.admin_secret=123456
#发送通知消息的管理员用户ID
im.admin_user_id=admin
#如果发送消息为乱码,请检查服务器是否支持中文
#如果您不需要登录欢迎消息,请删掉下面两行
im.welcome_for_new_user=欢迎您的加入!(如果您自己部署的这个服务,可以在应用服务的配置文件im.properties文件中修改这条欢迎语)
im.welcome_for_back_user=欢迎您的归来!。(如果您自己部署的这个服务,可以在应用服务的配置文件im.properties文件中修改这条欢迎语)
#是否使用随机用户名
im.use_random_name=true
# 新用户注册时,自动添加机器人为好友。这里可以修改为添加销售人员id,或者客服人员id,用户跟客户进行沟通。
# 如果不需要要设置机器人为好友和发送欢迎信息,请删除下面三行
im.new_user_robot_friend=true
im.robot_friend_id=FireRobot
im.robot_welcome=您好,我是人见人爱、花见花开、天下第一帅的机器人小火!可以跟我聊天哦!\n\n也可以给我打音视频电话,我现在可以接听电话了,我会延迟三秒播放您的声音。\n\n给我发送 流式文本 四个字,我会像 ChatGPT 那样采用流式输出的方式回复你。
# 用户登录后发送广告语,一条文本消息,再加上一条图片消息。如果为空就不发送。
im.prompt_text=
im.image_msg_url=
im.image_msg_base64_thumbnail=
# 新用户注册时,自动关注频道,频道ID不能加双引号。如果不需要关注,下面配置内容设置为空就OK了。
# im.new_user_subscribe_channel_id=vwzqmws2k
im.new_user_subscribe_channel_id=
# 用户再次登陆时,自动关注频道,频道ID不能加双引号。如果不需要关注,下面配置内容设置为空就OK了。
im.back_user_subscribe_channel_id=
================================================
FILE: config/tencent_sms.properties
================================================
sms.secretId=AKIsaepMSEL91dsMESAUMO2smphIdgSxB8oD
sms.secretKey=91dADocdksuw23AEFCD78lsdudf35ta0
sms.appId=1432000001
sms.templateId=592276
sms.sign=北京野火无限网络科技
================================================
FILE: deb/control/control
================================================
Package: app-server
Version: [[version]]
Section: misc
Priority: optional
Architecture: all
Maintainer: Wildfirechat
Description: App Server
Distribution: development
Depends: openjdk-8-jre-headless
Homepage: https://wildfirechat.cn
================================================
FILE: deb/control/postinst
================================================
mv -f /opt/app-server/app-*.jar /opt/app-server/app-server.jar
systemctl daemon-reload
================================================
FILE: deb/control/postrm
================================================
rm -rf /opt/app-server
rm -rf /usr/lib/systemd/system/app-server.service
systemctl daemon-reload
================================================
FILE: docker/Dockerfile
================================================
FROM openjdk:8-jre-alpine
COPY ../target/app-*.jar /opt/app-server/app.jar
COPY ../config /opt/app-server/config
WORKDIR /opt/app-server
VOLUME /opt/app-server/config
VOLUME /opt/app-server/h2db
EXPOSE 8888/tcp
ENV JVM_XMX 256M
ENV JVM_XMS 256M
CMD java -server -Xmx$JVM_XMX -Xms$JVM_XMS -jar app.jar
================================================
FILE: docker/README.md
================================================
# 野火应用服务docker使用说明
## 编译镜像
首先需要先编译应用服务,使用下面命令编译
```
mvn clean package
```
然后进入到docker目录编译镜像
```
sudo docker build -t app-server -f Dockerfile ..
```
## 运行
直接运行:
```
sudo docker run -it -p 8888:8888 -e JVM_XMX=256M -e JVM_XMS=256M app-server
```
配置:
如果配置需要修改,可以修改config目录下的配置,然后重新打包镜像,也可以手动指定配置目录,这样不用重新打包镜像。手动指定配置目录的方法如下,注意路径需要绝对路径
```
sudo docker run -it -v $PATH_TO_CONFIG:/opt/app-server/config -v $PATH_TO_H2DB:/opt/app-server/h2db -e JVM_XMX=256M -e JVM_XMS=256M -p 8888:8888 app-server
```
================================================
FILE: mvnw
================================================
#!/bin/sh
# ----------------------------------------------------------------------------
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# 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.
# ----------------------------------------------------------------------------
# ----------------------------------------------------------------------------
# Maven2 Start Up Batch script
#
# Required ENV vars:
# ------------------
# JAVA_HOME - location of a JDK home dir
#
# Optional ENV vars
# -----------------
# M2_HOME - location of maven2's installed home dir
# MAVEN_OPTS - parameters passed to the Java VM when running Maven
# e.g. to debug Maven itself, use
# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
# MAVEN_SKIP_RC - flag to disable loading of mavenrc files
# ----------------------------------------------------------------------------
if [ -z "$MAVEN_SKIP_RC" ] ; then
if [ -f /etc/mavenrc ] ; then
. /etc/mavenrc
fi
if [ -f "$HOME/.mavenrc" ] ; then
. "$HOME/.mavenrc"
fi
fi
# OS specific support. $var _must_ be set to either true or false.
cygwin=false;
darwin=false;
mingw=false
case "`uname`" in
CYGWIN*) cygwin=true ;;
MINGW*) mingw=true;;
Darwin*) darwin=true
# Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home
# See https://developer.apple.com/library/mac/qa/qa1170/_index.html
if [ -z "$JAVA_HOME" ]; then
if [ -x "/usr/libexec/java_home" ]; then
export JAVA_HOME="`/usr/libexec/java_home`"
else
export JAVA_HOME="/Library/Java/Home"
fi
fi
;;
esac
if [ -z "$JAVA_HOME" ] ; then
if [ -r /etc/gentoo-release ] ; then
JAVA_HOME=`java-config --jre-home`
fi
fi
if [ -z "$M2_HOME" ] ; then
## resolve links - $0 may be a link to maven's home
PRG="$0"
# need this for relative symlinks
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG="`dirname "$PRG"`/$link"
fi
done
saveddir=`pwd`
M2_HOME=`dirname "$PRG"`/..
# make it fully qualified
M2_HOME=`cd "$M2_HOME" && pwd`
cd "$saveddir"
# echo Using m2 at $M2_HOME
fi
# For Cygwin, ensure paths are in UNIX format before anything is touched
if $cygwin ; then
[ -n "$M2_HOME" ] &&
M2_HOME=`cygpath --unix "$M2_HOME"`
[ -n "$JAVA_HOME" ] &&
JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
[ -n "$CLASSPATH" ] &&
CLASSPATH=`cygpath --path --unix "$CLASSPATH"`
fi
# For Migwn, ensure paths are in UNIX format before anything is touched
if $mingw ; then
[ -n "$M2_HOME" ] &&
M2_HOME="`(cd "$M2_HOME"; pwd)`"
[ -n "$JAVA_HOME" ] &&
JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`"
# TODO classpath?
fi
if [ -z "$JAVA_HOME" ]; then
javaExecutable="`which javac`"
if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then
# readlink(1) is not available as standard on Solaris 10.
readLink=`which readlink`
if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then
if $darwin ; then
javaHome="`dirname \"$javaExecutable\"`"
javaExecutable="`cd \"$javaHome\" && pwd -P`/javac"
else
javaExecutable="`readlink -f \"$javaExecutable\"`"
fi
javaHome="`dirname \"$javaExecutable\"`"
javaHome=`expr "$javaHome" : '\(.*\)/bin'`
JAVA_HOME="$javaHome"
export JAVA_HOME
fi
fi
fi
if [ -z "$JAVACMD" ] ; then
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
else
JAVACMD="`which java`"
fi
fi
if [ ! -x "$JAVACMD" ] ; then
echo "Error: JAVA_HOME is not defined correctly." >&2
echo " We cannot execute $JAVACMD" >&2
exit 1
fi
if [ -z "$JAVA_HOME" ] ; then
echo "Warning: JAVA_HOME environment variable is not set."
fi
CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher
# traverses directory structure from process work directory to filesystem root
# first directory with .mvn subdirectory is considered project base directory
find_maven_basedir() {
if [ -z "$1" ]
then
echo "Path not specified to find_maven_basedir"
return 1
fi
basedir="$1"
wdir="$1"
while [ "$wdir" != '/' ] ; do
if [ -d "$wdir"/.mvn ] ; then
basedir=$wdir
break
fi
# workaround for JBEAP-8937 (on Solaris 10/Sparc)
if [ -d "${wdir}" ]; then
wdir=`cd "$wdir/.."; pwd`
fi
# end of workaround
done
echo "${basedir}"
}
# concatenates all lines of a file
concat_lines() {
if [ -f "$1" ]; then
echo "$(tr -s '\n' ' ' < "$1")"
fi
}
BASE_DIR=`find_maven_basedir "$(pwd)"`
if [ -z "$BASE_DIR" ]; then
exit 1;
fi
export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}
echo $MAVEN_PROJECTBASEDIR
MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS"
# For Cygwin, switch paths to Windows format before running java
if $cygwin; then
[ -n "$M2_HOME" ] &&
M2_HOME=`cygpath --path --windows "$M2_HOME"`
[ -n "$JAVA_HOME" ] &&
JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"`
[ -n "$CLASSPATH" ] &&
CLASSPATH=`cygpath --path --windows "$CLASSPATH"`
[ -n "$MAVEN_PROJECTBASEDIR" ] &&
MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"`
fi
WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
exec "$JAVACMD" \
$MAVEN_OPTS \
-classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \
"-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \
${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@"
================================================
FILE: mvnw.cmd
================================================
@REM ----------------------------------------------------------------------------
@REM Licensed to the Apache Software Foundation (ASF) under one
@REM or more contributor license agreements. See the NOTICE file
@REM distributed with this work for additional information
@REM regarding copyright ownership. The ASF licenses this file
@REM to you under the Apache License, Version 2.0 (the
@REM "License"); you may not use this file except in compliance
@REM with the License. You may obtain a copy of the License at
@REM
@REM http://www.apache.org/licenses/LICENSE-2.0
@REM
@REM Unless required by applicable law or agreed to in writing,
@REM software distributed under the License is distributed on an
@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
@REM KIND, either express or implied. See the License for the
@REM specific language governing permissions and limitations
@REM under the License.
@REM ----------------------------------------------------------------------------
@REM ----------------------------------------------------------------------------
@REM Maven2 Start Up Batch script
@REM
@REM Required ENV vars:
@REM JAVA_HOME - location of a JDK home dir
@REM
@REM Optional ENV vars
@REM M2_HOME - location of maven2's installed home dir
@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands
@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending
@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven
@REM e.g. to debug Maven itself, use
@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files
@REM ----------------------------------------------------------------------------
@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on'
@echo off
@REM enable echoing my setting MAVEN_BATCH_ECHO to 'on'
@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO%
@REM set %HOME% to equivalent of $HOME
if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%")
@REM Execute a user defined script before this one
if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre
@REM check for pre script, once with legacy .bat ending and once with .cmd ending
if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat"
if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd"
:skipRcPre
@setlocal
set ERROR_CODE=0
@REM To isolate internal variables from possible post scripts, we use another setlocal
@setlocal
@REM ==== START VALIDATION ====
if not "%JAVA_HOME%" == "" goto OkJHome
echo.
echo Error: JAVA_HOME not found in your environment. >&2
echo Please set the JAVA_HOME variable in your environment to match the >&2
echo location of your Java installation. >&2
echo.
goto error
:OkJHome
if exist "%JAVA_HOME%\bin\java.exe" goto init
echo.
echo Error: JAVA_HOME is set to an invalid directory. >&2
echo JAVA_HOME = "%JAVA_HOME%" >&2
echo Please set the JAVA_HOME variable in your environment to match the >&2
echo location of your Java installation. >&2
echo.
goto error
@REM ==== END VALIDATION ====
:init
@REM Find the project base dir, i.e. the directory that contains the folder ".mvn".
@REM Fallback to current working directory if not found.
set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR%
IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir
set EXEC_DIR=%CD%
set WDIR=%EXEC_DIR%
:findBaseDir
IF EXIST "%WDIR%"\.mvn goto baseDirFound
cd ..
IF "%WDIR%"=="%CD%" goto baseDirNotFound
set WDIR=%CD%
goto findBaseDir
:baseDirFound
set MAVEN_PROJECTBASEDIR=%WDIR%
cd "%EXEC_DIR%"
goto endDetectBaseDir
:baseDirNotFound
set MAVEN_PROJECTBASEDIR=%EXEC_DIR%
cd "%EXEC_DIR%"
:endDetectBaseDir
IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig
@setlocal EnableExtensions EnableDelayedExpansion
for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a
@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS%
:endReadAdditionalConfig
SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe"
set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar"
set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %*
if ERRORLEVEL 1 goto error
goto end
:error
set ERROR_CODE=1
:end
@endlocal & set ERROR_CODE=%ERROR_CODE%
if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost
@REM check for post script, once with legacy .bat ending and once with .cmd ending
if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat"
if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd"
:skipRcPost
@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on'
if "%MAVEN_BATCH_PAUSE%" == "on" pause
if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE%
exit /B %ERROR_CODE%
================================================
FILE: nginx/appserver.conf
================================================
server {
listen 80;
server_name apptest.wildfirechat.cn;
rewrite ^(.*)$ https://apptest.wildfirechat.cn permanent;
location ~ / {
index index.html index.php index.htm;
}
}
server {
listen 443 ssl;
server_name apptest.wildfirechat.cn;
root html;
index index.html index.htm;
client_max_body_size 30m; #文件最大大小
ssl_certificate cert/app.pem;
ssl_certificate_key cert/app.key;
ssl_session_timeout 5m;
ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE:ECDH:AES:HIGH:!NULL:!aNULL:!MD5:!ADH:!RC4;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_prefer_server_ciphers on;
## 不需要添加 add_header Access-Control-Allow-Origin $http_origin; 等添加跨域相关 header 的配置,app-server 已经处理了跨域了,所有请求透传过去即可
##
## send request back to app server ##
location / {
# 扫码超时时间是 1 分钟,配置了大于一分钟
proxy_read_timeout 100s;
proxy_pass http://127.0.0.1:8888;
}
## 如果需要通过 path 来分流的话,请参考下的配置,path后面的/和 8888 后面的/ 都不能省略,否则会提示 没有登录
# 可参考这儿:https://www.jb51.net/article/244331.htm
#location /app/ {
# proxy_pass http://127.0.0.1:8888/;
#}
}
================================================
FILE: pom.xml
================================================
4.0.0
cn.wildfirechat
app
0.72
jar
app
Demo project for Wildfire chat app server
org.springframework.boot
spring-boot-starter-parent
2.2.10.RELEASE
UTF-8
UTF-8
1.8
2.17.2
1.4.4
org.springframework.boot
spring-boot-starter
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-test
test
org.springframework.boot
spring-boot-starter-data-rest
org.springframework.boot
spring-boot-starter-data-jpa
com.h2database
h2
mysql
mysql-connector-java
8.0.28
com.dameng
DmJdbcDriver8
8.1.3.162
system
${project.basedir}/src/lib/DmJdbcDriver8.jar
com.dameng
DmDialect-for-hibernate5.3
8.1.3.162
system
${project.basedir}/src/lib/DmDialect-for-hibernate5.4.jar
com.google.code.gson
gson
2.8.9
com.google.protobuf
protobuf-java
2.5.0
org.apache.logging.log4j
log4j-slf4j-impl
${log4j2.version}
org.apache.logging.log4j
log4j-api
${log4j2.version}
org.apache.logging.log4j
log4j-core
${log4j2.version}
org.apache.logging.log4j
log4j-to-slf4j
${log4j2.version}
commons-io
commons-io
2.7
com.googlecode.json-simple
json-simple
1.1.1
org.slf4j
slf4j-api
1.7.5
org.slf4j
slf4j-log4j12
1.7.5
commons-httpclient
commons-httpclient
3.1
uk.org.lidalia
slf4j-test
1.0.0-jdk6
test
org.mockito
mockito-all
1.9.5
jar
test
com.tencentcloudapi
tencentcloud-sdk-java-sms
3.1.410
cn.wildfirechat
sdk
${wfc.sdk.version}
system
${project.basedir}/src/lib/sdk-${wfc.sdk.version}.jar
cn.wildfirechat
common
${wfc.sdk.version}
system
${project.basedir}/src/lib/common-${wfc.sdk.version}.jar
com.aliyun
aliyun-java-sdk-core
4.1.0
com.qiniu
qiniu-java-sdk
7.3.0
com.aliyun.oss
aliyun-sdk-oss
3.10.2
io.minio
minio
7.0.2
com.qcloud
cos_api
5.6.28
com.google.guava
guava
25.1-jre
org.apache.shiro
shiro-spring
1.7.1
ws.schild
jave-core
2.7.3
ws.schild
jave-nativebin-linux64
2.7.3
org.springframework.boot
spring-boot-starter-mail
org.springframework.boot
spring-boot-maven-plugin
true
jdeb
org.vafer
1.8
package
jdeb
${project.basedir}/deb/control
false
${project.build.directory}/app-server-${project.version}.deb
${project.build.directory}/${project.name}-${project.version}.jar
file
perm
/opt/app-server
${project.basedir}/config
directory
perm
/opt/app-server/config
${project.basedir}/systemd/app-server.service
file
perm
/usr/lib/systemd/system
================================================
FILE: release_note.md
================================================
# 当前版本更新记录
0.70 Release note:
1. 解决上传文件可能存在的漏洞
# 升级注意事项
1. 如果从0.40以前版本升级上来,需要注意升级兼容有问题 请参考Readme中的兼容问题说明
2. 如果从0.42以前版本升级上来,需要注意升级兼容有问题 请参考Readme中的兼容问题说明
3. 如果从0.45以前版本升级上来,需要注意升级PC和Web代码到0.45版本发布日志之后的版本
4. 如果从0.45.1以前版本升级上来,需要注意添加配置文件中的wfc.all_client_support_ssl开关
5. 0.51版本添加了token认证。可以同时支持token和cookies认证,客户端也做了对应修改,优先使用token。注意做好兼容。
6. 从0.53版本开始,所以数据都存储在数据库中,因此应用服务为无状态服务,可以部署多台应用服务做高可用和水平扩展。需要注意数据都是存储在数据库中,如果用户量较大或者业务量比较大,可以自己二开应用服务,添加redis缓存。
7. 从0.54版本开始,替换了腾讯云SDK,使用腾讯云短信的客户需要注意修改配置文件调试短信功能
# 历史更新记录
-------------
0.72 Release note:
1. 添加LDAP示例代码
2. 解决等待PC扫码长轮训超时问题
3. 添加配置应用前缀
4. 添加滑动验证功能
5. 升级IM Server SDK
-------------
0.71 Release note:
1. 升级IM SDK到1.3.8
2. 获取群组成员头像接口额外返回用户名称
3. 解决ios分享绕过限制问题
-------------
0.70 Release note:
1. 解决上传文件漏洞问题
-------------
0.69 Release note:
1. 升级IM SDK
2. 每次登录时都要发送机器人欢迎语
3. amr2mp3允许匿名访问
-------------
0.68 Release note:
1. 调整 amr转 mp3 失败时的状态码。
2. 添加默认密码功能。
3. 配置json去掉null的属性。
4. 升级IM server SDK。
-------------
0.67 Release note:
1. 修改群公告长度到2000。
2. 升级IM server SDK。
3. 生成头像的接口允许匿名访问。
-------------
0.66 Release note:
1. 添加生成用户和群组头像功能。
2. 修正使用超级验证码无法更改密码的问题。
-------------
0.65 Release note:
1. 通过应用服务发送消息时,返回消息ID和消息时间信息
-------------
0.64 Release note:
1. 当用户名登录时,当用户不存在时也返回密码错误。
2. 会议设置最大参与人数参数
-------------
0.63 Release note:
1. 删掉部分无用依赖减少程序大小
2. 会议支持设置焦点用户功能
3. 添加nginx示例配置
4. 升级IM SDK
5. 定期清理无效的pcsession
-------------
0.62 Release note:
1. 回归用户自动关注官方频道
2. 优化报警通知
3. 发送登录验证短信之前先检查是否被封禁。
4. 升级IM SDK
5. 添加会议录制接口
6. 添加rpm和deb格式
-------------
0.61 Release note:
1. 销毁用户时删掉密码
-------------
0.60 Release note:
1. 添加当新用户注册时关注频道功能
-------------
0.59 Release note:
1. mysql connector升级到8.0.28
2. commons-io升级到2.7
3. 添加密码登录方式
4. 升级im server sdk到0.92
-------------
0.58 Release note:
1. 升级log4j2版本到2.17.2
2. 升级server sdk到0.89
3. 添加删除账户功能
-------------
0.57 Release note:
1. 升级log4j2版本到2.17.1
2. 升级server sdk到0.86
3. 添加获取群组成员接口用来拼接头像
4. 添加对腾讯云对象存储的支持
-------------
0.56 Release note:
1. 升级log4j2版本到2.17.0
-------------
0.55 Release note:
1. 升级log4j2版本
-------------
0.54 Release note:
1. 更新腾讯云短信SDK,使用新的SDK进行接入短信,使用腾讯云短信的客户需要重新配置和调试短信功能。
6. 升级野火Server SDK。
--------------
0.53 Release note:
1. 短信验证码存储到数据库中
3. PC和Web端登录的session存储到数据库中
7. 支持会议相关接口
--------------
0.52 Release note:
1. 升级IM SDK
2. 修正token鉴权方式错误
--------------
0.51 Release note:
1. 优化收藏功能
2. 添加token认证方式
--------------
0.50 Release note:
1. 升级IM Server SDK
--------------
0.49 Release note:
1. 解决某些情况下mysql连不上的问题
2. 解决收藏错误问题
4. 添加第三方敏感词接口示例
8. 更新腾讯短信接口
--------------
0.48 Release note:
1. 添加IM服务异常报警功能
2. 修改PC快速登录消息有效时间为1分钟
--------------
0.47.1 Release note:
1. 修正收藏内容没有区分用户的问题,0.47版本都需要升级。
--------------
0.47 Release note:
1. 添加收藏功能
--------------
0.46.1 Release note:
1. 紧急修复PC端同时显示二维码太多无法登录的问题
--------------
0.46 Release note:
1. 升级spring boot版本
2. 支持iOS客户端通过appserver分享
--------------
0.45.1 Release note:
1. 添加配置,客户端是否支持SSL
--------------
0.45 Release note:
1. PC和Web增加快速登录功能
2. 解决PC和Web群公告获取失败的问题
--------------
0.44 Release note:
1. 添加针对请求IP限频。
2. 升级server api版本。
--------------
0.43 Release note:
1. 添加语音转码功能,支持微信小程序语音消息。
2. 修改pc或web端登录方式,改为长轮训方式。
3. 修改用户名生成方式,默认使用短ID。
--------------
0.42 Release note:
1. 更新到IM server SDK 0.41
2. 添加对物联网设备的支持(仅支持专业版)
--------------
0.41 Release note:
1. 生成用户账户时,使用uuid作为账户名称
--------------
0.40 Release note:
1. 增加阿里云短信配置,可以选择阿里云或者腾讯云短信
2. 增加了客户端上传日志功能,移动端在设置中选择上传日志
3. 为扫码登录和群公告添加shiro控制,提供系统的安全性。
================================================
FILE: src/main/java/cn/wildfirechat/app/AppController.java
================================================
package cn.wildfirechat.app;
import cn.wildfirechat.app.jpa.FavoriteItem;
import cn.wildfirechat.app.pojo.*;
import cn.wildfirechat.pojos.InputCreateDevice;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.subject.Subject;
import org.h2.util.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.context.request.async.DeferredResult;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
@RestController
public class AppController {
private static final Logger LOG = LoggerFactory.getLogger(AppController.class);
@Autowired
private Service mService;
@GetMapping()
public Object health() {
return "Ok";
}
/*
移动端登录
*/
// 生成滑动验证码
@PostMapping(value = "/slide_verify/generate", produces = "application/json;charset=UTF-8")
public Object generateSlideVerify() {
return mService.generateSlideVerify();
}
// 验证滑动验证码
@PostMapping(value = "/slide_verify/verify", produces = "application/json;charset=UTF-8")
public Object verifySlide(@RequestBody SlideVerifyRequest request) {
return mService.verifySlide(request.getToken(), request.getX());
}
@PostMapping(value = "/send_code", produces = "application/json;charset=UTF-8")
public Object sendLoginCode(@RequestBody SendCodeRequestWithSlideVerify request) {
return mService.sendLoginCode(request.getMobile(), request.getSlideVerifyToken());
}
@PostMapping(value = "/login", produces = "application/json;charset=UTF-8")
public Object loginWithMobileCode(@RequestBody PhoneCodeLoginRequestWithSlideVerify request, HttpServletResponse response) {
return mService.loginWithMobileCode(response, request.getMobile(), request.getCode(), request.getClientId(), request.getPlatform() == null ? 0 : request.getPlatform(), request.getSlideVerifyToken());
}
@PostMapping(value = "/login_pwd", produces = "application/json;charset=UTF-8")
public Object loginWithPassword(@RequestBody UserPasswordLoginRequestWithSlideVerify request, HttpServletResponse response) {
return mService.loginWithPassword(response, request.getMobile(), request.getPassword(), request.getClientId(), request.getPlatform() == null ? 0 : request.getPlatform(), request.getSlideVerifyToken());
}
@PostMapping(value = "/change_pwd", produces = "application/json;charset=UTF-8")
public Object changePassword(@RequestBody ChangePasswordRequest request) {
return mService.changePassword(request.getOldPassword(), request.getNewPassword(), request.getSlideVerifyToken());
}
@PostMapping(value = "/send_reset_code", produces = "application/json;charset=UTF-8")
public Object sendResetCode(@RequestBody SendCodeRequest request) {
return mService.sendResetCode(request.getMobile(), request.getSlideVerifyToken());
}
@PostMapping(value = "/reset_pwd", produces = "application/json;charset=UTF-8")
public Object resetPassword(@RequestBody ResetPasswordRequest request) {
return mService.resetPassword(request.getMobile(), request.getResetCode(), request.getNewPassword());
}
@PostMapping(value = "/send_destroy_code", produces = "application/json;charset=UTF-8")
public Object sendDestroyCode(@RequestBody SendDestroyCodeRequest request) {
return mService.sendDestroyCode(request.getSlideVerifyToken());
}
@PostMapping(value = "/destroy", produces = "application/json;charset=UTF-8")
public Object destroy(@RequestBody DestroyRequest code, HttpServletResponse response) {
return mService.destroy(response, code.getCode());
}
/* PC扫码操作
1, PC -> App 创建会话
2, PC -> App 轮询调用session_login进行登陆,如果已经扫码确认返回token,否则返回错误码9(已经扫码还没确认)或者10(还没有被扫码)
*/
@CrossOrigin
@PostMapping(value = "/pc_session", produces = "application/json;charset=UTF-8")
public Object createPcSession(@RequestBody CreateSessionRequest request) {
return mService.createPcSession(request);
}
@CrossOrigin
@PostMapping(value = "/session_login/{token}", produces = "application/json;charset=UTF-8")
public Object loginWithSession(@PathVariable("token") String token) {
LOG.info("receive login with session key {}", token);
RestResult timeoutResult = RestResult.error(RestResult.RestCode.ERROR_SESSION_EXPIRED);
ResponseEntity timeoutResponseEntity = new ResponseEntity<>(timeoutResult, HttpStatus.OK);
int timeoutSecond = 50;
DeferredResult deferredResult = new DeferredResult<>(timeoutSecond * 1000L, timeoutResponseEntity);
CompletableFuture.runAsync(() -> {
try {
int i = 0;
while (i < timeoutSecond) {
RestResult restResult = mService.loginWithSession(token);
if (restResult.getCode() == RestResult.RestCode.ERROR_SESSION_NOT_VERIFIED.code && restResult.getResult() != null) {
deferredResult.setResult(new ResponseEntity(restResult, HttpStatus.OK));
break;
} else if (restResult.getCode() == RestResult.RestCode.SUCCESS.code
|| restResult.getCode() == RestResult.RestCode.ERROR_SESSION_EXPIRED.code
|| restResult.getCode() == RestResult.RestCode.ERROR_SERVER_ERROR.code
|| restResult.getCode() == RestResult.RestCode.ERROR_SESSION_CANCELED.code
|| restResult.getCode() == RestResult.RestCode.ERROR_CODE_INCORRECT.code) {
ResponseEntity.BodyBuilder builder =ResponseEntity.ok();
if(restResult.getCode() == RestResult.RestCode.SUCCESS.code){
Subject subject = SecurityUtils.getSubject();
Object sessionId = subject.getSession().getId();
builder.header("authToken", sessionId.toString());
}
deferredResult.setResult(builder.body(restResult));
break;
} else {
TimeUnit.SECONDS.sleep(1);
}
i ++;
}
} catch (Exception ex) {
ex.printStackTrace();
deferredResult.setResult(new ResponseEntity(RestResult.error(RestResult.RestCode.ERROR_SERVER_ERROR), HttpStatus.OK));
}
}, Executors.newCachedThreadPool());
return deferredResult;
}
/* 手机扫码操作
1,扫码,调用/scan_pc接口。
2,调用/confirm_pc 接口进行确认
*/
@PostMapping(value = "/scan_pc/{token}", produces = "application/json;charset=UTF-8")
public Object scanPc(@PathVariable("token") String token) {
return mService.scanPc(token);
}
@PostMapping(value = "/confirm_pc", produces = "application/json;charset=UTF-8")
public Object confirmPc(@RequestBody ConfirmSessionRequest request) {
return mService.confirmPc(request);
}
@PostMapping(value = "/cancel_pc", produces = "application/json;charset=UTF-8")
public Object cancelPc(@RequestBody CancelSessionRequest request) {
return mService.cancelPc(request);
}
/*
修改野火账户
*/
@CrossOrigin
@PostMapping(value = "/change_name", produces = "application/json;charset=UTF-8")
public Object changeName(@RequestBody ChangeNameRequest request) {
if (StringUtils.isNullOrEmpty(request.getNewName())) {
return RestResult.error(RestResult.RestCode.ERROR_INVALID_PARAMETER);
}
return mService.changeName(request.getNewName());
}
/*
群公告相关接口
*/
@CrossOrigin
@PostMapping(value = "/put_group_announcement", produces = "application/json;charset=UTF-8")
public Object putGroupAnnouncement(@RequestBody GroupAnnouncementPojo request) {
return mService.putGroupAnnouncement(request);
}
@CrossOrigin
@PostMapping(value = "/get_group_announcement", produces = "application/json;charset=UTF-8")
public Object getGroupAnnouncement(@RequestBody GroupIdPojo request) {
return mService.getGroupAnnouncement(request.groupId);
}
/*
客户端上传协议栈日志
*/
@PostMapping(value = "/logs/{userId}/upload")
public Object uploadFiles(@RequestParam("file") MultipartFile file, @PathVariable("userId") String userId) throws IOException {
return mService.saveUserLogs(userId, file);
}
/*
投诉和建议
*/
@CrossOrigin
@PostMapping(value = "/complain", produces = "application/json;charset=UTF-8")
public Object complain(@RequestBody ComplainRequest request) {
return mService.complain(request.text);
}
/*
物联网相关接口
*/
@PostMapping(value = "/things/add_device")
public Object addDevice(@RequestBody InputCreateDevice createDevice) {
return mService.addDevice(createDevice);
}
@PostMapping(value = "/things/list_device")
public Object getDeviceList() {
return mService.getDeviceList();
}
@PostMapping(value = "/things/del_device")
public Object delDevice(@RequestBody InputCreateDevice createDevice) {
return mService.delDevice(createDevice);
}
/*
发送消息
*/
@PostMapping(value = "/messages/send")
public Object sendUserMessage(@RequestBody SendMessageRequest sendMessageRequest) {
return mService.sendUserMessage(sendMessageRequest);
}
/*
iOS设备Share extension分享图片文件等使用
*/
@PostMapping(value = "/media/upload/{media_type}")
public Object uploadMedia(@RequestParam("file") MultipartFile file, @PathVariable("media_type") int mediaType) throws IOException {
return mService.uploadMedia(mediaType, file);
}
@CrossOrigin
@PostMapping(value = "/fav/add", produces = "application/json;charset=UTF-8")
public Object putFavoriteItem(@RequestBody FavoriteItem request) {
return mService.putFavoriteItem(request);
}
@CrossOrigin
@PostMapping(value = "/fav/del/{fav_id}", produces = "application/json;charset=UTF-8")
public Object removeFavoriteItem(@PathVariable("fav_id") int favId) {
return mService.removeFavoriteItems(favId);
}
@CrossOrigin
@PostMapping(value = "/fav/list", produces = "application/json;charset=UTF-8")
public Object getFavoriteItems(@RequestBody LoadFavoriteRequest request) {
return mService.getFavoriteItems(request.id, request.count);
}
@CrossOrigin
@PostMapping(value = "/group/members_for_portrait", produces = "application/json;charset=UTF-8")
public Object getGroupMembersForPortrait(@RequestBody GroupIdPojo groupIdPojo) {
return mService.getGroupMembersForPortrait(groupIdPojo.groupId);
}
}
================================================
FILE: src/main/java/cn/wildfirechat/app/Application.java
================================================
package cn.wildfirechat.app;
import cn.wildfirechat.app.jpa.PCSessionRepository;
import cn.wildfirechat.app.slide.SlideVerifyCleanupService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.MultipartConfigFactory;
import org.springframework.boot.web.servlet.ServletComponentScan;
import org.springframework.context.annotation.Bean;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.util.unit.DataSize;
import javax.servlet.MultipartConfigElement;
@SpringBootApplication
@ServletComponentScan
@EnableScheduling
public class Application {
@Autowired
private PCSessionRepository pcSessionRepository;
@Autowired
private SlideVerifyCleanupService slideVerifyCleanupService;
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
/**
* 文件上传配置
* @return
*/
@Bean
public MultipartConfigElement multipartConfigElement() {
MultipartConfigFactory factory = new MultipartConfigFactory();
//单个文件最大
factory.setMaxFileSize(DataSize.ofMegabytes(20)); //20MB
/// 设置总上传数据总大小
factory.setMaxRequestSize(DataSize.ofMegabytes(100));
return factory.createMultipartConfig();
}
@Scheduled(fixedRate = 60 * 60 * 1000)
public void clearPCSession(){
pcSessionRepository.deleteByCreateDtBefore(System.currentTimeMillis() - 60 * 60 * 1000);
}
@Scheduled(fixedRate = 60 * 60 * 1000)
public void cleanExpiredSlideVerify(){
slideVerifyCleanupService.cleanupExpired();
}
}
================================================
FILE: src/main/java/cn/wildfirechat/app/AudioController.java
================================================
package cn.wildfirechat.app;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.InputStreamResource;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import ws.schild.jave.*;
import javax.annotation.PostConstruct;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.concurrent.CompletableFuture;
import java.util.function.Supplier;
@RestController
public class AudioController {
@Value("${wfc.audio.cache.dir}")
String cacheDirPath;
private File cacheDir;
@PostConstruct
public void init() {
cacheDir = new File(cacheDirPath);
if (!cacheDir.exists()) {
cacheDir.mkdirs();
}
}
@GetMapping("amr2mp3")
public CompletableFuture> amr2mp3(@RequestParam("path") String amrUrl) throws FileNotFoundException {
MediaType mediaType = new MediaType("audio", "mp3");
String mp3FileName = amrUrl.substring(amrUrl.lastIndexOf('/') + 1) + ".mp3";
File mp3File = new File(cacheDir, mp3FileName);
if (mp3File.exists()) {
InputStreamResource resource = new InputStreamResource(new FileInputStream(mp3File));
return CompletableFuture.completedFuture(ResponseEntity.ok()
// .header(HttpHeaders.CONTENT_DISPOSITION, "attachment;filename=" + mp3File.getName())
.contentType(mediaType)
.contentLength(mp3File.length())
.body(resource));
}
return CompletableFuture.supplyAsync(new Supplier>() {
/**
* Gets a result.
*
* @return a result
*/
@Override
public ResponseEntity get() {
try {
amr2mp3(amrUrl, mp3File);
InputStreamResource resource = new InputStreamResource(new FileInputStream(mp3File));
return ResponseEntity.ok()
// .header(HttpHeaders.CONTENT_DISPOSITION, "attachment;filename=" + mp3File.getName())
.contentType(mediaType)
.contentLength(mp3File.length())
.body(resource);
} catch (MalformedURLException e) {
System.out.println(amrUrl);
e.printStackTrace();
} catch (EncoderException e) {
e.printStackTrace();
} catch (FileNotFoundException e) {
e.printStackTrace();
}
return ResponseEntity.status(500).build();
}
});
}
private static void amr2mp3(String sourceUrl, File target) throws MalformedURLException, EncoderException {
//Audio Attributes
AudioAttributes audio = new AudioAttributes();
audio.setCodec("libmp3lame");
audio.setBitRate(128000);
audio.setChannels(2);
audio.setSamplingRate(44100);
//Encoding attributes
EncodingAttributes attrs = new EncodingAttributes();
attrs.setFormat("mp3");
attrs.setAudioAttributes(audio);
//Encode
Encoder encoder = new Encoder();
encoder.encode(new MultimediaObject(new URL(sourceUrl)), target, attrs);
}
}
================================================
FILE: src/main/java/cn/wildfirechat/app/ForbiddenException.java
================================================
package cn.wildfirechat.app;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;
@ResponseStatus(value = HttpStatus.FORBIDDEN, reason="Forbidden")
public class ForbiddenException extends RuntimeException {
}
================================================
FILE: src/main/java/cn/wildfirechat/app/IMCallbackController.java
================================================
package cn.wildfirechat.app;
import cn.wildfirechat.pojos.*;
import cn.wildfirechat.pojos.moments.CommentPojo;
import cn.wildfirechat.pojos.moments.FeedPojo;
import cn.wildfirechat.pojos.moments.IdPojo;
import com.google.gson.Gson;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
/*
IM对应事件发生时,会回调到配置地址。需要注意IM服务单线程进行回调,如果接收方处理太慢会导致推送线程被阻塞,导致延迟发生,甚至导致IM系统异常。
建议异步处理快速返回,这里收到后转到异步线程处理,并且立即返回。另外两个服务器的ping值不能太大。
*/
@RestController()
public class IMCallbackController {
/*
用户在线状态回调
*/
@PostMapping(value = "/im_event/user/online")
public Object onUserOnlineEvent(@RequestBody UserOnlineStatus event) {
System.out.println("User:" + event.userId + " on device:" + event.clientId + " online status:" + event.status);
return "ok";
}
/*
用户关系变更回调
*/
@PostMapping(value = "/im_event/user/relation")
public Object onUserRelationUpdated(@RequestBody RelationUpdateEvent event) {
System.out.println("User relation updated:" + event.userId);
return "ok";
}
/*
用户信息更新回调
*/
@PostMapping(value = "/im_event/user/info")
public Object onUserInfoUpdated(@RequestBody InputOutputUserInfo event) {
System.out.println("User info updated:" + event.getUserId());
return "ok";
}
/*
发送消息回调
*/
@PostMapping(value = "/im_event/message")
public Object onMessage(@RequestBody OutputMessageData event) {
System.out.println("message:" +event.getMessageId());
return "ok";
}
/*
发送消息回调
*/
@PostMapping(value = "/im_event/recall_message")
public Object onRecallMessage(@RequestBody OutputRecallMessageData event) {
System.out.println("recall message:" +event.getUserId());
return "ok";
}
/*
物联网消息回调
*/
@PostMapping(value = "/im_event/things/message")
public Object onThingsMessage(@RequestBody OutputMessageData event) {
System.out.println("message:" + event.getMessageId());
return "ok";
}
/*
消息已读回调
*/
@PostMapping(value = "/im_event/message_read")
public Object onMessageRead(@RequestBody OutputReadData event) {
System.out.println("message:" +event.user);
return "ok";
}
/*
群组信息更新回调
*/
@PostMapping(value = "/im_event/group/info")
public Object onGroupInfoUpdated(@RequestBody GroupUpdateEvent event) {
System.out.println("group info updated:" + event.type);
return "ok";
}
/*
群组成员更新回调
*/
@PostMapping(value = "/im_event/group/member")
public Object onGroupMemberUpdated(@RequestBody GroupMemberUpdateEvent event) {
System.out.println("group member updated:" + event.type);
return "ok";
}
/*
频道信息更新回调
*/
@PostMapping(value = "/im_event/channel/info")
public Object onChannelInfoUpdated(@RequestBody ChannelUpdateEvent event) {
System.out.println("channel info updated:" + event.type);
return "ok";
}
/*
聊天室信息更新回调
*/
@PostMapping(value = "/im_event/chatroom/info")
public Object onChatroomInfoUpdated(@RequestBody ChatroomUpdateEvent event) {
System.out.println("chatroom info updated:" + event.type);
return "ok";
}
/*
聊天室成员更新回调
*/
@PostMapping(value = "/im_event/chatroom/member")
public Object onChatroomMemberUpdated(@RequestBody ChatroomMemberUpdateEvent event) {
System.out.println("chatroom member updated:" + event.type);
return "ok";
}
/*
消息审查示例。
如果允许发送,返回状态码为200,内容为空;如果替换内容发送,返回状态码200,内容为替换过的payload内容。如果不允许发送,返回状态码403。
注意如果没有替换内容运行原消息发送,要返回空内容,不要返回原消息!!!
*/
@PostMapping(value = "/message/censor")
public Object censorMessage(@RequestBody OutputMessageData event) {
System.out.println("message:" +event.getMessageId());
if(event.getPayload().getSearchableContent() != null && event.getPayload().getSearchableContent().contains("testkongbufenzi")) {
throw new ForbiddenException();
}
if(event.getPayload().getSearchableContent() != null && event.getPayload().getSearchableContent().contains("testzhaopian")) {
event.getPayload().setSearchableContent(event.getPayload().getSearchableContent().replace("zhaopian", "照片"));
return new Gson().toJson(event.getPayload());
}
return "";
}
@PostMapping(value = "/im_event/conference/create")
public Object onConferenceCreated(@RequestBody ConferenceCreateEvent event) {
System.out.println("conference created:" + event);
return "ok";
}
@PostMapping(value = "/im_event/conference/destroy")
public Object onConferenceDestroyed(@RequestBody ConferenceDestroyEvent event) {
System.out.println("conference destroyed:" + event);
return "ok";
}
@PostMapping(value = "/im_event/conference/member_join")
public Object onConferenceMemberJoined(@RequestBody ConferenceJoinEvent event) {
System.out.println("conference member joined:" + event);
return "ok";
}
@PostMapping(value = "/im_event/conference/member_leave")
public Object onConferenceMemberLeaved(@RequestBody ConferenceLeaveEvent event) {
System.out.println("conference member leaved:" + event);
return "ok";
}
@PostMapping(value = "/im_event/conference/member_publish")
public Object onConferenceMemberPublished(@RequestBody ConferencePublishEvent event) {
System.out.println("conference member published:" + event);
return "ok";
}
@PostMapping(value = "/im_event/conference/member_unpublish")
public Object onConferenceMemberUnpublished(@RequestBody ConferenceUnpublishEvent event) {
System.out.println("conference member unpublished:" + event);
return "ok";
}
@PostMapping(value = "/im_event/moments_feed")
public Object onMomentsFeed(@RequestBody FeedPojo event) {
System.out.println("feed posted:" + event.sender + ", " + event.feedId + ", " + event.text);
return "ok";
}
@PostMapping(value = "/im_event/moments_feed_recall")
public Object onMomentsFeedRecall(@RequestBody IdPojo event) {
System.out.println("recall feed:" + event.id);
return "ok";
}
@PostMapping(value = "/im_event/moments_comment")
public Object onMomentsComment(@RequestBody CommentPojo event) {
System.out.println("feed posted:" + event.sender + ", " + event.commentId + ", " + event.feedId + ", " + event.text);
return "ok";
}
@PostMapping(value = "/im_event/moments_comment_recall")
public Object onMomentsCommentRecall(@RequestBody IdPojo event) {
System.out.println("recall comment:" + event.id + "," + event.id2);
return "ok";
}
}
================================================
FILE: src/main/java/cn/wildfirechat/app/IMConfig.java
================================================
package cn.wildfirechat.app;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
@Configuration
@ConfigurationProperties(prefix="im")
@PropertySource(value = "file:config/im.properties", encoding = "UTF-8")
public class IMConfig {
public String admin_url;
public String admin_secret;
public String admin_user_id;
public boolean isUse_random_name() {
return use_random_name;
}
public void setUse_random_name(boolean use_random_name) {
this.use_random_name = use_random_name;
}
boolean use_random_name;
String welcome_for_new_user;
String welcome_for_back_user;
boolean new_user_robot_friend;
String robot_friend_id;
String robot_welcome;
String prompt_text;
String image_msg_url;
String image_msg_base64_thumbnail;
String new_user_subscribe_channel_id;
String back_user_subscribe_channel_id;
public String getAdmin_url() {
return admin_url;
}
public void setAdmin_url(String admin_url) {
this.admin_url = admin_url;
}
public String getAdmin_secret() {
return admin_secret;
}
public void setAdmin_secret(String admin_secret) {
this.admin_secret = admin_secret;
}
public String getWelcome_for_new_user() {
return welcome_for_new_user;
}
public void setWelcome_for_new_user(String welcome_for_new_user) {
this.welcome_for_new_user = welcome_for_new_user;
}
public String getWelcome_for_back_user() {
return welcome_for_back_user;
}
public void setWelcome_for_back_user(String welcome_for_back_user) {
this.welcome_for_back_user = welcome_for_back_user;
}
public boolean isNew_user_robot_friend() {
return new_user_robot_friend;
}
public void setNew_user_robot_friend(boolean new_user_robot_friend) {
this.new_user_robot_friend = new_user_robot_friend;
}
public String getRobot_friend_id() {
return robot_friend_id;
}
public void setRobot_friend_id(String robot_friend_id) {
this.robot_friend_id = robot_friend_id;
}
public String getRobot_welcome() {
return robot_welcome;
}
public void setRobot_welcome(String robot_welcome) {
this.robot_welcome = robot_welcome;
}
public String getNew_user_subscribe_channel_id() {
return new_user_subscribe_channel_id;
}
public void setNew_user_subscribe_channel_id(String new_user_subscribe_channel_id) {
this.new_user_subscribe_channel_id = new_user_subscribe_channel_id;
}
public String getBack_user_subscribe_channel_id() {
return back_user_subscribe_channel_id;
}
public void setBack_user_subscribe_channel_id(String back_user_subscribe_channel_id) {
this.back_user_subscribe_channel_id = back_user_subscribe_channel_id;
}
public String getAdmin_user_id() {
return admin_user_id;
}
public void setAdmin_user_id(String admin_user_id) {
this.admin_user_id = admin_user_id;
}
public String getPrompt_text() {
return prompt_text;
}
public void setPrompt_text(String prompt_text) {
this.prompt_text = prompt_text;
}
public String getImage_msg_url() {
return image_msg_url;
}
public void setImage_msg_url(String image_msg_url) {
this.image_msg_url = image_msg_url;
}
public String getImage_msg_base64_thumbnail() {
return image_msg_base64_thumbnail;
}
public void setImage_msg_base64_thumbnail(String image_msg_base64_thumbnail) {
this.image_msg_base64_thumbnail = image_msg_base64_thumbnail;
}
}
================================================
FILE: src/main/java/cn/wildfirechat/app/IMExceptionEventController.java
================================================
package cn.wildfirechat.app;
import cn.wildfirechat.common.IMExceptionEvent;
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.core.io.InputStreamResource;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.mail.SimpleMailMessage;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.web.bind.annotation.*;
import ws.schild.jave.*;
import javax.annotation.PostConstruct;
import javax.mail.MessagingException;
import javax.mail.internet.MimeMessage;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.List;
import java.util.Set;
import java.util.concurrent.BlockingDeque;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentSkipListSet;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.function.Supplier;
@RestController
public class IMExceptionEventController {
private BlockingDeque events = new LinkedBlockingDeque<>();
private final Logger logger = LoggerFactory.getLogger(this.getClass());
@Value("${spring.mail.username}")
private String from;
@Value("${spring.mail.to_lists}")
private String toLists;
@Autowired
private JavaMailSender mailSender;
@PostConstruct
void init() {
new Thread(()->{
while (true) {
try {
IMExceptionEvent event = events.take();
if (event.event_type == IMExceptionEvent.EventType.HEART_BEAT) {
sendTextMail("恭喜您,您的服务已经连续24小时没有异常发生了", "恭喜您,您的服务已经连续24小时没有异常发生了");
} else {
sendTextMail("IM服务报警通知:节点" + event.node_id + ",发生" + event.count + "次 " + event.msg, "call stack:" + event.call_stack);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}).start();
}
@PostMapping("im_exception_event")
public String onIMException(@RequestBody IMExceptionEvent event) {
System.out.println(event);
events.add(event);
return "ok";
}
/**
* 文本邮件
* @param subject 邮件主题
* @param content 邮件内容
*/
public void sendTextMail(String subject, String content){
SimpleMailMessage message = new SimpleMailMessage();
String[] tos = toLists.split(",");
message.setTo(tos);
message.setSubject(subject);
message.setText(content);
message.setFrom(from);
mailSender.send(message);
}
//content HTML内容
public void sendHtmlMail(String subject, String content) throws MessagingException {
MimeMessage message = mailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(message, true);
String[] tos = toLists.split(",");
helper.setTo(tos);
helper.setSubject(subject);
helper.setText(content, true);
helper.setFrom(from);
mailSender.send(message);
}
}
================================================
FILE: src/main/java/cn/wildfirechat/app/RestResult.java
================================================
package cn.wildfirechat.app;
public class RestResult {
public enum RestCode {
SUCCESS(0, "success"),
ERROR_INVALID_MOBILE(1, "无效的电话号码"),
ERROR_SEND_SMS_OVER_FREQUENCY(3, "请求验证码太频繁"),
ERROR_SERVER_ERROR(4, "服务器异常"),
ERROR_CODE_EXPIRED(5, "验证码已过期"),
ERROR_CODE_INCORRECT(6, "验证码或密码错误"),
ERROR_SERVER_CONFIG_ERROR(7, "服务器配置错误"),
ERROR_SESSION_EXPIRED(8, "会话不存在或已过期"),
ERROR_SESSION_NOT_VERIFIED(9, "会话没有验证"),
ERROR_SESSION_NOT_SCANED(10, "会话没有被扫码"),
ERROR_SERVER_NOT_IMPLEMENT(11, "功能没有实现"),
ERROR_GROUP_ANNOUNCEMENT_NOT_EXIST(12, "群公告不存在"),
ERROR_NOT_LOGIN(13, "没有登录"),
ERROR_NO_RIGHT(14, "没有权限"),
ERROR_INVALID_PARAMETER(15, "无效参数"),
ERROR_NOT_EXIST(16, "对象不存在"),
ERROR_USER_NAME_ALREADY_EXIST(17, "用户名已经存在"),
ERROR_SESSION_CANCELED(18, "会话已经取消"),
ERROR_PASSWORD_INCORRECT(19, "密码错误"),
ERROR_FAILURE_TOO_MUCH_TIMES(20, "密码错误次数太多,请等5分钟再试试"),
ERROR_USER_FORBIDDEN(21, "用户被封禁"),
ERROR_SLIDE_VERIFY_NOT_PASS(22, "滑动验证未通过"),
ERROR_CONFERENCE_QUOTA_EXCEEDED(23, "会议额度已用完");
public int code;
public String msg;
RestCode(int code, String msg) {
this.code = code;
this.msg = msg;
}
}
private int code;
private String message;
private Object result;
public static RestResult ok() {
return new RestResult(RestCode.SUCCESS, null);
}
public static RestResult ok(Object object) {
return new RestResult(RestCode.SUCCESS, object);
}
public static RestResult error(RestCode code) {
return new RestResult(code, null);
}
public static RestResult result(RestCode code, Object object){
return new RestResult(code, object);
}
public static RestResult result(int code, String message, Object object){
RestResult r = new RestResult(RestCode.SUCCESS, object);
r.code = code;
r.message = message;
return r;
}
private RestResult(RestCode code, Object result) {
this.code = code.code;
this.message = code.msg;
this.result = result;
}
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public Object getResult() {
return result;
}
public void setResult(Object result) {
this.result = result;
}
}
================================================
FILE: src/main/java/cn/wildfirechat/app/Service.java
================================================
package cn.wildfirechat.app;
import cn.wildfirechat.app.jpa.FavoriteItem;
import cn.wildfirechat.app.pojo.*;
import cn.wildfirechat.pojos.InputCreateDevice;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletResponse;
public interface Service {
RestResult sendLoginCode(String mobile);
RestResult sendLoginCode(String mobile, String slideVerifyToken);
RestResult sendResetCode(String mobile, String slideVerifyToken);
RestResult loginWithMobileCode(HttpServletResponse response, String mobile, String code, String clientId, int platform, String slideVerifyToken);
RestResult loginWithPassword(HttpServletResponse response, String mobile, String password, String clientId, int platform, String slideVerifyToken);
RestResult changePassword(String oldPwd, String newPwd, String slideVerifyToken);
RestResult resetPassword(String mobile, String resetCode, String newPwd);
RestResult sendDestroyCode();
RestResult sendDestroyCode(String slideVerifyToken);
RestResult destroy(HttpServletResponse response, String code);
RestResult createPcSession(CreateSessionRequest request);
RestResult loginWithSession(String token);
RestResult scanPc(String token);
RestResult confirmPc(ConfirmSessionRequest request);
RestResult cancelPc(CancelSessionRequest request);
RestResult changeName(String newName);
RestResult complain(String text);
RestResult putGroupAnnouncement(GroupAnnouncementPojo request);
RestResult getGroupAnnouncement(String groupId);
RestResult saveUserLogs(String userId, MultipartFile file);
RestResult generateSlideVerify();
RestResult verifySlide(String token, int x);
RestResult addDevice(InputCreateDevice createDevice);
RestResult getDeviceList();
RestResult delDevice(InputCreateDevice createDevice);
RestResult sendUserMessage(SendMessageRequest request);
RestResult uploadMedia(int mediaType, MultipartFile file);
RestResult putFavoriteItem(FavoriteItem request);
RestResult removeFavoriteItems(long id);
RestResult getFavoriteItems(long id, int count);
RestResult getGroupMembersForPortrait(String groupId);
}
================================================
FILE: src/main/java/cn/wildfirechat/app/ServiceImpl.java
================================================
package cn.wildfirechat.app;
import cn.wildfirechat.app.jpa.*;
import cn.wildfirechat.app.pojo.*;
import cn.wildfirechat.app.shiro.AuthDataSource;
import cn.wildfirechat.app.shiro.LdapToken;
import cn.wildfirechat.app.shiro.PhoneCodeToken;
import cn.wildfirechat.app.shiro.TokenAuthenticationToken;
import cn.wildfirechat.app.sms.SmsService;
import cn.wildfirechat.app.tools.*;
import cn.wildfirechat.common.ErrorCode;
import cn.wildfirechat.pojos.*;
import cn.wildfirechat.proto.ProtoConstants;
import cn.wildfirechat.sdk.*;
import cn.wildfirechat.sdk.model.IMResult;
import com.aliyun.oss.*;
import com.aliyun.oss.model.PutObjectRequest;
import com.google.gson.Gson;
import com.qcloud.cos.COSClient;
import com.qcloud.cos.ClientConfig;
import com.qcloud.cos.auth.BasicCOSCredentials;
import com.qcloud.cos.auth.COSCredentials;
import com.qcloud.cos.exception.CosClientException;
import com.qcloud.cos.http.HttpProtocol;
import com.qiniu.common.QiniuException;
import com.qiniu.http.Response;
import com.qiniu.storage.BucketManager;
import com.qiniu.storage.Configuration;
import com.qiniu.storage.Region;
import com.qiniu.storage.UploadManager;
import com.qiniu.storage.model.DefaultPutRet;
import com.qiniu.util.Auth;
import io.minio.MinioClient;
import io.minio.PutObjectOptions;
import io.minio.errors.MinioException;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.crypto.hash.Sha1Hash;
import org.apache.shiro.subject.Subject;
import org.json.simple.JSONObject;
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.util.Base64Utils;
import org.springframework.util.StringUtils;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.multipart.MultipartFile;
import javax.annotation.PostConstruct;
import javax.naming.NamingException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import static cn.wildfirechat.app.RestResult.RestCode.*;
import static cn.wildfirechat.app.jpa.PCSession.PCSessionStatus.*;
@org.springframework.stereotype.Service
public class ServiceImpl implements Service {
private static final Logger LOG = LoggerFactory.getLogger(ServiceImpl.class);
@Autowired
private SmsService smsService;
@Autowired
private IMConfig mIMConfig;
@Autowired
private AnnouncementRepository announcementRepository;
@Autowired
private FavoriteRepository favoriteRepository;
@Autowired
private UserPasswordRepository userPasswordRepository;
@Autowired
private cn.wildfirechat.app.slide.SlideVerifyService slideVerifyService;
@Value("${slide.verify.force:false}")
private boolean forceSlideVerify;
@Value("${sms.super_code}")
private String superCode;
@Value("${logs.user_logs_path}")
private String userLogPath;
@Value("${im.admin_url}")
private String adminUrl;
@Value("${wfc.default_user_password}")
private boolean defaultUserPwd;
@Value("${ldap.enable}")
private boolean enableLdap;
@Value("${ldap.admin_dn}")
private String ADMIN_DN;
@Value("${ldap.admin_password}")
private String ADMIN_PWD;
@Value("${ldap.ldap_url}")
private String LDAP_URL;
@Value("${ldap.search_base}")
private String SEARCH_BASE;
@Value("${wfc.user_register_forbidden:false}")
private boolean userRegisterForbidden;
@Autowired
private ShortUUIDGenerator userNameGenerator;
@Autowired
private AuthDataSource authDataSource;
private RateLimiter rateLimiter;
@Value("${wfc.compat_pc_quick_login}")
protected boolean compatPcQuickLogin;
@Value("${media.server.media_type}")
private int ossType;
@Value("${media.server_url}")
private String ossUrl;
@Value("${media.access_key}")
private String ossAccessKey;
@Value("${media.secret_key}")
private String ossSecretKey;
@Value("${media.bucket_general_name}")
private String ossGeneralBucket;
@Value("${media.bucket_general_domain}")
private String ossGeneralBucketDomain;
@Value("${media.bucket_image_name}")
private String ossImageBucket;
@Value("${media.bucket_image_domain}")
private String ossImageBucketDomain;
@Value("${media.bucket_voice_name}")
private String ossVoiceBucket;
@Value("${media.bucket_voice_domain}")
private String ossVoiceBucketDomain;
@Value("${media.bucket_video_name}")
private String ossVideoBucket;
@Value("${media.bucket_video_domain}")
private String ossVideoBucketDomain;
@Value("${media.bucket_file_name}")
private String ossFileBucket;
@Value("${media.bucket_file_domain}")
private String ossFileBucketDomain;
@Value("${media.bucket_sticker_name}")
private String ossStickerBucket;
@Value("${media.bucket_sticker_domain}")
private String ossStickerBucketDomain;
@Value("${media.bucket_moments_name}")
private String ossMomentsBucket;
@Value("${media.bucket_moments_domain}")
private String ossMomentsBucketDomain;
@Value("${media.bucket_favorite_name}")
private String ossFavoriteBucket;
@Value("${media.bucket_favorite_domain}")
private String ossFavoriteBucketDomain;
@Value("${local.media.temp_storage}")
private String ossTempPath;
private ConcurrentHashMap supportPCQuickLoginUsers = new ConcurrentHashMap<>();
@PostConstruct
private void init() {
AdminConfig.initAdmin(mIMConfig.admin_url, mIMConfig.admin_secret);
rateLimiter = new RateLimiter(60, 200);
if(StringUtils.isEmpty(mIMConfig.admin_user_id)) {
mIMConfig.admin_user_id = "admin";
}
}
private String getIp() {
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = requestAttributes.getRequest();
String ip = request.getHeader("X-Real-IP");
if (!StringUtils.isEmpty(ip) && !"unknown".equalsIgnoreCase(ip)) {
return ip;
}
ip = request.getHeader("X-Forwarded-For");
if (!StringUtils.isEmpty(ip) && !"unknown".equalsIgnoreCase(ip)) {
// 多次反向代理后会有多个IP值,第一个为真实IP。
int index = ip.indexOf(',');
if (index != -1) {
return ip.substring(0, index);
} else {
return ip;
}
} else {
return request.getRemoteAddr();
}
}
private int getUserStatus(String mobile) {
try {
IMResult inputOutputUserInfoIMResult = UserAdmin.getUserByMobile(mobile);
if(inputOutputUserInfoIMResult != null && inputOutputUserInfoIMResult.getErrorCode() == ErrorCode.ERROR_CODE_SUCCESS) {
IMResult outputUserStatusIMResult = UserAdmin.checkUserBlockStatus(inputOutputUserInfoIMResult.getResult().getUserId());
if(outputUserStatusIMResult != null && outputUserStatusIMResult.getErrorCode() == ErrorCode.ERROR_CODE_SUCCESS) {
return outputUserStatusIMResult.getResult().getStatus();
}
}
} catch (Exception e) {
e.printStackTrace();
}
return 0;
}
@Override
public RestResult sendLoginCode(String mobile) {
return sendLoginCode(mobile, null);
}
@Override
public RestResult sendLoginCode(String mobile, String slideVerifyToken) {
// 验证滑动验证码
if (forceSlideVerify) {
// 强制模式:必须提供token且验证通过
if (slideVerifyToken == null || slideVerifyToken.isEmpty()) {
return RestResult.error(ERROR_SLIDE_VERIFY_NOT_PASS);
}
if (!slideVerifyService.isVerified(slideVerifyToken)) {
return RestResult.error(ERROR_SLIDE_VERIFY_NOT_PASS);
}
} else {
// 可选模式:如果提供了token就验证
if (slideVerifyToken != null && !slideVerifyToken.isEmpty()) {
if (!slideVerifyService.isVerified(slideVerifyToken)) {
return RestResult.error(ERROR_SLIDE_VERIFY_NOT_PASS);
}
}
}
String remoteIp = getIp();
LOG.info("request send sms from {} {}", mobile, remoteIp);
//判断当前IP发送是否超频。
//另外 cn.wildfirechat.app.shiro.AuthDataSource.Count 会对用户发送消息限频
if (!rateLimiter.isGranted(remoteIp)) {
return RestResult.result(ERROR_SEND_SMS_OVER_FREQUENCY.code, "IP " + remoteIp + " 请求短信超频", null);
}
try {
//检查用户是否被封禁
//https://docs.wildfirechat.cn/server/admin_api/user_api.html#查询用户状态
int userStatus = getUserStatus(mobile);
if(userStatus == 2) {
return RestResult.error(ERROR_USER_FORBIDDEN);
}
// 如果禁止注册,检查用户是否存在
if (userRegisterForbidden) {
try {
IMResult userResult = UserAdmin.getUserByMobile(mobile);
if (userResult.getErrorCode() == ErrorCode.ERROR_CODE_NOT_EXIST) {
LOG.info("User not exist and register is forbidden, cannot send login code");
return RestResult.error(ERROR_NOT_EXIST);
}
} catch (Exception e) {
LOG.error("Check user exist error", e);
return RestResult.error(ERROR_SERVER_ERROR);
}
}
String code = Utils.getRandomCode(6);
RestResult.RestCode restCode = authDataSource.insertRecord(mobile, code);
if (restCode != SUCCESS) {
return RestResult.error(restCode);
}
restCode = smsService.sendCode(mobile, code);
if (restCode == RestResult.RestCode.SUCCESS) {
return RestResult.ok(restCode);
} else {
authDataSource.clearRecode(mobile);
return RestResult.error(restCode);
}
} catch (Exception e) {
// json解析错误
e.printStackTrace();
authDataSource.clearRecode(mobile);
}
return RestResult.error(RestResult.RestCode.ERROR_SERVER_ERROR);
}
@Override
public RestResult generateSlideVerify() {
try {
Map result = slideVerifyService.generateSlideVerify();
return RestResult.ok(result);
} catch (Exception e) {
LOG.error("生成滑动验证码失败", e);
return RestResult.error(RestResult.RestCode.ERROR_SERVER_ERROR);
}
}
@Override
public RestResult verifySlide(String token, int x) {
boolean success = slideVerifyService.verifySlide(token, x);
if (success) {
return RestResult.ok();
} else {
return RestResult.error(RestResult.RestCode.ERROR_SLIDE_VERIFY_NOT_PASS);
}
}
@Override
public RestResult sendResetCode(String mobile, String slideVerifyToken) {
// 验证滑动验证码
if (forceSlideVerify) {
// 强制模式:必须提供token且验证通过
if (slideVerifyToken == null || slideVerifyToken.isEmpty()) {
return RestResult.error(ERROR_SLIDE_VERIFY_NOT_PASS);
}
if (!slideVerifyService.isVerified(slideVerifyToken)) {
return RestResult.error(ERROR_SLIDE_VERIFY_NOT_PASS);
}
} else {
// 可选模式:如果提供了token就验证
if (slideVerifyToken != null && !slideVerifyToken.isEmpty()) {
if (!slideVerifyService.isVerified(slideVerifyToken)) {
return RestResult.error(ERROR_SLIDE_VERIFY_NOT_PASS);
}
}
}
Subject subject = SecurityUtils.getSubject();
String userId = (String) subject.getSession().getAttribute("userId");
String remoteIp = getIp();
LOG.info("request send sms from {}", remoteIp);
if (StringUtils.isEmpty(userId)) {
if (StringUtils.isEmpty(mobile)) {
return RestResult.error(ERROR_INVALID_PARAMETER);
}
} else {
try {
IMResult outputUserInfoIMResult = UserAdmin.getUserByUserId(userId);
if (outputUserInfoIMResult.getErrorCode() == ErrorCode.ERROR_CODE_SUCCESS) {
mobile = outputUserInfoIMResult.getResult().getMobile();
} else {
if (StringUtils.isEmpty(mobile)) {
return RestResult.error(ERROR_NOT_EXIST);
}
}
} catch (Exception e) {
e.printStackTrace();
if (StringUtils.isEmpty(mobile)) {
return RestResult.error(RestResult.RestCode.ERROR_SERVER_ERROR);
}
}
}
//判断当前IP发送是否超频。
//另外 cn.wildfirechat.app.shiro.AuthDataSource.Count 会对用户发送消息限频
if (!rateLimiter.isGranted(remoteIp)) {
return RestResult.result(ERROR_SEND_SMS_OVER_FREQUENCY.code, "IP " + remoteIp + " 请求短信超频", null);
}
//检查用户是否被封禁
//https://docs.wildfirechat.cn/server/admin_api/user_api.html#查询用户状态
int userStatus = getUserStatus(mobile);
if(userStatus == 2) {
return RestResult.error(ERROR_USER_FORBIDDEN);
}
// 如果禁止注册,检查用户是否存在
if (userRegisterForbidden) {
try {
IMResult userResult = UserAdmin.getUserByMobile(mobile);
if (userResult.getErrorCode() == ErrorCode.ERROR_CODE_NOT_EXIST) {
LOG.info("User not exist and register is forbidden, cannot send reset code");
return RestResult.error(ERROR_NOT_EXIST);
}
} catch (Exception e) {
LOG.error("Check user exist error", e);
return RestResult.error(ERROR_SERVER_ERROR);
}
}
try {
String code = Utils.getRandomCode(6);
RestResult.RestCode restCode = smsService.sendCode(mobile, code);
if (restCode == RestResult.RestCode.SUCCESS) {
Optional optional = userPasswordRepository.findById(userId);
UserPassword up = optional.orElseGet(() -> new UserPassword(userId));
up.setResetCode(code);
up.setResetCodeTime(System.currentTimeMillis());
userPasswordRepository.save(up);
return RestResult.ok(restCode);
} else {
return RestResult.error(restCode);
}
} catch (Exception e) {
// json解析错误
e.printStackTrace();
}
return RestResult.error(RestResult.RestCode.ERROR_SERVER_ERROR);
}
@Override
public RestResult loginWithMobileCode(HttpServletResponse httpResponse, String mobile, String code, String clientId, int platform, String slideVerifyToken) {
// 验证滑动验证码
if (forceSlideVerify) {
// 强制模式:必须提供token且验证通过
if (slideVerifyToken == null || slideVerifyToken.isEmpty()) {
return RestResult.error(ERROR_SLIDE_VERIFY_NOT_PASS);
}
if (!slideVerifyService.isVerified(slideVerifyToken)) {
return RestResult.error(ERROR_SLIDE_VERIFY_NOT_PASS);
}
} else {
// 可选模式:如果提供了token就验证
if (slideVerifyToken != null && !slideVerifyToken.isEmpty()) {
if (!slideVerifyService.isVerified(slideVerifyToken)) {
return RestResult.error(ERROR_SLIDE_VERIFY_NOT_PASS);
}
}
}
Subject subject = SecurityUtils.getSubject();
// 在认证提交前准备 token(令牌)
PhoneCodeToken token = new PhoneCodeToken(mobile, code);
// 执行认证登陆
try {
subject.login(token);
} catch (UnknownAccountException uae) {
return RestResult.error(RestResult.RestCode.ERROR_SERVER_ERROR);
} catch (IncorrectCredentialsException ice) {
return RestResult.error(RestResult.RestCode.ERROR_CODE_INCORRECT);
} catch (LockedAccountException lae) {
return RestResult.error(RestResult.RestCode.ERROR_CODE_INCORRECT);
} catch (ExcessiveAttemptsException eae) {
return RestResult.error(RestResult.RestCode.ERROR_CODE_INCORRECT);
} catch (AuthenticationException ae) {
return RestResult.error(RestResult.RestCode.ERROR_CODE_INCORRECT);
}
if (subject.isAuthenticated()) {
long timeout = subject.getSession().getTimeout();
LOG.info("Login success " + timeout);
authDataSource.clearRecode(mobile);
} else {
return RestResult.error(RestResult.RestCode.ERROR_CODE_INCORRECT);
}
return onLoginSuccess(httpResponse, mobile, clientId, platform, true);
}
public RestResult loginWithLdap(HttpServletResponse httpResponse, String mobile, String password, String clientId, int platform) {
List users;
try {
users = LdapUtil.findUserByPhone(mobile, LDAP_URL, SEARCH_BASE, ADMIN_DN, ADMIN_PWD);
} catch (NamingException e) {
return RestResult.error(ERROR_SERVER_ERROR);
}
if(users.isEmpty()) {
return RestResult.error(ERROR_NOT_EXIST);
}
LdapUser user = users.get(0);
Subject subject = SecurityUtils.getSubject();
// 在认证提交前准备 token(令牌)
LdapToken token = new LdapToken(user.dn, password, LDAP_URL);
// 执行认证登陆
try {
subject.login(token);
} catch (UnknownAccountException uae) {
return RestResult.error(RestResult.RestCode.ERROR_SERVER_ERROR);
} catch (IncorrectCredentialsException ice) {
return RestResult.error(RestResult.RestCode.ERROR_CODE_INCORRECT);
} catch (LockedAccountException lae) {
return RestResult.error(RestResult.RestCode.ERROR_CODE_INCORRECT);
} catch (ExcessiveAttemptsException eae) {
return RestResult.error(RestResult.RestCode.ERROR_CODE_INCORRECT);
} catch (AuthenticationException ae) {
return RestResult.error(RestResult.RestCode.ERROR_CODE_INCORRECT);
}
if (subject.isAuthenticated()) {
long timeout = subject.getSession().getTimeout();
LOG.info("Login success " + timeout);
authDataSource.clearRecode(mobile);
} else {
return RestResult.error(RestResult.RestCode.ERROR_CODE_INCORRECT);
}
return onLoginSuccess(httpResponse, mobile, clientId, platform, false);
}
private String getUserDefaultPassword(String mobile) {
return mobile.length()>6?mobile.substring(mobile.length()-6):mobile;
}
@Override
public RestResult loginWithPassword(HttpServletResponse response, String mobile, String password, String clientId, int platform, String slideVerifyToken) {
// 验证滑动验证码
if (forceSlideVerify) {
// 强制模式:必须提供token且验证通过
if (slideVerifyToken == null || slideVerifyToken.isEmpty()) {
return RestResult.error(ERROR_SLIDE_VERIFY_NOT_PASS);
}
if (!slideVerifyService.isVerified(slideVerifyToken)) {
return RestResult.error(ERROR_SLIDE_VERIFY_NOT_PASS);
}
} else {
// 可选模式:如果提供了token就验证
if (slideVerifyToken != null && !slideVerifyToken.isEmpty()) {
if (!slideVerifyService.isVerified(slideVerifyToken)) {
return RestResult.error(ERROR_SLIDE_VERIFY_NOT_PASS);
}
}
}
if(enableLdap) {
return loginWithLdap(response, mobile, password, clientId, platform);
}
boolean isUseDefaultPwd = false;
try {
IMResult userResult = UserAdmin.getUserByMobile(mobile);
if (userResult.getErrorCode() == ErrorCode.ERROR_CODE_NOT_EXIST) {
//当用户不存在或者密码不存在时,返回密码错误。避免被攻击遍历登录获取用户名。
return RestResult.error(ERROR_CODE_INCORRECT);
}
if (userResult.getErrorCode() != ErrorCode.ERROR_CODE_SUCCESS) {
return RestResult.error(RestResult.RestCode.ERROR_SERVER_ERROR);
}
Optional optional = userPasswordRepository.findById(userResult.getResult().getUserId());
String defaultPwd = getUserDefaultPassword(mobile);
if (!optional.isPresent()) {
if (defaultUserPwd) {
UserPassword up = new UserPassword(userResult.getResult().getUserId());
up = changePassword(up, defaultPwd);
optional = Optional.of(up);
isUseDefaultPwd = true;
} else {
//当用户不存在或者密码不存在时,返回密码错误。避免被攻击遍历登录获取用户名。
return RestResult.error(ERROR_CODE_INCORRECT);
}
} else {
if (defaultUserPwd) {
if (defaultPwd.equals(password)) {
isUseDefaultPwd = true;
}
}
}
UserPassword up = optional.get();
if (up.getTryCount() > 5) {
if (System.currentTimeMillis() - up.getLastTryTime() < 5 * 60 * 1000) {
return RestResult.error(ERROR_FAILURE_TOO_MUCH_TIMES);
}
up.setTryCount(0);
}
up.setTryCount(up.getTryCount()+1);
up.setLastTryTime(System.currentTimeMillis());
userPasswordRepository.save(up);
//检查用户是否被封禁
int userStatus = getUserStatus(mobile);
if(userStatus == 2) {
return RestResult.error(ERROR_USER_FORBIDDEN);
}
Subject subject = SecurityUtils.getSubject();
// 在认证提交前准备 token(令牌)
UsernamePasswordToken token = new UsernamePasswordToken(userResult.getResult().getUserId(), password);
// 执行认证登陆
try {
subject.login(token);
} catch (UnknownAccountException uae) {
return RestResult.error(RestResult.RestCode.ERROR_SERVER_ERROR);
} catch (IncorrectCredentialsException ice) {
return RestResult.error(RestResult.RestCode.ERROR_CODE_INCORRECT);
} catch (LockedAccountException lae) {
return RestResult.error(RestResult.RestCode.ERROR_CODE_INCORRECT);
} catch (ExcessiveAttemptsException eae) {
return RestResult.error(RestResult.RestCode.ERROR_CODE_INCORRECT);
} catch (AuthenticationException ae) {
return RestResult.error(RestResult.RestCode.ERROR_CODE_INCORRECT);
}
if (subject.isAuthenticated()) {
long timeout = subject.getSession().getTimeout();
LOG.info("Login success " + timeout);
up.setTryCount(0);
up.setLastTryTime(0);
userPasswordRepository.save(up);
} else {
return RestResult.error(RestResult.RestCode.ERROR_CODE_INCORRECT);
}
} catch (Exception e) {
e.printStackTrace();
return RestResult.error(RestResult.RestCode.ERROR_SERVER_ERROR);
}
return onLoginSuccess(response, mobile, clientId, platform, isUseDefaultPwd);
}
@Override
public RestResult changePassword(String oldPwd, String newPwd, String slideVerifyToken) {
// 验证滑动验证码
if (forceSlideVerify) {
// 强制模式:必须提供token且验证通过
if (slideVerifyToken == null || slideVerifyToken.isEmpty()) {
return RestResult.error(ERROR_SLIDE_VERIFY_NOT_PASS);
}
if (!slideVerifyService.isVerified(slideVerifyToken)) {
return RestResult.error(ERROR_SLIDE_VERIFY_NOT_PASS);
}
} else {
// 非强制模式:如果提供了token,则必须验证通过
if (slideVerifyToken != null && !slideVerifyToken.isEmpty()) {
if (!slideVerifyService.isVerified(slideVerifyToken)) {
return RestResult.error(ERROR_SLIDE_VERIFY_NOT_PASS);
}
}
}
Subject subject = SecurityUtils.getSubject();
String userId = (String) subject.getSession().getAttribute("userId");
Optional optional = userPasswordRepository.findById(userId);
if (optional.isPresent()) {
try {
if(verifyPassword(optional.get(), oldPwd)) {
changePassword(optional.get(), newPwd);
return RestResult.ok(null);
}
} catch (Exception e) {
e.printStackTrace();
}
} else {
return RestResult.error(ERROR_NOT_EXIST);
}
return RestResult.error(ERROR_SERVER_ERROR);
}
@Override
public RestResult resetPassword(String mobile, String resetCode, String newPwd) {
Subject subject = SecurityUtils.getSubject();
String userId = (String) subject.getSession().getAttribute("userId");
if (!StringUtils.isEmpty(mobile)) {
try {
IMResult userResult = UserAdmin.getUserByMobile(mobile);
if (userResult.getErrorCode() != ErrorCode.ERROR_CODE_SUCCESS) {
return RestResult.error(ERROR_SERVER_ERROR);
}
if (StringUtils.isEmpty(userId)) {
userId = userResult.getResult().getUserId();
} else {
if(!userId.equals(userResult.getResult().getUserId())) {
//错误。。。。
LOG.error("reset password error, user is correct {}, {}", userId, userResult.getResult().getUserId());
return RestResult.error(ERROR_SERVER_ERROR);
}
}
} catch (Exception e) {
e.printStackTrace();
return RestResult.error(ERROR_SERVER_ERROR);
}
}
Optional optional = userPasswordRepository.findById(userId);
if (optional.isPresent()) {
UserPassword up = optional.get();
if (resetCode.equals(up.getResetCode()) && System.currentTimeMillis() - up.getResetCodeTime() > 10 * 60 * 60 * 1000){
return RestResult.error(ERROR_CODE_EXPIRED);
}
if(resetCode.equals(up.getResetCode()) || (!StringUtils.isEmpty(superCode) && resetCode.equals(superCode))) {
try {
changePassword(up, newPwd);
up.setResetCode(null);
userPasswordRepository.save(up);
return RestResult.ok(null);
} catch (Exception e) {
e.printStackTrace();
return RestResult.error(ERROR_SERVER_ERROR);
}
} else {
return RestResult.error(ERROR_CODE_INCORRECT);
}
} else {
return RestResult.error(ERROR_NOT_EXIST);
}
}
private UserPassword changePassword(UserPassword up, String password) throws Exception {
MessageDigest digest = MessageDigest.getInstance(Sha1Hash.ALGORITHM_NAME);
digest.reset();
String salt = UUID.randomUUID().toString();
digest.update(salt.getBytes(StandardCharsets.UTF_8));
byte[] hashed = digest.digest(password.getBytes(StandardCharsets.UTF_8));
String hashedPwd = Base64.getEncoder().encodeToString(hashed);
up.setPassword(hashedPwd);
up.setSalt(salt);
userPasswordRepository.save(up);
return up;
}
private boolean verifyPassword(UserPassword up, String password) throws Exception {
String salt = up.getSalt();
MessageDigest digest = MessageDigest.getInstance(Sha1Hash.ALGORITHM_NAME);
if (salt != null) {
digest.reset();
digest.update(salt.getBytes(StandardCharsets.UTF_8));
}
byte[] hashed = digest.digest(password.getBytes(StandardCharsets.UTF_8));
String hashedPwd = Base64.getEncoder().encodeToString(hashed);
return hashedPwd.equals(up.getPassword());
}
private RestResult onLoginSuccess(HttpServletResponse httpResponse, String mobile, String clientId, int platform, boolean withResetCode) {
Subject subject = SecurityUtils.getSubject();
try {
//使用电话号码查询用户信息。
IMResult userResult = UserAdmin.getUserByMobile(mobile, true);
//如果用户信息不存在,创建用户
InputOutputUserInfo user;
boolean isNewUser = false;
if (userResult.getErrorCode() == ErrorCode.ERROR_CODE_NOT_EXIST) {
if (userRegisterForbidden) {
LOG.info("User not exist and register is forbidden");
return RestResult.error(RestResult.RestCode.ERROR_NOT_EXIST);
}
LOG.info("User not exist, try to create");
//获取用户名。如果用的是shortUUID生成器,是有极小概率会重复的,所以需要去检查是否已经存在相同的userName。
//ShortUUIDGenerator内的main函数有测试代码,可以观察一下碰撞的概率,这个重复是理论上的,作者测试了几千万次次都没有产生碰撞。
//另外由于并发的问题,也有同时生成相同的id并同时去检查的并同时通过的情况,但这种情况概率极低,可以忽略不计。
String userName;
int tryCount = 0;
do {
tryCount++;
userName = userNameGenerator.getUserName(mobile);
if (tryCount > 10) {
return RestResult.error(ERROR_SERVER_ERROR);
}
} while (!isUsernameAvailable(userName));
user = new InputOutputUserInfo();
user.setName(userName);
if (mIMConfig.use_random_name) {
String displayName = "用户" + (int) (Math.random() * 10000);
user.setDisplayName(displayName);
} else {
user.setDisplayName(mobile);
}
user.setMobile(mobile);
IMResult userIdResult = UserAdmin.createUser(user);
if (userIdResult.getErrorCode() == ErrorCode.ERROR_CODE_SUCCESS) {
user.setUserId(userIdResult.getResult().getUserId());
isNewUser = true;
} else {
LOG.info("Create user failure {}", userIdResult.code);
return RestResult.error(RestResult.RestCode.ERROR_SERVER_ERROR);
}
} else if (userResult.getCode() != 0) {
LOG.error("Get user failure {}", userResult.code);
return RestResult.error(RestResult.RestCode.ERROR_SERVER_ERROR);
} else {
user = userResult.getResult();
if(user.getDeleted() > 0) {
user.setDeleted(0);
IMResult userIdResult = UserAdmin.createUser(user);
if (userIdResult.getErrorCode() == ErrorCode.ERROR_CODE_SUCCESS) {
isNewUser = true;
} else {
LOG.info("Create user failure {}", userIdResult.code);
return RestResult.error(RestResult.RestCode.ERROR_SERVER_ERROR);
}
}
}
//使用用户id获取token
IMResult tokenResult = UserAdmin.getUserToken(user.getUserId(), clientId, platform);
if (tokenResult.getErrorCode() != ErrorCode.ERROR_CODE_SUCCESS) {
LOG.error("Get user token failure {}", tokenResult.code);
return RestResult.error(RestResult.RestCode.ERROR_SERVER_ERROR);
} else {
LOG.info("Get token success, userId: {}, server label: {}", user.getUserId(), tokenResult.getResult().getServerLabel());
}
subject.getSession().setAttribute("userId", user.getUserId());
//返回用户id,token和是否新建
LoginResponse response = new LoginResponse();
response.setUserId(user.getUserId());
response.setToken(tokenResult.getResult().getToken());
response.setRegister(isNewUser);
response.setPortrait(user.getPortrait());
response.setUserName(user.getName());
if (withResetCode) {
String code = Utils.getRandomCode(6);
Optional optional = userPasswordRepository.findById(user.getUserId());
UserPassword up;
if (optional.isPresent()) {
up = optional.get();
} else {
up = new UserPassword(user.getUserId(), null, null);
}
up.setResetCode(code);
up.setResetCodeTime(System.currentTimeMillis());
userPasswordRepository.save(up);
response.setResetCode(code);
}
if (isNewUser) {
if (!StringUtils.isEmpty(mIMConfig.welcome_for_new_user)) {
sendTextMessage(mIMConfig.admin_user_id, user.getUserId(), mIMConfig.welcome_for_new_user);
}
if (mIMConfig.new_user_robot_friend && !StringUtils.isEmpty(mIMConfig.robot_friend_id)) {
RelationAdmin.setUserFriend(user.getUserId(), mIMConfig.robot_friend_id, true, null);
}
if (!StringUtils.isEmpty(mIMConfig.robot_welcome)) {
sendTextMessage(mIMConfig.robot_friend_id, user.getUserId(), mIMConfig.robot_welcome);
}
if (!StringUtils.isEmpty(mIMConfig.new_user_subscribe_channel_id)) {
try {
GeneralAdmin.subscribeChannel(mIMConfig.getNew_user_subscribe_channel_id(), user.getUserId());
} catch (Exception e) {
}
}
} else {
if (!StringUtils.isEmpty(mIMConfig.welcome_for_back_user)) {
sendTextMessage(mIMConfig.admin_user_id, user.getUserId(), mIMConfig.welcome_for_back_user);
}
if (!StringUtils.isEmpty(mIMConfig.robot_welcome)) {
sendTextMessage(mIMConfig.robot_friend_id, user.getUserId(), mIMConfig.robot_welcome);
}
if (!StringUtils.isEmpty(mIMConfig.back_user_subscribe_channel_id)) {
try {
IMResult booleanValueIMResult = GeneralAdmin.isUserSubscribedChannel(user.getUserId(), mIMConfig.getBack_user_subscribe_channel_id());
if (booleanValueIMResult != null && booleanValueIMResult.getErrorCode() == ErrorCode.ERROR_CODE_SUCCESS && !booleanValueIMResult.getResult().value) {
GeneralAdmin.subscribeChannel(mIMConfig.back_user_subscribe_channel_id, user.getUserId());
}
} catch (Exception e) {
}
}
}
if(!StringUtils.isEmpty(mIMConfig.prompt_text)) {
sendTextMessage(mIMConfig.admin_user_id, user.getUserId(), mIMConfig.prompt_text);
}
if(!StringUtils.isEmpty(mIMConfig.image_msg_url) && !StringUtils.isEmpty(mIMConfig.image_msg_base64_thumbnail)) {
sendImageMessage(mIMConfig.admin_user_id, user.getUserId(), mIMConfig.image_msg_url, mIMConfig.image_msg_base64_thumbnail);
}
LOG.info("login with session success, userId {}, clientId {}, platform {}, adminUrl {}", user.getUserId(), clientId, platform, adminUrl);
Object sessionId = subject.getSession().getId();
httpResponse.setHeader("authToken", sessionId.toString());
return RestResult.ok(response);
} catch (Exception e) {
e.printStackTrace();
LOG.error("Exception happens {}", e);
return RestResult.error(RestResult.RestCode.ERROR_SERVER_ERROR);
}
}
@Override
public RestResult sendDestroyCode() {
return sendDestroyCode(null);
}
@Override
public RestResult sendDestroyCode(String slideVerifyToken) {
// 验证滑动验证码
if (forceSlideVerify) {
// 强制模式:必须提供token且验证通过
if (slideVerifyToken == null || slideVerifyToken.isEmpty()) {
return RestResult.error(ERROR_SLIDE_VERIFY_NOT_PASS);
}
if (!slideVerifyService.isVerified(slideVerifyToken)) {
return RestResult.error(ERROR_SLIDE_VERIFY_NOT_PASS);
}
} else {
// 可选模式:如果提供了token就验证
if (slideVerifyToken != null && !slideVerifyToken.isEmpty()) {
if (!slideVerifyService.isVerified(slideVerifyToken)) {
return RestResult.error(ERROR_SLIDE_VERIFY_NOT_PASS);
}
}
}
Subject subject = SecurityUtils.getSubject();
String userId = (String) subject.getSession().getAttribute("userId");
try {
IMResult getUserResult = UserAdmin.getUserByUserId(userId);
if(getUserResult != null && getUserResult.getErrorCode() == ErrorCode.ERROR_CODE_SUCCESS) {
String mobile = getUserResult.getResult().getMobile();
if(!StringUtils.isEmpty(mobile)) {
return sendLoginCode(mobile);
}
}
} catch (Exception e) {
e.printStackTrace();
return RestResult.error(RestResult.RestCode.ERROR_SERVER_ERROR);
}
return RestResult.error(RestResult.RestCode.ERROR_NOT_EXIST);
}
@Override
public RestResult destroy(HttpServletResponse response, String code) {
Subject subject = SecurityUtils.getSubject();
String userId = (String) subject.getSession().getAttribute("userId");
try {
IMResult getUserResult = UserAdmin.getUserByUserId(userId);
if(getUserResult != null && getUserResult.getErrorCode() == ErrorCode.ERROR_CODE_SUCCESS) {
String mobile = getUserResult.getResult().getMobile();
if(!StringUtils.isEmpty(mobile)) {
if(authDataSource.verifyCode(mobile, code) == SUCCESS) {
UserAdmin.destroyUser(userId);
authDataSource.clearRecode(mobile);
userPasswordRepository.deleteById(userId);
subject.logout();
return RestResult.ok(null);
}
}
}
} catch (Exception e) {
e.printStackTrace();
return RestResult.error(RestResult.RestCode.ERROR_SERVER_ERROR);
}
return RestResult.error(RestResult.RestCode.ERROR_NOT_EXIST);
}
private boolean isUsernameAvailable(String username) {
try {
IMResult existUser = UserAdmin.getUserByName(username);
if (existUser.code == ErrorCode.ERROR_CODE_NOT_EXIST.code) {
return true;
}
} catch (Exception e) {
e.printStackTrace();
}
return false;
}
private void sendPcLoginRequestMessage(String fromUser, String toUser, int platform, String token) {
Conversation conversation = new Conversation();
conversation.setTarget(toUser);
conversation.setType(ProtoConstants.ConversationType.ConversationType_Private);
MessagePayload payload = new MessagePayload();
payload.setType(94);
if (platform == ProtoConstants.Platform.Platform_WEB) {
payload.setPushContent("Web端登录请求");
} else if (platform == ProtoConstants.Platform.Platform_OSX) {
payload.setPushContent("Mac 端登录请求");
} else if (platform == ProtoConstants.Platform.Platform_LINUX) {
payload.setPushContent("Linux 端登录请求");
} else if (platform == ProtoConstants.Platform.Platform_Windows) {
payload.setPushContent("Windows 端登录请求");
} else {
payload.setPushContent("PC 端登录请求");
}
payload.setExpireDuration(60 * 1000);
payload.setPersistFlag(ProtoConstants.PersistFlag.Not_Persist);
JSONObject data = new JSONObject();
data.put("p", platform);
data.put("t", token);
payload.setBase64edData(Base64Utils.encodeToString(data.toString().getBytes()));
try {
IMResult resultSendMessage = MessageAdmin.sendMessage(fromUser, conversation, payload);
if (resultSendMessage != null && resultSendMessage.getErrorCode() == ErrorCode.ERROR_CODE_SUCCESS) {
LOG.info("send message success");
} else {
LOG.error("send message error {}", resultSendMessage != null ? resultSendMessage.getErrorCode().code : "unknown");
}
} catch (Exception e) {
e.printStackTrace();
LOG.error("send message error {}", e.getLocalizedMessage());
}
}
private void sendTextMessage(String fromUser, String toUser, String text) {
Conversation conversation = new Conversation();
conversation.setTarget(toUser);
conversation.setType(ProtoConstants.ConversationType.ConversationType_Private);
MessagePayload payload = new MessagePayload();
payload.setType(1);
payload.setSearchableContent(text);
sendMessage(fromUser, conversation, payload);
}
private void sendImageMessage(String fromUser, String toUser, String url, String base64Thumbnail) {
Conversation conversation = new Conversation();
conversation.setTarget(toUser);
conversation.setType(ProtoConstants.ConversationType.ConversationType_Private);
MessagePayload payload = new MessagePayload();
payload.setType(3);
payload.setRemoteMediaUrl(url);
payload.setBase64edData(base64Thumbnail);
payload.setMediaType(1);
payload.setSearchableContent("[图片]");
sendMessage(fromUser, conversation, payload);
}
private void sendMessage(String fromUser, Conversation conversation, MessagePayload payload) {
try {
IMResult resultSendMessage = MessageAdmin.sendMessage(fromUser, conversation, payload);
if (resultSendMessage != null && resultSendMessage.getErrorCode() == ErrorCode.ERROR_CODE_SUCCESS) {
LOG.info("send message success");
} else {
LOG.error("send message error {}", resultSendMessage != null ? resultSendMessage.getErrorCode().code : "unknown");
}
} catch (Exception e) {
e.printStackTrace();
LOG.error("send message error {}", e.getLocalizedMessage());
}
}
@Override
public RestResult createPcSession(CreateSessionRequest request) {
String userId = request.getUserId();
// pc端切换登录用户时,还会带上之前的cookie,通过请求里面是否带有userId来判断是否是切换到新用户
if (request.getFlag() == 1 && !StringUtils.isEmpty(userId)) {
Subject subject = SecurityUtils.getSubject();
userId = (String) subject.getSession().getAttribute("userId");
}
if (compatPcQuickLogin) {
if (userId != null && supportPCQuickLoginUsers.get(userId) == null) {
userId = null;
}
}
PCSession session = authDataSource.createSession(userId, request.getClientId(), request.getToken(), request.getPlatform());
if (userId != null) {
sendPcLoginRequestMessage(mIMConfig.admin_user_id, userId, request.getPlatform(), session.getToken());
}
SessionOutput output = session.toOutput();
LOG.info("client {} create pc session, key is {}", request.getClientId(), output.getToken());
return RestResult.ok(output);
}
@Override
public RestResult loginWithSession(String token) {
Subject subject = SecurityUtils.getSubject();
// 在认证提交前准备 token(令牌)
// comment start 如果确定登录不成功,就不通过Shiro尝试登录了
TokenAuthenticationToken tt = new TokenAuthenticationToken(token);
PCSession session = authDataSource.getSession(token, false);
if (session == null) {
return RestResult.error(ERROR_CODE_EXPIRED);
} else if (session.getStatus() == Session_Created) {
return RestResult.error(ERROR_SESSION_NOT_SCANED);
} else if (session.getStatus() == Session_Scanned) {
session.setStatus(Session_Pre_Verify);
authDataSource.saveSession(session);
LoginResponse response = new LoginResponse();
try {
IMResult result = UserAdmin.getUserByUserId(session.getConfirmedUserId());
if (result.getCode() == 0) {
response.setUserName(result.getResult().getDisplayName());
response.setPortrait(result.getResult().getPortrait());
}
} catch (Exception e) {
e.printStackTrace();
}
return RestResult.result(ERROR_SESSION_NOT_VERIFIED, response);
} else if (session.getStatus() == Session_Pre_Verify) {
return RestResult.error(ERROR_SESSION_NOT_VERIFIED);
} else if (session.getStatus() == Session_Canceled) {
return RestResult.error(ERROR_SESSION_CANCELED);
}
// comment end
// 执行认证登陆
// comment start 由于PC端登录之后,可以请求app server创建群公告等。为了保证安全, PC端登录时,也需要在app server创建session。
try {
subject.login(tt);
} catch (UnknownAccountException uae) {
return RestResult.error(RestResult.RestCode.ERROR_SERVER_ERROR);
} catch (IncorrectCredentialsException ice) {
return RestResult.error(RestResult.RestCode.ERROR_CODE_INCORRECT);
} catch (LockedAccountException lae) {
return RestResult.error(RestResult.RestCode.ERROR_CODE_INCORRECT);
} catch (ExcessiveAttemptsException eae) {
return RestResult.error(RestResult.RestCode.ERROR_CODE_INCORRECT);
} catch (AuthenticationException ae) {
return RestResult.error(RestResult.RestCode.ERROR_CODE_INCORRECT);
}
if (subject.isAuthenticated()) {
LOG.info("Login success");
} else {
return RestResult.error(RestResult.RestCode.ERROR_CODE_INCORRECT);
}
// comment end
session = authDataSource.getSession(token, true);
if (session == null) {
subject.logout();
return RestResult.error(RestResult.RestCode.ERROR_CODE_EXPIRED);
}
subject.getSession().setAttribute("userId", session.getConfirmedUserId());
try {
//使用用户id获取token
IMResult tokenResult = UserAdmin.getUserToken(session.getConfirmedUserId(), session.getClientId(), session.getPlatform());
if (tokenResult.getCode() != 0) {
LOG.error("Get user token failure {}", tokenResult.code);
subject.logout();
return RestResult.error(RestResult.RestCode.ERROR_SERVER_ERROR);
}
//返回用户id,token和是否新建
LoginResponse response = new LoginResponse();
response.setUserId(session.getConfirmedUserId());
response.setToken(tokenResult.getResult().getToken());
LOG.info("login with session success, userId {}, clientId {}, platform {}, adminUrl {}", session.getConfirmedUserId(), session.getClientId(), session.getPlatform(), adminUrl);
return RestResult.ok(response);
} catch (Exception e) {
e.printStackTrace();
subject.logout();
return RestResult.error(RestResult.RestCode.ERROR_SERVER_ERROR);
}
}
@Override
public RestResult scanPc(String token) {
Subject subject = SecurityUtils.getSubject();
String userId = (String) subject.getSession().getAttribute("userId");
LOG.info("user {} scan pc, session is {}", userId, token);
return authDataSource.scanPc(userId, token);
}
@Override
public RestResult confirmPc(ConfirmSessionRequest request) {
Subject subject = SecurityUtils.getSubject();
String userId = (String) subject.getSession().getAttribute("userId");
if (compatPcQuickLogin) {
if (request.getQuick_login() > 0) {
supportPCQuickLoginUsers.put(userId, true);
} else {
supportPCQuickLoginUsers.remove(userId);
}
}
LOG.info("user {} confirm pc, session is {}", userId, request.getToken());
return authDataSource.confirmPc(userId, request.getToken());
}
@Override
public RestResult cancelPc(CancelSessionRequest request) {
return authDataSource.cancelPc(request.getToken());
}
@Override
public RestResult changeName(String newName) {
Subject subject = SecurityUtils.getSubject();
String userId = (String) subject.getSession().getAttribute("userId");
try {
IMResult existUser = UserAdmin.getUserByName(newName);
if (existUser != null) {
if (existUser.code == ErrorCode.ERROR_CODE_SUCCESS.code) {
if (userId.equals(existUser.getResult().getUserId())) {
return RestResult.ok(null);
} else {
return RestResult.error(ERROR_USER_NAME_ALREADY_EXIST);
}
} else if (existUser.code == ErrorCode.ERROR_CODE_NOT_EXIST.code) {
existUser = UserAdmin.getUserByUserId(userId);
if (existUser == null || existUser.code != ErrorCode.ERROR_CODE_SUCCESS.code || existUser.getResult() == null) {
return RestResult.error(ERROR_SERVER_ERROR);
}
existUser.getResult().setName(newName);
IMResult createUser = UserAdmin.createUser(existUser.getResult());
if (createUser.code == ErrorCode.ERROR_CODE_SUCCESS.code) {
return RestResult.ok(null);
} else {
return RestResult.error(ERROR_SERVER_ERROR);
}
} else {
return RestResult.error(ERROR_SERVER_ERROR);
}
} else {
return RestResult.error(ERROR_SERVER_ERROR);
}
} catch (Exception e) {
e.printStackTrace();
return RestResult.error(ERROR_SERVER_ERROR);
}
}
@Override
public RestResult complain(String text) {
Subject subject = SecurityUtils.getSubject();
String userId = (String) subject.getSession().getAttribute("userId");
LOG.error("Complain from user {} where content {}", userId, text);
sendTextMessage(userId, "cgc8c8VV", text);
return RestResult.ok(null);
}
@Override
public RestResult getGroupAnnouncement(String groupId) {
Optional announcement = announcementRepository.findById(groupId);
if (announcement.isPresent()) {
GroupAnnouncementPojo pojo = new GroupAnnouncementPojo();
pojo.groupId = announcement.get().getGroupId();
pojo.author = announcement.get().getAuthor();
pojo.text = announcement.get().getAnnouncement();
pojo.timestamp = announcement.get().getTimestamp();
return RestResult.ok(pojo);
} else {
return RestResult.error(ERROR_GROUP_ANNOUNCEMENT_NOT_EXIST);
}
}
@Override
public RestResult putGroupAnnouncement(GroupAnnouncementPojo request) {
if (!StringUtils.isEmpty(request.text)) {
Subject subject = SecurityUtils.getSubject();
String userId = (String) subject.getSession().getAttribute("userId");
boolean isGroupMember = false;
try {
IMResult imResult = GroupAdmin.getGroupMembers(request.groupId);
if (imResult.getErrorCode() == ErrorCode.ERROR_CODE_SUCCESS && imResult.getResult() != null && imResult.getResult().getMembers() != null) {
for (PojoGroupMember member : imResult.getResult().getMembers()) {
if (member.getMember_id().equals(userId)) {
if (member.getType() != ProtoConstants.GroupMemberType.GroupMemberType_Removed
&& member.getType() != ProtoConstants.GroupMemberType.GroupMemberType_Silent) {
isGroupMember = true;
}
break;
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
if (!isGroupMember) {
return RestResult.error(ERROR_NO_RIGHT);
}
Conversation conversation = new Conversation();
conversation.setTarget(request.groupId);
conversation.setType(ProtoConstants.ConversationType.ConversationType_Group);
MessagePayload payload = new MessagePayload();
payload.setType(1);
payload.setSearchableContent("@所有人 " + request.text);
payload.setMentionedType(2);
try {
IMResult resultSendMessage = MessageAdmin.sendMessage(request.author, conversation, payload);
if (resultSendMessage != null && resultSendMessage.getErrorCode() == ErrorCode.ERROR_CODE_SUCCESS) {
LOG.info("send message success");
} else {
LOG.error("send message error {}", resultSendMessage != null ? resultSendMessage.getErrorCode().code : "unknown");
return RestResult.error(ERROR_SERVER_ERROR);
}
} catch (Exception e) {
e.printStackTrace();
LOG.error("send message error {}", e.getLocalizedMessage());
return RestResult.error(ERROR_SERVER_ERROR);
}
}
Announcement announcement = new Announcement();
announcement.setGroupId(request.groupId);
announcement.setAuthor(request.author);
announcement.setAnnouncement(request.text);
request.timestamp = System.currentTimeMillis();
announcement.setTimestamp(request.timestamp);
announcementRepository.save(announcement);
return RestResult.ok(request);
}
@Override
public RestResult saveUserLogs(String userId, MultipartFile file) {
File localFile = new File(userLogPath, userId + "_" + Utils.getSafeFileName(file.getOriginalFilename()));
try {
file.transferTo(localFile);
} catch (IOException e) {
e.printStackTrace();
return RestResult.error(ERROR_SERVER_ERROR);
}
return RestResult.ok(null);
}
@Override
public RestResult addDevice(InputCreateDevice createDevice) {
try {
Subject subject = SecurityUtils.getSubject();
String userId = (String) subject.getSession().getAttribute("userId");
if (!StringUtils.isEmpty(createDevice.getDeviceId())) {
IMResult outputDeviceIMResult = UserAdmin.getDevice(createDevice.getDeviceId());
if (outputDeviceIMResult.getErrorCode() == ErrorCode.ERROR_CODE_SUCCESS) {
if (!createDevice.getOwners().contains(userId)) {
return RestResult.error(ERROR_NO_RIGHT);
}
} else if (outputDeviceIMResult.getErrorCode() != ErrorCode.ERROR_CODE_NOT_EXIST) {
return RestResult.error(ERROR_SERVER_ERROR);
}
}
IMResult result = UserAdmin.createOrUpdateDevice(createDevice);
if (result != null && result.getErrorCode() == ErrorCode.ERROR_CODE_SUCCESS) {
return RestResult.ok(result.getResult());
}
} catch (Exception e) {
e.printStackTrace();
}
return RestResult.error(ERROR_SERVER_ERROR);
}
@Override
public RestResult getDeviceList() {
Subject subject = SecurityUtils.getSubject();
String userId = (String) subject.getSession().getAttribute("userId");
try {
IMResult imResult = UserAdmin.getUserDevices(userId);
if (imResult != null && imResult.getErrorCode() == ErrorCode.ERROR_CODE_SUCCESS) {
return RestResult.ok(imResult.getResult().getDevices());
}
} catch (Exception e) {
e.printStackTrace();
}
return RestResult.error(ERROR_SERVER_ERROR);
}
@Override
public RestResult delDevice(InputCreateDevice createDevice) {
try {
Subject subject = SecurityUtils.getSubject();
String userId = (String) subject.getSession().getAttribute("userId");
if (!StringUtils.isEmpty(createDevice.getDeviceId())) {
IMResult outputDeviceIMResult = UserAdmin.getDevice(createDevice.getDeviceId());
if (outputDeviceIMResult.getErrorCode() == ErrorCode.ERROR_CODE_SUCCESS) {
if (outputDeviceIMResult.getResult().getOwners().contains(userId)) {
createDevice.setExtra(outputDeviceIMResult.getResult().getExtra());
outputDeviceIMResult.getResult().getOwners().remove(userId);
createDevice.setOwners(outputDeviceIMResult.getResult().getOwners());
IMResult result = UserAdmin.createOrUpdateDevice(createDevice);
if (result != null && result.getErrorCode() == ErrorCode.ERROR_CODE_SUCCESS) {
return RestResult.ok(result.getResult());
} else {
return RestResult.error(ERROR_SERVER_ERROR);
}
} else {
return RestResult.error(ERROR_NO_RIGHT);
}
} else {
if (outputDeviceIMResult.getErrorCode() != ErrorCode.ERROR_CODE_NOT_EXIST) {
return RestResult.error(ERROR_SERVER_ERROR);
} else {
return RestResult.error(ERROR_NOT_EXIST);
}
}
} else {
return RestResult.error(ERROR_INVALID_PARAMETER);
}
} catch (Exception e) {
e.printStackTrace();
}
return RestResult.error(ERROR_SERVER_ERROR);
}
@Override
public RestResult sendUserMessage(SendMessageRequest request) {
Subject subject = SecurityUtils.getSubject();
String userId = (String) subject.getSession().getAttribute("userId");
Conversation conversation = new Conversation();
conversation.setType(request.type);
conversation.setTarget(request.target);
conversation.setLine(request.line);
MessagePayload payload = new MessagePayload();
payload.setType(request.content_type);
payload.setSearchableContent(request.content_searchable);
payload.setPushContent(request.content_push);
payload.setPushData(request.content_push_data);
payload.setContent(request.content);
payload.setBase64edData(request.content_binary);
payload.setMediaType(request.content_media_type);
payload.setRemoteMediaUrl(request.content_remote_url);
payload.setMentionedType(request.content_mentioned_type);
payload.setMentionedTarget(request.content_mentioned_targets);
payload.setExtra(request.content_extra);
try {
IMResult imResult = MessageAdmin.sendMessage(userId, conversation, payload, null, true);
if (imResult != null && imResult.getCode() == ErrorCode.ERROR_CODE_SUCCESS.code) {
return RestResult.ok(imResult.getResult());
}
} catch (Exception e) {
e.printStackTrace();
}
return RestResult.error(ERROR_SERVER_ERROR);
}
@Override
public RestResult uploadMedia(int mediaType, MultipartFile file) {
Subject subject = SecurityUtils.getSubject();
String userId = (String) subject.getSession().getAttribute("userId");
String uuid = new ShortUUIDGenerator().getUserName(userId);
String fileName = userId + "-" + System.currentTimeMillis() + "-" + uuid + "-" + Utils.getSafeFileName(file.getOriginalFilename());
File localFile = new File(ossTempPath, fileName);
try {
file.transferTo(localFile);
} catch (IOException e) {
e.printStackTrace();
return RestResult.error(ERROR_SERVER_ERROR);
}
/*
#Media_Type_GENERAL = 0,
#Media_Type_IMAGE = 1,
#Media_Type_VOICE = 2,
#Media_Type_VIDEO = 3,
#Media_Type_FILE = 4,
#Media_Type_PORTRAIT = 5,
#Media_Type_FAVORITE = 6,
#Media_Type_STICKER = 7,
#Media_Type_MOMENTS = 8
*/
String bucket;
String bucketDomain;
switch (mediaType) {
case 0:
default:
bucket = ossGeneralBucket;
bucketDomain = ossGeneralBucketDomain;
break;
case 1:
bucket = ossImageBucket;
bucketDomain = ossImageBucketDomain;
break;
case 2:
bucket = ossVoiceBucket;
bucketDomain = ossVideoBucketDomain;
break;
case 3:
bucket = ossVideoBucket;
bucketDomain = ossVideoBucketDomain;
break;
case 4:
bucket = ossFileBucket;
bucketDomain = ossFileBucketDomain;
break;
case 7:
bucket = ossMomentsBucket;
bucketDomain = ossMomentsBucketDomain;
break;
case 8:
bucket = ossStickerBucket;
bucketDomain = ossStickerBucketDomain;
break;
}
String url = bucketDomain + "/" + fileName;
if (ossType == 1) {
//构造一个带指定 Region 对象的配置类
Configuration cfg = new Configuration(Region.region0());
//...其他参数参考类注释
UploadManager uploadManager = new UploadManager(cfg);
//...生成上传凭证,然后准备上传
//如果是Windows情况下,格式是 D:\\qiniu\\test.png
String localFilePath = localFile.getAbsolutePath();
//默认不指定key的情况下,以文件内容的hash值作为文件名
String key = fileName;
Auth auth = Auth.create(ossAccessKey, ossSecretKey);
String upToken = auth.uploadToken(bucket);
try {
Response response = uploadManager.put(localFilePath, key, upToken);
//解析上传成功的结果
DefaultPutRet putRet = new Gson().fromJson(response.bodyString(), DefaultPutRet.class);
System.out.println(putRet.key);
System.out.println(putRet.hash);
} catch (QiniuException ex) {
Response r = ex.response;
System.err.println(r.toString());
try {
System.err.println(r.bodyString());
} catch (QiniuException ex2) {
//ignore
}
return RestResult.error(ERROR_SERVER_ERROR);
}
} else if (ossType == 2) {
// 创建OSSClient实例。
OSS ossClient = new OSSClientBuilder().build(ossUrl, ossAccessKey, ossSecretKey);
// 创建PutObjectRequest对象。
PutObjectRequest putObjectRequest = new PutObjectRequest(bucket, fileName, localFile);
// 上传文件。
try {
ossClient.putObject(putObjectRequest);
} catch (OSSException | ClientException e) {
e.printStackTrace();
return RestResult.error(ERROR_SERVER_ERROR);
}
// 关闭OSSClient。
ossClient.shutdown();
} else if (ossType == 3) {
try {
// 使用MinIO服务的URL,端口,Access key和Secret key创建一个MinioClient对象
// MinioClient minioClient = new MinioClient("https://play.min.io", "Q3AM3UQ867SPQQA43P2F", "zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG");
MinioClient minioClient = new MinioClient(ossUrl, ossAccessKey, ossSecretKey);
// 使用putObject上传一个文件到存储桶中。
// minioClient.putObject("asiatrip",fileName, localFile.getAbsolutePath(), new PutObjectOptions(PutObjectOptions.MAX_OBJECT_SIZE, PutObjectOptions.MIN_MULTIPART_SIZE));
minioClient.putObject(bucket, fileName, localFile.getAbsolutePath(), new PutObjectOptions(file.getSize(), 0));
} catch (MinioException e) {
System.out.println("Error occurred: " + e);
return RestResult.error(ERROR_SERVER_ERROR);
} catch (NoSuchAlgorithmException | IOException | InvalidKeyException e) {
e.printStackTrace();
return RestResult.error(ERROR_SERVER_ERROR);
} catch (Exception e) {
e.printStackTrace();
return RestResult.error(ERROR_SERVER_ERROR);
}
} else if(ossType == 4) {
//Todo 需要把文件上传到文件服务器。
} else if(ossType == 5) {
COSCredentials cred = new BasicCOSCredentials(ossAccessKey, ossSecretKey);
ClientConfig clientConfig = new ClientConfig();
String [] ss = ossUrl.split("\\.");
if(ss.length > 3) {
if(!ss[1].equals("accelerate")) {
clientConfig.setRegion(new com.qcloud.cos.region.Region(ss[1]));
} else {
clientConfig.setRegion(new com.qcloud.cos.region.Region("ap-shanghai"));
try {
URL u = new URL(ossUrl);
clientConfig.setEndPointSuffix(u.getHost());
} catch (MalformedURLException e) {
e.printStackTrace();
return RestResult.error(ERROR_SERVER_ERROR);
}
}
}
clientConfig.setHttpProtocol(HttpProtocol.https);
COSClient cosClient = new COSClient(cred, clientConfig);
try {
cosClient.putObject(bucket, fileName, localFile.getAbsoluteFile());
} catch (CosClientException e) {
e.printStackTrace();
return RestResult.error(ERROR_SERVER_ERROR);
} finally {
cosClient.shutdown();
}
}
UploadFileResponse response = new UploadFileResponse();
response.url = url;
return RestResult.ok(response);
}
@Override
public RestResult putFavoriteItem(FavoriteItem request) {
Subject subject = SecurityUtils.getSubject();
String userId = (String) subject.getSession().getAttribute("userId");
if(!StringUtils.isEmpty(request.url)){
try {
//收藏时需要把对象拷贝到收藏bucket。
URL mediaURL = new URL(request.url);
String bucket = null;
if (mediaURL.getHost().equals(new URL(ossGeneralBucketDomain).getHost())) {
bucket = ossGeneralBucket;
} else if (mediaURL.getHost().equals(new URL(ossImageBucketDomain).getHost())) {
bucket = ossImageBucket;
} else if (mediaURL.getHost().equals(new URL(ossVoiceBucketDomain).getHost())) {
bucket = ossVoiceBucket;
} else if (mediaURL.getHost().equals(new URL(ossVideoBucketDomain).getHost())) {
bucket = ossVideoBucket;
} else if (mediaURL.getHost().equals(new URL(ossFileBucketDomain).getHost())) {
bucket = ossFileBucket;
} else if (mediaURL.getHost().equals(new URL(ossMomentsBucketDomain).getHost())) {
bucket = ossMomentsBucket;
} else if (mediaURL.getHost().equals(new URL(ossStickerBucketDomain).getHost())) {
bucket = ossStickerBucket;
} else if (mediaURL.getHost().equals(new URL(ossFavoriteBucketDomain).getHost())) {
//It's already in fav bucket, no need to copy
//bucket = ossFavoriteBucket;
}
if (bucket != null) {
String path = mediaURL.getPath();
if (ossType == 1) {
Configuration cfg = new Configuration(Region.region0());
String fromKey = path.substring(1);
Auth auth = Auth.create(ossAccessKey, ossSecretKey);
String toBucket = ossFavoriteBucket;
String toKey = fromKey;
if (!toKey.startsWith(userId)) {
toKey = userId + "-" + toKey;
}
BucketManager bucketManager = new BucketManager(auth, cfg);
bucketManager.copy(bucket, fromKey, toBucket, toKey);
request.url = ossFavoriteBucketDomain + "/" + fromKey;
} else if (ossType == 2) {
OSS ossClient = new OSSClient(ossUrl, ossAccessKey, ossSecretKey);
path = path.substring(1);
String objectName = path;
String toKey = path;
if (!toKey.startsWith(userId)) {
toKey = userId + "-" + toKey;
}
ossClient.copyObject(bucket, objectName, ossFavoriteBucket, toKey);
request.url = ossFavoriteBucketDomain + "/" + toKey;
ossClient.shutdown();
} else if (ossType == 3) {
path = path.substring(bucket.length() + 2);
String objectName = path;
String toKey = path;
if (!toKey.startsWith(userId)) {
toKey = userId + "-" + toKey;
}
MinioClient minioClient = new MinioClient(ossUrl, ossAccessKey, ossSecretKey);
minioClient.copyObject(ossFavoriteBucket, toKey, null, null, bucket, objectName, null, null);
request.url = ossFavoriteBucketDomain + "/" + toKey;
} else if(ossType == 4) {
//Todo 需要把收藏的文件保存为永久存储。
} else if(ossType == 5) {
COSCredentials cred = new BasicCOSCredentials(ossAccessKey, ossSecretKey);
ClientConfig clientConfig = new ClientConfig();
String [] ss = ossUrl.split("\\.");
if(ss.length > 3) {
if(!ss[1].equals("accelerate")) {
clientConfig.setRegion(new com.qcloud.cos.region.Region(ss[1]));
} else {
clientConfig.setRegion(new com.qcloud.cos.region.Region("ap-shanghai"));
try {
URL u = new URL(ossUrl);
clientConfig.setEndPointSuffix(u.getHost());
} catch (MalformedURLException e) {
e.printStackTrace();
return RestResult.error(ERROR_SERVER_ERROR);
}
}
}
clientConfig.setHttpProtocol(HttpProtocol.https);
COSClient cosClient = new COSClient(cred, clientConfig);
path = path.substring(1);
String objectName = path;
String toKey = path;
if (!toKey.startsWith(userId)) {
toKey = userId + "-" + toKey;
}
try {
cosClient.copyObject(bucket, objectName, ossFavoriteBucket, toKey);
request.url = ossFavoriteBucketDomain + "/" + toKey;
} catch (CosClientException e) {
e.printStackTrace();
return RestResult.error(ERROR_SERVER_ERROR);
} finally {
cosClient.shutdown();
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
request.userId = userId;
request.timestamp = System.currentTimeMillis();
favoriteRepository.save(request);
return RestResult.ok(null);
}
@Override
public RestResult removeFavoriteItems(long id) {
favoriteRepository.deleteById(id);
return RestResult.ok(null);
}
@Override
public RestResult getFavoriteItems(long id, int count) {
Subject subject = SecurityUtils.getSubject();
String userId = (String) subject.getSession().getAttribute("userId");
id = id > 0 ? id : Long.MAX_VALUE;
List favs = favoriteRepository.loadFav(userId, id, count);
LoadFavoriteResponse response = new LoadFavoriteResponse();
response.items = favs;
response.hasMore = favs.size() == count;
return RestResult.ok(response);
}
@Override
public RestResult getGroupMembersForPortrait(String groupId) {
try {
IMResult groupMemberListIMResult = GroupAdmin.getGroupMembers(groupId);
if(groupMemberListIMResult.getErrorCode() != ErrorCode.ERROR_CODE_SUCCESS) {
LOG.error("getGroupMembersForPortrait failure {},{}", groupMemberListIMResult.getErrorCode().getCode(), groupMemberListIMResult.getErrorCode().getMsg());
return RestResult.error(ERROR_SERVER_ERROR);
}
List groupMembers = new ArrayList<>();
for (PojoGroupMember member:groupMemberListIMResult.getResult().getMembers()) {
if(member.getType() != 4)
groupMembers.add(member);
}
if (groupMembers.size() > 9) {
groupMembers.sort((o1, o2) -> {
if(o1.getType() == 2)
return -1;
if(o2.getType() == 2)
return 1;
if(o1.getType() == 1 && o2.getType() != 1)
return -1;
if(o2.getType() == 1 && o1.getType() != 1)
return 1;
return Long.compare(o1.getCreateDt(), o2.getCreateDt());
});
groupMembers = groupMembers.subList(0, 9);
}
List mids = new ArrayList<>();
for (PojoGroupMember member:groupMembers) {
IMResult userInfoIMResult = UserAdmin.getUserByUserId(member.getMember_id());
if(userInfoIMResult.getErrorCode() == ErrorCode.ERROR_CODE_SUCCESS) {
mids.add(new UserIdNamePortraitPojo(member.getMember_id(), userInfoIMResult.result.getDisplayName(), userInfoIMResult.result.getPortrait()));
} else {
mids.add(new UserIdNamePortraitPojo(member.getMember_id(),"", ""));
}
}
return RestResult.ok(mids);
} catch (Exception e) {
e.printStackTrace();
LOG.error("getGroupMembersForPortrait exception", e);
return RestResult.error(ERROR_SERVER_ERROR);
}
}
}
================================================
FILE: src/main/java/cn/wildfirechat/app/conference/ConferenceCleanupService.java
================================================
package cn.wildfirechat.app.conference;
import cn.wildfirechat.app.jpa.ConferenceEntity;
import cn.wildfirechat.app.jpa.ConferenceEntityRepository;
import cn.wildfirechat.app.jpa.UserConferenceRepository;
import cn.wildfirechat.common.ErrorCode;
import cn.wildfirechat.sdk.ConferenceAdmin;
import cn.wildfirechat.sdk.model.IMResult;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@Service
public class ConferenceCleanupService {
private static final Logger LOG = LoggerFactory.getLogger(ConferenceCleanupService.class);
@Autowired
private ConferenceEntityRepository conferenceEntityRepository;
@Autowired
private ConferenceServiceImpl conferenceServiceImpl;
@Autowired
private UserConferenceRepository userConferenceRepository;
/**
* 每5分钟检查一次过期会议,并调用SDK销毁
*/
@Scheduled(fixedRate = 5 * 60 * 1000)
@Transactional
public void cleanupExpiredConferences() {
long currentTime = System.currentTimeMillis() / 1000; // 转换为秒
LOG.info("开始检查过期会议,当前时间: {} 秒", currentTime);
List expiredConferences = conferenceEntityRepository.findExpiredConferences(currentTime);
LOG.info("发现 {} 个过期会议", expiredConferences.size());
for (ConferenceEntity conference : expiredConferences) {
try {
LOG.info("正在销毁过期会议: {}, endTime: {}", conference.id, conference.endTime);
// 调用SDK销毁会议
IMResult result = ConferenceAdmin.destroy(conference.id, conference.advance);
if (result != null && result.getErrorCode() == ErrorCode.ERROR_CODE_SUCCESS) {
LOG.info("成功销毁会议: {}", conference.id);
} else {
LOG.warn("销毁会议 {} 返回错误: {}", conference.id,
result != null ? result.getErrorCode().getMsg() : "null result");
}
Thread.sleep(100);
// 记录会议结束并更新使用量(使用计划的endTime作为实际结束时间)
conferenceServiceImpl.endConferenceAndUpdateUsage(conference.id, conference.endTime);
// 删除该会议的所有收藏记录
userConferenceRepository.deleteByConferenceId(conference.id);
LOG.info("已删除会议的收藏记录: {}", conference.id);
// 从数据库删除会议记录
conferenceEntityRepository.delete(conference);
LOG.info("已从数据库删除会议记录: {}", conference.id);
} catch (Exception e) {
LOG.error("销毁会议 {} 时发生异常", conference.id, e);
}
}
LOG.info("过期会议清理完成,共处理 {} 个会议", expiredConferences.size());
}
}
================================================
FILE: src/main/java/cn/wildfirechat/app/conference/ConferenceController.java
================================================
package cn.wildfirechat.app.conference;
import cn.wildfirechat.app.Service;
import cn.wildfirechat.app.pojo.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.io.IOException;
@RestController
public class ConferenceController {
private static final Logger LOG = LoggerFactory.getLogger(ConferenceController.class);
@Autowired
private ConferenceService mService;
@CrossOrigin
@PostMapping(value = "/conference/get_id/{userId}")
public Object getUserConferenceId(@PathVariable("userId") String userId) throws IOException {
return mService.getUserConferenceId(userId);
}
@CrossOrigin
@PostMapping(value = "/conference/get_my_id")
public Object getMyConferenceId() throws IOException {
return mService.getMyConferenceId();
}
@CrossOrigin
@PostMapping(value = "/conference/info")
public Object getConferenceInfo(@RequestBody ConferenceInfoRequest request) throws IOException {
return mService.getConferenceInfo(request.conferenceId, request.password);
}
@CrossOrigin
@PostMapping(value = "/conference/put_info")
public Object putConferenceInfo(@RequestBody ConferenceInfo info) throws IOException {
return mService.putConferenceInfo(info);
}
@CrossOrigin
@PostMapping(value = "/conference/create")
public Object createConference(@RequestBody ConferenceInfo info) throws IOException {
return mService.createConference(info);
}
@CrossOrigin
@PostMapping(value = "/conference/destroy/{conferenceId}")
public Object destroyConference(@PathVariable("conferenceId") String conferenceId) throws IOException {
return mService.destroyConference(conferenceId);
}
@CrossOrigin
@PostMapping(value = "/conference/recording/{conferenceId}")
public Object recordingConference(@PathVariable("conferenceId") String conferenceId, @RequestBody RecordingRequest recordingRequest) throws IOException {
return mService.recordingConference(conferenceId, recordingRequest.recording);
}
@CrossOrigin
@PostMapping(value = "/conference/focus/{conferenceId}")
public Object focusConference(@PathVariable("conferenceId") String conferenceId, @RequestBody UserIdPojo request) throws IOException {
return mService.focusConference(conferenceId, request.userId);
}
@CrossOrigin
@PostMapping(value = "/conference/fav/{conferenceId}")
public Object favConference(@PathVariable("conferenceId") String conferenceId) throws IOException {
return mService.favConference(conferenceId);
}
@CrossOrigin
@PostMapping(value = "/conference/unfav/{conferenceId}")
public Object unfavConference(@PathVariable("conferenceId") String conferenceId) throws IOException {
return mService.unfavConference(conferenceId);
}
@CrossOrigin
@PostMapping(value = "/conference/is_fav/{conferenceId}")
public Object isFavConference(@PathVariable("conferenceId") String conferenceId) throws IOException {
return mService.isFavConference(conferenceId);
}
@CrossOrigin
@PostMapping(value = "/conference/fav_conferences")
public Object getFavConferences() throws IOException {
return mService.getFavConferences();
}
@CrossOrigin
@PostMapping(value = "/conference/quota")
public Object getMyConferenceQuota() throws IOException {
return mService.getMyConferenceQuota();
}
}
================================================
FILE: src/main/java/cn/wildfirechat/app/conference/ConferenceService.java
================================================
package cn.wildfirechat.app.conference;
import cn.wildfirechat.app.RestResult;
import cn.wildfirechat.app.jpa.FavoriteItem;
import cn.wildfirechat.app.pojo.*;
import cn.wildfirechat.pojos.InputCreateDevice;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletResponse;
public interface ConferenceService {
RestResult getUserConferenceId(String userId);
RestResult getMyConferenceId();
RestResult getConferenceInfo(String conferenceId, String password);
RestResult putConferenceInfo(ConferenceInfo info);
RestResult createConference(ConferenceInfo info);
RestResult destroyConference(String conferenceId);
RestResult recordingConference(String conferenceId, boolean recording);
RestResult focusConference(String conferenceId, String userId);
RestResult favConference(String conferenceId);
RestResult unfavConference(String conferenceId);
RestResult getFavConferences();
RestResult isFavConference(String conferenceId);
/**
* 查询当前用户的会议额度
* @return 额度信息(包含总额度、已使用、剩余额度)
*/
RestResult getMyConferenceQuota();
}
================================================
FILE: src/main/java/cn/wildfirechat/app/conference/ConferenceServiceImpl.java
================================================
package cn.wildfirechat.app.conference;
import cn.wildfirechat.app.IMConfig;
import cn.wildfirechat.app.RestResult;
import cn.wildfirechat.app.jpa.*;
import cn.wildfirechat.app.model.ConferenceDTO;
import cn.wildfirechat.app.pojo.*;
import cn.wildfirechat.app.tools.NumericIdGenerator;
import cn.wildfirechat.common.ErrorCode;
import cn.wildfirechat.pojos.PojoConferenceInfo;
import cn.wildfirechat.pojos.PojoConferenceInfoList;
import cn.wildfirechat.sdk.*;
import cn.wildfirechat.sdk.model.IMResult;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.subject.Subject;
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.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import javax.annotation.PostConstruct;
import java.time.Instant;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
@org.springframework.stereotype.Service
public class ConferenceServiceImpl implements ConferenceService {
private static final Logger LOG = LoggerFactory.getLogger(ConferenceServiceImpl.class);
@Autowired
private IMConfig mIMConfig;
@Autowired
private ConferenceEntityRepository conferenceEntityRepository;
@Autowired
private UserPrivateConferenceIdRepository userPrivateConferenceIdRepository;
@Autowired
private UserConferenceRepository userConferenceRepository;
@Autowired
private UserConferenceQuotaRepository userConferenceQuotaRepository;
@Autowired
private UserQuotaUsageRepository userQuotaUsageRepository;
@Autowired
private ConferenceRecordRepository conferenceRecordRepository;
@Value("${conference.default_quota_minutes:0}")
private int defaultQuotaMinutes;
@PostConstruct
private void init() {
AdminConfig.initAdmin(mIMConfig.admin_url, mIMConfig.admin_secret);
}
@Override
public RestResult getUserConferenceId(String userId) {
String conferenceId = getPrivateConferenceId(userId);
return RestResult.ok(conferenceId);
}
@Override
public RestResult getMyConferenceId() {
Subject subject = SecurityUtils.getSubject();
String userId = (String) subject.getSession().getAttribute("userId");
return getUserConferenceId(userId);
}
private String getPrivateConferenceId(String userId) {
Optional privateConferenceIdOptional = userPrivateConferenceIdRepository.findById(userId);
if(privateConferenceIdOptional.isPresent()) {
return privateConferenceIdOptional.get().getConferenceId();
}
String conferenceId = NumericIdGenerator.getId(null, Arrays.asList(0), 8);
userPrivateConferenceIdRepository.save(new UserPrivateConferenceId(userId, conferenceId));
return conferenceId;
}
@Override
public RestResult getConferenceInfo(String conferenceId, String password) {
Optional conferenceEntityOptional = conferenceEntityRepository.findById(conferenceId);
if(conferenceEntityOptional.isPresent()) {
Subject subject = SecurityUtils.getSubject();
String userId = (String) subject.getSession().getAttribute("userId");
ConferenceEntity entity = conferenceEntityOptional.get();
if(StringUtils.isEmpty(entity.password) || entity.password.equals(password) || userId.equals(entity.owner)) {
return RestResult.ok(convertConference(entity));
}
}
return RestResult.error(RestResult.RestCode.ERROR_NOT_EXIST);
}
@Override
public RestResult putConferenceInfo(ConferenceInfo info) {
Optional conferenceEntityOptional = conferenceEntityRepository.findById(info.conferenceId);
if(conferenceEntityOptional.isPresent()) {
Subject subject = SecurityUtils.getSubject();
String userId = (String) subject.getSession().getAttribute("userId");
ConferenceEntity entity = conferenceEntityOptional.get();
if(userId.equals(entity.owner)) {
conferenceEntityRepository.save(convertConference(info));
} else {
return RestResult.error(RestResult.RestCode.ERROR_NO_RIGHT);
}
} else {
conferenceEntityRepository.save(convertConference(info));
}
return RestResult.ok(null);
}
@Override
public RestResult createConference(ConferenceInfo info) {
String userId = getUserId();
if(!StringUtils.isEmpty(info.owner)) {
if(!info.owner.equals(userId)) {
return RestResult.error(RestResult.RestCode.ERROR_INVALID_PARAMETER);
}
} else {
info.owner = userId;
}
//如果开始时间小于当前时间,改成当前时间
if(info.startTime < System.currentTimeMillis()/1000) {
info.startTime = System.currentTimeMillis()/1000;
}
//如果没有指定最大参与者人数,默认指定为20
if(info.maxParticipants <= 0) {
info.maxParticipants = 20;
}
// 检查配额(仅当会议有结束时间时)
if (info.endTime > 0) {
// 计算计划时长,calculateDurationMinutes 内部会处理开始时间
int plannedMinutes = calculateDurationMinutes(info.startTime, info.endTime);
LOG.info("用户 {} 创建会议,计划时长 {} 分钟(原始开始时间: {}, 结束时间: {})",
userId, plannedMinutes, info.startTime, info.endTime);
QuotaCheckResult checkResult = checkUserQuota(userId, plannedMinutes);
if (!checkResult.isEnough()) {
LOG.warn("用户 {} 会议额度不足,需要 {} 分钟,剩余 {} 分钟",
userId, plannedMinutes, checkResult.getRemaining());
return RestResult.error(RestResult.RestCode.ERROR_CONFERENCE_QUOTA_EXCEEDED);
}
LOG.info("用户 {} 配额检查通过,计划使用 {} 分钟,剩余 {} 分钟",
userId, plannedMinutes, checkResult.getRemaining());
} else {
LOG.info("用户 {} 创建永久会议(无结束时间),跳过配额检查", userId);
}
if(StringUtils.isEmpty(info.conferenceId)) {
/*
没有传来会议ID,这里生成随机会议ID。个人会议的长度是8位,随机会议ID是10位
*/
String conferenceId = null;
do {
conferenceId = NumericIdGenerator.getId(null, Arrays.asList(0), 10);
if(!conferenceEntityRepository.findById(conferenceId).isPresent()) {
break;
}
} while (true);
info.conferenceId = conferenceId;
} else {
Optional conferenceEntityOptional = conferenceEntityRepository.findById(info.conferenceId);
if(conferenceEntityOptional.isPresent()) {
ConferenceEntity entity = conferenceEntityOptional.get();
if(!userId.equals(entity.owner)) {
return RestResult.error(RestResult.RestCode.ERROR_NO_RIGHT);
}
}
}
try {
IMResult result = ConferenceAdmin.createRoom(info.conferenceId, info.conferenceTitle, info.pin, info.maxParticipants, info.advance, 0, info.recording, false);
if(result != null && result.getErrorCode() == ErrorCode.ERROR_CODE_SUCCESS) {
conferenceEntityRepository.save(convertConference(info));
LOG.info("会议创建成功: conferenceId={}, owner={}, title={}",
info.conferenceId, userId, info.conferenceTitle);
// 创建会议记录(仅当会议有结束时间时)
if (info.endTime > 0) {
createConferenceRecord(info);
}
favConference(info.conferenceId);
return RestResult.ok(info.conferenceId);
} else {
LOG.error("创建会议失败: conferenceId={}, errorCode={}, errorMsg={}",
info.conferenceId,
result != null ? result.getErrorCode().code : "null",
result != null ? result.getErrorCode().msg : "null result");
}
} catch (Exception e) {
LOG.error("创建会议异常: conferenceId={}", info.conferenceId, e);
}
return RestResult.error(RestResult.RestCode.ERROR_SERVER_ERROR);
}
@Override
@Transactional
public RestResult destroyConference(String conferenceId) {
String userId = getUserId();
LOG.info("用户 {} 请求销毁会议: {}", userId, conferenceId);
Optional conferenceEntityOptional = conferenceEntityRepository.findById(conferenceId);
if(conferenceEntityOptional.isPresent()) {
ConferenceEntity entity = conferenceEntityOptional.get();
if(!userId.equals(entity.owner)) {
LOG.warn("用户 {} 无权销毁会议 {},会议所有者是 {}", userId, conferenceId, entity.owner);
return RestResult.error(RestResult.RestCode.ERROR_NO_RIGHT);
}
try {
ConferenceAdmin.destroy(entity.id, entity.advance);
LOG.info("SDK销毁会议成功: conferenceId={}", conferenceId);
} catch (Exception e) {
LOG.error("SDK销毁会议失败: conferenceId={}", conferenceId, e);
}
long actualEndTime = System.currentTimeMillis() / 1000;
// 记录会议结束,更新使用时长
endConferenceAndUpdateUsage(conferenceId, actualEndTime);
// 删除该会议的所有收藏记录
userConferenceRepository.deleteByConferenceId(conferenceId);
LOG.info("已删除会议的收藏记录: conferenceId={}", conferenceId);
conferenceEntityRepository.deleteById(conferenceId);
LOG.info("会议已从数据库删除: conferenceId={}", conferenceId);
} else {
LOG.warn("销毁会议时未找到会议记录: conferenceId={}", conferenceId);
try {
IMResult conferenceInfoListIMResult = ConferenceAdmin.listConferences(1000, 0);
if(conferenceInfoListIMResult != null && conferenceInfoListIMResult.getErrorCode() != ErrorCode.ERROR_CODE_SUCCESS) {
for (PojoConferenceInfo info : conferenceInfoListIMResult.getResult().conferenceInfoList) {
if(info.roomId.equals(conferenceId)) {
ConferenceAdmin.destroy(info.roomId, info.advance);
LOG.info("通过列表找到并销毁会议: conferenceId={}", conferenceId);
break;
}
}
}
} catch (Exception e) {
LOG.error("销毁会议异常: conferenceId={}", conferenceId, e);
}
}
return RestResult.ok(null);
}
@Override
public RestResult recordingConference(String conferenceId, boolean recording) {
Optional conferenceEntityOptional = conferenceEntityRepository.findById(conferenceId);
if(conferenceEntityOptional.isPresent()) {
Subject subject = SecurityUtils.getSubject();
String userId = (String) subject.getSession().getAttribute("userId");
ConferenceEntity entity = conferenceEntityOptional.get();
if(userId.equals(entity.owner)) {
if(entity.isRecording() == recording) {
return RestResult.ok();
} else {
entity.setRecording(recording);
try {
IMResult voidIMResult = ConferenceAdmin.enableRecording(entity.getId(), entity.isAdvance(), entity.recording);
if(voidIMResult != null & voidIMResult.getErrorCode() == ErrorCode.ERROR_CODE_SUCCESS) {
conferenceEntityRepository.save(entity);
return RestResult.ok();
} else {
return RestResult.error(RestResult.RestCode.ERROR_SERVER_ERROR);
}
} catch (Exception e) {
e.printStackTrace();
return RestResult.error(RestResult.RestCode.ERROR_SERVER_ERROR);
}
}
} else {
return RestResult.error(RestResult.RestCode.ERROR_NO_RIGHT);
}
} else {
return RestResult.error(RestResult.RestCode.ERROR_NOT_EXIST);
}
}
@Override
public RestResult focusConference(String conferenceId, String focusedUserId) {
Optional conferenceEntityOptional = conferenceEntityRepository.findById(conferenceId);
if(conferenceEntityOptional.isPresent()) {
Subject subject = SecurityUtils.getSubject();
String userId = (String) subject.getSession().getAttribute("userId");
ConferenceEntity entity = conferenceEntityOptional.get();
if(userId.equals(entity.owner)) {
entity.setFocus(focusedUserId);
conferenceEntityRepository.save(entity);
} else {
return RestResult.error(RestResult.RestCode.ERROR_NO_RIGHT);
}
} else {
return RestResult.error(RestResult.RestCode.ERROR_NOT_EXIST);
}
return RestResult.error(RestResult.RestCode.SUCCESS);
}
@Override
public RestResult favConference(String conferenceId) {
String userId = getUserId();
UserConference userConference = new UserConference(userId, conferenceId);
userConferenceRepository.save(userConference);
return RestResult.ok(null);
}
@Override
public RestResult unfavConference(String conferenceId) {
userConferenceRepository.deleteByUserIdAndConferenceId(getUserId(), conferenceId);
return RestResult.ok(null);
}
@Override
public RestResult getFavConferences() {
List ucs = userConferenceRepository.findByUserId(getUserId(), System.currentTimeMillis()/1000);
List infos = new ArrayList<>();
for (ConferenceDTO dto:ucs) {
ConferenceInfo info = new ConferenceInfo();
info.conferenceId = dto.getId();
info.conferenceTitle = dto.getConference_title();
info.password = dto.getPassword();
info.pin = dto.getPin();
info.owner = dto.getOwner();
info.startTime = dto.getStart_time();
info.endTime = dto.getEnd_time();
info.audience = dto.isAudience();
info.advance = dto.isAdvance();
info.allowSwitchMode = dto.isAllow_switch_mode();
info.noJoinBeforeStart = dto.isNo_join_before_start();
info.recording = dto.isRecording();
info.focus = dto.getFocus();
info.maxParticipants = dto.getMax_participants();
String managers = dto.getManages();
if(!StringUtils.isEmpty(managers)) {
info.managers = Arrays.asList(managers.split(","));
}
infos.add(info);
}
return RestResult.ok(infos);
}
@Override
public RestResult isFavConference(String conferenceId) {
Optional userConference = userConferenceRepository.findByUserIdAndConferenceId(getUserId(), conferenceId);
if(userConference.isPresent())
return RestResult.ok(null);
return RestResult.error(RestResult.RestCode.ERROR_NOT_EXIST);
}
@Override
public RestResult getMyConferenceQuota() {
String userId = getUserId();
String currentYearMonth = getCurrentYearMonth();
LOG.info("用户 {} 查询会议额度,年月: {}", userId, currentYearMonth);
ConferenceQuotaResponse response = new ConferenceQuotaResponse();
response.setYearMonth(currentYearMonth);
// 获取用户配额
Optional quotaOptional = userConferenceQuotaRepository.findByUserId(userId);
int totalQuota;
String quotaSource;
if (quotaOptional.isPresent()) {
totalQuota = quotaOptional.get().getTotalMinutes();
quotaSource = "用户自定义配额";
} else {
totalQuota = defaultQuotaMinutes;
quotaSource = "默认配额";
}
response.setTotalQuota(totalQuota);
// 配额为0表示不限制
if (totalQuota == 0) {
response.setUnlimited(true);
response.setUsedMinutes(0);
response.setRemainingMinutes(0);
LOG.info("用户 {} 查询额度结果: 来源={}, 无限制", userId, quotaSource);
} else {
response.setUnlimited(false);
// 查询当月已使用额度
Optional usageOptional = userQuotaUsageRepository.findByUserIdAndYearMonth(userId, currentYearMonth);
int usedMinutes = usageOptional.map(UserQuotaUsage::getUsedMinutes).orElse(0);
int remaining = totalQuota - usedMinutes;
response.setUsedMinutes(usedMinutes);
response.setRemainingMinutes(remaining);
LOG.info("用户 {} 查询额度结果: 来源={}, 总额度={}分钟, 已使用={}分钟, 剩余={}分钟",
userId, quotaSource, totalQuota, usedMinutes, remaining);
}
return RestResult.ok(response);
}
/**
* 检查用户配额是否充足
* 配额不分月份,但使用量按月统计
*/
private QuotaCheckResult checkUserQuota(String userId, int needMinutes) {
// 获取用户配额(优先查用户自定义配额,没有则使用默认配置)
Optional quotaOptional = userConferenceQuotaRepository.findByUserId(userId);
int totalQuota;
String quotaSource;
if (quotaOptional.isPresent()) {
totalQuota = quotaOptional.get().getTotalMinutes();
quotaSource = "用户自定义配额";
} else {
totalQuota = defaultQuotaMinutes;
quotaSource = "默认配额";
}
LOG.debug("用户 {} 配额检查: 来源={}, 总额度={}分钟, 需要={}分钟",
userId, quotaSource, totalQuota, needMinutes);
// 默认配额为0表示不限制
if (totalQuota == 0) {
LOG.debug("用户 {} 使用默认配额0,不限制会议时长", userId);
return new QuotaCheckResult(true, 0, 0);
}
// 查询当月已使用额度
String currentYearMonth = getCurrentYearMonth();
Optional usageOptional = userQuotaUsageRepository.findByUserIdAndYearMonth(userId, currentYearMonth);
int usedMinutes = usageOptional.map(UserQuotaUsage::getUsedMinutes).orElse(0);
// 检查是否足够
boolean enough = (usedMinutes + needMinutes) <= totalQuota;
int remaining = totalQuota - usedMinutes;
LOG.debug("用户 {} 配额详情: 年月={}, 总额度={}, 已使用={}, 需要={}, 剩余={}, 结果={}",
userId, currentYearMonth, totalQuota, usedMinutes, needMinutes, remaining,
enough ? "通过" : "不足");
return new QuotaCheckResult(enough, remaining, totalQuota);
}
/**
* 创建会议记录
*/
private void createConferenceRecord(ConferenceInfo info) {
try {
ConferenceRecord record = new ConferenceRecord();
record.setConferenceId(info.conferenceId);
record.setOwner(info.owner);
long actualStartTime = info.startTime;
record.setStartTime(actualStartTime);
record.setEndTime(info.endTime);
// 使用 actualStartTime 计算计划时长,确保与配额检查时一致
record.setPlannedDuration(calculateDurationMinutes(actualStartTime, info.endTime));
record.setActualDuration(0);
record.setStatus(ConferenceRecord.Status.ONGOING.getValue());
record.setYearMonth(getCurrentYearMonth());
conferenceRecordRepository.save(record);
LOG.info("创建会议记录: conferenceId={}, owner={}, startTime={}, plannedDuration={}分钟",
info.conferenceId, info.owner, actualStartTime, record.getPlannedDuration());
} catch (Exception e) {
LOG.error("创建会议记录失败: conferenceId={}", info.conferenceId, e);
}
}
/**
* 结束会议并更新使用量
*/
@Transactional
public void endConferenceAndUpdateUsage(String conferenceId, long actualEndTime) {
try {
Optional recordOptional = conferenceRecordRepository.findByConferenceId(conferenceId);
if (!recordOptional.isPresent()) {
LOG.warn("未找到会议记录: conferenceId={}", conferenceId);
return;
}
ConferenceRecord record = recordOptional.get();
if (record.getStatus() == ConferenceRecord.Status.ENDED.getValue()) {
LOG.warn("会议已结束,跳过重复处理: conferenceId={}", conferenceId);
return;
}
// 如果开始时间为0,使用当前时间作为开始时间(兼容老数据)
long startTime = record.getStartTime();
if (startTime <= 0) {
startTime = actualEndTime; // 如果开始时间为0,结束时间作为开始时间,时长为0
LOG.warn("会议记录开始时间为0,使用结束时间作为开始时间: conferenceId={}", conferenceId);
}
// 计算实际时长(分钟)
int actualDuration = calculateDurationMinutes(startTime, actualEndTime);
// 更新会议记录
record.setActualDuration(actualDuration);
record.setEndTime(actualEndTime);
record.setStatus(ConferenceRecord.Status.ENDED.getValue());
conferenceRecordRepository.save(record);
// 更新用户使用量
updateQuotaUsage(record.getOwner(), record.getYearMonth(), actualDuration);
LOG.info("会议结束并更新使用量: conferenceId={}, owner={}, startTime={}, endTime={}, actualDuration={}分钟",
conferenceId, record.getOwner(), startTime, actualEndTime, actualDuration);
} catch (Exception e) {
LOG.error("结束会议并更新使用量失败: conferenceId={}", conferenceId, e);
}
}
/**
* 更新用户配额使用量
*/
@Transactional
public void updateQuotaUsage(String userId, String yearMonth, int minutes) {
if (minutes <= 0) {
LOG.debug("更新使用量跳过: 用户={}, 时长={} 分钟(小于等于0)", userId, minutes);
return;
}
Optional usageOptional = userQuotaUsageRepository.findByUserIdAndYearMonth(userId, yearMonth);
if (usageOptional.isPresent()) {
UserQuotaUsage usage = usageOptional.get();
int oldMinutes = usage.getUsedMinutes();
usage.setUsedMinutes(oldMinutes + minutes);
userQuotaUsageRepository.save(usage);
LOG.info("更新用户使用量: 用户={}, 年月={}, 新增 {} 分钟, 原使用 {} 分钟, 现使用 {} 分钟",
userId, yearMonth, minutes, oldMinutes, usage.getUsedMinutes());
} else {
UserQuotaUsage usage = new UserQuotaUsage(userId, yearMonth, minutes);
userQuotaUsageRepository.save(usage);
LOG.info("创建用户使用量记录: 用户={}, 年月={}, 使用 {} 分钟",
userId, yearMonth, minutes);
}
}
/**
* 计算时长(分钟)
*/
private int calculateDurationMinutes(long startTime, long endTime) {
long durationSeconds = endTime - startTime;
if (durationSeconds <= 0) {
return 0;
}
// 转换为分钟,向上取整
return (int) ((durationSeconds + 59) / 60);
}
/**
* 获取当前年月 (yyyyMM格式)
*/
private String getCurrentYearMonth() {
return Instant.now()
.atZone(ZoneId.systemDefault())
.format(DateTimeFormatter.ofPattern("yyyyMM"));
}
private ConferenceEntity convertConference(ConferenceInfo info) {
ConferenceEntity entity = new ConferenceEntity();
entity.id = info.conferenceId;
entity.conferenceTitle = info.conferenceTitle;
entity.password = info.password;
entity.pin = info.pin;
entity.owner = info.owner;
entity.startTime = info.startTime;
entity.endTime = info.endTime;
entity.audience = info.audience;
entity.advance = info.advance;
entity.allowSwitchMode = info.allowSwitchMode;
entity.noJoinBeforeStart = info.noJoinBeforeStart;
entity.recording = info.recording;
entity.focus = info.focus;
entity.maxParticipants = info.maxParticipants;
if(info.managers != null && !info.managers.isEmpty()) {
entity.manages = String.join(",", info.managers);
}
return entity;
}
private ConferenceInfo convertConference(ConferenceEntity entity) {
ConferenceInfo info = new ConferenceInfo();
info.conferenceId = entity.id;
info.conferenceTitle = entity.conferenceTitle;
info.password = entity.password;
info.pin = entity.pin;
info.owner = entity.owner;
info.startTime = entity.startTime;
info.endTime = entity.endTime;
info.audience = entity.audience;
info.advance = entity.advance;
info.allowSwitchMode = entity.allowSwitchMode;
info.noJoinBeforeStart = entity.noJoinBeforeStart;
info.recording = entity.recording;
info.focus = entity.focus;
info.maxParticipants = entity.maxParticipants;
if(!StringUtils.isEmpty(info.managers)) {
info.managers = Arrays.asList(entity.manages.split(","));
}
return info;
}
private String getUserId() {
Subject subject = SecurityUtils.getSubject();
return (String) subject.getSession().getAttribute("userId");
}
/**
* 配额检查结果
*/
private static class QuotaCheckResult {
private final boolean enough;
private final int remaining;
private final int total;
public QuotaCheckResult(boolean enough, int remaining, int total) {
this.enough = enough;
this.remaining = remaining;
this.total = total;
}
public boolean isEnough() {
return enough;
}
public int getRemaining() {
return remaining;
}
public int getTotal() {
return total;
}
}
}
================================================
FILE: src/main/java/cn/wildfirechat/app/jpa/Announcement.java
================================================
package cn.wildfirechat.app.jpa;
import javax.persistence.*;
@Entity
@Table(name = "text")
public class Announcement {
@Id
@Column(length = 128)
private String groupId;
private String author;
@Column(length = 2048)
private String announcement;
private long timestamp;
public String getGroupId() {
return groupId;
}
public void setGroupId(String groupId) {
this.groupId = groupId;
}
public String getAnnouncement() {
return announcement;
}
public void setAnnouncement(String announcement) {
this.announcement = announcement;
}
public String getAuthor() {
return author;
}
public void setAuthor(String author) {
this.author = author;
}
public long getTimestamp() {
return timestamp;
}
public void setTimestamp(long timestamp) {
this.timestamp = timestamp;
}
}
================================================
FILE: src/main/java/cn/wildfirechat/app/jpa/AnnouncementRepository.java
================================================
package cn.wildfirechat.app.jpa;
import org.springframework.data.repository.PagingAndSortingRepository;
import org.springframework.data.repository.query.Param;
import org.springframework.data.rest.core.annotation.RepositoryRestResource;
import java.util.List;
@RepositoryRestResource()
public interface AnnouncementRepository extends PagingAndSortingRepository {
}
================================================
FILE: src/main/java/cn/wildfirechat/app/jpa/ConferenceEntity.java
================================================
package cn.wildfirechat.app.jpa;
import javax.persistence.*;
@Entity
@Table(name = "conference")
public class ConferenceEntity {
@Id
@Column(length = 12)
public String id;
public String conferenceTitle;
public String password;
public String pin;
public String owner;
public String manages;
public long startTime;
public long endTime;
public boolean audience;
public boolean advance;
public boolean allowSwitchMode;
public boolean noJoinBeforeStart;
public boolean recording;
public String focus;
public int maxParticipants;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getConferenceTitle() {
return conferenceTitle;
}
public void setConferenceTitle(String conferenceTitle) {
this.conferenceTitle = conferenceTitle;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getPin() {
return pin;
}
public void setPin(String pin) {
this.pin = pin;
}
public String getOwner() {
return owner;
}
public void setOwner(String owner) {
this.owner = owner;
}
public long getStartTime() {
return startTime;
}
public void setStartTime(long startTime) {
this.startTime = startTime;
}
public long getEndTime() {
return endTime;
}
public void setEndTime(long endTime) {
this.endTime = endTime;
}
public boolean isAudience() {
return audience;
}
public void setAudience(boolean audience) {
this.audience = audience;
}
public boolean isAdvance() {
return advance;
}
public void setAdvance(boolean advance) {
this.advance = advance;
}
public boolean isAllowSwitchMode() {
return allowSwitchMode;
}
public void setAllowSwitchMode(boolean allowSwitchMode) {
this.allowSwitchMode = allowSwitchMode;
}
public boolean isNoJoinBeforeStart() {
return noJoinBeforeStart;
}
public void setNoJoinBeforeStart(boolean noJoinBeforeStart) {
this.noJoinBeforeStart = noJoinBeforeStart;
}
public boolean isRecording() {
return recording;
}
public void setRecording(boolean recording) {
this.recording = recording;
}
public String getManages() {
return manages;
}
public void setManages(String manages) {
this.manages = manages;
}
public String getFocus() {
return focus;
}
public void setFocus(String focus) {
this.focus = focus;
}
public int getMaxParticipants() {
return maxParticipants;
}
public void setMaxParticipants(int maxParticipants) {
this.maxParticipants = maxParticipants;
}
}
================================================
FILE: src/main/java/cn/wildfirechat/app/jpa/ConferenceEntityRepository.java
================================================
package cn.wildfirechat.app.jpa;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.PagingAndSortingRepository;
import org.springframework.data.rest.core.annotation.RepositoryRestResource;
import java.util.List;
@RepositoryRestResource()
public interface ConferenceEntityRepository extends PagingAndSortingRepository {
/**
* 查询已过期的会议(endTime > 0 且 endTime < 当前时间)
* @param currentTime 当前时间(秒)
* @return 过期会议列表
*/
@Query("SELECT c FROM ConferenceEntity c WHERE c.endTime > 0 AND c.endTime < ?1")
List findExpiredConferences(long currentTime);
}
================================================
FILE: src/main/java/cn/wildfirechat/app/jpa/ConferenceRecord.java
================================================
package cn.wildfirechat.app.jpa;
import javax.persistence.*;
import java.util.Date;
/**
* 会议记录表
* 记录每一次会议的详细信息,用于计费和统计
*/
@Entity
@Table(name = "conference_record")
public class ConferenceRecord {
public enum Status {
ONGOING(0), // 进行中
ENDED(1); // 已结束
private final int value;
Status(int value) {
this.value = value;
}
public int getValue() {
return value;
}
}
@Id
@Column(name = "conference_id", length = 12)
private String conferenceId;
@Column(name = "owner", length = 64, nullable = false)
private String owner;
@Column(name = "start_time")
private long startTime;
@Column(name = "end_time")
private long endTime;
@Column(name = "planned_duration")
private int plannedDuration; // 计划时长(分钟)
@Column(name = "actual_duration")
private int actualDuration; // 实际时长(分钟)
@Column(name = "status")
private int status; // 0=进行中, 1=已结束
@Column(name = "billing_month", length = 8)
private String yearMonth; // 会议开始时间所在月份,用于配额统计
@Column(name = "created_at")
@Temporal(TemporalType.TIMESTAMP)
private Date createdAt;
@Column(name = "updated_at")
@Temporal(TemporalType.TIMESTAMP)
private Date updatedAt;
@PrePersist
protected void onCreate() {
createdAt = new Date();
updatedAt = new Date();
}
@PreUpdate
protected void onUpdate() {
updatedAt = new Date();
}
public ConferenceRecord() {
}
public String getConferenceId() {
return conferenceId;
}
public void setConferenceId(String conferenceId) {
this.conferenceId = conferenceId;
}
public String getOwner() {
return owner;
}
public void setOwner(String owner) {
this.owner = owner;
}
public long getStartTime() {
return startTime;
}
public void setStartTime(long startTime) {
this.startTime = startTime;
}
public long getEndTime() {
return endTime;
}
public void setEndTime(long endTime) {
this.endTime = endTime;
}
public int getPlannedDuration() {
return plannedDuration;
}
public void setPlannedDuration(int plannedDuration) {
this.plannedDuration = plannedDuration;
}
public int getActualDuration() {
return actualDuration;
}
public void setActualDuration(int actualDuration) {
this.actualDuration = actualDuration;
}
public int getStatus() {
return status;
}
public void setStatus(int status) {
this.status = status;
}
public String getYearMonth() {
return yearMonth;
}
public void setYearMonth(String yearMonth) {
this.yearMonth = yearMonth;
}
public Date getCreatedAt() {
return createdAt;
}
public void setCreatedAt(Date createdAt) {
this.createdAt = createdAt;
}
public Date getUpdatedAt() {
return updatedAt;
}
public void setUpdatedAt(Date updatedAt) {
this.updatedAt = updatedAt;
}
}
================================================
FILE: src/main/java/cn/wildfirechat/app/jpa/ConferenceRecordRepository.java
================================================
package cn.wildfirechat.app.jpa;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Repository;
import java.util.Optional;
/**
* 会议记录Repository
*/
@Repository
public interface ConferenceRecordRepository extends CrudRepository {
/**
* 根据会议ID查询记录
*/
Optional findByConferenceId(String conferenceId);
/**
* 更新会议结束信息
*/
@Modifying
@Query("UPDATE ConferenceRecord r SET r.endTime = ?2, r.actualDuration = ?3, r.status = 1, r.updatedAt = CURRENT_TIMESTAMP WHERE r.conferenceId = ?1")
int endConference(String conferenceId, long endTime, int actualDuration);
}
================================================
FILE: src/main/java/cn/wildfirechat/app/jpa/FavoriteItem.java
================================================
package cn.wildfirechat.app.jpa;
import javax.annotation.Nullable;
import javax.persistence.*;
@Entity
@Table(name = "t_favorites", indexes = {@Index(columnList = "user_id, type")})
public class FavoriteItem {
@Id
@GeneratedValue(strategy=GenerationType.IDENTITY)
@Column(name = "id")
public Long id;
@Column(name = "messageUid")
@Nullable
public Long messageUid;
@Column(name = "user_id", length = 64)
public String userId;
@Column(name = "type")
public int type;
@Column(name = "timestamp")
public long timestamp;
@Column(name = "conv_type")
public int convType;
@Column(name = "conv_line")
public int convLine;
@Column(name = "conv_target")
public String convTarget;
@Column(name = "origin")
public String origin;
@Column(name = "sender")
public String sender;
@Lob
@Column(name="title")
public String title;
@Column(name="url",length = 1024)
public String url;
@Column(name = "thumb_url",length = 1024)
public String thumbUrl;
@Lob
@Column(name="data")
public String data;
}
================================================
FILE: src/main/java/cn/wildfirechat/app/jpa/FavoriteRepository.java
================================================
package cn.wildfirechat.app.jpa;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository;
import org.springframework.data.rest.core.annotation.RepositoryRestResource;
import java.util.List;
@RepositoryRestResource()
public interface FavoriteRepository extends CrudRepository {
@Query(value = "select * from t_favorites where user_id = ?1 and id < ?2 order by id desc limit ?3", nativeQuery = true)
List loadFav(String userId, long startId, int count);
}
================================================
FILE: src/main/java/cn/wildfirechat/app/jpa/PCSession.java
================================================
package cn.wildfirechat.app.jpa;
import cn.wildfirechat.app.pojo.SessionOutput;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;
@Entity
@Table(name = "pc_session")
public class PCSession {
public interface PCSessionStatus {
int Session_Created = 0;
int Session_Scanned = 1;
int Session_Pre_Verify = 3;
int Session_Verified = 2;
int Session_Canceled = 4;
}
@Id
@Column(length = 128)
private String token;
private String clientId;
private long createDt;
private long duration;
//PCSessionStatus
private int status;
private String confirmedUserId;
private String device_name;
private int platform;
public int getPlatform() {
return platform;
}
public void setPlatform(int platform) {
this.platform = platform;
}
public String getToken() {
return token;
}
public void setToken(String token) {
this.token = token;
}
public String getClientId() {
return clientId;
}
public void setClientId(String clientId) {
this.clientId = clientId;
}
public long getCreateDt() {
return createDt;
}
public void setCreateDt(long createDt) {
this.createDt = createDt;
}
public long getDuration() {
return duration;
}
public void setDuration(long duration) {
this.duration = duration;
}
public int getStatus() {
return status;
}
public void setStatus(int status) {
this.status = status;
}
public String getConfirmedUserId() {
return confirmedUserId;
}
public void setConfirmedUserId(String confirmedUserId) {
this.confirmedUserId = confirmedUserId;
}
public String getDevice_name() {
return device_name;
}
public void setDevice_name(String device_name) {
this.device_name = device_name;
}
public SessionOutput toOutput() {
return new SessionOutput(confirmedUserId, token, status, duration - (System.currentTimeMillis() - createDt), device_name, platform);
}
}
================================================
FILE: src/main/java/cn/wildfirechat/app/jpa/PCSessionRepository.java
================================================
package cn.wildfirechat.app.jpa;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.repository.CrudRepository;
import org.springframework.transaction.annotation.Transactional;
public interface PCSessionRepository extends CrudRepository {
@Modifying
@Transactional
Long deleteByCreateDtBefore(long timestamp);
}
================================================
FILE: src/main/java/cn/wildfirechat/app/jpa/Record.java
================================================
package cn.wildfirechat.app.jpa;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;
@Entity
@Table(name = "phone_code_record")
public class Record {
@Id
@Column(length = 128)
private String mobile;
private String code;
//发送时间,当小于1分钟不允许发送。
private long timestamp;
//计算时间段内发送次数的起始时间
private long startTime;
//startTime到现在的发送次数
private int requestCount;
public Record(String code, String mobile) {
this.code = code;
this.mobile = mobile;
this.timestamp = 0;
this.startTime = System.currentTimeMillis();
this.requestCount = 0;
}
public Record() {
}
public boolean increaseAndCheck() {
long now = System.currentTimeMillis();
if (now - startTime > 86400000) {
reset();
}
requestCount++;
if (requestCount > 10) {
return false;
}
return true;
}
public void reset() {
requestCount = 1;
startTime = System.currentTimeMillis();
}
public int getRequestCount() {
return requestCount;
}
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
public String getMobile() {
return mobile;
}
public long getTimestamp() {
return timestamp;
}
public void setTimestamp(long timestamp) {
this.timestamp = timestamp;
}
public long getStartTime() {
return startTime;
}
public void setStartTime(long startTime) {
this.startTime = startTime;
}
}
================================================
FILE: src/main/java/cn/wildfirechat/app/jpa/RecordRepository.java
================================================
package cn.wildfirechat.app.jpa;
import org.springframework.data.repository.CrudRepository;
public interface RecordRepository extends CrudRepository {
}
================================================
FILE: src/main/java/cn/wildfirechat/app/jpa/ShiroSession.java
================================================
package cn.wildfirechat.app.jpa;
import org.hibernate.annotations.Type;
import javax.persistence.*;
@Entity
@Table(name = "shiro_session")
public class ShiroSession {
@Id
@Column(length = 128)
private String sessionId;
@Lob
@Column(name="session_data", length = 2048)
@Type(type="org.hibernate.type.BinaryType")
private byte[] sessionData;
public ShiroSession(String sessionId, byte[] sessionData) {
this.sessionId = sessionId;
this.sessionData = sessionData;
}
public ShiroSession() {
}
public String getSessionId() {
return sessionId;
}
public void setSessionId(String sessionId) {
this.sessionId = sessionId;
}
public byte[] getSessionData() {
return sessionData;
}
public void setSessionData(byte[] sessionData) {
this.sessionData = sessionData;
}
}
================================================
FILE: src/main/java/cn/wildfirechat/app/jpa/ShiroSessionRepository.java
================================================
package cn.wildfirechat.app.jpa;
import org.springframework.data.repository.CrudRepository;
public interface ShiroSessionRepository extends CrudRepository {
}
================================================
FILE: src/main/java/cn/wildfirechat/app/jpa/SlideVerify.java
================================================
package cn.wildfirechat.app.jpa;
import javax.persistence.*;
import java.sql.Timestamp;
@Entity
@Table(name = "slide_verify")
public class SlideVerify {
@Id
@Column(length = 64)
private String token;
@Column
private int x;
@Column
private long timestamp;
@Column
private boolean verified;
public SlideVerify() {
}
public SlideVerify(String token, int x, long timestamp) {
this.token = token;
this.x = x;
this.timestamp = timestamp;
this.verified = false;
}
public String getToken() {
return token;
}
public void setToken(String token) {
this.token = token;
}
public int getX() {
return x;
}
public void setX(int x) {
this.x = x;
}
public long getTimestamp() {
return timestamp;
}
public void setTimestamp(long timestamp) {
this.timestamp = timestamp;
}
public boolean isVerified() {
return verified;
}
public void setVerified(boolean verified) {
this.verified = verified;
}
public boolean isExpired(int timeoutSeconds) {
return System.currentTimeMillis() - timestamp > timeoutSeconds * 1000L;
}
}
================================================
FILE: src/main/java/cn/wildfirechat/app/jpa/SlideVerifyRepository.java
================================================
package cn.wildfirechat.app.jpa;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import java.util.Optional;
@Repository
public interface SlideVerifyRepository extends CrudRepository {
Optional findByToken(String token);
@Modifying
@Query("DELETE FROM SlideVerify s WHERE s.timestamp < :cutoff")
int deleteExpired(@Param("cutoff") long cutoff);
}
================================================
FILE: src/main/java/cn/wildfirechat/app/jpa/UserConference.java
================================================
package cn.wildfirechat.app.jpa;
import javax.persistence.*;
import java.util.List;
import static javax.persistence.CascadeType.ALL;
@Entity
@Table(name = "user_conference", uniqueConstraints = {@UniqueConstraint(columnNames = {"userId","conferenceId"})})
public class UserConference {
@Id
@GeneratedValue(strategy=GenerationType.IDENTITY)
@Column(name = "id")
public Long id;
@Column(length = 128)
private String userId;
private String conferenceId;
private long timestamp;
public UserConference() {
}
public UserConference(String userId, String conferenceId) {
this.userId = userId;
this.conferenceId = conferenceId;
this.timestamp = System.currentTimeMillis();
}
public String getUserId() {
return userId;
}
public void setUserId(String userId) {
this.userId = userId;
}
public String getConferenceId() {
return conferenceId;
}
public void setConferenceId(String conferenceId) {
this.conferenceId = conferenceId;
}
}
================================================
FILE: src/main/java/cn/wildfirechat/app/jpa/UserConferenceQuota.java
================================================
package cn.wildfirechat.app.jpa;
import javax.persistence.*;
import java.util.Date;
/**
* 用户会议额度表
* 存储用户的会议分钟数配额(不分月份,固定额度)
*/
@Entity
@Table(name = "user_conference_quota")
public class UserConferenceQuota {
@Id
@Column(name = "user_id", length = 64)
private String userId;
@Column(name = "total_minutes")
private int totalMinutes;
@Column(name = "created_at")
@Temporal(TemporalType.TIMESTAMP)
private Date createdAt;
@Column(name = "updated_at")
@Temporal(TemporalType.TIMESTAMP)
private Date updatedAt;
@PrePersist
protected void onCreate() {
createdAt = new Date();
updatedAt = new Date();
}
@PreUpdate
protected void onUpdate() {
updatedAt = new Date();
}
public UserConferenceQuota() {
}
public UserConferenceQuota(String userId, int totalMinutes) {
this.userId = userId;
this.totalMinutes = totalMinutes;
}
public String getUserId() {
return userId;
}
public void setUserId(String userId) {
this.userId = userId;
}
public int getTotalMinutes() {
return totalMinutes;
}
public void setTotalMinutes(int totalMinutes) {
this.totalMinutes = totalMinutes;
}
public Date getCreatedAt() {
return createdAt;
}
public void setCreatedAt(Date createdAt) {
this.createdAt = createdAt;
}
public Date getUpdatedAt() {
return updatedAt;
}
public void setUpdatedAt(Date updatedAt) {
this.updatedAt = updatedAt;
}
}
================================================
FILE: src/main/java/cn/wildfirechat/app/jpa/UserConferenceQuotaRepository.java
================================================
package cn.wildfirechat.app.jpa;
import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Repository;
import java.util.Optional;
/**
* 用户会议额度Repository
*/
@Repository
public interface UserConferenceQuotaRepository extends CrudRepository {
/**
* 根据用户ID查询额度配置
*/
Optional findByUserId(String userId);
}
================================================
FILE: src/main/java/cn/wildfirechat/app/jpa/UserConferenceRepository.java
================================================
package cn.wildfirechat.app.jpa;
import cn.wildfirechat.app.model.ConferenceDTO;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.PagingAndSortingRepository;
import org.springframework.data.rest.core.annotation.RepositoryRestResource;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.Optional;
@RepositoryRestResource()
public interface UserConferenceRepository extends PagingAndSortingRepository {
@Transactional
@Modifying
@Query(value = "delete from user_conference where user_id = ?1 and conference_id = ?2", nativeQuery = true)
void deleteByUserIdAndConferenceId(String userId, String conferenceId);
@Transactional
@Modifying
@Query(value = "delete from user_conference where conference_id = ?1", nativeQuery = true)
void deleteByConferenceId(String conferenceId);
@Query(value = "select c.* from user_conference uc, conference c where uc.user_id = ?1 and uc.conference_id = c.id and (c.end_time = 0 or c.end_time > ?2) order by c.id desc", nativeQuery = true)
List findByUserId(String userId, long now);
Optional findByUserIdAndConferenceId(String userId, String conferenceId);
}
================================================
FILE: src/main/java/cn/wildfirechat/app/jpa/UserNameEntry.java
================================================
package cn.wildfirechat.app.jpa;
import javax.persistence.*;
@Entity
@Table(name = "t_user_name")
public class UserNameEntry {
@Id
@GeneratedValue(strategy=GenerationType.IDENTITY)
@Column(name = "id")
private Integer id;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
}
================================================
FILE: src/main/java/cn/wildfirechat/app/jpa/UserNameRepository.java
================================================
package cn.wildfirechat.app.jpa;
import org.springframework.data.repository.CrudRepository;
import org.springframework.data.rest.core.annotation.RepositoryRestResource;
@RepositoryRestResource()
public interface UserNameRepository extends CrudRepository {}
================================================
FILE: src/main/java/cn/wildfirechat/app/jpa/UserPassword.java
================================================
package cn.wildfirechat.app.jpa;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;
@Entity
@Table(name = "user_password")
public class UserPassword {
@Id
@Column(length = 128)
private String userId;
private String password;
private String salt;
private String resetCode;
private long resetCodeTime;
private int tryCount;
private long lastTryTime;
public UserPassword() {
}
public UserPassword(String userId) {
this.userId = userId;
}
public UserPassword(String userId, String password, String salt) {
this.userId = userId;
this.password = password;
this.salt = salt;
this.resetCodeTime = 0;
this.tryCount = 0;
this.lastTryTime = 0;
}
public UserPassword(String userId, String password, String salt, String resetCode, long resetCodeTime) {
this.userId = userId;
this.password = password;
this.salt = salt;
this.resetCode = resetCode;
this.resetCodeTime = resetCodeTime;
this.tryCount = 0;
this.lastTryTime = 0;
}
public String getUserId() {
return userId;
}
public void setUserId(String userId) {
this.userId = userId;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getSalt() {
return salt;
}
public void setSalt(String salt) {
this.salt = salt;
}
public String getResetCode() {
return resetCode;
}
public void setResetCode(String resetCode) {
this.resetCode = resetCode;
}
public long getResetCodeTime() {
return resetCodeTime;
}
public void setResetCodeTime(long resetCodeTime) {
this.resetCodeTime = resetCodeTime;
}
public int getTryCount() {
return tryCount;
}
public void setTryCount(int tryCount) {
this.tryCount = tryCount;
}
public long getLastTryTime() {
return lastTryTime;
}
public void setLastTryTime(long lastTryTime) {
this.lastTryTime = lastTryTime;
}
}
================================================
FILE: src/main/java/cn/wildfirechat/app/jpa/UserPasswordRepository.java
================================================
package cn.wildfirechat.app.jpa;
import org.springframework.data.repository.CrudRepository;
import org.springframework.data.repository.PagingAndSortingRepository;
import org.springframework.data.rest.core.annotation.RepositoryRestResource;
@RepositoryRestResource()
public interface UserPasswordRepository extends CrudRepository {
}
================================================
FILE: src/main/java/cn/wildfirechat/app/jpa/UserPrivateConferenceId.java
================================================
package cn.wildfirechat.app.jpa;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;
@Entity
@Table(name = "user_private_conference_id")
public class UserPrivateConferenceId {
@Id
@Column(length = 128)
private String userId;
private String conferenceId;
public UserPrivateConferenceId() {
}
public UserPrivateConferenceId(String userId, String conferenceId) {
this.userId = userId;
this.conferenceId = conferenceId;
}
public String getUserId() {
return userId;
}
public void setUserId(String userId) {
this.userId = userId;
}
public String getConferenceId() {
return conferenceId;
}
public void setConferenceId(String conferenceId) {
this.conferenceId = conferenceId;
}
}
================================================
FILE: src/main/java/cn/wildfirechat/app/jpa/UserPrivateConferenceIdRepository.java
================================================
package cn.wildfirechat.app.jpa;
import org.springframework.data.repository.PagingAndSortingRepository;
import org.springframework.data.rest.core.annotation.RepositoryRestResource;
@RepositoryRestResource()
public interface UserPrivateConferenceIdRepository extends PagingAndSortingRepository {
}
================================================
FILE: src/main/java/cn/wildfirechat/app/jpa/UserQuotaUsage.java
================================================
package cn.wildfirechat.app.jpa;
import javax.persistence.*;
import java.util.Date;
/**
* 用户额度使用表
* 记录每个月用户实际使用的会议分钟数
*/
@Entity
@Table(name = "user_quota_usage")
public class UserQuotaUsage {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "user_id", length = 64, nullable = false)
private String userId;
@Column(name = "billing_month", length = 8, nullable = false)
private String yearMonth;
@Column(name = "used_minutes")
private int usedMinutes;
@Column(name = "created_at")
@Temporal(TemporalType.TIMESTAMP)
private Date createdAt;
@Column(name = "updated_at")
@Temporal(TemporalType.TIMESTAMP)
private Date updatedAt;
@PrePersist
protected void onCreate() {
createdAt = new Date();
updatedAt = new Date();
}
@PreUpdate
protected void onUpdate() {
updatedAt = new Date();
}
public UserQuotaUsage() {
}
public UserQuotaUsage(String userId, String yearMonth, int usedMinutes) {
this.userId = userId;
this.yearMonth = yearMonth;
this.usedMinutes = usedMinutes;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getUserId() {
return userId;
}
public void setUserId(String userId) {
this.userId = userId;
}
public String getYearMonth() {
return yearMonth;
}
public void setYearMonth(String yearMonth) {
this.yearMonth = yearMonth;
}
public int getUsedMinutes() {
return usedMinutes;
}
public void setUsedMinutes(int usedMinutes) {
this.usedMinutes = usedMinutes;
}
public Date getCreatedAt() {
return createdAt;
}
public void setCreatedAt(Date createdAt) {
this.createdAt = createdAt;
}
public Date getUpdatedAt() {
return updatedAt;
}
public void setUpdatedAt(Date updatedAt) {
this.updatedAt = updatedAt;
}
}
================================================
FILE: src/main/java/cn/wildfirechat/app/jpa/UserQuotaUsageRepository.java
================================================
package cn.wildfirechat.app.jpa;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Repository;
import java.util.Optional;
/**
* 用户额度使用Repository
*/
@Repository
public interface UserQuotaUsageRepository extends CrudRepository {
/**
* 根据用户ID和年月查询使用记录
*/
Optional findByUserIdAndYearMonth(String userId, String yearMonth);
/**
* 增加用户使用分钟数
*/
@Modifying
@Query("UPDATE UserQuotaUsage u SET u.usedMinutes = u.usedMinutes + ?3 WHERE u.userId = ?1 AND u.yearMonth = ?2")
int addUsedMinutes(String userId, String yearMonth, int minutes);
}
================================================
FILE: src/main/java/cn/wildfirechat/app/model/ConferenceDTO.java
================================================
package cn.wildfirechat.app.model;
public interface ConferenceDTO {
String getId();
String getConference_title();
String getPassword();
String getPin();
String getOwner();
public String getManages();
long getStart_time();
long getEnd_time();
boolean isAudience();
boolean isAdvance();
boolean isAllow_switch_mode();
boolean isNo_join_before_start();
boolean isRecording();
String getFocus();
int getMax_participants();
}
================================================
FILE: src/main/java/cn/wildfirechat/app/pojo/CancelSessionRequest.java
================================================
package cn.wildfirechat.app.pojo;
public class CancelSessionRequest {
private String token;
public String getToken() {
return token;
}
public void setToken(String token) {
this.token = token;
}
}
================================================
FILE: src/main/java/cn/wildfirechat/app/pojo/ChangeNameRequest.java
================================================
package cn.wildfirechat.app.pojo;
public class ChangeNameRequest {
private String newName;
public String getNewName() {
return newName;
}
public void setNewName(String newName) {
this.newName = newName;
}
}
================================================
FILE: src/main/java/cn/wildfirechat/app/pojo/ChangePasswordRequest.java
================================================
package cn.wildfirechat.app.pojo;
public class ChangePasswordRequest {
private String oldPassword;
private String newPassword;
private String slideVerifyToken;
public String getOldPassword() {
return oldPassword;
}
public void setOldPassword(String oldPassword) {
this.oldPassword = oldPassword;
}
public String getNewPassword() {
return newPassword;
}
public void setNewPassword(String newPassword) {
this.newPassword = newPassword;
}
public String getSlideVerifyToken() {
return slideVerifyToken;
}
public void setSlideVerifyToken(String slideVerifyToken) {
this.slideVerifyToken = slideVerifyToken;
}
}
================================================
FILE: src/main/java/cn/wildfirechat/app/pojo/ComplainRequest.java
================================================
package cn.wildfirechat.app.pojo;
public class ComplainRequest {
public String text;
}
================================================
FILE: src/main/java/cn/wildfirechat/app/pojo/ConferenceInfo.java
================================================
package cn.wildfirechat.app.pojo;
import java.util.List;
public class ConferenceInfo {
public String conferenceId;
public String conferenceTitle;
public String password;
public String pin;
public String owner;
public List managers;
public long startTime;
public long endTime;
public boolean audience;
public boolean advance;
public boolean allowSwitchMode;
public boolean noJoinBeforeStart;
public boolean recording;
public String focus;
public int maxParticipants;
}
================================================
FILE: src/main/java/cn/wildfirechat/app/pojo/ConferenceInfoRequest.java
================================================
package cn.wildfirechat.app.pojo;
public class ConferenceInfoRequest {
public String conferenceId;
public String password;
}
================================================
FILE: src/main/java/cn/wildfirechat/app/pojo/ConferenceQuotaResponse.java
================================================
package cn.wildfirechat.app.pojo;
/**
* 会议额度查询响应
*/
public class ConferenceQuotaResponse {
// 用户月度额度(分钟)
private int totalQuota;
// 当月已使用额度(分钟)
private int usedMinutes;
// 当月剩余额度(分钟)
private int remainingMinutes;
// 是否不限制(true表示无额度限制)
private boolean unlimited;
// 当前年月(yyyyMM格式)
private String yearMonth;
public ConferenceQuotaResponse() {
}
public int getTotalQuota() {
return totalQuota;
}
public void setTotalQuota(int totalQuota) {
this.totalQuota = totalQuota;
}
public int getUsedMinutes() {
return usedMinutes;
}
public void setUsedMinutes(int usedMinutes) {
this.usedMinutes = usedMinutes;
}
public int getRemainingMinutes() {
return remainingMinutes;
}
public void setRemainingMinutes(int remainingMinutes) {
this.remainingMinutes = remainingMinutes;
}
public boolean isUnlimited() {
return unlimited;
}
public void setUnlimited(boolean unlimited) {
this.unlimited = unlimited;
}
public String getYearMonth() {
return yearMonth;
}
public void setYearMonth(String yearMonth) {
this.yearMonth = yearMonth;
}
}
================================================
FILE: src/main/java/cn/wildfirechat/app/pojo/ConfirmSessionRequest.java
================================================
package cn.wildfirechat.app.pojo;
public class ConfirmSessionRequest {
private String token;
private String user_id;
private int quick_login;
//{"token":"22295ee9-d4e3-4fc0-bda3-bcfe008dce08","user_id":"CeDRCRtt","quick_login":true}
public String getToken() {
return token;
}
public void setToken(String token) {
this.token = token;
}
public String getUser_id() {
return user_id;
}
public void setUser_id(String user_id) {
this.user_id = user_id;
}
public int getQuick_login() {
return quick_login;
}
public void setQuick_login(int quick_login) {
this.quick_login = quick_login;
}
}
================================================
FILE: src/main/java/cn/wildfirechat/app/pojo/CreateSessionRequest.java
================================================
package cn.wildfirechat.app.pojo;
public class CreateSessionRequest {
private String token;
private String device_name;
private String clientId;
private int platform;
// 0,表示pc端为旧版本,不支持快速登录;1,表示pc端为新版本,支持快速登录
private int flag;
private String userId;
public int getPlatform() {
return platform;
}
public void setPlatform(int platform) {
this.platform = platform;
}
public String getToken() {
return token;
}
public void setToken(String token) {
this.token = token;
}
public String getDevice_name() {
return device_name;
}
public void setDevice_name(String device_name) {
this.device_name = device_name;
}
public String getClientId() {
return clientId;
}
public void setClientId(String clientId) {
this.clientId = clientId;
}
public int getFlag() {
return flag;
}
public void setFlag(int flag) {
this.flag = flag;
}
public String getUserId() {
return userId;
}
}
================================================
FILE: src/main/java/cn/wildfirechat/app/pojo/DestroyRequest.java
================================================
package cn.wildfirechat.app.pojo;
public class DestroyRequest {
private String code;
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
}
================================================
FILE: src/main/java/cn/wildfirechat/app/pojo/GroupAnnouncementPojo.java
================================================
package cn.wildfirechat.app.pojo;
public class GroupAnnouncementPojo {
public String groupId;
public String author;
public String text;
public long timestamp;
}
================================================
FILE: src/main/java/cn/wildfirechat/app/pojo/GroupIdPojo.java
================================================
package cn.wildfirechat.app.pojo;
public class GroupIdPojo {
public String groupId;
}
================================================
FILE: src/main/java/cn/wildfirechat/app/pojo/LoadFavoriteRequest.java
================================================
package cn.wildfirechat.app.pojo;
public class LoadFavoriteRequest {
public long id;
public int count;
}
================================================
FILE: src/main/java/cn/wildfirechat/app/pojo/LoadFavoriteResponse.java
================================================
package cn.wildfirechat.app.pojo;
import cn.wildfirechat.app.jpa.FavoriteItem;
import java.util.List;
public class LoadFavoriteResponse {
public List items;
public boolean hasMore;
}
================================================
FILE: src/main/java/cn/wildfirechat/app/pojo/LoginResponse.java
================================================
package cn.wildfirechat.app.pojo;
public class LoginResponse {
private String userId;
private String token;
private boolean register;
private String userName;
private String portrait;
private String resetCode;
public String getUserId() {
return userId;
}
public void setUserId(String userId) {
this.userId = userId;
}
public String getToken() {
return token;
}
public void setToken(String token) {
this.token = token;
}
public boolean isRegister() {
return register;
}
public void setRegister(boolean register) {
this.register = register;
}
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
public String getPortrait() {
return portrait;
}
public void setPortrait(String portrait) {
this.portrait = portrait;
}
public String getResetCode() {
return resetCode;
}
public void setResetCode(String resetCode) {
this.resetCode = resetCode;
}
}
================================================
FILE: src/main/java/cn/wildfirechat/app/pojo/PhoneCodeLoginRequest.java
================================================
package cn.wildfirechat.app.pojo;
public class PhoneCodeLoginRequest {
private String mobile;
private String code;
private String clientId;
private Integer platform;
public String getClientId() {
return clientId;
}
public void setClientId(String clientId) {
this.clientId = clientId;
}
public String getMobile() {
return mobile;
}
public void setMobile(String mobile) {
this.mobile = mobile;
}
public String getCode() {
return code;
}
public Integer getPlatform() {
return platform;
}
public void setPlatform(Integer platform) {
this.platform = platform;
}
public void setCode(String code) {
this.code = code;
}
}
================================================
FILE: src/main/java/cn/wildfirechat/app/pojo/PhoneCodeLoginRequestWithSlideVerify.java
================================================
package cn.wildfirechat.app.pojo;
public class PhoneCodeLoginRequestWithSlideVerify {
private String mobile;
private String code;
private String clientId;
private Integer platform;
private String slideVerifyToken;
public String getMobile() {
return mobile;
}
public void setMobile(String mobile) {
this.mobile = mobile;
}
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
public String getClientId() {
return clientId;
}
public void setClientId(String clientId) {
this.clientId = clientId;
}
public Integer getPlatform() {
return platform;
}
public void setPlatform(Integer platform) {
this.platform = platform;
}
public String getSlideVerifyToken() {
return slideVerifyToken;
}
public void setSlideVerifyToken(String slideVerifyToken) {
this.slideVerifyToken = slideVerifyToken;
}
}
================================================
FILE: src/main/java/cn/wildfirechat/app/pojo/RecordingRequest.java
================================================
package cn.wildfirechat.app.pojo;
public class RecordingRequest {
public boolean recording;
}
================================================
FILE: src/main/java/cn/wildfirechat/app/pojo/ResetPasswordRequest.java
================================================
package cn.wildfirechat.app.pojo;
public class ResetPasswordRequest {
private String mobile;
private String resetCode;
private String newPassword;
public String getMobile() {
return mobile;
}
public void setMobile(String mobile) {
this.mobile = mobile;
}
public String getResetCode() {
return resetCode;
}
public void setResetCode(String resetCode) {
this.resetCode = resetCode;
}
public String getNewPassword() {
return newPassword;
}
public void setNewPassword(String newPassword) {
this.newPassword = newPassword;
}
}
================================================
FILE: src/main/java/cn/wildfirechat/app/pojo/SendCodeRequest.java
================================================
package cn.wildfirechat.app.pojo;
public class SendCodeRequest {
private String mobile;
private String slideVerifyToken;
public String getMobile() {
return mobile;
}
public void setMobile(String mobile) {
this.mobile = mobile;
}
public String getSlideVerifyToken() {
return slideVerifyToken;
}
public void setSlideVerifyToken(String slideVerifyToken) {
this.slideVerifyToken = slideVerifyToken;
}
}
================================================
FILE: src/main/java/cn/wildfirechat/app/pojo/SendCodeRequestWithSlideVerify.java
================================================
package cn.wildfirechat.app.pojo;
public class SendCodeRequestWithSlideVerify {
private String mobile;
private String slideVerifyToken;
public String getMobile() {
return mobile;
}
public void setMobile(String mobile) {
this.mobile = mobile;
}
public String getSlideVerifyToken() {
return slideVerifyToken;
}
public void setSlideVerifyToken(String slideVerifyToken) {
this.slideVerifyToken = slideVerifyToken;
}
}
================================================
FILE: src/main/java/cn/wildfirechat/app/pojo/SendDestroyCodeRequest.java
================================================
package cn.wildfirechat.app.pojo;
public class SendDestroyCodeRequest {
private String slideVerifyToken;
public String getSlideVerifyToken() {
return slideVerifyToken;
}
public void setSlideVerifyToken(String slideVerifyToken) {
this.slideVerifyToken = slideVerifyToken;
}
}
================================================
FILE: src/main/java/cn/wildfirechat/app/pojo/SendMessageRequest.java
================================================
package cn.wildfirechat.app.pojo;
import java.util.List;
public class SendMessageRequest {
public int type;
public String target;
public int line;
public int content_type;
public String content_searchable;
public String content_binary;
public String content;
public String content_push;
public String content_push_data;
public int content_media_type;
public String content_remote_url;
public String content_extra;
public int content_mentioned_type;
public List content_mentioned_targets;
}
================================================
FILE: src/main/java/cn/wildfirechat/app/pojo/SessionOutput.java
================================================
package cn.wildfirechat.app.pojo;
public class SessionOutput {
private String token;
private int status;
private long expired;
private int platform;
private String device_name;
private String userId;
public String getUserId() {
return userId;
}
public void setUserId(String userId) {
this.userId = userId;
}
public SessionOutput() {
}
public SessionOutput(String userId, String token, int status, long expired, String device_name, int platform) {
this.userId = userId;
this.token = token;
this.status = status;
this.expired = expired;
this.device_name = device_name;
this.platform = platform;
}
public String getToken() {
return token;
}
public void setToken(String token) {
this.token = token;
}
public int getStatus() {
return status;
}
public void setStatus(int status) {
this.status = status;
}
public long getExpired() {
return expired;
}
public void setExpired(long expired) {
this.expired = expired;
}
public String getDevice_name() {
return device_name;
}
public void setDevice_name(String device_name) {
this.device_name = device_name;
}
public int getPlatform() {
return platform;
}
public void setPlatform(int platform) {
this.platform = platform;
}
}
================================================
FILE: src/main/java/cn/wildfirechat/app/pojo/SlideVerifyRequest.java
================================================
package cn.wildfirechat.app.pojo;
public class SlideVerifyRequest {
private String token;
private int x; // 滑动块的x坐标位置
public String getToken() {
return token;
}
public void setToken(String token) {
this.token = token;
}
public int getX() {
return x;
}
public void setX(int x) {
this.x = x;
}
}
================================================
FILE: src/main/java/cn/wildfirechat/app/pojo/SlideVerifyResponse.java
================================================
package cn.wildfirechat.app.pojo;
public class SlideVerifyResponse {
private String token;
private String backgroundImage; // base64编码的背景图
private String sliderImage; // base64编码的滑块图
private int y; // 滑块在背景图中的y坐标
public String getToken() {
return token;
}
public void setToken(String token) {
this.token = token;
}
public String getBackgroundImage() {
return backgroundImage;
}
public void setBackgroundImage(String backgroundImage) {
this.backgroundImage = backgroundImage;
}
public String getSliderImage() {
return sliderImage;
}
public void setSliderImage(String sliderImage) {
this.sliderImage = sliderImage;
}
public int getY() {
return y;
}
public void setY(int y) {
this.y = y;
}
}
================================================
FILE: src/main/java/cn/wildfirechat/app/pojo/UploadFileResponse.java
================================================
package cn.wildfirechat.app.pojo;
public class UploadFileResponse {
public String url;
}
================================================
FILE: src/main/java/cn/wildfirechat/app/pojo/UserIdNamePortraitPojo.java
================================================
package cn.wildfirechat.app.pojo;
public class UserIdNamePortraitPojo {
public String userId;
public String name;
public String portrait;
public UserIdNamePortraitPojo() {
}
public UserIdNamePortraitPojo(String userId, String name, String portrait) {
this.userId = userId;
this.name = name;
this.portrait = portrait;
}
}
================================================
FILE: src/main/java/cn/wildfirechat/app/pojo/UserIdPojo.java
================================================
package cn.wildfirechat.app.pojo;
public class UserIdPojo {
public String userId;
}
================================================
FILE: src/main/java/cn/wildfirechat/app/pojo/UserPasswordLoginRequest.java
================================================
package cn.wildfirechat.app.pojo;
public class UserPasswordLoginRequest {
private String mobile;
private String password;
private String clientId;
private Integer platform;
public String getClientId() {
return clientId;
}
public void setClientId(String clientId) {
this.clientId = clientId;
}
public String getMobile() {
return mobile;
}
public void setMobile(String mobile) {
this.mobile = mobile;
}
public String getPassword() {
return password;
}
public Integer getPlatform() {
return platform;
}
public void setPlatform(Integer platform) {
this.platform = platform;
}
public void setPassword(String password) {
this.password = password;
}
}
================================================
FILE: src/main/java/cn/wildfirechat/app/pojo/UserPasswordLoginRequestWithSlideVerify.java
================================================
package cn.wildfirechat.app.pojo;
public class UserPasswordLoginRequestWithSlideVerify {
private String mobile;
private String password;
private String clientId;
private Integer platform;
private String slideVerifyToken;
public String getMobile() {
return mobile;
}
public void setMobile(String mobile) {
this.mobile = mobile;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getClientId() {
return clientId;
}
public void setClientId(String clientId) {
this.clientId = clientId;
}
public Integer getPlatform() {
return platform;
}
public void setPlatform(Integer platform) {
this.platform = platform;
}
public String getSlideVerifyToken() {
return slideVerifyToken;
}
public void setSlideVerifyToken(String slideVerifyToken) {
this.slideVerifyToken = slideVerifyToken;
}
}
================================================
FILE: src/main/java/cn/wildfirechat/app/shiro/AuthDataSource.java
================================================
package cn.wildfirechat.app.shiro;
import cn.wildfirechat.app.RestResult;
import cn.wildfirechat.app.jpa.PCSession;
import cn.wildfirechat.app.jpa.PCSessionRepository;
import cn.wildfirechat.app.jpa.Record;
import cn.wildfirechat.app.jpa.RecordRepository;
import cn.wildfirechat.app.pojo.SessionOutput;
import cn.wildfirechat.app.tools.Utils;
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.stereotype.Service;
import org.springframework.util.StringUtils;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Supplier;
import static cn.wildfirechat.app.RestResult.RestCode.*;
import static cn.wildfirechat.app.jpa.PCSession.PCSessionStatus.*;
@Service
public class AuthDataSource {
private static final Logger LOG = LoggerFactory.getLogger(AuthDataSource.class);
@Value("${sms.super_code}")
private String superCode;
@Autowired
private PCSessionRepository pcSessionRepository;
@Autowired
private RecordRepository recordRepository;
public RestResult.RestCode insertRecord(String mobile, String code) {
if (!Utils.isMobile(mobile)) {
LOG.error("Not valid mobile {}", mobile);
return RestResult.RestCode.ERROR_INVALID_MOBILE;
}
Record record = recordRepository.findById(mobile).orElseGet(() -> new Record(code, mobile));
if (System.currentTimeMillis() - record.getTimestamp() < 60 * 1000) {
LOG.error("Send code over frequency. timestamp {}, now {}", record.getTimestamp(), System.currentTimeMillis());
return RestResult.RestCode.ERROR_SEND_SMS_OVER_FREQUENCY;
}
if (!record.increaseAndCheck()) {
LOG.error("Count check failure, already send {} messages today", record.getRequestCount());
RestResult.RestCode c = RestResult.RestCode.ERROR_SEND_SMS_OVER_FREQUENCY;
c.msg = "发送给用户 " + mobile + " 超出频率限制";
return c;
}
record.setCode(code);
record.setTimestamp(System.currentTimeMillis());
recordRepository.save(record);
return RestResult.RestCode.SUCCESS;
}
public void clearRecode(String mobile) {
try {
recordRepository.deleteById(mobile);
} catch (Exception e) {
}
}
public RestResult.RestCode verifyCode(String mobile, String code) {
if (StringUtils.isEmpty(superCode) || !code.equals(superCode)) {
Optional recordOptional = recordRepository.findById(mobile);
if (!recordOptional.isPresent()) {
LOG.error("code not exist");
return RestResult.RestCode.ERROR_CODE_INCORRECT;
}
if(!recordOptional.get().getCode().equals(code)) {
LOG.error("code not matched");
return RestResult.RestCode.ERROR_CODE_INCORRECT;
}
if (System.currentTimeMillis() - recordOptional.get().getTimestamp() > 5 * 60 * 1000) {
LOG.error("Code expired. timestamp {}, now {}", recordOptional.get().getTimestamp(), System.currentTimeMillis());
return RestResult.RestCode.ERROR_CODE_EXPIRED;
}
}
return RestResult.RestCode.SUCCESS;
}
public PCSession createSession(String userId, String clientId, String token, int platform) {
PCSession session = new PCSession();
session.setConfirmedUserId(userId);
session.setStatus(StringUtils.isEmpty(userId) ? Session_Created : Session_Scanned);
session.setClientId(clientId);
session.setCreateDt(System.currentTimeMillis());
session.setPlatform(platform);
session.setDuration(300 * 1000); //300 seconds
if (StringUtils.isEmpty(token)) {
token = UUID.randomUUID().toString();
}
session.setToken(token);
pcSessionRepository.save(session);
return session;
}
public PCSession getSession(String token, boolean clear) {
Optional session = pcSessionRepository.findById(token);
if (clear) {
pcSessionRepository.deleteById(token);
}
return session.orElse(null);
}
public void saveSession(PCSession session) {
pcSessionRepository.save(session);
}
public RestResult scanPc(String userId, String token) {
Optional session = pcSessionRepository.findById(token);
if (session.isPresent()) {
SessionOutput output = session.get().toOutput();
LOG.info("user {} scan pc, session {} expired time left {}", userId, token, output.getExpired());
if (output.getExpired() > 0) {
session.get().setStatus(Session_Scanned);
session.get().setConfirmedUserId(userId);
output.setStatus(Session_Scanned);
output.setUserId(userId);
pcSessionRepository.save(session.get());
return RestResult.ok(output);
} else {
return RestResult.error(RestResult.RestCode.ERROR_SESSION_EXPIRED);
}
} else {
LOG.info("user {} scan pc, session {} not exist!", userId, token);
return RestResult.error(RestResult.RestCode.ERROR_SESSION_EXPIRED);
}
}
public RestResult confirmPc(String userId, String token) {
Optional session = pcSessionRepository.findById(token);
if (session.isPresent()) {
SessionOutput output = session.get().toOutput();
LOG.info("user {} confirm pc, session {} expired time left {}", userId, token, output.getExpired());
if (output.getExpired() > 0) {
session.get().setStatus(Session_Verified);
output.setStatus(Session_Verified);
session.get().setConfirmedUserId(userId);
pcSessionRepository.save(session.get());
return RestResult.ok(output);
} else {
return RestResult.error(RestResult.RestCode.ERROR_SESSION_EXPIRED);
}
} else {
LOG.error("user {} scan pc, session {} not exist!", userId, token);
return RestResult.error(RestResult.RestCode.ERROR_SESSION_EXPIRED);
}
}
public RestResult cancelPc(String token) {
LOG.error("session {} canceled", token);
Optional session = pcSessionRepository.findById(token);
if (session.isPresent()) {
session.get().setStatus(Session_Canceled);
pcSessionRepository.save(session.get());
}
return RestResult.ok(null);
}
public RestResult.RestCode checkPcSession(String token) {
Optional session = pcSessionRepository.findById(token);
if (session.isPresent()) {
if (session.get().getStatus() == Session_Verified) {
//使用用户id获取token
return SUCCESS;
} else {
if (session.get().getStatus() == Session_Created) {
return ERROR_SESSION_NOT_SCANED;
} else if (session.get().getStatus() == Session_Canceled) {
return ERROR_SESSION_CANCELED;
} else {
return ERROR_SESSION_NOT_VERIFIED;
}
}
} else {
return RestResult.RestCode.ERROR_SESSION_EXPIRED;
}
}
public String getUserId(String token, boolean clear) {
Optional session = pcSessionRepository.findById(token);
if (clear) {
pcSessionRepository.deleteById(token);
}
if (session.isPresent()) {
return session.get().getConfirmedUserId();
}
return null;
}
}
================================================
FILE: src/main/java/cn/wildfirechat/app/shiro/CorsFilter.java
================================================
package cn.wildfirechat.app.shiro;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Component
public class CorsFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String origin = request.getHeader("Origin");
response.setHeader("Access-Control-Allow-Origin", origin == null ? "*" : origin);
response.setHeader("Access-Control-Allow-Credentials", "true");
response.setHeader("Access-Control-Allow-Headers", "Content-Type, Access-Control-Allow-Headers, Cookie, X-Requested-With, authToken");
// refer to https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Expose-Headers
response.setHeader("Access-Control-Expose-Headers", "authToken");
filterChain.doFilter(request, response);
}
}
================================================
FILE: src/main/java/cn/wildfirechat/app/shiro/DBSessionDao.java
================================================
package cn.wildfirechat.app.shiro;
import cn.wildfirechat.app.jpa.ShiroSession;
import cn.wildfirechat.app.jpa.ShiroSessionRepository;
import com.google.gson.Gson;
import org.apache.shiro.session.Session;
import org.apache.shiro.session.UnknownSessionException;
import org.apache.shiro.session.mgt.SimpleSession;
import org.apache.shiro.session.mgt.eis.SessionDAO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.io.*;
import java.util.Collection;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
@Component
public class DBSessionDao implements SessionDAO {
private Map