实现后台及前台通过API访问UV埋点,所有代码全部保存

This commit is contained in:
2025-11-08 18:15:26 +08:00
parent 6bad32d9b1
commit e440631275
43 changed files with 5960 additions and 1105 deletions

24
.env Normal file
View File

@@ -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

1
.gitignore vendored
View File

@@ -1 +1,2 @@
src/runtime src/runtime
src/upload

4
.idea/dataSources.xml generated
View File

@@ -1,11 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="DataSourceManagerImpl" format="xml" multifile-model="true"> <component name="DataSourceManagerImpl" format="xml" multifile-model="true">
<data-source source="LOCAL" name="@localhost" uuid="84bbe120-3407-4e1f-b749-7a6a28f41e21"> <data-source source="LOCAL" name="local_shop_xcx30.5g" uuid="84bbe120-3407-4e1f-b749-7a6a28f41e21">
<driver-ref>mysql.8</driver-ref> <driver-ref>mysql.8</driver-ref>
<synchronize>true</synchronize> <synchronize>true</synchronize>
<jdbc-driver>com.mysql.cj.jdbc.Driver</jdbc-driver> <jdbc-driver>com.mysql.cj.jdbc.Driver</jdbc-driver>
<jdbc-url>jdbc:mysql://localhost:3306</jdbc-url> <jdbc-url>jdbc:mysql://localhost:3316</jdbc-url>
<working-dir>$ProjectFileDir$</working-dir> <working-dir>$ProjectFileDir$</working-dir>
</data-source> </data-source>
</component> </component>

4
.idea/sqldialects.xml generated
View File

@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="SqlDialectMappings"> <component name="SqlDialectMappings">
<file url="file://$PROJECT_DIR$/../../zips/php后端源码/5g_2025-10-28_11-51-21_mysql_data_kZXBL.sql" dialect="MySQL" /> <file url="file://$PROJECT_DIR$/../../zips/php后端源码/5g_-xcx30.5.g-quickapp-2025-11-03_00-30-01_mysql_data.sql" dialect="MySQL" />
<file url="file://$PROJECT_DIR$/../../zips/php后端源码/shop_mallnew_2025-10-28_01-30-01_mysql_data.sql" dialect="MySQL" /> <file url="file://$PROJECT_DIR$/../../zips/php后端源码/shop_mallnew-xcx30.5.g-quickapp_2025-11-03_00-30-20_mysql_data.sql" dialect="MySQL" />
</component> </component>
</project> </project>

View File

@@ -36,6 +36,12 @@ services:
- xdebug_logs:/tmp # Xdebug 日志目录 - xdebug_logs:/tmp # Xdebug 日志目录
depends_on: depends_on:
- db - 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: networks:
- sass-platform-net - sass-platform-net
labels: labels:

View File

@@ -7,8 +7,12 @@ WORKDIR /var/www/html
# 拷贝本地的 sources.list 文件以加速 apt-get # 拷贝本地的 sources.list 文件以加速 apt-get
COPY ./sources.list /etc/apt/sources.list 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 \ RUN apt-get update && apt-get install -y \
supervisor \
git \ git \
curl \ curl \
vim \ vim \
@@ -73,4 +77,5 @@ RUN echo "zend_extension=xdebug.so" > /usr/local/etc/php/conf.d/xdebug.ini
# 暴露端口 # 暴露端口
EXPOSE 9000 9003 EXPOSE 9000 9003
CMD ["php-fpm"] # 启动Supervisor
CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf"]

View File

@@ -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

60
docs/common/const.md Normal file
View File

@@ -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' ];
}
```

37
docs/common/cron.md Normal file
View File

@@ -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);
}
```

13
docs/common/layout.md Normal file
View File

@@ -0,0 +1,13 @@
# 布局
## 针对与商户的布局
```html
<!-- 文件位置 -->
src\app\shop\view\layout\base.html
```
- 涉及到的静态资源位置:
- 静态资源文件位置:`src\public\static\`

398
docs/common/stat.md Normal file
View File

@@ -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`

428
docs/readme.md Normal file
View File

