From b3f2b2b3c23b7c42c74981cdf65cdb1c45c87453 Mon Sep 17 00:00:00 2001 From: mars <3361409208@qq.com> Date: Wed, 5 Aug 2026 15:45:39 +0800 Subject: [PATCH] =?UTF-8?q?feat(mmp):=20=E9=A2=86=E5=8F=96=E8=BF=9B?= =?UTF-8?q?=E5=BA=A6/=E8=BF=87=E6=9C=9F=E7=8A=B6=E6=80=81=EF=BC=8C?= =?UTF-8?q?=E5=B9=B6=E5=B1=95=E7=A4=BA=E7=BA=A2=E5=8C=85=E4=B8=8E=E9=A2=86?= =?UTF-8?q?=E5=8F=96=E4=BA=BAID?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 状态优先显示已全部领取或已领取N/M,未领完才附加已过期;列表与最高最低补全 activityId、receiverId。 --- .../smsmessage/Activity/MmpClaimActivity.java | 54 +++++--- .../Activity/MmpDetailActivity.java | 28 ++-- .../smsmessage/mmp/MmpModuleInfo.java | 4 +- .../smsmessage/mmp/MmpPacket.java | 95 +++++++++++++- .../smsmessage/mmp/MmpPacketStore.java | 118 ++++++++++++++++- .../main/res/layout/activity_mmp_detail.xml | 4 +- app/src/main/res/layout/item_mmp_packet.xml | 48 ++++--- debug-server/mmp_page.html | 76 +++++++++-- debug-server/server.py | 120 +++++++++++++++++- xposed-module/build.gradle | 4 +- .../smsmessage/xposed/HookBridge.java | 4 +- .../xposed/hook/TngMoneyPacketHook.java | 80 +++++++++++- 12 files changed, 559 insertions(+), 76 deletions(-) diff --git a/app/src/main/java/com/miraclegarden/smsmessage/Activity/MmpClaimActivity.java b/app/src/main/java/com/miraclegarden/smsmessage/Activity/MmpClaimActivity.java index 7a19fca..130dc07 100644 --- a/app/src/main/java/com/miraclegarden/smsmessage/Activity/MmpClaimActivity.java +++ b/app/src/main/java/com/miraclegarden/smsmessage/Activity/MmpClaimActivity.java @@ -7,6 +7,7 @@ import android.os.Bundle; import android.os.Handler; import android.os.Looper; import android.text.Editable; +import android.text.TextUtils; import android.text.TextWatcher; import android.view.View; import android.widget.TextView; @@ -157,42 +158,52 @@ public class MmpClaimActivity extends MiracleGardenActivity 0 ? packet.sumClaimed : parseDouble(packet.total))); + TextView idView = holder.getView(R.id.tv_id); + if (!TextUtils.isEmpty(packet.packetId)) { + idView.setVisibility(View.VISIBLE); + idView.setText("ID " + packet.packetId); + } else { + idView.setVisibility(View.GONE); + idView.setText(""); + } + android.widget.TextView status = holder.getView(R.id.tv_status); - if (packet.finished) { - status.setText("已领完"); + status.setText(packet.statusLabel()); + if (packet.isFullyClaimed()) { status.setTextColor(Color.parseColor("#047857")); status.setBackgroundColor(Color.parseColor("#ECFDF5")); + } else if (packet.isExpired()) { + status.setTextColor(Color.parseColor("#6B7280")); + status.setBackgroundColor(Color.parseColor("#F3F4F6")); } else { - status.setText("领取中"); status.setTextColor(Color.parseColor("#B45309")); status.setBackgroundColor(Color.parseColor("#FFFBEB")); } - String meta = packet.claimantCount + " 人"; + String meta; + if (packet.effectiveTotalCount() > 0) { + meta = packet.effectiveClaimedCount() + "/" + packet.effectiveTotalCount() + " 人"; + } else { + meta = packet.claimantCount + " 人"; + } if (packet.issuedAt != null && !packet.issuedAt.isEmpty()) { meta += " · 发放 " + packet.issuedAt; } + if (packet.expiresAt != null && !packet.expiresAt.isEmpty()) { + meta += " · 到期 " + packet.expiresAt; + } holder.setText(R.id.tv_meta, meta); String hi = packet.finished ? "最佳" : "最高"; String lo = packet.finished ? "最差" : "最低"; com.miraclegarden.smsmessage.mmp.MmpClaim best = packet.bestClaim(); com.miraclegarden.smsmessage.mmp.MmpClaim worst = packet.worstClaim(); - if (best != null) { - String amt = best.amountText != null ? best.amountText : formatMoney(best.amount); - String nick = best.nickname != null ? best.nickname : "?"; - holder.setText(R.id.tv_best, hi + " " + nick + " " + amt); - } else { - holder.setText(R.id.tv_best, hi + " -"); - } + holder.setText(R.id.tv_best, formatHiLoLine(hi, best)); if (worst != null && packet.claimantCount > 1) { - String amt = worst.amountText != null ? worst.amountText : formatMoney(worst.amount); - String nick = worst.nickname != null ? worst.nickname : "?"; - holder.setText(R.id.tv_worst, lo + " " + nick + " " + amt); + holder.setText(R.id.tv_worst, formatHiLoLine(lo, worst)); } else { holder.setText(R.id.tv_worst, lo + " -"); } - holder.setText(R.id.tv_id, packet.shortId()); holder.itemView.setOnClickListener(v -> openDetail(packet.packetId)); } }; @@ -538,6 +549,19 @@ public class MmpClaimActivity extends MiracleGardenActivity 0) { sub.append(" · "); } - sub.append("已领完"); - } else { - if (sub.length() > 0) { - sub.append(" · "); - } - sub.append("领取中"); + sub.append("到期 ").append(packet.expiresAt); } + if (sub.length() > 0) { + sub.append(" · "); + } + sub.append(packet.statusLabel()); binding.tvSub.setText(sub.toString()); binding.tvStatPeople.setText(String.valueOf(packet.claimantCount)); @@ -181,7 +180,7 @@ public class MmpDetailActivity extends MiracleGardenActivity 1) { worstAmount = worst.amount; - binding.tvWorstName.setText(worst.nickname != null ? worst.nickname : "?"); + binding.tvWorstName.setText(formatClaimName(worst)); binding.tvWorstAmt.setText(worst.amountText != null ? worst.amountText : String.format(Locale.US, "%.2f", worst.amount)); } else { @@ -213,6 +212,17 @@ public class MmpDetailActivity extends MiracleGardenActivity leaderboard = new ArrayList<>(); @@ -74,10 +81,12 @@ public class MmpPacket { } public String luckSummaryLine() { + refreshClosedState(); MmpClaim best = bestClaim(); MmpClaim worst = worstClaim(); - String hi = finished ? "手气最佳" : "目前最高"; - String lo = finished ? "手气最差" : "目前最低"; + boolean closed = finished; + String hi = closed ? "手气最佳" : "目前最高"; + String lo = closed ? "手气最差" : "目前最低"; if (best == null) { return hi + ":- " + lo + ":-"; } @@ -93,6 +102,87 @@ public class MmpPacket { return hi + ":" + bn + "(" + ba + ") " + lo + ":" + wn + "(" + wa + ")"; } + /** 根据 expire/status/人数 刷新 finished;过期未领完也会标结束 */ + public void refreshClosedState() { + if (isFullyClaimed()) { + finished = true; + return; + } + if (finished) { + return; + } + if (isTerminalStatus(status) || isExpirePassed()) { + finished = true; + } + } + + public boolean isExpired() { + if (isExpirePassed()) { + return true; + } + if (status == null) { + return false; + } + String s = status.toUpperCase(Locale.US); + return s.contains("EXPIRE") || s.contains("ENDED") || s.contains("CLOSED") + || s.contains("CANCEL"); + } + + public boolean isFullyClaimed() { + int claimed = effectiveClaimedCount(); + int total = effectiveTotalCount(); + return total > 0 && claimed >= total; + } + + public int effectiveClaimedCount() { + return Math.max(claimedCount, claimantCount); + } + + public int effectiveTotalCount() { + return Math.max(0, totalCount); + } + + /** + * 优先展示领取进度,过期放后面: + * 已全部领取 / 已领取 N/M 人 · 已过期 + */ + public String statusLabel() { + refreshClosedState(); + String progress; + if (isFullyClaimed()) { + progress = "已全部领取"; + } else { + int claimed = effectiveClaimedCount(); + int total = effectiveTotalCount(); + if (total > 0) { + progress = "已领取 " + claimed + "/" + total + " 人"; + } else if (claimed > 0) { + progress = "已领取 " + claimed + " 人"; + } else { + progress = finished ? "未领取" : "领取中"; + } + } + if (isExpired() && !isFullyClaimed()) { + return progress + " · 已过期"; + } + return progress; + } + + private boolean isExpirePassed() { + long ms = parseIssueMs(expiresAt); + return ms > 0 && System.currentTimeMillis() >= ms; + } + + private static boolean isTerminalStatus(String status) { + if (status == null || status.isEmpty()) { + return false; + } + String s = status.toUpperCase(Locale.US); + return s.contains("FINISH") || s.contains("COMPLETE") || s.contains("EXPIRE") + || s.contains("ENDED") || s.contains("CLOSED") || s.contains("CANCEL") + || s.contains("DONE"); + } + public boolean matchesFilter(String keyword, int statusFilter) { return matchesFilter(keyword, statusFilter, 0, 0L, 0L); } @@ -103,6 +193,7 @@ public class MmpPacket { */ public boolean matchesFilter(String keyword, int statusFilter, int timeMode, long customFromMs, long customToMs) { + refreshClosedState(); if (statusFilter == 1 && finished) { return false; } diff --git a/app/src/main/java/com/miraclegarden/smsmessage/mmp/MmpPacketStore.java b/app/src/main/java/com/miraclegarden/smsmessage/mmp/MmpPacketStore.java index 32c2f46..cda59e5 100644 --- a/app/src/main/java/com/miraclegarden/smsmessage/mmp/MmpPacketStore.java +++ b/app/src/main/java/com/miraclegarden/smsmessage/mmp/MmpPacketStore.java @@ -96,14 +96,30 @@ public final class MmpPacketStore { incoming.total = nullToEmpty(parsed.meta.get("total")); incoming.via = nullToEmpty(parsed.meta.get("via")); incoming.issuedAt = firstNonEmpty(parsed.meta, "issued", "createTime", "issuedAt", "create"); + incoming.expiresAt = firstNonEmpty(parsed.meta, "expire", "expireTime", "expiry", "expiresAt"); + incoming.status = firstNonEmpty(parsed.meta, "status", "activityStatus"); + parseCountsMeta(incoming, parsed.meta.get("counts")); + if (incoming.claimedCount <= 0) { + tryParseIntField(incoming, parsed.meta, "claimed", "claimedCount"); + } + if (incoming.totalCount <= 0) { + tryParseIntField(incoming, parsed.meta, "slots", "totalCount", "quantity"); + } incoming.finished = "1".equals(parsed.meta.get("done")) || "true".equalsIgnoreCase(parsed.meta.get("done")) - || "FINISHED".equalsIgnoreCase(parsed.meta.get("status")); + || isTerminalStatusMeta(incoming.status); + incoming.refreshClosedState(); incoming.updatedAt = nowText(); incoming.snapshots = 1; List ranked = aggregateClaims(parsed.claims); incoming.leaderboard = ranked; incoming.claimantCount = ranked.size(); + if (incoming.claimedCount <= 0) { + incoming.claimedCount = ranked.size(); + } + if (incoming.totalCount > 0 && incoming.claimedCount >= incoming.totalCount) { + incoming.finished = true; + } double sum = 0; for (MmpClaim c : ranked) { sum += c.amount; @@ -127,9 +143,23 @@ public final class MmpPacketStore { if (TextUtils.isEmpty(incoming.issuedAt) && !TextUtils.isEmpty(existing.issuedAt)) { incoming.issuedAt = existing.issuedAt; } + if (TextUtils.isEmpty(incoming.expiresAt) && !TextUtils.isEmpty(existing.expiresAt)) { + incoming.expiresAt = existing.expiresAt; + } + if (TextUtils.isEmpty(incoming.status) && !TextUtils.isEmpty(existing.status)) { + incoming.status = existing.status; + } + if (incoming.totalCount <= 0 && existing.totalCount > 0) { + incoming.totalCount = existing.totalCount; + } + if (incoming.claimedCount < existing.claimedCount) { + // 保留更大的已领人数 + incoming.claimedCount = existing.claimedCount; + } if (!incoming.finished && existing.finished) { incoming.finished = true; } + incoming.refreshClosedState(); map.put(packetId, incoming); } else { existing.snapshots = existing.snapshots + 1; @@ -193,6 +223,11 @@ public final class MmpPacketStore { return nullToEmpty(b.packetId).compareTo(nullToEmpty(a.packetId)); } }); + for (MmpPacket p : list) { + if (p != null) { + p.refreshClosedState(); + } + } return list; } } @@ -213,7 +248,11 @@ public final class MmpPacketStore { return null; } synchronized (MmpPacketStore.class) { - return loadMap(context).get(packetId); + MmpPacket p = loadMap(context).get(packetId); + if (p != null) { + p.refreshClosedState(); + } + return p; } } @@ -601,9 +640,13 @@ public final class MmpPacketStore { o.put("total", nullToEmpty(p.total)); o.put("via", nullToEmpty(p.via)); o.put("issuedAt", nullToEmpty(p.issuedAt)); + o.put("expiresAt", nullToEmpty(p.expiresAt)); + o.put("status", nullToEmpty(p.status)); o.put("updatedAt", nullToEmpty(p.updatedAt)); o.put("finished", p.finished); o.put("snapshots", p.snapshots); + o.put("claimedCount", p.claimedCount); + o.put("totalCount", p.totalCount); o.put("claimantCount", p.claimantCount); o.put("sumClaimed", p.sumClaimed); JSONArray board = new JSONArray(); @@ -633,9 +676,16 @@ public final class MmpPacketStore { p.total = o.optString("total", ""); p.via = o.optString("via", ""); p.issuedAt = o.optString("issuedAt", ""); + p.expiresAt = o.optString("expiresAt", ""); + p.status = o.optString("status", ""); p.updatedAt = o.optString("updatedAt", ""); p.finished = o.optBoolean("finished", false); p.snapshots = o.optInt("snapshots", 1); + p.claimedCount = o.optInt("claimedCount", 0); + p.totalCount = o.optInt("totalCount", 0); + p.claimantCount = o.optInt("claimantCount", 0); + p.refreshClosedState(); + p.snapshots = o.optInt("snapshots", 1); p.claimantCount = o.optInt("claimantCount", 0); p.sumClaimed = o.optDouble("sumClaimed", 0); JSONArray board = o.optJSONArray("leaderboard"); @@ -657,6 +707,10 @@ public final class MmpPacketStore { if (p.claimantCount <= 0) { p.claimantCount = p.leaderboard.size(); } + if (p.claimedCount <= 0) { + p.claimedCount = p.claimantCount; + } + p.refreshClosedState(); return p; } @@ -684,6 +738,66 @@ public final class MmpPacketStore { return ""; } + /** counts=1/5 */ + private static void parseCountsMeta(MmpPacket packet, String counts) { + if (packet == null || TextUtils.isEmpty(counts)) { + return; + } + String raw = counts.trim(); + int slash = raw.indexOf('/'); + try { + if (slash >= 0) { + String left = raw.substring(0, slash).trim(); + String right = raw.substring(slash + 1).trim(); + if (!left.isEmpty()) { + packet.claimedCount = Integer.parseInt(left); + } + if (!right.isEmpty()) { + packet.totalCount = Integer.parseInt(right); + } + } else { + packet.claimedCount = Integer.parseInt(raw); + } + } catch (NumberFormatException ignored) { + } + } + + private static void tryParseIntField(MmpPacket packet, Map meta, + String... keys) { + if (packet == null || meta == null) { + return; + } + for (String k : keys) { + String v = meta.get(k); + if (TextUtils.isEmpty(v)) { + continue; + } + try { + int n = Integer.parseInt(v.trim()); + if (n < 0) { + continue; + } + if ("claimed".equals(k) || "claimedCount".equals(k)) { + packet.claimedCount = n; + } else { + packet.totalCount = n; + } + return; + } catch (NumberFormatException ignored) { + } + } + } + + private static boolean isTerminalStatusMeta(String status) { + if (TextUtils.isEmpty(status)) { + return false; + } + String s = status.toUpperCase(Locale.US); + return s.contains("FINISH") || s.contains("COMPLETE") || s.contains("EXPIRE") + || s.contains("ENDED") || s.contains("CLOSED") || s.contains("CANCEL") + || s.contains("DONE"); + } + private static String nowText() { return new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()) .format(new java.util.Date()); diff --git a/app/src/main/res/layout/activity_mmp_detail.xml b/app/src/main/res/layout/activity_mmp_detail.xml index f0dbec4..d64a039 100644 --- a/app/src/main/res/layout/activity_mmp_detail.xml +++ b/app/src/main/res/layout/activity_mmp_detail.xml @@ -179,7 +179,7 @@ android:layout_height="wrap_content" android:layout_marginTop="4dp" android:ellipsize="end" - android:maxLines="1" + android:maxLines="2" android:text="-" android:textColor="#047857" android:textSize="16sp" @@ -219,7 +219,7 @@ android:layout_height="wrap_content" android:layout_marginTop="4dp" android:ellipsize="end" - android:maxLines="1" + android:maxLines="2" android:text="-" android:textColor="#B45309" android:textSize="16sp" diff --git a/app/src/main/res/layout/item_mmp_packet.xml b/app/src/main/res/layout/item_mmp_packet.xml index a3f3be2..872f039 100644 --- a/app/src/main/res/layout/item_mmp_packet.xml +++ b/app/src/main/res/layout/item_mmp_packet.xml @@ -16,17 +16,34 @@ android:gravity="center_vertical" android:orientation="horizontal"> - + android:orientation="vertical"> + + + + + - - =8&&s.indexOf('-')>0)?s.slice(0,8):(s.slice(0,16)||'-'); } + function formatHiLo(prefix, row){ + if(!row) return prefix + ' -'; + const nick = row.nickname || row.bestNick || '-'; + const amt = row.amountText || row.bestAmount || (row.amount!=null ? money(row.amount) : ''); + const uid = row.userId || ''; + if(uid) return `${prefix} ${esc(nick)} ${esc(amt)}
ID ${esc(uid)}
`; + return `${prefix} ${esc(nick)} ${esc(amt)}`.trim(); + } function hiLo(p){ return { hi: p&&p.finished?'手气最佳':'目前最高', @@ -407,19 +418,35 @@ const div=document.createElement('div'); div.className='pkt'+(p.packetId===activeId?' active':''); div.onclick=()=>{ activeId=p.packetId; renderList(); }; - const badge=p.finished?'已领完':'领取中'; - const hiShort = p.finished ? '最佳' : '最高'; - const loShort = p.finished ? '最差' : '最低'; - const bestLine = `${hiShort} ${esc(p.bestNick||'-')} ${esc(p.bestAmount||'')}`.trim(); + const label = p.statusLabel || (p.finished ? '已领完' : '领取中'); + const fully = label.indexOf('已全部领取') >= 0; + const expired = label.indexOf('已过期') >= 0; + const badgeClass = fully ? 'done' : (expired ? 'expired' : 'open'); + const badge=`${esc(label)}`; + const hiShort = p.finished || fully ? '最佳' : '最高'; + const loShort = p.finished || fully ? '最差' : '最低'; + const best = (p.leaderboard && p.leaderboard[0]) || null; + const worst = (p.leaderboard && p.leaderboard.length > 1) + ? p.leaderboard[p.leaderboard.length - 1] : null; + const bestLine = formatHiLo(hiShort, best || {nickname:p.bestNick, amountText:p.bestAmount, userId:p.bestUserId}); const worstLine = (p.claimantCount||0)<=1 ? `${loShort} -` - : `${loShort} ${esc(p.worstNick||'-')} ${esc(p.worstAmount||'')}`.trim(); + : formatHiLo(loShort, worst || {nickname:p.worstNick, amountText:p.worstAmount, userId:p.worstUserId}); + const timeLine = p.expiresAt + ? `${esc(p.issuedAt||'时间未知')} · 到期 ${esc(p.expiresAt)}` + : esc(p.issuedAt||'时间未知'); + const countLine = (p.totalCount > 0) + ? `${p.claimedCount||p.claimantCount||0}/${p.totalCount}人` + : `${p.claimantCount||0}人`; div.innerHTML=`
-
${esc(p.sender||p.title||'红包')}
+
+
${esc(p.sender||p.title||'红包')}
+ ${p.packetId ? `
ID ${esc(p.packetId)}
` : ''} +
${money(p.sumClaimed??p.total??0)}
-
${badge}${p.claimantCount||0}人 · ${esc(p.issuedAt||'时间未知')}${esc(shortId(p.packetId))}
+
${badge}${countLine} · ${timeLine}
${bestLine}
${worstLine}
@@ -437,21 +464,42 @@ const parts=[]; if(p.packetId) parts.push('ID '+p.packetId); if(p.issuedAt) parts.push('发放 '+p.issuedAt); + if(p.expiresAt) parts.push('到期 '+p.expiresAt); if(p.via) parts.push(p.via); sub.textContent = parts.join(' · '); cPeople.textContent = p.claimantCount || 0; cSum.textContent = money(p.sumClaimed ?? 0); cTotal.textContent = p.total || '-'; - cStatus.innerHTML = p.finished ? '已领完' : '领取中'; + cStatus.innerHTML = (()=>{ + const label = p.statusLabel || (p.finished ? '已领完' : '领取中'); + const fully = label.indexOf('已全部领取') >= 0; + const expired = label.indexOf('已过期') >= 0; + const cls = fully ? 'done' : (expired ? 'expired' : 'open'); + return `${esc(label)}`; + })(); luckBestLab.textContent = labels.hi; luckWorstLab.textContent = labels.lo; - luckBestName.textContent = p.bestNick || '-'; - luckBestAmt.textContent = p.bestAmount ? ('RM ' + p.bestAmount) : '-'; - if((p.claimantCount||0)<=1){ - luckWorstName.textContent='-'; luckWorstAmt.textContent='-'; + const best = (p.leaderboard && p.leaderboard[0]) || null; + const worst = (p.leaderboard && p.leaderboard.length > 1) + ? p.leaderboard[p.leaderboard.length - 1] : null; + if(best){ + luckBestName.innerHTML = best.userId + ? `${esc(best.nickname||'-')}
ID ${esc(best.userId)}
` + : esc(best.nickname||p.bestNick||'-'); + luckBestAmt.textContent = best.amountText ? ('RM ' + best.amountText) + : (p.bestAmount ? ('RM ' + p.bestAmount) : '-'); } else { - luckWorstName.textContent = p.worstNick || '-'; - luckWorstAmt.textContent = p.worstAmount ? ('RM ' + p.worstAmount) : '-'; + luckBestName.textContent = p.bestNick || '-'; + luckBestAmt.textContent = p.bestAmount ? ('RM ' + p.bestAmount) : '-'; + } + if(worst && (p.claimantCount||0)>1){ + luckWorstName.innerHTML = worst.userId + ? `${esc(worst.nickname||'-')}
ID ${esc(worst.userId)}
` + : esc(worst.nickname||p.worstNick||'-'); + luckWorstAmt.textContent = worst.amountText ? ('RM ' + worst.amountText) + : (p.worstAmount ? ('RM ' + p.worstAmount) : '-'); + } else { + luckWorstName.textContent='-'; luckWorstAmt.textContent='-'; } const rows=p.leaderboard||[]; board.innerHTML=''; diff --git a/debug-server/server.py b/debug-server/server.py index 70d3af5..c48df2a 100644 --- a/debug-server/server.py +++ b/debug-server/server.py @@ -19,8 +19,8 @@ PACKETS_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "mmp_pac HOOK_STATUS_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "mmp_hook_status.json") # 与 xposed-module / App MmpModuleInfo 同步 -EXPECTED_MODULE_CODE = 3 -EXPECTED_MODULE_NAME = "1.2.0" +EXPECTED_MODULE_CODE = 5 +EXPECTED_MODULE_NAME = "1.2.2" _messages = deque(maxlen=MAX_MESSAGES) _mmp_messages = deque(maxlen=MAX_MESSAGES) @@ -629,6 +629,9 @@ def _normalize_synced_packet(raw): elif sum_claimed: total = "{0:.2f}".format(sum_claimed) issued = raw.get("issuedAt") or raw.get("issued") or "" + expires = raw.get("expiresAt") or raw.get("expire") or raw.get("expireTime") or "" + status = raw.get("status") or raw.get("activityStatus") or "" + finished = bool(raw.get("finished")) or _is_terminal_status(status) or _is_expire_passed(expires) return { "packetId": pid, "title": raw.get("title") or "TNG 红包", @@ -637,11 +640,15 @@ def _normalize_synced_packet(raw): "total": total, "via": raw.get("via") or "phone-sync", "issuedAt": issued, + "expiresAt": expires, + "status": status, "updatedAt": raw.get("updatedAt") or "", "fetchedAt": raw.get("updatedAt") or raw.get("fetchedAt") or _now_iso(), "latestAt": issued or raw.get("updatedAt") or "", - "finished": bool(raw.get("finished")), + "finished": finished, "snapshots": int(raw.get("snapshots") or 1), + "claimedCount": int(raw.get("claimedCount") or 0), + "totalCount": int(raw.get("totalCount") or 0), "leaderboard": norm_board, "claimantCount": len(norm_board) if norm_board else int(raw.get("claimantCount") or 0), "sumClaimed": sum_claimed, @@ -741,7 +748,8 @@ def _mmp_packets(): key = str(packet_id) issued = meta.get("issued") or "" finished = str(meta.get("done") or "").lower() in ("1", "true") \ - or str(meta.get("status") or "").upper() in ("FINISHED", "COMPLETE", "COMPLETED", "EXPIRED") + or _is_terminal_status(meta.get("status") or "") \ + or _is_expire_passed(meta.get("expire") or meta.get("expireTime") or "") item = { "packetId": key, "title": msg.get("title") or "TNG 红包", @@ -750,12 +758,15 @@ def _mmp_packets(): "total": meta.get("total") or "", "via": meta.get("via") or "", "issuedAt": issued, + "expiresAt": meta.get("expire") or meta.get("expireTime") or "", + "status": meta.get("status") or "", "fetchedAt": msg.get("receivedAt") or "", "latestAt": issued or (msg.get("receivedAt") or ""), "finished": finished, "snapshots": 1, "claims": claims, } + _apply_counts_from_meta(item, meta) existing = packets.get(key) def _score(it): return ( @@ -801,6 +812,10 @@ def _mmp_packets(): p["bestAmount"] = (best or {}).get("amountText") or "" p["worstNick"] = (worst or {}).get("nickname") or "" p["worstAmount"] = (worst or {}).get("amountText") or "" + if not p.get("claimedCount"): + p["claimedCount"] = len(ranked) + if p.get("totalCount") and int(p.get("claimedCount") or 0) >= int(p.get("totalCount") or 0): + p["finished"] = True del p["claims"] result.append(p) # 合并手机全量同步落盘数据(电脑重启后仍可看) @@ -821,6 +836,9 @@ def _mmp_packets(): merged["latestAt"] = merged["issuedAt"] by_id[key] = merged result = list(by_id.values()) + for p in result: + _refresh_packet_closed(p) + p["statusLabel"] = _packet_status_label(p) result.sort(key=lambda x: _mmp_time_sort_key(x.get("issuedAt") or x.get("latestAt") or ""), reverse=True) return result @@ -834,6 +852,100 @@ def _mmp_time_sort_key(text): return s +def _parse_mmp_time_ms(text): + s = (text or "").strip() + if not s: + return 0 + m = re.match(r"^(\d{2})/(\d{2})/(\d{4})(?:\s+(\d{2}):(\d{2})(?::(\d{2}))?)?", s) + if m: + try: + dt = datetime( + int(m.group(3)), int(m.group(2)), int(m.group(1)), + int(m.group(4) or 23), int(m.group(5) or 59), int(m.group(6) or 59)) + return int(dt.timestamp() * 1000) + except ValueError: + return 0 + m = re.match(r"^(\d{4})-(\d{2})-(\d{2})(?:[ T](\d{2}):(\d{2})(?::(\d{2}))?)?", s) + if m: + try: + dt = datetime( + int(m.group(1)), int(m.group(2)), int(m.group(3)), + int(m.group(4) or 0), int(m.group(5) or 0), int(m.group(6) or 0)) + return int(dt.timestamp() * 1000) + except ValueError: + return 0 + if re.match(r"^\d{13}$", s): + return int(s) + if re.match(r"^\d{10}$", s): + return int(s) * 1000 + return 0 + + +def _is_terminal_status(status): + s = (status or "").upper() + if not s: + return False + return any(k in s for k in ( + "FINISH", "COMPLETE", "EXPIRE", "ENDED", "CLOSED", "CANCEL", "DONE")) + + +def _is_expire_passed(expire_text): + ms = _parse_mmp_time_ms(expire_text) + return ms > 0 and int(time.time() * 1000) >= ms + + +def _refresh_packet_closed(p): + if not isinstance(p, dict): + return p + if p.get("finished"): + return p + if _is_terminal_status(p.get("status") or "") or _is_expire_passed( + p.get("expiresAt") or p.get("expire") or ""): + p["finished"] = True + return p + + +def _packet_status_label(p): + _refresh_packet_closed(p) + claimed = int(p.get("claimedCount") or 0) + total = int(p.get("totalCount") or 0) + board_n = len(p.get("leaderboard") or []) if isinstance(p.get("leaderboard"), list) else 0 + claimed = max(claimed, board_n, int(p.get("claimantCount") or 0)) + fully = total > 0 and claimed >= total + if fully: + progress = "已全部领取" + elif total > 0: + progress = "已领取 {0}/{1} 人".format(claimed, total) + elif claimed > 0: + progress = "已领取 {0} 人".format(claimed) + else: + progress = "未领取" if p.get("finished") else "领取中" + expired = _is_expire_passed(p.get("expiresAt") or "") or any( + k in (p.get("status") or "").upper() for k in ("EXPIRE", "ENDED", "CLOSED", "CANCEL")) + if expired and not fully: + return progress + " · 已过期" + return progress + + +def _apply_counts_from_meta(item, meta): + counts = (meta.get("counts") or "").strip() + if counts and "/" in counts: + left, right = counts.split("/", 1) + try: + item["claimedCount"] = int(left.strip() or 0) + except ValueError: + pass + try: + item["totalCount"] = int(right.strip() or 0) + except ValueError: + pass + board = item.get("claims") or item.get("leaderboard") or [] + if not item.get("claimedCount") and isinstance(board, list): + item["claimedCount"] = len(board) + if item.get("totalCount") and item.get("claimedCount", 0) >= item["totalCount"]: + item["finished"] = True + + def _mmp_html_page(filename="mmp_page.html"): page = os.path.join(os.path.dirname(os.path.abspath(__file__)), filename) try: diff --git a/xposed-module/build.gradle b/xposed-module/build.gradle index 83d9519..174f478 100644 --- a/xposed-module/build.gradle +++ b/xposed-module/build.gradle @@ -10,8 +10,8 @@ android { applicationId "com.miraclegarden.smsmessage.xposed" minSdk 24 targetSdk 34 - versionCode 3 - versionName "1.2.0" + versionCode 5 + versionName "1.2.2" } buildTypes { diff --git a/xposed-module/src/main/java/com/miraclegarden/smsmessage/xposed/HookBridge.java b/xposed-module/src/main/java/com/miraclegarden/smsmessage/xposed/HookBridge.java index ed8e523..4f1b912 100644 --- a/xposed-module/src/main/java/com/miraclegarden/smsmessage/xposed/HookBridge.java +++ b/xposed-module/src/main/java/com/miraclegarden/smsmessage/xposed/HookBridge.java @@ -14,8 +14,8 @@ public final class HookBridge { public static final String EXTRA_MODULE_VERSION_NAME = "moduleVersionName"; public static final String EXTRA_HOST_PACKAGE = "hostPackage"; /** 与 xposed-module/build.gradle 保持同步 */ - public static final int MODULE_VERSION_CODE = 3; - public static final String MODULE_VERSION_NAME = "1.2.0"; + public static final int MODULE_VERSION_CODE = 5; + public static final String MODULE_VERSION_NAME = "1.2.2"; public static final String SOURCE_XPOSED_SQLITE = "xposed_sqlite"; public static final String SOURCE_XPOSED_WECHAT = "xposed_wechat"; public static final String SOURCE_XPOSED_TELEGRAM = "xposed_telegram"; diff --git a/xposed-module/src/main/java/com/miraclegarden/smsmessage/xposed/hook/TngMoneyPacketHook.java b/xposed-module/src/main/java/com/miraclegarden/smsmessage/xposed/hook/TngMoneyPacketHook.java index 5a9edfb..74cfdb4 100644 --- a/xposed-module/src/main/java/com/miraclegarden/smsmessage/xposed/hook/TngMoneyPacketHook.java +++ b/xposed-module/src/main/java/com/miraclegarden/smsmessage/xposed/hook/TngMoneyPacketHook.java @@ -938,7 +938,7 @@ public final class TngMoneyPacketHook { return s == null ? "" : s; } - /** remaining / claimed/total / status 任一表明已领完 */ + /** remaining / claimed/total / status 任一表明已领完或已结束 */ private static boolean isFinishedFlags(String remain, String claimedSlashTotal, String status) { if (!TextUtils.isEmpty(remain)) { try { @@ -961,7 +961,9 @@ public final class TngMoneyPacketHook { } if (!TextUtils.isEmpty(status)) { String s = status.toUpperCase(Locale.US); + // TNG ActivityStatus: ACTIVE/INIT/FINISHED/ENDED/CLOSED/CANCELLED if (s.contains("FINISH") || s.contains("COMPLETE") || s.contains("EXPIRE") + || s.contains("ENDED") || s.contains("CLOSED") || s.contains("CANCEL") || s.contains("DONE") || "TRUE".equals(s)) { return true; } @@ -978,6 +980,9 @@ public final class TngMoneyPacketHook { snapshot.activityStatus)) { return true; } + if (isExpireTimePassed(snapshot.expireTime)) { + return true; + } // 已领金额 ≈ 总额 Double claimedAmt = parseMoneyDouble(snapshot.claimedAmountText); Double totalAmt = parseMoneyDouble(snapshot.totalAmount); @@ -998,6 +1003,58 @@ public final class TngMoneyPacketHook { return false; } + /** expireTime 形如 05/08/2026 14:06:56 或 ISO;已过则视为结束 */ + private static boolean isExpireTimePassed(String expireTime) { + long ms = parseIssueTimeMs(expireTime); + return ms > 0 && System.currentTimeMillis() >= ms; + } + + private static long parseIssueTimeMs(String text) { + if (TextUtils.isEmpty(text)) { + return 0L; + } + String s = text.trim(); + try { + java.util.regex.Matcher m = java.util.regex.Pattern + .compile("^(\\d{2})/(\\d{2})/(\\d{4})(?:\\s+(\\d{2}):(\\d{2})(?::(\\d{2}))?)?") + .matcher(s); + if (m.find()) { + java.util.Calendar c = java.util.Calendar.getInstance(); + c.set(Integer.parseInt(m.group(3)), + Integer.parseInt(m.group(2)) - 1, + Integer.parseInt(m.group(1)), + m.group(4) != null ? Integer.parseInt(m.group(4)) : 23, + m.group(5) != null ? Integer.parseInt(m.group(5)) : 59, + m.group(6) != null ? Integer.parseInt(m.group(6)) : 59); + c.set(java.util.Calendar.MILLISECOND, 999); + return c.getTimeInMillis(); + } + if (s.matches("^\\d{13}$")) { + return Long.parseLong(s); + } + if (s.matches("^\\d{10}$")) { + return Long.parseLong(s) * 1000L; + } + // 2026-08-05T14:06:56 / 2026-08-05 14:06:56 + java.util.regex.Matcher iso = java.util.regex.Pattern + .compile("^(\\d{4})-(\\d{2})-(\\d{2})[ T](\\d{2}):(\\d{2})(?::(\\d{2}))?") + .matcher(s); + if (iso.find()) { + java.util.Calendar c = java.util.Calendar.getInstance(); + c.set(Integer.parseInt(iso.group(1)), + Integer.parseInt(iso.group(2)) - 1, + Integer.parseInt(iso.group(3)), + Integer.parseInt(iso.group(4)), + Integer.parseInt(iso.group(5)), + iso.group(6) != null ? Integer.parseInt(iso.group(6)) : 0); + c.set(java.util.Calendar.MILLISECOND, 0); + return c.getTimeInMillis(); + } + } catch (Throwable ignored) { + } + return 0L; + } + private static String buildCountHint(String claimed, String total) { if (TextUtils.isEmpty(claimed) && TextUtils.isEmpty(total)) { return null; @@ -2034,6 +2091,11 @@ public final class TngMoneyPacketHook { stringField(result, "activityStatus", "getActivityStatus"), boolFieldText(summary, "isFinished", "isFinished"), boolFieldText(result, "isFinished", "isFinished")); + snapshot.expireTime = firstNonEmpty( + stringField(summary, "expireTime", "getExpireTime"), + stringField(result, "expireTime", "getExpireTime"), + stringField(summary, "expiryTime", "getExpiryTime"), + stringField(result, "expiryTime", "getExpiryTime")); if (TextUtils.isEmpty(snapshot.claimedAmountText)) { try { Object money = XposedHelpers.callMethod(summary, "getClaimedAmount"); @@ -2649,6 +2711,7 @@ public final class TngMoneyPacketHook { String claimedCount; String totalCount; String activityStatus; + String expireTime; String claimedAmountText; boolean finished; final List claims = new ArrayList<>(); @@ -2713,6 +2776,21 @@ public final class TngMoneyPacketHook { if (!TextUtils.isEmpty(issueTime)) { sb.append(" | issued=").append(sanitizeMeta(issueTime)); } + if (!TextUtils.isEmpty(expireTime)) { + sb.append(" | expire=").append(sanitizeMeta(expireTime)); + } + if (!TextUtils.isEmpty(activityStatus)) { + sb.append(" | status=").append(sanitizeMeta(activityStatus)); + } + String claimed = claimedCount; + String totalCnt = totalCount; + if (TextUtils.isEmpty(claimed) && claims != null) { + claimed = String.valueOf(claims.size()); + } + if (!TextUtils.isEmpty(claimed) || !TextUtils.isEmpty(totalCnt)) { + sb.append(" | counts=").append(sanitizeMeta(nullToEmpty(claimed))) + .append('/').append(sanitizeMeta(nullToEmpty(totalCnt))); + } if (finished) { sb.append(" | done=1"); }