Sfoglia il codice sorgente

Feature/safety notifications (#56)

* [feat](app) 初步完成秩序管理后台优化需求客户端业务

* [feat](app) 更改性别更换弹窗按钮、显示倒计时按钮

* (feat)[app] 更改秩序需求实现方式、通过Anchor模块监听消息

* feat: 性别变更弹窗修改

* feat: 代码review修改

* (refactor)[app] 实现通用模版消息弹窗

* (merge)[app/safety_notifications] 合并通用模版、移除Debug部分

* (fix)[app/safety_notifications] 处理封禁通知弹窗问题

* (fix)[app/safety_notifications] 处理封禁通知弹窗问题

* [refactor](safety) 修改封禁房间弹窗逻辑.

* [refactor](safety) 修改封禁房间弹窗逻辑.

* [refactor](safety) 修改封禁房间弹窗逻辑.

* [fix](safety) 修复缩小房间时不弹窗问题.

* Revert "[fix](safety) 修复缩小房间时不弹窗问题."

This reverts commit 0dad31fbd3195a3373c08d7dd8ad8bf9f57c6984.

---------

Co-authored-by: XiaodongLin <450468291@qq.com>
evanbiz08 9 mesi fa
parent
commit
23f9f2c968

+ 0 - 1
app/src/main/AndroidManifest.xml

@@ -180,7 +180,6 @@
             android:name="com.adealink.weparty.videoselect.VideoSelectActivity"
             android:screenOrientation="portrait" />
 
-
     </application>
 
 </manifest>

+ 17 - 12
app/src/main/java/com/adealink/weparty/commonui/dialogchain/BaseDialogData.kt

@@ -109,18 +109,6 @@ sealed class BaseDialogData(
         // 低质量头像弹窗数据
     }
 
-    @Parcelize
-    class GreetSettingGuideDialogData(val extraToUid: Long) : Parcelable,
-        BaseDialogData("GreetSettingGuideDialog")
-
-    @Parcelize
-    class DailyMsgLimitReminderDialogData(val curCount: Long, val totalCount: Long) : Parcelable,
-        BaseDialogData("DailyMsgLimitReminderDialog")
-
-    @Parcelize
-    class MsgSendFailedDialogData(val countDownTime: Long, val totalCount: Long) : Parcelable,
-        BaseDialogData("MsgSendFailedDialog")
-
     /**
      * 性别变更弹窗
      */
@@ -133,5 +121,22 @@ sealed class BaseDialogData(
         val positiveText: CharSequence?,
         val negativeText: CharSequence?,
     ) : Parcelable, BaseDialogData("GenderChangeDialog")
+
+    @Parcelize
+    data class SafetyNotificationData(
+        val messageId: Long,
+        val messageType: Int
+    ) : Parcelable, BaseDialogData("SafetyNotificationDialog")
+    @Parcelize
+    class GreetSettingGuideDialogData(val extraToUid: Long) : Parcelable,
+        BaseDialogData("GreetSettingGuideDialog")
+
+    @Parcelize
+    class DailyMsgLimitReminderDialogData(val curCount: Long, val totalCount: Long) : Parcelable,
+        BaseDialogData("DailyMsgLimitReminderDialog")
+
+    @Parcelize
+    class MsgSendFailedDialogData(val countDownTime: Long, val totalCount: Long) : Parcelable,
+        BaseDialogData("MsgSendFailedDialog")
 }
 

+ 48 - 0
app/src/main/java/com/adealink/weparty/commonui/dialogchain/dialogtask/CommonDialogTask.kt

