Merge branch 'main' into feature/distribution-system

合入 main 的推广追踪(adtracking)模块,并解决与分销业务的 8 处冲突。

冲突解决原则:两边的字段、菜单与路由全部保留,不删任何一方。

后端:
- model/order.go:保留分销字段注释块,追加 SourceTrackCode
- model/user.go:追加 RegisterTrackCode;TokenVersion 默认值保持 1
  (与本库 users.token_version 列默认值一致)
- auth/repository.go:两处建号结构体同时写入 RegisterTrackCode 与 TokenVersion
- order/dto.go:OrderDTO 同时暴露 distribution_site_id 与 source_track_code;
  CreateRequest 同时接受 distribution_site_code 与 track_code
- order/presenter.go:同时映射 DistributionSiteID 与 SourceTrackCode

前端:
- orders/api/orders.ts:createOrder 同时携带 distribution_site_code 与 track_code
- layouts/AdminLayout.vue:分销管理菜单组与推广追踪菜单组并列保留
- router/adminRoutes.ts:分销 4 条路由与 tracking-links 路由并列保留

迁移:feature 侧 000064-000067 已在前一提交顺延为 000065-000068,
为 main 的 000064_ad_tracking 让号,合并后编号 000063→000068 连续无重复。
This commit is contained in:
yml2213
2026-09-10 13:06:18 +08:00
28 changed files with 2461 additions and 55 deletions
@@ -0,0 +1,110 @@
import { apiClient } from '@/shared/api/client'
import type { ApiResponse } from '@/shared/types/types'
export interface TrackingLinkItem {
id: number
name: string
code: string
channel_type: string
target_url: string
short_url: string
status: 'active' | 'disabled'
cost_cent: number
notes: string
created_by: number
created_at: string
updated_at: string
click_pv: number
click_uv: number
click_ip: number
register_count: number
order_count: number
paid_order_count: number
paid_gmv_cent: number
conversion_rate: number
}
export interface TrackingOverviewStats {
total_links: number
active_links: number
total_pv: number
total_uv: number
total_ip: number
total_registers: number
total_orders: number
total_paid_orders: number
total_paid_gmv_cent: number
today_pv: number
today_uv: number
today_registers: number
today_orders: number
today_paid_gmv_cent: number
}
export interface ListTrackingLinksParams {
keyword?: string
channel_type?: string
status?: string
page?: number
page_size?: number
}
export interface CreateTrackingLinkPayload {
name: string
code: string
channel_type?: string
target_url?: string
cost_cent?: number
notes?: string
}
export interface UpdateTrackingLinkPayload {
name: string
channel_type?: string
target_url?: string
cost_cent?: number
notes?: string
}
export async function fetchTrackingOverview(): Promise<TrackingOverviewStats> {
const { data } = await apiClient.get<ApiResponse<TrackingOverviewStats>>(
'/admin/tracking-links/overview'
)
return data.data
}
export async function fetchTrackingLinks(
params: ListTrackingLinksParams
): Promise<{ items: TrackingLinkItem[]; total: number }> {
const { data } = await apiClient.get<ApiResponse<{ items: TrackingLinkItem[]; total: number }>>(
'/admin/tracking-links',
{ params }
)
return data.data
}
export async function createTrackingLink(
payload: CreateTrackingLinkPayload
): Promise<TrackingLinkItem> {
const { data } = await apiClient.post<ApiResponse<TrackingLinkItem>>(
'/admin/tracking-links',
payload
)
return data.data
}
export async function updateTrackingLink(
id: number,
payload: UpdateTrackingLinkPayload
): Promise<void> {
await apiClient.put(`/admin/tracking-links/${id}`, payload)
}
export async function updateTrackingLinkStatus(
id: number,
status: 'active' | 'disabled'
): Promise<void> {
await apiClient.put(`/admin/tracking-links/${id}/status`, { status })
}
@@ -112,6 +112,7 @@ const resourceLabels: Record<string, string> = {
role: '角色管理',
system_config: '系统配置',
audit_log: '审计日志',
ad_tracking: '推广追踪',
}
</script>
File diff suppressed because it is too large Load Diff
+13 -6
View File
@@ -1,6 +1,7 @@
import { apiClient } from '@/shared/api/client'
import type { ApiResponse } from '@/shared/types/types'
import type { RealnameStatusValue, RiskStatus, UserStatus } from '@/shared/types/status'
import { getTrackCode } from '@/shared/utils/trackCode'
export interface AuthUser {
id: number
@@ -51,7 +52,12 @@ export async function sendSmsCode(phone: string, captchaId: string, captchaCode:
}
export async function loginWithSms(phone: string, code: string) {
const { data } = await apiClient.post<ApiResponse<LoginData>>('/auth/sms/login', { phone, code })
const trackCode = getTrackCode()
const payload: Record<string, unknown> = { phone, code }
if (trackCode) {
payload.track_code = trackCode
}
const { data } = await apiClient.post<ApiResponse<LoginData>>('/auth/sms/login', payload)
return data.data
}
@@ -64,11 +70,12 @@ export async function loginWithPassword(phone: string, password: string) {
}
export async function registerWithPassword(phone: string, code: string, password: string) {
const { data } = await apiClient.post<ApiResponse<LoginData>>('/auth/password/register', {
phone,
code,
password,
})
const trackCode = getTrackCode()
const payload: Record<string, unknown> = { phone, code, password }
if (trackCode) {
payload.track_code = trackCode
}
const { data } = await apiClient.post<ApiResponse<LoginData>>('/auth/password/register', payload)
return data.data
}
+10 -1
View File
@@ -3,6 +3,7 @@ import type { ApiResponse, PaginatedResult } from '@/shared/types/types'
import type { HandoffStatus, OrderStatus, SettlementStatus } from '@/shared/types/status'
import { yuanToCent } from '@/shared/utils/money'
import { getEffectiveSiteCode } from '@/stores/distributionSite'
import { getTrackCode } from '@/shared/utils/trackCode'
export interface Order {
id: number
@@ -361,10 +362,18 @@ export async function fetchOrder(id: string | number, distributionSiteCode?: str
export async function createOrder(listingId: number, distributionSiteCode?: string) {
const siteCode = distributionSiteCode || getEffectiveSiteCode() || undefined
const payload: { listing_id: number; distribution_site_code?: string } = { listing_id: listingId }
const trackCode = getTrackCode()
const payload: {
listing_id: number
distribution_site_code?: string
track_code?: string
} = { listing_id: listingId }
if (siteCode) {
payload.distribution_site_code = siteCode
}
if (trackCode) {
payload.track_code = trackCode
}
const { data } = await apiClient.post<ApiResponse<Order>>('/orders', payload)
return data.data
}
+15
View File
@@ -16,6 +16,8 @@ import {
Operation,
Picture,
Present,
Promotion,
Link,
ScaleToOriginal,
Service,
Shop,
@@ -163,6 +165,19 @@ const allNavGroups: NavGroup[] = [
},
],
},
{
label: '推广追踪',
index: 'ad-tracking',
icon: Promotion,
children: [
{
label: '推广链接管理',
to: adminPath('tracking-links'),
icon: Link,
permission: 'ad_tracking:manage',
},
],
},
{
label: '客服与风控',
index: 'support-risk',
+6
View File
@@ -240,4 +240,10 @@ export const adminRoutes: RouteRecordRaw[] = [
component: () => import('@/features/admin/views/AdminDistributionSettlementsView.vue'),
meta: adminMeta,
},
{
path: adminPath('tracking-links'),
name: 'admin-tracking-links',
component: () => import('@/features/admin/views/AdminTrackingLinksView.vue'),
meta: adminMeta,
},
]
+5
View File
@@ -10,6 +10,7 @@ import { isMobileHost, isMobileDevice, getMobilePath, getPcPath } from './mobile
import { mobileRoutes } from './mobileRoutes'
import { publicRoutes } from './publicRoutes'
import { sellerRoutes } from './sellerRoutes'
import { saveTrackCode } from '@/shared/utils/trackCode'
const router = createRouter({
history: createWebHistory(),
@@ -27,6 +28,10 @@ const router = createRouter({
})
router.beforeEach(async to => {
if (to.query.track_code) {
saveTrackCode(String(to.query.track_code))
}
const isMobile = isMobileHost() || isMobileDevice()
if (isMobile) {
+53
View File
@@ -0,0 +1,53 @@
const TRACK_CODE_KEY = 'hfb_track_code'
const TRACK_CODE_TTL_MS = 30 * 24 * 60 * 60 * 1000 // 30 天有效
const TRACK_CODE_PATTERN = /^[a-zA-Z0-9_-]{2,64}$/
interface StoredTrackCode {
code: string
timestamp: number
}
function sanitizeTrackCode(code: string): string {
const cleanCode = code.trim()
if (!TRACK_CODE_PATTERN.test(cleanCode)) return ''
return cleanCode
}
export function saveTrackCode(code: string): void {
if (!code) return
const cleanCode = sanitizeTrackCode(code)
if (!cleanCode) return
try {
const item: StoredTrackCode = {
code: cleanCode,
timestamp: Date.now(),
}
localStorage.setItem(TRACK_CODE_KEY, JSON.stringify(item))
} catch {
// 忽略 localStorage 存取异常
}
}
export function getTrackCode(): string {
try {
const raw = localStorage.getItem(TRACK_CODE_KEY)
if (!raw) return ''
const item: StoredTrackCode = JSON.parse(raw)
if (!item || !item.code) return ''
if (Date.now() - item.timestamp > TRACK_CODE_TTL_MS) {
localStorage.removeItem(TRACK_CODE_KEY)
return ''
}
return sanitizeTrackCode(item.code)
} catch {
return ''
}
}
export function clearTrackCode(): void {
try {
localStorage.removeItem(TRACK_CODE_KEY)
} catch {
// 忽略
}
}