hejianhao
2026-05-27 36019cf212229fcf833774fc97489284d9cc2fb2
feat(robotaxi): 新增用户位置快照, 优化埋点和站点状态管理

- 新增用户当前位置快照回调机制,在定位完成后同步位置信息供全页面使用
- 重构埋点工具函数,新增结构化参数序列化、页面上下文自动采集能力
- 新增下单支付流程埋点与修改上下车点按钮的埋点上报逻辑
- 新增附近站点状态管理,完善各业务hook间的数据传递链路
7个文件已修改
314 ■■■■■ 已修改文件
src/pages/robotaxi/home/hooks/use-location-flow.ts 50 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/robotaxi/home/hooks/use-map-state.ts 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/robotaxi/home/hooks/use-navigation.ts 65 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/robotaxi/home/hooks/use-origin-action.ts 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/robotaxi/home/index.tsx 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/use-order-submit.ts 67 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/utils/Robotaxi-public-method.ts 93 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/robotaxi/home/hooks/use-location-flow.ts
@@ -18,6 +18,13 @@
  cityState: any;
  isLoggedIn: boolean;
  onLocationSettled: (lat: number, lng: number, isInside: boolean) => void;
  onUserLocationResolved?: (location: {
    lat: number;
    lng: number;
    poi_id: string;
    poi_name: string;
    city: string;
  }) => void;
};
type GetLocationInfoOptions = {
@@ -32,7 +39,35 @@
/**
 * 定位流程编排:不持有状态,仅组合 map/site/city 状态 hook 完成定位主链路。
 */
export default function useLocationFlow({ mapState, siteState, cityState, isLoggedIn, onLocationSettled }: Deps) {
export default function useLocationFlow({
  mapState,
  siteState,
  cityState,
  isLoggedIn,
  onLocationSettled,
  onUserLocationResolved
}: Deps) {
  /**
   * 同步用户当前位置快照,供页面埋点直接复用。
   */
  const syncUserCurrentLocation = (payload: {
    latitude: number;
    longitude: number;
    poiId?: any;
    poiName?: any;
    city?: any;
  }) => {
    if (typeof payload.latitude !== 'number' || typeof payload.longitude !== 'number') {
      return;
    }
    onUserLocationResolved?.({
      lat: payload.latitude,
      lng: payload.longitude,
      poi_id: String(payload.poiId || ''),
      poi_name: String(payload.poiName || ''),
      city: String(payload.city || '')
    });
  };
  /**
   * 解析当前城市下仍然有效的手动确认上车点。
   */
@@ -101,6 +136,12 @@
        latitude: res.latitude,
        longitude: res.longitude
      });
      syncUserCurrentLocation({
        latitude: res.latitude,
        longitude: res.longitude,
        poiName: cityName,
        city: cityName
      });
    } catch (error) {
      mapState.setLatitude(DEFAULT_LATITUDE);
      mapState.setLongitude(DEFAULT_LONGITUDE);
@@ -154,6 +195,13 @@
            false,
            forceCityRefresh
          );
          syncUserCurrentLocation({
            latitude: res.latitude,
            longitude: res.longitude,
            poiId: addressData?.id ?? addressData?.poiId,
            poiName: addressData?.name ?? addressData?.desc,
            city: cityInfo?.name || cachedCity?.name || ''
          });
          if (cityInfo?.code) {
            const { isInside, newMarkers, sites } = await mapState.updateMapData(res.latitude, res.longitude, cityInfo.code);
            const lockedManualStartSite = forceNearestSite ? null : resolveLockedManualStartSite(cityInfo.code);
src/pages/robotaxi/home/hooks/use-map-state.ts
@@ -37,6 +37,7 @@
  const [mapScale, setMapScale] = useState(14);
  const [isOperatingArea, setIsOperatingArea] = useState(true);
  const [electronicAreaId, setElectronicAreaId] = useState('');
  const [nearbySites, setNearbySites] = useState<any[]>([]);
  const [isLocationAuthDenied, setIsLocationAuthDenied] = useState(false);
  const [nearbyLoading, setNearbyLoading] = useState(false);
@@ -105,6 +106,7 @@
      const result = await fetchMapData(lat, lng, code);
      setIsOperatingArea(result.isInside);
      setElectronicAreaId(result.electronicAreaId);
      setNearbySites(Array.isArray(result?.sites) ? result.sites : []);
      if (options?.commitMarkers !== false) {
        scheduleMarkerCommit(result.newMarkers, options?.markerCommitDelayMs || 0);
      }
@@ -143,6 +145,7 @@
    mapScale,
    isOperatingArea,
    electronicAreaId,
    nearbySites,
    isLocationAuthDenied,
    nearbyLoading,
    setLongitude,
@@ -153,6 +156,7 @@
    setMapScale,
    setIsOperatingArea,
    setElectronicAreaId,
    setNearbySites,
    setIsLocationAuthDenied,
    shouldIgnoreRegionChange,
    isReturningToOrigin,
src/pages/robotaxi/home/hooks/use-navigation.ts
@@ -25,6 +25,14 @@
  electronicAreaId: string;
  /** 当前选中站点 */
  currentSite: any;
  /** 首页确认上车点。 */
  preferredStartSite: any;
  /** 用户当前位置快照。 */
  userCurrentLocation: any;
  /** 附近上车站点列表。 */
  nearbyPickupStations: any[];
  /** 推荐目的地站点。 */
  recommendedDropoffStation: any;
  /** 当前选中城市 */
  selectedCity: any;
  /** 地图围栏 */
@@ -52,10 +60,64 @@
  queryPointerByCenter: (lat: number, lng: number) => Promise<void>;
};
/** 修改上车点按钮埋点事件标识。 */
const EDIT_PICKUP_EVENT_ID = 'RT_xcx_Click_RideHome_EditPickup_Button';
/** 修改下车点按钮埋点事件标识。 */
const EDIT_DROPOFF_EVENT_ID = 'RT_xcx_Click_RideHome_EditDropoff_Button';
/**
 * 首页导航与页面操作编排。
 */
