Commit f54168be799ecfd70783bfa8fe6d2866b939b968

Authored by yibo
1 parent 2b1f5fb3

feat(crm): 人物列表显示真实最近联系时间

- ContactRow 右侧显示最近联系时间(今天/昨天/N天前/N月前)
- 超过 30 天显示红色「⚠ N月前」警告
- ★ 最近联系人排序改用真实联系时间(微信消息 MAX + 被提到 MAX),
  替代原来只记打开档案时间的 lastAccess

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
lifelog-recorder/app/src/main/kotlin/com/lemon/lifelog/PhotologScreen.kt
... ... @@ -671,6 +671,20 @@ private fun TagFaceDialog(
671 671 private fun ContactList(vm: PhotologViewModel, persons: List<Person>, onOpen: (Long) -> Unit, onOpenPicker: () -> Unit, onOpenCommunities: () -> Unit, onOpenFollowups: () -> Unit, onOpenTopics: () -> Unit) {
672 672 var query by remember { mutableStateOf("") }
673 673  
  674 + // 真实最近联系时间:微信最后一条消息 + 最后一次被提到,取 MAX
  675 + val wxTimes by vm.repo.wechatMessages.observeLastContactTimes().collectAsState(emptyList())
  676 + val mentionTimes by vm.repo.mentions.observeLastMentionTimes().collectAsState(emptyList())
  677 + val lastContactMap = remember(persons, wxTimes, mentionTimes) {
  678 + val wxMap = wxTimes.associate { it.talker to it.maxTime }
  679 + val mnMap = mentionTimes.associate { it.personId to it.maxTime }
  680 + persons.associate { p ->
  681 + val wxid = p.contactRef?.takeIf { it.startsWith("wechat:") }?.removePrefix("wechat:")
  682 + val msgTs = if (wxid != null) wxMap[wxid] ?: 0L else 0L
  683 + val mnTs = mnMap[p.id] ?: 0L
  684 + p.id to maxOf(msgTs, mnTs)
  685 + }
  686 + }
  687 +
674 688 val sections = remember(persons, query) {
675 689 val q = query.trim()
676 690 val filtered = if (q.isBlank()) persons else persons.filter { p ->
... ... @@ -682,9 +696,12 @@ private fun ContactList(vm: PhotologViewModel, persons: List&lt;Person&gt;, onOpen: (L
682 696 .groupBy { sectionLetter(it) }
683 697 .toSortedMap(compareBy { if (it == "#") "[" else it }) // # 排最后
684 698 }
685   - val recent = remember(persons, query) {
  699 + val recent = remember(persons, query, lastContactMap) {
686 700 if (query.isNotBlank()) emptyList()
687   - else persons.filter { it.lastAccess > 0 }.sortedByDescending { it.lastAccess }.take(12)
  701 + else persons
  702 + .filter { (lastContactMap[it.id] ?: 0L) > 0L }
  703 + .sortedByDescending { lastContactMap[it.id] ?: 0L }
  704 + .take(12)
688 705 }
689 706  
690 707 val ctx = LocalContext.current
... ... @@ -730,7 +747,7 @@ private fun ContactList(vm: PhotologViewModel, persons: List&lt;Person&gt;, onOpen: (L
730 747 Text("★ 最近联系人", style = MaterialTheme.typography.labelMedium, modifier = Modifier.padding(horizontal = 16.dp, vertical = 3.dp))
731 748 }
732 749 }
733   - items(recent, key = { "r${it.id}" }) { p -> ContactRow(p) { onOpen(p.id) } }
  750 + items(recent, key = { "r${it.id}" }) { p -> ContactRow(p, lastContactMap[p.id] ?: 0L) { onOpen(p.id) } }
734 751 }
735 752 sections.forEach { (letter, group) ->
736 753 stickyHeader {
... ... @@ -738,7 +755,7 @@ private fun ContactList(vm: PhotologViewModel, persons: List&lt;Person&gt;, onOpen: (L
738 755 Text(letter, style = MaterialTheme.typography.labelMedium, modifier = Modifier.padding(horizontal = 16.dp, vertical = 3.dp))
739 756 }
740 757 }
741   - items(group, key = { it.id }) { p -> ContactRow(p) { onOpen(p.id) } }
  758 + items(group, key = { it.id }) { p -> ContactRow(p, lastContactMap[p.id] ?: 0L) { onOpen(p.id) } }
742 759 }
743 760 }
744 761 }
... ... @@ -751,7 +768,7 @@ private fun sectionLetter(p: Person): String {
751 768 }
752 769  
753 770 @Composable
754   -private fun ContactRow(p: Person, onClick: () -> Unit) {
  771 +private fun ContactRow(p: Person, lastContactTs: Long = 0L, onClick: () -> Unit) {
755 772 Row(
756 773 modifier = Modifier.fillMaxWidth().clickable(onClick = onClick).padding(horizontal = 16.dp, vertical = 7.dp),
757 774 verticalAlignment = Alignment.CenterVertically,
... ... @@ -773,6 +790,23 @@ private fun ContactRow(p: Person, onClick: () -&gt; Unit) {
773 790 )
774 791 if (tags.isNotEmpty()) Text(tags.joinToString(" · "), style = MaterialTheme.typography.labelSmall)
775 792 }
  793 + // 最近联系时间(右侧对齐)
  794 + if (lastContactTs > 0L) {
  795 + val days = ((System.currentTimeMillis() - lastContactTs) / 86_400_000L).toInt()
  796 + val label = when {
  797 + days == 0 -> "今天"
  798 + days == 1 -> "昨天"
  799 + days < 30 -> "${days}天前"
  800 + days < 365 -> "${days / 30}月前"
  801 + else -> "${days / 365}年前"
  802 + }
  803 + Text(
  804 + if (days > 30) "⚠ $label" else label,
  805 + style = MaterialTheme.typography.labelSmall,
  806 + color = if (days > 30) MaterialTheme.colorScheme.error
  807 + else MaterialTheme.colorScheme.onSurfaceVariant,
  808 + )
  809 + }
776 810 }
777 811 }
778 812  
... ...
lifelog-recorder/app/src/main/kotlin/com/lemon/lifelog/data/Daos.kt
... ... @@ -344,6 +344,10 @@ interface MentionDao {
344 344 @Query("SELECT segmentId, personId FROM mentions")
345 345 suspend fun allPairs(): List<MentionPair>
346 346  
  347 + /** 联系人列表用:每个人最后一次被提到的时间戳。 */
  348 + @Query("SELECT personId, MAX(createdAt) AS maxTime FROM mentions GROUP BY personId")
  349 + fun observeLastMentionTimes(): Flow<List<PersonMentionTime>>
  350 +
347 351 /** 某人「被提到」的录音段(join 转写 + 段 + 说话人=谁说的)。 */
348 352 @Query(
349 353 """
... ... @@ -368,6 +372,9 @@ interface MentionDao {
368 372  
369 373 data class MentionPair(val segmentId: Long, val personId: Long)
370 374  
  375 +/** 最近联系时间:每个人最后一次被提到的时间戳。 */
  376 +data class PersonMentionTime(val personId: Long, val maxTime: Long)
  377 +
371 378 /** 段元信息(共现重建:按时间切会话 + 声纹在场)。 */
372 379 data class SegMeta(val id: Long, val startTs: Long, val speakerId: Long?)
373 380  
... ... @@ -881,6 +888,9 @@ interface MomentDao {
881 888 fun observeRecent(n: Int): Flow<List<Moment>>
882 889 }
883 890  
  891 +/** 最近联系时间:每个 wxid 最后一条消息的时间戳。 */
  892 +data class TalkerTime(val talker: String, val maxTime: Long)
  893 +
884 894 @Dao
885 895 interface WechatMessageDao {
886 896 @Insert
... ... @@ -919,6 +929,10 @@ interface WechatMessageDao {
919 929 /** 时间轴用:最近 n 条私聊(排除群聊)。 */
920 930 @Query("SELECT * FROM wechat_messages WHERE talker NOT LIKE '%@chatroom' ORDER BY createTime DESC LIMIT :n")
921 931 fun observeRecent(n: Int): Flow<List<WechatMessage>>
  932 +
  933 + /** 联系人列表用:每个私聊对象最后一条消息时间(用于最近联系时间排序)。 */
  934 + @Query("SELECT talker, MAX(createTime) AS maxTime FROM wechat_messages WHERE talker NOT LIKE '%@chatroom' GROUP BY talker")
  935 + fun observeLastContactTimes(): Flow<List<TalkerTime>>
922 936 }
923 937  
924 938 @Dao
... ...