Skip to main content

privchat_protocol/rpc/
sync.rs

1// Copyright 2025 Shanghai Boyu Information Technology Co., Ltd.
2// https://privchat.dev
3//
4// Author: zoujiaqing <zoujiaqing@gmail.com>
5//
6// Licensed under the Apache License, Version 2.0 (the "License");
7// you may not use this file except in compliance with the License.
8// You may obtain a copy of the License at
9//
10//     http://www.apache.org/licenses/LICENSE-2.0
11//
12// Unless required by applicable law or agreed to in writing, software
13// distributed under the License is distributed on an "AS IS" BASIS,
14// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15// See the License for the specific language governing permissions and
16// limitations under the License.
17
18/// pts-Based 同步协议
19///
20/// 设计原则:
21/// - pts(per-channel monotonic)为权威顺序
22/// - local_message_id 用于幂等和关联
23/// - 服务器是唯一仲裁方
24use serde::{Deserialize, Serialize};
25
26// ============================================================
27// SessionReady - 客户端声明已完成 bootstrap
28// ============================================================
29
30/// 会话 ready 请求
31///
32/// RPC 路由: `sync/session_ready`
33/// 语义:客户端 bootstrap sync 已完成,服务端可开始补差+实时推送
34#[derive(Debug, Clone, Serialize, Deserialize, Default)]
35pub struct SessionReadyRequest {}
36
37/// 会话 ready 响应
38///
39/// RPC 路由: `sync/session_ready`
40pub type SessionReadyResponse = bool;
41
42// ============================================================
43// ClientSubmit - 客户端提交命令
44// ============================================================
45
46/// 客户端提交请求
47///
48/// RPC路由: `sync/submit`
49#[derive(Debug, Clone, Serialize, Deserialize)]
50pub struct ClientSubmitRequest {
51    /// 客户端消息号(Snowflake u64,用于幂等)
52    pub local_message_id: u64,
53
54    /// 频道 ID
55    pub channel_id: u64,
56
57    /// 频道类型(1=私聊,2=群聊)
58    pub channel_type: u8,
59
60    /// 客户端已知的最后 pts(用于间隙检测)
61    pub last_pts: u64,
62
63    /// 命令类型
64    pub command_type: String,
65
66    /// 命令负载(JSON)
67    pub payload: serde_json::Value,
68
69    /// 客户端时间戳(毫秒)
70    pub client_timestamp: i64,
71
72    /// 设备 ID(可选,用于多设备去重)
73    #[serde(skip_serializing_if = "Option::is_none")]
74    pub device_id: Option<String>,
75}
76
77/// 服务器提交响应
78///
79/// RPC路由: `sync/submit`
80/// 注意:如果 decision 是 Rejected,SDK 层会返回错误,不会反序列化这个结构
81#[derive(Debug, Clone, Serialize, Deserialize)]
82pub struct ClientSubmitResponse {
83    /// 服务器决策
84    pub decision: ServerDecision,
85
86    /// 分配的 pts(如果 accepted/transformed)
87    #[serde(skip_serializing_if = "Option::is_none")]
88    pub pts: Option<u64>,
89
90    /// 服务器消息 ID(如果 accepted/transformed)
91    #[serde(skip_serializing_if = "Option::is_none")]
92    pub server_msg_id: Option<u64>,
93
94    /// 服务器时间戳
95    pub server_timestamp: i64,
96
97    /// 关联的 local_message_id
98    pub local_message_id: u64,
99
100    /// 是否需要补齐(has_gap)
101    pub has_gap: bool,
102
103    /// 服务器当前最新 pts
104    pub current_pts: u64,
105}
106
107/// 服务器决策类型
108#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
109#[serde(rename_all = "snake_case")]
110pub enum ServerDecision {
111    /// 接受(原样接受)
112    Accepted,
113    /// 转换(服务器修改了某些字段)
114    Transformed {
115        /// 转换说明
116        reason: String,
117    },
118    /// 拒绝(不符合规则)
119    Rejected {
120        /// 拒绝原因
121        reason: String,
122    },
123}
124
125// ============================================================
126// GetDifference - 获取差异(补齐间隙)
127// ============================================================
128
129/// 获取差异请求
130///
131/// RPC路由: `sync/get_difference`
132#[derive(Debug, Clone, Serialize, Deserialize)]
133pub struct GetDifferenceRequest {
134    /// 频道 ID
135    pub channel_id: u64,
136
137    /// 频道类型
138    pub channel_type: u8,
139
140    /// 客户端已知的最后 pts
141    pub last_pts: u64,
142
143    /// 限制数量(可选,默认 100)
144    #[serde(skip_serializing_if = "Option::is_none")]
145    pub limit: Option<u32>,
146}
147
148/// 获取差异响应
149///
150/// RPC路由: `sync/get_difference`
151#[derive(Debug, Clone, Serialize, Deserialize)]
152pub struct GetDifferenceResponse {
153    /// Commits 列表(pts 递增)
154    pub commits: Vec<ServerCommit>,
155
156    /// 服务器当前最新 pts
157    pub current_pts: u64,
158
159    /// 是否还有更多(需要继续拉取)
160    pub has_more: bool,
161}
162
163/// 服务器 Commit(权威事实)
164#[derive(Debug, Clone, Serialize, Deserialize)]
165pub struct ServerCommit {
166    /// pts(per-channel 单调递增)
167    pub pts: u64,
168
169    /// 服务器消息 ID
170    pub server_msg_id: u64,
171
172    /// 关联的 local_message_id(如果来自客户端)
173    #[serde(skip_serializing_if = "Option::is_none")]
174    pub local_message_id: Option<u64>,
175
176    /// 频道 ID
177    pub channel_id: u64,
178
179    /// 频道类型
180    pub channel_type: u8,
181
182    /// 消息类型
183    pub message_type: String,
184
185    /// 消息内容(JSON)
186    pub content: serde_json::Value,
187
188    /// 服务器时间戳(毫秒)
189    pub server_timestamp: i64,
190
191    /// 发送者 ID
192    pub sender_id: u64,
193
194    /// 发送者信息(可选)
195    #[serde(skip_serializing_if = "Option::is_none")]
196    pub sender_info: Option<SenderInfo>,
197}
198
199/// 发送者信息(简化的用户信息)
200#[derive(Debug, Clone, Serialize, Deserialize)]
201pub struct SenderInfo {
202    pub user_id: u64,
203    pub username: String,
204    #[serde(skip_serializing_if = "Option::is_none")]
205    pub nickname: Option<String>,
206    #[serde(skip_serializing_if = "Option::is_none")]
207    pub avatar_url: Option<String>,
208}
209
210// ============================================================
211// GetChannelPts - 获取频道当前 pts(用于初始化)
212// ============================================================
213
214/// 获取频道 pts 请求
215///
216/// RPC路由: `sync/get_channel_pts`
217#[derive(Debug, Clone, Serialize, Deserialize)]
218pub struct GetChannelPtsRequest {
219    /// 频道 ID
220    pub channel_id: u64,
221
222    /// 频道类型
223    pub channel_type: u8,
224}
225
226/// 获取频道 pts 响应
227///
228/// RPC路由: `sync/get_channel_pts`
229#[derive(Debug, Clone, Serialize, Deserialize)]
230pub struct GetChannelPtsResponse {
231    /// 当前 pts
232    pub current_pts: u64,
233}
234
235// ============================================================
236// BatchGetChannelPts - 批量获取多个频道的 pts
237// ============================================================
238
239/// 批量获取频道 pts 请求
240///
241/// RPC路由: `sync/batch_get_channel_pts`
242#[derive(Debug, Clone, Serialize, Deserialize)]
243pub struct BatchGetChannelPtsRequest {
244    /// 频道列表
245    pub channels: Vec<ChannelIdentifier>,
246}
247
248/// 频道标识符
249#[derive(Debug, Clone, Serialize, Deserialize)]
250pub struct ChannelIdentifier {
251    pub channel_id: u64,
252    pub channel_type: u8,
253}
254
255/// 批量获取频道 pts 响应
256///
257/// RPC路由: `sync/batch_get_channel_pts`
258#[derive(Debug, Clone, Serialize, Deserialize)]
259pub struct BatchGetChannelPtsResponse {
260    /// 频道 pts 映射
261    pub channel_pts_map: Vec<ChannelPtsInfo>,
262}
263
264/// 频道 pts 信息
265#[derive(Debug, Clone, Serialize, Deserialize)]
266pub struct ChannelPtsInfo {
267    pub channel_id: u64,
268    pub channel_type: u8,
269    pub current_pts: u64,
270}
271
272// ============================================================
273// Entity State Sync(实体状态同步,与 PTS 消息流正交)
274// ============================================================
275// 设计见 privchat-docs/design/ENTITY_SYNC_V1.md
276
277/// 实体同步请求
278///
279/// RPC 路由: `entity/sync_entities`(待服务端实现)
280#[derive(Debug, Clone, Serialize, Deserialize)]
281pub struct SyncEntitiesRequest {
282    /// 实体类型:friend, group, channel, group_member, user, user_settings, user_block 等(受控枚举)
283    pub entity_type: String,
284    /// 客户端上次同步到的版本号,0 或空表示全量
285    #[serde(skip_serializing_if = "Option::is_none")]
286    pub since_version: Option<u64>,
287    /// 可选:同步范围,如 group_member 需带 group_id,user 按需拉取时带 user_id
288    #[serde(skip_serializing_if = "Option::is_none")]
289    pub scope: Option<String>,
290    /// 可选:每页数量
291    #[serde(skip_serializing_if = "Option::is_none")]
292    pub limit: Option<u32>,
293}
294
295/// 单条实体同步项
296#[derive(Debug, Clone, Serialize, Deserialize)]
297pub struct SyncEntityItem {
298    /// 实体 ID(如 user_id, group_id, channel_id)
299    pub entity_id: String,
300    /// 该实体最新版本号
301    pub version: u64,
302    /// true 表示服务端已删除(Tombstone)
303    #[serde(default)]
304    pub deleted: bool,
305    /// 实体数据,具体字段随 entity_type 变化
306    #[serde(skip_serializing_if = "Option::is_none")]
307    pub payload: Option<serde_json::Value>,
308}
309
310#[derive(Debug, Clone, Default, Serialize, Deserialize)]
311pub struct FriendSyncFriendPayload {
312    #[serde(skip_serializing_if = "Option::is_none")]
313    pub created_at: Option<i64>,
314    #[serde(skip_serializing_if = "Option::is_none")]
315    pub updated_at: Option<i64>,
316    #[serde(skip_serializing_if = "Option::is_none")]
317    pub version: Option<i64>,
318}
319
320#[derive(Debug, Clone, Default, Serialize, Deserialize)]
321pub struct FriendSyncUserPayload {
322    #[serde(skip_serializing_if = "Option::is_none")]
323    pub username: Option<String>,
324    #[serde(skip_serializing_if = "Option::is_none")]
325    pub nickname: Option<String>,
326    #[serde(skip_serializing_if = "Option::is_none")]
327    pub name: Option<String>,
328    #[serde(skip_serializing_if = "Option::is_none")]
329    pub alias: Option<String>,
330    #[serde(skip_serializing_if = "Option::is_none")]
331    pub avatar: Option<String>,
332    #[serde(skip_serializing_if = "Option::is_none")]
333    pub user_type: Option<i32>,
334    #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
335    pub type_field: Option<i32>,
336    #[serde(skip_serializing_if = "Option::is_none")]
337    pub updated_at: Option<i64>,
338    #[serde(skip_serializing_if = "Option::is_none")]
339    pub version: Option<i64>,
340}
341
342#[derive(Debug, Clone, Default, Serialize, Deserialize)]
343pub struct FriendSyncPayload {
344    #[serde(skip_serializing_if = "Option::is_none")]
345    pub user_id: Option<u64>,
346    #[serde(skip_serializing_if = "Option::is_none")]
347    pub uid: Option<u64>,
348    #[serde(skip_serializing_if = "Option::is_none")]
349    pub tags: Option<String>,
350    #[serde(skip_serializing_if = "Option::is_none")]
351    pub is_pinned: Option<bool>,
352    #[serde(skip_serializing_if = "Option::is_none")]
353    pub pinned: Option<bool>,
354    #[serde(skip_serializing_if = "Option::is_none")]
355    pub created_at: Option<i64>,
356    #[serde(skip_serializing_if = "Option::is_none")]
357    pub updated_at: Option<i64>,
358    #[serde(skip_serializing_if = "Option::is_none")]
359    pub version: Option<i64>,
360    #[serde(skip_serializing_if = "Option::is_none")]
361    pub friend: Option<FriendSyncFriendPayload>,
362    #[serde(skip_serializing_if = "Option::is_none")]
363    pub user: Option<FriendSyncUserPayload>,
364}
365
366#[derive(Debug, Clone, Default, Serialize, Deserialize)]
367pub struct GroupSyncPayload {
368    #[serde(skip_serializing_if = "Option::is_none")]
369    pub group_id: Option<u64>,
370    #[serde(skip_serializing_if = "Option::is_none")]
371    pub name: Option<String>,
372    #[serde(skip_serializing_if = "Option::is_none")]
373    pub description: Option<String>,
374    #[serde(skip_serializing_if = "Option::is_none")]
375    pub avatar: Option<String>,
376    #[serde(skip_serializing_if = "Option::is_none")]
377    pub avatar_url: Option<String>,
378    #[serde(skip_serializing_if = "Option::is_none")]
379    pub owner_id: Option<u64>,
380    #[serde(skip_serializing_if = "Option::is_none")]
381    pub member_count: Option<u32>,
382    #[serde(skip_serializing_if = "Option::is_none")]
383    pub created_at: Option<i64>,
384    #[serde(skip_serializing_if = "Option::is_none")]
385    pub updated_at: Option<i64>,
386}
387
388#[derive(Debug, Clone, Default, Serialize, Deserialize)]
389pub struct ChannelSyncPayload {
390    #[serde(skip_serializing_if = "Option::is_none")]
391    pub channel_id: Option<u64>,
392    #[serde(skip_serializing_if = "Option::is_none")]
393    pub channel_type: Option<i64>,
394    #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
395    pub type_field: Option<i64>,
396    #[serde(skip_serializing_if = "Option::is_none")]
397    pub channel_name: Option<String>,
398    #[serde(skip_serializing_if = "Option::is_none")]
399    pub name: Option<String>,
400    #[serde(skip_serializing_if = "Option::is_none")]
401    pub avatar: Option<String>,
402    #[serde(skip_serializing_if = "Option::is_none")]
403    pub unread_count: Option<i32>,
404    #[serde(skip_serializing_if = "Option::is_none")]
405    pub last_msg_content: Option<String>,
406    #[serde(skip_serializing_if = "Option::is_none")]
407    pub last_msg_timestamp: Option<i64>,
408    #[serde(skip_serializing_if = "Option::is_none")]
409    pub top: Option<i32>,
410    #[serde(skip_serializing_if = "Option::is_none")]
411    pub mute: Option<i32>,
412}
413
414#[derive(Debug, Clone, Default, Serialize, Deserialize)]
415pub struct ChannelExtraSyncPayload {
416    #[serde(skip_serializing_if = "Option::is_none")]
417    pub channel_id: Option<u64>,
418    #[serde(skip_serializing_if = "Option::is_none")]
419    pub channel_type: Option<i64>,
420    #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
421    pub type_field: Option<i64>,
422    #[serde(skip_serializing_if = "Option::is_none")]
423    pub browse_to: Option<u64>,
424    #[serde(skip_serializing_if = "Option::is_none")]
425    pub keep_pts: Option<u64>,
426    #[serde(skip_serializing_if = "Option::is_none")]
427    pub keep_offset_y: Option<i32>,
428    #[serde(skip_serializing_if = "Option::is_none")]
429    pub draft: Option<String>,
430    #[serde(skip_serializing_if = "Option::is_none")]
431    pub draft_updated_at: Option<u64>,
432}
433
434#[derive(Debug, Clone, Default, Serialize, Deserialize)]
435pub struct ChannelUnreadSyncPayload {
436    #[serde(skip_serializing_if = "Option::is_none")]
437    pub channel_id: Option<u64>,
438    #[serde(skip_serializing_if = "Option::is_none")]
439    pub channel_type: Option<i64>,
440    #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
441    pub type_field: Option<i64>,
442    #[serde(skip_serializing_if = "Option::is_none")]
443    pub unread_count: Option<i32>,
444}
445
446#[derive(Debug, Clone, Default, Serialize, Deserialize)]
447pub struct UserSettingsSyncPayload {
448    #[serde(skip_serializing_if = "Option::is_none")]
449    pub setting_key: Option<String>,
450    #[serde(skip_serializing_if = "Option::is_none")]
451    pub value: Option<serde_json::Value>,
452}
453
454#[derive(Debug, Clone, Default, Serialize, Deserialize)]
455pub struct GroupMemberSyncPayload {
456    #[serde(skip_serializing_if = "Option::is_none")]
457    pub group_id: Option<u64>,
458    #[serde(skip_serializing_if = "Option::is_none")]
459    pub user_id: Option<u64>,
460    #[serde(skip_serializing_if = "Option::is_none")]
461    pub uid: Option<u64>,
462    #[serde(skip_serializing_if = "Option::is_none")]
463    pub role: Option<i32>,
464    #[serde(skip_serializing_if = "Option::is_none")]
465    pub status: Option<i32>,
466    #[serde(skip_serializing_if = "Option::is_none")]
467    pub alias: Option<String>,
468    #[serde(skip_serializing_if = "Option::is_none")]
469    pub is_muted: Option<bool>,
470    #[serde(skip_serializing_if = "Option::is_none")]
471    pub joined_at: Option<i64>,
472    #[serde(skip_serializing_if = "Option::is_none")]
473    pub updated_at: Option<i64>,
474    #[serde(skip_serializing_if = "Option::is_none")]
475    pub version: Option<i64>,
476}
477
478#[derive(Debug, Clone, Default, Serialize, Deserialize)]
479pub struct ChannelMemberSyncPayload {
480    #[serde(skip_serializing_if = "Option::is_none")]
481    pub channel_id: Option<u64>,
482    #[serde(skip_serializing_if = "Option::is_none")]
483    pub channel_type: Option<i32>,
484    #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
485    pub type_field: Option<i32>,
486    #[serde(skip_serializing_if = "Option::is_none")]
487    pub member_uid: Option<u64>,
488    #[serde(skip_serializing_if = "Option::is_none")]
489    pub user_id: Option<u64>,
490    #[serde(skip_serializing_if = "Option::is_none")]
491    pub uid: Option<u64>,
492    #[serde(skip_serializing_if = "Option::is_none")]
493    pub member_name: Option<String>,
494    #[serde(skip_serializing_if = "Option::is_none")]
495    pub name: Option<String>,
496    #[serde(skip_serializing_if = "Option::is_none")]
497    pub member_remark: Option<String>,
498    #[serde(skip_serializing_if = "Option::is_none")]
499    pub remark: Option<String>,
500    #[serde(skip_serializing_if = "Option::is_none")]
501    pub member_avatar: Option<String>,
502    #[serde(skip_serializing_if = "Option::is_none")]
503    pub avatar: Option<String>,
504    #[serde(skip_serializing_if = "Option::is_none")]
505    pub member_invite_uid: Option<u64>,
506    #[serde(skip_serializing_if = "Option::is_none")]
507    pub inviter_uid: Option<u64>,
508    #[serde(skip_serializing_if = "Option::is_none")]
509    pub role: Option<i32>,
510    #[serde(skip_serializing_if = "Option::is_none")]
511    pub status: Option<i32>,
512    #[serde(skip_serializing_if = "Option::is_none")]
513    pub is_deleted: Option<bool>,
514    #[serde(skip_serializing_if = "Option::is_none")]
515    pub robot: Option<i32>,
516    #[serde(skip_serializing_if = "Option::is_none")]
517    pub version: Option<i64>,
518    #[serde(skip_serializing_if = "Option::is_none")]
519    pub created_at: Option<i64>,
520    #[serde(skip_serializing_if = "Option::is_none")]
521    pub updated_at: Option<i64>,
522    #[serde(skip_serializing_if = "Option::is_none")]
523    pub extra: Option<String>,
524    #[serde(skip_serializing_if = "Option::is_none")]
525    pub forbidden_expiration_time: Option<i64>,
526    #[serde(skip_serializing_if = "Option::is_none")]
527    pub member_avatar_cache_key: Option<String>,
528}
529
530#[derive(Debug, Clone, Default, Serialize, Deserialize)]
531pub struct MessageSyncPayload {
532    #[serde(skip_serializing_if = "Option::is_none")]
533    pub server_message_id: Option<u64>,
534    #[serde(skip_serializing_if = "Option::is_none")]
535    pub message_id: Option<u64>,
536    #[serde(skip_serializing_if = "Option::is_none")]
537    pub id: Option<u64>,
538    #[serde(skip_serializing_if = "Option::is_none")]
539    pub local_message_id: Option<u64>,
540    #[serde(skip_serializing_if = "Option::is_none")]
541    pub channel_id: Option<u64>,
542    #[serde(skip_serializing_if = "Option::is_none")]
543    pub channel_type: Option<i32>,
544    #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
545    pub type_field: Option<i32>,
546    #[serde(skip_serializing_if = "Option::is_none")]
547    pub conversation_type: Option<i32>,
548    #[serde(skip_serializing_if = "Option::is_none")]
549    pub timestamp: Option<i64>,
550    #[serde(skip_serializing_if = "Option::is_none")]
551    pub created_at: Option<i64>,
552    #[serde(skip_serializing_if = "Option::is_none")]
553    pub send_time: Option<i64>,
554    #[serde(skip_serializing_if = "Option::is_none")]
555    pub from_uid: Option<u64>,
556    #[serde(skip_serializing_if = "Option::is_none")]
557    pub sender_id: Option<u64>,
558    #[serde(skip_serializing_if = "Option::is_none")]
559    pub from: Option<u64>,
560    #[serde(skip_serializing_if = "Option::is_none")]
561    pub uid: Option<u64>,
562    #[serde(skip_serializing_if = "Option::is_none")]
563    pub message_type: Option<i32>,
564    #[serde(skip_serializing_if = "Option::is_none")]
565    pub content: Option<String>,
566    #[serde(skip_serializing_if = "Option::is_none")]
567    pub text: Option<String>,
568    #[serde(skip_serializing_if = "Option::is_none")]
569    pub body: Option<String>,
570    #[serde(skip_serializing_if = "Option::is_none")]
571    pub status: Option<i32>,
572    #[serde(skip_serializing_if = "Option::is_none")]
573    pub pts: Option<i64>,
574    #[serde(skip_serializing_if = "Option::is_none")]
575    pub setting: Option<i32>,
576    #[serde(skip_serializing_if = "Option::is_none")]
577    pub order_seq: Option<i64>,
578    #[serde(skip_serializing_if = "Option::is_none")]
579    pub searchable_word: Option<String>,
580    #[serde(skip_serializing_if = "Option::is_none")]
581    pub extra: Option<String>,
582    #[serde(skip_serializing_if = "Option::is_none")]
583    pub topic: Option<String>,
584    #[serde(skip_serializing_if = "Option::is_none")]
585    pub stream_no: Option<String>,
586    #[serde(skip_serializing_if = "Option::is_none")]
587    pub stream_seq: Option<i64>,
588    #[serde(skip_serializing_if = "Option::is_none")]
589    pub stream_flag: Option<i64>,
590    #[serde(skip_serializing_if = "Option::is_none")]
591    pub msg_key: Option<String>,
592    #[serde(skip_serializing_if = "Option::is_none")]
593    pub expire: Option<i64>,
594}
595
596#[derive(Debug, Clone, Default, Serialize, Deserialize)]
597pub struct MessageStatusSyncPayload {
598    #[serde(skip_serializing_if = "Option::is_none")]
599    pub message_id: Option<u64>,
600    #[serde(skip_serializing_if = "Option::is_none")]
601    pub server_message_id: Option<u64>,
602    #[serde(skip_serializing_if = "Option::is_none")]
603    pub id: Option<u64>,
604    #[serde(skip_serializing_if = "Option::is_none")]
605    pub channel_id: Option<u64>,
606    #[serde(skip_serializing_if = "Option::is_none")]
607    pub channel_type: Option<i32>,
608    #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
609    pub type_field: Option<i32>,
610    #[serde(skip_serializing_if = "Option::is_none")]
611    pub conversation_type: Option<i32>,
612    #[serde(skip_serializing_if = "Option::is_none")]
613    pub status: Option<i32>,
614    #[serde(skip_serializing_if = "Option::is_none")]
615    pub readed: Option<bool>,
616    #[serde(skip_serializing_if = "Option::is_none")]
617    pub is_read: Option<bool>,
618    #[serde(skip_serializing_if = "Option::is_none")]
619    pub read: Option<bool>,
620}
621
622/// 实体同步响应
623#[derive(Debug, Clone, Serialize, Deserialize)]
624pub struct SyncEntitiesResponse {
625    pub items: Vec<SyncEntityItem>,
626    /// 本次同步完成后的最新版本号,客户端下次请求 since_version
627    pub next_version: u64,
628    /// 是否还有更多数据
629    #[serde(default)]
630    pub has_more: bool,
631    /// 可选:客户端过旧时服务端返回最小支持版本,触发全量
632    #[serde(skip_serializing_if = "Option::is_none")]
633    pub min_version: Option<u64>,
634}
635
636// ============================================================
637// 测试
638// ============================================================
639
640#[cfg(test)]
641mod tests {
642    use super::*;
643
644    #[test]
645    fn test_client_submit_request_serialization() {
646        let req = ClientSubmitRequest {
647            local_message_id: 123456789,
648            channel_id: 1001,
649            channel_type: 1,
650            last_pts: 100,
651            command_type: "send_message".to_string(),
652            payload: serde_json::json!({"text": "Hello"}),
653            client_timestamp: 1700000000000,
654            device_id: Some("device_001".to_string()),
655        };
656
657        let json = serde_json::to_string(&req).unwrap();
658        assert!(json.contains("local_message_id"));
659        assert!(json.contains("123456789"));
660    }
661
662    #[test]
663    fn test_server_decision() {
664        let decision_accepted = ServerDecision::Accepted;
665        let decision_transformed = ServerDecision::Transformed {
666            reason: "Content filtered".to_string(),
667        };
668        let decision_rejected = ServerDecision::Rejected {
669            reason: "Spam detected".to_string(),
670        };
671
672        assert_eq!(decision_accepted, ServerDecision::Accepted);
673        assert!(matches!(
674            decision_transformed,
675            ServerDecision::Transformed { .. }
676        ));
677        assert!(matches!(decision_rejected, ServerDecision::Rejected { .. }));
678    }
679}