@@ -0,0 +1,48 @@
+package com.adealink.weparty.commonui.dialogchain.dialogtask
+
+import androidx.fragment.app.FragmentActivity
+import com.adealink.frame.base.Rlt
+import com.adealink.weparty.commonui.dialogchain.BaseDialogData
+import com.adealink.weparty.commonui.dialogchain.BaseDialogTask
+import com.adealink.weparty.commonui.dialogchain.Priority
+import com.adealink.weparty.commonui.widget.CommonDialog
+import com.adealink.weparty.module.anchor.AnchorModule
+import com.adealink.weparty.module.anchor.data.AnchorMessage
+import com.adealink.weparty.module.anchor.data.AnchorMessageReplyCode
+
+/**
+ * 通用安全通知对话框任务
+ * Dialog构建位于AnchorManager
+ */
+class CommonDialogTask(
+    private val message: AnchorMessage,
+    private val dialog: CommonDialog,
+) : BaseDialogTask<BaseDialogData.SafetyNotificationData>() {
+
+    override val priority: Int = Priority.HIGHEST.priority
+    override val tag: String = "CommonDialogTask"
+
+    override suspend fun canShow(): Rlt<BaseDialogData.SafetyNotificationData> {
+        return Rlt.Success(
+            BaseDialogData.SafetyNotificationData(
+                messageId = message.messageId,
+                messageType = message.messageType
+            )
+        )
+    }
+
+    override fun showDialog(
+        dialogData: BaseDialogData.SafetyNotificationData,
+        fragmentActivity: FragmentActivity
+    ): Int {
+        // 展示即消费
+        val anchorViewModel = AnchorModule.getAnchorViewModel(fragmentActivity)
+        anchorViewModel?.replyAnchorMessage(
+            dialogData.messageId,
+            dialogData.messageType,
+            AnchorMessageReplyCode.REPLY_READ
+        )
+        dialog.show(fragmentActivity.supportFragmentManager,"CommonDialogTask")
+        return dialog.hashCode()
+    }
+}

+ 0 - 4
app/src/main/java/com/adealink/weparty/debug/DebugActivity.kt

@@ -28,7 +28,6 @@ import com.adealink.weparty.log.viewmodel.LogViewModel
 import com.adealink.weparty.module.account.AccountLocalService
 import com.adealink.weparty.module.call.Call
 import com.adealink.weparty.module.call.CallModule
-import com.adealink.weparty.module.safety.debug.Safety
 import com.adealink.weparty.module.setting.Setting
 import com.adealink.weparty.module.webview.Web
 import com.adealink.weparty.storage.AppPref
