修改百度地图AK

This commit is contained in:
xuqiuyun
2025-11-04 09:38:19 +08:00
parent 4b6d14a6ec
commit eacb0453dd
52 changed files with 3865 additions and 65 deletions

View File

@@ -1,9 +1,10 @@
package com.aiotagro.cattletrade.business.dto;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import java.util.Date;
/**
* 运送清单编辑DTO
@@ -45,4 +46,16 @@ public class DeliveryEditDto {
private String endLat;
private String endLon;
/**
* 预计出发时间
*/
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date estimatedDepartureTime;
/**
* 预计到达时间
*/
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date estimatedArrivalTime;
}

View File

@@ -165,6 +165,13 @@ public class Delivery implements Serializable {
@TableField("end_lon")
private String endLon;
/**
* 预计出发时间
*/
@TableField("estimated_departure_time")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date estimatedDepartureTime;
/**
* 预计送达时间
*/

View File

@@ -197,10 +197,16 @@ public class IotDeviceLogSyncService {
log.setYWalkSteps(device.getSameDaySteps().longValue());
}
if (device.getLatitude() != null) {
log.setLatitude(device.getLatitude());
String lat = sanitizeCoordinate(device.getLatitude());
if (lat != null) {
log.setLatitude(lat);
}
}
if (device.getLongitude() != null) {
log.setLongitude(device.getLongitude());
String lng = sanitizeCoordinate(device.getLongitude());
if (lng != null) {
log.setLongitude(lng);
}
}
// 设置时间字段
@@ -236,10 +242,16 @@ public class IotDeviceLogSyncService {
log.setYWalkSteps(device.getSameDaySteps().longValue());
}
if (device.getLatitude() != null) {
log.setLatitude(device.getLatitude());
String lat = sanitizeCoordinate(device.getLatitude());
if (lat != null) {
log.setLatitude(lat);
}
}
if (device.getLongitude() != null) {
log.setLongitude(device.getLongitude());
String lng = sanitizeCoordinate(device.getLongitude());
if (lng != null) {
log.setLongitude(lng);
}
}
// 设置时间字段
@@ -289,9 +301,10 @@ public class IotDeviceLogSyncService {
if (device.getLatitude() != null) {
// latitude是String类型直接使用
String latStr = device.getLatitude();
latStr = sanitizeCoordinate(latStr);
// 检查是否为无效坐标
if (!latStr.equals("0") && !latStr.trim().isEmpty() && !latStr.equals("null") && !latStr.equals("NULL")) {
if (latStr != null && !latStr.equals("0") && !latStr.trim().isEmpty() && !latStr.equals("null") && !latStr.equals("NULL")) {
hasValidLatitude = true;
log.setLatitude(latStr);
} else {
@@ -302,9 +315,10 @@ public class IotDeviceLogSyncService {
if (device.getLongitude() != null) {
// longitude是String类型直接使用
String lngStr = device.getLongitude();
lngStr = sanitizeCoordinate(lngStr);
// 检查是否为无效坐标
if (!lngStr.equals("0") && !lngStr.trim().isEmpty() && !lngStr.equals("null") && !lngStr.equals("NULL")) {
if (lngStr != null && !lngStr.equals("0") && !lngStr.trim().isEmpty() && !lngStr.equals("null") && !lngStr.equals("NULL")) {
hasValidLongitude = true;
log.setLongitude(lngStr);
} else {
@@ -328,6 +342,26 @@ public class IotDeviceLogSyncService {
return log;
}
/**
* 规范化经纬度字符串,移除首尾空格,限制最大长度,过滤无效字面量
*/
private String sanitizeCoordinate(String coord) {
if (coord == null) {
return null;
}
String v = coord.trim();
if (v.isEmpty()) {
return null;
}
if ("null".equalsIgnoreCase(v)) {
return null;
}
// 限长,避免超过数据库字段长度
if (v.length() > 32) {
v = v.substring(0, 32);
}
return v;
}
/**
* 获取设备日志同步统计信息
*/

View File

@@ -16,7 +16,6 @@ import com.aiotagro.common.core.utils.DateUtils;
import com.aiotagro.common.core.utils.EnumUtil;
import com.aiotagro.common.core.utils.SecurityUtil;
import com.aiotagro.common.core.utils.StringUtils;
import com.aiotagro.common.core.constant.RoleConstants;
import com.aiotagro.common.core.web.domain.AjaxResult;
import com.aiotagro.common.core.web.domain.PageResultResponse;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
@@ -24,7 +23,6 @@ import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.github.pagehelper.Page;
import com.github.pagehelper.PageHelper;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.logging.log4j.util.Strings;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeanUtils;
@@ -225,8 +223,30 @@ public class DeliveryServiceImpl extends ServiceImpl<DeliveryMapper, Delivery> i
delivery.setDriverName(driverName);
}
// 处理车身照片分割
if (carImg != null && !carImg.isEmpty()) {
// 优先从车辆表获取车身照片(根据车牌号)
// 如果车辆表中没有,再从司机信息中获取作为后备
boolean vehiclePhotoSet = false;
if (delivery.getLicensePlate() != null && StringUtils.isNotEmpty(delivery.getLicensePlate())) {
try {
Vehicle vehicle = vehicleMapper.selectByLicensePlate(delivery.getLicensePlate());
if (vehicle != null) {
String carFrontPhoto = vehicle.getCarFrontPhoto();
String carRearPhoto = vehicle.getCarRearPhoto();
if (StringUtils.isNotEmpty(carFrontPhoto) || StringUtils.isNotEmpty(carRearPhoto)) {
delivery.setCarFrontPhoto(StringUtils.isNotEmpty(carFrontPhoto) ? carFrontPhoto : null);
delivery.setCarBehindPhoto(StringUtils.isNotEmpty(carRearPhoto) ? carRearPhoto : null);
vehiclePhotoSet = true;
logger.debug("从车辆表获取照片: 车牌号={}, 车头照片={}, 车尾照片={}",
delivery.getLicensePlate(), delivery.getCarFrontPhoto(), delivery.getCarBehindPhoto());
}
}
} catch (Exception e) {
logger.error("从车辆表获取照片失败: " + e.getMessage(), e);
}
}
// 如果车辆表中没有照片,从司机信息中获取作为后备
if (!vehiclePhotoSet && carImg != null && !carImg.isEmpty()) {
// 按逗号分割car_img字段分别映射到车头和车尾照片
String[] carImgUrls = carImg.split(",");
@@ -244,14 +264,14 @@ public class DeliveryServiceImpl extends ServiceImpl<DeliveryMapper, Delivery> i
delivery.setCarFrontPhoto(singlePhoto);
delivery.setCarBehindPhoto(singlePhoto);
} else {
// 没有有效URL
delivery.setCarFrontPhoto("");
delivery.setCarBehindPhoto("");
// 没有有效URL设置为null
delivery.setCarFrontPhoto(null);
delivery.setCarBehindPhoto(null);
}
} else {
// 如果司机信息中没有照片,清空delivery表中的照片
delivery.setCarFrontPhoto("");
delivery.setCarBehindPhoto("");
} else if (!vehiclePhotoSet) {
// 如果车辆表和司机信息中没有照片,设置为null
delivery.setCarFrontPhoto(null);
delivery.setCarBehindPhoto(null);
}
}
} catch (Exception e) {
@@ -522,12 +542,14 @@ public class DeliveryServiceImpl extends ServiceImpl<DeliveryMapper, Delivery> i
delivery.setEndLocation(dto.getEndLocation());
delivery.setEndLon(dto.getEndLon());
delivery.setEndLat(dto.getEndLat());
// 预计到达
// 预计出发时间
delivery.setEstimatedDepartureTime(dto.getEstimatedDepartureTime());
// 预计到达时间
delivery.setEstimatedDeliveryTime(dto.getEstimatedArrivalTime());
// 过磅重量
delivery.setEmptyWeight(dto.getEmptyWeight());
delivery.setEntruckWeight(dto.getEntruckWeight());
delivery.setLandingEntruckWeight(dto.getLandingEntruckWeight());
// 过磅重量将空字符串转换为null避免数据库DECIMAL字段类型错误
delivery.setEmptyWeight(StringUtils.isNotEmpty(dto.getEmptyWeight()) ? dto.getEmptyWeight() : null);
delivery.setEntruckWeight(StringUtils.isNotEmpty(dto.getEntruckWeight()) ? dto.getEntruckWeight() : null);
delivery.setLandingEntruckWeight(StringUtils.isNotEmpty(dto.getLandingEntruckWeight()) ? dto.getLandingEntruckWeight() : null);
// 照片
delivery.setQuarantineTickeyUrl(dto.getQuarantineTickeyUrl());
delivery.setPoundListImg(dto.getPoundListImg());
@@ -551,10 +573,13 @@ public class DeliveryServiceImpl extends ServiceImpl<DeliveryMapper, Delivery> i
try {
Vehicle vehicle = vehicleMapper.selectByLicensePlate(dto.getPlateNumber());
if (vehicle != null) {
delivery.setCarFrontPhoto(vehicle.getCarFrontPhoto());
delivery.setCarBehindPhoto(vehicle.getCarRearPhoto());
// 将空字符串转换为null避免前端显示问题
String carFrontPhoto = vehicle.getCarFrontPhoto();
String carRearPhoto = vehicle.getCarRearPhoto();
delivery.setCarFrontPhoto(StringUtils.isNotEmpty(carFrontPhoto) ? carFrontPhoto : null);
delivery.setCarBehindPhoto(StringUtils.isNotEmpty(carRearPhoto) ? carRearPhoto : null);
logger.info("设置车辆照片: 车头照片={}, 车尾照片={}",
vehicle.getCarFrontPhoto(), vehicle.getCarRearPhoto());
delivery.getCarFrontPhoto(), delivery.getCarBehindPhoto());
}
} catch (Exception e) {
}
@@ -691,6 +716,14 @@ public class DeliveryServiceImpl extends ServiceImpl<DeliveryMapper, Delivery> i
delivery.setEndLon(dto.getEndLon());
}
// 更新时间字段
if (dto.getEstimatedDepartureTime() != null) {
delivery.setEstimatedDepartureTime(dto.getEstimatedDepartureTime());
}
if (dto.getEstimatedArrivalTime() != null) {
delivery.setEstimatedDeliveryTime(dto.getEstimatedArrivalTime());
}
// 更新其他业务字段
if (dto.getSupplierId() != null) {
delivery.setSupplierId(dto.getSupplierId());
@@ -717,6 +750,29 @@ public class DeliveryServiceImpl extends ServiceImpl<DeliveryMapper, Delivery> i
delivery.setFirmPrice(dto.getFirmPrice());
}
// 根据车牌号从车辆表重新获取车辆照片(编辑时同步更新照片)
// 这样即使车辆表中的照片有更新,也能同步到运送清单
if (delivery.getLicensePlate() != null && StringUtils.isNotEmpty(delivery.getLicensePlate())) {
try {
Vehicle vehicle = vehicleMapper.selectByLicensePlate(delivery.getLicensePlate());
if (vehicle != null) {
// 将空字符串转换为null避免前端显示问题
String carFrontPhoto = vehicle.getCarFrontPhoto();
String carRearPhoto = vehicle.getCarRearPhoto();
delivery.setCarFrontPhoto(StringUtils.isNotEmpty(carFrontPhoto) ? carFrontPhoto : null);
delivery.setCarBehindPhoto(StringUtils.isNotEmpty(carRearPhoto) ? carRearPhoto : null);
logger.info("更新车辆照片: 车牌号={}, 车头照片={}, 车尾照片={}",
delivery.getLicensePlate(), delivery.getCarFrontPhoto(), delivery.getCarBehindPhoto());
} else {
// 如果车辆表中没有该车牌号的记录,清空照片
delivery.setCarFrontPhoto(null);
delivery.setCarBehindPhoto(null);
}
} catch (Exception e) {
logger.error("从车辆表获取照片失败: " + e.getMessage(), e);
}
}
// 保存更新
boolean updated = this.updateById(delivery);
if (updated) {
@@ -920,14 +976,14 @@ public class DeliveryServiceImpl extends ServiceImpl<DeliveryMapper, Delivery> i
deliveryLogVo.setCarFrontPhoto(singlePhoto);
deliveryLogVo.setCarBehindPhoto(singlePhoto);
} else {
// 没有有效URL
deliveryLogVo.setCarFrontPhoto("");
deliveryLogVo.setCarBehindPhoto("");
// 没有有效URL设置为null
deliveryLogVo.setCarFrontPhoto(null);
deliveryLogVo.setCarBehindPhoto(null);
}
} else {
// 如果司机信息中没有照片,清空delivery表中的照片
deliveryLogVo.setCarFrontPhoto("");
deliveryLogVo.setCarBehindPhoto("");
// 如果司机信息中没有照片,设置为null
deliveryLogVo.setCarFrontPhoto(null);
deliveryLogVo.setCarBehindPhoto(null);
}
// 如果车身照片仍然为空,尝试根据司机姓名和车牌号查询其他相关记录
@@ -1348,8 +1404,30 @@ public class DeliveryServiceImpl extends ServiceImpl<DeliveryMapper, Delivery> i
// 注意:车牌号不从司机表获取,车牌号是独立的模块
delivery.setDriverMobile(driverMobile);
// 强制从司机信息中获取最新的车身照片,确保数据一致性
if (carImg != null && !carImg.isEmpty()) {
// 优先从车辆表获取车身照片(根据车牌号)
// 如果车辆表中没有,再从司机信息中获取作为后备
boolean vehiclePhotoSet = false;
if (delivery.getLicensePlate() != null && StringUtils.isNotEmpty(delivery.getLicensePlate())) {
try {
Vehicle vehicle = vehicleMapper.selectByLicensePlate(delivery.getLicensePlate());
if (vehicle != null) {
String carFrontPhoto = vehicle.getCarFrontPhoto();
String carRearPhoto = vehicle.getCarRearPhoto();
if (StringUtils.isNotEmpty(carFrontPhoto) || StringUtils.isNotEmpty(carRearPhoto)) {
delivery.setCarFrontPhoto(StringUtils.isNotEmpty(carFrontPhoto) ? carFrontPhoto : null);
delivery.setCarBehindPhoto(StringUtils.isNotEmpty(carRearPhoto) ? carRearPhoto : null);
vehiclePhotoSet = true;
logger.debug("从车辆表获取照片: 车牌号={}, 车头照片={}, 车尾照片={}",
delivery.getLicensePlate(), delivery.getCarFrontPhoto(), delivery.getCarBehindPhoto());
}
}
} catch (Exception e) {
logger.error("从车辆表获取照片失败: " + e.getMessage(), e);
}
}
// 如果车辆表中没有照片,从司机信息中获取作为后备
if (!vehiclePhotoSet && carImg != null && !carImg.isEmpty()) {
// 按逗号分割car_img字段分别映射到车头和车尾照片
String[] carImgUrls = carImg.split(",");
@@ -1367,14 +1445,14 @@ public class DeliveryServiceImpl extends ServiceImpl<DeliveryMapper, Delivery> i
delivery.setCarFrontPhoto(singlePhoto);
delivery.setCarBehindPhoto(singlePhoto);
} else {
// 没有有效URL
delivery.setCarFrontPhoto("");
delivery.setCarBehindPhoto("");
// 没有有效URL设置为null
delivery.setCarFrontPhoto(null);
delivery.setCarBehindPhoto(null);
}
} else {
// 如果司机信息中没有照片,清空delivery表中的照片
delivery.setCarFrontPhoto("");
delivery.setCarBehindPhoto("");
} else if (!vehiclePhotoSet) {
// 如果车辆表和司机信息中没有照片,设置为null
delivery.setCarFrontPhoto(null);
delivery.setCarBehindPhoto(null);
}
// 已废弃:不再根据司机姓名和车牌号查询相关记录

View File

@@ -11,6 +11,8 @@ import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.github.pagehelper.Page;
import com.github.pagehelper.PageHelper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -75,13 +77,48 @@ public class VehicleServiceImpl extends ServiceImpl<VehicleMapper, Vehicle> impl
vehicle.setCreatedBy(userId);
vehicle.setCreateTime(new Date());
// 插入数据库
int result = vehicleMapper.insert(vehicle);
try {
// 插入数据库
int result = vehicleMapper.insert(vehicle);
if (result > 0) {
return AjaxResult.success("新增车辆成功");
} else {
return AjaxResult.error("新增车辆失败");
if (result > 0) {
return AjaxResult.success("新增车辆成功");
} else {
return AjaxResult.error("新增车辆失败");
}
} catch (DuplicateKeyException e) {
// 捕获重复键异常(车牌号重复)
String errorMessage = e.getMessage();
if (errorMessage != null && errorMessage.contains("license_plate")) {
return AjaxResult.error("车牌号已存在,请使用其他车牌号");
}
return AjaxResult.error("数据重复:" + errorMessage);
} catch (DataIntegrityViolationException e) {
// 捕获数据库约束异常,提供更友好的错误信息
String errorMessage = e.getMessage();
// 记录完整错误信息以便调试
e.printStackTrace();
if (errorMessage != null && errorMessage.contains("record_code") && errorMessage.contains("too long")) {
return AjaxResult.error("牧运通备案码图片URL过长请重新上传或联系管理员调整数据库字段长度。如果已修改数据库字段为TEXT类型请重启应用或清空连接池缓存");
} else if (errorMessage != null && errorMessage.contains("Data too long")) {
return AjaxResult.error("上传的数据内容过长请检查图片URL或联系管理员");
} else if (errorMessage != null && errorMessage.contains("license_plate")) {
return AjaxResult.error("车牌号已存在,请使用其他车牌号");
}
// 返回完整错误信息以便调试
return AjaxResult.error("新增车辆失败:" + errorMessage);
} catch (Exception e) {
// 捕获其他可能的异常
String errorMessage = e.getMessage();
// 记录完整错误信息以便调试
e.printStackTrace();
if (errorMessage != null && errorMessage.contains("record_code") && errorMessage.contains("too long")) {
return AjaxResult.error("牧运通备案码图片URL过长请重新上传或联系管理员调整数据库字段长度。如果已修改数据库字段为TEXT类型请重启应用或清空连接池缓存");
} else if (errorMessage != null && errorMessage.contains("license_plate") && errorMessage.contains("Duplicate")) {
return AjaxResult.error("车牌号已存在,请使用其他车牌号");
}
// 返回完整错误信息以便调试
return AjaxResult.error("新增车辆失败:" + errorMessage);
}
}
@@ -106,13 +143,48 @@ public class VehicleServiceImpl extends ServiceImpl<VehicleMapper, Vehicle> impl
vehicle.setUpdatedBy(userId);
vehicle.setUpdateTime(new Date());
// 更新数据库
int result = vehicleMapper.updateById(vehicle);
try {
// 更新数据库
int result = vehicleMapper.updateById(vehicle);
if (result > 0) {
return AjaxResult.success("更新车辆成功");
} else {
return AjaxResult.error("更新车辆失败");
if (result > 0) {
return AjaxResult.success("更新车辆成功");
} else {
return AjaxResult.error("更新车辆失败");
}
} catch (DuplicateKeyException e) {
// 捕获重复键异常(车牌号重复)
String errorMessage = e.getMessage();
if (errorMessage != null && errorMessage.contains("license_plate")) {
return AjaxResult.error("车牌号已存在,请使用其他车牌号");
}
return AjaxResult.error("数据重复:" + errorMessage);
} catch (DataIntegrityViolationException e) {
// 捕获数据库约束异常,提供更友好的错误信息
String errorMessage = e.getMessage();
// 记录完整错误信息以便调试
e.printStackTrace();
if (errorMessage != null && errorMessage.contains("record_code") && errorMessage.contains("too long")) {
return AjaxResult.error("牧运通备案码图片URL过长请重新上传或联系管理员调整数据库字段长度。如果已修改数据库字段为TEXT类型请重启应用或清空连接池缓存");
} else if (errorMessage != null && errorMessage.contains("Data too long")) {
return AjaxResult.error("上传的数据内容过长请检查图片URL或联系管理员");
} else if (errorMessage != null && errorMessage.contains("license_plate")) {
return AjaxResult.error("车牌号已存在,请使用其他车牌号");
}
// 返回完整错误信息以便调试
return AjaxResult.error("更新车辆失败:" + errorMessage);
} catch (Exception e) {
// 捕获其他可能的异常
String errorMessage = e.getMessage();
// 记录完整错误信息以便调试
e.printStackTrace();
if (errorMessage != null && errorMessage.contains("record_code") && errorMessage.contains("too long")) {
return AjaxResult.error("牧运通备案码图片URL过长请重新上传或联系管理员调整数据库字段长度。如果已修改数据库字段为TEXT类型请重启应用或清空连接池缓存");
} else if (errorMessage != null && errorMessage.contains("license_plate") && errorMessage.contains("Duplicate")) {
return AjaxResult.error("车牌号已存在,请使用其他车牌号");
}
// 返回完整错误信息以便调试
return AjaxResult.error("更新车辆失败:" + errorMessage);
}
}

View File

@@ -0,0 +1,14 @@
-- 扩展耳标与项圈日志表的经纬度字段长度,避免坐标精度过高导致超长
USE cattletrade;
-- jbq_client_log智能耳标日志表
ALTER TABLE jbq_client_log
MODIFY COLUMN latitude VARCHAR(32) COMMENT '纬度',
MODIFY COLUMN longitude VARCHAR(32) COMMENT '经度';
-- xq_client_log项圈日志表
ALTER TABLE xq_client_log
MODIFY COLUMN latitude VARCHAR(32) COMMENT '纬度',
MODIFY COLUMN longitude VARCHAR(32) COMMENT '经度';

View File

@@ -0,0 +1,16 @@
-- 为 delivery 表添加 estimated_departure_time 字段(预计出发时间)
-- 此脚本已手动执行,保留作为参考文档
USE cattletrade;
-- 添加预计出发时间字段(如果字段已存在,此语句会报错,可以忽略)
-- ALTER TABLE delivery
-- ADD COLUMN estimated_departure_time DATETIME COMMENT '预计出发时间'
-- AFTER estimated_delivery_time;
-- 为现有记录设置默认值(使用创建时间作为默认的预计出发时间)
-- 如果字段中已有部分数据,可以执行此更新语句为 NULL 值设置默认值
UPDATE delivery
SET estimated_departure_time = create_time
WHERE estimated_departure_time IS NULL;

View File

@@ -0,0 +1,18 @@
-- 修复 vehicle 表 record_code 字段长度问题
-- 将 record_code 字段从当前长度扩展为 VARCHAR(1000) 以支持较长的图片URL
-- 同时扩展其他图片URL字段以确保一致性
-- 修改 record_code 字段
ALTER TABLE vehicle MODIFY COLUMN record_code VARCHAR(1000) COMMENT '牧运通备案码图片URL';
-- 修改其他图片URL字段如果需要建议也一起修改
ALTER TABLE vehicle MODIFY COLUMN car_front_photo VARCHAR(1000) COMMENT '车头照片URL';
ALTER TABLE vehicle MODIFY COLUMN car_rear_photo VARCHAR(1000) COMMENT '车尾照片URL';
ALTER TABLE vehicle MODIFY COLUMN driving_license_photo VARCHAR(1000) COMMENT '行驶证照片URL';
-- 如果您的数据库不支持修改字段长度,或者需要更大的容量,可以使用 TEXT 类型:
-- ALTER TABLE vehicle MODIFY COLUMN record_code TEXT COMMENT '牧运通备案码图片URL';
-- ALTER TABLE vehicle MODIFY COLUMN car_front_photo TEXT COMMENT '车头照片URL';
-- ALTER TABLE vehicle MODIFY COLUMN car_rear_photo TEXT COMMENT '车尾照片URL';
-- ALTER TABLE vehicle MODIFY COLUMN driving_license_photo TEXT COMMENT '行驶证照片URL';