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:
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
]
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 {
|
||||
// 忽略
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user