Merge branch 'merge/unify-all' of http://115.190.64.57:8000/xiaozhang/zhibo into IM-gift
This commit is contained in:
commit
e7746a5bbb
|
|
@ -29,6 +29,16 @@ spring:
|
||||||
url: jdbc:mysql://1.15.149.240:3306/zhibo?characterEncoding=utf-8&useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=GMT%2B8
|
url: jdbc:mysql://1.15.149.240:3306/zhibo?characterEncoding=utf-8&useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=GMT%2B8
|
||||||
username: zhibo
|
username: zhibo
|
||||||
password: zCETFpGMwYN3CNeH
|
password: zCETFpGMwYN3CNeH
|
||||||
|
# JPA 配置 - 只新增字段,不删除已有字段
|
||||||
|
jpa:
|
||||||
|
hibernate:
|
||||||
|
ddl-auto: update # update: 只新增表/字段,不删除
|
||||||
|
show-sql: false
|
||||||
|
open-in-view: false
|
||||||
|
properties:
|
||||||
|
hibernate:
|
||||||
|
dialect: org.hibernate.dialect.MySQL5InnoDBDialect
|
||||||
|
physical_naming_strategy: org.springframework.boot.orm.jpa.hibernate.SpringPhysicalNamingStrategy
|
||||||
redis:
|
redis:
|
||||||
host: 127.0.0.1 #地址
|
host: 127.0.0.1 #地址
|
||||||
port: 6379 #端口
|
port: 6379 #端口
|
||||||
|
|
|
||||||
|
|
@ -79,6 +79,12 @@
|
||||||
<artifactId>mybatis-plus-boot-starter</artifactId>
|
<artifactId>mybatis-plus-boot-starter</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<!-- JPA 自动建表 -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-data-jpa</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<!-- 模板引擎 mybatis code generator时需要使用-->
|
<!-- 模板引擎 mybatis code generator时需要使用-->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
|
|
||||||
|
|
@ -9,16 +9,20 @@ import lombok.Data;
|
||||||
import lombok.EqualsAndHashCode;
|
import lombok.EqualsAndHashCode;
|
||||||
import lombok.experimental.Accessors;
|
import lombok.experimental.Accessors;
|
||||||
|
|
||||||
|
import javax.persistence.*;
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 私聊会话实体类
|
* 私聊会话实体类
|
||||||
|
* 同时支持 MyBatis-Plus 和 JPA
|
||||||
*/
|
*/
|
||||||
@Data
|
@Data
|
||||||
@EqualsAndHashCode(callSuper = false)
|
@EqualsAndHashCode(callSuper = false)
|
||||||
@Accessors(chain = true)
|
@Accessors(chain = true)
|
||||||
@TableName("eb_conversation")
|
@TableName("eb_conversation")
|
||||||
|
@Entity
|
||||||
|
@Table(name = "eb_conversation")
|
||||||
@ApiModel(value = "Conversation对象", description = "私聊会话")
|
@ApiModel(value = "Conversation对象", description = "私聊会话")
|
||||||
public class Conversation implements Serializable {
|
public class Conversation implements Serializable {
|
||||||
|
|
||||||
|
|
@ -26,41 +30,55 @@ public class Conversation implements Serializable {
|
||||||
|
|
||||||
@ApiModelProperty(value = "会话ID")
|
@ApiModelProperty(value = "会话ID")
|
||||||
@TableId(value = "id", type = IdType.AUTO)
|
@TableId(value = "id", type = IdType.AUTO)
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
private Long id;
|
private Long id;
|
||||||
|
|
||||||
@ApiModelProperty(value = "用户1的ID")
|
@ApiModelProperty(value = "用户1的ID")
|
||||||
|
@Column(name = "user1_id", nullable = false)
|
||||||
private Integer user1Id;
|
private Integer user1Id;
|
||||||
|
|
||||||
@ApiModelProperty(value = "用户2的ID")
|
@ApiModelProperty(value = "用户2的ID")
|
||||||
|
@Column(name = "user2_id", nullable = false)
|
||||||
private Integer user2Id;
|
private Integer user2Id;
|
||||||
|
|
||||||
@ApiModelProperty(value = "最后一条消息内容")
|
@ApiModelProperty(value = "最后一条消息内容")
|
||||||
|
@Column(name = "last_message", length = 500)
|
||||||
private String lastMessage;
|
private String lastMessage;
|
||||||
|
|
||||||
@ApiModelProperty(value = "最后一条消息时间")
|
@ApiModelProperty(value = "最后一条消息时间")
|
||||||
|
@Column(name = "last_message_time")
|
||||||
private Date lastMessageTime;
|
private Date lastMessageTime;
|
||||||
|
|
||||||
@ApiModelProperty(value = "用户1的未读数量")
|
@ApiModelProperty(value = "用户1的未读数量")
|
||||||
|
@Column(name = "user1_unread_count", columnDefinition = "INT DEFAULT 0")
|
||||||
private Integer user1UnreadCount;
|
private Integer user1UnreadCount;
|
||||||
|
|
||||||
@ApiModelProperty(value = "用户2的未读数量")
|
@ApiModelProperty(value = "用户2的未读数量")
|
||||||
|
@Column(name = "user2_unread_count", columnDefinition = "INT DEFAULT 0")
|
||||||
private Integer user2UnreadCount;
|
private Integer user2UnreadCount;
|
||||||
|
|
||||||
@ApiModelProperty(value = "用户1是否删除会话")
|
@ApiModelProperty(value = "用户1是否删除会话")
|
||||||
|
@Column(name = "user1_deleted", columnDefinition = "TINYINT(1) DEFAULT 0")
|
||||||
private Boolean user1Deleted;
|
private Boolean user1Deleted;
|
||||||
|
|
||||||
@ApiModelProperty(value = "用户2是否删除会话")
|
@ApiModelProperty(value = "用户2是否删除会话")
|
||||||
|
@Column(name = "user2_deleted", columnDefinition = "TINYINT(1) DEFAULT 0")
|
||||||
private Boolean user2Deleted;
|
private Boolean user2Deleted;
|
||||||
|
|
||||||
@ApiModelProperty(value = "用户1是否静音")
|
@ApiModelProperty(value = "用户1是否静音")
|
||||||
|
@Column(name = "user1_muted", columnDefinition = "TINYINT(1) DEFAULT 0")
|
||||||
private Boolean user1Muted;
|
private Boolean user1Muted;
|
||||||
|
|
||||||
@ApiModelProperty(value = "用户2是否静音")
|
@ApiModelProperty(value = "用户2是否静音")
|
||||||
|
@Column(name = "user2_muted", columnDefinition = "TINYINT(1) DEFAULT 0")
|
||||||
private Boolean user2Muted;
|
private Boolean user2Muted;
|
||||||
|
|
||||||
@ApiModelProperty(value = "创建时间")
|
@ApiModelProperty(value = "创建时间")
|
||||||
|
@Column(name = "create_time")
|
||||||
private Date createTime;
|
private Date createTime;
|
||||||
|
|
||||||
@ApiModelProperty(value = "更新时间")
|
@ApiModelProperty(value = "更新时间")
|
||||||
|
@Column(name = "update_time")
|
||||||
private Date updateTime;
|
private Date updateTime;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,16 +9,25 @@ import lombok.Data;
|
||||||
import lombok.EqualsAndHashCode;
|
import lombok.EqualsAndHashCode;
|
||||||
import lombok.experimental.Accessors;
|
import lombok.experimental.Accessors;
|
||||||
|
|
||||||
|
import javax.persistence.*;
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 私信消息实体类
|
* 私信消息实体类
|
||||||
|
* 同时支持 MyBatis-Plus 和 JPA
|
||||||
*/
|
*/
|
||||||
@Data
|
@Data
|
||||||
@EqualsAndHashCode(callSuper = false)
|
@EqualsAndHashCode(callSuper = false)
|
||||||
@Accessors(chain = true)
|
@Accessors(chain = true)
|
||||||
@TableName("eb_private_message")
|
@TableName("eb_private_message")
|
||||||
|
@Entity
|
||||||
|
@Table(name = "eb_private_message", indexes = {
|
||||||
|
@Index(name = "idx_conversation_id", columnList = "conversation_id"),
|
||||||
|
@Index(name = "idx_sender_id", columnList = "sender_id"),
|
||||||
|
@Index(name = "idx_receiver_id", columnList = "receiver_id"),
|
||||||
|
@Index(name = "idx_create_time", columnList = "create_time")
|
||||||
|
})
|
||||||
@ApiModel(value = "PrivateMessage对象", description = "私信消息")
|
@ApiModel(value = "PrivateMessage对象", description = "私信消息")
|
||||||
public class PrivateMessage implements Serializable {
|
public class PrivateMessage implements Serializable {
|
||||||
|
|
||||||
|
|
@ -26,35 +35,47 @@ public class PrivateMessage implements Serializable {
|
||||||
|
|
||||||
@ApiModelProperty(value = "消息ID")
|
@ApiModelProperty(value = "消息ID")
|
||||||
@TableId(value = "id", type = IdType.AUTO)
|
@TableId(value = "id", type = IdType.AUTO)
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
private Long id;
|
private Long id;
|
||||||
|
|
||||||
@ApiModelProperty(value = "会话ID")
|
@ApiModelProperty(value = "会话ID")
|
||||||
|
@Column(name = "conversation_id", nullable = false)
|
||||||
private Long conversationId;
|
private Long conversationId;
|
||||||
|
|
||||||
@ApiModelProperty(value = "发送者用户ID")
|
@ApiModelProperty(value = "发送者用户ID")
|
||||||
|
@Column(name = "sender_id", nullable = false)
|
||||||
private Integer senderId;
|
private Integer senderId;
|
||||||
|
|
||||||
@ApiModelProperty(value = "接收者用户ID")
|
@ApiModelProperty(value = "接收者用户ID")
|
||||||
|
@Column(name = "receiver_id", nullable = false)
|
||||||
private Integer receiverId;
|
private Integer receiverId;
|
||||||
|
|
||||||
@ApiModelProperty(value = "消息内容")
|
@ApiModelProperty(value = "消息内容")
|
||||||
|
@Column(name = "content", columnDefinition = "TEXT")
|
||||||
private String content;
|
private String content;
|
||||||
|
|
||||||
@ApiModelProperty(value = "消息类型: text, image, file")
|
@ApiModelProperty(value = "消息类型: text, image, file")
|
||||||
|
@Column(name = "message_type", length = 20, columnDefinition = "VARCHAR(20) DEFAULT 'text'")
|
||||||
private String messageType;
|
private String messageType;
|
||||||
|
|
||||||
@ApiModelProperty(value = "消息状态: sending, sent, read")
|
@ApiModelProperty(value = "消息状态: sending, sent, read")
|
||||||
|
@Column(name = "status", length = 20, columnDefinition = "VARCHAR(20) DEFAULT 'sent'")
|
||||||
private String status;
|
private String status;
|
||||||
|
|
||||||
@ApiModelProperty(value = "是否已删除(发送者)")
|
@ApiModelProperty(value = "是否已删除(发送者)")
|
||||||
|
@Column(name = "sender_deleted", columnDefinition = "TINYINT(1) DEFAULT 0")
|
||||||
private Boolean senderDeleted;
|
private Boolean senderDeleted;
|
||||||
|
|
||||||
@ApiModelProperty(value = "是否已删除(接收者)")
|
@ApiModelProperty(value = "是否已删除(接收者)")
|
||||||
|
@Column(name = "receiver_deleted", columnDefinition = "TINYINT(1) DEFAULT 0")
|
||||||
private Boolean receiverDeleted;
|
private Boolean receiverDeleted;
|
||||||
|
|
||||||
@ApiModelProperty(value = "创建时间")
|
@ApiModelProperty(value = "创建时间")
|
||||||
|
@Column(name = "create_time")
|
||||||
private Date createTime;
|
private Date createTime;
|
||||||
|
|
||||||
@ApiModelProperty(value = "已读时间")
|
@ApiModelProperty(value = "已读时间")
|
||||||
|
@Column(name = "read_time")
|
||||||
private Date readTime;
|
private Date readTime;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,7 @@ public class StoreCoupon implements Serializable {
|
||||||
@TableId(value = "id", type = IdType.AUTO)
|
@TableId(value = "id", type = IdType.AUTO)
|
||||||
private Integer id;
|
private Integer id;
|
||||||
|
|
||||||
|
|
||||||
@ApiModelProperty(value = "优惠券名称")
|
@ApiModelProperty(value = "优惠券名称")
|
||||||
private String name;
|
private String name;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,10 @@ import org.mybatis.spring.annotation.MapperScan;
|
||||||
import org.springframework.boot.SpringApplication;
|
import org.springframework.boot.SpringApplication;
|
||||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
|
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
|
||||||
|
import org.springframework.boot.autoconfigure.domain.EntityScan;
|
||||||
import org.springframework.context.annotation.ComponentScan;
|
import org.springframework.context.annotation.ComponentScan;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
|
||||||
import org.springframework.scheduling.annotation.EnableAsync;
|
import org.springframework.scheduling.annotation.EnableAsync;
|
||||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||||
import org.springframework.transaction.annotation.EnableTransactionManagement;
|
import org.springframework.transaction.annotation.EnableTransactionManagement;
|
||||||
|
|
@ -31,6 +33,8 @@ import springfox.documentation.swagger2.annotations.EnableSwagger2;
|
||||||
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class) //去掉数据源
|
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class) //去掉数据源
|
||||||
@ComponentScan(basePackages = {"com.zbkj", "com.zbkj.front"})
|
@ComponentScan(basePackages = {"com.zbkj", "com.zbkj.front"})
|
||||||
@MapperScan(basePackages = {"com.zbkj.**.dao"})
|
@MapperScan(basePackages = {"com.zbkj.**.dao"})
|
||||||
|
@EntityScan(basePackages = {"com.zbkj.common.model"}) // JPA实体扫描
|
||||||
|
@EnableJpaRepositories(basePackages = {"com.zbkj.service.repository"}) // JPA仓库扫描(可选)
|
||||||
public class CrmebFrontApplication {
|
public class CrmebFrontApplication {
|
||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
SpringApplication.run(CrmebFrontApplication.class, args);
|
SpringApplication.run(CrmebFrontApplication.class, args);
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,19 @@ spring:
|
||||||
url: jdbc:mysql://1.15.149.240:3306/zhibo?characterEncoding=utf-8&useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=GMT%2B8
|
url: jdbc:mysql://1.15.149.240:3306/zhibo?characterEncoding=utf-8&useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=GMT%2B8
|
||||||
username: zhibo
|
username: zhibo
|
||||||
password: zCETFpGMwYN3CNeH
|
password: zCETFpGMwYN3CNeH
|
||||||
|
# JPA 配置 - 只新增字段,不删除已有字段
|
||||||
|
jpa:
|
||||||
|
hibernate:
|
||||||
|
ddl-auto: update # update: 只新增表/字段,不删除;none: 不做任何操作
|
||||||
|
show-sql: false # 生产环境建议关闭
|
||||||
|
open-in-view: false # 关闭懒加载视图
|
||||||
|
properties:
|
||||||
|
hibernate:
|
||||||
|
format_sql: true
|
||||||
|
dialect: org.hibernate.dialect.MySQL5InnoDBDialect
|
||||||
|
# 命名策略:将驼峰转为下划线
|
||||||
|
physical_naming_strategy: org.springframework.boot.orm.jpa.hibernate.SpringPhysicalNamingStrategy
|
||||||
|
implicit_naming_strategy: org.springframework.boot.orm.jpa.hibernate.SpringImplicitNamingStrategy
|
||||||
redis:
|
redis:
|
||||||
host: 127.0.0.1 #地址
|
host: 127.0.0.1 #地址
|
||||||
port: 6379 #端口
|
port: 6379 #端口
|
||||||
|
|
|
||||||
|
|
@ -100,6 +100,13 @@
|
||||||
<version>3.3.1</version>
|
<version>3.3.1</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<!-- JPA 自动建表 -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-data-jpa</artifactId>
|
||||||
|
<version>2.2.6.RELEASE</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<!--generator-->
|
<!--generator-->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.baomidou</groupId>
|
<groupId>com.baomidou</groupId>
|
||||||
|
|
|
||||||
40
Zhibo/zhibo-h/sql/create_conversation_tables.sql
Normal file
40
Zhibo/zhibo-h/sql/create_conversation_tables.sql
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
-- 私聊会话表
|
||||||
|
CREATE TABLE IF NOT EXISTS `eb_conversation` (
|
||||||
|
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '会话ID',
|
||||||
|
`user1_id` int(11) NOT NULL COMMENT '用户1的ID',
|
||||||
|
`user2_id` int(11) NOT NULL COMMENT '用户2的ID',
|
||||||
|
`last_message` varchar(500) DEFAULT '' COMMENT '最后一条消息内容',
|
||||||
|
`last_message_time` datetime DEFAULT NULL COMMENT '最后一条消息时间',
|
||||||
|
`user1_unread_count` int(11) DEFAULT 0 COMMENT '用户1的未读数量',
|
||||||
|
`user2_unread_count` int(11) DEFAULT 0 COMMENT '用户2的未读数量',
|
||||||
|
`user1_deleted` tinyint(1) DEFAULT 0 COMMENT '用户1是否删除会话',
|
||||||
|
`user2_deleted` tinyint(1) DEFAULT 0 COMMENT '用户2是否删除会话',
|
||||||
|
`user1_muted` tinyint(1) DEFAULT 0 COMMENT '用户1是否静音',
|
||||||
|
`user2_muted` tinyint(1) DEFAULT 0 COMMENT '用户2是否静音',
|
||||||
|
`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||||
|
`update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
KEY `idx_user1_id` (`user1_id`),
|
||||||
|
KEY `idx_user2_id` (`user2_id`),
|
||||||
|
KEY `idx_last_message_time` (`last_message_time`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='私聊会话表';
|
||||||
|
|
||||||
|
-- 私信消息表
|
||||||
|
CREATE TABLE IF NOT EXISTS `eb_private_message` (
|
||||||
|
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '消息ID',
|
||||||
|
`conversation_id` bigint(20) NOT NULL COMMENT '会话ID',
|
||||||
|
`sender_id` int(11) NOT NULL COMMENT '发送者用户ID',
|
||||||
|
`receiver_id` int(11) NOT NULL COMMENT '接收者用户ID',
|
||||||
|
`content` text COMMENT '消息内容',
|
||||||
|
`message_type` varchar(20) DEFAULT 'text' COMMENT '消息类型: text, image, file',
|
||||||
|
`status` varchar(20) DEFAULT 'sent' COMMENT '消息状态: sending, sent, read',
|
||||||
|
`sender_deleted` tinyint(1) DEFAULT 0 COMMENT '发送者是否删除',
|
||||||
|
`receiver_deleted` tinyint(1) DEFAULT 0 COMMENT '接收者是否删除',
|
||||||
|
`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||||
|
`read_time` datetime DEFAULT NULL COMMENT '已读时间',
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
KEY `idx_conversation_id` (`conversation_id`),
|
||||||
|
KEY `idx_sender_id` (`sender_id`),
|
||||||
|
KEY `idx_receiver_id` (`receiver_id`),
|
||||||
|
KEY `idx_create_time` (`create_time`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='私信消息表';
|
||||||
|
|
@ -8,8 +8,8 @@ import android.os.Bundle;
|
||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
import android.os.Looper;
|
import android.os.Looper;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
|
import android.util.Log;
|
||||||
import android.view.KeyEvent;
|
import android.view.KeyEvent;
|
||||||
import android.view.MenuItem;
|
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
|
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
|
@ -19,22 +19,47 @@ import androidx.appcompat.widget.PopupMenu;
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||||
|
|
||||||
import com.example.livestreaming.databinding.ActivityConversationBinding;
|
import com.example.livestreaming.databinding.ActivityConversationBinding;
|
||||||
|
import com.example.livestreaming.net.AuthStore;
|
||||||
import com.google.android.material.snackbar.Snackbar;
|
import com.google.android.material.snackbar.Snackbar;
|
||||||
|
|
||||||
|
import org.json.JSONArray;
|
||||||
|
import org.json.JSONObject;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
import okhttp3.Call;
|
||||||
|
import okhttp3.Callback;
|
||||||
|
import okhttp3.MediaType;
|
||||||
|
import okhttp3.OkHttpClient;
|
||||||
|
import okhttp3.Request;
|
||||||
|
import okhttp3.RequestBody;
|
||||||
|
import okhttp3.Response;
|
||||||
|
|
||||||
public class ConversationActivity extends AppCompatActivity {
|
public class ConversationActivity extends AppCompatActivity {
|
||||||
|
|
||||||
|
private static final String TAG = "ConversationActivity";
|
||||||
private static final String EXTRA_CONVERSATION_ID = "extra_conversation_id";
|
private static final String EXTRA_CONVERSATION_ID = "extra_conversation_id";
|
||||||
private static final String EXTRA_CONVERSATION_TITLE = "extra_conversation_title";
|
private static final String EXTRA_CONVERSATION_TITLE = "extra_conversation_title";
|
||||||
|
private static final String EXTRA_UNREAD_COUNT = "extra_unread_count";
|
||||||
|
private static final int PAGE_SIZE = 20;
|
||||||
|
private static final long POLL_INTERVAL = 3000; // 3秒轮询一次
|
||||||
|
|
||||||
private ActivityConversationBinding binding;
|
private ActivityConversationBinding binding;
|
||||||
|
private final OkHttpClient httpClient = new OkHttpClient();
|
||||||
|
|
||||||
private ConversationMessagesAdapter adapter;
|
private ConversationMessagesAdapter adapter;
|
||||||
private final List<ChatMessage> messages = new ArrayList<>();
|
private final List<ChatMessage> messages = new ArrayList<>();
|
||||||
private Handler handler;
|
private Handler handler;
|
||||||
private Runnable statusUpdateRunnable;
|
private Runnable pollRunnable;
|
||||||
|
private boolean isPolling = false;
|
||||||
|
|
||||||
|
private String conversationId;
|
||||||
|
private int currentPage = 1;
|
||||||
|
private int initialUnreadCount = 0;
|
||||||
|
private String currentUserId;
|
||||||
|
|
||||||
public static void start(Context context, String conversationId, String title) {
|
public static void start(Context context, String conversationId, String title) {
|
||||||
Intent intent = new Intent(context, ConversationActivity.class);
|
Intent intent = new Intent(context, ConversationActivity.class);
|
||||||
|
|
@ -43,10 +68,6 @@ public class ConversationActivity extends AppCompatActivity {
|
||||||
context.startActivity(intent);
|
context.startActivity(intent);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final String EXTRA_UNREAD_COUNT = "extra_unread_count";
|
|
||||||
|
|
||||||
private int initialUnreadCount = 0;
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(@Nullable Bundle savedInstanceState) {
|
protected void onCreate(@Nullable Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
|
|
@ -58,8 +79,17 @@ public class ConversationActivity extends AppCompatActivity {
|
||||||
String title = getIntent() != null ? getIntent().getStringExtra(EXTRA_CONVERSATION_TITLE) : null;
|
String title = getIntent() != null ? getIntent().getStringExtra(EXTRA_CONVERSATION_TITLE) : null;
|
||||||
binding.titleText.setText(title != null ? title : "会话");
|
binding.titleText.setText(title != null ? title : "会话");
|
||||||
|
|
||||||
// 获取该会话的初始未读数量
|
conversationId = getIntent() != null ? getIntent().getStringExtra(EXTRA_CONVERSATION_ID) : null;
|
||||||
initialUnreadCount = getIntent() != null ? getIntent().getIntExtra(EXTRA_UNREAD_COUNT, 0) : 0;
|
initialUnreadCount = getIntent() != null ? getIntent().getIntExtra(EXTRA_UNREAD_COUNT, 0) : 0;
|
||||||
|
|
||||||
|
// 先尝试从 AuthStore 获取 userId
|
||||||
|
currentUserId = AuthStore.getUserId(this);
|
||||||
|
Log.d(TAG, "从AuthStore获取的用户ID: " + currentUserId);
|
||||||
|
|
||||||
|
// 如果 userId 为空,尝试从用户信息接口获取
|
||||||
|
if (currentUserId == null || currentUserId.isEmpty()) {
|
||||||
|
fetchCurrentUserId();
|
||||||
|
}
|
||||||
|
|
||||||
binding.backButton.setOnClickListener(new DebounceClickListener() {
|
binding.backButton.setOnClickListener(new DebounceClickListener() {
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -71,28 +101,55 @@ public class ConversationActivity extends AppCompatActivity {
|
||||||
setupMessages();
|
setupMessages();
|
||||||
setupInput();
|
setupInput();
|
||||||
|
|
||||||
// TODO: 接入后端接口 - 标记会话消息为已读
|
// 标记会话为已读
|
||||||
// 接口路径: POST /api/conversations/{conversationId}/read
|
if (initialUnreadCount > 0 && conversationId != null) {
|
||||||
// 请求参数:
|
markConversationAsRead();
|
||||||
// - conversationId: 会话ID(路径参数)
|
|
||||||
// - userId: 当前用户ID(从token中获取)
|
|
||||||
// 返回数据格式: ApiResponse<{success: boolean}>
|
|
||||||
// 用户进入会话时,标记该会话的所有消息为已读,减少未读数量
|
|
||||||
if (initialUnreadCount > 0) {
|
|
||||||
UnreadMessageManager.decrementUnreadCount(this, initialUnreadCount);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onPause() {
|
protected void onPause() {
|
||||||
super.onPause();
|
super.onPause();
|
||||||
// 当用户离开会话时,更新总未读数量(如果会话未读数量已减少)
|
stopPolling();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onResume() {
|
||||||
|
super.onResume();
|
||||||
|
startPolling();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 开始轮询新消息
|
||||||
|
*/
|
||||||
|
private void startPolling() {
|
||||||
|
if (isPolling) return;
|
||||||
|
isPolling = true;
|
||||||
|
|
||||||
|
pollRunnable = new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
if (!isPolling || isFinishing() || isDestroyed()) return;
|
||||||
|
loadMessagesFromServer();
|
||||||
|
handler.postDelayed(this, POLL_INTERVAL);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// 延迟开始轮询,避免和初始加载冲突
|
||||||
|
handler.postDelayed(pollRunnable, POLL_INTERVAL);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 停止轮询
|
||||||
|
*/
|
||||||
|
private void stopPolling() {
|
||||||
|
isPolling = false;
|
||||||
|
if (pollRunnable != null && handler != null) {
|
||||||
|
handler.removeCallbacks(pollRunnable);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setupMessages() {
|
private void setupMessages() {
|
||||||
adapter = new ConversationMessagesAdapter();
|
adapter = new ConversationMessagesAdapter();
|
||||||
|
|
||||||
// 设置长按监听
|
|
||||||
adapter.setOnMessageLongClickListener((message, position, view) -> {
|
adapter.setOnMessageLongClickListener((message, position, view) -> {
|
||||||
showMessageMenu(message, position, view);
|
showMessageMenu(message, position, view);
|
||||||
});
|
});
|
||||||
|
|
@ -102,43 +159,224 @@ public class ConversationActivity extends AppCompatActivity {
|
||||||
binding.messagesRecyclerView.setLayoutManager(layoutManager);
|
binding.messagesRecyclerView.setLayoutManager(layoutManager);
|
||||||
binding.messagesRecyclerView.setAdapter(adapter);
|
binding.messagesRecyclerView.setAdapter(adapter);
|
||||||
|
|
||||||
// TODO: 接入后端接口 - 获取会话消息列表
|
loadMessagesFromServer();
|
||||||
// 接口路径: GET /api/conversations/{conversationId}/messages
|
|
||||||
// 请求参数:
|
|
||||||
// - conversationId: 会话ID(路径参数)
|
|
||||||
// - page (可选): 页码,用于分页加载历史消息
|
|
||||||
// - pageSize (可选): 每页数量,默认20
|
|
||||||
// - beforeMessageId (可选): 获取指定消息ID之前的消息,用于上拉加载更多
|
|
||||||
// 返回数据格式: ApiResponse<List<ChatMessage>>
|
|
||||||
// ChatMessage对象应包含: messageId, userId, username, avatarUrl, message, timestamp, status等字段
|
|
||||||
// 消息列表应按时间正序排列(最早的在前面)
|
|
||||||
messages.clear();
|
|
||||||
String title = binding.titleText.getText() != null ? binding.titleText.getText().toString() : "";
|
|
||||||
ChatMessage incomingMsg = new ChatMessage(title, "你好~");
|
|
||||||
incomingMsg.setStatus(ChatMessage.MessageStatus.SENT);
|
|
||||||
messages.add(incomingMsg);
|
|
||||||
|
|
||||||
ChatMessage outgoingMsg = new ChatMessage("我", "在的,有什么需要帮忙?");
|
|
||||||
outgoingMsg.setStatus(ChatMessage.MessageStatus.READ);
|
|
||||||
messages.add(outgoingMsg);
|
|
||||||
|
|
||||||
adapter.submitList(new ArrayList<>(messages));
|
|
||||||
scrollToBottom();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从服务器获取当前用户ID
|
||||||
|
*/
|
||||||
|
private void fetchCurrentUserId() {
|
||||||
|
String token = AuthStore.getToken(this);
|
||||||
|
if (token == null) {
|
||||||
|
Log.w(TAG, "未登录,无法获取用户ID");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String url = ApiConfig.getBaseUrl() + "/api/front/user/info";
|
||||||
|
Log.d(TAG, "获取用户信息: " + url);
|
||||||
|
|
||||||
|
Request request = new Request.Builder()
|
||||||
|
.url(url)
|
||||||
|
.addHeader("Authori-zation", token)
|
||||||
|
.get()
|
||||||
|
.build();
|
||||||
|
|
||||||
|
httpClient.newCall(request).enqueue(new Callback() {
|
||||||
|
@Override
|
||||||
|
public void onFailure(Call call, IOException e) {
|
||||||
|
Log.e(TAG, "获取用户信息失败", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onResponse(Call call, Response response) throws IOException {
|
||||||
|
String body = response.body() != null ? response.body().string() : "";
|
||||||
|
Log.d(TAG, "用户信息响应: " + body);
|
||||||
|
try {
|
||||||
|
JSONObject json = new JSONObject(body);
|
||||||
|
if (json.optInt("code", -1) == 200) {
|
||||||
|
JSONObject data = json.optJSONObject("data");
|
||||||
|
if (data != null) {
|
||||||
|
int uid = data.optInt("uid", 0);
|
||||||
|
if (uid > 0) {
|
||||||
|
currentUserId = String.valueOf(uid);
|
||||||
|
// 保存到 AuthStore
|
||||||
|
AuthStore.setUserInfo(ConversationActivity.this, currentUserId, data.optString("nickname", ""));
|
||||||
|
Log.d(TAG, "从服务器获取到用户ID: " + currentUserId);
|
||||||
|
// 重新加载消息以正确显示
|
||||||
|
runOnUiThread(() -> loadMessagesFromServer());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "解析用户信息失败", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从服务器加载消息列表
|
||||||
|
*/
|
||||||
|
private void loadMessagesFromServer() {
|
||||||
|
String token = AuthStore.getToken(this);
|
||||||
|
if (token == null || conversationId == null) {
|
||||||
|
Log.w(TAG, "未登录或会话ID为空");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String url = ApiConfig.getBaseUrl() + "/api/front/conversations/" + conversationId + "/messages?page=" + currentPage + "&pageSize=" + PAGE_SIZE;
|
||||||
|
Log.d(TAG, "加载消息列表: " + url);
|
||||||
|
|
||||||
|
Request request = new Request.Builder()
|
||||||
|
.url(url)
|
||||||
|
.addHeader("Authori-zation", token)
|
||||||
|
.get()
|
||||||
|
.build();
|
||||||
|
|
||||||
|
httpClient.newCall(request).enqueue(new Callback() {
|
||||||
|
@Override
|
||||||
|
public void onFailure(Call call, IOException e) {
|
||||||
|
Log.e(TAG, "加载消息列表失败", e);
|
||||||
|
runOnUiThread(() -> Snackbar.make(binding.getRoot(), "加载消息失败", Snackbar.LENGTH_SHORT).show());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onResponse(Call call, Response response) throws IOException {
|
||||||
|
String body = response.body() != null ? response.body().string() : "";
|
||||||
|
Log.d(TAG, "消息列表响应: " + body);
|
||||||
|
runOnUiThread(() -> parseMessages(body));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void parseMessages(String body) {
|
||||||
|
try {
|
||||||
|
JSONObject json = new JSONObject(body);
|
||||||
|
if (json.optInt("code", -1) == 200) {
|
||||||
|
JSONArray data = json.optJSONArray("data");
|
||||||
|
messages.clear();
|
||||||
|
if (data != null) {
|
||||||
|
for (int i = 0; i < data.length(); i++) {
|
||||||
|
JSONObject item = data.getJSONObject(i);
|
||||||
|
ChatMessage msg = parseChatMessage(item);
|
||||||
|
if (msg != null) {
|
||||||
|
messages.add(msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 消息按时间倒序返回,需要反转为正序显示
|
||||||
|
Collections.reverse(messages);
|
||||||
|
adapter.submitList(new ArrayList<>(messages));
|
||||||
|
scrollToBottom();
|
||||||
|
} else {
|
||||||
|
String msg = json.optString("message", "加载失败");
|
||||||
|
Snackbar.make(binding.getRoot(), msg, Snackbar.LENGTH_SHORT).show();
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "解析消息列表失败", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private ChatMessage parseChatMessage(JSONObject item) {
|
||||||
|
try {
|
||||||
|
String messageId = item.optString("messageId", "");
|
||||||
|
int senderId = item.optInt("userId", 0); // 后端返回的 userId 实际是 senderId
|
||||||
|
String username = item.optString("username", "未知用户");
|
||||||
|
String message = item.optString("message", "");
|
||||||
|
long timestamp = item.optLong("timestamp", System.currentTimeMillis());
|
||||||
|
String status = item.optString("status", "sent");
|
||||||
|
String avatarUrl = item.optString("avatarUrl", "");
|
||||||
|
boolean isSystem = item.optBoolean("isSystemMessage", false);
|
||||||
|
|
||||||
|
// 判断是否是自己发送的消息
|
||||||
|
// 每次都重新从 AuthStore 获取最新的 userId,确保登录后能正确获取
|
||||||
|
String myUserId = AuthStore.getUserId(this);
|
||||||
|
if (myUserId == null || myUserId.isEmpty()) {
|
||||||
|
myUserId = currentUserId;
|
||||||
|
} else {
|
||||||
|
currentUserId = myUserId; // 同步更新
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean isMine = false;
|
||||||
|
if (myUserId != null && !myUserId.isEmpty() && senderId > 0) {
|
||||||
|
isMine = myUserId.equals(String.valueOf(senderId));
|
||||||
|
}
|
||||||
|
Log.d(TAG, "消息判断: myUserId=" + myUserId + ", senderId=" + senderId + ", isMine=" + isMine);
|
||||||
|
|
||||||
|
String displayName = isMine ? "我" : username;
|
||||||
|
|
||||||
|
ChatMessage.MessageStatus msgStatus;
|
||||||
|
switch (status) {
|
||||||
|
case "sending":
|
||||||
|
msgStatus = ChatMessage.MessageStatus.SENDING;
|
||||||
|
break;
|
||||||
|
case "read":
|
||||||
|
msgStatus = ChatMessage.MessageStatus.READ;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
msgStatus = ChatMessage.MessageStatus.SENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
ChatMessage chatMessage = new ChatMessage(messageId, displayName, message, timestamp, isSystem, msgStatus);
|
||||||
|
chatMessage.setAvatarUrl(avatarUrl);
|
||||||
|
return chatMessage;
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "解析消息失败", e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 标记会话为已读
|
||||||
|
*/
|
||||||
|
private void markConversationAsRead() {
|
||||||
|
String token = AuthStore.getToken(this);
|
||||||
|
if (token == null || conversationId == null) return;
|
||||||
|
|
||||||
|
String url = ApiConfig.getBaseUrl() + "/api/front/conversations/" + conversationId + "/read";
|
||||||
|
Log.d(TAG, "标记已读: " + url);
|
||||||
|
|
||||||
|
Request request = new Request.Builder()
|
||||||
|
.url(url)
|
||||||
|
.addHeader("Authori-zation", token)
|
||||||
|
.post(RequestBody.create("", MediaType.parse("application/json")))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
httpClient.newCall(request).enqueue(new Callback() {
|
||||||
|
@Override
|
||||||
|
public void onFailure(Call call, IOException e) {
|
||||||
|
Log.e(TAG, "标记已读失败", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onResponse(Call call, Response response) throws IOException {
|
||||||
|
String body = response.body() != null ? response.body().string() : "";
|
||||||
|
Log.d(TAG, "标记已读响应: " + body);
|
||||||
|
runOnUiThread(() -> {
|
||||||
|
if (initialUnreadCount > 0) {
|
||||||
|
UnreadMessageManager.decrementUnreadCount(ConversationActivity.this, initialUnreadCount);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private void showMessageMenu(ChatMessage message, int position, View anchorView) {
|
private void showMessageMenu(ChatMessage message, int position, View anchorView) {
|
||||||
PopupMenu popupMenu = new PopupMenu(this, anchorView);
|
PopupMenu popupMenu = new PopupMenu(this, anchorView);
|
||||||
popupMenu.getMenu().add(0, 0, 0, "复制");
|
popupMenu.getMenu().add(0, 0, 0, "复制");
|
||||||
popupMenu.getMenu().add(0, 1, 0, "删除");
|
// 只有自己发送的消息才能删除
|
||||||
|
if ("我".equals(message.getUsername())) {
|
||||||
|
popupMenu.getMenu().add(0, 1, 0, "删除");
|
||||||
|
}
|
||||||
|
|
||||||
popupMenu.setOnMenuItemClickListener(item -> {
|
popupMenu.setOnMenuItemClickListener(item -> {
|
||||||
if (item.getItemId() == 0) {
|
if (item.getItemId() == 0) {
|
||||||
// 复制消息
|
|
||||||
copyMessage(message);
|
copyMessage(message);
|
||||||
return true;
|
return true;
|
||||||
} else if (item.getItemId() == 1) {
|
} else if (item.getItemId() == 1) {
|
||||||
// 删除消息
|
deleteMessage(message, position);
|
||||||
deleteMessage(position);
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -154,30 +392,63 @@ public class ConversationActivity extends AppCompatActivity {
|
||||||
Snackbar.make(binding.getRoot(), "已复制到剪贴板", Snackbar.LENGTH_SHORT).show();
|
Snackbar.make(binding.getRoot(), "已复制到剪贴板", Snackbar.LENGTH_SHORT).show();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void deleteMessage(int position) {
|
private void deleteMessage(ChatMessage message, int position) {
|
||||||
// TODO: 接入后端接口 - 删除消息
|
|
||||||
// 接口路径: DELETE /api/messages/{messageId}
|
|
||||||
// 请求参数:
|
|
||||||
// - messageId: 消息ID(路径参数)
|
|
||||||
// - userId: 当前用户ID(从token中获取,验证是否为消息发送者)
|
|
||||||
// 返回数据格式: ApiResponse<{success: boolean}>
|
|
||||||
// 删除成功后,从本地消息列表移除
|
|
||||||
if (position < 0 || position >= messages.size()) return;
|
if (position < 0 || position >= messages.size()) return;
|
||||||
|
|
||||||
new AlertDialog.Builder(this)
|
new AlertDialog.Builder(this)
|
||||||
.setTitle("删除消息")
|
.setTitle("删除消息")
|
||||||
.setMessage("确定要删除这条消息吗?")
|
.setMessage("确定要删除这条消息吗?")
|
||||||
.setPositiveButton("删除", (dialog, which) -> {
|
.setPositiveButton("删除", (dialog, which) -> deleteMessageFromServer(message, position))
|
||||||
messages.remove(position);
|
|
||||||
adapter.submitList(new ArrayList<>(messages));
|
|
||||||
Snackbar.make(binding.getRoot(), "消息已删除", Snackbar.LENGTH_SHORT).show();
|
|
||||||
})
|
|
||||||
.setNegativeButton("取消", null)
|
.setNegativeButton("取消", null)
|
||||||
.show();
|
.show();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void deleteMessageFromServer(ChatMessage message, int position) {
|
||||||
|
String token = AuthStore.getToken(this);
|
||||||
|
if (token == null) return;
|
||||||
|
|
||||||
|
String messageId = message.getMessageId();
|
||||||
|
String url = ApiConfig.getBaseUrl() + "/api/front/conversations/messages/" + messageId;
|
||||||
|
Log.d(TAG, "删除消息: " + url);
|
||||||
|
|
||||||
|
Request request = new Request.Builder()
|
||||||
|
.url(url)
|
||||||
|
.addHeader("Authori-zation", token)
|
||||||
|
.delete()
|
||||||
|
.build();
|
||||||
|
|
||||||
|
httpClient.newCall(request).enqueue(new Callback() {
|
||||||
|
@Override
|
||||||
|
public void onFailure(Call call, IOException e) {
|
||||||
|
Log.e(TAG, "删除消息失败", e);
|
||||||
|
runOnUiThread(() -> Snackbar.make(binding.getRoot(), "删除失败", Snackbar.LENGTH_SHORT).show());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onResponse(Call call, Response response) throws IOException {
|
||||||
|
String body = response.body() != null ? response.body().string() : "";
|
||||||
|
Log.d(TAG, "删除消息响应: " + body);
|
||||||
|
runOnUiThread(() -> {
|
||||||
|
try {
|
||||||
|
JSONObject json = new JSONObject(body);
|
||||||
|
if (json.optInt("code", -1) == 200) {
|
||||||
|
messages.remove(position);
|
||||||
|
adapter.submitList(new ArrayList<>(messages));
|
||||||
|
Snackbar.make(binding.getRoot(), "消息已删除", Snackbar.LENGTH_SHORT).show();
|
||||||
|
} else {
|
||||||
|
Snackbar.make(binding.getRoot(), json.optString("message", "删除失败"), Snackbar.LENGTH_SHORT).show();
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "解析删除响应失败", e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
private void setupInput() {
|
private void setupInput() {
|
||||||
binding.sendButton.setOnClickListener(new DebounceClickListener(300) { // 发送按钮使用300ms防抖
|
binding.sendButton.setOnClickListener(new DebounceClickListener(300) {
|
||||||
@Override
|
@Override
|
||||||
public void onDebouncedClick(View v) {
|
public void onDebouncedClick(View v) {
|
||||||
sendMessage();
|
sendMessage();
|
||||||
|
|
@ -198,55 +469,91 @@ public class ConversationActivity extends AppCompatActivity {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void sendMessage() {
|
private void sendMessage() {
|
||||||
// 检查登录状态,发送私信需要登录
|
|
||||||
if (!AuthHelper.requireLoginWithToast(this, "发送私信需要登录")) {
|
if (!AuthHelper.requireLoginWithToast(this, "发送私信需要登录")) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: 接入后端接口 - 发送私信消息
|
|
||||||
// 接口路径: POST /api/conversations/{conversationId}/messages
|
|
||||||
// 请求参数:
|
|
||||||
// - conversationId: 会话ID(路径参数)
|
|
||||||
// - message: 消息内容
|
|
||||||
// - userId: 发送者用户ID(从token中获取)
|
|
||||||
// 返回数据格式: ApiResponse<ChatMessage>
|
|
||||||
// ChatMessage对象应包含: messageId, userId, username, avatarUrl, message, timestamp, status等字段
|
|
||||||
// 发送成功后,更新消息状态为SENT,并添加到消息列表
|
|
||||||
// TODO: 接入后端接口 - 接收对方已读状态(WebSocket或轮询)
|
|
||||||
// 方案1: WebSocket实时推送
|
|
||||||
// - 连接: ws://api.example.com/conversations/{conversationId}
|
|
||||||
// - 接收消息格式: {type: "read", messageId: "xxx"} 或 {type: "new_message", data: ChatMessage}
|
|
||||||
// 方案2: 轮询检查消息状态
|
|
||||||
// - 接口路径: GET /api/conversations/{conversationId}/messages/{messageId}/status
|
|
||||||
// - 返回数据格式: ApiResponse<{status: "sent"|"read"}>
|
|
||||||
String text = binding.messageInput.getText() != null ? binding.messageInput.getText().toString().trim() : "";
|
String text = binding.messageInput.getText() != null ? binding.messageInput.getText().toString().trim() : "";
|
||||||
if (TextUtils.isEmpty(text)) return;
|
if (TextUtils.isEmpty(text)) return;
|
||||||
|
if (conversationId == null) {
|
||||||
|
Snackbar.make(binding.getRoot(), "会话ID无效", Snackbar.LENGTH_SHORT).show();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 先在本地显示消息(发送中状态)
|
||||||
ChatMessage newMessage = new ChatMessage("我", text);
|
ChatMessage newMessage = new ChatMessage("我", text);
|
||||||
newMessage.setStatus(ChatMessage.MessageStatus.SENDING);
|
newMessage.setStatus(ChatMessage.MessageStatus.SENDING);
|
||||||
messages.add(newMessage);
|
messages.add(newMessage);
|
||||||
adapter.submitList(new ArrayList<>(messages));
|
adapter.submitList(new ArrayList<>(messages));
|
||||||
binding.messageInput.setText("");
|
binding.messageInput.setText("");
|
||||||
scrollToBottom();
|
scrollToBottom();
|
||||||
|
|
||||||
// 模拟消息发送过程:发送中 -> 已发送 -> 已读
|
// 发送到服务器
|
||||||
if (statusUpdateRunnable != null) {
|
sendMessageToServer(text, newMessage);
|
||||||
handler.removeCallbacks(statusUpdateRunnable);
|
}
|
||||||
|
|
||||||
|
private void sendMessageToServer(String text, ChatMessage localMessage) {
|
||||||
|
String token = AuthStore.getToken(this);
|
||||||
|
if (token == null) return;
|
||||||
|
|
||||||
|
String url = ApiConfig.getBaseUrl() + "/api/front/conversations/" + conversationId + "/messages";
|
||||||
|
Log.d(TAG, "发送消息: " + url);
|
||||||
|
|
||||||
|
try {
|
||||||
|
JSONObject body = new JSONObject();
|
||||||
|
body.put("message", text);
|
||||||
|
body.put("messageType", "text");
|
||||||
|
|
||||||
|
Request request = new Request.Builder()
|
||||||
|
.url(url)
|
||||||
|
.addHeader("Authori-zation", token)
|
||||||
|
.post(RequestBody.create(body.toString(), MediaType.parse("application/json")))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
httpClient.newCall(request).enqueue(new Callback() {
|
||||||
|
@Override
|
||||||
|
public void onFailure(Call call, IOException e) {
|
||||||
|
Log.e(TAG, "发送消息失败", e);
|
||||||
|
runOnUiThread(() -> {
|
||||||
|
Snackbar.make(binding.getRoot(), "发送失败", Snackbar.LENGTH_SHORT).show();
|
||||||
|
// 移除发送失败的消息
|
||||||
|
messages.remove(localMessage);
|
||||||
|
adapter.submitList(new ArrayList<>(messages));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onResponse(Call call, Response response) throws IOException {
|
||||||
|
String responseBody = response.body() != null ? response.body().string() : "";
|
||||||
|
Log.d(TAG, "发送消息响应: " + responseBody);
|
||||||
|
runOnUiThread(() -> handleSendResponse(responseBody, localMessage));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "构建请求失败", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleSendResponse(String responseBody, ChatMessage localMessage) {
|
||||||
|
try {
|
||||||
|
JSONObject json = new JSONObject(responseBody);
|
||||||
|
if (json.optInt("code", -1) == 200) {
|
||||||
|
JSONObject data = json.optJSONObject("data");
|
||||||
|
if (data != null) {
|
||||||
|
// 更新本地消息的ID和状态
|
||||||
|
localMessage.setMessageId(data.optString("messageId", localMessage.getMessageId()));
|
||||||
|
localMessage.setStatus(ChatMessage.MessageStatus.SENT);
|
||||||
|
adapter.notifyItemChanged(messages.indexOf(localMessage));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
String msg = json.optString("message", "发送失败");
|
||||||
|
Snackbar.make(binding.getRoot(), msg, Snackbar.LENGTH_SHORT).show();
|
||||||
|
messages.remove(localMessage);
|
||||||
|
adapter.submitList(new ArrayList<>(messages));
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "解析发送响应失败", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
statusUpdateRunnable = () -> {
|
|
||||||
// 1秒后更新为已发送
|
|
||||||
newMessage.setStatus(ChatMessage.MessageStatus.SENT);
|
|
||||||
adapter.notifyItemChanged(messages.size() - 1);
|
|
||||||
|
|
||||||
// 再2秒后更新为已读
|
|
||||||
handler.postDelayed(() -> {
|
|
||||||
newMessage.setStatus(ChatMessage.MessageStatus.READ);
|
|
||||||
adapter.notifyItemChanged(messages.size() - 1);
|
|
||||||
}, 2000);
|
|
||||||
};
|
|
||||||
|
|
||||||
handler.postDelayed(statusUpdateRunnable, 1000);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void scrollToBottom() {
|
private void scrollToBottom() {
|
||||||
|
|
@ -262,11 +569,7 @@ public class ConversationActivity extends AppCompatActivity {
|
||||||
@Override
|
@Override
|
||||||
protected void onDestroy() {
|
protected void onDestroy() {
|
||||||
super.onDestroy();
|
super.onDestroy();
|
||||||
// 清理Handler中的延迟任务,防止内存泄漏
|
stopPolling();
|
||||||
if (handler != null && statusUpdateRunnable != null) {
|
|
||||||
handler.removeCallbacks(statusUpdateRunnable);
|
|
||||||
}
|
|
||||||
handler = null;
|
handler = null;
|
||||||
statusUpdateRunnable = null;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -300,17 +300,17 @@ public class ConversationMessagesAdapter extends ListAdapter<ChatMessage, Recycl
|
||||||
switch (message.getStatus()) {
|
switch (message.getStatus()) {
|
||||||
case SENDING:
|
case SENDING:
|
||||||
statusIcon.setImageResource(R.drawable.ic_clock_24);
|
statusIcon.setImageResource(R.drawable.ic_clock_24);
|
||||||
statusIcon.setColorFilter(itemView.getContext().getColor(android.R.color.darker_gray));
|
statusIcon.setColorFilter(android.graphics.Color.GRAY, android.graphics.PorterDuff.Mode.SRC_IN);
|
||||||
statusIcon.setVisibility(View.VISIBLE);
|
statusIcon.setVisibility(View.VISIBLE);
|
||||||
break;
|
break;
|
||||||
case SENT:
|
case SENT:
|
||||||
statusIcon.setImageResource(R.drawable.ic_check_24);
|
statusIcon.setImageResource(R.drawable.ic_check_24);
|
||||||
statusIcon.setColorFilter(itemView.getContext().getColor(android.R.color.darker_gray));
|
statusIcon.setColorFilter(android.graphics.Color.GRAY, android.graphics.PorterDuff.Mode.SRC_IN);
|
||||||
statusIcon.setVisibility(View.VISIBLE);
|
statusIcon.setVisibility(View.VISIBLE);
|
||||||
break;
|
break;
|
||||||
case READ:
|
case READ:
|
||||||
statusIcon.setImageResource(R.drawable.ic_check_double_24);
|
statusIcon.setImageResource(R.drawable.ic_check_double_24);
|
||||||
statusIcon.setColorFilter(0xFF4CAF50);
|
statusIcon.setColorFilter(android.graphics.Color.parseColor("#4CAF50"), android.graphics.PorterDuff.Mode.SRC_IN);
|
||||||
statusIcon.setVisibility(View.VISIBLE);
|
statusIcon.setVisibility(View.VISIBLE);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
|
|
|
||||||
|
|
@ -100,23 +100,7 @@ public class LoginActivity extends AppCompatActivity {
|
||||||
binding.loginButton.setEnabled(false);
|
binding.loginButton.setEnabled(false);
|
||||||
binding.loadingProgress.setVisibility(View.VISIBLE);
|
binding.loadingProgress.setVisibility(View.VISIBLE);
|
||||||
|
|
||||||
// ============================================
|
// 调用后端登录接口
|
||||||
// 测试模式:直接使用演示模式登录,跳过网络请求
|
|
||||||
// 当后端接口接入后,可以删除此代码,恢复下面的网络请求
|
|
||||||
// ============================================
|
|
||||||
// 模拟网络请求延迟(让用户看到加载状态)
|
|
||||||
binding.getRoot().postDelayed(() -> {
|
|
||||||
handleDemoModeLogin(account);
|
|
||||||
}, 500); // 延迟500ms,模拟网络请求
|
|
||||||
|
|
||||||
// 以下代码在测试模式下被注释,接入后端后可以恢复
|
|
||||||
/*
|
|
||||||
// TODO: 接入后端接口 - 用户登录
|
|
||||||
// 接口路径: POST /api/front/login(ApiService中已定义)
|
|
||||||
// 请求参数: LoginRequest {account: string, password: string}
|
|
||||||
// 返回数据格式: ApiResponse<LoginResponse>
|
|
||||||
// LoginResponse对象应包含: token, userId, nickname, avatarUrl等字段
|
|
||||||
// 登录成功后,保存token到AuthStore,并更新用户信息到本地SharedPreferences
|
|
||||||
ApiClient.getService(getApplicationContext()).login(new LoginRequest(account, password))
|
ApiClient.getService(getApplicationContext()).login(new LoginRequest(account, password))
|
||||||
.enqueue(new Callback<ApiResponse<LoginResponse>>() {
|
.enqueue(new Callback<ApiResponse<LoginResponse>>() {
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -128,15 +112,8 @@ public class LoginActivity extends AppCompatActivity {
|
||||||
ApiResponse<LoginResponse> body = response.body();
|
ApiResponse<LoginResponse> body = response.body();
|
||||||
LoginResponse loginData = body != null ? body.getData() : null;
|
LoginResponse loginData = body != null ? body.getData() : null;
|
||||||
|
|
||||||
// 如果响应不成功或数据无效,检查是否是后端未接入的情况
|
// 如果响应不成功或数据无效
|
||||||
if (!response.isSuccessful() || body == null || !body.isOk() || loginData == null) {
|
if (!response.isSuccessful() || body == null || !body.isOk() || loginData == null) {
|
||||||
// 如果是404、500等错误,可能是后端未接入,使用演示模式
|
|
||||||
if (!response.isSuccessful() && (response.code() == 404 || response.code() == 500 || response.code() == 502 || response.code() == 503)) {
|
|
||||||
// 后端服务未启动或未接入,使用演示模式
|
|
||||||
handleDemoModeLogin(account);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
String errorMsg = "登录失败";
|
String errorMsg = "登录失败";
|
||||||
if (body != null && !TextUtils.isEmpty(body.getMessage())) {
|
if (body != null && !TextUtils.isEmpty(body.getMessage())) {
|
||||||
errorMsg = body.getMessage();
|
errorMsg = body.getMessage();
|
||||||
|
|
@ -158,9 +135,10 @@ public class LoginActivity extends AppCompatActivity {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 保存用户信息到 AuthStore
|
// 保存用户信息到 AuthStore
|
||||||
AuthStore.setUserInfo(getApplicationContext(),
|
String uid = loginData.getUid();
|
||||||
loginData.getUid(),
|
String nickname = loginData.getNikeName();
|
||||||
loginData.getNikeName());
|
android.util.Log.d("LoginActivity", "登录返回的 uid: " + uid + ", nickname: " + nickname);
|
||||||
|
AuthStore.setUserInfo(getApplicationContext(), uid, nickname);
|
||||||
|
|
||||||
// 保存用户信息到本地 SharedPreferences
|
// 保存用户信息到本地 SharedPreferences
|
||||||
SharedPreferences prefs = getSharedPreferences("profile_prefs", MODE_PRIVATE);
|
SharedPreferences prefs = getSharedPreferences("profile_prefs", MODE_PRIVATE);
|
||||||
|
|
@ -171,8 +149,7 @@ public class LoginActivity extends AppCompatActivity {
|
||||||
prefs.edit().putString("profile_phone", loginData.getPhone()).apply();
|
prefs.edit().putString("profile_phone", loginData.getPhone()).apply();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 登录成功,返回上一页(如果是从其他页面跳转过来的)
|
// 登录成功
|
||||||
// 如果是直接打开登录页面,则跳转到主页面
|
|
||||||
Toast.makeText(LoginActivity.this, "登录成功", Toast.LENGTH_SHORT).show();
|
Toast.makeText(LoginActivity.this, "登录成功", Toast.LENGTH_SHORT).show();
|
||||||
|
|
||||||
// 检查是否有上一个Activity
|
// 检查是否有上一个Activity
|
||||||
|
|
@ -194,74 +171,14 @@ public class LoginActivity extends AppCompatActivity {
|
||||||
binding.loginButton.setEnabled(true);
|
binding.loginButton.setEnabled(true);
|
||||||
binding.loadingProgress.setVisibility(View.GONE);
|
binding.loadingProgress.setVisibility(View.GONE);
|
||||||
|
|
||||||
// 检测是否是网络连接错误(后端未启动)
|
|
||||||
boolean isNetworkError = false;
|
|
||||||
String errorMsg = "网络错误";
|
String errorMsg = "网络错误";
|
||||||
if (t != null) {
|
if (t != null && t.getMessage() != null) {
|
||||||
String msg = t.getMessage();
|
errorMsg = "网络错误:" + t.getMessage();
|
||||||
if (msg != null) {
|
|
||||||
if (msg.contains("Unable to resolve host") ||
|
|
||||||
msg.contains("timeout") ||
|
|
||||||
msg.contains("Connection refused") ||
|
|
||||||
msg.contains("Failed to connect")) {
|
|
||||||
isNetworkError = true;
|
|
||||||
errorMsg = "无法连接到服务器,已切换到演示模式";
|
|
||||||
} else {
|
|
||||||
errorMsg = "网络错误:" + msg;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果是网络连接错误(后端未接入),使用演示模式登录
|
|
||||||
if (isNetworkError) {
|
|
||||||
handleDemoModeLogin(account);
|
|
||||||
} else {
|
|
||||||
Toast.makeText(LoginActivity.this, errorMsg, Toast.LENGTH_LONG).show();
|
|
||||||
}
|
}
|
||||||
|
Toast.makeText(LoginActivity.this, errorMsg, Toast.LENGTH_LONG).show();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
*/
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 处理演示模式登录(测试模式:直接登录成功)
|
|
||||||
*/
|
|
||||||
private void handleDemoModeLogin(String account) {
|
|
||||||
// 恢复UI状态
|
|
||||||
isLoggingIn = false;
|
|
||||||
binding.loginButton.setEnabled(true);
|
|
||||||
binding.loadingProgress.setVisibility(View.GONE);
|
|
||||||
|
|
||||||
// 测试模式:允许任意账号密码登录(仅用于开发测试)
|
|
||||||
// 生成一个演示token(基于账号)
|
|
||||||
String demoToken = "demo_token_" + account.hashCode();
|
|
||||||
android.util.Log.d("LoginActivity", "演示模式 - 生成 token: " + demoToken);
|
|
||||||
AuthStore.setToken(getApplicationContext(), demoToken);
|
|
||||||
|
|
||||||
// 保存用户信息到本地
|
|
||||||
SharedPreferences prefs = getSharedPreferences("profile_prefs", MODE_PRIVATE);
|
|
||||||
String displayName = account.length() > 0 ? account : "演示用户";
|
|
||||||
prefs.edit()
|
|
||||||
.putString("profile_name", displayName)
|
|
||||||
.putString("user_id", "demo_user_" + account.hashCode())
|
|
||||||
.apply();
|
|
||||||
|
|
||||||
// 登录成功提示
|
|
||||||
Toast.makeText(this, "登录成功", Toast.LENGTH_SHORT).show();
|
|
||||||
|
|
||||||
// 登录成功,返回上一页(如果是从其他页面跳转过来的)
|
|
||||||
// 如果是直接打开登录页面,则跳转到主页面
|
|
||||||
// 检查是否有上一个Activity
|
|
||||||
if (isTaskRoot()) {
|
|
||||||
// 如果没有上一个Activity,跳转到主页面
|
|
||||||
Intent intent = new Intent(this, MainActivity.class);
|
|
||||||
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK);
|
|
||||||
startActivity(intent);
|
|
||||||
finish();
|
|
||||||
} else {
|
|
||||||
// 有上一个Activity,直接返回
|
|
||||||
finish();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -43,8 +43,10 @@ import org.json.JSONObject;
|
||||||
|
|
||||||
import okhttp3.Call;
|
import okhttp3.Call;
|
||||||
import okhttp3.Callback;
|
import okhttp3.Callback;
|
||||||
|
import okhttp3.MediaType;
|
||||||
import okhttp3.OkHttpClient;
|
import okhttp3.OkHttpClient;
|
||||||
import okhttp3.Request;
|
import okhttp3.Request;
|
||||||
|
import okhttp3.RequestBody;
|
||||||
import okhttp3.Response;
|
import okhttp3.Response;
|
||||||
|
|
||||||
public class MessagesActivity extends AppCompatActivity {
|
public class MessagesActivity extends AppCompatActivity {
|
||||||
|
|
@ -222,12 +224,13 @@ public class MessagesActivity extends AppCompatActivity {
|
||||||
try {
|
try {
|
||||||
JSONObject item = data.getJSONObject(i);
|
JSONObject item = data.getJSONObject(i);
|
||||||
String id = String.valueOf(item.opt("id"));
|
String id = String.valueOf(item.opt("id"));
|
||||||
String title = item.optString("otherUserName", "未知用户");
|
// 后端返回的字段名是 title,不是 otherUserName
|
||||||
|
String title = item.optString("title", item.optString("otherUserName", "未知用户"));
|
||||||
String lastMessage = item.optString("lastMessage", "");
|
String lastMessage = item.optString("lastMessage", "");
|
||||||
String timeText = item.optString("lastMessageTime", "");
|
String timeText = item.optString("timeText", item.optString("lastMessageTime", ""));
|
||||||
int unreadCount = item.optInt("unreadCount", 0);
|
int unreadCount = item.optInt("unreadCount", 0);
|
||||||
boolean isMuted = item.optBoolean("isMuted", false);
|
boolean isMuted = item.optBoolean("muted", item.optBoolean("isMuted", false));
|
||||||
String avatarUrl = item.optString("otherUserAvatar", "");
|
String avatarUrl = item.optString("avatarUrl", item.optString("otherUserAvatar", ""));
|
||||||
|
|
||||||
ConversationItem convItem = new ConversationItem(id, title, lastMessage, timeText, unreadCount, isMuted);
|
ConversationItem convItem = new ConversationItem(id, title, lastMessage, timeText, unreadCount, isMuted);
|
||||||
allConversations.add(convItem);
|
allConversations.add(convItem);
|
||||||
|
|
@ -575,67 +578,116 @@ public class MessagesActivity extends AppCompatActivity {
|
||||||
* 标记会话为已读
|
* 标记会话为已读
|
||||||
*/
|
*/
|
||||||
private void markAsReadAt(int position) {
|
private void markAsReadAt(int position) {
|
||||||
// TODO: 接入后端接口 - 标记会话为已读
|
|
||||||
// 接口路径: POST /api/conversations/{conversationId}/read
|
|
||||||
// 请求参数:
|
|
||||||
// - conversationId: 会话ID(路径参数)
|
|
||||||
// - userId: 当前用户ID(从token中获取)
|
|
||||||
// 返回数据格式: ApiResponse<{success: boolean}>
|
|
||||||
// 标记成功后,更新本地未读数量为0,并更新总未读数量
|
|
||||||
if (position < 0 || position >= conversations.size()) return;
|
if (position < 0 || position >= conversations.size()) return;
|
||||||
|
|
||||||
ConversationItem item = conversations.get(position);
|
ConversationItem item = conversations.get(position);
|
||||||
if (item == null) return;
|
if (item == null) return;
|
||||||
|
|
||||||
// 获取该会话的未读数量
|
|
||||||
int unreadCount = item.getUnreadCount();
|
int unreadCount = item.getUnreadCount();
|
||||||
|
if (unreadCount <= 0) return;
|
||||||
// 如果已经有未读消息,才需要更新
|
|
||||||
if (unreadCount > 0) {
|
String token = AuthStore.getToken(this);
|
||||||
// 将该会话的未读数量设为0
|
if (token == null) return;
|
||||||
// updateConversationUnreadCount 方法会自动重新计算总未读数量并更新徽章
|
|
||||||
updateConversationUnreadCount(item.getId(), 0);
|
String url = ApiConfig.getBaseUrl() + "/api/front/conversations/" + item.getId() + "/read";
|
||||||
}
|
Log.d(TAG, "标记已读: " + url);
|
||||||
|
|
||||||
|
Request request = new Request.Builder()
|
||||||
|
.url(url)
|
||||||
|
.addHeader("Authori-zation", token)
|
||||||
|
.post(RequestBody.create("", MediaType.parse("application/json")))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
httpClient.newCall(request).enqueue(new Callback() {
|
||||||
|
@Override
|
||||||
|
public void onFailure(Call call, IOException e) {
|
||||||
|
Log.e(TAG, "标记已读失败", e);
|
||||||
|
runOnUiThread(() -> Toast.makeText(MessagesActivity.this, "标记已读失败", Toast.LENGTH_SHORT).show());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onResponse(Call call, Response response) throws IOException {
|
||||||
|
String body = response.body() != null ? response.body().string() : "";
|
||||||
|
Log.d(TAG, "标记已读响应: " + body);
|
||||||
|
runOnUiThread(() -> {
|
||||||
|
try {
|
||||||
|
JSONObject json = new JSONObject(body);
|
||||||
|
if (json.optInt("code", -1) == 200) {
|
||||||
|
updateConversationUnreadCount(item.getId(), 0);
|
||||||
|
Toast.makeText(MessagesActivity.this, "已标记为已读", Toast.LENGTH_SHORT).show();
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "解析标记已读响应失败", e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 删除会话
|
* 删除会话
|
||||||
*/
|
*/
|
||||||
private void deleteConversationAt(int position) {
|
private void deleteConversationAt(int position) {
|
||||||
// TODO: 接入后端接口 - 删除会话
|
|
||||||
// 接口路径: DELETE /api/conversations/{conversationId}
|
|
||||||
// 请求参数:
|
|
||||||
// - conversationId: 会话ID(路径参数)
|
|
||||||
// - userId: 当前用户ID(从token中获取)
|
|
||||||
// 返回数据格式: ApiResponse<{success: boolean}>
|
|
||||||
// 删除成功后,从本地列表移除,并更新总未读数量
|
|
||||||
if (position < 0 || position >= conversations.size()) return;
|
if (position < 0 || position >= conversations.size()) return;
|
||||||
|
|
||||||
// 获取要删除的会话的未读数量
|
|
||||||
ConversationItem itemToDelete = conversations.get(position);
|
ConversationItem itemToDelete = conversations.get(position);
|
||||||
int unreadCountToRemove = itemToDelete != null ? itemToDelete.getUnreadCount() : 0;
|
if (itemToDelete == null) return;
|
||||||
String itemId = itemToDelete != null ? itemToDelete.getId() : null;
|
|
||||||
|
String token = AuthStore.getToken(this);
|
||||||
// 从当前显示列表和全部列表中删除
|
if (token == null) return;
|
||||||
conversations.remove(position);
|
|
||||||
if (itemId != null) {
|
String url = ApiConfig.getBaseUrl() + "/api/front/conversations/" + itemToDelete.getId();
|
||||||
allConversations.removeIf(item -> item != null && item.getId().equals(itemId));
|
Log.d(TAG, "删除会话: " + url);
|
||||||
}
|
|
||||||
|
Request request = new Request.Builder()
|
||||||
if (conversationsAdapter != null) {
|
.url(url)
|
||||||
conversationsAdapter.submitList(new ArrayList<>(conversations));
|
.addHeader("Authori-zation", token)
|
||||||
}
|
.delete()
|
||||||
|
.build();
|
||||||
// 更新总未读数量:减少被删除会话的未读数量
|
|
||||||
if (unreadCountToRemove > 0) {
|
httpClient.newCall(request).enqueue(new Callback() {
|
||||||
UnreadMessageManager.decrementUnreadCount(this, unreadCountToRemove);
|
@Override
|
||||||
// 更新底部导航栏徽章
|
public void onFailure(Call call, IOException e) {
|
||||||
if (binding != null) {
|
Log.e(TAG, "删除会话失败", e);
|
||||||
UnreadMessageManager.updateBadge(binding.bottomNavInclude.bottomNavigation);
|
runOnUiThread(() -> Toast.makeText(MessagesActivity.this, "删除失败", Toast.LENGTH_SHORT).show());
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
@Override
|
||||||
updateEmptyState();
|
public void onResponse(Call call, Response response) throws IOException {
|
||||||
|
String body = response.body() != null ? response.body().string() : "";
|
||||||
|
Log.d(TAG, "删除会话响应: " + body);
|
||||||
|
runOnUiThread(() -> {
|
||||||
|
try {
|
||||||
|
JSONObject json = new JSONObject(body);
|
||||||
|
if (json.optInt("code", -1) == 200) {
|
||||||
|
int unreadCountToRemove = itemToDelete.getUnreadCount();
|
||||||
|
String itemId = itemToDelete.getId();
|
||||||
|
|
||||||
|
conversations.remove(position);
|
||||||
|
allConversations.removeIf(item -> item != null && item.getId().equals(itemId));
|
||||||
|
|
||||||
|
if (conversationsAdapter != null) {
|
||||||
|
conversationsAdapter.submitList(new ArrayList<>(conversations));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (unreadCountToRemove > 0) {
|
||||||
|
UnreadMessageManager.decrementUnreadCount(MessagesActivity.this, unreadCountToRemove);
|
||||||
|
if (binding != null) {
|
||||||
|
UnreadMessageManager.updateBadge(binding.bottomNavInclude.bottomNavigation);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateEmptyState();
|
||||||
|
Toast.makeText(MessagesActivity.this, "会话已删除", Toast.LENGTH_SHORT).show();
|
||||||
|
} else {
|
||||||
|
Toast.makeText(MessagesActivity.this, json.optString("message", "删除失败"), Toast.LENGTH_SHORT).show();
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "解析删除响应失败", e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private float dp(float value) {
|
private float dp(float value) {
|
||||||
|
|
|
||||||
|
|
@ -336,8 +336,57 @@ public class MyFriendsActivity extends AppCompatActivity {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void openConversation(FriendItem friend) {
|
private void openConversation(FriendItem friend) {
|
||||||
// 打开与好友的私聊会话
|
// 先获取或创建与好友的会话,再打开私聊页面
|
||||||
ConversationActivity.start(this, friend.getId(), friend.getName());
|
String token = AuthStore.getToken(this);
|
||||||
|
if (token == null) {
|
||||||
|
Toast.makeText(this, "请先登录", Toast.LENGTH_SHORT).show();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String url = ApiConfig.getBaseUrl() + "/api/front/conversations/with/" + friend.getId();
|
||||||
|
Log.d(TAG, "获取或创建会话: " + url);
|
||||||
|
|
||||||
|
Request request = new Request.Builder()
|
||||||
|
.url(url)
|
||||||
|
.addHeader("Authori-zation", token)
|
||||||
|
.post(RequestBody.create("", MediaType.parse("application/json")))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
httpClient.newCall(request).enqueue(new Callback() {
|
||||||
|
@Override
|
||||||
|
public void onFailure(Call call, IOException e) {
|
||||||
|
Log.e(TAG, "获取会话失败", e);
|
||||||
|
runOnUiThread(() -> {
|
||||||
|
Toast.makeText(MyFriendsActivity.this, "打开会话失败", Toast.LENGTH_SHORT).show();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onResponse(Call call, Response response) throws IOException {
|
||||||
|
String body = response.body() != null ? response.body().string() : "";
|
||||||
|
Log.d(TAG, "获取会话响应: " + body);
|
||||||
|
runOnUiThread(() -> {
|
||||||
|
try {
|
||||||
|
JSONObject json = new JSONObject(body);
|
||||||
|
if (json.optInt("code", -1) == 200) {
|
||||||
|
JSONObject data = json.optJSONObject("data");
|
||||||
|
String conversationId = data != null ? data.optString("conversationId", "") : "";
|
||||||
|
if (!conversationId.isEmpty()) {
|
||||||
|
ConversationActivity.start(MyFriendsActivity.this, conversationId, friend.getName());
|
||||||
|
} else {
|
||||||
|
Toast.makeText(MyFriendsActivity.this, "获取会话ID失败", Toast.LENGTH_SHORT).show();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
String msg = json.optString("message", "打开会话失败");
|
||||||
|
Toast.makeText(MyFriendsActivity.this, msg, Toast.LENGTH_SHORT).show();
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "解析会话响应失败", e);
|
||||||
|
Toast.makeText(MyFriendsActivity.this, "打开会话失败", Toast.LENGTH_SHORT).show();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private void applyFilter(String query) {
|
private void applyFilter(String query) {
|
||||||
|
|
|
||||||
|
|
@ -85,6 +85,15 @@ public class RoomDetailActivity extends AppCompatActivity {
|
||||||
private WebSocket webSocket;
|
private WebSocket webSocket;
|
||||||
private OkHttpClient wsClient;
|
private OkHttpClient wsClient;
|
||||||
private static final String WS_BASE_URL = "ws://192.168.1.164:8081/ws/live/chat/";
|
private static final String WS_BASE_URL = "ws://192.168.1.164:8081/ws/live/chat/";
|
||||||
|
|
||||||
|
// WebSocket 心跳检测
|
||||||
|
private Runnable heartbeatRunnable;
|
||||||
|
private Runnable reconnectRunnable;
|
||||||
|
private static final long HEARTBEAT_INTERVAL = 30000; // 30秒心跳间隔
|
||||||
|
private static final long RECONNECT_DELAY = 5000; // 5秒重连延迟
|
||||||
|
private int reconnectAttempts = 0;
|
||||||
|
private static final int MAX_RECONNECT_ATTEMPTS = 5;
|
||||||
|
private boolean isWebSocketConnected = false;
|
||||||
|
|
||||||
// 礼物相关
|
// 礼物相关
|
||||||
private BottomSheetDialog giftDialog;
|
private BottomSheetDialog giftDialog;
|
||||||
|
|
@ -231,7 +240,12 @@ public class RoomDetailActivity extends AppCompatActivity {
|
||||||
private void connectWebSocket() {
|
private void connectWebSocket() {
|
||||||
if (TextUtils.isEmpty(roomId)) return;
|
if (TextUtils.isEmpty(roomId)) return;
|
||||||
|
|
||||||
wsClient = new OkHttpClient();
|
// 停止之前的重连任务
|
||||||
|
stopReconnect();
|
||||||
|
|
||||||
|
wsClient = new OkHttpClient.Builder()
|
||||||
|
.pingInterval(30, java.util.concurrent.TimeUnit.SECONDS) // OkHttp 内置 ping
|
||||||
|
.build();
|
||||||
Request request = new Request.Builder()
|
Request request = new Request.Builder()
|
||||||
.url(WS_BASE_URL + roomId)
|
.url(WS_BASE_URL + roomId)
|
||||||
.build();
|
.build();
|
||||||
|
|
@ -240,6 +254,10 @@ public class RoomDetailActivity extends AppCompatActivity {
|
||||||
@Override
|
@Override
|
||||||
public void onOpen(WebSocket webSocket, okhttp3.Response response) {
|
public void onOpen(WebSocket webSocket, okhttp3.Response response) {
|
||||||
android.util.Log.d("WebSocket", "连接成功: roomId=" + roomId);
|
android.util.Log.d("WebSocket", "连接成功: roomId=" + roomId);
|
||||||
|
isWebSocketConnected = true;
|
||||||
|
reconnectAttempts = 0;
|
||||||
|
// 启动心跳检测
|
||||||
|
startHeartbeat();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -261,6 +279,9 @@ public class RoomDetailActivity extends AppCompatActivity {
|
||||||
handler.post(() -> {
|
handler.post(() -> {
|
||||||
addChatMessage(new ChatMessage(content, true));
|
addChatMessage(new ChatMessage(content, true));
|
||||||
});
|
});
|
||||||
|
} else if ("pong".equals(type)) {
|
||||||
|
// 收到心跳响应
|
||||||
|
android.util.Log.d("WebSocket", "收到心跳响应");
|
||||||
}
|
}
|
||||||
} catch (JSONException e) {
|
} catch (JSONException e) {
|
||||||
android.util.Log.e("WebSocket", "解析消息失败: " + e.getMessage());
|
android.util.Log.e("WebSocket", "解析消息失败: " + e.getMessage());
|
||||||
|
|
@ -270,15 +291,94 @@ public class RoomDetailActivity extends AppCompatActivity {
|
||||||
@Override
|
@Override
|
||||||
public void onFailure(WebSocket webSocket, Throwable t, okhttp3.Response response) {
|
public void onFailure(WebSocket webSocket, Throwable t, okhttp3.Response response) {
|
||||||
android.util.Log.e("WebSocket", "连接失败: " + t.getMessage());
|
android.util.Log.e("WebSocket", "连接失败: " + t.getMessage());
|
||||||
|
isWebSocketConnected = false;
|
||||||
|
stopHeartbeat();
|
||||||
|
// 尝试重连
|
||||||
|
scheduleReconnect();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onClosed(WebSocket webSocket, int code, String reason) {
|
public void onClosed(WebSocket webSocket, int code, String reason) {
|
||||||
android.util.Log.d("WebSocket", "连接关闭: " + reason);
|
android.util.Log.d("WebSocket", "连接关闭: " + reason);
|
||||||
|
isWebSocketConnected = false;
|
||||||
|
stopHeartbeat();
|
||||||
|
// 非正常关闭时尝试重连
|
||||||
|
if (code != 1000) {
|
||||||
|
scheduleReconnect();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 启动心跳检测
|
||||||
|
*/
|
||||||
|
private void startHeartbeat() {
|
||||||
|
stopHeartbeat();
|
||||||
|
heartbeatRunnable = new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
if (webSocket != null && isWebSocketConnected) {
|
||||||
|
try {
|
||||||
|
JSONObject ping = new JSONObject();
|
||||||
|
ping.put("type", "ping");
|
||||||
|
webSocket.send(ping.toString());
|
||||||
|
android.util.Log.d("WebSocket", "发送心跳");
|
||||||
|
} catch (JSONException e) {
|
||||||
|
android.util.Log.e("WebSocket", "发送心跳失败: " + e.getMessage());
|
||||||
|
}
|
||||||
|
handler.postDelayed(heartbeatRunnable, HEARTBEAT_INTERVAL);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
handler.postDelayed(heartbeatRunnable, HEARTBEAT_INTERVAL);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 停止心跳检测
|
||||||
|
*/
|
||||||
|
private void stopHeartbeat() {
|
||||||
|
if (heartbeatRunnable != null) {
|
||||||
|
handler.removeCallbacks(heartbeatRunnable);
|
||||||
|
heartbeatRunnable = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 安排重连
|
||||||
|
*/
|
||||||
|
private void scheduleReconnect() {
|
||||||
|
if (isFinishing() || isDestroyed()) return;
|
||||||
|
if (reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) {
|
||||||
|
android.util.Log.w("WebSocket", "达到最大重连次数,停止重连");
|
||||||
|
handler.post(() -> {
|
||||||
|
addChatMessage(new ChatMessage("弹幕连接断开,请刷新页面重试", true));
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
reconnectAttempts++;
|
||||||
|
long delay = RECONNECT_DELAY * reconnectAttempts; // 递增延迟
|
||||||
|
android.util.Log.d("WebSocket", "将在 " + delay + "ms 后尝试第 " + reconnectAttempts + " 次重连");
|
||||||
|
|
||||||
|
reconnectRunnable = () -> {
|
||||||
|
if (!isFinishing() && !isDestroyed()) {
|
||||||
|
connectWebSocket();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
handler.postDelayed(reconnectRunnable, delay);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 停止重连
|
||||||
|
*/
|
||||||
|
private void stopReconnect() {
|
||||||
|
if (reconnectRunnable != null) {
|
||||||
|
handler.removeCallbacks(reconnectRunnable);
|
||||||
|
reconnectRunnable = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void sendChatViaWebSocket(String content) {
|
private void sendChatViaWebSocket(String content) {
|
||||||
if (webSocket == null) {
|
if (webSocket == null) {
|
||||||
// 如果 WebSocket 未连接,先本地显示
|
// 如果 WebSocket 未连接,先本地显示
|
||||||
|
|
@ -302,6 +402,10 @@ public class RoomDetailActivity extends AppCompatActivity {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void disconnectWebSocket() {
|
private void disconnectWebSocket() {
|
||||||
|
stopHeartbeat();
|
||||||
|
stopReconnect();
|
||||||
|
isWebSocketConnected = false;
|
||||||
|
|
||||||
if (webSocket != null) {
|
if (webSocket != null) {
|
||||||
webSocket.close(1000, "Activity destroyed");
|
webSocket.close(1000, "Activity destroyed");
|
||||||
webSocket = null;
|
webSocket = null;
|
||||||
|
|
@ -784,8 +888,18 @@ public class RoomDetailActivity extends AppCompatActivity {
|
||||||
if (chatSimulationRunnable != null) {
|
if (chatSimulationRunnable != null) {
|
||||||
handler.removeCallbacks(chatSimulationRunnable);
|
handler.removeCallbacks(chatSimulationRunnable);
|
||||||
}
|
}
|
||||||
|
// 清理心跳和重连回调
|
||||||
|
if (heartbeatRunnable != null) {
|
||||||
|
handler.removeCallbacks(heartbeatRunnable);
|
||||||
|
}
|
||||||
|
if (reconnectRunnable != null) {
|
||||||
|
handler.removeCallbacks(reconnectRunnable);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 断开 WebSocket 连接
|
||||||
|
disconnectWebSocket();
|
||||||
|
|
||||||
// 释放播放器资源
|
// 释放播放器资源
|
||||||
releasePlayer();
|
releasePlayer();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,7 @@ public final class AuthStore {
|
||||||
|
|
||||||
public static void setUserInfo(Context context, @Nullable String userId, @Nullable String nickname) {
|
public static void setUserInfo(Context context, @Nullable String userId, @Nullable String nickname) {
|
||||||
if (context == null) return;
|
if (context == null) return;
|
||||||
|
Log.d(TAG, "setUserInfo: userId=" + userId + ", nickname=" + nickname);
|
||||||
context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
|
context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
|
||||||
.edit()
|
.edit()
|
||||||
.putString(KEY_USER_ID, userId)
|
.putString(KEY_USER_ID, userId)
|
||||||
|
|
@ -61,8 +62,14 @@ public final class AuthStore {
|
||||||
@Nullable
|
@Nullable
|
||||||
public static String getUserId(Context context) {
|
public static String getUserId(Context context) {
|
||||||
if (context == null) return null;
|
if (context == null) return null;
|
||||||
return context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
|
String userId = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
|
||||||
.getString(KEY_USER_ID, "");
|
.getString(KEY_USER_ID, null);
|
||||||
|
// 确保空字符串也返回 null
|
||||||
|
if (userId != null && userId.trim().isEmpty()) {
|
||||||
|
userId = null;
|
||||||
|
}
|
||||||
|
Log.d(TAG, "getUserId: " + userId);
|
||||||
|
return userId;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user