文件
yjyf/server/application/api/controller/Home.php
linda e1bf139c5a feat(order): 下单支持一口价/师傅报价竞价/用户自主定价三种方式
- 下单页新增计价方式选择;自主定价填写总价,师傅报价单进入报价大厅
- 用户订单详情展示师傅报价列表并可选定师傅;选定后同步师傅、金额与订单状态
- 师傅大厅新增用户定价筛选与按价接单;报价单禁止直接抢单
- 后端订单新增 custom 价态、金额口径统一由 calcAmount 输出
- 报价中状态纳入用户可取消范围;超时回流重新开放报价
- 新增 fa_order.price_type custom 枚举迁移脚本(schema_phase6)
2026-10-28 17:48:23 +08:00

289 行
11 KiB
PHP

此文件含有模棱两可的 Unicode 字符

此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。

<?php
namespace app\api\controller;
use think\Db;
/**
* 首页 / 服务类目 / 服务项目 / 费用试算
*/
class Home extends Base
{
protected $noNeedLogin = ['index', 'cats', 'services', 'detail'];
/**
* 首页聚合:类目 + 热门服务 + 我的角标
*/
public function index()
{
$cats = Db::name('service_cat')
->field('id,name,icon')
->where('status', 'normal')
->order('weigh desc,id asc')
->select();
$hot = Db::name('service')
->field('id,cat_id,name,price_type,base_price,max_price,unit,image,sales')
->where('status', 'normal')
->order('sales desc,id asc')
->limit(6)
->select();
$data = [
'cats' => $cats,
'hot' => $this->formatServices($hot),
'banners' => $this->banners(),
'assures' => $this->configList('home_assure', '平台认证,先行赔付,极速上门,售后无忧'),
'tutorial' => $this->tutorial(),
'badge' => ['coupon' => 0, 'unread' => 0],
];
if ($this->auth->isLogin()) {
$uid = $this->uid();
$data['badge']['coupon'] = (int)Db::name('user_coupon')->where('user_id', $uid)->where('status', 'unused')->count();
$data['badge']['unread'] = (int)Db::name('message')->where('receive_type', 'user')->where('receive_id', $uid)->where('is_read', 0)->count();
}
$this->success('', $data);
}
/**
* 全部类目
*/
public function cats()
{
$cats = Db::name('service_cat')
->field('id,name,icon')
->where('status', 'normal')
->order('weigh desc,id asc')
->select();
$this->success('', ['list' => $cats]);
}
/**
* 类目下的服务列表
*/
public function services()
{
$catId = (int)$this->request->param('cat_id');
$keyword = trim((string)$this->request->param('keyword'));
if ($catId <= 0 && $keyword === '') {
$this->error('请选择服务类目');
}
$query = Db::name('service')->where('status', 'normal');
if ($catId > 0) {
$query->where('cat_id', $catId);
}
if ($keyword !== '') {
$query->where('name', 'like', '%' . $keyword . '%');
}
$list = $query->order('weigh desc,sales desc,id asc')->select();
$this->success('', ['list' => $this->formatServices($list)]);
}
/**
* 服务详情
*/
public function detail()
{
$id = (int)$this->request->param('id');
$service = Db::name('service')->where('id', $id)->where('status', 'normal')->find();
if (!$service) {
$this->error('服务不存在');
}
$cat = Db::name('service_cat')->where('id', $service['cat_id'])->find();
$service['cat_name'] = $cat ? $cat['name'] : '';
$this->success('', ['detail' => $this->formatService($service)]);
}
/**
* 费用试算(基础价 + 楼层费 + 加急费 - 优惠)
* 支持下单页实时切换:一口价 / 师傅报价 / 用户自主定价
*/
public function calc()
{
$serviceId = (int)$this->request->param('service_id');
$service = Db::name('service')->where('id', $serviceId)->where('status', 'normal')->find();
if (!$service) {
$this->error('服务不存在');
}
$result = $this->calcAmount($service, [
'floor' => (int)$this->request->param('floor', 1),
'has_elevator' => (int)$this->request->param('has_elevator', 1),
'urgent' => (int)$this->request->param('urgent', 0),
], (int)$this->request->param('coupon_id', 0), $this->uid(),
(string)$this->request->param('price_type', ''),
(float)$this->request->param('user_price', 0));
$this->success('', $result);
}
/**
* 金额计算(下单与试算共用,保证前后端口径一致)
*
* @param array $service 服务行
* @param array $params floor / has_elevator / urgent
* @param int $couponId 用户优惠券 id
* @param int $uid 用户 id
* @param string $priceType 计价方式:fixed 一口价 / quote 师傅报价 / custom 用户自主定价
* @param float $userPrice 用户自主定价金额(总价,含楼层/加急)
*/
public static function calcAmount($service, $params, $couponId = 0, $uid = 0, $priceType = '', $userPrice = 0)
{
$allowTypes = ['fixed', 'quote', 'range', 'custom'];
$type = in_array($priceType, $allowTypes, true) ? $priceType : $service['price_type'];
$base = (float)$service['base_price'];
$floorAmount = 0;
$urgentAmount = 0;
if ($type === 'custom') {
// 用户自主定价:出价即订单总价,已包含楼层费/加急费,师傅按此价接单
$base = max(0, round((float)$userPrice, 2));
$total = $base;
} else {
$floor = max(0, (int)$params['floor']);
$freeFloor = (int)$service['free_floor'];
$floorFee = (float)$service['floor_fee'];
if ((int)$params['has_elevator'] === 0 && $floor > $freeFloor) {
$floorAmount = ($floor - $freeFloor) * $floorFee;
}
$urgentAmount = (int)$params['urgent'] === 1 ? round($base * 0.2, 2) : 0;
$total = round($base + $floorAmount + $urgentAmount, 2);
}
$discount = 0;
$couponInfo = null;
if ($couponId > 0 && $uid > 0) {
$uc = Db::name('user_coupon')->where('id', $couponId)->where('user_id', $uid)->where('status', 'unused')->find();
if ($uc) {
$coupon = Db::name('coupon')->where('id', $uc['coupon_id'])->where('status', 'normal')->find();
$notExpired = $coupon && (int)$coupon['end_time'] === 0 ? true : ($coupon && $coupon['end_time'] > time());
if ($notExpired && $total >= (float)$coupon['min_amount']) {
$discount = $coupon['type'] === 'cash'
? (float)$coupon['amount']
: round($total * (1 - (float)$coupon['discount']), 2);
$discount = min($discount, $total);
$couponInfo = ['id' => (int)$uc['id'], 'name' => $coupon['name']];
}
}
}
return [
'price_type' => $type,
'user_price' => $type === 'custom' ? round($base, 2) : 0,
'base_amount' => round($base, 2),
'floor_amount' => round($floorAmount, 2),
'urgent_amount' => round($urgentAmount, 2),
'total_amount' => $total,
'discount_amount' => round($discount, 2),
'pay_amount' => max(0, round($total - $discount, 2)),
'coupon' => $couponInfo,
'free_floor' => $type === 'custom' ? 0 : (int)$service['free_floor'],
'floor_fee' => $type === 'custom' ? 0 : round((float)$service['floor_fee'], 2),
];
}
/**
* 首页运营位(后台 fa_banner 可管,缺省回落到内置文案)
*/
protected function banners()
{
$rows = Db::name('banner')->where('status', 'normal')->order('weigh desc,id desc')->limit(5)->select();
if (!$rows) {
return [[
'id' => 0, 'badge' => '家装季', 'title' => '双11 狂欢大促',
'subtitle' => '下单立减 · 满额再返券', 'date_text' => '10.23 - 11.15',
'image' => '', 'link' => '',
]];
}
$ret = [];
foreach ($rows as $r) {
$ret[] = [
'id' => (int)$r['id'],
'badge' => $r['badge'],
'title' => $r['title'],
'subtitle' => $r['subtitle'],
'date_text' => $r['date_text'],
'image' => $r['image'],
'link' => $r['link'],
];
}
return $ret;
}
/**
* 读取 fa_config 中的逗号分隔列表
*/
protected function configList($name, $default = '')
{
$v = Db::name('config')->where('name', $name)->value('value');
if (!$v) {
$v = $default;
}
return array_values(array_filter(array_map('trim', explode(',', (string)$v))));
}
/**
* 首页教程卡(fa_config: 标题|副标题)
*/
protected function tutorial()
{
$v = Db::name('config')->where('name', 'home_tutorial')->value('value');
if (!$v) {
return ['title' => '下单找师傅教程', 'sub' => '3 分钟看懂,下单不踩坑'];
}
$parts = explode('|', (string)$v);
return [
'title' => trim($parts[0]),
'sub' => isset($parts[1]) ? trim($parts[1]) : '',
];
}
/**
* 列表格式化
*/
public function formatServices($list)
{
$ret = [];
foreach ($list as $row) {
$ret[] = $this->formatService($row);
}
return $ret;
}
/**
* 单项格式化(价格文案统一由此输出)
*/
public function formatService($row)
{
$type = $row['price_type'];
$priceText = '¥' . (float)$row['base_price'] . '起';
if ($type === 'range') {
$priceText = '¥' . (float)$row['base_price'] . '-' . (float)$row['max_price'] . '/' . $row['unit'];
} elseif ($type === 'quote') {
$priceText = '¥' . (float)$row['base_price'] . '起 · 师傅报价';
}
return [
'id' => (int)$row['id'],
'cat_id' => (int)$row['cat_id'],
'name' => $row['name'],
'cat_name' => isset($row['cat_name']) ? $row['cat_name'] : '',
'image' => $row['image'],
'price_type' => $type,
'price_type_text' => $type === 'fixed' ? '一口价' : ($type === 'quote' ? '师傅报价' : '按面积计价'),
'base_price' => (float)$row['base_price'],
'max_price' => (float)$row['max_price'],
'unit' => $row['unit'],
'price_text' => $priceText,
'sales' => (int)$row['sales'],
'floor_fee' => isset($row['floor_fee']) ? (float)$row['floor_fee'] : 0,
'free_floor' => isset($row['free_floor']) ? (int)$row['free_floor'] : 0,
'need_model' => isset($row['need_model']) ? (int)$row['need_model'] : 1,
'description' => isset($row['description']) ? $row['description'] : '',
];
}
}