Zory 3 тижнів тому
батько
коміт
42508ebfe8

+ 16 - 0
app/controller/IndexController.php

@@ -2,6 +2,9 @@
 
 namespace app\controller;
 
+use app\extra\ip2region\IpSearch;
+use app\extra\ip2region\xdb\IPv4;
+use app\extra\ip2region\xdb\Searcher;
 use app\model\saas\SaasChatStore;
 use app\model\saas\SaasUserOpen;
 use support\Request;
@@ -56,4 +59,17 @@ class IndexController
         return json(['code' => 0, 'msg' => 'ok']);
     }
 
+    public function protocol()
+    {
+        $name = input("name","privacy");
+        if ($name == "privacy") {
+            $data = sConf('service.privacy');
+        } else {
+            $data = sConf('service.agreements');
+        }
+//        $ipAddress = (new IpSearch)->getIpArea("223.73.60.241");
+//        print_r($ipAddress);
+        return view('protocol',['data' => $data,'name' => $name]);
+    }
+
 }

+ 3 - 1
app/controller/api/Solution.php

@@ -101,6 +101,7 @@ class Solution extends Base
                         $goodsImg = is_string($goods['image_list'])?json_decode($goods['image_list'],true):$goods['image_list'];
                         $order->insertGetId([
                             "out_order_no"  => $orderSn,
+                            "item_order_id" => $msgData['item_order_info_list'][0]['item_order_id']?:'0',
                             "order_sn"  => $msgData['order_id'],
                             "openid"    => $msgData['open_id'],
                             "goods_id"  => $goods['id'],
@@ -117,7 +118,8 @@ class Solution extends Base
                             "scene"         => $cp_extra['scene']??'',
                             "mobile"        => $msgData['phone_num']??'',
                             "pay_money"     => $msgData['total_amount']??0,
-                            "is_show"       => 0
+                            "is_show"       => 0,
+                            "is_new"        => 1, // 三方码订单
                         ]);
                         $itemData = [];
                         foreach ($msgData['item_order_info_list'] as $key=>$val) {

+ 150 - 0
app/controller/merchant/Black.php

@@ -0,0 +1,150 @@
+<?php
+
+namespace app\controller\merchant;
+
+use app\extra\basic\Base;
+use app\middleware\AuthMiddleware;
+use app\model\saas\SaasOrderArea;
+use app\model\saas\SaasUserOpen;
+use app\service\saas\OrderAreaService;
+use app\service\saas\UserOpenService;
+use DI\Attribute\Inject;
+use LinFly\Annotation\Attributes\Route\Controller;
+use LinFly\Annotation\Attributes\Route\GetMapping;
+use LinFly\Annotation\Attributes\Route\Middleware;
+use LinFly\Annotation\Attributes\Route\PostMapping;
+use support\Request;
+use support\Response;
+
+
+#[Controller("/api/merchant/black"),Middleware(AuthMiddleware::class)]
+class Black extends Base
+{
+
+    #[Inject]
+    protected UserOpenService $service;
+
+    #[Inject]
+    protected SaasUserOpen $model;
+
+    #[Inject]
+    protected OrderAreaService $serviceArea;
+
+    #[Inject]
+    protected SaasOrderArea $modelArea;
+
+    #[GetMapping('list')]
+    public function getDataList(Request $request): Response
+    {
+        try {
+            $param = $request->all();
+            $param['poi_id'] = $request->user['store_id'];
+            $param['black'] = 1;
+            $data = $this->service->setModel()->getList($param);
+            return successTrans(100010,pageFormat($data),200);
+        } catch (\Throwable $th) {
+            return error($th->getMessage());
+        }
+    }
+
+
+    #[PostMapping('undel')]
+    public function setUserBlack(Request $request): Response
+    {
+        try {
+            $param = $this->_valid([
+                "id.require" => trans("empty.require"),
+            ],$request->method());
+            if (!is_array($param)) return error($param);
+            $user = $this->model->where("id",$param["id"])->findOrEmpty();
+            if ($user->isEmpty()) return errorTrans("error.data");
+            $user->is_black = 0;
+            $user->poi_id = 0;
+            $state = $user->save();
+            if (!$state) return errorTrans("error.data");
+            return successTrans("success.data");
+        } catch (\Throwable $th) {
+            return error($th->getMessage());
+        }
+    }
+
+    #[GetMapping('area')]
+    public function getBlackArea(Request $request): Response
+    {
+        try {
+            $param = $request->all();
+            $param['poi_id'] = $request->user['store_id'];
+            $data = $this->serviceArea->setModel()->getList($param);
+            return successTrans(100010,pageFormat($data),200);
+        } catch (\Throwable $th) {
+            return error($th->getMessage());
+        }
+    }
+
+    #[PostMapping('area/save')]
+    public function setBlackArea(Request $request): Response
+    {
+        try {
+            $param = $this->_valid([
+                "name.require" => trans("empty.name.require"),
+            ],$request->method());
+            if (!is_array($param)) return error($param);
+            $param['poi_id'] = $request->user['store_id'];
+            $data = $this->modelArea->where($param)->findOrEmpty();
+            if (!$data->isEmpty()) return error("请勿重复添加");
+            $state = $data->insertGetId($param);
+            if (!$state) return errorTrans("error.data");
+            return successTrans("success.data");
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+    #[PostMapping('area/del')]
+    public function delBlackArea(Request $request): Response
+    {
+        try {
+            $param = $this->_valid([
+                "id.require" => trans("empty.name.require"),
+            ],$request->method());
+            if (!is_array($param)) return error($param);
+            $data = $this->modelArea->where($param)->findOrEmpty();
+            if ($data->isEmpty()) return errorTrans("error.data");
+            $state = $data->delete();
+            if (!$state) return errorTrans("error.data");
+            return successTrans("success.data");
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+    /**
+     * 三级城市信息
+     * @param Request $request
+     * @return Response
+     */
+    #[GetMapping("city")]
+    public function getCityJson(Request $request): Response
+    {
+        try {
+            $data = json_decode(file_get_contents(base_path()."/city.json"),true);
+            $cityData = [];
+            foreach ($data as $key=>$item){
+                $cityData[$key]['text'] = $item['text'];
+                $cityData[$key]['value'] = $item['value'];
+                $children = [];
+                foreach ($item['children'] as $ckey=>$child){
+                    $children[$ckey]['text'] = $child['text'];
+                    $children[$ckey]['value'] = $child['value'];
+                    $children[$ckey]['children'] = [];
+                }
+                $cityData[$key]['children'] = $children;
+            }
+            return successTrans("success.data",$cityData);
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+
+}

+ 51 - 1
app/controller/merchant/Order.php

@@ -3,12 +3,14 @@
 namespace app\controller\merchant;
 
 use app\extra\basic\Base;
+use app\extra\dyLife\data\BaseData;
 use app\extra\dyLife\data\OrderData;
 use app\middleware\AuthMiddleware;
 use app\model\saas\SaasOrder;
 use app\model\saas\SaasOrderAddress;
 use app\model\saas\SaasOrderItem;
 use app\model\saas\SaasStore;
+use app\model\saas\SaasUserOpen;
 use app\model\system\SystemImport;
 use app\service\saas\GoodsService;
 use app\service\saas\OrderService;
@@ -43,7 +45,7 @@ class Order extends Base
             $param['poi_id'] = $request->user['store_id'];
             $productType = $this->goodsService->productType();
             $data = $this->service->setModel()->getList($param,['product' => function($query) use($productType){
-                $query->field("product_id,product_name,product_type")->append(['types'])->withAttr(['types' => function($query,$resp) use($productType){
+                $query->field("product_id,product_name,product_type,is_new,price,line_price")->append(['types'])->withAttr(['types' => function($query,$resp) use($productType){
                     $productTypeArr = [];
                     foreach ($productType as $val) {
                         $productTypeArr[$val['key']] = $val['name'];
@@ -230,4 +232,52 @@ class Order extends Base
         }
     }
 
+    #[PostMapping("refund")]
+    public function setOrderRefund(Request $request): Response
+    {
+        try {
+            $param = $this->_valid([
+                "order.require"         => trans("empty.require"),
+            ],$request->method());
+            if (!is_array($param)) return error($param);
+            $order = (new SaasOrder)->where("out_order_no",$param["order"])->with(['poi'])->findOrEmpty();
+            if ($order->isEmpty()) return error("关联订单错误");
+            if ($order['status'] <> 1) return error("关联订单错误");
+            if (empty($order['item_order_id'])) return error("订单数据不完整");
+            $data = (new OrderData)->config([
+                "appid"     => sConf("wechat.mini_appid"),
+                "secret"    => sConf("wechat.mini_secret"),
+            ])->token()->refundOrderOp($order->toArray());
+            echo getDateFull()."==={$order["order_sn"]}===手动发起退款\n";
+            print_r($data);
+            if (empty($data['refund_id'])) return error("发起退款失败");
+            $order->status = 3;
+            $order->refund_at = getDateFull();
+            $state = $order->save();
+            if (!$state) return errorTrans("error.data");
+            return successTrans("success.data");
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+    #[PostMapping("black")]
+    public function setOrderBlack(Request $request): Response
+    {
+        try {
+            $param = $this->_valid([
+                "order.require"         => trans("empty.require"),
+            ],$request->method());
+            if (!is_array($param)) return error($param);
+            $order = (new SaasOrder)->where("out_order_no",$param["order"])->with(['user'])->findOrEmpty();
+            if ($order->isEmpty()) return error("关联订单错误");
+            if ($order['user']['is_black'] == 1) return error("该用户已在黑名单,无需重复操作");
+            $state = (new SaasUserOpen)->where("openid",$order['openid'])->save(['is_black' => 1,'poi_id' => $order['poi_id']]);
+            if (!$state) return errorTrans("error.data");
+            return successTrans("success.data");
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
 }

+ 1 - 0
app/controller/service/Chat.php

@@ -199,6 +199,7 @@ class Chat extends Base
             $user = (new SystemUser)->where("id",$request->user['id'])->findOrEmpty();
             if ($user->isEmpty()) return error("数据不存在");
             $user->is_line = $param['status'];
+            $user->last_active_at = getDateFull();
             $state = $user->save();
             if (!$state) return error('操作失败');
             return success("ok",['line' => $user['is_line']]);

+ 7 - 7
app/controller/service/Heart.php

@@ -21,13 +21,13 @@ class Heart extends Base
     public function setHeartBeat(Request $request): Response
     {
         try {
-//            $user = (new SystemUser)->where("id",$request->user['id'])->findOrEmpty();
-//            if ($user->isEmpty()) return errorTrans("empty.data");
-//            if ($user['is_line'] == 0) {
-//                $user->is_line = 1;
-//            }
-//            $user->last_active_at = getDateFull();
-//            $user->save();
+            $user = (new SystemUser)->where("id",$request->user['id'])->findOrEmpty();
+            if ($user->isEmpty()) return errorTrans("empty.data");
+            if ($user['is_line'] == 0) {
+                $user->is_line = 1;
+            }
+            $user->last_active_at = getDateFull();
+            $user->save();
             return success("ok");
         } catch (\Throwable $throwable) {
             return error($throwable->getMessage());

+ 63 - 0
app/controller/v2/Complaint.php

@@ -0,0 +1,63 @@
+<?php
+
+namespace app\controller\v2;
+
+use app\extra\basic\Base;
+use app\middleware\AuthMiddleware;
+use app\model\saas\SaasComplaint;
+use app\service\saas\ComplaintService;
+use DI\Attribute\Inject;
+use LinFly\Annotation\Attributes\Route\Controller;
+use LinFly\Annotation\Attributes\Route\GetMapping;
+use LinFly\Annotation\Attributes\Route\Middleware;
+use support\Request;
+use support\Response;
+
+
+#[Controller("/v2/complaint"),Middleware(AuthMiddleware::class)]
+class Complaint extends Base
+{
+
+    #[Inject]
+    protected ComplaintService $service;
+
+    #[Inject]
+    protected SaasComplaint $model;
+
+    #[GetMapping('list')]
+    public function getDataList(Request $request): Response
+    {
+        try {
+            $param = $request->all();
+            if (!empty($param['size'])) {
+                $param['pageSize'] = $param['size'];
+            }
+//            $param['openid'] = $request->user['openid'];
+            $data = $this->service->setModel()->getList($param);
+            return successTrans("success.data",pageFormat($data));
+        } catch (\Throwable $th) {
+            return error($th->getMessage());
+        }
+    }
+
+    #[GetMapping('detail')]
+    public function getDataDetail(Request $request): Response
+    {
+        try {
+            $param = $this->_valid([
+                "id.require"    => trans("empty.require")
+            ],$request->method());
+            if (!is_array($param)) return error($param);
+            $data = $this->model->where("id",$param["id"])->with(['store' => function($query){
+                $query->field("poi_id,poi_name,poi_address,poi_city");
+            }])->findOrEmpty();
+            if ($data->isEmpty()) return errorTrans("empty.data");
+//            if ($data['openid'] <> $request->user['openid']) return error("非法操作");
+            $data['mobile'] = hide_mobile($data['mobile']);
+            return successTrans("success.data",$data->toArray());
+        } catch (\Throwable $th) {
+            return error($th->getMessage());
+        }
+    }
+
+}

+ 21 - 1
app/controller/v2/Goods.php

@@ -3,8 +3,10 @@
 namespace app\controller\v2;
 
 use app\extra\basic\Base;
+use app\extra\ip2region\IpSearch;
 use app\middleware\AuthMiddleware;
 use app\model\saas\SaasGoods;
+use app\model\saas\SaasOrderArea;
 use app\model\saas\SaasUserOpen;
 use DI\Attribute\Inject;
 use LinFly\Annotation\Attributes\Route\Controller;
@@ -65,13 +67,31 @@ class Goods extends Base
     public function checkGoodsUser (Request $request): Response
     {
         try {
+            $param = $this->_valid([
+                "poi.default"       => ""
+            ],$request->method());
+            if (!is_array($param)) return error($param);
             $user = (new SaasUserOpen)->where(['openid' => $request->user['openid']])->findOrEmpty();
             if ($user->isEmpty()) return error("数据错误");
             $userState = 1;
+            $isBuy = 1;
             if (empty($user['avatar'])) {
                 $userState = 0;
             }
-            return success("ok",['user' => $userState]);
+            if ($user['is_black'] == 1) {
+                $isBuy = 0;
+            }
+            // 查询IP
+            $userIp = $request->getRealIp();
+            $ipRegion = (new IpSearch)->getIpArea($userIp);
+            if (isset($ipRegion[2]) && !empty($param['poi'])) {
+                $areaBlack = (new SaasOrderArea)->where(['poi_id' => $param['poi'],'name' => $ipRegion[2]])->findOrEmpty();
+                if (!$areaBlack->isEmpty()) {
+                    $isBuy = 0;
+                }
+            }
+            $msg = "当前门店无库存,请咨询客服";
+            return success("ok",['user' => $userState,'buy' => $isBuy,'msg' => $msg]);
         } catch (\Throwable $throwable) {
             return error($throwable->getMessage());
         }

+ 13 - 2
app/controller/v2/Home.php

@@ -64,10 +64,21 @@ class Home extends Base
 
 
     #[GetMapping("license")]
-    public function getLicense(): Response
+    public function getLicense(Request $request): Response
     {
         try {
-            return success("ok",['img' => sConf("service.license")]);
+            $param = $this->_valid([
+                "poi.default" => 0
+            ],$request->method());
+            if (!is_array($param)) return error($param);
+            $store = (new SaasStore)->where("poi_id",$param['poi'])->findOrEmpty();
+            if ($store->isEmpty()) return error("数据错误");
+            if (empty($store['license'])) {
+                $license = sConf("service.license");
+            } else {
+                $license = $store['license'];
+            }
+            return success("ok",['img' => $license]);
         } catch (\Throwable $throwable) {
             return error($throwable->getMessage());
         }

+ 95 - 0
app/controller/v2/Order.php

@@ -5,7 +5,9 @@ namespace app\controller\v2;
 use app\extra\basic\Base;
 use app\middleware\AuthMiddleware;
 use app\model\saas\SaasComplaint;
+use app\model\saas\SaasGoodsOff;
 use app\model\saas\SaasOrder;
+use app\model\saas\SaasOrderAddress;
 use app\model\saas\SaasOrderItem;
 use app\model\saas\SaasUserOpen;
 use app\service\saas\OrderService;
@@ -142,4 +144,97 @@ class Order extends Base
         }
     }
 
+
+    #[GetMapping("goods")]
+    public function getOrderGoods(Request $request): Response
+    {
+        try {
+            $param = $this->_valid([
+                "order.require"     => trans("empty.require"),
+                "page.default"      => 0,
+                "size.require"      => 10,
+            ],$request->method());
+            if (!is_array($param)) return error($param);
+            $order = (new SaasOrder)->where("out_order_no",$param['order'])->findOrEmpty();
+            if ($order->isEmpty()) return error("该笔交易不存在");
+//            if ($order['openid'] <> $request->user['openid']) return error("非法操作");
+            $goods = (new SaasGoodsOff)->where("goods_id",$order['life_goods_id'])->append(['check','number'])->withAttr(['check' => function(){
+                return false;
+            },'number' => function () {
+                return 1;
+            }])->paginate([
+                "list_rows" => $param['size'],
+                "page"      => $param['page']
+            ]);
+            return successTrans("success.data",$goods->toArray());
+        } catch (\Throwable $th) {
+            return error($th->getMessage());
+        }
+    }
+
+    /**
+     * 提交核销信息-状态变为 进行中
+     * @param Request $request
+     * @return Response
+     */
+    #[PostMapping("done")]
+    public function setOrderAddress(Request $request): Response
+    {
+        try {
+            $param = $this->_valid([
+                "city.require"      => trans("empty.require"),
+                "mobile.require"    => trans("empty.require"),
+                "mobile.mobile"     => trans("error.mobile"),
+                "address.require"   => trans("empty.require"),
+                "nickname.require"  => trans("empty.require"),
+                "order.require"     => trans("empty.require"),
+                "goods.require"     => trans("empty.require"),
+            ],$request->method());
+            if (!is_array($param)) return error($param);
+            $cityJson = json_decode($param['city'],true);
+            $goodsJson = json_decode($param['goods'],true);
+            $param['city_text'] = $cityJson['hide'];
+            $param['city_code'] = json_encode($cityJson['list']);
+            $param['goods'] = $goodsJson[0]['id'];
+            $param['openid'] = $request->user['openid'];
+            $order = (new SaasOrder)->where("out_order_no",$param["order"])->with(['poi'])->findOrEmpty();
+            if ($order->isEmpty()) return error("关联订单错误");
+            if ($order['openid'] <> $request->user['openid']) return error("关联订单错误");
+            if ($order['status'] <> 1) return error("关联订单错误");
+            $address = (new SaasOrderAddress)->where(['openid' => $request->user['openid'],'order_sn' => $param['order']])->findOrEmpty();
+            if (!$address->isEmpty()) return error("请勿重复提交");
+            $address->setAutoData($param);
+            $order->status = 7; // 7进行中
+            $order->express_status = 1;
+            $order->save();
+            return success("提交成功");
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+    /**
+     * 发货后申请退款
+     * Array
+     * (
+     *  [order] => AL202608215835460704413
+     *  [msg] => 计划有变,暂时不需要了
+     *  [type] => alipay
+     *  [content] => {"truename":"测试","account":"13213442334"}
+     *  [content] => {"truename":"测试","bank":"13213442334","name":"中国银行xxx支行"}
+     * )
+     * @param Request $request
+     * @return Response
+     */
+    #[PostMapping("refund")]
+    public function setOrderRefund(Request $request): Response
+    {
+        try {
+            print_r($request->all());
+            return error("error");
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
 }

+ 2 - 2
app/extra/dyLife/data/BaseData.php

@@ -184,8 +184,8 @@ class BaseData extends BasicLife
                 }
                 break;
             case "show_channel":
-//                $return = (string) $data['show_channel'];
-                $return = "1";
+                $return = (string) $data['show_channel'];
+//                $return = "1";
                 break;
             case "use_date": // 使用日期 券码的可以核销日期,履约核销强依赖
                 if ($data['use_date_type'] == 1) { // 指定天数

+ 58 - 0
app/extra/dyLife/data/OrderData.php

@@ -102,4 +102,62 @@ class OrderData extends BasicLife
         return $this->curlPostApi("goodlife/v1/groupon/order/refund/apply/",$param);
     }
 
+    /**
+     * 开发者发起退款
+     * https://developer.open-douyin.com/docs/resource/zh-CN/mini-app/develop/server/locallife/general-ability/agency-trade-system/refund/apply
+     */
+    public function refundOrderOp(array $data = []): array
+    {
+        $param = [
+            "out_order_no"          => $data['out_order_no'], // AL开头订单号
+            "out_refund_no"         => $data['out_order_no']."1",
+            "order_entry_schema"    => [
+                "path"      => "pages/order/detail",
+                "params"    => json_encode(['order' => $data['out_order_no']])
+            ],
+            "notify_url"            => "https://tran.jsshuita.cn/notify/refund",
+            "item_order_detail"     => [
+                [
+                    "item_order_id" => $data['item_order_id'],
+                ]
+            ]
+        ];
+        return $this->curlPostApi("api/apps/trade/v2/refund/create_refund",$param);
+    }
+
+    /**
+     * 小程序版核销 - 三方码
+     * https://developer.open-douyin.com/docs/resource/zh-CN/mini-app/develop/server/locallife/general-ability/agency-trade-system/fulfillment/third-code/push-delivery
+     * @param array $data
+     * @return array
+     * {
+     * "data": {
+     * "verify_results": [
+     * {
+     * "certificate_id": "7676385765337382962",
+     * "item_order_id": "800014550969473945813031711",
+     * "verify_id": "7676388522927933482",
+     * "verify_time": 1787298487000
+     * }
+     * ],
+     * "error_code": 0,
+     * "description": ""
+     * },
+     * }
+     */
+    public function verifyOrder(array $data = []): array
+    {
+        $param = [
+            "delivery_status"   => 2,
+            "item_order_list"   => [
+                [
+                    "item_order_id" => "",
+                ]
+            ],
+            "out_order_no"      => "",
+            "use_all"           => true
+        ];
+        return $this->curlPostApi("api/apps/trade/v2/fulfillment/push_delivery/",$param);
+    }
+
 }

+ 25 - 0
app/extra/ip2region/IpSearch.php

@@ -0,0 +1,25 @@
+<?php
+
+namespace app\extra\ip2region;
+
+use app\extra\ip2region\xdb\IPv4;
+use app\extra\ip2region\xdb\Searcher;
+use app\extra\ip2region\xdb\SearchIp;
+
+class IpSearch
+{
+
+    /**
+     * @param string $ip
+     * @return array
+     */
+    public function getIpArea(string $ip = ""): array
+    {
+        $dbFile = __DIR__ . '/ip2region_v4.xdb';
+        $version = IPv4::default();
+        $searcher = Searcher::newWithFileOnly($version,$dbFile);
+        $region = $searcher->search($ip);
+        return explode("|",$region);
+    }
+
+}

BIN
app/extra/ip2region/ip2region_v4.xdb


+ 57 - 0
app/extra/ip2region/xdb/IPv4.php

@@ -0,0 +1,57 @@
+<?php
+
+namespace app\extra\ip2region\xdb;
+
+const IPv4VersionNo    = 4;
+
+class IPv4
+{
+    public $id;
+    public $name;
+    public $bytes;
+    public $segmentIndexSize;
+
+    private static $C = null;
+    public static function default() {
+        if (self::$C == null) {
+            // 14 = 4 + 4 + 2 + 4
+            self::$C = new self(IPv4VersionNo, 'IPv4', 4, 14);
+        }
+        return self::$C;
+    }
+
+    public function __construct($id, $name, $bytes, $segmentIndexSize) {
+        $this->id = $id;
+        $this->name = $name;
+        $this->bytes = $bytes;
+        $this->segmentIndexSize = $segmentIndexSize;
+    }
+
+    // compare the two ip bytes with the current version
+    public function ipSubCompare($ip1, $buff, $offset) {
+        // ip1: Little endian byte order encoded long from searcher.
+        // ip2: Little endian byte order read from xdb index.
+        $len  = strlen($ip1);
+        $eIdx = $offset + $len;
+        for ($i = 0, $j = $eIdx - 1; $i < $len; $i++, $j--) {
+            $i1 = ord($ip1[$i]) & 0xFF;
+            $i2 = ord($buff[$j]) & 0xFF;
+            // printf("i:%d, j:%d, i1:%d, i2:%d\n", $i, $j, $i1, $i2);
+            if ($i1 > $i2) {
+                return 1;
+            } else if ($i1 < $i2) {
+                return -1;
+            }
+        }
+
+        return 0;
+    }
+
+    public function __toString() {
+        return sprintf(
+            "{id:%d, name:%s, bytes:%d, segmentIndexSize:%d}",
+            $this->id, $this->name, $this->bytes, $this->segmentIndexSize
+        );
+    }
+
+}

+ 43 - 0
app/extra/ip2region/xdb/IPv6.php

@@ -0,0 +1,43 @@
+<?php
+
+namespace app\extra\ip2region\xdb;
+
+const IPv6VersionNo    = 6;
+
+class IPv6
+{
+
+    public $id;
+    public $name;
+    public $bytes;
+    public $segmentIndexSize;
+
+    private static $C = null;
+    public static function default() {
+        if (self::$C == null) {
+            // 38 = 16 + 16 + 2 + 4
+            self::$C = new self(IPv6VersionNo, 'IPv6', 16, 38);
+        }
+
+        return self::$C;
+    }
+
+    public function __construct($id, $name, $bytes, $segmentIndexSize) {
+        $this->id = $id;
+        $this->name = $name;
+        $this->bytes = $bytes;
+        $this->segmentIndexSize = $segmentIndexSize;
+    }
+
+    public function ipSubCompare($ip, $buff, $offset) {
+        // return Util::ipCompare($ip, substr($buff, $offset, strlen($ip)));
+        return SearchIp::ipSubCompare($ip, $buff, $offset);
+    }
+
+    public function __toString() {
+        return sprintf(
+            "{id:%d, name:%s, bytes:%d, segmentIndexSize:%d}",
+            $this->id, $this->name, $this->bytes, $this->segmentIndexSize
+        );
+    }
+}

+ 285 - 0
app/extra/ip2region/xdb/SearchIp.php

@@ -0,0 +1,285 @@
+<?php
+
+namespace app\extra\ip2region\xdb;
+
+use \Exception;
+
+// global constants
+const Structure_20     = 2;
+const Structure_30     = 3;
+const HeaderInfoLength = 256;
+const VectorIndexRows  = 256;
+const VectorIndexCols  = 256;
+const VectorIndexSize  = 8;
+
+class SearchIp
+{
+    // parse the specified IP address and return its bytes.
+    // returns: NULL for failed or the packed bytes
+    public static function parseIP($ipString) {
+        $flag = FILTER_FLAG_IPV4 | FILTER_FLAG_IPV6;
+        if (!filter_var($ipString, FILTER_VALIDATE_IP, $flag)) {
+            return null;
+        }
+
+        return inet_pton($ipString);
+    }
+
+    // IP bytes to string
+    public static function ipToString($ipBytes) {
+        $l = strlen($ipBytes);
+        return ($l == 4 || $l == 16) ? inet_ntop($ipBytes) : '<invalid-ip-bytes>';
+    }
+
+    // compare two ip bytes (packed string return by parsedIP)
+    // returns: -1 if ip1 < ip2, 0 if ip1 == ip2 or 1 if ip1 > ip2
+    public static function ipSubCompare($ip1, $buff, $offset) {
+        // $r = substr_compare($ip1, $buff, $offset, strlen($ip1));
+        // @Note: substr_compare is not working, use the substr + strcmp instead
+        $r = strcmp($ip1, substr($buff, $offset, strlen($ip1)));
+        if ($r < 0) {
+            return -1;
+        } else if ($r > 0) {
+            return 1;
+        } else {
+            return 0;
+        }
+    }
+
+    // returns: -1 if ip1 < ip2, 0 if ip1 == ip2 or 1 if ip1 > ip2
+    public static function ipCompare($ip1, $ip2) {
+        $r = strcmp($ip1, $ip2);
+        if ($r < 0) {
+            return -1;
+        } else if ($r > 0) {
+            return 1;
+        } else {
+            return 0;
+        }
+    }
+
+    // version parse
+    public static function versionFromName($ver_name) {
+        $name = strtoupper($ver_name);
+        if ($name == "V4" || $name == "IPv4") {
+            return IPv4::default();
+        } else if ($name == "V6" || $name == "IPv6") {
+            return IPv6::default();
+        } else {
+            throw new Exception("invalid verstion name `{$ver_name}`");
+        }
+    }
+
+    // version parse from header
+    public static function versionFromHeader($header) {
+        // Old structure 2.0 with IPv4 supports ONLY
+        if ($header['version'] == Structure_20) {
+            return IPv4::default();
+        }
+
+        // structure 3.0 after IPv6 supporting
+        if ($header['version'] != Structure_30) {
+            throw new Exception("invalid xdb structure version `{$header['version']}`");
+        }
+
+        if ($header['ipVersion'] == IPv4VersionNo) {
+            return IPv4::default();
+        } else if ($header['ipVersion'] == IPv6VersionNo) {
+            return IPv6::default();
+        } else {
+            throw new Exception("invalid ip version number `{$header['ipVersion']}`");
+        }
+    }
+
+    // binary string chars implode with space
+    public static function bytesToString($buff, $offset, $length) {
+        $sb = [];
+        for ($i = 0; $i < $length; $i++) {
+            $sb[] = ord($buff[$offset+$i]) & 0xFF;
+        }
+        return '['.implode(' ', $sb).']';
+    }
+
+    // decode a 4bytes long with Little endian byte order from a byte buffer
+    public static function le_getUint32($b, $idx) {
+        $val = (ord($b[$idx])) | (ord($b[$idx+1]) << 8)
+            | (ord($b[$idx+2]) << 16) | (ord($b[$idx+3]) << 24);
+
+        // convert signed int to unsigned int if on 32 bit operating system
+        if ($val < 0 && PHP_INT_SIZE == 4) {
+            $val = sprintf("%u", $val);
+        }
+
+        return $val;
+    }
+
+    // read a 2bytes int with litten endian byte order from a byte buffer
+    public static function le_getUint16($b, $idx) {
+        return ((ord($b[$idx])) | (ord($b[$idx+1]) << 8));
+    }
+
+    // Verify if the current Searcher could be used to search the specified xdb file.
+    // Why do we need this check ?
+    // The future features of the xdb impl may cause the current searcher not able to work properly.
+    //
+    // @Note: You Just need to check this ONCE when the service starts
+    // Or use another process (eg, A command) to check once Just to confirm the suitability.
+    // returns: null for everything is ok or the error string.
+    public static function verify($handle) {
+        // load the header
+        $header = self::loadHeader($handle);
+        if ($header == null) {
+            return 'failed to load the header';
+        }
+
+        // get the runtime ptr bytes
+        $runtimePtrBytes = 0;
+        if ($header['version'] == Structure_20) {
+            $runtimePtrBytes = 4;
+        } else if ($header['version'] == Structure_30) {
+            $runtimePtrBytes = $header['runtimePtrBytes'];
+        } else {
+            return "invalid structure version `{$header['version']}`";
+        }
+
+        // 1, confirm the xdb file size
+        // to ensure that the maximum file pointer does not overflow
+        $stat = fstat($handle);
+        if ($stat == false) {
+            return 'failed to stat the xdb file';
+        }
+
+        $maxFilePtr = (1 << ($runtimePtrBytes * 8)) - 1;
+        // print_r([$stat['size'], $maxFilePtr]);
+        if ($stat['size'] > $maxFilePtr) {
+            return "xdb file exceeds the maximum supported bytes: {$maxFilePtr}";
+        }
+
+        return null;
+    }
+
+    public static function verifyFromFile($dbFile) {
+        $handle = fopen($dbFile, 'r');
+        if ($handle === false) {
+            return null;
+        }
+
+        $r = self::verify($handle);
+        fclose($handle);
+        return $r;
+    }
+
+    // load header info from a specified file handle
+    public static function loadHeader($handle) {
+        if (fseek($handle, 0) == -1) {
+            return null;
+        }
+
+        $buff = fread($handle, HeaderInfoLength);
+        if ($buff === false) {
+            return null;
+        }
+
+        // read bytes length checking
+        if (strlen($buff) != HeaderInfoLength) {
+            return null;
+        }
+
+        // return the decoded header info
+        return array(
+            'version'         => self::le_getUint16($buff, 0),
+            'indexPolicy'     => self::le_getUint16($buff, 2),
+            'createdAt'       => self::le_getUint32($buff, 4),
+            'startIndexPtr'   => self::le_getUint32($buff, 8),
+            'endIndexPtr'     => self::le_getUint32($buff, 12),
+            'ipVersion'       => self::le_getUint16($buff, 16),
+            'runtimePtrBytes' => self::le_getUint16($buff, 18)
+        );
+    }
+
+    // load header info from the specified xdb file path
+    public static function loadHeaderFromFile($dbFile) {
+        $handle = fopen($dbFile, 'r');
+        if ($handle === false) {
+            return null;
+        }
+
+        $header = self::loadHeader($handle);
+        fclose($handle);
+        return $header;
+    }
+
+    // load vector index from a file handle
+    public static function loadVectorIndex($handle) {
+        if (fseek($handle, HeaderInfoLength) == -1) {
+            return null;
+        }
+
+        $rLen = VectorIndexRows * VectorIndexCols * VectorIndexSize;
+        $buff = fread($handle, $rLen);
+        if ($buff === false) {
+            return null;
+        }
+
+        if (strlen($buff) != $rLen) {
+            return null;
+        }
+
+        return $buff;
+    }
+
+    // load vector index from a specified xdb file path
+    public static function loadVectorIndexFromFile($dbFile) {
+        $handle = fopen($dbFile, 'r');
+        if ($handle === false) {
+            return null;
+        }
+
+        $vIndex = self::loadVectorIndex($handle);
+        fclose($handle);
+        return $vIndex;
+    }
+
+    // load the xdb content from a file handle
+    public static function loadContent($handle) {
+        if (fseek($handle, 0, SEEK_END) == -1) {
+            return null;
+        }
+
+        $size = ftell($handle);
+        if ($size === false) {
+            return null;
+        }
+
+        // seek to the head for reading
+        if (fseek($handle, 0) == -1) {
+            return null;
+        }
+
+        $buff = fread($handle, $size);
+        if ($buff === false) {
+            return null;
+        }
+
+        // read length checking
+        if (strlen($buff) != $size) {
+            return null;
+        }
+
+        return $buff;
+    }
+
+    // load the xdb content from a file path
+    public static function loadContentFromFile($dbFile) {
+        $str = file_get_contents($dbFile, false);
+        if ($str === false) {
+            return null;
+        } else {
+            return $str;
+        }
+    }
+
+    public static function now() {
+        return (microtime(true) * 1000);
+    }
+}

+ 196 - 0
app/extra/ip2region/xdb/Searcher.php

@@ -0,0 +1,196 @@
+<?php
+
+namespace app\extra\ip2region\xdb;
+
+class Searcher
+{
+    // ip version
+    private $version;
+
+    // xdb file handle
+    private $handle  = null;
+    private $ioCount = 0;
+
+    // vector index in binary string.
+    // string decode will be faster than the map based Array.
+    private $vectorIndex = null;
+
+    // xdb content buffer
+    private $contentBuff = null;
+
+    // ---
+    // static function to create searcher
+
+    /**
+     * @throws Exception
+     */
+    public static function newWithFileOnly($version, $dbFile) {
+        return new self($version, $dbFile, null, null);
+    }
+
+    /**
+     * @throws Exception
+     */
+    public static function newWithVectorIndex($version, $dbFile, $vIndex) {
+        return new self($version, $dbFile, $vIndex, null);
+    }
+
+    /**
+     * @throws Exception
+     */
+    public static function newWithBuffer($version, $cBuff) {
+        return new self($version, null, null, $cBuff);
+    }
+
+    // --- End of static creator
+
+    /**
+     * initialize the xdb searcher
+     * @throws Exception
+     */
+    function __construct($version, $dbFile, $vectorIndex=null, $cBuff=null) {
+        $this->version = $version;
+        // check the content buffer first
+        if ($cBuff != null) {
+            $this->vectorIndex = null;
+            $this->contentBuff = $cBuff;
+        } else {
+            // open the xdb binary file
+            $this->handle = fopen($dbFile, "r");
+            if ($this->handle === false) {
+                throw new \Exception("failed to open xdb file '%s'", $dbFile);
+            }
+
+            $this->vectorIndex = $vectorIndex;
+        }
+    }
+
+    public function close() {
+        if ($this->handle != null) {
+            fclose($this->handle);
+        }
+    }
+
+    public function getIPVersion() {
+        return $this->version;
+    }
+
+    public function getIOCount() {
+        return $this->ioCount;
+    }
+
+    /**
+     * find the region info for the specified ip address.
+     * @Note: the ip address couldO ONLY be a human-readable IP address string,
+     * DO not use the packed binary string returned by #parseIP
+     *
+     * @throws Exception
+     */
+    public function search($ip) {
+        $ipBytes = SearchIp::parseIP($ip);
+        if ($ipBytes == null) {
+            throw new \Exception("invalid ip address `{$ip}`");
+        }
+
+        return $this->searchByBytes($ipBytes);
+    }
+
+    /**
+     * find the region info for the specified binary ip bytes returned by #parseIP.
+     *
+     * @throws Exception
+     */
+    public function searchByBytes($ipBytes) {
+        // ip version check
+        if (strlen($ipBytes) != $this->version->bytes) {
+            throw new \Exception("invalid ip address ({$this->version->name} expected)");
+        }
+
+        // reset the global counter
+        $this->ioCount = 0;
+
+        // locate the segment index block based on the vector index
+        $il0 = ord($ipBytes[0]) & 0xFF;
+        $il1 = ord($ipBytes[1]) & 0xFF;
+        $idx = $il0 * VectorIndexCols * VectorIndexSize + $il1 * VectorIndexSize;
+        if ($this->vectorIndex != null) {
+            $sPtr = SearchIp::le_getUint32($this->vectorIndex, $idx);
+            $ePtr = SearchIp::le_getUint32($this->vectorIndex, $idx + 4);
+        } else if ($this->contentBuff != null) {
+            $sPtr = SearchIp::le_getUint32($this->contentBuff, HeaderInfoLength + $idx);
+            $ePtr = SearchIp::le_getUint32($this->contentBuff, HeaderInfoLength + $idx + 4);
+        } else {
+            // read the vector index block
+            $buff = $this->read(HeaderInfoLength + $idx, 8);
+            $sPtr = SearchIp::le_getUint32($buff, 0);
+            $ePtr = SearchIp::le_getUint32($buff, 4);
+        }
+
+        // printf("sPtr: %d, ePtr: %d\n", $sPtr, $ePtr);
+        // @Note: ptr validate, zero ptr means source data missing
+        // so we could just stop here and return an empty string.
+        if ($sPtr == 0 || $ePtr == 0) {
+            return "";
+        }
+
+        [$bytes, $dBytes] = [strlen($ipBytes), strlen($ipBytes) << 1];
+
+        // binary search the segment index to get the region info
+        $idxSize = $this->version->segmentIndexSize;
+        [$dataLen, $dataPtr, $l, $h] = [0, 0, 0, ($ePtr - $sPtr) / $idxSize];
+        while ($l <= $h) {
+            $m = ($l + $h) >> 1;
+            $p = $sPtr + $m * $idxSize;
+
+            // read the segment index
+            $buff = $this->read($p, $idxSize);
+
+            // compare the segment index
+            if ($this->version->ipSubCompare($ipBytes, $buff, 0) < 0) {
+                $h = $m - 1;
+            } else if ($this->version->ipSubCompare($ipBytes, $buff, $bytes) > 0) {
+                $l = $m + 1;
+            } else {
+                $dataLen = SearchIp::le_getUint16($buff, $dBytes);
+                $dataPtr = SearchIp::le_getUint32($buff, $dBytes + 2);
+                break;
+            }
+        }
+
+        // empty match interception.
+        // printf("dataLen: %d, dataPtr: %d\n", $dataLen, $dataPtr);
+        if ($dataLen == 0) {
+            return "";
+        }
+
+        // load and return the region data
+        return $this->read($dataPtr, $dataLen);
+    }
+
+    // read specified bytes from the specified index
+    private function read($offset, $len) {
+        // check the in-memory buffer first
+        if ($this->contentBuff != null) {
+            return substr($this->contentBuff, $offset, $len);
+        }
+
+        // read from the file
+        $r = fseek($this->handle, $offset);
+        if ($r == -1) {
+            throw new \Exception("failed to fseek to {$offset}");
+        }
+
+        $this->ioCount++;
+        $buff = fread($this->handle, $len);
+        if ($buff === false) {
+            throw new \Exception("failed to fread from {$len}");
+        }
+
+        if (strlen($buff) != $len) {
+            throw new \Exception("incomplete read: read bytes should be {$len}");
+        }
+
+        return $buff;
+    }
+
+}

+ 45 - 0
app/model/saas/SaasOrderArea.php

@@ -0,0 +1,45 @@
+<?php
+
+namespace app\model\saas;
+
+use app\extra\basic\Model;
+
+
+/**
+ * @property integer $id (主键)
+ * @property mixed $poi_id 
+ * @property string $name 
+ * @property mixed $create_at
+ */
+class SaasOrderArea extends Model
+{
+    /**
+     * The connection name for the model.
+     *
+     * @var string|null
+     */
+    protected $connection = 'mysql';
+    
+    /**
+     * The table associated with the model.
+     *
+     * @var string
+     */
+    protected string $table = "saas_order_area";
+    
+    /**
+     * The primary key associated with the table.
+     *
+     * @var string
+     */
+    protected string $primaryKey = "id";
+    
+    /**
+     * Indicates if the model should be timestamped.
+     *
+     * @var bool
+     */
+    public bool $timestamps = false;
+
+
+}

+ 1 - 1
app/queue/redis/slow/ImportOrder.php

@@ -100,7 +100,7 @@ class ImportOrder implements Consumer
                 'success_num'   => (!empty($successData)) ? count(array_values($successData)) : 0,
                 'success_data'  => (!empty($successData)) ? json_encode(array_values($successData)) : "",
                 'error_num'     => (!empty($errData)) ? count(array_values($errData)) : 0,
-                'error_data'    => (!empty($errData)) ? json_encode(array_values($errData)) : '',
+                'error_data'    => (!empty($errData)) ? json_encode(array_values($errData)) : '[]',
             ]);
             return true;
         } catch (\Throwable $throwable) {

+ 1 - 1
app/service/saas/ComplaintService.php

@@ -29,7 +29,7 @@ class ComplaintService extends Service
         $filter = [];
         !empty($param['openid']) && $filter[] = ["openid", '=', $param['openid']];
         !empty($param['poi_id']) && $filter[] = ["poi_id", '=', $param['poi_id']];
-        !empty($param['poi']) && $filter[] = ["poi_id", '=', $param['poi']];
+//        !empty($param['poi']) && $filter[] = ["poi_id", '=', $param['poi']];
         !empty($param['mobile']) && $filter[] = ["mobile", 'like', "%{$param['mobile']}%"];
         !empty($param['status']) && $filter[] = ["status", '=', ($param['status']-1)];
         return $filter;

+ 38 - 0
app/service/saas/OrderAreaService.php

@@ -0,0 +1,38 @@
+<?php
+
+namespace app\service\saas;
+
+use app\extra\basic\Service;
+use app\model\saas\SaasOrderArea;
+
+class OrderAreaService extends Service
+{
+
+
+
+    /**
+     *
+     * @return $this
+     */
+    public function setModel()
+    {
+        $this->mode = (new SaasOrderArea);
+        return $this;
+    }
+
+
+    /**
+     *
+     * @param array $param
+     * @return array
+     */
+    public function searchFilter(array $param = []): array
+    {
+        $filter = [];
+        !empty($param['poi_id']) && $filter[] = ["poi_id", '=', $param['poi_id']];
+        !empty($param['poi']) && $filter[] = ["poi_id", '=', $param['poi']];
+        !empty($param['name']) && $filter[] = ["name", 'like', "%{$param['name']}%"];
+        return $filter;
+    }
+
+}

+ 40 - 0
app/service/saas/UserOpenService.php

@@ -0,0 +1,40 @@
+<?php
+
+namespace app\service\saas;
+
+use app\extra\basic\Service;
+use app\model\saas\SaasUserOpen;
+
+class UserOpenService extends Service
+{
+
+
+    /**
+     *
+     * @return $this
+     */
+    public function setModel(): static
+    {
+        $this->mode = (new SaasUserOpen);
+        return $this;
+    }
+
+
+    /**
+     *
+     * @param array $param
+     * @return array
+     */
+    public function searchFilter(array $param = []): array
+    {
+        $filter = [];
+        !empty($param['openid']) && $filter[] = ["openid", '=', $param['openid']];
+        !empty($param['service_id']) && $filter[] = ["service_id", '=', $param['service_id']];
+        !empty($param['poi_id']) && $filter[] = ["poi_id", '=', $param['poi_id']];
+        !empty($param['poi']) && $filter[] = ["poi_id", '=', $param['poi']];
+        !empty($param['black']) && $filter[] = ["is_black", '=', $param['black']];
+        !empty($param['name']) && $filter[] = ["nickname", 'like', "%{$param['name']}%"];
+        return $filter;
+    }
+
+}

+ 12 - 0
app/view/protocol.html

@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <title><?php echo ($name=='privacy'?'隐私协议':'服务协议');?></title>
+</head>
+<body>
+<div class="">
+    <?php echo htmlspecialchars_decode($data);?>
+</div>
+</body>
+</html>