diff --git a/addons/address/.addonrc b/addons/address/.addonrc
new file mode 100644
index 0000000..6ca1422
--- /dev/null
+++ b/addons/address/.addonrc
@@ -0,0 +1 @@
+{"files":["public\\assets\\addons\\address\\js\\gcoord.min.js","public\\assets\\addons\\address\\js\\jquery.autocomplete.js"],"license":"regular","licenseto":"26425","licensekey":"yO6C1uwFmRnsXIkp klcX6YFU+U3ad3Xy5gdiig==","domains":["fenleixinxi"],"licensecodes":[],"validations":["bd9cd2ccafa213b7a49c36d1a5919d55"]}
\ No newline at end of file
diff --git a/addons/address/Address.php b/addons/address/Address.php
new file mode 100644
index 0000000..4bc36ea
--- /dev/null
+++ b/addons/address/Address.php
@@ -0,0 +1,32 @@
+
+ */
+class Address extends Addons
+{
+
+ /**
+ * 插件安装方法
+ * @return bool
+ */
+ public function install()
+ {
+ return true;
+ }
+
+ /**
+ * 插件卸载方法
+ * @return bool
+ */
+ public function uninstall()
+ {
+ return true;
+ }
+
+}
diff --git a/addons/address/bootstrap.js b/addons/address/bootstrap.js
new file mode 100644
index 0000000..655162d
--- /dev/null
+++ b/addons/address/bootstrap.js
@@ -0,0 +1,34 @@
+require([], function () {
+ //绑定data-toggle=addresspicker属性点击事件
+
+ $(document).on('click', "[data-toggle='addresspicker']", function () {
+ var that = this;
+ var callback = $(that).data('callback');
+ var input_id = $(that).data("input-id") ? $(that).data("input-id") : "";
+ var lat_id = $(that).data("lat-id") ? $(that).data("lat-id") : "";
+ var lng_id = $(that).data("lng-id") ? $(that).data("lng-id") : "";
+ var zoom_id = $(that).data("zoom-id") ? $(that).data("zoom-id") : "";
+ var lat = lat_id ? $("#" + lat_id).val() : '';
+ var lng = lng_id ? $("#" + lng_id).val() : '';
+ var zoom = zoom_id ? $("#" + zoom_id).val() : '';
+ var url = "/addons/address/index/select";
+ url += (lat && lng) ? '?lat=' + lat + '&lng=' + lng + (input_id ? "&address=" + $("#" + input_id).val() : "") + (zoom ? "&zoom=" + zoom : "") : '';
+ Fast.api.open(url, '位置选择', {
+ callback: function (res) {
+ input_id && $("#" + input_id).val(res.address).trigger("change");
+ lat_id && $("#" + lat_id).val(res.lat).trigger("change");
+ lng_id && $("#" + lng_id).val(res.lng).trigger("change");
+ zoom_id && $("#" + zoom_id).val(res.zoom).trigger("change");
+
+ try {
+ //执行回调函数
+ if (typeof callback === 'function') {
+ callback.call(that, res);
+ }
+ } catch (e) {
+
+ }
+ }
+ });
+ });
+});
diff --git a/addons/address/config.php b/addons/address/config.php
new file mode 100644
index 0000000..208286b
--- /dev/null
+++ b/addons/address/config.php
@@ -0,0 +1,132 @@
+ 'maptype',
+ 'title' => '默认地图类型',
+ 'type' => 'radio',
+ 'content' => [
+ 'baidu' => '百度地图',
+ 'amap' => '高德地图',
+ 'tencent' => '腾讯地图',
+ ],
+ 'value' => 'tencent',
+ 'rule' => 'required',
+ 'msg' => '',
+ 'tip' => '',
+ 'ok' => '',
+ 'extend' => '',
+ ],
+ [
+ 'name' => 'zoom',
+ 'title' => '默认缩放级别',
+ 'type' => 'string',
+ 'content' => [],
+ 'value' => '11',
+ 'rule' => 'required',
+ 'msg' => '',
+ 'tip' => '',
+ 'ok' => '',
+ 'extend' => '',
+ ],
+ [
+ 'name' => 'lat',
+ 'title' => '默认Lat',
+ 'type' => 'string',
+ 'content' => [],
+ 'value' => '26.647000',
+ 'rule' => 'required',
+ 'msg' => '',
+ 'tip' => '',
+ 'ok' => '',
+ 'extend' => '',
+ ],
+ [
+ 'name' => 'lng',
+ 'title' => '默认Lng',
+ 'type' => 'string',
+ 'content' => [],
+ 'value' => '106.630000',
+ 'rule' => 'required',
+ 'msg' => '',
+ 'tip' => '',
+ 'ok' => '',
+ 'extend' => '',
+ ],
+ [
+ 'name' => 'baidukey',
+ 'title' => '百度地图KEY',
+ 'type' => 'string',
+ 'content' => [],
+ 'value' => '',
+ 'rule' => '',
+ 'msg' => '',
+ 'tip' => '',
+ 'ok' => '',
+ 'extend' => '',
+ ],
+ [
+ 'name' => 'amapkey',
+ 'title' => '高德地图KEY',
+ 'type' => 'string',
+ 'content' => [],
+ 'value' => '',
+ 'rule' => '',
+ 'msg' => '',
+ 'tip' => '',
+ 'ok' => '',
+ 'extend' => '',
+ ],
+ [
+ 'name' => 'amapsecurityjscode',
+ 'title' => '高德地图安全密钥',
+ 'type' => 'string',
+ 'content' => [],
+ 'value' => '',
+ 'rule' => '',
+ 'msg' => '',
+ 'tip' => '',
+ 'ok' => '',
+ 'extend' => '',
+ ],
+ [
+ 'name' => 'tencentkey',
+ 'title' => '腾讯地图KEY',
+ 'type' => 'string',
+ 'content' => [],
+ 'value' => 'WM7BZ-ZAYLL-MZEPZ-MRZBT-MBYJE-ZAFIT',
+ 'rule' => '',
+ 'msg' => '',
+ 'tip' => '',
+ 'ok' => '',
+ 'extend' => '',
+ ],
+ [
+ 'name' => 'coordtype',
+ 'title' => '坐标系类型',
+ 'type' => 'select',
+ 'content' => [
+ 'DEFAULT' => '默认(使用所选地图默认坐标系)',
+ 'GCJ02' => 'GCJ-02',
+ 'BD09' => 'BD-09',
+ ],
+ 'value' => 'DEFAULT',
+ 'rule' => 'required',
+ 'msg' => '',
+ 'tip' => '',
+ 'ok' => '',
+ 'extend' => '',
+ ],
+ [
+ 'name' => '__tips__',
+ 'title' => '温馨提示',
+ 'type' => '',
+ 'content' => [],
+ 'value' => '请先申请对应地图的Key,配置后再使用',
+ 'rule' => '',
+ 'msg' => '',
+ 'tip' => '',
+ 'ok' => '',
+ 'extend' => 'alert-danger-light',
+ ],
+];
diff --git a/addons/address/controller/Index.php b/addons/address/controller/Index.php
new file mode 100644
index 0000000..5cd31e3
--- /dev/null
+++ b/addons/address/controller/Index.php
@@ -0,0 +1,64 @@
+request->langset();
+ $lang = preg_match("/^([a-zA-Z\-_]{2,10})\$/i", $lang) ? $lang : 'zh-cn';
+
+ $site = Config::get("site");
+
+ // 配置信息
+ $config = [
+ 'site' => array_intersect_key($site, array_flip(['name', 'cdnurl', 'version', 'timezone', 'languages'])),
+ 'upload' => null,
+ 'modulename' => 'addons',
+ 'controllername' => 'index',
+ 'actionname' => 'index',
+ 'jsname' => 'addons/address',
+ 'moduleurl' => '',
+ 'language' => $lang
+ ];
+ $config = array_merge($config, Config::get("view_replace_str"));
+
+ // 配置信息后
+ Hook::listen("config_init", $config);
+ // 加载当前控制器语言包
+ $this->view->assign('site', $site);
+ $this->view->assign('config', $config);
+
+ return $this->view->fetch();
+ }
+
+ // 选择地址
+ public function select()
+ {
+ $config = get_addon_config('address');
+ $zoom = (int)$this->request->get('zoom', $config['zoom']);
+ $lng = (float)$this->request->get('lng');
+ $lat = (float)$this->request->get('lat');
+ $address = $this->request->get('address');
+ $lng = $lng ?: $config['lng'];
+ $lat = $lat ?: $config['lat'];
+ $this->view->assign('zoom', $zoom);
+ $this->view->assign('lng', $lng);
+ $this->view->assign('lat', $lat);
+ $this->view->assign('address', $address);
+ $maptype = $config['maptype'];
+ if (!isset($config[$maptype . 'key']) || !$config[$maptype . 'key']) {
+ $this->error("请在配置中配置对应类型地图的密钥");
+ }
+ return $this->view->fetch('index/' . $maptype);
+ }
+
+}
diff --git a/addons/address/info.ini b/addons/address/info.ini
new file mode 100644
index 0000000..c0f914b
--- /dev/null
+++ b/addons/address/info.ini
@@ -0,0 +1,10 @@
+name = address
+title = 地址位置选择插件
+intro = 地图位置选择插件,可返回地址和经纬度
+author = FastAdmin
+website =
+version = 1.1.8
+state = 1
+url = /addons/address
+license = regular
+licenseto = 26425
diff --git a/addons/address/view/index/amap.html b/addons/address/view/index/amap.html
new file mode 100644
index 0000000..62972fc
--- /dev/null
+++ b/addons/address/view/index/amap.html
@@ -0,0 +1,258 @@
+
+
+
+
+ 地址选择器
+
+
+
+
+
+
+确定
+
+
+
+
+
+
+
+
+
+
diff --git a/addons/address/view/index/baidu.html b/addons/address/view/index/baidu.html
new file mode 100644
index 0000000..86aa74b
--- /dev/null
+++ b/addons/address/view/index/baidu.html
@@ -0,0 +1,243 @@
+
+
+
+
+ 地址选择器
+
+
+
+
+
+
+确定
+
+
+
+
+
+
+
diff --git a/addons/address/view/index/index.html b/addons/address/view/index/index.html
new file mode 100644
index 0000000..07f9712
--- /dev/null
+++ b/addons/address/view/index/index.html
@@ -0,0 +1,132 @@
+
+
+
+
+ 地图位置(经纬度)选择插件
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ | 参数 |
+ 释义 |
+
+
+
+
+ | data-input-id |
+ 填充地址的文本框ID |
+
+
+ | data-lng-id |
+ 填充经度的文本框ID |
+
+
+ | data-lat-id |
+ 填充纬度的文本框ID |
+
+
+ | data-zoom-id |
+ 填充缩放的文本框ID |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/addons/address/view/index/tencent.html b/addons/address/view/index/tencent.html
new file mode 100644
index 0000000..89df386
--- /dev/null
+++ b/addons/address/view/index/tencent.html
@@ -0,0 +1,291 @@
+
+
+
+
+ 地址选择器
+
+
+
+
+
+
+确定
+
+
+
+
+
+
+
+
+
+
diff --git a/addons/epay/certs/apiclient_cert.pem b/addons/epay/certs/apiclient_cert.pem
index e69de29..ad68c62 100644
--- a/addons/epay/certs/apiclient_cert.pem
+++ b/addons/epay/certs/apiclient_cert.pem
@@ -0,0 +1,25 @@
+-----BEGIN CERTIFICATE-----
+MIIELjCCAxagAwIBAgIUZDUZYP2jboFh2yUKMMx6fYXLEGIwDQYJKoZIhvcNAQEL
+BQAwXjELMAkGA1UEBhMCQ04xEzARBgNVBAoTClRlbnBheS5jb20xHTAbBgNVBAsT
+FFRlbnBheS5jb20gQ0EgQ2VudGVyMRswGQYDVQQDExJUZW5wYXkuY29tIFJvb3Qg
+Q0EwHhcNMjYxMDE3MDIxNjIxWhcNMzExMDE2MDIxNjIxWjCBhzETMBEGA1UEAwwK
+MTExODg5ODAxODEbMBkGA1UECgwS5b6u5L+h5ZWG5oi357O757ufMTMwMQYDVQQL
+DCrotLXlt57nlLDpl7TlsI/ku5Pnp5HmioDmnInpmZDotKPku7vlhazlj7gxCzAJ
+BgNVBAYTAkNOMREwDwYDVQQHDAhTaGVuWmhlbjCCASIwDQYJKoZIhvcNAQEBBQAD
+ggEPADCCAQoCggEBALubtbejpAiZJiXXQOU0k+1OoJMXXXoiab98FVSASvI6oudj
+CwiSX9kPL5yNB5W96MCsoak19bDps6cZ9xk/Zd0oF7cMMc2qNGshrxwYYCnpuwF4
+/uyUjKHkIUqUgCq6jxitqWcPH/pCiIklNJ+I0J2jsu3m/Nb/Q1i2FPOhYG2MeOdE
+hHuNwe3xAWdTQJxoVTGx5XgkjuNfyzR+Gqa5cXh/v5PVCBfpFPtwWgWRy7Epdenu
+nQ+SKCakicTgfK80oFXUVXNfYsHK3S1ZdX8ad5nGQS5rDZ4XKUW8T3smrfxkQ7h/
+8Y41ttQxeAiO2LZ5n9SNQxF5dXh5Ez27xg1ZGNMCAwEAAaOBuTCBtjAJBgNVHRME
+AjAAMAsGA1UdDwQEAwID+DCBmwYDVR0fBIGTMIGQMIGNoIGKoIGHhoGEaHR0cDov
+L2V2Y2EuaXRydXMuY29tLmNuL3B1YmxpYy9pdHJ1c2NybD9DQT0xQkQ0MjIwRTUw
+REJDMDRCMDZBRDM5NzU0OTg0NkMwMUMzRThFQkQyJnNnPUhBQ0M0NzFCNjU0MjJF
+MTJCMjdBOUQzM0E4N0FEMUNERjU5MjZFMTQwMzcxMA0GCSqGSIb3DQEBCwUAA4IB
+AQAyU4UMwSU3Zf7k3xV85FkN6kekr3JXzg+UeOYSTUcylQrgMsW6W6s5jafuoJAf
+MYWPB8Sc9fDoKYHOBK237hU2+BeAf6iBvrVdTbhW8v9aGgCUaf/Uf0T4tEGMmOu6
+PZq+h04WgtLW6jyps3rcn/Cgc3Femz9UjP28k4wlIOwo1o5UJKlZRlfZ9G0i8bCZ
+CA2SSCL3UL9N1vMzb2HXhVkwwSJr0dk9rE67ijuh64UuERpUkPArmGUFYqcXV17U
+rs3Yrd6VR2XITE0HkpxCxtR3QIs4385VdgwQ5RsQQQbIIqoXf0Pz1qyz5OCzOPjp
+6qN93xkOIzIkLy0jPwAx0DZV
+-----END CERTIFICATE-----
diff --git a/addons/epay/certs/apiclient_key.pem b/addons/epay/certs/apiclient_key.pem
index e69de29..26a6ffc 100644
--- a/addons/epay/certs/apiclient_key.pem
+++ b/addons/epay/certs/apiclient_key.pem
@@ -0,0 +1,28 @@
+-----BEGIN PRIVATE KEY-----
+MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC7m7W3o6QImSYl
+10DlNJPtTqCTF116Imm/fBVUgEryOqLnYwsIkl/ZDy+cjQeVvejArKGpNfWw6bOn
+GfcZP2XdKBe3DDHNqjRrIa8cGGAp6bsBeP7slIyh5CFKlIAquo8YralnDx/6QoiJ
+JTSfiNCdo7Lt5vzW/0NYthTzoWBtjHjnRIR7jcHt8QFnU0CcaFUxseV4JI7jX8s0
+fhqmuXF4f7+T1QgX6RT7cFoFkcuxKXXp7p0PkigmpInE4HyvNKBV1FVzX2LByt0t
+WXV/GneZxkEuaw2eFylFvE97Jq38ZEO4f/GONbbUMXgIjti2eZ/UjUMReXV4eRM9
+u8YNWRjTAgMBAAECggEAcWUAROsH7pqC0ER22RMahkkMDVwGCBhcynx5eAXDRv3i
+EPFwbdHrVQs0v+8OMpgC4tJ1NA2FX1UiXVH4ZK5JJ49gJiecwq6p2EcXzGd/WFYJ
+zi4QB3cSOZfl92oivbDdZREyXzRpI56rYBm44OSVE3MqZ1lCYcEQN8J2j3tvCUbh
+NAhm6wgaTdx7PmwXyualb2aWX6X0jxwcNm8MPHpSSH73pjUMN0zZetMGfVr42utM
+ltSU1yLEYkW4VvopWtW+jSD1ry4gVnSqO3TaS8mwiOv9LSPSEXdN6mSHtrWLmDCb
+vIOUUzf0HhVRJw3HQb34ZTCwLaGX4GyaL2LA2cdjoQKBgQDo0vCWxQL6QKsJOiKA
+4EtAqW+jtvprCZyR37R+xHUwwZAW30czMOWKNp4speVg7Vhabr9SDBM8FAyYvctx
+XjoyawbYjRHR7P1d9ruKFv+TK4RthG8O/cmvq4/BhvUwJfnVP42qkjAI2a9mk7XE
+xyhrvihFPdUvopUPmKgf13MOZQKBgQDOSIk2uCAV1jqP5tQ2/afEbvNmjTPVLg69
+BjtcXyKTx9w+gLgBIXs526t4XUKLP0DPxsPIKLKTN4VJRxKPdXw+LZqsSHxBXL7P
+Kz+azMSZtIQe1KhSJEMJCX9XQjPoGUpTS8ogKhYf/jioMJVBpF4xTQ/Ujc6vI3CZ
+gHCvQT7a1wKBgD+KgjrDneJqj8M+6He/vwnwtBU8NO/NebueEAag21LpIMf9O9NO
+vvaPnW+V/xoK7JgRrYlY3k8BMcbl1YYlL0DfUMj9ByYBxcxFMD2ptSqXL9cr05uF
+y+tzqRIqIPR5CMwYQjORHibsMiCOo4cGy5NMHHV5QJt1hbTx9sBp7m65AoGAFe3i
+IF4MkyW2Dgw07S+kAR7mYzWJmBUQXgnmj5hRfbJWeJKIfR5JEkqG/QG1DEaKJkx0
+Dbg926agVbznfTa5IYcJlV0TUIOCiVWIqnUwRULKdNiYmDrgoXpP/j+8cuUVYKuC
+7k5rEbWd4ktf4p2NoeIWV7tHiJjU5Iyh7g+alfsCgYEAtYcLzzZ8Wmp5zeI9e0g6
+kXTK+4oBPs+L4f9rs5YcwKqtTRS0rFI7noncTnlvabmR1BQM1vb26veWAbnoydv6
+g35qCUttXauL4EERY/VeNelbCuEwhKuxLv0s3FlGMTN70PLqKAm3JNdKMgzvnUA6
+S3gxh/fyFLprRfkLbeQz6dY=
+-----END PRIVATE KEY-----
diff --git a/addons/epay/certs/public_key.pem b/addons/epay/certs/public_key.pem
index e69de29..a1c834c 100644
--- a/addons/epay/certs/public_key.pem
+++ b/addons/epay/certs/public_key.pem
@@ -0,0 +1,9 @@
+-----BEGIN PUBLIC KEY-----
+MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA7QW1+UaVd76VUmnEWZ8Z
+pkUFQcK3QElxwKhb8fuY6i+DZR/i6MNI9GEW2N/4qWa/WRHHx9jwOas8ORyp1f8/
+CgY1aGhC9h/m9B7IJHGb9zJXxQnkJOLaOYk8g5p2wGG1THVLNryy/8KKRwCdZJ/P
+gy5MB3hteK/5rp1/HMnVMdHrCHgt9L+diyNGb7FW6d2RT5vM2+jRrdbpiFwIuQYT
++uGMQyE9qKdnAlTvqr4J/T0dtdBd6vidEktEDb+UnPsufa5LWk2MbF66diw6TE8w
+lcsX7EvSJEd1tVBHn6MOeHoEs1iqfKI557fCTlLitGk4SszlKusR0OmbrEVhBigV
+uQIDAQAB
+-----END PUBLIC KEY-----
diff --git a/addons/epay/config.php b/addons/epay/config.php
index c650f28..720e1e2 100644
--- a/addons/epay/config.php
+++ b/addons/epay/config.php
@@ -6,7 +6,7 @@ return [
'title' => 'API版本(请勿修改该值)',
'type' => 'radio',
'content' => [],
- 'value' => 'v2',
+ 'value' => 'v3',
'rule' => '',
'msg' => '',
'tip' => 'V2版本只支持微信支付V2密钥,V3版本只支持微信支付V3密钥,请勿修改该值!!!',
@@ -25,8 +25,8 @@ return [
'miniapp_id' => 'wxbfcb778f3019cff8',
'mch_id' => '1118898018',
'key' => 'cfgherdhjbfshf3356586feejfbvjvhe',
- 'key_v3' => '',
- 'public_key_id' => '',
+ 'key_v3' => 'cfgherdhjbfshf3356586feejfbvjvhe',
+ 'public_key_id' => 'PUB_KEY_ID_0111188980182026101700181872001201',
'public_key' => '/addons/epay/certs/public_key.pem',
'cert_client' => '/addons/epay/certs/apiclient_cert.pem',
'cert_key' => '/addons/epay/certs/apiclient_key.pem',
diff --git a/addons/epay/library/v3new/Yansongda/Pay/Shortcut/Wechat/MchTransferShortcut.php b/addons/epay/library/v3new/Yansongda/Pay/Shortcut/Wechat/MchTransferShortcut.php
new file mode 100644
index 0000000..aa80f17
--- /dev/null
+++ b/addons/epay/library/v3new/Yansongda/Pay/Shortcut/Wechat/MchTransferShortcut.php
@@ -0,0 +1,93 @@
+mchTransfer([...]); // 发起转账
+ * Pay::wechat($config)->mchTransfer(['_action' => 'query', 'out_bill_no' => ...]); // 查询
+ *
+ * 说明:该文件属于 epay 插件内置 SDK 的补丁,插件升级后需重新确认是否存在。
+ */
+class MchTransferShortcut implements ShortcutInterface
+{
+ /**
+ * @throws InvalidParamsException
+ */
+ public function getPlugins(array $params): array
+ {
+ $method = Str::camel($params['_action'] ?? 'default').'Plugins';
+
+ if (method_exists($this, $method)) {
+ return $this->{$method}();
+ }
+
+ throw new InvalidParamsException(
+ Exception::PARAMS_SHORTCUT_ACTION_INVALID,
+ "不支持的 action 参数 [{$method}],可用:默认(发起转账)、query(查询)"
+ );
+ }
+
+ protected function defaultPlugins(): array
+ {
+ return $this->createPlugins();
+ }
+
+ protected function createPlugins(): array
+ {
+ return [
+ StartPlugin::class,
+ CreatePlugin::class,
+ AddPayloadBodyPlugin::class,
+ AddPayloadSignaturePlugin::class,
+ AddRadarPlugin::class,
+ VerifySignaturePlugin::class,
+ ResponsePlugin::class,
+ ParserPlugin::class,
+ ];
+ }
+
+ protected function queryPlugins(): array
+ {
+ return [
+ StartPlugin::class,
+ QueryPlugin::class,
+ AddPayloadSignaturePlugin::class,
+ AddRadarPlugin::class,
+ VerifySignaturePlugin::class,
+ ResponsePlugin::class,
+ ParserPlugin::class,
+ ];
+ }
+
+ protected function cancelPlugins(): array
+ {
+ return [
+ StartPlugin::class,
+ \Yansongda\Pay\Plugin\Wechat\V3\Marketing\MchTransfer\CancelPlugin::class,
+ AddPayloadSignaturePlugin::class,
+ AddRadarPlugin::class,
+ VerifySignaturePlugin::class,
+ ResponsePlugin::class,
+ ParserPlugin::class,
+ ];
+ }
+}
diff --git a/application/admin/command/FcInstall.php b/application/admin/command/FcInstall.php
index 0199cf4..1f1e354 100644
--- a/application/admin/command/FcInstall.php
+++ b/application/admin/command/FcInstall.php
@@ -83,6 +83,15 @@ class FcInstall extends Command
$output->writeln('==> LBS / 店铺装修 / 支付 / 优惠券 / 举报升级(v1.7.0)');
$this->upgradeV170($output);
+ $output->writeln('==> 提现商家转账字段升级(v1.2.1)');
+ $this->upgradeV121($output);
+
+ $output->writeln('==> 优惠券抵扣 / 佣金结算字段升级(v1.2.2)');
+ $this->upgradeV122($output);
+
+ $output->writeln('==> 服务区域平铺升级(省 / 市 / 区县)');
+ $this->upgradeArea($output);
+
$output->writeln('==> 初始化后台菜单');
$this->menu($output);
@@ -329,6 +338,226 @@ class FcInstall extends Command
: ' - LBS / 店铺装修 / 支付 / 优惠券 / 举报配置已是最新,无需处理');
}
+ /**
+ * v1.2.1:提现「商家转账」字段
+ * - fa_fc_withdraw:out_bill_no / transfer_status / transfer_id / package_info / fail_reason / transfer_time
+ * - fa_fc_user_ext:realname(收款人真实姓名)
+ * 幂等:已存在则跳过
+ */
+ private function upgradeV121(Output $output)
+ {
+ $prefix = \think\Config::get('database.prefix');
+ $w = $prefix . 'fc_withdraw';
+ $e = $prefix . 'fc_user_ext';
+
+ $cols = [
+ 'out_bill_no' => "ALTER TABLE `{$w}` ADD COLUMN `out_bill_no` varchar(32) NOT NULL DEFAULT '' COMMENT '微信商家转账商户单号'",
+ 'transfer_status' => "ALTER TABLE `{$w}` ADD COLUMN `transfer_status` varchar(16) NOT NULL DEFAULT '' COMMENT 'WAIT_USER_CONFIRM/PROCESSING/SUCCESS/FAIL/CANCELLED'",
+ 'transfer_id' => "ALTER TABLE `{$w}` ADD COLUMN `transfer_id` varchar(64) NOT NULL DEFAULT '' COMMENT '微信转账单号'",
+ 'package_info' => "ALTER TABLE `{$w}` ADD COLUMN `package_info` varchar(512) NOT NULL DEFAULT '' COMMENT '拉起确认收款页的package'",
+ 'fail_reason' => "ALTER TABLE `{$w}` ADD COLUMN `fail_reason` varchar(255) NOT NULL DEFAULT ''",
+ 'transfer_time' => "ALTER TABLE `{$w}` ADD COLUMN `transfer_time` int(10) DEFAULT NULL",
+ ];
+
+ $changed = 0;
+ foreach ($cols as $col => $sql) {
+ if ($this->hasColumn($w, $col)) {
+ continue;
+ }
+ Db::execute($sql);
+ $changed++;
+ }
+
+ try {
+ Db::execute("ALTER TABLE `{$w}` ADD KEY `out_bill_no` (`out_bill_no`)");
+ $changed++;
+ } catch (\Exception $e2) {
+ // 索引已存在则忽略
+ }
+
+ if (!$this->hasColumn($e, 'realname')) {
+ Db::execute("ALTER TABLE `{$e}` ADD COLUMN `realname` varchar(32) NOT NULL DEFAULT '' COMMENT '收款人真实姓名(提现用)'");
+ $changed++;
+ }
+
+ // 退款单号(微信退款结果回调按此匹配订单)
+ $o = $prefix . 'fc_order';
+ if (!$this->hasColumn($o, 'out_refund_no')) {
+ Db::execute("ALTER TABLE `{$o}` ADD COLUMN `out_refund_no` varchar(64) NOT NULL DEFAULT '' COMMENT '微信退款单号(商户侧)'");
+ $changed++;
+ }
+
+ $output->writeln($changed
+ ? ' - 完成(新增结构 ' . $changed . ' 处)'
+ : ' - 提现转账字段已是最新,无需处理');
+ }
+
+ /**
+ * 服务区域平铺升级(省 / 市 / 区县三列,不再使用层级)
+ *
+ * 目标结构:fc_area(id, province, city, district, lat, lng, sort, enabled)
+ * 每条记录 = 一个服务区域(区县);省 / 市只是该行的归属字段,供前端级联。
+ * 历史层级数据会自动压平:每个区县保留原 ID,省 / 市节点删除。
+ * 幂等:可重复执行。
+ */
+ private function upgradeArea(Output $output)
+ {
+ $prefix = \think\Config::get('database.prefix');
+ $areaTable = $prefix . 'fc_area';
+ $postTable = $prefix . 'fc_post';
+ $draftTable = $prefix . 'fc_draft';
+ $now = time();
+ $changed = 0;
+
+ // ① fc_area 平铺字段
+ $areaCols = [
+ 'province' => "ALTER TABLE `{$areaTable}` ADD COLUMN `province` varchar(32) NOT NULL DEFAULT '' COMMENT '省份' AFTER `id`",
+ 'city' => "ALTER TABLE `{$areaTable}` ADD COLUMN `city` varchar(32) NOT NULL DEFAULT '' COMMENT '城市' AFTER `province`",
+ 'district' => "ALTER TABLE `{$areaTable}` ADD COLUMN `district` varchar(32) NOT NULL DEFAULT '' COMMENT '区县(最终服务区域)' AFTER `city`",
+ 'lat' => "ALTER TABLE `{$areaTable}` ADD COLUMN `lat` decimal(10,6) DEFAULT NULL COMMENT '中心纬度(gcj02)' AFTER `district`",
+ 'lng' => "ALTER TABLE `{$areaTable}` ADD COLUMN `lng` decimal(10,6) DEFAULT NULL COMMENT '中心经度(gcj02)' AFTER `lat`",
+ ];
+ foreach ($areaCols as $col => $sql) {
+ if ($this->hasColumn($areaTable, $col)) {
+ continue;
+ }
+ Db::execute($sql);
+ $changed++;
+ }
+ if (!$this->hasIndex($areaTable, 'province_city')) {
+ Db::execute("ALTER TABLE `{$areaTable}` ADD KEY `province_city` (`province`,`city`)");
+ }
+
+ // ② 旧层级数据压平:区县保留原 ID,省 / 市节点删除
+ $hasLevel = $this->hasColumn($areaTable, 'level');
+ $hasParent = $this->hasColumn($areaTable, 'parent_id');
+ $hasName = $this->hasColumn($areaTable, 'name');
+
+ if ($hasLevel || $hasParent || $hasName) {
+ $rows = Db::name('fc_area')->select();
+ $byId = [];
+ foreach ($rows as $row) {
+ $byId[(int)$row['id']] = $row;
+ }
+
+ $flat = 0;
+ foreach ($rows as $row) {
+ $level = $hasLevel ? (int)$row['level'] : 0;
+ // 只保留区县(level=3);真实省份/城市节点稍后删除
+ if ($hasLevel && $level > 0 && $level < 3) {
+ continue;
+ }
+
+ $provinceName = '';
+ $cityName = '';
+ $districtName = $hasName ? (string)$row['name'] : (string)$row['district'];
+
+ if ($hasParent && (int)$row['parent_id'] > 0 && isset($byId[(int)$row['parent_id']])) {
+ $cityRow = $byId[(int)$row['parent_id']];
+ $cityName = $hasName ? (string)$cityRow['name'] : (string)$cityRow['city'];
+ if ((int)$cityRow['parent_id'] > 0 && isset($byId[(int)$cityRow['parent_id']])) {
+ $provinceRow = $byId[(int)$cityRow['parent_id']];
+ $provinceName = $hasName ? (string)$provinceRow['name'] : (string)$provinceRow['province'];
+ }
+ }
+
+ Db::name('fc_area')->where('id', $row['id'])->update([
+ 'province' => $provinceName,
+ 'city' => $cityName,
+ 'district' => $districtName,
+ 'updatetime' => $now,
+ ]);
+ $flat++;
+ }
+
+ // 删除省 / 市节点(它们不是服务区域)
+ if ($hasLevel) {
+ $deleted = Db::name('fc_area')->where('level', 'in', [1, 2])->delete();
+ }
+ // 删除旧字段,彻底去掉层级概念
+ foreach (['parent_id', 'level', 'name'] as $col) {
+ if ($this->hasColumn($areaTable, $col)) {
+ Db::execute("ALTER TABLE `{$areaTable}` DROP COLUMN `{$col}`");
+ }
+ }
+ $output->writeln(' - 已将 ' . $flat . ' 条区域压平为「省 / 市 / 区县」三列');
+ }
+
+ // ③ 已知区县中心坐标(仅补空值)
+ $coords = [
+ '云岩区' => [26.605, 106.710],
+ '南明区' => [26.568, 106.716],
+ '观山湖区' => [26.625, 106.625],
+ '花溪区' => [26.430, 106.670],
+ '白云区' => [26.678, 106.623],
+ ];
+ foreach ($coords as $name => $ll) {
+ Db::name('fc_area')->where('district', $name)->whereNull('lat')->update([
+ 'lat' => $ll[0], 'lng' => $ll[1], 'updatetime' => $now,
+ ]);
+ }
+
+ // ④ fc_post:保留 district_id 作为服务区域ID,移除省/市 ID 列
+ if (!$this->hasColumn($postTable, 'district_id')) {
+ Db::execute("ALTER TABLE `{$postTable}` ADD COLUMN `district_id` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '服务区域ID(fc_area.id)' AFTER `area`");
+ $changed++;
+ }
+ foreach (['province_id', 'city_id'] as $col) {
+ if ($this->hasColumn($postTable, $col)) {
+ Db::execute("ALTER TABLE `{$postTable}` DROP COLUMN `{$col}`");
+ }
+ }
+ if (!$this->hasIndex($postTable, 'district_id')) {
+ Db::execute("ALTER TABLE `{$postTable}` ADD KEY `district_id` (`district_id`)");
+ }
+
+ // ⑤ fc_draft:同样只保留 area + district_id
+ foreach (['area', 'district_id'] as $col) {
+ if ($this->hasColumn($draftTable, $col)) {
+ continue;
+ }
+ $sql = $col === 'area'
+ ? "ALTER TABLE `{$draftTable}` ADD COLUMN `area` varchar(32) NOT NULL DEFAULT '' COMMENT '区县名称' AFTER `address`"
+ : "ALTER TABLE `{$draftTable}` ADD COLUMN `district_id` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '服务区域ID(fc_area.id)' AFTER `area`";
+ Db::execute($sql);
+ $changed++;
+ }
+ foreach (['province_id', 'city_id'] as $col) {
+ if ($this->hasColumn($draftTable, $col)) {
+ Db::execute("ALTER TABLE `{$draftTable}` DROP COLUMN `{$col}`");
+ }
+ }
+
+ // ⑥ 按区县名称回填 district_id(信息 / 草稿)
+ $byName = [];
+ foreach (Db::name('fc_area')->select() as $area) {
+ $byName[(string)$area['district']] = (int)$area['id'];
+ }
+ $backfilled = 0;
+ foreach ([['fc_post', 'updatetime'], ['fc_draft', null]] as $t) {
+ $rows = Db::name($t[0])->where('district_id', 0)->where('area', '<>', '')->field('id,area')->select();
+ foreach ($rows as $row) {
+ if (!isset($byName[$row['area']])) {
+ continue;
+ }
+ $data = ['district_id' => $byName[$row['area']]];
+ if ($t[1]) {
+ $data[$t[1]] = $now;
+ }
+ Db::name($t[0])->where('id', $row['id'])->update($data);
+ $backfilled++;
+ }
+ }
+ if ($backfilled) {
+ $output->writeln(' - 已回填 ' . $backfilled . ' 条信息/草稿的服务区域ID');
+ }
+
+ $output->writeln($changed || $backfilled
+ ? ' - 完成(结构变更 ' . $changed . ' 处,回填 ' . $backfilled . ' 条)'
+ : ' - 服务区域已是平铺结构,无需处理');
+ }
+
+
/** 表是否存在某列 */
private function hasColumn($table, $column)
{
@@ -336,6 +565,48 @@ class FcInstall extends Command
return !empty($cols);
}
+ /**
+ * v1.2.2:优惠券抵扣 + 佣金结算周期
+ * - fa_fc_order:origin_amount / coupon_amount / coupon_id
+ * - fa_fc_commission:settle_due(预计结算时间)
+ */
+ private function upgradeV122(Output $output)
+ {
+ $prefix = \think\Config::get('database.prefix');
+ $o = $prefix . 'fc_order';
+ $c = $prefix . 'fc_commission';
+
+ $changed = 0;
+
+ $orderCols = [
+ 'origin_amount' => "ALTER TABLE `{$o}` ADD COLUMN `origin_amount` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '优惠前金额'",
+ 'coupon_amount' => "ALTER TABLE `{$o}` ADD COLUMN `coupon_amount` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '优惠券抵扣金额'",
+ 'coupon_id' => "ALTER TABLE `{$o}` ADD COLUMN `coupon_id` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '使用的用户优惠券ID'",
+ ];
+ foreach ($orderCols as $col => $sql) {
+ if (!$this->hasColumn($o, $col)) {
+ Db::execute($sql);
+ $changed++;
+ }
+ }
+
+ if (!$this->hasColumn($c, 'settle_due')) {
+ Db::execute("ALTER TABLE `{$c}` ADD COLUMN `settle_due` int(10) DEFAULT NULL COMMENT '预计结算时间'");
+ $changed++;
+ }
+
+ $output->writeln($changed
+ ? ' - 完成(新增结构 ' . $changed . ' 处)'
+ : ' - 优惠券/结算字段已是最新,无需处理');
+ }
+
+ /** 表是否存在某个索引 */
+ private function hasIndex($table, $index)
+ {
+ $rows = Db::query("SHOW INDEX FROM `{$table}` WHERE Key_name = '" . addslashes($index) . "'");
+ return !empty($rows);
+ }
+
/** 读配置默认值(不依赖 api 基类的 cfg 缓存) */
private function cfgValue($key, $default = null)
{
@@ -350,184 +621,15 @@ class FcInstall extends Command
/**
* 初始化 FastAdmin 后台菜单(幂等)
*
- * 结构:
- * 同城业务 (fc)
- * ├─ 内容管理 分类管理 / 轮播管理 / 信息管理 / 评价管理 / 举报处理
- * ├─ 用户与商家 商家管理 / 会员管理 / 消息管理
- * ├─ 交易管理 订单管理 / 会员套餐 / 优惠券管理 / 资金流水 / 佣金明细 / 付费查看记录
- * ├─ 分销管理 分销关系 / 提现审核
- * └─ 基础数据 服务区域 / 草稿箱
- * v1.7.0 追加(幂等):数据总览 fc/dashboard(置首)、系统设置 fc/setting(置末),
- * 以及上述「交易管理」下的优惠券管理 fc/coupon 与它们的节点权限。
+ * 菜单结构统一定义在 FcMenu::GROUPS / FcMenu::ITEMS,
+ * 保证 fc:install 与 fc:menu 产出同一套菜单。
*/
protected function menu(Output $output)
{
- $now = time();
- $ruleTable = Db::name('auth_rule');
-
- // 1) 顶级菜单
- $top = $ruleTable->where('name', 'fc')->find();
- $topData = [
- 'type' => 'file', 'title' => '同城业务', 'icon' => 'fa fa-map-marker',
- 'ismenu' => 1, 'menutype' => 'addtabs', 'weigh' => 95,
- 'status' => 'normal', 'updatetime' => $now,
- ];
- if ($top) {
- $ruleTable->where('id', $top['id'])->update($topData);
- $topId = (int)$top['id'];
- } else {
- $topData['pid'] = 0;
- $topData['name'] = 'fc';
- $topData['url'] = '';
- $topData['createtime'] = $now;
- $topId = (int)$ruleTable->insertGetId($topData);
- }
-
- // 2) 分组菜单
- $groups = [
- ['fc/content', '内容管理', 'fa fa-file-text-o', 90],
- ['fc/user', '用户与商家', 'fa fa-users', 80],
- ['fc/trade', '交易管理', 'fa fa-shopping-cart', 70],
- ['fc/dist', '分销管理', 'fa fa-share-alt', 60],
- ['fc/base', '基础数据', 'fa fa-database', 50],
- ];
- $groupId = [];
- foreach ($groups as $g) {
- $row = $ruleTable->where('name', $g[0])->find();
- $data = [
- 'type' => 'menu', 'pid' => $topId, 'title' => $g[1], 'icon' => $g[2],
- 'url' => '', 'ismenu' => 1, 'menutype' => 'addtabs',
- 'weigh' => $g[3], 'status' => 'normal', 'updatetime' => $now,
- ];
- if ($row) {
- $ruleTable->where('id', $row['id'])->update($data);
- $groupId[$g[0]] = (int)$row['id'];
- } else {
- $data['name'] = $g[0];
- $data['createtime'] = $now;
- $groupId[$g[0]] = (int)$ruleTable->insertGetId($data);
- }
- }
-
- // 3) 各业务控制器归组 + 中文标题 + 图标 + 排序
- $items = [
- 'fc/category' => ['fc/content', '分类管理', 'fa fa-list', 95],
- 'fc/banner' => ['fc/content', '轮播管理', 'fa fa-image', 90],
- 'fc/post' => ['fc/content', '信息管理', 'fa fa-file-text', 85],
- 'fc/comment' => ['fc/content', '评价管理', 'fa fa-star', 80],
- 'fc/report' => ['fc/content', '举报处理', 'fa fa-flag', 75],
- 'fc/merchant' => ['fc/user', '商家管理', 'fa fa-building', 95],
- 'fc/userext' => ['fc/user', '会员管理', 'fa fa-user-circle', 90],
- 'fc/message' => ['fc/user', '消息管理', 'fa fa-bell', 85],
- 'fc/order' => ['fc/trade', '订单管理', 'fa fa-file-text-o', 95],
- 'fc/plan' => ['fc/trade', '会员套餐', 'fa fa-diamond', 90],
- 'fc/financeflow' => ['fc/trade', '资金流水', 'fa fa-money', 85],
- 'fc/commission' => ['fc/trade', '佣金明细', 'fa fa-cny', 80],
- 'fc/payview' => ['fc/trade', '付费查看记录', 'fa fa-eye', 75],
- 'fc/distrelation' => ['fc/dist', '分销关系', 'fa fa-sitemap', 95],
- 'fc/withdraw' => ['fc/dist', '提现审核', 'fa fa-credit-card', 90],
- 'fc/area' => ['fc/base', '服务区域', 'fa fa-map-o', 95],
- 'fc/publish' => ['fc/base', '发布与费用配置', 'fa fa-cny', 85],
- 'fc/draft' => ['fc/base', '草稿箱', 'fa fa-folder-o', 90],
- ];
- $done = 0;
- foreach ($items as $name => $m) {
- if (!isset($groupId[$m[0]])) {
- continue;
- }
- $aff = $ruleTable->where('name', $name)->update([
- 'pid' => $groupId[$m[0]],
- 'title' => $m[1],
- 'icon' => $m[2],
- 'weigh' => $m[3],
- 'ismenu' => 1,
- 'status' => 'normal',
- 'updatetime' => $now,
- ]);
- $done += $aff > 0 ? 1 : 1;
- }
-
- // 4) v1.7.0 新增功能项(按 name upsert:不存在则插入,存在则纠正归属/名称/图标/排序)
- // ID 交给数据库自增分配(当前 fa_auth_rule 最大 ID 为 224),不写死 ID,故不会占用已用 ID。
- // 顶级分组「同城业务」下:数据总览置首(weigh 99 > 内容管理 90)、系统设置置末(weigh 40 < 基础数据 50);
- // 「交易管理」下:优惠券管理排在会员套餐之后、资金流水之前。
- $groupId['fc'] = $topId;
- $newItems = [
- 'fc/dashboard' => ['fc', '数据总览', 'fa fa-dashboard', 99],
- 'fc/setting' => ['fc', '小程序设置', 'fa fa-cogs', 40],
- 'fc/coupon' => ['fc/trade', '优惠券管理', 'fa fa-ticket', 88],
- ];
- $newIds = [];
- $inserted = 0;
- foreach ($newItems as $name => $m) {
- if (!isset($groupId[$m[0]])) {
- continue;
- }
- $row = $ruleTable->where('name', $name)->find();
- $data = [
- 'pid' => $groupId[$m[0]], 'title' => $m[1], 'icon' => $m[2], 'weigh' => $m[3],
- 'ismenu' => 1, 'menutype' => 'addtabs', 'status' => 'normal', 'updatetime' => $now,
- ];
- if ($row) {
- $ruleTable->where('id', $row['id'])->update($data);
- $newIds[$name] = (int)$row['id'];
- } else {
- $data['type'] = 'menu';
- $data['name'] = $name;
- $data['url'] = '';
- $data['createtime'] = $now;
- $newIds[$name] = (int)$ruleTable->insertGetId($data);
- $inserted++;
- }
- }
-
- // 5) 新功能项的节点权限(Backend 按「控制器/方法」校验权限,缺节点则非超管会 403)
- $nodes = [
- 'fc/dashboard/index' => ['fc/dashboard', '查看'],
- 'fc/setting/index' => ['fc/setting', '查看'],
- 'fc/coupon/index' => ['fc/coupon', '查看'],
- 'fc/coupon/add' => ['fc/coupon', '添加'],
- 'fc/coupon/edit' => ['fc/coupon', '编辑'],
- 'fc/coupon/del' => ['fc/coupon', '删除'],
- 'fc/coupon/multi' => ['fc/coupon', '批量更新'],
- ];
- $nodeCount = 0;
- foreach ($nodes as $name => $n) {
- if (!isset($newIds[$n[0]])) {
- continue;
- }
- $row = $ruleTable->where('name', $name)->find();
- $data = [
- 'pid' => $newIds[$n[0]], 'title' => $n[1], 'icon' => 'fa fa-circle-o',
- 'ismenu' => 0, 'weigh' => 0, 'status' => 'normal', 'updatetime' => $now,
- ];
- if ($row) {
- $ruleTable->where('id', $row['id'])->update($data);
- } else {
- $data['type'] = 'file';
- $data['name'] = $name;
- $data['url'] = '';
- $data['menutype'] = 'addtabs';
- $data['createtime'] = $now;
- $ruleTable->insert($data);
- }
- $nodeCount++;
- }
-
- $output->writeln(' - 顶级 1 / 分组 ' . count($groups) . ' / 功能项 ' . $done);
- $output->writeln(' - 新功能项 ' . count($newItems) . ' 个(本次新插入 ' . $inserted . ')+ 权限节点 ' . $nodeCount . ' 个'
- . ':数据总览 fc/dashboard、系统设置 fc/setting、优惠券管理 fc/coupon');
-
- // 左侧菜单来自 Auth::getSidebar() 的 `__menu__` 缓存:不清这里,本命令刚插入的
- // 「数据总览 / 系统设置 / 优惠券管理」在缓存过期前不会出现在后台侧边栏。
- try {
- \think\Cache::rm('__menu__');
- $output->writeln(' - 已清理后台菜单缓存 __menu__(新菜单项即时可见)');
- } catch (\Exception $e) {
- // 清理失败不影响主流程:缓存过期后会自动重建
- }
+ FcMenu::apply($output);
}
+
/** 清空业务表(保留表结构) */
protected function truncateAll()
{
@@ -580,13 +682,19 @@ class FcInstall extends Command
'area' => "CREATE TABLE IF NOT EXISTS `#P#area` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
- `name` varchar(32) NOT NULL DEFAULT '',
+ `province` varchar(32) NOT NULL DEFAULT '' COMMENT '省份',
+ `city` varchar(32) NOT NULL DEFAULT '' COMMENT '城市',
+ `district` varchar(32) NOT NULL DEFAULT '' COMMENT '区县(最终服务区域)',
+ `lat` decimal(10,6) DEFAULT NULL COMMENT '中心纬度(gcj02)',
+ `lng` decimal(10,6) DEFAULT NULL COMMENT '中心经度(gcj02)',
`sort` int(10) NOT NULL DEFAULT '0',
`enabled` tinyint(1) NOT NULL DEFAULT '1',
`createtime` int(10) DEFAULT NULL,
`updatetime` int(10) DEFAULT NULL,
- PRIMARY KEY (`id`)
- ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='同城-服务区域';",
+ PRIMARY KEY (`id`),
+ KEY `province_city` (`province`,`city`),
+ KEY `enabled` (`enabled`)
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='同城-服务区域(省/市/区县平铺,区县为服务区域)';",
'post' => "CREATE TABLE IF NOT EXISTS `#P#post` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
@@ -598,7 +706,8 @@ class FcInstall extends Command
`media` text COMMENT '媒体JSON数组',
`price` decimal(10,2) NOT NULL DEFAULT '0.00',
`price_unit` varchar(16) NOT NULL DEFAULT '',
- `area` varchar(32) NOT NULL DEFAULT '',
+ `area` varchar(32) NOT NULL DEFAULT '' COMMENT '区县名称(展示用)',
+ `district_id` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '服务区域ID(fc_area.id)',
`address` varchar(128) NOT NULL DEFAULT '',
`contact` varchar(32) NOT NULL DEFAULT '' COMMENT '联系人',
`phone` varchar(20) NOT NULL DEFAULT '' COMMENT '联系电话(完整)',
@@ -621,7 +730,8 @@ class FcInstall extends Command
PRIMARY KEY (`id`),
KEY `category_id` (`category_id`),
KEY `user_id` (`user_id`),
- KEY `audit_status` (`audit_status`)
+ KEY `audit_status` (`audit_status`),
+ KEY `district_id` (`district_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='同城-信息';",
'draft' => "CREATE TABLE IF NOT EXISTS `#P#draft` (
@@ -634,6 +744,8 @@ class FcInstall extends Command
`contact` varchar(32) NOT NULL DEFAULT '',
`phone` varchar(20) NOT NULL DEFAULT '',
`address` varchar(128) NOT NULL DEFAULT '',
+ `area` varchar(32) NOT NULL DEFAULT '' COMMENT '区县名称',
+ `district_id` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '服务区域ID(fc_area.id)',
`savedat` int(10) DEFAULT NULL,
`createtime` int(10) DEFAULT NULL,
`updatetime` int(10) DEFAULT NULL,
@@ -698,10 +810,14 @@ class FcInstall extends Command
`title` varchar(64) NOT NULL DEFAULT '',
`amount` decimal(10,2) NOT NULL DEFAULT '0.00',
`pay_type` varchar(16) NOT NULL DEFAULT '',
- `status` varchar(16) NOT NULL DEFAULT 'pending' COMMENT 'pending/paid/refunded/closed',
+ `status` varchar(16) NOT NULL DEFAULT 'pending' COMMENT 'pending/paid/refunding/refunded/closed',
`paytime` int(10) DEFAULT NULL,
+ `origin_amount` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '优惠前金额',
+ `coupon_amount` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '优惠券抵扣金额',
+ `coupon_id` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '使用的用户优惠券ID(fc_user_coupon.id)',
`refund_amount` decimal(10,2) NOT NULL DEFAULT '0.00',
`refund_reason` varchar(255) NOT NULL DEFAULT '',
+ `out_refund_no` varchar(64) NOT NULL DEFAULT '' COMMENT '微信退款单号(商户侧)',
`related_id` int(10) NOT NULL DEFAULT '0' COMMENT '关联业务ID',
`createtime` int(10) DEFAULT NULL,
`updatetime` int(10) DEFAULT NULL,
@@ -734,6 +850,7 @@ class FcInstall extends Command
`openid` varchar(64) NOT NULL DEFAULT '' COMMENT '微信openid',
`user_type` tinyint(1) NOT NULL DEFAULT '1' COMMENT '1个人2商家',
`realname_status` tinyint(1) NOT NULL DEFAULT '0',
+ `realname` varchar(32) NOT NULL DEFAULT '' COMMENT '收款人真实姓名(提现用)',
`is_member` tinyint(1) NOT NULL DEFAULT '0',
`member_level` varchar(32) NOT NULL DEFAULT '',
`member_expiretime` int(10) DEFAULT NULL,
@@ -772,6 +889,7 @@ class FcInstall extends Command
`rate` decimal(5,2) NOT NULL DEFAULT '0.00' COMMENT '分佣比例%',
`level` tinyint(1) NOT NULL DEFAULT '1',
`status` tinyint(1) NOT NULL DEFAULT '0' COMMENT '0未结算1已结算',
+ `settle_due` int(10) DEFAULT NULL,
`settletime` int(10) DEFAULT NULL,
`createtime` int(10) DEFAULT NULL,
PRIMARY KEY (`id`),
@@ -784,14 +902,21 @@ class FcInstall extends Command
`user_id` int(10) unsigned NOT NULL DEFAULT '0',
`amount` decimal(10,2) NOT NULL DEFAULT '0.00',
`channel` varchar(16) NOT NULL DEFAULT 'wxpay',
- `status` varchar(16) NOT NULL DEFAULT 'auditing' COMMENT 'auditing/paid/rejected',
+ `status` varchar(16) NOT NULL DEFAULT 'auditing' COMMENT 'auditing/paying/paid/failed/rejected',
`reason` varchar(255) NOT NULL DEFAULT '' COMMENT '驳回原因',
`paytime` int(10) DEFAULT NULL,
+ `out_bill_no` varchar(32) NOT NULL DEFAULT '' COMMENT '微信商家转账商户单号',
+ `transfer_status` varchar(16) NOT NULL DEFAULT '' COMMENT 'WAIT_USER_CONFIRM/PROCESSING/SUCCESS/FAIL/CANCELLED',
+ `transfer_id` varchar(64) NOT NULL DEFAULT '' COMMENT '微信转账单号',
+ `package_info` varchar(512) NOT NULL DEFAULT '' COMMENT '拉起确认收款页的package',
+ `fail_reason` varchar(255) NOT NULL DEFAULT '',
+ `transfer_time` int(10) DEFAULT NULL,
`createtime` int(10) DEFAULT NULL,
`updatetime` int(10) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `withdraw_no` (`withdraw_no`),
- KEY `user_id` (`user_id`)
+ KEY `user_id` (`user_id`),
+ KEY `out_bill_no` (`out_bill_no`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='同城-提现';",
'finance_flow' => "CREATE TABLE IF NOT EXISTS `#P#finance_flow` (
@@ -977,11 +1102,19 @@ class FcInstall extends Command
]);
}
- /* ---------- 服务区域 ---------- */
- $areas = [['云岩区', 1, 1], ['南明区', 2, 1], ['观山湖区', 3, 1], ['花溪区', 4, 1], ['白云区', 5, 0]];
+ /* ---------- 服务区域(省 / 市 / 区县平铺,区县为服务区域) ---------- */
+ $areas = [
+ ['贵州省', '贵阳市', '云岩区', 1, 1, 26.605, 106.710],
+ ['贵州省', '贵阳市', '南明区', 2, 1, 26.568, 106.716],
+ ['贵州省', '贵阳市', '观山湖区', 3, 1, 26.625, 106.625],
+ ['贵州省', '贵阳市', '花溪区', 4, 1, 26.430, 106.670],
+ ['贵州省', '贵阳市', '白云区', 5, 0, 26.678, 106.623],
+ ];
foreach ($areas as $a) {
Db::name('fc_area')->insert([
- 'name' => $a[0], 'sort' => $a[1], 'enabled' => $a[2],
+ 'province' => $a[0], 'city' => $a[1], 'district' => $a[2],
+ 'lat' => $a[5], 'lng' => $a[6],
+ 'sort' => $a[3], 'enabled' => $a[4],
'createtime' => $now, 'updatetime' => $now,
]);
}
@@ -1000,7 +1133,7 @@ class FcInstall extends Command
'createtime' => $now, 'updatetime' => $now,
]);
}
- $output->writeln(' - 分类8 / 轮播3 / 区域5 / 套餐3');
+ $output->writeln(' - 分类8 / 轮播3 / 服务区域5 / 套餐3');
/* ---------- 演示用户(显式ID,与前端 mock 数据集一致) ---------- */
$salt = 'fcseed';
diff --git a/application/admin/command/FcMenu.php b/application/admin/command/FcMenu.php
new file mode 100644
index 0000000..972e3cc
--- /dev/null
+++ b/application/admin/command/FcMenu.php
@@ -0,0 +1,309 @@
+ [标题, 图标, 权重] */
+ const GROUPS = [
+ 'console' => ['数据看板', 'fa fa-dashboard', 100],
+ 'fc/content' => ['信息管理', 'fa fa-file-text-o', 90],
+ 'fc/user' => ['商家会员', 'fa fa-users', 80],
+ 'fc/trade' => ['交易财务', 'fa fa-shopping-cart', 70],
+ 'fc/marketing' => ['运营推广', 'fa fa-share-alt', 60],
+ 'fc/auth' => ['权限配置', 'fa fa-group', 50],
+ 'fc/system' => ['系统设置', 'fa fa-cogs', 40],
+ ];
+
+ /** 功能菜单:name => [一级菜单, 标题, 图标, 权重] */
+ const ITEMS = [
+ // 数据看板
+ 'fc/dashboard' => ['console', '业务总览', 'fa fa-dashboard', 98],
+ // 信息管理:信息 + 分类 + 审核
+ 'fc/post' => ['fc/content', '信息列表', 'fa fa-file-text', 95],
+ 'fc/category' => ['fc/content', '分类管理', 'fa fa-list', 90],
+ 'fc/draft' => ['fc/content', '草稿管理', 'fa fa-folder-o', 85],
+ 'fc/comment' => ['fc/content', '评价管理', 'fa fa-star', 80],
+ 'fc/report' => ['fc/content', '举报处理', 'fa fa-flag', 75],
+ // 商家会员
+ 'fc/merchant' => ['fc/user', '商家管理', 'fa fa-building', 95],
+ 'fc/userext' => ['fc/user', '用户管理', 'fa fa-user-circle', 90],
+ 'fc/message' => ['fc/user', '消息管理', 'fa fa-bell', 85],
+ // 交易财务
+ 'fc/order' => ['fc/trade', '订单管理', 'fa fa-file-text-o', 95],
+ 'fc/financeflow' => ['fc/trade', '资金流水', 'fa fa-money', 90],
+ 'fc/payview' => ['fc/trade', '付费查看', 'fa fa-eye', 85],
+ 'fc/commission' => ['fc/trade', '佣金明细', 'fa fa-cny', 80],
+ 'fc/withdraw' => ['fc/trade', '提现审核', 'fa fa-credit-card', 75],
+ // 运营推广
+ 'fc/banner' => ['fc/marketing', '轮播管理', 'fa fa-image', 95],
+ 'fc/plan' => ['fc/marketing', '会员套餐', 'fa fa-diamond', 90],
+ 'fc/coupon' => ['fc/marketing', '优惠券', 'fa fa-ticket', 85],
+ 'fc/distrelation' => ['fc/marketing', '分销关系', 'fa fa-sitemap', 80],
+ // 权限配置
+ 'auth/admin' => ['fc/auth', '管理员', 'fa fa-user', 95],
+ 'auth/group' => ['fc/auth', '角色管理', 'fa fa-group', 90],
+ 'auth/rule' => ['fc/auth', '菜单规则', 'fa fa-bars', 85],
+ 'auth/adminlog' => ['fc/auth', '操作日志', 'fa fa-list-alt', 80],
+ // 系统设置
+ 'general/config' => ['fc/system', '系统配置', 'fa fa-cog', 99],
+ 'dashboard' => ['fc/system', '平台概况', 'fa fa-dashboard', 95],
+ 'fc/area' => ['fc/system', '服务区域', 'fa fa-map-o', 90],
+ 'general/attachment' => ['fc/system', '附件管理', 'fa fa-file-image-o', 80],
+ 'general/profile' => ['fc/system', '个人资料', 'fa fa-user', 70],
+ 'addon' => ['fc/system', '插件管理', 'fa fa-rocket', 60],
+ ];
+
+ /** 已并入系统配置页、不再单独显示的历史菜单 */
+ const RETIRED = ['fc/setting', 'fc/publish/index'];
+
+ /** 权限节点:name => [上级菜单, 标题] */
+ const NODES = [
+ 'fc/dashboard/index' => ['fc/dashboard', '查看'],
+ 'fc/coupon/index' => ['fc/coupon', '查看'],
+ 'fc/coupon/add' => ['fc/coupon', '添加'],
+ 'fc/coupon/edit' => ['fc/coupon', '编辑'],
+ 'fc/coupon/del' => ['fc/coupon', '删除'],
+ 'fc/coupon/multi' => ['fc/coupon', '批量更新'],
+ ];
+
+ /** 需要整体下线的 FastAdmin 内置菜单(公众号模块 / 框架会员模块) */
+ const OFFLINE = [
+ 'wechat' => '公众号模块与本项目小程序无关,下线',
+ 'user' => '框架会员模块与「商家会员 → 用户管理」重复,下线',
+ ];
+
+ /** 下线菜单统一改为 4 字中文,避免规则列表里出现英文/超长标题 */
+ const OFFLINE_TITLES = [
+ 'user' => '会员管理',
+ 'user/user' => '会员管理',
+ 'user/group' => '会员分组',
+ 'user/rule' => '会员规则',
+ 'wechat' => '微信管理',
+ 'wechat/autoreply' => '自动回复',
+ 'wechat/response' => '资源管理',
+ 'wechat/config' => '配置管理',
+ 'wechat/menu' => '菜单管理',
+ ];
+
+ /** 需要清理的演示菜单 */
+ const DEMO = ['category'];
+
+ protected function configure()
+ {
+ $this->setName('fc:menu')
+ ->setDescription('重建同城业务后台菜单结构(幂等)');
+ }
+
+ protected function execute(Input $input, Output $output)
+ {
+ self::apply($output);
+ }
+
+ /**
+ * 应用目标菜单树
+ *
+ * @param Output|null $output
+ * @return void
+ */
+ public static function apply(Output $output = null)
+ {
+ $now = time();
+ $table = Db::name('auth_rule');
+
+ // 0) 旧分组更名:分销管理 → 运营推广,避免按 name 定位时新建一个空分组
+ if (!$table->where('name', 'fc/marketing')->find()) {
+ $old = $table->where('name', 'fc/dist')->find();
+ if ($old) {
+ $table->where('id', $old['id'])->update([
+ 'name' => 'fc/marketing',
+ 'updatetime' => $now,
+ ]);
+ }
+ }
+
+ // 1) 一级菜单
+ $groupIds = [];
+ foreach (self::GROUPS as $name => $item) {
+ $groupIds[$name] = self::put($table, $name, [
+ 'type' => 'menu',
+ 'pid' => 0,
+ 'title' => $item[0],
+ 'icon' => $item[1],
+ 'ismenu' => 1,
+ 'weigh' => $item[2],
+ ], $now);
+ }
+
+ // 2) 功能菜单
+ $itemIds = [];
+ foreach (self::ITEMS as $name => $item) {
+ $parent = $groupIds[$item[0]] ?? ($groupIds['fc/system'] ?? 0);
+ $itemIds[$name] = self::put($table, $name, [
+ 'type' => 'file',
+ 'pid' => $parent,
+ 'title' => $item[1],
+ 'icon' => $item[2],
+ 'ismenu' => 1,
+ 'weigh' => $item[3],
+ ], $now);
+ }
+
+ // 3) 权限节点(只补缺,不改业务已用的标题)
+ $nodeCount = 0;
+ foreach (self::NODES as $name => $node) {
+ $parent = $itemIds[$node[0]] ?? ($groupIds[$node[0]] ?? 0);
+ if (!$parent) {
+ continue;
+ }
+ self::put($table, $name, [
+ 'type' => 'file',
+ 'pid' => $parent,
+ 'title' => $node[1],
+ 'icon' => 'fa fa-circle-o',
+ 'ismenu' => 0,
+ 'weigh' => 0,
+ ], $now);
+ $nodeCount++;
+ }
+
+ // 4) 菜单页说明(build_heading 读取 auth_rule.remark,而不是控制器注释)
+ $table->where('name', 'auth/rule')->update([
+ 'remark' => '左侧菜单与权限节点都在这里维护;修改后左侧导航自动刷新,删除菜单会连同其子级一起删除',
+ ]);
+
+ // 5) 演示菜单清理
+ $removed = 0;
+ foreach (self::DEMO as $name) {
+ $row = $table->where('name', $name)->find();
+ if (!$row) {
+ continue;
+ }
+ $ids = array_merge([(int)$row['id']], $table->where('pid', $row['id'])->column('id'));
+ $removed += $table->where('id', 'in', $ids)->delete();
+ }
+
+ // 6) 下线框架/公众号菜单(父级隐藏后,子级不会再出现在侧边栏)
+ $offline = 0;
+ foreach (array_keys(self::OFFLINE) as $name) {
+ $row = $table->where('name', $name)->find();
+ if (!$row) {
+ continue;
+ }
+ $table->where('id', $row['id'])->update(['status' => 'hidden', 'updatetime' => $now]);
+ $table->where('pid', $row['id'])->update(['status' => 'hidden', 'updatetime' => $now]);
+ $offline++;
+ }
+ foreach (self::OFFLINE_TITLES as $name => $title) {
+ $table->where('name', $name)->update(['title' => $title]);
+ }
+
+ // 6.1) 已并入系统配置页的历史菜单,不再单独显示
+ foreach (self::RETIRED as $name) {
+ $table->where('name', $name)->update(['status' => 'hidden', 'updatetime' => $now]);
+ }
+
+ // 7) 清理已搬空的历史分组(同城业务 / 基础数据 / 常规管理 / 权限管理)
+ $emptyRemoved = 0;
+ foreach (['fc', 'fc/base', 'general', 'auth'] as $name) {
+ $row = $table->where('name', $name)->find();
+ if ($row && !$table->where('pid', $row['id'])->count()) {
+ $table->where('id', $row['id'])->delete();
+ $emptyRemoved++;
+ }
+ }
+
+ // 8) 菜单缓存
+ try {
+ Cache::rm('__menu__');
+ } catch (\Exception $e) {
+ // 缓存清理失败不影响菜单数据,过期后会自动重建
+ }
+
+ if ($output) {
+ // 菜单命名规范:不超过 5 个汉字
+ $tooLong = [];
+ foreach (self::GROUPS as $group) {
+ if (mb_strlen($group[0], 'UTF-8') > 5) {
+ $tooLong[] = $group[0];
+ }
+ }
+ foreach (self::ITEMS as $item) {
+ if (mb_strlen($item[1], 'UTF-8') > 5) {
+ $tooLong[] = $item[1];
+ }
+ }
+
+ $output->writeln(' - 一级菜单 ' . count(self::GROUPS) . ' / 功能菜单 ' . count(self::ITEMS));
+ $output->writeln(' - 权限节点 ' . $nodeCount . ' 个;清理演示菜单 ' . $removed . ' 条;下线框架菜单 ' . $offline . ' 组;移除空分组 ' . $emptyRemoved . ' 个');
+ if ($tooLong) {
+ $output->writeln(' - 警告:以下菜单超过 5 个汉字:' . implode('、', $tooLong));
+ }
+ $output->writeln(' - 已清理后台菜单缓存 __menu__');
+ }
+ }
+
+ /**
+ * 按 name 写入一条规则:存在则更新,不存在则插入
+ *
+ * @param \think\db\Query $table
+ * @param string $name
+ * @param array $data
+ * @param int $now
+ * @return int
+ */
+ protected static function put($table, $name, array $data, $now)
+ {
+ $data = array_merge([
+ 'type' => 'file',
+ 'pid' => 0,
+ 'title' => $name,
+ 'icon' => 'fa fa-circle-o',
+ 'url' => '',
+ 'ismenu' => 1,
+ 'menutype' => 'addtabs',
+ 'weigh' => 0,
+ 'status' => 'normal',
+ ], $data);
+ $data['updatetime'] = $now;
+
+ $row = $table->where('name', $name)->find();
+ if ($row) {
+ $table->where('id', $row['id'])->update($data);
+ return (int)$row['id'];
+ }
+
+ $data['name'] = $name;
+ $data['createtime'] = $now;
+ return (int)$table->insertGetId($data);
+ }
+}
diff --git a/application/admin/command/FcSeedPosts.php b/application/admin/command/FcSeedPosts.php
new file mode 100644
index 0000000..7e4ea68
--- /dev/null
+++ b/application/admin/command/FcSeedPosts.php
@@ -0,0 +1,464 @@
+ [昵称, 手机号] */
+ const USERS = [
+ 1001 => ['同城小助手', '13800001111'],
+ 1002 => ['小王同学', '15033443344'],
+ 1003 => ['洁净到家', '13156785678'],
+ 2001 => ['顺捷物流', '13988888888'],
+ 2002 => ['安家房产', '13566666666'],
+ 2003 => ['骑行爱好者', '13712341234'],
+ 2004 => ['车行天下', '13800000000'],
+ 2005 => ['洁净到家', '13156785678'],
+ 2006 => ['小王同学', '15033443344'],
+ 2007 => ['优选二手车', '13700000000'],
+ ];
+
+ /** 区域中心坐标(gcj02 近似值,用于首页「附近」排序演示) */
+ const AREA_COORD = [
+ '云岩区' => [26.605, 106.710],
+ '南明区' => [26.568, 106.716],
+ '观山湖区' => [26.625, 106.625],
+ '花溪区' => [26.430, 106.670],
+ ];
+
+ protected function configure()
+ {
+ $this->setName('fc:seed-posts')
+ ->setDescription('清理不规范测试数据,按分类重建信息演示数据');
+ }
+
+ protected function execute(Input $input, Output $output)
+ {
+ $now = time();
+ $prefix = \think\Config::get('database.prefix');
+ $output->writeln('==> 清理不规范测试数据');
+ $this->clean();
+
+ $data = $this->posts();
+ $owners = $this->owners();
+
+ Db::startTrans();
+ try {
+ $id = 101;
+ $seq = 0;
+ $stat = [];
+ foreach ($data as $categoryId => $rows) {
+ $stat[$categoryId] = 0;
+ foreach ($rows as $i => $row) {
+ $uid = $owners[$categoryId][$i];
+ $user = self::USERS[$uid];
+ $audit = $this->auditStatus($categoryId, $i);
+ $paid = $audit === 1 && $categoryId !== 8 && $i % 4 === 1;
+ $views = 160 + (($categoryId * 977 + $i * 613) % 3200);
+ $likes = max(2, (int)($views / 48));
+ $areas = $this->coord($row[4], $i);
+ $created = $now - (int)((0.15 + $i * 0.41 + $categoryId * 0.13) * 86400);
+ $media = $this->mediaPaths($categoryId, $i);
+
+ $areaId = $this->areaId($row[5]);
+
+ Db::name('fc_post')->insert([
+ 'id' => $id,
+ 'user_id' => $uid,
+ 'category_id' => $categoryId,
+ 'title' => $row[1],
+ 'content' => $row[2],
+ 'cover' => $media[0],
+ 'media' => json_encode($media, JSON_UNESCAPED_UNICODE),
+ 'price' => $row[3],
+ 'price_unit' => $row[4],
+ 'area' => $row[5],
+ 'district_id' => $areaId,
+ 'address' => $row[6],
+ 'lat' => $areas[0],
+ 'lng' => $areas[1],
+ 'contact' => $row[0],
+ 'phone' => $user[1],
+ 'wechat' => 'gyfc' . $id,
+ 'tags' => $row[7],
+ 'is_paid' => $paid ? 1 : 0,
+ 'amount' => $paid ? 5 : 0,
+ 'pay_type' => $paid ? ($i % 2 === 1 ? 'wxpay' : 'balance') : '',
+ 'audit_status' => $audit,
+ 'audit_reason' => $audit === 2 ? '信息涉嫌重复发布' : '',
+ 'views' => $audit === 1 ? $views : 0,
+ 'likes' => $audit === 1 ? $likes : 0,
+ 'comment_count' => 0,
+ 'status' => 1,
+ 'is_top' => $this->isTop($categoryId, $i) ? 1 : 0,
+ 'distance' => 0,
+ 'createtime' => $created,
+ 'updatetime' => $created,
+ ]);
+
+ $id++;
+ $seq++;
+ if ($audit === 1) {
+ $stat[$categoryId]++;
+ }
+ }
+ }
+ $output->writeln(' - 信息 ' . $seq . ' 条(已更新 ID 101-' . ($id - 1) . ')');
+
+ $this->seedComments($now);
+ $this->seedDrafts($now);
+ $this->seedReports($now);
+ $this->syncCounts();
+
+ Db::commit();
+ } catch (\Exception $e) {
+ Db::rollback();
+ $output->writeln(' [x] 重建失败:' . $e->getMessage());
+ return;
+ }
+
+ Db::execute('ALTER TABLE `' . $prefix . 'fc_post` AUTO_INCREMENT = 165');
+
+ foreach ($stat as $categoryId => $count) {
+ $name = Db::name('fc_category')->where('id', $categoryId)->value('name');
+ $output->writeln(' - ' . $name . ':' . $count . ' 条已发布');
+ }
+ $output->writeln('==> 完成');
+ }
+
+ /** 清理内容类测试数据(保留用户、订单、佣金、配置) */
+ protected function clean()
+ {
+ Db::name('fc_comment')->where('id', '>', 0)->delete();
+ Db::name('fc_favorite')->where('id', '>', 0)->delete();
+ Db::name('fc_pay_view')->where('id', '>', 0)->delete();
+ Db::name('fc_draft')->where('id', '>', 0)->delete();
+ Db::name('fc_report')->where('id', '>', 0)->delete();
+ Db::name('fc_post')->where('id', '>', 0)->delete();
+ Db::name('fc_area')->where('district', '接口验证区')->delete();
+ Db::name('fc_message')->where('title', 'like', '%接口验证%')->whereOr('content', 'like', '%接口验证%')->delete();
+ $this->cleanMockUsers();
+ }
+
+ /** 清理历史自动化脚本遗留的临时用户及其关联数据 */
+ protected function cleanMockUsers()
+ {
+ $ids = Db::name('user')
+ ->where('username', 'like', 'test\\_dist\\_%')
+ ->whereOr('username', 'like', 'mock\\_test\\_dist\\_%')
+ ->column('id');
+ if (!$ids) {
+ return;
+ }
+ Db::name('fc_user_ext')->where('user_id', 'in', $ids)->delete();
+ Db::name('fc_dist_relation')->where('user_id', 'in', $ids)->whereOr('parent_id', 'in', $ids)->delete();
+ Db::name('fc_order')->where('user_id', 'in', $ids)->delete();
+ Db::name('fc_commission')->where('user_id', 'in', $ids)->whereOr('from_user_id', 'in', $ids)->delete();
+ Db::name('fc_withdraw')->where('user_id', 'in', $ids)->delete();
+ Db::name('fc_message')->where('user_id', 'in', $ids)->delete();
+ Db::name('user')->where('id', 'in', $ids)->delete();
+ }
+
+ /** 每个分类下的发布账号,按信息顺序一一对应 */
+ protected function owners()
+ {
+ return [
+ 1 => [2001, 1002, 2002, 2005, 1002, 2001, 1003, 2004],
+ 2 => [2002, 2002, 2002, 2002, 1001, 2002, 2002, 1001],
+ 3 => [2003, 2006, 1002, 2005, 1003, 1001, 2003, 2006],
+ 4 => [2004, 2007, 2004, 2007, 2004, 2007, 2003, 2007],
+ 5 => [2005, 1003, 1001, 1003, 2005, 1003, 1002, 1001],
+ 6 => [1002, 1003, 1001, 1002, 1003, 1001, 1002, 1003],
+ 7 => [1003, 2005, 1003, 2005, 1003, 2005, 1003, 2005],
+ 8 => [1002, 2003, 1001, 1002, 2003, 1001, 1002, 2003],
+ ];
+ }
+
+ /** 审核状态:默认通过;少量待审/驳回用于验证后台流程 */
+ protected function auditStatus($categoryId, $index)
+ {
+ $pending = [[4, 6], [8, 7]];
+ $rejected = [[3, 5]];
+ if (in_array([$categoryId, $index], $pending, true)) {
+ return 0;
+ }
+ if (in_array([$categoryId, $index], $rejected, true)) {
+ return 2;
+ }
+ return 1;
+ }
+
+ /** 置顶:每个分类最多一条,制造首页头部内容 */
+ protected function isTop($categoryId, $index)
+ {
+ $tops = [[1, 0], [3, 1], [5, 0], [7, 1]];
+ return in_array([$categoryId, $index], $tops, true);
+ }
+
+ /** 区县名称 → 服务区域ID(fc_area.id) */
+ protected function areaId($area)
+ {
+ static $cache = [];
+ if (isset($cache[$area])) {
+ return $cache[$area];
+ }
+ $row = Db::name('fc_area')->where('district', $area)->find();
+ return $cache[$area] = $row ? (int)$row['id'] : 0;
+ }
+
+ /** 区域中心 + 稳定偏移,避免所有信息坐标完全重合 */
+ protected function coord($area, $index)
+ {
+ $base = isset(self::AREA_COORD[$area]) ? self::AREA_COORD[$area] : self::AREA_COORD['云岩区'];
+ $lat = $base[0] + (($index % 5) - 2) * 0.0055;
+ $lng = $base[1] + ((int)($index / 2) - 2) * 0.0065;
+ return [round($lat, 6), round($lng, 6)];
+ }
+
+ /**
+ * 信息配图:每条信息使用 1 张与内容匹配的真实照片作为封面和详情图。
+ * 图片存放在 public/static/posts/,随代码仓库部署,不依赖第三方外链。
+ */
+ protected function mediaPaths($categoryId, $index)
+ {
+ $primary = $this->primaries()[$categoryId][$index];
+ return ['/static/posts/' . $primary . '.jpg'];
+ }
+
+ /** 每条信息的首图(与 posts() 的顺序一一对应) */
+ protected function primaries()
+ {
+ return [
+ 1 => ['job-warehouse', 'job-cafe', 'job-office', 'job-cleaning', 'job-warehouse', 'job-delivery', 'job-classroom', 'job-handshake'],
+ 2 => ['house-living1', 'house-living3', 'house-office', 'house-model', 'house-living2', 'house-living4', 'house-exterior', 'job-cafe'],
+ 3 => ['goods-bike', 'goods-ipad', 'goods-dining', 'goods-fridge', 'goods-stroller', 'goods-laptop', 'goods-treadmill', 'goods-desk'],
+ 4 => ['car-sedan-white', 'car-compact-blue', 'car-moto', 'car-hatch-red', 'car-coupe-blue', 'car-suv-white', 'goods-bike', 'car-sedan-dark'],
+ 5 => ['svc-spray', 'svc-keys', 'svc-code', 'svc-wrench', 'svc-moving', 'svc-drill', 'svc-camera', 'svc-night'],
+ 6 => ['edu-art', 'edu-books', 'edu-shelf', 'edu-guitar', 'edu-laptop', 'edu-class', 'car-sedan-white', 'edu-shelf'],
+ 7 => ['clean-window', 'clean-bath', 'clean-baby', 'clean-kitchen', 'clean-kitchen', 'clean-window', 'clean-elder', 'clean-vacuum'],
+ 8 => ['friend-hike', 'friend-badminton', 'friend-books', 'friend-run', 'friend-camera', 'friend-bike', 'friend-board', 'friend-group'],
+ ];
+ }
+
+ /** 信息数据集:联系人 / 标题 / 内容 / 价格 / 单位 / 区域 / 地址 / 标签 */
+ protected function posts()
+ {
+ return [
+ // 招聘求职
+ 1 => [
+ ['杨经理', '急招快递分拣员(日结)', '站点直招,日结 180-220 元,包工作餐,当天结算。能吃苦、手脚麻利即可,年龄 18-50 岁。', 0, '面议', '云岩区', '贵阳北站物流园', '日结,包吃'],
+ ['李店长', '奶茶店店员(包住)', '花果园商圈奶茶店招店员,负责点单和出杯。早班 10 点到 18 点,月休 4 天,包住,有提成。', 3800, '元/月', '南明区', '花果园购物中心', '包住,有提成'],
+ ['周主管', '行政文员(双休)', '负责合同整理、客户接待和日常表格。会基本办公软件,做事细心,转正后买五险。', 4200, '元/月', '观山湖区', '金融城', '双休,五险'],
+ ['王师傅', '招保洁员 5 名', '承接小区和写字楼保洁,公司派单,不用自己找客户。多劳多得,工资月结。', 3000, '元/月', '云岩区', '喷水池', '多劳多得,月结'],
+ ['张店长', '超市理货员(兼职)', '每天下午 4 点到 9 点,负责补货和整理货架。适合学生和宝妈,工资周结。', 18, '元/小时', '南明区', '花果园', '兼职,周结'],
+ ['刘站长', '外卖骑手(可日结)', '自带电动车优先,站点提供充电和休息区。跑单时间自由,多跑多得。', 6000, '元/月', '观山湖区', '世纪城', '日结,时间自由'],
+ ['陈老师', '幼儿园保育员', '负责班级卫生和孩子日常照料,有保育员证优先。周末双休,包午餐。', 3200, '元/月', '花溪区', '贵州大学附近', '双休,包午餐'],
+ ['吴经理', '汽车销售顾问', '负责到店客户接待和车型介绍,有销售经验优先。底薪加提成,上不封顶。', 5000, '元/月', '观山湖区', '金阳客车站旁', '底薪加提成,五险'],
+ ],
+ // 房产租售
+ 2 => [
+ ['刘经理', '整租·未来方舟精装一室一厅', '家电齐全,拎包入住,近地铁 2 号线,押一付一,可短租。', 1800, '元/月', '南明区', '未来方舟 B 区', '精装,近地铁'],
+ ['王女士', '花果园两室一厅 南北通透', '中间楼层,采光好,家具家电全带,随时看房。', 2200, '元/月', '南明区', '花果园', '采光好,随时看房'],
+ ['周先生', '观山湖金融城写字楼出租', '80 平,独立办公室,带中央空调和前台,适合小团队。', 45, '元/㎡/月', '观山湖区', '金融城', '写字楼,带空调'],
+ ['李女士', '云岩区学区房出售 满五唯一', '两室一厅 78 平,划片实验小学,业主置换急售。', 98, '万元', '云岩区', '喷水池', '学区房,满五唯一'],
+ ['小杨', '花溪大学城单间合租', '独立卫生间,室友都是上班族,环境安静,押一付三。', 650, '元/月', '花溪区', '贵州大学北门', '合租,独卫'],
+ ['刘经理', '世纪城三室两厅 拎包入住', '130 平,电梯房,家电齐全,适合一家人住。', 2600, '元/月', '观山湖区', '世纪城', '电梯房,家电齐全'],
+ ['陈先生', '甲秀楼旁两室出售', '65 平,位置方便,出门就是菜场和公交站,价格可谈。', 62, '万元', '南明区', '甲秀楼', '位置好,可议价'],
+ ['小杨', '观山湖门面转让(可空转)', '临街 40 平,带基础装修,适合小吃和便利店,接手就能营业。', 3500, '元/月', '观山湖区', '奥体中心', '临街,可空转'],
+ ],
+ // 二手闲置
+ 3 => [
+ ['小周', '九成新山地自行车 转让', '骑行不到 300 公里,27 速,原价 1599,现 680 出,可小刀,同城自提。', 680, '元', '观山湖区', '世纪城', '自提,9成新'],
+ ['小王', 'iPad 2021 64G 深空灰', '学生自用,无磕碰,配件齐全,支持当面验机。', 1450, '元', '观山湖区', '贵阳奥体中心', '验机,无磕碰'],
+ ['小林', '实木餐桌椅 四人位', '搬家带不走,桌面无划痕,椅子结实,自提价。', 420, '元', '南明区', '花果园', '自提,实木'],
+ ['王姐', '美的双门冰箱 260L', '用了两年,制冷正常,因为换大冰箱所以出,可上门看货。', 550, '元', '云岩区', '喷水池', '可看货,制冷正常'],
+ ['小陈', '好孩子婴儿推车', '宝宝大了用不上,可坐可躺,折叠方便,清洗干净。', 180, '元', '花溪区', '花溪公园', '可折叠,已清洗'],
+ ['小杨', '联想拯救者游戏本', '16G 内存,512G 固态,打游戏不卡,电池健康,带原装充电器。', 3200, '元', '南明区', '花果园', '游戏本,带充电器'],
+ ['小周', '家用跑步机 九成新', '买来跑了不到十次,可折叠收纳,同城自提。', 800, '元', '观山湖区', '观山湖公园', '自提,可折叠'],
+ ['小王', '书架加办公桌一套', '租房换地方,便宜出,书桌 1.2 米,书架五层。', 200, '元', '观山湖区', '世纪城', '一套,便宜出'],
+ ],
+ // 车辆买卖
+ 4 => [
+ ['吴经理', '2019 款奥迪 A4L 一手车', '白色车身,车况好,无事故无泡水,4S 店保养记录齐全,支持第三方检测。', 12.8, '万元', '云岩区', '金阳客车站旁', '无事故,可检测'],
+ ['优选二手车', '城市代步小车 2022 款', '车身小巧好停车,续航够日常通勤,跑了 1.2 万公里,手续齐全。', 2.6, '万元', '观山湖区', '世纪城', '代步,手续齐全'],
+ ['吴经理', '雅马哈踏板摩托车', '125 排量,车况好,油耗低,带牌照一起出,可试车。', 4500, '元', '南明区', '花果园', '可试车,带牌'],
+ ['老李', '二手代步小车 省油好开', '个人一手车,1.5 排量,市区代步省油,保养刚做,可试车。', 1.8, '万元', '云岩区', '贵阳北站', '省油,可试车'],
+ ['吴经理', '宝马 4 系 2018 款', '蓝色车身,车况好,无事故,支持第三方检测,可分期。', 15.8, '万元', '观山湖区', '金融城', '无事故,可分期'],
+ ['优选二手车', '家用七座 SUV 车况好', '七座大空间,适合一家人出行,无事故,保养记录齐全,可检测。', 8.8, '万元', '南明区', '龙洞堡', '7座,可检测'],
+ ['小周', '电助力山地车', '36V 电池,助力续航 50 公里,上下班代步很省力。', 1600, '元', '观山湖区', '奥体中心', '助力,省力'],
+ ['老李', '宝马 3 系 2019 款', '车况好,内饰干净,无事故,支持第三方检测,可分期。', 12.8, '万元', '花溪区', '石板物流园', '无事故,可分期'],
+ ],
+ // 本地服务
+ 5 => [
+ ['王师傅', '专业空调清洗加氟上门', '10 年老师傅,清洗 60 元一台,加氟 80 元起,不满意不收费。', 60, '元/台', '南明区', '花果园', '上门,不满意不收费'],
+ ['陈师傅', '开锁换锁 24 小时上门', '防盗门、汽车锁都能开,换锁芯明码标价,半夜也能来。', 80, '元起', '云岩区', '喷水池', '24小时,明码标价'],
+ ['小杨', '电脑维修 重装系统', '蓝屏、开不了机、清灰换硅脂都能修,先检测后报价。', 50, '元起', '南明区', '花果园', '先检测,上门'],
+ ['陈师傅', '管道疏通 高压清洗', '厨房下水、马桶、地漏疏通,不通不收费,节假日不休。', 100, '元起', '云岩区', '黔灵山公园', '不通不收费,节假日不休'],
+ ['王师傅', '搬家拉货 小面包车', '市区内 120 元起,可帮忙搬下楼,费用提前说清楚。', 120, '元起', '观山湖区', '世纪城', '搬家,价格透明'],
+ ['陈师傅', '防水补漏 免费上门检测', '屋顶、卫生间、外墙漏水都能做,先看现场再报价,质保两年。', 0, '面议', '花溪区', '花溪公园', '免费检测,质保两年'],
+ ['小林', '摄影跟拍 婚礼活动', '婚礼、生日、毕业照都能拍,当天出精修图,可约周末。', 800, '元/场', '观山湖区', '观山湖公园', '跟拍,当天出图'],
+ ['小杨', '酒后代驾 市区内', '晚上 8 点到凌晨 2 点接单,市区 39 元起,安全送到家。', 39, '元起', '南明区', '甲秀楼', '夜间,安全送达'],
+ ],
+ // 教育培训
+ 6 => [
+ ['李老师', '少儿美术体验课 9.9 元', '4 到 12 岁都能来,画材全包,先体验再决定,周末可约。', 9.9, '元', '观山湖区', '世纪城', '体验课,画材全包'],
+ ['张老师', '初三数学一对一辅导', '在职老师,带过三届毕业班,先测水平再定计划,可上门。', 150, '元/课时', '南明区', '花果园', '一对一,可上门'],
+ ['招生办', '成人高考报名咨询', '大专本科都能报,流程有人指导,考前有资料,不收中介费。', 0, '面议', '云岩区', '喷水池', '学历提升,免费咨询'],
+ ['阿杰老师', '吉他尤克里里 零基础班', '小班教学,一人一把琴,学会三首歌再续课,晚上和周末上课。', 80, '元/课时', '观山湖区', '奥体中心', '零基础,小班'],
+ ['李老师', '少儿编程体验课', '图形化编程入门,边玩边学,适合 7 到 12 岁,电脑教室上课。', 19.9, '元', '花溪区', '花溪公园', '体验课,边玩边学'],
+ ['招生办', '普通话考级培训', '针对二乙二甲考试,小班纠音,考前模拟测试,不过可重听。', 680, '元', '云岩区', '黔灵山公园', '考级,可重听'],
+ ['王教练', '驾校 C1 报名', '本地练车,随到随学,科目一到科目四全程带,费用一次交清。', 2680, '元', '南明区', '龙洞堡', '一费制,随到随学'],
+ ['张老师', '英语四六级冲刺班', '每周三次晚课,讲真题和作文模板,适合基础一般的大学生。', 499, '元', '花溪区', '贵州大学', '冲刺班,真题讲解'],
+ ],
+ // 家政保洁
+ 7 => [
+ ['陈姐', '日常保洁 3 小时', '扫地拖地、擦灰、厨房卫生间清洁,自带工具,满意再付款。', 120, '元/次', '南明区', '花果园', '自带工具,满意付款'],
+ ['王师傅', '新房开荒保洁', '装修后除尘除胶,玻璃和柜子里外都擦,按面积算,量大优惠。', 5, '元/㎡', '观山湖区', '世纪城', '开荒,按面积'],
+ ['陈姐', '月嫂育儿嫂 可预约', '带过 30 多个宝宝,会做月子餐,有健康证,可先面试。', 8000, '元/月', '云岩区', '喷水池', '有健康证,可面试'],
+ ['王师傅', '油烟机洗衣机深度清洗', '拆洗不伤机器,清洗完当场试机,两台一起更划算。', 199, '元', '南明区', '花果园', '拆洗,当场试机'],
+ ['陈姐', '钟点工 做饭打扫', '每天两小时,做饭加简单收拾,口味可以按家里习惯来。', 35, '元/小时', '观山湖区', '金融城', '做饭,按小时'],
+ ['王师傅', '高层玻璃清洗', '内外都能擦,带安全绳,雨天不接单,提前一天预约。', 150, '元起', '观山湖区', '奥体中心', '高层,带安全绳'],
+ ['陈姐', '老人陪护 住家', '照顾能自理的老人,做饭陪着说话,有护理经验,可长期。', 4500, '元/月', '花溪区', '花溪公园', '住家,有护理经验'],
+ ['王师傅', '地毯清洗 上门', '公司办公室和家庭地毯都能洗,洗完当天能干,按平方计价。', 8, '元/㎡', '云岩区', '贵阳北站', '上门,当天干'],
+ ],
+ // 同城交友
+ 8 => [
+ ['小林', '周末爬山搭子 黔灵山', '周六早上 8 点黔灵山门口集合,走慢线,新手也能来,自带水。', 0, '面议', '云岩区', '黔灵山公园', '爬山,新手友好'],
+ ['小周', '羽毛球约球 每周三晚', '观山湖体育馆,水平一般,主要活动身体,场地费 AA。', 0, '面议', '观山湖区', '奥体中心', '羽毛球,AA'],
+ ['小杨', '贵阳本地读书会 招募', '每月一本书,线下聊两小时,不限年龄职业,安静参与就好。', 0, '面议', '南明区', '甲秀楼', '读书会,每月一次'],
+ ['小林', '90 后女生 找运动搭子', '平时跑步、骑车都可以,周末有空,希望住得近一点,互相监督。', 0, '面议', '观山湖区', '观山湖公园', '运动,住得近'],
+ ['小周', '摄影爱好者 周末扫街', '喜欢拍老街和人文,器材不限,手机也能一起玩,拍完一起吃饭。', 0, '面议', '云岩区', '喷水池', '摄影,手机也行'],
+ ['小杨', '骑行群招募 周末环湖', '休闲骑,速度不快,观山湖集合,骑完喝茶,安全第一。', 0, '面议', '观山湖区', '观山湖公园', '骑行,休闲'],
+ ['小林', '桌游局 缺 2 人', '周六下午玩狼人杀和剧本,新手有人带,场地在花果园,费用 AA。', 0, '面议', '南明区', '花果园', '桌游,新手可来'],
+ ['小周', '单身青年联谊活动 报名', '25 到 35 岁,工作稳定,活动有游戏和自由聊天,提前报名。', 0, '面议', '南明区', '花果园', '联谊,提前报名'],
+ ],
+ ];
+ }
+
+ /** 评价:post_id => [用户ID, 评分, 内容, 商家回复] */
+ protected function seedComments($now)
+ {
+ $comments = [
+ 101 => [[1002, 5, '联系很及时,当天就上岗了。', ''], [2003, 5, '工资日结是真的,站长人也不错。', '']],
+ 102 => [[1003, 5, '店里氛围挺好,排班也合理。', '']],
+ 109 => [[1003, 4, '房子不错,房东好说话。', '感谢支持,欢迎续租~']],
+ 110 => [[1002, 5, '采光确实好,看房很方便。', '']],
+ 117 => [[2006, 5, '车况和描述一致,交易很顺利。', ''], [1001, 4, '卖家很爽快,还送了车锁。', '']],
+ 118 => [[2003, 5, '当面验过机,没有问题。', '']],
+ 125 => [[2003, 5, '检测报告齐全,买得放心。', '']],
+ 126 => [[1002, 4, '代步够用,充电也方便。', '']],
+ 133 => [[1001, 5, '师傅上门很准时,清洗得很干净。', '感谢认可,有需要随时联系。']],
+ 134 => [[2006, 5, '半夜叫的开锁,十分钟就到了。', '']],
+ 141 => [[1003, 5, '孩子很喜欢,老师也有耐心。', '']],
+ 142 => [[2003, 4, '讲得清楚,孩子愿意学。', '']],
+ 149 => [[1001, 5, '打扫得仔细,下次还约。', '']],
+ 150 => [[1003, 5, '开荒做得很干净,柜子里面都擦了。', '']],
+ 157 => [[1003, 5, '上周去过,路线轻松,人也很好。', '']],
+ 158 => [[1001, 5, '水平差不多,打得很尽兴。', '']],
+ ];
+
+ $id = 5001;
+ $i = 0;
+ foreach ($comments as $postId => $rows) {
+ foreach ($rows as $row) {
+ $t = $now - (int)((0.4 + $i * 0.23) * 86400);
+ Db::name('fc_comment')->insert([
+ 'id' => $id,
+ 'post_id' => $postId,
+ 'user_id' => $row[0],
+ 'score' => $row[1],
+ 'content' => $row[2],
+ 'images' => json_encode([], JSON_UNESCAPED_UNICODE),
+ 'reply' => $row[3],
+ 'replytime' => $row[3] !== '' ? $t + 3600 : null,
+ 'top' => 0,
+ 'status' => 1,
+ 'createtime' => $t,
+ 'updatetime' => $t,
+ ]);
+ $id++;
+ $i++;
+ }
+ }
+ }
+
+ /** 草稿:保留两条未完成信息,用于验证草稿箱 */
+ protected function seedDrafts($now)
+ {
+ $rows = [
+ [1001, 2, '转租单间(草稿)', '房子还在收拾,晚点补照片。'],
+ [1003, 7, '保洁套餐(草稿)', '想做一个三次卡的套餐,价格还没定。'],
+ ];
+ $t = $now - 7200;
+ foreach ($rows as $row) {
+ Db::name('fc_draft')->insert([
+ 'user_id' => $row[0],
+ 'category_id' => $row[1],
+ 'title' => $row[2],
+ 'content' => $row[3],
+ 'media' => json_encode([], JSON_UNESCAPED_UNICODE),
+ 'contact' => '',
+ 'phone' => '',
+ 'address' => '',
+ 'savedat' => $t,
+ 'createtime' => $t,
+ 'updatetime' => $t,
+ ]);
+ $t -= 1800;
+ }
+ }
+
+ /** 举报:两条不同状态,便于后台处理演示 */
+ protected function seedReports($now)
+ {
+ $rows = [
+ ['post', 117, '价格与描述不一致', 1002, 'pending', '', 0.6],
+ ['comment', 5002, '评论里留了外部联系方式', 2006, 'handled', '已删除违规评论', 3],
+ ];
+ foreach ($rows as $row) {
+ $t = $now - (int)($row[6] * 86400);
+ Db::name('fc_report')->insert([
+ 'target_type' => $row[0],
+ 'target_id' => $row[1],
+ 'reason' => $row[2],
+ 'reporter_id' => $row[3],
+ 'status' => $row[4],
+ 'remark' => $row[5],
+ 'createtime' => $t,
+ 'updatetime' => $t,
+ ]);
+ }
+ }
+
+ /** 回写分类信息量 / 评价数,保证列表计数与真实数据一致 */
+ protected function syncCounts()
+ {
+ Db::execute('UPDATE `' . \think\Config::get('database.prefix') . 'fc_post` p SET p.`comment_count` = (SELECT COUNT(*) FROM `' . \think\Config::get('database.prefix') . 'fc_comment` c WHERE c.`post_id` = p.`id` AND c.`status` = 1)');
+
+ foreach ([1, 2, 3, 4, 5, 6, 7, 8] as $categoryId) {
+ $count = Db::name('fc_post')
+ ->where('category_id', $categoryId)
+ ->where('audit_status', 1)
+ ->where('status', 1)
+ ->whereNull('deletetime')
+ ->count();
+ Db::name('fc_category')->where('id', $categoryId)->update(['post_count' => $count]);
+ }
+ }
+}
diff --git a/application/admin/command/FcSettle.php b/application/admin/command/FcSettle.php
new file mode 100644
index 0000000..21ce7fb
--- /dev/null
+++ b/application/admin/command/FcSettle.php
@@ -0,0 +1,82 @@
+setName('fc:settle')
+ ->setDescription('分销佣金到期结算 / 关闭超时订单 / 同步转账状态');
+ }
+
+ protected function execute(Input $input, Output $output)
+ {
+ $now = time();
+
+ /* 1. 佣金到期结算 */
+ $settled = Db::name('fc_commission')
+ ->where('status', 0)
+ ->where('settle_due', '<=', $now)
+ ->update([
+ 'status' => 1,
+ 'settletime' => $now,
+ ]);
+ $output->writeln('==> 佣金结算:' . $settled . ' 笔');
+
+ /* 2. 关闭超时未支付订单(24 小时) */
+ $expired = Db::name('fc_order')
+ ->where('status', 'pending')
+ ->where('createtime', '<', $now - 86400)
+ ->select();
+
+ $closed = 0;
+ foreach ($expired as $order) {
+ $affected = Db::name('fc_order')
+ ->where('id', (int)$order['id'])
+ ->where('status', 'pending')
+ ->update([
+ 'status' => 'closed',
+ 'updatetime' => $now,
+ ]);
+ if ($affected) {
+ CouponService::release((string)$order['order_no']);
+ $closed++;
+ }
+ }
+ $output->writeln('==> 关闭超时订单:' . $closed . ' 笔');
+
+ /* 3. 同步转账中的提现单(最多 20 笔) */
+ $synced = 0;
+ if (class_exists('\app\common\library\WxTransfer') && \app\common\library\WxTransfer::isReady()) {
+ $rows = Db::name('fc_withdraw')->where('status', 'paying')->limit(20)->select();
+ foreach ($rows as $row) {
+ try {
+ \app\common\library\WxTransfer::sync((int)$row['id']);
+ $synced++;
+ } catch (\Throwable $e) {
+ // 单笔失败不影响其它
+ }
+ }
+ }
+ $output->writeln('==> 同步转账状态:' . $synced . ' 笔');
+
+ $output->writeln('==> 完成');
+ }
+}
diff --git a/application/admin/controller/Index.php b/application/admin/controller/Index.php
index 8c55447..511b2e5 100755
--- a/application/admin/controller/Index.php
+++ b/application/admin/controller/Index.php
@@ -5,6 +5,7 @@ namespace app\admin\controller;
use app\admin\model\AdminLog;
use app\common\controller\Backend;
use think\Config;
+use think\Db;
use think\Hook;
use think\Session;
use think\Validate;
@@ -40,11 +41,29 @@ class Index extends Backend
}
}
//左侧菜单
- list($menulist, $navlist, $fixedmenu, $referermenu) = $this->auth->getSidebar([
- 'dashboard' => 'hot',
- 'addon' => ['new', 'red', 'badge'],
- 'auth/rule' => __('Menu'),
- ], $this->view->site['fixedpage']);
+ // 角标只展示需要处理的业务,不再使用 hot/new 这类装饰性角标
+ $badges = [];
+ try {
+ $pendingReport = (int)Db::name('fc_report')->where('status', 'pending')->count();
+ $pendingWithdraw = (int)Db::name('fc_withdraw')->where('status', 'auditing')->count();
+ $pendingMerchant = (int)Db::name('fc_merchant')->where('audit_status', 0)->count();
+ if ($pendingReport > 0) {
+ $badges['fc/report'] = [$pendingReport, 'orange'];
+ $badges['fc/content'] = [$pendingReport, 'orange'];
+ }
+ if ($pendingWithdraw > 0) {
+ $badges['fc/withdraw'] = [$pendingWithdraw, 'red'];
+ $badges['fc/trade'] = [$pendingWithdraw, 'red'];
+ }
+ if ($pendingMerchant > 0) {
+ $badges['fc/merchant'] = [$pendingMerchant, 'yellow'];
+ $badges['fc/user'] = [$pendingMerchant, 'yellow'];
+ }
+ } catch (\Exception $e) {
+ // 业务表缺失时不影响后台首页
+ }
+
+ list($menulist, $navlist, $fixedmenu, $referermenu) = $this->auth->getSidebar($badges, $this->view->site['fixedpage']);
$action = $this->request->request('action');
if ($this->request->isPost()) {
if ($action == 'refreshmenu') {
diff --git a/application/admin/controller/auth/Rule.php b/application/admin/controller/auth/Rule.php
index 4e30d3c..1694473 100755
--- a/application/admin/controller/auth/Rule.php
+++ b/application/admin/controller/auth/Rule.php
@@ -11,7 +11,7 @@ use think\Cache;
* 规则管理
*
* @icon fa fa-list
- * @remark 规则通常对应一个控制器的方法,同时左侧的菜单栏数据也从规则中体现,通常建议通过控制台进行生成规则节点
+ * @remark 左侧菜单与权限节点都在这里维护;修改后左侧导航自动刷新,删除菜单会连同其子级一起删除
*/
class Rule extends Backend
{
@@ -57,8 +57,8 @@ class Rule extends Backend
public function index()
{
if ($this->request->isAjax()) {
- $list = $this->rulelist;
- $total = count($this->rulelist);
+ $list = $this->assignSeq($this->filterRuleList($this->rulelist));
+ $total = count($list);
$result = array("total" => $total, "rows" => $list);
return json($result);
@@ -66,6 +66,116 @@ class Rule extends Backend
return $this->view->fetch();
}
+ /**
+ * 按筛选条件过滤规则树
+ *
+ * 支持:仅菜单 / 仅权限节点 / 全部、状态、关键词(标题或规则名)。
+ * 命中节点的父级会一并保留,避免树形层级断裂。
+ *
+ * @param array $list
+ * @return array
+ */
+ protected function filterRuleList(array $list)
+ {
+ $type = (string)$this->request->request('rule_filter', 'menu');
+ $status = (string)$this->request->request('rule_status', '');
+ $keyword = trim((string)$this->request->request('rule_keyword', ''));
+
+ $type = in_array($type, ['menu', 'node', 'all'], true) ? $type : 'all';
+ $status = in_array($status, ['normal', 'hidden'], true) ? $status : '';
+ if ($type === 'all' && $status === '' && $keyword === '') {
+ return $list;
+ }
+
+ $byId = [];
+ foreach ($list as $row) {
+ $byId[(int)$row['id']] = $row;
+ }
+
+ $keep = [];
+ foreach ($list as $row) {
+ $match = true;
+ if ($type === 'menu' && !$row['ismenu']) {
+ $match = false;
+ }
+ if ($type === 'node' && $row['ismenu']) {
+ $match = false;
+ }
+ if ($status !== '' && $row['status'] !== $status) {
+ $match = false;
+ }
+ if ($keyword !== '' && stripos($row['title'], $keyword) === false && stripos($row['name'], $keyword) === false) {
+ $match = false;
+ }
+ if (!$match) {
+ continue;
+ }
+ // 保留命中节点的所有父级
+ $id = (int)$row['id'];
+ $guard = 0;
+ while ($id && isset($byId[$id]) && $guard++ < 20) {
+ $keep[$id] = true;
+ $id = (int)$byId[$id]['pid'];
+ }
+ }
+
+ $result = [];
+ foreach ($list as $row) {
+ if (isset($keep[(int)$row['id']])) {
+ $result[] = $row;
+ }
+ }
+ return $result;
+ }
+
+ /**
+ * 生成层级序号:1、1.1、1.2、2、2.1……
+ *
+ * 树形列表默认会隐藏子行,若按行号显示序号会出现 1/7/28 这类跳号;
+ * 这里按父子层级编号,展开/收起都稳定可读。
+ *
+ * @param array $list
+ * @return array
+ */
+ protected function assignSeq(array $list)
+ {
+ $byId = [];
+ foreach ($list as $row) {
+ $byId[(int)$row['id']] = $row;
+ }
+
+ $seqById = [];
+ $counters = [];
+ foreach ($list as &$row) {
+ $depth = 0;
+ $pid = (int)$row['pid'];
+ $guard = 0;
+ while ($pid && isset($byId[$pid]) && $guard++ < 20) {
+ $depth++;
+ $pid = (int)$byId[$pid]['pid'];
+ }
+
+ $counters[$depth] = isset($counters[$depth]) ? $counters[$depth] + 1 : 1;
+ if ($depth === 0) {
+ $seq = (string)$counters[$depth];
+ } else {
+ $parentSeq = isset($seqById[(int)$row['pid']]) ? $seqById[(int)$row['pid']] : '';
+ $seq = ($parentSeq !== '' ? $parentSeq . '.' : '') . $counters[$depth];
+ }
+
+ // 进入新分支后,重置更深的层级计数
+ for ($i = $depth + 1; $i <= 10; $i++) {
+ unset($counters[$i]);
+ }
+
+ $row['seq'] = $seq;
+ $seqById[(int)$row['id']] = $seq;
+ }
+ unset($row);
+
+ return $list;
+ }
+
/**
* 添加
*/
diff --git a/application/admin/controller/fc/Area.php b/application/admin/controller/fc/Area.php
index f54c289..30fdad1 100644
--- a/application/admin/controller/fc/Area.php
+++ b/application/admin/controller/fc/Area.php
@@ -3,11 +3,12 @@
namespace app\admin\controller\fc;
use app\common\controller\Backend;
+use think\Db;
/**
- * 同城-服务区域
+ * 同城-服务区域(省 / 市 / 区县平铺,区县为最终服务区域)
*
- * @icon fa fa-circle-o
+ * @icon fa fa-map-o
*/
class Area extends Backend
{
@@ -22,16 +23,81 @@ class Area extends Backend
{
parent::_initialize();
$this->model = new \app\admin\model\FcArea;
-
}
-
+ /**
+ * 新增:保存前校验
+ */
+ public function add()
+ {
+ $this->assignAreaOptions();
+ if ($this->request->isPost()) {
+ $this->validateArea($this->request->post('row/a'), 0);
+ }
+ return parent::add();
+ }
/**
- * 默认生成的控制器所继承的父类中有index/add/edit/del/multi五个基础方法、destroy/restore/recyclebin三个回收站方法
- * 因此在当前控制器中可不用编写增删改查的代码,除非需要自己控制这部分逻辑
- * 需要将application/admin/library/traits/Backend.php中对应的方法复制到当前控制器,然后进行修改
+ * 编辑:保存前校验
+ * @param string $ids
*/
+ public function edit($ids = null)
+ {
+ $this->assignAreaOptions();
+ if ($this->request->isPost()) {
+ $this->validateArea($this->request->post('row/a'), (int)$this->request->param('ids'));
+ }
+ return parent::edit($ids);
+ }
+ /** 省 / 市 / 区县候选值,供表单级联下拉使用 */
+ protected function assignAreaOptions()
+ {
+ $rows = Db::name('fc_area')
+ ->field('province,city,district')
+ ->order('province asc, city asc, district asc')
+ ->select();
+ $this->view->assign('areaOptionsJson', json_encode($rows, JSON_UNESCAPED_UNICODE));
+ }
+ /**
+ * 保存前校验:省 / 市 / 区县均必填,同一组合不可重复
+ * @param array $params 表单数据
+ * @param int $id 编辑时的区域ID
+ */
+ protected function validateArea($params, $id = 0)
+ {
+ $province = isset($params['province']) ? trim((string)$params['province']) : '';
+ $city = isset($params['city']) ? trim((string)$params['city']) : '';
+ $district = isset($params['district']) ? trim((string)$params['district']) : '';
+
+ $values = ['省份' => $province, '城市' => $city, '区县' => $district];
+ foreach ($values as $label => $value) {
+ if ($value === '') {
+ $this->error($label . '不能为空');
+ }
+ if (mb_strlen($value) > 32) {
+ $this->error($label . '不能超过 32 个字符');
+ }
+ }
+
+ $duplicate = Db::name('fc_area')
+ ->where('province', $province)
+ ->where('city', $city)
+ ->where('district', $district)
+ ->where('id', '<>', $id)
+ ->find();
+ if ($duplicate) {
+ $this->error('该省市区组合已存在');
+ }
+
+ foreach (['lat' => 90, 'lng' => 180] as $field => $limit) {
+ if (!isset($params[$field]) || $params[$field] === '' || $params[$field] === null) {
+ continue;
+ }
+ if (!is_numeric($params[$field]) || (float)$params[$field] < -$limit || (float)$params[$field] > $limit) {
+ $this->error($field === 'lat' ? '纬度格式不正确' : '经度格式不正确');
+ }
+ }
+ }
}
diff --git a/application/admin/controller/fc/Dashboard.php b/application/admin/controller/fc/Dashboard.php
index e27fb5a..2acfeba 100644
--- a/application/admin/controller/fc/Dashboard.php
+++ b/application/admin/controller/fc/Dashboard.php
@@ -89,6 +89,13 @@ class Dashboard extends Backend
$membersMap[$row['day']] = (int)$row['num'];
}
+ /* ---------- 今日较昨日的变化(用于首页涨跌标识) ---------- */
+ $yesterday = date('Y-m-d', $todayStart - 86400);
+ $metrics['yesterdayPost'] = isset($postsMap[$yesterday]) ? $postsMap[$yesterday] : 0;
+ $metrics['yesterdayOrder'] = isset($ordersMap[$yesterday]) ? $ordersMap[$yesterday] : 0;
+ $metrics['postDelta'] = $metrics['todayPost'] - $metrics['yesterdayPost'];
+ $metrics['orderDelta'] = round($metrics['todayOrderAmount'] - $metrics['yesterdayOrder'], 2);
+
$trend = [
'dates' => $days,
'posts' => [],
@@ -115,6 +122,7 @@ class Dashboard extends Backend
$this->view->assign('metrics', $metrics);
$this->view->assign('trendRows', $trendRows);
+ $this->view->assign('updatedAt', date('Y-m-d H:i'));
$this->assignconfig('trend', $trend);
return $this->view->fetch();
diff --git a/application/admin/controller/fc/Financeflow.php b/application/admin/controller/fc/Financeflow.php
index e49cc3e..061f925 100644
--- a/application/admin/controller/fc/Financeflow.php
+++ b/application/admin/controller/fc/Financeflow.php
@@ -3,6 +3,7 @@
namespace app\admin\controller\fc;
use app\common\controller\Backend;
+use think\Db;
/**
* 同城-资金流水
@@ -34,4 +35,132 @@ class Financeflow extends Backend
*/
+ /**
+ * 导出资金流水(Excel)
+ * GET fc/financeflow/export?type=income|expense
+ */
+ public function export()
+ {
+ $type = trim((string)$this->request->get('type', ''));
+
+ $q = Db::name('fc_finance_flow');
+ if ($type !== '') {
+ $q->where('type', $type);
+ }
+ $rows = $q->order('createtime desc')->limit(50000)->select();
+
+ $spreadsheet = new \PhpOffice\PhpSpreadsheet\Spreadsheet();
+ $sheet = $spreadsheet->getActiveSheet();
+ $sheet->setTitle('资金流水');
+ $sheet->fromArray(['流水号', '类型', '场景', '金额(元)', '账户余额(元)', '关联ID', '备注', '时间'], null, 'A1');
+
+ $r = 2;
+ foreach ($rows as $row) {
+ $sheet->fromArray([
+ (string)$row['flow_no'],
+ $row['type'] === 'income' ? '收入' : '支出',
+ (string)$row['scene'],
+ (float)$row['amount'],
+ (float)$row['balance'],
+ (int)$row['related_id'],
+ (string)$row['remark'],
+ date('Y-m-d H:i:s', (int)$row['createtime']),
+ ], null, 'A' . $r);
+ $r++;
+ }
+
+ $this->downloadXlsx($spreadsheet, '资金流水_' . date('YmdHis'));
+ }
+
+ /**
+ * 导出营收报表(Excel)
+ * GET fc/financeflow/exportReport?period=day|month|year
+ */
+ public function exportReport()
+ {
+ $period = trim((string)$this->request->get('period', 'month'));
+ if (!in_array($period, ['day', 'month', 'year'], true)) {
+ $period = 'month';
+ }
+ $fmtMap = ['day' => '%Y-%m-%d', 'month' => '%Y-%m', 'year' => '%Y'];
+ $fmt = $fmtMap[$period];
+
+ $post = $this->periodSum('post', $fmt);
+ $member = $this->periodSum('member', $fmt);
+ $recharge = $this->periodSum('recharge', $fmt);
+ $withdraw = $this->periodWithdraw($fmt);
+
+ $keys = array_unique(array_merge(array_keys($post), array_keys($member), array_keys($recharge), array_keys($withdraw)));
+ sort($keys);
+
+ $spreadsheet = new \PhpOffice\PhpSpreadsheet\Spreadsheet();
+ $sheet = $spreadsheet->getActiveSheet();
+ $sheet->setTitle('营收报表');
+ $sheet->fromArray(['周期', '发布收入', '会员收入', '充值收入', '提现支出', '净收入'], null, 'A1');
+
+ $r = 2;
+ foreach ($keys as $k) {
+ $p = isset($post[$k]) ? (float)$post[$k] : 0;
+ $m = isset($member[$k]) ? (float)$member[$k] : 0;
+ $rc = isset($recharge[$k]) ? (float)$recharge[$k] : 0;
+ $w = isset($withdraw[$k]) ? (float)$withdraw[$k] : 0;
+ $sheet->fromArray([
+ (string)$k,
+ round($p, 2),
+ round($m, 2),
+ round($rc, 2),
+ round($w, 2),
+ round($p + $m + $rc - $w, 2),
+ ], null, 'A' . $r);
+ $r++;
+ }
+
+ $label = ['day' => '日', 'month' => '月', 'year' => '年'][$period];
+ $this->downloadXlsx($spreadsheet, '营收报表_按' . $label . '_' . date('YmdHis'));
+ }
+
+ /** 按周期聚合某类已支付订单 */
+ protected function periodSum($type, $fmt)
+ {
+ $rows = Db::name('fc_order')
+ ->where('status', 'paid')
+ ->where('type', $type)
+ ->field("FROM_UNIXTIME(createtime, '{$fmt}') AS period, SUM(amount) AS total")
+ ->group('period')
+ ->select();
+ $map = [];
+ foreach ($rows as $row) {
+ $map[$row['period']] = (float)$row['total'];
+ }
+ return $map;
+ }
+
+ /** 按周期聚合已到账提现 */
+ protected function periodWithdraw($fmt)
+ {
+ $rows = Db::name('fc_withdraw')
+ ->where('status', 'paid')
+ ->whereNotNull('paytime')
+ ->field("FROM_UNIXTIME(paytime, '{$fmt}') AS period, SUM(amount) AS total")
+ ->group('period')
+ ->select();
+ $map = [];
+ foreach ($rows as $row) {
+ $map[$row['period']] = (float)$row['total'];
+ }
+ return $map;
+ }
+
+ /** 输出 xlsx 下载 */
+ protected function downloadXlsx($spreadsheet, $filename)
+ {
+ $filename = preg_replace('/[^\w\x{4e00}-\x{9fa5}\-]/u', '', $filename) . '.xlsx';
+ header('Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
+ header('Content-Disposition: attachment;filename="' . $filename . '"');
+ header('Cache-Control: max-age=0');
+
+ $writer = new \PhpOffice\PhpSpreadsheet\Writer\Xlsx($spreadsheet);
+ $writer->save('php://output');
+ exit;
+ }
}
diff --git a/application/admin/controller/fc/Order.php b/application/admin/controller/fc/Order.php
index 3086b35..38a5848 100644
--- a/application/admin/controller/fc/Order.php
+++ b/application/admin/controller/fc/Order.php
@@ -40,37 +40,43 @@ class Order extends Backend
public function refund()
{
$ids = $this->request->post('ids');
- $amount = (float)$this->request->post('amount');
+ $amount = $this->request->post('amount');
$reason = $this->request->post('reason', '');
- if ($amount <= 0) {
- $this->error('退款金额需大于 0');
- }
+
$ids = is_array($ids) ? $ids : explode(',', (string)$ids);
$ids = array_filter($ids);
if (!$ids) {
$this->error('请选择要退款的订单');
}
+
$orders = Db::name('fc_order')->where('id', 'in', $ids)->select();
if (!$orders) {
$this->error('订单不存在');
}
+
+ // 金额留空 = 按每笔订单实付金额全额退款
+ $amount = ($amount === '' || $amount === null || (float)$amount <= 0) ? null : (float)$amount;
+
+ $ok = 0;
+ $errors = [];
foreach ($orders as $order) {
- Db::name('fc_order')->where('id', $order['id'])->update([
- 'status' => 'refunded',
- 'refund_amount' => $amount,
- 'refund_reason' => $reason,
- 'updatetime' => time(),
- ]);
- Db::name('fc_message')->insert([
- 'user_id' => $order['user_id'],
- 'type' => 'system',
- 'title' => '订单退款',
- 'content' => '您的订单「' . $order['order_no'] . '」已退款' . $amount . '元' . ($reason ? ',原因:' . $reason : ''),
- 'isread' => 0,
- 'createtime' => time(),
- ]);
+ try {
+ \app\common\library\OrderRefund::refund((int)$order['id'], $amount, (string)$reason);
+ $ok++;
+ } catch (\Throwable $e) {
+ $errors[] = $order['order_no'] . ':' . $e->getMessage();
+ }
}
- $this->success('退款成功');
+
+ if ($ok === 0) {
+ $this->error('退款失败:' . implode(';', $errors));
+ }
+
+ $msg = '退款成功 ' . $ok . ' 笔';
+ if ($errors) {
+ $msg .= ',失败 ' . count($errors) . ' 笔:' . implode(';', $errors);
+ }
+ $this->success($msg);
}
/**
diff --git a/application/admin/controller/fc/Setting.php b/application/admin/controller/fc/Setting.php
index 31edb39..fdd8242 100644
--- a/application/admin/controller/fc/Setting.php
+++ b/application/admin/controller/fc/Setting.php
@@ -47,14 +47,20 @@ class Setting extends Backend
if ($appName === '') {
$this->error('小程序名称不能为空');
}
+ $logo = isset($params['logo']) ? trim((string)$params['logo']) : '';
- $this->setConfig([
+ $values = [
'base.appName' => ['value' => $appName, 'group_key' => 'base', 'remark' => '小程序名称'],
- 'base.logo' => ['value' => isset($params['logo']) ? trim((string)$params['logo']) : '', 'group_key' => 'base', 'remark' => 'LOGO地址'],
+ 'base.logo' => ['value' => $logo, 'group_key' => 'base', 'remark' => 'LOGO地址'],
'base.servicePhone' => ['value' => isset($params['servicePhone']) ? trim((string)$params['servicePhone']) : '', 'group_key' => 'base', 'remark' => '客服电话'],
'base.aboutUs' => ['value' => isset($params['aboutUs']) ? trim((string)$params['aboutUs']) : '', 'group_key' => 'base', 'remark' => '关于我们'],
'base.shareTitle' => ['value' => isset($params['shareTitle']) ? trim((string)$params['shareTitle']) : '', 'group_key' => 'base', 'remark' => '分享标题'],
- ]);
+ // 兼容旧 ui.* 键(用户端已优先读 base.*)
+ 'ui.siteName' => ['value' => $appName, 'group_key' => 'ui', 'remark' => '站点名称'],
+ 'ui.logo' => ['value' => $logo, 'group_key' => 'ui', 'remark' => '自定义LOGO'],
+ ];
+
+ $this->setConfig($values);
$this->success('小程序设置已保存');
}
diff --git a/application/admin/controller/fc/Userext.php b/application/admin/controller/fc/Userext.php
index b802321..af277b0 100644
--- a/application/admin/controller/fc/Userext.php
+++ b/application/admin/controller/fc/Userext.php
@@ -67,6 +67,8 @@ class Userext extends Backend
* 说明:
* 余额存放在核心表 fa_user.money,本表(fc_user_ext)仅作业务扩展,
* 因此这里按 user_id 关联 fa_user 进行加减。
+ * 【注意】ids 必须是 fa_user.id(即本表的 user_id 字段),
+ * 不能传 fc_user_ext.id,否则会充错人(或报「未找到对应用户」)。
* 为可追溯,每次充值同时写入 fa_fc_finance_flow(资金流水)与
* fa_fc_message(站内通知),后台操作留痕由 Backend 自动记录。
*/
@@ -89,52 +91,66 @@ class Userext extends Backend
$remark = trim((string)$this->request->post('remark', ''));
$now = time();
$done = 0;
+ $missing = [];
- foreach ($userIds as $uid) {
- $user = Db::name('user')->where('id', $uid)->find();
- if (!$user) {
- continue;
+ Db::startTrans();
+ try {
+ foreach ($userIds as $uid) {
+ // 加行锁,避免并发充值互相覆盖余额
+ $user = Db::name('user')->where('id', $uid)->lock(true)->find();
+ if (!$user) {
+ $missing[] = $uid;
+ continue;
+ }
+
+ $balance = round((float)$user['money'] + $amount, 2);
+ Db::name('user')->where('id', $uid)->update([
+ 'money' => $balance,
+ 'updatetime' => $now,
+ ]);
+
+ // 资金流水(平台口径:余额充值属于收入)
+ Db::name('fc_finance_flow')->insert([
+ 'flow_no' => 'R' . date('YmdHis') . mt_rand(100, 999) . $uid,
+ 'type' => 'income',
+ 'scene' => '余额充值',
+ 'amount' => $amount,
+ 'balance' => $balance,
+ 'related_id' => $uid,
+ 'remark' => $remark !== '' ? $remark : ('后台充值给用户 #' . $uid),
+ 'createtime' => $now,
+ ]);
+
+ // 站内通知
+ Db::name('fc_message')->insert([
+ 'user_id' => $uid,
+ 'type' => 'system',
+ 'title' => '余额充值到账',
+ 'content' => '您的账户已充值 ¥' . number_format($amount, 2, '.', '')
+ . ',当前余额 ¥' . number_format($balance, 2, '.', '')
+ . '。' . ($remark !== '' ? '备注:' . $remark : ''),
+ 'isread' => 0,
+ 'readtime' => null,
+ 'createtime' => $now,
+ ]);
+
+ $done++;
}
-
- $balance = round((float)$user['money'] + $amount, 2);
- Db::name('user')->where('id', $uid)->update([
- 'money' => $balance,
- 'updatetime' => $now,
- ]);
-
- // 资金流水(平台口径:余额充值属于收入)
- Db::name('fc_finance_flow')->insert([
- 'flow_no' => 'R' . date('YmdHis') . mt_rand(100, 999) . $uid,
- 'type' => 'income',
- 'scene' => '余额充值',
- 'amount' => $amount,
- 'balance' => $balance,
- 'related_id' => $uid,
- 'remark' => $remark !== '' ? $remark : ('后台充值给用户 #' . $uid),
- 'createtime' => $now,
- ]);
-
- // 站内通知
- Db::name('fc_message')->insert([
- 'user_id' => $uid,
- 'type' => 'system',
- 'title' => '余额充值到账',
- 'content' => '您的账户已充值 ¥' . number_format($amount, 2, '.', '')
- . ',当前余额 ¥' . number_format($balance, 2, '.', '')
- . '。' . ($remark !== '' ? '备注:' . $remark : ''),
- 'isread' => 0,
- 'readtime' => null,
- 'createtime' => $now,
- ]);
-
- $done++;
+ Db::commit();
+ } catch (\Exception $e) {
+ Db::rollback();
+ $this->error('充值失败:' . $e->getMessage());
}
if (!$done) {
- $this->error('未找到对应用户');
+ $this->error('未找到对应用户:' . implode(',', $missing) . '(请确认传的是用户ID)');
}
- $this->success('充值成功:' . $done . ' 个用户,每人 ¥' . number_format($amount, 2, '.', ''));
+ $msg = '充值成功:' . $done . ' 个用户,每人 ¥' . number_format($amount, 2, '.', '');
+ if ($missing) {
+ $msg .= ';已跳过不存在的用户:' . implode(',', $missing);
+ }
+ $this->success($msg);
}
/**
diff --git a/application/admin/controller/fc/Withdraw.php b/application/admin/controller/fc/Withdraw.php
index 048752f..672111f 100644
--- a/application/admin/controller/fc/Withdraw.php
+++ b/application/admin/controller/fc/Withdraw.php
@@ -57,42 +57,81 @@ class Withdraw extends Backend
if (!$withdraws) {
$this->error('提现记录不存在');
}
- foreach ($withdraws as $withdraw) {
- if ($op === 'pay') {
- Db::name('fc_withdraw')->where('id', $withdraw['id'])->update([
- 'status' => 'paid',
- 'paytime' => time(),
- 'updatetime' => time(),
- ]);
- Db::name('fc_finance_flow')->insert([
- 'flow_no' => 'WD' . date('YmdHis') . $withdraw['id'],
- 'type' => 'expense',
- 'scene' => '佣金提现',
- 'amount' => $withdraw['amount'],
- 'balance' => 0,
- 'related_id' => $withdraw['id'],
- 'remark' => '提现打款',
- 'createtime' => time(),
- ]);
- } else {
- Db::name('fc_withdraw')->where('id', $withdraw['id'])->update([
- 'status' => 'rejected',
- 'reason' => $reason,
- 'updatetime' => time(),
- ]);
+
+ $done = 0;
+ $pending = 0;
+ $failed = 0;
+ $errors = [];
+
+ if ($op === 'reject') {
+ try {
+ Db::transaction(function () use ($withdraws, $reason, &$done) {
+ foreach ($withdraws as $withdraw) {
+ // 行锁 + 状态校验:已处理的记录不会被二次驳回
+ $locked = Db::name('fc_withdraw')->where('id', $withdraw['id'])->lock(true)->find();
+ if (!$locked || (string)$locked['status'] !== 'auditing') {
+ continue;
+ }
+ $done++;
+
+ Db::name('fc_withdraw')->where('id', $locked['id'])->update([
+ 'status' => 'rejected',
+ 'reason' => $reason,
+ 'updatetime' => time(),
+ ]);
+ Db::name('fc_message')->insert([
+ 'user_id' => $locked['user_id'],
+ 'type' => 'system',
+ 'title' => '提现驳回',
+ 'content' => '您的提现申请「' . $locked['withdraw_no'] . '」被驳回' . ($reason ? ',原因:' . $reason : ''),
+ 'isread' => 0,
+ 'createtime' => time(),
+ ]);
+ }
+ });
+ } catch (\Throwable $e) {
+ $this->error('操作失败:' . $e->getMessage());
}
- Db::name('fc_message')->insert([
- 'user_id' => $withdraw['user_id'],
- 'type' => 'system',
- 'title' => $op === 'pay' ? '提现成功' : '提现驳回',
- 'content' => $op === 'pay'
- ? '您的提现申请「' . $withdraw['withdraw_no'] . '」已打款,金额:' . $withdraw['amount'] . '元'
- : '您的提现申请「' . $withdraw['withdraw_no'] . '」被驳回' . ($reason ? ',原因:' . $reason : ''),
- 'isread' => 0,
- 'createtime' => time(),
- ]);
+
+ if ($done === 0) {
+ $this->error('选中的提现记录均已处理,请刷新后重试');
+ }
+ $this->success('驳回成功(' . $done . ' 条)');
}
- $this->success($op === 'pay' ? '打款成功' : '驳回成功');
+
+ // 通过 → 逐笔发起微信商家转账(外部调用不能放在事务里)
+ foreach ($withdraws as $withdraw) {
+ if ((string)$withdraw['status'] !== 'auditing') {
+ continue;
+ }
+ try {
+ $res = \app\common\library\WxTransfer::approve((int)$withdraw['id']);
+ $done++;
+ if ($res['withdrawState'] === 'paying') {
+ $pending++;
+ } elseif ($res['withdrawState'] === 'failed') {
+ $failed++;
+ }
+ } catch (\Throwable $e) {
+ $errors[] = $withdraw['withdraw_no'] . ':' . $e->getMessage();
+ }
+ }
+
+ if ($done === 0) {
+ $this->error('转账失败:' . implode(';', $errors));
+ }
+
+ $msg = '已发起转账 ' . $done . ' 笔';
+ if ($pending > 0) {
+ $msg .= '(其中 ' . $pending . ' 笔等待用户在微信确认收款)';
+ }
+ if ($failed > 0) {
+ $msg .= ',' . $failed . ' 笔转账失败';
+ }
+ if ($errors) {
+ $msg .= ';失败 ' . count($errors) . ' 笔:' . implode(';', $errors);
+ }
+ $this->success($msg);
}
diff --git a/application/admin/controller/general/Config.php b/application/admin/controller/general/Config.php
index 5dac56c..9fdf5ba 100755
--- a/application/admin/controller/general/Config.php
+++ b/application/admin/controller/general/Config.php
@@ -4,6 +4,7 @@ namespace app\admin\controller\general;
use app\common\controller\Backend;
use app\common\library\Email;
+use app\common\library\FcConfig;
use app\common\model\Config as ConfigModel;
use think\Cache;
use think\Db;
@@ -75,11 +76,30 @@ class Config extends Backend
}
$siteList[$v['group']]['list'][] = $value;
}
+ // 把小程序 / 发布 / 分销三组业务配置并入系统配置页(值真源仍是 fa_fc_config)
+ $fcGroups = FcConfig::groups();
+ $merged = [];
+ foreach ($siteList as $k => $v) {
+ $merged[$k] = $v;
+ if ($k === 'basic') {
+ foreach ($fcGroups as $gk => $gv) {
+ $merged[$gk] = $gv;
+ }
+ }
+ }
+ if (!isset($merged['fc_miniapp'])) {
+ foreach ($fcGroups as $gk => $gv) {
+ $merged[$gk] = $gv;
+ }
+ }
+ $siteList = $merged;
+
$index = 0;
foreach ($siteList as $k => &$v) {
$v['active'] = !$index ? true : false;
$index++;
}
+ unset($v);
$this->view->assign('siteList', $siteList);
$this->view->assign('typeList', ConfigModel::getTypeList());
$this->view->assign('ruleList', ConfigModel::getRegexList());
@@ -138,6 +158,13 @@ class Config extends Backend
$this->token();
$row = $this->request->post("row/a", [], 'trim');
if ($row) {
+ // 小程序 / 发布 / 分销:写入 fc_config(系统配置页的业务分组)
+ try {
+ FcConfig::save($row);
+ } catch (\Exception $e) {
+ $this->error($e->getMessage());
+ }
+
$configList = [];
foreach ($this->model->all() as $v) {
if (isset($row[$v['name']])) {
@@ -152,14 +179,18 @@ class Config extends Backend
}
}
try {
- $this->model->allowField(true)->saveAll($configList);
+ if ($configList) {
+ $this->model->allowField(true)->saveAll($configList);
+ }
} catch (Exception $e) {
$this->error($e->getMessage());
}
- try {
- ConfigModel::refreshFile();
- } catch (Exception $e) {
- $this->error($e->getMessage());
+ if ($configList) {
+ try {
+ ConfigModel::refreshFile();
+ } catch (Exception $e) {
+ $this->error($e->getMessage());
+ }
}
$this->success();
}
diff --git a/application/admin/lang/zh-cn/fc/area.php b/application/admin/lang/zh-cn/fc/area.php
index 63bde8a..6315444 100644
--- a/application/admin/lang/zh-cn/fc/area.php
+++ b/application/admin/lang/zh-cn/fc/area.php
@@ -2,7 +2,12 @@
return [
'Id' => 'ID',
- 'Name' => '区域名称',
+ 'Province' => '省份',
+ 'City' => '城市',
+ 'District' => '区县',
+ 'FullName' => '完整区域',
+ 'Lat' => '中心纬度',
+ 'Lng' => '中心经度',
'Sort' => '排序',
'Enabled' => '是否启用',
'Createtime' => '创建时间',
diff --git a/application/admin/lang/zh-cn/fc/dashboard.php b/application/admin/lang/zh-cn/fc/dashboard.php
index 37f2984..8e6a5a0 100644
--- a/application/admin/lang/zh-cn/fc/dashboard.php
+++ b/application/admin/lang/zh-cn/fc/dashboard.php
@@ -4,6 +4,18 @@ return [
'核心看板' => '核心看板',
'全部指标由后台直接统计;订单额仅统计「已支付」订单,佣金不含已驳回部分,提现为「待审核」笔数。'
=> '全部指标由后台直接统计;订单额仅统计「已支付」订单,佣金不含已驳回部分,提现为「待审核」笔数。',
+ '业务总览' => '业务总览',
+ '数据更新于' => '数据更新于',
+ '订单额仅统计已支付订单' => '订单额仅统计已支付订单',
+ '内容管理' => '内容管理',
+ '提现审核' => '提现审核',
+ '刷新数据' => '刷新数据',
+ '今日核心指标' => '今日核心指标',
+ '较昨日' => '较昨日',
+ '与昨日持平' => '与昨日持平',
+ '去审核' => '去审核',
+ '去处理' => '去处理',
+ '暂无待审核' => '暂无待审核',
'今日发布量' => '今日发布量',
'累计发布量' => '累计发布量',
'待审核信息数' => '待审核信息数',
@@ -12,15 +24,27 @@ return [
'会员数' => '会员数',
'商家数' => '商家数',
'注册用户数' => '注册用户数',
+ '平台累计' => '平台累计',
+ '分销佣金' => '分销佣金',
'待结算佣金' => '待结算佣金',
'已结算佣金' => '已结算佣金',
'待审核提现数' => '待审核提现数',
'近 14 天趋势' => '近 14 天趋势',
- '(发布量按发布时间统计;订单额按支付时间统计;新增会员为新增注册用户)'
- => '(发布量按发布时间统计;订单额按支付时间统计;新增会员为新增注册用户)',
+ '近 14 天' => '近 14 天',
+ '趋势图' => '趋势图',
+ '近 14 天合计' => '近 14 天合计',
+ '切换趋势指标' => '切换趋势指标',
+ '发布量按发布时间统计,订单额按支付时间统计'
+ => '发布量按发布时间统计,订单额按支付时间统计',
+ '近 14 天明细' => '近 14 天明细',
+ '展开查看每日数据' => '展开查看每日数据',
+ '收起明细' => '收起明细',
+ '近 14 天每日发布量、订单额与新增注册用户'
+ => '近 14 天每日发布量、订单额与新增注册用户',
'日期' => '日期',
'发布量' => '发布量',
'订单额' => '订单额',
'订单额(元)' => '订单额(元)',
+ '新增用户' => '新增用户',
'新增注册用户' => '新增注册用户',
];
diff --git a/application/admin/model/FcArea.php b/application/admin/model/FcArea.php
index 6be3c9e..8db2bfa 100644
--- a/application/admin/model/FcArea.php
+++ b/application/admin/model/FcArea.php
@@ -4,17 +4,14 @@ namespace app\admin\model;
use think\Model;
-
+/**
+ * 同城-服务区域(省 / 市 / 区县平铺,区县为最终服务区域)
+ */
class FcArea extends Model
{
-
-
-
-
-
// 表名
protected $name = 'fc_area';
-
+
// 自动写入时间戳字段
protected $autoWriteTimestamp = 'integer';
@@ -25,9 +22,17 @@ class FcArea extends Model
// 追加属性
protected $append = [
-
+ 'full_name',
];
-
+
+ /** 完整区域:省 / 市 / 区县 */
+ public function getFullNameAttr($value, $data)
+ {
+ $province = isset($data['province']) ? (string)$data['province'] : '';
+ $city = isset($data['city']) ? (string)$data['city'] : '';
+ $district = isset($data['district']) ? (string)$data['district'] : '';
+ return $province . ' / ' . $city . ' / ' . $district;
+ }
protected static function init()
{
@@ -38,13 +43,4 @@ class FcArea extends Model
}
});
}
-
-
-
-
-
-
-
-
-
}
diff --git a/application/admin/model/FcOrder.php b/application/admin/model/FcOrder.php
index 5e738e4..bfa980b 100644
--- a/application/admin/model/FcOrder.php
+++ b/application/admin/model/FcOrder.php
@@ -40,10 +40,11 @@ class FcOrder extends Model
public function getStatusList()
{
return [
- 'pending' => __('待支付'),
- 'paid' => __('已支付'),
- 'refunded' => __('已退款'),
- 'closed' => __('已关闭'),
+ 'pending' => __('待支付'),
+ 'paid' => __('已支付'),
+ 'refunding' => __('退款中'),
+ 'refunded' => __('已退款'),
+ 'closed' => __('已关闭'),
];
}
diff --git a/application/admin/model/FcPost.php b/application/admin/model/FcPost.php
index f42616a..69efd04 100644
--- a/application/admin/model/FcPost.php
+++ b/application/admin/model/FcPost.php
@@ -39,6 +39,21 @@ class FcPost extends Model
{
return FcRelation::categoryName(isset($data['category_id']) ? $data['category_id'] : 0);
}
+
+ protected static function init()
+ {
+ $syncArea = function ($row) {
+ $name = isset($row['area']) ? trim((string)$row['area']) : '';
+ if ($name === '') {
+ $row['district_id'] = 0;
+ return;
+ }
+ $area = \think\Db::name('fc_area')->where('district', $name)->find();
+ $row['district_id'] = $area ? (int)$area['id'] : 0;
+ };
+ self::beforeInsert($syncArea);
+ self::beforeUpdate($syncArea);
+ }
diff --git a/application/admin/model/FcWithdraw.php b/application/admin/model/FcWithdraw.php
index bd9b2ad..5dc37c2 100644
--- a/application/admin/model/FcWithdraw.php
+++ b/application/admin/model/FcWithdraw.php
@@ -41,7 +41,9 @@ class FcWithdraw extends Model
{
return [
'auditing' => __('待审核'),
- 'paid' => __('已打款'),
+ 'paying' => __('转账中'),
+ 'paid' => __('已到账'),
+ 'failed' => __('转账失败'),
'rejected' => __('已驳回'),
];
}
diff --git a/application/admin/view/auth/rule/add.html b/application/admin/view/auth/rule/add.html
index 33fd459..9c4b8ca 100755
--- a/application/admin/view/auth/rule/add.html
+++ b/application/admin/view/auth/rule/add.html
@@ -4,18 +4,21 @@
{:build_radios('row[ismenu]', ['1'=>__('Yes'), '0'=>__('No')])}
+
开启后显示在左侧菜单;关闭则只作为权限节点。