@@ -143,9 +142,6 @@ class DebugActivity : BaseActivity(), OnReturnValue {
         binding.tvSelectLanguage.setOnClickListener {
             Router.build(this, Setting.Language.PATH).start()
         }
-        binding.btSafetyDebug.setOnClickListener {
-            Router.build(this, Safety.Safety.PATH).start()
-        }
 
         binding.swPerformanceFloatView.isChecked = DebugPrefs.showPerformanceFloatView
         binding.swPerformanceFloatView.setOnCheckedChangeListener { _, isChecked ->

+ 9 - 1
app/src/main/java/com/adealink/weparty/module/anchor/AnchorModule.kt

@@ -10,7 +10,7 @@ import com.adealink.weparty.module.anchor.data.AnchorMessageReplyCode
 import com.adealink.weparty.module.anchor.viewmodel.IAnchorViewModel
 
 
-object AnchorModule: BaseDynamicModule<IAnchorService>(IAnchorService::class), IAnchorService {
+object AnchorModule : BaseDynamicModule<IAnchorService>(IAnchorService::class), IAnchorService {
     override val moduleNameResId: Int
         get() = R.string.module_anchor
     override val featureName: String
@@ -33,6 +33,10 @@ object AnchorModule: BaseDynamicModule<IAnchorService>(IAnchorService::class), I
         getService().handleAnchorMessage(context, fm, message, replayCallback)
     }
 
+    override fun handleAnchorTemplateMessage(message: AnchorMessage) {
+        getService().handleAnchorTemplateMessage(message)
+    }
+
     override fun emptyService(): IAnchorService {
         return object : IAnchorService {
             override fun getAnchorViewModel(owner: ViewModelStoreOwner): IAnchorViewModel? {
@@ -55,6 +59,10 @@ object AnchorModule: BaseDynamicModule<IAnchorService>(IAnchorService::class), I
             override fun getService(): IAnchorService? {
                 return null
             }
+
+            override fun handleAnchorTemplateMessage(message: AnchorMessage) {
+                // nth
+            }
         }
     }
 

+ 6 - 0
app/src/main/java/com/adealink/weparty/module/anchor/IAnchorService.kt

@@ -18,4 +18,10 @@ interface IAnchorService : IService<IAnchorService> {
         message: AnchorMessage,
         replayCallback: (AnchorMessage, AnchorMessageReplyCode) -> Unit
     )
+
+    /**
+     * 消费通用模版类型的广播消息
+     * @message:AnchorMessage
+     */
+    fun handleAnchorTemplateMessage(message: AnchorMessage)
 }

+ 16 - 3
app/src/main/java/com/adealink/weparty/module/anchor/data/AnchorData.kt

@@ -102,6 +102,17 @@ data class AnchorMessage(
             }
             return field
         }
+    /**
+     * Body作通用模版
+     */
+    var templateMessageBody: AnchorMessageTemplateBody? = null
+        get() {
+            if (field == null) {
+                field = froJsonErrorNull(messageBody)
+            }
+            return field
+        }
+
 }
 
 enum class AnchorBeReportedMessageType(val type: Int) {
@@ -118,8 +129,8 @@ enum class AnchorBeReportedMessageType(val type: Int) {
 
 enum class AnchorMessageType(val type: Int) {
     MESSAGR_DIAMOND_REWARD_NOTIFY(29),//工会长获得每月的钻石激励通知
-    MESSAGE_IM_DAILY_GREETING_NOTIFY(30),//日常消息上限提醒
-    USER_CHANGE_GENDER(32); // 性别变更
+    MESSAGE_IM_DAILY_GREETING_NOTIFY(30), //日常消息上限提醒
+    MESSAGE_USER_CHANGE_GENDER(32); // 性别变更
 
     companion object {
         fun map(type: Int): AnchorMessageType? {
@@ -193,7 +204,9 @@ enum class AnchorMsgBtnType(val type: Int) {
 enum class AnchorMsgBtnAction(val action: Int) {
     NO(0),//无行为
     HttpPost(1),//http post请求
-    Deeplink(2);//deeplink
+    Deeplink(2),//deeplink
+    Logout(3),//退出登录
+    ExitRoom(4);//退出房间
 
     companion object {
         fun map(action: Int): AnchorMsgBtnAction? {

+ 0 - 30
app/src/main/java/com/adealink/weparty/module/safety/data/Data.kt

@@ -1,30 +0,0 @@
-//package com.adealink.weparty.module.safety.data
-//
-//
-///**
-// * 处罚动作类型(安全消息类型)
-// */
-//enum class SafetyActionType(val actionType: Int) {
-//    USER_CHANGE_GENDER(32), // 性别变更
-//    USER_WARNING(33), // 警告
-//    USER_MUTE(34), // 禁言
-//    USER_BAN_ACCOUNT(35), // 封禁账户(user)
-//    USER_BAN_DEVICE(36), // 封禁设备(device)
-//
-//    ROOM_HIDE(37), // 隐藏房间
-//    ROOM_BAN(38); // 封禁房间
-//
-//    companion object {
-//        private val map by lazy { entries.associateBy { it.actionType } }
-//        fun fromValue(actionType: Int): SafetyActionType = map[actionType] ?: USER_WARNING
-//        fun isValuedActionType(messageType: Int): Boolean {
-//            return map.containsKey(messageType)
-//        }
-//
-//        /*
-//        fun fromValue(actionType: Int): SafetyActionType{
-//            return SafetyActionType.entries.firstOrNull{it.actionType == actionType} ?: USER_WARNING
-//        }
-//        */
-//    }
-//}

+ 0 - 15
app/src/main/java/com/adealink/weparty/module/safety/debug/Router.kt

@@ -1,15 +0,0 @@
-package com.adealink.weparty.module.safety.debug
-
-import org.jetbrains.annotations.TestOnly
-
-/**
- * Safety路由(联调后删)
- */
-@TestOnly
-class Safety {
-    interface Safety{
-        companion object{
-            const val PATH = "/safety"
-        }
-    }
-}

+ 0 - 147
app/src/main/java/com/adealink/weparty/module/safety/debug/SafetyDebugTool.kt

@@ -1,147 +0,0 @@
-//package com.adealink.weparty.module.safety.debug
-//
-//import androidx.fragment.app.FragmentManager
-//import com.adealink.frame.util.AppUtil
-//import com.adealink.weparty.commonui.BaseActivity
-//import com.adealink.weparty.module.anchor.AnchorModule
-//import com.adealink.weparty.module.anchor.data.AnchorMessage
-//import com.adealink.weparty.module.safety.data.SafetyActionType
-//import com.adealink.weparty.module.safety.safetyManager
-//import org.jetbrains.annotations.TestOnly
-//import org.json.JSONArray
-//import org.json.JSONObject
-//
-///**
-// * 安全提示调试数据(联调后删)
-// */
-//@TestOnly
-//object SafetyDebugTool {
-//    /**
-//     * 通过anchor消息测试
-//     */
-//    fun testSafetyAnchorMessage(message: AnchorMessage) {
-//        // 获取顶层活动的FragmentManager
-//        val activity = AppUtil.currentActivity as? BaseActivity ?: return
-//        val fm = activity.supportFragmentManager
-//
-////        // 调用SafetyManager处理消息并提供空回调
-////        safetyManager.handleSafetyAnchorMessage(fm, message) { _, _ -> }
-//    }
-//
-//    /**
-//     * 创建anchor消息体
-//     */
-//    private fun createMessageBody(
-//        title: String,
-//        text: String,
-//        buttonText: String = "确认"
-//    ): String {
-//        val buttons = JSONArray().apply {
-//            put(JSONObject().apply {
-//                put("actionType", 0)
-//                put("buttonText", buttonText)
-//                put("buttonType", 1)
-//            })
-//        }
-//
-//        return JSONObject().apply {
-//            put("messageButtons", buttons)
-//            put("text", text)
-//            put("title", title)
-//        }.toString()
-//    }
-//
-//    /**
-//     * 创建基础Anchor消息
-//     */
-//    private fun createBaseAnchorMessage(
-//        messageType: Int,
-//        title: String,
-//        text: String,
-//        buttonText: String = "确认"
-//    ): AnchorMessage {
-//        return AnchorMessage(
-//            messageId = 1463,
-//            messageType = messageType,
-//            templateMessage = 1,
-//            messageBody = createMessageBody(title, text, buttonText),
-//            messageTime = System.currentTimeMillis()
-//        )
-//    }
-//
-//    // 测试用户警告弹窗
-//    fun testWarning() {
-//        val message = createBaseAnchorMessage(
-//            messageType = SafetyActionType.USER_WARNING.actionType,
-//            title = "警告",
-//            text = "系统检测到您的行为存在违规,请注意您的行为举止,系统将持续监控您的一切违规行为。"
-//        )
-//        testSafetyAnchorMessage(message)
-//    }
-//
-//    // 测试禁言通知
-//    fun testMute() {
-//        val expireTimeText = "禁言将于24小时后解除"
-//        val message = createBaseAnchorMessage(
-//            messageType = SafetyActionType.USER_WARNING.actionType,
-//            title = "禁言通知",
-//            text = "系统检测到您的行为存在违规,您将被禁言,禁言期间您无法发送任何形式的消息。$expireTimeText"
-//        )
-//        testSafetyAnchorMessage(message)
-//    }
-//
-//    // 测试封禁账户通知
-//    fun testBanAccount() {
-//        val expireTimeText = "封禁将于7天后解除"
-//        val message = createBaseAnchorMessage(
-//            messageType = SafetyActionType.USER_BAN_ACCOUNT.actionType,
-//            title = "封号通知",
-//            text = "由于您多次违反社区规范,您的账号已被暂时封禁。$expireTimeText"
-//        )
-//        testSafetyAnchorMessage(message)
-//    }
-//
-//    // 测试永久封号通知
-//    fun testPermanentBanAccount() {
-//        val message = createBaseAnchorMessage(
-//            messageType = SafetyActionType.USER_BAN_ACCOUNT.actionType,
-//            title = "封号通知(永久)",
-//            text = "由于您严重违反平台规则,您的账号已被永久封禁。"
-//        )
-//        testSafetyAnchorMessage(message)
-//    }
-//
-//    // 测试设备封禁通知
-//    fun testBanDevice() {
-//        val expireTimeText = "封禁将于30天后解除"
-//        val message = createBaseAnchorMessage(
-//            messageType = SafetyActionType.USER_BAN_DEVICE.actionType,
-//            title = "设备封禁通知",
-//            text = "由于您的设备存在违规行为,该设备已被封禁。$expireTimeText"
-//        )
-//        testSafetyAnchorMessage(message)
-//    }
-//
-//    // 测试性别变更通知
-//    fun testGenderChange() {
-//        val anchorMessage = AnchorMessage(
-//            messageId = System.currentTimeMillis(),
-//            messageType = SafetyActionType.USER_CHANGE_GENDER.actionType,
-//            templateMessage = 1,
-//            messageBody = """{"messageButtons":[{"actionType":0,"buttonText":"确认","buttonType":1}],"text":"系统检测到【账户】【100033043】【账户性别与真实性别不符】,已自动纠正性别设置。","title":"性别变更通知"}""",
-//            messageTime = System.currentTimeMillis()
-//        )
-//        testSafetyAnchorMessage(anchorMessage)
-//    }
-//
-//    // 测试房间封禁通知
-//    fun testRoomBan() {
-//        val expireTimeText = "房间封禁将于7天后解除"
-//        val message = createBaseAnchorMessage(
-//            messageType = SafetyActionType.ROOM_BAN.actionType,
-//            title = "房间封禁通知",
-//            text = "房间ID:18091 该房间因违规已被封禁,所有用户将被强制退出房间。$expireTimeText"
-//        )
-//        testSafetyAnchorMessage(message)
-//    }
-//}

+ 1 - 1
app/src/main/java/com/adealink/weparty/ui/home/BaseHomeFragment.kt

@@ -232,7 +232,7 @@ abstract class BaseHomeFragment : BaseFragment, ITabManager {
                 }
             }
 
-            AnchorMessageType.USER_CHANGE_GENDER -> {
+            AnchorMessageType.MESSAGE_USER_CHANGE_GENDER -> {
                 val messageBody = froJsonErrorNull<AnchorMessageTemplateBody>(message.messageBody)
                 if (messageBody == null) {
                     Log.e(TAG_ANCHOR, "消息体解析失败")

+ 1 - 9
app/src/main/res/layout/activity_debug.xml

@@ -568,15 +568,7 @@
             app:layout_constraintStart_toStartOf="parent"
             app:layout_constraintTop_toBottomOf="@id/cl_call_test">
 
-            <Button
-                android:id="@+id/bt_safety_debug"
-                android:layout_width="wrap_content"
-                android:layout_height="50dp"
-                android:layout_marginTop="15dp"
-                android:text="Safety Debug"
-                app:layout_constraintEnd_toEndOf="parent"
-                app:layout_constraintStart_toStartOf="parent"
-                app:layout_constraintTop_toTopOf="parent" />
+
         </androidx.constraintlayout.widget.ConstraintLayout>
     </androidx.constraintlayout.widget.ConstraintLayout>
 </ScrollView>

+ 1 - 0
app/src/main/res/values/strings.xml

@@ -788,4 +788,5 @@
    <string name="common_version_too_old">Your app version is too low. Please update to continue</string>
    <string name="common_later">Later</string>
    <string name="common_set">Set</string>
+    <string name="common_confirm_message">Confirm %s?</string>
 </resources>

+ 4 - 0
module/anchor/src/main/java/com/adealink/weparty/anchor/AnchorServiceImpl.kt

@@ -33,6 +33,10 @@ class AnchorServiceImpl : IAnchorService {
 
     }
 
+    override fun handleAnchorTemplateMessage(message: AnchorMessage) {
+        anchorManager.handleAnchorTemplateMessage(message)
+    }
+
     override fun getService(): IAnchorService {
         return this
     }

+ 8 - 0
module/anchor/src/main/java/com/adealink/weparty/anchor/datasource/remote/AnchorHttpService.kt

@@ -5,8 +5,10 @@ import com.adealink.frame.network.data.Res
 import com.adealink.weparty.anchor.data.GetAnchorMessagesReq
 import com.adealink.weparty.anchor.data.GetAnchorMessagesRes
 import com.adealink.weparty.module.anchor.data.ReplyAnchorMessageReq
+import okhttp3.RequestBody
 import retrofit2.http.Body
 import retrofit2.http.POST
+import retrofit2.http.Path
 
 interface AnchorHttpService {
     @POST("config/reply_message/unread")
@@ -14,4 +16,10 @@ interface AnchorHttpService {
 
     @POST("config/reply_message/reply")
     suspend fun replyAnchorMessage(@Body req: ReplyAnchorMessageReq): Rlt<Res<Any>>
+
+    @POST("{param}")
+    suspend fun msgBtnHttpReq(
+        @Path(value = "param", encoded = true) url: String,
+        @Body req: RequestBody?,
+    ): Rlt<Res<Any>>
 }

+ 158 - 12
module/anchor/src/main/java/com/adealink/weparty/anchor/manager/AnchorManager.kt

@@ -1,6 +1,8 @@
 package com.adealink.weparty.anchor.manager
 
 import com.adealink.frame.aab.util.getCompatString
+import androidx.core.net.toUri
+import androidx.fragment.app.FragmentActivity
 import com.adealink.frame.base.CommonDataNullError
 import com.adealink.frame.base.Rlt
 import com.adealink.frame.data.json.froJsonErrorNull
@@ -8,7 +10,9 @@ import com.adealink.frame.frame.BaseFrame
 import com.adealink.frame.log.Log
 import com.adealink.frame.network.ISocketNotify
 import com.adealink.frame.network.data.Res
+import com.adealink.frame.router.Router
 import com.adealink.frame.util.AppUtil
+import com.adealink.frame.util.DisplayUtil
 import com.adealink.frame.util.runOnUiThread
 import com.adealink.weparty.App
 import com.adealink.weparty.R
@@ -18,18 +22,29 @@ import com.adealink.weparty.anchor.datasource.remote.AnchorHttpService
 import com.adealink.weparty.anchor.listener.IAnchorListener
 import com.adealink.weparty.commonui.BaseActivity
 import com.adealink.weparty.commonui.dialogchain.DialogTaskManager
+import com.adealink.weparty.commonui.dialogchain.dialogtask.CommonDialogTask
 import com.adealink.weparty.commonui.dialogchain.dialogtask.GenderChangeDialogTask
+import com.adealink.weparty.commonui.widget.CommonDialog
+import com.adealink.weparty.module.account.AccountModule
 import com.adealink.weparty.module.anchor.constant.TAG_ANCHOR
 import com.adealink.weparty.module.anchor.data.AnchorMessage
+import com.adealink.weparty.module.anchor.data.AnchorMessageButton
 import com.adealink.weparty.module.anchor.data.AnchorMessageReplyCode
 import com.adealink.weparty.module.anchor.data.AnchorMessageTemplateBody
 import com.adealink.weparty.module.anchor.data.AnchorMessageTemplateType
 import com.adealink.weparty.module.anchor.data.AnchorMessageType
+import com.adealink.weparty.module.anchor.data.AnchorMsgBtnAction
+import com.adealink.weparty.module.anchor.data.AnchorMsgBtnType
 import com.adealink.weparty.module.anchor.data.ReplyAnchorMessageReq
 import com.adealink.weparty.module.operation.OperationModule
+import com.adealink.weparty.module.room.Room
+import com.adealink.weparty.module.room.RoomModule
+import com.adealink.weparty.util.goLocalLinkPage
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.withContext
+import okhttp3.MediaType.Companion.toMediaTypeOrNull
+import okhttp3.RequestBody.Companion.toRequestBody
 
 val anchorManager: IAnchorManager by lazy { AnchorManager() }
 
@@ -67,31 +82,44 @@ class AnchorManager : BaseFrame<IAnchorListener>(), IAnchorManager {
             TAG_ANCHOR,
             "handleAnchorMessageNotify, messageId:${data.messageId}, messageType: ${data.messageType}, templateMessage:${data.templateMessage}"
         )
+
         runOnUiThread {
-            val templateType = AnchorMessageTemplateType.map(data.templateMessage)
             val messageType = AnchorMessageType.map(data.messageType)
-            val handle = (templateType != null && templateType != AnchorMessageTemplateType.Non)
-                    || messageType == AnchorMessageType.MESSAGR_DIAMOND_REWARD_NOTIFY
+            val templateType = AnchorMessageTemplateType.map(data.templateMessage)
+            // 是否是模版消息(非Non)
+            val isTemplateMessage =
+                templateType != null && templateType != AnchorMessageTemplateType.Non
+
+            // 如果是特定消息类型或是有效的模板消息就处理
+            val handle = messageType == AnchorMessageType.MESSAGR_DIAMOND_REWARD_NOTIFY
                     || messageType == AnchorMessageType.MESSAGE_IM_DAILY_GREETING_NOTIFY
-                    || messageType == AnchorMessageType.USER_CHANGE_GENDER
+                    || messageType == AnchorMessageType.MESSAGE_USER_CHANGE_GENDER
+                    || isTemplateMessage
             if (handle) {
-                handleAnchorMessage(data)
+                handleAnchorMessage(data, messageType, isTemplateMessage)
             }
         }
     }
 
-    private fun handleAnchorMessage(data: AnchorMessage) {
-        when (AnchorMessageType.map(data.messageType)) {
+    private fun handleAnchorMessage(
+        data: AnchorMessage,
+        messageType: AnchorMessageType?,
+        isTemplateMessage: Boolean
+    ) {
+        Log.i(TAG_ANCHOR, "处理安全相关消息: messageType=${data.messageType}")
+
+        // 不干预原有的实现、再处理通用模版
+        when (messageType) {
             AnchorMessageType.MESSAGR_DIAMOND_REWARD_NOTIFY -> {
                 val activity = (AppUtil.currentActivity as? BaseActivity) ?: return
                 OperationModule.handleOperationMessage(
                     activity.supportFragmentManager,
                     data
-                ) { msg, replayCode ->
-
-                }
+                ) { msg, replayCode -> }
             }
-            AnchorMessageType.USER_CHANGE_GENDER->{
+
+            AnchorMessageType.MESSAGE_USER_CHANGE_GENDER -> {
+                // 解析消息体
                 val messageBody = froJsonErrorNull<AnchorMessageTemplateBody>(data.messageBody)
                 if (messageBody == null) {
                     Log.e(TAG_ANCHOR, "消息体解析失败")
@@ -119,7 +147,9 @@ class AnchorManager : BaseFrame<IAnchorListener>(), IAnchorManager {
             }
 
             else -> {
-
+                if (isTemplateMessage) {
+                    handleAnchorTemplateMessage(data)
+                }
             }
         }
     }
@@ -201,4 +231,120 @@ class AnchorManager : BaseFrame<IAnchorListener>(), IAnchorManager {
         }
     }
 
+    override fun handleAnchorTemplateMessage(message: AnchorMessage) {
+        when (AnchorMessageTemplateType.map(message.templateMessage)) {
+            AnchorMessageTemplateType.Normal -> {
+                handleNormalTemplateAnchorMsg(message)
+            }
+
+            // Radio模版暂不处理
+            AnchorMessageTemplateType.Radio -> {}
+            else -> {}
+        }
+    }
+
+    private fun handleNormalTemplateAnchorMsg(message: AnchorMessage) {
+        runOnUiThread {
+            val templateBody = message.templateMessageBody
+
+            val title = templateBody?.title
+            val text = templateBody?.text
+            val headUrl = templateBody?.url
+
+            var dialog: CommonDialog? = null
+            val dialogBuilder = CommonDialog.Builder()
+            if (title != null) {
+                dialogBuilder.title(title)
+            }
+            if (text != null) {
+                dialogBuilder.message(text)
+            }
+            if (headUrl != null) {
+                dialogBuilder.topImageUrl(headUrl, DisplayUtil.dp2px(90f), DisplayUtil.dp2px(80f))
+            }
+            var addedBtnCnt = 0
+            val buttons = templateBody?.messageButtons
+            buttons?.forEach loop@{ messageBtn ->
+                //目前只支持加两个按钮
+                if (addedBtnCnt >= 2) {
+                    return@loop
+                }
+                if (messageBtn.buttonType == AnchorMsgBtnType.Strong.type) {
+                    dialogBuilder.positiveText(messageBtn.buttonText)
+                    dialogBuilder.onPositive {
+                        onMsgButtonClick(messageBtn)
+                        dialog?.dismissAllowingStateLoss()
+                    }
+                    addedBtnCnt++
+                }
+                if (messageBtn.buttonType == AnchorMsgBtnType.Normal.type) {
+                    dialogBuilder.negativeText(messageBtn.buttonText)
+                    dialogBuilder.onNegative {
+                        onMsgButtonClick(messageBtn)
+                        dialog?.dismissAllowingStateLoss()
+                    }
+                    addedBtnCnt++
+                }
+            }
+            if (addedBtnCnt == 0) {
+                //如果没有按钮,增加一个确定按钮,点确定已读
+                dialogBuilder.onPositive {
+                    dialog?.dismissAllowingStateLoss()
+                }
+            }
+            dialogBuilder.dismissAfterClick(false)
+                .canceledOnTouchOutside(false)
+                .onDismiss { }
+            dialog = dialogBuilder.build()
+
+            // 创建并提交任务
+            val task = CommonDialogTask(message, dialog)
+            DialogTaskManager.submit(task)
+        }
+    }
+
+    private fun onMsgButtonClick(messageButton: AnchorMessageButton) {
+        when (AnchorMsgBtnAction.map(messageButton.actionType)) {
+            AnchorMsgBtnAction.Deeplink -> {
+                val activity = AppUtil.currentActivity ?: return
+                val deeplink = messageButton.buttonActionUrl
+                var path: String? = null
+                if (deeplink.isEmpty().not()) {
+                    path = deeplink.toUri().path
+                }
+                var canDispatch = false
+                if (path != null) {
+                    canDispatch = Router.getClazz(path) != null
+                }
+                if (canDispatch) {
+                    goLocalLinkPage(activity, messageButton.buttonActionUrl)
+                }
+            }
+
+            AnchorMsgBtnAction.HttpPost -> {
+                launch {
+                    val body =
+                        messageButton.buttonActionBody?.toRequestBody("application/json; charset=utf-8".toMediaTypeOrNull())
+                    anchorHttpService.msgBtnHttpReq(messageButton.buttonActionUrl, body)
+                }
+            }
+
+            AnchorMsgBtnAction.Logout -> {
+                // 执行登出操作
+                AccountModule.logout()
+            }
+
+            AnchorMsgBtnAction.ExitRoom -> {
+                RoomModule.logout()
+                // 尝试Finish
+                val topActivity = AppUtil.currentActivity ?: return
+                if (topActivity::class.java.name == (Router.getClazz(Room.Room.PATH)?.name ?: "") ) {
+                    topActivity.finish()
+                }
+            }
+
+            AnchorMsgBtnAction.NO -> {}
+            else -> {}
+        }
+    }
 }

+ 6 - 0
module/anchor/src/main/java/com/adealink/weparty/anchor/manager/IAnchorManager.kt

@@ -14,4 +14,10 @@ interface IAnchorManager : IBaseFrame<IAnchorListener> {
         messageType: Int,
         replyCode: AnchorMessageReplyCode
     ): Rlt<Res<Any>>
+
+    /**
+     * 消费通用模版类型的广播消息
+     * @message:AnchorMessage
+     */
+    fun handleAnchorTemplateMessage(message: AnchorMessage)
 }

+ 10 - 0
module/room/src/main/java/com/adealink/weparty/room/RoomActivity.kt

@@ -81,6 +81,11 @@ class RoomActivity : BaseActivity(), IRoomOpListener {
         private const val REJOIN_TIP_DIALOG = "RejoinTipDialog"
     }
 
+    /**
+     * 是否是房主(用于封禁/踢出通知弹窗逻辑)
+     */
+    private var isRoomOwner = false;
+
     private val binding by viewBinding(ActivityRoomBinding::inflate)
 
     //默认聊天房间
@@ -129,6 +134,7 @@ class RoomActivity : BaseActivity(), IRoomOpListener {
     override fun loadData() {
         super.loadData()
         if (memberViewModel.isIAmRoomAdmin()) {
+            isRoomOwner = true
             updateAdminRoomSetting()
         }
     }
@@ -142,6 +148,8 @@ class RoomActivity : BaseActivity(), IRoomOpListener {
             //当前房间已经销毁,finish
             finish()
         }
+        // 成功获取后标记是否Owner
+        isRoomOwner = memberViewModel.isIAmRoomOwner()
     }
 
     private fun inflateRoomFragment(roomType: RoomType) {
@@ -264,6 +272,7 @@ class RoomActivity : BaseActivity(), IRoomOpListener {
                         stopAndClearMusic()
                     }
                 }
+
                 else -> {}
             }
         }
@@ -384,6 +393,7 @@ class RoomActivity : BaseActivity(), IRoomOpListener {
     }
 
     private fun showKickOutDialog(reason: Long) {
+        if (isRoomOwner) return // 只显示通用模版消息
         CommonDialog.Builder()
             .message(
                 when (reason) {