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 sessionMap = new ConcurrentHashMap<>(); @Autowired private ShiroSessionRepository shiroSessionRepository; @Override public Serializable create(Session session) { String sessionId = UUID.randomUUID().toString().replaceAll("-", ""); ((SimpleSession) session).setId(sessionId); return sessionId; } @Override public Session readSession(Serializable sessionId) throws UnknownSessionException { // return sessionMap.get(sessionId); ShiroSession shiroSession = shiroSessionRepository.findById((String) sessionId).orElse(null); if (shiroSession != null) { Session session = byteToSession(shiroSession.getSessionData()); return session; } return null; } @Override public void update(Session session) throws UnknownSessionException { byte[] bb = sessionToByte(session); ShiroSession shiroSession = new ShiroSession((String)session.getId(), bb); // sessionMap.put(session.getId(), session); shiroSessionRepository.save(shiroSession); } @Override public void delete(Session session) { sessionMap.remove(session.getId()); } @Override public Collection getActiveSessions() { return sessionMap.values(); } // convert session object to byte, then store it to redis private byte[] sessionToByte(Session session){ ByteArrayOutputStream bo = new ByteArrayOutputStream(); byte[] bytes = null; try { ObjectOutputStream oo = new ObjectOutputStream(bo); oo.writeObject(session); bytes = bo.toByteArray(); } catch (IOException e) { e.printStackTrace(); } return bytes; } // restore session private Session byteToSession(byte[] bytes){ ByteArrayInputStream bi = new ByteArrayInputStream(bytes); ObjectInputStream in; SimpleSession session = null; try { in = new ObjectInputStream(bi); session = (SimpleSession) in.readObject(); } catch (ClassNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } return session; } } ================================================ FILE: src/main/java/cn/wildfirechat/app/shiro/JsonAuthLoginFilter.java ================================================ package cn.wildfirechat.app.shiro; import cn.wildfirechat.app.RestResult; import com.google.gson.Gson; import org.apache.shiro.web.filter.AccessControlFilter; import org.apache.shiro.SecurityUtils; import org.apache.shiro.subject.Subject; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import java.io.IOException; import java.io.PrintWriter; public class JsonAuthLoginFilter extends AccessControlFilter { @Override protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception { if (request instanceof HttpServletRequest) { if (((HttpServletRequest) request).getMethod().toUpperCase().equals("OPTIONS")) { return true; } } Subject subject = SecurityUtils.getSubject(); if(null != subject){ if(subject.isRemembered()){ return Boolean.TRUE; } if(subject.isAuthenticated()){ return Boolean.TRUE; } } return Boolean.FALSE ; } @Override protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception { PrintWriter out = null; try { response.setCharacterEncoding("UTF-8"); response.setContentType("application/json; charset=utf-8"); out = response.getWriter(); out.write(new Gson().toJson(RestResult.error(RestResult.RestCode.ERROR_NOT_LOGIN))); } catch (IOException e) { e.printStackTrace(); } finally { if (out != null) { out.close(); } } return Boolean.FALSE ; } } ================================================ FILE: src/main/java/cn/wildfirechat/app/shiro/LdapMatcher.java ================================================ package cn.wildfirechat.app.shiro; import cn.wildfirechat.app.tools.LdapUser; import org.apache.shiro.authc.AuthenticationInfo; import org.apache.shiro.authc.AuthenticationToken; import org.apache.shiro.authc.credential.CredentialsMatcher; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import javax.naming.AuthenticationException; import javax.naming.Context; import javax.naming.NamingEnumeration; import javax.naming.NamingException; import javax.naming.directory.*; import java.util.ArrayList; import java.util.Hashtable; import java.util.List; @Service public class LdapMatcher implements CredentialsMatcher { @Override public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) { if (token instanceof LdapToken) { try { LdapToken tt = (LdapToken)token; Hashtable env = new Hashtable<>(); env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory"); env.put(Context.PROVIDER_URL, tt.getLdapUrl()); env.put(Context.SECURITY_AUTHENTICATION, "simple"); env.put(Context.SECURITY_PRINCIPAL, tt.getPrincipal().toString()); env.put(Context.SECURITY_CREDENTIALS, tt.getCredentials().toString()); new InitialDirContext(env).close(); // 能 bind 就算成功 return true; } catch (NamingException e) { e.printStackTrace(); return false; } } return false; } /* --------- 验证入口 --------- */ public static boolean authenticate(String ldapUrl, String dn, String password) { try { Hashtable env = new Hashtable<>(); env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory"); env.put(Context.PROVIDER_URL, ldapUrl); env.put(Context.SECURITY_AUTHENTICATION, "simple"); env.put(Context.SECURITY_PRINCIPAL, dn); env.put(Context.SECURITY_CREDENTIALS, password); /* 成功 bind 立即关闭 */ new InitialDirContext(env).close(); return true; } catch (AuthenticationException e) { // 密码错或账号不存在 return false; } catch (NamingException e) { throw new RuntimeException("LDAP 异常", e); } } private static final String LDAP_URL = "ldap://192.168.1.48:389"; // 换成你的 LDAP 地址 private static final String USER_DN = "uid=user6,ou=people,dc=wildfirechat,dc=net"; public static void main(String[] args) { boolean ok = authenticate(LDAP_URL, USER_DN, "123456"); // 与条目里明文一致 System.out.println(ok ? "登录成功" : "用户名或密码错误"); } } ================================================ FILE: src/main/java/cn/wildfirechat/app/shiro/LdapRealm.java ================================================ package cn.wildfirechat.app.shiro; import cn.wildfirechat.app.jpa.UserPassword; import cn.wildfirechat.app.jpa.UserPasswordRepository; import org.apache.shiro.authc.*; import org.apache.shiro.authc.credential.Sha1CredentialsMatcher; import org.apache.shiro.authz.AuthorizationInfo; import org.apache.shiro.authz.SimpleAuthorizationInfo; import org.apache.shiro.realm.AuthorizingRealm; import org.apache.shiro.subject.PrincipalCollection; import org.apache.shiro.util.ByteSource; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import javax.annotation.PostConstruct; import java.nio.charset.StandardCharsets; import java.util.Optional; @Service public class LdapRealm extends AuthorizingRealm { @Autowired private UserPasswordRepository userPasswordRepository; @PostConstruct private void initMatcher() { LdapMatcher matcher = new LdapMatcher(); setCredentialsMatcher(matcher); } @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(); // Set stringSet = new HashSet<>(); // stringSet.add("user:show"); // stringSet.add("user:admin"); // info.setStringPermissions(stringSet); return info; } @Override public boolean supports(AuthenticationToken token) { if (token instanceof LdapToken) return true; return super.supports(token); } @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { if (authenticationToken instanceof LdapToken) { String userId = (String) authenticationToken.getPrincipal(); return new SimpleAuthenticationInfo(authenticationToken.getPrincipal(), authenticationToken.getCredentials(), getName()); } throw new AuthenticationException("没有密码"); } } ================================================ FILE: src/main/java/cn/wildfirechat/app/shiro/LdapToken.java ================================================ package cn.wildfirechat.app.shiro; import org.apache.shiro.authc.AuthenticationToken; public class LdapToken implements AuthenticationToken { final private String phone; final private String password; final private String ldapUrl; public LdapToken(String phone, String password, String ldapUrl) { this.phone = phone; this.password = password; this.ldapUrl = ldapUrl; } @Override public Object getPrincipal() { return phone; } @Override public Object getCredentials() { return password; } public String getLdapUrl() { return ldapUrl; } } ================================================ FILE: src/main/java/cn/wildfirechat/app/shiro/PhoneCodeRealm.java ================================================ package cn.wildfirechat.app.shiro; import cn.wildfirechat.app.RestResult; import org.apache.shiro.authc.*; import org.apache.shiro.authz.AuthorizationInfo; import org.apache.shiro.authz.SimpleAuthorizationInfo; import org.apache.shiro.realm.AuthorizingRealm; import org.apache.shiro.subject.PrincipalCollection; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import javax.annotation.PostConstruct; @Service public class PhoneCodeRealm extends AuthorizingRealm { @Autowired AuthDataSource authDataSource; @PostConstruct void initRealm() { setAuthenticationTokenClass(PhoneCodeToken.class); } @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(); // Set stringSet = new HashSet<>(); // stringSet.add("user:show"); // stringSet.add("user:admin"); // info.setStringPermissions(stringSet); return info; } @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { if (authenticationToken instanceof PhoneCodeToken) { String mobile = (String) authenticationToken.getPrincipal(); String code = (String)authenticationToken.getCredentials(); RestResult.RestCode restCode = authDataSource.verifyCode(mobile, code); if (restCode == RestResult.RestCode.SUCCESS) { return new SimpleAuthenticationInfo(mobile, code.getBytes(), getName()); } } throw new AuthenticationException("没发送验证码或者验证码过期"); } } ================================================ FILE: src/main/java/cn/wildfirechat/app/shiro/PhoneCodeToken.java ================================================ package cn.wildfirechat.app.shiro; import org.apache.shiro.authc.AuthenticationToken; public class PhoneCodeToken implements AuthenticationToken { final private String phone; final private String code; public PhoneCodeToken(String phone, String code) { this.phone = phone; this.code = code; } @Override public Object getPrincipal() { return phone; } @Override public Object getCredentials() { return code; } } ================================================ FILE: src/main/java/cn/wildfirechat/app/shiro/ScanCodeRealm.java ================================================ package cn.wildfirechat.app.shiro; import cn.wildfirechat.app.jpa.PCSession; import org.apache.shiro.authc.*; import org.apache.shiro.authz.AuthorizationInfo; import org.apache.shiro.authz.SimpleAuthorizationInfo; import org.apache.shiro.realm.AuthorizingRealm; import org.apache.shiro.subject.PrincipalCollection; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import javax.annotation.PostConstruct; @Service public class ScanCodeRealm extends AuthorizingRealm { @Autowired AuthDataSource authDataSource; @Autowired TokenMatcher tokenMatcher; @PostConstruct private void initMatcher() { setCredentialsMatcher(tokenMatcher); } @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(); // Set stringSet = new HashSet<>(); // stringSet.add("user:show"); // stringSet.add("user:admin"); // info.setStringPermissions(stringSet); return info; } @Override public boolean supports(AuthenticationToken token) { if (token instanceof TokenAuthenticationToken) return true; return super.supports(token); } @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { String token = (String) authenticationToken.getPrincipal(); PCSession session = authDataSource.getSession(token, false); if (session == null) { throw new AuthenticationException("会话不存在"); } return new SimpleAuthenticationInfo(token, token, getName()); } } ================================================ FILE: src/main/java/cn/wildfirechat/app/shiro/ShiroConfig.java ================================================ package cn.wildfirechat.app.shiro; import org.apache.shiro.SecurityUtils; import org.apache.shiro.mgt.SecurityManager; import org.apache.shiro.session.mgt.SessionManager; import org.apache.shiro.spring.web.ShiroFilterFactoryBean; import org.apache.shiro.web.mgt.DefaultWebSecurityManager; import org.apache.shiro.web.servlet.Cookie; import org.apache.shiro.web.servlet.ShiroHttpSession; import org.apache.shiro.web.servlet.SimpleCookie; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import java.util.Arrays; import java.util.LinkedHashMap; import java.util.Map; @Configuration public class ShiroConfig { @Autowired DBSessionDao dbSessionDao; @Autowired private PhoneCodeRealm phoneCodeRealm; @Autowired private ScanCodeRealm scanCodeRealm; @Autowired private UserPasswordRealm userPasswordRealm; @Autowired private LdapRealm ldapRealm; @Value("${wfc.all_client_support_ssl}") private boolean All_Client_Support_SSL; @Bean(name = "shiroFilter") public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) { ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean(); shiroFilterFactoryBean.setSecurityManager(securityManager); shiroFilterFactoryBean.setLoginUrl("/login"); shiroFilterFactoryBean.setUnauthorizedUrl("/notRole"); Map filterChainDefinitionMap = new LinkedHashMap<>(); // filterChainDefinitionMap.put("/send_code", "anon"); filterChainDefinitionMap.put("/login", "anon"); filterChainDefinitionMap.put("/pc_session", "anon"); filterChainDefinitionMap.put("/login_pwd", "anon"); filterChainDefinitionMap.put("/send_reset_code", "anon"); filterChainDefinitionMap.put("/reset_pwd", "anon"); filterChainDefinitionMap.put("/session_login/**", "anon"); filterChainDefinitionMap.put("/user/online_event", "anon"); filterChainDefinitionMap.put("/logs/**", "anon"); filterChainDefinitionMap.put("/im_event/**", "anon"); filterChainDefinitionMap.put("/im_exception_event/**", "anon"); filterChainDefinitionMap.put("/message/censor", "anon"); // 滑动验证接口 - 匿名访问 filterChainDefinitionMap.put("/slide_verify/generate", "anon"); filterChainDefinitionMap.put("/slide_verify/verify", "anon"); filterChainDefinitionMap.put("/", "anon"); filterChainDefinitionMap.put("/confirm_pc", "login"); filterChainDefinitionMap.put("/cancel_pc", "login"); filterChainDefinitionMap.put("/scan_pc/**", "login"); filterChainDefinitionMap.put("/put_group_announcement", "login"); filterChainDefinitionMap.put("/get_group_announcement", "login"); filterChainDefinitionMap.put("/things/add_device", "login"); filterChainDefinitionMap.put("/things/list_device", "login"); filterChainDefinitionMap.put("/amr2mp3", "anon"); filterChainDefinitionMap.put("/avatar/**", "anon"); //主要这行代码必须放在所有权限设置的最后,不然会导致所有 url 都被拦截 剩余的都需要认证 filterChainDefinitionMap.put("/**", "login"); shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap); shiroFilterFactoryBean.getFilters().put("login", new JsonAuthLoginFilter()); return shiroFilterFactoryBean; } @Bean public SecurityManager securityManager() { DefaultWebSecurityManager defaultSecurityManager = new DefaultWebSecurityManager(); defaultSecurityManager.setRealms(Arrays.asList(phoneCodeRealm, scanCodeRealm, userPasswordRealm, ldapRealm)); ShiroSessionManager sessionManager = new ShiroSessionManager(); sessionManager.setGlobalSessionTimeout(Long.MAX_VALUE); sessionManager.setSessionDAO(dbSessionDao); Cookie cookie = new SimpleCookie(ShiroHttpSession.DEFAULT_SESSION_ID_NAME); if (All_Client_Support_SSL) { cookie.setSameSite(Cookie.SameSiteOptions.NONE); cookie.setSecure(true); } else { cookie.setSameSite(null); } cookie.setMaxAge(Integer.MAX_VALUE); sessionManager.setSessionIdCookie(cookie); sessionManager.setSessionIdCookieEnabled(true); sessionManager.setSessionIdUrlRewritingEnabled(true); defaultSecurityManager.setSessionManager(sessionManager); SecurityUtils.setSecurityManager(defaultSecurityManager); return defaultSecurityManager; } } ================================================ FILE: src/main/java/cn/wildfirechat/app/shiro/ShiroSessionManager.java ================================================ package cn.wildfirechat.app.shiro; import com.aliyuncs.utils.StringUtils; import org.apache.shiro.web.servlet.ShiroHttpServletRequest; import org.apache.shiro.web.session.mgt.DefaultWebSessionManager; import org.apache.shiro.web.util.WebUtils; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import java.io.Serializable; public class ShiroSessionManager extends DefaultWebSessionManager { private static final String AUTHORIZATION = "authToken"; private static final String REFERENCED_SESSION_ID_SOURCE = "Stateless request"; public ShiroSessionManager(){ super(); } @Override protected Serializable getSessionId(ServletRequest request, ServletResponse response){ String id = WebUtils.toHttp(request).getHeader(AUTHORIZATION); if(StringUtils.isEmpty(id)){ //如果没有携带id参数则按照父类的方式在cookie进行获取 System.out.println("super:"+super.getSessionId(request, response)); return super.getSessionId(request, response); }else{ //如果请求头中有 authToken 则其值为sessionId request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE,REFERENCED_SESSION_ID_SOURCE); request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID,id); request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID,Boolean.TRUE); return id; } } } ================================================ FILE: src/main/java/cn/wildfirechat/app/shiro/TokenAuthenticationToken.java ================================================ package cn.wildfirechat.app.shiro; import org.apache.shiro.authc.AuthenticationToken; public class TokenAuthenticationToken implements AuthenticationToken { private String token; public TokenAuthenticationToken(String token) { this.token = token; } public String getToken() { return token; } public void setToken(String token) { this.token = token; } @Override public Object getPrincipal() { return token; } @Override public Object getCredentials() { return null; } } ================================================ FILE: src/main/java/cn/wildfirechat/app/shiro/TokenMatcher.java ================================================ package cn.wildfirechat.app.shiro; import cn.wildfirechat.app.RestResult; import cn.wildfirechat.pojos.InputOutputUserInfo; import cn.wildfirechat.sdk.UserAdmin; import cn.wildfirechat.sdk.model.IMResult; import cn.wildfirechat.sdk.utilities.AdminHttpUtils; import org.apache.shiro.authc.AuthenticationInfo; import org.apache.shiro.authc.AuthenticationToken; import org.apache.shiro.authc.credential.CredentialsMatcher; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @Service public class TokenMatcher implements CredentialsMatcher { @Autowired private AuthDataSource authDataSource; @Override public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) { if (token instanceof TokenAuthenticationToken) { TokenAuthenticationToken tt = (TokenAuthenticationToken)token; RestResult.RestCode restCode = authDataSource.checkPcSession(tt.getToken()); if (restCode == RestResult.RestCode.SUCCESS) { return true; } } return false; } public static void main(String[] args) { AdminHttpUtils.init("http://wildfirechat.cn:18080", "37923"); try { IMResult userByMobile = UserAdmin.getUserByMobile("13888888888"); System.out.println(userByMobile.msg); } catch (Exception e) { e.printStackTrace(); } } } ================================================ FILE: src/main/java/cn/wildfirechat/app/shiro/UserPasswordRealm.java ================================================ package cn.wildfirechat.app.shiro; import cn.wildfirechat.app.RestResult; import cn.wildfirechat.app.jpa.ShiroSession; import cn.wildfirechat.app.jpa.UserPassword; import cn.wildfirechat.app.jpa.UserPasswordRepository; import org.apache.shiro.authc.*; import org.apache.shiro.authc.credential.HashedCredentialsMatcher; import org.apache.shiro.authc.credential.Sha1CredentialsMatcher; import org.apache.shiro.authz.AuthorizationInfo; import org.apache.shiro.authz.SimpleAuthorizationInfo; import org.apache.shiro.crypto.hash.Sha1Hash; import org.apache.shiro.realm.AuthorizingRealm; import org.apache.shiro.subject.PrincipalCollection; import org.apache.shiro.util.ByteSource; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import javax.annotation.PostConstruct; import java.nio.charset.StandardCharsets; import java.util.Optional; @Service public class UserPasswordRealm extends AuthorizingRealm { @Autowired private UserPasswordRepository userPasswordRepository; @PostConstruct private void initMatcher() { Sha1CredentialsMatcher matcher = new Sha1CredentialsMatcher(); matcher.setStoredCredentialsHexEncoded(false); setCredentialsMatcher(matcher); } @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(); // Set stringSet = new HashSet<>(); // stringSet.add("user:show"); // stringSet.add("user:admin"); // info.setStringPermissions(stringSet); return info; } @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { if (authenticationToken instanceof UsernamePasswordToken) { String userId = (String) authenticationToken.getPrincipal(); Optional optional = userPasswordRepository.findById(userId); if (optional.isPresent()) { UserPassword up = optional.get(); return new SimpleAuthenticationInfo(authenticationToken.getPrincipal(), up.getPassword(), ByteSource.Util.bytes(up.getSalt().getBytes(StandardCharsets.UTF_8)), getName()); } } throw new AuthenticationException("没有密码"); } } ================================================ FILE: src/main/java/cn/wildfirechat/app/slide/SlideVerifyCleanupService.java ================================================ package cn.wildfirechat.app.slide; import cn.wildfirechat.app.jpa.SlideVerifyRepository; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @Service public class SlideVerifyCleanupService { @Autowired private SlideVerifyRepository slideVerifyRepository; @Transactional public void cleanupExpired() { slideVerifyRepository.deleteExpired(System.currentTimeMillis() - 300 * 1000); } } ================================================ FILE: src/main/java/cn/wildfirechat/app/slide/SlideVerifyService.java ================================================ package cn.wildfirechat.app.slide; import cn.wildfirechat.app.jpa.SlideVerify; import cn.wildfirechat.app.jpa.SlideVerifyRepository; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import javax.imageio.ImageIO; import java.awt.*; import java.awt.geom.RoundRectangle2D; import java.awt.image.BufferedImage; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.util.Base64; import java.util.HashMap; import java.util.Map; import java.util.UUID; @Service public class SlideVerifyService { private static final Logger LOG = LoggerFactory.getLogger(SlideVerifyService.class); @Autowired private SlideVerifyRepository slideVerifyRepository; // 验证码有效时间(秒) private static final int VERIFY_TIMEOUT = 300; // 5分钟 // 验证误差范围(像素) private static final int TOLERANCE = 10; // 图片尺寸 private static final int IMAGE_WIDTH = 300; private static final int IMAGE_HEIGHT = 150; private static final int SLIDER_WIDTH = 50; private static final int SLIDER_HEIGHT = 50; /** * 生成滑动验证码 */ public Map generateSlideVerify() { // 生成token String token = UUID.randomUUID().toString(); // 随机生成滑块的x和y坐标 int x = 50 + (int)(Math.random() * (IMAGE_WIDTH - SLIDER_WIDTH - 100)); // 留出左右余量 int y = 20 + (int)(Math.random() * (IMAGE_HEIGHT - SLIDER_HEIGHT - 40)); // 留出上下余量 LOG.info("生成滑动验证码: token={}, x={}, y={}", token, x, y); // 保存验证数据到数据库 SlideVerify slideVerify = new SlideVerify(token, x, System.currentTimeMillis()); slideVerifyRepository.save(slideVerify); try { // 生成背景图(带缺口) String backgroundImage = generateBackgroundWithHole(x, y); LOG.info("背景图生成完成,长度: {}", backgroundImage.length()); // 生成滑块图 String sliderImage = generateSlider(x, y); LOG.info("滑块图生成完成,长度: {}", sliderImage.length()); Map result = new HashMap<>(); result.put("token", token); result.put("backgroundImage", backgroundImage); result.put("sliderImage", sliderImage); result.put("y", y); return result; } catch (Exception e) { LOG.error("生成滑动验证码失败", e); slideVerifyRepository.delete(slideVerify); throw new RuntimeException("生成滑动验证码失败"); } } /** * 验证滑动位置 */ public boolean verifySlide(String token, int userX) { SlideVerify data = slideVerifyRepository.findByToken(token).orElse(null); if (data == null) { LOG.warn("验证token不存在: {}", token); return false; } if (data.isExpired(VERIFY_TIMEOUT)) { LOG.warn("验证token已过期: {}", token); slideVerifyRepository.delete(data); return false; } if (data.isVerified()) { LOG.warn("验证token已使用: {}", token); return false; } // 验证位置是否在误差范围内 int difference = Math.abs(data.getX() - userX); boolean success = difference <= TOLERANCE; if (success) { data.setVerified(true); slideVerifyRepository.save(data); LOG.info("滑动验证成功,token: {}, 正确位置: {}, 用户位置: {}, 差值: {}", token, data.getX(), userX, difference); } else { LOG.warn("滑动验证失败,token: {}, 正确位置: {}, 用户位置: {}, 差值: {}, 容差: {}", token, data.getX(), userX, difference, TOLERANCE); slideVerifyRepository.delete(data); // 验证失败则删除记录 } return success; } /** * 检查token是否已验证(一次性使用) */ public boolean isVerified(String token) { SlideVerify data = slideVerifyRepository.findByToken(token).orElse(null); if (data == null || data.isExpired(VERIFY_TIMEOUT)) { return false; } // token已验证通过,立即删除,确保只能使用一次 if (data.isVerified()) { LOG.info("验证token已使用,删除token: {}", token); slideVerifyRepository.delete(data); return true; } return false; } /** * 清理过期的验证数据 */ public void cleanExpiredData() { long cutoff = System.currentTimeMillis() - VERIFY_TIMEOUT * 1000L; slideVerifyRepository.deleteExpired(cutoff); } /** * 生成带缺口的背景图 */ private String generateBackgroundWithHole(int x, int y) throws IOException { // 创建背景图片 BufferedImage image = new BufferedImage(IMAGE_WIDTH, IMAGE_HEIGHT, BufferedImage.TYPE_INT_RGB); Graphics2D g = image.createGraphics(); // 设置抗锯齿 g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); // 绘制渐变背景 GradientPaint gradient = new GradientPaint(0, 0, getRandomColor(), IMAGE_WIDTH, IMAGE_HEIGHT, getRandomColor()); g.setPaint(gradient); g.fillRect(0, 0, IMAGE_WIDTH, IMAGE_HEIGHT); // 添加一些随机图形增加复杂度 addRandomShapes(g); // 绘制缺口(带阴影效果的镂空) drawHole(g, x, y); g.dispose(); return imageToBase64(image); } /** * 生成滑块图 */ private String generateSlider(int x, int y) throws IOException { // 创建滑块图片(透明背景) BufferedImage image = new BufferedImage(SLIDER_WIDTH, SLIDER_HEIGHT, BufferedImage.TYPE_INT_ARGB); Graphics2D g = image.createGraphics(); // 设置抗锯齿 g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); // 绘制拼图形状的滑块 drawSliderShape(g); g.dispose(); return imageToBase64(image); } /** * 绘制缺口 */ private void drawHole(Graphics2D g, int x, int y) { // 使用半透明黑色绘制缺口 g.setColor(new Color(0, 0, 0, 130)); // 绘制拼图形状的缺口 int w = SLIDER_WIDTH; int h = SLIDER_HEIGHT; // 使用圆角矩形绘制简单的缺口,不绘制边框 RoundRectangle2D shape = new RoundRectangle2D.Float(x, y, w, h, 10, 10); g.fill(shape); } /** * 绘制滑块 */ private void drawSliderShape(Graphics2D g) { int w = SLIDER_WIDTH; int h = SLIDER_HEIGHT; // 绘制白色滑块,不绘制边框 g.setColor(new Color(255, 255, 255, 250)); RoundRectangle2D shape = new RoundRectangle2D.Float(0, 0, w, h, 10, 10); g.fill(shape); // 在滑块中间添加箭头图标 g.setColor(new Color(80, 80, 80)); int arrowX = w / 2 - 8; int arrowY = h / 2 - 8; int[] xp = {arrowX, arrowX + 16, arrowX + 8}; int[] yp = {arrowY, arrowY + 8, arrowY + 16}; g.fillPolygon(xp, yp, 3); } /** * 添加随机图形 */ private void addRandomShapes(Graphics2D g) { for (int i = 0; i < 20; i++) { g.setColor(getRandomColor()); int shapeType = (int)(Math.random() * 3); int x = (int)(Math.random() * IMAGE_WIDTH); int y = (int)(Math.random() * IMAGE_HEIGHT); int size = 10 + (int)(Math.random() * 30); switch (shapeType) { case 0: g.fillOval(x, y, size, size); break; case 1: g.fillRect(x, y, size, size); break; case 2: g.drawLine(x, y, x + size, y + size); break; } } } /** * 获取随机颜色 */ private Color getRandomColor() { return new Color( (int)(Math.random() * 256), (int)(Math.random() * 256), (int)(Math.random() * 256) ); } /** * 将图片转换为Base64编码 */ private String imageToBase64(BufferedImage image) throws IOException { ByteArrayOutputStream baos = new ByteArrayOutputStream(); ImageIO.write(image, "png", baos); byte[] imageBytes = baos.toByteArray(); // 返回带前缀的 data URI return "data:image/png;base64," + Base64.getEncoder().encodeToString(imageBytes); } } ================================================ FILE: src/main/java/cn/wildfirechat/app/sms/AliyunSMSConfig.java ================================================ package cn.wildfirechat.app.sms; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; @Configuration @ConfigurationProperties(prefix="alisms") @PropertySource(value = "file:config/aliyun_sms.properties",encoding = "utf-8") public class AliyunSMSConfig { String accessKeyId; String accessSecret; String signName; String templateCode; public String getAccessKeyId() { return accessKeyId; } public void setAccessKeyId(String accessKeyId) { this.accessKeyId = accessKeyId; } public String getAccessSecret() { return accessSecret; } public void setAccessSecret(String accessSecret) { this.accessSecret = accessSecret; } public String getSignName() { return signName; } public void setSignName(String signName) { this.signName = signName; } public String getTemplateCode() { return templateCode; } public void setTemplateCode(String templateCode) { this.templateCode = templateCode; } } ================================================ FILE: src/main/java/cn/wildfirechat/app/sms/SmsService.java ================================================ package cn.wildfirechat.app.sms; import cn.wildfirechat.app.RestResult; public interface SmsService { RestResult.RestCode sendCode(String mobile, String code); } ================================================ FILE: src/main/java/cn/wildfirechat/app/sms/SmsServiceImpl.java ================================================ package cn.wildfirechat.app.sms; import cn.wildfirechat.app.RestResult; import com.aliyuncs.CommonRequest; import com.aliyuncs.CommonResponse; import com.aliyuncs.DefaultAcsClient; import com.aliyuncs.IAcsClient; import com.aliyuncs.exceptions.ClientException; import com.aliyuncs.exceptions.ServerException; import com.aliyuncs.http.MethodType; import com.aliyuncs.profile.DefaultProfile; import com.google.gson.Gson; import com.tencentcloudapi.common.Credential; import com.tencentcloudapi.common.exception.TencentCloudSDKException; import com.tencentcloudapi.common.profile.ClientProfile; import com.tencentcloudapi.common.profile.HttpProfile; import com.tencentcloudapi.sms.v20210111.SmsClient; import com.tencentcloudapi.sms.v20210111.models.SendSmsRequest; import com.tencentcloudapi.sms.v20210111.models.SendSmsResponse; 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 java.io.IOException; @Service public class SmsServiceImpl implements SmsService { private static final Logger LOG = LoggerFactory.getLogger(SmsServiceImpl.class); private static class AliyunCommonResponse { String Message; String Code; } @Value("${sms.verdor}") private int smsVerdor; @Autowired private TencentSMSConfig mTencentSMSConfig; @Autowired private AliyunSMSConfig aliyunSMSConfig; @Override public RestResult.RestCode sendCode(String mobile, String code) { if (smsVerdor == 1) { return sendTencentCode(mobile, code); } else if(smsVerdor == 2) { return sendAliyunCode(mobile, code); } else { return RestResult.RestCode.ERROR_SERVER_NOT_IMPLEMENT; } } private RestResult.RestCode sendTencentCode(String mobile, String code) { try { /* 必要步骤: * 实例化一个认证对象,入参需要传入腾讯云账户密钥对secretId,secretKey。 * 这里采用的是从环境变量读取的方式,需要在环境变量中先设置这两个值。 * 你也可以直接在代码中写死密钥对,但是小心不要将代码复制、上传或者分享给他人, * 以免泄露密钥对危及你的财产安全。 * CAM密匙查询: https://console.cloud.tencent.com/cam/capi*/ Credential cred = new Credential(mTencentSMSConfig.secretId, mTencentSMSConfig.secretKey); // 实例化一个http选项,可选,没有特殊需求可以跳过 HttpProfile httpProfile = new HttpProfile(); // 设置代理 // httpProfile.setProxyHost("真实代理ip"); // httpProfile.setProxyPort(真实代理端口); /* SDK默认使用POST方法。 * 如果你一定要使用GET方法,可以在这里设置。GET方法无法处理一些较大的请求 */ httpProfile.setReqMethod("POST"); /* SDK有默认的超时时间,非必要请不要进行调整 * 如有需要请在代码中查阅以获取最新的默认值 */ httpProfile.setConnTimeout(60); /* SDK会自动指定域名。通常是不需要特地指定域名的,但是如果你访问的是金融区的服务 * 则必须手动指定域名,例如sms的上海金融区域名: sms.ap-shanghai-fsi.tencentcloudapi.com */ httpProfile.setEndpoint("sms.tencentcloudapi.com"); /* 非必要步骤: * 实例化一个客户端配置对象,可以指定超时时间等配置 */ ClientProfile clientProfile = new ClientProfile(); /* SDK默认用TC3-HMAC-SHA256进行签名 * 非必要请不要修改这个字段 */ clientProfile.setSignMethod("HmacSHA256"); clientProfile.setHttpProfile(httpProfile); /* 实例化要请求产品(以sms为例)的client对象 * 第二个参数是地域信息,可以直接填写字符串ap-guangzhou,或者引用预设的常量 */ SmsClient client = new SmsClient(cred, "ap-guangzhou",clientProfile); /* 实例化一个请求对象,根据调用的接口和实际情况,可以进一步设置请求参数 * 你可以直接查询SDK源码确定接口有哪些属性可以设置 * 属性可能是基本类型,也可能引用了另一个数据结构 * 推荐使用IDE进行开发,可以方便的跳转查阅各个接口和数据结构的文档说明 */ SendSmsRequest req = new SendSmsRequest(); /* 填充请求参数,这里request对象的成员变量即对应接口的入参 * 你可以通过官网接口文档或跳转到request对象的定义处查看请求参数的定义 * 基本类型的设置: * 帮助链接: * 短信控制台: https://console.cloud.tencent.com/smsv2 * sms helper: https://cloud.tencent.com/document/product/382/3773 */ /* 短信应用ID: 短信SdkAppId在 [短信控制台] 添加应用后生成的实际SdkAppId,示例如1400006666 */ req.setSmsSdkAppId(mTencentSMSConfig.appId); /* 短信签名内容: 使用 UTF-8 编码,必须填写已审核通过的签名,签名信息可登录 [短信控制台] 查看 */ req.setSignName(mTencentSMSConfig.sign); /* 国际/港澳台短信 SenderId: 国内短信填空,默认未开通,如需开通请联系 [sms helper] */ String senderid = ""; req.setSenderId(senderid); /* 用户的 session 内容: 可以携带用户侧 ID 等上下文信息,server 会原样返回 */ String sessionContext = "xxx"; req.setSessionContext(sessionContext); /* 短信号码扩展号: 默认未开通,如需开通请联系 [sms helper] */ String extendCode = ""; req.setExtendCode(extendCode); /* 模板 ID: 必须填写已审核通过的模板 ID。模板ID可登录 [短信控制台] 查看 */ req.setTemplateId(mTencentSMSConfig.templateId); /* 下发手机号码,采用 E.164 标准,+[国家或地区码][手机号] * 示例如:+8613711112222, 其中前面有一个+号 ,86为国家码,13711112222为手机号,最多不要超过200个手机号 */ String[] phoneNumberSet = {mobile}; req.setPhoneNumberSet(phoneNumberSet); /* 模板参数: 若无模板参数,则设置为空 */ String[] templateParamSet = {code}; req.setTemplateParamSet(templateParamSet); /* 通过 client 对象调用 SendSms 方法发起请求。注意请求方法名与请求对象是对应的 * 返回的 res 是一个 SendSmsResponse 类的实例,与请求对象对应 */ SendSmsResponse res = client.SendSms(req); // 输出json格式的字符串回包 System.out.println(SendSmsResponse.toJsonString(res)); // 也可以取出单个值,你可以通过官网接口文档或跳转到response对象的定义处查看返回字段的定义 System.out.println(res.getRequestId()); return RestResult.RestCode.SUCCESS; } catch (TencentCloudSDKException e) { e.printStackTrace(); } return RestResult.RestCode.ERROR_SERVER_ERROR; } private RestResult.RestCode sendAliyunCode(String mobile, String code) { DefaultProfile profile = DefaultProfile.getProfile("default", aliyunSMSConfig.getAccessKeyId(), aliyunSMSConfig.getAccessSecret()); IAcsClient client = new DefaultAcsClient(profile); String templateparam = "{\"code\":\"" + code + "\"}"; CommonRequest request = new CommonRequest(); request.setMethod(MethodType.POST); request.setDomain("dysmsapi.aliyuncs.com"); request.setVersion("2017-05-25"); request.setAction("SendSms"); request.putQueryParameter("PhoneNumbers", mobile); request.putQueryParameter("SignName", aliyunSMSConfig.getSignName()); request.putQueryParameter("TemplateCode", aliyunSMSConfig.getTemplateCode()); request.putQueryParameter("TemplateParam", templateparam); try { CommonResponse response = client.getCommonResponse(request); System.out.println(response.getData()); if (response.getData() != null) { AliyunCommonResponse aliyunCommonResponse = new Gson().fromJson(response.getData(), AliyunCommonResponse.class); if (aliyunCommonResponse != null) { if (aliyunCommonResponse.Code.equalsIgnoreCase("OK")) { return RestResult.RestCode.SUCCESS; } else { System.out.println("Send aliyun sms failure with message:" + aliyunCommonResponse.Message); } } } } catch (ServerException e) { e.printStackTrace(); } catch (ClientException e) { e.printStackTrace(); } return RestResult.RestCode.ERROR_SERVER_ERROR; } } ================================================ FILE: src/main/java/cn/wildfirechat/app/sms/TencentSMSConfig.java ================================================ package cn.wildfirechat.app.sms; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; @Configuration @ConfigurationProperties(prefix="sms") @PropertySource(value = "file:config/tencent_sms.properties", encoding = "UTF-8") public class TencentSMSConfig { public String secretId; public String secretKey; public String appId; public String templateId; public String sign; public String getSecretId() { return secretId; } public void setSecretId(String secretId) { this.secretId = secretId; } public String getSecretKey() { return secretKey; } public void setSecretKey(String secretKey) { this.secretKey = secretKey; } public String getAppId() { return appId; } public void setAppId(String appId) { this.appId = appId; } public String getTemplateId() { return templateId; } public void setTemplateId(String templateId) { this.templateId = templateId; } public String getSign() { return sign; } public void setSign(String sign) { this.sign = sign; } } ================================================ FILE: src/main/java/cn/wildfirechat/app/tools/LdapUser.java ================================================ package cn.wildfirechat.app.tools; public class LdapUser { public final String uid, cn, mail, phone, dn; public LdapUser(String uid, String cn, String mail, String phone, String dn) { this.uid = uid; this.cn = cn; this.mail = mail; this.phone = phone; this.dn = dn; } @Override public String toString() { return String.format("User{uid='%s', cn='%s', mail='%s', phone='%s', dn='%s'}", uid, cn, mail, phone, dn); } } ================================================ FILE: src/main/java/cn/wildfirechat/app/tools/LdapUtil.java ================================================ package cn.wildfirechat.app.tools; import javax.naming.Context; import javax.naming.NamingEnumeration; import javax.naming.NamingException; import javax.naming.directory.*; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Base64; import java.util.Hashtable; import java.util.List; public class LdapUtil { /** 根据电话号码反向查人 */ public static List findUserByPhone(String phone, String ldapUrl, String searchBase, String adminDn, String adminPwd) throws NamingException { Hashtable env = new Hashtable<>(); env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory"); env.put(Context.PROVIDER_URL, ldapUrl); env.put(Context.SECURITY_AUTHENTICATION, "simple"); env.put(Context.SECURITY_PRINCIPAL, adminDn); env.put(Context.SECURITY_CREDENTIALS, adminPwd); DirContext ctx = new InitialDirContext(env); try { /* 搜索过滤器:电话号码完全匹配 */ String filter = "(&(objectClass=inetOrgPerson)(telephoneNumber={0}))"; Object[] params = { phone }; SearchControls ctls = new SearchControls(); ctls.setSearchScope(SearchControls.SUBTREE_SCOPE); /* 只取我们关心的属性 */ ctls.setReturningAttributes(new String[] { "uid", "cn", "mail", "telephoneNumber" }); NamingEnumeration rs = ctx.search(searchBase, filter, params, ctls); List ldapUsers = new ArrayList<>(); while (rs.hasMore()) { SearchResult sr = rs.next(); Attributes attrs = sr.getAttributes(); String dn = sr.getNameInNamespace(); ldapUsers.add(new LdapUser( getAttr(attrs, "uid"), getAttr(attrs, "cn"), getAttr(attrs, "mail"), getAttr(attrs, "telephoneNumber"), dn)); } return ldapUsers; } finally { ctx.close(); } } /** * OpenLDAP 密码加密:SSHA(Salted SHA-1,带随机盐值,更安全) * @param password 明文密码 * @return Base64 编码后的 SSHA 密码(LDAP 存储格式) */ private static String encodeSshaPassword(String password) { try { // 1. 生成 8 字节随机盐值(增加破解难度) byte[] salt = new byte[8]; new java.security.SecureRandom().nextBytes(salt); // 2. 密码字节 + 盐值字节 byte[] passwordBytes = password.getBytes(StandardCharsets.UTF_8); byte[] passwordWithSalt = new byte[passwordBytes.length + salt.length]; System.arraycopy(passwordBytes, 0, passwordWithSalt, 0, passwordBytes.length); System.arraycopy(salt, 0, passwordWithSalt, passwordBytes.length, salt.length); // 3. SHA-1 哈希(可替换为 SHA-256,需 LDAP 服务器支持) byte[] shaHash = java.security.MessageDigest.getInstance("SHA-1").digest(passwordWithSalt); // 4. SSHA 格式:{SSHA} + Base64(哈希值 + 盐值) byte[] sshaBytes = new byte[shaHash.length + salt.length]; System.arraycopy(shaHash, 0, sshaBytes, 0, shaHash.length); System.arraycopy(salt, 0, sshaBytes, shaHash.length, salt.length); return "{SSHA}" + Base64.getEncoder().encodeToString(sshaBytes); } catch (Exception e) { throw new RuntimeException("SSHA 密码加密失败", e); } } private static String getAttr(Attributes attrs, String name) throws NamingException { Attribute attr = attrs.get(name); return attr == null ? null : (String) attr.get(); } } ================================================ FILE: src/main/java/cn/wildfirechat/app/tools/NumericIdGenerator.java ================================================ package cn.wildfirechat.app.tools; import java.util.ArrayList; import java.util.Arrays; import java.util.List; public class NumericIdGenerator { public static String getId(List firstNumber, List firstExceptNumber, int idLength) { List numbers = new ArrayList<>(); if(firstNumber != null && !firstNumber.isEmpty()) { numbers.addAll(firstNumber); } else { for (int i = 0; i <= 9; i++) { numbers.add(i); } } if(firstExceptNumber != null && !firstExceptNumber.isEmpty()) { numbers.removeAll(firstExceptNumber); } numbers.remove((Integer)4); StringBuilder sb = new StringBuilder(); for (int i = 0; i < idLength; i++) { if(i == 0 && !numbers.isEmpty()) { sb.append(numbers.get((int)(Math.random() * numbers.size()))); } else { int n; do { n = (int)(Math.random()*10); } while (n == 4); sb.append(n); } } return sb.toString(); } public static void main(String[] args) { for (int i = 0; i < 100; i++) { String id = getId(null, Arrays.asList(0), 6); System.out.println(id); } } } ================================================ FILE: src/main/java/cn/wildfirechat/app/tools/OrderedIdUserNameGenerator.java ================================================ package cn.wildfirechat.app.tools; import cn.wildfirechat.app.jpa.UserNameEntry; import cn.wildfirechat.app.jpa.UserNameRepository; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; @Component public class OrderedIdUserNameGenerator implements UserNameGenerator { @Autowired private UserNameRepository userIdRepository; @Override public String getUserName(String phone) { UserNameEntry entry = new UserNameEntry(); userIdRepository.save(entry); return entry.getId() + ""; } } ================================================ FILE: src/main/java/cn/wildfirechat/app/tools/PhoneNumberUserNameGenerator.java ================================================ package cn.wildfirechat.app.tools; import org.springframework.stereotype.Component; @Component public class PhoneNumberUserNameGenerator implements UserNameGenerator { @Override public String getUserName(String phone) { return phone; } } ================================================ FILE: src/main/java/cn/wildfirechat/app/tools/RateLimiter.java ================================================ package cn.wildfirechat.app.tools; import java.util.HashMap; import java.util.Iterator; import java.util.Map; /** * 漏桶算法 * capacity * 1000是为了更精确, 漏水的小洞更小^~^ */ public class RateLimiter { private static final int DEFAULT_LIMIT_TIME_SECOND = 5; private static final int DEFAULT_LIMIT_COUNT = 100; private static final long expire = 2 * 60 * 60 * 1000; private double rate = (double) DEFAULT_LIMIT_COUNT / (DEFAULT_LIMIT_TIME_SECOND); private long capacity = DEFAULT_LIMIT_COUNT * 1000; private long lastCleanTime; private Map requestCountMap = new HashMap<>(); private Map requestTimeMap = new HashMap<>(); private SpinLock lock = new SpinLock(); public RateLimiter() { } public RateLimiter(int limitTimeSecond, int limitCount) { if (limitTimeSecond <= 0 || limitCount <= 0) { throw new IllegalArgumentException(); } this.capacity = limitCount * 1000; this.rate = (double) limitCount / limitTimeSecond; } /** * 漏桶算法,https://en.wikipedia.org/wiki/Leaky_bucket */ public boolean isGranted(String userId) { try { lock.lock(); long current = System.currentTimeMillis(); cleanUp(current); Long lastRequestTime = requestTimeMap.get(userId); long count = 0; if (lastRequestTime == null) { count += 1000; requestTimeMap.put(userId, current); requestCountMap.put(userId, count); return true; } else { count = requestCountMap.get(userId); lastRequestTime = requestTimeMap.get(userId); count -= (current - lastRequestTime) * rate; count = count > 0 ? count : 0; requestTimeMap.put(userId, current); if (count < capacity) { count += 1000; requestCountMap.put(userId, count); return true; } else { requestCountMap.put(userId, count); return false; } } } finally { lock.unLock(); } } private void cleanUp(long current) { if (current - lastCleanTime > expire) { for (Iterator> it = requestTimeMap.entrySet().iterator(); it.hasNext();) { Map.Entry entry = it.next(); if (entry.getValue() < current - expire) { it.remove(); requestCountMap.remove(entry.getKey()); } } lastCleanTime = current; } } public static void main(String[] args) throws InterruptedException { RateLimiter limiter = new RateLimiter(1, 10); long start = System.currentTimeMillis(); for (int i = 0; i < 53; i++) { if (!limiter.isGranted("test")) { System.out.println("1 too frequency " + i); } } Thread.sleep(1 * 1000); System.out.println("sleep 1 s"); for (int i = 0; i < 53; i++) { if (!limiter.isGranted("test")) { System.out.println("2 too frequency " + i); } } Thread.sleep(5 * 1000); System.out.println("sleep 5 s"); for (int i = 0; i < 53; i++) { if (!limiter.isGranted("test")) { System.out.println("3 too frequency " + i); } } Thread.sleep(5 * 1000); System.out.println("sleep 5 s"); long second = System.currentTimeMillis(); for (int i = 0; i < 100; i++) { if (!limiter.isGranted("test")) { System.out.println("4 too frequency " + i); } Thread.sleep(50); } System.out.println("second: " + (System.currentTimeMillis() - second)); System.out.println("end: " + (System.currentTimeMillis() - start)); } } ================================================ FILE: src/main/java/cn/wildfirechat/app/tools/ShortUUIDGenerator.java ================================================ package cn.wildfirechat.app.tools; import org.springframework.stereotype.Component; import java.util.HashSet; import java.util.Set; import java.util.UUID; @Component public class ShortUUIDGenerator implements UserNameGenerator { public static String[] chars = new String[] { "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z" }; @Override public String getUserName(String phone) { return getShortUUID(); } public String getShortUUID() { StringBuffer shortBuffer = new StringBuffer(); String uuid = UUID.randomUUID().toString().replace("-", ""); for (int i = 0; i < 8; i++) { String str = uuid.substring(i * 4, i * 4 + 4); int x = Integer.parseInt(str, 16); shortBuffer.append(chars[x % chars.length]); } return shortBuffer.toString(); } public static void main(String[] args) { Set idSet = new HashSet<>(); ShortUUIDGenerator generator = new ShortUUIDGenerator(); int duplatedCount = 0; for (int i = 0; i < 1000000; i++) { String id = generator.getUserName(null); if(!idSet.add(id)) { System.out.println("Duplated id of " + id); duplatedCount++; } } System.out.println("Duplated id count is " + duplatedCount); } } ================================================ FILE: src/main/java/cn/wildfirechat/app/tools/SpinLock.java ================================================ package cn.wildfirechat.app.tools; import java.util.concurrent.atomic.AtomicReference; public class SpinLock { //java中原子(CAS)操作 AtomicReference owner = new AtomicReference<>();//持有自旋锁的线程对象 private int count; public void lock() { Thread cur = Thread.currentThread(); //lock函数将owner设置为当前线程,并且预测原来的值为空。unlock函数将owner设置为null,并且预测值为当前线程。当有第二个线程调用lock操作时由于owner值不为空,导致循环 //一直被执行,直至第一个线程调用unlock函数将owner设置为null,第二个线程才能进入临界区。 while (!owner.compareAndSet(null, cur)){ } } public void unLock() { Thread cur = Thread.currentThread(); owner.compareAndSet(cur, null); } } ================================================ FILE: src/main/java/cn/wildfirechat/app/tools/UUIDUserNameGenerator.java ================================================ package cn.wildfirechat.app.tools; import org.springframework.stereotype.Component; import java.util.UUID; @Component public class UUIDUserNameGenerator implements UserNameGenerator { @Override public String getUserName(String phone) { return "wfid-" + UUID.randomUUID().toString().replaceAll("-", ""); } } ================================================ FILE: src/main/java/cn/wildfirechat/app/tools/UserNameGenerator.java ================================================ package cn.wildfirechat.app.tools; public interface UserNameGenerator { String getUserName(String phone); } ================================================ FILE: src/main/java/cn/wildfirechat/app/tools/Utils.java ================================================ package cn.wildfirechat.app.tools; import java.nio.file.Paths; import java.util.Random; import java.util.UUID; import java.util.regex.Matcher; import java.util.regex.Pattern; public class Utils { public static String getRandomCode(int length) { StringBuilder sb = new StringBuilder(); for (int i = 0; i < length; i++) { sb.append(((int)(Math.random()*100))%10); } return sb.toString(); } public static boolean isMobile(String mobile) { boolean flag = false; try { Pattern p = Pattern.compile("^(1[3-9][0-9])\\d{8}$"); Matcher m = p.matcher(mobile); flag = m.matches(); } catch (Exception e) { flag = false; } return flag; } public static String getSafeFileName(String fileName) { if (fileName == null || fileName.isEmpty()) { return UUID.randomUUID().toString(); } // 使用 Paths.get 解析文件名 try { String newName = Paths.get(fileName).getFileName().toString(); if(!newName.isEmpty()) { return newName; } } catch (Exception e) { // 处理解析异常 e.printStackTrace(); } return UUID.randomUUID().toString(); } public static void main(String[] args) { String filename1 = "/aa../../../hello.txt"; String filename2 = "..\\..\\1.txt"; System.out.println(getSafeFileName(filename1)); System.out.println(getSafeFileName(filename2)); } } ================================================ FILE: src/main/resources/application.properties ================================================ ================================================ FILE: src/test/java/cn/wildfirechat/app/ApplicationTests.java ================================================ package cn.wildfirechat.app; import com.zaxxer.hikari.HikariDataSource; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.TestConfiguration; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Primary; import org.springframework.test.context.junit4.SpringRunner; import javax.sql.DataSource; import static org.junit.Assert.assertNotNull; /** * Application Tests * * 数据库配置说明: * - 默认使用 H2 内存数据库(无需外部数据库即可运行测试) * - 如需测试 MySQL,启用 MySqlTestConfig * - 如需测试达梦数据库,启用 DamengTestConfig * * 启用方式:将对应配置类的 @Primary 注解取消注释即可 */ @RunWith(SpringRunner.class) @SpringBootTest public class ApplicationTests { @Test public void contextLoads() { assertNotNull("Application should load", true); } // ==================== H2 内存数据库配置(默认) ==================== @TestConfiguration static class H2TestConfig { @Bean @Primary public DataSource dataSource() { HikariDataSource dataSource = new HikariDataSource(); dataSource.setDriverClassName("org.h2.Driver"); dataSource.setJdbcUrl("jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;MODE=MySQL"); dataSource.setUsername("sa"); dataSource.setPassword(""); dataSource.setMinimumIdle(1); dataSource.setMaximumPoolSize(5); return dataSource; } } // ==================== MySQL 数据库配置示例 ==================== // 使用方法:取消下方 @Primary 注解的注释,并将 H2TestConfig 的 @Primary 注释掉 /* @TestConfiguration static class MySqlTestConfig { @Bean @Primary public DataSource dataSource() { HikariDataSource dataSource = new HikariDataSource(); dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver"); dataSource.setJdbcUrl("jdbc:mysql://localhost:3306/appdata?serverTimezone=UTC&useSSL=false"); dataSource.setUsername("root"); dataSource.setPassword("123456"); dataSource.setMinimumIdle(1); dataSource.setMaximumPoolSize(10); return dataSource; } } */ // ==================== 达梦数据库配置示例 ==================== // 使用方法:取消下方 @Primary 注解的注释,并将 H2TestConfig 的 @Primary 注释掉 /* @TestConfiguration static class DamengTestConfig { @Bean @Primary public DataSource dataSource() { HikariDataSource dataSource = new HikariDataSource(); dataSource.setDriverClassName("dm.jdbc.driver.DmDriver"); dataSource.setJdbcUrl("jdbc:dm://192.168.1.6:5237"); dataSource.setUsername("SYSDBA"); dataSource.setPassword("Wfc123!@"); dataSource.setMinimumIdle(1); dataSource.setMaximumPoolSize(5); return dataSource; } } */ } ================================================ FILE: src/test/java/cn/wildfirechat/app/jpa/AnnouncementTest.java ================================================ package cn.wildfirechat.app.jpa; import org.junit.Test; import static org.junit.Assert.*; public class AnnouncementTest { @Test public void testConstructorAndGetters() { // Given String groupId = "group123"; String author = "user456"; String content = "Test announcement content"; long timestamp = System.currentTimeMillis(); // When Announcement announcement = new Announcement(); announcement.setGroupId(groupId); announcement.setAuthor(author); announcement.setAnnouncement(content); announcement.setTimestamp(timestamp); // Then assertEquals(groupId, announcement.getGroupId()); assertEquals(author, announcement.getAuthor()); assertEquals(content, announcement.getAnnouncement()); assertEquals(timestamp, announcement.getTimestamp()); } @Test public void testEmptyAnnouncement() { // When Announcement announcement = new Announcement(); // Then assertNull(announcement.getGroupId()); assertNull(announcement.getAuthor()); assertNull(announcement.getAnnouncement()); assertEquals(0L, announcement.getTimestamp()); } @Test public void testLongContent() { // Given StringBuilder longContent = new StringBuilder(); for (int i = 0; i < 100; i++) { longContent.append("This is a long announcement content. "); } // When Announcement announcement = new Announcement(); announcement.setAnnouncement(longContent.toString()); // Then assertEquals(longContent.toString(), announcement.getAnnouncement()); } } ================================================ FILE: src/test/java/cn/wildfirechat/app/jpa/ConferenceEntityTest.java ================================================ package cn.wildfirechat.app.jpa; import org.junit.Test; import static org.junit.Assert.*; public class ConferenceEntityTest { @Test public void testDefaultValues() { // When ConferenceEntity conference = new ConferenceEntity(); // Then assertNull(conference.getId()); assertNull(conference.getConferenceTitle()); assertNull(conference.getPassword()); assertNull(conference.getOwner()); assertEquals(0, conference.getMaxParticipants()); } @Test public void testSettersAndGetters() { // Given ConferenceEntity conference = new ConferenceEntity(); long now = System.currentTimeMillis(); // When conference.setId("conf123"); conference.setConferenceTitle("Test Conference"); conference.setPassword("123456"); conference.setPin("7890"); conference.setOwner("user456"); conference.setManages("admin1,admin2"); conference.setStartTime(now); conference.setEndTime(now + 3600000); conference.setAudience(true); conference.setAdvance(false); conference.setAllowSwitchMode(true); conference.setNoJoinBeforeStart(false); conference.setRecording(true); conference.setFocus("speaker1"); conference.setMaxParticipants(100); // Then assertEquals("conf123", conference.getId()); assertEquals("Test Conference", conference.getConferenceTitle()); assertEquals("123456", conference.getPassword()); assertEquals("7890", conference.getPin()); assertEquals("user456", conference.getOwner()); assertEquals("admin1,admin2", conference.getManages()); assertEquals(now, conference.getStartTime()); assertEquals(now + 3600000, conference.getEndTime()); assertTrue(conference.isAudience()); assertFalse(conference.isAdvance()); assertTrue(conference.isAllowSwitchMode()); assertFalse(conference.isNoJoinBeforeStart()); assertTrue(conference.isRecording()); assertEquals("speaker1", conference.getFocus()); assertEquals(100, conference.getMaxParticipants()); } @Test public void testBooleanFlags() { // Given ConferenceEntity conference = new ConferenceEntity(); // Test all combinations conference.setAudience(true); assertTrue(conference.isAudience()); conference.setAdvance(true); assertTrue(conference.isAdvance()); conference.setAllowSwitchMode(true); assertTrue(conference.isAllowSwitchMode()); conference.setNoJoinBeforeStart(true); assertTrue(conference.isNoJoinBeforeStart()); conference.setRecording(true); assertTrue(conference.isRecording()); // Set to false conference.setAudience(false); assertFalse(conference.isAudience()); } @Test public void testMaxParticipantsBoundary() { // Given ConferenceEntity conference = new ConferenceEntity(); // When - test boundary values conference.setMaxParticipants(0); assertEquals(0, conference.getMaxParticipants()); conference.setMaxParticipants(1); assertEquals(1, conference.getMaxParticipants()); conference.setMaxParticipants(Integer.MAX_VALUE); assertEquals(Integer.MAX_VALUE, conference.getMaxParticipants()); } } ================================================ FILE: src/test/java/cn/wildfirechat/app/jpa/FavoriteItemTest.java ================================================ package cn.wildfirechat.app.jpa; import org.junit.Test; import static org.junit.Assert.*; public class FavoriteItemTest { @Test public void testDefaultConstructor() { // When FavoriteItem item = new FavoriteItem(); // Then assertNull(item.id); assertNull(item.messageUid); assertNull(item.userId); assertEquals(0, item.type); assertEquals(0, item.timestamp); assertNull(item.title); assertNull(item.url); } @Test public void testSettersAndGetters() { // Given FavoriteItem item = new FavoriteItem(); // When item.id = 1L; item.messageUid = 12345L; item.userId = "user123"; item.type = 1; item.timestamp = System.currentTimeMillis(); item.convType = 0; item.convLine = 0; item.convTarget = "target456"; item.origin = "original data"; item.sender = "sender789"; item.title = "Favorite Title"; item.url = "https://example.com/image.png"; item.thumbUrl = "https://example.com/thumb.png"; item.data = "{\"key\":\"value\"}"; // Then assertEquals(Long.valueOf(1L), item.id); assertEquals(Long.valueOf(12345L), item.messageUid); assertEquals("user123", item.userId); assertEquals(1, item.type); assertTrue(item.timestamp > 0); assertEquals(0, item.convType); assertEquals("target456", item.convTarget); assertEquals("original data", item.origin); assertEquals("sender789", item.sender); assertEquals("Favorite Title", item.title); assertEquals("https://example.com/image.png", item.url); assertEquals("https://example.com/thumb.png", item.thumbUrl); assertEquals("{\"key\":\"value\"}", item.data); } @Test public void testDifferentTypes() { // Given FavoriteItem item = new FavoriteItem(); // When - test different favorite types for (int type = 0; type < 10; type++) { item.type = type; // Then assertEquals(type, item.type); } } @Test public void testLongUrls() { // Given FavoriteItem item = new FavoriteItem(); String longUrl = "https://example.com/very/long/path/to/image.png?param1=value1¶m2=value2"; // When item.url = longUrl; // Then assertEquals(longUrl, item.url); } } ================================================ FILE: src/test/java/cn/wildfirechat/app/jpa/PCSessionTest.java ================================================ package cn.wildfirechat.app.jpa; import org.junit.Test; import static org.junit.Assert.*; public class PCSessionTest { @Test public void testDefaultConstructor() { // When PCSession session = new PCSession(); // Then assertNull(session.getToken()); assertNull(session.getClientId()); assertEquals(0, session.getStatus()); assertNull(session.getConfirmedUserId()); assertEquals(0, session.getPlatform()); } @Test public void testSettersAndGetters() { // Given PCSession session = new PCSession(); long currentTime = System.currentTimeMillis(); // When session.setToken("test-token-123"); session.setClientId("client-456"); session.setCreateDt(currentTime); session.setDuration(300000); session.setStatus(PCSession.PCSessionStatus.Session_Scanned); session.setConfirmedUserId("user-789"); session.setDevice_name("Windows PC"); session.setPlatform(3); // Then assertEquals("test-token-123", session.getToken()); assertEquals("client-456", session.getClientId()); assertEquals(currentTime, session.getCreateDt()); assertEquals(300000, session.getDuration()); assertEquals(PCSession.PCSessionStatus.Session_Scanned, session.getStatus()); assertEquals("user-789", session.getConfirmedUserId()); assertEquals("Windows PC", session.getDevice_name()); assertEquals(3, session.getPlatform()); } @Test public void testSessionStatusConstants() { assertEquals(0, PCSession.PCSessionStatus.Session_Created); assertEquals(1, PCSession.PCSessionStatus.Session_Scanned); assertEquals(2, PCSession.PCSessionStatus.Session_Verified); assertEquals(3, PCSession.PCSessionStatus.Session_Pre_Verify); assertEquals(4, PCSession.PCSessionStatus.Session_Canceled); } @Test public void testStatusTransitions() { // Given PCSession session = new PCSession(); // When - simulate status transitions session.setStatus(PCSession.PCSessionStatus.Session_Created); assertEquals(PCSession.PCSessionStatus.Session_Created, session.getStatus()); session.setStatus(PCSession.PCSessionStatus.Session_Scanned); assertEquals(PCSession.PCSessionStatus.Session_Scanned, session.getStatus()); session.setStatus(PCSession.PCSessionStatus.Session_Pre_Verify); assertEquals(PCSession.PCSessionStatus.Session_Pre_Verify, session.getStatus()); session.setStatus(PCSession.PCSessionStatus.Session_Verified); assertEquals(PCSession.PCSessionStatus.Session_Verified, session.getStatus()); session.setStatus(PCSession.PCSessionStatus.Session_Canceled); assertEquals(PCSession.PCSessionStatus.Session_Canceled, session.getStatus()); } @Test public void testPlatformValues() { // Given PCSession session = new PCSession(); // When - test different platform values int[] platforms = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}; for (int platform : platforms) { session.setPlatform(platform); assertEquals(platform, session.getPlatform()); } } } ================================================ FILE: src/test/java/cn/wildfirechat/app/jpa/RecordTest.java ================================================ package cn.wildfirechat.app.jpa; import org.junit.Test; import static org.junit.Assert.*; public class RecordTest { @Test public void testDefaultConstructor() { // When Record record = new Record(); // Then assertNull(record.getMobile()); assertNull(record.getCode()); assertEquals(0L, record.getTimestamp()); } @Test public void testConstructorWithParams() { // Given String code = "123456"; String mobile = "13800138000"; // When Record record = new Record(code, mobile); // Then assertEquals(code, record.getCode()); assertEquals(mobile, record.getMobile()); assertTrue(record.getStartTime() > 0); assertEquals(0, record.getRequestCount()); } @Test public void testIncreaseAndCheckWithinLimit() { // Given Record record = new Record("123456", "13800138000"); // When - within limit for (int i = 0; i < 5; i++) { boolean result = record.increaseAndCheck(); // Then assertTrue(result); } assertEquals(5, record.getRequestCount()); } @Test public void testIncreaseAndCheckExceedsLimit() { // Given Record record = new Record("123456", "13800138000"); // When - exceed limit for (int i = 0; i < 10; i++) { record.increaseAndCheck(); } // Then - should return false after 10 requests boolean result = record.increaseAndCheck(); assertFalse(result); } @Test public void testReset() { // Given Record record = new Record("123456", "13800138000"); for (int i = 0; i < 5; i++) { record.increaseAndCheck(); } assertEquals(5, record.getRequestCount()); // When record.reset(); // Then assertEquals(1, record.getRequestCount()); assertTrue(record.getStartTime() > 0); } @Test public void testSettersAndGetters() { // Given Record record = new Record(); // When record.setCode("654321"); record.setTimestamp(System.currentTimeMillis()); record.setStartTime(System.currentTimeMillis()); // Then assertEquals("654321", record.getCode()); assertTrue(record.getTimestamp() > 0); assertTrue(record.getStartTime() > 0); } @Test public void testResetAfter24Hours() { // Given Record record = new Record("123456", "13800138000"); record.setStartTime(System.currentTimeMillis() - 86400001L); // 24 hours + 1ms ago // When boolean result = record.increaseAndCheck(); // Then - should reset and allow (count becomes 1 after increment) assertTrue(result); assertTrue(record.getRequestCount() >= 1); // Could be 1 or 2 depending on internal logic } } ================================================ FILE: src/test/java/cn/wildfirechat/app/jpa/ShiroSessionTest.java ================================================ package cn.wildfirechat.app.jpa; import org.junit.Test; import java.util.Arrays; import static org.junit.Assert.*; public class ShiroSessionTest { @Test public void testDefaultConstructor() { // When ShiroSession session = new ShiroSession(); // Then assertNull(session.getSessionId()); assertNull(session.getSessionData()); } @Test public void testConstructorWithParams() { // Given String sessionId = "session-123"; byte[] sessionData = "test session data".getBytes(); // When ShiroSession session = new ShiroSession(sessionId, sessionData); // Then assertEquals(sessionId, session.getSessionId()); assertArrayEquals(sessionData, session.getSessionData()); } @Test public void testSetSessionData() { // Given ShiroSession session = new ShiroSession(); byte[] newData = "new session data".getBytes(); // When session.setSessionId("new-session-id"); session.setSessionData(newData); // Then assertEquals("new-session-id", session.getSessionId()); assertArrayEquals(newData, session.getSessionData()); } @Test public void testEmptySessionData() { // Given ShiroSession session = new ShiroSession("empty-session", new byte[0]); // Then assertNotNull(session.getSessionData()); assertEquals(0, session.getSessionData().length); } @Test public void testBinarySessionData() { // Given byte[] binaryData = new byte[]{0x00, 0x01, 0x02, (byte) 0xFF, (byte) 0xFE}; ShiroSession session = new ShiroSession("binary-session", binaryData); // When byte[] retrievedData = session.getSessionData(); // Then assertArrayEquals(binaryData, retrievedData); assertEquals(5, retrievedData.length); } @Test public void testLargeSessionData() { // Given byte[] largeData = new byte[2048]; Arrays.fill(largeData, (byte) 'A'); // When ShiroSession session = new ShiroSession("large-session", largeData); // Then assertArrayEquals(largeData, session.getSessionData()); assertEquals(2048, session.getSessionData().length); } } ================================================ FILE: src/test/java/cn/wildfirechat/app/jpa/SlideVerifyRepositoryTest.java ================================================ package cn.wildfirechat.app.jpa; import cn.wildfirechat.app.slide.SlideVerifyCleanupService; import cn.wildfirechat.app.slide.SlideVerifyService; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; import java.util.Optional; import static org.junit.Assert.*; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.*; /** * Unit tests for SlideVerifyRepository * Uses mocks to avoid Spring context loading issues */ @RunWith(MockitoJUnitRunner.class) public class SlideVerifyRepositoryTest { @Mock private SlideVerifyRepository repository; private SlideVerifyCleanupService cleanupService; @Before public void setUp() { cleanupService = new SlideVerifyCleanupService(); try { java.lang.reflect.Field field = SlideVerifyCleanupService.class.getDeclaredField("slideVerifyRepository"); field.setAccessible(true); field.set(cleanupService, repository); } catch (Exception e) { fail("Failed to inject mock repository: " + e.getMessage()); } } @Test public void testFindByTokenExists() { // Given String token = "test-token-123"; SlideVerify expected = new SlideVerify(token, 150, System.currentTimeMillis()); when(repository.findByToken(token)).thenReturn(Optional.of(expected)); // When Optional result = repository.findByToken(token); // Then assertTrue(result.isPresent()); assertEquals(token, result.get().getToken()); assertEquals(150, result.get().getX()); } @Test public void testFindByTokenNotExists() { // Given String token = "non-existent-token"; when(repository.findByToken(token)).thenReturn(Optional.empty()); // When Optional result = repository.findByToken(token); // Then assertFalse(result.isPresent()); } @Test public void testSave() { // Given SlideVerify slideVerify = new SlideVerify("save-token", 100, System.currentTimeMillis()); when(repository.save(any(SlideVerify.class))).thenReturn(slideVerify); // When SlideVerify result = repository.save(slideVerify); // Then assertNotNull(result); assertEquals("save-token", result.getToken()); } @Test public void testDelete() { // Given SlideVerify slideVerify = new SlideVerify("delete-token", 100, System.currentTimeMillis()); // When repository.delete(slideVerify); // Then - no exception means success verify(repository, times(1)).delete(slideVerify); } @Test public void testCleanupExpired() { // Given int deletedCount = 5; when(repository.deleteExpired(anyLong())).thenReturn(deletedCount); // When cleanupService.cleanupExpired(); // Then verify(repository, times(1)).deleteExpired(anyLong()); } @Test public void testUpdateVerified() { // Given String token = "update-token"; SlideVerify slideVerify = new SlideVerify(token, 100, System.currentTimeMillis()); slideVerify.setVerified(true); when(repository.save(any(SlideVerify.class))).thenReturn(slideVerify); // When SlideVerify result = repository.save(slideVerify); // Then assertTrue(result.isVerified()); } } ================================================ FILE: src/test/java/cn/wildfirechat/app/jpa/SlideVerifyTest.java ================================================ package cn.wildfirechat.app.jpa; import org.junit.Test; import static org.junit.Assert.*; public class SlideVerifyTest { @Test public void testConstructor() { // Given String token = "test-token"; int x = 150; long timestamp = System.currentTimeMillis(); // When SlideVerify slideVerify = new SlideVerify(token, x, timestamp); // Then assertEquals(token, slideVerify.getToken()); assertEquals(x, slideVerify.getX()); assertEquals(timestamp, slideVerify.getTimestamp()); assertFalse(slideVerify.isVerified()); } @Test public void testSettersAndGetters() { // Given SlideVerify slideVerify = new SlideVerify(); String token = "new-token"; int x = 200; long timestamp = 1234567890L; boolean verified = true; // When slideVerify.setToken(token); slideVerify.setX(x); slideVerify.setTimestamp(timestamp); slideVerify.setVerified(verified); // Then assertEquals(token, slideVerify.getToken()); assertEquals(x, slideVerify.getX()); assertEquals(timestamp, slideVerify.getTimestamp()); assertTrue(slideVerify.isVerified()); } @Test public void testIsExpiredNotExpired() { // Given SlideVerify slideVerify = new SlideVerify("token", 100, System.currentTimeMillis()); int timeoutSeconds = 300; // When boolean expired = slideVerify.isExpired(timeoutSeconds); // Then assertFalse(expired); } @Test public void testIsExpiredJustExpired() { // Given SlideVerify slideVerify = new SlideVerify("token", 100, System.currentTimeMillis() - 300001); // 300秒零1毫秒前 int timeoutSeconds = 300; // When boolean expired = slideVerify.isExpired(timeoutSeconds); // Then assertTrue(expired); } @Test public void testIsExpiredLongAgo() { // Given SlideVerify slideVerify = new SlideVerify("token", 100, System.currentTimeMillis() - 600000); // 10分钟前 int timeoutSeconds = 300; // When boolean expired = slideVerify.isExpired(timeoutSeconds); // Then assertTrue(expired); } @Test public void testDefaultConstructor() { // When SlideVerify slideVerify = new SlideVerify(); // Then assertNull(slideVerify.getToken()); assertEquals(0, slideVerify.getX()); assertEquals(0L, slideVerify.getTimestamp()); assertFalse(slideVerify.isVerified()); } @Test public void testVerifiedStateToggle() { // Given SlideVerify slideVerify = new SlideVerify("token", 100, System.currentTimeMillis()); // Initially not verified assertFalse(slideVerify.isVerified()); // When - set to verified slideVerify.setVerified(true); // Then assertTrue(slideVerify.isVerified()); // When - toggle back slideVerify.setVerified(false); // Then assertFalse(slideVerify.isVerified()); } @Test public void testBoundaryConditions() { // Test with x at boundary values SlideVerify slideVerify = new SlideVerify("token", 0, System.currentTimeMillis()); assertEquals(0, slideVerify.getX()); slideVerify.setX(1000); assertEquals(1000, slideVerify.getX()); } @Test public void testTimestampBoundary() { // Test with timestamp at boundary long currentTime = System.currentTimeMillis(); SlideVerify slideVerify = new SlideVerify("token", 100, currentTime); // Not expired at exactly timeout assertFalse(slideVerify.isExpired(0)); // Immediate timeout // Expired with negative timeout should always be true // (edge case, but testing robustness) assertTrue(slideVerify.isExpired(-1)); } } ================================================ FILE: src/test/java/cn/wildfirechat/app/jpa/UserConferenceTest.java ================================================ package cn.wildfirechat.app.jpa; import org.junit.Test; import static org.junit.Assert.*; public class UserConferenceTest { @Test public void testDefaultConstructor() { // When UserConference userConference = new UserConference(); // Then - fields should have default values assertNull(userConference.getUserId()); assertNull(userConference.getConferenceId()); } @Test public void testConstructorWithParams() { // Given String userId = "user123"; String conferenceId = "conf456"; // When UserConference userConference = new UserConference(userId, conferenceId); // Then assertEquals(userId, userConference.getUserId()); assertEquals(conferenceId, userConference.getConferenceId()); } @Test public void testSettersAndGetters() { // Given UserConference userConference = new UserConference(); // When userConference.setUserId("user789"); userConference.setConferenceId("conf012"); // Then assertEquals("user789", userConference.getUserId()); assertEquals("conf012", userConference.getConferenceId()); } @Test public void testMultipleUserConferences() { // Given String[] userIds = {"user1", "user2", "user3"}; String conferenceId = "conf123"; // When/Then for (int i = 0; i < userIds.length; i++) { UserConference uc = new UserConference(userIds[i], conferenceId); assertEquals(userIds[i], uc.getUserId()); assertEquals(conferenceId, uc.getConferenceId()); } } } ================================================ FILE: src/test/java/cn/wildfirechat/app/jpa/UserNameEntryTest.java ================================================ package cn.wildfirechat.app.jpa; import org.junit.Test; import static org.junit.Assert.*; public class UserNameEntryTest { @Test public void testDefaultConstructor() { // When UserNameEntry entry = new UserNameEntry(); // Then assertNull(entry.getId()); } @Test public void testSetAndGetId() { // Given UserNameEntry entry = new UserNameEntry(); // When entry.setId(1); // Then assertEquals(Integer.valueOf(1), entry.getId()); } @Test public void testDifferentIds() { // Given UserNameEntry entry = new UserNameEntry(); // When - assign different IDs for (int i = 0; i < 100; i++) { entry.setId(i); // Then assertEquals(Integer.valueOf(i), entry.getId()); } } } ================================================ FILE: src/test/java/cn/wildfirechat/app/jpa/UserPasswordTest.java ================================================ package cn.wildfirechat.app.jpa; import org.junit.Test; import static org.junit.Assert.*; public class UserPasswordTest { @Test public void testDefaultConstructor() { // When UserPassword userPassword = new UserPassword(); // Then assertNull(userPassword.getUserId()); assertNull(userPassword.getPassword()); assertNull(userPassword.getSalt()); assertNull(userPassword.getResetCode()); assertEquals(0, userPassword.getTryCount()); assertEquals(0L, userPassword.getResetCodeTime()); assertEquals(0L, userPassword.getLastTryTime()); } @Test public void testConstructorWithUserId() { // Given String userId = "user123"; // When UserPassword userPassword = new UserPassword(userId); // Then assertEquals(userId, userPassword.getUserId()); assertNull(userPassword.getPassword()); assertEquals(0, userPassword.getTryCount()); } @Test public void testConstructorWithAllParams() { // Given String userId = "user123"; String password = "hashedPassword"; String salt = "randomSalt"; String resetCode = "123456"; long resetCodeTime = System.currentTimeMillis(); // When UserPassword userPassword = new UserPassword(userId, password, salt, resetCode, resetCodeTime); // Then assertEquals(userId, userPassword.getUserId()); assertEquals(password, userPassword.getPassword()); assertEquals(salt, userPassword.getSalt()); assertEquals(resetCode, userPassword.getResetCode()); assertEquals(resetCodeTime, userPassword.getResetCodeTime()); assertEquals(0, userPassword.getTryCount()); } @Test public void testSettersAndGetters() { // Given UserPassword userPassword = new UserPassword(); // When userPassword.setUserId("user456"); userPassword.setPassword("newPassword"); userPassword.setSalt("newSalt"); userPassword.setResetCode("654321"); userPassword.setResetCodeTime(1234567890L); userPassword.setTryCount(5); userPassword.setLastTryTime(9876543210L); // Then assertEquals("user456", userPassword.getUserId()); assertEquals("newPassword", userPassword.getPassword()); assertEquals("newSalt", userPassword.getSalt()); assertEquals("654321", userPassword.getResetCode()); assertEquals(1234567890L, userPassword.getResetCodeTime()); assertEquals(5, userPassword.getTryCount()); assertEquals(9876543210L, userPassword.getLastTryTime()); } @Test public void testTryCountIncrementation() { // Given UserPassword userPassword = new UserPassword("user", "pass", "salt"); assertEquals(0, userPassword.getTryCount()); // When - increment from constructor default (0) userPassword.setTryCount(userPassword.getTryCount() + 1); userPassword.setTryCount(userPassword.getTryCount() + 1); // Then assertEquals(2, userPassword.getTryCount()); } } ================================================ FILE: src/test/java/cn/wildfirechat/app/jpa/UserPrivateConferenceIdTest.java ================================================ package cn.wildfirechat.app.jpa; import org.junit.Test; import static org.junit.Assert.*; public class UserPrivateConferenceIdTest { @Test public void testDefaultConstructor() { // When UserPrivateConferenceId id = new UserPrivateConferenceId(); // Then assertNull(id.getUserId()); assertNull(id.getConferenceId()); } @Test public void testConstructorWithParams() { // Given String userId = "user123"; String conferenceId = "conf456"; // When UserPrivateConferenceId id = new UserPrivateConferenceId(userId, conferenceId); // Then assertEquals(userId, id.getUserId()); assertEquals(conferenceId, id.getConferenceId()); } @Test public void testSettersAndGetters() { // Given UserPrivateConferenceId id = new UserPrivateConferenceId(); // When id.setUserId("user789"); id.setConferenceId("conf012"); // Then assertEquals("user789", id.getUserId()); assertEquals("conf012", id.getConferenceId()); } @Test public void testUpdateConferenceId() { // Given UserPrivateConferenceId id = new UserPrivateConferenceId("user1", "conf1"); // When id.setConferenceId("conf2"); // Then assertEquals("user1", id.getUserId()); assertEquals("conf2", id.getConferenceId()); } } ================================================ FILE: src/test/java/cn/wildfirechat/app/slide/SlideVerifyCleanupServiceTest.java ================================================ package cn.wildfirechat.app.slide; import cn.wildfirechat.app.jpa.SlideVerify; import cn.wildfirechat.app.jpa.SlideVerifyRepository; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; import static org.junit.Assert.*; import static org.mockito.Mockito.*; @RunWith(MockitoJUnitRunner.class) public class SlideVerifyCleanupServiceTest { @Mock private SlideVerifyRepository slideVerifyRepository; private SlideVerifyCleanupService cleanupService; @Before public void setUp() { cleanupService = new SlideVerifyCleanupService(); try { java.lang.reflect.Field field = SlideVerifyCleanupService.class.getDeclaredField("slideVerifyRepository"); field.setAccessible(true); field.set(cleanupService, slideVerifyRepository); } catch (Exception e) { fail("Failed to inject mock repository: " + e.getMessage()); } } @Test public void testCleanupExpired() { // Given int expectedDeleted = 5; when(slideVerifyRepository.deleteExpired(anyLong())).thenReturn(expectedDeleted); // When cleanupService.cleanupExpired(); // Then verify(slideVerifyRepository, times(1)).deleteExpired(anyLong()); } @Test public void testCleanupExpiredNoData() { // Given when(slideVerifyRepository.deleteExpired(anyLong())).thenReturn(0); // When cleanupService.cleanupExpired(); // Then verify(slideVerifyRepository, times(1)).deleteExpired(anyLong()); } } ================================================ FILE: src/test/java/cn/wildfirechat/app/slide/SlideVerifyServiceTest.java ================================================ package cn.wildfirechat.app.slide; import cn.wildfirechat.app.jpa.SlideVerify; import cn.wildfirechat.app.jpa.SlideVerifyRepository; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; import java.util.HashMap; import java.util.Map; import java.util.Optional; import static org.junit.Assert.*; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.*; @RunWith(MockitoJUnitRunner.class) public class SlideVerifyServiceTest { @Mock private SlideVerifyRepository slideVerifyRepository; private SlideVerifyService slideVerifyService; @Before public void setUp() { slideVerifyService = new SlideVerifyService(); // 通过反射注入 mock repository try { java.lang.reflect.Field field = SlideVerifyService.class.getDeclaredField("slideVerifyRepository"); field.setAccessible(true); field.set(slideVerifyService, slideVerifyRepository); } catch (Exception e) { fail("Failed to inject mock repository: " + e.getMessage()); } } @Test public void testGenerateSlideVerify() { // When Map result = slideVerifyService.generateSlideVerify(); // Then assertNotNull(result); assertTrue(result.containsKey("token")); assertTrue(result.containsKey("backgroundImage")); assertTrue(result.containsKey("sliderImage")); assertTrue(result.containsKey("y")); String token = (String) result.get("token"); assertNotNull(token); assertFalse(token.isEmpty()); int y = (int) result.get("y"); assertTrue(y >= 20 && y <= 100); // 20 + random(0, 80) // Verify repository save was called verify(slideVerifyRepository, times(1)).save(any(SlideVerify.class)); } @Test public void testVerifySlideSuccess() { // Given String token = "test-token-success"; int correctX = 150; int userX = 155; // within tolerance SlideVerify slideVerify = new SlideVerify(token, correctX, System.currentTimeMillis()); when(slideVerifyRepository.findByToken(token)).thenReturn(Optional.of(slideVerify)); // When boolean result = slideVerifyService.verifySlide(token, userX); // Then assertTrue(result); verify(slideVerifyRepository, times(1)).save(any(SlideVerify.class)); } @Test public void testVerifySlideFailure() { // Given String token = "test-token-failure"; int correctX = 150; int userX = 100; // outside tolerance SlideVerify slideVerify = new SlideVerify(token, correctX, System.currentTimeMillis()); when(slideVerifyRepository.findByToken(token)).thenReturn(Optional.of(slideVerify)); // When boolean result = slideVerifyService.verifySlide(token, userX); // Then assertFalse(result); verify(slideVerifyRepository, times(1)).delete(any(SlideVerify.class)); } @Test public void testVerifySlideTokenNotFound() { // Given String token = "non-existent-token"; when(slideVerifyRepository.findByToken(token)).thenReturn(Optional.empty()); // When boolean result = slideVerifyService.verifySlide(token, 100); // Then assertFalse(result); verify(slideVerifyRepository, never()).save(any()); verify(slideVerifyRepository, never()).delete(any()); } @Test public void testVerifySlideAlreadyVerified() { // Given String token = "already-verified-token"; SlideVerify slideVerify = new SlideVerify(token, 150, System.currentTimeMillis()); slideVerify.setVerified(true); // Already verified when(slideVerifyRepository.findByToken(token)).thenReturn(Optional.of(slideVerify)); // When boolean result = slideVerifyService.verifySlide(token, 150); // Then assertFalse(result); verify(slideVerifyRepository, never()).save(any()); verify(slideVerifyRepository, never()).delete(any()); } @Test public void testIsVerifiedWhenVerified() { // Given String token = "verified-token"; SlideVerify slideVerify = new SlideVerify(token, 150, System.currentTimeMillis()); slideVerify.setVerified(true); when(slideVerifyRepository.findByToken(token)).thenReturn(Optional.of(slideVerify)); // When boolean result = slideVerifyService.isVerified(token); // Then assertTrue(result); verify(slideVerifyRepository, times(1)).delete(any(SlideVerify.class)); } @Test public void testIsVerifiedWhenNotVerified() { // Given String token = "not-verified-token"; SlideVerify slideVerify = new SlideVerify(token, 150, System.currentTimeMillis()); slideVerify.setVerified(false); when(slideVerifyRepository.findByToken(token)).thenReturn(Optional.of(slideVerify)); // When boolean result = slideVerifyService.isVerified(token); // Then assertFalse(result); verify(slideVerifyRepository, never()).delete(any()); } @Test public void testIsVerifiedWhenNotFound() { // Given String token = "non-existent-token"; when(slideVerifyRepository.findByToken(token)).thenReturn(Optional.empty()); // When boolean result = slideVerifyService.isVerified(token); // Then assertFalse(result); } @Test public void testIsVerifiedWhenExpired() { // Given String token = "expired-token"; SlideVerify slideVerify = new SlideVerify(token, 150, System.currentTimeMillis() - 400000); // 400秒前 slideVerify.setVerified(false); when(slideVerifyRepository.findByToken(token)).thenReturn(Optional.of(slideVerify)); // When boolean result = slideVerifyService.isVerified(token); // Then assertFalse(result); } } ================================================ FILE: systemd/README.md ================================================ # Linux Service 方式运行 除了命令行方式直接执行APP服务外,还可以以linux systemd service方式来运行,注意以这种方式运行,APP服务的配置还是需要按照常规方法来配置。 ## 获取软件包 下载野火release或则会自己源码编译,得到```app-${version}.jar```、```app-${version}.deb```和```app-${version}.rpm```。 ## 手动部署 ### 依赖 野火IM依赖JRE1.8手动部署需要手动安装JRE1.8,确保命令:```java -version```能看到正确的java版本信息才行。 ### 部署软件包 创建```/opt/app-server```目录,把Jar包```app-${version}.jar```改名为```app-server.jar```;把config目录也拷贝到```/opt/app-server```目录下。 ### 放置systemd server file 把```app-server.service```放到```/usr/lib/systemd/system/```目录下。 ### 测试 根据下面管理服务的说明,启动服务,查看控制台日志,确认启动没有异常,服务器本地执行 ```curl -v http://127.0.0.1:8888``` 能够返回字符串```Ok```。 ## 安装部署 ### 依赖 安装包安装将会自动安装依赖,不需要手动安装java。如果服务器上有其他版本的Java,请注意可能的冲突问题。 ### 部署软件包 可以直接安装```deb```和```rpm```格式的安装包,在debian系的linux系统(Ubuntu等使用```apt```命令安装软件的系统)中,使用命令: ```shell sudo apt install ./app-server-{version}.deb ``` 在红帽系的linux系统(Centos等使用```yum```命令安装软件的系统)中,使用命令: ```shell sudo yum install ./app-server-${version}.deb ``` 注意在上述两个命令中,都使用的是本地安装,注意安装包名前的```./```路径。如果使用```dpkg -i ./app-server-${version}.deb```命令将不会安装依赖。 ### 测试 根据下面管理服务的说明,启动服务,查看控制台日志,确认启动没有异常,服务器本地执行 ```curl -v http://127.0.0.1:8888``` 能够返回字符串```Ok```。 ## 管理服务 * 刷新配置,当安装或者更新后需要执行: ```sudo systemctl daemon-reload``` * 启动服务: ```sudo systemctl start app-server``` * 停止服务: ```sudo systemctl stop app-server``` * 重启服务: ```sudo systemctl restart app-server``` * 查看服务状态:```sudo systemctl status app-server``` * 设置开机自启动:```sudo systemctl enable app-server``` * 禁止开机自启动:```sudo systemctl disable app-server``` * 查看控制台日志: ```journalctl -f -u app-server``` ## 日志 日志主要看制台日志。如果需要看日志,请使用命令```journalctl -f -u app-server```来查看日志。 ## 配置 需要对APP服务配置来达到最好的执行效果,配置文件在````/opt/app-server/config````目录下。另外还可以设置服务的内存大小,修改```/usr/lib/systemd/system/app-server```文件,在java命令中添加```-Xmx```参数。 ================================================ FILE: systemd/app-server.service ================================================ [Unit] Description=WildfirechatAPP Documentation=https://docs.wildfirechat.cn Wants=network-online.target After=network-online.target [Service] WorkingDirectory=/opt/app-server #ExecStart=/usr/bin/java -server -Xmx2G -Xms2G -jar app-server.jar 2>&1 ExecStart=/usr/bin/java -server -jar app-server.jar 2>&1 # Let systemd restart this service always Restart=always RestartSec=5 # Specifies the maximum file descriptor number that can be opened by this process LimitNOFILE=65536 # Specifies the maximum number of threads this process can create TasksMax=infinity # Disable timeout logic and wait until process is stopped TimeoutStopSec=infinity SendSIGKILL=no [Install] WantedBy=multi-user.target