feat: 新增地址选择插件及订阅消息等多项功能优化

- 新增地址选择插件,支持地图位置选择并返回地址和经纬度
- 新增微信订阅消息发送功能,支持审核结果、提现通知等场景
- 新增订单支付状态查询、提现收款人姓名保存等接口
- 新增服务区域管理,支持按省市区划分并同步到信息发布
- 新增资金流水和营收报表导出功能
- 新增可用优惠券查询、订单退款拆分处理等功能
- 优化后台角标显示、首页数据统计、UI配置下发等
- 修复订单支付并发问题、地区管理逻辑等
这个提交包含在:
2026-10-22 17:14:56 +08:00
父节点 088a08d43a
当前提交 cb12bf5fc8
共有 180 个文件被更改,包括 9695 次插入 和 1680 次删除

查看文件

@@ -18,6 +18,13 @@ use think\Db;
*/
class AdminSetting extends V1
{
/** 管理端接口必须校验管理员身份(V1 默认关闭了 URI 权限校验) */
public function _initialize()
{
parent::_initialize();
$this->adminOnly();
}
/**
* AS1 基础配置
* GET → {appName,logo,servicePhone,aboutUs,shareTitle}
@@ -30,11 +37,15 @@ class AdminSetting extends V1
if ($appName === '') {
$this->fail('小程序名称不能为空');
}
$logo = (string)$this->input('logo', $this->cfg('base.logo', ''));
$this->setCfg('base.appName', $appName);
$this->setCfg('base.logo', (string)$this->input('logo', $this->cfg('base.logo', '')));
$this->setCfg('base.logo', $logo);
$this->setCfg('base.servicePhone', (string)$this->input('servicePhone', $this->cfg('base.servicePhone', '400-000-0000')));
$this->setCfg('base.aboutUs', (string)$this->input('aboutUs', $this->cfg('base.aboutUs', '')));
$this->setCfg('base.shareTitle', (string)$this->input('shareTitle', $this->cfg('base.shareTitle', '')));
// 兼容旧 ui.* 键,保持名称/LOGO 一致
$this->setCfg('ui.siteName', $appName);
$this->setCfg('ui.logo', $logo);
$this->adminLog('修改基础配置');
}
@@ -116,20 +127,26 @@ class AdminSetting extends V1
}
/**
* AS4 区域列表
* GET /api/v1/admin/setting/areas → 数组 [{id,name,sort,enabled}]
* AS4 服务区域列表(平铺:每条记录自带省 / 市 / 区县)
* GET /api/v1/admin/setting/areas → [{id,province,city,district,fullName,lat,lng,sort,enabled}]
* 规则:按 province asc, city asc, sort asc, id asc;返回全量(含停用)。
*/
public function areas()
{
$rows = Db::name('fc_area')->order('sort asc')->select();
$rows = Db::name('fc_area')->order('province asc, city asc, sort asc, id asc')->select();
$list = [];
foreach ($rows as $row) {
$list[] = [
'id' => (int)$row['id'],
'name' => (string)$row['name'],
'sort' => (int)$row['sort'],
'enabled' => (bool)$row['enabled'],
'id' => (int)$row['id'],
'province' => (string)$row['province'],
'city' => (string)$row['city'],
'district' => (string)$row['district'],
'fullName' => $row['province'] . ' / ' . $row['city'] . ' / ' . $row['district'],
'lat' => $row['lat'] !== null ? (float)$row['lat'] : null,
'lng' => $row['lng'] !== null ? (float)$row['lng'] : null,
'sort' => (int)$row['sort'],
'enabled' => (bool)$row['enabled'],
];
}
@@ -137,35 +154,82 @@ class AdminSetting extends V1
}
/**
* AS5 保存区域
* POST /api/v1/admin/setting/area 入参 {id?,name,sort,enabled} → {id,name}
* AS5 保存服务区域
* POST /api/v1/admin/setting/area 入参 {id?,province,city,district,sort,enabled,lat?,lng?} → {id,fullName}
* 规则:省 / 市 / 区县均必填;同一「省+市+区县」不允许重复。
*/
public function saveArea()
{
$id = (int)$this->input('id', 0);
$name = trim((string)$this->input('name', ''));
$province = trim((string)$this->input('province', ''));
$city = trim((string)$this->input('city', ''));
$district = trim((string)$this->input('district', ''));
$sort = (int)$this->input('sort', 0);
$enabled = (bool)$this->input('enabled', true);
$lat = $this->input('lat', null);
$lng = $this->input('lng', null);
if ($name === '') {
$this->fail('区域名称不能为空');
$values = ['省份' => $province, '城市' => $city, '区县' => $district];
foreach ($values as $label => $value) {
if ($value === '') {
$this->fail($label . '不能为空');
}
if (mb_strlen($value) > 32) {
$this->fail($label . '不能超过 32 个字符');
}
}
// 同一条服务区域(省+市+区县)不允许重复
$duplicate = Db::name('fc_area')
->where('province', $province)
->where('city', $city)
->where('district', $district)
->where('id', '<>', $id)
->find();
if ($duplicate) {
$this->fail('该省市区组合已存在');
}
if ($lat !== null && $lat !== '') {
if (!is_numeric($lat) || (float)$lat < -90 || (float)$lat > 90) {
$this->fail('纬度格式不正确');
}
$lat = round((float)$lat, 6);
} else {
$lat = null;
}
if ($lng !== null && $lng !== '') {
if (!is_numeric($lng) || (float)$lng < -180 || (float)$lng > 180) {
$this->fail('经度格式不正确');
}
$lng = round((float)$lng, 6);
} else {
$lng = null;
}
$now = time();
$data = [
'name' => $name,
'sort' => $sort,
'enabled' => $enabled ? 1 : 0,
'province' => $province,
'city' => $city,
'district' => $district,
'lat' => $lat,
'lng' => $lng,
'sort' => $sort,
'enabled' => $enabled ? 1 : 0,
'updatetime' => $now,
];
if ($id && Db::name('fc_area')->where('id', $id)->find()) {
Db::name('fc_area')->where('id', $id)->update($data);
} else {
$data['createtime'] = $now;
$id = Db::name('fc_area')->insertGetId($data);
}
$this->adminLog('保存区域 #' . $id . ' ' . $name);
$fullName = $province . ' / ' . $city . ' / ' . $district;
$this->adminLog('保存服务区域 #' . $id . ' ' . $fullName);
$this->ok(['id' => (int)$id, 'name' => $name]);
$this->ok(['id' => (int)$id, 'fullName' => $fullName]);
}
/**
@@ -178,11 +242,19 @@ class AdminSetting extends V1
if ($this->request->isPost()) {
$siteName = $this->input('siteName', null);
if ($siteName !== null) {
$this->setCfg('ui.siteName', (string)$siteName);
$siteName = trim((string)$siteName);
if ($siteName === '') {
$this->fail('小程序名称不能为空');
}
$this->setCfg('base.appName', $siteName);
// 兼容旧 ui.siteName(用户端已统一优先读 base.appName)
$this->setCfg('ui.siteName', $siteName);
}
$logo = $this->input('logo', null);
if ($logo !== null) {
$this->setCfg('base.logo', (string)$logo);
// 兼容旧 ui.logo
$this->setCfg('ui.logo', (string)$logo);
}
@@ -206,7 +278,7 @@ class AdminSetting extends V1
}
$this->setCfg('ui.updatedAt', date('c'));
$this->adminLog('修改LOGO与图标配置');
$this->adminLog('修改小程序名称/LOGO/图标配置');
}
$this->ok($this->uiConfig());
@@ -218,17 +290,16 @@ class AdminSetting extends V1
*/
public function uiReset()
{
// LOGO 已统一到 base.logo,重置时两处都清空,避免旧 ui.logo 回落“复活”
$this->setCfg('base.logo', '');
$this->setCfg('ui.logo', '');
$this->setCfg('ui.icons', []);
$this->setCfg('ui.updatedAt', date('c'));
$this->adminLog('重置LOGO与图标配置');
$this->ok([
'siteName' => (string)$this->cfg('ui.siteName', '同城分类信息'),
'logo' => '',
'icons' => (object)[],
'reset' => true,
]);
$config = $this->uiConfig();
$config['reset'] = true;
$this->ok($config);
}
/**
@@ -275,17 +346,29 @@ class AdminSetting extends V1
];
}
/** UI 配置结构(icons 保证为对象) */
/** UI 配置结构(名称/LOGO 以 base.* 为准,icons 保证为对象) */
protected function uiConfig()
{
$icons = $this->cfg('ui.icons', []);
$icons = is_array($icons) ? $icons : (array)$icons;
$baseName = trim((string)$this->cfg('base.appName', ''));
$siteName = $baseName !== '' ? $baseName : (string)$this->cfg('ui.siteName', '同城分类信息');
$baseLogo = (string)$this->cfg('base.logo', '');
$logo = $baseLogo !== '' ? $baseLogo : (string)$this->cfg('ui.logo', '');
$shareTitle = trim((string)$this->cfg('base.shareTitle', ''));
return [
'siteName' => (string)$this->cfg('ui.siteName', '同城分类信息'),
'logo' => (string)$this->cfg('ui.logo', ''),
'icons' => (object)$icons,
'updatedAt' => (string)$this->cfg('ui.updatedAt', date('c')),
'appName' => $siteName,
'siteName' => $siteName,
'logo' => $logo,
'servicePhone' => (string)$this->cfg('base.servicePhone', ''),
'aboutUs' => (string)$this->cfg('base.aboutUs', ''),
'shareTitle' => $shareTitle !== '' ? $shareTitle : $siteName,
'icons' => (object)$icons,
'updatedAt' => (string)$this->cfg('ui.updatedAt', date('c')),
];
}
}