export default function useNavigation(deps: Deps) {
  /**
   * 归一化当前位置埋点字段。
   */
  const createUserCurrentLocationPayload = () => ({
    lat: Number(deps.userCurrentLocation?.lat || 0),
    lng: Number(deps.userCurrentLocation?.lng || 0),
    poi_id: String(deps.userCurrentLocation?.poi_id || ''),
    poi_name: String(deps.userCurrentLocation?.poi_name || ''),
    city: String(deps.userCurrentLocation?.city || deps.selectedCity?.name || '')
  });
  /**
   * 归一化推荐上车站点埋点字段。
   */
  const createRecommendedPickupStationPayload = () => {
    const recommendedPickupStation = deps.currentSite?.id
      ? deps.currentSite
      : (deps.preferredStartSite || resolveHomeStartSiteDisplaySite() || {});
    return {
      poi_id: String(recommendedPickupStation?.id || ''),
      poi_name: String(recommendedPickupStation?.name || ''),
      distance: Number(recommendedPickupStation?.distance || 0),
      city: String(recommendedPickupStation?.city || deps.selectedCity?.name || '')
    };
  };
  /**
   * 归一化推荐目的地站点埋点字段。
   */
  const createRecommendedDropoffStationPayload = () => ({
    poi_id: String(deps.recommendedDropoffStation?.id || ''),
    poi_name: String(deps.recommendedDropoffStation?.name || ''),
    city: String(deps.recommendedDropoffStation?.city || deps.selectedCity?.name || '')
  });
  /**
   * 上报首页修改上下车点埋点。
   */
  const reportEditStationEvent = (type: string) => {
    const eventId = type === 'end' ? EDIT_DROPOFF_EVENT_ID : EDIT_PICKUP_EVENT_ID;
    reportEvent(eventId, {
      user_cur_loc: createUserCurrentLocationPayload(),
      recommended_pickup_station: createRecommendedPickupStationPayload(),
      nearby_pickup_stations: Array.isArray(deps.nearbyPickupStations) ? deps.nearbyPickupStations : [],
      recommended_dropoff_station: createRecommendedDropoffStationPayload()
    });
  };
  /**
   * 网络可用性检查。
   */
@@ -124,6 +186,7 @@
    }
    if (isToMapStart) {
      Taro.setStorageSync('startSite', deps.currentSite);
      reportEditStationEvent(type);
      Taro.navigateTo({
        url: '/robotaxi/pages/select-map-site/index?pageType=' + type,
        success: () => reportEvent('RT_xcx_search_click', { element_type: 'start' })
@@ -138,6 +201,7 @@
        return;
      }
      Taro.setStorageSync('startSite', effectiveStartSite);
      reportEditStationEvent(type);
      Taro.navigateTo({
        url: `/robotaxi/pages/site-search/index?pageType=${type}&electronicAreaId=${effectiveStartSite.electronicAreaId}&longitude=${effectiveStartSite.longitude}&latitude=${effectiveStartSite.latitude}`,
        success: () => {
@@ -146,6 +210,7 @@
      });
      return;
    }
    reportEditStationEvent(type);
    Taro.navigateTo({
      url: `/robotaxi/pages/site-search/index?pageType=${type}&electronicAreaId=${deps.electronicAreaId}&longitude=${deps.currentSite.longitude}&latitude=${deps.currentSite.latitude}`,
      success: () => {
src/pages/robotaxi/home/hooks/use-origin-action.ts
@@ -17,6 +17,14 @@
  ensureSelectedCityReady: (lng: number, lat: number, skipCityEffect?: boolean, forceRefresh?: boolean) => Promise<any>;
  /** 最近城市编码引用 */
  lastSelectedCityCodeRef: React.MutableRefObject<string>;
  /** 同步用户当前位置快照。 */
  onUserLocationResolved?: (location: {
    lat: number;
    lng: number;
    poi_id: string;
    poi_name: string;
    city: string;
  }) => void;
};
/**
@@ -58,6 +66,13 @@
      deps.setLatitude(loc.latitude);
      deps.setLongitude(loc.longitude);
      const cityInfo = await deps.ensureSelectedCityReady(loc.longitude, loc.latitude, false, true);
      deps.onUserLocationResolved?.({
        lat: loc.latitude,
        lng: loc.longitude,
        poi_id: '',
        poi_name: '',
        city: String(cityInfo?.name || Taro.getStorageSync('selectedCity')?.name || '')
      });
      if (cityInfo?.code) {
        deps.lastSelectedCityCodeRef.current = cityInfo.code;
      }
src/pages/robotaxi/home/index.tsx
@@ -52,6 +52,13 @@
} from './hooks/use-home-start-site-display';
type HomeTripStatus = TripStatus | 0;
type UserCurrentLocation = {
  lat: number;
  lng: number;
  poi_id: string;
  poi_name: string;
  city: string;
};
/**
 * Robotaxi 首页页面容器:仅负责组合领域 hook 与布局组件。
@@ -66,6 +73,7 @@
  const [overlayAnchorBottom, setOverlayAnchorBottom] = useState<number | null>(null); // 地图浮层锚点
  const [temperatureUnit, setTemperatureUnit] = useState(DEFAULT_TEMPERATURE_UNIT); // 温度单位配置
  const [preferredStartSite, setPreferredStartSite] = useState<any>(null); // 首页确认上车点
  const [userCurrentLocation, setUserCurrentLocation] = useState<UserCurrentLocation | null>(null); // 用户当前位置快照
  const mapState = useMapState(); // 地图领域状态
  const siteState = useSiteState(); // 站点吸附状态
@@ -79,6 +87,7 @@
    mapScale,
    isOperatingArea,
    electronicAreaId,
    nearbySites,
    isLocationAuthDenied,
    setLongitude,
    setLatitude,
@@ -154,7 +163,8 @@
    siteState,
    cityState,
    isLoggedIn,
    onLocationSettled: syncStationFenceVisibility
    onLocationSettled: syncStationFenceVisibility,
    onUserLocationResolved: setUserCurrentLocation
  });
  const { toOrigin } = useOriginAction({
    isLocationAuthDenied,
@@ -164,7 +174,8 @@
    setLatitude,
    setLongitude,
    ensureSelectedCityReady,
    lastSelectedCityCodeRef
    lastSelectedCityCodeRef,
    onUserLocationResolved: setUserCurrentLocation
  });
  const bootstrap = useHomeBootstrap({
    wsHost: _ROBOTAXI_WS_HOST,
@@ -201,6 +212,7 @@
    toOrigin
  });
  const {
    capsuleSite,
    quickCapsuleText,
    showQuickCapsule,
    refreshQuickCapsule,
@@ -270,6 +282,10 @@
    isLocationAuthDenied,
    electronicAreaId,
    currentSite,
    preferredStartSite,
    userCurrentLocation,
    nearbyPickupStations: nearbySites,
    recommendedDropoffStation: capsuleSite,
    selectedCity,
    polygons,
    fencePolylines,
src/robotaxi/pages/begin-route/hooks/use-order-submit.ts
@@ -40,6 +40,41 @@
/** 协议接口约定:true=已同意,false=未同意。 */
const resolveAgreementStatus = (result: any) => result === true;
/** 下单发起埋点事件标识。 */
const PAY_START_EVENT_ID = 'RT_xcx_pay_start';
/** 下单结果埋点事件标识。 */
const PAY_RESULT_EVENT_ID = 'RT_xcx_pay_result';
/** 下单结果埋点状态。 */
type PayResultStatus =
  | 'success'
  | 'user_cancel'
  | 'network_error'
  | 'insufficient_balance'
  | 'backend_timeout';
/** 推断下单结果埋点状态。 */
const resolvePayResultStatus = (value: any): PayResultStatus => {
  const normalizedMessage = normalizeText(value);
  if (!normalizedMessage) {
    return 'network_error';
  }
  if (normalizedMessage.includes('余额不足')) {
    return 'insufficient_balance';
  }
  if (normalizedMessage.includes('取消')) {
    return 'user_cancel';
  }
  if (normalizedMessage.includes('超时')) {
    return 'backend_timeout';
  }
  if (normalizedMessage.includes('网络')) {
    return 'network_error';
  }
  return 'network_error';
};
/**
 * 管理下单前校验与提交动作。
 */
@@ -49,6 +84,32 @@
   */
  const stopCallSubmitting = () => {
    deps.setCallSubmitting(false);
  };
  /**
   * 构造支付埋点公共字段。
   */
  const createPayEventPayload = (orderId = '') => ({
      order_id: String(orderId || '').trim(),
      amount: Number(deps.fareEstimate?.finalFare || 0),
      timestamp: Date.now()
    });
  /**
   * 下单发起后上报支付起始埋点。
   */
  const reportPayStartEvent = () => {
    reportEvent(PAY_START_EVENT_ID, createPayEventPayload());
  };
  /**
   * 接口返回后上报支付结果埋点。
   */
  const reportPayResultEvent = (payStatus: PayResultStatus, orderId = '') => {
    reportEvent(PAY_RESULT_EVENT_ID, {
      ...createPayEventPayload(orderId),
      pay_status: payStatus
    });
  };
  /**
@@ -112,11 +173,13 @@
      || isNotAgreedMessage(response?.data)
      || isNotAgreedMessage(response?.msg)
    ) {
      reportPayResultEvent(resolvePayResultStatus(response?.msg || response?.data || response));
      deps.handleOpenPrivacyPopup();
      return null;
    }
    if (typeof response === 'string') {
      reportPayResultEvent(resolvePayResultStatus(response));
      Taro.showToast({ title: response, icon: 'none', mask: true });
      setTimeout(() => {
        Taro.reLaunch({ url: HOME_PAGE_URL });
@@ -125,10 +188,12 @@
    }
    if (!response?.orderId) {
      reportPayResultEvent(resolvePayResultStatus(response?.msg || response?.data || response));
      Taro.showToast({ title: '下单失败,请重试', icon: 'none' });
      return null;
    }
    reportPayResultEvent('success', response.orderId);
    Taro.removeStorageSync('selectedCouponId');
    Taro.setStorageSync('orderId', response.orderId);
    return { orderId: response.orderId };
@@ -257,6 +322,7 @@
      return;
    }
    deps.setCallSubmitting(true);
    reportPayStartEvent();
    const authReady = await checkCallAuth();
    if (!authReady) {
@@ -306,6 +372,7 @@
      deps.bindSocketListeners();
      void subscribeAfterOrderSuccess(submitResult.orderId);
    } catch (error) {
      reportPayResultEvent(resolvePayResultStatus((error as any)?.msg || (error as any)?.message || error));
      stopCallSubmitting();
    }
  };
src/utils/Robotaxi-public-method.ts
@@ -1083,11 +1083,80 @@
  }
}
// mvp埋点
export const reportEvent = async (eventId: string, para: any) => {
/** 埋点操作类型。 */
type ReportEventType = 'pageView' | 'track';
/** 默认埋点操作类型。 */
const DEFAULT_REPORT_EVENT_TYPE: ReportEventType = 'track';
/** 页面路径兜底值。 */
const DEFAULT_PAGE_PATH = '/';
/** 来源页路径兜底值。 */
const DEFAULT_REFERRER_PATH = '';
/** 标准化埋点页面路径。 */
const normalizeReportPagePath = (route: any, fallbackPath: string) => {
  const normalizedRoute = String(route || '').trim();
  if (!normalizedRoute) {
    return fallbackPath;
  }
  return normalizedRoute.startsWith('/') ? normalizedRoute : `/${normalizedRoute}`;
};
/** 读取当前页与来源页路径。 */
const getReportPageContext = () => {
  const pages = Taro.getCurrentPages();
  const currentPage = pages[pages.length - 1];
  const referrerPage = pages[pages.length - 2];
  return {
    pagePath: normalizeReportPagePath(currentPage?.route, DEFAULT_PAGE_PATH),
    referrerPath: normalizeReportPagePath(referrerPage?.route, DEFAULT_REFERRER_PATH)
  };
};
/** 判断埋点字段是否为需要自动 JSON 化的结构化数据。 */
const isStructuredReportValue = (value: any) => {
  if (Array.isArray(value)) {
    return true;
  }
  return Object.prototype.toString.call(value) === '[object Object]';
};
/** 序列化单个埋点字段,避免对象被底层隐式转成 [object Object]。 */
const serializeReportValue = (value: any) => {
  if (!isStructuredReportValue(value)) {
    return value;
  }
  try {
    return JSON.stringify(value);
  } catch (error) {
    return Array.isArray(value) ? '[]' : '{}';
  }
};
/** 归一化自定义埋点参数:仅自动 JSON 化顶层对象/数组字段。 */
const normalizeReportParams = (para: any) => {
  if (!para || Object.prototype.toString.call(para) !== '[object Object]') {
    return {};
  }
  return Object.keys(para).reduce((result: Record<string, any>, key: string) => {
    result[key] = serializeReportValue(para[key]);
    return result;
  }, {});
};
// 埋点
export const reportEvent = async (
  eventId: string,
  para: any,
  type: ReportEventType = DEFAULT_REPORT_EVENT_TYPE
) => {
  let appVersion = '';
  const $app: any = Taro.getApp().$app;
  const { openId } = await $app.getIdData() || {};
  const { pagePath, referrerPath } = getReportPageContext();
  const normalizedPara = normalizeReportParams(para);
  try {
    const accountInfo = Taro.getAccountInfoSync();
    appVersion = accountInfo?.miniProgram?.version || accountInfo?.miniProgram?.envVersion || '';
@@ -1099,14 +1168,18 @@
    $app.xpReportSend({
      eventId,
      para: {
        ...para,
        user_id: uid || '-1',
        masked_device_id: openId,
        event_time: Date.now(),
        city: Taro.getStorageSync('selectedCity').name,
        orderId: Taro.getStorageSync('orderId') || '',
        os: Taro.getSystemInfoSync().system,
        app_ver: appVersion,
        ...normalizedPara,  // 自定义参数
        page_path: pagePath,  // 页面路径
        referrer_path: referrerPath,  // 来源页路径
        type,  // 埋点操作类型
        user_id: uid || '-1',  // 用户ID
        masked_device_id: openId,  // 设备ID
        event_time: Date.now(),  // 事件时间
        city: Taro.getStorageSync('selectedCity').name || '',  // 城市
        orderId: Taro.getStorageSync('orderId') || '',  // 订单ID
        os: Taro.getSystemInfoSync().system || '',  // 操作系统
        app_ver: appVersion,  // 微信小程序版本
        session_id: '',  // 会话ID
      },
    });
  });