feat: 新增地址选择插件及订阅消息等多项功能优化
- 新增地址选择插件,支持地图位置选择并返回地址和经纬度 - 新增微信订阅消息发送功能,支持审核结果、提现通知等场景 - 新增订单支付状态查询、提现收款人姓名保存等接口 - 新增服务区域管理,支持按省市区划分并同步到信息发布 - 新增资金流水和营收报表导出功能 - 新增可用优惠券查询、订单退款拆分处理等功能 - 优化后台角标显示、首页数据统计、UI配置下发等 - 修复订单支付并发问题、地区管理逻辑等
这个提交包含在:
@@ -11,7 +11,8 @@ use think\Db;
|
||||
* GET /api/v1/home/categories H2 分类导航
|
||||
* GET /api/v1/home/search/hot H3 热门搜索
|
||||
* GET /api/v1/home/feed H4 信息流(支持 lat/lng + sort=nearby 距离排序)
|
||||
* GET /api/v1/home/areas H5 区域列表(用户端下拉筛选,读后台「区域管理」)
|
||||
* GET /api/v1/home/areas H5 服务区域列表(平铺省/市/区县,区县为服务区域)
|
||||
* GET /api/v1/home/locate H6 按定位坐标匹配服务区域
|
||||
*/
|
||||
class Home extends V1
|
||||
{
|
||||
@@ -90,28 +91,81 @@ class Home extends V1
|
||||
}
|
||||
|
||||
/**
|
||||
* H5 区域列表(用户端筛选用)
|
||||
* GET /api/v1/home/areas → [string]
|
||||
* 规则:仅 enabled=1,按 sort asc;与后台「区域管理」(fc_area) 同源,
|
||||
* 前端首页/信息列表/发布页的「区域」选项均由此下发,不再前端写死。
|
||||
* H5 服务区域列表(平铺:每条记录自带省 / 市 / 区县)
|
||||
* GET /api/v1/home/areas → [{id,province,city,district,lat,lng}]
|
||||
* 规则:仅 enabled=1;按省、市、sort、id 排序。
|
||||
* 区县为最终服务区域;省 / 市由前端按 distinct 值生成级联列。
|
||||
*/
|
||||
public function areas()
|
||||
{
|
||||
$rows = Db::name('fc_area')
|
||||
->where('enabled', 1)
|
||||
->order('sort asc')
|
||||
->order('province asc, city asc, sort asc, id asc')
|
||||
->select();
|
||||
|
||||
$list = [];
|
||||
foreach ($rows as $row) {
|
||||
$list[] = (string)$row['name'];
|
||||
$list[] = [
|
||||
'id' => (int)$row['id'],
|
||||
'province' => (string)$row['province'],
|
||||
'city' => (string)$row['city'],
|
||||
'district' => (string)$row['district'],
|
||||
'lat' => $row['lat'] !== null ? (float)$row['lat'] : null,
|
||||
'lng' => $row['lng'] !== null ? (float)$row['lng'] : null,
|
||||
];
|
||||
}
|
||||
|
||||
$this->ok($list);
|
||||
}
|
||||
|
||||
/**
|
||||
* H6 按定位坐标匹配服务区域(区县)
|
||||
* GET /api/v1/home/locate?lat=&lng= → {areaId,province,city,district,distance}
|
||||
* 规则:在启用的服务区域中心点中取最近的一个(Haversine),超出 300km 视为不在服务范围,返回 null。
|
||||
*/
|
||||
public function locate()
|
||||
{
|
||||
$lat = $this->input('lat', null);
|
||||
$lng = $this->input('lng', null);
|
||||
if (!is_numeric($lat) || !is_numeric($lng)) {
|
||||
$this->ok(null);
|
||||
}
|
||||
$lat = (float)$lat;
|
||||
$lng = (float)$lng;
|
||||
if ($lat < -90 || $lat > 90 || $lng < -180 || $lng > 180 || ($lat == 0.0 && $lng == 0.0)) {
|
||||
$this->ok(null);
|
||||
}
|
||||
|
||||
$rows = Db::name('fc_area')
|
||||
->where('enabled', 1)
|
||||
->whereNotNull('lat')
|
||||
->whereNotNull('lng')
|
||||
->select();
|
||||
|
||||
$best = null;
|
||||
foreach ($rows as $row) {
|
||||
$distance = $this->haversine($lat, $lng, (float)$row['lat'], (float)$row['lng']);
|
||||
if ($best === null || $distance < $best['distance']) {
|
||||
$best = ['row' => $row, 'distance' => $distance];
|
||||
}
|
||||
}
|
||||
if (!$best || $best['distance'] > 300) {
|
||||
$this->ok(null);
|
||||
}
|
||||
|
||||
$area = $best['row'];
|
||||
$this->ok([
|
||||
'areaId' => (int)$area['id'],
|
||||
'province' => (string)$area['province'],
|
||||
'city' => (string)$area['city'],
|
||||
'district' => (string)$area['district'],
|
||||
'distance' => $best['distance'],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* H4 信息流
|
||||
* GET /api/v1/home/feed {categoryId?,keyword?,area?,sort?,lat?,lng?,page,size}
|
||||
* GET /api/v1/home/feed {categoryId?,keyword?,areaId?,area?,sort?,lat?,lng?,page,size}
|
||||
* → {list,page,size,total,hasMore}
|
||||
* 规则:仅 audit_status=1 AND status=1 AND deletetime IS NULL
|
||||
* - sort=nearby 时按距离升序(无坐标的信息排最后;请求未带 lat/lng 则回落发布时间倒序);
|
||||
@@ -124,6 +178,11 @@ class Home extends V1
|
||||
$categoryId = (int)$this->input('categoryId', 0);
|
||||
$keyword = trim((string)$this->input('keyword', ''));
|
||||
$area = trim((string)$this->input('area', ''));
|
||||
// 服务区域 ID(兼容上一版的 districtId 参数)
|
||||
$areaId = (int)$this->input('areaId', 0);
|
||||
if (!$areaId) {
|
||||
$areaId = (int)$this->input('districtId', 0);
|
||||
}
|
||||
$sort = (string)$this->input('sort', '');
|
||||
// 容错处理:过滤前端小程序可能序列化出的 undefined 与 null 字符串
|
||||
if ($area === 'undefined' || $area === 'null') {
|
||||
@@ -146,9 +205,22 @@ class Home extends V1
|
||||
if ($keyword !== '') {
|
||||
$query->where('title', 'like', '%' . $keyword . '%');
|
||||
}
|
||||
if ($area !== '') {
|
||||
$query->where('area', $area);
|
||||
}
|
||||
$applyArea = function ($q) use ($area, $areaId) {
|
||||
// 服务区域 = 区县:areaId 优先,兼容旧的 area=区县名称
|
||||
if ($areaId > 0) {
|
||||
$row = Db::name('fc_area')->where('id', $areaId)->find();
|
||||
if ($row) {
|
||||
$q->where(function ($w) use ($row) {
|
||||
$w->where('district_id', (int)$row['id'])->whereOr('area', (string)$row['district']);
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
if ($area !== '') {
|
||||
$q->where('area', $area);
|
||||
}
|
||||
};
|
||||
$applyArea($query);
|
||||
|
||||
$counter = Db::name('fc_post')
|
||||
->where('audit_status', 1)
|
||||
@@ -160,9 +232,7 @@ class Home extends V1
|
||||
if ($keyword !== '') {
|
||||
$counter->where('title', 'like', '%' . $keyword . '%');
|
||||
}
|
||||
if ($area !== '') {
|
||||
$counter->where('area', $area);
|
||||
}
|
||||
$applyArea($counter);
|
||||
$total = $counter->count();
|
||||
|
||||
// 「附近」:距离在 PHP 内用 Haversine 计算后排序(不用 SQL 距离函数,避免 MySQL 兼容问题);
|
||||
@@ -306,6 +376,7 @@ class Home extends V1
|
||||
'price' => (float)$row['price'],
|
||||
'priceUnit' => (string)$row['price_unit'],
|
||||
'area' => (string)$row['area'],
|
||||
'areaId' => (int)$row['district_id'],
|
||||
'address' => (string)$row['address'],
|
||||
'contact' => $this->maskPhone($row['phone']),
|
||||
'views' => (int)$row['views'],
|
||||
|
||||
在新工单中引用
屏蔽一个用户