530 lines
16 KiB
PHP
530 lines
16 KiB
PHP
<?php
|
|
|
|
namespace app\Service;
|
|
|
|
use app\Model\UsedCar;
|
|
use app\Model\UsedCarPdfLog;
|
|
use app\Model\User;
|
|
use support\Db;
|
|
use support\Redis;
|
|
|
|
/**
|
|
* 二手车业务服务
|
|
*/
|
|
class UsedCarService
|
|
{
|
|
private UsedCarCrawlService $crawlService;
|
|
|
|
public function __construct()
|
|
{
|
|
$this->crawlService = new UsedCarCrawlService();
|
|
}
|
|
|
|
/**
|
|
* 获取二手车列表
|
|
*
|
|
* @param int $userId 用户ID
|
|
* @param string $type 列表类型: all-全部, self-我创建的
|
|
* @param int $page 页码
|
|
* @param int $pageSize 每页数量
|
|
* @param string|null $keyword 搜索关键词
|
|
* @param int|null $status 状态筛选
|
|
* @return array
|
|
*/
|
|
public function getList(int $userId, string $type = 'self', int $page = 1, int $pageSize = 20, ?string $keyword = null, ?int $status = null): array
|
|
{
|
|
$query = UsedCar::query();
|
|
|
|
// 筛选条件
|
|
switch ($type) {
|
|
case 'self':
|
|
$query->where('user_id', $userId);
|
|
break;
|
|
case 'all':
|
|
// 管理员可以查看所有
|
|
break;
|
|
}
|
|
|
|
// 状态筛选
|
|
if ($status !== null) {
|
|
$query->where('status', $status);
|
|
} else {
|
|
// 默认不显示已删除的
|
|
$query->where('status', '!=', UsedCar::STATUS_DELETED);
|
|
}
|
|
|
|
// 关键词搜索
|
|
if ($keyword) {
|
|
$query->where(function ($q) use ($keyword) {
|
|
$q->where('product_name', 'like', "%{$keyword}%")
|
|
->orWhere('code', 'like', "%{$keyword}%");
|
|
});
|
|
}
|
|
|
|
// 获取总数
|
|
$total = $query->count();
|
|
|
|
// 分页查询
|
|
$list = $query->orderBy('sort', 'desc')
|
|
->orderBy('id', 'desc')
|
|
->offset(($page - 1) * $pageSize)
|
|
->limit($pageSize)
|
|
->get();
|
|
|
|
// 格式化数据
|
|
$formattedList = $list->map(function ($item) {
|
|
return $this->formatListItem($item);
|
|
})->toArray();
|
|
|
|
return [
|
|
'list' => $formattedList,
|
|
'total' => $total,
|
|
'page' => $page,
|
|
'page_size' => $pageSize,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* 获取二手车详情
|
|
*
|
|
* @param int $id 二手车ID
|
|
* @param int $userId 当前用户ID
|
|
* @return array
|
|
* @throws \Exception
|
|
*/
|
|
public function getDetail(int $id, int $userId): array
|
|
{
|
|
$car = UsedCar::with(['pdfLogs', 'user'])->findOrFail($id);
|
|
|
|
// 格式化数据
|
|
return $this->formatDetailItem($car);
|
|
}
|
|
|
|
/**
|
|
* 创建二手车记录
|
|
*
|
|
* @param array $data
|
|
* @param int $userId
|
|
* @return UsedCar
|
|
* @throws \Exception
|
|
*/
|
|
public function create(array $data, int $userId): UsedCar
|
|
{
|
|
// 验证数据
|
|
$this->validateCarData($data);
|
|
|
|
return Db::connection()->transaction(function () use ($data, $userId) {
|
|
// 获取用户信息
|
|
$user = User::findOrFail($userId);
|
|
|
|
// 生成唯一编号
|
|
$code = $this->generateCode($userId, $user->name ?? '');
|
|
|
|
// 处理图片
|
|
$images = $data['images'] ?? [];
|
|
if (!is_array($images)) {
|
|
$images = explode(',', $images);
|
|
}
|
|
$images = array_filter($images);
|
|
|
|
// 处理附件
|
|
$attachment = $data['attachment'] ?? null;
|
|
if (is_array($attachment)) {
|
|
$attachment = json_encode($attachment, JSON_UNESCAPED_UNICODE);
|
|
}
|
|
|
|
// 检查source_url是否已存在
|
|
if (!empty($data['source_url'])) {
|
|
$exists = UsedCar::where('source_url', $data['source_url'])
|
|
->where('user_id', $userId)
|
|
->exists();
|
|
if ($exists) {
|
|
throw new \Exception('该车源已收录');
|
|
}
|
|
}
|
|
|
|
// 创建记录
|
|
$car = UsedCar::create([
|
|
'code' => $code,
|
|
'user_id' => $userId,
|
|
'role' => $user->role ?? 1,
|
|
'product_name' => $data['product_name'],
|
|
'vin' => $data['vin'] ?? null,
|
|
'price' => $this->parsePrice($data['price'] ?? 0),
|
|
'source_place' => $data['source_place'] ?? null,
|
|
'plate_place' => $data['plate_place'] ?? null,
|
|
'plate_time' => $data['plate_time'] ?? null,
|
|
'displacement' => $data['displacement'] ?? null,
|
|
'mileage' => $this->parseMileage($data['mileage'] ?? 0),
|
|
'power' => $this->parsePower($data['power'] ?? ''),
|
|
'body_color' => $data['body_color'] ?? null,
|
|
'images' => implode(',', $images),
|
|
'condition_desc' => $data['condition_desc'] ?? null,
|
|
'attachment' => $attachment,
|
|
'source_url' => $data['source_url'] ?? null,
|
|
'crawl_title' => $data['crawl_title'] ?? null,
|
|
'crawl_brand' => $data['crawl_brand'] ?? null,
|
|
'crawl_brand_id' => $data['crawl_brand_id'] ?? 0,
|
|
'crawl_series' => $data['crawl_series'] ?? null,
|
|
'crawl_series_id' => $data['crawl_series_id'] ?? 0,
|
|
'crawl_year' => $data['crawl_year'] ?? null,
|
|
'crawl_msrp' => $this->parsePrice($data['crawl_msrp'] ?? 0),
|
|
'crawl_car_id' => $data['crawl_car_id'] ?? 0,
|
|
'status' => UsedCar::STATUS_NORMAL,
|
|
'sort' => $data['sort'] ?? 0,
|
|
]);
|
|
|
|
return $car;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 更新二手车记录
|
|
*
|
|
* @param int $id
|
|
* @param array $data
|
|
* @param int $userId
|
|
* @return UsedCar
|
|
* @throws \Exception
|
|
*/
|
|
public function update(int $id, array $data, int $userId): UsedCar
|
|
{
|
|
$car = UsedCar::findOrFail($id);
|
|
|
|
// 检查权限
|
|
if ($car->user_id !== $userId) {
|
|
throw new \Exception('无权编辑此记录');
|
|
}
|
|
|
|
// 验证数据
|
|
$this->validateCarData($data);
|
|
|
|
// 处理图片
|
|
if (isset($data['images'])) {
|
|
$images = is_array($data['images']) ? $data['images'] : explode(',', $data['images']);
|
|
$data['images'] = implode(',', array_filter($images));
|
|
}
|
|
|
|
// 处理附件
|
|
if (isset($data['attachment']) && is_array($data['attachment'])) {
|
|
$data['attachment'] = json_encode($data['attachment'], JSON_UNESCAPED_UNICODE);
|
|
}
|
|
|
|
// 处理价格和里程
|
|
if (isset($data['price'])) {
|
|
$data['price'] = $this->parsePrice($data['price']);
|
|
}
|
|
if (isset($data['mileage'])) {
|
|
$data['mileage'] = $this->parseMileage($data['mileage']);
|
|
}
|
|
if (isset($data['power'])) {
|
|
$data['power'] = $this->parsePower($data['power']);
|
|
}
|
|
if (isset($data['crawl_msrp'])) {
|
|
$data['crawl_msrp'] = $this->parsePrice($data['crawl_msrp']);
|
|
}
|
|
|
|
// 只更新允许的字段
|
|
$fillable = [
|
|
'product_name', 'vin', 'price', 'source_place', 'plate_place',
|
|
'plate_time', 'displacement', 'mileage', 'power', 'body_color',
|
|
'images', 'condition_desc', 'attachment',
|
|
];
|
|
|
|
foreach ($fillable as $field) {
|
|
if (array_key_exists($field, $data)) {
|
|
$car->$field = $data[$field];
|
|
}
|
|
}
|
|
|
|
$car->save();
|
|
return $car;
|
|
}
|
|
|
|
/**
|
|
* 删除二手车记录(软删除)
|
|
*
|
|
* @param int $id
|
|
* @param int $userId
|
|
* @return bool
|
|
* @throws \Exception
|
|
*/
|
|
public function delete(int $id, int $userId): bool
|
|
{
|
|
$car = UsedCar::findOrFail($id);
|
|
|
|
// 检查权限
|
|
if ($car->user_id !== $userId) {
|
|
throw new \Exception('无权删除此记录');
|
|
}
|
|
|
|
$car->status = UsedCar::STATUS_DELETED;
|
|
return $car->save();
|
|
}
|
|
|
|
/**
|
|
* 更新状态
|
|
*
|
|
* @param int $id
|
|
* @param int $status
|
|
* @param int $userId
|
|
* @return bool
|
|
* @throws \Exception
|
|
*/
|
|
public function updateStatus(int $id, int $status, int $userId): bool
|
|
{
|
|
$car = UsedCar::findOrFail($id);
|
|
|
|
// 检查权限
|
|
if ($car->user_id !== $userId) {
|
|
throw new \Exception('无权修改此记录');
|
|
}
|
|
|
|
$car->status = $status;
|
|
return $car->save();
|
|
}
|
|
|
|
/**
|
|
* URL抓取车辆信息
|
|
*
|
|
* @param string $url
|
|
* @return array
|
|
* @throws \Exception
|
|
*/
|
|
public function crawlByUrl(string $url): array
|
|
{
|
|
$data = $this->crawlService->crawlByUrl($url);
|
|
|
|
// 下载图片到临时目录
|
|
$tempDir = runtime_path('tmp/usedcar_images/');
|
|
$images = $data['images'] ?? [];
|
|
|
|
return [
|
|
'product_name' => $data['title'] ?? '',
|
|
'price' => $this->parsePrice($data['price'] ?? 0),
|
|
'source_place' => $data['source_place'] ?? '',
|
|
'plate_time' => $data['plate_time'] ?? '',
|
|
'displacement' => $data['displacement'] ?? '',
|
|
'mileage' => $data['mileage'] ?? 0,
|
|
'power' => $data['power'] ?? '',
|
|
'body_color' => $data['body_color'] ?? '',
|
|
'images' => $images,
|
|
'condition_desc' => $data['condition_desc'] ?? '',
|
|
'source_url' => $data['source_url'] ?? $url,
|
|
'crawl_title' => $data['title'] ?? '',
|
|
'crawl_brand' => $data['brand_name'] ?? '',
|
|
'crawl_brand_id' => $data['brand_id'] ?? 0,
|
|
'crawl_series' => $data['series_name'] ?? '',
|
|
'crawl_series_id' => $data['series_id'] ?? 0,
|
|
'crawl_year' => $data['year'] ?? '',
|
|
'crawl_car_id' => $data['car_id'] ?? 0,
|
|
'used_car_id' => $data['used_car_id'] ?? 0,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* 生成唯一编号
|
|
*
|
|
* @param int $userId
|
|
* @param string $userName
|
|
* @return string
|
|
*/
|
|
private function generateCode(int $userId, string $userName): string
|
|
{
|
|
// 获取用户名拼音首字母
|
|
$nameInitials = $this->getNameInitials($userName);
|
|
$userCode = str_pad($userId, 3, '0', STR_PAD_LEFT);
|
|
|
|
// 从Redis获取今日计数
|
|
$redisKey = 'used_car_count_' . $userId;
|
|
$todayStart = strtotime('today');
|
|
|
|
// 检查数据库中今日已创建的数量
|
|
$count = UsedCar::where('user_id', $userId)
|
|
->where('created_at', '>=', $todayStart)
|
|
->count();
|
|
|
|
// 从Redis获取缓存计数
|
|
$cachedCount = Redis::get($redisKey, 0);
|
|
$count = max($count, $cachedCount) + 1;
|
|
|
|
// 更新Redis缓存(24小时过期)
|
|
Redis::setex($redisKey, 86400, $count);
|
|
|
|
$countCode = str_pad($count, 3, '0', STR_PAD_LEFT);
|
|
|
|
return $nameInitials . $userCode . date('Ymd') . $countCode;
|
|
}
|
|
|
|
/**
|
|
* 获取中文名拼音首字母
|
|
*/
|
|
private function getNameInitials(string $name): string
|
|
{
|
|
$initials = '';
|
|
$len = mb_strlen($name);
|
|
|
|
for ($i = 0; $i < $len; $i++) {
|
|
$char = mb_substr($name, $i, 1);
|
|
$initials .= $this->getFirstChar($char);
|
|
}
|
|
|
|
return strtoupper($initials);
|
|
}
|
|
|
|
/**
|
|
* 获取单个汉字的首字母
|
|
*/
|
|
private function getFirstChar(string $char): string
|
|
{
|
|
// 英文字母直接返回
|
|
if (preg_match('/^[a-zA-Z]$/', $char)) {
|
|
return strtoupper($char);
|
|
}
|
|
|
|
// 转换为GB2312编码
|
|
$s = @iconv('UTF-8', 'GB2312//IGNORE', $char);
|
|
if ($s === false || strlen($s) < 2) {
|
|
return '';
|
|
}
|
|
|
|
$code = ord($s[0]) * 256 + ord($s[1]) - 65536;
|
|
|
|
$map = [
|
|
[-20319, -20284, 'A'], [-20283, -19776, 'B'], [-19775, -19219, 'C'],
|
|
[-19218, -18711, 'D'], [-18710, -18527, 'E'], [-18526, -18240, 'F'],
|
|
[-18239, -17923, 'G'], [-17922, -17418, 'H'], [-17417, -16475, 'J'],
|
|
[-16474, -16213, 'K'], [-16212, -15641, 'L'], [-15640, -15166, 'M'],
|
|
[-15165, -14923, 'N'], [-14922, -14915, 'O'], [-14914, -14631, 'P'],
|
|
[-14630, -14150, 'Q'], [-14149, -14091, 'R'], [-14090, -13319, 'S'],
|
|
[-13318, -12839, 'T'], [-12838, -12557, 'W'], [-12556, -11848, 'X'],
|
|
[-11847, -11056, 'Y'], [-11055, -10247, 'Z']
|
|
];
|
|
|
|
foreach ($map as [$start, $end, $letter]) {
|
|
if ($code >= $start && $code <= $end) {
|
|
return $letter;
|
|
}
|
|
}
|
|
|
|
return '';
|
|
}
|
|
|
|
/**
|
|
* 解析价格(单位统一转换为分)
|
|
*/
|
|
private function parsePrice($price): int
|
|
{
|
|
$price = floatval(str_replace(['万', ','], '', $price));
|
|
// 如果小于200,说明是以万为单位
|
|
if ($price < 200 && $price > 0) {
|
|
$price = $price * 10000;
|
|
}
|
|
return (int)($price * 100); // 转换为分
|
|
}
|
|
|
|
/**
|
|
* 解析里程(移除单位)
|
|
*/
|
|
private function parseMileage($mileage): int
|
|
{
|
|
$mileage = str_replace(['万', 'K', 'M', 'k', 'm', '公里'], '', $mileage);
|
|
return (int)floatval($mileage);
|
|
}
|
|
|
|
/**
|
|
* 解析马力(移除单位)
|
|
*/
|
|
private function parsePower($power): string
|
|
{
|
|
return str_replace(['P', 'S', 'p', 's', '马力', 'Ps', 'PS'], '', $power);
|
|
}
|
|
|
|
/**
|
|
* 验证车辆数据
|
|
*/
|
|
private function validateCarData(array $data): void
|
|
{
|
|
if (empty($data['product_name'])) {
|
|
throw new \Exception('产品名称不能为空');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 格式化列表项
|
|
*/
|
|
private function formatListItem(UsedCar $car): array
|
|
{
|
|
$data = $car->toArray();
|
|
|
|
// 格式化时间
|
|
if ($car->created_at) {
|
|
$data['created_at'] = $car->created_at->toDateTimeString();
|
|
}
|
|
if ($car->updated_at) {
|
|
$data['updated_at'] = $car->updated_at->toDateTimeString();
|
|
}
|
|
|
|
// 处理图片
|
|
$data['images_array'] = [];
|
|
if (!empty($car->images)) {
|
|
$data['images_array'] = explode(',', $car->images);
|
|
}
|
|
|
|
return $data;
|
|
}
|
|
|
|
/**
|
|
* 格式化详情项
|
|
*/
|
|
private function formatDetailItem(UsedCar $car): array
|
|
{
|
|
$data = $car->toArray();
|
|
|
|
// 格式化时间
|
|
if ($car->created_at) {
|
|
$data['created_at'] = $car->created_at->toDateTimeString();
|
|
}
|
|
if ($car->updated_at) {
|
|
$data['updated_at'] = $car->updated_at->toDateTimeString();
|
|
}
|
|
|
|
// 处理图片
|
|
$data['images_array'] = [];
|
|
if (!empty($car->images)) {
|
|
$data['images_array'] = explode(',', $car->images);
|
|
}
|
|
|
|
// 处理附件
|
|
$data['attachment_array'] = [];
|
|
if (!empty($car->attachment)) {
|
|
$attachment = json_decode($car->attachment, true);
|
|
if (is_array($attachment)) {
|
|
$data['attachment_array'] = $attachment;
|
|
}
|
|
}
|
|
|
|
// 添加用户信息
|
|
if ($car->user) {
|
|
$data['user'] = [
|
|
'id' => $car->user->id,
|
|
'name' => $car->user->name,
|
|
];
|
|
}
|
|
|
|
// PDF生成日志
|
|
if ($car->pdfLogs) {
|
|
$data['pdf_logs'] = $car->pdfLogs->map(function ($log) {
|
|
return [
|
|
'id' => $log->id,
|
|
'price_snapshot' => $log->price_snapshot,
|
|
'pdf_path' => $log->pdf_path,
|
|
'created_at' => $log->created_at ? $log->created_at->toDateTimeString() : null,
|
|
];
|
|
})->toArray();
|
|
}
|
|
|
|
return $data;
|
|
}
|
|
}
|