Full Code of wildfirechat/im-app_server for AI

master f06dc8460a5f cached
148 files
341.2 KB
86.4k tokens
883 symbols
1 requests
Download .txt
Showing preview only (405K chars total). Download the full file or copy to clipboard to get everything.
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
    <dependency>
        <groupId>com.aliyun</groupId>
        <artifactId>aliyun-java-sdk-core</artifactId>
        <version>4.1.0</version>
    </dependency>
    ```

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 <support@wildfirechat.cn>
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
================================================
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>

	<groupId>cn.wildfirechat</groupId>
	<artifactId>app</artifactId>
	<version>0.72</version>
	<packaging>jar</packaging>

	<name>app</name>
	<description>Demo project for Wildfire chat app server</description>

	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.2.10.RELEASE</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>

	<properties>
		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
		<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
		<java.version>1.8</java.version>
		<log4j2.version>2.17.2</log4j2.version>
		<wfc.sdk.version>1.4.4</wfc.sdk.version>
	</properties>

	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter</artifactId>
		</dependency>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-data-rest</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-data-jpa</artifactId>
		</dependency>
		<dependency>
			<groupId>com.h2database</groupId>
			<artifactId>h2</artifactId>
		</dependency>

		<dependency>
			<groupId>mysql</groupId>
			<artifactId>mysql-connector-java</artifactId>
			<version>8.0.28</version>
		</dependency>

		<!-- 达梦数据库 JDBC 驱动 -->
		<dependency>
			<groupId>com.dameng</groupId>
			<artifactId>DmJdbcDriver8</artifactId>
			<version>8.1.3.162</version>
			<scope>system</scope>
			<systemPath>${project.basedir}/src/lib/DmJdbcDriver8.jar</systemPath>
		</dependency>


		<!-- Hibernate 达梦方言 -->
		<dependency>
			<groupId>com.dameng</groupId>
			<artifactId>DmDialect-for-hibernate5.3</artifactId>
			<version>8.1.3.162</version>
			<scope>system</scope>
			<systemPath>${project.basedir}/src/lib/DmDialect-for-hibernate5.4.jar</systemPath>
		</dependency>

		<dependency>
			<groupId>com.google.code.gson</groupId>
			<artifactId>gson</artifactId>
			<version>2.8.9</version>
		</dependency>

		<dependency>
			<groupId>com.google.protobuf</groupId>
			<artifactId>protobuf-java</artifactId>
			<version>2.5.0</version>
		</dependency>

		<dependency>
			<groupId>org.apache.logging.log4j</groupId>
			<artifactId>log4j-slf4j-impl</artifactId>
			<version>${log4j2.version}</version>
		</dependency>
		<dependency>
			<groupId>org.apache.logging.log4j</groupId>
			<artifactId>log4j-api</artifactId>
			<version>${log4j2.version}</version>
		</dependency>
		<dependency>
			<groupId>org.apache.logging.log4j</groupId>
			<artifactId>log4j-core</artifactId>
			<version>${log4j2.version}</version>
		</dependency>
		<dependency>
			<groupId>org.apache.logging.log4j</groupId>
			<artifactId>log4j-to-slf4j</artifactId>
			<version>${log4j2.version}</version>
		</dependency>

		<dependency>
			<groupId>commons-io</groupId>
			<artifactId>commons-io</artifactId>
			<version>2.7</version>
		</dependency>

		<dependency>
			<groupId>com.googlecode.json-simple</groupId>
			<artifactId>json-simple</artifactId>
			<version>1.1.1</version>
		</dependency>

		<dependency>
			<groupId>org.slf4j</groupId>
			<artifactId>slf4j-api</artifactId>
			<version>1.7.5</version>
		</dependency>

		<dependency>
			<groupId>org.slf4j</groupId>
			<artifactId>slf4j-log4j12</artifactId>
			<version>1.7.5</version>
		</dependency>


		<dependency>
			<groupId>commons-httpclient</groupId>
			<artifactId>commons-httpclient</artifactId>
			<version>3.1</version>
		</dependency>

		<dependency>
			<groupId>uk.org.lidalia</groupId>
			<artifactId>slf4j-test</artifactId>
			<version>1.0.0-jdk6</version>
			<scope>test</scope>
		</dependency>

		<dependency>
			<groupId>org.mockito</groupId>
			<artifactId>mockito-all</artifactId>
			<version>1.9.5</version>
			<type>jar</type>
			<scope>test</scope>
		</dependency>

		<dependency>
			<groupId>com.tencentcloudapi</groupId>
			<artifactId>tencentcloud-sdk-java-sms</artifactId>
			<version>3.1.410</version>
		</dependency>

		<dependency>
			<groupId>cn.wildfirechat</groupId>
			<artifactId>sdk</artifactId>
			<version>${wfc.sdk.version}</version>
			<scope>system</scope>
			<systemPath>${project.basedir}/src/lib/sdk-${wfc.sdk.version}.jar</systemPath>
		</dependency>

		<dependency>
			<groupId>cn.wildfirechat</groupId>
			<artifactId>common</artifactId>
			<version>${wfc.sdk.version}</version>
			<scope>system</scope>
			<systemPath>${project.basedir}/src/lib/common-${wfc.sdk.version}.jar</systemPath>
		</dependency>


		<dependency>
			<groupId>com.aliyun</groupId>
			<artifactId>aliyun-java-sdk-core</artifactId>
			<version>4.1.0</version>
		</dependency>

		<dependency>
			<groupId>com.qiniu</groupId>
			<artifactId>qiniu-java-sdk</artifactId>
			<version>7.3.0</version>
		</dependency>

		<dependency>
			<groupId>com.aliyun.oss</groupId>
			<artifactId>aliyun-sdk-oss</artifactId>
			<version>3.10.2</version>
		</dependency>

		<dependency>
			<groupId>io.minio</groupId>
			<artifactId>minio</artifactId>
			<version>7.0.2</version>
		</dependency>

		<dependency>
			<groupId>com.qcloud</groupId>
			<artifactId>cos_api</artifactId>
			<version>5.6.28</version>
		</dependency>

		<dependency>
			<groupId>com.google.guava</groupId>
			<artifactId>guava</artifactId>
			<version>25.1-jre</version>
		</dependency>

		<dependency>
			<groupId>org.apache.shiro</groupId>
			<artifactId>shiro-spring</artifactId>
			<version>1.7.1</version>
		</dependency>

		<dependency>
			<groupId>ws.schild</groupId>
			<artifactId>jave-core</artifactId>
			<version>2.7.3</version>
		</dependency>

		<dependency>
			<groupId>ws.schild</groupId>
			<artifactId>jave-nativebin-linux64</artifactId>
			<version>2.7.3</version>
		</dependency>

<!--		为了减少包的大小,ws.schild只打包目标架构的库。如果您使用非Linux,请把上面注释掉,然后下面打开自己平台-->

<!--		<dependency>-->
<!--			<groupId>ws.schild</groupId>-->
<!--			<artifactId>jave-nativebin-win64</artifactId>-->
<!--			<version>2.7.3</version>-->
<!--		</dependency>-->

<!--		<dependency>-->
<!--			<groupId>ws.schild</groupId>-->
<!--			<artifactId>jave-nativebin-osx64</artifactId>-->
<!--			<version>2.7.3</version>-->
<!--		</dependency>-->

<!--		<dependency>-->
<!--			<groupId>ws.schild</groupId>-->
<!--			<artifactId>jave-all-deps</artifactId>-->
<!--			<version>2.7.3</version>-->
<!--		</dependency>-->

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-mail</artifactId>
		</dependency>
	</dependencies>

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
				<configuration>
					<includeSystemScope>true</includeSystemScope>
				</configuration>
			</plugin>

			<plugin>
				<artifactId>jdeb</artifactId>
				<groupId>org.vafer</groupId>
				<version>1.8</version>
				<executions>
					<execution>
						<phase>package</phase>
						<goals>
							<goal>jdeb</goal>
						</goals>
						<configuration>
							<controlDir>${project.basedir}/deb/control</controlDir>
							<skipPOMs>false</skipPOMs>
							<deb>${project.build.directory}/app-server-${project.version}.deb</deb>
							<dataSet>
								<data>
									<src>${project.build.directory}/${project.name}-${project.version}.jar</src>
									<type>file</type>
									<mapper>
										<type>perm</type>
										<prefix>/opt/app-server</prefix>
									</mapper>
								</data>
								<data>
									<src>${project.basedir}/config</src>
									<type>directory</type>
									<mapper>
										<type>perm</type>
										<prefix>/opt/app-server/config</prefix>
									</mapper>
								</data>
								<data>
									<src>${project.basedir}/systemd/app-server.service</src>
									<type>file</type>
									<mapper>
										<type>perm</type>
										<prefix>/usr/lib/systemd/system</prefix>
									</mapper>
								</data>
							</dataSet>
						</configuration>
					</execution>
				</executions>
			</plugin>

			<!--        RPM Plugin 开始-->
			<!--        打包RPM 包需要本地有rpm命令才可以,linux和mac都可以安装rpm。如果是windows需要cygwin安装rpm才可以,如果需要rpm包,可以把这个plugin取消注释-->
<!--			<plugin>-->
<!--				<groupId>org.codehaus.mojo</groupId>-->
<!--				<artifactId>rpm-maven-plugin</artifactId>-->
<!--				<version>2.2.0</version>-->
<!--				<executions>-->
<!--					<execution>-->
<!--						<id>generate-rpm</id>-->
<!--						<goals>-->
<!--							<goal>rpm</goal>-->
<!--						</goals>-->
<!--					</execution>-->
<!--				</executions>-->
<!--				<configuration>-->
<!--					<group>Applications/Chat</group>-->
<!--					<name>app-server</name>-->
<!--					<needarch>noarch</needarch>-->
<!--					<targetOS>linux</targetOS>-->
<!--					<prefix>/opt/app-server</prefix>-->
<!--					<defineStatements>-->
<!--						<defineStatement>_unpackaged_files_terminate_build 0</defineStatement>-->
<!--					</defineStatements>-->
<!--					<copyTo>-->
<!--						target/app-server-${project.version}.rpm-->
<!--					</copyTo>-->
<!--					<requires>-->
<!--						<require>java-1.8.0-openjdk-headless</require>-->
<!--					</requires>-->
<!--					<mappings>-->
<!--						<mapping>-->
<!--							<directory>/opt/app-server</directory>-->
<!--							<filemode>755</filemode>-->
<!--							<sources>-->
<!--								<source>-->
<!--									<location>${project.build.directory}/${project.name}-${project.version}.jar</location>-->
<!--								</source>-->
<!--							</sources>-->
<!--						</mapping>-->
<!--						<mapping>-->
<!--							<directory>/opt/app-server/config</directory>-->
<!--							<filemode>644</filemode>-->
<!--							<sources>-->
<!--								<source>-->
<!--									<location>${project.basedir}/config</location>-->
<!--								</source>-->
<!--							</sources>-->
<!--						</mapping>-->
<!--						<mapping>-->
<!--							<directory>/usr/lib/systemd/system</directory>-->
<!--							<filemode>644</filemode>-->
<!--							<username>root</username>-->
<!--							<groupname>root</groupname>-->
<!--							<directoryIncluded>false</directoryIncluded>-->
<!--							<sources>-->
<!--								<source>-->
<!--									<location>${project.basedir}/systemd/app-server.service</location>-->
<!--								</source>-->
<!--							</sources>-->
<!--						</mapping>-->
<!--					</mappings>-->
<!--					<postinstallScriptlet>-->
<!--						<script>-->
<!--							mv -f /opt/app-server/app-*.jar /opt/app-server/app-server.jar ; echo "Im server installed in /opt/app-server" ; systemctl daemon-reload-->
<!--						</script>-->
<!--					</postinstallScriptlet>-->
<!--					<postremoveScriptlet>-->
<!--						<script>-->
<!--							echo "Remove files..." ; cd /opt/app-server ; rm -rf * ; rm -rf /usr/lib/systemd/system/app-server.service ; systemctl daemon-reload-->
<!--						</script>-->
<!--					</postremoveScriptlet>-->
<!--				</configuration>-->
<!--			</plugin>-->
			<!--            RPM Plugin 结束-->

		</plugins>
	</build>

</project>


================================================
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<RestResult> timeoutResponseEntity = new ResponseEntity<>(timeoutResult, HttpStatus.OK);
        int timeoutSecond = 50;
        DeferredResult<ResponseEntity> 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<ResponseEntity<InputStreamResource>> 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<ResponseEntity<InputStreamResource>>() {
            /**
             * Gets a result.
             *
             * @return a result
             */
            @Override
            public ResponseEntity<InputStreamResource> 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<IMExceptionEvent> 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<String, Boolean> 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<InputOutputUserInfo> inputOutputUserInfoIMResult = UserAdmin.getUserByMobile(mobile);
            if(inputOutputUserInfoIMResult != null && inputOutputUserInfoIMResult.getErrorCode() == ErrorCode.ERROR_CODE_SUCCESS) {
                IMResult<OutputUserStatus> 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<InputOutputUserInfo> 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<String, Object> 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<InputOutputUserInfo> 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<InputOutputUserInfo> 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<UserPassword> 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<LdapUser> 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<InputOutputUserInfo> 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<UserPassword> 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<UserPassword> 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<InputOutputUserInfo> 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<UserPassword> 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<InputOutputUserInfo> 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<OutputCreateUser> 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<OutputCreateUser> 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<OutputGetIMTokenData> 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<UserPassword> 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<OutputBooleanValue> 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<InputOutputUserInfo> 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<InputOutputUserInfo> 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<InputOutputUserInfo> 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<SendMessageResult> 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<SendMessageResult> 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<InputOutputUserInfo> 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<OutputGetIMTokenData> 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<InputOutputUserInfo> 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<OutputCreateUser> 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> 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<OutputGroupMemberList> 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<SendMessageResult> 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<OutputDevice> 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<OutputCreateDevice> 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<OutputDeviceList> 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<OutputDevice> 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<OutputCreateDevice> 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<SendMessageResult> 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<FavoriteItem> 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<OutputGroupMemberList> 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<PojoGroupMember> 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<UserIdNamePortraitPojo> mids = new ArrayList<>();
            for (PojoGroupMember member:groupMembers) {
                IMResult<InputOutputUserInfo> 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<ConferenceEntity> expiredConferences = conferenceEntityRepository.findExpiredConferences(currentTime);
        LOG.info("发现 {} 个过期会议", expiredConferences.size());

        for (ConferenceEntity conference : expiredConferences) {
            try {
                LOG.info("正在销毁过期会议: {}, endTime: {}", conference.id, conference.endTime);
                
                // 调用SDK销毁会议
                IMResult<Void> 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<UserPrivateConferenceId> 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<ConferenceEntity> 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<ConferenceEntity> 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<ConferenceEntity> 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<Void> 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<ConferenceEntity> 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<PojoConferenceInfoList> 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<ConferenceEntity> 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<Void> 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<ConferenceEntity> 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<ConferenceDTO> ucs = userConferenceRepository.findByUserId(getUserId(), System.currentTimeMillis()/1000);
        List<ConferenceInfo> 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> 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<UserConferenceQuota> 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<UserQuotaUsage> 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<UserConferenceQuota> 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<UserQuotaUsage> 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<ConferenceRecord> 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<UserQuotaUsage> 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<Announcement, String> {


}


================================================
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) {
Download .txt
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
Download .txt
SYMBOL INDEX (883 symbols across 121 files)

FILE: src/main/java/cn/wildfirechat/app/AppController.java
  class AppController (line 24) | @RestController
    method health (line 30) | @GetMapping()
    method generateSlideVerify (line 39) | @PostMapping(value = "/slide_verify/generate", produces = "application...
    method verifySlide (line 45) | @PostMapping(value = "/slide_verify/verify", produces = "application/j...
    method sendLoginCode (line 50) | @PostMapping(value = "/send_code", produces = "application/json;charse...
    method loginWithMobileCode (line 55) | @PostMapping(value = "/login", produces = "application/json;charset=UT...
    method loginWithPassword (line 60) | @PostMapping(value = "/login_pwd", produces = "application/json;charse...
    method changePassword (line 65) | @PostMapping(value = "/change_pwd", produces = "application/json;chars...
    method sendResetCode (line 70) | @PostMapping(value = "/send_reset_code", produces = "application/json;...
    method resetPassword (line 75) | @PostMapping(value = "/reset_pwd", produces = "application/json;charse...
    method sendDestroyCode (line 80) | @PostMapping(value = "/send_destroy_code", produces = "application/jso...
    method destroy (line 85) | @PostMapping(value = "/destroy", produces = "application/json;charset=...
    method createPcSession (line 94) | @CrossOrigin
    method loginWithSession (line 100) | @CrossOrigin
    method scanPc (line 146) | @PostMapping(value = "/scan_pc/{token}", produces = "application/json;...
    method confirmPc (line 151) | @PostMapping(value = "/confirm_pc", produces = "application/json;chars...
    method cancelPc (line 155) | @PostMapping(value = "/cancel_pc", produces = "application/json;charse...
    method changeName (line 163) | @CrossOrigin
    method putGroupAnnouncement (line 176) | @CrossOrigin
    method getGroupAnnouncement (line 182) | @CrossOrigin
    method uploadFiles (line 191) | @PostMapping(value = "/logs/{userId}/upload")
    method complain (line 199) | @CrossOrigin
    method addDevice (line 208) | @PostMapping(value = "/things/add_device")
    method getDeviceList (line 213) | @PostMapping(value = "/things/list_device")
    method delDevice (line 218) | @PostMapping(value = "/things/del_device")
    method sendUserMessage (line 226) | @PostMapping(value = "/messages/send")
    method uploadMedia (line 234) | @PostMapping(value = "/media/upload/{media_type}")
    method putFavoriteItem (line 239) | @CrossOrigin
    method removeFavoriteItem (line 245) | @CrossOrigin
    method getFavoriteItems (line 251) | @CrossOrigin
    method getGroupMembersForPortrait (line 257) | @CrossOrigin

FILE: src/main/java/cn/wildfirechat/app/Application.java
  class Application (line 17) | @SpringBootApplication
    method main (line 27) | public static void main(String[] args) {
    method multipartConfigElement (line 36) | @Bean
    method clearPCSession (line 46) | @Scheduled(fixedRate = 60 * 60 * 1000)
    method cleanExpiredSlideVerify (line 51) | @Scheduled(fixedRate = 60 * 60 * 1000)

FILE: src/main/java/cn/wildfirechat/app/AudioController.java
  class AudioController (line 21) | @RestController
    method init (line 28) | @PostConstruct
    method amr2mp3 (line 36) | @GetMapping("amr2mp3")
    method amr2mp3 (line 82) | private static void amr2mp3(String sourceUrl, File target) throws Malf...

FILE: src/main/java/cn/wildfirechat/app/ForbiddenException.java
  class ForbiddenException (line 6) | @ResponseStatus(value = HttpStatus.FORBIDDEN, reason="Forbidden")

FILE: src/main/java/cn/wildfirechat/app/IMCallbackController.java
  class IMCallbackController (line 16) | @RestController()
    method onUserOnlineEvent (line 21) | @PostMapping(value = "/im_event/user/online")
    method onUserRelationUpdated (line 30) | @PostMapping(value = "/im_event/user/relation")
    method onUserInfoUpdated (line 39) | @PostMapping(value = "/im_event/user/info")
    method onMessage (line 48) | @PostMapping(value = "/im_event/message")
    method onRecallMessage (line 57) | @PostMapping(value = "/im_event/recall_message")
    method onThingsMessage (line 66) | @PostMapping(value = "/im_event/things/message")
    method onMessageRead (line 75) | @PostMapping(value = "/im_event/message_read")
    method onGroupInfoUpdated (line 84) | @PostMapping(value = "/im_event/group/info")
    method onGroupMemberUpdated (line 93) | @PostMapping(value = "/im_event/group/member")
    method onChannelInfoUpdated (line 102) | @PostMapping(value = "/im_event/channel/info")
    method onChatroomInfoUpdated (line 111) | @PostMapping(value = "/im_event/chatroom/info")
    method onChatroomMemberUpdated (line 120) | @PostMapping(value = "/im_event/chatroom/member")
    method censorMessage (line 132) | @PostMapping(value = "/message/censor")
    method onConferenceCreated (line 145) | @PostMapping(value = "/im_event/conference/create")
    method onConferenceDestroyed (line 151) | @PostMapping(value = "/im_event/conference/destroy")
    method onConferenceMemberJoined (line 157) | @PostMapping(value = "/im_event/conference/member_join")
    method onConferenceMemberLeaved (line 163) | @PostMapping(value = "/im_event/conference/member_leave")
    method onConferenceMemberPublished (line 169) | @PostMapping(value = "/im_event/conference/member_publish")
    method onConferenceMemberUnpublished (line 175) | @PostMapping(value = "/im_event/conference/member_unpublish")
    method onMomentsFeed (line 181) | @PostMapping(value = "/im_event/moments_feed")
    method onMomentsFeedRecall (line 187) | @PostMapping(value = "/im_event/moments_feed_recall")
    method onMomentsComment (line 193) | @PostMapping(value = "/im_event/moments_comment")
    method onMomentsCommentRecall (line 199) | @PostMapping(value = "/im_event/moments_comment_recall")

FILE: src/main/java/cn/wildfirechat/app/IMConfig.java
  class IMConfig (line 7) | @Configuration
    method isUse_random_name (line 16) | public boolean isUse_random_name() {
    method setUse_random_name (line 20) | public void setUse_random_name(boolean use_random_name) {
    method getAdmin_url (line 39) | public String getAdmin_url() {
    method setAdmin_url (line 43) | public void setAdmin_url(String admin_url) {
    method getAdmin_secret (line 47) | public String getAdmin_secret() {
    method setAdmin_secret (line 51) | public void setAdmin_secret(String admin_secret) {
    method getWelcome_for_new_user (line 55) | public String getWelcome_for_new_user() {
    method setWelcome_for_new_user (line 59) | public void setWelcome_for_new_user(String welcome_for_new_user) {
    method getWelcome_for_back_user (line 63) | public String getWelcome_for_back_user() {
    method setWelcome_for_back_user (line 67) | public void setWelcome_for_back_user(String welcome_for_back_user) {
    method isNew_user_robot_friend (line 71) | public boolean isNew_user_robot_friend() {
    method setNew_user_robot_friend (line 75) | public void setNew_user_robot_friend(boolean new_user_robot_friend) {
    method getRobot_friend_id (line 79) | public String getRobot_friend_id() {
    method setRobot_friend_id (line 83) | public void setRobot_friend_id(String robot_friend_id) {
    method getRobot_welcome (line 87) | public String getRobot_welcome() {
    method setRobot_welcome (line 91) | public void setRobot_welcome(String robot_welcome) {
    method getNew_user_subscribe_channel_id (line 95) | public String getNew_user_subscribe_channel_id() {
    method setNew_user_subscribe_channel_id (line 99) | public void setNew_user_subscribe_channel_id(String new_user_subscribe...
    method getBack_user_subscribe_channel_id (line 103) | public String getBack_user_subscribe_channel_id() {
    method setBack_user_subscribe_channel_id (line 107) | public void setBack_user_subscribe_channel_id(String back_user_subscri...
    method getAdmin_user_id (line 111) | public String getAdmin_user_id() {
    method setAdmin_user_id (line 115) | public void setAdmin_user_id(String admin_user_id) {
    method getPrompt_text (line 119) | public String getPrompt_text() {
    method setPrompt_text (line 123) | public void setPrompt_text(String prompt_text) {
    method getImage_msg_url (line 127) | public String getImage_msg_url() {
    method setImage_msg_url (line 131) | public void setImage_msg_url(String image_msg_url) {
    method getImage_msg_base64_thumbnail (line 135) | public String getImage_msg_base64_thumbnail() {
    method setImage_msg_base64_thumbnail (line 139) | public void setImage_msg_base64_thumbnail(String image_msg_base64_thum...

FILE: src/main/java/cn/wildfirechat/app/IMExceptionEventController.java
  class IMExceptionEventController (line 33) | @RestController
    method init (line 47) | @PostConstruct
    method onIMException (line 65) | @PostMapping("im_exception_event")
    method sendTextMail (line 77) | public void sendTextMail(String subject, String content){
    method sendHtmlMail (line 89) | public void sendHtmlMail(String subject, String content) throws Messag...

FILE: src/main/java/cn/wildfirechat/app/RestResult.java
  class RestResult (line 3) | public class RestResult {
    type RestCode (line 4) | public enum  RestCode {
      method RestCode (line 31) | RestCode(int code, String msg) {
    method ok (line 41) | public static RestResult ok() {
    method ok (line 45) | public static RestResult ok(Object object) {
    method error (line 49) | public static RestResult error(RestCode code) {
    method result (line 53) | public static RestResult result(RestCode code, Object object){
    method result (line 57) | public static RestResult result(int code, String message, Object object){
    method RestResult (line 64) | private RestResult(RestCode code, Object result) {
    method getCode (line 70) | public int getCode() {
    method setCode (line 74) | public void setCode(int code) {
    method getMessage (line 78) | public String getMessage() {
    method setMessage (line 82) | public void setMessage(String message) {
    method getResult (line 86) | public Object getResult() {
    method setResult (line 90) | public void setResult(Object result) {

FILE: src/main/java/cn/wildfirechat/app/Service.java
  type Service (line 11) | public interface Service {
    method sendLoginCode (line 12) | RestResult sendLoginCode(String mobile);
    method sendLoginCode (line 13) | RestResult sendLoginCode(String mobile, String slideVerifyToken);
    method sendResetCode (line 14) | RestResult sendResetCode(String mobile, String slideVerifyToken);
    method loginWithMobileCode (line 15) | RestResult loginWithMobileCode(HttpServletResponse response, String mo...
    method loginWithPassword (line 16) | RestResult loginWithPassword(HttpServletResponse response, String mobi...
    method changePassword (line 17) | RestResult changePassword(String oldPwd, String newPwd, String slideVe...
    method resetPassword (line 18) | RestResult resetPassword(String mobile, String resetCode, String newPwd);
    method sendDestroyCode (line 19) | RestResult sendDestroyCode();
    method sendDestroyCode (line 20) | RestResult sendDestroyCode(String slideVerifyToken);
    method destroy (line 21) | RestResult destroy(HttpServletResponse response, String code);
    method createPcSession (line 23) | RestResult createPcSession(CreateSessionRequest request);
    method loginWithSession (line 24) | RestResult loginWithSession(String token);
    method scanPc (line 26) | RestResult scanPc(String token);
    method confirmPc (line 27) | RestResult confirmPc(ConfirmSessionRequest request);
    method cancelPc (line 28) | RestResult cancelPc(CancelSessionRequest request);
    method changeName (line 30) | RestResult changeName(String newName);
    method complain (line 31) | RestResult complain(String text);
    method putGroupAnnouncement (line 33) | RestResult putGroupAnnouncement(GroupAnnouncementPojo request);
    method getGroupAnnouncement (line 34) | RestResult getGroupAnnouncement(String groupId);
    method saveUserLogs (line 36) | RestResult saveUserLogs(String userId, MultipartFile file);
    method generateSlideVerify (line 38) | RestResult generateSlideVerify();
    method verifySlide (line 39) | RestResult verifySlide(String token, int x);
    method addDevice (line 41) | RestResult addDevice(InputCreateDevice createDevice);
    method getDeviceList (line 42) | RestResult getDeviceList();
    method delDevice (line 43) | RestResult delDevice(InputCreateDevice createDevice);
    method sendUserMessage (line 45) | RestResult sendUserMessage(SendMessageRequest request);
    method uploadMedia (line 46) | RestResult uploadMedia(int mediaType, MultipartFile file);
    method putFavoriteItem (line 48) | RestResult putFavoriteItem(FavoriteItem request);
    method removeFavoriteItems (line 49) | RestResult removeFavoriteItems(long id);
    method getFavoriteItems (line 50) | RestResult getFavoriteItems(long id, int count);
    method getGroupMembersForPortrait (line 51) | RestResult getGroupMembersForPortrait(String groupId);

FILE: src/main/java/cn/wildfirechat/app/ServiceImpl.java
  class ServiceImpl (line 70) | @org.springframework.stereotype.Service
    method init (line 195) | @PostConstruct
    method getIp (line 204) | private String getIp() {
    method getUserStatus (line 225) | private int getUserStatus(String mobile) {
    method sendLoginCode (line 239) | @Override
    method sendLoginCode (line 244) | @Override
    method generateSlideVerify (line 318) | @Override
    method verifySlide (line 329) | @Override
    method sendResetCode (line 339) | @Override
    method loginWithMobileCode (line 433) | @Override
    method loginWithLdap (line 481) | public RestResult loginWithLdap(HttpServletResponse httpResponse, Stri...
    method getUserDefaultPassword (line 521) | private String getUserDefaultPassword(String mobile) {
    method loginWithPassword (line 525) | @Override
    method changePassword (line 629) | @Override
    method resetPassword (line 667) | @Override
    method changePassword (line 717) | private UserPassword changePassword(UserPassword up, String password) ...
    method verifyPassword (line 730) | private boolean verifyPassword(UserPassword up, String password) throw...
    method onLoginSuccess (line 743) | private RestResult onLoginSuccess(HttpServletResponse httpResponse, St...
    method sendDestroyCode (line 897) | @Override
    method sendDestroyCode (line 902) | @Override
    method destroy (line 939) | @Override
    method isUsernameAvailable (line 964) | private boolean isUsernameAvailable(String username) {
    method sendPcLoginRequestMessage (line 976) | private void sendPcLoginRequestMessage(String fromUser, String toUser,...
    method sendTextMessage (line 1015) | private void sendTextMessage(String fromUser, String toUser, String te...
    method sendImageMessage (line 1026) | private void sendImageMessage(String fromUser, String toUser, String u...
    method sendMessage (line 1040) | private void sendMessage(String fromUser, Conversation conversation, M...
    method createPcSession (line 1055) | @Override
    method loginWithSession (line 1079) | @Override
    method scanPc (line 1162) | @Override
    method confirmPc (line 1171) | @Override
    method cancelPc (line 1187) | @Override
    method changeName (line 1192) | @Override
    method complain (line 1230) | @Override
    method getGroupAnnouncement (line 1239) | @Override
    method putGroupAnnouncement (line 1254) | @Override
    method saveUserLogs (line 1315) | @Override
    method addDevice (line 1329) | @Override
    method getDeviceList (line 1356) | @Override
    method delDevice (line 1372) | @Override
    method sendUserMessage (line 1411) | @Override
    method uploadMedia (line 1445) | @Override
    method putFavoriteItem (line 1608) | @Override
    method removeFavoriteItems (line 1729) | @Override
    method getFavoriteItems (line 1735) | @Override
    method getGroupMembersForPortrait (line 1748) | @Override

FILE: src/main/java/cn/wildfirechat/app/conference/ConferenceCleanupService.java
  class ConferenceCleanupService (line 18) | @Service
    method cleanupExpiredConferences (line 34) | @Scheduled(fixedRate = 5 * 60 * 1000)

FILE: src/main/java/cn/wildfirechat/app/conference/ConferenceController.java
  class ConferenceController (line 12) | @RestController
    method getUserConferenceId (line 18) | @CrossOrigin
    method getMyConferenceId (line 24) | @CrossOrigin
    method getConferenceInfo (line 30) | @CrossOrigin
    method putConferenceInfo (line 36) | @CrossOrigin
    method createConference (line 42) | @CrossOrigin
    method destroyConference (line 48) | @CrossOrigin
    method recordingConference (line 54) | @CrossOrigin
    method focusConference (line 60) | @CrossOrigin
    method favConference (line 66) | @CrossOrigin
    method unfavConference (line 72) | @CrossOrigin
    method isFavConference (line 78) | @CrossOrigin
    method getFavConferences (line 84) | @CrossOrigin
    method getMyConferenceQuota (line 90) | @CrossOrigin

FILE: src/main/java/cn/wildfirechat/app/conference/ConferenceService.java
  type ConferenceService (line 12) | public interface ConferenceService {
    method getUserConferenceId (line 13) | RestResult getUserConferenceId(String userId);
    method getMyConferenceId (line 14) | RestResult getMyConferenceId();
    method getConferenceInfo (line 15) | RestResult getConferenceInfo(String conferenceId, String password);
    method putConferenceInfo (line 16) | RestResult putConferenceInfo(ConferenceInfo info);
    method createConference (line 17) | RestResult createConference(ConferenceInfo info);
    method destroyConference (line 18) | RestResult destroyConference(String conferenceId);
    method recordingConference (line 19) | RestResult recordingConference(String conferenceId, boolean recording);
    method focusConference (line 20) | RestResult focusConference(String conferenceId, String userId);
    method favConference (line 21) | RestResult favConference(String conferenceId);
    method unfavConference (line 22) | RestResult unfavConference(String conferenceId);
    method getFavConferences (line 23) | RestResult getFavConferences();
    method isFavConference (line 24) | RestResult isFavConference(String conferenceId);
    method getMyConferenceQuota (line 30) | RestResult getMyConferenceQuota();

FILE: src/main/java/cn/wildfirechat/app/conference/ConferenceServiceImpl.java
  class ConferenceServiceImpl (line 33) | @org.springframework.stereotype.Service
    method init (line 61) | @PostConstruct
    method getUserConferenceId (line 66) | @Override
    method getMyConferenceId (line 72) | @Override
    method getPrivateConferenceId (line 79) | private String getPrivateConferenceId(String userId) {
    method getConferenceInfo (line 90) | @Override
    method putConferenceInfo (line 105) | @Override
    method createConference (line 124) | @Override
    method destroyConference (line 211) | @Override
    method recordingConference (line 261) | @Override
    method focusConference (line 294) | @Override
    method favConference (line 313) | @Override
    method unfavConference (line 321) | @Override
    method getFavConferences (line 327) | @Override
    method isFavConference (line 356) | @Override
    method getMyConferenceQuota (line 365) | @Override
    method checkUserQuota (line 416) | private QuotaCheckResult checkUserQuota(String userId, int needMinutes) {
    method createConferenceRecord (line 458) | private void createConferenceRecord(ConferenceInfo info) {
    method endConferenceAndUpdateUsage (line 484) | @Transactional
    method updateQuotaUsage (line 529) | @Transactional
    method calculateDurationMinutes (line 555) | private int calculateDurationMinutes(long startTime, long endTime) {
    method getCurrentYearMonth (line 567) | private String getCurrentYearMonth() {
    method convertConference (line 573) | private ConferenceEntity convertConference(ConferenceInfo info) {
    method convertConference (line 595) | private ConferenceInfo convertConference(ConferenceEntity entity) {
    method getUserId (line 617) | private String getUserId() {
    class QuotaCheckResult (line 625) | private static class QuotaCheckResult {
      method QuotaCheckResult (line 630) | public QuotaCheckResult(boolean enough, int remaining, int total) {
      method isEnough (line 636) | public boolean isEnough() {
      method getRemaining (line 640) | public int getRemaining() {
      method getTotal (line 644) | public int getTotal() {

FILE: src/main/java/cn/wildfirechat/app/jpa/Announcement.java
  class Announcement (line 5) | @Entity
    method getGroupId (line 19) | public String getGroupId() {
    method setGroupId (line 23) | public void setGroupId(String groupId) {
    method getAnnouncement (line 27) | public String getAnnouncement() {
    method setAnnouncement (line 31) | public void setAnnouncement(String announcement) {
    method getAuthor (line 35) | public String getAuthor() {
    method setAuthor (line 39) | public void setAuthor(String author) {
    method getTimestamp (line 43) | public long getTimestamp() {
    method setTimestamp (line 47) | public void setTimestamp(long timestamp) {

FILE: src/main/java/cn/wildfirechat/app/jpa/AnnouncementRepository.java
  type AnnouncementRepository (line 9) | @RepositoryRestResource()

FILE: src/main/java/cn/wildfirechat/app/jpa/ConferenceEntity.java
  class ConferenceEntity (line 5) | @Entity
    method getId (line 26) | public String getId() {
    method setId (line 30) | public void setId(String id) {
    method getConferenceTitle (line 34) | public String getConferenceTitle() {
    method setConferenceTitle (line 38) | public void setConferenceTitle(String conferenceTitle) {
    method getPassword (line 42) | public String getPassword() {
    method setPassword (line 46) | public void setPassword(String password) {
    method getPin (line 50) | public String getPin() {
    method setPin (line 54) | public void setPin(String pin) {
    method getOwner (line 58) | public String getOwner() {
    method setOwner (line 62) | public void setOwner(String owner) {
    method getStartTime (line 66) | public long getStartTime() {
    method setStartTime (line 70) | public void setStartTime(long startTime) {
    method getEndTime (line 74) | public long getEndTime() {
    method setEndTime (line 78) | public void setEndTime(long endTime) {
    method isAudience (line 82) | public boolean isAudience() {
    method setAudience (line 86) | public void setAudience(boolean audience) {
    method isAdvance (line 90) | public boolean isAdvance() {
    method setAdvance (line 94) | public void setAdvance(boolean advance) {
    method isAllowSwitchMode (line 98) | public boolean isAllowSwitchMode() {
    method setAllowSwitchMode (line 102) | public void setAllowSwitchMode(boolean allowSwitchMode) {
    method isNoJoinBeforeStart (line 106) | public boolean isNoJoinBeforeStart() {
    method setNoJoinBeforeStart (line 110) | public void setNoJoinBeforeStart(boolean noJoinBeforeStart) {
    method isRecording (line 114) | public boolean isRecording() {
    method setRecording (line 118) | public void setRecording(boolean recording) {
    method getManages (line 122) | public String getManages() {
    method setManages (line 126) | public void setManages(String manages) {
    method getFocus (line 130) | public String getFocus() {
    method setFocus (line 134) | public void setFocus(String focus) {
    method getMaxParticipants (line 138) | public int getMaxParticipants() {
    method setMaxParticipants (line 142) | public void setMaxParticipants(int maxParticipants) {

FILE: src/main/java/cn/wildfirechat/app/jpa/ConferenceEntityRepository.java
  type ConferenceEntityRepository (line 9) | @RepositoryRestResource()
    method findExpiredConferences (line 17) | @Query("SELECT c FROM ConferenceEntity c WHERE c.endTime > 0 AND c.end...

FILE: src/main/java/cn/wildfirechat/app/jpa/ConferenceRecord.java
  class ConferenceRecord (line 10) | @Entity
    type Status (line 14) | public enum Status {
      method Status (line 20) | Status(int value) {
      method getValue (line 24) | public int getValue() {
    method onCreate (line 62) | @PrePersist
    method onUpdate (line 68) | @PreUpdate
    method ConferenceRecord (line 73) | public ConferenceRecord() {
    method getConferenceId (line 76) | public String getConferenceId() {
    method setConferenceId (line 80) | public void setConferenceId(String conferenceId) {
    method getOwner (line 84) | public String getOwner() {
    method setOwner (line 88) | public void setOwner(String owner) {
    method getStartTime (line 92) | public long getStartTime() {
    method setStartTime (line 96) | public void setStartTime(long startTime) {
    method getEndTime (line 100) | public long getEndTime() {
    method setEndTime (line 104) | public void setEndTime(long endTime) {
    method getPlannedDuration (line 108) | public int getPlannedDuration() {
    method setPlannedDuration (line 112) | public void setPlannedDuration(int plannedDuration) {
    method getActualDuration (line 116) | public int getActualDuration() {
    method setActualDuration (line 120) | public void setActualDuration(int actualDuration) {
    method getStatus (line 124) | public int getStatus() {
    method setStatus (line 128) | public void setStatus(int status) {
    method getYearMonth (line 132) | public String getYearMonth() {
    method setYearMonth (line 136) | public void setYearMonth(String yearMonth) {
    method getCreatedAt (line 140) | public Date getCreatedAt() {
    method setCreatedAt (line 144) | public void setCreatedAt(Date createdAt) {
    method getUpdatedAt (line 148) | public Date getUpdatedAt() {
    method setUpdatedAt (line 152) | public void setUpdatedAt(Date updatedAt) {

FILE: src/main/java/cn/wildfirechat/app/jpa/ConferenceRecordRepository.java
  type ConferenceRecordRepository (line 13) | @Repository
    method findByConferenceId (line 19) | Optional<ConferenceRecord> findByConferenceId(String conferenceId);
    method endConference (line 24) | @Modifying

FILE: src/main/java/cn/wildfirechat/app/jpa/FavoriteItem.java
  class FavoriteItem (line 6) | @Entity

FILE: src/main/java/cn/wildfirechat/app/jpa/FavoriteRepository.java
  type FavoriteRepository (line 9) | @RepositoryRestResource()
    method loadFav (line 12) | @Query(value = "select * from t_favorites where user_id = ?1 and id < ...

FILE: src/main/java/cn/wildfirechat/app/jpa/PCSession.java
  class PCSession (line 10) | @Entity
    type PCSessionStatus (line 13) | public interface PCSessionStatus {
    method getPlatform (line 33) | public int getPlatform() {
    method setPlatform (line 37) | public void setPlatform(int platform) {
    method getToken (line 41) | public String getToken() {
    method setToken (line 45) | public void setToken(String token) {
    method getClientId (line 49) | public String getClientId() {
    method setClientId (line 53) | public void setClientId(String clientId) {
    method getCreateDt (line 57) | public long getCreateDt() {
    method setCreateDt (line 61) | public void setCreateDt(long createDt) {
    method getDuration (line 65) | public long getDuration() {
    method setDuration (line 69) | public void setDuration(long duration) {
    method getStatus (line 73) | public int getStatus() {
    method setStatus (line 77) | public void setStatus(int status) {
    method getConfirmedUserId (line 81) | public String getConfirmedUserId() {
    method setConfirmedUserId (line 85) | public void setConfirmedUserId(String confirmedUserId) {
    method getDevice_name (line 89) | public String getDevice_name() {
    method setDevice_name (line 93) | public void setDevice_name(String device_name) {
    method toOutput (line 97) | public SessionOutput toOutput() {

FILE: src/main/java/cn/wildfirechat/app/jpa/PCSessionRepository.java
  type PCSessionRepository (line 7) | public interface PCSessionRepository extends CrudRepository<PCSession, S...
    method deleteByCreateDtBefore (line 9) | @Modifying

FILE: src/main/java/cn/wildfirechat/app/jpa/Record.java
  class Record (line 8) | @Entity
    method Record (line 22) | public Record(String code, String mobile) {
    method Record (line 30) | public Record() {
    method increaseAndCheck (line 33) | public boolean increaseAndCheck() {
    method reset (line 45) | public void reset() {
    method getRequestCount (line 50) | public int getRequestCount() {
    method getCode (line 54) | public String getCode() {
    method setCode (line 58) | public void setCode(String code) {
    method getMobile (line 62) | public String getMobile() {
    method getTimestamp (line 66) | public long getTimestamp() {
    method setTimestamp (line 70) | public void setTimestamp(long timestamp) {
    method getStartTime (line 74) | public long getStartTime() {
    method setStartTime (line 78) | public void setStartTime(long startTime) {

FILE: src/main/java/cn/wildfirechat/app/jpa/RecordRepository.java
  type RecordRepository (line 5) | public interface RecordRepository extends CrudRepository<Record, String> {

FILE: src/main/java/cn/wildfirechat/app/jpa/ShiroSession.java
  class ShiroSession (line 7) | @Entity
    method ShiroSession (line 19) | public ShiroSession(String sessionId, byte[] sessionData) {
    method ShiroSession (line 24) | public ShiroSession() {
    method getSessionId (line 27) | public String getSessionId() {
    method setSessionId (line 31) | public void setSessionId(String sessionId) {
    method getSessionData (line 35) | public byte[] getSessionData() {
    method setSessionData (line 39) | public void setSessionData(byte[] sessionData) {

FILE: src/main/java/cn/wildfirechat/app/jpa/ShiroSessionRepository.java
  type ShiroSessionRepository (line 5) | public interface ShiroSessionRepository extends CrudRepository<ShiroSess...

FILE: src/main/java/cn/wildfirechat/app/jpa/SlideVerify.java
  class SlideVerify (line 6) | @Entity
    method SlideVerify (line 22) | public SlideVerify() {
    method SlideVerify (line 25) | public SlideVerify(String token, int x, long timestamp) {
    method getToken (line 32) | public String getToken() {
    method setToken (line 36) | public void setToken(String token) {
    method getX (line 40) | public int getX() {
    method setX (line 44) | public void setX(int x) {
    method getTimestamp (line 48) | public long getTimestamp() {
    method setTimestamp (line 52) | public void setTimestamp(long timestamp) {
    method isVerified (line 56) | public boolean isVerified() {
    method setVerified (line 60) | public void setVerified(boolean verified) {
    method isExpired (line 64) | public boolean isExpired(int timeoutSeconds) {

FILE: src/main/java/cn/wildfirechat/app/jpa/SlideVerifyRepository.java
  type SlideVerifyRepository (line 11) | @Repository
    method findByToken (line 14) | Optional<SlideVerify> findByToken(String token);
    method deleteExpired (line 16) | @Modifying

FILE: src/main/java/cn/wildfirechat/app/jpa/UserConference.java
  class UserConference (line 9) | @Entity
    method UserConference (line 24) | public UserConference() {
    method UserConference (line 27) | public UserConference(String userId, String conferenceId) {
    method getUserId (line 33) | public String getUserId() {
    method setUserId (line 37) | public void setUserId(String userId) {
    method getConferenceId (line 41) | public String getConferenceId() {
    method setConferenceId (line 45) | public void setConferenceId(String conferenceId) {

FILE: src/main/java/cn/wildfirechat/app/jpa/UserConferenceQuota.java
  class UserConferenceQuota (line 10) | @Entity
    method onCreate (line 29) | @PrePersist
    method onUpdate (line 35) | @PreUpdate
    method UserConferenceQuota (line 40) | public UserConferenceQuota() {
    method UserConferenceQuota (line 43) | public UserConferenceQuota(String userId, int totalMinutes) {
    method getUserId (line 48) | public String getUserId() {
    method setUserId (line 52) | public void setUserId(String userId) {
    method getTotalMinutes (line 56) | public int getTotalMinutes() {
    method setTotalMinutes (line 60) | public void setTotalMinutes(int totalMinutes) {
    method getCreatedAt (line 64) | public Date getCreatedAt() {
    method setCreatedAt (line 68) | public void setCreatedAt(Date createdAt) {
    method getUpdatedAt (line 72) | public Date getUpdatedAt() {
    method setUpdatedAt (line 76) | public void setUpdatedAt(Date updatedAt) {

FILE: src/main/java/cn/wildfirechat/app/jpa/UserConferenceQuotaRepository.java
  type UserConferenceQuotaRepository (line 11) | @Repository
    method findByUserId (line 17) | Optional<UserConferenceQuota> findByUserId(String userId);

FILE: src/main/java/cn/wildfirechat/app/jpa/UserConferenceRepository.java
  type UserConferenceRepository (line 13) | @RepositoryRestResource()
    method deleteByUserIdAndConferenceId (line 15) | @Transactional
    method deleteByConferenceId (line 20) | @Transactional
    method findByUserId (line 25) | @Query(value = "select c.* from user_conference uc, conference c where...
    method findByUserIdAndConferenceId (line 28) | Optional<UserConference> findByUserIdAndConferenceId(String userId, St...

FILE: src/main/java/cn/wildfirechat/app/jpa/UserNameEntry.java
  class UserNameEntry (line 5) | @Entity
    method getId (line 13) | public Integer getId() {
    method setId (line 17) | public void setId(Integer id) {

FILE: src/main/java/cn/wildfirechat/app/jpa/UserNameRepository.java
  type UserNameRepository (line 6) | @RepositoryRestResource()

FILE: src/main/java/cn/wildfirechat/app/jpa/UserPassword.java
  class UserPassword (line 8) | @Entity
    method UserPassword (line 27) | public UserPassword() {
    method UserPassword (line 30) | public UserPassword(String userId) {
    method UserPassword (line 34) | public UserPassword(String userId, String password, String salt) {
    method UserPassword (line 43) | public UserPassword(String userId, String password, String salt, Strin...
    method getUserId (line 53) | public String getUserId() {
    method setUserId (line 57) | public void setUserId(String userId) {
    method getPassword (line 61) | public String getPassword() {
    method setPassword (line 65) | public void setPassword(String password) {
    method getSalt (line 69) | public String getSalt() {
    method setSalt (line 73) | public void setSalt(String salt) {
    method getResetCode (line 77) | public String getResetCode() {
    method setResetCode (line 81) | public void setResetCode(String resetCode) {
    method getResetCodeTime (line 85) | public long getResetCodeTime() {
    method setResetCodeTime (line 89) | public void setResetCodeTime(long resetCodeTime) {
    method getTryCount (line 93) | public int getTryCount() {
    method setTryCount (line 97) | public void setTryCount(int tryCount) {
    method getLastTryTime (line 101) | public long getLastTryTime() {
    method setLastTryTime (line 105) | public void setLastTryTime(long lastTryTime) {

FILE: src/main/java/cn/wildfirechat/app/jpa/UserPasswordRepository.java
  type UserPasswordRepository (line 7) | @RepositoryRestResource()

FILE: src/main/java/cn/wildfirechat/app/jpa/UserPrivateConferenceId.java
  class UserPrivateConferenceId (line 8) | @Entity
    method UserPrivateConferenceId (line 17) | public UserPrivateConferenceId() {
    method UserPrivateConferenceId (line 20) | public UserPrivateConferenceId(String userId, String conferenceId) {
    method getUserId (line 25) | public String getUserId() {
    method setUserId (line 29) | public void setUserId(String userId) {
    method getConferenceId (line 33) | public String getConferenceId() {
    method setConferenceId (line 37) | public void setConferenceId(String conferenceId) {

FILE: src/main/java/cn/wildfirechat/app/jpa/UserPrivateConferenceIdRepository.java
  type UserPrivateConferenceIdRepository (line 6) | @RepositoryRestResource()

FILE: src/main/java/cn/wildfirechat/app/jpa/UserQuotaUsage.java
  class UserQuotaUsage (line 10) | @Entity
    method onCreate (line 35) | @PrePersist
    method onUpdate (line 41) | @PreUpdate
    method UserQuotaUsage (line 46) | public UserQuotaUsage() {
    method UserQuotaUsage (line 49) | public UserQuotaUsage(String userId, String yearMonth, int usedMinutes) {
    method getId (line 55) | public Long getId() {
    method setId (line 59) | public void setId(Long id) {
    method getUserId (line 63) | public String getUserId() {
    method setUserId (line 67) | public void setUserId(String userId) {
    method getYearMonth (line 71) | public String getYearMonth() {
    method setYearMonth (line 75) | public void setYearMonth(String yearMonth) {
    method getUsedMinutes (line 79) | public int getUsedMinutes() {
    method setUsedMinutes (line 83) | public void setUsedMinutes(int usedMinutes) {
    method getCreatedAt (line 87) | public Date getCreatedAt() {
    method setCreatedAt (line 91) | public void setCreatedAt(Date createdAt) {
    method getUpdatedAt (line 95) | public Date getUpdatedAt() {
    method setUpdatedAt (line 99) | public void setUpdatedAt(Date updatedAt) {

FILE: src/main/java/cn/wildfirechat/app/jpa/UserQuotaUsageRepository.java
  type UserQuotaUsageRepository (line 13) | @Repository
    method findByUserIdAndYearMonth (line 19) | Optional<UserQuotaUsage> findByUserIdAndYearMonth(String userId, Strin...
    method addUsedMinutes (line 24) | @Modifying

FILE: src/main/java/cn/wildfirechat/app/model/ConferenceDTO.java
  type ConferenceDTO (line 3) | public interface ConferenceDTO {
    method getId (line 4) | String getId();
    method getConference_title (line 5) | String getConference_title();
    method getPassword (line 6) | String getPassword();
    method getPin (line 7) | String getPin();
    method getOwner (line 8) | String getOwner();
    method getManages (line 9) | public String getManages();
    method getStart_time (line 10) | long getStart_time();
    method getEnd_time (line 11) | long getEnd_time();
    method isAudience (line 12) | boolean isAudience();
    method isAdvance (line 13) | boolean isAdvance();
    method isAllow_switch_mode (line 14) | boolean isAllow_switch_mode();
    method isNo_join_before_start (line 15) | boolean isNo_join_before_start();
    method isRecording (line 16) | boolean isRecording();
    method getFocus (line 17) | String getFocus();
    method getMax_participants (line 18) | int getMax_participants();

FILE: src/main/java/cn/wildfirechat/app/pojo/CancelSessionRequest.java
  class CancelSessionRequest (line 3) | public class CancelSessionRequest {
    method getToken (line 6) | public String getToken() {
    method setToken (line 10) | public void setToken(String token) {

FILE: src/main/java/cn/wildfirechat/app/pojo/ChangeNameRequest.java
  class ChangeNameRequest (line 3) | public class ChangeNameRequest {
    method getNewName (line 6) | public String getNewName() {
    method setNewName (line 10) | public void setNewName(String newName) {

FILE: src/main/java/cn/wildfirechat/app/pojo/ChangePasswordRequest.java
  class ChangePasswordRequest (line 3) | public class ChangePasswordRequest {
    method getOldPassword (line 8) | public String getOldPassword() {
    method setOldPassword (line 12) | public void setOldPassword(String oldPassword) {
    method getNewPassword (line 16) | public String getNewPassword() {
    method setNewPassword (line 20) | public void setNewPassword(String newPassword) {
    method getSlideVerifyToken (line 24) | public String getSlideVerifyToken() {
    method setSlideVerifyToken (line 28) | public void setSlideVerifyToken(String slideVerifyToken) {

FILE: src/main/java/cn/wildfirechat/app/pojo/ComplainRequest.java
  class ComplainRequest (line 3) | public class ComplainRequest {

FILE: src/main/java/cn/wildfirechat/app/pojo/ConferenceInfo.java
  class ConferenceInfo (line 5) | public class ConferenceInfo {

FILE: src/main/java/cn/wildfirechat/app/pojo/ConferenceInfoRequest.java
  class ConferenceInfoRequest (line 3) | public class ConferenceInfoRequest {

FILE: src/main/java/cn/wildfirechat/app/pojo/ConferenceQuotaResponse.java
  class ConferenceQuotaResponse (line 6) | public class ConferenceQuotaResponse {
    method ConferenceQuotaResponse (line 23) | public ConferenceQuotaResponse() {
    method getTotalQuota (line 26) | public int getTotalQuota() {
    method setTotalQuota (line 30) | public void setTotalQuota(int totalQuota) {
    method getUsedMinutes (line 34) | public int getUsedMinutes() {
    method setUsedMinutes (line 38) | public void setUsedMinutes(int usedMinutes) {
    method getRemainingMinutes (line 42) | public int getRemainingMinutes() {
    method setRemainingMinutes (line 46) | public void setRemainingMinutes(int remainingMinutes) {
    method isUnlimited (line 50) | public boolean isUnlimited() {
    method setUnlimited (line 54) | public void setUnlimited(boolean unlimited) {
    method getYearMonth (line 58) | public String getYearMonth() {
    method setYearMonth (line 62) | public void setYearMonth(String yearMonth) {

FILE: src/main/java/cn/wildfirechat/app/pojo/ConfirmSessionRequest.java
  class ConfirmSessionRequest (line 3) | public class ConfirmSessionRequest {
    method getToken (line 9) | public String getToken() {
    method setToken (line 13) | public void setToken(String token) {
    method getUser_id (line 17) | public String getUser_id() {
    method setUser_id (line 21) | public void setUser_id(String user_id) {
    method getQuick_login (line 25) | public int getQuick_login() {
    method setQuick_login (line 29) | public void setQuick_login(int quick_login) {

FILE: src/main/java/cn/wildfirechat/app/pojo/CreateSessionRequest.java
  class CreateSessionRequest (line 3) | public class CreateSessionRequest {
    method getPlatform (line 12) | public int getPlatform() {
    method setPlatform (line 16) | public void setPlatform(int platform) {
    method getToken (line 20) | public String getToken() {
    method setToken (line 24) | public void setToken(String token) {
    method getDevice_name (line 28) | public String getDevice_name() {
    method setDevice_name (line 32) | public void setDevice_name(String device_name) {
    method getClientId (line 36) | public String getClientId() {
    method setClientId (line 40) | public void setClientId(String clientId) {
    method getFlag (line 44) | public int getFlag() {
    method setFlag (line 48) | public void setFlag(int flag) {
    method getUserId (line 52) | public String getUserId() {

FILE: src/main/java/cn/wildfirechat/app/pojo/DestroyRequest.java
  class DestroyRequest (line 3) | public class DestroyRequest {
    method getCode (line 6) | public String getCode() {
    method setCode (line 10) | public void setCode(String code) {

FILE: src/main/java/cn/wildfirechat/app/pojo/GroupAnnouncementPojo.java
  class GroupAnnouncementPojo (line 3) | public class GroupAnnouncementPojo {

FILE: src/main/java/cn/wildfirechat/app/pojo/GroupIdPojo.java
  class GroupIdPojo (line 3) | public class GroupIdPojo {

FILE: src/main/java/cn/wildfirechat/app/pojo/LoadFavoriteRequest.java
  class LoadFavoriteRequest (line 3) | public class LoadFavoriteRequest {

FILE: src/main/java/cn/wildfirechat/app/pojo/LoadFavoriteResponse.java
  class LoadFavoriteResponse (line 7) | public class LoadFavoriteResponse {

FILE: src/main/java/cn/wildfirechat/app/pojo/LoginResponse.java
  class LoginResponse (line 3) | public class LoginResponse {
    method getUserId (line 11) | public String getUserId() {
    method setUserId (line 15) | public void setUserId(String userId) {
    method getToken (line 19) | public String getToken() {
    method setToken (line 23) | public void setToken(String token) {
    method isRegister (line 27) | public boolean isRegister() {
    method setRegister (line 31) | public void setRegister(boolean register) {
    method getUserName (line 35) | public String getUserName() {
    method setUserName (line 39) | public void setUserName(String userName) {
    method getPortrait (line 43) | public String getPortrait() {
    method setPortrait (line 47) | public void setPortrait(String portrait) {
    method getResetCode (line 51) | public String getResetCode() {
    method setResetCode (line 55) | public void setResetCode(String resetCode) {

FILE: src/main/java/cn/wildfirechat/app/pojo/PhoneCodeLoginRequest.java
  class PhoneCodeLoginRequest (line 3) | public class PhoneCodeLoginRequest {
    method getClientId (line 9) | public String getClientId() {
    method setClientId (line 13) | public void setClientId(String clientId) {
    method getMobile (line 17) | public String getMobile() {
    method setMobile (line 21) | public void setMobile(String mobile) {
    method getCode (line 25) | public String getCode() {
    method getPlatform (line 29) | public Integer getPlatform() {
    method setPlatform (line 33) | public void setPlatform(Integer platform) {
    method setCode (line 37) | public void setCode(String code) {

FILE: src/main/java/cn/wildfirechat/app/pojo/PhoneCodeLoginRequestWithSlideVerify.java
  class PhoneCodeLoginRequestWithSlideVerify (line 3) | public class PhoneCodeLoginRequestWithSlideVerify {
    method getMobile (line 10) | public String getMobile() {
    method setMobile (line 14) | public void setMobile(String mobile) {
    method getCode (line 18) | public String getCode() {
    method setCode (line 22) | public void setCode(String code) {
    method getClientId (line 26) | public String getClientId() {
    method setClientId (line 30) | public void setClientId(String clientId) {
    method getPlatform (line 34) | public Integer getPlatform() {
    method setPlatform (line 38) | public void setPlatform(Integer platform) {
    method getSlideVerifyToken (line 42) | public String getSlideVerifyToken() {
    method setSlideVerifyToken (line 46) | public void setSlideVerifyToken(String slideVerifyToken) {

FILE: src/main/java/cn/wildfirechat/app/pojo/RecordingRequest.java
  class RecordingRequest (line 3) | public class RecordingRequest {

FILE: src/main/java/cn/wildfirechat/app/pojo/ResetPasswordRequest.java
  class ResetPasswordRequest (line 3) | public class ResetPasswordRequest {
    method getMobile (line 8) | public String getMobile() {
    method setMobile (line 12) | public void setMobile(String mobile) {
    method getResetCode (line 16) | public String getResetCode() {
    method setResetCode (line 20) | public void setResetCode(String resetCode) {
    method getNewPassword (line 24) | public String getNewPassword() {
    method setNewPassword (line 28) | public void setNewPassword(String newPassword) {

FILE: src/main/java/cn/wildfirechat/app/pojo/SendCodeRequest.java
  class SendCodeRequest (line 3) | public class SendCodeRequest {
    method getMobile (line 7) | public String getMobile() {
    method setMobile (line 11) | public void setMobile(String mobile) {
    method getSlideVerifyToken (line 15) | public String getSlideVerifyToken() {
    method setSlideVerifyToken (line 19) | public void setSlideVerifyToken(String slideVerifyToken) {

FILE: src/main/java/cn/wildfirechat/app/pojo/SendCodeRequestWithSlideVerify.java
  class SendCodeRequestWithSlideVerify (line 3) | public class SendCodeRequestWithSlideVerify {
    method getMobile (line 7) | public String getMobile() {
    method setMobile (line 11) | public void setMobile(String mobile) {
    method getSlideVerifyToken (line 15) | public String getSlideVerifyToken() {
    method setSlideVerifyToken (line 19) | public void setSlideVerifyToken(String slideVerifyToken) {

FILE: src/main/java/cn/wildfirechat/app/pojo/SendDestroyCodeRequest.java
  class SendDestroyCodeRequest (line 3) | public class SendDestroyCodeRequest {
    method getSlideVerifyToken (line 6) | public String getSlideVerifyToken() {
    method setSlideVerifyToken (line 10) | public void setSlideVerifyToken(String slideVerifyToken) {

FILE: src/main/java/cn/wildfirechat/app/pojo/SendMessageRequest.java
  class SendMessageRequest (line 5) | public class SendMessageRequest {

FILE: src/main/java/cn/wildfirechat/app/pojo/SessionOutput.java
  class SessionOutput (line 3) | public class SessionOutput {
    method getUserId (line 11) | public String getUserId() {
    method setUserId (line 15) | public void setUserId(String userId) {
    method SessionOutput (line 19) | public SessionOutput() {
    method SessionOutput (line 22) | public SessionOutput(String userId, String token, int status, long exp...
    method getToken (line 31) | public String getToken() {
    method setToken (line 35) | public void setToken(String token) {
    method getStatus (line 39) | public int getStatus() {
    method setStatus (line 43) | public void setStatus(int status) {
    method getExpired (line 47) | public long getExpired() {
    method setExpired (line 51) | public void setExpired(long expired) {
    method getDevice_name (line 55) | public String getDevice_name() {
    method setDevice_name (line 59) | public void setDevice_name(String device_name) {
    method getPlatform (line 63) | public int getPlatform() {
    method setPlatform (line 67) | public void setPlatform(int platform) {

FILE: src/main/java/cn/wildfirechat/app/pojo/SlideVerifyRequest.java
  class SlideVerifyRequest (line 3) | public class SlideVerifyRequest {
    method getToken (line 7) | public String getToken() {
    method setToken (line 11) | public void setToken(String token) {
    method getX (line 15) | public int getX() {
    method setX (line 19) | public void setX(int x) {

FILE: src/main/java/cn/wildfirechat/app/pojo/SlideVerifyResponse.java
  class SlideVerifyResponse (line 3) | public class SlideVerifyResponse {
    method getToken (line 9) | public String getToken() {
    method setToken (line 13) | public void setToken(String token) {
    method getBackgroundImage (line 17) | public String getBackgroundImage() {
    method setBackgroundImage (line 21) | public void setBackgroundImage(String backgroundImage) {
    method getSliderImage (line 25) | public String getSliderImage() {
    method setSliderImage (line 29) | public void setSliderImage(String sliderImage) {
    method getY (line 33) | public int getY() {
    method setY (line 37) | public void setY(int y) {

FILE: src/main/java/cn/wildfirechat/app/pojo/UploadFileResponse.java
  class UploadFileResponse (line 3) | public class UploadFileResponse {

FILE: src/main/java/cn/wildfirechat/app/pojo/UserIdNamePortraitPojo.java
  class UserIdNamePortraitPojo (line 3) | public class UserIdNamePortraitPojo {
    method UserIdNamePortraitPojo (line 8) | public UserIdNamePortraitPojo() {
    method UserIdNamePortraitPojo (line 11) | public UserIdNamePortraitPojo(String userId, String name, String portr...

FILE: src/main/java/cn/wildfirechat/app/pojo/UserIdPojo.java
  class UserIdPojo (line 3) | public class UserIdPojo {

FILE: src/main/java/cn/wildfirechat/app/pojo/UserPasswordLoginRequest.java
  class UserPasswordLoginRequest (line 3) | public class UserPasswordLoginRequest {
    method getClientId (line 9) | public String getClientId() {
    method setClientId (line 13) | public void setClientId(String clientId) {
    method getMobile (line 17) | public String getMobile() {
    method setMobile (line 21) | public void setMobile(String mobile) {
    method getPassword (line 25) | public String getPassword() {
    method getPlatform (line 29) | public Integer getPlatform() {
    method setPlatform (line 33) | public void setPlatform(Integer platform) {
    method setPassword (line 37) | public void setPassword(String password) {

FILE: src/main/java/cn/wildfirechat/app/pojo/UserPasswordLoginRequestWithSlideVerify.java
  class UserPasswordLoginRequestWithSlideVerify (line 3) | public class UserPasswordLoginRequestWithSlideVerify {
    method getMobile (line 10) | public String getMobile() {
    method setMobile (line 14) | public void setMobile(String mobile) {
    method getPassword (line 18) | public String getPassword() {
    method setPassword (line 22) | public void setPassword(String password) {
    method getClientId (line 26) | public String getClientId() {
    method setClientId (line 30) | public void setClientId(String clientId) {
    method getPlatform (line 34) | public Integer getPlatform() {
    method setPlatform (line 38) | public void setPlatform(Integer platform) {
    method getSlideVerifyToken (line 42) | public String getSlideVerifyToken() {
    method setSlideVerifyToken (line 46) | public void setSlideVerifyToken(String slideVerifyToken) {

FILE: src/main/java/cn/wildfirechat/app/shiro/AuthDataSource.java
  class AuthDataSource (line 25) | @Service
    method insertRecord (line 37) | public RestResult.RestCode insertRecord(String mobile, String code) {
    method clearRecode (line 63) | public void clearRecode(String mobile) {
    method verifyCode (line 70) | public RestResult.RestCode verifyCode(String mobile, String code) {
    method createSession (line 90) | public PCSession createSession(String userId, String clientId, String ...
    method getSession (line 109) | public PCSession getSession(String token, boolean clear) {
    method saveSession (line 117) | public void saveSession(PCSession session) {
    method scanPc (line 121) | public RestResult scanPc(String userId, String token) {
    method confirmPc (line 142) | public RestResult confirmPc(String userId, String token) {
    method cancelPc (line 162) | public RestResult cancelPc(String token) {
    method checkPcSession (line 173) | public RestResult.RestCode checkPcSession(String token) {
    method getUserId (line 193) | public String getUserId(String token, boolean clear) {

FILE: src/main/java/cn/wildfirechat/app/shiro/CorsFilter.java
  class CorsFilter (line 13) | @Component
    method doFilterInternal (line 15) | @Override

FILE: src/main/java/cn/wildfirechat/app/shiro/DBSessionDao.java
  class DBSessionDao (line 19) | @Component
    method create (line 26) | @Override
    method readSession (line 33) | @Override
    method update (line 44) | @Override
    method delete (line 52) | @Override
    method getActiveSessions (line 57) | @Override
    method sessionToByte (line 63) | private byte[] sessionToByte(Session session){
    method byteToSession (line 77) | private Session byteToSession(byte[] bytes){

FILE: src/main/java/cn/wildfirechat/app/shiro/JsonAuthLoginFilter.java
  class JsonAuthLoginFilter (line 16) | public class JsonAuthLoginFilter extends AccessControlFilter {
    method isAccessAllowed (line 18) | @Override
    method onAccessDenied (line 41) | @Override

FILE: src/main/java/cn/wildfirechat/app/shiro/LdapMatcher.java
  class LdapMatcher (line 19) | @Service
    method doCredentialsMatch (line 22) | @Override
    method authenticate (line 46) | public static boolean authenticate(String ldapUrl, String dn, String p...
    method main (line 69) | public static void main(String[] args) {

FILE: src/main/java/cn/wildfirechat/app/shiro/LdapRealm.java
  class LdapRealm (line 20) | @Service
    method initMatcher (line 25) | @PostConstruct
    method doGetAuthorizationInfo (line 30) | @Override
    method supports (line 40) | @Override
    method doGetAuthenticationInfo (line 47) | @Override

FILE: src/main/java/cn/wildfirechat/app/shiro/LdapToken.java
  class LdapToken (line 5) | public class LdapToken implements AuthenticationToken {
    method LdapToken (line 10) | public LdapToken(String phone, String password, String ldapUrl) {
    method getPrincipal (line 16) | @Override
    method getCredentials (line 21) | @Override
    method getLdapUrl (line 26) | public String getLdapUrl() {

FILE: src/main/java/cn/wildfirechat/app/shiro/PhoneCodeRealm.java
  class PhoneCodeRealm (line 15) | @Service
    method initRealm (line 21) | @PostConstruct
    method doGetAuthorizationInfo (line 26) | @Override
    method doGetAuthenticationInfo (line 36) | @Override

FILE: src/main/java/cn/wildfirechat/app/shiro/PhoneCodeToken.java
  class PhoneCodeToken (line 5) | public class PhoneCodeToken implements AuthenticationToken {
    method PhoneCodeToken (line 9) | public PhoneCodeToken(String phone, String code) {
    method getPrincipal (line 14) | @Override
    method getCredentials (line 19) | @Override

FILE: src/main/java/cn/wildfirechat/app/shiro/ScanCodeRealm.java
  class ScanCodeRealm (line 15) | @Service
    method initMatcher (line 24) | @PostConstruct
    method doGetAuthorizationInfo (line 29) | @Override
    method supports (line 39) | @Override
    method doGetAuthenticationInfo (line 46) | @Override

FILE: src/main/java/cn/wildfirechat/app/shiro/ShiroConfig.java
  class ShiroConfig (line 21) | @Configuration
    method shiroFilter (line 42) | @Bean(name = "shiroFilter")
    method securityManager (line 90) | @Bean

FILE: src/main/java/cn/wildfirechat/app/shiro/ShiroSessionManager.java
  class ShiroSessionManager (line 13) | public class ShiroSessionManager extends DefaultWebSessionManager {
    method ShiroSessionManager (line 19) | public ShiroSessionManager(){
    method getSessionId (line 23) | @Override

FILE: src/main/java/cn/wildfirechat/app/shiro/TokenAuthenticationToken.java
  class TokenAuthenticationToken (line 5) | public class TokenAuthenticationToken implements AuthenticationToken {
    method TokenAuthenticationToken (line 8) | public TokenAuthenticationToken(String token) {
    method getToken (line 12) | public String getToken() {
    method setToken (line 16) | public void setToken(String token) {
    method getPrincipal (line 20) | @Override
    method getCredentials (line 25) | @Override

FILE: src/main/java/cn/wildfirechat/app/shiro/TokenMatcher.java
  class TokenMatcher (line 14) | @Service
    method doCredentialsMatch (line 19) | @Override
    method main (line 31) | public static void main(String[] args) {

FILE: src/main/java/cn/wildfirechat/app/shiro/UserPasswordRealm.java
  class UserPasswordRealm (line 24) | @Service
    method initMatcher (line 29) | @PostConstruct
    method doGetAuthorizationInfo (line 35) | @Override
    method doGetAuthenticationInfo (line 45) | @Override

FILE: src/main/java/cn/wildfirechat/app/slide/SlideVerifyCleanupService.java
  class SlideVerifyCleanupService (line 8) | @Service
    method cleanupExpired (line 14) | @Transactional

FILE: src/main/java/cn/wildfirechat/app/slide/SlideVerifyService.java
  class SlideVerifyService (line 21) | @Service
    method generateSlideVerify (line 43) | public Map<String, Object> generateSlideVerify() {
    method verifySlide (line 83) | public boolean verifySlide(String token, int userX) {
    method isVerified (line 121) | public boolean isVerified(String token) {
    method cleanExpiredData (line 140) | public void cleanExpiredData() {
    method generateBackgroundWithHole (line 148) | private String generateBackgroundWithHole(int x, int y) throws IOExcep...
    method generateSlider (line 175) | private String generateSlider(int x, int y) throws IOException {
    method drawHole (line 194) | private void drawHole(Graphics2D g, int x, int y) {
    method drawSliderShape (line 210) | private void drawSliderShape(Graphics2D g) {
    method addRandomShapes (line 231) | private void addRandomShapes(Graphics2D g) {
    method getRandomColor (line 256) | private Color getRandomColor() {
    method imageToBase64 (line 267) | private String imageToBase64(BufferedImage image) throws IOException {

FILE: src/main/java/cn/wildfirechat/app/sms/AliyunSMSConfig.java
  class AliyunSMSConfig (line 7) | @Configuration
    method getAccessKeyId (line 16) | public String getAccessKeyId() {
    method setAccessKeyId (line 20) | public void setAccessKeyId(String accessKeyId) {
    method getAccessSecret (line 24) | public String getAccessSecret() {
    method setAccessSecret (line 28) | public void setAccessSecret(String accessSecret) {
    method getSignName (line 32) | public String getSignName() {
    method setSignName (line 36) | public void setSignName(String signName) {
    method getTemplateCode (line 40) | public String getTemplateCode() {
    method setTemplateCode (line 44) | public void setTemplateCode(String templateCode) {

FILE: src/main/java/cn/wildfirechat/app/sms/SmsService.java
  type SmsService (line 6) | public interface SmsService {
    method sendCode (line 7) | RestResult.RestCode sendCode(String mobile, String code);

FILE: src/main/java/cn/wildfirechat/app/sms/SmsServiceImpl.java
  class SmsServiceImpl (line 28) | @Service
    class AliyunCommonResponse (line 33) | private static class AliyunCommonResponse {
    method sendCode (line 47) | @Override
    method sendTencentCode (line 58) | private RestResult.RestCode sendTencentCode(String mobile, String code) {
    method sendAliyunCode (line 152) | private RestResult.RestCode sendAliyunCode(String mobile, String code) {

FILE: src/main/java/cn/wildfirechat/app/sms/TencentSMSConfig.java
  class TencentSMSConfig (line 7) | @Configuration
    method getSecretId (line 17) | public String getSecretId() {
    method setSecretId (line 21) | public void setSecretId(String secretId) {
    method getSecretKey (line 25) | public String getSecretKey() {
    method setSecretKey (line 29) | public void setSecretKey(String secretKey) {
    method getAppId (line 33) | public String getAppId() {
    method setAppId (line 37) | public void setAppId(String appId) {
    method getTemplateId (line 41) | public String getTemplateId() {
    method setTemplateId (line 45) | public void setTemplateId(String templateId) {
    method getSign (line 49) | public String getSign() {
    method setSign (line 53) | public void setSign(String sign) {

FILE: src/main/java/cn/wildfirechat/app/tools/LdapUser.java
  class LdapUser (line 3) | public class LdapUser {
    method LdapUser (line 5) | public LdapUser(String uid, String cn, String mail, String phone, Stri...
    method toString (line 8) | @Override public String toString() {

FILE: src/main/java/cn/wildfirechat/app/tools/LdapUtil.java
  class LdapUtil (line 13) | public class LdapUtil {
    method findUserByPhone (line 16) | public static List<LdapUser> findUserByPhone(String phone, String ldap...
    method encodeSshaPassword (line 59) | private static String encodeSshaPassword(String password) {
    method getAttr (line 84) | private static String getAttr(Attributes attrs, String name) throws Na...

FILE: src/main/java/cn/wildfirechat/app/tools/NumericIdGenerator.java
  class NumericIdGenerator (line 7) | public class NumericIdGenerator {
    method getId (line 8) | public static String getId(List<Integer> firstNumber, List<Integer> fi...
    method main (line 38) | public static void main(String[] args) {

FILE: src/main/java/cn/wildfirechat/app/tools/OrderedIdUserNameGenerator.java
  class OrderedIdUserNameGenerator (line 8) | @Component
    method getUserName (line 13) | @Override

FILE: src/main/java/cn/wildfirechat/app/tools/PhoneNumberUserNameGenerator.java
  class PhoneNumberUserNameGenerator (line 5) | @Component
    method getUserName (line 7) | @Override

FILE: src/main/java/cn/wildfirechat/app/tools/RateLimiter.java
  class RateLimiter (line 12) | public class RateLimiter {
    method RateLimiter (line 24) | public RateLimiter() {
    method RateLimiter (line 28) | public RateLimiter(int limitTimeSecond, int limitCount) {
    method isGranted (line 39) | public boolean isGranted(String userId) {
    method cleanUp (line 71) | private void cleanUp(long current) {
    method main (line 84) | public static void main(String[] args) throws InterruptedException {

FILE: src/main/java/cn/wildfirechat/app/tools/ShortUUIDGenerator.java
  class ShortUUIDGenerator (line 9) | @Component
    method getUserName (line 16) | @Override
    method getShortUUID (line 21) | public String getShortUUID() {
    method main (line 31) | public static void main(String[] args) {

FILE: src/main/java/cn/wildfirechat/app/tools/SpinLock.java
  class SpinLock (line 5) | public class SpinLock {
    method lock (line 9) | public void lock() {
    method unLock (line 17) | public void unLock() {

FILE: src/main/java/cn/wildfirechat/app/tools/UUIDUserNameGenerator.java
  class UUIDUserNameGenerator (line 7) | @Component
    method getUserName (line 9) | @Override

FILE: src/main/java/cn/wildfirechat/app/tools/UserNameGenerator.java
  type UserNameGenerator (line 3) | public interface UserNameGenerator {
    method getUserName (line 4) | String getUserName(String phone);

FILE: src/main/java/cn/wildfirechat/app/tools/Utils.java
  class Utils (line 9) | public class Utils {
    method getRandomCode (line 10) | public static String getRandomCode(int length) {
    method isMobile (line 17) | public static boolean isMobile(String mobile) {
    method getSafeFileName (line 29) | public static String getSafeFileName(String fileName) {
    method main (line 47) | public static void main(String[] args) {

FILE: src/test/java/cn/wildfirechat/app/ApplicationTests.java
  class ApplicationTests (line 26) | @RunWith(SpringRunner.class)
    method contextLoads (line 30) | @Test
    class H2TestConfig (line 36) | @TestConfiguration
      method dataSource (line 38) | @Bean

FILE: src/test/java/cn/wildfirechat/app/jpa/AnnouncementTest.java
  class AnnouncementTest (line 7) | public class AnnouncementTest {
    method testConstructorAndGetters (line 9) | @Test
    method testEmptyAnnouncement (line 31) | @Test
    method testLongContent (line 43) | @Test

FILE: src/test/java/cn/wildfirechat/app/jpa/ConferenceEntityTest.java
  class ConferenceEntityTest (line 7) | public class ConferenceEntityTest {
    method testDefaultValues (line 9) | @Test
    method testSettersAndGetters (line 22) | @Test
    method testBooleanFlags (line 63) | @Test
    method testMaxParticipantsBoundary (line 89) | @Test

FILE: src/test/java/cn/wildfirechat/app/jpa/FavoriteItemTest.java
  class FavoriteItemTest (line 7) | public class FavoriteItemTest {
    method testDefaultConstructor (line 9) | @Test
    method testSettersAndGetters (line 24) | @Test
    method testDifferentTypes (line 61) | @Test
    method testLongUrls (line 74) | @Test

FILE: src/test/java/cn/wildfirechat/app/jpa/PCSessionTest.java
  class PCSessionTest (line 7) | public class PCSessionTest {
    method testDefaultConstructor (line 9) | @Test
    method testSettersAndGetters (line 22) | @Test
    method testSessionStatusConstants (line 49) | @Test
    method testStatusTransitions (line 58) | @Test
    method testPlatformValues (line 80) | @Test

FILE: src/test/java/cn/wildfirechat/app/jpa/RecordTest.java
  class RecordTest (line 7) | public class RecordTest {
    method testDefaultConstructor (line 9) | @Test
    method testConstructorWithParams (line 20) | @Test
    method testIncreaseAndCheckWithinLimit (line 36) | @Test
    method testIncreaseAndCheckExceedsLimit (line 50) | @Test
    method testReset (line 65) | @Test
    method testSettersAndGetters (line 82) | @Test
    method testResetAfter24Hours (line 98) | @Test

FILE: src/test/java/cn/wildfirechat/app/jpa/ShiroSessionTest.java
  class ShiroSessionTest (line 9) | public class ShiroSessionTest {
    method testDefaultConstructor (line 11) | @Test
    method testConstructorWithParams (line 21) | @Test
    method testSetSessionData (line 35) | @Test
    method testEmptySessionData (line 50) | @Test
    method testBinarySessionData (line 60) | @Test
    method testLargeSessionData (line 74) | @Test

FILE: src/test/java/cn/wildfirechat/app/jpa/SlideVerifyRepositoryTest.java
  class SlideVerifyRepositoryTest (line 21) | @RunWith(MockitoJUnitRunner.class)
    method setUp (line 29) | @Before
    method testFindByTokenExists (line 41) | @Test
    method testFindByTokenNotExists (line 57) | @Test
    method testSave (line 70) | @Test
    method testDelete (line 84) | @Test
    method testCleanupExpired (line 96) | @Test
    method testUpdateVerified (line 109) | @Test

FILE: src/test/java/cn/wildfirechat/app/jpa/SlideVerifyTest.java
  class SlideVerifyTest (line 7) | public class SlideVerifyTest {
    method testConstructor (line 9) | @Test
    method testSettersAndGetters (line 26) | @Test
    method testIsExpiredNotExpired (line 48) | @Test
    method testIsExpiredJustExpired (line 61) | @Test
    method testIsExpiredLongAgo (line 74) | @Test
    method testDefaultConstructor (line 87) | @Test
    method testVerifiedStateToggle (line 99) | @Test
    method testBoundaryConditions (line 120) | @Test
    method testTimestampBoundary (line 130) | @Test

FILE: src/test/java/cn/wildfirechat/app/jpa/UserConferenceTest.java
  class UserConferenceTest (line 7) | public class UserConferenceTest {
    method testDefaultConstructor (line 9) | @Test
    method testConstructorWithParams (line 19) | @Test
    method testSettersAndGetters (line 33) | @Test
    method testMultipleUserConferences (line 47) | @Test

FILE: src/test/java/cn/wildfirechat/app/jpa/UserNameEntryTest.java
  class UserNameEntryTest (line 7) | public class UserNameEntryTest {
    method testDefaultConstructor (line 9) | @Test
    method testSetAndGetId (line 18) | @Test
    method testDifferentIds (line 30) | @Test

FILE: src/test/java/cn/wildfirechat/app/jpa/UserPasswordTest.java
  class UserPasswordTest (line 7) | public class UserPasswordTest {
    method testDefaultConstructor (line 9) | @Test
    method testConstructorWithUserId (line 24) | @Test
    method testConstructorWithAllParams (line 38) | @Test
    method testSettersAndGetters (line 59) | @Test
    method testTryCountIncrementation (line 83) | @Test

FILE: src/test/java/cn/wildfirechat/app/jpa/UserPrivateConferenceIdTest.java
  class UserPrivateConferenceIdTest (line 7) | public class UserPrivateConferenceIdTest {
    method testDefaultConstructor (line 9) | @Test
    method testConstructorWithParams (line 19) | @Test
    method testSettersAndGetters (line 33) | @Test
    method testUpdateConferenceId (line 47) | @Test

FILE: src/test/java/cn/wildfirechat/app/slide/SlideVerifyCleanupServiceTest.java
  class SlideVerifyCleanupServiceTest (line 14) | @RunWith(MockitoJUnitRunner.class)
    method setUp (line 22) | @Before
    method testCleanupExpired (line 34) | @Test
    method testCleanupExpiredNoData (line 47) | @Test

FILE: src/test/java/cn/wildfirechat/app/slide/SlideVerifyServiceTest.java
  class SlideVerifyServiceTest (line 19) | @RunWith(MockitoJUnitRunner.class)
    method setUp (line 27) | @Before
    method testGenerateSlideVerify (line 40) | @Test
    method testVerifySlideSuccess (line 63) | @Test
    method testVerifySlideFailure (line 81) | @Test
    method testVerifySlideTokenNotFound (line 99) | @Test
    method testVerifySlideAlreadyVerified (line 114) | @Test
    method testIsVerifiedWhenVerified (line 132) | @Test
    method testIsVerifiedWhenNotVerified (line 149) | @Test
    method testIsVerifiedWhenNotFound (line 166) | @Test
    method testIsVerifiedWhenExpired (line 179) | @Test
Condensed preview — 148 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (398K chars).
[
  {
    "path": ".github/workflows/build.yml",
    "chars": 542,
    "preview": "# This workflow will build a Java project with Maven\n# For more information see: https://help.github.com/actions/languag"
  },
  {
    "path": ".gitignore",
    "chars": 62,
    "preview": "target\n.idea\nappdata.mv.db\nnohup.out\nappdata.trace.db\navatar/\n"
  },
  {
    "path": "LICENSE",
    "chars": 1640,
    "preview": "MIT License\n\nCopyright (c) 2019 wildfirechat\n\nPermission is hereby granted, free of charge, to any person obtaining a co"
  },
  {
    "path": "README.md",
    "chars": 5834,
    "preview": "## 野火IM解决方案\n\n野火IM是专业级即时通讯和实时音视频整体解决方案,由北京野火无限网络科技有限公司维护和支持。\n\n主要特性有:私有部署安全可靠,性能强大,功能齐全,全平台支持,开源率高,部署运维简单,二次开发友好,方便与第三方系统对"
  },
  {
    "path": "aliyun_sms.md",
    "chars": 941,
    "preview": "# 阿里云短信功能说明\n\n## 短信对接\n1. 在[这里](https://usercenter.console.aliyun.com/#/manage/ak)申请阿里云***accessKeyId***和***accessSecret**"
  },
  {
    "path": "build_release.sh",
    "chars": 457,
    "preview": "if [ $# -eq 0 ]; then\n    echo \"Usage: sh build_release.sh version\"\n    exit -1\nfi\necho \"build release $1\"\n\nmvn clean pa"
  },
  {
    "path": "config/aliyun_sms.properties",
    "chars": 152,
    "preview": "alisms.accessKeyId=MTAI82gOTQQTuKtW\nalisms.accessSecret=4p7HlgMTOQWHsX82IICabcea556677\nalisms.signName=\\u91CE\\u706BIM\nal"
  },
  {
    "path": "config/application.properties",
    "chars": 6010,
    "preview": "spring.message.encoding=UTF-8\nserver.port=8888\n\n## 给服务添加统一的路径前缀,方便代理统一转换。\n## 注意,如果这里改了,在客户端配置文件中修改APP_SERVER_ADDRESS,加上这"
  },
  {
    "path": "config/im.properties",
    "chars": 1000,
    "preview": "im.admin_url=http://localhost:18080\n#需要和im server里面配置的http.admin.secret_key一致\nim.admin_secret=123456\n\n#发送通知消息的管理员用户ID\nim"
  },
  {
    "path": "config/tencent_sms.properties",
    "chars": 159,
    "preview": "sms.secretId=AKIsaepMSEL91dsMESAUMO2smphIdgSxB8oD\nsms.secretKey=91dADocdksuw23AEFCD78lsdudf35ta0\nsms.appId=1432000001\nsm"
  },
  {
    "path": "deb/control/control",
    "chars": 260,
    "preview": "Package: app-server\nVersion: [[version]]\nSection: misc\nPriority: optional\nArchitecture: all\nMaintainer: Wildfirechat <su"
  },
  {
    "path": "deb/control/postinst",
    "chars": 87,
    "preview": "mv -f /opt/app-server/app-*.jar /opt/app-server/app-server.jar\nsystemctl daemon-reload\n"
  },
  {
    "path": "deb/control/postrm",
    "chars": 96,
    "preview": "rm -rf /opt/app-server\nrm -rf /usr/lib/systemd/system/app-server.service\nsystemctl daemon-reload"
  },
  {
    "path": "docker/Dockerfile",
    "chars": 308,
    "preview": "FROM openjdk:8-jre-alpine\n\nCOPY ../target/app-*.jar /opt/app-server/app.jar\nCOPY ../config  /opt/app-server/config\n\nWORK"
  },
  {
    "path": "docker/README.md",
    "chars": 500,
    "preview": "# 野火应用服务docker使用说明\n\n## 编译镜像\n首先需要先编译应用服务,使用下面命令编译\n```\nmvn clean package\n```\n\n然后进入到docker目录编译镜像\n```\nsudo docker build -t a"
  },
  {
    "path": "mvnw",
    "chars": 6468,
    "preview": "#!/bin/sh\n# ----------------------------------------------------------------------------\n# Licensed to the Apache Softwa"
  },
  {
    "path": "mvnw.cmd",
    "chars": 4994,
    "preview": "@REM ----------------------------------------------------------------------------\n@REM Licensed to the Apache Software F"
  },
  {
    "path": "nginx/appserver.conf",
    "chars": 1250,
    "preview": "server {\n        listen 80;\n        server_name apptest.wildfirechat.cn;\n        rewrite ^(.*)$  https://apptest.wildfir"
  },
  {
    "path": "pom.xml",
    "chars": 11451,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2"
  },
  {
    "path": "release_note.md",
    "chars": 3400,
    "preview": "# 当前版本更新记录\n0.70 Release note:\n1. 解决上传文件可能存在的漏洞\n\n# 升级注意事项\n1. 如果从0.40以前版本升级上来,需要注意升级兼容有问题 请参考Readme中的兼容问题说明\n\n2. 如果从0.42以前版"
  },
  {
    "path": "src/main/java/cn/wildfirechat/app/AppController.java",
    "chars": 11193,
    "preview": "package cn.wildfirechat.app;\n\nimport cn.wildfirechat.app.jpa.FavoriteItem;\nimport cn.wildfirechat.app.pojo.*;\nimport cn."
  },
  {
    "path": "src/main/java/cn/wildfirechat/app/Application.java",
    "chars": 1749,
    "preview": "package cn.wildfirechat.app;\n\nimport cn.wildfirechat.app.jpa.PCSessionRepository;\nimport cn.wildfirechat.app.slide.Slide"
  },
  {
    "path": "src/main/java/cn/wildfirechat/app/AudioController.java",
    "chars": 3692,
    "preview": "package cn.wildfirechat.app;\n\nimport org.springframework.beans.factory.annotation.Value;\nimport org.springframework.core"
  },
  {
    "path": "src/main/java/cn/wildfirechat/app/ForbiddenException.java",
    "chars": 265,
    "preview": "package cn.wildfirechat.app;\n\nimport org.springframework.http.HttpStatus;\nimport org.springframework.web.bind.annotation"
  },
  {
    "path": "src/main/java/cn/wildfirechat/app/IMCallbackController.java",
    "chars": 6927,
    "preview": "package cn.wildfirechat.app;\n\nimport cn.wildfirechat.pojos.*;\nimport cn.wildfirechat.pojos.moments.CommentPojo;\nimport c"
  },
  {
    "path": "src/main/java/cn/wildfirechat/app/IMConfig.java",
    "chars": 3814,
    "preview": "package cn.wildfirechat.app;\n\nimport org.springframework.boot.context.properties.ConfigurationProperties;\nimport org.spr"
  },
  {
    "path": "src/main/java/cn/wildfirechat/app/IMExceptionEventController.java",
    "chars": 3360,
    "preview": "package cn.wildfirechat.app;\n\nimport cn.wildfirechat.common.IMExceptionEvent;\nimport org.slf4j.Logger;\nimport org.slf4j."
  },
  {
    "path": "src/main/java/cn/wildfirechat/app/RestResult.java",
    "chars": 2627,
    "preview": "package cn.wildfirechat.app;\n\npublic class RestResult {\n    public enum  RestCode {\n        SUCCESS(0, \"success\"),\n     "
  },
  {
    "path": "src/main/java/cn/wildfirechat/app/Service.java",
    "chars": 2204,
    "preview": "package cn.wildfirechat.app;\n\n\nimport cn.wildfirechat.app.jpa.FavoriteItem;\nimport cn.wildfirechat.app.pojo.*;\nimport cn"
  },
  {
    "path": "src/main/java/cn/wildfirechat/app/ServiceImpl.java",
    "chars": 77515,
    "preview": "package cn.wildfirechat.app;\n\n\nimport cn.wildfirechat.app.jpa.*;\nimport cn.wildfirechat.app.pojo.*;\nimport cn.wildfirech"
  },
  {
    "path": "src/main/java/cn/wildfirechat/app/conference/ConferenceCleanupService.java",
    "chars": 2921,
    "preview": "package cn.wildfirechat.app.conference;\n\nimport cn.wildfirechat.app.jpa.ConferenceEntity;\nimport cn.wildfirechat.app.jpa"
  },
  {
    "path": "src/main/java/cn/wildfirechat/app/conference/ConferenceController.java",
    "chars": 3574,
    "preview": "package cn.wildfirechat.app.conference;\n\nimport cn.wildfirechat.app.Service;\nimport cn.wildfirechat.app.pojo.*;\nimport o"
  },
  {
    "path": "src/main/java/cn/wildfirechat/app/conference/ConferenceService.java",
    "chars": 1128,
    "preview": "package cn.wildfirechat.app.conference;\n\n\nimport cn.wildfirechat.app.RestResult;\nimport cn.wildfirechat.app.jpa.Favorite"
  },
  {
    "path": "src/main/java/cn/wildfirechat/app/conference/ConferenceServiceImpl.java",
    "chars": 26175,
    "preview": "package cn.wildfirechat.app.conference;\n\n\nimport cn.wildfirechat.app.IMConfig;\nimport cn.wildfirechat.app.RestResult;\nim"
  },
  {
    "path": "src/main/java/cn/wildfirechat/app/jpa/Announcement.java",
    "chars": 810,
    "preview": "package cn.wildfirechat.app.jpa;\n\nimport javax.persistence.*;\n\n@Entity\n@Table(name = \"text\")\npublic class Announcement {"
  },
  {
    "path": "src/main/java/cn/wildfirechat/app/jpa/AnnouncementRepository.java",
    "chars": 392,
    "preview": "package cn.wildfirechat.app.jpa;\n\nimport org.springframework.data.repository.PagingAndSortingRepository;\nimport org.spri"
  },
  {
    "path": "src/main/java/cn/wildfirechat/app/jpa/ConferenceEntity.java",
    "chars": 2558,
    "preview": "package cn.wildfirechat.app.jpa;\n\nimport javax.persistence.*;\n\n@Entity\n@Table(name = \"conference\")\npublic class Conferen"
  },
  {
    "path": "src/main/java/cn/wildfirechat/app/jpa/ConferenceEntityRepository.java",
    "chars": 671,
    "preview": "package cn.wildfirechat.app.jpa;\n\nimport org.springframework.data.jpa.repository.Query;\nimport org.springframework.data."
  },
  {
    "path": "src/main/java/cn/wildfirechat/app/jpa/ConferenceRecord.java",
    "chars": 3172,
    "preview": "package cn.wildfirechat.app.jpa;\n\nimport javax.persistence.*;\nimport java.util.Date;\n\n/**\n * 会议记录表\n * 记录每一次会议的详细信息,用于计费和"
  },
  {
    "path": "src/main/java/cn/wildfirechat/app/jpa/ConferenceRecordRepository.java",
    "chars": 807,
    "preview": "package cn.wildfirechat.app.jpa;\n\nimport org.springframework.data.jpa.repository.Modifying;\nimport org.springframework.d"
  },
  {
    "path": "src/main/java/cn/wildfirechat/app/jpa/FavoriteItem.java",
    "chars": 1058,
    "preview": "package cn.wildfirechat.app.jpa;\n\nimport javax.annotation.Nullable;\nimport javax.persistence.*;\n\n@Entity\n@Table(name = \""
  },
  {
    "path": "src/main/java/cn/wildfirechat/app/jpa/FavoriteRepository.java",
    "chars": 556,
    "preview": "package cn.wildfirechat.app.jpa;\n\nimport org.springframework.data.jpa.repository.Query;\nimport org.springframework.data."
  },
  {
    "path": "src/main/java/cn/wildfirechat/app/jpa/PCSession.java",
    "chars": 2200,
    "preview": "package cn.wildfirechat.app.jpa;\n\nimport cn.wildfirechat.app.pojo.SessionOutput;\n\nimport javax.persistence.Column;\nimpor"
  },
  {
    "path": "src/main/java/cn/wildfirechat/app/jpa/PCSessionRepository.java",
    "chars": 384,
    "preview": "package cn.wildfirechat.app.jpa;\n\nimport org.springframework.data.jpa.repository.Modifying;\nimport org.springframework.d"
  },
  {
    "path": "src/main/java/cn/wildfirechat/app/jpa/Record.java",
    "chars": 1686,
    "preview": "package cn.wildfirechat.app.jpa;\n\nimport javax.persistence.Column;\nimport javax.persistence.Entity;\nimport javax.persist"
  },
  {
    "path": "src/main/java/cn/wildfirechat/app/jpa/RecordRepository.java",
    "chars": 171,
    "preview": "package cn.wildfirechat.app.jpa;\n\nimport org.springframework.data.repository.CrudRepository;\n\npublic interface RecordRep"
  },
  {
    "path": "src/main/java/cn/wildfirechat/app/jpa/ShiroSession.java",
    "chars": 795,
    "preview": "package cn.wildfirechat.app.jpa;\n\nimport org.hibernate.annotations.Type;\n\nimport javax.persistence.*;\n\n@Entity\n@Table(na"
  },
  {
    "path": "src/main/java/cn/wildfirechat/app/jpa/ShiroSessionRepository.java",
    "chars": 183,
    "preview": "package cn.wildfirechat.app.jpa;\n\nimport org.springframework.data.repository.CrudRepository;\n\npublic interface ShiroSess"
  },
  {
    "path": "src/main/java/cn/wildfirechat/app/jpa/SlideVerify.java",
    "chars": 1241,
    "preview": "package cn.wildfirechat.app.jpa;\n\nimport javax.persistence.*;\nimport java.sql.Timestamp;\n\n@Entity\n@Table(name = \"slide_v"
  },
  {
    "path": "src/main/java/cn/wildfirechat/app/jpa/SlideVerifyRepository.java",
    "chars": 630,
    "preview": "package cn.wildfirechat.app.jpa;\n\nimport org.springframework.data.jpa.repository.Modifying;\nimport org.springframework.d"
  },
  {
    "path": "src/main/java/cn/wildfirechat/app/jpa/UserConference.java",
    "chars": 967,
    "preview": "package cn.wildfirechat.app.jpa;\n\nimport javax.persistence.*;\n\nimport java.util.List;\n\nimport static javax.persistence.C"
  },
  {
    "path": "src/main/java/cn/wildfirechat/app/jpa/UserConferenceQuota.java",
    "chars": 1580,
    "preview": "package cn.wildfirechat.app.jpa;\n\nimport javax.persistence.*;\nimport java.util.Date;\n\n/**\n * 用户会议额度表\n * 存储用户的会议分钟数配额(不分月"
  },
  {
    "path": "src/main/java/cn/wildfirechat/app/jpa/UserConferenceQuotaRepository.java",
    "chars": 415,
    "preview": "package cn.wildfirechat.app.jpa;\n\nimport org.springframework.data.repository.CrudRepository;\nimport org.springframework."
  },
  {
    "path": "src/main/java/cn/wildfirechat/app/jpa/UserConferenceRepository.java",
    "chars": 1351,
    "preview": "package cn.wildfirechat.app.jpa;\n\nimport cn.wildfirechat.app.model.ConferenceDTO;\nimport org.springframework.data.jpa.re"
  },
  {
    "path": "src/main/java/cn/wildfirechat/app/jpa/UserNameEntry.java",
    "chars": 335,
    "preview": "package cn.wildfirechat.app.jpa;\n\nimport javax.persistence.*;\n\n@Entity\n@Table(name = \"t_user_name\")\npublic class UserNam"
  },
  {
    "path": "src/main/java/cn/wildfirechat/app/jpa/UserNameRepository.java",
    "chars": 280,
    "preview": "package cn.wildfirechat.app.jpa;\n\nimport org.springframework.data.repository.CrudRepository;\nimport org.springframework."
  },
  {
    "path": "src/main/java/cn/wildfirechat/app/jpa/UserPassword.java",
    "chars": 1962,
    "preview": "package cn.wildfirechat.app.jpa;\n\nimport javax.persistence.Column;\nimport javax.persistence.Entity;\nimport javax.persist"
  },
  {
    "path": "src/main/java/cn/wildfirechat/app/jpa/UserPasswordRepository.java",
    "chars": 359,
    "preview": "package cn.wildfirechat.app.jpa;\n\nimport org.springframework.data.repository.CrudRepository;\nimport org.springframework."
  },
  {
    "path": "src/main/java/cn/wildfirechat/app/jpa/UserPrivateConferenceId.java",
    "chars": 784,
    "preview": "package cn.wildfirechat.app.jpa;\n\nimport javax.persistence.Column;\nimport javax.persistence.Entity;\nimport javax.persist"
  },
  {
    "path": "src/main/java/cn/wildfirechat/app/jpa/UserPrivateConferenceIdRepository.java",
    "chars": 334,
    "preview": "package cn.wildfirechat.app.jpa;\n\nimport org.springframework.data.repository.PagingAndSortingRepository;\nimport org.spri"
  },
  {
    "path": "src/main/java/cn/wildfirechat/app/jpa/UserQuotaUsage.java",
    "chars": 2064,
    "preview": "package cn.wildfirechat.app.jpa;\n\nimport javax.persistence.*;\nimport java.util.Date;\n\n/**\n * 用户额度使用表\n * 记录每个月用户实际使用的会议分钟"
  },
  {
    "path": "src/main/java/cn/wildfirechat/app/jpa/UserQuotaUsageRepository.java",
    "chars": 780,
    "preview": "package cn.wildfirechat.app.jpa;\n\nimport org.springframework.data.jpa.repository.Modifying;\nimport org.springframework.d"
  },
  {
    "path": "src/main/java/cn/wildfirechat/app/model/ConferenceDTO.java",
    "chars": 482,
    "preview": "package cn.wildfirechat.app.model;\n\npublic interface ConferenceDTO {\n    String getId();\n    String getConference_title("
  },
  {
    "path": "src/main/java/cn/wildfirechat/app/pojo/CancelSessionRequest.java",
    "chars": 235,
    "preview": "package cn.wildfirechat.app.pojo;\n\npublic class CancelSessionRequest {\n    private String token;\n\n    public String getT"
  },
  {
    "path": "src/main/java/cn/wildfirechat/app/pojo/ChangeNameRequest.java",
    "chars": 246,
    "preview": "package cn.wildfirechat.app.pojo;\n\npublic class ChangeNameRequest {\n    private String newName;\n\n    public String getNe"
  },
  {
    "path": "src/main/java/cn/wildfirechat/app/pojo/ChangePasswordRequest.java",
    "chars": 721,
    "preview": "package cn.wildfirechat.app.pojo;\n\npublic class ChangePasswordRequest {\n    private String oldPassword;\n    private Stri"
  },
  {
    "path": "src/main/java/cn/wildfirechat/app/pojo/ComplainRequest.java",
    "chars": 92,
    "preview": "package cn.wildfirechat.app.pojo;\n\npublic class ComplainRequest {\n    public String text;\n}\n"
  },
  {
    "path": "src/main/java/cn/wildfirechat/app/pojo/ConferenceInfo.java",
    "chars": 538,
    "preview": "package cn.wildfirechat.app.pojo;\n\nimport java.util.List;\n\npublic class ConferenceInfo {\n    public String conferenceId;"
  },
  {
    "path": "src/main/java/cn/wildfirechat/app/pojo/ConferenceInfoRequest.java",
    "chars": 134,
    "preview": "package cn.wildfirechat.app.pojo;\n\npublic class ConferenceInfoRequest {\n    public String conferenceId;\n    public Strin"
  },
  {
    "path": "src/main/java/cn/wildfirechat/app/pojo/ConferenceQuotaResponse.java",
    "chars": 1265,
    "preview": "package cn.wildfirechat.app.pojo;\n\n/**\n * 会议额度查询响应\n */\npublic class ConferenceQuotaResponse {\n    \n    // 用户月度额度(分钟)\n   "
  },
  {
    "path": "src/main/java/cn/wildfirechat/app/pojo/ConfirmSessionRequest.java",
    "chars": 702,
    "preview": "package cn.wildfirechat.app.pojo;\n\npublic class ConfirmSessionRequest {\n    private String token;\n    private String use"
  },
  {
    "path": "src/main/java/cn/wildfirechat/app/pojo/CreateSessionRequest.java",
    "chars": 1077,
    "preview": "package cn.wildfirechat.app.pojo;\n\npublic class CreateSessionRequest {\n    private String token;\n    private String devi"
  },
  {
    "path": "src/main/java/cn/wildfirechat/app/pojo/DestroyRequest.java",
    "chars": 222,
    "preview": "package cn.wildfirechat.app.pojo;\n\npublic class DestroyRequest {\n    private String code;\n\n    public String getCode() {"
  },
  {
    "path": "src/main/java/cn/wildfirechat/app/pojo/GroupAnnouncementPojo.java",
    "chars": 178,
    "preview": "package cn.wildfirechat.app.pojo;\n\npublic class GroupAnnouncementPojo {\n    public String groupId;\n    public String aut"
  },
  {
    "path": "src/main/java/cn/wildfirechat/app/pojo/GroupIdPojo.java",
    "chars": 91,
    "preview": "package cn.wildfirechat.app.pojo;\n\npublic class GroupIdPojo {\n    public String groupId;\n}\n"
  },
  {
    "path": "src/main/java/cn/wildfirechat/app/pojo/LoadFavoriteRequest.java",
    "chars": 114,
    "preview": "package cn.wildfirechat.app.pojo;\n\npublic class LoadFavoriteRequest {\n    public long id;\n    public int count;\n}\n"
  },
  {
    "path": "src/main/java/cn/wildfirechat/app/pojo/LoadFavoriteResponse.java",
    "chars": 208,
    "preview": "package cn.wildfirechat.app.pojo;\n\nimport cn.wildfirechat.app.jpa.FavoriteItem;\n\nimport java.util.List;\n\npublic class Lo"
  },
  {
    "path": "src/main/java/cn/wildfirechat/app/pojo/LoginResponse.java",
    "chars": 1138,
    "preview": "package cn.wildfirechat.app.pojo;\n\npublic class LoginResponse {\n    private String userId;\n    private String token;\n   "
  },
  {
    "path": "src/main/java/cn/wildfirechat/app/pojo/PhoneCodeLoginRequest.java",
    "chars": 767,
    "preview": "package cn.wildfirechat.app.pojo;\n\npublic class PhoneCodeLoginRequest {\n    private String mobile;\n    private String co"
  },
  {
    "path": "src/main/java/cn/wildfirechat/app/pojo/PhoneCodeLoginRequestWithSlideVerify.java",
    "chars": 1021,
    "preview": "package cn.wildfirechat.app.pojo;\n\npublic class PhoneCodeLoginRequestWithSlideVerify {\n    private String mobile;\n    pr"
  },
  {
    "path": "src/main/java/cn/wildfirechat/app/pojo/RecordingRequest.java",
    "chars": 99,
    "preview": "package cn.wildfirechat.app.pojo;\n\npublic class RecordingRequest {\n    public boolean recording;\n}\n"
  },
  {
    "path": "src/main/java/cn/wildfirechat/app/pojo/ResetPasswordRequest.java",
    "chars": 636,
    "preview": "package cn.wildfirechat.app.pojo;\n\npublic class ResetPasswordRequest {\n    private String mobile;\n    private String res"
  },
  {
    "path": "src/main/java/cn/wildfirechat/app/pojo/SendCodeRequest.java",
    "chars": 476,
    "preview": "package cn.wildfirechat.app.pojo;\n\npublic class SendCodeRequest {\n    private String mobile;\n    private String slideVer"
  },
  {
    "path": "src/main/java/cn/wildfirechat/app/pojo/SendCodeRequestWithSlideVerify.java",
    "chars": 491,
    "preview": "package cn.wildfirechat.app.pojo;\n\npublic class SendCodeRequestWithSlideVerify {\n    private String mobile;\n    private "
  },
  {
    "path": "src/main/java/cn/wildfirechat/app/pojo/SendDestroyCodeRequest.java",
    "chars": 314,
    "preview": "package cn.wildfirechat.app.pojo;\n\npublic class SendDestroyCodeRequest {\n    private String slideVerifyToken;\n\n    publi"
  },
  {
    "path": "src/main/java/cn/wildfirechat/app/pojo/SendMessageRequest.java",
    "chars": 557,
    "preview": "package cn.wildfirechat.app.pojo;\n\nimport java.util.List;\n\npublic class SendMessageRequest {\n    public int type;\n    pu"
  },
  {
    "path": "src/main/java/cn/wildfirechat/app/pojo/SessionOutput.java",
    "chars": 1458,
    "preview": "package cn.wildfirechat.app.pojo;\n\npublic class SessionOutput {\n    private String token;\n    private int status;\n    pr"
  },
  {
    "path": "src/main/java/cn/wildfirechat/app/pojo/SlideVerifyRequest.java",
    "chars": 371,
    "preview": "package cn.wildfirechat.app.pojo;\n\npublic class SlideVerifyRequest {\n    private String token;\n    private int x; // 滑动块"
  },
  {
    "path": "src/main/java/cn/wildfirechat/app/pojo/SlideVerifyResponse.java",
    "chars": 866,
    "preview": "package cn.wildfirechat.app.pojo;\n\npublic class SlideVerifyResponse {\n    private String token;\n    private String backg"
  },
  {
    "path": "src/main/java/cn/wildfirechat/app/pojo/UploadFileResponse.java",
    "chars": 94,
    "preview": "package cn.wildfirechat.app.pojo;\n\npublic class UploadFileResponse {\n    public String url;\n}\n"
  },
  {
    "path": "src/main/java/cn/wildfirechat/app/pojo/UserIdNamePortraitPojo.java",
    "chars": 376,
    "preview": "package cn.wildfirechat.app.pojo;\n\npublic class UserIdNamePortraitPojo {\n    public String userId;\n    public String nam"
  },
  {
    "path": "src/main/java/cn/wildfirechat/app/pojo/UserIdPojo.java",
    "chars": 89,
    "preview": "package cn.wildfirechat.app.pojo;\n\npublic class UserIdPojo {\n    public String userId;\n}\n"
  },
  {
    "path": "src/main/java/cn/wildfirechat/app/pojo/UserPasswordLoginRequest.java",
    "chars": 798,
    "preview": "package cn.wildfirechat.app.pojo;\n\npublic class UserPasswordLoginRequest {\n    private String mobile;\n    private String"
  },
  {
    "path": "src/main/java/cn/wildfirechat/app/pojo/UserPasswordLoginRequestWithSlideVerify.java",
    "chars": 1052,
    "preview": "package cn.wildfirechat.app.pojo;\n\npublic class UserPasswordLoginRequestWithSlideVerify {\n    private String mobile;\n   "
  },
  {
    "path": "src/main/java/cn/wildfirechat/app/shiro/AuthDataSource.java",
    "chars": 7943,
    "preview": "package cn.wildfirechat.app.shiro;\n\nimport cn.wildfirechat.app.RestResult;\nimport cn.wildfirechat.app.jpa.PCSession;\nimp"
  },
  {
    "path": "src/main/java/cn/wildfirechat/app/shiro/CorsFilter.java",
    "chars": 1178,
    "preview": "package cn.wildfirechat.app.shiro;\n\n\nimport org.springframework.stereotype.Component;\nimport org.springframework.web.fil"
  },
  {
    "path": "src/main/java/cn/wildfirechat/app/shiro/DBSessionDao.java",
    "chars": 2995,
    "preview": "package cn.wildfirechat.app.shiro;\n\nimport cn.wildfirechat.app.jpa.ShiroSession;\nimport cn.wildfirechat.app.jpa.ShiroSes"
  },
  {
    "path": "src/main/java/cn/wildfirechat/app/shiro/JsonAuthLoginFilter.java",
    "chars": 1793,
    "preview": "package cn.wildfirechat.app.shiro;\n\nimport cn.wildfirechat.app.RestResult;\nimport com.google.gson.Gson;\nimport org.apach"
  },
  {
    "path": "src/main/java/cn/wildfirechat/app/shiro/LdapMatcher.java",
    "chars": 2805,
    "preview": "package cn.wildfirechat.app.shiro;\n\nimport cn.wildfirechat.app.tools.LdapUser;\nimport org.apache.shiro.authc.Authenticat"
  },
  {
    "path": "src/main/java/cn/wildfirechat/app/shiro/LdapRealm.java",
    "chars": 2025,
    "preview": "package cn.wildfirechat.app.shiro;\n\n\nimport cn.wildfirechat.app.jpa.UserPassword;\nimport cn.wildfirechat.app.jpa.UserPas"
  },
  {
    "path": "src/main/java/cn/wildfirechat/app/shiro/LdapToken.java",
    "chars": 643,
    "preview": "package cn.wildfirechat.app.shiro;\n\nimport org.apache.shiro.authc.AuthenticationToken;\n\npublic class LdapToken implement"
  },
  {
    "path": "src/main/java/cn/wildfirechat/app/shiro/PhoneCodeRealm.java",
    "chars": 1750,
    "preview": "package cn.wildfirechat.app.shiro;\n\n\nimport cn.wildfirechat.app.RestResult;\nimport org.apache.shiro.authc.*;\nimport org."
  },
  {
    "path": "src/main/java/cn/wildfirechat/app/shiro/PhoneCodeToken.java",
    "chars": 487,
    "preview": "package cn.wildfirechat.app.shiro;\n\nimport org.apache.shiro.authc.AuthenticationToken;\n\npublic class PhoneCodeToken impl"
  },
  {
    "path": "src/main/java/cn/wildfirechat/app/shiro/ScanCodeRealm.java",
    "chars": 1774,
    "preview": "package cn.wildfirechat.app.shiro;\n\n\nimport cn.wildfirechat.app.jpa.PCSession;\nimport org.apache.shiro.authc.*;\nimport o"
  },
  {
    "path": "src/main/java/cn/wildfirechat/app/shiro/ShiroConfig.java",
    "chars": 4720,
    "preview": "package cn.wildfirechat.app.shiro;\n\n\nimport org.apache.shiro.SecurityUtils;\nimport org.apache.shiro.mgt.SecurityManager;"
  },
  {
    "path": "src/main/java/cn/wildfirechat/app/shiro/ShiroSessionManager.java",
    "chars": 1427,
    "preview": "package cn.wildfirechat.app.shiro;\n\n\nimport com.aliyuncs.utils.StringUtils;\nimport org.apache.shiro.web.servlet.ShiroHtt"
  },
  {
    "path": "src/main/java/cn/wildfirechat/app/shiro/TokenAuthenticationToken.java",
    "chars": 567,
    "preview": "package cn.wildfirechat.app.shiro;\n\nimport org.apache.shiro.authc.AuthenticationToken;\n\npublic class TokenAuthentication"
  },
  {
    "path": "src/main/java/cn/wildfirechat/app/shiro/TokenMatcher.java",
    "chars": 1488,
    "preview": "package cn.wildfirechat.app.shiro;\n\nimport cn.wildfirechat.app.RestResult;\nimport cn.wildfirechat.pojos.InputOutputUserI"
  },
  {
    "path": "src/main/java/cn/wildfirechat/app/shiro/UserPasswordRealm.java",
    "chars": 2384,
    "preview": "package cn.wildfirechat.app.shiro;\n\n\nimport cn.wildfirechat.app.RestResult;\nimport cn.wildfirechat.app.jpa.ShiroSession;"
  },
  {
    "path": "src/main/java/cn/wildfirechat/app/slide/SlideVerifyCleanupService.java",
    "chars": 538,
    "preview": "package cn.wildfirechat.app.slide;\n\nimport cn.wildfirechat.app.jpa.SlideVerifyRepository;\nimport org.springframework.bea"
  },
  {
    "path": "src/main/java/cn/wildfirechat/app/slide/SlideVerifyService.java",
    "chars": 8063,
    "preview": "package cn.wildfirechat.app.slide;\n\nimport cn.wildfirechat.app.jpa.SlideVerify;\nimport cn.wildfirechat.app.jpa.SlideVeri"
  },
  {
    "path": "src/main/java/cn/wildfirechat/app/sms/AliyunSMSConfig.java",
    "chars": 1181,
    "preview": "package cn.wildfirechat.app.sms;\n\nimport org.springframework.boot.context.properties.ConfigurationProperties;\nimport org"
  },
  {
    "path": "src/main/java/cn/wildfirechat/app/sms/SmsService.java",
    "chars": 169,
    "preview": "package cn.wildfirechat.app.sms;\n\n\nimport cn.wildfirechat.app.RestResult;\n\npublic interface SmsService {\n    RestResult."
  },
  {
    "path": "src/main/java/cn/wildfirechat/app/sms/SmsServiceImpl.java",
    "chars": 7609,
    "preview": "package cn.wildfirechat.app.sms;\n\nimport cn.wildfirechat.app.RestResult;\nimport com.aliyuncs.CommonRequest;\nimport com.a"
  },
  {
    "path": "src/main/java/cn/wildfirechat/app/sms/TencentSMSConfig.java",
    "chars": 1286,
    "preview": "package cn.wildfirechat.app.sms;\n\nimport org.springframework.boot.context.properties.ConfigurationProperties;\nimport org"
  },
  {
    "path": "src/main/java/cn/wildfirechat/app/tools/LdapUser.java",
    "chars": 453,
    "preview": "package cn.wildfirechat.app.tools;\n\npublic class LdapUser {\n    public final String uid, cn, mail, phone, dn;\n    public"
  },
  {
    "path": "src/main/java/cn/wildfirechat/app/tools/LdapUtil.java",
    "chars": 3630,
    "preview": "package cn.wildfirechat.app.tools;\n\nimport javax.naming.Context;\nimport javax.naming.NamingEnumeration;\nimport javax.nam"
  },
  {
    "path": "src/main/java/cn/wildfirechat/app/tools/NumericIdGenerator.java",
    "chars": 1348,
    "preview": "package cn.wildfirechat.app.tools;\n\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.List;\n\npublic "
  },
  {
    "path": "src/main/java/cn/wildfirechat/app/tools/OrderedIdUserNameGenerator.java",
    "chars": 585,
    "preview": "package cn.wildfirechat.app.tools;\n\nimport cn.wildfirechat.app.jpa.UserNameEntry;\nimport cn.wildfirechat.app.jpa.UserNam"
  },
  {
    "path": "src/main/java/cn/wildfirechat/app/tools/PhoneNumberUserNameGenerator.java",
    "chars": 260,
    "preview": "package cn.wildfirechat.app.tools;\n\nimport org.springframework.stereotype.Component;\n\n@Component\npublic class PhoneNumbe"
  },
  {
    "path": "src/main/java/cn/wildfirechat/app/tools/RateLimiter.java",
    "chars": 4064,
    "preview": "package cn.wildfirechat.app.tools;\n\n\nimport java.util.HashMap;\nimport java.util.Iterator;\nimport java.util.Map;\n\n/**\n * "
  },
  {
    "path": "src/main/java/cn/wildfirechat/app/tools/ShortUUIDGenerator.java",
    "chars": 1526,
    "preview": "package cn.wildfirechat.app.tools;\n\nimport org.springframework.stereotype.Component;\n\nimport java.util.HashSet;\nimport j"
  },
  {
    "path": "src/main/java/cn/wildfirechat/app/tools/SpinLock.java",
    "chars": 654,
    "preview": "package cn.wildfirechat.app.tools;\n\nimport java.util.concurrent.atomic.AtomicReference;\n\npublic class SpinLock {\n    //j"
  },
  {
    "path": "src/main/java/cn/wildfirechat/app/tools/UUIDUserNameGenerator.java",
    "chars": 330,
    "preview": "package cn.wildfirechat.app.tools;\n\nimport org.springframework.stereotype.Component;\n\nimport java.util.UUID;\n\n@Component"
  },
  {
    "path": "src/main/java/cn/wildfirechat/app/tools/UserNameGenerator.java",
    "chars": 113,
    "preview": "package cn.wildfirechat.app.tools;\n\npublic interface UserNameGenerator {\n    String getUserName(String phone);\n}\n"
  },
  {
    "path": "src/main/java/cn/wildfirechat/app/tools/Utils.java",
    "chars": 1561,
    "preview": "package cn.wildfirechat.app.tools;\n\nimport java.nio.file.Paths;\nimport java.util.Random;\nimport java.util.UUID;\nimport j"
  },
  {
    "path": "src/main/resources/application.properties",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "src/test/java/cn/wildfirechat/app/ApplicationTests.java",
    "chars": 2569,
    "preview": "package cn.wildfirechat.app;\n\nimport com.zaxxer.hikari.HikariDataSource;\nimport org.junit.Test;\nimport org.junit.runner."
  },
  {
    "path": "src/test/java/cn/wildfirechat/app/jpa/AnnouncementTest.java",
    "chars": 1718,
    "preview": "package cn.wildfirechat.app.jpa;\n\nimport org.junit.Test;\n\nimport static org.junit.Assert.*;\n\npublic class AnnouncementTe"
  },
  {
    "path": "src/test/java/cn/wildfirechat/app/jpa/ConferenceEntityTest.java",
    "chars": 3455,
    "preview": "package cn.wildfirechat.app.jpa;\n\nimport org.junit.Test;\n\nimport static org.junit.Assert.*;\n\npublic class ConferenceEnti"
  },
  {
    "path": "src/test/java/cn/wildfirechat/app/jpa/FavoriteItemTest.java",
    "chars": 2497,
    "preview": "package cn.wildfirechat.app.jpa;\n\nimport org.junit.Test;\n\nimport static org.junit.Assert.*;\n\npublic class FavoriteItemTe"
  },
  {
    "path": "src/test/java/cn/wildfirechat/app/jpa/PCSessionTest.java",
    "chars": 3301,
    "preview": "package cn.wildfirechat.app.jpa;\n\nimport org.junit.Test;\n\nimport static org.junit.Assert.*;\n\npublic class PCSessionTest "
  },
  {
    "path": "src/test/java/cn/wildfirechat/app/jpa/RecordTest.java",
    "chars": 2958,
    "preview": "package cn.wildfirechat.app.jpa;\n\nimport org.junit.Test;\n\nimport static org.junit.Assert.*;\n\npublic class RecordTest {\n\n"
  },
  {
    "path": "src/test/java/cn/wildfirechat/app/jpa/ShiroSessionTest.java",
    "chars": 2336,
    "preview": "package cn.wildfirechat.app.jpa;\n\nimport org.junit.Test;\n\nimport java.util.Arrays;\n\nimport static org.junit.Assert.*;\n\np"
  },
  {
    "path": "src/test/java/cn/wildfirechat/app/jpa/SlideVerifyRepositoryTest.java",
    "chars": 3569,
    "preview": "package cn.wildfirechat.app.jpa;\n\nimport cn.wildfirechat.app.slide.SlideVerifyCleanupService;\nimport cn.wildfirechat.app"
  },
  {
    "path": "src/test/java/cn/wildfirechat/app/jpa/SlideVerifyTest.java",
    "chars": 3991,
    "preview": "package cn.wildfirechat.app.jpa;\n\nimport org.junit.Test;\n\nimport static org.junit.Assert.*;\n\npublic class SlideVerifyTes"
  },
  {
    "path": "src/test/java/cn/wildfirechat/app/jpa/UserConferenceTest.java",
    "chars": 1674,
    "preview": "package cn.wildfirechat.app.jpa;\n\nimport org.junit.Test;\n\nimport static org.junit.Assert.*;\n\npublic class UserConference"
  },
  {
    "path": "src/test/java/cn/wildfirechat/app/jpa/UserNameEntryTest.java",
    "chars": 864,
    "preview": "package cn.wildfirechat.app.jpa;\n\nimport org.junit.Test;\n\nimport static org.junit.Assert.*;\n\npublic class UserNameEntryT"
  },
  {
    "path": "src/test/java/cn/wildfirechat/app/jpa/UserPasswordTest.java",
    "chars": 3167,
    "preview": "package cn.wildfirechat.app.jpa;\n\nimport org.junit.Test;\n\nimport static org.junit.Assert.*;\n\npublic class UserPasswordTe"
  },
  {
    "path": "src/test/java/cn/wildfirechat/app/jpa/UserPrivateConferenceIdTest.java",
    "chars": 1445,
    "preview": "package cn.wildfirechat.app.jpa;\n\nimport org.junit.Test;\n\nimport static org.junit.Assert.*;\n\npublic class UserPrivateCon"
  },
  {
    "path": "src/test/java/cn/wildfirechat/app/slide/SlideVerifyCleanupServiceTest.java",
    "chars": 1664,
    "preview": "package cn.wildfirechat.app.slide;\n\nimport cn.wildfirechat.app.jpa.SlideVerify;\nimport cn.wildfirechat.app.jpa.SlideVeri"
  },
  {
    "path": "src/test/java/cn/wildfirechat/app/slide/SlideVerifyServiceTest.java",
    "chars": 6132,
    "preview": "package cn.wildfirechat.app.slide;\n\nimport cn.wildfirechat.app.jpa.SlideVerify;\nimport cn.wildfirechat.app.jpa.SlideVeri"
  },
  {
    "path": "systemd/README.md",
    "chars": 1770,
    "preview": "# Linux Service 方式运行\n除了命令行方式直接执行APP服务外,还可以以linux systemd service方式来运行,注意以这种方式运行,APP服务的配置还是需要按照常规方法来配置。\n\n## 获取软件包\n下载野火rel"
  },
  {
    "path": "systemd/app-server.service",
    "chars": 700,
    "preview": "[Unit]\nDescription=WildfirechatAPP\nDocumentation=https://docs.wildfirechat.cn\nWants=network-online.target\nAfter=network-"
  }
]

// ... and 4 more files (download for full content)

About this extraction

This page contains the full source code of the wildfirechat/im-app_server GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 148 files (341.2 KB), approximately 86.4k tokens, and a symbol index with 883 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!