chore: 移除废弃的增项服务模块并升级服务架构

- 删除 Addon 相关控制器、模型、语言包和视图文件
- 新增服务类型支持一口价属性配置
- 新增系统监控、用户资料、订单评价等后端管理页面
- 新增数据库表结构及索引优化脚本
- 更新应用版本号至 1.0.44
- 更新小程序应用标识
- 完善 .gitignore 配置
这个提交包含在:
2026-11-10 16:47:41 +08:00
父节点 e2b0044b64
当前提交 9e83a869e1
共有 60 个文件被更改,包括 5710 次插入 和 1020 次删除

查看文件

@@ -415,9 +415,6 @@ class Home extends Base
$types = $dec;
}
}
if (empty($types)) {
$types = $this->resolveServiceTypes($csRow);
}
foreach ($types as $st) {
$code = isset($st['code']) ? (string)$st['code'] : '';
if ($code === $serviceType) {
@@ -728,9 +725,11 @@ class Home extends Base
$allowTypes = ['fixed', 'quote', 'range', 'custom'];
$type = in_array($priceType, $allowTypes, true) ? $priceType : ($service ? $service['price_type'] : 'fixed');
// 支持多商品组合与增项计算
// 支持多商品组合与增项计算(后端结合 fa_service 与 fa_service_spec 权威核验单价)
$base = 0;
if (!empty($goodsList) && is_array($goodsList)) {
$serviceCache = [];
$specCache = [];
foreach ($goodsList as $g) {
if (is_string($g)) {
$rawG = htmlspecialchars_decode($g);
@@ -742,16 +741,88 @@ class Home extends Base
if (!is_array($g)) {
continue;
}
$itemPrice = isset($g['base_price']) ? (float)$g['base_price'] : 0;
if ($itemPrice <= 0 && !empty($g['service_id'])) {
$itemPrice = (float)Db::name('service')->where('id', (int)$g['service_id'])->value('base_price');
$sid = isset($g['service_id']) ? (int)$g['service_id'] : 0;
$specId = isset($g['spec_id']) ? (int)$g['spec_id'] : 0;
$reqServiceType = isset($g['service_type']) ? trim((string)$g['service_type']) : '';
$isCustom = !empty($g['is_custom']) || (isset($g['cat_id']) && (int)$g['cat_id'] === 15);
$dbSvc = null;
if ($sid > 0 && !$isCustom) {
if (!array_key_exists($sid, $serviceCache)) {
$serviceCache[$sid] = Db::name('service')->where('id', $sid)->find() ?: null;
}
$dbSvc = $serviceCache[$sid];
}
$specPrice = 0.0;
if ($specId > 0 && !$isCustom) {
if (!array_key_exists($specId, $specCache)) {
$spQuery = Db::name('service_spec')->where('id', $specId);
if ($sid > 0) {
$spQuery->where('service_id', $sid);
}
$specCache[$specId] = $spQuery->find() ?: null;
}
if (!empty($specCache[$specId])) {
$specPrice = (float)$specCache[$specId]['base_price'];
}
}
$itemPrice = isset($g['base_price']) ? max(0, (float)$g['base_price']) : 0.0;
$dbAddonsMap = [];
if ($dbSvc) {
$svcTypes = !empty($dbSvc['service_types']) ? (json_decode((string)$dbSvc['service_types'], true) ?: []) : [];
$defaultCode = 'install';
$matchedTypePrice = 0.0;
foreach ($svcTypes as $st) {
if (!empty($st['is_default']) && !empty($st['code'])) {
$defaultCode = (string)$st['code'];
}
if ($reqServiceType !== '' && isset($st['code']) && (string)$st['code'] === $reqServiceType) {
$matchedTypePrice = isset($st['price']) ? (float)$st['price'] : 0.0;
}
}
// 方案2计价优先级:若规格设置了专属一口价(>0)且当前为默认主服务,则优先采用规格专属价;否则采用所选主服务定价或商品默认价
$isDefaultService = ($reqServiceType === '' || $reqServiceType === $defaultCode || $reqServiceType === 'install');
if ($specPrice > 0 && $isDefaultService) {
$itemPrice = $specPrice;
} elseif ($matchedTypePrice > 0) {
$itemPrice = $matchedTypePrice;
} elseif ($specPrice > 0) {
$itemPrice = $specPrice;
} elseif ((float)$dbSvc['base_price'] > 0) {
$itemPrice = (float)$dbSvc['base_price'];
}
$svcAddons = !empty($dbSvc['addons']) ? (json_decode((string)$dbSvc['addons'], true) ?: []) : [];
foreach ($svcAddons as $ad) {
if (!empty($ad['name']) && isset($ad['price']) && (float)$ad['price'] > 0) {
$dbAddonsMap[(string)$ad['name']] = (float)$ad['price'];
}
if (!empty($ad['code']) && isset($ad['price']) && (float)$ad['price'] > 0) {
$dbAddonsMap[(string)$ad['code']] = (float)$ad['price'];
}
}
}
$itemNum = isset($g['num']) ? max(1, (int)$g['num']) : 1;
$base += $itemPrice * $itemNum;
if (!empty($g['extras']) && is_array($g['extras'])) {
foreach ($g['extras'] as $ex) {
$exPrice = isset($ex['price']) ? (float)$ex['price'] : 0;
$exNum = isset($ex['num']) ? max(0, (int)$ex['num']) : 0;
if ($exNum <= 0) {
continue;
}
$exPrice = isset($ex['price']) ? max(0, (float)$ex['price']) : 0.0;
$exName = isset($ex['name']) ? (string)$ex['name'] : '';
$exCode = isset($ex['code']) ? (string)$ex['code'] : '';
if ($exName !== '' && isset($dbAddonsMap[$exName])) {
$exPrice = $dbAddonsMap[$exName];
} elseif ($exCode !== '' && isset($dbAddonsMap[$exCode])) {
$exPrice = $dbAddonsMap[$exCode];
}
$base += $exPrice * $exNum;
}
}
@@ -842,9 +913,6 @@ class Home extends Base
foreach ($rows as $r) {
$theme = isset($themeText[$r['theme']]) ? $r['theme'] : 'promo';
$stats = $this->parseBannerStats(isset($r['stats']) ? $r['stats'] : '');
if ($theme === 'brand' && empty($stats)) {
$stats = $this->defaultBrandStats();
}
$actionText = trim((string)$r['action_text']);
if ($actionText === '' && $theme !== 'brand') {
$actionText = '立即查看';
@@ -892,47 +960,6 @@ class Home extends Base
return $out;
}
/**
* 平台实力默认指标
*/
protected function defaultBrandStats()
{
return [
['value' => '460万+', 'label' => '专业师傅'],
['value' => '99.9%', 'label' => '区域覆盖到村镇'],
['value' => '1762万', 'label' => '成功订单'],
];
}
/**
* 无后台数据时的兜底轮播
*/
protected function defaultBanners()
{
return [
[
'id' => 0, 'badge' => '家装季', 'title' => '双11 狂欢大促',
'subtitle' => '下单立减 · 满额再返券', 'date_text' => '10.23 - 11.15',
'theme' => 'promo', 'theme_text' => '活动促销', 'action_text' => '立即抢购',
'stats' => [], 'image' => '',
'link' => '/pages/service/list?cat_id=1&cat_name=%E5%AE%B6%E5%85%B7',
],
[
'id' => -1, 'badge' => '平台实力', 'title' => '平台实力',
'subtitle' => '已有 176276994 个订单找到师傅', 'date_text' => '下单越多 · 越优惠',
'theme' => 'brand', 'theme_text' => '平台实力', 'action_text' => '',
'stats' => $this->defaultBrandStats(), 'image' => '', 'link' => '',
],
[
'id' => -2, 'badge' => '服务主题', 'title' => '家电清洗季',
'subtitle' => '空调 / 洗衣机 / 油烟机深度清洗', 'date_text' => '9.9 折起',
'theme' => 'service', 'theme_text' => '服务主题', 'action_text' => '立即预约',
'stats' => [], 'image' => '',
'link' => '/pages/service/list?cat_id=10&cat_name=%E5%AE%B6%E7%94%B5',
],
];
}
/**
* 读取 fa_config 中的逗号分隔列表
*/
@@ -1054,44 +1081,42 @@ class Home extends Base
$serviceTypes = [];
if (!empty($row['service_types'])) {
$serviceTypes = is_array($row['service_types']) ? $row['service_types'] : json_decode($row['service_types'], true);
$serviceTypes = is_array($row['service_types']) ? $row['service_types'] : (json_decode($row['service_types'], true) ?: []);
}
if (empty($serviceTypes)) {
$serviceTypes = $this->resolveServiceTypes($row);
}
$FixedCodes = ['install', 'repair', 'remove'];
$isOtherCat = ((int)$row['cat_id'] === 15);
if (!empty($activeTypeCodes)) {
$filteredTypes = [];
foreach ($serviceTypes as $st) {
$c = isset($st['code']) ? $st['code'] : '';
if ($c !== '' && isset($activeTypeCodes[$c])) {
if (!isset($st['support_fixed'])) {
$st['support_fixed'] = in_array($c, $FixedCodes, true) ? 1 : 0;
$dictSupport = isset($activeTypeCodes[$c]['support_fixed']) ? (int)$activeTypeCodes[$c]['support_fixed'] : 1;
$stPrice = isset($st['price']) ? (float)$st['price'] : 0.0;
if ($isOtherCat || $dictSupport === 0) {
$st['support_fixed'] = 0;
$st['price'] = 0;
} else {
$st['support_fixed'] = $stPrice > 0 ? 1 : 0;
}
$filteredTypes[] = $st;
}
}
if (!empty($filteredTypes)) {
$serviceTypes = $filteredTypes;
}
$serviceTypes = $filteredTypes;
} else {
foreach ($serviceTypes as &$stItem) {
$c = isset($stItem['code']) ? $stItem['code'] : '';
$stPrice = isset($stItem['price']) ? (float)$stItem['price'] : 0.0;
if (!isset($stItem['support_fixed'])) {
$stItem['support_fixed'] = in_array($c, $FixedCodes, true) ? 1 : 0;
$stItem['support_fixed'] = (!$isOtherCat && $stPrice > 0) ? 1 : 0;
}
}
unset($stItem);
}
// 解析增项服务(优先读取商品绑定的 addons JSON,并与统一服务表 fa_service_type 联动过滤)
// 解析增项服务(读取商品绑定的 addons JSON,并与统一服务表 fa_service_type 联动过滤)
$addons = [];
if (!empty($row['addons'])) {
$addons = is_array($row['addons']) ? $row['addons'] : json_decode($row['addons'], true);
$addons = is_array($row['addons']) ? $row['addons'] : (json_decode($row['addons'], true) ?: []);
}
if (empty($addons)) {
$addons = $this->resolveServiceAddons($row);
} elseif (!empty($activeTypeCodes)) {
if (!empty($addons) && !empty($activeTypeCodes)) {
$addons = array_values(array_filter($addons, function ($ad) use ($activeTypeCodes, $activeTypeNames) {
$code = isset($ad['code']) ? $ad['code'] : '';
$name = isset($ad['name']) ? $ad['name'] : '';
@@ -1126,450 +1151,6 @@ class Home extends Base
];
}
/**
* 根据具体商品名称与所属类目返回精准匹配的服务项目列表(1:1 对标奇兵到家图2 & 图3)
*/
protected function resolveServiceTypes($row)
{
$catId = (int)$row['cat_id'];
$name = (string)$row['name'];
$bp = max(30, (float)$row['base_price']);
$make = function ($items) use ($bp) {
$res = [];
$fixedCodes = ['install', 'repair', 'remove'];
foreach ($items as $idx => $it) {
$ratio = isset($it[2]) ? (float)$it[2] : 1.0;
$fixed = isset($it[3]) && $it[3] !== null ? (float)$it[3] : round($bp * $ratio, 2);
$supportFixed = isset($it[4]) ? (int)$it[4] : (in_array($it[0], $fixedCodes, true) ? 1 : 0);
$res[] = [
'code' => $it[0],
'name' => $it[1],
'price' => $fixed,
'is_default' => $idx === 0 ? 1 : 0,
'support_fixed' => $supportFixed,
];
}
return $res;
};
// 类目 15:其它(1:1 复刻图3)
if ($catId === 15) {
return $make([
['install', '安装', 1.0, null, 0],
['repair', '维修', 0.8, null, 0],
['grout', '美缝', 1.0, null, 0],
['glue', '打胶', 0.6, null, 0],
['remove', '拆旧', 0.5, null, 0],
['deliver', '送货到家', 0.7, 50, 0],
['deliver_install', '送货到家并安装', 1.3, null, 0],
['deliver_downstairs', '送货到楼下(不含上楼)', 0.5, 35, 0],
['clean', '保洁', 1.0, null, 0],
['drill', '打孔', 0.5, 30, 0],
['film', '贴膜', 0.8, null, 0],
['move', '移机', 1.1, null, 0],
['other', '其他', 1.0, null, 0],
]);
}
// 类目 6:墙纸/壁画(1:1 复刻图2)
if ($catId === 6) {
if (strpos($name, '画') !== false && strpos($name, '壁画') === false) {
return $make([
['install', '安装', 1.0],
['repair', '维修', 1.15],
['remove', '拆旧', 0.5, 30],
['drill', '打孔', 0.6, 30, 0],
['deliver_install', '送货到家并安装', 1.3, null, 0],
]);
}
return $make([
['install', '安装', 1.0],
['repair', '维修', 1.15],
['deliver', '送货到家', 0.7, 50, 0],
['deliver_install', '送货到家并安装', 1.3, null, 0],
['deliver_downstairs', '送货到楼下(不含上楼)', 0.5, 35, 0],
]);
}
// 类目 1:家具(1:1 复刻视频 00:02 的 11 项服务项目,且安装/维修/拆旧支持一口价,其余自动转报价)
if ($catId === 1) {
$repairPrice = round($bp + 10, 2);
return $make([
['install', '安装', 1.0, $bp, 1],
['repair', '维修', 1.18, $repairPrice, 1],
['remove', '拆旧', 0.9, 50, 1],
['paint_repair', '补漆修复', 1.0, 80, 0],
['measure', '测量', 0.8, 50, 0],
['deliver', '送货到家', 0.7, 60, 0],
['deliver_install', '送货到家并安装', 1.35, round($bp * 1.35, 2), 0],
['deliver_downstairs', '送货到楼下(不含上楼)', 0.5, 40, 0],
['return_goods', '返货', 0.8, 50, 0],
['maintain', '保养', 0.8, 60, 0],
['open_hole', '开孔', 0.5, 30, 0],
]);
}
// 类目 2:卫浴(区分马桶、花洒龙头、浴室柜/浴缸、浴霸)
if ($catId === 2) {
if (preg_match('/(马桶|便器)/u', $name)) {
return $make([
['install', '安装', 1.0],
['repair', '维修', 0.7],
['remove', '拆旧', 0.5],
['glue', '打胶', 0.4, 30],
['grout', '美缝', 0.6, 50],
['deliver_install', '送货到家并安装', 1.3],
]);
}
if (preg_match('/(花洒|龙头|扶手|镜)/u', $name)) {
return $make([
['install', '安装', 1.0],
['repair', '维修', 0.7],
['remove', '拆旧', 0.4],
['drill', '打孔', 0.4, 25],
]);
}
if (preg_match('/(浴霸|水泵)/u', $name)) {
return $make([
['install', '安装', 1.0],
['repair', '维修', 0.75],
['remove', '拆旧', 0.45],
['deliver_install', '送货到家并安装', 1.25],
]);
}
return $make([
['install', '安装', 1.0],
['repair', '维修', 0.7],
['remove', '拆旧', 0.5],
['glue', '打胶', 0.4, 30],
['deliver', '送货到家', 0.6, 50],
['deliver_install', '送货到家并安装', 1.3],
]);
}
// 类目 3:灯具
if ($catId === 3) {
return $make([
['install', '安装', 1.0],
['repair', '维修', 0.7],
['remove', '拆旧', 0.4],
['deliver_install', '送货到家并安装', 1.25],
]);
}
// 类目 4:锁具
if ($catId === 4) {
return $make([
['install', '安装', 1.0],
['repair', '维修', 0.7],
['remove', '拆旧', 0.4],
['drill', '打孔', 0.5, 40],
]);
}
// 类目 5:窗帘
if ($catId === 5) {
return $make([
['install', '安装', 1.0],
['repair', '维修', 0.7],
['measure', '测量', 0.5, 40],
['remove', '拆旧', 0.4],
['deliver_install', '送货到家并安装', 1.25],
]);
}
// 类目 7:净水器
if ($catId === 7) {
return $make([
['install', '安装', 1.0],
['repair', '维修', 0.7],
['filter', '换滤芯', 0.5, 40],
['remove', '拆旧', 0.4],
['move', '移机', 1.1],
]);
}
// 类目 8:晾衣架(区分电动、手摇、户外推拉/落地)
if ($catId === 8) {
if (strpos($name, '电动') !== false) {
return $make([
['install', '安装', 1.0],
['repair', '维修', 0.7],
['remove', '拆旧', 0.4],
['deliver', '送货到家', 0.6, 45],
['deliver_install', '送货到家并安装', 1.25],
['move', '移机', 1.1],
]);
}
if (strpos($name, '户外') !== false || strpos($name, '推拉') !== false) {
return $make([
['install', '安装', 1.0],
['repair', '维修', 0.7],
['remove', '拆旧', 0.4],
['drill', '打孔', 0.5, 30],
]);
}
return $make([
['install', '安装', 1.0],
['repair', '维修', 0.7],
['remove', '拆旧', 0.4],
['deliver', '送货到家', 0.6, 45],
['deliver_install', '送货到家并安装', 1.25],
]);
}
// 类目 9:门窗
if ($catId === 9) {
return $make([
['install', '安装', 1.0],
['repair', '维修', 0.7],
['measure', '测量', 0.4, 50],
['remove', '拆旧', 0.5],
['glue', '打胶', 0.4, 40],
['deliver_install', '送货到家并安装', 1.3],
]);
}
// 类目 10:家电(区分空调、厨电、洗衣机冰箱)
if ($catId === 10) {
if (strpos($name, '空调') !== false) {
return $make([
['install', '安装', 1.0],
['repair', '维修', 0.7],
['move', '移机', 1.2],
['remove', '拆旧', 0.5],
['clean', '保洁清洗', 0.6, 80],
['deliver_install', '送货到家并安装', 1.3],
]);
}
return $make([
['install', '安装', 1.0],
['repair', '维修', 0.7],
['remove', '拆旧', 0.45],
['clean', '保洁清洗', 0.6, 70],
['drill', '打孔', 0.4, 40],
['deliver_install', '送货到家并安装', 1.25],
]);
}
// 类目 11:电视/投影
if ($catId === 11) {
return $make([
['install', '安装', 1.0],
['repair', '维修', 0.7],
['move', '移机', 1.0],
['remove', '拆旧', 0.4],
['deliver_install', '送货到家并安装', 1.25],
]);
}
// 类目 12:淋浴房
if ($catId === 12) {
return $make([
['install', '安装', 1.0],
['repair', '维修', 0.7],
['measure', '测量', 0.3, 50],
['remove', '拆旧', 0.45],
['glue', '打胶', 0.3, 40],
['film', '贴膜', 0.4, 80],
['deliver_install', '送货到家并安装', 1.3],
]);
}
// 类目 13:商用厨电
if ($catId === 13) {
return $make([
['install', '安装', 1.0],
['repair', '维修', 0.7],
['clean', '保洁', 0.8],
['remove', '拆旧', 0.5],
['deliver_install', '送货到家并安装', 1.25],
]);
}
// 类目 14:健身器材
if ($catId === 14) {
return $make([
['install', '安装', 1.0],
['repair', '维修', 0.7],
['move', '移机', 1.1],
['remove', '拆旧', 0.5],
['deliver', '送货到家', 0.6, 60],
['deliver_install', '送货到家并安装', 1.3],
]);
}
return $make([
['install', '安装', 1.0],
['repair', '维修', 0.7],
['remove', '拆旧', 0.5],
['deliver', '送货到家', 0.6, 50],
['deliver_install', '送货到家并安装', 1.3],
]);
}
/**
* 根据具体商品名称与所属类目返回极简增值服务(增项)列表(1:1 对标奇兵到家:仅保留“拆旧”、“切割”等 1~2 项极简短词)
*/
protected function resolveServiceAddons($row)
{
$catId = (int)$row['cat_id'];
$name = (string)$row['name'];
$codeMap = [
'拆旧' => 'remove',
'切割' => 'cut',
'打孔' => 'drill',
'开孔' => 'open_hole',
];
$build = function ($items) use ($codeMap) {
$res = [];
foreach ($items as $idx => $it) {
$res[] = [
'id' => $idx + 1,
'code' => isset($codeMap[$it[0]]) ? $codeMap[$it[0]] : ('addon_' . ($idx + 1)),
'name' => $it[0],
'unit' => $it[1],
'price' => (float)$it[2],
];
}
return $res;
};
// 类目 1:家具(对标视频 00:06 / 00:17:电脑桌仅“拆旧 50元/份”,柜类多一项“切割 20元/份”)
if ($catId === 1) {
if (preg_match('/(柜)/u', $name)) {
return $build([
['拆旧', '份', 50],
['切割', '份', 20],
]);
}
return $build([
['拆旧', '份', 50],
]);
}
// 类目 2:卫浴(对标奇兵到家截图 frame_010:洗衣柜/浴室柜为“拆旧 50元/份”、“切割 20元/份”,马桶/花洒为“拆旧”)
if ($catId === 2) {
if (preg_match('/(浴室柜|洗衣柜|面盆|洗面盆|水槽|拖把池)/u', $name)) {
return $build([
['拆旧', '份', 50],
['切割', '份', 20],
]);
}
if (preg_match('/(马桶|便器)/u', $name)) {
return $build([
['拆旧', '份', 50],
]);
}
if (preg_match('/(浴缸)/u', $name)) {
return $build([
['拆旧', '份', 80],
]);
}
return $build([
['拆旧', '份', 30],
]);
}
// 类目 3:灯具
if ($catId === 3) {
return $build([
['拆旧', '份', 20],
]);
}
// 类目 4:锁具
if ($catId === 4) {
return $build([
['拆旧', '份', 30],
['开孔', '份', 30],
]);
}
// 类目 5:窗帘
if ($catId === 5) {
return $build([
['拆旧', '份', 30],
]);
}
// 类目 6:墙纸/壁画
if ($catId === 6) {
return $build([
['拆旧', '份', 30],
]);
}
// 类目 7:净水器
if ($catId === 7) {
return $build([
['拆旧', '份', 30],
['打孔', '份', 30],
]);
}
// 类目 8:晾衣架
if ($catId === 8) {
return $build([
['拆旧', '份', 30],
]);
}
// 类目 9:门窗
if ($catId === 9) {
return $build([
['拆旧', '份', 50],
]);
}
// 类目 10:家电
if ($catId === 10) {
if (strpos($name, '空调') !== false) {
return $build([
['拆旧', '份', 80],
['打孔', '份', 50],
]);
}
return $build([
['拆旧', '份', 40],
]);
}
// 类目 11:电视/投影
if ($catId === 11) {
return $build([
['拆旧', '份', 30],
]);
}
// 类目 12:淋浴房
if ($catId === 12) {
return $build([
['拆旧', '份', 80],
]);
}
// 类目 13:商用厨电
if ($catId === 13) {
return $build([
['拆旧', '份', 80],
]);
}
// 类目 14:健身器材
if ($catId === 14) {
return $build([
['拆旧', '份', 60],
]);
}
return $build([
['拆旧', '份', 30],
]);
}
/**
* 相对时间计算(更具真实动态感)
*/