界面原型
第一步: 用户在课程信息编辑界面
可以上传课程图片或者修改上传的课程图片
第二步: 请求媒资管理服务
将课程图片上传至分布式文件系统同时在媒资管理数据库保存文件信息,上传成功后返回图片在MinIO中的地址
第三步: 请求内容管理服务
保存课程信息含课程封面对应图片所在的地址
数据模型
MediaFiles
表保存上传的文件信息,文件的Id和file_id
都是文件的md5值,file_path(文件Minio中存储的路径)和url(文件访问地址)
类似,对于视频需要转码
CourseBase
表保存课程信息含课程图片地址
请求响应模型类
请求模型类
上传文件一般需要文件名称、文件内容类型、文件类型(对应数据字典表中的类型)、文件大小、标签、上传人、备注
@Data
@ToString
public class UploadFileParamsDto {
/**
* 文件名称
*/
private String filename;
/**
* 文件content-type(扩展属性)
*/
private String contentType;
/**
* 文件类型(文档,图片,视频)
*/
private String fileType;
/**
* 文件大小
*/
private Long fileSize;
/**
* 标签
*/
private String tags;
/**
* 上传人
*/
private String username;
/**
* 备注
*/
private String remark;
}
# 上传文件
POST {{media_host}}/media/upload/coursefile
Content-Type: multipart/form-data; boundary=WebAppBoundary # 指定上传文件的内容类型
--WebAppBoundary
Content-Disposition: form-data; name="filedata"; filename="1.jpg"# filedata表示上传文件请求参数中的name,1.jpg表示原始文件的名称
Content-Type: application/octet-stream
< d:/develop/upload/1.jpg # 本地文件
响应模型类
: 由于PO类的属性和数据表的字段一一映射不方便修改,所以单独定义一个DTO类直接继承MediaFiles
,如果后期要修改响应结果可以修改响应模型类
/**
* @description 上传普通文件成功响应结果
* @author Mr.M
* @date 2022/9/12 18:49
* @version 1.0
*/
@Data
public class UploadFileResultDto extends MediaFiles {
}
{
"id": "a16da7a132559daf9e1193166b3e7f52",
"companyId": 1232141425,
"companyName": null,
"filename": "1.jpg",
"fileType": "001001",
"tags": "",
"bucket": "/testbucket/2022/09/12/a16da7a132559daf9e1193166b3e7f52.jpg",
"fileId": "a16da7a132559daf9e1193166b3e7f52",
"url": "/testbucket/2022/09/12/a16da7a132559daf9e1193166b3e7f52.jpg",
"timelength": null,
"username": null,
"createDate": "2022-09-12T21:57:18",
"changeDate": null,
"status": "1",
"remark": "",
"auditStatus": null,
"auditMind": null,
"fileSize": 248329
}
配置环境
第一步: 在minio
中创建一个名为mediafiles
的bucket,并将其权限设置为public
,这样我们就可以在浏览器中通过URL直接访问桶内的文件
第二步: 在nacos
的开发环境dev
中的media-service-dev.yam
文件中新增配置
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/xc_media?serverTimezone=UTC&userUnicode=true&useSSL=false
username: root
password: 123456
cloud:
config:
override-none: true
minio:
endpoint: http://127.0.0.1:9000
accessKey: minioadmin
secretKey: minioadmin
bucket:
files: mediafiles
videofiles: video
第三步: 在本地的media-service
工程中添加bootstrap.yml
文件
spring:
application:
name: media-service
cloud:
nacos:
server-addr: 127.0.0.1:8848
discovery:
namespace: ${spring.profiles.active}
group: xuecheng-plus-project
config:
namespace: ${spring.profiles.active}
group: xuecheng-plus-project
file-extension: yaml
refresh-enabled: true
shared-configs:
- data-id: logging-${spring.profiles.active}.yaml
group: xuecheng-plus-common
refresh: true
#profiles默认为dev
profiles:
active: dev
第四步: 在media-service工程中编写config/MinioConfigminio
配置类,根据yaml文件中的minio配置信息创建一个MinioClient对象
并交给容器管理
@Configuration
public class MinioConfig {
@Value("${minio.endpoint}")
private String endpoint;
@Value("${minio.accessKey}")
private String accessKey;
@Value("${minio.secretKey}")
private String secretKey;
@Bean
public MinioClient minioClient() {
return MinioClient.builder().
endpoint(endpoint).
credentials(accessKey, secretKey).
build();
}
}
api工程定义接口
在api接口工程
中定义一个通用的上传文件的接口,可以上传图片或其他文件
/**
* @description 上传文件接口
* @param objectName 对象名称,如果传入objectname则需要按照其指定的目录存储,如果不传默认按年月日目录结构去存储
* @return com.xuecheng.media.model.dto.UploadFileResultDto
*/
@ApiOperation ("上传文件")
@RequestMapping(value = "/upload/coursefile",consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
@ResponseBody
public UploadFileResultDto upload(@RequestPart("filedata") MultipartFile filedata,@RequestParam(value = "objectName",required=false) String objectName) throws IOException {
Long companyId = 1232141425L;
// 准备上传文件的信息
UploadFileParamsDto uploadFileParamsDto = new UploadFileParamsDto();
// 文件名称
uploadFileParamsDto.setFilename(filedata.getOriginalFilename());
// 文件大小
uploadFileParamsDto.setFileSize(filedata.getSize());
// 获取请求参数中上传文件的内容类型
String contentType = upload.getContentType();
if (contentType.contains("image")) {
// 图片
uploadFileParamsDto.setFileType("001001");
} else {
// 其他
uploadFileParamsDto.setFileType("001003");
}
// 创建临时文件
File tempFile = File.createTempFile("minio", "temp");
// 将上传的文件拷贝到临时文件
filedata.transferTo(tempFile);
// 获取File对象在硬盘中对应临时文件的绝对路径
String absolutePath = tempFile.getAbsolutePath();
// 上传文件
UploadFileResultDto uploadFileResultDto = mediaFileService.uploadFile(companyId, uploadFileParamsDto, absolutePath,objectName);
return uploadFileResultDto;
}
service工程定义业务类
上传文件时需要指定上传的机构ID,上传的文件信息,上传源文件的磁盘路径(这里的源文件是服务器接收的临时文件)
getMimeType
: 根据上传文件的扩展名获取文件的媒体类型MimeTypegetDefaultFolderPath
: 获取文件的默认存储目录,遵循年/月/日/文件名
规范addMediaFilesToMinIO
: 将文件上传到MinIOaddMediaFilesToDb
: 将上传的文件信息存储到数据库/**
* 上传文件
* @param companyId 上传文件的机构ID
* @param uploadFileParamsDto 上传的文件信息
* @param localFilePath 上传文件对应的临时文件在服务器的绝对路径
* @param objectname 对象名称,包含存储目录
* @return 文件信息
*/
public UploadFileResultDto uploadFile(Long companyId, UploadFileParamsDto uploadFileParamsDto, String localFilePath,String objectname);
@Autowired
MediaFilesMapper mediaFilesMapper;
@Autowired
MinioClient minioClient;
// 存储普通文件的桶
@Value("${minio.bucket.files}")
private String bucket_files;
// 这里把事务添加到uploadFile上,细粒度比较大
@Transactional
@Override
public UploadFileResultDto uploadFile(Long companyId, UploadFileParamsDto uploadFileParamsDto, String localFilePath,String objectname) {
// 获取临时文件对象的File对象
File file = new File(localFilePath);
if (!file.exists()) {
XueChengPlusException.cast("文件不存在");
}
// 文件名称
String filename = uploadFileParamsDto.getFilename();
// 文件扩展名
String extension = filename.substring(filename.lastIndexOf("."));
// 根据文件扩展名获取文件对应的mimeType
String mimeType = getMimeType(extension);
// 获取文件在Minio中默认的存储目录
String defaultFolderPath = getDefaultFolderPath();
// 根据File对象获取文件对应的md5值
String fileMd5 = getFileMd5(file);
if(StringUtils.isEmpty(objectName)){
// 默认存储方式,存储到minio中的完整对象名,默认目录/文件MD5值.扩展名
objectName = defaultFolderPath + fileMd5 + extension;
}
// 将文件上传到minio
boolean b = addMediaFilesToMinIO(localFilePath, mimeType, bucket_files, objectName);
if(!result){
XueChengPlusException.cast("上传文件失败");
}
// 将文件信息存储到数据库
MediaFiles mediaFiles = addMediaFilesToDb(companyId, fileMd5, uploadFileParamsDto, bucket_files, objectName);
if(mediaFiles==null){
XueChengPlusException.cast("文件上传后保存信息失败");
}
// 准备返回的UploadFileResultDto对象
UploadFileResultDto uploadFileResultDto = new UploadFileResultDto();
BeanUtils.copyProperties(mediaFiles, uploadFileResultDto);
return uploadFileResultDto;
}
在base工程中创建一个根据上传文件的扩展名取出mimeType
媒体类型的工具类,后续可以供其他微服务使用
simplemagic
依赖,它提供的方法可以根据文件扩展名得到资源的媒体类型<dependency>
<groupId>com.j256.simplemagic</groupId>
<artifactId>simplemagic</artifactId>
<version>1.17</version>
</dependency>
private String getMimeType(String extension){
if(extension==null)
extension = "";
// 根据文件扩展名取出mimeType,extension不能为null,否则会报空指针异常
ContentInfo extensionMatch = ContentInfoUtil.findExtensionMatch(extension);
// 对于未知扩展名的文件采用通用的mimeType
String mimeType = MediaType.APPLICATION_OCTET_STREAM_VALUE;
if(extensionMatch!=null){
mimeType = extensionMatch.getMimeType();
}
return mimeType;
}
获取文件默认存储目录路径,遵守年/月/日
规范
private String getDefaultFolderPath() {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
String folder = sdf.format(new Date()).replace("-", "/")+"/";
return folder;
}
根据文件的字节流获取文件的MD5
值
private String getFileMd5(File file) {
try (FileInputStream fileInputStream = new FileInputStream(file)) {
String fileMd5 = DigestUtils.md5Hex(fileInputStream);
return fileMd5;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
将服务器中接收的临时文件上传到minIO
,如果前端没有指定存储文件的对象名称objectName,默认由默认目录/文件MD5值.扩展名
组成
/**
* @param localFilePath 上传文件对应的临时文件在服务器的绝对路径
* @param objectName 待存储文件的对象名称,默认目录/文件名.扩展名
* @return void
*/
public boolean addMediaFilesToMinIO(String localFilePath,String mimeType,String bucket, String objectName) {
try {
UploadObjectArgs testbucket = UploadObjectArgs.builder()
.bucket(bucket)
.object(objectName)
.filename(localFilePath)
.contentType(mimeType)
.build();
minioClient.uploadObject(testbucket);
log.debug("上传文件到minio成功,bucket:{},objectName:{}",bucket,objectName);
System.out.println("上传成功");
return true;
} catch (Exception e) {
e.printStackTrace();
log.error("上传文件到minio出错,bucket:{},objectName:{},错误原因:{}",bucket,objectName,e.getMessage(),e);
XueChengPlusException.cast("上传文件到文件系统失败");
}
return false;
}
将上传到Minio中的文件信息添加到media_files
表
/**
* @description 将文件信息添加到文件表
* @param companyId 机构id
* @param fileMd5 文件md5值
* @param uploadFileParamsDto 上传文件的信息
* @param bucket 桶
* @param objectName 对象名称
*/
@Transactional
public MediaFiles addMediaFilesToDb(Long companyId,String fileMd5,UploadFileParamsDto uploadFileParamsDto,String bucket,String objectName){
// 从数据库查询文件
MediaFiles mediaFiles = mediaFilesMapper.selectById(fileMd5);
if (mediaFiles == null) {
mediaFiles = new MediaFiles();
// 拷贝请求参数中的基本信息
BeanUtils.copyProperties(uploadFileParamsDto, mediaFiles);
mediaFiles.setId(fileMd5);
mediaFiles.setFileId(fileMd5);
mediaFiles.setCompanyId(companyId);
mediaFiles.setUrl("/" + bucket + "/" + objectName);
mediaFiles.setBucket(bucket);
mediaFiles.setFilePath(objectName);
mediaFiles.setCreateDate(LocalDateTime.now());
// 对于上传的图片默认审核通过
mediaFiles.setAuditStatus("002003");
mediaFiles.setStatus("1");
// 保存文件信息到文件表
int insert = mediaFilesMapper.insert(mediaFiles);
if (insert < 0) {
log.error("保存文件信息到数据库失败,{}",mediaFiles.toString());
XueChengPlusException.cast("保存文件信息失败");
}
log.debug("保存文件信息到数据库成功,{}",mediaFiles.toString());
}
return mediaFiles;
}
事务失效优化
在updateFile
方法上添加@Transactional
注解表示在调用updateFile
方法前开启数据库事务
在addMediaFilesToDb
添加事务控制表示在调用addMediaFilesToDb
方法前开启数据库事务,但需要避免事务失效的问题
在一个非事务控制的方法里直接使用this调用
一个被事务控制的方法,此时的事务不会生效
@Transactional
注解并且需要通过代理对象(Spring注入的对象)
执行该方法事务才会生效第一步: 在addMediaFilesToDb
方法上加上@Transactional
注解并将其提取成MediaFileService
的接口方法,这样我们就可以通过代理对象调用该方法
/**
* @description 将文件信息添加到文件表
* @param companyId 机构id
* @param fileMd5 文件md5值
* @param uploadFileParamsDto 上传文件的信息
* @param bucket 桶
* @param objectName 对象名称
*/
public MediaFiles addMediaFilesToDb(Long companyId,String fileMd5,UploadFileParamsDto uploadFileParamsDto,String bucket,String objectName);
第二步: 在MediaFileService
的实现类MediaFileServiceImpl
中注入MediaFileService
的代理对象
// 注入代理对象
@Autowired
MediaFileService currentProxy;
@Override
public UploadFileResultDto uploadFile(Long companyId, UploadFileParamsDto uploadFileParamsDto, String localFilePath,String objectname) {
// 使用代理对象调用方法,将文件信息存储到数据库
MediaFiles mediaFiles = currentProxy.addMediaFilesToDb(companyId, fileMd5, uploadFileParamsDto, bucket_files, objectName);
// ........
}
查看上传的媒资信息
上传图片完成后我们可以在媒资管理界面查看刚刚上传的图片信息
在MediaFileServiceImpl中
编写queryMediaFiles
方法查询上传的文件信息
@Override
public PageResult<MediaFiles> queryMediaFiles(Long companyId, PageParams pageParams, QueryMediaParamsDto queryMediaParamsDto) {
//构建查询条件对象
LambdaQueryWrapper<MediaFiles> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.like(!StringUtils.isEmpty(queryMediaParamsDto.getFilename()), MediaFiles::getFilename, queryMediaParamsDto.getFilename());
queryWrapper.eq(!StringUtils.isEmpty(queryMediaParamsDto.getFileType()), MediaFiles::getFileType, queryMediaParamsDto.getFileType());
// 分页对象
Page<MediaFiles> page = new Page<>(pageParams.getPageNo(), pageParams.getPageSize());
// 查询数据内容获得结果
Page<MediaFiles> pageResult = mediaFilesMapper.selectPage(page, queryWrapper);
// 获取数据列表
List<MediaFiles> list = pageResult.getRecords();
// 获取数据总数
long total = pageResult.getTotal();
// 构建结果集
PageResult<MediaFiles> mediaListResult = new PageResult<>(list, total, pageParams.getPageNo(), pageParams.getPageSize());
return mediaListResult;
}
api工程定义接口
MultipartFile
是SpringMVC提供简化上传操作的工具类,为了使接口更通用可以使用字节数组代替MultpartFile
类型
HttpServletRequest
: 用来接收上传的数据,如果上传的是文件将以二进制流的形式传递到后端@ApiOperation("上传文件")
@RequestMapping(value = "/upload/coursefile",consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public UploadFileResultDto upload(@RequestPart("filedata") MultipartFile upload,
@RequestParam(value = "folder", required = false) String folder,
@RequestParam(value = "objectName", required = false) String objectName) {
// 封装上传文件的信息
UploadFileParamsDto uploadFileParamsDto = new UploadFileParamsDto();
// 文件名称
uploadFileParamsDto.setFilename(upload.getOriginalFilename());
// 文件大小
uploadFileParamsDto.setFileSize(upload.getSize());
// 获取请求参数中上传文件的内容类型
String contentType = upload.getContentType();
if (contentType.contains("image")) {
// 图片
uploadFileParamsDto.setFileType("001001");
} else {
// 其他
uploadFileParamsDto.setFileType("001003");
}
uploadFileParamsDto.setContentType(contentType);
Long companyId = 1232141425L;
try {
// 获取上传文件对应的字节
UploadFileResultDto uploadFileResultDto = mediaFileService.uploadFile(companyId, uploadFileParamsDto, upload.getBytes(), folder, objectName);
return uploadFileResultDto;
} catch (IOException e) {
XueChengPlusException.cast("上传文件过程出错");
}
return null;
}
service工程定义业务类
/**
* @description 上传文件的通用接口
* @param companyId 机构id
* @param uploadFileParamsDto 文件信息
* @param bytes 文件字节数组
* @param folder 桶下边的子目录
* @param objectName 对象名称不包含存储目录
* @return com.xuecheng.media.model.dto.UploadFileResultDto
*/
UploadFileResultDto uploadFile(Long companyId, UploadFileParamsDto uploadFileParamsDto, byte[] bytes, String folder, String objectName);
@Override
public UploadFileResultDto uploadFile(Long companyId, UploadFileParamsDto uploadFileParamsDto, byte[] bytes, String folder, String objectName) {
// 根据上传文件对应的字节获取对应的MD5值,md5Hex方法是根据上传文件的字节流获取对应的MD5值
String fileMD5 = DigestUtils.md5DigestAsHex(bytes);
// 获取存储的目录
if (StringUtils.isEmpty(folder)) {
// 如果桶下的子目录不存在,则按照年/月/日的规范自动生成一个目录
folder = getFileFolder(true, true, true);
} else if (!folder.endsWith("/")) {
// 如果目录末尾没有/后缀则加一个
folder = folder + "/";
}
// 这里接收的objectName不包含存储目录
if (StringUtils.isEmpty(objectName)) {
// 如果文件名为空,则设置其默认文件名为文件的md5码+文件后缀名
String filename = uploadFileParamsDto.getFilename();
objectName = fileMD5 + filename.substring(filename.lastIndexOf("."));
}
objectName = folder + objectName;
try {
// 将服务中接收的临时文件上传到minio
addMediaFilesToMinIO(bytes, bucket_files, objectName);
// 将上传文件的信息保存到数据库
MediaFiles mediaFiles = addMediaFilesToDB(companyId, uploadFileParamsDto, objectName, fileMD5, bucket_files);
// 准备返回的UploadFileResultDto对象
UploadFileResultDto uploadFileResultDto = new UploadFileResultDto();
BeanUtils.copyProperties(mediaFiles, uploadFileResultDto);
return uploadFileResultDto;
} catch (Exception e) {
XueChengPlusException.cast("上传过程中出错");
}
return null;
}
根据上传文件的对象名称中包含的扩展名获取文件对应的mimeType
媒体类型
// 根
private static String getContentType(String objectName) {
// 对于未知扩展名的文件默认content-type为通用的二进制流
String contentType = MediaType.APPLICATION_OCTET_STREAM_VALUE;
if (objectName.indexOf(".") >= 0) {
// 对象名称包含`.`则划分出扩展名
String extension = objectName.substring(objectName.lastIndexOf("."));
if(extension==null)
extension = "";
// 根据扩展名得到content-type,extension为null会报空指针异常,对于未知扩展名的则会返回null
ContentInfo extensionMatch = ContentInfoUtil.findExtensionMatch(extension);
// 如果得到了正常的content-type则重新赋值覆盖默认类型
if (extensionMatch != null) {
contentType = extensionMatch.getMimeType();
}
}
return contentType;
}
按照条件生成文件的存储目录,按照年/月/日
规范
/**
* 自动生成目录
* @param year 是否包含年
* @param month 是否包含月
* @param day 是否包含日
* @return
*/
private String getFileFolder(boolean year, boolean month, boolean day) {
StringBuffer stringBuffer = new StringBuffer();
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
String dateString = dateFormat.format(new Date());
String[] split = dateString.split("-");
if (year) {
stringBuffer.append(split[0]).append("/");
}
if (month) {
stringBuffer.append(split[1]).append("/");
}
if (day) {
stringBuffer.append(split[2]).append("/");
}
return stringBuffer.toString();
}
将服务器中接收的临时文件上传到minIO
,如果前端没有指定存储文件的目录folder
和对象名称objectName
,默认由默认目录/文件MD5值.扩展名
组成
/**
* @param bytes 文件字节数组
* @param bucket 桶
* @param objectName 对象名称 23/02/15/porn.mp4
*/
private void addMediaFilesToMinIO(byte[] bytes, String bucket, String objectName) {
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
// 获取上传文件的媒体类型
String contentType = getContentType(objectName);
try {
minioClient.putObject(PutObjectArgs.builder()
.bucket(bucket)
.object(objectName)
.stream(byteArrayInputStream, byteArrayInputStream.available(), -1)
.contentType(contentType)
.build());
} catch (Exception e) {
log.debug("上传到文件系统出错:{}", e.getMessage());
throw new XueChengPlusException("上传到文件系统出错");
}
}
将上传到Minio中的文件信息添加到media_files
表
/**
* 将文件信息添加到文件表
* @param companyId 机构id
* @param uploadFileParamsDto 上传文件的信息
* @param objectName 对象名称
* @param fileMD5 文件的md5码
* @param bucket 桶
* @return
*/
private MediaFiles addMediaFilesToDB(Long companyId, UploadFileParamsDto uploadFileParamsDto, String objectName, String fileMD5, String bucket) {
// 保存到数据库
MediaFiles mediaFiles = mediaFilesMapper.selectById(fileMD5);
if (mediaFiles == null) {
mediaFiles = new MediaFiles();
BeanUtils.copyProperties(uploadFileParamsDto, mediaFiles);
mediaFiles.setId(fileMD5);
mediaFiles.setFileId(fileMD5);
mediaFiles.setCompanyId(companyId);
mediaFiles.setBucket(bucket);
mediaFiles.setCreateDate(LocalDateTime.now());
mediaFiles.setStatus("1");
mediaFiles.setFilePath(objectName);
mediaFiles.setUrl("/" + bucket + "/" + objectName);
// 查阅数据字典,002003表示审核通过
mediaFiles.setAuditStatus("002003");
}
int insert = mediaFilesMapper.insert(mediaFiles);
if (insert <= 0) {
XueChengPlusException.cast("保存文件信息失败");
}
return mediaFiles;
前后端联调测试
第一步: 使用HTTP Client
测试,然后在Minio分布式文件系统对应的bucket中查看上传的图片
// 上传文件
POST {{media_host}}/media/upload/coursefile
Content-Type: multipart/form-data; boundary=WebAppBoundary
--WebAppBoundary
Content-Disposition: form-data; name="filedata"; filename="test01.jpg"
Content-Type: application/octet-stream
< C:\Users\kyle\Desktop\Picture\photo\bg01.jpg
// 响应结果如下
POST http://localhost:53050/media/upload/coursefile
HTTP/1.1 200
Content-Type: application/json
Transfer-Encoding: chunked
Date: Thu, 16 Feb 2023 09:57:48 GMT
Keep-Alive: timeout=60
Connection: keep-alive
{
"id": "632fb34166d91865da576032b9330ced",
"companyId": 1232141425,
"companyName": null,
"filename": "test01.jpg",
"fileType": "001003",
"tags": null,
"bucket": "mediafiles",
"filePath": "2023/57/16/632fb34166d91865da576032b9330ced.jpg",
"fileId": "632fb34166d91865da576032b9330ced",
"url": "/mediafiles/2023/57/16/632fb34166d91865da576032b9330ced.jpg",
"username": null,
"createDate": "2023-02-16 17:57:48",
"changeDate": null,
"status": "1",
"remark": "",
"auditStatus": "002003",
"auditMind": null,
"fileSize": 22543
}
第二步: 将前端保存图片的服务器地址
改为自己的minio服务的地址,这样我们就可以在浏览器中查看上传的图片信息
## 图片服务器地址
VUE_APP_SERVER_PICSERVER_URL=http://127.0.0.1:9000