fix:修改bug

This commit is contained in:
zerosaturation 2026-07-15 13:12:27 +08:00
parent 365f783ea0
commit 18aff3b682
8 changed files with 2129 additions and 1781 deletions

View File

@ -51,24 +51,33 @@ var allowedTypes = map[string]struct{}{
// NotificationService 通知服务业务层。
type NotificationService struct {
db *gorm.DB
notifRepo *repository.NotificationRepository
statsRepo *repository.NotificationStatsRepository
device *UserDeviceService // 用于推送时拉取用户活跃 cid;若 nil 则跳过推送
pusher push.Pusher // 推送客户端;若 nil 则跳过推送
db *gorm.DB
notifRepo *repository.NotificationRepository
statsRepo *repository.NotificationStatsRepository
device *UserDeviceService // 用于推送时拉取用户活跃 cid;若 nil 则跳过推送
pusher push.Pusher // 推送客户端;若 nil 则跳过推送
rateLimiter *push.RateLimiter // P1-3:推送节流器,按 (user, star, type) 60s 滑窗,>1 改 summary 标题
}
// NewNotificationService 创建 NotificationService。
//
// 参数 device 与 pusher 用于在 CreateNotification 成功后触发手机通知栏推送;
// 若任一为 nil,则不会触发推送(便于测试 / 关闭推送功能)。
//
// P1-3 修复:rateLimiter 在 NewNotificationService 中初始化,pusher 为 nil 时不创建
// (避免占用内存)。triggerPush 内部仍需 nil 检查,避免 panic。
func NewNotificationService(db *gorm.DB, device *UserDeviceService, pusher push.Pusher) *NotificationService {
var rl *push.RateLimiter
if pusher != nil {
rl = push.NewRateLimiter(60000)
}
return &NotificationService{
db: db,
notifRepo: repository.NewNotificationRepository(db),
statsRepo: repository.NewNotificationStatsRepository(db),
device: device,
pusher: pusher,
db: db,
notifRepo: repository.NewNotificationRepository(db),
statsRepo: repository.NewNotificationStatsRepository(db),
device: device,
pusher: pusher,
rateLimiter: rl,
}
}
@ -260,9 +269,22 @@ func (s *NotificationService) triggerPush(n *model.Notification) {
go func() {
cctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
// P1-3 fix:节流后再发送——60s 滑窗内同一 (user, star, type) 多条推送,
// 首条走原 title,后续合并为 "您有 N 条新<type中文>" summary。
// 注意只改 Title,不覆盖 Content(Content 是具体业务文案,客户端要展示)。
title := n.Title
if s.rateLimiter != nil {
key := fmt.Sprintf("%d:%d:%s", n.UserID, n.StarID, n.Type)
mode, count := s.rateLimiter.Allow(key)
if mode == "summary" && count > 1 {
title = fmt.Sprintf("您有 %d 条新%s", count, push.TypeChinese(n.Type))
}
}
if err := s.pusher.Send(cctx, push.Payload{
CIDs: cids,
Title: n.Title,
Title: title,
Content: n.Content,
Data: data,
}); err != nil {

View File

@ -13,7 +13,9 @@ import { clearAllSandboxTmpFiles } from '@/utils/ioPath.js'
// storage key
const HIDE_TIME_KEY = "app_last_hide_time";
// : type PUSH_DEBOUNCE_MS (/)
// : (type, notification_id) PUSH_DEBOUNCE_MS
// (/) P1-4 : keyed-by-type
// " nid type ", (type, nid)
const PUSH_DEBOUNCE_MS = 10000;
const recentByType = {};
@ -320,13 +322,16 @@ export default {
const envelope = payloadStr ? JSON.parse(payloadStr) : {};
const payload = envelope?.data || data || {};
const t = payload.type;
const nid = payload.notification_id || "";
const debounceKey = `${t}:${nid}`;
const now = Date.now();
// V1.2.7 10s type , dispatch
if (t && recentByType[t] && now - recentByType[t] < PUSH_DEBOUNCE_MS) {
console.log("[push] debounce skip", t);
// 10s (type, notification_id) , dispatch
// (P1-4: key=type nid, key)
if (debounceKey && recentByType[debounceKey] && now - recentByType[debounceKey] < PUSH_DEBOUNCE_MS) {
console.log("[push] debounce skip", debounceKey);
return;
}
if (t) recentByType[t] = now;
if (debounceKey) recentByType[debounceKey] = now;
// mailbox store: + ()PREPEND_ITEM
this.$store.dispatch("mailbox/applyPushPayload", {
data: payload,

View File

@ -32,7 +32,7 @@
"distribute" : {
/* android */
"android" : {
"packagename" : "online.topfans.app",
"packagename" : "online.topfans.app",
"permissions" : [
"<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>",
"<uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\"/>",

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -44,13 +44,22 @@ const typeMap = {
const typeLabel = computed(() => typeMap[props.notification.type] || props.notification.type || '通知')
const businessMeta = computed(() => {
// data JSON ,
// data string(JSON) object(proto structpb.AsMap() )
// P0-1 fix: convertNotification AsMap() , JSON.parse(obj) SyntaxError {}
let data = {}
try { data = JSON.parse(props.notification.data || '{}') } catch (e) { data = {} }
const raw = props.notification.data
if (raw) {
if (typeof raw === 'string') {
try { data = JSON.parse(raw) } catch (e) { data = {} }
} else if (typeof raw === 'object') {
data = raw
}
}
const list = []
if (data.feedback_id) list.push({ k: '反馈 ID', v: data.feedback_id })
if (data.report_id) list.push({ k: '举报 ID', v: data.report_id })
if (data.resolved_action) list.push({ k: '处理动作', v: actionLabel(data.resolved_action) })
// P1-1 fix:admin backend moderation_admin.py action(), resolved_action
if (data.action) list.push({ k: '处理动作', v: actionLabel(data.action) })
if (data.category_code) list.push({ k: '分类', v: data.category_code })
if (data.original_title) list.push({ k: '原标题', v: data.original_title })
if (data.activity_id) list.push({ k: '活动 ID', v: data.activity_id })

View File

@ -70,8 +70,10 @@ const notification = computed(() => {
async function loadOne() {
try {
// P0-2 fix:utils/api.js { code, message, data: { items, ... } },
// resp.items undefined find null detailCache
const resp = await getNotificationsApi({ type: type.value, page: 1, page_size: 50 })
const item = (resp.items || []).find(n => n.id === nid.value)
const item = (resp.data?.items || []).find(n => n.id === nid.value)
if (item) store.commit('mailbox/SET_DETAIL_CACHE', item)
} catch (e) {
console.warn('[mailbox/detail] loadOne failed:', e)

View File

@ -23,7 +23,8 @@
</template>
<script setup>
import { onShow } from '@dcloudio/uni-app'
import { onLoad, onShow, onHide } from '@dcloudio/uni-app'
import { useStore } from 'vuex'
// import Header from '../components/Header.vue'
import MailboxGroup from './components/MailboxGroup.vue'
import { useMailboxCenter } from './composables/useMailboxCenter.js'
@ -35,8 +36,11 @@ const {
onMarkAllRead, onClearAll,
onSelectItem, onLongPress,
isCollapsed, toggleCollapse,
onMarkRead,
} = useMailboxCenter()
const store = useStore()
const goBack = () => {
//
const pages = getCurrentPages();
@ -51,7 +55,34 @@ const goBack = () => {
}
};
onShow(() => { loadAll() })
// P1-5 fix:App.vue click ?focus=mailbox&nid=N&type=T ,
// query "",""
// composable onMarkRead(nid, type), store action()
let focusHandled = false
onLoad((options) => {
if (!options || focusHandled) return
const nid = Number(options.nid)
const type = options.type || ''
if (nid && type) {
focusHandled = true
// fire-and-forget: warn,
onMarkRead(nid, type).catch((e) => {
console.warn('[mailbox/index] markAsRead from push failed:', e)
})
uni.navigateTo({ url: `/pages/mailbox/detail?nid=${nid}&type=${encodeURIComponent(type)}` })
}
})
// P1-2 fix:, PREPEND_ITEM()
// onShow SET_IS_IN_PAGE=true,onHide=falseApp.vue mailbox/applyPushPayload ,
// mailbox store applyPushPayload rootState.mailbox.isInMailboxPage PREPEND_ITEM
onShow(() => {
store.commit('mailbox/SET_IS_IN_PAGE', true)
loadAll()
})
onHide(() => {
store.commit('mailbox/SET_IS_IN_PAGE', false)
})
</script>
<style lang="scss" scoped>