From e440631275fb25379945c9e29f3d6c813d6ebee8 Mon Sep 17 00:00:00 2001 From: ZF sun <34314687@qq.com> Date: Sat, 8 Nov 2025 18:15:26 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=9E=E7=8E=B0=E5=90=8E=E5=8F=B0=E5=8F=8A?= =?UTF-8?q?=E5=89=8D=E5=8F=B0=E9=80=9A=E8=BF=87API=E8=AE=BF=E9=97=AEUV?= =?UTF-8?q?=E5=9F=8B=E7=82=B9=EF=BC=8C=E6=89=80=E6=9C=89=E4=BB=A3=E7=A0=81?= =?UTF-8?q?=E5=85=A8=E9=83=A8=E4=BF=9D=E5=AD=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env | 24 + .gitignore | 3 +- .idea/dataSources.xml | 4 +- .idea/sqldialects.xml | 4 +- docker-compose.yml | 6 + docker/php/Dockerfile | 7 +- docker/php/supervisord.conf | 29 + docs/common/const.md | 60 + docs/common/cron.md | 37 + docs/common/layout.md | 13 + docs/common/stat.md | 398 ++++++ docs/readme.md | 428 +++++++ src/app/Controller.php | 18 + src/app/api/controller/AI.php | 505 ++++++++ src/app/api/controller/BaseApi.php | 10 + src/app/api/controller/Config.php | 2 +- src/app/command/Schedule.php | 12 +- src/app/common.php | 73 ++ src/app/common/library/BrowserDetector.php | 346 +++++ src/app/common/library/CallerInfo.php | 112 ++ src/app/common/library/EnhancedLogger.php | 115 ++ .../library/PerformanceAwareCallerInfo.php | 63 + src/app/cron/controller/Task.php | 8 +- src/app/event/init/InitConfig.php | 1 + src/app/event/init/InitCron.php | 101 +- src/app/job/Cronexecute.php | 19 +- src/app/model/ai/AiChatHistory.php | 261 ++++ src/app/model/ai/AiChatSession.php | 266 ++++ src/app/model/stat/StatShop.php | 84 +- src/app/model/system/Cron.php | 235 ++-- src/app/model/web/Config.php | 697 +++++----- src/app/shop/controller/BaseShop.php | 3 +- src/app/shop/controller/Config.php | 46 + src/app/shop/controller/Index.php | 1 - src/app/shop/view/config/ai.html | 617 --------- src/app/shop/view/config/ai/agent.html | 141 +++ src/app/shop/view/config/ai/index.html | 209 +++ src/app/shop/view/config/ai/js/platform.js | 397 ++++++ src/app/shop/view/config/ai/platform.html | 465 +++++++ src/app/shop/view/layout/base.html | 1 + src/app/tracking/VisitorTracker.php | 101 ++ src/config/cron.php | 19 +- src/public/static/js/deep-proxy-1.0.js | 1124 +++++++++++++++++ 43 files changed, 5960 insertions(+), 1105 deletions(-) create mode 100644 .env create mode 100644 docker/php/supervisord.conf create mode 100644 docs/common/const.md create mode 100644 docs/common/cron.md create mode 100644 docs/common/layout.md create mode 100644 docs/common/stat.md create mode 100644 docs/readme.md create mode 100644 src/app/api/controller/AI.php create mode 100644 src/app/common/library/BrowserDetector.php create mode 100644 src/app/common/library/CallerInfo.php create mode 100644 src/app/common/library/EnhancedLogger.php create mode 100644 src/app/common/library/PerformanceAwareCallerInfo.php create mode 100644 src/app/model/ai/AiChatHistory.php create mode 100644 src/app/model/ai/AiChatSession.php delete mode 100644 src/app/shop/view/config/ai.html create mode 100644 src/app/shop/view/config/ai/agent.html create mode 100644 src/app/shop/view/config/ai/index.html create mode 100644 src/app/shop/view/config/ai/js/platform.js create mode 100644 src/app/shop/view/config/ai/platform.html create mode 100644 src/app/tracking/VisitorTracker.php create mode 100644 src/public/static/js/deep-proxy-1.0.js diff --git a/.env b/.env new file mode 100644 index 000000000..261d0502c --- /dev/null +++ b/.env @@ -0,0 +1,24 @@ +# 项目配置, 请根据实际情况修改 +PROJECT_NAME=newshop + +# PHP/PHP-FPM 配置 +PHP_VERSION=7.4 +PHP_FPM_VERSION=7.4-fpm +PHP_FPM_PORT=9100 +XDEBUG_POST=9103 + +# 数据库配置 +MYSQL_ROOT_HOST=% +MYSQL_DATABASE=shop_mallnew +MYSQL_USER=shop_mallnew +MYSQL_PASSWORD=shop_mallnew +MYSQL_PORT=3316 + +# Redis 配置 +REDIS_PASSWORD=luckyshop123!@# +REDIS_PORT=6399 + +# Nginx 配置 +NGINX_PORT=8010 +NGINX_SSL_PORT=8012 + diff --git a/.gitignore b/.gitignore index e552d08db..6e4eacd0c 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ -src/runtime \ No newline at end of file +src/runtime +src/upload \ No newline at end of file diff --git a/.idea/dataSources.xml b/.idea/dataSources.xml index 7efd2f2f4..22de66943 100644 --- a/.idea/dataSources.xml +++ b/.idea/dataSources.xml @@ -1,11 +1,11 @@ - + mysql.8 true com.mysql.cj.jdbc.Driver - jdbc:mysql://localhost:3306 + jdbc:mysql://localhost:3316 $ProjectFileDir$ diff --git a/.idea/sqldialects.xml b/.idea/sqldialects.xml index e3e93dcf5..8c9894fca 100644 --- a/.idea/sqldialects.xml +++ b/.idea/sqldialects.xml @@ -1,7 +1,7 @@ - - + + \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 95a37412f..11a257c87 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -36,6 +36,12 @@ services: - xdebug_logs:/tmp # Xdebug 日志目录 depends_on: - db + healthcheck: + test: ["CMD", "bash", "-c", "curl -f http://localhost:9000/status && ps aux | grep '[p]hp think cron:schedule'"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 60s networks: - sass-platform-net labels: diff --git a/docker/php/Dockerfile b/docker/php/Dockerfile index 932a2fbe3..d79591777 100644 --- a/docker/php/Dockerfile +++ b/docker/php/Dockerfile @@ -7,8 +7,12 @@ WORKDIR /var/www/html # 拷贝本地的 sources.list 文件以加速 apt-get COPY ./sources.list /etc/apt/sources.list +# 复制Supervisor配置 +COPY supervisord.conf /etc/supervisor/conf.d/supervisord.conf + # 安装系统依赖 RUN apt-get update && apt-get install -y \ + supervisor \ git \ curl \ vim \ @@ -73,4 +77,5 @@ RUN echo "zend_extension=xdebug.so" > /usr/local/etc/php/conf.d/xdebug.ini # 暴露端口 EXPOSE 9000 9003 -CMD ["php-fpm"] \ No newline at end of file +# 启动Supervisor +CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf"] \ No newline at end of file diff --git a/docker/php/supervisord.conf b/docker/php/supervisord.conf new file mode 100644 index 000000000..c9873e86a --- /dev/null +++ b/docker/php/supervisord.conf @@ -0,0 +1,29 @@ +[supervisord] +nodaemon=true +logfile=/var/log/supervisord.log +logfile_maxbytes=50MB +logfile_backups=10 +loglevel=info +pidfile=/var/run/supervisord.pid + +[program:php-fpm] +command=php-fpm +autostart=true +autorestart=true +startretries=3 +startsecs=1 +stdout_logfile=/dev/stdout +stdout_logfile_maxbytes=0 +stderr_logfile=/dev/stderr +stderr_logfile_maxbytes=0 + +[program:think-cron] +command=php /var/www/html/think cron:schedule +autostart=true +autorestart=true +startretries=5 +startsecs=2 +stdout_logfile=/dev/stdout +stdout_logfile_maxbytes=0 +stderr_logfile=/dev/stderr +stderr_logfile_maxbytes=0 \ No newline at end of file diff --git a/docs/common/const.md b/docs/common/const.md new file mode 100644 index 000000000..35b8be245 --- /dev/null +++ b/docs/common/const.md @@ -0,0 +1,60 @@ +# 常量的定义 + +src\app\event\init\InitConfig.php + +``` + /** + * 初始化常量 + */ + private function initConst() + { + //加载版本信息 + define('SHOP_MODULE', 'shop'); + defined('SYS_VERSION_NO') or define('SYS_VERSION_NO', Config::get('info.version')); //版本号 + defined('SYS_VERSION_NAME') or define('SYS_VERSION_NAME', Config::get('info.title')); //版本名称 + defined('SYS_VERSION') or define('SYS_VERSION', Config::get('info.name')); //版本类型 + defined('SYS_RELEASE') or define('SYS_RELEASE', Config::get('info.version_no')); //版本号 + //是否展示帮助快捷链接 + define('HELP_SHOW', 1); + //加载基础化配置信息 + define('__ROOT__', str_replace([ '/index.php', '/install.php' ], '', request()->root(true))); + + define('__PUBLIC__', __ROOT__ . '/public'); + define('__UPLOAD__', 'upload'); + + //插件目录名称 + define('ADDON_DIR_NAME', 'addon'); + //插件目录路径 + define('ADDON_PATH', 'addon/'); + //分页每页数量 + define('PAGE_LIST_ROWS', 10); + + define('MEMBER_LEVEL', 10); + + //伪静态模式是否开启 + define('REWRITE_MODULE', true); + + // public目录绝对路径 + define('PUBLIC_PATH', root_path() . '/public/'); + // 项目绝对路径 + define('ROOT_PATH', root_path()); + + //兼容模式访问 + if (!REWRITE_MODULE) { + define('ROOT_URL', request()->root(true) . '/?s='); + } else { + define('ROOT_URL', request()->root(true)); + } + + //检测网址访问 + $url = request()->url(true); + $url = strtolower($url); + if (strstr($url, 'call_user_func_array') || strstr($url, 'invokefunction') || strstr($url, 'think\view')) { + die("非法请求"); + } + + // 应用模块 + $GLOBALS[ 'system_array' ] = [ 'shop', 'install', 'cron', 'api', 'pay', 'public', 'app', 'index', SHOP_MODULE ]; + $GLOBALS[ 'app_array' ] = [ 'shop', 'store' ]; + } +``` \ No newline at end of file diff --git a/docs/common/cron.md b/docs/common/cron.md new file mode 100644 index 000000000..47d2835d9 --- /dev/null +++ b/docs/common/cron.md @@ -0,0 +1,37 @@ +# 计划任务 + +## 特别说明 + +本项目使用的 是 `yunwuxin/think-cron` 包,该包提供了丰富的功能,包括任务调度、任务执行、任务状态管理等。该包支持多种任务类型,如固定任务、循环任务、一次性任务等,同时支持多种执行周期,如分钟、小时、天、周、月等。 + + + +## 计划任务管理 + +``` php +/** + * 添加计划任务 + * @param int $type 任务类型 1.固定任务 2.循环任务 + * @param int $period 执行周期 + * @param string $name 任务名称 + * @param string $event 执行事件 + * @param int $execute_time 待执行时间 + * @param int $relate_id 关联id + * @param int $period_type 周期类型 0 分钟 1 天 2 周 3 月 + */ +public function addCron($type, $period, $name, $event, $execute_time, $relate_id, $period_type = 0) +{ + $data = [ + 'type' => $type, + 'period' => $period, + 'period_type' => $period_type, + 'name' => $name, + 'event' => $event, + 'execute_time' => $execute_time, + 'relate_id' => $relate_id, + 'create_time' => time() + ]; + $res = model('cron')->add($data); + return $this->success($res); +} +``` diff --git a/docs/common/layout.md b/docs/common/layout.md new file mode 100644 index 000000000..fc0318f32 --- /dev/null +++ b/docs/common/layout.md @@ -0,0 +1,13 @@ + +# 布局 + + +## 针对与商户的布局 + +```html + +src\app\shop\view\layout\base.html +``` + +- 涉及到的静态资源位置: + - 静态资源文件位置:`src\public\static\` \ No newline at end of file diff --git a/docs/common/stat.md b/docs/common/stat.md new file mode 100644 index 000000000..3d3a08692 --- /dev/null +++ b/docs/common/stat.md @@ -0,0 +1,398 @@ +# 统计信息 + + +## 表结构设计 + +- lucky_stat_shop 店铺统计信息,按天统计 +- lucky_stat_shop_hour 店铺统计信息,按小时统计 + +``` sql + +数据源: local_shop_xcx30.5g +架构: shop_mallnew +表: lucky_stat_shop + + +-- auto-generated definition +create table lucky_stat_shop +( + id int(11) unsigned auto_increment + primary key, + site_id int default 0 not null comment '站点id', + year int default 0 not null comment '年', + month int default 0 not null comment '月', + day int default 0 not null comment '日', + day_time int default 0 not null comment '当日时间', + order_total decimal(10, 2) default 0.00 not null comment '订单金额', + shipping_total decimal(10, 2) default 0.00 not null comment '运费金额', + refund_total decimal(10, 2) default 0.00 not null comment '退款金额', + order_pay_count int default 0 not null comment '订单总数', + goods_pay_count int default 0 not null comment '订单商品总数', + shop_money decimal(10, 2) default 0.00 not null comment '店铺金额', + platform_money decimal(10, 2) default 0.00 not null comment '平台金额', + create_time int default 0 not null comment '创建时间', + modify_time int default 0 not null comment '修改时间', + collect_shop int default 0 not null comment '店铺收藏量', + collect_goods int default 0 not null comment '商品收藏量', + visit_count int default 0 not null comment '浏览量', + order_count int default 0 not null comment '订单量(总)', + goods_count int default 0 not null comment '订单商品量(总)', + add_goods_count int default 0 not null comment '添加商品数', + member_count int default 0 not null comment '会员统计', + order_member_count int default 0 not null comment '下单会员数', + order_refund_count int default 0 not null comment '退款订单数', + order_refund_grand_count decimal(10, 2) default 0.00 not null comment '退款金额', + order_refund_grand_total_money decimal(10, 2) default 0.00 not null comment '累计退款金额', + coupon_member_count int default 0 not null comment '领券会员数量', + member_level_count int default 0 not null comment '超级会员卡销售量', + member_level_total_money decimal(10, 2) default 0.00 not null comment '超级会员卡销售额', + member_level_grand_count int default 0 not null comment '累计超级会员卡销售量', + member_level_grand_total_money decimal(10, 2) default 0.00 not null comment '累计超级会员卡销售额', + member_recharge_count int default 0 not null comment '会员储值总订单量', + member_recharge_grand_count decimal(10, 2) default 0.00 not null comment '累计会员储值总量', + member_recharge_total_money decimal(10, 2) default 0.00 not null comment '会员充值总额', + member_recharge_grand_total_money decimal(10, 2) default 0.00 not null comment '累计会员充值总额', + member_recharge_member_count int default 0 not null comment '储值会员数', + member_giftcard_count int default 0 not null comment '礼品卡订单总量', + member_giftcard_grand_count int default 0 not null comment '累计礼品卡订单总量', + member_giftcard_total_money decimal(10, 2) default 0.00 not null comment '礼品卡订单总额', + h5_visit_count int default 0 not null comment 'h5访问量', + wechat_visit_count int default 0 not null comment 'wechat访问量', + weapp_visit_count int default 0 not null comment 'weapp访问量', + pc_visit_count int default 0 not null comment 'pc访问量', + expected_earnings_total_money decimal(10, 2) default 0.00 not null comment '预计收入', + expenditure_total_money decimal(10, 2) default 0.00 not null comment '总支出', + earnings_total_money decimal(10, 2) default 0.00 not null comment '总收入', + member_withdraw_count int default 0 not null comment '会员提现总量', + member_withdraw_total_money decimal(10, 2) default 0.00 not null comment '会员提现总额', + coupon_count int default 0 not null comment '领券数', + add_coupon_count int default 0 not null comment '新增优惠券', + order_pay_money decimal(10, 2) default 0.00 not null comment '订单实际支付', + add_fenxiao_member_count int default 0 not null comment '新增分销商', + fenxiao_order_total_money decimal(10, 2) default 0.00 not null comment '分销订单总额', + fenxiao_order_count int default 0 not null comment '分销订单总数', + goods_on_type_count int default 0 not null comment '在架商品数', + goods_visited_type_count int default 0 not null comment '被访问商品数(仅详情页浏览数)', + goods_order_type_count int default 0 not null comment '动销商品数', + goods_exposure_count int default 0 not null comment '商品曝光数', + goods_visit_count int default 0 not null comment '商品浏览量', + goods_visit_member_count int default 0 not null comment '商品访客数', + goods_cart_count int default 0 not null comment '加购件数', + goods_order_count decimal(12, 3) default 0.000 not null comment '下单件数', + order_create_money decimal(10, 2) default 0.00 not null comment '订单下单总额', + order_create_count int default 0 not null comment '订单下单量', + balance_deduction decimal(10, 2) unsigned default 0.00 not null comment '余额抵扣总额', + cashier_billing_count int default 0 not null comment '开单数量', + cashier_billing_money decimal(10, 2) default 0.00 not null comment '开单金额', + cashier_buycard_count int default 0 not null comment '办卡数量', + cashier_buycard_money decimal(10, 2) default 0.00 not null comment '办卡金额', + cashier_recharge_count int default 0 not null comment '收银台充值数量', + cashier_recharge_money decimal(10, 2) default 0.00 not null comment '收银台充值金额', + cashier_refund_count int default 0 not null comment '收银台退款数量', + cashier_refund_money decimal(10, 2) default 0.00 not null comment '收银台退款金额', + cashier_order_member_count int default 0 not null comment '收银台下单会员数', + cashier_balance_money decimal(10, 2) default 0.00 not null comment '收银台余额消费金额', + cashier_online_pay_money decimal(10, 2) default 0.00 not null comment '收银台线上金额', + cashier_online_refund_money decimal(10, 2) default 0.00 not null comment '收银台线上退款金额', + cashier_balance_deduction decimal(10, 2) default 0.00 not null comment '门店余额总计' +) + charset = utf8 + row_format = DYNAMIC; + +create index IDX_ns_stat_shop_day + on lucky_stat_shop (day); + +create index IDX_ns_stat_shop_day_time + on lucky_stat_shop (day_time); + +create index IDX_ns_stat_shop_month + on lucky_stat_shop (month); + +create index IDX_ns_stat_shop_site_id + on lucky_stat_shop (site_id); + +create index IDX_ns_stat_shop_year + on lucky_stat_shop (year); + + + +数据源: local_shop_xcx30.5g +架构: shop_mallnew +表: lucky_stat_shop_hour + + +-- auto-generated definition +create table lucky_stat_shop_hour +( + id int(11) unsigned auto_increment + primary key, + site_id int default 0 not null comment '站点id', + year int default 0 not null comment '年', + month int default 0 not null comment '月', + day int default 0 not null comment '日', + hour int default 0 not null comment '时', + day_time int default 0 not null comment '当日时间', + order_total decimal(10, 2) default 0.00 not null comment '订单金额', + shipping_total decimal(10, 2) default 0.00 not null comment '运费金额', + refund_total decimal(10, 2) default 0.00 not null comment '退款金额', + order_pay_count int default 0 not null comment '订单总数', + goods_pay_count int default 0 not null comment '订单商品总数', + shop_money decimal(10, 2) default 0.00 not null comment '店铺金额', + platform_money decimal(10, 2) default 0.00 not null comment '平台金额', + create_time int default 0 not null comment '创建时间', + modify_time int default 0 not null comment '修改时间', + collect_shop int default 0 not null comment '店铺收藏量', + collect_goods int default 0 not null comment '商品收藏量', + visit_count int default 0 not null comment '浏览量', + order_count int default 0 not null comment '订单量(总)', + goods_count int default 0 not null comment '订单商品量(总)', + add_goods_count int default 0 not null comment '添加商品数', + member_count int default 0 not null comment '会员统计', + order_member_count int default 0 not null comment '下单会员数', + order_refund_count int default 0 not null comment '退款订单数', + order_refund_grand_count decimal(10, 2) default 0.00 not null comment '退款金额', + order_refund_grand_total_money decimal(10, 2) default 0.00 not null comment '累计退款金额', + coupon_member_count int default 0 not null comment '领券会员数量', + member_level_count int default 0 not null comment '超级会员卡销售量', + member_level_total_money decimal(10, 2) default 0.00 not null comment '超级会员卡销售额', + member_level_grand_count int default 0 not null comment '累计超级会员卡销售量', + member_level_grand_total_money decimal(10, 2) default 0.00 not null comment '累计超级会员卡销售额', + member_recharge_count int default 0 not null comment '会员储值总订单量', + member_recharge_grand_count decimal(10, 2) default 0.00 not null comment '累计会员储值总量', + member_recharge_total_money decimal(10, 2) default 0.00 not null comment '会员充值总额', + member_recharge_grand_total_money decimal(10, 2) default 0.00 not null comment '累计会员充值总额', + member_recharge_member_count int default 0 not null comment '储值会员数', + member_giftcard_count int default 0 not null comment '礼品卡订单总量', + member_giftcard_grand_count int default 0 not null comment '累计礼品卡订单总量', + member_giftcard_total_money decimal(10, 2) default 0.00 not null comment '礼品卡订单总额', + h5_visit_count int default 0 not null comment 'h5访问量', + wechat_visit_count int default 0 not null comment 'wechat访问量', + weapp_visit_count int default 0 not null comment 'weapp访问量', + pc_visit_count int default 0 not null comment 'pc访问量', + expected_earnings_total_money decimal(10, 2) default 0.00 not null comment '预计收入', + expenditure_total_money decimal(10, 2) default 0.00 not null comment '总支出', + earnings_total_money decimal(10, 2) default 0.00 not null comment '总收入', + member_withdraw_count int default 0 not null comment '会员提现总量', + member_withdraw_total_money decimal(10, 2) default 0.00 not null comment '会员提现总额', + coupon_count int default 0 not null comment '领券数', + add_coupon_count int default 0 not null comment '新增优惠券', + order_pay_money decimal(10, 2) default 0.00 not null comment '订单实际支付', + add_fenxiao_member_count int default 0 not null comment '新增分销商', + fenxiao_order_total_money decimal(10, 2) default 0.00 not null comment '分销订单总额', + fenxiao_order_count int default 0 not null comment '分销订单总数', + goods_on_type_count int default 0 not null comment '在架商品数', + goods_visited_type_count int default 0 not null comment '被访问商品数(仅详情页浏览数)', + goods_order_type_count int default 0 not null comment '动销商品数', + goods_exposure_count int default 0 not null comment '商品曝光数', + goods_visit_count int default 0 not null comment '商品浏览量', + goods_visit_member_count int default 0 not null comment '商品访客数', + goods_cart_count int default 0 not null comment '加购件数', + goods_order_count decimal(12, 3) default 0.000 not null comment '下单件数', + order_create_money decimal(10, 2) default 0.00 not null comment '订单下单总额', + order_create_count int default 0 not null comment '订单下单量', + balance_deduction decimal(10, 2) default 0.00 not null comment '余额抵扣总额', + cashier_billing_count int default 0 not null comment '开单数量', + cashier_billing_money decimal(10, 2) default 0.00 not null comment '开单金额', + cashier_buycard_count int default 0 not null comment '办卡数量', + cashier_buycard_money decimal(10, 2) default 0.00 not null comment '办卡金额', + cashier_recharge_count int default 0 not null comment '收银台充值数量', + cashier_recharge_money decimal(10, 2) default 0.00 not null comment '收银台充值金额', + cashier_refund_count int default 0 not null comment '收银台退款数量', + cashier_refund_money decimal(10, 2) default 0.00 not null comment '收银台退款金额', + cashier_order_member_count int default 0 not null comment '收银台下单会员数', + cashier_balance_money decimal(10, 2) default 0.00 not null comment '收银台余额消费金额', + cashier_online_pay_money decimal(10, 2) default 0.00 not null comment '收银台线上金额', + cashier_online_refund_money decimal(10, 2) default 0.00 not null comment '收银台线上退款金额', + cashier_balance_deduction decimal(10, 2) default 0.00 not null comment '门店余额总计' +) + charset = utf8 + row_format = DYNAMIC; + +create index IDX_ns_stat_shop_hour_day + on lucky_stat_shop_hour (day); + +create index IDX_ns_stat_shop_hour_day_time + on lucky_stat_shop_hour (day_time); + +create index IDX_ns_stat_shop_hour_hour + on lucky_stat_shop_hour (hour); + +create index IDX_ns_stat_shop_hour_month + on lucky_stat_shop_hour (month); + +create index IDX_ns_stat_shop_hour_site_id + on lucky_stat_shop_hour (site_id); + +create index IDX_ns_stat_shop_hour_year + on lucky_stat_shop_hour (year); + +``` + +## 数据关联 + + +### 1. 数据的写入 addShopStat + +与添加店铺统计数据有关的函数,addShopStat 在三个文件中,都存在 + +- File1: `src\app\model\stat\StatShop.php` +- File2: `src\app\model\system\Stat.php` + +实现细节,该File1文件中的 `addShopStat` 函数,从自己函数 `getStatData` 获得整理后的数据,然后将数据传输给 File2 `src/app/model/stat/StatShop.php` 的 `addShopStat` 函数 + +```php + +// src/app/model/stat/StatShop.php +public function addShopStat($data) +{ + $carbon = Carbon::now(); + $dir = __UPLOAD__.'/stat/stat_shop/'; + if (!is_dir($dir) && !mkdir($dir, 0777, true) && !is_dir($dir)) { + return $this->error(sprintf('Directory "%s" was not created', $dir)); + } + $filename = $dir.$carbon->year.'_'.$carbon->month.'_'.$carbon->day.'_'.$carbon->second.'_'.unique_random().'.json'; + $stat_extend = new Stat($filename, 'stat_shop',$data['site_id']); + $stat_extend->handleData($data);//写入文件 + + //增加当天时统计 + $this->addShopHourStat($data, $carbon); + return $this->success(); +} +``` + +分析:addShopStat,是将统计记录写入到文件中,为快速存储数据及留痕提供数据支持。 + +### 2. 数据的提取 cronShopStat 计划任务统计数据处理 + +文件:src/app/model/stat/StatShop.php +`cronShopStat`,是将统计记录从文件中提取出来,然后写入到数据库中,为快速存储数据及留痕提供数据支持。 + +``` php +// src\app\model\stat\StatShop.php +/** + * 从stat_shop目录下读取所有文件,将数据处理后,写入数据表中 + * 处理完每个文件后,删除文件 + */ +public function cronShopStat() +{ + $path = __UPLOAD__.'/stat/stat_shop'; + if(!is_dir($path)) return; + + $result = $this->scanFile($path); + if(empty($result)) return; + + try { + $json_array = []; + foreach ($result as $key => $val){ + $stat_extend = new Stat($path.'/'.$val, 'stat_shop'); + $json_array[] = $stat_extend->load(); + unlink($path.'/'.$val); // 处理完文件后,删除文件 + } + + + $data_array = []; + foreach ($json_array as $json_k => $json_v){ + $k = $json_v['year'].'_'.$json_v['month'].'_'.$json_v['day']; + if (isset($data_array[$k])){ + foreach ($data_array[$k] as $data_k => $data_v){ + if($data_k != 'site_id' && $data_k != 'year' && $data_k != 'month' && $data_k != 'day' && $data_k != 'day_time'){ + if ($json_v[$data_k] > 0) { + $data_array[$k][$data_k] += $json_v[$data_k]; + } else if ($json_v[$data_k] < 0) { + $data_array[$k][$data_k] -= abs($json_v[$data_k]); + } + } + } + }else{ + $data_array[$k] = $json_v; + } + } + Log::write(time().'stat_shop_'.json_encode($data_array)); + $system_stat = new \app\model\system\Stat(); + foreach ($data_array as $json_k => $json_v){ + $system_stat->addStatShopModel($json_v); + } + } catch (\Exception $e) { + + } + +} +``` + +1. 从函数的开头可以看出,该函数用于计划任务。 +2. 这个函数将被 `src\app\event\stat\CronStatShop.php` 中的 handle() 方法调用. +3. 事件的绑定在 `src\app\event.php` 文件中定义。 + +```php +//店铺统计更新(按时) +'CronStatShopHour' => [ + 'app\event\stat\CronStatShopHour' +], +``` + +说明,需要通过ThinkPHP的Event的手动触发或者自动触发来处理 + +方式1:手动触发(最常用) +```php +// 通过 event()助手函数或 Event门面触发: +// 在控制器、模型或任何地方触发事件 +event('UserLogin', $user); + +// 等价于 +use think\facade\Event; +Event::trigger('UserLogin', $user); +``` + +方式2:自动触发(通过事件类) +定义事件类并直接触发: +```php +// 定义事件类 +namespace app\event; +class UserLogin +{ + public function __construct(public $user) {} +} + +// 触发事件 +$event = new \app\event\UserLogin($user); +event($event); +``` + +### 3. 数据的展示调用 + + + +## 数据获取 + +数据对应的文件:`src\app\model\system\Stat.php` + +1. 获取天统计表统计字段:getStatField +2. 获取时统计表统计字段:getStatHourField +3. 获取商品销量排行榜:getGoodsSaleNumRankingList + +## 应用场景 + +### 优惠券的统计 + +调用者:`src\addon\coupon\model\CouponStat.php` +被调用者:`src\app\model\system\Stat.php` + +### 分销订单的统计 + +调用者:`src\addon\fenxiao\model\FenxiaoStat.php` +被调用者:`src\app\model\system\Stat.php` + +### 虚拟商品的统计 + +调用者:`src\addon\virtualcard\model\VirtualGoods.php` +被调用者:`src\app\model\system\Stat.php` + +### 商品收藏的统计 + +调用者:`src\app\model\goods\GoodsCollect.php` +被调用者:`src\app\model\system\Stat.php` + +### 购物车的统计 + +调用者:`src\app\model\stat\GoodsCartStat.php` +被调用者:`src\app\model\system\Stat.php` diff --git a/docs/readme.md b/docs/readme.md new file mode 100644 index 000000000..07b63ea94 --- /dev/null +++ b/docs/readme.md @@ -0,0 +1,428 @@ +# 说明 + + +## 基本概念 + +店铺和门店的关系可以从不同角度来理解,让我为您详细解释: + +## 🏪 **店铺 vs 门店:概念解析** + +### **1. 从层级关系理解** + +#### **店铺(总店/品牌店)** +```mermaid +graph TD + A[品牌总部/总店] --> B1[门店A] + A --> B2[门店B] + A --> B3[门店C] + A --> B4[线上店铺] + + B1 --> C1[分店A-1] + B1 --> C2[分店A-2] + B2 --> C3[分店B-1] +``` + +**店铺特征:** +- **管理层面**:品牌、总部、总店的概念 +- **业务范围**:可能包含多个物理门店和线上渠道 +- **功能定位**:战略决策、品牌管理、供应链控制 + +#### **门店(分店/实体店)** +```mermaid +graph LR + A[门店] --> B[具体营业场所] + A --> C[固定地址] + A --> D[面对面服务] +``` + +**门店特征:** +- **物理存在**:有具体的营业场所和地址 +- **服务范围**:服务特定区域客户 +- **运营独立**:相对独立的日常运营 + +### **2. 从业务模式区分** + +| 维度 | 店铺(Shop) | 门店(Store) | +|------|-------------|--------------| +| **存在形式** | 可虚拟可实体 | 必须是实体 | +| **数量关系** | 1个店铺可有多门店 | 1个门店属于1个店铺 | +| **管理范围** | 品牌、战略、标准 | 日常运营、客户服务 | +| **系统关系** | 总部系统、ERP | POS系统、库存管理 | + +### **3. 实际业务场景** + +#### **场景1:连锁品牌** +``` +店铺(总店) +├── 北京门店(朝阳区店) +├── 上海门店(浦东店) +├── 广州门店(天河店) +└── 线上官方店铺 +``` + +#### **场景2:电商+实体结合** +``` +店铺(品牌旗舰店) +├── 天猫官方店铺(线上) +├── 京东官方店铺(线上) +├── 线下体验门店(实体) +└── 微信小程序店铺(线上) +``` + +#### **场景3:单店经营** +``` +店铺 = 门店(合二为一) +└── 既是品牌也是具体营业点 +``` + +## 💼 **在智能客服系统中的数据建模** + +### **1. 数据库设计** + +```sql +-- 店铺表(总店/品牌) +CREATE TABLE shops ( + id INT PRIMARY KEY AUTO_INCREMENT, + name VARCHAR(100) NOT NULL, -- 店铺名称 + brand_name VARCHAR(100), -- 品牌名称 + type ENUM('single', 'chain', 'online'), -- 店铺类型 + status ENUM('active', 'inactive'), + created_at TIMESTAMP +); + +-- 门店表(分店/实体店) +CREATE TABLE stores ( + id INT PRIMARY KEY AUTO_INCREMENT, + shop_id INT, -- 所属店铺 + name VARCHAR(100) NOT NULL, -- 门店名称 + address TEXT, -- 具体地址 + phone VARCHAR(20), + manager_id INT, -- 店长 + business_hours JSON, -- 营业时间 + latitude DECIMAL(10, 8), -- 纬度 + longitude DECIMAL(11, 8), -- 经度 + status ENUM('open', 'closed', 'maintenance'), + FOREIGN KEY (shop_id) REFERENCES shops(id) +); + +-- 员工表(关联门店) +CREATE TABLE staff ( + id INT PRIMARY KEY AUTO_INCREMENT, + store_id INT, -- 所属门店 + name VARCHAR(50), + role ENUM('manager', 'assistant', 'cashier'), + phone VARCHAR(20), + FOREIGN KEY (store_id) REFERENCES stores(id) +); +``` + +### **2. 业务逻辑实现** + +```javascript +// 店铺管理类 +class ShopManager { + constructor() { + this.shops = new Map(); + this.stores = new Map(); + } + + // 创建店铺 + createShop(shopData) { + const shop = { + id: this.generateId(), + name: shopData.name, + brand: shopData.brand, + type: shopData.type || 'single', + stores: [], // 下属门店 + createdAt: new Date(), + status: 'active' + }; + + this.shops.set(shop.id, shop); + return shop; + } + + // 添加门店到店铺 + addStoreToShop(shopId, storeData) { + const shop = this.shops.get(shopId); + if (!shop) throw new Error('店铺不存在'); + + const store = { + id: this.generateId(), + shopId: shopId, + name: storeData.name, + address: storeData.address, + contact: storeData.contact, + coordinates: storeData.coordinates, + businessHours: storeData.businessHours, + staff: [], + createdAt: new Date() + }; + + shop.stores.push(store.id); + this.stores.set(store.id, store); + + return store; + } + + // 根据位置查找最近门店 + findNearestStore(shopId, userLocation) { + const shop = this.shops.get(shopId); + if (!shop) return null; + + let nearestStore = null; + let minDistance = Infinity; + + shop.stores.forEach(storeId => { + const store = this.stores.get(storeId); + if (store && store.status === 'open') { + const distance = this.calculateDistance( + userLocation, + store.coordinates + ); + + if (distance < minDistance) { + minDistance = distance; + nearestStore = store; + } + } + }); + + return nearestStore; + } + + // 计算距离(简化版) + calculateDistance(loc1, loc2) { + const R = 6371; // 地球半径公里 + const dLat = this.deg2rad(loc2.lat - loc1.lat); + const dLon = this.deg2rad(loc2.lon - loc1.lon); + + const a = Math.sin(dLat/2) * Math.sin(dLat/2) + + Math.cos(this.deg2rad(loc1.lat)) * Math.cos(this.deg2rad(loc2.lat)) * + Math.sin(dLon/2) * Math.sin(dLon/2); + + const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a)); + return R * c; + } + + deg2rad(deg) { + return deg * (Math.PI/180); + } +} +``` + +### **3. 在智能客服中的对话逻辑** + +```javascript +// 店铺门店相关的对话处理 +class StoreDialogHandler { + constructor(shopManager) { + this.shopManager = shopManager; + } + + // 处理用户查询 + handleStoreQuery(userMessage, userContext) { + const intent = this.detectIntent(userMessage); + + switch(intent) { + case 'find_nearest': + return this.handleFindNearest(userMessage, userContext); + + case 'store_hours': + return this.handleStoreHours(userMessage, userContext); + + case 'store_contact': + return this.handleStoreContact(userMessage, userContext); + + case 'all_stores': + return this.handleAllStores(userMessage, userContext); + + default: + return this.handleGeneralQuery(userMessage, userContext); + } + } + + // 查找最近门店 + handleFindNearest(userMessage, userContext) { + const location = this.extractLocation(userMessage); + const shopId = userContext.currentShopId; + + if (!location) { + return { + type: 'request_location', + message: '请问您当前在哪个位置?我可以帮您查找最近的门店。', + options: ['使用当前位置', '手动输入地址'] + }; + } + + const nearestStore = this.shopManager.findNearestStore(shopId, location); + + if (nearestStore) { + return { + type: 'store_info', + message: `离您最近的门店是:${nearestStore.name}`, + data: { + store: nearestStore, + distance: this.calculateDistance(location, nearestStore.coordinates), + estimatedTime: this.estimateTravelTime(nearestStore.coordinates, location) + }, + actions: [ + { text: '查看详情', action: 'show_store_detail' }, + { text: '导航前往', action: 'navigate_to_store' }, + { text: '联系门店', action: 'contact_store' } + ] + }; + } else { + return { + type: 'no_store_found', + message: '在您附近没有找到我们的门店,建议您尝试线上服务或查看其他区域的门店。', + alternatives: this.findAlternativeStores(shopId) + }; + } + } + + // 处理营业时间查询 + handleStoreHours(userMessage, userContext) { + const storeName = this.extractStoreName(userMessage); + const store = this.findStoreByName(storeName, userContext.currentShopId); + + if (store) { + const hours = store.businessHours; + const currentStatus = this.getCurrentStatus(store); + + return { + type: 'business_hours', + message: `${store.name}的营业时间:${this.formatBusinessHours(hours)}`, + data: { + store: store, + currentStatus: currentStatus, + todayHours: this.getTodayHours(hours) + } + }; + } + } +} +``` + +### **4. 响应消息模板** + +```javascript +// 店铺门店信息响应模板 +const StoreResponseTemplates = { + // 门店列表响应 + storeList: (stores, options = {}) => { + return { + type: 'store_list', + layout: 'card', + title: options.title || '我们的门店', + items: stores.map(store => ({ + title: store.name, + description: store.address, + image: store.image, + meta: { + distance: store.distance ? `${store.distance}km` : '', + status: store.status === 'open' ? '营业中' : '休息中', + hours: store.businessHours ? '查看营业时间' : '' + }, + actions: [ + { text: '导航', action: 'navigate', data: { storeId: store.id } }, + { text: '电话', action: 'call', data: { phone: store.phone } }, + { text: '详情', action: 'detail', data: { storeId: store.id } } + ] + })), + quickReplies: [ + '找最近的门店', + '查看所有门店', + '联系客服' + ] + }; + }, + + // 单个门店详情 + storeDetail: (store) => { + return { + type: 'store_detail', + layout: 'detail', + title: store.name, + sections: [ + { + title: '📍 地址', + content: store.address, + action: { type: 'map', data: store.coordinates } + }, + { + title: '📞 联系方式', + content: store.phone, + action: { type: 'call', data: store.phone } + }, + { + title: '🕒 营业时间', + content: formatBusinessHours(store.businessHours), + action: { type: 'hours', data: store.businessHours } + }, + { + title: '👥 门店服务', + content: store.services.join('、'), + action: { type: 'services', data: store.services } + } + ], + actions: [ + { text: '分享门店', action: 'share' }, + { text: '收藏门店', action: 'favorite' }, + { text: '投诉建议', action: 'feedback' } + ] + }; + }, + + // 营业时间响应 + businessHours: (store, currentStatus) => { + return { + type: 'business_hours', + message: `**${store.name}**\n\n` + + `当前状态:**${currentStatus}**\n\n` + + `营业时间:\n${formatBusinessHours(store.businessHours)}`, + quickActions: [ + { text: '明天营业时间', action: 'tomorrow_hours' }, + { text: '节假日安排', action: 'holiday_hours' }, + { text: '联系确认', action: 'confirm_hours' } + ] + }; + } +}; + +// 工具函数 +function formatBusinessHours(hours) { + if (!hours) return '暂无信息'; + + const days = ['周一', '周二', '周三', '周四', '周五', '周六', '周日']; + return days.map((day, index) => { + const dayHours = hours[index] || hours['default']; + return `${day}: ${dayHours || '休息'}`; + }).join('\n'); +} +``` + +## 🎯 **总结:店铺与门店的关系** + +### **核心关系** +1. **包含关系**:店铺包含门店(1:N) +2. **品牌关系**:店铺是品牌,门店是落地实体 +3. **管理关系**:店铺管理门店的标准和运营 + +### **在智能客服中的应用** +- **用户说"我要找店铺"** → 理解为品牌/总店概念 +- **用户说"找最近的门店"** → 理解为具体营业点 +- **用户说"你们店在哪里"** → 需要区分是指品牌还是具体门店 + +### **数据处理建议** +```javascript +// 在客服系统中这样区分: +const customerQuery = { + "找苹果店": { type: "shop", brand: "apple" }, + "找朝阳区的苹果店": { type: "store", brand: "apple", location: "朝阳区" }, + "线上店铺": { type: "online_shop" }, + "实体门店": { type: "physical_store" } +}; +``` + +这样的区分让智能客服能够更精准地理解用户意图,提供更准确的店铺/门店信息服务。 diff --git a/src/app/Controller.php b/src/app/Controller.php index 0a095445d..1a52760f4 100644 --- a/src/app/Controller.php +++ b/src/app/Controller.php @@ -13,6 +13,7 @@ use liliuwei\think\Jump; use think\facade\Route; use think\facade\Config; use think\facade\Env; +use app\tracking\VisitorTracker; /** * 控制器基础类 @@ -79,6 +80,23 @@ abstract class Controller } //固定一个版本号 $this->assign('version', 128); + + // ------------ 埋点:新增用户访问来源统计数据 ------------ + try { + $visitorTracker = new VisitorTracker(); + $siteId = property_exists($this, 'site_id') ? $this->site_id : 0; + $visitorTracker->shop_visit(['site_id' => $siteId]); + if (property_exists($this, 'store_id')) { + $storeId = $this->store_id; + if (!empty($storeId)) { + $visitorTracker->store_visit(['site_id' => $siteId, 'store_id' => $storeId]); + } + } + } catch (\Exception $e) { + // 埋点异常不影响正常业务 + log_write('埋点异常:' . $e->getMessage(), 'error'); + } + return View::fetch($template, $vars); } diff --git a/src/app/api/controller/AI.php b/src/app/api/controller/AI.php new file mode 100644 index 000000000..0eb21163b --- /dev/null +++ b/src/app/api/controller/AI.php @@ -0,0 +1,505 @@ +aiChatSessionModel = new AiChatSession(); + $this->aiChatHistoryModel = new AiChatHistory(); + $this->configModel = new Config(); + } + + /** + * 平台类型常量 + */ + const PLATFORM_DIFY = 'dify'; + const PLATFORM_RAGFLOW = 'ragflow'; + + /** + * AI对话接口 + * 发送对话或者创建会话 + * 1. 支持与Dify平台或RAGFlow平台的智能体进行交互 + * 2. 支持会话管理,每个用户在每个平台上的会话是独立的 + * 3. 支持上下文管理,每个会话都维护一个上下文信息,用于持续对话 + * + * @return Json + */ + public function chat() + { + log_write('AI chat request: ' . json_encode($this->params), 'info', $this->log_file); + try { + // 获取请求参数 + $message = $this->params['message'] ?? ''; // 用户消息 + $session_id = $this->params['session_id'] ?? ''; // 会话ID + $user_id = $this->params['user_id'] ?? $this->member_id; // 用户ID + $context = $this->params['context'] ?? []; // 上下文信息 + + $site_id = $this->params['uniacid'] ?? $this->site_id; // 站点ID + $app_module = $this->params['app_module'] ?? $this->app_module; // 应用模块 + + // 参数验证 + if (empty($message)) { + return $this->response($this->error('', 'MESSAGE_EMPTY')); + } + + // 获取平台配置 + $config = $this->getPlatformConfig($site_id, $app_module); + if (!$config['status']) { + return $this->response($this->error('', $config['message'])); + } + + $platform = $config['data']['default']['type'] ?? self::PLATFORM_DIFY; // 平台类型 + + // 生成或使用现有会话ID + $session_id = $session_id ?: $this->generateSessionId($user_id, $platform); + + // 根据平台类型调用不同的方法 + $result = []; + if ($platform === self::PLATFORM_DIFY) { + $result = $this->callDifyApi($config['data'], $message, $session_id, $user_id, $context); + } else if ($platform === self::PLATFORM_RAGFLOW) { + $result = $this->callRagflowApi($config['data'], $message, $session_id, $user_id, $context); + } + + if (!$result['status']) { + return $this->response($this->error('', $result['message'])); + } + + // 保存会话记录 + $this->saveChatHistory($user_id, $session_id, $platform, $message, $result['data']['content']); + + return $this->response($this->success([ + 'session_id' => $session_id, + 'content' => $result['data']['content'], + 'tokens' => $result['data']['tokens'] ?? null, + 'response_time' => $result['data']['response_time'] ?? null + ])); + + } catch (Exception $e) { + // 记录错误日志 + log_write('AI chat error: ' . $e->getMessage(), 'error', $this->log_file); + return $this->response($this->error('', 'AI_SERVICE_ERROR')); + } + } + + /** + * 获取平台配置 + * @param int $site_id 站点ID 默认为1, 业务站点uniacid值与site_id保持一致 + * @param string $app_module 应用模块 默认为shop + * @return array + */ + private function getPlatformConfig($site_id, $app_module) + { + $config = []; + try { + // 从配置模型获取平台配置 + $config = $this->configModel->getAIPlatformConfig($site_id, $app_module); + + throw new \Exception('Get AI platform config error: ' . json_encode($config)); + + if (!$config || !$config['status']) { + return ['status' => false, 'message' => 'PLATFORM_CONFIG_NOT_FOUND']; + } + + $config_data = json_decode($config['config'], true); + if (!$config_data || empty($config_data['api_key']) || empty($config_data['base_url']) || empty($config_data['app_id'])) { + return ['status' => false, 'message' => 'PLATFORM_CONFIG_INVALID']; + } + + return ['status' => true, 'data' => $config_data]; + + } catch (Exception $e) { + return ['status' => false, 'message' => 'GET_CONFIG_ERROR' . $e->getMessage()]; + } + } + + /** + * 生成会话ID + * @param string $user_id + * @param string $platform + * @return string + */ + private function generateSessionId($user_id, $platform) + { + return md5($user_id . '_' . $platform . '_' . time() . '_' . rand(1000, 9999)); + } + + /** + * 调用Dify API + * @param array $config + * @param string $message + * @param string $session_id + * @param string $user_id + * @param array $context + * @return array + */ + private function callDifyApi($config, $message, $session_id, $user_id, $context = []) + { + try { + $start_time = microtime(true); + + $headers = [ + 'Authorization: Bearer ' . $config['api_key'], + 'Content-Type: application/json', + 'Accept: application/json', + ]; + + $data = [ + 'inputs' => $context, + 'query' => $message, + 'response_mode' => $config['response_mode'] ?? 'streaming', + 'user' => $user_id, + 'conversation_id' => $session_id + ]; + + $url = rtrim($config['base_url'], '/') . '/v1/chat-messages'; + + // 发送请求 + $result = $this->httpRequest($url, $data, $headers); + + if (!$result['status']) { + return ['status' => false, 'message' => 'DIFY_API_ERROR']; + } + + $response_time = round((microtime(true) - $start_time) * 1000, 2); + + return [ + 'status' => true, + 'data' => [ + 'content' => $result['data']['answer'] ?? $result['data']['choices'][0]['message']['content'] ?? '', + 'tokens' => [ + 'prompt' => $result['data']['usage']['prompt_tokens'] ?? 0, + 'completion' => $result['data']['usage']['completion_tokens'] ?? 0, + 'total' => $result['data']['usage']['total_tokens'] ?? 0 + ], + 'response_time' => $response_time + ] + ]; + + } catch (Exception $e) { + log_write('Dify API error: ' . $e->getMessage(), 'error', $this->log_file); + return ['status' => false, 'message' => 'DIFY_CALL_ERROR']; + } + } + + /** + * 调用RAGFlow API + * @param array $config + * @param string $message + * @param string $session_id + * @param string $user_id + * @param array $context + * @return array + */ + private function callRagflowApi($config, $message, $session_id, $user_id, $context = []) + { + try { + $start_time = microtime(true); + + $headers = [ + 'Authorization: Bearer ' . $config['api_key'], + 'Content-Type: application/json', + 'Accept: application/json', + ]; + + $data = [ + 'query' => $message, + 'conversation_id' => $session_id, + 'user_id' => $user_id, + 'agent_id' => $config['app_id'], + 'stream' => $config['stream'] ?? false + ]; + + $url = rtrim($config['base_url'], '/') . '/api/v1/chat/completions'; + + // 发送请求 + $result = $this->httpRequest($url, $data, $headers); + + if (!$result['status']) { + return ['status' => false, 'message' => 'RAGFLOW_API_ERROR']; + } + + $response_time = round((microtime(true) - $start_time) * 1000, 2); + + return [ + 'status' => true, + 'data' => [ + 'content' => $result['data']['choices'][0]['message']['content'] ?? $result['data']['answer'] ?? '', + 'tokens' => [ + 'prompt' => $result['data']['usage']['prompt_tokens'] ?? 0, + 'completion' => $result['data']['usage']['completion_tokens'] ?? 0, + 'total' => $result['data']['usage']['total_tokens'] ?? 0 + ], + 'response_time' => $response_time + ] + ]; + + } catch (Exception $e) { + log_write('RAGFlow API error: ' . $e->getMessage(), 'error', $this->log_file); + return ['status' => false, 'message' => 'RAGFLOW_CALL_ERROR']; + } + } + + /** + * 保存聊天记录 + * @param string $user_id + * @param string $session_id + * @param string $platform + * @param string $user_message + * @param string $ai_message + */ + private function saveChatHistory($user_id, $session_id, $platform, $user_message, $ai_message) + { + try { + $data = [ + 'site_id' => $this->site_id, + 'user_id' => $user_id, + 'session_id' => $session_id, + 'platform' => $platform, + 'user_message' => $user_message, + 'ai_message' => $ai_message, + 'create_time' => time(), + 'ip' => Request::ip() + ]; + + // 使用事务保存聊天记录 + Db::startTrans(); + try { + // 使用模型保存聊天记录 + $save_result = $this->aiChatHistoryModel->saveHistory($data); + if (!$save_result['success']) { + log_write('Save chat history failed: ' . $save_result['msg'], 'error', $this->log_file); + } + + // 更新会话最后活动时间 + $update_result = $this->aiChatSessionModel->updateLastActiveTime($session_id); + if (!$update_result['success']) { + log_write('Update session active time failed: ' . $update_result['msg'], 'error', $this->log_file); + } + + // 如果会话不存在,创建新会话 + $session_info = $this->aiChatSessionModel->getSessionInfo(['session_id' => $session_id]); + if (!$session_info['success']) { + $session_data = [ + 'site_id' => $this->site_id, + 'user_id' => $user_id, + 'session_id' => $session_id, + 'platform' => $platform, + 'create_time' => time(), + 'last_active_time' => time() + ]; + $create_result = $this->aiChatSessionModel->createSession($session_data); + if (!$create_result['success']) { + log_write('Create session failed: ' . $create_result['msg'], 'error', $this->log_file); + } + } + + Db::commit(); + } catch (Exception $e) { + Db::rollback(); + log_write('Save chat history error: ' . $e->getMessage(), 'error', $this->log_file); + } + } catch (Exception $e) { + // 记录错误但不影响主流程 + log_write('Save chat history exception: ' . $e->getMessage(), 'error', $this->log_file); + } + } + + /** + * HTTP请求方法 + * @param string $url + * @param array $data + * @param array $headers + * @return array + */ + private function httpRequest($url, $data = [], $headers = []) + { + try { + $ch = curl_init(); + curl_setopt($ch, CURLOPT_URL, $url); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_TIMEOUT, 30); + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); + curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); + + if (!empty($data)) { + curl_setopt($ch, CURLOPT_POST, true); + curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data)); + } + + if (!empty($headers)) { + curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); + } + + $response = curl_exec($ch); + $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE); + + curl_close($ch); + + if ($http_code != 200) { + log_write('HTTP request failed: URL=' . $url . ', Code=' . $http_code . ', Response=' . $response, 'error', $this->log_file); + return ['status' => false, 'message' => 'HTTP_REQUEST_FAILED', 'code' => $http_code]; + } + + $result = json_decode($response, true); + if (json_last_error() !== JSON_ERROR_NONE) { + log_write('JSON decode error: ' . json_last_error_msg(), 'error', $this->log_file); + return ['status' => false, 'message' => 'JSON_DECODE_ERROR']; + } + + return ['status' => true, 'data' => $result]; + + } catch (Exception $e) { + log_write('HTTP request exception: ' . $e->getMessage(), 'error', $this->log_file); + return ['status' => false, 'message' => 'HTTP_REQUEST_EXCEPTION']; + } + } + + /** + * 获取会话历史 + * @return Json + */ + public function getHistory() + { + try { + $session_id = $this->params['session_id'] ?? ''; + $user_id = $this->params['user_id'] ?? $this->member_id; + $page = $this->params['page'] ?? 1; + $page_size = $this->params['page_size'] ?? 20; + + if (empty($session_id)) { + return $this->response($this->error('', 'SESSION_ID_EMPTY')); + } + + // 使用模型获取会话历史 + $where = [ + 'site_id' => $this->site_id, + 'user_id' => $user_id + ]; + + $result = $this->aiChatHistoryModel->getHistoryBySessionId($session_id, $where, $page, $page_size); + + if (!$result['success']) { + log_write('Get history failed: ' . $result['msg'], 'error', $this->log_file); + return $this->response($this->error('', 'GET_HISTORY_ERROR')); + } + + return $this->response($this->success($result['data'])); + + } catch (Exception $e) { + log_write('Get history error: ' . $e->getMessage(), 'error', $this->log_file); + return $this->response($this->error('', 'GET_HISTORY_ERROR')); + } + } + + /** + * 获取用户的会话列表 + * @return Json + */ + public function getSessions() + { + try { + $user_id = $this->params['user_id'] ?? $this->member_id; + $page = $this->params['page'] ?? 1; + $page_size = $this->params['page_size'] ?? 20; + + // 使用模型获取会话列表 + $where = [ + 'site_id' => $this->site_id, + 'user_id' => $user_id + ]; + + $result = $this->aiChatSessionModel->getSessionList($where, ['*'], 'last_active_time DESC', $page, $page_size); + + if (!$result['success']) { + return $this->response($this->error('', 'GET_SESSIONS_ERROR')); + } + + return $this->response($this->success($result['data'])); + + } catch (Exception $e) { + log_write('Get sessions error: ' . $e->getMessage(), 'error', $this->log_file); + return $this->response($this->error('', 'GET_SESSIONS_ERROR')); + } + } + + /** + * 删除会话 + * @return Json + */ + public function deleteSession() + { + try { + $session_id = $this->params['session_id'] ?? ''; + $user_id = $this->params['user_id'] ?? $this->member_id; + + if (empty($session_id)) { + return $this->response($this->error('', 'SESSION_ID_EMPTY')); + } + + // 使用模型删除会话 + $where = [ + 'site_id' => $this->site_id, + 'session_id' => $session_id, + 'user_id' => $user_id + ]; + + $result = $this->aiChatSessionModel->deleteSession($where); + + if (!$result['success']) { + return $this->response($this->error('', 'DELETE_SESSION_ERROR')); + } + + return $this->response($this->success()); + + } catch (Exception $e) { + log_write('Delete session exception: ' . $e->getMessage(), 'error', $this->log_file); + return $this->response($this->error('', 'DELETE_SESSION_EXCEPTION')); + } + } +} \ No newline at end of file diff --git a/src/app/api/controller/BaseApi.php b/src/app/api/controller/BaseApi.php index ac61b3c39..e42a8a3af 100644 --- a/src/app/api/controller/BaseApi.php +++ b/src/app/api/controller/BaseApi.php @@ -18,6 +18,7 @@ use think\facade\Cache; use addon\store\model\Config as StoreConfig; use app\model\store\Store; use think\Response; +use app\tracking\VisitorTracker; class BaseApi { @@ -97,6 +98,15 @@ class BaseApi } $this->store_id = $this->params[ 'store_id' ] ?? 0; + + + // ------------ 埋点:新增用户访问来源统计数据 ------------ + $visitorTracker = new VisitorTracker(); + $visitorTracker->shop_visit(['site_id' => $this->site_id,]); + if (!empty($this->params[ 'store_id' ])) { + $visitorTracker->store_visit(['site_id' => $this->site_id, 'store_id' => $this->store_id]); + } + // ------------------------------------------- ------------ } /** diff --git a/src/app/api/controller/Config.php b/src/app/api/controller/Config.php index d41f9712f..6931e4b28 100644 --- a/src/app/api/controller/Config.php +++ b/src/app/api/controller/Config.php @@ -11,6 +11,7 @@ use app\model\web\DiyView as DiyViewModel; use app\model\shop\Shop as ShopModel; use app\model\member\Config as ConfigMemberModel; + class Config extends BaseApi { @@ -107,7 +108,6 @@ class Config extends BaseApi */ public function init() { - $diy_view = new DiyViewModel(); $diy_style = $diy_view->getStyleConfig($this->site_id)[ 'data' ][ 'value' ]; diff --git a/src/app/command/Schedule.php b/src/app/command/Schedule.php index 66ea626c0..fcdea86be 100644 --- a/src/app/command/Schedule.php +++ b/src/app/command/Schedule.php @@ -21,8 +21,14 @@ class Schedule extends Task */ protected function execute() { - //...具体的任务执行 - $cron_model = new Cron(); - $cron_model->execute(ScheduleDict::cli); + try { + log_write('Cron的Schedule开始执行', 'debug'); + //...具体的任务执行 + $cron_model = new Cron(); + $cron_model->execute(ScheduleDict::cli); + log_write('Cron的Schedule执行完成', 'debug'); + } catch (\Exception $e) { + log_write('Cron的Schedule执行失败:' . $e->getMessage(), 'error'); + } } } diff --git a/src/app/common.php b/src/app/common.php index 1d50c4a78..4690aca97 100644 --- a/src/app/common.php +++ b/src/app/common.php @@ -8,8 +8,10 @@ error_reporting(E_NOTICE); use extend\QRcode as QRcode; use think\facade\Session; use think\facade\Event; +use think\facade\App; use app\model\system\Addon; use extend\Barcode; +use app\common\library\CallerInfo; /*****************************************************基础函数*********************************************************/ @@ -1877,6 +1879,77 @@ function paramFilter($param) return preg_replace($filter_rule, '', $param); } +/** + * 简单日志写入 + * @param string $msg 日志内容 + * @param string $level 日志级别 debug、info、warning、error + * @param string $file 日志文件名(不含路径) + */ +function log_write(string $message, string $level = 'info', string $filename = '', int $maxFileSize = 5242880): void +{ + $callerInfo = CallerInfo::getCallerInfo(1); + + // 格式化日志内容 + $content = sprintf( + "[%s] %s: %s (文件:%s,行:%d) " . PHP_EOL, + date('Y-m-d H:i:s'), + strtoupper($level), + is_array($message) ? json_encode($message, JSON_UNESCAPED_UNICODE) : $message, + $callerInfo['file'], + $callerInfo['line'] + ); + + $logPath = app()->getRuntimePath() . 'log/app_run_info/'; // eg. /runtime/shop/log + + // 确保日志目录存在 + if (!is_dir($logPath)) { + mkdir($logPath, 0755, true); + } + + // 生成日志文件名 + if (empty($filename)) { + $filename = date('Y-m-d') . '.log'; + } + + // 获取基础日志文件名(不含扩展名)和扩展名 + $fileInfo = pathinfo($filename); + $baseName = $fileInfo['filename']; + $extension = isset($fileInfo['extension']) ? '.' . $fileInfo['extension'] : '.log'; + + // 日志文件路径 + $logFile = $logPath . $filename; + + // 检查文件大小并处理日志分割 + if (file_exists($logFile) && filesize($logFile) >= $maxFileSize) { + // 查找现有的带序号的日志文件,确定下一个序号 + $nextIndex = 1; + $pattern = $logPath . $baseName . '-???' . $extension; + $matchingFiles = glob($pattern); + + if (!empty($matchingFiles)) { + // 提取最大序号 + $maxIndex = 0; + foreach ($matchingFiles as $file) { + $fileBase = pathinfo($file, PATHINFO_FILENAME); + if (preg_match('/-([0-9]{3})$/', $fileBase, $matches)) { + $index = (int)$matches[1]; + $maxIndex = max($maxIndex, $index); + } + } + $nextIndex = $maxIndex + 1; + } + + // 生成带序号的新文件名 + $newFilename = $baseName . '-' . sprintf('%03d', $nextIndex) . $extension; + $logFile = $logPath . $newFilename; + } + + // 写入文件(追加模式) + file_put_contents($logFile, $content, FILE_APPEND | LOCK_EX); +} + + + /** * 将异常格式化HTML输出,方便定位 * @param mixed $th diff --git a/src/app/common/library/BrowserDetector.php b/src/app/common/library/BrowserDetector.php new file mode 100644 index 000000000..99bed8b6b --- /dev/null +++ b/src/app/common/library/BrowserDetector.php @@ -0,0 +1,346 @@ +userAgent = $_SERVER['HTTP_USER_AGENT'] ?? ''; + $this->headers = $this->getAllHeaders(); + $this->server = $_SERVER; + $this->request = $_REQUEST; + } + + /** + * 综合检测方法 + */ + public function detect() + { + return [ + // 基础信息 + 'user_agent' => $this->userAgent, + 'ip_address' => $this->getClientIp(), + 'request_time' => date('Y-m-d H:i:s', $_SERVER['REQUEST_TIME']), + + // 平台类型 + 'is_wechat' => $this->isWeChat(), + 'is_alipay' => $this->isAlipay(), + 'is_dingtalk' => $this->isDingTalk(), + 'is_miniprogram' => $this->isMiniProgram(), + + // 设备类型 + 'is_mobile' => $this->isMobile(), + 'is_tablet' => $this->isTablet(), + 'is_desktop' => $this->isDesktop(), + + // 浏览器类型 + 'is_chrome' => $this->isChrome(), + 'is_firefox' => $this->isFirefox(), + 'is_safari' => $this->isSafari(), + 'is_edge' => $this->isEdge(), + 'is_ie' => $this->isIE(), + + // 操作系统 + 'is_windows' => $this->isWindows(), + 'is_mac' => $this->isMac(), + 'is_linux' => $this->isLinux(), + 'is_ios' => $this->isIOS(), + 'is_android' => $this->isAndroid(), + + // 来源类型 + 'source_type' => $this->getSourceType(), + 'source_name' => $this->getSourceName(), + + // URL参数 + 'referer' => $_SERVER['HTTP_REFERER'] ?? '', + 'utm_source' => $_GET['utm_source'] ?? '', + 'utm_medium' => $_GET['utm_medium'] ?? '', + 'utm_campaign' => $_GET['utm_campaign'] ?? '', + + // 自定义来源 + 'custom_source' => $this->getCustomSource(), + + // 请求信息 + 'request_method' => $_SERVER['REQUEST_METHOD'], + 'request_uri' => $_SERVER['REQUEST_URI'], + 'query_string' => $_SERVER['QUERY_STRING'] ?? '', + ]; + } + + /** + * 获取所有请求头 + */ + private function getAllHeaders() + { + if (function_exists('getallheaders')) { + return getallheaders(); + } + + $headers = []; + foreach ($_SERVER as $name => $value) { + if (substr($name, 0, 5) == 'HTTP_') { + $headers[str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', substr($name, 5)))))] = $value; + } + } + return $headers; + } + + /** + * 获取客户端IP + */ + private function getClientIp() + { + $ip = ''; + if (!empty($_SERVER['HTTP_CLIENT_IP'])) { + $ip = $_SERVER['HTTP_CLIENT_IP']; + } elseif (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) { + $ip = $_SERVER['HTTP_X_FORWARDED_FOR']; + } elseif (!empty($_SERVER['REMOTE_ADDR'])) { + $ip = $_SERVER['REMOTE_ADDR']; + } + return $ip; + } + + /** + * 判断微信环境 + */ + public function isWeChat() + { + return stripos($this->userAgent, 'micromessenger') !== false; + } + + /** + * 判断支付宝环境 + */ + public function isAlipay() + { + return stripos($this->userAgent, 'alipay') !== false; + } + + /** + * 判断钉钉环境 + */ + public function isDingTalk() + { + return stripos($this->userAgent, 'dingtalk') !== false; + } + + /** + * 判断小程序环境 + */ + public function isMiniProgram() + { + // 微信小程序 + if ($this->isWeChat() && stripos($this->userAgent, 'miniprogram') !== false) { + return true; + } + + // 支付宝小程序 + if ($this->isAlipay() && stripos($this->userAgent, 'miniprogram') !== false) { + return true; + } + + // 通过Referer判断(小程序跳转H5时) + $referer = $_SERVER['HTTP_REFERER'] ?? ''; + if (strpos($referer, 'servicewechat.com') !== false || + strpos($referer, 'alipay.com') !== false) { + return true; + } + + return false; + } + + /** + * 判断移动设备 + */ + public function isMobile() + { + return preg_match('/android|webos|iphone|ipad|ipod|blackberry|iemobile|opera mini/i', $this->userAgent); + } + + /** + * 判断平板设备 + */ + public function isTablet() + { + return preg_match('/ipad|android(?!.*mobile)|tablet/i', $this->userAgent); + } + + /** + * 判断桌面设备 + */ + public function isDesktop() + { + return !$this->isMobile() && !$this->isTablet(); + } + + /** + * 浏览器类型判断 + */ + public function isChrome() + { + return stripos($this->userAgent, 'chrome') !== false && stripos($this->userAgent, 'edge') === false; + } + + public function isFirefox() + { + return stripos($this->userAgent, 'firefox') !== false; + } + + public function isSafari() + { + return stripos($this->userAgent, 'safari') !== false && stripos($this->userAgent, 'chrome') === false; + } + + public function isEdge() + { + return stripos($this->userAgent, 'edge') !== false; + } + + public function isIE() + { + return stripos($this->userAgent, 'msie') !== false || stripos($this->userAgent, 'trident') !== false; + } + + /** + * 操作系统判断 + */ + public function isWindows() + { + return stripos($this->userAgent, 'windows') !== false; + } + + public function isMac() + { + return stripos($this->userAgent, 'macintosh') !== false || stripos($this->userAgent, 'mac os x') !== false; + } + + public function isLinux() + { + return stripos($this->userAgent, 'linux') !== false && !$this->isAndroid(); + } + + public function isIOS() + { + return preg_match('/iphone|ipad|ipod/i', $this->userAgent); + } + + public function isAndroid() + { + return stripos($this->userAgent, 'android') !== false; + } + + /** + * 获取自定义来源 + */ + private function getCustomSource() + { + return [ + 'from' => $_GET['from'] ?? '', + 'source' => $_GET['source'] ?? '', + 'channel' => $_GET['channel'] ?? '', + 'campaign' => $_GET['campaign'] ?? '' + ]; + } + + /** + * 综合判断来源类型 + */ + public function getSourceType() + { + if ($this->isMiniProgram()) { + return 'miniprogram'; + } elseif ($this->isWeChat()) { + return 'wechat'; + } elseif ($this->isAlipay()) { + return 'alipay'; + } elseif ($this->isMobile()) { + return 'h5'; + } elseif ($this->isDesktop()) { + return 'pc'; + } else { + return 'unknown'; + } + } + + /** + * 获取详细的来源名称 + */ + public function getSourceName() + { + $sourceType = $this->getSourceType(); + + switch ($sourceType) { + case 'miniprogram': + if ($this->isWeChat()) return 'wechat_miniprogram'; + if ($this->isAlipay()) return 'alipay_miniprogram'; + return 'miniprogram'; + + case 'wechat': + if ($this->isIOS()) return 'wechat_ios'; + if ($this->isAndroid()) return 'wechat_android'; + return 'wechat'; + + case 'h5': + if ($this->isChrome()) return 'h5_chrome'; + if ($this->isSafari()) return 'h5_safari'; + return 'h5'; + + case 'pc': + if ($this->isChrome()) return 'pc_chrome'; + if ($this->isFirefox()) return 'pc_firefox'; + if ($this->isEdge()) return 'pc_edge'; + return 'pc'; + + default: + return 'unknown'; + } + } + + /** + * 生成客户端指纹 + */ + public function generateFingerprint() + { + $components = [ + $this->userAgent, + $_SERVER['HTTP_ACCEPT_LANGUAGE'] ?? '', + $this->getClientIp(), + $_SERVER['HTTP_ACCEPT'] ?? '', + $_SERVER['HTTP_ACCEPT_ENCODING'] ?? '' + ]; + + return md5(implode('|', $components)); + } + + /** + * 保存检测结果到日志 + */ + public function saveToLog($filename = 'browser_detection.log') + { + $data = $this->detect(); + $logEntry = json_encode($data, JSON_UNESCAPED_UNICODE) . PHP_EOL; + file_put_contents($filename, $logEntry, FILE_APPEND | LOCK_EX); + } + + /** + * 获取统计信息 + */ + public function getStats() + { + return [ + 'user_agent_length' => strlen($this->userAgent), + 'header_count' => count($this->headers), + 'is_secure' => !empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off', + 'is_ajax' => !empty($_SERVER['HTTP_X_REQUESTED_WITH']) && + strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) == 'xmlhttprequest', + 'is_post' => $_SERVER['REQUEST_METHOD'] === 'POST' + ]; + } +} \ No newline at end of file diff --git a/src/app/common/library/CallerInfo.php b/src/app/common/library/CallerInfo.php new file mode 100644 index 000000000..b59663bb8 --- /dev/null +++ b/src/app/common/library/CallerInfo.php @@ -0,0 +1,112 @@ + 'unknown', + 'line' => 0, + 'function' => 'unknown', + 'class' => 'unknown', + 'type' => 'unknown' + ]; + } + + $caller = $backtrace[$depth]; + + return [ + 'file' => $caller['file'] ?? 'unknown', + 'line' => $caller['line'] ?? 0, + 'function' => $caller['function'] ?? 'unknown', + 'class' => $caller['class'] ?? 'unknown', + 'type' => $caller['type'] ?? 'unknown', // '->' 或 '::' + 'args_count' => isset($caller['args']) ? count($caller['args']) : 0 + ]; + } + + /** + * 获取调用者文件路径 + */ + public static function getCallerFile(int $depth = 1): string + { + $info = self::getCallerInfo($depth + 1); + return $info['file']; + } + + /** + * 获取调用者行号 + */ + public static function getCallerLine(int $depth = 1): int + { + $info = self::getCallerInfo($depth + 1); + return $info['line']; + } + + /** + * 获取调用栈信息(完整) + */ + public static function getCallStack(int $limit = 10): array + { + $backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, $limit); + $stack = []; + + // 跳过当前函数 + array_shift($backtrace); + + foreach ($backtrace as $index => $trace) { + $stack[] = [ + 'level' => $index, + 'file' => $trace['file'] ?? 'internal', + 'line' => $trace['line'] ?? 0, + 'function' => $trace['function'] ?? 'unknown', + 'class' => $trace['class'] ?? '', + 'type' => $trace['type'] ?? '', + 'args' => isset($trace['args']) ? count($trace['args']) : 0 + ]; + } + + return $stack; + } + + /** + * 获取调用链字符串(用于日志) + */ + public static function getCallChain(): string + { + $backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 5); + $chain = []; + + // 跳过当前函数 + array_shift($backtrace); + + foreach ($backtrace as $trace) { + $call = ''; + + if (isset($trace['class'])) { + $call .= $trace['class'] . $trace['type']; + } + + $call .= $trace['function']; + + if (isset($trace['file'])) { + $call .= ' (' . basename($trace['file']) . ':' . $trace['line'] . ')'; + } + + $chain[] = $call; + } + + return implode(' -> ', array_reverse($chain)); + } +} \ No newline at end of file diff --git a/src/app/common/library/EnhancedLogger.php b/src/app/common/library/EnhancedLogger.php new file mode 100644 index 000000000..79243c9ae --- /dev/null +++ b/src/app/common/library/EnhancedLogger.php @@ -0,0 +1,115 @@ + date('Y-m-d H:i:s'), + 'level' => strtoupper($level), + 'message' => $message, + 'context' => $context, + 'caller' => [ + 'file' => $callerInfo['file'], + 'line' => $callerInfo['line'], + 'function' => $callerInfo['function'], + 'class' => $callerInfo['class'] + ], + 'memory_usage' => memory_get_usage(true), + 'peak_memory' => memory_get_peak_usage(true) + ]; + + // 写入日志文件 + self::writeToFile($logData); + } + + /** + * 调试日志(自动包含调用栈) + */ + public static function debug(string $message, array $context = []): void + { + $callStack = CallerInfo::getCallStack(5); + + self::log($message, 'debug', array_merge($context, [ + 'call_stack' => $callStack + ])); + } + + /** + * 性能日志 + */ + public static function profile(string $operation, callable $callback, array $context = []): mixed + { + $startTime = microtime(true); + $startMemory = memory_get_usage(true); + + try { + $result = $callback(); + + $endTime = microtime(true); + $endMemory = memory_get_usage(true); + + self::log($operation, 'profile', array_merge($context, [ + 'execution_time' => round(($endTime - $startTime) * 1000, 2) . 'ms', + 'memory_used' => self::formatBytes($endMemory - $startMemory), + 'success' => true + ])); + + return $result; + + } catch (\Exception $e) { + $endTime = microtime(true); + + self::log($operation, 'profile', array_merge($context, [ + 'execution_time' => round(($endTime - $startTime) * 1000, 2) . 'ms', + 'success' => false, + 'error' => $e->getMessage() + ])); + + throw $e; + } + } + + private static function writeToFile(array $logData): void + { + $logLine = sprintf( + "[%s] %s: %s [%s:%d in %s::%s()] %s\n", + $logData['timestamp'], + $logData['level'], + $logData['message'], + basename($logData['caller']['file']), + $logData['caller']['line'], + $logData['caller']['class'], + $logData['caller']['function'], + json_encode($logData['context'], JSON_UNESCAPED_UNICODE) + ); + + file_put_contents( + __DIR__ . '/../logs/app.log', + $logLine, + FILE_APPEND | LOCK_EX + ); + } + + private static function formatBytes(int $bytes): string + { + $units = ['B', 'KB', 'MB', 'GB']; + $bytes = max($bytes, 0); + $pow = floor(($bytes ? log($bytes) : 0) / log(1024)); + $pow = min($pow, count($units) - 1); + $bytes /= pow(1024, $pow); + + return round($bytes, 2) . ' ' . $units[$pow]; + } +} diff --git a/src/app/common/library/PerformanceAwareCallerInfo.php b/src/app/common/library/PerformanceAwareCallerInfo.php new file mode 100644 index 000000000..3ffa4ec14 --- /dev/null +++ b/src/app/common/library/PerformanceAwareCallerInfo.php @@ -0,0 +1,63 @@ + 'disabled', 'line' => 0]; + } + + // 生产环境使用更轻量的方式 + if (app()->isDebug()) { + // 开发环境:详细信息 + return self::getDetailedCallerInfo($depth); + } else { + // 生产环境:基本信息 + return self::getBasicCallerInfo($depth); + } + } + + private static function getDetailedCallerInfo(int $depth): array + { + $backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, $depth + 2); + return $backtrace[$depth] ?? ['file' => 'unknown', 'line' => 0]; + } + + private static function getBasicCallerInfo(int $depth): array + { + // 使用更轻量的方法 + $backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, $depth + 2); + $caller = $backtrace[$depth] ?? []; + + return [ + 'file' => $caller['file'] ?? 'unknown', + 'line' => $caller['line'] ?? 0, + 'function' => $caller['function'] ?? 'unknown' + ]; + } +} \ No newline at end of file diff --git a/src/app/cron/controller/Task.php b/src/app/cron/controller/Task.php index 06474513b..bc9fc11f8 100644 --- a/src/app/cron/controller/Task.php +++ b/src/app/cron/controller/Task.php @@ -28,8 +28,10 @@ class Task extends Controller */ public function checkCron() { + log_write('Task checkCron ...', 'debug'); $cron_model = new Cron(); $result = $cron_model->checkSchedule(); + log_write('Task checkCron result: ' . json_encode($result), 'debug'); return $result; } @@ -39,7 +41,10 @@ class Task extends Controller */ public function run() { - if (config('cron.default') == ScheduleDict::url) { + // 只允许系统配置中定义默认的是通过url请求,才执行该计划任务 + if (config('cron.default') != ScheduleDict::url) { + return $this->error('只允许系统配置cron.default=url请求,才执行该计划任务'); + } else { $cron_model = new Cron(); $cron_model->execute(); return true; @@ -51,6 +56,7 @@ class Task extends Controller */ public function execute() { + log_write('单独计划任务 开始执行', 'info'); if (config('cron.default') == ScheduleDict::default) { ignore_user_abort(true); set_time_limit(0); diff --git a/src/app/event/init/InitConfig.php b/src/app/event/init/InitConfig.php index 1bdf8e5bc..1fd17bd59 100644 --- a/src/app/event/init/InitConfig.php +++ b/src/app/event/init/InitConfig.php @@ -25,6 +25,7 @@ class InitConfig $this->initConst(); //初始化配置信息 $this->initConfig(); + log_write('系统配置信息已初始化', 'debug'); } /** diff --git a/src/app/event/init/InitCron.php b/src/app/event/init/InitCron.php index 46ab7e12d..f9aae8cd0 100644 --- a/src/app/event/init/InitCron.php +++ b/src/app/event/init/InitCron.php @@ -1,4 +1,5 @@ module(); - if ($module != 'cron') { - if (!defined('CRON_EXECUTE') && time() - $last_time > 100 && time() - $last_exec_time > 100) { - Cache::set("cron_http_last_exec_time", time()); - defined('CRON_EXECUTE') or define('CRON_EXECUTE', 1); - $url = url('cron/task/cronExecute'); - $ch = curl_init(); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); - curl_setopt($ch, CURLOPT_HEADER, true); - curl_setopt($ch, CURLOPT_URL, $url); - curl_setopt($ch, CURLOPT_TIMEOUT, 1); - curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); - curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); - curl_exec($ch); - -// // 获取错误信息并打印 -// $error = curl_error($ch); -// if($error){ -// //保存错误 -// Cron::setError(ScheduleDict::default, $error); -// } -// // 关闭cURL资源句柄 -// curl_close($ch); + log_write('InitCron计划任务初始化', 'debug'); + try { + //根据计划任务类型来判断 + if (config('cron.default') != ScheduleDict::default) { + log_write('InitCron计划任务未开启', 'warning'); + return; } + if (defined('BIND_MODULE') && BIND_MODULE === 'install') { + log_write('InitCron计划任务未开启,安装模块时不执行计划任务', 'warning'); + return; + } + $last_time = Cache::get("cron_last_load_time"); + if (empty($last_time)) { + $last_time = 0; + } + $last_exec_time = Cache::get("cron_http_last_exec_time"); + if (empty($last_exec_time)) { + $last_exec_time = 0; + } + $module = request()->module(); + if ($module == 'cron') { + log_write('InitCron计划任务未开启, 请求模块是 cron 模块', 'warning'); + return; + } + + $enable_start = !defined('CRON_EXECUTE') && time() - $last_time > 100 && time() - $last_exec_time > 100; + + if (!$enable_start) { + $content = sprintf('InitCron计划任务未启动,[%s, %s, %s], cron_last_load_time: %s, cron_http_last_exec_time: %s, CRON_EXECUTE:%s', + !defined('CRON_EXECUTE') ? '1' : '0', + time() - $last_time > 100 ? '1' : '0', + time() - $last_exec_time > 100 ? '1' : '0', + date('Y-m-d H:i:s', $last_time), + date('Y-m-d H:i:s', $last_exec_time), + defined('CRON_EXECUTE') ? 'true' : 'false' + ); + log_write($content, 'debug'); + return; + } + + Cache::set("cron_http_last_exec_time", time()); + defined('CRON_EXECUTE') or define('CRON_EXECUTE', 1); + $url = url('cron/task/cronExecute'); + $ch = curl_init(); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_HEADER, true); + curl_setopt($ch, CURLOPT_URL, $url); + curl_setopt($ch, CURLOPT_TIMEOUT, 1); + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); + curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); + curl_exec($ch); + + // 获取错误信息并打印 + $error = curl_error($ch); + if ($error) { + //保存错误 + Cron::setError(ScheduleDict::default, $error); + } + // 关闭cURL资源句柄 + curl_close($ch); + log_write('InitCron计划任务已启动', 'debug'); + } catch (\Exception $e) { + // 计划任务异常,直接返回 + log_write('InitCron计划任务异常:' . $e->getMessage(), 'error'); + return; } } } diff --git a/src/app/job/Cronexecute.php b/src/app/job/Cronexecute.php index aa74df83e..c7ff272b1 100644 --- a/src/app/job/Cronexecute.php +++ b/src/app/job/Cronexecute.php @@ -15,27 +15,26 @@ class Cronexecute { public function fire(Job $job, $data) { - $job->delete(); try { - - $res = event($data[ 'event' ], [ 'relate_id' => $data[ 'relate_id' ] ]); + log_write('Job开始执行:' . $data['name'], 'debug'); + $job->delete(); + $res = event($data['event'], ['relate_id' => $data['relate_id']]); $data_log = [ - 'name' => $data[ 'name' ], - 'event' => $data[ 'event' ], - 'relate_id' => $data[ 'relate_id' ], + 'name' => $data['name'], + 'event' => $data['event'], + 'relate_id' => $data['relate_id'], 'message' => json_encode($res) ]; - Log::write("计划任务:{$data[ 'event' ]} relate_id: {$data[ 'relate_id' ]}执行结果:" . json_encode($res, JSON_UNESCAPED_UNICODE)); + Log::write("计划任务:{$data['event']} relate_id: {$data['relate_id']}执行结果:" . json_encode($res, JSON_UNESCAPED_UNICODE)); $cron_model = new Cron(); //定义最新的执行时间或错误 $cron_model->addCronLog($data_log); - - + log_write('Job执行成功:' . $data['name'], 'debug'); } catch (\Exception $e) { Log::write($e->getMessage()); + log_write('Job执行失败:' . $e->getMessage(), 'error'); $job->delete(); } } - } diff --git a/src/app/model/ai/AiChatHistory.php b/src/app/model/ai/AiChatHistory.php new file mode 100644 index 000000000..867f70200 --- /dev/null +++ b/src/app/model/ai/AiChatHistory.php @@ -0,0 +1,261 @@ +error('', 'PARAMETERS_INCOMPLETE'); + } + + // 补充默认字段 + $data['create_time'] = $data['create_time'] ?? time(); + $data['ip'] = $data['ip'] ?? request()->ip(); + + // 保存数据 + $result = Db::name($this->name)->insert($data); + + if ($result) { + return $this->success(['id' => Db::name($this->name)->getLastInsID()]); + } else { + return $this->error('', 'SAVE_FAILED'); + } + } catch (Exception $e) { + // 记录错误日志 + log_write('Save chat history error: ' . $e->getMessage(), 'error', $this->log_file); + return $this->error('', 'SAVE_EXCEPTION'); + } + } + + /** + * 获取聊天历史记录列表 + * @param array $where 条件数组 + * @param array $field 查询字段 + * @param string $order 排序方式 + * @param int $page 页码 + * @param int $page_size 每页数量 + * @return array + */ + public function getHistoryList($where = [], $field = ['*'], $order = 'create_time ASC', $page = 1, $page_size = 20) + { + try { + // 计算偏移量 + $offset = ($page - 1) * $page_size; + + // 查询列表 + $list = Db::name($this->name) + ->field($field) + ->where($where) + ->order($order) + ->limit($offset, $page_size) + ->select(); + + // 查询总数 + $total = Db::name($this->name) + ->where($where) + ->count(); + + return $this->success([ + 'list' => $list, + 'total' => $total, + 'page' => $page, + 'page_size' => $page_size, + 'total_page' => ceil($total / $page_size) + ]); + } catch (Exception $e) { + log_write('Get chat history list error: ' . $e->getMessage(), 'error', $this->log_file); + return $this->error('', 'GET_LIST_FAILED'); + } + } + + /** + * 根据会话ID获取聊天记录 + * @param string $session_id 会话ID + * @param array $where 额外条件 + * @param int $page 页码 + * @param int $page_size 每页数量 + * @return array + */ + public function getHistoryBySessionId($session_id, $where = [], $page = 1, $page_size = 20) + { + if (empty($session_id)) { + return $this->error('', 'SESSION_ID_EMPTY'); + } + + $where['session_id'] = $session_id; + return $this->getHistoryList($where, ['*'], 'create_time ASC', $page, $page_size); + } + + /** + * 获取用户的所有聊天记录 + * @param string $user_id 用户ID + * @param array $where 额外条件 + * @param int $page 页码 + * @param int $page_size 每页数量 + * @return array + */ + public function getUserHistory($user_id, $where = [], $page = 1, $page_size = 20) + { + if (empty($user_id)) { + return $this->error('', 'USER_ID_EMPTY'); + } + + $where['user_id'] = $user_id; + return $this->getHistoryList($where, ['*'], 'create_time DESC', $page, $page_size); + } + + /** + * 删除聊天记录 + * @param array $where 条件数组 + * @return array + */ + public function deleteHistory($where) + { + try { + if (empty($where)) { + return $this->error('', 'DELETE_CONDITION_EMPTY'); + } + + $result = Db::name($this->name)->where($where)->delete(); + + if ($result !== false) { + return $this->success(['deleted' => $result]); + } else { + return $this->error('', 'DELETE_FAILED'); + } + } catch (Exception $e) { + log_write('Delete chat history error: ' . $e->getMessage(), 'error', $this->log_file); + return $this->error('', 'DELETE_EXCEPTION'); + } + } + + /** + * 根据会话ID删除聊天记录 + * @param string $session_id 会话ID + * @param array $where 额外条件 + * @return array + */ + public function deleteHistoryBySessionId($session_id, $where = []) + { + if (empty($session_id)) { + return $this->error('', 'SESSION_ID_EMPTY'); + } + + $where['session_id'] = $session_id; + return $this->deleteHistory($where); + } + + /** + * 获取用户的会话消息统计 + * @param string $user_id 用户ID + * @param array $where 额外条件 + * @return array + */ + public function getUserMessageStats($user_id, $where = []) + { + try { + if (empty($user_id)) { + return $this->error('', 'USER_ID_EMPTY'); + } + + $where['user_id'] = $user_id; + + // 统计总消息数 + $total_count = Db::name($this->name)->where($where)->count(); + + // 统计今日消息数 + $today_count = Db::name($this->name) + ->where($where) + ->whereTime('create_time', 'today') + ->count(); + + // 统计最近消息时间 + $last_message = Db::name($this->name) + ->where($where) + ->order('create_time DESC') + ->find(); + + return $this->success([ + 'total_count' => $total_count, + 'today_count' => $today_count, + 'last_message_time' => $last_message ? $last_message['create_time'] : 0 + ]); + } catch (Exception $e) { + log_write('Get user message stats error: ' . $e->getMessage(), 'error', $this->log_file); + return $this->error('', 'GET_STATS_FAILED'); + } + } + + /** + * 清理过期的聊天记录 + * @param int $days 保留天数 + * @param array $where 额外条件 + * @return array + */ + public function cleanupExpiredHistory($days = 30, $where = []) + { + try { + $expire_time = time() - ($days * 24 * 60 * 60); + $where['create_time'] = ['<', $expire_time]; + + return $this->deleteHistory($where); + } catch (Exception $e) { + log_write('Cleanup expired history error: ' . $e->getMessage(), 'error', $this->log_file); + return $this->error('', 'CLEANUP_FAILED'); + } + } + + /** + * 批量保存聊天记录 + * @param array $data 聊天记录数组 + * @return array + */ + public function batchSaveHistory($data) + { + try { + if (empty($data) || !is_array($data)) { + return $this->error('', 'DATA_EMPTY'); + } + + // 批量插入数据 + $result = Db::name($this->name)->insertAll($data); + + if ($result) { + return $this->success(['count' => $result]); + } else { + return $this->error('', 'BATCH_SAVE_FAILED'); + } + } catch (Exception $e) { + log_write('Batch save chat history error: ' . $e->getMessage(), 'error', $this->log_file); + return $this->error('', 'BATCH_SAVE_EXCEPTION'); + } + } +} \ No newline at end of file diff --git a/src/app/model/ai/AiChatSession.php b/src/app/model/ai/AiChatSession.php new file mode 100644 index 000000000..985782bbe --- /dev/null +++ b/src/app/model/ai/AiChatSession.php @@ -0,0 +1,266 @@ +name) + ->where($where) + ->field($field) + ->find(); + + return $this->success($data); + } catch (\Exception $e) { + log_write('Get session info error: ' . $e->getMessage(), 'error', $this->log_file); + return $this->error('', 'GET_SESSION_INFO_ERROR'); + } + } + + /** + * 获取会话列表 + * @param array $where 条件 + * @param array $field 字段 + * @param string $order 排序 + * @param int $page 页码 + * @param int $page_size 每页数量 + * @return array + */ + public function getSessionList($where = [], $field = ['*'], $order = 'last_active_time DESC', $page = 1, $page_size = 20) + { + try { + $count = Db::name($this->name) + ->where($where) + ->count(); + + $list = []; + if ($count > 0) { + $list = Db::name($this->name) + ->where($where) + ->field($field) + ->order($order) + ->page($page, $page_size) + ->select(); + } + + return $this->success(json_encode([ + 'list' => $list, + 'total' => $count, + 'page' => $page, + 'page_size' => $page_size + ])); + } catch (\Exception $e) { + log_write('Get session list error: ' . $e->getMessage(), 'error', $this->log_file); + return $this->error('', 'GET_SESSION_LIST_ERROR'); + } + } + + /** + * 创建会话 + * @param array $data 数据 + * @return array + */ + public function createSession($data = []) + { + try { + // 确保必要字段存在 + if (empty($data['session_id']) || empty($data['user_id']) || empty($data['site_id'])) { + return $this->error('', 'MISSING_REQUIRED_FIELDS'); + } + + // 检查会话是否已存在 + $exists = Db::name($this->name) + ->where('session_id', $data['session_id']) + ->count(); + + if ($exists > 0) { + return $this->error('', 'SESSION_ALREADY_EXISTS'); + } + + // 设置默认值 + $data['create_time'] = $data['create_time'] ?? time(); + $data['last_active_time'] = $data['last_active_time'] ?? time(); + + $result = Db::name($this->name)->insert($data); + + if ($result) { + return $this->success(json_encode(['session_id' => $data['session_id']])); + } else { + return $this->error('', 'CREATE_SESSION_FAILED'); + } + } catch (\Exception $e) { + log_write('Create session error: ' . $e->getMessage(), 'error', $this->log_file); + return $this->error('', 'CREATE_SESSION_ERROR'); + } + } + + /** + * 更新会话 + * @param array $where 条件 + * @param array $data 数据 + * @return array + */ + public function updateSession($where = [], $data = []) + { + try { + if (empty($where)) { + return $this->error('', 'WHERE_CONDITION_EMPTY'); + } + + $result = Db::name($this->name) + ->where($where) + ->update($data); + + return $this->success(['affected_rows' => $result]); + } catch (\Exception $e) { + log_write('Update session error: ' . $e->getMessage(), 'error', $this->log_file); + return $this->error('', 'UPDATE_SESSION_ERROR'); + } + } + + /** + * 删除会话 + * @param array $where 条件 + * @return array + */ + public function deleteSession($where = []) + { + try { + if (empty($where)) { + return $this->error('', 'WHERE_CONDITION_EMPTY'); + } + + // 开启事务 + Db::startTrans(); + try { + // 删除会话 + $result = Db::name($this->name) + ->where($where) + ->delete(); + + // 删除相关的聊天历史 + Db::name('shop_ai_chat_history') + ->where($where) + ->delete(); + + Db::commit(); + return $this->success(['affected_rows' => $result]); + } catch (\Exception $e) { + Db::rollback(); + throw $e; + } + } catch (\Exception $e) { + log_write('Delete session error: ' . $e->getMessage(), 'error', $this->log_file); + return $this->error('', 'DELETE_SESSION_ERROR'); + } + } + + /** + * 更新会话最后活动时间 + * @param string $session_id 会话ID + * @param int $time 时间戳 + * @return array + */ + public function updateLastActiveTime($session_id, $time = null) + { + $time = $time ?? time(); + return $this->updateSession(['session_id' => $session_id], ['last_active_time' => $time]); + } + + /** + * 获取用户的活跃会话数 + * @param int $user_id 用户ID + * @param int $site_id 站点ID + * @param int $days 天数 + * @return array + */ + public function getUserActiveSessionsCount($user_id, $site_id, $days = 7) + { + try { + $start_time = time() - ($days * 24 * 3600); + + $count = Db::name($this->name) + ->where('user_id', $user_id) + ->where('site_id', $site_id) + ->where('last_active_time', '>=', $start_time) + ->count(); + + return $this->success(['count' => $count]); + } catch (\Exception $e) { + log_write('Get active sessions count error: ' . $e->getMessage(), 'error', $this->log_file); + return $this->error('', 'GET_ACTIVE_SESSIONS_COUNT_ERROR'); + } + } + + /** + * 清理过期会话 + * @param int $days 天数 + * @return array + */ + public function cleanupExpiredSessions($days = 30) + { + try { + $expire_time = time() - ($days * 24 * 3600); + + // 开启事务 + Db::startTrans(); + try { + // 找出所有过期会话ID + $expired_sessions = Db::name($this->name) + ->where('last_active_time', '<', $expire_time) + ->column('session_id'); + + if (!empty($expired_sessions)) { + // 删除过期会话 + $session_result = Db::name($this->name) + ->where('session_id', 'in', $expired_sessions) + ->delete(); + + // 删除相关聊天历史 + $history_result = Db::name('shop_ai_chat_history') + ->where('session_id', 'in', $expired_sessions) + ->delete(); + } + + Db::commit(); + return $this->success([ + 'expired_count' => count($expired_sessions), + 'session_deleted' => $session_result ?? 0, + 'history_deleted' => $history_result ?? 0 + ]); + } catch (\Exception $e) { + Db::rollback(); + throw $e; + } + } catch (\Exception $e) { + log_write('Cleanup expired sessions error: ' . $e->getMessage(), 'error', $this->log_file); + return $this->error('', 'CLEANUP_EXPIRED_SESSIONS_ERROR'); + } + } +} \ No newline at end of file diff --git a/src/app/model/stat/StatShop.php b/src/app/model/stat/StatShop.php index 8771fa05f..fe0aabfb7 100644 --- a/src/app/model/stat/StatShop.php +++ b/src/app/model/stat/StatShop.php @@ -30,34 +30,53 @@ class StatShop extends BaseModel */ public function addShopStat($data) { - $carbon = Carbon::now(); - $dir = __UPLOAD__.'/stat/stat_shop/'; - if (!is_dir($dir) && !mkdir($dir, 0777, true) && !is_dir($dir)) { - return $this->error(sprintf('Directory "%s" was not created', $dir)); - } - $filename = $dir.$carbon->year.'_'.$carbon->month.'_'.$carbon->day.'_'.$carbon->second.'_'.unique_random().'.json'; - $stat_extend = new Stat($filename, 'stat_shop',$data['site_id']); - $stat_extend->handleData($data);//写入文件 + log_write('店铺按天新统计数据开始添加', 'debug'); + try{ + $carbon = Carbon::now(); + $dir = __UPLOAD__.'/stat/stat_shop/'; + if (!is_dir($dir) && !mkdir($dir, 0777, true) && !is_dir($dir)) { + return $this->error(sprintf('Directory "%s" was not created', $dir)); + } + $filename = $dir.$carbon->year.'_'.$carbon->month.'_'.$carbon->day.'_'.$carbon->second.'_'.unique_random().'.json'; + $stat_extend = new Stat($filename, 'stat_shop',$data['site_id']); + $stat_extend->handleData($data);//写入文件 - //增加当天时统计 - $this->addShopHourStat($data, $carbon); + //增加当天时统计 + $this->addShopHourStat($data, $carbon); + }catch (\Exception $e){ + log_write('店铺按天新统计数据添加失败:' . $e->getMessage(), 'error'); + return $this->error('店铺按天新统计数据添加失败'); + } + + log_write('店铺按天新统计数据已添加', 'debug'); return $this->success(); } + /** + * 从stat_shop目录下读取所有文件,将数据处理后,写入数据表中 + * 处理完每个文件后,删除文件 + */ public function cronShopStat() { + log_write('店铺按天统计数据开始处理', 'debug'); $path = __UPLOAD__.'/stat/stat_shop'; - if(!is_dir($path)) return; + if(!is_dir($path)) { + log_write('店铺按天统计数据处理失败:目录不存在', 'error'); + return; + } $result = $this->scanFile($path); - if(empty($result)) return; + if(empty($result)) { + log_write('店铺按天统计数据处理失败:目录下无文件', 'error'); + return; + } try { $json_array = []; foreach ($result as $key => $val){ $stat_extend = new Stat($path.'/'.$val, 'stat_shop'); $json_array[] = $stat_extend->load(); - unlink($path.'/'.$val); + unlink($path.'/'.$val); // 处理完文件后,删除文件 } @@ -83,8 +102,10 @@ class StatShop extends BaseModel foreach ($data_array as $json_k => $json_v){ $system_stat->addStatShopModel($json_v); } + log_write('店铺按天统计数据处理成功', 'debug'); } catch (\Exception $e) { - + log_write('店铺按天统计数据处理失败:' . $e->getMessage(), 'error'); + return $this->error('店铺按天统计数据处理失败'); } } @@ -97,23 +118,37 @@ class StatShop extends BaseModel */ public function addShopHourStat($data, $carbon) { - $dir = __UPLOAD__.'/stat/stat_shop_hour/'; - if (!is_dir($dir) && !mkdir($dir, 0777, true) && !is_dir($dir)) { - return $this->error(sprintf('Directory "%s" was not created', $dir)); - } - $filename = $dir.$carbon->year.'_'.$carbon->month.'_'.$carbon->day.'_'.$carbon->hour.'_'.$carbon->second.'_'.unique_random().'.json'; - $stat_extend = new Stat($filename, 'stat_shop_hour',$data['site_id']); - $stat_extend->handleData($data);//写入文件 + log_write('店铺按小时新统计数据开始添加', 'debug'); + try{ + $dir = __UPLOAD__.'/stat/stat_shop_hour/'; + if (!is_dir($dir) && !mkdir($dir, 0777, true) && !is_dir($dir)) { + return $this->error(sprintf('Directory "%s" was not created', $dir)); + } + $filename = $dir.$carbon->year.'_'.$carbon->month.'_'.$carbon->day.'_'.$carbon->hour.'_'.$carbon->second.'_'.unique_random().'.json'; + $stat_extend = new Stat($filename, 'stat_shop_hour',$data['site_id']); + $stat_extend->handleData($data);//写入文件 + log_write('店铺按小时新统计数据已添加', 'debug'); + }catch (\Exception $e){ + log_write('店铺按小时新统计数据添加失败:' . $e->getMessage(), 'error'); + return $this->error('店铺按小时新统计数据添加失败'); + } return $this->success(); } public function cronShopStatHour() { + log_write('系统计划任务开始执行店铺按小时统计'); $path = __UPLOAD__.'/stat/stat_shop_hour'; - if(!is_dir($path)) return; + if(!is_dir($path)) { + log_write('系统计划任务执行店铺按小时统计异常:目录不存在' . $path . ',请检查目录是否存在'); + return; + } $result = $this->scanFile($path); - if(empty($result)) return; + if(empty($result)) { + log_write('系统计划任务执行店铺按小时统计异常:目录下无文件' . $path . ',请检查是否有文件存在'); + return; + } try { $json_array = []; @@ -145,8 +180,9 @@ class StatShop extends BaseModel foreach ($json_array as $json_k => $json_v){ $system_stat->addStatShopHourModel($json_v); } + log_write('系统计划任务执行店铺按小时统计完成'); } catch (\Exception $e) { - + log_write('系统计划任务执行店铺按小时统计异常:'.$e->getMessage()); } } diff --git a/src/app/model/system/Cron.php b/src/app/model/system/Cron.php index 96be87699..bd53fc8e1 100644 --- a/src/app/model/system/Cron.php +++ b/src/app/model/system/Cron.php @@ -1,4 +1,5 @@ time() ]; $res = model('cron')->add($data); + log_write('添加计划任务:' . json_encode($data), 'debug'); return $this->success($res); } @@ -59,87 +61,98 @@ class Cron extends BaseModel */ public function execute($type = 'default') { - if(config('cron.default') != $type) + log_write('计划任务开始执行', 'debug'); + if (config('cron.default') != $type) { + log_write('计划任务方式不匹配<不能执行/model/system/Cron/execute>:' . config('cron.default') . ' != ' . $type, 'debug'); return true; + } - Log::write('计划任务方式'.$type); - //写入计划任务标记运行 - $this->writeSchedule(); - $system_config_model = new SystemConfig(); - $config = $system_config_model->getSystemConfig()[ 'data' ] ?? []; - $is_open_queue = $config[ 'is_open_queue' ] ?? 0; - $query_execute_time = $is_open_queue == 1 ? time() + 60 : time(); - $list = model('cron')->getList([ [ 'execute_time', '<=', $query_execute_time ] ]); - $now_time = time(); - if (!empty($list)) { - foreach ($list as $k => $v) { - $event_res = checkQueue($v, function($params) { - //加入消息队列 - $job_handler_classname = 'Cronexecute'; - try { - if ($params[ 'execute_time' ] <= time()) { - Queue::push($job_handler_classname, $params); - } else { - Queue::later($params[ 'execute_time' ] - time(), $job_handler_classname, $params); + log_write('当前执行方式:' . $type, 'debug'); + + try { + //写入计划任务标记运行 + $this->writeSchedule(); + $system_config_model = new SystemConfig(); + $config = $system_config_model->getSystemConfig()['data'] ?? []; + $is_open_queue = $config['is_open_queue'] ?? 0; + $query_execute_time = $is_open_queue == 1 ? time() + 60 : time(); + $list = model('cron')->getList([['execute_time', '<=', $query_execute_time]]); + $now_time = time(); + log_write('计划任务开始执行,查询计划任务列表', 'debug'); + + if (!empty($list)) { + foreach ($list as $k => $v) { + $event_res = checkQueue($v, function ($params) { + //加入消息队列 + $job_handler_classname = 'Cronexecute'; + try { + if ($params['execute_time'] <= time()) { + Queue::push($job_handler_classname, $params); + } else { + Queue::later($params['execute_time'] - time(), $job_handler_classname, $params); + } + } catch (\Exception $e) { + $res = $this->error($e->getMessage(), $e->getMessage()); } - } catch (\Exception $e) { - $res = $this->error($e->getMessage(), $e->getMessage()); - } - return $res ?? $this->success(); - }, function($params) { - try { - $res = event($params[ 'event' ], [ 'relate_id' => $params[ 'relate_id' ] ]); - } catch (\Exception $e) { - $res = $this->error($e->getMessage(), $e->getMessage()); + return $res ?? $this->success(); + }, function ($params) { + try { + log_write('调用事件名称:' . $params['name'], 'debug'); + $res = event($params['event'], ['relate_id' => $params['relate_id']]); + } catch (\Exception $e) { + $res = $this->error($e->getMessage(), $e->getMessage()); + } + + $data_log = [ + 'name' => $params['name'], + 'event' => $params['event'], + 'relate_id' => $params['relate_id'], + 'message' => json_encode($res) + ]; + $this->addCronLog($data_log); + return $res; + }); + + //定义最新的执行时间或错误 + $event_code = $event_res['code'] ?? 0; + if ($event_code < 0) { + Log::write($event_res); + continue; } - $data_log = [ - 'name' => $params[ 'name' ], - 'event' => $params[ 'event' ], - 'relate_id' => $params[ 'relate_id' ], - 'message' => json_encode($res) - ]; - $this->addCronLog($data_log); - return $res; - }); + //循环任务 + if ($v['type'] == 2) { + $period = $v['period'] == 0 ? 1 : $v['period']; + switch ($v['period_type']) { + case 0: //分 - //定义最新的执行时间或错误 - $event_code = $event_res[ 'code' ] ?? 0; - if ($event_code < 0) { - Log::write($event_res); - continue; - } + $execute_time = $now_time + $period * 60; + break; + case 1: //天 - //循环任务 - if ($v[ 'type' ] == 2) { - $period = $v[ 'period' ] == 0 ? 1 : $v[ 'period' ]; - switch ( $v[ 'period_type' ] ) { - case 0://分 + $execute_time = strtotime('+' . $period . 'day', $v['execute_time']); + break; + case 2: //周 - $execute_time = $now_time + $period * 60; - break; - case 1://天 + $execute_time = strtotime('+' . $period . 'week', $v['execute_time']); + break; + case 3: //月 - $execute_time = strtotime('+' . $period . 'day', $v[ 'execute_time' ]); - break; - case 2://周 - - $execute_time = strtotime('+' . $period . 'week', $v[ 'execute_time' ]); - break; - case 3://月 - - $execute_time = strtotime('+' . $period . 'month', $v[ 'execute_time' ]); - break; + $execute_time = strtotime('+' . $period . 'month', $v['execute_time']); + break; + } + model('cron')->update(['execute_time' => $execute_time], [['id', '=', $v['id']]]); + } else { + model('cron')->delete([['id', '=', $v['id']]]); } - model('cron')->update([ 'execute_time' => $execute_time ], [ [ 'id', '=', $v[ 'id' ] ] ]); - - } else { - model('cron')->delete([ [ 'id', '=', $v[ 'id' ] ] ]); } } + // $this->setCron(); + return true; + } catch (\Exception $e) { + log_write('计划任务执行异常:' . $e->getMessage(), 'debug'); + return true; } -// $this->setCron(); - return true; } /** @@ -150,8 +163,8 @@ class Cron extends BaseModel public function addCronLog($data) { // 日常不需要添加,调试使用 -// $data[ 'execute_time' ] = time(); -// model('cron_log')->add($data); + $data['execute_time'] = time(); + model('cron_log')->add($data); return $this->success(); } @@ -167,22 +180,22 @@ class Cron extends BaseModel if (empty($cron_cache)) { //todo 不存在缓存标识,并不视为任务停止 //创建缓存标识,当前时间填充 - Cache::set('cron_cache', [ 'time' => $now_time, 'error' => '' ]); + Cache::set('cron_cache', ['time' => $now_time, 'error' => '']); } else { - $time = $cron_cache[ 'time' ]; - $error = $cron_cache[ 'error' ] ?? ''; - $attempts = $cron_cache[ 'attempts' ] ?? 0;//尝试次数 - if (!empty($error) || ( $now_time - $time ) > $diff) { + $time = $cron_cache['time']; + $error = $cron_cache['error'] ?? ''; + $attempts = $cron_cache['attempts'] ?? 0; //尝试次数 + if (!empty($error) || ($now_time - $time) > $diff) { $message = '自动任务已停止'; if (!empty($error)) { $message .= ',停止原因:' . $error; } else { $system_config_model = new \app\model\system\SystemConfig(); - $config = $system_config_model->getSystemConfig()[ 'data' ] ?? []; - $is_open_queue = $config[ 'is_open_queue' ] ?? 0; - if (!$is_open_queue) {//如果不是消息队列的话,可以尝试异步调用一下 + $config = $system_config_model->getSystemConfig()['data'] ?? []; + $is_open_queue = $config['is_open_queue'] ?? 0; + if (!$is_open_queue) { //如果不是消息队列的话,可以尝试异步调用一下 if ($attempts < 1) { - Cache::set('cron_cache', [ 'time' => $now_time, 'error' => '', 'attempts' => 1 ]); + Cache::set('cron_cache', ['time' => $now_time, 'error' => '', 'attempts' => 1]); $url = url('cron/task/execute'); http($url, 1); return $this->success(); @@ -194,10 +207,8 @@ class Cron extends BaseModel //判断任务是 消息队列自动任务,还是默认睡眠sleep自动任务 return $this->error([], $message); } - } return $this->success(); - } /** @@ -211,14 +222,14 @@ class Cron extends BaseModel if (empty($cron_cache)) { $cron_cache = []; } -// $code = $params['code'] ?? 0; -// if($code < 0){ -// $error = $params['message'] ?? '位置的错误'; -// $cron_cache['error'] = $error; -// } + // $code = $params['code'] ?? 0; + // if($code < 0){ + // $error = $params['message'] ?? '位置的错误'; + // $cron_cache['error'] = $error; + // } - $cron_cache[ 'time' ] = time(); - $cron_cache[ 'attempts' ] = 0; + $cron_cache['time'] = time(); + $cron_cache['attempts'] = 0; Cache::set('cron_cache', $cron_cache); return $this->success(); } @@ -229,27 +240,35 @@ class Cron extends BaseModel */ public function checkSchedule() { - $file = root_path('runtime') . '.schedule'; - if (file_exists($file)) { - $time = file_get_contents($file); - if (!empty($time) && abs($time - time()) < 90) { - return $this->success(); + try { + $file = root_path('runtime') . '.schedule'; + if (file_exists($file)) { + $time = file_get_contents($file); + if (!empty($time) && abs($time - time()) < 90) { + return $this->success(); + } } + + $remark = 'Cron计划任务已停止!当前启动的任务方式:' . ScheduleDict::getType(config('cron.default')) . '。'; + $error = self::getError(config('cron.default')); + if (!empty($error)) { + $remark .= json_encode($error); + } + log_write('Cron计划任务校验计划任务是否正常运行,计划任务异常,异常信息:' . json_encode($error) . ',文件路径:' . $file, 'warning'); + return $this->error([], $remark); + } catch (\Exception $e) { + log_write('Cron计划任务校验计划任务是否正常运行异常:' . $e->getMessage() . ',异常行:' . $e->getLine() . ',文件路径:' . $file, 'error'); + return $this->error([], '计划任务校验计划任务是否正常运行异常:' . $e->getMessage()); } - $remark = '计划任务已停止!当前启动的任务方式:'.ScheduleDict::getType(config('cron.default')).'。'; - $error = self::getError(config('cron.default')); - if(!empty($error)){ - $remark .= $error; - } - return $this->error([], $remark); } /** * 写入校验计划任务 * @return true */ - public function writeSchedule(){ - $file = root_path('runtime').'.schedule'; + public function writeSchedule() + { + $file = root_path('runtime') . '.schedule'; file_put_contents($file, time()); return true; } @@ -260,7 +279,8 @@ class Cron extends BaseModel * @param $error * @return true */ - public static function setError($type, $error = ''){ + public static function setError($type, $error = '') + { Cache::set('cron_error', [$type => $error]); return true; } @@ -270,10 +290,11 @@ class Cron extends BaseModel * @param $type * @return mixed */ - public static function getError($type = ''){ + public static function getError($type = '') + { $error = Cache::get('cron_error'); - if(!empty($type)) + if (!empty($type)) return $error; return $error[$type]; } -} \ No newline at end of file +} diff --git a/src/app/model/web/Config.php b/src/app/model/web/Config.php index 2c61d6a86..f9df67224 100644 --- a/src/app/model/web/Config.php +++ b/src/app/model/web/Config.php @@ -1,4 +1,5 @@ '清除', 'icon' => 'public/static/img/cache/template.png' ], - /* [ + /* [ 'name' => '刷新菜单', 'desc' => '新增/修改插件菜单后,需要刷新插件菜单', 'key' => 'menu_cache', @@ -60,7 +61,7 @@ class Config extends BaseModel public function setCaptchaConfig($data, $site_id = 1, $app_module = 'shop') { $config = new ConfigModel(); - $res = $config->setConfig($data, '验证码设置', 1, [ [ 'site_id', '=', $site_id ], [ 'app_module', '=', $app_module ], [ 'config_key', '=', 'CAPTCHA_CONFIG' ] ]); + $res = $config->setConfig($data, '验证码设置', 1, [['site_id', '=', $site_id], ['app_module', '=', $app_module], ['config_key', '=', 'CAPTCHA_CONFIG']]); return $res; } @@ -73,25 +74,38 @@ class Config extends BaseModel public function getCaptchaConfig($site_id = 1, $app_module = 'shop') { $config = new ConfigModel(); - $res = $config->getConfig([ [ 'site_id', '=', $site_id ], [ 'app_module', '=', $app_module ], [ 'config_key', '=', 'CAPTCHA_CONFIG' ] ]); + $res = $config->getConfig([['site_id', '=', $site_id], ['app_module', '=', $app_module], ['config_key', '=', 'CAPTCHA_CONFIG']]); - if (empty($res[ 'data' ][ 'value' ])) { - $res[ 'data' ][ 'value' ] = [ + if (empty($res['data']['value'])) { + $res['data']['value'] = [ 'shop_login' => 1, 'shop_reception_login' => 1, 'shop_reception_register' => 1 ]; } else { - if (isset($res[ 'data' ][ 'value' ][ 'shop_reception_login' ]) === false) { - $res[ 'data' ][ 'value' ][ 'shop_reception_login' ] = 1; + if (isset($res['data']['value']['shop_reception_login']) === false) { + $res['data']['value']['shop_reception_login'] = 1; } - if (isset($res[ 'data' ][ 'value' ][ 'shop_reception_register' ]) === false) { - $res[ 'data' ][ 'value' ][ 'shop_reception_register' ] = 1; + if (isset($res['data']['value']['shop_reception_register']) === false) { + $res['data']['value']['shop_reception_register'] = 1; } } return $res; } + /** + * 获取支持的应用模块 + * @return array + */ + public function getSupportAppModules() + { + return [ + ['value' => 'shop', 'label' => '商家'], + ['value' => 'h5', 'label' => 'H5轻应用'], + ['value' => 'weixin', 'label' => '微信小程序'], + ]; + } + /** * 默认图上传配置 * @param $data @@ -101,31 +115,31 @@ class Config extends BaseModel */ public function setDefaultImg($data, $site_id = 0, $app_module = 'shop') { - $config_info = $this->getDefaultImg($site_id, $app_module)[ 'data' ][ 'value' ]; + $config_info = $this->getDefaultImg($site_id, $app_module)['data']['value']; if (!empty($config_info)) { $upload_model = new Upload(); - if ($data[ 'goods' ] && $config_info[ 'goods' ] && $data[ 'goods' ] != $config_info[ 'goods' ]) { - $upload_model->deletePic($config_info[ 'goods' ], $site_id); + if ($data['goods'] && $config_info['goods'] && $data['goods'] != $config_info['goods']) { + $upload_model->deletePic($config_info['goods'], $site_id); } - if ($data[ 'head' ] && $config_info[ 'head' ] && $data[ 'head' ] != $config_info[ 'head' ]) { - $upload_model->deletePic($config_info[ 'head' ], $site_id); + if ($data['head'] && $config_info['head'] && $data['head'] != $config_info['head']) { + $upload_model->deletePic($config_info['head'], $site_id); } - if ($data[ 'store' ] && $config_info[ 'store' ] && $data[ 'store' ] != $config_info[ 'store' ]) { - $upload_model->deletePic($config_info[ 'store' ], $site_id); + if ($data['store'] && $config_info['store'] && $data['store'] != $config_info['store']) { + $upload_model->deletePic($config_info['store'], $site_id); } - if ($data[ 'article' ] && $config_info[ 'article' ] && $data[ 'article' ] != $config_info[ 'article' ]) { - $upload_model->deletePic($config_info[ 'article' ], $site_id); + if ($data['article'] && $config_info['article'] && $data['article'] != $config_info['article']) { + $upload_model->deletePic($config_info['article'], $site_id); } - if ($data[ 'kefu' ] && $config_info[ 'kefu' ] && $data[ 'kefu' ] != $config_info[ 'kefu' ]) { - $upload_model->deletePic($config_info[ 'kefu' ], $site_id); + if ($data['kefu'] && $config_info['kefu'] && $data['kefu'] != $config_info['kefu']) { + $upload_model->deletePic($config_info['kefu'], $site_id); } - if ($data[ 'phone' ] && $config_info[ 'phone' ] && $data[ 'phone' ] != $config_info[ 'phone' ]) { - $upload_model->deletePic($config_info[ 'phone' ], $site_id); + if ($data['phone'] && $config_info['phone'] && $data['phone'] != $config_info['phone']) { + $upload_model->deletePic($config_info['phone'], $site_id); } } $config = new ConfigModel(); - $res = $config->setConfig($data, '默认图设置', 1, [ [ 'site_id', '=', $site_id ], [ 'app_module', '=', $app_module ], [ 'config_key', '=', 'DEFAULT_IMAGE' ] ]); + $res = $config->setConfig($data, '默认图设置', 1, [['site_id', '=', $site_id], ['app_module', '=', $app_module], ['config_key', '=', 'DEFAULT_IMAGE']]); return $res; } @@ -138,9 +152,9 @@ class Config extends BaseModel public function getDefaultImg($site_id, $app_model = 'shop') { $config = new ConfigModel(); - $res = $config->getConfig([ [ 'site_id', '=', $site_id ], [ 'app_module', '=', $app_model ], [ 'config_key', '=', 'DEFAULT_IMAGE' ] ]); - if (empty($res[ 'data' ][ 'value' ])) { - $res[ 'data' ][ 'value' ] = [ + $res = $config->getConfig([['site_id', '=', $site_id], ['app_module', '=', $app_model], ['config_key', '=', 'DEFAULT_IMAGE']]); + if (empty($res['data']['value'])) { + $res['data']['value'] = [ 'goods' => 'public/static/img/default_img/square.png', 'head' => 'public/static/img/default_img/head.png', 'store' => 'public/static/img/default_img/store.png', @@ -150,14 +164,14 @@ class Config extends BaseModel ]; } - if (empty($res[ 'data' ][ 'value' ][ 'head' ])) { - $res[ 'data' ][ 'value' ][ 'head' ] = 'public/static/img/default_img/head.png'; + if (empty($res['data']['value']['head'])) { + $res['data']['value']['head'] = 'public/static/img/default_img/head.png'; } - if (empty($res[ 'data' ][ 'value' ][ 'article' ])) { - $res[ 'data' ][ 'value' ][ 'article' ] = 'public/static/img/default_img/article.png'; + if (empty($res['data']['value']['article'])) { + $res['data']['value']['article'] = 'public/static/img/default_img/article.png'; } - if (empty($res[ 'data' ][ 'value' ][ 'store' ])) { - $res[ 'data' ][ 'value' ][ 'store' ] = 'public/static/img/default_img/store.png'; + if (empty($res['data']['value']['store'])) { + $res['data']['value']['store'] = 'public/static/img/default_img/store.png'; } return $res; } @@ -173,7 +187,7 @@ class Config extends BaseModel public function setCopyright($data, $site_id = 1, $app_model = 'shop') { $config = new ConfigModel(); - $res = $config->setConfig($data, '版权设置', 1, [ [ 'site_id', '=', $site_id ], [ 'app_module', '=', $app_model ], [ 'config_key', '=', 'COPYRIGHT' ] ]); + $res = $config->setConfig($data, '版权设置', 1, [['site_id', '=', $site_id], ['app_module', '=', $app_model], ['config_key', '=', 'COPYRIGHT']]); return $res; } @@ -186,16 +200,16 @@ class Config extends BaseModel public function getCopyright($site_id = 1, $app_module = 'shop') { $config = new ConfigModel(); - $res = $config->getConfig([ [ 'site_id', '=', $site_id ], [ 'app_module', '=', $app_module ], [ 'config_key', '=', 'COPYRIGHT' ] ]); + $res = $config->getConfig([['site_id', '=', $site_id], ['app_module', '=', $app_module], ['config_key', '=', 'COPYRIGHT']]); $auth_info = cache('auth_info_copyright'); if (empty($auth_info)) { $upgrade_model = new Upgrade(); $auth_info = $upgrade_model->authInfo(); - cache('auth_info_copyright', $auth_info, [ 'expire' => 604800 ]); + cache('auth_info_copyright', $auth_info, ['expire' => 604800]); } - if (empty($res[ 'data' ][ 'value' ])) { - $res[ 'data' ][ 'value' ] = [ + if (empty($res['data']['value'])) { + $res['data']['value'] = [ 'logo' => '', 'company_name' => '', 'copyright_link' => '', @@ -206,16 +220,15 @@ class Config extends BaseModel 'market_supervision_url' => '' ]; } else { - if (is_null($auth_info) || $auth_info[ 'code' ] != 0) { - $res[ 'data' ][ 'value' ][ 'logo' ] = ''; - $res[ 'data' ][ 'value' ][ 'company_name' ] = ''; - $res[ 'data' ][ 'value' ][ 'copyright_link' ] = ''; - $res[ 'data' ][ 'value' ][ 'copyright_desc' ] = ''; + if (is_null($auth_info) || $auth_info['code'] != 0) { + $res['data']['value']['logo'] = ''; + $res['data']['value']['company_name'] = ''; + $res['data']['value']['copyright_link'] = ''; + $res['data']['value']['copyright_desc'] = ''; } - } // 检查是否授权 - $res[ 'data' ][ 'value' ][ 'auth' ] = true; + $res['data']['value']['auth'] = true; return $res; } @@ -229,7 +242,7 @@ class Config extends BaseModel public function setAuth($data, $site_id = 1, $app_model = 'shop') { $config = new ConfigModel(); - $res = $config->setConfig($data, '授权设置', 1, [ [ 'site_id', '=', $site_id ], [ 'app_module', '=', $app_model ], [ 'config_key', '=', 'AUTH' ] ]); + $res = $config->setConfig($data, '授权设置', 1, [['site_id', '=', $site_id], ['app_module', '=', $app_model], ['config_key', '=', 'AUTH']]); return $res; } @@ -240,9 +253,9 @@ class Config extends BaseModel public function getAuth($site_id = 1, $app_module = 'shop') { $config = new ConfigModel(); - $res = $config->getConfig([ [ 'site_id', '=', $site_id ], [ 'app_module', '=', $app_module ], [ 'config_key', '=', 'AUTH' ] ]); - if (empty($res[ 'data' ][ 'value' ])) { - $res[ 'data' ][ 'value' ] = [ + $res = $config->getConfig([['site_id', '=', $site_id], ['app_module', '=', $app_module], ['config_key', '=', 'AUTH']]); + if (empty($res['data']['value'])) { + $res['data']['value'] = [ 'code' => '', ]; } @@ -259,11 +272,11 @@ class Config extends BaseModel public function setMapConfig($data, $site_id, $app_model = 'shop') { $config = new ConfigModel(); - $res = $config->setConfig($data, '地图设置', 1, [ [ 'site_id', '=', $site_id ], [ 'app_module', '=', $app_model ], [ 'config_key', '=', 'MAP_CONFIG' ] ]); + $res = $config->setConfig($data, '地图设置', 1, [['site_id', '=', $site_id], ['app_module', '=', $app_model], ['config_key', '=', 'MAP_CONFIG']]); return $res; } - /** + /** * 获取地图设置 * @param int $site_id * @param string $app_module @@ -273,16 +286,16 @@ class Config extends BaseModel { $config = new ConfigModel(); $res = $config->getConfig([['site_id', '=', $site_id], ['app_module', '=', $app_module], ['config_key', '=', 'MAP_CONFIG']]); - if (empty($res[ 'data' ][ 'value' ])) { - $res[ 'data' ][ 'value' ] = [ - 'tencent_map_key' => '2PJBZ-A263Q-SED5B-4SAAB-HCUQ5-DUFHE',//默认一个地图TB5BZ-FBRRX-2RJ4C-76SZY-TYQ3H-F4BFC + if (empty($res['data']['value'])) { + $res['data']['value'] = [ + 'tencent_map_key' => '2PJBZ-A263Q-SED5B-4SAAB-HCUQ5-DUFHE', //默认一个地图TB5BZ-FBRRX-2RJ4C-76SZY-TYQ3H-F4BFC 'wap_is_open' => 1, // 手机端是否开启定位 'wap_valid_time' => 5 // 手机端定位有效期/分钟,过期后将重新获取定位信息,0为不过期 ]; } - $res[ 'data' ][ 'value' ][ 'wap_is_open' ] = $res[ 'data' ][ 'value' ][ 'wap_is_open' ] ?? 1; - $res[ 'data' ][ 'value' ][ 'wap_valid_time' ] = $res[ 'data' ][ 'value' ][ 'wap_valid_time' ] ?? 5; - $res[ 'data' ][ 'value' ]['tencent_map_key'] = '2PJBZ-A263Q-SED5B-4SAAB-HCUQ5-DUFHE'; + $res['data']['value']['wap_is_open'] = $res['data']['value']['wap_is_open'] ?? 1; + $res['data']['value']['wap_valid_time'] = $res['data']['value']['wap_valid_time'] ?? 5; + $res['data']['value']['tencent_map_key'] = '2PJBZ-A263Q-SED5B-4SAAB-HCUQ5-DUFHE'; return $res; } @@ -299,193 +312,285 @@ class Config extends BaseModel } /** - * 设置AI第三平台提供的配置信息 - * @$data - * @param int $site_id - * @param string $app_module + * 通用AI相关配置获取方法 + * @param int $site_id 站点ID, 默认为-1, 业务站点uniacid值与site_id保持一致 + * @param string $app_module 应用模块, 默认为*,表示所有应用模块都生效 + * @param string $config_key 配置键, 默认为空字符串, 表示获取通用配置信息 + * @param array $common_config 通用配置信息, 默认为空数组 * @return array */ - public function setAIPlatformConfig($data, $site_id = 1, $app_module = 'shop') + private function _getAIConfig($site_id = -1, $app_module = '*', $config_key ='', $common_config = []) { + // 应用模块配置获取 + try { + // 站点ID为-1时,获取通用配置信息 + if ($site_id == -1) { + return $this->error('', 'MISSING_SITE_ID'); + } + + // 配置键为空字符串, 表示获取通用配置信息 + if (empty($config_key)) { + return $this->error('', 'MISSING_CONFIG_KEY'); + } + + $config = new ConfigModel(); + $support_app_modules = $this->getSupportAppModules(); + + // 检查应用模块是否支持, 如果是*,则表示所有应用模块都生效 + if ($app_module != '*') { + if (!in_array($app_module, array_column($support_app_modules, 'value'))) { + return $this->error('', 'APP_MODULE_NOT_SUPPORTED'); + } + } + + // 如果是*,则表示所有应用模块都生效, 遍历所有应用模块,根据应用模块获取配置信息, 为每一种应用模块生成独立的配置信息,每一种类型的应用模块,对应自己的配置信息,然后将所有的配置信息返回 + $app_module_config = []; + foreach ($support_app_modules as $item) { + $app_module = $item['value']; + + // 检查应用模块是否支持 + try { + $res = $config->getConfig([['site_id', '=', $site_id], ['app_module', '=', $app_module], ['config_key', '=', $config_key]]); + if (empty($res['data']['value'])) { + $res['data']['value'] = $common_config; + } + } catch (\Exception $e) { + $res['data']['value'] = $common_config; + } + + $app_module_config[$app_module] = $res['data']['value']; + } + $res['data']['value'] = $app_module_config; + + return $res; + } catch (\Exception $e) { + return $this->error('', 'GET_CONFIG_ERROR' . $e->getMessage()); + } + } + + /** + * 通用AI相关配置设置方法 + * @param $data 配置信息 + * @param int $site_id 站点ID, 默认为-1, 业务站点uniacid值与site_id保持一致 + * @param string $app_module 应用模块, 默认为空字符串 + * @param string $config_key 配置键, 默认为空字符串 + * @param string $data_desc 配置描述, 默认为空字符串 + * @return array + */ + private function _setAIConfig($data, $site_id = -1, $app_module = '', $config_key = '', $data_desc = '') + { + // 站点ID为-1时,设置通用配置信息 + if ($site_id == -1) { + return $this->error('', 'MISSING_SITE_ID'); + } + + // 应用模块为空字符串 + if (empty($app_module)) { + return $this->error('', 'MISSING_APP_MODULE'); + } + + // 配置键为空字符串 + if (empty($config_key)) { + return $this->error('', 'MISSING_CONFIG_KEY'); + } + $config = new ConfigModel(); - $res = $config->setConfig($data, 'AI第三平台提供的配置信息', 1, [ [ 'site_id', '=', $site_id ], [ 'app_module', '=', $app_module ], [ 'config_key', '=', 'AI_PLATFORM_CONFIG' ] ]); + $res = $config->setConfig($data, $data_desc, 1, [['site_id', '=', $site_id], ['app_module', '=', $app_module], ['config_key', '=', $config_key]]); return $res; } - /** - * 获取AI第三平台提供的配置信息 - * @param int $site_id - * @param string $app_module + /** + * 设置AI第三平台提供的配置信息 + * @param $data 配置信息 + * @param int $site_id 站点ID, 默认为-1, 业务站点uniacid值与site_id保持一致 + * @param string $app_module 应用模块, 默认为*, 表示所有应用模块都生效 * @return array */ - public function getAIPlatformConfig($site_id = 1, $app_module = 'shop') + public function setAIPlatformConfig($data, $site_id = -1, $app_module = '') { - $config = new ConfigModel(); - $res = $config->getConfig([ [ 'site_id', '=', $site_id ], [ 'app_module', '=', $app_module ], [ 'config_key', '=', 'AI_PLATFORM_CONFIG' ] ]); - if (empty($res[ 'data' ][ 'value' ])) { - $id = time(); - $res[ 'data' ][ 'value' ] = [ - 'default' => [ 'id' => $id, 'name'=> 'dify-demo' ], - 'list' => [ - // Dify Demo 版本, - ['id' => $id, 'name' => 'dify-demo', 'type' => 'dify', 'type_label' => 'Dify', 'desc' => 'Dify Demo 版本', 'enable' => 1, 'api_url' => 'https://api.dify.cn', 'api_key' => '', 'create_time' => time()], - ] - ]; - return $res; - } - - return $res; + return $this->_setAIConfig($data, $site_id, $app_module, 'AI_PLATFORM_CONFIG', 'AI第三平台提供的配置信息'); } /** * 设置AI智能客服配置信息 * @param $data 配置信息 - * @param int $site_id - * @param string $app_module + * @param int $site_id 站点ID, 默认为1, 业务站点uniacid值与site_id保持一致 + * @param string $app_module 应用模块, 默认为*, 表示所有应用模块都生效 * @return array */ - public function setAIAgentServicesConfig($data, $site_id = 1, $app_module = 'shop') + public function setAIAgentServicesConfig($data, $site_id = 1, $app_module = '*') { - $config = new ConfigModel(); - $res = $config->setConfig($data, 'AI配置信息', 1, [ [ 'site_id', '=', $site_id ], [ 'app_module', '=', $app_module ], [ 'config_key', '=', 'AI_AGENT_SERVICES_CONFIG' ] ]); - return $res; + return $this->_setAIConfig($data, $site_id, $app_module, 'AI_AGENT_SERVICES_CONFIG', 'AI智能客服配置信息'); + } + + /** + * 获取AI第三平台提供的通用配置信息 + * @return array + */ + private function getCommonAIPlatformConfig() + { + // 默认ID + $id = time(); + + // 通用配置 + return [ + 'default' => ['id' => $id, 'name' => 'dify-demo'], + 'list' => [ + // Dify Demo 版本, + ['id' => $id, 'name' => 'dify-demo', 'type' => 'dify', 'type_label' => 'Dify', 'desc' => 'Dify 线上Demo 版本', 'enable' => 1, 'base_url' => 'https://localhost/api/ai', 'api_key' => 'xxxxxxx', 'create_time' => time()], + ] + ]; + } + + /** + * 获取AI第三平台提供的配置信息 + * @param int $site_id 站点ID, 默认为1, 业务站点uniacid值与site_id保持一致 + * @param string $app_module 应用模块, 默认为*, 表示所有应用模块都生效 + * @return array + */ + public function getAIPlatformConfig($site_id = 1, $app_module = '*') + { + return $this->_getAIConfig($site_id, $app_module, 'AI_PLATFORM_CONFIG', $this->getCommonAIPlatformConfig()); } /** - * 获得AI智能客服配置信息 - * @param int $site_id - * @param string $app_module + * 获取AI智能客服的通用配置信息 * @return array */ - public function getAIAgentServicesConfig($site_id = 1, $app_module = 'shop') + private function getCommonAIAgentServicesConfig() { - $config = new ConfigModel(); - $res = $config->getConfig([ [ 'site_id', '=', $site_id ], [ 'app_module', '=', $app_module ], [ 'config_key', '=', 'AI_AGENT_SERVICES_CONFIG' ] ]); - if (empty($res[ 'data' ][ 'value' ])) { - $res[ 'data' ][ 'value' ] = [ - // 是否开启AI功能 + return [ + // 是否开启AI功能 + 'enable' => false, + // 用户头像url + 'user_avatar' => [ + 'en' => '', + 'zh_CN' => '', + ], + // AI客服头像url + 'ai_avatar' => [ + 'en' => '', + 'zh_CN' => '', + ], + // AI客服姓名 + 'ai_name' => [ + 'en' => '', + 'zh_CN' => '', + ], + // 欢迎语, 也称为开场白,初始化消息 + 'welcome_messages' => [ + 'en' => [], + 'zh_CN' => [], + ], + // 是否显示加载更多按钮 + 'show_load_more_button' => true, + // 最大消息数量 + 'max_messages' => 100, + // 是否启用流式响应 + 'stream_mode' => true, + // 流式响应的超时时间,单位:秒 + 'stream_timeout' => 30, + // 流式响应速度(字符/秒) + 'stream_speed' => 20, + // 是否开启支持语音输入 + 'support_voice_input' => false, + // 语音输入的提示信息 + 'voice_input_prompt' => [ + 'en' => 'Please click the microphone icon to start recording', + 'zh_CN' => '请点击麦克风图标开始录音', + ], + // 是否开启语音输入提示 + 'support_voice_input_prompt' => false, + // 输入文本内容提示 + 'text_input_prompt' => [ + 'en' => 'Please enter your problem', + 'zh_CN' => '请输入您的问题', + ], + // 输入的问题描述的字符长度限制 + 'max_char_length_for_problem' => 500, + // 是否支持显示更多工具面板 + 'tool_panel_config' => [ 'enable' => false, - // 用户头像url - 'user_avatar' => [ - 'en'=> '', - 'zh_CN' => '', + 'title' => [ + 'en' => 'More Tools', + 'zh_CN' => '更多功能', ], - // AI客服头像url - 'ai_avatar' => [ - 'en'=> '', - 'zh_CN' => '', - ], - // AI客服姓名 - 'ai_name' => [ - 'en'=> '', - 'zh_CN' => '', - ], - // 欢迎语, 也称为开场白,初始化消息 - 'welcome_messages' => [ - 'en'=> [], - 'zh_CN' => [], - ], - // 是否显示加载更多按钮 - 'show_load_more_button' => true, - // 最大消息数量 - 'max_messages' => 100, - // 是否启用流式响应 - 'stream_mode' => true, - // 流式响应的超时时间,单位:秒 - 'stream_timeout' => 30, - // 流式响应速度(字符/秒) - 'stream_speed' => 20, - // 是否开启支持语音输入 - 'support_voice_input' => false, - // 语音输入的提示信息 - 'voice_input_prompt' => [ - 'en' => 'Please click the microphone icon to start recording', - 'zh_CN' => '请点击麦克风图标开始录音', - ], - // 是否开启语音输入提示 - 'support_voice_input_prompt' => false, - // 输入文本内容提示 - 'text_input_prompt' => [ - 'en' => 'Please enter your problem', - 'zh_CN' => '请输入您的问题', - ], - // 输入的问题描述的字符长度限制 - 'max_char_length_for_problem' => 500, - // 是否支持显示更多工具面板 - 'tool_panel_config' =>[ - 'enable' => false, - 'title' => [ - 'en' => 'More Tools', - 'zh_CN' => '更多功能', + 'icon' => 'icon-more', + // 工具面板中的工具项 + 'tools' => [ + 'image' => [ + 'enable' => false, + 'name' => [ + 'en' => 'Image', + 'zh_CN' => '图片', + ], + 'description' => [ + 'en' => 'Upload image', + 'zh_CN' => '上传图片', + ], + 'icon' => 'icon-image', ], - 'icon' => 'icon-more', - // 工具面板中的工具项 - 'tools' => [ - 'image' => [ - 'enable' => false, - 'name' => [ - 'en' => 'Image', - 'zh_CN' => '图片', - ], - 'description' =>[ - 'en'=> 'Upload image', - 'zh_CN' => '上传图片', - ], - 'icon' => 'icon-image', + 'video' => [ + 'enable' => false, + 'name' => [ + 'en' => 'Video', + 'zh_CN' => '视频', ], - 'video' =>[ - 'enable' => false, - 'name' => [ - 'en' => 'Video', - 'zh_CN' => '视频', - ], - 'description' => [ - 'en'=> 'Upload video', - 'zh_CN' => '上传视频', - ], - 'icon' => 'icon-video', - ], - 'file' => [ - 'enable' => false, - 'name' => [ - 'en' => 'File', - 'zh_CN' => '文件', - ], - 'description' => [ - 'en'=> 'Upload file', - 'zh_CN' => '上传文件', - ], - 'icon' => 'icon-file', - ], - 'voice' =>[ - 'enable' => false, - 'name'=> [ - 'en' => 'Voice', - 'zh_CN' => '语音', - ], - 'description' =>[ - 'en'=> 'Voice input', - 'zh_CN' => '语音输入', - ], - 'icon'=> 'icon-voice', - ], - 'location' => [ - 'enable' => false, - 'name' => [ - 'en' => 'Location', - 'zh_CN' => '位置', - ], - 'description' => [ - 'en'=> 'Location input', - 'zh_CN' => '位置输入', - ], - 'icon'=> 'icon-location', + 'description' => [ + 'en' => 'Upload video', + 'zh_CN' => '上传视频', ], + 'icon' => 'icon-video', ], - ] - ]; - } - return $res; + 'file' => [ + 'enable' => false, + 'name' => [ + 'en' => 'File', + 'zh_CN' => '文件', + ], + 'description' => [ + 'en' => 'Upload file', + 'zh_CN' => '上传文件', + ], + 'icon' => 'icon-file', + ], + 'voice' => [ + 'enable' => false, + 'name' => [ + 'en' => 'Voice', + 'zh_CN' => '语音', + ], + 'description' => [ + 'en' => 'Voice input', + 'zh_CN' => '语音输入', + ], + 'icon' => 'icon-voice', + ], + 'location' => [ + 'enable' => false, + 'name' => [ + 'en' => 'Location', + 'zh_CN' => '位置', + ], + 'description' => [ + 'en' => 'Location input', + 'zh_CN' => '位置输入', + ], + 'icon' => 'icon-location', + ], + ], + ] + ]; + } + /** + * 获得AI智能客服配置信息 + * @param int $site_id 站点ID, 默认为1, 业务站点uniacid值与site_id保持一致 + * @param string $app_module 应用模块, 默认为*, 表示所有应用模块都生效 + * @return array + */ + public function getAIAgentServicesConfig($site_id = 1, $app_module = '*') + { + return $this->_getAIConfig($site_id, $app_module, 'AI_AGENT_SERVICES_CONFIG', $this->getCommonAIAgentServicesConfig()); } /** @@ -498,19 +603,19 @@ class Config extends BaseModel public function seth5DomainName($data, $site_id = 1, $app_module = 'shop') { $search = '/^([hH][tT]{2}[pP]:\/\/|[hH][tT]{2}[pP][sS]:\/\/)(([A-Za-z0-9-~]+)\.)+([A-Za-z0-9-~\/])+$/'; - if ($data[ 'deploy_way' ] == 'separate') { - if (!preg_match($search, $data[ 'domain_name_h5' ])) { + if ($data['deploy_way'] == 'separate') { + if (!preg_match($search, $data['domain_name_h5'])) { return $this->error('', '请输入正确的域名地址'); } } // 默认部署,更新店铺域名 - if ($data[ 'deploy_way' ] == 'default') { + if ($data['deploy_way'] == 'default') { $this->setShopDomainConfig([ 'domain_name' => __ROOT__ ], $site_id); } $config = new ConfigModel(); - $res = $config->setConfig($data, 'H5域名配置', 1, [ [ 'site_id', '=', $site_id ], [ 'app_module', '=', $app_module ], [ 'config_key', '=', 'H5_DOMAIN_NAME' ] ]); + $res = $config->setConfig($data, 'H5域名配置', 1, [['site_id', '=', $site_id], ['app_module', '=', $app_module], ['config_key', '=', 'H5_DOMAIN_NAME']]); return $res; } @@ -524,9 +629,9 @@ class Config extends BaseModel public function getH5DomainName($site_id = 1, $app_module = 'shop') { $config = new ConfigModel(); - $res = $config->getConfig([ [ 'site_id', '=', $site_id ], [ 'app_module', '=', $app_module ], [ 'config_key', '=', 'H5_DOMAIN_NAME' ] ]); - if (empty($res[ 'data' ][ 'value' ])) { - $res[ 'data' ][ 'value' ] = [ + $res = $config->getConfig([['site_id', '=', $site_id], ['app_module', '=', $app_module], ['config_key', '=', 'H5_DOMAIN_NAME']]); + if (empty($res['data']['value'])) { + $res['data']['value'] = [ 'domain_name_h5' => __ROOT__ . '/h5', 'deploy_way' => 'default' ]; @@ -544,7 +649,7 @@ class Config extends BaseModel public function setDomainJumpConfig($data, $site_id = 1, $app_module = 'shop') { $config = new ConfigModel(); - $res = $config->setConfig($data, '获取域名跳转配置', 1, [ [ 'site_id', '=', $site_id ], [ 'app_module', '=', $app_module ], [ 'config_key', '=', 'DOMAIN_JUMP_CONFIG' ] ]); + $res = $config->setConfig($data, '获取域名跳转配置', 1, [['site_id', '=', $site_id], ['app_module', '=', $app_module], ['config_key', '=', 'DOMAIN_JUMP_CONFIG']]); return $res; } @@ -558,12 +663,12 @@ class Config extends BaseModel { $config = new ConfigModel(); $res = $config->getConfig([ - [ 'site_id', '=', $site_id ], - [ 'app_module', '=', $app_module ], - [ 'config_key', '=', 'DOMAIN_JUMP_CONFIG' ] + ['site_id', '=', $site_id], + ['app_module', '=', $app_module], + ['config_key', '=', 'DOMAIN_JUMP_CONFIG'] ]); - if (empty($res[ 'data' ][ 'value' ])) { - $res[ 'data' ][ 'value' ] = [ + if (empty($res['data']['value'])) { + $res['data']['value'] = [ 'jump_type' => 3, // 1:用户前台,2:商家后台,3:引导页 ]; } @@ -580,19 +685,19 @@ class Config extends BaseModel public function setPcDomainName($data, $site_id = 1, $app_module = 'shop') { $search = '/^([hH][tT]{2}[pP]:\/\/|[hH][tT]{2}[pP][sS]:\/\/)(([A-Za-z0-9-~]+)\.)+([A-Za-z0-9-~\/])+$/'; - if ($data[ 'deploy_way' ] == 'separate') { - if (!preg_match($search, $data[ 'domain_name_pc' ])) { + if ($data['deploy_way'] == 'separate') { + if (!preg_match($search, $data['domain_name_pc'])) { return $this->error('', '请输入正确的域名地址'); } } // 默认部署,更新店铺域名 - if ($data[ 'deploy_way' ] == 'default') { + if ($data['deploy_way'] == 'default') { $this->setShopDomainConfig([ 'domain_name' => __ROOT__ ], $site_id); } $config = new ConfigModel(); - $res = $config->setConfig($data, 'PC域名配置', 1, [ [ 'site_id', '=', $site_id ], [ 'app_module', '=', $app_module ], [ 'config_key', '=', 'PC_DOMAIN_NAME' ] ]); + $res = $config->setConfig($data, 'PC域名配置', 1, [['site_id', '=', $site_id], ['app_module', '=', $app_module], ['config_key', '=', 'PC_DOMAIN_NAME']]); return $res; } @@ -605,19 +710,19 @@ class Config extends BaseModel public function getPcDomainName($site_id = 1, $app_module = 'shop') { $config = new ConfigModel(); - $res = $config->getConfig([ [ 'site_id', '=', $site_id ], [ 'app_module', '=', $app_module ], [ 'config_key', '=', 'PC_DOMAIN_NAME' ] ]); - if (empty($res[ 'data' ][ 'value' ])) { - $res[ 'data' ][ 'value' ] = [ + $res = $config->getConfig([['site_id', '=', $site_id], ['app_module', '=', $app_module], ['config_key', '=', 'PC_DOMAIN_NAME']]); + if (empty($res['data']['value'])) { + $res['data']['value'] = [ 'domain_name_pc' => __ROOT__ . '/web', 'deploy_way' => 'default' ]; } else { - if ($res[ 'data' ][ 'value' ][ 'domain_name_pc' ] == '' || empty($res[ 'data' ][ 'value' ][ 'deploy_way' ]) || $res[ 'data' ][ 'value' ][ 'deploy_way' ] == 'default') { - $res[ 'data' ][ 'value' ] = [ + if ($res['data']['value']['domain_name_pc'] == '' || empty($res['data']['value']['deploy_way']) || $res['data']['value']['deploy_way'] == 'default') { + $res['data']['value'] = [ 'domain_name_pc' => __ROOT__ . '/web' ]; } - $res[ 'data' ][ 'value' ][ 'deploy_way' ] = $res[ 'data' ][ 'value' ][ 'deploy_way' ] ?? 'default'; + $res['data']['value']['deploy_way'] = $res['data']['value']['deploy_way'] ?? 'default'; } return $res; } @@ -632,7 +737,7 @@ class Config extends BaseModel public function setHotSearchWords($data, $site_id, $app_module) { $config = new ConfigModel(); - $res = $config->setConfig($data, '商品热门搜索关键词', 1, [ [ 'site_id', '=', $site_id ], [ 'app_module', '=', $app_module ], [ 'config_key', '=', 'GOODS_HOT_SEARCH_WORDS_CONFIG' ] ]); + $res = $config->setConfig($data, '商品热门搜索关键词', 1, [['site_id', '=', $site_id], ['app_module', '=', $app_module], ['config_key', '=', 'GOODS_HOT_SEARCH_WORDS_CONFIG']]); return $res; } @@ -645,9 +750,9 @@ class Config extends BaseModel public function getHotSearchWords($site_id, $app_module) { $config = new ConfigModel(); - $res = $config->getConfig([ [ 'site_id', '=', $site_id ], [ 'app_module', '=', $app_module ], [ 'config_key', '=', 'GOODS_HOT_SEARCH_WORDS_CONFIG' ] ]); - if (empty($res[ 'data' ][ 'value' ])) { - $res[ 'data' ][ 'value' ] = [ + $res = $config->getConfig([['site_id', '=', $site_id], ['app_module', '=', $app_module], ['config_key', '=', 'GOODS_HOT_SEARCH_WORDS_CONFIG']]); + if (empty($res['data']['value'])) { + $res['data']['value'] = [ 'words' => '' ]; } @@ -664,7 +769,7 @@ class Config extends BaseModel public function setGuessYouLike($data, $site_id, $app_module) { $config = new ConfigModel(); - $res = $config->setConfig($data, '商品推荐', 1, [ [ 'site_id', '=', $site_id ], [ 'app_module', '=', $app_module ], [ 'config_key', '=', 'GOODS_GUESS_YOU_LIKE_CONFIG' ] ]); + $res = $config->setConfig($data, '商品推荐', 1, [['site_id', '=', $site_id], ['app_module', '=', $app_module], ['config_key', '=', 'GOODS_GUESS_YOU_LIKE_CONFIG']]); return $res; } @@ -677,11 +782,11 @@ class Config extends BaseModel public function getGuessYouLike($site_id, $app_module) { $config = new ConfigModel(); - $res = $config->getConfig([ [ 'site_id', '=', $site_id ], [ 'app_module', '=', $app_module ], [ 'config_key', '=', 'GOODS_GUESS_YOU_LIKE_CONFIG' ] ]); - if (empty($res[ 'data' ][ 'value' ])) { - $res[ 'data' ][ 'value' ] = [ + $res = $config->getConfig([['site_id', '=', $site_id], ['app_module', '=', $app_module], ['config_key', '=', 'GOODS_GUESS_YOU_LIKE_CONFIG']]); + if (empty($res['data']['value'])) { + $res['data']['value'] = [ 'title' => '猜你喜欢', - 'supportPage' => [ 'goods_detail', 'cart', 'collect', 'pay', 'order_detail', 'super_member', 'guafen', 'fenxiao_level' ], + 'supportPage' => ['goods_detail', 'cart', 'collect', 'pay', 'order_detail', 'super_member', 'guafen', 'fenxiao_level'], 'sources' => 'sort', 'goodsIds' => [], 'fontWeight' => false, @@ -710,7 +815,7 @@ class Config extends BaseModel ], ]; } - $res[ 'data' ][ 'value' ][ 'nameLineMode' ] = $res[ 'data' ][ 'value' ][ 'nameLineMode' ] ?? 'single'; // 商品名称,单行、多行展示 + $res['data']['value']['nameLineMode'] = $res['data']['value']['nameLineMode'] ?? 'single'; // 商品名称,单行、多行展示 return $res; } @@ -724,17 +829,17 @@ class Config extends BaseModel public function getDiyAdv($site_id, $app_module) { $config = new ConfigModel(); - $res = $config->getConfig([ [ 'site_id', '=', $site_id ], [ 'app_module', '=', $app_module ], [ 'config_key', '=', 'DIY_STARTADV' ] ]); - if (empty($res[ 'data' ][ 'value' ])){ - $res[ 'data' ][ 'value' ] = [ - 'list' =>[ + $res = $config->getConfig([['site_id', '=', $site_id], ['app_module', '=', $app_module], ['config_key', '=', 'DIY_STARTADV']]); + if (empty($res['data']['value'])) { + $res['data']['value'] = [ + 'list' => [ [ - 'title'=>'启动广告', - 'link'=>[ - 'name'=>'' + 'title' => '启动广告', + 'link' => [ + 'name' => '' ], - 'iconType'=>'img', - 'imageUrl'=>"public/static/ext/diyview/img/preview/advs_default.png" + 'iconType' => 'img', + 'imageUrl' => "public/static/ext/diyview/img/preview/advs_default.png" ] ], 'advtype' => 1, @@ -754,12 +859,12 @@ class Config extends BaseModel public function setDiyAdv($data, $site_id, $app_module) { $config = new ConfigModel(); - $res = $config->setConfig($data, '启动广告', 1, [ [ 'site_id', '=', $site_id ], [ 'app_module', '=', $app_module ], [ 'config_key', '=', 'DIY_STARTADV' ] ]); + $res = $config->setConfig($data, '启动广告', 1, [['site_id', '=', $site_id], ['app_module', '=', $app_module], ['config_key', '=', 'DIY_STARTADV']]); return $res; } - /** + /** * 获取VR * @param $site_id * @param $app_module @@ -768,14 +873,14 @@ class Config extends BaseModel public function getDiyVr($site_id, $app_module) { $config = new ConfigModel(); - $res = $config->getConfig([ [ 'site_id', '=', $site_id ], [ 'app_module', '=', $app_module ], [ 'config_key', '=', 'DIY_VR' ] ]); - if (empty($res[ 'data' ][ 'value' ])){ - $res[ 'data' ][ 'value' ] = [ + $res = $config->getConfig([['site_id', '=', $site_id], ['app_module', '=', $app_module], ['config_key', '=', 'DIY_VR']]); + if (empty($res['data']['value'])) { + $res['data']['value'] = [ 'title' => '工厂展示', 'url' => 'https://baidu.com', ]; } -// $res[ 'data' ][ 'value' ][ 'nameLineMode' ] = $res[ 'data' ][ 'value' ][ 'nameLineMode' ] ?? 'single'; // 商品名称,单行、多行展示 + // $res[ 'data' ][ 'value' ][ 'nameLineMode' ] = $res[ 'data' ][ 'value' ][ 'nameLineMode' ] ?? 'single'; // 商品名称,单行、多行展示 return $res; } @@ -789,7 +894,7 @@ class Config extends BaseModel public function setDiyVr($data, $site_id, $app_module) { $config = new ConfigModel(); - $res = $config->setConfig($data, 'VR展示', 1, [ [ 'site_id', '=', $site_id ], [ 'app_module', '=', $app_module ], [ 'config_key', '=', 'DIY_VR' ] ]); + $res = $config->setConfig($data, 'VR展示', 1, [['site_id', '=', $site_id], ['app_module', '=', $app_module], ['config_key', '=', 'DIY_VR']]); return $res; } @@ -804,7 +909,7 @@ class Config extends BaseModel public function setGoodsListConfig($data, $site_id, $app_module) { $config = new ConfigModel(); - $res = $config->setConfig($data, '商品列表配置', 1, [ [ 'site_id', '=', $site_id ], [ 'app_module', '=', $app_module ], [ 'config_key', '=', 'GOODS_LIST_CONFIG' ] ]); + $res = $config->setConfig($data, '商品列表配置', 1, [['site_id', '=', $site_id], ['app_module', '=', $app_module], ['config_key', '=', 'GOODS_LIST_CONFIG']]); return $res; } @@ -817,9 +922,9 @@ class Config extends BaseModel public function getGoodsListConfig($site_id, $app_module) { $config = new ConfigModel(); - $res = $config->getConfig([ [ 'site_id', '=', $site_id ], [ 'app_module', '=', $app_module ], [ 'config_key', '=', 'GOODS_LIST_CONFIG' ] ]); + $res = $config->getConfig([['site_id', '=', $site_id], ['app_module', '=', $app_module], ['config_key', '=', 'GOODS_LIST_CONFIG']]); //数据格式化 - if (empty($res[ 'data' ][ 'value' ])) { + if (empty($res['data']['value'])) { $data = [ 'fontWeight' => false, 'padding' => 10, @@ -846,9 +951,9 @@ class Config extends BaseModel ] ] ]; - $res[ 'data' ][ 'value' ] = $data; + $res['data']['value'] = $data; } - $res[ 'data' ][ 'value' ][ 'nameLineMode' ] = $res[ 'data' ][ 'value' ][ 'nameLineMode' ] ?? 'single'; // 商品名称,单行、多行展示 + $res['data']['value']['nameLineMode'] = $res['data']['value']['nameLineMode'] ?? 'single'; // 商品名称,单行、多行展示 return $res; } @@ -862,7 +967,7 @@ class Config extends BaseModel public function setDefaultSearchWords($data, $site_id, $app_module) { $config = new ConfigModel(); - $res = $config->setConfig($data, '默认搜索关键词', 1, [ [ 'site_id', '=', $site_id ], [ 'app_module', '=', $app_module ], [ 'config_key', '=', 'GOODS_DEFAULT_SEARCH_WORDS_CONFIG' ] ]); + $res = $config->setConfig($data, '默认搜索关键词', 1, [['site_id', '=', $site_id], ['app_module', '=', $app_module], ['config_key', '=', 'GOODS_DEFAULT_SEARCH_WORDS_CONFIG']]); return $res; } @@ -875,9 +980,9 @@ class Config extends BaseModel public function getDefaultSearchWords($site_id, $app_module) { $config = new ConfigModel(); - $res = $config->getConfig([ [ 'site_id', '=', $site_id ], [ 'app_module', '=', $app_module ], [ 'config_key', '=', 'GOODS_DEFAULT_SEARCH_WORDS_CONFIG' ] ]); - if (empty($res[ 'data' ][ 'value' ])) { - $res[ 'data' ][ 'value' ] = [ + $res = $config->getConfig([['site_id', '=', $site_id], ['app_module', '=', $app_module], ['config_key', '=', 'GOODS_DEFAULT_SEARCH_WORDS_CONFIG']]); + if (empty($res['data']['value'])) { + $res['data']['value'] = [ 'words' => '搜索 商品' ]; } @@ -894,7 +999,7 @@ class Config extends BaseModel public function setGoodsSort($data, $site_id, $app_module) { $config = new ConfigModel(); - $res = $config->setConfig($data, '商品默认排序方式', 1, [ [ 'site_id', '=', $site_id ], [ 'app_module', '=', $app_module ], [ 'config_key', '=', 'GOODS_SORT_CONFIG' ] ]); + $res = $config->setConfig($data, '商品默认排序方式', 1, [['site_id', '=', $site_id], ['app_module', '=', $app_module], ['config_key', '=', 'GOODS_SORT_CONFIG']]); return $res; } @@ -907,9 +1012,9 @@ class Config extends BaseModel public function getGoodsSort($site_id, $app_module = 'shop') { $config = new ConfigModel(); - $res = $config->getConfig([ [ 'site_id', '=', $site_id ], [ 'app_module', '=', $app_module ], [ 'config_key', '=', 'GOODS_SORT_CONFIG' ] ]); - if (empty($res[ 'data' ][ 'value' ])) { - $res[ 'data' ][ 'value' ] = [ + $res = $config->getConfig([['site_id', '=', $site_id], ['app_module', '=', $app_module], ['config_key', '=', 'GOODS_SORT_CONFIG']]); + if (empty($res['data']['value'])) { + $res['data']['value'] = [ 'type' => 'asc', 'default_value' => 100 ]; @@ -927,7 +1032,7 @@ class Config extends BaseModel public function setCategoryConfig($data, $site_id = 1, $app_module = 'shop') { $config = new ConfigModel(); - $res = $config->setConfig($data, 'PC端首页分类设置', 1, [ [ 'site_id', '=', $site_id ], [ 'app_module', '=', $app_module ], [ 'config_key', '=', 'SHOP_CATEGORY_CONFIG' ] ]); + $res = $config->setConfig($data, 'PC端首页分类设置', 1, [['site_id', '=', $site_id], ['app_module', '=', $app_module], ['config_key', '=', 'SHOP_CATEGORY_CONFIG']]); return $res; } @@ -940,9 +1045,9 @@ class Config extends BaseModel public function getCategoryConfig($site_id = 1, $app_module = 'shop') { $config = new ConfigModel(); - $res = $config->getConfig([ [ 'site_id', '=', $site_id ], [ 'app_module', '=', $app_module ], [ 'config_key', '=', 'SHOP_CATEGORY_CONFIG' ] ]); - if (empty($res[ 'data' ][ 'value' ])) { - $res[ 'data' ][ 'value' ] = [ + $res = $config->getConfig([['site_id', '=', $site_id], ['app_module', '=', $app_module], ['config_key', '=', 'SHOP_CATEGORY_CONFIG']]); + if (empty($res['data']['value'])) { + $res['data']['value'] = [ 'category' => 1, 'img' => 1 ]; @@ -960,7 +1065,7 @@ class Config extends BaseModel public function setGoodsDetailConfig($data, $site_id, $app_module = 'shop') { $config = new ConfigModel(); - $res = $config->setConfig($data, '商品详情配置', 1, [ [ 'site_id', '=', $site_id ], [ 'app_module', '=', $app_module ], [ 'config_key', '=', 'GOODS_DETAIL_CONFIG' ] ]); + $res = $config->setConfig($data, '商品详情配置', 1, [['site_id', '=', $site_id], ['app_module', '=', $app_module], ['config_key', '=', 'GOODS_DETAIL_CONFIG']]); return $res; } @@ -973,9 +1078,9 @@ class Config extends BaseModel public function getGoodsDetailConfig($site_id, $app_module = 'shop') { $config = new ConfigModel(); - $res = $config->getConfig([ [ 'site_id', '=', $site_id ], [ 'app_module', '=', $app_module ], [ 'config_key', '=', 'GOODS_DETAIL_CONFIG' ] ]); - if (empty($res[ 'data' ][ 'value' ])) { - $res[ 'data' ][ 'value' ] = [ + $res = $config->getConfig([['site_id', '=', $site_id], ['app_module', '=', $app_module], ['config_key', '=', 'GOODS_DETAIL_CONFIG']]); + if (empty($res['data']['value'])) { + $res['data']['value'] = [ 'nav_bar_switch' => 0, // 是否透明,0:不透明,1:透明 'introduction_color' => '#303133', ]; @@ -993,7 +1098,7 @@ class Config extends BaseModel public function setShopDomainConfig($data, $site_id = 1, $app_module = 'shop') { $config = new ConfigModel(); - $res = $config->setConfig($data, '店铺域名配置', 1, [ [ 'site_id', '=', $site_id ], [ 'app_module', '=', $app_module ], [ 'config_key', '=', 'SHOP_DOMAIN_CONFIG' ] ]); + $res = $config->setConfig($data, '店铺域名配置', 1, [['site_id', '=', $site_id], ['app_module', '=', $app_module], ['config_key', '=', 'SHOP_DOMAIN_CONFIG']]); return $res; } @@ -1006,12 +1111,12 @@ class Config extends BaseModel public function getShopDomainConfig($site_id = 1, $app_module = 'shop') { $config = new ConfigModel(); - $res = $config->getConfig([ [ 'site_id', '=', $site_id ], [ 'app_module', '=', $app_module ], [ 'config_key', '=', 'SHOP_DOMAIN_CONFIG' ] ]); - if (empty($res[ 'data' ][ 'value' ])) { - $res[ 'data' ][ 'value' ] = [ + $res = $config->getConfig([['site_id', '=', $site_id], ['app_module', '=', $app_module], ['config_key', '=', 'SHOP_DOMAIN_CONFIG']]); + if (empty($res['data']['value'])) { + $res['data']['value'] = [ 'domain_name' => __ROOT__, ]; - $this->setShopDomainConfig($res[ 'data' ][ 'value' ], $site_id); + $this->setShopDomainConfig($res['data']['value'], $site_id); } return $res; } @@ -1019,11 +1124,11 @@ class Config extends BaseModel { $qq_map = new \app\model\map\QqMap(['key' => $tencent_map_key]); $res = $qq_map->ipToDetail([ - 'ip' => request()->ip() != '127.0.0.1' ? $_SERVER[ 'REMOTE_ADDR' ] : '', + 'ip' => request()->ip() != '127.0.0.1' ? $_SERVER['REMOTE_ADDR'] : '', ]); if (!empty($res)) { - if ($res[ 'status' ] != 0 && $type == 0) { - $res[ 'message' ] = '腾讯地图配置错误,无法定位地址'; + if ($res['status'] != 0 && $type == 0) { + $res['message'] = '腾讯地图配置错误,无法定位地址'; } } return $res; @@ -1059,7 +1164,7 @@ class Config extends BaseModel public function setGoodsNo($data, $site_id, $app_module) { $config = new ConfigModel(); - $res = $config->setConfig($data, '商品编码设置', 1, [ [ 'site_id', '=', $site_id ], [ 'app_module', '=', $app_module ], [ 'config_key', '=', 'GOODS_NO_CONFIG' ] ]); + $res = $config->setConfig($data, '商品编码设置', 1, [['site_id', '=', $site_id], ['app_module', '=', $app_module], ['config_key', '=', 'GOODS_NO_CONFIG']]); return $res; } @@ -1072,12 +1177,12 @@ class Config extends BaseModel public function getGoodsNo($site_id, $app_module = 'shop') { $config = new ConfigModel(); - $res = $config->getConfig([ [ 'site_id', '=', $site_id ], [ 'app_module', '=', $app_module ], [ 'config_key', '=', 'GOODS_NO_CONFIG' ] ]); - if (empty($res[ 'data' ][ 'value' ])) { - $res[ 'data' ][ 'value' ] = [ + $res = $config->getConfig([['site_id', '=', $site_id], ['app_module', '=', $app_module], ['config_key', '=', 'GOODS_NO_CONFIG']]); + if (empty($res['data']['value'])) { + $res['data']['value'] = [ 'uniqueness_switch' => 1, ]; } return $res; } -} \ No newline at end of file +} diff --git a/src/app/shop/controller/BaseShop.php b/src/app/shop/controller/BaseShop.php index a502904c7..874e8dd34 100644 --- a/src/app/shop/controller/BaseShop.php +++ b/src/app/shop/controller/BaseShop.php @@ -123,12 +123,11 @@ class BaseShop extends Controller $config_view = Config::get('view'); $config_view[ 'tpl_replace_string' ] = array_merge($config_view[ 'tpl_replace_string' ], $this->replace); Config::set($config_view, 'view'); - + // 其他操作 if (!request()->isAjax()) { $this->initBaseInfo(); $this->loadTemplate(); } - } /** diff --git a/src/app/shop/controller/Config.php b/src/app/shop/controller/Config.php index a8316c045..c73eb57ad 100644 --- a/src/app/shop/controller/Config.php +++ b/src/app/shop/controller/Config.php @@ -188,6 +188,52 @@ class Config extends BaseShop } } + /** + * AI配置 + */ + public function ai() + { + // 获取当前请求的 site_id + $site_id = $this->site_id; + $app_module = $this->app_module; + + $config_model = new ConfigModel(); + if (request()->isJson()) { + $type = input('type', ''); + $data = input('config', []); + + if ($type == 'save_platform_cfg') { + $data = json_decode($data, true); + $result_platform = $config_model->setAIPlatformConfig($data, $site_id, $app_module); + return $result_platform; + } else if ($type == 'save_aiagent_cfg') { + $data = json_decode($data, true); + $result_agent = $config_model->setAIAgentServicesConfig($data, $site_id, $app_module); + return $result_agent; + } + + return '无法识别的操作类型: ' . $type; + } else { + $support_app_modules = $config_model->getSupportAppModules(); + $support_ai_platform_types = $config_model->getSupportAIPlatformTypes(); + $config_platform = $config_model->getAIPlatformConfig($site_id)[ 'data' ][ 'value' ]; + $config_agent = $config_model->getAIAgentServicesConfig($site_id)[ 'data' ][ 'value' ]; + + $this->assign('support_app_modules', $support_app_modules); + $this->assign('support_ai_platform_types', $support_ai_platform_types); + $this->assign('platform_info', $config_platform); + $this->assign('agent_info', $config_agent); + + // return json_encode([ + // 'support_ai_platform_types' => $support_ai_platform_types, + // 'support_app_modules' => $support_app_modules, + // 'platform_info' => $config_platform, + // ]); + + return $this->fetch('config/ai/index'); + } + } + /** * 客服配置 */ diff --git a/src/app/shop/controller/Index.php b/src/app/shop/controller/Index.php index 81b4178f1..c63d2d83b 100644 --- a/src/app/shop/controller/Index.php +++ b/src/app/shop/controller/Index.php @@ -35,7 +35,6 @@ class Index extends BaseShop */ public function index() { - $this->assign('shop_status', 1); $this->handlePromotion(); diff --git a/src/app/shop/view/config/ai.html b/src/app/shop/view/config/ai.html deleted file mode 100644 index a19ef65f7..000000000 --- a/src/app/shop/view/config/ai.html +++ /dev/null @@ -1,617 +0,0 @@ - - - -
- -
-
    -
  • AI智能客服
  • -
  • AI服务平台
  • -
