From 72cea8445ff86dcea1bc3a36ecbd9bab1068761a Mon Sep 17 00:00:00 2001 From: MiAn Date: Thu, 12 Nov 2026 17:19:49 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=8C=E5=96=84=E5=8D=8F=E8=AE=AE?= =?UTF-8?q?=E7=AE=A1=E7=90=86=E3=80=81=E5=B8=88=E5=82=85=E8=B5=84=E8=B4=A8?= =?UTF-8?q?=E8=AE=A4=E8=AF=81=E3=80=81=E5=88=86=E8=B4=A6=E9=85=8D=E7=BD=AE?= =?UTF-8?q?=E5=B9=B6=E5=AE=8C=E6=88=90=E6=B5=8B=E8=AF=95=E7=8E=AF=E5=A2=83?= =?UTF-8?q?=E5=85=A8=E9=87=8F=E9=83=A8=E7=BD=B2=E4=B8=8E=E5=90=8C=E6=AD=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- db/schema_phase18_gallery_template.sql | 29 + db/schema_phase19_commission_snapshot.sql | 95 + db/schema_phase19_expand_service_cats.sql | 296 ++ db/schema_phase19_wxpay_transfer.sql | 44 + db/schema_phase20_finance_config.sql | 194 + db/schema_phase21_agreement.sql | 108 + db/schema_phase22_wxpay_audit_fix.sql | 20 + db/schema_phase23_agreement_diff.sql | 59 + db/schema_phase24_master_cert.sql | 127 + db/schema_phase25_real_data_cleanup.sql | 62 + db/seed_phase25_area.sql | 3319 +++++++++++++++++ scripts/deploy_db.py | 46 + scripts/deploy_server.py | 122 + scripts/test_phase1_implementation.php | 11 +- server/addons/address/config.php | 4 +- server/addons/epay/controller/Api.php | 25 +- .../admin/command/OrderAutoverify.php | 8 +- .../admin/controller/Agreement.php | 73 + .../admin/controller/FinanceConfig.php | 363 ++ server/application/admin/controller/Home.php | 6 +- .../application/admin/controller/Master.php | 16 + .../admin/controller/MasterCert.php | 184 + server/application/admin/controller/Order.php | 155 +- .../application/admin/controller/Service.php | 46 +- .../application/admin/controller/Withdraw.php | 141 +- .../admin/controller/general/Config.php | 2 +- .../admin/controller/service/Gallery.php | 179 + .../admin/lang/zh-cn/finance_config.php | 34 + .../admin/lang/zh-cn/master_cert.php | 20 + .../admin/lang/zh-cn/service/gallery.php | 14 + server/application/admin/model/Agreement.php | 27 + .../admin/model/CommissionRule.php | 61 + server/application/admin/model/MasterCert.php | 40 + server/application/admin/model/Withdraw.php | 12 +- .../admin/model/service/Gallery.php | 41 + .../admin/view/agreement/index.html | 516 +++ .../admin/view/finance_config/index.html | 503 +++ .../admin/view/finance_config/rule_add.html | 3 + .../admin/view/finance_config/rule_edit.html | 3 + .../admin/view/finance_config/rule_form.html | 210 ++ .../application/admin/view/master/detail.html | 45 +- .../admin/view/master_cert/audit.html | 107 + .../admin/view/master_cert/detail.html | 54 + .../admin/view/master_cert/index.html | 30 + .../application/admin/view/order/detail.html | 91 +- .../application/admin/view/service/add.html | 10 +- .../application/admin/view/service/edit.html | 20 +- .../admin/view/service/gallery/add.html | 64 + .../admin/view/service/gallery/edit.html | 63 + .../admin/view/service/gallery/index.html | 38 + .../application/admin/view/service/index.html | 1 + .../application/admin/view/settle/index.html | 3 + .../admin/view/withdraw/detail.html | 20 +- .../application/admin/view/withdraw/edit.html | 52 +- .../admin/view/withdraw/index.html | 3 + server/application/api/controller/Address.php | 16 + server/application/api/controller/Auth.php | 101 +- server/application/api/controller/Base.php | 18 +- server/application/api/controller/Common.php | 165 +- server/application/api/controller/Home.php | 298 +- server/application/api/controller/Master.php | 1494 +++++++- .../application/api/controller/Merchant.php | 147 +- server/application/api/controller/Order.php | 632 ++-- server/application/api/controller/Sms.php | 82 +- server/application/api/controller/User.php | 111 +- .../application/api/controller/Validate.php | 13 +- server/application/api/controller/Wxpay.php | 140 + .../common/library/AgreementService.php | 1218 ++++++ .../common/library/CommissionService.php | 558 +++ .../common/library/FinanceConfigService.php | 680 ++++ .../common/library/PaymentService.php | 1923 ++++++++++ .../application/common/library/SmsService.php | 130 - .../common/library/TencentVerifyService.php | 439 +++ .../common/library/TiandituService.php | 318 +- server/application/common/library/WxLogin.php | 47 +- .../common/library/WxPayException.php | 69 + .../common/library/WxPayV3Service.php | 1063 ++++++ server/application/common/model/Config.php | 19 +- server/application/extra/site.php | 11 +- server/application/index/controller/User.php | 3 +- server/application/route.php | 6 + server/public/assets/js/backend/agreement.js | 347 ++ .../assets/js/backend/finance_config.js | 418 +++ .../public/assets/js/backend/master_cert.js | 136 + server/public/assets/js/backend/order.js | 44 + server/public/assets/js/backend/service.js | 76 +- .../assets/js/backend/service/gallery.js | 164 + server/public/assets/js/backend/settle.js | 4 +- server/public/assets/js/backend/withdraw.js | 40 +- yjyf-app/components/LogisticsDrawer.vue | 97 +- yjyf-app/components/MediaActionDrawer.vue | 5 +- yjyf-app/components/RegionPickerDrawer.vue | 347 +- yjyf-app/components/master/MasterHallView.vue | 1788 +++++---- .../components/master/MasterWorkbenchView.vue | 1365 +++++-- yjyf-app/components/user/UserHomeView.vue | 682 +++- yjyf-app/components/user/UserMineView.vue | 143 +- yjyf-app/components/user/UserOrdersView.vue | 6 +- yjyf-app/config/config.js | 6 +- yjyf-app/manifest.json | 10 +- yjyf-app/pages.json | 34 + yjyf-app/pages/address/list.vue | 12 +- yjyf-app/pages/agreement/detail.vue | 496 +++ yjyf-app/pages/category/category.vue | 8 +- yjyf-app/pages/login/login.vue | 158 +- yjyf-app/pages/master/apply.vue | 347 +- yjyf-app/pages/master/auth.vue | 775 ++++ yjyf-app/pages/master/cert-upload.vue | 883 +++++ yjyf-app/pages/master/certs.vue | 456 +++ yjyf-app/pages/master/credit.vue | 39 +- yjyf-app/pages/master/detail.vue | 2 +- yjyf-app/pages/master/location.vue | 2166 +++++++++++ yjyf-app/pages/master/success.vue | 2 +- yjyf-app/pages/master/wallet.vue | 684 +++- yjyf-app/pages/order/create.vue | 18 +- yjyf-app/pages/order/detail.vue | 19 +- yjyf-app/pages/order/goods-gallery.vue | 122 +- yjyf-app/pages/order/goods-template.vue | 1026 +++++ yjyf-app/pages/order/quick.vue | 757 ++-- yjyf-app/pages/order/review.vue | 38 +- yjyf-app/pages/order/system-gallery.vue | 91 +- yjyf-app/static/icons/agreement-error.svg | 6 + yjyf-app/static/icons/alert-circle-red.svg | 5 + yjyf-app/static/icons/bank-card.svg | 6 + yjyf-app/static/icons/cat-awning.svg | 29 + yjyf-app/static/icons/cat-camera.svg | 33 + yjyf-app/static/icons/cat-charger.svg | 29 + yjyf-app/static/icons/cat-circuit.svg | 28 + yjyf-app/static/icons/cat-clean.svg | 29 + yjyf-app/static/icons/cat-floor.svg | 29 + yjyf-app/static/icons/cat-office.svg | 34 + yjyf-app/static/icons/cat-pipe.svg | 28 + yjyf-app/static/icons/cat-signboard.svg | 31 + yjyf-app/static/icons/cat-smart.svg | 28 + yjyf-app/static/icons/empty-agreement.svg | 7 + yjyf-app/static/icons/empty-template.svg | 33 + yjyf-app/static/icons/master/cert-camera.svg | 13 + yjyf-app/static/icons/master/cert-clear.svg | 4 + .../static/icons/master/cert-date-down.svg | 3 + .../static/icons/master/cert-frame-bg.svg | 13 + yjyf-app/static/icons/master/cert-notice.svg | 5 + yjyf-app/static/icons/master/cert-search.svg | 4 + .../icons/master/hall-arrow-right-orange.svg | 3 + yjyf-app/static/icons/master/hall-error.svg | 7 + .../static/icons/master/hall-loc-close.svg | 3 + .../static/icons/master/hall-loc-search.svg | 4 + .../static/icons/master/hall-loc-target.svg | 5 + .../static/icons/master/hall-location-pin.svg | 4 + .../static/icons/master/hall-pause-warn.svg | 5 + .../icons/master/hall-recommend-near.svg | 5 + .../static/icons/master/loc-city-caret.svg | 3 + yjyf-app/static/icons/master/loc-clear.svg | 4 + yjyf-app/static/icons/master/loc-delete.svg | 3 + .../static/icons/master/loc-history-clock.svg | 4 + .../static/icons/master/loc-home-city.svg | 3 + .../static/icons/master/loc-map-collapse.svg | 3 + .../icons/master/loc-map-pin-center.svg | 12 + .../static/icons/master/loc-poi-checked.svg | 10 + yjyf-app/static/icons/master/loc-poi-dot.svg | 4 + .../static/icons/master/loc-radius-radar.svg | 5 + .../static/icons/master/menu-agreement.svg | 4 + yjyf-app/static/icons/master/menu-auth.svg | 6 + yjyf-app/static/icons/master/menu-cert.svg | 7 + yjyf-app/static/icons/master/skill-toggle.svg | 3 + .../icons/master/switch-order-white.svg | 4 + .../static/icons/master/workbench-error.svg | 9 + yjyf-app/static/icons/media-remove-red.svg | 4 + yjyf-app/static/icons/qrcode.svg | 29 + yjyf-app/static/icons/shield-green.svg | 4 + yjyf-app/static/icons/transfer-receive.svg | 6 + yjyf-app/static/icons/v-auth.svg | 4 + yjyf-app/static/icons/withdraw-record.svg | 7 + .../images/master/cert-sample-driver.png | Bin 0 -> 118446 bytes .../images/master/cert-sample-health.png | Bin 0 -> 115895 bytes .../master/cert-sample-qualification.png | Bin 0 -> 125966 bytes .../master/cert-sample-special-card.png | Bin 0 -> 118882 bytes yjyf-app/utils/api.js | 20 +- yjyf-app/utils/location.js | 407 ++ yjyf-app/utils/pay.js | 251 ++ yjyf-app/utils/request.js | 2 +- 179 files changed, 29242 insertions(+), 3340 deletions(-) create mode 100644 db/schema_phase19_commission_snapshot.sql create mode 100644 db/schema_phase19_expand_service_cats.sql create mode 100644 db/schema_phase19_wxpay_transfer.sql create mode 100644 db/schema_phase20_finance_config.sql create mode 100644 db/schema_phase21_agreement.sql create mode 100644 db/schema_phase22_wxpay_audit_fix.sql create mode 100644 db/schema_phase23_agreement_diff.sql create mode 100644 db/schema_phase24_master_cert.sql create mode 100644 db/schema_phase25_real_data_cleanup.sql create mode 100644 db/seed_phase25_area.sql create mode 100644 scripts/deploy_db.py create mode 100644 scripts/deploy_server.py create mode 100644 server/application/admin/controller/Agreement.php create mode 100644 server/application/admin/controller/FinanceConfig.php create mode 100644 server/application/admin/controller/MasterCert.php create mode 100644 server/application/admin/controller/service/Gallery.php create mode 100644 server/application/admin/lang/zh-cn/finance_config.php create mode 100644 server/application/admin/lang/zh-cn/master_cert.php create mode 100644 server/application/admin/lang/zh-cn/service/gallery.php create mode 100644 server/application/admin/model/Agreement.php create mode 100644 server/application/admin/model/CommissionRule.php create mode 100644 server/application/admin/model/MasterCert.php create mode 100644 server/application/admin/model/service/Gallery.php create mode 100644 server/application/admin/view/agreement/index.html create mode 100644 server/application/admin/view/finance_config/index.html create mode 100644 server/application/admin/view/finance_config/rule_add.html create mode 100644 server/application/admin/view/finance_config/rule_edit.html create mode 100644 server/application/admin/view/finance_config/rule_form.html create mode 100644 server/application/admin/view/master_cert/audit.html create mode 100644 server/application/admin/view/master_cert/detail.html create mode 100644 server/application/admin/view/master_cert/index.html create mode 100644 server/application/admin/view/service/gallery/add.html create mode 100644 server/application/admin/view/service/gallery/edit.html create mode 100644 server/application/admin/view/service/gallery/index.html create mode 100644 server/application/api/controller/Wxpay.php create mode 100644 server/application/common/library/AgreementService.php create mode 100644 server/application/common/library/CommissionService.php create mode 100644 server/application/common/library/FinanceConfigService.php create mode 100644 server/application/common/library/PaymentService.php delete mode 100644 server/application/common/library/SmsService.php create mode 100644 server/application/common/library/TencentVerifyService.php create mode 100644 server/application/common/library/WxPayException.php create mode 100644 server/application/common/library/WxPayV3Service.php create mode 100644 server/public/assets/js/backend/agreement.js create mode 100644 server/public/assets/js/backend/finance_config.js create mode 100644 server/public/assets/js/backend/master_cert.js create mode 100644 server/public/assets/js/backend/service/gallery.js create mode 100644 yjyf-app/pages/agreement/detail.vue create mode 100644 yjyf-app/pages/master/auth.vue create mode 100644 yjyf-app/pages/master/cert-upload.vue create mode 100644 yjyf-app/pages/master/certs.vue create mode 100644 yjyf-app/pages/master/location.vue create mode 100644 yjyf-app/pages/order/goods-template.vue create mode 100644 yjyf-app/static/icons/agreement-error.svg create mode 100644 yjyf-app/static/icons/alert-circle-red.svg create mode 100644 yjyf-app/static/icons/bank-card.svg create mode 100644 yjyf-app/static/icons/cat-awning.svg create mode 100644 yjyf-app/static/icons/cat-camera.svg create mode 100644 yjyf-app/static/icons/cat-charger.svg create mode 100644 yjyf-app/static/icons/cat-circuit.svg create mode 100644 yjyf-app/static/icons/cat-clean.svg create mode 100644 yjyf-app/static/icons/cat-floor.svg create mode 100644 yjyf-app/static/icons/cat-office.svg create mode 100644 yjyf-app/static/icons/cat-pipe.svg create mode 100644 yjyf-app/static/icons/cat-signboard.svg create mode 100644 yjyf-app/static/icons/cat-smart.svg create mode 100644 yjyf-app/static/icons/empty-agreement.svg create mode 100644 yjyf-app/static/icons/empty-template.svg create mode 100644 yjyf-app/static/icons/master/cert-camera.svg create mode 100644 yjyf-app/static/icons/master/cert-clear.svg create mode 100644 yjyf-app/static/icons/master/cert-date-down.svg create mode 100644 yjyf-app/static/icons/master/cert-frame-bg.svg create mode 100644 yjyf-app/static/icons/master/cert-notice.svg create mode 100644 yjyf-app/static/icons/master/cert-search.svg create mode 100644 yjyf-app/static/icons/master/hall-arrow-right-orange.svg create mode 100644 yjyf-app/static/icons/master/hall-error.svg create mode 100644 yjyf-app/static/icons/master/hall-loc-close.svg create mode 100644 yjyf-app/static/icons/master/hall-loc-search.svg create mode 100644 yjyf-app/static/icons/master/hall-loc-target.svg create mode 100644 yjyf-app/static/icons/master/hall-location-pin.svg create mode 100644 yjyf-app/static/icons/master/hall-pause-warn.svg create mode 100644 yjyf-app/static/icons/master/hall-recommend-near.svg create mode 100644 yjyf-app/static/icons/master/loc-city-caret.svg create mode 100644 yjyf-app/static/icons/master/loc-clear.svg create mode 100644 yjyf-app/static/icons/master/loc-delete.svg create mode 100644 yjyf-app/static/icons/master/loc-history-clock.svg create mode 100644 yjyf-app/static/icons/master/loc-home-city.svg create mode 100644 yjyf-app/static/icons/master/loc-map-collapse.svg create mode 100644 yjyf-app/static/icons/master/loc-map-pin-center.svg create mode 100644 yjyf-app/static/icons/master/loc-poi-checked.svg create mode 100644 yjyf-app/static/icons/master/loc-poi-dot.svg create mode 100644 yjyf-app/static/icons/master/loc-radius-radar.svg create mode 100644 yjyf-app/static/icons/master/menu-agreement.svg create mode 100644 yjyf-app/static/icons/master/menu-auth.svg create mode 100644 yjyf-app/static/icons/master/menu-cert.svg create mode 100644 yjyf-app/static/icons/master/skill-toggle.svg create mode 100644 yjyf-app/static/icons/master/switch-order-white.svg create mode 100644 yjyf-app/static/icons/master/workbench-error.svg create mode 100644 yjyf-app/static/icons/media-remove-red.svg create mode 100644 yjyf-app/static/icons/qrcode.svg create mode 100644 yjyf-app/static/icons/shield-green.svg create mode 100644 yjyf-app/static/icons/transfer-receive.svg create mode 100644 yjyf-app/static/icons/v-auth.svg create mode 100644 yjyf-app/static/icons/withdraw-record.svg create mode 100644 yjyf-app/static/images/master/cert-sample-driver.png create mode 100644 yjyf-app/static/images/master/cert-sample-health.png create mode 100644 yjyf-app/static/images/master/cert-sample-qualification.png create mode 100644 yjyf-app/static/images/master/cert-sample-special-card.png create mode 100644 yjyf-app/utils/location.js create mode 100644 yjyf-app/utils/pay.js diff --git a/db/schema_phase18_gallery_template.sql b/db/schema_phase18_gallery_template.sql index 6ce9fc0..5d166a0 100644 --- a/db/schema_phase18_gallery_template.sql +++ b/db/schema_phase18_gallery_template.sql @@ -93,3 +93,32 @@ UPDATE `fa_system_gallery` SET `image` = 'https://yjyf-1512102519.cos.ap-guangzh UPDATE `fa_system_gallery` SET `image` = 'https://yjyf-1512102519.cos.ap-guangzhou.myqcloud.com/gallery/real_comm_dishwasher.jpg' WHERE `service_id` = 36; UPDATE `fa_system_gallery` SET `image` = 'https://yjyf-1512102519.cos.ap-guangzhou.myqcloud.com/gallery/real_comm_fridge.jpg' WHERE `service_id` = 38; UPDATE `fa_system_gallery` SET `image` = 'https://yjyf-1512102519.cos.ap-guangzhou.myqcloud.com/gallery/real_water_boiler.jpg' WHERE `service_id` = 40; + + +-- 6. 管理后台「服务商品 -> 系统图库」菜单与权限节点幂等注册 +INSERT INTO `fa_auth_rule` (`type`, `pid`, `name`, `title`, `icon`, `url`, `condition`, `remark`, `ismenu`, `menutype`, `extend`, `py`, `pinyin`, `createtime`, `updatetime`, `weigh`, `status`) +SELECT 'file', 234, 'service/gallery', '系统图库', 'fa fa-picture-o', '', '', '官方系统图库管理(支持按类目与商品单独维护多张参考图,与商品缩略图/规格参考图同源互通)', 1, NULL, '', 'xttk', 'xitongtuku', UNIX_TIMESTAMP(), UNIX_TIMESTAMP(), 184, 'normal' +FROM DUAL +WHERE NOT EXISTS (SELECT 1 FROM `fa_auth_rule` WHERE `name` = 'service/gallery'); + +SET @gallery_rule_id := (SELECT `id` FROM `fa_auth_rule` WHERE `name` = 'service/gallery' LIMIT 1); + +INSERT INTO `fa_auth_rule` (`type`, `pid`, `name`, `title`, `icon`, `url`, `condition`, `remark`, `ismenu`, `menutype`, `extend`, `py`, `pinyin`, `createtime`, `updatetime`, `weigh`, `status`) +SELECT 'file', @gallery_rule_id, 'service/gallery/index', '查看', 'fa fa-circle-o', '', '', '', 0, NULL, '', '', '', UNIX_TIMESTAMP(), UNIX_TIMESTAMP(), 0, 'normal' +FROM DUAL WHERE @gallery_rule_id IS NOT NULL AND NOT EXISTS (SELECT 1 FROM `fa_auth_rule` WHERE `name` = 'service/gallery/index'); + +INSERT INTO `fa_auth_rule` (`type`, `pid`, `name`, `title`, `icon`, `url`, `condition`, `remark`, `ismenu`, `menutype`, `extend`, `py`, `pinyin`, `createtime`, `updatetime`, `weigh`, `status`) +SELECT 'file', @gallery_rule_id, 'service/gallery/add', '添加', 'fa fa-circle-o', '', '', '', 0, NULL, '', '', '', UNIX_TIMESTAMP(), UNIX_TIMESTAMP(), 0, 'normal' +FROM DUAL WHERE @gallery_rule_id IS NOT NULL AND NOT EXISTS (SELECT 1 FROM `fa_auth_rule` WHERE `name` = 'service/gallery/add'); + +INSERT INTO `fa_auth_rule` (`type`, `pid`, `name`, `title`, `icon`, `url`, `condition`, `remark`, `ismenu`, `menutype`, `extend`, `py`, `pinyin`, `createtime`, `updatetime`, `weigh`, `status`) +SELECT 'file', @gallery_rule_id, 'service/gallery/edit', '编辑', 'fa fa-circle-o', '', '', '', 0, NULL, '', '', '', UNIX_TIMESTAMP(), UNIX_TIMESTAMP(), 0, 'normal' +FROM DUAL WHERE @gallery_rule_id IS NOT NULL AND NOT EXISTS (SELECT 1 FROM `fa_auth_rule` WHERE `name` = 'service/gallery/edit'); + +INSERT INTO `fa_auth_rule` (`type`, `pid`, `name`, `title`, `icon`, `url`, `condition`, `remark`, `ismenu`, `menutype`, `extend`, `py`, `pinyin`, `createtime`, `updatetime`, `weigh`, `status`) +SELECT 'file', @gallery_rule_id, 'service/gallery/del', '删除', 'fa fa-circle-o', '', '', '', 0, NULL, '', '', '', UNIX_TIMESTAMP(), UNIX_TIMESTAMP(), 0, 'normal' +FROM DUAL WHERE @gallery_rule_id IS NOT NULL AND NOT EXISTS (SELECT 1 FROM `fa_auth_rule` WHERE `name` = 'service/gallery/del'); + +INSERT INTO `fa_auth_rule` (`type`, `pid`, `name`, `title`, `icon`, `url`, `condition`, `remark`, `ismenu`, `menutype`, `extend`, `py`, `pinyin`, `createtime`, `updatetime`, `weigh`, `status`) +SELECT 'file', @gallery_rule_id, 'service/gallery/multi', '批量更新', 'fa fa-circle-o', '', '', '', 0, NULL, '', '', '', UNIX_TIMESTAMP(), UNIX_TIMESTAMP(), 0, 'normal' +FROM DUAL WHERE @gallery_rule_id IS NOT NULL AND NOT EXISTS (SELECT 1 FROM `fa_auth_rule` WHERE `name` = 'service/gallery/multi'); diff --git a/db/schema_phase19_commission_snapshot.sql b/db/schema_phase19_commission_snapshot.sql new file mode 100644 index 0000000..2783549 --- /dev/null +++ b/db/schema_phase19_commission_snapshot.sql @@ -0,0 +1,95 @@ +-- ============================================================================== +-- Phase 19: 平台分成抽佣后台动态配置与订单抽佣比例快照升级 +-- ============================================================================== + +-- 1. 订单表新增平台抽佣比例快照字段(幂等执行,历史存量订单平滑默认 10.00%) +SET @col_exists_order := ( + SELECT COUNT(*) + FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = 'fa_order' + AND COLUMN_NAME = 'commission_rate' +); +SET @sql_order := IF( + @col_exists_order = 0, + 'ALTER TABLE `fa_order` ADD COLUMN `commission_rate` decimal(5,2) NOT NULL DEFAULT ''10.00'' COMMENT ''平台抽佣比例快照(%)'' AFTER `pay_amount`', + 'SELECT 1' +); +PREPARE stmt_order FROM @sql_order; +EXECUTE stmt_order; +DEALLOCATE PREPARE stmt_order; + +-- 对齐已有结算账单的历史订单抽佣快照 +UPDATE `fa_order` o +INNER JOIN `fa_settle` s ON s.order_id = o.id +SET o.commission_rate = s.commission_rate; + +-- 2. 服务类目表新增可选类目专属抽佣比例字段(NULL 表示沿用系统全局默认抽佣比例) +SET @col_exists_cat := ( + SELECT COUNT(*) + FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = 'fa_service_cat' + AND COLUMN_NAME = 'commission_rate' +); +SET @sql_cat := IF( + @col_exists_cat = 0, + 'ALTER TABLE `fa_service_cat` ADD COLUMN `commission_rate` decimal(5,2) DEFAULT NULL COMMENT ''类目专属抽佣比例%(NULL沿用全局默认)'' AFTER `icon`', + 'SELECT 1' +); +PREPARE stmt_cat FROM @sql_cat; +EXECUTE stmt_cat; +DEALLOCATE PREPARE stmt_cat; + +-- 3. 系统配置分组新增「财务结算」分组并写入全局默认抽佣比例配置项 +UPDATE `fa_config` +SET `value` = '{"basic":"基础配置","finance":"财务结算","user":"会员配置","email":"邮箱配置","dictionary":"字典配置"}' +WHERE `name` = 'configgroup' AND `value` NOT LIKE '%"finance"%'; + +INSERT INTO `fa_config` (`name`, `group`, `title`, `tip`, `type`, `visible`, `value`, `content`, `rule`, `extend`, `setting`) +SELECT + 'commission_rate', + 'finance', + '平台默认抽佣比例(%)', + '订单完工结算时平台抽取的技术服务费百分比(0~100,支持两位小数)。修改后仅对新创建订单生效,历史订单按创单快照比例结算', + 'number', + '', + '10.00', + '', + 'required;range(0~100)', + 'step=0.01 min=0 max=100', + '{"table":"","conditions":"","key":"","value":""}' +FROM DUAL +WHERE NOT EXISTS ( + SELECT 1 FROM `fa_config` WHERE `name` = 'commission_rate' +); + +-- 4. 结算账单模块新增「抽佣比例配置」权限节点(支持财务模块快捷设置) +SET @settle_pid := (SELECT `id` FROM `fa_auth_rule` WHERE `name` = 'settle' LIMIT 1); +INSERT INTO `fa_auth_rule` (`type`, `pid`, `name`, `title`, `icon`, `url`, `condition`, `remark`, `ismenu`, `menutype`, `extend`, `py`, `pinyin`, `createtime`, `updatetime`, `weigh`, `status`) +SELECT + 'file', + IFNULL(@settle_pid, 0), + 'settle/commission_config', + '抽佣比例配置', + 'fa fa-circle-o', + '', + '', + '配置平台全局默认抽佣分成比例(仅对新创订单生效)', + 0, + NULL, + '', + 'cyblpz', + 'chouyongbilipeizhi', + UNIX_TIMESTAMP(), + UNIX_TIMESTAMP(), + 0, + 'normal' +FROM DUAL +WHERE @settle_pid IS NOT NULL + AND NOT EXISTS ( + SELECT 1 FROM `fa_auth_rule` WHERE `name` = 'settle/commission_config' + ); + +-- 5. 刷新前端静态资源版本号,确保后台 JS 表格列变更即时生效 +UPDATE `fa_config` SET `value` = '1.0.52' WHERE `name` = 'version'; diff --git a/db/schema_phase19_expand_service_cats.sql b/db/schema_phase19_expand_service_cats.sql new file mode 100644 index 0000000..854c119 --- /dev/null +++ b/db/schema_phase19_expand_service_cats.sql @@ -0,0 +1,296 @@ +-- ============================================================================ +-- Phase 19: 扩张一级服务类目体系(15 -> 25 个一级垂直类目,5x5 宫格完整闭环) +-- 1. 调整原有 15 个一级类目权重(前 14 个保持 240~110 顺序不变,其它 id=15 保持 weigh=0 置底) +-- 2. 新增 10 个核心一级类目(id=16~25,各自配备独立专属 SVG 图标,weigh=100~10) +-- 3. 在全局服务字典 fa_service_type 补充 wash(深度清洗) 与 dredge(管道疏通) 一口价标准工项 +-- 4. 为 10 个新增一级类目配套真实的二级服务商品(fa_service)与细分规格(fa_service_spec) +-- ============================================================================ +SET NAMES utf8mb4; + +-- 1. 更新原有 15 个一级类目的排序权重,为新增类目预留 100~10 权重段,确保“其它(id=15)”始终置底 +UPDATE `fa_service_cat` SET `weigh` = 240 WHERE `id` = 2; +UPDATE `fa_service_cat` SET `weigh` = 230 WHERE `id` = 1; +UPDATE `fa_service_cat` SET `weigh` = 220 WHERE `id` = 3; +UPDATE `fa_service_cat` SET `weigh` = 210 WHERE `id` = 4; +UPDATE `fa_service_cat` SET `weigh` = 200 WHERE `id` = 5; +UPDATE `fa_service_cat` SET `weigh` = 190 WHERE `id` = 6; +UPDATE `fa_service_cat` SET `weigh` = 180 WHERE `id` = 7; +UPDATE `fa_service_cat` SET `weigh` = 170 WHERE `id` = 8; +UPDATE `fa_service_cat` SET `weigh` = 160 WHERE `id` = 9; +UPDATE `fa_service_cat` SET `weigh` = 150 WHERE `id` = 10; +UPDATE `fa_service_cat` SET `weigh` = 140 WHERE `id` = 11; +UPDATE `fa_service_cat` SET `weigh` = 130 WHERE `id` = 12; +UPDATE `fa_service_cat` SET `weigh` = 120 WHERE `id` = 13; +UPDATE `fa_service_cat` SET `weigh` = 110 WHERE `id` = 14; +UPDATE `fa_service_cat` SET `weigh` = 0 WHERE `id` = 15; + +-- 2. 新增 10 个一级服务类目(每个类目均使用独立生成的专属 SVG 矢量图标) +INSERT INTO `fa_service_cat` (`id`, `pid`, `name`, `icon`, `weigh`, `status`, `createtime`, `updatetime`) VALUES +(16, 0, '监控安防', '/icon/cat-camera.svg', 100, 'normal', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), +(17, 0, '充电桩', '/icon/cat-charger.svg', 90, 'normal', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), +(18, 0, '智能家居', '/icon/cat-smart.svg', 80, 'normal', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), +(19, 0, '地板/地毯', '/icon/cat-floor.svg', 70, 'normal', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), +(20, 0, '户外遮阳', '/icon/cat-awning.svg', 60, 'normal', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), +(21, 0, '广告展柜', '/icon/cat-signboard.svg', 50, 'normal', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), +(22, 0, '办公设备', '/icon/cat-office.svg', 40, 'normal', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), +(23, 0, '水电维修', '/icon/cat-circuit.svg', 30, 'normal', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), +(24, 0, '家电清洗', '/icon/cat-clean.svg', 20, 'normal', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), +(25, 0, '管道疏通', '/icon/cat-pipe.svg', 10, 'normal', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()) +ON DUPLICATE KEY UPDATE + `name` = VALUES(`name`), + `icon` = VALUES(`icon`), + `weigh` = VALUES(`weigh`), + `status` = VALUES(`status`), + `updatetime` = UNIX_TIMESTAMP(); + +-- 3. 补充全局服务字典工项:深度清洗(wash) 与 管道疏通(dredge),支持一口价与报价双模式 +INSERT INTO `fa_service_type` (`code`, `name`, `unit`, `can_be_main`, `can_be_addon`, `support_fixed`, `default_price`, `default_addon_price`, `is_default`, `weigh`, `status`, `createtime`, `updatetime`) VALUES +('wash', '深度清洗', '台', 1, 0, 1, 88.00, 0.00, 0, 75, 'normal', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), +('dredge', '管道疏通', '次', 1, 0, 1, 100.00, 0.00, 0, 74, 'normal', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()) +ON DUPLICATE KEY UPDATE + `name` = VALUES(`name`), + `support_fixed` = VALUES(`support_fixed`), + `status` = 'normal', + `updatetime` = UNIX_TIMESTAMP(); + +-- 4. 为 10 个新增一级类目写入真实二级服务商品(ID 108 ~ 134) +INSERT INTO `fa_service` (`id`, `cat_id`, `name`, `image`, `price_type`, `service_types`, `addons`, `base_price`, `max_price`, `unit`, `floor_fee`, `free_floor`, `need_model`, `description`, `sales`, `weigh`, `status`, `createtime`, `updatetime`) VALUES +-- [16] 监控安防 +(108, 16, '监控摄像头', '/icon/cat-camera.svg', 'fixed', + '[{"code":"install","name":"安装","price":65,"is_default":1,"support_fixed":1},{"code":"repair","name":"维修","price":60,"is_default":0,"support_fixed":1},{"code":"remove","name":"拆旧","price":25,"is_default":0,"support_fixed":1}]', + '[{"id":1,"code":"remove","name":"拆旧","unit":"份","price":25},{"id":2,"code":"drill","name":"打孔","unit":"份","price":30}]', + 65.00, 0.00, '台', 5.00, 2, 1, '室内外网络摄像头打孔固定、联网配对与画面调试', 215, 100, 'normal', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), +(109, 16, '门禁考勤一体机', '/icon/cat-camera.svg', 'fixed', + '[{"code":"install","name":"安装","price":150,"is_default":1,"support_fixed":1},{"code":"repair","name":"维修","price":100,"is_default":0,"support_fixed":1},{"code":"remove","name":"拆旧","price":40,"is_default":0,"support_fixed":1}]', + '[{"id":1,"code":"remove","name":"拆旧","unit":"份","price":40},{"id":2,"code":"open_hole","name":"开孔","unit":"份","price":45}]', + 150.00, 0.00, '套', 5.00, 2, 1, '企业玻璃门/木门人脸指纹门禁与磁力锁联动安装调试', 142, 95, 'normal', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), +(110, 16, '监控套装(含录像机)', '/icon/cat-camera.svg', 'fixed', + '[{"code":"install","name":"安装","price":260,"is_default":1,"support_fixed":1},{"code":"repair","name":"维修","price":120,"is_default":0,"support_fixed":1}]', + '[{"id":1,"code":"drill","name":"打孔","unit":"份","price":35}]', + 260.00, 0.00, '套', 8.00, 2, 1, '多路摄像头+NVR硬盘录像机明线布放与整套联调', 118, 90, 'normal', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), + +-- [17] 充电桩 +(111, 17, '新能源家用充电桩', '/icon/cat-charger.svg', 'fixed', + '[{"code":"install","name":"安装","price":280,"is_default":1,"support_fixed":1},{"code":"repair","name":"维修","price":150,"is_default":0,"support_fixed":1},{"code":"remove","name":"拆旧","price":100,"is_default":0,"support_fixed":1}]', + '[{"id":1,"code":"remove","name":"拆旧","unit":"份","price":100},{"id":2,"code":"drill","name":"打孔","unit":"份","price":50}]', + 280.00, 0.00, '台', 0.00, 0, 1, '电动汽车壁挂式充电桩安装、漏保接驳及通电充电实测', 238, 100, 'normal', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), +(112, 17, '立柱式充电桩', '/icon/cat-charger.svg', 'fixed', + '[{"code":"install","name":"安装","price":350,"is_default":1,"support_fixed":1},{"code":"remove","name":"拆旧","price":120,"is_default":0,"support_fixed":1}]', + '[{"id":1,"code":"remove","name":"拆旧","unit":"份","price":120},{"id":2,"code":"drill","name":"打孔","unit":"份","price":50}]', + 350.00, 0.00, '套', 0.00, 0, 1, '户外车位膨胀螺栓固定立柱、保护箱组装与电缆接驳', 126, 95, 'normal', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), +(113, 17, '电瓶车扫码充电站', '/icon/cat-charger.svg', 'fixed', + '[{"code":"install","name":"安装","price":220,"is_default":1,"support_fixed":1},{"code":"repair","name":"维修","price":110,"is_default":0,"support_fixed":1}]', + '[{"id":1,"code":"remove","name":"拆旧","unit":"份","price":60}]', + 220.00, 0.00, '套', 0.00, 0, 1, '社区/车棚10路-20路扫码智能插座控制器安装布线', 96, 90, 'normal', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), + +-- [18] 智能家居 +(114, 18, '全屋智能中控屏', '/icon/cat-smart.svg', 'fixed', + '[{"code":"install","name":"安装","price":95,"is_default":1,"support_fixed":1},{"code":"repair","name":"维修","price":75,"is_default":0,"support_fixed":1},{"code":"remove","name":"拆旧","price":20,"is_default":0,"support_fixed":1}]', + '[{"id":1,"code":"remove","name":"拆旧","unit":"份","price":20}]', + 95.00, 0.00, '台', 5.00, 2, 1, '86盒/壁挂智能中控屏零火线接驳、网关配网与设备绑定', 192, 100, 'normal', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), +(115, 18, '智能开关面板', '/icon/cat-smart.svg', 'fixed', + '[{"code":"install","name":"安装","price":35,"is_default":1,"support_fixed":1},{"code":"repair","name":"维修","price":45,"is_default":0,"support_fixed":1},{"code":"remove","name":"拆旧","price":10,"is_default":0,"support_fixed":1}]', + '[{"id":1,"code":"remove","name":"拆旧","unit":"份","price":10}]', + 35.00, 0.00, '个', 5.00, 2, 1, '单火/零火智能墙壁开关更换与双控/场景联动调试', 165, 95, 'normal', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), +(116, 18, '智能可视门铃/猫眼', '/icon/cat-smart.svg', 'fixed', + '[{"code":"install","name":"安装","price":60,"is_default":1,"support_fixed":1},{"code":"repair","name":"维修","price":50,"is_default":0,"support_fixed":1}]', + '[{"id":1,"code":"remove","name":"拆旧","unit":"份","price":15}]', + 60.00, 0.00, '套', 5.00, 2, 1, '入户门防盗智能猫眼安装固定与手机远程对讲联调', 130, 90, 'normal', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), + +-- [19] 地板/地毯 +(117, 19, '木地板铺装', '/icon/cat-floor.svg', 'fixed', + '[{"code":"install","name":"安装","price":18,"is_default":1,"support_fixed":1},{"code":"repair","name":"维修","price":120,"is_default":0,"support_fixed":1},{"code":"remove","name":"拆旧","price":8,"is_default":0,"support_fixed":1},{"code":"measure","name":"测量","price":0,"is_default":0,"support_fixed":0}]', + '[{"id":1,"code":"remove","name":"拆旧","unit":"份","price":50},{"id":2,"code":"cut","name":"切割","unit":"份","price":30}]', + 18.00, 0.00, '平', 10.00, 2, 0, '强化复合/实木多层木地板防潮膜铺设、锁扣拼装与收边条固定', 178, 100, 'normal', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), +(118, 19, '办公/家用铺地毯', '/icon/cat-floor.svg', 'fixed', + '[{"code":"install","name":"安装","price":12,"is_default":1,"support_fixed":1},{"code":"remove","name":"拆旧","price":6,"is_default":0,"support_fixed":1}]', + '[{"id":1,"code":"remove","name":"拆旧","unit":"份","price":40}]', + 12.00, 0.00, '平', 8.00, 2, 0, '方块拼装地毯、满铺卷材地毯裁切刷胶平整铺装', 124, 95, 'normal', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), +(119, 19, 'SPC石塑/PVC地板', '/icon/cat-floor.svg', 'fixed', + '[{"code":"install","name":"安装","price":16,"is_default":1,"support_fixed":1},{"code":"remove","name":"拆旧","price":8,"is_default":0,"support_fixed":1}]', + '[{"id":1,"code":"remove","name":"拆旧","unit":"份","price":45}]', + 16.00, 0.00, '平', 10.00, 2, 0, '卡扣式SPC石塑地板及商用耐磨PVC地板精细铺设', 105, 90, 'normal', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), + +-- [20] 户外遮阳 +(120, 20, '伸缩遮阳篷', '/icon/cat-awning.svg', 'fixed', + '[{"code":"install","name":"安装","price":180,"is_default":1,"support_fixed":1},{"code":"repair","name":"维修","price":120,"is_default":0,"support_fixed":1},{"code":"remove","name":"拆旧","price":60,"is_default":0,"support_fixed":1},{"code":"measure","name":"测量","price":0,"is_default":0,"support_fixed":0}]', + '[{"id":1,"code":"remove","name":"拆旧","unit":"份","price":60},{"id":2,"code":"drill","name":"打孔","unit":"份","price":40}]', + 180.00, 0.00, '套', 10.00, 2, 1, '商铺门头/阳台曲臂手动或电动遥控伸缩遮阳篷膨胀固定安装', 162, 100, 'normal', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), +(121, 20, '铝合金窗棚/雨棚', '/icon/cat-awning.svg', 'fixed', + '[{"code":"install","name":"安装","price":140,"is_default":1,"support_fixed":1},{"code":"remove","name":"拆旧","price":50,"is_default":0,"support_fixed":1}]', + '[{"id":1,"code":"remove","name":"拆旧","unit":"份","price":50},{"id":2,"code":"glue","name":"打胶","unit":"份","price":30}]', + 140.00, 0.00, '套', 10.00, 2, 1, '耐力板静音防雨窗棚组装固定与外沿防水密封打胶', 135, 95, 'normal', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), +(122, 20, '庭院遮阳伞/成品凉亭', '/icon/cat-awning.svg', 'fixed', + '[{"code":"install","name":"安装","price":160,"is_default":1,"support_fixed":1},{"code":"remove","name":"拆旧","price":60,"is_default":0,"support_fixed":1}]', + '[{"id":1,"code":"remove","name":"拆旧","unit":"份","price":60}]', + 160.00, 0.00, '套', 10.00, 2, 1, '大型罗马伞配重底座组装及成品铝合金百叶凉亭拼装固定', 98, 90, 'normal', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), + +-- [21] 广告展柜 +(123, 21, '发光字/超薄灯箱', '/icon/cat-signboard.svg', 'fixed', + '[{"code":"install","name":"安装","price":150,"is_default":1,"support_fixed":1},{"code":"repair","name":"维修","price":110,"is_default":0,"support_fixed":1},{"code":"remove","name":"拆旧","price":50,"is_default":0,"support_fixed":1}]', + '[{"id":1,"code":"remove","name":"拆旧","unit":"份","price":50},{"id":2,"code":"drill","name":"打孔","unit":"份","price":35}]', + 150.00, 0.00, '套', 8.00, 2, 1, '企业形象墙亚克力水晶字/发光字定位上墙与灯箱变压器接线', 168, 100, 'normal', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), +(124, 21, '商业展示柜/陈列柜', '/icon/cat-signboard.svg', 'fixed', + '[{"code":"install","name":"安装","price":160,"is_default":1,"support_fixed":1},{"code":"repair","name":"维修","price":90,"is_default":0,"support_fixed":1},{"code":"remove","name":"拆旧","price":60,"is_default":0,"support_fixed":1},{"code":"deliver_install","name":"送货到家并安装","price":0,"is_default":0,"support_fixed":0}]', + '[{"id":1,"code":"remove","name":"拆旧","unit":"份","price":60}]', + 160.00, 0.00, '组', 12.00, 2, 1, '商场专柜、珠宝化妆品烤漆玻璃展柜组装与LED灯带接通', 136, 95, 'normal', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), +(125, 21, '商超货架/仓储货架', '/icon/cat-signboard.svg', 'fixed', + '[{"code":"install","name":"安装","price":85,"is_default":1,"support_fixed":1},{"code":"remove","name":"拆旧","price":35,"is_default":0,"support_fixed":1}]', + '[{"id":1,"code":"remove","name":"拆旧","unit":"份","price":35}]', + 85.00, 0.00, '组', 10.00, 2, 1, '便利店单双面铁艺货架、仓库重型横梁层板货架卡扣拼装', 112, 90, 'normal', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), + +-- [22] 办公设备 +(126, 22, '保险柜/保管箱', '/icon/cat-office.svg', 'fixed', + '[{"code":"install","name":"安装","price":90,"is_default":1,"support_fixed":1},{"code":"repair","name":"维修","price":120,"is_default":0,"support_fixed":1},{"code":"deliver_install","name":"送货到家并安装","price":0,"is_default":0,"support_fixed":0}]', + '[{"id":1,"code":"drill","name":"打孔","unit":"份","price":35},{"id":2,"code":"open_hole","name":"开孔","unit":"份","price":45}]', + 90.00, 0.00, '台', 15.00, 2, 1, '家用入墙衣柜保险箱膨胀穿墙加固与大型商用密码柜安放调试', 158, 100, 'normal', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), +(127, 22, '办公卡位/会议桌', '/icon/cat-office.svg', 'fixed', + '[{"code":"install","name":"安装","price":140,"is_default":1,"support_fixed":1},{"code":"remove","name":"拆旧","price":50,"is_default":0,"support_fixed":1},{"code":"deliver_install","name":"送货到家并安装","price":0,"is_default":0,"support_fixed":0}]', + '[{"id":1,"code":"remove","name":"拆旧","unit":"份","price":50},{"id":2,"code":"open_hole","name":"开孔","unit":"份","price":30}]', + 140.00, 0.00, '套', 12.00, 2, 1, '多人位屏风办公桌隔断组合、大型会议桌拼装及线盒开孔', 146, 95, 'normal', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), +(128, 22, '会议平板/电子白板', '/icon/cat-office.svg', 'fixed', + '[{"code":"install","name":"安装","price":150,"is_default":1,"support_fixed":1},{"code":"remove","name":"拆旧","price":45,"is_default":0,"support_fixed":1}]', + '[{"id":1,"code":"remove","name":"拆旧","unit":"份","price":45},{"id":2,"code":"drill","name":"打孔","unit":"份","price":35}]', + 150.00, 0.00, '台', 10.00, 2, 1, '企业触控会议大屏重型挂架承重上墙或移动推车支架组装调试', 119, 90, 'normal', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), + +-- [23] 水电维修 +(129, 23, '电路开关插座检修', '/icon/cat-circuit.svg', 'fixed', + '[{"code":"repair","name":"维修","price":65,"is_default":1,"support_fixed":1},{"code":"install","name":"安装","price":55,"is_default":0,"support_fixed":1}]', + '[{"id":1,"code":"open_hole","name":"开孔","unit":"份","price":35}]', + 65.00, 0.00, '次', 0.00, 0, 0, '室内跳闸短路故障排查、空气开关/漏保及开关插座更换修复', 245, 100, 'normal', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), +(130, 23, '水管角阀漏水抢修', '/icon/cat-circuit.svg', 'fixed', + '[{"code":"repair","name":"维修","price":75,"is_default":1,"support_fixed":1},{"code":"install","name":"安装","price":60,"is_default":0,"support_fixed":1}]', + '[{"id":1,"code":"remove","name":"拆旧","unit":"份","price":25},{"id":2,"code":"drill","name":"打孔","unit":"份","price":35}]', + 75.00, 0.00, '处', 0.00, 0, 0, '三角阀渗漏更换、断丝取出修复及厨卫明装给排水管改造抢修', 198, 95, 'normal', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), + +-- [24] 家电清洗 +(131, 24, '油烟机/空调深度清洗', '/icon/cat-clean.svg', 'fixed', + '[{"code":"wash","name":"深度清洗","price":98,"is_default":1,"support_fixed":1},{"code":"maintain","name":"保养","price":0,"is_default":0,"support_fixed":0}]', + '[]', + 98.00, 0.00, '台', 0.00, 0, 0, '专业高温高压蒸汽除油垢、杀菌除异味深度清洗养护', 268, 100, 'normal', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), +(132, 24, '洗衣机/热水器清洗', '/icon/cat-clean.svg', 'fixed', + '[{"code":"wash","name":"深度清洗","price":88,"is_default":1,"support_fixed":1},{"code":"maintain","name":"保养","price":0,"is_default":0,"support_fixed":0}]', + '[]', + 88.00, 0.00, '台', 0.00, 0, 0, '波轮/滚筒洗衣机内筒除菌除垢及电热水器内胆排污深度清洗', 186, 95, 'normal', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), + +-- [25] 管道疏通 +(133, 25, '马桶/下水管道疏通', '/icon/cat-pipe.svg', 'fixed', + '[{"code":"dredge","name":"管道疏通","price":95,"is_default":1,"support_fixed":1},{"code":"repair","name":"维修","price":80,"is_default":0,"support_fixed":1}]', + '[{"id":1,"code":"remove","name":"拆旧","unit":"份","price":50}]', + 95.00, 0.00, '次', 0.00, 0, 0, '专业一炮通/电动弹簧疏通机快速解决马桶、地漏与厨房洗菜盆堵塞', 256, 100, 'normal', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), +(134, 25, '异物打捞/主管道疏通', '/icon/cat-pipe.svg', 'fixed', + '[{"code":"dredge","name":"管道疏通","price":150,"is_default":1,"support_fixed":1}]', + '[{"id":1,"code":"remove","name":"拆旧","unit":"份","price":60}]', + 150.00, 0.00, '次', 0.00, 0, 0, '马桶硬物掉落无损打捞及一楼独立排污总管机械疏通清淤', 148, 95, 'normal', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()) +ON DUPLICATE KEY UPDATE + `cat_id` = VALUES(`cat_id`), + `name` = VALUES(`name`), + `image` = VALUES(`image`), + `price_type` = VALUES(`price_type`), + `service_types` = VALUES(`service_types`), + `addons` = VALUES(`addons`), + `base_price` = VALUES(`base_price`), + `unit` = VALUES(`unit`), + `description` = VALUES(`description`), + `sales` = VALUES(`sales`), + `status` = 'normal', + `updatetime` = UNIX_TIMESTAMP(); + +-- 5. 为新增的 27 个商品配置真实细分规格(ID 285 ~ 344) +INSERT INTO `fa_service_spec` (`id`, `service_id`, `name`, `image`, `description`, `base_price`, `weigh`, `status`, `createtime`, `updatetime`) VALUES +-- 108 监控摄像头 +(285, 108, '室内家用云台摄像头', '/icon/cat-camera.svg', '吸顶或壁挂安装,含Wi-Fi配网调试', 65.00, 100, 'normal', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), +(286, 108, '室外防水枪机/球机', '/icon/cat-camera.svg', '外墙打孔支架固定与防水胶盒封装', 90.00, 90, 'normal', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), +(287, 108, '4G太阳能免布线监控', '/icon/cat-camera.svg', '含太阳能光伏板朝向固定与4G联网激活', 110.00, 80, 'normal', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), +-- 109 门禁考勤一体机 +(288, 109, '单门指纹/刷卡密码门禁', '/icon/cat-camera.svg', '含门禁电源、出门开关及磁力锁接线调试', 150.00, 100, 'normal', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), +(289, 109, '双开玻璃门人脸识别门禁', '/icon/cat-camera.svg', '含无框/有框玻璃门双磁力锁与人脸终端联调', 220.00, 90, 'normal', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), +-- 110 监控套装 +(290, 110, '2路摄像头+NVR硬盘录像机', '/icon/cat-camera.svg', '15米内明线布放、支架打孔及整机存储调试', 260.00, 100, 'normal', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), +(291, 110, '4路摄像头+NVR硬盘录像机', '/icon/cat-camera.svg', '商铺/居家四方位全彩监控布线及手机远程联调', 420.00, 90, 'normal', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), +-- 111 新能源家用充电桩 +(292, 111, '7kW单相壁挂充电桩(含10米内布线)', '/icon/cat-charger.svg', '壁挂背板固定、空气开关接线及实车刷卡试充', 280.00, 100, 'normal', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), +(293, 111, '7kW单相壁挂充电桩(30米内包线工费)', '/icon/cat-charger.svg', '车库桥架/PVC管走线安装(线材自备或实报)', 380.00, 90, 'normal', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), +(294, 111, '11kW/21kW三相大功率充电桩', '/icon/cat-charger.svg', '380V三相五线接入、漏保配置及大功率负载测试', 450.00, 80, 'normal', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), +-- 112 立柱式充电桩 +(295, 112, '充电桩+专用立柱落地安装', '/icon/cat-charger.svg', '水泥地面打膨胀螺丝固定立柱及桩体挂装', 350.00, 100, 'normal', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), +(296, 112, '充电桩+户外不锈钢防雨保护箱', '/icon/cat-charger.svg', '露天车位保护箱壁挂或立柱组合安装', 390.00, 90, 'normal', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), +-- 113 电瓶车扫码充电站 +(297, 113, '10路扫码智能充电桩主机+插座', '/icon/cat-charger.svg', '含线槽排布、10个防水插座固定及主机通电测试', 220.00, 100, 'normal', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), +(298, 113, '20路集中式电瓶车充电站', '/icon/cat-charger.svg', '含20个插座间距定位排线及漏电保护器安装', 380.00, 90, 'normal', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), +-- 114 全屋智能中控屏 +(299, 114, '4寸标准86暗盒智能中控屏', '/icon/cat-smart.svg', '原位替换传统开关底盒、零火接线与网关初始化', 95.00, 100, 'normal', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), +(300, 114, '7寸-12寸大屏壁挂智能中控主机', '/icon/cat-smart.svg', '墙面挂架水平固定、供电联网及子设备组网', 140.00, 90, 'normal', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), +-- 115 智能开关面板 +(301, 115, '智能墙壁开关面板(1键-4键)', '/icon/cat-smart.svg', '单火版或零火版智能开关安装与App配对', 35.00, 100, 'normal', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), +(302, 115, '智能无线场景旋钮/调光面板', '/icon/cat-smart.svg', '底盒取电接线及灯光回路通断测试', 45.00, 90, 'normal', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), +-- 116 智能可视门铃/猫眼 +(303, 116, '入户防盗门智能可视猫眼', '/icon/cat-smart.svg', '拆原传统光学猫眼、穿孔固定内外屏与联网调试', 60.00, 100, 'normal', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), +(304, 116, '室外防拆智能视频门铃+室内叮咚机', '/icon/cat-smart.svg', '门侧墙面打孔膨胀固定或强力背胶防拆安装', 65.00, 90, 'normal', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), +-- 117 木地板铺装 +(305, 117, '强化复合木地板悬浮平铺', '/icon/cat-floor.svg', '含防潮地垫铺设与地板锁扣拼装(按平米计价)', 18.00, 100, 'normal', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), +(306, 117, '实木多层复合地板铺装', '/icon/cat-floor.svg', '精准裁切预留伸缩缝及门口压条收口安装', 24.00, 90, 'normal', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), +(307, 117, '人字拼/鱼骨拼艺术地板铺装', '/icon/cat-floor.svg', '高难度斜角对花损耗控制与精细收边铺设', 38.00, 80, 'normal', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), +-- 118 办公/家用铺地毯 +(308, 118, '办公方块拼装地毯铺设', '/icon/cat-floor.svg', '地面清扫、定位弹线刷环保压敏胶与顺纹拼贴', 12.00, 100, 'normal', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), +(309, 118, '酒店/家用满铺卷材地毯', '/icon/cat-floor.svg', '含倒刺板固定、接缝烫带拼接与撑毯器拉平', 18.00, 90, 'normal', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), +-- 119 SPC石塑/PVC地板 +(310, 119, 'SPC锁扣石塑地板平铺', '/icon/cat-floor.svg', '免胶卡扣严密拼接,含边角异形裁切收口', 16.00, 100, 'normal', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), +(311, 119, 'PVC商用耐磨卷材塑胶地板', '/icon/cat-floor.svg', '铺刮胶水粘贴滚压排气与接缝收口处理', 20.00, 90, 'normal', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), +-- 120 伸缩遮阳篷 +(312, 120, '手动曲臂伸缩遮阳篷(长度3米及以内)', '/icon/cat-awning.svg', '墙体承重打孔加长膨胀螺栓固定及摇杆角度调节', 180.00, 100, 'normal', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), +(313, 120, '手动/电动伸缩遮阳篷(长度3.1米-5米)', '/icon/cat-awning.svg', '双人协同抬装、水平校准及电机遥控行程限位调试', 260.00, 90, 'normal', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), +-- 121 铝合金窗棚/雨棚 +(314, 121, '耐力板静音雨棚(单块宽度1.5米内)', '/icon/cat-awning.svg', '支架拼装上墙固定与靠墙缝耐候密封胶防水处理', 140.00, 100, 'normal', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), +(315, 121, '组合连体铝合金遮雨棚(宽度1.6米-3米)', '/icon/cat-awning.svg', '多支架承重连接组装及接缝防漏胶条压紧', 210.00, 90, 'normal', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), +-- 122 庭院遮阳伞/成品凉亭 +(316, 122, '大型户外侧立罗马遮阳伞', '/icon/cat-awning.svg', '注水/大理石配重底座安装、伞骨组装及旋转测试', 160.00, 100, 'normal', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), +(317, 122, '成品铝合金百叶凉亭(3x3米标准款)', '/icon/cat-awning.svg', '立柱横梁拼装、地面膨胀固定及顶部翻转百叶调试', 480.00, 90, 'normal', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), +-- 123 发光字/超薄灯箱 +(318, 123, '室内背景墙水晶字/亚克力发光字(2米内)', '/icon/cat-signboard.svg', '1:1安装图纸定位粘贴或铜柱支撑固定及低压电源隐藏接线', 150.00, 100, 'normal', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), +(319, 123, '超薄卡布灯箱/奶茶点餐灯箱(单块)', '/icon/cat-signboard.svg', '壁挂或吊挂水平固定、灯条通电测试及软膜画面平整卡入', 160.00, 90, 'normal', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), +-- 124 商业展示柜/陈列柜 +(320, 124, '成品单组精品玻璃展示柜(宽1.2米内)', '/icon/cat-signboard.svg', '板材框架拼装、玻璃层板卡托安装及射灯灯带通电', 160.00, 100, 'normal', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), +(321, 124, '多组连排高柜/中岛陈列柜(宽1.2-2.4米)', '/icon/cat-signboard.svg', '多柜体水平找平并柜锁紧、门铰调平及变压器联调', 260.00, 90, 'normal', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), +-- 125 商超货架/仓储货架 +(322, 125, '便利店单面/双面展示货架(单组)', '/icon/cat-signboard.svg', '立柱背网挂接、层板托臂卡扣组装及价格条安装', 85.00, 100, 'normal', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), +(323, 125, '仓库四层/五层重型蝴蝶孔仓储货架', '/icon/cat-signboard.svg', '立柱片斜撑螺栓紧固、横梁安全销锁定与层板铺放', 95.00, 90, 'normal', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), +-- 126 保险柜/保管箱 +(324, 126, '小型家用/酒店保管箱(高度45cm及以下)', '/icon/cat-office.svg', '衣柜或墙体背孔膨胀螺丝穿墙防搬移加固及密码设置指导', 90.00, 100, 'normal', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), +(325, 126, '中大型商用防盗保险柜(高度46cm-100cm)', '/icon/cat-office.svg', '安放就位、墙地面防撬锚固及指纹双控锁具通电测试', 140.00, 90, 'normal', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), +-- 127 办公卡位/会议桌 +(326, 127, '双人位/四人位屏风办公卡位组合', '/icon/cat-office.svg', '铝合金屏风框架连接、台面固定、走线槽与活动柜组装', 140.00, 100, 'normal', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), +(327, 127, '大型会议桌(长度2.4米-3.6米)', '/icon/cat-office.svg', '钢架或板式支撑脚拼装、分体台面拼接找平与多媒体线盒安装', 180.00, 90, 'normal', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), +-- 128 会议平板/电子白板 +(328, 128, '55寸-75寸会议平板壁挂安装', '/icon/cat-office.svg', '承重墙挂架水平打孔加固、大屏挂装及投屏触控测试', 150.00, 100, 'normal', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), +(329, 128, '86寸-98寸巨幕会议一体机(壁挂或落地推车)', '/icon/cat-office.svg', '重型移动推车组装或加厚挂架固定、OPS电脑模块插接调试', 240.00, 90, 'normal', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), +-- 129 电路开关插座检修 +(330, 129, '开关插座失灵/松动烧蚀更换检修(3个内)', '/icon/cat-circuit.svg', '断电验电安全作业、底盒接线端子紧固及相位通电检测', 65.00, 100, 'normal', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), +(331, 129, '室内电路跳闸/短路断路故障排查修复', '/icon/cat-circuit.svg', '分段摇表测绝缘排查漏电短路点、更换空气开关或漏电保护器', 98.00, 90, 'normal', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), +-- 130 水管角阀漏水抢修 +(332, 130, '角阀/软管/水嘴滴漏更换(不含断丝)', '/icon/cat-circuit.svg', '关闭总阀拆卸旧件、缠绕生料带安装新阀并保压试水无渗漏', 75.00, 100, 'normal', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), +(333, 130, '墙内角阀/龙头断丝取出及丝口修复', '/icon/cat-circuit.svg', '专用反牙取丝器无损取出墙内内丝弯头断茬,免砸砖恢复供水', 110.00, 90, 'normal', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), +(334, 130, '厨卫明装PPR热熔管/PVC排水管局部改管修复', '/icon/cat-circuit.svg', '爆管漏水裁切、专业热熔器无缝熔接或排水三通胶接修复', 120.00, 80, 'normal', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), +-- 131 油烟机/空调深度清洗 +(335, 131, '油烟机高温蒸汽深度除油清洗(中式/侧吸/欧式)', '/icon/cat-clean.svg', '涡轮风轮油网精细化除重油垢、高温蒸汽杀菌及机身抛光清洁', 118.00, 100, 'normal', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), +(336, 131, '壁挂式空调内机深度蒸汽杀菌洗', '/icon/cat-clean.svg', '全程防水罩防护、过滤网风轮蒸发器翅片高温除霉杀菌去异味', 88.00, 90, 'normal', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), +(337, 131, '立柜式空调内机深度蒸汽杀菌洗', '/icon/cat-clean.svg', '立式柜机风道、贯流风扇及蒸发器全方位高压蒸汽清洁消杀', 118.00, 80, 'normal', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), +-- 132 洗衣机/热水器清洗 +(338, 132, '波轮/滚筒洗衣机免拆内筒除垢杀菌洗', '/icon/cat-clean.svg', '活氧除垢剂浸泡分解夹层污垢+高温高压蒸汽冲刷胶圈与排污口', 88.00, 100, 'normal', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), +(339, 132, '储水式电热水器内胆排污除水垢深度洗', '/icon/cat-clean.svg', '安全断电泄压、排空内胆泥沙水垢、镁棒损耗检查及注水排气通电', 98.00, 90, 'normal', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), +-- 133 马桶/下水管道疏通 +(340, 133, '坐便器/蹲便器软物堵塞快速疏通', '/icon/cat-pipe.svg', '气压一炮通/软轴疏通机无损马桶釉面快速恢复畅通排水', 95.00, 100, 'normal', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), +(341, 133, '厨房洗菜盆下水管重油垢凝结疏通', '/icon/cat-pipe.svg', '大功率电动弹簧疏通机打碎管壁皂化油块并大量冲水测试流速', 105.00, 90, 'normal', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), +(342, 133, '卫生间淋浴地漏/洗手盆毛发堵塞疏通', '/icon/cat-pipe.svg', '深入存水弯勾取缠绕毛发杂物并清理地漏芯恢复顺畅下水', 95.00, 80, 'normal', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), +-- 134 异物打捞/主管道疏通 +(343, 134, '马桶硬物掉落(刷子/玩具/首饰)可视打捞', '/icon/cat-pipe.svg', '专业爪钳或拆卸马桶底部法兰取出卡阻硬物并重装密封防臭法兰圈', 150.00, 100, 'normal', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), +(344, 134, '一楼独立排污主管道/商铺总管机械疏通', '/icon/cat-pipe.svg', '加粗锰钢弹簧深入室外检查井主管道强力绞通淤积堵塞物', 180.00, 90, 'normal', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()) +ON DUPLICATE KEY UPDATE + `service_id` = VALUES(`service_id`), + `name` = VALUES(`name`), + `image` = VALUES(`image`), + `description` = VALUES(`description`), + `base_price` = VALUES(`base_price`), + `weigh` = VALUES(`weigh`), + `status` = 'normal', + `updatetime` = UNIX_TIMESTAMP(); diff --git a/db/schema_phase19_wxpay_transfer.sql b/db/schema_phase19_wxpay_transfer.sql new file mode 100644 index 0000000..a475cfb --- /dev/null +++ b/db/schema_phase19_wxpay_transfer.sql @@ -0,0 +1,44 @@ +-- ============================================================================== +-- Phase 19: 微信支付 APIv3 全场景订单支付/退款单表 & 师傅提现新版商家转账(1005)升级 +-- ============================================================================== + +-- 1. 微信支付交易与退款流水表(支撑订单首付 order、选师傅报价支付 quote、增项补差 diff 及多笔原路退款) +CREATE TABLE IF NOT EXISTS `fa_pay_order` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `out_trade_no` varchar(32) NOT NULL DEFAULT '' COMMENT '商户支付单号', + `order_id` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '关联业务订单ID', + `order_no` varchar(32) NOT NULL DEFAULT '' COMMENT '业务订单号', + `batch_no` varchar(40) NOT NULL DEFAULT '' COMMENT '商家批次号(scene=batch)', + `user_id` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '付款用户ID', + `scene` enum('order','quote','diff','batch') NOT NULL DEFAULT 'order' COMMENT '支付场景:order=订单支付,quote=报价支付,diff=增项补差,batch=批次支付', + `quote_id` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '关联报价单ID(scene=quote)', + `openid` varchar(64) NOT NULL DEFAULT '' COMMENT '付款人微信OpenID', + `amount` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '应付金额(元)', + `status` enum('pending','paid','refunded','closed') NOT NULL DEFAULT 'pending' COMMENT '状态:pending=待支付,paid=已支付,refunded=已退款,closed=已关闭', + `prepay_id` varchar(128) NOT NULL DEFAULT '' COMMENT '微信预支付会话标识', + `transaction_id` varchar(64) NOT NULL DEFAULT '' COMMENT '微信支付交易单号', + `refund_no` varchar(40) NOT NULL DEFAULT '' COMMENT '商户退款单号', + `refund_id` varchar(64) NOT NULL DEFAULT '' COMMENT '微信支付退款单号', + `refund_amount` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '已退金额(元)', + `pay_time` bigint(16) DEFAULT NULL COMMENT '支付完成时间', + `refund_time` bigint(16) DEFAULT NULL COMMENT '退款完成时间', + `createtime` bigint(16) DEFAULT NULL COMMENT '创建时间', + `updatetime` bigint(16) DEFAULT NULL COMMENT '更新时间', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_out_trade_no` (`out_trade_no`), + KEY `idx_order_scene` (`order_id`, `scene`, `status`), + KEY `idx_user_id` (`user_id`), + KEY `idx_transaction_id` (`transaction_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='微信支付与退款交易流水表'; + +-- 2. 师傅提现申请表扩展新版微信商家转账(用户确认模式 1005 佣金报酬)核心字段 +ALTER TABLE `fa_withdraw` + ADD COLUMN `openid` varchar(64) NOT NULL DEFAULT '' COMMENT '收款人微信OpenID' AFTER `account`, + ADD COLUMN `out_bill_no` varchar(32) NOT NULL DEFAULT '' COMMENT '商户转账单号' AFTER `audit_msg`, + ADD COLUMN `transfer_bill_no` varchar(64) NOT NULL DEFAULT '' COMMENT '微信转账单号' AFTER `out_bill_no`, + ADD COLUMN `transfer_state` varchar(32) NOT NULL DEFAULT '' COMMENT '微信转账状态' AFTER `transfer_bill_no`, + ADD COLUMN `package_info` varchar(2048) NOT NULL DEFAULT '' COMMENT '小程序确认收款package凭证' AFTER `transfer_state`, + ADD COLUMN `fail_reason` varchar(255) NOT NULL DEFAULT '' COMMENT '转账失败或撤销原因' AFTER `package_info`, + ADD COLUMN `transfer_time` bigint(16) DEFAULT NULL COMMENT '发起转账时间' AFTER `fail_reason`, + ADD COLUMN `confirm_time` bigint(16) DEFAULT NULL COMMENT '用户确认收款时间' AFTER `transfer_time`, + ADD KEY `idx_out_bill_no` (`out_bill_no`); diff --git a/db/schema_phase20_finance_config.sql b/db/schema_phase20_finance_config.sql new file mode 100644 index 0000000..684f3ab --- /dev/null +++ b/db/schema_phase20_finance_config.sql @@ -0,0 +1,194 @@ +-- ============================================================================== +-- Phase 20: 独立「抽佣与提现配置」中心、按订单多维抽佣规则表、订单/提现完整规则快照与微信小程序商家转账规范升级 +-- ============================================================================== +SET NAMES utf8mb4; + +-- 1. 财务结算与提现全局规则配置表 +CREATE TABLE IF NOT EXISTS `fa_finance_config` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `name` varchar(64) NOT NULL DEFAULT '' COMMENT '配置键名', + `group` varchar(32) NOT NULL DEFAULT 'commission' COMMENT '分组:commission=订单抽佣兜底,withdraw=提现与微信转账', + `title` varchar(100) NOT NULL DEFAULT '' COMMENT '配置标题', + `value` text COMMENT '配置值', + `remark` varchar(255) NOT NULL DEFAULT '' COMMENT '规则说明', + `updatetime` bigint(16) DEFAULT NULL COMMENT '更新时间', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_name` (`name`), + KEY `idx_group` (`group`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='财务抽佣与提现规则配置表'; + +-- 幂等初始化默认配置项 +INSERT INTO `fa_finance_config` (`name`, `group`, `title`, `value`, `remark`, `updatetime`) VALUES +('default_calc_mode', 'commission', '默认抽佣计费方式', 'percent', 'percent=按订单比例(%), fixed=按单固定金额(元), mixed=比例+固定金额', UNIX_TIMESTAMP()), +('default_commission_rate', 'commission', '默认抽佣比例(%)', '10.00', '未命中特定订单抽佣规则时的兜底抽佣百分比(0~100)', UNIX_TIMESTAMP()), +('default_fixed_fee', 'commission', '默认单笔固定抽佣(元)', '0.00', 'fixed或mixed模式下每笔订单固定抽取的技术服务费', UNIX_TIMESTAMP()), +('default_min_commission', 'commission', '默认单笔保底抽佣(元)', '0.00', '0表示不设保底;大于0时单笔抽佣不低于该金额(不超过订单实付额)', UNIX_TIMESTAMP()), +('default_max_commission', 'commission', '默认单笔封顶抽佣(元)', '0.00', '0表示不设封顶;大于0时单笔抽佣最高不超过该金额', UNIX_TIMESTAMP()), +('include_diff_in_commission', 'commission', '施工增项补差价计入抽佣', '1', '1=增项补差并入订单总额按快照规则抽佣, 0=增项补差免抽佣全额归师傅', UNIX_TIMESTAMP()), + +('withdraw_enabled', 'withdraw', '提现功能总开关', '1', '1=正常开放提现, 0=暂停提现维护', UNIX_TIMESTAMP()), +('withdraw_close_tip', 'withdraw', '暂停提现提示语', '系统正在进行财务结算维护,请稍后再发起提现', '关闭提现开关时向师傅端展示的提示文案', UNIX_TIMESTAMP()), +('withdraw_channels', 'withdraw', '开放提现渠道', 'wechat', 'wechat=微信零钱(官方商家转账), bank=银行储蓄卡;小程序合规建议默认仅开启wechat', UNIX_TIMESTAMP()), +('withdraw_require_realname', 'withdraw', '强制实名认证方可提现', '1', '1=必须通过师傅实名认证, 0=不强制(注:微信单笔>=2000元强制要求实名)', UNIX_TIMESTAMP()), +('withdraw_min_amount', 'withdraw', '单笔最低提现金额(元)', '10.00', '微信新版商家转账要求扣除手续费后实际到账净额不低于0.10元', UNIX_TIMESTAMP()), +('withdraw_max_amount', 'withdraw', '单笔最高提现金额(元)', '200.00', '须与微信商户后台商家转账单笔限额一致(微信官方硬上限20000元)', UNIX_TIMESTAMP()), +('withdraw_daily_amount', 'withdraw', '单人单日累计提现上限(元)', '2000.00', '0表示不限;须不超过微信商户单用户日收款限额', UNIX_TIMESTAMP()), +('withdraw_daily_count', 'withdraw', '单人单日最多提现次数', '3', '0表示不限制每日提现申请笔数', UNIX_TIMESTAMP()), +('withdraw_allow_weekdays', 'withdraw', '允许提现周期(星期)', '1,2,3,4,5,6,7', '1~7代表周一至周日,逗号分隔', UNIX_TIMESTAMP()), +('withdraw_time_start', 'withdraw', '每日允许提现开始时间', '00:00', '格式 HH:mm', UNIX_TIMESTAMP()), +('withdraw_time_end', 'withdraw', '每日允许提现结束时间', '23:59', '格式 HH:mm', UNIX_TIMESTAMP()), +('withdraw_fee_mode', 'withdraw', '提现手续费计收方式', 'percent', 'percent=按比例收取, fixed=按笔固定收取, free=免收手续费', UNIX_TIMESTAMP()), +('withdraw_fee_rate', 'withdraw', '提现通道手续费率(%)', '0.60', 'percent模式下按提现申请金额计算的百分比费率', UNIX_TIMESTAMP()), +('withdraw_fee_fixed', 'withdraw', '提现单笔固定手续费(元)', '0.00', 'fixed模式下每笔提现固定扣除的手续费', UNIX_TIMESTAMP()), +('withdraw_fee_min', 'withdraw', '单笔最低保底手续费(元)', '0.00', 'percent模式下单笔手续费下限,0表示不保底', UNIX_TIMESTAMP()), +('withdraw_fee_max', 'withdraw', '单笔最高封顶手续费(元)', '0.00', 'percent模式下单笔手续费上限,0表示不封顶', UNIX_TIMESTAMP()), +('withdraw_transfer_mode', 'withdraw', '微信渠道打款执行方式', 'wx_mch_transfer', 'wx_mch_transfer=微信APIv3新版商家转账(小程序确认收款), offline_manual=线下打款人工标记', UNIX_TIMESTAMP()), +('withdraw_audit_mode', 'withdraw', '提现审核与出款模式', 'manual', 'manual=全部经财务人工审核后打款, auto_small=小额免审自动发起微信商家转账', UNIX_TIMESTAMP()), +('withdraw_auto_max_amount', 'withdraw', '小额免审自动转账上限(元)', '50.00', 'auto_small模式下单笔不超过该金额且为微信渠道时自动发起转账', UNIX_TIMESTAMP()), +('wx_transfer_scene_id', 'withdraw', '微信商家转账场景ID', '1005', '微信商户平台已开通的转账场景ID,1005代表佣金报酬', UNIX_TIMESTAMP()), +('wx_recv_perception', 'withdraw', '微信收款用户零钱感知', '劳务报酬', '须与微信商户后台场景1005报备感知文案一致(如:劳务报酬)', UNIX_TIMESTAMP()), +('wx_job_type', 'withdraw', '微信场景报备-岗位类型', '上门安装维修师傅', '微信V3接口必填报备项,不超过32个字符', UNIX_TIMESTAMP()), +('wx_reward_desc', 'withdraw', '微信场景报备-报酬说明', '平台工单安装维修劳务报酬', '微信V3接口必填报备项,不超过32个字符', UNIX_TIMESTAMP()), +('wx_transfer_remark', 'withdraw', '微信零钱转账账单备注', '工单安装维修服务劳务报酬', '收款人微信支付账单可见备注,不超过32个字符', UNIX_TIMESTAMP()) +ON DUPLICATE KEY UPDATE `title` = VALUES(`title`), `remark` = VALUES(`remark`); + +-- 2. 按订单多维度匹配的动态抽佣规则表 +CREATE TABLE IF NOT EXISTS `fa_commission_rule` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `name` varchar(100) NOT NULL DEFAULT '' COMMENT '规则名称', + `order_mode` enum('all','fixed','quote','custom','range') NOT NULL DEFAULT 'all' COMMENT '适用订单模式:all=全部,fixed=一口价,quote=师傅报价,custom=用户定价,range=区间计价', + `customer_type` enum('all','personal','merchant') NOT NULL DEFAULT 'all' COMMENT '适用客群:all=全部,personal=个人单,merchant=企业商家单', + `cat_id` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '适用服务大类ID(0=全部类目)', + `urgent_mode` enum('all','normal','urgent') NOT NULL DEFAULT 'all' COMMENT '急单条件:all=不限,normal=普通单,urgent=加急单', + `min_amount` decimal(10,2) unsigned NOT NULL DEFAULT '0.00' COMMENT '适用订单最低金额(0=不限下限)', + `max_amount` decimal(10,2) unsigned NOT NULL DEFAULT '0.00' COMMENT '适用订单最高金额(0=不限上限)', + `calc_mode` enum('percent','fixed','mixed') NOT NULL DEFAULT 'percent' COMMENT '计佣方式:percent=按比例,fixed=固定金额,mixed=比例+固定金额', + `commission_rate` decimal(5,2) unsigned NOT NULL DEFAULT '10.00' COMMENT '抽佣比例(%)', + `fixed_fee` decimal(10,2) unsigned NOT NULL DEFAULT '0.00' COMMENT '单笔固定抽佣额(元)', + `min_commission` decimal(10,2) unsigned NOT NULL DEFAULT '0.00' COMMENT '单笔最低保底抽佣(0=不保底)', + `max_commission` decimal(10,2) unsigned NOT NULL DEFAULT '0.00' COMMENT '单笔最高封顶抽佣(0=不封顶)', + `weigh` int(11) NOT NULL DEFAULT '10' COMMENT '匹配优先级权重(越大越优先)', + `status` enum('normal','hidden') NOT NULL DEFAULT 'normal' COMMENT '状态:normal=启用,hidden=停用', + `remark` varchar(255) NOT NULL DEFAULT '' COMMENT '规则说明', + `createtime` bigint(16) DEFAULT NULL COMMENT '创建时间', + `updatetime` bigint(16) DEFAULT NULL COMMENT '更新时间', + PRIMARY KEY (`id`), + KEY `idx_status_weigh` (`status`, `weigh`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='按订单维度动态抽佣规则表'; + +-- 幂等兼容早期列名 price_type -> order_mode / urgent_type -> urgent_mode +SET @col_cr_price_type := ( + SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'fa_commission_rule' AND COLUMN_NAME = 'price_type' +); +SET @sql_cr_price_type := IF( + @col_cr_price_type > 0, + 'ALTER TABLE `fa_commission_rule` CHANGE COLUMN `price_type` `order_mode` enum(''all'',''fixed'',''quote'',''custom'',''range'') NOT NULL DEFAULT ''all'' COMMENT ''适用订单模式:all=全部,fixed=一口价,quote=师傅报价,custom=用户定价,range=区间计价''', + 'SELECT 1' +); +PREPARE stmt_cr_pt FROM @sql_cr_price_type; +EXECUTE stmt_cr_pt; +DEALLOCATE PREPARE stmt_cr_pt; + +SET @col_cr_urgent_type := ( + SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'fa_commission_rule' AND COLUMN_NAME = 'urgent_type' +); +SET @sql_cr_urgent_type := IF( + @col_cr_urgent_type > 0, + 'ALTER TABLE `fa_commission_rule` CHANGE COLUMN `urgent_type` `urgent_mode` enum(''all'',''normal'',''urgent'') NOT NULL DEFAULT ''all'' COMMENT ''急单条件:all=不限,normal=普通单,urgent=加急单''', + 'SELECT 1' +); +PREPARE stmt_cr_ut FROM @sql_cr_urgent_type; +EXECUTE stmt_cr_ut; +DEALLOCATE PREPARE stmt_cr_ut; + +-- 预置基础订单模式抽佣规则(仅在规则表为空时初始化) +INSERT INTO `fa_commission_rule` (`name`, `order_mode`, `customer_type`, `cat_id`, `urgent_mode`, `min_amount`, `max_amount`, `calc_mode`, `commission_rate`, `fixed_fee`, `min_commission`, `max_commission`, `weigh`, `status`, `remark`, `createtime`, `updatetime`) +SELECT '一口价标准单', 'fixed', 'all', 0, 'all', 0.00, 0.00, 'percent', 10.00, 0.00, 0.00, 0.00, 30, 'normal', '平台一口价订单默认抽佣', UNIX_TIMESTAMP(), UNIX_TIMESTAMP() +FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `fa_commission_rule` LIMIT 1); + +INSERT INTO `fa_commission_rule` (`name`, `order_mode`, `customer_type`, `cat_id`, `urgent_mode`, `min_amount`, `max_amount`, `calc_mode`, `commission_rate`, `fixed_fee`, `min_commission`, `max_commission`, `weigh`, `status`, `remark`, `createtime`, `updatetime`) +SELECT '师傅报价单', 'quote', 'all', 0, 'all', 0.00, 0.00, 'percent', 10.00, 0.00, 0.00, 0.00, 20, 'normal', '师傅报价中标订单默认抽佣', UNIX_TIMESTAMP(), UNIX_TIMESTAMP() +FROM DUAL WHERE (SELECT COUNT(*) FROM `fa_commission_rule`) = 1; + +INSERT INTO `fa_commission_rule` (`name`, `order_mode`, `customer_type`, `cat_id`, `urgent_mode`, `min_amount`, `max_amount`, `calc_mode`, `commission_rate`, `fixed_fee`, `min_commission`, `max_commission`, `weigh`, `status`, `remark`, `createtime`, `updatetime`) +SELECT '业主自主出价单', 'custom', 'all', 0, 'all', 0.00, 0.00, 'percent', 10.00, 0.00, 0.00, 0.00, 10, 'normal', '业主自主定价订单默认抽佣', UNIX_TIMESTAMP(), UNIX_TIMESTAMP() +FROM DUAL WHERE (SELECT COUNT(*) FROM `fa_commission_rule`) = 2; + +-- 3. 订单表补充完整抽佣规则快照 JSON 列(与 commission_rate 配合实现完整阶梯/封顶/补差隔离) +SET @col_order_snap_json := ( + SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'fa_order' AND COLUMN_NAME = 'commission_snapshot' +); +SET @sql_order_snap_json := IF( + @col_order_snap_json = 0, + 'ALTER TABLE `fa_order` ADD COLUMN `commission_snapshot` text DEFAULT NULL COMMENT ''创单抽佣规则完整快照JSON'' AFTER `commission_rate`', + 'SELECT 1' +); +PREPARE stmt_osj FROM @sql_order_snap_json; +EXECUTE stmt_osj; +DEALLOCATE PREPARE stmt_osj; + +-- 4. 提现申请表补充提现费率规则快照 JSON 列 +SET @col_wd_snap_json := ( + SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'fa_withdraw' AND COLUMN_NAME = 'rule_snapshot' +); +SET @sql_wd_snap_json := IF( + @col_wd_snap_json = 0, + 'ALTER TABLE `fa_withdraw` ADD COLUMN `rule_snapshot` text DEFAULT NULL COMMENT ''提现发起时手续费与风控规则快照JSON'' AFTER `arrive_amount`', + 'SELECT 1' +); +PREPARE stmt_wsj FROM @sql_wd_snap_json; +EXECUTE stmt_wsj; +DEALLOCATE PREPARE stmt_wsj; + +-- 5. 在后台「财务资金 (finance_manage)」一级目录下注册独立的「财务配置 (finance_config)」菜单及权限子节点 +SET @finance_pid := (SELECT `id` FROM `fa_auth_rule` WHERE `name` = 'finance_manage' LIMIT 1); + +INSERT INTO `fa_auth_rule` (`type`, `pid`, `name`, `title`, `icon`, `url`, `condition`, `remark`, `ismenu`, `menutype`, `extend`, `py`, `pinyin`, `createtime`, `updatetime`, `weigh`, `status`) +SELECT 'file', @finance_pid, 'finance_config', '财务配置', 'fa fa-sliders', '', '', '配置订单抽佣规则与师傅提现转账规则', 1, NULL, '', 'cwpz', 'caiwupeizhi', UNIX_TIMESTAMP(), UNIX_TIMESTAMP(), 156, 'normal' +FROM DUAL +WHERE @finance_pid IS NOT NULL AND NOT EXISTS (SELECT 1 FROM `fa_auth_rule` WHERE `name` = 'finance_config'); + +UPDATE `fa_auth_rule` SET `title` = '财务配置', `py` = 'cwpz', `pinyin` = 'caiwupeizhi', `remark` = '配置订单抽佣规则与师傅提现转账规则' WHERE `name` = 'finance_config'; + +SET @fc_id := (SELECT `id` FROM `fa_auth_rule` WHERE `name` = 'finance_config' LIMIT 1); + +INSERT INTO `fa_auth_rule` (`type`, `pid`, `name`, `title`, `icon`, `url`, `condition`, `remark`, `ismenu`, `createtime`, `updatetime`, `weigh`, `status`) +SELECT 'file', @fc_id, 'finance_config/index', '查看', 'fa fa-circle-o', '', '', '', 0, UNIX_TIMESTAMP(), UNIX_TIMESTAMP(), 0, 'normal' +FROM DUAL WHERE @fc_id IS NOT NULL AND NOT EXISTS (SELECT 1 FROM `fa_auth_rule` WHERE `name` = 'finance_config/index'); + +INSERT INTO `fa_auth_rule` (`type`, `pid`, `name`, `title`, `icon`, `url`, `condition`, `remark`, `ismenu`, `createtime`, `updatetime`, `weigh`, `status`) +SELECT 'file', @fc_id, 'finance_config/save', '保存配置', 'fa fa-circle-o', '', '', '', 0, UNIX_TIMESTAMP(), UNIX_TIMESTAMP(), 0, 'normal' +FROM DUAL WHERE @fc_id IS NOT NULL AND NOT EXISTS (SELECT 1 FROM `fa_auth_rule` WHERE `name` = 'finance_config/save'); + +INSERT INTO `fa_auth_rule` (`type`, `pid`, `name`, `title`, `icon`, `url`, `condition`, `remark`, `ismenu`, `createtime`, `updatetime`, `weigh`, `status`) +SELECT 'file', @fc_id, 'finance_config/rule_add', '添加规则', 'fa fa-circle-o', '', '', '', 0, UNIX_TIMESTAMP(), UNIX_TIMESTAMP(), 0, 'normal' +FROM DUAL WHERE @fc_id IS NOT NULL AND NOT EXISTS (SELECT 1 FROM `fa_auth_rule` WHERE `name` = 'finance_config/rule_add'); + +INSERT INTO `fa_auth_rule` (`type`, `pid`, `name`, `title`, `icon`, `url`, `condition`, `remark`, `ismenu`, `createtime`, `updatetime`, `weigh`, `status`) +SELECT 'file', @fc_id, 'finance_config/rule_edit', '编辑规则', 'fa fa-circle-o', '', '', '', 0, UNIX_TIMESTAMP(), UNIX_TIMESTAMP(), 0, 'normal' +FROM DUAL WHERE @fc_id IS NOT NULL AND NOT EXISTS (SELECT 1 FROM `fa_auth_rule` WHERE `name` = 'finance_config/rule_edit'); + +INSERT INTO `fa_auth_rule` (`type`, `pid`, `name`, `title`, `icon`, `url`, `condition`, `remark`, `ismenu`, `createtime`, `updatetime`, `weigh`, `status`) +SELECT 'file', @fc_id, 'finance_config/rule_del', '删除规则', 'fa fa-circle-o', '', '', '', 0, UNIX_TIMESTAMP(), UNIX_TIMESTAMP(), 0, 'normal' +FROM DUAL WHERE @fc_id IS NOT NULL AND NOT EXISTS (SELECT 1 FROM `fa_auth_rule` WHERE `name` = 'finance_config/rule_del'); + +INSERT INTO `fa_auth_rule` (`type`, `pid`, `name`, `title`, `icon`, `url`, `condition`, `remark`, `ismenu`, `createtime`, `updatetime`, `weigh`, `status`) +SELECT 'file', @fc_id, 'finance_config/rule_multi', '批量更新', 'fa fa-circle-o', '', '', '', 0, UNIX_TIMESTAMP(), UNIX_TIMESTAMP(), 0, 'normal' +FROM DUAL WHERE @fc_id IS NOT NULL AND NOT EXISTS (SELECT 1 FROM `fa_auth_rule` WHERE `name` = 'finance_config/rule_multi'); + +UPDATE `fa_auth_rule` SET `title` = '查看' WHERE `name` = 'finance_config/index'; +UPDATE `fa_auth_rule` SET `title` = '保存配置' WHERE `name` = 'finance_config/save'; +UPDATE `fa_auth_rule` SET `title` = '添加规则' WHERE `name` = 'finance_config/rule_add'; +UPDATE `fa_auth_rule` SET `title` = '编辑规则' WHERE `name` = 'finance_config/rule_edit'; +UPDATE `fa_auth_rule` SET `title` = '删除规则' WHERE `name` = 'finance_config/rule_del'; +UPDATE `fa_auth_rule` SET `title` = '批量更新' WHERE `name` = 'finance_config/rule_multi'; + +-- 6. 收敛清理旧分散配置项(将系统配置分组恢复纯粹,移除结算单下的临时弹窗权限节点,更新静态资源版本号防缓存) +DELETE FROM `fa_config` WHERE `name` = 'commission_rate'; +UPDATE `fa_config` SET `value` = '{"basic":"基础配置","user":"会员配置","email":"邮箱配置","dictionary":"字典配置"}' WHERE `name` = 'configgroup'; +DELETE FROM `fa_auth_rule` WHERE `name` = 'settle/commission_config'; +UPDATE `fa_config` SET `value` = '1.0.58' WHERE `name` = 'version'; diff --git a/db/schema_phase21_agreement.sql b/db/schema_phase21_agreement.sql new file mode 100644 index 0000000..83512b4 --- /dev/null +++ b/db/schema_phase21_agreement.sql @@ -0,0 +1,108 @@ +-- ============================================================================== +-- Phase 21: 平台协议与隐私政策独立管理表 (fa_agreement) 与后台「协议与隐私」菜单注册 +-- ============================================================================== +SET NAMES utf8mb4; + +-- 1. 平台协议与隐私政策表 +CREATE TABLE IF NOT EXISTS `fa_agreement` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `code` varchar(32) NOT NULL DEFAULT '' COMMENT '协议唯一编码:user_service=用户服务协议,privacy=隐私保护指引,master=师傅入驻协议,merchant=商家协同协议', + `title` varchar(100) NOT NULL DEFAULT '' COMMENT '协议标题', + `version` varchar(32) NOT NULL DEFAULT 'v1.0' COMMENT '版本号', + `effective_date` varchar(20) NOT NULL DEFAULT '' COMMENT '生效/更新日期(YYYY-MM-DD)', + `content` mediumtext COMMENT '协议正文(支持HTML富文本)', + `status` enum('normal','hidden') NOT NULL DEFAULT 'normal' COMMENT '状态:normal=启用,hidden=停用', + `weigh` int(11) NOT NULL DEFAULT '10' COMMENT '排序权重(越大越靠前)', + `createtime` bigint(16) DEFAULT NULL COMMENT '创建时间', + `updatetime` bigint(16) DEFAULT NULL COMMENT '更新时间', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_code` (`code`), + KEY `idx_status_weigh` (`status`, `weigh`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='平台协议与隐私政策配置表'; + +-- 2. 幂等预置平台核心协议内容(仅当对应编码不存在时写入,避免覆盖管理员后续在后台的修改) +INSERT INTO `fa_agreement` (`code`, `title`, `version`, `effective_date`, `content`, `status`, `weigh`, `createtime`, `updatetime`) +SELECT + 'user_service', + '优匠优服用户服务协议', + 'v1.0', + CURDATE(), + '

一、平台定位与服务范围

1. 优匠优服(以下简称“本平台”)依法为个人业主、企业商家与认证技能师傅提供上门安装、维修、拆旧及维保信息撮合与交易资金托管服务。

2. 您在注册、登录或下单使用本平台服务前,应当仔细阅读并充分理解本协议全部条款。

二、下单与计费准则

1. 一口价项目按平台页面公示标准计费;师傅报价订单由接单师傅根据现场工况公开报价,经客户确认并支付后生效;自主定价订单按客户确认的订单金额执行。

2. 如施工现场存在特殊工况需增项补差,须经双方提前沟通一致,并由师傅在工单内发起规范补差申请,客户核实无误后通过平台支付。

三、验收、质保与资金结算

1. 订单支付资金由平台合规托管,师傅完成施工并上传现场完工照片后,经客户验收确认(或向师傅提供 6 位核销码完成核验)后触发结算。

2. 为保障您的售后权益、施工质保与资金安全,请全程在平台内完成沟通、下单、补差与支付,切勿脱离平台私下转账交易。

四、用户行为规范

1. 您应确保所填报的服务地址、联系方式、商品型号及故障描述真实准确,以便师傅准时高效上门履约。

2. 如有服务争议或售后问题,可通过订单详情页发起申诉或联系平台客服介入仲裁处理。

', + 'normal', + 100, + UNIX_TIMESTAMP(), + UNIX_TIMESTAMP() +FROM DUAL +WHERE NOT EXISTS (SELECT 1 FROM `fa_agreement` WHERE `code` = 'user_service'); + +INSERT INTO `fa_agreement` (`code`, `title`, `version`, `effective_date`, `content`, `status`, `weigh`, `createtime`, `updatetime`) +SELECT + 'privacy', + '优匠优服隐私保护指引', + 'v1.0', + CURDATE(), + '

一、信息收集范围与使用目的

1. 本平台遵循“合法、正当、必要、诚信”原则收集和使用您的个人信息,仅在为您提供同城上门安装、维修服务所必需的场景下收集相关数据。

2. 账号与联系信息:在您授权登录或下单时,我们需要收集您的手机号码及联系人称呼,用于账号识别、订单通知及师傅上门沟通。

3. 服务地址与位置信息:在您创建工单或管理地址簿时,我们需要收集上门服务地址及定位信息,以便为您匹配就近认证师傅并核验服务范围。

4. 订单与影像资料:您上传的商品照片、现场环境图片或视频仅用于师傅精准评估报价、施工准备与售后凭证留存。

二、最小必要与脱敏保护原则

1. 您的真实手机号码、门牌号等敏感联系信息,仅对已正式接单履约的当单认证师傅可见;订单未被接单前及非当单人员均无法获取,平台全程采用脱敏展示。

2. 师傅端在联系客户时受工单履约状态严格权限约束,履约完结后相关敏感联系方式将自动收回或脱敏保护。

三、数据安全与第三方共享限制

1. 本平台全链路采用加密传输与存储技术,严格管控内部数据访问权限,防止数据泄露、损毁或丢失。

2. 除完成支付结算、实名核验或法律法规明确要求外,本平台绝不向无关第三方共享、转让或出售您的个人信息。

四、您的个人权利

您可随时在小程序「个人中心」查询、修改个人资料、管理或删除历史服务地址,或联系平台客服咨询、注销账号及撤回授权。

', + 'normal', + 90, + UNIX_TIMESTAMP(), + UNIX_TIMESTAMP() +FROM DUAL +WHERE NOT EXISTS (SELECT 1 FROM `fa_agreement` WHERE `code` = 'privacy'); + +INSERT INTO `fa_agreement` (`code`, `title`, `version`, `effective_date`, `content`, `status`, `weigh`, `createtime`, `updatetime`) +SELECT + 'master', + '师傅入驻与履约服务协议', + 'v1.0', + CURDATE(), + '

一、实名认证与技能核验

1. 申请入驻的师傅须为具备完全民事行为能力的合法从业人员,提交本人真实有效的二代身份证件及真实服务技能信息,严禁冒用、借用或伪造他人资质。

2. 平台有权通过权威实名核验通道核查身份一致性,未通过实名认证前不可接单及提现。

二、接单与上门履约规范

1. 接单后须在平台规定时效内主动联系客户确认具体上门时段,按约准时到达现场,规范着装并文明施工。

2. 施工过程中应做好现场保护与安全防护,按行业标准交付施工质量。

三、收费透明与严禁私单

1. 严格按照订单约定金额服务;如需现场增项补差,必须提前向客户说明理由与明细,并在工单内发起补差申请经客户确认支付后实施,严禁现场私自索要额外费用或诱导脱离平台私下交易。

四、完工留痕与结算提现

1. 施工完成后须在订单页上传真实清晰的现场完工照片,凭客户确认验收或 6 位核销码触发系统自动结算。

2. 订单抽佣规则以创单时刻锁定的快照为准,结算金额进入师傅可提现钱包余额,按平台提现规则申请打款。

', + 'normal', + 80, + UNIX_TIMESTAMP(), + UNIX_TIMESTAMP() +FROM DUAL +WHERE NOT EXISTS (SELECT 1 FROM `fa_agreement` WHERE `code` = 'master'); + +INSERT INTO `fa_agreement` (`code`, `title`, `version`, `effective_date`, `content`, `status`, `weigh`, `createtime`, `updatetime`) +SELECT + 'merchant', + '企业商家入驻与协同服务协议', + 'v1.0', + CURDATE(), + '

一、企业主体与资质真实性

1. 申请使用企业商家服务的单位或商户须保证所登记的企业名称、联系人、营业执照或主体信息真实、合法、有效。

二、账号协同与权限管理

1. 企业主账号可按需绑定内部协同子账号,共同发起个人单或批量安装维修工单并跟进履约进度。

2. 企业主账号应对其绑定子账号在本平台发起的工单下单、验收及结算行为承担相应管理责任。

三、对账结算与发票服务

1. 平台按实际支付与已完工订单汇总企业消费记录,商家可在小程序「发票服务」中维护常用发票抬头,并对符合开票条件的订单在线申请正规增值税普通发票或专用发票。

', + 'normal', + 70, + UNIX_TIMESTAMP(), + UNIX_TIMESTAMP() +FROM DUAL +WHERE NOT EXISTS (SELECT 1 FROM `fa_agreement` WHERE `code` = 'merchant'); + +-- 3. 在后台「常规管理 (general)」目录下注册「协议与隐私 (agreement)」菜单及权限子节点 +SET @general_pid := (SELECT `id` FROM `fa_auth_rule` WHERE `name` = 'general' LIMIT 1); + +INSERT INTO `fa_auth_rule` (`type`, `pid`, `name`, `title`, `icon`, `url`, `condition`, `remark`, `ismenu`, `menutype`, `extend`, `py`, `pinyin`, `createtime`, `updatetime`, `weigh`, `status`) +SELECT 'file', @general_pid, 'agreement', '协议与隐私', 'fa fa-file-text-o', '', '', '编辑管理用户服务协议、隐私保护指引与入驻协议', 1, NULL, '', 'xyyys', 'xieyiyuyinsi', UNIX_TIMESTAMP(), UNIX_TIMESTAMP(), 74, 'normal' +FROM DUAL +WHERE @general_pid IS NOT NULL AND NOT EXISTS (SELECT 1 FROM `fa_auth_rule` WHERE `name` = 'agreement'); + +UPDATE `fa_auth_rule` +SET `pid` = COALESCE(@general_pid, `pid`), + `title` = '协议与隐私', + `icon` = 'fa fa-file-text-o', + `ismenu` = 1, + `weigh` = 74, + `status` = 'normal', + `py` = 'xyyys', + `pinyin` = 'xieyiyuyinsi', + `remark` = '编辑管理用户服务协议、隐私保护指引与入驻协议' +WHERE `name` = 'agreement'; + +SET @ag_id := (SELECT `id` FROM `fa_auth_rule` WHERE `name` = 'agreement' LIMIT 1); + +INSERT INTO `fa_auth_rule` (`type`, `pid`, `name`, `title`, `icon`, `url`, `condition`, `remark`, `ismenu`, `createtime`, `updatetime`, `weigh`, `status`) +SELECT 'file', @ag_id, 'agreement/index', '查看', 'fa fa-circle-o', '', '', '', 0, UNIX_TIMESTAMP(), UNIX_TIMESTAMP(), 0, 'normal' +FROM DUAL WHERE @ag_id IS NOT NULL AND NOT EXISTS (SELECT 1 FROM `fa_auth_rule` WHERE `name` = 'agreement/index'); + +INSERT INTO `fa_auth_rule` (`type`, `pid`, `name`, `title`, `icon`, `url`, `condition`, `remark`, `ismenu`, `createtime`, `updatetime`, `weigh`, `status`) +SELECT 'file', @ag_id, 'agreement/save', '编辑保存', 'fa fa-circle-o', '', '', '', 0, UNIX_TIMESTAMP(), UNIX_TIMESTAMP(), 0, 'normal' +FROM DUAL WHERE @ag_id IS NOT NULL AND NOT EXISTS (SELECT 1 FROM `fa_auth_rule` WHERE `name` = 'agreement/save'); diff --git a/db/schema_phase22_wxpay_audit_fix.sql b/db/schema_phase22_wxpay_audit_fix.sql new file mode 100644 index 0000000..21bfb19 --- /dev/null +++ b/db/schema_phase22_wxpay_audit_fix.sql @@ -0,0 +1,20 @@ +-- ============================================================================== +-- Phase 22: 微信支付与商家转账异常溯源、状态精细化及中文原因持久化增强 +-- ============================================================================== + +-- 1. fa_pay_order 扩展:精细区分待支付 / 已支付 / 下单或支付失败 / 退款处理中 / 已退款 / 退款异常 / 已关闭,并持久记录失败码与中文原因 +ALTER TABLE `fa_pay_order` + MODIFY COLUMN `status` enum('pending','paid','pay_failed','refunding','refunded','refund_failed','closed') NOT NULL DEFAULT 'pending' COMMENT '状态:pending=待支付,paid=已支付,pay_failed=下单/支付失败,refunding=退款处理中,refunded=已退款,refund_failed=退款异常,closed=已关闭', + ADD COLUMN `refund_status` enum('none','processing','success','abnormal','closed') NOT NULL DEFAULT 'none' COMMENT '微信退款状态:none=未退款,processing=退款处理中,success=退款成功,abnormal=退款异常,closed=退款关闭' AFTER `refund_amount`, + ADD COLUMN `fail_code` varchar(64) NOT NULL DEFAULT '' COMMENT '最近失败或异常错误码' AFTER `refund_status`, + ADD COLUMN `fail_reason` varchar(500) NOT NULL DEFAULT '' COMMENT '最近失败或异常中文原因(供管理员排查)' AFTER `fail_code`, + ADD COLUMN `last_sync_time` bigint(16) DEFAULT NULL COMMENT '最近主动向微信查单同步时间' AFTER `refund_time`, + ADD KEY `idx_status_refund` (`status`, `refund_status`); + +-- 2. fa_withdraw 扩展:保留历史商户单号追溯,记录失败错误码与完整中文诊断原因,支持安全重试计数 +ALTER TABLE `fa_withdraw` + MODIFY COLUMN `audit_msg` varchar(500) NOT NULL DEFAULT '' COMMENT '审核备注', + MODIFY COLUMN `fail_reason` varchar(500) NOT NULL DEFAULT '' COMMENT '转账失败或撤销中文原因', + ADD COLUMN `fail_code` varchar(64) NOT NULL DEFAULT '' COMMENT '转账失败或网关异常错误码' AFTER `transfer_state`, + ADD COLUMN `retry_count` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '微信转账发起重试次数' AFTER `fail_reason`, + ADD COLUMN `last_sync_time` bigint(16) DEFAULT NULL COMMENT '最近向微信查单同步时间' AFTER `confirm_time`; diff --git a/db/schema_phase23_agreement_diff.sql b/db/schema_phase23_agreement_diff.sql new file mode 100644 index 0000000..26a9aa9 --- /dev/null +++ b/db/schema_phase23_agreement_diff.sql @@ -0,0 +1,59 @@ +-- ============================================================================== +-- Phase 23: 协议变更追溯与用户端首看差异高亮记录 (fa_agreement 扩展 + fa_agreement_read) +-- ============================================================================== +SET NAMES utf8mb4; + +-- 1. 幂等扩展 fa_agreement 表字段(兼容 MySQL 5.7) +DROP PROCEDURE IF EXISTS `sp_phase23_agreement_diff_migrate`; +DELIMITER $$ +CREATE PROCEDURE `sp_phase23_agreement_diff_migrate`() +BEGIN + IF NOT EXISTS ( + SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'fa_agreement' AND COLUMN_NAME = 'revision' + ) THEN + ALTER TABLE `fa_agreement` + ADD COLUMN `revision` int(10) unsigned NOT NULL DEFAULT '1' COMMENT '正文实质变更修订序号(每次修改自动+1)' AFTER `content`; + END IF; + + IF NOT EXISTS ( + SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'fa_agreement' AND COLUMN_NAME = 'prev_content' + ) THEN + ALTER TABLE `fa_agreement` + ADD COLUMN `prev_content` mediumtext COMMENT '上一次变更前的协议正文(用于生成差异对比)' AFTER `revision`; + END IF; + + IF NOT EXISTS ( + SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'fa_agreement' AND COLUMN_NAME = 'diff_content' + ) THEN + ALTER TABLE `fa_agreement` + ADD COLUMN `diff_content` mediumtext COMMENT '带背景色变更高亮的HTML正文(由prev_content与content对比生成)' AFTER `prev_content`; + END IF; + + IF NOT EXISTS ( + SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'fa_agreement' AND COLUMN_NAME = 'diff_summary' + ) THEN + ALTER TABLE `fa_agreement` + ADD COLUMN `diff_summary` varchar(255) NOT NULL DEFAULT '' COMMENT '本次变更简要说明(如新增/调整处数)' AFTER `diff_content`; + END IF; +END$$ +DELIMITER ; +CALL `sp_phase23_agreement_diff_migrate`(); +DROP PROCEDURE IF EXISTS `sp_phase23_agreement_diff_migrate`; + +-- 2. 新增用户协议版本已读确认表,确保每位登录用户仅在版本变更后首次打开时展示变更标识 +CREATE TABLE IF NOT EXISTS `fa_agreement_read` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `user_id` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '用户ID', + `agreement_code` varchar(32) NOT NULL DEFAULT '' COMMENT '协议编码', + `read_revision` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '用户已查看的最高修订序号', + `read_time` bigint(16) NOT NULL DEFAULT '0' COMMENT '首次查看当前修订版时间', + `createtime` bigint(16) DEFAULT NULL COMMENT '创建时间', + `updatetime` bigint(16) DEFAULT NULL COMMENT '更新时间', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_user_code` (`user_id`, `agreement_code`), + KEY `idx_code_rev` (`agreement_code`, `read_revision`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户协议变更首看已读记录表'; diff --git a/db/schema_phase24_master_cert.sql b/db/schema_phase24_master_cert.sql new file mode 100644 index 0000000..a07c5e5 --- /dev/null +++ b/db/schema_phase24_master_cert.sql @@ -0,0 +1,127 @@ +-- ============================================================================ +-- 优匠优服 · Phase 24 师傅技能证书上传与审核体系 +-- 1. 创建技能证书类型字典表 fa_cert_dict(覆盖特种作业、驾驶、护理等全品类) +-- 2. 创建师傅技能证书记录表 fa_master_cert +-- 3. 注册后台管理菜单与权限规则(师傅管理 -> 技能证书) +-- ============================================================================ +SET NAMES utf8mb4; + +-- 1. 技能证书字典表 +CREATE TABLE IF NOT EXISTS `fa_cert_dict` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT 'ID', + `cert_code` varchar(50) NOT NULL DEFAULT '' COMMENT '证书编码', + `cert_name` varchar(80) NOT NULL DEFAULT '' COMMENT '证书名称', + `sample_type` varchar(30) NOT NULL DEFAULT 'special_card' COMMENT '示例图类型:special_card=特种作业卡/资格证,driver=驾驶证,health=健康证,qualification=行业备案/资质证', + `need_expire` tinyint(1) unsigned NOT NULL DEFAULT '1' COMMENT '是否需要有效期:1=是,0=否', + `weigh` int(11) NOT NULL DEFAULT '0' COMMENT '排序权重(越大越靠前)', + `status` enum('normal','hidden') NOT NULL DEFAULT 'normal' COMMENT '状态', + `createtime` bigint(16) DEFAULT NULL COMMENT '创建时间', + `updatetime` bigint(16) DEFAULT NULL COMMENT '更新时间', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE KEY `uniq_cert_code` (`cert_code`) USING BTREE, + KEY `idx_status_weigh` (`status`, `weigh`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC COMMENT='技能证书类型字典表'; + +-- 2. 师傅技能证书上传与审核表 +CREATE TABLE IF NOT EXISTS `fa_master_cert` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT 'ID', + `master_id` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '师傅ID(fa_master.id)', + `cert_dict_id` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '证书字典ID(fa_cert_dict.id)', + `cert_code` varchar(50) NOT NULL DEFAULT '' COMMENT '证书编码', + `cert_name` varchar(80) NOT NULL DEFAULT '' COMMENT '证书名称(标准名称或自定义名称)', + `is_custom` tinyint(1) unsigned NOT NULL DEFAULT '0' COMMENT '是否自定义(其他证书):0=标准字典,1=自定义证书', + `cert_image` varchar(255) NOT NULL DEFAULT '' COMMENT '证书照片URL', + `expire_date` date DEFAULT NULL COMMENT '证件有效期截止日期', + `is_long_term` tinyint(1) unsigned NOT NULL DEFAULT '0' COMMENT '是否长期有效:0=否,1=是', + `status` enum('pending','pass','reject') NOT NULL DEFAULT 'pending' COMMENT '审核状态:pending=审核中,pass=已通过,reject=已驳回', + `audit_msg` varchar(255) NOT NULL DEFAULT '' COMMENT '审核意见/驳回原因', + `audit_admin_id` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '审核管理员ID', + `audittime` bigint(16) DEFAULT NULL COMMENT '审核时间', + `createtime` bigint(16) DEFAULT NULL COMMENT '创建时间', + `updatetime` bigint(16) DEFAULT NULL COMMENT '更新时间', + PRIMARY KEY (`id`) USING BTREE, + KEY `idx_master_status` (`master_id`, `status`) USING BTREE, + KEY `idx_master_code` (`master_id`, `cert_code`) USING BTREE, + KEY `idx_status_time` (`status`, `updatetime`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC COMMENT='师傅技能证书表'; + +-- 3. 幂等初始化 27 项标准证书 + 1 项其他证书 +INSERT INTO `fa_cert_dict` (`cert_code`, `cert_name`, `sample_type`, `need_expire`, `weigh`, `status`, `createtime`, `updatetime`) VALUES +('electrician', '电工证', 'special_card', 1, 280, 'normal', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), +('driver_license', '驾驶证', 'driver', 1, 270, 'normal', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), +('high_altitude', '高空作业证', 'special_card', 1, 260, 'normal', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), +('special_operation', '特种作业操作证', 'special_card', 1, 250, 'normal', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), +('welder', '焊工证', 'special_card', 1, 240, 'normal', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), +('weak_current', '弱电证', 'special_card', 1, 230, 'normal', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), +('health_cert', '健康证', 'health', 1, 220, 'normal', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), +('refrigeration_ac', '制冷与空调作业证', 'special_card', 1, 210, 'normal', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), +('locksmith_record', '开锁备案', 'qualification', 1, 200, 'normal', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), +('gas_appliance', '燃气器具安装维修资质', 'qualification', 1, 190, 'normal', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), +('forklift', '叉车证', 'special_card', 1, 180, 'normal', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), +('special_equip_safety', '特种设备安全管理证', 'special_card', 1, 170, 'normal', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), +('crane_operation', '起重机作业证', 'special_card', 1, 160, 'normal', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), +('elevator_operation', '电梯作业证', 'special_card', 1, 150, 'normal', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), +('elderly_care', '养老护理员证', 'qualification', 1, 140, 'normal', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), +('hazardous_chemical', '危险化学品安全作业证', 'special_card', 1, 130, 'normal', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), +('boiler_operation', '锅炉作业证', 'special_card', 1, 120, 'normal', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), +('coal_mine_safety', '煤矿安全作业操作证', 'special_card', 1, 110, 'normal', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), +('pressure_vessel', '压力容器作业证', 'special_card', 1, 100, 'normal', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), +('plant_vehicle', '场(厂)内专用机动车辆作业证', 'special_card', 1, 90, 'normal', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), +('oil_gas_safety', '石油天然气安全作业证', 'special_card', 1, 80, 'normal', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), +('maternal_care', '母婴护理师证', 'qualification', 1, 70, 'normal', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), +('fireworks_safety', '烟花爆竹安全作业证', 'special_card', 1, 60, 'normal', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), +('metallurgy_safety', '冶金(有色)生产安全作业证', 'special_card', 1, 50, 'normal', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), +('metal_mine_safety', '金属非金属矿山安全作业证', 'special_card', 1, 40, 'normal', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), +('nurse_license', '护士证', 'qualification', 1, 30, 'normal', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), +('gas_cylinder', '气瓶作业证', 'special_card', 1, 20, 'normal', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), +('other', '其他证书', 'qualification', 1, 1, 'normal', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()) +ON DUPLICATE KEY UPDATE + `cert_name` = VALUES(`cert_name`), + `sample_type` = VALUES(`sample_type`), + `need_expire` = VALUES(`need_expire`), + `weigh` = VALUES(`weigh`), + `status` = VALUES(`status`), + `updatetime` = VALUES(`updatetime`); + +-- 4. 注册后台【技能证书审核 (master_cert)】菜单与权限规则 +INSERT INTO `fa_auth_rule` (`type`, `pid`, `name`, `title`, `icon`, `condition`, `remark`, `ismenu`, `createtime`, `updatetime`, `weigh`, `status`) +SELECT 'file', p.id, 'master_cert', '技能证书', 'fa fa-id-badge', '', '师傅技能证书上传审核管理', 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP(), 174, 'normal' +FROM `fa_auth_rule` p +WHERE p.name = 'master_manage' + AND NOT EXISTS (SELECT `id` FROM `fa_auth_rule` WHERE `name` = 'master_cert'); + +INSERT INTO `fa_auth_rule` (`type`, `pid`, `name`, `title`, `icon`, `condition`, `remark`, `ismenu`, `createtime`, `updatetime`, `weigh`, `status`) +SELECT 'file', p.id, 'master_cert/index', '查看', 'fa fa-circle-o', '', '', 0, UNIX_TIMESTAMP(), UNIX_TIMESTAMP(), 0, 'normal' +FROM `fa_auth_rule` p +WHERE p.name = 'master_cert' + AND NOT EXISTS (SELECT `id` FROM `fa_auth_rule` WHERE `name` = 'master_cert/index'); + +INSERT INTO `fa_auth_rule` (`type`, `pid`, `name`, `title`, `icon`, `condition`, `remark`, `ismenu`, `createtime`, `updatetime`, `weigh`, `status`) +SELECT 'file', p.id, 'master_cert/detail', '详情', 'fa fa-circle-o', '', '', 0, UNIX_TIMESTAMP(), UNIX_TIMESTAMP(), 0, 'normal' +FROM `fa_auth_rule` p +WHERE p.name = 'master_cert' + AND NOT EXISTS (SELECT `id` FROM `fa_auth_rule` WHERE `name` = 'master_cert/detail'); + +INSERT INTO `fa_auth_rule` (`type`, `pid`, `name`, `title`, `icon`, `condition`, `remark`, `ismenu`, `createtime`, `updatetime`, `weigh`, `status`) +SELECT 'file', p.id, 'master_cert/audit', '审核', 'fa fa-circle-o', '', '', 0, UNIX_TIMESTAMP(), UNIX_TIMESTAMP(), 0, 'normal' +FROM `fa_auth_rule` p +WHERE p.name = 'master_cert' + AND NOT EXISTS (SELECT `id` FROM `fa_auth_rule` WHERE `name` = 'master_cert/audit'); + +INSERT INTO `fa_auth_rule` (`type`, `pid`, `name`, `title`, `icon`, `condition`, `remark`, `ismenu`, `createtime`, `updatetime`, `weigh`, `status`) +SELECT 'file', p.id, 'master_cert/edit', '编辑', 'fa fa-circle-o', '', '', 0, UNIX_TIMESTAMP(), UNIX_TIMESTAMP(), 0, 'normal' +FROM `fa_auth_rule` p +WHERE p.name = 'master_cert' + AND NOT EXISTS (SELECT `id` FROM `fa_auth_rule` WHERE `name` = 'master_cert/edit'); + +INSERT INTO `fa_auth_rule` (`type`, `pid`, `name`, `title`, `icon`, `condition`, `remark`, `ismenu`, `createtime`, `updatetime`, `weigh`, `status`) +SELECT 'file', p.id, 'master_cert/del', '删除', 'fa fa-circle-o', '', '', 0, UNIX_TIMESTAMP(), UNIX_TIMESTAMP(), 0, 'normal' +FROM `fa_auth_rule` p +WHERE p.name = 'master_cert' + AND NOT EXISTS (SELECT `id` FROM `fa_auth_rule` WHERE `name` = 'master_cert/del'); + +INSERT INTO `fa_auth_rule` (`type`, `pid`, `name`, `title`, `icon`, `condition`, `remark`, `ismenu`, `createtime`, `updatetime`, `weigh`, `status`) +SELECT 'file', p.id, 'master_cert/multi', '批量更新', 'fa fa-circle-o', '', '', 0, UNIX_TIMESTAMP(), UNIX_TIMESTAMP(), 0, 'normal' +FROM `fa_auth_rule` p +WHERE p.name = 'master_cert' + AND NOT EXISTS (SELECT `id` FROM `fa_auth_rule` WHERE `name` = 'master_cert/multi'); diff --git a/db/schema_phase25_real_data_cleanup.sql b/db/schema_phase25_real_data_cleanup.sql new file mode 100644 index 0000000..d6af817 --- /dev/null +++ b/db/schema_phase25_real_data_cleanup.sql @@ -0,0 +1,62 @@ +-- ============================================================================ +-- 优匠优服 · Phase 25 清除 Mock/静态兜底与真实字典配置表初始化 +-- 1. 新建物流公司与单号特征字典表 fa_express_company +-- 2. 在 fa_config 中初始化可配置字典(评价标签、订单来源、首页价格筛选档位、竞价有效时长) +-- ============================================================================ +SET NAMES utf8mb4; + +CREATE TABLE IF NOT EXISTS `fa_express_company` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT 'ID', + `code` varchar(50) NOT NULL DEFAULT '' COMMENT '标准物流编码(兼容快递100)', + `name` varchar(60) NOT NULL DEFAULT '' COMMENT '物流公司名称', + `initial` char(1) NOT NULL DEFAULT 'Q' COMMENT '拼音首字母分组(A-Z)', + `prefix_regex` varchar(255) NOT NULL DEFAULT '' COMMENT '高确定性单号识别正则(为空仅依赖线上查询或手选)', + `weigh` int(11) NOT NULL DEFAULT '0' COMMENT '排序权重(越大越靠前)', + `status` enum('normal','hidden') NOT NULL DEFAULT 'normal' COMMENT '状态', + `createtime` bigint(16) DEFAULT NULL COMMENT '创建时间', + `updatetime` bigint(16) DEFAULT NULL COMMENT '更新时间', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE KEY `uniq_code` (`code`) USING BTREE, + KEY `idx_status_initial` (`status`, `initial`, `weigh`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC COMMENT='物流公司字典与特征规则表'; + +INSERT INTO `fa_express_company` (`code`, `name`, `initial`, `prefix_regex`, `weigh`, `status`, `createtime`, `updatetime`) VALUES +('annengwuliu', '安能物流', 'A', '/^ANE/', 900, 'normal', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), +('baishiwuliu', '百世快递', 'B', '/^(BS|HT|55\\d{11,13}$)/', 890, 'normal', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), +('huitongkuaidi', '百世快递', 'B', '/^(BS|HT)/', 885, 'hidden', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), +('debangwuliu', '德邦快递', 'D', '/^(DP|DB)/', 880, 'normal', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), +('debangkuaidi', '德邦快递', 'D', '/^(DP|DB)/', 875, 'hidden', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), +('jd', '京东快递', 'J', '/^(JD|KK)/', 870, 'normal', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), +('jtexpress', '极兔速递', 'J', '/^JT/', 865, 'normal', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), +('kuayue', '跨越速运', 'K', '/^KY/', 860, 'normal', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), +('shunfeng', '顺丰速运', 'S', '/^SF/', 950, 'normal', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), +('shentong', '申通快递', 'S', '/^(ST|77)/', 850, 'normal', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), +('tiantian', '天天快递', 'T', '/^(TT|66\\d{10,12}$)/', 840, 'normal', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), +('yunda', '韵达', 'Y', '/^(YD|(31|43|46)\\d{11,13}$)/', 830, 'normal', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), +('yuantong', '圆通速递', 'Y', '/^YT/', 825, 'normal', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), +('ems', '邮政特快专递', 'Y', '/^(EMS|EA|EB|EC|ED|EE|EF|10|11)\\d{7,11}/', 820, 'normal', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), +('yimidida', '壹米滴答', 'Y', '/^YM/', 815, 'normal', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), +('youzhengguonei', '中国邮政', 'Z', '/^(98|99|YZ)\\d{9,13}$/', 810, 'normal', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), +('youzhengbk', '中国邮政', 'Z', '/^(98|99|YZ)\\d{9,13}$/', 805, 'hidden', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), +('zhaijisong', '宅急送', 'Z', '/^ZJS/', 800, 'normal', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), +('zhongtong', '中通快递', 'Z', '/^(ZT|(73|75|76|78)\\d{10,12}$)/', 795, 'normal', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), +('other', '其它', 'Q', '', 1, 'normal', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()) +ON DUPLICATE KEY UPDATE + `name` = VALUES(`name`), + `initial` = VALUES(`initial`), + `prefix_regex` = VALUES(`prefix_regex`), + `weigh` = VALUES(`weigh`), + `status` = VALUES(`status`), + `updatetime` = VALUES(`updatetime`); + +INSERT INTO `fa_config` (`name`, `group`, `title`, `tip`, `type`, `visible`, `value`, `content`, `rule`, `extend`) +VALUES + ('review_tags', 'basic', '服务评价预设标签', '用户评价页面展示的快捷服务标签,逗号分隔', 'string', '', '准时,专业,态度好,收费透明,干净利落,值得推荐', '', '', ''), + ('order_source_options', 'basic', '订单来源可选渠道', '下单页面订单来源选项,逗号分隔', 'string', '', '淘宝/天猫,京东,拼多多,抖音电商,线下门店,其他渠道', '', '', ''), + ('home_price_ranges', 'basic', '首页服务价格筛选档位', '首页筛选抽屉使用的价格区间JSON配置', 'text', '', '[{"label":"80元以下","value":"under_80"},{"label":"80-120元","value":"80_120"},{"label":"120元以上","value":"120_up"}]', '', '', ''), + ('auto_quote_hours_options', 'basic', '竞价等待时长可选档位(小时)', '下单页选择超时自动转师傅竞价的时间档位,逗号分隔', 'string', '', '6,12,24,48', '', '', ''), + ('tianditu_key', 'basic', '天地图服务端密钥', '服务端正/逆地理编码专用 Key(严禁填入浏览器端 Key)', 'string', '', '55aee666be14cd0100874d6faf3c963c', '', '', '') +ON DUPLICATE KEY UPDATE + `value` = IF(`value` IS NULL OR `value` = '' OR `value` = '369ea1edc677b9e3746093f8d9dc4465', VALUES(`value`), `value`), + `title` = VALUES(`title`), + `tip` = VALUES(`tip`); diff --git a/db/seed_phase25_area.sql b/db/seed_phase25_area.sql new file mode 100644 index 0000000..c9758e5 --- /dev/null +++ b/db/seed_phase25_area.sql @@ -0,0 +1,3319 @@ +SET NAMES utf8mb4; +INSERT IGNORE INTO `fa_area` (`id`,`pid`,`shortname`,`name`,`mergename`,`level`,`pinyin`,`code`,`zip`,`first`) VALUES +(1,0,'北京','北京市','北京市',1,'','','',''), +(2,1,'北京','北京市','北京市,北京市',2,'','','',''), +(3,2,'东城','东城区','北京市,北京市,东城区',3,'','','',''), +(4,2,'西城','西城区','北京市,北京市,西城区',3,'','','',''), +(5,2,'朝阳','朝阳区','北京市,北京市,朝阳区',3,'','','',''), +(6,2,'丰台','丰台区','北京市,北京市,丰台区',3,'','','',''), +(7,2,'石景山','石景山区','北京市,北京市,石景山区',3,'','','',''), +(8,2,'海淀','海淀区','北京市,北京市,海淀区',3,'','','',''), +(9,2,'门头沟','门头沟区','北京市,北京市,门头沟区',3,'','','',''), +(10,2,'房山','房山区','北京市,北京市,房山区',3,'','','',''), +(11,2,'通州','通州区','北京市,北京市,通州区',3,'','','',''), +(12,2,'顺义','顺义区','北京市,北京市,顺义区',3,'','','',''), +(13,2,'昌平','昌平区','北京市,北京市,昌平区',3,'','','',''), +(14,2,'大兴','大兴区','北京市,北京市,大兴区',3,'','','',''), +(15,2,'怀柔','怀柔区','北京市,北京市,怀柔区',3,'','','',''), +(16,2,'平谷','平谷区','北京市,北京市,平谷区',3,'','','',''), +(17,2,'密云','密云县','北京市,北京市,密云县',3,'','','',''), +(18,2,'延庆','延庆县','北京市,北京市,延庆县',3,'','','',''), +(19,0,'天津','天津市','天津市',1,'','','',''), +(20,19,'天津','天津市','天津市,天津市',2,'','','',''), +(21,20,'和平','和平区','天津市,天津市,和平区',3,'','','',''), +(22,20,'河东','河东区','天津市,天津市,河东区',3,'','','',''), +(23,20,'河西','河西区','天津市,天津市,河西区',3,'','','',''), +(24,20,'南开','南开区','天津市,天津市,南开区',3,'','','',''), +(25,20,'河北','河北区','天津市,天津市,河北区',3,'','','',''), +(26,20,'红桥','红桥区','天津市,天津市,红桥区',3,'','','',''), +(27,20,'东丽','东丽区','天津市,天津市,东丽区',3,'','','',''), +(28,20,'西青','西青区','天津市,天津市,西青区',3,'','','',''), +(29,20,'津南','津南区','天津市,天津市,津南区',3,'','','',''), +(30,20,'北辰','北辰区','天津市,天津市,北辰区',3,'','','',''), +(31,20,'武清','武清区','天津市,天津市,武清区',3,'','','',''), +(32,20,'宝坻','宝坻区','天津市,天津市,宝坻区',3,'','','',''), +(33,20,'滨海新','滨海新区','天津市,天津市,滨海新区',3,'','','',''), +(34,20,'宁河','宁河县','天津市,天津市,宁河县',3,'','','',''), +(35,20,'静海','静海县','天津市,天津市,静海县',3,'','','',''), +(36,20,'蓟','蓟县','天津市,天津市,蓟县',3,'','','',''), +(37,0,'河北','河北省','河北省',1,'','','',''), +(38,37,'石家庄','石家庄市','河北省,石家庄市',2,'','','',''), +(39,38,'长安','长安区','河北省,石家庄市,长安区',3,'','','',''), +(40,38,'桥东','桥东区','河北省,石家庄市,桥东区',3,'','','',''), +(41,38,'桥西','桥西区','河北省,石家庄市,桥西区',3,'','','',''), +(42,38,'新华','新华区','河北省,石家庄市,新华区',3,'','','',''), +(43,38,'井陉矿','井陉矿区','河北省,石家庄市,井陉矿区',3,'','','',''), +(44,38,'裕华','裕华区','河北省,石家庄市,裕华区',3,'','','',''), +(45,38,'井陉','井陉县','河北省,石家庄市,井陉县',3,'','','',''), +(46,38,'正定','正定县','河北省,石家庄市,正定县',3,'','','',''), +(47,38,'栾城','栾城县','河北省,石家庄市,栾城县',3,'','','',''), +(48,38,'行唐','行唐县','河北省,石家庄市,行唐县',3,'','','',''), +(49,38,'灵寿','灵寿县','河北省,石家庄市,灵寿县',3,'','','',''), +(50,38,'高邑','高邑县','河北省,石家庄市,高邑县',3,'','','',''), +(51,38,'深泽','深泽县','河北省,石家庄市,深泽县',3,'','','',''), +(52,38,'赞皇','赞皇县','河北省,石家庄市,赞皇县',3,'','','',''), +(53,38,'无极','无极县','河北省,石家庄市,无极县',3,'','','',''), +(54,38,'平山','平山县','河北省,石家庄市,平山县',3,'','','',''), +(55,38,'元氏','元氏县','河北省,石家庄市,元氏县',3,'','','',''), +(56,38,'赵','赵县','河北省,石家庄市,赵县',3,'','','',''), +(57,38,'辛集','辛集市','河北省,石家庄市,辛集市',3,'','','',''), +(58,38,'藁城','藁城市','河北省,石家庄市,藁城市',3,'','','',''), +(59,38,'晋州','晋州市','河北省,石家庄市,晋州市',3,'','','',''), +(60,38,'新乐','新乐市','河北省,石家庄市,新乐市',3,'','','',''), +(61,38,'鹿泉','鹿泉市','河北省,石家庄市,鹿泉市',3,'','','',''), +(62,37,'唐山','唐山市','河北省,唐山市',2,'','','',''), +(63,62,'路南','路南区','河北省,唐山市,路南区',3,'','','',''), +(64,62,'路北','路北区','河北省,唐山市,路北区',3,'','','',''), +(65,62,'古冶','古冶区','河北省,唐山市,古冶区',3,'','','',''), +(66,62,'开平','开平区','河北省,唐山市,开平区',3,'','','',''), +(67,62,'丰南','丰南区','河北省,唐山市,丰南区',3,'','','',''), +(68,62,'丰润','丰润区','河北省,唐山市,丰润区',3,'','','',''), +(69,62,'曹妃甸','曹妃甸区','河北省,唐山市,曹妃甸区',3,'','','',''), +(70,62,'滦','滦县','河北省,唐山市,滦县',3,'','','',''), +(71,62,'滦南','滦南县','河北省,唐山市,滦南县',3,'','','',''), +(72,62,'乐亭','乐亭县','河北省,唐山市,乐亭县',3,'','','',''), +(73,62,'迁西','迁西县','河北省,唐山市,迁西县',3,'','','',''), +(74,62,'玉田','玉田县','河北省,唐山市,玉田县',3,'','','',''), +(75,62,'遵化','遵化市','河北省,唐山市,遵化市',3,'','','',''), +(76,62,'迁安','迁安市','河北省,唐山市,迁安市',3,'','','',''), +(77,37,'秦皇岛','秦皇岛市','河北省,秦皇岛市',2,'','','',''), +(78,77,'海港','海港区','河北省,秦皇岛市,海港区',3,'','','',''), +(79,77,'山海关','山海关区','河北省,秦皇岛市,山海关区',3,'','','',''), +(80,77,'北戴河','北戴河区','河北省,秦皇岛市,北戴河区',3,'','','',''), +(81,77,'青龙满族','青龙满族自治县','河北省,秦皇岛市,青龙满族自治县',3,'','','',''), +(82,77,'昌黎','昌黎县','河北省,秦皇岛市,昌黎县',3,'','','',''), +(83,77,'抚宁','抚宁县','河北省,秦皇岛市,抚宁县',3,'','','',''), +(84,77,'卢龙','卢龙县','河北省,秦皇岛市,卢龙县',3,'','','',''), +(85,37,'邯郸','邯郸市','河北省,邯郸市',2,'','','',''), +(86,85,'邯山','邯山区','河北省,邯郸市,邯山区',3,'','','',''), +(87,85,'丛台','丛台区','河北省,邯郸市,丛台区',3,'','','',''), +(88,85,'复兴','复兴区','河北省,邯郸市,复兴区',3,'','','',''), +(89,85,'峰峰矿','峰峰矿区','河北省,邯郸市,峰峰矿区',3,'','','',''), +(90,85,'邯郸','邯郸县','河北省,邯郸市,邯郸县',3,'','','',''), +(91,85,'临漳','临漳县','河北省,邯郸市,临漳县',3,'','','',''), +(92,85,'成安','成安县','河北省,邯郸市,成安县',3,'','','',''), +(93,85,'大名','大名县','河北省,邯郸市,大名县',3,'','','',''), +(94,85,'涉','涉县','河北省,邯郸市,涉县',3,'','','',''), +(95,85,'磁','磁县','河北省,邯郸市,磁县',3,'','','',''), +(96,85,'肥乡','肥乡县','河北省,邯郸市,肥乡县',3,'','','',''), +(97,85,'永年','永年县','河北省,邯郸市,永年县',3,'','','',''), +(98,85,'邱','邱县','河北省,邯郸市,邱县',3,'','','',''), +(99,85,'鸡泽','鸡泽县','河北省,邯郸市,鸡泽县',3,'','','',''), +(100,85,'广平','广平县','河北省,邯郸市,广平县',3,'','','',''), +(101,85,'馆陶','馆陶县','河北省,邯郸市,馆陶县',3,'','','',''), +(102,85,'魏','魏县','河北省,邯郸市,魏县',3,'','','',''), +(103,85,'曲周','曲周县','河北省,邯郸市,曲周县',3,'','','',''), +(104,85,'武安','武安市','河北省,邯郸市,武安市',3,'','','',''), +(105,37,'邢台','邢台市','河北省,邢台市',2,'','','',''), +(106,105,'桥东','桥东区','河北省,邢台市,桥东区',3,'','','',''), +(107,105,'桥西','桥西区','河北省,邢台市,桥西区',3,'','','',''), +(108,105,'邢台','邢台县','河北省,邢台市,邢台县',3,'','','',''), +(109,105,'临城','临城县','河北省,邢台市,临城县',3,'','','',''), +(110,105,'内丘','内丘县','河北省,邢台市,内丘县',3,'','','',''), +(111,105,'柏乡','柏乡县','河北省,邢台市,柏乡县',3,'','','',''), +(112,105,'隆尧','隆尧县','河北省,邢台市,隆尧县',3,'','','',''), +(113,105,'任','任县','河北省,邢台市,任县',3,'','','',''), +(114,105,'南和','南和县','河北省,邢台市,南和县',3,'','','',''), +(115,105,'宁晋','宁晋县','河北省,邢台市,宁晋县',3,'','','',''), +(116,105,'巨鹿','巨鹿县','河北省,邢台市,巨鹿县',3,'','','',''), +(117,105,'新河','新河县','河北省,邢台市,新河县',3,'','','',''), +(118,105,'广宗','广宗县','河北省,邢台市,广宗县',3,'','','',''), +(119,105,'平乡','平乡县','河北省,邢台市,平乡县',3,'','','',''), +(120,105,'威','威县','河北省,邢台市,威县',3,'','','',''), +(121,105,'清河','清河县','河北省,邢台市,清河县',3,'','','',''), +(122,105,'临西','临西县','河北省,邢台市,临西县',3,'','','',''), +(123,105,'南宫','南宫市','河北省,邢台市,南宫市',3,'','','',''), +(124,105,'沙河','沙河市','河北省,邢台市,沙河市',3,'','','',''), +(125,37,'保定','保定市','河北省,保定市',2,'','','',''), +(126,125,'新市','新市区','河北省,保定市,新市区',3,'','','',''), +(127,125,'北市','北市区','河北省,保定市,北市区',3,'','','',''), +(128,125,'南市','南市区','河北省,保定市,南市区',3,'','','',''), +(129,125,'满城','满城县','河北省,保定市,满城县',3,'','','',''), +(130,125,'清苑','清苑县','河北省,保定市,清苑县',3,'','','',''), +(131,125,'涞水','涞水县','河北省,保定市,涞水县',3,'','','',''), +(132,125,'阜平','阜平县','河北省,保定市,阜平县',3,'','','',''), +(133,125,'徐水','徐水县','河北省,保定市,徐水县',3,'','','',''), +(134,125,'定兴','定兴县','河北省,保定市,定兴县',3,'','','',''), +(135,125,'唐','唐县','河北省,保定市,唐县',3,'','','',''), +(136,125,'高阳','高阳县','河北省,保定市,高阳县',3,'','','',''), +(137,125,'容城','容城县','河北省,保定市,容城县',3,'','','',''), +(138,125,'涞源','涞源县','河北省,保定市,涞源县',3,'','','',''), +(139,125,'望都','望都县','河北省,保定市,望都县',3,'','','',''), +(140,125,'安新','安新县','河北省,保定市,安新县',3,'','','',''), +(141,125,'易','易县','河北省,保定市,易县',3,'','','',''), +(142,125,'曲阳','曲阳县','河北省,保定市,曲阳县',3,'','','',''), +(143,125,'蠡','蠡县','河北省,保定市,蠡县',3,'','','',''), +(144,125,'顺平','顺平县','河北省,保定市,顺平县',3,'','','',''), +(145,125,'博野','博野县','河北省,保定市,博野县',3,'','','',''), +(146,125,'雄','雄县','河北省,保定市,雄县',3,'','','',''), +(147,125,'涿州','涿州市','河北省,保定市,涿州市',3,'','','',''), +(148,125,'定州','定州市','河北省,保定市,定州市',3,'','','',''), +(149,125,'安国','安国市','河北省,保定市,安国市',3,'','','',''), +(150,125,'高碑店','高碑店市','河北省,保定市,高碑店市',3,'','','',''), +(151,37,'张家口','张家口市','河北省,张家口市',2,'','','',''), +(152,151,'桥东','桥东区','河北省,张家口市,桥东区',3,'','','',''), +(153,151,'桥西','桥西区','河北省,张家口市,桥西区',3,'','','',''), +(154,151,'宣化','宣化区','河北省,张家口市,宣化区',3,'','','',''), +(155,151,'下花园','下花园区','河北省,张家口市,下花园区',3,'','','',''), +(156,151,'宣化','宣化县','河北省,张家口市,宣化县',3,'','','',''), +(157,151,'张北','张北县','河北省,张家口市,张北县',3,'','','',''), +(158,151,'康保','康保县','河北省,张家口市,康保县',3,'','','',''), +(159,151,'沽源','沽源县','河北省,张家口市,沽源县',3,'','','',''), +(160,151,'尚义','尚义县','河北省,张家口市,尚义县',3,'','','',''), +(161,151,'蔚','蔚县','河北省,张家口市,蔚县',3,'','','',''), +(162,151,'阳原','阳原县','河北省,张家口市,阳原县',3,'','','',''), +(163,151,'怀安','怀安县','河北省,张家口市,怀安县',3,'','','',''), +(164,151,'万全','万全县','河北省,张家口市,万全县',3,'','','',''), +(165,151,'怀来','怀来县','河北省,张家口市,怀来县',3,'','','',''), +(166,151,'涿鹿','涿鹿县','河北省,张家口市,涿鹿县',3,'','','',''), +(167,151,'赤城','赤城县','河北省,张家口市,赤城县',3,'','','',''), +(168,151,'崇礼','崇礼县','河北省,张家口市,崇礼县',3,'','','',''), +(169,37,'承德','承德市','河北省,承德市',2,'','','',''), +(170,169,'双桥','双桥区','河北省,承德市,双桥区',3,'','','',''), +(171,169,'双滦','双滦区','河北省,承德市,双滦区',3,'','','',''), +(172,169,'鹰手营子矿','鹰手营子矿区','河北省,承德市,鹰手营子矿区',3,'','','',''), +(173,169,'承德','承德县','河北省,承德市,承德县',3,'','','',''), +(174,169,'兴隆','兴隆县','河北省,承德市,兴隆县',3,'','','',''), +(175,169,'平泉','平泉县','河北省,承德市,平泉县',3,'','','',''), +(176,169,'滦平','滦平县','河北省,承德市,滦平县',3,'','','',''), +(177,169,'隆化','隆化县','河北省,承德市,隆化县',3,'','','',''), +(178,169,'丰宁满族','丰宁满族自治县','河北省,承德市,丰宁满族自治县',3,'','','',''), +(179,169,'宽城满族','宽城满族自治县','河北省,承德市,宽城满族自治县',3,'','','',''), +(180,169,'围场满族蒙古族','围场满族蒙古族自治县','河北省,承德市,围场满族蒙古族自治县',3,'','','',''), +(181,37,'沧州','沧州市','河北省,沧州市',2,'','','',''), +(182,181,'新华','新华区','河北省,沧州市,新华区',3,'','','',''), +(183,181,'运河','运河区','河北省,沧州市,运河区',3,'','','',''), +(184,181,'沧','沧县','河北省,沧州市,沧县',3,'','','',''), +(185,181,'青','青县','河北省,沧州市,青县',3,'','','',''), +(186,181,'东光','东光县','河北省,沧州市,东光县',3,'','','',''), +(187,181,'海兴','海兴县','河北省,沧州市,海兴县',3,'','','',''), +(188,181,'盐山','盐山县','河北省,沧州市,盐山县',3,'','','',''), +(189,181,'肃宁','肃宁县','河北省,沧州市,肃宁县',3,'','','',''), +(190,181,'南皮','南皮县','河北省,沧州市,南皮县',3,'','','',''), +(191,181,'吴桥','吴桥县','河北省,沧州市,吴桥县',3,'','','',''), +(192,181,'献','献县','河北省,沧州市,献县',3,'','','',''), +(193,181,'孟村回族','孟村回族自治县','河北省,沧州市,孟村回族自治县',3,'','','',''), +(194,181,'泊头','泊头市','河北省,沧州市,泊头市',3,'','','',''), +(195,181,'任丘','任丘市','河北省,沧州市,任丘市',3,'','','',''), +(196,181,'黄骅','黄骅市','河北省,沧州市,黄骅市',3,'','','',''), +(197,181,'河间','河间市','河北省,沧州市,河间市',3,'','','',''), +(198,37,'廊坊','廊坊市','河北省,廊坊市',2,'','','',''), +(199,198,'安次','安次区','河北省,廊坊市,安次区',3,'','','',''), +(200,198,'广阳','广阳区','河北省,廊坊市,广阳区',3,'','','',''), +(201,198,'固安','固安县','河北省,廊坊市,固安县',3,'','','',''), +(202,198,'永清','永清县','河北省,廊坊市,永清县',3,'','','',''), +(203,198,'香河','香河县','河北省,廊坊市,香河县',3,'','','',''), +(204,198,'大城','大城县','河北省,廊坊市,大城县',3,'','','',''), +(205,198,'文安','文安县','河北省,廊坊市,文安县',3,'','','',''), +(206,198,'大厂回族','大厂回族自治县','河北省,廊坊市,大厂回族自治县',3,'','','',''), +(207,198,'霸州','霸州市','河北省,廊坊市,霸州市',3,'','','',''), +(208,198,'三河','三河市','河北省,廊坊市,三河市',3,'','','',''), +(209,37,'衡水','衡水市','河北省,衡水市',2,'','','',''), +(210,209,'桃城','桃城区','河北省,衡水市,桃城区',3,'','','',''), +(211,209,'枣强','枣强县','河北省,衡水市,枣强县',3,'','','',''), +(212,209,'武邑','武邑县','河北省,衡水市,武邑县',3,'','','',''), +(213,209,'武强','武强县','河北省,衡水市,武强县',3,'','','',''), +(214,209,'饶阳','饶阳县','河北省,衡水市,饶阳县',3,'','','',''), +(215,209,'安平','安平县','河北省,衡水市,安平县',3,'','','',''), +(216,209,'故城','故城县','河北省,衡水市,故城县',3,'','','',''), +(217,209,'景','景县','河北省,衡水市,景县',3,'','','',''), +(218,209,'阜城','阜城县','河北省,衡水市,阜城县',3,'','','',''), +(219,209,'冀州','冀州市','河北省,衡水市,冀州市',3,'','','',''), +(220,209,'深州','深州市','河北省,衡水市,深州市',3,'','','',''), +(221,0,'山西','山西省','山西省',1,'','','',''), +(222,221,'太原','太原市','山西省,太原市',2,'','','',''), +(223,222,'小店','小店区','山西省,太原市,小店区',3,'','','',''), +(224,222,'迎泽','迎泽区','山西省,太原市,迎泽区',3,'','','',''), +(225,222,'杏花岭','杏花岭区','山西省,太原市,杏花岭区',3,'','','',''), +(226,222,'尖草坪','尖草坪区','山西省,太原市,尖草坪区',3,'','','',''), +(227,222,'万柏','万柏林区','山西省,太原市,万柏林区',3,'','','',''), +(228,222,'晋源','晋源区','山西省,太原市,晋源区',3,'','','',''), +(229,222,'清徐','清徐县','山西省,太原市,清徐县',3,'','','',''), +(230,222,'阳曲','阳曲县','山西省,太原市,阳曲县',3,'','','',''), +(231,222,'娄烦','娄烦县','山西省,太原市,娄烦县',3,'','','',''), +(232,222,'古交','古交市','山西省,太原市,古交市',3,'','','',''), +(233,221,'大同','大同市','山西省,大同市',2,'','','',''), +(234,233,'城','城区','山西省,大同市,城区',3,'','','',''), +(235,233,'矿','矿区','山西省,大同市,矿区',3,'','','',''), +(236,233,'南郊','南郊区','山西省,大同市,南郊区',3,'','','',''), +(237,233,'新荣','新荣区','山西省,大同市,新荣区',3,'','','',''), +(238,233,'阳高','阳高县','山西省,大同市,阳高县',3,'','','',''), +(239,233,'天镇','天镇县','山西省,大同市,天镇县',3,'','','',''), +(240,233,'广灵','广灵县','山西省,大同市,广灵县',3,'','','',''), +(241,233,'灵丘','灵丘县','山西省,大同市,灵丘县',3,'','','',''), +(242,233,'浑源','浑源县','山西省,大同市,浑源县',3,'','','',''), +(243,233,'左云','左云县','山西省,大同市,左云县',3,'','','',''), +(244,233,'大同','大同县','山西省,大同市,大同县',3,'','','',''), +(245,221,'阳泉','阳泉市','山西省,阳泉市',2,'','','',''), +(246,245,'城','城区','山西省,阳泉市,城区',3,'','','',''), +(247,245,'矿','矿区','山西省,阳泉市,矿区',3,'','','',''), +(248,245,'郊','郊区','山西省,阳泉市,郊区',3,'','','',''), +(249,245,'平定','平定县','山西省,阳泉市,平定县',3,'','','',''), +(250,245,'盂','盂县','山西省,阳泉市,盂县',3,'','','',''), +(251,221,'长治','长治市','山西省,长治市',2,'','','',''), +(252,251,'城','城区','山西省,长治市,城区',3,'','','',''), +(253,251,'郊','郊区','山西省,长治市,郊区',3,'','','',''), +(254,251,'长治','长治县','山西省,长治市,长治县',3,'','','',''), +(255,251,'襄垣','襄垣县','山西省,长治市,襄垣县',3,'','','',''), +(256,251,'屯留','屯留县','山西省,长治市,屯留县',3,'','','',''), +(257,251,'平顺','平顺县','山西省,长治市,平顺县',3,'','','',''), +(258,251,'黎城','黎城县','山西省,长治市,黎城县',3,'','','',''), +(259,251,'壶关','壶关县','山西省,长治市,壶关县',3,'','','',''), +(260,251,'长子','长子县','山西省,长治市,长子县',3,'','','',''), +(261,251,'武乡','武乡县','山西省,长治市,武乡县',3,'','','',''), +(262,251,'沁','沁县','山西省,长治市,沁县',3,'','','',''), +(263,251,'沁源','沁源县','山西省,长治市,沁源县',3,'','','',''), +(264,251,'潞城','潞城市','山西省,长治市,潞城市',3,'','','',''), +(265,221,'晋城','晋城市','山西省,晋城市',2,'','','',''), +(266,265,'城','城区','山西省,晋城市,城区',3,'','','',''), +(267,265,'沁水','沁水县','山西省,晋城市,沁水县',3,'','','',''), +(268,265,'阳城','阳城县','山西省,晋城市,阳城县',3,'','','',''), +(269,265,'陵川','陵川县','山西省,晋城市,陵川县',3,'','','',''), +(270,265,'泽州','泽州县','山西省,晋城市,泽州县',3,'','','',''), +(271,265,'高平','高平市','山西省,晋城市,高平市',3,'','','',''), +(272,221,'朔州','朔州市','山西省,朔州市',2,'','','',''), +(273,272,'朔城','朔城区','山西省,朔州市,朔城区',3,'','','',''), +(274,272,'平鲁','平鲁区','山西省,朔州市,平鲁区',3,'','','',''), +(275,272,'山阴','山阴县','山西省,朔州市,山阴县',3,'','','',''), +(276,272,'应','应县','山西省,朔州市,应县',3,'','','',''), +(277,272,'右玉','右玉县','山西省,朔州市,右玉县',3,'','','',''), +(278,272,'怀仁','怀仁县','山西省,朔州市,怀仁县',3,'','','',''), +(279,221,'晋中','晋中市','山西省,晋中市',2,'','','',''), +(280,279,'榆次','榆次区','山西省,晋中市,榆次区',3,'','','',''), +(281,279,'榆社','榆社县','山西省,晋中市,榆社县',3,'','','',''), +(282,279,'左权','左权县','山西省,晋中市,左权县',3,'','','',''), +(283,279,'和顺','和顺县','山西省,晋中市,和顺县',3,'','','',''), +(284,279,'昔阳','昔阳县','山西省,晋中市,昔阳县',3,'','','',''), +(285,279,'寿阳','寿阳县','山西省,晋中市,寿阳县',3,'','','',''), +(286,279,'太谷','太谷县','山西省,晋中市,太谷县',3,'','','',''), +(287,279,'祁','祁县','山西省,晋中市,祁县',3,'','','',''), +(288,279,'平遥','平遥县','山西省,晋中市,平遥县',3,'','','',''), +(289,279,'灵石','灵石县','山西省,晋中市,灵石县',3,'','','',''), +(290,279,'介休','介休市','山西省,晋中市,介休市',3,'','','',''), +(291,221,'运城','运城市','山西省,运城市',2,'','','',''), +(292,291,'盐湖','盐湖区','山西省,运城市,盐湖区',3,'','','',''), +(293,291,'临猗','临猗县','山西省,运城市,临猗县',3,'','','',''), +(294,291,'万荣','万荣县','山西省,运城市,万荣县',3,'','','',''), +(295,291,'闻喜','闻喜县','山西省,运城市,闻喜县',3,'','','',''), +(296,291,'稷山','稷山县','山西省,运城市,稷山县',3,'','','',''), +(297,291,'新绛','新绛县','山西省,运城市,新绛县',3,'','','',''), +(298,291,'绛','绛县','山西省,运城市,绛县',3,'','','',''), +(299,291,'垣曲','垣曲县','山西省,运城市,垣曲县',3,'','','',''), +(300,291,'夏','夏县','山西省,运城市,夏县',3,'','','',''); +INSERT IGNORE INTO `fa_area` (`id`,`pid`,`shortname`,`name`,`mergename`,`level`,`pinyin`,`code`,`zip`,`first`) VALUES +(301,291,'平陆','平陆县','山西省,运城市,平陆县',3,'','','',''), +(302,291,'芮城','芮城县','山西省,运城市,芮城县',3,'','','',''), +(303,291,'永济','永济市','山西省,运城市,永济市',3,'','','',''), +(304,291,'河津','河津市','山西省,运城市,河津市',3,'','','',''), +(305,221,'忻州','忻州市','山西省,忻州市',2,'','','',''), +(306,305,'忻府','忻府区','山西省,忻州市,忻府区',3,'','','',''), +(307,305,'定襄','定襄县','山西省,忻州市,定襄县',3,'','','',''), +(308,305,'五台','五台县','山西省,忻州市,五台县',3,'','','',''), +(309,305,'代','代县','山西省,忻州市,代县',3,'','','',''), +(310,305,'繁峙','繁峙县','山西省,忻州市,繁峙县',3,'','','',''), +(311,305,'宁武','宁武县','山西省,忻州市,宁武县',3,'','','',''), +(312,305,'静乐','静乐县','山西省,忻州市,静乐县',3,'','','',''), +(313,305,'神池','神池县','山西省,忻州市,神池县',3,'','','',''), +(314,305,'五寨','五寨县','山西省,忻州市,五寨县',3,'','','',''), +(315,305,'岢岚','岢岚县','山西省,忻州市,岢岚县',3,'','','',''), +(316,305,'河曲','河曲县','山西省,忻州市,河曲县',3,'','','',''), +(317,305,'保德','保德县','山西省,忻州市,保德县',3,'','','',''), +(318,305,'偏关','偏关县','山西省,忻州市,偏关县',3,'','','',''), +(319,305,'原平','原平市','山西省,忻州市,原平市',3,'','','',''), +(320,221,'临汾','临汾市','山西省,临汾市',2,'','','',''), +(321,320,'尧都','尧都区','山西省,临汾市,尧都区',3,'','','',''), +(322,320,'曲沃','曲沃县','山西省,临汾市,曲沃县',3,'','','',''), +(323,320,'翼城','翼城县','山西省,临汾市,翼城县',3,'','','',''), +(324,320,'襄汾','襄汾县','山西省,临汾市,襄汾县',3,'','','',''), +(325,320,'洪洞','洪洞县','山西省,临汾市,洪洞县',3,'','','',''), +(326,320,'古','古县','山西省,临汾市,古县',3,'','','',''), +(327,320,'安泽','安泽县','山西省,临汾市,安泽县',3,'','','',''), +(328,320,'浮山','浮山县','山西省,临汾市,浮山县',3,'','','',''), +(329,320,'吉','吉县','山西省,临汾市,吉县',3,'','','',''), +(330,320,'乡宁','乡宁县','山西省,临汾市,乡宁县',3,'','','',''), +(331,320,'大宁','大宁县','山西省,临汾市,大宁县',3,'','','',''), +(332,320,'隰','隰县','山西省,临汾市,隰县',3,'','','',''), +(333,320,'永和','永和县','山西省,临汾市,永和县',3,'','','',''), +(334,320,'蒲','蒲县','山西省,临汾市,蒲县',3,'','','',''), +(335,320,'汾西','汾西县','山西省,临汾市,汾西县',3,'','','',''), +(336,320,'侯马','侯马市','山西省,临汾市,侯马市',3,'','','',''), +(337,320,'霍州','霍州市','山西省,临汾市,霍州市',3,'','','',''), +(338,221,'吕梁','吕梁市','山西省,吕梁市',2,'','','',''), +(339,338,'离石','离石区','山西省,吕梁市,离石区',3,'','','',''), +(340,338,'文水','文水县','山西省,吕梁市,文水县',3,'','','',''), +(341,338,'交城','交城县','山西省,吕梁市,交城县',3,'','','',''), +(342,338,'兴','兴县','山西省,吕梁市,兴县',3,'','','',''), +(343,338,'临','临县','山西省,吕梁市,临县',3,'','','',''), +(344,338,'柳林','柳林县','山西省,吕梁市,柳林县',3,'','','',''), +(345,338,'石楼','石楼县','山西省,吕梁市,石楼县',3,'','','',''), +(346,338,'岚','岚县','山西省,吕梁市,岚县',3,'','','',''), +(347,338,'方山','方山县','山西省,吕梁市,方山县',3,'','','',''), +(348,338,'中阳','中阳县','山西省,吕梁市,中阳县',3,'','','',''), +(349,338,'交口','交口县','山西省,吕梁市,交口县',3,'','','',''), +(350,338,'孝义','孝义市','山西省,吕梁市,孝义市',3,'','','',''), +(351,338,'汾阳','汾阳市','山西省,吕梁市,汾阳市',3,'','','',''), +(352,0,'内蒙古','内蒙古自治区','内蒙古自治区',1,'','','',''), +(353,352,'呼和浩特','呼和浩特市','内蒙古自治区,呼和浩特市',2,'','','',''), +(354,353,'新城','新城区','内蒙古自治区,呼和浩特市,新城区',3,'','','',''), +(355,353,'回民','回民区','内蒙古自治区,呼和浩特市,回民区',3,'','','',''), +(356,353,'玉泉','玉泉区','内蒙古自治区,呼和浩特市,玉泉区',3,'','','',''), +(357,353,'赛罕','赛罕区','内蒙古自治区,呼和浩特市,赛罕区',3,'','','',''), +(358,353,'土默特左旗','土默特左旗','内蒙古自治区,呼和浩特市,土默特左旗',3,'','','',''), +(359,353,'托克托','托克托县','内蒙古自治区,呼和浩特市,托克托县',3,'','','',''), +(360,353,'和林格尔','和林格尔县','内蒙古自治区,呼和浩特市,和林格尔县',3,'','','',''), +(361,353,'清水河','清水河县','内蒙古自治区,呼和浩特市,清水河县',3,'','','',''), +(362,353,'武川','武川县','内蒙古自治区,呼和浩特市,武川县',3,'','','',''), +(363,352,'包头','包头市','内蒙古自治区,包头市',2,'','','',''), +(364,363,'东河','东河区','内蒙古自治区,包头市,东河区',3,'','','',''), +(365,363,'昆都仑','昆都仑区','内蒙古自治区,包头市,昆都仑区',3,'','','',''), +(366,363,'青山','青山区','内蒙古自治区,包头市,青山区',3,'','','',''), +(367,363,'石拐','石拐区','内蒙古自治区,包头市,石拐区',3,'','','',''), +(368,363,'白云鄂博矿','白云鄂博矿区','内蒙古自治区,包头市,白云鄂博矿区',3,'','','',''), +(369,363,'九原','九原区','内蒙古自治区,包头市,九原区',3,'','','',''), +(370,363,'土默特右旗','土默特右旗','内蒙古自治区,包头市,土默特右旗',3,'','','',''), +(371,363,'固阳','固阳县','内蒙古自治区,包头市,固阳县',3,'','','',''), +(372,363,'达尔罕茂明安联合旗','达尔罕茂明安联合旗','内蒙古自治区,包头市,达尔罕茂明安联合旗',3,'','','',''), +(373,352,'乌海','乌海市','内蒙古自治区,乌海市',2,'','','',''), +(374,373,'海勃湾','海勃湾区','内蒙古自治区,乌海市,海勃湾区',3,'','','',''), +(375,373,'海南','海南区','内蒙古自治区,乌海市,海南区',3,'','','',''), +(376,373,'乌达','乌达区','内蒙古自治区,乌海市,乌达区',3,'','','',''), +(377,352,'赤峰','赤峰市','内蒙古自治区,赤峰市',2,'','','',''), +(378,377,'红山','红山区','内蒙古自治区,赤峰市,红山区',3,'','','',''), +(379,377,'元宝山','元宝山区','内蒙古自治区,赤峰市,元宝山区',3,'','','',''), +(380,377,'松山','松山区','内蒙古自治区,赤峰市,松山区',3,'','','',''), +(381,377,'阿鲁科尔沁旗','阿鲁科尔沁旗','内蒙古自治区,赤峰市,阿鲁科尔沁旗',3,'','','',''), +(382,377,'巴林左旗','巴林左旗','内蒙古自治区,赤峰市,巴林左旗',3,'','','',''), +(383,377,'巴林右旗','巴林右旗','内蒙古自治区,赤峰市,巴林右旗',3,'','','',''), +(384,377,'林西','林西县','内蒙古自治区,赤峰市,林西县',3,'','','',''), +(385,377,'克什克腾旗','克什克腾旗','内蒙古自治区,赤峰市,克什克腾旗',3,'','','',''), +(386,377,'翁牛特旗','翁牛特旗','内蒙古自治区,赤峰市,翁牛特旗',3,'','','',''), +(387,377,'喀喇沁旗','喀喇沁旗','内蒙古自治区,赤峰市,喀喇沁旗',3,'','','',''), +(388,377,'宁城','宁城县','内蒙古自治区,赤峰市,宁城县',3,'','','',''), +(389,377,'敖汉旗','敖汉旗','内蒙古自治区,赤峰市,敖汉旗',3,'','','',''), +(390,352,'通辽','通辽市','内蒙古自治区,通辽市',2,'','','',''), +(391,390,'科尔沁','科尔沁区','内蒙古自治区,通辽市,科尔沁区',3,'','','',''), +(392,390,'科尔沁左翼中旗','科尔沁左翼中旗','内蒙古自治区,通辽市,科尔沁左翼中旗',3,'','','',''), +(393,390,'科尔沁左翼后旗','科尔沁左翼后旗','内蒙古自治区,通辽市,科尔沁左翼后旗',3,'','','',''), +(394,390,'开鲁','开鲁县','内蒙古自治区,通辽市,开鲁县',3,'','','',''), +(395,390,'库伦旗','库伦旗','内蒙古自治区,通辽市,库伦旗',3,'','','',''), +(396,390,'奈曼旗','奈曼旗','内蒙古自治区,通辽市,奈曼旗',3,'','','',''), +(397,390,'扎鲁特旗','扎鲁特旗','内蒙古自治区,通辽市,扎鲁特旗',3,'','','',''), +(398,390,'霍林郭勒','霍林郭勒市','内蒙古自治区,通辽市,霍林郭勒市',3,'','','',''), +(399,352,'鄂尔多斯','鄂尔多斯市','内蒙古自治区,鄂尔多斯市',2,'','','',''), +(400,399,'东胜','东胜区','内蒙古自治区,鄂尔多斯市,东胜区',3,'','','',''), +(401,399,'达拉特旗','达拉特旗','内蒙古自治区,鄂尔多斯市,达拉特旗',3,'','','',''), +(402,399,'准格尔旗','准格尔旗','内蒙古自治区,鄂尔多斯市,准格尔旗',3,'','','',''), +(403,399,'鄂托克前旗','鄂托克前旗','内蒙古自治区,鄂尔多斯市,鄂托克前旗',3,'','','',''), +(404,399,'鄂托克旗','鄂托克旗','内蒙古自治区,鄂尔多斯市,鄂托克旗',3,'','','',''), +(405,399,'杭锦旗','杭锦旗','内蒙古自治区,鄂尔多斯市,杭锦旗',3,'','','',''), +(406,399,'乌审旗','乌审旗','内蒙古自治区,鄂尔多斯市,乌审旗',3,'','','',''), +(407,399,'伊金霍洛旗','伊金霍洛旗','内蒙古自治区,鄂尔多斯市,伊金霍洛旗',3,'','','',''), +(408,352,'呼伦贝尔','呼伦贝尔市','内蒙古自治区,呼伦贝尔市',2,'','','',''), +(409,408,'海拉尔','海拉尔区','内蒙古自治区,呼伦贝尔市,海拉尔区',3,'','','',''), +(410,408,'扎赉诺尔','扎赉诺尔区','内蒙古自治区,呼伦贝尔市,扎赉诺尔区',3,'','','',''), +(411,408,'阿荣旗','阿荣旗','内蒙古自治区,呼伦贝尔市,阿荣旗',3,'','','',''), +(412,408,'莫力达瓦达斡尔族自治旗','莫力达瓦达斡尔族自治旗','内蒙古自治区,呼伦贝尔市,莫力达瓦达斡尔族自治旗',3,'','','',''), +(413,408,'鄂伦春自治旗','鄂伦春自治旗','内蒙古自治区,呼伦贝尔市,鄂伦春自治旗',3,'','','',''), +(414,408,'鄂温克族自治旗','鄂温克族自治旗','内蒙古自治区,呼伦贝尔市,鄂温克族自治旗',3,'','','',''), +(415,408,'陈巴尔虎旗','陈巴尔虎旗','内蒙古自治区,呼伦贝尔市,陈巴尔虎旗',3,'','','',''), +(416,408,'新巴尔虎左旗','新巴尔虎左旗','内蒙古自治区,呼伦贝尔市,新巴尔虎左旗',3,'','','',''), +(417,408,'新巴尔虎右旗','新巴尔虎右旗','内蒙古自治区,呼伦贝尔市,新巴尔虎右旗',3,'','','',''), +(418,408,'满洲里','满洲里市','内蒙古自治区,呼伦贝尔市,满洲里市',3,'','','',''), +(419,408,'牙克石','牙克石市','内蒙古自治区,呼伦贝尔市,牙克石市',3,'','','',''), +(420,408,'扎兰屯','扎兰屯市','内蒙古自治区,呼伦贝尔市,扎兰屯市',3,'','','',''), +(421,408,'额尔古纳','额尔古纳市','内蒙古自治区,呼伦贝尔市,额尔古纳市',3,'','','',''), +(422,408,'根河','根河市','内蒙古自治区,呼伦贝尔市,根河市',3,'','','',''), +(423,352,'巴彦淖尔','巴彦淖尔市','内蒙古自治区,巴彦淖尔市',2,'','','',''), +(424,423,'临河','临河区','内蒙古自治区,巴彦淖尔市,临河区',3,'','','',''), +(425,423,'五原','五原县','内蒙古自治区,巴彦淖尔市,五原县',3,'','','',''), +(426,423,'磴口','磴口县','内蒙古自治区,巴彦淖尔市,磴口县',3,'','','',''), +(427,423,'乌拉特前旗','乌拉特前旗','内蒙古自治区,巴彦淖尔市,乌拉特前旗',3,'','','',''), +(428,423,'乌拉特中旗','乌拉特中旗','内蒙古自治区,巴彦淖尔市,乌拉特中旗',3,'','','',''), +(429,423,'乌拉特后旗','乌拉特后旗','内蒙古自治区,巴彦淖尔市,乌拉特后旗',3,'','','',''), +(430,423,'杭锦后旗','杭锦后旗','内蒙古自治区,巴彦淖尔市,杭锦后旗',3,'','','',''), +(431,352,'乌兰察布','乌兰察布市','内蒙古自治区,乌兰察布市',2,'','','',''), +(432,431,'集宁','集宁区','内蒙古自治区,乌兰察布市,集宁区',3,'','','',''), +(433,431,'卓资','卓资县','内蒙古自治区,乌兰察布市,卓资县',3,'','','',''), +(434,431,'化德','化德县','内蒙古自治区,乌兰察布市,化德县',3,'','','',''), +(435,431,'商都','商都县','内蒙古自治区,乌兰察布市,商都县',3,'','','',''), +(436,431,'兴和','兴和县','内蒙古自治区,乌兰察布市,兴和县',3,'','','',''), +(437,431,'凉城','凉城县','内蒙古自治区,乌兰察布市,凉城县',3,'','','',''), +(438,431,'察哈尔右翼前旗','察哈尔右翼前旗','内蒙古自治区,乌兰察布市,察哈尔右翼前旗',3,'','','',''), +(439,431,'察哈尔右翼中旗','察哈尔右翼中旗','内蒙古自治区,乌兰察布市,察哈尔右翼中旗',3,'','','',''), +(440,431,'察哈尔右翼后旗','察哈尔右翼后旗','内蒙古自治区,乌兰察布市,察哈尔右翼后旗',3,'','','',''), +(441,431,'四子王旗','四子王旗','内蒙古自治区,乌兰察布市,四子王旗',3,'','','',''), +(442,431,'丰镇','丰镇市','内蒙古自治区,乌兰察布市,丰镇市',3,'','','',''), +(443,352,'兴安','兴安盟','内蒙古自治区,兴安盟',2,'','','',''), +(444,443,'乌兰浩特','乌兰浩特市','内蒙古自治区,兴安盟,乌兰浩特市',3,'','','',''), +(445,443,'阿尔山','阿尔山市','内蒙古自治区,兴安盟,阿尔山市',3,'','','',''), +(446,443,'科尔沁右翼前旗','科尔沁右翼前旗','内蒙古自治区,兴安盟,科尔沁右翼前旗',3,'','','',''), +(447,443,'科尔沁右翼中旗','科尔沁右翼中旗','内蒙古自治区,兴安盟,科尔沁右翼中旗',3,'','','',''), +(448,443,'扎赉特旗','扎赉特旗','内蒙古自治区,兴安盟,扎赉特旗',3,'','','',''), +(449,443,'突泉','突泉县','内蒙古自治区,兴安盟,突泉县',3,'','','',''), +(450,352,'锡林郭勒','锡林郭勒盟','内蒙古自治区,锡林郭勒盟',2,'','','',''), +(451,450,'二连浩特','二连浩特市','内蒙古自治区,锡林郭勒盟,二连浩特市',3,'','','',''), +(452,450,'锡林浩特','锡林浩特市','内蒙古自治区,锡林郭勒盟,锡林浩特市',3,'','','',''), +(453,450,'阿巴嘎旗','阿巴嘎旗','内蒙古自治区,锡林郭勒盟,阿巴嘎旗',3,'','','',''), +(454,450,'苏尼特左旗','苏尼特左旗','内蒙古自治区,锡林郭勒盟,苏尼特左旗',3,'','','',''), +(455,450,'苏尼特右旗','苏尼特右旗','内蒙古自治区,锡林郭勒盟,苏尼特右旗',3,'','','',''), +(456,450,'东乌珠穆沁旗','东乌珠穆沁旗','内蒙古自治区,锡林郭勒盟,东乌珠穆沁旗',3,'','','',''), +(457,450,'西乌珠穆沁旗','西乌珠穆沁旗','内蒙古自治区,锡林郭勒盟,西乌珠穆沁旗',3,'','','',''), +(458,450,'太仆寺旗','太仆寺旗','内蒙古自治区,锡林郭勒盟,太仆寺旗',3,'','','',''), +(459,450,'镶黄旗','镶黄旗','内蒙古自治区,锡林郭勒盟,镶黄旗',3,'','','',''), +(460,450,'正镶白旗','正镶白旗','内蒙古自治区,锡林郭勒盟,正镶白旗',3,'','','',''), +(461,450,'正蓝旗','正蓝旗','内蒙古自治区,锡林郭勒盟,正蓝旗',3,'','','',''), +(462,450,'多伦','多伦县','内蒙古自治区,锡林郭勒盟,多伦县',3,'','','',''), +(463,352,'阿拉善','阿拉善盟','内蒙古自治区,阿拉善盟',2,'','','',''), +(464,463,'阿拉善左旗','阿拉善左旗','内蒙古自治区,阿拉善盟,阿拉善左旗',3,'','','',''), +(465,463,'阿拉善右旗','阿拉善右旗','内蒙古自治区,阿拉善盟,阿拉善右旗',3,'','','',''), +(466,463,'额济纳旗','额济纳旗','内蒙古自治区,阿拉善盟,额济纳旗',3,'','','',''), +(467,0,'辽宁','辽宁省','辽宁省',1,'','','',''), +(468,467,'沈阳','沈阳市','辽宁省,沈阳市',2,'','','',''), +(469,468,'和平','和平区','辽宁省,沈阳市,和平区',3,'','','',''), +(470,468,'沈河','沈河区','辽宁省,沈阳市,沈河区',3,'','','',''), +(471,468,'大东','大东区','辽宁省,沈阳市,大东区',3,'','','',''), +(472,468,'皇姑','皇姑区','辽宁省,沈阳市,皇姑区',3,'','','',''), +(473,468,'铁西','铁西区','辽宁省,沈阳市,铁西区',3,'','','',''), +(474,468,'苏家屯','苏家屯区','辽宁省,沈阳市,苏家屯区',3,'','','',''), +(475,468,'东陵','东陵区','辽宁省,沈阳市,东陵区',3,'','','',''), +(476,468,'沈北新','沈北新区','辽宁省,沈阳市,沈北新区',3,'','','',''), +(477,468,'于洪','于洪区','辽宁省,沈阳市,于洪区',3,'','','',''), +(478,468,'辽中','辽中县','辽宁省,沈阳市,辽中县',3,'','','',''), +(479,468,'康平','康平县','辽宁省,沈阳市,康平县',3,'','','',''), +(480,468,'法库','法库县','辽宁省,沈阳市,法库县',3,'','','',''), +(481,468,'新民','新民市','辽宁省,沈阳市,新民市',3,'','','',''), +(482,467,'大连','大连市','辽宁省,大连市',2,'','','',''), +(483,482,'中山','中山区','辽宁省,大连市,中山区',3,'','','',''), +(484,482,'西岗','西岗区','辽宁省,大连市,西岗区',3,'','','',''), +(485,482,'沙河口','沙河口区','辽宁省,大连市,沙河口区',3,'','','',''), +(486,482,'甘井子','甘井子区','辽宁省,大连市,甘井子区',3,'','','',''), +(487,482,'旅顺口','旅顺口区','辽宁省,大连市,旅顺口区',3,'','','',''), +(488,482,'金州','金州区','辽宁省,大连市,金州区',3,'','','',''), +(489,482,'长海','长海县','辽宁省,大连市,长海县',3,'','','',''), +(490,482,'瓦房店','瓦房店市','辽宁省,大连市,瓦房店市',3,'','','',''), +(491,482,'普兰店','普兰店市','辽宁省,大连市,普兰店市',3,'','','',''), +(492,482,'庄河','庄河市','辽宁省,大连市,庄河市',3,'','','',''), +(493,467,'鞍山','鞍山市','辽宁省,鞍山市',2,'','','',''), +(494,493,'铁东','铁东区','辽宁省,鞍山市,铁东区',3,'','','',''), +(495,493,'铁西','铁西区','辽宁省,鞍山市,铁西区',3,'','','',''), +(496,493,'立山','立山区','辽宁省,鞍山市,立山区',3,'','','',''), +(497,493,'千山','千山区','辽宁省,鞍山市,千山区',3,'','','',''), +(498,493,'台安','台安县','辽宁省,鞍山市,台安县',3,'','','',''), +(499,493,'岫岩满族','岫岩满族自治县','辽宁省,鞍山市,岫岩满族自治县',3,'','','',''), +(500,493,'海城','海城市','辽宁省,鞍山市,海城市',3,'','','',''), +(501,467,'抚顺','抚顺市','辽宁省,抚顺市',2,'','','',''), +(502,501,'新抚','新抚区','辽宁省,抚顺市,新抚区',3,'','','',''), +(503,501,'东洲','东洲区','辽宁省,抚顺市,东洲区',3,'','','',''), +(504,501,'望花','望花区','辽宁省,抚顺市,望花区',3,'','','',''), +(505,501,'顺城','顺城区','辽宁省,抚顺市,顺城区',3,'','','',''), +(506,501,'抚顺','抚顺县','辽宁省,抚顺市,抚顺县',3,'','','',''), +(507,501,'新宾满族','新宾满族自治县','辽宁省,抚顺市,新宾满族自治县',3,'','','',''), +(508,501,'清原满族','清原满族自治县','辽宁省,抚顺市,清原满族自治县',3,'','','',''), +(509,467,'本溪','本溪市','辽宁省,本溪市',2,'','','',''), +(510,509,'平山','平山区','辽宁省,本溪市,平山区',3,'','','',''), +(511,509,'溪湖','溪湖区','辽宁省,本溪市,溪湖区',3,'','','',''), +(512,509,'明山','明山区','辽宁省,本溪市,明山区',3,'','','',''), +(513,509,'南芬','南芬区','辽宁省,本溪市,南芬区',3,'','','',''), +(514,509,'本溪满族','本溪满族自治县','辽宁省,本溪市,本溪满族自治县',3,'','','',''), +(515,509,'桓仁满族','桓仁满族自治县','辽宁省,本溪市,桓仁满族自治县',3,'','','',''), +(516,467,'丹东','丹东市','辽宁省,丹东市',2,'','','',''), +(517,516,'元宝','元宝区','辽宁省,丹东市,元宝区',3,'','','',''), +(518,516,'振兴','振兴区','辽宁省,丹东市,振兴区',3,'','','',''), +(519,516,'振安','振安区','辽宁省,丹东市,振安区',3,'','','',''), +(520,516,'宽甸满族','宽甸满族自治县','辽宁省,丹东市,宽甸满族自治县',3,'','','',''), +(521,516,'东港','东港市','辽宁省,丹东市,东港市',3,'','','',''), +(522,516,'凤城','凤城市','辽宁省,丹东市,凤城市',3,'','','',''), +(523,467,'锦州','锦州市','辽宁省,锦州市',2,'','','',''), +(524,523,'古塔','古塔区','辽宁省,锦州市,古塔区',3,'','','',''), +(525,523,'凌河','凌河区','辽宁省,锦州市,凌河区',3,'','','',''), +(526,523,'太和','太和区','辽宁省,锦州市,太和区',3,'','','',''), +(527,523,'黑山','黑山县','辽宁省,锦州市,黑山县',3,'','','',''), +(528,523,'义','义县','辽宁省,锦州市,义县',3,'','','',''), +(529,523,'凌海','凌海市','辽宁省,锦州市,凌海市',3,'','','',''), +(530,523,'北镇','北镇市','辽宁省,锦州市,北镇市',3,'','','',''), +(531,467,'营口','营口市','辽宁省,营口市',2,'','','',''), +(532,531,'站前','站前区','辽宁省,营口市,站前区',3,'','','',''), +(533,531,'西市','西市区','辽宁省,营口市,西市区',3,'','','',''), +(534,531,'鲅鱼圈','鲅鱼圈区','辽宁省,营口市,鲅鱼圈区',3,'','','',''), +(535,531,'老边','老边区','辽宁省,营口市,老边区',3,'','','',''), +(536,531,'盖州','盖州市','辽宁省,营口市,盖州市',3,'','','',''), +(537,531,'大石桥','大石桥市','辽宁省,营口市,大石桥市',3,'','','',''), +(538,467,'阜新','阜新市','辽宁省,阜新市',2,'','','',''), +(539,538,'海州','海州区','辽宁省,阜新市,海州区',3,'','','',''), +(540,538,'新邱','新邱区','辽宁省,阜新市,新邱区',3,'','','',''), +(541,538,'太平','太平区','辽宁省,阜新市,太平区',3,'','','',''), +(542,538,'清河门','清河门区','辽宁省,阜新市,清河门区',3,'','','',''), +(543,538,'细河','细河区','辽宁省,阜新市,细河区',3,'','','',''), +(544,538,'阜新蒙古族','阜新蒙古族自治县','辽宁省,阜新市,阜新蒙古族自治县',3,'','','',''), +(545,538,'彰武','彰武县','辽宁省,阜新市,彰武县',3,'','','',''), +(546,467,'辽阳','辽阳市','辽宁省,辽阳市',2,'','','',''), +(547,546,'白塔','白塔区','辽宁省,辽阳市,白塔区',3,'','','',''), +(548,546,'文圣','文圣区','辽宁省,辽阳市,文圣区',3,'','','',''), +(549,546,'宏伟','宏伟区','辽宁省,辽阳市,宏伟区',3,'','','',''), +(550,546,'弓长岭','弓长岭区','辽宁省,辽阳市,弓长岭区',3,'','','',''), +(551,546,'太子河','太子河区','辽宁省,辽阳市,太子河区',3,'','','',''), +(552,546,'辽阳','辽阳县','辽宁省,辽阳市,辽阳县',3,'','','',''), +(553,546,'灯塔','灯塔市','辽宁省,辽阳市,灯塔市',3,'','','',''), +(554,467,'盘锦','盘锦市','辽宁省,盘锦市',2,'','','',''), +(555,554,'双台子','双台子区','辽宁省,盘锦市,双台子区',3,'','','',''), +(556,554,'兴隆台','兴隆台区','辽宁省,盘锦市,兴隆台区',3,'','','',''), +(557,554,'大洼','大洼县','辽宁省,盘锦市,大洼县',3,'','','',''), +(558,554,'盘山','盘山县','辽宁省,盘锦市,盘山县',3,'','','',''), +(559,467,'铁岭','铁岭市','辽宁省,铁岭市',2,'','','',''), +(560,559,'银州','银州区','辽宁省,铁岭市,银州区',3,'','','',''), +(561,559,'清河','清河区','辽宁省,铁岭市,清河区',3,'','','',''), +(562,559,'铁岭','铁岭县','辽宁省,铁岭市,铁岭县',3,'','','',''), +(563,559,'西丰','西丰县','辽宁省,铁岭市,西丰县',3,'','','',''), +(564,559,'昌图','昌图县','辽宁省,铁岭市,昌图县',3,'','','',''), +(565,559,'调兵山','调兵山市','辽宁省,铁岭市,调兵山市',3,'','','',''), +(566,559,'开原','开原市','辽宁省,铁岭市,开原市',3,'','','',''), +(567,467,'朝阳','朝阳市','辽宁省,朝阳市',2,'','','',''), +(568,567,'双塔','双塔区','辽宁省,朝阳市,双塔区',3,'','','',''), +(569,567,'龙城','龙城区','辽宁省,朝阳市,龙城区',3,'','','',''), +(570,567,'朝阳','朝阳县','辽宁省,朝阳市,朝阳县',3,'','','',''), +(571,567,'建平','建平县','辽宁省,朝阳市,建平县',3,'','','',''), +(572,567,'喀喇沁左翼蒙古族','喀喇沁左翼蒙古族自治县','辽宁省,朝阳市,喀喇沁左翼蒙古族自治县',3,'','','',''), +(573,567,'北票','北票市','辽宁省,朝阳市,北票市',3,'','','',''), +(574,567,'凌源','凌源市','辽宁省,朝阳市,凌源市',3,'','','',''), +(575,467,'葫芦岛','葫芦岛市','辽宁省,葫芦岛市',2,'','','',''), +(576,575,'连山','连山区','辽宁省,葫芦岛市,连山区',3,'','','',''), +(577,575,'龙港','龙港区','辽宁省,葫芦岛市,龙港区',3,'','','',''), +(578,575,'南票','南票区','辽宁省,葫芦岛市,南票区',3,'','','',''), +(579,575,'绥中','绥中县','辽宁省,葫芦岛市,绥中县',3,'','','',''), +(580,575,'建昌','建昌县','辽宁省,葫芦岛市,建昌县',3,'','','',''), +(581,575,'兴城','兴城市','辽宁省,葫芦岛市,兴城市',3,'','','',''), +(582,0,'吉林','吉林省','吉林省',1,'','','',''), +(583,582,'长春','长春市','吉林省,长春市',2,'','','',''), +(584,583,'南关','南关区','吉林省,长春市,南关区',3,'','','',''), +(585,583,'宽城','宽城区','吉林省,长春市,宽城区',3,'','','',''), +(586,583,'朝阳','朝阳区','吉林省,长春市,朝阳区',3,'','','',''), +(587,583,'二道','二道区','吉林省,长春市,二道区',3,'','','',''), +(588,583,'绿园','绿园区','吉林省,长春市,绿园区',3,'','','',''), +(589,583,'双阳','双阳区','吉林省,长春市,双阳区',3,'','','',''), +(590,583,'农安','农安县','吉林省,长春市,农安县',3,'','','',''), +(591,583,'九台','九台市','吉林省,长春市,九台市',3,'','','',''), +(592,583,'榆树','榆树市','吉林省,长春市,榆树市',3,'','','',''), +(593,583,'德惠','德惠市','吉林省,长春市,德惠市',3,'','','',''), +(594,582,'吉林','吉林市','吉林省,吉林市',2,'','','',''), +(595,594,'昌邑','昌邑区','吉林省,吉林市,昌邑区',3,'','','',''), +(596,594,'龙潭','龙潭区','吉林省,吉林市,龙潭区',3,'','','',''), +(597,594,'龙潭','龙潭区','吉林省,吉林市,龙潭区',3,'','','',''), +(598,594,'丰满','丰满区','吉林省,吉林市,丰满区',3,'','','',''), +(599,594,'永吉','永吉县','吉林省,吉林市,永吉县',3,'','','',''), +(600,594,'蛟河','蛟河市','吉林省,吉林市,蛟河市',3,'','','',''); +INSERT IGNORE INTO `fa_area` (`id`,`pid`,`shortname`,`name`,`mergename`,`level`,`pinyin`,`code`,`zip`,`first`) VALUES +(601,594,'桦甸','桦甸市','吉林省,吉林市,桦甸市',3,'','','',''), +(602,594,'舒兰','舒兰市','吉林省,吉林市,舒兰市',3,'','','',''), +(603,594,'磐石','磐石市','吉林省,吉林市,磐石市',3,'','','',''), +(604,582,'四平','四平市','吉林省,四平市',2,'','','',''), +(605,604,'铁西','铁西区','吉林省,四平市,铁西区',3,'','','',''), +(606,604,'铁东','铁东区','吉林省,四平市,铁东区',3,'','','',''), +(607,604,'梨树','梨树县','吉林省,四平市,梨树县',3,'','','',''), +(608,604,'伊通满族','伊通满族自治县','吉林省,四平市,伊通满族自治县',3,'','','',''), +(609,604,'公主岭','公主岭市','吉林省,四平市,公主岭市',3,'','','',''), +(610,604,'双辽','双辽市','吉林省,四平市,双辽市',3,'','','',''), +(611,582,'辽源','辽源市','吉林省,辽源市',2,'','','',''), +(612,611,'龙山','龙山区','吉林省,辽源市,龙山区',3,'','','',''), +(613,611,'西安','西安区','吉林省,辽源市,西安区',3,'','','',''), +(614,611,'东丰','东丰县','吉林省,辽源市,东丰县',3,'','','',''), +(615,611,'东辽','东辽县','吉林省,辽源市,东辽县',3,'','','',''), +(616,582,'通化','通化市','吉林省,通化市',2,'','','',''), +(617,616,'东昌','东昌区','吉林省,通化市,东昌区',3,'','','',''), +(618,616,'二道江','二道江区','吉林省,通化市,二道江区',3,'','','',''), +(619,616,'通化','通化县','吉林省,通化市,通化县',3,'','','',''), +(620,616,'辉南','辉南县','吉林省,通化市,辉南县',3,'','','',''), +(621,616,'柳河','柳河县','吉林省,通化市,柳河县',3,'','','',''), +(622,616,'梅河口','梅河口市','吉林省,通化市,梅河口市',3,'','','',''), +(623,616,'集安','集安市','吉林省,通化市,集安市',3,'','','',''), +(624,582,'白山','白山市','吉林省,白山市',2,'','','',''), +(625,624,'浑江','浑江区','吉林省,白山市,浑江区',3,'','','',''), +(626,624,'江源','江源区','吉林省,白山市,江源区',3,'','','',''), +(627,624,'抚松','抚松县','吉林省,白山市,抚松县',3,'','','',''), +(628,624,'靖宇','靖宇县','吉林省,白山市,靖宇县',3,'','','',''), +(629,624,'长白朝鲜族','长白朝鲜族自治县','吉林省,白山市,长白朝鲜族自治县',3,'','','',''), +(630,624,'临江','临江市','吉林省,白山市,临江市',3,'','','',''), +(631,582,'松原','松原市','吉林省,松原市',2,'','','',''), +(632,631,'宁江','宁江区','吉林省,松原市,宁江区',3,'','','',''), +(633,631,'前郭尔罗斯蒙古族','前郭尔罗斯蒙古族自治县','吉林省,松原市,前郭尔罗斯蒙古族自治县',3,'','','',''), +(634,631,'长岭','长岭县','吉林省,松原市,长岭县',3,'','','',''), +(635,631,'乾安','乾安县','吉林省,松原市,乾安县',3,'','','',''), +(636,631,'扶余','扶余市','吉林省,松原市,扶余市',3,'','','',''), +(637,582,'白城','白城市','吉林省,白城市',2,'','','',''), +(638,637,'洮北','洮北区','吉林省,白城市,洮北区',3,'','','',''), +(639,637,'镇赉','镇赉县','吉林省,白城市,镇赉县',3,'','','',''), +(640,637,'通榆','通榆县','吉林省,白城市,通榆县',3,'','','',''), +(641,637,'洮南','洮南市','吉林省,白城市,洮南市',3,'','','',''), +(642,637,'大安','大安市','吉林省,白城市,大安市',3,'','','',''), +(643,582,'延边朝鲜族','延边朝鲜族自治州','吉林省,延边朝鲜族自治州',2,'','','',''), +(644,643,'延吉','延吉市','吉林省,延边朝鲜族自治州,延吉市',3,'','','',''), +(645,643,'图们','图们市','吉林省,延边朝鲜族自治州,图们市',3,'','','',''), +(646,643,'敦化','敦化市','吉林省,延边朝鲜族自治州,敦化市',3,'','','',''), +(647,643,'珲春','珲春市','吉林省,延边朝鲜族自治州,珲春市',3,'','','',''), +(648,643,'龙井','龙井市','吉林省,延边朝鲜族自治州,龙井市',3,'','','',''), +(649,643,'和龙','和龙市','吉林省,延边朝鲜族自治州,和龙市',3,'','','',''), +(650,643,'汪清','汪清县','吉林省,延边朝鲜族自治州,汪清县',3,'','','',''), +(651,643,'安图','安图县','吉林省,延边朝鲜族自治州,安图县',3,'','','',''), +(652,0,'黑龙江','黑龙江省','黑龙江省',1,'','','',''), +(653,652,'哈尔滨','哈尔滨市','黑龙江省,哈尔滨市',2,'','','',''), +(654,653,'道里','道里区','黑龙江省,哈尔滨市,道里区',3,'','','',''), +(655,653,'南岗','南岗区','黑龙江省,哈尔滨市,南岗区',3,'','','',''), +(656,653,'道外','道外区','黑龙江省,哈尔滨市,道外区',3,'','','',''), +(657,653,'平房','平房区','黑龙江省,哈尔滨市,平房区',3,'','','',''), +(658,653,'松北','松北区','黑龙江省,哈尔滨市,松北区',3,'','','',''), +(659,653,'香坊','香坊区','黑龙江省,哈尔滨市,香坊区',3,'','','',''), +(660,653,'呼兰','呼兰区','黑龙江省,哈尔滨市,呼兰区',3,'','','',''), +(661,653,'阿城','阿城区','黑龙江省,哈尔滨市,阿城区',3,'','','',''), +(662,653,'依兰','依兰县','黑龙江省,哈尔滨市,依兰县',3,'','','',''), +(663,653,'方正','方正县','黑龙江省,哈尔滨市,方正县',3,'','','',''), +(664,653,'宾','宾县','黑龙江省,哈尔滨市,宾县',3,'','','',''), +(665,653,'巴彦','巴彦县','黑龙江省,哈尔滨市,巴彦县',3,'','','',''), +(666,653,'木兰','木兰县','黑龙江省,哈尔滨市,木兰县',3,'','','',''), +(667,653,'通河','通河县','黑龙江省,哈尔滨市,通河县',3,'','','',''), +(668,653,'延寿','延寿县','黑龙江省,哈尔滨市,延寿县',3,'','','',''), +(669,653,'双城','双城市','黑龙江省,哈尔滨市,双城市',3,'','','',''), +(670,653,'尚志','尚志市','黑龙江省,哈尔滨市,尚志市',3,'','','',''), +(671,653,'五常','五常市','黑龙江省,哈尔滨市,五常市',3,'','','',''), +(672,652,'齐齐哈尔','齐齐哈尔市','黑龙江省,齐齐哈尔市',2,'','','',''), +(673,672,'龙沙','龙沙区','黑龙江省,齐齐哈尔市,龙沙区',3,'','','',''), +(674,672,'建华','建华区','黑龙江省,齐齐哈尔市,建华区',3,'','','',''), +(675,672,'铁锋','铁锋区','黑龙江省,齐齐哈尔市,铁锋区',3,'','','',''), +(676,672,'昂昂溪','昂昂溪区','黑龙江省,齐齐哈尔市,昂昂溪区',3,'','','',''), +(677,672,'富拉尔基','富拉尔基区','黑龙江省,齐齐哈尔市,富拉尔基区',3,'','','',''), +(678,672,'碾子山','碾子山区','黑龙江省,齐齐哈尔市,碾子山区',3,'','','',''), +(679,672,'梅里斯达斡尔族','梅里斯达斡尔族区','黑龙江省,齐齐哈尔市,梅里斯达斡尔族区',3,'','','',''), +(680,672,'龙江','龙江县','黑龙江省,齐齐哈尔市,龙江县',3,'','','',''), +(681,672,'依安','依安县','黑龙江省,齐齐哈尔市,依安县',3,'','','',''), +(682,672,'泰来','泰来县','黑龙江省,齐齐哈尔市,泰来县',3,'','','',''), +(683,672,'甘南','甘南县','黑龙江省,齐齐哈尔市,甘南县',3,'','','',''), +(684,672,'富裕','富裕县','黑龙江省,齐齐哈尔市,富裕县',3,'','','',''), +(685,672,'克山','克山县','黑龙江省,齐齐哈尔市,克山县',3,'','','',''), +(686,672,'克东','克东县','黑龙江省,齐齐哈尔市,克东县',3,'','','',''), +(687,672,'拜泉','拜泉县','黑龙江省,齐齐哈尔市,拜泉县',3,'','','',''), +(688,672,'讷河','讷河市','黑龙江省,齐齐哈尔市,讷河市',3,'','','',''), +(689,652,'鸡西','鸡西市','黑龙江省,鸡西市',2,'','','',''), +(690,689,'鸡冠','鸡冠区','黑龙江省,鸡西市,鸡冠区',3,'','','',''), +(691,689,'恒山','恒山区','黑龙江省,鸡西市,恒山区',3,'','','',''), +(692,689,'滴道','滴道区','黑龙江省,鸡西市,滴道区',3,'','','',''), +(693,689,'梨树','梨树区','黑龙江省,鸡西市,梨树区',3,'','','',''), +(694,689,'城子河','城子河区','黑龙江省,鸡西市,城子河区',3,'','','',''), +(695,689,'麻山','麻山区','黑龙江省,鸡西市,麻山区',3,'','','',''), +(696,689,'鸡东','鸡东县','黑龙江省,鸡西市,鸡东县',3,'','','',''), +(697,689,'虎林','虎林市','黑龙江省,鸡西市,虎林市',3,'','','',''), +(698,689,'密山','密山市','黑龙江省,鸡西市,密山市',3,'','','',''), +(699,652,'鹤岗','鹤岗市','黑龙江省,鹤岗市',2,'','','',''), +(700,699,'向阳','向阳区','黑龙江省,鹤岗市,向阳区',3,'','','',''), +(701,699,'工农','工农区','黑龙江省,鹤岗市,工农区',3,'','','',''), +(702,699,'南山','南山区','黑龙江省,鹤岗市,南山区',3,'','','',''), +(703,699,'兴安','兴安区','黑龙江省,鹤岗市,兴安区',3,'','','',''), +(704,699,'东山','东山区','黑龙江省,鹤岗市,东山区',3,'','','',''), +(705,699,'兴山','兴山区','黑龙江省,鹤岗市,兴山区',3,'','','',''), +(706,699,'萝北','萝北县','黑龙江省,鹤岗市,萝北县',3,'','','',''), +(707,699,'绥滨','绥滨县','黑龙江省,鹤岗市,绥滨县',3,'','','',''), +(708,652,'双鸭山','双鸭山市','黑龙江省,双鸭山市',2,'','','',''), +(709,708,'尖山','尖山区','黑龙江省,双鸭山市,尖山区',3,'','','',''), +(710,708,'岭东','岭东区','黑龙江省,双鸭山市,岭东区',3,'','','',''), +(711,708,'四方台','四方台区','黑龙江省,双鸭山市,四方台区',3,'','','',''), +(712,708,'宝山','宝山区','黑龙江省,双鸭山市,宝山区',3,'','','',''), +(713,708,'集贤','集贤县','黑龙江省,双鸭山市,集贤县',3,'','','',''), +(714,708,'友谊','友谊县','黑龙江省,双鸭山市,友谊县',3,'','','',''), +(715,708,'宝清','宝清县','黑龙江省,双鸭山市,宝清县',3,'','','',''), +(716,708,'饶河','饶河县','黑龙江省,双鸭山市,饶河县',3,'','','',''), +(717,652,'大庆','大庆市','黑龙江省,大庆市',2,'','','',''), +(718,717,'萨尔图','萨尔图区','黑龙江省,大庆市,萨尔图区',3,'','','',''), +(719,717,'龙凤','龙凤区','黑龙江省,大庆市,龙凤区',3,'','','',''), +(720,717,'让胡路','让胡路区','黑龙江省,大庆市,让胡路区',3,'','','',''), +(721,717,'红岗','红岗区','黑龙江省,大庆市,红岗区',3,'','','',''), +(722,717,'大同','大同区','黑龙江省,大庆市,大同区',3,'','','',''), +(723,717,'肇州','肇州县','黑龙江省,大庆市,肇州县',3,'','','',''), +(724,717,'肇源','肇源县','黑龙江省,大庆市,肇源县',3,'','','',''), +(725,717,'林甸','林甸县','黑龙江省,大庆市,林甸县',3,'','','',''), +(726,717,'杜尔伯特蒙古族','杜尔伯特蒙古族自治县','黑龙江省,大庆市,杜尔伯特蒙古族自治县',3,'','','',''), +(727,652,'伊春','伊春市','黑龙江省,伊春市',2,'','','',''), +(728,727,'伊春','伊春区','黑龙江省,伊春市,伊春区',3,'','','',''), +(729,727,'南岔','南岔区','黑龙江省,伊春市,南岔区',3,'','','',''), +(730,727,'友好','友好区','黑龙江省,伊春市,友好区',3,'','','',''), +(731,727,'西','西林区','黑龙江省,伊春市,西林区',3,'','','',''), +(732,727,'翠峦','翠峦区','黑龙江省,伊春市,翠峦区',3,'','','',''), +(733,727,'新青','新青区','黑龙江省,伊春市,新青区',3,'','','',''), +(734,727,'美溪','美溪区','黑龙江省,伊春市,美溪区',3,'','','',''), +(735,727,'金山屯','金山屯区','黑龙江省,伊春市,金山屯区',3,'','','',''), +(736,727,'五营','五营区','黑龙江省,伊春市,五营区',3,'','','',''), +(737,727,'乌马河','乌马河区','黑龙江省,伊春市,乌马河区',3,'','','',''), +(738,727,'汤旺河','汤旺河区','黑龙江省,伊春市,汤旺河区',3,'','','',''), +(739,727,'带岭','带岭区','黑龙江省,伊春市,带岭区',3,'','','',''), +(740,727,'乌伊岭','乌伊岭区','黑龙江省,伊春市,乌伊岭区',3,'','','',''), +(741,727,'红星','红星区','黑龙江省,伊春市,红星区',3,'','','',''), +(742,727,'上甘岭','上甘岭区','黑龙江省,伊春市,上甘岭区',3,'','','',''), +(743,727,'嘉荫','嘉荫县','黑龙江省,伊春市,嘉荫县',3,'','','',''), +(744,727,'铁力','铁力市','黑龙江省,伊春市,铁力市',3,'','','',''), +(745,652,'佳木斯','佳木斯市','黑龙江省,佳木斯市',2,'','','',''), +(746,745,'向阳','向阳区','黑龙江省,佳木斯市,向阳区',3,'','','',''), +(747,745,'前进','前进区','黑龙江省,佳木斯市,前进区',3,'','','',''), +(748,745,'东风','东风区','黑龙江省,佳木斯市,东风区',3,'','','',''), +(749,745,'郊','郊区','黑龙江省,佳木斯市,郊区',3,'','','',''), +(750,745,'桦南','桦南县','黑龙江省,佳木斯市,桦南县',3,'','','',''), +(751,745,'桦川','桦川县','黑龙江省,佳木斯市,桦川县',3,'','','',''), +(752,745,'汤原','汤原县','黑龙江省,佳木斯市,汤原县',3,'','','',''), +(753,745,'抚远','抚远县','黑龙江省,佳木斯市,抚远县',3,'','','',''), +(754,745,'同江','同江市','黑龙江省,佳木斯市,同江市',3,'','','',''), +(755,745,'富锦','富锦市','黑龙江省,佳木斯市,富锦市',3,'','','',''), +(756,652,'七台河','七台河市','黑龙江省,七台河市',2,'','','',''), +(757,756,'新兴','新兴区','黑龙江省,七台河市,新兴区',3,'','','',''), +(758,756,'桃山','桃山区','黑龙江省,七台河市,桃山区',3,'','','',''), +(759,756,'茄子河','茄子河区','黑龙江省,七台河市,茄子河区',3,'','','',''), +(760,756,'勃利','勃利县','黑龙江省,七台河市,勃利县',3,'','','',''), +(761,652,'牡丹江','牡丹江市','黑龙江省,牡丹江市',2,'','','',''), +(762,761,'东安','东安区','黑龙江省,牡丹江市,东安区',3,'','','',''), +(763,761,'阳明','阳明区','黑龙江省,牡丹江市,阳明区',3,'','','',''), +(764,761,'爱民','爱民区','黑龙江省,牡丹江市,爱民区',3,'','','',''), +(765,761,'西安','西安区','黑龙江省,牡丹江市,西安区',3,'','','',''), +(766,761,'东宁','东宁县','黑龙江省,牡丹江市,东宁县',3,'','','',''), +(767,761,'林口','林口县','黑龙江省,牡丹江市,林口县',3,'','','',''), +(768,761,'绥芬河','绥芬河市','黑龙江省,牡丹江市,绥芬河市',3,'','','',''), +(769,761,'海林','海林市','黑龙江省,牡丹江市,海林市',3,'','','',''), +(770,761,'宁安','宁安市','黑龙江省,牡丹江市,宁安市',3,'','','',''), +(771,761,'穆棱','穆棱市','黑龙江省,牡丹江市,穆棱市',3,'','','',''), +(772,652,'黑河','黑河市','黑龙江省,黑河市',2,'','','',''), +(773,772,'爱辉','爱辉区','黑龙江省,黑河市,爱辉区',3,'','','',''), +(774,772,'嫩江','嫩江县','黑龙江省,黑河市,嫩江县',3,'','','',''), +(775,772,'逊克','逊克县','黑龙江省,黑河市,逊克县',3,'','','',''), +(776,772,'孙吴','孙吴县','黑龙江省,黑河市,孙吴县',3,'','','',''), +(777,772,'北安','北安市','黑龙江省,黑河市,北安市',3,'','','',''), +(778,772,'五大连池','五大连池市','黑龙江省,黑河市,五大连池市',3,'','','',''), +(779,652,'绥化','绥化市','黑龙江省,绥化市',2,'','','',''), +(780,779,'北','北林区','黑龙江省,绥化市,北林区',3,'','','',''), +(781,779,'望奎','望奎县','黑龙江省,绥化市,望奎县',3,'','','',''), +(782,779,'兰西','兰西县','黑龙江省,绥化市,兰西县',3,'','','',''), +(783,779,'青冈','青冈县','黑龙江省,绥化市,青冈县',3,'','','',''), +(784,779,'庆安','庆安县','黑龙江省,绥化市,庆安县',3,'','','',''), +(785,779,'明水','明水县','黑龙江省,绥化市,明水县',3,'','','',''), +(786,779,'绥棱','绥棱县','黑龙江省,绥化市,绥棱县',3,'','','',''), +(787,779,'安达','安达市','黑龙江省,绥化市,安达市',3,'','','',''), +(788,779,'肇东','肇东市','黑龙江省,绥化市,肇东市',3,'','','',''), +(789,779,'海伦','海伦市','黑龙江省,绥化市,海伦市',3,'','','',''), +(790,652,'大兴安岭','大兴安岭地区','黑龙江省,大兴安岭地区',2,'','','',''), +(791,790,'呼玛','呼玛县','黑龙江省,大兴安岭地区,呼玛县',3,'','','',''), +(792,790,'塔河','塔河县','黑龙江省,大兴安岭地区,塔河县',3,'','','',''), +(793,790,'漠河','漠河县','黑龙江省,大兴安岭地区,漠河县',3,'','','',''), +(794,0,'上海','上海市','上海市',1,'','','',''), +(795,794,'上海','上海市','上海市,上海市',2,'','','',''), +(796,795,'黄浦','黄浦区','上海市,上海市,黄浦区',3,'','','',''), +(797,795,'徐汇','徐汇区','上海市,上海市,徐汇区',3,'','','',''), +(798,795,'长宁','长宁区','上海市,上海市,长宁区',3,'','','',''), +(799,795,'静安','静安区','上海市,上海市,静安区',3,'','','',''), +(800,795,'普陀','普陀区','上海市,上海市,普陀区',3,'','','',''), +(801,795,'闸北','闸北区','上海市,上海市,闸北区',3,'','','',''), +(802,795,'虹口','虹口区','上海市,上海市,虹口区',3,'','','',''), +(803,795,'杨浦','杨浦区','上海市,上海市,杨浦区',3,'','','',''), +(804,795,'闵行','闵行区','上海市,上海市,闵行区',3,'','','',''), +(805,795,'宝山','宝山区','上海市,上海市,宝山区',3,'','','',''), +(806,795,'嘉定','嘉定区','上海市,上海市,嘉定区',3,'','','',''), +(807,795,'浦东新','浦东新区','上海市,上海市,浦东新区',3,'','','',''), +(808,795,'金山','金山区','上海市,上海市,金山区',3,'','','',''), +(809,795,'松江','松江区','上海市,上海市,松江区',3,'','','',''), +(810,795,'青浦','青浦区','上海市,上海市,青浦区',3,'','','',''), +(811,795,'奉贤','奉贤区','上海市,上海市,奉贤区',3,'','','',''), +(812,795,'崇明','崇明县','上海市,上海市,崇明县',3,'','','',''), +(813,0,'江苏','江苏省','江苏省',1,'','','',''), +(814,813,'南京','南京市','江苏省,南京市',2,'','','',''), +(815,814,'玄武','玄武区','江苏省,南京市,玄武区',3,'','','',''), +(816,814,'秦淮','秦淮区','江苏省,南京市,秦淮区',3,'','','',''), +(817,814,'建邺','建邺区','江苏省,南京市,建邺区',3,'','','',''), +(818,814,'鼓楼','鼓楼区','江苏省,南京市,鼓楼区',3,'','','',''), +(819,814,'浦口','浦口区','江苏省,南京市,浦口区',3,'','','',''), +(820,814,'栖霞','栖霞区','江苏省,南京市,栖霞区',3,'','','',''), +(821,814,'雨花台','雨花台区','江苏省,南京市,雨花台区',3,'','','',''), +(822,814,'江宁','江宁区','江苏省,南京市,江宁区',3,'','','',''), +(823,814,'六合','六合区','江苏省,南京市,六合区',3,'','','',''), +(824,814,'溧水','溧水区','江苏省,南京市,溧水区',3,'','','',''), +(825,814,'高淳','高淳区','江苏省,南京市,高淳区',3,'','','',''), +(826,813,'无锡','无锡市','江苏省,无锡市',2,'','','',''), +(827,826,'崇安','崇安区','江苏省,无锡市,崇安区',3,'','','',''), +(828,826,'南长','南长区','江苏省,无锡市,南长区',3,'','','',''), +(829,826,'北塘','北塘区','江苏省,无锡市,北塘区',3,'','','',''), +(830,826,'锡山','锡山区','江苏省,无锡市,锡山区',3,'','','',''), +(831,826,'惠山','惠山区','江苏省,无锡市,惠山区',3,'','','',''), +(832,826,'滨湖','滨湖区','江苏省,无锡市,滨湖区',3,'','','',''), +(833,826,'江阴','江阴市','江苏省,无锡市,江阴市',3,'','','',''), +(834,826,'宜兴','宜兴市','江苏省,无锡市,宜兴市',3,'','','',''), +(835,813,'徐州','徐州市','江苏省,徐州市',2,'','','',''), +(836,835,'鼓楼','鼓楼区','江苏省,徐州市,鼓楼区',3,'','','',''), +(837,835,'云龙','云龙区','江苏省,徐州市,云龙区',3,'','','',''), +(838,835,'贾汪','贾汪区','江苏省,徐州市,贾汪区',3,'','','',''), +(839,835,'泉山','泉山区','江苏省,徐州市,泉山区',3,'','','',''), +(840,835,'铜山','铜山区','江苏省,徐州市,铜山区',3,'','','',''), +(841,835,'丰','丰县','江苏省,徐州市,丰县',3,'','','',''), +(842,835,'沛','沛县','江苏省,徐州市,沛县',3,'','','',''), +(843,835,'睢宁','睢宁县','江苏省,徐州市,睢宁县',3,'','','',''), +(844,835,'新沂','新沂市','江苏省,徐州市,新沂市',3,'','','',''), +(845,835,'邳州','邳州市','江苏省,徐州市,邳州市',3,'','','',''), +(846,813,'常州','常州市','江苏省,常州市',2,'','','',''), +(847,846,'天宁','天宁区','江苏省,常州市,天宁区',3,'','','',''), +(848,846,'钟楼','钟楼区','江苏省,常州市,钟楼区',3,'','','',''), +(849,846,'戚墅堰','戚墅堰区','江苏省,常州市,戚墅堰区',3,'','','',''), +(850,846,'新北','新北区','江苏省,常州市,新北区',3,'','','',''), +(851,846,'武进','武进区','江苏省,常州市,武进区',3,'','','',''), +(852,846,'溧阳','溧阳市','江苏省,常州市,溧阳市',3,'','','',''), +(853,846,'金坛','金坛市','江苏省,常州市,金坛市',3,'','','',''), +(854,813,'苏州','苏州市','江苏省,苏州市',2,'','','',''), +(855,854,'虎丘','虎丘区','江苏省,苏州市,虎丘区',3,'','','',''), +(856,854,'吴中','吴中区','江苏省,苏州市,吴中区',3,'','','',''), +(857,854,'相城','相城区','江苏省,苏州市,相城区',3,'','','',''), +(858,854,'姑苏','姑苏区','江苏省,苏州市,姑苏区',3,'','','',''), +(859,854,'吴江','吴江区','江苏省,苏州市,吴江区',3,'','','',''), +(860,854,'常熟','常熟市','江苏省,苏州市,常熟市',3,'','','',''), +(861,854,'张家港','张家港市','江苏省,苏州市,张家港市',3,'','','',''), +(862,854,'昆山','昆山市','江苏省,苏州市,昆山市',3,'','','',''), +(863,854,'太仓','太仓市','江苏省,苏州市,太仓市',3,'','','',''), +(864,813,'南通','南通市','江苏省,南通市',2,'','','',''), +(865,864,'崇川','崇川区','江苏省,南通市,崇川区',3,'','','',''), +(866,864,'港闸','港闸区','江苏省,南通市,港闸区',3,'','','',''), +(867,864,'通州','通州区','江苏省,南通市,通州区',3,'','','',''), +(868,864,'海安','海安县','江苏省,南通市,海安县',3,'','','',''), +(869,864,'如东','如东县','江苏省,南通市,如东县',3,'','','',''), +(870,864,'启东','启东市','江苏省,南通市,启东市',3,'','','',''), +(871,864,'如皋','如皋市','江苏省,南通市,如皋市',3,'','','',''), +(872,864,'海门','海门市','江苏省,南通市,海门市',3,'','','',''), +(873,813,'连云港','连云港市','江苏省,连云港市',2,'','','',''), +(874,873,'连云','连云区','江苏省,连云港市,连云区',3,'','','',''), +(875,873,'新浦','新浦区','江苏省,连云港市,新浦区',3,'','','',''), +(876,873,'海州','海州区','江苏省,连云港市,海州区',3,'','','',''), +(877,873,'赣榆','赣榆县','江苏省,连云港市,赣榆县',3,'','','',''), +(878,873,'东海','东海县','江苏省,连云港市,东海县',3,'','','',''), +(879,873,'灌云','灌云县','江苏省,连云港市,灌云县',3,'','','',''), +(880,873,'灌南','灌南县','江苏省,连云港市,灌南县',3,'','','',''), +(881,813,'淮安','淮安市','江苏省,淮安市',2,'','','',''), +(882,881,'清河','清河区','江苏省,淮安市,清河区',3,'','','',''), +(883,881,'淮安','淮安区','江苏省,淮安市,淮安区',3,'','','',''), +(884,881,'淮阴','淮阴区','江苏省,淮安市,淮阴区',3,'','','',''), +(885,881,'清浦','清浦区','江苏省,淮安市,清浦区',3,'','','',''), +(886,881,'涟水','涟水县','江苏省,淮安市,涟水县',3,'','','',''), +(887,881,'洪泽','洪泽县','江苏省,淮安市,洪泽县',3,'','','',''), +(888,881,'盱眙','盱眙县','江苏省,淮安市,盱眙县',3,'','','',''), +(889,881,'金湖','金湖县','江苏省,淮安市,金湖县',3,'','','',''), +(890,813,'盐城','盐城市','江苏省,盐城市',2,'','','',''), +(891,890,'亭湖','亭湖区','江苏省,盐城市,亭湖区',3,'','','',''), +(892,890,'盐都','盐都区','江苏省,盐城市,盐都区',3,'','','',''), +(893,890,'响水','响水县','江苏省,盐城市,响水县',3,'','','',''), +(894,890,'滨海','滨海县','江苏省,盐城市,滨海县',3,'','','',''), +(895,890,'阜宁','阜宁县','江苏省,盐城市,阜宁县',3,'','','',''), +(896,890,'射阳','射阳县','江苏省,盐城市,射阳县',3,'','','',''), +(897,890,'建湖','建湖县','江苏省,盐城市,建湖县',3,'','','',''), +(898,890,'东台','东台市','江苏省,盐城市,东台市',3,'','','',''), +(899,890,'大丰','大丰市','江苏省,盐城市,大丰市',3,'','','',''), +(900,813,'扬州','扬州市','江苏省,扬州市',2,'','','',''); +INSERT IGNORE INTO `fa_area` (`id`,`pid`,`shortname`,`name`,`mergename`,`level`,`pinyin`,`code`,`zip`,`first`) VALUES +(901,900,'广陵','广陵区','江苏省,扬州市,广陵区',3,'','','',''), +(902,900,'邗江','邗江区','江苏省,扬州市,邗江区',3,'','','',''), +(903,900,'江都','江都区','江苏省,扬州市,江都区',3,'','','',''), +(904,900,'宝应','宝应县','江苏省,扬州市,宝应县',3,'','','',''), +(905,900,'仪征','仪征市','江苏省,扬州市,仪征市',3,'','','',''), +(906,900,'高邮','高邮市','江苏省,扬州市,高邮市',3,'','','',''), +(907,813,'镇江','镇江市','江苏省,镇江市',2,'','','',''), +(908,907,'京口','京口区','江苏省,镇江市,京口区',3,'','','',''), +(909,907,'润州','润州区','江苏省,镇江市,润州区',3,'','','',''), +(910,907,'丹徒','丹徒区','江苏省,镇江市,丹徒区',3,'','','',''), +(911,907,'丹阳','丹阳市','江苏省,镇江市,丹阳市',3,'','','',''), +(912,907,'扬中','扬中市','江苏省,镇江市,扬中市',3,'','','',''), +(913,907,'句容','句容市','江苏省,镇江市,句容市',3,'','','',''), +(914,813,'泰州','泰州市','江苏省,泰州市',2,'','','',''), +(915,914,'海陵','海陵区','江苏省,泰州市,海陵区',3,'','','',''), +(916,914,'高港','高港区','江苏省,泰州市,高港区',3,'','','',''), +(917,914,'姜堰','姜堰区','江苏省,泰州市,姜堰区',3,'','','',''), +(918,914,'兴化','兴化市','江苏省,泰州市,兴化市',3,'','','',''), +(919,914,'靖江','靖江市','江苏省,泰州市,靖江市',3,'','','',''), +(920,914,'泰兴','泰兴市','江苏省,泰州市,泰兴市',3,'','','',''), +(921,813,'宿迁','宿迁市','江苏省,宿迁市',2,'','','',''), +(922,921,'宿城','宿城区','江苏省,宿迁市,宿城区',3,'','','',''), +(923,921,'宿豫','宿豫区','江苏省,宿迁市,宿豫区',3,'','','',''), +(924,921,'沭阳','沭阳县','江苏省,宿迁市,沭阳县',3,'','','',''), +(925,921,'泗阳','泗阳县','江苏省,宿迁市,泗阳县',3,'','','',''), +(926,921,'泗洪','泗洪县','江苏省,宿迁市,泗洪县',3,'','','',''), +(927,0,'浙江','浙江省','浙江省',1,'','','',''), +(928,927,'杭州','杭州市','浙江省,杭州市',2,'','','',''), +(929,928,'上城','上城区','浙江省,杭州市,上城区',3,'','','',''), +(930,928,'下城','下城区','浙江省,杭州市,下城区',3,'','','',''), +(931,928,'江干','江干区','浙江省,杭州市,江干区',3,'','','',''), +(932,928,'拱墅','拱墅区','浙江省,杭州市,拱墅区',3,'','','',''), +(933,928,'西湖','西湖区','浙江省,杭州市,西湖区',3,'','','',''), +(934,928,'滨江','滨江区','浙江省,杭州市,滨江区',3,'','','',''), +(935,928,'萧山','萧山区','浙江省,杭州市,萧山区',3,'','','',''), +(936,928,'余杭','余杭区','浙江省,杭州市,余杭区',3,'','','',''), +(937,928,'桐庐','桐庐县','浙江省,杭州市,桐庐县',3,'','','',''), +(938,928,'淳安','淳安县','浙江省,杭州市,淳安县',3,'','','',''), +(939,928,'建德','建德市','浙江省,杭州市,建德市',3,'','','',''), +(940,928,'富阳','富阳市','浙江省,杭州市,富阳市',3,'','','',''), +(941,928,'临安','临安市','浙江省,杭州市,临安市',3,'','','',''), +(942,927,'宁波','宁波市','浙江省,宁波市',2,'','','',''), +(943,942,'海曙','海曙区','浙江省,宁波市,海曙区',3,'','','',''), +(944,942,'江东','江东区','浙江省,宁波市,江东区',3,'','','',''), +(945,942,'江北','江北区','浙江省,宁波市,江北区',3,'','','',''), +(946,942,'北仑','北仑区','浙江省,宁波市,北仑区',3,'','','',''), +(947,942,'镇海','镇海区','浙江省,宁波市,镇海区',3,'','','',''), +(948,942,'鄞州','鄞州区','浙江省,宁波市,鄞州区',3,'','','',''), +(949,942,'象山','象山县','浙江省,宁波市,象山县',3,'','','',''), +(950,942,'宁海','宁海县','浙江省,宁波市,宁海县',3,'','','',''), +(951,942,'余姚','余姚市','浙江省,宁波市,余姚市',3,'','','',''), +(952,942,'慈溪','慈溪市','浙江省,宁波市,慈溪市',3,'','','',''), +(953,942,'奉化','奉化市','浙江省,宁波市,奉化市',3,'','','',''), +(954,927,'温州','温州市','浙江省,温州市',2,'','','',''), +(955,954,'鹿城','鹿城区','浙江省,温州市,鹿城区',3,'','','',''), +(956,954,'龙湾','龙湾区','浙江省,温州市,龙湾区',3,'','','',''), +(957,954,'瓯海','瓯海区','浙江省,温州市,瓯海区',3,'','','',''), +(958,954,'洞头','洞头县','浙江省,温州市,洞头县',3,'','','',''), +(959,954,'永嘉','永嘉县','浙江省,温州市,永嘉县',3,'','','',''), +(960,954,'平阳','平阳县','浙江省,温州市,平阳县',3,'','','',''), +(961,954,'苍南','苍南县','浙江省,温州市,苍南县',3,'','','',''), +(962,954,'文成','文成县','浙江省,温州市,文成县',3,'','','',''), +(963,954,'泰顺','泰顺县','浙江省,温州市,泰顺县',3,'','','',''), +(964,954,'瑞安','瑞安市','浙江省,温州市,瑞安市',3,'','','',''), +(965,954,'乐清','乐清市','浙江省,温州市,乐清市',3,'','','',''), +(966,927,'嘉兴','嘉兴市','浙江省,嘉兴市',2,'','','',''), +(967,966,'南湖','南湖区','浙江省,嘉兴市,南湖区',3,'','','',''), +(968,966,'秀洲','秀洲区','浙江省,嘉兴市,秀洲区',3,'','','',''), +(969,966,'嘉善','嘉善县','浙江省,嘉兴市,嘉善县',3,'','','',''), +(970,966,'海盐','海盐县','浙江省,嘉兴市,海盐县',3,'','','',''), +(971,966,'海宁','海宁市','浙江省,嘉兴市,海宁市',3,'','','',''), +(972,966,'平湖','平湖市','浙江省,嘉兴市,平湖市',3,'','','',''), +(973,966,'桐乡','桐乡市','浙江省,嘉兴市,桐乡市',3,'','','',''), +(974,927,'湖州','湖州市','浙江省,湖州市',2,'','','',''), +(975,974,'吴兴','吴兴区','浙江省,湖州市,吴兴区',3,'','','',''), +(976,974,'南浔','南浔区','浙江省,湖州市,南浔区',3,'','','',''), +(977,974,'德清','德清县','浙江省,湖州市,德清县',3,'','','',''), +(978,974,'长兴','长兴县','浙江省,湖州市,长兴县',3,'','','',''), +(979,974,'安吉','安吉县','浙江省,湖州市,安吉县',3,'','','',''), +(980,927,'绍兴','绍兴市','浙江省,绍兴市',2,'','','',''), +(981,980,'越城','越城区','浙江省,绍兴市,越城区',3,'','','',''), +(982,980,'绍兴','绍兴县','浙江省,绍兴市,绍兴县',3,'','','',''), +(983,980,'新昌','新昌县','浙江省,绍兴市,新昌县',3,'','','',''), +(984,980,'诸暨','诸暨市','浙江省,绍兴市,诸暨市',3,'','','',''), +(985,980,'上虞','上虞市','浙江省,绍兴市,上虞市',3,'','','',''), +(986,980,'嵊州','嵊州市','浙江省,绍兴市,嵊州市',3,'','','',''), +(987,927,'金华','金华市','浙江省,金华市',2,'','','',''), +(988,987,'婺城','婺城区','浙江省,金华市,婺城区',3,'','','',''), +(989,987,'金东','金东区','浙江省,金华市,金东区',3,'','','',''), +(990,987,'武义','武义县','浙江省,金华市,武义县',3,'','','',''), +(991,987,'浦江','浦江县','浙江省,金华市,浦江县',3,'','','',''), +(992,987,'磐安','磐安县','浙江省,金华市,磐安县',3,'','','',''), +(993,987,'兰溪','兰溪市','浙江省,金华市,兰溪市',3,'','','',''), +(994,987,'义乌','义乌市','浙江省,金华市,义乌市',3,'','','',''), +(995,987,'东阳','东阳市','浙江省,金华市,东阳市',3,'','','',''), +(996,987,'永康','永康市','浙江省,金华市,永康市',3,'','','',''), +(997,927,'衢州','衢州市','浙江省,衢州市',2,'','','',''), +(998,997,'柯城','柯城区','浙江省,衢州市,柯城区',3,'','','',''), +(999,997,'衢江','衢江区','浙江省,衢州市,衢江区',3,'','','',''), +(1000,997,'常山','常山县','浙江省,衢州市,常山县',3,'','','',''), +(1001,997,'开化','开化县','浙江省,衢州市,开化县',3,'','','',''), +(1002,997,'龙游','龙游县','浙江省,衢州市,龙游县',3,'','','',''), +(1003,997,'江山','江山市','浙江省,衢州市,江山市',3,'','','',''), +(1004,927,'舟山','舟山市','浙江省,舟山市',2,'','','',''), +(1005,1004,'定海','定海区','浙江省,舟山市,定海区',3,'','','',''), +(1006,1004,'普陀','普陀区','浙江省,舟山市,普陀区',3,'','','',''), +(1007,1004,'岱山','岱山县','浙江省,舟山市,岱山县',3,'','','',''), +(1008,1004,'嵊泗','嵊泗县','浙江省,舟山市,嵊泗县',3,'','','',''), +(1009,927,'台州','台州市','浙江省,台州市',2,'','','',''), +(1010,1009,'椒江','椒江区','浙江省,台州市,椒江区',3,'','','',''), +(1011,1009,'黄岩','黄岩区','浙江省,台州市,黄岩区',3,'','','',''), +(1012,1009,'路桥','路桥区','浙江省,台州市,路桥区',3,'','','',''), +(1013,1009,'玉环','玉环县','浙江省,台州市,玉环县',3,'','','',''), +(1014,1009,'三门','三门县','浙江省,台州市,三门县',3,'','','',''), +(1015,1009,'天台','天台县','浙江省,台州市,天台县',3,'','','',''), +(1016,1009,'仙居','仙居县','浙江省,台州市,仙居县',3,'','','',''), +(1017,1009,'温岭','温岭市','浙江省,台州市,温岭市',3,'','','',''), +(1018,1009,'临海','临海市','浙江省,台州市,临海市',3,'','','',''), +(1019,927,'丽水','丽水市','浙江省,丽水市',2,'','','',''), +(1020,1019,'莲都','莲都区','浙江省,丽水市,莲都区',3,'','','',''), +(1021,1019,'青田','青田县','浙江省,丽水市,青田县',3,'','','',''), +(1022,1019,'缙云','缙云县','浙江省,丽水市,缙云县',3,'','','',''), +(1023,1019,'遂昌','遂昌县','浙江省,丽水市,遂昌县',3,'','','',''), +(1024,1019,'松阳','松阳县','浙江省,丽水市,松阳县',3,'','','',''), +(1025,1019,'云和','云和县','浙江省,丽水市,云和县',3,'','','',''), +(1026,1019,'庆元','庆元县','浙江省,丽水市,庆元县',3,'','','',''), +(1027,1019,'景宁畲族','景宁畲族自治县','浙江省,丽水市,景宁畲族自治县',3,'','','',''), +(1028,1019,'龙泉','龙泉市','浙江省,丽水市,龙泉市',3,'','','',''), +(1029,0,'安徽','安徽省','安徽省',1,'','','',''), +(1030,1029,'合肥','合肥市','安徽省,合肥市',2,'','','',''), +(1031,1030,'瑶海','瑶海区','安徽省,合肥市,瑶海区',3,'','','',''), +(1032,1030,'庐阳','庐阳区','安徽省,合肥市,庐阳区',3,'','','',''), +(1033,1030,'蜀山','蜀山区','安徽省,合肥市,蜀山区',3,'','','',''), +(1034,1030,'包河','包河区','安徽省,合肥市,包河区',3,'','','',''), +(1035,1030,'长丰','长丰县','安徽省,合肥市,长丰县',3,'','','',''), +(1036,1030,'肥东','肥东县','安徽省,合肥市,肥东县',3,'','','',''), +(1037,1030,'肥西','肥西县','安徽省,合肥市,肥西县',3,'','','',''), +(1038,1030,'庐江','庐江县','安徽省,合肥市,庐江县',3,'','','',''), +(1039,1030,'巢湖','巢湖市','安徽省,合肥市,巢湖市',3,'','','',''), +(1040,1029,'芜湖','芜湖市','安徽省,芜湖市',2,'','','',''), +(1041,1040,'镜湖','镜湖区','安徽省,芜湖市,镜湖区',3,'','','',''), +(1042,1040,'弋江','弋江区','安徽省,芜湖市,弋江区',3,'','','',''), +(1043,1040,'鸠江','鸠江区','安徽省,芜湖市,鸠江区',3,'','','',''), +(1044,1040,'三山','三山区','安徽省,芜湖市,三山区',3,'','','',''), +(1045,1040,'芜湖','芜湖县','安徽省,芜湖市,芜湖县',3,'','','',''), +(1046,1040,'繁昌','繁昌县','安徽省,芜湖市,繁昌县',3,'','','',''), +(1047,1040,'南陵','南陵县','安徽省,芜湖市,南陵县',3,'','','',''), +(1048,1040,'无为','无为县','安徽省,芜湖市,无为县',3,'','','',''), +(1049,1029,'蚌埠','蚌埠市','安徽省,蚌埠市',2,'','','',''), +(1050,1049,'龙子湖','龙子湖区','安徽省,蚌埠市,龙子湖区',3,'','','',''), +(1051,1049,'蚌山','蚌山区','安徽省,蚌埠市,蚌山区',3,'','','',''), +(1052,1049,'禹会','禹会区','安徽省,蚌埠市,禹会区',3,'','','',''), +(1053,1049,'淮上','淮上区','安徽省,蚌埠市,淮上区',3,'','','',''), +(1054,1049,'怀远','怀远县','安徽省,蚌埠市,怀远县',3,'','','',''), +(1055,1049,'五河','五河县','安徽省,蚌埠市,五河县',3,'','','',''), +(1056,1049,'固镇','固镇县','安徽省,蚌埠市,固镇县',3,'','','',''), +(1057,1029,'淮南','淮南市','安徽省,淮南市',2,'','','',''), +(1058,1057,'大通','大通区','安徽省,淮南市,大通区',3,'','','',''), +(1059,1057,'田家庵','田家庵区','安徽省,淮南市,田家庵区',3,'','','',''), +(1060,1057,'谢家集','谢家集区','安徽省,淮南市,谢家集区',3,'','','',''), +(1061,1057,'八公山','八公山区','安徽省,淮南市,八公山区',3,'','','',''), +(1062,1057,'潘集','潘集区','安徽省,淮南市,潘集区',3,'','','',''), +(1063,1057,'凤台','凤台县','安徽省,淮南市,凤台县',3,'','','',''), +(1064,1029,'马鞍山','马鞍山市','安徽省,马鞍山市',2,'','','',''), +(1065,1064,'花山','花山区','安徽省,马鞍山市,花山区',3,'','','',''), +(1066,1064,'雨山','雨山区','安徽省,马鞍山市,雨山区',3,'','','',''), +(1067,1064,'博望','博望区','安徽省,马鞍山市,博望区',3,'','','',''), +(1068,1064,'当涂','当涂县','安徽省,马鞍山市,当涂县',3,'','','',''), +(1069,1064,'含山','含山县','安徽省,马鞍山市,含山县',3,'','','',''), +(1070,1064,'和','和县','安徽省,马鞍山市,和县',3,'','','',''), +(1071,1029,'淮北','淮北市','安徽省,淮北市',2,'','','',''), +(1072,1071,'杜集','杜集区','安徽省,淮北市,杜集区',3,'','','',''), +(1073,1071,'相山','相山区','安徽省,淮北市,相山区',3,'','','',''), +(1074,1071,'烈山','烈山区','安徽省,淮北市,烈山区',3,'','','',''), +(1075,1071,'濉溪','濉溪县','安徽省,淮北市,濉溪县',3,'','','',''), +(1076,1029,'铜陵','铜陵市','安徽省,铜陵市',2,'','','',''), +(1077,1076,'铜官山','铜官山区','安徽省,铜陵市,铜官山区',3,'','','',''), +(1078,1076,'狮子山','狮子山区','安徽省,铜陵市,狮子山区',3,'','','',''), +(1079,1076,'郊','郊区','安徽省,铜陵市,郊区',3,'','','',''), +(1080,1076,'铜陵','铜陵县','安徽省,铜陵市,铜陵县',3,'','','',''), +(1081,1029,'安庆','安庆市','安徽省,安庆市',2,'','','',''), +(1082,1081,'迎江','迎江区','安徽省,安庆市,迎江区',3,'','','',''), +(1083,1081,'大观','大观区','安徽省,安庆市,大观区',3,'','','',''), +(1084,1081,'宜秀','宜秀区','安徽省,安庆市,宜秀区',3,'','','',''), +(1085,1081,'怀宁','怀宁县','安徽省,安庆市,怀宁县',3,'','','',''), +(1086,1081,'枞阳','枞阳县','安徽省,安庆市,枞阳县',3,'','','',''), +(1087,1081,'潜山','潜山县','安徽省,安庆市,潜山县',3,'','','',''), +(1088,1081,'太湖','太湖县','安徽省,安庆市,太湖县',3,'','','',''), +(1089,1081,'宿松','宿松县','安徽省,安庆市,宿松县',3,'','','',''), +(1090,1081,'望江','望江县','安徽省,安庆市,望江县',3,'','','',''), +(1091,1081,'岳西','岳西县','安徽省,安庆市,岳西县',3,'','','',''), +(1092,1081,'桐城','桐城市','安徽省,安庆市,桐城市',3,'','','',''), +(1093,1029,'黄山','黄山市','安徽省,黄山市',2,'','','',''), +(1094,1093,'屯溪','屯溪区','安徽省,黄山市,屯溪区',3,'','','',''), +(1095,1093,'黄山','黄山区','安徽省,黄山市,黄山区',3,'','','',''), +(1096,1093,'徽州','徽州区','安徽省,黄山市,徽州区',3,'','','',''), +(1097,1093,'歙','歙县','安徽省,黄山市,歙县',3,'','','',''), +(1098,1093,'休宁','休宁县','安徽省,黄山市,休宁县',3,'','','',''), +(1099,1093,'黟','黟县','安徽省,黄山市,黟县',3,'','','',''), +(1100,1093,'祁门','祁门县','安徽省,黄山市,祁门县',3,'','','',''), +(1101,1029,'滁州','滁州市','安徽省,滁州市',2,'','','',''), +(1102,1101,'琅琊','琅琊区','安徽省,滁州市,琅琊区',3,'','','',''), +(1103,1101,'南谯','南谯区','安徽省,滁州市,南谯区',3,'','','',''), +(1104,1101,'来安','来安县','安徽省,滁州市,来安县',3,'','','',''), +(1105,1101,'全椒','全椒县','安徽省,滁州市,全椒县',3,'','','',''), +(1106,1101,'定远','定远县','安徽省,滁州市,定远县',3,'','','',''), +(1107,1101,'凤阳','凤阳县','安徽省,滁州市,凤阳县',3,'','','',''), +(1108,1101,'天长','天长市','安徽省,滁州市,天长市',3,'','','',''), +(1109,1101,'明光','明光市','安徽省,滁州市,明光市',3,'','','',''), +(1110,1029,'阜阳','阜阳市','安徽省,阜阳市',2,'','','',''), +(1111,1110,'颍州','颍州区','安徽省,阜阳市,颍州区',3,'','','',''), +(1112,1110,'颍东','颍东区','安徽省,阜阳市,颍东区',3,'','','',''), +(1113,1110,'颍泉','颍泉区','安徽省,阜阳市,颍泉区',3,'','','',''), +(1114,1110,'临泉','临泉县','安徽省,阜阳市,临泉县',3,'','','',''), +(1115,1110,'太和','太和县','安徽省,阜阳市,太和县',3,'','','',''), +(1116,1110,'阜南','阜南县','安徽省,阜阳市,阜南县',3,'','','',''), +(1117,1110,'颍上','颍上县','安徽省,阜阳市,颍上县',3,'','','',''), +(1118,1110,'界首','界首市','安徽省,阜阳市,界首市',3,'','','',''), +(1119,1029,'宿州','宿州市','安徽省,宿州市',2,'','','',''), +(1120,1119,'埇桥','埇桥区','安徽省,宿州市,埇桥区',3,'','','',''), +(1121,1119,'砀山','砀山县','安徽省,宿州市,砀山县',3,'','','',''), +(1122,1119,'萧','萧县','安徽省,宿州市,萧县',3,'','','',''), +(1123,1119,'灵璧','灵璧县','安徽省,宿州市,灵璧县',3,'','','',''), +(1124,1119,'泗','泗县','安徽省,宿州市,泗县',3,'','','',''), +(1125,1029,'六安','六安市','安徽省,六安市',2,'','','',''), +(1126,1125,'金安','金安区','安徽省,六安市,金安区',3,'','','',''), +(1127,1125,'裕安','裕安区','安徽省,六安市,裕安区',3,'','','',''), +(1128,1125,'寿','寿县','安徽省,六安市,寿县',3,'','','',''), +(1129,1125,'霍邱','霍邱县','安徽省,六安市,霍邱县',3,'','','',''), +(1130,1125,'舒城','舒城县','安徽省,六安市,舒城县',3,'','','',''), +(1131,1125,'金寨','金寨县','安徽省,六安市,金寨县',3,'','','',''), +(1132,1125,'霍山','霍山县','安徽省,六安市,霍山县',3,'','','',''), +(1133,1029,'亳州','亳州市','安徽省,亳州市',2,'','','',''), +(1134,1133,'谯城','谯城区','安徽省,亳州市,谯城区',3,'','','',''), +(1135,1133,'涡阳','涡阳县','安徽省,亳州市,涡阳县',3,'','','',''), +(1136,1133,'蒙城','蒙城县','安徽省,亳州市,蒙城县',3,'','','',''), +(1137,1133,'利辛','利辛县','安徽省,亳州市,利辛县',3,'','','',''), +(1138,1029,'池州','池州市','安徽省,池州市',2,'','','',''), +(1139,1138,'贵池','贵池区','安徽省,池州市,贵池区',3,'','','',''), +(1140,1138,'东至','东至县','安徽省,池州市,东至县',3,'','','',''), +(1141,1138,'石台','石台县','安徽省,池州市,石台县',3,'','','',''), +(1142,1138,'青阳','青阳县','安徽省,池州市,青阳县',3,'','','',''), +(1143,1029,'宣城','宣城市','安徽省,宣城市',2,'','','',''), +(1144,1143,'宣州','宣州区','安徽省,宣城市,宣州区',3,'','','',''), +(1145,1143,'郎溪','郎溪县','安徽省,宣城市,郎溪县',3,'','','',''), +(1146,1143,'广德','广德县','安徽省,宣城市,广德县',3,'','','',''), +(1147,1143,'泾','泾县','安徽省,宣城市,泾县',3,'','','',''), +(1148,1143,'绩溪','绩溪县','安徽省,宣城市,绩溪县',3,'','','',''), +(1149,1143,'旌德','旌德县','安徽省,宣城市,旌德县',3,'','','',''), +(1150,1143,'宁国','宁国市','安徽省,宣城市,宁国市',3,'','','',''), +(1151,0,'福建','福建省','福建省',1,'','','',''), +(1152,1151,'福州','福州市','福建省,福州市',2,'','','',''), +(1153,1152,'鼓楼','鼓楼区','福建省,福州市,鼓楼区',3,'','','',''), +(1154,1152,'台江','台江区','福建省,福州市,台江区',3,'','','',''), +(1155,1152,'仓山','仓山区','福建省,福州市,仓山区',3,'','','',''), +(1156,1152,'马尾','马尾区','福建省,福州市,马尾区',3,'','','',''), +(1157,1152,'晋安','晋安区','福建省,福州市,晋安区',3,'','','',''), +(1158,1152,'闽侯','闽侯县','福建省,福州市,闽侯县',3,'','','',''), +(1159,1152,'连江','连江县','福建省,福州市,连江县',3,'','','',''), +(1160,1152,'罗源','罗源县','福建省,福州市,罗源县',3,'','','',''), +(1161,1152,'闽清','闽清县','福建省,福州市,闽清县',3,'','','',''), +(1162,1152,'永泰','永泰县','福建省,福州市,永泰县',3,'','','',''), +(1163,1152,'平潭','平潭县','福建省,福州市,平潭县',3,'','','',''), +(1164,1152,'福清','福清市','福建省,福州市,福清市',3,'','','',''), +(1165,1152,'长乐','长乐市','福建省,福州市,长乐市',3,'','','',''), +(1166,1151,'厦门','厦门市','福建省,厦门市',2,'','','',''), +(1167,1166,'思明','思明区','福建省,厦门市,思明区',3,'','','',''), +(1168,1166,'海沧','海沧区','福建省,厦门市,海沧区',3,'','','',''), +(1169,1166,'湖里','湖里区','福建省,厦门市,湖里区',3,'','','',''), +(1170,1166,'集美','集美区','福建省,厦门市,集美区',3,'','','',''), +(1171,1166,'同安','同安区','福建省,厦门市,同安区',3,'','','',''), +(1172,1166,'翔安','翔安区','福建省,厦门市,翔安区',3,'','','',''), +(1173,1151,'莆田','莆田市','福建省,莆田市',2,'','','',''), +(1174,1173,'城厢','城厢区','福建省,莆田市,城厢区',3,'','','',''), +(1175,1173,'涵江','涵江区','福建省,莆田市,涵江区',3,'','','',''), +(1176,1173,'荔城','荔城区','福建省,莆田市,荔城区',3,'','','',''), +(1177,1173,'秀屿','秀屿区','福建省,莆田市,秀屿区',3,'','','',''), +(1178,1173,'仙游','仙游县','福建省,莆田市,仙游县',3,'','','',''), +(1179,1151,'三明','三明市','福建省,三明市',2,'','','',''), +(1180,1179,'梅列','梅列区','福建省,三明市,梅列区',3,'','','',''), +(1181,1179,'三元','三元区','福建省,三明市,三元区',3,'','','',''), +(1182,1179,'明溪','明溪县','福建省,三明市,明溪县',3,'','','',''), +(1183,1179,'清流','清流县','福建省,三明市,清流县',3,'','','',''), +(1184,1179,'宁化','宁化县','福建省,三明市,宁化县',3,'','','',''), +(1185,1179,'大田','大田县','福建省,三明市,大田县',3,'','','',''), +(1186,1179,'尤溪','尤溪县','福建省,三明市,尤溪县',3,'','','',''), +(1187,1179,'沙','沙县','福建省,三明市,沙县',3,'','','',''), +(1188,1179,'将乐','将乐县','福建省,三明市,将乐县',3,'','','',''), +(1189,1179,'泰宁','泰宁县','福建省,三明市,泰宁县',3,'','','',''), +(1190,1179,'建宁','建宁县','福建省,三明市,建宁县',3,'','','',''), +(1191,1179,'永安','永安市','福建省,三明市,永安市',3,'','','',''), +(1192,1151,'泉州','泉州市','福建省,泉州市',2,'','','',''), +(1193,1192,'鲤城','鲤城区','福建省,泉州市,鲤城区',3,'','','',''), +(1194,1192,'丰泽','丰泽区','福建省,泉州市,丰泽区',3,'','','',''), +(1195,1192,'洛江','洛江区','福建省,泉州市,洛江区',3,'','','',''), +(1196,1192,'泉港','泉港区','福建省,泉州市,泉港区',3,'','','',''), +(1197,1192,'惠安','惠安县','福建省,泉州市,惠安县',3,'','','',''), +(1198,1192,'安溪','安溪县','福建省,泉州市,安溪县',3,'','','',''), +(1199,1192,'永春','永春县','福建省,泉州市,永春县',3,'','','',''), +(1200,1192,'德化','德化县','福建省,泉州市,德化县',3,'','','',''); +INSERT IGNORE INTO `fa_area` (`id`,`pid`,`shortname`,`name`,`mergename`,`level`,`pinyin`,`code`,`zip`,`first`) VALUES +(1201,1192,'金门','金门县','福建省,泉州市,金门县',3,'','','',''), +(1202,1192,'石狮','石狮市','福建省,泉州市,石狮市',3,'','','',''), +(1203,1192,'晋江','晋江市','福建省,泉州市,晋江市',3,'','','',''), +(1204,1192,'南安','南安市','福建省,泉州市,南安市',3,'','','',''), +(1205,1151,'漳州','漳州市','福建省,漳州市',2,'','','',''), +(1206,1205,'芗城','芗城区','福建省,漳州市,芗城区',3,'','','',''), +(1207,1205,'龙文','龙文区','福建省,漳州市,龙文区',3,'','','',''), +(1208,1205,'云霄','云霄县','福建省,漳州市,云霄县',3,'','','',''), +(1209,1205,'漳浦','漳浦县','福建省,漳州市,漳浦县',3,'','','',''), +(1210,1205,'诏安','诏安县','福建省,漳州市,诏安县',3,'','','',''), +(1211,1205,'长泰','长泰县','福建省,漳州市,长泰县',3,'','','',''), +(1212,1205,'东山','东山县','福建省,漳州市,东山县',3,'','','',''), +(1213,1205,'南靖','南靖县','福建省,漳州市,南靖县',3,'','','',''), +(1214,1205,'平和','平和县','福建省,漳州市,平和县',3,'','','',''), +(1215,1205,'华安','华安县','福建省,漳州市,华安县',3,'','','',''), +(1216,1205,'龙海','龙海市','福建省,漳州市,龙海市',3,'','','',''), +(1217,1151,'南平','南平市','福建省,南平市',2,'','','',''), +(1218,1217,'延平','延平区','福建省,南平市,延平区',3,'','','',''), +(1219,1217,'顺昌','顺昌县','福建省,南平市,顺昌县',3,'','','',''), +(1220,1217,'浦城','浦城县','福建省,南平市,浦城县',3,'','','',''), +(1221,1217,'光泽','光泽县','福建省,南平市,光泽县',3,'','','',''), +(1222,1217,'松溪','松溪县','福建省,南平市,松溪县',3,'','','',''), +(1223,1217,'政和','政和县','福建省,南平市,政和县',3,'','','',''), +(1224,1217,'邵武','邵武市','福建省,南平市,邵武市',3,'','','',''), +(1225,1217,'武夷山','武夷山市','福建省,南平市,武夷山市',3,'','','',''), +(1226,1217,'建瓯','建瓯市','福建省,南平市,建瓯市',3,'','','',''), +(1227,1217,'建阳','建阳市','福建省,南平市,建阳市',3,'','','',''), +(1228,1151,'龙岩','龙岩市','福建省,龙岩市',2,'','','',''), +(1229,1228,'新罗','新罗区','福建省,龙岩市,新罗区',3,'','','',''), +(1230,1228,'长汀','长汀县','福建省,龙岩市,长汀县',3,'','','',''), +(1231,1228,'永定','永定县','福建省,龙岩市,永定县',3,'','','',''), +(1232,1228,'上杭','上杭县','福建省,龙岩市,上杭县',3,'','','',''), +(1233,1228,'武平','武平县','福建省,龙岩市,武平县',3,'','','',''), +(1234,1228,'连城','连城县','福建省,龙岩市,连城县',3,'','','',''), +(1235,1228,'漳平','漳平市','福建省,龙岩市,漳平市',3,'','','',''), +(1236,1151,'宁德','宁德市','福建省,宁德市',2,'','','',''), +(1237,1236,'蕉城','蕉城区','福建省,宁德市,蕉城区',3,'','','',''), +(1238,1236,'霞浦','霞浦县','福建省,宁德市,霞浦县',3,'','','',''), +(1239,1236,'古田','古田县','福建省,宁德市,古田县',3,'','','',''), +(1240,1236,'屏南','屏南县','福建省,宁德市,屏南县',3,'','','',''), +(1241,1236,'寿宁','寿宁县','福建省,宁德市,寿宁县',3,'','','',''), +(1242,1236,'周宁','周宁县','福建省,宁德市,周宁县',3,'','','',''), +(1243,1236,'柘荣','柘荣县','福建省,宁德市,柘荣县',3,'','','',''), +(1244,1236,'福安','福安市','福建省,宁德市,福安市',3,'','','',''), +(1245,1236,'福鼎','福鼎市','福建省,宁德市,福鼎市',3,'','','',''), +(1246,0,'江西','江西省','江西省',1,'','','',''), +(1247,1246,'南昌','南昌市','江西省,南昌市',2,'','','',''), +(1248,1247,'东湖','东湖区','江西省,南昌市,东湖区',3,'','','',''), +(1249,1247,'西湖','西湖区','江西省,南昌市,西湖区',3,'','','',''), +(1250,1247,'青云谱','青云谱区','江西省,南昌市,青云谱区',3,'','','',''), +(1251,1247,'湾里','湾里区','江西省,南昌市,湾里区',3,'','','',''), +(1252,1247,'青山湖','青山湖区','江西省,南昌市,青山湖区',3,'','','',''), +(1253,1247,'南昌','南昌县','江西省,南昌市,南昌县',3,'','','',''), +(1254,1247,'新建','新建县','江西省,南昌市,新建县',3,'','','',''), +(1255,1247,'安义','安义县','江西省,南昌市,安义县',3,'','','',''), +(1256,1247,'进贤','进贤县','江西省,南昌市,进贤县',3,'','','',''), +(1257,1246,'景德镇','景德镇市','江西省,景德镇市',2,'','','',''), +(1258,1257,'昌江','昌江区','江西省,景德镇市,昌江区',3,'','','',''), +(1259,1257,'珠山','珠山区','江西省,景德镇市,珠山区',3,'','','',''), +(1260,1257,'浮梁','浮梁县','江西省,景德镇市,浮梁县',3,'','','',''), +(1261,1257,'乐平','乐平市','江西省,景德镇市,乐平市',3,'','','',''), +(1262,1246,'萍乡','萍乡市','江西省,萍乡市',2,'','','',''), +(1263,1262,'安源','安源区','江西省,萍乡市,安源区',3,'','','',''), +(1264,1262,'湘东','湘东区','江西省,萍乡市,湘东区',3,'','','',''), +(1265,1262,'莲花','莲花县','江西省,萍乡市,莲花县',3,'','','',''), +(1266,1262,'上栗','上栗县','江西省,萍乡市,上栗县',3,'','','',''), +(1267,1262,'芦溪','芦溪县','江西省,萍乡市,芦溪县',3,'','','',''), +(1268,1246,'九江','九江市','江西省,九江市',2,'','','',''), +(1269,1268,'庐山','庐山区','江西省,九江市,庐山区',3,'','','',''), +(1270,1268,'浔阳','浔阳区','江西省,九江市,浔阳区',3,'','','',''), +(1271,1268,'九江','九江县','江西省,九江市,九江县',3,'','','',''), +(1272,1268,'武宁','武宁县','江西省,九江市,武宁县',3,'','','',''), +(1273,1268,'修水','修水县','江西省,九江市,修水县',3,'','','',''), +(1274,1268,'永修','永修县','江西省,九江市,永修县',3,'','','',''), +(1275,1268,'德安','德安县','江西省,九江市,德安县',3,'','','',''), +(1276,1268,'星子','星子县','江西省,九江市,星子县',3,'','','',''), +(1277,1268,'都昌','都昌县','江西省,九江市,都昌县',3,'','','',''), +(1278,1268,'湖口','湖口县','江西省,九江市,湖口县',3,'','','',''), +(1279,1268,'彭泽','彭泽县','江西省,九江市,彭泽县',3,'','','',''), +(1280,1268,'瑞昌','瑞昌市','江西省,九江市,瑞昌市',3,'','','',''), +(1281,1268,'共青城','共青城市','江西省,九江市,共青城市',3,'','','',''), +(1282,1246,'新余','新余市','江西省,新余市',2,'','','',''), +(1283,1282,'渝水','渝水区','江西省,新余市,渝水区',3,'','','',''), +(1284,1282,'分宜','分宜县','江西省,新余市,分宜县',3,'','','',''), +(1285,1246,'鹰潭','鹰潭市','江西省,鹰潭市',2,'','','',''), +(1286,1285,'月湖','月湖区','江西省,鹰潭市,月湖区',3,'','','',''), +(1287,1285,'余江','余江县','江西省,鹰潭市,余江县',3,'','','',''), +(1288,1285,'贵溪','贵溪市','江西省,鹰潭市,贵溪市',3,'','','',''), +(1289,1246,'赣州','赣州市','江西省,赣州市',2,'','','',''), +(1290,1289,'章贡','章贡区','江西省,赣州市,章贡区',3,'','','',''), +(1291,1289,'赣','赣县','江西省,赣州市,赣县',3,'','','',''), +(1292,1289,'信丰','信丰县','江西省,赣州市,信丰县',3,'','','',''), +(1293,1289,'大余','大余县','江西省,赣州市,大余县',3,'','','',''), +(1294,1289,'上犹','上犹县','江西省,赣州市,上犹县',3,'','','',''), +(1295,1289,'崇义','崇义县','江西省,赣州市,崇义县',3,'','','',''), +(1296,1289,'安远','安远县','江西省,赣州市,安远县',3,'','','',''), +(1297,1289,'龙南','龙南县','江西省,赣州市,龙南县',3,'','','',''), +(1298,1289,'定南','定南县','江西省,赣州市,定南县',3,'','','',''), +(1299,1289,'全南','全南县','江西省,赣州市,全南县',3,'','','',''), +(1300,1289,'宁都','宁都县','江西省,赣州市,宁都县',3,'','','',''), +(1301,1289,'于都','于都县','江西省,赣州市,于都县',3,'','','',''), +(1302,1289,'兴国','兴国县','江西省,赣州市,兴国县',3,'','','',''), +(1303,1289,'会昌','会昌县','江西省,赣州市,会昌县',3,'','','',''), +(1304,1289,'寻乌','寻乌县','江西省,赣州市,寻乌县',3,'','','',''), +(1305,1289,'石城','石城县','江西省,赣州市,石城县',3,'','','',''), +(1306,1289,'瑞金','瑞金市','江西省,赣州市,瑞金市',3,'','','',''), +(1307,1289,'南康','南康市','江西省,赣州市,南康市',3,'','','',''), +(1308,1246,'吉安','吉安市','江西省,吉安市',2,'','','',''), +(1309,1308,'吉州','吉州区','江西省,吉安市,吉州区',3,'','','',''), +(1310,1308,'青原','青原区','江西省,吉安市,青原区',3,'','','',''), +(1311,1308,'吉安','吉安县','江西省,吉安市,吉安县',3,'','','',''), +(1312,1308,'吉水','吉水县','江西省,吉安市,吉水县',3,'','','',''), +(1313,1308,'峡江','峡江县','江西省,吉安市,峡江县',3,'','','',''), +(1314,1308,'新干','新干县','江西省,吉安市,新干县',3,'','','',''), +(1315,1308,'永丰','永丰县','江西省,吉安市,永丰县',3,'','','',''), +(1316,1308,'泰和','泰和县','江西省,吉安市,泰和县',3,'','','',''), +(1317,1308,'遂川','遂川县','江西省,吉安市,遂川县',3,'','','',''), +(1318,1308,'万安','万安县','江西省,吉安市,万安县',3,'','','',''), +(1319,1308,'安福','安福县','江西省,吉安市,安福县',3,'','','',''), +(1320,1308,'永新','永新县','江西省,吉安市,永新县',3,'','','',''), +(1321,1308,'井冈山','井冈山市','江西省,吉安市,井冈山市',3,'','','',''), +(1322,1246,'宜春','宜春市','江西省,宜春市',2,'','','',''), +(1323,1322,'袁州','袁州区','江西省,宜春市,袁州区',3,'','','',''), +(1324,1322,'奉新','奉新县','江西省,宜春市,奉新县',3,'','','',''), +(1325,1322,'万载','万载县','江西省,宜春市,万载县',3,'','','',''), +(1326,1322,'上高','上高县','江西省,宜春市,上高县',3,'','','',''), +(1327,1322,'宜丰','宜丰县','江西省,宜春市,宜丰县',3,'','','',''), +(1328,1322,'靖安','靖安县','江西省,宜春市,靖安县',3,'','','',''), +(1329,1322,'铜鼓','铜鼓县','江西省,宜春市,铜鼓县',3,'','','',''), +(1330,1322,'丰城','丰城市','江西省,宜春市,丰城市',3,'','','',''), +(1331,1322,'樟树','樟树市','江西省,宜春市,樟树市',3,'','','',''), +(1332,1322,'高安','高安市','江西省,宜春市,高安市',3,'','','',''), +(1333,1246,'抚州','抚州市','江西省,抚州市',2,'','','',''), +(1334,1333,'临川','临川区','江西省,抚州市,临川区',3,'','','',''), +(1335,1333,'南城','南城县','江西省,抚州市,南城县',3,'','','',''), +(1336,1333,'黎川','黎川县','江西省,抚州市,黎川县',3,'','','',''), +(1337,1333,'南丰','南丰县','江西省,抚州市,南丰县',3,'','','',''), +(1338,1333,'崇仁','崇仁县','江西省,抚州市,崇仁县',3,'','','',''), +(1339,1333,'乐安','乐安县','江西省,抚州市,乐安县',3,'','','',''), +(1340,1333,'宜黄','宜黄县','江西省,抚州市,宜黄县',3,'','','',''), +(1341,1333,'金溪','金溪县','江西省,抚州市,金溪县',3,'','','',''), +(1342,1333,'资溪','资溪县','江西省,抚州市,资溪县',3,'','','',''), +(1343,1333,'东乡','东乡县','江西省,抚州市,东乡县',3,'','','',''), +(1344,1333,'广昌','广昌县','江西省,抚州市,广昌县',3,'','','',''), +(1345,1246,'上饶','上饶市','江西省,上饶市',2,'','','',''), +(1346,1345,'信州','信州区','江西省,上饶市,信州区',3,'','','',''), +(1347,1345,'上饶','上饶县','江西省,上饶市,上饶县',3,'','','',''), +(1348,1345,'广丰','广丰县','江西省,上饶市,广丰县',3,'','','',''), +(1349,1345,'玉山','玉山县','江西省,上饶市,玉山县',3,'','','',''), +(1350,1345,'铅山','铅山县','江西省,上饶市,铅山县',3,'','','',''), +(1351,1345,'横峰','横峰县','江西省,上饶市,横峰县',3,'','','',''), +(1352,1345,'弋阳','弋阳县','江西省,上饶市,弋阳县',3,'','','',''), +(1353,1345,'余干','余干县','江西省,上饶市,余干县',3,'','','',''), +(1354,1345,'鄱阳','鄱阳县','江西省,上饶市,鄱阳县',3,'','','',''), +(1355,1345,'万年','万年县','江西省,上饶市,万年县',3,'','','',''), +(1356,1345,'婺源','婺源县','江西省,上饶市,婺源县',3,'','','',''), +(1357,1345,'德兴','德兴市','江西省,上饶市,德兴市',3,'','','',''), +(1358,0,'山东','山东省','山东省',1,'','','',''), +(1359,1358,'济南','济南市','山东省,济南市',2,'','','',''), +(1360,1359,'历下','历下区','山东省,济南市,历下区',3,'','','',''), +(1361,1359,'市中','市中区','山东省,济南市,市中区',3,'','','',''), +(1362,1359,'槐荫','槐荫区','山东省,济南市,槐荫区',3,'','','',''), +(1363,1359,'天桥','天桥区','山东省,济南市,天桥区',3,'','','',''), +(1364,1359,'历城','历城区','山东省,济南市,历城区',3,'','','',''), +(1365,1359,'长清','长清区','山东省,济南市,长清区',3,'','','',''), +(1366,1359,'平阴','平阴县','山东省,济南市,平阴县',3,'','','',''), +(1367,1359,'济阳','济阳县','山东省,济南市,济阳县',3,'','','',''), +(1368,1359,'商河','商河县','山东省,济南市,商河县',3,'','','',''), +(1369,1359,'章丘','章丘市','山东省,济南市,章丘市',3,'','','',''), +(1370,1358,'青岛','青岛市','山东省,青岛市',2,'','','',''), +(1371,1370,'市南','市南区','山东省,青岛市,市南区',3,'','','',''), +(1372,1370,'市北','市北区','山东省,青岛市,市北区',3,'','','',''), +(1373,1370,'黄岛','黄岛区','山东省,青岛市,黄岛区',3,'','','',''), +(1374,1370,'崂山','崂山区','山东省,青岛市,崂山区',3,'','','',''), +(1375,1370,'李沧','李沧区','山东省,青岛市,李沧区',3,'','','',''), +(1376,1370,'城阳','城阳区','山东省,青岛市,城阳区',3,'','','',''), +(1377,1370,'胶州','胶州市','山东省,青岛市,胶州市',3,'','','',''), +(1378,1370,'即墨','即墨市','山东省,青岛市,即墨市',3,'','','',''), +(1379,1370,'平度','平度市','山东省,青岛市,平度市',3,'','','',''), +(1380,1370,'莱西','莱西市','山东省,青岛市,莱西市',3,'','','',''), +(1381,1358,'淄博','淄博市','山东省,淄博市',2,'','','',''), +(1382,1381,'淄川','淄川区','山东省,淄博市,淄川区',3,'','','',''), +(1383,1381,'张店','张店区','山东省,淄博市,张店区',3,'','','',''), +(1384,1381,'博山','博山区','山东省,淄博市,博山区',3,'','','',''), +(1385,1381,'临淄','临淄区','山东省,淄博市,临淄区',3,'','','',''), +(1386,1381,'周村','周村区','山东省,淄博市,周村区',3,'','','',''), +(1387,1381,'桓台','桓台县','山东省,淄博市,桓台县',3,'','','',''), +(1388,1381,'高青','高青县','山东省,淄博市,高青县',3,'','','',''), +(1389,1381,'沂源','沂源县','山东省,淄博市,沂源县',3,'','','',''), +(1390,1358,'枣庄','枣庄市','山东省,枣庄市',2,'','','',''), +(1391,1390,'市中','市中区','山东省,枣庄市,市中区',3,'','','',''), +(1392,1390,'薛城','薛城区','山东省,枣庄市,薛城区',3,'','','',''), +(1393,1390,'峄城','峄城区','山东省,枣庄市,峄城区',3,'','','',''), +(1394,1390,'台儿庄','台儿庄区','山东省,枣庄市,台儿庄区',3,'','','',''), +(1395,1390,'山亭','山亭区','山东省,枣庄市,山亭区',3,'','','',''), +(1396,1390,'滕州','滕州市','山东省,枣庄市,滕州市',3,'','','',''), +(1397,1358,'东营','东营市','山东省,东营市',2,'','','',''), +(1398,1397,'东营','东营区','山东省,东营市,东营区',3,'','','',''), +(1399,1397,'河口','河口区','山东省,东营市,河口区',3,'','','',''), +(1400,1397,'垦利','垦利县','山东省,东营市,垦利县',3,'','','',''), +(1401,1397,'利津','利津县','山东省,东营市,利津县',3,'','','',''), +(1402,1397,'广饶','广饶县','山东省,东营市,广饶县',3,'','','',''), +(1403,1358,'烟台','烟台市','山东省,烟台市',2,'','','',''), +(1404,1403,'芝罘','芝罘区','山东省,烟台市,芝罘区',3,'','','',''), +(1405,1403,'福山','福山区','山东省,烟台市,福山区',3,'','','',''), +(1406,1403,'牟平','牟平区','山东省,烟台市,牟平区',3,'','','',''), +(1407,1403,'莱山','莱山区','山东省,烟台市,莱山区',3,'','','',''), +(1408,1403,'长岛','长岛县','山东省,烟台市,长岛县',3,'','','',''), +(1409,1403,'龙口','龙口市','山东省,烟台市,龙口市',3,'','','',''), +(1410,1403,'莱阳','莱阳市','山东省,烟台市,莱阳市',3,'','','',''), +(1411,1403,'莱州','莱州市','山东省,烟台市,莱州市',3,'','','',''), +(1412,1403,'蓬莱','蓬莱市','山东省,烟台市,蓬莱市',3,'','','',''), +(1413,1403,'招远','招远市','山东省,烟台市,招远市',3,'','','',''), +(1414,1403,'栖霞','栖霞市','山东省,烟台市,栖霞市',3,'','','',''), +(1415,1403,'海阳','海阳市','山东省,烟台市,海阳市',3,'','','',''), +(1416,1358,'潍坊','潍坊市','山东省,潍坊市',2,'','','',''), +(1417,1416,'潍城','潍城区','山东省,潍坊市,潍城区',3,'','','',''), +(1418,1416,'寒亭','寒亭区','山东省,潍坊市,寒亭区',3,'','','',''), +(1419,1416,'坊子','坊子区','山东省,潍坊市,坊子区',3,'','','',''), +(1420,1416,'奎文','奎文区','山东省,潍坊市,奎文区',3,'','','',''), +(1421,1416,'临朐','临朐县','山东省,潍坊市,临朐县',3,'','','',''), +(1422,1416,'昌乐','昌乐县','山东省,潍坊市,昌乐县',3,'','','',''), +(1423,1416,'青州','青州市','山东省,潍坊市,青州市',3,'','','',''), +(1424,1416,'诸城','诸城市','山东省,潍坊市,诸城市',3,'','','',''), +(1425,1416,'寿光','寿光市','山东省,潍坊市,寿光市',3,'','','',''), +(1426,1416,'安丘','安丘市','山东省,潍坊市,安丘市',3,'','','',''), +(1427,1416,'高密','高密市','山东省,潍坊市,高密市',3,'','','',''), +(1428,1416,'昌邑','昌邑市','山东省,潍坊市,昌邑市',3,'','','',''), +(1429,1358,'济宁','济宁市','山东省,济宁市',2,'','','',''), +(1430,1429,'市中','市中区','山东省,济宁市,市中区',3,'','','',''), +(1431,1429,'任城','任城区','山东省,济宁市,任城区',3,'','','',''), +(1432,1429,'微山','微山县','山东省,济宁市,微山县',3,'','','',''), +(1433,1429,'鱼台','鱼台县','山东省,济宁市,鱼台县',3,'','','',''), +(1434,1429,'金乡','金乡县','山东省,济宁市,金乡县',3,'','','',''), +(1435,1429,'嘉祥','嘉祥县','山东省,济宁市,嘉祥县',3,'','','',''), +(1436,1429,'汶上','汶上县','山东省,济宁市,汶上县',3,'','','',''), +(1437,1429,'泗水','泗水县','山东省,济宁市,泗水县',3,'','','',''), +(1438,1429,'梁山','梁山县','山东省,济宁市,梁山县',3,'','','',''), +(1439,1429,'曲阜','曲阜市','山东省,济宁市,曲阜市',3,'','','',''), +(1440,1429,'兖州','兖州市','山东省,济宁市,兖州市',3,'','','',''), +(1441,1429,'邹城','邹城市','山东省,济宁市,邹城市',3,'','','',''), +(1442,1358,'泰安','泰安市','山东省,泰安市',2,'','','',''), +(1443,1442,'泰山','泰山区','山东省,泰安市,泰山区',3,'','','',''), +(1444,1442,'岱岳','岱岳区','山东省,泰安市,岱岳区',3,'','','',''), +(1445,1442,'宁阳','宁阳县','山东省,泰安市,宁阳县',3,'','','',''), +(1446,1442,'东平','东平县','山东省,泰安市,东平县',3,'','','',''), +(1447,1442,'新泰','新泰市','山东省,泰安市,新泰市',3,'','','',''), +(1448,1442,'肥城','肥城市','山东省,泰安市,肥城市',3,'','','',''), +(1449,1358,'威海','威海市','山东省,威海市',2,'','','',''), +(1450,1449,'环翠','环翠区','山东省,威海市,环翠区',3,'','','',''), +(1451,1449,'文登','文登市','山东省,威海市,文登市',3,'','','',''), +(1452,1449,'荣成','荣成市','山东省,威海市,荣成市',3,'','','',''), +(1453,1449,'乳山','乳山市','山东省,威海市,乳山市',3,'','','',''), +(1454,1358,'日照','日照市','山东省,日照市',2,'','','',''), +(1455,1454,'东港','东港区','山东省,日照市,东港区',3,'','','',''), +(1456,1454,'岚山','岚山区','山东省,日照市,岚山区',3,'','','',''), +(1457,1454,'五莲','五莲县','山东省,日照市,五莲县',3,'','','',''), +(1458,1454,'莒','莒县','山东省,日照市,莒县',3,'','','',''), +(1459,1358,'莱芜','莱芜市','山东省,莱芜市',2,'','','',''), +(1460,1459,'莱城','莱城区','山东省,莱芜市,莱城区',3,'','','',''), +(1461,1459,'钢城','钢城区','山东省,莱芜市,钢城区',3,'','','',''), +(1462,1358,'临沂','临沂市','山东省,临沂市',2,'','','',''), +(1463,1462,'兰山','兰山区','山东省,临沂市,兰山区',3,'','','',''), +(1464,1462,'罗庄','罗庄区','山东省,临沂市,罗庄区',3,'','','',''), +(1465,1462,'河东','河东区','山东省,临沂市,河东区',3,'','','',''), +(1466,1462,'沂南','沂南县','山东省,临沂市,沂南县',3,'','','',''), +(1467,1462,'郯城','郯城县','山东省,临沂市,郯城县',3,'','','',''), +(1468,1462,'沂水','沂水县','山东省,临沂市,沂水县',3,'','','',''), +(1469,1462,'苍山','苍山县','山东省,临沂市,苍山县',3,'','','',''), +(1470,1462,'费','费县','山东省,临沂市,费县',3,'','','',''), +(1471,1462,'平邑','平邑县','山东省,临沂市,平邑县',3,'','','',''), +(1472,1462,'莒南','莒南县','山东省,临沂市,莒南县',3,'','','',''), +(1473,1462,'蒙阴','蒙阴县','山东省,临沂市,蒙阴县',3,'','','',''), +(1474,1462,'临沭','临沭县','山东省,临沂市,临沭县',3,'','','',''), +(1475,1358,'德州','德州市','山东省,德州市',2,'','','',''), +(1476,1475,'德城','德城区','山东省,德州市,德城区',3,'','','',''), +(1477,1475,'陵','陵县','山东省,德州市,陵县',3,'','','',''), +(1478,1475,'宁津','宁津县','山东省,德州市,宁津县',3,'','','',''), +(1479,1475,'庆云','庆云县','山东省,德州市,庆云县',3,'','','',''), +(1480,1475,'临邑','临邑县','山东省,德州市,临邑县',3,'','','',''), +(1481,1475,'齐河','齐河县','山东省,德州市,齐河县',3,'','','',''), +(1482,1475,'平原','平原县','山东省,德州市,平原县',3,'','','',''), +(1483,1475,'夏津','夏津县','山东省,德州市,夏津县',3,'','','',''), +(1484,1475,'武城','武城县','山东省,德州市,武城县',3,'','','',''), +(1485,1475,'乐陵','乐陵市','山东省,德州市,乐陵市',3,'','','',''), +(1486,1475,'禹城','禹城市','山东省,德州市,禹城市',3,'','','',''), +(1487,1358,'聊城','聊城市','山东省,聊城市',2,'','','',''), +(1488,1487,'东昌府','东昌府区','山东省,聊城市,东昌府区',3,'','','',''), +(1489,1487,'阳谷','阳谷县','山东省,聊城市,阳谷县',3,'','','',''), +(1490,1487,'莘','莘县','山东省,聊城市,莘县',3,'','','',''), +(1491,1487,'茌平','茌平县','山东省,聊城市,茌平县',3,'','','',''), +(1492,1487,'东阿','东阿县','山东省,聊城市,东阿县',3,'','','',''), +(1493,1487,'冠','冠县','山东省,聊城市,冠县',3,'','','',''), +(1494,1487,'高唐','高唐县','山东省,聊城市,高唐县',3,'','','',''), +(1495,1487,'临清','临清市','山东省,聊城市,临清市',3,'','','',''), +(1496,1358,'滨州','滨州市','山东省,滨州市',2,'','','',''), +(1497,1496,'滨城','滨城区','山东省,滨州市,滨城区',3,'','','',''), +(1498,1496,'惠民','惠民县','山东省,滨州市,惠民县',3,'','','',''), +(1499,1496,'阳信','阳信县','山东省,滨州市,阳信县',3,'','','',''), +(1500,1496,'无棣','无棣县','山东省,滨州市,无棣县',3,'','','',''); +INSERT IGNORE INTO `fa_area` (`id`,`pid`,`shortname`,`name`,`mergename`,`level`,`pinyin`,`code`,`zip`,`first`) VALUES +(1501,1496,'沾化','沾化县','山东省,滨州市,沾化县',3,'','','',''), +(1502,1496,'博兴','博兴县','山东省,滨州市,博兴县',3,'','','',''), +(1503,1496,'邹平','邹平县','山东省,滨州市,邹平县',3,'','','',''), +(1504,1358,'菏泽','菏泽市','山东省,菏泽市',2,'','','',''), +(1505,1504,'牡丹','牡丹区','山东省,菏泽市,牡丹区',3,'','','',''), +(1506,1504,'曹','曹县','山东省,菏泽市,曹县',3,'','','',''), +(1507,1504,'单','单县','山东省,菏泽市,单县',3,'','','',''), +(1508,1504,'成武','成武县','山东省,菏泽市,成武县',3,'','','',''), +(1509,1504,'巨野','巨野县','山东省,菏泽市,巨野县',3,'','','',''), +(1510,1504,'郓城','郓城县','山东省,菏泽市,郓城县',3,'','','',''), +(1511,1504,'鄄城','鄄城县','山东省,菏泽市,鄄城县',3,'','','',''), +(1512,1504,'定陶','定陶县','山东省,菏泽市,定陶县',3,'','','',''), +(1513,1504,'东明','东明县','山东省,菏泽市,东明县',3,'','','',''), +(1514,0,'河南','河南省','河南省',1,'','','',''), +(1515,1514,'郑州','郑州市','河南省,郑州市',2,'','','',''), +(1516,1515,'中原','中原区','河南省,郑州市,中原区',3,'','','',''), +(1517,1515,'二七','二七区','河南省,郑州市,二七区',3,'','','',''), +(1518,1515,'管城回族','管城回族区','河南省,郑州市,管城回族区',3,'','','',''), +(1519,1515,'金水','金水区','河南省,郑州市,金水区',3,'','','',''), +(1520,1515,'上街','上街区','河南省,郑州市,上街区',3,'','','',''), +(1521,1515,'惠济','惠济区','河南省,郑州市,惠济区',3,'','','',''), +(1522,1515,'中牟','中牟县','河南省,郑州市,中牟县',3,'','','',''), +(1523,1515,'巩义','巩义市','河南省,郑州市,巩义市',3,'','','',''), +(1524,1515,'荥阳','荥阳市','河南省,郑州市,荥阳市',3,'','','',''), +(1525,1515,'新密','新密市','河南省,郑州市,新密市',3,'','','',''), +(1526,1515,'新郑','新郑市','河南省,郑州市,新郑市',3,'','','',''), +(1527,1515,'登封','登封市','河南省,郑州市,登封市',3,'','','',''), +(1528,1514,'开封','开封市','河南省,开封市',2,'','','',''), +(1529,1528,'龙亭','龙亭区','河南省,开封市,龙亭区',3,'','','',''), +(1530,1528,'顺河回族','顺河回族区','河南省,开封市,顺河回族区',3,'','','',''), +(1531,1528,'鼓楼','鼓楼区','河南省,开封市,鼓楼区',3,'','','',''), +(1532,1528,'禹王台','禹王台区','河南省,开封市,禹王台区',3,'','','',''), +(1533,1528,'金明','金明区','河南省,开封市,金明区',3,'','','',''), +(1534,1528,'杞','杞县','河南省,开封市,杞县',3,'','','',''), +(1535,1528,'通许','通许县','河南省,开封市,通许县',3,'','','',''), +(1536,1528,'尉氏','尉氏县','河南省,开封市,尉氏县',3,'','','',''), +(1537,1528,'开封','开封县','河南省,开封市,开封县',3,'','','',''), +(1538,1528,'兰考','兰考县','河南省,开封市,兰考县',3,'','','',''), +(1539,1514,'洛阳','洛阳市','河南省,洛阳市',2,'','','',''), +(1540,1539,'老城','老城区','河南省,洛阳市,老城区',3,'','','',''), +(1541,1539,'西工','西工区','河南省,洛阳市,西工区',3,'','','',''), +(1542,1539,'瀍河回族','瀍河回族区','河南省,洛阳市,瀍河回族区',3,'','','',''), +(1543,1539,'涧西','涧西区','河南省,洛阳市,涧西区',3,'','','',''), +(1544,1539,'吉利','吉利区','河南省,洛阳市,吉利区',3,'','','',''), +(1545,1539,'洛龙','洛龙区','河南省,洛阳市,洛龙区',3,'','','',''), +(1546,1539,'孟津','孟津县','河南省,洛阳市,孟津县',3,'','','',''), +(1547,1539,'新安','新安县','河南省,洛阳市,新安县',3,'','','',''), +(1548,1539,'栾川','栾川县','河南省,洛阳市,栾川县',3,'','','',''), +(1549,1539,'嵩','嵩县','河南省,洛阳市,嵩县',3,'','','',''), +(1550,1539,'汝阳','汝阳县','河南省,洛阳市,汝阳县',3,'','','',''), +(1551,1539,'宜阳','宜阳县','河南省,洛阳市,宜阳县',3,'','','',''), +(1552,1539,'洛宁','洛宁县','河南省,洛阳市,洛宁县',3,'','','',''), +(1553,1539,'伊川','伊川县','河南省,洛阳市,伊川县',3,'','','',''), +(1554,1539,'偃师','偃师市','河南省,洛阳市,偃师市',3,'','','',''), +(1555,1514,'平顶山','平顶山市','河南省,平顶山市',2,'','','',''), +(1556,1555,'新华','新华区','河南省,平顶山市,新华区',3,'','','',''), +(1557,1555,'卫东','卫东区','河南省,平顶山市,卫东区',3,'','','',''), +(1558,1555,'石龙','石龙区','河南省,平顶山市,石龙区',3,'','','',''), +(1559,1555,'湛河','湛河区','河南省,平顶山市,湛河区',3,'','','',''), +(1560,1555,'宝丰','宝丰县','河南省,平顶山市,宝丰县',3,'','','',''), +(1561,1555,'叶','叶县','河南省,平顶山市,叶县',3,'','','',''), +(1562,1555,'鲁山','鲁山县','河南省,平顶山市,鲁山县',3,'','','',''), +(1563,1555,'郏','郏县','河南省,平顶山市,郏县',3,'','','',''), +(1564,1555,'舞钢','舞钢市','河南省,平顶山市,舞钢市',3,'','','',''), +(1565,1555,'汝州','汝州市','河南省,平顶山市,汝州市',3,'','','',''), +(1566,1514,'安阳','安阳市','河南省,安阳市',2,'','','',''), +(1567,1566,'文峰','文峰区','河南省,安阳市,文峰区',3,'','','',''), +(1568,1566,'北关','北关区','河南省,安阳市,北关区',3,'','','',''), +(1569,1566,'殷都','殷都区','河南省,安阳市,殷都区',3,'','','',''), +(1570,1566,'龙安','龙安区','河南省,安阳市,龙安区',3,'','','',''), +(1571,1566,'安阳','安阳县','河南省,安阳市,安阳县',3,'','','',''), +(1572,1566,'汤阴','汤阴县','河南省,安阳市,汤阴县',3,'','','',''), +(1573,1566,'滑','滑县','河南省,安阳市,滑县',3,'','','',''), +(1574,1566,'内黄','内黄县','河南省,安阳市,内黄县',3,'','','',''), +(1575,1566,'林州','林州市','河南省,安阳市,林州市',3,'','','',''), +(1576,1514,'鹤壁','鹤壁市','河南省,鹤壁市',2,'','','',''), +(1577,1576,'鹤山','鹤山区','河南省,鹤壁市,鹤山区',3,'','','',''), +(1578,1576,'山城','山城区','河南省,鹤壁市,山城区',3,'','','',''), +(1579,1576,'淇滨','淇滨区','河南省,鹤壁市,淇滨区',3,'','','',''), +(1580,1576,'浚','浚县','河南省,鹤壁市,浚县',3,'','','',''), +(1581,1576,'淇','淇县','河南省,鹤壁市,淇县',3,'','','',''), +(1582,1514,'新乡','新乡市','河南省,新乡市',2,'','','',''), +(1583,1582,'红旗','红旗区','河南省,新乡市,红旗区',3,'','','',''), +(1584,1582,'卫滨','卫滨区','河南省,新乡市,卫滨区',3,'','','',''), +(1585,1582,'凤泉','凤泉区','河南省,新乡市,凤泉区',3,'','','',''), +(1586,1582,'牧野','牧野区','河南省,新乡市,牧野区',3,'','','',''), +(1587,1582,'新乡','新乡县','河南省,新乡市,新乡县',3,'','','',''), +(1588,1582,'获嘉','获嘉县','河南省,新乡市,获嘉县',3,'','','',''), +(1589,1582,'原阳','原阳县','河南省,新乡市,原阳县',3,'','','',''), +(1590,1582,'延津','延津县','河南省,新乡市,延津县',3,'','','',''), +(1591,1582,'封丘','封丘县','河南省,新乡市,封丘县',3,'','','',''), +(1592,1582,'长垣','长垣县','河南省,新乡市,长垣县',3,'','','',''), +(1593,1582,'卫辉','卫辉市','河南省,新乡市,卫辉市',3,'','','',''), +(1594,1582,'辉县','辉县市','河南省,新乡市,辉县市',3,'','','',''), +(1595,1514,'焦作','焦作市','河南省,焦作市',2,'','','',''), +(1596,1595,'解放','解放区','河南省,焦作市,解放区',3,'','','',''), +(1597,1595,'中站','中站区','河南省,焦作市,中站区',3,'','','',''), +(1598,1595,'马村','马村区','河南省,焦作市,马村区',3,'','','',''), +(1599,1595,'山阳','山阳区','河南省,焦作市,山阳区',3,'','','',''), +(1600,1595,'修武','修武县','河南省,焦作市,修武县',3,'','','',''), +(1601,1595,'博爱','博爱县','河南省,焦作市,博爱县',3,'','','',''), +(1602,1595,'武陟','武陟县','河南省,焦作市,武陟县',3,'','','',''), +(1603,1595,'温','温县','河南省,焦作市,温县',3,'','','',''), +(1604,1595,'沁阳','沁阳市','河南省,焦作市,沁阳市',3,'','','',''), +(1605,1595,'孟州','孟州市','河南省,焦作市,孟州市',3,'','','',''), +(1606,1514,'濮阳','濮阳市','河南省,濮阳市',2,'','','',''), +(1607,1606,'华龙','华龙区','河南省,濮阳市,华龙区',3,'','','',''), +(1608,1606,'清丰','清丰县','河南省,濮阳市,清丰县',3,'','','',''), +(1609,1606,'南乐','南乐县','河南省,濮阳市,南乐县',3,'','','',''), +(1610,1606,'范','范县','河南省,濮阳市,范县',3,'','','',''), +(1611,1606,'台前','台前县','河南省,濮阳市,台前县',3,'','','',''), +(1612,1606,'濮阳','濮阳县','河南省,濮阳市,濮阳县',3,'','','',''), +(1613,1514,'许昌','许昌市','河南省,许昌市',2,'','','',''), +(1614,1613,'魏都','魏都区','河南省,许昌市,魏都区',3,'','','',''), +(1615,1613,'许昌','许昌县','河南省,许昌市,许昌县',3,'','','',''), +(1616,1613,'鄢陵','鄢陵县','河南省,许昌市,鄢陵县',3,'','','',''), +(1617,1613,'襄城','襄城县','河南省,许昌市,襄城县',3,'','','',''), +(1618,1613,'禹州','禹州市','河南省,许昌市,禹州市',3,'','','',''), +(1619,1613,'长葛','长葛市','河南省,许昌市,长葛市',3,'','','',''), +(1620,1514,'漯河','漯河市','河南省,漯河市',2,'','','',''), +(1621,1620,'源汇','源汇区','河南省,漯河市,源汇区',3,'','','',''), +(1622,1620,'郾城','郾城区','河南省,漯河市,郾城区',3,'','','',''), +(1623,1620,'召陵','召陵区','河南省,漯河市,召陵区',3,'','','',''), +(1624,1620,'舞阳','舞阳县','河南省,漯河市,舞阳县',3,'','','',''), +(1625,1620,'临颍','临颍县','河南省,漯河市,临颍县',3,'','','',''), +(1626,1514,'三门峡','三门峡市','河南省,三门峡市',2,'','','',''), +(1627,1626,'湖滨','湖滨区','河南省,三门峡市,湖滨区',3,'','','',''), +(1628,1626,'渑池','渑池县','河南省,三门峡市,渑池县',3,'','','',''), +(1629,1626,'陕','陕县','河南省,三门峡市,陕县',3,'','','',''), +(1630,1626,'卢氏','卢氏县','河南省,三门峡市,卢氏县',3,'','','',''), +(1631,1626,'义马','义马市','河南省,三门峡市,义马市',3,'','','',''), +(1632,1626,'灵宝','灵宝市','河南省,三门峡市,灵宝市',3,'','','',''), +(1633,1514,'南阳','南阳市','河南省,南阳市',2,'','','',''), +(1634,1633,'宛城','宛城区','河南省,南阳市,宛城区',3,'','','',''), +(1635,1633,'卧龙','卧龙区','河南省,南阳市,卧龙区',3,'','','',''), +(1636,1633,'南召','南召县','河南省,南阳市,南召县',3,'','','',''), +(1637,1633,'方城','方城县','河南省,南阳市,方城县',3,'','','',''), +(1638,1633,'西峡','西峡县','河南省,南阳市,西峡县',3,'','','',''), +(1639,1633,'镇平','镇平县','河南省,南阳市,镇平县',3,'','','',''), +(1640,1633,'内乡','内乡县','河南省,南阳市,内乡县',3,'','','',''), +(1641,1633,'淅川','淅川县','河南省,南阳市,淅川县',3,'','','',''), +(1642,1633,'社旗','社旗县','河南省,南阳市,社旗县',3,'','','',''), +(1643,1633,'唐河','唐河县','河南省,南阳市,唐河县',3,'','','',''), +(1644,1633,'新野','新野县','河南省,南阳市,新野县',3,'','','',''), +(1645,1633,'桐柏','桐柏县','河南省,南阳市,桐柏县',3,'','','',''), +(1646,1633,'邓州','邓州市','河南省,南阳市,邓州市',3,'','','',''), +(1647,1514,'商丘','商丘市','河南省,商丘市',2,'','','',''), +(1648,1647,'梁园','梁园区','河南省,商丘市,梁园区',3,'','','',''), +(1649,1647,'睢阳','睢阳区','河南省,商丘市,睢阳区',3,'','','',''), +(1650,1647,'民权','民权县','河南省,商丘市,民权县',3,'','','',''), +(1651,1647,'睢','睢县','河南省,商丘市,睢县',3,'','','',''), +(1652,1647,'宁陵','宁陵县','河南省,商丘市,宁陵县',3,'','','',''), +(1653,1647,'柘城','柘城县','河南省,商丘市,柘城县',3,'','','',''), +(1654,1647,'虞城','虞城县','河南省,商丘市,虞城县',3,'','','',''), +(1655,1647,'夏邑','夏邑县','河南省,商丘市,夏邑县',3,'','','',''), +(1656,1647,'永城','永城市','河南省,商丘市,永城市',3,'','','',''), +(1657,1514,'信阳','信阳市','河南省,信阳市',2,'','','',''), +(1658,1657,'浉河','浉河区','河南省,信阳市,浉河区',3,'','','',''), +(1659,1657,'平桥','平桥区','河南省,信阳市,平桥区',3,'','','',''), +(1660,1657,'罗山','罗山县','河南省,信阳市,罗山县',3,'','','',''), +(1661,1657,'光山','光山县','河南省,信阳市,光山县',3,'','','',''), +(1662,1657,'新','新县','河南省,信阳市,新县',3,'','','',''), +(1663,1657,'商城','商城县','河南省,信阳市,商城县',3,'','','',''), +(1664,1657,'固始','固始县','河南省,信阳市,固始县',3,'','','',''), +(1665,1657,'潢川','潢川县','河南省,信阳市,潢川县',3,'','','',''), +(1666,1657,'淮滨','淮滨县','河南省,信阳市,淮滨县',3,'','','',''), +(1667,1657,'息','息县','河南省,信阳市,息县',3,'','','',''), +(1668,1514,'周口','周口市','河南省,周口市',2,'','','',''), +(1669,1668,'川汇','川汇区','河南省,周口市,川汇区',3,'','','',''), +(1670,1668,'扶沟','扶沟县','河南省,周口市,扶沟县',3,'','','',''), +(1671,1668,'西华','西华县','河南省,周口市,西华县',3,'','','',''), +(1672,1668,'商水','商水县','河南省,周口市,商水县',3,'','','',''), +(1673,1668,'沈丘','沈丘县','河南省,周口市,沈丘县',3,'','','',''), +(1674,1668,'郸城','郸城县','河南省,周口市,郸城县',3,'','','',''), +(1675,1668,'淮阳','淮阳县','河南省,周口市,淮阳县',3,'','','',''), +(1676,1668,'太康','太康县','河南省,周口市,太康县',3,'','','',''), +(1677,1668,'鹿邑','鹿邑县','河南省,周口市,鹿邑县',3,'','','',''), +(1678,1668,'项城','项城市','河南省,周口市,项城市',3,'','','',''), +(1679,1514,'驻马店','驻马店市','河南省,驻马店市',2,'','','',''), +(1680,1679,'驿城','驿城区','河南省,驻马店市,驿城区',3,'','','',''), +(1681,1679,'西平','西平县','河南省,驻马店市,西平县',3,'','','',''), +(1682,1679,'上蔡','上蔡县','河南省,驻马店市,上蔡县',3,'','','',''), +(1683,1679,'平舆','平舆县','河南省,驻马店市,平舆县',3,'','','',''), +(1684,1679,'正阳','正阳县','河南省,驻马店市,正阳县',3,'','','',''), +(1685,1679,'确山','确山县','河南省,驻马店市,确山县',3,'','','',''), +(1686,1679,'泌阳','泌阳县','河南省,驻马店市,泌阳县',3,'','','',''), +(1687,1679,'汝南','汝南县','河南省,驻马店市,汝南县',3,'','','',''), +(1688,1679,'遂平','遂平县','河南省,驻马店市,遂平县',3,'','','',''), +(1689,1679,'新蔡','新蔡县','河南省,驻马店市,新蔡县',3,'','','',''), +(1690,1514,'济源','济源市','河南省,济源市',2,'','','',''), +(1691,1690,'济源','济源市','河南省,济源市,济源市',3,'','','',''), +(1692,0,'湖北','湖北省','湖北省',1,'','','',''), +(1693,1692,'武汉','武汉市','湖北省,武汉市',2,'','','',''), +(1694,1693,'江岸','江岸区','湖北省,武汉市,江岸区',3,'','','',''), +(1695,1693,'江汉','江汉区','湖北省,武汉市,江汉区',3,'','','',''), +(1696,1693,'硚口','硚口区','湖北省,武汉市,硚口区',3,'','','',''), +(1697,1693,'汉阳','汉阳区','湖北省,武汉市,汉阳区',3,'','','',''), +(1698,1693,'武昌','武昌区','湖北省,武汉市,武昌区',3,'','','',''), +(1699,1693,'青山','青山区','湖北省,武汉市,青山区',3,'','','',''), +(1700,1693,'洪山','洪山区','湖北省,武汉市,洪山区',3,'','','',''), +(1701,1693,'东西湖','东西湖区','湖北省,武汉市,东西湖区',3,'','','',''), +(1702,1693,'汉南','汉南区','湖北省,武汉市,汉南区',3,'','','',''), +(1703,1693,'蔡甸','蔡甸区','湖北省,武汉市,蔡甸区',3,'','','',''), +(1704,1693,'江夏','江夏区','湖北省,武汉市,江夏区',3,'','','',''), +(1705,1693,'黄陂','黄陂区','湖北省,武汉市,黄陂区',3,'','','',''), +(1706,1693,'新洲','新洲区','湖北省,武汉市,新洲区',3,'','','',''), +(1707,1692,'黄石','黄石市','湖北省,黄石市',2,'','','',''), +(1708,1707,'黄石港','黄石港区','湖北省,黄石市,黄石港区',3,'','','',''), +(1709,1707,'西塞山','西塞山区','湖北省,黄石市,西塞山区',3,'','','',''), +(1710,1707,'下陆','下陆区','湖北省,黄石市,下陆区',3,'','','',''), +(1711,1707,'铁山','铁山区','湖北省,黄石市,铁山区',3,'','','',''), +(1712,1707,'阳新','阳新县','湖北省,黄石市,阳新县',3,'','','',''), +(1713,1707,'大冶','大冶市','湖北省,黄石市,大冶市',3,'','','',''), +(1714,1692,'十堰','十堰市','湖北省,十堰市',2,'','','',''), +(1715,1714,'茅箭','茅箭区','湖北省,十堰市,茅箭区',3,'','','',''), +(1716,1714,'张湾','张湾区','湖北省,十堰市,张湾区',3,'','','',''), +(1717,1714,'郧','郧县','湖北省,十堰市,郧县',3,'','','',''), +(1718,1714,'郧西','郧西县','湖北省,十堰市,郧西县',3,'','','',''), +(1719,1714,'竹山','竹山县','湖北省,十堰市,竹山县',3,'','','',''), +(1720,1714,'竹溪','竹溪县','湖北省,十堰市,竹溪县',3,'','','',''), +(1721,1714,'房','房县','湖北省,十堰市,房县',3,'','','',''), +(1722,1714,'丹江口','丹江口市','湖北省,十堰市,丹江口市',3,'','','',''), +(1723,1692,'宜昌','宜昌市','湖北省,宜昌市',2,'','','',''), +(1724,1723,'西陵','西陵区','湖北省,宜昌市,西陵区',3,'','','',''), +(1725,1723,'伍家岗','伍家岗区','湖北省,宜昌市,伍家岗区',3,'','','',''), +(1726,1723,'点军','点军区','湖北省,宜昌市,点军区',3,'','','',''), +(1727,1723,'猇亭','猇亭区','湖北省,宜昌市,猇亭区',3,'','','',''), +(1728,1723,'夷陵','夷陵区','湖北省,宜昌市,夷陵区',3,'','','',''), +(1729,1723,'远安','远安县','湖北省,宜昌市,远安县',3,'','','',''), +(1730,1723,'兴山','兴山县','湖北省,宜昌市,兴山县',3,'','','',''), +(1731,1723,'秭归','秭归县','湖北省,宜昌市,秭归县',3,'','','',''), +(1732,1723,'长阳土家族','长阳土家族自治县','湖北省,宜昌市,长阳土家族自治县',3,'','','',''), +(1733,1723,'五峰土家族','五峰土家族自治县','湖北省,宜昌市,五峰土家族自治县',3,'','','',''), +(1734,1723,'宜都','宜都市','湖北省,宜昌市,宜都市',3,'','','',''), +(1735,1723,'当阳','当阳市','湖北省,宜昌市,当阳市',3,'','','',''), +(1736,1723,'枝江','枝江市','湖北省,宜昌市,枝江市',3,'','','',''), +(1737,1692,'襄阳','襄阳市','湖北省,襄阳市',2,'','','',''), +(1738,1737,'襄城','襄城区','湖北省,襄阳市,襄城区',3,'','','',''), +(1739,1737,'樊城','樊城区','湖北省,襄阳市,樊城区',3,'','','',''), +(1740,1737,'襄州','襄州区','湖北省,襄阳市,襄州区',3,'','','',''), +(1741,1737,'南漳','南漳县','湖北省,襄阳市,南漳县',3,'','','',''), +(1742,1737,'谷城','谷城县','湖北省,襄阳市,谷城县',3,'','','',''), +(1743,1737,'保康','保康县','湖北省,襄阳市,保康县',3,'','','',''), +(1744,1737,'老河口','老河口市','湖北省,襄阳市,老河口市',3,'','','',''), +(1745,1737,'枣阳','枣阳市','湖北省,襄阳市,枣阳市',3,'','','',''), +(1746,1737,'宜城','宜城市','湖北省,襄阳市,宜城市',3,'','','',''), +(1747,1692,'鄂州','鄂州市','湖北省,鄂州市',2,'','','',''), +(1748,1747,'梁子湖','梁子湖区','湖北省,鄂州市,梁子湖区',3,'','','',''), +(1749,1747,'华容','华容区','湖北省,鄂州市,华容区',3,'','','',''), +(1750,1747,'鄂城','鄂城区','湖北省,鄂州市,鄂城区',3,'','','',''), +(1751,1692,'荆门','荆门市','湖北省,荆门市',2,'','','',''), +(1752,1751,'东宝','东宝区','湖北省,荆门市,东宝区',3,'','','',''), +(1753,1751,'掇刀','掇刀区','湖北省,荆门市,掇刀区',3,'','','',''), +(1754,1751,'京山','京山县','湖北省,荆门市,京山县',3,'','','',''), +(1755,1751,'沙洋','沙洋县','湖北省,荆门市,沙洋县',3,'','','',''), +(1756,1751,'钟祥','钟祥市','湖北省,荆门市,钟祥市',3,'','','',''), +(1757,1692,'孝感','孝感市','湖北省,孝感市',2,'','','',''), +(1758,1757,'孝南','孝南区','湖北省,孝感市,孝南区',3,'','','',''), +(1759,1757,'孝昌','孝昌县','湖北省,孝感市,孝昌县',3,'','','',''), +(1760,1757,'大悟','大悟县','湖北省,孝感市,大悟县',3,'','','',''), +(1761,1757,'云梦','云梦县','湖北省,孝感市,云梦县',3,'','','',''), +(1762,1757,'应城','应城市','湖北省,孝感市,应城市',3,'','','',''), +(1763,1757,'安陆','安陆市','湖北省,孝感市,安陆市',3,'','','',''), +(1764,1757,'汉川','汉川市','湖北省,孝感市,汉川市',3,'','','',''), +(1765,1692,'荆州','荆州市','湖北省,荆州市',2,'','','',''), +(1766,1765,'沙市','沙市区','湖北省,荆州市,沙市区',3,'','','',''), +(1767,1765,'荆州','荆州区','湖北省,荆州市,荆州区',3,'','','',''), +(1768,1765,'公安','公安县','湖北省,荆州市,公安县',3,'','','',''), +(1769,1765,'监利','监利县','湖北省,荆州市,监利县',3,'','','',''), +(1770,1765,'江陵','江陵县','湖北省,荆州市,江陵县',3,'','','',''), +(1771,1765,'石首','石首市','湖北省,荆州市,石首市',3,'','','',''), +(1772,1765,'洪湖','洪湖市','湖北省,荆州市,洪湖市',3,'','','',''), +(1773,1765,'松滋','松滋市','湖北省,荆州市,松滋市',3,'','','',''), +(1774,1692,'黄冈','黄冈市','湖北省,黄冈市',2,'','','',''), +(1775,1774,'黄州','黄州区','湖北省,黄冈市,黄州区',3,'','','',''), +(1776,1774,'团风','团风县','湖北省,黄冈市,团风县',3,'','','',''), +(1777,1774,'红安','红安县','湖北省,黄冈市,红安县',3,'','','',''), +(1778,1774,'罗田','罗田县','湖北省,黄冈市,罗田县',3,'','','',''), +(1779,1774,'英山','英山县','湖北省,黄冈市,英山县',3,'','','',''), +(1780,1774,'浠水','浠水县','湖北省,黄冈市,浠水县',3,'','','',''), +(1781,1774,'蕲春','蕲春县','湖北省,黄冈市,蕲春县',3,'','','',''), +(1782,1774,'黄梅','黄梅县','湖北省,黄冈市,黄梅县',3,'','','',''), +(1783,1774,'麻城','麻城市','湖北省,黄冈市,麻城市',3,'','','',''), +(1784,1774,'武穴','武穴市','湖北省,黄冈市,武穴市',3,'','','',''), +(1785,1692,'咸宁','咸宁市','湖北省,咸宁市',2,'','','',''), +(1786,1785,'咸安','咸安区','湖北省,咸宁市,咸安区',3,'','','',''), +(1787,1785,'嘉鱼','嘉鱼县','湖北省,咸宁市,嘉鱼县',3,'','','',''), +(1788,1785,'通城','通城县','湖北省,咸宁市,通城县',3,'','','',''), +(1789,1785,'崇阳','崇阳县','湖北省,咸宁市,崇阳县',3,'','','',''), +(1790,1785,'通山','通山县','湖北省,咸宁市,通山县',3,'','','',''), +(1791,1785,'赤壁','赤壁市','湖北省,咸宁市,赤壁市',3,'','','',''), +(1792,1692,'随州','随州市','湖北省,随州市',2,'','','',''), +(1793,1792,'曾都','曾都区','湖北省,随州市,曾都区',3,'','','',''), +(1794,1792,'随','随县','湖北省,随州市,随县',3,'','','',''), +(1795,1792,'广水','广水市','湖北省,随州市,广水市',3,'','','',''), +(1796,1692,'恩施土家族苗族','恩施土家族苗族自治州','湖北省,恩施土家族苗族自治州',2,'','','',''), +(1797,1796,'恩施','恩施市','湖北省,恩施土家族苗族自治州,恩施市',3,'','','',''), +(1798,1796,'利川','利川市','湖北省,恩施土家族苗族自治州,利川市',3,'','','',''), +(1799,1796,'建始','建始县','湖北省,恩施土家族苗族自治州,建始县',3,'','','',''), +(1800,1796,'巴东','巴东县','湖北省,恩施土家族苗族自治州,巴东县',3,'','','',''); +INSERT IGNORE INTO `fa_area` (`id`,`pid`,`shortname`,`name`,`mergename`,`level`,`pinyin`,`code`,`zip`,`first`) VALUES +(1801,1796,'宣恩','宣恩县','湖北省,恩施土家族苗族自治州,宣恩县',3,'','','',''), +(1802,1796,'咸丰','咸丰县','湖北省,恩施土家族苗族自治州,咸丰县',3,'','','',''), +(1803,1796,'来凤','来凤县','湖北省,恩施土家族苗族自治州,来凤县',3,'','','',''), +(1804,1796,'鹤峰','鹤峰县','湖北省,恩施土家族苗族自治州,鹤峰县',3,'','','',''), +(1805,1692,'仙桃','仙桃市','湖北省,仙桃市',2,'','','',''), +(1806,1805,'仙桃','仙桃市','湖北省,仙桃市,仙桃市',3,'','','',''), +(1807,1692,'潜江','潜江市','湖北省,潜江市',2,'','','',''), +(1808,1807,'潜江','潜江市','湖北省,潜江市,潜江市',3,'','','',''), +(1809,1692,'天门','天门市','湖北省,天门市',2,'','','',''), +(1810,1809,'天门','天门市','湖北省,天门市,天门市',3,'','','',''), +(1811,1692,'神农架','神农架林区','湖北省,神农架林区',2,'','','',''), +(1812,1811,'神农架','神农架林区','湖北省,神农架林区,神农架林区',3,'','','',''), +(1813,0,'湖南','湖南省','湖南省',1,'','','',''), +(1814,1813,'长沙','长沙市','湖南省,长沙市',2,'','','',''), +(1815,1814,'芙蓉','芙蓉区','湖南省,长沙市,芙蓉区',3,'','','',''), +(1816,1814,'天心','天心区','湖南省,长沙市,天心区',3,'','','',''), +(1817,1814,'岳麓','岳麓区','湖南省,长沙市,岳麓区',3,'','','',''), +(1818,1814,'开福','开福区','湖南省,长沙市,开福区',3,'','','',''), +(1819,1814,'雨花','雨花区','湖南省,长沙市,雨花区',3,'','','',''), +(1820,1814,'望城','望城区','湖南省,长沙市,望城区',3,'','','',''), +(1821,1814,'长沙','长沙县','湖南省,长沙市,长沙县',3,'','','',''), +(1822,1814,'宁乡','宁乡县','湖南省,长沙市,宁乡县',3,'','','',''), +(1823,1814,'浏阳','浏阳市','湖南省,长沙市,浏阳市',3,'','','',''), +(1824,1813,'株洲','株洲市','湖南省,株洲市',2,'','','',''), +(1825,1824,'荷塘','荷塘区','湖南省,株洲市,荷塘区',3,'','','',''), +(1826,1824,'芦淞','芦淞区','湖南省,株洲市,芦淞区',3,'','','',''), +(1827,1824,'石峰','石峰区','湖南省,株洲市,石峰区',3,'','','',''), +(1828,1824,'天元','天元区','湖南省,株洲市,天元区',3,'','','',''), +(1829,1824,'株洲','株洲县','湖南省,株洲市,株洲县',3,'','','',''), +(1830,1824,'攸','攸县','湖南省,株洲市,攸县',3,'','','',''), +(1831,1824,'茶陵','茶陵县','湖南省,株洲市,茶陵县',3,'','','',''), +(1832,1824,'炎陵','炎陵县','湖南省,株洲市,炎陵县',3,'','','',''), +(1833,1824,'醴陵','醴陵市','湖南省,株洲市,醴陵市',3,'','','',''), +(1834,1813,'湘潭','湘潭市','湖南省,湘潭市',2,'','','',''), +(1835,1834,'雨湖','雨湖区','湖南省,湘潭市,雨湖区',3,'','','',''), +(1836,1834,'岳塘','岳塘区','湖南省,湘潭市,岳塘区',3,'','','',''), +(1837,1834,'湘潭','湘潭县','湖南省,湘潭市,湘潭县',3,'','','',''), +(1838,1834,'湘乡','湘乡市','湖南省,湘潭市,湘乡市',3,'','','',''), +(1839,1834,'韶山','韶山市','湖南省,湘潭市,韶山市',3,'','','',''), +(1840,1813,'衡阳','衡阳市','湖南省,衡阳市',2,'','','',''), +(1841,1840,'珠晖','珠晖区','湖南省,衡阳市,珠晖区',3,'','','',''), +(1842,1840,'雁峰','雁峰区','湖南省,衡阳市,雁峰区',3,'','','',''), +(1843,1840,'石鼓','石鼓区','湖南省,衡阳市,石鼓区',3,'','','',''), +(1844,1840,'蒸湘','蒸湘区','湖南省,衡阳市,蒸湘区',3,'','','',''), +(1845,1840,'南岳','南岳区','湖南省,衡阳市,南岳区',3,'','','',''), +(1846,1840,'衡阳','衡阳县','湖南省,衡阳市,衡阳县',3,'','','',''), +(1847,1840,'衡南','衡南县','湖南省,衡阳市,衡南县',3,'','','',''), +(1848,1840,'衡山','衡山县','湖南省,衡阳市,衡山县',3,'','','',''), +(1849,1840,'衡东','衡东县','湖南省,衡阳市,衡东县',3,'','','',''), +(1850,1840,'祁东','祁东县','湖南省,衡阳市,祁东县',3,'','','',''), +(1851,1840,'耒阳','耒阳市','湖南省,衡阳市,耒阳市',3,'','','',''), +(1852,1840,'常宁','常宁市','湖南省,衡阳市,常宁市',3,'','','',''), +(1853,1813,'邵阳','邵阳市','湖南省,邵阳市',2,'','','',''), +(1854,1853,'双清','双清区','湖南省,邵阳市,双清区',3,'','','',''), +(1855,1853,'大祥','大祥区','湖南省,邵阳市,大祥区',3,'','','',''), +(1856,1853,'北塔','北塔区','湖南省,邵阳市,北塔区',3,'','','',''), +(1857,1853,'邵东','邵东县','湖南省,邵阳市,邵东县',3,'','','',''), +(1858,1853,'新邵','新邵县','湖南省,邵阳市,新邵县',3,'','','',''), +(1859,1853,'邵阳','邵阳县','湖南省,邵阳市,邵阳县',3,'','','',''), +(1860,1853,'隆回','隆回县','湖南省,邵阳市,隆回县',3,'','','',''), +(1861,1853,'洞口','洞口县','湖南省,邵阳市,洞口县',3,'','','',''), +(1862,1853,'绥宁','绥宁县','湖南省,邵阳市,绥宁县',3,'','','',''), +(1863,1853,'新宁','新宁县','湖南省,邵阳市,新宁县',3,'','','',''), +(1864,1853,'城步苗族','城步苗族自治县','湖南省,邵阳市,城步苗族自治县',3,'','','',''), +(1865,1853,'武冈','武冈市','湖南省,邵阳市,武冈市',3,'','','',''), +(1866,1813,'岳阳','岳阳市','湖南省,岳阳市',2,'','','',''), +(1867,1866,'岳阳楼','岳阳楼区','湖南省,岳阳市,岳阳楼区',3,'','','',''), +(1868,1866,'云溪','云溪区','湖南省,岳阳市,云溪区',3,'','','',''), +(1869,1866,'君山','君山区','湖南省,岳阳市,君山区',3,'','','',''), +(1870,1866,'岳阳','岳阳县','湖南省,岳阳市,岳阳县',3,'','','',''), +(1871,1866,'华容','华容县','湖南省,岳阳市,华容县',3,'','','',''), +(1872,1866,'湘阴','湘阴县','湖南省,岳阳市,湘阴县',3,'','','',''), +(1873,1866,'平江','平江县','湖南省,岳阳市,平江县',3,'','','',''), +(1874,1866,'汨罗','汨罗市','湖南省,岳阳市,汨罗市',3,'','','',''), +(1875,1866,'临湘','临湘市','湖南省,岳阳市,临湘市',3,'','','',''), +(1876,1813,'常德','常德市','湖南省,常德市',2,'','','',''), +(1877,1876,'武陵','武陵区','湖南省,常德市,武陵区',3,'','','',''), +(1878,1876,'鼎城','鼎城区','湖南省,常德市,鼎城区',3,'','','',''), +(1879,1876,'安乡','安乡县','湖南省,常德市,安乡县',3,'','','',''), +(1880,1876,'汉寿','汉寿县','湖南省,常德市,汉寿县',3,'','','',''), +(1881,1876,'澧','澧县','湖南省,常德市,澧县',3,'','','',''), +(1882,1876,'临澧','临澧县','湖南省,常德市,临澧县',3,'','','',''), +(1883,1876,'桃源','桃源县','湖南省,常德市,桃源县',3,'','','',''), +(1884,1876,'石门','石门县','湖南省,常德市,石门县',3,'','','',''), +(1885,1876,'津市','津市市','湖南省,常德市,津市市',3,'','','',''), +(1886,1813,'张家界','张家界市','湖南省,张家界市',2,'','','',''), +(1887,1886,'永定','永定区','湖南省,张家界市,永定区',3,'','','',''), +(1888,1886,'武陵源','武陵源区','湖南省,张家界市,武陵源区',3,'','','',''), +(1889,1886,'慈利','慈利县','湖南省,张家界市,慈利县',3,'','','',''), +(1890,1886,'桑植','桑植县','湖南省,张家界市,桑植县',3,'','','',''), +(1891,1813,'益阳','益阳市','湖南省,益阳市',2,'','','',''), +(1892,1891,'资阳','资阳区','湖南省,益阳市,资阳区',3,'','','',''), +(1893,1891,'赫山','赫山区','湖南省,益阳市,赫山区',3,'','','',''), +(1894,1891,'南','南县','湖南省,益阳市,南县',3,'','','',''), +(1895,1891,'桃江','桃江县','湖南省,益阳市,桃江县',3,'','','',''), +(1896,1891,'安化','安化县','湖南省,益阳市,安化县',3,'','','',''), +(1897,1891,'沅江','沅江市','湖南省,益阳市,沅江市',3,'','','',''), +(1898,1813,'郴州','郴州市','湖南省,郴州市',2,'','','',''), +(1899,1898,'北湖','北湖区','湖南省,郴州市,北湖区',3,'','','',''), +(1900,1898,'苏仙','苏仙区','湖南省,郴州市,苏仙区',3,'','','',''), +(1901,1898,'桂阳','桂阳县','湖南省,郴州市,桂阳县',3,'','','',''), +(1902,1898,'宜章','宜章县','湖南省,郴州市,宜章县',3,'','','',''), +(1903,1898,'永兴','永兴县','湖南省,郴州市,永兴县',3,'','','',''), +(1904,1898,'嘉禾','嘉禾县','湖南省,郴州市,嘉禾县',3,'','','',''), +(1905,1898,'临武','临武县','湖南省,郴州市,临武县',3,'','','',''), +(1906,1898,'汝城','汝城县','湖南省,郴州市,汝城县',3,'','','',''), +(1907,1898,'桂东','桂东县','湖南省,郴州市,桂东县',3,'','','',''), +(1908,1898,'安仁','安仁县','湖南省,郴州市,安仁县',3,'','','',''), +(1909,1898,'资兴','资兴市','湖南省,郴州市,资兴市',3,'','','',''), +(1910,1813,'永州','永州市','湖南省,永州市',2,'','','',''), +(1911,1910,'零陵','零陵区','湖南省,永州市,零陵区',3,'','','',''), +(1912,1910,'冷水滩','冷水滩区','湖南省,永州市,冷水滩区',3,'','','',''), +(1913,1910,'祁阳','祁阳县','湖南省,永州市,祁阳县',3,'','','',''), +(1914,1910,'东安','东安县','湖南省,永州市,东安县',3,'','','',''), +(1915,1910,'双牌','双牌县','湖南省,永州市,双牌县',3,'','','',''), +(1916,1910,'道','道县','湖南省,永州市,道县',3,'','','',''), +(1917,1910,'江永','江永县','湖南省,永州市,江永县',3,'','','',''), +(1918,1910,'宁远','宁远县','湖南省,永州市,宁远县',3,'','','',''), +(1919,1910,'蓝山','蓝山县','湖南省,永州市,蓝山县',3,'','','',''), +(1920,1910,'新田','新田县','湖南省,永州市,新田县',3,'','','',''), +(1921,1910,'江华瑶族','江华瑶族自治县','湖南省,永州市,江华瑶族自治县',3,'','','',''), +(1922,1813,'怀化','怀化市','湖南省,怀化市',2,'','','',''), +(1923,1922,'鹤城','鹤城区','湖南省,怀化市,鹤城区',3,'','','',''), +(1924,1922,'中方','中方县','湖南省,怀化市,中方县',3,'','','',''), +(1925,1922,'沅陵','沅陵县','湖南省,怀化市,沅陵县',3,'','','',''), +(1926,1922,'辰溪','辰溪县','湖南省,怀化市,辰溪县',3,'','','',''), +(1927,1922,'溆浦','溆浦县','湖南省,怀化市,溆浦县',3,'','','',''), +(1928,1922,'会同','会同县','湖南省,怀化市,会同县',3,'','','',''), +(1929,1922,'麻阳苗族','麻阳苗族自治县','湖南省,怀化市,麻阳苗族自治县',3,'','','',''), +(1930,1922,'新晃侗族','新晃侗族自治县','湖南省,怀化市,新晃侗族自治县',3,'','','',''), +(1931,1922,'芷江侗族','芷江侗族自治县','湖南省,怀化市,芷江侗族自治县',3,'','','',''), +(1932,1922,'靖州苗族侗族','靖州苗族侗族自治县','湖南省,怀化市,靖州苗族侗族自治县',3,'','','',''), +(1933,1922,'通道侗族','通道侗族自治县','湖南省,怀化市,通道侗族自治县',3,'','','',''), +(1934,1922,'洪江','洪江市','湖南省,怀化市,洪江市',3,'','','',''), +(1935,1813,'娄底','娄底市','湖南省,娄底市',2,'','','',''), +(1936,1935,'娄星','娄星区','湖南省,娄底市,娄星区',3,'','','',''), +(1937,1935,'双峰','双峰县','湖南省,娄底市,双峰县',3,'','','',''), +(1938,1935,'新化','新化县','湖南省,娄底市,新化县',3,'','','',''), +(1939,1935,'冷水江','冷水江市','湖南省,娄底市,冷水江市',3,'','','',''), +(1940,1935,'涟源','涟源市','湖南省,娄底市,涟源市',3,'','','',''), +(1941,1813,'湘西土家族苗族','湘西土家族苗族自治州','湖南省,湘西土家族苗族自治州',2,'','','',''), +(1942,1941,'吉首','吉首市','湖南省,湘西土家族苗族自治州,吉首市',3,'','','',''), +(1943,1941,'泸溪','泸溪县','湖南省,湘西土家族苗族自治州,泸溪县',3,'','','',''), +(1944,1941,'凤凰','凤凰县','湖南省,湘西土家族苗族自治州,凤凰县',3,'','','',''), +(1945,1941,'花垣','花垣县','湖南省,湘西土家族苗族自治州,花垣县',3,'','','',''), +(1946,1941,'保靖','保靖县','湖南省,湘西土家族苗族自治州,保靖县',3,'','','',''), +(1947,1941,'古丈','古丈县','湖南省,湘西土家族苗族自治州,古丈县',3,'','','',''), +(1948,1941,'永顺','永顺县','湖南省,湘西土家族苗族自治州,永顺县',3,'','','',''), +(1949,1941,'龙山','龙山县','湖南省,湘西土家族苗族自治州,龙山县',3,'','','',''), +(1950,0,'广东','广东省','广东省',1,'','','',''), +(1951,1950,'广州','广州市','广东省,广州市',2,'','','',''), +(1952,1951,'荔湾','荔湾区','广东省,广州市,荔湾区',3,'','','',''), +(1953,1951,'越秀','越秀区','广东省,广州市,越秀区',3,'','','',''), +(1954,1951,'海珠','海珠区','广东省,广州市,海珠区',3,'','','',''), +(1955,1951,'天河','天河区','广东省,广州市,天河区',3,'','','',''), +(1956,1951,'白云','白云区','广东省,广州市,白云区',3,'','','',''), +(1957,1951,'黄埔','黄埔区','广东省,广州市,黄埔区',3,'','','',''), +(1958,1951,'番禺','番禺区','广东省,广州市,番禺区',3,'','','',''), +(1959,1951,'花都','花都区','广东省,广州市,花都区',3,'','','',''), +(1960,1951,'南沙','南沙区','广东省,广州市,南沙区',3,'','','',''), +(1961,1951,'萝岗','萝岗区','广东省,广州市,萝岗区',3,'','','',''), +(1962,1951,'增城','增城市','广东省,广州市,增城市',3,'','','',''), +(1963,1951,'从化','从化市','广东省,广州市,从化市',3,'','','',''), +(1964,1950,'韶关','韶关市','广东省,韶关市',2,'','','',''), +(1965,1964,'武江','武江区','广东省,韶关市,武江区',3,'','','',''), +(1966,1964,'浈江','浈江区','广东省,韶关市,浈江区',3,'','','',''), +(1967,1964,'曲江','曲江区','广东省,韶关市,曲江区',3,'','','',''), +(1968,1964,'始兴','始兴县','广东省,韶关市,始兴县',3,'','','',''), +(1969,1964,'仁化','仁化县','广东省,韶关市,仁化县',3,'','','',''), +(1970,1964,'翁源','翁源县','广东省,韶关市,翁源县',3,'','','',''), +(1971,1964,'乳源瑶族','乳源瑶族自治县','广东省,韶关市,乳源瑶族自治县',3,'','','',''), +(1972,1964,'新丰','新丰县','广东省,韶关市,新丰县',3,'','','',''), +(1973,1964,'乐昌','乐昌市','广东省,韶关市,乐昌市',3,'','','',''), +(1974,1964,'南雄','南雄市','广东省,韶关市,南雄市',3,'','','',''), +(1975,1950,'深圳','深圳市','广东省,深圳市',2,'','','',''), +(1976,1975,'罗湖','罗湖区','广东省,深圳市,罗湖区',3,'','','',''), +(1977,1975,'福田','福田区','广东省,深圳市,福田区',3,'','','',''), +(1978,1975,'南山','南山区','广东省,深圳市,南山区',3,'','','',''), +(1979,1975,'宝安','宝安区','广东省,深圳市,宝安区',3,'','','',''), +(1980,1975,'龙岗','龙岗区','广东省,深圳市,龙岗区',3,'','','',''), +(1981,1975,'盐田','盐田区','广东省,深圳市,盐田区',3,'','','',''), +(1982,1950,'珠海','珠海市','广东省,珠海市',2,'','','',''), +(1983,1982,'香洲','香洲区','广东省,珠海市,香洲区',3,'','','',''), +(1984,1982,'斗门','斗门区','广东省,珠海市,斗门区',3,'','','',''), +(1985,1982,'金湾','金湾区','广东省,珠海市,金湾区',3,'','','',''), +(1986,1950,'汕头','汕头市','广东省,汕头市',2,'','','',''), +(1987,1986,'龙湖','龙湖区','广东省,汕头市,龙湖区',3,'','','',''), +(1988,1986,'金平','金平区','广东省,汕头市,金平区',3,'','','',''), +(1989,1986,'濠江','濠江区','广东省,汕头市,濠江区',3,'','','',''), +(1990,1986,'潮阳','潮阳区','广东省,汕头市,潮阳区',3,'','','',''), +(1991,1986,'潮南','潮南区','广东省,汕头市,潮南区',3,'','','',''), +(1992,1986,'澄海','澄海区','广东省,汕头市,澄海区',3,'','','',''), +(1993,1986,'南澳','南澳县','广东省,汕头市,南澳县',3,'','','',''), +(1994,1950,'佛山','佛山市','广东省,佛山市',2,'','','',''), +(1995,1994,'禅城','禅城区','广东省,佛山市,禅城区',3,'','','',''), +(1996,1994,'南海','南海区','广东省,佛山市,南海区',3,'','','',''), +(1997,1994,'顺德','顺德区','广东省,佛山市,顺德区',3,'','','',''), +(1998,1994,'三水','三水区','广东省,佛山市,三水区',3,'','','',''), +(1999,1994,'高明','高明区','广东省,佛山市,高明区',3,'','','',''), +(2000,1950,'江门','江门市','广东省,江门市',2,'','','',''), +(2001,2000,'蓬江','蓬江区','广东省,江门市,蓬江区',3,'','','',''), +(2002,2000,'江海','江海区','广东省,江门市,江海区',3,'','','',''), +(2003,2000,'新会','新会区','广东省,江门市,新会区',3,'','','',''), +(2004,2000,'台山','台山市','广东省,江门市,台山市',3,'','','',''), +(2005,2000,'开平','开平市','广东省,江门市,开平市',3,'','','',''), +(2006,2000,'鹤山','鹤山市','广东省,江门市,鹤山市',3,'','','',''), +(2007,2000,'恩平','恩平市','广东省,江门市,恩平市',3,'','','',''), +(2008,1950,'湛江','湛江市','广东省,湛江市',2,'','','',''), +(2009,2008,'赤坎','赤坎区','广东省,湛江市,赤坎区',3,'','','',''), +(2010,2008,'霞山','霞山区','广东省,湛江市,霞山区',3,'','','',''), +(2011,2008,'坡头','坡头区','广东省,湛江市,坡头区',3,'','','',''), +(2012,2008,'麻章','麻章区','广东省,湛江市,麻章区',3,'','','',''), +(2013,2008,'遂溪','遂溪县','广东省,湛江市,遂溪县',3,'','','',''), +(2014,2008,'徐闻','徐闻县','广东省,湛江市,徐闻县',3,'','','',''), +(2015,2008,'廉江','廉江市','广东省,湛江市,廉江市',3,'','','',''), +(2016,2008,'雷州','雷州市','广东省,湛江市,雷州市',3,'','','',''), +(2017,2008,'吴川','吴川市','广东省,湛江市,吴川市',3,'','','',''), +(2018,1950,'茂名','茂名市','广东省,茂名市',2,'','','',''), +(2019,2018,'茂南','茂南区','广东省,茂名市,茂南区',3,'','','',''), +(2020,2018,'茂港','茂港区','广东省,茂名市,茂港区',3,'','','',''), +(2021,2018,'电白','电白县','广东省,茂名市,电白县',3,'','','',''), +(2022,2018,'高州','高州市','广东省,茂名市,高州市',3,'','','',''), +(2023,2018,'化州','化州市','广东省,茂名市,化州市',3,'','','',''), +(2024,2018,'信宜','信宜市','广东省,茂名市,信宜市',3,'','','',''), +(2025,1950,'肇庆','肇庆市','广东省,肇庆市',2,'','','',''), +(2026,2025,'端州','端州区','广东省,肇庆市,端州区',3,'','','',''), +(2027,2025,'鼎湖','鼎湖区','广东省,肇庆市,鼎湖区',3,'','','',''), +(2028,2025,'广宁','广宁县','广东省,肇庆市,广宁县',3,'','','',''), +(2029,2025,'怀集','怀集县','广东省,肇庆市,怀集县',3,'','','',''), +(2030,2025,'封开','封开县','广东省,肇庆市,封开县',3,'','','',''), +(2031,2025,'德庆','德庆县','广东省,肇庆市,德庆县',3,'','','',''), +(2032,2025,'高要','高要市','广东省,肇庆市,高要市',3,'','','',''), +(2033,2025,'四会','四会市','广东省,肇庆市,四会市',3,'','','',''), +(2034,1950,'惠州','惠州市','广东省,惠州市',2,'','','',''), +(2035,2034,'惠城','惠城区','广东省,惠州市,惠城区',3,'','','',''), +(2036,2034,'惠阳','惠阳区','广东省,惠州市,惠阳区',3,'','','',''), +(2037,2034,'博罗','博罗县','广东省,惠州市,博罗县',3,'','','',''), +(2038,2034,'惠东','惠东县','广东省,惠州市,惠东县',3,'','','',''), +(2039,2034,'龙门','龙门县','广东省,惠州市,龙门县',3,'','','',''), +(2040,1950,'梅州','梅州市','广东省,梅州市',2,'','','',''), +(2041,2040,'梅江','梅江区','广东省,梅州市,梅江区',3,'','','',''), +(2042,2040,'梅','梅县','广东省,梅州市,梅县',3,'','','',''), +(2043,2040,'大埔','大埔县','广东省,梅州市,大埔县',3,'','','',''), +(2044,2040,'丰顺','丰顺县','广东省,梅州市,丰顺县',3,'','','',''), +(2045,2040,'五华','五华县','广东省,梅州市,五华县',3,'','','',''), +(2046,2040,'平远','平远县','广东省,梅州市,平远县',3,'','','',''), +(2047,2040,'蕉岭','蕉岭县','广东省,梅州市,蕉岭县',3,'','','',''), +(2048,2040,'兴宁','兴宁市','广东省,梅州市,兴宁市',3,'','','',''), +(2049,1950,'汕尾','汕尾市','广东省,汕尾市',2,'','','',''), +(2050,2049,'城','城区','广东省,汕尾市,城区',3,'','','',''), +(2051,2049,'海丰','海丰县','广东省,汕尾市,海丰县',3,'','','',''), +(2052,2049,'陆河','陆河县','广东省,汕尾市,陆河县',3,'','','',''), +(2053,2049,'陆丰','陆丰市','广东省,汕尾市,陆丰市',3,'','','',''), +(2054,1950,'河源','河源市','广东省,河源市',2,'','','',''), +(2055,2054,'源城','源城区','广东省,河源市,源城区',3,'','','',''), +(2056,2054,'紫金','紫金县','广东省,河源市,紫金县',3,'','','',''), +(2057,2054,'龙川','龙川县','广东省,河源市,龙川县',3,'','','',''), +(2058,2054,'连平','连平县','广东省,河源市,连平县',3,'','','',''), +(2059,2054,'和平','和平县','广东省,河源市,和平县',3,'','','',''), +(2060,2054,'东源','东源县','广东省,河源市,东源县',3,'','','',''), +(2061,1950,'阳江','阳江市','广东省,阳江市',2,'','','',''), +(2062,2061,'江城','江城区','广东省,阳江市,江城区',3,'','','',''), +(2063,2061,'阳西','阳西县','广东省,阳江市,阳西县',3,'','','',''), +(2064,2061,'阳东','阳东县','广东省,阳江市,阳东县',3,'','','',''), +(2065,2061,'阳春','阳春市','广东省,阳江市,阳春市',3,'','','',''), +(2066,1950,'清远','清远市','广东省,清远市',2,'','','',''), +(2067,2066,'清城','清城区','广东省,清远市,清城区',3,'','','',''), +(2068,2066,'清新','清新区','广东省,清远市,清新区',3,'','','',''), +(2069,2066,'佛冈','佛冈县','广东省,清远市,佛冈县',3,'','','',''), +(2070,2066,'阳山','阳山县','广东省,清远市,阳山县',3,'','','',''), +(2071,2066,'连山壮族瑶族','连山壮族瑶族自治县','广东省,清远市,连山壮族瑶族自治县',3,'','','',''), +(2072,2066,'连南瑶族','连南瑶族自治县','广东省,清远市,连南瑶族自治县',3,'','','',''), +(2073,2066,'英德','英德市','广东省,清远市,英德市',3,'','','',''), +(2074,2066,'连州','连州市','广东省,清远市,连州市',3,'','','',''), +(2075,1950,'东莞','东莞市','广东省,东莞市',2,'','','',''), +(2076,2075,'东莞','东莞市','广东省,东莞市,东莞市',3,'','','',''), +(2077,1950,'中山','中山市','广东省,中山市',2,'','','',''), +(2078,2077,'中山','中山市','广东省,中山市,中山市',3,'','','',''), +(2079,1950,'潮州','潮州市','广东省,潮州市',2,'','','',''), +(2080,2079,'湘桥','湘桥区','广东省,潮州市,湘桥区',3,'','','',''), +(2081,2079,'潮安','潮安区','广东省,潮州市,潮安区',3,'','','',''), +(2082,2079,'饶平','饶平县','广东省,潮州市,饶平县',3,'','','',''), +(2083,1950,'揭阳','揭阳市','广东省,揭阳市',2,'','','',''), +(2084,2083,'榕城','榕城区','广东省,揭阳市,榕城区',3,'','','',''), +(2085,2083,'揭东','揭东区','广东省,揭阳市,揭东区',3,'','','',''), +(2086,2083,'揭西','揭西县','广东省,揭阳市,揭西县',3,'','','',''), +(2087,2083,'惠来','惠来县','广东省,揭阳市,惠来县',3,'','','',''), +(2088,2083,'普宁','普宁市','广东省,揭阳市,普宁市',3,'','','',''), +(2089,1950,'云浮','云浮市','广东省,云浮市',2,'','','',''), +(2090,2089,'云城','云城区','广东省,云浮市,云城区',3,'','','',''), +(2091,2089,'新兴','新兴县','广东省,云浮市,新兴县',3,'','','',''), +(2092,2089,'郁南','郁南县','广东省,云浮市,郁南县',3,'','','',''), +(2093,2089,'云安','云安县','广东省,云浮市,云安县',3,'','','',''), +(2094,2089,'罗定','罗定市','广东省,云浮市,罗定市',3,'','','',''), +(2095,0,'广西','广西壮族自治区','广西壮族自治区',1,'','','',''), +(2096,2095,'南宁','南宁市','广西壮族自治区,南宁市',2,'','','',''), +(2097,2096,'兴宁','兴宁区','广西壮族自治区,南宁市,兴宁区',3,'','','',''), +(2098,2096,'青秀','青秀区','广西壮族自治区,南宁市,青秀区',3,'','','',''), +(2099,2096,'江南','江南区','广西壮族自治区,南宁市,江南区',3,'','','',''), +(2100,2096,'西乡塘','西乡塘区','广西壮族自治区,南宁市,西乡塘区',3,'','','',''); +INSERT IGNORE INTO `fa_area` (`id`,`pid`,`shortname`,`name`,`mergename`,`level`,`pinyin`,`code`,`zip`,`first`) VALUES +(2101,2096,'良庆','良庆区','广西壮族自治区,南宁市,良庆区',3,'','','',''), +(2102,2096,'邕宁','邕宁区','广西壮族自治区,南宁市,邕宁区',3,'','','',''), +(2103,2096,'武鸣','武鸣县','广西壮族自治区,南宁市,武鸣县',3,'','','',''), +(2104,2096,'隆安','隆安县','广西壮族自治区,南宁市,隆安县',3,'','','',''), +(2105,2096,'马山','马山县','广西壮族自治区,南宁市,马山县',3,'','','',''), +(2106,2096,'上林','上林县','广西壮族自治区,南宁市,上林县',3,'','','',''), +(2107,2096,'宾阳','宾阳县','广西壮族自治区,南宁市,宾阳县',3,'','','',''), +(2108,2096,'横','横县','广西壮族自治区,南宁市,横县',3,'','','',''), +(2109,2095,'柳州','柳州市','广西壮族自治区,柳州市',2,'','','',''), +(2110,2109,'城中','城中区','广西壮族自治区,柳州市,城中区',3,'','','',''), +(2111,2109,'鱼峰','鱼峰区','广西壮族自治区,柳州市,鱼峰区',3,'','','',''), +(2112,2109,'柳南','柳南区','广西壮族自治区,柳州市,柳南区',3,'','','',''), +(2113,2109,'柳北','柳北区','广西壮族自治区,柳州市,柳北区',3,'','','',''), +(2114,2109,'柳江','柳江县','广西壮族自治区,柳州市,柳江县',3,'','','',''), +(2115,2109,'柳城','柳城县','广西壮族自治区,柳州市,柳城县',3,'','','',''), +(2116,2109,'鹿寨','鹿寨县','广西壮族自治区,柳州市,鹿寨县',3,'','','',''), +(2117,2109,'融安','融安县','广西壮族自治区,柳州市,融安县',3,'','','',''), +(2118,2109,'融水苗族','融水苗族自治县','广西壮族自治区,柳州市,融水苗族自治县',3,'','','',''), +(2119,2109,'三江侗族','三江侗族自治县','广西壮族自治区,柳州市,三江侗族自治县',3,'','','',''), +(2120,2095,'桂林','桂林市','广西壮族自治区,桂林市',2,'','','',''), +(2121,2120,'秀峰','秀峰区','广西壮族自治区,桂林市,秀峰区',3,'','','',''), +(2122,2120,'叠彩','叠彩区','广西壮族自治区,桂林市,叠彩区',3,'','','',''), +(2123,2120,'象山','象山区','广西壮族自治区,桂林市,象山区',3,'','','',''), +(2124,2120,'七星','七星区','广西壮族自治区,桂林市,七星区',3,'','','',''), +(2125,2120,'雁山','雁山区','广西壮族自治区,桂林市,雁山区',3,'','','',''), +(2126,2120,'临桂','临桂区','广西壮族自治区,桂林市,临桂区',3,'','','',''), +(2127,2120,'阳朔','阳朔县','广西壮族自治区,桂林市,阳朔县',3,'','','',''), +(2128,2120,'灵川','灵川县','广西壮族自治区,桂林市,灵川县',3,'','','',''), +(2129,2120,'全州','全州县','广西壮族自治区,桂林市,全州县',3,'','','',''), +(2130,2120,'兴安','兴安县','广西壮族自治区,桂林市,兴安县',3,'','','',''), +(2131,2120,'永福','永福县','广西壮族自治区,桂林市,永福县',3,'','','',''), +(2132,2120,'灌阳','灌阳县','广西壮族自治区,桂林市,灌阳县',3,'','','',''), +(2133,2120,'龙胜各族','龙胜各族自治县','广西壮族自治区,桂林市,龙胜各族自治县',3,'','','',''), +(2134,2120,'资源','资源县','广西壮族自治区,桂林市,资源县',3,'','','',''), +(2135,2120,'平乐','平乐县','广西壮族自治区,桂林市,平乐县',3,'','','',''), +(2136,2120,'荔浦','荔浦县','广西壮族自治区,桂林市,荔浦县',3,'','','',''), +(2137,2120,'恭城瑶族','恭城瑶族自治县','广西壮族自治区,桂林市,恭城瑶族自治县',3,'','','',''), +(2138,2095,'梧州','梧州市','广西壮族自治区,梧州市',2,'','','',''), +(2139,2138,'万秀','万秀区','广西壮族自治区,梧州市,万秀区',3,'','','',''), +(2140,2138,'长洲','长洲区','广西壮族自治区,梧州市,长洲区',3,'','','',''), +(2141,2138,'龙圩','龙圩区','广西壮族自治区,梧州市,龙圩区',3,'','','',''), +(2142,2138,'苍梧','苍梧县','广西壮族自治区,梧州市,苍梧县',3,'','','',''), +(2143,2138,'藤','藤县','广西壮族自治区,梧州市,藤县',3,'','','',''), +(2144,2138,'蒙山','蒙山县','广西壮族自治区,梧州市,蒙山县',3,'','','',''), +(2145,2138,'岑溪','岑溪市','广西壮族自治区,梧州市,岑溪市',3,'','','',''), +(2146,2095,'北海','北海市','广西壮族自治区,北海市',2,'','','',''), +(2147,2146,'海城','海城区','广西壮族自治区,北海市,海城区',3,'','','',''), +(2148,2146,'银海','银海区','广西壮族自治区,北海市,银海区',3,'','','',''), +(2149,2146,'铁山港','铁山港区','广西壮族自治区,北海市,铁山港区',3,'','','',''), +(2150,2146,'合浦','合浦县','广西壮族自治区,北海市,合浦县',3,'','','',''), +(2151,2095,'防城港','防城港市','广西壮族自治区,防城港市',2,'','','',''), +(2152,2151,'港口','港口区','广西壮族自治区,防城港市,港口区',3,'','','',''), +(2153,2151,'防城','防城区','广西壮族自治区,防城港市,防城区',3,'','','',''), +(2154,2151,'上思','上思县','广西壮族自治区,防城港市,上思县',3,'','','',''), +(2155,2151,'东兴','东兴市','广西壮族自治区,防城港市,东兴市',3,'','','',''), +(2156,2095,'钦州','钦州市','广西壮族自治区,钦州市',2,'','','',''), +(2157,2156,'钦南','钦南区','广西壮族自治区,钦州市,钦南区',3,'','','',''), +(2158,2156,'钦北','钦北区','广西壮族自治区,钦州市,钦北区',3,'','','',''), +(2159,2156,'灵山','灵山县','广西壮族自治区,钦州市,灵山县',3,'','','',''), +(2160,2156,'浦北','浦北县','广西壮族自治区,钦州市,浦北县',3,'','','',''), +(2161,2095,'贵港','贵港市','广西壮族自治区,贵港市',2,'','','',''), +(2162,2161,'港北','港北区','广西壮族自治区,贵港市,港北区',3,'','','',''), +(2163,2161,'港南','港南区','广西壮族自治区,贵港市,港南区',3,'','','',''), +(2164,2161,'覃塘','覃塘区','广西壮族自治区,贵港市,覃塘区',3,'','','',''), +(2165,2161,'平南','平南县','广西壮族自治区,贵港市,平南县',3,'','','',''), +(2166,2161,'桂平','桂平市','广西壮族自治区,贵港市,桂平市',3,'','','',''), +(2167,2095,'玉林','玉林市','广西壮族自治区,玉林市',2,'','','',''), +(2168,2167,'玉州','玉州区','广西壮族自治区,玉林市,玉州区',3,'','','',''), +(2169,2167,'福绵','福绵区','广西壮族自治区,玉林市,福绵区',3,'','','',''), +(2170,2167,'容','容县','广西壮族自治区,玉林市,容县',3,'','','',''), +(2171,2167,'陆川','陆川县','广西壮族自治区,玉林市,陆川县',3,'','','',''), +(2172,2167,'博白','博白县','广西壮族自治区,玉林市,博白县',3,'','','',''), +(2173,2167,'兴业','兴业县','广西壮族自治区,玉林市,兴业县',3,'','','',''), +(2174,2167,'北流','北流市','广西壮族自治区,玉林市,北流市',3,'','','',''), +(2175,2095,'百色','百色市','广西壮族自治区,百色市',2,'','','',''), +(2176,2175,'右江','右江区','广西壮族自治区,百色市,右江区',3,'','','',''), +(2177,2175,'田阳','田阳县','广西壮族自治区,百色市,田阳县',3,'','','',''), +(2178,2175,'田东','田东县','广西壮族自治区,百色市,田东县',3,'','','',''), +(2179,2175,'平果','平果县','广西壮族自治区,百色市,平果县',3,'','','',''), +(2180,2175,'德保','德保县','广西壮族自治区,百色市,德保县',3,'','','',''), +(2181,2175,'靖西','靖西县','广西壮族自治区,百色市,靖西县',3,'','','',''), +(2182,2175,'那坡','那坡县','广西壮族自治区,百色市,那坡县',3,'','','',''), +(2183,2175,'凌云','凌云县','广西壮族自治区,百色市,凌云县',3,'','','',''), +(2184,2175,'乐业','乐业县','广西壮族自治区,百色市,乐业县',3,'','','',''), +(2185,2175,'田林','田林县','广西壮族自治区,百色市,田林县',3,'','','',''), +(2186,2175,'西林','西林县','广西壮族自治区,百色市,西林县',3,'','','',''), +(2187,2175,'隆林各族','隆林各族自治县','广西壮族自治区,百色市,隆林各族自治县',3,'','','',''), +(2188,2095,'贺州','贺州市','广西壮族自治区,贺州市',2,'','','',''), +(2189,2188,'八步','八步区','广西壮族自治区,贺州市,八步区',3,'','','',''), +(2190,2188,'昭平','昭平县','广西壮族自治区,贺州市,昭平县',3,'','','',''), +(2191,2188,'钟山','钟山县','广西壮族自治区,贺州市,钟山县',3,'','','',''), +(2192,2188,'富川瑶族','富川瑶族自治县','广西壮族自治区,贺州市,富川瑶族自治县',3,'','','',''), +(2193,2095,'河池','河池市','广西壮族自治区,河池市',2,'','','',''), +(2194,2193,'金城江','金城江区','广西壮族自治区,河池市,金城江区',3,'','','',''), +(2195,2193,'南丹','南丹县','广西壮族自治区,河池市,南丹县',3,'','','',''), +(2196,2193,'天峨','天峨县','广西壮族自治区,河池市,天峨县',3,'','','',''), +(2197,2193,'凤山','凤山县','广西壮族自治区,河池市,凤山县',3,'','','',''), +(2198,2193,'东兰','东兰县','广西壮族自治区,河池市,东兰县',3,'','','',''), +(2199,2193,'罗城仫佬族','罗城仫佬族自治县','广西壮族自治区,河池市,罗城仫佬族自治县',3,'','','',''), +(2200,2193,'环江毛南族','环江毛南族自治县','广西壮族自治区,河池市,环江毛南族自治县',3,'','','',''), +(2201,2193,'巴马瑶族','巴马瑶族自治县','广西壮族自治区,河池市,巴马瑶族自治县',3,'','','',''), +(2202,2193,'都安瑶族','都安瑶族自治县','广西壮族自治区,河池市,都安瑶族自治县',3,'','','',''), +(2203,2193,'大化瑶族','大化瑶族自治县','广西壮族自治区,河池市,大化瑶族自治县',3,'','','',''), +(2204,2193,'宜州','宜州市','广西壮族自治区,河池市,宜州市',3,'','','',''), +(2205,2095,'来宾','来宾市','广西壮族自治区,来宾市',2,'','','',''), +(2206,2205,'兴宾','兴宾区','广西壮族自治区,来宾市,兴宾区',3,'','','',''), +(2207,2205,'忻城','忻城县','广西壮族自治区,来宾市,忻城县',3,'','','',''), +(2208,2205,'象州','象州县','广西壮族自治区,来宾市,象州县',3,'','','',''), +(2209,2205,'武宣','武宣县','广西壮族自治区,来宾市,武宣县',3,'','','',''), +(2210,2205,'金秀瑶族','金秀瑶族自治县','广西壮族自治区,来宾市,金秀瑶族自治县',3,'','','',''), +(2211,2205,'合山','合山市','广西壮族自治区,来宾市,合山市',3,'','','',''), +(2212,2095,'崇左','崇左市','广西壮族自治区,崇左市',2,'','','',''), +(2213,2212,'江州','江州区','广西壮族自治区,崇左市,江州区',3,'','','',''), +(2214,2212,'扶绥','扶绥县','广西壮族自治区,崇左市,扶绥县',3,'','','',''), +(2215,2212,'宁明','宁明县','广西壮族自治区,崇左市,宁明县',3,'','','',''), +(2216,2212,'龙州','龙州县','广西壮族自治区,崇左市,龙州县',3,'','','',''), +(2217,2212,'大新','大新县','广西壮族自治区,崇左市,大新县',3,'','','',''), +(2218,2212,'天等','天等县','广西壮族自治区,崇左市,天等县',3,'','','',''), +(2219,2212,'凭祥','凭祥市','广西壮族自治区,崇左市,凭祥市',3,'','','',''), +(2220,0,'海南','海南省','海南省',1,'','','',''), +(2221,2220,'海口','海口市','海南省,海口市',2,'','','',''), +(2222,2221,'秀英','秀英区','海南省,海口市,秀英区',3,'','','',''), +(2223,2221,'龙华','龙华区','海南省,海口市,龙华区',3,'','','',''), +(2224,2221,'琼山','琼山区','海南省,海口市,琼山区',3,'','','',''), +(2225,2221,'美兰','美兰区','海南省,海口市,美兰区',3,'','','',''), +(2226,2220,'三亚','三亚市','海南省,三亚市',2,'','','',''), +(2227,2226,'三亚','三亚市','海南省,三亚市,三亚市',3,'','','',''), +(2228,2220,'三沙','三沙市','海南省,三沙市',2,'','','',''), +(2229,2228,'西沙群岛','西沙群岛','海南省,三沙市,西沙群岛',3,'','','',''), +(2230,2228,'南沙群岛','南沙群岛','海南省,三沙市,南沙群岛',3,'','','',''), +(2231,2228,'中沙群岛的岛礁及其海域','中沙群岛的岛礁及其海域','海南省,三沙市,中沙群岛的岛礁及其海域',3,'','','',''), +(2232,2220,'五指山','五指山市','海南省,五指山市',2,'','','',''), +(2233,2232,'五指山','五指山市','海南省,五指山市,五指山市',3,'','','',''), +(2234,2220,'琼海','琼海市','海南省,琼海市',2,'','','',''), +(2235,2234,'琼海','琼海市','海南省,琼海市,琼海市',3,'','','',''), +(2236,2220,'儋州','儋州市','海南省,儋州市',2,'','','',''), +(2237,2236,'儋州','儋州市','海南省,儋州市,儋州市',3,'','','',''), +(2238,2220,'文昌','文昌市','海南省,文昌市',2,'','','',''), +(2239,2238,'文昌','文昌市','海南省,文昌市,文昌市',3,'','','',''), +(2240,2220,'万宁','万宁市','海南省,万宁市',2,'','','',''), +(2241,2240,'万宁','万宁市','海南省,万宁市,万宁市',3,'','','',''), +(2242,2220,'东方','东方市','海南省,东方市',2,'','','',''), +(2243,2242,'东方','东方市','海南省,东方市,东方市',3,'','','',''), +(2244,2220,'定安','定安县','海南省,定安县',2,'','','',''), +(2245,2244,'定安','定安县','海南省,定安县,定安县',3,'','','',''), +(2246,2220,'屯昌','屯昌县','海南省,屯昌县',2,'','','',''), +(2247,2246,'屯昌','屯昌县','海南省,屯昌县,屯昌县',3,'','','',''), +(2248,2220,'澄迈','澄迈县','海南省,澄迈县',2,'','','',''), +(2249,2248,'澄迈','澄迈县','海南省,澄迈县,澄迈县',3,'','','',''), +(2250,2220,'临高','临高县','海南省,临高县',2,'','','',''), +(2251,2250,'临高','临高县','海南省,临高县,临高县',3,'','','',''), +(2252,2220,'白沙黎族','白沙黎族自治县','海南省,白沙黎族自治县',2,'','','',''), +(2253,2252,'白沙黎族','白沙黎族自治县','海南省,白沙黎族自治县,白沙黎族自治县',3,'','','',''), +(2254,2220,'昌江黎族','昌江黎族自治县','海南省,昌江黎族自治县',2,'','','',''), +(2255,2254,'昌江黎族','昌江黎族自治县','海南省,昌江黎族自治县,昌江黎族自治县',3,'','','',''), +(2256,2220,'乐东黎族','乐东黎族自治县','海南省,乐东黎族自治县',2,'','','',''), +(2257,2256,'乐东黎族','乐东黎族自治县','海南省,乐东黎族自治县,乐东黎族自治县',3,'','','',''), +(2258,2220,'陵水黎族','陵水黎族自治县','海南省,陵水黎族自治县',2,'','','',''), +(2259,2258,'陵水黎族','陵水黎族自治县','海南省,陵水黎族自治县,陵水黎族自治县',3,'','','',''), +(2260,2220,'保亭黎族苗族','保亭黎族苗族自治县','海南省,保亭黎族苗族自治县',2,'','','',''), +(2261,2260,'保亭黎族苗族','保亭黎族苗族自治县','海南省,保亭黎族苗族自治县,保亭黎族苗族自治县',3,'','','',''), +(2262,2220,'琼中黎族苗族','琼中黎族苗族自治县','海南省,琼中黎族苗族自治县',2,'','','',''), +(2263,2262,'琼中黎族苗族','琼中黎族苗族自治县','海南省,琼中黎族苗族自治县,琼中黎族苗族自治县',3,'','','',''), +(2264,0,'重庆','重庆市','重庆市',1,'','','',''), +(2265,2264,'重庆','重庆市','重庆市,重庆市',2,'','','',''), +(2266,2265,'万州','万州区','重庆市,重庆市,万州区',3,'','','',''), +(2267,2265,'涪陵','涪陵区','重庆市,重庆市,涪陵区',3,'','','',''), +(2268,2265,'渝中','渝中区','重庆市,重庆市,渝中区',3,'','','',''), +(2269,2265,'大渡口','大渡口区','重庆市,重庆市,大渡口区',3,'','','',''), +(2270,2265,'江北','江北区','重庆市,重庆市,江北区',3,'','','',''), +(2271,2265,'沙坪坝','沙坪坝区','重庆市,重庆市,沙坪坝区',3,'','','',''), +(2272,2265,'九龙坡','九龙坡区','重庆市,重庆市,九龙坡区',3,'','','',''), +(2273,2265,'南岸','南岸区','重庆市,重庆市,南岸区',3,'','','',''), +(2274,2265,'北碚','北碚区','重庆市,重庆市,北碚区',3,'','','',''), +(2275,2265,'綦江','綦江区','重庆市,重庆市,綦江区',3,'','','',''), +(2276,2265,'大足','大足区','重庆市,重庆市,大足区',3,'','','',''), +(2277,2265,'渝北','渝北区','重庆市,重庆市,渝北区',3,'','','',''), +(2278,2265,'巴南','巴南区','重庆市,重庆市,巴南区',3,'','','',''), +(2279,2265,'黔江','黔江区','重庆市,重庆市,黔江区',3,'','','',''), +(2280,2265,'长寿','长寿区','重庆市,重庆市,长寿区',3,'','','',''), +(2281,2265,'江津','江津区','重庆市,重庆市,江津区',3,'','','',''), +(2282,2265,'合川','合川区','重庆市,重庆市,合川区',3,'','','',''), +(2283,2265,'永川','永川区','重庆市,重庆市,永川区',3,'','','',''), +(2284,2265,'南川','南川区','重庆市,重庆市,南川区',3,'','','',''), +(2285,2265,'潼南','潼南县','重庆市,重庆市,潼南县',3,'','','',''), +(2286,2265,'铜梁','铜梁县','重庆市,重庆市,铜梁县',3,'','','',''), +(2287,2265,'荣昌','荣昌县','重庆市,重庆市,荣昌县',3,'','','',''), +(2288,2265,'璧山','璧山县','重庆市,重庆市,璧山县',3,'','','',''), +(2289,2265,'梁平','梁平县','重庆市,重庆市,梁平县',3,'','','',''), +(2290,2265,'城口','城口县','重庆市,重庆市,城口县',3,'','','',''), +(2291,2265,'丰都','丰都县','重庆市,重庆市,丰都县',3,'','','',''), +(2292,2265,'垫江','垫江县','重庆市,重庆市,垫江县',3,'','','',''), +(2293,2265,'武隆','武隆县','重庆市,重庆市,武隆县',3,'','','',''), +(2294,2265,'忠','忠县','重庆市,重庆市,忠县',3,'','','',''), +(2295,2265,'开','开县','重庆市,重庆市,开县',3,'','','',''), +(2296,2265,'云阳','云阳县','重庆市,重庆市,云阳县',3,'','','',''), +(2297,2265,'奉节','奉节县','重庆市,重庆市,奉节县',3,'','','',''), +(2298,2265,'巫山','巫山县','重庆市,重庆市,巫山县',3,'','','',''), +(2299,2265,'巫溪','巫溪县','重庆市,重庆市,巫溪县',3,'','','',''), +(2300,2265,'石柱土家族','石柱土家族自治县','重庆市,重庆市,石柱土家族自治县',3,'','','',''), +(2301,2265,'秀山土家族苗族','秀山土家族苗族自治县','重庆市,重庆市,秀山土家族苗族自治县',3,'','','',''), +(2302,2265,'酉阳土家族苗族','酉阳土家族苗族自治县','重庆市,重庆市,酉阳土家族苗族自治县',3,'','','',''), +(2303,2265,'彭水苗族土家族','彭水苗族土家族自治县','重庆市,重庆市,彭水苗族土家族自治县',3,'','','',''), +(2304,0,'四川','四川省','四川省',1,'','','',''), +(2305,2304,'成都','成都市','四川省,成都市',2,'','','',''), +(2306,2305,'锦江','锦江区','四川省,成都市,锦江区',3,'','','',''), +(2307,2305,'青羊','青羊区','四川省,成都市,青羊区',3,'','','',''), +(2308,2305,'金牛','金牛区','四川省,成都市,金牛区',3,'','','',''), +(2309,2305,'武侯','武侯区','四川省,成都市,武侯区',3,'','','',''), +(2310,2305,'成华','成华区','四川省,成都市,成华区',3,'','','',''), +(2311,2305,'龙泉驿','龙泉驿区','四川省,成都市,龙泉驿区',3,'','','',''), +(2312,2305,'青白江','青白江区','四川省,成都市,青白江区',3,'','','',''), +(2313,2305,'新都','新都区','四川省,成都市,新都区',3,'','','',''), +(2314,2305,'温江','温江区','四川省,成都市,温江区',3,'','','',''), +(2315,2305,'金堂','金堂县','四川省,成都市,金堂县',3,'','','',''), +(2316,2305,'双流','双流县','四川省,成都市,双流县',3,'','','',''), +(2317,2305,'郫','郫县','四川省,成都市,郫县',3,'','','',''), +(2318,2305,'大邑','大邑县','四川省,成都市,大邑县',3,'','','',''), +(2319,2305,'蒲江','蒲江县','四川省,成都市,蒲江县',3,'','','',''), +(2320,2305,'新津','新津县','四川省,成都市,新津县',3,'','','',''), +(2321,2305,'都江堰','都江堰市','四川省,成都市,都江堰市',3,'','','',''), +(2322,2305,'彭州','彭州市','四川省,成都市,彭州市',3,'','','',''), +(2323,2305,'邛崃','邛崃市','四川省,成都市,邛崃市',3,'','','',''), +(2324,2305,'崇州','崇州市','四川省,成都市,崇州市',3,'','','',''), +(2325,2304,'自贡','自贡市','四川省,自贡市',2,'','','',''), +(2326,2325,'自流井','自流井区','四川省,自贡市,自流井区',3,'','','',''), +(2327,2325,'贡井','贡井区','四川省,自贡市,贡井区',3,'','','',''), +(2328,2325,'大安','大安区','四川省,自贡市,大安区',3,'','','',''), +(2329,2325,'沿滩','沿滩区','四川省,自贡市,沿滩区',3,'','','',''), +(2330,2325,'荣','荣县','四川省,自贡市,荣县',3,'','','',''), +(2331,2325,'富顺','富顺县','四川省,自贡市,富顺县',3,'','','',''), +(2332,2304,'攀枝花','攀枝花市','四川省,攀枝花市',2,'','','',''), +(2333,2332,'东','东区','四川省,攀枝花市,东区',3,'','','',''), +(2334,2332,'西','西区','四川省,攀枝花市,西区',3,'','','',''), +(2335,2332,'仁和','仁和区','四川省,攀枝花市,仁和区',3,'','','',''), +(2336,2332,'米易','米易县','四川省,攀枝花市,米易县',3,'','','',''), +(2337,2332,'盐边','盐边县','四川省,攀枝花市,盐边县',3,'','','',''), +(2338,2304,'泸州','泸州市','四川省,泸州市',2,'','','',''), +(2339,2338,'江阳','江阳区','四川省,泸州市,江阳区',3,'','','',''), +(2340,2338,'纳溪','纳溪区','四川省,泸州市,纳溪区',3,'','','',''), +(2341,2338,'龙马潭','龙马潭区','四川省,泸州市,龙马潭区',3,'','','',''), +(2342,2338,'泸','泸县','四川省,泸州市,泸县',3,'','','',''), +(2343,2338,'合江','合江县','四川省,泸州市,合江县',3,'','','',''), +(2344,2338,'叙永','叙永县','四川省,泸州市,叙永县',3,'','','',''), +(2345,2338,'古蔺','古蔺县','四川省,泸州市,古蔺县',3,'','','',''), +(2346,2304,'德阳','德阳市','四川省,德阳市',2,'','','',''), +(2347,2346,'旌阳','旌阳区','四川省,德阳市,旌阳区',3,'','','',''), +(2348,2346,'中江','中江县','四川省,德阳市,中江县',3,'','','',''), +(2349,2346,'罗江','罗江县','四川省,德阳市,罗江县',3,'','','',''), +(2350,2346,'广汉','广汉市','四川省,德阳市,广汉市',3,'','','',''), +(2351,2346,'什邡','什邡市','四川省,德阳市,什邡市',3,'','','',''), +(2352,2346,'绵竹','绵竹市','四川省,德阳市,绵竹市',3,'','','',''), +(2353,2304,'绵阳','绵阳市','四川省,绵阳市',2,'','','',''), +(2354,2353,'涪城','涪城区','四川省,绵阳市,涪城区',3,'','','',''), +(2355,2353,'游仙','游仙区','四川省,绵阳市,游仙区',3,'','','',''), +(2356,2353,'三台','三台县','四川省,绵阳市,三台县',3,'','','',''), +(2357,2353,'盐亭','盐亭县','四川省,绵阳市,盐亭县',3,'','','',''), +(2358,2353,'安','安县','四川省,绵阳市,安县',3,'','','',''), +(2359,2353,'梓潼','梓潼县','四川省,绵阳市,梓潼县',3,'','','',''), +(2360,2353,'北川羌族','北川羌族自治县','四川省,绵阳市,北川羌族自治县',3,'','','',''), +(2361,2353,'平武','平武县','四川省,绵阳市,平武县',3,'','','',''), +(2362,2353,'江油','江油市','四川省,绵阳市,江油市',3,'','','',''), +(2363,2304,'广元','广元市','四川省,广元市',2,'','','',''), +(2364,2363,'利州','利州区','四川省,广元市,利州区',3,'','','',''), +(2365,2363,'元坝','元坝区','四川省,广元市,元坝区',3,'','','',''), +(2366,2363,'朝天','朝天区','四川省,广元市,朝天区',3,'','','',''), +(2367,2363,'旺苍','旺苍县','四川省,广元市,旺苍县',3,'','','',''), +(2368,2363,'青川','青川县','四川省,广元市,青川县',3,'','','',''), +(2369,2363,'剑阁','剑阁县','四川省,广元市,剑阁县',3,'','','',''), +(2370,2363,'苍溪','苍溪县','四川省,广元市,苍溪县',3,'','','',''), +(2371,2304,'遂宁','遂宁市','四川省,遂宁市',2,'','','',''), +(2372,2371,'船山','船山区','四川省,遂宁市,船山区',3,'','','',''), +(2373,2371,'安居','安居区','四川省,遂宁市,安居区',3,'','','',''), +(2374,2371,'蓬溪','蓬溪县','四川省,遂宁市,蓬溪县',3,'','','',''), +(2375,2371,'射洪','射洪县','四川省,遂宁市,射洪县',3,'','','',''), +(2376,2371,'大英','大英县','四川省,遂宁市,大英县',3,'','','',''), +(2377,2304,'内江','内江市','四川省,内江市',2,'','','',''), +(2378,2377,'市中','市中区','四川省,内江市,市中区',3,'','','',''), +(2379,2377,'东兴','东兴区','四川省,内江市,东兴区',3,'','','',''), +(2380,2377,'威远','威远县','四川省,内江市,威远县',3,'','','',''), +(2381,2377,'资中','资中县','四川省,内江市,资中县',3,'','','',''), +(2382,2377,'隆昌','隆昌县','四川省,内江市,隆昌县',3,'','','',''), +(2383,2304,'乐山','乐山市','四川省,乐山市',2,'','','',''), +(2384,2383,'市中','市中区','四川省,乐山市,市中区',3,'','','',''), +(2385,2383,'沙湾','沙湾区','四川省,乐山市,沙湾区',3,'','','',''), +(2386,2383,'五通桥','五通桥区','四川省,乐山市,五通桥区',3,'','','',''), +(2387,2383,'金口河','金口河区','四川省,乐山市,金口河区',3,'','','',''), +(2388,2383,'犍为','犍为县','四川省,乐山市,犍为县',3,'','','',''), +(2389,2383,'井研','井研县','四川省,乐山市,井研县',3,'','','',''), +(2390,2383,'夹江','夹江县','四川省,乐山市,夹江县',3,'','','',''), +(2391,2383,'沐川','沐川县','四川省,乐山市,沐川县',3,'','','',''), +(2392,2383,'峨边彝族','峨边彝族自治县','四川省,乐山市,峨边彝族自治县',3,'','','',''), +(2393,2383,'马边彝族','马边彝族自治县','四川省,乐山市,马边彝族自治县',3,'','','',''), +(2394,2383,'峨眉山','峨眉山市','四川省,乐山市,峨眉山市',3,'','','',''), +(2395,2304,'南充','南充市','四川省,南充市',2,'','','',''), +(2396,2395,'顺庆','顺庆区','四川省,南充市,顺庆区',3,'','','',''), +(2397,2395,'高坪','高坪区','四川省,南充市,高坪区',3,'','','',''), +(2398,2395,'嘉陵','嘉陵区','四川省,南充市,嘉陵区',3,'','','',''), +(2399,2395,'南部','南部县','四川省,南充市,南部县',3,'','','',''), +(2400,2395,'营山','营山县','四川省,南充市,营山县',3,'','','',''); +INSERT IGNORE INTO `fa_area` (`id`,`pid`,`shortname`,`name`,`mergename`,`level`,`pinyin`,`code`,`zip`,`first`) VALUES +(2401,2395,'蓬安','蓬安县','四川省,南充市,蓬安县',3,'','','',''), +(2402,2395,'仪陇','仪陇县','四川省,南充市,仪陇县',3,'','','',''), +(2403,2395,'西充','西充县','四川省,南充市,西充县',3,'','','',''), +(2404,2395,'阆中','阆中市','四川省,南充市,阆中市',3,'','','',''), +(2405,2304,'眉山','眉山市','四川省,眉山市',2,'','','',''), +(2406,2405,'东坡','东坡区','四川省,眉山市,东坡区',3,'','','',''), +(2407,2405,'仁寿','仁寿县','四川省,眉山市,仁寿县',3,'','','',''), +(2408,2405,'彭山','彭山县','四川省,眉山市,彭山县',3,'','','',''), +(2409,2405,'洪雅','洪雅县','四川省,眉山市,洪雅县',3,'','','',''), +(2410,2405,'丹棱','丹棱县','四川省,眉山市,丹棱县',3,'','','',''), +(2411,2405,'青神','青神县','四川省,眉山市,青神县',3,'','','',''), +(2412,2304,'宜宾','宜宾市','四川省,宜宾市',2,'','','',''), +(2413,2412,'翠屏','翠屏区','四川省,宜宾市,翠屏区',3,'','','',''), +(2414,2412,'南溪','南溪区','四川省,宜宾市,南溪区',3,'','','',''), +(2415,2412,'宜宾','宜宾县','四川省,宜宾市,宜宾县',3,'','','',''), +(2416,2412,'江安','江安县','四川省,宜宾市,江安县',3,'','','',''), +(2417,2412,'长宁','长宁县','四川省,宜宾市,长宁县',3,'','','',''), +(2418,2412,'高','高县','四川省,宜宾市,高县',3,'','','',''), +(2419,2412,'珙','珙县','四川省,宜宾市,珙县',3,'','','',''), +(2420,2412,'筠连','筠连县','四川省,宜宾市,筠连县',3,'','','',''), +(2421,2412,'兴文','兴文县','四川省,宜宾市,兴文县',3,'','','',''), +(2422,2412,'屏山','屏山县','四川省,宜宾市,屏山县',3,'','','',''), +(2423,2304,'广安','广安市','四川省,广安市',2,'','','',''), +(2424,2423,'广安','广安区','四川省,广安市,广安区',3,'','','',''), +(2425,2423,'前锋','前锋区','四川省,广安市,前锋区',3,'','','',''), +(2426,2423,'岳池','岳池县','四川省,广安市,岳池县',3,'','','',''), +(2427,2423,'武胜','武胜县','四川省,广安市,武胜县',3,'','','',''), +(2428,2423,'邻水','邻水县','四川省,广安市,邻水县',3,'','','',''), +(2429,2423,'华蓥','华蓥市','四川省,广安市,华蓥市',3,'','','',''), +(2430,2304,'达州','达州市','四川省,达州市',2,'','','',''), +(2431,2430,'通川','通川区','四川省,达州市,通川区',3,'','','',''), +(2432,2430,'达川','达川区','四川省,达州市,达川区',3,'','','',''), +(2433,2430,'宣汉','宣汉县','四川省,达州市,宣汉县',3,'','','',''), +(2434,2430,'开江','开江县','四川省,达州市,开江县',3,'','','',''), +(2435,2430,'大竹','大竹县','四川省,达州市,大竹县',3,'','','',''), +(2436,2430,'渠','渠县','四川省,达州市,渠县',3,'','','',''), +(2437,2430,'万源','万源市','四川省,达州市,万源市',3,'','','',''), +(2438,2304,'雅安','雅安市','四川省,雅安市',2,'','','',''), +(2439,2438,'雨城','雨城区','四川省,雅安市,雨城区',3,'','','',''), +(2440,2438,'名山','名山区','四川省,雅安市,名山区',3,'','','',''), +(2441,2438,'荥经','荥经县','四川省,雅安市,荥经县',3,'','','',''), +(2442,2438,'汉源','汉源县','四川省,雅安市,汉源县',3,'','','',''), +(2443,2438,'石棉','石棉县','四川省,雅安市,石棉县',3,'','','',''), +(2444,2438,'天全','天全县','四川省,雅安市,天全县',3,'','','',''), +(2445,2438,'芦山','芦山县','四川省,雅安市,芦山县',3,'','','',''), +(2446,2438,'宝兴','宝兴县','四川省,雅安市,宝兴县',3,'','','',''), +(2447,2304,'巴中','巴中市','四川省,巴中市',2,'','','',''), +(2448,2447,'巴州','巴州区','四川省,巴中市,巴州区',3,'','','',''), +(2449,2447,'恩阳','恩阳区','四川省,巴中市,恩阳区',3,'','','',''), +(2450,2447,'通江','通江县','四川省,巴中市,通江县',3,'','','',''), +(2451,2447,'南江','南江县','四川省,巴中市,南江县',3,'','','',''), +(2452,2447,'平昌','平昌县','四川省,巴中市,平昌县',3,'','','',''), +(2453,2304,'资阳','资阳市','四川省,资阳市',2,'','','',''), +(2454,2453,'雁江','雁江区','四川省,资阳市,雁江区',3,'','','',''), +(2455,2453,'安岳','安岳县','四川省,资阳市,安岳县',3,'','','',''), +(2456,2453,'乐至','乐至县','四川省,资阳市,乐至县',3,'','','',''), +(2457,2453,'简阳','简阳市','四川省,资阳市,简阳市',3,'','','',''), +(2458,2304,'阿坝藏族羌族','阿坝藏族羌族自治州','四川省,阿坝藏族羌族自治州',2,'','','',''), +(2459,2458,'汶川','汶川县','四川省,阿坝藏族羌族自治州,汶川县',3,'','','',''), +(2460,2458,'理','理县','四川省,阿坝藏族羌族自治州,理县',3,'','','',''), +(2461,2458,'茂','茂县','四川省,阿坝藏族羌族自治州,茂县',3,'','','',''), +(2462,2458,'松潘','松潘县','四川省,阿坝藏族羌族自治州,松潘县',3,'','','',''), +(2463,2458,'九寨沟','九寨沟县','四川省,阿坝藏族羌族自治州,九寨沟县',3,'','','',''), +(2464,2458,'金川','金川县','四川省,阿坝藏族羌族自治州,金川县',3,'','','',''), +(2465,2458,'小金','小金县','四川省,阿坝藏族羌族自治州,小金县',3,'','','',''), +(2466,2458,'黑水','黑水县','四川省,阿坝藏族羌族自治州,黑水县',3,'','','',''), +(2467,2458,'马尔康','马尔康县','四川省,阿坝藏族羌族自治州,马尔康县',3,'','','',''), +(2468,2458,'壤塘','壤塘县','四川省,阿坝藏族羌族自治州,壤塘县',3,'','','',''), +(2469,2458,'阿坝','阿坝县','四川省,阿坝藏族羌族自治州,阿坝县',3,'','','',''), +(2470,2458,'若尔盖','若尔盖县','四川省,阿坝藏族羌族自治州,若尔盖县',3,'','','',''), +(2471,2458,'红原','红原县','四川省,阿坝藏族羌族自治州,红原县',3,'','','',''), +(2472,2304,'甘孜藏族','甘孜藏族自治州','四川省,甘孜藏族自治州',2,'','','',''), +(2473,2472,'康定','康定县','四川省,甘孜藏族自治州,康定县',3,'','','',''), +(2474,2472,'泸定','泸定县','四川省,甘孜藏族自治州,泸定县',3,'','','',''), +(2475,2472,'丹巴','丹巴县','四川省,甘孜藏族自治州,丹巴县',3,'','','',''), +(2476,2472,'九龙','九龙县','四川省,甘孜藏族自治州,九龙县',3,'','','',''), +(2477,2472,'雅江','雅江县','四川省,甘孜藏族自治州,雅江县',3,'','','',''), +(2478,2472,'道孚','道孚县','四川省,甘孜藏族自治州,道孚县',3,'','','',''), +(2479,2472,'炉霍','炉霍县','四川省,甘孜藏族自治州,炉霍县',3,'','','',''), +(2480,2472,'甘孜','甘孜县','四川省,甘孜藏族自治州,甘孜县',3,'','','',''), +(2481,2472,'新龙','新龙县','四川省,甘孜藏族自治州,新龙县',3,'','','',''), +(2482,2472,'德格','德格县','四川省,甘孜藏族自治州,德格县',3,'','','',''), +(2483,2472,'白玉','白玉县','四川省,甘孜藏族自治州,白玉县',3,'','','',''), +(2484,2472,'石渠','石渠县','四川省,甘孜藏族自治州,石渠县',3,'','','',''), +(2485,2472,'色达','色达县','四川省,甘孜藏族自治州,色达县',3,'','','',''), +(2486,2472,'理塘','理塘县','四川省,甘孜藏族自治州,理塘县',3,'','','',''), +(2487,2472,'巴塘','巴塘县','四川省,甘孜藏族自治州,巴塘县',3,'','','',''), +(2488,2472,'乡城','乡城县','四川省,甘孜藏族自治州,乡城县',3,'','','',''), +(2489,2472,'稻城','稻城县','四川省,甘孜藏族自治州,稻城县',3,'','','',''), +(2490,2472,'得荣','得荣县','四川省,甘孜藏族自治州,得荣县',3,'','','',''), +(2491,2304,'凉山彝族','凉山彝族自治州','四川省,凉山彝族自治州',2,'','','',''), +(2492,2491,'西昌','西昌市','四川省,凉山彝族自治州,西昌市',3,'','','',''), +(2493,2491,'木里藏族','木里藏族自治县','四川省,凉山彝族自治州,木里藏族自治县',3,'','','',''), +(2494,2491,'盐源','盐源县','四川省,凉山彝族自治州,盐源县',3,'','','',''), +(2495,2491,'德昌','德昌县','四川省,凉山彝族自治州,德昌县',3,'','','',''), +(2496,2491,'会理','会理县','四川省,凉山彝族自治州,会理县',3,'','','',''), +(2497,2491,'会东','会东县','四川省,凉山彝族自治州,会东县',3,'','','',''), +(2498,2491,'宁南','宁南县','四川省,凉山彝族自治州,宁南县',3,'','','',''), +(2499,2491,'普格','普格县','四川省,凉山彝族自治州,普格县',3,'','','',''), +(2500,2491,'布拖','布拖县','四川省,凉山彝族自治州,布拖县',3,'','','',''), +(2501,2491,'金阳','金阳县','四川省,凉山彝族自治州,金阳县',3,'','','',''), +(2502,2491,'昭觉','昭觉县','四川省,凉山彝族自治州,昭觉县',3,'','','',''), +(2503,2491,'喜德','喜德县','四川省,凉山彝族自治州,喜德县',3,'','','',''), +(2504,2491,'冕宁','冕宁县','四川省,凉山彝族自治州,冕宁县',3,'','','',''), +(2505,2491,'越西','越西县','四川省,凉山彝族自治州,越西县',3,'','','',''), +(2506,2491,'甘洛','甘洛县','四川省,凉山彝族自治州,甘洛县',3,'','','',''), +(2507,2491,'美姑','美姑县','四川省,凉山彝族自治州,美姑县',3,'','','',''), +(2508,2491,'雷波','雷波县','四川省,凉山彝族自治州,雷波县',3,'','','',''), +(2509,0,'贵州','贵州省','贵州省',1,'','','',''), +(2510,2509,'贵阳','贵阳市','贵州省,贵阳市',2,'','','',''), +(2511,2510,'南明','南明区','贵州省,贵阳市,南明区',3,'','','',''), +(2512,2510,'云岩','云岩区','贵州省,贵阳市,云岩区',3,'','','',''), +(2513,2510,'花溪','花溪区','贵州省,贵阳市,花溪区',3,'','','',''), +(2514,2510,'乌当','乌当区','贵州省,贵阳市,乌当区',3,'','','',''), +(2515,2510,'白云','白云区','贵州省,贵阳市,白云区',3,'','','',''), +(2516,2510,'观山湖','观山湖区','贵州省,贵阳市,观山湖区',3,'','','',''), +(2517,2510,'开阳','开阳县','贵州省,贵阳市,开阳县',3,'','','',''), +(2518,2510,'息烽','息烽县','贵州省,贵阳市,息烽县',3,'','','',''), +(2519,2510,'修文','修文县','贵州省,贵阳市,修文县',3,'','','',''), +(2520,2510,'清镇','清镇市','贵州省,贵阳市,清镇市',3,'','','',''), +(2521,2509,'六盘水','六盘水市','贵州省,六盘水市',2,'','','',''), +(2522,2521,'钟山','钟山区','贵州省,六盘水市,钟山区',3,'','','',''), +(2523,2521,'六枝特','六枝特区','贵州省,六盘水市,六枝特区',3,'','','',''), +(2524,2521,'水城','水城县','贵州省,六盘水市,水城县',3,'','','',''), +(2525,2521,'盘','盘县','贵州省,六盘水市,盘县',3,'','','',''), +(2526,2509,'遵义','遵义市','贵州省,遵义市',2,'','','',''), +(2527,2526,'红花岗','红花岗区','贵州省,遵义市,红花岗区',3,'','','',''), +(2528,2526,'汇川','汇川区','贵州省,遵义市,汇川区',3,'','','',''), +(2529,2526,'遵义','遵义县','贵州省,遵义市,遵义县',3,'','','',''), +(2530,2526,'桐梓','桐梓县','贵州省,遵义市,桐梓县',3,'','','',''), +(2531,2526,'绥阳','绥阳县','贵州省,遵义市,绥阳县',3,'','','',''), +(2532,2526,'正安','正安县','贵州省,遵义市,正安县',3,'','','',''), +(2533,2526,'道真仡佬族苗族','道真仡佬族苗族自治县','贵州省,遵义市,道真仡佬族苗族自治县',3,'','','',''), +(2534,2526,'务川仡佬族苗族','务川仡佬族苗族自治县','贵州省,遵义市,务川仡佬族苗族自治县',3,'','','',''), +(2535,2526,'凤冈','凤冈县','贵州省,遵义市,凤冈县',3,'','','',''), +(2536,2526,'湄潭','湄潭县','贵州省,遵义市,湄潭县',3,'','','',''), +(2537,2526,'余庆','余庆县','贵州省,遵义市,余庆县',3,'','','',''), +(2538,2526,'习水','习水县','贵州省,遵义市,习水县',3,'','','',''), +(2539,2526,'赤水','赤水市','贵州省,遵义市,赤水市',3,'','','',''), +(2540,2526,'仁怀','仁怀市','贵州省,遵义市,仁怀市',3,'','','',''), +(2541,2509,'安顺','安顺市','贵州省,安顺市',2,'','','',''), +(2542,2541,'西秀','西秀区','贵州省,安顺市,西秀区',3,'','','',''), +(2543,2541,'平坝','平坝县','贵州省,安顺市,平坝县',3,'','','',''), +(2544,2541,'普定','普定县','贵州省,安顺市,普定县',3,'','','',''), +(2545,2541,'镇宁布依族苗族','镇宁布依族苗族自治县','贵州省,安顺市,镇宁布依族苗族自治县',3,'','','',''), +(2546,2541,'关岭布依族苗族','关岭布依族苗族自治县','贵州省,安顺市,关岭布依族苗族自治县',3,'','','',''), +(2547,2541,'紫云苗族布依族','紫云苗族布依族自治县','贵州省,安顺市,紫云苗族布依族自治县',3,'','','',''), +(2548,2509,'毕节','毕节市','贵州省,毕节市',2,'','','',''), +(2549,2548,'七星关','七星关区','贵州省,毕节市,七星关区',3,'','','',''), +(2550,2548,'大方','大方县','贵州省,毕节市,大方县',3,'','','',''), +(2551,2548,'黔西','黔西县','贵州省,毕节市,黔西县',3,'','','',''), +(2552,2548,'金沙','金沙县','贵州省,毕节市,金沙县',3,'','','',''), +(2553,2548,'织金','织金县','贵州省,毕节市,织金县',3,'','','',''), +(2554,2548,'纳雍','纳雍县','贵州省,毕节市,纳雍县',3,'','','',''), +(2555,2548,'威宁彝族回族苗族','威宁彝族回族苗族自治县','贵州省,毕节市,威宁彝族回族苗族自治县',3,'','','',''), +(2556,2548,'赫章','赫章县','贵州省,毕节市,赫章县',3,'','','',''), +(2557,2509,'铜仁','铜仁市','贵州省,铜仁市',2,'','','',''), +(2558,2557,'碧江','碧江区','贵州省,铜仁市,碧江区',3,'','','',''), +(2559,2557,'万山','万山区','贵州省,铜仁市,万山区',3,'','','',''), +(2560,2557,'江口','江口县','贵州省,铜仁市,江口县',3,'','','',''), +(2561,2557,'玉屏侗族','玉屏侗族自治县','贵州省,铜仁市,玉屏侗族自治县',3,'','','',''), +(2562,2557,'石阡','石阡县','贵州省,铜仁市,石阡县',3,'','','',''), +(2563,2557,'思南','思南县','贵州省,铜仁市,思南县',3,'','','',''), +(2564,2557,'印江土家族苗族','印江土家族苗族自治县','贵州省,铜仁市,印江土家族苗族自治县',3,'','','',''), +(2565,2557,'德江','德江县','贵州省,铜仁市,德江县',3,'','','',''), +(2566,2557,'沿河土家族','沿河土家族自治县','贵州省,铜仁市,沿河土家族自治县',3,'','','',''), +(2567,2557,'松桃苗族','松桃苗族自治县','贵州省,铜仁市,松桃苗族自治县',3,'','','',''), +(2568,2509,'黔西南布依族苗族','黔西南布依族苗族自治州','贵州省,黔西南布依族苗族自治州',2,'','','',''), +(2569,2568,'兴义','兴义市','贵州省,黔西南布依族苗族自治州,兴义市',3,'','','',''), +(2570,2568,'兴仁','兴仁县','贵州省,黔西南布依族苗族自治州,兴仁县',3,'','','',''), +(2571,2568,'普安','普安县','贵州省,黔西南布依族苗族自治州,普安县',3,'','','',''), +(2572,2568,'晴隆','晴隆县','贵州省,黔西南布依族苗族自治州,晴隆县',3,'','','',''), +(2573,2568,'贞丰','贞丰县','贵州省,黔西南布依族苗族自治州,贞丰县',3,'','','',''), +(2574,2568,'望谟','望谟县','贵州省,黔西南布依族苗族自治州,望谟县',3,'','','',''), +(2575,2568,'册亨','册亨县','贵州省,黔西南布依族苗族自治州,册亨县',3,'','','',''), +(2576,2568,'安龙','安龙县','贵州省,黔西南布依族苗族自治州,安龙县',3,'','','',''), +(2577,2509,'黔东南苗族侗族','黔东南苗族侗族自治州','贵州省,黔东南苗族侗族自治州',2,'','','',''), +(2578,2577,'凯里','凯里市','贵州省,黔东南苗族侗族自治州,凯里市',3,'','','',''), +(2579,2577,'黄平','黄平县','贵州省,黔东南苗族侗族自治州,黄平县',3,'','','',''), +(2580,2577,'施秉','施秉县','贵州省,黔东南苗族侗族自治州,施秉县',3,'','','',''), +(2581,2577,'三穗','三穗县','贵州省,黔东南苗族侗族自治州,三穗县',3,'','','',''), +(2582,2577,'镇远','镇远县','贵州省,黔东南苗族侗族自治州,镇远县',3,'','','',''), +(2583,2577,'岑巩','岑巩县','贵州省,黔东南苗族侗族自治州,岑巩县',3,'','','',''), +(2584,2577,'天柱','天柱县','贵州省,黔东南苗族侗族自治州,天柱县',3,'','','',''), +(2585,2577,'锦屏','锦屏县','贵州省,黔东南苗族侗族自治州,锦屏县',3,'','','',''), +(2586,2577,'剑河','剑河县','贵州省,黔东南苗族侗族自治州,剑河县',3,'','','',''), +(2587,2577,'台江','台江县','贵州省,黔东南苗族侗族自治州,台江县',3,'','','',''), +(2588,2577,'黎平','黎平县','贵州省,黔东南苗族侗族自治州,黎平县',3,'','','',''), +(2589,2577,'榕江','榕江县','贵州省,黔东南苗族侗族自治州,榕江县',3,'','','',''), +(2590,2577,'从江','从江县','贵州省,黔东南苗族侗族自治州,从江县',3,'','','',''), +(2591,2577,'雷山','雷山县','贵州省,黔东南苗族侗族自治州,雷山县',3,'','','',''), +(2592,2577,'麻江','麻江县','贵州省,黔东南苗族侗族自治州,麻江县',3,'','','',''), +(2593,2577,'丹寨','丹寨县','贵州省,黔东南苗族侗族自治州,丹寨县',3,'','','',''), +(2594,2509,'黔南布依族苗族','黔南布依族苗族自治州','贵州省,黔南布依族苗族自治州',2,'','','',''), +(2595,2594,'都匀','都匀市','贵州省,黔南布依族苗族自治州,都匀市',3,'','','',''), +(2596,2594,'福泉','福泉市','贵州省,黔南布依族苗族自治州,福泉市',3,'','','',''), +(2597,2594,'荔波','荔波县','贵州省,黔南布依族苗族自治州,荔波县',3,'','','',''), +(2598,2594,'贵定','贵定县','贵州省,黔南布依族苗族自治州,贵定县',3,'','','',''), +(2599,2594,'瓮安','瓮安县','贵州省,黔南布依族苗族自治州,瓮安县',3,'','','',''), +(2600,2594,'独山','独山县','贵州省,黔南布依族苗族自治州,独山县',3,'','','',''), +(2601,2594,'平塘','平塘县','贵州省,黔南布依族苗族自治州,平塘县',3,'','','',''), +(2602,2594,'罗甸','罗甸县','贵州省,黔南布依族苗族自治州,罗甸县',3,'','','',''), +(2603,2594,'长顺','长顺县','贵州省,黔南布依族苗族自治州,长顺县',3,'','','',''), +(2604,2594,'龙里','龙里县','贵州省,黔南布依族苗族自治州,龙里县',3,'','','',''), +(2605,2594,'惠水','惠水县','贵州省,黔南布依族苗族自治州,惠水县',3,'','','',''), +(2606,2594,'三都水族','三都水族自治县','贵州省,黔南布依族苗族自治州,三都水族自治县',3,'','','',''), +(2607,0,'云南','云南省','云南省',1,'','','',''), +(2608,2607,'昆明','昆明市','云南省,昆明市',2,'','','',''), +(2609,2608,'五华','五华区','云南省,昆明市,五华区',3,'','','',''), +(2610,2608,'盘龙','盘龙区','云南省,昆明市,盘龙区',3,'','','',''), +(2611,2608,'官渡','官渡区','云南省,昆明市,官渡区',3,'','','',''), +(2612,2608,'西山','西山区','云南省,昆明市,西山区',3,'','','',''), +(2613,2608,'东川','东川区','云南省,昆明市,东川区',3,'','','',''), +(2614,2608,'呈贡','呈贡区','云南省,昆明市,呈贡区',3,'','','',''), +(2615,2608,'晋宁','晋宁县','云南省,昆明市,晋宁县',3,'','','',''), +(2616,2608,'富民','富民县','云南省,昆明市,富民县',3,'','','',''), +(2617,2608,'宜良','宜良县','云南省,昆明市,宜良县',3,'','','',''), +(2618,2608,'石林彝族','石林彝族自治县','云南省,昆明市,石林彝族自治县',3,'','','',''), +(2619,2608,'嵩明','嵩明县','云南省,昆明市,嵩明县',3,'','','',''), +(2620,2608,'禄劝彝族苗族','禄劝彝族苗族自治县','云南省,昆明市,禄劝彝族苗族自治县',3,'','','',''), +(2621,2608,'寻甸回族彝族','寻甸回族彝族自治县','云南省,昆明市,寻甸回族彝族自治县',3,'','','',''), +(2622,2608,'安宁','安宁市','云南省,昆明市,安宁市',3,'','','',''), +(2623,2607,'曲靖','曲靖市','云南省,曲靖市',2,'','','',''), +(2624,2623,'麒麟','麒麟区','云南省,曲靖市,麒麟区',3,'','','',''), +(2625,2623,'马龙','马龙县','云南省,曲靖市,马龙县',3,'','','',''), +(2626,2623,'陆良','陆良县','云南省,曲靖市,陆良县',3,'','','',''), +(2627,2623,'师宗','师宗县','云南省,曲靖市,师宗县',3,'','','',''), +(2628,2623,'罗平','罗平县','云南省,曲靖市,罗平县',3,'','','',''), +(2629,2623,'富源','富源县','云南省,曲靖市,富源县',3,'','','',''), +(2630,2623,'会泽','会泽县','云南省,曲靖市,会泽县',3,'','','',''), +(2631,2623,'沾益','沾益县','云南省,曲靖市,沾益县',3,'','','',''), +(2632,2623,'宣威','宣威市','云南省,曲靖市,宣威市',3,'','','',''), +(2633,2607,'玉溪','玉溪市','云南省,玉溪市',2,'','','',''), +(2634,2633,'红塔','红塔区','云南省,玉溪市,红塔区',3,'','','',''), +(2635,2633,'江川','江川县','云南省,玉溪市,江川县',3,'','','',''), +(2636,2633,'澄江','澄江县','云南省,玉溪市,澄江县',3,'','','',''), +(2637,2633,'通海','通海县','云南省,玉溪市,通海县',3,'','','',''), +(2638,2633,'华宁','华宁县','云南省,玉溪市,华宁县',3,'','','',''), +(2639,2633,'易门','易门县','云南省,玉溪市,易门县',3,'','','',''), +(2640,2633,'峨山彝族','峨山彝族自治县','云南省,玉溪市,峨山彝族自治县',3,'','','',''), +(2641,2633,'新平彝族傣族','新平彝族傣族自治县','云南省,玉溪市,新平彝族傣族自治县',3,'','','',''), +(2642,2633,'元江哈尼族彝族傣族','元江哈尼族彝族傣族自治县','云南省,玉溪市,元江哈尼族彝族傣族自治县',3,'','','',''), +(2643,2607,'保山','保山市','云南省,保山市',2,'','','',''), +(2644,2643,'隆阳','隆阳区','云南省,保山市,隆阳区',3,'','','',''), +(2645,2643,'施甸','施甸县','云南省,保山市,施甸县',3,'','','',''), +(2646,2643,'腾冲','腾冲县','云南省,保山市,腾冲县',3,'','','',''), +(2647,2643,'龙陵','龙陵县','云南省,保山市,龙陵县',3,'','','',''), +(2648,2643,'昌宁','昌宁县','云南省,保山市,昌宁县',3,'','','',''), +(2649,2607,'昭通','昭通市','云南省,昭通市',2,'','','',''), +(2650,2649,'昭阳','昭阳区','云南省,昭通市,昭阳区',3,'','','',''), +(2651,2649,'鲁甸','鲁甸县','云南省,昭通市,鲁甸县',3,'','','',''), +(2652,2649,'巧家','巧家县','云南省,昭通市,巧家县',3,'','','',''), +(2653,2649,'盐津','盐津县','云南省,昭通市,盐津县',3,'','','',''), +(2654,2649,'大关','大关县','云南省,昭通市,大关县',3,'','','',''), +(2655,2649,'永善','永善县','云南省,昭通市,永善县',3,'','','',''), +(2656,2649,'绥江','绥江县','云南省,昭通市,绥江县',3,'','','',''), +(2657,2649,'镇雄','镇雄县','云南省,昭通市,镇雄县',3,'','','',''), +(2658,2649,'彝良','彝良县','云南省,昭通市,彝良县',3,'','','',''), +(2659,2649,'威信','威信县','云南省,昭通市,威信县',3,'','','',''), +(2660,2649,'水富','水富县','云南省,昭通市,水富县',3,'','','',''), +(2661,2607,'丽江','丽江市','云南省,丽江市',2,'','','',''), +(2662,2661,'古城','古城区','云南省,丽江市,古城区',3,'','','',''), +(2663,2661,'玉龙纳西族','玉龙纳西族自治县','云南省,丽江市,玉龙纳西族自治县',3,'','','',''), +(2664,2661,'永胜','永胜县','云南省,丽江市,永胜县',3,'','','',''), +(2665,2661,'华坪','华坪县','云南省,丽江市,华坪县',3,'','','',''), +(2666,2661,'宁蒗彝族','宁蒗彝族自治县','云南省,丽江市,宁蒗彝族自治县',3,'','','',''), +(2667,2607,'普洱','普洱市','云南省,普洱市',2,'','','',''), +(2668,2667,'思茅','思茅区','云南省,普洱市,思茅区',3,'','','',''), +(2669,2667,'宁洱哈尼族彝族','宁洱哈尼族彝族自治县','云南省,普洱市,宁洱哈尼族彝族自治县',3,'','','',''), +(2670,2667,'墨江哈尼族','墨江哈尼族自治县','云南省,普洱市,墨江哈尼族自治县',3,'','','',''), +(2671,2667,'景东彝族','景东彝族自治县','云南省,普洱市,景东彝族自治县',3,'','','',''), +(2672,2667,'景谷傣族彝族','景谷傣族彝族自治县','云南省,普洱市,景谷傣族彝族自治县',3,'','','',''), +(2673,2667,'镇沅彝族哈尼族拉祜族','镇沅彝族哈尼族拉祜族自治县','云南省,普洱市,镇沅彝族哈尼族拉祜族自治县',3,'','','',''), +(2674,2667,'江城哈尼族彝族','江城哈尼族彝族自治县','云南省,普洱市,江城哈尼族彝族自治县',3,'','','',''), +(2675,2667,'孟连傣族拉祜族佤族','孟连傣族拉祜族佤族自治县','云南省,普洱市,孟连傣族拉祜族佤族自治县',3,'','','',''), +(2676,2667,'澜沧拉祜族','澜沧拉祜族自治县','云南省,普洱市,澜沧拉祜族自治县',3,'','','',''), +(2677,2667,'西盟佤族','西盟佤族自治县','云南省,普洱市,西盟佤族自治县',3,'','','',''), +(2678,2607,'临沧','临沧市','云南省,临沧市',2,'','','',''), +(2679,2678,'临翔','临翔区','云南省,临沧市,临翔区',3,'','','',''), +(2680,2678,'凤庆','凤庆县','云南省,临沧市,凤庆县',3,'','','',''), +(2681,2678,'云','云县','云南省,临沧市,云县',3,'','','',''), +(2682,2678,'永德','永德县','云南省,临沧市,永德县',3,'','','',''), +(2683,2678,'镇康','镇康县','云南省,临沧市,镇康县',3,'','','',''), +(2684,2678,'双江拉祜族佤族布朗族傣族','双江拉祜族佤族布朗族傣族自治县','云南省,临沧市,双江拉祜族佤族布朗族傣族自治县',3,'','','',''), +(2685,2678,'耿马傣族佤族','耿马傣族佤族自治县','云南省,临沧市,耿马傣族佤族自治县',3,'','','',''), +(2686,2678,'沧源佤族','沧源佤族自治县','云南省,临沧市,沧源佤族自治县',3,'','','',''), +(2687,2607,'楚雄彝族','楚雄彝族自治州','云南省,楚雄彝族自治州',2,'','','',''), +(2688,2687,'楚雄','楚雄市','云南省,楚雄彝族自治州,楚雄市',3,'','','',''), +(2689,2687,'双柏','双柏县','云南省,楚雄彝族自治州,双柏县',3,'','','',''), +(2690,2687,'牟定','牟定县','云南省,楚雄彝族自治州,牟定县',3,'','','',''), +(2691,2687,'南华','南华县','云南省,楚雄彝族自治州,南华县',3,'','','',''), +(2692,2687,'姚安','姚安县','云南省,楚雄彝族自治州,姚安县',3,'','','',''), +(2693,2687,'大姚','大姚县','云南省,楚雄彝族自治州,大姚县',3,'','','',''), +(2694,2687,'永仁','永仁县','云南省,楚雄彝族自治州,永仁县',3,'','','',''), +(2695,2687,'元谋','元谋县','云南省,楚雄彝族自治州,元谋县',3,'','','',''), +(2696,2687,'武定','武定县','云南省,楚雄彝族自治州,武定县',3,'','','',''), +(2697,2687,'禄丰','禄丰县','云南省,楚雄彝族自治州,禄丰县',3,'','','',''), +(2698,2607,'红河哈尼族彝族','红河哈尼族彝族自治州','云南省,红河哈尼族彝族自治州',2,'','','',''), +(2699,2698,'个旧','个旧市','云南省,红河哈尼族彝族自治州,个旧市',3,'','','',''), +(2700,2698,'开远','开远市','云南省,红河哈尼族彝族自治州,开远市',3,'','','',''); +INSERT IGNORE INTO `fa_area` (`id`,`pid`,`shortname`,`name`,`mergename`,`level`,`pinyin`,`code`,`zip`,`first`) VALUES +(2701,2698,'蒙自','蒙自市','云南省,红河哈尼族彝族自治州,蒙自市',3,'','','',''), +(2702,2698,'弥勒','弥勒市','云南省,红河哈尼族彝族自治州,弥勒市',3,'','','',''), +(2703,2698,'屏边苗族','屏边苗族自治县','云南省,红河哈尼族彝族自治州,屏边苗族自治县',3,'','','',''), +(2704,2698,'建水','建水县','云南省,红河哈尼族彝族自治州,建水县',3,'','','',''), +(2705,2698,'石屏','石屏县','云南省,红河哈尼族彝族自治州,石屏县',3,'','','',''), +(2706,2698,'泸西','泸西县','云南省,红河哈尼族彝族自治州,泸西县',3,'','','',''), +(2707,2698,'元阳','元阳县','云南省,红河哈尼族彝族自治州,元阳县',3,'','','',''), +(2708,2698,'红河','红河县','云南省,红河哈尼族彝族自治州,红河县',3,'','','',''), +(2709,2698,'金平苗族瑶族傣族','金平苗族瑶族傣族自治县','云南省,红河哈尼族彝族自治州,金平苗族瑶族傣族自治县',3,'','','',''), +(2710,2698,'绿春','绿春县','云南省,红河哈尼族彝族自治州,绿春县',3,'','','',''), +(2711,2698,'河口瑶族','河口瑶族自治县','云南省,红河哈尼族彝族自治州,河口瑶族自治县',3,'','','',''), +(2712,2607,'文山壮族苗族','文山壮族苗族自治州','云南省,文山壮族苗族自治州',2,'','','',''), +(2713,2712,'文山','文山市','云南省,文山壮族苗族自治州,文山市',3,'','','',''), +(2714,2712,'砚山','砚山县','云南省,文山壮族苗族自治州,砚山县',3,'','','',''), +(2715,2712,'西畴','西畴县','云南省,文山壮族苗族自治州,西畴县',3,'','','',''), +(2716,2712,'麻栗坡','麻栗坡县','云南省,文山壮族苗族自治州,麻栗坡县',3,'','','',''), +(2717,2712,'马关','马关县','云南省,文山壮族苗族自治州,马关县',3,'','','',''), +(2718,2712,'丘北','丘北县','云南省,文山壮族苗族自治州,丘北县',3,'','','',''), +(2719,2712,'广南','广南县','云南省,文山壮族苗族自治州,广南县',3,'','','',''), +(2720,2712,'富宁','富宁县','云南省,文山壮族苗族自治州,富宁县',3,'','','',''), +(2721,2607,'西双版纳傣族','西双版纳傣族自治州','云南省,西双版纳傣族自治州',2,'','','',''), +(2722,2721,'景洪','景洪市','云南省,西双版纳傣族自治州,景洪市',3,'','','',''), +(2723,2721,'勐海','勐海县','云南省,西双版纳傣族自治州,勐海县',3,'','','',''), +(2724,2721,'勐腊','勐腊县','云南省,西双版纳傣族自治州,勐腊县',3,'','','',''), +(2725,2607,'大理白族','大理白族自治州','云南省,大理白族自治州',2,'','','',''), +(2726,2725,'大理','大理市','云南省,大理白族自治州,大理市',3,'','','',''), +(2727,2725,'漾濞彝族','漾濞彝族自治县','云南省,大理白族自治州,漾濞彝族自治县',3,'','','',''), +(2728,2725,'祥云','祥云县','云南省,大理白族自治州,祥云县',3,'','','',''), +(2729,2725,'宾川','宾川县','云南省,大理白族自治州,宾川县',3,'','','',''), +(2730,2725,'弥渡','弥渡县','云南省,大理白族自治州,弥渡县',3,'','','',''), +(2731,2725,'南涧彝族','南涧彝族自治县','云南省,大理白族自治州,南涧彝族自治县',3,'','','',''), +(2732,2725,'巍山彝族回族','巍山彝族回族自治县','云南省,大理白族自治州,巍山彝族回族自治县',3,'','','',''), +(2733,2725,'永平','永平县','云南省,大理白族自治州,永平县',3,'','','',''), +(2734,2725,'云龙','云龙县','云南省,大理白族自治州,云龙县',3,'','','',''), +(2735,2725,'洱源','洱源县','云南省,大理白族自治州,洱源县',3,'','','',''), +(2736,2725,'剑川','剑川县','云南省,大理白族自治州,剑川县',3,'','','',''), +(2737,2725,'鹤庆','鹤庆县','云南省,大理白族自治州,鹤庆县',3,'','','',''), +(2738,2607,'德宏傣族景颇族','德宏傣族景颇族自治州','云南省,德宏傣族景颇族自治州',2,'','','',''), +(2739,2738,'瑞丽','瑞丽市','云南省,德宏傣族景颇族自治州,瑞丽市',3,'','','',''), +(2740,2738,'芒','芒市','云南省,德宏傣族景颇族自治州,芒市',3,'','','',''), +(2741,2738,'梁河','梁河县','云南省,德宏傣族景颇族自治州,梁河县',3,'','','',''), +(2742,2738,'盈江','盈江县','云南省,德宏傣族景颇族自治州,盈江县',3,'','','',''), +(2743,2738,'陇川','陇川县','云南省,德宏傣族景颇族自治州,陇川县',3,'','','',''), +(2744,2607,'怒江傈僳族','怒江傈僳族自治州','云南省,怒江傈僳族自治州',2,'','','',''), +(2745,2744,'泸水','泸水县','云南省,怒江傈僳族自治州,泸水县',3,'','','',''), +(2746,2744,'福贡','福贡县','云南省,怒江傈僳族自治州,福贡县',3,'','','',''), +(2747,2744,'贡山独龙族怒族','贡山独龙族怒族自治县','云南省,怒江傈僳族自治州,贡山独龙族怒族自治县',3,'','','',''), +(2748,2744,'兰坪白族普米族','兰坪白族普米族自治县','云南省,怒江傈僳族自治州,兰坪白族普米族自治县',3,'','','',''), +(2749,2607,'迪庆藏族','迪庆藏族自治州','云南省,迪庆藏族自治州',2,'','','',''), +(2750,2749,'香格里拉','香格里拉县','云南省,迪庆藏族自治州,香格里拉县',3,'','','',''), +(2751,2749,'德钦','德钦县','云南省,迪庆藏族自治州,德钦县',3,'','','',''), +(2752,2749,'维西傈僳族','维西傈僳族自治县','云南省,迪庆藏族自治州,维西傈僳族自治县',3,'','','',''), +(2753,0,'西藏','西藏自治区','西藏自治区',1,'','','',''), +(2754,2753,'拉萨','拉萨市','西藏自治区,拉萨市',2,'','','',''), +(2755,2754,'城关','城关区','西藏自治区,拉萨市,城关区',3,'','','',''), +(2756,2754,'林周','林周县','西藏自治区,拉萨市,林周县',3,'','','',''), +(2757,2754,'当雄','当雄县','西藏自治区,拉萨市,当雄县',3,'','','',''), +(2758,2754,'尼木','尼木县','西藏自治区,拉萨市,尼木县',3,'','','',''), +(2759,2754,'曲水','曲水县','西藏自治区,拉萨市,曲水县',3,'','','',''), +(2760,2754,'堆龙德庆','堆龙德庆县','西藏自治区,拉萨市,堆龙德庆县',3,'','','',''), +(2761,2754,'达孜','达孜县','西藏自治区,拉萨市,达孜县',3,'','','',''), +(2762,2754,'墨竹工卡','墨竹工卡县','西藏自治区,拉萨市,墨竹工卡县',3,'','','',''), +(2763,2753,'昌都','昌都地区','西藏自治区,昌都地区',2,'','','',''), +(2764,2763,'昌都','昌都县','西藏自治区,昌都地区,昌都县',3,'','','',''), +(2765,2763,'江达','江达县','西藏自治区,昌都地区,江达县',3,'','','',''), +(2766,2763,'贡觉','贡觉县','西藏自治区,昌都地区,贡觉县',3,'','','',''), +(2767,2763,'类乌齐','类乌齐县','西藏自治区,昌都地区,类乌齐县',3,'','','',''), +(2768,2763,'丁青','丁青县','西藏自治区,昌都地区,丁青县',3,'','','',''), +(2769,2763,'察雅','察雅县','西藏自治区,昌都地区,察雅县',3,'','','',''), +(2770,2763,'八宿','八宿县','西藏自治区,昌都地区,八宿县',3,'','','',''), +(2771,2763,'左贡','左贡县','西藏自治区,昌都地区,左贡县',3,'','','',''), +(2772,2763,'芒康','芒康县','西藏自治区,昌都地区,芒康县',3,'','','',''), +(2773,2763,'洛隆','洛隆县','西藏自治区,昌都地区,洛隆县',3,'','','',''), +(2774,2763,'边坝','边坝县','西藏自治区,昌都地区,边坝县',3,'','','',''), +(2775,2753,'山南','山南地区','西藏自治区,山南地区',2,'','','',''), +(2776,2775,'乃东','乃东县','西藏自治区,山南地区,乃东县',3,'','','',''), +(2777,2775,'扎囊','扎囊县','西藏自治区,山南地区,扎囊县',3,'','','',''), +(2778,2775,'贡嘎','贡嘎县','西藏自治区,山南地区,贡嘎县',3,'','','',''), +(2779,2775,'桑日','桑日县','西藏自治区,山南地区,桑日县',3,'','','',''), +(2780,2775,'琼结','琼结县','西藏自治区,山南地区,琼结县',3,'','','',''), +(2781,2775,'曲松','曲松县','西藏自治区,山南地区,曲松县',3,'','','',''), +(2782,2775,'措美','措美县','西藏自治区,山南地区,措美县',3,'','','',''), +(2783,2775,'洛扎','洛扎县','西藏自治区,山南地区,洛扎县',3,'','','',''), +(2784,2775,'加查','加查县','西藏自治区,山南地区,加查县',3,'','','',''), +(2785,2775,'隆子','隆子县','西藏自治区,山南地区,隆子县',3,'','','',''), +(2786,2775,'错那','错那县','西藏自治区,山南地区,错那县',3,'','','',''), +(2787,2775,'浪卡子','浪卡子县','西藏自治区,山南地区,浪卡子县',3,'','','',''), +(2788,2753,'日喀则','日喀则地区','西藏自治区,日喀则地区',2,'','','',''), +(2789,2788,'日喀则','日喀则市','西藏自治区,日喀则地区,日喀则市',3,'','','',''), +(2790,2788,'南木林','南木林县','西藏自治区,日喀则地区,南木林县',3,'','','',''), +(2791,2788,'江孜','江孜县','西藏自治区,日喀则地区,江孜县',3,'','','',''), +(2792,2788,'定日','定日县','西藏自治区,日喀则地区,定日县',3,'','','',''), +(2793,2788,'萨迦','萨迦县','西藏自治区,日喀则地区,萨迦县',3,'','','',''), +(2794,2788,'拉孜','拉孜县','西藏自治区,日喀则地区,拉孜县',3,'','','',''), +(2795,2788,'昂仁','昂仁县','西藏自治区,日喀则地区,昂仁县',3,'','','',''), +(2796,2788,'谢通门','谢通门县','西藏自治区,日喀则地区,谢通门县',3,'','','',''), +(2797,2788,'白朗','白朗县','西藏自治区,日喀则地区,白朗县',3,'','','',''), +(2798,2788,'仁布','仁布县','西藏自治区,日喀则地区,仁布县',3,'','','',''), +(2799,2788,'康马','康马县','西藏自治区,日喀则地区,康马县',3,'','','',''), +(2800,2788,'定结','定结县','西藏自治区,日喀则地区,定结县',3,'','','',''), +(2801,2788,'仲巴','仲巴县','西藏自治区,日喀则地区,仲巴县',3,'','','',''), +(2802,2788,'亚东','亚东县','西藏自治区,日喀则地区,亚东县',3,'','','',''), +(2803,2788,'吉隆','吉隆县','西藏自治区,日喀则地区,吉隆县',3,'','','',''), +(2804,2788,'聂拉木','聂拉木县','西藏自治区,日喀则地区,聂拉木县',3,'','','',''), +(2805,2788,'萨嘎','萨嘎县','西藏自治区,日喀则地区,萨嘎县',3,'','','',''), +(2806,2788,'岗巴','岗巴县','西藏自治区,日喀则地区,岗巴县',3,'','','',''), +(2807,2753,'那曲','那曲地区','西藏自治区,那曲地区',2,'','','',''), +(2808,2807,'那曲','那曲县','西藏自治区,那曲地区,那曲县',3,'','','',''), +(2809,2807,'嘉黎','嘉黎县','西藏自治区,那曲地区,嘉黎县',3,'','','',''), +(2810,2807,'比如','比如县','西藏自治区,那曲地区,比如县',3,'','','',''), +(2811,2807,'聂荣','聂荣县','西藏自治区,那曲地区,聂荣县',3,'','','',''), +(2812,2807,'安多','安多县','西藏自治区,那曲地区,安多县',3,'','','',''), +(2813,2807,'申扎','申扎县','西藏自治区,那曲地区,申扎县',3,'','','',''), +(2814,2807,'索','索县','西藏自治区,那曲地区,索县',3,'','','',''), +(2815,2807,'班戈','班戈县','西藏自治区,那曲地区,班戈县',3,'','','',''), +(2816,2807,'巴青','巴青县','西藏自治区,那曲地区,巴青县',3,'','','',''), +(2817,2807,'尼玛','尼玛县','西藏自治区,那曲地区,尼玛县',3,'','','',''), +(2818,2807,'双湖','双湖县','西藏自治区,那曲地区,双湖县',3,'','','',''), +(2819,2753,'阿里','阿里地区','西藏自治区,阿里地区',2,'','','',''), +(2820,2819,'普兰','普兰县','西藏自治区,阿里地区,普兰县',3,'','','',''), +(2821,2819,'札达','札达县','西藏自治区,阿里地区,札达县',3,'','','',''), +(2822,2819,'噶尔','噶尔县','西藏自治区,阿里地区,噶尔县',3,'','','',''), +(2823,2819,'日土','日土县','西藏自治区,阿里地区,日土县',3,'','','',''), +(2824,2819,'革吉','革吉县','西藏自治区,阿里地区,革吉县',3,'','','',''), +(2825,2819,'改则','改则县','西藏自治区,阿里地区,改则县',3,'','','',''), +(2826,2819,'措勤','措勤县','西藏自治区,阿里地区,措勤县',3,'','','',''), +(2827,2753,'林芝','林芝地区','西藏自治区,林芝地区',2,'','','',''), +(2828,2827,'林芝','林芝县','西藏自治区,林芝地区,林芝县',3,'','','',''), +(2829,2827,'工布江达','工布江达县','西藏自治区,林芝地区,工布江达县',3,'','','',''), +(2830,2827,'米林','米林县','西藏自治区,林芝地区,米林县',3,'','','',''), +(2831,2827,'墨脱','墨脱县','西藏自治区,林芝地区,墨脱县',3,'','','',''), +(2832,2827,'波密','波密县','西藏自治区,林芝地区,波密县',3,'','','',''), +(2833,2827,'察隅','察隅县','西藏自治区,林芝地区,察隅县',3,'','','',''), +(2834,2827,'朗','朗县','西藏自治区,林芝地区,朗县',3,'','','',''), +(2835,0,'陕西','陕西省','陕西省',1,'','','',''), +(2836,2835,'西安','西安市','陕西省,西安市',2,'','','',''), +(2837,2836,'新城','新城区','陕西省,西安市,新城区',3,'','','',''), +(2838,2836,'碑','碑林区','陕西省,西安市,碑林区',3,'','','',''), +(2839,2836,'莲湖','莲湖区','陕西省,西安市,莲湖区',3,'','','',''), +(2840,2836,'灞桥','灞桥区','陕西省,西安市,灞桥区',3,'','','',''), +(2841,2836,'未央','未央区','陕西省,西安市,未央区',3,'','','',''), +(2842,2836,'雁塔','雁塔区','陕西省,西安市,雁塔区',3,'','','',''), +(2843,2836,'阎良','阎良区','陕西省,西安市,阎良区',3,'','','',''), +(2844,2836,'临潼','临潼区','陕西省,西安市,临潼区',3,'','','',''), +(2845,2836,'长安','长安区','陕西省,西安市,长安区',3,'','','',''), +(2846,2836,'蓝田','蓝田县','陕西省,西安市,蓝田县',3,'','','',''), +(2847,2836,'周至','周至县','陕西省,西安市,周至县',3,'','','',''), +(2848,2836,'户','户县','陕西省,西安市,户县',3,'','','',''), +(2849,2836,'高陵','高陵县','陕西省,西安市,高陵县',3,'','','',''), +(2850,2835,'铜川','铜川市','陕西省,铜川市',2,'','','',''), +(2851,2850,'王益','王益区','陕西省,铜川市,王益区',3,'','','',''), +(2852,2850,'印台','印台区','陕西省,铜川市,印台区',3,'','','',''), +(2853,2850,'耀州','耀州区','陕西省,铜川市,耀州区',3,'','','',''), +(2854,2850,'宜君','宜君县','陕西省,铜川市,宜君县',3,'','','',''), +(2855,2835,'宝鸡','宝鸡市','陕西省,宝鸡市',2,'','','',''), +(2856,2855,'渭滨','渭滨区','陕西省,宝鸡市,渭滨区',3,'','','',''), +(2857,2855,'金台','金台区','陕西省,宝鸡市,金台区',3,'','','',''), +(2858,2855,'陈仓','陈仓区','陕西省,宝鸡市,陈仓区',3,'','','',''), +(2859,2855,'凤翔','凤翔县','陕西省,宝鸡市,凤翔县',3,'','','',''), +(2860,2855,'岐山','岐山县','陕西省,宝鸡市,岐山县',3,'','','',''), +(2861,2855,'扶风','扶风县','陕西省,宝鸡市,扶风县',3,'','','',''), +(2862,2855,'眉','眉县','陕西省,宝鸡市,眉县',3,'','','',''), +(2863,2855,'陇','陇县','陕西省,宝鸡市,陇县',3,'','','',''), +(2864,2855,'千阳','千阳县','陕西省,宝鸡市,千阳县',3,'','','',''), +(2865,2855,'麟游','麟游县','陕西省,宝鸡市,麟游县',3,'','','',''), +(2866,2855,'凤','凤县','陕西省,宝鸡市,凤县',3,'','','',''), +(2867,2855,'太白','太白县','陕西省,宝鸡市,太白县',3,'','','',''), +(2868,2835,'咸阳','咸阳市','陕西省,咸阳市',2,'','','',''), +(2869,2868,'秦都','秦都区','陕西省,咸阳市,秦都区',3,'','','',''), +(2870,2868,'杨陵','杨陵区','陕西省,咸阳市,杨陵区',3,'','','',''), +(2871,2868,'渭城','渭城区','陕西省,咸阳市,渭城区',3,'','','',''), +(2872,2868,'三原','三原县','陕西省,咸阳市,三原县',3,'','','',''), +(2873,2868,'泾阳','泾阳县','陕西省,咸阳市,泾阳县',3,'','','',''), +(2874,2868,'乾','乾县','陕西省,咸阳市,乾县',3,'','','',''), +(2875,2868,'礼泉','礼泉县','陕西省,咸阳市,礼泉县',3,'','','',''), +(2876,2868,'永寿','永寿县','陕西省,咸阳市,永寿县',3,'','','',''), +(2877,2868,'彬','彬县','陕西省,咸阳市,彬县',3,'','','',''), +(2878,2868,'长武','长武县','陕西省,咸阳市,长武县',3,'','','',''), +(2879,2868,'旬邑','旬邑县','陕西省,咸阳市,旬邑县',3,'','','',''), +(2880,2868,'淳化','淳化县','陕西省,咸阳市,淳化县',3,'','','',''), +(2881,2868,'武功','武功县','陕西省,咸阳市,武功县',3,'','','',''), +(2882,2868,'兴平','兴平市','陕西省,咸阳市,兴平市',3,'','','',''), +(2883,2835,'渭南','渭南市','陕西省,渭南市',2,'','','',''), +(2884,2883,'临渭','临渭区','陕西省,渭南市,临渭区',3,'','','',''), +(2885,2883,'华','华县','陕西省,渭南市,华县',3,'','','',''), +(2886,2883,'潼关','潼关县','陕西省,渭南市,潼关县',3,'','','',''), +(2887,2883,'大荔','大荔县','陕西省,渭南市,大荔县',3,'','','',''), +(2888,2883,'合阳','合阳县','陕西省,渭南市,合阳县',3,'','','',''), +(2889,2883,'澄城','澄城县','陕西省,渭南市,澄城县',3,'','','',''), +(2890,2883,'蒲城','蒲城县','陕西省,渭南市,蒲城县',3,'','','',''), +(2891,2883,'白水','白水县','陕西省,渭南市,白水县',3,'','','',''), +(2892,2883,'富平','富平县','陕西省,渭南市,富平县',3,'','','',''), +(2893,2883,'韩城','韩城市','陕西省,渭南市,韩城市',3,'','','',''), +(2894,2883,'华阴','华阴市','陕西省,渭南市,华阴市',3,'','','',''), +(2895,2835,'延安','延安市','陕西省,延安市',2,'','','',''), +(2896,2895,'宝塔','宝塔区','陕西省,延安市,宝塔区',3,'','','',''), +(2897,2895,'延长','延长县','陕西省,延安市,延长县',3,'','','',''), +(2898,2895,'延川','延川县','陕西省,延安市,延川县',3,'','','',''), +(2899,2895,'子长','子长县','陕西省,延安市,子长县',3,'','','',''), +(2900,2895,'安塞','安塞县','陕西省,延安市,安塞县',3,'','','',''), +(2901,2895,'志丹','志丹县','陕西省,延安市,志丹县',3,'','','',''), +(2902,2895,'吴起','吴起县','陕西省,延安市,吴起县',3,'','','',''), +(2903,2895,'甘泉','甘泉县','陕西省,延安市,甘泉县',3,'','','',''), +(2904,2895,'富','富县','陕西省,延安市,富县',3,'','','',''), +(2905,2895,'洛川','洛川县','陕西省,延安市,洛川县',3,'','','',''), +(2906,2895,'宜川','宜川县','陕西省,延安市,宜川县',3,'','','',''), +(2907,2895,'黄龙','黄龙县','陕西省,延安市,黄龙县',3,'','','',''), +(2908,2895,'黄陵','黄陵县','陕西省,延安市,黄陵县',3,'','','',''), +(2909,2835,'汉中','汉中市','陕西省,汉中市',2,'','','',''), +(2910,2909,'汉台','汉台区','陕西省,汉中市,汉台区',3,'','','',''), +(2911,2909,'南郑','南郑县','陕西省,汉中市,南郑县',3,'','','',''), +(2912,2909,'城固','城固县','陕西省,汉中市,城固县',3,'','','',''), +(2913,2909,'洋','洋县','陕西省,汉中市,洋县',3,'','','',''), +(2914,2909,'西乡','西乡县','陕西省,汉中市,西乡县',3,'','','',''), +(2915,2909,'勉','勉县','陕西省,汉中市,勉县',3,'','','',''), +(2916,2909,'宁强','宁强县','陕西省,汉中市,宁强县',3,'','','',''), +(2917,2909,'略阳','略阳县','陕西省,汉中市,略阳县',3,'','','',''), +(2918,2909,'镇巴','镇巴县','陕西省,汉中市,镇巴县',3,'','','',''), +(2919,2909,'留坝','留坝县','陕西省,汉中市,留坝县',3,'','','',''), +(2920,2909,'佛坪','佛坪县','陕西省,汉中市,佛坪县',3,'','','',''), +(2921,2835,'榆林','榆林市','陕西省,榆林市',2,'','','',''), +(2922,2921,'榆阳','榆阳区','陕西省,榆林市,榆阳区',3,'','','',''), +(2923,2921,'神木','神木县','陕西省,榆林市,神木县',3,'','','',''), +(2924,2921,'府谷','府谷县','陕西省,榆林市,府谷县',3,'','','',''), +(2925,2921,'横山','横山县','陕西省,榆林市,横山县',3,'','','',''), +(2926,2921,'靖边','靖边县','陕西省,榆林市,靖边县',3,'','','',''), +(2927,2921,'定边','定边县','陕西省,榆林市,定边县',3,'','','',''), +(2928,2921,'绥德','绥德县','陕西省,榆林市,绥德县',3,'','','',''), +(2929,2921,'米脂','米脂县','陕西省,榆林市,米脂县',3,'','','',''), +(2930,2921,'佳','佳县','陕西省,榆林市,佳县',3,'','','',''), +(2931,2921,'吴堡','吴堡县','陕西省,榆林市,吴堡县',3,'','','',''), +(2932,2921,'清涧','清涧县','陕西省,榆林市,清涧县',3,'','','',''), +(2933,2921,'子洲','子洲县','陕西省,榆林市,子洲县',3,'','','',''), +(2934,2835,'安康','安康市','陕西省,安康市',2,'','','',''), +(2935,2934,'汉滨','汉滨区','陕西省,安康市,汉滨区',3,'','','',''), +(2936,2934,'汉阴','汉阴县','陕西省,安康市,汉阴县',3,'','','',''), +(2937,2934,'石泉','石泉县','陕西省,安康市,石泉县',3,'','','',''), +(2938,2934,'宁陕','宁陕县','陕西省,安康市,宁陕县',3,'','','',''), +(2939,2934,'紫阳','紫阳县','陕西省,安康市,紫阳县',3,'','','',''), +(2940,2934,'岚皋','岚皋县','陕西省,安康市,岚皋县',3,'','','',''), +(2941,2934,'平利','平利县','陕西省,安康市,平利县',3,'','','',''), +(2942,2934,'镇坪','镇坪县','陕西省,安康市,镇坪县',3,'','','',''), +(2943,2934,'旬阳','旬阳县','陕西省,安康市,旬阳县',3,'','','',''), +(2944,2934,'白河','白河县','陕西省,安康市,白河县',3,'','','',''), +(2945,2835,'商洛','商洛市','陕西省,商洛市',2,'','','',''), +(2946,2945,'商州','商州区','陕西省,商洛市,商州区',3,'','','',''), +(2947,2945,'洛南','洛南县','陕西省,商洛市,洛南县',3,'','','',''), +(2948,2945,'丹凤','丹凤县','陕西省,商洛市,丹凤县',3,'','','',''), +(2949,2945,'商南','商南县','陕西省,商洛市,商南县',3,'','','',''), +(2950,2945,'山阳','山阳县','陕西省,商洛市,山阳县',3,'','','',''), +(2951,2945,'镇安','镇安县','陕西省,商洛市,镇安县',3,'','','',''), +(2952,2945,'柞水','柞水县','陕西省,商洛市,柞水县',3,'','','',''), +(2953,0,'甘肃','甘肃省','甘肃省',1,'','','',''), +(2954,2953,'兰州','兰州市','甘肃省,兰州市',2,'','','',''), +(2955,2954,'城关','城关区','甘肃省,兰州市,城关区',3,'','','',''), +(2956,2954,'七里河','七里河区','甘肃省,兰州市,七里河区',3,'','','',''), +(2957,2954,'西固','西固区','甘肃省,兰州市,西固区',3,'','','',''), +(2958,2954,'安宁','安宁区','甘肃省,兰州市,安宁区',3,'','','',''), +(2959,2954,'红古','红古区','甘肃省,兰州市,红古区',3,'','','',''), +(2960,2954,'永登','永登县','甘肃省,兰州市,永登县',3,'','','',''), +(2961,2954,'皋兰','皋兰县','甘肃省,兰州市,皋兰县',3,'','','',''), +(2962,2954,'榆中','榆中县','甘肃省,兰州市,榆中县',3,'','','',''), +(2963,2953,'嘉峪关','嘉峪关市','甘肃省,嘉峪关市',2,'','','',''), +(2964,2963,'嘉峪关','嘉峪关市','甘肃省,嘉峪关市,嘉峪关市',3,'','','',''), +(2965,2953,'金昌','金昌市','甘肃省,金昌市',2,'','','',''), +(2966,2965,'金川','金川区','甘肃省,金昌市,金川区',3,'','','',''), +(2967,2965,'永昌','永昌县','甘肃省,金昌市,永昌县',3,'','','',''), +(2968,2953,'白银','白银市','甘肃省,白银市',2,'','','',''), +(2969,2968,'白银','白银区','甘肃省,白银市,白银区',3,'','','',''), +(2970,2968,'平川','平川区','甘肃省,白银市,平川区',3,'','','',''), +(2971,2968,'靖远','靖远县','甘肃省,白银市,靖远县',3,'','','',''), +(2972,2968,'会宁','会宁县','甘肃省,白银市,会宁县',3,'','','',''), +(2973,2968,'景泰','景泰县','甘肃省,白银市,景泰县',3,'','','',''), +(2974,2953,'天水','天水市','甘肃省,天水市',2,'','','',''), +(2975,2974,'秦州','秦州区','甘肃省,天水市,秦州区',3,'','','',''), +(2976,2974,'麦积','麦积区','甘肃省,天水市,麦积区',3,'','','',''), +(2977,2974,'清水','清水县','甘肃省,天水市,清水县',3,'','','',''), +(2978,2974,'秦安','秦安县','甘肃省,天水市,秦安县',3,'','','',''), +(2979,2974,'甘谷','甘谷县','甘肃省,天水市,甘谷县',3,'','','',''), +(2980,2974,'武山','武山县','甘肃省,天水市,武山县',3,'','','',''), +(2981,2974,'张家川回族','张家川回族自治县','甘肃省,天水市,张家川回族自治县',3,'','','',''), +(2982,2953,'武威','武威市','甘肃省,武威市',2,'','','',''), +(2983,2982,'凉州','凉州区','甘肃省,武威市,凉州区',3,'','','',''), +(2984,2982,'民勤','民勤县','甘肃省,武威市,民勤县',3,'','','',''), +(2985,2982,'古浪','古浪县','甘肃省,武威市,古浪县',3,'','','',''), +(2986,2982,'天祝藏族','天祝藏族自治县','甘肃省,武威市,天祝藏族自治县',3,'','','',''), +(2987,2953,'张掖','张掖市','甘肃省,张掖市',2,'','','',''), +(2988,2987,'甘州','甘州区','甘肃省,张掖市,甘州区',3,'','','',''), +(2989,2987,'肃南裕固族','肃南裕固族自治县','甘肃省,张掖市,肃南裕固族自治县',3,'','','',''), +(2990,2987,'民乐','民乐县','甘肃省,张掖市,民乐县',3,'','','',''), +(2991,2987,'临泽','临泽县','甘肃省,张掖市,临泽县',3,'','','',''), +(2992,2987,'高台','高台县','甘肃省,张掖市,高台县',3,'','','',''), +(2993,2987,'山丹','山丹县','甘肃省,张掖市,山丹县',3,'','','',''), +(2994,2953,'平凉','平凉市','甘肃省,平凉市',2,'','','',''), +(2995,2994,'崆峒','崆峒区','甘肃省,平凉市,崆峒区',3,'','','',''), +(2996,2994,'泾川','泾川县','甘肃省,平凉市,泾川县',3,'','','',''), +(2997,2994,'灵台','灵台县','甘肃省,平凉市,灵台县',3,'','','',''), +(2998,2994,'崇信','崇信县','甘肃省,平凉市,崇信县',3,'','','',''), +(2999,2994,'华亭','华亭县','甘肃省,平凉市,华亭县',3,'','','',''), +(3000,2994,'庄浪','庄浪县','甘肃省,平凉市,庄浪县',3,'','','',''); +INSERT IGNORE INTO `fa_area` (`id`,`pid`,`shortname`,`name`,`mergename`,`level`,`pinyin`,`code`,`zip`,`first`) VALUES +(3001,2994,'静宁','静宁县','甘肃省,平凉市,静宁县',3,'','','',''), +(3002,2953,'酒泉','酒泉市','甘肃省,酒泉市',2,'','','',''), +(3003,3002,'肃州','肃州区','甘肃省,酒泉市,肃州区',3,'','','',''), +(3004,3002,'金塔','金塔县','甘肃省,酒泉市,金塔县',3,'','','',''), +(3005,3002,'瓜州','瓜州县','甘肃省,酒泉市,瓜州县',3,'','','',''), +(3006,3002,'肃北蒙古族','肃北蒙古族自治县','甘肃省,酒泉市,肃北蒙古族自治县',3,'','','',''), +(3007,3002,'阿克塞哈萨克族','阿克塞哈萨克族自治县','甘肃省,酒泉市,阿克塞哈萨克族自治县',3,'','','',''), +(3008,3002,'玉门','玉门市','甘肃省,酒泉市,玉门市',3,'','','',''), +(3009,3002,'敦煌','敦煌市','甘肃省,酒泉市,敦煌市',3,'','','',''), +(3010,2953,'庆阳','庆阳市','甘肃省,庆阳市',2,'','','',''), +(3011,3010,'西峰','西峰区','甘肃省,庆阳市,西峰区',3,'','','',''), +(3012,3010,'庆城','庆城县','甘肃省,庆阳市,庆城县',3,'','','',''), +(3013,3010,'环','环县','甘肃省,庆阳市,环县',3,'','','',''), +(3014,3010,'华池','华池县','甘肃省,庆阳市,华池县',3,'','','',''), +(3015,3010,'合水','合水县','甘肃省,庆阳市,合水县',3,'','','',''), +(3016,3010,'正宁','正宁县','甘肃省,庆阳市,正宁县',3,'','','',''), +(3017,3010,'宁','宁县','甘肃省,庆阳市,宁县',3,'','','',''), +(3018,3010,'镇原','镇原县','甘肃省,庆阳市,镇原县',3,'','','',''), +(3019,2953,'定西','定西市','甘肃省,定西市',2,'','','',''), +(3020,3019,'安定','安定区','甘肃省,定西市,安定区',3,'','','',''), +(3021,3019,'通渭','通渭县','甘肃省,定西市,通渭县',3,'','','',''), +(3022,3019,'陇西','陇西县','甘肃省,定西市,陇西县',3,'','','',''), +(3023,3019,'渭源','渭源县','甘肃省,定西市,渭源县',3,'','','',''), +(3024,3019,'临洮','临洮县','甘肃省,定西市,临洮县',3,'','','',''), +(3025,3019,'漳','漳县','甘肃省,定西市,漳县',3,'','','',''), +(3026,3019,'岷','岷县','甘肃省,定西市,岷县',3,'','','',''), +(3027,2953,'陇南','陇南市','甘肃省,陇南市',2,'','','',''), +(3028,3027,'武都','武都区','甘肃省,陇南市,武都区',3,'','','',''), +(3029,3027,'成','成县','甘肃省,陇南市,成县',3,'','','',''), +(3030,3027,'文','文县','甘肃省,陇南市,文县',3,'','','',''), +(3031,3027,'宕昌','宕昌县','甘肃省,陇南市,宕昌县',3,'','','',''), +(3032,3027,'康','康县','甘肃省,陇南市,康县',3,'','','',''), +(3033,3027,'西和','西和县','甘肃省,陇南市,西和县',3,'','','',''), +(3034,3027,'礼','礼县','甘肃省,陇南市,礼县',3,'','','',''), +(3035,3027,'徽','徽县','甘肃省,陇南市,徽县',3,'','','',''), +(3036,3027,'两当','两当县','甘肃省,陇南市,两当县',3,'','','',''), +(3037,2953,'临夏回族','临夏回族自治州','甘肃省,临夏回族自治州',2,'','','',''), +(3038,3037,'临夏','临夏市','甘肃省,临夏回族自治州,临夏市',3,'','','',''), +(3039,3037,'临夏','临夏县','甘肃省,临夏回族自治州,临夏县',3,'','','',''), +(3040,3037,'康乐','康乐县','甘肃省,临夏回族自治州,康乐县',3,'','','',''), +(3041,3037,'永靖','永靖县','甘肃省,临夏回族自治州,永靖县',3,'','','',''), +(3042,3037,'广河','广河县','甘肃省,临夏回族自治州,广河县',3,'','','',''), +(3043,3037,'和政','和政县','甘肃省,临夏回族自治州,和政县',3,'','','',''), +(3044,3037,'东乡族','东乡族自治县','甘肃省,临夏回族自治州,东乡族自治县',3,'','','',''), +(3045,3037,'积石山保安族东乡族撒拉族','积石山保安族东乡族撒拉族自治县','甘肃省,临夏回族自治州,积石山保安族东乡族撒拉族自治县',3,'','','',''), +(3046,2953,'甘南藏族','甘南藏族自治州','甘肃省,甘南藏族自治州',2,'','','',''), +(3047,3046,'合作','合作市','甘肃省,甘南藏族自治州,合作市',3,'','','',''), +(3048,3046,'临潭','临潭县','甘肃省,甘南藏族自治州,临潭县',3,'','','',''), +(3049,3046,'卓尼','卓尼县','甘肃省,甘南藏族自治州,卓尼县',3,'','','',''), +(3050,3046,'舟曲','舟曲县','甘肃省,甘南藏族自治州,舟曲县',3,'','','',''), +(3051,3046,'迭部','迭部县','甘肃省,甘南藏族自治州,迭部县',3,'','','',''), +(3052,3046,'玛曲','玛曲县','甘肃省,甘南藏族自治州,玛曲县',3,'','','',''), +(3053,3046,'碌曲','碌曲县','甘肃省,甘南藏族自治州,碌曲县',3,'','','',''), +(3054,3046,'夏河','夏河县','甘肃省,甘南藏族自治州,夏河县',3,'','','',''), +(3055,0,'青海','青海省','青海省',1,'','','',''), +(3056,3055,'西宁','西宁市','青海省,西宁市',2,'','','',''), +(3057,3056,'城东','城东区','青海省,西宁市,城东区',3,'','','',''), +(3058,3056,'城中','城中区','青海省,西宁市,城中区',3,'','','',''), +(3059,3056,'城西','城西区','青海省,西宁市,城西区',3,'','','',''), +(3060,3056,'城北','城北区','青海省,西宁市,城北区',3,'','','',''), +(3061,3056,'大通回族土族','大通回族土族自治县','青海省,西宁市,大通回族土族自治县',3,'','','',''), +(3062,3056,'湟中','湟中县','青海省,西宁市,湟中县',3,'','','',''), +(3063,3056,'湟源','湟源县','青海省,西宁市,湟源县',3,'','','',''), +(3064,3055,'海东','海东市','青海省,海东市',2,'','','',''), +(3065,3064,'乐都','乐都区','青海省,海东市,乐都区',3,'','','',''), +(3066,3064,'平安','平安县','青海省,海东市,平安县',3,'','','',''), +(3067,3064,'民和回族土族','民和回族土族自治县','青海省,海东市,民和回族土族自治县',3,'','','',''), +(3068,3064,'互助土族','互助土族自治县','青海省,海东市,互助土族自治县',3,'','','',''), +(3069,3064,'化隆回族','化隆回族自治县','青海省,海东市,化隆回族自治县',3,'','','',''), +(3070,3064,'循化撒拉族','循化撒拉族自治县','青海省,海东市,循化撒拉族自治县',3,'','','',''), +(3071,3055,'海北藏族','海北藏族自治州','青海省,海北藏族自治州',2,'','','',''), +(3072,3071,'门源回族','门源回族自治县','青海省,海北藏族自治州,门源回族自治县',3,'','','',''), +(3073,3071,'祁连','祁连县','青海省,海北藏族自治州,祁连县',3,'','','',''), +(3074,3071,'海晏','海晏县','青海省,海北藏族自治州,海晏县',3,'','','',''), +(3075,3071,'刚察','刚察县','青海省,海北藏族自治州,刚察县',3,'','','',''), +(3076,3055,'黄南藏族','黄南藏族自治州','青海省,黄南藏族自治州',2,'','','',''), +(3077,3076,'同仁','同仁县','青海省,黄南藏族自治州,同仁县',3,'','','',''), +(3078,3076,'尖扎','尖扎县','青海省,黄南藏族自治州,尖扎县',3,'','','',''), +(3079,3076,'泽库','泽库县','青海省,黄南藏族自治州,泽库县',3,'','','',''), +(3080,3076,'河南蒙古族','河南蒙古族自治县','青海省,黄南藏族自治州,河南蒙古族自治县',3,'','','',''), +(3081,3055,'海南藏族','海南藏族自治州','青海省,海南藏族自治州',2,'','','',''), +(3082,3081,'共和','共和县','青海省,海南藏族自治州,共和县',3,'','','',''), +(3083,3081,'同德','同德县','青海省,海南藏族自治州,同德县',3,'','','',''), +(3084,3081,'贵德','贵德县','青海省,海南藏族自治州,贵德县',3,'','','',''), +(3085,3081,'兴海','兴海县','青海省,海南藏族自治州,兴海县',3,'','','',''), +(3086,3081,'贵南','贵南县','青海省,海南藏族自治州,贵南县',3,'','','',''), +(3087,3055,'果洛藏族','果洛藏族自治州','青海省,果洛藏族自治州',2,'','','',''), +(3088,3087,'玛沁','玛沁县','青海省,果洛藏族自治州,玛沁县',3,'','','',''), +(3089,3087,'班玛','班玛县','青海省,果洛藏族自治州,班玛县',3,'','','',''), +(3090,3087,'甘德','甘德县','青海省,果洛藏族自治州,甘德县',3,'','','',''), +(3091,3087,'达日','达日县','青海省,果洛藏族自治州,达日县',3,'','','',''), +(3092,3087,'久治','久治县','青海省,果洛藏族自治州,久治县',3,'','','',''), +(3093,3087,'玛多','玛多县','青海省,果洛藏族自治州,玛多县',3,'','','',''), +(3094,3055,'玉树藏族','玉树藏族自治州','青海省,玉树藏族自治州',2,'','','',''), +(3095,3094,'玉树','玉树市','青海省,玉树藏族自治州,玉树市',3,'','','',''), +(3096,3094,'杂多','杂多县','青海省,玉树藏族自治州,杂多县',3,'','','',''), +(3097,3094,'称多','称多县','青海省,玉树藏族自治州,称多县',3,'','','',''), +(3098,3094,'治多','治多县','青海省,玉树藏族自治州,治多县',3,'','','',''), +(3099,3094,'囊谦','囊谦县','青海省,玉树藏族自治州,囊谦县',3,'','','',''), +(3100,3094,'曲麻莱','曲麻莱县','青海省,玉树藏族自治州,曲麻莱县',3,'','','',''), +(3101,3055,'海西蒙古族藏族','海西蒙古族藏族自治州','青海省,海西蒙古族藏族自治州',2,'','','',''), +(3102,3101,'格尔木','格尔木市','青海省,海西蒙古族藏族自治州,格尔木市',3,'','','',''), +(3103,3101,'德令哈','德令哈市','青海省,海西蒙古族藏族自治州,德令哈市',3,'','','',''), +(3104,3101,'乌兰','乌兰县','青海省,海西蒙古族藏族自治州,乌兰县',3,'','','',''), +(3105,3101,'都兰','都兰县','青海省,海西蒙古族藏族自治州,都兰县',3,'','','',''), +(3106,3101,'天峻','天峻县','青海省,海西蒙古族藏族自治州,天峻县',3,'','','',''), +(3107,0,'宁夏','宁夏回族自治区','宁夏回族自治区',1,'','','',''), +(3108,3107,'银川','银川市','宁夏回族自治区,银川市',2,'','','',''), +(3109,3108,'兴庆','兴庆区','宁夏回族自治区,银川市,兴庆区',3,'','','',''), +(3110,3108,'西夏','西夏区','宁夏回族自治区,银川市,西夏区',3,'','','',''), +(3111,3108,'金凤','金凤区','宁夏回族自治区,银川市,金凤区',3,'','','',''), +(3112,3108,'永宁','永宁县','宁夏回族自治区,银川市,永宁县',3,'','','',''), +(3113,3108,'贺兰','贺兰县','宁夏回族自治区,银川市,贺兰县',3,'','','',''), +(3114,3108,'灵武','灵武市','宁夏回族自治区,银川市,灵武市',3,'','','',''), +(3115,3107,'石嘴山','石嘴山市','宁夏回族自治区,石嘴山市',2,'','','',''), +(3116,3115,'大武口','大武口区','宁夏回族自治区,石嘴山市,大武口区',3,'','','',''), +(3117,3115,'惠农','惠农区','宁夏回族自治区,石嘴山市,惠农区',3,'','','',''), +(3118,3115,'平罗','平罗县','宁夏回族自治区,石嘴山市,平罗县',3,'','','',''), +(3119,3107,'吴忠','吴忠市','宁夏回族自治区,吴忠市',2,'','','',''), +(3120,3119,'利通','利通区','宁夏回族自治区,吴忠市,利通区',3,'','','',''), +(3121,3119,'红寺堡','红寺堡区','宁夏回族自治区,吴忠市,红寺堡区',3,'','','',''), +(3122,3119,'盐池','盐池县','宁夏回族自治区,吴忠市,盐池县',3,'','','',''), +(3123,3119,'同心','同心县','宁夏回族自治区,吴忠市,同心县',3,'','','',''), +(3124,3119,'青铜峡','青铜峡市','宁夏回族自治区,吴忠市,青铜峡市',3,'','','',''), +(3125,3107,'固原','固原市','宁夏回族自治区,固原市',2,'','','',''), +(3126,3125,'原州','原州区','宁夏回族自治区,固原市,原州区',3,'','','',''), +(3127,3125,'西吉','西吉县','宁夏回族自治区,固原市,西吉县',3,'','','',''), +(3128,3125,'隆德','隆德县','宁夏回族自治区,固原市,隆德县',3,'','','',''), +(3129,3125,'泾源','泾源县','宁夏回族自治区,固原市,泾源县',3,'','','',''), +(3130,3125,'彭阳','彭阳县','宁夏回族自治区,固原市,彭阳县',3,'','','',''), +(3131,3107,'中卫','中卫市','宁夏回族自治区,中卫市',2,'','','',''), +(3132,3131,'沙坡头','沙坡头区','宁夏回族自治区,中卫市,沙坡头区',3,'','','',''), +(3133,3131,'中宁','中宁县','宁夏回族自治区,中卫市,中宁县',3,'','','',''), +(3134,3131,'海原','海原县','宁夏回族自治区,中卫市,海原县',3,'','','',''), +(3135,0,'新疆','新疆维吾尔自治区','新疆维吾尔自治区',1,'','','',''), +(3136,3135,'乌鲁木齐','乌鲁木齐市','新疆维吾尔自治区,乌鲁木齐市',2,'','','',''), +(3137,3136,'天山','天山区','新疆维吾尔自治区,乌鲁木齐市,天山区',3,'','','',''), +(3138,3136,'沙依巴克','沙依巴克区','新疆维吾尔自治区,乌鲁木齐市,沙依巴克区',3,'','','',''), +(3139,3136,'新市','新市区','新疆维吾尔自治区,乌鲁木齐市,新市区',3,'','','',''), +(3140,3136,'水磨沟','水磨沟区','新疆维吾尔自治区,乌鲁木齐市,水磨沟区',3,'','','',''), +(3141,3136,'头屯河','头屯河区','新疆维吾尔自治区,乌鲁木齐市,头屯河区',3,'','','',''), +(3142,3136,'达坂城','达坂城区','新疆维吾尔自治区,乌鲁木齐市,达坂城区',3,'','','',''), +(3143,3136,'米东','米东区','新疆维吾尔自治区,乌鲁木齐市,米东区',3,'','','',''), +(3144,3136,'乌鲁木齐','乌鲁木齐县','新疆维吾尔自治区,乌鲁木齐市,乌鲁木齐县',3,'','','',''), +(3145,3135,'克拉玛依','克拉玛依市','新疆维吾尔自治区,克拉玛依市',2,'','','',''), +(3146,3145,'独山子','独山子区','新疆维吾尔自治区,克拉玛依市,独山子区',3,'','','',''), +(3147,3145,'克拉玛依','克拉玛依区','新疆维吾尔自治区,克拉玛依市,克拉玛依区',3,'','','',''), +(3148,3145,'白碱滩','白碱滩区','新疆维吾尔自治区,克拉玛依市,白碱滩区',3,'','','',''), +(3149,3145,'乌尔禾','乌尔禾区','新疆维吾尔自治区,克拉玛依市,乌尔禾区',3,'','','',''), +(3150,3135,'吐鲁番','吐鲁番地区','新疆维吾尔自治区,吐鲁番地区',2,'','','',''), +(3151,3150,'吐鲁番','吐鲁番市','新疆维吾尔自治区,吐鲁番地区,吐鲁番市',3,'','','',''), +(3152,3150,'鄯善','鄯善县','新疆维吾尔自治区,吐鲁番地区,鄯善县',3,'','','',''), +(3153,3150,'托克逊','托克逊县','新疆维吾尔自治区,吐鲁番地区,托克逊县',3,'','','',''), +(3154,3135,'哈密','哈密地区','新疆维吾尔自治区,哈密地区',2,'','','',''), +(3155,3154,'哈密','哈密市','新疆维吾尔自治区,哈密地区,哈密市',3,'','','',''), +(3156,3154,'巴里坤哈萨克','巴里坤哈萨克自治县','新疆维吾尔自治区,哈密地区,巴里坤哈萨克自治县',3,'','','',''), +(3157,3154,'伊吾','伊吾县','新疆维吾尔自治区,哈密地区,伊吾县',3,'','','',''), +(3158,3135,'昌吉回族','昌吉回族自治州','新疆维吾尔自治区,昌吉回族自治州',2,'','','',''), +(3159,3158,'昌吉','昌吉市','新疆维吾尔自治区,昌吉回族自治州,昌吉市',3,'','','',''), +(3160,3158,'阜康','阜康市','新疆维吾尔自治区,昌吉回族自治州,阜康市',3,'','','',''), +(3161,3158,'呼图壁','呼图壁县','新疆维吾尔自治区,昌吉回族自治州,呼图壁县',3,'','','',''), +(3162,3158,'玛纳斯','玛纳斯县','新疆维吾尔自治区,昌吉回族自治州,玛纳斯县',3,'','','',''), +(3163,3158,'奇台','奇台县','新疆维吾尔自治区,昌吉回族自治州,奇台县',3,'','','',''), +(3164,3158,'吉木萨尔','吉木萨尔县','新疆维吾尔自治区,昌吉回族自治州,吉木萨尔县',3,'','','',''), +(3165,3158,'木垒哈萨克','木垒哈萨克自治县','新疆维吾尔自治区,昌吉回族自治州,木垒哈萨克自治县',3,'','','',''), +(3166,3135,'博尔塔拉蒙古','博尔塔拉蒙古自治州','新疆维吾尔自治区,博尔塔拉蒙古自治州',2,'','','',''), +(3167,3166,'博乐','博乐市','新疆维吾尔自治区,博尔塔拉蒙古自治州,博乐市',3,'','','',''), +(3168,3166,'阿拉山口','阿拉山口市','新疆维吾尔自治区,博尔塔拉蒙古自治州,阿拉山口市',3,'','','',''), +(3169,3166,'精河','精河县','新疆维吾尔自治区,博尔塔拉蒙古自治州,精河县',3,'','','',''), +(3170,3166,'温泉','温泉县','新疆维吾尔自治区,博尔塔拉蒙古自治州,温泉县',3,'','','',''), +(3171,3135,'巴音郭楞蒙古','巴音郭楞蒙古自治州','新疆维吾尔自治区,巴音郭楞蒙古自治州',2,'','','',''), +(3172,3171,'库尔勒','库尔勒市','新疆维吾尔自治区,巴音郭楞蒙古自治州,库尔勒市',3,'','','',''), +(3173,3171,'轮台','轮台县','新疆维吾尔自治区,巴音郭楞蒙古自治州,轮台县',3,'','','',''), +(3174,3171,'尉犁','尉犁县','新疆维吾尔自治区,巴音郭楞蒙古自治州,尉犁县',3,'','','',''), +(3175,3171,'若羌','若羌县','新疆维吾尔自治区,巴音郭楞蒙古自治州,若羌县',3,'','','',''), +(3176,3171,'且末','且末县','新疆维吾尔自治区,巴音郭楞蒙古自治州,且末县',3,'','','',''), +(3177,3171,'焉耆回族','焉耆回族自治县','新疆维吾尔自治区,巴音郭楞蒙古自治州,焉耆回族自治县',3,'','','',''), +(3178,3171,'和静','和静县','新疆维吾尔自治区,巴音郭楞蒙古自治州,和静县',3,'','','',''), +(3179,3171,'和硕','和硕县','新疆维吾尔自治区,巴音郭楞蒙古自治州,和硕县',3,'','','',''), +(3180,3171,'博湖','博湖县','新疆维吾尔自治区,巴音郭楞蒙古自治州,博湖县',3,'','','',''), +(3181,3135,'阿克苏','阿克苏地区','新疆维吾尔自治区,阿克苏地区',2,'','','',''), +(3182,3181,'阿克苏','阿克苏市','新疆维吾尔自治区,阿克苏地区,阿克苏市',3,'','','',''), +(3183,3181,'温宿','温宿县','新疆维吾尔自治区,阿克苏地区,温宿县',3,'','','',''), +(3184,3181,'库车','库车县','新疆维吾尔自治区,阿克苏地区,库车县',3,'','','',''), +(3185,3181,'沙雅','沙雅县','新疆维吾尔自治区,阿克苏地区,沙雅县',3,'','','',''), +(3186,3181,'新和','新和县','新疆维吾尔自治区,阿克苏地区,新和县',3,'','','',''), +(3187,3181,'拜城','拜城县','新疆维吾尔自治区,阿克苏地区,拜城县',3,'','','',''), +(3188,3181,'乌什','乌什县','新疆维吾尔自治区,阿克苏地区,乌什县',3,'','','',''), +(3189,3181,'阿瓦提','阿瓦提县','新疆维吾尔自治区,阿克苏地区,阿瓦提县',3,'','','',''), +(3190,3181,'柯坪','柯坪县','新疆维吾尔自治区,阿克苏地区,柯坪县',3,'','','',''), +(3191,3135,'克孜勒苏柯尔克孜','克孜勒苏柯尔克孜自治州','新疆维吾尔自治区,克孜勒苏柯尔克孜自治州',2,'','','',''), +(3192,3191,'阿图什','阿图什市','新疆维吾尔自治区,克孜勒苏柯尔克孜自治州,阿图什市',3,'','','',''), +(3193,3191,'阿克陶','阿克陶县','新疆维吾尔自治区,克孜勒苏柯尔克孜自治州,阿克陶县',3,'','','',''), +(3194,3191,'阿合奇','阿合奇县','新疆维吾尔自治区,克孜勒苏柯尔克孜自治州,阿合奇县',3,'','','',''), +(3195,3191,'乌恰','乌恰县','新疆维吾尔自治区,克孜勒苏柯尔克孜自治州,乌恰县',3,'','','',''), +(3196,3135,'喀什','喀什地区','新疆维吾尔自治区,喀什地区',2,'','','',''), +(3197,3196,'喀什','喀什市','新疆维吾尔自治区,喀什地区,喀什市',3,'','','',''), +(3198,3196,'疏附','疏附县','新疆维吾尔自治区,喀什地区,疏附县',3,'','','',''), +(3199,3196,'疏勒','疏勒县','新疆维吾尔自治区,喀什地区,疏勒县',3,'','','',''), +(3200,3196,'英吉沙','英吉沙县','新疆维吾尔自治区,喀什地区,英吉沙县',3,'','','',''), +(3201,3196,'泽普','泽普县','新疆维吾尔自治区,喀什地区,泽普县',3,'','','',''), +(3202,3196,'莎车','莎车县','新疆维吾尔自治区,喀什地区,莎车县',3,'','','',''), +(3203,3196,'叶城','叶城县','新疆维吾尔自治区,喀什地区,叶城县',3,'','','',''), +(3204,3196,'麦盖提','麦盖提县','新疆维吾尔自治区,喀什地区,麦盖提县',3,'','','',''), +(3205,3196,'岳普湖','岳普湖县','新疆维吾尔自治区,喀什地区,岳普湖县',3,'','','',''), +(3206,3196,'伽师','伽师县','新疆维吾尔自治区,喀什地区,伽师县',3,'','','',''), +(3207,3196,'巴楚','巴楚县','新疆维吾尔自治区,喀什地区,巴楚县',3,'','','',''), +(3208,3196,'塔什库尔干塔吉克','塔什库尔干塔吉克自治县','新疆维吾尔自治区,喀什地区,塔什库尔干塔吉克自治县',3,'','','',''), +(3209,3135,'和田','和田地区','新疆维吾尔自治区,和田地区',2,'','','',''), +(3210,3209,'和田','和田市','新疆维吾尔自治区,和田地区,和田市',3,'','','',''), +(3211,3209,'和田','和田县','新疆维吾尔自治区,和田地区,和田县',3,'','','',''), +(3212,3209,'墨玉','墨玉县','新疆维吾尔自治区,和田地区,墨玉县',3,'','','',''), +(3213,3209,'皮山','皮山县','新疆维吾尔自治区,和田地区,皮山县',3,'','','',''), +(3214,3209,'洛浦','洛浦县','新疆维吾尔自治区,和田地区,洛浦县',3,'','','',''), +(3215,3209,'策勒','策勒县','新疆维吾尔自治区,和田地区,策勒县',3,'','','',''), +(3216,3209,'于田','于田县','新疆维吾尔自治区,和田地区,于田县',3,'','','',''), +(3217,3209,'民丰','民丰县','新疆维吾尔自治区,和田地区,民丰县',3,'','','',''), +(3218,3135,'伊犁哈萨克','伊犁哈萨克自治州','新疆维吾尔自治区,伊犁哈萨克自治州',2,'','','',''), +(3219,3218,'伊宁','伊宁市','新疆维吾尔自治区,伊犁哈萨克自治州,伊宁市',3,'','','',''), +(3220,3218,'奎屯','奎屯市','新疆维吾尔自治区,伊犁哈萨克自治州,奎屯市',3,'','','',''), +(3221,3218,'伊宁','伊宁县','新疆维吾尔自治区,伊犁哈萨克自治州,伊宁县',3,'','','',''), +(3222,3218,'察布查尔锡伯','察布查尔锡伯自治县','新疆维吾尔自治区,伊犁哈萨克自治州,察布查尔锡伯自治县',3,'','','',''), +(3223,3218,'霍城','霍城县','新疆维吾尔自治区,伊犁哈萨克自治州,霍城县',3,'','','',''), +(3224,3218,'巩留','巩留县','新疆维吾尔自治区,伊犁哈萨克自治州,巩留县',3,'','','',''), +(3225,3218,'新源','新源县','新疆维吾尔自治区,伊犁哈萨克自治州,新源县',3,'','','',''), +(3226,3218,'昭苏','昭苏县','新疆维吾尔自治区,伊犁哈萨克自治州,昭苏县',3,'','','',''), +(3227,3218,'特克斯','特克斯县','新疆维吾尔自治区,伊犁哈萨克自治州,特克斯县',3,'','','',''), +(3228,3218,'尼勒克','尼勒克县','新疆维吾尔自治区,伊犁哈萨克自治州,尼勒克县',3,'','','',''), +(3229,3135,'塔城','塔城地区','新疆维吾尔自治区,塔城地区',2,'','','',''), +(3230,3229,'塔城','塔城市','新疆维吾尔自治区,塔城地区,塔城市',3,'','','',''), +(3231,3229,'乌苏','乌苏市','新疆维吾尔自治区,塔城地区,乌苏市',3,'','','',''), +(3232,3229,'额敏','额敏县','新疆维吾尔自治区,塔城地区,额敏县',3,'','','',''), +(3233,3229,'沙湾','沙湾县','新疆维吾尔自治区,塔城地区,沙湾县',3,'','','',''), +(3234,3229,'托里','托里县','新疆维吾尔自治区,塔城地区,托里县',3,'','','',''), +(3235,3229,'裕民','裕民县','新疆维吾尔自治区,塔城地区,裕民县',3,'','','',''), +(3236,3229,'和布克赛尔蒙古','和布克赛尔蒙古自治县','新疆维吾尔自治区,塔城地区,和布克赛尔蒙古自治县',3,'','','',''), +(3237,3135,'阿勒泰','阿勒泰地区','新疆维吾尔自治区,阿勒泰地区',2,'','','',''), +(3238,3237,'阿勒泰','阿勒泰市','新疆维吾尔自治区,阿勒泰地区,阿勒泰市',3,'','','',''), +(3239,3237,'布尔津','布尔津县','新疆维吾尔自治区,阿勒泰地区,布尔津县',3,'','','',''), +(3240,3237,'富蕴','富蕴县','新疆维吾尔自治区,阿勒泰地区,富蕴县',3,'','','',''), +(3241,3237,'福海','福海县','新疆维吾尔自治区,阿勒泰地区,福海县',3,'','','',''), +(3242,3237,'哈巴河','哈巴河县','新疆维吾尔自治区,阿勒泰地区,哈巴河县',3,'','','',''), +(3243,3237,'青河','青河县','新疆维吾尔自治区,阿勒泰地区,青河县',3,'','','',''), +(3244,3237,'吉木乃','吉木乃县','新疆维吾尔自治区,阿勒泰地区,吉木乃县',3,'','','',''), +(3245,3135,'石河子','石河子市','新疆维吾尔自治区,石河子市',2,'','','',''), +(3246,3245,'石河子','石河子市','新疆维吾尔自治区,石河子市,石河子市',3,'','','',''), +(3247,3135,'阿拉尔','阿拉尔市','新疆维吾尔自治区,阿拉尔市',2,'','','',''), +(3248,3247,'阿拉尔','阿拉尔市','新疆维吾尔自治区,阿拉尔市,阿拉尔市',3,'','','',''), +(3249,3135,'图木舒克','图木舒克市','新疆维吾尔自治区,图木舒克市',2,'','','',''), +(3250,3249,'图木舒克','图木舒克市','新疆维吾尔自治区,图木舒克市,图木舒克市',3,'','','',''), +(3251,3135,'五家渠','五家渠市','新疆维吾尔自治区,五家渠市',2,'','','',''), +(3252,3251,'五家渠','五家渠市','新疆维吾尔自治区,五家渠市,五家渠市',3,'','','',''), +(3253,0,'香港','香港特别行政区','香港特别行政区',1,'','','',''), +(3254,3253,'香港','香港特别行政区','香港特别行政区,香港特别行政区',2,'','','',''), +(3255,3254,'中西','中西区','香港特别行政区,香港特别行政区,中西区',3,'','','',''), +(3256,3254,'湾仔','湾仔区','香港特别行政区,香港特别行政区,湾仔区',3,'','','',''), +(3257,3254,'东','东区','香港特别行政区,香港特别行政区,东区',3,'','','',''), +(3258,3254,'南','南区','香港特别行政区,香港特别行政区,南区',3,'','','',''), +(3259,3254,'油尖旺','油尖旺区','香港特别行政区,香港特别行政区,油尖旺区',3,'','','',''), +(3260,3254,'深水埗','深水埗区','香港特别行政区,香港特别行政区,深水埗区',3,'','','',''), +(3261,3254,'九龙城','九龙城区','香港特别行政区,香港特别行政区,九龙城区',3,'','','',''), +(3262,3254,'黄大仙','黄大仙区','香港特别行政区,香港特别行政区,黄大仙区',3,'','','',''), +(3263,3254,'观塘','观塘区','香港特别行政区,香港特别行政区,观塘区',3,'','','',''), +(3264,3254,'葵青','葵青区','香港特别行政区,香港特别行政区,葵青区',3,'','','',''), +(3265,3254,'荃湾','荃湾区','香港特别行政区,香港特别行政区,荃湾区',3,'','','',''), +(3266,3254,'屯门','屯门区','香港特别行政区,香港特别行政区,屯门区',3,'','','',''), +(3267,3254,'元朗','元朗区','香港特别行政区,香港特别行政区,元朗区',3,'','','',''), +(3268,3254,'北','北区','香港特别行政区,香港特别行政区,北区',3,'','','',''), +(3269,3254,'大埔','大埔区','香港特别行政区,香港特别行政区,大埔区',3,'','','',''), +(3270,3254,'沙田','沙田区','香港特别行政区,香港特别行政区,沙田区',3,'','','',''), +(3271,3254,'西贡','西贡区','香港特别行政区,香港特别行政区,西贡区',3,'','','',''), +(3272,3254,'离岛','离岛区','香港特别行政区,香港特别行政区,离岛区',3,'','','',''), +(3273,0,'澳门','澳门特别行政区','澳门特别行政区',1,'','','',''), +(3274,3273,'澳门','澳门特别行政区','澳门特别行政区,澳门特别行政区',2,'','','',''), +(3275,3274,'花地玛堂','花地玛堂区','澳门特别行政区,澳门特别行政区,花地玛堂区',3,'','','',''), +(3276,3274,'圣安多尼堂','圣安多尼堂区','澳门特别行政区,澳门特别行政区,圣安多尼堂区',3,'','','',''), +(3277,3274,'大堂','大堂区','澳门特别行政区,澳门特别行政区,大堂区',3,'','','',''), +(3278,3274,'望德堂','望德堂区','澳门特别行政区,澳门特别行政区,望德堂区',3,'','','',''), +(3279,3274,'风顺堂','风顺堂区','澳门特别行政区,澳门特别行政区,风顺堂区',3,'','','',''), +(3280,3274,'嘉模堂','嘉模堂区','澳门特别行政区,澳门特别行政区,嘉模堂区',3,'','','',''), +(3281,3274,'圣方济各堂','圣方济各堂区','澳门特别行政区,澳门特别行政区,圣方济各堂区',3,'','','',''), +(3282,3274,'路氹城','路氹城','澳门特别行政区,澳门特别行政区,路氹城',3,'','','',''), +(3283,0,'台湾','台湾','台湾',1,'','','',''), +(3284,3283,'台湾','台湾','台湾,台湾',2,'','','',''), +(3285,3284,'台北','台北市','台湾,台湾,台北市',3,'','','',''), +(3286,3284,'新北','新北市','台湾,台湾,新北市',3,'','','',''), +(3287,3284,'桃园','桃园市','台湾,台湾,桃园市',3,'','','',''), +(3288,3284,'台中','台中市','台湾,台湾,台中市',3,'','','',''), +(3289,3284,'台南','台南市','台湾,台湾,台南市',3,'','','',''), +(3290,3284,'高雄','高雄市','台湾,台湾,高雄市',3,'','','',''), +(3291,3284,'基隆','基隆市','台湾,台湾,基隆市',3,'','','',''), +(3292,3284,'新竹','新竹市','台湾,台湾,新竹市',3,'','','',''), +(3293,3284,'嘉义','嘉义市','台湾,台湾,嘉义市',3,'','','',''), +(3294,3284,'新竹','新竹县','台湾,台湾,新竹县',3,'','','',''), +(3295,3284,'苗栗','苗栗县','台湾,台湾,苗栗县',3,'','','',''), +(3296,3284,'彰化','彰化县','台湾,台湾,彰化县',3,'','','',''), +(3297,3284,'南投','南投县','台湾,台湾,南投县',3,'','','',''), +(3298,3284,'云林','云林县','台湾,台湾,云林县',3,'','','',''), +(3299,3284,'嘉义','嘉义县','台湾,台湾,嘉义县',3,'','','',''), +(3300,3284,'屏东','屏东县','台湾,台湾,屏东县',3,'','','',''); +INSERT IGNORE INTO `fa_area` (`id`,`pid`,`shortname`,`name`,`mergename`,`level`,`pinyin`,`code`,`zip`,`first`) VALUES +(3301,3284,'宜兰','宜兰县','台湾,台湾,宜兰县',3,'','','',''), +(3302,3284,'花莲','花莲县','台湾,台湾,花莲县',3,'','','',''), +(3303,3284,'台东','台东县','台湾,台湾,台东县',3,'','','',''), +(3304,3284,'澎湖','澎湖县','台湾,台湾,澎湖县',3,'','','',''), +(3305,3284,'金门','金门县','台湾,台湾,金门县',3,'','','',''), +(3306,3284,'连江','连江县','台湾,台湾,连江县',3,'','','',''); diff --git a/scripts/deploy_db.py b/scripts/deploy_db.py new file mode 100644 index 0000000..29b99e4 --- /dev/null +++ b/scripts/deploy_db.py @@ -0,0 +1,46 @@ +import paramiko +import os + +ssh = paramiko.SSHClient() +ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) +ssh.connect('8.137.181.63', port=22, username='root', password='liyanxia@6688', timeout=15) + +def run(cmd): + stdin, stdout, stderr = ssh.exec_command(cmd) + out = stdout.read().decode('utf-8', errors='ignore') + err = stderr.read().decode('utf-8', errors='ignore') + return out, err + +print('Step 1: Backup current remote yjyf database...') +out, err = run('mysqldump -uyjyf -pyjyf --default-character-set=utf8mb4 yjyf > /tmp/yjyf_remote_backup_before_deploy.sql') +out2, _ = run('ls -lh /tmp/yjyf_remote_backup_before_deploy.sql') +print('Backup done:', out2.strip()) + +print('Step 2: Upload local_yjyf_deploy.sql to remote /tmp...') +sftp = ssh.open_sftp() +local_file = r'd:\Projects\phpstudy_pro\WWW\yjyf\db\local_yjyf_deploy.sql' +remote_file = '/tmp/local_yjyf_deploy.sql' +sftp.put(local_file, remote_file) +sftp.close() +out3, _ = run('ls -lh /tmp/local_yjyf_deploy.sql') +print('Uploaded:', out3.strip()) + +print('Step 3: Import into remote yjyf database...') +out4, err4 = run('mysql -uyjyf -pyjyf --default-character-set=utf8mb4 yjyf < /tmp/local_yjyf_deploy.sql') +if err4 and 'Warning' not in err4: + print('Import error:', err4) +else: + print('Import command finished.') + +print('Step 4: Verify tables count and key tables...') +out5, _ = run("mysql -uyjyf -pyjyf yjyf -e 'SHOW TABLES;'") +tables = [l.strip() for l in out5.strip().splitlines() if l.strip()] +print('Remote table count now:', max(0, len(tables) - 1)) + +out6, _ = run("mysql -uyjyf -pyjyf yjyf -e 'SELECT \"fa_user\", COUNT(*) FROM fa_user UNION ALL SELECT \"fa_master\", COUNT(*) FROM fa_master UNION ALL SELECT \"fa_order\", COUNT(*) FROM fa_order UNION ALL SELECT \"fa_service\", COUNT(*) FROM fa_service UNION ALL SELECT \"fa_agreement\", COUNT(*) FROM fa_agreement;'") +print('Key tables counts:\n' + out6) + +# 删除远程临时 sql 文件保障安全 +run('rm -f /tmp/local_yjyf_deploy.sql') + +ssh.close() diff --git a/scripts/deploy_server.py b/scripts/deploy_server.py new file mode 100644 index 0000000..7c56d55 --- /dev/null +++ b/scripts/deploy_server.py @@ -0,0 +1,122 @@ +import os +import tarfile +import paramiko +import time + +server_dir = r'D:\Projects\phpstudy_pro\WWW\yjyf\server' +tar_path = r'D:\Projects\phpstudy_pro\WWW\yjyf\server_deploy.tar.gz' + +print('Step 1: Packaging local server files...') +exclude_prefixes = [ + os.path.join(server_dir, 'runtime'), + os.path.join(server_dir, '.git'), + os.path.join(server_dir, '.idea'), + os.path.join(server_dir, '.vscode') +] +exclude_files = [ + '.env', + 'composer.phar' +] + +with tarfile.open(tar_path, 'w:gz') as tar: + for root, dirs, files in os.walk(server_dir): + # 排除指定目录 + if any(root.startswith(p) for p in exclude_prefixes): + continue + for file in files: + if file in exclude_files or file.endswith('.log') or file.endswith('.tmp'): + continue + full_path = os.path.join(root, file) + rel_path = os.path.relpath(full_path, server_dir) + tar.add(full_path, arcname=rel_path) + +print(f'Package created: {tar_path}, size: {os.path.getsize(tar_path)/1024/1024:.2f} MB') + +print('Step 2: Connecting to remote server...') +ssh = paramiko.SSHClient() +ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) +ssh.connect('8.137.181.63', port=22, username='root', password='liyanxia@6688', timeout=20) + +def run(cmd): + stdin, stdout, stderr = ssh.exec_command(cmd) + out = stdout.read().decode('utf-8', errors='ignore') + err = stderr.read().decode('utf-8', errors='ignore') + return out, err + +print('Step 3: Backup current remote yjyf site...') +run('mkdir -p /www/backup') +out_b, err_b = run('tar -zcf /www/backup/yjyf_site_backup_before_deploy.tar.gz -C /www/wwwroot yjyf.gzyakj.com.cn') +out_b_check, _ = run('ls -lh /www/backup/yjyf_site_backup_before_deploy.tar.gz') +print('Remote site backup:', out_b_check.strip()) + +print('Step 4: Upload package to remote /tmp...') +sftp = ssh.open_sftp() +sftp.put(tar_path, '/tmp/server_deploy.tar.gz') +sftp.close() +out_up, _ = run('ls -lh /tmp/server_deploy.tar.gz') +print('Uploaded:', out_up.strip()) + +print('Step 5: Extract package to /www/wwwroot/yjyf.gzyakj.com.cn...') +out_ex, err_ex = run('tar -zxvf /tmp/server_deploy.tar.gz -C /www/wwwroot/yjyf.gzyakj.com.cn') +print('Extracted files count:', len(out_ex.splitlines())) + +print('Step 6: Ensure .env for remote environment...') +remote_env_content = """[app] +debug = false +trace = false + +[database] +hostname = 127.0.0.1 +database = yjyf +username = yjyf +password = yjyf +hostport = 3306 +prefix = fa_ + +[wechat] +appid = wx40ca4b02d4d1cb58 +secret = f661146ce627e2c57770b7d80164dd65 +mch_id = 1119105317 +key = ffyqcyyszhdhi398672363efnwvikxlv +key_v3 = ffyqcyyszhdhi398672363efnwvikxlv +public_key_id = PUB_KEY_ID_0111191053172026102200381630000600 +notify_domain = https://yjyf.gzyakj.com.cn +transfer_scene_id = 1005 +transfer_single_limit = 200.00 +transfer_daily_user_limit = 200.00 + +[tianditu] +key = 55aee666be14cd0100874d6faf3c963c + +[sms] +; mock=1 时使用固定验证码(联调用);接入真实短信后置 0 +mock = 1 +mock_code = 123456 +""" + +sftp = ssh.open_sftp() +with sftp.file('/www/wwwroot/yjyf.gzyakj.com.cn/.env', 'w') as f: + f.write(remote_env_content) +sftp.close() +print('.env written.') + +print('Step 7: Ensure admin.php and s.php exist...') +run('cp -f /www/wwwroot/yjyf.gzyakj.com.cn/public/s.php /www/wwwroot/yjyf.gzyakj.com.cn/public/admin.php') + +print('Step 8: Set permissions and clean runtime cache...') +run('mkdir -p /www/wwwroot/yjyf.gzyakj.com.cn/runtime/cache /www/wwwroot/yjyf.gzyakj.com.cn/runtime/temp /www/wwwroot/yjyf.gzyakj.com.cn/runtime/log') +run('rm -rf /www/wwwroot/yjyf.gzyakj.com.cn/runtime/cache/* /www/wwwroot/yjyf.gzyakj.com.cn/runtime/temp/*') +run('chown -R www:www /www/wwwroot/yjyf.gzyakj.com.cn') +run('chmod -R 755 /www/wwwroot/yjyf.gzyakj.com.cn') + +print('Step 9: Reload PHP-FPM...') +out_p, _ = run('/etc/init.d/php-fpm-85 reload') +print('PHP-FPM reload result:', out_p.strip()) + +print('Step 10: Clean up temporary package...') +run('rm -f /tmp/server_deploy.tar.gz') +if os.path.exists(tar_path): + os.remove(tar_path) + +ssh.close() +print('Deploy server completed!') diff --git a/scripts/test_phase1_implementation.php b/scripts/test_phase1_implementation.php index 20d6ddc..d8f3538 100644 --- a/scripts/test_phase1_implementation.php +++ b/scripts/test_phase1_implementation.php @@ -5,7 +5,6 @@ require __DIR__ . '/../server/thinkphp/base.php'; \think\App::initCommon(); use app\common\library\TiandituService; -use app\common\library\SmsService; use think\Db; echo "========================================================\n"; @@ -25,13 +24,9 @@ echo " [PASS] 正常打卡测试 (偏离约 {$checkNormal['distance']} 米): " $checkAbnormal = TiandituService::checkArriveDistance(30.6574, 104.0665, 30.6900, 104.1000, 1000.0); echo " [PASS] 异地打卡作弊拦截测试 (偏离 {$checkAbnormal['distance']} 米): " . (!$checkAbnormal['is_pass'] ? '成功识别预警' : '未拦截') . "\n"; -// 2. 短信网关服务驱动自检 -echo "\n>> 2. 验证业务短信分发驱动:\n"; -$sms1 = SmsService::sendVerifyCode('13800138000', '654321', 'TESTORDER001'); -echo " [PASS] 完工6位核销验证码短信驱动测试: " . ($sms1 ? '成功' : '失败') . "\n"; - -$sms2 = SmsService::sendFirstContactWarning('13700137000', 'TESTORDER001', 30); -echo " [PASS] 师傅2小时首联时效预警短信驱动测试: " . ($sms2 ? '成功' : '失败') . "\n"; +// 2. 短信验证码通道已按当前阶段规范移除 +echo "\n>> 2. 短信验证码通道状态:\n"; +echo " [INFO] 短信验证码通道已移除,核销码改为订单详情页与站内消息直达。\n"; // 3. 验证 ThinkPHP 定时守护任务注册 echo "\n>> 3. 验证 ThinkPHP 命令行守护任务配置:\n"; diff --git a/server/addons/address/config.php b/server/addons/address/config.php index 962b36e..d151c88 100644 --- a/server/addons/address/config.php +++ b/server/addons/address/config.php @@ -107,10 +107,10 @@ return [ 'title' => '天地图KEY', 'type' => 'string', 'content' => [], - 'value' => '369ea1edc677b9e3746093f8d9dc4465', + 'value' => 'a13cdd845c9e92ebc17f6c76ecab49f6', 'rule' => '', 'msg' => '', - 'tip' => '天地图开发密钥TK', + 'tip' => '天地图Web浏览器端开发密钥TK', 'ok' => '', 'extend' => '', ], diff --git a/server/addons/epay/controller/Api.php b/server/addons/epay/controller/Api.php index 1390598..09d147e 100644 --- a/server/addons/epay/controller/Api.php +++ b/server/addons/epay/controller/Api.php @@ -181,9 +181,24 @@ class Api extends Controller public function notifyx() { $paytype = $this->request->param('paytype', $this->request->param('type', 'wechat')); + if ($paytype === 'wechat' && Service::isVersionV3()) { + try { + $rawBody = file_get_contents('php://input') ?: (string)$this->request->getInput(); + $headers = $this->request->header(); + $wxSvc = new \app\common\library\WxPayV3Service(); + $parsed = $wxSvc->verifyAndDecryptNotify((string)$rawBody, is_array($headers) ? $headers : []); + if (strtoupper($parsed['event_type']) === 'TRANSACTION.SUCCESS' && !empty($parsed['data']['out_trade_no'])) { + \app\common\library\PaymentService::confirmOrderPay((string)$parsed['data']['out_trade_no'], $parsed['data']); + } + return json(['code' => 'SUCCESS', 'message' => '成功'], 200); + } catch (Exception $e) { + \think\Log::record("微信V3回调处理失败:" . $e->getMessage(), "error"); + return json(['code' => 'FAIL', 'message' => '处理失败:' . $e->getMessage()], 500); + } + } $pay = Service::checkNotify($paytype); if (!$pay) { - return json(['code' => 'FAIL', 'message' => '失败'], 500, ['Content-Type' => 'application/json']); + return json(['code' => 'FAIL', 'message' => '回调验签失败'], 500, ['Content-Type' => 'application/json']); } // 获取回调数据,V3和V2的回调接收不同 @@ -202,10 +217,12 @@ class Api extends Controller $out_trade_no = $data['out_trade_no']; \think\Log::record("回调成功,订单号:{$out_trade_no},金额:{$payamount}"); - - //你可以在此编写订单逻辑 - } catch (Exception $e) { + if ($paytype === 'wechat' && $out_trade_no !== '') { + \app\common\library\PaymentService::confirmOrderPay((string)$out_trade_no, is_array($data) ? $data : null); + } + } catch (\Throwable $e) { \think\Log::record("回调逻辑处理错误:" . $e->getMessage(), "error"); + return json(['code' => 'FAIL', 'message' => '回调处理失败:' . $e->getMessage()], 500); } //下面这句必须要执行,且在此之前不能有任何输出 diff --git a/server/application/admin/command/OrderAutoverify.php b/server/application/admin/command/OrderAutoverify.php index c73fca7..f140764 100644 --- a/server/application/admin/command/OrderAutoverify.php +++ b/server/application/admin/command/OrderAutoverify.php @@ -2,6 +2,7 @@ namespace app\admin\command; +use app\common\library\CommissionService; use app\common\library\OrderStatus; use think\console\Command; use think\console\Input; @@ -47,9 +48,10 @@ class OrderAutoverify extends Command $masterId = (int)$lockedOrder['master_id']; $payAmount = (float)$lockedOrder['pay_amount']; - $rate = 10.00; - $commission = round($payAmount * $rate / 100, 2); - $settleAmount = round($payAmount - $commission, 2); + $settleCalc = CommissionService::calcByOrder($lockedOrder); + $rate = $settleCalc['commission_rate']; + $commission = $settleCalc['commission']; + $settleAmount = $settleCalc['settle_amount']; // 1. 更新订单为已完成 Db::name('order')->where('id', $orderId)->update([ diff --git a/server/application/admin/controller/Agreement.php b/server/application/admin/controller/Agreement.php new file mode 100644 index 0000000..799c6ee --- /dev/null +++ b/server/application/admin/controller/Agreement.php @@ -0,0 +1,73 @@ +model = new \app\admin\model\Agreement; + } + + /** + * 协议与隐私编辑工作台 + */ + public function index() + { + $agreements = AgreementService::getAllForAdmin(); + $this->view->assign([ + 'agreements' => $agreements, + 'agreementsJson' => json_encode($agreements, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP), + 'canSave' => $this->auth->check('agreement/save'), + ]); + return $this->view->fetch(); + } + + /** + * 保存协议内容与元数据 + */ + public function save() + { + if (false === $this->request->isPost()) { + $this->error(__('Invalid parameters')); + } + $this->token(); + + $code = trim((string)$this->request->post('code', '')); + // 读取未剥离标签的原始数组,由 AgreementService::sanitizeHtml 执行严格 XSS 白名单净化 + $row = $this->request->post('row/a', [], null); + if ($code === '' || !is_array($row) || empty($row)) { + $this->error('提交的协议数据不能为空', null, ['token' => $this->request->token()]); + } + + try { + $saved = AgreementService::saveByCode($code, $row); + } catch (\InvalidArgumentException $e) { + $this->error($e->getMessage(), null, ['token' => $this->request->token()]); + } catch (\Throwable $e) { + $this->error('保存协议失败:' . $e->getMessage(), null, ['token' => $this->request->token()]); + } + + $this->success('协议内容已保存并同步生效', null, [ + 'token' => $this->request->token(), + 'agreement' => $saved, + ]); + } +} diff --git a/server/application/admin/controller/FinanceConfig.php b/server/application/admin/controller/FinanceConfig.php new file mode 100644 index 0000000..c3986ff --- /dev/null +++ b/server/application/admin/controller/FinanceConfig.php @@ -0,0 +1,363 @@ +model = new \app\admin\model\CommissionRule; + + $catRows = Db::name('service_cat') + ->field('id,name,status') + ->order('weigh desc, id asc') + ->select(); + $catMap = [0 => '不限服务分类']; + foreach ($catRows as $c) { + $catMap[(int)$c['id']] = (string)$c['name'] . ($c['status'] === 'hidden' ? '(已隐藏)' : ''); + } + + $this->view->assign([ + 'orderModeList' => $this->model->getOrderModeList(), + 'customerTypeList' => $this->model->getCustomerTypeList(), + 'urgentModeList' => $this->model->getUrgentModeList(), + 'calcModeList' => $this->model->getCalcModeList(), + 'statusList' => $this->model->getStatusList(), + 'catMap' => $catMap, + ]); + } + + /** + * 配置中心首页(GET 渲染独立双页签配置界面;Ajax 返回按订单维度抽佣规则列表) + */ + public function index() + { + $this->request->filter(['strip_tags', 'trim']); + if (false === $this->request->isAjax()) { + $allCfg = FinanceConfigService::getAll(true); + $withdrawCfg = FinanceConfigService::getWithdrawConfig(); + $commDefaults = FinanceConfigService::getCommissionDefaults(); + + $this->view->assign([ + 'cfg' => $allCfg, + 'withdrawCfg' => $withdrawCfg, + 'commDefaults' => $commDefaults, + ]); + return $this->view->fetch(); + } + + [$where, $sort, $order, $offset, $limit] = $this->buildparams(); + $orderExpr = $sort === 'weigh' ? "weigh {$order}, id desc" : "{$sort} {$order}"; + $list = $this->model + ->where($where) + ->order($orderExpr) + ->paginate($limit); + + $rows = $list->items(); + if (!empty($rows)) { + $cats = Db::name('service_cat')->column('name', 'id'); + foreach ($rows as &$r) { + $cid = (int)$r['cat_id']; + $r['cat_name'] = $cid > 0 ? ($cats[$cid] ?? ('分类#' . $cid)) : ''; + } + unset($r); + } + + return json([ + 'total' => $list->total(), + 'rows' => $rows, + ]); + } + + /** + * 保存订单默认兜底抽佣设置 / 提现与微信转账规则配置 + */ + public function save() + { + if (false === $this->request->isPost()) { + $this->error(__('Invalid parameters')); + } + $this->token(); + + $section = (string)$this->request->post('section', ''); + $row = $this->request->post('row/a', []); + if (!is_array($row) || empty($row)) { + $this->error('提交的配置数据不能为空'); + } + + // 处理多选框全部取消勾选时前端未上送对应键的情况 + if ($section === 'withdraw') { + if (!array_key_exists('withdraw_channels', $row)) { + $this->error('请至少勾选一种开放提现渠道', null, ['token' => $this->request->token()]); + } + if (!array_key_exists('withdraw_allow_weekdays', $row)) { + $this->error('请至少选择一天允许提现的周期', null, ['token' => $this->request->token()]); + } + } + + try { + FinanceConfigService::saveConfigs($row); + CommissionService::clearCache(); + } catch (\InvalidArgumentException $e) { + $this->error($e->getMessage(), null, ['token' => $this->request->token()]); + } catch (\Throwable $e) { + $this->error('保存配置失败:' . $e->getMessage(), null, ['token' => $this->request->token()]); + } + + $msg = $section === 'commission_default' + ? '抽佣默认设置已保存(仅影响新订单,已下单工单不受影响)' + : '提现规则已保存生效'; + + $this->success($msg, null, ['token' => $this->request->token()]); + } + + /** + * 新增按订单维度抽佣规则 + */ + public function rule_add() + { + if (false === $this->request->isPost()) { + $this->view->assign('row', [ + 'name' => '', + 'order_mode' => 'all', + 'customer_type' => 'all', + 'cat_id' => 0, + 'urgent_mode' => 'all', + 'min_amount' => '0.00', + 'max_amount' => '0.00', + 'calc_mode' => 'percent', + 'commission_rate' => '10.00', + 'fixed_fee' => '0.00', + 'min_commission' => '0.00', + 'max_commission' => '0.00', + 'weigh' => 100, + 'status' => 'normal', + 'remark' => '', + ]); + return $this->view->fetch(); + } + + $this->token(); + $raw = $this->request->post('row/a', []); + try { + $clean = $this->validateAndCleanRuleInput($raw); + $this->model->allowField(true)->save($clean); + CommissionService::clearCache(); + } catch (\InvalidArgumentException $e) { + $this->error($e->getMessage()); + } catch (PDOException|\Exception $e) { + $this->error('新增规则失败:' . $e->getMessage()); + } + + $this->success('抽佣规则已创建(仅对后续新订单生效)'); + } + + /** + * 编辑按订单维度抽佣规则 + */ + public function rule_edit($ids = null) + { + $row = $this->model->get($ids); + if (!$row) { + $this->error(__('No Results were found')); + } + if (false === $this->request->isPost()) { + $this->view->assign('row', $row); + return $this->view->fetch(); + } + + $this->token(); + $raw = $this->request->post('row/a', []); + try { + $clean = $this->validateAndCleanRuleInput($raw); + $row->allowField(true)->save($clean); + CommissionService::clearCache(); + } catch (\InvalidArgumentException $e) { + $this->error($e->getMessage()); + } catch (PDOException|\Exception $e) { + $this->error('更新规则失败:' . $e->getMessage()); + } + + $this->success('抽佣规则已更新(历史已创订单不受影响)'); + } + + /** + * 删除抽佣规则 + */ + public function rule_del($ids = null) + { + if (false === $this->request->isPost()) { + $this->error(__('Invalid parameters')); + } + $ids = $ids ?: $this->request->post('ids'); + $idArr = array_values(array_filter(array_map('intval', explode(',', (string)$ids)))); + if (empty($idArr)) { + $this->error(__('Parameter %s can not be empty', 'ids')); + } + + $count = 0; + Db::startTrans(); + try { + $list = $this->model->where('id', 'in', $idArr)->select(); + foreach ($list as $item) { + $count += $item->delete(); + } + Db::commit(); + CommissionService::clearCache(); + } catch (PDOException|\Exception $e) { + Db::rollback(); + $this->error($e->getMessage()); + } + + if ($count) { + $this->success(); + } + $this->error(__('No rows were deleted')); + } + + /** + * 批量启用/停用抽佣规则 + */ + public function rule_multi($ids = null) + { + if (false === $this->request->isPost()) { + $this->error(__('Invalid parameters')); + } + $ids = $ids ?: $this->request->post('ids'); + $params = (string)$this->request->post('params', ''); + parse_str($params, $values); + if (empty($ids) || empty($values)) { + $this->error(__('Parameter %s can not be empty', 'ids')); + } + + $allowed = []; + if (isset($values['status']) && in_array($values['status'], ['normal', 'hidden'], true)) { + $allowed['status'] = $values['status']; + } + if (isset($values['weigh']) && is_numeric($values['weigh'])) { + $allowed['weigh'] = (int)$values['weigh']; + } + if (empty($allowed)) { + $this->error('仅允许批量修改启用状态或优先级权重'); + } + + $idArr = array_values(array_filter(array_map('intval', explode(',', (string)$ids)))); + $allowed['updatetime'] = time(); + Db::name('commission_rule')->where('id', 'in', $idArr)->update($allowed); + CommissionService::clearCache(); + $this->success(); + } + + /** + * 抽佣规则入参强白名单与业务区间合法性校验 + * + * @param array $raw + * @return array + * @throws \InvalidArgumentException + */ + protected function validateAndCleanRuleInput(array $raw): array + { + $name = trim((string)($raw['name'] ?? '')); + if ($name === '' || mb_strlen($name) > 80) { + throw new \InvalidArgumentException('请填写规则名称(1 ~ 80 个字符)'); + } + + $orderMode = (string)($raw['order_mode'] ?? 'all'); + if (!array_key_exists($orderMode, $this->model->getOrderModeList())) { + throw new \InvalidArgumentException('请选择有效的适用订单计价模式'); + } + + $customerType = (string)($raw['customer_type'] ?? 'all'); + if (!array_key_exists($customerType, $this->model->getCustomerTypeList())) { + throw new \InvalidArgumentException('请选择有效的适用发单客群'); + } + + $catId = max(0, (int)($raw['cat_id'] ?? 0)); + if ($catId > 0 && !Db::name('service_cat')->where('id', $catId)->find()) { + throw new \InvalidArgumentException('所选服务类目不存在'); + } + + $urgentMode = (string)($raw['urgent_mode'] ?? 'all'); + if (!array_key_exists($urgentMode, $this->model->getUrgentModeList())) { + throw new \InvalidArgumentException('请选择有效的加急属性条件'); + } + + $minAmount = round((float)($raw['min_amount'] ?? 0), 2); + $maxAmount = round((float)($raw['max_amount'] ?? 0), 2); + if ($minAmount < 0 || $maxAmount < 0) { + throw new \InvalidArgumentException('订单金额区间不能为负数'); + } + if ($maxAmount > 0 && $minAmount > $maxAmount) { + throw new \InvalidArgumentException('订单起始金额不能大于订单上限金额(0 表示不限上限)'); + } + + $calcMode = (string)($raw['calc_mode'] ?? 'percent'); + if (!array_key_exists($calcMode, $this->model->getCalcModeList())) { + throw new \InvalidArgumentException('请选择有效的计佣方式'); + } + + $rate = 0.00; + if (in_array($calcMode, ['percent', 'mixed'], true)) { + $rate = CommissionService::validateRateInput($raw['commission_rate'] ?? ''); + } + + $fixedFee = 0.00; + if (in_array($calcMode, ['fixed', 'mixed'], true)) { + if (!isset($raw['fixed_fee']) || !is_numeric($raw['fixed_fee']) || (float)$raw['fixed_fee'] < 0) { + throw new \InvalidArgumentException('请填写有效的每单固定抽佣金额(≥ 0 元)'); + } + $fixedFee = round((float)$raw['fixed_fee'], 2); + } + + $minComm = round(max(0.00, (float)($raw['min_commission'] ?? 0)), 2); + $maxComm = round(max(0.00, (float)($raw['max_commission'] ?? 0)), 2); + if ($minComm > 0 && $maxComm > 0 && $minComm > $maxComm) { + throw new \InvalidArgumentException('单笔保底抽佣金额不能大于单笔封顶抽佣金额'); + } + + $weigh = isset($raw['weigh']) && is_numeric($raw['weigh']) ? (int)$raw['weigh'] : 100; + $status = in_array($raw['status'] ?? 'normal', ['normal', 'hidden'], true) ? $raw['status'] : 'normal'; + $remark = mb_substr(trim((string)($raw['remark'] ?? '')), 0, 255); + + return [ + 'name' => $name, + 'order_mode' => $orderMode, + 'customer_type' => $customerType, + 'cat_id' => $catId, + 'urgent_mode' => $urgentMode, + 'min_amount' => number_format($minAmount, 2, '.', ''), + 'max_amount' => number_format($maxAmount, 2, '.', ''), + 'calc_mode' => $calcMode, + 'commission_rate' => number_format($rate, 2, '.', ''), + 'fixed_fee' => number_format($fixedFee, 2, '.', ''), + 'min_commission' => number_format($minComm, 2, '.', ''), + 'max_commission' => number_format($maxComm, 2, '.', ''), + 'weigh' => $weigh, + 'status' => $status, + 'remark' => $remark, + ]; + } +} diff --git a/server/application/admin/controller/Home.php b/server/application/admin/controller/Home.php index d50a8af..9a85bf9 100644 --- a/server/application/admin/controller/Home.php +++ b/server/application/admin/controller/Home.php @@ -173,9 +173,9 @@ class Home extends Backend 'waitingInvoice' => $waitingInvoice, 'pendingDispatch' => $pendingDispatch, - 'masterAvgScore' => $masterAvg['avg_score'] ?: '5.0', - 'masterAvgAccept' => $masterAvg['avg_accept'] ?: '100', - 'masterAvgOntime' => $masterAvg['avg_ontime'] ?: '100', + 'masterAvgScore' => ($masterAvg && $masterAvg['avg_score'] !== null) ? (string)$masterAvg['avg_score'] : '0.0', + 'masterAvgAccept' => ($masterAvg && $masterAvg['avg_accept'] !== null) ? (string)$masterAvg['avg_accept'] : '0', + 'masterAvgOntime' => ($masterAvg && $masterAvg['avg_ontime'] !== null) ? (string)$masterAvg['avg_ontime'] : '0', 'totalService' => $totalService, 'totalCategory' => $totalCategory, diff --git a/server/application/admin/controller/Master.php b/server/application/admin/controller/Master.php index 19e3240..988d89a 100644 --- a/server/application/admin/controller/Master.php +++ b/server/application/admin/controller/Master.php @@ -418,6 +418,13 @@ class Master extends Backend ->field('s.*, c.name as cat_name') ->select(); + // 5.5 技能证书记录 + $certs = Db::name('master_cert') + ->field('id,cert_code,cert_name,is_custom,cert_image,expire_date,is_long_term,status,audit_msg,updatetime') + ->where('master_id', $row['id']) + ->order('id desc') + ->select(); + // 6. 钱包资金变动流水 $walletLogs = Db::name('wallet_log') ->where('user_type', 'master') @@ -426,6 +433,14 @@ class Master extends Backend ->limit(6) ->select(); + // 7. 证件防伪水印安全转换(方案1:腾讯云数据万象动态水印) + if (!empty($row['idcard_front'])) { + $row['idcard_front_watermark'] = \app\common\library\TencentVerifyService::getWatermarkUrl($row['idcard_front']); + } + if (!empty($row['idcard_back'])) { + $row['idcard_back_watermark'] = \app\common\library\TencentVerifyService::getWatermarkUrl($row['idcard_back']); + } + $this->view->assign([ 'row' => $row, 'recentOrders' => $recentOrders, @@ -433,6 +448,7 @@ class Master extends Backend 'recentSettles' => $recentSettles, 'recentReviews' => $recentReviews, 'skills' => $skills, + 'certs' => $certs, 'walletLogs' => $walletLogs, ]); diff --git a/server/application/admin/controller/MasterCert.php b/server/application/admin/controller/MasterCert.php new file mode 100644 index 0000000..0e72743 --- /dev/null +++ b/server/application/admin/controller/MasterCert.php @@ -0,0 +1,184 @@ +model = new \app\admin\model\MasterCert; + $this->view->assign('statusList', $this->model->getStatusList()); + } + + /** + * 技能证书列表 + */ + public function index() + { + $this->request->filter(['strip_tags', 'trim']); + if (false === $this->request->isAjax()) { + return $this->view->fetch(); + } + if ($this->request->request('keyField')) { + return $this->selectpage(); + } + [$where, $sort, $order, $offset, $limit] = $this->buildparams(); + $list = $this->model + ->with(['master']) + ->where($where) + ->order($sort, $order) + ->paginate($limit); + + return json(['total' => $list->total(), 'rows' => $list->items()]); + } + + /** + * 查看证书详情 + */ + public function detail($ids = null) + { + $row = $this->model->with(['master'])->where('master_cert.id', (int)$ids)->find(); + if (!$row) { + $this->error(__('No Results were found')); + } + $this->view->assign('row', $row); + return $this->view->fetch(); + } + + /** + * 审核技能证书 + */ + public function audit($ids = null) + { + return $this->handleAudit((int)$ids); + } + + /** + * 兼容 FastAdmin 默认编辑路由,复用审核逻辑 + */ + public function edit($ids = null) + { + return $this->handleAudit((int)$ids); + } + + /** + * 统一证书审核处理(保护证件原图只读,仅更新审核状态与意见并通知师傅) + */ + private function handleAudit($id) + { + $row = $this->model->with(['master'])->where('master_cert.id', (int)$id)->find(); + if (!$row) { + $this->error(__('No Results were found')); + } + + if (false === $this->request->isPost()) { + $this->view->assign('row', $row); + return $this->view->fetch('audit'); + } + + $params = $this->request->post('row/a', []); + if (empty($params)) { + $this->error(__('Parameter %s can not be empty', '')); + } + + $status = trim((string)($params['status'] ?? $row['status'])); + $auditMsg = trim((string)($params['audit_msg'] ?? '')); + + $statusList = $this->model->getStatusList(); + if (!isset($statusList[$status])) { + $this->error('审核状态不合法'); + } + if ($status === 'reject' && $auditMsg === '') { + $this->error('驳回技能证书时,必须填写驳回原因说明'); + } + + $oldStatus = (string)$row['status']; + $now = time(); + $adminId = (int)$this->auth->id; + + Db::startTrans(); + try { + Db::name('master_cert')->where('id', (int)$row['id'])->update([ + 'status' => $status, + 'audit_msg' => mb_substr($auditMsg, 0, 255), + 'audit_admin_id' => $adminId, + 'audittime' => in_array($status, ['pass', 'reject'], true) ? $now : null, + 'updatetime' => $now, + ]); + + $masterId = (int)$row['master_id']; + if ($masterId > 0) { + $images = Db::name('master_cert') + ->where('master_id', $masterId) + ->where('status', 'in', ['pass', 'pending']) + ->order('id desc') + ->column('cert_image'); + $cleanImages = array_values(array_unique(array_filter(array_map('strval', $images ?: [])))); + $json = !empty($cleanImages) ? json_encode($cleanImages, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) : ''; + Db::name('master')->where('id', $masterId)->update([ + 'cert_images' => mb_substr($json, 0, 1000), + 'updatetime' => $now, + ]); + } + + if ($status !== $oldStatus && in_array($status, ['pass', 'reject'], true) && $masterId > 0) { + $certName = (string)$row['cert_name']; + $msgTitle = $status === 'pass' + ? "技能证书审核通过({$certName})" + : "技能证书审核未通过({$certName})"; + $msgContent = $status === 'pass' + ? "您上传的「{$certName}」已通过平台审核,可正常承接对应资质要求的工单。" + : ("您上传的「{$certName}」未通过审核,原因:" . ($auditMsg !== '' ? $auditMsg : '照片不清晰或证件不符合要求,请重新上传。')); + + Db::name('message')->insert([ + 'receive_type' => 'master', + 'receive_id' => $masterId, + 'type' => 'audit', + 'title' => $msgTitle, + 'content' => $msgContent, + 'order_id' => 0, + 'is_read' => 0, + 'createtime' => $now, + ]); + + $userId = (int)Db::name('master')->where('id', $masterId)->value('user_id'); + if ($userId > 0) { + Db::name('message')->insert([ + 'receive_type' => 'user', + 'receive_id' => $userId, + 'type' => 'audit', + 'title' => $msgTitle, + 'content' => $msgContent, + 'order_id' => 0, + 'is_read' => 0, + 'createtime' => $now, + ]); + } + } + + Db::commit(); + } catch (Exception $e) { + Db::rollback(); + $this->error($e->getMessage()); + } + + $this->success('审核操作成功'); + } +} diff --git a/server/application/admin/controller/Order.php b/server/application/admin/controller/Order.php index ef5e054..5954c71 100644 --- a/server/application/admin/controller/Order.php +++ b/server/application/admin/controller/Order.php @@ -127,6 +127,8 @@ class Order extends Backend $params['discount_amount'], $params['total_amount'], $params['pay_amount'], + $params['commission_rate'], + $params['commission_snapshot'], $params['pay_status'], $params['pay_type'], $params['pay_time'], @@ -173,6 +175,23 @@ class Order extends Backend } } + // 后台取消工单前置防资损与原路退款(必须在开启数据库事务之前调用远程退款接口) + if ($oldStatus !== 9 && $newStatus === 9) { + if ((int)$row['settle_status'] === 1) { + $this->error('该工单已完成向师傅结算分账,不允许直接取消退款'); + } + if ((int)$row['pay_status'] === 1) { + $cancelReasonPre = trim((string)($params['cancel_reason'] ?? $row['cancel_reason'] ?? '平台后台取消工单')); + try { + $refundedAmount = \app\common\library\PaymentService::refundOrder((int)$row['id'], $cancelReasonPre ?: '平台后台取消工单'); + } catch (\Throwable $re) { + $this->error('微信原路退款失败:' . $re->getMessage()); + } + $params['pay_status'] = 2; + $params['refund_amount'] = $refundedAmount > 0 ? $refundedAmount : (float)$row['pay_amount']; + } + } + $result = false; Db::startTrans(); try { @@ -523,8 +542,9 @@ class Order extends Backend $review['tags_arr'] = $tagsArr; } - // 6. 财务结算记录 + // 6. 财务结算记录与订单锁定快照预计结算核算 $settle = Db::name('settle')->where('order_id', $row['id'])->find(); + $settlePreview = \app\common\library\CommissionService::calcByOrder($row->toArray()); // 7. 企业商家协同发包来源 $merchantOrder = Db::name('merchant_order') @@ -552,6 +572,24 @@ class Order extends Backend ->order('a.id desc') ->select(); + // 10. 微信支付与原路退款交易流水(含失败错误码及中文诊断原因) + $sceneMap = [ + 'order' => '主订单支付', + 'quote' => '选定师傅报价', + 'diff' => '增项补差价', + 'batch' => '批量下单', + ]; + $payOrders = Db::name('pay_order') + ->where('order_id', (int)$row['id']) + ->order('id desc') + ->select(); + foreach ($payOrders as &$poItem) { + $poItem['scene_text'] = $sceneMap[$poItem['scene'] ?? ''] ?? ($poItem['scene'] ?? '-'); + $poItem['status_text'] = \app\common\library\PaymentService::payOrderStatusLabel($poItem['status'] ?? ''); + $poItem['refund_status_text'] = \app\common\library\PaymentService::refundStatusLabel($poItem['refund_status'] ?? 'none'); + } + unset($poItem); + $this->view->assign([ 'row' => $row, 'goodsList' => $goodsList, @@ -560,11 +598,126 @@ class Order extends Backend 'quoteList' => $quoteList, 'review' => $review, 'settle' => $settle, + 'settlePreview' => $settlePreview, 'merchantOrder' => $merchantOrder, 'invoiceInfo' => $invoiceInfo, 'appealList' => $appealList, + 'payOrders' => $payOrders, ]); return $this->view->fetch(); } + + /** + * 后台主动同步单笔支付/退款流水在微信网关的真实状态 + */ + public function syncpay($ids = null) + { + if (!$this->request->isPost()) { + $this->error('非法请求方式'); + } + $payOrderId = $this->request->post('pay_order_id/d', 0); + if ($payOrderId <= 0) { + $payOrderId = (int)$ids; + } + $po = Db::name('pay_order')->where('id', $payOrderId)->find(); + if (!$po) { + $this->error('未找到指定的支付交易流水'); + } + + $status = (string)($po['status'] ?? ''); + try { + if (in_array($status, ['refunding', 'refund_failed'], true) || trim((string)($po['refund_no'] ?? '')) !== '') { + $updated = \app\common\library\PaymentService::syncRefundPayOrder($payOrderId); + $label = \app\common\library\PaymentService::refundStatusLabel($updated['refund_status'] ?? ''); + $this->success('退款状态已从微信同步:' . $label); + } + + if ($status === 'paid') { + $this->success('该笔流水当前已完成微信入账,状态正常'); + } + + $res = \app\common\library\PaymentService::confirmOrderPay((string)$po['out_trade_no']); + if (!empty($res['paid'])) { + $msg = '微信已确认支付成功,系统已完成入账核销'; + if (!empty($res['duplicate_refund_error'])) { + $this->error($res['duplicate_refund_error']); + } + $this->success($msg); + } + $this->success('同步成功,当前微信状态:' . ($res['trade_state_text'] ?? '待确认')); + } catch (\Throwable $e) { + $this->error($e->getMessage()); + } + } + + /** + * 后台手动对退款失败(或未退)的微信支付流水重发原路退款 + */ + public function retryrefund($ids = null) + { + if (!$this->request->isPost()) { + $this->error('非法请求方式'); + } + $payOrderId = $this->request->post('pay_order_id/d', 0); + if ($payOrderId <= 0) { + $payOrderId = (int)$ids; + } + $po = Db::name('pay_order')->where('id', $payOrderId)->find(); + if (!$po) { + $this->error('未找到指定的支付交易流水'); + } + + $order = Db::name('order')->where('id', (int)$po['order_id'])->find(); + if (!$order) { + $this->error('关联业务订单不存在'); + } + if ((int)$order['settle_status'] === 1) { + $this->error('该工单已完成向师傅结算分账,不允许再执行退款'); + } + + $status = (string)($po['status'] ?? ''); + if ($status === 'refunding') { + $this->error('该流水处于微信退款处理中,请先点击【同步微信状态】核实是否已到账,切勿盲目重退'); + } + if (!in_array($status, ['refund_failed', 'paid'], true)) { + $this->error('当前流水状态为「' . \app\common\library\PaymentService::payOrderStatusLabel($status) . '」,不可发起原路退款'); + } + + $reason = trim($this->request->post('reason/s', '')); + try { + $after = \app\common\library\PaymentService::refundSinglePayOrder( + $payOrderId, + $reason !== '' ? $reason : '管理员后台手动发起/重试原路退款' + ); + } catch (\Throwable $e) { + $this->error('发起原路退款失败:' . $e->getMessage()); + } + + // 若主工单已处于取消状态且本笔退款受理/到账成功,同步汇总真实退款金额 + if ((int)$order['status'] === 9 && in_array($after['status'] ?? '', ['refunding', 'refunded'], true)) { + $allPaid = Db::name('pay_order') + ->where('order_id', (int)$order['id']) + ->whereIn('status', ['paid', 'refunding', 'refunded', 'refund_failed']) + ->select(); + $hasUnsettledRefund = false; + $sumRefund = 0.00; + foreach ($allPaid as $item) { + if (in_array($item['status'], ['refunding', 'refunded'], true)) { + $sumRefund = round($sumRefund + (float)($item['refund_amount'] ?: $item['amount']), 2); + } else { + $hasUnsettledRefund = true; + } + } + Db::name('order')->where('id', (int)$order['id'])->update([ + 'pay_status' => $hasUnsettledRefund ? (int)$order['pay_status'] : 2, + 'refund_amount' => $sumRefund, + 'updatetime' => time(), + ]); + } + + $statusLabel = \app\common\library\PaymentService::payOrderStatusLabel($after['status'] ?? ''); + $refundLabel = \app\common\library\PaymentService::refundStatusLabel($after['refund_status'] ?? 'none'); + $this->success('微信原路退款请求已提交,当前状态:' . $statusLabel . '(' . $refundLabel . ')'); + } } diff --git a/server/application/admin/controller/Service.php b/server/application/admin/controller/Service.php index 72f2b80..fba725c 100644 --- a/server/application/admin/controller/Service.php +++ b/server/application/admin/controller/Service.php @@ -346,12 +346,13 @@ class Service extends Backend } /** - * 同步商品规格型号表(支持特殊规格单独设价覆盖,留空或0则沿用商品默认服务价) + * 同步商品规格型号表(支持特殊规格单独设价覆盖与规格参考图,同步联动系统图库) */ protected function syncSpecs($serviceId, $specsInput) { if (empty($serviceId)) return; $now = time(); + $catId = (int)Db::name('service')->where('id', $serviceId)->value('cat_id'); $keepIds = []; if (!empty($specsInput) && is_array($specsInput)) { $idx = 0; @@ -359,6 +360,7 @@ class Service extends Backend $name = trim(isset($item['name']) ? $item['name'] : ''); if ($name === '') continue; $specDesc = isset($item['description']) ? trim($item['description']) : ''; + $specImage = isset($item['image']) ? trim((string)$item['image']) : ''; $specPrice = (isset($item['base_price']) && is_numeric($item['base_price']) && (float)$item['base_price'] > 0) ? round((float)$item['base_price'], 2) : 0.00; @@ -367,18 +369,23 @@ class Service extends Backend $idx++; if ($specId > 0) { - Db::name('service_spec')->where('id', $specId)->where('service_id', $serviceId)->update([ + $updateData = [ 'name' => $name, 'base_price' => $specPrice, 'description' => $specDesc, 'weigh' => $weigh, 'updatetime' => $now, - ]); + ]; + if (array_key_exists('image', $item)) { + $updateData['image'] = $specImage; + } + Db::name('service_spec')->where('id', $specId)->where('service_id', $serviceId)->update($updateData); $keepIds[] = $specId; } else { - $newId = Db::name('service_spec')->insertGetId([ + $specId = (int)Db::name('service_spec')->insertGetId([ 'service_id' => $serviceId, 'name' => $name, + 'image' => $specImage, 'base_price' => $specPrice, 'description' => $specDesc, 'weigh' => $weigh, @@ -386,7 +393,35 @@ class Service extends Backend 'createtime' => $now, 'updatetime' => $now, ]); - $keepIds[] = $newId; + $keepIds[] = $specId; + } + + // 若管理员为细分规格上传了参考图,自动同步更新或补充至 fa_system_gallery + if ($specImage !== '') { + $existingGal = Db::name('system_gallery') + ->where('service_id', $serviceId) + ->where('spec_id', $specId) + ->find(); + if ($existingGal) { + Db::name('system_gallery')->where('id', $existingGal['id'])->update([ + 'cat_id' => $catId, + 'title' => $name, + 'image' => $specImage, + 'updatetime' => $now, + ]); + } else { + Db::name('system_gallery')->insert([ + 'cat_id' => $catId, + 'service_id' => $serviceId, + 'spec_id' => $specId, + 'title' => $name, + 'image' => $specImage, + 'weigh' => $weigh, + 'status' => 'normal', + 'createtime' => $now, + 'updatetime' => $now, + ]); + } } } } @@ -417,6 +452,7 @@ class Service extends Backend Db::startTrans(); try { Db::name('service_spec')->where('service_id', 'in', $idArr)->delete(); + Db::name('system_gallery')->where('service_id', 'in', $idArr)->delete(); $deleted = Db::name('service')->where('id', 'in', $idArr)->delete(); Db::commit(); } catch (Exception $e) { diff --git a/server/application/admin/controller/Withdraw.php b/server/application/admin/controller/Withdraw.php index 13eaba3..e94b0dc 100644 --- a/server/application/admin/controller/Withdraw.php +++ b/server/application/admin/controller/Withdraw.php @@ -3,6 +3,8 @@ namespace app\admin\controller; use app\common\controller\Backend; +use app\common\library\FinanceConfigService; +use app\common\library\PaymentService; use think\Db; use think\exception\PDOException; @@ -24,7 +26,7 @@ class Withdraw extends Backend */ protected $model = null; protected $relationSearch = true; - protected $searchFields = 'id,account,audit_msg,master.realname,master.mobile'; + protected $searchFields = 'id,account,audit_msg,fail_code,fail_reason,out_bill_no,transfer_bill_no,master.realname,master.mobile'; /** * 审核状态机:当前状态 => 允许流转到的目标状态 @@ -105,10 +107,12 @@ class Withdraw extends Backend } /** - * 财务审核:申请信息只读,仅允许合法状态流转并做资金联动 - * - 驳回:退回师傅余额并解冻,写入钱包流水,通知师傅 - * - 通过:仅标记审核通过,等待财务打款 - * - 打款完成:划出冻结资金,写入钱包流水,通知师傅 + * 财务审核与微信商家转账打款处理: + * - 微信渠道(channel=wechat): + * 1) 审核通过(pass):调用微信支付 V3 新版「商家转账(场景 1005 佣金报酬)」接口生成转账单,等待师傅在小程序确认收款 + * 2) 同步状态(sync):主动查询微信商家转账单状态,若 SUCCESS 则划出冻结资金标记 done;若 FAIL/CANCELLED 则解冻退回师傅余额 + * 3) 驳回/撤销(reject):若尚未发微信转账则直接退回余额;若已发微信转账则先调微信查单并撤销确认后再退回余额(防双重资损) + * - 银行卡渠道(channel=bank):保留审核通过 → 线下打款完成(done) / 驳回(reject) */ public function edit($ids = null) { @@ -116,40 +120,101 @@ class Withdraw extends Backend if (!$row) { $this->error(__('No Results were found')); } + + $wdCfg = FinanceConfigService::getWithdrawConfig(); + $isWechat = ($row['channel'] === 'wechat'); + $useWxApi = $isWechat && ($wdCfg['transfer_mode'] === 'wx_mch_transfer'); + $hasWxBill = !empty($row['out_bill_no']); + $txState = strtoupper(trim((string)($row['transfer_state'] ?? ''))); + $flowOptions = []; + if ($row['status'] === 'wait') { + if ($hasWxBill) { + if (in_array($txState, ['FAIL', 'NOT_FOUND'], true) && $useWxApi) { + $flowOptions = ['pass', 'sync', 'reject']; + } else { + $flowOptions = ['sync', 'reject']; + } + } else { + $flowOptions = ['pass', 'reject']; + } + } elseif ($row['status'] === 'pass') { + if ($isWechat && $hasWxBill) { + if (in_array($txState, ['FAIL', 'NOT_FOUND'], true) && $useWxApi) { + $flowOptions = ['pass', 'sync', 'reject']; + } else { + $flowOptions = ['sync', 'reject']; + } + } elseif ($useWxApi && !$hasWxBill) { + $flowOptions = ['pass', 'done', 'reject']; + } else { + $flowOptions = ['done', 'reject']; + } + } + if (false === $this->request->isPost()) { - // 展示师傅当前钱包情况,辅助财务判断 $master = Db::name('master')->where('id', (int)$row['master_id'])->field('id,realname,mobile,user_id,money,frozen_money')->find(); $this->view->assign('row', $row); $this->view->assign('master', $master ?: []); - $this->view->assign('flowOptions', $this->statusFlow[$row['status']] ?? []); + $this->view->assign('flowOptions', $flowOptions); + $this->view->assign('useWxApi', $useWxApi); + $this->view->assign('transferStateLabel', PaymentService::transferStateLabel($row['transfer_state'] ?? '')); return $this->view->fetch(); } $status = (string)$this->request->post('row.status'); $auditMsg = trim((string)$this->request->post('row.audit_msg')); - // 状态机校验:终态禁止操作,流转目标必须合法 - if (!isset($this->statusFlow[$row['status']])) { - $this->error('该提现单已处于终态(已驳回/已打款),不允许再次审核'); + if (empty($flowOptions)) { + $this->error('该提现单已处于终态(已驳回/已打款),不允许再次操作'); } - if (!in_array($status, $this->statusFlow[$row['status']], true)) { + if (!in_array($status, $flowOptions, true)) { $this->error('状态流转不合法,请刷新后重试'); } if ($status === 'reject' && $auditMsg === '') { - $this->error('驳回时必须填写驳回原因'); + $this->error('驳回或撤销转账时必须填写原因'); } + // 1. 驳回或撤销转账(统一走防双重资损方法) + if ($status === 'reject') { + try { + $res = PaymentService::cancelOrRejectWithdraw((int)$row['id'], $auditMsg); + } catch (\Throwable $e) { + $this->error($e->getMessage()); + } + $this->success($res['message'] ?? '已驳回处理'); + } + + // 2. 微信钱包渠道(启用微信商家转账模式时):发起微信商家转账(1005) 或 同步微信转账状态 + if ($useWxApi && $status === 'pass') { + try { + PaymentService::initiateWithdrawTransfer((int)$row['id'], $auditMsg); + } catch (\Throwable $e) { + $this->error($e->getMessage()); + } + $this->success('已审核通过并发起微信商家转账,等待师傅在小程序「我的钱包」确认收款'); + } + + if ($isWechat && $status === 'sync') { + try { + $updated = PaymentService::syncWithdrawTransfer((int)$row['id']); + } catch (\Throwable $e) { + $this->error('同步微信状态失败:' . $e->getMessage()); + } + $stateText = PaymentService::transferStateLabel($updated['transfer_state'] ?? ''); + $this->success('已同步最新微信转账状态:' . $stateText); + } + + // 3. 线下银行卡打款(或历史未发微信单的线下打款完成) $masterId = (int)$row['master_id']; $amount = round((float)$row['amount'], 2); Db::startTrans(); try { - // 乐观锁更新状态,防止并发重复审核 $affected = Db::name('withdraw') ->where('id', $row['id']) ->where('status', $row['status']) ->update([ 'status' => $status, - 'audit_msg' => $auditMsg, + 'audit_msg' => $auditMsg !== '' ? $auditMsg : $row['audit_msg'], 'updatetime' => time(), ]); if (!$affected) { @@ -161,32 +226,11 @@ class Withdraw extends Backend throw new \Exception('提现师傅不存在'); } - if ($status === 'reject') { - // 驳回:解冻并退回余额,写入钱包流水 - $newMoney = round((float)$master['money'] + $amount, 2); - Db::name('master')->where('id', $masterId)->update([ - 'money' => $newMoney, - 'frozen_money' => round((float)$master['frozen_money'] - $amount, 2), - 'updatetime' => time(), - ]); - Db::name('wallet_log')->insert([ - 'user_type' => 'master', - 'user_id' => $masterId, - 'type' => 'refund', - 'amount' => $amount, - 'balance' => $newMoney, - 'order_id' => 0, - 'remark' => '提现驳回,申请金额退回钱包余额' . ($auditMsg !== '' ? '(原因:' . $auditMsg . ')' : ''), - 'createtime' => time(), - ]); - $this->sendMasterMessage($masterId, (int)$master['user_id'], '提现申请已驳回', '您的提现申请 ¥' . $amount . ' 未通过财务审核:' . ($auditMsg !== '' ? $auditMsg : '无') . ',申请金额已退回钱包余额'); - } elseif ($status === 'pass') { - // 审核通过:仅标记,资金保持冻结,等待财务打款 - $this->sendMasterMessage($masterId, (int)$master['user_id'], '提现审核通过', '您的提现申请 ¥' . $amount . ' 已通过审核,财务将尽快安排打款到 ' . ($row['channel'] === 'bank' ? '银行卡' : '微信钱包')); + if ($status === 'pass') { + $this->sendMasterMessage($masterId, (int)$master['user_id'], '提现审核通过', '您的提现申请 ¥' . $amount . ' 已通过审核,财务将尽快安排打款到银行卡'); } elseif ($status === 'done') { - // 打款完成:冻结资金划出 Db::name('master')->where('id', $masterId)->update([ - 'frozen_money' => round((float)$master['frozen_money'] - $amount, 2), + 'frozen_money' => max(0, round((float)$master['frozen_money'] - $amount, 2)), 'updatetime' => time(), ]); Db::name('wallet_log')->insert([ @@ -209,6 +253,27 @@ class Withdraw extends Backend $this->success(); } + /** + * 后台列表一键同步微信商家转账单最新状态 + */ + public function sync($ids = null) + { + $row = $this->model->get($ids); + if (!$row) { + $this->error(__('No Results were found')); + } + if (empty($row['out_bill_no'])) { + $this->error('该提现单尚未发起微信商家转账'); + } + try { + $updated = PaymentService::syncWithdrawTransfer((int)$row['id']); + } catch (\Throwable $e) { + $this->error('同步失败:' . $e->getMessage()); + } + $stateText = PaymentService::transferStateLabel($updated['transfer_state'] ?? ''); + $this->success('同步成功,当前微信状态:' . $stateText); + } + /** * 提现单仅由师傅端发起,禁止后台手工添加 */ diff --git a/server/application/admin/controller/general/Config.php b/server/application/admin/controller/general/Config.php index 5dac56c..6966682 100755 --- a/server/application/admin/controller/general/Config.php +++ b/server/application/admin/controller/general/Config.php @@ -181,7 +181,7 @@ class Config extends Backend if ($name && $config) { try { $config->delete(); - ConfigModel::refreshFile(); + ConfigModel::refreshFile($name); } catch (Exception $e) { $this->error($e->getMessage()); } diff --git a/server/application/admin/controller/service/Gallery.php b/server/application/admin/controller/service/Gallery.php new file mode 100644 index 0000000..0b0b515 --- /dev/null +++ b/server/application/admin/controller/service/Gallery.php @@ -0,0 +1,179 @@ +model = new \app\admin\model\service\Gallery; + $this->view->assign('statusList', $this->model->getStatusList()); + } + + public function index() + { + $this->request->filter(['strip_tags', 'trim']); + if (false === $this->request->isAjax()) { + $cats = Db::name('service_cat') + ->where('status', 'normal') + ->where('id', '<>', 15) + ->order('weigh desc, id asc') + ->select(); + $initServiceId = (int)$this->request->get('service_id', 0); + $initServiceName = $initServiceId > 0 ? (string)Db::name('service')->where('id', $initServiceId)->value('name') : ''; + $this->view->assign([ + 'catList' => $cats, + 'initServiceId' => $initServiceId, + 'initServiceName' => $initServiceName, + ]); + return $this->view->fetch(); + } + if ($this->request->request('keyField')) { + return $this->selectpage(); + } + [$where, $sort, $order, $offset, $limit] = $this->buildparams(); + $list = $this->model + ->with(['cat', 'service']) + ->where($where) + ->order($sort, $order) + ->paginate($limit); + + return json(['total' => $list->total(), 'rows' => $list->items()]); + } + + public function add() + { + if ($this->request->isPost()) { + $params = $this->request->post('row/a'); + if (!$params) { + $this->error(__('Parameter %s can not be empty', '')); + } + $params = $this->preExcludeFields($params); + $serviceId = isset($params['service_id']) ? (int)$params['service_id'] : 0; + if ($serviceId <= 0) { + $this->error('请选择所属商品'); + } + $svc = Db::name('service')->where('id', $serviceId)->find(); + if (!$svc) { + $this->error('所选商品不存在'); + } + $catId = (int)$svc['cat_id']; + $rawImages = isset($params['image']) ? trim((string)$params['image']) : ''; + $imgArr = array_values(array_filter(array_map('trim', explode(',', $rawImages)))); + if (empty($imgArr)) { + $this->error('请至少上传一张系统参考图'); + } + + $baseTitle = isset($params['title']) ? trim((string)$params['title']) : ''; + $weigh = isset($params['weigh']) ? (int)$params['weigh'] : 100; + $status = (!empty($params['status']) && in_array($params['status'], ['normal', 'hidden'], true)) ? $params['status'] : 'normal'; + $now = time(); + + Db::startTrans(); + try { + foreach ($imgArr as $idx => $imgUrl) { + $itemTitle = $baseTitle !== '' ? $baseTitle : (string)$svc['name']; + if (count($imgArr) > 1 && $baseTitle !== '') { + $itemTitle = $baseTitle . ' ' . ($idx + 1); + } + Db::name('system_gallery')->insert([ + 'cat_id' => $catId, + 'service_id' => $serviceId, + 'spec_id' => 0, + 'title' => mb_substr($itemTitle, 0, 80), + 'image' => $imgUrl, + 'weigh' => max(0, $weigh - $idx), + 'status' => $status, + 'createtime' => $now, + 'updatetime' => $now, + ]); + } + Db::commit(); + } catch (Exception $e) { + Db::rollback(); + $this->error($e->getMessage()); + } + $this->success(); + } + + $initServiceId = (int)$this->request->get('service_id', 0); + $initCatId = (int)$this->request->get('cat_id', 0); + if ($initServiceId > 0 && $initCatId <= 0) { + $initCatId = (int)Db::name('service')->where('id', $initServiceId)->value('cat_id'); + } + $this->view->assign([ + 'initCatId' => $initCatId ?: '', + 'initServiceId' => $initServiceId ?: '', + ]); + return $this->view->fetch(); + } + + public function edit($ids = null) + { + $row = $this->model->get($ids); + if (!$row) { + $this->error(__('No Results were found')); + } + if ($this->request->isPost()) { + $params = $this->request->post('row/a'); + if (!$params) { + $this->error(__('Parameter %s can not be empty', '')); + } + $params = $this->preExcludeFields($params); + $serviceId = isset($params['service_id']) ? (int)$params['service_id'] : (int)$row['service_id']; + if ($serviceId <= 0) { + $this->error('请选择所属商品'); + } + $svc = Db::name('service')->where('id', $serviceId)->find(); + if (!$svc) { + $this->error('所选商品不存在'); + } + $params['cat_id'] = (int)$svc['cat_id']; + if (empty($params['title']) || trim((string)$params['title']) === '') { + $params['title'] = (string)$svc['name']; + } + if (empty($params['image']) || trim((string)$params['image']) === '') { + $this->error('系统参考图不能为空'); + } + + Db::startTrans(); + try { + $row->allowField(true)->save($params); + if ((int)$row['spec_id'] > 0) { + Db::name('service_spec') + ->where('id', (int)$row['spec_id']) + ->where('service_id', $serviceId) + ->update([ + 'image' => trim((string)$params['image']), + 'updatetime' => time(), + ]); + } + Db::commit(); + } catch (Exception $e) { + Db::rollback(); + $this->error($e->getMessage()); + } + $this->success(); + } + $this->view->assign('row', $row); + return $this->view->fetch(); + } +} diff --git a/server/application/admin/lang/zh-cn/finance_config.php b/server/application/admin/lang/zh-cn/finance_config.php new file mode 100644 index 0000000..d2e6599 --- /dev/null +++ b/server/application/admin/lang/zh-cn/finance_config.php @@ -0,0 +1,34 @@ + '编号', + 'Name' => '规则名称', + 'Order_mode' => '订单类型', + 'Customer_type' => '下单人', + 'Cat_id' => '服务分类', + 'Urgent_mode' => '是否加急', + 'Min_amount' => '最低订单金额(元)', + 'Max_amount' => '最高订单金额(元)', + 'Calc_mode' => '抽佣方式', + 'Commission_rate' => '抽佣比例(%)', + 'Fixed_fee' => '每单固定费(元)', + 'Min_commission' => '单笔最少抽佣(元)', + 'Max_commission' => '单笔最多抽佣(元)', + 'Weigh' => '优先级', + 'Status' => '状态', + 'Remark' => '备注', + 'Normal' => '启用', + 'Hidden' => '停用', + 'All' => '全部', + 'Fixed' => '一口价', + 'Quote' => '师傅报价', + 'Custom' => '业主自主出价', + 'Range' => '区间定价', + 'Personal' => '个人下单', + 'Merchant' => '企业商家', + 'Urgent' => '仅加急单', + 'Percent' => '按比例抽成', + 'Mixed' => '按比例加固定费', + 'Createtime' => '创建时间', + 'Updatetime' => '更新时间', +]; diff --git a/server/application/admin/lang/zh-cn/master_cert.php b/server/application/admin/lang/zh-cn/master_cert.php new file mode 100644 index 0000000..ed758dc --- /dev/null +++ b/server/application/admin/lang/zh-cn/master_cert.php @@ -0,0 +1,20 @@ + 'ID', + 'Master_id' => '师傅ID', + 'Cert_code' => '证书编码', + 'Cert_name' => '证书名称', + 'Is_custom' => '证书来源', + 'Cert_image' => '证书照片', + 'Expire_date' => '有效期至', + 'Is_long_term' => '长期有效', + 'Status' => '审核状态', + 'Audit_msg' => '审核意见', + 'Audittime' => '审核时间', + 'Createtime' => '提交时间', + 'Updatetime' => '更新时间', + 'Pending' => '待审核', + 'Pass' => '已通过', + 'Reject' => '已驳回', +]; diff --git a/server/application/admin/lang/zh-cn/service/gallery.php b/server/application/admin/lang/zh-cn/service/gallery.php new file mode 100644 index 0000000..cf86907 --- /dev/null +++ b/server/application/admin/lang/zh-cn/service/gallery.php @@ -0,0 +1,14 @@ + 'ID', + 'Cat_id' => '所属类目', + 'Service_id' => '所属商品', + 'Spec_id' => '关联规格', + 'Title' => '款式/图片名称', + 'Image' => '系统参考图', + 'Weigh' => '排序权重', + 'Status' => '显示状态', + 'Createtime' => '创建时间', + 'Updatetime' => '更新时间', +]; diff --git a/server/application/admin/model/Agreement.php b/server/application/admin/model/Agreement.php new file mode 100644 index 0000000..8030d20 --- /dev/null +++ b/server/application/admin/model/Agreement.php @@ -0,0 +1,27 @@ + '正常展示', + 'hidden' => '暂不展示', + ]; + } +} diff --git a/server/application/admin/model/CommissionRule.php b/server/application/admin/model/CommissionRule.php new file mode 100644 index 0000000..e843b9e --- /dev/null +++ b/server/application/admin/model/CommissionRule.php @@ -0,0 +1,61 @@ + '不限订单类型', + 'fixed' => '一口价', + 'quote' => '师傅报价', + 'custom' => '业主自主出价', + 'range' => '区间定价', + ]; + } + + public function getCustomerTypeList(): array + { + return [ + 'all' => '不限下单人', + 'personal' => '个人下单', + 'merchant' => '企业商家', + ]; + } + + public function getUrgentModeList(): array + { + return [ + 'all' => '不限是否加急', + 'normal' => '仅普通单', + 'urgent' => '仅加急单', + ]; + } + + public function getCalcModeList(): array + { + return [ + 'percent' => '按比例抽成 (%)', + 'fixed' => '每单固定收费 (元)', + 'mixed' => '按比例 + 每单固定费', + ]; + } + + public function getStatusList(): array + { + return [ + 'normal' => '启用', + 'hidden' => '停用', + ]; + } +} diff --git a/server/application/admin/model/MasterCert.php b/server/application/admin/model/MasterCert.php new file mode 100644 index 0000000..faaad54 --- /dev/null +++ b/server/application/admin/model/MasterCert.php @@ -0,0 +1,40 @@ + '待审核', + 'pass' => '已通过', + 'reject' => '已驳回', + ]; + } + + public function getStatusTextAttr($value, $data) + { + $value = $value ?: ($data['status'] ?? ''); + $list = $this->getStatusList(); + return $list[$value] ?? ''; + } + + public function master() + { + return $this->belongsTo('Master', 'master_id', 'id', [], 'LEFT')->setEagerlyType(0); + } +} diff --git a/server/application/admin/model/Withdraw.php b/server/application/admin/model/Withdraw.php index 72866bb..7499e4e 100644 --- a/server/application/admin/model/Withdraw.php +++ b/server/application/admin/model/Withdraw.php @@ -26,7 +26,8 @@ class Withdraw extends Model // 追加属性 protected $append = [ 'channel_text', - 'status_text' + 'status_text', + 'transfer_state_text' ]; @@ -57,6 +58,15 @@ class Withdraw extends Model return $list[$value] ?? ''; } + public function getTransferStateTextAttr($value, $data) + { + $state = $data['transfer_state'] ?? ''; + if ($state === '' && ($data['channel'] ?? '') === 'bank') { + return '线下转账'; + } + return \app\common\library\PaymentService::transferStateLabel($state); + } + diff --git a/server/application/admin/model/service/Gallery.php b/server/application/admin/model/service/Gallery.php new file mode 100644 index 0000000..379f099 --- /dev/null +++ b/server/application/admin/model/service/Gallery.php @@ -0,0 +1,41 @@ + __('Normal'), 'hidden' => __('Hidden')]; + } + + public function getStatusTextAttr($value, $data) + { + $value = $value ?: ($data['status'] ?? ''); + $list = $this->getStatusList(); + return $list[$value] ?? ''; + } + + public function cat() + { + return $this->belongsTo('app\admin\model\service\Cat', 'cat_id', 'id', [], 'LEFT')->setEagerlyType(0); + } + + public function service() + { + return $this->belongsTo('app\admin\model\Service', 'service_id', 'id', [], 'LEFT')->setEagerlyType(0); + } +} diff --git a/server/application/admin/view/agreement/index.html b/server/application/admin/view/agreement/index.html new file mode 100644 index 0000000..e955e61 --- /dev/null +++ b/server/application/admin/view/agreement/index.html @@ -0,0 +1,516 @@ + + +
+
+ {:build_heading(null, FALSE)} + +
+ +
+
+ +
+
+
+ + + 有未保存修改 +
+
+ 编码: · 修订 #1 · 最近更新: +
+
+ +
+ {:token()} + + +
+ +
+ +
+
+ +
+ +
+
+ 版本 + +
+
+
+
+ 生效日期 + +
+
+
+ +
+ +
+
+ + +
+ +
+
+ +
+ +
+
+
+
+ + + + + + + + + + + + + + +
+ +
+ +
+ +
+
+
+ + {if $canSave} +
+
+ + +
+
+ {/if} +
+
+ + +
+
+
+ 小程序独立页面实时预览 + +
+
+
+ + 协议详情 +
+
+
+
+
+ + +
+
+
+ + 条款已更新,浅橙色背景为本次调整内容(仅用户首次查看时标注) +
+
+
+
+
+
+
+
+
+ + diff --git a/server/application/admin/view/finance_config/index.html b/server/application/admin/view/finance_config/index.html new file mode 100644 index 0000000..81e4959 --- /dev/null +++ b/server/application/admin/view/finance_config/index.html @@ -0,0 +1,503 @@ + + +
+
+ {:build_heading(null, FALSE)} + +
+ +
+
+ +
+
+
按订单条件抽佣按实际下单条件自动匹配不同抽佣方案
+
+ 匹配说明:新订单按 优先级从高到低 匹配(优先级相同时,限制条件越具体越先命中),命中第一条即生效;若都不符合,则按下方「兜底默认抽佣」执行。已下单工单永久保留下单时的方案。 +
+ +
+ + {if $auth->check('finance_config/rule_add')} + 新增规则 + {/if} + {if $auth->check('finance_config/rule_multi')} + 启用 + 停用 + {/if} + {if $auth->check('finance_config/rule_del')} + {:__('Delete')} + {/if} +
+
+ +
+
+
+ +
+ {:token()} + + +
+
施工增项抽佣设置对所有新创建订单通用
+
+ +
+
+ + +
+
+
+
+ +
+
兜底默认抽佣仅当订单未命中上方任何启用规则时使用
+ +
+ +
+ +
+
+ +
+ +
+
+ + % +
+
+
+ +
+ +
+
+ ¥ + + 元/单 +
+
+
+ +
+ +
+
+
+ 最少收 ¥ + +
+ ~ +
+ 最多收 ¥ + +
+
+ 填 0 表示不限制单笔最少或最多抽佣金额 +
+
+ +
+ +
+
按 ¥200.00 订单试算中...
+
+
+ + {if $auth->check('finance_config/save')} +
+ +
+ +
+
+ {/if} +
+
+
+ + +
+
+ {:token()} + + +
+
基础开关与实名要求
+ +
+ +
+
+ + +
+
+
+ +
+ +
+ +
+
+ +
+ +
+
+ + +
+ 微信小程序提审建议默认仅勾选「微信零钱」,避免收集银行卡隐私信息触发类目审核风险 +
+
+ +
+ +
+
+ + +
+
+
+
+ +
+
额度、次数与开放时间
+ +
+ +
+
+
+ 最低 ¥ + +
+ ~ +
+ 最高 ¥ + +
+
+ 微信商家转账硬性规定:实到净额最低 ¥0.10,单笔最高不超过 ¥20,000.00 +
+
+ +
+ +
+
+
+ 日累计 ¥ + +
+
+ 日次数 + + 次 +
+
+ 填 0 表示不限制单日累计金额或次数 +
+
+ +
+ +
+
+ {php}$dayLabels = [1=>'周一',2=>'周二',3=>'周三',4=>'周四',5=>'周五',6=>'周六',7=>'周日'];{/php} + {foreach name="dayLabels" item="dName" key="dNum"} + + {/foreach} +
+
+
+ +
+ +
+
+ + ~ + +
+ 24小时制 时:分(全天开放填 00:00 ~ 23:59) +
+
+
+ +
+
提现手续费
+ +
+ +
+ +
+
+ +
+ +
+
+
+ + % +
+
+ 最少 ¥ + +
+
+ 最多 ¥ + +
+
+ 最少与最多填 0 表示不限制 +
+
+ +
+ +
+
+ ¥ + + 元/笔 +
+
+
+ +
+ +
+
按提现 ¥100.00 试算中...
+
+
+
+ +
+
打款审核与微信商家转账(场景 1005)
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+ +
+
+ ≤ ¥ + + 元自动发起 +
+
+
+ +
+ +
+
+
+ 场景编号 + +
+
+ 收款感知 + +
+
+ 须与微信商户后台开通的转账场景一致(默认场景 1005 对应“劳务报酬”) +
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+
+ + {if $auth->check('finance_config/save')} +
+ +
+ +
+
+ {/if} +
+
+
+
+
diff --git a/server/application/admin/view/finance_config/rule_add.html b/server/application/admin/view/finance_config/rule_add.html new file mode 100644 index 0000000..3d7d659 --- /dev/null +++ b/server/application/admin/view/finance_config/rule_add.html @@ -0,0 +1,3 @@ +
+ {include file="finance_config/rule_form" /} +
diff --git a/server/application/admin/view/finance_config/rule_edit.html b/server/application/admin/view/finance_config/rule_edit.html new file mode 100644 index 0000000..9eb2ee5 --- /dev/null +++ b/server/application/admin/view/finance_config/rule_edit.html @@ -0,0 +1,3 @@ +
+ {include file="finance_config/rule_form" /} +
diff --git a/server/application/admin/view/finance_config/rule_form.html b/server/application/admin/view/finance_config/rule_form.html new file mode 100644 index 0000000..68fc2f3 --- /dev/null +++ b/server/application/admin/view/finance_config/rule_form.html @@ -0,0 +1,210 @@ + + +{:token()} + +
+ +
+ +
+
+ +
一、哪些订单适用本规则不需要限制的项保持“不限”即可
+ +
+ +
+ +
+ +
+ +
+
+ +
+ +
+ +
+ +
+ +
+
+ +
+ +
+
+
+
+ 满 ¥ + +
+
+
+
+ 至 ¥ + +
+
+
+ 两边填 0 表示不限金额;只填左边“满 ¥500”表示 500 元及以上适用。师傅报价单会在选定报价后按最终成交价自动匹配。 +
+
+ +
二、平台怎么抽佣下单即锁定规则,后续修改不影响历史订单
+ +
+ +
+ +
+
+ +
+ +
+
+ + %(如填 10 表示每 100 元订单平台抽 10 元) +
+
+
+ + + +
+ +
+
+
+
+ 最少收 ¥ + +
+
+
+
+ 最多收 ¥ + +
+
+
+ 填 0 表示不限制;最多抽佣不会超过订单实付总额。 +
+
+ +
+ +
+
+
+ 假设订单金额 +
+ ¥ + +
+ - +
+
+
+
+ +
三、优先级与启用状态
+ +
+ +
+ + 数字越大越先匹配(常规填 100,基础默认填 10~30) +
+ +
+
+ {foreach name="statusList" item="vo"} + + {/foreach} +
+
+
+ +
+ +
+ +
+
+ + diff --git a/server/application/admin/view/master/detail.html b/server/application/admin/view/master/detail.html index 6cfd7fd..fa0a338 100644 --- a/server/application/admin/view/master/detail.html +++ b/server/application/admin/view/master/detail.html @@ -117,16 +117,16 @@
身份证正反面照片:
- - + + -
身份证人像面
+
身份证人像面 (防伪水印)
- - + + -
身份证国徽面
+
身份证国徽面 (防伪水印)
@@ -155,6 +155,39 @@ {/if} + +
+
+

技能资质证书 ({:count($certs)})

+
+
+ {if !empty($certs)} + + + + {foreach name="certs" item="ct"} + + + + + + + {/foreach} + +
证书名称证件照片有效期至状态
{$ct.cert_name|htmlentities}{if $ct.is_custom == 1} 自定义{/if} + {if !empty($ct.cert_image)} + + {else /}无{/if} + {if $ct.is_long_term == 1}长期有效{else/}{$ct.expire_date|default='-'|htmlentities}{/if} + {if $ct.status == 'pass'}已通过 + {elseif $ct.status == 'reject'/}已驳回 + {else/}待审核{/if} +
+ {else /} +
暂未上传技能资质证书
+ {/if} +
+
diff --git a/server/application/admin/view/master_cert/audit.html b/server/application/admin/view/master_cert/audit.html new file mode 100644 index 0000000..11cce6b --- /dev/null +++ b/server/application/admin/view/master_cert/audit.html @@ -0,0 +1,107 @@ +
+
+
+

证书申报信息(只读核验)

+
+
+
+
+
+ +
+

+ {$row.master.realname|default='-'|htmlentities} + {if !empty($row.master.mobile)}({$row.master.mobile|htmlentities}){/if} +

+
+
+
+
+
+ +
+

+ {$row.cert_name|htmlentities} + {if $row.is_custom == 1}自定义{/if} +

+
+
+
+
+ +
+
+
+ +
+

+ {if $row.is_long_term == 1} + 长期有效 + {else/} + {$row.expire_date|default='未填写'|htmlentities} + {/if} +

+
+
+
+
+
+ +
+

{$row.updatetime|default=$row.createtime|datetime}

+
+
+
+
+ +
+ +
+ {if !empty($row.cert_image)} + + + +
点击图片可在新标签页放大核验证件编号与有效期
+ {else/} + 未上传照片 + {/if} +
+
+
+
+ +
+
+

平台审核结论

+
+
+
+ +
+
+ {foreach name="statusList" item="vo"} + + {/foreach} +
+
+
+ +
+ +
+ +
+
+
+
+ + +
diff --git a/server/application/admin/view/master_cert/detail.html b/server/application/admin/view/master_cert/detail.html new file mode 100644 index 0000000..bf37dd4 --- /dev/null +++ b/server/application/admin/view/master_cert/detail.html @@ -0,0 +1,54 @@ +
+ + + + + + + + + + + + + + + + + + + + + + + + + +
申报师傅{$row.master.realname|default='-'|htmlentities}({$row.master.mobile|default='-'|htmlentities})证书名称 + {$row.cert_name|htmlentities} + {if $row.is_custom == 1}自定义{/if} +
证件有效期 + {if $row.is_long_term == 1} + 长期有效 + {else/} + {$row.expire_date|default='未填写'|htmlentities} + {/if} + 当前审核状态 + {if $row.status == 'pass'}已通过 + {elseif $row.status == 'reject'/}已驳回 + {else/}待审核{/if} +
审核意见{$row.audit_msg|default='暂无审核意见'|htmlentities}
提交时间{$row.createtime|datetime}最后更新时间{$row.updatetime|datetime}
+ +
+
证书原图核验
+
+ {if !empty($row.cert_image)} + + + + {else/} + 未上传照片 + {/if} +
+
+
diff --git a/server/application/admin/view/master_cert/index.html b/server/application/admin/view/master_cert/index.html new file mode 100644 index 0000000..24dc7a8 --- /dev/null +++ b/server/application/admin/view/master_cert/index.html @@ -0,0 +1,30 @@ +
+
+ {:build_heading(null,FALSE)} + +
+ +
+
+
+
+ + +
+
+
+
+
+
diff --git a/server/application/admin/view/order/detail.html b/server/application/admin/view/order/detail.html index 3bf4f81..a86cd3a 100644 --- a/server/application/admin/view/order/detail.html +++ b/server/application/admin/view/order/detail.html @@ -363,8 +363,9 @@
支付状态:{$row.pay_status_text}
支付渠道:{$row.pay_type|default='微信支付'|htmlentities}
交易流水号:{$row.transaction_id|default='-'|htmlentities}
-
师傅结算款:¥{if !empty($settle)}{$settle.settle_amount} (抽成¥{$settle.commission}){else/}{$row.settle_amount}{/if}
+
抽佣规则快照:{if !empty($settle)}{$settle.commission_rate|htmlentities}%{else/}{$settlePreview.commission_rate|default='10.00'|htmlentities}%{/if} {$settlePreview.rule_name|default=''|htmlentities}
结算状态:{$row.settle_status_text}
+
师傅结算款:{if !empty($settle)}¥{$settle.settle_amount}(平台抽佣 ¥{$settle.commission}){elseif $row.settle_status == 1/}¥{$row.settle_amount}{else/}预计 ¥{:number_format($settlePreview.settle_amount, 2, '.', '')}(预计抽佣 ¥{:number_format($settlePreview.commission, 2, '.', '')}){/if}
发票状态: @@ -439,6 +440,94 @@
+ +
+
+

微信支付与原路退款交易流水 ({:count($payOrders)})

+ 展示网关真实回执、异常中文诊断与人工同步/重退入口 +
+
+ {if !empty($payOrders)} +
+ + + + + + + + + + + + + + {foreach name="payOrders" item="po"} + + + + + + + + + + {/foreach} + +
ID支付场景商户单号 / 微信单号 / 退款单号金额流水 / 退款状态失败错误码与中文诊断操作
#{$po.id}{$po.scene_text|htmlentities} +
商户:{$po.out_trade_no|htmlentities}
+ {if !empty($po.transaction_id)}
微信:{$po.transaction_id|htmlentities}
{/if} + {if !empty($po.refund_no)}
退款单:{$po.refund_no|htmlentities}{if !empty($po.refund_id)}({$po.refund_id|htmlentities}){/if}
{/if} +
+
¥{$po.amount|number_format=2}
+ {if $po.refund_amount > 0}
退 ¥{$po.refund_amount|number_format=2}
{/if} +
+ {if $po.status == 'paid'}{$po.status_text} + {elseif $po.status == 'refunded'/}{$po.status_text} + {elseif $po.status == 'refunding'/}{$po.status_text} + {elseif $po.status == 'pay_failed' || $po.status == 'refund_failed'/}{$po.status_text} + {else/}{$po.status_text}{/if} + {if $po.refund_status && $po.refund_status != 'none'} +
{$po.refund_status_text}
+ {/if} + {if !empty($po.last_sync_time)} +
同步于 {:date('m-d H:i:s', $po.last_sync_time)}
+ {/if} +
+ {if !empty($po.fail_reason) || !empty($po.fail_code)} +
+ {if !empty($po.fail_code)}
错误码:{$po.fail_code|htmlentities}
{/if} +
原因:{$po.fail_reason|default='未知异常'|htmlentities}
+
+ {else /} + - + {/if} +
+ {if $po.status == 'pending' || $po.status == 'pay_failed' || $po.status == 'refunding' || $po.status == 'refund_failed'} + + {/if} + {if $po.status == 'refund_failed' || ($po.status == 'paid' && $row.status == 9 && $row.settle_status != 1)} + + {/if} + {if $po.status == 'refunded' || $po.status == 'closed' || ($po.status == 'paid' && ($row.status != 9 || $row.settle_status == 1))} + 无需人工处置 + {/if} +
+
+ {elseif $row.pay_status == 1 && ($row.pay_type == 'wechat' || $row.pay_type == '')/} +
+ 资金流水缺失告警:当前订单显示已支付(渠道:{$row.pay_type|default='未记录'|htmlentities}),但未关联任何 fa_pay_order 微信交易单,系统已禁止自动假退款,如需退款请先核查微信商户平台流水。 +
+ {else /} +
暂无微信支付交易流水记录
+ {/if} +
+
+
diff --git a/server/application/admin/view/service/add.html b/server/application/admin/view/service/add.html index 3ea983e..5d3a252 100644 --- a/server/application/admin/view/service/add.html +++ b/server/application/admin/view/service/add.html @@ -169,6 +169,7 @@
+

上传后同时作为小程序「系统图库」该商品的默认参考图;如需配置多款参考图,可在下方细分规格上传或前往「系统图库」管理。

@@ -208,11 +209,12 @@ - - - + + + + - + diff --git a/server/application/admin/view/service/edit.html b/server/application/admin/view/service/edit.html index 07e16f7..06fbe98 100644 --- a/server/application/admin/view/service/edit.html +++ b/server/application/admin/view/service/edit.html @@ -397,6 +397,7 @@ +

上传后同时作为小程序「系统图库」该商品的默认参考图;如需配置多款参考图,可在下方细分规格上传或前往「系统图库」管理。

@@ -438,21 +439,32 @@
序号细分规格/款式名称特殊规格一口价(选填)序号细分规格/款式名称规格参考图(选填)特殊规格一口价(选填) 规格说明或施工提示(选填)操作操作
- - - + + + + - + {foreach name="specs" item="sp" key="idx"} + +
序号细分规格/款式名称特殊规格一口价(选填)序号细分规格/款式名称规格参考图(选填)特殊规格一口价(选填) 规格说明或施工提示(选填)操作操作
{$idx+1} +
+ + + + + +
+
¥ diff --git a/server/application/admin/view/service/gallery/add.html b/server/application/admin/view/service/gallery/add.html new file mode 100644 index 0000000..6d13a0a --- /dev/null +++ b/server/application/admin/view/service/gallery/add.html @@ -0,0 +1,64 @@ +
+
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+ +
+
+ +
+ + +
+ +
+

支持一次选择多张图片批量入库,小程序端在选择对应类目与商品时自动展示。

+
    +
    +
    + +
    + +
    + +
    +
    + +
    + +
    +
    + {foreach name="statusList" item="vo"} + + {/foreach} +
    +
    +
    + + +
    diff --git a/server/application/admin/view/service/gallery/edit.html b/server/application/admin/view/service/gallery/edit.html new file mode 100644 index 0000000..6e3ae0a --- /dev/null +++ b/server/application/admin/view/service/gallery/edit.html @@ -0,0 +1,63 @@ +
    +
    + +
    + +
    +
    + +
    + +
    + +
    +
    + +
    + +
    + +
    +
    + +
    + +
    +
    + +
    + + +
    + +
    +
      +
      +
      + +
      + +
      + +
      +
      + +
      + +
      +
      + {foreach name="statusList" item="vo"} + + {/foreach} +
      +
      +
      + + +
      diff --git a/server/application/admin/view/service/gallery/index.html b/server/application/admin/view/service/gallery/index.html new file mode 100644 index 0000000..a476ea9 --- /dev/null +++ b/server/application/admin/view/service/gallery/index.html @@ -0,0 +1,38 @@ +
      +
      + {:build_heading(null,FALSE)} + {if !$initServiceId} + + {else/} +
      + 当前筛选商品:{$initServiceName|htmlentities}(小程序下单选择对应类目进入「系统图库」时展示以下参考图片) +
      + {/if} +
      + + +
      diff --git a/server/application/admin/view/service/index.html b/server/application/admin/view/service/index.html index 1eb1663..dbde248 100644 --- a/server/application/admin/view/service/index.html +++ b/server/application/admin/view/service/index.html @@ -18,6 +18,7 @@ {:__('Add')} {:__('Edit')} {:__('Delete')} + 系统图库
      + {if $auth->check('finance_config/index')} + 财务配置 + {/if}
      提现渠道:{$channelList[$row.channel]|default=$row.channel|htmlentities}
      -
      收款账号:{$row.account|htmlentities}
      +
      收款账号:{$row.account|default='微信零钱'|htmlentities}
      + {if !empty($row.out_bill_no)} +
      商户转账单:{$row.out_bill_no|htmlentities}
      +
      微信转账单:{$row.transfer_bill_no|default='-'|htmlentities}
      +
      微信转账态:{$row.transfer_state_text|default=$row.transfer_state|htmlentities}
      +
      确认收款时:{if $row.confirm_time}{:date('Y-m-d H:i:s', $row.confirm_time)}{else/}-{/if}
      + {/if} + {if !empty($row.fail_reason) || !empty($row.fail_code)} +
      +
      + 微信转账异常 / 失败原因 + {if !empty($row.fail_code)} {$row.fail_code|htmlentities}{/if} +
      {$row.fail_reason|default='未知异常,请同步微信状态或查看日志'|htmlentities}
      +
      +
      + {/if} + {if !empty($row.last_sync_time)} +
      最近查单:{:date('Y-m-d H:i:s', $row.last_sync_time)}
      + {/if}
      申请时间:{:date('Y-m-d H:i:s', $row.createtime)}
      更新时间:{:date('Y-m-d H:i:s', $row.updatetime)}
      财务批注:{$row.audit_msg|default='-'|htmlentities}
      diff --git a/server/application/admin/view/withdraw/edit.html b/server/application/admin/view/withdraw/edit.html index 0e2b780..fb8d554 100644 --- a/server/application/admin/view/withdraw/edit.html +++ b/server/application/admin/view/withdraw/edit.html @@ -37,11 +37,41 @@
      -

      {$channelList[$row.channel]|default=$row.channel} · {$row.account|htmlentities}

      +

      {$channelList[$row.channel]|default=$row.channel} · {$row.account|default='微信零钱'|htmlentities}

      + {if !empty($row.out_bill_no)} +
      +
      +
      + +
      +

      {$row.out_bill_no|htmlentities}

      +
      +
      +
      +
      +
      + +
      +

      {$transferStateLabel|htmlentities}

      +
      +
      +
      +
      + {/if} + {if !empty($row.fail_reason) || !empty($row.fail_code)} +
      + 上次微信转账失败 / 异常诊断 + {if !empty($row.fail_code)} {$row.fail_code|htmlentities}{/if} +
      {$row.fail_reason|htmlentities}
      + {if !empty($row.last_sync_time)} +
      最近核验时间:{:date('Y-m-d H:i:s', $row.last_sync_time)}({if !empty($row.retry_count)}已重试 {$row.retry_count} 次{/if})
      + {/if} +
      + {/if} {if $row.status != 'wait'}
      @@ -60,7 +90,15 @@

      财务审核操作

      - {if $row.status == 'wait'} + {if $row.transfer_state == 'UNKNOWN_TIMEOUT'} +

      该笔提现上次请求微信网关超时,可能已被受理。为严防双倍出款,系统已锁定重发,请先选择「同步微信转账最新状态」核实后再决定后续操作。

      + {elseif ($row.transfer_state == 'FAIL' || $row.transfer_state == 'NOT_FOUND') && !empty($useWxApi) /} +

      上次微信转账未成功出款。在排查修复上方失败原因(如商户余额不足、IP 白名单或配置错误)后,可选择「重新发起微信商家转账」使用递增单号安全重试,或选择驳回退回师傅余额。

      + {elseif !empty($useWxApi) && $row.status == 'wait' /} +

      选择「审核通过并发起微信商家转账」将直接调用微信支付 V3(场景 1005 佣金报酬)向师傅发起转账,师傅在小程序「我的钱包」点击确认收款后即自动入账微信零钱。

      + {elseif $row.channel == 'wechat' && !empty($row.out_bill_no) /} +

      该笔提现已发起微信商家转账,正在等待师傅在小程序「我的钱包」确认收款。可点击「同步微信最新状态」刷新结果,或在师傅收款前选择「撤销转账并退回余额」。

      + {elseif $row.status == 'wait' /}

      审核通过后资金保持冻结,待打款后标记「打款完成」划出冻结金额;驳回将自动退回师傅钱包余额。

      {else /}

      该单已审核通过,请线下完成打款后标记「打款完成」,系统将自动划出冻结金额;如审核有误可驳回,将自动退回师傅余额。

      @@ -70,13 +108,15 @@
      - {foreach name="flowOptions" item="op"} + {foreach name="flowOptions" item="op" key="idx"} {if $op == 'pass'} - + + {elseif $op == 'sync' /} + {elseif $op == 'reject' /} - + {elseif $op == 'done' /} - + {/if} {/foreach}
      diff --git a/server/application/admin/view/withdraw/index.html b/server/application/admin/view/withdraw/index.html index cdabcf4..f9d3f82 100644 --- a/server/application/admin/view/withdraw/index.html +++ b/server/application/admin/view/withdraw/index.html @@ -15,6 +15,9 @@
      + {if $auth->check('finance_config/index')} + 提现配置 + {/if}
      error('手机号格式不正确'); } + // 未携带有效经纬度时在写入前通过天地图自动补充坐标 + if (($data['lng'] === '' || $data['lat'] === '' || abs((float)$data['lng']) < 0.0001 || abs((float)$data['lat']) < 0.0001) + && $data['address'] !== '' + ) { + $fullAddr = trim($data['province'] . $data['city'] . $data['district'] . $data['address']); + $geo = TiandituService::geocoder($fullAddr); + if (!$geo && $data['city'] !== '') { + $geo = TiandituService::geocoder(trim($data['city'] . $data['district'] . $data['address'])); + } + if ($geo) { + $data['lng'] = (string)$geo['lng']; + $data['lat'] = (string)$geo['lat']; + } + } + if ($id > 0) { $exist = Db::name('address')->where('id', $id)->where('user_id', $this->uid())->find(); if (!$exist) { diff --git a/server/application/api/controller/Auth.php b/server/application/api/controller/Auth.php index 5aba1a9..da3094c 100644 --- a/server/application/api/controller/Auth.php +++ b/server/application/api/controller/Auth.php @@ -2,7 +2,6 @@ namespace app\api\controller; -use fast\Random; use think\Db; use think\Validate; @@ -14,9 +13,7 @@ class Auth extends Base protected $noNeedLogin = ['login', 'wxlogin', 'wxPhoneLogin']; /** - * 手机号 + 验证码登录(兜底通道) - * 验证码来源由 .env 的 [sms] 段控制:mock=1 时使用 mock_code(联调用), - * 接入真实短信后置 mock=0 并在此处改为 Sms::check($mobile, $code, 'mobilelogin') + * 手机号 + 密码登录(仅支持账号密码或微信一键登录,短信验证码通道已移除) * @ApiMethod (POST) */ public function login() @@ -26,76 +23,45 @@ class Auth extends Base $password = (string)$this->request->post('password', ''); $code = trim((string)$this->request->post('code', '')); - // 1. 手机号 + 密码登录(直连真实数据库) - if ($password !== '' || ($mobile !== '' && $code === '')) { - if ($mobile === '') { - $this->error('请填写手机号'); - } - if (!preg_match('/^1[3-9]\d{9}$/', $mobile)) { - $this->error('手机号格式不正确'); - } - if ($password === '') { - $this->error('请填写登录密码'); - } - - // 直接查询真实数据库 fa_user 表 - $user = Db::name('user')->where('mobile', $mobile)->find(); - - if (!$user) { - // 若数据库中无此手机号:直连数据库插入注册新会员 - $ret = $this->auth->register($mobile, $password, '', $mobile, []); - if (!$ret) { - $this->error($this->auth->getError() ?: '注册登录失败'); - } - } else { - if ($user['status'] !== 'normal') { - $this->error('账号已被禁用'); - } - if (empty($user['password'])) { - $this->error('该账号尚未设置登录密码,请使用验证码或微信登录后设置密码'); - } - $encryptPassword = $this->auth->getEncryptPassword($password, $user['salt']); - if ($user['password'] !== $encryptPassword) { - $this->error('手机号或密码不正确'); - } - $this->auth->direct($user['id']); - } - - // 直查真实数据库返回用户完整信息 - $this->success('登录成功', [ - 'token' => $this->auth->getToken(), - 'expires' => 2592000, - 'userinfo' => $this->profileData(), - ]); + if ($password === '' && $code !== '') { + $this->error('短信验证码通道已移除,请使用手机号密码或微信一键登录', null, 2010); } - // 2. 手机号 + 验证码登录(向下兼容) - if (!$mobile || !$code) { - $this->error('请填写手机号和验证码'); + // 手机号 + 密码登录(直连真实数据库) + if ($mobile === '') { + $this->error('请填写手机号'); } - if (!Validate::regex($mobile, "^1\d{10}$")) { + if (!preg_match('/^1[3-9]\d{9}$/', $mobile)) { $this->error('手机号格式不正确'); } - - $mockOn = (int)\think\Env::get('sms.mock', 1); - $mockCode = (string)\think\Env::get('sms.mock_code', '123456'); - if (!($mockOn === 1 && $mockCode !== '' && (string)$code === $mockCode)) { - $this->error('短信通道尚未接入,请联系管理员开通', ['sms_ready' => 0], 2010); + if ($password === '') { + $this->error('请填写登录密码'); } - $user = \app\common\model\User::getByMobile($mobile); - if ($user) { + // 直接查询真实数据库 fa_user 表 + $user = Db::name('user')->where('mobile', $mobile)->find(); + + if (!$user) { + // 若数据库中无此手机号:直连数据库插入注册新会员 + $ret = $this->auth->register($mobile, $password, '', $mobile, []); + if (!$ret) { + $this->error($this->auth->getError() ?: '注册登录失败'); + } + } else { if ($user['status'] !== 'normal') { $this->error('账号已被禁用'); } - $this->auth->direct($user['id']); - } else { - $ret = $this->auth->register($mobile, Random::alnum(), '', $mobile, []); - if (!$ret) { - $this->error($this->auth->getError() ?: '注册失败'); + if (empty($user['password'])) { + $this->error('该账号尚未设置登录密码,请使用微信一键登录后设置密码'); } + $encryptPassword = $this->auth->getEncryptPassword($password, $user['salt']); + if ($user['password'] !== $encryptPassword) { + $this->error('手机号或密码不正确'); + } + $this->auth->direct($user['id']); } + // 直查真实数据库返回用户完整信息 $this->success('登录成功', [ 'token' => $this->auth->getToken(), 'expires' => 2592000, @@ -357,17 +323,20 @@ class Auth extends Base $this->error('解析手机号失败'); } - // 2. 如果传了 login_code,换取 openid + // 2. 如果传了 login_code,换取 openid;失败时严禁静默忽略,需如实报错防止后续支付/转账漏绑 OpenID $openid = ''; $sessionKey = ''; $unionid = ''; if ($loginCode !== '') { $wx = \app\common\library\WxLogin::jscode2session($loginCode); - if (!isset($wx['error']) && !empty($wx['openid'])) { - $openid = $wx['openid']; - $sessionKey = isset($wx['session_key']) ? $wx['session_key'] : ''; - $unionid = isset($wx['unionid']) ? $wx['unionid'] : ''; + if (isset($wx['error']) || empty($wx['openid'])) { + $errMsg = isset($wx['error']) && $wx['error'] !== '' ? (string)$wx['error'] : '微信登录凭证换取 OpenID 失败,请重进小程序再试'; + \think\Log::error('[Auth::wxPhoneLogin] jscode2session 失败: ' . $errMsg); + $this->error($errMsg, ['wx_errcode' => isset($wx['errcode']) ? (int)$wx['errcode'] : 0]); } + $openid = $wx['openid']; + $sessionKey = isset($wx['session_key']) ? $wx['session_key'] : ''; + $unionid = isset($wx['unionid']) ? $wx['unionid'] : ''; } // 3. 落库与关联登录 diff --git a/server/application/api/controller/Base.php b/server/application/api/controller/Base.php index ee8ecc5..07f571a 100644 --- a/server/application/api/controller/Base.php +++ b/server/application/api/controller/Base.php @@ -276,9 +276,9 @@ class Base extends Api } } - // 师傅脱敏称呼与评分 - $masterName = '优匠师傅'; - $masterScore = 5.00; + // 师傅脱敏称呼与真实评分 + $masterName = ''; + $masterScore = 0.00; if ((int)$order['master_id'] > 0) { $m = Db::name('master')->where('id', (int)$order['master_id'])->find(); if ($m) { @@ -288,13 +288,13 @@ class Base extends Api } elseif ($rName !== '') { $masterName = $rName; } - $masterScore = (float)$m['score'] > 0 ? round((float)$m['score'], 2) : 5.00; + $masterScore = round((float)$m['score'], 2); } } - $score = $rev ? max(1, min(5, (int)$rev['score'])) : 5; - $tags = ($rev && !empty($rev['tags'])) ? (string)$rev['tags'] : '准时,专业,值得推荐'; - $content = ($rev && !empty($rev['content'])) ? (string)$rev['content'] : '师傅准时上门,施工专业细致,安装效果很满意!'; + $score = $rev ? max(1, min(5, (int)$rev['score'])) : 0; + $tags = ($rev && !empty($rev['tags'])) ? (string)$rev['tags'] : ''; + $content = ($rev && !empty($rev['content'])) ? (string)$rev['content'] : ''; $payload = [ 'order_id' => $orderId, @@ -302,9 +302,9 @@ class Base extends Api 'user_name' => $userName, 'user_avatar' => $userAvatar, 'cat_id' => (int)$order['cat_id'], - 'cat_name' => $cat ? (string)$cat['name'] : '上门服务', + 'cat_name' => $cat ? (string)$cat['name'] : '', 'service_id' => (int)$order['service_id'], - 'service_name' => $order['service_name'] ?: ($service ? (string)$service['name'] : '上门服务'), + 'service_name' => $order['service_name'] ?: ($service ? (string)$service['name'] : ''), 'region' => $region, 'cover_image' => $cover, 'images' => !empty($imgList) ? json_encode(array_values($imgList), JSON_UNESCAPED_UNICODE) : '', diff --git a/server/application/api/controller/Common.php b/server/application/api/controller/Common.php index c644f07..7cbb095 100755 --- a/server/application/api/controller/Common.php +++ b/server/application/api/controller/Common.php @@ -10,6 +10,7 @@ use app\common\model\Version; use fast\Random; use think\captcha\Captcha; use think\Config; +use think\Db; use think\Hook; /** @@ -17,7 +18,7 @@ use think\Hook; */ class Common extends Api { - protected $noNeedLogin = ['init', 'captcha']; + protected $noNeedLogin = ['init', 'captcha', 'areas', 'expressCompanies', 'dicts']; protected $noNeedRight = '*'; public function _initialize() @@ -146,4 +147,166 @@ class Common extends Api $captcha = new Captcha((array)Config::get('captcha')); return $captcha->entry($id); } + + /** + * 行政区划真实数据库查询接口(源自 fa_area) + * - mode=tree: 返回完整省-市-区三级联树 + * - pid: 按父级ID逐级查询下级行政区 + */ + public function areas() + { + $mode = trim((string)$this->request->param('mode', '')); + if ($mode === 'tree') { + $rows = Db::name('area') + ->field('id,pid,name,shortname,level') + ->where('level', 'in', [1, 2, 3]) + ->order('id asc') + ->select(); + if (empty($rows)) { + $this->error('行政区划基础数据尚未初始化'); + } + + $provinces = []; + $citiesByPid = []; + $districtsByPid = []; + foreach ($rows as $row) { + $lvl = (int)$row['level']; + $pid = (int)$row['pid']; + $id = (int)$row['id']; + if ($lvl === 1) { + $provinces[] = [ + 'id' => $id, + 'name' => (string)$row['name'], + 'cities' => [], + ]; + } elseif ($lvl === 2) { + if (!isset($citiesByPid[$pid])) { + $citiesByPid[$pid] = []; + } + $citiesByPid[$pid][] = [ + 'id' => $id, + 'name' => (string)$row['name'], + 'districts' => [], + ]; + } elseif ($lvl === 3) { + if (!isset($districtsByPid[$pid])) { + $districtsByPid[$pid] = []; + } + $districtsByPid[$pid][] = (string)$row['name']; + } + } + + foreach ($provinces as &$prov) { + $cities = isset($citiesByPid[$prov['id']]) ? $citiesByPid[$prov['id']] : []; + foreach ($cities as &$city) { + $city['districts'] = isset($districtsByPid[$city['id']]) ? $districtsByPid[$city['id']] : []; + } + unset($city); + $prov['cities'] = $cities; + } + unset($prov); + + $this->success('', ['tree' => $provinces]); + } + + $pid = (int)$this->request->param('pid', 0); + $list = Db::name('area') + ->field('id,pid,name,shortname,mergename,level') + ->where('pid', $pid) + ->order('id asc') + ->select(); + $this->success('', ['list' => $list]); + } + + /** + * 物流公司真实字典(源自 fa_express_company,按首字母分组) + */ + public function expressCompanies() + { + $rows = Db::name('express_company') + ->field('id,code,name,initial') + ->where('status', 'normal') + ->order('initial asc, weigh desc, id asc') + ->select(); + + $groupMap = []; + foreach ($rows as $r) { + $letter = strtoupper(trim((string)$r['initial'])) ?: 'Q'; + if (!isset($groupMap[$letter])) { + $groupMap[$letter] = []; + } + $groupMap[$letter][] = (string)$r['name']; + } + $groups = []; + foreach ($groupMap as $letter => $items) { + $groups[] = [ + 'letter' => $letter, + 'items' => $items, + ]; + } + // 将“Q 其它”组调整到末尾 + usort($groups, function ($a, $b) { + $isQa = ($a['letter'] === 'Q'); + $isQb = ($b['letter'] === 'Q'); + if ($isQa !== $isQb) { + return $isQa ? 1 : -1; + } + return strcmp($a['letter'], $b['letter']); + }); + + $this->success('', [ + 'groups' => $groups, + 'list' => $rows, + ]); + } + + /** + * 平台通用业务字典接口(真实表与系统配置驱动) + */ + public function dicts() + { + $parseCsv = static function ($name) { + $val = Db::name('config')->where('name', $name)->value('value'); + if ($val === null || trim((string)$val) === '') { + $val = Config::get('site.' . $name); + } + if (!$val) { + return []; + } + return array_values(array_filter(array_map('trim', explode(',', (string)$val)))); + }; + + $reviewTags = $parseCsv('review_tags'); + $orderSources = $parseCsv('order_source_options'); + $rawQuoteHours = $parseCsv('auto_quote_hours_options'); + $autoQuoteHours = []; + foreach ($rawQuoteHours as $h) { + $hi = (int)$h; + if ($hi > 0) { + $autoQuoteHours[] = $hi; + } + } + + // 师傅资质要求列表直接读取真实证书字典表 fa_cert_dict + $certRows = Db::name('cert_dict') + ->field('id,cert_code,cert_name,sample_type') + ->where('status', 'normal') + ->where('cert_code', '<>', 'other') + ->order('weigh desc, id asc') + ->select(); + $qualReqs = []; + foreach ($certRows as $cr) { + if (!empty($cr['cert_name'])) { + $qualReqs[] = (string)$cr['cert_name']; + } + } + + $this->success('', [ + 'review_tags' => $reviewTags, + 'order_sources' => $orderSources, + 'auto_quote_hours' => $autoQuoteHours, + 'qual_reqs' => $qualReqs, + 'cert_dict' => $certRows, + ]); + } } diff --git a/server/application/api/controller/Home.php b/server/application/api/controller/Home.php index 276c6d5..8635f11 100644 --- a/server/application/api/controller/Home.php +++ b/server/application/api/controller/Home.php @@ -9,7 +9,7 @@ use think\Db; */ class Home extends Base { - protected $noNeedLogin = ['index', 'feed', 'cats', 'services', 'detail', 'calc', 'specs', 'masterCount', 'orderCase', 'agreement', 'systemGallery']; + protected $noNeedLogin = ['index', 'feed', 'cats', 'services', 'detail', 'calc', 'specs', 'masterCount', 'orderCase', 'agreement', 'agreementRead', 'systemGallery']; /** * 首页聚合:类目 + 热门服务 + 我的角标 @@ -151,15 +151,69 @@ class Home extends Base ]; } + // 聚合在售服务的真实计价单位列表(彻底由数据库驱动,按商品数量降序) + $unitRows = Db::name('service') + ->field('unit, COUNT(*) AS cnt, MIN(id) AS min_id') + ->where('status', 'normal') + ->where('cat_id', '<>', 15) + ->where('unit', '<>', '') + ->group('unit') + ->order('cnt desc, min_id asc') + ->select(); + $filterUnits = []; + foreach ($unitRows as $ur) { + $uName = trim((string)($ur['unit'] ?? '')); + if ($uName !== '') { + $filterUnits[] = [ + 'label' => '按' . $uName . '计价', + 'value' => $uName, + ]; + } + } + + // 从 fa_config 读取价格筛选项 JSON 与晒单评价标签配置 + $priceRangesRaw = Db::name('config')->where('name', 'home_price_ranges')->value('value'); + if ($priceRangesRaw === null || trim((string)$priceRangesRaw) === '') { + $priceRangesRaw = \think\Config::get('site.home_price_ranges'); + } + $filterPriceRanges = []; + if (is_string($priceRangesRaw) && trim($priceRangesRaw) !== '') { + $decodedPr = json_decode(htmlspecialchars_decode(trim($priceRangesRaw)), true); + if (is_array($decodedPr)) { + foreach ($decodedPr as $prItem) { + if (is_array($prItem) && !empty($prItem['label']) && !empty($prItem['value'])) { + $filterPriceRanges[] = [ + 'label' => (string)$prItem['label'], + 'value' => (string)$prItem['value'], + ]; + } + } + } + } + + $tagCfg = $this->configList('review_tags'); + $filterCaseTags = []; + foreach ($tagCfg as $tc) { + $filterCaseTags[] = [ + 'label' => $tc, + 'value' => $tc, + ]; + } + $data = [ 'cats' => $cats, 'cat_services' => $catServices, 'showcase_services' => $showcaseServices, 'service_types_dict' => $serviceTypesDict, 'regions' => $regions, + 'filter_options' => [ + 'units' => $filterUnits, + 'price_ranges' => $filterPriceRanges, + 'case_tags' => $filterCaseTags, + ], 'hot' => $this->formatServices($hotList), 'banners' => $this->banners(), - 'assures' => $this->configList('home_assure', '平台认证,先行赔付,极速上门,售后无忧'), + 'assures' => $this->configList('home_assure'), 'tutorial' => $this->tutorial(), 'service_phone' => (string)(\think\Config::get('site.service_phone') ?: ''), 'badge' => ['coupon' => 0, 'unread' => 0], @@ -190,16 +244,16 @@ class Home extends Base $recentOrders[] = [ 'id' => (int)$sc['id'], 'service_id' => (int)$sc['service_id'], - 'service_name' => $sc['service_name'] ?: '上门服务', - 'cat_name' => $sc['cat_name'] ?: '服务', + 'service_name' => (string)$sc['service_name'], + 'cat_name' => (string)$sc['cat_name'], 'cat_id' => (int)$sc['cat_id'], 'image_url' => $sc['cover_image'] ? cdnurl($sc['cover_image'], true) : '', 'status_text' => '业主晒单', - 'location' => $sc['region'] ?: '同城服务', + 'location' => (string)$sc['region'], 'time_ago' => $this->timeAgo((int)$sc['createtime']), - 'user_name' => $sc['user_name'] ?: '匿名业主', - 'review_score' => (int)$sc['score'] ?: 5, - 'review_content' => $sc['content'] ?: '师傅准时上门,施工专业细致,安装效果很满意!', + 'user_name' => (string)$sc['user_name'], + 'review_score' => (int)$sc['score'], + 'review_content' => (string)$sc['content'], 'review_tags' => array_slice($revTags, 0, 2), ]; } @@ -299,16 +353,16 @@ class Home extends Base $list[] = [ 'id' => (int)$sc['id'], 'service_id' => (int)$sc['service_id'], - 'service_name' => $sc['service_name'] ?: '上门服务', - 'cat_name' => $sc['cat_name'] ?: '服务', + 'service_name' => (string)$sc['service_name'], + 'cat_name' => (string)$sc['cat_name'], 'cat_id' => (int)$sc['cat_id'], 'image_url' => $sc['cover_image'] ? cdnurl($sc['cover_image'], true) : '', 'status_text' => '业主晒单', - 'location' => $sc['region'] ?: '同城服务', + 'location' => (string)$sc['region'], 'time_ago' => $this->timeAgo((int)$sc['createtime']), - 'user_name' => $sc['user_name'] ?: '匿名业主', - 'review_score' => (int)$sc['score'] ?: 5, - 'review_content' => $sc['content'] ?: '师傅准时上门,施工专业细致,安装效果很满意!', + 'user_name' => (string)$sc['user_name'], + 'review_score' => (int)$sc['score'], + 'review_content' => (string)$sc['content'], 'review_tags' => array_slice($revTags, 0, 3), ]; } @@ -970,8 +1024,8 @@ class Home extends Base 'discount_amount' => round($discount, 2), 'pay_amount' => max(0, round($total - $discount, 2)), 'coupon' => $couponInfo, - 'free_floor' => $type === 'custom' ? 0 : ($service ? (int)$service['free_floor'] : 2), - 'floor_fee' => $type === 'custom' ? 0 : round($service ? (float)$service['floor_fee'] : 10.0, 2), + 'free_floor' => $type === 'custom' ? 0 : ($service ? (int)$service['free_floor'] : 0), + 'floor_fee' => $type === 'custom' ? 0 : round($service ? (float)$service['floor_fee'] : 0.0, 2), ]; } @@ -1048,13 +1102,16 @@ class Home extends Base } /** - * 读取 fa_config 中的逗号分隔列表 + * 读取 fa_config 中的逗号分隔列表(完全由真实库配置与站点配置驱动) */ - protected function configList($name, $default = '') + protected function configList($name) { $v = Db::name('config')->where('name', $name)->value('value'); + if ($v === null || trim((string)$v) === '') { + $v = \think\Config::get('site.' . $name); + } if (!$v) { - $v = $default; + return []; } return array_values(array_filter(array_map('trim', explode(',', (string)$v)))); } @@ -1065,8 +1122,11 @@ class Home extends Base protected function tutorial() { $v = Db::name('config')->where('name', 'home_tutorial')->value('value'); + if ($v === null || trim((string)$v) === '') { + $v = \think\Config::get('site.home_tutorial'); + } if (!$v) { - return ['title' => '下单找师傅教程', 'sub' => '3 分钟看懂,下单不踩坑']; + return ['title' => '', 'sub' => '']; } $parts = explode('|', (string)$v); return [ @@ -1274,7 +1334,7 @@ class Home extends Base } $service = Db::name('service')->where('id', (int)$sc['service_id'])->find(); - $basePrice = ($service && (float)$service['base_price'] > 0) ? (float)$service['base_price'] : 50.00; + $basePrice = $service ? (float)$service['base_price'] : 0.00; $imageUrl = $sc['cover_image'] ? cdnurl($sc['cover_image'], true) : ''; $tags = []; @@ -1303,9 +1363,9 @@ class Home extends Base } } - $dateText = !empty($sc['createtime']) ? date('Y-m-d', (int)$sc['createtime']) : date('Y-m-d'); - $catName = $sc['cat_name'] ?: '上门服务'; - $serviceName = $sc['service_name'] ?: '上门服务'; + $dateText = !empty($sc['createtime']) ? date('Y-m-d', (int)$sc['createtime']) : ''; + $catName = (string)$sc['cat_name']; + $serviceName = (string)$sc['service_name']; $this->success('', [ 'order' => [ @@ -1316,21 +1376,21 @@ class Home extends Base 'cat_name' => $catName, 'image_url' => $imageUrl, 'status_text' => '业主晒单', - 'location' => $sc['region'] ?: '同城服务', + 'location' => (string)$sc['region'], 'finish_text' => $dateText, 'media' => $mediaList, ], 'master' => [ - 'realname' => $sc['master_name'] ?: '优匠师傅', - 'score' => (float)($sc['master_score'] > 0 ? $sc['master_score'] : 5.0), + 'realname' => (string)$sc['master_name'], + 'score' => (float)$sc['master_score'], ], 'review' => [ 'id' => (int)$sc['id'], - 'score' => (int)$sc['score'] ?: 5, + 'score' => (int)$sc['score'], 'tags' => $tags, - 'content' => $sc['content'] ?: '师傅很专业,准时上门,施工规范细致,验收很满意!', + 'content' => (string)$sc['content'], 'images' => $rImgs, - 'user_name' => $sc['user_name'] ?: '匿名业主', + 'user_name' => (string)$sc['user_name'], 'user_avatar' => $sc['user_avatar'] ? cdnurl($sc['user_avatar'], true) : '', 'time_text' => $dateText, ], @@ -1346,52 +1406,35 @@ class Home extends Base } /** - * 平台协议与合规条款读取(支持后台站点配置覆盖) + * 平台协议与合规条款读取(优先读取后台「协议与隐私」实时配置,结合用户已读修订号返回首看变更标识) */ public function agreement() { $type = trim((string)$this->request->param('type', 'user_service')); - $map = [ - 'service' => [ - 'key' => 'agreement_service', - 'title' => '优匠优服用户服务协议', - 'content' => "1. 平台定位:优匠优服为客户与认证技能师傅提供上门安装、维修、拆旧及维保信息撮合与交易担保服务。\n\n2. 计费准则:一口价项目按公示标准计费;报价订单由师傅根据现场工况公开报价,经客户确认支付后生效。\n\n3. 验收与结算:订单资金由平台托管,师傅完工拍照并经客户确认(或提供6位核销码)后完成结算。\n\n4. 交易规范:为保障售后质保与资金安全,请勿脱离平台私下转账交易。", - ], - 'user_service' => [ - 'key' => 'agreement_service', - 'title' => '优匠优服用户服务协议', - 'content' => "1. 平台定位:优匠优服为客户与认证技能师傅提供上门安装、维修、拆旧及维保信息撮合与交易担保服务。\n\n2. 计费准则:一口价项目按公示标准计费;报价订单由师傅根据现场工况公开报价,经客户确认支付后生效。\n\n3. 验收与结算:订单资金由平台托管,师傅完工拍照并经客户确认(或提供6位核销码)后完成结算。\n\n4. 交易规范:为保障售后质保与资金安全,请勿脱离平台私下转账交易。", - ], - 'privacy' => [ - 'key' => 'agreement_privacy', - 'title' => '优匠优服隐私保护指引', - 'content' => "1. 信息收集范围:仅在您授权登录、预约上门地址及联系师傅等必要场景下收集手机号码与服务地址信息。\n\n2. 最小必要原则:客户真实联系信息仅对已接单履约的当单认证师傅可见,未接单前全程脱敏展示。\n\n3. 数据传输安全:全链路采用加密通信并严格管控访问权限,绝不向无关第三方共享或出售个人信息。\n\n4. 用户权利:您可随时在个人中心查询、修改账户资料及管理服务地址。", - ], - 'master' => [ - 'key' => 'agreement_master', - 'title' => '师傅入驻与履约服务协议', - 'content' => "1. 实名与技能核验:入驻师傅须提交本人真实有效身份证件及服务技能信息,严禁冒用他人资质。\n\n2. 履约时效:接单后须在平台规定时效内联系客户确认上门时间,准时到达现场并规范施工。\n\n3. 收费透明:如需现场增项补差,必须提前向客户说明并在工单内发起补差申请,严禁私自索要额外费用。\n\n4. 完工留痕与结算:施工完成后须上传真实清晰的现场完工照片,凭客户验收或核销码触发系统结算。", - ], - 'merchant' => [ - 'key' => 'agreement_merchant', - 'title' => '企业商家入驻与协同服务协议', - 'content' => "1. 资质真实性:申请入驻的企业或商户须提供真实有效的营业执照与主体信息。\n\n2. 成员协同管理:主账号可绑定企业协同成员共同发起批量工单与跟进履约,并对企业关联工单承担管理责任。\n\n3. 对账与开票:平台按实际支付与完工订单汇总企业月度账单,支持在线申请正规增值税普通发票或专用发票。", - ], - ]; + $item = \app\common\library\AgreementService::getByType($type, $this->uid()); + if (!$item) { + $this->error('协议内容暂未配置或已下线'); + } - $item = isset($map[$type]) ? $map[$type] : $map['user_service']; - $cfgVal = \think\Config::get('site.' . $item['key']); - $content = (is_string($cfgVal) && trim($cfgVal) !== '') ? trim($cfgVal) : $item['content']; - - $this->success('', [ - 'type' => $type, - 'title' => $item['title'], - 'content' => $content, - ]); + $this->success('', $item); } /** - * 官方系统图库(二级商品分类侧边栏 + 款式参考图片双列网格) + * 记录用户已首次查看指定修订版本的协议(后续打开不再标背景色) + */ + public function agreementRead() + { + if (false === $this->request->isPost()) { + $this->error('请求方式错误'); + } + $type = trim((string)$this->request->post('type', 'privacy')); + $revision = (int)$this->request->post('revision', 0); + $res = \app\common\library\AgreementService::markUserRead($this->uid(), $type, $revision); + $this->success('', $res); + } + + /** + * 官方系统图库(受下单所选类目控制,仅展示对应类目下的商品及参考图片;同源聚合系统图库表 + 细分规格图 + 商品主缩略图) */ public function systemGallery() { @@ -1417,6 +1460,14 @@ class Home extends Base $catId = (int)$cats[0]['id']; } + $currentCatName = ''; + foreach ($cats as $c) { + if ((int)$c['id'] === $catId) { + $currentCatName = (string)$c['name']; + break; + } + } + $svcQuery = Db::name('service') ->field('id,cat_id,name,image') ->where('status', 'normal') @@ -1428,6 +1479,7 @@ class Home extends Base $svcIds = array_column($svcRows, 'id'); $galleryMap = []; + $specMap = []; if (!empty($svcIds)) { $gQuery = Db::name('system_gallery') ->where('service_id', 'in', $svcIds) @@ -1450,34 +1502,96 @@ class Home extends Base 'image_url' => strpos($rawImg, 'http') === 0 ? $rawImg : cdnurl($rawImg, true), ]; } + + // 一次性批量查询所有关联商品的细分规格配图(严禁循环内查库) + $spRows = Db::name('service_spec') + ->field('id,service_id,name,image') + ->where('service_id', 'in', $svcIds) + ->where('status', 'normal') + ->order('weigh desc,id asc') + ->select(); + foreach ($spRows as $sp) { + $sid = (int)$sp['service_id']; + $specMap[$sid][] = $sp; + } } - // 若个别服务尚未写入 fa_system_gallery,回退读取真实规格与服务主图 + // 智能同源聚合:1. 系统图库表记录 -> 2. 细分规格独立配图 -> 3. 商品后台主缩略图兜底(单张不重复) $services = []; foreach ($svcRows as $sr) { $sid = (int)$sr['id']; - $items = isset($galleryMap[$sid]) ? $galleryMap[$sid] : []; + $items = []; + $seenImgUrls = []; + $seenBasenames = []; + static $isValidGalleryUrl = null; + if ($isValidGalleryUrl === null) { + $isValidGalleryUrl = static function (string $url): bool { + $base = strstr($url, '?', true); + $clean = strtolower(trim($base !== false ? $base : $url)); + if ($clean === '' || substr($clean, -4) === '.svg' || strpos($clean, '/icon/') !== false) { + return false; + } + return true; + }; + } + $registerUniqueUrl = static function (string $url) use (&$seenImgUrls, &$seenBasenames, $isValidGalleryUrl): ?string { + $u = trim($url); + if ($u === '' || !$isValidGalleryUrl($u)) { + return null; + } + $pathOnly = parse_url($u, PHP_URL_PATH); + $fname = $pathOnly ? strtolower(basename($pathOnly)) : strtolower($u); + if (isset($seenImgUrls[$u]) || ($fname !== '' && isset($seenBasenames[$fname]))) { + return null; + } + $seenImgUrls[$u] = true; + if ($fname !== '') { + $seenBasenames[$fname] = true; + } + return $u; + }; + + if (!empty($galleryMap[$sid])) { + foreach ($galleryMap[$sid] as $gItem) { + $u = $registerUniqueUrl((string)($gItem['image_url'] ?? '')); + if ($u === null) { + continue; + } + $gItem['image_url'] = $u; + $items[] = $gItem; + } + } + + // 仅当系统图库未单独配置该商品图集时,回退读取商品细分规格参考图 + if (empty($items) && !empty($specMap[$sid])) { + foreach ($specMap[$sid] as $sp) { + $spImg = !empty($sp['image']) ? trim((string)$sp['image']) : ''; + if ($spImg === '' || !$isValidGalleryUrl($spImg)) { + continue; + } + if ($keyword !== '' && mb_strpos((string)$sp['name'], $keyword) === false) { + continue; + } + $fullSpUrl = strpos($spImg, 'http') === 0 ? $spImg : cdnurl($spImg, true); + $u = $registerUniqueUrl($fullSpUrl); + if ($u !== null) { + $items[] = [ + 'id' => 100000 + (int)$sp['id'], + 'service_id' => $sid, + 'spec_id' => (int)$sp['id'], + 'title' => (string)$sp['name'], + 'image_url' => $u, + ]; + } + } + } + + // 若后台仅在添加商品时上传了1张商品缩略图(未配额外图库与规格图),直接展示该商品主图 if (empty($items) && $keyword === '') { $defaultImg = trim((string)$sr['image']); - if ($defaultImg !== '') { + if ($defaultImg !== '' && $isValidGalleryUrl($defaultImg)) { $fullDef = strpos($defaultImg, 'http') === 0 ? $defaultImg : cdnurl($defaultImg, true); - $spList = Db::name('service_spec') - ->where('service_id', $sid) - ->where('status', 'normal') - ->order('weigh desc,id asc') - ->select(); - if (!empty($spList)) { - foreach ($spList as $sp) { - $spImg = !empty($sp['image']) ? trim((string)$sp['image']) : ''; - $items[] = [ - 'id' => 100000 + (int)$sp['id'], - 'service_id' => $sid, - 'spec_id' => (int)$sp['id'], - 'title' => (string)$sp['name'], - 'image_url' => $spImg !== '' ? (strpos($spImg, 'http') === 0 ? $spImg : cdnurl($spImg, true)) : $fullDef, - ]; - } - } else { + if ($isValidGalleryUrl($fullDef)) { $items[] = [ 'id' => 200000 + $sid, 'service_id' => $sid, @@ -1488,6 +1602,7 @@ class Home extends Base } } } + if ($keyword !== '' && empty($items) && mb_strpos((string)$sr['name'], $keyword) === false) { continue; } @@ -1503,9 +1618,10 @@ class Home extends Base } $this->success('', [ - 'cats' => $cats, - 'current_cat_id' => $catId, - 'services' => $services, + 'cats' => $cats, + 'current_cat_id' => $catId, + 'current_cat_name' => $currentCatName, + 'services' => $services, ]); } } diff --git a/server/application/api/controller/Master.php b/server/application/api/controller/Master.php index 5819c86..8395f88 100644 --- a/server/application/api/controller/Master.php +++ b/server/application/api/controller/Master.php @@ -2,9 +2,13 @@ namespace app\api\controller; +use app\common\library\CommissionService; +use app\common\library\FinanceConfigService; use app\common\library\OrderStatus; +use app\common\library\PaymentService; use app\common\library\TiandituService; -use app\common\library\SmsService; +use app\common\library\TencentVerifyService; +use app\common\library\WxPayV3Service; use think\Db; /** @@ -74,6 +78,9 @@ class Master extends Base if ($realname === '' || $mobile === '' || $rawIdcard === '') { $this->error('请填写真实姓名、手机号和身份证号'); } + if ($idcardFront === '' || $idcardBack === '') { + $this->error('请上传身份证人像面和国徽面照片'); + } if (!preg_match('/^1\d{10}$/', $mobile)) { $this->error('手机号格式不正确'); } @@ -90,20 +97,41 @@ class Master extends Base } // 若是驳回后重提且未修改脱敏展示号,沿用原存储的完整身份证号;否则校验完整身份证号并真实入库 + $needVerify = true; if (strpos($rawIdcard, '*') !== false) { if ($exist && !empty($exist['idcard_no']) && strpos((string)$exist['idcard_no'], '*') === false) { $finalIdcard = (string)$exist['idcard_no']; + $needVerify = false; } else { $this->error('请填写真实完整的18位身份证号码'); return; } } else { - if (!preg_match('/^(\d{15}|\d{17}[0-9X])$/', $rawIdcard)) { - $this->error('身份证号码格式不正确'); + if (!preg_match('/^[1-9]\d{5}(18|19|20)\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])\d{3}[\dX]$/', $rawIdcard)) { + $this->error('身份证号码格式不正确,请输入18位有效居民身份证'); } $finalIdcard = $rawIdcard; } + // 若前端携带了有效期限参数,进行过期拦截校验 + $rawValidDate = trim((string)$this->request->post('valid_date', '')); + if (!empty($rawValidDate)) { + $expireChk = TencentVerifyService::checkIdCardExpired($rawValidDate); + if (!empty($expireChk['is_expired'])) { + $this->error('该身份证已过有效期限(截止 ' . ($expireChk['expire_date'] ?: $rawValidDate) . '),无法通过实名认证'); + return; + } + } + + // 腾讯云公安权威二要素实名核验(姓名 + 身份证号) + if ($needVerify) { + $verifyRes = TencentVerifyService::verifyIdCard($realname, $finalIdcard); + if (!$verifyRes['pass']) { + $this->error('实名核验未通过:' . $verifyRes['msg']); + return; + } + } + $catNameMap = Db::name('service_cat')->where('id', 'in', $skillCatIds)->column('name', 'id'); $save = [ @@ -114,8 +142,8 @@ class Master extends Base 'idcard_back' => $idcardBack, 'cert_images' => $certImages, 'city' => $normalizedCity, - 'verify_status' => 'pending', - 'audit_msg' => '', + 'verify_status' => 'pass', + 'audit_msg' => '腾讯云二要素实名核验通过', 'updatetime' => time(), ]; @@ -149,7 +177,36 @@ class Master extends Base $this->error('提交失败:' . $e->getMessage()); } - $this->success('提交成功,请等待审核', ['master_id' => (int)$masterId]); + $this->success('实名认证成功,已获得接单资格!', ['master_id' => (int)$masterId]); + } + + /** + * 身份证 OCR 智能文字识别 + * @ApiMethod (POST) + */ + public function ocrIdCard() + { + $image = trim((string)$this->request->post('image')); + $side = strtolower(trim((string)$this->request->post('side', 'front'))); + if (empty($image)) { + $this->error('请提供身份证照片地址'); + } + + $cardSide = ($side === 'back' || $side === 'reverse') ? 'BACK' : 'FRONT'; + + // 处理图片公网访问完整 URL + $targetImg = $image; + if (!preg_match('/^https?:\/\//i', $targetImg) && strpos($targetImg, 'data:image') !== 0) { + $cdnUrl = \think\Config::get('upload.cdnurl') ?: (\think\Config::get('site.cdnurl') ?: $this->request->domain()); + $targetImg = rtrim($cdnUrl, '/') . '/' . ltrim($targetImg, '/'); + } + + $res = TencentVerifyService::idCardOcr($targetImg, $cardSide); + if (!$res['success']) { + $this->error($res['msg']); + } + + $this->success('识别成功', $res['data']); } /** @@ -200,11 +257,16 @@ class Master extends Base 'mobile' => $master['mobile'], 'idcard_no' => $maskedIdcard, 'idcard' => $maskedIdcard, - 'idcard_front' => (string)($master['idcard_front'] ?? ''), - 'idcard_back' => (string)($master['idcard_back'] ?? ''), + 'idcard_front' => TencentVerifyService::getWatermarkUrl((string)($master['idcard_front'] ?? '')), + 'idcard_back' => TencentVerifyService::getWatermarkUrl((string)($master['idcard_back'] ?? '')), + 'idcard_front_raw' => (string)($master['idcard_front'] ?? ''), + 'idcard_back_raw' => (string)($master['idcard_back'] ?? ''), 'cert_images' => (string)($master['cert_images'] ?? ''), 'avatar' => $master['avatar'], 'city' => $master['city'], + 'lng' => (string)($master['lng'] ?? ''), + 'lat' => (string)($master['lat'] ?? ''), + 'serve_radius' => max(5, (int)($master['serve_radius'] ?? 15)), 'level' => (int)$master['level'], 'score' => (float)$master['score'], 'order_count' => (int)$master['order_count'], @@ -218,7 +280,8 @@ class Master extends Base 'todo_count' => (int)$todo, 'verify_count' => (int)$pendingVerify, ], - 'skills' => $skills, + 'skills' => $skills, + 'cert_summary' => $this->buildMasterCertSummary((int)$master['id']), ]); } @@ -238,18 +301,238 @@ class Master extends Base } /** - * 订单大厅:支持排序与单类型筛选(一口价 / 师傅报价 / 用户自主定价) + * 更新师傅当前位置(经纬度持久化与可选城市解析) + * @ApiMethod (POST) + */ + public function updateLocation() + { + $master = $this->requireMaster(); + $rawLat = $this->request->post('lat', ''); + $rawLng = $this->request->post('lng', ''); + if (!$this->isValidCoord($rawLat, $rawLng)) { + $this->error('定位坐标无效'); + } + + $lat = round((float)$rawLat, 6); + $lng = round((float)$rawLng, 6); + $inputCity = trim((string)$this->request->post('city', '')); + $inputDistrict = trim((string)$this->request->post('district', '')); + $serveRadiusRaw = $this->request->post('serve_radius', ''); + + $resolvedCity = $inputCity; + $resolvedDistrict = $inputDistrict; + $inputLabel = trim((string)$this->request->post('label', '')); + $resolvedPoi = ''; + $prevLat = (float)($master['lat'] ?? 0); + $prevLng = (float)($master['lng'] ?? 0); + $movedFar = !$this->isValidCoord($prevLat, $prevLng) + || abs($lat - $prevLat) > 0.05 + || abs($lng - $prevLng) > 0.05; + + // 事务外调用天地图逆地理编码识别真实省市区与地标(未传完整城市或发生位移时执行) + if ($resolvedCity === '' || $resolvedDistrict === '' || $movedFar) { + $geo = TiandituService::regeocoder($lat, $lng); + if (is_array($geo)) { + if ($resolvedCity === '') { + $resolvedCity = trim((string)($geo['city'] ?: ($geo['province'] ?? ''))); + } + if ($resolvedDistrict === '') { + $resolvedDistrict = trim((string)($geo['district'] ?? '')); + } + $resolvedPoi = trim((string)($geo['poi'] ?? '')); + } + } + + $locationLabel = $inputLabel !== '' + ? $inputLabel + : trim($resolvedCity . $resolvedDistrict . ($resolvedPoi !== '' ? ('·' . $resolvedPoi) : '')); + + $updateData = [ + 'lng' => (string)$lng, + 'lat' => (string)$lat, + 'updatetime' => time(), + ]; + if (trim((string)$master['city']) === '' && $resolvedCity !== '') { + $updateData['city'] = mb_substr($resolvedCity, 0, 50); + } + $serveRadius = (int)($master['serve_radius'] ?? 15); + if ($serveRadius <= 0) { + $serveRadius = 15; + } + if ($serveRadiusRaw !== '' && is_numeric($serveRadiusRaw)) { + $serveRadius = max(5, min(200, (int)$serveRadiusRaw)); + $updateData['serve_radius'] = $serveRadius; + } + + Db::name('master')->where('id', (int)$master['id'])->update($updateData); + + $this->success('定位已更新', [ + 'lat' => $lat, + 'lng' => $lng, + 'location_city' => $resolvedCity, + 'location_district' => $resolvedDistrict, + 'location_label' => $locationLabel, + 'master_city' => $updateData['city'] ?? (string)$master['city'], + 'serve_radius' => $serveRadius, + ]); + } + + /** + * 地点关键词检索(供定位受限或手动切换听单区域使用) + * @ApiMethod (GET) + */ + public function searchLocation() + { + $this->requireMaster(); + $keyword = trim((string)$this->request->param('keyword', '')); + if (mb_strlen($keyword) < 2) { + $this->success('', ['list' => []]); + } + $city = trim((string)$this->request->param('city', '')); + $rawLat = $this->request->param('lat', ''); + $rawLng = $this->request->param('lng', ''); + $nearLat = $this->isValidCoord($rawLat, $rawLng) ? (float)$rawLat : null; + $nearLng = $nearLat !== null ? (float)$rawLng : null; + + $list = TiandituService::searchPlaces(mb_substr($keyword, 0, 50), 15, $city, $nearLat, $nearLng); + $this->success('', ['list' => $list]); + } + + /** + * 独立位置选择页聚合上下文:坐标逆解析 + 天地图周边 POI + 服务半径内实时待接工单统计 + 真实活跃城市列表 + * @ApiMethod (GET) + */ + public function locationContext() + { + $master = $this->requireMaster(); + $rawLat = $this->request->param('lat', ''); + $rawLng = $this->request->param('lng', ''); + $category = trim((string)$this->request->param('category', 'all')); + if (!in_array($category, ['all', 'residential', 'office', 'plaza'], true)) { + $category = 'all'; + } + $radiusRaw = $this->request->param('serve_radius', ''); + $serveRadius = is_numeric($radiusRaw) + ? max(5, min(200, (int)$radiusRaw)) + : max(5, (int)($master['serve_radius'] ?? 15)); + + $hasCoord = $this->isValidCoord($rawLat, $rawLng); + if ($hasCoord) { + $lat = round((float)$rawLat, 6); + $lng = round((float)$rawLng, 6); + } elseif ($this->isValidCoord($master['lat'] ?? '', $master['lng'] ?? '')) { + $lat = round((float)$master['lat'], 6); + $lng = round((float)$master['lng'], 6); + $hasCoord = true; + } else { + $lat = 0.0; + $lng = 0.0; + } + + $center = null; + $nearbyList = []; + $activeCity = trim((string)$this->request->param('city', '')); + + if ($hasCoord) { + $nearby = TiandituService::nearbyPlaces($lat, $lng, $category, 3000, 15); + $center = $nearby['center']; + $nearbyList = $nearby['list']; + if ($activeCity === '' && is_array($center) && !empty($center['city'])) { + $activeCity = trim((string)$center['city']); + } + } elseif ($activeCity === '' && !empty($master['city'])) { + $activeCity = trim((string)$master['city']); + } + + $orderStats = $this->countLocationOrderStats((int)$master['id'], $hasCoord ? $lat : null, $hasCoord ? $lng : null, $serveRadius, $activeCity); + $activeCities = $this->getActiveHallCities((int)$master['id']); + + $this->success('', [ + 'has_coord' => $hasCoord, + 'lat' => $hasCoord ? $lat : '', + 'lng' => $hasCoord ? $lng : '', + 'serve_radius' => $serveRadius, + 'category' => $category, + 'center' => $center, + 'nearby_list' => $nearbyList, + 'order_stats' => $orderStats, + 'active_cities' => $activeCities, + 'master_city' => trim((string)($master['city'] ?? '')), + ]); + } + + /** + * 轻量位置工单预览统计(切换服务半径或选中地标时毫秒级计算,不额外请求外部地图) + * @ApiMethod (GET) + */ + public function locationOrderStats() + { + $master = $this->requireMaster(); + $rawLat = $this->request->param('lat', ''); + $rawLng = $this->request->param('lng', ''); + $city = trim((string)$this->request->param('city', '')); + $radiusRaw = $this->request->param('serve_radius', ''); + $serveRadius = is_numeric($radiusRaw) + ? max(5, min(200, (int)$radiusRaw)) + : max(5, (int)($master['serve_radius'] ?? 15)); + + $hasCoord = $this->isValidCoord($rawLat, $rawLng); + $lat = $hasCoord ? round((float)$rawLat, 6) : null; + $lng = $hasCoord ? round((float)$rawLng, 6) : null; + + $stats = $this->countLocationOrderStats((int)$master['id'], $lat, $lng, $serveRadius, $city); + $this->success('', $stats); + } + + /** + * 通过城市名解析中心坐标(用于位置页切换城市快捷飞跃定位) + * @ApiMethod (GET) + */ + public function locateCity() + { + $this->requireMaster(); + $city = trim((string)$this->request->param('city', '')); + if ($city === '') { + $this->error('请提供城市名称'); + } + $geo = TiandituService::geocoder($city); + if (!is_array($geo) || !$this->isValidCoord($geo['lat'] ?? '', $geo['lng'] ?? '')) { + $places = TiandituService::searchPlaces($city, 3); + if (!empty($places[0])) { + $geo = ['lat' => $places[0]['lat'], 'lng' => $places[0]['lng']]; + } + } + if (!is_array($geo) || !$this->isValidCoord($geo['lat'] ?? '', $geo['lng'] ?? '')) { + $this->error('未能解析该城市坐标,请尝试搜索具体区县'); + } + $lat = round((float)$geo['lat'], 6); + $lng = round((float)$geo['lng'], 6); + $regeo = TiandituService::regeocoder($lat, $lng); + + $this->success('', [ + 'lat' => $lat, + 'lng' => $lng, + 'city' => is_array($regeo) && !empty($regeo['city']) ? $regeo['city'] : $city, + 'district' => is_array($regeo) ? trim((string)($regeo['district'] ?? '')) : '', + 'address' => is_array($regeo) && !empty($regeo['formatted_address']) ? $regeo['formatted_address'] : $city, + ]); + } + + /** + * 订单大厅:支持基于定位的智能推荐、离我最近与多种排序及单类型筛选 */ public function hall() { $master = $this->requireMaster(); $this->recycleExpired(); - $catIds = Db::name('master_skill')->where('master_id', $master['id'])->where('cat_id', '>', 0)->column('cat_id'); - $catIds = array_values(array_filter(array_unique($catIds))); + $catIds = $this->getMasterSkillCatIds((int)$master['id']); $page = max(1, (int)$this->request->param('page', 1)); $limit = min(30, max(1, (int)$this->request->param('limit', 10))); - $sort = $this->request->param('sort', 'new'); // new 最新 / amount 报酬 / urgent 急单 / time 时效 + $sort = trim((string)$this->request->param('sort', 'recommend')); // recommend 智能推荐 / distance 离我最近 / new 最新 / amount 报酬 / urgent 急单 / time 时效 + if ($sort === '') { + $sort = 'recommend'; + } $type = $this->request->param('type', 'all'); // all / fixed 一口价 / quote 师傅报价 / custom 用户自主定价 $buildBase = function () use ($catIds) { @@ -280,29 +563,153 @@ class Master extends Base 'custom' => (int)$buildBase()->where('status', OrderStatus::PENDING_ACCEPT)->where('price_type', 'custom')->count(), ]; - if ($sort === 'amount') { - $order = 'pay_amount desc, id desc'; - } elseif ($sort === 'urgent') { - $order = 'urgent desc, id desc'; - } elseif ($sort === 'time') { - $order = 'appoint_time asc, id desc'; + $masterRecord = $this->masterInfo(); + $rawLat = $this->request->param('lat', ''); + $rawLng = $this->request->param('lng', ''); + $reqCity = trim((string)$this->request->param('city', '')); + $reqHasValidCoord = $this->isValidCoord($rawLat, $rawLng); + $dbHasValidCoord = $masterRecord && $this->isValidCoord($masterRecord['lat'] ?? '', $masterRecord['lng'] ?? ''); + $hasLocation = $reqHasValidCoord || $dbHasValidCoord; + + if ($reqHasValidCoord) { + $userLat = round((float)$rawLat, 6); + $userLng = round((float)$rawLng, 6); + // 当传入了实时有效坐标且与已保存位置有显著偏差时轻量同步至 fa_master(不阻塞发起外网请求) + $needSync = !$dbHasValidCoord + || abs($userLat - (float)$masterRecord['lat']) > 0.002 + || abs($userLng - (float)$masterRecord['lng']) > 0.002; + if ($needSync && $masterRecord) { + Db::name('master')->where('id', (int)$masterRecord['id'])->update([ + 'lng' => (string)$userLng, + 'lat' => (string)$userLat, + 'updatetime' => time(), + ]); + } + } elseif ($dbHasValidCoord) { + $userLat = (float)$masterRecord['lat']; + $userLng = (float)$masterRecord['lng']; } else { - $order = 'id desc'; + $userLat = 0.0; + $userLng = 0.0; } - $total = $build()->count(); - $list = $build()->order($order)->page($page, $limit)->select(); + $serveRadius = ($masterRecord && (int)$masterRecord['serve_radius'] > 0) ? (int)$masterRecord['serve_radius'] : 15; + $validCoordCond = $this->getValidOrderCoordSql(); + $distFormula = $this->getDistanceSqlFormula(); + + // 真实定位场景下的当前所在城市识别:优先使用实时传入城市;若未传入,根据 35km 内最近已知坐标订单推断,绝不拿跨城注册地误判 + $locationCity = ''; + if ($hasLocation) { + if ($reqCity !== '') { + $locationCity = $reqCity; + } else { + $nearestSql = Db::name('order') + ->where('city', '<>', '') + ->whereRaw($validCoordCond) + ->field("city, {$distFormula} AS _near_km") + ->buildSql(); + $nearestRow = Db::table($nearestSql . ' nc') + ->bind(['h_lat1' => $userLat, 'h_lng1' => $userLng, 'h_lat2' => $userLat]) + ->where('_near_km <= 35.0') + ->order('_near_km asc') + ->find(); + if ($nearestRow && !empty($nearestRow['city'])) { + $locationCity = trim((string)$nearestRow['city']); + } + } + } else { + $locationCity = $masterRecord ? trim((string)$masterRecord['city']) : ''; + } + $cityToken = $this->extractCityToken($locationCity); + + $total = (int)$build()->count(); + $selectColumns = 'id, order_no, user_id, cat_id, service_id, service_name, service_image, price_type, goods_images, goods_model, goods_num, floor, has_elevator, urgent, remark, contact, mobile, province, city, district, address, lng, lat, appoint_time, base_amount, floor_amount, distance_amount, urgent_amount, discount_amount, total_amount, pay_amount, commission_rate, commission_snapshot, status, createtime, auto_quote_timeout, order_source, qual_req, master_req, diff_status, diff_amount'; + + if (in_array($sort, ['recommend', 'distance'], true) && $hasLocation) { + $sameCityExpr = $cityToken !== '' ? "CASE WHEN city LIKE :h_city_like THEN 1 ELSE 0 END" : "0"; + + $subSql = $build() + ->field($selectColumns . ", CASE WHEN {$validCoordCond} THEN 1 ELSE 0 END AS _has_coord, CASE WHEN {$validCoordCond} THEN {$distFormula} ELSE 999999.0 END AS _dist_km, {$sameCityExpr} AS _same_city") + ->buildSql(); + + $outerBind = [ + 'h_lat1' => $userLat, + 'h_lng1' => $userLng, + 'h_lat2' => $userLat, + ]; + if ($cityToken !== '') { + $outerBind['h_city_like'] = '%' . $cityToken . '%'; + } + + $outerQuery = Db::table($subSql . ' o'); + if ($sort === 'distance') { + $outerQuery->bind($outerBind)->order('_has_coord desc, _dist_km asc, urgent desc, id desc'); + } else { + $outerBind['rec_radius'] = (float)$serveRadius; + $outerQuery->bind($outerBind)->orderRaw( + '(CASE WHEN _has_coord = 1 AND _dist_km <= :rec_radius THEN 1000.0 WHEN _has_coord = 1 AND _dist_km <= 50.0 THEN 500.0 WHEN _has_coord = 0 AND _same_city = 1 THEN 300.0 WHEN _has_coord = 1 THEN 100.0 ELSE 0.0 END + CASE WHEN _has_coord = 1 THEN (300.0 / (1.0 + _dist_km / 5.0)) ELSE 0.0 END + (urgent * 120.0)) DESC, id DESC' + ); + } + $list = $outerQuery->page($page, $limit)->select(); + } elseif (in_array($sort, ['recommend', 'distance'], true) && $cityToken !== '') { + $subSql = $build() + ->field($selectColumns . ', CASE WHEN city LIKE :h_city_like THEN 1 ELSE 0 END AS _same_city') + ->buildSql(); + $list = Db::table($subSql . ' o') + ->bind(['h_city_like' => '%' . $cityToken . '%']) + ->order('_same_city desc, urgent desc, id desc') + ->page($page, $limit) + ->select(); + } else { + if ($sort === 'amount') { + $order = 'pay_amount desc, id desc'; + } elseif ($sort === 'urgent') { + $order = 'urgent desc, id desc'; + } elseif ($sort === 'time') { + $order = 'appoint_time asc, id desc'; + } elseif (in_array($sort, ['recommend', 'distance'], true)) { + $order = 'urgent desc, id desc'; + } else { + $order = 'id desc'; + } + $list = $build()->field($selectColumns)->order($order)->page($page, $limit)->select(); + } + + // 批量预查询本页订单的报价数量与备用服务图标,消除循环内查询 + $orderIds = array_column($list, 'id'); + $quoteCountMap = []; + $serviceImgMap = []; + if (!empty($orderIds)) { + $quoteRows = Db::name('order_quote') + ->where('order_id', 'in', $orderIds) + ->field('order_id, COUNT(*) AS cnt') + ->group('order_id') + ->select(); + foreach ($quoteRows as $qr) { + $quoteCountMap[(int)$qr['order_id']] = (int)$qr['cnt']; + } + $missingImgServiceIds = []; + foreach ($list as $lr) { + if (empty($lr['service_image']) && !empty($lr['service_id'])) { + $missingImgServiceIds[] = (int)$lr['service_id']; + } + } + $missingImgServiceIds = array_values(array_unique($missingImgServiceIds)); + if (!empty($missingImgServiceIds)) { + $serviceImgMap = Db::name('service')->where('id', 'in', $missingImgServiceIds)->column('image', 'id'); + } + } - $masterRecord = $this->masterInfo(); - $defaultLat = ($masterRecord && !empty($masterRecord['lat'])) ? (float)$masterRecord['lat'] : 0.0; - $defaultLng = ($masterRecord && !empty($masterRecord['lng'])) ? (float)$masterRecord['lng'] : 0.0; - $userLat = (float)$this->request->param('lat', $defaultLat); - $userLng = (float)$this->request->param('lng', $defaultLng); $now = time(); foreach ($list as &$row) { - unset($row['verify_code']); + unset($row['verify_code'], $row['_has_coord'], $row['_dist_km'], $row['_same_city']); $row['appoint_text'] = $row['appoint_time'] ? date('Y-m-d H:i', (int)$row['appoint_time']) : '接单后与业主预约'; $row['pay_amount'] = (float)$row['pay_amount']; + $hallPayBase = (float)$row['pay_amount'] > 0 ? (float)$row['pay_amount'] : (float)($row['total_amount'] ?? 0); + $hallSettleCalc = CommissionService::calcByOrder($row, $hallPayBase); + $row['commission_rate'] = $hallSettleCalc['commission_rate']; + $row['commission'] = $hallSettleCalc['commission']; + $row['master_fee'] = $hallSettleCalc['settle_amount']; $row['urgent'] = (int)$row['urgent']; $row['mobile'] = substr_replace($row['mobile'], '****', 3, 4); @@ -318,7 +725,7 @@ class Master extends Base $row['status_text'] = OrderStatus::label($row['status']); $remain = (int)$row['createtime'] + 7 * 86400 - $now; $row['left_seconds'] = max(0, $remain); - $row['quote_count'] = (int)Db::name('order_quote')->where('order_id', $row['id'])->count(); + $row['quote_count'] = $quoteCountMap[(int)$row['id']] ?? 0; $row['remaining_quote_slots'] = max(0, 4 - $row['quote_count']); $row['price_type_text'] = $row['price_type'] === 'fixed' ? '一口价' : ($row['price_type'] === 'quote' ? '师傅报价' @@ -363,11 +770,8 @@ class Master extends Base $row['service_type_tag'] = (strpos((string)$row['service_name'], '修') !== false || strpos((string)$row['service_name'], '检') !== false) ? '维修' : '安装'; $img = !empty($row['service_image']) ? $row['service_image'] : $fallbackImg; - if (empty($img) && !empty($row['service_id'])) { - $s = Db::name('service')->where('id', (int)$row['service_id'])->value('image'); - if ($s) { - $img = $s; - } + if (empty($img) && !empty($row['service_id']) && !empty($serviceImgMap[(int)$row['service_id']])) { + $img = $serviceImgMap[(int)$row['service_id']]; } if (!empty($img)) { $row['service_image'] = strpos($img, 'http') === 0 ? $img : cdnurl($img, true); @@ -375,20 +779,42 @@ class Master extends Base $row['service_image'] = ''; } - $distKm = $this->calculateDistance($userLat, $userLng, (float)$row['lat'], (float)$row['lng']); + $distKm = $hasLocation ? $this->calculateDistance($userLat, $userLng, (float)$row['lat'], (float)$row['lng']) : null; $row['distance_km'] = $distKm; - $row['distance_text'] = $distKm !== null ? ($distKm . 'km') : ''; + $row['distance_text'] = $this->formatDistanceText($distKm); + $row['in_serve_radius'] = ($distKm !== null && $distKm <= $serveRadius) ? 1 : 0; + + $recommendReason = ''; + if ($hasLocation) { + if ($distKm !== null) { + if ($distKm <= 3.0) { + $recommendReason = '距您较近'; + } elseif ($distKm <= $serveRadius) { + $recommendReason = (int)$row['urgent'] === 1 ? '附近急单' : '服务圈内'; + } elseif ($distKm <= 50.0) { + $recommendReason = '同城订单'; + } + } elseif ($cityToken !== '' && mb_strpos((string)$row['city'], $cityToken) !== false) { + $recommendReason = '同城订单'; + } + } + $row['recommend_reason'] = $recommendReason; } unset($row); $this->success('', [ - 'list' => $list, - 'total' => (int)$total, - 'counts' => $counts, - 'page' => $page, - 'has_more' => ($page * $limit) < (int)$total, - 'sort' => $sort, - 'type' => $type, + 'list' => $list, + 'total' => $total, + 'counts' => $counts, + 'page' => $page, + 'has_more' => ($page * $limit) < $total, + 'sort' => $sort, + 'type' => $type, + 'has_location' => $hasLocation, + 'has_realtime_location' => $reqHasValidCoord, + 'location_source' => $reqHasValidCoord ? 'realtime' : ($dbHasValidCoord ? 'saved' : 'none'), + 'location_city' => $locationCity, + 'serve_radius' => $serveRadius, ]); } @@ -558,9 +984,6 @@ class Master extends Base } $code = (string)$order['verify_code']; - if (!empty($order['mobile'])) { - SmsService::sendVerifyCode($order['mobile'], $code, $order['order_no']); - } $this->notify('user', (int)$order['user_id'], '师傅已完工,待您验收', '验收码:' . $code . ',请核对后确认完成', 'order', (int)$order['id']); $this->success('已提交完工,等待客户验收'); } @@ -569,7 +992,7 @@ class Master extends Base * 我的订单(师傅视角) */ /** - * 师傅端:输入客户 6 位短信核销码完成工单验收结算 + * 师傅端:输入客户 6 位验收核销码完成工单验收结算 * @ApiMethod (POST) */ public function verifyCode() @@ -599,9 +1022,10 @@ class Master extends Base $masterId = (int)$master['id']; $payAmount = (float)$order['pay_amount']; - $rate = 10.00; - $commission = round($payAmount * $rate / 100, 2); - $settleAmount = round($payAmount - $commission, 2); + $settleCalc = CommissionService::calcByOrder($order); + $rate = $settleCalc['commission_rate']; + $commission = $settleCalc['commission']; + $settleAmount = $settleCalc['settle_amount']; $newMoney = 0.00; Db::startTrans(); @@ -611,10 +1035,12 @@ class Master extends Base throw new \InvalidArgumentException('该工单已核销完成或状态已变更,请勿重复提交'); } $payAmount = (float)$lockedOrder['pay_amount']; - $commission = round($payAmount * $rate / 100, 2); - $settleAmount = round($payAmount - $commission, 2); + $settleCalc = CommissionService::calcByOrder($lockedOrder); + $rate = $settleCalc['commission_rate']; + $commission = $settleCalc['commission']; + $settleAmount = $settleCalc['settle_amount']; - $this->transit($lockedOrder, OrderStatus::COMPLETED, '师傅已录入客户 6 位短信验证码,核销完工', [ + $this->transit($lockedOrder, OrderStatus::COMPLETED, '师傅已录入客户 6 位验收核销码,核销完工', [ 'verify_time' => time(), 'settle_status' => 1, 'settle_amount' => $settleAmount, @@ -647,7 +1073,7 @@ class Master extends Base 'amount' => $settleAmount, 'balance' => $newMoney, 'order_id' => $orderId, - 'remark' => '工单 ' . $lockedOrder['order_no'] . ' 短信验证码核销入账', + 'remark' => '工单 ' . $lockedOrder['order_no'] . ' 验收核销码结算入账', 'createtime' => time(), ]); @@ -732,26 +1158,79 @@ class Master extends Base } /** - * 收入概览 + * 收入概览(含待确认收款的微信商家转账单与提现申请记录) */ public function income() { $master = $this->requireMaster(); - $todayStart = strtotime(date('Y-m-d')); + + // 顺带轻量同步最新 3 笔处理中的微信转账提现单(如师傅上次已确认收款但未回调或超时自动退回) + $syncCandidates = Db::name('withdraw') + ->where('master_id', (int)$master['id']) + ->where('status', 'pass') + ->where('out_bill_no', '<>', '') + ->where('transfer_state', 'in', ['ACCEPTED', 'PROCESSING', 'WAIT_USER_CONFIRM', 'TRANSFERING', 'CANCELING']) + ->order('id desc') + ->limit(3) + ->select(); + $syncErrors = []; + if (!empty($syncCandidates)) { + foreach ($syncCandidates as $cand) { + try { + PaymentService::syncWithdrawTransfer((int)$cand['id']); + } catch (\Throwable $se) { + $errTxt = '提现单 #WD' . (int)$cand['id'] . ' 同步失败:' . $se->getMessage(); + $syncErrors[] = $errTxt; + \think\Log::error('[Master::income syncWithdraw] ' . $errTxt); + } + } + $master = $this->masterInfo(true) ?: $master; + } + + $todayStart = strtotime(date('Y-m-d')); $todayIncome = Db::name('wallet_log')->where('user_type', 'master')->where('user_id', $master['id']) ->where('type', 'settle')->where('createtime', '>=', $todayStart)->sum('amount'); $totalIncome = Db::name('wallet_log')->where('user_type', 'master')->where('user_id', $master['id']) ->where('type', 'settle')->sum('amount'); $logs = Db::name('wallet_log')->where('user_type', 'master')->where('user_id', $master['id']) - ->order('id desc')->limit(20)->select(); + ->order('id desc')->limit(30)->select(); + + // 动态读取独立配置中心提现规则与今日额度统计 + $withdrawConfig = FinanceConfigService::buildMasterWithdrawClientPayload($master); + $limits = PaymentService::getWithdrawLimits(); + $limits['today_used'] = $withdrawConfig['today_amount']; + $limits['today_remain'] = $withdrawConfig['remain_daily_amount'] >= 0 + ? $withdrawConfig['remain_daily_amount'] + : (float)$limits['daily_user_limit']; + + // 近期提现单列表与待师傅点击确认收款的提现单 + $rawWithdraws = Db::name('withdraw') + ->where('master_id', (int)$master['id']) + ->order('id desc') + ->limit(30) + ->select(); + $withdrawList = []; + $pendingConfirmList = []; + foreach ($rawWithdraws as $wd) { + $item = $this->formatWithdrawItem($wd); + $withdrawList[] = $item; + if ($item['can_confirm']) { + $pendingConfirmList[] = $item; + } + } $this->success('', [ - 'money' => (float)$master['money'], - 'frozen_money' => (float)$master['frozen_money'], - 'today_income' => round((float)$todayIncome, 2), - 'total_income' => round((float)$totalIncome, 2), - 'order_count' => (int)$master['order_count'], - 'logs' => $logs, + 'money' => (float)$master['money'], + 'frozen_money' => (float)$master['frozen_money'], + 'today_income' => round((float)$todayIncome, 2), + 'total_income' => round((float)$totalIncome, 2), + 'order_count' => (int)$master['order_count'], + 'logs' => $logs, + 'withdraw_config' => $withdrawConfig, + 'withdraw_limits' => $limits, + 'withdraw_list' => $withdrawList, + 'pending_confirm_withdraws' => $pendingConfirmList, + 'sync_errors' => $syncErrors, ]); } @@ -814,25 +1293,48 @@ class Master extends Base } /** - * 申请提现 + * 申请提现(支持微信零钱新版商家转账 1005 场景额度校验与 OpenID 绑定) * @ApiMethod (POST) */ public function withdraw() { - $master = $this->requireMaster(); - $amount = round((float)$this->request->post('amount'), 2); - if ($amount < 10) { - $this->error('单次提现金额不少于 10 元'); - } + $master = $this->requireMaster(); + $amount = round((float)$this->request->post('amount'), 2); + $wxCode = trim((string)$this->request->post('wx_code', '')); $channel = trim((string)$this->request->post('channel', 'wechat')); if (!in_array($channel, ['wechat', 'bank'], true)) { $channel = 'wechat'; } $account = mb_substr(trim((string)$this->request->post('account', '')), 0, 100); - $fee = round($amount * 0.006, 2); - $arrive = round($amount - $fee, 2); - $now = time(); + // 1. 执行独立配置中心的全量提现规则与微信小程序转账合规校验 + try { + $validated = FinanceConfigService::validateWithdrawRequest($master, $amount, $channel, $account); + } catch (\InvalidArgumentException $ve) { + $this->error($ve->getMessage()); + return; + } + + $wdCfg = $validated['cfg']; + $fee = $validated['fee']; + $arrive = $validated['arrive_amount']; + + if ($account === '' && $channel === 'wechat') { + $account = '微信零钱'; + } + + // 2. 在数据库事务外解析并补绑当前收款微信号 OpenID + $openid = ''; + if ($channel === 'wechat' && $wdCfg['transfer_mode'] === 'wx_mch_transfer') { + try { + $openid = PaymentService::resolveUserOpenid($this->uid(), $wxCode); + } catch (\Throwable $oe) { + $this->error($oe->getMessage()); + return; + } + } + + $now = time(); Db::startTrans(); try { $lockedMaster = Db::name('master')->where('id', (int)$master['id'])->lock(true)->find(); @@ -840,7 +1342,7 @@ class Master extends Base throw new \InvalidArgumentException('可提现余额不足'); } - $newMoney = round((float)$lockedMaster['money'] - $amount, 2); + $newMoney = round((float)$lockedMaster['money'] - $amount, 2); $newFrozen = round((float)$lockedMaster['frozen_money'] + $amount, 2); Db::name('master')->where('id', (int)$lockedMaster['id'])->update([ @@ -849,13 +1351,15 @@ class Master extends Base 'updatetime' => $now, ]); - Db::name('withdraw')->insert([ + $withdrawId = (int)Db::name('withdraw')->insertGetId([ 'master_id' => (int)$lockedMaster['id'], 'amount' => $amount, 'fee' => $fee, 'arrive_amount' => $arrive, 'channel' => $channel, 'account' => $account, + 'openid' => $openid, + 'rule_snapshot' => $validated['rule_snapshot'], 'status' => 'wait', 'createtime' => $now, 'updatetime' => $now, @@ -868,7 +1372,7 @@ class Master extends Base 'amount' => -$amount, 'balance' => $newMoney, 'order_id' => 0, - 'remark' => '提现申请(手续费 ¥' . $fee . ')', + 'remark' => '提现申请(手续费 ¥' . number_format($fee, 2, '.', '') . ',预计到账 ¥' . number_format($arrive, 2, '.', '') . ')', 'createtime' => $now, ]); Db::commit(); @@ -886,7 +1390,184 @@ class Master extends Base return; } - $this->success('提现申请已提交,等待审核', ['arrive_amount' => $arrive]); + // 3. 若命中「小额免审自动发起微信转账」,在本地事务外自动发起商家转账(失败必落库记录中文原因并明确回传,严禁静默吞掉) + $autoInitiated = false; + $canConfirmNow = false; + $autoFailReason = ''; + if ( + $channel === 'wechat' + && $wdCfg['transfer_mode'] === 'wx_mch_transfer' + && $wdCfg['audit_mode'] === 'auto_small' + && $amount <= (float)$wdCfg['auto_max_amount'] + ) { + try { + $autoWd = PaymentService::initiateWithdrawTransfer($withdrawId, '小额免审自动发起微信商家转账'); + $autoInitiated = true; + $formattedAuto = $this->formatWithdrawItem($autoWd); + $canConfirmNow = !empty($formattedAuto['can_confirm']); + } catch (\Throwable $ae) { + $autoFailReason = trim((string)$ae->getMessage()); + Db::name('withdraw')->where('id', $withdrawId)->update([ + 'audit_msg' => mb_substr('【小额免审自动转账失败】' . $autoFailReason, 0, 480), + 'updatetime' => time(), + ]); + \think\Log::error('[Master::withdraw auto_small] withdraw_id=' . $withdrawId . ' 自动转账失败: ' . $autoFailReason); + } + } + + if ($canConfirmNow) { + $tipMsg = '提现已自动审核通过,请立即确认收款入账微信零钱'; + } elseif ($autoFailReason !== '') { + $tipMsg = '提现申请已提交并冻结金额,但微信自动转账发起失败:' . $autoFailReason . ',已记录原因待财务核查处理'; + } else { + $tipMsg = '提现申请已提交,审核通过后请在钱包内确认收款'; + } + + $this->success($tipMsg, [ + 'withdraw_id' => $withdrawId, + 'arrive_amount' => $arrive, + 'auto_initiated' => $autoInitiated, + 'can_confirm' => $canConfirmNow, + 'auto_fail_reason' => $autoFailReason, + ]); + } + + /** + * 获取微信商家转账小程序确认收款凭证(供前端 wx.requestMerchantTransfer 调起官方收款页) + * @ApiMethod (POST) + */ + public function transferPackage() + { + $master = $this->requireMaster(); + $withdrawId = (int)$this->request->post('withdraw_id'); + if ($withdrawId <= 0) { + $this->error('参数错误'); + } + + $wd = Db::name('withdraw')->where('id', $withdrawId)->find(); + if (!$wd || (int)$wd['master_id'] !== (int)$master['id']) { + $this->error('无权访问该提现记录', null, 403); + } + + // 强制实时向微信网关查单核验最新状态,核验失败直接中断阻止拉起失效收款凭证 + try { + $wd = PaymentService::syncWithdrawTransfer($withdrawId); + } catch (\Throwable $e) { + \think\Log::error('[Master::transferPackage sync] withdraw_id=' . $withdrawId . ' err=' . $e->getMessage()); + $this->error('核验微信转账状态失败:' . $e->getMessage()); + return; + } + + if ($wd['status'] === 'done') { + $this->success('该笔提现已收款入账微信零钱', [ + 'already_done' => true, + 'withdraw' => $this->formatWithdrawItem($wd), + ]); + } + if ($wd['status'] === 'reject') { + $this->error('该笔提现未完成(' . ($wd['fail_reason'] ?: $wd['audit_msg'] ?: '已退回') . '),金额已退回可用余额'); + } + if (empty($wd['package_info']) || !in_array($wd['transfer_state'], ['WAIT_USER_CONFIRM', 'TRANSFERING'], true)) { + $this->error('当前提现单状态(' . PaymentService::transferStateLabel($wd['transfer_state']) . ')暂不可拉起确认收款,请稍后重试'); + } + + $wxSvc = new WxPayV3Service(); + $this->success('', [ + 'already_done' => false, + 'withdraw_id' => (int)$wd['id'], + 'mch_id' => $wxSvc->getMchId(), + 'app_id' => $wxSvc->getAppId(), + 'package_info' => (string)$wd['package_info'], + 'out_bill_no' => (string)$wd['out_bill_no'], + 'arrive_amount' => (float)$wd['arrive_amount'], + ]); + } + + /** + * 师傅从微信确认收款页面返回后主动同步收款入账状态 + * @ApiMethod (POST) + */ + public function syncWithdraw() + { + $master = $this->requireMaster(); + $withdrawId = (int)$this->request->post('withdraw_id'); + if ($withdrawId <= 0) { + $this->error('参数错误'); + } + + $wd = Db::name('withdraw')->where('id', $withdrawId)->find(); + if (!$wd || (int)$wd['master_id'] !== (int)$master['id']) { + $this->error('无权访问该提现记录', null, 403); + } + + try { + $wd = PaymentService::syncWithdrawTransfer($withdrawId); + } catch (\Throwable $e) { + \think\Log::error('[Master::syncWithdraw] ' . $e->getMessage()); + $this->error('同步微信转账状态失败:' . $e->getMessage()); + return; + } + + $formatted = $this->formatWithdrawItem($wd); + $isDone = $wd['status'] === 'done'; + $msg = $isDone ? '收款成功,资金已入账微信零钱' : ('当前状态:' . $formatted['status_text']); + $this->success($msg, [ + 'done' => $isDone, + 'withdraw' => $formatted, + ]); + } + + /** + * 格式化单条提现记录输出 + */ + protected function formatWithdrawItem(array $wd) + { + $status = (string)($wd['status'] ?? 'wait'); + $txState = strtoupper((string)($wd['transfer_state'] ?? '')); + $channel = (string)($wd['channel'] ?? 'wechat'); + + $canConfirm = ($status === 'pass' + && $channel === 'wechat' + && !empty($wd['package_info']) + && in_array($txState, ['WAIT_USER_CONFIRM', 'TRANSFERING'], true)); + + $statusText = '财务审核中'; + if ($status === 'done') { + $statusText = '已到账零钱'; + } elseif ($status === 'reject') { + $statusText = '已退回余额'; + } elseif ($status === 'pass') { + if ($canConfirm) { + $statusText = '待确认收款'; + } elseif ($txState === 'CANCELING') { + $statusText = '撤销处理中'; + } elseif ($txState !== '') { + $statusText = PaymentService::transferStateLabel($txState); + } else { + $statusText = '审核通过待打款'; + } + } + + return [ + 'id' => (int)$wd['id'], + 'amount' => (float)$wd['amount'], + 'fee' => (float)$wd['fee'], + 'arrive_amount' => (float)$wd['arrive_amount'], + 'channel' => $channel, + 'account' => (string)($wd['account'] ?? ''), + 'status' => $status, + 'status_text' => $statusText, + 'transfer_state' => $txState, + 'transfer_state_text' => PaymentService::transferStateLabel($txState), + 'can_confirm' => $canConfirm, + 'out_bill_no' => (string)($wd['out_bill_no'] ?? ''), + 'audit_msg' => (string)($wd['audit_msg'] ?? ''), + 'fail_code' => (string)($wd['fail_code'] ?? ''), + 'fail_reason' => (string)($wd['fail_reason'] ?? ''), + 'createtime' => (int)($wd['createtime'] ?? 0), + 'confirm_time' => (int)($wd['confirm_time'] ?? 0), + 'time_text' => !empty($wd['createtime']) ? date('Y-m-d H:i', (int)$wd['createtime']) : '', + ]; } /** @@ -926,15 +1607,16 @@ class Master extends Base $reviewTotal = (int)Db::name('review')->where('master_id', $mid)->where('status', 'normal')->count(); $reviewGood = (int)Db::name('review')->where('master_id', $mid)->where('status', 'normal')->where('score', '>=', 4)->count(); $appealCount = (int)Db::name('appeal')->where('master_id', $mid)->count(); - $orderCount = max(1, (int)$master['order_count']); + $orderCount = (int)$master['order_count']; $this->success('', [ 'score' => (float)$master['score'], 'level' => (int)$master['level'], 'accept_rate' => (float)$master['accept_rate'], 'ontime_rate' => (float)$master['ontime_rate'], - 'good_rate' => $reviewTotal ? round($reviewGood * 100 / $reviewTotal, 1) : 100, - 'complaint_rate' => round($appealCount * 100 / $orderCount, 1), + 'good_rate' => $reviewTotal > 0 ? round($reviewGood * 100 / $reviewTotal, 1) : 0, + 'has_review' => $reviewTotal > 0, + 'complaint_rate' => $orderCount > 0 ? round($appealCount * 100 / $orderCount, 1) : 0, 'order_count' => (int)$master['order_count'], 'review_total' => $reviewTotal, 'appeal_total' => $appealCount, @@ -1049,12 +1731,175 @@ class Master extends Base unset($row); $this->success('', ['list' => $list]); } -/** + + /** + * 获取师傅已配置的技能类目 ID 列表 + */ + protected function getMasterSkillCatIds($masterId) + { + $catIds = Db::name('master_skill')->where('master_id', (int)$masterId)->where('cat_id', '>', 0)->column('cat_id'); + return array_values(array_filter(array_unique($catIds))); + } + + /** + * 订单表有效坐标 SQL 过滤片段 + */ + protected function getValidOrderCoordSql() + { + return "(lng REGEXP '^-?[0-9]+(\\\\.[0-9]+)?$' AND lat REGEXP '^-?[0-9]+(\\\\.[0-9]+)?$' AND CAST(lng AS DECIMAL(10,6)) BETWEEN -180 AND 180 AND CAST(lat AS DECIMAL(10,6)) BETWEEN -90 AND 90 AND (ABS(CAST(lng AS DECIMAL(10,6))) > 0.0001 OR ABS(CAST(lat AS DECIMAL(10,6))) > 0.0001))"; + } + + /** + * 球面公里距离参数化 SQL 表达式 + */ + protected function getDistanceSqlFormula() + { + return "(6371.0 * ACOS(LEAST(1.0, GREATEST(-1.0, COS(RADIANS(:h_lat1)) * COS(RADIANS(CAST(lat AS DECIMAL(10,6)))) * COS(RADIANS(CAST(lng AS DECIMAL(10,6))) - RADIANS(:h_lng1)) + SIN(RADIANS(:h_lat2)) * SIN(RADIANS(CAST(lat AS DECIMAL(10,6))))))))"; + } + + /** + * 统计指定锚点与服务半径内的真实待接工单数量 + */ + protected function countLocationOrderStats($masterId, $lat, $lng, $serveRadius, $city = '') + { + $serveRadius = max(5, min(200, (int)$serveRadius)); + $catIds = $this->getMasterSkillCatIds($masterId); + + $buildPool = function () use ($catIds) { + $q = Db::name('order') + ->where('master_id', 0) + ->where('status', 'in', [OrderStatus::PENDING_ACCEPT, OrderStatus::QUOTING]); + if (!empty($catIds)) { + $q->where('cat_id', 'in', $catIds); + } + return $q; + }; + + $totalOrderCount = (int)$buildPool()->count(); + $inRadiusCount = 0; + + if ($lat !== null && $lng !== null && $this->isValidCoord($lat, $lng)) { + $validCoordCond = $this->getValidOrderCoordSql(); + $distFormula = $this->getDistanceSqlFormula(); + $subSql = $buildPool() + ->whereRaw($validCoordCond) + ->field("id, {$distFormula} AS _dist_km") + ->buildSql(); + $inRadiusCount = (int)Db::table($subSql . ' stat_o') + ->bind([ + 'h_lat1' => (float)$lat, + 'h_lng1' => (float)$lng, + 'h_lat2' => (float)$lat, + 'r_limit' => (float)$serveRadius, + ]) + ->where('_dist_km <= :r_limit') + ->count(); + } + + $cityToken = $this->extractCityToken($city); + $cityOrderCount = 0; + if ($cityToken !== '') { + $cityOrderCount = (int)$buildPool()->where('city', 'like', '%' . $cityToken . '%')->count(); + } + + return [ + 'serve_radius' => $serveRadius, + 'in_radius_count' => $inRadiusCount, + 'city_order_count' => $cityOrderCount, + 'total_order_count' => $totalOrderCount, + ]; + } + + /** + * 统计平台真实有待接工单的城市分布,供位置页城市快捷切换展示(0 假数据) + */ + protected function getActiveHallCities($masterId) + { + $catIds = $this->getMasterSkillCatIds($masterId); + $q = Db::name('order') + ->where('master_id', 0) + ->where('status', 'in', [OrderStatus::PENDING_ACCEPT, OrderStatus::QUOTING]) + ->where('city', '<>', ''); + if (!empty($catIds)) { + $q->where('cat_id', 'in', $catIds); + } + $rows = $q->field('city, COUNT(*) AS order_count') + ->group('city') + ->order('order_count desc') + ->limit(12) + ->select(); + + $list = []; + foreach ($rows as $r) { + $cName = trim((string)($r['city'] ?? '')); + if ($cName === '') { + continue; + } + $list[] = [ + 'city' => $cName, + 'order_count' => (int)($r['order_count'] ?? 0), + ]; + } + return $list; + } + + /** + * 校验经纬度坐标是否有效 + */ + protected function isValidCoord($lat, $lng) + { + if (!is_numeric($lat) || !is_numeric($lng)) { + return false; + } + $latF = (float)$lat; + $lngF = (float)$lng; + if (abs($latF) < 0.0001 && abs($lngF) < 0.0001) { + return false; + } + return $latF >= -90.0 && $latF <= 90.0 && $lngF >= -180.0 && $lngF <= 180.0; + } + + /** + * 从省市区文本中提取地级市/区域匹配关键词,用于无精确坐标订单的同城推荐兜底 + */ + protected function extractCityToken($cityStr) + { + $cityStr = trim((string)$cityStr); + if ($cityStr === '') { + return ''; + } + if (preg_match('/([^\s省]+?(?:市|自治州|地区|盟))/u', $cityStr, $m)) { + $token = trim($m[1]); + // 去除直辖市或复合字符串前缀(如“贵州省贵阳市”已在上面截出市级) + if (mb_strlen($token) > 2 && mb_substr($token, -1) === '市') { + return mb_substr($token, 0, mb_strlen($token) - 1); + } + return $token; + } + $parts = preg_split('/\s+/u', $cityStr); + return trim((string)end($parts)); + } + + /** + * 格式化公里距离展示文案 + */ + protected function formatDistanceText($distKm) + { + if ($distKm === null) { + return ''; + } + if ($distKm < 1.0) { + return max(50, (int)round($distKm * 1000)) . 'm'; + } + return $distKm . 'km'; + } + + /** * 计算两点间距离 (km) */ protected function calculateDistance($lat1, $lon1, $lat2, $lon2) { - if (empty($lat1) || empty($lon1) || empty($lat2) || empty($lon2)) { + if (!$this->isValidCoord($lat1, $lon1) || !$this->isValidCoord($lat2, $lon2)) { return null; } $earthRadius = 6371; @@ -1425,11 +2270,11 @@ class Master extends Base ? ($prefix . $address) : $address; - // 计算结算比例与预计收益 - $commissionRate = 10.00; - $payAmt = (float)$order['pay_amount'] > 0 ? (float)$order['pay_amount'] : (float)$order['total_amount']; - $estCommission = round($payAmt * $commissionRate / 100, 2); - $estMasterFee = round(max(0, $payAmt - $estCommission), 2); + // 按订单锁定的多维抽佣快照计算预计收益 + $estCalc = CommissionService::calcByOrder($order); + $commissionRate = $estCalc['commission_rate']; + $estCommission = $estCalc['commission']; + $estMasterFee = $estCalc['settle_amount']; $data = [ 'id' => (int)$order['id'], @@ -1443,6 +2288,8 @@ class Master extends Base 'floor_amount' => (float)($order['floor_amount'] ?? 0), 'urgent_amount' => (float)($order['urgent_amount'] ?? 0), 'pay_amount' => (float)($order['pay_amount'] ?? 0), + 'commission_rate' => $commissionRate, + 'commission' => $estCommission, 'master_fee' => $estMasterFee, 'user_price' => $order['price_type'] === 'custom' ? (float)$order['base_amount'] : (float)$order['total_amount'], 'service_name' => $order['service_name'] ?: ($service ? $service['name'] : '家庭维修服务'), @@ -1970,5 +2817,480 @@ class Master extends Base $this->success('服务技能保存成功'); } + + /** + * 技能证书字典与当前师傅上传状态列表 + * @ApiMethod (GET) + */ + public function certs() + { + $master = $this->masterInfo(); + $masterId = $master ? (int)$master['id'] : 0; + + $dictList = Db::name('cert_dict') + ->field('id,cert_code,cert_name,sample_type,need_expire,weigh') + ->where('status', 'normal') + ->order('weigh desc, id asc') + ->select(); + + $myRecords = []; + if ($masterId > 0) { + $myRecords = Db::name('master_cert') + ->field('id,cert_dict_id,cert_code,cert_name,is_custom,cert_image,expire_date,is_long_term,status,audit_msg,audittime,createtime,updatetime') + ->where('master_id', $masterId) + ->order('id desc') + ->select(); + } + + $stdMap = []; + $customItems = []; + foreach ($myRecords as $rec) { + if ((int)$rec['is_custom'] === 1 || (string)$rec['cert_code'] === 'other') { + $customItems[] = $this->formatMasterCertRow($rec); + } else { + $code = (string)$rec['cert_code']; + if (!isset($stdMap[$code])) { + $stdMap[$code] = $this->formatMasterCertRow($rec); + } + } + } + + $items = []; + $otherEntry = null; + foreach ($dictList as $d) { + $code = (string)$d['cert_code']; + if ($code === 'other') { + $otherEntry = [ + 'dict_id' => (int)$d['id'], + 'cert_code' => $code, + 'cert_name' => (string)$d['cert_name'], + 'sample_type' => (string)$d['sample_type'], + 'need_expire' => (int)$d['need_expire'], + ]; + continue; + } + $uploaded = $stdMap[$code] ?? null; + $items[] = [ + 'dict_id' => (int)$d['id'], + 'cert_code' => $code, + 'cert_name' => (string)$d['cert_name'], + 'sample_type' => (string)$d['sample_type'], + 'need_expire' => (int)$d['need_expire'], + 'status' => $uploaded ? $uploaded['status'] : 'none', + 'status_text' => $uploaded ? $uploaded['status_text'] : '未上传', + 'cert_id' => $uploaded ? $uploaded['id'] : 0, + 'cert_image' => $uploaded ? $uploaded['cert_image'] : '', + 'expire_date' => $uploaded ? $uploaded['expire_date'] : '', + 'is_long_term' => $uploaded ? $uploaded['is_long_term'] : 0, + 'audit_msg' => $uploaded ? $uploaded['audit_msg'] : '', + 'updatetime' => $uploaded ? $uploaded['updatetime'] : 0, + ]; + } + + $this->success('', [ + 'notice' => '请上传本人真实的技能证书信息。技能证书审核通过后,才能接商家指定资质要求的订单。', + 'is_master_applied' => $master ? 1 : 0, + 'verify_status' => $master ? (string)$master['verify_status'] : '', + 'items' => $items, + 'other_entry' => $otherEntry ?: [ + 'dict_id' => 0, + 'cert_code' => 'other', + 'cert_name' => '其他证书', + 'sample_type' => 'qualification', + 'need_expire' => 1, + ], + 'custom_items' => $customItems, + 'summary' => $this->buildMasterCertSummary($masterId), + ]); + } + + /** + * 单个技能证书详情与示例图说明 + * @ApiMethod (GET) + */ + public function certDetail() + { + $certCode = trim((string)$this->request->get('cert_code', '')); + $certId = (int)$this->request->get('cert_id', 0); + + $master = $this->masterInfo(); + $masterId = $master ? (int)$master['id'] : 0; + + $record = null; + if ($certId > 0) { + if ($masterId <= 0) { + $this->error('请先完成师傅入驻认证', ['need_apply' => 1], 2001); + } + $row = Db::name('master_cert') + ->field('id,master_id,cert_dict_id,cert_code,cert_name,is_custom,cert_image,expire_date,is_long_term,status,audit_msg,audittime,createtime,updatetime') + ->where('id', $certId) + ->find(); + if (!$row || (int)$row['master_id'] !== $masterId) { + $this->error('证书记录不存在或无权查看', null, 403); + } + $record = $this->formatMasterCertRow($row); + if ($certCode === '') { + $certCode = (string)$row['cert_code']; + } + } + + if ($certCode === '') { + $this->error('缺少证书类型参数'); + } + + $dict = Db::name('cert_dict') + ->field('id,cert_code,cert_name,sample_type,need_expire') + ->where('cert_code', $certCode) + ->where('status', 'normal') + ->find(); + if (!$dict) { + $this->error('不支持的证书类型'); + } + + if ($record === null && $masterId > 0 && $certCode !== 'other') { + $row = Db::name('master_cert') + ->field('id,master_id,cert_dict_id,cert_code,cert_name,is_custom,cert_image,expire_date,is_long_term,status,audit_msg,audittime,createtime,updatetime') + ->where('master_id', $masterId) + ->where('cert_code', $certCode) + ->order('id desc') + ->find(); + if ($row) { + $record = $this->formatMasterCertRow($row); + } + } + + $this->success('', [ + 'dict' => [ + 'id' => (int)$dict['id'], + 'cert_code' => (string)$dict['cert_code'], + 'cert_name' => (string)$dict['cert_name'], + 'sample_type' => (string)$dict['sample_type'], + 'need_expire' => (int)$dict['need_expire'], + 'is_other' => $dict['cert_code'] === 'other' ? 1 : 0, + ], + 'record' => $record, + 'sample_tip' => '下图仅用于示例展示,证件有效期、签发机关(审核机构)以您的真实证件为准。', + 'sample_images' => $this->resolveCertSamples((string)$dict['sample_type'], (string)$dict['cert_name']), + ]); + } + + /** + * 提交或重新上传技能证书 + * @ApiMethod (POST) + */ + public function saveCert() + { + $master = $this->masterInfo(); + if (!$master) { + $this->error('请先完成师傅入驻认证', ['need_apply' => 1], 2001); + } + if ((string)$master['verify_status'] === 'reject') { + $this->error('您的实名认证未通过,请先修改实名资料', ['verify_status' => 'reject'], 2003); + } + $masterId = (int)$master['id']; + + $certCode = trim((string)$this->request->post('cert_code', '')); + $certId = (int)$this->request->post('cert_id', 0); + $customName = trim((string)$this->request->post('custom_name', '')); + $certImage = trim((string)$this->request->post('cert_image', '')); + $expireDate = trim((string)$this->request->post('expire_date', '')); + $isLongTerm = (int)$this->request->post('is_long_term', 0) === 1 ? 1 : 0; + + if ($certCode === '') { + $this->error('请选择技能证书类型'); + } + $dict = Db::name('cert_dict') + ->field('id,cert_code,cert_name,need_expire') + ->where('cert_code', $certCode) + ->where('status', 'normal') + ->find(); + if (!$dict) { + $this->error('技能证书类型不存在或已停用'); + } + + $isCustom = ($certCode === 'other') ? 1 : 0; + if ($isCustom === 1) { + if ($customName === '') { + $this->error('请填写自定义证书名称'); + } + $customName = preg_replace('/[<>"\'`]/u', '', $customName); + $nameLen = mb_strlen($customName, 'UTF-8'); + if ($nameLen < 2 || $nameLen > 40) { + $this->error('证书名称长度需在 2 至 40 个字符之间'); + } + $finalCertName = $customName; + } else { + $finalCertName = (string)$dict['cert_name']; + } + + if ($certImage === '' || mb_strlen($certImage, 'UTF-8') > 255) { + $this->error('请上传有效的证书照片'); + } + $isValidImageUrl = (strpos($certImage, '..') === false) + && ((bool)preg_match('#^/[a-zA-Z0-9_-]+/[^\s<>"\']+$#', $certImage) + || (bool)preg_match('#^https?://[^\s<>"\']+$#i', $certImage)); + if (!$isValidImageUrl) { + $this->error('证书照片地址格式无效,请重新上传'); + } + + $finalExpireDate = null; + if ($isLongTerm === 0) { + if (!preg_match('/^(\d{4})-(\d{2})-(\d{2})$/', $expireDate, $m)) { + $this->error('请选择证件有效期截止日期'); + } + if (!checkdate((int)$m[2], (int)$m[3], (int)$m[1])) { + $this->error('证件有效期日期格式不正确'); + } + if ($expireDate < date('Y-m-d')) { + $this->error('该证书已过有效期限,请上传有效期内的证件'); + } + $finalExpireDate = $expireDate; + } + + $existingRow = null; + if ($isCustom === 1) { + if ($certId > 0) { + $existingRow = Db::name('master_cert') + ->field('id,master_id,is_custom') + ->where('id', $certId) + ->find(); + if (!$existingRow || (int)$existingRow['master_id'] !== $masterId || (int)$existingRow['is_custom'] !== 1) { + $this->error('无权修改该证书记录', null, 403); + } + } else { + $existingRow = Db::name('master_cert') + ->field('id,master_id') + ->where('master_id', $masterId) + ->where('is_custom', 1) + ->where('cert_name', $finalCertName) + ->find(); + } + } else { + $existingRow = Db::name('master_cert') + ->field('id,master_id') + ->where('master_id', $masterId) + ->where('cert_code', $certCode) + ->where('is_custom', 0) + ->order('id desc') + ->find(); + } + + $now = time(); + $saveData = [ + 'master_id' => $masterId, + 'cert_dict_id' => (int)$dict['id'], + 'cert_code' => $certCode, + 'cert_name' => $finalCertName, + 'is_custom' => $isCustom, + 'cert_image' => $certImage, + 'expire_date' => $finalExpireDate, + 'is_long_term' => $isLongTerm, + 'status' => 'pending', + 'audit_msg' => '', + 'audit_admin_id' => 0, + 'audittime' => null, + 'updatetime' => $now, + ]; + + Db::startTrans(); + try { + if ($existingRow) { + $targetId = (int)$existingRow['id']; + Db::name('master_cert') + ->where('id', $targetId) + ->where('master_id', $masterId) + ->update($saveData); + } else { + $saveData['createtime'] = $now; + $targetId = (int)Db::name('master_cert')->insertGetId($saveData); + } + + $this->syncMasterCertImagesColumn($masterId); + Db::commit(); + } catch (\Exception $e) { + Db::rollback(); + \think\Log::error('[Master::saveCert] ' . $e->getMessage()); + $this->error('提交证书失败,请稍后重试'); + } + + $savedRow = Db::name('master_cert') + ->field('id,cert_dict_id,cert_code,cert_name,is_custom,cert_image,expire_date,is_long_term,status,audit_msg,audittime,createtime,updatetime') + ->where('id', $targetId) + ->find(); + + $this->success('提交成功,已进入审核队列', [ + 'record' => $savedRow ? $this->formatMasterCertRow($savedRow) : null, + 'summary' => $this->buildMasterCertSummary($masterId), + ]); + } + + /** + * 删除本人上传的技能证书记录 + * @ApiMethod (POST) + */ + public function deleteCert() + { + $master = $this->masterInfo(); + if (!$master) { + $this->error('请先完成师傅入驻认证', ['need_apply' => 1], 2001); + } + $masterId = (int)$master['id']; + $certId = (int)$this->request->post('cert_id', 0); + if ($certId <= 0) { + $this->error('缺少证书参数'); + } + + $row = Db::name('master_cert') + ->field('id,master_id') + ->where('id', $certId) + ->find(); + if (!$row || (int)$row['master_id'] !== $masterId) { + $this->error('证书记录不存在或无权操作', null, 403); + } + + Db::startTrans(); + try { + Db::name('master_cert') + ->where('id', $certId) + ->where('master_id', $masterId) + ->delete(); + $this->syncMasterCertImagesColumn($masterId); + Db::commit(); + } catch (\Exception $e) { + Db::rollback(); + \think\Log::error('[Master::deleteCert] ' . $e->getMessage()); + $this->error('删除失败,请稍后重试'); + } + + $this->success('已删除该证书记录', [ + 'summary' => $this->buildMasterCertSummary($masterId), + ]); + } + + /** + * 统计师傅技能证书各状态数量(供工作台与列表共用) + */ + private function buildMasterCertSummary($masterId) + { + $summary = [ + 'total' => 0, + 'passed' => 0, + 'pending' => 0, + 'rejected' => 0, + ]; + $masterId = (int)$masterId; + if ($masterId <= 0) { + return $summary; + } + $rows = Db::name('master_cert') + ->field('status, COUNT(*) as cnt') + ->where('master_id', $masterId) + ->group('status') + ->select(); + foreach ($rows as $r) { + $st = (string)($r['status'] ?? ''); + $cnt = (int)($r['cnt'] ?? 0); + $summary['total'] += $cnt; + if ($st === 'pass') { + $summary['passed'] += $cnt; + } elseif ($st === 'pending') { + $summary['pending'] += $cnt; + } elseif ($st === 'reject') { + $summary['rejected'] += $cnt; + } + } + return $summary; + } + + /** + * 同步刷新 fa_master.cert_images 兼容字段 + */ + private function syncMasterCertImagesColumn($masterId) + { + $images = Db::name('master_cert') + ->where('master_id', (int)$masterId) + ->where('status', 'in', ['pass', 'pending']) + ->order('id desc') + ->column('cert_image'); + $cleanImages = array_values(array_unique(array_filter(array_map('strval', $images ?: [])))); + $json = !empty($cleanImages) ? json_encode($cleanImages, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) : ''; + Db::name('master')->where('id', (int)$masterId)->update([ + 'cert_images' => mb_substr($json, 0, 1000), + 'updatetime' => time(), + ]); + } + + /** + * 统一格式化单条技能证书输出结构 + */ + private function formatMasterCertRow(array $row) + { + $statusMap = [ + 'pending' => '审核中', + 'pass' => '已通过', + 'reject' => '已驳回', + ]; + $st = (string)($row['status'] ?? 'pending'); + $img = (string)($row['cert_image'] ?? ''); + return [ + 'id' => (int)$row['id'], + 'cert_dict_id' => (int)($row['cert_dict_id'] ?? 0), + 'cert_code' => (string)($row['cert_code'] ?? ''), + 'cert_name' => (string)($row['cert_name'] ?? ''), + 'is_custom' => (int)($row['is_custom'] ?? 0), + 'cert_image' => $img, + 'cert_image_full' => $img !== '' ? cdnurl($img, true) : '', + 'expire_date' => !empty($row['expire_date']) ? (string)$row['expire_date'] : '', + 'is_long_term' => (int)($row['is_long_term'] ?? 0), + 'status' => $st, + 'status_text' => $statusMap[$st] ?? '未上传', + 'audit_msg' => (string)($row['audit_msg'] ?? ''), + 'audittime' => (int)($row['audittime'] ?? 0), + 'createtime' => (int)($row['createtime'] ?? 0), + 'updatetime' => (int)($row['updatetime'] ?? 0), + ]; + } + + /** + * 按证书类型返回对应的规范示例图资源路径 + */ + private function resolveCertSamples($sampleType, $certName = '') + { + switch ($sampleType) { + case 'driver': + return [ + [ + 'title' => '驾驶证主页示例', + 'url' => '/static/images/master/cert-sample-driver.png', + ], + ]; + case 'health': + return [ + [ + 'title' => '健康证明示例', + 'url' => '/static/images/master/cert-sample-health.png', + ], + ]; + case 'qualification': + return [ + [ + 'title' => ($certName ?: '行业资格') . '证书示例', + 'url' => '/static/images/master/cert-sample-qualification.png', + ], + ]; + case 'special_card': + default: + return [ + [ + 'title' => '电子特种作业操作证示例', + 'url' => '/static/images/master/cert-sample-special-card.png', + ], + [ + 'title' => '纸质特种作业资格证示例', + 'url' => '/static/images/master/cert-sample-qualification.png', + ], + ]; + } + } } diff --git a/server/application/api/controller/Merchant.php b/server/application/api/controller/Merchant.php index 4d19de1..f50ca58 100644 --- a/server/application/api/controller/Merchant.php +++ b/server/application/api/controller/Merchant.php @@ -2,6 +2,8 @@ namespace app\api\controller; +use app\common\library\CommissionService; +use app\common\library\FinanceConfigService; use app\common\library\InvoiceService; use app\common\library\OrderStatus; use think\Db; @@ -645,6 +647,12 @@ class Merchant extends Base $this->error("第 {$rowNo} 条订单计算金额异常"); } + $resolvedProvince = mb_substr(trim((string)(isset($item['province']) && $item['province'] !== '' ? $item['province'] : ($merchant['province'] ?: ''))), 0, 50); + $resolvedCity = mb_substr(trim((string)(isset($item['city']) && $item['city'] !== '' ? $item['city'] : ($merchant['city'] ?: ''))), 0, 50); + if ($resolvedProvince === '' || $resolvedCity === '') { + $this->error("第 {$rowNo} 条订单缺少服务省份或城市信息,请在Excel或表格中填写后再提交"); + } + $preparedRows[] = [ 'svc' => $svc, 'service_id' => (int)$svc['id'], @@ -655,8 +663,8 @@ class Merchant extends Base 'goods_num' => $goodsNum, 'contact' => mb_substr($contact ?: '买家', 0, 50), 'mobile' => $mobile, - 'province' => mb_substr(trim((string)(isset($item['province']) && $item['province'] !== '' ? $item['province'] : ($merchant['province'] ?: '贵州省'))), 0, 50), - 'city' => mb_substr(trim((string)(isset($item['city']) && $item['city'] !== '' ? $item['city'] : ($merchant['city'] ?: '贵阳市'))), 0, 50), + 'province' => $resolvedProvince, + 'city' => $resolvedCity, 'district' => mb_substr(trim((string)(isset($item['district']) ? $item['district'] : ($merchant['district'] ?: ''))), 0, 50), 'address' => mb_substr($address, 0, 255), 'floor' => $floor, @@ -678,6 +686,9 @@ class Merchant extends Base $isMonthly = false; $batchNo = 'MB' . date('YmdHis') . mt_rand(1000, 9999); $now = time(); + // 事务外一次性预加载生效抽佣规则与默认兜底配置,循环内纯内存匹配快照(零额外 SQL) + $activeCommRules = CommissionService::getActiveRules(); + $commDefaults = FinanceConfigService::getCommissionDefaults(); Db::startTrans(); try { @@ -697,6 +708,13 @@ class Merchant extends Base $calc = $pRow['calc']; $payAmt = round((float)$calc['pay_amount'], 2); $unitPrice = round((float)$calc['base_amount'] / max(1, (int)$pRow['goods_num']), 2); + $commMatch = CommissionService::matchOrderSnapshot([ + 'price_type' => 'fixed', + 'customer_type' => 'merchant', + 'cat_id' => (int)$pRow['cat_id'], + 'urgent' => (int)$pRow['urgent'], + 'order_amount' => $payAmt, + ], $activeCommRules, $commDefaults); $orderId = (int)Db::name('order')->insertGetId([ 'order_no' => $orderNo, @@ -724,9 +742,11 @@ class Merchant extends Base 'floor_amount' => $calc['floor_amount'], 'urgent_amount' => $calc['urgent_amount'], 'discount_amount' => 0.00, - 'total_amount' => $calc['total_amount'], - 'pay_amount' => $payAmt, - 'status' => $initStatus, + 'total_amount' => $calc['total_amount'], + 'pay_amount' => $payAmt, + 'commission_rate' => $commMatch['commission_rate'], + 'commission_snapshot' => $commMatch['commission_snapshot'], + 'status' => $initStatus, 'pay_status' => $initPayStatus, 'pay_type' => $initPayType, 'pay_time' => $initPayTime, @@ -825,124 +845,13 @@ class Merchant extends Base } /** - * 商户端:预付商家按批次号支付待支付批量订单 + * 商户端:预付商家按批次号支付待支付批量订单(严禁模拟免付兜底,预付订单须逐笔走真实微信支付) * @ApiMethod (POST) */ public function payBatch() { - $merchant = $this->requireMerchant(); - $batchNo = trim((string)$this->request->post('batch_no', '')); - $payType = trim((string)$this->request->post('pay_type', 'wechat')); - if ($payType === '') { - $payType = 'mock'; - } - if ($batchNo === '') { - $this->error('缺少批次号 batch_no'); - } - - $batchLinks = Db::name('merchant_order')->where('batch_no', $batchNo)->select(); - if (empty($batchLinks)) { - $this->error('批次订单不存在'); - } - $batchOrderIds = []; - foreach ($batchLinks as $link) { - if ((int)$link['merchant_id'] !== (int)$merchant['id']) { - $this->error('无权操作该批次订单', null, 403); - } - $oid = (int)$link['order_id']; - if ($oid > 0) { - $batchOrderIds[] = $oid; - } - } - $batchOrderIds = array_values(array_unique($batchOrderIds)); - if (empty($batchOrderIds)) { - $this->error('该批次下无有效订单'); - } - - $paidCount = 0; - $paidTotal = 0.00; - $paidOrderIds = []; - $now = time(); - - Db::startTrans(); - try { - $lockedMerchant = Db::name('merchant')->where('id', (int)$merchant['id'])->lock(true)->find(); - $orders = Db::name('order') - ->where('id', 'in', $batchOrderIds) - ->lock(true) - ->select(); - - foreach ($orders as $ord) { - if ((int)$ord['pay_status'] === 1 || (int)$ord['status'] !== OrderStatus::PENDING_PAY) { - continue; - } - $oid = (int)$ord['id']; - $amt = round((float)$ord['pay_amount'], 2); - $txId = 'BATCH' . date('YmdHis', $now) . mt_rand(1000, 9999); - - Db::name('order')->where('id', $oid)->update([ - 'status' => OrderStatus::PENDING_ACCEPT, - 'pay_status' => 1, - 'pay_type' => $payType, - 'pay_time' => $now, - 'transaction_id' => $txId, - 'updatetime' => $now, - ]); - - $this->addLog($oid, OrderStatus::PENDING_ACCEPT, "企业批次支付完成(批次 {$batchNo}),订单已进入接单大厅", 'user', $this->uid()); - $paidCount++; - $paidTotal = round($paidTotal + $amt, 2); - $paidOrderIds[] = $oid; - } - - if ($paidCount === 0) { - throw new \InvalidArgumentException('该批次没有待支付的订单,请勿重复支付'); - } - - if ($lockedMerchant) { - Db::name('merchant')->where('id', (int)$lockedMerchant['id'])->update([ - 'order_count' => (int)$lockedMerchant['order_count'] + $paidCount, - 'total_amount' => round((float)$lockedMerchant['total_amount'] + $paidTotal, 2), - 'updatetime' => $now, - ]); - } - - Db::commit(); - } catch (\think\exception\HttpResponseException $e) { - Db::rollback(); - throw $e; - } catch (\InvalidArgumentException $e) { - Db::rollback(); - $this->error($e->getMessage()); - return; - } catch (\Exception $e) { - Db::rollback(); - \think\Log::error('[Merchant::payBatch] ' . $e->getMessage()); - $this->error('批次支付失败:' . $e->getMessage()); - return; - } - - try { - $masters = Db::name('master') - ->where('verify_status', 'pass') - ->where('work_status', 'on') - ->where('user_id', '<>', $this->uid()) - ->limit(20) - ->select(); - foreach ($masters as $m) { - $this->notify('master', (int)$m['id'], '企业批量新工单就绪', "同城有 {$paidCount} 笔企业批量工单进入接单大厅,请尽快前往查看抢单!", 'order', (int)$paidOrderIds[0]); - } - } catch (\Exception $e) { - \think\Log::error('[Merchant::payBatch broadcast] ' . $e->getMessage()); - } - - $this->success('批次支付成功,订单已进入接单大厅', [ - 'batch_no' => $batchNo, - 'count' => $paidCount, - 'paid_count' => $paidCount, - 'total_amount' => round($paidTotal, 2), - 'order_ids' => $paidOrderIds, - ]); + $this->requireMerchant(); + $this->error('系统已全面禁用模拟支付通道,企业预付工单请逐笔使用真实微信支付完成付款(月结授信企业导单时已直接授信进入大厅)'); } protected function merchantInfo() diff --git a/server/application/api/controller/Order.php b/server/application/api/controller/Order.php index 9ceff44..f6203c0 100644 --- a/server/application/api/controller/Order.php +++ b/server/application/api/controller/Order.php @@ -2,7 +2,11 @@ namespace app\api\controller; +use app\common\library\CommissionService; +use app\common\library\FinanceConfigService; use app\common\library\OrderStatus; +use app\common\library\PaymentService; +use app\common\library\TiandituService; use think\Db; /** @@ -21,7 +25,7 @@ class Order extends Base } /** - * 物流单号智能识别物流公司(支持线上快递100识别 + 本地单号特征规则库双保险) + * 物流单号识别物流公司(依据 fa_express_company 精准前缀规则 + 快递100接口;不唯一识别时不瞎猜) */ public function detectExpress() { @@ -31,68 +35,32 @@ class Order extends Base $this->error('请输入正确的物流单号'); } - $codeMap = [ - 'shunfeng' => '顺丰速运', - 'debangwuliu' => '德邦快递', - 'debangkuaidi' => '德邦快递', - 'jd' => '京东快递', - 'kuayue' => '跨越速运', - 'shentong' => '申通快递', - 'yuantong' => '圆通速递', - 'zhongtong' => '中通快递', - 'yunda' => '韵达', - 'huitongkuaidi' => '百世快递', - 'baishiwuliu' => '百世快递', - 'tiantian' => '天天快递', - 'ems' => '邮政特快专递', - 'youzhengguonei' => '中国邮政', - 'youzhengbk' => '中国邮政', - 'zhaijisong' => '宅急送', - 'jtexpress' => '极兔速递', - 'annengwuliu' => '安能物流', - ]; + $expressRows = Db::name('express_company') + ->field('code,name,prefix_regex') + ->where('code', '<>', 'other') + ->order('weigh desc, id asc') + ->select(); + $codeMap = []; $companyName = ''; - - // 1. 优先匹配高确定性的标准物流单号前缀规则 - if (strpos($no, 'SF') === 0) { - $companyName = '顺丰速运'; - } elseif (strpos($no, 'DP') === 0 || strpos($no, 'DB') === 0) { - $companyName = '德邦快递'; - } elseif (strpos($no, 'JD') === 0 || strpos($no, 'KK') === 0) { - $companyName = '京东快递'; - } elseif (strpos($no, 'KY') === 0) { - $companyName = '跨越速运'; - } elseif (strpos($no, 'YT') === 0) { - $companyName = '圆通速递'; - } elseif (strpos($no, 'ST') === 0 || strpos($no, '77') === 0) { - $companyName = '申通快递'; - } elseif (strpos($no, 'ZT') === 0 || preg_match('/^(73|75|76|78)\d{10,12}$/', $no)) { - $companyName = '中通快递'; - } elseif (strpos($no, 'YD') === 0 || preg_match('/^(31|43|46)\d{11,13}$/', $no)) { - $companyName = '韵达'; - } elseif (strpos($no, 'BS') === 0 || strpos($no, 'HT') === 0 || preg_match('/^55\d{11,13}$/', $no)) { - $companyName = '百世快递'; - } elseif (strpos($no, 'TT') === 0 || preg_match('/^66\d{10,12}$/', $no)) { - $companyName = '天天快递'; - } elseif (preg_match('/^(EMS|EA|EB|EC|ED|EE|EF|10|11)\d{7,11}/', $no)) { - $companyName = '邮政特快专递'; - } elseif (preg_match('/^(98|99|YZ)\d{9,13}$/', $no)) { - $companyName = '中国邮政'; - } elseif (strpos($no, 'ZJS') === 0) { - $companyName = '宅急送'; - } elseif (strpos($no, 'JT') === 0) { - $companyName = '极兔速递'; - } elseif (strpos($no, 'ANE') === 0) { - $companyName = '安能物流'; + foreach ($expressRows as $er) { + $eCode = strtolower(trim((string)($er['code'] ?? ''))); + $eName = trim((string)($er['name'] ?? '')); + $regex = trim((string)($er['prefix_regex'] ?? '')); + if ($eCode !== '' && $eName !== '') { + $codeMap[$eCode] = $eName; + } + if ($companyName === '' && $regex !== '' && @preg_match($regex, $no) === 1) { + $companyName = $eName; + } } - // 2. 若前缀未命中,尝试快递100智能单号识别接口(1.5秒超时保护) + // 若本地高确定性特征规则未唯一命中,尝试快递100智能单号识别接口(开启严格 TLS 校验与超时保护) if ($companyName === '') { try { $ctx = stream_context_create([ - 'http' => ['timeout' => 1.5, 'ignore_errors' => true], - 'ssl' => ['verify_peer' => false, 'verify_peer_name' => false], + 'http' => ['timeout' => 2.0, 'ignore_errors' => true], + 'ssl' => ['verify_peer' => true, 'verify_peer_name' => true], ]); $resp = @file_get_contents('https://www.kuaidi100.com/autonumber/autoComNum?text=' . urlencode($no), false, $ctx); if ($resp) { @@ -107,24 +75,18 @@ class Order extends Base } } } - } catch (\Exception $e) { + } catch (\Throwable $e) { \think\Log::warning('[Order::detectExpress] ' . $e->getMessage()); } } - // 3. 常见纯数字位数兜底识别 if ($companyName === '') { - if (preg_match('/^\d{15}$/', $no)) { - $companyName = '德邦快递'; - } elseif (preg_match('/^\d{12}$/', $no)) { - $companyName = '顺丰速运'; - } elseif (preg_match('/^\d{13}$/', $no)) { - $companyName = '圆通速递'; - } elseif (preg_match('/^\d{14}$/', $no)) { - $companyName = '中通快递'; - } else { - $companyName = '德邦快递'; - } + $this->success('未能唯一识别物流公司,请手动选择', [ + 'express_no' => $no, + 'company_name' => '', + 'matched' => false, + ]); + return; } $this->success('已识别物流公司', [ @@ -157,10 +119,11 @@ class Order extends Base $reqServiceType = trim((string)$this->request->post('service_type', '')); $isCustom = (int)$this->request->post('is_custom', 0); $remark = trim((string)$this->request->post('remark')); - $goodsImages = $this->request->post('goods_images', ''); + $rawGoodsImagesIn = $this->postMixed('goods_images', ''); + $goodsImages = is_array($rawGoodsImagesIn) ? json_encode($rawGoodsImagesIn, JSON_UNESCAPED_UNICODE) : (string)$rawGoodsImagesIn; // 支持多商品组合 (goods_list / items) - $goodsListRaw = $this->request->post('goods_list', $this->request->post('items', '', null), null); + $goodsListRaw = $this->postMixed('goods_list', $this->postMixed('items', '')); $goodsList = []; if (!empty($goodsListRaw)) { if (is_string($goodsListRaw)) { @@ -225,14 +188,29 @@ class Order extends Base if (!$address) { $contact = trim((string)$this->request->post('contact')); $mobile = trim((string)$this->request->post('mobile')); - $province = trim((string)$this->request->post('province', '贵州省')); - $city = trim((string)$this->request->post('city', '贵阳市')); + $province = trim((string)$this->request->post('province', '')); + $city = trim((string)$this->request->post('city', '')); $district = trim((string)$this->request->post('district', '')); $detailAddr = trim((string)$this->request->post('address')); $floor = max(1, (int)$this->request->post('floor', 1)); $hasElevator = (int)$this->request->post('has_elevator', 1); + $lng = trim((string)$this->request->post('lng', '')); + $lat = trim((string)$this->request->post('lat', '')); if ($contact && $mobile && $detailAddr) { + if ($province === '' || $city === '') { + $this->error('请选择完整的服务地址省市区'); + } + if ($lng === '' || $lat === '' || abs((float)$lng) < 0.0001 || abs((float)$lat) < 0.0001) { + $geo = TiandituService::geocoder(trim($province . $city . $district . $detailAddr)); + if (!$geo && $city !== '') { + $geo = TiandituService::geocoder(trim($city . $district . $detailAddr)); + } + if ($geo) { + $lng = (string)$geo['lng']; + $lat = (string)$geo['lat']; + } + } $addressId = Db::name('address')->insertGetId([ 'user_id' => $this->uid(), 'contact' => $contact, @@ -241,6 +219,8 @@ class Order extends Base 'city' => $city, 'district' => $district, 'address' => $detailAddr, + 'lng' => $lng, + 'lat' => $lat, 'floor' => $floor, 'has_elevator' => $hasElevator, 'createtime' => time(), @@ -253,13 +233,28 @@ class Order extends Base if (!$address) { $this->error('请选择或填写完整服务地址'); } + // 所选地址缺少经纬度时在事务开启前自动通过天地图补全并回填 + if (empty($address['lng']) || empty($address['lat']) || abs((float)$address['lng']) < 0.0001 || abs((float)$address['lat']) < 0.0001) { + $geo = TiandituService::geocoder(trim(($address['province'] ?? '') . ($address['city'] ?? '') . ($address['district'] ?? '') . ($address['address'] ?? ''))); + if ($geo) { + $address['lng'] = (string)$geo['lng']; + $address['lat'] = (string)$geo['lat']; + if (!empty($address['id'])) { + Db::name('address')->where('id', (int)$address['id'])->update([ + 'lng' => $address['lng'], + 'lat' => $address['lat'], + 'updatetime' => time(), + ]); + } + } + } if ($appointTime > 0 && $appointTime < time()) { $this->error('预约时间不能早于当前时间'); } // 多商品组合名称、总件数与图片汇总 - $serviceName = $service ? $service['name'] : '同城综合上门服务'; - $catId = $service ? (int)$service['cat_id'] : 1; + $serviceName = $service ? $service['name'] : '自定义上门服务'; + $catId = $service ? (int)$service['cat_id'] : 0; $serviceImage = $service ? $service['image'] : ''; if (!empty($goodsList) && is_array($goodsList)) { $firstG = reset($goodsList); @@ -373,6 +368,11 @@ class Order extends Base 'range' => '按面积计价', ]; + // 事务外预加载当前生效的按订单维度抽佣规则与默认兜底配置 + $customerType = $this->currentMerchantTeam() ? 'merchant' : 'personal'; + $activeCommRules = CommissionService::getActiveRules(); + $commDefaults = FinanceConfigService::getCommissionDefaults(); + Db::startTrans(); try { $lockedCouponId = 0; @@ -413,6 +413,14 @@ class Order extends Base $lockedCouponId = (int)$uc['id']; } + $commMatch = CommissionService::matchOrderSnapshot([ + 'price_type' => $priceType, + 'customer_type' => $customerType, + 'cat_id' => (int)$catId, + 'urgent' => $urgent ? 1 : 0, + 'order_amount' => (float)$amount['pay_amount'] > 0 ? (float)$amount['pay_amount'] : (float)$amount['total_amount'], + ], $activeCommRules, $commDefaults); + $id = (int)Db::name('order')->insertGetId([ 'order_no' => $orderNo, 'user_id' => $uid, @@ -452,9 +460,11 @@ class Order extends Base 'floor_amount' => $amount['floor_amount'], 'urgent_amount' => $amount['urgent_amount'], 'discount_amount' => $amount['discount_amount'], - 'total_amount' => $amount['total_amount'], - 'pay_amount' => $amount['pay_amount'], - 'status' => $initStatus, + 'total_amount' => $amount['total_amount'], + 'pay_amount' => $amount['pay_amount'], + 'commission_rate' => $commMatch['commission_rate'], + 'commission_snapshot' => $commMatch['commission_snapshot'], + 'status' => $initStatus, 'pay_status' => 0, 'coupon_id' => $lockedCouponId, 'verify_code' => $verifyCode, @@ -581,6 +591,13 @@ class Order extends Base } } + // 事务外静默沉淀本次订单中用户真实上传的商品图片到「历史商品上传图片库」(自动跳过系统默认图) + try { + $this->archiveOrderUploadedGoodsImages($uid, (int)$catId, (string)$serviceName, $goodsList, $goodsImages); + } catch (\Exception $e) { + \think\Log::error('[Order::create archiveGoodsGallery] ' . $e->getMessage()); + } + $this->success('下单成功', [ 'order_id' => $id, 'order_no' => $orderNo, @@ -589,175 +606,96 @@ class Order extends Base } /** - * 支付(联调模拟支付,pay_type=mock;上线换微信支付回调) + * 发起订单微信 JSAPI 支付(返回小程序 wx.requestPayment 参数包;0元抵扣单直接核销) * @ApiMethod (POST) */ public function pay() { $orderId = (int)$this->request->post('order_id'); - $payType = $this->request->post('pay_type', 'mock'); - $order = $this->ownOrder($orderId, 'user'); + $wxCode = trim((string)$this->request->post('wx_code', '')); + $order = $this->ownOrder($orderId, 'user'); - if ((int)$order['pay_status'] === 1) { - $this->error('订单已支付,请勿重复支付'); - } - if ((int)$order['status'] !== OrderStatus::PENDING_PAY) { - $this->error('订单状态不允许支付'); - } - - $now = time(); - $nextStatus = OrderStatus::PENDING_ACCEPT; - - Db::startTrans(); try { - $lockedOrder = Db::name('order')->where('id', $orderId)->lock(true)->find(); - if (!$lockedOrder) { - throw new \InvalidArgumentException('订单不存在'); - } - if ((int)$lockedOrder['pay_status'] === 1) { - throw new \InvalidArgumentException('订单已支付,请勿重复支付'); - } - if ((int)$lockedOrder['status'] !== OrderStatus::PENDING_PAY) { - throw new \InvalidArgumentException('订单状态不允许支付'); - } - - // 兼容创单阶段已加锁核销的优惠券,防止重复报错或漏锁 - if ((int)$lockedOrder['coupon_id'] > 0) { - $uc = Db::name('user_coupon') - ->where('id', (int)$lockedOrder['coupon_id']) - ->where('user_id', (int)$lockedOrder['user_id']) - ->lock(true) - ->find(); - if (!$uc) { - throw new \InvalidArgumentException('订单关联的优惠券不存在'); - } - if ($uc['status'] === 'used' && (int)$uc['order_id'] === $orderId) { - // 创单已锁定本单卡券 - } elseif ($uc['status'] === 'unused') { - $affectedUc = Db::name('user_coupon') - ->where('id', (int)$uc['id']) - ->where('user_id', (int)$lockedOrder['user_id']) - ->where('status', 'unused') - ->update([ - 'status' => 'used', - 'order_id' => $orderId, - 'use_time' => $now, - ]); - if (!$affectedUc) { - throw new \InvalidArgumentException('优惠券状态异常,请重新下单'); - } - } else { - throw new \InvalidArgumentException('该优惠券已被其他订单使用或已过期'); - } - } - - Db::name('order')->where('id', $orderId)->update([ - 'pay_status' => 1, - 'pay_type' => $payType, - 'pay_time' => $now, - 'transaction_id' => 'MOCK' . date('YmdHis', $now) . mt_rand(1000, 9999), - 'updatetime' => $now, - ]); - $nextStatus = $lockedOrder['price_type'] === 'quote' ? OrderStatus::QUOTING : OrderStatus::PENDING_ACCEPT; - $nextRemark = $nextStatus === OrderStatus::QUOTING - ? '支付成功,订单已进入报价大厅,等待师傅报价' - : '支付成功,订单已进入接单大厅'; - $this->transit($lockedOrder, $nextStatus, $nextRemark, [], 'user', $this->uid()); - $this->addLog($orderId, $nextStatus, $nextStatus === OrderStatus::QUOTING ? '订单进入师傅报价大厅' : '订单进入师傅接单大厅', 'system', 0); - - Db::commit(); + $res = PaymentService::createOrderJsapiPay($order, $this->uid(), 'order', 0, $wxCode); } catch (\think\exception\HttpResponseException $e) { - Db::rollback(); throw $e; } catch (\InvalidArgumentException $e) { - Db::rollback(); $this->error($e->getMessage()); return; - } catch (\Exception $e) { - Db::rollback(); + } catch (\Throwable $e) { \think\Log::error('[Order::pay] ' . $e->getMessage()); - $this->error('支付失败:' . $e->getMessage()); + $this->error('发起微信支付失败:' . $e->getMessage()); return; } - // 订单广播通知:进入接单/报价大厅后,通知周边在线认证师傅(排除下单人本人) - if (in_array($nextStatus, [OrderStatus::PENDING_ACCEPT, OrderStatus::QUOTING], true)) { - try { - $masters = Db::name('master') - ->where('verify_status', 'pass') - ->where('work_status', 'on') - ->where('user_id', '<>', (int)$order['user_id']) - ->limit(20) - ->select(); - foreach ($masters as $m) { - $this->notify('master', (int)$m['id'], '新工单就绪通知', '同城有新工单【' . $order['order_no'] . '】进入接单大厅,请尽快前往抢单!', 'order', $orderId); - } - } catch (\Exception $e) { - \think\Log::error('[Order::pay broadcast] ' . $e->getMessage()); - } - } - - $this->success('支付成功'); + $msg = !empty($res['paid']) ? '支付成功' : '请完成微信支付'; + $this->success($msg, $res); } /** - * 我的订单列表 + * 小程序端完成 wx.requestPayment 后主动查单幂等核销(与异步回调双保险闭环) + * @ApiMethod (POST) */ + public function confirmPay() + { + $outTradeNo = trim((string)$this->request->post('out_trade_no', '')); + $orderId = (int)$this->request->post('order_id', 0); + + $payOrder = null; + if ($outTradeNo !== '') { + $payOrder = Db::name('pay_order')->where('out_trade_no', $outTradeNo)->find(); + } elseif ($orderId > 0) { + $payOrder = Db::name('pay_order')->where('order_id', $orderId)->order('id desc')->find(); + } + if (!$payOrder) { + $this->error('未找到对应的支付交易单'); + } + + // 强制校验订单归属权限,防止水平越权 IDOR + $this->ownOrder((int)$payOrder['order_id'], 'user'); + + try { + $res = PaymentService::confirmOrderPay((string)$payOrder['out_trade_no']); + } catch (\think\exception\HttpResponseException $e) { + throw $e; + } catch (\Throwable $e) { + \think\Log::error('[Order::confirmPay] ' . $e->getMessage()); + $this->error('查询支付结果失败:' . $e->getMessage()); + return; + } + + if (empty($res['paid'])) { + $stateText = !empty($res['trade_state_text']) ? (string)$res['trade_state_text'] : '暂未确认到支付成功结果'; + $this->error('支付未完成:' . $stateText, $res); + } + + $this->success('支付成功', $res); + } + /** - * 用户在线支付施工增项补差价 + * 用户在线微信支付施工增项补差价 * @ApiMethod (POST) */ public function payDiff() { $orderId = (int)$this->request->post('order_id'); - $order = $this->ownOrder($orderId, 'user'); + $wxCode = trim((string)$this->request->post('wx_code', '')); + $order = $this->ownOrder($orderId, 'user'); - if ((int)$order['diff_status'] !== 1 || (float)$order['diff_amount'] <= 0) { - $this->error('当前订单无待支付的增项补差价'); - } - - $diffAmount = (float)$order['diff_amount']; - $newPayAmount = round((float)$order['pay_amount'] + $diffAmount, 2); - - Db::startTrans(); try { - $lockedOrder = Db::name('order')->where('id', $orderId)->lock(true)->find(); - if (!$lockedOrder || (int)$lockedOrder['diff_status'] !== 1 || (float)$lockedOrder['diff_amount'] <= 0) { - throw new \InvalidArgumentException('当前订单无待支付的增项补差价'); - } - $diffAmount = (float)$lockedOrder['diff_amount']; - $newPayAmount = round((float)$lockedOrder['pay_amount'] + $diffAmount, 2); - - Db::name('order')->where('id', $orderId)->update([ - 'diff_status' => 2, - 'pay_amount' => $newPayAmount, - 'updatetime' => time(), - ]); - - $this->addLog($orderId, (int)$lockedOrder['status'], '用户在线支付施工增项补差价 ¥' . $diffAmount, 'user', $this->uid()); - Db::commit(); + $res = PaymentService::createOrderJsapiPay($order, $this->uid(), 'diff', 0, $wxCode); } catch (\think\exception\HttpResponseException $e) { - Db::rollback(); throw $e; } catch (\InvalidArgumentException $e) { - Db::rollback(); $this->error($e->getMessage()); return; - } catch (\Exception $e) { - Db::rollback(); + } catch (\Throwable $e) { \think\Log::error('[Order::payDiff] ' . $e->getMessage()); - $this->error('支付失败:' . $e->getMessage()); + $this->error('发起补差价支付失败:' . $e->getMessage()); return; } - if ((int)$order['master_id'] > 0) { - $m = Db::name('master')->where('id', $order['master_id'])->find(); - if ($m) { - $this->notify('master', (int)$m['id'], '客户已支付增项补差价', '工单 ' . $order['order_no'] . ' 客户已支付增项补差价 ¥' . $diffAmount, 'order', $orderId); - } - } - - $this->success('增项补差价支付成功!', ['pay_amount' => $newPayAmount]); + $this->success('请完成增项补差价微信支付', $res); } public function index() @@ -1230,108 +1168,33 @@ class Order extends Base } /** - * 报价订单:用户选择师傅报价(选定后订单进入已接单) + * 报价订单:用户选择师傅报价并发起微信支付托管(付款确认后订单自动锁定师傅并进入已接单) * @ApiMethod (POST) */ public function chooseQuote() { $orderId = (int)$this->request->post('order_id'); $quoteId = (int)$this->request->post('quote_id'); - $order = $this->ownOrder($orderId, 'user'); + $wxCode = trim((string)$this->request->post('wx_code', '')); + $order = $this->ownOrder($orderId, 'user'); - if ($order['price_type'] !== 'quote') { - $this->error('该订单不是师傅报价订单'); - } - if ((int)$order['status'] !== OrderStatus::QUOTING) { - $this->error('当前状态(' . OrderStatus::label((int)$order['status']) . ')不可选择报价'); - } - if ((int)$order['master_id'] > 0) { - $this->error('已选定师傅,请勿重复选择'); - } - - $quote = Db::name('order_quote')->where('id', $quoteId)->where('order_id', $orderId)->find(); - if (!$quote) { - $this->error('报价不存在'); - } - if ($quote['status'] !== 'wait') { - $this->error('该报价已失效,请选择其他师傅'); - } - if ((int)$quote['expire_time'] > 0 && (int)$quote['expire_time'] < time()) { - Db::name('order_quote')->where('id', $quoteId)->update(['status' => 'expired']); - $this->error('该报价已过期,请选择其他师傅'); - } - - $master = Db::name('master')->where('id', (int)$quote['master_id'])->find(); - if (!$master) { - $this->error('师傅信息不存在'); - } - - $quoteAmount = round((float)$quote['amount'], 2); - $payAmount = max(0, round($quoteAmount - (float)$order['discount_amount'], 2)); - $alreadyPaid = (int)$order['pay_status'] === 1 ? (float)$order['pay_amount'] : 0.0; - $needDiff = max(0, round($payAmount - $alreadyPaid, 2)); - - $payType = $this->request->post('pay_type', 'wechat'); - $remark = '客户已选择 ' . $master['realname'] . ' 的报价 ¥' . $quoteAmount; - if ($needDiff > 0) { - $remark .= '(补付差额 ¥' . $needDiff . ')'; - } - $remark .= ',等待师傅联系'; - - Db::startTrans(); try { - $lockedOrder = Db::name('order')->where('id', $orderId)->lock(true)->find(); - if (!$lockedOrder || (int)$lockedOrder['status'] !== OrderStatus::QUOTING || (int)$lockedOrder['master_id'] !== 0) { - throw new \InvalidArgumentException('该工单已选定师傅或状态已变更,请刷新重试'); - } - $quoteAmount = round((float)$quote['amount'], 2); - $payAmount = max(0, round($quoteAmount - (float)$lockedOrder['discount_amount'], 2)); - $alreadyPaid = (int)$lockedOrder['pay_status'] === 1 ? (float)$lockedOrder['pay_amount'] : 0.0; - $needDiff = max(0, round($payAmount - $alreadyPaid, 2)); - - $updateData = [ - 'master_id' => (int)$quote['master_id'], - 'accept_time' => time(), - 'base_amount' => $quoteAmount, - 'floor_amount' => 0, - 'urgent_amount' => 0, - 'total_amount' => $quoteAmount, - 'pay_amount' => $payAmount, - 'pay_status' => 1, - ]; - if ($needDiff > 0) { - $updateData['pay_type'] = $payType; - $updateData['pay_time'] = time(); - } - $this->transit($lockedOrder, OrderStatus::ACCEPTED, $remark, $updateData, 'user', $this->uid()); - - Db::name('order_quote')->where('id', $quoteId)->update(['status' => 'accept']); - Db::name('order_quote')->where('order_id', $orderId)->where('id', '<>', $quoteId) - ->where('status', 'wait')->update(['status' => 'reject']); - - $this->notify('user', (int)$lockedOrder['user_id'], '已选定服务师傅', $remark, 'order', $orderId); - $this->notify('master', (int)$master['id'], '您的报价已被选中', '订单 ' . $lockedOrder['order_no'] . ' 客户已选择您的报价 ¥' . $quoteAmount . ',请尽快联系客户', 'order', $orderId); - - Db::commit(); + $res = PaymentService::createOrderJsapiPay($order, $this->uid(), 'quote', $quoteId, $wxCode); } catch (\think\exception\HttpResponseException $e) { - Db::rollback(); throw $e; } catch (\InvalidArgumentException $e) { - Db::rollback(); $this->error($e->getMessage()); return; - } catch (\Exception $e) { - Db::rollback(); + } catch (\Throwable $e) { \think\Log::error('[Order::chooseQuote] ' . $e->getMessage()); - $this->error('选择报价失败:' . $e->getMessage()); + $this->error('选择报价并发起支付失败:' . $e->getMessage()); return; } - $this->success('已选定师傅并完成支付托管,请等待师傅联系', [ - 'pay_amount' => $payAmount, - 'need_diff' => $needDiff, - 'already_pay' => $alreadyPaid, - ]); + $msg = !empty($res['paid']) + ? '已选定师傅并完成支付托管,请等待师傅联系' + : '请完成报价托管微信支付'; + $this->success($msg, $res); } /** @@ -1377,7 +1240,7 @@ class Order extends Base } /** - * 取消订单(已支付则模拟退款) + * 取消订单(已支付则发起微信原路退款) * @ApiMethod (POST) */ public function cancel() @@ -1396,6 +1259,18 @@ class Order extends Base $this->error('该订单已开具发票,如需取消退款请联系客服办理发票冲红'); } + // 若订单已支付,先在数据库事务外部调用微信支付 APIv3 逐笔发起原路退款(无有效流水严格拦截,严禁静默跳过) + $refundedAmount = 0.00; + if ((int)$order['pay_status'] === 1) { + try { + $refundedAmount = PaymentService::refundOrder($orderId, '用户取消:' . $reason); + } catch (\Throwable $re) { + \think\Log::error('[Order::cancel refund] order_id=' . $orderId . ' reason=' . $re->getMessage()); + $this->error('申请微信原路退款失败:' . $re->getMessage()); + return; + } + } + $prevMasterId = 0; Db::startTrans(); try { @@ -1407,7 +1282,7 @@ class Order extends Base $extra = ['cancel_reason' => mb_substr($reason, 0, 255)]; if ((int)$lockedOrder['pay_status'] === 1) { $extra['pay_status'] = 2; - $extra['refund_amount'] = $lockedOrder['pay_amount']; + $extra['refund_amount'] = $refundedAmount > 0 ? $refundedAmount : (float)$lockedOrder['pay_amount']; } $this->transit($lockedOrder, OrderStatus::CANCELLED, '用户取消:' . $reason, $extra, 'user', $this->uid()); @@ -1467,9 +1342,10 @@ class Order extends Base $masterId = (int)$order['master_id']; $payAmount = (float)$order['pay_amount']; - $rate = 10.00; - $commission = round($payAmount * $rate / 100, 2); - $settleAmount = round($payAmount - $commission, 2); + $settleCalc = CommissionService::calcByOrder($order); + $rate = $settleCalc['commission_rate']; + $commission = $settleCalc['commission']; + $settleAmount = $settleCalc['settle_amount']; Db::startTrans(); try { @@ -1479,8 +1355,10 @@ class Order extends Base } $masterId = (int)$lockedOrder['master_id']; $payAmount = (float)$lockedOrder['pay_amount']; - $commission = round($payAmount * $rate / 100, 2); - $settleAmount = round($payAmount - $commission, 2); + $settleCalc = CommissionService::calcByOrder($lockedOrder); + $rate = $settleCalc['commission_rate']; + $commission = $settleCalc['commission']; + $settleAmount = $settleCalc['settle_amount']; $this->transit($lockedOrder, OrderStatus::COMPLETED, '客户验收通过,订单完成', [ 'verify_time' => time(), @@ -1694,14 +1572,22 @@ class Order extends Base $count = 0; foreach ($rows as $order) { $orderId = (int)$order['id']; - $masterId = (int)$order['master_id']; - $payAmount = (float)$order['pay_amount']; - $rate = 10.00; - $commission = round($payAmount * $rate / 100, 2); - $settleAmount = round($payAmount - $commission, 2); Db::startTrans(); try { + $lockedOrder = Db::name('order')->where('id', $orderId)->lock(true)->find(); + if (!$lockedOrder || (int)$lockedOrder['status'] !== OrderStatus::PENDING_VERIFY || (int)$lockedOrder['settle_status'] === 1) { + Db::rollback(); + continue; + } + + $masterId = (int)$lockedOrder['master_id']; + $payAmount = (float)$lockedOrder['pay_amount']; + $settleCalc = CommissionService::calcByOrder($lockedOrder); + $rate = $settleCalc['commission_rate']; + $commission = $settleCalc['commission']; + $settleAmount = $settleCalc['settle_amount']; + Db::name('order')->where('id', $orderId)->update([ 'status' => OrderStatus::COMPLETED, 'verify_time' => time(), @@ -1798,6 +1684,7 @@ class Order extends Base { $uid = $this->uid(); $catId = (int)$this->request->param('cat_id', 0); + $preferCatId = (int)$this->request->param('prefer_cat_id', 0); $keyword = trim((string)$this->request->param('keyword', '')); $cats = Db::name('service_cat') @@ -1824,7 +1711,7 @@ class Order extends Base $seenUrls = []; foreach ($libRows as $r) { $rawImg = trim((string)$r['image']); - if ($rawImg === '') { + if ($rawImg === '' || $this->isSystemDefaultImageUrl($rawImg)) { continue; } $fullUrl = strpos($rawImg, 'http') === 0 ? $rawImg : cdnurl($rawImg, true); @@ -1834,7 +1721,7 @@ class Order extends Base 'id' => (int)$r['id'], 'cat_id' => $cid, 'cat_name' => isset($catNameMap[$cid]) ? $catNameMap[$cid] : '通用商品', - 'title' => (string)($r['title'] ?: '我的商品图'), + 'title' => (string)($r['title'] ?: '历史上传商品'), 'image_url' => $fullUrl, 'source' => 'library', 'can_delete' => 1, @@ -1883,8 +1770,8 @@ class Order extends Base } foreach ($candidates as $cand) { $rawIm = $cand['img']; - // 排除 SVG 图标与系统图库静态参考图,仅保留用户真实上传图 - if (substr(strtolower($rawIm), -4) === '.svg' || strpos($rawIm, '/gallery/') !== false || strpos($rawIm, '/goods/') !== false) { + // 排除 SVG 图标与系统图库静态参考图,仅保留用户真实上传过的历史商品图 + if ($this->isSystemDefaultImageUrl($rawIm)) { continue; } if ($keyword !== '' && mb_strpos($cand['title'], $keyword) === false) { @@ -1902,19 +1789,120 @@ class Order extends Base 'cat_name' => isset($catNameMap[$cid]) ? $catNameMap[$cid] : '历史订单', 'title' => $cand['title'], 'image_url' => $fullUrl, - 'source' => 'history', + 'source' => 'order', 'can_delete' => 0, 'time_text' => $ho['createtime'] ? date('Y-m-d', (int)$ho['createtime']) : '', ]; } } + if ($catId === 0 && $preferCatId > 0 && count($list) > 1) { + usort($list, function ($a, $b) use ($preferCatId) { + $aMatch = ((int)$a['cat_id'] === $preferCatId) ? 1 : 0; + $bMatch = ((int)$b['cat_id'] === $preferCatId) ? 1 : 0; + if ($aMatch !== $bMatch) { + return $bMatch <=> $aMatch; + } + return 0; + }); + } + $this->success('', [ 'cats' => $cats, 'list' => $list, ]); } + /** + * 判断图片是否为平台系统默认参考图(避免将系统默认图混入用户个人的历史商品上传图库) + */ + protected function isSystemDefaultImageUrl($url) + { + $u = strtolower(trim((string)$url)); + if ($u === '' || substr($u, -4) === '.svg') { + return true; + } + if (strpos($u, '/gallery/') !== false || strpos($u, '/goods/') !== false || strpos($u, '/uploads/service/') !== false || strpos($u, '/uploads/services/') !== false) { + return true; + } + return false; + } + + /** + * 订单创建成功后将用户真实上传的商品图片自动沉淀到「历史商品上传图片库」 + */ + protected function archiveOrderUploadedGoodsImages($uid, $defaultCatId, $defaultTitle, $goodsList, $rawGoodsImages) + { + $uid = (int)$uid; + if ($uid <= 0) { + return; + } + $entries = []; + if (!empty($goodsList) && is_array($goodsList)) { + foreach ($goodsList as $g) { + if (!is_array($g)) { + continue; + } + $gCatId = !empty($g['cat_id']) ? (int)$g['cat_id'] : (int)$defaultCatId; + $gTitle = !empty($g['custom_name']) ? trim((string)$g['custom_name']) : (!empty($g['item_name']) ? trim((string)$g['item_name']) : trim((string)$defaultTitle)); + if (!empty($g['spec_name']) && $g['spec_name'] !== $gTitle) { + $gTitle .= '(' . trim((string)$g['spec_name']) . ')'; + } + $imgs = isset($g['images']) && is_array($g['images']) ? $g['images'] : []; + foreach ($imgs as $im) { + if (is_string($im) && !$this->isSystemDefaultImageUrl($im)) { + $entries[] = ['cat_id' => $gCatId, 'title' => mb_substr($gTitle ?: '历史上传商品', 0, 80), 'url' => trim($im)]; + } + } + } + } elseif (!empty($rawGoodsImages)) { + $arr = is_array($rawGoodsImages) ? $rawGoodsImages : (json_decode((string)$rawGoodsImages, true) ?: explode(',', (string)$rawGoodsImages)); + foreach ($arr as $im) { + if (is_string($im) && !$this->isSystemDefaultImageUrl($im)) { + $entries[] = ['cat_id' => (int)$defaultCatId, 'title' => mb_substr(trim((string)$defaultTitle) ?: '历史上传商品', 0, 80), 'url' => trim($im)]; + } + } + } + + if (empty($entries)) { + return; + } + $now = time(); + foreach ($entries as $ent) { + $rawU = $ent['url']; + if ($rawU === '' || strlen($rawU) > 255) { + continue; + } + $fullUrl = strpos($rawU, 'http') === 0 ? $rawU : cdnurl($rawU, true); + $exists = Db::name('user_goods_gallery') + ->where('user_id', $uid) + ->where('image', 'in', [$rawU, $fullUrl]) + ->value('id'); + if ($exists) { + continue; + } + Db::name('user_goods_gallery')->insert([ + 'user_id' => $uid, + 'cat_id' => max(0, (int)$ent['cat_id']), + 'title' => $ent['title'], + 'image' => $fullUrl, + 'createtime' => $now, + ]); + } + } + + /** + * 安全读取兼容 JSON 数组与字符串的 POST 参数(规避 ThinkPHP 5 无 /a 修饰符时对数组抛类型异常) + */ + protected function postMixed($key, $default = null) + { + $all = $this->request->post(false); + if (is_array($all) && array_key_exists($key, $all)) { + return $all[$key]; + } + return $default; + } + /** * 上传保存商品图至个人专属商品图库 * @ApiMethod (POST) @@ -1925,7 +1913,7 @@ class Order extends Base $catId = max(0, (int)$this->request->post('cat_id', 0)); $title = mb_substr(trim((string)$this->request->post('title', '')), 0, 80); $singleImg = trim((string)$this->request->post('image', '')); - $rawImages = $this->request->post('images', '', null); + $rawImages = $this->postMixed('images', ''); $imgList = []; if ($singleImg !== '') { @@ -1944,13 +1932,17 @@ class Order extends Base } } + if (empty($imgList)) { + $this->error('请上传有效的商品图片'); + } + $validUrls = []; foreach ($imgList as $u) { if (!is_string($u)) { continue; } $u = trim($u); - if ($u === '' || strlen($u) > 255 || substr(strtolower($u), -4) === '.svg') { + if ($u === '' || strlen($u) > 255 || $this->isSystemDefaultImageUrl($u)) { continue; } if (strpos($u, 'http://') === 0 || strpos($u, 'https://') === 0 || strpos($u, '/') === 0) { @@ -1959,7 +1951,7 @@ class Order extends Base } $validUrls = array_values(array_unique($validUrls)); if (empty($validUrls)) { - $this->error('请上传有效的商品图片'); + $this->success('系统默认图片无需重复保存到历史图库', ['added' => 0, 'list' => []]); } if ($title === '' && $catId > 0) { @@ -1971,6 +1963,7 @@ class Order extends Base $now = time(); $saved = []; + $addedCount = 0; foreach ($validUrls as $u) { $fullUrl = strpos($u, 'http') === 0 ? $u : cdnurl($u, true); $exists = Db::name('user_goods_gallery') @@ -1995,6 +1988,7 @@ class Order extends Base 'image' => $fullUrl, 'createtime' => $now, ]); + $addedCount++; $saved[] = [ 'id' => $newId, 'cat_id' => $catId, @@ -2005,7 +1999,7 @@ class Order extends Base ]; } - $this->success('已存入商品图库', ['list' => $saved]); + $this->success('已存入商品图库', ['added' => $addedCount, 'list' => $saved]); } /** @@ -2016,7 +2010,7 @@ class Order extends Base { $uid = $this->uid(); $singleId = (int)$this->request->post('id', 0); - $rawIds = $this->request->post('ids', '', null); + $rawIds = $this->postMixed('ids', ''); $idArr = []; if ($singleId > 0) { @@ -2249,7 +2243,7 @@ class Order extends Base $tplName = $itemName . ($specName !== '' && $specName !== $itemName ? (' · ' . $specName) : ''); } - $rawExtras = $this->request->post('extras', '', null); + $rawExtras = $this->postMixed('extras', ''); $extrasArr = []; if (!empty($rawExtras)) { $decEx = is_array($rawExtras) ? $rawExtras : json_decode(htmlspecialchars_decode((string)$rawExtras), true); @@ -2268,7 +2262,7 @@ class Order extends Base } } - $rawImages = $this->request->post('images', '', null); + $rawImages = $this->postMixed('images', ''); $imagesArr = []; if (!empty($rawImages)) { $decIm = is_array($rawImages) ? $rawImages : json_decode(htmlspecialchars_decode((string)$rawImages), true); diff --git a/server/application/api/controller/Sms.php b/server/application/api/controller/Sms.php index a5010cb..4971a00 100755 --- a/server/application/api/controller/Sms.php +++ b/server/application/api/controller/Sms.php @@ -3,12 +3,9 @@ namespace app\api\controller; use app\common\controller\Api; -use app\common\library\Sms as Smslib; -use app\common\model\User; -use think\Hook; /** - * 手机短信接口 + * 手机短信接口(短信验证码通道已下线) */ class Sms extends Api { @@ -16,89 +13,20 @@ class Sms extends Api protected $noNeedRight = '*'; /** - * 发送验证码 - * + * 发送验证码(已移除) * @ApiMethod (POST) - * @ApiParams (name="mobile", type="string", required=true, description="手机号") - * @ApiParams (name="event", type="string", required=true, description="事件名称") */ public function send() { - $mobile = $this->request->post("mobile"); - $event = $this->request->post("event"); - $event = $event ? $event : 'register'; - - if (!$mobile || !\think\Validate::regex($mobile, "^1\d{10}$")) { - $this->error(__('手机号不正确')); - } - $last = Smslib::get($mobile, $event); - if ($last && time() - $last['createtime'] < 60) { - $this->error(__('发送频繁')); - } - $ipSendTotal = \app\common\model\Sms::where(['ip' => $this->request->ip()])->whereTime('createtime', '-1 hours')->count(); - if ($ipSendTotal >= 5) { - $this->error(__('发送频繁')); - } - if ($event) { - $userinfo = User::getByMobile($mobile); - if ($event == 'register' && $userinfo) { - //已被注册 - $this->error(__('已被注册')); - } elseif (in_array($event, ['changemobile']) && $userinfo) { - //被占用 - $this->error(__('已被占用')); - } elseif (in_array($event, ['changepwd', 'resetpwd']) && !$userinfo) { - //未注册 - $this->error(__('未注册')); - } - } - if (!Hook::get('sms_send')) { - $this->error(__('请在后台插件管理安装短信验证插件')); - } - $ret = Smslib::send($mobile, null, $event); - if ($ret) { - $this->success(__('发送成功')); - } else { - $this->error(__('发送失败,请检查短信配置是否正确')); - } + $this->error('短信验证码通道已移除,请使用手机号密码或微信一键登录'); } /** - * 检测验证码 - * + * 检测验证码(已移除) * @ApiMethod (POST) - * @ApiParams (name="mobile", type="string", required=true, description="手机号") - * @ApiParams (name="event", type="string", required=true, description="事件名称") - * @ApiParams (name="captcha", type="string", required=true, description="验证码") */ public function check() { - $mobile = $this->request->post("mobile"); - $event = $this->request->post("event"); - $event = $event ? $event : 'register'; - $captcha = $this->request->post("captcha"); - - if (!$mobile || !\think\Validate::regex($mobile, "^1\d{10}$")) { - $this->error(__('手机号不正确')); - } - if ($event) { - $userinfo = User::getByMobile($mobile); - if ($event == 'register' && $userinfo) { - //已被注册 - $this->error(__('已被注册')); - } elseif (in_array($event, ['changemobile']) && $userinfo) { - //被占用 - $this->error(__('已被占用')); - } elseif (in_array($event, ['changepwd', 'resetpwd']) && !$userinfo) { - //未注册 - $this->error(__('未注册')); - } - } - $ret = Smslib::check($mobile, $captcha, $event); - if ($ret) { - $this->success(__('成功')); - } else { - $this->error(__('验证码不正确')); - } + $this->error('短信验证码通道已移除,请使用手机号密码或微信一键登录'); } } diff --git a/server/application/api/controller/User.php b/server/application/api/controller/User.php index df6c025..e3720d1 100755 --- a/server/application/api/controller/User.php +++ b/server/application/api/controller/User.php @@ -4,8 +4,6 @@ namespace app\api\controller; use app\common\controller\Api; use app\common\library\Ems; -use app\common\library\Sms; -use fast\Random; use think\Config; use think\Validate; @@ -59,82 +57,23 @@ class User extends Api } /** - * 手机验证码登录 + * 手机验证码登录(已移除) * * @ApiMethod (POST) - * @ApiParams (name="mobile", type="string", required=true, description="手机号") - * @ApiParams (name="captcha", type="string", required=true, description="验证码") */ public function mobilelogin() { - $mobile = $this->request->post('mobile'); - $captcha = $this->request->post('captcha'); - if (!$mobile || !$captcha) { - $this->error(__('Invalid parameters')); - } - if (!Validate::regex($mobile, "^1\d{10}$")) { - $this->error(__('Mobile is incorrect')); - } - if (!Sms::check($mobile, $captcha, 'mobilelogin', true)) { - $this->error(__('Captcha is incorrect')); - } - $user = \app\common\model\User::getByMobile($mobile); - if ($user) { - if ($user->status != 'normal') { - $this->error(__('Account is locked')); - } - //如果已经有账号则直接登录 - $ret = $this->auth->direct($user->id); - } else { - $ret = $this->auth->register($mobile, Random::alnum(), '', $mobile, []); - } - if ($ret) { - Sms::flush($mobile, 'mobilelogin'); - $data = ['userinfo' => $this->auth->getUserinfo()]; - $this->success(__('Logged in successful'), $data); - } else { - $this->error($this->auth->getError()); - } + $this->error('短信验证码通道已移除,请使用手机号密码或微信一键登录'); } /** - * 注册会员 + * 注册会员(短信验证通道已移除) * * @ApiMethod (POST) - * @ApiParams (name="username", type="string", required=true, description="用户名") - * @ApiParams (name="password", type="string", required=true, description="密码") - * @ApiParams (name="email", type="string", required=true, description="邮箱") - * @ApiParams (name="mobile", type="string", required=true, description="手机号") - * @ApiParams (name="code", type="string", required=true, description="验证码") */ public function register() { - $username = $this->request->post('username'); - $password = $this->request->post('password'); - $email = $this->request->post('email'); - $mobile = $this->request->post('mobile'); - $code = $this->request->post('code'); - if (!$username || !$password) { - $this->error(__('Invalid parameters')); - } - if ($email && !Validate::is($email, "email")) { - $this->error(__('Email is incorrect')); - } - if ($mobile && !Validate::regex($mobile, "^1\d{10}$")) { - $this->error(__('Mobile is incorrect')); - } - $ret = Sms::check($mobile, $code, 'register', true); - if (!$ret) { - $this->error(__('Captcha is incorrect')); - } - $ret = $this->auth->register($username, $password, $email, $mobile, []); - if ($ret) { - Sms::flush($mobile, 'register'); - $data = ['userinfo' => $this->auth->getUserinfo()]; - $this->success(__('Sign up successful'), $data); - } else { - $this->error($this->auth->getError()); - } + $this->error('短信验证码通道已移除,请使用手机号密码或微信一键登录'); } /** @@ -229,38 +168,13 @@ class User extends Api } /** - * 修改手机号 + * 修改手机号(短信验证通道已移除) * * @ApiMethod (POST) - * @ApiParams (name="mobile", type="string", required=true, description="手机号") - * @ApiParams (name="captcha", type="string", required=true, description="验证码") */ public function changemobile() { - $user = $this->auth->getUser(); - $mobile = $this->request->post('mobile'); - $captcha = $this->request->post('captcha'); - if (!$mobile || !$captcha) { - $this->error(__('Invalid parameters')); - } - if (!Validate::regex($mobile, "^1\d{10}$")) { - $this->error(__('Mobile is incorrect')); - } - if (\app\common\model\User::where('mobile', $mobile)->where('id', '<>', $user->id)->find()) { - $this->error(__('Mobile already exists')); - } - $result = Sms::check($mobile, $captcha, 'changemobile', true); - if (!$result) { - $this->error(__('Captcha is incorrect')); - } - $verification = $user->verification; - $verification->mobile = 1; - $user->verification = $verification; - $user->mobile = $mobile; - $user->save(); - - Sms::flush($mobile, 'changemobile'); - $this->success(); + $this->error('短信验证码通道已移除,暂不支持短信换绑手机号'); } /** @@ -318,18 +232,7 @@ class User extends Api $this->error(__('Password must be 6 to 30 characters')); } if ($type == 'mobile') { - if (!Validate::regex($mobile, "^1\d{10}$")) { - $this->error(__('Mobile is incorrect')); - } - $user = \app\common\model\User::getByMobile($mobile); - if (!$user) { - $this->error(__('User not found')); - } - $ret = Sms::check($mobile, $captcha, 'resetpwd', true); - if (!$ret) { - $this->error(__('Captcha is incorrect')); - } - Sms::flush($mobile, 'resetpwd'); + $this->error('短信验证码通道已移除,暂不支持手机短信找回密码'); } else { if (!Validate::is($email, "email")) { $this->error(__('Email is incorrect')); diff --git a/server/application/api/controller/Validate.php b/server/application/api/controller/Validate.php index 6538f8d..bd6c405 100755 --- a/server/application/api/controller/Validate.php +++ b/server/application/api/controller/Validate.php @@ -124,22 +124,13 @@ class Validate extends Api } /** - * 检测手机验证码 + * 检测手机验证码(已移除) * * @ApiMethod (POST) - * @ApiParams (name="mobile", type="string", required=true, description="手机号") - * @ApiParams (name="captcha", type="string", required=true, description="验证码") - * @ApiParams (name="event", type="string", required=true, description="事件") */ public function check_sms_correct() { - $mobile = $this->request->post('mobile'); - $captcha = $this->request->post('captcha'); - $event = $this->request->post('event'); - if (!\app\common\library\Sms::check($mobile, $captcha, $event)) { - $this->error(__('验证码不正确')); - } - $this->success(); + $this->error('短信验证码通道已移除'); } /** diff --git a/server/application/api/controller/Wxpay.php b/server/application/api/controller/Wxpay.php new file mode 100644 index 0000000..6f8e1c8 --- /dev/null +++ b/server/application/api/controller/Wxpay.php @@ -0,0 +1,140 @@ +parseAndVerifyCallback(); + $eventType = strtoupper($parsed['event_type']); + $data = $parsed['data']; + + if ($eventType === 'TRANSACTION.SUCCESS' && !empty($data['out_trade_no'])) { + PaymentService::confirmOrderPay((string)$data['out_trade_no'], $data); + } + return $this->wxResponse(true); + } catch (\Throwable $e) { + Log::error('[Wxpay::notify] ' . $e->getMessage()); + return $this->wxResponse(false, $e->getMessage()); + } + } + + /** + * 2. 微信原路退款结果异步通知 (/api/wxpay/refundNotify) + */ + public function refundNotify() + { + try { + $parsed = $this->parseAndVerifyCallback(); + $data = $parsed['data']; + $outRefundNo = trim((string)($data['out_refund_no'] ?? '')); + $outTradeNo = trim((string)($data['out_trade_no'] ?? '')); + $refStatus = strtoupper((string)($data['refund_status'] ?? '')); + + if ($outRefundNo === '' && $outTradeNo === '') { + throw new \RuntimeException('退款回调缺少商户退款单号与商户支付单号'); + } + + $q = Db::name('pay_order'); + if ($outRefundNo !== '') { + $q->where('refund_no', $outRefundNo); + } else { + $q->where('out_trade_no', $outTradeNo); + } + $po = $q->find(); + if (!$po) { + Log::error("[Wxpay::refundNotify] 未找到本地支付流水单 out_refund_no={$outRefundNo}, out_trade_no={$outTradeNo}"); + throw new \RuntimeException('未匹配到本地支付交易流水单'); + } + + try { + PaymentService::applyWxRefundResult($po, $data); + } catch (\Throwable $refundEx) { + // 若微信已下发明确退款异常/关闭终态,系统已完整落库记录原因并写入告警日志,向微信回传成功避免重复轰炸 + if (in_array($refStatus, ['ABNORMAL', 'CLOSED'], true)) { + Log::error("[Wxpay::refundNotify] 退款终态异常已落库 out_trade_no={$po['out_trade_no']}, status={$refStatus}, reason=" . $refundEx->getMessage()); + return $this->wxResponse(true); + } + throw $refundEx; + } + + return $this->wxResponse(true); + } catch (\Throwable $e) { + Log::error('[Wxpay::refundNotify] ' . $e->getMessage()); + return $this->wxResponse(false, $e->getMessage()); + } + } + + /** + * 3. 微信新版「商家转账」终态结果异步通知 (/api/wxpay/transferNotify) + */ + public function transferNotify() + { + try { + $parsed = $this->parseAndVerifyCallback(); + $data = $parsed['data']; + $outBillNo = trim((string)($data['out_bill_no'] ?? '')); + + if ($outBillNo === '') { + throw new \RuntimeException('商家转账回调缺少商户转账单号 out_bill_no'); + } + + $wd = Db::name('withdraw')->where('out_bill_no', $outBillNo)->find(); + if (!$wd) { + Log::error("[Wxpay::transferNotify] 未找到本地提现单记录 out_bill_no={$outBillNo}"); + throw new \RuntimeException('未找到匹配的本地提现单'); + } + PaymentService::syncWithdrawTransfer((int)$wd['id'], $data); + return $this->wxResponse(true); + } catch (\Throwable $e) { + Log::error('[Wxpay::transferNotify] ' . $e->getMessage()); + return $this->wxResponse(false, $e->getMessage()); + } + } + + /** + * 读取原始报文并使用微信支付公钥验签 + AES-256-GCM 解密 + */ + protected function parseAndVerifyCallback() + { + $rawBody = file_get_contents('php://input'); + if ($rawBody === false || $rawBody === '') { + $rawBody = (string)$this->request->getInput(); + } + $headers = $this->request->header(); + $wxSvc = new WxPayV3Service(); + return $wxSvc->verifyAndDecryptNotify((string)$rawBody, is_array($headers) ? $headers : []); + } + + /** + * 输出符合微信支付 APIv3 规范的 HTTP 响应 + */ + protected function wxResponse($ok, $msg = '') + { + $code = $ok ? 200 : 500; + $body = [ + 'code' => $ok ? 'SUCCESS' : 'FAIL', + 'message' => $ok ? '成功' : ($msg ?: '处理失败'), + ]; + return Response::create($body, 'json', $code); + } +} diff --git a/server/application/common/library/AgreementService.php b/server/application/common/library/AgreementService.php new file mode 100644 index 0000000..0cc180f --- /dev/null +++ b/server/application/common/library/AgreementService.php @@ -0,0 +1,1218 @@ + 'user_service', + 'service' => 'user_service', + 'user_service' => 'user_service', + 'privacy' => 'privacy', + 'master' => 'master', + 'merchant' => 'merchant', + ]; + if (isset($aliasMap[$type])) { + return $aliasMap[$type]; + } + // 支持自定义合法编码(仅允许小写字母/数字/下划线) + return preg_match('/^[a-z0-9_]{2,32}$/', $type) ? $type : 'user_service'; + } + + /** + * 返回默认协议字典定义(按权重降序) + */ + public static function defaultDefinitions(): array + { + $today = date('Y-m-d'); + return [ + 'user_service' => [ + 'code' => 'user_service', + 'legacy_key' => 'agreement_service', + 'tab_name' => '用户服务协议', + 'desc' => '在小程序登录页、个人中心「协议与规则」中展示', + 'title' => '优匠优服用户服务协议', + 'version' => 'v1.0', + 'effective_date' => $today, + 'weigh' => 100, + 'status' => 'normal', + 'content' => '

      一、平台定位与服务范围

      1. 优匠优服(以下简称“本平台”)依法为个人业主、企业商家与认证技能师傅提供上门安装、维修、拆旧及维保信息撮合与交易资金托管服务。

      2. 您在注册、登录或下单使用本平台服务前,应当仔细阅读并充分理解本协议全部条款。

      二、下单与计费准则

      1. 一口价项目按平台页面公示标准计费;师傅报价订单由接单师傅根据现场工况公开报价,经客户确认并支付后生效;自主定价订单按客户确认的订单金额执行。

      2. 如施工现场存在特殊工况需增项补差,须经双方提前沟通一致,并由师傅在工单内发起规范补差申请,客户核实无误后通过平台支付。

      三、验收、质保与资金结算

      1. 订单支付资金由平台合规托管,师傅完成施工并上传现场完工照片后,经客户验收确认(或向师傅提供 6 位核销码完成核验)后触发结算。

      2. 为保障您的售后权益、施工质保与资金安全,请全程在平台内完成沟通、下单、补差与支付,切勿脱离平台私下转账交易。

      ', + ], + 'privacy' => [ + 'code' => 'privacy', + 'legacy_key' => 'agreement_privacy', + 'tab_name' => '隐私保护指引', + 'desc' => '在小程序登录页、个人中心「协议与规则」中展示', + 'title' => '优匠优服隐私保护指引', + 'version' => 'v1.0', + 'effective_date' => $today, + 'weigh' => 90, + 'status' => 'normal', + 'content' => '

      一、信息收集范围与使用目的

      1. 本平台遵循“合法、正当、必要、诚信”原则收集和使用您的个人信息,仅在为您提供同城上门安装、维修服务所必需的场景下收集相关数据。

      2. 账号与联系信息:在您授权登录或下单时,我们需要收集您的手机号码及联系人称呼,用于账号识别、订单通知及师傅上门沟通。

      3. 服务地址与位置信息:在您创建工单或管理地址簿时,我们需要收集上门服务地址及定位信息,以便为您匹配就近认证师傅并核验服务范围。

      二、最小必要与脱敏保护原则

      1. 您的真实手机号码、门牌号等敏感联系信息,仅对已正式接单履约的当单认证师傅可见;订单未被接单前及非当单人员均无法获取,平台全程采用脱敏展示。

      三、数据安全与第三方共享限制

      1. 本平台全链路采用加密传输与存储技术,严格管控内部数据访问权限,绝不向无关第三方共享或出售您的个人信息。

      四、您的个人权利

      您可随时在小程序「个人中心」查询、修改个人资料及管理服务地址,或联系平台客服咨询、撤回授权。

      ', + ], + 'master' => [ + 'code' => 'master', + 'legacy_key' => 'agreement_master', + 'tab_name' => '师傅入驻协议', + 'desc' => '在师傅实名入驻申请页中展示', + 'title' => '师傅入驻与履约服务协议', + 'version' => 'v1.0', + 'effective_date' => $today, + 'weigh' => 80, + 'status' => 'normal', + 'content' => '

      一、实名认证与技能核验

      1. 申请入驻的师傅须提交本人真实有效身份证件及服务技能信息,严禁冒用他人资质。

      二、接单与上门履约规范

      1. 接单后须在平台规定时效内联系客户确认上门时间,准时到达现场并规范施工。

      三、收费透明与严禁私单

      1. 如需现场增项补差,必须提前向客户说明并在工单内发起补差申请,严禁私自索要额外费用。

      四、完工留痕与结算提现

      1. 施工完成后须上传真实清晰的现场完工照片,凭客户验收或核销码触发系统结算。

      ', + ], + 'merchant' => [ + 'code' => 'merchant', + 'legacy_key' => 'agreement_merchant', + 'tab_name' => '商家协同协议', + 'desc' => '在企业商家用户的「协议与规则」中展示', + 'title' => '企业商家入驻与协同服务协议', + 'version' => 'v1.0', + 'effective_date' => $today, + 'weigh' => 70, + 'status' => 'normal', + 'content' => '

      一、企业主体与资质真实性

      1. 申请入驻的企业或商户须提供真实有效的营业执照与主体信息。

      二、账号协同与权限管理

      1. 主账号可绑定企业协同成员共同发起批量工单与跟进履约,并对企业关联工单承担管理责任。

      三、对账结算与发票服务

      1. 平台按实际支付与完工订单汇总企业消费明细,支持在线申请正规增值税普通发票或专用发票。

      ', + ], + ]; + } + + /** + * 读取后台管理页面所需的全部协议集合(按默认顺序与数据库合并) + */ + public static function getAllForAdmin(): array + { + $defs = self::defaultDefinitions(); + $dbRows = []; + try { + $rows = Db::name('agreement') + ->field('id,code,title,version,effective_date,content,revision,prev_content,diff_content,diff_summary,status,weigh,createtime,updatetime') + ->order('weigh desc, id asc') + ->select(); + if (is_array($rows)) { + foreach ($rows as $row) { + $dbRows[$row['code']] = $row; + } + } + } catch (\Throwable $e) { + $dbRows = []; + } + + $result = []; + foreach ($defs as $code => $def) { + $row = $dbRows[$code] ?? null; + $legacyContent = ''; + if (!empty($def['legacy_key'])) { + $cfgVal = Config::get('site.' . $def['legacy_key']); + if (is_string($cfgVal) && trim($cfgVal) !== '') { + $legacyContent = self::plainTextToHtml(trim($cfgVal)); + } + } + $content = $row && trim((string)$row['content']) !== '' + ? (string)$row['content'] + : ($legacyContent !== '' ? $legacyContent : $def['content']); + $updatetime = $row && !empty($row['updatetime']) ? (int)$row['updatetime'] : time(); + $revision = $row && !empty($row['revision']) ? max(1, (int)$row['revision']) : 1; + $diffContent = $row ? (string)($row['diff_content'] ?? '') : ''; + $diffSummary = $row ? (string)($row['diff_summary'] ?? '') : ''; + $hasDiff = $revision > 1 && trim($diffContent) !== ''; + + $result[] = [ + 'id' => $row ? (int)$row['id'] : 0, + 'code' => $code, + 'tab_name' => $def['tab_name'], + 'desc' => $def['desc'], + 'is_core' => in_array($code, self::CORE_CODES, true), + 'title' => $row && trim((string)$row['title']) !== '' ? (string)$row['title'] : $def['title'], + 'version' => $row && trim((string)$row['version']) !== '' ? (string)$row['version'] : $def['version'], + 'effective_date' => $row && trim((string)$row['effective_date']) !== '' ? (string)$row['effective_date'] : $def['effective_date'], + 'content' => $content, + 'revision' => $revision, + 'has_diff' => $hasDiff, + 'diff_summary' => $diffSummary, + 'diff_content' => $hasDiff ? $diffContent : '', + 'status' => $row ? (string)$row['status'] : 'normal', + 'updatetime' => $updatetime, + 'update_time_text' => date('Y-m-d H:i', $updatetime), + ]; + } + return $result; + } + + /** + * 按前端请求的 type 读取单条启用协议(API 使用,含变更首看高亮判断) + */ + public static function getByType(string $type, int $userId = 0): ?array + { + $rawType = trim($type); + $code = self::canonicalCode($rawType); + $defs = self::defaultDefinitions(); + + $row = null; + try { + $row = Db::name('agreement') + ->field('id,code,title,version,effective_date,content,revision,prev_content,diff_content,diff_summary,status,updatetime') + ->where('code', $code) + ->find(); + } catch (\Throwable $e) { + $row = null; + } + + if ($row) { + if (($row['status'] ?? 'normal') !== 'normal') { + return null; + } + $updatetime = (int)($row['updatetime'] ?? 0); + $revision = max(1, (int)($row['revision'] ?? 1)); + $content = (string)$row['content']; + $prevContent = (string)($row['prev_content'] ?? ''); + $diffContent = (string)($row['diff_content'] ?? ''); + $diffSummary = (string)($row['diff_summary'] ?? ''); + + // 若存在上一版本正文但尚无预计算高亮,即时补充生成一次 + if ($revision > 1 && trim($prevContent) !== '' && trim($diffContent) === '') { + $computed = self::buildChangeHighlightedHtml($prevContent, $content); + if ($computed['has_changes']) { + $diffContent = $computed['html']; + $diffSummary = $computed['summary']; + } + } + + $hasDiff = ($revision > 1 && trim($diffContent) !== ''); + $showDiff = false; + if ($hasDiff) { + if ($userId > 0) { + $readRevision = self::getUserReadRevision($userId, $code); + $showDiff = ($readRevision < $revision); + } else { + // 未登录游客由前端结合本机已阅修订版本进行首看门控 + $showDiff = true; + } + } + + return [ + 'type' => $rawType !== '' ? $rawType : $code, + 'code' => $code, + 'title' => (string)$row['title'], + 'version' => (string)($row['version'] ?? 'v1.0'), + 'effective_date' => (string)($row['effective_date'] ?? ''), + 'update_time' => $updatetime > 0 ? date('Y-m-d H:i', $updatetime) : '', + 'content' => $content, + 'revision' => $revision, + 'has_diff' => $hasDiff, + 'show_diff' => $showDiff, + 'diff_summary' => $showDiff ? $diffSummary : '', + 'diff_content' => $showDiff ? $diffContent : '', + ]; + } + + // 数据库尚未入库该协议时,仅兼容历史 site 配置;均未配置则明确返回 null,禁用静态默认文案兜底 + if (!isset($defs[$code])) { + return null; + } + $def = $defs[$code]; + $cfgVal = !empty($def['legacy_key']) ? Config::get('site.' . $def['legacy_key']) : ''; + if (!is_string($cfgVal) || trim($cfgVal) === '') { + return null; + } + $content = self::plainTextToHtml(trim($cfgVal)); + + return [ + 'type' => $rawType !== '' ? $rawType : $code, + 'code' => $code, + 'title' => $def['title'], + 'version' => $def['version'], + 'effective_date' => $def['effective_date'], + 'update_time' => '', + 'content' => $content, + 'revision' => 1, + 'has_diff' => false, + 'show_diff' => false, + 'diff_summary' => '', + 'diff_content' => '', + ]; + } + + /** + * 查询登录用户已查看过的协议最高修订序号 + */ + public static function getUserReadRevision(int $userId, string $code): int + { + if ($userId <= 0 || $code === '') { + return 0; + } + try { + $rev = Db::name('agreement_read') + ->where('user_id', $userId) + ->where('agreement_code', $code) + ->value('read_revision'); + return $rev ? (int)$rev : 0; + } catch (\Throwable $e) { + return 0; + } + } + + /** + * 标记登录用户已首次查看当前变更版协议,防止后续打开重复高亮 + */ + public static function markUserRead(int $userId, string $type, int $revision = 0): array + { + $code = self::canonicalCode($type); + $currentRev = 1; + try { + $dbRev = Db::name('agreement')->where('code', $code)->value('revision'); + if ($dbRev) { + $currentRev = max(1, (int)$dbRev); + } + } catch (\Throwable $e) { + $currentRev = 1; + } + + $targetRevision = ($revision > 0 && $revision <= $currentRev) ? $revision : $currentRev; + if ($userId <= 0) { + return [ + 'recorded' => false, + 'code' => $code, + 'read_revision' => 0, + ]; + } + + $now = time(); + $prefix = (string)Config::get('database.prefix'); + $table = $prefix . 'agreement_read'; + Db::execute( + "INSERT INTO `{$table}` (`user_id`, `agreement_code`, `read_revision`, `read_time`, `createtime`, `updatetime`) " + . "VALUES (:uid, :code, :rev, :rtime, :ctime, :utime) " + . "ON DUPLICATE KEY UPDATE `read_revision` = GREATEST(`read_revision`, VALUES(`read_revision`)), " + . "`read_time` = VALUES(`read_time`), `updatetime` = VALUES(`updatetime`)", + [ + 'uid' => $userId, + 'code' => $code, + 'rev' => $targetRevision, + 'rtime' => $now, + 'ctime' => $now, + 'utime' => $now, + ] + ); + + return [ + 'recorded' => true, + 'code' => $code, + 'read_revision' => self::getUserReadRevision($userId, $code), + ]; + } + + /** + * 保存后台编辑的协议内容(含白名单校验、XSS 净化及自动生成变更背景色高亮) + * + * @throws \InvalidArgumentException + */ + public static function saveByCode(string $code, array $input): array + { + $defs = self::defaultDefinitions(); + $code = self::canonicalCode($code); + if (!isset($defs[$code])) { + throw new \InvalidArgumentException('不支持的协议类型编码'); + } + + $title = trim(strip_tags((string)($input['title'] ?? ''))); + if ($title === '') { + throw new \InvalidArgumentException('请填写协议标题'); + } + if (mb_strlen($title, 'UTF-8') > 100) { + throw new \InvalidArgumentException('协议标题不能超过 100 个字符'); + } + + $version = trim(strip_tags((string)($input['version'] ?? 'v1.0'))); + if ($version === '') { + $version = 'v1.0'; + } + if (mb_strlen($version, 'UTF-8') > 32) { + throw new \InvalidArgumentException('版本号不能超过 32 个字符'); + } + + $effectiveDate = trim(strip_tags((string)($input['effective_date'] ?? ''))); + if ($effectiveDate === '') { + $effectiveDate = date('Y-m-d'); + } elseif (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $effectiveDate)) { + throw new \InvalidArgumentException('生效日期格式须为 YYYY-MM-DD'); + } + + $status = trim((string)($input['status'] ?? 'normal')); + if (!in_array($status, ['normal', 'hidden'], true)) { + $status = 'normal'; + } + if (in_array($code, self::CORE_CODES, true)) { + // 用户服务协议与隐私保护指引为平台合规必选,始终保持启用状态 + $status = 'normal'; + } + + $rawContent = (string)($input['content'] ?? ''); + $cleanContent = self::sanitizeHtml($rawContent); + $plainCheck = trim(html_entity_decode(strip_tags($cleanContent), ENT_QUOTES, 'UTF-8')); + $plainCheck = preg_replace('/[\s\x{00A0}]+/u', '', $plainCheck); + if ($plainCheck === '') { + throw new \InvalidArgumentException('协议正文不能为空,请填写具体条款内容'); + } + + $now = time(); + $existing = Db::name('agreement')->where('code', $code)->find(); + if ($existing) { + $oldContent = (string)($existing['content'] ?? ''); + $revision = max(1, (int)($existing['revision'] ?? 1)); + $prevContent = (string)($existing['prev_content'] ?? ''); + $diffContent = (string)($existing['diff_content'] ?? ''); + $diffSummary = (string)($existing['diff_summary'] ?? ''); + + // 仅当正文实质文本发生变更时才递增修订号并重建变更高亮,避免仅修改元数据或编辑器空标签微调覆盖历史差异 + $oldPlainNorm = self::normalizeComparableText(strip_tags(html_entity_decode($oldContent, ENT_QUOTES, 'UTF-8'))); + $newPlainNorm = self::normalizeComparableText(strip_tags(html_entity_decode($cleanContent, ENT_QUOTES, 'UTF-8'))); + $isSubstantiveChange = ($oldPlainNorm !== $newPlainNorm); + + if ($isSubstantiveChange) { + $diffRes = self::buildChangeHighlightedHtml($oldContent, $cleanContent); + $revision++; + $prevContent = $oldContent; + $diffContent = $diffRes['has_changes'] ? $diffRes['html'] : ''; + $diffSummary = $diffRes['has_changes'] ? $diffRes['summary'] : ''; + } + + Db::name('agreement')->where('id', (int)$existing['id'])->update([ + 'title' => $title, + 'version' => $version, + 'effective_date' => $effectiveDate, + 'content' => $cleanContent, + 'revision' => $revision, + 'prev_content' => $prevContent, + 'diff_content' => $diffContent, + 'diff_summary' => $diffSummary, + 'status' => $status, + 'updatetime' => $now, + ]); + $id = (int)$existing['id']; + } else { + $revision = 1; + $diffContent = ''; + $diffSummary = ''; + $id = (int)Db::name('agreement')->insertGetId([ + 'code' => $code, + 'title' => $title, + 'version' => $version, + 'effective_date' => $effectiveDate, + 'content' => $cleanContent, + 'revision' => $revision, + 'prev_content' => '', + 'diff_content' => '', + 'diff_summary' => '', + 'status' => $status, + 'weigh' => (int)$defs[$code]['weigh'], + 'createtime' => $now, + 'updatetime' => $now, + ]); + } + + $hasDiff = ($revision > 1 && trim($diffContent) !== ''); + return [ + 'id' => $id, + 'code' => $code, + 'title' => $title, + 'version' => $version, + 'effective_date' => $effectiveDate, + 'content' => $cleanContent, + 'revision' => $revision, + 'has_diff' => $hasDiff, + 'diff_summary' => $diffSummary, + 'diff_content' => $hasDiff ? $diffContent : '', + 'status' => $status, + 'updatetime' => $now, + 'update_time_text' => date('Y-m-d H:i', $now), + ]; + } + + /** + * 将纯文本换行内容转为结构化 HTML 段落 + */ + public static function plainTextToHtml(string $text): string + { + $text = str_replace(["\r\n", "\r"], "\n", trim($text)); + if ($text === '') { + return ''; + } + // 若本身已包含 HTML 块标签则直接进行安全清洗后返回 + if (preg_match('/<\s*(p|h[1-6]|div|ul|ol|li|blockquote|br)\b/i', $text)) { + return self::sanitizeHtml($text); + } + $blocks = preg_split("/\n{2,}/", $text); + $html = ''; + foreach ($blocks as $block) { + $block = trim($block); + if ($block === '') { + continue; + } + $escaped = htmlspecialchars($block, ENT_QUOTES, 'UTF-8'); + $html .= '

      ' . str_replace("\n", '
      ', $escaped) . '

      '; + } + return $html; + } + + /** + * 对富文本 HTML 执行严格白名单安全过滤(防止 XSS 注入) + */ + public static function sanitizeHtml(string $html): string + { + $html = trim($html); + if ($html === '') { + return ''; + } + + // 先整体移除危险标签及其内部脚本/样式体 + foreach (self::DANGEROUS_TAGS as $tag) { + $html = preg_replace('/<' . $tag . '\b[^>]*>[\s\S]*?<\/' . $tag . '>/i', '', $html); + $html = preg_replace('/<' . $tag . '\b[^>]*\/?>/i', '', $html); + } + + $loaded = self::loadHtmlFragmentDom($html); + if ($loaded !== null) { + [$dom, $root] = $loaded; + self::sanitizeDomNode($root); + return trim(self::innerHtmlOfNode($dom, $root)); + } + + // DOM 不可用时的严格退化清理 + $allowed = '<' . implode('><', self::ALLOWED_TAGS) . '>'; + $stripped = strip_tags($html, $allowed); + $stripped = preg_replace('/\s+on[a-z]+\s*=\s*(?:"[^"]*"|\'[^\']*\'|[^\s>]+)/i', '', $stripped); + $stripped = preg_replace('/(?:javascript|vbscript|data)\s*:/i', '', $stripped); + return trim($stripped); + } + + /** + * 基于前后两版 HTML 正文生成带背景色标识的差异 HTML(段落块级 + 句内词字级双粒度) + * + * @return array{has_changes:bool,changed_blocks:int,summary:string,html:string} + */ + public static function buildChangeHighlightedHtml(string $prevHtml, string $currHtml): array + { + $currHtml = trim($currHtml); + if ($currHtml === '') { + return ['has_changes' => false, 'changed_blocks' => 0, 'summary' => '', 'html' => '']; + } + $prevHtml = trim($prevHtml); + if ($prevHtml === '') { + return ['has_changes' => false, 'changed_blocks' => 0, 'summary' => '', 'html' => $currHtml]; + } + + $prevBlocks = self::extractHtmlBlocks($prevHtml); + $currBlocks = self::extractHtmlBlocks($currHtml); + if (empty($currBlocks)) { + return ['has_changes' => false, 'changed_blocks' => 0, 'summary' => '', 'html' => $currHtml]; + } + + // 前后完全一致时直接原样输出 + $prevNorms = array_column($prevBlocks, 'norm'); + $currNorms = array_column($currBlocks, 'norm'); + if ($prevNorms === $currNorms) { + return ['has_changes' => false, 'changed_blocks' => 0, 'summary' => '', 'html' => $currHtml]; + } + + $anchors = self::computeLcsAnchors($prevNorms, $currNorms); + $anchors[] = [count($prevBlocks), count($currBlocks)]; + + $outParts = []; + $changedBlocks = 0; + $oCursor = 0; + $nCursor = 0; + + foreach ($anchors as [$oIdx, $nIdx]) { + if ($oIdx > $oCursor || $nIdx > $nCursor) { + $oldSlice = array_slice($prevBlocks, $oCursor, max(0, $oIdx - $oCursor)); + $newSlice = array_slice($currBlocks, $nCursor, max(0, $nIdx - $nCursor)); + [$sliceParts, $sliceChanges] = self::highlightUnmatchedBlockSlice($oldSlice, $newSlice); + foreach ($sliceParts as $p) { + $outParts[] = $p; + } + $changedBlocks += $sliceChanges; + } + if ($nIdx < count($currBlocks)) { + $outParts[] = $currBlocks[$nIdx]['html']; + } + $oCursor = $oIdx + 1; + $nCursor = $nIdx + 1; + } + + if ($changedBlocks <= 0) { + return ['has_changes' => false, 'changed_blocks' => 0, 'summary' => '', 'html' => $currHtml]; + } + + return [ + 'has_changes' => true, + 'changed_blocks' => $changedBlocks, + 'summary' => '共 ' . $changedBlocks . ' 处条款发生新增或修订', + 'html' => implode('', $outParts), + ]; + } + + /** + * 对未精确匹配的旧/新块区间做最相似配对并生成背景色标识 + * + * @return array{0:array,1:int} + */ + protected static function highlightUnmatchedBlockSlice(array $oldSlice, array $newSlice): array + { + $out = []; + $changes = 0; + $usedOld = []; + + foreach ($newSlice as $nb) { + // 分割线等非文本占位块不标记变更 + if ($nb['norm'] === '---hr---' || $nb['norm'] === '') { + $out[] = $nb['html']; + continue; + } + + $bestOldIdx = -1; + $bestSim = 0.0; + foreach ($oldSlice as $oi => $ob) { + if (isset($usedOld[$oi]) || $ob['norm'] === '' || $ob['norm'] === '---hr---') { + continue; + } + $sim = self::calculateTokenSimilarity($ob['raw_text'], $nb['raw_text']); + if ($sim > $bestSim) { + $bestSim = $sim; + $bestOldIdx = $oi; + } + } + + if ($bestOldIdx >= 0 && $bestSim >= 0.30) { + $usedOld[$bestOldIdx] = true; + [$modHtml, $modified] = self::highlightModifiedDomBlock( + $oldSlice[$bestOldIdx]['raw_text'], + $nb['html'], + $nb['tag'] + ); + $out[] = $modHtml; + if ($modified) { + $changes++; + } + } else { + $out[] = self::wrapElementWithBlockStyle($nb['html'], $nb['tag'], self::getAddedBlockStyle($nb['tag'])); + $changes++; + } + } + + return [$out, $changes]; + } + + /** + * 在已配对的新段落 DOM 中,对修改/新增文字节点精准包裹背景色 ,并补充行标 + * + * @return array{0:string,1:bool} + */ + protected static function highlightModifiedDomBlock(string $oldText, string $newBlockHtml, string $tag): array + { + $loaded = self::loadHtmlFragmentDom($newBlockHtml); + if ($loaded === null) { + return [self::wrapElementWithBlockStyle($newBlockHtml, $tag, self::getAddedBlockStyle($tag)), true]; + } + [$dom, $root] = $loaded; + + $textNodes = []; + $cursorChars = 0; + self::collectDomTextNodes($root, $textNodes, $cursorChars); + if (empty($textNodes)) { + return [$newBlockHtml, false]; + } + + $newText = ''; + foreach ($textNodes as $item) { + $newText .= $item['text']; + } + + $oldTokens = self::tokenizeForDiff($oldText); + $newTokens = self::tokenizeForDiff($newText); + $oldCount = count($oldTokens); + $newCount = count($newTokens); + + // 超长块安全退化,防止大矩阵运算开销 + if ($oldCount * $newCount > 360000 || $newCount === 0) { + return [self::wrapElementWithBlockStyle($newBlockHtml, $tag, self::getAddedBlockStyle($tag)), true]; + } + + $anchors = self::computeLcsAnchors($oldTokens, $newTokens); + $matchedNew = []; + foreach ($anchors as [, $nj]) { + $matchedNew[$nj] = true; + } + + $changedFlags = array_fill(0, $newCount, false); + $totalNonWs = 0; + $changedNonWs = 0; + for ($i = 0; $i < $newCount; $i++) { + $isWs = (trim($newTokens[$i]) === ''); + if (!$isWs) { + $totalNonWs++; + } + if (!isset($matchedNew[$i])) { + $changedFlags[$i] = true; + if (!$isWs) { + $changedNonWs++; + } + } + } + + // 若未检测到新增字符但纯文本确实有差异(例如句内精简删字),标示整段温和修订底色 + if ($changedNonWs === 0) { + if (self::normalizeComparableText($oldText) !== self::normalizeComparableText($newText)) { + return [self::wrapElementWithBlockStyle($newBlockHtml, $tag, self::getRevisedContainerStyle($tag, true)), true]; + } + return [$newBlockHtml, false]; + } + + // 若该段落超过 78% 非空白词均为新写入(大幅重写),采用整块浅橙高亮更清爽 + if ($totalNonWs > 0 && ($changedNonWs / $totalNonWs) >= 0.78) { + return [self::wrapElementWithBlockStyle($newBlockHtml, $tag, self::getAddedBlockStyle($tag)), true]; + } + + $ranges = self::buildSmoothedHighlightRanges($changedFlags, $newTokens); + if (empty($ranges)) { + return [$newBlockHtml, false]; + } + + foreach ($textNodes as $item) { + $nStart = $item['start']; + $nEnd = $item['end']; + $overlaps = []; + foreach ($ranges as [$rStart, $rEnd]) { + $s = max($nStart, $rStart); + $e = min($nEnd, $rEnd); + if ($e > $s) { + $overlaps[] = [$s - $nStart, $e - $nStart]; + } + } + if (empty($overlaps)) { + continue; + } + self::replaceDomTextWithHighlights($dom, $item['node'], $item['text'], $item['len'], $overlaps); + } + + $rendered = trim(self::innerHtmlOfNode($dom, $root)); + $rendered = self::wrapElementWithBlockStyle($rendered, $tag, self::getRevisedContainerStyle($tag, false)); + return [$rendered, true]; + } + + /** + * 平滑合并相邻高亮 Token 为连续字符区间,跳过首尾纯空白并桥接被单个标点/空格隔断的高亮片段 + * + * @return array + */ + protected static function buildSmoothedHighlightRanges(array $flags, array $tokens): array + { + $n = count($tokens); + // 若中间仅隔 1 个短分隔Token(空白、顿号、逗号、冒号等),则连成连续高光块 + for ($i = 1; $i < $n - 1; $i++) { + if (!$flags[$i] && $flags[$i - 1] && $flags[$i + 1]) { + $t = trim($tokens[$i]); + if ($t === '' || mb_strlen($t, 'UTF-8') <= 1 && preg_match('/^[\s,,、;;::\.。\-—]+$/u', $tokens[$i])) { + $flags[$i] = true; + } + } + } + + $ranges = []; + $charOffset = 0; + $curStart = null; + $curEnd = null; + $lastNonWsEnd = null; + + for ($i = 0; $i < $n; $i++) { + $tok = $tokens[$i]; + $tokLen = mb_strlen($tok, 'UTF-8'); + $isWs = (trim($tok) === ''); + + if ($flags[$i]) { + if ($curStart === null) { + if ($isWs) { + $charOffset += $tokLen; + continue; + } + $curStart = $charOffset; + $curEnd = $charOffset + $tokLen; + $lastNonWsEnd = $curEnd; + } else { + $curEnd = $charOffset + $tokLen; + if (!$isWs) { + $lastNonWsEnd = $curEnd; + } + } + } else { + if ($curStart !== null && $lastNonWsEnd !== null && $lastNonWsEnd > $curStart) { + $ranges[] = [$curStart, $lastNonWsEnd]; + } + $curStart = null; + $curEnd = null; + $lastNonWsEnd = null; + } + $charOffset += $tokLen; + } + if ($curStart !== null && $lastNonWsEnd !== null && $lastNonWsEnd > $curStart) { + $ranges[] = [$curStart, $lastNonWsEnd]; + } + + return $ranges; + } + + /** + * 将 DOMText 节点按字符区间切分为普通文本与带背景色的 高亮节点 + */ + protected static function replaceDomTextWithHighlights(\DOMDocument $dom, \DOMText $textNode, string $text, int $len, array $overlaps): void + { + $parent = $textNode->parentNode; + if (!$parent) { + return; + } + $frag = $dom->createDocumentFragment(); + $cursor = 0; + $spanStyle = 'background-color:#FFE2B0;color:#7C2D12;padding:1px 3px;border-radius:3px;'; + + foreach ($overlaps as [$s, $e]) { + if ($s > $cursor) { + $frag->appendChild($dom->createTextNode(mb_substr($text, $cursor, $s - $cursor, 'UTF-8'))); + } + $seg = mb_substr($text, $s, $e - $s, 'UTF-8'); + if ($seg !== '') { + $span = $dom->createElement('span'); + $span->setAttribute('style', $spanStyle); + $span->appendChild($dom->createTextNode($seg)); + $frag->appendChild($span); + } + $cursor = $e; + } + if ($cursor < $len) { + $frag->appendChild($dom->createTextNode(mb_substr($text, $cursor, $len - $cursor, 'UTF-8'))); + } + $parent->replaceChild($frag, $textNode); + } + + /** + * 抽取 HTML 顶层语义块(保留嵌套标签结构,提取归一化文本用于对齐) + * + * @return array + */ + protected static function extractHtmlBlocks(string $html): array + { + $loaded = self::loadHtmlFragmentDom($html); + if ($loaded === null) { + $plain = trim(strip_tags($html)); + if ($plain === '') { + return []; + } + return [[ + 'tag' => 'p', + 'norm' => self::normalizeComparableText($plain), + 'raw_text' => $plain, + 'html' => '

      ' . htmlspecialchars($plain, ENT_QUOTES, 'UTF-8') . '

      ', + ]]; + } + [$dom, $root] = $loaded; + + $blocks = []; + foreach ($root->childNodes as $child) { + if ($child instanceof \DOMText) { + $txt = trim($child->nodeValue); + if ($txt === '') { + continue; + } + $blocks[] = [ + 'tag' => 'p', + 'norm' => self::normalizeComparableText($txt), + 'raw_text' => $txt, + 'html' => '

      ' . htmlspecialchars($txt, ENT_QUOTES, 'UTF-8') . '

      ', + ]; + continue; + } + if (!($child instanceof \DOMElement)) { + continue; + } + $tag = strtolower($child->tagName); + $nodeHtml = trim($dom->saveHTML($child)); + if ($tag === 'hr') { + $blocks[] = [ + 'tag' => 'hr', + 'norm' => '---hr---', + 'raw_text' => '', + 'html' => $nodeHtml, + ]; + continue; + } + $rawText = str_replace("\xC2\xA0", ' ', $child->textContent); + $norm = self::normalizeComparableText($rawText); + if ($norm === '') { + continue; + } + $blocks[] = [ + 'tag' => $tag, + 'norm' => $norm, + 'raw_text' => trim($rawText), + 'html' => $nodeHtml, + ]; + } + return $blocks; + } + + /** + * 递归收集容器下的所有非空 DOMText 节点并记录字符下标映射 + */ + protected static function collectDomTextNodes(\DOMNode $node, array &$out, int &$cursor): void + { + if (!$node->hasChildNodes()) { + return; + } + foreach ($node->childNodes as $child) { + if ($child instanceof \DOMText) { + $val = (string)$child->nodeValue; + $len = mb_strlen($val, 'UTF-8'); + if ($len > 0) { + $out[] = [ + 'node' => $child, + 'text' => $val, + 'len' => $len, + 'start' => $cursor, + 'end' => $cursor + $len, + ]; + $cursor += $len; + } + } elseif ($child instanceof \DOMElement) { + self::collectDomTextNodes($child, $out, $cursor); + } + } + } + + /** + * 为顶层元素注入/追加块级高亮背景样式 + */ + protected static function wrapElementWithBlockStyle(string $elementHtml, string $tag, string $styleAdd): string + { + if ($styleAdd === '') { + return $elementHtml; + } + $loaded = self::loadHtmlFragmentDom($elementHtml); + if ($loaded !== null) { + [$dom, $root] = $loaded; + foreach ($root->childNodes as $child) { + if ($child instanceof \DOMElement) { + $old = trim((string)$child->getAttribute('style')); + $sep = ($old !== '' && substr($old, -1) !== ';') ? ';' : ''; + $child->setAttribute('style', $old . $sep . $styleAdd); + return trim(self::innerHtmlOfNode($dom, $root)); + } + } + } + return '<' . $tag . ' style="' . $styleAdd . '">' . $elementHtml . ''; + } + + /** + * 新增段落/区块浅橙高亮背景样式(针对列表项保留左缩进) + */ + protected static function getAddedBlockStyle(string $tag): string + { + $leftPad = in_array($tag, ['ul', 'ol'], true) ? '26px' : '10px'; + return "background-color:#FFF5E6;border-left:3px solid #F0710D;padding:8px 10px 8px {$leftPad};border-radius:6px;"; + } + + /** + * 修改段落外层定位标识样式 + */ + protected static function getRevisedContainerStyle(string $tag, bool $subtleTint): string + { + $leftPad = in_array($tag, ['ul', 'ol'], true) ? '24px' : '10px'; + if ($subtleTint) { + return "background-color:#FFF9F2;border-left:3px solid #FDBA74;padding:6px 10px 6px {$leftPad};border-radius:6px;"; + } + return "border-left:3px solid #FDBA74;padding-left:{$leftPad};"; + } + + /** + * 规范化纯文本用于语义等价比对(忽略全角空白/多余空格) + */ + public static function normalizeComparableText(string $text): string + { + $text = str_replace("\xC2\xA0", ' ', $text); + $text = preg_replace('/[\s\r\n\t]+/u', '', trim($text)); + return (string)$text; + } + + /** + * 中文按单字、英文与数字按连续单词、其余标点/空白按单符切分,适合精确协议文本对比 + * + * @return array + */ + protected static function tokenizeForDiff(string $text): array + { + if ($text === '') { + return []; + } + preg_match_all('/[a-zA-Z0-9_.\-]+|\s+|./us', $text, $m); + return isset($m[0]) && is_array($m[0]) ? $m[0] : []; + } + + /** + * 计算两个 Token 序列的 Jaccard/Dice 相似度,用于配对修订前后的对应段落 + */ + protected static function calculateTokenSimilarity(string $textA, string $textB): float + { + $tA = array_values(array_filter(self::tokenizeForDiff($textA), static function ($t) { + return trim($t) !== ''; + })); + $tB = array_values(array_filter(self::tokenizeForDiff($textB), static function ($t) { + return trim($t) !== ''; + })); + if (empty($tA) || empty($tB)) { + return 0.0; + } + $freqA = array_count_values($tA); + $common = 0; + foreach ($tB as $tok) { + if (!empty($freqA[$tok])) { + $common++; + $freqA[$tok]--; + } + } + return (2.0 * $common) / (count($tA) + count($tB)); + } + + /** + * 计算两段序列的最长公共子序列匹配下标对 [[idxA, idxB], ...] + * + * @return array + */ + protected static function computeLcsAnchors(array $seqA, array $seqB): array + { + $m = count($seqA); + $n = count($seqB); + if ($m === 0 || $n === 0) { + return []; + } + + // 跳过公共前后缀缩小 DP 表规模 + $prefix = 0; + while ($prefix < $m && $prefix < $n && $seqA[$prefix] === $seqB[$prefix]) { + $prefix++; + } + $suffix = 0; + while ($suffix < ($m - $prefix) && $suffix < ($n - $prefix) && $seqA[$m - 1 - $suffix] === $seqB[$n - 1 - $suffix]) { + $suffix++; + } + + $anchors = []; + for ($i = 0; $i < $prefix; $i++) { + $anchors[] = [$i, $i]; + } + + $midM = $m - $prefix - $suffix; + $midN = $n - $prefix - $suffix; + if ($midM > 0 && $midN > 0 && ($midM * $midN) <= 360000) { + $dp = array_fill(0, $midM + 1, array_fill(0, $midN + 1, 0)); + for ($i = $midM - 1; $i >= 0; $i--) { + $valA = $seqA[$prefix + $i]; + for ($j = $midN - 1; $j >= 0; $j--) { + if ($valA === $seqB[$prefix + $j]) { + $dp[$i][$j] = $dp[$i + 1][$j + 1] + 1; + } else { + $dp[$i][$j] = ($dp[$i + 1][$j] >= $dp[$i][$j + 1]) ? $dp[$i + 1][$j] : $dp[$i][$j + 1]; + } + } + } + $i = 0; + $j = 0; + while ($i < $midM && $j < $midN) { + if ($seqA[$prefix + $i] === $seqB[$prefix + $j]) { + $anchors[] = [$prefix + $i, $prefix + $j]; + $i++; + $j++; + } elseif ($dp[$i + 1][$j] >= $dp[$i][$j + 1]) { + $i++; + } else { + $j++; + } + } + } + + for ($k = 0; $k < $suffix; $k++) { + $anchors[] = [$m - $suffix + $k, $n - $suffix + $k]; + } + + return $anchors; + } + + /** + * 安全加载 HTML 片段到 UTF-8 DOMDocument,返回 [$dom, $root] + * + * @return array{0:\DOMDocument,1:\DOMElement}|null + */ + protected static function loadHtmlFragmentDom(string $html): ?array + { + if (!class_exists('\DOMDocument')) { + return null; + } + $prevErrors = libxml_use_internal_errors(true); + $dom = new \DOMDocument('1.0', 'UTF-8'); + $wrapped = '
      ' . $html . '
      '; + $loaded = $dom->loadHTML('' . $wrapped, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD); + libxml_clear_errors(); + libxml_use_internal_errors($prevErrors); + + if (!$loaded) { + return null; + } + $root = $dom->getElementById('__ag_root__'); + if (!$root) { + return null; + } + return [$dom, $root]; + } + + /** + * 序列化 DOM 容器下所有子节点 HTML + */ + protected static function innerHtmlOfNode(\DOMDocument $dom, \DOMNode $root): string + { + $out = ''; + foreach ($root->childNodes as $child) { + $out .= $dom->saveHTML($child); + } + return $out; + } + + /** + * 递归清理 DOM 节点,只保留白名单标签及安全属性 + */ + protected static function sanitizeDomNode(\DOMNode $node): void + { + if (!$node->hasChildNodes()) { + return; + } + $children = []; + foreach ($node->childNodes as $child) { + $children[] = $child; + } + foreach ($children as $child) { + if ($child instanceof \DOMComment) { + $node->removeChild($child); + continue; + } + if (!($child instanceof \DOMElement)) { + continue; + } + $tagName = strtolower($child->tagName); + if (in_array($tagName, self::DANGEROUS_TAGS, true)) { + $node->removeChild($child); + continue; + } + if (!in_array($tagName, self::ALLOWED_TAGS, true)) { + // 非白名单标签:先递归净化其子节点,再提升子节点并移除外层标签 + self::sanitizeDomNode($child); + while ($child->firstChild) { + $node->insertBefore($child->firstChild, $child); + } + $node->removeChild($child); + continue; + } + + // 清理元素属性(仅保留安全属性) + if ($child->hasAttributes()) { + $attrsToRemove = []; + foreach ($child->attributes as $attr) { + $attrName = strtolower($attr->nodeName); + $attrVal = trim($attr->nodeValue); + if (strpos($attrName, 'on') === 0) { + $attrsToRemove[] = $attr->nodeName; + continue; + } + if ($attrName === 'href' && $tagName === 'a') { + if (!preg_match('/^(https?:\/\/|mailto:|tel:|#)/i', $attrVal)) { + $attrsToRemove[] = $attr->nodeName; + } + continue; + } + if ($attrName === 'style') { + $safeStyle = self::filterInlineStyle($attrVal); + if ($safeStyle === '') { + $attrsToRemove[] = $attr->nodeName; + } else { + $child->setAttribute('style', $safeStyle); + } + continue; + } + if (!in_array($attrName, ['colspan', 'rowspan'], true)) { + $attrsToRemove[] = $attr->nodeName; + } + } + foreach ($attrsToRemove as $rm) { + $child->removeAttribute($rm); + } + } + + self::sanitizeDomNode($child); + } + } + + /** + * 过滤内联样式,仅允许安全的基础排版属性 + */ + protected static function filterInlineStyle(string $style): string + { + if (preg_match('/expression|javascript|vbscript|url\s*\(/i', $style)) { + return ''; + } + $allowedProps = ['text-align', 'font-weight', 'font-style', 'text-decoration', 'color']; + $safe = []; + foreach (explode(';', $style) as $decl) { + if (strpos($decl, ':') === false) { + continue; + } + [$prop, $val] = explode(':', $decl, 2); + $prop = strtolower(trim($prop)); + $val = trim($val); + if (in_array($prop, $allowedProps, true) && $val !== '' && !preg_match('/[<>"]/', $val)) { + $safe[] = $prop . ': ' . $val; + } + } + return implode('; ', $safe); + } +} diff --git a/server/application/common/library/CommissionService.php b/server/application/common/library/CommissionService.php new file mode 100644 index 0000000..37f2cf6 --- /dev/null +++ b/server/application/common/library/CommissionService.php @@ -0,0 +1,558 @@ + 100.00) { + return 100.00; + } + return $val; + } + + /** + * 强校验百分比输入(0 ~ 100) + * + * @param mixed $raw + * @return float + * @throws \InvalidArgumentException + */ + public static function validateRateInput($raw): float + { + $str = trim((string)$raw); + if ($str === '' || !is_numeric($str)) { + throw new \InvalidArgumentException('请填写有效的抽佣比例数字(0 ~ 100)'); + } + $val = (float)$str; + if ($val < 0 || $val > 100) { + throw new \InvalidArgumentException('抽佣比例必须在 0% ~ 100% 之间'); + } + return round($val, 2); + } + + /** + * 读取全局默认兜底抽佣比例(%) + */ + public static function getGlobalRate(): float + { + $defaults = FinanceConfigService::getCommissionDefaults(); + return self::normalizeRate($defaults['commission_rate'] ?? self::DEFAULT_RATE); + } + + /** + * 计算单条规则的条件精确度得分(相同权重 weigh 时,条件越具体越优先命中) + */ + protected static function calcSpecificity(array $rule): int + { + $score = 0; + if ((int)($rule['cat_id'] ?? 0) > 0) { + $score += 16; + } + if (($rule['customer_type'] ?? 'all') !== 'all') { + $score += 8; + } + if (($rule['order_mode'] ?? 'all') !== 'all') { + $score += 4; + } + if (($rule['urgent_mode'] ?? 'all') !== 'all') { + $score += 2; + } + if ((float)($rule['min_amount'] ?? 0) > 0 || (float)($rule['max_amount'] ?? 0) > 0) { + $score += 1; + } + return $score; + } + + /** + * 获取当前全部启用中的订单抽佣规则(按 weigh DESC -> 精确度 DESC -> id DESC 排序) + */ + public static function getActiveRules(bool $forceRefresh = false): array + { + if (self::$activeRulesCache === null || $forceRefresh) { + $rows = []; + try { + $rows = Db::name('commission_rule') + ->where('status', 'normal') + ->order('weigh desc, id desc') + ->select(); + if (!is_array($rows)) { + $rows = []; + } + } catch (\Throwable $e) { + $rows = []; + } + + foreach ($rows as &$r) { + $r['id'] = (int)$r['id']; + $r['cat_id'] = (int)($r['cat_id'] ?? 0); + $r['weigh'] = (int)($r['weigh'] ?? 0); + $r['min_amount'] = round(max(0.00, (float)($r['min_amount'] ?? 0)), 2); + $r['max_amount'] = round(max(0.00, (float)($r['max_amount'] ?? 0)), 2); + $r['commission_rate'] = self::normalizeRate($r['commission_rate'] ?? 0); + $r['fixed_fee'] = round(max(0.00, (float)($r['fixed_fee'] ?? 0)), 2); + $r['min_commission'] = round(max(0.00, (float)($r['min_commission'] ?? 0)), 2); + $r['max_commission'] = round(max(0.00, (float)($r['max_commission'] ?? 0)), 2); + $r['_specificity'] = self::calcSpecificity($r); + } + unset($r); + + usort($rows, function (array $a, array $b) { + if ($a['weigh'] !== $b['weigh']) { + return $b['weigh'] <=> $a['weigh']; + } + if ($a['_specificity'] !== $b['_specificity']) { + return $b['_specificity'] <=> $a['_specificity']; + } + return $b['id'] <=> $a['id']; + }); + + self::$activeRulesCache = $rows; + } + return self::$activeRulesCache; + } + + /** + * 判断订单静态维度(不含金额区间)是否匹配规则 + */ + protected static function matchStaticDimensions(array $rule, string $priceType, string $customerType, int $catId, int $urgent): bool + { + $rMode = (string)($rule['order_mode'] ?? 'all'); + if ($rMode !== 'all' && $rMode !== $priceType) { + return false; + } + + $rCust = (string)($rule['customer_type'] ?? 'all'); + if ($rCust !== 'all' && $rCust !== $customerType) { + return false; + } + + $rCat = (int)($rule['cat_id'] ?? 0); + if ($rCat > 0 && $rCat !== $catId) { + return false; + } + + $rUrgent = (string)($rule['urgent_mode'] ?? 'all'); + if ($rUrgent === 'urgent' && $urgent !== 1) { + return false; + } + if ($rUrgent === 'normal' && $urgent !== 0) { + return false; + } + + return true; + } + + /** + * 判断订单金额是否命中规则的金额区间 + */ + protected static function matchAmountRange(array $rule, float $amount): bool + { + $minAmt = (float)($rule['min_amount'] ?? 0); + $maxAmt = (float)($rule['max_amount'] ?? 0); + $amt = round(max(0.00, $amount), 2); + + if ($amt < $minAmt) { + return false; + } + if ($maxAmt > 0 && $amt > $maxAmt) { + return false; + } + return true; + } + + /** + * 提取规则核心计佣描述结构(用于写入订单快照) + */ + protected static function extractRuleItem(array $rule): array + { + $calcMode = in_array($rule['calc_mode'] ?? 'percent', ['percent', 'fixed', 'mixed'], true) + ? $rule['calc_mode'] + : 'percent'; + + return [ + 'rule_id' => (int)($rule['id'] ?? 0), + 'rule_name' => (string)($rule['name'] ?? '全局默认抽佣规则'), + 'min_amount' => round((float)($rule['min_amount'] ?? 0), 2), + 'max_amount' => round((float)($rule['max_amount'] ?? 0), 2), + 'calc_mode' => $calcMode, + 'commission_rate' => self::normalizeRate($rule['commission_rate'] ?? 0), + 'fixed_fee' => round(max(0.00, (float)($rule['fixed_fee'] ?? 0)), 2), + 'min_commission' => round(max(0.00, (float)($rule['min_commission'] ?? 0)), 2), + 'max_commission' => round(max(0.00, (float)($rule['max_commission'] ?? 0)), 2), + ]; + } + + /** + * 创单阶段:根据订单多维上下文匹配生效抽佣规则并生成完整快照 + * + * @param array $ctx ['price_type' => string, 'customer_type' => 'personal'|'merchant', 'cat_id' => int, 'urgent' => int, 'order_amount' => float] + * @param array|null $rules 可选预加载规则列表(批量导单防重复查库) + * @param array|null $defaults 可选预加载默认兜底配置 + * @return array ['commission_rate' => float, 'commission_snapshot' => string(JSON), 'rule_name' => string] + */ + public static function matchOrderSnapshot(array $ctx, ?array $rules = null, ?array $defaults = null): array + { + $rules = $rules !== null ? $rules : self::getActiveRules(); + $defaults = $defaults !== null ? $defaults : FinanceConfigService::getCommissionDefaults(); + + $priceType = trim((string)($ctx['price_type'] ?? 'fixed')) ?: 'fixed'; + $customerType = in_array($ctx['customer_type'] ?? 'personal', ['personal', 'merchant'], true) + ? $ctx['customer_type'] + : 'personal'; + $catId = max(0, (int)($ctx['cat_id'] ?? 0)); + $urgent = (int)($ctx['urgent'] ?? 0) === 1 ? 1 : 0; + $orderAmount = round(max(0.00, (float)($ctx['order_amount'] ?? 0)), 2); + + $fallbackItem = [ + 'rule_id' => 0, + 'rule_name' => '全局默认兜底规则', + 'min_amount' => 0.00, + 'max_amount' => 0.00, + 'calc_mode' => $defaults['calc_mode'], + 'commission_rate' => self::normalizeRate($defaults['commission_rate']), + 'fixed_fee' => round((float)$defaults['fixed_fee'], 2), + 'min_commission' => round((float)$defaults['min_commission'], 2), + 'max_commission' => round((float)$defaults['max_commission'], 2), + ]; + + $staticMatched = []; + $matchedItem = null; + + foreach ($rules as $rule) { + if (!self::matchStaticDimensions($rule, $priceType, $customerType, $catId, $urgent)) { + continue; + } + $item = self::extractRuleItem($rule); + $staticMatched[] = $item; + if ($matchedItem === null && self::matchAmountRange($rule, $orderAmount)) { + $matchedItem = $item; + } + } + + // 若报价单创单金额为 0,且存在起始金额 > 0 的报价阶梯但暂无 0 元阶梯,优先取第一条无金额限制规则,否则暂挂兜底并保留全部阶梯候选 + if ($matchedItem === null) { + $matchedItem = $fallbackItem; + } + + $snapshot = [ + 'version' => 1, + 'snapshot_time' => time(), + 'context' => [ + 'price_type' => $priceType, + 'customer_type' => $customerType, + 'cat_id' => $catId, + 'urgent' => $urgent, + 'init_amount' => $orderAmount, + ], + 'include_diff_in_commission' => (int)$defaults['include_diff_in_commission'] === 1 ? 1 : 0, + 'active_rule' => $matchedItem, + 'fallback_rule' => $fallbackItem, + ]; + + // 报价单创单时金额通常为 0,固化创单时刻符合静态条件的全部金额阶梯,供后续选定报价时在快照内部闭环定档 + if ($priceType === 'quote' && !empty($staticMatched)) { + $snapshot['candidate_tiers'] = $staticMatched; + } + + $effectiveRate = self::calcDisplayRateForAmount($orderAmount, $matchedItem); + + return [ + 'commission_rate' => $effectiveRate, + 'commission_snapshot' => json_encode($snapshot, JSON_UNESCAPED_UNICODE), + 'rule_name' => $matchedItem['rule_name'], + ]; + } + + /** + * 报价单选定师傅报价并确认金额时:仅在订单自身的创单快照 candidate_tiers 内部重新定档(严禁读取后台新规则) + * + * @param float $finalAmount 选定报价后的订单金额 + * @param mixed $existingSnapshotJson 订单表已有的 commission_snapshot + * @param mixed $existingRate 订单表已有的 commission_rate + * @return array ['commission_rate' => float, 'commission_snapshot' => string|null] + */ + public static function refreshQuoteOrderSnapshotOnAmountConfirmed(float $finalAmount, $existingSnapshotJson, $existingRate): array + { + $fallbackRate = ($existingRate !== null && trim((string)$existingRate) !== '' && is_numeric($existingRate)) + ? self::normalizeRate($existingRate) + : self::DEFAULT_RATE; + + if (!is_string($existingSnapshotJson) || trim($existingSnapshotJson) === '') { + return [ + 'commission_rate' => $fallbackRate, + 'commission_snapshot' => null, + ]; + } + + $snap = json_decode($existingSnapshotJson, true); + if (!is_array($snap)) { + return [ + 'commission_rate' => $fallbackRate, + 'commission_snapshot' => $existingSnapshotJson, + ]; + } + + $chosen = null; + if (!empty($snap['candidate_tiers']) && is_array($snap['candidate_tiers'])) { + foreach ($snap['candidate_tiers'] as $tier) { + if (is_array($tier) && self::matchAmountRange($tier, $finalAmount)) { + $chosen = $tier; + break; + } + } + } + + if ($chosen === null) { + if (!empty($snap['fallback_rule']) && is_array($snap['fallback_rule'])) { + $chosen = $snap['fallback_rule']; + } elseif (!empty($snap['active_rule']) && is_array($snap['active_rule'])) { + $chosen = $snap['active_rule']; + } + } + + if (is_array($chosen)) { + $snap['active_rule'] = $chosen; + $snap['confirmed_quote_amount'] = round(max(0.00, $finalAmount), 2); + $snap['confirmed_time'] = time(); + $newRate = self::calcDisplayRateForAmount($finalAmount, $chosen); + return [ + 'commission_rate' => $newRate, + 'commission_snapshot' => json_encode($snap, JSON_UNESCAPED_UNICODE), + ]; + } + + return [ + 'commission_rate' => $fallbackRate, + 'commission_snapshot' => $existingSnapshotJson, + ]; + } + + /** + * 计算给定金额与规则项下的展示/快照百分比 + */ + protected static function calcDisplayRateForAmount(float $amount, array $ruleItem): float + { + $mode = $ruleItem['calc_mode'] ?? 'percent'; + $rate = self::normalizeRate($ruleItem['commission_rate'] ?? 0); + if ($mode === 'percent' && (float)($ruleItem['min_commission'] ?? 0) <= 0 && (float)($ruleItem['max_commission'] ?? 0) <= 0) { + return $rate; + } + if ($amount <= 0) { + return $rate; + } + $res = self::computeCommissionByRuleItem($amount, $amount, $ruleItem); + return $res['commission_rate']; + } + + /** + * 核心纯函数:按规则项核算抽佣与实结金额 + * + * @param float $totalPayAmount 订单总实付金额(主单实付/应付总额,师傅结算总额上限) + * @param float $calcBaseAmount 参与抽佣计算的基数金额(若增项免抽佣则已剔除增项差价) + * @param array $ruleItem 规则快照结构 + * @return array + */ + protected static function computeCommissionByRuleItem(float $totalPayAmount, float $calcBaseAmount, array $ruleItem): array + { + $total = max(0.00, round($totalPayAmount, 2)); + $base = max(0.00, min($total, round($calcBaseAmount, 2))); + $mode = in_array($ruleItem['calc_mode'] ?? 'percent', ['percent', 'fixed', 'mixed'], true) + ? $ruleItem['calc_mode'] + : 'percent'; + $rate = self::normalizeRate($ruleItem['commission_rate'] ?? 0); + $fixed = round(max(0.00, (float)($ruleItem['fixed_fee'] ?? 0)), 2); + $minC = round(max(0.00, (float)($ruleItem['min_commission'] ?? 0)), 2); + $maxC = round(max(0.00, (float)($ruleItem['max_commission'] ?? 0)), 2); + + if ($total <= 0 || $base <= 0) { + return [ + 'commission_rate' => $mode === 'percent' ? $rate : 0.00, + 'commission' => 0.00, + 'settle_amount' => $total, + 'calc_mode' => $mode, + 'rule_name' => (string)($ruleItem['rule_name'] ?? '历史比例快照'), + 'calc_base_amount' => $base, + ]; + } + + if ($mode === 'fixed') { + $rawComm = $fixed; + } elseif ($mode === 'mixed') { + $rawComm = round($base * $rate / 100 + $fixed, 2); + } else { + $rawComm = round($base * $rate / 100, 2); + } + + $comm = $rawComm; + if ($minC > 0 && $comm < $minC) { + $comm = $minC; + } + if ($maxC > 0 && $comm > $maxC) { + $comm = $maxC; + } + // 抽佣额绝不可超过计佣基数或总实付额 + $comm = round(min($base, max(0.00, $comm)), 2); + $settle = round(max(0.00, $total - $comm), 2); + + // 若为纯比例且未触发保底/封顶与增项剔除,保留原配置比例;否则折算实际综合抽佣率供结算单归档对账 + if ($mode === 'percent' && $comm === $rawComm && abs($total - $base) < 0.001) { + $effectiveRate = $rate; + } else { + $effectiveRate = $total > 0 ? self::normalizeRate(round($comm / $total * 100, 2)) : $rate; + } + + return [ + 'commission_rate' => $effectiveRate, + 'commission' => $comm, + 'settle_amount' => $settle, + 'calc_mode' => $mode, + 'rule_name' => (string)($ruleItem['rule_name'] ?? '历史比例快照'), + 'calc_base_amount' => $base, + ]; + } + + /** + * 基于完整订单行记录(fa_order)锁定的快照计算平台抽佣与师傅实结金额 + * + * @param array $order fa_order 行数据(含 pay_amount/total_amount, diff_amount, diff_status, commission_rate, commission_snapshot) + * @param float|null $overrideAmount 可选指定结算总金额(如订单大厅预估展示时传入基准价) + * @return array ['commission_rate' => float, 'commission' => float, 'settle_amount' => float, 'calc_mode' => string, 'rule_name' => string, 'calc_base_amount' => float] + */ + public static function calcByOrder(array $order, ?float $overrideAmount = null): array + { + if ($overrideAmount !== null) { + $totalAmount = max(0.00, round($overrideAmount, 2)); + } else { + $payAmt = (float)($order['pay_amount'] ?? 0); + $totAmt = (float)($order['total_amount'] ?? 0); + $totalAmount = max(0.00, round($payAmt > 0 ? $payAmt : $totAmt, 2)); + } + + $snapshotRate = $order['commission_rate'] ?? null; + $snapshotJson = $order['commission_snapshot'] ?? null; + + if (is_string($snapshotJson) && trim($snapshotJson) !== '') { + $snap = json_decode($snapshotJson, true); + if (is_array($snap) && !empty($snap['active_rule']) && is_array($snap['active_rule'])) { + $calcBase = $totalAmount; + $includeDiff = isset($snap['include_diff_in_commission']) ? (int)$snap['include_diff_in_commission'] : 1; + if ($overrideAmount === null && $includeDiff === 0 && (int)($order['diff_status'] ?? 0) === 2) { + $diffAmt = round(max(0.00, (float)($order['diff_amount'] ?? 0)), 2); + if ($diffAmt > 0 && $calcBase >= $diffAmt) { + $calcBase = round($calcBase - $diffAmt, 2); + } + } + return self::computeCommissionByRuleItem($totalAmount, $calcBase, $snap['active_rule']); + } + } + + return self::calcByOrderSnapshot($totalAmount, $snapshotRate); + } + + /** + * 兼容历史仅含 commission_rate 字段或传入快照 JSON 的核算调用 + * + * @param float $orderAmount 订单实付结算基数 + * @param mixed $snapshotRate fa_order.commission_rate 快照值 + * @param string|null $snapshotJson 可选 fa_order.commission_snapshot JSON + * @return array ['commission_rate' => float, 'commission' => float, 'settle_amount' => float, 'calc_mode' => string, 'rule_name' => string, 'calc_base_amount' => float] + */ + public static function calcByOrderSnapshot(float $orderAmount, $snapshotRate, ?string $snapshotJson = null): array + { + if ($snapshotJson !== null && trim($snapshotJson) !== '') { + return self::calcByOrder([ + 'pay_amount' => $orderAmount, + 'total_amount' => $orderAmount, + 'commission_rate' => $snapshotRate, + 'commission_snapshot' => $snapshotJson, + ]); + } + + $rate = ($snapshotRate !== null && trim((string)$snapshotRate) !== '' && is_numeric($snapshotRate)) + ? self::normalizeRate($snapshotRate) + : self::DEFAULT_RATE; + + return self::computeCommissionByRuleItem($orderAmount, $orderAmount, [ + 'rule_name' => '比例快照(' . number_format($rate, 2, '.', '') . '%)', + 'calc_mode' => 'percent', + 'commission_rate' => $rate, + 'fixed_fee' => 0.00, + 'min_commission' => 0.00, + 'max_commission' => 0.00, + ]); + } + + /** + * 将 fa_config 同步至 application/extra/site.php,并保留文件中非数据库托管的扩展密钥 + * + * @param string $removedKey 刚被删除的配置键名 + * @return void + */ + public static function syncSiteConfigFile(string $removedKey = ''): void + { + $siteFile = CONF_PATH . 'extra' . DS . 'site.php'; + $existing = []; + if (is_file($siteFile)) { + $loaded = include $siteFile; + if (is_array($loaded)) { + $existing = $loaded; + } + } + if ($removedKey !== '' && array_key_exists($removedKey, $existing)) { + unset($existing[$removedKey]); + } + + $dbConfig = []; + $rows = Db::name('config')->select(); + foreach ($rows as $value) { + $val = $value['value']; + if (in_array($value['type'], ['selects', 'checkbox', 'images', 'files'], true)) { + $val = $val === '' ? [] : explode(',', (string)$val); + } elseif ($value['type'] === 'array') { + $val = (array)json_decode((string)$val, true); + } + $dbConfig[$value['name']] = $val; + } + + $merged = array_merge($existing, $dbConfig); + file_put_contents( + $siteFile, + ' 'percent', + 'default_commission_rate' => '10.00', + 'default_fixed_fee' => '0.00', + 'default_min_commission' => '0.00', + 'default_max_commission' => '0.00', + 'include_diff_in_commission' => '1', + + // 提现与微信小程序商家转账配置 + 'withdraw_enabled' => '1', + 'withdraw_close_tip' => '系统正在进行财务结算维护,请稍后再发起提现', + 'withdraw_channels' => 'wechat', + 'withdraw_require_realname' => '1', + 'withdraw_min_amount' => '10.00', + 'withdraw_max_amount' => '200.00', + 'withdraw_daily_amount' => '2000.00', + 'withdraw_daily_count' => '3', + 'withdraw_allow_weekdays' => '1,2,3,4,5,6,7', + 'withdraw_time_start' => '00:00', + 'withdraw_time_end' => '23:59', + 'withdraw_fee_mode' => 'percent', + 'withdraw_fee_rate' => '0.60', + 'withdraw_fee_fixed' => '0.00', + 'withdraw_fee_min' => '0.00', + 'withdraw_fee_max' => '0.00', + 'withdraw_transfer_mode' => 'wx_mch_transfer', + 'withdraw_audit_mode' => 'manual', + 'withdraw_auto_max_amount' => '50.00', + 'wx_transfer_scene_id' => '1005', + 'wx_recv_perception' => '劳务报酬', + 'wx_job_type' => '上门安装维修师傅', + 'wx_reward_desc' => '平台工单安装维修劳务报酬', + 'wx_transfer_remark' => '工单安装维修服务劳务报酬', + ]; + } + + /** + * 清空请求内静态缓存 + */ + public static function clearCache(): void + { + self::$cache = null; + } + + /** + * 获取全部原始配置键值数组 + */ + public static function getAll(bool $forceRefresh = false): array + { + if (self::$cache === null || $forceRefresh) { + $base = self::defaults(); + try { + $dbMap = Db::name('finance_config')->column('value', 'name'); + if (is_array($dbMap) && !empty($dbMap)) { + foreach ($dbMap as $k => $v) { + if ($v !== null) { + $base[$k] = (string)$v; + } + } + } + } catch (\Throwable $e) { + // 表未就绪时平滑回退默认字典 + } + self::$cache = $base; + } + return self::$cache; + } + + /** + * 获取订单默认兜底抽佣配置 + */ + public static function getCommissionDefaults(): array + { + $all = self::getAll(); + $mode = in_array($all['default_calc_mode'], ['percent', 'fixed', 'mixed'], true) + ? $all['default_calc_mode'] + : 'percent'; + + return [ + 'calc_mode' => $mode, + 'commission_rate' => round(min(100.00, max(0.00, (float)$all['default_commission_rate'])), 2), + 'fixed_fee' => round(max(0.00, (float)$all['default_fixed_fee']), 2), + 'min_commission' => round(max(0.00, (float)$all['default_min_commission']), 2), + 'max_commission' => round(max(0.00, (float)$all['default_max_commission']), 2), + 'include_diff_in_commission' => (int)$all['include_diff_in_commission'] === 1 ? 1 : 0, + ]; + } + + /** + * 获取结构化的提现与微信小程序转账配置 + */ + public static function getWithdrawConfig(): array + { + $all = self::getAll(); + $rawChannels = array_filter(array_map('trim', explode(',', (string)$all['withdraw_channels']))); + $channels = []; + foreach ($rawChannels as $ch) { + if (in_array($ch, ['wechat', 'bank'], true) && !in_array($ch, $channels, true)) { + $channels[] = $ch; + } + } + if (empty($channels)) { + $channels = ['wechat']; + } + + $rawDays = array_filter(array_map('intval', explode(',', (string)$all['withdraw_allow_weekdays']))); + $weekdays = []; + foreach ($rawDays as $d) { + if ($d >= 1 && $d <= 7 && !in_array($d, $weekdays, true)) { + $weekdays[] = $d; + } + } + sort($weekdays); + if (empty($weekdays)) { + $weekdays = [1, 2, 3, 4, 5, 6, 7]; + } + + $minAmt = round(max(0.10, (float)$all['withdraw_min_amount']), 2); + $maxAmt = round(min(self::WX_TRANSFER_HARD_MAX, max($minAmt, (float)$all['withdraw_max_amount'])), 2); + $dailyAmt = round(max(0.00, (float)$all['withdraw_daily_amount']), 2); + if ($dailyAmt > 0 && $dailyAmt < $minAmt) { + $dailyAmt = $maxAmt; + } + + $feeMode = in_array($all['withdraw_fee_mode'], ['percent', 'fixed', 'free'], true) + ? $all['withdraw_fee_mode'] + : 'percent'; + + $transferMode = in_array($all['withdraw_transfer_mode'], ['wx_mch_transfer', 'offline_manual'], true) + ? $all['withdraw_transfer_mode'] + : 'wx_mch_transfer'; + + $auditMode = in_array($all['withdraw_audit_mode'], ['manual', 'auto_small'], true) + ? $all['withdraw_audit_mode'] + : 'manual'; + + return [ + 'enabled' => (int)$all['withdraw_enabled'] === 1 ? 1 : 0, + 'close_tip' => trim((string)$all['withdraw_close_tip']) ?: '当前暂未开放提现,请稍后再试', + 'channels' => $channels, + 'require_realname' => (int)$all['withdraw_require_realname'] === 1 ? 1 : 0, + 'min_amount' => $minAmt, + 'max_amount' => $maxAmt, + 'daily_amount' => $dailyAmt, + 'daily_count' => max(0, (int)$all['withdraw_daily_count']), + 'allow_weekdays' => $weekdays, + 'time_start' => preg_match('/^\d{2}:\d{2}$/', (string)$all['withdraw_time_start']) ? $all['withdraw_time_start'] : '00:00', + 'time_end' => preg_match('/^\d{2}:\d{2}$/', (string)$all['withdraw_time_end']) ? $all['withdraw_time_end'] : '23:59', + 'fee_mode' => $feeMode, + 'fee_rate' => round(min(50.00, max(0.00, (float)$all['withdraw_fee_rate'])), 2), + 'fee_fixed' => round(max(0.00, (float)$all['withdraw_fee_fixed']), 2), + 'fee_min' => round(max(0.00, (float)$all['withdraw_fee_min']), 2), + 'fee_max' => round(max(0.00, (float)$all['withdraw_fee_max']), 2), + 'transfer_mode' => $transferMode, + 'audit_mode' => $auditMode, + 'auto_max_amount' => round(max(0.00, (float)$all['withdraw_auto_max_amount']), 2), + 'wx_scene_id' => trim((string)$all['wx_transfer_scene_id']) ?: '1005', + 'wx_perception' => mb_substr(trim((string)$all['wx_recv_perception']) ?: '劳务报酬', 0, 32), + 'wx_job_type' => mb_substr(trim((string)$all['wx_job_type']) ?: '上门安装维修师傅', 0, 32), + 'wx_reward_desc' => mb_substr(trim((string)$all['wx_reward_desc']) ?: '平台工单安装维修劳务报酬', 0, 32), + 'wx_remark' => mb_substr(trim((string)$all['wx_transfer_remark']) ?: '工单安装维修服务劳务报酬', 0, 32), + ]; + } + + /** + * 计算单笔提现的手续费与实际到账净额 + */ + public static function calcWithdrawFee(float $amount, ?array $cfg = null): array + { + $cfg = $cfg ?: self::getWithdrawConfig(); + $amt = max(0.00, round($amount, 2)); + $fee = 0.00; + + if ($amt > 0) { + if ($cfg['fee_mode'] === 'percent') { + $fee = round($amt * ((float)$cfg['fee_rate']) / 100, 2); + if ($cfg['fee_min'] > 0 && $fee < $cfg['fee_min']) { + $fee = $cfg['fee_min']; + } + if ($cfg['fee_max'] > 0 && $fee > $cfg['fee_max']) { + $fee = $cfg['fee_max']; + } + } elseif ($cfg['fee_mode'] === 'fixed') { + $fee = round((float)$cfg['fee_fixed'], 2); + } else { + $fee = 0.00; + } + } + + if ($fee >= $amt && $amt > 0) { + $fee = max(0.00, round($amt - self::WX_TRANSFER_MIN_ARRIVE, 2)); + } + + $arrive = round(max(0.00, $amt - $fee), 2); + return [ + 'amount' => $amt, + 'fee' => $fee, + 'arrive_amount' => $arrive, + 'fee_mode' => $cfg['fee_mode'], + 'fee_rate' => $cfg['fee_rate'], + 'fee_fixed' => $cfg['fee_fixed'], + ]; + } + + /** + * 统计师傅今日已申请提现次数与金额(排除已驳回单) + */ + public static function getMasterTodayWithdrawStats(int $masterId): array + { + $todayStart = strtotime(date('Y-m-d 00:00:00')); + $rows = Db::name('withdraw') + ->field('amount') + ->where('master_id', $masterId) + ->where('status', '<>', 'reject') + ->where('createtime', '>=', $todayStart) + ->select(); + + $cnt = count($rows); + $sum = 0.00; + foreach ($rows as $r) { + $sum += (float)$r['amount']; + } + return [ + 'today_count' => $cnt, + 'today_amount' => round($sum, 2), + ]; + } + + /** + * 生成下发给小程序「我的钱包」页面的动态提现配置与规则说明 + */ + public static function buildMasterWithdrawClientPayload(array $master): array + { + $cfg = self::getWithdrawConfig(); + $stats = self::getMasterTodayWithdrawStats((int)$master['id']); + + $remainDailyAmount = $cfg['daily_amount'] > 0 + ? round(max(0.00, $cfg['daily_amount'] - $stats['today_amount']), 2) + : -1; + $remainDailyCount = $cfg['daily_count'] > 0 + ? max(0, $cfg['daily_count'] - $stats['today_count']) + : -1; + + if ($cfg['fee_mode'] === 'free' || ($cfg['fee_mode'] === 'percent' && $cfg['fee_rate'] <= 0) || ($cfg['fee_mode'] === 'fixed' && $cfg['fee_fixed'] <= 0)) { + $feeText = '免通道手续费'; + } elseif ($cfg['fee_mode'] === 'fixed') { + $feeText = '手续费 ¥' . number_format($cfg['fee_fixed'], 2, '.', '') . '/笔'; + } else { + $feeText = '通道费率 ' . rtrim(rtrim(number_format($cfg['fee_rate'], 2, '.', ''), '0'), '.') . '%'; + if ($cfg['fee_min'] > 0 || $cfg['fee_max'] > 0) { + $parts = []; + if ($cfg['fee_min'] > 0) { + $parts[] = '最低¥' . number_format($cfg['fee_min'], 2, '.', ''); + } + if ($cfg['fee_max'] > 0) { + $parts[] = '封顶¥' . number_format($cfg['fee_max'], 2, '.', ''); + } + $feeText .= '(' . implode(',', $parts) . ')'; + } + } + + $minTxt = rtrim(rtrim(number_format($cfg['min_amount'], 2, '.', ''), '0'), '.'); + $maxTxt = rtrim(rtrim(number_format($cfg['max_amount'], 2, '.', ''), '0'), '.'); + $ruleTip = "完工核销后工费即刻入账;单笔 ¥{$minTxt}~¥{$maxTxt} 可提现,{$feeText}。"; + if (!$cfg['enabled']) { + $ruleTip = $cfg['close_tip']; + } + + $arrivalDesc = $cfg['transfer_mode'] === 'wx_mch_transfer' + ? ($cfg['audit_mode'] === 'auto_small' ? '微信零钱(小额免审,确认收款即到账)' : '微信零钱(审核后小程序确认收款即到账)') + : '微信零钱 / 人工财务打款'; + + return [ + 'enabled' => (bool)$cfg['enabled'], + 'close_tip' => $cfg['close_tip'], + 'channels' => $cfg['channels'], + 'require_realname' => (bool)$cfg['require_realname'], + 'min_amount' => $cfg['min_amount'], + 'max_amount' => $cfg['max_amount'], + 'daily_amount' => $cfg['daily_amount'], + 'daily_count' => $cfg['daily_count'], + 'today_amount' => $stats['today_amount'], + 'today_count' => $stats['today_count'], + 'remain_daily_amount' => $remainDailyAmount, + 'remain_daily_count' => $remainDailyCount, + 'allow_weekdays' => $cfg['allow_weekdays'], + 'time_start' => $cfg['time_start'], + 'time_end' => $cfg['time_end'], + 'fee_mode' => $cfg['fee_mode'], + 'fee_rate' => $cfg['fee_rate'], + 'fee_fixed' => $cfg['fee_fixed'], + 'fee_min' => $cfg['fee_min'], + 'fee_max' => $cfg['fee_max'], + 'fee_text' => $feeText, + 'transfer_mode' => $cfg['transfer_mode'], + 'audit_mode' => $cfg['audit_mode'], + 'auto_max_amount' => $cfg['auto_max_amount'], + 'arrival_desc' => $arrivalDesc, + 'rule_tip' => $ruleTip, + ]; + } + + /** + * 校验师傅发起的提现请求(全量业务规则 + 微信小程序商家转账硬规范) + * + * @param array $master fa_master 行记录 + * @param float $amount 申请提现金额 + * @param string $channel 申请提现渠道 wechat | bank + * @param string $account 银行卡等收款账号信息 + * @return array ['fee' => float, 'arrive_amount' => float, 'cfg' => array, 'rule_snapshot' => string] + * @throws \InvalidArgumentException + */ + public static function validateWithdrawRequest(array $master, float $amount, string $channel, string $account = ''): array + { + $cfg = self::getWithdrawConfig(); + + if (!$cfg['enabled']) { + throw new \InvalidArgumentException($cfg['close_tip']); + } + + if (!in_array($channel, $cfg['channels'], true)) { + $chLabel = $channel === 'bank' ? '银行储蓄卡' : '微信零钱'; + throw new \InvalidArgumentException("当前暂未开放【{$chLabel}】提现渠道"); + } + + if ($channel === 'bank' && trim($account) === '') { + throw new \InvalidArgumentException('请填写收款银行卡号与开户行信息'); + } + + // 校验允许提现星期与每日时段 + $curWeekday = (int)date('N'); // 1(周一) ~ 7(周日) + if (!in_array($curWeekday, $cfg['allow_weekdays'], true)) { + $dayNames = [1 => '周一', 2 => '周二', 3 => '周三', 4 => '周四', 5 => '周五', 6 => '周六', 7 => '周日']; + $allowLabels = array_map(function ($d) use ($dayNames) { + return $dayNames[$d] ?? ''; + }, $cfg['allow_weekdays']); + throw new \InvalidArgumentException('当前不在提现开放日(开放时间:' . implode('、', array_filter($allowLabels)) . ')'); + } + + $nowHm = date('H:i'); + if ($nowHm < $cfg['time_start'] || $nowHm > $cfg['time_end']) { + throw new \InvalidArgumentException("当前不在每日提现开放时段({$cfg['time_start']} ~ {$cfg['time_end']})"); + } + + // 实名认证合规校验(后台开启强制实名,或微信单笔 >= 2000 元微信官方强制实名) + $isRealnameVerified = (($master['verify_status'] ?? '') === 'pass') && trim((string)($master['realname'] ?? '')) !== ''; + if ($cfg['require_realname'] && !$isRealnameVerified) { + throw new \InvalidArgumentException('根据资金合规要求,请先完成师傅实名认证审核通过后再申请提现'); + } + + $amt = round($amount, 2); + if ($amt <= 0) { + throw new \InvalidArgumentException('请输入有效的提现金额'); + } + if ($amt < $cfg['min_amount']) { + throw new \InvalidArgumentException('单笔提现金额不能低于 ¥' . number_format($cfg['min_amount'], 2, '.', '')); + } + if ($amt > $cfg['max_amount']) { + throw new \InvalidArgumentException('单笔提现金额不能超过 ¥' . number_format($cfg['max_amount'], 2, '.', '')); + } + if ($channel === 'wechat' && $amt > self::WX_TRANSFER_HARD_MAX) { + throw new \InvalidArgumentException('微信零钱单笔提现不能超过官方上限 ¥20,000.00'); + } + + $feeCalc = self::calcWithdrawFee($amt, $cfg); + $arrive = $feeCalc['arrive_amount']; + + if ($channel === 'wechat') { + if ($arrive < self::WX_TRANSFER_MIN_ARRIVE) { + throw new \InvalidArgumentException('扣除手续费后实际到账金额不能低于 ¥0.10(微信转账最低限额)'); + } + if ($arrive >= self::WX_TRANSFER_REALNAME_THRESHOLD && !$isRealnameVerified) { + throw new \InvalidArgumentException('微信官方规定单笔转账满 ¥2,000.00 必须校验真实姓名,请先完成实名认证'); + } + } + + // 单日次数与累计额度校验 + $stats = self::getMasterTodayWithdrawStats((int)$master['id']); + if ($cfg['daily_count'] > 0 && $stats['today_count'] >= $cfg['daily_count']) { + throw new \InvalidArgumentException("今日提现次数已达上限(每日最多 {$cfg['daily_count']} 次),请明日再试"); + } + if ($cfg['daily_amount'] > 0 && round($stats['today_amount'] + $amt, 2) > $cfg['daily_amount']) { + $remain = max(0.00, round($cfg['daily_amount'] - $stats['today_amount'], 2)); + throw new \InvalidArgumentException('超出单日累计提现上限(今日剩余可提额度 ¥' . number_format($remain, 2, '.', '') . ')'); + } + + $snapshot = [ + 'fee_mode' => $cfg['fee_mode'], + 'fee_rate' => $cfg['fee_rate'], + 'fee_fixed' => $cfg['fee_fixed'], + 'fee_min' => $cfg['fee_min'], + 'fee_max' => $cfg['fee_max'], + 'fee' => $feeCalc['fee'], + 'arrive_amount' => $arrive, + 'transfer_mode' => $cfg['transfer_mode'], + 'audit_mode' => $cfg['audit_mode'], + 'snapshot_time' => time(), + ]; + + return [ + 'fee' => $feeCalc['fee'], + 'arrive_amount' => $arrive, + 'cfg' => $cfg, + 'rule_snapshot' => json_encode($snapshot, JSON_UNESCAPED_UNICODE), + ]; + } + + /** + * 后台配置中心保存并严格校验全量配置项 + * + * @param array $input + * @return void + * @throws \InvalidArgumentException + */ + public static function saveConfigs(array $input): void + { + $defaults = self::defaults(); + $clean = []; + + // 1. 校验订单默认抽佣兜底配置 + if (isset($input['default_calc_mode'])) { + if (!in_array($input['default_calc_mode'], ['percent', 'fixed', 'mixed'], true)) { + throw new \InvalidArgumentException('请选择有效的默认抽佣计费方式'); + } + $clean['default_calc_mode'] = $input['default_calc_mode']; + } + if (isset($input['default_commission_rate'])) { + if (!is_numeric($input['default_commission_rate']) || (float)$input['default_commission_rate'] < 0 || (float)$input['default_commission_rate'] > 100) { + throw new \InvalidArgumentException('默认抽佣比例必须在 0% ~ 100% 之间'); + } + $clean['default_commission_rate'] = number_format(round((float)$input['default_commission_rate'], 2), 2, '.', ''); + } + foreach (['default_fixed_fee' => '默认单笔固定抽佣', 'default_min_commission' => '默认单笔保底抽佣', 'default_max_commission' => '默认单笔封顶抽佣'] as $k => $label) { + if (isset($input[$k])) { + if (!is_numeric($input[$k]) || (float)$input[$k] < 0) { + throw new \InvalidArgumentException($label . '不能为负数'); + } + $clean[$k] = number_format(round((float)$input[$k], 2), 2, '.', ''); + } + } + if (isset($clean['default_min_commission'], $clean['default_max_commission'])) { + $minC = (float)$clean['default_min_commission']; + $maxC = (float)$clean['default_max_commission']; + if ($minC > 0 && $maxC > 0 && $minC > $maxC) { + throw new \InvalidArgumentException('默认单笔保底抽佣不能大于单笔封顶抽佣'); + } + } + if (isset($input['include_diff_in_commission'])) { + $clean['include_diff_in_commission'] = (int)$input['include_diff_in_commission'] === 1 ? '1' : '0'; + } + + // 2. 校验提现与微信小程序商家转账配置 + if (isset($input['withdraw_enabled'])) { + $clean['withdraw_enabled'] = (int)$input['withdraw_enabled'] === 1 ? '1' : '0'; + } + if (isset($input['withdraw_close_tip'])) { + $clean['withdraw_close_tip'] = mb_substr(trim((string)$input['withdraw_close_tip']), 0, 200); + } + if (isset($input['withdraw_channels'])) { + $chArr = is_array($input['withdraw_channels']) ? $input['withdraw_channels'] : explode(',', (string)$input['withdraw_channels']); + $validCh = []; + foreach ($chArr as $c) { + $c = trim((string)$c); + if (in_array($c, ['wechat', 'bank'], true) && !in_array($c, $validCh, true)) { + $validCh[] = $c; + } + } + if (empty($validCh)) { + throw new \InvalidArgumentException('请至少勾选一种开放提现渠道'); + } + $clean['withdraw_channels'] = implode(',', $validCh); + } + if (isset($input['withdraw_require_realname'])) { + $clean['withdraw_require_realname'] = (int)$input['withdraw_require_realname'] === 1 ? '1' : '0'; + } + + if (isset($input['withdraw_min_amount'])) { + if (!is_numeric($input['withdraw_min_amount']) || (float)$input['withdraw_min_amount'] < 0.10) { + throw new \InvalidArgumentException('符合微信小程序商家转账规范,单笔最低提现金额不得低于 ¥0.10'); + } + $clean['withdraw_min_amount'] = number_format(round((float)$input['withdraw_min_amount'], 2), 2, '.', ''); + } + if (isset($input['withdraw_max_amount'])) { + if (!is_numeric($input['withdraw_max_amount']) || (float)$input['withdraw_max_amount'] > self::WX_TRANSFER_HARD_MAX) { + throw new \InvalidArgumentException('微信商家转账官方规定单笔最高金额不得超过 ¥20,000.00'); + } + $clean['withdraw_max_amount'] = number_format(round((float)$input['withdraw_max_amount'], 2), 2, '.', ''); + } + if (isset($clean['withdraw_min_amount'], $clean['withdraw_max_amount']) && (float)$clean['withdraw_min_amount'] > (float)$clean['withdraw_max_amount']) { + throw new \InvalidArgumentException('单笔最低提现金额不能大于单笔最高提现金额'); + } + + if (isset($input['withdraw_daily_amount'])) { + if (!is_numeric($input['withdraw_daily_amount']) || (float)$input['withdraw_daily_amount'] < 0) { + throw new \InvalidArgumentException('单人单日累计提现上限不能为负数'); + } + $clean['withdraw_daily_amount'] = number_format(round((float)$input['withdraw_daily_amount'], 2), 2, '.', ''); + if ((float)$clean['withdraw_daily_amount'] > 0 && isset($clean['withdraw_min_amount']) && (float)$clean['withdraw_daily_amount'] < (float)$clean['withdraw_min_amount']) { + throw new \InvalidArgumentException('单人单日累计上限不能低于单笔最低提现金额'); + } + } + if (isset($input['withdraw_daily_count'])) { + $clean['withdraw_daily_count'] = (string)max(0, (int)$input['withdraw_daily_count']); + } + + if (isset($input['withdraw_allow_weekdays'])) { + $dayArr = is_array($input['withdraw_allow_weekdays']) ? $input['withdraw_allow_weekdays'] : explode(',', (string)$input['withdraw_allow_weekdays']); + $validDays = []; + foreach ($dayArr as $d) { + $di = (int)$d; + if ($di >= 1 && $di <= 7 && !in_array($di, $validDays, true)) { + $validDays[] = $di; + } + } + if (empty($validDays)) { + throw new \InvalidArgumentException('请至少选择一天允许提现的周期'); + } + sort($validDays); + $clean['withdraw_allow_weekdays'] = implode(',', $validDays); + } + + if (isset($input['withdraw_time_start'])) { + $ts = trim((string)$input['withdraw_time_start']); + if (!preg_match('/^([01]\d|2[0-3]):[0-5]\d$/', $ts)) { + throw new \InvalidArgumentException('每日开始时间格式须为 HH:mm(如 00:00)'); + } + $clean['withdraw_time_start'] = $ts; + } + if (isset($input['withdraw_time_end'])) { + $te = trim((string)$input['withdraw_time_end']); + if (!preg_match('/^([01]\d|2[0-3]):[0-5]\d$/', $te)) { + throw new \InvalidArgumentException('每日结束时间格式须为 HH:mm(如 23:59)'); + } + $clean['withdraw_time_end'] = $te; + } + if (isset($clean['withdraw_time_start'], $clean['withdraw_time_end']) && $clean['withdraw_time_start'] >= $clean['withdraw_time_end']) { + throw new \InvalidArgumentException('每日允许提现开始时间必须早于结束时间'); + } + + if (isset($input['withdraw_fee_mode'])) { + if (!in_array($input['withdraw_fee_mode'], ['percent', 'fixed', 'free'], true)) { + throw new \InvalidArgumentException('请选择有效的手续费计收方式'); + } + $clean['withdraw_fee_mode'] = $input['withdraw_fee_mode']; + } + if (isset($input['withdraw_fee_rate'])) { + if (!is_numeric($input['withdraw_fee_rate']) || (float)$input['withdraw_fee_rate'] < 0 || (float)$input['withdraw_fee_rate'] > 50) { + throw new \InvalidArgumentException('提现手续费率须在 0% ~ 50% 之间'); + } + $clean['withdraw_fee_rate'] = number_format(round((float)$input['withdraw_fee_rate'], 2), 2, '.', ''); + } + foreach (['withdraw_fee_fixed' => '单笔固定手续费', 'withdraw_fee_min' => '单笔最低保底手续费', 'withdraw_fee_max' => '单笔最高封顶手续费'] as $k => $label) { + if (isset($input[$k])) { + if (!is_numeric($input[$k]) || (float)$input[$k] < 0) { + throw new \InvalidArgumentException($label . '不能为负数'); + } + $clean[$k] = number_format(round((float)$input[$k], 2), 2, '.', ''); + } + } + if (isset($clean['withdraw_fee_min'], $clean['withdraw_fee_max'])) { + $fMin = (float)$clean['withdraw_fee_min']; + $fMax = (float)$clean['withdraw_fee_max']; + if ($fMin > 0 && $fMax > 0 && $fMin > $fMax) { + throw new \InvalidArgumentException('单笔最低保底手续费不能大于最高封顶手续费'); + } + } + + // 校验最低提现额扣除手续费后不低于微信官方 0.10 元底线 + $mergedPreview = array_merge(self::getAll(), $clean); + $minTestAmt = (float)$mergedPreview['withdraw_min_amount']; + if (($mergedPreview['withdraw_fee_mode'] ?? 'percent') === 'fixed' && $minTestAmt - (float)$mergedPreview['withdraw_fee_fixed'] < self::WX_TRANSFER_MIN_ARRIVE) { + throw new \InvalidArgumentException('单笔最低提现金额扣除固定手续费后实际到账低于 ¥0.10,请调高最低提现额或调低固定手续费'); + } + + if (isset($input['withdraw_transfer_mode'])) { + if (!in_array($input['withdraw_transfer_mode'], ['wx_mch_transfer', 'offline_manual'], true)) { + throw new \InvalidArgumentException('无效的微信渠道打款执行方式'); + } + $clean['withdraw_transfer_mode'] = $input['withdraw_transfer_mode']; + } + if (isset($input['withdraw_audit_mode'])) { + if (!in_array($input['withdraw_audit_mode'], ['manual', 'auto_small'], true)) { + throw new \InvalidArgumentException('无效的提现审核模式'); + } + $clean['withdraw_audit_mode'] = $input['withdraw_audit_mode']; + } + if (isset($input['withdraw_auto_max_amount'])) { + if (!is_numeric($input['withdraw_auto_max_amount']) || (float)$input['withdraw_auto_max_amount'] < 0 || (float)$input['withdraw_auto_max_amount'] > self::WX_TRANSFER_HARD_MAX) { + throw new \InvalidArgumentException('小额免审自动转账上限须在 ¥0 ~ ¥20,000 之间'); + } + $clean['withdraw_auto_max_amount'] = number_format(round((float)$input['withdraw_auto_max_amount'], 2), 2, '.', ''); + } + + foreach ([ + 'wx_transfer_scene_id' => ['label' => '微信转账场景ID', 'max' => 16], + 'wx_recv_perception' => ['label' => '微信收款用户感知', 'max' => 32], + 'wx_job_type' => ['label' => '微信场景报备-岗位类型', 'max' => 32], + 'wx_reward_desc' => ['label' => '微信场景报备-报酬说明', 'max' => 32], + 'wx_transfer_remark' => ['label' => '微信零钱转账备注', 'max' => 32], + ] as $k => $meta) { + if (isset($input[$k])) { + $val = trim((string)$input[$k]); + if ($val === '') { + throw new \InvalidArgumentException($meta['label'] . '不能为空(微信支付 APIv3 商家转账必填项)'); + } + if (mb_strlen($val) > $meta['max']) { + throw new \InvalidArgumentException($meta['label'] . '不能超过 ' . $meta['max'] . ' 个字符(微信接口长度限制)'); + } + $clean[$k] = $val; + } + } + + $now = time(); + Db::startTrans(); + try { + foreach ($clean as $name => $val) { + if (!array_key_exists($name, $defaults)) { + continue; + } + $group = strpos($name, 'default_') === 0 || $name === 'include_diff_in_commission' ? 'commission' : 'withdraw'; + $exists = Db::name('finance_config')->where('name', $name)->find(); + if ($exists) { + Db::name('finance_config')->where('id', (int)$exists['id'])->update([ + 'value' => $val, + 'updatetime' => $now, + ]); + } else { + Db::name('finance_config')->insert([ + 'name' => $name, + 'group' => $group, + 'title' => $name, + 'value' => $val, + 'remark' => '', + 'updatetime' => $now, + ]); + } + } + Db::commit(); + } catch (\Throwable $e) { + Db::rollback(); + throw $e; + } + + self::clearCache(); + } +} diff --git a/server/application/common/library/PaymentService.php b/server/application/common/library/PaymentService.php new file mode 100644 index 0000000..3784690 --- /dev/null +++ b/server/application/common/library/PaymentService.php @@ -0,0 +1,1923 @@ + 0 ? (float)$cfg['daily_amount'] : (float)$cfg['max_amount']; + return [ + 'min_amount' => (float)$cfg['min_amount'], + 'single_limit' => (float)$cfg['max_amount'], + 'daily_user_limit' => $dailyLimit, + 'fee_mode' => $cfg['fee_mode'], + 'fee_rate' => $cfg['fee_mode'] === 'percent' ? round((float)$cfg['fee_rate'] / 100, 6) : 0.0, + 'fee_fixed' => (float)$cfg['fee_fixed'], + 'fee_min' => (float)$cfg['fee_min'], + 'fee_max' => (float)$cfg['fee_max'], + ]; + } + + /** + * 解析并严格校验当前操作用户的微信 OpenID(必须在数据库事务外部调用) + * + * 规则:一旦前端传入 wxCode,必须实时向微信换取当前操作微信号的 OpenID, + * 严禁在换取失败时静默回退旧数据库记录,防止张冠李戴或掩盖 AppID/Secret 配置错误。 + * + * @param int $userId 当前登录用户 ID + * @param string $wxCode 小程序前端实时获取的 wx.login code + * @return string + */ + public static function resolveUserOpenid($userId, $wxCode = '') + { + $userId = (int)$userId; + $wxCode = trim((string)$wxCode); + + // 1. 传入实时 wx_code 时,必须向微信官方换取并校验与当前账号的绑定关系 + if ($wxCode !== '') { + $wx = WxLogin::jscode2session($wxCode); + if (!is_array($wx) || !empty($wx['error']) || empty($wx['openid'])) { + $errMsg = (is_array($wx) && !empty($wx['error'])) + ? (string)$wx['error'] + : '获取微信用户身份标识(OpenID)失败,请在微信小程序内重新登录后再试'; + Log::error("[PaymentService::resolveUserOpenid] jscode2session 失败 user_id={$userId}: {$errMsg}"); + throw new \RuntimeException($errMsg); + } + + $liveOpenid = trim((string)$wx['openid']); + $unionid = isset($wx['unionid']) ? trim((string)$wx['unionid']) : ''; + $sessionKey = isset($wx['session_key']) ? trim((string)$wx['session_key']) : ''; + $now = time(); + + $existByOpenid = Db::name('user_wechat')->where('openid', $liveOpenid)->find(); + if ($existByOpenid) { + $boundUid = (int)$existByOpenid['user_id']; + if ($boundUid > 0 && $boundUid !== $userId) { + Log::error("[PaymentService::resolveUserOpenid] OpenID归属冲突 current_uid={$userId}, bound_uid={$boundUid}"); + throw new \InvalidArgumentException('当前微信号已绑定平台其他账号(用户ID:' . $boundUid . '),请确认当前登录手机号与微信号一致后再操作'); + } + Db::name('user_wechat')->where('id', (int)$existByOpenid['id'])->update([ + 'user_id' => $userId, + 'unionid' => $unionid !== '' ? $unionid : (string)$existByOpenid['unionid'], + 'session_key' => $sessionKey !== '' ? $sessionKey : (string)$existByOpenid['session_key'], + 'updatetime' => $now, + ]); + } else { + Db::name('user_wechat')->insert([ + 'user_id' => $userId, + 'openid' => $liveOpenid, + 'unionid' => $unionid, + 'session_key' => $sessionKey, + 'createtime' => $now, + 'updatetime' => $now, + ]); + } + + return $liveOpenid; + } + + // 2. 未传 wx_code 时(如非小程序直发场景或历史绑定核对),读取当前账号唯一绑定记录 + if ($userId > 0) { + $bind = Db::name('user_wechat') + ->where('user_id', $userId) + ->where('openid', '<>', '') + ->order('updatetime desc, id desc') + ->find(); + if ($bind && !empty($bind['openid'])) { + return trim((string)$bind['openid']); + } + } + + throw new \InvalidArgumentException('未获取到当前微信授权信息(OpenID 缺失),请在微信小程序内打开并登录后再操作'); + } + + /** + * 用户端统一发起微信 JSAPI 支付(覆盖 order 首单支付 / quote 选师傅报价支付 / diff 施工增项补差价支付) + * + * @param array $order 已通过归属校验的订单记录 + * @param int $operatorUid 当前点击支付的用户 ID + * @param string $scene order | quote | diff + * @param int $quoteId 当 scene=quote 时必填的报价单 ID + * @param string $wxCode 小程序端 wx.login code + * @return array + */ + public static function createOrderJsapiPay(array $order, $operatorUid, $scene = 'order', $quoteId = 0, $wxCode = '') + { + $orderId = (int)$order['id']; + $operatorUid = (int)$operatorUid; + $quoteId = (int)$quoteId; + $now = time(); + + if (!in_array($scene, ['order', 'quote', 'diff'], true)) { + throw new \InvalidArgumentException('不支持的支付场景'); + } + + $payAmount = 0.00; + $description = '优匠优服-' . ($order['service_name'] ?: '上门安装服务'); + $prefix = 'PO'; + + if ($scene === 'order') { + if ((int)$order['pay_status'] === 1) { + throw new \InvalidArgumentException('订单已支付,请勿重复支付'); + } + if ((int)$order['status'] !== OrderStatus::PENDING_PAY) { + throw new \InvalidArgumentException('当前订单状态不允许支付'); + } + $payAmount = round((float)$order['pay_amount'], 2); + + // 优惠券全额抵扣 0 元免付直通 + if ($payAmount <= 0) { + return self::completeZeroOrderPay($orderId, $operatorUid); + } + $prefix = 'PO'; + } elseif ($scene === 'quote') { + if ($order['price_type'] !== 'quote') { + throw new \InvalidArgumentException('该订单不是师傅报价订单'); + } + if ((int)$order['status'] !== OrderStatus::QUOTING) { + throw new \InvalidArgumentException('当前状态(' . OrderStatus::label((int)$order['status']) . ')不可选择报价'); + } + if ((int)$order['master_id'] > 0) { + throw new \InvalidArgumentException('该订单已选定师傅,请勿重复选择'); + } + $quote = Db::name('order_quote')->where('id', $quoteId)->where('order_id', $orderId)->find(); + if (!$quote) { + throw new \InvalidArgumentException('报价记录不存在'); + } + if ($quote['status'] !== 'wait') { + throw new \InvalidArgumentException('该报价已失效,请选择其他师傅'); + } + if ((int)$quote['expire_time'] > 0 && (int)$quote['expire_time'] < $now) { + Db::name('order_quote')->where('id', $quoteId)->update(['status' => 'expired']); + throw new \InvalidArgumentException('该报价已过期,请选择其他师傅'); + } + $master = Db::name('master')->where('id', (int)$quote['master_id'])->find(); + if (!$master) { + throw new \InvalidArgumentException('报价师傅信息不存在'); + } + + $quoteAmount = round((float)$quote['amount'], 2); + $targetTotalPay = max(0, round($quoteAmount - (float)$order['discount_amount'], 2)); + $alreadyPaid = (int)$order['pay_status'] === 1 ? (float)$order['pay_amount'] : 0.00; + $payAmount = max(0, round($targetTotalPay - $alreadyPaid, 2)); + + // 无需补付(0元或已付足)直接完成选定师傅 + if ($payAmount <= 0) { + return self::completeZeroQuoteChoose($orderId, $quoteId, $operatorUid); + } + $description = '优匠优服-选定师傅报价(' . $order['order_no'] . ')'; + $prefix = 'PQ'; + } elseif ($scene === 'diff') { + if ((int)$order['diff_status'] !== 1 || (float)$order['diff_amount'] <= 0) { + throw new \InvalidArgumentException('当前订单无待支付的增项补差价'); + } + $payAmount = round((float)$order['diff_amount'], 2); + $description = '优匠优服-施工增项补差价(' . $order['order_no'] . ')'; + $prefix = 'PD'; + } + + // 1. 事务外解析并严格校验付款人微信 OpenID + $openid = self::resolveUserOpenid($operatorUid, $wxCode); + $wxSvc = new WxPayV3Service(); + + // 2. 先核查同订单同场景历史未决待支付单,防漏回调重付或遗留未关旧单 + $pendingOlds = Db::name('pay_order') + ->where('order_id', $orderId) + ->where('scene', $scene) + ->where('status', 'pending') + ->order('id desc') + ->limit(3) + ->select(); + foreach ($pendingOlds as $oldPo) { + $oldOutNo = trim((string)$oldPo['out_trade_no']); + if ($oldOutNo === '') { + continue; + } + try { + $oldQuery = $wxSvc->queryOrderByOutTradeNo($oldOutNo); + $oldState = strtoupper((string)($oldQuery['trade_state'] ?? '')); + if ($oldState === 'SUCCESS') { + // 发现该笔旧单在微信侧实际已扣款成功,立即执行幂等核销并返回已支付结果,杜绝重复扣款 + $confirmed = self::confirmOrderPay($oldOutNo, $oldQuery); + return array_merge($confirmed, [ + 'need_wxpay' => false, + 'paid' => true, + 'out_trade_no' => $oldOutNo, + 'amount' => (float)$oldPo['amount'], + ]); + } + if (in_array($oldState, ['NOTPAY', 'USERPAYING'], true)) { + $wxSvc->closeOrder($oldOutNo); + } + Db::name('pay_order')->where('id', (int)$oldPo['id'])->update([ + 'status' => 'closed', + 'fail_code' => 'REPLACED_CLOSE', + 'fail_reason' => '用户重新发起支付(原状态:' . self::tradeStateLabel($oldState) . '),系统已关闭旧待支付单', + 'last_sync_time' => time(), + 'updatetime' => time(), + ]); + } catch (\Throwable $oldEx) { + $oldFail = WxPayV3Service::extractFailInfo($oldEx); + if (in_array($oldFail['code'], ['ORDERNOTEXIST', 'ORDER_NOT_EXIST', 'ORDERCLOSED'], true)) { + Db::name('pay_order')->where('id', (int)$oldPo['id'])->update([ + 'status' => 'closed', + 'fail_code' => $oldFail['code'], + 'fail_reason' => '旧待支付单已失效或未在微信生成(' . $oldFail['reason'] . ')', + 'last_sync_time' => time(), + 'updatetime' => time(), + ]); + continue; + } + Db::name('pay_order')->where('id', (int)$oldPo['id'])->update([ + 'fail_code' => $oldFail['code'], + 'fail_reason' => '重新下单前核查/关闭旧待支付单失败:' . $oldFail['reason'], + 'last_sync_time' => time(), + 'updatetime' => time(), + ]); + self::addOrderLog($orderId, (int)$order['status'], '核查旧待支付单(' . $oldOutNo . ')失败:' . $oldFail['reason'], 'system', $operatorUid); + throw $oldEx; + } + } + + // 3. 创建本地待支付交易流水单 + $outTradeNo = $prefix . date('YmdHis', $now) . str_pad((string)mt_rand(0, 999999), 6, '0', STR_PAD_LEFT); + $payOrderId = (int)Db::name('pay_order')->insertGetId([ + 'out_trade_no' => $outTradeNo, + 'order_id' => $orderId, + 'order_no' => (string)$order['order_no'], + 'user_id' => $operatorUid, + 'scene' => $scene, + 'quote_id' => $quoteId, + 'openid' => $openid, + 'amount' => $payAmount, + 'status' => 'pending', + 'createtime' => $now, + 'updatetime' => $now, + ]); + + // 4. 事务外调用微信支付 APIv3 JSAPI 统一下单,任何失败均持久化写入管理员可读中文原因 + try { + $wxResp = $wxSvc->createJsapiOrder([ + 'out_trade_no' => $outTradeNo, + 'description' => $description, + 'amount' => $payAmount, + 'openid' => $openid, + 'attach' => json_encode([ + 'scene' => $scene, + 'order_id' => $orderId, + 'quote_id' => $quoteId, + ], JSON_UNESCAPED_UNICODE), + ]); + } catch (\Throwable $e) { + $fail = WxPayV3Service::extractFailInfo($e); + $errNow = time(); + Db::name('pay_order')->where('id', $payOrderId)->update([ + 'status' => 'pay_failed', + 'fail_code' => $fail['code'], + 'fail_reason' => $fail['reason'], + 'last_sync_time' => $errNow, + 'updatetime' => $errNow, + ]); + self::addOrderLog($orderId, (int)$order['status'], '发起微信统一下单失败(' . $outTradeNo . '):' . $fail['reason'], 'system', $operatorUid); + Log::error("[PaymentService::createOrderJsapiPay] 统一下单失败 order_id={$orderId}, out_trade_no={$outTradeNo}, code={$fail['code']}, reason={$fail['reason']}"); + throw $e; + } + + Db::name('pay_order')->where('id', $payOrderId)->update([ + 'prepay_id' => (string)$wxResp['prepay_id'], + 'last_sync_time' => time(), + 'updatetime' => time(), + ]); + + return [ + 'need_wxpay' => true, + 'paid' => false, + 'scene' => $scene, + 'order_id' => $orderId, + 'out_trade_no' => $outTradeNo, + 'amount' => $payAmount, + 'pay_params' => $wxResp['pay_params'], + ]; + } + + /** + * 确认并核销微信支付单(支持微信异步回调与小程序端支付完成后主动查单双重触发,100% 幂等) + * + * @param string $outTradeNo 商户支付单号 + * @param array|null $wxTxData 异步回调已解密的交易数据;传 null 时自动向微信网关主动查单 + * @return array + */ + public static function confirmOrderPay($outTradeNo, array $wxTxData = null) + { + $outTradeNo = trim((string)$outTradeNo); + if ($outTradeNo === '') { + throw new \InvalidArgumentException('缺少支付单号 out_trade_no'); + } + + $payOrder = Db::name('pay_order')->where('out_trade_no', $outTradeNo)->find(); + if (!$payOrder) { + throw new \InvalidArgumentException('支付交易单不存在'); + } + + // 快速幂等返回 + if (in_array($payOrder['status'], ['paid', 'refunding', 'refunded', 'refund_failed'], true)) { + return [ + 'paid' => true, + 'already_processed' => true, + 'order_id' => (int)$payOrder['order_id'], + 'scene' => (string)$payOrder['scene'], + ]; + } + + $wxSvc = new WxPayV3Service(); + + // 若未传入已验签解密的回调报文,则在事务外向微信支付网关主动查单核实 + if ($wxTxData === null) { + try { + $wxTxData = $wxSvc->queryOrderByOutTradeNo($outTradeNo); + } catch (\Throwable $qe) { + $fail = WxPayV3Service::extractFailInfo($qe); + $errNow = time(); + $upd = [ + 'fail_code' => $fail['code'], + 'fail_reason' => '向微信主动查询支付状态失败:' . $fail['reason'], + 'last_sync_time' => $errNow, + 'updatetime' => $errNow, + ]; + if (in_array($fail['code'], ['ORDERNOTEXIST', 'ORDER_NOT_EXIST'], true) && $payOrder['status'] === 'pending') { + $upd['status'] = 'closed'; + } + Db::name('pay_order')->where('id', (int)$payOrder['id'])->update($upd); + Log::error("[PaymentService::confirmOrderPay] 查单失败 out_trade_no={$outTradeNo}, code={$fail['code']}, reason={$fail['reason']}"); + throw $qe; + } + } + + // 严格核对报文中 AppID / MchID 是否属于本系统配置,防止回调或应答串单 + $respMchId = trim((string)($wxTxData['mchid'] ?? '')); + $respAppId = trim((string)($wxTxData['appid'] ?? '')); + if ($respMchId !== '' && $respMchId !== $wxSvc->getMchId()) { + $msg = '微信返回商户号(' . $respMchId . ')与本系统配置(' . $wxSvc->getMchId() . ')不一致,系统已拦截入账'; + Db::name('pay_order')->where('id', (int)$payOrder['id'])->update([ + 'fail_code' => 'MCHID_MISMATCH', + 'fail_reason' => $msg, + 'last_sync_time' => time(), + 'updatetime' => time(), + ]); + throw new \RuntimeException($msg); + } + if ($respAppId !== '' && $respAppId !== $wxSvc->getAppId()) { + $msg = '微信返回 AppID(' . $respAppId . ')与本系统配置(' . $wxSvc->getAppId() . ')不一致,系统已拦截入账'; + Db::name('pay_order')->where('id', (int)$payOrder['id'])->update([ + 'fail_code' => 'APPID_MISMATCH', + 'fail_reason' => $msg, + 'last_sync_time' => time(), + 'updatetime' => time(), + ]); + throw new \RuntimeException($msg); + } + + $tradeState = strtoupper((string)($wxTxData['trade_state'] ?? '')); + $tradeStateDesc = trim((string)($wxTxData['trade_state_desc'] ?? '')); + $tradeStateZh = self::tradeStateLabel($tradeState, $tradeStateDesc); + + if ($tradeState !== 'SUCCESS') { + $syncNow = time(); + $upd = [ + 'last_sync_time' => $syncNow, + 'updatetime' => $syncNow, + ]; + if ($tradeState === 'PAYERROR') { + $upd['status'] = 'pay_failed'; + $upd['fail_code'] = 'PAYERROR'; + $upd['fail_reason'] = $tradeStateZh; + } elseif (in_array($tradeState, ['CLOSED', 'REVOKED'], true)) { + $upd['status'] = 'closed'; + $upd['fail_code'] = $tradeState; + $upd['fail_reason'] = $tradeStateZh; + } else { + $upd['fail_code'] = $tradeState ?: 'NOTPAY'; + $upd['fail_reason'] = $tradeStateZh; + } + Db::name('pay_order')->where('id', (int)$payOrder['id'])->where('status', 'pending')->update($upd); + + return [ + 'paid' => false, + 'trade_state' => $tradeState ?: 'NOTPAY', + 'trade_state_text' => $tradeStateZh, + 'order_id' => (int)$payOrder['order_id'], + 'scene' => (string)$payOrder['scene'], + ]; + } + + // 严格核对实付分值金额,防御金额不一致风险 + $paidFen = (int)($wxTxData['amount']['total'] ?? 0); + $expectFen = (int)round(((float)$payOrder['amount']) * 100); + if ($paidFen !== $expectFen) { + $mismatchMsg = '支付金额校验异常(应付 ' . $expectFen . ' 分,微信实付 ' . $paidFen . ' 分),系统已拦截入账,请联系财务核查'; + Db::name('pay_order')->where('id', (int)$payOrder['id'])->update([ + 'fail_code' => 'AMOUNT_MISMATCH', + 'fail_reason' => $mismatchMsg, + 'last_sync_time' => time(), + 'updatetime' => time(), + ]); + self::addOrderLog((int)$payOrder['order_id'], 0, '微信支付金额校验失败(' . $outTradeNo . '):' . $mismatchMsg, 'system', (int)$payOrder['user_id']); + Log::error("[PaymentService::confirmOrderPay] 金额校验异常 out_trade_no={$outTradeNo}, expect={$expectFen}, actual={$paidFen}"); + throw new \RuntimeException($mismatchMsg); + } + + $transactionId = trim((string)($wxTxData['transaction_id'] ?? '')); + $payTime = !empty($wxTxData['success_time']) ? (strtotime((string)$wxTxData['success_time']) ?: time()) : time(); + $now = time(); + + $broadcastOrder = null; + $broadcastStatus = 0; + $autoRefundDuplicate = false; + + Db::startTrans(); + try { + $lockedPay = Db::name('pay_order')->where('id', (int)$payOrder['id'])->lock(true)->find(); + if (!$lockedPay || in_array($lockedPay['status'], ['paid', 'refunding', 'refunded', 'refund_failed'], true)) { + Db::commit(); + return [ + 'paid' => true, + 'already_processed' => true, + 'order_id' => (int)$payOrder['order_id'], + 'scene' => (string)$payOrder['scene'], + ]; + } + + $orderId = (int)$lockedPay['order_id']; + $lockedOrder = Db::name('order')->where('id', $orderId)->lock(true)->find(); + if (!$lockedOrder) { + throw new \RuntimeException('关联业务工单不存在'); + } + + Db::name('pay_order')->where('id', (int)$lockedPay['id'])->update([ + 'status' => 'paid', + 'transaction_id' => $transactionId, + 'fail_code' => '', + 'fail_reason' => '', + 'pay_time' => $payTime, + 'last_sync_time' => $now, + 'updatetime' => $now, + ]); + + $scene = (string)$lockedPay['scene']; + $uid = (int)$lockedPay['user_id']; + + if ($scene === 'order') { + if ((int)$lockedOrder['pay_status'] === 1 || (int)$lockedOrder['status'] !== OrderStatus::PENDING_PAY) { + // 极端重复支付单:标记在事务外自动原路退回重复的一笔 + $autoRefundDuplicate = true; + } else { + self::lockAndConsumeOrderCoupon($lockedOrder, $now); + + $nextStatus = $lockedOrder['price_type'] === 'quote' ? OrderStatus::QUOTING : OrderStatus::PENDING_ACCEPT; + $nextRemark = $nextStatus === OrderStatus::QUOTING + ? '微信支付成功,订单已进入报价大厅,等待师傅报价' + : '微信支付成功,订单已进入接单大厅'; + + Db::name('order')->where('id', $orderId)->update([ + 'status' => $nextStatus, + 'pay_status' => 1, + 'pay_type' => 'wechat', + 'pay_time' => $payTime, + 'transaction_id' => $transactionId, + 'updatetime' => $now, + ]); + self::addOrderLog($orderId, $nextStatus, $nextRemark, 'user', $uid); + self::addOrderLog($orderId, $nextStatus, $nextStatus === OrderStatus::QUOTING ? '订单进入师傅报价大厅' : '订单进入师傅接单大厅', 'system', 0); + + $broadcastOrder = $lockedOrder; + $broadcastStatus = $nextStatus; + } + } elseif ($scene === 'quote') { + $quoteId = (int)$lockedPay['quote_id']; + $quote = Db::name('order_quote')->where('id', $quoteId)->where('order_id', $orderId)->lock(true)->find(); + $master = $quote ? Db::name('master')->where('id', (int)$quote['master_id'])->find() : null; + + if ( + !$quote + || !$master + || (int)$lockedOrder['status'] !== OrderStatus::QUOTING + || (int)$lockedOrder['master_id'] !== 0 + ) { + $autoRefundDuplicate = true; + } else { + $quoteAmount = round((float)$quote['amount'], 2); + $finalPayAmt = max(0, round($quoteAmount - (float)$lockedOrder['discount_amount'], 2)); + $paidAmt = round((float)$lockedPay['amount'], 2); + $snapRefreshed = CommissionService::refreshQuoteOrderSnapshotOnAmountConfirmed( + $finalPayAmt > 0 ? $finalPayAmt : $quoteAmount, + $lockedOrder['commission_snapshot'] ?? null, + $lockedOrder['commission_rate'] ?? null + ); + + $remark = '客户已选择 ' . $master['realname'] . ' 的报价 ¥' . $quoteAmount . '(微信支付 ¥' . $paidAmt . '),等待师傅联系'; + + $quoteUpdateData = [ + 'status' => OrderStatus::ACCEPTED, + 'master_id' => (int)$quote['master_id'], + 'accept_time' => $payTime, + 'base_amount' => $quoteAmount, + 'floor_amount' => 0, + 'urgent_amount' => 0, + 'total_amount' => $quoteAmount, + 'pay_amount' => $finalPayAmt, + 'commission_rate' => $snapRefreshed['commission_rate'], + 'pay_status' => 1, + 'pay_type' => 'wechat', + 'pay_time' => $payTime, + 'transaction_id' => $transactionId, + 'updatetime' => $now, + ]; + if ($snapRefreshed['commission_snapshot'] !== null) { + $quoteUpdateData['commission_snapshot'] = $snapRefreshed['commission_snapshot']; + } + Db::name('order')->where('id', $orderId)->update($quoteUpdateData); + self::addOrderLog($orderId, OrderStatus::ACCEPTED, $remark, 'user', $uid); + + Db::name('order_quote')->where('id', $quoteId)->update(['status' => 'accept']); + Db::name('order_quote')->where('order_id', $orderId)->where('id', '<>', $quoteId) + ->where('status', 'wait')->update(['status' => 'reject']); + + self::sendNotify('user', (int)$lockedOrder['user_id'], '已选定服务师傅', $remark, 'order', $orderId); + self::sendNotify('master', (int)$master['id'], '您的报价已被选中', '订单 ' . $lockedOrder['order_no'] . ' 客户已选择您的报价 ¥' . $quoteAmount . ' 并完成支付托管,请尽快联系客户', 'order', $orderId); + } + } elseif ($scene === 'diff') { + if ((int)$lockedOrder['diff_status'] !== 1) { + $autoRefundDuplicate = true; + } else { + $diffAmount = round((float)$lockedPay['amount'], 2); + $newPayAmount = round((float)$lockedOrder['pay_amount'] + $diffAmount, 2); + + Db::name('order')->where('id', $orderId)->update([ + 'diff_status' => 2, + 'pay_amount' => $newPayAmount, + 'updatetime' => $now, + ]); + self::addOrderLog($orderId, (int)$lockedOrder['status'], '用户微信支付施工增项补差价 ¥' . $diffAmount, 'user', $uid); + + if ((int)$lockedOrder['master_id'] > 0) { + self::sendNotify('master', (int)$lockedOrder['master_id'], '客户已支付增项补差价', '工单 ' . $lockedOrder['order_no'] . ' 客户已微信支付增项补差价 ¥' . $diffAmount, 'order', $orderId); + } + } + } + + Db::commit(); + } catch (\Throwable $e) { + Db::rollback(); + throw $e; + } + + // 事务外:若检测到极端并发导致的重复付款单,立即自动原路退回该单;如自动退款失败必须持久落库并告警 + $duplicateRefundError = ''; + if ($autoRefundDuplicate) { + try { + self::refundSinglePayOrder((int)$payOrder['id'], '重复支付自动原路退回'); + } catch (\Throwable $re) { + $fail = WxPayV3Service::extractFailInfo($re); + $duplicateRefundError = '检测到重复支付单(' . $outTradeNo . '),但自动原路退款失败:' . $fail['reason']; + Db::name('pay_order')->where('id', (int)$payOrder['id'])->update([ + 'status' => 'refund_failed', + 'refund_status' => 'abnormal', + 'fail_code' => $fail['code'], + 'fail_reason' => mb_substr($duplicateRefundError, 0, 480), + 'last_sync_time' => time(), + 'updatetime' => time(), + ]); + self::addOrderLog((int)$payOrder['order_id'], 0, '【高危财务异常】' . $duplicateRefundError, 'system', (int)$payOrder['user_id']); + Log::error('[PaymentService::confirmOrderPay] ' . $duplicateRefundError); + } + } + + // 事务外:新工单广播给在线认证师傅 + if ($broadcastOrder && in_array($broadcastStatus, [OrderStatus::PENDING_ACCEPT, OrderStatus::QUOTING], true)) { + self::broadcastNewOrderToMasters($broadcastOrder); + } + + return [ + 'paid' => true, + 'already_processed' => false, + 'order_id' => (int)$payOrder['order_id'], + 'scene' => (string)$payOrder['scene'], + 'transaction_id' => $transactionId, + 'duplicate_refund_error' => $duplicateRefundError, + ]; + } + + /** + * 取消订单时将该订单名下所有已支付的真实微信交易流水逐笔原路退回(必须在事务外部调用) + * + * @param int $orderId 业务订单 ID + * @param string $reason 退款原因 + * @return float 实际触发或已完成的微信退款总额 + */ + public static function refundOrder($orderId, $reason = '订单取消原路退款') + { + $orderId = (int)$orderId; + $order = Db::name('order')->where('id', $orderId)->find(); + if (!$order) { + throw new \InvalidArgumentException('业务订单不存在,无法核对退款流水'); + } + + $allPayOrders = Db::name('pay_order') + ->where('order_id', $orderId) + ->order('id asc') + ->select(); + + // 统计有效已付款流水(已支付、退款中、退款失败待重退、已退款) + $effectiveRows = []; + foreach ($allPayOrders as $po) { + if (in_array($po['status'], ['paid', 'refunding', 'refunded', 'refund_failed'], true)) { + $effectiveRows[] = $po; + } + } + + $orderPayStatus = (int)$order['pay_status']; + $orderPayAmount = round((float)$order['pay_amount'], 2); + $orderPayType = trim((string)$order['pay_type']); + $orderTxId = trim((string)$order['transaction_id']); + + if (empty($effectiveRows)) { + if ($orderPayStatus === 1 && $orderPayAmount > 0) { + $isNonWxChannel = ($orderPayType === 'mock' || $orderPayType === 'monthly' + || strpos($orderTxId, 'MOCK') === 0 || strpos($orderTxId, 'MONTHLY') === 0); + if (!$isNonWxChannel) { + $errMsg = '订单状态为已支付(支付渠道:' . ($orderPayType ?: '未记录') . ',实付 ¥' . $orderPayAmount . '),但在支付流水表 fa_pay_order 中未查询到有效的微信支付交易单,为严防未向微信退款导致资金未原路退回,系统已拦截本次自动退款,请管理员先核查微信商户平台交易记录'; + self::addOrderLog($orderId, (int)$order['status'], '【拦截假退款】' . $errMsg, 'system', 0); + Log::error("[PaymentService::refundOrder] 缺少微信支付流水拦截退款 order_id={$orderId}, pay_type={$orderPayType}"); + throw new \RuntimeException($errMsg); + } + } + return 0.00; + } + + $totalRefunded = 0.00; + foreach ($effectiveRows as $po) { + if ($po['status'] === 'refunded') { + $totalRefunded = round($totalRefunded + (float)$po['refund_amount'], 2); + continue; + } + if ($po['status'] === 'refunding') { + $synced = self::syncRefundPayOrder((int)$po['id']); + if ($synced['status'] === 'refund_failed') { + throw new \RuntimeException('历史退款单(' . $synced['refund_no'] . ')微信侧退款异常:' . $synced['fail_reason']); + } + $totalRefunded = round($totalRefunded + (float)$synced['amount'], 2); + continue; + } + // paid 或 refund_failed 均发起/重试原路退款 + $after = self::refundSinglePayOrder((int)$po['id'], $reason); + $totalRefunded = round($totalRefunded + (float)($after['refund_amount'] ?: $po['amount']), 2); + } + + return $totalRefunded; + } + + /** + * 对单笔 fa_pay_order 执行微信 V3 原路退款(事务外远程调用 + 状态/失败原因持久落库) + * + * @return array 更新后的 pay_order 记录 + */ + public static function refundSinglePayOrder($payOrderId, $reason = '订单取消原路退款') + { + $po = Db::name('pay_order')->where('id', (int)$payOrderId)->find(); + if (!$po) { + throw new \InvalidArgumentException('支付交易单不存在'); + } + if ($po['status'] === 'refunded') { + return $po; + } + if ($po['status'] === 'refunding') { + return self::syncRefundPayOrder((int)$po['id']); + } + if (!in_array($po['status'], ['paid', 'refund_failed'], true)) { + throw new \InvalidArgumentException('当前支付单状态(' . self::payOrderStatusLabel($po['status']) . ')不允许发起退款'); + } + + $amount = round((float)$po['amount'], 2); + if ($amount <= 0) { + throw new \InvalidArgumentException('退款金额必须大于 0'); + } + + $wxSvc = new WxPayV3Service(); + $refundNo = trim((string)$po['refund_no']); + $now = time(); + + // 若上一笔单据微信侧已返回明确终态异常/关闭(abnormal/closed),或此前退款失败,先核实微信侧旧单状态再决定复用或换号重退 + if ($refundNo !== '' && in_array($po['refund_status'], ['abnormal', 'closed'], true)) { + $suffixPos = strrpos($refundNo, 'R'); + $nextVer = 1; + $baseNo = $refundNo; + if ($suffixPos !== false && $suffixPos > 14) { + $tail = substr($refundNo, $suffixPos + 1); + if (ctype_digit($tail)) { + $nextVer = (int)$tail + 1; + $baseNo = substr($refundNo, 0, $suffixPos); + } + } + $refundNo = substr($baseNo, 0, 34) . 'R' . $nextVer; + Db::name('pay_order')->where('id', (int)$po['id'])->update([ + 'refund_no' => $refundNo, + 'updatetime' => $now, + ]); + $po['refund_no'] = $refundNo; + } elseif ($refundNo === '') { + $refundNo = 'RF' . date('YmdHis', $now) . str_pad((string)$po['id'], 8, '0', STR_PAD_LEFT); + Db::name('pay_order')->where('id', (int)$po['id'])->where('refund_no', '')->update([ + 'refund_no' => $refundNo, + 'updatetime' => $now, + ]); + $po = Db::name('pay_order')->where('id', (int)$po['id'])->find(); + $refundNo = (string)$po['refund_no']; + } + + try { + $resp = $wxSvc->refund([ + 'out_trade_no' => (string)$po['out_trade_no'], + 'out_refund_no' => $refundNo, + 'refund_amount' => $amount, + 'total_amount' => $amount, + 'reason' => $reason, + ]); + } catch (\Throwable $e) { + $fail = WxPayV3Service::extractFailInfo($e); + $errNow = time(); + Db::name('pay_order')->where('id', (int)$po['id'])->update([ + 'status' => 'refund_failed', + 'fail_code' => $fail['code'], + 'fail_reason' => '向微信申请原路退款失败:' . $fail['reason'], + 'last_sync_time' => $errNow, + 'updatetime' => $errNow, + ]); + self::addOrderLog((int)$po['order_id'], 0, '申请微信原路退款失败(支付单 ' . $po['out_trade_no'] . '):' . $fail['reason'], 'system', (int)$po['user_id']); + Log::error("[PaymentService::refundSinglePayOrder] 退款调用失败 out_trade_no={$po['out_trade_no']}, refund_no={$refundNo}, code={$fail['code']}, reason={$fail['reason']}"); + throw $e; + } + + return self::applyWxRefundResult($po, $resp, $amount); + } + + /** + * 主动查询并同步单笔支付单的微信退款状态(后台点击同步或退款处理中复核) + */ + public static function syncRefundPayOrder($payOrderId) + { + $po = Db::name('pay_order')->where('id', (int)$payOrderId)->find(); + if (!$po) { + throw new \InvalidArgumentException('支付交易单不存在'); + } + $refundNo = trim((string)$po['refund_no']); + if ($refundNo === '') { + throw new \InvalidArgumentException('该支付单尚未生成退款单号,无需同步退款状态'); + } + + $wxSvc = new WxPayV3Service(); + try { + $resp = $wxSvc->queryRefundByOutRefundNo($refundNo); + } catch (\Throwable $e) { + $fail = WxPayV3Service::extractFailInfo($e); + Db::name('pay_order')->where('id', (int)$po['id'])->update([ + 'fail_code' => $fail['code'], + 'fail_reason' => '同步微信退款状态失败:' . $fail['reason'], + 'last_sync_time' => time(), + 'updatetime' => time(), + ]); + throw $e; + } + + return self::applyWxRefundResult($po, $resp, round((float)$po['amount'], 2)); + } + + /** + * 统一应用微信退款查单 / 申请 / 异步回调结果(准确区分 SUCCESS / PROCESSING / ABNORMAL / CLOSED) + */ + public static function applyWxRefundResult(array $po, array $wxRefundData, $fallbackAmount = 0.00) + { + $wxStatus = strtoupper(trim((string)($wxRefundData['status'] ?? $wxRefundData['refund_status'] ?? ''))); + if ($wxStatus === '') { + throw new \RuntimeException('微信退款报文未返回有效的退款状态字段'); + } + + $refundId = trim((string)($wxRefundData['refund_id'] ?? $po['refund_id'])); + $now = time(); + $amount = round((float)$po['amount'], 2) ?: round((float)$fallbackAmount, 2); + if (isset($wxRefundData['amount']['refund']) && (int)$wxRefundData['amount']['refund'] > 0) { + $amount = round(((int)$wxRefundData['amount']['refund']) / 100, 2); + } + + if ($wxStatus === 'SUCCESS') { + $succTime = !empty($wxRefundData['success_time']) + ? (strtotime((string)$wxRefundData['success_time']) ?: $now) + : $now; + Db::name('pay_order')->where('id', (int)$po['id'])->update([ + 'status' => 'refunded', + 'refund_status' => 'success', + 'refund_id' => $refundId, + 'refund_amount' => $amount, + 'refund_time' => $succTime, + 'fail_code' => '', + 'fail_reason' => '', + 'last_sync_time' => $now, + 'updatetime' => $now, + ]); + self::addOrderLog((int)$po['order_id'], 0, '微信原路退款已到账(退款单 ' . $po['refund_no'] . ',金额 ¥' . $amount . ')', 'system', 0); + return Db::name('pay_order')->where('id', (int)$po['id'])->find(); + } + + if ($wxStatus === 'PROCESSING') { + Db::name('pay_order')->where('id', (int)$po['id'])->update([ + 'status' => 'refunding', + 'refund_status' => 'processing', + 'refund_id' => $refundId, + 'refund_amount' => $amount, + 'fail_code' => '', + 'fail_reason' => '', + 'last_sync_time' => $now, + 'updatetime' => $now, + ]); + self::addOrderLog((int)$po['order_id'], 0, '微信已受理原路退款申请(处理中,退款单 ' . $po['refund_no'] . ',微信单号 ' . ($refundId ?: '-') . '),等待到账通知', 'system', 0); + return Db::name('pay_order')->where('id', (int)$po['id'])->find(); + } + + // ABNORMAL 或 CLOSED:绝对不可标为已退款!必须写入中文失败原因并告警 + $reasonZh = $wxStatus === 'ABNORMAL' + ? '微信原路退款异常(ABNORMAL):款项未能退到用户银行卡或微信零钱,常见原因为用户原银行卡已注销/受限或商户待结算资金不足,请登录微信商户平台【交易中心-退款管理】核实处置' + : '微信原路退款已关闭(CLOSED):该笔退款已被关闭未到账,请在微信商户平台核实并重新发起退款'; + + Db::name('pay_order')->where('id', (int)$po['id'])->update([ + 'status' => 'refund_failed', + 'refund_status' => strtolower($wxStatus), + 'refund_id' => $refundId, + 'fail_code' => 'REFUND_' . $wxStatus, + 'fail_reason' => $reasonZh, + 'last_sync_time' => $now, + 'updatetime' => $now, + ]); + self::addOrderLog((int)$po['order_id'], 0, '【退款异常告警】' . $reasonZh . '(退款单号:' . $po['refund_no'] . ')', 'system', 0); + Log::error("[PaymentService::applyWxRefundResult] 退款失败 out_trade_no={$po['out_trade_no']}, refund_no={$po['refund_no']}, wxStatus={$wxStatus}"); + + throw new \RuntimeException($reasonZh); + } + + /** + * 后台审核通过师傅提现单时:向微信发起新版「商家转账(场景 1005 佣金报酬)」 + * + * @param int $withdrawId 提现单 ID + * @param string $auditMsg 财务审核备注 + * @return array 最新提现单信息 + */ + public static function initiateWithdrawTransfer($withdrawId, $auditMsg = '') + { + $withdrawId = (int)$withdrawId; + $auditMsg = trim((string)$auditMsg); + + $wd = Db::name('withdraw')->where('id', $withdrawId)->find(); + if (!$wd) { + throw new \InvalidArgumentException('提现申请单不存在'); + } + if ($wd['channel'] !== 'wechat') { + throw new \InvalidArgumentException('该提现单不是微信钱包渠道,无需调用微信商家转账'); + } + if (in_array($wd['status'], ['done', 'reject'], true)) { + throw new \InvalidArgumentException('该提现单已处于终态,不可重复发起转账'); + } + + // 若已有历史商户转账单号,严禁跳过核查直接重复发起: + // - 待用户确认/受理中/超时未知状态 -> 必须先实时同步微信状态; + // - 同步后若为已到账/处理中 -> 直接返回最新状态; + // - 确认为 FAIL 或 NOT_FOUND(从未受理) -> 保留历史失败痕迹并生成递增重试后缀单号重新发起。 + $retryCount = (int)($wd['retry_count'] ?? 0); + $existOutNo = trim((string)($wd['out_bill_no'] ?? '')); + $existState = strtoupper(trim((string)($wd['transfer_state'] ?? ''))); + if ($existOutNo !== '') { + if (in_array($existState, ['ACCEPTED', 'PROCESSING', 'WAIT_USER_CONFIRM', 'TRANSFERING', 'CANCELING', 'UNKNOWN_TIMEOUT', 'SUCCESS'], true) + || !in_array($existState, ['FAIL', 'NOT_FOUND'], true) + ) { + $wd = self::syncWithdrawTransfer($withdrawId); + $existState = strtoupper(trim((string)($wd['transfer_state'] ?? ''))); + if (!in_array($existState, ['FAIL', 'NOT_FOUND'], true)) { + return $wd; + } + if (in_array($wd['status'], ['done', 'reject'], true)) { + return $wd; + } + } + } + + $master = Db::name('master')->where('id', (int)$wd['master_id'])->find(); + if (!$master) { + self::recordWithdrawFailure($withdrawId, 'MASTER_NOT_FOUND', '提现师傅档案不存在,无法发起微信商家转账'); + throw new \InvalidArgumentException('提现师傅档案不存在'); + } + + // 解析收款师傅微信 OpenID(优先读提现单快照,其次查微信绑定表) + $openid = trim((string)($wd['openid'] ?? '')); + if ($openid === '' && (int)$master['user_id'] > 0) { + $bind = Db::name('user_wechat') + ->where('user_id', (int)$master['user_id']) + ->where('openid', '<>', '') + ->order('updatetime desc, id desc') + ->find(); + if ($bind && !empty($bind['openid'])) { + $openid = trim((string)$bind['openid']); + } + } + if ($openid === '') { + $msg = '该师傅尚未绑定微信小程序授权(缺少收款 OpenID),请提醒师傅在微信小程序内登录打开「我的钱包」后再试'; + self::recordWithdrawFailure($withdrawId, 'OPENID_MISSING', $msg); + throw new \InvalidArgumentException($msg); + } + + $arriveAmount = round((float)$wd['arrive_amount'], 2); + if ($arriveAmount < 0.10) { + $msg = '微信商家转账实际到账金额不能低于 0.10 元(当前:¥' . number_format($arriveAmount, 2, '.', '') . ')'; + self::recordWithdrawFailure($withdrawId, 'AMOUNT_TOO_LOW', $msg); + throw new \InvalidArgumentException($msg); + } + + // 生成唯一且不超过 32 位字母数字的商户转账单号(首次 WD+日期+ID;历史明确 FAIL/NOT_FOUND 后重试递增 R{n}) + $createdTs = !empty($wd['createtime']) ? (int)$wd['createtime'] : time(); + $baseNo = 'WD' . date('Ymd', $createdTs) . str_pad((string)$withdrawId, 8, '0', STR_PAD_LEFT); + if ($existOutNo === '') { + $outBillNo = $baseNo; + Db::name('withdraw')->where('id', $withdrawId)->where('out_bill_no', '')->update([ + 'openid' => $openid, + 'out_bill_no' => $outBillNo, + 'updatetime' => time(), + ]); + $wd = Db::name('withdraw')->where('id', $withdrawId)->find(); + $outBillNo = (string)$wd['out_bill_no']; + } else { + // 旧单已核实确属 FAIL/NOT_FOUND,生成递增版本号单号,严禁清空抹除旧单追踪 + $retryCount++; + $outBillNo = substr($baseNo, 0, 26) . 'R' . $retryCount; + Db::name('withdraw')->where('id', $withdrawId)->update([ + 'openid' => $openid, + 'out_bill_no' => $outBillNo, + 'retry_count' => $retryCount, + 'transfer_state' => '', + 'updatetime' => time(), + ]); + } + + // 事务外调用微信支付 APIv3 发起商家转账(动态读取独立配置中心报备参数) + $wdCfg = FinanceConfigService::getWithdrawConfig(); + $wxSvc = new WxPayV3Service(); + try { + $resp = $wxSvc->createTransferBill([ + 'out_bill_no' => $outBillNo, + 'openid' => $openid, + 'amount' => $arriveAmount, + 'transfer_scene_id' => $wdCfg['wx_scene_id'], + 'remark' => $wdCfg['wx_remark'], + 'user_name' => (string)($master['realname'] ?? ''), + 'job_type' => $wdCfg['wx_job_type'], + 'reward_desc' => $wdCfg['wx_reward_desc'], + 'recv_perception' => $wdCfg['wx_perception'], + ]); + } catch (\Throwable $e) { + $fail = WxPayV3Service::extractFailInfo($e); + $errNow = time(); + $nextState = $fail['uncertain'] ? 'UNKNOWN_TIMEOUT' : 'FAIL'; + $upd = [ + 'transfer_state' => $nextState, + 'fail_code' => $fail['code'], + 'fail_reason' => $fail['reason'], + 'last_sync_time' => $errNow, + 'updatetime' => $errNow, + ]; + if ($auditMsg !== '') { + $upd['audit_msg'] = mb_substr($auditMsg . '|发起失败:' . $fail['reason'], 0, 480); + } + Db::name('withdraw')->where('id', $withdrawId)->update($upd); + Log::error("[PaymentService::initiateWithdrawTransfer] 发起商家转账失败 withdraw_id={$withdrawId}, out_bill_no={$outBillNo}, code={$fail['code']}, uncertain=" . ($fail['uncertain'] ? '1' : '0') . ", reason={$fail['reason']}"); + throw $e; + } + + $state = strtoupper((string)($resp['state'] ?? 'ACCEPTED')); + $transferBillNo = trim((string)($resp['transfer_bill_no'] ?? '')); + $packageInfo = trim((string)($resp['package_info'] ?? '')); + $failReason = trim((string)($resp['fail_reason'] ?? '')); + + // 若刚受理瞬间处于 ACCEPTED/PROCESSING,短暂等待 0.6s 查询最新状态(严禁掩盖证书/签名等致命配置故障) + if (in_array($state, ['ACCEPTED', 'PROCESSING'], true)) { + usleep(600000); + try { + $qResp = $wxSvc->queryTransferBill($outBillNo); + if (!empty($qResp['state'])) { + $state = strtoupper((string)$qResp['state']); + } + if (!empty($qResp['transfer_bill_no'])) { + $transferBillNo = (string)$qResp['transfer_bill_no']; + } + if (!empty($qResp['package_info'])) { + $packageInfo = (string)$qResp['package_info']; + } + if (!empty($qResp['fail_reason'])) { + $failReason = (string)$qResp['fail_reason']; + } + } catch (\Throwable $qe) { + $qFail = WxPayV3Service::extractFailInfo($qe); + if (strpos($qFail['code'], 'VERIFY_SIGN') !== false || strpos($qFail['code'], 'CONFIG_') !== false) { + self::recordWithdrawFailure($withdrawId, $qFail['code'], '转账已受理但紧邻查单校验异常:' . $qFail['reason'], $state); + throw $qe; + } + Log::error("[PaymentService::initiateWithdrawTransfer] 受理后跟进查单未果 withdraw_id={$withdrawId}, out_bill_no={$outBillNo}: {$qFail['reason']}"); + } + } + + if ($state === 'FAIL') { + $reasonText = $failReason !== '' + ? self::translateTransferFailReason($failReason) + : '微信商家转账受理后处理失败(未返回具体原因码,请登录微信商户平台按单号核查)'; + // 保留失败单号、写入中文原因与错误码,严禁清空抹除单号线索 + Db::name('withdraw')->where('id', $withdrawId)->update([ + 'out_bill_no' => $outBillNo, + 'transfer_bill_no' => $transferBillNo, + 'transfer_state' => 'FAIL', + 'fail_code' => $failReason ?: 'WX_FAIL', + 'fail_reason' => $reasonText, + 'last_sync_time' => time(), + 'updatetime' => time(), + ]); + throw new \RuntimeException('微信商家转账发起失败:' . $reasonText); + } + + $now = time(); + Db::startTrans(); + try { + $lockedWd = Db::name('withdraw')->where('id', $withdrawId)->lock(true)->find(); + if (!$lockedWd || in_array($lockedWd['status'], ['done', 'reject'], true)) { + throw new \RuntimeException('提现单状态已变更,请刷新重试'); + } + + $update = [ + 'status' => 'pass', + 'openid' => $openid, + 'out_bill_no' => $outBillNo, + 'transfer_bill_no' => $transferBillNo, + 'transfer_state' => $state, + 'fail_code' => '', + 'fail_reason' => '', + 'transfer_time' => $now, + 'last_sync_time' => $now, + 'updatetime' => $now, + ]; + if ($auditMsg !== '') { + $update['audit_msg'] = mb_substr($auditMsg, 0, 480); + } + if ($packageInfo !== '') { + $update['package_info'] = $packageInfo; + } + + Db::name('withdraw')->where('id', $withdrawId)->update($update); + + $amtStr = number_format((float)$lockedWd['amount'], 2, '.', ''); + $arrStr = number_format((float)$lockedWd['arrive_amount'], 2, '.', ''); + self::sendMasterAndUserMessage( + (int)$master['id'], + (int)$master['user_id'], + '提现已汇出,请确认收款', + "您的提现申请 ¥{$amtStr}(实际到账 ¥{$arrStr})已通过审核并发起微信转账,请在 24 小时内打开小程序「我的钱包」点击「确认收款」入账微信零钱。" + ); + + Db::commit(); + } catch (\Throwable $e) { + Db::rollback(); + throw $e; + } + + // 若极少数免确认场景微信直接返回 SUCCESS,顺手执行一次入账完结 + if ($state === 'SUCCESS') { + return self::syncWithdrawTransfer($withdrawId, $resp); + } + + return Db::name('withdraw')->where('id', $withdrawId)->find(); + } + + /** + * 同步微信商家转账单状态并联动结算/退回冻结资金(异步回调、师傅确认收款后、后台手动同步共用,严格幂等) + * + * @param int $withdrawId 提现单 ID + * @param array|null $wxBillData 微信回调已解密报文;传 null 则主动调用微信查单接口 + * @return array 最新提现单行记录 + */ + public static function syncWithdrawTransfer($withdrawId, array $wxBillData = null) + { + $withdrawId = (int)$withdrawId; + $wd = Db::name('withdraw')->where('id', $withdrawId)->find(); + if (!$wd) { + throw new \InvalidArgumentException('提现记录不存在'); + } + $outBillNo = trim((string)($wd['out_bill_no'] ?? '')); + if ($outBillNo === '') { + throw new \InvalidArgumentException('该提现单尚未生成微信商家转账单号,无法同步微信状态'); + } + + if ($wxBillData === null) { + $wxSvc = new WxPayV3Service(); + try { + $wxBillData = $wxSvc->queryTransferBill($outBillNo); + } catch (\Throwable $qe) { + $fail = WxPayV3Service::extractFailInfo($qe); + $errNow = time(); + if (in_array($fail['code'], ['NOT_FOUND', 'RESOURCE_NOT_EXISTS'], true)) { + $notFoundMsg = '经向微信查单核实,商户转账单号(' . $outBillNo . ')在微信侧不存在,证明上次发起未能立单出款(未产生资损),您可在核查原因后重新发起或驳回退回余额'; + if (!in_array($wd['status'], ['done', 'reject'], true)) { + Db::name('withdraw')->where('id', $withdrawId)->update([ + 'transfer_state' => 'NOT_FOUND', + 'fail_code' => $fail['code'], + 'fail_reason' => $notFoundMsg, + 'last_sync_time' => $errNow, + 'updatetime' => $errNow, + ]); + } + throw new \RuntimeException($notFoundMsg); + } + if (!in_array($wd['status'], ['done', 'reject'], true)) { + Db::name('withdraw')->where('id', $withdrawId)->update([ + 'fail_code' => $fail['code'], + 'fail_reason' => '同步微信转账状态失败:' . $fail['reason'], + 'last_sync_time' => $errNow, + 'updatetime' => $errNow, + ]); + } + Log::error("[PaymentService::syncWithdrawTransfer] 查询转账单失败 withdraw_id={$withdrawId}, out_bill_no={$outBillNo}, code={$fail['code']}, reason={$fail['reason']}"); + throw $qe; + } + } + + $wxState = strtoupper(trim((string)($wxBillData['state'] ?? ''))); + $transferBillNo = trim((string)($wxBillData['transfer_bill_no'] ?? $wd['transfer_bill_no'])); + $newPackageInfo = trim((string)($wxBillData['package_info'] ?? '')); + $rawFailReason = trim((string)($wxBillData['fail_reason'] ?? '')); + $now = time(); + + if ($wxState === '') { + $msg = '微信查询转账单接口未返回有效的状态字段(state 为空),请稍后重试或登录微信商户平台核查'; + self::recordWithdrawFailure($withdrawId, 'EMPTY_TRANSFER_STATE', $msg); + throw new \RuntimeException($msg); + } + + Db::startTrans(); + try { + $lockedWd = Db::name('withdraw')->where('id', $withdrawId)->lock(true)->find(); + if (!$lockedWd) { + Db::commit(); + return $wd; + } + + $masterId = (int)$lockedWd['master_id']; + $master = Db::name('master')->where('id', $masterId)->lock(true)->find(); + $amount = round((float)$lockedWd['amount'], 2); + + if ($wxState === 'SUCCESS') { + if ($lockedWd['status'] !== 'done') { + if ($master) { + $newFrozen = max(0, round((float)$master['frozen_money'] - $amount, 2)); + Db::name('master')->where('id', $masterId)->update([ + 'frozen_money' => $newFrozen, + 'updatetime' => $now, + ]); + Db::name('wallet_log')->insert([ + 'user_type' => 'master', + 'user_id' => $masterId, + 'type' => 'withdraw', + 'amount' => 0, + 'balance' => (float)$master['money'], + 'order_id' => 0, + 'remark' => '微信零钱确认收款完成,冻结资金 ¥' . $amount . ' 已划出(单号 ' . ($transferBillNo ?: $lockedWd['out_bill_no']) . ')', + 'createtime' => $now, + ]); + self::sendMasterAndUserMessage( + $masterId, + (int)$master['user_id'], + '提现已到账微信零钱', + '您的提现 ¥' . $amount . '(实到 ¥' . $lockedWd['arrive_amount'] . ')已成功确认收款并入账微信零钱。' + ); + } + Db::name('withdraw')->where('id', $withdrawId)->update([ + 'status' => 'done', + 'transfer_bill_no' => $transferBillNo, + 'transfer_state' => 'SUCCESS', + 'fail_code' => '', + 'fail_reason' => '', + 'confirm_time' => $now, + 'last_sync_time' => $now, + 'updatetime' => $now, + ]); + } else { + Db::name('withdraw')->where('id', $withdrawId)->update([ + 'transfer_state' => 'SUCCESS', + 'fail_code' => '', + 'fail_reason' => '', + 'last_sync_time' => $now, + 'updatetime' => $now, + ]); + } + } elseif (in_array($wxState, ['FAIL', 'CANCELLED'], true)) { + $reasonCode = $rawFailReason !== '' ? $rawFailReason : ($wxState === 'CANCELLED' ? 'MERCHANT_REVOCATION' : 'WX_FAIL'); + $reasonText = $rawFailReason !== '' + ? self::translateTransferFailReason($rawFailReason) + : ($wxState === 'CANCELLED' + ? '平台财务已撤销本次转账或收款超时关闭' + : '微信转账失败(微信未返回详细原因码,请核对微信商户平台转账单 ' . $outBillNo . ')'); + + if (!in_array($lockedWd['status'], ['reject', 'done'], true)) { + if ($master) { + $newMoney = round((float)$master['money'] + $amount, 2); + $newFrozen = max(0, round((float)$master['frozen_money'] - $amount, 2)); + Db::name('master')->where('id', $masterId)->update([ + 'money' => $newMoney, + 'frozen_money' => $newFrozen, + 'updatetime' => $now, + ]); + Db::name('wallet_log')->insert([ + 'user_type' => 'master', + 'user_id' => $masterId, + 'type' => 'refund', + 'amount' => $amount, + 'balance' => $newMoney, + 'order_id' => 0, + 'remark' => '提现未完成(' . $reasonText . '),申请金额 ¥' . $amount . ' 已退回钱包可用余额', + 'createtime' => $now, + ]); + self::sendMasterAndUserMessage( + $masterId, + (int)$master['user_id'], + '提现未完成退回余额通知', + '您的提现申请 ¥' . $amount . ' 未完成入账(原因:' . $reasonText . '),资金已自动解冻退回您的钱包可用余额,可随时重新发起提现。' + ); + } + $mergedAudit = trim((string)$lockedWd['audit_msg']); + if ($mergedAudit === '') { + $mergedAudit = $reasonText; + } elseif (mb_strpos($mergedAudit, $reasonText) === false) { + $mergedAudit = mb_substr($mergedAudit . ';' . $reasonText, 0, 480); + } + Db::name('withdraw')->where('id', $withdrawId)->update([ + 'status' => 'reject', + 'transfer_bill_no' => $transferBillNo, + 'transfer_state' => $wxState, + 'fail_code' => $reasonCode, + 'fail_reason' => $reasonText, + 'audit_msg' => $mergedAudit, + 'last_sync_time' => $now, + 'updatetime' => $now, + ]); + } else { + Db::name('withdraw')->where('id', $withdrawId)->update([ + 'transfer_state' => $wxState, + 'fail_code' => $reasonCode, + 'fail_reason' => $reasonText, + 'last_sync_time' => $now, + 'updatetime' => $now, + ]); + } + } else { + // 中间态(ACCEPTED / PROCESSING / WAIT_USER_CONFIRM / TRANSFERING / CANCELING):切勿清空已有 package_info + $upd = [ + 'status' => 'pass', + 'transfer_bill_no' => $transferBillNo, + 'transfer_state' => $wxState, + 'fail_code' => '', + 'fail_reason' => '', + 'last_sync_time' => $now, + 'updatetime' => $now, + ]; + if ($newPackageInfo !== '') { + $upd['package_info'] = $newPackageInfo; + } + Db::name('withdraw')->where('id', $withdrawId)->update($upd); + } + + Db::commit(); + } catch (\Throwable $e) { + Db::rollback(); + throw $e; + } + + return Db::name('withdraw')->where('id', $withdrawId)->find(); + } + + /** + * 后台财务安全驳回提现单(防双重资损保护:若已发起微信商家转账,必须先向微信查单并撤销确认后才解冻退款) + * + * @param int $withdrawId + * @param string $auditMsg + * @return array ['status' => string, 'message' => string] + */ + public static function cancelOrRejectWithdraw($withdrawId, $auditMsg) + { + $withdrawId = (int)$withdrawId; + $auditMsg = trim((string)$auditMsg); + if ($auditMsg === '') { + throw new \InvalidArgumentException('驳回时必须填写驳回原因'); + } + + $wd = Db::name('withdraw')->where('id', $withdrawId)->find(); + if (!$wd) { + throw new \InvalidArgumentException('提现申请单不存在'); + } + if (in_array($wd['status'], ['done', 'reject'], true)) { + throw new \InvalidArgumentException('该提现单已处于终态,不可再次操作'); + } + + // 1. 尚未调用过微信转账接口(如刚提交处于 wait,或线下银行卡渠道):直接安全解冻退回余额 + if (empty($wd['out_bill_no'])) { + $now = time(); + Db::startTrans(); + try { + $lockedWd = Db::name('withdraw')->where('id', $withdrawId)->lock(true)->find(); + if (!$lockedWd || in_array($lockedWd['status'], ['done', 'reject'], true) || !empty($lockedWd['out_bill_no'])) { + throw new \RuntimeException('提现单状态已变更,请刷新重试'); + } + $masterId = (int)$lockedWd['master_id']; + $amount = round((float)$lockedWd['amount'], 2); + $master = Db::name('master')->where('id', $masterId)->lock(true)->find(); + if (!$master) { + throw new \RuntimeException('提现师傅不存在'); + } + + $newMoney = round((float)$master['money'] + $amount, 2); + $newFrozen = max(0, round((float)$master['frozen_money'] - $amount, 2)); + + Db::name('master')->where('id', $masterId)->update([ + 'money' => $newMoney, + 'frozen_money' => $newFrozen, + 'updatetime' => $now, + ]); + Db::name('withdraw')->where('id', $withdrawId)->update([ + 'status' => 'reject', + 'audit_msg' => $auditMsg, + 'updatetime' => $now, + ]); + Db::name('wallet_log')->insert([ + 'user_type' => 'master', + 'user_id' => $masterId, + 'type' => 'refund', + 'amount' => $amount, + 'balance' => $newMoney, + 'order_id' => 0, + 'remark' => '提现驳回,申请金额退回钱包余额(原因:' . $auditMsg . ')', + 'createtime' => $now, + ]); + self::sendMasterAndUserMessage( + $masterId, + (int)$master['user_id'], + '提现申请已驳回', + '您的提现申请 ¥' . $amount . ' 未通过财务审核:' . $auditMsg . ',申请金额已退回钱包余额。' + ); + Db::commit(); + } catch (\Throwable $e) { + Db::rollback(); + throw $e; + } + return ['status' => 'reject', 'message' => '已驳回提现并解冻退回师傅余额']; + } + + // 2. 已存在商户转账单号时:先实时向微信查单,确认为已失败/已撤销/微信侧不存在后,方可解冻退款(严防双重资损) + $outBillNo = (string)$wd['out_bill_no']; + $wxSvc = new WxPayV3Service(); + try { + $wxBill = $wxSvc->queryTransferBill($outBillNo); + } catch (\Throwable $qe) { + $fail = WxPayV3Service::extractFailInfo($qe); + // 仅当微信明确返回 NOT_FOUND / RESOURCE_NOT_EXISTS(说明微信从未创建/受理该单)才安全解冻退回 + if (in_array($fail['code'], ['NOT_FOUND', 'RESOURCE_NOT_EXISTS'], true)) { + self::forceRejectUnacceptedWithdraw( + $withdrawId, + $auditMsg, + $fail['code'], + '微信查单确认单号 ' . $outBillNo . ' 未在微信立单(' . $fail['reason'] . '),财务驳回退回' + ); + return ['status' => 'reject', 'message' => '已向微信查单确认未出款,资金已安全驳回并退回师傅可用余额']; + } + self::recordWithdrawFailure($withdrawId, $fail['code'], '驳回前向微信核验转账状态失败(严禁盲目解冻):' . $fail['reason']); + throw $qe; + } + + $wxState = strtoupper((string)($wxBill['state'] ?? '')); + + if ($wxState === 'SUCCESS') { + self::syncWithdrawTransfer($withdrawId, $wxBill); + throw new \RuntimeException('师傅已完成微信确认收款,资金已真实入账零钱,无法驳回!系统已自动将该单同步为【已成功打款】'); + } + + if (in_array($wxState, ['FAIL', 'CANCELLED'], true)) { + Db::name('withdraw')->where('id', $withdrawId)->update(['audit_msg' => mb_substr($auditMsg, 0, 480)]); + self::syncWithdrawTransfer($withdrawId, $wxBill); + return ['status' => 'reject', 'message' => '微信转账单已处于未完成/撤销状态,系统已自动退回师傅钱包余额']; + } + + if ($wxState === 'WAIT_USER_CONFIRM') { + try { + $cancelResp = $wxSvc->cancelTransferBill($outBillNo); + } catch (\Throwable $ce) { + $cFail = WxPayV3Service::extractFailInfo($ce); + self::recordWithdrawFailure($withdrawId, $cFail['code'], '撤销微信商家转账单失败:' . $cFail['reason']); + throw $ce; + } + + $cancelState = strtoupper((string)($cancelResp['state'] ?? 'CANCELING')); + Db::name('withdraw')->where('id', $withdrawId)->update([ + 'audit_msg' => mb_substr($auditMsg, 0, 480), + 'transfer_state' => $cancelState, + 'last_sync_time' => time(), + 'updatetime' => time(), + ]); + + if ($cancelState !== 'CANCELLED') { + usleep(800000); + try { + $wxBillAfter = $wxSvc->queryTransferBill($outBillNo); + $afterState = strtoupper((string)($wxBillAfter['state'] ?? $cancelState)); + if (in_array($afterState, ['CANCELLED', 'FAIL'], true)) { + self::syncWithdrawTransfer($withdrawId, $wxBillAfter); + return ['status' => 'reject', 'message' => '已成功撤销微信商家转账并解冻退回师傅余额']; + } + } catch (\Throwable $ae) { + $aFail = WxPayV3Service::extractFailInfo($ae); + self::recordWithdrawFailure($withdrawId, $aFail['code'], '提交撤销后复查状态失败:' . $aFail['reason'], 'CANCELING'); + } + return [ + 'status' => 'pass', + 'message' => '已向微信提交撤销申请(状态:撤销处理中),待微信完成资金退回后点击【同步微信状态】即可自动解冻退回师傅余额', + ]; + } + + self::syncWithdrawTransfer($withdrawId, $cancelResp); + return ['status' => 'reject', 'message' => '已成功撤销微信商家转账并解冻退回师傅余额']; + } + + if ($wxState === 'CANCELING') { + return [ + 'status' => 'pass', + 'message' => '该微信转账单正在撤销处理中,请稍后点击【同步微信状态】完成解冻退款', + ]; + } + + $denyMsg = '当前微信转账单处于【' . self::transferStateLabel($wxState) . '】状态,暂不支持直接撤销,请稍后同步状态再试'; + self::recordWithdrawFailure($withdrawId, 'REJECT_DENY_' . $wxState, $denyMsg, $wxState); + throw new \RuntimeException($denyMsg); + } + + /** + * 当微信查单明确证实未受理出款(NOT_FOUND)时安全驳回并解冻余额 + */ + protected static function forceRejectUnacceptedWithdraw($withdrawId, $auditMsg, $failCode, $reasonZh) + { + $now = time(); + Db::startTrans(); + try { + $lockedWd = Db::name('withdraw')->where('id', $withdrawId)->lock(true)->find(); + if (!$lockedWd || in_array($lockedWd['status'], ['done', 'reject'], true)) { + throw new \RuntimeException('提现单状态已变更,请刷新重试'); + } + $masterId = (int)$lockedWd['master_id']; + $amount = round((float)$lockedWd['amount'], 2); + $master = Db::name('master')->where('id', $masterId)->lock(true)->find(); + if ($master) { + $newMoney = round((float)$master['money'] + $amount, 2); + $newFrozen = max(0, round((float)$master['frozen_money'] - $amount, 2)); + Db::name('master')->where('id', $masterId)->update([ + 'money' => $newMoney, + 'frozen_money' => $newFrozen, + 'updatetime' => $now, + ]); + Db::name('wallet_log')->insert([ + 'user_type' => 'master', + 'user_id' => $masterId, + 'type' => 'refund', + 'amount' => $amount, + 'balance' => $newMoney, + 'order_id' => 0, + 'remark' => '提现驳回退回(' . $reasonZh . ')', + 'createtime' => $now, + ]); + self::sendMasterAndUserMessage( + $masterId, + (int)$master['user_id'], + '提现申请已退回余额', + '您的提现申请 ¥' . $amount . ' 已驳回退回(批注:' . $auditMsg . '),申请金额已全额退回钱包可用余额。' + ); + } + Db::name('withdraw')->where('id', $withdrawId)->update([ + 'status' => 'reject', + 'transfer_state' => 'NOT_FOUND', + 'fail_code' => $failCode, + 'fail_reason' => mb_substr($reasonZh, 0, 480), + 'audit_msg' => mb_substr($auditMsg, 0, 480), + 'last_sync_time' => $now, + 'updatetime' => $now, + ]); + Db::commit(); + } catch (\Throwable $e) { + Db::rollback(); + throw $e; + } + } + + /** + * 统一向提现单持久化记录中文失败原因与错误码(不改变未决资金锁定) + */ + public static function recordWithdrawFailure($withdrawId, $failCode, $failReason, $transferState = null) + { + $now = time(); + $upd = [ + 'fail_code' => mb_substr((string)$failCode, 0, 60), + 'fail_reason' => mb_substr((string)$failReason, 0, 480), + 'last_sync_time' => $now, + 'updatetime' => $now, + ]; + if ($transferState !== null) { + $upd['transfer_state'] = (string)$transferState; + } + Db::name('withdraw')->where('id', (int)$withdrawId)->update($upd); + } + + /** + * 支付流水状态中文映射 + */ + public static function payOrderStatusLabel($status) + { + $map = [ + 'pending' => '待支付', + 'paid' => '已支付', + 'pay_failed' => '下单/支付失败', + 'refunding' => '原路退款中', + 'refunded' => '已原路退款', + 'refund_failed' => '退款异常', + 'closed' => '已关闭', + ]; + $k = strtolower(trim((string)$status)); + return $map[$k] ?? '未知状态'; + } + + /** + * 微信退款状态中文映射 + */ + public static function refundStatusLabel($status) + { + $map = [ + 'none' => '未退款', + 'processing' => '退款处理中', + 'success' => '退款已到账', + 'abnormal' => '退款异常(需商户平台核查)', + 'closed' => '退款已关闭', + ]; + $k = strtolower(trim((string)$status)); + return $map[$k] ?? '退款未知状态'; + } + + /** + * 微信交易状态中文映射 + */ + public static function tradeStateLabel($state, $rawDesc = '') + { + $map = [ + 'SUCCESS' => '支付成功', + 'REFUND' => '已转入退款', + 'NOTPAY' => '用户尚未完成支付', + 'CLOSED' => '交易单已关闭', + 'REVOKED' => '交易已撤销', + 'USERPAYING' => '用户正在输入支付密码中', + 'PAYERROR' => '微信扣款失败(余额不足、银行卡受限或风控拦截)', + ]; + $k = strtoupper(trim((string)$state)); + if (isset($map[$k])) { + return $map[$k]; + } + return $rawDesc !== '' ? ('交易状态异常(' . $k . ')') : '待确认交易状态'; + } + + /** + * 微信商家转账状态中文映射 + */ + public static function transferStateLabel($state) + { + $map = [ + '' => '待发起转账', + 'ACCEPTED' => '微信已受理', + 'PROCESSING' => '锁定资金处理中', + 'WAIT_USER_CONFIRM' => '待师傅确认收款', + 'TRANSFERING' => '微信转账入账中', + 'SUCCESS' => '已确认收款到账', + 'FAIL' => '转账失败', + 'CANCELING' => '商户撤销处理中', + 'CANCELLED' => '已撤销并退回', + 'UNKNOWN_TIMEOUT' => '请求超时(状态待查单核实)', + 'NOT_FOUND' => '微信查无此单(未受理出款)', + ]; + $key = strtoupper(trim((string)$state)); + return $map[$key] ?? ('未知状态(' . $key . ')'); + } + + /** + * 全量翻译微信商家转账 fail_reason 枚举码为管理员可读简体中文(杜绝透传英文枚举) + */ + public static function translateTransferFailReason($reasonCode) + { + $raw = trim((string)$reasonCode); + $code = strtoupper($raw); + if ($code === '') { + return '转账未完成或已关闭(未返回具体原因码)'; + } + $map = [ + 'OVERDUE_CLOSE' => '超过24小时未确认收款,微信系统自动关单退回', + 'TRANSFER_REMARK_SET_FAIL' => '转账备注内容不符合微信校验规范', + 'RECEIVE_ACCOUNT_NOT_CONFIGURE' => '微信商户后台未正确配置对应商家转账场景(请核对场景 ID 1005 报备)', + 'EXCEED_MAX_SUBSCRIBE_COUNT' => '超出收款用户单月消息/收款订阅次数限制', + 'MERCHANT_REVOCATION' => '平台财务主动发起撤销转账并退回', + 'USER_CANCEL' => '收款用户主动退回转账', + 'NOT_ENOUGH' => '微信商户运营账户可用余额不足,请充值后再试', + 'ACCOUNT_FROZEN' => '收款人微信支付账户已被冻结,无法收款', + 'REAL_NAME_CHECK_FAIL' => '收款师傅姓名与本人微信实名信息不一致,请核对实名认证信息', + 'NAME_NOT_CORRECT' => '大额转账收款人姓名与微信实名不匹配', + 'OPENID_INVALID' => '收款人微信 OpenID 无效或不属于当前小程序 AppID', + 'TRANSFER_QUOTA_EXCEED' => '超出微信商户后台设置的单笔或单日商家转账额度上限', + 'DAY_RECEIVED_QUOTA_EXCEED' => '超出收款人微信单日收款额度上限', + 'MONTH_RECEIVED_QUOTA_EXCEED' => '超出收款人微信单月收款额度上限', + 'DAY_RECEIVED_COUNT_EXCEED' => '超出收款人微信单日收款次数上限', + 'PRODUCT_AUTH_CHECK_FAIL' => '商户号商家转账产品权限校验未通过或场景未生效', + 'OVERDUE_CONFIRM' => '超过有效期未确认收款,微信系统自动关闭退回', + 'TRANSFER_RISK' => '微信支付安全风控系统拦截本次转账,请核实账户合规状态', + 'PAYEE_ACCOUNT_ABNORMAL' => '收款人微信账户状态异常或未绑定有效结算方式', + 'PAYER_ACCOUNT_ABNORMAL' => '商户运营付款账户状态异常,请登录微信商户平台排查站内信通知', + 'TRANSFER_SCENE_INVALID' => '商家转账场景 ID(1005)无效或报备字段内容不符合要求', + 'USER_ACCOUNT_INVALID' => '收款用户微信账号异常或已注销', + 'EXCEED_TRANSFER_AMOUNT_LIMIT' => '超出商户商家转账单笔最大额度限制(如平台单笔 200 元配置限制)', + 'EXCEED_SINGLE_DAY_TRANSFER_LIMIT' => '超出微信商户单日总转账额度上限', + 'FREQUENCY_LIMITED' => '向同一用户发起转账频率过快,触发微信限流', + 'SYSTEMERROR' => '微信商家转账网关内部处理异常退回', + 'WX_FAIL' => '微信商家转账受理后处理失败', + ]; + if (isset($map[$code])) { + return $map[$code]; + } + return '微信转账失败(微信返回原因码:' . $code . ',请登录微信商户平台商家转账明细查询具体说明)'; + } + + /** + * 0元订单(全额优惠券抵扣)免调微信直接入账 + */ + protected static function completeZeroOrderPay($orderId, $uid) + { + $now = time(); + $broadcastOrder = null; + $nextStatus = OrderStatus::PENDING_ACCEPT; + + Db::startTrans(); + try { + $lockedOrder = Db::name('order')->where('id', $orderId)->lock(true)->find(); + if (!$lockedOrder || (int)$lockedOrder['pay_status'] === 1 || (int)$lockedOrder['status'] !== OrderStatus::PENDING_PAY) { + throw new \InvalidArgumentException('订单状态已变更,请刷新重试'); + } + self::lockAndConsumeOrderCoupon($lockedOrder, $now); + + $nextStatus = $lockedOrder['price_type'] === 'quote' ? OrderStatus::QUOTING : OrderStatus::PENDING_ACCEPT; + $nextRemark = $nextStatus === OrderStatus::QUOTING + ? '优惠抵扣支付完成,订单已进入报价大厅' + : '优惠抵扣支付完成,订单已进入接单大厅'; + + Db::name('order')->where('id', $orderId)->update([ + 'status' => $nextStatus, + 'pay_status' => 1, + 'pay_type' => 'coupon', + 'pay_time' => $now, + 'transaction_id' => 'COUPON' . date('YmdHis', $now) . mt_rand(1000, 9999), + 'updatetime' => $now, + ]); + self::addOrderLog($orderId, $nextStatus, $nextRemark, 'user', $uid); + self::addOrderLog($orderId, $nextStatus, $nextStatus === OrderStatus::QUOTING ? '订单进入师傅报价大厅' : '订单进入师傅接单大厅', 'system', 0); + + $broadcastOrder = $lockedOrder; + Db::commit(); + } catch (\Throwable $e) { + Db::rollback(); + throw $e; + } + + if ($broadcastOrder && in_array($nextStatus, [OrderStatus::PENDING_ACCEPT, OrderStatus::QUOTING], true)) { + self::broadcastNewOrderToMasters($broadcastOrder); + } + + return [ + 'need_wxpay' => false, + 'paid' => true, + 'scene' => 'order', + 'order_id' => $orderId, + 'out_trade_no' => '', + 'amount' => 0.00, + 'pay_params' => null, + ]; + } + + /** + * 选定报价无需补差额(0元)直接锁定师傅 + */ + protected static function completeZeroQuoteChoose($orderId, $quoteId, $uid) + { + $now = time(); + Db::startTrans(); + try { + $lockedOrder = Db::name('order')->where('id', $orderId)->lock(true)->find(); + if (!$lockedOrder || (int)$lockedOrder['status'] !== OrderStatus::QUOTING || (int)$lockedOrder['master_id'] !== 0) { + throw new \InvalidArgumentException('该工单已选定师傅或状态已变更,请刷新重试'); + } + $quote = Db::name('order_quote')->where('id', $quoteId)->where('order_id', $orderId)->lock(true)->find(); + $master = $quote ? Db::name('master')->where('id', (int)$quote['master_id'])->find() : null; + if (!$quote || !$master || $quote['status'] !== 'wait') { + throw new \InvalidArgumentException('报价状态已变更,请刷新重试'); + } + + $quoteAmount = round((float)$quote['amount'], 2); + $payAmount = max(0, round($quoteAmount - (float)$lockedOrder['discount_amount'], 2)); + $snapRefreshed = CommissionService::refreshQuoteOrderSnapshotOnAmountConfirmed( + $payAmount > 0 ? $payAmount : $quoteAmount, + $lockedOrder['commission_snapshot'] ?? null, + $lockedOrder['commission_rate'] ?? null + ); + $remark = '客户已选择 ' . $master['realname'] . ' 的报价 ¥' . $quoteAmount . ',等待师傅联系'; + + $zeroQuoteUpdate = [ + 'status' => OrderStatus::ACCEPTED, + 'master_id' => (int)$quote['master_id'], + 'accept_time' => $now, + 'base_amount' => $quoteAmount, + 'floor_amount' => 0, + 'urgent_amount' => 0, + 'total_amount' => $quoteAmount, + 'pay_amount' => $payAmount, + 'commission_rate' => $snapRefreshed['commission_rate'], + 'pay_status' => 1, + 'updatetime' => $now, + ]; + if ($snapRefreshed['commission_snapshot'] !== null) { + $zeroQuoteUpdate['commission_snapshot'] = $snapRefreshed['commission_snapshot']; + } + Db::name('order')->where('id', $orderId)->update($zeroQuoteUpdate); + self::addOrderLog($orderId, OrderStatus::ACCEPTED, $remark, 'user', $uid); + + Db::name('order_quote')->where('id', $quoteId)->update(['status' => 'accept']); + Db::name('order_quote')->where('order_id', $orderId)->where('id', '<>', $quoteId) + ->where('status', 'wait')->update(['status' => 'reject']); + + self::sendNotify('user', (int)$lockedOrder['user_id'], '已选定服务师傅', $remark, 'order', $orderId); + self::sendNotify('master', (int)$master['id'], '您的报价已被选中', '订单 ' . $lockedOrder['order_no'] . ' 客户已选择您的报价 ¥' . $quoteAmount . ',请尽快联系客户', 'order', $orderId); + + Db::commit(); + } catch (\Throwable $e) { + Db::rollback(); + throw $e; + } + + return [ + 'need_wxpay' => false, + 'paid' => true, + 'scene' => 'quote', + 'order_id' => $orderId, + 'out_trade_no' => '', + 'amount' => 0.00, + 'pay_params' => null, + ]; + } + + /** + * 在事务内校验并消费订单锁定的优惠券 + */ + protected static function lockAndConsumeOrderCoupon(array $lockedOrder, $now) + { + $couponId = (int)($lockedOrder['coupon_id'] ?? 0); + $orderId = (int)$lockedOrder['id']; + if ($couponId <= 0) { + return; + } + $uc = Db::name('user_coupon') + ->where('id', $couponId) + ->where('user_id', (int)$lockedOrder['user_id']) + ->lock(true) + ->find(); + if (!$uc) { + throw new \InvalidArgumentException('订单关联的优惠券不存在'); + } + if ($uc['status'] === 'used' && (int)$uc['order_id'] === $orderId) { + return; + } + if ($uc['status'] === 'unused') { + $affectedUc = Db::name('user_coupon') + ->where('id', (int)$uc['id']) + ->where('user_id', (int)$lockedOrder['user_id']) + ->where('status', 'unused') + ->update([ + 'status' => 'used', + 'order_id' => $orderId, + 'use_time' => $now, + ]); + if (!$affectedUc) { + throw new \InvalidArgumentException('优惠券状态异常,请重新下单'); + } + return; + } + throw new \InvalidArgumentException('该优惠券已被其他订单使用或已过期'); + } + + protected static function broadcastNewOrderToMasters(array $order) + { + try { + $masters = Db::name('master') + ->where('verify_status', 'pass') + ->where('work_status', 'on') + ->where('user_id', '<>', (int)$order['user_id']) + ->limit(20) + ->select(); + foreach ($masters as $m) { + self::sendNotify('master', (int)$m['id'], '新工单就绪通知', '同城有新工单【' . $order['order_no'] . '】进入接单大厅,请尽快前往抢单!', 'order', (int)$order['id']); + } + } catch (\Throwable $e) { + Log::error('[PaymentService::broadcastNewOrderToMasters] ' . $e->getMessage()); + } + } + + protected static function addOrderLog($orderId, $status, $remark = '', $operatorType = 'system', $operatorId = 0) + { + Db::name('order_log')->insert([ + 'order_id' => (int)$orderId, + 'status' => (int)$status, + 'remark' => mb_substr((string)$remark, 0, 255), + 'operator_type' => $operatorType, + 'operator_id' => (int)$operatorId, + 'createtime' => time(), + ]); + } + + protected static function sendNotify($receiveType, $receiveId, $title, $content, $type = 'order', $orderId = 0) + { + if ((int)$receiveId <= 0) { + return; + } + Db::name('message')->insert([ + 'receive_type' => $receiveType, + 'receive_id' => (int)$receiveId, + 'type' => $type, + 'title' => mb_substr((string)$title, 0, 100), + 'content' => mb_substr((string)$content, 0, 500), + 'order_id' => (int)$orderId, + 'is_read' => 0, + 'createtime' => time(), + ]); + } + + protected static function sendMasterAndUserMessage($masterId, $userId, $title, $content) + { + $now = time(); + $rows = []; + if ((int)$masterId > 0) { + $rows[] = [ + 'receive_type' => 'master', + 'receive_id' => (int)$masterId, + 'type' => 'audit', + 'title' => mb_substr((string)$title, 0, 100), + 'content' => mb_substr((string)$content, 0, 500), + 'order_id' => 0, + 'is_read' => 0, + 'createtime' => $now, + ]; + } + if ((int)$userId > 0) { + $rows[] = [ + 'receive_type' => 'user', + 'receive_id' => (int)$userId, + 'type' => 'system', + 'title' => mb_substr((string)$title, 0, 100), + 'content' => mb_substr((string)$content, 0, 500), + 'order_id' => 0, + 'is_read' => 0, + 'createtime' => $now, + ]; + } + if ($rows) { + Db::name('message')->insertAll($rows); + } + } +} diff --git a/server/application/common/library/SmsService.php b/server/application/common/library/SmsService.php deleted file mode 100644 index 97e68a1..0000000 --- a/server/application/common/library/SmsService.php +++ /dev/null @@ -1,130 +0,0 @@ - $verifyCode, - 'order_no' => $orderNo - ], $content); - } - - /** - * 发送师傅 2 小时首联时效超时预警短信 - * @param string $mobile 师傅手机号 - * @param string $orderNo 订单号 - * @param int $remainMinutes 剩余分钟数 - * @return bool - */ - public static function sendFirstContactWarning($mobile, $orderNo, $remainMinutes = 30) - { - $content = "【优匠优服】您承接的工单 {$orderNo} 距离2小时首联考核截止仅剩 {$remainMinutes} 分钟,请尽快电话联系客户确认上门时间并在线打卡,超时将自动回收大厅并扣减信誉分!"; - return self::dispatch($mobile, 'contact_warning', [ - 'order_no' => $orderNo, - 'remain' => $remainMinutes - ], $content); - } - - /** - * 发送新工单接单/派单通知 - * @param string $mobile 师傅手机号 - * @param string $orderNo 订单号 - * @param string $serviceName 服务名称 - * @return bool - */ - public static function sendNewOrderNotice($mobile, $orderNo, $serviceName) - { - $content = "【优匠优服】您有新的待接工单 {$orderNo}({$serviceName}),请尽快前往师傅工作台查看并接单。"; - return self::dispatch($mobile, 'new_order', [ - 'order_no' => $orderNo, - 'service_name' => $serviceName - ], $content); - } - - /** - * 统一短信分发网关 - * @param string $mobile 手机号 - * @param string $event 事件 - * @param array $params 变量参数 - * @param string $content 完整文本内容 - * @return bool - */ - public static function dispatch($mobile, $event, array $params, $content) - { - $mobile = trim((string)$mobile); - if (strlen($mobile) !== 11) { - Log::record("[SmsService] 无效手机号: {$mobile}", 'warning'); - return false; - } - - // 记录到本地 fa_sms 短信流水表,保证可追溯性 - try { - Db::name('sms')->insert([ - 'event' => $event, - 'mobile' => $mobile, - 'code' => isset($params['code']) ? (string)$params['code'] : '', - 'times' => 0, - 'ip' => request() ? request()->ip() : '127.0.0.1', - 'createtime' => time() - ]); - } catch (\Exception $e) { - // 忽略重复或日志写入异常 - } - - // 获取当前配置驱动(默认为 local 本地安全开发模式) - $driver = Config::get('site.sms_driver'); - if (empty($driver)) { - $driver = 'local'; - } - - if ($driver === 'aliyun') { - return self::sendAliyun($mobile, $event, $params); - } elseif ($driver === 'qcloud') { - return self::sendQcloud($mobile, $event, $params); - } - - // 默认开发/模拟环境驱动:将真实内容打印至日志与控制台,返回成功 - Log::record("[SmsService::{$driver}] 发送至 {$mobile} | 事件: {$event} | 内容: {$content}", 'info'); - return true; - } - - /** - * 阿里云短信发送接口 - */ - protected static function sendAliyun($mobile, $event, array $params) - { - // 生产接入阿里云官方 SDK 规范 - Log::record("[SmsService::Aliyun] 发送短信至 {$mobile} 模板参数: " . json_encode($params, JSON_UNESCAPED_UNICODE), 'info'); - return true; - } - - /** - * 腾讯云短信发送接口 - */ - protected static function sendQcloud($mobile, $event, array $params) - { - // 生产接入腾讯云官方 SDK 规范 - Log::record("[SmsService::Qcloud] 发送短信至 {$mobile} 模板参数: " . json_encode($params, JSON_UNESCAPED_UNICODE), 'info'); - return true; - } -} diff --git a/server/application/common/library/TencentVerifyService.php b/server/application/common/library/TencentVerifyService.php new file mode 100644 index 0000000..7b02c71 --- /dev/null +++ b/server/application/common/library/TencentVerifyService.php @@ -0,0 +1,439 @@ + int, 'pass' => bool, 'msg' => string, 'raw' => array] + */ + public static function verifyIdCard($realname, $idCardNo) + { + $realname = trim((string)$realname); + $idCardNo = strtoupper(trim((string)$idCardNo)); + + if (empty($realname) || empty($idCardNo)) { + return [ + 'code' => -1, + 'pass' => false, + 'msg' => '姓名和身份证号不能为空', + 'raw' => [] + ]; + } + + // 基础格式校验(18位合法身份证号格式) + if (!preg_match('/^[1-9]\d{5}(18|19|20)\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])\d{3}[\dX]$/', $idCardNo)) { + return [ + 'code' => -2, + 'pass' => false, + 'msg' => '身份证号码格式不正确', + 'raw' => [] + ]; + } + + $payload = [ + 'Name' => $realname, + 'IdCard' => $idCardNo + ]; + + try { + $resp = self::request('faceid', '2018-03-01', 'IdCardVerification', $payload); + if (!isset($resp['Response'])) { + return [ + 'code' => -99, + 'pass' => false, + 'msg' => '核验服务未返回有效响应', + 'raw' => $resp + ]; + } + + $response = $resp['Response']; + + // 存在错误信息 + if (isset($response['Error'])) { + $errCode = $response['Error']['Code'] ?? ''; + $errMsg = $response['Error']['Message'] ?? '核验服务异常'; + self::log("[TencentVerify] IdCardVerification Error: {$errCode} - {$errMsg}", 'error'); + return [ + 'code' => -9, + 'pass' => false, + 'msg' => '实名核验接口调用失败:' . $errMsg, + 'raw' => $response + ]; + } + + $result = (string)($response['Result'] ?? ''); + $desc = (string)($response['Description'] ?? ''); + + // Result: 0 一致, -1 不一致, -2 非法身份证号, -3 非法姓名, -4 证件库服务异常, -5 证件库中无此身份证记录 + if ($result === '0') { + return [ + 'code' => 0, + 'pass' => true, + 'msg' => '实名核验一致', + 'raw' => $response + ]; + } + + $msgMap = [ + '-1' => '姓名与身份证号不匹配,请核对后重试', + '-2' => '身份证号码不合法,请检查输入', + '-3' => '姓名格式不合法,请确认无误', + '-4' => '全国公安公民身份库系统繁忙,请稍后重试', + '-5' => '全国身份库中无此身份证记录,请核对信息' + ]; + $friendlyMsg = $msgMap[$result] ?? ($desc ?: '实名核验未通过,请核对信息'); + + return [ + 'code' => (int)$result, + 'pass' => false, + 'msg' => $friendlyMsg, + 'raw' => $response + ]; + } catch (\Exception $e) { + self::log("[TencentVerify] Exception in verifyIdCard: " . $e->getMessage(), 'error'); + return [ + 'code' => -999, + 'pass' => false, + 'msg' => '实名核验系统通信异常:' . $e->getMessage(), + 'raw' => [] + ]; + } + } + + /** + * 身份证 OCR 文字识别 + * 支持公网 ImageUrl、本地文件路径自动 Base64 转换以及原始 Base64 编码 + * + * @param string $image 图片 URL 或本地文件相对路径/Base64 + * @param string $cardSide FRONT:人像面, BACK:国徽面 + * @return array ['code' => int, 'success' => bool, 'data' => array, 'msg' => string] + */ + public static function idCardOcr($image, $cardSide = 'FRONT') + { + $image = trim((string)$image); + $cardSide = strtoupper(trim((string)$cardSide)) === 'BACK' ? 'BACK' : 'FRONT'; + + if (empty($image)) { + return [ + 'code' => -1, + 'success' => false, + 'data' => [], + 'msg' => '图片地址不能为空' + ]; + } + + $payload = [ + 'CardSide' => $cardSide + ]; + + // 识别输入类型:若为 Base64 或本地文件,优先使用 ImageBase64(保障内网或CDN延迟时依然100%识别成功) + if (strpos($image, 'data:image') === 0) { + $payload['ImageBase64'] = preg_replace('/^data:image\/\w+;base64,/', '', $image); + } elseif (strlen($image) > 500 && strpos($image, 'http') !== 0) { + $payload['ImageBase64'] = $image; + } elseif (preg_match('/^https?:\/\//i', $image)) { + $payload['ImageUrl'] = $image; + } else { + // 本地相对路径检查 + $publicPath = defined('ROOT_PATH') ? (ROOT_PATH . 'public') : (dirname(dirname(dirname(__DIR__))) . '/public'); + $localFile = $publicPath . (strpos($image, '/') === 0 ? '' : '/') . $image; + if (file_exists($localFile) && is_readable($localFile)) { + $payload['ImageBase64'] = base64_encode(file_get_contents($localFile)); + } else { + $cdnUrl = class_exists('\\think\\Config') ? (\think\Config::get('upload.cdnurl') ?: \think\Config::get('site.cdnurl')) : ''; + $payload['ImageUrl'] = !empty($cdnUrl) ? (rtrim($cdnUrl, '/') . '/' . ltrim($image, '/')) : $image; + } + } + + try { + $resp = self::request('ocr', '2018-11-19', 'IDCardOCR', $payload); + if (!isset($resp['Response'])) { + return [ + 'code' => -99, + 'success' => false, + 'data' => [], + 'msg' => 'OCR 服务无有效响应' + ]; + } + + $response = $resp['Response']; + if (isset($response['Error'])) { + $errMsg = $response['Error']['Message'] ?? '识别失败'; + return [ + 'code' => -2, + 'success' => false, + 'data' => [], + 'msg' => '身份证识别失败:' . $errMsg + ]; + } + + $parsed = [ + 'side' => $cardSide, + ]; + + if ($cardSide === 'FRONT') { + $parsed['name'] = (string)($response['Name'] ?? ''); + $parsed['sex'] = (string)($response['Sex'] ?? ''); + $parsed['nation'] = (string)($response['Nation'] ?? ''); + $parsed['birth'] = (string)($response['Birth'] ?? ''); + $parsed['address'] = (string)($response['Address'] ?? ''); + $parsed['idcard_no'] = (string)($response['IdNum'] ?? ''); + } else { + $authority = (string)($response['Authority'] ?? ''); + $validDate = (string)($response['ValidDate'] ?? ''); + $parsed['authority'] = $authority; + $parsed['valid_date'] = $validDate; + + // 智能检测身份证是否已过有效期 + $expireInfo = self::checkIdCardExpired($validDate); + $parsed['is_expired'] = $expireInfo['is_expired']; + $parsed['expire_info'] = $expireInfo; + } + + return [ + 'code' => 0, + 'success' => true, + 'data' => $parsed, + 'msg' => '识别成功' + ]; + } catch (\Exception $e) { + self::log("[TencentVerify] Exception in idCardOcr: " . $e->getMessage(), 'error'); + return [ + 'code' => -999, + 'success' => false, + 'data' => [], + 'msg' => 'OCR 系统通信异常:' . $e->getMessage() + ]; + } + } + + /** + * 发送腾讯云 API 3.0 请求 (TC3-HMAC-SHA256 签名) + * + * @param string $service 服务标识 (如 faceid, ocr) + * @param string $version 接口版本 (如 2018-03-01) + * @param string $action 接口动作 (如 IdCardVerification) + * @param array $payload 请求体 JSON 字典 + * @param string $region 地域标识 + * @return array + * @throws \Exception + */ + public static function request($service, $version, $action, array $payload, $region = '') + { + $secretId = self::getSecretId(); + $secretKey = self::getSecretKey(); + if ($secretId === '' || $secretKey === '') { + throw new \Exception('腾讯云实名核验密钥未配置,请在后台站点配置或 .env 中设置 tencent_secret_id / tencent_secret_key'); + } + $region = !empty($region) ? $region : self::DEFAULT_REGION; + $host = "{$service}.tencentcloudapi.com"; + + $payloadJson = json_encode($payload, JSON_UNESCAPED_UNICODE); + $timestamp = time(); + $date = gmdate("Y-m-d", $timestamp); + + // 1. 规范请求串 + $httpRequestMethod = "POST"; + $canonicalUri = "/"; + $canonicalQueryString = ""; + $canonicalHeaders = "content-type:application/json; charset=utf-8\nhost:" . $host . "\nx-tc-action:" . strtolower($action) . "\n"; + $signedHeaders = "content-type;host;x-tc-action"; + $hashedRequestPayload = hash("sha256", $payloadJson); + $canonicalRequest = $httpRequestMethod . "\n" + . $canonicalUri . "\n" + . $canonicalQueryString . "\n" + . $canonicalHeaders . "\n" + . $signedHeaders . "\n" + . $hashedRequestPayload; + + // 2. 待签名字符串 + $algorithm = "TC3-HMAC-SHA256"; + $credentialScope = $date . "/" . $service . "/tc3_request"; + $hashedCanonicalRequest = hash("sha256", $canonicalRequest); + $stringToSign = $algorithm . "\n" + . $timestamp . "\n" + . $credentialScope . "\n" + . $hashedCanonicalRequest; + + // 3. 计算签名 + $secretDate = hash_hmac("sha256", $date, "TC3" . $secretKey, true); + $secretService = hash_hmac("sha256", $service, $secretDate, true); + $secretSigning = hash_hmac("sha256", "tc3_request", $secretService, true); + $signature = hash_hmac("sha256", $stringToSign, $secretSigning); + + // 4. 组装请求头 + $authorization = $algorithm . " " + . "Credential=" . $secretId . "/" . $credentialScope . ", " + . "SignedHeaders=" . $signedHeaders . ", " + . "Signature=" . $signature; + + $headers = [ + "Authorization: " . $authorization, + "Content-Type: application/json; charset=utf-8", + "Host: " . $host, + "X-TC-Action: " . $action, + "X-TC-Timestamp: " . $timestamp, + "X-TC-Version: " . $version, + "X-TC-Region: " . $region + ]; + + // 5. 执行 cURL 请求 + $ch = curl_init(); + curl_setopt($ch, CURLOPT_URL, "https://" . $host); + curl_setopt($ch, CURLOPT_POST, true); + curl_setopt($ch, CURLOPT_POSTFIELDS, $payloadJson); + curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_TIMEOUT, 15); + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true); + curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2); + + $response = curl_exec($ch); + $curlError = curl_error($ch); + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + + if ($curlError) { + throw new \Exception("cURL 执行失败: " . $curlError); + } + + $decoded = json_decode($response, true); + if ($decoded === null) { + throw new \Exception("解析腾讯云响应 JSON 失败(HTTP {$httpCode}): {$response}"); + } + + return $decoded; + } + + /** + * 智能检测身份证是否已过有效期 + * + * @param string $validDateStr 腾讯云 OCR 返回的有效期字符串,如 "2024.01.15-2044.01.15" 或 "2014.05.20-长期" + * @return array ['has_date' => bool, 'is_expired' => bool, 'is_permanent' => bool, 'expire_date' => string, 'text' => string] + */ + public static function checkIdCardExpired($validDateStr) + { + $validDateStr = trim((string)$validDateStr); + if (empty($validDateStr)) { + return [ + 'has_date' => false, + 'is_expired' => false, + 'is_permanent' => false, + 'expire_date' => '', + 'text' => '未检测到有效期限' + ]; + } + + // 若包含“长期”字样,判定永久有效 + if (mb_strpos($validDateStr, '长期') !== false) { + return [ + 'has_date' => true, + 'is_expired' => false, + 'is_permanent' => true, + 'expire_date' => '长期', + 'text' => '长期有效' + ]; + } + + // 正则提取所有符合 YYYY.MM.DD / YYYY-MM-DD / YYYYMMDD 的日期片段 + preg_match_all('/(\d{4})[\.\-\/年]?(\d{2})[\.\-\/月]?(\d{2})/', $validDateStr, $matches); + if (!empty($matches[0])) { + $lastIdx = count($matches[0]) - 1; + $year = $matches[1][$lastIdx]; + $month = $matches[2][$lastIdx]; + $day = $matches[3][$lastIdx]; + $formatted = "{$year}-{$month}-{$day}"; + + $expireTs = strtotime("{$formatted} 23:59:59"); + $todayTs = strtotime(date('Y-m-d 00:00:00')); + $isExpired = ($expireTs < $todayTs); + + return [ + 'has_date' => true, + 'is_expired' => $isExpired, + 'is_permanent' => false, + 'expire_date' => $formatted, + 'text' => $formatted . ($isExpired ? '(已过期)' : '(有效)') + ]; + } + + return [ + 'has_date' => true, + 'is_expired' => false, + 'is_permanent' => false, + 'expire_date' => $validDateStr, + 'text' => $validDateStr + ]; + } +} diff --git a/server/application/common/library/TiandituService.php b/server/application/common/library/TiandituService.php index 780ff11..d112888 100644 --- a/server/application/common/library/TiandituService.php +++ b/server/application/common/library/TiandituService.php @@ -11,20 +11,17 @@ use think\Log; */ class TiandituService { - /** 默认天地图服务 Key(可在 application/config.php 或后台全局配置覆盖) */ - const DEFAULT_KEY = '369ea1edc677b9e3746093f8d9dc4465'; - /** - * 获取天地图 Key + * 获取天地图 Key(严格读取站点配置或 .env,不在源码内硬编码密钥) * @return string */ public static function getKey() { $key = Config::get('site.tianditu_key'); - if (empty($key)) { - $key = self::DEFAULT_KEY; + if (empty($key) && class_exists('\\think\\Env')) { + $key = \think\Env::get('tianditu.key', ''); } - return $key; + return trim((string)$key); } /** @@ -40,6 +37,10 @@ class TiandituService } $key = self::getKey(); + if ($key === '') { + Log::record('[TiandituService] 天地图密钥 site.tianditu_key 未配置,无法执行正地理编码', 'error'); + return null; + } $url = 'https://api.tianditu.gov.cn/geocoder?ds=' . urlencode(json_encode(['keyWord' => $address])) . '&tk=' . $key; $response = self::httpGet($url); @@ -75,6 +76,10 @@ class TiandituService } $key = self::getKey(); + if ($key === '') { + Log::record('[TiandituService] 天地图密钥 site.tianditu_key 未配置,无法执行逆地理编码', 'error'); + return null; + } $postData = [ 'postStr' => json_encode([ 'lon' => $lng, @@ -108,6 +113,298 @@ class TiandituService return null; } + /** + * 关键词地点检索(POI 搜索 + 正地理编码融合兜底) + * @param string $keyword 地名/区域/小区关键词 + * @param int $limit 最多返回条数 + * @param string $city 可选限定城市名,提高同城检索命中率 + * @param float|null $nearLat 可选参考中心纬度(用于计算距离展示) + * @param float|null $nearLng 可选参考中心经度 + * @return array> + */ + public static function searchPlaces($keyword, $limit = 10, $city = '', $nearLat = null, $nearLng = null) + { + $keyword = trim((string)$keyword); + if ($keyword === '') { + return []; + } + $limit = min(20, max(1, (int)$limit)); + $key = self::getKey(); + if ($key === '') { + return []; + } + + $city = trim((string)$city); + $effectiveQuery = $keyword; + if ($city !== '' && mb_strpos($keyword, mb_substr($city, 0, 2)) === false) { + $effectiveQuery = $city . ' ' . $keyword; + } + + $results = []; + $seen = []; + $hasNear = $nearLat !== null && $nearLng !== null && (abs((float)$nearLat) > 0.0001 || abs((float)$nearLng) > 0.0001); + + $pois = self::requestPoiSearch([ + 'keyWord' => $effectiveQuery, + 'level' => '12', + 'mapBound' => '73.0,3.0,136.0,54.0', + 'queryType' => '1', + 'count' => (string)$limit, + 'start' => '0', + ], $key); + + // 若限定城市后结果为空,放宽至全国关键词再查一次 + if (empty($pois) && $effectiveQuery !== $keyword) { + $pois = self::requestPoiSearch([ + 'keyWord' => $keyword, + 'level' => '12', + 'mapBound' => '73.0,3.0,136.0,54.0', + 'queryType' => '1', + 'count' => (string)$limit, + 'start' => '0', + ], $key); + } + + foreach ($pois as $poi) { + $item = self::formatPoiItem($poi, $keyword, '', $hasNear ? (float)$nearLat : null, $hasNear ? (float)$nearLng : null); + if ($item === null) { + continue; + } + $dedupKey = mb_strtolower($item['name']) . '|' . round($item['lat'], 4) . '|' . round($item['lng'], 4); + if (isset($seen[$dedupKey])) { + continue; + } + $seen[$dedupKey] = true; + $results[] = $item; + } + + // 若属于行政区划或短关键词且结果偏少,补充一次正地理编码结果置顶 + $geo = self::geocoder($effectiveQuery) ?: ($effectiveQuery !== $keyword ? self::geocoder($keyword) : null); + if (is_array($geo) && (!empty($geo['lng']) && !empty($geo['lat']))) { + $geoLng = round((float)$geo['lng'], 6); + $geoLat = round((float)$geo['lat'], 6); + $regeo = self::regeocoder($geoLat, $geoLng); + $cityLabel = ''; + $districtLabel = ''; + $fullLabel = $keyword; + if (is_array($regeo)) { + $cityLabel = trim((string)($regeo['city'] ?: ($regeo['province'] ?? ''))); + $districtLabel = trim((string)($regeo['district'] ?? '')); + $fullLabel = trim(($regeo['province'] ?? '') . ' ' . $cityLabel . ' ' . $districtLabel); + if (!empty($regeo['formatted_address'])) { + $fullLabel = $regeo['formatted_address']; + } + } + $distM = $hasNear ? (int)round(self::distance((float)$nearLat, (float)$nearLng, $geoLat, $geoLng)) : null; + $geoDedup = mb_strtolower($keyword) . '|' . round($geoLat, 4) . '|' . round($geoLng, 4); + if (!isset($seen[$geoDedup])) { + array_unshift($results, [ + 'name' => $keyword, + 'address' => $fullLabel ?: $keyword, + 'city' => $cityLabel, + 'district' => $districtLabel, + 'lng' => $geoLng, + 'lat' => $geoLat, + 'distance_m' => $distM, + 'distance_text' => self::formatMetersText($distM), + 'category' => 'area', + ]); + } + } + + return array_slice($results, 0, $limit); + } + + /** + * 基于经纬度的周边真实 POI 地标检索(配合地图选点展示周边小区/大厦/广场) + * @param float $lat 中心纬度 + * @param float $lng 中心经度 + * @param string $category all|residential|office|plaza + * @param int $radius 搜索半径(米,默认 3000) + * @param int $limit 最多返回条数 + * @return array{center: array|null, list: array>} + */ + public static function nearbyPlaces($lat, $lng, $category = 'all', $radius = 3000, $limit = 14) + { + $lat = round((float)$lat, 6); + $lng = round((float)$lng, 6); + $limit = min(20, max(1, (int)$limit)); + $radius = max(500, min(10000, (int)$radius)); + + if (abs($lat) < 0.0001 && abs($lng) < 0.0001) { + return ['center' => null, 'list' => []]; + } + + $regeo = self::regeocoder($lat, $lng); + $centerCity = is_array($regeo) ? trim((string)($regeo['city'] ?: ($regeo['province'] ?? ''))) : ''; + $centerDistrict = is_array($regeo) ? trim((string)($regeo['district'] ?? '')) : ''; + $centerPoi = is_array($regeo) ? trim((string)($regeo['poi'] ?? '')) : ''; + $centerRoad = is_array($regeo) ? trim((string)($regeo['road'] ?? '')) : ''; + $centerFormatted = is_array($regeo) ? trim((string)($regeo['formatted_address'] ?? '')) : ''; + $centerName = $centerPoi !== '' ? $centerPoi : ($centerRoad !== '' ? $centerRoad : ($centerDistrict !== '' ? ($centerDistrict . '附近') : '当前地图中心')); + $centerAddress = $centerFormatted !== '' ? $centerFormatted : trim($centerCity . $centerDistrict . $centerRoad); + + $centerInfo = [ + 'name' => $centerName, + 'address' => $centerAddress ?: '已选定坐标点位', + 'province' => is_array($regeo) ? trim((string)($regeo['province'] ?? '')) : '', + 'city' => $centerCity, + 'district' => $centerDistrict, + 'road' => $centerRoad, + 'poi' => $centerPoi, + 'formatted_address' => $centerFormatted, + 'lng' => $lng, + 'lat' => $lat, + ]; + + $list = []; + $seen = []; + + // 中心位置锚点本身固定作为首条 + $list[] = [ + 'name' => $centerName, + 'address' => $centerAddress ?: '当前选定中心点', + 'city' => $centerCity, + 'district' => $centerDistrict, + 'lng' => $lng, + 'lat' => $lat, + 'distance_m' => 0, + 'distance_text' => '当前点', + 'category' => 'center', + 'is_center' => 1, + ]; + $seen[mb_strtolower($centerName)] = true; + + $key = self::getKey(); + if ($key !== '') { + $categoryKeywordMap = [ + 'residential' => [['kw' => '小区', 'cat' => 'residential']], + 'office' => [['kw' => '大厦', 'cat' => 'office']], + 'plaza' => [['kw' => '广场', 'cat' => 'plaza']], + 'all' => [ + ['kw' => '小区', 'cat' => 'residential'], + ['kw' => '大厦', 'cat' => 'office'], + ['kw' => '广场', 'cat' => 'plaza'], + ], + ]; + $queries = $categoryKeywordMap[$category] ?? $categoryKeywordMap['all']; + $perCount = count($queries) > 1 ? 6 : $limit; + $poiRows = []; + + foreach ($queries as $qItem) { + $rawPois = self::requestPoiSearch([ + 'keyWord' => $qItem['kw'], + 'level' => '15', + 'queryType' => '3', + 'pointLonlat' => $lng . ',' . $lat, + 'queryRadius' => (string)$radius, + 'count' => (string)$perCount, + 'start' => '0', + ], $key); + + foreach ($rawPois as $rp) { + $item = self::formatPoiItem($rp, $qItem['kw'], $qItem['cat'], $lat, $lng, $centerCity, $centerDistrict); + if ($item === null) { + continue; + } + $nameKey = mb_strtolower($item['name']); + if (isset($seen[$nameKey])) { + continue; + } + $seen[$nameKey] = true; + $poiRows[] = $item; + } + } + + usort($poiRows, function ($a, $b) { + return ((int)($a['distance_m'] ?? 999999)) <=> ((int)($b['distance_m'] ?? 999999)); + }); + + $list = array_merge($list, array_slice($poiRows, 0, max(0, $limit - 1))); + } + + return [ + 'center' => $centerInfo, + 'list' => $list, + ]; + } + + /** + * 统一请求天地图 v2/search 接口 + */ + protected static function requestPoiSearch(array $postParams, $key) + { + $postStr = json_encode($postParams, JSON_UNESCAPED_UNICODE); + $url = 'https://api.tianditu.gov.cn/v2/search?postStr=' . urlencode($postStr) . '&type=query&tk=' . $key; + $resp = self::httpGet($url); + if (!$resp) { + return []; + } + $data = json_decode($resp, true); + if (is_array($data) && !empty($data['pois']) && is_array($data['pois'])) { + return $data['pois']; + } + return []; + } + + /** + * 统一标准化 POI 输出项,避免搜索与周边查询逻辑重复 + */ + protected static function formatPoiItem(array $poi, $fallbackName, $category = '', $refLat = null, $refLng = null, $defaultCity = '', $defaultDistrict = '') + { + if (empty($poi['lonlat'])) { + return null; + } + $parts = explode(',', (string)$poi['lonlat']); + if (count($parts) !== 2) { + return null; + } + $lng = round((float)$parts[0], 6); + $lat = round((float)$parts[1], 6); + if (abs($lat) < 0.0001 && abs($lng) < 0.0001) { + return null; + } + + $distM = null; + if ($refLat !== null && $refLng !== null) { + $distM = (int)round(self::distance($refLat, $refLng, $lat, $lng)); + } elseif (!empty($poi['distance'])) { + $rawDist = strtolower(trim((string)$poi['distance'])); + if (strpos($rawDist, 'km') !== false) { + $distM = (int)round((float)str_replace('km', '', $rawDist) * 1000); + } else { + $distM = (int)round((float)str_replace('m', '', $rawDist)); + } + } + + return [ + 'name' => trim((string)($poi['name'] ?? $fallbackName)), + 'address' => trim((string)($poi['address'] ?? '')), + 'city' => trim((string)($poi['city'] ?? $defaultCity)), + 'district' => trim((string)($poi['county'] ?? $defaultDistrict)), + 'lng' => $lng, + 'lat' => $lat, + 'distance_m' => $distM, + 'distance_text' => self::formatMetersText($distM), + 'category' => $category, + ]; + } + + /** + * 米数转紧凑展示距离 + */ + protected static function formatMetersText($meters) + { + if ($meters === null) { + return ''; + } + $m = (int)$meters; + if ($m < 1000) { + return max(10, $m) . 'm'; + } + return round($m / 1000, 2) . 'km'; + } + /** * 高精度两点大圆球面距离计算(Haversine Formula) * @param float $lat1 起点纬度 @@ -179,10 +476,13 @@ class TiandituService $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); - curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); - curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true); + curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2); curl_setopt($ch, CURLOPT_TIMEOUT, $timeout); $result = curl_exec($ch); + if ($result === false) { + Log::record('[TiandituService] curl_exec 失败: ' . curl_error($ch), 'warning'); + } curl_close($ch); return $result; } diff --git a/server/application/common/library/WxLogin.php b/server/application/common/library/WxLogin.php index 335f71b..ebf52fa 100644 --- a/server/application/common/library/WxLogin.php +++ b/server/application/common/library/WxLogin.php @@ -17,31 +17,46 @@ class WxLogin */ public static function jscode2session($code) { - $appid = \think\Env::get('wechat.appid'); - $secret = \think\Env::get('wechat.secret'); - if (!$appid || !$secret) { - return ['error' => '微信登录未配置']; + $appid = trim((string)\think\Env::get('wechat.appid', '')); + $secret = trim((string)\think\Env::get('wechat.secret', '')); + if ($appid === '' || $secret === '') { + return ['error' => '微信登录参数未配置完整(请检查 server/.env 的 wechat.appid 与 wechat.secret)', 'errcode' => -1001]; } - $url = 'https://api.weixin.qq.com/sns/jscode2session?appid=' . $appid - . '&secret=' . $secret + $code = trim((string)$code); + if ($code === '') { + return ['error' => '缺少微信登录临时凭证 code,请在微信小程序内重新打开重试', 'errcode' => -1002]; + } + $url = 'https://api.weixin.qq.com/sns/jscode2session?appid=' . urlencode($appid) + . '&secret=' . urlencode($secret) . '&js_code=' . urlencode($code) . '&grant_type=authorization_code'; - $ctx = stream_context_create(['http' => ['timeout' => 8, 'ignore_errors' => true]]); + $ctx = stream_context_create([ + 'http' => ['timeout' => 8, 'ignore_errors' => true], + 'ssl' => ['verify_peer' => true, 'verify_peer_name' => true], + ]); $resp = @file_get_contents($url, false, $ctx); if ($resp === false) { - return ['error' => '微信服务不可达,请稍后重试']; + $lastErr = error_get_last(); + $errHint = !empty($lastErr['message']) ? $lastErr['message'] : '网络超时或 SSL 证书校验失败'; + \think\Log::error('[WxLogin::jscode2session] HTTP请求失败: ' . $errHint); + return ['error' => '连接微信授权接口失败(请检查服务器外网HTTPS与CA根证书配置)', 'errcode' => -1003]; } $wx = json_decode($resp, true); - if (empty($wx['openid'])) { - $ec = isset($wx['errcode']) ? (int)$wx['errcode'] : 0; - $em = isset($wx['errmsg']) ? $wx['errmsg'] : '未知错误'; + if (!is_array($wx) || empty($wx['openid'])) { + $ec = isset($wx['errcode']) ? (int)$wx['errcode'] : -1004; + $em = isset($wx['errmsg']) ? (string)$wx['errmsg'] : '接口返回报文无效'; $hint = [ - 40029 => '小程序 appid 与服务端配置不一致:请确认 manifest.json 的 mp-weixin.appid 为 ' . $appid, - 40125 => 'appsecret 配置错误,请检查 .env 的 [wechat] secret', - 45011 => '登录请求过于频繁,请稍后重试', - 40226 => '该微信账号被风控限制,请稍后再试', + -1 => '微信登录服务暂时繁忙,请稍后重试', + 40029 => '微信临时登录凭证 code 无效或小程序 AppID 与服务端配置不一致(服务端配置为:' . $appid . ')', + 40125 => '微信小程序密钥 appsecret 配置错误,请检查 server/.env 的 [wechat] secret', + 40163 => '微信登录凭证 code 已被使用过一次,请重新点击操作获取新授权', + 41002 => '请求缺少小程序 AppID 参数', + 41004 => '请求缺少小程序密钥 appsecret 参数', + 45011 => '微信登录请求过于频繁触发限流,请稍后再试', + 40226 => '该微信账号触发微信安全风控拦截,暂时无法授权登录', ]; - $msg = isset($hint[$ec]) ? $hint[$ec] : ('微信登录失败(' . $ec . '):' . $em); + $msg = isset($hint[$ec]) ? $hint[$ec] : ('微信授权登录校验未通过(微信错误码:' . $ec . '),请核对小程序 AppID/Secret 配置或重试'); + \think\Log::error("[WxLogin::jscode2session] errcode={$ec}, errmsg={$em}"); return ['error' => $msg, 'errcode' => $ec]; } return $wx; diff --git a/server/application/common/library/WxPayException.php b/server/application/common/library/WxPayException.php new file mode 100644 index 0000000..a1d7568 --- /dev/null +++ b/server/application/common/library/WxPayException.php @@ -0,0 +1,69 @@ +wxCode = (string)$wxCode; + $this->httpCode = (int)$httpCode; + $this->curlErrno = (int)$curlErrno; + $this->isUncertain = (bool)$isUncertain; + $this->rawDetail = (string)$rawDetail; + } + + public function getWxCode() + { + return $this->wxCode; + } + + public function getHttpCode() + { + return $this->httpCode; + } + + public function getCurlErrno() + { + return $this->curlErrno; + } + + public function isUncertain() + { + return $this->isUncertain; + } + + public function getRawDetail() + { + return $this->rawDetail; + } +} diff --git a/server/application/common/library/WxPayV3Service.php b/server/application/common/library/WxPayV3Service.php new file mode 100644 index 0000000..a81d388 --- /dev/null +++ b/server/application/common/library/WxPayV3Service.php @@ -0,0 +1,1063 @@ +appId = trim((string)($overrideConfig['appid'] ?? Env::get('wechat.appid', ''))); + $this->mchId = trim((string)($overrideConfig['mch_id'] ?? Env::get('wechat.mch_id', ''))); + $this->keyV3 = trim((string)($overrideConfig['key_v3'] ?? Env::get('wechat.key_v3', ''))); + $this->publicKeyId = trim((string)($overrideConfig['public_key_id'] ?? Env::get('wechat.public_key_id', ''))); + + $certRel = trim((string)($overrideConfig['cert_client'] ?? (Env::get('wechat.cert_client', '') ?: '/addons/epay/certs/apiclient_cert.pem'))); + $keyRel = trim((string)($overrideConfig['cert_key'] ?? (Env::get('wechat.cert_key', '') ?: '/addons/epay/certs/apiclient_key.pem'))); + $pubRel = trim((string)($overrideConfig['public_key'] ?? (Env::get('wechat.public_key', '') ?: '/addons/epay/certs/public_key.pem'))); + + $this->certClientPath = $this->resolveCertPath($certRel); + $this->certKeyPath = $this->resolveCertPath($keyRel); + $this->publicKeyPath = $this->resolveCertPath($pubRel); + + $this->notifyDomain = rtrim(trim((string)($overrideConfig['notify_domain'] ?? Env::get('wechat.notify_domain', ''))), '/'); + $this->transferSceneId = trim((string)($overrideConfig['transfer_scene_id'] ?? (Env::get('wechat.transfer_scene_id', '') ?: '1005'))); + } + + /** + * 获取当前生效的商户号与小程序 AppID + */ + public function getMchId() + { + return $this->mchId; + } + + public function getAppId() + { + return $this->appId; + } + + /** + * 构建公网 HTTPS 回调 URL(严格基于配置的 notify_domain,不做硬编码域名或请求头兜底) + * @param string $path 如 /api/wxpay/notify + * @return string + */ + public function buildNotifyUrl($path) + { + $path = '/' . ltrim((string)$path, '/'); + $this->assertValidNotifyDomain($this->notifyDomain); + return $this->notifyDomain . $path; + } + + /** + * 校验公网回调域名必须合法有效 + */ + protected function assertValidNotifyDomain($domain) + { + $domain = trim((string)$domain); + if ($domain === '') { + throw new WxPayException( + '微信支付公网回调域名(wechat.notify_domain)未配置,请在 server/.env 的 [wechat] 段补充正确的 HTTPS 公网域名', + 'CONFIG_NOTIFY_DOMAIN_EMPTY' + ); + } + $parsed = parse_url($domain); + $scheme = strtolower((string)($parsed['scheme'] ?? '')); + $host = strtolower((string)($parsed['host'] ?? '')); + if ($scheme !== 'https' || $host === '' || in_array($host, ['localhost', '127.0.0.1', '::1'], true) || strpos($host, '.') === false) { + throw new WxPayException( + '微信支付回调域名配置不合法(当前:' . $domain . '),必须为可公开访问的 HTTPS 域名(不支持 HTTP 或本地回环地址)', + 'CONFIG_NOTIFY_DOMAIN_INVALID' + ); + } + } + + /** + * 1. 小程序 JSAPI 统一下单,并生成前端 wx.requestPayment 所需签名包 + * + * @param array $params [ + * 'out_trade_no' => string (必填, 6-32位), + * 'description' => string (必填, 商品描述), + * 'amount' => float (必填, 单位:元), + * 'openid' => string (必填, 付款用户 OpenID), + * 'attach' => string (选填, 附加数据 <=128字符), + * 'notify_url' => string (选填), + * ] + * @return array ['prepay_id' => string, 'pay_params' => array] + */ + public function createJsapiOrder(array $params) + { + $this->assertConfigReady(); + + $outTradeNo = trim((string)($params['out_trade_no'] ?? '')); + $desc = mb_substr(trim((string)($params['description'] ?? '优匠优服上门安装维修服务')), 0, 60); + $amountFen = (int)round(((float)($params['amount'] ?? 0)) * 100); + $openid = trim((string)($params['openid'] ?? '')); + $attach = mb_substr(trim((string)($params['attach'] ?? '')), 0, 127); + $notifyUrl = !empty($params['notify_url']) ? (string)$params['notify_url'] : $this->buildNotifyUrl('/api/wxpay/notify'); + + if ($outTradeNo === '' || $amountFen <= 0 || $openid === '') { + throw new \InvalidArgumentException('统一下单参数不完整(单号/金额/OpenID)'); + } + + $payload = [ + 'appid' => $this->appId, + 'mchid' => $this->mchId, + 'description' => $desc, + 'out_trade_no' => $outTradeNo, + 'notify_url' => $notifyUrl, + 'amount' => [ + 'total' => $amountFen, + 'currency' => 'CNY', + ], + 'payer' => [ + 'openid' => $openid, + ], + ]; + if ($attach !== '') { + $payload['attach'] = $attach; + } + + $resp = $this->request('POST', '/v3/pay/transactions/jsapi', $payload); + if (empty($resp['prepay_id'])) { + throw new \RuntimeException('微信统一下单未返回 prepay_id'); + } + + $prepayId = (string)$resp['prepay_id']; + return [ + 'prepay_id' => $prepayId, + 'pay_params' => $this->buildMiniPayParams($prepayId), + ]; + } + + /** + * 生成小程序 wx.requestPayment 二次签名参数包 + */ + public function buildMiniPayParams($prepayId) + { + $timeStamp = (string)time(); + $nonceStr = $this->generateNonceStr(); + $package = 'prepay_id=' . trim((string)$prepayId); + $message = $this->appId . "\n" . $timeStamp . "\n" . $nonceStr . "\n" . $package . "\n"; + $paySign = $this->signMessage($message); + + return [ + 'appId' => $this->appId, + 'timeStamp' => $timeStamp, + 'nonceStr' => $nonceStr, + 'package' => $package, + 'signType' => 'RSA', + 'paySign' => $paySign, + ]; + } + + /** + * 2. 商户订单号主动查询微信支付状态 + * @param string $outTradeNo + * @return array 微信交易单详情(含 trade_state, transaction_id, amount, success_time 等) + */ + public function queryOrderByOutTradeNo($outTradeNo) + { + $this->assertConfigReady(); + $outTradeNo = trim((string)$outTradeNo); + if ($outTradeNo === '') { + throw new \InvalidArgumentException('缺少商户订单号'); + } + $uri = '/v3/pay/transactions/out-trade-no/' . rawurlencode($outTradeNo) . '?mchid=' . rawurlencode($this->mchId); + return $this->request('GET', $uri); + } + + /** + * 3. 关闭未支付订单 + */ + public function closeOrder($outTradeNo) + { + $this->assertConfigReady(); + $outTradeNo = trim((string)$outTradeNo); + if ($outTradeNo === '') { + throw new \InvalidArgumentException('关闭微信支付单失败:缺少商户支付单号 out_trade_no'); + } + $uri = '/v3/pay/transactions/out-trade-no/' . rawurlencode($outTradeNo) . '/close'; + $this->request('POST', $uri, ['mchid' => $this->mchId]); + return true; + } + + /** + * 4. 交易原路退款申请 + * + * @param array $params [ + * 'out_trade_no' => string, + * 'out_refund_no' => string, + * 'refund_amount' => float (元), + * 'total_amount' => float (元, 原支付单总金额), + * 'reason' => string, + * 'notify_url' => string (选填), + * ] + * @return array 微信退款响应(refund_id, status: SUCCESS|CLOSED|PROCESSING|ABNORMAL) + */ + public function refund(array $params) + { + $this->assertConfigReady(); + + $outTradeNo = trim((string)($params['out_trade_no'] ?? '')); + $outRefundNo = trim((string)($params['out_refund_no'] ?? '')); + $refundFen = (int)round(((float)($params['refund_amount'] ?? 0)) * 100); + $totalFen = (int)round(((float)($params['total_amount'] ?? 0)) * 100); + $reason = mb_substr(trim((string)($params['reason'] ?? '订单取消原路退款')), 0, 80); + $notifyUrl = !empty($params['notify_url']) ? (string)$params['notify_url'] : $this->buildNotifyUrl('/api/wxpay/refundNotify'); + + if ($outTradeNo === '' || $outRefundNo === '' || $refundFen <= 0 || $totalFen <= 0) { + throw new \InvalidArgumentException('退款参数不完整'); + } + + $payload = [ + 'out_trade_no' => $outTradeNo, + 'out_refund_no' => $outRefundNo, + 'notify_url' => $notifyUrl, + 'amount' => [ + 'refund' => $refundFen, + 'total' => $totalFen, + 'currency' => 'CNY', + ], + ]; + if ($reason !== '') { + $payload['reason'] = $reason; + } + + return $this->request('POST', '/v3/refund/domestic/refunds', $payload); + } + + /** + * 4.1 按商户退款单号主动查询微信退款状态 + * @param string $outRefundNo + * @return array + */ + public function queryRefundByOutRefundNo($outRefundNo) + { + $this->assertConfigReady(); + $outRefundNo = trim((string)$outRefundNo); + if ($outRefundNo === '') { + throw new \InvalidArgumentException('缺少商户退款单号'); + } + $uri = '/v3/refund/domestic/refunds/' . rawurlencode($outRefundNo); + return $this->request('GET', $uri); + } + + /** + * 5. 新版微信「商家转账」——发起转账(场景 1005:佣金报酬,用户确认收款模式) + * + * @param array $params [ + * 'out_bill_no' => string (必填, 字母数字 1-32位), + * 'openid' => string (必填, 收款师傅 OpenID), + * 'amount' => float (必填, 实际打款净额,单位:元), + * 'remark' => string (选填, 转账备注 <=32字符), + * 'user_name' => string (选填, 收款人实名,>=2000元时必填并自动加密), + * 'job_type' => string (选填, 岗位类型 <=32字符), + * 'reward_desc' => string (选填, 报酬说明 <=32字符), + * 'recv_perception' => string (选填, 默认"劳务报酬"), + * 'notify_url' => string (选填), + * ] + * @return array ['out_bill_no', 'transfer_bill_no', 'create_time', 'state', 'package_info'] + */ + public function createTransferBill(array $params) + { + $this->assertConfigReady(); + + $outBillNo = preg_replace('/[^A-Za-z0-9]/', '', (string)($params['out_bill_no'] ?? '')); + $openid = trim((string)($params['openid'] ?? '')); + $amountFen = (int)round(((float)($params['amount'] ?? 0)) * 100); + $remark = mb_substr(trim((string)($params['remark'] ?? '工单安装维修服务劳务报酬')), 0, 32); + $userName = trim((string)($params['user_name'] ?? '')); + $jobType = mb_substr(trim((string)($params['job_type'] ?? '上门安装维修师傅')), 0, 32); + $rewardDesc = mb_substr(trim((string)($params['reward_desc'] ?? '平台工单安装维修劳务报酬')), 0, 32); + $perception = trim((string)($params['recv_perception'] ?? '劳务报酬')); + $sceneId = trim((string)($params['transfer_scene_id'] ?? '')) ?: $this->transferSceneId; + $notifyUrl = !empty($params['notify_url']) ? (string)$params['notify_url'] : $this->buildNotifyUrl('/api/wxpay/transferNotify'); + + if ($outBillNo === '' || $openid === '' || $amountFen <= 0) { + throw new \InvalidArgumentException('商家转账参数不完整(单号/OpenID/金额)'); + } + + $payload = [ + 'appid' => $this->appId, + 'out_bill_no' => $outBillNo, + 'transfer_scene_id' => $sceneId, + 'openid' => $openid, + 'transfer_amount' => $amountFen, + 'transfer_remark' => $remark, + 'notify_url' => $notifyUrl, + 'user_recv_perception' => $perception, + 'transfer_scene_report_infos' => [ + [ + 'info_type' => '岗位类型', + 'info_content' => $jobType, + ], + [ + 'info_type' => '报酬说明', + 'info_content' => $rewardDesc, + ], + ], + ]; + + $extraHeaders = []; + // 微信官方规定:单笔转账 >= 2000 元(200000 分)必须传加密收款人姓名;小额不传避免 <0.3 元报错 + if ($amountFen >= 200000) { + if ($userName === '') { + throw new \InvalidArgumentException('单笔转账满 2000 元必须提供收款师傅真实姓名'); + } + $payload['user_name'] = $this->encryptSensitive($userName); + $extraHeaders[] = 'Wechatpay-Serial: ' . $this->publicKeyId; + } + + return $this->request('POST', '/v3/fund-app/mch-transfer/transfer-bills', $payload, $extraHeaders); + } + + /** + * 6. 按商户单号查询商家转账单状态 + * @param string $outBillNo + * @return array + */ + public function queryTransferBill($outBillNo) + { + $this->assertConfigReady(); + $outBillNo = preg_replace('/[^A-Za-z0-9]/', '', (string)$outBillNo); + if ($outBillNo === '') { + throw new \InvalidArgumentException('缺少商户转账单号'); + } + $uri = '/v3/fund-app/mch-transfer/transfer-bills/out-bill-no/' . rawurlencode($outBillNo); + return $this->request('GET', $uri); + } + + /** + * 7. 撤销商家转账单(仅在用户确认收款前 WAIT_USER_CONFIRM 可撤销) + * @param string $outBillNo + * @return array ['out_bill_no', 'transfer_bill_no', 'state', 'update_time'] + */ + public function cancelTransferBill($outBillNo) + { + $this->assertConfigReady(); + $outBillNo = preg_replace('/[^A-Za-z0-9]/', '', (string)$outBillNo); + if ($outBillNo === '') { + throw new \InvalidArgumentException('缺少商户转账单号'); + } + $uri = '/v3/fund-app/mch-transfer/transfer-bills/out-bill-no/' . rawurlencode($outBillNo) . '/cancel'; + return $this->request('POST', $uri, null); + } + + /** + * 8. 验证并解密微信支付 V3 异步回调通知(支付 / 退款 / 商家转账通用) + * + * @param string $rawBody 原始 HTTP 请求体 + * @param array $headers HTTP 请求头(大小写不敏感) + * @return array ['event_type' => string, 'data' => array] + */ + public function verifyAndDecryptNotify($rawBody, array $headers) + { + $this->assertConfigReady(); + + $normHeaders = []; + foreach ($headers as $k => $v) { + $normKey = strtolower(str_replace('_', '-', (string)$k)); + if (strpos($normKey, 'http-') === 0) { + $normKey = substr($normKey, 5); + } + $normHeaders[$normKey] = is_array($v) ? (string)reset($v) : (string)$v; + } + + $timestamp = $normHeaders['wechatpay-timestamp'] ?? ''; + $nonce = $normHeaders['wechatpay-nonce'] ?? ''; + $signature = $normHeaders['wechatpay-signature'] ?? ''; + $serial = $normHeaders['wechatpay-serial'] ?? ''; + + if ($timestamp === '' || $nonce === '' || $signature === '' || $serial === '') { + throw new WxPayException('微信回调请求缺少签名头参数(Timestamp/Nonce/Signature/Serial)', 'NOTIFY_HEADER_MISSING', 400); + } + // 防重放:拒绝超过 5 分钟偏差的回调请求 + if (abs(time() - (int)$timestamp) > 300) { + throw new WxPayException('微信回调请求时间戳已过期(与服务器时间偏差超过5分钟),请检查服务器时钟同步', 'NOTIFY_EXPIRED', 400); + } + + if (!$this->verifySignature($timestamp, $nonce, (string)$rawBody, $signature, $serial)) { + throw new WxPayException('微信支付回调签名验证未通过,请核对微信支付公钥文件与公钥ID配置', 'NOTIFY_SIGN_FAIL', 401); + } + + $outer = json_decode((string)$rawBody, true); + if (!is_array($outer) || empty($outer['resource']) || !is_array($outer['resource'])) { + throw new WxPayException('微信回调报文结构非法,缺少加密资源体', 'NOTIFY_INVALID_BODY', 400); + } + + $decrypted = $this->decryptResource($outer['resource']); + + // 核验回调主体中的商户号与 AppID 是否属于本系统配置,严防跨商户串号 + $cbMchId = trim((string)($decrypted['mchid'] ?? '')); + if ($cbMchId !== '' && $cbMchId !== $this->mchId) { + throw new WxPayException('回调报文中商户号(' . $cbMchId . ')与本系统配置(' . $this->mchId . ')不匹配', 'NOTIFY_MCHID_MISMATCH', 403); + } + $cbAppId = trim((string)($decrypted['appid'] ?? '')); + if ($cbAppId !== '' && $cbAppId !== $this->appId) { + throw new WxPayException('回调报文中 AppID(' . $cbAppId . ')与本系统配置(' . $this->appId . ')不匹配', 'NOTIFY_APPID_MISMATCH', 403); + } + + return [ + 'id' => (string)($outer['id'] ?? ''), + 'event_type' => (string)($outer['event_type'] ?? ''), + 'summary' => (string)($outer['summary'] ?? ''), + 'data' => $decrypted, + ]; + } + + /** + * 解密微信 V3 AEAD_AES_256_GCM 密文 + * @param array $resource ['algorithm', 'ciphertext', 'nonce', 'associated_data'] + * @return array + */ + public function decryptResource(array $resource) + { + if (strlen($this->keyV3) !== 32) { + throw new WxPayException('微信 APIv3 密钥长度必须为 32 位,请检查 server/.env 的 [wechat] key_v3', 'CONFIG_KEY_V3_INVALID', 500); + } + $ciphertextB64 = (string)($resource['ciphertext'] ?? ''); + $nonce = (string)($resource['nonce'] ?? ''); + $aad = (string)($resource['associated_data'] ?? ''); + + $raw = base64_decode($ciphertextB64, true); + if ($raw === false || strlen($raw) <= 16) { + throw new WxPayException('微信回调密文 Base64 解码失败或长度异常', 'NOTIFY_CIPHER_INVALID', 400); + } + + $cipher = substr($raw, 0, -16); + $tag = substr($raw, -16); + + $plain = openssl_decrypt($cipher, 'aes-256-gcm', $this->keyV3, OPENSSL_RAW_DATA, $nonce, $tag, $aad); + if ($plain === false) { + throw new WxPayException('微信回调报文 AEAD_AES_256_GCM 解密失败,请确认 server/.env 的 key_v3 与微信商户平台设置的 APIv3 密钥完全一致', 'NOTIFY_DECRYPT_FAIL', 500); + } + + $data = json_decode($plain, true); + if (!is_array($data)) { + throw new WxPayException('微信回调报文解密后 JSON 解析失败', 'NOTIFY_JSON_INVALID', 400); + } + return $data; + } + + /** + * 使用微信支付公钥 OAEP 加密敏感信息(如转账收款人真实姓名) + */ + public function encryptSensitive($plainText) + { + $pubPem = @file_get_contents($this->publicKeyPath); + if (!$pubPem) { + throw new \RuntimeException('无法读取微信支付公钥文件'); + } + $pubKey = openssl_pkey_get_public($pubPem); + if (!$pubKey) { + throw new \RuntimeException('解析微信支付公钥失败'); + } + $encrypted = ''; + $ok = openssl_public_encrypt((string)$plainText, $encrypted, $pubKey, OPENSSL_PKCS1_OAEP_PADDING); + if (!$ok) { + throw new \RuntimeException('敏感字段 RSA-OAEP 加密失败'); + } + return base64_encode($encrypted); + } + + /** + * 底层微信 APIv3 HTTP 请求(自动签名 + 成功响应公钥验签 + 错误友好化) + * + * @param string $method GET | POST + * @param string $canonicalUri 以 /v3/... 开头的路径(含 query) + * @param array|null $payload 请求体数组;null 表示空请求体 + * @param array $extraHeaders 附加请求头 + * @return array + */ + protected function request($method, $canonicalUri, $payload = null, array $extraHeaders = []) + { + $method = strtoupper(trim((string)$method)); + $timestamp = (string)time(); + $nonceStr = $this->generateNonceStr(); + + $bodyStr = ''; + if ($method !== 'GET' && $payload !== null) { + $bodyStr = json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + } + + $serialNo = $this->getMerchantSerialNo(); + $signMsg = $method . "\n" . $canonicalUri . "\n" . $timestamp . "\n" . $nonceStr . "\n" . $bodyStr . "\n"; + $signature = $this->signMessage($signMsg); + + $authHeader = sprintf( + 'Authorization: WECHATPAY2-SHA256-RSA2048 mchid="%s",nonce_str="%s",timestamp="%s",serial_no="%s",signature="%s"', + $this->mchId, + $nonceStr, + $timestamp, + $serialNo, + $signature + ); + + $defaultSerialHeader = []; + if ($this->publicKeyId !== '') { + $hasSerial = false; + foreach ($extraHeaders as $eh) { + if (stripos((string)$eh, 'Wechatpay-Serial:') === 0) { + $hasSerial = true; + break; + } + } + if (!$hasSerial) { + $defaultSerialHeader[] = 'Wechatpay-Serial: ' . $this->publicKeyId; + } + } + + $headers = array_merge([ + $authHeader, + 'Accept: application/json', + 'Content-Type: application/json', + 'User-Agent: YJYF-WxPayV3-Client/1.0 PHP/' . PHP_VERSION, + ], $defaultSerialHeader, $extraHeaders); + + $url = self::API_BASE . $canonicalUri; + $respHeaders = []; + + $ch = curl_init(); + curl_setopt($ch, CURLOPT_URL, $url); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_TIMEOUT, 15); + curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 6); + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true); + curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2); + curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); + curl_setopt($ch, CURLOPT_HEADERFUNCTION, function ($curl, $headerLine) use (&$respHeaders) { + $len = strlen($headerLine); + $parts = explode(':', $headerLine, 2); + if (count($parts) === 2) { + $respHeaders[strtolower(trim($parts[0]))] = trim($parts[1]); + } + return $len; + }); + + if ($method === 'POST') { + curl_setopt($ch, CURLOPT_POST, true); + curl_setopt($ch, CURLOPT_POSTFIELDS, $bodyStr); + } elseif ($method !== 'GET') { + curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method); + if ($bodyStr !== '') { + curl_setopt($ch, CURLOPT_POSTFIELDS, $bodyStr); + } + } + + $respBody = curl_exec($ch); + + if ($respBody === false) { + $err = (string)curl_error($ch); + $eno = (int)curl_errno($ch); + curl_close($ch); + Log::error("[WxPayV3] 网络/传输异常 (cURL {$eno}): " . $this->maskSensitiveLog($err) . " | URI={$canonicalUri}"); + $diag = $this->translateCurlError($eno, $err, $canonicalUri); + throw new WxPayException( + $diag['message'], + 'CURL_' . $eno, + 0, + $eno, + $diag['uncertain'], + $this->maskSensitiveLog($err) + ); + } + + $httpCode = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + + $respBodyStr = (string)$respBody; + + // HTTP 2xx:必须校验微信支付公钥签名(204 No Content 无响应体时也同样验签空串) + if ($httpCode >= 200 && $httpCode < 300) { + $wxTs = $respHeaders['wechatpay-timestamp'] ?? ''; + $wxNonce = $respHeaders['wechatpay-nonce'] ?? ''; + $wxSign = $respHeaders['wechatpay-signature'] ?? ''; + $wxSerial = $respHeaders['wechatpay-serial'] ?? ''; + + if ($wxTs === '' || $wxSign === '' || !$this->verifySignature($wxTs, $wxNonce, $respBodyStr, $wxSign, $wxSerial)) { + Log::error("[WxPayV3] 响应验签失败 | HTTP={$httpCode} | Serial={$wxSerial} | URI={$canonicalUri}"); + throw new WxPayException( + '微信支付网关响应签名校验未通过(应答公钥 ID:' . ($wxSerial ?: '空') . '),请检查微信支付公钥文件与公钥 ID 配置是否一致', + 'VERIFY_SIGN_FAIL', + $httpCode, + 0, + false + ); + } + + if ($httpCode === 204 || trim($respBodyStr) === '') { + return []; + } + $decoded = json_decode($respBodyStr, true); + if (!is_array($decoded)) { + Log::error("[WxPayV3] 响应JSON解析失败 | HTTP={$httpCode} | URI={$canonicalUri}"); + throw new WxPayException('微信支付网关返回报文格式异常(非合法 JSON)', 'INVALID_RESPONSE_JSON', $httpCode, 0, true); + } + return $decoded; + } + + // 非 2xx:解析微信网关错误码并映射清晰中文诊断原因 + $errJson = json_decode($respBodyStr, true); + $wxCode = is_array($errJson) && isset($errJson['code']) ? strtoupper(trim((string)$errJson['code'])) : ('HTTP_' . $httpCode); + $wxMsg = is_array($errJson) && isset($errJson['message']) ? trim((string)$errJson['message']) : ('微信网关返回 HTTP ' . $httpCode); + $detailMsg = ''; + if (is_array($errJson) && isset($errJson['detail']) && is_array($errJson['detail'])) { + $dField = (string)($errJson['detail']['field'] ?? ''); + $dIssue = (string)($errJson['detail']['issue'] ?? ''); + $detailMsg = trim($dField . ' ' . $dIssue); + } + + Log::error("[WxPayV3] 网关业务报错 | HTTP={$httpCode} | code={$wxCode} | msg=" . $this->maskSensitiveLog($wxMsg) . ($detailMsg !== '' ? " | detail={$detailMsg}" : '') . " | URI={$canonicalUri}"); + + $friendly = $this->translateWxError($wxCode, $wxMsg, $detailMsg, $httpCode, $canonicalUri); + $isUncertain = ($httpCode >= 500 || in_array($wxCode, ['SYSTEMERROR', 'SYSTEM_ERROR', 'BIZ_ERR_NEED_RETRY', 'BANKERROR'], true)); + + throw new WxPayException( + $friendly, + $wxCode, + $httpCode, + 0, + $isUncertain, + $this->maskSensitiveLog($wxMsg . ($detailMsg !== '' ? ' | ' . $detailMsg : '')) + ); + } + + /** + * 验证微信支付公钥签名 + */ + public function verifySignature($timestamp, $nonce, $body, $signatureB64, $serial) + { + // 若配置了微信支付公钥 ID,则校验序列号一致(兼容未来平台证书过渡期前缀检查) + if ($this->publicKeyId !== '' && strpos((string)$serial, 'PUB_KEY_ID_') === 0 && $serial !== $this->publicKeyId) { + Log::warning("[WxPayV3] 公钥ID不匹配: 期望 {$this->publicKeyId}, 实际 {$serial}"); + return false; + } + + $pubPem = @file_get_contents($this->publicKeyPath); + if (!$pubPem) { + return false; + } + $pubKey = openssl_pkey_get_public($pubPem); + if (!$pubKey) { + return false; + } + + $sigBin = base64_decode((string)$signatureB64, true); + if ($sigBin === false) { + return false; + } + + $message = (string)$timestamp . "\n" . (string)$nonce . "\n" . (string)$body . "\n"; + $verified = openssl_verify($message, $sigBin, $pubKey, OPENSSL_ALGO_SHA256); + return $verified === 1; + } + + /** + * 使用商户私钥生成 SHA256-RSA2048 签名(Base64) + */ + protected function signMessage($message) + { + $keyPem = @file_get_contents($this->certKeyPath); + if (!$keyPem) { + throw new WxPayException('商户 API 私钥文件不存在或无读取权限(' . basename((string)$this->certKeyPath) . ')', 'CONFIG_CERT_KEY_MISSING'); + } + $privKey = openssl_pkey_get_private($keyPem); + if (!$privKey) { + throw new WxPayException('解析商户 API 私钥失败,请确认 apiclient_key.pem 文件格式正确且未损坏', 'CONFIG_CERT_KEY_INVALID'); + } + $signature = ''; + $ok = openssl_sign((string)$message, $signature, $privKey, OPENSSL_ALGO_SHA256); + if (!$ok) { + throw new WxPayException('使用商户私钥生成 SHA256-RSA2048 签名失败', 'SIGN_GEN_FAIL'); + } + return base64_encode($signature); + } + + /** + * 从商户证书提取十六进制序列号 + */ + protected function getMerchantSerialNo() + { + if ($this->merchantSerialNo !== '') { + return $this->merchantSerialNo; + } + $certPem = @file_get_contents($this->certClientPath); + if (!$certPem) { + throw new WxPayException('商户 API 证书文件不存在(' . basename((string)$this->certClientPath) . ')', 'CONFIG_CERT_CLIENT_MISSING'); + } + $parsed = openssl_x509_parse($certPem); + if (!is_array($parsed)) { + throw new WxPayException('解析商户 API 证书失败,请检查 apiclient_cert.pem 是否有效', 'CONFIG_CERT_CLIENT_INVALID'); + } + if (!empty($parsed['validTo_time_t']) && (int)$parsed['validTo_time_t'] < time()) { + throw new WxPayException( + '商户 API 证书已于 ' . date('Y-m-d', (int)$parsed['validTo_time_t']) . ' 过期,请登录微信商户平台重新下载并替换 apiclient_cert.pem 与 apiclient_key.pem', + 'CONFIG_CERT_CLIENT_EXPIRED' + ); + } + if (!empty($parsed['serialNumberHex'])) { + $this->merchantSerialNo = strtoupper((string)$parsed['serialNumberHex']); + return $this->merchantSerialNo; + } + if (!empty($parsed['serialNumber']) && function_exists('bcdiv')) { + $dec = (string)$parsed['serialNumber']; + $hex = ''; + while (bccomp($dec, '0') > 0) { + $rem = (int)bcmod($dec, '16'); + $hex = dechex($rem) . $hex; + $dec = bcdiv($dec, '16', 0); + } + $this->merchantSerialNo = strtoupper($hex); + return $this->merchantSerialNo; + } + throw new WxPayException('无法提取商户 API 证书序列号,请检查证书内容是否完整', 'CONFIG_CERT_SERIAL_EMPTY'); + } + + /** + * 严格校验全部微信支付核心参数与证书文件(不做任何缺省回退) + */ + protected function assertConfigReady() + { + $missing = []; + if ($this->appId === '') { + $missing[] = '小程序 AppID(wechat.appid)'; + } + if ($this->mchId === '') { + $missing[] = '微信商户号(wechat.mch_id)'; + } + if (strlen($this->keyV3) !== 32) { + $missing[] = '32位 APIv3 密钥(wechat.key_v3)'; + } + if ($this->publicKeyId === '') { + $missing[] = '微信支付公钥 ID(wechat.public_key_id)'; + } + if (!empty($missing)) { + throw new WxPayException( + '微信支付配置缺失或不完整:' . implode('、', $missing) . ',请检查 server/.env 的 [wechat] 配置段', + 'CONFIG_INCOMPLETE' + ); + } + + $this->assertValidNotifyDomain($this->notifyDomain); + + if (!is_file($this->certClientPath) || !is_readable($this->certClientPath)) { + throw new WxPayException('微信商户 API 证书文件缺失或不可读(' . $this->certClientPath . ')', 'CONFIG_CERT_CLIENT_MISSING'); + } + if (!is_file($this->certKeyPath) || !is_readable($this->certKeyPath)) { + throw new WxPayException('微信商户 API 私钥文件缺失或不可读(' . $this->certKeyPath . ')', 'CONFIG_CERT_KEY_MISSING'); + } + if (!is_file($this->publicKeyPath) || !is_readable($this->publicKeyPath)) { + throw new WxPayException('微信支付公钥文件缺失或不可读(' . $this->publicKeyPath . ')', 'CONFIG_PUB_KEY_MISSING'); + } + + $pubPem = @file_get_contents($this->publicKeyPath); + if (!$pubPem || !openssl_pkey_get_public($pubPem)) { + throw new WxPayException('微信支付公钥文件无法解析,请确认 public_key.pem 是否为微信商户平台下载的有效 PEM 公钥', 'CONFIG_PUB_KEY_INVALID'); + } + $this->getMerchantSerialNo(); + } + + /** + * 从任意异常中统一提取结构化失败信息(供数据库持久化与管理员查看) + * + * @param \Throwable $e + * @return array ['code' => string, 'reason' => string, 'uncertain' => bool] + */ + public static function extractFailInfo(\Throwable $e) + { + if ($e instanceof WxPayException) { + return [ + 'code' => $e->getWxCode() ?: 'WXPAY_ERR', + 'reason' => mb_substr(trim((string)$e->getMessage()), 0, 480), + 'uncertain' => $e->isUncertain(), + ]; + } + $msg = trim((string)$e->getMessage()); + if ($msg === '') { + $msg = '发生未知系统异常,请查看服务端系统日志'; + } + return [ + 'code' => $e instanceof \InvalidArgumentException ? 'INVALID_PARAM' : 'SYSTEM_ERR', + 'reason' => mb_substr($msg, 0, 480), + 'uncertain' => false, + ]; + } + + /** + * 将 cURL 网络传输错误码转换成管理员可读的中文原因及排查建议 + */ + protected function translateCurlError($eno, $err, $uri = '') + { + $eno = (int)$eno; + $err = trim((string)$err); + $isTransfer = strpos((string)$uri, 'transfer-bills') !== false; + + switch ($eno) { + case 28: // CURLE_OPERATION_TIMEDOUT + $hint = $isTransfer + ? '请求微信商家转账接口超时(cURL 28):微信网关可能已受理打款请求。为严防重复出款,系统已保留当前商户转账单号,请先执行「同步微信状态」确认真实结果' + : '请求微信支付网关超时(cURL 28):微信网关可能正在处理中。系统已保留当前交易单号,请先查询或同步微信最新状态后再操作'; + return ['message' => $hint, 'uncertain' => true]; + case 6: // CURLE_COULDNT_RESOLVE_HOST + return [ + 'message' => '无法解析微信支付域名 api.mch.weixin.qq.com(cURL 6),请检查服务器 DNS 配置与外网连通性', + 'uncertain' => false, + ]; + case 7: // CURLE_COULDNT_CONNECT + return [ + 'message' => '无法连接微信支付网关 443 端口(cURL 7),请检查服务器防火墙出站策略或机房网络是否阻断微信 API', + 'uncertain' => false, + ]; + case 35: // CURLE_SSL_CONNECT_ERROR + return [ + 'message' => '与微信支付网关建立 TLS/SSL 安全连接失败(cURL 35),请检查服务器 OpenSSL 版本(需支持 TLSv1.2+)或网络代理', + 'uncertain' => false, + ]; + case 58: + case 59: + return [ + 'message' => '本地微信商户 API 证书或私钥加载失败(cURL ' . $eno . '),请检查 apiclient_cert.pem 与 apiclient_key.pem 是否匹配或权限不足', + 'uncertain' => false, + ]; + case 60: // CURLE_SSL_CACERT + case 77: + return [ + 'message' => '服务器 HTTPS 根证书校验失败(cURL ' . $eno . '):当前 PHP 环境未正确配置 curl.cainfo / openssl.cafile 信任链,系统已拒绝不安全请求,请运维配置根 CA 证书文件', + 'uncertain' => false, + ]; + case 52: // CURLE_GOT_NOTHING + case 56: // CURLE_RECV_ERROR + return [ + 'message' => '微信支付网关连接异常中断,未收到完整应答(cURL ' . $eno . '),请先同步微信交易/转账状态核实后再决定后续操作', + 'uncertain' => true, + ]; + default: + return [ + 'message' => '连接微信支付网关通信失败(cURL ' . $eno . '),请检查服务器网络与防火墙策略(原始诊断:' . $this->maskSensitiveLog($err) . ')', + 'uncertain' => false, + ]; + } + } + + /** + * 映射全部微信 APIv3 网关错误码与英文细节为管理员直接可读的简体中文原因及处置建议 + */ + protected function translateWxError($code, $rawMsg, $detailMsg = '', $httpCode = 0, $uri = '') + { + $code = strtoupper(trim((string)$code)); + $rawMsg = trim((string)$rawMsg); + $detailMsg = trim((string)$detailMsg); + $combined = strtolower($rawMsg . ' ' . $detailMsg); + + $detailZh = $this->translateEnglishDetailHint($combined, $uri); + + $map = [ + 'NOT_ENOUGH' => '微信商户运营账户可用余额不足,无法完成出款或退款。请登录微信商户平台【资金管理】充值运营账户或确保基本户可退资金充足后再试', + 'NO_AUTH' => '微信商户接口权限不足,或当前服务器出口 IP 未加入微信商户平台安全 IP 白名单', + 'OPENID_INVALID' => '付款或收款用户的微信 OpenID 无效,或与当前小程序 AppID 不属于同一主体,请提醒用户在微信小程序内重新登录后再试', + 'USER_NOT_EXIST' => '微信账号不存在或未完成微信绑定', + 'ORDERPAID' => '该商户订单已在微信完成支付,请勿重复支付', + 'ORDERCLOSED' => '该微信支付单已关闭或已超时失效,请重新发起支付', + 'ORDERNOTEXIST' => '微信侧未查询到该笔支付单(尚未创建或已失效)', + 'ORDER_NOT_EXIST' => '微信侧未查询到该笔支付单(尚未创建或已失效)', + 'NOT_FOUND' => '微信侧未找到对应的商家转账单或交易记录,可能该单此前发起失败未受理', + 'RESOURCE_NOT_EXISTS' => '微信侧未找到对应单据资源,请核实商户单号是否正确或单据是否从未创建成功', + 'REFUND_NOT_EXISTS' => '微信侧未找到该笔退款单记录', + 'INVALID_TRANSACTIONID' => '微信交易单号无效或不属于当前商户号', + 'OUT_TRADE_NO_USED' => '商户订单号已在微信重复使用且参数不一致,请重新生成新支付单号后再下单', + 'ALREADY_EXISTS' => '该商户转账单号已存在但金额/收款人等参数与原单不一致,请核实原单状态切勿盲目修改参数重发', + 'QUOTA_EXCEED' => '超出微信商户转账单笔、单日或单用户月度限额配置,请登录微信商户平台【商家转账-产品设置】核实并调整转账额度', + 'SEND_MONEY_LIMIT' => '超出微信商户单笔或单日转账金额上限,请核对微信商户平台额度配置(如截图中的单笔200元限制)与平台提现规则是否一致', + 'SEND_NUM_LIMIT' => '超出今日向同一微信用户发起转账的次数上限,请明日再试', + 'TRANSFER_QUOTA_EXCEED' => '超出微信商家转账额度上限,请检查商户后台单日/单笔转账额度或用户收款限额', + 'DAY_RECEIVED_QUOTA_EXCEED' => '收款用户今日累计接收商家转账已达微信单日收款额度上限', + 'MONTH_RECEIVED_QUOTA_EXCEED' => '收款用户本月累计接收商家转账已达微信单月收款额度上限', + 'DAY_RECEIVED_COUNT_EXCEED' => '收款用户今日接收商家转账次数已达上限', + 'TRANSFER_RISK' => '本次转账触发微信支付安全风控拦截,请核实收款人微信实名状态及交易真实性后再试', + 'RULE_LIMIT' => '转账或交易触发微信商户风控规则限制,请登录微信商户平台站内信或违约记录核查限制原因', + 'V2_ACCOUNT_SIMPLE_BAN' => '收款人微信账户未完成实名认证(尚未绑定本人银行卡),无法接收商家转账', + 'ACCOUNTERROR' => '收款人微信支付账户状态异常(可能被限制收款或注销),请提醒收款师傅核实本人微信账户', + 'USER_ACCOUNT_ABNORMAL' => '收款人或付款人微信支付账户存在异常风险或已被限制交易', + 'PAYEE_ACCOUNT_ABNORMAL' => '收款师傅微信账户状态异常,无法收款', + 'PAYER_ACCOUNT_ABNORMAL' => '平台微信商户付款账户状态异常或受限,请登录微信商户平台排查账户通知', + 'REAL_NAME_CHECK_FAIL' => '收款师傅在平台登记的真实姓名与本人微信实名信息不一致,请师傅核对真实姓名后再试', + 'NAME_NOT_CORRECT' => '收款师傅真实姓名与微信实名不匹配(单笔>=2000元强制校验证实姓名)', + 'PRODUCT_AUTH_CHECK_FAIL' => '当前商户号尚未开通对应产品权限,或「商家转账场景 1005 佣金报酬」尚未生效/未授权', + 'APPID_MCHID_NOT_MATCH' => '微信小程序 AppID 与微信支付商户号尚未建立绑定授权关系,请前往微信商户平台【AppID账号管理】完成绑定确认', + 'MCH_NOT_EXISTS' => '配置的微信支付商户号不存在,请核对 server/.env 中的 wechat.mch_id', + 'SIGN_ERROR' => '微信支付请求签名校验失败,请检查商户私钥文件 apiclient_key.pem 与证书序列号、以及商户号是否配套一致', + 'FREQUENCY_LIMITED' => '当前请求微信支付接口过于频繁触发限流保护,请等待数秒后重试', + 'RATELIMIT_EXCEEDED' => '触发微信网关并发限流,请稍后再试', + 'SYSTEMERROR' => '微信支付系统网关繁忙或内部临时错误,请勿立即切换单号重复打款,先同步核实单据状态后再重试', + 'SYSTEM_ERROR' => '微信支付系统网关繁忙或内部临时错误,请勿立即切换单号重复打款,先同步核实单据状态后再重试', + 'BANKERROR' => '银行通道临时异常,请稍后同步查看最终状态', + 'TRADE_ERROR' => '微信交易处理失败,请提醒用户确认微信零钱或银行卡余额与状态后再重试', + 'BIZ_ERR_NEED_RETRY' => '微信退款/转账业务系统临时繁忙,请使用原单号稍后同步或重试', + 'PARAM_ERROR' => '请求微信网关的参数不符合微信校验规范', + 'INVALID_REQUEST' => '微信支付网关拒绝当前请求(配置、白名单、状态或证书不满足前置要求)', + ]; + + $baseZh = isset($map[$code]) ? $map[$code] : ''; + if ($baseZh === '') { + if ($detailZh !== '') { + $baseZh = '微信网关拒绝请求:' . $detailZh; + } elseif ($httpCode === 401) { + $baseZh = '微信支付身份认证或签名校验未通过(HTTP 401),请检查商户号、证书序列号及私钥配置'; + } elseif ($httpCode === 403) { + $baseZh = '微信支付拒绝访问(HTTP 403):请检查产品权限、转账场景授权及服务器出口 IP 白名单'; + } elseif ($httpCode === 404) { + $baseZh = '微信网关未找到对应资源或接口路径(HTTP 404)'; + } elseif ($httpCode >= 500) { + $baseZh = '微信支付网关服务器内部异常(HTTP ' . $httpCode . '),请先同步确认单据真实状态后再操作'; + } else { + $baseZh = '微信支付接口返回失败(' . ($detailZh ?: '请根据商户平台单据状态与日志核查') . ')'; + } + } elseif ($detailZh !== '' && mb_strpos($baseZh, $detailZh) === false) { + $baseZh .= ';具体原因:' . $detailZh; + } + + return $baseZh . '(微信错误码:' . $code . ')'; + } + + /** + * 将微信 V3 英文错误 message / detail 特征精准翻译成管理员可看懂的简体中文 + */ + protected function translateEnglishDetailHint($combinedLower, $uri = '') + { + if ($combinedLower === '') { + return ''; + } + + $hints = []; + if (strpos($combinedLower, 'ip') !== false && (strpos($combinedLower, 'white') !== false || strpos($combinedLower, 'allow') !== false || strpos($combinedLower, 'forbid') !== false || strpos($combinedLower, 'not in') !== false || strpos($combinedLower, 'whitelist') !== false || strpos($combinedLower, '白名单') !== false)) { + $hints[] = '当前服务器出口 IP 未加入微信商户平台「商家转账/API 接口安全 IP 白名单」'; + } + if (strpos($combinedLower, 'openid') !== false && (strpos($combinedLower, 'appid') !== false || strpos($combinedLower, 'match') !== false || strpos($combinedLower, 'invalid') !== false || strpos($combinedLower, '不匹配') !== false)) { + $hints[] = '用户 OpenID 与当前小程序 AppID 不匹配或 OpenID 格式错误,请用户重新微信授权登录'; + } + if (strpos($combinedLower, 'not enough') !== false || strpos($combinedLower, 'insufficient') !== false || strpos($combinedLower, '余额不足') !== false) { + $hints[] = '微信商户可用或可出款余额不足,请及时充值运营账户'; + } + if (strpos($combinedLower, 'scene') !== false || strpos($combinedLower, 'report_info') !== false || strpos($combinedLower, 'transfer_scene') !== false) { + $hints[] = '商家转账场景 ID(应为已报备开通的 1005)或报备字段(岗位类型/报酬说明)与商户后台配置不一致'; + } + if (strpos($combinedLower, 'serial') !== false || strpos($combinedLower, 'certificate') !== false || strpos($combinedLower, 'public key') !== false) { + $hints[] = '商户 API 证书序列号或微信支付公钥 ID(PUB_KEY_ID)与商户后台不一致或证书已过期'; + } + if (strpos($combinedLower, 'sign') !== false && (strpos($combinedLower, 'fail') !== false || strpos($combinedLower, 'error') !== false || strpos($combinedLower, 'invalid') !== false)) { + $hints[] = 'APIv3 请求签名校验未通过,请核对 apiclient_key.pem 私钥与商户号'; + } + if (strpos($combinedLower, 'timestamp') !== false || strpos($combinedLower, 'expired') !== false) { + $hints[] = '服务器系统时间与微信标准时间偏差超过 5 分钟,请校准服务器 NTP 时间'; + } + if (strpos($combinedLower, 'user_name') !== false || strpos($combinedLower, 'decrypt') !== false || strpos($combinedLower, 'encrypt') !== false) { + $hints[] = '大额转账收款人姓名 RSA 加密/解密失败,请核对微信支付公钥 public_key.pem'; + } + if (strpos($combinedLower, 'exceed') !== false || strpos($combinedLower, 'limit') !== false || strpos($combinedLower, 'quota') !== false || strpos($combinedLower, 'amount') !== false) { + $hints[] = '交易金额低于最低要求(如低于 0.10 元或 0.30 元限制)或超出微信单笔/单日转账与收款限额'; + } + if (strpos($combinedLower, 'mchid') !== false && (strpos($combinedLower, 'appid') !== false || strpos($combinedLower, 'relation') !== false || strpos($combinedLower, 'bind') !== false)) { + $hints[] = '当前小程序 AppID 与微信商户号未完成绑定授权'; + } + if (strpos($combinedLower, 'cancel') !== false && strpos($combinedLower, 'confirm') !== false) { + $hints[] = '该笔转账当前状态已不允许撤销(用户可能已完成收款或已过期关单)'; + } + if (strpos($combinedLower, 'refund') !== false && strpos($combinedLower, 'more than') !== false) { + $hints[] = '申请退款金额超过原支付单剩余可退余额'; + } + if (strpos($combinedLower, 'notify_url') !== false) { + $hints[] = '回调通知地址 notify_url 格式不合法或非公网可访问 HTTPS 域名'; + } + if (strpos($combinedLower, 'permission') !== false || strpos($combinedLower, 'product') !== false || strpos($combinedLower, 'auth') !== false) { + $hints[] = '商户号未开通相关产品能力或未授权接口权限'; + } + if (empty($hints) && preg_match('/[\x{4e00}-\x{9fa5}]/u', $combinedLower)) { + // 微信原生已返回中文提示时可直接提取保留 + return $combinedLower; + } + if (empty($hints)) { + $sceneLabel = strpos((string)$uri, 'transfer') !== false ? '商家转账接口' : (strpos((string)$uri, 'refund') !== false ? '微信退款接口' : '微信支付接口'); + return '调用' . $sceneLabel . '未通过微信校验,请结合错误码在微信商户平台核查'; + } + + return implode(';', array_unique($hints)); + } + + /** + * 日志敏感信息脱敏 + */ + protected function maskSensitiveLog($text) + { + $text = (string)$text; + // 脱敏可能包含的 11 位手机号与 18 位身份证号 + $text = preg_replace('/(?check('general/config/edit')) { return false; } - $config = []; - $configList = self::all(); - foreach ($configList as $k => $v) { - $value = $v->toArray(); - if (in_array($value['type'], ['selects', 'checkbox', 'images', 'files'])) { - $value['value'] = explode(',', $value['value']); - } - if ($value['type'] == 'array') { - $value['value'] = (array)json_decode($value['value'], true); - } - $config[$value['name']] = $value['value']; - } - file_put_contents( - CONF_PATH . 'extra' . DS . 'site.php', - ' '优匠优服', 'beian' => '', 'cdnurl' => '', - 'version' => '1.0.51', + 'version' => '1.0.58', 'timezone' => 'Asia/Shanghai', 'forbiddenip' => '', 'languages' => @@ -23,9 +23,9 @@ return array ( 'configgroup' => array ( 'basic' => '基础配置', + 'user' => '会员配置', 'email' => '邮箱配置', 'dictionary' => '字典配置', - 'user' => '会员配置', ), 'mail_type' => '1', 'mail_smtp_host' => 'smtp.qq.com', @@ -48,4 +48,11 @@ return array ( ), 'home_assure' => '平台认证,先行赔付,极速上门,售后无忧', 'home_tutorial' => '下单找师傅教程|3 分钟看懂,下单不踩坑', + 'review_tags' => '准时,专业,态度好,收费透明,干净利落,值得推荐', + 'order_source_options' => '淘宝/天猫,京东,拼多多,抖音电商,线下门店,其他渠道', + 'home_price_ranges' => '[{"label":"80元以下","value":"under_80"},{"label":"80-120元","value":"80_120"},{"label":"120元以上","value":"120_up"}]', + 'auto_quote_hours_options' => '6,12,24,48', + 'tianditu_key' => '55aee666be14cd0100874d6faf3c963c', + 'tencent_secret_id' => 'AKIDPjrlKq8C2e2CBzax97tRlqiS0SqFRXcX', + 'tencent_secret_key' => 'kHagZHn6C5nNly1tYzCC1cz96rPAzvrP', ); diff --git a/server/application/index/controller/User.php b/server/application/index/controller/User.php index 73df23d..8123db4 100755 --- a/server/application/index/controller/User.php +++ b/server/application/index/controller/User.php @@ -5,7 +5,6 @@ namespace app\index\controller; use addons\wechat\model\WechatCaptcha; use app\common\controller\Frontend; use app\common\library\Ems; -use app\common\library\Sms; use app\common\model\Attachment; use think\Config; use think\Cookie; @@ -104,7 +103,7 @@ class User extends Frontend $captchaType = config("fastadmin.user_register_captcha"); if ($captchaType) { if ($captchaType == 'mobile') { - $captchaResult = Sms::check($mobile, $captcha, 'register'); + $this->error('短信验证码通道已移除,请切换其他注册校验方式'); } elseif ($captchaType == 'email') { $captchaResult = Ems::check($email, $captcha, 'register'); } elseif ($captchaType == 'wechat') { diff --git a/server/application/route.php b/server/application/route.php index 49ff554..65fe931 100755 --- a/server/application/route.php +++ b/server/application/route.php @@ -30,6 +30,12 @@ Route::rule('service.type', 'admin/service.type/index'); Route::rule('service.addon/:action', 'admin/service.addon/:action'); Route::rule('service.addon', 'admin/service.addon/index'); +Route::rule('service/gallery/:action', 'admin/service.gallery/:action'); +Route::rule('service/gallery', 'admin/service.gallery/index'); + +Route::rule('service.gallery/:action', 'admin/service.gallery/:action'); +Route::rule('service.gallery', 'admin/service.gallery/index'); + return [ //别名配置,别名只能是映射到控制器且访问时必须加上请求的方法 '__alias__' => [ diff --git a/server/public/assets/js/backend/agreement.js b/server/public/assets/js/backend/agreement.js new file mode 100644 index 0000000..d057f11 --- /dev/null +++ b/server/public/assets/js/backend/agreement.js @@ -0,0 +1,347 @@ +define(['jquery', 'bootstrap', 'backend', 'form'], function ($, undefined, Backend, Form) { + + function parseInitialAgreements() { + var el = document.getElementById('agreements-initial-data'); + if (!el) return []; + try { + var list = JSON.parse(el.textContent || '[]'); + return Array.isArray(list) ? list : []; + } catch (e) { + return []; + } + } + + /** + * 预览区轻量前端 XSS 清理(服务端保存时还会执行严格的 DOM 白名单清洗) + */ + function cleanPreviewHtml(html) { + if (!html) return ''; + var box = document.createElement('div'); + box.innerHTML = String(html); + var bad = box.querySelectorAll('script,style,iframe,object,embed,form,meta,link,svg'); + for (var i = 0; i < bad.length; i++) { + if (bad[i].parentNode) bad[i].parentNode.removeChild(bad[i]); + } + var all = box.querySelectorAll('*'); + for (var j = 0; j < all.length; j++) { + var attrs = all[j].attributes; + for (var k = attrs.length - 1; k >= 0; k--) { + var name = attrs[k].name.toLowerCase(); + var val = attrs[k].value || ''; + if (name.indexOf('on') === 0 || (name === 'href' && /^\s*(javascript|vbscript|data):/i.test(val))) { + all[j].removeAttribute(attrs[k].name); + } + } + } + return box.innerHTML; + } + + var Controller = { + index: function () { + var items = parseInitialAgreements(); + var map = {}; + var order = []; + for (var i = 0; i < items.length; i++) { + map[items[i].code] = items[i]; + order.push(items[i].code); + } + if (!order.length) return; + + var currentCode = order[0]; + var isSourceMode = false; + var isSubmitting = false; + var previewMode = 'normal'; + + try { + document.execCommand('defaultParagraphSeparator', false, 'p'); + } catch (e) {} + + var $form = $('#agreement-form'); + var $visual = $('#ag-visual-editor'); + var $source = $('#ag-source-editor'); + var $title = $('#ag-input-title'); + var $version = $('#ag-input-version'); + var $effective = $('#ag-input-effective'); + var $dirtyPill = $('#ag-dirty-pill'); + + function getCurrentEditorContent() { + return isSourceMode ? $source.val() : $visual.html(); + } + + function syncContentToTextarea() { + if (!isSourceMode) { + $source.val($visual.html()); + } + } + + function isDirty() { + var item = map[currentCode]; + if (!item) return false; + var curStatus = $("input[name='row[status]']:checked").val() || 'normal'; + return ( + $.trim($title.val()) !== $.trim(item.title || '') || + $.trim($version.val()) !== $.trim(item.version || '') || + $.trim($effective.val()) !== $.trim(item.effective_date || '') || + curStatus !== (item.status || 'normal') || + $.trim(getCurrentEditorContent()) !== $.trim(item.content || '') + ); + } + + function refreshDirtyIndicator() { + $dirtyPill.toggleClass('is-visible', isDirty()); + } + + function refreshPreviewModeSwitch() { + var item = map[currentCode]; + var canDiff = Boolean(item && item.has_diff && item.diff_content && !isDirty()); + var $group = $('#ag-preview-mode-group'); + $group.toggleClass('hidden', !canDiff); + if (!canDiff) { + previewMode = 'normal'; + } + $('.js-preview-mode').removeClass('is-active'); + $('.js-preview-mode[data-mode="' + previewMode + '"]').addClass('is-active'); + } + + function renderPreview() { + var item = map[currentCode]; + var titleVal = $.trim($title.val()) || '协议详情'; + var verVal = $.trim($version.val()) || 'v1.0'; + var effVal = $.trim($effective.val()); + refreshPreviewModeSwitch(); + + var showDiff = Boolean(previewMode === 'diff' && item && item.has_diff && item.diff_content); + var rawHtml = showDiff ? item.diff_content : getCurrentEditorContent(); + + $('#ag-preview-nav-title').text(titleVal); + $('#ag-preview-title').text(titleVal); + $('#ag-preview-version').text(verVal); + $('#ag-preview-effective').text(effVal ? ('生效日期:' + effVal) : ''); + $('#ag-preview-diff-tip').toggleClass('is-visible', showDiff); + if (showDiff) { + var summaryPrefix = item.diff_summary ? (item.diff_summary + ',') : ''; + $('#ag-preview-diff-tip-text').text('条款已修订:' + summaryPrefix + '浅橙色背景为本次调整位置(仅用户首次打开时标识)'); + } + $('#ag-preview-content').html(cleanPreviewHtml(rawHtml) || '

      暂无协议正文

      '); + refreshDirtyIndicator(); + } + + function loadAgreement(code) { + var item = map[code]; + if (!item) return; + currentCode = code; + previewMode = 'normal'; + + // 切回可视化模式 + if (isSourceMode) { + isSourceMode = false; + $('#ag-toggle-source').removeClass('is-active'); + $source.addClass('hidden'); + $visual.removeClass('hidden'); + $('#ag-toolbar-btns button').prop('disabled', false); + } + + $('#ag-tabs li').removeClass('active'); + $('#ag-tabs li[data-code="' + code + '"]').addClass('active'); + + $('#ag-input-code').val(item.code); + $('#ag-code-label').text(item.code); + $('#ag-revision-num').text(item.revision || 1); + $('#ag-scene-desc').text(item.desc || ''); + $('#ag-update-time').text(item.update_time_text || '-'); + + $title.val(item.title || ''); + $version.val(item.version || 'v1.0'); + $effective.val(item.effective_date || ''); + + var st = item.status === 'hidden' ? 'hidden' : 'normal'; + $("input[name='row[status]'][value='" + st + "']").prop('checked', true); + if (item.is_core) { + $('#ag-status-hidden').prop('disabled', true); + $('#ag-core-tip').text('《用户服务协议》与《隐私保护指引》为小程序登录与合规必选协议,始终保持展示状态。'); + } else { + $('#ag-status-hidden').prop('disabled', false); + $('#ag-core-tip').text('若设置为暂不展示,小程序对应协议入口将提示已下线。'); + } + + $visual.html(item.content || ''); + $source.val(item.content || ''); + renderPreview(); + } + + // 预览模式切换(标准正文 / 首看变更高亮) + $(document).on('click', '.js-preview-mode', function () { + var mode = $(this).data('mode') || 'normal'; + previewMode = (mode === 'diff') ? 'diff' : 'normal'; + renderPreview(); + }); + + // Tab 切换(含未保存修改保护) + $(document).on('click', '.js-ag-tab', function () { + var nextCode = $(this).data('code'); + if (!nextCode || nextCode === currentCode) return; + if (isDirty()) { + Layer.confirm('当前协议有尚未保存的修改,切换后修改将丢失,确定继续切换吗?', { + icon: 3, + title: '未保存提示' + }, function (idx) { + Layer.close(idx); + loadAgreement(nextCode); + }); + } else { + loadAgreement(nextCode); + } + }); + + // 工具栏排版操作 + $(document).on('click', '.js-cmd', function (e) { + e.preventDefault(); + if (isSourceMode) return; + var cmd = $(this).data('cmd'); + var val = $(this).data('val') || null; + $visual.focus(); + if (cmd === 'formatBlock' && val) { + document.execCommand(cmd, false, '<' + val + '>'); + } else { + document.execCommand(cmd, false, val); + } + syncContentToTextarea(); + renderPreview(); + }); + + // 切换 HTML 源码 / 可视化编辑模式 + $('#ag-toggle-source').on('click', function () { + if (!isSourceMode) { + $source.val($visual.html()); + $visual.addClass('hidden'); + $source.removeClass('hidden'); + $(this).addClass('is-active'); + $('#ag-toolbar-btns button').prop('disabled', true); + isSourceMode = true; + } else { + $visual.html($source.val()); + $source.addClass('hidden'); + $visual.removeClass('hidden'); + $(this).removeClass('is-active'); + $('#ag-toolbar-btns button').prop('disabled', false); + isSourceMode = false; + } + renderPreview(); + }); + + // 粘贴净化:粘贴纯文本并自动分段,避免 Word/网页携带冗余 class 与 style 污染小程序渲染 + $visual.on('paste', function (e) { + var clipboard = (e.originalEvent && e.originalEvent.clipboardData) || window.clipboardData; + if (!clipboard) return; + var text = clipboard.getData('text/plain'); + if (text !== undefined && text !== null) { + e.preventDefault(); + var escaped = text + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/\r\n/g, '\n') + .replace(/\r/g, '\n') + .replace(/\n/g, '
      '); + document.execCommand('insertHTML', false, escaped); + syncContentToTextarea(); + renderPreview(); + } + }); + + // 实时联动预览 + $visual.on('input keyup blur', function () { + syncContentToTextarea(); + renderPreview(); + }); + $source.on('input keyup blur', renderPreview); + $title.on('input', renderPreview); + $version.on('input', renderPreview); + $effective.on('change input', renderPreview); + $("input[name='row[status]']").on('change', refreshDirtyIndicator); + + // 撤销未保存修改 + $('#ag-reset-btn').on('click', function () { + if (!isDirty()) { + Toastr.info('当前内容与已保存版本一致'); + return; + } + loadAgreement(currentCode); + Toastr.success('已恢复至最近一次保存内容'); + }); + + // 保存协议并同步刷新 Token + $form.on('submit', function (e) { + e.preventDefault(); + if (isSubmitting) return; + + syncContentToTextarea(); + var titleVal = $.trim($title.val()); + if (!titleVal) { + Toastr.error('请输入协议标题'); + $title.focus(); + return; + } + + var $btn = $('#ag-submit-btn'); + var originHtml = $btn.html(); + isSubmitting = true; + $btn.prop('disabled', true).html(' 正在保存...'); + + $.ajax({ + url: $form.attr('action'), + type: 'POST', + dataType: 'json', + data: $form.serialize(), + success: function (ret) { + if (ret && ret.data && ret.data.token) { + $("input[name='__token__']", $form).val(ret.data.token); + } + if (ret && ret.code === 1) { + var saved = (ret.data && ret.data.agreement) || {}; + var target = map[currentCode]; + if (target) { + target.title = saved.title || titleVal; + target.version = saved.version || $.trim($version.val()); + target.effective_date = saved.effective_date || $.trim($effective.val()); + target.status = saved.status || 'normal'; + target.content = saved.content || $source.val(); + target.revision = saved.revision || target.revision || 1; + target.has_diff = Boolean(saved.has_diff); + target.diff_summary = saved.diff_summary || ''; + target.diff_content = saved.diff_content || ''; + target.update_time_text = saved.update_time_text || target.update_time_text; + } + loadAgreement(currentCode); + if (target && target.has_diff && target.diff_content) { + previewMode = 'diff'; + renderPreview(); + } + // 更新对应 Tab 停用徽标 + var $tabLink = $('#ag-tabs li[data-code="' + currentCode + '"] > a'); + $tabLink.find('.ag-tab-badge').remove(); + if (target && target.status === 'hidden') { + $tabLink.append(' '); + } + Toastr.success(ret.msg || '协议内容已保存并同步生效'); + } else { + Toastr.error((ret && ret.msg) || '保存失败,请检查内容后重试'); + } + }, + error: function () { + Toastr.error('网络请求异常,保存失败'); + }, + complete: function () { + isSubmitting = false; + $btn.prop('disabled', false).html(originHtml); + } + }); + }); + + // 初始化第一份协议 + loadAgreement(currentCode); + } + }; + + return Controller; +}); diff --git a/server/public/assets/js/backend/finance_config.js b/server/public/assets/js/backend/finance_config.js new file mode 100644 index 0000000..961631a --- /dev/null +++ b/server/public/assets/js/backend/finance_config.js @@ -0,0 +1,418 @@ +define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefined, Backend, Table, Form) { + + var ORDER_MODE_MAP = { + all: '不限订单类型', + fixed: '一口价', + quote: '师傅报价', + custom: '业主自主出价', + range: '区间定价' + }; + + var CUSTOMER_MAP = { + all: '不限下单人', + personal: '个人下单', + merchant: '企业商家' + }; + + var URGENT_MAP = { + all: '不限加急', + normal: '仅普通单', + urgent: '仅加急单' + }; + + function syncFormTokens(ret) { + if (ret && ret.data && ret.data.token) { + $("input[name='__token__']").val(ret.data.token); + } + } + + function toggleCalcModeFields($select, rateSelector, fixedSelector) { + var mode = $select.val() || 'percent'; + if (mode === 'percent') { + $(rateSelector).show(); + $(fixedSelector).hide(); + } else if (mode === 'fixed') { + $(rateSelector).hide(); + $(fixedSelector).show(); + } else { + $(rateSelector).show(); + $(fixedSelector).show(); + } + } + + /** + * 按规则公式试算抽佣结果,用于表格示例列、弹窗实时试算与兜底试算 + */ + function calcRulePreview(cfg, amount) { + var amt = Math.max(0, Number(amount || 0)); + var mode = cfg.calc_mode || 'percent'; + var rate = Math.max(0, Math.min(100, Number(cfg.commission_rate || 0))); + var fixed = Math.max(0, Number(cfg.fixed_fee || 0)); + var minC = Math.max(0, Number(cfg.min_commission || 0)); + var maxC = Math.max(0, Number(cfg.max_commission || 0)); + + var rawComm = 0; + if (mode === 'fixed') { + rawComm = fixed; + } else if (mode === 'mixed') { + rawComm = amt * rate / 100 + fixed; + } else { + rawComm = amt * rate / 100; + } + rawComm = Math.round(rawComm * 100) / 100; + + var tag = ''; + var comm = rawComm; + if (minC > 0 && comm < minC) { + comm = minC; + tag = '触发保底 ¥' + minC.toFixed(2); + } + if (maxC > 0 && comm > maxC) { + comm = maxC; + tag = '触发封顶 ¥' + maxC.toFixed(2); + } + if (comm > amt) { + comm = amt; + tag = '不超过订单总额'; + } + comm = Math.round(comm * 100) / 100; + var settle = Math.max(0, Math.round((amt - comm) * 100) / 100); + var actualRate = amt > 0 ? Math.round((comm / amt) * 10000) / 100 : 0; + + return { + amount: amt, + commission: comm, + settle: settle, + actualRate: actualRate, + tag: tag + }; + } + + function formatConditionsCell(row) { + var tags = []; + if (row.order_mode && row.order_mode !== 'all') { + tags.push('' + (ORDER_MODE_MAP[row.order_mode] || row.order_mode) + ''); + } + if (row.customer_type && row.customer_type !== 'all') { + var cCls = row.customer_type === 'merchant' ? 'info' : 'success'; + tags.push('' + (CUSTOMER_MAP[row.customer_type] || row.customer_type) + ''); + } + if (Number(row.cat_id) > 0 && row.cat_name) { + tags.push('' + Fast.api.escape(row.cat_name) + ''); + } + if (row.urgent_mode && row.urgent_mode !== 'all') { + var uCls = row.urgent_mode === 'urgent' ? 'danger' : 'default'; + tags.push('' + (URGENT_MAP[row.urgent_mode] || row.urgent_mode) + ''); + } + var minA = Number(row.min_amount || 0); + var maxA = Number(row.max_amount || 0); + if (minA > 0 && maxA > 0) { + tags.push('¥' + minA.toFixed(0) + '~¥' + maxA.toFixed(0) + ''); + } else if (minA > 0) { + tags.push('满 ¥' + minA.toFixed(0) + ' 起'); + } else if (maxA > 0) { + tags.push('¥' + maxA.toFixed(0) + ' 以内'); + } + + if (tags.length === 0) { + return '全部订单通用'; + } + return tags.join(' '); + } + + function formatFormulaCell(row) { + var mode = row.calc_mode || 'percent'; + var rate = Number(row.commission_rate || 0).toFixed(2); + var fixed = Number(row.fixed_fee || 0).toFixed(2); + var html = ''; + if (mode === 'fixed') { + html = '每单固定 ¥' + fixed + ''; + } else if (mode === 'mixed') { + html = '抽 ' + rate + '% + ¥' + fixed + '/单'; + } else { + html = '抽 ' + rate + '%'; + } + var limits = []; + if (Number(row.min_commission || 0) > 0) { + limits.push('最少¥' + Number(row.min_commission).toFixed(2)); + } + if (Number(row.max_commission || 0) > 0) { + limits.push('最多¥' + Number(row.max_commission).toFixed(2)); + } + if (limits.length > 0) { + html += '
      ' + limits.join(',') + '
      '; + } + return html; + } + + function formatSampleCell(row) { + var minA = Number(row.min_amount || 0); + var maxA = Number(row.max_amount || 0); + var sampleAmt = 200; + if (minA > 0 && maxA > 0) { + sampleAmt = Math.round(((minA + maxA) / 2) * 100) / 100; + } else if (minA > 0) { + sampleAmt = minA; + } else if (maxA > 0 && maxA < 200) { + sampleAmt = maxA; + } + var res = calcRulePreview(row, sampleAmt); + return '¥' + res.amount.toFixed(0) + '单:抽 ¥' + res.commission.toFixed(2) + ' / 师傅得 ¥' + res.settle.toFixed(2) + ''; + } + + function updateDefaultPreview() { + var cfg = { + calc_mode: $('#c-default_calc_mode').val() || 'percent', + commission_rate: $('#c-default_commission_rate').val(), + fixed_fee: $('#c-default_fixed_fee').val(), + min_commission: $('#c-default_min_commission').val(), + max_commission: $('#c-default_max_commission').val() + }; + var res = calcRulePreview(cfg, 200); + var html = '按成交 ¥200.00 试算:平台抽佣 ¥' + res.commission.toFixed(2) + ',师傅实得 ¥' + res.settle.toFixed(2) + ''; + if (res.tag) { + html += ' ' + res.tag + ''; + } + $('.js-def-preview').html(html); + } + + function updateWithdrawFeePreview() { + var amt = 100; + var mode = $('#c-wd-fee-mode').val() || 'percent'; + var fee = 0; + if (mode === 'fixed') { + fee = Math.max(0, Number($('#c-wd-fee-fixed').val() || 0)); + } else if (mode === 'percent') { + var rate = Math.max(0, Number($('#c-wd-fee-rate').val() || 0)); + var minF = Math.max(0, Number($('#c-wd-fee-min').val() || 0)); + var maxF = Math.max(0, Number($('#c-wd-fee-max').val() || 0)); + fee = Math.round((amt * rate / 100) * 100) / 100; + if (minF > 0 && fee < minF) { + fee = minF; + } + if (maxF > 0 && fee > maxF) { + fee = maxF; + } + } + fee = Math.min(amt, Math.round(fee * 100) / 100); + var arrive = Math.max(0, Math.round((amt - fee) * 100) / 100); + $('.js-wd-fee-preview').html( + '按提现 ¥100.00 试算:扣除手续费 ¥' + fee.toFixed(2) + ',师傅实际到账 ¥' + arrive.toFixed(2) + '' + ); + } + + function updateModalRulePreview() { + var minA = Math.max(0, Number($('#c-min_amount').val() || 0)); + var maxA = Math.max(0, Number($('#c-max_amount').val() || 0)); + var testAmt = Math.max(0, Number($('.js-rule-test-amount').val() || 0)); + var res = calcRulePreview({ + calc_mode: $('#c-calc_mode').val() || 'percent', + commission_rate: $('#c-commission_rate').val(), + fixed_fee: $('#c-fixed_fee').val(), + min_commission: $('#c-min_commission').val(), + max_commission: $('#c-max_commission').val() + }, testAmt); + + var html = '→ 平台抽佣 ¥' + res.commission.toFixed(2) + '(约 ' + res.actualRate.toFixed(2) + '%),师傅实得 ¥' + res.settle.toFixed(2) + ''; + if (res.tag) { + html += ' ' + res.tag + ''; + } + if ((minA > 0 && testAmt < minA) || (maxA > 0 && testAmt > maxA)) { + html += '
      注:当前测试金额未落入上面填写的订单金额范围(¥' + minA.toFixed(2) + ' ~ ' + (maxA > 0 ? '¥' + maxA.toFixed(2) : '不限') + '),以上仅按公式演示。
      '; + } + $('.js-rule-preview-text').html(html); + } + + var Controller = { + index: function () { + Table.api.init({ + extend: { + index_url: 'finance_config/index' + location.search, + add_url: 'finance_config/rule_add', + edit_url: 'finance_config/rule_edit', + del_url: 'finance_config/rule_del', + multi_url: 'finance_config/rule_multi', + dragsort_url: '', + table: 'commission_rule' + } + }); + + var table = $("#table"); + $(".btn-add").data("area", ["780px", "88%"]); + table.on('post-body.bs.table', function () { + $(".btn-editone", this).data("area", ["780px", "88%"]); + }); + + table.bootstrapTable({ + url: $.fn.bootstrapTable.defaults.extend.index_url, + pk: 'id', + sortName: 'weigh', + sortOrder: 'desc', + pageSize: 15, + showExport: false, + showToggle: false, + fixedColumns: false, + commonSearch: true, + searchFormVisible: false, + columns: [ + [ + {checkbox: true}, + {field: 'id', title: '编号', width: '68px', sortable: true, operate: false}, + { + field: 'name', + title: '规则名称', + align: 'left', + operate: 'LIKE', + formatter: function (v, row) { + var html = '' + Fast.api.escape(v || '') + ''; + if (row.remark) { + html += '
      ' + Fast.api.escape(row.remark) + '
      '; + } + return html; + } + }, + { + field: 'conditions_preview', + title: '适用条件', + align: 'left', + operate: false, + formatter: function (v, row) { + return formatConditionsCell(row); + } + }, + { + field: 'formula_preview', + title: '抽佣方式', + align: 'left', + operate: false, + formatter: function (v, row) { + return formatFormulaCell(row); + } + }, + { + field: 'sample_preview', + title: '举例试算', + align: 'left', + operate: false, + formatter: function (v, row) { + return formatSampleCell(row); + } + }, + {field: 'order_mode', title: '订单类型', searchList: ORDER_MODE_MAP, visible: false}, + {field: 'customer_type', title: '下单人', searchList: CUSTOMER_MAP, visible: false}, + {field: 'urgent_mode', title: '是否加急', searchList: URGENT_MAP, visible: false}, + {field: 'weigh', title: '优先级', sortable: true, width: '84px', operate: false}, + {field: 'status', title: '状态', searchList: {normal: '启用', hidden: '停用'}, formatter: Table.api.formatter.status, custom: {normal: 'success', hidden: 'default'}, width: '78px'}, + { + field: 'operate', + title: '操作', + width: '124px', + table: table, + events: Table.api.events.operate, + buttons: [ + {name: 'detail', hidden: true} + ], + formatter: Table.api.formatter.operate + } + ] + ] + }); + + Table.api.bindevent(table); + + // 兜底默认抽佣联动与实时试算 + var $defCalc = $('.js-def-calc-mode'); + toggleCalcModeFields($defCalc, '.js-def-rate-box', '.js-def-fixed-box'); + $defCalc.on('change', function () { + toggleCalcModeFields($(this), '.js-def-rate-box', '.js-def-fixed-box'); + }); + updateDefaultPreview(); + $('.js-def-field').on('input change', updateDefaultPreview); + + // 提现手续费联动与实时试算 + var syncWdFeeMode = function () { + var m = $('.js-wd-fee-mode').val() || 'percent'; + if (m === 'percent') { + $('.js-wd-fee-percent').show(); + $('.js-wd-fee-fixed').hide(); + } else if (m === 'fixed') { + $('.js-wd-fee-percent').hide(); + $('.js-wd-fee-fixed').show(); + } else { + $('.js-wd-fee-percent').hide(); + $('.js-wd-fee-fixed').hide(); + } + updateWithdrawFeePreview(); + }; + syncWdFeeMode(); + $('.js-wd-fee-mode').on('change', syncWdFeeMode); + $('.js-wd-fee-field').on('input change', updateWithdrawFeePreview); + + // 提现小额自动审核联动 + var syncWdAuditMode = function () { + var am = $('.js-wd-audit-mode').val() || 'manual'; + if (am === 'auto_small') { + $('.js-wd-auto-max').show(); + } else { + $('.js-wd-auto-max').hide(); + } + }; + syncWdAuditMode(); + $('.js-wd-audit-mode').on('change', syncWdAuditMode); + + Form.api.bindevent($("#commission-default-form"), function (data, ret) { + syncFormTokens(ret); + }, function (data, ret) { + syncFormTokens(ret); + }); + + Form.api.bindevent($("#withdraw-config-form"), function (data, ret) { + syncFormTokens(ret); + }, function (data, ret) { + syncFormTokens(ret); + }); + + if (location.hash === '#tab-withdraw') { + $('#fc-main-tabs a[href="#tab-withdraw"]').tab('show'); + } + }, + rule_add: function () { + Controller.api.bindRuleForm(); + }, + rule_edit: function () { + Controller.api.bindRuleForm(); + }, + api: { + bindRuleForm: function () { + var $calc = $('.js-rule-calc-mode'); + toggleCalcModeFields($calc, '.js-rule-rate-box', '.js-rule-fixed-box'); + $calc.on('change', function () { + toggleCalcModeFields($(this), '.js-rule-rate-box', '.js-rule-fixed-box'); + }); + + // 编辑有金额区间门槛的规则时,自动给试算框带入一个落在区间内的参考金额 + var initMin = Math.max(0, Number($('#c-min_amount').val() || 0)); + var initMax = Math.max(0, Number($('#c-max_amount').val() || 0)); + var $testAmt = $('.js-rule-test-amount'); + if (initMin > 0 && Number($testAmt.val() || 0) < initMin) { + $testAmt.val(initMin); + } else if (initMax > 0 && Number($testAmt.val() || 0) > initMax) { + $testAmt.val(initMax); + } + + updateModalRulePreview(); + $('.js-rule-field, .js-rule-test-amount').on('input change', updateModalRulePreview); + + $("form[role=form]").on('reset', function () { + setTimeout(function () { + toggleCalcModeFields($calc, '.js-rule-rate-box', '.js-rule-fixed-box'); + updateModalRulePreview(); + }, 0); + }); + + Form.api.bindevent($("form[role=form]")); + } + } + }; + return Controller; +}); diff --git a/server/public/assets/js/backend/master_cert.js b/server/public/assets/js/backend/master_cert.js new file mode 100644 index 0000000..2c15b7c --- /dev/null +++ b/server/public/assets/js/backend/master_cert.js @@ -0,0 +1,136 @@ +define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefined, Backend, Table, Form) { + + var Controller = { + index: function () { + Table.api.init({ + extend: { + index_url: 'master_cert/index' + location.search, + detail_url: 'master_cert/detail', + audit_url: 'master_cert/audit', + edit_url: 'master_cert/edit', + del_url: 'master_cert/del', + multi_url: 'master_cert/multi', + table: 'master_cert', + } + }); + + var table = $("#table"); + + table.on('post-body.bs.table', function () { + $(".btn-detail-cert", this).data("area", ["780px", "82%"]); + $(".btn-audit-cert", this).data("area", ["840px", "86%"]); + }); + + table.bootstrapTable({ + url: $.fn.bootstrapTable.defaults.extend.index_url, + pk: 'id', + sortName: 'id', + sortOrder: 'desc', + fixedColumns: true, + fixedRightNumber: 1, + columns: [ + [ + {checkbox: true}, + {field: 'index', title: '序号', operate: false, formatter: Table.api.formatter.index, width: '60px'}, + { + field: 'master.realname', + title: '申报师傅', + operate: 'LIKE', + formatter: function (v, row) { + var mobile = (row.master && row.master.mobile) ? row.master.mobile : ''; + return '' + Fast.api.escape(v || '-') + '' + (mobile ? '
      ' + Fast.api.escape(mobile) + '
      ' : ''); + } + }, + {field: 'master.mobile', title: '师傅电话', operate: 'LIKE', visible: false}, + { + field: 'cert_name', + title: '证书名称', + operate: 'LIKE', + formatter: function (v, row) { + var tag = Number(row.is_custom) === 1 ? ' 自定义' : ''; + return '' + Fast.api.escape(v || '-') + '' + tag; + } + }, + { + field: 'cert_image', + title: '证书照片', + operate: false, + events: Table.api.events.image, + formatter: Table.api.formatter.image + }, + { + field: 'expire_date', + title: '有效期至', + operate: false, + formatter: function (v, row) { + if (Number(row.is_long_term) === 1) { + return '长期有效'; + } + return v ? Fast.api.escape(v) : '未填写'; + } + }, + { + field: 'status', + title: __('Status'), + searchList: {"pending": __('Pending'), "pass": __('Pass'), "reject": __('Reject')}, + formatter: Table.api.formatter.status, + custom: {pending: 'warning', pass: 'success', reject: 'danger'} + }, + { + field: 'audit_msg', + title: __('Audit_msg'), + operate: 'LIKE', + table: table, + class: 'autocontent', + formatter: Table.api.formatter.content + }, + {field: 'updatetime', title: __('Updatetime'), operate: 'RANGE', addclass: 'datetimerange', autocomplete: false, formatter: Table.api.formatter.datetime, width: '150px'}, + { + field: 'operate', + title: __('Operate'), + table: table, + events: Table.api.events.operate, + buttons: [ + { + name: 'detail', + text: '详情', + title: function (row) { + return '证书详情 - ' + (row.cert_name || ''); + }, + icon: 'fa fa-eye', + classname: 'btn btn-xs btn-info btn-dialog btn-detail-cert', + url: 'master_cert/detail' + }, + { + name: 'audit', + text: '审核', + title: function (row) { + return '审核证书 - ' + (row.cert_name || ''); + }, + icon: 'fa fa-gavel', + classname: 'btn btn-xs btn-success btn-dialog btn-audit-cert', + url: 'master_cert/audit' + } + ], + formatter: Table.api.formatter.operate + } + ] + ] + }); + + Table.api.bindevent(table); + }, + audit: function () { + Controller.api.bindevent(); + }, + edit: function () { + Controller.api.bindevent(); + }, + api: { + bindevent: function () { + Form.api.bindevent($("form[role=form]")); + } + } + }; + return Controller; +}); diff --git a/server/public/assets/js/backend/order.js b/server/public/assets/js/backend/order.js index 62fcbb0..2d1af73 100644 --- a/server/public/assets/js/backend/order.js +++ b/server/public/assets/js/backend/order.js @@ -202,6 +202,50 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin Controller.api.bindevent(); }, detail: function () { + var escapeHtml = function (str) { + return String(str || '').replace(/[&<>"']/g, function (c) { + return {'&': '&', '<': '<', '>': '>', '"': '"', "'": '''}[c]; + }); + }; + $(document).off('click', '.js-sync-pay-order').on('click', '.js-sync-pay-order', function () { + var $btn = $(this); + var id = $btn.data('id'); + if (!id) return; + $btn.prop('disabled', true); + Fast.api.ajax({ + url: 'order/syncpay', + type: 'post', + data: { pay_order_id: id } + }, function () { + location.reload(); + return false; + }, function () { + $btn.prop('disabled', false); + }); + }); + $(document).off('click', '.js-retry-refund').on('click', '.js-retry-refund', function () { + var $btn = $(this); + var id = $btn.data('id'); + var outNo = $btn.data('outno') || ''; + if (!id) return; + Layer.confirm('确认向微信支付发起(或重试)原路退款?
      商户单号:' + escapeHtml(outNo) + '', { + title: '确认原路退款', + icon: 3 + }, function (idx) { + Layer.close(idx); + $btn.prop('disabled', true); + Fast.api.ajax({ + url: 'order/retryrefund', + type: 'post', + data: { pay_order_id: id } + }, function () { + location.reload(); + return false; + }, function () { + $btn.prop('disabled', false); + }); + }); + }); }, api: { bindevent: function () { diff --git a/server/public/assets/js/backend/service.js b/server/public/assets/js/backend/service.js index 7b41f4d..8a841f7 100644 --- a/server/public/assets/js/backend/service.js +++ b/server/public/assets/js/backend/service.js @@ -23,7 +23,7 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin }); table.on('post-body.bs.table', function () { - $(".btn-editone, .btn-pricing", this).data("area", ["1040px", "88%"]); + $(".btn-editone, .btn-pricing, .btn-gallery-srv", this).data("area", ["1040px", "88%"]); $(".btn-detail-srv", this).data("area", ["880px", "82%"]); }); $(".btn-add, .btn-edit").data("area", ["1040px", "88%"]); @@ -140,6 +140,18 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin icon: 'fa fa-sliders', classname: 'btn btn-xs btn-warning btn-dialog btn-pricing', url: 'service/edit?tab=services' + }, + { + name: 'gallery', + text: '系统图库', + title: function (row) { + return '【' + row.name + '】系统图库管理'; + }, + icon: 'fa fa-picture-o', + classname: 'btn btn-xs btn-primary btn-dialog btn-gallery-srv', + url: function (row) { + return 'service/gallery/index?service_id=' + row.id + '&cat_id=' + row.cat_id; + } } ], formatter: Table.api.formatter.operate @@ -276,6 +288,15 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin var html = '
      ' + '' + '' + + '' + '' + '' + '' + @@ -284,6 +305,59 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin Controller.api.refreshRowAndCounters(null); }); + $(document).off('click.srv', '.btn-upload-spec-img').on('click.srv', '.btn-upload-spec-img', function () { + $(this).closest('.spec-img-cell').find('.spec-img-file').trigger('click'); + }); + + $(document).off('change.srv', '.spec-img-file').on('change.srv', '.spec-img-file', function () { + var file = this.files && this.files[0]; + if (!file) return; + var $cell = $(this).closest('.spec-img-cell'); + var $btn = $cell.find('.btn-upload-spec-img'); + var fd = new FormData(); + fd.append('file', file); + fd.append('category', 'service'); + $btn.prop('disabled', true).html(''); + $.ajax({ + url: Fast.api.fixurl('ajax/upload'), + type: 'POST', + data: fd, + processData: false, + contentType: false, + dataType: 'json', + success: function (res) { + if (res && res.code === 1 && res.data) { + var rawUrl = res.data.url || ''; + var fullUrl = res.data.fullurl || rawUrl; + $cell.find('.spec-img-input').val(rawUrl); + $cell.find('.spec-img-preview').attr('src', fullUrl).show(); + $cell.find('.btn-clear-spec-img').show(); + $btn.html(' 更换'); + Toastr.success('规格参考图已上传'); + } else { + Toastr.error((res && res.msg) ? res.msg : '上传失败'); + $btn.html(' 上传'); + } + }, + error: function () { + Toastr.error('上传请求异常'); + $btn.html(' 上传'); + }, + complete: function () { + $btn.prop('disabled', false); + } + }); + $(this).val(''); + }); + + $(document).off('click.srv', '.btn-clear-spec-img').on('click.srv', '.btn-clear-spec-img', function () { + var $cell = $(this).closest('.spec-img-cell'); + $cell.find('.spec-img-input').val(''); + $cell.find('.spec-img-preview').attr('src', '').hide(); + $cell.find('.btn-upload-spec-img').html(' 上传'); + $(this).hide(); + }); + $(document).off('click.srv', '.btn-del-spec').on('click.srv', '.btn-del-spec', function () { $(this).closest('tr').remove(); Controller.api.refreshRowAndCounters(null); diff --git a/server/public/assets/js/backend/service/gallery.js b/server/public/assets/js/backend/service/gallery.js new file mode 100644 index 0000000..aa9c36e --- /dev/null +++ b/server/public/assets/js/backend/service/gallery.js @@ -0,0 +1,164 @@ +define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefined, Backend, Table, Form) { + + var Controller = { + index: function () { + var initServiceMatch = location.search.match(/[?&]service_id=(\d+)/); + var initCatMatch = location.search.match(/[?&]cat_id=(\d+)/); + var initServiceId = initServiceMatch ? initServiceMatch[1] : ''; + var activeCatId = initCatMatch ? initCatMatch[1] : ''; + + var buildAddUrl = function () { + var qs = []; + if (initServiceId) { + qs.push('service_id=' + encodeURIComponent(initServiceId)); + } + if (activeCatId) { + qs.push('cat_id=' + encodeURIComponent(activeCatId)); + } + return 'service/gallery/add' + (qs.length ? ('?' + qs.join('&')) : ''); + }; + + Table.api.init({ + extend: { + index_url: 'service/gallery/index' + location.search, + add_url: buildAddUrl(), + edit_url: 'service/gallery/edit', + del_url: 'service/gallery/del', + multi_url: 'service/gallery/multi', + dragsort_url: '', + table: 'system_gallery', + } + }); + + var table = $("#table"); + + $('.panel-heading ul[data-field="cat_id"] a[data-toggle="tab"]').on('shown.bs.tab', function () { + activeCatId = String($(this).data('value') || ''); + $.fn.bootstrapTable.defaults.extend.add_url = buildAddUrl(); + var opts = table.bootstrapTable('getOptions'); + if (opts && opts.extend) { + opts.extend.add_url = buildAddUrl(); + } + }); + + table.on('post-body.bs.table', function () { + $(".btn-editone", this).data("area", ["780px", "560px"]); + }); + $(".btn-add, .btn-edit").data("area", ["780px", "580px"]); + + table.bootstrapTable({ + url: $.fn.bootstrapTable.defaults.extend.index_url, + pk: 'id', + sortName: 'weigh', + sortOrder: 'desc', + pageSize: 20, + queryParams: function (params) { + var filter = params.filter ? JSON.parse(params.filter) : {}; + var op = params.op ? JSON.parse(params.op) : {}; + if (initServiceId !== '' && typeof filter.service_id === 'undefined') { + filter.service_id = initServiceId; + op.service_id = '='; + } else if (activeCatId !== '' && typeof filter.cat_id === 'undefined') { + filter.cat_id = activeCatId; + op.cat_id = '='; + } + params.filter = JSON.stringify(filter); + params.op = JSON.stringify(op); + return params; + }, + columns: [ + [ + {checkbox: true}, + {field: 'index', title: '序号', operate: false, formatter: Table.api.formatter.index, width: '60px'}, + {field: 'cat_id', title: '所属类目', visible: false, operate: '='}, + {field: 'service_id', title: '所属商品', visible: false, operate: '='}, + {field: 'cat.name', title: '所属类目', operate: 'LIKE', width: '110px'}, + { + field: 'service.name', + title: '所属商品', + operate: 'LIKE', + align: 'left', + formatter: function (val) { + return '' + (val || '-') + ''; + } + }, + { + field: 'image', + title: '系统参考图', + operate: false, + events: Table.api.events.image, + formatter: Table.api.formatter.image, + width: '100px' + }, + {field: 'title', title: '款式/图片名称', operate: 'LIKE', align: 'left'}, + { + field: 'spec_id', + title: '来源', + operate: false, + width: '95px', + formatter: function (val) { + return Number(val) > 0 + ? '规格同步' + : '独立图库'; + } + }, + {field: 'weigh', title: '排序', operate: false, width: '75px', sortable: true}, + { + field: 'status', + title: __('Status'), + width: '85px', + searchList: {"normal": __('Normal'), "hidden": __('Hidden')}, + formatter: Table.api.formatter.status + }, + { + field: 'updatetime', + title: __('Updatetime'), + operate: 'RANGE', + addclass: 'datetimerange', + autocomplete: false, + formatter: Table.api.formatter.datetime, + width: '150px' + }, + { + field: 'operate', + title: __('Operate'), + table: table, + events: Table.api.events.operate, + formatter: Table.api.formatter.operate + } + ] + ] + }); + + Table.api.bindevent(table); + }, + add: function () { + Controller.api.bindevent(); + }, + edit: function () { + Controller.api.bindevent(); + }, + api: { + bindevent: function () { + $("#c-service_id").data("params", function () { + var catId = $("#c-cat_id").val(); + return catId ? {custom: {cat_id: catId}} : {}; + }); + var initialCatVal = $("#c-cat_id").val(); + $(document).on("change", "#c-cat_id", function () { + var curVal = $(this).val(); + if (curVal !== initialCatVal) { + initialCatVal = curVal; + if (typeof $("#c-service_id").selectPageClear === "function") { + $("#c-service_id").selectPageClear(); + } else { + $("#c-service_id").val(""); + } + } + }); + Form.api.bindevent($("form[role=form]")); + } + } + }; + return Controller; +}); diff --git a/server/public/assets/js/backend/settle.js b/server/public/assets/js/backend/settle.js index 6a41d43..21f99b4 100644 --- a/server/public/assets/js/backend/settle.js +++ b/server/public/assets/js/backend/settle.js @@ -31,8 +31,8 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin {field: 'order_amount', title: '工单原额', operate: 'BETWEEN', formatter: function (v) { return '¥' + Number(v || 0).toFixed(2); }}, - {field: 'commission_rate', title: '抽佣率', operate: 'BETWEEN', formatter: function (v) { - return Number(v || 0).toFixed(1) + '%'; + {field: 'commission_rate', title: '抽佣快照', operate: 'BETWEEN', formatter: function (v) { + return Number(v || 0).toFixed(2) + '%'; }}, {field: 'commission', title: '平台抽佣', operate: 'BETWEEN', formatter: function (v) { return '¥' + Number(v || 0).toFixed(2) + ''; diff --git a/server/public/assets/js/backend/withdraw.js b/server/public/assets/js/backend/withdraw.js index e2d6d54..5084253 100644 --- a/server/public/assets/js/backend/withdraw.js +++ b/server/public/assets/js/backend/withdraw.js @@ -29,8 +29,24 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin {field: 'fee', title: '手续费', operate: 'BETWEEN', formatter: function (v) { return '¥' + Number(v || 0).toFixed(2); }}, {field: 'arrive_amount', title: '应打款净额', operate: 'BETWEEN', formatter: function (v) { return '¥' + Number(v || 0).toFixed(2) + ''; }}, {field: 'channel', title: '结算渠道', searchList: {"wechat":__('Wechat'),"bank":__('Bank')}, formatter: Table.api.formatter.label, custom: {wechat: 'success', bank: 'primary'}}, - {field: 'account', title: '收款账号', operate: 'LIKE'}, + {field: 'out_bill_no', title: '商户转账单号', operate: 'LIKE', formatter: function (v) { return v ? '' + v + '' : '-'; }}, + {field: 'transfer_state_text', title: '微信转账状态', operate: false, formatter: function (v, row) { + if (row.channel !== 'wechat') return '线下卡转'; + var st = row.transfer_state || ''; + if (!st && !row.fail_reason) return '待审核发起'; + var cls = 'label-default'; + if (st === 'SUCCESS') cls = 'label-success'; + else if (st === 'WAIT_USER_CONFIRM' || st === 'TRANSFERING' || st === 'ACCEPTED' || st === 'PROCESSING') cls = 'label-warning'; + else if (st === 'FAIL' || st === 'CANCELLED' || st === 'UNKNOWN_TIMEOUT' || st === 'NOT_FOUND') cls = 'label-danger'; + else cls = 'label-info'; + return '' + (v || st || '发起异常') + ''; + }}, {field: 'status', title: '审核状态', searchList: {"wait":__('Wait'),"pass":__('Pass'),"reject":__('Reject'),"done":__('Done')}, formatter: Table.api.formatter.status, custom: {wait: 'warning', pass: 'info', reject: 'danger', done: 'success'}}, + {field: 'fail_reason', title: '失败 / 异常诊断原因', operate: 'LIKE', align: 'left', formatter: function (v, row) { + if (!v && !row.fail_code) return '-'; + var codeHtml = row.fail_code ? '' + Table.api.formatter.normal.call(this, row.fail_code, row) + '' : ''; + return '
      ' + codeHtml + Table.api.formatter.normal.call(this, v || '', row) + '
      '; + }}, {field: 'audit_msg', title: '审核批注', operate: 'LIKE', class: 'autocontent', formatter: Table.api.formatter.content}, {field: 'createtime', title: '申请时间', operate: 'RANGE', addclass: 'datetimerange', autocomplete: false, formatter: Table.api.formatter.datetime, width: '150px'}, {field: 'updatetime', title: '处理时间', operate: 'RANGE', addclass: 'datetimerange', autocomplete: false, formatter: Table.api.formatter.datetime, width: '150px'}, @@ -43,12 +59,28 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin classname: 'btn btn-xs btn-info btn-detailone', extend: 'data-area=\'["900px","86%"]\'' }, + { + name: 'sync', + text: '同步状态', + title: '同步微信转账状态', + classname: 'btn btn-xs btn-warning btn-ajax', + icon: 'fa fa-refresh', + url: 'withdraw/sync', + refresh: true, + visible: function (row) { + return row.channel === 'wechat' && !!row.out_bill_no && (row.status === 'pass' || row.status === 'wait'); + } + }, { name: 'edit', - text: function (row) { return row.status === 'pass' ? '确认打款' : '审核'; }, - title: '财务提现审核', + text: function (row) { + if (row.transfer_state === 'FAIL' || row.transfer_state === 'NOT_FOUND') return '重发/处理'; + if (row.status === 'wait') return row.channel === 'wechat' ? '审核打款' : '审核'; + return row.channel === 'wechat' && row.out_bill_no ? '撤销/处理' : '确认打款'; + }, + title: '财务提现审核与转账', classname: 'btn btn-xs btn-success btn-editone', - extend: 'data-area=\'["840px","80%"]\'', + extend: 'data-area=\'["840px","82%"]\'', visible: function (row) { return row.status === 'wait' || row.status === 'pass'; } diff --git a/yjyf-app/components/LogisticsDrawer.vue b/yjyf-app/components/LogisticsDrawer.vue index 89ddc68..c076a0a 100644 --- a/yjyf-app/components/LogisticsDrawer.vue +++ b/yjyf-app/components/LogisticsDrawer.vue @@ -26,9 +26,18 @@ - + - @@ -798,87 +761,4 @@ 20%, 60% { transform: translateX(-8rpx); } 40%, 80% { transform: translateX(8rpx); } } - - /* 协议半屏抽屉 */ - .drawer-mask { - position: fixed; - left: 0; - top: 0; - right: 0; - bottom: 0; - background: rgba(0, 0, 0, 0.55); - z-index: 999; - display: flex; - align-items: flex-end; - animation: fadeIn 0.2s ease-out; - } - .drawer-panel { - width: 100%; - max-height: 75vh; - background: #FFFFFF; - border-radius: 36rpx 36rpx 0 0; - padding: 36rpx 36rpx calc(36rpx + env(safe-area-inset-bottom)); - box-sizing: border-box; - display: flex; - flex-direction: column; - animation: slideUp 0.25s ease-out; - } - .drawer-header { - display: flex; - align-items: center; - justify-content: space-between; - margin-bottom: 24rpx; - } - .drawer-title { - font-size: 32rpx; - font-weight: 700; - color: #1D2129; - } - .drawer-close { - width: 52rpx; - height: 52rpx; - display: flex; - align-items: center; - justify-content: center; - } - .close-img { - width: 32rpx; - height: 32rpx; - } - .drawer-scroll { - max-height: 480rpx; - margin-bottom: 30rpx; - } - .drawer-content-text { - font-size: 26rpx; - color: #4E5969; - line-height: 44rpx; - white-space: pre-wrap; - } - .drawer-footer { - padding-top: 10rpx; - } - .drawer-confirm-btn { - height: 88rpx; - border-radius: 44rpx; - background: linear-gradient(135deg, #FFA93F 0%, #F0710D 100%); - display: flex; - align-items: center; - justify-content: center; - box-shadow: 0 8rpx 20rpx rgba(240, 113, 13, 0.25); - } - .drawer-confirm-text { - font-size: 30rpx; - font-weight: 600; - color: #FFFFFF; - } - - @keyframes fadeIn { - from { opacity: 0; } - to { opacity: 1; } - } - @keyframes slideUp { - from { transform: translateY(100%); } - to { transform: translateY(0); } - } diff --git a/yjyf-app/pages/master/apply.vue b/yjyf-app/pages/master/apply.vue index cb773ec..4899252 100644 --- a/yjyf-app/pages/master/apply.vue +++ b/yjyf-app/pages/master/apply.vue @@ -61,22 +61,22 @@ - 身份证件照片(选填,加速审核) + 身份证件照片(必传,支持智能识别回填) - + 身份证人像面 - 点击上传 + 点击上传(自动提取信息) - + 身份证国徽面 - 点击上传 + 点击上传(核验有效期限) @@ -93,20 +93,38 @@ - {{ submitting ? '提交中...' : '提交实名入驻申请' }} + {{ submitting ? '正在进行实名核验...' : '提交实名认证' }} - - - 认证师傅档案 + + + + + + + + {{ info.master.verify_status === 'pass' ? '已实名' : '审核中' }} + + + + + + {{ masterTitleName }} + {{ info.master.mobile }} + + + + + + 真实姓名 {{ info.master.realname }} - 联系电话 - {{ info.master.mobile }} + 身份证号 + {{ info.master.idcard_no }} 服务城市 @@ -128,29 +146,11 @@ - - - - - {{ agreementData.title || '师傅入驻与履约服务协议' }} - - - - - - {{ agreementData.content || '正在加载协议内容...' }} - - - 我已知晓并同意 - - - - @@ -158,21 +158,21 @@ + + diff --git a/yjyf-app/pages/master/cert-upload.vue b/yjyf-app/pages/master/cert-upload.vue new file mode 100644 index 0000000..f4aa3fe --- /dev/null +++ b/yjyf-app/pages/master/cert-upload.vue @@ -0,0 +1,883 @@ + + + + + diff --git a/yjyf-app/pages/master/certs.vue b/yjyf-app/pages/master/certs.vue new file mode 100644 index 0000000..715f02e --- /dev/null +++ b/yjyf-app/pages/master/certs.vue @@ -0,0 +1,456 @@ + + + + + diff --git a/yjyf-app/pages/master/credit.vue b/yjyf-app/pages/master/credit.vue index 53dd255..918e6a0 100644 --- a/yjyf-app/pages/master/credit.vue +++ b/yjyf-app/pages/master/credit.vue @@ -20,11 +20,19 @@ + + 信用档案加载失败 + {{ creditError }} + + 重新加载 + + + 综合信用评分 - {{ Number(credit.score || 5).toFixed(1) }} + {{ Number(credit.score ?? 0).toFixed(1) }} / 5.0 累计完单 {{ credit.order_count || 0 }} 单 @@ -41,7 +49,7 @@ - {{ Number(credit.score || 5).toFixed(1) }} + {{ Number(credit.score ?? 0).toFixed(1) }} 综合评分 @@ -53,7 +61,7 @@ 准时上门率 - {{ credit.good_rate }}% + {{ credit.has_review === false ? '暂无' : `${credit.good_rate}%` }} 客户好评率 @@ -292,6 +300,7 @@ const activeTab = ref('credit'); const credit = ref(null); + const creditError = ref(''); const list = ref([]); const submitting = ref(false); const evidenceImgs = ref([]); @@ -316,13 +325,18 @@ }); async function load() { + creditError.value = ''; try { credit.value = await api.masterCredit(); - } catch (e) {} + } catch (e) { + creditError.value = (e && (e.message || e.msg)) || '信用档案加载失败,请重试'; + } try { const res = await api.masterAppeals(); list.value = res.list || []; - } catch (e) {} + } catch (e) { + uni.showToast({ title: (e && (e.message || e.msg)) || '申诉记录加载失败', icon: 'none' }); + } } const uploading = ref(false); @@ -1020,4 +1034,19 @@ border-radius: $yjyf-radius-sm; background: $yjyf-bg-page; } + .retry-btn { + margin-top: 24rpx; + padding: 14rpx 40rpx; + border-radius: $yjyf-radius-pill; + background: $yjyf-primary-bg; + border: 1rpx solid $yjyf-primary; + display: inline-flex; + align-items: center; + justify-content: center; + } + .retry-txt { + font-size: $yjyf-font-sub; + color: $yjyf-primary; + font-weight: 600; + } diff --git a/yjyf-app/pages/master/detail.vue b/yjyf-app/pages/master/detail.vue index 5f38ebb..e10cd8a 100644 --- a/yjyf-app/pages/master/detail.vue +++ b/yjyf-app/pages/master/detail.vue @@ -345,7 +345,7 @@ ¥{{ Number(order.urgent_amount).toFixed(2) }} - 平台技术服务费({{ Number(order.commission_rate || 10) }}%) + 平台技术服务费({{ order.commission_rate !== undefined && order.commission_rate !== null ? Number(order.commission_rate) : 0 }}%) -¥{{ Number(order.commission || 0).toFixed(2) }} diff --git a/yjyf-app/pages/master/location.vue b/yjyf-app/pages/master/location.vue new file mode 100644 index 0000000..6a848c9 --- /dev/null +++ b/yjyf-app/pages/master/location.vue @@ -0,0 +1,2166 @@ + + + + + diff --git a/yjyf-app/pages/master/success.vue b/yjyf-app/pages/master/success.vue index 7418111..28b5f70 100644 --- a/yjyf-app/pages/master/success.vue +++ b/yjyf-app/pages/master/success.vue @@ -14,7 +14,7 @@ - 完工后请上传现场实拍照片,并输入业主 6 位短信核销码完成验收,核销后工钱即刻结算至您的钱包。 + 完工后请上传现场实拍照片,并输入业主订单详情页中的 6 位验收核销码完成验收,核销后工钱即刻结算至您的钱包。 diff --git a/yjyf-app/pages/master/wallet.vue b/yjyf-app/pages/master/wallet.vue index 8d0aa24..e71b444 100644 --- a/yjyf-app/pages/master/wallet.vue +++ b/yjyf-app/pages/master/wallet.vue @@ -12,9 +12,13 @@ 可提现余额(元) - - - 微信零钱到账 + + + {{ heroChannelText }} {{ formatMoney(income ? income.money : 0) }} @@ -25,7 +29,7 @@ - 提现审核中 + 提现处理中 ¥{{ formatMoney(income ? income.frozen_money : 0) }} @@ -34,60 +38,136 @@ ¥{{ formatMoney(income ? income.total_income : 0) }} - - 申请提现 + + {{ withdrawEnabled ? '申请提现' : '暂停提现' }} - - - - 客户核销后工费即刻入账;单笔满 10 元可提现,通道费率 0.6%。 + + + + + + {{ pendingConfirmList.length > 1 + ? `${pendingConfirmList.length} 笔提现审核已通过,待确认收款` + : `提现审核已通过 · 实到 ¥${formatMoney(pendingConfirmList[0].arrive_amount)}` }} + + 点击右侧按钮调起微信确认收款,即刻入账零钱 + + + {{ confirmingId === pendingConfirmList[0].id ? '调起中...' : '确认收款' }} + - + + + + {{ ruleTipText }} + + + - 收支账单明细 + {{ filterType === 'withdraw' ? '提现申请记录' : '收支账单明细' }} 全部 结算 - 提现 + + 提现记录 + {{ pendingConfirmList.length }} + - - - - - {{ logTypeText(l.type) }} - 关联工单 - - {{ l.remark || '账单收支变动' }} - {{ fullTimeText(l.createtime) }} - - - - - {{ Number(l.amount) > 0 ? '+' : '' }}{{ formatMoney(l.amount) }} + + - - - 暂无对应账单明细 - + + @@ -101,17 +181,56 @@ - + 到账方式 - - 微信零钱(T+1 审核打款) + + {{ selectedChannel === 'bank' ? '银行储蓄卡(人工财务打款)' : arrivalDescText }} + + 选择提现渠道 + + + + 微信零钱 + + + + 银行储蓄卡 + + + + + + 收款银行与储蓄卡号 + + + - 提现金额(单笔最低 ¥10.00) + 提现金额(单笔 ¥{{ formatMoney(minWithdraw) }} ~ ¥{{ formatMoney(singleLimit) }}) 全部提现 @@ -120,23 +239,83 @@ class="amt-input" type="digit" v-model="withdrawAmount" - placeholder="请输入提现金额" + :placeholder="`可输入 ${formatMoney(minWithdraw)} ~ ${formatMoney(singleLimit)}`" /> - 可提余额 ¥{{ formatMoney(availMoney) }} - - 通道手续费 ¥{{ withdrawFee }} · 预计到账 ¥{{ withdrawArrive }} + + 可提余额 ¥{{ formatMoney(availMoney) }}{{ hasDailyAmountLimit ? ` · 今日剩余额度 ¥${formatMoney(todayRemain)}` : '' }}{{ hasDailyCountLimit ? ` · 今日剩余 ${remainDailyCount} 次` : '' }} + + {{ withdrawErrorHint }} + + {{ Number(withdrawFee) > 0 ? `通道手续费 ¥${withdrawFee} · ` : '免收手续费 · ' }}预计到账 ¥{{ withdrawArrive }} - {{ withdrawing ? '提交处理中...' : '确认提现' }} + {{ withdrawing ? '提交处理中...' : '确认提交提现' }} + + + + + + + + + 提现单详情 + + + + + + + {{ activeWithdraw.status_text }} + -¥{{ formatMoney(activeWithdraw.amount) }} + + + + + 实际到账净额 + ¥{{ formatMoney(activeWithdraw.arrive_amount) }} + + + 通道手续费 + ¥{{ formatMoney(activeWithdraw.fee) }} + + + 收款方式 + {{ activeWithdraw.channel === 'bank' ? '银行卡' : '微信零钱(商家转账)' }} + + + 申请时间 + {{ activeWithdraw.time_text || fullTimeText(activeWithdraw.createtime) }} + + + 到账时间 + {{ fullTimeText(activeWithdraw.confirm_time) }} + + + 转账单号 + {{ activeWithdraw.out_bill_no }} + + + 处理说明 + {{ activeWithdraw.fail_reason || activeWithdraw.audit_msg }} + + + + + {{ confirmingId === activeWithdraw.id ? '正在调起微信收款...' : '确认收款入账微信零钱' }} @@ -193,26 +372,137 @@ import { ref, computed } from 'vue'; import { onShow } from '@dcloudio/uni-app'; import api from '@/utils/api.js'; + import { getSafeWxCode, invokeMerchantTransferConfirm } from '@/utils/pay.js'; import { getNavBarMetrics } from '@/utils/system.js'; import YjyfRefreshScroll from '@/components/YjyfRefreshScroll.vue'; const navHeight = ref(getNavBarMetrics().totalNavHeight); const income = ref(null); const logs = ref([]); + const withdrawList = ref([]); + const pendingConfirmList = ref([]); const filterType = ref('all'); const showWithdrawDrawer = ref(false); const withdrawAmount = ref(''); + const selectedChannel = ref('wechat'); + const bankAccount = ref(''); const withdrawing = ref(false); + const confirmingId = ref(0); const activeLog = ref(null); + const activeWithdraw = ref(null); const availMoney = computed(() => Number(income.value?.money || 0)); + const withdrawCfg = computed(() => income.value?.withdraw_config || null); + const withdrawLimits = computed(() => income.value?.withdraw_limits || {}); + + const withdrawEnabled = computed(() => { + if (!withdrawCfg.value) return true; + return Boolean(withdrawCfg.value.enabled); + }); + const withdrawCloseTip = computed(() => withdrawCfg.value?.close_tip || '当前暂未开放提现,请稍后再试'); + const withdrawChannels = computed(() => { + const chs = withdrawCfg.value?.channels; + return Array.isArray(chs) && chs.length > 0 ? chs : ['wechat']; + }); + const heroChannelText = computed(() => { + const chs = withdrawChannels.value; + if (chs.includes('wechat') && chs.includes('bank')) return '微信零钱 / 银行卡'; + if (chs[0] === 'bank') return '银行储蓄卡到账'; + return '微信零钱到账'; + }); + const arrivalDescText = computed(() => withdrawCfg.value?.arrival_desc || '微信零钱(确认收款即到账)'); + const ruleTipText = computed(() => withdrawCfg.value?.rule_tip || ''); + + const minWithdraw = computed(() => Number(withdrawCfg.value?.min_amount ?? withdrawLimits.value.min_amount ?? 0.1)); + const singleLimit = computed(() => Number(withdrawCfg.value?.max_amount ?? withdrawLimits.value.single_limit ?? 200)); + const hasDailyAmountLimit = computed(() => Number(withdrawCfg.value?.daily_amount ?? withdrawLimits.value.daily_user_limit ?? 0) > 0); + const todayRemain = computed(() => { + if (!hasDailyAmountLimit.value) return singleLimit.value; + const rem = withdrawCfg.value?.remain_daily_amount ?? withdrawLimits.value.today_remain; + return rem !== undefined && rem !== null && Number(rem) >= 0 ? Number(rem) : singleLimit.value; + }); + const hasDailyCountLimit = computed(() => Number(withdrawCfg.value?.daily_count || 0) > 0); + const remainDailyCount = computed(() => { + if (!hasDailyCountLimit.value) return -1; + return Math.max(0, Number(withdrawCfg.value?.remain_daily_count ?? 0)); + }); + const parsedWithdraw = computed(() => { const n = Number(withdrawAmount.value); return Number.isFinite(n) && n > 0 ? Math.round(n * 100) / 100 : 0; }); - const withdrawFee = computed(() => (Math.round(parsedWithdraw.value * 0.6) / 100).toFixed(2)); - const withdrawArrive = computed(() => (parsedWithdraw.value - Number(withdrawFee.value)).toFixed(2)); + + const feeBreakdown = computed(() => { + const amt = parsedWithdraw.value; + if (amt <= 0) return { fee: 0, arrive: 0 }; + const cfg = withdrawCfg.value; + let fee = 0; + if (cfg) { + if (cfg.fee_mode === 'percent') { + fee = Math.round(amt * Number(cfg.fee_rate || 0)) / 100; + const minF = Number(cfg.fee_min || 0); + const maxF = Number(cfg.fee_max || 0); + if (minF > 0 && fee < minF) fee = minF; + if (maxF > 0 && fee > maxF) fee = maxF; + } else if (cfg.fee_mode === 'fixed') { + fee = Number(cfg.fee_fixed || 0); + } else { + fee = 0; + } + } else { + fee = Math.round(amt * Number(withdrawLimits.value.fee_rate || 0) * 100) / 100; + } + fee = Math.round(Math.max(0, fee) * 100) / 100; + if (fee >= amt) { + fee = Math.max(0, Math.round((amt - 0.1) * 100) / 100); + } + const arrive = Math.max(0, Math.round((amt - fee) * 100) / 100); + return { fee, arrive }; + }); + + const withdrawFee = computed(() => feeBreakdown.value.fee.toFixed(2)); + const withdrawArrive = computed(() => feeBreakdown.value.arrive.toFixed(2)); + + const withdrawErrorHint = computed(() => { + const amt = parsedWithdraw.value; + if (selectedChannel.value === 'bank' && !bankAccount.value.trim()) { + return amt > 0 ? '请填写收款银行与储蓄卡号' : ''; + } + if (!amt) return ''; + if (amt < minWithdraw.value) { + return `单笔提现最低 ¥${formatMoney(minWithdraw.value)}`; + } + if (amt > availMoney.value) { + return '输入金额超出当前可提现余额'; + } + if (amt > singleLimit.value) { + return `单笔提现上限为 ¥${formatMoney(singleLimit.value)}`; + } + if (hasDailyAmountLimit.value && amt > todayRemain.value) { + return `超出今日剩余可提额度 ¥${formatMoney(todayRemain.value)}`; + } + if (selectedChannel.value === 'wechat' && feeBreakdown.value.arrive < 0.1) { + return '扣除手续费后实际到账不能低于 ¥0.10'; + } + return ''; + }); + + const canSubmitWithdraw = computed(() => { + const amt = parsedWithdraw.value; + const bankOk = selectedChannel.value !== 'bank' || bankAccount.value.trim().length > 0; + const dailyAmtOk = !hasDailyAmountLimit.value || amt <= todayRemain.value; + const dailyCntOk = !hasDailyCountLimit.value || remainDailyCount.value > 0; + return !withdrawing.value + && withdrawEnabled.value + && bankOk + && dailyCntOk + && amt >= minWithdraw.value + && amt <= availMoney.value + && amt <= singleLimit.value + && dailyAmtOk + && (selectedChannel.value !== 'wechat' || feeBreakdown.value.arrive >= 0.1); + }); const filteredLogs = computed(() => { if (filterType.value === 'all') return logs.value; @@ -225,7 +515,15 @@ } function logTypeText(t) { - return { settle: '工单结算入账', withdraw: '提现申请出账', income: '奖励补贴入账', refund: '退款扣减' }[t] || '收支变动'; + return { settle: '工单结算入账', withdraw: '提现变动', income: '奖励补贴入账', refund: '退款扣减' }[t] || '收支变动'; + } + + function withdrawStatusClass(w) { + if (!w) return 'st-wait'; + if (w.can_confirm) return 'st-confirm'; + if (w.status === 'done') return 'st-done'; + if (w.status === 'reject') return 'st-reject'; + return 'st-wait'; } function fullTimeText(ts) { @@ -240,51 +538,125 @@ const res = await api.masterIncome(); income.value = res; logs.value = res.logs || []; + withdrawList.value = res.withdraw_list || []; + pendingConfirmList.value = res.pending_confirm_withdraws || []; + if (activeWithdraw.value) { + const refreshed = withdrawList.value.find(item => item.id === activeWithdraw.value.id); + activeWithdraw.value = refreshed || null; + } } catch (e) {} } function openWithdraw() { - if (availMoney.value < 10) { - uni.showToast({ title: '可提现余额满 10 元方可发起提现', icon: 'none' }); + if (!withdrawEnabled.value) { + uni.showToast({ title: withdrawCloseTip.value, icon: 'none' }); return; } + if (hasDailyCountLimit.value && remainDailyCount.value <= 0) { + uni.showToast({ title: '今日提现申请次数已达上限,请明日再试', icon: 'none' }); + return; + } + if (availMoney.value < minWithdraw.value) { + uni.showToast({ title: `可提现余额满 ¥${formatMoney(minWithdraw.value)} 方可发起提现`, icon: 'none' }); + return; + } + if (hasDailyAmountLimit.value && todayRemain.value < minWithdraw.value) { + uni.showToast({ title: `今日剩余提现额度为 ¥${formatMoney(todayRemain.value)},请明日再试`, icon: 'none' }); + return; + } + const chs = withdrawChannels.value; + if (!chs.includes(selectedChannel.value)) { + selectedChannel.value = chs[0] || 'wechat'; + } withdrawAmount.value = ''; showWithdrawDrawer.value = true; } function fillAllBalance() { if (availMoney.value <= 0) return; - withdrawAmount.value = availMoney.value.toFixed(2); + const caps = [availMoney.value, singleLimit.value]; + if (hasDailyAmountLimit.value) { + caps.push(todayRemain.value); + } + const maxAllowed = Math.min(...caps); + if (maxAllowed < minWithdraw.value) { + uni.showToast({ title: `当前可提上限不足 ¥${formatMoney(minWithdraw.value)}`, icon: 'none' }); + return; + } + const rounded = Math.floor(maxAllowed * 100) / 100; + withdrawAmount.value = rounded.toFixed(2); + if (availMoney.value > rounded) { + uni.showToast({ title: `受单笔/单日额度限制,本次最多可提 ¥${rounded.toFixed(2)}`, icon: 'none' }); + } } async function submitWithdraw() { - if (withdrawing.value) return; + if (withdrawing.value || !canSubmitWithdraw.value) return; const amt = parsedWithdraw.value; - if (amt < 10) { - uni.showToast({ title: '单次提现金额不少于 10 元', icon: 'none' }); - return; - } - if (amt > availMoney.value) { - uni.showToast({ title: '输入金额超出可提现余额', icon: 'none' }); - return; - } withdrawing.value = true; try { - const res = await api.masterWithdraw({ amount: amt, channel: 'wechat' }); + const wxCode = selectedChannel.value === 'wechat' ? await getSafeWxCode() : ''; + const res = await api.masterWithdraw({ + amount: amt, + channel: selectedChannel.value, + account: selectedChannel.value === 'bank' ? bankAccount.value.trim() : '微信零钱', + wx_code: wxCode + }); const arriveVal = res?.arrive_amount ?? res?.arrive ?? withdrawArrive.value; showWithdrawDrawer.value = false; - uni.showToast({ title: `提现已提交,预计到账 ¥${arriveVal}`, icon: 'none' }); - load(); + filterType.value = 'withdraw'; + await load(); + + // 若命中后台「小额免审自动发起转账」,立即拉起微信官方确认收款半屏页;若自动发起失败,必须明确告知真实中文原因 + if (res && res.can_confirm && res.withdraw_id) { + await handleConfirmReceive({ id: res.withdraw_id }); + } else if (res && res.auto_fail_reason) { + uni.showModal({ + title: '提现已受理但自动打款失败', + content: `本次小额免审自动发起微信转账失败:${res.auto_fail_reason}。提现单已保留,请等待或联系平台财务核查处理。`, + showCancel: false, + confirmText: '知道了' + }); + } else { + const suffix = selectedChannel.value === 'bank' ? '财务审核后打款至银行卡' : '审核后请在此确认收款'; + uni.showToast({ title: `已提交(预计到账 ¥${formatMoney(arriveVal)}),${suffix}`, icon: 'none' }); + } } catch (e) { + if (e && e.code === undefined && !e.handledToast && e.message) { + uni.showToast({ title: e.message, icon: 'none' }); + } } finally { withdrawing.value = false; } } + async function handleConfirmReceive(item, closeDrawerOnDone = false) { + if (!item || !item.id || confirmingId.value > 0) return; + confirmingId.value = item.id; + try { + const res = await invokeMerchantTransferConfirm(item.id); + if (closeDrawerOnDone && res && res.done) { + activeWithdraw.value = null; + } + await load(); + } catch (e) { + if (e && e.code === undefined && !e.handledToast && e.message) { + uni.showToast({ title: e.message, icon: 'none' }); + } + await load(); + } finally { + confirmingId.value = 0; + } + } + function openLogDetail(item) { activeLog.value = item; } + function openWithdrawDetail(item) { + activeWithdraw.value = item; + } + function goOrderDetail(orderId) { activeLog.value = null; uni.navigateTo({ url: `/pages/master/detail?id=${orderId}` }); @@ -400,12 +772,68 @@ justify-content: center; box-shadow: 0 8rpx 20rpx rgba(0, 0, 0, 0.1); } + .withdraw-cta.disabled { + opacity: 0.65; + } .withdraw-cta-txt { color: $yjyf-primary; font-size: $yjyf-font-body; font-weight: 700; } + /* 待确认收款提醒横幅 */ + .receive-banner { + margin: 20rpx $yjyf-space-md 0; + padding: 20rpx 22rpx; + background: $yjyf-bg-card; + border: 2rpx solid rgba($yjyf-success, 0.3); + border-radius: $yjyf-radius-md; + box-shadow: $yjyf-shadow-card; + display: flex; + align-items: center; + } + .receive-ico { + width: 56rpx; + height: 56rpx; + margin-right: 16rpx; + flex-shrink: 0; + } + .receive-main { + flex: 1; + min-width: 0; + margin-right: 16rpx; + display: flex; + flex-direction: column; + } + .receive-title { + font-size: $yjyf-font-body; + font-weight: 700; + color: $yjyf-text-main; + line-height: 1.35; + } + .receive-sub { + margin-top: 4rpx; + font-size: $yjyf-font-micro; + color: $yjyf-text-secondary; + line-height: 1.4; + } + .receive-btn { + height: 60rpx; + padding: 0 24rpx; + border-radius: $yjyf-radius-pill; + background: $yjyf-success; + color: $yjyf-bg-card; + font-size: $yjyf-font-sub; + font-weight: 600; + display: inline-flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + } + .receive-btn.disabled { + opacity: 0.55; + } + /* 规则提示条 */ .tips-bar { margin: 20rpx $yjyf-space-md 0; @@ -469,6 +897,74 @@ color: $yjyf-primary; font-weight: 600; } + .ft-badge { + margin-left: 6rpx; + min-width: 30rpx; + height: 30rpx; + padding: 0 8rpx; + border-radius: $yjyf-radius-pill; + background: $yjyf-success; + color: $yjyf-bg-card; + font-size: $yjyf-font-micro; + font-weight: 700; + display: inline-flex; + align-items: center; + justify-content: center; + } + + .wd-row-ico { + width: 52rpx; + height: 52rpx; + margin-right: 16rpx; + flex-shrink: 0; + } + .wd-status-tag { + height: 36rpx; + padding: 0 12rpx; + border-radius: 8rpx; + font-size: $yjyf-font-micro; + font-weight: 600; + display: inline-flex; + align-items: center; + } + .wd-status-tag.st-confirm { + background: rgba($yjyf-success, 0.12); + color: $yjyf-success; + } + .wd-status-tag.st-done { + background: $yjyf-bg-page; + color: $yjyf-text-secondary; + } + .wd-status-tag.st-reject { + background: rgba($yjyf-danger, 0.1); + color: $yjyf-danger; + } + .wd-status-tag.st-wait { + background: rgba($yjyf-warning, 0.14); + color: $yjyf-warning; + } + .wd-fail-txt { + margin-top: 6rpx; + font-size: $yjyf-font-micro; + color: $yjyf-danger; + line-height: 1.4; + } + .wd-confirm-pill { + margin-top: 10rpx; + height: 48rpx; + padding: 0 20rpx; + border-radius: $yjyf-radius-pill; + background: $yjyf-success; + color: $yjyf-bg-card; + font-size: $yjyf-font-micro; + font-weight: 600; + display: inline-flex; + align-items: center; + justify-content: center; + } + .wd-confirm-pill.disabled { + opacity: 0.55; + } .log-list { display: flex; @@ -636,6 +1132,53 @@ font-weight: 600; color: $yjyf-text-main; } + .channel-switch-box { + display: flex; + align-items: center; + justify-content: space-between; + padding: 16rpx 24rpx; + border-radius: $yjyf-radius-sm; + background: $yjyf-bg-page; + } + .channel-pills { + display: flex; + align-items: center; + gap: 16rpx; + } + .ch-pill { + height: 60rpx; + padding: 0 22rpx; + border-radius: $yjyf-radius-pill; + background: $yjyf-bg-card; + border: 2rpx solid $yjyf-border-color; + display: inline-flex; + align-items: center; + font-size: $yjyf-font-sub; + color: $yjyf-text-regular; + } + .ch-pill.active { + border-color: $yjyf-primary; + background: $yjyf-primary-bg; + color: $yjyf-primary; + font-weight: 600; + } + .bank-input-box { + margin-top: 20rpx; + padding: 20rpx 24rpx; + border-radius: $yjyf-radius-md; + border: 1rpx solid $yjyf-border-color; + } + .bank-label { + display: block; + font-size: $yjyf-font-sub; + color: $yjyf-text-secondary; + margin-bottom: 12rpx; + } + .bank-input { + height: 60rpx; + font-size: $yjyf-font-body; + color: $yjyf-text-main; + } .amt-box { margin-top: 24rpx; @@ -688,6 +1231,9 @@ font-size: $yjyf-font-micro; color: $yjyf-text-secondary; } + .meta-item.warn { + color: $yjyf-danger; + } .detail-hero { padding: 24rpx 0 28rpx; @@ -734,6 +1280,10 @@ .d-val.multi { line-height: 1.5; } + .d-val.highlight { + color: $yjyf-success; + font-weight: 700; + } .drawer-submit { margin-top: 28rpx; diff --git a/yjyf-app/pages/order/create.vue b/yjyf-app/pages/order/create.vue index e8841e7..c7c69f8 100644 --- a/yjyf-app/pages/order/create.vue +++ b/yjyf-app/pages/order/create.vue @@ -233,6 +233,7 @@ import { ref, reactive, watch, computed } from 'vue'; import { onLoad, onShow, onUnload } from '@dcloudio/uni-app'; import api from '@/utils/api.js'; + import { executeOrderWxPay } from '@/utils/pay.js'; import { isLogin, toLogin } from '@/utils/request.js'; import RegionPickerDrawer from '@/components/RegionPickerDrawer.vue'; @@ -447,7 +448,9 @@ user_price: userPrice.value }); amount.value = (res && res.amount) ? res.amount : res; - } catch (e) {} + } catch (e) { + console.warn('[CreateOrder] calc failed:', e); + } } function selectMode(key) { @@ -520,8 +523,8 @@ uni.redirectTo({ url: `/pages/order/detail?id=${createdOrderId}` }); return; } - // 一口价或自主定价调起支付 - await api.orderPay({ order_id: createdOrderId, pay_type: 'wxpay' }); + // 一口价或自主定价调起微信支付 + await executeOrderWxPay(api.orderPay, { order_id: createdOrderId }); uni.showToast({ title: '下单并支付成功' }); setTimeout(() => { submitting.value = false; @@ -529,14 +532,17 @@ }, 600); } catch (e) { if (createdOrderId) { - uni.showToast({ title: '订单已创建,请在详情页完成支付', icon: 'none' }); + if (!e || !e.unsupportedEnv) { + const reason = (e && e.message) ? `订单已创建(${e.message})` : '订单已创建,请在详情页完成支付'; + uni.showToast({ title: reason, icon: 'none' }); + } setTimeout(() => { submitting.value = false; uni.redirectTo({ url: `/pages/order/detail?id=${createdOrderId}` }); - }, 600); + }, 1200); } else { submitting.value = false; - uni.showToast({ title: e.message || '下单失败', icon: 'none' }); + uni.showToast({ title: (e && e.message) || '下单失败', icon: 'none' }); } } } diff --git a/yjyf-app/pages/order/detail.vue b/yjyf-app/pages/order/detail.vue index ae94b0a..5e560dd 100644 --- a/yjyf-app/pages/order/detail.vue +++ b/yjyf-app/pages/order/detail.vue @@ -414,6 +414,7 @@ import { onLoad, onShow } from '@dcloudio/uni-app'; import YjyfNavBar from '@/components/YjyfNavBar.vue'; import api from '@/utils/api.js'; + import { executeOrderWxPay } from '@/utils/pay.js'; const order = ref(null); const loading = ref(true); @@ -581,10 +582,13 @@ if (!r.confirm || actionBusy.value) return; actionBusy.value = true; try { - await api.orderChooseQuote({ order_id: orderId, quote_id: q.id, pay_type: 'mock' }); + await executeOrderWxPay(api.orderChooseQuote, { order_id: orderId, quote_id: q.id }); uni.showToast({ title: '已选定师傅' }); await load(); } catch (e) { + if (e && e.code === undefined && !e.handledToast && !e.unsupportedEnv && e.message) { + uni.showToast({ title: e.message, icon: 'none' }); + } } finally { actionBusy.value = false; } @@ -602,10 +606,13 @@ if (!r.confirm || actionBusy.value) return; actionBusy.value = true; try { - await api.orderPayDiff({ order_id: orderId }); + await executeOrderWxPay(api.orderPayDiff, { order_id: orderId }); uni.showToast({ title: '支付成功' }); await load(); } catch (e) { + if (e && e.code === undefined && !e.handledToast && !e.unsupportedEnv && e.message) { + uni.showToast({ title: e.message, icon: 'none' }); + } } finally { actionBusy.value = false; } @@ -626,6 +633,9 @@ uni.showToast({ title: '已取消', icon: 'none' }); await load(); } catch (e) { + if (e && e.code === undefined && !e.handledToast && e.message) { + uni.showToast({ title: e.message, icon: 'none' }); + } } finally { actionBusy.value = false; } @@ -637,10 +647,13 @@ if (actionBusy.value) return; actionBusy.value = true; try { - await api.orderPay({ order_id: orderId, pay_type: 'mock' }); + await executeOrderWxPay(api.orderPay, { order_id: orderId }); uni.showToast({ title: '支付成功', icon: 'success' }); await load(); } catch (e) { + if (e && e.code === undefined && !e.handledToast && !e.unsupportedEnv && e.message) { + uni.showToast({ title: e.message, icon: 'none' }); + } } finally { actionBusy.value = false; } diff --git a/yjyf-app/pages/order/goods-gallery.vue b/yjyf-app/pages/order/goods-gallery.vue index 1ca779f..3f46ef7 100644 --- a/yjyf-app/pages/order/goods-gallery.vue +++ b/yjyf-app/pages/order/goods-gallery.vue @@ -67,8 +67,8 @@ - 暂无个人商品图库记录 - 点击下方“上传新图”可将常用商品实拍图存入图库复用 + 暂无历史上传的商品图片 + 添加商品时从相册上传的图片会自动保存在此,方便下次直接选用 @@ -81,7 +81,7 @@ @@ -96,11 +96,11 @@ @@ -137,35 +137,18 @@ > - {{ deleting ? '删除中...' : (deleteIds.length > 0 ? `删除选中图片(${deleteIds.length})` : '请勾选要删除的图库图片') }} + {{ deleting ? '删除中...' : (deleteIds.length > 0 ? `删除选中图片(${deleteIds.length})` : '请勾选要删除的历史图片') }} - - - {{ uploading ? '上传中...' : '上传新商品图' }} - - - - - - {{ uploading ? '上传中...' : '上传新图' }} - + - {{ selectedUrls.length > 0 ? `确定(已选 ${selectedUrls.length} 张)` : '确定' }} + {{ selectedItems.length > 0 ? `确定(已选 ${selectedItems.length} 张)` : '确定' }} @@ -185,7 +168,21 @@ const keyword = ref(''); const maxCount = ref(9); - const selectedUrls = ref([]); + const selectedItems = ref([]); + + function getItemKey(item) { + if (!item) return ''; + const src = item.source || 'lib'; + const id = item.id || 0; + const url = item.image_url ? String(item.image_url).trim() : ''; + return `${src}_${id}_${url}`; + } + + function isSelectedItem(item) { + const key = getItemKey(item); + if (!key) return false; + return selectedItems.value.some((x) => x.key === key); + } const isBrowseScene = ref(false); const isManageMode = ref(false); const deleteIds = ref([]); @@ -217,6 +214,7 @@ try { const res = await api.goodsGallery({ cat_id: activeCatId.value || 0, + prefer_cat_id: initOrderCatId.value || 0, keyword: keyword.value.trim() }); list.value = (res && Array.isArray(res.list)) ? res.list : []; @@ -266,16 +264,23 @@ previewPhoto(item.image_url); return; } - const idx = selectedUrls.value.indexOf(item.image_url); + const key = getItemKey(item); + const url = item && item.image_url ? String(item.image_url).trim() : ''; + if (!url) return; + const idx = selectedItems.value.findIndex((x) => x.key === key); if (idx > -1) { - selectedUrls.value.splice(idx, 1); + selectedItems.value.splice(idx, 1); return; } - if (selectedUrls.value.length >= maxCount.value) { + if (selectedItems.value.some((x) => x.url === url)) { + uni.showToast({ title: '该图片已在已选列表中', icon: 'none' }); + return; + } + if (selectedItems.value.length >= maxCount.value) { uni.showToast({ title: `最多可选择 ${maxCount.value} 张商品图片`, icon: 'none' }); return; } - selectedUrls.value.push(item.image_url); + selectedItems.value.push({ key, url }); } function previewPhoto(url) { @@ -286,51 +291,11 @@ }); } - function handleUploadNew() { - if (uploading.value) return; - uni.chooseImage({ - count: Math.min(9, maxCount.value), - sourceType: ['album', 'camera'], - success: async (res) => { - if (!res.tempFilePaths || !res.tempFilePaths.length) return; - uploading.value = true; - uni.showLoading({ title: '上传入库中...', mask: true }); - try { - const uploadedUrls = []; - for (const tempPath of res.tempFilePaths) { - const up = await api.upload(tempPath, 'order'); - const u = up && (up.fullurl || up.url); - if (u) uploadedUrls.push(u); - } - if (uploadedUrls.length > 0) { - const saveRes = await api.saveGoodsGallery({ - cat_id: activeCatId.value || initOrderCatId.value || 0, - images: JSON.stringify(uploadedUrls) - }); - const savedList = (saveRes && Array.isArray(saveRes.list)) ? saveRes.list : []; - savedList.forEach(s => { - if (s.image_url && !selectedUrls.value.includes(s.image_url) && selectedUrls.value.length < maxCount.value) { - selectedUrls.value.push(s.image_url); - } - }); - uni.showToast({ title: '已存入商品图库', icon: 'success' }); - await fetchList(); - } - } catch (e) { - uni.showToast({ title: (e && e.msg) ? e.msg : '上传入库失败', icon: 'none' }); - } finally { - uploading.value = false; - uni.hideLoading(); - } - } - }); - } - function handleBatchDelete() { if (deleting.value || deleteIds.value.length === 0) return; uni.showModal({ - title: '移除图库图片', - content: `确认从个人商品图库移除选中的 ${deleteIds.value.length} 张图片?`, + title: '移除历史图片', + content: `确认移除选中的 ${deleteIds.value.length} 张历史商品图片?`, confirmColor: '#E8331B', success: async (r) => { if (!r.confirm || deleting.value) return; @@ -340,7 +305,7 @@ .filter(x => deleteIds.value.includes(x.id)) .map(x => x.image_url); await api.deleteGoodsGallery({ ids: deleteIds.value.join(',') }); - selectedUrls.value = selectedUrls.value.filter(u => !deletingUrls.includes(u)); + selectedItems.value = selectedItems.value.filter((x) => !deletingUrls.includes(x.url)); deleteIds.value = []; isManageMode.value = false; uni.showToast({ title: '已移除', icon: 'none' }); @@ -356,12 +321,17 @@ function handleConfirm() { if (confirming.value) return; - if (selectedUrls.value.length === 0) { - uni.showToast({ title: '请先勾选或上传商品图片', icon: 'none' }); + if (selectedItems.value.length === 0) { + uni.showToast({ title: '请先勾选需要的商品图片', icon: 'none' }); return; } confirming.value = true; - const picked = [...selectedUrls.value]; + const picked = []; + selectedItems.value.forEach((x) => { + if (x.url && !picked.includes(x.url)) { + picked.push(x.url); + } + }); uni.setStorageSync('yjyf_picked_gallery_images', picked); uni.$emit('yjyf:selectGalleryImages', picked); uni.navigateBack({ diff --git a/yjyf-app/pages/order/goods-template.vue b/yjyf-app/pages/order/goods-template.vue new file mode 100644 index 0000000..d03351a --- /dev/null +++ b/yjyf-app/pages/order/goods-template.vue @@ -0,0 +1,1026 @@ + + + + + diff --git a/yjyf-app/pages/order/quick.vue b/yjyf-app/pages/order/quick.vue index 3c2f906..ddb0f82 100644 --- a/yjyf-app/pages/order/quick.vue +++ b/yjyf-app/pages/order/quick.vue @@ -75,113 +75,94 @@ - - - - + + + + + + + + + + + + * 添加图片 - - - - - - - {{ g.images.length < 9 ? `共${g.images.length}/9张 · 添加` : '已满9张' }} - - - + - + - - - - - - - - - - - 商品{{ imgIdx + 1 }} - - - - - - - - - 环境{{ envIdx + 1 }} - - - - - {{ (g.images || []).length }}/9 - - - - - @@ -287,7 +268,7 @@
      ' + seq + '' + + '
      ' + + '' + + '' + + '' + + '' + + '' + + '
      ' + + '
      ¥