@@ -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" }
};
```
这样的区分让智能客服能够更精准地理解用户意图,提供更准确的店铺/门店信息服务。

View File

@@ -13,6 +13,7 @@ use liliuwei\think\Jump;
use think\facade\Route; use think\facade\Route;
use think\facade\Config; use think\facade\Config;
use think\facade\Env; use think\facade\Env;
use app\tracking\VisitorTracker;
/** /**
* 控制器基础类 * 控制器基础类
@@ -79,6 +80,23 @@ abstract class Controller
} }
//固定一个版本号 //固定一个版本号
$this->assign('version', 128); $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); return View::fetch($template, $vars);
} }

View File

@@ -0,0 +1,505 @@
<?php
/**
* AI API服务, 主要用来与终端进行AI相关的交互
* 1. 与微信小程序/H5的智能客服进行交互
* 2. 连接Dify平台或RAGFlow平台的智能体
*/
namespace app\api\controller;
use Exception;
use think\facade\Cache;
use think\facade\Db;
use think\facade\Request;
use think\response\Json;
use app\exception\ApiException;
use app\model\ai\AiChatSession;
use app\model\ai\AiChatHistory;
use app\model\web\Config;
class AI extends BaseApi
{
/**
* 日志文件名称
* @var string
*/
private $log_file = 'ai.log';
/**
* 配置模型实例
* @var Config
*/
protected $configModel;
/**
* AiChatSession模型实例
* @var AiChatSession
*/
protected $aiChatSessionModel;
/**
* AiChatHistory模型实例
* @var AiChatHistory
*/
protected $aiChatHistoryModel;
/**
* 构造函数
*/
public function __construct()
{
parent::__construct();
$this->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'));
}
}
}

View File

@@ -18,6 +18,7 @@ use think\facade\Cache;
use addon\store\model\Config as StoreConfig; use addon\store\model\Config as StoreConfig;
use app\model\store\Store; use app\model\store\Store;
use think\Response; use think\Response;
use app\tracking\VisitorTracker;
class BaseApi class BaseApi
{ {
@@ -97,6 +98,15 @@ class BaseApi
} }
$this->store_id = $this->params[ 'store_id' ] ?? 0; $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]);
}
// ------------------------------------------- ------------
} }
/** /**

View File

@@ -11,6 +11,7 @@ use app\model\web\DiyView as DiyViewModel;
use app\model\shop\Shop as ShopModel; use app\model\shop\Shop as ShopModel;
use app\model\member\Config as ConfigMemberModel; use app\model\member\Config as ConfigMemberModel;
class Config extends BaseApi class Config extends BaseApi
{ {
@@ -107,7 +108,6 @@ class Config extends BaseApi
*/ */
public function init() public function init()
{ {
$diy_view = new DiyViewModel(); $diy_view = new DiyViewModel();
$diy_style = $diy_view->getStyleConfig($this->site_id)[ 'data' ][ 'value' ]; $diy_style = $diy_view->getStyleConfig($this->site_id)[ 'data' ][ 'value' ];

View File

@@ -21,8 +21,14 @@ class Schedule extends Task
*/ */
protected function execute() protected function execute()
{ {
try {
log_write('Cron的Schedule开始执行', 'debug');
//...具体的任务执行 //...具体的任务执行
$cron_model = new Cron(); $cron_model = new Cron();
$cron_model->execute(ScheduleDict::cli); $cron_model->execute(ScheduleDict::cli);
log_write('Cron的Schedule执行完成', 'debug');
} catch (\Exception $e) {
log_write('Cron的Schedule执行失败' . $e->getMessage(), 'error');
}
} }
} }

View File

@@ -8,8 +8,10 @@ error_reporting(E_NOTICE);
use extend\QRcode as QRcode; use extend\QRcode as QRcode;
use think\facade\Session; use think\facade\Session;
use think\facade\Event; use think\facade\Event;
use think\facade\App;
use app\model\system\Addon; use app\model\system\Addon;
use extend\Barcode; use extend\Barcode;
use app\common\library\CallerInfo;
/*****************************************************基础函数*********************************************************/ /*****************************************************基础函数*********************************************************/
@@ -1877,6 +1879,77 @@ function paramFilter($param)
return preg_replace($filter_rule, '', $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输出方便定位 * 将异常格式化HTML输出方便定位
* @param mixed $th * @param mixed $th

View File

@@ -0,0 +1,346 @@
<?php
// app/common/library/BrowserDetector.php
namespace app\common\library;
class BrowserDetector
{
private $userAgent;
private $headers;
private $server;
private $request;
public function __construct()
{
$this->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'
];
}
}

View File

@@ -0,0 +1,112 @@
<?php
namespace app\common\library;
/**
* 获取调用者信息的基础类
*/
class CallerInfo
{
/**
* 获取调用者信息
* @param int $depth 调用深度0-当前函数1-直接调用者2-调用者的调用者)
* @return array
*/
public static function getCallerInfo(int $depth = 1): array
{
$backtrace = debug_backtrace(DEBUG_BACKTRACE_PROVIDE_OBJECT, $depth + 2);
if (!isset($backtrace[$depth])) {
return [
'file' => '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));
}
}

View File

@@ -0,0 +1,115 @@
<?php
namespace app\common\library;
use app\common\library\CallerInfo;
/**
* 增强的日志类(包含调用者信息)
*/
class EnhancedLogger
{
/**
* 记录日志(包含调用者信息)
*/
public static function log(string $message, string $level = 'info', array $context = []): void
{
$callerInfo = CallerInfo::getCallerInfo(2); // 跳过log方法本身
$logData = [
'timestamp' => 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];
}
}

View File

@@ -0,0 +1,63 @@
<?php
namespace app\common\library;
use think\facade\App;
class PerformanceAwareCallerInfo
{
private static $enabled = true;
/**
* 启用/禁用调用者信息(生产环境可禁用)
*/
public static function setEnabled(bool $enabled): void
{
self::$enabled = $enabled;
}
/**
* 检查是否启用
*/
public static function isEnabled(): bool
{
return self::$enabled;
}
/**
* 高性能的调用者信息获取
*/
public static function getCallerInfoOpt(int $depth = 1): array
{
if (!self::$enabled) {
return ['file' => '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'
];
}
}

View File

@@ -28,8 +28,10 @@ class Task extends Controller
*/ */
public function checkCron() public function checkCron()
{ {
log_write('Task checkCron ...', 'debug');
$cron_model = new Cron(); $cron_model = new Cron();
$result = $cron_model->checkSchedule(); $result = $cron_model->checkSchedule();
log_write('Task checkCron result: ' . json_encode($result), 'debug');
return $result; return $result;
} }
@@ -39,7 +41,10 @@ class Task extends Controller
*/ */
public function run() 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 = new Cron();
$cron_model->execute(); $cron_model->execute();
return true; return true;
@@ -51,6 +56,7 @@ class Task extends Controller
*/ */
public function execute() public function execute()
{ {
log_write('单独计划任务 开始执行', 'info');
if (config('cron.default') == ScheduleDict::default) { if (config('cron.default') == ScheduleDict::default) {
ignore_user_abort(true); ignore_user_abort(true);
set_time_limit(0); set_time_limit(0);

View File

@@ -25,6 +25,7 @@ class InitConfig
$this->initConst(); $this->initConst();
//初始化配置信息 //初始化配置信息
$this->initConfig(); $this->initConfig();
log_write('系统配置信息已初始化', 'debug');
} }
/** /**

View File

@@ -1,4 +1,5 @@
<?php <?php
/** /**
@@ -23,9 +24,15 @@ class InitCron
{ {
public function handle() public function handle()
{ {
log_write('InitCron计划任务初始化', 'debug');
try {
//根据计划任务类型来判断 //根据计划任务类型来判断
if(config('cron.default') != ScheduleDict::default) return; if (config('cron.default') != ScheduleDict::default) {
log_write('InitCron计划任务未开启', 'warning');
return;
}
if (defined('BIND_MODULE') && BIND_MODULE === 'install') { if (defined('BIND_MODULE') && BIND_MODULE === 'install') {
log_write('InitCron计划任务未开启安装模块时不执行计划任务', 'warning');
return; return;
} }
$last_time = Cache::get("cron_last_load_time"); $last_time = Cache::get("cron_last_load_time");
@@ -37,8 +44,26 @@ class InitCron
$last_exec_time = 0; $last_exec_time = 0;
} }
$module = request()->module(); $module = request()->module();
if ($module != 'cron') { if ($module == 'cron') {
if (!defined('CRON_EXECUTE') && time() - $last_time > 100 && time() - $last_exec_time > 100) { 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()); Cache::set("cron_http_last_exec_time", time());
defined('CRON_EXECUTE') or define('CRON_EXECUTE', 1); defined('CRON_EXECUTE') or define('CRON_EXECUTE', 1);
$url = url('cron/task/cronExecute'); $url = url('cron/task/cronExecute');
@@ -51,15 +76,19 @@ class InitCron
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
curl_exec($ch); curl_exec($ch);
// // 获取错误信息并打印 // 获取错误信息并打印
// $error = curl_error($ch); $error = curl_error($ch);
// if($error){ if ($error) {
// //保存错误 //保存错误
// Cron::setError(ScheduleDict::default, $error); Cron::setError(ScheduleDict::default, $error);
// }
// // 关闭cURL资源句柄
// curl_close($ch);
} }
// 关闭cURL资源句柄
curl_close($ch);
log_write('InitCron计划任务已启动', 'debug');
} catch (\Exception $e) {
// 计划任务异常,直接返回
log_write('InitCron计划任务异常' . $e->getMessage(), 'error');
return;
} }
} }
} }

View File

@@ -15,27 +15,26 @@ class Cronexecute
{ {
public function fire(Job $job, $data) public function fire(Job $job, $data)
{ {
$job->delete();
try { try {
log_write('Job开始执行' . $data['name'], 'debug');
$res = event($data[ 'event' ], [ 'relate_id' => $data[ 'relate_id' ] ]); $job->delete();
$res = event($data['event'], ['relate_id' => $data['relate_id']]);
$data_log = [ $data_log = [
'name' => $data[ 'name' ], 'name' => $data['name'],
'event' => $data[ 'event' ], 'event' => $data['event'],
'relate_id' => $data[ 'relate_id' ], 'relate_id' => $data['relate_id'],
'message' => json_encode($res) '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 = new Cron();
//定义最新的执行时间或错误 //定义最新的执行时间或错误
$cron_model->addCronLog($data_log); $cron_model->addCronLog($data_log);
log_write('Job执行成功' . $data['name'], 'debug');
} catch (\Exception $e) { } catch (\Exception $e) {
Log::write($e->getMessage()); Log::write($e->getMessage());
log_write('Job执行失败' . $e->getMessage(), 'error');
$job->delete(); $job->delete();
} }
} }
} }

View File

@@ -0,0 +1,261 @@
<?php
/**
* AI聊天历史记录模型
* 负责处理AI对话中的聊天记录数据存储和管理
*/
namespace app\model\ai;
use think\facade\Db;
use app\model\BaseModel;
use Exception;
class AiChatHistory extends BaseModel
{
/**
* 日志文件名称
* @var string
*/
private $log_file = 'ai_chat_history.log';
/**
* 表名
* @var string
*/
protected $name = 'ai_chat_history';
/**
* 保存聊天记录
* @param array $data 聊天记录数据
* @return array
*/
public function saveHistory($data)
{
try {
// 验证必要字段
if (empty($data['site_id']) || empty($data['user_id']) || empty($data['session_id']) ||
empty($data['platform']) || empty($data['user_message']) || empty($data['ai_message'])) {
return $this->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');
}
}
}

View File

@@ -0,0 +1,266 @@
<?php
/**
* AI聊天会话模型
* 用于管理AI聊天的会话信息
*/
namespace app\model\ai;
use \app\model\BaseModel;
use think\facade\Db;
class AiChatSession extends BaseModel
{
/**
* 日志文件名称
* @var string
*/
private $log_file = 'ai_chat_session.log';
/**
* 表名
* @var string
*/
protected $name = 'ai_chat_session';
/**
* 获取会话信息
* @param array $where 条件
* @param array $field 字段
* @return array
*/
public function getSessionInfo($where = [], $field = ['*'])
{
try {
$data = Db::name($this->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');
}
}
}

View File

@@ -30,6 +30,8 @@ class StatShop extends BaseModel
*/ */
public function addShopStat($data) public function addShopStat($data)
{ {
log_write('店铺按天新统计数据开始添加', 'debug');
try{
$carbon = Carbon::now(); $carbon = Carbon::now();
$dir = __UPLOAD__.'/stat/stat_shop/'; $dir = __UPLOAD__.'/stat/stat_shop/';
if (!is_dir($dir) && !mkdir($dir, 0777, true) && !is_dir($dir)) { if (!is_dir($dir) && !mkdir($dir, 0777, true) && !is_dir($dir)) {
@@ -41,23 +43,40 @@ class StatShop extends BaseModel
//增加当天时统计 //增加当天时统计
$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(); return $this->success();
} }
/**
* 从stat_shop目录下读取所有文件将数据处理后写入数据表中
* 处理完每个文件后,删除文件
*/
public function cronShopStat() public function cronShopStat()
{ {
log_write('店铺按天统计数据开始处理', 'debug');
$path = __UPLOAD__.'/stat/stat_shop'; $path = __UPLOAD__.'/stat/stat_shop';
if(!is_dir($path)) return; if(!is_dir($path)) {
log_write('店铺按天统计数据处理失败:目录不存在', 'error');
return;
}
$result = $this->scanFile($path); $result = $this->scanFile($path);
if(empty($result)) return; if(empty($result)) {
log_write('店铺按天统计数据处理失败:目录下无文件', 'error');
return;
}
try { try {
$json_array = []; $json_array = [];
foreach ($result as $key => $val){ foreach ($result as $key => $val){
$stat_extend = new Stat($path.'/'.$val, 'stat_shop'); $stat_extend = new Stat($path.'/'.$val, 'stat_shop');
$json_array[] = $stat_extend->load(); $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){ foreach ($data_array as $json_k => $json_v){
$system_stat->addStatShopModel($json_v); $system_stat->addStatShopModel($json_v);
} }
log_write('店铺按天统计数据处理成功', 'debug');
} catch (\Exception $e) { } catch (\Exception $e) {
log_write('店铺按天统计数据处理失败:' . $e->getMessage(), 'error');
return $this->error('店铺按天统计数据处理失败');
} }
} }
@@ -97,6 +118,8 @@ class StatShop extends BaseModel
*/ */
public function addShopHourStat($data, $carbon) public function addShopHourStat($data, $carbon)
{ {
log_write('店铺按小时新统计数据开始添加', 'debug');
try{
$dir = __UPLOAD__.'/stat/stat_shop_hour/'; $dir = __UPLOAD__.'/stat/stat_shop_hour/';
if (!is_dir($dir) && !mkdir($dir, 0777, true) && !is_dir($dir)) { if (!is_dir($dir) && !mkdir($dir, 0777, true) && !is_dir($dir)) {
return $this->error(sprintf('Directory "%s" was not created', $dir)); return $this->error(sprintf('Directory "%s" was not created', $dir));
@@ -104,16 +127,28 @@ class StatShop extends BaseModel
$filename = $dir.$carbon->year.'_'.$carbon->month.'_'.$carbon->day.'_'.$carbon->hour.'_'.$carbon->second.'_'.unique_random().'.json'; $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 = new Stat($filename, 'stat_shop_hour',$data['site_id']);
$stat_extend->handleData($data);//写入文件 $stat_extend->handleData($data);//写入文件
log_write('店铺按小时新统计数据已添加', 'debug');
}catch (\Exception $e){
log_write('店铺按小时新统计数据添加失败:' . $e->getMessage(), 'error');
return $this->error('店铺按小时新统计数据添加失败');
}
return $this->success(); return $this->success();
} }
public function cronShopStatHour() public function cronShopStatHour()
{ {
log_write('系统计划任务开始执行店铺按小时统计');
$path = __UPLOAD__.'/stat/stat_shop_hour'; $path = __UPLOAD__.'/stat/stat_shop_hour';
if(!is_dir($path)) return; if(!is_dir($path)) {
log_write('系统计划任务执行店铺按小时统计异常:目录不存在' . $path . ',请检查目录是否存在');
return;
}
$result = $this->scanFile($path); $result = $this->scanFile($path);
if(empty($result)) return; if(empty($result)) {
log_write('系统计划任务执行店铺按小时统计异常:目录下无文件' . $path . ',请检查是否有文件存在');
return;
}
try { try {
$json_array = []; $json_array = [];
@@ -145,8 +180,9 @@ class StatShop extends BaseModel
foreach ($json_array as $json_k => $json_v){ foreach ($json_array as $json_k => $json_v){
$system_stat->addStatShopHourModel($json_v); $system_stat->addStatShopHourModel($json_v);
} }
log_write('系统计划任务执行店铺按小时统计完成');
} catch (\Exception $e) { } catch (\Exception $e) {
log_write('系统计划任务执行店铺按小时统计异常:'.$e->getMessage());
} }
} }

View File

@@ -1,4 +1,5 @@
<?php <?php
namespace app\model\system; namespace app\model\system;
use app\dict\system\ScheduleDict; use app\dict\system\ScheduleDict;
@@ -15,7 +16,7 @@ use think\facade\Queue;
class Cron extends BaseModel class Cron extends BaseModel
{ {
public $time_diff = 60;//默认半个小时检测一次 public $time_diff = 60; //默认半个小时检测一次
/** /**
* 添加计划任务 * 添加计划任务
@@ -25,7 +26,7 @@ class Cron extends BaseModel
* @param string $event 执行事件 * @param string $event 执行事件
* @param int $execute_time 待执行时间 * @param int $execute_time 待执行时间
* @param int $relate_id 关联id * @param int $relate_id 关联id
* @param int $period_type 周期类型 * @param int $period_type 周期类型 0 分钟 1 天 2 周 3 月
*/ */
public function addCron($type, $period, $name, $event, $execute_time, $relate_id, $period_type = 0) public function addCron($type, $period, $name, $event, $execute_time, $relate_id, $period_type = 0)
{ {
@@ -40,6 +41,7 @@ class Cron extends BaseModel
'create_time' => time() 'create_time' => time()
]; ];
$res = model('cron')->add($data); $res = model('cron')->add($data);
log_write('添加计划任务:' . json_encode($data), 'debug');
return $this->success($res); return $this->success($res);
} }
@@ -59,44 +61,52 @@ class Cron extends BaseModel
*/ */
public function execute($type = 'default') 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; return true;
}
Log::write('计划任务方式'.$type); log_write('当前执行方式:' . $type, 'debug');
try {
//写入计划任务标记运行 //写入计划任务标记运行
$this->writeSchedule(); $this->writeSchedule();
$system_config_model = new SystemConfig(); $system_config_model = new SystemConfig();
$config = $system_config_model->getSystemConfig()[ 'data' ] ?? []; $config = $system_config_model->getSystemConfig()['data'] ?? [];
$is_open_queue = $config[ 'is_open_queue' ] ?? 0; $is_open_queue = $config['is_open_queue'] ?? 0;
$query_execute_time = $is_open_queue == 1 ? time() + 60 : time(); $query_execute_time = $is_open_queue == 1 ? time() + 60 : time();
$list = model('cron')->getList([ [ 'execute_time', '<=', $query_execute_time ] ]); $list = model('cron')->getList([['execute_time', '<=', $query_execute_time]]);
$now_time = time(); $now_time = time();
log_write('计划任务开始执行,查询计划任务列表', 'debug');
if (!empty($list)) { if (!empty($list)) {
foreach ($list as $k => $v) { foreach ($list as $k => $v) {
$event_res = checkQueue($v, function($params) { $event_res = checkQueue($v, function ($params) {
//加入消息队列 //加入消息队列
$job_handler_classname = 'Cronexecute'; $job_handler_classname = 'Cronexecute';
try { try {
if ($params[ 'execute_time' ] <= time()) { if ($params['execute_time'] <= time()) {
Queue::push($job_handler_classname, $params); Queue::push($job_handler_classname, $params);
} else { } else {
Queue::later($params[ 'execute_time' ] - time(), $job_handler_classname, $params); Queue::later($params['execute_time'] - time(), $job_handler_classname, $params);
} }
} catch (\Exception $e) { } catch (\Exception $e) {
$res = $this->error($e->getMessage(), $e->getMessage()); $res = $this->error($e->getMessage(), $e->getMessage());
} }
return $res ?? $this->success(); return $res ?? $this->success();
}, function($params) { }, function ($params) {
try { try {
$res = event($params[ 'event' ], [ 'relate_id' => $params[ 'relate_id' ] ]); log_write('调用事件名称:' . $params['name'], 'debug');
$res = event($params['event'], ['relate_id' => $params['relate_id']]);
} catch (\Exception $e) { } catch (\Exception $e) {
$res = $this->error($e->getMessage(), $e->getMessage()); $res = $this->error($e->getMessage(), $e->getMessage());
} }
$data_log = [ $data_log = [
'name' => $params[ 'name' ], 'name' => $params['name'],
'event' => $params[ 'event' ], 'event' => $params['event'],
'relate_id' => $params[ 'relate_id' ], 'relate_id' => $params['relate_id'],
'message' => json_encode($res) 'message' => json_encode($res)
]; ];
$this->addCronLog($data_log); $this->addCronLog($data_log);
@@ -104,42 +114,45 @@ class Cron extends BaseModel
}); });
//定义最新的执行时间或错误 //定义最新的执行时间或错误
$event_code = $event_res[ 'code' ] ?? 0; $event_code = $event_res['code'] ?? 0;
if ($event_code < 0) { if ($event_code < 0) {
Log::write($event_res); Log::write($event_res);
continue; continue;
} }
//循环任务 //循环任务
if ($v[ 'type' ] == 2) { if ($v['type'] == 2) {
$period = $v[ 'period' ] == 0 ? 1 : $v[ 'period' ]; $period = $v['period'] == 0 ? 1 : $v['period'];
switch ( $v[ 'period_type' ] ) { switch ($v['period_type']) {
case 0:// case 0: //分
$execute_time = $now_time + $period * 60; $execute_time = $now_time + $period * 60;
break; break;
case 1:// case 1: //天
$execute_time = strtotime('+' . $period . 'day', $v[ 'execute_time' ]); $execute_time = strtotime('+' . $period . 'day', $v['execute_time']);
break; break;
case 2:// case 2: //周
$execute_time = strtotime('+' . $period . 'week', $v[ 'execute_time' ]); $execute_time = strtotime('+' . $period . 'week', $v['execute_time']);
break; break;
case 3:// case 3: //月
$execute_time = strtotime('+' . $period . 'month', $v[ 'execute_time' ]); $execute_time = strtotime('+' . $period . 'month', $v['execute_time']);
break; break;
} }
model('cron')->update([ 'execute_time' => $execute_time ], [ [ 'id', '=', $v[ 'id' ] ] ]); model('cron')->update(['execute_time' => $execute_time], [['id', '=', $v['id']]]);
} else { } else {
model('cron')->delete([ [ 'id', '=', $v[ 'id' ] ] ]); model('cron')->delete([['id', '=', $v['id']]]);
} }
} }
} }
// $this->setCron(); // $this->setCron();
return true; return true;
} catch (\Exception $e) {
log_write('计划任务执行异常<model/system/Cron/execute>' . $e->getMessage(), 'debug');
return true;
}
} }
/** /**
@@ -150,8 +163,8 @@ class Cron extends BaseModel
public function addCronLog($data) public function addCronLog($data)
{ {
// 日常不需要添加,调试使用 // 日常不需要添加,调试使用
// $data[ 'execute_time' ] = time(); $data['execute_time'] = time();
// model('cron_log')->add($data); model('cron_log')->add($data);
return $this->success(); return $this->success();
} }
@@ -167,22 +180,22 @@ class Cron extends BaseModel
if (empty($cron_cache)) { if (empty($cron_cache)) {
//todo 不存在缓存标识,并不视为任务停止 //todo 不存在缓存标识,并不视为任务停止
//创建缓存标识,当前时间填充 //创建缓存标识,当前时间填充
Cache::set('cron_cache', [ 'time' => $now_time, 'error' => '' ]); Cache::set('cron_cache', ['time' => $now_time, 'error' => '']);
} else { } else {
$time = $cron_cache[ 'time' ]; $time = $cron_cache['time'];
$error = $cron_cache[ 'error' ] ?? ''; $error = $cron_cache['error'] ?? '';
$attempts = $cron_cache[ 'attempts' ] ?? 0;//尝试次数 $attempts = $cron_cache['attempts'] ?? 0; //尝试次数
if (!empty($error) || ( $now_time - $time ) > $diff) { if (!empty($error) || ($now_time - $time) > $diff) {
$message = '自动任务已停止'; $message = '自动任务已停止';
if (!empty($error)) { if (!empty($error)) {
$message .= ',停止原因:' . $error; $message .= ',停止原因:' . $error;
} else { } else {
$system_config_model = new \app\model\system\SystemConfig(); $system_config_model = new \app\model\system\SystemConfig();
$config = $system_config_model->getSystemConfig()[ 'data' ] ?? []; $config = $system_config_model->getSystemConfig()['data'] ?? [];
$is_open_queue = $config[ 'is_open_queue' ] ?? 0; $is_open_queue = $config['is_open_queue'] ?? 0;
if (!$is_open_queue) {//如果不是消息队列的话,可以尝试异步调用一下 if (!$is_open_queue) { //如果不是消息队列的话,可以尝试异步调用一下
if ($attempts < 1) { 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'); $url = url('cron/task/execute');
http($url, 1); http($url, 1);
return $this->success(); return $this->success();
@@ -194,10 +207,8 @@ class Cron extends BaseModel
//判断任务是 消息队列自动任务,还是默认睡眠sleep自动任务 //判断任务是 消息队列自动任务,还是默认睡眠sleep自动任务
return $this->error([], $message); return $this->error([], $message);
} }
} }
return $this->success(); return $this->success();
} }
/** /**
@@ -211,14 +222,14 @@ class Cron extends BaseModel
if (empty($cron_cache)) { if (empty($cron_cache)) {
$cron_cache = []; $cron_cache = [];
} }
// $code = $params['code'] ?? 0; // $code = $params['code'] ?? 0;
// if($code < 0){ // if($code < 0){
// $error = $params['message'] ?? '位置的错误'; // $error = $params['message'] ?? '位置的错误';
// $cron_cache['error'] = $error; // $cron_cache['error'] = $error;
// } // }
$cron_cache[ 'time' ] = time(); $cron_cache['time'] = time();
$cron_cache[ 'attempts' ] = 0; $cron_cache['attempts'] = 0;
Cache::set('cron_cache', $cron_cache); Cache::set('cron_cache', $cron_cache);
return $this->success(); return $this->success();
} }
@@ -229,6 +240,7 @@ class Cron extends BaseModel
*/ */
public function checkSchedule() public function checkSchedule()
{ {
try {
$file = root_path('runtime') . '.schedule'; $file = root_path('runtime') . '.schedule';
if (file_exists($file)) { if (file_exists($file)) {
$time = file_get_contents($file); $time = file_get_contents($file);
@@ -236,20 +248,27 @@ class Cron extends BaseModel
return $this->success(); return $this->success();
} }
} }
$remark = '计划任务已停止!当前启动的任务方式:'.ScheduleDict::getType(config('cron.default')).'。';
$remark = 'Cron计划任务已停止当前启动的任务方式' . ScheduleDict::getType(config('cron.default')) . '。';
$error = self::getError(config('cron.default')); $error = self::getError(config('cron.default'));
if(!empty($error)){ if (!empty($error)) {
$remark .= $error; $remark .= json_encode($error);
} }
log_write('Cron计划任务校验计划任务是否正常运行计划任务异常异常信息' . json_encode($error) . ',文件路径:' . $file, 'warning');
return $this->error([], $remark); return $this->error([], $remark);
} catch (\Exception $e) {
log_write('Cron计划任务校验计划任务是否正常运行异常' . $e->getMessage() . ',异常行:' . $e->getLine() . ',文件路径:' . $file, 'error');
return $this->error([], '计划任务校验计划任务是否正常运行异常:' . $e->getMessage());
}
} }
/** /**
* 写入校验计划任务 * 写入校验计划任务
* @return true * @return true
*/ */
public function writeSchedule(){ public function writeSchedule()
$file = root_path('runtime').'.schedule'; {
$file = root_path('runtime') . '.schedule';
file_put_contents($file, time()); file_put_contents($file, time());
return true; return true;
} }
@@ -260,7 +279,8 @@ class Cron extends BaseModel
* @param $error * @param $error
* @return true * @return true
*/ */
public static function setError($type, $error = ''){ public static function setError($type, $error = '')
{
Cache::set('cron_error', [$type => $error]); Cache::set('cron_error', [$type => $error]);
return true; return true;
} }
@@ -270,9 +290,10 @@ class Cron extends BaseModel
* @param $type * @param $type
* @return mixed * @return mixed
*/ */
public static function getError($type = ''){ public static function getError($type = '')
{
$error = Cache::get('cron_error'); $error = Cache::get('cron_error');
if(!empty($type)) if (!empty($type))
return $error; return $error;
return $error[$type]; return $error[$type];
} }

View File

@@ -1,4 +1,5 @@
<?php <?php
namespace app\model\web; namespace app\model\web;
use app\model\system\Config as ConfigModel; use app\model\system\Config as ConfigModel;
@@ -60,7 +61,7 @@ class Config extends BaseModel
public function setCaptchaConfig($data, $site_id = 1, $app_module = 'shop') public function setCaptchaConfig($data, $site_id = 1, $app_module = 'shop')
{ {
$config = new ConfigModel(); $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; return $res;
} }
@@ -73,25 +74,38 @@ class Config extends BaseModel
public function getCaptchaConfig($site_id = 1, $app_module = 'shop') public function getCaptchaConfig($site_id = 1, $app_module = 'shop')
{ {
$config = new ConfigModel(); $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' ])) { if (empty($res['data']['value'])) {
$res[ 'data' ][ 'value' ] = [ $res['data']['value'] = [
'shop_login' => 1, 'shop_login' => 1,
'shop_reception_login' => 1, 'shop_reception_login' => 1,
'shop_reception_register' => 1 'shop_reception_register' => 1
]; ];
} else { } else {
if (isset($res[ 'data' ][ 'value' ][ 'shop_reception_login' ]) === false) { if (isset($res['data']['value']['shop_reception_login']) === false) {
$res[ 'data' ][ 'value' ][ 'shop_reception_login' ] = 1; $res['data']['value']['shop_reception_login'] = 1;
} }
if (isset($res[ 'data' ][ 'value' ][ 'shop_reception_register' ]) === false) { if (isset($res['data']['value']['shop_reception_register']) === false) {
$res[ 'data' ][ 'value' ][ 'shop_reception_register' ] = 1; $res['data']['value']['shop_reception_register'] = 1;
} }
} }
return $res; return $res;
} }
/**
* 获取支持的应用模块
* @return array
*/
public function getSupportAppModules()
{
return [
['value' => 'shop', 'label' => '商家'],
['value' => 'h5', 'label' => 'H5轻应用'],
['value' => 'weixin', 'label' => '微信小程序'],
];
}
/** /**
* 默认图上传配置 * 默认图上传配置
* @param $data * @param $data
@@ -101,31 +115,31 @@ class Config extends BaseModel
*/ */
public function setDefaultImg($data, $site_id = 0, $app_module = 'shop') 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)) { if (!empty($config_info)) {
$upload_model = new Upload(); $upload_model = new Upload();
if ($data[ 'goods' ] && $config_info[ 'goods' ] && $data[ 'goods' ] != $config_info[ 'goods' ]) { if ($data['goods'] && $config_info['goods'] && $data['goods'] != $config_info['goods']) {
$upload_model->deletePic($config_info[ 'goods' ], $site_id); $upload_model->deletePic($config_info['goods'], $site_id);
} }
if ($data[ 'head' ] && $config_info[ 'head' ] && $data[ 'head' ] != $config_info[ 'head' ]) { if ($data['head'] && $config_info['head'] && $data['head'] != $config_info['head']) {
$upload_model->deletePic($config_info[ 'head' ], $site_id); $upload_model->deletePic($config_info['head'], $site_id);
} }
if ($data[ 'store' ] && $config_info[ 'store' ] && $data[ 'store' ] != $config_info[ 'store' ]) { if ($data['store'] && $config_info['store'] && $data['store'] != $config_info['store']) {
$upload_model->deletePic($config_info[ 'store' ], $site_id); $upload_model->deletePic($config_info['store'], $site_id);
} }
if ($data[ 'article' ] && $config_info[ 'article' ] && $data[ 'article' ] != $config_info[ 'article' ]) { if ($data['article'] && $config_info['article'] && $data['article'] != $config_info['article']) {
$upload_model->deletePic($config_info[ 'article' ], $site_id); $upload_model->deletePic($config_info['article'], $site_id);
} }
if ($data[ 'kefu' ] && $config_info[ 'kefu' ] && $data[ 'kefu' ] != $config_info[ 'kefu' ]) { if ($data['kefu'] && $config_info['kefu'] && $data['kefu'] != $config_info['kefu']) {
$upload_model->deletePic($config_info[ 'kefu' ], $site_id); $upload_model->deletePic($config_info['kefu'], $site_id);
} }
if ($data[ 'phone' ] && $config_info[ 'phone' ] && $data[ 'phone' ] != $config_info[ 'phone' ]) { if ($data['phone'] && $config_info['phone'] && $data['phone'] != $config_info['phone']) {
$upload_model->deletePic($config_info[ 'phone' ], $site_id); $upload_model->deletePic($config_info['phone'], $site_id);
} }
} }
$config = new ConfigModel(); $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; return $res;
} }
@@ -138,9 +152,9 @@ class Config extends BaseModel
public function getDefaultImg($site_id, $app_model = 'shop') public function getDefaultImg($site_id, $app_model = 'shop')
{ {
$config = new ConfigModel(); $config = new ConfigModel();
$res = $config->getConfig([ [ 'site_id', '=', $site_id ], [ 'app_module', '=', $app_model ], [ 'config_key', '=', 'DEFAULT_IMAGE' ] ]); $res = $config->getConfig([['site_id', '=', $site_id], ['app_module', '=', $app_model], ['config_key', '=', 'DEFAULT_IMAGE']]);
if (empty($res[ 'data' ][ 'value' ])) { if (empty($res['data']['value'])) {
$res[ 'data' ][ 'value' ] = [ $res['data']['value'] = [
'goods' => 'public/static/img/default_img/square.png', 'goods' => 'public/static/img/default_img/square.png',
'head' => 'public/static/img/default_img/head.png', 'head' => 'public/static/img/default_img/head.png',
'store' => 'public/static/img/default_img/store.png', 'store' => 'public/static/img/default_img/store.png',
@@ -150,14 +164,14 @@ class Config extends BaseModel
]; ];
} }
if (empty($res[ 'data' ][ 'value' ][ 'head' ])) { if (empty($res['data']['value']['head'])) {
$res[ 'data' ][ 'value' ][ 'head' ] = 'public/static/img/default_img/head.png'; $res['data']['value']['head'] = 'public/static/img/default_img/head.png';
} }
if (empty($res[ 'data' ][ 'value' ][ 'article' ])) { if (empty($res['data']['value']['article'])) {
$res[ 'data' ][ 'value' ][ 'article' ] = 'public/static/img/default_img/article.png'; $res['data']['value']['article'] = 'public/static/img/default_img/article.png';
} }
if (empty($res[ 'data' ][ 'value' ][ 'store' ])) { if (empty($res['data']['value']['store'])) {
$res[ 'data' ][ 'value' ][ 'store' ] = 'public/static/img/default_img/store.png'; $res['data']['value']['store'] = 'public/static/img/default_img/store.png';
} }
return $res; return $res;
} }
@@ -173,7 +187,7 @@ class Config extends BaseModel
public function setCopyright($data, $site_id = 1, $app_model = 'shop') public function setCopyright($data, $site_id = 1, $app_model = 'shop')
{ {
$config = new ConfigModel(); $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; return $res;
} }
@@ -186,16 +200,16 @@ class Config extends BaseModel
public function getCopyright($site_id = 1, $app_module = 'shop') public function getCopyright($site_id = 1, $app_module = 'shop')
{ {
$config = new ConfigModel(); $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'); $auth_info = cache('auth_info_copyright');
if (empty($auth_info)) { if (empty($auth_info)) {
$upgrade_model = new Upgrade(); $upgrade_model = new Upgrade();
$auth_info = $upgrade_model->authInfo(); $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' ])) { if (empty($res['data']['value'])) {
$res[ 'data' ][ 'value' ] = [ $res['data']['value'] = [
'logo' => '', 'logo' => '',
'company_name' => '', 'company_name' => '',
'copyright_link' => '', 'copyright_link' => '',
@@ -206,16 +220,15 @@ class Config extends BaseModel
'market_supervision_url' => '' 'market_supervision_url' => ''
]; ];
} else { } else {
if (is_null($auth_info) || $auth_info[ 'code' ] != 0) { if (is_null($auth_info) || $auth_info['code'] != 0) {
$res[ 'data' ][ 'value' ][ 'logo' ] = ''; $res['data']['value']['logo'] = '';
$res[ 'data' ][ 'value' ][ 'company_name' ] = ''; $res['data']['value']['company_name'] = '';
$res[ 'data' ][ 'value' ][ 'copyright_link' ] = ''; $res['data']['value']['copyright_link'] = '';
$res[ 'data' ][ 'value' ][ 'copyright_desc' ] = ''; $res['data']['value']['copyright_desc'] = '';
} }
} }
// 检查是否授权 // 检查是否授权
$res[ 'data' ][ 'value' ][ 'auth' ] = true; $res['data']['value']['auth'] = true;
return $res; return $res;
} }
@@ -229,7 +242,7 @@ class Config extends BaseModel
public function setAuth($data, $site_id = 1, $app_model = 'shop') public function setAuth($data, $site_id = 1, $app_model = 'shop')
{ {
$config = new ConfigModel(); $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; return $res;
} }
@@ -240,9 +253,9 @@ class Config extends BaseModel
public function getAuth($site_id = 1, $app_module = 'shop') public function getAuth($site_id = 1, $app_module = 'shop')
{ {
$config = new ConfigModel(); $config = new ConfigModel();
$res = $config->getConfig([ [ 'site_id', '=', $site_id ], [ 'app_module', '=', $app_module ], [ 'config_key', '=', 'AUTH' ] ]); $res = $config->getConfig([['site_id', '=', $site_id], ['app_module', '=', $app_module], ['config_key', '=', 'AUTH']]);
if (empty($res[ 'data' ][ 'value' ])) { if (empty($res['data']['value'])) {
$res[ 'data' ][ 'value' ] = [ $res['data']['value'] = [
'code' => '', 'code' => '',
]; ];
} }
@@ -259,7 +272,7 @@ class Config extends BaseModel
public function setMapConfig($data, $site_id, $app_model = 'shop') public function setMapConfig($data, $site_id, $app_model = 'shop')
{ {
$config = new ConfigModel(); $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; return $res;
} }
@@ -273,16 +286,16 @@ class Config extends BaseModel
{ {
$config = new ConfigModel(); $config = new ConfigModel();
$res = $config->getConfig([['site_id', '=', $site_id], ['app_module', '=', $app_module], ['config_key', '=', 'MAP_CONFIG']]); $res = $config->getConfig([['site_id', '=', $site_id], ['app_module', '=', $app_module], ['config_key', '=', 'MAP_CONFIG']]);
if (empty($res[ 'data' ][ 'value' ])) { if (empty($res['data']['value'])) {
$res[ 'data' ][ 'value' ] = [ $res['data']['value'] = [
'tencent_map_key' => '2PJBZ-A263Q-SED5B-4SAAB-HCUQ5-DUFHE',//默认一个地图TB5BZ-FBRRX-2RJ4C-76SZY-TYQ3H-F4BFC 'tencent_map_key' => '2PJBZ-A263Q-SED5B-4SAAB-HCUQ5-DUFHE', //默认一个地图TB5BZ-FBRRX-2RJ4C-76SZY-TYQ3H-F4BFC
'wap_is_open' => 1, // 手机端是否开启定位 'wap_is_open' => 1, // 手机端是否开启定位
'wap_valid_time' => 5 // 手机端定位有效期/分钟过期后将重新获取定位信息0为不过期 'wap_valid_time' => 5 // 手机端定位有效期/分钟过期后将重新获取定位信息0为不过期
]; ];
} }
$res[ 'data' ][ 'value' ][ 'wap_is_open' ] = $res[ 'data' ][ 'value' ][ 'wap_is_open' ] ?? 1; $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']['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']['tencent_map_key'] = '2PJBZ-A263Q-SED5B-4SAAB-HCUQ5-DUFHE';
return $res; return $res;
} }
@@ -299,90 +312,174 @@ class Config extends BaseModel
} }
/** /**
* 设置AI第三平台提供的配置信息 * 通用AI相关配置获取方法
* @$data * @param int $site_id 站点ID, 默认为-1, 业务站点uniacid值与site_id保持一致
* @param int $site_id * @param string $app_module 应用模块, 默认为*,表示所有应用模块都生效
* @param string $app_module * @param string $config_key 配置键, 默认为空字符串, 表示获取通用配置信息
* @param array $common_config 通用配置信息, 默认为空数组
* @return array * @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(); $config = new ConfigModel();
$res = $config->setConfig($data, 'AI第三平台提供的配置信息', 1, [ [ 'site_id', '=', $site_id ], [ 'app_module', '=', $app_module ], [ 'config_key', '=', 'AI_PLATFORM_CONFIG' ] ]); $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, $data_desc, 1, [['site_id', '=', $site_id], ['app_module', '=', $app_module], ['config_key', '=', $config_key]]);
return $res; return $res;
} }
/** /**
* 获取AI第三平台提供的配置信息 * 设置AI第三平台提供的配置信息
* @param int $site_id * @param $data 配置信息
* @param string $app_module * @param int $site_id 站点ID, 默认为-1, 业务站点uniacid值与site_id保持一致
* @param string $app_module 应用模块, 默认为*, 表示所有应用模块都生效
* @return array * @return array
*/ */
public function getAIPlatformConfig($site_id = 1, $app_module = 'shop') public function setAIPlatformConfig($data, $site_id = -1, $app_module = '')
{ {
$config = new ConfigModel(); return $this->_setAIConfig($data, $site_id, $app_module, 'AI_PLATFORM_CONFIG', 'AI第三平台提供的配置信息');
$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;
} }
/** /**
* 设置AI智能客服配置信息 * 设置AI智能客服配置信息
* @param $data 配置信息 * @param $data 配置信息
* @param int $site_id * @param int $site_id 站点ID, 默认为1, 业务站点uniacid值与site_id保持一致
* @param string $app_module * @param string $app_module 应用模块, 默认为*, 表示所有应用模块都生效
* @return array * @return array
*/ */
public function setAIAgentServicesConfig($data, $site_id = 1, $app_module = 'shop') public function setAIAgentServicesConfig($data, $site_id = 1, $app_module = '*')
{ {
$config = new ConfigModel(); return $this->_setAIConfig($data, $site_id, $app_module, 'AI_AGENT_SERVICES_CONFIG', 'AI智能客服配置信息');
$res = $config->setConfig($data, 'AI配置信息', 1, [ [ 'site_id', '=', $site_id ], [ 'app_module', '=', $app_module ], [ 'config_key', '=', 'AI_AGENT_SERVICES_CONFIG' ] ]);
return $res;
} }
/** /**
* 获AI智能客服配置信息 * 获AI第三平台提供的通用配置信息
* @param int $site_id
* @param string $app_module
* @return array * @return array
*/ */
public function getAIAgentServicesConfig($site_id = 1, $app_module = 'shop') private function getCommonAIPlatformConfig()
{ {
$config = new ConfigModel(); // 默认ID
$res = $config->getConfig([ [ 'site_id', '=', $site_id ], [ 'app_module', '=', $app_module ], [ 'config_key', '=', 'AI_AGENT_SERVICES_CONFIG' ] ]); $id = time();
if (empty($res[ 'data' ][ 'value' ])) {
$res[ 'data' ][ 'value' ] = [ // 通用配置
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智能客服的通用配置信息
* @return array
*/
private function getCommonAIAgentServicesConfig()
{
return [
// 是否开启AI功能 // 是否开启AI功能
'enable' => false, 'enable' => false,
// 用户头像url // 用户头像url
'user_avatar' => [ 'user_avatar' => [
'en'=> '', 'en' => '',
'zh_CN' => '', 'zh_CN' => '',
], ],
// AI客服头像url // AI客服头像url
'ai_avatar' => [ 'ai_avatar' => [
'en'=> '', 'en' => '',
'zh_CN' => '', 'zh_CN' => '',
], ],
// AI客服姓名 // AI客服姓名
'ai_name' => [ 'ai_name' => [
'en'=> '', 'en' => '',
'zh_CN' => '', 'zh_CN' => '',
], ],
// 欢迎语, 也称为开场白,初始化消息 // 欢迎语, 也称为开场白,初始化消息
'welcome_messages' => [ 'welcome_messages' => [
'en'=> [], 'en' => [],
'zh_CN' => [], 'zh_CN' => [],
], ],
// 是否显示加载更多按钮 // 是否显示加载更多按钮
@@ -412,7 +509,7 @@ class Config extends BaseModel
// 输入的问题描述的字符长度限制 // 输入的问题描述的字符长度限制
'max_char_length_for_problem' => 500, 'max_char_length_for_problem' => 500,
// 是否支持显示更多工具面板 // 是否支持显示更多工具面板
'tool_panel_config' =>[ 'tool_panel_config' => [
'enable' => false, 'enable' => false,
'title' => [ 'title' => [
'en' => 'More Tools', 'en' => 'More Tools',
@@ -427,20 +524,20 @@ class Config extends BaseModel
'en' => 'Image', 'en' => 'Image',
'zh_CN' => '图片', 'zh_CN' => '图片',
], ],
'description' =>[ 'description' => [
'en'=> 'Upload image', 'en' => 'Upload image',
'zh_CN' => '上传图片', 'zh_CN' => '上传图片',
], ],
'icon' => 'icon-image', 'icon' => 'icon-image',
], ],
'video' =>[ 'video' => [
'enable' => false, 'enable' => false,
'name' => [ 'name' => [
'en' => 'Video', 'en' => 'Video',
'zh_CN' => '视频', 'zh_CN' => '视频',
], ],
'description' => [ 'description' => [
'en'=> 'Upload video', 'en' => 'Upload video',
'zh_CN' => '上传视频', 'zh_CN' => '上传视频',
], ],
'icon' => 'icon-video', 'icon' => 'icon-video',
@@ -452,22 +549,22 @@ class Config extends BaseModel
'zh_CN' => '文件', 'zh_CN' => '文件',
], ],
'description' => [ 'description' => [
'en'=> 'Upload file', 'en' => 'Upload file',
'zh_CN' => '上传文件', 'zh_CN' => '上传文件',
], ],
'icon' => 'icon-file', 'icon' => 'icon-file',
], ],
'voice' =>[ 'voice' => [
'enable' => false, 'enable' => false,
'name'=> [ 'name' => [
'en' => 'Voice', 'en' => 'Voice',
'zh_CN' => '语音', 'zh_CN' => '语音',
], ],
'description' =>[ 'description' => [
'en'=> 'Voice input', 'en' => 'Voice input',
'zh_CN' => '语音输入', 'zh_CN' => '语音输入',
], ],
'icon'=> 'icon-voice', 'icon' => 'icon-voice',
], ],
'location' => [ 'location' => [
'enable' => false, 'enable' => false,
@@ -476,16 +573,24 @@ class Config extends BaseModel
'zh_CN' => '位置', 'zh_CN' => '位置',
], ],
'description' => [ 'description' => [
'en'=> 'Location input', 'en' => 'Location input',
'zh_CN' => '位置输入', 'zh_CN' => '位置输入',
], ],
'icon'=> 'icon-location', 'icon' => 'icon-location',
], ],
], ],
] ]
]; ];
} }
return $res; /**
* 获得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') 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-~\/])+$/'; $search = '/^([hH][tT]{2}[pP]:\/\/|[hH][tT]{2}[pP][sS]:\/\/)(([A-Za-z0-9-~]+)\.)+([A-Za-z0-9-~\/])+$/';
if ($data[ 'deploy_way' ] == 'separate') { if ($data['deploy_way'] == 'separate') {
if (!preg_match($search, $data[ 'domain_name_h5' ])) { if (!preg_match($search, $data['domain_name_h5'])) {
return $this->error('', '请输入正确的域名地址'); return $this->error('', '请输入正确的域名地址');
} }
} }
// 默认部署,更新店铺域名 // 默认部署,更新店铺域名
if ($data[ 'deploy_way' ] == 'default') { if ($data['deploy_way'] == 'default') {
$this->setShopDomainConfig([ $this->setShopDomainConfig([
'domain_name' => __ROOT__ 'domain_name' => __ROOT__
], $site_id); ], $site_id);
} }
$config = new ConfigModel(); $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; return $res;
} }
@@ -524,9 +629,9 @@ class Config extends BaseModel
public function getH5DomainName($site_id = 1, $app_module = 'shop') public function getH5DomainName($site_id = 1, $app_module = 'shop')
{ {
$config = new ConfigModel(); $config = new ConfigModel();
$res = $config->getConfig([ [ 'site_id', '=', $site_id ], [ 'app_module', '=', $app_module ], [ 'config_key', '=', 'H5_DOMAIN_NAME' ] ]); $res = $config->getConfig([['site_id', '=', $site_id], ['app_module', '=', $app_module], ['config_key', '=', 'H5_DOMAIN_NAME']]);
if (empty($res[ 'data' ][ 'value' ])) { if (empty($res['data']['value'])) {
$res[ 'data' ][ 'value' ] = [ $res['data']['value'] = [
'domain_name_h5' => __ROOT__ . '/h5', 'domain_name_h5' => __ROOT__ . '/h5',
'deploy_way' => 'default' 'deploy_way' => 'default'
]; ];
@@ -544,7 +649,7 @@ class Config extends BaseModel
public function setDomainJumpConfig($data, $site_id = 1, $app_module = 'shop') public function setDomainJumpConfig($data, $site_id = 1, $app_module = 'shop')
{ {
$config = new ConfigModel(); $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; return $res;
} }
@@ -558,12 +663,12 @@ class Config extends BaseModel
{ {
$config = new ConfigModel(); $config = new ConfigModel();
$res = $config->getConfig([ $res = $config->getConfig([
[ 'site_id', '=', $site_id ], ['site_id', '=', $site_id],
[ 'app_module', '=', $app_module ], ['app_module', '=', $app_module],
[ 'config_key', '=', 'DOMAIN_JUMP_CONFIG' ] ['config_key', '=', 'DOMAIN_JUMP_CONFIG']
]); ]);
if (empty($res[ 'data' ][ 'value' ])) { if (empty($res['data']['value'])) {
$res[ 'data' ][ 'value' ] = [ $res['data']['value'] = [
'jump_type' => 3, // 1用户前台2商家后台3引导页 'jump_type' => 3, // 1用户前台2商家后台3引导页
]; ];
} }
@@ -580,19 +685,19 @@ class Config extends BaseModel
public function setPcDomainName($data, $site_id = 1, $app_module = 'shop') 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-~\/])+$/'; $search = '/^([hH][tT]{2}[pP]:\/\/|[hH][tT]{2}[pP][sS]:\/\/)(([A-Za-z0-9-~]+)\.)+([A-Za-z0-9-~\/])+$/';
if ($data[ 'deploy_way' ] == 'separate') { if ($data['deploy_way'] == 'separate') {
if (!preg_match($search, $data[ 'domain_name_pc' ])) { if (!preg_match($search, $data['domain_name_pc'])) {
return $this->error('', '请输入正确的域名地址'); return $this->error('', '请输入正确的域名地址');
} }
} }
// 默认部署,更新店铺域名 // 默认部署,更新店铺域名
if ($data[ 'deploy_way' ] == 'default') { if ($data['deploy_way'] == 'default') {
$this->setShopDomainConfig([ $this->setShopDomainConfig([
'domain_name' => __ROOT__ 'domain_name' => __ROOT__
], $site_id); ], $site_id);
} }
$config = new ConfigModel(); $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; return $res;
} }
@@ -605,19 +710,19 @@ class Config extends BaseModel
public function getPcDomainName($site_id = 1, $app_module = 'shop') public function getPcDomainName($site_id = 1, $app_module = 'shop')
{ {
$config = new ConfigModel(); $config = new ConfigModel();
$res = $config->getConfig([ [ 'site_id', '=', $site_id ], [ 'app_module', '=', $app_module ], [ 'config_key', '=', 'PC_DOMAIN_NAME' ] ]); $res = $config->getConfig([['site_id', '=', $site_id], ['app_module', '=', $app_module], ['config_key', '=', 'PC_DOMAIN_NAME']]);
if (empty($res[ 'data' ][ 'value' ])) { if (empty($res['data']['value'])) {
$res[ 'data' ][ 'value' ] = [ $res['data']['value'] = [
'domain_name_pc' => __ROOT__ . '/web', 'domain_name_pc' => __ROOT__ . '/web',
'deploy_way' => 'default' 'deploy_way' => 'default'
]; ];
} else { } else {
if ($res[ 'data' ][ 'value' ][ 'domain_name_pc' ] == '' || empty($res[ 'data' ][ 'value' ][ 'deploy_way' ]) || $res[ 'data' ][ 'value' ][ 'deploy_way' ] == 'default') { if ($res['data']['value']['domain_name_pc'] == '' || empty($res['data']['value']['deploy_way']) || $res['data']['value']['deploy_way'] == 'default') {
$res[ 'data' ][ 'value' ] = [ $res['data']['value'] = [
'domain_name_pc' => __ROOT__ . '/web' '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; return $res;
} }
@@ -632,7 +737,7 @@ class Config extends BaseModel
public function setHotSearchWords($data, $site_id, $app_module) public function setHotSearchWords($data, $site_id, $app_module)
{ {
$config = new ConfigModel(); $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; return $res;
} }
@@ -645,9 +750,9 @@ class Config extends BaseModel
public function getHotSearchWords($site_id, $app_module) public function getHotSearchWords($site_id, $app_module)
{ {
$config = new ConfigModel(); $config = new ConfigModel();
$res = $config->getConfig([ [ 'site_id', '=', $site_id ], [ 'app_module', '=', $app_module ], [ 'config_key', '=', 'GOODS_HOT_SEARCH_WORDS_CONFIG' ] ]); $res = $config->getConfig([['site_id', '=', $site_id], ['app_module', '=', $app_module], ['config_key', '=', 'GOODS_HOT_SEARCH_WORDS_CONFIG']]);
if (empty($res[ 'data' ][ 'value' ])) { if (empty($res['data']['value'])) {
$res[ 'data' ][ 'value' ] = [ $res['data']['value'] = [
'words' => '' 'words' => ''
]; ];
} }
@@ -664,7 +769,7 @@ class Config extends BaseModel
public function setGuessYouLike($data, $site_id, $app_module) public function setGuessYouLike($data, $site_id, $app_module)
{ {
$config = new ConfigModel(); $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; return $res;
} }
@@ -677,11 +782,11 @@ class Config extends BaseModel
public function getGuessYouLike($site_id, $app_module) public function getGuessYouLike($site_id, $app_module)
{ {
$config = new ConfigModel(); $config = new ConfigModel();
$res = $config->getConfig([ [ 'site_id', '=', $site_id ], [ 'app_module', '=', $app_module ], [ 'config_key', '=', 'GOODS_GUESS_YOU_LIKE_CONFIG' ] ]); $res = $config->getConfig([['site_id', '=', $site_id], ['app_module', '=', $app_module], ['config_key', '=', 'GOODS_GUESS_YOU_LIKE_CONFIG']]);
if (empty($res[ 'data' ][ 'value' ])) { if (empty($res['data']['value'])) {
$res[ 'data' ][ 'value' ] = [ $res['data']['value'] = [
'title' => '猜你喜欢', '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', 'sources' => 'sort',
'goodsIds' => [], 'goodsIds' => [],
'fontWeight' => false, '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; return $res;
} }
@@ -724,17 +829,17 @@ class Config extends BaseModel
public function getDiyAdv($site_id, $app_module) public function getDiyAdv($site_id, $app_module)
{ {
$config = new ConfigModel(); $config = new ConfigModel();
$res = $config->getConfig([ [ 'site_id', '=', $site_id ], [ 'app_module', '=', $app_module ], [ 'config_key', '=', 'DIY_STARTADV' ] ]); $res = $config->getConfig([['site_id', '=', $site_id], ['app_module', '=', $app_module], ['config_key', '=', 'DIY_STARTADV']]);
if (empty($res[ 'data' ][ 'value' ])){ if (empty($res['data']['value'])) {
$res[ 'data' ][ 'value' ] = [ $res['data']['value'] = [
'list' =>[ 'list' => [
[ [
'title'=>'启动广告', 'title' => '启动广告',
'link'=>[ 'link' => [
'name'=>'' 'name' => ''
], ],
'iconType'=>'img', 'iconType' => 'img',
'imageUrl'=>"public/static/ext/diyview/img/preview/advs_default.png" 'imageUrl' => "public/static/ext/diyview/img/preview/advs_default.png"
] ]
], ],
'advtype' => 1, 'advtype' => 1,
@@ -754,7 +859,7 @@ class Config extends BaseModel
public function setDiyAdv($data, $site_id, $app_module) public function setDiyAdv($data, $site_id, $app_module)
{ {
$config = new ConfigModel(); $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; return $res;
} }
@@ -768,14 +873,14 @@ class Config extends BaseModel
public function getDiyVr($site_id, $app_module) public function getDiyVr($site_id, $app_module)
{ {
$config = new ConfigModel(); $config = new ConfigModel();
$res = $config->getConfig([ [ 'site_id', '=', $site_id ], [ 'app_module', '=', $app_module ], [ 'config_key', '=', 'DIY_VR' ] ]); $res = $config->getConfig([['site_id', '=', $site_id], ['app_module', '=', $app_module], ['config_key', '=', 'DIY_VR']]);
if (empty($res[ 'data' ][ 'value' ])){ if (empty($res['data']['value'])) {
$res[ 'data' ][ 'value' ] = [ $res['data']['value'] = [
'title' => '工厂展示', 'title' => '工厂展示',
'url' => 'https://baidu.com', 'url' => 'https://baidu.com',
]; ];
} }
// $res[ 'data' ][ 'value' ][ 'nameLineMode' ] = $res[ 'data' ][ 'value' ][ 'nameLineMode' ] ?? 'single'; // 商品名称,单行、多行展示 // $res[ 'data' ][ 'value' ][ 'nameLineMode' ] = $res[ 'data' ][ 'value' ][ 'nameLineMode' ] ?? 'single'; // 商品名称,单行、多行展示
return $res; return $res;
} }
@@ -789,7 +894,7 @@ class Config extends BaseModel
public function setDiyVr($data, $site_id, $app_module) public function setDiyVr($data, $site_id, $app_module)
{ {
$config = new ConfigModel(); $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; return $res;
} }
@@ -804,7 +909,7 @@ class Config extends BaseModel
public function setGoodsListConfig($data, $site_id, $app_module) public function setGoodsListConfig($data, $site_id, $app_module)
{ {
$config = new ConfigModel(); $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; return $res;
} }
@@ -817,9 +922,9 @@ class Config extends BaseModel
public function getGoodsListConfig($site_id, $app_module) public function getGoodsListConfig($site_id, $app_module)
{ {
$config = new ConfigModel(); $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 = [ $data = [
'fontWeight' => false, 'fontWeight' => false,
'padding' => 10, '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; return $res;
} }
@@ -862,7 +967,7 @@ class Config extends BaseModel
public function setDefaultSearchWords($data, $site_id, $app_module) public function setDefaultSearchWords($data, $site_id, $app_module)
{ {
$config = new ConfigModel(); $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; return $res;
} }
@@ -875,9 +980,9 @@ class Config extends BaseModel
public function getDefaultSearchWords($site_id, $app_module) public function getDefaultSearchWords($site_id, $app_module)
{ {
$config = new ConfigModel(); $config = new ConfigModel();
$res = $config->getConfig([ [ 'site_id', '=', $site_id ], [ 'app_module', '=', $app_module ], [ 'config_key', '=', 'GOODS_DEFAULT_SEARCH_WORDS_CONFIG' ] ]); $res = $config->getConfig([['site_id', '=', $site_id], ['app_module', '=', $app_module], ['config_key', '=', 'GOODS_DEFAULT_SEARCH_WORDS_CONFIG']]);
if (empty($res[ 'data' ][ 'value' ])) { if (empty($res['data']['value'])) {
$res[ 'data' ][ 'value' ] = [ $res['data']['value'] = [
'words' => '搜索 商品' 'words' => '搜索 商品'
]; ];
} }
@@ -894,7 +999,7 @@ class Config extends BaseModel
public function setGoodsSort($data, $site_id, $app_module) public function setGoodsSort($data, $site_id, $app_module)
{ {
$config = new ConfigModel(); $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; return $res;
} }
@@ -907,9 +1012,9 @@ class Config extends BaseModel
public function getGoodsSort($site_id, $app_module = 'shop') public function getGoodsSort($site_id, $app_module = 'shop')
{ {
$config = new ConfigModel(); $config = new ConfigModel();
$res = $config->getConfig([ [ 'site_id', '=', $site_id ], [ 'app_module', '=', $app_module ], [ 'config_key', '=', 'GOODS_SORT_CONFIG' ] ]); $res = $config->getConfig([['site_id', '=', $site_id], ['app_module', '=', $app_module], ['config_key', '=', 'GOODS_SORT_CONFIG']]);
if (empty($res[ 'data' ][ 'value' ])) { if (empty($res['data']['value'])) {
$res[ 'data' ][ 'value' ] = [ $res['data']['value'] = [
'type' => 'asc', 'type' => 'asc',
'default_value' => 100 'default_value' => 100
]; ];
@@ -927,7 +1032,7 @@ class Config extends BaseModel
public function setCategoryConfig($data, $site_id = 1, $app_module = 'shop') public function setCategoryConfig($data, $site_id = 1, $app_module = 'shop')
{ {
$config = new ConfigModel(); $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; return $res;
} }
@@ -940,9 +1045,9 @@ class Config extends BaseModel
public function getCategoryConfig($site_id = 1, $app_module = 'shop') public function getCategoryConfig($site_id = 1, $app_module = 'shop')
{ {
$config = new ConfigModel(); $config = new ConfigModel();
$res = $config->getConfig([ [ 'site_id', '=', $site_id ], [ 'app_module', '=', $app_module ], [ 'config_key', '=', 'SHOP_CATEGORY_CONFIG' ] ]); $res = $config->getConfig([['site_id', '=', $site_id], ['app_module', '=', $app_module], ['config_key', '=', 'SHOP_CATEGORY_CONFIG']]);
if (empty($res[ 'data' ][ 'value' ])) { if (empty($res['data']['value'])) {
$res[ 'data' ][ 'value' ] = [ $res['data']['value'] = [
'category' => 1, 'category' => 1,
'img' => 1 'img' => 1
]; ];
@@ -960,7 +1065,7 @@ class Config extends BaseModel
public function setGoodsDetailConfig($data, $site_id, $app_module = 'shop') public function setGoodsDetailConfig($data, $site_id, $app_module = 'shop')
{ {
$config = new ConfigModel(); $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; return $res;
} }
@@ -973,9 +1078,9 @@ class Config extends BaseModel
public function getGoodsDetailConfig($site_id, $app_module = 'shop') public function getGoodsDetailConfig($site_id, $app_module = 'shop')
{ {
$config = new ConfigModel(); $config = new ConfigModel();
$res = $config->getConfig([ [ 'site_id', '=', $site_id ], [ 'app_module', '=', $app_module ], [ 'config_key', '=', 'GOODS_DETAIL_CONFIG' ] ]); $res = $config->getConfig([['site_id', '=', $site_id], ['app_module', '=', $app_module], ['config_key', '=', 'GOODS_DETAIL_CONFIG']]);
if (empty($res[ 'data' ][ 'value' ])) { if (empty($res['data']['value'])) {
$res[ 'data' ][ 'value' ] = [ $res['data']['value'] = [
'nav_bar_switch' => 0, // 是否透明0不透明1透明 'nav_bar_switch' => 0, // 是否透明0不透明1透明
'introduction_color' => '#303133', 'introduction_color' => '#303133',
]; ];
@@ -993,7 +1098,7 @@ class Config extends BaseModel
public function setShopDomainConfig($data, $site_id = 1, $app_module = 'shop') public function setShopDomainConfig($data, $site_id = 1, $app_module = 'shop')
{ {
$config = new ConfigModel(); $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; return $res;
} }
@@ -1006,12 +1111,12 @@ class Config extends BaseModel
public function getShopDomainConfig($site_id = 1, $app_module = 'shop') public function getShopDomainConfig($site_id = 1, $app_module = 'shop')
{ {
$config = new ConfigModel(); $config = new ConfigModel();
$res = $config->getConfig([ [ 'site_id', '=', $site_id ], [ 'app_module', '=', $app_module ], [ 'config_key', '=', 'SHOP_DOMAIN_CONFIG' ] ]); $res = $config->getConfig([['site_id', '=', $site_id], ['app_module', '=', $app_module], ['config_key', '=', 'SHOP_DOMAIN_CONFIG']]);
if (empty($res[ 'data' ][ 'value' ])) { if (empty($res['data']['value'])) {
$res[ 'data' ][ 'value' ] = [ $res['data']['value'] = [
'domain_name' => __ROOT__, 'domain_name' => __ROOT__,
]; ];
$this->setShopDomainConfig($res[ 'data' ][ 'value' ], $site_id); $this->setShopDomainConfig($res['data']['value'], $site_id);
} }
return $res; return $res;
} }
@@ -1019,11 +1124,11 @@ class Config extends BaseModel
{ {
$qq_map = new \app\model\map\QqMap(['key' => $tencent_map_key]); $qq_map = new \app\model\map\QqMap(['key' => $tencent_map_key]);
$res = $qq_map->ipToDetail([ $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 (!empty($res)) {
if ($res[ 'status' ] != 0 && $type == 0) { if ($res['status'] != 0 && $type == 0) {
$res[ 'message' ] = '腾讯地图配置错误,无法定位地址'; $res['message'] = '腾讯地图配置错误,无法定位地址';
} }
} }
return $res; return $res;
@@ -1059,7 +1164,7 @@ class Config extends BaseModel
public function setGoodsNo($data, $site_id, $app_module) public function setGoodsNo($data, $site_id, $app_module)
{ {
$config = new ConfigModel(); $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; return $res;
} }
@@ -1072,9 +1177,9 @@ class Config extends BaseModel
public function getGoodsNo($site_id, $app_module = 'shop') public function getGoodsNo($site_id, $app_module = 'shop')
{ {
$config = new ConfigModel(); $config = new ConfigModel();
$res = $config->getConfig([ [ 'site_id', '=', $site_id ], [ 'app_module', '=', $app_module ], [ 'config_key', '=', 'GOODS_NO_CONFIG' ] ]); $res = $config->getConfig([['site_id', '=', $site_id], ['app_module', '=', $app_module], ['config_key', '=', 'GOODS_NO_CONFIG']]);
if (empty($res[ 'data' ][ 'value' ])) { if (empty($res['data']['value'])) {
$res[ 'data' ][ 'value' ] = [ $res['data']['value'] = [
'uniqueness_switch' => 1, 'uniqueness_switch' => 1,
]; ];
} }

View File

@@ -123,12 +123,11 @@ class BaseShop extends Controller
$config_view = Config::get('view'); $config_view = Config::get('view');
$config_view[ 'tpl_replace_string' ] = array_merge($config_view[ 'tpl_replace_string' ], $this->replace); $config_view[ 'tpl_replace_string' ] = array_merge($config_view[ 'tpl_replace_string' ], $this->replace);
Config::set($config_view, 'view'); Config::set($config_view, 'view');
// 其他操作
if (!request()->isAjax()) { if (!request()->isAjax()) {
$this->initBaseInfo(); $this->initBaseInfo();
$this->loadTemplate(); $this->loadTemplate();
} }
} }
/** /**

View File

@@ -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');
}
}
/** /**
* 客服配置 * 客服配置
*/ */

View File

@@ -35,7 +35,6 @@ class Index extends BaseShop
*/ */
public function index() public function index()
{ {
$this->assign('shop_status', 1); $this->assign('shop_status', 1);
$this->handlePromotion(); $this->handlePromotion();

View File

@@ -1,617 +0,0 @@
<!-- 页面样式定义 -->
<style>
.layui-card {
margin: 20px 0;
border-radius: 4px;
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
}
.layui-card-header {
background-color: #f2f2f2;
padding: 12px 20px;
font-weight: 500;
border-bottom: 1px solid #e6e6e6;
}
.layui-card-body {
padding: 20px;
}
.layui-form-item {
margin-bottom: 20px;
}
.layui-field-title {
margin-top: 25px;
margin-bottom: 20px;
}
.layui-field-title legend {
padding: 0 10px;
font-size: 16px;
font-weight: 500;
}
.word-aux {
color: #999;
font-size: 12px;
margin-top: 5px;
display: block;
}
.required {
color: #f00;
margin-right: 4px;
}
.form-wrap {
padding: 20px;
background: #fff;
min-height: calc(100vh - 40px);
}
.lang-tabs {
margin-bottom: 20px;
}
.lang-content {
display: none;
}
.lang-content.active {
display: block;
}
.layui-form-label {
padding: 9px 15px;
width: 110px;
}
.layui-input-block {
margin-left: 110px;
}
.all-shop-information {
width: 100%;
background: white;
padding: 15px;
box-sizing: border-box;
margin-top: 15px;
margin-bottom: 30px;
}
.all-shop-information .all-top {
display: flex;
align-items: center;
justify-content: space-between;
}
.all-shop-information .all-top .title {
color: #333333;
margin-bottom: 0;
font-size: 17px;
padding-left: 10px;
border-left: 3px solid var(--base-color);
box-sizing: border-box;
}
select[name="selected_platform"] + .layui-form-select {
min-width: 400px;
max-width: fit-content;
}
/* 扁平化表格样式 */
.layui-table {
margin: 10px 0;
border-radius: 0;
box-shadow: none;
}
.layui-table th {
font-weight: 400;
background-color: #f5f7fa;
border-bottom: 1px solid #e6e8eb;
text-align: center;
}
.layui-table td {
vertical-align: top;
border-bottom: 1px solid #e6e8eb;
padding: 12px 15px;
}
.layui-table tbody tr:hover {
background-color: #f5f7fa;
}
/* 调整类型列宽度 */
.layui-table th:nth-child(2),
.layui-table td:nth-child(2) {
width: 120px;
}
/* 调整输入框样式 */
.layui-table-edit {
width: calc(100% - 4px);
border-radius: 2px;
border: 1px solid #dcdfe6;
transition: border-color 0.2s;
/* 确保输入框和选择框垂直居中对齐 */
line-height: 30px;
padding: 0 10px;
box-sizing: border-box;
margin: 4px;
margin-top: 12px;
height: 34px;
}
.layui-table-edit:focus {
border-color: #c0c4cc;
outline: none;
}
.layui-table td select.layui-select {
height: 30px;
line-height: 30px;
padding: 0 10px;
box-sizing: border-box;
}
/* 调整操作列宽度 */
.layui-table th:last-child,
.layui-table td:last-child {
width: 120px;
white-space: nowrap;
overflow: hidden;
}
/* 按钮图标样式 */
.btn-icon {
width: 28px;
height: 28px;
line-height: 28px;
padding: 0;
text-align: center;
margin: 0 2px;
font-size: 14px;
}
</style>
<div class="layui-form">
<!-- 根据功能分两个Tab进行配置一个是AI智能客服一个是AI平台 -->
<div class="layui-tab" lay-filter="mainTab">
<ul class="layui-tab-title">
<li class="layui-this" lay-id="ai_agent">AI智能客服</li>
<li lay-id="ai_platform">AI服务平台</li>
</ul>
<div class="layui-tab-content">
<div class="layui-tab-item layui-show">
<!-- AI智能客服配置 -->
<div class="layui-form form-wrap card-common">
<!-- 基础设置 -->
<fieldset class="layui-elem-field layui-field-title">
<legend>基础设置</legend>
</fieldset>
<div class="layui-form-item">
<label class="layui-form-label">启用状态</label>
<div class="layui-input-block">
<input type="checkbox" name="enable" lay-skin="switch" lay-text="开启|关闭" {if
condition="isset($agent_info) && $agent_info.enable" }checked{/if}>
</div>
</div>
<!-- 语言切换 -->
<div class="layui-form-item">
<div class="layui-tab lang-tabs" lay-filter="langTab">
<ul class="layui-tab-title">
<li class="layui-this" lay-id="zh_CN">简体中文</li>
<li lay-id="en">English</li>
</ul>
<div class="layui-tab-content">
<div class="layui-tab-item layui-show">
<div class="layui-form-item">
<label class="layui-form-label">AI客服姓名</label>
<div class="layui-input-block">
<input type="text" name="ai_name[zh_CN]" placeholder="请输入AI客服姓名"
class="layui-input" value="{$agent_info.ai_name.zh_CN|default=''}">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">AI客服头像</label>
<div class="layui-input-block">
<input type="text" name="ai_avatar[zh_CN]" placeholder="请输入AI客服头像URL"
class="layui-input" value="{$agent_info.ai_avatar.zh_CN|default=''}">
<div class="word-aux">支持JPG、PNG、GIF格式建议尺寸100x100px</div>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">用户头像</label>
<div class="layui-input-block">
<input type="text" name="user_avatar[zh_CN]" placeholder="请输入用户头像URL"
class="layui-input" value="{$agent_info.user_avatar.zh_CN|default=''}">
<div class="word-aux">支持JPG、PNG、GIF格式建议尺寸100x100px</div>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">欢迎语</label>
<div class="layui-input-block">
<textarea name="welcome_messages[zh_CN]" placeholder="请输入欢迎语,每行一条"
class="layui-textarea" rows="4">{if isset($agent_info.welcome_messages.zh_CN)}{foreach $agent_info.welcome_messages.zh_CN as $msg}{$msg}
{/foreach}{else}您好,我是智能客服助手,有什么可以帮助您的吗?{/if}</textarea>
<!-- <div class="word-aux">每行一条欢迎语,系统会随机选择一条显示</div> -->
</div>
</div>
</div>
<div class="layui-tab-item">
<div class="layui-form-item">
<label class="layui-form-label">AI Name</label>
<div class="layui-input-block">
<input type="text" name="ai_name[en]" placeholder="Please enter AI name"
class="layui-input" value="{$agent_info.ai_name.en|default=''}">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">AI Avatar</label>
<div class="layui-input-block">
<input type="text" name="ai_avatar[en]"
placeholder="Please enter AI avatar URL" class="layui-input"
value="{$agent_info.ai_avatar.en|default=''}">
<div class="word-aux">Support JPG, PNG, GIF formats, recommended size:
100x100px</div>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">User Avatar</label>
<div class="layui-input-block">
<input type="text" name="user_avatar[en]"
placeholder="Please enter user avatar URL" class="layui-input"
value="{$agent_info.user_avatar.en|default=''}">
<div class="word-aux">Support JPG, PNG, GIF formats, recommended size:
100x100px</div>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">Welcome Messages</label>
<div class="layui-input-block">
<textarea name="welcome_messages[en]"
placeholder="Please enter welcome messages, one per line"
class="layui-textarea" rows="4">{if isset($agent_info.welcome_messages.en)}{foreach $agent_info.welcome_messages.en as $msg}{$msg}
{/foreach}{else}Hello, I'm the AI customer service assistant. How can I help you?{/if}</textarea>
<!-- <div class="word-aux">One welcome message per line, the system will randomly select one to display</div> -->
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 显示设置 -->
<fieldset class="layui-elem-field layui-field-title">
<legend>显示设置</legend>
</fieldset>
<div class="layui-form-item">
<label class="layui-form-label">显示加载更多按钮</label>
<div class="layui-input-block">
<input type="checkbox" name="show_load_more_button" lay-skin="switch" lay-text="开启|关闭" {if
condition="isset($agent_info) && $agent_info.show_load_more_button"
}checked{else}checked{/if}>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">最大消息数量</label>
<div class="layui-input-inline" style="width: 100px;">
<input type="number" name="max_messages" value="{$agent_info.max_messages|default='100'}"
class="layui-input" min="10" max="500">
</div>
<div class="layui-form-mid"></div>
<div class="word-aux" style="margin-left: 110px;">建议设置在10-500条之间</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">启用流式响应</label>
<div class="layui-input-block">
<input type="checkbox" name="stream_mode" lay-skin="switch" lay-text="开启|关闭" {if
condition="isset($agent_info) && $agent_info.stream_mode" }checked{else}checked{/if}>
</div>
</div>
</div>
</div>
<div class="layui-tab-item" lay-id="ai_platform">
<!-- AI平台配置 -->
<div class="layui-form form-wrap card-common">
<!-- 平台选择 -->
<fieldset class="layui-elem-field layui-field-title">
<legend>基本设置</legend>
</fieldset>
<div class="layui-form-item">
<label class="layui-form-label">默认平台</label>
<div class="layui-input-block">
<select name="selected_platform" lay-filter="platformSelect">
<option value="">请选择要默认使用的AI服务平台</option>
{if isset($enable_platform_list) && !empty($enable_platform_list)}
{foreach $enable_platform_list as $platform}
<option value="{$platform.id}">{$platform.name}</option>
{/foreach}
{/if}
</select>
</div>
</div>
<!-- 平台配置表格 -->
<fieldset class="layui-elem-field layui-field-title">
<legend>AI服务平台配置</legend>
</fieldset>
<div class="layui-form-item">
<table class="layui-table" lay-even lay-skin="line">
<thead>
<tr>
<th>名称</th>
<th>类型</th>
<th>API URL</th>
<th>API Key</th>
<th>操作</th>
</tr>
</thead>
<tbody id="platformTable">
{if isset($platform_list) && !empty($platform_list)}
{foreach $platform_list as $platform}
<tr data-id="{$platform.id}">
<td>
<input type="text" name="platform_name[{$platform.id}]" value="{$platform.name}"
class="layui-input layui-table-edit" placeholder="请输入AI服务平台名称">
</td>
<td>
<select name="platform_type[{$platform.id}]" class="layui-select layui-table-edit">
{foreach $support_ai_platform_types as $item}
<option value="{$item.value}" {if $platform.type == $item.value}selected{/if}>{$item.label}</option>
{/foreach}
</select>
</td>
<td>
<input type="text" name="platform_api_url[{$platform.id}]" value="{$platform.api_url}"
class="layui-input layui-table-edit" placeholder="请输入API URL">
</td>
<td>
<input type="text" name="platform_api_key[{$platform.id}]" value="{$platform.api_key}"
class="layui-input layui-table-edit" placeholder="请输入API Key">
</td>
<td class="layui-row">
<button class="layui-col-xs4 layui-col-sm12 layui-col-md4 layui-btn layui-btn-xs layui-btn-danger btn-icon delete-platform" data-id="{$platform.id}" title="删除">
<i class="layui-icon layui-icon-delete"></i>
</button>
<button class="layui-col-xs4 layui-col-sm12 layui-col-md4 layui-btn layui-btn-xs layui-btn-warm btn-icon toggle-platform" data-id="{$platform.id}" data-status="{if $platform.enable === 1}1{else}0{/if}" title="{if $platform.enable === 1}禁用{else}开启{/if}">
{if $platform.enable === 1}<i class="layui-icon layui-icon-radio"></i>{else}<i class="layui-icon layui-icon-circle"></i>{/if}
</button>
<button class="layui-col-xs4 layui-col-sm12 layui-col-md4 layui-btn layui-btn-xs btn-icon save-platform" data-id="{$platform.id}" title="保存">
<i class="layui-icon layui-icon-ok"></i>
</button>
</td>
</tr>
{/foreach}
{else}
<tr>
<td colspan="5" style="text-align: center;">暂无平台配置</td>
</tr>
{/if}
</tbody>
</table>
</div>
<!-- 添加新平台 -->
<div class="layui-form-item">
<label class="layui-form-label"></label>
<div class="layui-input-block">
<button class="layui-btn" id="addPlatform">添加平台</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 页面脚本 -->
<script>
layui.use(['form', 'layer', 'element'], function () {
var form = layui.form;
var layer = layui.layer;
var element = layui.element;
// 初始化表单
form.render();
// layui 2.5.5版本需要手动初始化Tab组件
element.render('tab');
// 主Tab切换处理
element.on('tab(mainTab)', function (data) { });
// 语言切换 - 使用layui原生的Tab切换不再需要自定义逻辑
element.on('tab(langTab)', function (data) { });
// 平台选择下拉框处理
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');
}
});
// 保存平台配置
$(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_api_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;
}
// 这里应该是AJAX请求保存数据
// 模拟保存成功并获取新的平台ID
var newPlatformId = platformId.startsWith('temp_') ? 'new_' + new Date().getTime() : 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');
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('<option value="' + newPlatformId + '">' + name + '</option>');
form.render('select'); // 重新渲染select
}
// 显示保存成功提示
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('<tr><td colspan="5" style="text-align: center;">暂无平台配置</td></tr>');
}
layer.close(index);
layer.msg('删除成功', {icon: 1});
});
});
// 禁用/开启平台
$(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 ? '<i class="layui-icon layui-icon-radio"></i>' : '<i class="layui-icon layui-icon-circle"></i>');
$(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('<option value="' + platformId + '">' + platformName + '</option>');
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 = 'temp_' + new Date().getTime();
// 创建新行HTML
var newRow = '<tr data-id="' + tempId + '">' +
'<td>' +
'<input type="text" name="platform_name[' + tempId + ']" ' +
'class="layui-input layui-table-edit" placeholder="请输入AI服务平台名称">' +
'</td>' +
'<td>' +
'<select name="platform_type[' + tempId + ']" class="layui-select layui-table-edit">' +
'{foreach $support_ai_platform_types as $type}' +
'<option value="{$type.value}">{$type.label}</option>' +
'{/foreach}' +
'</select>' +
'</td>' +
'<td>' +
'<input type="text" name="platform_api_url[' + tempId + ']" ' +
'class="layui-input layui-table-edit" placeholder="请输入API URL">' +
'</td>' +
'<td>' +
'<input type="text" name="platform_api_key[' + tempId + ']" ' +
'class="layui-input layui-table-edit" placeholder="请输入API Key">' +
'</td>' +
'<td class="layui-row">' +
'<button class="layui-col-xs4 layui-col-sm12 layui-col-md4 layui-btn layui-btn-xs btn-icon delete-platform" data-id="' + tempId + '" title="删除">' +
'<i class="layui-icon layui-icon-delete"></i>' +
'</button>' +
'<button class="layui-col-xs4 layui-col-sm12 layui-col-md4 layui-btn layui-btn-xs btn-icon toggle-platform" data-id="' + tempId + '" data-status="0" title="开启">'+
'<i class="layui-icon layui-icon-circle"></i>' +
'</button>' +
'<button class="layui-col-xs4 layui-col-sm12 layui-col-md4 layui-btn layui-btn-xs btn-icon save-platform" data-id="' + tempId + '" title="保存">' +
'<i class="layui-icon layui-icon-ok"></i>' +
'</button>' +
'</td>' +
'</tr>'
// 检查是否有暂无平台配置的提示行
var emptyRow = $('#platformTable tr:has(td[colspan="5"])');
if (emptyRow.length > 0) {
emptyRow.replaceWith(newRow);
} else {
$('#platformTable').append(newRow);
}
// 重新渲染表单元素
form.render();
// 聚焦到新添加的行的第一个输入框
$('input[name="platform_name[' + tempId + ']"]').focus();
});
});
</script>

View File

@@ -0,0 +1,141 @@
<div class="layui-tab-item layui-show">
<!-- AI智能客服配置 -->
<div class="layui-form form-wrap card-common">
<!-- 基础设置 -->
<fieldset class="layui-elem-field layui-field-title">
<legend>基础设置</legend>
</fieldset>
<div class="layui-form-item">
<label class="layui-form-label">启用状态</label>
<div class="layui-input-block">
<input type="checkbox" name="enable" lay-skin="switch" lay-text="开启|关闭" {if
condition="isset($agent_info) && $agent_info.enable" }checked{/if}>
</div>
</div>
<!-- 语言切换 -->
<div class="layui-form-item">
<div class="layui-tab lang-tabs" lay-filter="langTab">
<ul class="layui-tab-title">
<li class="layui-this" lay-id="zh_CN">简体中文</li>
<li lay-id="en">English</li>
</ul>
<div class="layui-tab-content">
<div class="layui-tab-item layui-show">
<div class="layui-form-item">
<label class="layui-form-label">AI客服姓名</label>
<div class="layui-input-block">
<input type="text" name="ai_name[zh_CN]" placeholder="请输入AI客服姓名" class="layui-input"
value="{$agent_info.ai_name.zh_CN|default=''}">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">AI客服头像</label>
<div class="layui-input-block">
<input type="text" name="ai_avatar[zh_CN]" placeholder="请输入AI客服头像URL"
class="layui-input" value="{$agent_info.ai_avatar.zh_CN|default=''}">
<div class="word-aux">支持JPG、PNG、GIF格式建议尺寸100x100px</div>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">用户头像</label>
<div class="layui-input-block">
<input type="text" name="user_avatar[zh_CN]" placeholder="请输入用户头像URL"
class="layui-input" value="{$agent_info.user_avatar.zh_CN|default=''}">
<div class="word-aux">支持JPG、PNG、GIF格式建议尺寸100x100px</div>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">欢迎语</label>
<div class="layui-input-block">
<textarea name="welcome_messages[zh_CN]" placeholder="请输入欢迎语,每行一条"
class="layui-textarea" rows="4">{if isset($agent_info.welcome_messages.zh_CN)}{foreach $agent_info.welcome_messages.zh_CN as $msg}{$msg}
{/foreach}{else}您好,我是智能客服助手,有什么可以帮助您的吗?{/if}</textarea>
<!-- <div class="word-aux">每行一条欢迎语,系统会随机选择一条显示</div> -->
</div>
</div>
</div>
<div class="layui-tab-item">
<div class="layui-form-item">
<label class="layui-form-label">AI Name</label>
<div class="layui-input-block">
<input type="text" name="ai_name[en]" placeholder="Please enter AI name"
class="layui-input" value="{$agent_info.ai_name.en|default=''}">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">AI Avatar</label>
<div class="layui-input-block">
<input type="text" name="ai_avatar[en]" placeholder="Please enter AI avatar URL"
class="layui-input" value="{$agent_info.ai_avatar.en|default=''}">
<div class="word-aux">Support JPG, PNG, GIF formats, recommended size:
100x100px</div>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">User Avatar</label>
<div class="layui-input-block">
<input type="text" name="user_avatar[en]" placeholder="Please enter user avatar URL"
class="layui-input" value="{$agent_info.user_avatar.en|default=''}">
<div class="word-aux">Support JPG, PNG, GIF formats, recommended size:
100x100px</div>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">Welcome Messages</label>
<div class="layui-input-block">
<textarea name="welcome_messages[en]"
placeholder="Please enter welcome messages, one per line" class="layui-textarea"
rows="4">{if isset($agent_info.welcome_messages.en)}{foreach $agent_info.welcome_messages.en as $msg}{$msg}
{/foreach}{else}Hello, I'm the AI customer service assistant. How can I help you?{/if}</textarea>
<!-- <div class="word-aux">One welcome message per line, the system will randomly select one to display</div> -->
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 显示设置 -->
<fieldset class="layui-elem-field layui-field-title">
<legend>显示设置</legend>
</fieldset>
<div class="layui-form-item">
<label class="layui-form-label">显示加载更多按钮</label>
<div class="layui-input-block">
<input type="checkbox" name="show_load_more_button" lay-skin="switch" lay-text="开启|关闭" {if
condition="isset($agent_info) && $agent_info.show_load_more_button" }checked{else}checked{/if}>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">最大消息数量</label>
<div class="layui-input-inline" style="width: 100px; margin-left: 50px;">
<input type="number" name="max_messages" value="{$agent_info.max_messages|default=100}"
class="layui-input" min="10" max="500">
</div>
<div class="layui-form-mid"></div>
<div class="word-aux" style="margin-left: 110px;">建议设置在10-500条之间</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">启用流式响应</label>
<div class="layui-input-block">
<input type="checkbox" name="stream_mode" lay-skin="switch" lay-text="开启|关闭" {if
condition="isset($agent_info) && $agent_info.stream_mode" }checked{else}checked{/if}>
</div>
</div>
<!-- 独立的保存按钮 -->
<div class="layui-form-item">
<button class="layui-btn" lay-submit lay-filter="save_aiagent_cfg">保存</button>
</div>
</div>
</div>

View File

@@ -0,0 +1,209 @@
<!-- 页面样式定义 -->
<style>
.text-center {
text-align: center;
}
.layui-card {
margin: 20px 0;
border-radius: 4px;
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
}
.layui-card-header {
background-color: #f2f2f2;
padding: 12px 20px;
font-weight: 500;
border-bottom: 1px solid #e6e6e6;
}
.layui-card-body {
padding: 20px;
}
.layui-form-item {
margin-bottom: 20px;
}
.layui-field-title {
margin-top: 25px;
margin-bottom: 20px;
}
.layui-field-title legend {
padding: 0 10px;
font-size: 16px;
font-weight: 500;
}
.word-aux {
color: #999;
font-size: 12px;
margin-top: 5px;
display: block;
}
.required {
color: #f00;
margin-right: 4px;
}
.form-wrap {
padding: 20px;
background: #fff;
min-height: calc(100vh - 40px);
}
.lang-tabs {
margin-bottom: 20px;
}
.lang-content {
display: none;
}
.lang-content.active {
display: block;
}
.layui-form-label {
padding: 9px 15px;
width: 150px;
}
.layui-input-block {
margin-left: 150px;
}
.all-shop-information {
width: 100%;
background: white;
padding: 15px;
box-sizing: border-box;
margin-top: 15px;
margin-bottom: 30px;
}
.all-shop-information .all-top {
display: flex;
align-items: center;
justify-content: space-between;
}
.all-shop-information .all-top .title {
color: #333333;
margin-bottom: 0;
font-size: 17px;
padding-left: 10px;
border-left: 3px solid var(--base-color);
box-sizing: border-box;
}
select[name="selected_platform"] + .layui-form-select {
min-width: 400px;
max-width: fit-content;
}
/* 扁平化表格样式 */
.layui-table {
margin: 10px 0;
border-radius: 0;
box-shadow: none;
}
.layui-table th {
font-weight: 400;
background-color: #f5f7fa;
border-bottom: 1px solid #e6e8eb;
text-align: center;
}
.layui-table td {
vertical-align: top;
border-bottom: 1px solid #e6e8eb;
padding: 12px 15px;
}
.layui-table tbody tr:hover {
background-color: #f5f7fa;
}
/* 调整类型列宽度 */
.layui-table th:nth-child(2),
.layui-table td:nth-child(2) {
width: 120px;
}
/* 调整输入框样式 */
.layui-table-edit {
width: calc(100% - 4px);
border-radius: 2px;
border: 1px solid #dcdfe6;
transition: border-color 0.2s;
/* 确保输入框和选择框垂直居中对齐 */
line-height: 30px;
padding: 0 10px;
box-sizing: border-box;
margin: 4px;
margin-top: 12px;
height: 34px;
}
.layui-table-edit:focus {
border-color: #c0c4cc;
outline: none;
}
.layui-table td select.layui-select {
height: 30px;
line-height: 30px;
padding: 0 10px;
box-sizing: border-box;
}
/* 调整操作列宽度 */
.layui-table th:last-child,
.layui-table td:last-child {
width: 120px;
white-space: nowrap;
overflow: hidden;
}
/* 按钮图标样式 */
.btn-icon {
width: 28px;
height: 28px;
line-height: 28px;
padding: 0;
text-align: center;
margin: 0 2px;
font-size: 14px;
}
</style>
<div class="layui-form">
<!-- 根据功能分两个Tab进行配置一个是AI智能客服一个是AI平台 -->
<div class="layui-tab" lay-filter="mainTab">
<ul class="layui-tab-title">
<li class="layui-this" lay-id="ai_agent">AI智能客服</li>
<li lay-id="ai_platform">AI服务平台</li>
</ul>
<div class="layui-tab-content">
<!-- AI服务平台配置必须使用绝对路径 -->
{include file="app/shop/view/config/ai/platform.html" /}
</div>
</div>
</div>
<!-- 页面脚本 -->
<script>
// layui 2.5.5 版本
layui.use(['form', 'layer', 'element'], function () {
var form = layui.form;
var layer = layui.layer;
var element = layui.element;
// 初始化表单
form.render();
// layui 2.5.5版本需要手动初始化Tab组件
element.render('tab');
// 主Tab切换处理
element.on('tab(mainTab)', function (data) { });
// 语言切换 - 使用layui原生的Tab切换不再需要自定义逻辑
element.on('tab(langTab)', function (data) { });
});
</script>

View File

@@ -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('<option value="' + newPlatformId + '">' + name + '</option>');
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('<tr><td colspan="5" style="text-align: center;">暂无平台配置</td></tr>');
}
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 ? '<i class="layui-icon layui-icon-radio"></i>' : '<i class="layui-icon layui-icon-circle"></i>');
$(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('<option value="' + platformId + '">' + platformName + '</option>');
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 = '<tr data-id="' + tempId + '">' +
'<td>' +
'<input type="text" name="platform_name[' + tempId + ']" ' +
'class="layui-input layui-table-edit" placeholder="请输入AI服务平台名称">' +
'</td>' +
'<td>' +
'<select name="platform_type[' + tempId + ']" class="layui-select layui-table-edit">' +
'{foreach $support_ai_platform_types as $type}' +
'<option value="{$type.value}">{$type.label}</option>' +
'{/foreach}' +
'</select>' +
'</td>' +
'<td>' +
'<input type="text" name="platform_base_url[' + tempId + ']" ' +
'class="layui-input layui-table-edit" placeholder="请输入API URL">' +
'</td>' +
'<td>' +
'<input type="text" name="platform_api_key[' + tempId + ']" ' +
'class="layui-input layui-table-edit" placeholder="请输入API Key">' +
'</td>' +
'<td>' +
'<input type="text" name="platform_desc[' + tempId + ']" ' +
'class="layui-input layui-table-edit" placeholder="请输入描述">' +
'</td>' +
'<td class="layui-row">' +
'<button class="layui-col-xs6 layui-col-sm6 layui-col-md6 layui-btn layui-btn-xs btn-icon delete-platform" data-id="' + tempId + '" title="删除">' +
'<i class="layui-icon layui-icon-delete"></i>' +
'</button>' +
'<button class="layui-col-xs6 layui-col-sm6 layui-col-md6 layui-btn layui-btn-xs btn-icon toggle-platform" data-id="' + tempId + '" data-status="0" title="开启">' +
'<i class="layui-icon layui-icon-circle"></i>' +
'</button>' +
'<button class="layui-col-xs6 layui-col-sm6 layui-col-md6 layui-btn layui-btn-xs btn-icon save-platform" data-id="' + tempId + '" title="保存">' +
'<i class="layui-icon layui-icon-ok"></i>' +
'</button>' +
'</td>' +
'</tr>';
// 获取表格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 });
}
}
});
});

View File

@@ -0,0 +1,465 @@
<div class="layui-tab-item" lay-id="ai_platform">
<!-- AI平台配置 -->
<div class="layui-form form-wrap card-common">
<!-- 应用模块选择 -->
<fieldset class="layui-elem-field layui-field-title">
<legend>应用模块选择</legend>
</fieldset>
<div class="layui-form-item">
<label class="layui-form-label">应用模块</label>
<div class="layui-input-block">
<select name="app_module" class="layui-select" lay-filter="appModuleSelect">
<option value="">请选择应用模块</option>
{foreach $support_app_modules as $item}
<option value="{$item.value}">{$item.label}</option>
{/foreach}
</select>
</div>
</div>
<!-- 平台选择 -->
<fieldset class="layui-elem-field layui-field-title">
<legend>基本设置</legend>
</fieldset>
<div class="layui-form-item">
<label class="layui-form-label">默认平台</label>
<div class="layui-input-block">
<select name="selected_platform" lay-filter="platformSelect" id="selectedPlatformSelect">
<option value="">请选择要默认使用的AI服务平台</option>
</select>
</div>
</div>
<!-- 平台配置表格 -->
<fieldset class="layui-elem-field layui-field-title">
<legend>AI服务平台配置</legend>
</fieldset>
<div class="layui-form-item">
<table class="layui-table" lay-even lay-skin="line">
<thead>
<tr>
<th>名称</th>
<th>类型</th>
<th>Base URL</th>
<th>API Key</th>
<th>描述</th>
<th>操作</th>
</tr>
</thead>
<tbody id="platformTableBody">
<tr>
<td colspan="6" style="text-align: center;">请选择应用模块</td>
</tr>
</tbody>
</table>
</div>
<!-- 添加新平台 -->
<div class="layui-form-item text-center">
<button class="layui-btn" id="addPlatform">添加平台</button>
</div>
<!-- 保存按钮 -->
<div class="layui-form-item">
<button class="layui-btn" lay-submit lay-filter="save_platform_cfg">保存</button>
</div>
</div>
</div>
<script>
/**
* 从PHP获得的应用参数说明
* $support_ai_platform_types 支持的AI平台类型
* $support_app_modules 支持的应用模块
* $platform_info 平台的配置信息,包含各应用模块的平台配置
**/
// 存储PHP传递过来的平台配置数据
var support_ai_platform_types = <? php echo json_encode($support_ai_platform_types ?? []); ?>;
var support_app_modules = <? php echo json_encode($support_app_modules ?? []); ?>;
var platform_info = <? php echo json_encode($platform_info ?? []); ?>;
console.log('platform_info', platform_info);
// 创建深度代理对象
var proxy_platform_info = new PerformanceDeepProxy(platform_info, {});
console.log('proxy_platform_info', proxy_platform_info);
// 当前选中的应用模块
var currentAppModule = '';
// 临时编辑状态标识
var editingPlatformId = null;
// 验证平台数据
function validatePlatformData(platformData, isEdit, existingId) {
// 检查名称是否重复
if (!platformData.name || platformData.name.trim() === '') {
return '名称不能为空';
}
var moduleInfo = proxy_platform_info[currentAppModule];
var platformList = moduleInfo.list || [];
var nameExists = platformList.some(function (p) {
return p.name === platformData.name && (!isEdit || p.id.toString() !== existingId.toString());
});
if (nameExists) {
return '名称已存在';
}
// 检查类型是否选择
if (!platformData.type) {
return '类型必须选择';
}
// 检查Base URL是否为空
if (!platformData.base_url || platformData.base_url.trim() === '') {
return 'Base URL不能为空';
}
// 检查API Key是否为空
if (!platformData.api_key || platformData.api_key.trim() === '') {
return 'API Key不能为空';
}
return null; // 验证通过
}
// 初始化layui
layui.use(['form', 'layer'], function () {
var form = layui.form;
var layer = layui.layer;
// 监听应用模块选择变化
form.on('select(appModuleSelect)', function (data) {
currentAppModule = data.value;
editingPlatformId = null; // 重置编辑状态
updatePlatformSettings();
});
// 生成平台类型选择框HTML
function generatePlatformTypeSelect(selectedValue) {
var selectHtml = '<select class="layui-select">';
selectHtml += '<option value="">请选择类型</option>';
support_ai_platform_types.forEach(function (type) {
var selected = selectedValue === type.value ? ' selected' : '';
selectHtml += '<option value="' + type.value + '"' + selected + '>' + type.label + '</option>';
});
selectHtml += '</select>';
return selectHtml;
}
// 获取平台类型标签
function getPlatformTypeLabel(typeValue) {
var type = support_ai_platform_types.find(function (t) { return t.value === typeValue; });
return type ? type.label : typeValue;
}
// 更新平台设置(默认平台下拉框和平台配置表格)
function updatePlatformSettings() {
var platformSelect = document.getElementById('selectedPlatformSelect');
var tableBody = document.getElementById('platformTableBody');
// 清空默认平台下拉框
platformSelect.innerHTML = '<option value="">请选择要默认使用的AI服务平台</option>';
// 如果没有选择应用模块,显示提示信息
if (!currentAppModule || !proxy_platform_info[currentAppModule]) {
tableBody.innerHTML = '<tr><td colspan="6" style="text-align: center;">请选择应用模块</td></tr>';
form.render('select'); // 重新渲染表单
return;
}
// 获取当前应用模块的平台信息
var moduleInfo = proxy_platform_info[currentAppModule];
var platformList = moduleInfo.list || [];
var defaultPlatformId = moduleInfo.default && moduleInfo.default.id ? moduleInfo.default.id.toString() : '';
// 更新默认平台下拉框
platformList.forEach(function (platform) {
var option = document.createElement('option');
option.value = platform.id;
option.textContent = platform.name;
if (platform.id.toString() === defaultPlatformId) {
option.selected = true;
}
platformSelect.appendChild(option);
});
// 更新平台配置表格
if (platformList.length === 0) {
tableBody.innerHTML = '<tr><td colspan="6" style="text-align: center;">当前应用模块暂无平台配置</td></tr>';
} else {
var tableHtml = '';
platformList.forEach(function (platform) {
// API Key 脱敏显示
var displayApiKey = platform.api_key ? platform.api_key.substring(0, 4) + '****' + platform.api_key.substring(platform.api_key.length - 4) : '';
// 构建操作按钮
var actionButtons = '';
if (editingPlatformId === platform.id) {
// 编辑状态下显示保存和取消按钮
actionButtons =
'<button class="layui-col-xs6 layui-col-sm6 layui-col-md6 layui-btn layui-btn-xs layui-btn-danger btn-icon delete-platform" data-id="' + platform.id + '"><i class="layui-icon layui-icon-delete"></i></button>' +
'<button class="layui-col-xs6 layui-col-sm6 layui-col-md6 layui-btn layui-btn-xs layui-btn-primary btn-icon cancel-edit-platform" onclick="cancelEditPlatform(' + platform.id + ')">取消</button>' +
'<button class="layui-col-xs6 layui-col-sm6 layui-col-md6 layui-btn layui-btn-xs btn-icon save-platform" onclick="saveEditPlatform(' + platform.id + ')">保存</button>';
} else {
// 普通状态下显示编辑、删除和启用/禁用按钮
var toggleIcon = platform.enable ? 'layui-icon-radio' : 'layui-icon-circle';
var toggleText = platform.enable ? '启用' : '禁用';
var toggleStatus = platform.enable ? '0' : '1';
actionButtons =
'<button class="layui-col-xs6 layui-col-sm6 layui-col-md6 layui-btn layui-btn-xs layui-btn-danger btn-icon delete-platform" data-id="' + platform.id + '"><i class="layui-icon layui-icon-delete"></i></button>' +
'<button class="layui-col-xs6 layui-col-sm6 layui-col-md6 layui-btn layui-btn-xs layui-btn-warm btn-icon toggle-platform" data-id="' + platform.id + '" data-status="' + toggleStatus + '" title="' + toggleText + '">' + '<i class="layui-icon ' + toggleIcon + '"></i>' + '</button>' +
'<button class="layui-col-xs6 layui-col-sm6 layui-col-md6 layui-btn layui-btn-xs btn-icon save-platform" data-id="' + platform.id + '"><i class="layui-icon layui-icon-ok"></i></button>';
}
tableHtml += '<tr data-id="' + platform.id + '">' +
'<td>' + (editingPlatformId === platform.id ? '<input type="text" class="layui-input" value="' + platform.name + '" />' : platform.name) + '</td>' +
'<td>' + (editingPlatformId === platform.id ? generatePlatformTypeSelect(platform.type) : platform.type_label) + '</td>' +
'<td>' + (editingPlatformId === platform.id ? '<input type="text" class="layui-input" value="' + platform.base_url + '" />' : platform.base_url) + '</td>' +
'<td>' + (editingPlatformId === platform.id ? '<input type="text" class="layui-input" value="' + platform.api_key + '" />' : displayApiKey) + '</td>' +
'<td>' + (editingPlatformId === platform.id ? '<input type="text" class="layui-input" value="' + (platform.desc || '') + '" />' : (platform.desc || '')) + '</td>' +
'<td class="layui-row">' + actionButtons + '</td>' +
'</tr>';
});
tableBody.innerHTML = tableHtml;
// 如果当前有编辑的行重新渲染表单以初始化select组件
if (editingPlatformId) {
form.render('select');
}
}
// 重新渲染表单
form.render('select');
}
// 切换平台启用状态
$('.toggle-platform').on('click', function () {
var id = $(this).data('id');
var status = $(this).data('status');
if (!currentAppModule || !proxy_platform_info[currentAppModule]) return;
// 查找平台
var platformList = proxy_platform_info[currentAppModule].list;
var platform = platformList.find(function (p) { return p.id === id; });
if (platform) {
// 切换启用状态
platform.enable = platform.enable ? 0 : 1;
// 同步更新 platform_info 中的数据
var platformList = proxy_platform_info[currentAppModule].list;
var idx = platformList.findIndex(function (p) { return p.id === id; });
if (idx !== -1) {
platformList[idx].enable = platform.enable;
}
updatePlatformSettings();
layui.layer.msg(platform.enable ? '已启用' : '已关闭');
}
});
// 编辑平台
// 添加平台按钮点击事件
document.getElementById('addPlatform').onclick = function () {
if (!currentAppModule) {
layer.msg('请先选择应用模块');
return;
}
// 确保platform_info中存在当前应用模块的数据
if (!proxy_platform_info[currentAppModule]) {
proxy_platform_info[currentAppModule] = {
default: null,
list: []
};
}
// 生成新的平台ID使用时间戳
var newId = Date.now();
// 创建新的平台对象
var newPlatform = {
id: newId,
name: '',
type: '',
type_label: '',
base_url: '',
api_key: '',
desc: '',
enable: 1
};
// 添加到平台列表
proxy_platform_info[currentAppModule].list.push(newPlatform);
// 设置为编辑状态
editingPlatformId = newId;
// 更新表格显示
updatePlatformSettings();
// 滚动到新添加的行
setTimeout(function () {
var newRow = document.querySelector('tr[data-id="' + newId + '"]');
if (newRow) {
newRow.scrollIntoView({ behavior: 'smooth', block: 'center' });
// 聚焦到第一个输入框
var firstInput = newRow.querySelector('input');
if (firstInput) firstInput.focus();
}
}, 100);
};
// 保存按钮点击事件(占位,需要根据实际需求实现)
form.on('submit(save_platform_cfg)', function (data) {
if (!currentAppModule) {
layer.msg('请先选择应用模块');
return false;
}
// 检查是否有正在编辑的行
if (editingPlatformId) {
layer.msg('请先完成当前行的编辑操作');
return false;
}
// 添加应用模块信息到提交数据中
data.field.app_module = currentAppModule;
data.field.platform_data = proxy_platform_info[currentAppModule];
// 这里应该发送保存请求暂时用alert显示要保存的数据
layer.msg('保存功能待实现');
console.log('要保存的数据:', data.field);
return false;
});
});
// 编辑平台
function editPlatform(id) {
if (!currentAppModule || !proxy_platform_info[currentAppModule]) return;
// 查找要编辑的平台
var platformList = proxy_platform_info[currentAppModule].list;
var platform = platformList.find(function (p) { return p.id === id; });
if (platform) {
editingPlatformId = id;
updatePlatformSettings();
// 滚动到编辑行并聚焦
setTimeout(function () {
var editRow = document.querySelector('tr[data-id="' + id + '"]');
if (editRow) {
editRow.scrollIntoView({ behavior: 'smooth', block: 'center' });
var firstInput = editRow.querySelector('input');
if (firstInput) firstInput.focus();
}
}, 100);
}
}
// 取消编辑
function cancelEditPlatform(id) {
if (!currentAppModule || !proxy_platform_info[currentAppModule]) return;
// 如果是新增的行且未保存,则从列表中移除
var platformList = proxy_platform_info[currentAppModule].list;
var platformIndex = platformList.findIndex(function (p) { return p.id === id; });
if (platformIndex !== -1 && !platformList[platformIndex].name) {
// 如果是未填写任何内容的新行,直接删除
platformList.splice(platformIndex, 1);
}
editingPlatformId = null;
updatePlatformSettings();
}
// 保存编辑
function saveEditPlatform(id) {
if (!currentAppModule || !proxy_platform_info[currentAppModule]) return;
var rowElement = document.querySelector('tr[data-id="' + id + '"]');
if (!rowElement) return;
// 获取表单数据
var inputs = rowElement.querySelectorAll('input');
var selects = rowElement.querySelectorAll('select');
var platformData = {
name: inputs[0] ? inputs[0].value.trim() : '',
type: selects[0] ? selects[0].value : '',
base_url: inputs[1] ? inputs[1].value.trim() : '',
api_key: inputs[2] ? inputs[2].value.trim() : '',
desc: inputs[3] ? inputs[3].value.trim() : ''
};
// 验证数据
var validationError = validatePlatformData(platformData, true, id);
if (validationError) {
layui.layer.msg(validationError);
return;
}
// 查找要更新的平台
var platformList = proxy_platform_info[currentAppModule].list;
var platformIndex = platformList.findIndex(function (p) { return p.id === id; });
if (platformIndex !== -1) {
// 更新平台数据
platformList[platformIndex].name = platformData.name;
platformList[platformIndex].type = platformData.type;
platformList[platformIndex].type_label = getPlatformTypeLabel(platformData.type);
platformList[platformIndex].base_url = platformData.base_url;
platformList[platformIndex].api_key = platformData.api_key;
platformList[platformIndex].desc = platformData.desc;
}
editingPlatformId = null;
updatePlatformSettings();
layui.layer.msg('保存成功');
}
// 删除平台
function deletePlatform(id) {
layui.layer.confirm('确定要删除这个平台吗?', function (index) {
if (!currentAppModule || !proxy_platform_info[currentAppModule]) {
layui.layer.close(index);
return;
}
// 查找并删除平台
var platformList = proxy_platform_info[currentAppModule].list;
var platformIndex = platformList.findIndex(function (p) { return p.id === id; });
if (platformIndex !== -1) {
platformList.splice(platformIndex, 1);
// 如果删除的是默认平台,清除默认设置
if (proxy_platform_info[currentAppModule].default &&
proxy_platform_info[currentAppModule].default.id.toString() === id.toString()) {
proxy_platform_info[currentAppModule].default = null;
}
updatePlatformSettings();
layui.layer.msg('删除成功');
}
layui.layer.close(index);
});
}
// 获取平台类型标签
function getPlatformTypeLabel(typeValue) {
var type = support_ai_platform_types.find(function (t) { return t.value === typeValue; });
return type ? type.label : typeValue;
}
</script>

View File

@@ -22,6 +22,7 @@
<link rel="stylesheet" type="text/css" href="SHOP_CSS/style2/common.css?v={$version}" /> <link rel="stylesheet" type="text/css" href="SHOP_CSS/style2/common.css?v={$version}" />
<script src="__STATIC__/js/jquery-3.1.1.js"></script> <script src="__STATIC__/js/jquery-3.1.1.js"></script>
<script src="__STATIC__/js/jquery.cookie.js"></script> <script src="__STATIC__/js/jquery.cookie.js"></script>
<script src="__STATIC__/js/deep-proxy-1.0.js?t={$version}5"></script>
<script src="__STATIC__/ext/layui/layui.js"></script> <script src="__STATIC__/ext/layui/layui.js"></script>
<script> <script>
layui.use(['layer', 'upload', 'element'], function() {}); layui.use(['layer', 'upload', 'element'], function() {});

View File

@@ -0,0 +1,101 @@
<?php
namespace app\tracking;
use app\common\library\BrowserDetector;
use app\model\stat\StatShop;
use app\model\stat\StatStore;
class VisitorTracker
{
private $detector;
private $sourceType;
public function __construct()
{
$this->detector = new BrowserDetector();
$this->sourceType = $this->detector->getSourceType();
log_write('访问来源检测:' . $this->sourceType, 'debug');
}
/**
* 获取通用埋点数据
* @param $params 埋点参数
* @param array $fileds 埋点字段
* @return array 埋点数据
*/
private function getCommonTraceData($params, $fileds = ['site_id'])
{
$statData = [
'visit_count' => 1,
];
foreach ($fileds as $filed) {
$statData[$filed] = $params[$filed] ?? 0;
}
switch ($this->sourceType) {
case 'miniprogram':
$statData = array_merge($statData, ['weapp_visit_count' => 1,]);
break;
case 'wechat':
$statData = array_merge($statData, ['wechat_visit_count' => 1,]);
break;
case 'h5':
$statData = array_merge($statData, ['h5_visit_count' => 1,]);
break;
case 'pc':
$statData = array_merge($statData, ['pc_visit_count' => 1,]);
break;
default:
break;
}
return $statData;
}
/**
* 店铺UV埋点
* @param $params 埋点参数
* @return bool
*/
public function shop_visit($params)
{
try {
$statData = $this->getCommonTraceData($params, ['site_id', 'store_id']);
// 访问来源统计
if (!empty($statData)) {
$stat_model = new StatShop();
$stat_model->addShopStat($statData);
}
return true;
} catch (\Throwable $th) {
//throw $th;
log_write('访问者跟踪失败:' . $th->getMessage(), 'error');
}
return false;
}
/**
* 门店UV埋点
* @param $params 埋点参数
* @return bool
*/
public function store_visit($params)
{
try {
$statData = $this->getCommonTraceData($params);
// 访问来源统计
if (!empty($statData)) {
$stat_model = new StatStore();
$stat_model->addStoreStat($statData);
}
return true;
} catch (\Throwable $th) {
//throw $th;
log_write('访问者跟踪失败:' . $th->getMessage(), 'error');
}
return false;
}
}

View File

@@ -1,7 +1,24 @@
<?php <?php
return [ return [
'default' => 'default',//url URL接口启动 cli 命令启动 default 系统任务 // 是否开启定时任务
'enable' => true,
/**
* 定时任务默认模式
* url URL接口启动
* cli 命令启动。 是通过 php think cron:schedule 命令创建一个守护进程来启动的
* default 系统任务
*/
'default' => 'cli',
// 类方法任务列表, yunwuxin/think-cron 配置方式
// 然后通过 php think cron:schedule 命令启动一个进程来调度处理
// 特别要注意有的时候cli 模式下,计划任务会因为守护进程的原因,导致计划任务不执行
// 解决办法:
// 1. 检查是否开启了守护进程
// 2. 检查守护进程是否正常运行
// 3. 检查计划任务是否正常运行
'tasks' => [ 'tasks' => [
\app\command\Schedule::class \app\command\Schedule::class
] ]

File diff suppressed because it is too large Load Diff