-
-
- -
- -
- 基础设置 -
-
- -
- -
-
- - -
-
-
    -
  • 简体中文
  • -
  • English
  • -
-
-
-
- -
- -
-
- -
- -
- -
支持JPG、PNG、GIF格式,建议尺寸:100x100px
-
-
- -
- -
- -
支持JPG、PNG、GIF格式,建议尺寸:100x100px
-
-
- -
- -
- - -
-
-
-
-
- -
- -
-
- -
- -
- -
Support JPG, PNG, GIF formats, recommended size: - 100x100px
-
-
- -
- -
- -
Support JPG, PNG, GIF formats, recommended size: - 100x100px
-
-
- -
- -
- - -
-
-
-
-
- -
- - -
- 显示设置 -
-
- -
- -
-
- -
- -
- -
-
-
建议设置在10-500条之间
-
- -
- -
- -
-
-
-
-
- -
- -
- 基本设置 -
- -
- -
- -
-
- - -
- AI服务平台配置 -
- -
- - - - - - - - - - - - {if isset($platform_list) && !empty($platform_list)} - {foreach $platform_list as $platform} - - - - - - - - {/foreach} - {else} - - - - {/if} - -
名称类型API URLAPI Key操作
- - - - - - - - - - - -
暂无平台配置
-
- - -
- -
- -
-
- -
-
-
-
-
- - - \ No newline at end of file diff --git a/src/app/shop/view/config/ai/agent.html b/src/app/shop/view/config/ai/agent.html new file mode 100644 index 000000000..62297e4a8 --- /dev/null +++ b/src/app/shop/view/config/ai/agent.html @@ -0,0 +1,141 @@ +
+ +
+ +
+ 基础设置 +
+
+ +
+ +
+
+ + +
+
+
    +
  • 简体中文
  • +
  • English
  • +
