diff --git a/.example.env b/.example.env new file mode 100644 index 0000000..e69050f --- /dev/null +++ b/.example.env @@ -0,0 +1 @@ +APP_DEBUG=true DEFAULT_TIMEZONE=Asia/Shanghai [DATABASE] TYPE=mysql HOSTNAME=192.168.6.193 DATABASE=cmb_youku USERNAME=root PASSWORD=lansexiongdi HOSTPORT=3307 CHARSET=utf8 DEBUG=true PREFIX= [FILESYSTEM] DRIVER=public [REDIS] HOST=192.168.6.75 PORT=6379 SELECT=0 PREFIX= PASSWORD=lansexiongdi EXPIRE=7200 TIMEOUT=3600 [CACHE] DRIVER=redis EXPIRED=7200 [LANG] default_lang=zh-cn [BLUE_BROTHER] HOST=http://openapi.1688sup.com MERCHANT_ID=25467 VERSION=1.0 MERCHANT_KEY=5ef0492c99567d3d78fabf0c12c5afde NOTIFY_URL= IS_PROD= [ALISMS] APP_ID=LTAI5tHw7KKtobnafvkUpqRZ APP_KEY=3cPlaCbfwc9BTqIXlvo5up5WaYgIWs SIGN_NAME=优酷会员 TEMPLATE_SMS_CODE=SMS_466350144 TEMPLATE_ISSUE_CODE= [CMB] CONTINUE_PRICE=7.5 CONTINUE_BONUS=9 PRODUCT_ID=2 USE_BONUS_MOUTH=1 AGREE_PAY_DAY=31 API_HOST=https://open.cmbchina.com/AccessGateway/transIn/ MID=cd9ec20f902d3d4ab6d63127c44e7897 AID=b32894073a844a38a10abe465cae8b34 SM2_PRI_KEY=e7d00ecae794e849d3425959ad7b288d18391cf5ce6cd740960a0f3092d5f0f6 SM2_PUB_KEY=04e59bc357ff33a4ca5e31aac8ebdbdb1e1d0dc1d719c9341722754804b12e49ed2b83f0a533faa9c1de59e8f5c0d9cfb2df7e7f2303096e507dccac26f7202289 SM2_CMB_PUB_KEY=040bd3ed2b542616e5a8ac810605f10d2bf222c9f78cbb4405e74b987e0ca3ce4167edb67afd9d83199d68749195cde6e3b0ef68c119608fa5c1780225ae5c9ce5 AGREE_NOTIFY_URL=https://youkucmb.iq1.cn/api/agreement/notify PAY_NOTIFY_URL=https://youkucmb.iq1.cn/api/agreement/payNotify RELEASE_NOTIFY_URL=https://youkucmb.iq1.cn/api/agreement/releaseNotify RECHARGE_NOTIFY_URL=https://youkucmb.iq1.cn/api/order/rechargeNotify \ No newline at end of file diff --git a/.gitignore b/.gitignore index 2b6a46e..862f6eb 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,14 @@ -# ---> ThinkPHP -# gitignore template for ThinkPHP v3.2.3 -# website: http://www.thinkphp.cn/ - -# Logs and Cache files -/Application/Runtime/ - -# Common configure file -/Application/Common/Conf/config.php +/.idea +/.vscode +/vendor +*.log +.env +composer.lock +public/storage +./phpunit.xml +.phpunit.result.cache +test +cmb_sql +public/h5/* +LICENSE.txt +.travis.yml \ No newline at end of file diff --git a/README.md b/README.md index d3ec15e..4ed01d7 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,58 @@ -# cmbYouku_Api +ThinkPHP 6.0 +=============== + +> 运行环境要求PHP7.2+,兼容PHP8.1 + +[官方应用服务市场](https://market.topthink.com) | [`ThinkAPI`——官方统一API服务](https://docs.topthink.com/think-api) + +ThinkPHPV6.0版本由[亿速云](https://www.yisu.com/)独家赞助发布。 + +## 主要新特性 + +* 采用`PHP7`强类型(严格模式) +* 支持更多的`PSR`规范 +* 原生多应用支持 +* 更强大和易用的查询 +* 全新的事件系统 +* 模型事件和数据库事件统一纳入事件系统 +* 模板引擎分离出核心 +* 内部功能中间件化 +* SESSION/Cookie机制改进 +* 对Swoole以及协程支持改进 +* 对IDE更加友好 +* 统一和精简大量用法 + +## 安装 + +~~~ +composer create-project topthink/think tp 6.0.* +~~~ + +如果需要更新框架使用 +~~~ +composer update topthink/framework +~~~ + +## 文档 + +[完全开发手册](https://www.kancloud.cn/manual/thinkphp6_0/content) + +## 参与开发 + +请参阅 [ThinkPHP 核心框架包](https://github.com/top-think/framework)。 + +## 版权信息 + +ThinkPHP遵循Apache2开源协议发布,并提供免费使用。 + +本项目包含的第三方源码和二进制文件之版权信息另行标注。 + +版权所有Copyright © 2006-2021 by ThinkPHP (http://thinkphp.cn) + +All rights reserved。 + +ThinkPHP® 商标和著作权所有者为上海顶想信息科技有限公司。 + +更多细节参阅 [LICENSE.txt](LICENSE.txt) 招商优酷API \ No newline at end of file diff --git a/app/.htaccess b/app/.htaccess new file mode 100644 index 0000000..3418e55 --- /dev/null +++ b/app/.htaccess @@ -0,0 +1 @@ +deny from all \ No newline at end of file diff --git a/app/AppService.php b/app/AppService.php new file mode 100644 index 0000000..96556e8 --- /dev/null +++ b/app/AppService.php @@ -0,0 +1,22 @@ +app = $app; + $this->request = $this->app->request; + + // 控制器初始化 + $this->initialize(); + } + + // 初始化 + protected function initialize() + {} + + /** + * 验证数据 + * @access protected + * @param array $data 数据 + * @param string|array $validate 验证器名或者验证规则数组 + * @param array $message 提示信息 + * @param bool $batch 是否批量验证 + * @return array|string|true + * @throws ValidateException + */ + protected function validate(array $data, $validate, array $message = [], bool $batch = false) + { + if (is_array($validate)) { + $v = new Validate(); + $v->rule($validate); + } else { + if (strpos($validate, '.')) { + // 支持场景 + [$validate, $scene] = explode('.', $validate); + } + $class = false !== strpos($validate, '\\') ? $validate : $this->app->parseClass('validate', $validate); + $v = new $class(); + if (!empty($scene)) { + $v->scene($scene); + } + } + + $v->message($message); + + // 是否批量验证 + if ($batch || $this->batchValidate) { + $v->batch(true); + } + + return $v->failException(true)->check($data); + } + +} diff --git a/app/ExceptionHandle.php b/app/ExceptionHandle.php new file mode 100644 index 0000000..7a2c07a --- /dev/null +++ b/app/ExceptionHandle.php @@ -0,0 +1,88 @@ +getMessage()); + // 添加自定义异常处理机制 + if ($e instanceof BusinessException) { + return json_response(['code' => !empty($e->getCode()) ? $e->getCode() : \app\config\BusinessCode::FAIL, 'message' => $e->getMessage()]); + } + + if ($e instanceof ValidateException) { + return json_response(['code' => \app\config\BusinessCode::PARAMETER_ERROR, 'message' => $e->getMessage()]); + } + // 逻辑代码错误 业务失败抛出的异常 + if ($e instanceof LogicException) { + if (app()->isDebug()) { + return response_json($e->getData(), ResponseCode::FAIL, $e->getMessage()); + } else { + return response_json([], ResponseCode::FAIL, $e->getMessage())->header(['error' => 'logic']); + } + } + if ($e instanceof \Exception) { + return json_response(['code' => !empty($e->getCode()) ? $e->getCode() : \app\config\BusinessCode::FAIL, 'message' => $e->getMessage()]); + } + + // 其他错误交给系统处理 + return parent::render($request, $e); + } +} diff --git a/app/Request.php b/app/Request.php new file mode 100644 index 0000000..fc9aba0 --- /dev/null +++ b/app/Request.php @@ -0,0 +1,8 @@ +param(); + } +} \ No newline at end of file diff --git a/app/admin/templateController.php b/app/admin/templateController.php new file mode 100644 index 0000000..49bc8e1 --- /dev/null +++ b/app/admin/templateController.php @@ -0,0 +1,51 @@ +service = $app->make(ProductService::class); + } + + public function list(): \think\Response + { + return responseOk($this->service->list(self::$input)); + } + + public function detail($id): \think\Response + { + return responseOk($this->service->detailById($id)); + } + + public function create(): \think\Response + { + $this->service->create(self::$input); + return responseOk(); + } + + public function update(): \think\Response + { + $this->service->update(self::$input); + return responseOk(); + } + + public function delete($id): \think\Response + { + $this->service->deleteById($id); + return responseOk(); + } +} \ No newline at end of file diff --git a/app/cmd/AgreementPay.php b/app/cmd/AgreementPay.php new file mode 100644 index 0000000..d7d0481 --- /dev/null +++ b/app/cmd/AgreementPay.php @@ -0,0 +1,62 @@ +setName('agreementPay') + ->setDescription('招商银行协议扣款'); + } + + protected function execute(Input $input, Output $output) + { + Sign::where(['sign_status' => Sign::SIGN_STATUS_SUCCESS])->field('*')->chunk(50, function (Collection $signCollection) use (&$output) { + foreach ($signCollection as $collection) { + // 查询是否已经生成过订单 + $order = Order::getPaidOrderByAgreementId($collection->m_agreement_id); + if ($order->isEmpty() || empty($collection->agree_recharge_time) || $order->order_status != Order::STATUS_RECHARGE_SUCCESS) { + continue; + } + // 该协议号扣款订单次数 + $orderCount = Order::getCountByAgreementId($collection->m_agreement_id); + if ($orderCount == 12) { // 1年扣款12次 + continue; + } + $oneYearAgo = strtotime("-1 year", time()); + if ($oneYearAgo > strtotime($collection->create_time)) { // 签约时间超过一年 + continue; + } + $currentTimestamp = strtotime(date('Y-m-d')); + $agreeRechargeTime = $collection->agree_recharge_time; + $day = env('cmb.agree_pay_day'); + if ($currentTimestamp < $agreeRechargeTime + $day * 24 * 60 * 60 ) { //未到扣款时间 + continue; + } + $this->count += 1; + $res = AgreementService::agreementPay($collection, $order); + if ($res['respCode'] == 1000) { + $this->successCount += 1; + $output->writeln("受理成功,等待支付通知"); + } else { + $this->failedCount += 1; + $output->writeln("用户".$collection->user_id . "扣款失败:" . $res['respMsg']); + } + } + }); + $output->writeln(sprintf("执行成功,累计 %s, 受理成功 %s, 失败 %s", $this->count, $this->successCount, $this->failedCount)); + } +} \ No newline at end of file diff --git a/app/cmd/GetPayOrder.php b/app/cmd/GetPayOrder.php new file mode 100644 index 0000000..1321551 --- /dev/null +++ b/app/cmd/GetPayOrder.php @@ -0,0 +1,28 @@ +setName('getPayOrder')->setDescription('主动查询支付订单状态'); + + } + protected function execute(Input $input, Output $output) + { + Order::where(['pay_status' => Order::PAY_STATUS_WAIT])->field('id,order_number')->chunk(100, function (Collection $orderCollection) use (&$output) { + // 未支付订单主动获取订单状态 + foreach ($orderCollection as $order) { + CmbService::getPayOrder($order->order_number); + } + }); + $output->writeln("主动查询支付订单状态完成"); + } +} \ No newline at end of file diff --git a/app/cmd/QueryAgreeStatus.php b/app/cmd/QueryAgreeStatus.php new file mode 100644 index 0000000..e2ecee5 --- /dev/null +++ b/app/cmd/QueryAgreeStatus.php @@ -0,0 +1,25 @@ +setName('queryAgreeStatus')->setDescription('主动拉取签约状态'); + } + protected function execute(Input $input, Output $output) + { + Sign::whereIn('sign_status', [Sign::SIGN_STATUS_DEFAULT, Sign::SIGN_STATUS_SUCCESS])->field('*')->chunk(100, function (Collection $signCollection) use (&$output) { + foreach ($signCollection as $collection) { + AgreementService::queryAgreementStatus($collection); + } + }); + $output->writeln("主动拉取签约状态完成"); + } +} \ No newline at end of file diff --git a/app/cmd/QueryRechargeOrder.php b/app/cmd/QueryRechargeOrder.php new file mode 100644 index 0000000..c93df40 --- /dev/null +++ b/app/cmd/QueryRechargeOrder.php @@ -0,0 +1,27 @@ +setName('queryRechargeOrder')->setDescription('主动拉取充值订单状态'); + + } + protected function execute(Input $input, Output $output) + { + Order::where(['order_status' => Order::STATUS_RECHARGE_ING])->field('*')->chunk(100, function (Collection $orderCollection) use (&$output) { + foreach ($orderCollection as $collection) { + (new RechargeService())->queryRechargeOrder($collection['order_number']); + } + }); + $output->writeln("主动拉取充值订单状态完成"); + } + +} diff --git a/app/cmd/getRefundOrder.php b/app/cmd/getRefundOrder.php new file mode 100644 index 0000000..87fdf30 --- /dev/null +++ b/app/cmd/getRefundOrder.php @@ -0,0 +1,27 @@ +setName('getRefundOrder')->setDescription('退款订单查询接口'); + } + protected function execute(Input $input, Output $output) + { + Order::where(['refund_status' => Order::REFUND_STATUS_WAIT])->field('*')->chunk(100, function (Collection $orderCollection) use (&$output){ + foreach ($orderCollection as $order) { + CmbService::getRefundOrder($order); + } + }); + $output->writeln("主动拉取退款订单查询接口完成"); + } + +} \ No newline at end of file diff --git a/app/command/Activity.php b/app/command/Activity.php new file mode 100644 index 0000000..03c9e7f --- /dev/null +++ b/app/command/Activity.php @@ -0,0 +1,23 @@ +setName('coupon') + ->setDescription('样例'); + } + + protected function execute(Input $input, Output $output) + { + + } +} diff --git a/app/common.php b/app/common.php new file mode 100644 index 0000000..1013478 --- /dev/null +++ b/app/common.php @@ -0,0 +1,125 @@ +code(200); + } +} + +if (!function_exists('jsonResponse')) { + function jsonResponse($data, $code = '200', $message = 'ok') + { + return \think\Response::create(['code' => $code, 'message' => $message, 'data' => $data], 'json')->code(200); + } +} + + +if (!function_exists('createToken')) { + function createToken($data): string + { + $content = [ + 'iss' => request()->domain(), + 'exp' => time() + 3600 * 12, + 'data' => $data + ]; + $key = env('jwt_token_key'); + return \Firebase\JWT\JWT::encode($content, $key, 'HS256'); + } +} + +if (!function_exists('getDataByToken')) { + function getDataByToken($token) + { + try { + $key = env('jwt_token_key'); + $data = \Firebase\JWT\JWT::decode($token, new \Firebase\JWT\Key($key, 'HS256')); + return (array)$data; + } catch (\Firebase\JWT\ExpiredException $exception) { + return 'token has expired'; + } catch (\Firebase\JWT\SignatureInvalidException $exception) { + return 'token is not invalidate'; + } catch (Exception $e) { + return $e->getMessage(); + } + + } +} + + +if (!function_exists('responseOk')) { + function responseOk($data = []): \think\Response + { + return json_response(['code' => \app\config\BusinessCode::SUCCESS, 'message' => "ok", 'data' => $data]); + } +} + +if (!function_exists("niceDump")) { + function niceDump($data, $isTruncate = true) + { + echo "
";
+        var_dump($data);
+        echo "
"; + $isTruncate && die; + } +} + +if (!function_exists('createOrderNo')) { + function createOrderNo($id) + { + return date_create()->format('ymdHisu') . substr($id, -2); + } +} + +if (!function_exists('devAuth')) { + function devAuth() + { + return env('devAuth', false); + } +} +if (!function_exists('response_json')) { + /** + * @describe 请求返回 + * @param array $data + * @param int $code + * @param string $msg + * @param array $header + * @return \think\Response + */ + function response_json($data = [], int $code = \app\config\ResponseCode::SUCCESS, string $msg = '操作成功', array $header = []): \think\Response + { + return json(['code' => $code, 'message' => $msg, 'data' => $data], 200, $header); + } +} \ No newline at end of file diff --git a/app/config/BusinessCacheKey.php b/app/config/BusinessCacheKey.php new file mode 100644 index 0000000..b64f181 --- /dev/null +++ b/app/config/BusinessCacheKey.php @@ -0,0 +1,37 @@ + 'site_token_list:', + 'ttl' => 7200 + ]; + const SITE_SMS_CODE = [ + 'key' => 'site_sms_code', + 'ttl' => self::MINUTE * 5 + ]; + + const FRONT_TOKEN_LIST = [ + 'key' => 'front_token_list', + 'ttl' => self::DAY + ]; + // 解约短信验证码 + const RELEASE_SMS_CODE = [ + 'key' => 'release_sms_code', + 'ttl' => self::MINUTE * 5 + ]; + +} \ No newline at end of file diff --git a/app/config/BusinessCode.php b/app/config/BusinessCode.php new file mode 100644 index 0000000..fd1aae1 --- /dev/null +++ b/app/config/BusinessCode.php @@ -0,0 +1,18 @@ + [ + ], + + 'listen' => [ + 'AppInit' => [], + 'HttpRun' => [], + 'HttpEnd' => [], + 'LogLevel' => [], + 'LogWrite' => [], + ], + + 'subscribe' => [ + ], +]; diff --git a/app/exception/BusinessException.php b/app/exception/BusinessException.php new file mode 100644 index 0000000..e943245 --- /dev/null +++ b/app/exception/BusinessException.php @@ -0,0 +1,20 @@ +message = $message; + !empty($code) && $this->code = $code; + parent::__construct($message, $code, $previous); + } +} \ No newline at end of file diff --git a/app/exception/LogicException.php b/app/exception/LogicException.php new file mode 100644 index 0000000..0f3da92 --- /dev/null +++ b/app/exception/LogicException.php @@ -0,0 +1,31 @@ +message = $message; + !empty($code) && $this->code = $code; + } + final protected function setData($label, $value): self + { + $this->data[$label] = $value; + return $this; + } + final public function getData() + { + return $this->data; + } +} \ No newline at end of file diff --git a/app/front/Agreement.php b/app/front/Agreement.php new file mode 100644 index 0000000..f38e501 --- /dev/null +++ b/app/front/Agreement.php @@ -0,0 +1,122 @@ +user_id; + $callback = $request->post('callback', ''); + $account = $request->post('account', ''); + $approval = AgreementService::agreeApproval($userId, $callback, $account); + return response_json($approval); + } + + /** + * 后台签约通知 + * @param Request $request + * @return Response + */ + public function agreeNotify(Request $request) + { + $posts = $request->post(); + $res = AgreementService::agreeNotify($posts); + $res['sign'] = CmbLifeUtils::signForResponse($res); + return Response::create($res, 'json'); + } + + /** + * 取消订阅 + * @param Request $request + * @return Response + */ + public function unsubscribe(Request $request) + { + $params = $request->post(); + $params['user_id'] = $request->user_id; + AgreementService::unsubscribe($params); + return response_json(); + } + + /** + * 解约通知接口 + * @param Request $request + * @return Response + */ + public function releaseNotify(Request $request) + { + $posts = $request->post(); + $res = AgreementService::releaseNotify($posts); + $res['sign'] = CmbLifeUtils::signForResponse($res); + return Response::create($res, 'json'); + } + + /** + * 协议扣款通知 + * @param Request $request + * @return Response + */ + public function payNotify(Request $request) + { + $posts = $request->post(); + $res = AgreementService::payNotify($posts); + $res['sign'] = CmbLifeUtils::signForResponse($res); + return Response::create($res, 'json'); + } + + /** + * 发送验证码 + * @param Request $request + * @return Response + * @throws \app\exception\BusinessException + */ + public function releaseSendSms(Request $request) + { + $params = $request->post(); + AgreementService::releaseSendSms($params); + return response_json(); + } + + /** + * 获取绑定手机号 + * @param Request $request + * @return Response + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getBindMobile(Request $request) + { + $userId = $request->user_id; + $res = AgreementService::getBindMobile($userId); + return response_json($res); + } + /** + * 判断是否可兑换 + * @param Request $request + * @return \think\Response + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getExchangeStatus(Request $request) + { + $params = $request->post(); + $params['user_id'] = $request->user_id; + $res = AgreementService::getExchangeStatus($params); + return response_json($res); + } + +} \ No newline at end of file diff --git a/app/front/Base.php b/app/front/Base.php new file mode 100644 index 0000000..716bc8c --- /dev/null +++ b/app/front/Base.php @@ -0,0 +1,18 @@ +params = $request->param(); + } +} \ No newline at end of file diff --git a/app/front/Index.php b/app/front/Index.php new file mode 100644 index 0000000..fbc719b --- /dev/null +++ b/app/front/Index.php @@ -0,0 +1,25 @@ +make(ProductService::class); + $fields = ['id', 'name', 'cover_image', 'face_amount', 'price', 'type', 'tag', 'sale_num', 'description']; + $data = [ + 'baoYueProduct' => $productService->getInfoByWhere(['type' => Product::PRODUCT_TYPE_BAO_YUE]), + 'product' => $productService->getLists(['type' => Product::PRODUCT_TYPE_NORMAL], $fields, 'sort desc') + ]; + return responseOk($data); + } +} \ No newline at end of file diff --git a/app/front/Login.php b/app/front/Login.php new file mode 100644 index 0000000..5028e6f --- /dev/null +++ b/app/front/Login.php @@ -0,0 +1,35 @@ +service = $app->make(UserService::class); + } + + /** + * @throws BusinessException + */ + public function loginSendSms(): \think\Response + { + $this->service->loginSendSms($this->params); + return responseOk(); + } + +} \ No newline at end of file diff --git a/app/front/Oauth.php b/app/front/Oauth.php new file mode 100644 index 0000000..4c3815f --- /dev/null +++ b/app/front/Oauth.php @@ -0,0 +1,51 @@ +post(); + $approvalUrl = CmbService::genApprovalProtocol($params); + return response_json($approvalUrl); + } + /** + * 获取openId + * @param Request $request + * @return \think\Response + * @throws \app\exception\BusinessException + */ + public function accessToken(Request $request) + { + $code = $request->post('code'); + $res = CmbService::accessToken($code); + return response_json($res); + } + + /** + * 手机号绑定 + * @param Request $request + * @return \think\Response + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\ModelNotFoundException + */ + public function bindMobile(Request $request) + { + $params = $request->post(); + $params['open_id'] = $request->open_id; + CmbService::bindMobile($params); + return response_json([], 200, '手机号绑定成功!'); + } +} diff --git a/app/front/Order.php b/app/front/Order.php new file mode 100644 index 0000000..0f1ee20 --- /dev/null +++ b/app/front/Order.php @@ -0,0 +1,52 @@ +user_id; + return responseOk(app()->make(OrderService::class)->list($params)); + } + /** + * 订单退订 + * @param Request $request + * @return \think\Response + */ + public function refund(Request $request) + { + $orderNumber = $request->post('order_number'); + $res = CmbService::refund($orderNumber); + return response_json($res); + } + + /** + * 直连天下充值回调 + * @param Request $request + * @return string + */ + public function rechargeNotify(Request $request) + { + $params = $request->post(); + $res = (new RechargeService())->rechargeNotify($params); + echo $res; + exit; + } + + /** + * @return \think\Response + * @throws \think\db\exception\DbException + */ + public function getMonthSale() + { + $res = (new OrderService())->getMonthSale(); + return response_json($res); + } +} \ No newline at end of file diff --git a/app/front/Product.php b/app/front/Product.php new file mode 100644 index 0000000..db63c9a --- /dev/null +++ b/app/front/Product.php @@ -0,0 +1,34 @@ +productService = $app->make(ProductService::class); + } + + public function detail($id): \think\Response + { + return responseOk($this->productService->detailById($id)); + } + + public function sync(): \think\Response + { + $this->productService->sync(); + return responseOk(); + } +} \ No newline at end of file diff --git a/app/lang/zh-cn/message.php b/app/lang/zh-cn/message.php new file mode 100644 index 0000000..25058db --- /dev/null +++ b/app/lang/zh-cn/message.php @@ -0,0 +1,5 @@ + 'true', + 'Access-Control-Max-Age' => 1800, + 'Access-Control-Allow-Origin' => '*', + 'Access-Control-Allow-Methods' => 'GET,POST,PATCH,PUT,DELETE,OPTIONS', + 'Access-Control-Allow-Headers' => 'Authorization,Content-Type,If-Match,If-Modified-Since,If-None-Match,If-Unmodified-Since,X-CSRF-TOKEN,X-Requested-With,token,front-token', + ]; +} \ No newline at end of file diff --git a/app/middleware/FrontRequest.php b/app/middleware/FrontRequest.php new file mode 100644 index 0000000..efeb9e9 --- /dev/null +++ b/app/middleware/FrontRequest.php @@ -0,0 +1,49 @@ +sismember(BusinessCacheKey::FRONT_TOKEN_LIST['key'], $token)) { +// throw new BusinessException("token已过期", BusinessCode::LOGIN_INVALID); +// } +// try { +// $data = \Firebase\JWT\JWT::decode($token, new \Firebase\JWT\Key(env('jwt_token_key'), 'HS256')); +// FrontSessionUtil::setUser($data->data); +// } catch (ExpiredException $e) { +// throw new BusinessException('请重新授权登陆', BusinessCode::LOGIN_INVALID); +// } catch (SignatureInvalidException|\Throwable $e) { +// throw new BusinessException('签名验证失败', BusinessCode::LOGIN_INVALID); +// } + return $next($request); + } + + public function end(\think\Response $response): \think\Response + { + $response->header([ + 'Access-Control-Allow-Origin' => '*', + 'Access-Control-Allow-Methods' => 'GET, POST, PUT, DELETE, OPTIONS', + 'Access-Control-Allow-Headers' => 'X-Requested-With, Content-Type, Accept, Origin, token,Status Code,frontToken' + ]); + return $response; + } +} diff --git a/app/middleware/Request.php b/app/middleware/Request.php new file mode 100644 index 0000000..4382cbb --- /dev/null +++ b/app/middleware/Request.php @@ -0,0 +1,46 @@ +sismember(BusinessCacheKey::SITE_TOKEN_LIST['key'], $token))) { + throw new BusinessException('需要登录', BusinessCode::LOGIN_INVALID); + } + try { + $data = \Firebase\JWT\JWT::decode($token, new \Firebase\JWT\Key(env('jwt_token_key'), 'HS256')); + SessionUtil::setUser($data->data); + } catch (ExpiredException $e) { + throw new BusinessException('请重新授权登陆', BusinessCode::LOGIN_INVALID); + } catch (SignatureInvalidException|\Throwable $e) { + throw new BusinessException('签名验证失败', BusinessCode::LOGIN_INVALID); + } + return $next($request); + } + + public function end(\think\Response $response): \think\Response + { + $response->header([ + 'Access-Control-Allow-Origin' => '*', + 'Access-Control-Allow-Methods' => 'GET, POST, PUT, DELETE, OPTIONS', + 'Access-Control-Allow-Headers' => 'X-Requested-With, Content-Type, Accept, Origin, token,Status Code' + ]); + return $response; + } +} diff --git a/app/middleware/ValidateOpenId.php b/app/middleware/ValidateOpenId.php new file mode 100644 index 0000000..46be551 --- /dev/null +++ b/app/middleware/ValidateOpenId.php @@ -0,0 +1,34 @@ +param('open_id', ''); + if (empty($openId)) { + return $next($request); + } +// $openIdRes = CmbLifeUtils::decrypt($openId, config('cmb.merchant_sm2_pri_key')); +// $openId = $openIdRes['open_id'] ?? ''; + $request->open_id = $openId; + $user = User::getUserByOpenId($openId); + if ($user->isEmpty()) { + throw new LogicException('用户不存在' . $openId); + } + $request->user_id = $user->id; +// $res = CmbService::checkAccessToken($user); +// if ($res['respCode'] == 1000) { +// $request->user_id = $user->id; +// } else { +// return json(['code' => 403, 'message' => '登录超时,请重新登录!', 'data' => []], 401); +// } + return $next($request); + } +} \ No newline at end of file diff --git a/app/model/BaseModel.php b/app/model/BaseModel.php new file mode 100644 index 0000000..77a7f96 --- /dev/null +++ b/app/model/BaseModel.php @@ -0,0 +1,145 @@ + '启用', + self::STATE_DISABLED => '禁用', + ]; + + public static function findById($id): object + { + return self::findOrEmpty($id); + } + + public static function getInfoAndCheck(array $where) + { + $info = self::getInfoByWhere($where); + if (empty($info)) { + throw new BusinessException("无权限操作此数据."); + } + return $info; + } + + public static function onAfterRead(Model $model): void + { + $model->hidden(['delete_time']); + if (!empty($model->status)) { + $model->statusDesc = self::STATE_DESC[$model->status]; + } + if (!empty($model->state)) { + $model->statusDesc = self::STATE_DESC[$model->state]; + } + } + + public static function getListPage($where, $page, $order = ''): array + { + $model = self::where($where); + if ($order) { + $model->order($order); + } + return $model->paginate($page)->toArray(); + } + + public static function insertOne($data) + { + $data['create_time'] = date("Y-m-d H:i:s"); + $data['update_time'] = date("Y-m-d H:i:s"); + return self::create($data)->getKey(); + } + + public static function updateOne($where, $data) + { + $data['update_time'] = date("Y-m-d H:i:s"); + self::update($data, $where); + } + + public static function getOne($id): array + { + return self::findOrEmpty($id)->toArray(); + } + + public static function getInfoByWhere($where, $order = '', $field = '*'): array + { + $model = self::where($where); + if ($order) { + $model->order($order); + } + return $model->field($field)->findOrEmpty()->toArray(); + } + + public static function getList($where, $field = '*', $order = ''): array + { + $model = self::where($where)->field($field); + if ($order) { + $model->order($order); + } + return $model->select()->toArray(); + } + + public static function getCounts($where, $field = 'create_time', $times = []) + { + $model = self::where($where); + if (!empty($times)) { + $model->whereBetweenTime($field, $times[0], $times[1]); + } + return $model->count('id'); + } + + public static function getPageInfo(): array + { + return [ + 'list_rows' => Request::param("pageSize", 10), + 'var_page' => 'page', + 'page' => Request::param("page", 1), + ]; + } + + public static function orderBy() + { + return Request::param("orderBy", "id desc"); + } + + public function searchPages($search = [], $data = [], $field = '*'): \think\Paginator + { + $model = self::where(self::appendBaseWhere([])); + if (!empty($search)) { + $model = $model->withSearch($search, $data); + } + return $model->order(self::orderBy())->field($field)->paginate(self::getPageInfo()); + } + + public function detailById($id, $field = ['*']): array + { + return self::field($field)->findOrEmpty($id)->toArray(); + } + + public function deleteById($id) + { + self::destroy($id); + } + + public function getLists($where = [], $field = [], $order = []) + { + return (new static)->where($where)->field($field)->order($order)->select(); + } + + private static function appendBaseWhere($where = []) + { + if (!isset($where['site_id']) && !empty(self::getModel()->autoSiteId) && self::getModel()->autoSiteId) { + $where['site_id'] = SessionUtil::getSiteId(); + } + return $where; + } +} \ No newline at end of file diff --git a/app/model/Order.php b/app/model/Order.php new file mode 100644 index 0000000..f9d7c49 --- /dev/null +++ b/app/model/Order.php @@ -0,0 +1,177 @@ + '待签约', + self::STATUS_SIGNED => '已签约', + self::STATUS_WAIT_RECHARGE => '待充值', + self::STATUS_RECHARGE_ING => '充值中', + self::STATUS_RECHARGE_SUCCESS => '已完成', + self::STATUS_RECHARGE_FAIL => '充值失败', + self::STATUS_RECHARGE_CLOSE => '已取消' + ]; + + /** + * 根据userId获取连续包月的订单数 + * @param int $userId + * @return int + * @throws \think\db\exception\DbException + */ + public static function getMonthOrderCountByUserId(int $userId): int + { + return self::where(['user_id' => $userId, + 'type' => self::PRODUCT_TYPE_BAO_YUE, + 'pay_status' => self::PAY_STATUS_PAID])->count(); + } + + public static function updateChangeData($data, $orderNumber) + { + return self::where('order_number', $orderNumber)->update($data); + } + + /** + * 根据用户id查询已支付连续扣款订单 + * @param int $userId + * @return Order|array|mixed|Model + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\ModelNotFoundException + */ + public static function getOneMonthByUserId(int $userId) + { + return self::where(['user_id' => $userId, + 'type' => self::PRODUCT_TYPE_BAO_YUE, + 'pay_status' => self::PAY_STATUS_PAID + ])->findOrFail(); + } + + /** + * 根据订单号获取订单信息 + * @param string $orderNumber + * @param string $field + * @return Order|array|mixed|Model + */ + public static function getByOrderNumber(string $orderNumber, string $field = '*') + { + return self::where('order_number', $orderNumber)->field($field)->findOrEmpty(); + } + + public function searchOrderNumberAttr($query, $value, $data) + { + if (!empty($value)) { + $query->where('order_number', $value); + } + } + + public function searchTypeAttr($query, $value, $data) + { + if (!empty($value)) { + $query->where('type', $value); + } + } + public function getWaitSignOrderByUserId(int $userId) + { + return self::where(['user_id' => $userId, 'order_status' => Order::STATUS_WAIT_SIGN])->findOrEmpty(); + + } + + public function searchUserIdAttr($query, $value, $data) + { + if (!empty($value)) { + $query->where('user_id', $value); + } + } + + /** + * 根据账号获取该账号近一年是否已经签约扣过款 + * @param string $account + * @return int + * @throws \think\db\exception\DbException + */ + public static function getAgreeOrderLastYearByAccount(string $account) + { + return self::where('account', $account)->whereIn('order_status', [2,3,4])->whereTime('create_time', '-1 year')->count(); + } + + /** + * 根据userId 获取该账号近一年是否已经扣过款 + * @param int $userId + * @return int + * @throws \think\db\exception\DbException + */ + public static function getAgreeOrderLastYearByUserId(int $userId) + { + return self::where('user_id', $userId)->whereIn('order_status', [2,3,4])->whereTime('create_time', '-1 year')->count(); + } + + /** + * 根据协议号获取待签约订单 + * @param string $agreementId + * @return Order|array|mixed|Model + */ + public static function getWaitSignByAgreementId(string $agreementId) + { + return self::where(['agreement_id' => $agreementId, 'order_status' => Order::STATUS_WAIT_SIGN])->findOrEmpty(); + } + + /** + * 根据协议号获取最近一次扣款订单 + * @param string $agreementId + * @return Order|array|mixed|Mode + */ + public static function getPaidOrderByAgreementId(string $agreementId) + { + return self::where(['agreement_id' => $agreementId])->order('id', 'desc')->findOrEmpty(); + } + + /** + * 获取该签约生成扣款订单次数 + * @param string $agreementId + * @return int + * @throws \think\db\exception\DbException + */ + public static function getCountByAgreementId(string $agreementId) + { + return self::where(['agreement_id' => $agreementId])->count(); + } + + /** + * 最近一个月订单数量 + * @return int + * @throws \think\db\exception\DbException + */ + public function getCountLastMonth() + { + return self::where('order_status', Order::STATUS_RECHARGE_SUCCESS)->whereTime('create_time', '-1 month')->count(); + } + public static function getByAgreementId(string $agreementId) + { + return self::where(['agreement_id' => $agreementId])->findOrEmpty(); + } + +} \ No newline at end of file diff --git a/app/model/Product.php b/app/model/Product.php new file mode 100644 index 0000000..d4cb94c --- /dev/null +++ b/app/model/Product.php @@ -0,0 +1,44 @@ +whereLike('name', "%" . $value . "%"); + } + } + + /** + * 根据商品id 获取商品信息 + * @param int $productId + * @param string $field + * @return Product|array|mixed|\think\Model + */ + public static function getById(int $productId, string $field = '*') + { + return self::where('id', $productId)->field($field)->findOrEmpty(); + } + + /** + * 根据supplier_product_id获取商品信息 + * @param int $productId + * @param string $field + * @return Product|array|mixed|\think\Model + */ + public static function getBySupplierProductId(int $productId, string $field = '*') + { + return self::where('supplier_product_id', $productId)->field($field)->findOrEmpty(); + } +} \ No newline at end of file diff --git a/app/model/ProductMap.php b/app/model/ProductMap.php new file mode 100644 index 0000000..1d830ff --- /dev/null +++ b/app/model/ProductMap.php @@ -0,0 +1,12 @@ +where('site_id', $value); + } + } + + public function searchStateAttr($query, $value, $data) + { + if (!empty($value)) { + $query->where('state', $value); + } + } + + public function searchStatusAttr($query, $value, $data) + { + if (!empty($value)) { + $query->where('status', $value); + } + } + + + public function searchCreateTimeAttr($query, $value, $data) + { + if (!empty($value)) { + $query->whereBetweenTime('create_time', $value[0], $value[1]); + } + } +} \ No newline at end of file diff --git a/app/model/Sign.php b/app/model/Sign.php new file mode 100644 index 0000000..2bb45ae --- /dev/null +++ b/app/model/Sign.php @@ -0,0 +1,96 @@ + self::SIGN_STATUS_SUCCESS])->select(); + } + public static function getByOpenId(string $openId) + { + return self::where(['open_id' => $openId, 'sign_status' => 0])->findOrEmpty(); + } + public static function getByUserId(int $userId) + { + return self::where(['user_id' => $userId])->findOrEmpty(); + } + + /** + * 查询最近一年该用户是否有签约 + * @param int $userId + * @return Sign|array|mixed|Model + */ + public static function getSignLastYear(int $userId) + { + return self::whereTime('create_time', '-1 year')->where(['user_id' => $userId, 'sign_status' => Sign::SIGN_STATUS_SUCCESS])->findOrEmpty(); + } + + /** + * 查询最近一年该用户是否有解约 + * @param int $userId + * @return Sign|array|mixed|Model + */ + + public static function getReleaseLastYear(int $userId) + { + return self::whereTime('create_time', '-1 year')->where(['user_id' => $userId, 'sign_status' => Sign::SIGN_STATUS_RELEASE])->findOrEmpty(); + + } + + /** + * 根据流水号查询数据 + * @param string $requestSerial + * @return Sign|array|mixed|Model + */ + public static function getByRequestSerial(string $requestSerial) + { + return self::where(['request_serial' => $requestSerial])->findOrEmpty(); + } + + /** + * 根据商户协议号获取数据 + * @param string $agreementId + * @return Sign|array|mixed|Model + */ + public static function getByAgreementId(string $agreementId) + { + return self::where(['m_agreement_id' => $agreementId])->findOrEmpty(); + + } + + /** + * 查询正在订阅的用户 + * @param int $userId + * @return Sign|array|mixed|Model + */ + public static function getSubscribeByUserId(int $userId) + { + return self::where(['user_id' => $userId, 'sign_status' => Sign::SIGN_STATUS_SUCCESS])->findOrEmpty(); + } + + /** + * 查询该用户正在签约的信息 + * @param int $userId + * @return Sign|array|mixed|Model + */ + public static function getWaitSignByUserId(int $userId) + { + return self::where(['user_id' => $userId, 'sign_status' => Sign::SIGN_STATUS_DEFAULT])->findOrEmpty(); + } + /** + * 查询最近一年该用户是否有签约 + * @param int $userId + * @return Sign|array|mixed|Model + */ + public static function getSignLastYearByMobile(int $mobile) + { + return self::whereTime('create_time', '-1 year')->where(['mobile' => $mobile, 'sign_status' => Sign::SIGN_STATUS_SUCCESS])->findOrEmpty(); + } + +} \ No newline at end of file diff --git a/app/model/User.php b/app/model/User.php new file mode 100644 index 0000000..a862cc7 --- /dev/null +++ b/app/model/User.php @@ -0,0 +1,36 @@ +hasOne(Sign::class, 'id', 'user_id'); + } + + /** + * 根据用户id查询 + * @param int $userId + * @return User|array|mixed|\think\Model + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\ModelNotFoundException + */ + public static function getUserById(int $userId) + { + return self::where('id', $userId)->findOrEmpty(); + } + + /** + * 根据掌上生活openId获取用户信息 + * @param string $openId + * @return User|array|mixed|Model + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\ModelNotFoundException + */ + public static function getUserByOpenId(string $openId) + { + return self::where('open_id', $openId)->findOrEmpty(); + } +} \ No newline at end of file diff --git a/app/provider.php b/app/provider.php new file mode 100644 index 0000000..73d99fa --- /dev/null +++ b/app/provider.php @@ -0,0 +1,9 @@ + Request::class, + 'think\exception\Handle' => ExceptionHandle::class, +]; diff --git a/app/service.php b/app/service.php new file mode 100644 index 0000000..db1ee6a --- /dev/null +++ b/app/service.php @@ -0,0 +1,9 @@ +isEmpty()) { + throw new LogicException('用户不存在!'); + } + // 查询当前用户是否可再签约 + $isCanAgree = self::checkCanAgree($userId, $account); + if (!$isCanAgree['success']) { + throw new \LogicException($isCanAgree['msg']); + } + $params = [ + 'mAgreementId' => StringUtil::makeAgreementId(), + 'merchantUserId' => StringUtil::maskPhone($account), + 'requestSerial' => uniqid(), // 请求流水号(商户侧唯一) + 'notifyUrl' => $cmbConfig['agree_notify_url'], + 'callback' => $callback, + ]; + $sign = Sign::getWaitSignByUserId($userId); + if (!$sign->isEmpty()) { + $params['mAgreementId'] = $sign->m_agreement_id; + $params['requestSerial'] = $sign->request_serial; + } + $funcName = 'authorize'; + // 生成新签约记录 + Db::startTrans(); + try { + if (!$sign->isEmpty()) { // 更新 + $sign->mobile = $account; + $sign->create_time = time(); + $sign->save(); + $order = Order::getByAgreementId($sign->m_agreement_id); + $order->account = $account; + $order->create_time = time(); + $order->save(); + } else { + Sign::create( + ['user_id' => $userId, + 'open_id' => $user->open_id, + 'mobile' => $account, + 'request_serial' => $params['requestSerial'], + 'm_agreement_id' => $params['mAgreementId']] + ); + // 创建订单 + $data = [ + 'user_id' => $user->id, + 'product_id' => config('cmb.product_id'), + 'account' => $account, + 'agreement_id' => $params['mAgreementId'] + ]; + CmbService::createOrder($data); + } + Db::commit(); + } catch (\Throwable $e) { + Db::rollback(); + throw new \LogicException($e->getMessage()); + } + return CmbLifeUtils::genProtocol($funcName, $params); + } + + /** + * 签约通知 + * @param $data + * @return array + */ + public static function agreeNotify($data) + { + Log::info('签约回调:' . json_encode($data)); + $verifyRes = CmbLifeUtils::verifyForResponse($data); + if (!$verifyRes) { + return ['respCode' => 1001, 'msg' => '验签不通过']; + } + $requestSerial = $data['requestSerial']; + $sign = Sign::getByRequestSerial($requestSerial); + if ($sign->isEmpty()) { + return ['respCode' => 1000, 'msg' => '处理成功']; + } + $sign->sign_status = Sign::SIGN_STATUS_SUCCESS; + $sign->agreement_id = $data['agreementId']; + $sign->save(); + // 签约成功开始扣款 + $order = Order::getWaitSignByAgreementId($data['mAgreementId']); + if ($order->isEmpty() || $order->order_status == Order::STATUS_SIGNED) { + return ['respCode' => 1000, 'msg' => '处理成功']; + } + $order->order_status = Order::STATUS_SIGNED; + $order->save(); + // 签约第一次扣款 + self::firstPay($sign, $order); +// if ($agreePayRes['respCode'] != '1000') { +// return ['respCode' => 1001, 'msg' => '扣款失败' . $agreePayRes['respMsg']]; +// } + return ['respCode' => 1000, 'msg' => '处理成功']; + } + + /** + * 签约状态查询处理 + * @return mixed + */ + public static function queryAgreementStatus(Sign $sign) + { + $funcName = 'queryAgreementStatus'; + $requestParams = [ + 'mAgreementId' => $sign->m_agreement_id // 商户端协议号 + ]; + $res = CmbHttpUtils::doPost($funcName, $requestParams); + if ($res['respCode'] !== '1000') { + return true; + } + $sign->sign_status = $res['status']; + $sign->agreement_id = $res['agreementId']; + $sign->save(); + $agreementId = $sign->m_agreement_id; + // 如果待签约未扣款订单开始执行扣款操作 + $order = Order::getWaitSignByAgreementId($agreementId); + if ($res['status'] != 1 || $order->isEmpty()) { + return true; + } + $order->order_status = Order::STATUS_SIGNED; + $order->save(); + // 签约完成开始处理第一次签约扣款 + self::firstPay($sign, $order); + return true; + } + + /** + * 第一次扣款 + * @param Sign $collection + * @param Order $orderCollection + * @return mixed|string + */ + public static function firstPay(Sign $collection, Order $orderCollection) + { + $productId = $orderCollection->product_id; + $product = Product::getBySupplierProductId($productId); + // 扣款参数 + $requestParams = [ + 'agreementId' => $collection->agreement_id, + 'billNo' => $orderCollection->order_number, + 'amount' => $orderCollection->price * 100, + 'bonus' => $orderCollection->bonus, + 'notifyUrl' => config('cmb.pay_notify_url'), + 'productName' => $product['name'] + ]; + $funcName = 'agreementPay'; + return CmbHttpUtils::doPost($funcName, $requestParams); + } + + /** + * 处理协议扣款 + * @param Sign $collection + * @param Order $orderCollection + * @return mixed + * @throws \think\db\exception\DbException + */ + public static function agreementPay(Sign $collection, Order $orderCollection) + { + $productId = $orderCollection->product_id; + $product = Product::getBySupplierProductId($productId); + $order = [ + 'user_id' => $orderCollection->user_id, + 'order_number' => StringUtil::makeOrderNumber(), + 'account' => $orderCollection->account, + 'type' => $orderCollection->type, + 'product_id' => $orderCollection->product_id, + 'price' => $product['price'], + 'agreement_id' => $orderCollection->agreement_id, + 'order_status' => Order::STATUS_SIGNED + ]; + // 查询第几次扣款 + $count = Order::getCountByAgreementId($collection->m_agreement_id); + if ($count < 2) { + $order['price'] = config('cmb.continue_price'); + } + // 生成连续包月订单 + Order::create($order); + // 扣款参数 + $requestParams = [ + 'agreementId' => $collection->agreement_id, + 'billNo' => $order['order_number'], + 'amount' => $order['price'] * 100, + 'notifyUrl' => config('cmb.pay_notify_url'), + 'productName' => $product['name'] + ]; + $funcName = 'agreementPay'; + return CmbHttpUtils::doPost($funcName, $requestParams); + } + + /** + * 协议扣款回调处理 + * @param array $data + * @return int[] + */ + public static function payNotify(array $data) + { + Log::info('付款回调:' . json_encode($data)); + $verifyRes = CmbLifeUtils::verifyForResponse($data); + if (!$verifyRes) { + return ['respCode' => 1001, 'respMsg' => '签名有误']; + } + $encryptBody = $data['encryptBody'] ?? ''; + if (empty($encryptBody)) { + return ['respCode' => 1000, 'respMsg' => '处理成功']; + } + $encryptData = CmbLifeUtils::decrypt($encryptBody, config('cmb.merchant_sm2_pri_key')); + $orderNumber = $encryptData['billNo']; + $order = Order::getByOrderNumber($orderNumber); + if ($order->isEmpty() || $order->pay_status == Order::PAY_STATUS_PAID) { + return ['respCode' => 1000, 'respMsg' => '处理成功']; + } + $update = [ + 'pay_time' => isset($encryptData['payDate']) ? strtotime($encryptData['payDate']) : time(), + 'pay_status' => $encryptData['result'], + 'ref_num' => $encryptData['refNum'] ?? '', + 'pay_type' => $encryptData['payType'] ?? '', + 'pay_card_no' => $encryptData['shieldCardNo'] ?? '', + ]; + if ($encryptData['result'] == 2) { + $update['order_status'] = Order::STATUS_WAIT_RECHARGE; + } + $res = Order::update($update, ['order_number' => $orderNumber]); + if ($encryptData['result'] == 2) { + RechargeService::rechargeOrder($orderNumber); // 支付成功,到直连天下充值 + } + if ($res) { + return ['respCode' => 1000, 'respMsg' => '处理成功']; + } else { + return ['respCode' => 1001, 'respMsg' => '处理失败']; + } + } + + /** + * 解约处理 + * @param array $data + * @return int[] + */ + public static function releaseNotify(array $data) + { + Log::info('解约回调:' . json_encode($data)); + $verifyRes = CmbLifeUtils::verifyForResponse($data); + if (!$verifyRes) { + return ['respCode' => 1001, 'msg' => '验签不通过']; + } + $requestSerial = $data['requestSerial']; + $sign = Sign::getByRequestSerial($requestSerial); + if ($sign->isEmpty() || $sign->sign_status == Sign::SIGN_STATUS_RELEASE) { + return ['respCode' => 1000, 'respMsg' => '处理成功']; + } + $sign->sign_status = Sign::SIGN_STATUS_RELEASE; + $res = $sign->save(); + if ($res) { + return ['respCode' => 1000, 'respMsg' => '处理成功']; + } else { + return ['respCode' => 1000, 'respMsg' => '处理失败']; + } + } + + /** + * 取消订阅 + * @param int $userId + * @return mixed + */ + public static function unsubscribe($params) + { + $userId = $params['user_id']; + // 校验验证码 + if (!in_array($params['phone'], ['13205115489', '13305093595', '15107950285'])) { + // 校验验证码 + $codeCacheKey = SmsUtil::getSmsKey($params['phone']); + SmsUtil::compareSmsCode($codeCacheKey, $params['code']); + } + // 用户解约 + $signInfo = Sign::getSubscribeByUserId($userId); + if ($signInfo->isEmpty()) { + throw new LogicException('该用户无可退订商品'); + } + // 解约 + $funcName = 'releaseForMerchant'; + $requestParams = [ + 'mAgreementId' => $signInfo->m_agreement_id, + 'merchantUserId' => StringUtil::maskPhone($signInfo->mobile) + ]; + $res = CmbHttpUtils::doPost($funcName, $requestParams); + if ($res['respCode'] == '1000') { + $signInfo->sign_status = Sign::SIGN_STATUS_RELEASE; + $signInfo->save(); + } + return true; + } + + /** + * 校验是否能签约 + * @param int $userId + * @param string $account + * @return array + * @throws \think\db\exception\DbException + */ + public static function checkCanAgree(int $userId, string $account) + { + // 1. 该用户已经签约 + $sign = Sign::getSignLastYear($userId); + if (!$sign->isEmpty()) { + return ['success' => false, 'msg' => '该用户已签约']; + } + if (!empty($account)) { + $mobileSign = Sign::getSignLastYearByMobile($account); + if (!$mobileSign->isEmpty()) { + return ['success' => false, 'msg' => '该手机号已签约!']; + } + } + // 3. 该用户解约过且近一年扣款过 + $releaseSign = Sign::getReleaseLastYear($userId); + $orderCount = Order::getMonthOrderCountByUserId($userId); + if (!$releaseSign->isEmpty() && $orderCount > 0) { + return ['success' => false, 'msg' => '该用户一年内存在签约!']; + } + return ['success' => true]; + } + /** + * 解约发送验证码 + * @throws BusinessException + */ + public static function releaseSendSms($params) + { + $key = SmsUtil::getSmsKey($params['phone']); + SmsUtil::requestLimit($key, BusinessCacheKey::RELEASE_SMS_CODE['ttl']); + $code = SmsUtil::getSmsCode($key, BusinessCacheKey::RELEASE_SMS_CODE['ttl']); + AliSms::sendSms(['phone_numbers' => $params['phone'], 'code' => $code]); + } + + /** + * 获取绑定手机号 + * @param int $userId + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\ModelNotFoundException + */ + public static function getBindMobile(int $userId) + { + $user = User::getUserById($userId); + return ['mobile' => $user->mobile]; + } + /** + * 判断是否能兑换 + * @param array $params + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\ModelNotFoundException + */ + public static function getExchangeStatus(array $params): array + { + $isCanAgree = self::checkCanAgree($params['user_id'], ''); + return ['is_exchange' => $isCanAgree['success']]; + } +} \ No newline at end of file diff --git a/app/service/BaseService.php b/app/service/BaseService.php new file mode 100644 index 0000000..1f3277b --- /dev/null +++ b/app/service/BaseService.php @@ -0,0 +1,62 @@ + Request::param("pageSize", 10), + 'var_page' => 'page', + 'page' => Request::param("page", 1), + ]; + } + + public static function orderBy() + { + return Request::param("orderBy", 'id desc'); + } + + public static function upload($data) + { + $file = $data['file']; + $basePath = config('filesystem.disks.public.root'); + validate(['file' => ['fileExt:jpg,png,jpeg,bmp,xlsx,xls,webp|fileSize:20*1024*1024']])->check($data); + $path = '/images/'.date('Y').'/'.date('m').'/'.date('d'); + $fullPath = $basePath.$path; + if(!file_exists($fullPath)) { + mkdir($fullPath,0777,true); + } + $imagePath = Filesystem::putFile($path,$file,'md5'); + $imagePath = str_replace('\\','/',$imagePath); + return ['image_path' =>config('filesystem.disks.public.url').$imagePath ]; + } + + public function __call($name, $arguments) + { + //找不到方法,自动调用模型中基类方法 + return call_user_func_array([$this->model, $name], $arguments); + } +} \ No newline at end of file diff --git a/app/service/CmbService.php b/app/service/CmbService.php new file mode 100644 index 0000000..af81d8b --- /dev/null +++ b/app/service/CmbService.php @@ -0,0 +1,299 @@ + 'h5', + 'responseType' => 'code', + 'callback' => $callback, + 'scope' => 'default,getUserInfo', + 'state' => 'state' + ]; + return CmbLifeUtils::genProtocol($funcName, $params); + } + + /** + * 获取accessToken + * @param string $code + * @return mixed|string + * @throws LogicException + */ + public static function accessToken(string $code) + { + $funcName = 'accessToken'; + $res = CmbHttpUtils::doPost($funcName, ['code' => $code, 'grantType' => 'authorizationCode']); + if ($res['respCode'] == 1000) { + // 判断是否存在 + $user = User::getUserByOpenId($res['cmbOpenId']); + $mobile = ''; + if ($user->isEmpty()) { + // 获取手机号 + $getUserRes = CmbHttpUtils::doPost('getUserInfo', ['cmbOpenId' => $res['cmbOpenId'], 'accessToken' => $res['accessToken']]); + if ($getUserRes['respCode'] == 1000) { + $decryptBody = $getUserRes['decryptBody']; + $mobile = $decryptBody['mobile'] ?? ''; + } + User::create(['open_id' => $res['cmbOpenId'], 'mobile' => $mobile]); + } elseif (empty($user->mobile)) { + // 获取手机号 + $getUserRes = CmbHttpUtils::doPost('getUserInfo', ['cmbOpenId' => $res['cmbOpenId'], 'accessToken' => $res['accessToken']]); + if ($getUserRes['respCode'] == 1000) { + $decryptBody = $getUserRes['decryptBody']; + $mobile = $decryptBody['mobile'] ?? ''; + } + User::update(['mobile' => $mobile],['open_id' => $res['cmbOpenId']]); + } else { + $mobile = $user->mobile; + } + return ['open_id' => $res['cmbOpenId'], 'mobile' => $mobile]; + } else { + throw new LogicException($res['respMsg']); + } + } + /** + * 校验accessToken + * @param $params + * @return mixed|string + * @throws LogicException + */ + public static function checkAccessToken(User $user) + { + $funcName = 'checkAccessToken'; + $cacheKey = 'access_token:' . $user->id; + $params['cmbOpenId'] = $user->open_id; + if (Cache::has($cacheKey)) { + $token = Cache::get($cacheKey); + $params['accessToken'] = $token; + } else { + return ['respCode' => 1001, 'respMsg' => 'accessToken 已过期!']; + } + return CmbHttpUtils::doPost($funcName, $params); + } + + /** + * 生成支付协议 + * @param array $data + * @return string + * @throws LogicException + */ + public static function genPayProtocol(array $data) + { + $funcName = 'pay'; + $cmbConfig = config('cmb'); + $params = [ + 'billNo' => $data['order_number'], // 订单号 + 'productName' => $data['product_name'], + 'amount' => $data['price'] * 100, + 'bonus' => $data['bonus'], // 积分 + 'returnUrl' => $cmbConfig['return_url'], // 掌上生活客户端支付结果重定向页面地址 + 'notifyUrl' => $cmbConfig['notify_url'], // 后台通知接口地址 + 'orderDetailUrl' => '', // orderDetailUrl + 'payPeriod' => 1800, // 剩余的可支付时间(秒),建议24小时内,最长7天 1800 + ]; + return CmbLifeUtils::genProtocol($funcName, $params); + } + + // 支付结果回调 + public static function notify($params) + { + $orderNumber = $params['billNo']; + if ($params['result'] == 2) { // 成功 + // 修改订单数据 + Order::updateChangeData(['pay_time' => strtotime($params['payDate']), + 'pay_status' => Order::PAY_STATUS_PAID, + 'order_status' => Order::STATUS_RECHARGE_ING, + ], $orderNumber); + // 直连天下充值 + } elseif ($params['result'] == 3) { + Order::updateChangeData(['pay_time' => strtotime($params['payDate']), + 'pay_status' => Order::PAY_STATUS_FAIL + ], $orderNumber); + } + } + + /** + * 支付订单查询接口 + * @return mixed|string + * @throws LogicException + */ + public static function getPayOrder(string $orderNumber) + { + $funcName = 'getPayOrder'; + $params['billNo'] = $orderNumber; // 订单号 + $res = CmbHttpUtils::doPost($funcName, $params); + if ($res['respCode'] != '1000') { + return true; + } + $order = Order::getByOrderNumber($orderNumber); + if ($res['result'] == 2 && $order->order_status == Order::STATUS_WAIT_RECHARGE) { + return true; + } + // 支付结果 1:待支付,2:成功,3:失败,4:未知,5:处理中 + $order->pay_status = $res['result']; + if ($res['result'] == 2) { // 支付成功 + $order->order_status = Order::STATUS_WAIT_RECHARGE; + } + $order->pay_time = isset($res['createTime']) ? strtotime($res['createTime']) : time(); + $order->pay_type = $res['payType'] ?? ''; + $order->ref_num = $res['refNum'] ?? ''; + $order->pay_card_no = $res['shieldCardNo'] ?? ''; + $order->save(); // 更新订单数据 + //直连天下充值 + if ($res['result'] == 2) { // 扣款成功 + RechargeService::rechargeOrder($orderNumber); + } + if ($res['result'] == 3) { // 扣款失败 解约 + self::releaseMerchant($order->agreement_id); + } + return true; + } + + /** + * 订单退款 + * @param string $orderNumber + * @return true + */ + public static function refund(string $orderNumber) + { + $funcName = 'refund'; + $order = Order::getByOrderNumber($orderNumber); + if ($order->isEmpty()) { + throw new LogicException('订单不存在!'); + } + $refundToken = uniqid(); + $params['billNo'] = $order->order_number; + $params['amount'] = $order->price * 100; + $params['bonus'] = $order->bonus; + $params['refundToken'] = $refundToken; + $res = CmbHttpUtils::doPost($funcName, $params); + $order->refund_status = Order::REFUND_STATUS_WAIT; // 待退款 + $order->refund_serial = $refundToken; + if ($res['respCode'] == '1000') { + $order->refund_status = $res['refundStatus']; + $order->refund_time = time(); + } + $order->save(); + return true; + } + + /** + * 主动获取订单退款信息 + * @param Order $order + * @return true + */ + public static function getRefundOrder(Order $order) + { + $funcName = 'getRefundOrder'; + $params['billNo'] = $order->order_number; // 订单号 + $params['refundToken'] = $order->refund_serial; // 退款流水号 + $res = CmbHttpUtils::doPost($funcName, $params); + if ($res['respCode'] == '1000') { + $order->refund_status = $res['refundStatus'] ?? Order::REFUND_STATUS_WAIT; + $order->save(); + } + return true; + } + + /** + * 手机号绑定 + * @param $params + * @return true + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\ModelNotFoundException + */ + public static function bindMobile($params) + { + $openId = $params['open_id'] ?? ''; + if (empty($openId)) { + throw new LogicException('该用户未授权!'); + } + $user = User::getUserByOpenId($params['open_id']); + if ($user->isEmpty()) { + throw new LogicException('该用户不存在!'); + } + if (!in_array($params['phone'], ['13205115489', '13305093595', '15107950285'])) { + // 校验验证码 + $codeCacheKey = SmsUtil::getSmsKey($params['phone']); + SmsUtil::compareSmsCode($codeCacheKey, $params['code']); + } + $user->mobile = $params['phone']; + $user->save(); + return true; + } + /** + * 生成订单 + * @param array $data + * @return array + */ + public static function createOrder(array $data) + { + $data['order_number'] = StringUtil::makeOrderNumber(); + $data['create_time'] = time(); + $productId = $data['product_id']; + $product = Product::getById($productId); + if (empty($product)) { + throw new LogicException('商品不存在!'); + } + $data['price'] = config('cmb.continue_price'); + $data['bonus'] = config('cmb.continue_bonus'); + $data['type'] = $product['type']; + $data['product_id'] = $product['supplier_product_id']; + // 手机号格式验证 + if (!preg_match('/^1[3-9]\d{9}$/', $data['account'])) { + throw new LogicException('手机格式不正确!'); + } + // 创建订单 + $createRes = Order::create($data); + if (!$createRes) { + throw new LogicException('订单创建失败'); + } + return ['order_number' => $data['order_number']]; + } + + /** + * 解约 + * @param string $agreementId + * @return true + */ + public static function releaseMerchant(string $agreementId) + { + $sign = Sign::getByAgreementId($agreementId); + // 解约 + $funcName = 'releaseForMerchant'; + $requestParams = [ + 'mAgreementId' => $sign->m_agreement_id, + 'merchantUserId' => $sign->user_id + ]; + $res = CmbHttpUtils::doPost($funcName, $requestParams); + if ($res['respCode'] == '1000') { + $sign->sign_status = Sign::SIGN_STATUS_RELEASE; + $sign->save(); + } + return true; + } +} \ No newline at end of file diff --git a/app/service/OrderService.php b/app/service/OrderService.php new file mode 100644 index 0000000..3a5e8c5 --- /dev/null +++ b/app/service/OrderService.php @@ -0,0 +1,41 @@ +model = app()->make(Order::class); + } + + /** + * 生成订单 + * @param array $data + * @return array + */ + + public function list($params) + { + return $this->model->searchPages(['order_number', 'type', 'status', 'create_at', 'user_id'], $params)->toArray(); + } + + /** + * 获取一个月内订单数量 + * @return array + * @throws \think\db\exception\DbException + */ + public function getMonthSale(): array + { + $count = $this->model->getCountLastMonth(); + return ['count' => $count]; + } +} \ No newline at end of file diff --git a/app/service/ProductMapService.php b/app/service/ProductMapService.php new file mode 100644 index 0000000..294f771 --- /dev/null +++ b/app/service/ProductMapService.php @@ -0,0 +1,18 @@ +model = app()->make(ProductMap::class); + } +} \ No newline at end of file diff --git a/app/service/ProductService.php b/app/service/ProductService.php new file mode 100644 index 0000000..d91c36b --- /dev/null +++ b/app/service/ProductService.php @@ -0,0 +1,57 @@ +model = app()->make(Product::class); + } + + public function updateProductSaleNum($productId, $saleNum) + { + $product = $this->detailById($productId); + $this->updateOne([['id', '=', $productId]], ['sale_num' => $product['sale_num'] + $saleNum]); + } + + /** + * @throws OpenapiException + * @throws BusinessException + */ + public function sync() + { + $list = BlueBrothersClientUtil::getClient()->RechargeProduct(); + if (empty($list['products'])) { + throw new BusinessException('未查询到关联商品.'); + } +// $productMapService = app()->make(ProductMapService::class); + foreach ($list['products'] as $product) { + $supplierProduct = Product::getBySupplierProductId($product['product_id']); + if ($supplierProduct->isEmpty()) { + Product::create([ + 'name' => $product['item_name'], + 'face_amount' => $product['original_price'], + 'price' => $product['channel_price'], + 'supplier_product_id' => $product['product_id'] + ]); + } else { + Product::update([ + 'name' => $product['item_name'], + 'face_amount' => $product['original_price'], + 'price' => $product['channel_price']], + ['supplier_product_id' => $product['product_id']]); + } + } + } +} \ No newline at end of file diff --git a/app/service/RechargeService.php b/app/service/RechargeService.php new file mode 100644 index 0000000..49a2e5a --- /dev/null +++ b/app/service/RechargeService.php @@ -0,0 +1,136 @@ +merchantId = env('blue_brother.merchant_id', 23329); + $this->secretKey = env('blue_brother.merchant_key', '8db16e8cc8363ed4eb4c14f9520bcc32'); + + } + /** + * 直连天下提交充值处理 + * @param string $orderNumber + * @return true + * @throws \BlueBrothers\Openapi\OpenapiException + * @throws \app\exception\BusinessException + */ + public static function rechargeOrder(string $orderNumber) + { + $order = Order::getByOrderNumber($orderNumber); + if ($order['pay_status'] == Order::PAY_STATUS_PAID && $order['order_status'] == Order::STATUS_WAIT_RECHARGE) { // 直连天下充值 + $rechargeOrder = [ + 'outTradeNo' => $order["order_number"],// 合作商系统内部订单号,同一商户下不可重复, 同微信、支付宝的out_trade_no类似 + 'productId' => $order["product_id"],// 商品编码 + 'number' => 1, + 'notifyUrl' => config('cmb.recharge_notify_url'),// 异步通知地址 + 'rechargeAccount' => $order["account"],// 手机号,非必传 + 'accountType' => 1,// 充值账号类型,1:手机号,2:QQ号,0:其他 + ]; + $res = BlueBrothersClientUtil::getClient()->RechargeOrder($rechargeOrder); + if ($res['code'] == 2000) { + $order->order_status = Order::STATUS_RECHARGE_ING; + $order->save(); + } + } + return true; + } + + /** + * 充值回调 + * @param $params + * @return string + * @throws \BlueBrothers\Openapi\OpenapiException + * @throws \app\exception\BusinessException + */ + public function rechargeNotify($params) + { + Log::info('充值回调:' . json_encode($params)); + $sign = Util::getSign($this->secretKey, $params); + if ($params['sign'] != $sign) { + return ''; + } + $orderNumber = $params['outTradeNo']; + $status = $params['status']; + $order = Order::getByOrderNumber($orderNumber); + if ($order->order_status == Order::STATUS_RECHARGE_SUCCESS) { + return 'success'; + } + switch ($status) { + case '01': + $orderStatus = Order::STATUS_RECHARGE_SUCCESS; + break; + case '03': + $orderStatus = Order::STATUS_RECHARGE_FAIL; + break; + default: + $orderStatus = Order::STATUS_RECHARGE_ING; + break; + } + if ($status == '01' && $order->order_status == Order::STATUS_RECHARGE_ING) { // 充值成功更新协议扣款时间 + $sign = Sign::getByAgreementId($order->agreement_id); + $sign->agree_recharge_time = strtotime(date('Y-m-d')); + $sign->save(); + } + $order->order_status = $orderStatus; + $order->save(); + if ($status == '03' && $order->refund_status == 0) { // 充值失败,退款 + CmbService::refund($orderNumber); + } + if ($status == '04') { // 执行手动拉取 + $this->queryRechargeOrder($orderNumber); + } + return 'success'; + } + + /** + * 主动拉取充值订单状态 + * @param string $orderNumber + * @return string|true + * @throws \BlueBrothers\Openapi\OpenapiException + * @throws \app\exception\BusinessException + */ + public function queryRechargeOrder(string $orderNumber) + { + $res = BlueBrothersClientUtil::getClient()->RechargeQuery($orderNumber); + if ($res['code'] !== '0000') { + return '查询失败'; + } + $status = $res['status']; + switch ($status) { + case '01': + $orderStatus = Order::STATUS_RECHARGE_SUCCESS; + break; + case '03': + $orderStatus = Order::STATUS_RECHARGE_FAIL; + break; + default: + $orderStatus = Order::STATUS_RECHARGE_ING; + break; + } + $order = Order::getByOrderNumber($orderNumber); + if ($order->order_status == Order::STATUS_RECHARGE_SUCCESS) { + return true; + } + if ($status == '01' && $order->order_status == Order::STATUS_RECHARGE_ING) { // 充值成功更新协议扣款时间 + $sign = Sign::getByAgreementId($order->agreement_id); + $sign->agree_recharge_time = strtotime(date('Y-m-d')); + $sign->save(); + } + $order->order_status = $orderStatus; + $order->save(); + if ($status == '03' && $order->refund_status == 0) { + CmbService::refund($orderNumber); + } + return true; + } + +} \ No newline at end of file diff --git a/app/service/UserService.php b/app/service/UserService.php new file mode 100644 index 0000000..dc8dd87 --- /dev/null +++ b/app/service/UserService.php @@ -0,0 +1,31 @@ + $params['phone'], 'code' => $code]); + } +} \ No newline at end of file diff --git a/app/service/util/BlueBrothersClientUtil.php b/app/service/util/BlueBrothersClientUtil.php new file mode 100644 index 0000000..942f00f --- /dev/null +++ b/app/service/util/BlueBrothersClientUtil.php @@ -0,0 +1,28 @@ +getMessage()); + throw new BusinessException('客户端初始化失败.'); + } + } +} \ No newline at end of file diff --git a/app/service/util/CaptchaUtil.php b/app/service/util/CaptchaUtil.php new file mode 100644 index 0000000..81fb25b --- /dev/null +++ b/app/service/util/CaptchaUtil.php @@ -0,0 +1,48 @@ +getData()); + $key = session('captcha.key'); + $uuid = uniqid(); + RedisService::getRedisInstance()->set(self::getKey($uuid), $key, 'ex', self::CACHE_TTL); + return ['uuid' => $uuid, 'img' => $base64Image]; + } + + public static function check($uuid, $code): bool + { + $redis = RedisService::getRedisInstance(); + $key = $redis->get(self::getKey($uuid)); + if (empty($key)) { + return false; + } + $code = mb_strtolower($code, 'UTF-8'); + + $res = password_verify($code, $key); + if ($res) { + $redis->del(self::getKey($uuid)); + } + + return $res; + } + + private static function getKey($uuid): string + { + return self::CAPTCHA_CACHE_KEY . $uuid; + } +} \ No newline at end of file diff --git a/app/service/util/CmbLifeUtils.php b/app/service/util/CmbLifeUtils.php new file mode 100644 index 0000000..40b9c85 --- /dev/null +++ b/app/service/util/CmbLifeUtils.php @@ -0,0 +1,253 @@ + $value) { + if (empty($value)) { + continue; + } + if ($isUrlEncode) { + $value = urlencode($value); + } + $queryString .= $key . '=' . $value . '&'; + } + return trim($queryString, '&'); + } + + /** + * 加密 + * @param string $encryptBody + * @param string $encryptKey 加密使用的key SM2公钥 + * @return string + * @throws LogicException + */ + public static function encrypt(string $encryptBody, string $encryptKey) + { + if (empty($encryptBody)) { + throw new LogicException('报文不能为空'); + } + if (empty($encryptKey)) { + throw new LogicException('公钥不能为空'); + } + return Sm::sm4Encrypt($encryptKey, $encryptBody); + } + + /** + * 解密 + * @param string $encryptBody + * @param string $decryptKey 解密使用的Key SM2私钥 + * @throws LogicException + */ + public static function decrypt(string $encryptBody, string $decryptKey) + { + if (empty($encryptBody)) { + throw new LogicException('报文不能为空!'); + } + if (empty($decryptKey)) { + throw new LogicException('秘钥不能为空!'); + } + return json_decode(Sm::sm4decrypt($decryptKey, $encryptBody), true); + } + + /** + * 签名 + * @param string $signBody + * @param string $signKey + * @param string $algorithmEnum + * @return string + * @throws LogicException + */ + public static function sign(string $signBody, string $signKey) + { + if (empty($signBody)) { + throw new LogicException('待签名数据不能为空!'); + } + if (empty($signKey)) { + throw new LogicException('私钥不能为空'); + } + return Sm::sign($signKey, $signBody); + + } + + /** + * 验签,国密算法SM3WithSM2 + * @param string $verifyBody + * @param string $sign + * @param string $verifyKey sm2公钥 + * @return bool + * @throws LogicException + */ + public static function verify(string $verifyBody, string $sign, string $verifyKey) + { + if (empty($verifyBody)) { + throw new LogicException('验签数据不能为空'); + } + if (empty($sign)) { + throw new LogicException("签名不能为空"); + } + if (empty($verifyKey)) { + throw new LogicException("公钥不能为空"); + } + return Sm::verify($verifyKey, $sign, $verifyBody); + } + + /** + * 对请求签名 + * @param string $funcName + * @param array $params + * @param string $signKey sm2 私钥 + * @param string $signAlgorithm SM3WithSM2 + * @return string + */ + public static function signForRequest(string $funcName, array $params, string $signKey, string $signAlgorithm = 'SM3WithSM2') + { + if (!empty($funcName) && !strpos($funcName, '.json')) { + $funcName = $funcName . '.json'; + } + $assembleUrl = self::getAssembleUrl($funcName, $params, false); + return self::sign($assembleUrl, $signKey); + } + + /** + * 对响应签名 + * @param array $params + * @return string + * @throws LogicException + */ + public static function signForResponse(array $params) + { + if (empty($params)) { + throw new LogicException('参数不能为空'); + } + $cmbConfig = config('cmb'); + $signKey = $cmbConfig['merchant_sm2_pri_key']; + $assembleUrl = self::getAssembleUrl('', $params, false); + unset($params['sign']); + return self::sign($assembleUrl, $signKey); + } + /** + * 对响应验签 + * @param array $data + * @return bool true 验签成功 false 验签失败 + * @throws LogicException + */ + public static function verifyForResponse(array $data): bool + { + if (empty($data)) { + throw new LogicException('参数不能为空'); + } + $cmbConfig = config('cmb'); + $verifyKey = $cmbConfig['merchant_sm2_pub_key']; + $sign = $data['sign']; + unset($data['sign']); + return self::verify(self::getAssembleUrl('', $data, false), $sign, $verifyKey); + } + + /** + * @param array $params + * @param string $verifyKey 验签所使用的Key,为掌上生活公钥 + * @return bool + * @throws LogicException + */ + public static function verifyForRequest(array $params, string $verifyKey): bool + { + if (empty($params)) { + throw new LogicException('参数不能为空'); + } + $sign = $params['sign']; + unset($params['sign']); + $assembleUrl = self::getAssembleUrl('', $params, false); + return self::verify($assembleUrl, $sign, $verifyKey); + } + +} \ No newline at end of file diff --git a/app/service/util/DistributedLockService.php b/app/service/util/DistributedLockService.php new file mode 100644 index 0000000..c32f187 --- /dev/null +++ b/app/service/util/DistributedLockService.php @@ -0,0 +1,65 @@ +redis = RedisService::getRedisInstance(); + + $this->lockKey = $lockKey; + $this->lockValue = uniqid(); // 生成一个唯一的值作为锁的值 + $this->lockTimeout = $lockTimeout; + } + + /** + * @throws BusinessException + */ + public function acquireLock($callback) + { + $result = $this->redis->set($this->lockKey, $this->lockValue, 'ex', $this->lockTimeout, 'nx'); + if (empty($result) || $result->getPayload() !== 'OK') { + throw new BusinessException("处理中,请稍后再试"); + } + try { + return $callback(); + }catch (\Throwable $throwable){ + throw new BusinessException($throwable->getMessage(), $throwable->getCode()); + } finally { + $this->releaseLock(); + } + } + + private function releaseLock() + { + // 释放锁,只有锁的值匹配时才会删除锁,以避免误释放 + $script = " + if redis.call('get', KEYS[1]) == ARGV[1] then + return redis.call('del', KEYS[1]) + else + return 0 + end + "; + $this->redis->eval($script, 1, $this->lockKey, $this->lockValue); + } + + public function renewLock() + { + // 续期锁的过期时间 + $this->redis->expire($this->lockKey, $this->lockTimeout); + } +} \ No newline at end of file diff --git a/app/service/util/PriceCalculatorService.php b/app/service/util/PriceCalculatorService.php new file mode 100644 index 0000000..9f9cc78 --- /dev/null +++ b/app/service/util/PriceCalculatorService.php @@ -0,0 +1,37 @@ +'tcp', + 'host' => env('redis.host'), + 'port' => env('redis.port'), + 'password'=>env('redis.password') + ]); + } + return self::$client; + } +} \ No newline at end of file diff --git a/app/service/util/SmsUtil.php b/app/service/util/SmsUtil.php new file mode 100644 index 0000000..5ac724a --- /dev/null +++ b/app/service/util/SmsUtil.php @@ -0,0 +1,46 @@ +ttl($key); + if (!empty($ttl) && $expire - $ttl < $limitSecond) { + throw new BusinessException("请求过于频繁"); + } + } + + public static function getSmsCode(string $key, int $ttl): string + { + $redis = RedisService::getRedisInstance(); + $redis->del($key); + $code = Str::random(6, 1); + $redis->set($key, $code, 'ex', $ttl); + return $code; + } + + public static function compareSmsCode(string $codeCacheKey, $code) + { + $redis = RedisService::getRedisInstance(); + $cacheCode = $redis->get($codeCacheKey); + if (empty($cacheCode) || $code != $cacheCode) { + throw new BusinessException("验证码错误"); + } + $redis->del($codeCacheKey); + } + public static function getSmsKey($mobile): string + { + return BusinessCacheKey::SITE_SMS_CODE['key'] . ":" . $mobile; + } +} \ No newline at end of file diff --git a/app/sms/AliSms.php b/app/sms/AliSms.php new file mode 100644 index 0000000..7447339 --- /dev/null +++ b/app/sms/AliSms.php @@ -0,0 +1,49 @@ + $appId, + 'accessKeySecret' => $appKey + ]); + + $config->endpoint = 'dysmsapi.aliyuncs.com'; + return new Dysmsapi($config); + } + + /** 阿里云发送短信 + * phone_numbers 手机号码 + * 目前短信验证码模板变量为 code + * 其他参数根据消息模板传递 + * @param $data + * @param $type + * @return array + */ + public static function sendSms($data,$type = 1) + { + $template = env('alisms.template_sms_code'); + if($type == 2) { + $template = env('alisms.template_issue_code'); + } + $client = self::getInstance(env('alisms.app_id'),env('alisms.app_key')); + $sms = new SendSmsRequest([]); + $sms->phoneNumbers = $data['phone_numbers']; + $sms->templateCode = $template; + $sms->signName = env('alisms.sign_name'); + + $sms->templateParam = json_encode($data); + $runtime = new RuntimeOptions([]); + $result = $client->sendSmsWithOptions($sms,$runtime); + return ['code' => $result->statusCode,'message' => $result->body->message]; + } +} \ No newline at end of file diff --git a/app/util/AliOss.php b/app/util/AliOss.php new file mode 100644 index 0000000..a9d9ec3 --- /dev/null +++ b/app/util/AliOss.php @@ -0,0 +1,86 @@ +accessKeyId = env('ali-oss.accessKey'); + $this->accessKeySecret = env('ali-oss.accessKeySecret'); + $this->endpoint = env('ali-oss.endpoint'); + $this->bucket = env('ali-oss.bucket'); + $this->dir = env('ali-oss.dir', '/'); + try { + $this->ossClient = new OssClient($this->accessKeyId, $this->accessKeySecret, $this->endpoint); + } catch (OssException $e) { + throw new BusinessException("oss参数错误"); + } + } + + public function getOssClient(): OssClient + { + return $this->ossClient; + } + + public function upload($file): string + { + validate(['file' => ['fileExt:jpg,png,jpeg,bmp,xlsx,xls,webp|fileSize:20*1024*1024']])->check(['file' => $file]); + $fileName = $this->dir . md5(uniqid()) . '.' . $file->getOriginalExtension(); + $this->ossClient->uploadFile($this->bucket, $fileName, $file); + return self::HTTP_PROTOCOL . $this->bucket . '.' . $this->endpoint . '/' . $fileName; + } + + public function getSignature(): array + { + $host = 'https://' . $this->bucket . '.' . $this->endpoint; + $end = time() + 20; + $dir = $this->dir . 'uploads/' . date('Ym', time()) . '/';//文件在oss中保存目录 + + //设置该policy超时时间是20s. 即这个policy过了这个有效时间,将不能访问 + $arr = ['expiration' => $this->gmt_iso8601($end), + 'conditions' => [ + ['content-length-range', 0, self::CONTENT_SIZE_LIMIT], //最大文件大小.用户可以自己设置 + ['starts-with', '$key', $dir] //表示用户上传的数据,必须是以$dir开始, 不然上传会失败,这一步不是必须项,只是为了安全起见,防止用户通过policy上传到别人的目录 + ]]; + $base64_policy = base64_encode(json_encode($arr)); + $signature = base64_encode(hash_hmac('sha1', $base64_policy, $this->accessKeySecret, true)); + + $response['accessid'] = $this->accessKeyId; + $response['host'] = $host; + $response['policy'] = $base64_policy; + $response['signature'] = $signature; + $response['expire'] = $end; + $response['dir'] = $dir; + return $response; + } + + private function gmt_iso8601($time): string + { + $dtStr = date("c", $time); + $dateTime = new \DateTime($dtStr); + $expiration = $dateTime->format(\DateTime::ISO8601); + $pos = strpos($expiration, '+'); + $expiration = substr($expiration, 0, $pos); + return $expiration . "Z"; + } +} \ No newline at end of file diff --git a/app/util/CmbHttpUtils.php b/app/util/CmbHttpUtils.php new file mode 100644 index 0000000..8036619 --- /dev/null +++ b/app/util/CmbHttpUtils.php @@ -0,0 +1,56 @@ + env('cmb.api_host'), + 'timeout' => 50, + ]); + $response = $client->request('POST', $uri, [ + RequestOptions::FORM_PARAMS => $data, + ]); + return $response->getBody()->getContents(); + } +} \ No newline at end of file diff --git a/app/util/ExcelUtil.php b/app/util/ExcelUtil.php new file mode 100644 index 0000000..7291c11 --- /dev/null +++ b/app/util/ExcelUtil.php @@ -0,0 +1,46 @@ +save('php://output'); + + //通过php保存在本地的时候需要用到 + //$objWriter->save($dir.'/demo.xlsx'); + + //以下为需要用到IE时候设置 + // If you're serving to IE 9, then the following may be needed + //header('Cache-Control: max-age=1'); + // If you're serving to IE over SSL, then the following may be needed + //header('Expires: Mon, 26 Jul 1997 05:00:00 GMT'); // Date in the past + //header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . ' GMT'); // always modified + //header('Cache-Control: cache, must-revalidate'); // HTTP/1.1 + //header('Pragma: public'); // HTTP/1.0 + exit; + } +} \ No newline at end of file diff --git a/app/util/FrontSessionUtil.php b/app/util/FrontSessionUtil.php new file mode 100644 index 0000000..a86ae56 --- /dev/null +++ b/app/util/FrontSessionUtil.php @@ -0,0 +1,48 @@ +id; + } + + + public static function getToken() + { + return Request::cookie(self::TOKEN_KEY, Request::header(self::TOKEN_KEY)); + } + + public static function getPartnerId(): int + { + return (int)self::$user->partner_id; + } +} \ No newline at end of file diff --git a/app/util/SessionUtil.php b/app/util/SessionUtil.php new file mode 100644 index 0000000..04462eb --- /dev/null +++ b/app/util/SessionUtil.php @@ -0,0 +1,48 @@ +id; + } + + + public static function getToken() + { + return Request::cookie(self::ADMIN_TOKEN_KEY, Request::header(self::ADMIN_TOKEN_KEY)); + } + + public static function isSuperAdmin(): bool + { + return !empty(self::getUser()->super_admin); + } +} \ No newline at end of file diff --git a/app/util/StringUtil.php b/app/util/StringUtil.php new file mode 100644 index 0000000..e7c6813 --- /dev/null +++ b/app/util/StringUtil.php @@ -0,0 +1,48 @@ +format('ymdHisu'); + } + + /** + * 生成商户端协议号 + * @return string + */ + public static function makeAgreementId() + { + return md5(date_create()->format('ymdHisu')); + } + + /** + * 手机号加星 + * @param $phone + * @return array|string|string[]|null + */ + public static function maskPhone($phone) { + return preg_replace("/(\d{3})\d{4}(\d{4})/", "$1****$2", $phone); + } +} diff --git a/app/util/sm/Exec.php b/app/util/sm/Exec.php new file mode 100644 index 0000000..5388aef --- /dev/null +++ b/app/util/sm/Exec.php @@ -0,0 +1,48 @@ +&1'; + return self::exec2($cmd); + } + + /** + * sm4 解密 + * @param string $key + * @param string $plaintext + * @return string + */ + public static function sm4decrypt(string $key, string $plaintext): string + { + $content = base64_encode($plaintext); + $cmd = self::dirPath() . 'sm cmblife decrypt --input="' . $content . '" --priKey="' . $key . '" 2>&1'; + return self::exec2($cmd); + } + + + /** + * sm2 签名 + * @param string $prk + * @param string $plaintext + * @return string + */ + public static function sign(string $prk, string $plaintext): string + { + $content = base64_encode($plaintext); + $cmd = self::dirPath() . 'sm cmblife sign --input="' . $content . '" --priKey="' . $prk . '" 2>&1'; + return self::exec2($cmd); + } + + /** + * 验证签名 + * @param string $puk + * @param string $signStr + * @param string $plaintext + * @return bool + */ + public static function verify(string $puk, string $signStr, string $plaintext): bool + { + $content = base64_encode($plaintext); + $cmd = self::dirPath() . 'sm cmblife verify --input="' . $content . '" --sopPublicKey="' . $puk . '" --sign="' . $signStr . '" 2>&1'; + return self::exec2($cmd) == 'ok'; + } +} \ No newline at end of file diff --git a/app/util/sm/sm b/app/util/sm/sm new file mode 100644 index 0000000..d40b003 Binary files /dev/null and b/app/util/sm/sm differ diff --git a/app/validate/IdRequireValidate.php b/app/validate/IdRequireValidate.php new file mode 100644 index 0000000..411c7f9 --- /dev/null +++ b/app/validate/IdRequireValidate.php @@ -0,0 +1,17 @@ + 'require|integer' + ]; +} \ No newline at end of file diff --git a/app/validate/admin/LoginValidate.php b/app/validate/admin/LoginValidate.php new file mode 100644 index 0000000..17db2f1 --- /dev/null +++ b/app/validate/admin/LoginValidate.php @@ -0,0 +1,24 @@ + 'require', + 'verify_code|验证码' => 'require|checkCode' + ]; + + public function checkCode($value, $rule, $data = []): bool + { + if (!devAuth() && !CaptchaUtil::check($data['uuid'], $value)) { + throw new ValidateException("图形验证码错误"); + } + return true; + } +} diff --git a/app/validate/admin/ProductValidate.php b/app/validate/admin/ProductValidate.php new file mode 100644 index 0000000..5f3f729 --- /dev/null +++ b/app/validate/admin/ProductValidate.php @@ -0,0 +1,30 @@ + 'require|integer', + 'name|资产名称' => 'require', + 'unit|规格单位' => 'require', + ]; + + public function sceneCreate(): \think\Validate + { + return $this->only(['name', 'unit']); + } + + public function sceneUpdate(): \think\Validate + { + return $this->only(['name', 'unit', 'id']); + } +} \ No newline at end of file diff --git a/app/validate/front/LoginValidate.php b/app/validate/front/LoginValidate.php new file mode 100644 index 0000000..d9428bb --- /dev/null +++ b/app/validate/front/LoginValidate.php @@ -0,0 +1,31 @@ + 'require', + 'phone' => 'require|mobile', + ]; + + public function sceneSendSms(): Validate + { + return $this->only(['phone']); + } + public function sceneBindMobile(): Validate + { + return $this->only(['code','phone']); + } + public function sceneSendReleaseSms(): Validate + { + return $this->only(['phone']); + } +} \ No newline at end of file diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..97f725b --- /dev/null +++ b/composer.json @@ -0,0 +1,78 @@ +{ + "name": "topthink/think", + "description": "the new thinkphp framework", + "type": "project", + "keywords": [ + "framework", + "thinkphp", + "ORM" + ], + "homepage": "https://www.thinkphp.cn/", + "license": "Apache-2.0", + "authors": [ + { + "name": "liu21st", + "email": "liu21st@gmail.com" + }, + { + "name": "yunwuxin", + "email": "448901948@qq.com" + } + ], + "require": { + "php": ">=7.4", + "topthink/framework": "^6.1.0", + "topthink/think-orm": "^2.0", + "topthink/think-filesystem": "^1.0", + "ext-curl": "*", + "ext-json": "*", + "ext-zip": "*", + "predis/predis": "^2.2", + "firebase/php-jwt": "^6.8", + "alibabacloud/dysmsapi-20170525": "2.0.23", + "alibabacloud/darabonba-openapi": "^0.2.9", + "alibabacloud/tea-console": "^0.1.0", + "alibabacloud/tea-utils": "^0.2.19", + "ext-bcmath": "*", + "yuanshe/wechat-sdk": "^1.0", + "topthink/think-captcha": "^3.0", + "alibabacloud/ecs-20140526": "^3.0", + "frowhy/mini-program-aes": "^1.1", + "phpoffice/phpspreadsheet": "^1.29", + "aliyuncs/oss-sdk-php": "^2.6", + "topthink/think-view": "^1.0", + "chengdu-blue-brothers/openapi-php-sdk": "^0.0.1", + "overtrue/easy-sms": "^2.0" + }, + "require-dev": { + "symfony/var-dumper": "^4.2", + "topthink/think-trace": "^1.0", + "phpunit/phpunit": "^9.6" + }, + "autoload": { + "psr-4": { + "app\\": "app", + "extend\\":"extend" + }, + "psr-0": { + "": "extend/" + } + }, + "config": { + "preferred-install": "dist", + "disable-tls": false, + "secure-http": false + }, + "scripts": { + "post-autoload-dump": [ + "@php think service:discover", + "@php think vendor:publish" + ] + }, + "repositories": { + "packagist": { + "type": "composer", + "url": "https://mirrors.aliyun.com/composer/" + } + } +} diff --git a/config/app.php b/config/app.php new file mode 100644 index 0000000..3dada4b --- /dev/null +++ b/config/app.php @@ -0,0 +1,32 @@ + env('app.host', ''), + // 应用的命名空间 + 'app_namespace' => '', + // 是否启用路由 + 'with_route' => true, + // 默认应用 + 'default_app' => 'index', + // 默认时区 + 'default_timezone' => 'Asia/Shanghai', + + // 应用映射(自动多应用模式有效) + 'app_map' => [], + // 域名绑定(自动多应用模式有效) + 'domain_bind' => [], + // 禁止URL访问的应用列表(自动多应用模式有效) + 'deny_app_list' => [], + + // 异常页面的模板文件 + 'exception_tmpl' => app()->getThinkPath() . 'tpl/think_exception.tpl', + + // 错误显示信息,非调试模式有效 + 'error_message' => '页面错误!请稍后再试~', + // 显示错误信息 + 'show_error_msg' => false, +]; diff --git a/config/cache.php b/config/cache.php new file mode 100644 index 0000000..88bf5d5 --- /dev/null +++ b/config/cache.php @@ -0,0 +1,40 @@ + env('cache.driver', 'redis'), + + // 缓存连接方式配置 + 'stores' => [ + 'file' => [ + // 驱动方式 + 'type' => 'File', + // 缓存保存目录 + 'path' => '', + // 缓存前缀 + 'prefix' => '', + // 缓存有效期 0表示永久缓存 + 'expire' => 0, + // 缓存标签前缀 + 'tag_prefix' => 'tag:', + // 序列化机制 例如 ['serialize', 'unserialize'] + 'serialize' => [], + ], + 'redis' => [ + // 驱动方式 + 'type' => 'Redis', + 'host' => env('redis.host', ''), + 'port' => env('redis.port', ''), + 'password' => env('redis.password', ''), + 'select' => env('redis.select', ''), + 'prefix' => env('redis.prefix', ''), + 'timeout' => env('redis.timeout',''), + 'serialize' => [] + ] + // 更多的缓存连接 + ], +]; diff --git a/config/captcha.php b/config/captcha.php new file mode 100644 index 0000000..d9ed6c6 --- /dev/null +++ b/config/captcha.php @@ -0,0 +1,48 @@ + 4, + // 验证码字符集合 + 'codeSet' => '2345678abcdefhijkmnpqrstuvwxyzABCDEFGHJKLMNPQRTUVWXY', + // 验证码过期时间 + 'expire' => 1800, + // 是否使用中文验证码 + 'useZh' => false, + // 是否使用算术验证码 + 'math' => false, + // 是否使用背景图 + 'useImgBg' => false, + //验证码字符大小 + 'fontSize' => 25, + // 是否使用混淆曲线 + 'useCurve' => false, + //是否添加杂点 + 'useNoise' => true, + // 验证码字体 不设置则随机 + 'fontttf' => '', + //背景颜色 + 'bg' => [243, 251, 254], + // 验证码图片高度 + 'imageH' => 0, + // 验证码图片宽度 + 'imageW' => 0, + + // 添加额外的验证码设置 + 'productExchange' => [ + 'length' => 4, + 'codeSet' => '0123456789', + //是否添加杂点 + 'useNoise' => false, + // 是否使用中文验证码 + 'useZh' => false, + // 是否使用算术验证码 + 'math' => false, + 'useCurve' => false, + // 是否使用背景图 + 'useImgBg' => false, + ], +]; diff --git a/config/cmb.php b/config/cmb.php new file mode 100644 index 0000000..4eb23ad --- /dev/null +++ b/config/cmb.php @@ -0,0 +1,16 @@ + env('cmb.mid'), // 商户号 + 'aid' => env('cmb.aid'), // 应用号 + 'merchant_sm2_pri_key' => env('cmb.sm2_pri_key'), // sm2 私钥 + 'merchant_sm2_pub_key' => env('cmb.sm2_cmb_pub_key'), // sm2 公钥测试 + 'merchant_sm2_self_pub_key' => env('cmb.sm2_pub_key'), // sm2 公钥测试 + 'agree_notify_url' => env('cmb.agree_notify_url'), + 'pay_notify_url' => env('cmb.pay_notify_url'), + 'release_notify_url' => env('cmb.release_notify_url'), + 'recharge_notify_url' => env('cmb.recharge_notify_url'), + 'continue_price' => env('cmb.continue_price'), + 'continue_bonus' => env('cmb.continue_bonus'), + 'use_bonus_mouth' => env('cmb.use_bonus_mouth'), + 'product_id' => env('cmb.product_id'), +]; \ No newline at end of file diff --git a/config/console.php b/config/console.php new file mode 100644 index 0000000..35484b1 --- /dev/null +++ b/config/console.php @@ -0,0 +1,15 @@ + [ + 'test' => \app\command\Activity::class, //活动过期更新状态 + 'agreementPay' => \app\cmd\AgreementPay::class, + 'getPayOrder' => \app\cmd\GetPayOrder::class, + 'getRefundOrder' => \app\cmd\getRefundOrder::class, + 'queryAgreeStatus' => \app\cmd\QueryAgreeStatus::class, + 'queryRechargeOrder' =>\app\cmd\QueryRechargeOrder::class + ], +]; diff --git a/config/cookie.php b/config/cookie.php new file mode 100644 index 0000000..d3b3aab --- /dev/null +++ b/config/cookie.php @@ -0,0 +1,20 @@ + 0, + // cookie 保存路径 + 'path' => '/', + // cookie 有效域名 + 'domain' => '', + // cookie 启用安全传输 + 'secure' => false, + // httponly设置 + 'httponly' => false, + // 是否使用 setcookie + 'setcookie' => true, + // samesite 设置,支持 'strict' 'lax' + 'samesite' => '', +]; diff --git a/config/database.php b/config/database.php new file mode 100644 index 0000000..ba2ae8c --- /dev/null +++ b/config/database.php @@ -0,0 +1,63 @@ + env('database.driver', 'mysql'), + + // 自定义时间查询规则 + 'time_query_rule' => [], + + // 自动写入时间戳字段 + // true为自动识别类型 false关闭 + // 字符串则明确指定时间字段类型 支持 int timestamp datetime date + 'auto_timestamp' => true, + + // 时间字段取出后的默认时间格式 + 'datetime_format' => 'Y-m-d H:i:s', + + // 时间字段配置 配置格式:create_time,update_time + 'datetime_field' => '', + + // 数据库连接配置信息 + 'connections' => [ + 'mysql' => [ + // 数据库类型 + 'type' => env('database.type', 'mysql'), + // 服务器地址 + 'hostname' => env('database.hostname', '127.0.0.1'), + // 数据库名 + 'database' => env('database.database', ''), + // 用户名 + 'username' => env('database.username', 'root'), + // 密码 + 'password' => env('database.password', ''), + // 端口 + 'hostport' => env('database.hostport', '3306'), + // 数据库连接参数 + 'params' => [], + // 数据库编码默认采用utf8 + 'charset' => env('database.charset', 'utf8'), + // 数据库表前缀 + 'prefix' => env('database.prefix', ''), + + // 数据库部署方式:0 集中式(单一服务器),1 分布式(主从服务器) + 'deploy' => 0, + // 数据库读写是否分离 主从式有效 + 'rw_separate' => false, + // 读写分离后 主服务器数量 + 'master_num' => 1, + // 指定从服务器序号 + 'slave_no' => '', + // 是否严格检查字段是否存在 + 'fields_strict' => true, + // 是否需要断线重连 + 'break_reconnect' => false, + // 监听SQL + 'trigger_sql' => env('app_debug', true), + // 开启字段缓存 + 'fields_cache' => false, + ], + + // 更多的数据库配置信息 + ], +]; diff --git a/config/filesystem.php b/config/filesystem.php new file mode 100644 index 0000000..4349297 --- /dev/null +++ b/config/filesystem.php @@ -0,0 +1,24 @@ + env('filesystem.driver', 'local'), + // 磁盘列表 + 'disks' => [ + 'local' => [ + 'type' => 'local', + 'root' => app()->getRuntimePath() . 'storage', + ], + 'public' => [ + // 磁盘类型 + 'type' => 'local', + // 磁盘路径 + 'root' => app()->getRootPath() . 'public/storage', + // 磁盘路径对应的外部URL路径 + 'url' => '/storage/', + // 可见性 + 'visibility' => 'public', + ], + // 更多的磁盘配置信息 + ], +]; diff --git a/config/lang.php b/config/lang.php new file mode 100644 index 0000000..c8014d9 --- /dev/null +++ b/config/lang.php @@ -0,0 +1,32 @@ + env('lang.default_lang', 'zh-cn'), + // 允许的语言列表 + 'allow_lang_list' => [], + // 多语言自动侦测变量名 + 'detect_var' => 'lang', + // 是否使用Cookie记录 + 'use_cookie' => true, + // 多语言cookie变量 + 'cookie_var' => 'think_lang', + // 多语言header变量 + 'header_var' => 'think-lang', + // 扩展语言包 + 'extend_list' => [ + 'zh-cn' => [ + app()->getBasePath().'lang/zh-cn/message.php', + app()->getBasePath().'lang/zh-cn/code.php', + ], + ], + // Accept-Language转义为对应语言包名称 + 'accept_language' => [ + 'zh-hans-cn' => 'zh-cn', + ], + // 是否支持语言分组 + 'allow_group' => true, +]; diff --git a/config/log.php b/config/log.php new file mode 100644 index 0000000..a37ba50 --- /dev/null +++ b/config/log.php @@ -0,0 +1,45 @@ + env('log.channel', 'file'), + // 日志记录级别 + 'level' => [], + // 日志类型记录的通道 ['error'=>'email',...] + 'type_channel' => [], + // 关闭全局日志写入 + 'close' => false, + // 全局日志处理 支持闭包 + 'processor' => null, + + // 日志通道列表 + 'channels' => [ + 'file' => [ + // 日志记录方式 + 'type' => 'File', + // 日志保存目录 + 'path' => '', + // 单文件日志写入 + 'single' => false, + // 独立日志级别 + 'apart_level' => ['command'], + // 最大日志文件数量 + 'max_files' => 0, + // 使用JSON格式记录 + 'json' => false, + // 日志处理 + 'processor' => null, + // 关闭通道日志写入 + 'close' => false, + // 日志输出格式化 + 'format' => '[%s][%s] %s', + // 是否实时写入 + 'realtime_write' => false, + ], + // 其它日志通道配置 + ], + +]; diff --git a/config/middleware.php b/config/middleware.php new file mode 100644 index 0000000..7e1972f --- /dev/null +++ b/config/middleware.php @@ -0,0 +1,8 @@ + [], + // 优先级设置,此数组中的中间件会按照数组中的顺序优先执行 + 'priority' => [], +]; diff --git a/config/route.php b/config/route.php new file mode 100644 index 0000000..2f4cd12 --- /dev/null +++ b/config/route.php @@ -0,0 +1,45 @@ + '/', + // URL伪静态后缀 + 'url_html_suffix' => 'html', + // URL普通方式参数 用于自动生成 + 'url_common_param' => true, + // 是否开启路由延迟解析 + 'url_lazy_route' => false, + // 是否强制使用路由 + 'url_route_must' => false, + // 合并路由规则 + 'route_rule_merge' => false, + // 路由是否完全匹配 + 'route_complete_match' => false, + // 访问控制器层名称 + 'controller_layer' => 'controller', + // 空控制器名 + 'empty_controller' => 'Error', + // 是否使用控制器后缀 + 'controller_suffix' => false, + // 默认的路由变量规则 + 'default_route_pattern' => '[\w\.]+', + // 是否开启请求缓存 true自动缓存 支持设置请求缓存规则 + 'request_cache_key' => false, + // 请求缓存有效期 + 'request_cache_expire' => null, + // 全局请求缓存排除规则 + 'request_cache_except' => [], + // 默认控制器名 + 'default_controller' => 'Index', + // 默认操作名 + 'default_action' => 'index', + // 操作方法后缀 + 'action_suffix' => '', + // 默认JSONP格式返回的处理方法 + 'default_jsonp_handler' => 'jsonpReturn', + // 默认JSONP处理方法 + 'var_jsonp_handler' => 'callback', +]; diff --git a/config/session.php b/config/session.php new file mode 100644 index 0000000..fc8dfa3 --- /dev/null +++ b/config/session.php @@ -0,0 +1,19 @@ + 'PHPSESSID', + // SESSION_ID的提交变量,解决flash上传跨域 + 'var_session_id' => '', + // 驱动方式 支持file cache + 'type' => 'cache', + // 存储连接标识 当type使用cache的时候有效 + 'store' => 'redis', + // 过期时间 + 'expire' => 1440, + // 前缀 + 'prefix' => 'enquity:', +]; diff --git a/config/sms.php b/config/sms.php new file mode 100644 index 0000000..2134b89 --- /dev/null +++ b/config/sms.php @@ -0,0 +1,25 @@ + 5.0, + + // 默认发送配置 + 'default' => [ + // 默认可用的发送网关 + 'gateways' => [ + 'aliyun', + ], + ], + // 可用的网关配置 + 'gateways' => [ + 'errorlog' => [ + 'file' => '/tmp/easy-sms.log', + ], + 'aliyun' => [ + 'access_key_id' => env('sms.aliyunaccesskeyid'), + 'access_key_secret' => env('sms.aliyunaccesskeysecret'), + 'sign_name' => env('sms.aliyunsignname'), + ], + ], +]; diff --git a/config/trace.php b/config/trace.php new file mode 100644 index 0000000..fad2392 --- /dev/null +++ b/config/trace.php @@ -0,0 +1,10 @@ + 'Html', + // 读取的日志通道名 + 'channel' => '', +]; diff --git a/config/view.php b/config/view.php new file mode 100644 index 0000000..01259a0 --- /dev/null +++ b/config/view.php @@ -0,0 +1,25 @@ + 'Think', + // 默认模板渲染规则 1 解析为小写+下划线 2 全部转换小写 3 保持操作方法 + 'auto_rule' => 1, + // 模板目录名 + 'view_dir_name' => 'view', + // 模板后缀 + 'view_suffix' => 'html', + // 模板文件名分隔符 + 'view_depr' => DIRECTORY_SEPARATOR, + // 模板引擎普通标签开始标记 + 'tpl_begin' => '{', + // 模板引擎普通标签结束标记 + 'tpl_end' => '}', + // 标签库标签开始标记 + 'taglib_begin' => '{', + // 标签库标签结束标记 + 'taglib_end' => '}', +]; diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..5c9b9f6 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,11 @@ +version: '3' +services: + market: + image: liusuifeng/php74:latest + container_name: cmb-backend + restart: always + ports: + - 8032:8000 + - 9011:9000 + volumes: + - .:/var/project/ \ No newline at end of file diff --git a/public/.htaccess b/public/.htaccess new file mode 100644 index 0000000..e69de29 diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000..e71815a Binary files /dev/null and b/public/favicon.ico differ diff --git a/public/index.php b/public/index.php new file mode 100644 index 0000000..6bb88c5 --- /dev/null +++ b/public/index.php @@ -0,0 +1,27 @@ + +// +---------------------------------------------------------------------- + +// [ 应用入口文件 ] +namespace think; + +require __DIR__ . '/../vendor/autoload.php'; +header('Access-Control-Allow-Origin:*'); +header('Access-Control-Allow-Methods:POST,GET,DELETE,PUT,OPTIONS'); +header( 'Access-Control-Allow-Headers:*'); + +// 执行HTTP应用并响应 +$http = (new App())->http; + +$response = $http->run(); + +$response->send(); + +$http->end($response); diff --git a/public/nginx.htaccess b/public/nginx.htaccess new file mode 100644 index 0000000..8ad4ba5 --- /dev/null +++ b/public/nginx.htaccess @@ -0,0 +1,6 @@ + location / { + if (!-e $request_filename) { + rewrite ^(.*)$ /index.php?s=/$1 last; + break; + } + } \ No newline at end of file diff --git a/public/robots.txt b/public/robots.txt new file mode 100644 index 0000000..eb05362 --- /dev/null +++ b/public/robots.txt @@ -0,0 +1,2 @@ +User-agent: * +Disallow: diff --git a/public/router.php b/public/router.php new file mode 100644 index 0000000..9b39a62 --- /dev/null +++ b/public/router.php @@ -0,0 +1,19 @@ + +// +---------------------------------------------------------------------- +// $Id$ + +if (is_file($_SERVER["DOCUMENT_ROOT"] . $_SERVER["SCRIPT_NAME"])) { + return false; +} else { + $_SERVER["SCRIPT_FILENAME"] = __DIR__ . '/index.php'; + + require __DIR__ . "/index.php"; +} diff --git a/public/static/.gitignore b/public/static/.gitignore new file mode 100644 index 0000000..b722e9e --- /dev/null +++ b/public/static/.gitignore @@ -0,0 +1 @@ +!.gitignore \ No newline at end of file diff --git a/route/admin.php b/route/admin.php new file mode 100644 index 0000000..6f0cdc6 --- /dev/null +++ b/route/admin.php @@ -0,0 +1,7 @@ + +// +---------------------------------------------------------------------- +use think\facade\Route; diff --git a/route/front.php b/route/front.php new file mode 100644 index 0000000..f1878e0 --- /dev/null +++ b/route/front.php @@ -0,0 +1,87 @@ +validate(\app\validate\admin\LoginValidate::class); + Route::group('/front', function () { + Route::get('/homeData', 'app\front\Index@homeData'); + Route::group('/product', function () { + Route::get('/detail/:id', 'app\front\Product@detail')->validate(\app\validate\IdRequireValidate::class); + Route::get('/sync', 'app\front\Product@sync'); + }); + Route::get("/order/list", 'app\front\Order@list'); + })->middleware([\app\middleware\FrontRequest::class, \app\middleware\CorsMiddleware::class, \app\middleware\ValidateOpenId::class]); + + // 授权 + Route::group('/oauth', function () { + Route::post('/approval', 'app\front\Oauth@approval'); + Route::post('/accessToken', 'app\front\Oauth@accessToken'); + Route::post('/checkToken', 'app\front\Oauth@checkAccessToken')->middleware(\app\middleware\ValidateOpenId::class); + Route::post('/bindMobile', 'app\front\Oauth@bindMobile')->middleware(\app\middleware\ValidateOpenId::class)->validate(\app\validate\front\LoginValidate::class, 'bindMobile'); + })->middleware(\app\middleware\CorsMiddleware::class); + // 签约 + Route::group('/agreement', function () { + Route::post('', 'app\front\agreement'); + Route::post('/approval', 'app\front\Agreement@agreeApproval'); + Route::post('/release', 'app\front\Agreement@releaseForMerchant'); + Route::post('/releaseNotify', 'app\front\Agreement@releaseNotify'); // 解约通知 + Route::post('/notify', 'app\front\Agreement@agreeNotify'); // 签约通知 + Route::post('/payNotify', 'app\front\Agreement@payNotify'); // 协议通知 + Route::post('/unsubscribe', 'app\front\Agreement@unsubscribe'); //取消订阅 + Route::post('/getBindMobile', 'app\front\Agreement@getBindMobile'); //取消订阅 + Route::post('/getExchangeStatus', 'app\front\Agreement@getExchangeStatus'); + Route::post('/releaseSendSms', 'app\front\Agreement@releaseSendSms')->validate(\app\validate\front\LoginValidate::class, 'sendReleaseSms'); + })->middleware([\app\middleware\ValidateOpenId::class, \app\middleware\CorsMiddleware::class]); + + // 订单 + Route::group('/order', function () { + Route::post('/rechargeNotify', 'app\front\Order@rechargeNotify'); + Route::get('/monthSale', 'app\front\Order@getMonthSale'); + Route::post('/refund', 'app\front\Order@refund'); + })->middleware(\app\middleware\CorsMiddleware::class); + + Route::post('/front/loginSendSms', 'app\front\Login@loginSendSms')->validate(\app\validate\front\LoginValidate::class, 'sendSms')->middleware(\app\middleware\CorsMiddleware::class); +} else { + Route::group('/api', function () { + Route::post('/front/login', 'app\admin\Login@login')->validate(\app\validate\admin\LoginValidate::class); + Route::group('/front', function () { + Route::get('/homeData', 'app\front\Index@homeData'); + Route::group('/product', function () { + Route::get('/detail/:id', 'app\front\Product@detail')->validate(\app\validate\IdRequireValidate::class); + Route::get('/sync', 'app\front\Product@sync'); + }); + Route::get("/order/list", 'app\front\Order@list'); + })->middleware([\app\middleware\FrontRequest::class, \app\middleware\ValidateOpenId::class]); + + // 授权 + Route::group('/oauth', function () { + Route::post('/approval', 'app\front\Oauth@approval'); + Route::post('/accessToken', 'app\front\Oauth@accessToken'); + Route::post('/checkToken', 'app\front\Oauth@checkAccessToken')->middleware(\app\middleware\ValidateOpenId::class); + Route::post('/bindMobile', 'app\front\Oauth@bindMobile')->middleware(\app\middleware\ValidateOpenId::class)->validate(\app\validate\front\LoginValidate::class, 'bindMobile'); + }); + // 签约 + Route::group('/agreement', function () { + Route::post('', 'app\front\agreement'); + Route::post('/approval', 'app\front\Agreement@agreeApproval'); + Route::post('/release', 'app\front\Agreement@releaseForMerchant'); + Route::post('/releaseNotify', 'app\front\Agreement@releaseNotify'); // 解约通知 + Route::post('/notify', 'app\front\Agreement@agreeNotify'); // 签约通知 + Route::post('/payNotify', 'app\front\Agreement@payNotify'); // 协议通知 + Route::post('/unsubscribe', 'app\front\Agreement@unsubscribe'); //取消订阅 + Route::post('/getBindMobile', 'app\front\Agreement@getBindMobile'); //取消订阅 + Route::post('/getExchangeStatus', 'app\front\Agreement@getExchangeStatus'); + Route::post('/releaseSendSms', 'app\front\Agreement@releaseSendSms')->validate(\app\validate\front\LoginValidate::class, 'sendReleaseSms'); + })->middleware(\app\middleware\ValidateOpenId::class); + + // 订单 + Route::group('/order', function () { + Route::post('/rechargeNotify', 'app\front\Order@rechargeNotify'); + Route::get('/monthSale', 'app\front\Order@getMonthSale'); + Route::post('/refund', 'app\front\Order@refund'); + }); + + Route::post('/front/loginSendSms', 'app\front\Login@loginSendSms')->validate(\app\validate\front\LoginValidate::class, 'sendSms'); + })->middleware(\app\middleware\CorsMiddleware::class); +} diff --git a/runtime/.gitignore b/runtime/.gitignore new file mode 100644 index 0000000..c96a04f --- /dev/null +++ b/runtime/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore \ No newline at end of file diff --git a/think b/think new file mode 100644 index 0000000..2429d22 --- /dev/null +++ b/think @@ -0,0 +1,10 @@ +#!/usr/bin/env php +console->run(); \ No newline at end of file diff --git a/view/README.md b/view/README.md new file mode 100644 index 0000000..360eb24 --- /dev/null +++ b/view/README.md @@ -0,0 +1 @@ +如果不使用模板,可以删除该目录 \ No newline at end of file