feat: 完善协议管理、师傅资质认证、分账配置并完成测试环境全量部署与同步

这个提交包含在:
2026-11-12 17:19:49 +08:00
父节点 fafddceaf2
当前提交 72cea8445f
共有 179 个文件被更改,包括 29242 次插入 和 3340 次删除

查看文件

@@ -9,7 +9,7 @@ use think\Db;
*/
class Home extends Base
{
protected $noNeedLogin = ['index', 'feed', 'cats', 'services', 'detail', 'calc', 'specs', 'masterCount', 'orderCase', 'agreement', 'systemGallery'];
protected $noNeedLogin = ['index', 'feed', 'cats', 'services', 'detail', 'calc', 'specs', 'masterCount', 'orderCase', 'agreement', 'agreementRead', 'systemGallery'];
/**
* 首页聚合:类目 + 热门服务 + 我的角标
@@ -151,15 +151,69 @@ class Home extends Base
];
}
// 聚合在售服务的真实计价单位列表(彻底由数据库驱动,按商品数量降序)
$unitRows = Db::name('service')
->field('unit, COUNT(*) AS cnt, MIN(id) AS min_id')
->where('status', 'normal')
->where('cat_id', '<>', 15)
->where('unit', '<>', '')
->group('unit')
->order('cnt desc, min_id asc')
->select();
$filterUnits = [];
foreach ($unitRows as $ur) {
$uName = trim((string)($ur['unit'] ?? ''));
if ($uName !== '') {
$filterUnits[] = [
'label' => '按' . $uName . '计价',
'value' => $uName,
];
}
}
// 从 fa_config 读取价格筛选项 JSON 与晒单评价标签配置
$priceRangesRaw = Db::name('config')->where('name', 'home_price_ranges')->value('value');
if ($priceRangesRaw === null || trim((string)$priceRangesRaw) === '') {
$priceRangesRaw = \think\Config::get('site.home_price_ranges');
}
$filterPriceRanges = [];
if (is_string($priceRangesRaw) && trim($priceRangesRaw) !== '') {
$decodedPr = json_decode(htmlspecialchars_decode(trim($priceRangesRaw)), true);
if (is_array($decodedPr)) {
foreach ($decodedPr as $prItem) {
if (is_array($prItem) && !empty($prItem['label']) && !empty($prItem['value'])) {
$filterPriceRanges[] = [
'label' => (string)$prItem['label'],
'value' => (string)$prItem['value'],
];
}
}
}
}
$tagCfg = $this->configList('review_tags');
$filterCaseTags = [];
foreach ($tagCfg as $tc) {
$filterCaseTags[] = [
'label' => $tc,
'value' => $tc,
];
}
$data = [
'cats' => $cats,
'cat_services' => $catServices,
'showcase_services' => $showcaseServices,
'service_types_dict' => $serviceTypesDict,
'regions' => $regions,
'filter_options' => [
'units' => $filterUnits,
'price_ranges' => $filterPriceRanges,
'case_tags' => $filterCaseTags,
],
'hot' => $this->formatServices($hotList),
'banners' => $this->banners(),
'assures' => $this->configList('home_assure', '平台认证,先行赔付,极速上门,售后无忧'),
'assures' => $this->configList('home_assure'),
'tutorial' => $this->tutorial(),
'service_phone' => (string)(\think\Config::get('site.service_phone') ?: ''),
'badge' => ['coupon' => 0, 'unread' => 0],
@@ -190,16 +244,16 @@ class Home extends Base
$recentOrders[] = [
'id' => (int)$sc['id'],
'service_id' => (int)$sc['service_id'],
'service_name' => $sc['service_name'] ?: '上门服务',
'cat_name' => $sc['cat_name'] ?: '服务',
'service_name' => (string)$sc['service_name'],
'cat_name' => (string)$sc['cat_name'],
'cat_id' => (int)$sc['cat_id'],
'image_url' => $sc['cover_image'] ? cdnurl($sc['cover_image'], true) : '',
'status_text' => '业主晒单',
'location' => $sc['region'] ?: '同城服务',
'location' => (string)$sc['region'],
'time_ago' => $this->timeAgo((int)$sc['createtime']),
'user_name' => $sc['user_name'] ?: '匿名业主',
'review_score' => (int)$sc['score'] ?: 5,
'review_content' => $sc['content'] ?: '师傅准时上门,施工专业细致,安装效果很满意!',
'user_name' => (string)$sc['user_name'],
'review_score' => (int)$sc['score'],
'review_content' => (string)$sc['content'],
'review_tags' => array_slice($revTags, 0, 2),
];
}
@@ -299,16 +353,16 @@ class Home extends Base
$list[] = [
'id' => (int)$sc['id'],
'service_id' => (int)$sc['service_id'],
'service_name' => $sc['service_name'] ?: '上门服务',
'cat_name' => $sc['cat_name'] ?: '服务',
'service_name' => (string)$sc['service_name'],
'cat_name' => (string)$sc['cat_name'],
'cat_id' => (int)$sc['cat_id'],
'image_url' => $sc['cover_image'] ? cdnurl($sc['cover_image'], true) : '',
'status_text' => '业主晒单',
'location' => $sc['region'] ?: '同城服务',
'location' => (string)$sc['region'],
'time_ago' => $this->timeAgo((int)$sc['createtime']),
'user_name' => $sc['user_name'] ?: '匿名业主',
'review_score' => (int)$sc['score'] ?: 5,
'review_content' => $sc['content'] ?: '师傅准时上门,施工专业细致,安装效果很满意!',
'user_name' => (string)$sc['user_name'],
'review_score' => (int)$sc['score'],
'review_content' => (string)$sc['content'],
'review_tags' => array_slice($revTags, 0, 3),
];
}
@@ -970,8 +1024,8 @@ class Home extends Base
'discount_amount' => round($discount, 2),
'pay_amount' => max(0, round($total - $discount, 2)),
'coupon' => $couponInfo,
'free_floor' => $type === 'custom' ? 0 : ($service ? (int)$service['free_floor'] : 2),
'floor_fee' => $type === 'custom' ? 0 : round($service ? (float)$service['floor_fee'] : 10.0, 2),
'free_floor' => $type === 'custom' ? 0 : ($service ? (int)$service['free_floor'] : 0),
'floor_fee' => $type === 'custom' ? 0 : round($service ? (float)$service['floor_fee'] : 0.0, 2),
];
}
@@ -1048,13 +1102,16 @@ class Home extends Base
}
/**
* 读取 fa_config 中的逗号分隔列表
* 读取 fa_config 中的逗号分隔列表(完全由真实库配置与站点配置驱动)
*/
protected function configList($name, $default = '')
protected function configList($name)
{
$v = Db::name('config')->where('name', $name)->value('value');
if ($v === null || trim((string)$v) === '') {
$v = \think\Config::get('site.' . $name);
}
if (!$v) {
$v = $default;
return [];
}
return array_values(array_filter(array_map('trim', explode(',', (string)$v))));
}
@@ -1065,8 +1122,11 @@ class Home extends Base
protected function tutorial()
{
$v = Db::name('config')->where('name', 'home_tutorial')->value('value');
if ($v === null || trim((string)$v) === '') {
$v = \think\Config::get('site.home_tutorial');
}
if (!$v) {
return ['title' => '下单找师傅教程', 'sub' => '3 分钟看懂,下单不踩坑'];
return ['title' => '', 'sub' => ''];
}
$parts = explode('|', (string)$v);
return [
@@ -1274,7 +1334,7 @@ class Home extends Base
}
$service = Db::name('service')->where('id', (int)$sc['service_id'])->find();
$basePrice = ($service && (float)$service['base_price'] > 0) ? (float)$service['base_price'] : 50.00;
$basePrice = $service ? (float)$service['base_price'] : 0.00;
$imageUrl = $sc['cover_image'] ? cdnurl($sc['cover_image'], true) : '';
$tags = [];
@@ -1303,9 +1363,9 @@ class Home extends Base
}
}
$dateText = !empty($sc['createtime']) ? date('Y-m-d', (int)$sc['createtime']) : date('Y-m-d');
$catName = $sc['cat_name'] ?: '上门服务';
$serviceName = $sc['service_name'] ?: '上门服务';
$dateText = !empty($sc['createtime']) ? date('Y-m-d', (int)$sc['createtime']) : '';
$catName = (string)$sc['cat_name'];
$serviceName = (string)$sc['service_name'];
$this->success('', [
'order' => [
@@ -1316,21 +1376,21 @@ class Home extends Base
'cat_name' => $catName,
'image_url' => $imageUrl,
'status_text' => '业主晒单',
'location' => $sc['region'] ?: '同城服务',
'location' => (string)$sc['region'],
'finish_text' => $dateText,
'media' => $mediaList,
],
'master' => [
'realname' => $sc['master_name'] ?: '优匠师傅',
'score' => (float)($sc['master_score'] > 0 ? $sc['master_score'] : 5.0),
'realname' => (string)$sc['master_name'],
'score' => (float)$sc['master_score'],
],
'review' => [
'id' => (int)$sc['id'],
'score' => (int)$sc['score'] ?: 5,
'score' => (int)$sc['score'],
'tags' => $tags,
'content' => $sc['content'] ?: '师傅很专业,准时上门,施工规范细致,验收很满意!',
'content' => (string)$sc['content'],
'images' => $rImgs,
'user_name' => $sc['user_name'] ?: '匿名业主',
'user_name' => (string)$sc['user_name'],
'user_avatar' => $sc['user_avatar'] ? cdnurl($sc['user_avatar'], true) : '',
'time_text' => $dateText,
],
@@ -1346,52 +1406,35 @@ class Home extends Base
}
/**
* 平台协议与合规条款读取(支持后台站点配置覆盖)
* 平台协议与合规条款读取(优先读取后台「协议与隐私」实时配置,结合用户已读修订号返回首看变更标识)
*/
public function agreement()
{
$type = trim((string)$this->request->param('type', 'user_service'));
$map = [
'service' => [
'key' => 'agreement_service',
'title' => '优匠优服用户服务协议',
'content' => "1. 平台定位:优匠优服为客户与认证技能师傅提供上门安装、维修、拆旧及维保信息撮合与交易担保服务。\n\n2. 计费准则:一口价项目按公示标准计费;报价订单由师傅根据现场工况公开报价,经客户确认支付后生效。\n\n3. 验收与结算:订单资金由平台托管,师傅完工拍照并经客户确认(或提供6位核销码)后完成结算。\n\n4. 交易规范:为保障售后质保与资金安全,请勿脱离平台私下转账交易。",
],
'user_service' => [
'key' => 'agreement_service',
'title' => '优匠优服用户服务协议',
'content' => "1. 平台定位:优匠优服为客户与认证技能师傅提供上门安装、维修、拆旧及维保信息撮合与交易担保服务。\n\n2. 计费准则:一口价项目按公示标准计费;报价订单由师傅根据现场工况公开报价,经客户确认支付后生效。\n\n3. 验收与结算:订单资金由平台托管,师傅完工拍照并经客户确认(或提供6位核销码)后完成结算。\n\n4. 交易规范:为保障售后质保与资金安全,请勿脱离平台私下转账交易。",
],
'privacy' => [
'key' => 'agreement_privacy',
'title' => '优匠优服隐私保护指引',
'content' => "1. 信息收集范围:仅在您授权登录、预约上门地址及联系师傅等必要场景下收集手机号码与服务地址信息。\n\n2. 最小必要原则:客户真实联系信息仅对已接单履约的当单认证师傅可见,未接单前全程脱敏展示。\n\n3. 数据传输安全:全链路采用加密通信并严格管控访问权限,绝不向无关第三方共享或出售个人信息。\n\n4. 用户权利:您可随时在个人中心查询、修改账户资料及管理服务地址。",
],
'master' => [
'key' => 'agreement_master',
'title' => '师傅入驻与履约服务协议',
'content' => "1. 实名与技能核验:入驻师傅须提交本人真实有效身份证件及服务技能信息,严禁冒用他人资质。\n\n2. 履约时效:接单后须在平台规定时效内联系客户确认上门时间,准时到达现场并规范施工。\n\n3. 收费透明:如需现场增项补差,必须提前向客户说明并在工单内发起补差申请,严禁私自索要额外费用。\n\n4. 完工留痕与结算:施工完成后须上传真实清晰的现场完工照片,凭客户验收或核销码触发系统结算。",
],
'merchant' => [
'key' => 'agreement_merchant',
'title' => '企业商家入驻与协同服务协议',
'content' => "1. 资质真实性:申请入驻的企业或商户须提供真实有效的营业执照与主体信息。\n\n2. 成员协同管理:主账号可绑定企业协同成员共同发起批量工单与跟进履约,并对企业关联工单承担管理责任。\n\n3. 对账与开票:平台按实际支付与完工订单汇总企业月度账单,支持在线申请正规增值税普通发票或专用发票。",
],
];
$item = \app\common\library\AgreementService::getByType($type, $this->uid());
if (!$item) {
$this->error('协议内容暂未配置或已下线');
}
$item = isset($map[$type]) ? $map[$type] : $map['user_service'];
$cfgVal = \think\Config::get('site.' . $item['key']);
$content = (is_string($cfgVal) && trim($cfgVal) !== '') ? trim($cfgVal) : $item['content'];
$this->success('', [
'type' => $type,
'title' => $item['title'],
'content' => $content,
]);
$this->success('', $item);
}
/**
* 官方系统图库(二级商品分类侧边栏 + 款式参考图片双列网格)
* 记录用户已首次查看指定修订版本的协议(后续打开不再标背景色)
*/
public function agreementRead()
{
if (false === $this->request->isPost()) {
$this->error('请求方式错误');
}
$type = trim((string)$this->request->post('type', 'privacy'));
$revision = (int)$this->request->post('revision', 0);
$res = \app\common\library\AgreementService::markUserRead($this->uid(), $type, $revision);
$this->success('', $res);
}
/**
* 官方系统图库(受下单所选类目控制,仅展示对应类目下的商品及参考图片;同源聚合系统图库表 + 细分规格图 + 商品主缩略图)
*/
public function systemGallery()
{
@@ -1417,6 +1460,14 @@ class Home extends Base
$catId = (int)$cats[0]['id'];
}
$currentCatName = '';
foreach ($cats as $c) {
if ((int)$c['id'] === $catId) {
$currentCatName = (string)$c['name'];
break;
}
}
$svcQuery = Db::name('service')
->field('id,cat_id,name,image')
->where('status', 'normal')
@@ -1428,6 +1479,7 @@ class Home extends Base
$svcIds = array_column($svcRows, 'id');
$galleryMap = [];
$specMap = [];
if (!empty($svcIds)) {
$gQuery = Db::name('system_gallery')
->where('service_id', 'in', $svcIds)
@@ -1450,34 +1502,96 @@ class Home extends Base
'image_url' => strpos($rawImg, 'http') === 0 ? $rawImg : cdnurl($rawImg, true),
];
}
// 一次性批量查询所有关联商品的细分规格配图(严禁循环内查库)
$spRows = Db::name('service_spec')
->field('id,service_id,name,image')
->where('service_id', 'in', $svcIds)
->where('status', 'normal')
->order('weigh desc,id asc')
->select();
foreach ($spRows as $sp) {
$sid = (int)$sp['service_id'];
$specMap[$sid][] = $sp;
}
}
// 若个别服务尚未写入 fa_system_gallery,回退读取真实规格与服务主图
// 智能同源聚合:1. 系统图库表记录 -> 2. 细分规格独立配图 -> 3. 商品后台主缩略图兜底(单张不重复)
$services = [];
foreach ($svcRows as $sr) {
$sid = (int)$sr['id'];
$items = isset($galleryMap[$sid]) ? $galleryMap[$sid] : [];
$items = [];
$seenImgUrls = [];
$seenBasenames = [];
static $isValidGalleryUrl = null;
if ($isValidGalleryUrl === null) {
$isValidGalleryUrl = static function (string $url): bool {
$base = strstr($url, '?', true);
$clean = strtolower(trim($base !== false ? $base : $url));
if ($clean === '' || substr($clean, -4) === '.svg' || strpos($clean, '/icon/') !== false) {
return false;
}
return true;
};
}
$registerUniqueUrl = static function (string $url) use (&$seenImgUrls, &$seenBasenames, $isValidGalleryUrl): ?string {
$u = trim($url);
if ($u === '' || !$isValidGalleryUrl($u)) {
return null;
}
$pathOnly = parse_url($u, PHP_URL_PATH);
$fname = $pathOnly ? strtolower(basename($pathOnly)) : strtolower($u);
if (isset($seenImgUrls[$u]) || ($fname !== '' && isset($seenBasenames[$fname]))) {
return null;
}
$seenImgUrls[$u] = true;
if ($fname !== '') {
$seenBasenames[$fname] = true;
}
return $u;
};
if (!empty($galleryMap[$sid])) {
foreach ($galleryMap[$sid] as $gItem) {
$u = $registerUniqueUrl((string)($gItem['image_url'] ?? ''));
if ($u === null) {
continue;
}
$gItem['image_url'] = $u;
$items[] = $gItem;
}
}
// 仅当系统图库未单独配置该商品图集时,回退读取商品细分规格参考图
if (empty($items) && !empty($specMap[$sid])) {
foreach ($specMap[$sid] as $sp) {
$spImg = !empty($sp['image']) ? trim((string)$sp['image']) : '';
if ($spImg === '' || !$isValidGalleryUrl($spImg)) {
continue;
}
if ($keyword !== '' && mb_strpos((string)$sp['name'], $keyword) === false) {
continue;
}
$fullSpUrl = strpos($spImg, 'http') === 0 ? $spImg : cdnurl($spImg, true);
$u = $registerUniqueUrl($fullSpUrl);
if ($u !== null) {
$items[] = [
'id' => 100000 + (int)$sp['id'],
'service_id' => $sid,
'spec_id' => (int)$sp['id'],
'title' => (string)$sp['name'],
'image_url' => $u,
];
}
}
}
// 若后台仅在添加商品时上传了1张商品缩略图(未配额外图库与规格图),直接展示该商品主图
if (empty($items) && $keyword === '') {
$defaultImg = trim((string)$sr['image']);
if ($defaultImg !== '') {
if ($defaultImg !== '' && $isValidGalleryUrl($defaultImg)) {
$fullDef = strpos($defaultImg, 'http') === 0 ? $defaultImg : cdnurl($defaultImg, true);
$spList = Db::name('service_spec')
->where('service_id', $sid)
->where('status', 'normal')
->order('weigh desc,id asc')
->select();
if (!empty($spList)) {
foreach ($spList as $sp) {
$spImg = !empty($sp['image']) ? trim((string)$sp['image']) : '';
$items[] = [
'id' => 100000 + (int)$sp['id'],
'service_id' => $sid,
'spec_id' => (int)$sp['id'],
'title' => (string)$sp['name'],
'image_url' => $spImg !== '' ? (strpos($spImg, 'http') === 0 ? $spImg : cdnurl($spImg, true)) : $fullDef,
];
}
} else {
if ($isValidGalleryUrl($fullDef)) {
$items[] = [
'id' => 200000 + $sid,
'service_id' => $sid,
@@ -1488,6 +1602,7 @@ class Home extends Base
}
}
}
if ($keyword !== '' && empty($items) && mb_strpos((string)$sr['name'], $keyword) === false) {
continue;
}
@@ -1503,9 +1618,10 @@ class Home extends Base
}
$this->success('', [
'cats' => $cats,
'current_cat_id' => $catId,
'services' => $services,
'cats' => $cats,
'current_cat_id' => $catId,
'current_cat_name' => $currentCatName,
'services' => $services,
]);
}
}