+
+
+
+ +
+ +
+
+ +
+ +
+ +
支持JPG、PNG、GIF格式,建议尺寸:100x100px
+
+
+ +
+ +
+ +
支持JPG、PNG、GIF格式,建议尺寸:100x100px
+
+
+ +
+ +
+ + +
+
+
+
+
+ +
+ +
+
+ +
+ +
+ +
Support JPG, PNG, GIF formats, recommended size: + 100x100px
+
+
+ +
+ +
+ +
Support JPG, PNG, GIF formats, recommended size: + 100x100px
+
+
+ +
+ +
+ + +
+
+
+
+
+ +
+ + +
+ 显示设置 +
+
+ +
+ +
+
+ +
+ +
+ +
+
+
建议设置在10-500条之间
+
+ +
+ +
+ +
+
+ + +
+ +
+
+
\ No newline at end of file diff --git a/src/app/shop/view/config/ai/index.html b/src/app/shop/view/config/ai/index.html new file mode 100644 index 000000000..05ffd3256 --- /dev/null +++ b/src/app/shop/view/config/ai/index.html @@ -0,0 +1,209 @@ + + + +
+ +
+
    +
  • AI智能客服
  • +
  • AI服务平台
  • +
+
+ + {include file="app/shop/view/config/ai/platform.html" /} +
+
+
+ + + \ No newline at end of file diff --git a/src/app/shop/view/config/ai/js/platform.js b/src/app/shop/view/config/ai/js/platform.js new file mode 100644 index 000000000..60711b429 --- /dev/null +++ b/src/app/shop/view/config/ai/js/platform.js @@ -0,0 +1,397 @@ +// 平台选择下拉框处理 +form.on('select(platformSelect)', function (data) { + var platformId = data.value; + if (platformId) { + // 可以在这里添加根据平台ID高亮显示对应表格行的逻辑 + $('#platformTable tr').removeClass('layui-bg-blue'); + $('#platformTable tr[data-id="' + platformId + '"]').addClass('layui-bg-blue'); + } +}); + +// 应用模块选择下拉框处理 +form.on('select(appModuleSelect)', function (data) { + var appModule = data.value; + loadPlatformConfigByAppModule(appModule); +}); + +// 存储平台配置的初始状态,用于检测数据变更 +function storeInitialData() { + $('#platformTable tr[data-id]').each(function () { + var platformId = $(this).data('id'); + var initialData = { + name: $('input[name="platform_name[' + platformId + ']"]').val(), + type: $('select[name="platform_type[' + platformId + ']"]').val(), + base_url: $('input[name="platform_base_url[' + platformId + ']"]').val(), + api_key: $('input[name="platform_api_key[' + platformId + ']"]').val(), + desc: $('input[name="platform_desc[' + platformId + ']"]').val() + }; + // 使用data属性存储初始数据 + $(this).data('initial-data', initialData); + }); +} + +// 检查平台数据是否有变更 +function hasDataChanged(platformId) { + var row = $('#platformTable tr[data-id="' + platformId + '"]'); + var initialData = row.data('initial-data'); + + // 对于新添加的平台(没有初始数据),认为有变更 + if (!initialData) { + return true; + } + + var currentData = { + name: $('input[name="platform_name[' + platformId + ']"]').val(), + type: $('select[name="platform_type[' + platformId + ']"]').val(), + base_url: $('input[name="platform_base_url[' + platformId + ']"]').val(), + api_key: $('input[name="platform_api_key[' + platformId + ']"]').val(), + desc: $('input[name="platform_desc[' + platformId + ']"]').val() + }; + + // 比较初始数据和当前数据 + for (var key in initialData) { + if (initialData[key] !== currentData[key]) { + return true; + } + } + + return false; +} + +// 更新保存按钮图标 +function updateSaveButtonIcon(platformId) { + var saveButton = $('#platformTable tr[data-id="' + platformId + '"] .save-platform'); + var hasChanged = hasDataChanged(platformId); + + if (hasChanged) { + saveButton.find('i').removeClass('layui-icon-ok').addClass('layui-icon-edit'); + } else { + saveButton.find('i').removeClass('layui-icon-edit').addClass('layui-icon-ok'); + } +} + +// 监听输入框变化 +$(document).on('input propertychange', '#platformTable input.layui-table-edit', function () { + var platformId = $(this).closest('tr').data('id'); + updateSaveButtonIcon(platformId); +}); + +// 监听选择框变化 +$(document).on('change', '#platformTable select.layui-table-edit', function () { + var platformId = $(this).closest('tr').data('id'); + updateSaveButtonIcon(platformId); +}); + +// 初始化时存储初始数据 +storeInitialData(); + +function genNewPlatformId() { + return 'temp_' + new Date().getTime() + Math.random().toString(36).substring(2); +} + +// 保存平台配置 +$(document).on('click', '.save-platform', function () { + var platformId = $(this).data('id'); + var name = $('input[name="platform_name[' + platformId + ']"]').val(); + var type = $('select[name="platform_type[' + platformId + ']"]').val(); + var apiUrl = $('input[name="platform_base_url[' + platformId + ']"]').val(); + var apiKey = $('input[name="platform_api_key[' + platformId + ']"]').val(); + + if (!name) { + layer.msg('平台名称不能为空', { icon: 2 }); + return; + } + + if (!type) { + layer.msg('请选择平台类型', { icon: 2 }); + return; + } + + if (!apiUrl) { + layer.msg('API URL不能为空', { icon: 2 }); + return; + } + + if (!apiKey) { + layer.msg('API Key不能为空', { icon: 2 }); + return; + } + + // 建立临时的平台ID + var newPlatformId = String(platformId).startsWith('temp_') ? genNewPlatformId() : platformId; + + // 更新行的data-id属性 + $('#platformTable tr[data-id="' + platformId + '"]').attr('data-id', newPlatformId); + + // 更新表单元素的name属性中的ID + $('#platformTable tr[data-id="' + newPlatformId + '"] input, #platformTable tr[data-id="' + newPlatformId + '"] select').each(function () { + var oldName = $(this).attr('name'); + if (oldName) { // 确保oldName存在才调用replace方法 + var newName = oldName.replace(platformId, newPlatformId); + $(this).attr('name', newName); + } + }); + + // 更新按钮的data-id属性 + $('#platformTable tr[data-id="' + newPlatformId + '"] button').data('id', newPlatformId); + + // 更新默认平台下拉框 + var select = $('select[name="selected_platform"]'); + // 检查是否已存在相同ID的选项 + if (select.find('option[value="' + newPlatformId + '"]').length === 0) { + select.append(''); + form.render('select'); // 重新渲染select + } + + // 保存成功后,更新初始数据并将图标改回layui-icon-ok + var newRow = $('#platformTable tr[data-id="' + newPlatformId + '"]'); + var updatedData = { + name: name, + type: type, + base_url: apiUrl, + api_key: apiKey, + desc: $('input[name="platform_desc[' + newPlatformId + ']"]').val() + }; + newRow.data('initial-data', updatedData); + + // 更新保存按钮图标为layui-icon-ok + newRow.find('.save-platform i').removeClass('layui-icon-edit').addClass('layui-icon-ok'); + + // 显示保存成功提示 + layer.msg('保存成功', { icon: 1 }); +}); + +// 删除平台 +$(document).on('click', '.delete-platform', function () { + var platformId = $(this).data('id'); + layer.confirm('确定要删除这个平台配置吗?', { + btn: ['确定', '取消'] + }, function (index) { + // 删除行 + $('#platformTable tr[data-id="' + platformId + '"]').remove(); + + // 从下拉框中移除对应的选项 + $('select[name="selected_platform"] option[value="' + platformId + '"]').remove(); + form.render('select'); + + // 检查表格是否为空,如果为空则显示提示 + if ($('#platformTable tr').length === 0) { + $('#platformTable').append('暂无平台配置'); + } + + layer.close(index); + }); +}); + +// 禁用/开启平台 +$(document).on('click', '.toggle-platform', function () { + var platformId = $(this).data('id'); + var status = $(this).data('status'); + var newStatus = status === 1 ? 0 : 1; + + // 更新按钮状态 + $(this).data('status', newStatus); + $(this).html(newStatus === 1 ? '' : ''); + $(this).attr('title', newStatus === 1 ? '开启' : '禁用'); + + // 如果平台被禁用,从默认平台下拉框中移除 + if (newStatus === 0) { + var option = $('select[name="selected_platform"] option[value="' + platformId + '"]'); + if (option.length > 0) { + option.detach().data('disabled', true); // 保存选项但不显示 + form.render('select'); + } + } + // 如果平台被启用,将选项添加回下拉框 + else { + var savedOption = $('select[name="selected_platform"] option[data-disabled="true"][value="' + platformId + '"]'); + if (savedOption.length === 0) { + // 如果没有保存的选项,获取平台名称并创建新选项 + var platformName = $('#platformTable tr[data-id="' + platformId + '"] input[name^="platform_name"]').val(); + if (platformName) { + $('select[name="selected_platform"]').append(''); + form.render('select'); + } + } else { + savedOption.removeData('disabled').appendTo('select[name="selected_platform"]'); + form.render('select'); + } + } + + layer.msg(newStatus === 1 ? '已设置为开启,保存后生效' : '已设置为禁用,保存后生效', { icon: 1 }); +}); + +// 添加新平台 +$('#addPlatform').on('click', function () { + // 生成一个临时ID + var tempId = genNewPlatformId(); + + // 创建新行HTML + var newRow = '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + ''; + + // 获取表格body + var tbody = $('#platformTableBody'); + + // 检查是否有暂无平台配置的提示行 + var emptyRow = tbody.find('tr:has(td[colspan="5"])'); + if (emptyRow.length > 0) { + emptyRow.replaceWith(newRow); + } else { + tbody.append(newRow); + } + + // 重新渲染表单元素 + form.render(); + + // 为新添加的行设置初始数据为null,确保图标显示为编辑状态 + tbody.find('tr[data-id="' + tempId + '"]').data('initial-data', null); + + // 新添加的行默认显示编辑图标 + tbody.find('tr[data-id="' + tempId + '"] .save-platform i').removeClass('layui-icon-ok').addClass('layui-icon-edit'); + + // 聚焦到新添加的行的第一个输入框 + tbody.find('input[name="platform_name[' + tempId + ']"]').focus(); +}); + +// 保存平台配置 +form.on('submit(save_platform_cfg)', function (data) { + // 验证表单数据 + if (!form.verify()) { + return false; + } + + // 验证是否选择了默认平台 + if (!data.field['selected_platform']) { + layer.msg('请选择默认AI智能客服平台', { icon: 2 }); + return false; + } + + // 处理新增的平台配置 + var cfg = {}; + var platforms = []; + $('#platformTableBody tr[data-id]').each(function () { + var id = $(this).data('id'); + var platform = { + 'id': id, + 'name': $(this).find('input[name^="platform_name"]').val(), + 'type': $(this).find('select[name^="platform_type"]').val(), + 'type_label': $(this).find('select[name^="platform_type"]').find('option:selected').text(), + 'enable': parseInt($(this).find('button.toggle-platform').data('status') || 0), + 'desc': $(this).find('input[name^="platform_desc"]').val(), + 'base_url': $(this).find('input[name^="platform_base_url"]').val(), + 'api_key': $(this).find('input[name^="platform_api_key"]').val(), + }; + platforms.push(platform); + }); + + cfg = { + 'default': { + 'id': data.field['selected_platform'] || (platforms[0] ? platforms[0].id : ''), + 'name': data.field['selected_platform_name'] || (platforms[0] ? platforms[0].name : ''), + }, + 'list': platforms, + }; + + $.ajax({ + url: ns.url("shop/config/ai"), + data: { + config: JSON.stringify(cfg), + type: 'save_platform_cfg', + app_module: data.field['app_module'] + }, + dataType: 'JSON', + type: 'POST', + success: function (res) { + if (res.code == 0) { + layer.msg('保存成功', { icon: 1 }); + // 重新加载当前应用模块的配置,确保数据同步 + loadPlatformConfigByAppModule(data.field['app_module']); + layer.closeAll(); + } else { + layer.msg('保存失败:' + res.message, { icon: 2 }); + } + } + }); +}); + +// 保存AI智能客服平台配置 +form.on('submit(save_aiagent_cfg)', function (data) { + // 验证表单数据 + if (!form.verify()) { + return false; + } + + console.log('data.field =', data.field); + + var cfg = {}; + + // 处理关于AI智能客服配置,将ai_avater[en], ai_avater[zh] 合并到 ai_avater 字段 + ['ai_avater', 'ai_name', 'user_avater', 'welcome_messages'].forEach(function (field) { + cfg[field] = { + 'en': data.field[field + '[en]'], + 'zh_CN': data.field[field + '[zh_CN]'], + }; + }); + + ['show_load_more_button', 'stream_mode'].forEach(function (field) { + cfg[field] = data.field[field] === 'on'; + }); + + ['max_messages'].forEach(function (field) { + cfg[field] = data.field[field] || 0; + }); + + $.ajax({ + url: ns.url("shop/config/ai"), + data: { + config: JSON.stringify(cfg), + type: 'save_aiagent_cfg', + }, + dataType: 'JSON', + type: 'POST', + success: function (res) { + if (res.code == 0) { + layer.msg('保存成功', { icon: 1 }); + listenerHash(); // 刷新页面 + layer.closeAll(); + } else { + layer.msg('保存失败:' + res.message, { icon: 2 }); + } + } + }); +}); diff --git a/src/app/shop/view/config/ai/platform.html b/src/app/shop/view/config/ai/platform.html new file mode 100644 index 000000000..7bce932c3 --- /dev/null +++ b/src/app/shop/view/config/ai/platform.html @@ -0,0 +1,465 @@ +
+ +
+ +
+ 应用模块选择 +
+
+ +
+ +
+
+ + +
+ 基本设置 +
+ +
+ +
+ +
+
+ + +
+ AI服务平台配置 +
+ +
+ + + + + + + + + + + + + + + + +
名称类型Base URLAPI Key描述操作
请选择应用模块
+
+ + +
+ +
+ + +
+ +
+
+
+ + \ No newline at end of file diff --git a/src/app/shop/view/layout/base.html b/src/app/shop/view/layout/base.html index 478e2681c..b7023ea15 100644 --- a/src/app/shop/view/layout/base.html +++ b/src/app/shop/view/layout/base.html @@ -22,6 +22,7 @@ +