Compare commits
222 Commits
test/weixi
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
| 2d21993720 | |||
| 3a121e4db6 | |||
| ef708e6b40 | |||
| 0a7301f39d | |||
| c9d4d1d797 | |||
| d975abb3de | |||
| aa64c475e9 | |||
| 5cbd11be38 | |||
| 3341d41422 | |||
| d734ec45d6 | |||
| 6bedc732d1 | |||
| 5c4735a8f6 | |||
| ef7879c2b6 | |||
| b84ada7039 | |||
| 9a0dcc87e6 | |||
| f7dc9977ac | |||
| 82efca7135 | |||
| 82237f6879 | |||
| fe2a41cd33 | |||
| 0fb8e62b50 | |||
| 8e32bc0d7d | |||
| 2f7d9ed312 | |||
| ad5dcfea77 | |||
| e263f3bd58 | |||
| 266f810508 | |||
| f2d5ce1d7b | |||
| f4e4d2a855 | |||
| b737a7d51d | |||
| 673678a0e4 | |||
| 96b61ba533 | |||
| e000b61508 | |||
| 31456469a3 | |||
| 43edae2f90 | |||
| 05b80040f6 | |||
| e6929aa1f5 | |||
| 5f7017b78a | |||
| 949940dca6 | |||
| 620fa93149 | |||
| 0af78b796b | |||
| 7a1a59cd49 | |||
| 91f427b030 | |||
| b1bccafeb6 | |||
| 1914cc9958 | |||
| ef32e31e59 | |||
| d435aaf4a8 | |||
| e41b47cb62 | |||
| f577e47be6 | |||
| f8291dd2ba | |||
| ba5c2239ac | |||
| 498122f57e | |||
| 4e5d16e48c | |||
| 0980a8db27 | |||
| 3239891cd1 | |||
| 28b12b3dfe | |||
| 3aab5b9c75 | |||
| 09c859750e | |||
| 157ea7f46d | |||
| 572b4c4a00 | |||
| 9b38248cbf | |||
| 89f36ee666 | |||
| c8cf3cde16 | |||
| c63fce1ce8 | |||
| 3cafaae451 | |||
| 3275a159a1 | |||
| 0b2092a8fc | |||
| 4a53db1f4c | |||
| 34db5cd074 | |||
| 9ba444dbe7 | |||
| 357a479571 | |||
| 7b6a8500b0 | |||
| 99628f1d18 | |||
| ed81982239 | |||
| fd7593c72b | |||
| 1366d974cf | |||
| 62086f2332 | |||
| 15b8b5b039 | |||
| a12598eda3 | |||
| 7478e04472 | |||
| ddd6966494 | |||
| 37b3d62c74 | |||
| 2f9e1fabd6 | |||
| 345e7393ae | |||
| d9d6b596fd | |||
| bcf0fa72e2 | |||
| 65ec3dc740 | |||
| 717481dcba | |||
| cd37019675 | |||
| dfa0a4f433 | |||
| 96e00cf57c | |||
| ba17145705 | |||
| 443f3c38fc | |||
| 9c4ccbbd99 | |||
| 8134622cfc | |||
| c1b5ef72eb | |||
| a2e4d962da | |||
| 8369ff6c43 | |||
| a969a4cdf9 | |||
| 1d453e8663 | |||
| f4868f8a79 | |||
| 26e207c1ea | |||
| d3f56b899e | |||
| 6c5f163287 | |||
| 4e1126bf45 | |||
| 842b3a51ff | |||
| 54ef5ccf3d | |||
| e0aeea15f2 | |||
| 951836083e | |||
| 0a9eadcdd0 | |||
| ea322c9727 | |||
| 99ba1d812c | |||
| 58052b1271 | |||
| 510e8a07d2 | |||
| 7cc74f66b9 | |||
| d3a86b2900 | |||
| 79e6f6ebd7 | |||
| 0f965c5a8e | |||
| a1d400b134 | |||
| cff7580880 | |||
| 51fd354cbf | |||
| 7eadf2df56 | |||
| cfd791f148 | |||
| e4bb99aa1f | |||
| c4dc09b580 | |||
| f10b650a8f | |||
| f7413acd3e | |||
| f42b4a1036 | |||
| 7d3d71e0e3 | |||
| 6a44d27fd3 | |||
| a90a081973 | |||
| 4736273902 | |||
| fba01f4909 | |||
| 42aa934493 | |||
| fe73fdd5bd | |||
| c112f02fcc | |||
| 4d2467ae36 | |||
| 0b6e6914fd | |||
| 0f76b61152 | |||
| 6cfff15c62 | |||
| 6d3887ec06 | |||
| 5af0b07775 | |||
| 8ae10dd2c3 | |||
| 17c1ce2cc6 | |||
| a209dc8080 | |||
| 1d4fff13a1 | |||
| a811e36635 | |||
| d8a0dd5d31 | |||
| 0ff979917c | |||
| fc5615a9c7 | |||
| cdcd9eeffa | |||
| c0da89735c | |||
| fc34d83692 | |||
| 8ceb252d79 | |||
| 8da4563435 | |||
| 045e6ab3df | |||
| 5591e17446 | |||
| 8f783fd765 | |||
| 6b41e46f30 | |||
| ff89fdf5e9 | |||
| 402c425575 | |||
| ee2785f972 | |||
| bbb0271a5e | |||
| 75ff4bb0a4 | |||
| 776f0ed029 | |||
| eef56291eb | |||
| 8e159edf1d | |||
| 1793e4b2aa | |||
| a793ed541b | |||
| 09ed1bd427 | |||
| ff666975da | |||
| b4403cedd9 | |||
| 98d2eb8a2a | |||
| ae5f56c16f | |||
| d374034694 | |||
| b0f399c814 | |||
| 8489ef35cb | |||
| 7d8c2d4e37 | |||
| ec60eee8fe | |||
| dac15250a2 | |||
| fec0198537 | |||
| f5ac4d10c0 | |||
| ca07a6cea5 | |||
| b5d89aef72 | |||
| d151d45e99 | |||
| 3525af81bf | |||
| 01c86ce0a3 | |||
| e5e619a241 | |||
| d9b10c1621 | |||
| c6e72e5b79 | |||
| 980effc420 | |||
| e4040a27e7 | |||
| 23170dcc3f | |||
| 64c92857a6 | |||
| 4346bfebb7 | |||
| c24271c075 | |||
| 526982c431 | |||
| c161bc55e5 | |||
| eb79ad260c | |||
| ce8e59902c | |||
| b8ed400d12 | |||
| 5f48980d31 | |||
| b81e7b8b1b | |||
| a468c0919d | |||
| 1ae5b46523 | |||
| ecac45a74a | |||
| 51c4cb7767 | |||
| ae7cfebb44 | |||
| e4ccbbcbd1 | |||
| bdfcd1cedb | |||
| 6a7b465944 | |||
| 41ac96630c | |||
| 3a8fbc3e1b | |||
| e3e57ee154 | |||
| 0883c1318b | |||
| fe79d04343 | |||
| 2857283558 | |||
| 981779126c | |||
| 39ce5882cb | |||
| e51f6c6544 | |||
| 3a7f510e19 | |||
| 1c9e72e28d | |||
| 9d79b2585e | |||
| 1fc9a39ffe |
3
.env
3
.env
@@ -7,8 +7,6 @@ APP_ENV=development
|
||||
# PHP/PHP-FPM 配置
|
||||
PHP_VERSION=7.4
|
||||
PHP_FPM_VERSION=7.4-fpm
|
||||
PHP_FPM_PORT=9100
|
||||
XDEBUG_POST=9103
|
||||
|
||||
# 数据库配置
|
||||
MYSQL_ROOT_HOST=%
|
||||
@@ -23,5 +21,4 @@ REDIS_PORT=6399
|
||||
|
||||
# Nginx 配置
|
||||
NGINX_PORT=8010
|
||||
NGINX_SSL_PORT=8012
|
||||
|
||||
|
||||
24
.env.development
Normal file
24
.env.development
Normal file
@@ -0,0 +1,24 @@
|
||||
# 项目配置, 请根据实际情况修改
|
||||
PROJECT_NAME=newshop
|
||||
|
||||
# ThinkPHP 6.x 配置, 请根据实际情况修改
|
||||
APP_ENV=development
|
||||
|
||||
# PHP/PHP-FPM 配置
|
||||
PHP_VERSION=7.4
|
||||
PHP_FPM_VERSION=7.4-fpm
|
||||
|
||||
# 数据库配置
|
||||
MYSQL_ROOT_HOST=%
|
||||
MYSQL_DATABASE=shop_mallnew
|
||||
MYSQL_USER=shop_mallnew
|
||||
MYSQL_PASSWORD=shop_mallnew
|
||||
MYSQL_PORT=3326
|
||||
|
||||
# Redis 绑定端口及密码
|
||||
REDIS_PASSWORD=luckyshop123!@#
|
||||
REDIS_PORT=6499
|
||||
|
||||
# Nginx 暴漏端口
|
||||
NGINX_PORT=8050
|
||||
|
||||
13
.env.example
13
.env.example
@@ -1,24 +1,23 @@
|
||||
# 项目配置, 请根据实际情况修改
|
||||
PROJECT_NAME=newshop
|
||||
|
||||
# ThinkPHP 6.x 配置, 请根据实际情况修改
|
||||
APP_ENV=development
|
||||
|
||||
# PHP/PHP-FPM 配置
|
||||
PHP_VERSION=7.4
|
||||
PHP_FPM_VERSION=7.4-fpm
|
||||
PHP_FPM_PORT=9000
|
||||
XDEBUG_POST=9003
|
||||
|
||||
# 数据库配置
|
||||
MYSQL_ROOT_HOST=%
|
||||
MYSQL_DATABASE=shop_mallnew
|
||||
MYSQL_USER=shop_mallnew
|
||||
MYSQL_PASSWORD=shop_mallnew
|
||||
MYSQL_PORT=3306
|
||||
MYSQL_PORT=3316
|
||||
|
||||
# Redis 配置
|
||||
REDIS_PASSWORD=luckyshop123!@#
|
||||
REDIS_PORT=6379
|
||||
REDIS_PORT=6399
|
||||
|
||||
# Nginx 配置
|
||||
NGINX_PORT=80
|
||||
NGINX_SSL_PORT=443
|
||||
|
||||
NGINX_PORT=8010
|
||||
|
||||
24
.env.production
Normal file
24
.env.production
Normal file
@@ -0,0 +1,24 @@
|
||||
# 项目配置, 请根据实际情况修改
|
||||
PROJECT_NAME=newshop
|
||||
|
||||
# ThinkPHP 6.x 配置, 请根据实际情况修改
|
||||
APP_ENV=production
|
||||
|
||||
# PHP/PHP-FPM 配置
|
||||
PHP_VERSION=7.4
|
||||
PHP_FPM_VERSION=7.4-fpm
|
||||
|
||||
# 数据库配置
|
||||
MYSQL_ROOT_HOST=%
|
||||
MYSQL_DATABASE=shop_mallnew
|
||||
MYSQL_USER=shop_mallnew
|
||||
MYSQL_PASSWORD=shop_mallnew
|
||||
MYSQL_PORT=3926
|
||||
|
||||
# Redis 绑定端口及密码
|
||||
REDIS_PASSWORD=luckyshop123!@#
|
||||
REDIS_PORT=6829
|
||||
|
||||
# Nginx 暴漏端口
|
||||
NGINX_PORT=8858
|
||||
|
||||
24
.env.staging
Normal file
24
.env.staging
Normal file
@@ -0,0 +1,24 @@
|
||||
# 项目配置, 请根据实际情况修改
|
||||
PROJECT_NAME=newshop
|
||||
|
||||
# ThinkPHP 6.x 配置, 请根据实际情况修改
|
||||
APP_ENV=staging
|
||||
|
||||
# PHP/PHP-FPM 配置
|
||||
PHP_VERSION=7.4
|
||||
PHP_FPM_VERSION=7.4-fpm
|
||||
|
||||
# 数据库配置
|
||||
MYSQL_ROOT_HOST=%
|
||||
MYSQL_DATABASE=shop_mallnew
|
||||
MYSQL_USER=shop_mallnew
|
||||
MYSQL_PASSWORD=shop_mallnew
|
||||
MYSQL_PORT=3826
|
||||
|
||||
# Redis 绑定端口及密码
|
||||
REDIS_PASSWORD=luckyshop123!@#
|
||||
REDIS_PORT=6809
|
||||
|
||||
# Nginx 暴漏端口
|
||||
NGINX_PORT=8854
|
||||
|
||||
24
.env.test
Normal file
24
.env.test
Normal file
@@ -0,0 +1,24 @@
|
||||
# 项目配置, 请根据实际情况修改
|
||||
PROJECT_NAME=newshop
|
||||
|
||||
# ThinkPHP 6.x 配置, 请根据实际情况修改
|
||||
APP_ENV=test
|
||||
|
||||
# PHP/PHP-FPM 配置
|
||||
PHP_VERSION=7.4
|
||||
PHP_FPM_VERSION=7.4-fpm
|
||||
|
||||
# 数据库配置
|
||||
MYSQL_ROOT_HOST=%
|
||||
MYSQL_DATABASE=shop_mallnew
|
||||
MYSQL_USER=shop_mallnew
|
||||
MYSQL_PASSWORD=shop_mallnew
|
||||
MYSQL_PORT=3346
|
||||
|
||||
# Redis 绑定端口及密码
|
||||
REDIS_PASSWORD=luckyshop123!@#
|
||||
REDIS_PORT=6799
|
||||
|
||||
# Nginx 暴漏端口
|
||||
NGINX_PORT=8360
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -18,6 +18,9 @@ __pycache__
|
||||
.idea
|
||||
.vscode
|
||||
|
||||
# 环境变量
|
||||
.env
|
||||
|
||||
# 源码结构
|
||||
debug.txt
|
||||
.travis.yml
|
||||
|
||||
152
README.md
Normal file
152
README.md
Normal file
@@ -0,0 +1,152 @@
|
||||
# 在线商城PHP项目
|
||||
|
||||
## Git 分支策略
|
||||
|
||||
| 环境 | 推荐分支 | 备选分支 | 说明 |
|
||||
|------|----------|----------|------|
|
||||
| local | `dev` | `develop` | 本地开发环境 |
|
||||
| development | `dev` | `development` | 开发测试环境 |
|
||||
| test | `test` | `staging` | 测试环境 |
|
||||
| staging | `staging` | `pre-release` | 预发布环境 |
|
||||
| production | `master` | `main` | 生产环境 |
|
||||
|
||||
**部署建议**:
|
||||
- 每个环境部署前请先切换到对应的Git分支
|
||||
- 确保代码版本与目标环境匹配
|
||||
- 生产环境部署前建议先在staging环境验证
|
||||
|
||||
## Docker 部署
|
||||
|
||||
```bash
|
||||
cp .env.example .env.development
|
||||
```
|
||||
|
||||
**注意**
|
||||
|
||||
- 在同一目录下面,执行 `docker-compose` 命令时,需要指定项目名称。用来区分不同的环境。如 `shop_local`、`shop_dev` 等。
|
||||
- 本地部署时,需要将 `APP_ENV` 设置为 `local`。
|
||||
- 开发环境部署时,需要将 `APP_ENV` 设置为 `development`。
|
||||
|
||||
|
||||
|
||||
## 环境变量
|
||||
|
||||
- `APP_ENV`: 应用环境,默认值为 `development`。
|
||||
|
||||
## 开发环境-local 部署
|
||||
|
||||
**对应Git分支**: `main` 或 `develop`
|
||||
|
||||
```bash
|
||||
# 切换到本地开发分支
|
||||
git checkout main # 或 develop
|
||||
|
||||
# 本地部署时,需要将 APP_ENV 设置为 local, 并指定 docker-compose.local.yml 文件
|
||||
docker-compose --env-file .env.local -f docker-compose.local.yml up -d
|
||||
# docker-compose --project-name shop_local --env-file .env.local -f docker-compose.local.yml up -d
|
||||
|
||||
# docker-compose down 命令,用来停止并删除容器
|
||||
docker-compose -f docker-compose.local.yml down -v
|
||||
# docker-compose --project-name shop_local down -v
|
||||
```
|
||||
|
||||
|
||||
## 开发环境-development 部署
|
||||
|
||||
**对应Git分支**: `dev` 或 `development`
|
||||
|
||||
```bash
|
||||
# 切换到开发分支
|
||||
git checkout dev # 或 development
|
||||
|
||||
# 默认使用 docker-compose.yml 文件
|
||||
# 清理所有未使用的构建缓存
|
||||
docker builder prune -a -f
|
||||
|
||||
# 然后再执行无缓存构建和启动
|
||||
docker-compose --project-name shop_development --env-file .env.development build --no-cache
|
||||
docker-compose --project-name shop_development --env-file .env.development up -d
|
||||
|
||||
# docker-compose down 命令,用来停止并删除容器
|
||||
docker-compose --project-name shop_development down -v
|
||||
|
||||
|
||||
```
|
||||
|
||||
## 开发环境-test 部署 (测试环境)
|
||||
|
||||
**对应Git分支**: `test` 或 `staging`
|
||||
|
||||
```bash
|
||||
# 切换到测试分支
|
||||
git checkout test # 或 staging
|
||||
|
||||
# 默认使用 docker-compose.yml 文件
|
||||
# 清理所有未使用的构建缓存
|
||||
docker builder prune -a -f
|
||||
|
||||
# 然后再执行无缓存构建和启动
|
||||
docker-compose --project-name shop_test --env-file .env.test build --no-cache
|
||||
|
||||
# 默认使用 docker-compose.yml 文件
|
||||
docker-compose --project-name shop_test --env-file .env.test up -d
|
||||
|
||||
# docker-compose down 命令,用来停止并删除容器
|
||||
docker-compose --project-name shop_test down -v
|
||||
```
|
||||
|
||||
## 开发环境-staging 部署(预发布环境)
|
||||
|
||||
**对应Git分支**: `staging` 或 `pre-release`
|
||||
|
||||
```bash
|
||||
# 切换到预发布分支
|
||||
git checkout staging # 或 pre-release
|
||||
|
||||
# 默认使用 docker-compose.yml 文件
|
||||
docker-compose --project-name shop_staging --env-file .env.staging up -d
|
||||
|
||||
# docker-compose down 命令,用来停止并删除容器
|
||||
docker-compose --project-name shop_staging down -v
|
||||
```
|
||||
|
||||
## 生产环境-production 部署
|
||||
|
||||
**对应Git分支**: `master` 或 `main` 或 `production`
|
||||
|
||||
```bash
|
||||
# 切换到生产分支
|
||||
git checkout main
|
||||
|
||||
# 确保代码是最新的生产版本
|
||||
git pull origin main
|
||||
|
||||
# 默认使用 docker-compose.yml 文件
|
||||
docker-compose --project-name shop_production --env-file .env.production up -d
|
||||
|
||||
# docker-compose down 命令,用来停止并删除容器
|
||||
docker-compose --project-name shop_production down -v
|
||||
```
|
||||
|
||||
## 便捷部署脚本
|
||||
|
||||
### 环境切换与部署脚本
|
||||
|
||||
- `deploy.sh` 脚本:
|
||||
|
||||
|
||||
### 使用方法
|
||||
|
||||
```bash
|
||||
# 赋予执行权限
|
||||
chmod +x deploy.sh
|
||||
|
||||
# 部署到开发环境
|
||||
./deploy.sh development
|
||||
|
||||
# 部署到测试环境
|
||||
./deploy.sh test
|
||||
|
||||
# 部署到生产环境
|
||||
./deploy.sh production
|
||||
```
|
||||
100
deploy.sh
Normal file
100
deploy.sh
Normal file
@@ -0,0 +1,100 @@
|
||||
#!/bin/bash
|
||||
|
||||
# deploy.sh - 环境切换与部署脚本
|
||||
# 使用方法: ./deploy.sh <environment>
|
||||
# 示例: ./deploy.sh development
|
||||
|
||||
ENVIRONMENT=$1
|
||||
PROJECT_NAME="shop_${ENVIRONMENT}"
|
||||
BRANCH=""
|
||||
COMPOSE_FILE="docker-compose.yml"
|
||||
ENV_FILE=".env.${ENVIRONMENT}"
|
||||
|
||||
# 现实运行中的服务器网站,数据库备份目录
|
||||
RUN_SERVER_WEB_ROOT="/data/wwwroot/shop-projects"
|
||||
RUN_SERVER_DB_BACKUP_DIR="/data/backup/shop-projects"
|
||||
|
||||
case $ENVIRONMENT in
|
||||
"local")
|
||||
BRANCH="dev"
|
||||
COMPOSE_FILE="docker-compose.local.yml"
|
||||
ENV_FILE=".env.local"
|
||||
;;
|
||||
"development")
|
||||
BRANCH="dev"
|
||||
;;
|
||||
"test")
|
||||
BRANCH="test"
|
||||
;;
|
||||
"staging")
|
||||
BRANCH="staging"
|
||||
;;
|
||||
"production")
|
||||
BRANCH="main"
|
||||
;;
|
||||
*)
|
||||
echo "错误: 不支持的环境 '$ENVIRONMENT'"
|
||||
echo "支持的环境: local, development, test, staging, production"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
echo "=========================================="
|
||||
echo "部署环境: $ENVIRONMENT"
|
||||
echo "项目名称: $PROJECT_NAME"
|
||||
echo "Git分支: $BRANCH"
|
||||
echo "配置文件: $COMPOSE_FILE"
|
||||
echo "环境文件: $ENV_FILE"
|
||||
echo "=========================================="
|
||||
|
||||
# 切换分支
|
||||
echo "切换到Git分支: $BRANCH"
|
||||
git checkout $BRANCH
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "错误: 无法切换到分支 $BRANCH"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 拉取最新代码
|
||||
echo "拉取最新代码..."
|
||||
git pull origin $BRANCH
|
||||
|
||||
# 根据不同的环境,执行不同的操作
|
||||
# 预发布环境、生产环境都需要使用运行服务上的用户文件,将用户文件复制到指定目录
|
||||
|
||||
|
||||
|
||||
# 根据不同的环境,执行不同的操作
|
||||
# 预发布环境、生产环境都需要还原数据库,使用数据库备份文件,并尝试使用数据库升级
|
||||
if [ "$ENVIRONMENT" = "local" ]; then
|
||||
echo "本地环境,跳过数据库还原"
|
||||
else
|
||||
echo "还原数据库..."
|
||||
# 还原数据库
|
||||
docker-compose --project-name $PROJECT_NAME exec -T db bash -c "mysql -uroot -p$DB_ROOT_PASSWORD shop < $DB_BACKUP_DIR/shop.sql"
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "错误: 数据库还原失败"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 尝试使用数据库升级脚本
|
||||
echo "尝试使用数据库升级..."
|
||||
docker-compose --project-name $PROJECT_NAME exec -T db bash -c "php artisan migrate --force"
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "数据库升级失败"
|
||||
else
|
||||
echo "数据库升级成功"
|
||||
fi
|
||||
fi
|
||||
|
||||
# 构建并启动容器
|
||||
echo "构建并启动Docker容器..."
|
||||
if [ "$ENVIRONMENT" = "local" ]; then
|
||||
docker-compose --env-file $ENV_FILE -f $COMPOSE_FILE up -d
|
||||
else
|
||||
docker-compose --project-name $PROJECT_NAME --env-file $ENV_FILE up -d
|
||||
fi
|
||||
|
||||
echo "部署完成!"
|
||||
echo "查看容器状态: docker-compose --project-name $PROJECT_NAME ps"
|
||||
echo "查看日志: docker-compose --project-name $PROJECT_NAME logs -f"
|
||||
@@ -14,7 +14,7 @@ services:
|
||||
build:
|
||||
context: ./docker/php
|
||||
dockerfile: Dockerfile
|
||||
container_name: ${PROJECT_NAME}_php
|
||||
container_name: ${PROJECT_NAME}_${APP_ENV}_php
|
||||
restart: always
|
||||
extra_hosts:
|
||||
- "host.docker.internal:host-gateway" # 支持主机名解析
|
||||
@@ -25,23 +25,20 @@ services:
|
||||
# 不然,ThinkPHP 6.x 系列,会只加载 .env 文件,而不会加载 .env.local 文件,导致 .env.local 文件中的配置不会生效
|
||||
APP_ENV: ${APP_ENV:-development}
|
||||
APP_DEBUG: ${APP_DEBUG:-true}
|
||||
XDEBUG_CONFIG: ${XDEBUG_CONFIG:-client_host=host.docker.internal client_port=9003}
|
||||
PHP_IDE_CONFIG: serverName=docker-php
|
||||
ports:
|
||||
- "${PHP_FPM_PORT:-9000}:9000" # PHP-FPM
|
||||
- "${XDEBUG_POST:-9003}:9003" # Xdebug
|
||||
# PHP应用根目录(可选,默认 /var/www/html)
|
||||
PHP_APP_ROOT: ${PHP_APP_ROOT:-/var/www/html}
|
||||
# 用户ID映射(可选,用于解决挂载权限问题)
|
||||
USER_ID: ${USER_ID:-33}
|
||||
GROUP_ID: ${GROUP_ID:-33}
|
||||
volumes:
|
||||
- ./:/var/www/all_source
|
||||
- ./src:/var/www/html
|
||||
- ./src:/var/www/html:rw
|
||||
# 更新下载源列表以加速apt-get
|
||||
- ./docker/debian/sources.list:/etc/apt/sources.list:ro
|
||||
- ./docker/php/php.ini:/usr/local/etc/php/php.ini:ro
|
||||
- ./docker/php/xdebug.ini:/usr/local/etc/php/conf.d/xdebug.ini
|
||||
- xdebug_logs:/tmp # Xdebug 日志目录
|
||||
depends_on:
|
||||
- db
|
||||
healthcheck:
|
||||
test: ["CMD", "bash", "-c", "curl -f http://localhost:9000/status && ps aux | grep '[p]hp think cron:schedule'"]
|
||||
test: ["CMD", "bash", "-c", "curl -f http://localhost:9000/status"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
@@ -49,18 +46,18 @@ services:
|
||||
networks:
|
||||
- sass-platform-net
|
||||
labels:
|
||||
- "com.docker.compose.project.working_dir=${PROJECT_NAME}"
|
||||
- "com.docker.compose.project.working_dir=${PROJECT_NAME}_${APP_ENV}"
|
||||
|
||||
nginx:
|
||||
build:
|
||||
context: ./docker/nginx
|
||||
dockerfile: Dockerfile
|
||||
container_name: ${PROJECT_NAME}_nginx
|
||||
container_name: ${PROJECT_NAME}_${APP_ENV}_nginx
|
||||
restart: always
|
||||
ports:
|
||||
- "${NGINX_PORT:-80}:80"
|
||||
- "${NGINX_SSL_PORT:-443}:443"
|
||||
volumes:
|
||||
# 挂载项目代码到 Nginx 容器中
|
||||
- ./src:/var/www/html:rw
|
||||
# 更新下载源列表以加速apt-get
|
||||
- ./docker/debian/sources.list:/etc/apt/sources.list:ro
|
||||
@@ -72,11 +69,11 @@ services:
|
||||
networks:
|
||||
- sass-platform-net
|
||||
labels:
|
||||
- "com.docker.compose.project.working_dir=${PROJECT_NAME}"
|
||||
- "com.docker.compose.project.working_dir=${PROJECT_NAME}_${APP_ENV}"
|
||||
|
||||
db:
|
||||
image: mysql:5.7.44
|
||||
container_name: ${PROJECT_NAME}_mysql
|
||||
container_name: ${PROJECT_NAME}_${APP_ENV}_mysql
|
||||
environment:
|
||||
<<: *shared-api-env
|
||||
volumes:
|
||||
@@ -93,12 +90,12 @@ services:
|
||||
- --collation-server=utf8mb4_unicode_ci
|
||||
- --innodb_buffer_pool_size=256M
|
||||
labels:
|
||||
- "com.docker.compose.project.working_dir=${PROJECT_NAME}"
|
||||
- "com.docker.compose.project.working_dir=${PROJECT_NAME}_${APP_ENV}"
|
||||
|
||||
# Redis 服务(可选)
|
||||
redis:
|
||||
image: redis:8.2
|
||||
container_name: ${PROJECT_NAME}_redis
|
||||
container_name: ${PROJECT_NAME}_${APP_ENV}_redis
|
||||
environment:
|
||||
REDIS_PASSWORD: ${REDIS_PASSWORD:-luckyshop123!@#}
|
||||
REDISCLI_AUTH: ${REDIS_PASSWORD:-luckyshop123!@#}
|
||||
@@ -111,13 +108,26 @@ services:
|
||||
- sass-platform-net
|
||||
restart: unless-stopped
|
||||
labels:
|
||||
- "com.docker.compose.project.working_dir=${PROJECT_NAME}"
|
||||
- "com.docker.compose.project.working_dir=${PROJECT_NAME}_${APP_ENV}"
|
||||
|
||||
volumes:
|
||||
mysql_db_data:
|
||||
name: ${PROJECT_NAME}_${APP_ENV}_mysql_db_data
|
||||
driver: local
|
||||
driver_opts:
|
||||
type: none
|
||||
o: bind
|
||||
device: ./docker/mysql_db_data/${APP_ENV}
|
||||
redis_data:
|
||||
xdebug_logs:
|
||||
name: ${PROJECT_NAME}_${APP_ENV}_redis_data
|
||||
driver: local
|
||||
driver_opts:
|
||||
type: none
|
||||
o: bind
|
||||
device: ./docker/redis_data/${APP_ENV}
|
||||
|
||||
|
||||
networks:
|
||||
sass-platform-net:
|
||||
name: ${PROJECT_NAME}_${APP_ENV}_net
|
||||
driver: bridge
|
||||
File diff suppressed because one or more lines are too long
@@ -1,27 +1,45 @@
|
||||
[mysqld]
|
||||
# 字符集设置
|
||||
character-set-server=utf8mb4
|
||||
collation-server=utf8mb4_unicode_ci
|
||||
|
||||
# 连接设置
|
||||
max_connections=100
|
||||
wait_timeout=28800
|
||||
interactive_timeout=28800
|
||||
|
||||
# 缓冲区设置
|
||||
innodb_buffer_pool_size=256M
|
||||
key_buffer_size=64M
|
||||
|
||||
# 日志设置
|
||||
slow_query_log=1
|
||||
slow_query_log_file=/var/lib/mysql/slow.log
|
||||
long_query_time=2
|
||||
|
||||
# 其他设置
|
||||
sql_mode=NO_ENGINE_SUBSTITUTION,STRICT_TRANS_TABLES
|
||||
|
||||
[client]
|
||||
default-character-set=utf8mb4
|
||||
|
||||
[mysql]
|
||||
default-character-set=utf8mb4
|
||||
[mysqld]
|
||||
# 字符集设置
|
||||
character-set-server=utf8mb4
|
||||
collation-server=utf8mb4_unicode_ci
|
||||
init_connect='SET NAMES utf8mb4'
|
||||
|
||||
|
||||
# 连接设置
|
||||
max_connections=500
|
||||
wait_timeout=300
|
||||
interactive_timeout=300
|
||||
max_connect_errors=1000
|
||||
|
||||
# 缓冲区设置
|
||||
innodb_buffer_pool_size=12G
|
||||
key_buffer_size=256M
|
||||
sort_buffer_size=4M
|
||||
read_buffer_size=4M
|
||||
read_rnd_buffer_size=8M
|
||||
join_buffer_size=4M
|
||||
|
||||
# InnoDB 优化
|
||||
innodb_file_per_table=1
|
||||
innodb_flush_method=O_DIRECT
|
||||
innodb_flush_log_at_trx_commit=2
|
||||
innodb_io_capacity=1000
|
||||
innodb_io_capacity_max=2000
|
||||
innodb_buffer_pool_instances=8
|
||||
innodb_thread_concurrency=16
|
||||
innodb_purge_threads=4
|
||||
|
||||
# 日志设置
|
||||
slow_query_log=1
|
||||
slow_query_log_file=/var/lib/mysql/slow.log
|
||||
long_query_time=1
|
||||
log_queries_not_using_indexes=1
|
||||
log_slow_admin_statements=1
|
||||
log_slow_slave_statements=1
|
||||
|
||||
# 其他设置
|
||||
sql_mode=NO_ENGINE_SUBSTITUTION,STRICT_TRANS_TABLES
|
||||
lower_case_table_names=1
|
||||
|
||||
[client]
|
||||
default-character-set=utf8mb4
|
||||
|
||||
9
docker/mysql_db_data/.gitignore
vendored
Normal file
9
docker/mysql_db_data/.gitignore
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
# 忽略所有文件
|
||||
*
|
||||
|
||||
# 只保留指定的 .gitkeep 文件
|
||||
!.gitignore
|
||||
!development/.gitkeep
|
||||
!test/.gitkeep
|
||||
!production/.gitkeep
|
||||
!staging/.gitkeep
|
||||
0
docker/mysql_db_data/.gitkeep
Normal file
0
docker/mysql_db_data/.gitkeep
Normal file
@@ -1,29 +1,19 @@
|
||||
FROM nginx:alpine
|
||||
|
||||
# 删除默认配置
|
||||
RUN rm /etc/nginx/conf.d/default.conf
|
||||
|
||||
#
|
||||
# - ./.docker/nginx/conf.c:/etc/nginx/conf.c:ro
|
||||
# - ./.docker/nginx/default.conf:/etc/nginx/conf.d/default.conf:ro
|
||||
# - ./.docker/nginx/sites-enabled:/etc/nginx/sites-enabled:ro
|
||||
# 将本地 nginx 配置复制到镜像中并设置为只读
|
||||
COPY ./conf.c /etc/nginx/conf.c
|
||||
COPY ./default.conf /etc/nginx/conf.d/default.conf
|
||||
COPY ./sites-enabled /etc/nginx/sites-enabled
|
||||
|
||||
# 设置只读权限(文件 0444,目录及其内容 0555)
|
||||
RUN chmod 0444 /etc/nginx/conf.c \
|
||||
&& chmod 0444 /etc/nginx/conf.d/default.conf \
|
||||
&& chmod -R 0555 /etc/nginx/sites-enabled
|
||||
|
||||
# 设置工作目录
|
||||
WORKDIR /var/www/html
|
||||
|
||||
# 创建日志目录
|
||||
RUN mkdir -p /var/log/nginx
|
||||
|
||||
# 暴露端口
|
||||
EXPOSE 80 443
|
||||
|
||||
FROM nginx:alpine
|
||||
|
||||
# 删除默认配置
|
||||
RUN rm /etc/nginx/conf.d/default.conf
|
||||
|
||||
|
||||
# 将本地 nginx 配置复制到镜像中
|
||||
COPY ./conf.c/ /etc/nginx/conf.c/
|
||||
COPY ./default.conf /etc/nginx/conf.d/default.conf
|
||||
COPY ./sites-enabled/ /etc/nginx/sites-enabled/
|
||||
|
||||
# 暴露端口
|
||||
EXPOSE 80 443
|
||||
|
||||
# 直接在Dockerfile中执行权限设置,不使用entrypoint.sh
|
||||
RUN mkdir -p /var/log/nginx && chmod -R 0444 /etc/nginx/conf.c && chmod 0444 /etc/nginx/conf.d/default.conf && chmod -R 0755 /etc/nginx/sites-enabled
|
||||
|
||||
# 启动nginx
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
21
docker/nginx/conf.c/enable-websocket.conf
Normal file
21
docker/nginx/conf.c/enable-websocket.conf
Normal file
@@ -0,0 +1,21 @@
|
||||
location /ws {
|
||||
proxy_pass http://php-fpm:8080; # 注意:这里用的是 Docker 服务名或容器名
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
|
||||
# 禁用缓冲,确保WebSocket数据实时传输
|
||||
proxy_buffering off;
|
||||
proxy_buffer_size 4k;
|
||||
proxy_buffers 4 4k;
|
||||
proxy_busy_buffers_size 4k;
|
||||
proxy_max_temp_file_size 0;
|
||||
|
||||
# 可选:设置超时(WebSocket 是长连接)
|
||||
proxy_read_timeout 86400s;
|
||||
proxy_send_timeout 86400s;
|
||||
}
|
||||
18
docker/nginx/entrypoint.sh
Normal file
18
docker/nginx/entrypoint.sh
Normal file
@@ -0,0 +1,18 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
echo "=== NGINX Docker 权限初始化 ==="
|
||||
|
||||
# 设置权限
|
||||
chmod -R 0444 /etc/nginx/conf.c
|
||||
chmod 0444 /etc/nginx/conf.d/default.conf
|
||||
chmod -R 0755 /etc/nginx/sites-enabled
|
||||
|
||||
|
||||
# 创建日志目录
|
||||
mkdir -p /var/log/nginx
|
||||
|
||||
echo "=== NGINX Docker 权限初始化完成 ==="
|
||||
|
||||
# 执行原有的启动命令
|
||||
exec "$@"
|
||||
@@ -1,9 +1,11 @@
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
# 作为默认站点接管所有 Host(域名/IP)
|
||||
listen 80 default_server;
|
||||
# listen 443 ssl http2; # Enable HTTP/2
|
||||
|
||||
server_name localhost;
|
||||
# 匹配任意域名/IP(Host 不限制)
|
||||
server_name _ localhost 127.0.0.1;
|
||||
root /var/www/html;
|
||||
index index.php index.html index.htm default.php default.htm default.html;
|
||||
|
||||
@@ -24,6 +26,9 @@
|
||||
include conf.c/enable-php-74.conf;
|
||||
#PHP-INFO-END
|
||||
|
||||
# 启用 WebSocket 支持
|
||||
include conf.c/enable-websocket.conf;
|
||||
|
||||
# --- REWRITE-START --- URL重写规则引用,修改后将导致面板设置的伪静态规则失效
|
||||
# include /www/server/panel/vhost/rewrite/xcx30.5g-quickapp.com.conf; # 等于下面的内容
|
||||
location / {
|
||||
@@ -34,8 +39,19 @@
|
||||
}
|
||||
# --- REWRITE-END ---
|
||||
|
||||
# --- 子目录 hwapp 及 hwappx 的配置,请勿删除,支持子目录网站,刷新,重定位 ---
|
||||
location ~ ^/hwapp/(.*)$ {
|
||||
try_files $uri $uri/ /hwapp/index.html;
|
||||
}
|
||||
|
||||
location ~ ^/hwappx/([^/]+)/(.*)$ {
|
||||
try_files $uri $uri/ /hwappx/$1/index.html;
|
||||
}
|
||||
# --- 子目录 hwapp 及 hwappx 配置结束 ---
|
||||
|
||||
|
||||
#禁止访问的文件或目录
|
||||
location ~ ^/(\.user.ini|\.htaccess|\.git|\.svn|\.project|LICENSE|README.md)
|
||||
location ~ ^/(\.user.ini|\.htaccess|\.git|\.svn|\.project|LICENSE|README.md)$
|
||||
{
|
||||
return 404;
|
||||
}
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
# 使用官方PHP镜像
|
||||
FROM php:7.4.33-fpm-dev-newshop
|
||||
|
||||
# 设置工作目录
|
||||
WORKDIR /var/www/html
|
||||
|
||||
# 暴露端口
|
||||
EXPOSE 9000 9003
|
||||
|
||||
CMD ["php-fpm"]
|
||||
@@ -14,6 +14,7 @@ COPY supervisord.conf /etc/supervisor/conf.d/supervisord.conf
|
||||
RUN apt-get update && apt-get install -y \
|
||||
supervisor \
|
||||
git \
|
||||
lsof \
|
||||
curl \
|
||||
vim \
|
||||
libpng-dev \
|
||||
@@ -27,8 +28,14 @@ RUN apt-get update && apt-get install -y \
|
||||
libfreetype6-dev \
|
||||
libjpeg62-turbo-dev \
|
||||
libpng-dev \
|
||||
iputils-ping \
|
||||
&& apt-get clean \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# 安装 WebSocat 完成后,清理缓存
|
||||
COPY ./websocat /usr/local/bin/websocat
|
||||
RUN chmod +x /usr/local/bin/websocat
|
||||
|
||||
# 安装 PHP 扩展
|
||||
RUN docker-php-ext-configure gd --with-freetype --with-jpeg \
|
||||
&& docker-php-ext-install \
|
||||
@@ -45,9 +52,6 @@ RUN docker-php-ext-configure gd --with-freetype --with-jpeg \
|
||||
# 安装 Redis 扩展
|
||||
RUN pecl install redis-5.3.7 && docker-php-ext-enable redis
|
||||
|
||||
# 安装 Xdebug(兼容 PHP 7.4 的版本)
|
||||
RUN pecl install xdebug-3.1.6 && docker-php-ext-enable xdebug
|
||||
|
||||
# 安装Composer
|
||||
COPY --from=composer:2.2.25 /usr/bin/composer /usr/bin/composer
|
||||
|
||||
@@ -56,30 +60,15 @@ RUN composer --version
|
||||
|
||||
# 修改 PHP 配置
|
||||
RUN echo "memory_limit=256M" > /usr/local/etc/php/conf.d/memory-limit.ini \
|
||||
&& echo "upload_max_filesize=50M" >> /usr/local/etc/php/conf.d/uploads.ini \
|
||||
&& echo "post_max_size=50M" >> /usr/local/etc/php/conf.d/uploads.ini
|
||||
|
||||
# 创建 Xdebug 配置
|
||||
RUN echo "zend_extension=xdebug.so" > /usr/local/etc/php/conf.d/xdebug.ini
|
||||
&& echo "upload_max_filesize=150M" >> /usr/local/etc/php/conf.d/uploads.ini \
|
||||
&& echo "post_max_size=150M" >> /usr/local/etc/php/conf.d/uploads.ini
|
||||
|
||||
# # 使用Composer安装项目依赖(可选,根据需要启用, 更多的时候,会出错,要在容器中执行操作)
|
||||
# RUN composer config -g repo.packagist composer https://mirrors.aliyun.com/composer/
|
||||
# RUN composer install --no-dev --optimize-autoloader --working-dir=/var/www/html
|
||||
|
||||
# # 创建非 root 用户
|
||||
# RUN useradd -m -u 1000 phpuser && chown -R phpuser:phpuser /var/www/html
|
||||
|
||||
# 设置权限, 防止以下目录无法写入的问题
|
||||
RUN chmod -R a+rw /var/www/html/runtime
|
||||
RUN chmod -R a+rw /var/www/html/uploads
|
||||
RUN chmod -R a+rw /var/www/html/tmp
|
||||
RUN chmod -R a+rw /var/www/html/temp
|
||||
|
||||
# USER phpuser
|
||||
|
||||
# 暴露端口
|
||||
EXPOSE 9000 9003
|
||||
|
||||
# 暴露端口,9000 为 PHP-FPM 端口,8080 为 WebSocket 端口
|
||||
EXPOSE 9000 8080
|
||||
|
||||
############ 查看 cron 进程
|
||||
## 查看 cron 进程
|
||||
@@ -95,4 +84,10 @@ EXPOSE 9000 9003
|
||||
#######################################
|
||||
|
||||
# 启动Supervisor
|
||||
# 添加在Dockerfile末尾,CMD命令之前
|
||||
COPY ./entrypoint.sh /usr/local/bin/
|
||||
RUN chmod +x /usr/local/bin/entrypoint.sh
|
||||
ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
|
||||
|
||||
# 修改CMD命令
|
||||
CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf"]
|
||||
344
docker/php/entrypoint.sh
Normal file
344
docker/php/entrypoint.sh
Normal file
@@ -0,0 +1,344 @@
|
||||
#!/bin/bash
|
||||
# 移除 set -e 以便更好的错误控制
|
||||
|
||||
echo "=== Web应用权限初始化 ==="
|
||||
|
||||
# 定义应用根目录,优先使用环境变量,否则使用默认值
|
||||
APP_ROOT="${PHP_APP_ROOT:-/var/www/html}"
|
||||
|
||||
echo "使用应用根目录: $APP_ROOT"
|
||||
|
||||
# 如果应用根目录不存在,则跳过权限设置
|
||||
if [ ! -d "$APP_ROOT" ]; then
|
||||
echo "❌ 应用根目录:'$APP_ROOT'不存在,跳过权限设置"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 创建统一的Web组并配置所有用户(最高效的权限管理)
|
||||
configure_web_users() {
|
||||
# 常见Web服务器用户列表
|
||||
WEB_USERS=("www-data" "www" "apache" "nginx")
|
||||
|
||||
# 获取环境变量中的用户ID
|
||||
TARGET_UID=${USER_ID:-33}
|
||||
TARGET_GID=${GROUP_ID:-33}
|
||||
|
||||
echo "配置统一Web组权限,目标UID:GID = $TARGET_UID:$TARGET_GID"
|
||||
|
||||
# 创建统一的Web组(增强错误处理)
|
||||
WEB_GROUP="webaccess"
|
||||
if ! getent group "$WEB_GROUP" &>/dev/null; then
|
||||
echo "创建统一Web组: $WEB_GROUP"
|
||||
|
||||
# 尝试使用指定GID创建组
|
||||
if groupadd -g $TARGET_GID "$WEB_GROUP" 2>/dev/null; then
|
||||
echo "✅ 统一Web组创建成功,GID: $TARGET_GID"
|
||||
else
|
||||
echo "⚠️ GID $TARGET_GID 已被占用,尝试自动分配GID"
|
||||
|
||||
# 尝试不指定GID创建组
|
||||
if groupadd "$WEB_GROUP" 2>/dev/null; then
|
||||
ACTUAL_GID=$(getent group "$WEB_GROUP" | cut -d: -f3)
|
||||
echo "✅ 统一Web组创建成功,自动分配GID: $ACTUAL_GID"
|
||||
else
|
||||
echo "❌ 创建 $WEB_GROUP 组失败,尝试使用备用方案"
|
||||
|
||||
# 备用方案:使用现有的www-data组
|
||||
if getent group "www-data" &>/dev/null; then
|
||||
WEB_GROUP="www-data"
|
||||
echo "🔄 使用现有的www-data组作为统一组"
|
||||
else
|
||||
echo "❌ 备用方案也失败,权限配置可能不完整"
|
||||
WEB_GROUP=""
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
else
|
||||
ACTUAL_GID=$(getent group "$WEB_GROUP" | cut -d: -f3)
|
||||
echo "✅ 统一Web组 $WEB_GROUP 已存在,GID: $ACTUAL_GID"
|
||||
fi
|
||||
|
||||
# 最终验证组是否存在
|
||||
if [ -z "$WEB_GROUP" ] || ! getent group "$WEB_GROUP" &>/dev/null; then
|
||||
echo "❌ 无法创建或找到可用的Web组,权限配置将受限"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# 只将已存在的Web用户加入统一组(增强错误处理)
|
||||
success_count=0
|
||||
total_users=0
|
||||
|
||||
for web_user in "${WEB_USERS[@]}"; do
|
||||
total_users=$((total_users + 1))
|
||||
|
||||
if id "$web_user" &>/dev/null; then
|
||||
echo "📝 处理Web用户: $web_user"
|
||||
|
||||
# 获取用户当前组信息(安全的变量处理)
|
||||
current_groups=$(id -Gn "$web_user" 2>/dev/null | tr '\n' ' ' | sed 's/ *$//')
|
||||
echo " 当前所属组: ${current_groups:-无}"
|
||||
|
||||
# 尝试将用户加入统一组(使用-a参数保留现有组,只添加新组)
|
||||
if usermod -a -G "$WEB_GROUP" "$web_user" 2>/dev/null; then
|
||||
echo " ✅ 成功将 $web_user 添加到统一组 $WEB_GROUP"
|
||||
success_count=$((success_count + 1))
|
||||
else
|
||||
echo " ⚠️ 无法将 $web_user 添加到统一组,尝试设置主组"
|
||||
|
||||
# 备用方案:设置主组
|
||||
if usermod -g "$WEB_GROUP" "$web_user" 2>/dev/null; then
|
||||
echo " ✅ 成功将 $web_user 主组设置为 $WEB_GROUP"
|
||||
success_count=$((success_count + 1))
|
||||
else
|
||||
echo " ❌ 无法配置 $web_user 的组权限"
|
||||
fi
|
||||
fi
|
||||
else
|
||||
echo "⚭ Web用户 $web_user 不存在,跳过"
|
||||
fi
|
||||
done
|
||||
|
||||
echo "📊 用户配置汇总: $success_count/$total_users 个Web用户配置成功"
|
||||
|
||||
# 至少要有一个用户配置成功
|
||||
if [ $success_count -eq 0 ]; then
|
||||
echo "⚠️ 没有Web用户被成功配置,但继续执行"
|
||||
fi
|
||||
|
||||
echo "统一Web组配置完成"
|
||||
}
|
||||
|
||||
# 错误处理:如果配置失败,不要终止整个脚本
|
||||
configure_web_users || echo "⚠️ Web用户配置出现问题,但继续执行权限设置"
|
||||
|
||||
echo "当前用户: $(whoami)"
|
||||
echo "UID: $(id -u), GID: $(id -g)"
|
||||
|
||||
# 修复所有目录权限(使用统一Web组,最高效的权限管理)
|
||||
if [ -d "$APP_ROOT" ]; then
|
||||
# 重新获取最终的WEB_GROUP(可能已被修改)
|
||||
FINAL_WEB_GROUP=""
|
||||
|
||||
# 首选:使用创建的webaccess组
|
||||
if getent group "webaccess" &>/dev/null; then
|
||||
FINAL_WEB_GROUP="webaccess"
|
||||
echo "🎯 使用创建的统一Web组: $FINAL_WEB_GROUP"
|
||||
# 备选:使用www-data组
|
||||
elif getent group "www-data" &>/dev/null; then
|
||||
FINAL_WEB_GROUP="www-data"
|
||||
echo "🔄 回退到www-data组: $FINAL_WEB_GROUP"
|
||||
# 最后:使用当前用户的组
|
||||
else
|
||||
CURRENT_USER=$(whoami)
|
||||
CURRENT_GROUP=$(id -gn "$CURRENT_USER")
|
||||
FINAL_WEB_GROUP="$CURRENT_GROUP"
|
||||
echo "🔧 使用当前用户组: $FINAL_WEB_GROUP"
|
||||
fi
|
||||
|
||||
# 最终验证
|
||||
if [ -z "$FINAL_WEB_GROUP" ]; then
|
||||
echo "❌ 无法确定有效的Web组,跳过权限设置"
|
||||
echo "=== 启动应用 ==="
|
||||
exec "$@"
|
||||
fi
|
||||
|
||||
WEB_GROUP="$FINAL_WEB_GROUP"
|
||||
WEB_GROUP_GID=$(getent group "$WEB_GROUP" | cut -d: -f3)
|
||||
echo "✅ 最终使用Web组: $WEB_GROUP (GID: $WEB_GROUP_GID)"
|
||||
echo "🔒 统一组权限模式:所有Web用户通过组继承权限"
|
||||
|
||||
# 设置所有权为统一Web组(增强错误处理)
|
||||
echo "📁 设置应用目录所有权为统一Web组"
|
||||
CURRENT_USER=$(whoami)
|
||||
|
||||
if chown -R $CURRENT_USER:$WEB_GROUP "$APP_ROOT" 2>/dev/null; then
|
||||
echo "✅ 所有权设置成功: $CURRENT_USER:$WEB_GROUP"
|
||||
else
|
||||
echo "⚠️ 所有权设置失败,尝试只设置组权限"
|
||||
chgrp -R "$WEB_GROUP" "$APP_ROOT" 2>/dev/null || echo "❌ 组权限设置也失败"
|
||||
fi
|
||||
|
||||
# 设置目录权限为775(组权限为rwx,所有组内用户都有完整权限)
|
||||
echo "🔐 设置目录权限775,文件权限664"
|
||||
|
||||
# 使用更安全的权限设置方式,避免权限被拒绝
|
||||
dir_count=0
|
||||
file_count=0
|
||||
|
||||
# 设置目录权限(兼容性更好的方式)
|
||||
if command -v find >/dev/null 2>&1; then
|
||||
dir_count=$(find "$APP_ROOT" -type d -exec chmod 775 {} \; 2>/dev/null | wc -l)
|
||||
file_count=$(find "$APP_ROOT" -type f -exec chmod 664 {} \; 2>/dev/null | wc -l)
|
||||
find "$APP_ROOT" -type d -exec chmod g+s {} \; 2>/dev/null
|
||||
else
|
||||
# 备用方案:使用简单的循环
|
||||
echo "find命令不可用,跳过批量权限设置"
|
||||
dir_count=0
|
||||
file_count=0
|
||||
fi
|
||||
|
||||
echo "📊 权限设置完成: $dir_count个目录, $file_count个文件"
|
||||
|
||||
echo "✅ 统一组权限设置完成,所有Web用户通过组获得权限"
|
||||
|
||||
# 设置ACL(如果支持,只需设置统一组)
|
||||
if command -v setfacl >/dev/null 2>&1; then
|
||||
echo "🔒 设置ACL权限(只需设置统一Web组)"
|
||||
|
||||
acl_success=0
|
||||
|
||||
# 只为统一Web组设置ACL权限(限制处理深度)
|
||||
if setfacl -R -m g:$WEB_GROUP:rwx "$APP_ROOT" 2>/dev/null; then
|
||||
echo " ✅ 设置组ACL权限成功"
|
||||
acl_success=$((acl_success + 1))
|
||||
else
|
||||
echo " ❌ 设置组ACL权限失败"
|
||||
fi
|
||||
|
||||
# 设置默认ACL权限(新创建的文件自动继承权限)
|
||||
if setfacl -dR -m g:$WEB_GROUP:rwx "$APP_ROOT" 2>/dev/null; then
|
||||
echo " ✅ 设置默认ACL权限成功"
|
||||
acl_success=$((acl_success + 1))
|
||||
else
|
||||
echo " ❌ 设置默认ACL权限失败"
|
||||
fi
|
||||
|
||||
if [ $acl_success -eq 2 ]; then
|
||||
echo "🎉 统一组ACL设置完成,所有组内用户自动获得权限"
|
||||
elif [ $acl_success -eq 1 ]; then
|
||||
echo "⚠️ ACL部分设置成功,建议检查文件系统ACL支持"
|
||||
else
|
||||
echo "❌ ACL设置完全失败,文件系统可能不支持ACL"
|
||||
fi
|
||||
else
|
||||
echo "ℹ️ ACL不支持,依赖传统权限模式"
|
||||
echo "✅ 775权限已足够,所有组内用户都有rwx权限"
|
||||
fi
|
||||
|
||||
# 设置umask
|
||||
umask 0002
|
||||
|
||||
echo "✅ 应用目录权限修复完成"
|
||||
|
||||
# 验证文件权限是否足够(测试统一组权限效果)
|
||||
echo "=== 验证统一组权限效果 ==="
|
||||
|
||||
# 查找测试文件的更可靠方法
|
||||
test_file=""
|
||||
|
||||
# 方法1: 查找index.html
|
||||
if [ -f "$APP_ROOT/index.html" ]; then
|
||||
test_file="$APP_ROOT/index.html"
|
||||
fi
|
||||
|
||||
# 方法2: 查找任意HTML文件(更安全的方式)
|
||||
if [ -z "$test_file" ]; then
|
||||
first_html=$(find "$APP_ROOT" -maxdepth 2 -name "*.html" -type f 2>/dev/null | head -1)
|
||||
if [ -n "$first_html" ] && [ -f "$first_html" ]; then
|
||||
test_file="$first_html"
|
||||
fi
|
||||
fi
|
||||
|
||||
# 方法3: 查找index.php
|
||||
if [ -z "$test_file" ] && [ -f "$APP_ROOT/index.php" ]; then
|
||||
test_file="$APP_ROOT/index.php"
|
||||
fi
|
||||
|
||||
# 方法4: 创建专用测试文件(最安全的选择)
|
||||
if [ -z "$test_file" ]; then
|
||||
test_file="$APP_ROOT/.permission_test.html"
|
||||
echo "创建专用权限测试文件"
|
||||
cat > "$test_file" << 'EOF'
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head><title>Permission Test</title></head>
|
||||
<body><h1>Web Server Permission Test File</h1></body>
|
||||
</html>
|
||||
EOF
|
||||
# 设置正确的权限
|
||||
chown $(whoami):"$WEB_GROUP" "$test_file" 2>/dev/null || true
|
||||
chmod 664 "$test_file"
|
||||
fi
|
||||
|
||||
# 执行权限测试
|
||||
if [ -f "$test_file" ]; then
|
||||
echo "使用测试文件: $test_file"
|
||||
echo "文件权限: $(stat -c '%a %n' "$test_file")"
|
||||
echo "文件所有者: $(stat -c '%U:%G' "$test_file")"
|
||||
|
||||
# 测试所有Web用户的权限(通过组权限)
|
||||
for test_user in "www-data" "www" "apache" "nginx"; do
|
||||
if id "$test_user" &>/dev/null; then
|
||||
echo "🔍 测试 $test_user 用户权限(通过组权限):"
|
||||
|
||||
# 显示用户组信息(安全的变量处理)
|
||||
user_groups=$(id -Gn "$test_user" 2>/dev/null | tr '\n' ' ' | sed 's/ *$//')
|
||||
echo " 📋 所属组: ${user_groups:-无}"
|
||||
|
||||
# 测试读权限(安全:只读不修改)
|
||||
if su -s /bin/sh -c "cat '$test_file' >/dev/null 2>&1" "$test_user" 2>/dev/null; then
|
||||
echo " ✅ 读权限: 通过组权限可读"
|
||||
else
|
||||
echo " ❌ 读权限: 不可读"
|
||||
fi
|
||||
|
||||
# 测试写权限(使用临时文件,避免污染原文件)
|
||||
temp_test_file="${test_file}.write_test_${test_user}"
|
||||
if su -s /bin/sh -c "echo 'permission_test' > '$temp_test_file' 2>/dev/null" "$test_user" 2>/dev/null; then
|
||||
echo " ✅ 写权限: 通过组权限可写"
|
||||
rm -f "$temp_test_file" 2>/dev/null
|
||||
else
|
||||
echo " ❌ 写权限: 不可写"
|
||||
fi
|
||||
|
||||
# 测试目录创建权限(使用临时目录)
|
||||
temp_test_dir="${APP_ROOT}/.perm_test_${test_user}"
|
||||
if su -s /bin/sh -c "mkdir -p '$temp_test_dir' 2>/dev/null" "$test_user" 2>/dev/null; then
|
||||
echo " ✅ 创建目录: 通过组权限可创建"
|
||||
rm -rf "$temp_test_dir" 2>/dev/null
|
||||
else
|
||||
echo " ❌ 创建目录: 不可创建"
|
||||
fi
|
||||
|
||||
echo " 🔗 权限来源: 统一Web组 ($WEB_GROUP) 775权限"
|
||||
break # 只测试第一个可用的用户即可验证效果
|
||||
fi
|
||||
done
|
||||
|
||||
# 清理专用测试文件(如果是创建的)
|
||||
if echo "$test_file" | grep -q "\.permission_test\.html$"; then
|
||||
rm -f "$test_file" 2>/dev/null
|
||||
echo "🧹 已清理临时测试文件"
|
||||
fi
|
||||
else
|
||||
echo "❌ 无法找到或创建测试文件,跳过权限验证"
|
||||
fi
|
||||
|
||||
# 显示统一组和用户状态
|
||||
echo "=== 统一Web组状态检查 ==="
|
||||
if getent group "$WEB_GROUP" &>/dev/null; then
|
||||
echo "✅ 统一Web组 '$WEB_GROUP' 存在"
|
||||
echo "组信息: $(getent group "$WEB_GROUP" 2>/dev/null || echo '获取失败')"
|
||||
|
||||
# 检查哪些用户在统一组中
|
||||
echo "统一组成员检查:"
|
||||
for web_user in "www-data" "www" "apache" "nginx"; do
|
||||
if id "$web_user" &>/dev/null; then
|
||||
if id -Gn "$web_user" | grep -q "$WEB_GROUP"; then
|
||||
echo "✅ $web_user 在统一组 '$WEB_GROUP' 中"
|
||||
else
|
||||
echo "❌ $web_user 不在统一组 '$WEB_GROUP' 中"
|
||||
fi
|
||||
else
|
||||
echo "❌ $web_user 用户不存在"
|
||||
fi
|
||||
done
|
||||
else
|
||||
echo "❌ 统一Web组 '$WEB_GROUP' 不存在"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "=== 启动应用 ==="
|
||||
|
||||
# 执行原有的启动命令
|
||||
exec "$@"
|
||||
@@ -12,19 +12,42 @@ autostart=true
|
||||
autorestart=true
|
||||
startretries=3
|
||||
startsecs=1
|
||||
stdout_logfile=/dev/stdout
|
||||
stdout_logfile_maxbytes=0
|
||||
stderr_logfile=/dev/stderr
|
||||
stderr_logfile_maxbytes=0
|
||||
stopasgroup=true
|
||||
killasgroup=true
|
||||
stdout_logfile=/var/log/supervisor/php-fpm.log
|
||||
stdout_logfile_maxbytes=10MB
|
||||
stdout_logfile_backups=10
|
||||
stderr_logfile=/var/log/supervisor/php-fpm-error.log
|
||||
stderr_logfile_maxbytes=10MB
|
||||
stderr_logfile_backups=10
|
||||
|
||||
[program:think-cron]
|
||||
command=php /var/www/html/think cron:schedule
|
||||
environment=APP_ENV=local
|
||||
process_name=%(program_name)s_%(process_num)02d
|
||||
numprocs=1
|
||||
autostart=true
|
||||
autorestart=true
|
||||
startretries=5
|
||||
startsecs=2
|
||||
stdout_logfile=/dev/stdout
|
||||
stdout_logfile_maxbytes=0
|
||||
stderr_logfile=/dev/stderr
|
||||
stderr_logfile_maxbytes=0
|
||||
startretries=3
|
||||
stdout_logfile=/var/log/supervisor/think-cron.log
|
||||
stdout_logfile_maxbytes=10MB
|
||||
stdout_logfile_backups=10
|
||||
stderr_logfile=/var/log/supervisor/think-cron-error.log
|
||||
stderr_logfile_maxbytes=10MB
|
||||
stderr_logfile_backups=10
|
||||
startsecs=3
|
||||
stopwaitsecs=10
|
||||
|
||||
[program:websocket-server]
|
||||
command=/bin/bash -c "if [ -f /var/www/html/ws_server.php ]; then php ./ws_server.php; else echo 'ws_server.php not found, skipping websocket server'; fi"
|
||||
workdir=/var/www/html
|
||||
autostart=true
|
||||
autorestart=false
|
||||
startretries=0
|
||||
stdout_logfile=/var/log/supervisor/websocket-server.log
|
||||
stdout_logfile_maxbytes=10MB
|
||||
stdout_logfile_backups=10
|
||||
stderr_logfile=/var/log/supervisor/websocket-server-error.log
|
||||
stderr_logfile_maxbytes=10MB
|
||||
stderr_logfile_backups=10
|
||||
startsecs=3
|
||||
stopwaitsecs=10
|
||||
|
||||
BIN
docker/php/websocat
Normal file
BIN
docker/php/websocat
Normal file
Binary file not shown.
9
docker/redis_data/.gitignore
vendored
Normal file
9
docker/redis_data/.gitignore
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
# 忽略所有文件
|
||||
*
|
||||
|
||||
# 只保留指定的 .gitkeep 文件
|
||||
!.gitignore
|
||||
!development/.gitkeep
|
||||
!test/.gitkeep
|
||||
!production/.gitkeep
|
||||
!staging/.gitkeep
|
||||
0
docker/redis_data/.gitkeep
Normal file
0
docker/redis_data/.gitkeep
Normal file
10
docker/xdebug_logs/.gitignore
vendored
Normal file
10
docker/xdebug_logs/.gitignore
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
# 忽略目录下所有文件和子目录
|
||||
*
|
||||
# 忽略所有子目录
|
||||
*/
|
||||
# 但不忽略 .gitkeep 文件
|
||||
!.gitkeep
|
||||
# 不忽略 .gitignore 文件自身
|
||||
!.gitignore
|
||||
# 不忽略 development/.gitkeep 文件
|
||||
!development/.gitkeep
|
||||
0
docker/xdebug_logs/.gitkeep
Normal file
0
docker/xdebug_logs/.gitkeep
Normal file
128
docs/GIT_REALEASE.md
Normal file
128
docs/GIT_REALEASE.md
Normal file
@@ -0,0 +1,128 @@
|
||||
适用于中大型团队的方案主要有两种:
|
||||
|
||||
---
|
||||
|
||||
## ✅ 推荐方案一:**GitFlow(适合版本发布节奏明确的项目)**
|
||||
|
||||
### 📌 核心分支说明
|
||||
|
||||
| 分支 | 作用 | 生命周期 | 是否长期存在 |
|
||||
|------|------|--------|------------|
|
||||
| `main`(或 `master`) | **生产环境代码**,每个 commit 对应一个可发布版本 | 永久 | ✅ |
|
||||
| `develop` | **集成开发分支**,最新开发成果,用于测试环境部署 | 永久 | ✅ |
|
||||
| `feature/*` | 功能开发分支(如 `feature/user-auth`) | 临时 | ❌ |
|
||||
| `release/*` | 发布准备分支(如 `release/v1.2.0`) | 临时 | ❌ |
|
||||
| `hotfix/*` | 紧急线上修复分支(如 `hotfix/login-bug`) | 临时 | ❌ |
|
||||
|
||||
### 🔁 典型流程
|
||||
|
||||
1. **日常开发**
|
||||
- 从 `develop` 拉出 `feature/xxx`
|
||||
- 开发完成后,**PR/MR 合并回 `develop`**
|
||||
|
||||
2. **准备发布**
|
||||
- 从 `develop` 拉出 `release/vX.Y.Z`
|
||||
- 在此分支修复 bug、更新版本号、生成 changelog
|
||||
- 测试通过后:
|
||||
- 合并到 `main`(打 tag,如 `v1.2.0`)
|
||||
- 合并回 `develop`(同步修复)
|
||||
|
||||
3. **紧急修复**
|
||||
- 从 `main`(或对应 tag)拉出 `hotfix/xxx`
|
||||
- 修复后:
|
||||
- 合并到 `main`(打新 patch tag,如 `v1.2.1`)
|
||||
- 合并到 `develop`
|
||||
|
||||
### ✅ 优点
|
||||
- 版本清晰,适合有明确发布周期的系统(如每月发版)
|
||||
- 支持并行开发与紧急修复
|
||||
- `main` 始终代表线上状态
|
||||
|
||||
### ⚠️ 缺点
|
||||
- 分支较多,对小型团队略显复杂
|
||||
- 不适合持续部署(CI/CD 频繁上线)场景
|
||||
|
||||
---
|
||||
|
||||
## ✅ 推荐方案二:**Trunk-Based Development + Release Branches(适合 DevOps / 持续交付)**
|
||||
|
||||
> 越来越多互联网公司(如 Google、Facebook、Netflix)采用此模式,尤其适合**高频发布、自动化 CI/CD** 的后台服务。
|
||||
|
||||
### 📌 核心分支说明
|
||||
|
||||
| 分支 | 作用 |
|
||||
|------|------|
|
||||
| `main`(或 `trunk`) | **唯一主干分支**,所有开发直接或间接流向这里,保持可随时发布状态 |
|
||||
| `release/*`(可选) | 仅在需要维护多个线上版本时使用(如 `release/v1.3`) |
|
||||
| `feature/*`(短生命周期) | 功能分支,**必须短(<1天~2天)**,通过 PR 快速合并到 `main` |
|
||||
|
||||
> 💡 实践中常配合 **Feature Toggle(功能开关)**,即使未完成的功能也可合入 `main`,但默认关闭。
|
||||
|
||||
### 🔁 典型流程
|
||||
|
||||
1. 开发者从 `main` 拉出短命 `feature/xxx`
|
||||
2. 提交 PR → 自动化测试(单元、集成、安全扫描)→ Code Review
|
||||
3. 合并到 `main`
|
||||
4. **CI/CD 自动部署到测试/预发环境**
|
||||
5. 人工验证后,**一键发布到生产**(或自动金丝雀发布)
|
||||
6. 若需回滚,直接回退 `main` 的 commit 或触发回滚流程
|
||||
|
||||
### ✅ 优点
|
||||
- 极简分支模型,减少合并冲突
|
||||
- 支持每天多次发布
|
||||
- 与现代 CI/CD 工具链(Jenkins, GitLab CI, ArgoCD 等)天然契合
|
||||
|
||||
### ⚠️ 要求
|
||||
- 强大的自动化测试覆盖(>80%)
|
||||
- 快速 Code Review 文化
|
||||
- 监控与快速回滚能力
|
||||
|
||||
---
|
||||
|
||||
## 🏆 企业后台服务推荐选择
|
||||
|
||||
| 场景 | 推荐策略 |
|
||||
|------|--------|
|
||||
| **传统企业、月度/季度发版、强合规要求** | ✅ **GitFlow** |
|
||||
| **互联网公司、SaaS 服务、每日/每小时发布** | ✅ **Trunk-Based + Short-lived Feature Branches** |
|
||||
| **混合模式(主干开发 + 定期大版本)** | 主干开发为主,大版本前切 `release` 分支 |
|
||||
|
||||
> 🔔 **当前趋势**:越来越多企业后台服务(尤其是微服务架构)倾向于 **Trunk-Based**,因为:
|
||||
> - 后台服务通常无“客户端版本”约束
|
||||
> - 可独立部署、灰度发布
|
||||
> - 自动化程度高
|
||||
|
||||
---
|
||||
|
||||
## 🔐 补充建议(无论哪种策略)
|
||||
|
||||
1. **保护关键分支**
|
||||
- 在 GitHub/GitLab 中设置:
|
||||
- `main` / `develop` 为 **protected branch**
|
||||
- 要求 **PR/MR + 至少1人审批 + CI 通过**
|
||||
- (可选)要求 **GPG 签名提交**
|
||||
|
||||
2. **标准化 Commit & PR 模板**
|
||||
- 使用 Conventional Commits(如 `feat:`, `fix:`, `chore:`)
|
||||
- 自动生成 changelog 和版本号(配合 semantic-release)
|
||||
|
||||
3. **Tag 语义化版本**
|
||||
- 所有生产发布必须打 tag:`v1.2.3`
|
||||
- 格式遵循 [SemVer](https://semver.org/)
|
||||
|
||||
4. **禁止直接 push 到主干**
|
||||
- 所有代码必须通过 PR/MR 合并
|
||||
|
||||
---
|
||||
|
||||
## 📌 总结:最优实践(推荐)
|
||||
|
||||
> **对于大多数现代企业后台服务,采用:**
|
||||
> **✅ Trunk-Based Development(主干开发) + 短生命周期 feature 分支 + 自动化 CI/CD**
|
||||
> 是最高效、可扩展、符合 DevOps 理念的方式。
|
||||
|
||||
只有在**强版本管控、多客户定制、无法频繁上线**等特殊场景下,才考虑 GitFlow。
|
||||
|
||||
---
|
||||
|
||||
如果你能提供更多信息(如团队规模、发布频率、是否微服务、CI/CD 成熟度),我可以给出更定制化的建议!
|
||||
26
docs/addon/huaweipay/README.md
Normal file
26
docs/addon/huaweipay/README.md
Normal file
@@ -0,0 +1,26 @@
|
||||
# 华为支付插件
|
||||
|
||||
|
||||
## Demo
|
||||
|
||||
```
|
||||
安卓快应用ID:115644647
|
||||
安卓快应用包名:com.jieganfsj.fivegshop
|
||||
安卓快应用名称:秸秆粉碎机
|
||||
|
||||
|
||||
商户名称:徐州明文机械有限公司
|
||||
商户号:102751500028
|
||||
|
||||
开发者ID:10086000901972225
|
||||
支付ID:10086000901972225
|
||||
公钥:
|
||||
MIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEA9g1+QcqvC4f1pUiwJ1um1iBUlNn6hRDJrNdv5zB77l5DNo6S6hE4w7VyhkMnkIk89i8kTej1m1ByjRpo7B5OPqafNqI9JBQyQ26A1Zp71zSfe/UicAFiMtF4lWNnAHBYH06sUTvybwYllDVybpi6lL2i8VAGIN8YgoK36lPaYsxWZ911lPCegy7B3kDj1xhBe41cNHgu8wYmjqLU7njleY5Pseherx+Kb58aQvB5xQr8w7KgAyMrsfRH30Btpg/ZWRn8qOXd/DW6eEla3djah4ug8jKdi0qUkA24FLDdOZST4vb5qhgQDVXpqJhYmBIU14YOHsCX9Olu6b7DDjQo/dvOaY3vzWROfV+sV60fUVIps8Vy1EpS/UXeHUxg6r37U8WAxUbSV8d6e4VylLuiIgbX5JpSC1s7jq/cwUwXfSJmKzaCj+C+LJ958IM17FYxIz5xWJtZEzWsPAH7WVCP3b1m4MHU/UwGuMu/Gfdzusnr+Qtan6Wqn9AqUyJP/JfrAgMBAAE=
|
||||
|
||||
支付私钥:
|
||||
MIIG/QIBADANBgkqhkiG9w0BAQEFAASCBucwggbjAgEAAoIBgQD2DX5Byq8Lh/WlSLAnW6bWIFSU2fqFEMms12/nMHvuXkM2jpLqETjDtXKGQyeQiTz2LyRN6PWbUHKNGmjsHk4+pp82oj0kFDJDboDVmnvXNJ979SJwAWIy0XiVY2cAcFgfTqxRO/JvBiWUNXJumLqUvaLxUAYg3xiCgrfqU9pizFZn3XWU8J6DLsHeQOPXGEF7jVw0eC7zBiaOotTueOV5jk+x6F6vH4pvnxpC8HnFCvzDsqADIyux9EffQG2mD9lZGfyo5d38Nbp4SVrd2NqHi6DyMp2LSpSQDbgUsN05lJPi9vmqGBANVemomFiYEhTXhg4ewJf06W7pvsMONCj9285pje/NZE59X6xXrR9RUimzxXLUSlL9Rd4dTGDqvftTxYDFRtJXx3p7hXKUu6IiBtfkmlILWzuOr9zBTBd9ImYrNoKP4L4sn3nwgzXsVjEjPnFYm1kTNaw8AftZUI/dvWbgwdT9TAa4y78Z93O6yev5C1qfpaqf0CpTIk/8l+sCAwEAAQKCAYAVlJFiS9iWdlJBMOLiUNONLEC+3W9vhE1r72lNKZ91BKd4fYC9Ls1/vMZSqEksEB1cqj3Q54HDIYcqgQp6yx2puQt1yzz5kRvndiWulmIOOftS7+kZUcW/F0gwMguyqifQdyH97fgRbMSW/ykOMi8LJKbJ627eKzMHH1fqIXih+bIKYg4SBhihANTYHXDeSK5Vm8xefbwAbKWtFPMAB3J4+tZakDrduTJ3H8k53cWQVqpcr6oBHHCUpww2tHvpeLI3a/FXyHYBqrx8ErnXCjkVBHBtwQf+43H+buyDjrYUwJUi3RSJgeefcyyJoO0I9GwGb6nY8kK5kZ0aeIIkfiVOexMYS9w11FVl96LjC7HjJQrNY4jOLI/X76xyEwBFy9LNogRTafjZVZmVRj/9Kembx6+/eDxvyEv5FnelsHqfFbv1KPkR6e7FvwpDbYgJBpfKTE6SICqo52bHxewpSL7KHRlTpp0IfMM/IOOs8CCwI37ixKQun6W4en457j/tP4ECgcEA+0pa3omCKJfiVR4PpyXY+t9FEqdcZkdYEDtCSX9vqu6yE4sdt7rNnZ6cIiD6ViC2xvQ5VLry1y57JvP57svt5OJYhmILuj3jJ7pdcfKRAVaV4W7C8vGMit6ssckEOnOSj/bvEazfHorjXKU9cn9uCymczRQC9azaRkN+1LLk2mBbczghxd7rSJyXtbt9NVNUi9Gr11AzJsrWN/Bqqna5BizBLIaGYthJ/04LYTl1AjWPEJXjXI72/WvMdQ/w6itxAoHBAPqqAi5mbvjRDitmLQchc/R3Yl329OaB3GSz3YtFqfk9lLJIyl91+MzxBg1ChHnRFC8MB1s1Z05ZkFmcy8AVqvJ6MSZhUCLNjq8HyVgp24CqQTr8ciomF03ncXnKKSsH7tidInkQ8R3e7qRRGQMnDvV+Hs3FjlOk8vJyZCcGkUCVA0kkwkk/95qIfrZsthtewAxJR/rY63FnG+MUayEBB1lwyJZ7xDi/GyX6SAnPBl6bLRA1BdBeTV8K0MEwwIqzGwKBwBqo+9UKT7XQz2FqbAy2tjt/fouJGAN95DjsoI69p3JCGsB6DPAWMIRddIEmcIi8tceL151GrEbqFoS+c7DDD/0timjPdCEROc1YN1vEeV/j+MjPAH3X5KpDD51ZD0rIQi9l6l08svtBjvegTFGedWVXx9v2GI5KBWpY9NbKF/+XI3yo4uRkTyAIBQxx1MnYimq/FvUj/BlMgceziQ2GxQCDtQbtSsqn2cntVMW+28wdNI106YdDX67pRerRgyTE8QKBwHAwLxG9XuWWC5V5AaYzXsaHuEr+ANY6QP4BUqLG5zBaU3cIBSt8jYKMTX0ZzFkJLtNvussjt7zlcSnqd3bdO8mSzvSykT9CaR4FiiQfd9K6YL+ZxS8AJWYEtFEiHhLYVho1Gfy9jG0mHgEFGwDCNnvBmt/WD8F4DhRdBl5BHjmdd/8AqMRIEPXlKXFUbp0JZ0MYeVLYS2hSEbUsqlX3M+bgB6bydfw/7FKvFhbtxZgKM70RPizoSBDFsnEE9OgfCQKBwQD4M4f2f678YScvrwQnV2J8SSnJhQbZqht1Rlb34zU0JIcfpwOKpoNvRyZz1BxvzPSm5S3TLDytK16uvAsMFCHVByyCajPozlWnLzeEqvE4DZXZcWeY+/+MZ4nsajLTTeJWos6UrazZcd8/2c301x4OUKfgZWW1tS65MA48X0+Y4uPyOuH365wQ0obBfg4aB85sZl71v7Rveq+COHBstp/BdDVAsdufWCr7Nkbeu/3qh886ZumpYe+89So1oDIuoys=
|
||||
|
||||
|
||||
```
|
||||
|
||||
支付密钥用于交易过程中的签名认证,请妥善保管,谨防泄露,签名认证规范参考开发指南服务端开发。
|
||||
660
docs/api_kefu.md
Normal file
660
docs/api_kefu.md
Normal file
@@ -0,0 +1,660 @@
|
||||
# 智能客服API接口文档
|
||||
|
||||
## 一、接口说明
|
||||
|
||||
本接口用于连接微信小程序与Dify聊天机器人,实现智能客服功能。支持流式和非流式两种响应模式,具备完整的事务保护、数据一致性和状态管理功能。
|
||||
|
||||
## 二、配置说明
|
||||
|
||||
### 1. 安装插件
|
||||
|
||||
在ThinkPHP后台的插件管理页面中,找到智能客服插件(aikefu)并点击安装按钮。
|
||||
|
||||
### 2. 配置插件
|
||||
|
||||
1. 进入智能客服配置页面
|
||||
2. 输入从Dify平台获取的API密钥
|
||||
3. 配置API基础地址(默认:https://api.dify.ai/v1)
|
||||
4. 配置聊天接口端点(默认:/chat-messages)
|
||||
5. 启用智能客服功能
|
||||
|
||||
### 3. 获取Dify API密钥
|
||||
|
||||
1. 登录Dify平台
|
||||
2. 进入工作台
|
||||
3. 选择您的聊天机器人项目
|
||||
4. 点击"发布"按钮
|
||||
5. 在API访问页面获取API密钥
|
||||
|
||||
## 三、接口列表
|
||||
|
||||
### 1. 系统健康检查
|
||||
|
||||
**接口地址**:/api/kefu/health
|
||||
|
||||
**请求方式**:POST
|
||||
|
||||
**请求参数**:
|
||||
|
||||
| 参数名 | 类型 | 必填 | 说明 |
|
||||
| ------ | ---- | ---- | ---- |
|
||||
| uniacid | int | 是 | 站点ID |
|
||||
| check_type | string | 否 | 检查类型:full(完整)、basic(基础)、ai_service(AI服务),默认full |
|
||||
|
||||
**响应示例**:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"message": "success",
|
||||
"data": {
|
||||
"status": "healthy",
|
||||
"check_id": "health_56789",
|
||||
"timestamp": "2023-12-25 10:30:45",
|
||||
"total_checks": 2,
|
||||
"passed_checks": 2,
|
||||
"failed_checks": 0,
|
||||
"response_time_ms": 170,
|
||||
"components": {
|
||||
"ai_service": {
|
||||
"status": "healthy",
|
||||
"message": "AI服务正常",
|
||||
"response_time_ms": 150
|
||||
},
|
||||
"database": {
|
||||
"status": "healthy",
|
||||
"message": "数据库连接正常",
|
||||
"response_time_ms": 20
|
||||
}
|
||||
},
|
||||
"warnings": [],
|
||||
"errors": []
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 获取服务配置信息
|
||||
|
||||
**接口地址**:/api/kefu/info
|
||||
|
||||
**请求方式**:POST
|
||||
|
||||
**请求参数**:
|
||||
|
||||
| 参数名 | 类型 | 必填 | 说明 |
|
||||
| ------ | ---- | ---- | ---- |
|
||||
| uniacid | int | 是 | 站点ID |
|
||||
| member_id | int | 否 | 会员ID |
|
||||
| token | string | 否 | 访问令牌 |
|
||||
|
||||
**响应示例**:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"message": "success",
|
||||
"data": {
|
||||
"enabled": true,
|
||||
"status": "enabled"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 智能客服聊天接口
|
||||
|
||||
**接口地址**:`/api/kefu/chat`
|
||||
|
||||
**请求方式**:POST 或 GET(流式模式支持EventSource)
|
||||
|
||||
**请求参数**:
|
||||
|
||||
| 参数名 | 类型 | 必填 | 说明 |
|
||||
| ------ | ---- | ---- | ---- |
|
||||
| query | string | 是 | 用户输入的消息内容 |
|
||||
| user_id | string | 否 | 用户ID,默认使用当前登录会员ID |
|
||||
| conversation_id | string | 否 | 会话ID,第一次聊天可不传,系统会自动创建 |
|
||||
| stream | bool | 否 | 是否使用流式响应,默认false |
|
||||
| response_mode | string | 否 | 响应模式:streaming(流式)、blocking(阻塞),默认streaming |
|
||||
| uniacid | int | 是 | 站点ID |
|
||||
|
||||
**响应示例**:
|
||||
|
||||
#### 非流式响应(stream=false 或 response_mode=blocking)
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"message": "success",
|
||||
"data": {
|
||||
"conversation_id": "conv_123456789",
|
||||
"answer": "您好,我是智能客服,有什么可以帮助您的?",
|
||||
"message_id": "msg_123456789",
|
||||
"finish_reason": "stop",
|
||||
"usage": {
|
||||
"prompt_tokens": 10,
|
||||
"completion_tokens": 20,
|
||||
"total_tokens": 30
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 流式响应(stream=true 或 response_mode=streaming)
|
||||
|
||||
**响应格式**:Server-Sent Events (SSE)
|
||||
|
||||
**响应示例**:
|
||||
|
||||
```javascript
|
||||
data: {"event":"message","answer":"您好","conversation_id":"conv_123456789","message_id":"msg_123456789"}
|
||||
|
||||
data: {"event":"message","answer":",我是智能客服,","conversation_id":"conv_123456789","message_id":"msg_123456789"}
|
||||
|
||||
data: {"event":"message","answer":"有什么可以帮助您的?","conversation_id":"conv_123456789","message_id":"msg_123456789"}
|
||||
|
||||
data: {"event":"message_end","conversation_id":"conv_123456789","message_id":"msg_123456789"}
|
||||
|
||||
data: {"event":"done","data":{"conversation_id":"conv_123456789","message_id":"msg_123456789","content":"您好,我是智能客服,有什么可以帮助您的?"}}
|
||||
|
||||
data: {"event":"close","data":{"conversation_id":"conv_123456789","message_id":"msg_123456789"}}
|
||||
```
|
||||
|
||||
### 4. 获取会话历史
|
||||
|
||||
**接口地址**:/api/kefu/getHistory
|
||||
|
||||
**请求方式**:POST
|
||||
|
||||
**请求参数**:
|
||||
|
||||
| 参数名 | 类型 | 必填 | 说明 |
|
||||
| ------ | ---- | ---- | ---- |
|
||||
| uniacid | int | 是 | 站点ID |
|
||||
| conversation_id | string | 是 | 会话ID |
|
||||
| user_id | string | 否 | 用户ID,默认使用当前登录会员ID |
|
||||
| limit | int | 否 | 每页条数,默认20 |
|
||||
| offset | int | 否 | 偏移量,默认0 |
|
||||
| member_id | int | 否 | 会员ID |
|
||||
| token | string | 否 | 访问令牌 |
|
||||
|
||||
**响应示例**:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"message": "success",
|
||||
"data": {
|
||||
"messages": [
|
||||
{
|
||||
"id": "msg_123456789",
|
||||
"role": "user",
|
||||
"content": "您好",
|
||||
"create_time": 1703505845
|
||||
},
|
||||
{
|
||||
"id": "msg_123456790",
|
||||
"role": "assistant",
|
||||
"content": "您好,我是智能客服,有什么可以帮助您的?",
|
||||
"create_time": 1703505846
|
||||
}
|
||||
],
|
||||
"total": 2,
|
||||
"page_info": {
|
||||
"limit": 20,
|
||||
"offset": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 5. 清除会话历史
|
||||
|
||||
**接口地址**:`/api/kefu/clearConversation`
|
||||
|
||||
**请求方式**:POST
|
||||
|
||||
**请求参数**:
|
||||
|
||||
| 参数名 | 类型 | 必填 | 说明 |
|
||||
| ------ | ---- | ---- | ---- |
|
||||
| uniacid | int | 是 | 站点ID |
|
||||
| conversation_id | string | 否(与user_id二选一) | 会话ID(与user_id二选一) |
|
||||
| user_id | string | 否(与conversation_id二选一) | 用户ID,默认使用当前登录会员ID(与conversation_id二选一) |
|
||||
| member_id | int | 否 | 会员ID |
|
||||
| token | string | 否 | 访问令牌 |
|
||||
|
||||
**响应示例**:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"message": "success",
|
||||
"data": {}
|
||||
}
|
||||
```
|
||||
|
||||
## 四、前端调用示例
|
||||
|
||||
### 1. 非流式聊天(Fetch API)
|
||||
|
||||
```javascript
|
||||
// 非流式聊天
|
||||
async function chatWithAI(message, conversationId = '') {
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('query', message);
|
||||
formData.append('uniacid', '1');
|
||||
formData.append('stream', 'false');
|
||||
formData.append('response_mode', 'blocking');
|
||||
|
||||
if (conversationId) {
|
||||
formData.append('conversation_id', conversationId);
|
||||
}
|
||||
|
||||
const response = await fetch('/api/kefu/chat', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.code === 0) {
|
||||
return result.data;
|
||||
} else {
|
||||
console.error('聊天失败:', result.message);
|
||||
return null;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('聊天请求失败:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 流式聊天(EventSource)
|
||||
|
||||
```javascript
|
||||
// EventSource 流式聊天
|
||||
function chatWithAIEventSource(message, conversationId = '', onMessage, onComplete, onError) {
|
||||
// 关闭之前的连接
|
||||
if (window.currentEventSource) {
|
||||
window.currentEventSource.close();
|
||||
}
|
||||
|
||||
// 构建请求参数
|
||||
const params = new URLSearchParams({
|
||||
uniacid: '1',
|
||||
user_id: '123456',
|
||||
query: message,
|
||||
conversation_id: conversationId || '',
|
||||
stream: 'true'
|
||||
});
|
||||
|
||||
const url = `/api/kefu/chat?${params.toString()}`;
|
||||
|
||||
try {
|
||||
const eventSource = new EventSource(url);
|
||||
window.currentEventSource = eventSource;
|
||||
|
||||
let aiMessage = '';
|
||||
|
||||
// 监听消息事件
|
||||
eventSource.addEventListener('message', (event) => {
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
|
||||
if (data.event === 'message') {
|
||||
// 更新 AI 消息
|
||||
aiMessage += data.answer || '';
|
||||
if (onMessage) onMessage(data.answer || '');
|
||||
}
|
||||
|
||||
if (data.event === 'message_end') {
|
||||
// 对话完成
|
||||
if (onComplete) onComplete({
|
||||
conversation_id: data.conversation_id,
|
||||
message: aiMessage
|
||||
});
|
||||
}
|
||||
|
||||
if (data.conversation_id) {
|
||||
conversationId = data.conversation_id;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('解析消息失败:', error);
|
||||
}
|
||||
});
|
||||
|
||||
// 监听完成事件
|
||||
eventSource.addEventListener('done', (event) => {
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
if (onComplete) onComplete(data);
|
||||
} catch (error) {
|
||||
console.error('解析完成事件失败:', error);
|
||||
}
|
||||
});
|
||||
|
||||
// 监听关闭事件
|
||||
eventSource.addEventListener('close', (event) => {
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
console.log('连接正常结束:', data);
|
||||
} catch (error) {
|
||||
console.error('解析关闭事件失败:', error);
|
||||
}
|
||||
window.currentEventSource = null;
|
||||
});
|
||||
|
||||
// 监听错误事件
|
||||
eventSource.addEventListener('error', (error) => {
|
||||
console.error('EventSource错误:', error);
|
||||
if (onError) onError({ error: 'EventSource连接错误' });
|
||||
window.currentEventSource = null;
|
||||
});
|
||||
|
||||
return eventSource;
|
||||
|
||||
} catch (error) {
|
||||
console.error('创建EventSource失败:', error);
|
||||
if (onError) onError({ error: error.message });
|
||||
return null;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 流式聊天(Fetch API)
|
||||
|
||||
```javascript
|
||||
// Fetch API 流式聊天
|
||||
async function chatWithAIFetchStream(message, conversationId = '', onMessage, onComplete, onError) {
|
||||
try {
|
||||
// 构建请求体
|
||||
const formData = new FormData();
|
||||
formData.append('uniacid', '1');
|
||||
formData.append('user_id', '123456');
|
||||
formData.append('query', message);
|
||||
formData.append('conversation_id', conversationId || '');
|
||||
formData.append('stream', 'true');
|
||||
|
||||
const response = await fetch('/api/kefu/chat', {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
headers: {
|
||||
'Accept': 'text/event-stream'
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
if (!response.body) {
|
||||
throw new Error('响应体不可用');
|
||||
}
|
||||
|
||||
const reader = response.body.getReader();
|
||||
const decoder = new TextDecoder('utf-8');
|
||||
let buffer = '';
|
||||
let aiMessage = '';
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
|
||||
// 解码新接收的数据
|
||||
buffer += decoder.decode(value, { stream: true });
|
||||
|
||||
// 处理缓冲的数据,按行分割
|
||||
let lineEnd;
|
||||
while ((lineEnd = buffer.indexOf('\n')) !== -1) {
|
||||
const line = buffer.substring(0, lineEnd);
|
||||
buffer = buffer.substring(lineEnd + 1);
|
||||
|
||||
if (line.startsWith('data: ')) {
|
||||
try {
|
||||
const data = JSON.parse(line.substring(6));
|
||||
|
||||
if (data.event === 'message') {
|
||||
aiMessage += data.answer || '';
|
||||
if (onMessage) onMessage(data.answer || '');
|
||||
} else if (data.event === 'message_end') {
|
||||
if (onComplete) onComplete({
|
||||
conversation_id: data.conversation_id,
|
||||
message: aiMessage
|
||||
});
|
||||
} else if (data.event === 'done' && onComplete) {
|
||||
onComplete(data);
|
||||
} else if (data.event === 'error' && onError) {
|
||||
onError(data);
|
||||
}
|
||||
|
||||
if (data.conversation_id) {
|
||||
conversationId = data.conversation_id;
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('解析流式数据失败:', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 处理剩余的缓冲数据
|
||||
if (buffer.startsWith('data: ')) {
|
||||
try {
|
||||
const data = JSON.parse(buffer.substring(6));
|
||||
if (data.event === 'message' && onMessage) {
|
||||
onMessage(data.answer || '');
|
||||
} else if (data.event === 'done' && onComplete) {
|
||||
onComplete(data);
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('解析剩余数据失败:', e);
|
||||
}
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Fetch流式聊天请求失败:', error);
|
||||
if (onError) onError({ error: error.message });
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Uniapp调用示例
|
||||
|
||||
```javascript
|
||||
// Uniapp 非流式调用
|
||||
async function chatWithAI(message, conversationId = '') {
|
||||
try {
|
||||
const res = await uni.request({
|
||||
url: '/api/kefu/chat',
|
||||
method: 'POST',
|
||||
data: {
|
||||
query: message,
|
||||
uniacid: 1,
|
||||
conversation_id: conversationId,
|
||||
response_mode: 'blocking'
|
||||
}
|
||||
});
|
||||
|
||||
if (res[1].data.code === 0) {
|
||||
return res[1].data.data;
|
||||
} else {
|
||||
console.error('聊天失败:', res[1].data.message);
|
||||
return null;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('聊天请求失败:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Uniapp 获取历史记录
|
||||
async function getChatHistory(conversationId, limit = 20, offset = 0) {
|
||||
try {
|
||||
const res = await uni.request({
|
||||
url: '/api/kefu/getHistory',
|
||||
method: 'POST',
|
||||
data: {
|
||||
uniacid: 1,
|
||||
conversation_id: conversationId,
|
||||
limit: limit,
|
||||
offset: offset
|
||||
}
|
||||
});
|
||||
|
||||
if (res[1].data.code === 0) {
|
||||
return res[1].data.data;
|
||||
} else {
|
||||
console.error('获取历史记录失败:', res[1].data.message);
|
||||
return null;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取历史记录请求失败:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Uniapp 健康检查
|
||||
async function checkHealth(checkType = 'full') {
|
||||
try {
|
||||
const res = await uni.request({
|
||||
url: '/api/kefu/health',
|
||||
method: 'POST',
|
||||
data: {
|
||||
uniacid: 1,
|
||||
check_type: checkType
|
||||
}
|
||||
});
|
||||
|
||||
if (res[1].data.code === 0) {
|
||||
return res[1].data.data;
|
||||
} else {
|
||||
console.error('健康检查失败:', res[1].data.message);
|
||||
return null;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('健康检查请求失败:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 五、使用流程
|
||||
|
||||
1. **初始化检查**:小程序端启动时,调用`health`和`info`接口检查服务状态
|
||||
2. **获取会话**:进入客服页面时,系统会自动创建或使用已有会话
|
||||
3. **发送消息**:用户输入消息后,调用`chat`接口发送消息,获取机器人回复
|
||||
4. **显示消息**:将用户消息和机器人回复显示在聊天界面
|
||||
5. **加载历史记录**:需要时调用`getHistory`接口加载历史消息
|
||||
6. **维护会话**:保持会话ID,用于后续消息交流
|
||||
7. **清理数据**:根据用户需求调用`clearConversation`接口清理历史数据
|
||||
|
||||
## 六、数据存储机制
|
||||
|
||||
### 1. 存储状态
|
||||
|
||||
| 状态值 | 含义 | 说明 |
|
||||
|--------|------|------|
|
||||
| `streaming` | 流式中 | 正在进行流式输出的临时数据 |
|
||||
| `completed` | 已完成 | 正常完成的对话数据 |
|
||||
| `failed` | 失败 | 流式过程中发生失败的数据 |
|
||||
|
||||
### 2. 事务保护
|
||||
|
||||
- **流式对话**:使用临时会话ID机制,失败时自动回滚
|
||||
- **非流式对话**:完整的事务保护,确保数据一致性
|
||||
- **重复检查**:避免重复存储相同消息
|
||||
|
||||
### 3. 数据一致性
|
||||
|
||||
- 用户消息和助手消息通过`conversation_id`关联
|
||||
- 会话状态实时更新,便于管理和监控
|
||||
- 详细的日志记录,便于问题排查
|
||||
|
||||
## 七、注意事项
|
||||
|
||||
1. **必填参数**:所有接口都需要`uniacid`(站点ID)参数
|
||||
2. **参数更新**:新版本使用`query`替代`message`,使用`uniacid`替代`site_id`
|
||||
3. **事件驱动**:后端采用事件驱动架构,所有业务逻辑通过事件处理器执行
|
||||
4. **安全性**:请确保Dify API密钥的安全性,不要泄露给前端
|
||||
5. **用户标识**:建议对用户ID进行加密处理,避免直接使用敏感信息
|
||||
6. **流式体验**:推荐使用`stream: true`参数获得更好的用户体验
|
||||
7. **会话管理**:建议实现会话管理机制,定期清理过期会话
|
||||
8. **频率限制**:建议添加请求频率限制,防止恶意请求
|
||||
9. **生产环境**:在生产环境中,建议关闭DEBUG模式
|
||||
10. **数据完整性**:系统已内置事务保护和重复检查机制
|
||||
|
||||
## 八、测试建议
|
||||
|
||||
1. **基础检查**:首先调用`health`接口检查系统状态
|
||||
2. **配置验证**:调用`info`接口验证配置信息
|
||||
3. **接口测试**:使用Postman或类似工具测试各个API接口
|
||||
4. **流式测试**:测试`chat`接口的流式响应功能
|
||||
5. **完整流程**:在小程序端集成并测试完整流程
|
||||
6. **边界测试**:模拟不同场景下的用户输入,测试机器人回复效果
|
||||
7. **压力测试**:测试接口在高并发情况下的表现
|
||||
8. **数据验证**:检查非流式对话的存储完整性和一致性
|
||||
|
||||
## 九、常见问题
|
||||
|
||||
### 1. 接口返回400错误
|
||||
|
||||
**原因**:缺少必填参数`uniacid`或参数格式错误
|
||||
**解决方法**:确保请求中包含有效的站点ID,参数名已更新为`uniacid`
|
||||
|
||||
### 2. 健康检查返回503错误
|
||||
|
||||
**原因**:AI服务配置不完整或服务异常
|
||||
**解决方法**:检查插件配置和Dify API服务状态
|
||||
|
||||
### 3. 接口返回401错误
|
||||
|
||||
**原因**:Dify API密钥无效或过期
|
||||
**解决方法**:重新获取有效的API密钥并更新插件配置
|
||||
|
||||
### 4. 接口返回500错误
|
||||
|
||||
**原因**:后端服务器错误或Dify API服务异常
|
||||
**解决方法**:查看服务器日志,检查Dify API服务状态
|
||||
|
||||
### 5. 机器人回复为空
|
||||
|
||||
**原因**:Dify聊天机器人配置问题或请求参数错误
|
||||
**解决方法**:检查Dify机器人配置,验证请求参数是否正确
|
||||
|
||||
### 6. 流式响应无法解析
|
||||
|
||||
**原因**:客户端不支持SSE或解析方式错误
|
||||
**解决方法**:使用正确的方式解析Server-Sent Events格式,参考前端示例代码
|
||||
|
||||
### 7. 会话ID无效
|
||||
|
||||
**原因**:会话已过期或不存在
|
||||
**解决方法**:创建新会话,获取新的会话ID
|
||||
|
||||
### 8. 参数不匹配
|
||||
|
||||
**原因**:使用了旧版本的参数名称
|
||||
**解决方法**:更新参数:`message` → `query`,`site_id` → `uniacid`
|
||||
|
||||
## 十、性能优化建议
|
||||
|
||||
1. **缓存配置**:可对`info`接口返回的配置信息进行客户端缓存
|
||||
2. **连接复用**:HTTP请求使用连接池,减少建立连接的开销
|
||||
3. **压缩传输**:启用gzip压缩减少传输数据量
|
||||
4. **分页加载**:历史记录使用分页加载,避免一次性加载大量数据
|
||||
5. **CDN加速**:静态资源使用CDN加速访问
|
||||
6. **监控告警**:建立接口性能监控和告警机制
|
||||
7. **数据清理**:定期清理过期和失败状态的垃圾数据
|
||||
8. **索引优化**:为常用查询字段添加数据库索引
|
||||
|
||||
---
|
||||
|
||||
**文档更新时间**:2025-12-10
|
||||
**版本**:v2.1
|
||||
**兼容性**:向后兼容,推荐使用标准的`uniacid`参数
|
||||
375
docs/db/compare_sql_tables.py
Normal file
375
docs/db/compare_sql_tables.py
Normal file
@@ -0,0 +1,375 @@
|
||||
import re
|
||||
import os
|
||||
|
||||
# 解析SQL文件,提取表结构
|
||||
def parse_sql_file(file_path, ignore_prefix=None):
|
||||
tables = {}
|
||||
|
||||
# 读取文件内容
|
||||
with open(file_path, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
|
||||
# 匹配CREATE TABLE语句
|
||||
table_pattern = re.compile(r'CREATE TABLE\s+`?([^`\s]+)`?\s*\(([^;]+)\)\s*(?:[^;]+);', re.DOTALL | re.IGNORECASE)
|
||||
|
||||
matches = table_pattern.findall(content)
|
||||
|
||||
for full_table_name, table_def in matches:
|
||||
# 处理表名,忽略前缀
|
||||
table_name = full_table_name
|
||||
if ignore_prefix and table_name.startswith(ignore_prefix):
|
||||
table_name = table_name[len(ignore_prefix):]
|
||||
|
||||
# 提取列定义
|
||||
columns = []
|
||||
# 匹配列定义,包括列名、类型、约束等
|
||||
column_pattern = re.compile(r'\s*`?([^`\s,]+)`?\s+([^\s,]+)\s*([^,]+)(?:,|$)', re.DOTALL)
|
||||
column_matches = column_pattern.findall(table_def)
|
||||
|
||||
for col_name, col_type, col_constraints in column_matches:
|
||||
# 清理约束中的换行符和多余空格
|
||||
col_constraints = re.sub(r'\s+', ' ', col_constraints.strip())
|
||||
columns.append((col_name, col_type, col_constraints))
|
||||
|
||||
# 提取主键
|
||||
primary_key = None
|
||||
pk_pattern = re.compile(r'PRIMARY\s+KEY\s*\(`?([^`\s,]+)`?\)', re.IGNORECASE)
|
||||
pk_match = pk_pattern.search(table_def)
|
||||
if pk_match:
|
||||
primary_key = pk_match.group(1)
|
||||
|
||||
# 提取索引
|
||||
indexes = []
|
||||
index_pattern = re.compile(r'INDEX\s+`?([^`\s]+)`?\s*\(([^)]+)\)', re.IGNORECASE)
|
||||
index_matches = index_pattern.findall(table_def)
|
||||
for index_name, index_cols in index_matches:
|
||||
indexes.append((index_name, index_cols.strip()))
|
||||
|
||||
# 提取唯一约束
|
||||
unique_constraints = []
|
||||
unique_pattern = re.compile(r'UNIQUE\s+KEY\s*`?([^`\s]+)`?\s*\(([^)]+)\)', re.IGNORECASE)
|
||||
unique_matches = unique_pattern.findall(table_def)
|
||||
for unique_name, unique_cols in unique_matches:
|
||||
unique_constraints.append((unique_name, unique_cols.strip()))
|
||||
|
||||
tables[table_name] = {
|
||||
'full_name': full_table_name,
|
||||
'columns': columns,
|
||||
'primary_key': primary_key,
|
||||
'indexes': indexes,
|
||||
'unique_constraints': unique_constraints
|
||||
}
|
||||
|
||||
return tables
|
||||
|
||||
# 比较两个数据库表结构
|
||||
def compare_databases(db1, db2, db1_name, db2_name):
|
||||
diffs = {
|
||||
'only_in_db1': [],
|
||||
'only_in_db2': [],
|
||||
'column_diffs': {},
|
||||
'primary_key_diffs': {},
|
||||
'index_diffs': {},
|
||||
'unique_constraint_diffs': {}
|
||||
}
|
||||
|
||||
# 找出只在db1中存在的表
|
||||
for table_name in db1:
|
||||
if table_name not in db2:
|
||||
diffs['only_in_db1'].append(table_name)
|
||||
|
||||
# 找出只在db2中存在的表
|
||||
for table_name in db2:
|
||||
if table_name not in db1:
|
||||
diffs['only_in_db2'].append(table_name)
|
||||
|
||||
# 比较共同存在的表
|
||||
common_tables = set(db1.keys()) & set(db2.keys())
|
||||
for table_name in common_tables:
|
||||
table1 = db1[table_name]
|
||||
table2 = db2[table_name]
|
||||
|
||||
# 比较列
|
||||
col_diffs = {
|
||||
'only_in_db1': [],
|
||||
'only_in_db2': [],
|
||||
'type_diffs': [],
|
||||
'constraint_diffs': []
|
||||
}
|
||||
|
||||
# 列名集合
|
||||
cols1 = {col[0]: col for col in table1['columns']}
|
||||
cols2 = {col[0]: col for col in table2['columns']}
|
||||
|
||||
# 只在db1中的列
|
||||
for col_name in cols1:
|
||||
if col_name not in cols2:
|
||||
col_diffs['only_in_db1'].append(col_name)
|
||||
|
||||
# 只在db2中的列
|
||||
for col_name in cols2:
|
||||
if col_name not in cols1:
|
||||
col_diffs['only_in_db2'].append(col_name)
|
||||
|
||||
# 比较列类型和约束
|
||||
common_cols = set(cols1.keys()) & set(cols2.keys())
|
||||
for col_name in common_cols:
|
||||
col1 = cols1[col_name]
|
||||
col2 = cols2[col_name]
|
||||
|
||||
# 类型差异
|
||||
if col1[1] != col2[1]:
|
||||
col_diffs['type_diffs'].append((col_name, col1[1], col2[1]))
|
||||
|
||||
# 约束差异
|
||||
if col1[2] != col2[2]:
|
||||
col_diffs['constraint_diffs'].append((col_name, col1[2], col2[2]))
|
||||
|
||||
if any(col_diffs.values()):
|
||||
diffs['column_diffs'][table_name] = col_diffs
|
||||
|
||||
# 比较主键
|
||||
if table1['primary_key'] != table2['primary_key']:
|
||||
diffs['primary_key_diffs'][table_name] = (table1['primary_key'], table2['primary_key'])
|
||||
|
||||
# 比较索引
|
||||
index_diffs = {
|
||||
'only_in_db1': [],
|
||||
'only_in_db2': [],
|
||||
'definition_diffs': []
|
||||
}
|
||||
|
||||
indexes1 = {idx[0]: idx[1] for idx in table1['indexes']}
|
||||
indexes2 = {idx[0]: idx[1] for idx in table2['indexes']}
|
||||
|
||||
# 只在db1中的索引
|
||||
for idx_name in indexes1:
|
||||
if idx_name not in indexes2:
|
||||
index_diffs['only_in_db1'].append((idx_name, indexes1[idx_name]))
|
||||
|
||||
# 只在db2中的索引
|
||||
for idx_name in indexes2:
|
||||
if idx_name not in indexes1:
|
||||
index_diffs['only_in_db2'].append((idx_name, indexes2[idx_name]))
|
||||
|
||||
# 比较索引定义
|
||||
common_indexes = set(indexes1.keys()) & set(indexes2.keys())
|
||||
for idx_name in common_indexes:
|
||||
if indexes1[idx_name] != indexes2[idx_name]:
|
||||
index_diffs['definition_diffs'].append((idx_name, indexes1[idx_name], indexes2[idx_name]))
|
||||
|
||||
if any(index_diffs.values()):
|
||||
diffs['index_diffs'][table_name] = index_diffs
|
||||
|
||||
# 比较唯一约束
|
||||
unique_diffs = {
|
||||
'only_in_db1': [],
|
||||
'only_in_db2': [],
|
||||
'definition_diffs': []
|
||||
}
|
||||
|
||||
unique1 = {uc[0]: uc[1] for uc in table1['unique_constraints']}
|
||||
unique2 = {uc[0]: uc[1] for uc in table2['unique_constraints']}
|
||||
|
||||
# 只在db1中的唯一约束
|
||||
for uc_name in unique1:
|
||||
if uc_name not in unique2:
|
||||
unique_diffs['only_in_db1'].append((uc_name, unique1[uc_name]))
|
||||
|
||||
# 只在db2中的唯一约束
|
||||
for uc_name in unique2:
|
||||
if uc_name not in unique1:
|
||||
unique_diffs['only_in_db2'].append((uc_name, unique2[uc_name]))
|
||||
|
||||
# 比较唯一约束定义
|
||||
common_unique = set(unique1.keys()) & set(unique2.keys())
|
||||
for uc_name in common_unique:
|
||||
if unique1[uc_name] != unique2[uc_name]:
|
||||
unique_diffs['definition_diffs'].append((uc_name, unique1[uc_name], unique2[uc_name]))
|
||||
|
||||
if any(unique_diffs.values()):
|
||||
diffs['unique_constraint_diffs'][table_name] = unique_diffs
|
||||
|
||||
return diffs
|
||||
|
||||
# 打印差异报告
|
||||
# 生成Markdown格式的差异报告
|
||||
def generate_markdown_report(diffs, db1_name, db2_name, db1_table_count, db2_table_count):
|
||||
report = []
|
||||
|
||||
# 报告标题
|
||||
report.append(f"# 数据库差异报告: {db1_name} vs {db2_name}")
|
||||
report.append("\n## 1. 表数量统计")
|
||||
report.append("| 数据库文件 | 表数量 |")
|
||||
report.append("|------------|--------|")
|
||||
report.append(f"| {db1_name} | {db1_table_count} |")
|
||||
report.append(f"| {db2_name} | {db2_table_count} |")
|
||||
|
||||
# 表存在性差异
|
||||
report.append("\n## 2. 表存在性差异")
|
||||
|
||||
# 仅在db1中的表
|
||||
if diffs['only_in_db1']:
|
||||
report.append(f"\n### 2.1 仅在 {db1_name} 中存在的表 ({len(diffs['only_in_db1'])} 个)")
|
||||
report.append("| 表名 |")
|
||||
report.append("|------|")
|
||||
for table in sorted(diffs['only_in_db1']):
|
||||
report.append(f"| {table} |")
|
||||
|
||||
# 仅在db2中的表
|
||||
if diffs['only_in_db2']:
|
||||
report.append(f"\n### 2.2 仅在 {db2_name} 中存在的表 ({len(diffs['only_in_db2'])} 个)")
|
||||
report.append("| 表名 |")
|
||||
report.append("|------|")
|
||||
for table in sorted(diffs['only_in_db2']):
|
||||
report.append(f"| {table} |")
|
||||
|
||||
# 列结构差异
|
||||
if diffs['column_diffs']:
|
||||
report.append(f"\n## 3. 列结构差异的表 ({len(diffs['column_diffs'])} 个)")
|
||||
|
||||
for table, col_diffs in diffs['column_diffs'].items():
|
||||
report.append(f"\n### 3.1 表: {table}")
|
||||
|
||||
# 仅在db1中的列
|
||||
if col_diffs['only_in_db1']:
|
||||
report.append(f"\n#### 3.1.1 仅在 {db1_name} 中存在的列")
|
||||
report.append("| 列名 |")
|
||||
report.append("|------|")
|
||||
for col in col_diffs['only_in_db1']:
|
||||
report.append(f"| {col} |")
|
||||
|
||||
# 仅在db2中的列
|
||||
if col_diffs['only_in_db2']:
|
||||
report.append(f"\n#### 3.1.2 仅在 {db2_name} 中存在的列")
|
||||
report.append("| 列名 |")
|
||||
report.append("|------|")
|
||||
for col in col_diffs['only_in_db2']:
|
||||
report.append(f"| {col} |")
|
||||
|
||||
# 列类型差异
|
||||
if col_diffs['type_diffs']:
|
||||
report.append(f"\n#### 3.1.3 列类型差异")
|
||||
report.append(f"| 列名 | {db1_name} | {db2_name} |")
|
||||
report.append("|------|------------|------------|")
|
||||
for col_name, type1, type2 in col_diffs['type_diffs']:
|
||||
report.append(f"| {col_name} | {type1} | {type2} |")
|
||||
|
||||
# 列约束差异
|
||||
if col_diffs['constraint_diffs']:
|
||||
report.append(f"\n#### 3.1.4 列约束差异")
|
||||
report.append(f"| 列名 | {db1_name} | {db2_name} |")
|
||||
report.append("|------|------------|------------|")
|
||||
for col_name, constraint1, constraint2 in col_diffs['constraint_diffs']:
|
||||
report.append(f"| {col_name} | {constraint1} | {constraint2} |")
|
||||
|
||||
# 主键差异
|
||||
if diffs['primary_key_diffs']:
|
||||
report.append(f"\n## 4. 主键差异的表 ({len(diffs['primary_key_diffs'])} 个)")
|
||||
report.append(f"| 表名 | {db1_name} | {db2_name} |")
|
||||
report.append("|------|------------|------------|")
|
||||
for table, (pk1, pk2) in diffs['primary_key_diffs'].items():
|
||||
report.append(f"| {table} | {pk1} | {pk2} |")
|
||||
|
||||
# 索引差异
|
||||
if diffs['index_diffs']:
|
||||
report.append(f"\n## 5. 索引差异的表 ({len(diffs['index_diffs'])} 个)")
|
||||
|
||||
for table, idx_diffs in diffs['index_diffs'].items():
|
||||
report.append(f"\n### 5.1 表: {table}")
|
||||
|
||||
# 仅在db1中的索引
|
||||
if idx_diffs['only_in_db1']:
|
||||
report.append(f"\n#### 5.1.1 仅在 {db1_name} 中存在的索引")
|
||||
report.append("| 索引名 | 索引列 |")
|
||||
report.append("|--------|--------|")
|
||||
for idx_name, idx_cols in idx_diffs['only_in_db1']:
|
||||
report.append(f"| {idx_name} | {idx_cols} |")
|
||||
|
||||
# 仅在db2中的索引
|
||||
if idx_diffs['only_in_db2']:
|
||||
report.append(f"\n#### 5.1.2 仅在 {db2_name} 中存在的索引")
|
||||
report.append("| 索引名 | 索引列 |")
|
||||
report.append("|--------|--------|")
|
||||
for idx_name, idx_cols in idx_diffs['only_in_db2']:
|
||||
report.append(f"| {idx_name} | {idx_cols} |")
|
||||
|
||||
# 索引定义差异
|
||||
if idx_diffs['definition_diffs']:
|
||||
report.append(f"\n#### 5.1.3 索引定义差异")
|
||||
report.append(f"| 索引名 | {db1_name} | {db2_name} |")
|
||||
report.append("|--------|------------|------------|")
|
||||
for idx_name, idx1, idx2 in idx_diffs['definition_diffs']:
|
||||
report.append(f"| {idx_name} | {idx1} | {idx2} |")
|
||||
|
||||
# 唯一约束差异
|
||||
if diffs['unique_constraint_diffs']:
|
||||
report.append(f"\n## 6. 唯一约束差异的表 ({len(diffs['unique_constraint_diffs'])} 个)")
|
||||
|
||||
for table, uc_diffs in diffs['unique_constraint_diffs'].items():
|
||||
report.append(f"\n### 6.1 表: {table}")
|
||||
|
||||
# 仅在db1中的唯一约束
|
||||
if uc_diffs['only_in_db1']:
|
||||
report.append(f"\n#### 6.1.1 仅在 {db1_name} 中存在的唯一约束")
|
||||
report.append("| 约束名 | 约束列 |")
|
||||
report.append("|--------|--------|")
|
||||
for uc_name, uc_cols in uc_diffs['only_in_db1']:
|
||||
report.append(f"| {uc_name} | {uc_cols} |")
|
||||
|
||||
# 仅在db2中的唯一约束
|
||||
if uc_diffs['only_in_db2']:
|
||||
report.append(f"\n#### 6.1.2 仅在 {db2_name} 中存在的唯一约束")
|
||||
report.append("| 约束名 | 约束列 |")
|
||||
report.append("|--------|--------|")
|
||||
for uc_name, uc_cols in uc_diffs['only_in_db2']:
|
||||
report.append(f"| {uc_name} | {uc_cols} |")
|
||||
|
||||
# 唯一约束定义差异
|
||||
if uc_diffs['definition_diffs']:
|
||||
report.append(f"\n#### 6.1.3 唯一约束定义差异")
|
||||
report.append(f"| 约束名 | {db1_name} | {db2_name} |")
|
||||
report.append("|--------|------------|------------|")
|
||||
for uc_name, uc1, uc2 in uc_diffs['definition_diffs']:
|
||||
report.append(f"| {uc_name} | {uc1} | {uc2} |")
|
||||
|
||||
report.append("\n## 7. 总结")
|
||||
report.append("差异比较完成!")
|
||||
|
||||
return '\n'.join(report)
|
||||
|
||||
# 主函数
|
||||
def main():
|
||||
# 文件路径
|
||||
db1_path = r'D:\projects\shop-projects\backend\docs\db\niushop_database.sql'
|
||||
db2_path = r'D:\projects\shop-projects\backend\docs\db\init_v2.0_with_data.sql'
|
||||
report_path = r'D:\projects\shop-projects\backend\docs\db\database_diff_report.md'
|
||||
|
||||
# 解析数据库结构
|
||||
print(f"正在解析 {db1_path}...")
|
||||
db1 = parse_sql_file(db1_path)
|
||||
db1_table_count = len(db1)
|
||||
print(f"解析完成,共 {db1_table_count} 个表")
|
||||
|
||||
print(f"\n正在解析 {db2_path}...")
|
||||
db2 = parse_sql_file(db2_path, ignore_prefix='lucky_')
|
||||
db2_table_count = len(db2)
|
||||
print(f"解析完成,共 {db2_table_count} 个表")
|
||||
|
||||
# 比较差异
|
||||
print("\n正在比较数据库差异...")
|
||||
diffs = compare_databases(db1, db2, 'niushop_database.sql', 'init_v2.0_with_data.sql')
|
||||
|
||||
# 生成Markdown差异报告
|
||||
print("\n正在生成Markdown差异报告...")
|
||||
report = generate_markdown_report(diffs, 'niushop_database.sql', 'init_v2.0_with_data.sql', db1_table_count, db2_table_count)
|
||||
|
||||
# 保存报告到文件
|
||||
with open(report_path, 'w', encoding='utf-8') as f:
|
||||
f.write(report)
|
||||
|
||||
print(f"\n差异报告已生成: {report_path}")
|
||||
print("差异比较完成!")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
5278
docs/db/database_diff_report.md
Normal file
5278
docs/db/database_diff_report.md
Normal file
File diff suppressed because it is too large
Load Diff
11578
docs/db/niushop_database.sql
Normal file
11578
docs/db/niushop_database.sql
Normal file
File diff suppressed because it is too large
Load Diff
117
docs/db/update_sql_comments.py
Normal file
117
docs/db/update_sql_comments.py
Normal file
@@ -0,0 +1,117 @@
|
||||
import re
|
||||
import os
|
||||
|
||||
# 读取 SQL 文件内容
|
||||
def read_sql_file(file_path):
|
||||
with open(file_path, 'r', encoding='utf-8') as f:
|
||||
return f.read()
|
||||
|
||||
# 解析 database.sql,提取表结构和注释
|
||||
def parse_database_sql(sql_content):
|
||||
tables = {}
|
||||
|
||||
# 匹配 CREATE TABLE 语句,更灵活的格式
|
||||
# 匹配所有 CREATE TABLE 语句,不依赖于 ENGINE、CHARACTER SET 等子句的顺序
|
||||
table_pattern = re.compile(r"CREATE TABLE\s+`?([^`\s]+)`?\s*\(([^;]+)\)\s*(?:[^;]+COMMENT\s*=\s*'([^']+)'[^;]*)?\s*;", re.DOTALL | re.IGNORECASE)
|
||||
|
||||
matches = table_pattern.findall(sql_content)
|
||||
|
||||
for table_name, table_def, table_comment in matches:
|
||||
if not table_comment:
|
||||
# 如果没有匹配到表注释,尝试从其他位置获取
|
||||
comment_match = re.search(r"COMMENT\s*=\s*'([^']+)'", table_def, re.IGNORECASE)
|
||||
if comment_match:
|
||||
table_comment = comment_match.group(1)
|
||||
else:
|
||||
table_comment = ''
|
||||
|
||||
# 解析列定义和注释
|
||||
columns = {}
|
||||
# 匹配列定义,包括 COMMENT
|
||||
column_lines = table_def.split('\n')
|
||||
for line in column_lines:
|
||||
# 匹配列名、类型和注释
|
||||
column_match = re.search(r"\s*([^\s,]+)\s+([^\s,]+)\s*(?:[^,]+COMMENT\s*=\s*'([^']+)'[^,]*|[^,]*)", line)
|
||||
if column_match:
|
||||
column_name = column_match.group(1)
|
||||
column_comment = column_match.group(3) or ''
|
||||
if column_comment:
|
||||
columns[column_name] = column_comment
|
||||
|
||||
tables[table_name] = {
|
||||
'comment': table_comment,
|
||||
'columns': columns
|
||||
}
|
||||
|
||||
return tables
|
||||
|
||||
# 更新 init_v2.0.sql 文件中的注释
|
||||
def update_init_sql(init_sql_path, database_tables):
|
||||
# 读取 init_v2.0.sql 内容
|
||||
init_content = read_sql_file(init_sql_path)
|
||||
|
||||
# 匹配 CREATE TABLE 语句,适应 init_v2.0.sql 的格式
|
||||
table_pattern = re.compile(r"(create table if not exists lucky_([^\s]+)\s*\(([^;]+)\)\s*comment\s*=\s*'[^']*'\s*(.*?);)", re.DOTALL | re.IGNORECASE)
|
||||
|
||||
def replace_table(match):
|
||||
full_match = match.group(0)
|
||||
table_name = match.group(2)
|
||||
table_def = match.group(3)
|
||||
table_suffix = match.group(4)
|
||||
|
||||
if table_name in database_tables:
|
||||
# 获取数据库表的注释和列注释
|
||||
db_table = database_tables[table_name]
|
||||
table_comment = db_table['comment']
|
||||
columns = db_table['columns']
|
||||
|
||||
# 更新列注释
|
||||
new_table_def = table_def
|
||||
for column_name, column_comment in columns.items():
|
||||
# 匹配列定义,替换注释
|
||||
# 格式:列名 类型 default 默认值 not null comment '注释'
|
||||
column_pattern = re.compile(r"(\s*" + column_name + r"\s+[^\s,]+\s*(?:default\s+[^\s,]+\s*)?(?:not null\s*)?comment\s*=\s*')([^']*)'([^,]*)", re.IGNORECASE)
|
||||
new_table_def = column_pattern.sub(r"\1" + column_comment + r"'\3", new_table_def)
|
||||
|
||||
# 重新构建 CREATE TABLE 语句
|
||||
new_full_match = f"create table if not exists lucky_{table_name} ({new_table_def}) comment = '{table_comment}' {table_suffix};"
|
||||
return new_full_match
|
||||
|
||||
return full_match
|
||||
|
||||
# 替换所有表
|
||||
updated_content = table_pattern.sub(replace_table, init_content)
|
||||
|
||||
# 写回文件
|
||||
with open(init_sql_path, 'w', encoding='utf-8') as f:
|
||||
f.write(updated_content)
|
||||
|
||||
print(f"Updated {init_sql_path}")
|
||||
|
||||
# 主函数
|
||||
def main():
|
||||
# 文件路径
|
||||
database_sql_path = r'./niushop_database.sql'
|
||||
init_v20_sql_path = r'./init_v2.0.sql'
|
||||
init_v20_with_data_sql_path = r'./init_v2.0_with_data.sql'
|
||||
|
||||
# 解析 database.sql
|
||||
print("Parsing database.sql...")
|
||||
database_content = read_sql_file(database_sql_path)
|
||||
database_tables = parse_database_sql(database_content)
|
||||
print(f"Found {len(database_tables)} tables in database.sql")
|
||||
|
||||
# 更新 init_v2.0.sql
|
||||
if os.path.exists(init_v20_sql_path):
|
||||
print("Updating init_v2.0.sql...")
|
||||
update_init_sql(init_v20_sql_path, database_tables)
|
||||
|
||||
# 更新 init_v2.0_with_data.sql
|
||||
if os.path.exists(init_v20_with_data_sql_path):
|
||||
print("Updating init_v2.0_with_data.sql...")
|
||||
update_init_sql(init_v20_with_data_sql_path, database_tables)
|
||||
|
||||
print("All files updated successfully!")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -24,4 +24,44 @@ create table if not exists lucky_diy_view_util
|
||||
constraint name
|
||||
unique (name)
|
||||
)
|
||||
```
|
||||
```
|
||||
|
||||
## 页面设计及组件展示
|
||||
|
||||
- src\app\model\web\DiyView.php
|
||||
- src\app\shop\view\diy\edit.html
|
||||
- src\public\static\ext\diyview\js\components.js
|
||||
|
||||
## 如何添加新组件
|
||||
|
||||
|
||||
### 1. 添加组件到数据表中
|
||||
|
||||
```sql
|
||||
insert into lucky_diy_view_util (name, title, type, value, addon_name, sort, support_diy_view, max_count, is_delete, icon, icon_type)
|
||||
values ('test', '测试', 'SYSTEM', '{"test": "test"}', '', 0, '', 0, 0, '', 0);
|
||||
|
||||
--- 微信视频号组件
|
||||
-- 仅当WechatChannel不存在时添加记录
|
||||
INSERT INTO lucky_diy_view_util (`name`, `title`, `type`, `value`, `addon_name`, `sort`, `support_diy_view`, `max_count`, `is_delete`, `icon`, `icon_type`)
|
||||
SELECT 'WechatChannel', '微信视频号', 'SYSTEM', '{ "list": [{ "channelName":"", "finderUserName": "", "avatarImageType": "url", "avatarUrl": "", "videoTitle": "", "coverImageType": "url", "coverUrl": "", "feedId": "", "feedToken": "", "viewCount": 0, "showViewCount": true, "embedMode": false, "channelType":"wechat" }], "rowCount": 2, "showStyle": "fixed", "aspectRatio":"16:9", "titleLineClamp": 1, "showPlayBtn": true}', '', 100110, '', 0, 0, '/public/static/img/svg/xuanxiangka.svg', 0
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM lucky_diy_view_util WHERE name = 'WechatChannel'
|
||||
);
|
||||
|
||||
```
|
||||
|
||||
|
||||
### 2. 建立组件的控制器
|
||||
在 `src\app\component\controller` 目录下创建对应的控制器文件,处理组件的业务逻辑。
|
||||
|
||||
例如:创建 `src\app\component\controller\TestController.php` 文件,用于处理测试组件的业务逻辑。
|
||||
|
||||
### 3. 建立组件的视图
|
||||
在 src\app\component\view 目录下创建对应的视图文件,处理组件的前端展示。
|
||||
|
||||
例如:创建 `src\app\component\view\test.php` 文件,用于展示测试组件。
|
||||
|
||||
|
||||
### 4. 在前端页面中使用组件
|
||||
在前端页面中使用组件,需要在页面中添加对应的组件标签。
|
||||
178
docs/nginx/dev.aigc-quickapp.com.conf
Normal file
178
docs/nginx/dev.aigc-quickapp.com.conf
Normal file
@@ -0,0 +1,178 @@
|
||||
proxy_cache_path /www/dk_project/sites/dev.aigc-quickapp.com/proxy_cache_dir levels=1:2 keys_zone=dev_aigc-quickapp_com_cache:20m inactive=1d max_size=5g;
|
||||
|
||||
# 连接升级变量,避免 $connection_upgrade 未定义(用于 WebSocket)
|
||||
map $http_upgrade $connection_upgrade {
|
||||
default upgrade;
|
||||
'' close;
|
||||
}
|
||||
|
||||
# 轻量限速/并发控制(全局定义,按需调整阈值)
|
||||
limit_req_zone $binary_remote_addr zone=perip:10m rate=10r/s;
|
||||
limit_conn_zone $binary_remote_addr zone=perip_conn:10m;
|
||||
|
||||
# HTTP -> HTTPS 跳转独立 server,避免与业务混配
|
||||
server {
|
||||
listen 80;
|
||||
server_name dev.aigc-quickapp.com;
|
||||
|
||||
return 301 https://$host$request_uri;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 443 quic;
|
||||
listen 443 ssl;
|
||||
|
||||
http2 on;
|
||||
server_name dev.aigc-quickapp.com;
|
||||
index index.php index.html index.htm default.php default.htm default.html;
|
||||
root /www/dk_project/wwwroot/dev.aigc-quickapp.com;
|
||||
|
||||
|
||||
#CERT-APPLY-CHECK--START
|
||||
# 用于SSL证书申请时的文件验证相关配置 -- 请勿删除
|
||||
include /www/server/panel/vhost/nginx/well-known/dev.aigc-quickapp.com.conf;
|
||||
#CERT-APPLY-CHECK--END
|
||||
|
||||
#SSL-START SSL相关配置,请勿删除或修改下一行带注释的404规则
|
||||
#error_page 404/404.html;
|
||||
ssl_certificate /www/server/panel/vhost/cert/dev.aigc-quickapp.com/fullchain.pem;
|
||||
ssl_certificate_key /www/server/panel/vhost/cert/dev.aigc-quickapp.com/privkey.pem;
|
||||
ssl_protocols TLSv1.1 TLSv1.2 TLSv1.3; # 暂保留 TLSv1.1 兼容老端,后续可下线
|
||||
ssl_ciphers EECDH+CHACHA20:EECDH+CHACHA20-draft:EECDH+AES128:RSA+AES128:EECDH+AES256:RSA+AES256:EECDH+3DES:RSA+3DES:!MD5;
|
||||
ssl_prefer_server_ciphers on;
|
||||
ssl_session_cache shared:SSL:10m;
|
||||
ssl_session_timeout 10m;
|
||||
ssl_session_tickets off;
|
||||
|
||||
add_header Strict-Transport-Security "max-age=31536000" always;
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
||||
add_header Permissions-Policy "geolocation=(), microphone=(), camera=(), payment=()" always;
|
||||
# CSP 先以 Report-Only 方式上线,避免误杀;按前端依赖逐步收紧
|
||||
add_header Content-Security-Policy-Report-Only "default-src 'self' data: blob:; img-src * data: blob:; script-src 'self' 'unsafe-inline' 'unsafe-eval' https: blob:; style-src 'self' 'unsafe-inline' https:; connect-src *; font-src 'self' data: https:; frame-ancestors 'self';" always;
|
||||
server_tokens off;
|
||||
error_page 497 https://$host$request_uri;
|
||||
#SSL-END
|
||||
#REDIRECT START
|
||||
|
||||
|
||||
#REDIRECT END
|
||||
#ERROR-PAGE-START 错误页配置,可以注释、删除或修改
|
||||
error_page 404 /404.html;
|
||||
#error_page 502 /502.html;
|
||||
#ERROR-PAGE-END
|
||||
|
||||
#WEBSOCKET-SUPPORT START
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection $connection_upgrade;
|
||||
#WEBSOCKET-SUPPORT END
|
||||
|
||||
# 上传/超时治理(根据业务需要调整大小与时限)
|
||||
client_max_body_size 100m;
|
||||
client_body_timeout 15s;
|
||||
client_header_timeout 10s;
|
||||
send_timeout 30s;
|
||||
|
||||
#PROXY-CONF-START
|
||||
location ^~ / {
|
||||
proxy_pass http://127.0.0.1:8050;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Real-Port $remote_port;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Forwarded-Host $host;
|
||||
proxy_set_header X-Forwarded-Port $server_port;
|
||||
proxy_set_header X-Forwarded-Server $host;
|
||||
proxy_set_header REMOTE-HOST $remote_addr;
|
||||
proxy_redirect off;
|
||||
|
||||
proxy_buffering off;
|
||||
proxy_connect_timeout 60s;
|
||||
proxy_send_timeout 600s;
|
||||
proxy_read_timeout 600s;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection $connection_upgrade;
|
||||
|
||||
# 轻量限速:降低扫描/撞库/爆破效率(按需调大/关闭)
|
||||
limit_req zone=perip burst=20 nodelay;
|
||||
limit_conn perip_conn 50;
|
||||
}
|
||||
#PROXY-CONF-END
|
||||
|
||||
#SERVER-BLOCK START
|
||||
location ~* ^/ws/(.*)$ {
|
||||
# 先尝试直接转发,不修改路径
|
||||
proxy_pass http://localhost:8050;
|
||||
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
|
||||
# 禁用缓冲,确保WebSocket数据实时传输
|
||||
proxy_buffering off;
|
||||
proxy_buffer_size 4k;
|
||||
proxy_buffers 4 4k;
|
||||
proxy_busy_buffers_size 4k;
|
||||
proxy_max_temp_file_size 0;
|
||||
|
||||
# 可选:设置超时(WebSocket 是长连接)
|
||||
proxy_read_timeout 86400s;
|
||||
proxy_send_timeout 86400s;
|
||||
|
||||
# WebSocket 连接数可选限速:如需限制每 IP 并发,解除注释
|
||||
# limit_conn perip_conn 20;
|
||||
}
|
||||
# 可设置server|location等所有server字段,如:
|
||||
# location /web {
|
||||
# try_files $uri $uri/ /index.php$is_args$args;
|
||||
# }
|
||||
# error_page 404 /diy_404.html;
|
||||
# 如果反代网站访问异常且这里已经配置了内容,请优先排查此处的配置是否正确
|
||||
|
||||
#SERVER-BLOCK END
|
||||
|
||||
#禁止访问的文件或目录
|
||||
location ~ ^/(\.user.ini|\.htaccess|\.git|\.env|\.svn|\.project|LICENSE|README.md) {
|
||||
return 404;
|
||||
}
|
||||
|
||||
# 追加敏感路径/文件快速阻断
|
||||
location ~* /(composer\.(json|lock)|package(-lock)?\.json|pnpm-lock\.yaml|yarn\.lock|phpunit\.xml|\.ssh/|id_rsa|id_dsa|\.DS_Store) {
|
||||
return 404;
|
||||
}
|
||||
|
||||
# 禁止直连 phpinfo/adminer 等常见探测
|
||||
location ~* /(phpinfo|adminer)\.php$ {
|
||||
return 404;
|
||||
}
|
||||
|
||||
# 禁止可疑备份/数据库/压缩文件下载(如需放行请删除本段)
|
||||
location ~* \.(bak|sql|tar|tar\.gz|rar|7z|zip)$ {
|
||||
return 404;
|
||||
}
|
||||
|
||||
#一键申请SSL证书验证目录相关设置
|
||||
location /.well-known {
|
||||
allow all;
|
||||
}
|
||||
|
||||
#禁止在证书验证目录放入敏感文件
|
||||
if ( $uri ~ "^/\.well-known/.*\.(php|jsp|py|js|css|lua|ts|go|zip|tar\.gz|rar|7z|sql|bak)$" ) {
|
||||
return 403;
|
||||
}
|
||||
|
||||
#LOG START
|
||||
|
||||
access_log /www/wwwlogs/dev.aigc-quickapp.com.log;
|
||||
error_log /www/wwwlogs/dev.aigc-quickapp.com.error.log;
|
||||
|
||||
#LOG END
|
||||
}
|
||||
355
docs/websocket/README.md
Normal file
355
docs/websocket/README.md
Normal file
@@ -0,0 +1,355 @@
|
||||
# WebSocket Server 说明文档
|
||||
|
||||
## 1. 概述
|
||||
|
||||
WebSocket Server 是基于 Ratchet 库实现的纯 PHP WebSocket 服务器,主要用于为智能客服系统提供实时通信支持,特别是为不支持 EventSource 的微信小程序提供流式请求处理能力。
|
||||
|
||||
## 2. 安装与依赖
|
||||
|
||||
### 2.1 依赖库
|
||||
|
||||
- **Ratchet**: 纯 PHP WebSocket 实现库
|
||||
```bash
|
||||
composer require cboden/ratchet
|
||||
```
|
||||
|
||||
### 2.2 环境要求
|
||||
|
||||
- PHP 7.4+
|
||||
- Composer
|
||||
- ThinkPHP 6.x
|
||||
|
||||
## 3. 服务器文件结构
|
||||
|
||||
### 3.1 核心文件
|
||||
|
||||
- **启动脚本**: `src/ws_server.php` - WebSocket 服务器主启动文件
|
||||
- **智能客服控制器**: `src/addon/aikefu/api/controller/WebSocket.php` - aikefu 插件的 WebSocket 控制器实现
|
||||
|
||||
### 3.2 目录结构
|
||||
|
||||
```
|
||||
├── backend/
|
||||
│ ├── docs/
|
||||
│ │ └── websocket/
|
||||
│ │ └── README.md # 本文档
|
||||
│ └── src/
|
||||
│ ├── ws_server.php # WebSocket 服务器启动脚本
|
||||
│ └── addon/
|
||||
│ └── aikefu/
|
||||
│ └── api/
|
||||
│ └── controller/
|
||||
│ └── WebSocket.php # aikefu 插件 WebSocket 控制器
|
||||
```
|
||||
|
||||
## 4. 服务器启动与配置
|
||||
|
||||
### 4.1 启动命令
|
||||
|
||||
在 `src` 目录下执行:
|
||||
|
||||
```bash
|
||||
php ws_server.php
|
||||
```
|
||||
|
||||
### 4.2 默认配置
|
||||
|
||||
- **监听地址**: `0.0.0.0` (所有网络接口)
|
||||
- **端口**: `8080`
|
||||
- **WebSocket 地址**: `ws://localhost:8080`
|
||||
|
||||
### 4.3 自定义配置
|
||||
|
||||
可以在 `ws_server.php` 中修改以下配置:
|
||||
|
||||
```php
|
||||
// 配置WebSocket服务器
|
||||
$httpHost = 'localhost'; // 客户端连接时使用的主机名
|
||||
$port = 8080; // WebSocket服务器端口
|
||||
$address = '0.0.0.0'; // 监听所有网络接口
|
||||
```
|
||||
|
||||
## 5. WebSocket 路径
|
||||
|
||||
### 5.1 默认测试路径
|
||||
|
||||
- **路径**: `/ws`
|
||||
- **功能**: 用于测试 WebSocket 连接
|
||||
- **示例**: `ws://localhost:8080/ws`
|
||||
|
||||
### 5.2 智能客服路径
|
||||
|
||||
- **路径**: `/ws/aikefu`
|
||||
- **功能**: 智能客服聊天功能接口
|
||||
- **示例**: `ws://localhost:8080/ws/aikefu`
|
||||
|
||||
## 6. 消息格式
|
||||
|
||||
### 6.1 客户端发送消息格式
|
||||
|
||||
```json
|
||||
{
|
||||
"message": "用户输入的消息",
|
||||
"token": "用户认证令牌",
|
||||
"session_id": "会话ID",
|
||||
"action": "chat" // 动作类型
|
||||
}
|
||||
```
|
||||
|
||||
### 6.2 服务器响应消息格式
|
||||
|
||||
#### 聊天响应
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "chat",
|
||||
"message": "客服回复的消息内容",
|
||||
"session_id": "会话ID",
|
||||
"complete": false // 是否完成响应
|
||||
}
|
||||
```
|
||||
|
||||
#### 错误响应
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "error",
|
||||
"message": "错误信息描述",
|
||||
"code": "错误代码"
|
||||
}
|
||||
```
|
||||
|
||||
#### 系统消息
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "system",
|
||||
"message": "系统消息内容"
|
||||
}
|
||||
```
|
||||
|
||||
## 7. 客户端使用示例
|
||||
|
||||
### 7.1 JavaScript 示例
|
||||
|
||||
```javascript
|
||||
// 创建WebSocket连接
|
||||
const ws = new WebSocket('ws://localhost:8080/ws/aikefu');
|
||||
|
||||
// 连接打开时
|
||||
ws.onopen = function(event) {
|
||||
console.log('WebSocket连接已打开');
|
||||
|
||||
// 发送聊天消息
|
||||
ws.send(JSON.stringify({
|
||||
message: '你好',
|
||||
token: 'user_token_here',
|
||||
session_id: 'session_123',
|
||||
action: 'chat'
|
||||
}));
|
||||
};
|
||||
|
||||
// 接收消息
|
||||
ws.onmessage = function(event) {
|
||||
const data = JSON.parse(event.data);
|
||||
console.log('收到消息:', data);
|
||||
|
||||
if (data.type === 'chat') {
|
||||
// 处理聊天消息
|
||||
console.log('客服回复:', data.message);
|
||||
|
||||
if (data.complete) {
|
||||
console.log('对话完成');
|
||||
}
|
||||
} else if (data.type === 'error') {
|
||||
// 处理错误
|
||||
console.error('错误:', data.message);
|
||||
}
|
||||
};
|
||||
|
||||
// 连接关闭时
|
||||
ws.onclose = function(event) {
|
||||
console.log('WebSocket连接已关闭');
|
||||
};
|
||||
|
||||
// 连接错误时
|
||||
ws.onerror = function(error) {
|
||||
console.error('WebSocket错误:', error);
|
||||
};
|
||||
```
|
||||
|
||||
### 7.2 微信小程序示例
|
||||
|
||||
```javascript
|
||||
// 创建WebSocket连接
|
||||
const ws = wx.connectSocket({
|
||||
url: 'ws://localhost:8080/ws/aikefu',
|
||||
header: {
|
||||
'content-type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
// 连接打开时
|
||||
wx.onSocketOpen(function(res) {
|
||||
console.log('WebSocket连接已打开', res);
|
||||
|
||||
// 发送聊天消息
|
||||
wx.sendSocketMessage({
|
||||
data: JSON.stringify({
|
||||
message: '你好',
|
||||
token: 'user_token_here',
|
||||
session_id: 'session_123',
|
||||
action: 'chat'
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
// 接收消息
|
||||
wx.onSocketMessage(function(res) {
|
||||
const data = JSON.parse(res.data);
|
||||
console.log('收到消息:', data);
|
||||
|
||||
if (data.type === 'chat') {
|
||||
// 处理聊天消息
|
||||
console.log('客服回复:', data.message);
|
||||
|
||||
if (data.complete) {
|
||||
console.log('对话完成');
|
||||
}
|
||||
} else if (data.type === 'error') {
|
||||
// 处理错误
|
||||
console.error('错误:', data.message);
|
||||
}
|
||||
});
|
||||
|
||||
// 连接关闭时
|
||||
wx.onSocketClose(function(res) {
|
||||
console.log('WebSocket连接已关闭', res);
|
||||
});
|
||||
|
||||
// 连接错误时
|
||||
wx.onSocketError(function(res) {
|
||||
console.error('WebSocket错误:', res);
|
||||
});
|
||||
```
|
||||
|
||||
## 8. 功能特性
|
||||
|
||||
### 8.1 实时通信
|
||||
|
||||
- 支持双向实时通信
|
||||
- 消息即时推送
|
||||
- 支持流式响应
|
||||
|
||||
### 8.2 智能客服集成
|
||||
|
||||
- 与现有智能客服系统无缝集成
|
||||
- 支持上下文会话管理
|
||||
- 支持多用户并发访问
|
||||
|
||||
### 8.3 插件化设计
|
||||
|
||||
- 支持为不同插件注册独立的 WebSocket 控制器
|
||||
- 路径格式:`/ws/{addon_name}`
|
||||
- 便于扩展其他插件的 WebSocket 支持
|
||||
|
||||
## 9. 故障排除
|
||||
|
||||
### 9.1 常见问题
|
||||
|
||||
#### 问题:服务器无法启动
|
||||
|
||||
**可能原因**:
|
||||
- 端口被占用
|
||||
- 依赖库未安装
|
||||
- PHP 版本不兼容
|
||||
|
||||
**解决方案**:
|
||||
- 检查端口占用情况:`netstat -an | findstr 8080`
|
||||
- 重新安装依赖:`composer install`
|
||||
- 确保 PHP 版本 >= 7.4
|
||||
|
||||
#### 问题:客户端无法连接
|
||||
|
||||
**可能原因**:
|
||||
- 服务器未启动
|
||||
- 网络防火墙限制
|
||||
- WebSocket 地址错误
|
||||
|
||||
**解决方案**:
|
||||
- 确认服务器已启动
|
||||
- 检查防火墙设置,确保端口 8080 开放
|
||||
- 验证 WebSocket 地址格式
|
||||
|
||||
#### 问题:消息发送失败
|
||||
|
||||
**可能原因**:
|
||||
- 消息格式错误
|
||||
- 认证失败
|
||||
- 服务器内部错误
|
||||
|
||||
**解决方案**:
|
||||
- 检查消息格式是否符合要求
|
||||
- 验证用户认证信息
|
||||
- 查看服务器日志获取详细错误信息
|
||||
|
||||
### 9.2 日志查看
|
||||
|
||||
服务器启动时会输出详细日志,包括:
|
||||
- 已注册的 WebSocket 控制器
|
||||
- 连接信息
|
||||
- 错误信息
|
||||
|
||||
## 10. 扩展开发
|
||||
|
||||
### 10.1 为其他插件添加 WebSocket 支持
|
||||
|
||||
1. 在插件目录下创建 WebSocket 控制器:
|
||||
```
|
||||
addon/{addon_name}/api/controller/WebSocket.php
|
||||
```
|
||||
|
||||
2. 实现 MessageComponentInterface 接口:
|
||||
```php
|
||||
<?php
|
||||
namespace addon\{addon_name}\api\controller;
|
||||
|
||||
use Ratchet\MessageComponentInterface;
|
||||
use Ratchet\ConnectionInterface;
|
||||
|
||||
class WebSocket implements MessageComponentInterface {
|
||||
protected $clients;
|
||||
|
||||
public function __construct() {
|
||||
$this->clients = new \SplObjectStorage;
|
||||
}
|
||||
|
||||
public function onOpen(ConnectionInterface $conn) {
|
||||
// 处理连接打开
|
||||
}
|
||||
|
||||
public function onMessage(ConnectionInterface $conn, $msg) {
|
||||
// 处理收到的消息
|
||||
}
|
||||
|
||||
public function onClose(ConnectionInterface $conn) {
|
||||
// 处理连接关闭
|
||||
}
|
||||
|
||||
public function onError(ConnectionInterface $conn, \Exception $e) {
|
||||
// 处理错误
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
3. 重启 WebSocket 服务器,新插件的 WebSocket 控制器将自动注册到 `/ws/{addon_name}` 路径
|
||||
|
||||
## 11. 版本历史
|
||||
|
||||
| 版本 | 日期 | 说明 |
|
||||
|------|------|------|
|
||||
| v1.0 | 2025-12-19 | 初始版本,支持智能客服系统的 WebSocket 通信 |
|
||||
|
||||
## 12. 联系方式
|
||||
|
||||
如有问题或建议,请联系技术支持团队。
|
||||
124
docs/websocket/nginx/dev.aigc-quickapp.com.conf
Normal file
124
docs/websocket/nginx/dev.aigc-quickapp.com.conf
Normal file
@@ -0,0 +1,124 @@
|
||||
proxy_cache_path /www/dk_project/sites/dev.aigc-quickapp.com/proxy_cache_dir levels=1:2 keys_zone=dev_aigc-quickapp_com_cache:20m inactive=1d max_size=5g;
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
|
||||
listen 443 quic;
|
||||
listen 443 ssl;
|
||||
|
||||
http2 on;
|
||||
server_name dev.aigc-quickapp.com;
|
||||
index index.php index.html index.htm default.php default.htm default.html;
|
||||
root /www/dk_project/wwwroot/dev.aigc-quickapp.com;
|
||||
|
||||
|
||||
#CERT-APPLY-CHECK--START
|
||||
# 用于SSL证书申请时的文件验证相关配置 -- 请勿删除
|
||||
include /www/server/panel/vhost/nginx/well-known/dev.aigc-quickapp.com.conf;
|
||||
#CERT-APPLY-CHECK--END
|
||||
|
||||
#SSL-START SSL相关配置,请勿删除或修改下一行带注释的404规则
|
||||
#error_page 404/404.html;
|
||||
ssl_certificate /www/server/panel/vhost/cert/dev.aigc-quickapp.com/fullchain.pem;
|
||||
ssl_certificate_key /www/server/panel/vhost/cert/dev.aigc-quickapp.com/privkey.pem;
|
||||
ssl_protocols TLSv1.1 TLSv1.2 TLSv1.3;
|
||||
ssl_ciphers EECDH+CHACHA20:EECDH+CHACHA20-draft:EECDH+AES128:RSA+AES128:EECDH+AES256:RSA+AES256:EECDH+3DES:RSA+3DES:!MD5;
|
||||
ssl_prefer_server_ciphers on;
|
||||
ssl_session_cache shared:SSL:10m;
|
||||
ssl_session_timeout 10m;
|
||||
add_header Strict-Transport-Security "max-age=31536000";
|
||||
error_page 497 https://$host$request_uri;
|
||||
#SSL-END
|
||||
#REDIRECT START
|
||||
|
||||
|
||||
#REDIRECT END
|
||||
#ERROR-PAGE-START 错误页配置,可以注释、删除或修改
|
||||
error_page 404 /404.html;
|
||||
#error_page 502 /502.html;
|
||||
#ERROR-PAGE-END
|
||||
|
||||
#WEBSOCKET-SUPPORT START
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection $connection_upgrade;
|
||||
#WEBSOCKET-SUPPORT END
|
||||
|
||||
#PROXY-CONF-START
|
||||
location ^~ / {
|
||||
proxy_pass http://127.0.0.1:8050;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Real-Port $remote_port;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Forwarded-Host $host;
|
||||
proxy_set_header X-Forwarded-Port $server_port;
|
||||
proxy_set_header REMOTE-HOST $remote_addr;
|
||||
|
||||
proxy_buffering off;
|
||||
proxy_connect_timeout 60s;
|
||||
proxy_send_timeout 600s;
|
||||
proxy_read_timeout 600s;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection $connection_upgrade;
|
||||
}
|
||||
#PROXY-CONF-END
|
||||
|
||||
#SERVER-BLOCK START
|
||||
location ~* ^/ws/(.*)$ {
|
||||
# 先尝试直接转发,不修改路径
|
||||
proxy_pass http://localhost:8050;
|
||||
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
|
||||
# 禁用缓冲,确保WebSocket数据实时传输
|
||||
proxy_buffering off;
|
||||
proxy_buffer_size 4k;
|
||||
proxy_buffers 4 4k;
|
||||
proxy_busy_buffers_size 4k;
|
||||
proxy_max_temp_file_size 0;
|
||||
|
||||
# 可选:设置超时(WebSocket 是长连接)
|
||||
proxy_read_timeout 86400s;
|
||||
proxy_send_timeout 86400s;
|
||||
}
|
||||
# 可设置server|location等所有server字段,如:
|
||||
# location /web {
|
||||
# try_files $uri $uri/ /index.php$is_args$args;
|
||||
# }
|
||||
# error_page 404 /diy_404.html;
|
||||
# 如果反代网站访问异常且这里已经配置了内容,请优先排查此处的配置是否正确
|
||||
|
||||
client_max_body_size 500M; # 👈 关键配置!根据需要调整,比如 500M 或 1G
|
||||
#SERVER-BLOCK END
|
||||
|
||||
#禁止访问的文件或目录
|
||||
location ~ ^/(\.user.ini|\.htaccess|\.git|\.env|\.svn|\.project|LICENSE|README.md) {
|
||||
return 404;
|
||||
}
|
||||
|
||||
#一键申请SSL证书验证目录相关设置
|
||||
location /.well-known {
|
||||
allow all;
|
||||
}
|
||||
|
||||
#禁止在证书验证目录放入敏感文件
|
||||
if ( $uri ~ "^/\.well-known/.*\.(php|jsp|py|js|css|lua|ts|go|zip|tar\.gz|rar|7z|sql|bak)$" ) {
|
||||
return 403;
|
||||
}
|
||||
|
||||
#LOG START
|
||||
|
||||
access_log /www/wwwlogs/dev.aigc-quickapp.com.log;
|
||||
error_log /www/wwwlogs/dev.aigc-quickapp.com.error.log;
|
||||
|
||||
#LOG END
|
||||
}
|
||||
231
docs/websocket/test_readme.md
Normal file
231
docs/websocket/test_readme.md
Normal file
@@ -0,0 +1,231 @@
|
||||
# WebSocket 测试文件说明
|
||||
|
||||
本目录包含用于测试 WebSocket 服务器的各种测试文件,支持不同环境和场景的测试需求。
|
||||
|
||||
## 测试文件列表
|
||||
|
||||
1. **`test_websocket.php`** - PHP 客户端测试脚本
|
||||
2. **`test_websocket.html`** - 浏览器端 JavaScript 测试页面
|
||||
3. **`test_wechat_miniprogram.js`** - 微信小程序客户端测试代码
|
||||
4. **`test_readme.md`** - 本测试文件说明文档
|
||||
|
||||
## 测试前准备
|
||||
|
||||
在开始测试之前,请确保:
|
||||
|
||||
1. **启动 WebSocket 服务器**:
|
||||
```bash
|
||||
cd d:/projects/shop-projects/backend/src
|
||||
php ws_server.php
|
||||
```
|
||||
|
||||
2. **确保服务器运行正常**:
|
||||
服务器启动后,您应该看到类似以下输出:
|
||||
```
|
||||
WebSocket服务器已启动,监听地址: ws://localhost:8080
|
||||
|
||||
已注册WebSocket控制器的addon路径:
|
||||
- ws://localhost:8080/ws/aikefu (已注册)
|
||||
|
||||
默认测试路径:
|
||||
- ws://localhost:8080/ws (默认路径,用于连接测试)
|
||||
按 Ctrl+C 停止服务器
|
||||
```
|
||||
|
||||
## 1. PHP 客户端测试 (`test_websocket.php`)
|
||||
|
||||
### 功能说明
|
||||
用于在 PHP 环境下测试 WebSocket 服务器的连接和基本功能。
|
||||
|
||||
### 使用方法
|
||||
|
||||
1. **直接运行测试脚本**:
|
||||
```bash
|
||||
cd d:/projects/shop-projects/backend/docs/websocket
|
||||
php test_websocket.php
|
||||
```
|
||||
|
||||
2. **测试不同路径**:
|
||||
- 默认测试路径:`ws://localhost:8080/ws`
|
||||
- aikefu 插件路径:`ws://localhost:8080/ws/aikefu`
|
||||
|
||||
可以通过修改脚本中的 `$wsUrl` 变量来切换测试路径。
|
||||
|
||||
### 预期输出
|
||||
|
||||
成功连接并发送消息后,您应该看到类似以下输出:
|
||||
|
||||
```
|
||||
正在连接到 WebSocket 服务器: ws://localhost:8080/ws
|
||||
✅ 成功连接到 WebSocket 服务器
|
||||
📤 发送 ping 消息
|
||||
📤 发送测试消息
|
||||
📥 收到消息: {"type":"welcome","message":"欢迎连接到默认WebSocket测试路径","info":"此路径仅用于测试,不提供实际功能。请使用/ws/{addonName}连接到具体的addon服务。"}
|
||||
📥 收到消息: {"type":"pong"}
|
||||
📥 收到消息: {"type":"info","message":"收到消息,但默认路径不提供实际功能","received":{"message":"Hello WebSocket!","action":"test"}}
|
||||
```
|
||||
|
||||
## 2. 浏览器端 JavaScript 测试 (`test_websocket.html`)
|
||||
|
||||
### 功能说明
|
||||
用于在浏览器环境下测试 WebSocket 服务器的连接和交互功能,提供直观的用户界面。
|
||||
|
||||
### 使用方法
|
||||
|
||||
1. **打开测试页面**:
|
||||
- 直接用浏览器打开 `test_websocket.html` 文件
|
||||
- 或者通过 Web 服务器访问:`http://your-domain/docs/websocket/test_websocket.html`
|
||||
|
||||
2. **配置测试参数**:
|
||||
- 在 "设置" 区域输入 WebSocket 服务器地址
|
||||
- 默认地址:`ws://localhost:8080/ws`
|
||||
- aikefu 插件地址:`ws://localhost:8080/ws/aikefu`
|
||||
|
||||
3. **连接和测试**:
|
||||
- 点击 "连接" 按钮建立 WebSocket 连接
|
||||
- 使用 "发送 Ping"、"发送测试消息" 或 "发送自定义消息" 按钮进行测试
|
||||
- 在消息区域查看发送和接收的消息
|
||||
|
||||
### 主要功能
|
||||
|
||||
- **连接管理**:连接、断开连接、重连
|
||||
- **消息发送**:
|
||||
- Ping 消息:测试服务器响应
|
||||
- 测试消息:发送预设的测试数据
|
||||
- 自定义消息:支持发送任意 JSON 格式消息
|
||||
- **消息显示**:
|
||||
- 美化显示 JSON 格式消息
|
||||
- 区分发送和接收的消息
|
||||
- 显示消息时间和发送者
|
||||
|
||||
## 3. 微信小程序客户端测试 (`test_wechat_miniprogram.js`)
|
||||
|
||||
### 功能说明
|
||||
用于在微信小程序环境下测试 WebSocket 服务器的连接和通信功能,模拟实际的微信小程序客户端环境。
|
||||
|
||||
### 使用方法
|
||||
|
||||
1. **集成测试代码**:
|
||||
- 将 `test_wechat_miniprogram.js` 文件中的代码复制到微信小程序的页面 JavaScript 文件中
|
||||
- 将注释中的 WXML 和 WXSS 代码分别复制到对应的 `.wxml` 和 `.wxss` 文件中
|
||||
|
||||
2. **配置服务器地址**:
|
||||
- 修改 `wsUrl` 变量为您的 WebSocket 服务器地址
|
||||
- 注意:微信小程序要求使用 HTTPS/WSS 协议,需要配置 SSL 证书
|
||||
|
||||
3. **运行测试**:
|
||||
- 在微信开发者工具中打开小程序项目
|
||||
- 进入包含测试代码的页面
|
||||
- 点击 "连接" 按钮建立 WebSocket 连接
|
||||
- 使用测试按钮发送消息并查看接收结果
|
||||
|
||||
### 注意事项
|
||||
|
||||
- **微信小程序 WebSocket 限制**:
|
||||
- 只支持 HTTPS/WSS 协议
|
||||
- 需要在小程序管理后台配置合法域名
|
||||
- 最多同时存在 5 个 WebSocket 连接
|
||||
|
||||
- **调试建议**:
|
||||
- 使用微信开发者工具的调试模式查看日志
|
||||
- 先在浏览器端测试确保服务器正常
|
||||
- 注意网络环境和防火墙设置
|
||||
|
||||
## 测试场景和用例
|
||||
|
||||
### 基本连接测试
|
||||
|
||||
| 测试场景 | 预期结果 | 测试文件 |
|
||||
|---------|---------|---------|
|
||||
| 连接默认测试路径 | 成功连接,收到欢迎消息 | 所有测试文件 |
|
||||
| 连接 aikefu 插件路径 | 成功连接 | 所有测试文件 |
|
||||
| 断开连接 | 连接关闭,收到关闭通知 | 所有测试文件 |
|
||||
| 重连 | 成功重新建立连接 | 浏览器和小程序测试文件 |
|
||||
|
||||
### 消息功能测试
|
||||
|
||||
| 测试场景 | 预期结果 | 测试文件 |
|
||||
|---------|---------|---------|
|
||||
| 发送 Ping 消息 | 收到 Pong 响应 | 所有测试文件 |
|
||||
| 发送测试消息 | 收到服务器处理结果 | 所有测试文件 |
|
||||
| 发送 JSON 消息 | 正确解析和处理 | 所有测试文件 |
|
||||
| 发送错误格式消息 | 收到错误提示 | 所有测试文件 |
|
||||
|
||||
## 常见问题和解决方案
|
||||
|
||||
### 1. 连接失败
|
||||
|
||||
**可能原因**:
|
||||
- WebSocket 服务器未启动
|
||||
- 服务器地址或端口错误
|
||||
- 防火墙或网络设置阻止连接
|
||||
- 微信小程序域名未配置
|
||||
|
||||
**解决方案**:
|
||||
- 确认服务器已启动并运行正常
|
||||
- 检查服务器地址和端口配置
|
||||
- 关闭防火墙或添加例外规则
|
||||
- 在微信小程序管理后台配置合法域名
|
||||
|
||||
### 2. 消息发送失败
|
||||
|
||||
**可能原因**:
|
||||
- WebSocket 连接已关闭
|
||||
- 消息格式不正确
|
||||
- 服务器处理错误
|
||||
|
||||
**解决方案**:
|
||||
- 检查连接状态,必要时重新连接
|
||||
- 确保消息格式符合 JSON 规范
|
||||
- 查看服务器日志排查错误
|
||||
|
||||
### 3. 接收消息异常
|
||||
|
||||
**可能原因**:
|
||||
- 服务器返回格式错误
|
||||
- 网络传输问题
|
||||
- 客户端解析错误
|
||||
|
||||
**解决方案**:
|
||||
- 检查服务器代码确保返回正确格式
|
||||
- 检查网络连接稳定性
|
||||
- 查看客户端日志排查解析错误
|
||||
|
||||
## 测试完成后的清理
|
||||
|
||||
测试完成后,您可以:
|
||||
|
||||
1. **停止 WebSocket 服务器**:
|
||||
在运行服务器的终端中按 `Ctrl+C` 停止服务器。
|
||||
|
||||
2. **关闭测试客户端**:
|
||||
- 浏览器测试:关闭浏览器标签页
|
||||
- PHP 测试:测试完成后自动退出
|
||||
- 微信小程序测试:退出小程序页面
|
||||
|
||||
## 扩展测试建议
|
||||
|
||||
1. **性能测试**:
|
||||
- 测试同时连接多个客户端
|
||||
- 测试大消息传输
|
||||
- 测试长时间连接稳定性
|
||||
|
||||
2. **异常场景测试**:
|
||||
- 服务器意外关闭
|
||||
- 网络中断恢复
|
||||
- 消息丢失场景
|
||||
|
||||
3. **功能完整性测试**:
|
||||
- 测试所有消息类型
|
||||
- 测试错误处理机制
|
||||
- 测试安全验证功能
|
||||
|
||||
## 联系和支持
|
||||
|
||||
如果在测试过程中遇到问题,请联系开发人员或查看服务器日志获取更多信息。
|
||||
|
||||
---
|
||||
|
||||
**更新时间**:2025-12-19
|
||||
**版本**:1.0.0
|
||||
**维护人员**:WebSocket 开发团队
|
||||
754
docs/websocket/test_uniapp_wechat.vue
Normal file
754
docs/websocket/test_uniapp_wechat.vue
Normal file
@@ -0,0 +1,754 @@
|
||||
<!--
|
||||
Uniapp WebSocket 测试页面
|
||||
用于在 Uniapp 项目中测试微信小程序的 WebSocket 功能
|
||||
|
||||
使用方法:
|
||||
1. 在 Uniapp 项目的 pages 目录下创建 ws-test 目录
|
||||
2. 将此文件保存为 ws-test.vue
|
||||
3. 在 pages.json 中注册该页面
|
||||
4. 在微信开发者工具中运行项目并访问该页面
|
||||
-->
|
||||
|
||||
<template>
|
||||
<view class="container">
|
||||
<view class="header">
|
||||
<text class="title">WebSocket 测试</text>
|
||||
<view class="status" :class="statusClass">
|
||||
{{ connectionStatus }}
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="settings">
|
||||
<view class="form-item">
|
||||
<text class="label">服务器地址:</text>
|
||||
<input
|
||||
class="input"
|
||||
v-model="wsUrl"
|
||||
placeholder="wss://your-domain.com/ws/aikefu"
|
||||
/>
|
||||
</view>
|
||||
<view class="btn-group">
|
||||
<button
|
||||
class="btn btn-primary"
|
||||
@click="connectWebSocket"
|
||||
:disabled="connecting"
|
||||
>
|
||||
{{ connecting ? '连接中...' : '连接' }}
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-default"
|
||||
@click="disconnectWebSocket"
|
||||
:disabled="!connected"
|
||||
>
|
||||
断开连接
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-warn"
|
||||
@click="reconnectWebSocket"
|
||||
:disabled="connecting"
|
||||
>
|
||||
重连
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="message-area">
|
||||
<scroll-view
|
||||
class="message-list"
|
||||
scroll-y="true"
|
||||
:scroll-top="scrollTop"
|
||||
@scroll="onScroll"
|
||||
>
|
||||
<view
|
||||
v-for="(message, index) in messages"
|
||||
:key="message.id"
|
||||
class="message-item"
|
||||
:class="message.type"
|
||||
>
|
||||
<view class="message-header">
|
||||
<text class="sender">{{ message.sender }}</text>
|
||||
<text class="time">{{ message.time }}</text>
|
||||
</view>
|
||||
<view class="message-content">
|
||||
{{ formatMessage(message.content) }}
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
|
||||
<view class="input-area">
|
||||
<textarea
|
||||
class="message-input"
|
||||
v-model="inputMessage"
|
||||
placeholder="输入要发送的消息..."
|
||||
@confirm="sendCustomMessage"
|
||||
></textarea>
|
||||
<view class="btn-group">
|
||||
<button class="btn btn-sm" @click="sendPing">Ping</button>
|
||||
<button class="btn btn-sm" @click="sendTestMessage">测试消息</button>
|
||||
<button class="btn btn-sm btn-primary" @click="sendCustomMessage">发送</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
wsUrl: 'wss://your-domain.com/ws/aikefu', // 替换为你的 WebSocket 服务器地址
|
||||
connectionStatus: '未连接',
|
||||
connected: false,
|
||||
connecting: false,
|
||||
socketTask: null,
|
||||
messages: [],
|
||||
inputMessage: '',
|
||||
scrollTop: 0,
|
||||
autoScroll: true
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
statusClass() {
|
||||
if (this.connecting) return 'status-connecting';
|
||||
if (this.connected) return 'status-connected';
|
||||
return 'status-disconnected';
|
||||
}
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
// 页面加载时自动连接
|
||||
this.connectWebSocket();
|
||||
},
|
||||
|
||||
onUnload() {
|
||||
// 页面卸载时断开连接
|
||||
this.closeWebSocket();
|
||||
},
|
||||
|
||||
onHide() {
|
||||
// 页面隐藏时断开连接
|
||||
this.closeWebSocket();
|
||||
},
|
||||
|
||||
methods: {
|
||||
// 连接 WebSocket
|
||||
connectWebSocket() {
|
||||
if (this.connected || this.connecting) return;
|
||||
|
||||
this.connecting = true;
|
||||
this.updateStatus('连接中...');
|
||||
|
||||
try {
|
||||
// 创建 WebSocket 连接 (Uniapp API)
|
||||
this.socketTask = uni.connectSocket({
|
||||
url: this.wsUrl,
|
||||
header: {
|
||||
'content-type': 'application/json'
|
||||
},
|
||||
method: 'GET',
|
||||
success: (res) => {
|
||||
console.log('WebSocket 连接请求发送成功', res);
|
||||
},
|
||||
fail: (err) => {
|
||||
console.error('WebSocket 连接请求发送失败', err);
|
||||
this.connecting = false;
|
||||
this.updateStatus('连接失败');
|
||||
this.addMessage('系统', 'WebSocket 连接请求发送失败: ' + JSON.stringify(err), 'error');
|
||||
}
|
||||
});
|
||||
|
||||
// 监听 WebSocket 连接打开
|
||||
this.socketTask.onOpen((res) => {
|
||||
console.log('WebSocket 连接已打开', res);
|
||||
this.connected = true;
|
||||
this.connecting = false;
|
||||
this.updateStatus('已连接');
|
||||
this.addMessage('系统', 'WebSocket 连接已打开', 'system');
|
||||
});
|
||||
|
||||
// 监听 WebSocket 接收到服务器的消息
|
||||
this.socketTask.onMessage((res) => {
|
||||
console.log('收到服务器消息', res.data);
|
||||
this.addMessage('服务器', res.data, 'received');
|
||||
});
|
||||
|
||||
// 监听 WebSocket 连接关闭
|
||||
this.socketTask.onClose((res) => {
|
||||
console.log('WebSocket 连接已关闭', res);
|
||||
this.connected = false;
|
||||
this.connecting = false;
|
||||
this.updateStatus('已断开');
|
||||
this.addMessage('系统', 'WebSocket 连接已关闭', 'system');
|
||||
});
|
||||
|
||||
// 监听 WebSocket 错误
|
||||
this.socketTask.onError((res) => {
|
||||
console.error('WebSocket 连接发生错误', res);
|
||||
this.connected = false;
|
||||
this.connecting = false;
|
||||
this.updateStatus('连接错误');
|
||||
this.addMessage('系统', 'WebSocket 连接发生错误: ' + JSON.stringify(res), 'error');
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('WebSocket 连接异常', error);
|
||||
this.connecting = false;
|
||||
this.updateStatus('连接异常');
|
||||
this.addMessage('系统', 'WebSocket 连接异常: ' + error.message, 'error');
|
||||
}
|
||||
},
|
||||
|
||||
// 断开 WebSocket 连接
|
||||
disconnectWebSocket() {
|
||||
if (!this.connected && !this.connecting) return;
|
||||
|
||||
this.closeWebSocket();
|
||||
this.connected = false;
|
||||
this.connecting = false;
|
||||
this.updateStatus('已断开');
|
||||
this.addMessage('系统', 'WebSocket 连接已手动断开', 'system');
|
||||
},
|
||||
|
||||
// 关闭 WebSocket 连接(内部使用)
|
||||
closeWebSocket() {
|
||||
if (this.socketTask) {
|
||||
try {
|
||||
this.socketTask.close({
|
||||
code: 1000,
|
||||
reason: '用户主动断开连接'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('关闭 WebSocket 连接失败', error);
|
||||
}
|
||||
this.socketTask = null;
|
||||
}
|
||||
},
|
||||
|
||||
// 重连 WebSocket
|
||||
reconnectWebSocket() {
|
||||
this.closeWebSocket();
|
||||
this.connected = false;
|
||||
this.connecting = false;
|
||||
|
||||
// 延迟 1 秒后重连
|
||||
setTimeout(() => {
|
||||
this.connectWebSocket();
|
||||
}, 1000);
|
||||
},
|
||||
|
||||
// 发送 Ping 消息
|
||||
sendPing() {
|
||||
this.sendMessage(JSON.stringify({ action: 'ping' }));
|
||||
},
|
||||
|
||||
// 发送测试消息
|
||||
sendTestMessage() {
|
||||
this.sendMessage(JSON.stringify({
|
||||
message: '你好,这是 Uniapp 测试消息!',
|
||||
action: 'test',
|
||||
timestamp: Date.now()
|
||||
}));
|
||||
},
|
||||
|
||||
// 发送自定义消息
|
||||
sendCustomMessage() {
|
||||
if (!this.inputMessage.trim()) {
|
||||
uni.showToast({
|
||||
title: '请输入消息内容',
|
||||
icon: 'none'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 尝试解析为 JSON
|
||||
JSON.parse(this.inputMessage);
|
||||
this.sendMessage(this.inputMessage);
|
||||
} catch (error) {
|
||||
// 不是 JSON,包装为普通消息
|
||||
this.sendMessage(JSON.stringify({
|
||||
message: this.inputMessage,
|
||||
action: 'chat'
|
||||
}));
|
||||
}
|
||||
|
||||
// 清空输入框
|
||||
this.inputMessage = '';
|
||||
},
|
||||
|
||||
// 发送消息(通用方法)
|
||||
sendMessage(message) {
|
||||
if (!this.connected || !this.socketTask) {
|
||||
uni.showToast({
|
||||
title: 'WebSocket 未连接',
|
||||
icon: 'none'
|
||||
});
|
||||
this.updateStatus('已断开');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 使用 Uniapp API 发送消息
|
||||
this.socketTask.send({
|
||||
data: message,
|
||||
success: () => {
|
||||
this.addMessage('我', message, 'sent');
|
||||
},
|
||||
fail: (err) => {
|
||||
console.error('发送消息失败', err);
|
||||
this.addMessage('系统', '发送消息失败: ' + JSON.stringify(err), 'error');
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('发送消息异常', error);
|
||||
this.addMessage('系统', '发送消息异常: ' + error.message, 'error');
|
||||
}
|
||||
},
|
||||
|
||||
// 更新连接状态
|
||||
updateStatus(status) {
|
||||
this.connectionStatus = status;
|
||||
},
|
||||
|
||||
// 添加消息到消息列表
|
||||
addMessage(sender, content, type) {
|
||||
const message = {
|
||||
id: Date.now(),
|
||||
sender,
|
||||
content,
|
||||
type,
|
||||
time: this.formatTime(new Date())
|
||||
};
|
||||
|
||||
this.messages.push(message);
|
||||
|
||||
// 自动滚动到底部
|
||||
this.scrollToBottom();
|
||||
},
|
||||
|
||||
// 滚动到底部
|
||||
scrollToBottom() {
|
||||
if (this.autoScroll) {
|
||||
// 延迟执行以确保 DOM 已更新
|
||||
this.$nextTick(() => {
|
||||
uni.createSelectorQuery().in(this)
|
||||
.select('.message-list')
|
||||
.boundingClientRect((rect) => {
|
||||
if (rect) {
|
||||
this.scrollTop = rect.height;
|
||||
}
|
||||
})
|
||||
.exec();
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// 格式化时间
|
||||
formatTime(date) {
|
||||
const hours = date.getHours().toString().padStart(2, '0');
|
||||
const minutes = date.getMinutes().toString().padStart(2, '0');
|
||||
const seconds = date.getSeconds().toString().padStart(2, '0');
|
||||
return `${hours}:${minutes}:${seconds}`;
|
||||
},
|
||||
|
||||
// 格式化消息内容(美化 JSON)
|
||||
formatMessage(content) {
|
||||
try {
|
||||
const parsed = JSON.parse(content);
|
||||
return JSON.stringify(parsed, null, 2);
|
||||
} catch (error) {
|
||||
return content;
|
||||
}
|
||||
},
|
||||
|
||||
// 处理滚动事件
|
||||
onScroll(e) {
|
||||
const { scrollTop, scrollHeight, clientHeight } = e.detail;
|
||||
// 判断是否滚动到底部附近
|
||||
this.autoScroll = scrollTop + clientHeight >= scrollHeight - 20;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
padding: 20rpx;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20rpx;
|
||||
padding-bottom: 20rpx;
|
||||
border-bottom: 1rpx solid #eee;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 36rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.status {
|
||||
padding: 8rpx 16rpx;
|
||||
border-radius: 8rpx;
|
||||
font-size: 24rpx;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.status-connecting {
|
||||
background-color: #fff3cd;
|
||||
color: #856404;
|
||||
}
|
||||
|
||||
.status-connected {
|
||||
background-color: #d4edda;
|
||||
color: #155724;
|
||||
}
|
||||
|
||||
.status-disconnected {
|
||||
background-color: #f8d7da;
|
||||
color: #721c24;
|
||||
}
|
||||
|
||||
.settings {
|
||||
background-color: #fff;
|
||||
border-radius: 10rpx;
|
||||
padding: 20rpx;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.form-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.label {
|
||||
font-size: 28rpx;
|
||||
color: #666;
|
||||
margin-bottom: 10rpx;
|
||||
}
|
||||
|
||||
.input {
|
||||
height: 80rpx;
|
||||
padding: 0 20rpx;
|
||||
border: 1rpx solid #ddd;
|
||||
border-radius: 10rpx;
|
||||
font-size: 28rpx;
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
|
||||
.btn-group {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
gap: 15rpx;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.btn {
|
||||
flex: 1;
|
||||
min-width: 150rpx;
|
||||
height: 80rpx;
|
||||
line-height: 80rpx;
|
||||
text-align: center;
|
||||
border-radius: 10rpx;
|
||||
font-size: 28rpx;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background-color: #007aff;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.btn-default {
|
||||
background-color: #f5f5f5;
|
||||
color: #333;
|
||||
border: 1rpx solid #ddd;
|
||||
}
|
||||
|
||||
.btn-warn {
|
||||
background-color: #ff3b30;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.btn-sm {
|
||||
height: 60rpx;
|
||||
line-height: 60rpx;
|
||||
font-size: 24rpx;
|
||||
min-width: 100rpx;
|
||||
}
|
||||
|
||||
.message-area {
|
||||
flex: 1;
|
||||
background-color: #fff;
|
||||
border-radius: 10rpx;
|
||||
margin-bottom: 20rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.message-list {
|
||||
height: 100%;
|
||||
padding: 20rpx;
|
||||
}
|
||||
|
||||
.message-item {
|
||||
margin-bottom: 20rpx;
|
||||
padding: 15rpx;
|
||||
border-radius: 10rpx;
|
||||
max-width: 80%;
|
||||
}
|
||||
|
||||
.message-item.sent {
|
||||
align-self: flex-end;
|
||||
margin-left: auto;
|
||||
background-color: #d4edda;
|
||||
border-right: 4rpx solid #28a745;
|
||||
}
|
||||
|
||||
.message-item.received {
|
||||
align-self: flex-start;
|
||||
background-color: #e9ecef;
|
||||
border-left: 4rpx solid #007bff;
|
||||
}
|
||||
|
||||
.message-item.system {
|
||||
align-self: center;
|
||||
background-color: #fff3cd;
|
||||
border-left: 4rpx solid #856404;
|
||||
max-width: 90%;
|
||||
}
|
||||
|
||||
.message-item.error {
|
||||
align-self: center;
|
||||
background-color: #f8d7da;
|
||||
border-left: 4rpx solid #dc3545;
|
||||
max-width: 90%;
|
||||
}
|
||||
|
||||
.message-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 10rpx;
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.message-content {
|
||||
font-size: 28rpx;
|
||||
line-height: 1.5;
|
||||
word-break: break-all;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.input-area {
|
||||
background-color: #fff;
|
||||
border-radius: 10rpx;
|
||||
padding: 20rpx;
|
||||
}
|
||||
|
||||
.message-input {
|
||||
width: 100%;
|
||||
min-height: 120rpx;
|
||||
max-height: 200rpx;
|
||||
padding: 20rpx;
|
||||
margin-bottom: 20rpx;
|
||||
border: 1rpx solid #ddd;
|
||||
border-radius: 10rpx;
|
||||
font-size: 28rpx;
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script module="pages.json">
|
||||
// 该模块仅用于示例,实际应在项目根目录的 pages.json 中配置
|
||||
|
||||
/*
|
||||
{
|
||||
"pages": [
|
||||
{
|
||||
"path": "pages/ws-test/ws-test",
|
||||
"style": {
|
||||
"navigationBarTitleText": "WebSocket 测试"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
*/
|
||||
</script>
|
||||
|
||||
<script module="README">
|
||||
/*
|
||||
# Uniapp WebSocket 测试页面使用说明
|
||||
|
||||
## 功能说明
|
||||
用于在 Uniapp 项目中测试微信小程序的 WebSocket 功能,支持连接管理、消息发送和接收。
|
||||
|
||||
## 使用方法
|
||||
|
||||
### 1. 创建页面
|
||||
将本文件保存为 `pages/ws-test/ws-test.vue`
|
||||
|
||||
### 2. 配置页面路由
|
||||
在 `pages.json` 中添加以下配置:
|
||||
|
||||
```json
|
||||
{
|
||||
"pages": [
|
||||
{
|
||||
"path": "pages/ws-test/ws-test",
|
||||
"style": {
|
||||
"navigationBarTitleText": "WebSocket 测试"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 配置服务器地址
|
||||
在 `ws-test.vue` 文件中修改 `wsUrl` 变量:
|
||||
|
||||
```javascript
|
||||
data() {
|
||||
return {
|
||||
wsUrl: 'wss://your-domain.com/ws/aikefu', // 替换为你的 WebSocket 服务器地址
|
||||
// ...
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 运行测试
|
||||
- 使用 HBuilderX 开发工具
|
||||
- 选择 "运行" -> "运行到小程序模拟器" -> "微信开发者工具"
|
||||
- 在微信开发者工具中访问该页面
|
||||
|
||||
## 注意事项
|
||||
|
||||
### 微信小程序限制
|
||||
1. **域名配置**:需要在微信小程序管理后台配置合法域名
|
||||
2. **协议支持**:仅支持 HTTPS/WSS 协议
|
||||
3. **连接数限制**:最多同时存在 5 个 WebSocket 连接
|
||||
|
||||
### Uniapp API 特点
|
||||
1. Uniapp 使用 `uni.connectSocket()` 替代微信小程序的 `wx.connectSocket()`
|
||||
2. Uniapp 使用 `uni.createSelectorQuery()` 替代微信小程序的 `wx.createSelectorQuery()`
|
||||
3. 页面生命周期钩子函数使用 Vue 组件的 `onLoad()`、`onUnload()`、`onHide()`
|
||||
|
||||
## 扩展功能
|
||||
|
||||
### 添加消息类型支持
|
||||
可以根据业务需求扩展支持的消息类型:
|
||||
|
||||
```javascript
|
||||
// 发送聊天消息
|
||||
sendChatMessage() {
|
||||
this.sendMessage(JSON.stringify({
|
||||
action: 'chat',
|
||||
message: this.inputMessage,
|
||||
userId: 'user123',
|
||||
timestamp: Date.now()
|
||||
}));
|
||||
}
|
||||
|
||||
// 发送指令消息
|
||||
sendCommand(command, params) {
|
||||
this.sendMessage(JSON.stringify({
|
||||
action: 'command',
|
||||
command,
|
||||
params,
|
||||
timestamp: Date.now()
|
||||
}));
|
||||
}
|
||||
```
|
||||
|
||||
### 添加自动重连机制
|
||||
可以实现更智能的自动重连机制:
|
||||
|
||||
```javascript
|
||||
data() {
|
||||
return {
|
||||
// ...
|
||||
reconnectAttempts: 0,
|
||||
maxReconnectAttempts: 5,
|
||||
reconnectDelay: 1000
|
||||
};
|
||||
},
|
||||
|
||||
methods: {
|
||||
// 自动重连
|
||||
autoReconnect() {
|
||||
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
|
||||
this.addMessage('系统', '自动重连失败,已达到最大尝试次数', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
this.reconnectAttempts++;
|
||||
this.addMessage('系统', `尝试自动重连 (${this.reconnectAttempts}/${this.maxReconnectAttempts})`, 'system');
|
||||
|
||||
setTimeout(() => {
|
||||
this.connectWebSocket();
|
||||
}, this.reconnectDelay);
|
||||
},
|
||||
|
||||
// 连接成功后重置重连计数
|
||||
onConnectSuccess() {
|
||||
this.reconnectAttempts = 0;
|
||||
// ...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 常见问题
|
||||
|
||||
### 1. 连接失败
|
||||
**可能原因**:
|
||||
- 服务器地址错误
|
||||
- 未配置合法域名
|
||||
- 服务器未启动或网络不可达
|
||||
- 使用了 HTTP 协议而不是 HTTPS
|
||||
|
||||
**解决方案**:
|
||||
- 检查服务器地址和端口
|
||||
- 在微信小程序管理后台配置合法域名
|
||||
- 确保服务器正常运行
|
||||
- 切换到 WSS 协议
|
||||
|
||||
### 2. 消息发送失败
|
||||
**可能原因**:
|
||||
- WebSocket 连接已关闭
|
||||
- 消息格式不正确
|
||||
- 网络中断
|
||||
|
||||
**解决方案**:
|
||||
- 检查连接状态,必要时重新连接
|
||||
- 确保消息格式为 JSON 字符串
|
||||
- 检查网络连接
|
||||
|
||||
### 3. 接收消息异常
|
||||
**可能原因**:
|
||||
- 服务器返回格式错误
|
||||
- 客户端解析错误
|
||||
|
||||
**解决方案**:
|
||||
- 检查服务器返回的消息格式
|
||||
- 调试客户端解析逻辑
|
||||
|
||||
## 联系和支持
|
||||
|
||||
如果在使用过程中遇到问题,请联系开发人员或查看相关文档。
|
||||
|
||||
---
|
||||
|
||||
**更新时间**:2025-12-19
|
||||
**版本**:1.0.0
|
||||
**维护人员**:WebSocket 开发团队
|
||||
*/
|
||||
</script>
|
||||
283
docs/websocket/test_websocket.html
Normal file
283
docs/websocket/test_websocket.html
Normal file
@@ -0,0 +1,283 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>WebSocket 测试客户端</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
margin: 20px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
.container {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.header {
|
||||
background: #f5f5f5;
|
||||
padding: 10px;
|
||||
border-radius: 5px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.connection-info {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.status {
|
||||
padding: 5px 10px;
|
||||
border-radius: 3px;
|
||||
font-weight: bold;
|
||||
}
|
||||
.status.connected {
|
||||
background: #d4edda;
|
||||
color: #155724;
|
||||
}
|
||||
.status.disconnected {
|
||||
background: #f8d7da;
|
||||
color: #721c24;
|
||||
}
|
||||
.input-area {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.input-area textarea {
|
||||
width: 100%;
|
||||
height: 100px;
|
||||
padding: 10px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 5px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.input-area button {
|
||||
margin-right: 10px;
|
||||
padding: 8px 15px;
|
||||
background: #007bff;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 3px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.input-area button:hover {
|
||||
background: #0056b3;
|
||||
}
|
||||
.messages {
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 5px;
|
||||
padding: 10px;
|
||||
height: 300px;
|
||||
overflow-y: auto;
|
||||
background: #f9f9f9;
|
||||
}
|
||||
.message {
|
||||
margin-bottom: 10px;
|
||||
padding: 10px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
.message.received {
|
||||
background: #e9ecef;
|
||||
border-left: 4px solid #007bff;
|
||||
}
|
||||
.message.sent {
|
||||
background: #d4edda;
|
||||
border-right: 4px solid #28a745;
|
||||
text-align: right;
|
||||
}
|
||||
.message.error {
|
||||
background: #f8d7da;
|
||||
border-left: 4px solid #dc3545;
|
||||
}
|
||||
.message-header {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
.settings {
|
||||
margin-bottom: 20px;
|
||||
padding: 15px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 5px;
|
||||
}
|
||||
.settings input {
|
||||
width: 300px;
|
||||
padding: 5px;
|
||||
margin: 5px 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>WebSocket 服务器测试客户端</h1>
|
||||
<p>用于测试 WebSocket 服务器的连接和基本功能</p>
|
||||
</div>
|
||||
|
||||
<div class="settings">
|
||||
<h3>设置</h3>
|
||||
<label>WebSocket 服务器地址: </label>
|
||||
<input type="text" id="ws-url" value="ws://localhost:8080/ws" placeholder="ws://localhost:8080/ws">
|
||||
<br>
|
||||
<button onclick="connectWebSocket()">连接</button>
|
||||
<button onclick="disconnectWebSocket()">断开连接</button>
|
||||
</div>
|
||||
|
||||
<div class="connection-info">
|
||||
<strong>连接状态: </strong>
|
||||
<span id="connection-status" class="status disconnected">未连接</span>
|
||||
</div>
|
||||
|
||||
<div class="input-area">
|
||||
<textarea id="message-input" placeholder="输入要发送的消息..."></textarea>
|
||||
<br>
|
||||
<button onclick="sendPing()">发送 Ping</button>
|
||||
<button onclick="sendTestMessage()">发送测试消息</button>
|
||||
<button onclick="sendMessage()">发送自定义消息</button>
|
||||
</div>
|
||||
|
||||
<div class="messages" id="messages-container">
|
||||
<!-- 消息将显示在这里 -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let ws = null;
|
||||
|
||||
// 连接 WebSocket 服务器
|
||||
function connectWebSocket() {
|
||||
const wsUrl = document.getElementById('ws-url').value;
|
||||
|
||||
// 先断开已有连接
|
||||
if (ws) {
|
||||
ws.close();
|
||||
}
|
||||
|
||||
try {
|
||||
ws = new WebSocket(wsUrl);
|
||||
|
||||
ws.onopen = function() {
|
||||
updateStatus('connected', '已连接');
|
||||
addMessage('系统', '成功连接到 WebSocket 服务器', 'received');
|
||||
};
|
||||
|
||||
ws.onmessage = function(event) {
|
||||
addMessage('服务器', event.data, 'received');
|
||||
};
|
||||
|
||||
ws.onclose = function() {
|
||||
updateStatus('disconnected', '已断开连接');
|
||||
addMessage('系统', '与 WebSocket 服务器的连接已断开', 'error');
|
||||
};
|
||||
|
||||
ws.onerror = function(error) {
|
||||
addMessage('系统', 'WebSocket 错误: ' + error, 'error');
|
||||
};
|
||||
} catch (error) {
|
||||
addMessage('系统', '连接失败: ' + error, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// 断开连接
|
||||
function disconnectWebSocket() {
|
||||
if (ws) {
|
||||
ws.close();
|
||||
ws = null;
|
||||
}
|
||||
}
|
||||
|
||||
// 发送 Ping 消息
|
||||
function sendPing() {
|
||||
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
||||
addMessage('系统', 'WebSocket 未连接', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const message = JSON.stringify({action: 'ping'});
|
||||
ws.send(message);
|
||||
addMessage('我', message, 'sent');
|
||||
}
|
||||
|
||||
// 发送测试消息
|
||||
function sendTestMessage() {
|
||||
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
||||
addMessage('系统', 'WebSocket 未连接', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const message = JSON.stringify({message: 'Hello WebSocket!', action: 'test'});
|
||||
ws.send(message);
|
||||
addMessage('我', message, 'sent');
|
||||
}
|
||||
|
||||
// 发送自定义消息
|
||||
function sendMessage() {
|
||||
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
||||
addMessage('系统', 'WebSocket 未连接', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const messageInput = document.getElementById('message-input');
|
||||
const message = messageInput.value.trim();
|
||||
|
||||
if (!message) {
|
||||
addMessage('系统', '请输入消息内容', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 尝试解析为 JSON,如果不是 JSON 则直接发送
|
||||
JSON.parse(message);
|
||||
ws.send(message);
|
||||
} catch (error) {
|
||||
// 不是 JSON,作为普通文本发送
|
||||
ws.send(JSON.stringify({message: message}));
|
||||
}
|
||||
|
||||
addMessage('我', message, 'sent');
|
||||
messageInput.value = '';
|
||||
}
|
||||
|
||||
// 更新连接状态
|
||||
function updateStatus(statusClass, text) {
|
||||
const statusElement = document.getElementById('connection-status');
|
||||
statusElement.className = `status ${statusClass}`;
|
||||
statusElement.textContent = text;
|
||||
}
|
||||
|
||||
// 添加消息到消息列表
|
||||
function addMessage(sender, content, type) {
|
||||
const messagesContainer = document.getElementById('messages-container');
|
||||
const messageElement = document.createElement('div');
|
||||
messageElement.className = `message ${type}`;
|
||||
|
||||
const now = new Date();
|
||||
const time = now.toLocaleTimeString();
|
||||
|
||||
messageElement.innerHTML = `
|
||||
<div class="message-header">${sender} - ${time}</div>
|
||||
<div class="message-content">${formatMessage(content)}</div>
|
||||
`;
|
||||
|
||||
messagesContainer.appendChild(messageElement);
|
||||
messagesContainer.scrollTop = messagesContainer.scrollHeight;
|
||||
}
|
||||
|
||||
// 格式化消息显示(美化 JSON)
|
||||
function formatMessage(content) {
|
||||
try {
|
||||
const parsed = JSON.parse(content);
|
||||
return JSON.stringify(parsed, null, 2);
|
||||
} catch (error) {
|
||||
return content;
|
||||
}
|
||||
}
|
||||
|
||||
// 页面加载完成后自动连接
|
||||
window.onload = function() {
|
||||
connectWebSocket();
|
||||
};
|
||||
|
||||
// 页面关闭时断开连接
|
||||
window.onbeforeunload = function() {
|
||||
disconnectWebSocket();
|
||||
};
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
72
docs/websocket/test_websocket.php
Normal file
72
docs/websocket/test_websocket.php
Normal file
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* WebSocket 服务器 PHP 客户端测试脚本
|
||||
* 用于测试 WebSocket 服务器的连接和基本功能
|
||||
*
|
||||
* 使用方法:php test_websocket.php
|
||||
* 注意:需要先启动 WebSocket 服务器 (php ws_server.php)
|
||||
*/
|
||||
|
||||
use Ratchet\Client\WebSocket;use React\EventLoop\Factory;
|
||||
use React\Socket\Connector;
|
||||
use Ratchet\RFC6455\Messaging\MessageInterface;
|
||||
|
||||
require __DIR__ . '/../../src/vendor/autoload.php';
|
||||
|
||||
// WebSocket 服务器地址
|
||||
$wsUrl = 'ws://localhost:8080/ws'; // 默认测试路径
|
||||
// $wsUrl = 'ws://localhost:8080/ws/aikefu'; // aikefu 插件路径
|
||||
|
||||
echo "正在连接到 WebSocket 服务器: {$wsUrl}\n";
|
||||
|
||||
// 创建事件循环
|
||||
$loop = Factory::create();
|
||||
|
||||
// 创建连接
|
||||
$connector = new Connector($loop);
|
||||
|
||||
// 连接到 WebSocket 服务器
|
||||
$connector($wsUrl)
|
||||
->then(function ($conn) use ($loop) {
|
||||
echo "✅ 成功连接到 WebSocket 服务器\n";
|
||||
|
||||
// 创建 WebSocket 客户端
|
||||
$ws = new WebSocket($conn);
|
||||
|
||||
// 收到消息时的处理
|
||||
$ws->on('message', function (MessageInterface $msg) use ($ws) {
|
||||
echo "📥 收到消息: {$msg}\n";
|
||||
});
|
||||
|
||||
// 连接关闭时的处理
|
||||
$ws->on('close', function ($code = null, $reason = null) {
|
||||
echo "❌ 连接已关闭: {$code} - {$reason}\n";
|
||||
});
|
||||
|
||||
// 发送 ping 消息测试
|
||||
echo "📤 发送 ping 消息\n";
|
||||
$ws->send(json_encode(['action' => 'ping']));
|
||||
|
||||
// 发送测试消息
|
||||
echo "📤 发送测试消息\n";
|
||||
$ws->send(json_encode(['message' => 'Hello WebSocket!', 'action' => 'test']));
|
||||
|
||||
// 3秒后关闭连接
|
||||
$loop->addTimer(3, function () use ($ws) {
|
||||
echo "⏰ 关闭连接\n";
|
||||
$ws->close();
|
||||
});
|
||||
|
||||
// 如果是 aikefu 插件路径,可以发送聊天消息测试
|
||||
// $ws->send(json_encode(['message' => '你好,客服', 'token' => 'your_token', 'action' => 'chat']));
|
||||
|
||||
}, function ($e) use ($loop) {
|
||||
echo "❌ 连接失败: {$e->getMessage()}\n";
|
||||
$loop->stop();
|
||||
});
|
||||
|
||||
// 运行事件循环
|
||||
$loop->run();
|
||||
|
||||
echo "测试完成\n";
|
||||
429
docs/websocket/test_wechat_miniprogram.js
Normal file
429
docs/websocket/test_wechat_miniprogram.js
Normal file
@@ -0,0 +1,429 @@
|
||||
/**
|
||||
* 微信小程序 WebSocket 客户端测试代码
|
||||
* 用于测试微信小程序环境下与 WebSocket 服务器的连接和通信
|
||||
*
|
||||
* 使用方法:
|
||||
* 1. 将此代码复制到微信小程序的页面 JavaScript 文件中
|
||||
* 2. 在对应的 WXML 文件中添加测试按钮和消息显示区域
|
||||
* 3. 在微信开发者工具中运行测试
|
||||
*/
|
||||
|
||||
// WebSocket 连接实例
|
||||
let ws = null;
|
||||
|
||||
// 页面数据
|
||||
Page({
|
||||
data: {
|
||||
wsUrl: 'wss://your-domain.com/ws/aikefu', // 替换为你的 WebSocket 服务器地址
|
||||
connectionStatus: '未连接',
|
||||
messages: [],
|
||||
inputMessage: ''
|
||||
},
|
||||
|
||||
// 生命周期函数--监听页面加载
|
||||
onLoad: function(options) {
|
||||
this.connectWebSocket();
|
||||
},
|
||||
|
||||
// 生命周期函数--监听页面卸载
|
||||
onUnload: function() {
|
||||
this.closeWebSocket();
|
||||
},
|
||||
|
||||
// 生命周期函数--监听页面隐藏
|
||||
onHide: function() {
|
||||
this.closeWebSocket();
|
||||
},
|
||||
|
||||
// 连接 WebSocket 服务器
|
||||
connectWebSocket: function() {
|
||||
const that = this;
|
||||
const wsUrl = this.data.wsUrl;
|
||||
|
||||
// 创建 WebSocket 连接
|
||||
ws = wx.connectSocket({
|
||||
url: wsUrl,
|
||||
header: {
|
||||
'content-type': 'application/json'
|
||||
},
|
||||
method: 'GET',
|
||||
success: function(res) {
|
||||
console.log('WebSocket 连接请求发送成功', res);
|
||||
that.updateStatus('连接中...');
|
||||
},
|
||||
fail: function(err) {
|
||||
console.error('WebSocket 连接请求发送失败', err);
|
||||
that.updateStatus('连接失败');
|
||||
that.addMessage('系统', 'WebSocket 连接请求发送失败: ' + JSON.stringify(err), 'error');
|
||||
}
|
||||
});
|
||||
|
||||
// 监听 WebSocket 连接打开
|
||||
ws.onOpen(function(res) {
|
||||
console.log('WebSocket 连接已打开', res);
|
||||
that.updateStatus('已连接');
|
||||
that.addMessage('系统', 'WebSocket 连接已打开', 'system');
|
||||
});
|
||||
|
||||
// 监听 WebSocket 接收到服务器的消息
|
||||
ws.onMessage(function(res) {
|
||||
console.log('收到服务器消息', res.data);
|
||||
that.addMessage('服务器', res.data, 'received');
|
||||
});
|
||||
|
||||
// 监听 WebSocket 连接关闭
|
||||
ws.onClose(function(res) {
|
||||
console.log('WebSocket 连接已关闭', res);
|
||||
that.updateStatus('已断开');
|
||||
that.addMessage('系统', 'WebSocket 连接已关闭', 'system');
|
||||
});
|
||||
|
||||
// 监听 WebSocket 错误
|
||||
ws.onError(function(res) {
|
||||
console.error('WebSocket 连接发生错误', res);
|
||||
that.updateStatus('连接错误');
|
||||
that.addMessage('系统', 'WebSocket 连接发生错误: ' + JSON.stringify(res), 'error');
|
||||
});
|
||||
},
|
||||
|
||||
// 关闭 WebSocket 连接
|
||||
closeWebSocket: function() {
|
||||
if (ws) {
|
||||
wx.closeSocket();
|
||||
ws = null;
|
||||
}
|
||||
},
|
||||
|
||||
// 发送 Ping 消息
|
||||
sendPing: function() {
|
||||
this.sendMessage(JSON.stringify({ action: 'ping' }));
|
||||
},
|
||||
|
||||
// 发送测试消息
|
||||
sendTestMessage: function() {
|
||||
this.sendMessage(JSON.stringify({
|
||||
message: '你好,这是微信小程序的测试消息!',
|
||||
action: 'test'
|
||||
}));
|
||||
},
|
||||
|
||||
// 发送自定义消息
|
||||
sendCustomMessage: function() {
|
||||
const message = this.data.inputMessage.trim();
|
||||
if (!message) {
|
||||
wx.showToast({
|
||||
title: '请输入消息内容',
|
||||
icon: 'none'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 尝试解析为 JSON,如果不是 JSON 则包装为 JSON
|
||||
JSON.parse(message);
|
||||
this.sendMessage(message);
|
||||
} catch (error) {
|
||||
// 不是 JSON,作为普通文本发送
|
||||
this.sendMessage(JSON.stringify({ message: message }));
|
||||
}
|
||||
|
||||
// 清空输入框
|
||||
this.setData({
|
||||
inputMessage: ''
|
||||
});
|
||||
},
|
||||
|
||||
// 发送消息
|
||||
sendMessage: function(message) {
|
||||
if (!ws) {
|
||||
wx.showToast({
|
||||
title: 'WebSocket 未连接',
|
||||
icon: 'none'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查 WebSocket 连接状态
|
||||
wx.getSocketTask().then(task => {
|
||||
if (task.readyState === 1) { // 连接已打开
|
||||
wx.sendSocketMessage({
|
||||
data: message,
|
||||
success: () => {
|
||||
this.addMessage('我', message, 'sent');
|
||||
},
|
||||
fail: (err) => {
|
||||
console.error('发送消息失败', err);
|
||||
this.addMessage('系统', '发送消息失败: ' + JSON.stringify(err), 'error');
|
||||
}
|
||||
});
|
||||
} else {
|
||||
wx.showToast({
|
||||
title: 'WebSocket 连接未打开',
|
||||
icon: 'none'
|
||||
});
|
||||
this.updateStatus('连接已断开');
|
||||
}
|
||||
}).catch(err => {
|
||||
console.error('获取 SocketTask 失败', err);
|
||||
this.updateStatus('连接已断开');
|
||||
});
|
||||
},
|
||||
|
||||
// 更新输入框内容
|
||||
onInputMessage: function(e) {
|
||||
this.setData({
|
||||
inputMessage: e.detail.value
|
||||
});
|
||||
},
|
||||
|
||||
// 更新连接状态
|
||||
updateStatus: function(status) {
|
||||
this.setData({
|
||||
connectionStatus: status
|
||||
});
|
||||
},
|
||||
|
||||
// 添加消息到消息列表
|
||||
addMessage: function(sender, content, type) {
|
||||
const messages = this.data.messages;
|
||||
const now = new Date();
|
||||
const time = now.toLocaleTimeString();
|
||||
|
||||
// 尝试美化 JSON 格式
|
||||
let formattedContent = content;
|
||||
try {
|
||||
const parsed = JSON.parse(content);
|
||||
formattedContent = JSON.stringify(parsed, null, 2);
|
||||
} catch (error) {
|
||||
// 不是 JSON,保持原样
|
||||
}
|
||||
|
||||
messages.push({
|
||||
id: Date.now(),
|
||||
sender: sender,
|
||||
content: formattedContent,
|
||||
type: type,
|
||||
time: time
|
||||
});
|
||||
|
||||
this.setData({
|
||||
messages: messages
|
||||
});
|
||||
|
||||
// 滚动到最新消息
|
||||
this.scrollToBottom();
|
||||
},
|
||||
|
||||
// 滚动到消息底部
|
||||
scrollToBottom: function() {
|
||||
wx.createSelectorQuery().select('#message-list').boundingClientRect(function(rect) {
|
||||
if (rect) {
|
||||
wx.pageScrollTo({
|
||||
scrollTop: rect.height,
|
||||
duration: 300
|
||||
});
|
||||
}
|
||||
}).exec();
|
||||
},
|
||||
|
||||
// 重连 WebSocket
|
||||
reconnectWebSocket: function() {
|
||||
this.closeWebSocket();
|
||||
setTimeout(() => {
|
||||
this.connectWebSocket();
|
||||
}, 1000);
|
||||
}
|
||||
});
|
||||
|
||||
// 对应的 WXML 文件示例
|
||||
/*
|
||||
<view class="container">
|
||||
<view class="header">
|
||||
<text class="title">WebSocket 测试</text>
|
||||
<text class="status {{connectionStatus === '已连接' ? 'connected' : connectionStatus === '连接中...' ? 'connecting' : 'disconnected'}}">
|
||||
{{connectionStatus}}
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<view class="settings">
|
||||
<input class="url-input" placeholder="请输入 WebSocket 服务器地址" value="{{wsUrl}}" bindinput="onInputUrl" />
|
||||
<button class="btn" bindtap="connectWebSocket" type="primary" size="mini">连接</button>
|
||||
<button class="btn" bindtap="closeWebSocket" type="default" size="mini">断开</button>
|
||||
<button class="btn" bindtap="reconnectWebSocket" type="warn" size="mini">重连</button>
|
||||
</view>
|
||||
|
||||
<view class="message-area">
|
||||
<scroll-view id="message-list" scroll-y="true" class="message-list">
|
||||
<block wx:for="{{messages}}" wx:key="id">
|
||||
<view class="message-item {{item.type}}">
|
||||
<view class="message-header">
|
||||
<text class="sender">{{item.sender}}</text>
|
||||
<text class="time">{{item.time}}</text>
|
||||
</view>
|
||||
<view class="message-content">{{item.content}}</view>
|
||||
</view>
|
||||
</block>
|
||||
</scroll-view>
|
||||
</view>
|
||||
|
||||
<view class="input-area">
|
||||
<textarea class="message-input" placeholder="请输入消息..." value="{{inputMessage}}" bindinput="onInputMessage" />
|
||||
<view class="btn-group">
|
||||
<button class="btn" bindtap="sendPing" type="default" size="mini">发送 Ping</button>
|
||||
<button class="btn" bindtap="sendTestMessage" type="default" size="mini">测试消息</button>
|
||||
<button class="btn" bindtap="sendCustomMessage" type="primary" size="mini">发送</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
*/
|
||||
|
||||
// 对应的 WXSS 样式文件示例
|
||||
/*
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
padding: 20rpx;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 36rpx;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.status {
|
||||
padding: 8rpx 16rpx;
|
||||
border-radius: 8rpx;
|
||||
font-size: 24rpx;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.status.connected {
|
||||
background-color: #d4edda;
|
||||
color: #155724;
|
||||
}
|
||||
|
||||
.status.connecting {
|
||||
background-color: #fff3cd;
|
||||
color: #856404;
|
||||
}
|
||||
|
||||
.status.disconnected {
|
||||
background-color: #f8d7da;
|
||||
color: #721c24;
|
||||
}
|
||||
|
||||
.settings {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 20rpx;
|
||||
padding: 20rpx;
|
||||
background-color: white;
|
||||
border-radius: 10rpx;
|
||||
}
|
||||
|
||||
.url-input {
|
||||
flex: 1;
|
||||
height: 80rpx;
|
||||
padding: 20rpx;
|
||||
margin-right: 20rpx;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 10rpx;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.btn {
|
||||
margin-right: 10rpx;
|
||||
}
|
||||
|
||||
.message-area {
|
||||
flex: 1;
|
||||
margin-bottom: 20rpx;
|
||||
background-color: white;
|
||||
border-radius: 10rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.message-list {
|
||||
height: 100%;
|
||||
padding: 20rpx;
|
||||
}
|
||||
|
||||
.message-item {
|
||||
margin-bottom: 20rpx;
|
||||
padding: 15rpx;
|
||||
border-radius: 10rpx;
|
||||
max-width: 80%;
|
||||
}
|
||||
|
||||
.message-item.sent {
|
||||
align-self: flex-end;
|
||||
margin-left: auto;
|
||||
background-color: #d4edda;
|
||||
border-right: 4rpx solid #28a745;
|
||||
}
|
||||
|
||||
.message-item.received {
|
||||
align-self: flex-start;
|
||||
background-color: #e9ecef;
|
||||
border-left: 4rpx solid #007bff;
|
||||
}
|
||||
|
||||
.message-item.system {
|
||||
align-self: center;
|
||||
background-color: #fff3cd;
|
||||
border-left: 4rpx solid #856404;
|
||||
max-width: 90%;
|
||||
}
|
||||
|
||||
.message-item.error {
|
||||
align-self: center;
|
||||
background-color: #f8d7da;
|
||||
border-left: 4rpx solid #dc3545;
|
||||
max-width: 90%;
|
||||
}
|
||||
|
||||
.message-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 10rpx;
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.message-content {
|
||||
font-size: 28rpx;
|
||||
line-height: 1.5;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.input-area {
|
||||
background-color: white;
|
||||
padding: 20rpx;
|
||||
border-radius: 10rpx;
|
||||
}
|
||||
|
||||
.message-input {
|
||||
width: 100%;
|
||||
height: 120rpx;
|
||||
padding: 20rpx;
|
||||
margin-bottom: 20rpx;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 10rpx;
|
||||
font-size: 28rpx;
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
|
||||
.btn-group {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
*/
|
||||
170
replace_comments.py
Normal file
170
replace_comments.py
Normal file
@@ -0,0 +1,170 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import os
|
||||
import argparse
|
||||
|
||||
# 定义要查找和替换的注释内容
|
||||
# 使用原始字符串数组,支持多个旧内容,避免Unicode转义问题
|
||||
OLD_COMMENTS = [
|
||||
r"""
|
||||
<?php
|
||||
/**
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
*/
|
||||
""".strip(),
|
||||
r"""
|
||||
<?php
|
||||
|
||||
/**
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
*/
|
||||
""".strip(),
|
||||
# 格式1:标准注释格式
|
||||
r"""
|
||||
<?php
|
||||
/**
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
*/
|
||||
""".strip(),
|
||||
r"""
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Niushop商城系统 - 团队十年电商经验汇集巨献!
|
||||
* =========================================================
|
||||
* Copy right 2019-2029 杭州牛之云科技有限公司, 保留所有权利。
|
||||
* ----------------------------------------------
|
||||
* 官方网址: https://www.niushop.com
|
||||
* =========================================================
|
||||
*/
|
||||
""".strip(),
|
||||
# 格式1:标准注释格式
|
||||
r"""
|
||||
<?php
|
||||
/**
|
||||
* Niushop商城系统 - 团队十年电商经验汇集巨献!
|
||||
* =========================================================
|
||||
* Copy right 2019-2029 杭州牛之云科技有限公司, 保留所有权利。
|
||||
* ----------------------------------------------
|
||||
* 官方网址: https://www.niushop.com
|
||||
* =========================================================
|
||||
*/""".strip(),
|
||||
# 格式2:带有额外空行的注释格式
|
||||
r"""
|
||||
<?php
|
||||
/**
|
||||
* Niushop商城系统 - 团队十年电商经验汇集巨献!
|
||||
* =========================================================
|
||||
* Copy right 2019-2029 杭州牛之云科技有限公司, 保留所有权利。
|
||||
* ----------------------------------------------
|
||||
* 官方网址: https://www.niushop.com
|
||||
|
||||
* =========================================================
|
||||
*/""".strip(),
|
||||
]
|
||||
|
||||
NEW_COMMENT = r"""
|
||||
<?php
|
||||
""".strip()
|
||||
|
||||
# 定义要处理的文件类型
|
||||
FILE_TYPES = [".php", ".js", ".css", ".html", ".vue", ".ts", ".tsx", ".jsx", ".scss", ".less"]
|
||||
|
||||
def replace_comments(file_path):
|
||||
"""
|
||||
替换文件中的注释
|
||||
"""
|
||||
try:
|
||||
# 读取文件内容
|
||||
with open(file_path, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
|
||||
# 检查文件是否包含任何旧注释
|
||||
has_old_comment = False
|
||||
for old_comment in OLD_COMMENTS:
|
||||
if old_comment in content:
|
||||
has_old_comment = True
|
||||
break
|
||||
|
||||
if has_old_comment:
|
||||
# 替换所有旧注释
|
||||
new_content = content
|
||||
for old_comment in OLD_COMMENTS:
|
||||
if old_comment in new_content:
|
||||
new_content = new_content.replace(old_comment, NEW_COMMENT)
|
||||
|
||||
# 写回文件
|
||||
with open(file_path, 'w', encoding='utf-8') as f:
|
||||
f.write(new_content)
|
||||
|
||||
print(f"✓ 已处理: {file_path}")
|
||||
return True
|
||||
return False
|
||||
|
||||
except UnicodeDecodeError:
|
||||
# 跳过二进制文件
|
||||
print(f"✗ 跳过二进制文件: {file_path}")
|
||||
return False
|
||||
except PermissionError:
|
||||
# 跳过没有权限的文件
|
||||
print(f"✗ 权限不足: {file_path}")
|
||||
return False
|
||||
except Exception as e:
|
||||
# 处理其他异常
|
||||
print(f"✗ 处理失败 {file_path}: {str(e)}")
|
||||
return False
|
||||
|
||||
def main():
|
||||
"""
|
||||
主函数
|
||||
"""
|
||||
parser = argparse.ArgumentParser(description="替换Niushop商城系统的注释")
|
||||
parser.add_argument("--path", type=str, default=".", help="要遍历的目录路径")
|
||||
args = parser.parse_args()
|
||||
|
||||
root_path = args.path
|
||||
total_files = 0
|
||||
processed_files = 0
|
||||
|
||||
print(f"开始遍历目录: {root_path}")
|
||||
print(f"将处理的文件类型: {', '.join(FILE_TYPES)}")
|
||||
print("=" * 60)
|
||||
|
||||
# 遍历所有文件
|
||||
for root, dirs, files in os.walk(root_path):
|
||||
# 跳过某些目录(如.git、vendor、node_modules等)
|
||||
dirs[:] = [d for d in dirs if d not in ['.git', 'vendor', 'node_modules', 'runtime', 'upload', 'public', 'static']]
|
||||
|
||||
for file in files:
|
||||
# 检查文件类型
|
||||
if any(file.endswith(ext) for ext in FILE_TYPES):
|
||||
total_files += 1
|
||||
file_path = os.path.join(root, file)
|
||||
if replace_comments(file_path):
|
||||
processed_files += 1
|
||||
|
||||
print("=" * 60)
|
||||
print(f"处理完成!")
|
||||
print(f"总文件数: {total_files}")
|
||||
print(f"已处理文件数: {processed_files}")
|
||||
print(f"替换率: {processed_files / total_files * 100:.2f}%" if total_files > 0 else "未找到匹配的文件")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
22
scripts/generate_key_pair/main.js
Normal file
22
scripts/generate_key_pair/main.js
Normal file
@@ -0,0 +1,22 @@
|
||||
const crypto = require('crypto');
|
||||
// 生成密钥对
|
||||
const { publicKey, privateKey } = crypto.generateKeyPairSync('rsa', {
|
||||
modulusLength: 3072, // 密钥长度,不少于3072
|
||||
publicKeyEncoding: {
|
||||
type: 'spki', // 公钥编码格式
|
||||
format: 'pem' // 公钥输出格式
|
||||
},
|
||||
privateKeyEncoding: {
|
||||
type: 'pkcs8', // 私钥编码格式
|
||||
format: 'pem' // 私钥输出格式
|
||||
}
|
||||
});
|
||||
console.info('生成的公钥:');
|
||||
console.info(publicKey);
|
||||
console.info('生成的私钥:');
|
||||
console.info(privateKey);
|
||||
|
||||
// 保存密钥对到文件
|
||||
const fs = require('fs');
|
||||
fs.writeFileSync('merchant_public_key.pem', publicKey);
|
||||
fs.writeFileSync('merchant_private_key.pem', privateKey);
|
||||
@@ -3,23 +3,22 @@ APP_TRACE = true
|
||||
|
||||
[APP]
|
||||
DEFAULT_TIMEZONE = Asia/Shanghai
|
||||
ENV_MODE = development
|
||||
|
||||
[LANG]
|
||||
default_lang = zh-cn
|
||||
|
||||
[DATABASE]
|
||||
TYPE = mysql
|
||||
HOSTNAME = 127.0.0.1
|
||||
DATABASE = shop_mallnew_dev
|
||||
USERNAME = root
|
||||
PASSWORD = root
|
||||
HOSTNAME = db
|
||||
DATABASE = shop_mallnew
|
||||
USERNAME = shop_mallnew
|
||||
PASSWORD = shop_mallnew
|
||||
HOSTPORT = 3306
|
||||
CHARSET = utf8
|
||||
CHARSET = utf8mb4
|
||||
DEBUG = true
|
||||
|
||||
[redis]
|
||||
HOST = 127.0.0.1
|
||||
[REDIS]
|
||||
HOST = redis
|
||||
PORT = 6379
|
||||
PASSWORD = ''
|
||||
EXPIRY = 604800
|
||||
PASSWORD = 'luckyshop123!@#'
|
||||
EXPIRY = 10800
|
||||
@@ -10,16 +10,16 @@ default_lang = zh-cn
|
||||
|
||||
[DATABASE]
|
||||
TYPE = mysql
|
||||
HOSTNAME = production_mysql_host
|
||||
DATABASE = shop_mallnew_prod
|
||||
USERNAME = prod_user
|
||||
PASSWORD = prod_password
|
||||
HOSTNAME = db
|
||||
DATABASE = shop_mallnew
|
||||
USERNAME = shop_mallnew
|
||||
PASSWORD = shop_mallnew
|
||||
HOSTPORT = 3306
|
||||
CHARSET = utf8
|
||||
CHARSET = utf8mb4
|
||||
DEBUG = false
|
||||
|
||||
[redis]
|
||||
HOST = production_redis_host
|
||||
[REDIS]
|
||||
HOST = redis
|
||||
PORT = 6379
|
||||
PASSWORD = production_redis_password
|
||||
EXPIRY = 86400
|
||||
PASSWORD = 'luckyshop123!@#'
|
||||
EXPIRY = 10800
|
||||
@@ -1,22 +1,24 @@
|
||||
APP_DEBUG = true
|
||||
APP_TRACE = true
|
||||
|
||||
[APP]
|
||||
DEFAULT_TIMEZONE = Asia/Shanghai
|
||||
|
||||
[LANG]
|
||||
default_lang = zh-cn
|
||||
|
||||
[DATABASE]
|
||||
TYPE = mysql
|
||||
HOSTNAME = newshop_mysql
|
||||
DATABASE = shop_dev
|
||||
HOSTNAME = db
|
||||
DATABASE = shop_mallnew
|
||||
USERNAME = shop_mallnew
|
||||
PASSWORD = shop_mallnew
|
||||
HOSTPORT = 3306
|
||||
CHARSET = utf8
|
||||
CHARSET = utf8mb4
|
||||
DEBUG = true
|
||||
[RRDATABASE]
|
||||
HOSTNAME = 192.168.2.64
|
||||
[redis]
|
||||
HOST = newshop_redis
|
||||
|
||||
[REDIS]
|
||||
HOST = redis
|
||||
PORT = 6379
|
||||
PASSWORD = 'luckyshop123!@#'
|
||||
EXPIRY = 604800
|
||||
EXPIRY = 10800
|
||||
24
src/.env.test
Normal file
24
src/.env.test
Normal file
@@ -0,0 +1,24 @@
|
||||
APP_DEBUG = true
|
||||
APP_TRACE = true
|
||||
|
||||
[APP]
|
||||
DEFAULT_TIMEZONE = Asia/Shanghai
|
||||
|
||||
[LANG]
|
||||
default_lang = zh-cn
|
||||
|
||||
[DATABASE]
|
||||
TYPE = mysql
|
||||
HOSTNAME = db
|
||||
DATABASE = shop_mallnew
|
||||
USERNAME = shop_mallnew
|
||||
PASSWORD = shop_mallnew
|
||||
HOSTPORT = 3306
|
||||
CHARSET = utf8mb4
|
||||
DEBUG = true
|
||||
|
||||
[REDIS]
|
||||
HOST = redis
|
||||
PORT = 6379
|
||||
PASSWORD = 'luckyshop123!@#'
|
||||
EXPIRY = 10800
|
||||
1015
src/addon/aikefu/api/controller/Kefu.php
Normal file
1015
src/addon/aikefu/api/controller/Kefu.php
Normal file
File diff suppressed because it is too large
Load Diff
1758
src/addon/aikefu/api/controller/WebSocket.php
Normal file
1758
src/addon/aikefu/api/controller/WebSocket.php
Normal file
File diff suppressed because it is too large
Load Diff
33
src/addon/aikefu/config/event.php
Normal file
33
src/addon/aikefu/config/event.php
Normal file
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
/**
|
||||
* 智能客服扩展事件配置
|
||||
*/
|
||||
return [
|
||||
'bind' => [
|
||||
|
||||
],
|
||||
|
||||
'listen' => [
|
||||
'KefuGetConfig' => [
|
||||
'addon\aikefu\event\KefuGetConfig'
|
||||
],
|
||||
'KefuChat' => [
|
||||
'addon\aikefu\event\KefuChat'
|
||||
],
|
||||
'KefuGetHistory' => [
|
||||
'addon\aikefu\event\KefuGetHistory'
|
||||
],
|
||||
'KefuClearConversation' => [
|
||||
'addon\aikefu\event\KefuClearConversation'
|
||||
],
|
||||
'KefuHealthCheck' => [
|
||||
'addon\aikefu\event\KefuHealthCheck'
|
||||
],
|
||||
'KefuGetInfo' => [
|
||||
'addon\aikefu\event\KefuGetInfo'
|
||||
],
|
||||
],
|
||||
|
||||
'subscribe' => [
|
||||
],
|
||||
];
|
||||
13
src/addon/aikefu/config/info.php
Normal file
13
src/addon/aikefu/config/info.php
Normal file
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'name' => 'aikefu',
|
||||
'title' => '智能客服',
|
||||
'description' => '基于Dify的智能客服系统',
|
||||
'type' => 'system', //插件类型 system :系统插件(自动安装), business:业务插件 promotion:扩展营销插件 tool:工具插件
|
||||
'status' => 1,
|
||||
'author' => '',
|
||||
'version' => '1.0.0',
|
||||
'version_no' => '525231212001',
|
||||
'content' => '',
|
||||
];
|
||||
43
src/addon/aikefu/data/install.sql
Normal file
43
src/addon/aikefu/data/install.sql
Normal file
@@ -0,0 +1,43 @@
|
||||
-- 智能客服插件安装脚本
|
||||
-- 1. 智能客服插件使用系统配置表存储配置信息,无需创建独立数据表
|
||||
-- 2. 会话和消息数据存储在独立数据表中
|
||||
|
||||
-- 创建智能客服会话表
|
||||
CREATE TABLE IF NOT EXISTS `lucky_aikefu_conversation` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`site_id` int(11) NOT NULL COMMENT '站点ID',
|
||||
`user_id` varchar(50) NOT NULL COMMENT '用户ID',
|
||||
`conversation_id` varchar(100) NOT NULL COMMENT 'Dify会话ID',
|
||||
`name` varchar(255) NOT NULL COMMENT '会话名称',
|
||||
`status` tinyint(1) NOT NULL DEFAULT 1 COMMENT '状态:1活跃,0结束',
|
||||
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `site_id` (`site_id`),
|
||||
KEY `user_id` (`user_id`),
|
||||
KEY `conversation_id` (`conversation_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='智能客服会话表';
|
||||
-- 索引
|
||||
ALTER TABLE `lucky_aikefu_conversation`
|
||||
ADD INDEX `idx_status` (`status`);
|
||||
|
||||
-- 创建智能客服消息表
|
||||
CREATE TABLE IF NOT EXISTS `lucky_aikefu_message` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`site_id` int(11) NOT NULL COMMENT '站点ID',
|
||||
`user_id` varchar(50) NOT NULL COMMENT '用户ID',
|
||||
`conversation_id` varchar(100) NOT NULL COMMENT '会话ID',
|
||||
`message_id` varchar(100) NOT NULL COMMENT '消息ID',
|
||||
`role` varchar(20) NOT NULL COMMENT '角色:user用户,assistant助手',
|
||||
`content` text NOT NULL COMMENT '消息内容',
|
||||
`status` varchar(20) NOT NULL DEFAULT 'completed' COMMENT '消息状态:streaming(流式中), completed(已完成), failed(失败)',
|
||||
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `site_id` (`site_id`),
|
||||
KEY `user_id` (`user_id`),
|
||||
KEY `conversation_id` (`conversation_id`),
|
||||
KEY `message_id` (`message_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='智能客服消息表';
|
||||
-- 索引
|
||||
ALTER TABLE `lucky_aikefu_message`
|
||||
ADD INDEX `idx_status` (`status`);
|
||||
4
src/addon/aikefu/data/uninstall.sql
Normal file
4
src/addon/aikefu/data/uninstall.sql
Normal file
@@ -0,0 +1,4 @@
|
||||
-- 智能客服插件卸载脚本
|
||||
-- 删除智能客服相关表(配置信息存储在系统配置表中,无需单独删除)
|
||||
DROP TABLE IF EXISTS `lucky_aikefu_message`;
|
||||
DROP TABLE IF EXISTS `lucky_aikefu_conversation`;
|
||||
2
src/addon/aikefu/docs/jquery-4.0.0.min.js
vendored
Normal file
2
src/addon/aikefu/docs/jquery-4.0.0.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
595
src/addon/aikefu/docs/stream_chat_demo.html
Normal file
595
src/addon/aikefu/docs/stream_chat_demo.html
Normal file
@@ -0,0 +1,595 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>流式聊天测试 Demo</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.container {
|
||||
background-color: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
text-align: center;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.method-selector {
|
||||
margin-bottom: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.method-btn {
|
||||
padding: 10px 20px;
|
||||
margin: 0 10px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.method-btn.active {
|
||||
background-color: #0056b3;
|
||||
}
|
||||
|
||||
#chat-container {
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
height: 400px;
|
||||
overflow-y: auto;
|
||||
margin-bottom: 20px;
|
||||
padding: 10px;
|
||||
background-color: #fafafa;
|
||||
}
|
||||
|
||||
.message {
|
||||
margin-bottom: 15px;
|
||||
padding: 10px;
|
||||
border-radius: 8px;
|
||||
max-width: 80%;
|
||||
}
|
||||
|
||||
.user-message {
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.ai-message {
|
||||
background-color: #e9ecef;
|
||||
color: #333;
|
||||
margin-right: auto;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.input-area {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
#message-input {
|
||||
flex: 1;
|
||||
padding: 12px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
#send-btn {
|
||||
padding: 12px 20px;
|
||||
background-color: #28a745;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
#send-btn:disabled {
|
||||
background-color: #6c757d;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.api-config {
|
||||
margin-bottom: 20px;
|
||||
padding: 15px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
.api-config h3 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 10px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.input-group {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.input-group label {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.input-group input {
|
||||
flex: 1;
|
||||
min-width: 300px;
|
||||
padding: 8px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
#save-api-url {
|
||||
padding: 8px 16px;
|
||||
background-color: #28a745;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
#save-api-url:hover {
|
||||
background-color: #218838;
|
||||
}
|
||||
|
||||
#reset-api-url {
|
||||
padding: 8px 16px;
|
||||
background-color: #6c757d;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
#reset-api-url:hover {
|
||||
background-color: #5a6268;
|
||||
}
|
||||
|
||||
.config-hint {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
margin-top: 5px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.status {
|
||||
margin-top: 10px;
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>流式聊天测试 Demo</h1>
|
||||
|
||||
<div class="api-config">
|
||||
<h3>API 配置</h3>
|
||||
<div class="input-group">
|
||||
<label for="api-url-input">API URL:</label>
|
||||
<input type="text" id="api-url-input" placeholder="输入完整 API URL 或仅域名(默认添加 /api/kefu/chat)">
|
||||
<button id="save-api-url">保存</button>
|
||||
<button id="reset-api-url">重置默认</button>
|
||||
</div>
|
||||
<p class="config-hint">提示:您可以只修改域名部分(如 http://localhost:8050),系统会自动添加默认路径 /api/kefu/chat</p>
|
||||
</div>
|
||||
|
||||
<div class="method-selector">
|
||||
<h3>选择请求方式:</h3>
|
||||
<button class="method-btn active" data-method="eventsource">EventSource</button>
|
||||
<button class="method-btn" data-method="fetch">Fetch API</button>
|
||||
</div>
|
||||
|
||||
<div id="chat-container"></div>
|
||||
|
||||
<div class="input-area">
|
||||
<input type="text" id="message-input" placeholder="输入消息...">
|
||||
<button id="send-btn">发送</button>
|
||||
</div>
|
||||
|
||||
<div class="status">
|
||||
<span id="status-text">就绪</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// 配置
|
||||
let API_URL = localStorage.getItem('apiUrl') || 'http://localhost:8050/api/kefu/chat';
|
||||
const UNIACID = '1';
|
||||
let conversationId = '';
|
||||
let currentMethod = 'eventsource';
|
||||
let es = null;
|
||||
let controller = null;
|
||||
|
||||
// DOM 元素
|
||||
const chatContainer = document.getElementById('chat-container');
|
||||
const messageInput = document.getElementById('message-input');
|
||||
const sendBtn = document.getElementById('send-btn');
|
||||
const statusText = document.getElementById('status-text');
|
||||
const methodBtns = document.querySelectorAll('.method-btn');
|
||||
const apiUrlInput = document.getElementById('api-url-input');
|
||||
const saveApiUrlBtn = document.getElementById('save-api-url');
|
||||
const resetApiUrlBtn = document.getElementById('reset-api-url');
|
||||
|
||||
// 事件监听
|
||||
messageInput.addEventListener('keypress', (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
sendMessage();
|
||||
}
|
||||
});
|
||||
|
||||
sendBtn.addEventListener('click', sendMessage);
|
||||
|
||||
methodBtns.forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
methodBtns.forEach(b => b.classList.remove('active'));
|
||||
btn.classList.add('active');
|
||||
currentMethod = btn.dataset.method;
|
||||
statusText.textContent = `已切换到 ${btn.textContent} 方式`;
|
||||
});
|
||||
});
|
||||
|
||||
// API URL 保存事件
|
||||
saveApiUrlBtn.addEventListener('click', () => {
|
||||
let newApiUrl = apiUrlInput.value.trim();
|
||||
if (newApiUrl) {
|
||||
// 检查是否只输入了域名(没有 /api/kefu/chat 路径)
|
||||
if (!newApiUrl.includes('/api/kefu/chat')) {
|
||||
// 确保域名末尾没有斜杠
|
||||
newApiUrl = newApiUrl.replace(/\/$/, '') + '/api/kefu/chat';
|
||||
}
|
||||
localStorage.setItem('apiUrl', newApiUrl);
|
||||
API_URL = newApiUrl;
|
||||
// 更新输入框显示完整 URL
|
||||
apiUrlInput.value = newApiUrl;
|
||||
statusText.textContent = 'API URL 已保存';
|
||||
setTimeout(() => {
|
||||
statusText.textContent = '就绪';
|
||||
}, 2000);
|
||||
}
|
||||
});
|
||||
|
||||
// 重置默认 API URL
|
||||
resetApiUrlBtn.addEventListener('click', () => {
|
||||
const defaultApiUrl = 'http://localhost:8050/api/kefu/chat';
|
||||
localStorage.setItem('apiUrl', defaultApiUrl);
|
||||
API_URL = defaultApiUrl;
|
||||
apiUrlInput.value = defaultApiUrl;
|
||||
statusText.textContent = '已重置为默认 API URL';
|
||||
setTimeout(() => {
|
||||
statusText.textContent = '就绪';
|
||||
}, 2000);
|
||||
});
|
||||
|
||||
|
||||
|
||||
// 发送消息
|
||||
function sendMessage() {
|
||||
const message = messageInput.value.trim();
|
||||
if (!message) return;
|
||||
|
||||
// 清空输入框
|
||||
messageInput.value = '';
|
||||
|
||||
// 添加用户消息到聊天记录
|
||||
addMessageToChat(message, 'user');
|
||||
|
||||
// 禁用发送按钮
|
||||
sendBtn.disabled = true;
|
||||
statusText.textContent = '正在发送请求...';
|
||||
|
||||
// 根据选择的方式发送请求
|
||||
if (currentMethod === 'eventsource') {
|
||||
sendEventSourceRequest(message);
|
||||
} else {
|
||||
sendFetchStreamRequest(message);
|
||||
}
|
||||
}
|
||||
|
||||
// EventSource 方式
|
||||
function sendEventSourceRequest(message) {
|
||||
// 关闭之前的连接
|
||||
if (es) {
|
||||
es.close();
|
||||
es = null;
|
||||
}
|
||||
|
||||
// 构建请求 URL
|
||||
const params = new URLSearchParams({
|
||||
uniacid: UNIACID,
|
||||
user_id: '123456',
|
||||
query: message,
|
||||
conversation_id: conversationId || '',
|
||||
stream: 'true'
|
||||
});
|
||||
|
||||
const url = `${API_URL}?${params.toString()}`;
|
||||
|
||||
try {
|
||||
statusText.textContent = 'EventSource 连接中...';
|
||||
es = new EventSource(url);
|
||||
|
||||
let aiMessage = '';
|
||||
|
||||
// 监听消息事件
|
||||
es.addEventListener('message', (event) => {
|
||||
console.log('收到消息:', event);
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
|
||||
if (data.event === 'message') {
|
||||
// 更新 AI 消息
|
||||
aiMessage += data.answer || '';
|
||||
updateAIMessage(aiMessage);
|
||||
}
|
||||
|
||||
if (data.event === 'message_end') {
|
||||
statusText.textContent = '对话完成';
|
||||
// 不要在这里关闭连接,等待done和close事件
|
||||
}
|
||||
|
||||
if (data.conversation_id) {
|
||||
conversationId = data.conversation_id;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('解析消息失败:', error);
|
||||
}
|
||||
});
|
||||
|
||||
// 监听完成事件
|
||||
es.addEventListener('done', (event) => {
|
||||
console.log('收到完成事件:', event);
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
if (data.conversation_id) {
|
||||
conversationId = data.conversation_id;
|
||||
}
|
||||
statusText.textContent = '对话完成';
|
||||
sendBtn.disabled = false;
|
||||
} catch (error) {
|
||||
console.error('解析完成事件失败:', error);
|
||||
}
|
||||
});
|
||||
|
||||
// 监听连接关闭事件
|
||||
es.addEventListener('close', (event) => {
|
||||
console.log('收到连接关闭事件:', event);
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
console.log('连接正常结束:', data);
|
||||
if (es) {
|
||||
es.close();
|
||||
es = null;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('解析关闭事件失败:', error);
|
||||
}
|
||||
});
|
||||
|
||||
// 监听错误事件
|
||||
es.addEventListener('error', (error) => {
|
||||
console.error('EventSource 错误:', error);
|
||||
// 添加更详细的错误信息
|
||||
if (es.readyState === EventSource.CLOSED) {
|
||||
console.log('EventSource 连接已正常关闭');
|
||||
statusText.textContent = '连接已关闭';
|
||||
} else if (es.readyState === EventSource.CONNECTING) {
|
||||
console.error('EventSource 连接中出现错误');
|
||||
statusText.textContent = '连接错误';
|
||||
} else {
|
||||
console.error('EventSource 未知错误');
|
||||
statusText.textContent = '连接错误';
|
||||
}
|
||||
sendBtn.disabled = false;
|
||||
if (es) {
|
||||
es.close();
|
||||
es = null;
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('创建 EventSource 失败:', error);
|
||||
statusText.textContent = '请求失败';
|
||||
sendBtn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch API 流式请求方式
|
||||
async function sendFetchStreamRequest(message) {
|
||||
// 取消之前的请求
|
||||
if (controller) {
|
||||
controller.abort();
|
||||
controller = null;
|
||||
}
|
||||
|
||||
// 创建 AbortController
|
||||
controller = new AbortController();
|
||||
const signal = controller.signal;
|
||||
|
||||
// 构建请求体
|
||||
const body = new URLSearchParams({
|
||||
uniacid: UNIACID,
|
||||
user_id: '123456',
|
||||
query: message,
|
||||
conversation_id: conversationId || '',
|
||||
stream: 'true'
|
||||
});
|
||||
|
||||
try {
|
||||
statusText.textContent = 'Fetch 连接中...';
|
||||
const response = await fetch(API_URL, {
|
||||
method: 'POST',
|
||||
body: body,
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'Accept': 'text/event-stream'
|
||||
},
|
||||
signal: signal
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP 错误! 状态: ${response.status}`);
|
||||
}
|
||||
|
||||
if (!response.body) {
|
||||
throw new Error('响应体不可用');
|
||||
}
|
||||
|
||||
const reader = response.body.getReader();
|
||||
const decoder = new TextDecoder('utf-8');
|
||||
let buffer = '';
|
||||
let aiMessage = '';
|
||||
|
||||
statusText.textContent = '接收流式响应...';
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
|
||||
buffer += decoder.decode(value, { stream: true });
|
||||
|
||||
// 处理接收到的数据
|
||||
buffer = processStreamData(buffer, (newData) => {
|
||||
if (newData) {
|
||||
// 更新 AI 消息
|
||||
aiMessage += newData;
|
||||
updateAIMessage(aiMessage);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 处理剩余数据
|
||||
buffer = processStreamData(buffer, (newData) => {
|
||||
if (newData) {
|
||||
aiMessage += newData;
|
||||
updateAIMessage(aiMessage);
|
||||
}
|
||||
});
|
||||
|
||||
statusText.textContent = '对话完成';
|
||||
sendBtn.disabled = false;
|
||||
|
||||
} catch (error) {
|
||||
if (error.name === 'AbortError') {
|
||||
statusText.textContent = '请求已取消';
|
||||
} else {
|
||||
console.error('Fetch 请求失败:', error);
|
||||
statusText.textContent = `请求失败: ${error.message}`;
|
||||
}
|
||||
sendBtn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 处理流式数据
|
||||
function processStreamData(buffer, callback) {
|
||||
const lines = buffer.split('\n');
|
||||
buffer = lines.pop() || ''; // 最后一行可能不完整
|
||||
|
||||
lines.forEach(line => {
|
||||
line = line.trim();
|
||||
if (!line) return;
|
||||
|
||||
// 解析 SSE 格式
|
||||
if (line.startsWith('data:')) {
|
||||
const dataPart = line.slice(5).trim();
|
||||
if (dataPart) {
|
||||
try {
|
||||
const data = JSON.parse(dataPart);
|
||||
|
||||
if (data.event === 'message') {
|
||||
callback(data.answer || '');
|
||||
}
|
||||
|
||||
if (data.conversation_id) {
|
||||
conversationId = data.conversation_id;
|
||||
}
|
||||
|
||||
if (data.event === 'message_end') {
|
||||
// 对话完成
|
||||
console.log('对话完成');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('解析流式数据失败:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return buffer;
|
||||
}
|
||||
|
||||
// 添加消息到聊天记录
|
||||
function addMessageToChat(message, type) {
|
||||
const messageDiv = document.createElement('div');
|
||||
messageDiv.className = `message ${type}-message`;
|
||||
messageDiv.textContent = message;
|
||||
chatContainer.appendChild(messageDiv);
|
||||
chatContainer.scrollTop = chatContainer.scrollHeight;
|
||||
|
||||
// 如果是用户消息,添加一个临时的 AI 消息容器
|
||||
if (type === 'user') {
|
||||
const aiMessageDiv = document.createElement('div');
|
||||
aiMessageDiv.id = 'temp-ai-message';
|
||||
aiMessageDiv.className = 'message ai-message';
|
||||
chatContainer.appendChild(aiMessageDiv);
|
||||
chatContainer.scrollTop = chatContainer.scrollHeight;
|
||||
}
|
||||
}
|
||||
|
||||
// 更新 AI 消息
|
||||
function updateAIMessage(message) {
|
||||
let aiMessageDiv = document.getElementById('temp-ai-message');
|
||||
if (!aiMessageDiv) {
|
||||
// 如果临时容器不存在,创建一个新的
|
||||
aiMessageDiv = document.createElement('div');
|
||||
aiMessageDiv.id = 'temp-ai-message';
|
||||
aiMessageDiv.className = 'message ai-message';
|
||||
chatContainer.appendChild(aiMessageDiv);
|
||||
}
|
||||
|
||||
aiMessageDiv.textContent = message;
|
||||
chatContainer.scrollTop = chatContainer.scrollHeight;
|
||||
}
|
||||
|
||||
// 页面卸载时清理资源
|
||||
window.addEventListener('beforeunload', () => {
|
||||
if (es) {
|
||||
es.close();
|
||||
}
|
||||
if (controller) {
|
||||
controller.abort();
|
||||
}
|
||||
});
|
||||
|
||||
// 初始化
|
||||
apiUrlInput.value = API_URL;
|
||||
statusText.textContent = '就绪,使用 EventSource 方式';
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
13
src/addon/aikefu/docs/vue.3.6.0-beta.3.global.prod.js
Normal file
13
src/addon/aikefu/docs/vue.3.6.0-beta.3.global.prod.js
Normal file
File diff suppressed because one or more lines are too long
1127
src/addon/aikefu/docs/ws_multi_addon_test.html
Normal file
1127
src/addon/aikefu/docs/ws_multi_addon_test.html
Normal file
File diff suppressed because it is too large
Load Diff
50
src/addon/aikefu/event/Install.php
Normal file
50
src/addon/aikefu/event/Install.php
Normal file
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
namespace addon\aikefu\event;
|
||||
|
||||
use app\model\system\Addon as AddonModel;
|
||||
|
||||
/**
|
||||
* 智能客服插件安装
|
||||
*/
|
||||
class Install
|
||||
{
|
||||
public function handle()
|
||||
{
|
||||
$addon_model = new AddonModel();
|
||||
$info = $addon_model->getAddonInfo(['name' => 'aikefu']);
|
||||
|
||||
if (empty($info['data'])) {
|
||||
// 插件未安装,执行安装逻辑
|
||||
$addon_model->addAddon([
|
||||
'name' => 'aikefu',
|
||||
'title' => '智能客服',
|
||||
'description' => '基于Dify的智能客服系统',
|
||||
'author' => 'admin',
|
||||
'version' => '1.0.0',
|
||||
'scene' => 'web',
|
||||
'state' => 0,
|
||||
'category' => 'business',
|
||||
'need_install' => 1,
|
||||
'need_cache' => 1,
|
||||
'create_time' => time(),
|
||||
'update_time' => time()
|
||||
]);
|
||||
} else {
|
||||
// 插件已存在,更新插件信息
|
||||
$addon_model->updateAddon([
|
||||
'title' => '智能客服',
|
||||
'description' => '基于Dify的智能客服系统',
|
||||
'author' => 'admin',
|
||||
'version' => '1.0.0',
|
||||
'scene' => 'web',
|
||||
'category' => 'business',
|
||||
'need_install' => 1,
|
||||
'need_cache' => 1,
|
||||
'update_time' => time()
|
||||
], ['name' => 'aikefu']);
|
||||
}
|
||||
|
||||
return success(1);
|
||||
}
|
||||
}
|
||||
44
src/addon/aikefu/event/KefuChat.php
Normal file
44
src/addon/aikefu/event/KefuChat.php
Normal file
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
namespace addon\aikefu\event;
|
||||
|
||||
use addon\aikefu\api\controller\Kefu as KefuApi;
|
||||
|
||||
/**
|
||||
* 客服聊天
|
||||
*/
|
||||
class KefuChat
|
||||
{
|
||||
/**
|
||||
* 处理智能客服聊天事件
|
||||
* @param array $data 事件数据
|
||||
* @return array|null
|
||||
*/
|
||||
public function handle($data)
|
||||
{
|
||||
try {
|
||||
// 创建addon的KefuApi实例
|
||||
$kefu_api = new KefuApi();
|
||||
|
||||
// 调用初始化方法设置属性
|
||||
$kefu_api->initializeForEvent($data);
|
||||
|
||||
// 调用addon的chat方法
|
||||
$response = $kefu_api->chat();
|
||||
|
||||
// 对于流式请求,直接输出不返回数据
|
||||
if (isset($data['stream']) && $data['stream']) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 返回响应数据
|
||||
return json_decode($response->getContent(), true);
|
||||
} catch (\Exception $e) {
|
||||
return [
|
||||
'code' => -1,
|
||||
'message' => '聊天失败:' . $e->getMessage(),
|
||||
'data' => []
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
118
src/addon/aikefu/event/KefuClearConversation.php
Normal file
118
src/addon/aikefu/event/KefuClearConversation.php
Normal file
@@ -0,0 +1,118 @@
|
||||
<?php
|
||||
|
||||
namespace addon\aikefu\event;
|
||||
|
||||
use addon\aikefu\model\Conversation as KefuConversationModel;
|
||||
use addon\aikefu\model\Message as KefuMessageModel;
|
||||
|
||||
/**
|
||||
* 清除客服会话历史
|
||||
*/
|
||||
class KefuClearConversation
|
||||
{
|
||||
/**
|
||||
* 处理清除会话历史事件
|
||||
* @param array $data 事件数据
|
||||
* @return array
|
||||
*/
|
||||
public function handle($data)
|
||||
{
|
||||
try {
|
||||
$conversation_id = $data['conversation_id'] ?? '';
|
||||
$user_id = $data['user_id'] ?? '';
|
||||
$site_id = $data['site_id'] ?? 0;
|
||||
|
||||
// 验证参数
|
||||
if (empty($conversation_id) && empty($user_id)) {
|
||||
return [
|
||||
'code' => -1,
|
||||
'message' => '会话ID或用户ID不能为空',
|
||||
'data' => []
|
||||
];
|
||||
}
|
||||
|
||||
$conversation_model = new KefuConversationModel();
|
||||
$message_model = new KefuMessageModel();
|
||||
|
||||
$deleted_messages = 0;
|
||||
$deleted_conversations = 0;
|
||||
|
||||
if (!empty($conversation_id)) {
|
||||
// 删除指定会话的消息和会话记录
|
||||
|
||||
// 先删除该会话的所有消息
|
||||
$message_condition = [
|
||||
['site_id', '=', $site_id],
|
||||
['conversation_id', '=', $conversation_id]
|
||||
];
|
||||
|
||||
$message_result = $message_model->deleteMessage($message_condition);
|
||||
if ($message_result['code'] >= 0) {
|
||||
$deleted_messages = $message_result['data']['result'] ?? 0;
|
||||
}
|
||||
|
||||
// 再删除会话记录
|
||||
$conversation_condition = [
|
||||
['site_id', '=', $site_id],
|
||||
['conversation_id', '=', $conversation_id]
|
||||
];
|
||||
|
||||
$conversation_result = $conversation_model->deleteConversation($conversation_condition);
|
||||
if ($conversation_result['code'] >= 0) {
|
||||
$deleted_conversations = $conversation_result['data']['result'] ?? 0;
|
||||
}
|
||||
|
||||
} else if (!empty($user_id)) {
|
||||
// 删除指定用户的所有会话和消息
|
||||
|
||||
// 先获取该用户的所有会话ID
|
||||
$conversation_list = $conversation_model->getConversationList([
|
||||
['site_id', '=', $site_id],
|
||||
['user_id', '=', $user_id]
|
||||
], 'conversation_id');
|
||||
|
||||
$conversation_ids = array_column($conversation_list['data'], 'conversation_id');
|
||||
|
||||
if (!empty($conversation_ids)) {
|
||||
// 删除所有会话的消息
|
||||
$message_condition = [
|
||||
['site_id', '=', $site_id],
|
||||
['conversation_id', 'in', $conversation_ids]
|
||||
];
|
||||
|
||||
$message_result = $message_model->deleteMessage($message_condition);
|
||||
if ($message_result['code'] >= 0) {
|
||||
$deleted_messages = $message_result['data']['result'] ?? 0;
|
||||
}
|
||||
|
||||
// 删除所有会话记录
|
||||
$conversation_condition = [
|
||||
['site_id', '=', $site_id],
|
||||
['user_id', '=', $user_id]
|
||||
];
|
||||
|
||||
$conversation_result = $conversation_model->deleteConversation($conversation_condition);
|
||||
if ($conversation_result['code'] >= 0) {
|
||||
$deleted_conversations = $conversation_result['data']['result'] ?? 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'code' => 0,
|
||||
'message' => '清除成功',
|
||||
'data' => [
|
||||
'deleted_messages' => $deleted_messages,
|
||||
'deleted_conversations' => $deleted_conversations
|
||||
]
|
||||
];
|
||||
|
||||
} catch (\Exception $e) {
|
||||
return [
|
||||
'code' => -1,
|
||||
'message' => '清除失败:' . $e->getMessage(),
|
||||
'data' => []
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
53
src/addon/aikefu/event/KefuGetConfig.php
Normal file
53
src/addon/aikefu/event/KefuGetConfig.php
Normal file
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
namespace addon\aikefu\event;
|
||||
|
||||
use addon\aikefu\model\Config as KefuConfigModel;
|
||||
|
||||
/**
|
||||
* 获取智能客服配置信息
|
||||
*/
|
||||
class KefuGetConfig
|
||||
{
|
||||
/**
|
||||
* 处理获取配置信息事件
|
||||
* @param array $data 事件数据
|
||||
* @return array
|
||||
*/
|
||||
public function handle($data)
|
||||
{
|
||||
$site_id = $data['site_id'] ?? 0;
|
||||
|
||||
$response_data = [
|
||||
'enabled' => false,
|
||||
'status' => 'disabled'
|
||||
];
|
||||
|
||||
try {
|
||||
// 获取智能客服配置
|
||||
$kefu_config_model = new KefuConfigModel();
|
||||
$config_info = $kefu_config_model->getConfig($site_id);
|
||||
|
||||
$response_data = [
|
||||
'enabled' => false,
|
||||
'status' => 'disabled'
|
||||
];
|
||||
|
||||
// 处理配置信息
|
||||
if (!empty($config_info['data']['value'])) {
|
||||
$config = $config_info['data']['value'];
|
||||
|
||||
// 服务状态
|
||||
$response_data['enabled'] = $config['status'] == 1;
|
||||
$response_data['status'] = $config['status'] == 1 ? 'enabled' : 'disabled';
|
||||
}
|
||||
|
||||
return $response_data;
|
||||
|
||||
} catch (\Exception $e) {
|
||||
$response_data['status'] = 'error';
|
||||
$response_data['error'] = $e->getMessage();
|
||||
return $response_data;
|
||||
}
|
||||
}
|
||||
}
|
||||
39
src/addon/aikefu/event/KefuGetHistory.php
Normal file
39
src/addon/aikefu/event/KefuGetHistory.php
Normal file
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace addon\aikefu\event;
|
||||
|
||||
use addon\aikefu\api\controller\Kefu as KefuApi;
|
||||
|
||||
/**
|
||||
* 处理智能客服获取历史消息事件
|
||||
*/
|
||||
class KefuGetHistory
|
||||
{
|
||||
/**
|
||||
* 处理智能客服获取历史消息事件
|
||||
* @param array $data 事件数据
|
||||
* @return array
|
||||
*/
|
||||
public function handle($data)
|
||||
{
|
||||
try {
|
||||
// 创建addon的KefuApi实例
|
||||
$kefu_api = new KefuApi();
|
||||
|
||||
// 调用初始化方法设置属性
|
||||
$kefu_api->initializeForEvent($data);
|
||||
|
||||
// 调用addon的getHistory方法
|
||||
$response = $kefu_api->getHistory();
|
||||
|
||||
// 返回响应数据
|
||||
return json_decode($response->getContent(), true);
|
||||
} catch (\Exception $e) {
|
||||
return [
|
||||
'code' => -1,
|
||||
'message' => '获取历史消息失败:' . $e->getMessage(),
|
||||
'data' => []
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
125
src/addon/aikefu/event/KefuGetInfo.php
Normal file
125
src/addon/aikefu/event/KefuGetInfo.php
Normal file
@@ -0,0 +1,125 @@
|
||||
<?php
|
||||
|
||||
namespace addon\aikefu\event;
|
||||
|
||||
use addon\aikefu\model\Config as KefuConfigModel;
|
||||
|
||||
/**
|
||||
* 获取智能客服配置信息
|
||||
*/
|
||||
class KefuGetInfo
|
||||
{
|
||||
/**
|
||||
* 处理获取配置信息事件
|
||||
* @param array $data 事件数据
|
||||
* @return array
|
||||
*/
|
||||
public function handle($data)
|
||||
{
|
||||
$site_id = $data['site_id'] ?? 0;
|
||||
$member_id = $data['member_id'] ?? 0;
|
||||
$client_info = $data['client_info'] ?? [];
|
||||
|
||||
try {
|
||||
// 获取智能客服配置
|
||||
$kefu_config_model = new KefuConfigModel();
|
||||
$config_info = $kefu_config_model->getConfig($site_id);
|
||||
|
||||
$response_data = [
|
||||
'service_info' => [
|
||||
'name' => '智能客服',
|
||||
'version' => '1.0.0',
|
||||
'enabled' => false,
|
||||
'status' => 'disabled'
|
||||
],
|
||||
'features' => [],
|
||||
'limits' => [
|
||||
'max_message_length' => 4000,
|
||||
'max_conversation_history' => 100,
|
||||
'rate_limit' => [
|
||||
'requests_per_minute' => 60,
|
||||
'requests_per_hour' => 1000
|
||||
]
|
||||
],
|
||||
'endpoints' => [
|
||||
'chat' => '/api/kefu/chat',
|
||||
'get_history' => '/api/kefu/getHistory',
|
||||
'clear_conversation' => '/api/kefu/clearConversation',
|
||||
'health' => '/api/kefu/health',
|
||||
'info' => '/api/kefu/info'
|
||||
],
|
||||
'client_info' => $client_info,
|
||||
'server_info' => [
|
||||
'php_version' => PHP_VERSION,
|
||||
'server_time' => date('Y-m-d H:i:s'),
|
||||
'timezone' => date_default_timezone_get()
|
||||
]
|
||||
];
|
||||
|
||||
// 处理配置信息
|
||||
if (!empty($config_info['data']['value'])) {
|
||||
$config = $config_info['data']['value'];
|
||||
|
||||
// 服务状态
|
||||
$response_data['service_info']['enabled'] = $config['status'] == 1;
|
||||
$response_data['service_info']['status'] = $config['status'] == 1 ? 'enabled' : 'disabled';
|
||||
|
||||
// 可用功能
|
||||
if ($config['status'] == 1) {
|
||||
$response_data['features'] = [
|
||||
'chat' => true,
|
||||
'chat_stream' => true,
|
||||
'conversation_management' => true,
|
||||
'history_management' => true
|
||||
];
|
||||
}
|
||||
|
||||
// API端点信息(仅在启用时显示详细配置)
|
||||
if ($config['status'] == 1) {
|
||||
$response_data['api_config'] = [
|
||||
'base_url' => $config['base_url'] ?? '',
|
||||
'chat_endpoint' => $config['chat_endpoint'] ?? '',
|
||||
'supports_streaming' => true,
|
||||
'authentication' => 'bearer_token'
|
||||
];
|
||||
}
|
||||
|
||||
// 限制配置(如果有的话)
|
||||
if (isset($config['max_message_length'])) {
|
||||
$response_data['limits']['max_message_length'] = intval($config['max_message_length']);
|
||||
}
|
||||
if (isset($config['rate_limit_per_minute'])) {
|
||||
$response_data['limits']['rate_limit']['requests_per_minute'] = intval($config['rate_limit_per_minute']);
|
||||
}
|
||||
}
|
||||
|
||||
// 添加使用统计信息(如果需要的话)
|
||||
if ($member_id > 0) {
|
||||
$response_data['user_stats'] = [
|
||||
'can_use_service' => $response_data['service_info']['enabled'],
|
||||
'member_id' => $member_id,
|
||||
'site_id' => $site_id
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'code' => 0,
|
||||
'message' => '获取配置信息成功',
|
||||
'data' => $response_data
|
||||
];
|
||||
|
||||
} catch (\Exception $e) {
|
||||
return [
|
||||
'code' => -1,
|
||||
'message' => '获取配置信息失败:' . $e->getMessage(),
|
||||
'data' => [
|
||||
'service_info' => [
|
||||
'name' => '智能客服',
|
||||
'status' => 'error'
|
||||
],
|
||||
'error' => $e->getMessage()
|
||||
]
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
349
src/addon/aikefu/event/KefuHealthCheck.php
Normal file
349
src/addon/aikefu/event/KefuHealthCheck.php
Normal file
@@ -0,0 +1,349 @@
|
||||
<?php
|
||||
|
||||
namespace addon\aikefu\event;
|
||||
|
||||
use addon\aikefu\model\Config as KefuConfigModel;
|
||||
use think\facade\Db;
|
||||
|
||||
/**
|
||||
* 智能客服健康检查
|
||||
*/
|
||||
class KefuHealthCheck
|
||||
{
|
||||
/**
|
||||
* 处理健康检查事件
|
||||
* @param array $data 事件数据
|
||||
* @return array
|
||||
*/
|
||||
public function handle($data)
|
||||
{
|
||||
$check_results = [];
|
||||
$site_id = $data['site_id'] ?? 0;
|
||||
$check_type = $data['check_type'] ?? 'full';
|
||||
|
||||
try {
|
||||
// 1. 数据库连接检查
|
||||
if (in_array($check_type, ['full', 'basic'])) {
|
||||
$check_results[] = $this->checkDatabase();
|
||||
}
|
||||
|
||||
// 2. AI服务配置检查
|
||||
if (in_array($check_type, ['full', 'ai_service'])) {
|
||||
$check_results[] = $this->checkAIServiceConfig($site_id);
|
||||
}
|
||||
|
||||
// 3. AI服务连接检查
|
||||
if (in_array($check_type, ['full', 'ai_service'])) {
|
||||
$check_results[] = $this->checkAIServiceConnection($site_id);
|
||||
}
|
||||
|
||||
// 4. 系统资源检查
|
||||
if (in_array($check_type, ['full'])) {
|
||||
$check_results[] = $this->checkSystemResources();
|
||||
}
|
||||
|
||||
} catch (\Exception $e) {
|
||||
$check_results[] = [
|
||||
'component' => 'health_check_error',
|
||||
'status' => 'error',
|
||||
'message' => '健康检查过程异常:' . $e->getMessage(),
|
||||
'response_time_ms' => 0
|
||||
];
|
||||
}
|
||||
|
||||
return $check_results;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查数据库连接
|
||||
*/
|
||||
private function checkDatabase()
|
||||
{
|
||||
$start_time = microtime(true);
|
||||
|
||||
try {
|
||||
// 测试数据库连接
|
||||
$result = Db::query('SELECT 1 as test');
|
||||
|
||||
if (!empty($result) && $result[0]['test'] == 1) {
|
||||
return [
|
||||
'component' => 'database',
|
||||
'status' => 'healthy',
|
||||
'message' => '数据库连接正常',
|
||||
'response_time_ms' => round((microtime(true) - $start_time) * 1000, 2),
|
||||
'details' => [
|
||||
'connection' => 'success',
|
||||
'query_test' => 'passed'
|
||||
]
|
||||
];
|
||||
} else {
|
||||
return [
|
||||
'component' => 'database',
|
||||
'status' => 'error',
|
||||
'message' => '数据库查询测试失败',
|
||||
'response_time_ms' => round((microtime(true) - $start_time) * 1000, 2)
|
||||
];
|
||||
}
|
||||
|
||||
} catch (\Exception $e) {
|
||||
return [
|
||||
'component' => 'database',
|
||||
'status' => 'error',
|
||||
'message' => '数据库连接失败:' . $e->getMessage(),
|
||||
'response_time_ms' => round((microtime(true) - $start_time) * 1000, 2)
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查AI服务配置
|
||||
*/
|
||||
private function checkAIServiceConfig($site_id)
|
||||
{
|
||||
$start_time = microtime(true);
|
||||
|
||||
try {
|
||||
$config_model = new KefuConfigModel();
|
||||
$config_info = $config_model->getConfig($site_id);
|
||||
|
||||
if (empty($config_info['data']['value'])) {
|
||||
return [
|
||||
'component' => 'ai_service_config',
|
||||
'status' => 'warning',
|
||||
'message' => '智能客服配置未设置',
|
||||
'response_time_ms' => round((microtime(true) - $start_time) * 1000, 2),
|
||||
'details' => [
|
||||
'configured' => false,
|
||||
'required_fields' => ['api_key', 'base_url', 'chat_endpoint']
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
$config = $config_info['data']['value'];
|
||||
$required_fields = ['api_key', 'base_url', 'chat_endpoint'];
|
||||
$missing_fields = [];
|
||||
|
||||
foreach ($required_fields as $field) {
|
||||
if (empty($config[$field])) {
|
||||
$missing_fields[] = $field;
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($missing_fields)) {
|
||||
return [
|
||||
'component' => 'ai_service_config',
|
||||
'status' => 'warning',
|
||||
'message' => 'AI服务配置不完整,缺少字段:' . implode(', ', $missing_fields),
|
||||
'response_time_ms' => round((microtime(true) - $start_time) * 1000, 2),
|
||||
'details' => [
|
||||
'configured' => true,
|
||||
'complete' => false,
|
||||
'missing_fields' => $missing_fields
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
if ($config['status'] != 1) {
|
||||
return [
|
||||
'component' => 'ai_service_config',
|
||||
'status' => 'warning',
|
||||
'message' => '智能服务已禁用',
|
||||
'response_time_ms' => round((microtime(true) - $start_time) * 1000, 2),
|
||||
'details' => [
|
||||
'configured' => true,
|
||||
'complete' => true,
|
||||
'enabled' => false
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'component' => 'ai_service_config',
|
||||
'status' => 'healthy',
|
||||
'message' => 'AI服务配置正常',
|
||||
'response_time_ms' => round((microtime(true) - $start_time) * 1000, 2),
|
||||
'details' => [
|
||||
'configured' => true,
|
||||
'complete' => true,
|
||||
'enabled' => true,
|
||||
'base_url' => $config['base_url']
|
||||
]
|
||||
];
|
||||
|
||||
} catch (\Exception $e) {
|
||||
return [
|
||||
'component' => 'ai_service_config',
|
||||
'status' => 'error',
|
||||
'message' => 'AI服务配置检查失败:' . $e->getMessage(),
|
||||
'response_time_ms' => round((microtime(true) - $start_time) * 1000, 2)
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查AI服务连接
|
||||
*/
|
||||
private function checkAIServiceConnection($site_id)
|
||||
{
|
||||
$start_time = microtime(true);
|
||||
|
||||
try {
|
||||
$config_model = new KefuConfigModel();
|
||||
$config_info = $config_model->getConfig($site_id);
|
||||
|
||||
if (empty($config_info['data']['value'])) {
|
||||
return [
|
||||
'component' => 'ai_service_connection',
|
||||
'status' => 'warning',
|
||||
'message' => 'AI服务未配置,跳过连接检查',
|
||||
'response_time_ms' => round((microtime(true) - $start_time) * 1000, 2)
|
||||
];
|
||||
}
|
||||
|
||||
$config = $config_info['data']['value'];
|
||||
|
||||
if ($config['status'] != 1 || empty($config['api_key']) || empty($config['base_url'])) {
|
||||
return [
|
||||
'component' => 'ai_service_connection',
|
||||
'status' => 'warning',
|
||||
'message' => 'AI服务未启用或配置不完整,跳过连接检查',
|
||||
'response_time_ms' => round((microtime(true) - $start_time) * 1000, 2)
|
||||
];
|
||||
}
|
||||
|
||||
// 测试连接(发送一个简单的健康检查请求)
|
||||
$url = $config['base_url'];
|
||||
$headers = [
|
||||
'Authorization: Bearer ' . $config['api_key'],
|
||||
'Content-Type: application/json',
|
||||
];
|
||||
|
||||
$ch = curl_init();
|
||||
curl_setopt($ch, CURLOPT_URL, $url);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_HEADER, true);
|
||||
curl_setopt($ch, CURLOPT_NOBODY, true);
|
||||
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
|
||||
|
||||
$response = curl_exec($ch);
|
||||
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
|
||||
if ($response === false) {
|
||||
return [
|
||||
'component' => 'ai_service_connection',
|
||||
'status' => 'error',
|
||||
'message' => '无法连接到AI服务',
|
||||
'response_time_ms' => round((microtime(true) - $start_time) * 1000, 2)
|
||||
];
|
||||
}
|
||||
|
||||
if ($http_code >= 200 && $http_code < 300) {
|
||||
return [
|
||||
'component' => 'ai_service_connection',
|
||||
'status' => 'healthy',
|
||||
'message' => 'AI服务连接正常',
|
||||
'response_time_ms' => round((microtime(true) - $start_time) * 1000, 2),
|
||||
'details' => [
|
||||
'http_status' => $http_code,
|
||||
'url' => $url
|
||||
]
|
||||
];
|
||||
} else {
|
||||
return [
|
||||
'component' => 'ai_service_connection',
|
||||
'status' => 'warning',
|
||||
'message' => 'AI服务响应异常,HTTP状态码:' . $http_code,
|
||||
'response_time_ms' => round((microtime(true) - $start_time) * 1000, 2),
|
||||
'details' => [
|
||||
'http_status' => $http_code,
|
||||
'url' => $url
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
} catch (\Exception $e) {
|
||||
return [
|
||||
'component' => 'ai_service_connection',
|
||||
'status' => 'error',
|
||||
'message' => 'AI服务连接检查失败:' . $e->getMessage(),
|
||||
'response_time_ms' => round((microtime(true) - $start_time) * 1000, 2)
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查系统资源
|
||||
*/
|
||||
private function checkSystemResources()
|
||||
{
|
||||
$start_time = microtime(true);
|
||||
|
||||
try {
|
||||
$memory_usage = memory_get_usage(true);
|
||||
$memory_limit = $this->parseMemoryLimit(ini_get('memory_limit'));
|
||||
$memory_usage_percent = ($memory_usage / $memory_limit) * 100;
|
||||
|
||||
$details = [
|
||||
'php_version' => PHP_VERSION,
|
||||
'memory_usage' => round($memory_usage / 1024 / 1024, 2) . ' MB',
|
||||
'memory_limit' => round($memory_limit / 1024 / 1024, 2) . ' MB',
|
||||
'memory_usage_percent' => round($memory_usage_percent, 2) . '%',
|
||||
'max_execution_time' => ini_get('max_execution_time') . 's',
|
||||
'upload_max_filesize' => ini_get('upload_max_filesize'),
|
||||
'post_max_size' => ini_get('post_max_size')
|
||||
];
|
||||
|
||||
$status = 'healthy';
|
||||
$message = '系统资源正常';
|
||||
|
||||
// 检查内存使用率
|
||||
if ($memory_usage_percent > 90) {
|
||||
$status = 'error';
|
||||
$message = '内存使用率过高';
|
||||
} elseif ($memory_usage_percent > 80) {
|
||||
$status = 'warning';
|
||||
$message = '内存使用率较高';
|
||||
}
|
||||
|
||||
return [
|
||||
'component' => 'system_resources',
|
||||
'status' => $status,
|
||||
'message' => $message,
|
||||
'response_time_ms' => round((microtime(true) - $start_time) * 1000, 2),
|
||||
'details' => $details
|
||||
];
|
||||
|
||||
} catch (\Exception $e) {
|
||||
return [
|
||||
'component' => 'system_resources',
|
||||
'status' => 'error',
|
||||
'message' => '系统资源检查失败:' . $e->getMessage(),
|
||||
'response_time_ms' => round((microtime(true) - $start_time) * 1000, 2)
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析内存限制值
|
||||
*/
|
||||
private function parseMemoryLimit($val)
|
||||
{
|
||||
$val = trim($val);
|
||||
$last = strtolower($val[strlen($val)-1]);
|
||||
$val = (int)$val;
|
||||
|
||||
switch($last) {
|
||||
case 'g':
|
||||
$val *= 1024;
|
||||
case 'm':
|
||||
$val *= 1024;
|
||||
case 'k':
|
||||
$val *= 1024;
|
||||
}
|
||||
|
||||
return $val;
|
||||
}
|
||||
}
|
||||
20
src/addon/aikefu/event/UnInstall.php
Normal file
20
src/addon/aikefu/event/UnInstall.php
Normal file
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace addon\aikefu\event;
|
||||
|
||||
use app\model\system\Addon as AddonModel;
|
||||
|
||||
/**
|
||||
* 智能客服插件卸载
|
||||
*/
|
||||
class UnInstall
|
||||
{
|
||||
public function handle()
|
||||
{
|
||||
$addon_model = new AddonModel();
|
||||
// 删除插件信息
|
||||
$addon_model->deleteAddon(['name' => 'aikefu']);
|
||||
|
||||
return success(1);
|
||||
}
|
||||
}
|
||||
1
src/addon/aikefu/icon.png
Normal file
1
src/addon/aikefu/icon.png
Normal file
File diff suppressed because one or more lines are too long
67
src/addon/aikefu/model/Config.php
Normal file
67
src/addon/aikefu/model/Config.php
Normal file
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
/**
|
||||
* 智能客服配置模型
|
||||
* 用于存储和管理智能客服的配置信息
|
||||
* 版本:1.0.0
|
||||
*/
|
||||
|
||||
namespace addon\aikefu\model;
|
||||
|
||||
use app\model\system\Config as ConfigModel;
|
||||
use app\model\BaseModel;
|
||||
|
||||
/**
|
||||
* 智能客服配置
|
||||
*/
|
||||
class Config extends BaseModel
|
||||
{
|
||||
/**
|
||||
* 设置智能客服配置
|
||||
* @param array $data
|
||||
* @param int $site_id
|
||||
* @param string $app_module
|
||||
* @return array
|
||||
*/
|
||||
public function setConfig($data, $site_id = 0, $app_module = 'shop')
|
||||
{
|
||||
$config = new ConfigModel();
|
||||
|
||||
// 获取原始配置
|
||||
$original_config = $this->getConfig($site_id, $app_module)['data']['value'] ?? [];
|
||||
|
||||
// 如果 API Key 为空或保持不变,则使用原始值
|
||||
if (isset($data['api_key']) && empty($data['api_key'])) {
|
||||
$data['api_key'] = $original_config['api_key'] ?? '';
|
||||
}
|
||||
|
||||
$res = $config->setConfig($data, '智能客服配置', 1, [['site_id', '=', $site_id], ['app_module', '=', $app_module], ['config_key', '=', 'AIKEFU_CONFIG']]);
|
||||
return $res;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取智能客服配置
|
||||
* @param int $site_id
|
||||
* @param string $app_module
|
||||
* @return array
|
||||
*/
|
||||
public function getConfig($site_id = 0, $app_module = 'shop')
|
||||
{
|
||||
$config = new ConfigModel();
|
||||
$res = $config->getConfig([['site_id', '=', $site_id], ['app_module', '=', $app_module], ['config_key', '=', 'AIKEFU_CONFIG']]);
|
||||
return $res;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取智能客服配置信息
|
||||
* @param array $condition
|
||||
* @param string $field
|
||||
* @return array
|
||||
*/
|
||||
public function getConfigInfo($condition = [], $field = '*')
|
||||
{
|
||||
// 兼容旧的调用方式
|
||||
$site_id = $condition[0][1] ?? 0;
|
||||
$res = $this->getConfig($site_id);
|
||||
return $res;
|
||||
}
|
||||
}
|
||||
192
src/addon/aikefu/model/Conversation.php
Normal file
192
src/addon/aikefu/model/Conversation.php
Normal file
@@ -0,0 +1,192 @@
|
||||
<?php
|
||||
|
||||
namespace addon\aikefu\model;
|
||||
|
||||
use think\Model;
|
||||
|
||||
class Conversation extends Model
|
||||
{
|
||||
/**
|
||||
* 操作成功返回值函数
|
||||
* @param string $data
|
||||
* @param string $code_var
|
||||
* @return array
|
||||
*/
|
||||
public function success($data = '', $code_var = 'SUCCESS')
|
||||
{
|
||||
$lang_array = $this->getLang();
|
||||
$lang_var = $lang_array[$code_var] ?? $code_var;
|
||||
|
||||
if ($code_var == 'SUCCESS') {
|
||||
$code_var = 0;
|
||||
} else {
|
||||
$code_array = array_keys($lang_array);
|
||||
$code_index = array_search($code_var, $code_array);
|
||||
if ($code_index != false) {
|
||||
$code_var = 10000 + $code_index;
|
||||
}
|
||||
}
|
||||
|
||||
return success($code_var, $lang_var, $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 操作失败返回值函数
|
||||
* @param string $data
|
||||
* @param string $code_var
|
||||
* @return array
|
||||
*/
|
||||
public function error($data = '', $code_var = 'FAIL')
|
||||
{
|
||||
$lang_array = $this->getLang();
|
||||
|
||||
if (isset($lang_array[$code_var])) {
|
||||
$lang_var = $lang_array[$code_var];
|
||||
} else {
|
||||
$lang_var = $code_var;
|
||||
$code_var = 'FAIL';
|
||||
}
|
||||
$code_array = array_keys($lang_array);
|
||||
$code_index = array_search($code_var, $code_array);
|
||||
if ($code_index != false) {
|
||||
$code_var = -10000 - $code_index;
|
||||
}
|
||||
return error($code_var, $lang_var, $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取语言包数组
|
||||
* @return Ambigous <multitype:, unknown>
|
||||
*/
|
||||
public function getLang()
|
||||
{
|
||||
$default_lang = config("lang.default_lang");
|
||||
$cache_common = \think\facade\Cache::get("lang_app/lang/" . $default_lang . '/model.php');
|
||||
if (empty($cache_common)) {
|
||||
$cache_common = include 'app/lang/' . $default_lang . '/model.php';
|
||||
\think\facade\Cache::tag("lang")->set("lang_app/lang/" . $default_lang, $cache_common);
|
||||
}
|
||||
$lang_path = $this->lang ?? '';
|
||||
if (!empty($lang_path)) {
|
||||
$cache_path = \think\facade\Cache::get("lang_" . $lang_path . "/" . $default_lang . '/model.php');
|
||||
if (empty($cache_path)) {
|
||||
$cache_path = include $lang_path . "/" . $default_lang . '/model.php';
|
||||
\think\facade\Cache::tag("lang")->set("lang_" . $lang_path . "/" . $default_lang, $cache_path);
|
||||
}
|
||||
$lang = array_merge($cache_common, $cache_path);
|
||||
} else {
|
||||
$lang = $cache_common;
|
||||
}
|
||||
return $lang;
|
||||
}
|
||||
|
||||
/**
|
||||
* 表名
|
||||
* @var string
|
||||
*/
|
||||
protected $name = 'aikefu_conversation';
|
||||
|
||||
/**
|
||||
* 主键
|
||||
* @var string
|
||||
*/
|
||||
protected $pk = 'id';
|
||||
|
||||
/**
|
||||
* 获取会话信息
|
||||
* @param array $condition
|
||||
* @param string $field
|
||||
* @return array
|
||||
*/
|
||||
public function getConversationInfo($condition = [], $field = '*')
|
||||
{
|
||||
$info = $this->where($condition)->field($field)->find();
|
||||
return empty($info) ? [] : $info->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取会话列表
|
||||
* @param array $condition
|
||||
* @param string $field
|
||||
* @param string $order
|
||||
* @param int $page
|
||||
* @param int $limit
|
||||
* @return array
|
||||
*/
|
||||
public function getConversationList($condition = [], $field = '*', $order = 'id desc', $page = 1, $limit = 10)
|
||||
{
|
||||
$list = $this->where($condition)->field($field)->order($order)->paginate([
|
||||
'page' => $page,
|
||||
'list_rows' => $limit
|
||||
]);
|
||||
return $this->pageFormat($list);
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页数据格式化
|
||||
* @param \think\Paginator $paginator
|
||||
* @return array
|
||||
*/
|
||||
protected function pageFormat($paginator)
|
||||
{
|
||||
return [
|
||||
'data' => $paginator->items(),
|
||||
'total' => $paginator->total(),
|
||||
'per_page' => $paginator->listRows(),
|
||||
'current_page' => $paginator->currentPage(),
|
||||
'last_page' => $paginator->lastPage()
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加会话
|
||||
* @param array $data
|
||||
* @return array
|
||||
*/
|
||||
public function addConversation($data)
|
||||
{
|
||||
$result = $this->insert($data);
|
||||
return $this->success(['result' => $result]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新会话
|
||||
* @param array $data
|
||||
* @param array $condition
|
||||
* @return array
|
||||
*/
|
||||
public function updateConversation($data, $condition)
|
||||
{
|
||||
$result = $this->where($condition)->update($data);
|
||||
return $this->success(['result' => $result]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除会话
|
||||
* @param array $condition
|
||||
* @return array
|
||||
*/
|
||||
public function deleteConversation($condition)
|
||||
{
|
||||
$result = $this->where($condition)->delete();
|
||||
return $this->success(['result' => $result]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户会话列表
|
||||
* @param int $site_id
|
||||
* @param string $user_id
|
||||
* @param int $page
|
||||
* @param int $limit
|
||||
* @return array
|
||||
*/
|
||||
public function getUserConversationList($site_id, $user_id, $page = 1, $limit = 10)
|
||||
{
|
||||
$condition = [
|
||||
['site_id', '=', $site_id],
|
||||
['user_id', '=', $user_id]
|
||||
];
|
||||
|
||||
return $this->getConversationList($condition, '*', 'update_time desc', $page, $limit);
|
||||
}
|
||||
}
|
||||
214
src/addon/aikefu/model/Message.php
Normal file
214
src/addon/aikefu/model/Message.php
Normal file
@@ -0,0 +1,214 @@
|
||||
<?php
|
||||
|
||||
namespace addon\aikefu\model;
|
||||
|
||||
use think\Model;
|
||||
|
||||
class Message extends Model
|
||||
{
|
||||
/**
|
||||
* 操作成功返回值函数
|
||||
* @param string $data
|
||||
* @param string $code_var
|
||||
* @return array
|
||||
*/
|
||||
public function success($data = '', $code_var = 'SUCCESS')
|
||||
{
|
||||
$lang_array = $this->getLang();
|
||||
$lang_var = $lang_array[$code_var] ?? $code_var;
|
||||
|
||||
if ($code_var == 'SUCCESS') {
|
||||
$code_var = 0;
|
||||
} else {
|
||||
$code_array = array_keys($lang_array);
|
||||
$code_index = array_search($code_var, $code_array);
|
||||
if ($code_index != false) {
|
||||
$code_var = 10000 + $code_index;
|
||||
}
|
||||
}
|
||||
|
||||
return success($code_var, $lang_var, $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 操作失败返回值函数
|
||||
* @param string $data
|
||||
* @param string $code_var
|
||||
* @return array
|
||||
*/
|
||||
public function error($data = '', $code_var = 'FAIL')
|
||||
{
|
||||
$lang_array = $this->getLang();
|
||||
|
||||
if (isset($lang_array[$code_var])) {
|
||||
$lang_var = $lang_array[$code_var];
|
||||
} else {
|
||||
$lang_var = $code_var;
|
||||
$code_var = 'FAIL';
|
||||
}
|
||||
$code_array = array_keys($lang_array);
|
||||
$code_index = array_search($code_var, $code_array);
|
||||
if ($code_index != false) {
|
||||
$code_var = -10000 - $code_index;
|
||||
}
|
||||
return error($code_var, $lang_var, $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取语言包数组
|
||||
* @return Ambigous <multitype:, unknown>
|
||||
*/
|
||||
public function getLang()
|
||||
{
|
||||
$default_lang = config("lang.default_lang");
|
||||
$cache_common = \think\facade\Cache::get("lang_app/lang/" . $default_lang . '/model.php');
|
||||
if (empty($cache_common)) {
|
||||
$cache_common = include 'app/lang/' . $default_lang . '/model.php';
|
||||
\think\facade\Cache::tag("lang")->set("lang_app/lang/" . $default_lang, $cache_common);
|
||||
}
|
||||
$lang_path = $this->lang ?? '';
|
||||
if (!empty($lang_path)) {
|
||||
$cache_path = \think\facade\Cache::get("lang_" . $lang_path . "/" . $default_lang . '/model.php');
|
||||
if (empty($cache_path)) {
|
||||
$cache_path = include $lang_path . "/" . $default_lang . '/model.php';
|
||||
\think\facade\Cache::tag("lang")->set("lang_" . $lang_path . "/" . $default_lang, $cache_path);
|
||||
}
|
||||
$lang = array_merge($cache_common, $cache_path);
|
||||
} else {
|
||||
$lang = $cache_common;
|
||||
}
|
||||
return $lang;
|
||||
}
|
||||
|
||||
/**
|
||||
* 表名
|
||||
* @var string
|
||||
*/
|
||||
protected $name = 'aikefu_message';
|
||||
|
||||
/**
|
||||
* 主键
|
||||
* @var string
|
||||
*/
|
||||
protected $pk = 'id';
|
||||
|
||||
/**
|
||||
* 获取消息信息
|
||||
* @param array $condition
|
||||
* @param string $field
|
||||
* @return array
|
||||
*/
|
||||
public function getMessageInfo($condition = [], $field = '*')
|
||||
{
|
||||
$info = $this->where($condition)->field($field)->find();
|
||||
return empty($info) ? [] : $info->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取消息列表
|
||||
* @param array $condition
|
||||
* @param string $field
|
||||
* @param string $order
|
||||
* @param int $page
|
||||
* @param int $limit
|
||||
* @return array
|
||||
*/
|
||||
public function getMessageList($condition = [], $field = '*', $order = 'id asc', $page = 1, $limit = 20)
|
||||
{
|
||||
$list = $this->where($condition)->field($field)->order($order)->paginate([
|
||||
'page' => $page,
|
||||
'list_rows' => $limit
|
||||
]);
|
||||
return $this->pageFormat($list);
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页数据格式化
|
||||
* @param \think\Paginator $paginator
|
||||
* @return array
|
||||
*/
|
||||
protected function pageFormat($paginator)
|
||||
{
|
||||
return [
|
||||
'data' => $paginator->items(),
|
||||
'total' => $paginator->total(),
|
||||
'per_page' => $paginator->listRows(),
|
||||
'current_page' => $paginator->currentPage(),
|
||||
'last_page' => $paginator->lastPage()
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加消息
|
||||
* @param array $data
|
||||
* @return array
|
||||
*/
|
||||
public function addMessage($data)
|
||||
{
|
||||
$result = $this->insert($data);
|
||||
return $this->success(['result' => $result]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新消息
|
||||
* @param array $data
|
||||
* @param array $condition
|
||||
* @return array
|
||||
*/
|
||||
public function updateMessage($data, $condition)
|
||||
{
|
||||
$result = $this->where($condition)->update($data);
|
||||
return $this->success(['result' => $result]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除消息
|
||||
* @param array $condition
|
||||
* @return array
|
||||
*/
|
||||
public function deleteMessage($condition)
|
||||
{
|
||||
$result = $this->where($condition)->delete();
|
||||
return $this->success(['result' => $result]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取会话消息记录
|
||||
* @param int $site_id
|
||||
* @param string $conversation_id
|
||||
* @param int $limit
|
||||
* @param int $offset
|
||||
* @param string $status 状态过滤
|
||||
* @return array
|
||||
*/
|
||||
public function getConversationMessages($site_id, $conversation_id, $limit = 20, $offset = 0, $status = '')
|
||||
{
|
||||
$condition = [
|
||||
['site_id', '=', $site_id],
|
||||
['conversation_id', '=', $conversation_id]
|
||||
];
|
||||
|
||||
// 添加状态过滤
|
||||
if (!empty($status)) {
|
||||
$condition[] = ['status', '=', $status];
|
||||
}
|
||||
|
||||
return $this->getMessageList($condition, '*', 'create_time asc', ($offset / $limit) + 1, $limit);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户消息总数
|
||||
* @param int $site_id
|
||||
* @param string $user_id
|
||||
* @return array
|
||||
*/
|
||||
public function getUserMessageCount($site_id, $user_id)
|
||||
{
|
||||
$count = $this->where([
|
||||
['site_id', '=', $site_id],
|
||||
['user_id', '=', $user_id]
|
||||
])->count();
|
||||
|
||||
return $this->success(['count' => $count]);
|
||||
}
|
||||
}
|
||||
319
src/addon/aikefu/shop/controller/Kefu.php
Normal file
319
src/addon/aikefu/shop/controller/Kefu.php
Normal file
@@ -0,0 +1,319 @@
|
||||
<?php
|
||||
/**
|
||||
* 智能客服控制器
|
||||
*/
|
||||
|
||||
namespace addon\aikefu\shop\controller;
|
||||
|
||||
use addon\aikefu\model\Config as KefuConfigModel;
|
||||
use addon\aikefu\model\Conversation as KefuConversationModel;
|
||||
use addon\aikefu\model\Message as KefuMessageModel;
|
||||
use app\shop\controller\BaseShop;
|
||||
|
||||
use think\facade\Db as Db;
|
||||
|
||||
/**
|
||||
* 智能客服 控制器
|
||||
*/
|
||||
class Kefu extends BaseShop
|
||||
{
|
||||
/**
|
||||
* 智能客服默认页面
|
||||
* @return \think\response\View
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
$kefu_config_model = new KefuConfigModel();
|
||||
$config_info = $kefu_config_model->getConfig($this->site_id, $this->app_module)["data"]["value"] ?? [];
|
||||
$this->assign("config_info", $config_info);
|
||||
return $this->fetch("kefu/index");
|
||||
}
|
||||
|
||||
/**
|
||||
* 智能客服配置页
|
||||
* @return \think\response\View|\think\response\Json
|
||||
*/
|
||||
public function config()
|
||||
{
|
||||
$kefu_config_model = new KefuConfigModel();
|
||||
|
||||
if (request()->isJson()) {
|
||||
$api_key = input("api_key/s", "");//Dify API密钥
|
||||
$base_url = input("base_url/s", "https://api.dify.ai/v1");//API基础地址
|
||||
$chat_endpoint = input("chat_endpoint/s", "/chat-messages");//聊天接口端点
|
||||
$status = input("status/d", 0);//状态
|
||||
|
||||
$data = array(
|
||||
"api_key" => $api_key,
|
||||
"base_url" => $base_url,
|
||||
"chat_endpoint" => $chat_endpoint,
|
||||
"status" => $status
|
||||
);
|
||||
$result = $kefu_config_model->setConfig($data, $this->site_id, $this->app_module);
|
||||
return $result;
|
||||
} else {
|
||||
$config_info = $kefu_config_model->getConfig($this->site_id, $this->app_module)['data']['value'] ?? [];
|
||||
$this->assign("config_info", $config_info);
|
||||
return $this->fetch("kefu/config");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 会话管理列表
|
||||
* @return \think\response\View
|
||||
*/
|
||||
public function conversation()
|
||||
{
|
||||
return $this->fetch("kefu/conversation");
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取会话列表
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function getConversationList()
|
||||
{
|
||||
$page = input("page/d", 1);
|
||||
$limit = input("limit/d", 10);
|
||||
$user_id = input("user_id/s", "");
|
||||
$status = input("status/s", "");
|
||||
|
||||
$kefu_conversation_model = new KefuConversationModel();
|
||||
$condition = [[ 'site_id', '=', $this->site_id ]];
|
||||
|
||||
if (!empty($user_id)) {
|
||||
$condition[] = ['user_id', '=', $user_id];
|
||||
}
|
||||
|
||||
if ($status !== '') {
|
||||
$condition[] = ['status', '=', $status];
|
||||
}
|
||||
|
||||
$conversation_list = $kefu_conversation_model->getConversationList($condition, '*', 'update_time desc', $page, $limit);
|
||||
|
||||
// 适配layui table的返回格式,同时保持与Dify API风格一致
|
||||
$result = [
|
||||
'code' => 0, // layui table要求成功状态码为0
|
||||
'msg' => '获取会话列表成功',
|
||||
'count' => $conversation_list['total'], // 总记录数
|
||||
'data' => [
|
||||
'conversations' => $conversation_list['data'], // 会话列表
|
||||
'page_info' => [
|
||||
'limit' => $limit,
|
||||
'offset' => ($page - 1) * $limit
|
||||
]
|
||||
] // 数据列表
|
||||
];
|
||||
|
||||
return json($result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取会话信息
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function getConversationInfo()
|
||||
{
|
||||
$conversation_id = input("conversation_id/s", "");
|
||||
|
||||
if (empty($conversation_id)) {
|
||||
return json([
|
||||
'code' => -1,
|
||||
'msg' => '会话ID不能为空',
|
||||
]);
|
||||
}
|
||||
|
||||
$kefu_conversation_model = new KefuConversationModel();
|
||||
$conversation_info = $kefu_conversation_model->getConversationInfo([
|
||||
['site_id', '=', $this->site_id],
|
||||
['conversation_id', '=', $conversation_id]
|
||||
]);
|
||||
|
||||
if (empty($conversation_info)) {
|
||||
return json([
|
||||
'code' => -1,
|
||||
'msg' => '会话不存在',
|
||||
]);
|
||||
}
|
||||
|
||||
return json([
|
||||
'code' => 0,
|
||||
'msg' => '获取会话信息成功',
|
||||
'data' => $conversation_info,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 结束会话
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function endConversation()
|
||||
{
|
||||
$id = input("id/d", "");
|
||||
|
||||
if (empty($id)) {
|
||||
return json([
|
||||
'code' => -1,
|
||||
'msg' => '会话ID不能为空',
|
||||
]);
|
||||
}
|
||||
|
||||
$kefu_conversation_model = new KefuConversationModel();
|
||||
$result = $kefu_conversation_model->updateConversation(
|
||||
['status' => 0],
|
||||
[
|
||||
['id', '=', $id],
|
||||
['site_id', '=', $this->site_id]
|
||||
]
|
||||
);
|
||||
|
||||
if ($result === false) {
|
||||
return json([
|
||||
'code' => -1,
|
||||
'msg' => '会话结束失败',
|
||||
]);
|
||||
}
|
||||
|
||||
return json([
|
||||
'code' => 0,
|
||||
'msg' => '会话已结束',
|
||||
'data' => $result,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除会话
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function deleteConversation()
|
||||
{
|
||||
$id = input("id/d", "");
|
||||
$ids = input("ids/a", []);
|
||||
|
||||
// 验证参数
|
||||
if (empty($id) && empty($ids)) {
|
||||
return json([
|
||||
'code' => -1,
|
||||
'msg' => '会话ID不能为空',
|
||||
]);
|
||||
}
|
||||
|
||||
// 处理单个ID或ID数组
|
||||
if (!empty($id)) {
|
||||
$ids = [$id];
|
||||
}
|
||||
|
||||
$kefu_conversation_model = new KefuConversationModel();
|
||||
$kefu_message_model = new KefuMessageModel();
|
||||
|
||||
// 开启事务
|
||||
Db::startTrans();
|
||||
|
||||
try {
|
||||
// 获取所有要删除的会话信息
|
||||
$conversations = $kefu_conversation_model->getConversationList([
|
||||
['id', 'in', $ids],
|
||||
['site_id', '=', $this->site_id]
|
||||
]);
|
||||
|
||||
// 删除所有会话关联的消息
|
||||
if (!empty($conversations['data'])) {
|
||||
foreach ($conversations['data'] as $conversation) {
|
||||
$kefu_message_model->deleteMessage([
|
||||
['site_id', '=', $this->site_id],
|
||||
['conversation_id', '=', $conversation['conversation_id']]
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
// 删除会话
|
||||
$result = $kefu_conversation_model->deleteConversation([
|
||||
['id', 'in', $ids],
|
||||
['site_id', '=', $this->site_id]
|
||||
]);
|
||||
|
||||
// 提交事务
|
||||
Db::commit();
|
||||
|
||||
return json([
|
||||
'code' => 0,
|
||||
'msg' => '会话已删除',
|
||||
'data' => $result,
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
// 回滚事务
|
||||
Db::rollback();
|
||||
return json([
|
||||
'code' => -1,
|
||||
'msg' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 消息管理列表
|
||||
* @return \think\response\View
|
||||
*/
|
||||
public function message()
|
||||
{
|
||||
$conversation_id = input("conversation_id/s", "");
|
||||
$this->assign("conversation_id", $conversation_id);
|
||||
return $this->fetch("kefu/message");
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取消息列表
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function getMessageList()
|
||||
{
|
||||
$page = input("page/d", 1);
|
||||
$limit = input("limit/d", 50);
|
||||
$conversation_id = input("conversation_id/s", "");
|
||||
$user_id = input("user_id/s", "");
|
||||
$sort_field = input("sort_field/s", "create_time"); // 排序字段
|
||||
$sort_order = input("sort_order/s", "desc"); // 排序方式:asc或desc
|
||||
$status = input("status/s", "completed"); // 默认只显示已完成的消息
|
||||
|
||||
$kefu_message_model = new KefuMessageModel();
|
||||
$condition = [
|
||||
['site_id', '=', $this->site_id]
|
||||
];
|
||||
|
||||
// 只有当会话ID不为空时才添加会话ID条件
|
||||
if (!empty($conversation_id)) {
|
||||
$condition[] = ['conversation_id', '=', $conversation_id];
|
||||
}
|
||||
|
||||
// 只有当用户ID不为空时才添加用户ID条件
|
||||
if (!empty($user_id)) {
|
||||
$condition[] = ['user_id', '=', $user_id];
|
||||
}
|
||||
|
||||
// 添加状态过滤(默认只显示已完成的消息,避免显示临时数据)
|
||||
if (!empty($status)) {
|
||||
$condition[] = ['status', '=', $status];
|
||||
}
|
||||
|
||||
// 构建排序字符串
|
||||
$order = $sort_field . ' ' . $sort_order;
|
||||
$message_list = $kefu_message_model->getMessageList($condition, '*', $order, $page, $limit);
|
||||
|
||||
// 适配layui table的返回格式,同时保持与Dify API风格一致
|
||||
$result = [
|
||||
'code' => 0, // layui table要求成功状态码为0
|
||||
'msg' => '获取消息列表成功',
|
||||
'count' => $message_list['total'], // 总记录数
|
||||
'data' => [
|
||||
'messages' => $message_list['data'], // 消息列表
|
||||
'page_info' => [
|
||||
'limit' => $limit,
|
||||
'offset' => ($page - 1) * $limit,
|
||||
'total' => $message_list['total'] // 添加总记录数到page_info
|
||||
]
|
||||
] // 数据列表
|
||||
];
|
||||
|
||||
return json($result);
|
||||
}
|
||||
}
|
||||
104
src/addon/aikefu/shop/view/kefu/config.html
Normal file
104
src/addon/aikefu/shop/view/kefu/config.html
Normal file
@@ -0,0 +1,104 @@
|
||||
<style>
|
||||
.word-aux {
|
||||
margin-left: 110px;
|
||||
color: #999;
|
||||
font-size: 12px;
|
||||
margin-top: 5px;
|
||||
}
|
||||
.required {
|
||||
color: red;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="layui-form form-wrap">
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label"><span class="required">*</span>Dify API密钥:</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="text" name="api_key" placeholder="请输入Dify API密钥" value="{$config_info.api_key ?? ''}" class="layui-input">
|
||||
</div>
|
||||
<div class="word-aux">
|
||||
从Dify平台获取的API密钥,用于调用Dify聊天机器人API。
|
||||
<a href="https://dify.ai/" target="_blank">前往Dify平台</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label"><span class="required">*</span>API基础地址:</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="text" name="base_url" placeholder="请输入Dify API基础地址" value="{$config_info.base_url ?? 'https://api.dify.ai/v1'}" class="layui-input">
|
||||
</div>
|
||||
<div class="word-aux">Dify API的基础地址,默认为https://api.dify.ai/v1</div>
|
||||
</div>
|
||||
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">聊天接口端点:</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="text" name="chat_endpoint" placeholder="请输入聊天接口端点" value="{$config_info.chat_endpoint ?? '/chat-messages'}" class="layui-input">
|
||||
</div>
|
||||
<div class="word-aux">聊天接口的端点,默认为/chat-messages</div>
|
||||
</div>
|
||||
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label"><span class="required">*</span>状态:</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="checkbox" name="status" value="1" lay-skin="switch" {if condition="isset($config_info.status) && $config_info.status == 1"} checked {/if}>
|
||||
</div>
|
||||
<div class="word-aux">启用或禁用智能客服功能</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<button class="layui-btn" lay-submit lay-filter="save">保存</button>
|
||||
<button class="layui-btn layui-btn-primary" onclick="back()">返回</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
layui.use('form', function() {
|
||||
var form = layui.form;
|
||||
var repeat_flag = false; //防重复标识
|
||||
form.render();
|
||||
|
||||
/**
|
||||
* 监听提交
|
||||
*/
|
||||
form.on('submit(save)', function(data) {
|
||||
if (repeat_flag) return false;
|
||||
repeat_flag = true;
|
||||
|
||||
$.ajax({
|
||||
url: ns.url("aikefu://shop/kefu/config"),
|
||||
type: 'POST',
|
||||
data: data.field,
|
||||
dataType: 'json',
|
||||
success: function(res) {
|
||||
repeat_flag = false;
|
||||
if (res.code === 0) {
|
||||
layer.confirm('保存成功', {
|
||||
title: '操作提示',
|
||||
btn: ['返回列表', '继续编辑'],
|
||||
yes: function(index, layero) {
|
||||
location.reload();
|
||||
layer.close(index);
|
||||
},
|
||||
btn2: function(index, layero) {
|
||||
layer.close(index);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
layer.msg(res.message, {icon: 2});
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
repeat_flag = false;
|
||||
layer.msg('请求失败,请稍后重试', {icon: 2});
|
||||
}
|
||||
});
|
||||
|
||||
return false;
|
||||
});
|
||||
});
|
||||
|
||||
function back() {
|
||||
window.history.back();
|
||||
}
|
||||
</script>
|
||||
492
src/addon/aikefu/shop/view/kefu/conversation.html
Normal file
492
src/addon/aikefu/shop/view/kefu/conversation.html
Normal file
@@ -0,0 +1,492 @@
|
||||
<style>
|
||||
.search-box {
|
||||
padding: 15px;
|
||||
border-bottom: 1px solid #eee;
|
||||
margin-bottom: 15px;
|
||||
background-color: #f9f9f9;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.layui-btn-container {
|
||||
margin-bottom: 15px;
|
||||
text-align: right;
|
||||
}
|
||||
.layui-btn-sm {
|
||||
margin-left: 5px;
|
||||
}
|
||||
.status-active {
|
||||
color: #52c41a;
|
||||
}
|
||||
.status-inactive {
|
||||
color: #faad14;
|
||||
}
|
||||
|
||||
/* 消息列表样式优化 - 从message.html复制 */
|
||||
.message-list {
|
||||
max-height: 650px;
|
||||
overflow-y: auto;
|
||||
padding: 25px;
|
||||
background-color: #fafbfc;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 20px;
|
||||
border: 1px solid #e8e8e8;
|
||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
/* 消息项样式 */
|
||||
.message-item {
|
||||
margin-bottom: 25px;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.message-item.user {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.message-item.assistant {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
/* 头像样式 */
|
||||
.message-avatar {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 50%;
|
||||
margin: 0 12px;
|
||||
border: 2px solid #fff;
|
||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
/* 消息内容样式 */
|
||||
.message-content {
|
||||
max-width: 70%;
|
||||
padding: 16px 20px;
|
||||
border-radius: 18px;
|
||||
word-wrap: break-word;
|
||||
line-height: 1.6;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.message-item.user .message-content {
|
||||
background-color: #1E9FFF;
|
||||
color: white;
|
||||
border-bottom-right-radius: 4px;
|
||||
box-shadow: 0 2px 8px rgba(30, 159, 255, 0.3);
|
||||
}
|
||||
|
||||
.message-item.assistant .message-content {
|
||||
background-color: white;
|
||||
color: #333;
|
||||
border-bottom-left-radius: 4px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
/* 消息时间样式 */
|
||||
.message-time {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
margin-top: 8px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* 消息角色样式 */
|
||||
.message-role {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.message-item.user .message-role {
|
||||
color: rgba(106, 38, 38, 0.9);
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.message-item.assistant .message-role {
|
||||
color: #666;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
/* 空状态样式 */
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 60px 20px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.empty-state i {
|
||||
font-size: 48px;
|
||||
margin-bottom: 15px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.message-content {
|
||||
max-width: 85%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="layui-card-body">
|
||||
<!-- 搜索区域 -->
|
||||
<div class="search-box layui-form">
|
||||
<div class="layui-form-item" style="margin-bottom: 0;">
|
||||
<div class="layui-inline">
|
||||
<label class="layui-form-label">用户ID</label>
|
||||
<div class="layui-input-inline" style="width: 150px;">
|
||||
<input type="text" name="user_id" id="user_id" placeholder="请输入用户ID" class="layui-input">
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-inline">
|
||||
<label class="layui-form-label">状态</label>
|
||||
<div class="layui-input-inline" style="width: 120px;">
|
||||
<select name="status" id="status" class="layui-select">
|
||||
<option value="">全部</option>
|
||||
<option value="1">活跃</option>
|
||||
<option value="0">已结束</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-inline">
|
||||
<button type="button" class="layui-btn layui-btn-primary" id="searchBtn">搜索</button>
|
||||
<button type="button" class="layui-btn layui-btn-primary" id="resetBtn">重置</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 表格区域 -->
|
||||
<table class="layui-table" id="conversationTable" lay-filter="conversationTable"></table>
|
||||
</div>
|
||||
|
||||
<script type="text/html" id="toolbarDemo">
|
||||
<div class="layui-btn-container">
|
||||
<button type="button" class="layui-btn layui-btn-danger" id="batchDeleteBtn">批量删除</button>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type="text/html" id="barDemo">
|
||||
<div style="display: flex; gap: 5px;">
|
||||
<a class="layui-btn layui-btn-xs" lay-event="view">查看消息</a>
|
||||
<a class="layui-btn layui-btn-xs layui-btn-warning" lay-event="end">结束会话</a>
|
||||
<a class="layui-btn layui-btn-xs layui-btn-danger" lay-event="delete">删除</a>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type="text/html" id="statusTpl">
|
||||
{{# if(d.status == 1) {
|
||||
return '<span class="status-active">活跃</span>';
|
||||
} else {
|
||||
return '<span class="status-inactive">已结束</span>';
|
||||
}}}
|
||||
</script>
|
||||
|
||||
<!-- 消息列表模板 - 与message.html一致 -->
|
||||
<script type="text/html" id="messageListTpl">
|
||||
<div class="message-list">
|
||||
{{# if(d.length > 0) {
|
||||
d.forEach(function(item) {
|
||||
var role = item.role === 'user' ? (item.user_id ? '用户-' + item.user_id : '用户') : '机器人';
|
||||
var roleClass = item.role === 'user' ? 'user' : 'assistant';
|
||||
var avatar = item.role === 'user' ? '/addon/aikefu/static/images/user.svg' : '/addon/aikefu/static/images/robot.svg';
|
||||
<div class="message-item {{= roleClass }}">
|
||||
{{# if(item.role === 'assistant') { }}}
|
||||
<img src="{{= avatar }}" class="message-avatar">
|
||||
{{# } }}
|
||||
<div>
|
||||
<div class="message-role">{{= role }}</div>
|
||||
<div class="message-content">{{= item.content }}</div>
|
||||
<div class="message-time">{{= item.create_time }}</div>
|
||||
</div>
|
||||
{{# if(item.role === 'user') { }}}
|
||||
<img src="{{= avatar }}" class="message-avatar">
|
||||
{{# } }}
|
||||
</div>
|
||||
});
|
||||
} else {
|
||||
<div class="empty-state">
|
||||
<i class="layui-icon layui-icon-message"></i>
|
||||
<p>暂无消息记录</p>
|
||||
</div>
|
||||
} }}
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script>
|
||||
layui.use(['table', 'form', 'layer', 'laypage'], function() {
|
||||
var table = layui.table;
|
||||
var form = layui.form;
|
||||
var layer = layui.layer;
|
||||
var laypage = layui.laypage;
|
||||
|
||||
// 渲染表格
|
||||
var tableIns = table.render({
|
||||
elem: '#conversationTable',
|
||||
url: ns.url("aikefu://shop/kefu/getConversationList"),
|
||||
method: 'POST',
|
||||
toolbar: '#toolbarDemo',
|
||||
defaultToolbar: ['filter', 'exports', 'print'],
|
||||
title: '会话管理',
|
||||
width: '100%',
|
||||
cols: [[
|
||||
{type: 'checkbox', fixed: 'left'},
|
||||
{field: 'conversation_id', title: '会话ID', width: 300, align: 'center'},
|
||||
{field: 'user_id', title: '用户ID', width: 120, align: 'center'},
|
||||
{field: 'name', title: '会话名称', width: 180, align: 'center'},
|
||||
{field: 'status', title: '状态', width: 100, align: 'center', templet: '#statusTpl' },
|
||||
{field: 'create_time', title: '创建时间', width: 180, align: 'center'},
|
||||
{field: 'update_time', title: '更新时间', width: 180, align: 'center'},
|
||||
{fixed: 'right', title: '操作', width: 200, align: 'center', toolbar: '#barDemo'}
|
||||
]],
|
||||
page: true,
|
||||
limit: 10,
|
||||
limits: [10, 20, 30, 50, 100],
|
||||
height: 'full-200',
|
||||
text: {
|
||||
none: '暂无会话数据',
|
||||
error: '加载数据失败,请检查网络或稍后重试'
|
||||
},
|
||||
parseData: function(res) {
|
||||
// 调试:打印原始数据
|
||||
console.log('API返回原始数据:', res);
|
||||
|
||||
// 确保res存在
|
||||
if (!res) {
|
||||
return {
|
||||
"code": -1,
|
||||
"msg": "服务器返回数据为空",
|
||||
"count": 0,
|
||||
"data": []
|
||||
};
|
||||
}
|
||||
|
||||
// 适配后端返回的格式:
|
||||
// 后端返回的data字段包含conversations数组和page_info对象
|
||||
// 根级的count字段是总记录数
|
||||
return {
|
||||
"code": res.code || 0,
|
||||
"msg": res.msg || "获取数据成功",
|
||||
"count": res.count || 0, // 使用根级的count字段
|
||||
"data": Array.isArray(res.data?.conversations) ? res.data.conversations : [] // 使用data.conversations作为表格数据
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// 搜索按钮点击事件
|
||||
$('#searchBtn').click(function() {
|
||||
// 执行搜索
|
||||
tableIns.reload({
|
||||
page: {
|
||||
curr: 1
|
||||
},
|
||||
where: {
|
||||
user_id: $('#user_id').val(),
|
||||
status: $('#status').val()
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 重置按钮点击事件
|
||||
$('#resetBtn').click(function() {
|
||||
$('#user_id').val('');
|
||||
$('#status').val('');
|
||||
form.render('select');
|
||||
|
||||
// 执行重置后的搜索
|
||||
tableIns.reload({
|
||||
page: {
|
||||
curr: 1
|
||||
},
|
||||
where: {
|
||||
user_id: '',
|
||||
status: ''
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 监听行工具事件
|
||||
table.on('tool(conversationTable)', function(obj) {
|
||||
var data = obj.data;
|
||||
var layEvent = obj.event;
|
||||
|
||||
if (layEvent === 'view') {
|
||||
// 查看消息
|
||||
layer.open({
|
||||
type: 1,
|
||||
title: '消息记录',
|
||||
content: '<div style="padding: 25px; max-height: 700px;"><div class="message-list" style="max-height: 580px; overflow-y: auto; margin-bottom: 20px;">加载中...</div><div id="messagePage" style="text-align: center;"></div></div>',
|
||||
area: ['80%', '70%'],
|
||||
success: function(layero, index) {
|
||||
var currentPage = 1;
|
||||
var pageSize = 15; // 每页显示15条消息
|
||||
var total = 0;
|
||||
|
||||
// 加载消息列表函数
|
||||
function loadMessages(page) {
|
||||
$.ajax({
|
||||
url: ns.url("aikefu://shop/kefu/getMessageList"),
|
||||
type: 'POST',
|
||||
data: {
|
||||
conversation_id: data.conversation_id,
|
||||
page: page,
|
||||
limit: pageSize,
|
||||
sort_field: 'create_time',
|
||||
sort_order: 'desc' // 默认最新消息在前面
|
||||
},
|
||||
dataType: 'json',
|
||||
success: function(res) {
|
||||
var messageList = res.data?.messages || [];
|
||||
total = res.data?.total || 0;
|
||||
var html = '';
|
||||
|
||||
if (messageList.length > 0) {
|
||||
messageList.forEach(function(item) {
|
||||
var role = item.role === 'user' ? (item.user_id ? '用户-' + item.user_id : '用户') : '机器人';
|
||||
var roleClass = item.role === 'user' ? 'user' : 'assistant';
|
||||
var avatar = item.role === 'user' ? '/addon/aikefu/static/images/user.svg' : '/addon/aikefu/static/images/robot.svg';
|
||||
|
||||
html += '<div class="message-item ' + roleClass + '">';
|
||||
if (item.role === 'assistant') {
|
||||
html += '<img src="' + avatar + '" class="message-avatar">';
|
||||
}
|
||||
html += '<div>';
|
||||
html += '<div class="message-role">' + role + '</div>';
|
||||
html += '<div class="message-content">' + item.content + '</div>';
|
||||
html += '<div class="message-time">' + item.create_time + '</div>';
|
||||
html += '</div>';
|
||||
if (item.role === 'user') {
|
||||
html += '<img src="' + avatar + '" class="message-avatar">';
|
||||
}
|
||||
html += '</div>';
|
||||
});
|
||||
} else {
|
||||
html += '<div class="empty-state">';
|
||||
html += '<i class="layui-icon layui-icon-message"></i>';
|
||||
html += '<p>暂无消息记录</p>';
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
layero.find('.message-list').html(html);
|
||||
// 滚动到顶部显示当前页消息
|
||||
layero.find('.message-list').scrollTop(0);
|
||||
|
||||
// 渲染分页控件
|
||||
initPagination(total, page);
|
||||
},
|
||||
error: function() {
|
||||
layero.find('.message-list').html('<div class="empty-state">加载消息失败</div>');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 初始化分页控件
|
||||
function initPagination(total, curr) {
|
||||
layui.laypage.render({
|
||||
elem: layero.find('#messagePage')[0],
|
||||
count: total,
|
||||
limit: pageSize,
|
||||
curr: curr,
|
||||
layout: ['prev', 'page', 'next', 'count', 'skip'],
|
||||
jump: function(obj, first) {
|
||||
if (!first) {
|
||||
currentPage = obj.curr;
|
||||
loadMessages(currentPage);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 初始加载第一页消息
|
||||
loadMessages(currentPage);
|
||||
}
|
||||
});
|
||||
} else if (layEvent === 'end') {
|
||||
// 结束会话
|
||||
layer.confirm('确定要结束该会话吗?', function(index) {
|
||||
$.ajax({
|
||||
url: ns.url("aikefu://shop/kefu/endConversation"),
|
||||
type: 'POST',
|
||||
data: {id: data.id},
|
||||
dataType: 'json',
|
||||
success: function(res) {
|
||||
if (res.code === 0) {
|
||||
layer.msg('会话已结束', {icon: 1});
|
||||
// 重新加载表格数据
|
||||
tableIns.reload();
|
||||
} else {
|
||||
layer.msg('操作失败:' + res.message, {icon: 2});
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
layer.msg('请求失败,请稍后重试', {icon: 2});
|
||||
}
|
||||
});
|
||||
layer.close(index);
|
||||
});
|
||||
} else if (layEvent === 'delete') {
|
||||
// 删除会话
|
||||
layer.confirm('确定要删除该会话吗?删除后将无法恢复', function(index) {
|
||||
$.ajax({
|
||||
url: ns.url("aikefu://shop/kefu/deleteConversation"),
|
||||
type: 'POST',
|
||||
data: {id: data.id},
|
||||
dataType: 'json',
|
||||
success: function(res) {
|
||||
if (res.code === 0) {
|
||||
layer.msg('会话已删除', {icon: 1});
|
||||
// 重新加载表格数据
|
||||
tableIns.reload();
|
||||
} else {
|
||||
layer.msg('操作失败:' + res.message, {icon: 2});
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
layer.msg('请求失败,请稍后重试', {icon: 2});
|
||||
}
|
||||
});
|
||||
layer.close(index);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 批量删除按钮点击事件
|
||||
$('#batchDeleteBtn').click(function() {
|
||||
// 获取选中的行数据
|
||||
var checkStatus = table.checkStatus('conversationTable');
|
||||
var data = checkStatus.data;
|
||||
|
||||
if (data.length === 0) {
|
||||
layer.msg('请选择要删除的会话', {icon: 2});
|
||||
return;
|
||||
}
|
||||
|
||||
// 提取选中的会话ID
|
||||
var ids = [];
|
||||
for (var i = 0; i < data.length; i++) {
|
||||
ids.push(data[i].id);
|
||||
}
|
||||
|
||||
// 确认删除
|
||||
layer.confirm('确定要删除选中的 ' + data.length + ' 个会话吗?删除后将无法恢复', function(index) {
|
||||
$.ajax({
|
||||
url: ns.url("aikefu://shop/kefu/deleteConversation"),
|
||||
type: 'POST',
|
||||
data: {ids: ids},
|
||||
dataType: 'json',
|
||||
success: function(res) {
|
||||
if (res.code === 0) {
|
||||
layer.msg('删除成功', {icon: 1});
|
||||
// 重新加载表格数据
|
||||
tableIns.reload();
|
||||
} else {
|
||||
layer.msg('操作失败:' + res.message, {icon: 2});
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
layer.msg('请求失败,请稍后重试', {icon: 2});
|
||||
}
|
||||
});
|
||||
layer.close(index);
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
37
src/addon/aikefu/shop/view/kefu/index.html
Normal file
37
src/addon/aikefu/shop/view/kefu/index.html
Normal file
@@ -0,0 +1,37 @@
|
||||
<div class="layui-fluid">
|
||||
<div class="layui-card">
|
||||
<div class="layui-card-body">
|
||||
<div class="layui-tab layui-tab-brief" lay-filter="kefu-tab">
|
||||
<ul class="layui-tab-title">
|
||||
<li class="layui-this" lay-id="config">配置</li>
|
||||
<li lay-id="conversation">会话</li>
|
||||
<li lay-id="message">消息</li>
|
||||
</ul>
|
||||
<div class="layui-tab-content">
|
||||
<div class="layui-tab-item layui-show">
|
||||
{include file="kefu/config" /}
|
||||
</div>
|
||||
<div class="layui-tab-item">
|
||||
{include file="kefu/conversation" /}
|
||||
</div>
|
||||
<div class="layui-tab-item">
|
||||
{include file="kefu/message" /}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
layui.use(['element', 'form', 'table', 'layer', 'laypage'], function() {
|
||||
var element = layui.element;
|
||||
var form = layui.form;
|
||||
var table = layui.table;
|
||||
var layer = layui.layer;
|
||||
var laypage = layui.laypage;
|
||||
|
||||
// 初始化表单渲染
|
||||
form.render();
|
||||
});
|
||||
</script>
|
||||
521
src/addon/aikefu/shop/view/kefu/message.html
Normal file
521
src/addon/aikefu/shop/view/kefu/message.html
Normal file
@@ -0,0 +1,521 @@
|
||||
<style>
|
||||
/* 整体页面样式优化 */
|
||||
.layui-card-body {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
/* 搜索区域样式 */
|
||||
.search-box {
|
||||
margin-bottom: 20px;
|
||||
padding: 20px;
|
||||
background-color: #ffffff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.08);
|
||||
border: 1px solid #e8e8e8;
|
||||
}
|
||||
|
||||
.search-form {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.search-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.search-item label {
|
||||
width: 80px;
|
||||
text-align: right;
|
||||
margin-right: 10px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
/* 会话信息区域样式 */
|
||||
.conversation-info {
|
||||
margin-bottom: 20px;
|
||||
padding: 20px;
|
||||
background-color: #f7f9fc;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.08);
|
||||
border: 1px solid #e8e8e8;
|
||||
}
|
||||
|
||||
.conversation-info h3 {
|
||||
margin: 0 0 15px 0;
|
||||
font-size: 18px;
|
||||
color: #333;
|
||||
font-weight: 600;
|
||||
border-bottom: 2px solid #1E9FFF;
|
||||
padding-bottom: 8px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.conversation-info p {
|
||||
margin: 10px 0;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
/* 消息列表样式优化 */
|
||||
.message-list {
|
||||
max-height: 650px;
|
||||
overflow-y: auto;
|
||||
padding: 25px;
|
||||
background-color: #fafbfc;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 20px;
|
||||
border: 1px solid #e8e8e8;
|
||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
/* 消息项样式 */
|
||||
.message-item {
|
||||
margin-bottom: 25px;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.message-item.user {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.message-item.assistant {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
/* 头像样式 */
|
||||
.message-avatar {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 50%;
|
||||
margin: 0 12px;
|
||||
border: 2px solid #fff;
|
||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
/* 消息内容样式 */
|
||||
.message-content {
|
||||
max-width: 70%;
|
||||
padding: 16px 20px;
|
||||
border-radius: 18px;
|
||||
word-wrap: break-word;
|
||||
line-height: 1.6;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.message-item.user .message-content {
|
||||
background-color: #1E9FFF;
|
||||
color: white;
|
||||
border-bottom-right-radius: 4px;
|
||||
box-shadow: 0 2px 8px rgba(30, 159, 255, 0.3);
|
||||
}
|
||||
|
||||
.message-item.assistant .message-content {
|
||||
background-color: white;
|
||||
color: #333;
|
||||
border-bottom-left-radius: 4px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
/* 消息时间样式 */
|
||||
.message-time {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
margin-top: 8px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* 消息角色样式 */
|
||||
.message-role {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.message-item.user .message-role {
|
||||
color: rgba(106, 38, 38, 0.9);
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.message-item.assistant .message-role {
|
||||
color: #666;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
/* 状态标签样式 */
|
||||
.status-active {
|
||||
color: #52c41a;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.status-inactive {
|
||||
color: #faad14;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 分页样式 */
|
||||
#msg_pagination {
|
||||
text-align: center;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
/* 按钮样式 */
|
||||
.layui-btn {
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.layui-btn-primary:hover {
|
||||
border-color: #1E9FFF;
|
||||
color: #1E9FFF;
|
||||
}
|
||||
|
||||
/* 空状态样式 */
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 60px 20px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.empty-state i {
|
||||
font-size: 48px;
|
||||
margin-bottom: 15px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.message-content {
|
||||
max-width: 85%;
|
||||
}
|
||||
|
||||
.search-form {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.search-item {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.search-item label {
|
||||
width: auto;
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="layui-card-body">
|
||||
<!-- 搜索区域 -->
|
||||
<div class="search-box">
|
||||
<form class="layui-form search-form" id="msg_searchForm">
|
||||
<div class="search-item">
|
||||
<label for="conversation_id">会话ID</label>
|
||||
<input type="text" name="conversation_id" id="msg_conversation_id" placeholder="请输入会话ID进行过滤" value="{$conversation_id ?? ''}" class="layui-input" style="width: 250px; display: inline-block;">
|
||||
</div>
|
||||
<div class="search-item">
|
||||
<label for="user_id">用户ID</label>
|
||||
<input type="text" name="user_id" id="msg_user_id" placeholder="请输入用户ID进行过滤" value="{$user_id ?? ''}" class="layui-input" style="width: 200px; display: inline-block;">
|
||||
</div>
|
||||
<div class="search-item">
|
||||
<label for="status">消息状态</label>
|
||||
<div class="layui-input-inline" style="width: 150px;">
|
||||
<select name="status" id="msg_status" class="layui-select">
|
||||
<option value="completed">已完成</option>
|
||||
<option value="streaming">流式中</option>
|
||||
<option value="failed">失败</option>
|
||||
<option value="">全部状态</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="search-item">
|
||||
<label for="sort_order">排序方式</label>
|
||||
<div class="layui-input-inline" style="width: 200px;">
|
||||
<select name="sort_order" id="msg_sortOrder" class="layui-select">
|
||||
<option value="desc">最新消息在前面</option>
|
||||
<option value="asc">最新消息在后面</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="search-item">
|
||||
<button type="button" class="layui-btn layui-btn-primary" id="msg_searchBtn">搜索</button>
|
||||
<button type="button" class="layui-btn" id="msg_resetBtn">重置</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- 会话信息 -->
|
||||
<div id="msg_conversationInfo" class="conversation-info">
|
||||
<h3>会话记录</h3>
|
||||
<p><span class="status-active">当前未选择会话,可通过会话ID进行过滤</span></p>
|
||||
</div>
|
||||
|
||||
<!-- 消息列表 -->
|
||||
<div class="message-list" id="msg_messageList">
|
||||
<!-- 消息列表将通过JavaScript动态加载 -->
|
||||
</div>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div id="msg_pagination"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
layui.use(['laypage', 'layer', 'form'], function() {
|
||||
var laypage = layui.laypage;
|
||||
var layer = layui.layer;
|
||||
var form = layui.form;
|
||||
|
||||
// 渲染表单控件
|
||||
form.render();
|
||||
|
||||
// 分页和排序参数
|
||||
var page = 1;
|
||||
var limit = 50;
|
||||
var total = 0;
|
||||
var conversation_id = $('#msg_conversation_id').val();
|
||||
var user_id = $('#msg_user_id').val();
|
||||
var status = 'completed'; // 默认只显示已完成的消息
|
||||
var sortField = 'create_time'; // 排序字段:创建时间
|
||||
var sortOrder = 'desc'; // 默认倒序(最新消息在前面)
|
||||
|
||||
// 加载会话信息(当指定会话ID时显示)
|
||||
function loadConversationInfo() {
|
||||
if (!conversation_id) {
|
||||
$('#msg_conversationInfo').html('<h3>会话记录</h3><p><span class="status-active">当前未选择会话,可通过会话ID进行过滤</span></p>');
|
||||
return;
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
url: ns.url("aikefu://shop/kefu/getConversationInfo"),
|
||||
type: 'POST',
|
||||
data: {
|
||||
conversation_id: conversation_id
|
||||
},
|
||||
dataType: 'json',
|
||||
success: function(res) {
|
||||
if (res.code === 0) {
|
||||
var info = res.data;
|
||||
var html = '<h3>会话详情</h3>';
|
||||
html += '<p><strong>会话ID:</strong>' + info.conversation_id + '</p>';
|
||||
html += '<p><strong>用户ID:</strong>' + info.user_id + '</p>';
|
||||
html += '<p><strong>会话名称:</strong>' + info.name + '</p>';
|
||||
html += '<p><strong>状态:</strong>' + (info.status === 1 ? '<span class="status-active">活跃</span>' : '<span class="status-inactive">已结束</span>') + '</p>';
|
||||
html += '<p><strong>创建时间:</strong>' + info.create_time + '</p>';
|
||||
html += '<p><strong>更新时间:</strong>' + info.update_time + '</p>';
|
||||
$('#msg_conversationInfo').html(html);
|
||||
} else {
|
||||
$('#msg_conversationInfo').html('<h3>会话详情</h3><p>未找到该会话的详细信息</p>');
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
$('#msg_conversationInfo').html('<h3>会话详情</h3><p>加载会话信息失败</p>');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 加载消息列表
|
||||
function loadMessageList(forceScrollTop = false) {
|
||||
// 构建请求数据
|
||||
var requestData = {
|
||||
page: page,
|
||||
limit: limit,
|
||||
sort_field: sortField, // 排序字段:创建时间
|
||||
sort_order: sortOrder // 排序方式:倒序/正序
|
||||
};
|
||||
|
||||
// 如果有会话ID,则添加到请求数据中
|
||||
if (conversation_id) {
|
||||
requestData.conversation_id = conversation_id;
|
||||
}
|
||||
|
||||
// 如果有用户ID,则添加到请求数据中
|
||||
if (user_id) {
|
||||
requestData.user_id = user_id;
|
||||
}
|
||||
|
||||
// 添加状态过滤
|
||||
if (status) {
|
||||
requestData.status = status;
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
url: ns.url("aikefu://shop/kefu/getMessageList"),
|
||||
type: 'POST',
|
||||
data: requestData,
|
||||
dataType: 'json',
|
||||
success: function(res) {
|
||||
// 调试:打印原始数据
|
||||
console.log('消息API返回原始数据:', res);
|
||||
|
||||
// 确保res存在
|
||||
if (!res) {
|
||||
layer.msg('服务器返回数据为空', {icon: 2});
|
||||
return;
|
||||
}
|
||||
|
||||
if (res.code === 0) {
|
||||
// 适配后端返回的格式
|
||||
// 检查data字段是否存在
|
||||
if (!res.data) {
|
||||
layer.msg('服务器返回数据格式错误:缺少data字段', {icon: 2});
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取消息列表,确保是数组
|
||||
var list = Array.isArray(res.data.messages) ? res.data.messages : [];
|
||||
|
||||
// 获取总记录数
|
||||
// 支持多种可能的返回格式
|
||||
var totalCount = 0;
|
||||
if (res.data.page_info && typeof res.data.page_info.total === 'number') {
|
||||
totalCount = res.data.page_info.total;
|
||||
} else if (typeof res.count === 'number') {
|
||||
totalCount = res.count;
|
||||
} else if (typeof res.data.total === 'number') {
|
||||
totalCount = res.data.total;
|
||||
}
|
||||
|
||||
// 更新总记录数
|
||||
total = totalCount;
|
||||
|
||||
var html = '';
|
||||
if (list.length > 0) {
|
||||
list.forEach(function(item) {
|
||||
var role = item.role === 'user' ? (item.user_id ? '用户-' + item.user_id : '用户') : '机器人';
|
||||
var roleClass = item.role === 'user' ? 'user' : 'assistant';
|
||||
var avatar = item.role === 'user' ? '/addon/aikefu/static/images/user.svg' : '/addon/aikefu/static/images/robot.svg';
|
||||
|
||||
html += '<div class="message-item ' + roleClass + '">';
|
||||
if (item.role === 'assistant') {
|
||||
html += '<img src="' + avatar + '" class="message-avatar">';
|
||||
}
|
||||
html += '<div>';
|
||||
html += '<div class="message-role">' + role + '</div>';
|
||||
html += '<div class="message-content">' + item.content + '</div>';
|
||||
html += '<div class="message-time">' + item.create_time + '</div>';
|
||||
html += '</div>';
|
||||
if (item.role === 'user') {
|
||||
html += '<img src="' + avatar + '" class="message-avatar">';
|
||||
}
|
||||
html += '</div>';
|
||||
});
|
||||
} else {
|
||||
html += '<div class="empty-state">';
|
||||
html += '<i class="layui-icon layui-icon-message"></i>';
|
||||
html += '<p>暂无消息记录</p>';
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
$('#msg_messageList').html(html);
|
||||
// 调整滚动位置
|
||||
if (forceScrollTop) {
|
||||
// 搜索或分页时强制滚动到顶部
|
||||
$('#msg_messageList').scrollTop(0);
|
||||
} else if (sortOrder === 'asc') {
|
||||
// 正序时滚动到顶部
|
||||
$('#msg_messageList').scrollTop(0);
|
||||
} else {
|
||||
// 倒序时滚动到底部
|
||||
$('#msg_messageList').scrollTop($('#msg_messageList')[0].scrollHeight);
|
||||
}
|
||||
// 渲染分页
|
||||
renderPagination();
|
||||
} else {
|
||||
layer.msg('加载失败:' + (res.message || '未知错误'), {icon: 2});
|
||||
}
|
||||
},
|
||||
error: function(xhr, status, error) {
|
||||
console.error('消息API请求失败:', error);
|
||||
layer.msg('请求失败,请稍后重试', {icon: 2});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 渲染分页
|
||||
function renderPagination() {
|
||||
if (total <= limit) {
|
||||
$('#msg_pagination').html('');
|
||||
return;
|
||||
}
|
||||
|
||||
// 确保分页容器存在
|
||||
if ($('#msg_pagination').length === 0) {
|
||||
console.error('分页容器不存在');
|
||||
return;
|
||||
}
|
||||
|
||||
laypage.render({
|
||||
elem: 'msg_pagination',
|
||||
count: total,
|
||||
limit: limit,
|
||||
curr: page,
|
||||
layout: ['prev', 'page', 'next', 'count', 'skip'],
|
||||
jump: function(obj, first) {
|
||||
if (!first) {
|
||||
// 验证页码有效性
|
||||
var newPage = parseInt(obj.curr);
|
||||
if (isNaN(newPage) || newPage < 1) {
|
||||
console.error('无效的页码:', obj.curr);
|
||||
return;
|
||||
}
|
||||
|
||||
page = newPage;
|
||||
loadMessageList(true); // 分页时强制滚动到顶部
|
||||
}
|
||||
},
|
||||
done: function(obj, first) {
|
||||
// 分页渲染完成后的回调
|
||||
console.log('分页渲染完成,当前页:', obj.curr, '总记录数:', obj.count, '每页数量:', obj.limit);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 搜索按钮点击事件
|
||||
$('#msg_searchBtn').click(function() {
|
||||
conversation_id = $('#msg_conversation_id').val().trim();
|
||||
user_id = $('#msg_user_id').val().trim();
|
||||
status = $('#msg_status').val(); // 获取当前选择的状态
|
||||
sortOrder = $('#msg_sortOrder').val(); // 获取当前选择的排序方式
|
||||
page = 1;
|
||||
loadConversationInfo();
|
||||
loadMessageList(true); // 搜索时强制滚动到顶部
|
||||
});
|
||||
|
||||
// 重置按钮点击事件
|
||||
$('#msg_resetBtn').click(function() {
|
||||
$('#msg_conversation_id').val('');
|
||||
$('#msg_user_id').val('');
|
||||
$('#msg_status').val('completed'); // 重置为默认状态
|
||||
$('#msg_sortOrder').val('desc'); // 重置为默认排序
|
||||
conversation_id = '';
|
||||
user_id = '';
|
||||
status = 'completed'; // 重置为默认状态
|
||||
sortOrder = 'desc'; // 重置为默认排序
|
||||
page = 1;
|
||||
loadConversationInfo();
|
||||
loadMessageList(true); // 重置时强制滚动到顶部
|
||||
});
|
||||
|
||||
// 排序方式变更事件
|
||||
$('#msg_sortOrder').change(function() {
|
||||
sortOrder = $(this).val();
|
||||
page = 1; // 切换排序时重置到第一页
|
||||
loadMessageList(true); // 切换排序时强制滚动到顶部
|
||||
});
|
||||
|
||||
// 状态变更事件
|
||||
$('#msg_status').change(function() {
|
||||
status = $(this).val();
|
||||
page = 1; // 切换状态时重置到第一页
|
||||
loadMessageList(true); // 切换状态时强制滚动到顶部
|
||||
});
|
||||
|
||||
// 初始化加载(默认显示所有消息)
|
||||
loadConversationInfo();
|
||||
loadMessageList();
|
||||
});
|
||||
</script>
|
||||
7
src/addon/aikefu/static/images/robot.svg
Normal file
7
src/addon/aikefu/static/images/robot.svg
Normal file
@@ -0,0 +1,7 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="0 0 40 40">
|
||||
<rect x="5" y="10" width="30" height="20" rx="5" fill="#7C7C7C"/>
|
||||
<circle cx="15" cy="20" r="3" fill="#FFFFFF"/>
|
||||
<circle cx="25" cy="20" r="3" fill="#FFFFFF"/>
|
||||
<path d="M15 30 L25 30" stroke="#FFFFFF" stroke-width="3"/>
|
||||
<rect x="15" y="10" width="10" height="5" fill="#5C5C5C"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 379 B |
4
src/addon/aikefu/static/images/user.svg
Normal file
4
src/addon/aikefu/static/images/user.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="0 0 40 40">
|
||||
<circle cx="20" cy="15" r="10" fill="#4A90E2"/>
|
||||
<path d="M30 35c0-5-4-9-9-9s-9 4-9 9" fill="#4A90E2"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 197 B |
@@ -1,38 +1,31 @@
|
||||
<?php
|
||||
/**
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
*/
|
||||
return [
|
||||
|
||||
// 自定义模板页面类型,格式:[ 'title' => '页面类型名称', 'name' => '页面标识', 'path' => '页面路径', 'value' => '页面数据,json格式' ]
|
||||
'template' => [],
|
||||
|
||||
// 后台自定义组件——装修
|
||||
'util' => [],
|
||||
|
||||
// 自定义页面路径
|
||||
'link' => [],
|
||||
|
||||
// 自定义图标库
|
||||
'icon_library' => [],
|
||||
|
||||
// uni-app 组件,格式:[ 'name' => '组件名称/文件夹名称', 'path' => '文件路径/目录路径' ],多个逗号隔开,自定义组件名称前缀必须是diy-,也可以引用第三方组件
|
||||
'component' => [],
|
||||
|
||||
// uni-app 页面,多个逗号隔开
|
||||
'pages' => [],
|
||||
|
||||
// 模板信息,格式:'title' => '模板名称', 'name' => '模板标识', 'cover' => '模板封面图', 'preview' => '模板预览图', 'desc' => '模板描述'
|
||||
'info' => [],
|
||||
|
||||
// 主题风格配色,格式可以自由定义扩展,【在uni-app中通过:this.themeStyle... 获取定义的颜色字段,例如:this.themeStyle.main_color】
|
||||
'theme' => [],
|
||||
|
||||
// 自定义页面数据,格式:[ 'title' => '页面名称', 'name' => "页面标识", 'value' => [页面数据,json格式] ]
|
||||
'data' => []
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
// 自定义模板页面类型,格式:[ 'title' => '页面类型名称', 'name' => '页面标识', 'path' => '页面路径', 'value' => '页面数据,json格式' ]
|
||||
'template' => [],
|
||||
|
||||
// 后台自定义组件——装修
|
||||
'util' => [],
|
||||
|
||||
// 自定义页面路径
|
||||
'link' => [],
|
||||
|
||||
// 自定义图标库
|
||||
'icon_library' => [],
|
||||
|
||||
// uni-app 组件,格式:[ 'name' => '组件名称/文件夹名称', 'path' => '文件路径/目录路径' ],多个逗号隔开,自定义组件名称前缀必须是diy-,也可以引用第三方组件
|
||||
'component' => [],
|
||||
|
||||
// uni-app 页面,多个逗号隔开
|
||||
'pages' => [],
|
||||
|
||||
// 模板信息,格式:'title' => '模板名称', 'name' => '模板标识', 'cover' => '模板封面图', 'preview' => '模板预览图', 'desc' => '模板描述'
|
||||
'info' => [],
|
||||
|
||||
// 主题风格配色,格式可以自由定义扩展,【在uni-app中通过:this.themeStyle... 获取定义的颜色字段,例如:this.themeStyle.main_color】
|
||||
'theme' => [],
|
||||
|
||||
// 自定义页面数据,格式:[ 'title' => '页面名称', 'name' => "页面标识", 'value' => [页面数据,json格式] ]
|
||||
'data' => []
|
||||
];
|
||||
@@ -1,21 +1,13 @@
|
||||
<?php
|
||||
/**
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
*/
|
||||
return [
|
||||
'name' => 'alioss',
|
||||
'title' => '阿里云OSS',
|
||||
'description' => '阿里云OSS',
|
||||
'type' => 'system', //插件类型 system :系统插件(自动安装), business:业务插件 promotion:营销插件 tool:工具插件
|
||||
'status' => 1,
|
||||
'author' => '',
|
||||
'version' => '5.3.1',
|
||||
'version_no' => '525231212001',
|
||||
'content' => '',
|
||||
<?php
|
||||
|
||||
return [
|
||||
'name' => 'alioss',
|
||||
'title' => '阿里云OSS',
|
||||
'description' => '阿里云OSS',
|
||||
'type' => 'system', //插件类型 system :系统插件(自动安装), business:业务插件 promotion:营销插件 tool:工具插件
|
||||
'status' => 1,
|
||||
'author' => '',
|
||||
'version' => '5.3.1',
|
||||
'version_no' => '525231212001',
|
||||
'content' => '',
|
||||
];
|
||||
@@ -1,39 +1,32 @@
|
||||
<?php
|
||||
/**
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
*/
|
||||
|
||||
|
||||
namespace addon\alioss\event;
|
||||
|
||||
use addon\alioss\model\Alioss;
|
||||
use addon\alioss\model\Config;
|
||||
|
||||
/**
|
||||
* 删除阿里云图片
|
||||
*/
|
||||
class ClearAlbumPic
|
||||
{
|
||||
public function handle($params)
|
||||
{
|
||||
$config_model = new Config();
|
||||
$alioss_model = new Alioss();
|
||||
|
||||
$config = $config_model->getAliossConfig($params[ 'site_id' ]);
|
||||
if (!empty($config[ 'data' ])) {
|
||||
if (!empty($config[ 'data' ][ 'value' ][ 'endpoint' ]) && strpos($params[ 'pic_path' ], $config[ 'data' ][ 'value' ][ 'endpoint' ]) === 0) {
|
||||
$result = $alioss_model->deleteAlbumPic($params[ 'pic_path' ], $config[ 'data' ][ 'value' ][ 'endpoint' ]);
|
||||
return $result;
|
||||
}
|
||||
if (!empty($config[ 'data' ][ 'value' ][ 'domain' ]) && strpos($params[ 'pic_path' ], $config[ 'data' ][ 'value' ][ 'domain' ]) === 0) {
|
||||
$result = $alioss_model->deleteAlbumPic($params[ 'pic_path' ], $config[ 'data' ][ 'value' ][ 'domain' ]);
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
}
|
||||
<?php
|
||||
|
||||
|
||||
|
||||
namespace addon\alioss\event;
|
||||
|
||||
use addon\alioss\model\Alioss;
|
||||
use addon\alioss\model\Config;
|
||||
|
||||
/**
|
||||
* 删除阿里云图片
|
||||
*/
|
||||
class ClearAlbumPic
|
||||
{
|
||||
public function handle($params)
|
||||
{
|
||||
$config_model = new Config();
|
||||
$alioss_model = new Alioss();
|
||||
|
||||
$config = $config_model->getAliossConfig($params[ 'site_id' ]);
|
||||
if (!empty($config[ 'data' ])) {
|
||||
if (!empty($config[ 'data' ][ 'value' ][ 'endpoint' ]) && strpos($params[ 'pic_path' ], $config[ 'data' ][ 'value' ][ 'endpoint' ]) === 0) {
|
||||
$result = $alioss_model->deleteAlbumPic($params[ 'pic_path' ], $config[ 'data' ][ 'value' ][ 'endpoint' ]);
|
||||
return $result;
|
||||
}
|
||||
if (!empty($config[ 'data' ][ 'value' ][ 'domain' ]) && strpos($params[ 'pic_path' ], $config[ 'data' ][ 'value' ][ 'domain' ]) === 0) {
|
||||
$result = $alioss_model->deleteAlbumPic($params[ 'pic_path' ], $config[ 'data' ][ 'value' ][ 'domain' ]);
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,27 +1,20 @@
|
||||
<?php
|
||||
/**
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
*/
|
||||
|
||||
|
||||
namespace addon\alioss\event;
|
||||
|
||||
use addon\alioss\model\Config;
|
||||
|
||||
/**
|
||||
* 关闭云上传
|
||||
*/
|
||||
class CloseOss
|
||||
{
|
||||
public function handle()
|
||||
{
|
||||
$config_model = new Config();
|
||||
$result = $config_model->modifyConfigIsUse(0);
|
||||
return $result;
|
||||
}
|
||||
<?php
|
||||
|
||||
|
||||
|
||||
namespace addon\alioss\event;
|
||||
|
||||
use addon\alioss\model\Config;
|
||||
|
||||
/**
|
||||
* 关闭云上传
|
||||
*/
|
||||
class CloseOss
|
||||
{
|
||||
public function handle()
|
||||
{
|
||||
$config_model = new Config();
|
||||
$result = $config_model->modifyConfigIsUse(0);
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
@@ -1,26 +1,19 @@
|
||||
<?php
|
||||
/**
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
*/
|
||||
|
||||
|
||||
namespace addon\alioss\event;
|
||||
|
||||
/**
|
||||
* 应用安装
|
||||
*/
|
||||
class Install
|
||||
{
|
||||
/**
|
||||
* 执行安装
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
return success();
|
||||
}
|
||||
<?php
|
||||
|
||||
|
||||
|
||||
namespace addon\alioss\event;
|
||||
|
||||
/**
|
||||
* 应用安装
|
||||
*/
|
||||
class Install
|
||||
{
|
||||
/**
|
||||
* 执行安装
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
return success();
|
||||
}
|
||||
}
|
||||
@@ -1,33 +1,26 @@
|
||||
<?php
|
||||
/**
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
*/
|
||||
|
||||
|
||||
namespace addon\alioss\event;
|
||||
|
||||
/**
|
||||
* 云上传方式
|
||||
*/
|
||||
class OssType
|
||||
{
|
||||
/**
|
||||
* 短信发送方式方式及配置
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$info = array(
|
||||
"sms_type" => "alioss",
|
||||
"sms_type_name" => "阿里云上传",
|
||||
"edit_url" => "alioss://shop/config/config",
|
||||
"shop_url" => "alioss://shop/config/config",
|
||||
"desc" => "阿里云上传"
|
||||
);
|
||||
return $info;
|
||||
}
|
||||
<?php
|
||||
|
||||
|
||||
|
||||
namespace addon\alioss\event;
|
||||
|
||||
/**
|
||||
* 云上传方式
|
||||
*/
|
||||
class OssType
|
||||
{
|
||||
/**
|
||||
* 短信发送方式方式及配置
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$info = array(
|
||||
"sms_type" => "alioss",
|
||||
"sms_type_name" => "阿里云上传",
|
||||
"edit_url" => "alioss://shop/config/config",
|
||||
"shop_url" => "alioss://shop/config/config",
|
||||
"desc" => "阿里云上传"
|
||||
);
|
||||
return $info;
|
||||
}
|
||||
}
|
||||
@@ -1,31 +1,24 @@
|
||||
<?php
|
||||
/**
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
*/
|
||||
|
||||
|
||||
namespace addon\alioss\event;
|
||||
|
||||
use addon\alioss\model\Alioss;
|
||||
|
||||
/**
|
||||
* 云上传方式
|
||||
*/
|
||||
class Put
|
||||
{
|
||||
/**
|
||||
* @param $param
|
||||
* @return array
|
||||
*/
|
||||
public function handle($param)
|
||||
{
|
||||
$qiniu_model = new Alioss();
|
||||
$result = $qiniu_model->putFile($param);
|
||||
return $result;
|
||||
}
|
||||
<?php
|
||||
|
||||
|
||||
|
||||
namespace addon\alioss\event;
|
||||
|
||||
use addon\alioss\model\Alioss;
|
||||
|
||||
/**
|
||||
* 云上传方式
|
||||
*/
|
||||
class Put
|
||||
{
|
||||
/**
|
||||
* @param $param
|
||||
* @return array
|
||||
*/
|
||||
public function handle($param)
|
||||
{
|
||||
$qiniu_model = new Alioss();
|
||||
$result = $qiniu_model->putFile($param);
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
@@ -1,27 +1,18 @@
|
||||
<?php
|
||||
/**
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
*/
|
||||
|
||||
|
||||
namespace addon\alioss\event;
|
||||
|
||||
/**
|
||||
* 应用卸载
|
||||
*/
|
||||
class UnInstall
|
||||
{
|
||||
/**
|
||||
* 执行卸载
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
return success();
|
||||
}
|
||||
<?php
|
||||
|
||||
|
||||
namespace addon\alioss\event;
|
||||
|
||||
/**
|
||||
* 应用卸载
|
||||
*/
|
||||
class UnInstall
|
||||
{
|
||||
/**
|
||||
* 执行卸载
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
return success();
|
||||
}
|
||||
}
|
||||
@@ -1,146 +1,139 @@
|
||||
<?php
|
||||
/**
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
*/
|
||||
|
||||
namespace addon\alioss\model;
|
||||
|
||||
use app\model\BaseModel;
|
||||
use OSS\Core\OssException;
|
||||
use OSS\OssClient;
|
||||
use think\facade\Log;
|
||||
|
||||
/**
|
||||
* 阿里云OSS上传
|
||||
*/
|
||||
class Alioss extends BaseModel
|
||||
{
|
||||
|
||||
/**
|
||||
* 字节组上传
|
||||
* @param $data
|
||||
* @param $key
|
||||
* @return array
|
||||
*/
|
||||
public function put($param)
|
||||
{
|
||||
$data = $param['data'];
|
||||
$key = $param['key'];
|
||||
$config_model = new Config();
|
||||
$config_result = $config_model->getAliossConfig();
|
||||
$config = $config_result['data'];
|
||||
|
||||
if ($config['is_use'] == 1) {
|
||||
$config = $config['value'];
|
||||
$access_key_id = $config['access_key_id'];
|
||||
$access_key_secret = $config['access_key_secret'];
|
||||
$bucket = $config['bucket'];
|
||||
$endpoint = $config['endpoint'];
|
||||
try {
|
||||
$ossClient = new OssClient($access_key_id, $access_key_secret, $endpoint);
|
||||
|
||||
$result = $ossClient->putObject($bucket, $key, $data);
|
||||
$is_domain = $config[ 'is_domain' ] ?? 0;
|
||||
$path = $is_domain > 0 ? $config[ 'domain' ] . '/' . $key : $result['info']['url'];
|
||||
$data = array (
|
||||
'path' => $path,
|
||||
// "path" => $result["info"]["url"],
|
||||
'domain' => $endpoint,
|
||||
'bucket' => $bucket
|
||||
);
|
||||
return $this->success($data);
|
||||
} catch (OssException $e) {
|
||||
return $this->error('', $e->getErrorMessage());
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置阿里云OSS参数配置
|
||||
* @param unknown $filePath 上传图片路径
|
||||
* @param unknown $key 上传到阿里云后保存的文件名
|
||||
*/
|
||||
public function putFile($param)
|
||||
{
|
||||
$file_path = $param['file_path'];
|
||||
$key = $param['key'];
|
||||
$config_model = new Config();
|
||||
$config = $config_model->getAliossConfig()['data'];
|
||||
if ($config['is_use'] == 1) {
|
||||
$config = $config['value'];
|
||||
$access_key_id = $config['access_key_id'];
|
||||
$access_key_secret = $config['access_key_secret'];
|
||||
$bucket = $config['bucket'];
|
||||
//要上传的空间
|
||||
$endpoint = $config['endpoint'];
|
||||
try {
|
||||
$ossClient = new OssClient($access_key_id, $access_key_secret, $endpoint);
|
||||
$result = $ossClient->uploadFile($bucket, $key, $file_path);
|
||||
$is_domain = $config[ 'is_domain' ] ?? 0;
|
||||
$path = $is_domain > 0 ? $config[ 'domain' ] . '/' . $key : $result['info']['url'];
|
||||
$path = str_replace('http://', 'https://', $path);
|
||||
//返回图片的完整URL
|
||||
$data = array (
|
||||
// "path" => $this->subEndpoint($endpoint, $bucket)."/". $key,
|
||||
'path' => $path,
|
||||
'domain' => $endpoint,
|
||||
'bucket' => $bucket
|
||||
);
|
||||
return $this->success($data);
|
||||
} catch (\Exception $e) {
|
||||
return $this->error('', $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function subEndpoint($endpoint, $bucket)
|
||||
{
|
||||
if (strpos($endpoint, 'http://') === 0) {
|
||||
$temp = 'http://';
|
||||
} else {
|
||||
$temp = 'https://';
|
||||
}
|
||||
$temp_array = explode($temp, $endpoint);
|
||||
return $temp . $bucket . '.' . $temp_array[ 1 ];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $file_path
|
||||
* @return array
|
||||
* 删除阿里云图片
|
||||
*/
|
||||
public function deleteAlbumPic($file_path, $prefix)
|
||||
{
|
||||
$config_model = new Config();
|
||||
$config_result = $config_model->getAliossConfig();
|
||||
$config = $config_result['data'];
|
||||
|
||||
if (!empty($config)) {
|
||||
$config = $config['value'];
|
||||
$access_key_id = $config['access_key_id'];
|
||||
$access_key_secret = $config['access_key_secret'];
|
||||
$bucket = $config['bucket'];
|
||||
//要上传的空间
|
||||
$endpoint = $config['endpoint'];
|
||||
try {
|
||||
$ossClient = new OssClient($access_key_id, $access_key_secret, $endpoint);
|
||||
$ossClient->deleteObject($bucket, str_replace($prefix . '/', '', $file_path));
|
||||
$ossClient->deleteObject($bucket, str_replace($prefix . '/', '', img($file_path, 'big')));
|
||||
$ossClient->deleteObject($bucket, str_replace($prefix . '/', '', img($file_path, 'mid')));
|
||||
$ossClient->deleteObject($bucket, str_replace($prefix . '/', '', img($file_path, 'small')));
|
||||
|
||||
return $this->success();
|
||||
} catch (OssException $e) {
|
||||
return $this->error('', $e->getErrorMessage());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
<?php
|
||||
|
||||
|
||||
namespace addon\alioss\model;
|
||||
|
||||
use app\model\BaseModel;
|
||||
use OSS\Core\OssException;
|
||||
use OSS\OssClient;
|
||||
use think\facade\Log;
|
||||
|
||||
/**
|
||||
* 阿里云OSS上传
|
||||
*/
|
||||
class Alioss extends BaseModel
|
||||
{
|
||||
|
||||
/**
|
||||
* 字节组上传
|
||||
* @param $data
|
||||
* @param $key
|
||||
* @return array
|
||||
*/
|
||||
public function put($param)
|
||||
{
|
||||
$data = $param['data'];
|
||||
$key = $param['key'];
|
||||
$config_model = new Config();
|
||||
$config_result = $config_model->getAliossConfig();
|
||||
$config = $config_result['data'];
|
||||
|
||||
if ($config['is_use'] == 1) {
|
||||
$config = $config['value'];
|
||||
$access_key_id = $config['access_key_id'];
|
||||
$access_key_secret = $config['access_key_secret'];
|
||||
$bucket = $config['bucket'];
|
||||
$endpoint = $config['endpoint'];
|
||||
try {
|
||||
$ossClient = new OssClient($access_key_id, $access_key_secret, $endpoint);
|
||||
|
||||
$result = $ossClient->putObject($bucket, $key, $data);
|
||||
$is_domain = $config[ 'is_domain' ] ?? 0;
|
||||
$path = $is_domain > 0 ? $config[ 'domain' ] . '/' . $key : $result['info']['url'];
|
||||
$data = array (
|
||||
'path' => $path,
|
||||
// "path" => $result["info"]["url"],
|
||||
'domain' => $endpoint,
|
||||
'bucket' => $bucket
|
||||
);
|
||||
return $this->success($data);
|
||||
} catch (OssException $e) {
|
||||
return $this->error('', $e->getErrorMessage());
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置阿里云OSS参数配置
|
||||
* @param unknown $filePath 上传图片路径
|
||||
* @param unknown $key 上传到阿里云后保存的文件名
|
||||
*/
|
||||
public function putFile($param)
|
||||
{
|
||||
$file_path = $param['file_path'];
|
||||
$key = $param['key'];
|
||||
$config_model = new Config();
|
||||
$config = $config_model->getAliossConfig()['data'];
|
||||
if ($config['is_use'] == 1) {
|
||||
$config = $config['value'];
|
||||
$access_key_id = $config['access_key_id'];
|
||||
$access_key_secret = $config['access_key_secret'];
|
||||
$bucket = $config['bucket'];
|
||||
//要上传的空间
|
||||
$endpoint = $config['endpoint'];
|
||||
try {
|
||||
$ossClient = new OssClient($access_key_id, $access_key_secret, $endpoint);
|
||||
$result = $ossClient->uploadFile($bucket, $key, $file_path);
|
||||
$is_domain = $config[ 'is_domain' ] ?? 0;
|
||||
$path = $is_domain > 0 ? $config[ 'domain' ] . '/' . $key : $result['info']['url'];
|
||||
$path = str_replace('http://', 'https://', $path);
|
||||
//返回图片的完整URL
|
||||
$data = array (
|
||||
// "path" => $this->subEndpoint($endpoint, $bucket)."/". $key,
|
||||
'path' => $path,
|
||||
'domain' => $endpoint,
|
||||
'bucket' => $bucket
|
||||
);
|
||||
return $this->success($data);
|
||||
} catch (\Exception $e) {
|
||||
return $this->error('', $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function subEndpoint($endpoint, $bucket)
|
||||
{
|
||||
if (strpos($endpoint, 'http://') === 0) {
|
||||
$temp = 'http://';
|
||||
} else {
|
||||
$temp = 'https://';
|
||||
}
|
||||
$temp_array = explode($temp, $endpoint);
|
||||
return $temp . $bucket . '.' . $temp_array[ 1 ];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $file_path
|
||||
* @return array
|
||||
* 删除阿里云图片
|
||||
*/
|
||||
public function deleteAlbumPic($file_path, $prefix)
|
||||
{
|
||||
$config_model = new Config();
|
||||
$config_result = $config_model->getAliossConfig();
|
||||
$config = $config_result['data'];
|
||||
|
||||
if (!empty($config)) {
|
||||
$config = $config['value'];
|
||||
$access_key_id = $config['access_key_id'];
|
||||
$access_key_secret = $config['access_key_secret'];
|
||||
$bucket = $config['bucket'];
|
||||
//要上传的空间
|
||||
$endpoint = $config['endpoint'];
|
||||
try {
|
||||
$ossClient = new OssClient($access_key_id, $access_key_secret, $endpoint);
|
||||
$ossClient->deleteObject($bucket, str_replace($prefix . '/', '', $file_path));
|
||||
$ossClient->deleteObject($bucket, str_replace($prefix . '/', '', img($file_path, 'big')));
|
||||
$ossClient->deleteObject($bucket, str_replace($prefix . '/', '', img($file_path, 'mid')));
|
||||
$ossClient->deleteObject($bucket, str_replace($prefix . '/', '', img($file_path, 'small')));
|
||||
|
||||
return $this->success();
|
||||
} catch (OssException $e) {
|
||||
return $this->error('', $e->getErrorMessage());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,56 +1,49 @@
|
||||
<?php
|
||||
/**
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
*/
|
||||
|
||||
namespace addon\alioss\model;
|
||||
|
||||
use app\model\system\Config as ConfigModel;
|
||||
use app\model\BaseModel;
|
||||
|
||||
/**
|
||||
* 阿里云配置
|
||||
*/
|
||||
class Config extends BaseModel
|
||||
{
|
||||
/**
|
||||
* 设置阿里云OSS上传配置
|
||||
* array $data
|
||||
*/
|
||||
public function setAliossConfig($data, $status, $site_id = 1, $app_module = 'shop')
|
||||
{
|
||||
if ($status == 1) {
|
||||
event('CloseOss', []);//同步关闭所有云上传
|
||||
}
|
||||
|
||||
$config = new ConfigModel();
|
||||
$res = $config->setConfig($data, '阿里云OSS上传配置', $status, [ [ 'site_id', '=', $site_id ], [ 'app_module', '=', $app_module ], [ 'config_key', '=', 'ALIOSS_CONFIG' ] ]);
|
||||
return $res;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取阿里云上传配置
|
||||
*/
|
||||
public function getAliossConfig($site_id = 1, $app_module = 'shop')
|
||||
{
|
||||
$config = new ConfigModel();
|
||||
$res = $config->getConfig([ [ 'site_id', '=', $site_id ], [ 'app_module', '=', $app_module ], [ 'config_key', '=', 'ALIOSS_CONFIG' ] ]);
|
||||
return $res;
|
||||
}
|
||||
|
||||
/**
|
||||
* 配置阿里云开关状态
|
||||
* @param $status
|
||||
*/
|
||||
public function modifyConfigIsUse($status, $site_id = 1, $app_module = 'shop')
|
||||
{
|
||||
$config = new ConfigModel();
|
||||
$res = $config->modifyConfigIsUse($status, [ [ 'site_id', '=', $site_id ], [ 'app_module', '=', $app_module ], [ 'config_key', '=', 'ALIOSS_CONFIG' ] ]);
|
||||
return $res;
|
||||
}
|
||||
<?php
|
||||
|
||||
|
||||
namespace addon\alioss\model;
|
||||
|
||||
use app\model\system\Config as ConfigModel;
|
||||
use app\model\BaseModel;
|
||||
|
||||
/**
|
||||
* 阿里云配置
|
||||
*/
|
||||
class Config extends BaseModel
|
||||
{
|
||||
/**
|
||||
* 设置阿里云OSS上传配置
|
||||
* array $data
|
||||
*/
|
||||
public function setAliossConfig($data, $status, $site_id = 1, $app_module = 'shop')
|
||||
{
|
||||
if ($status == 1) {
|
||||
event('CloseOss', []);//同步关闭所有云上传
|
||||
}
|
||||
|
||||
$config = new ConfigModel();
|
||||
$res = $config->setConfig($data, '阿里云OSS上传配置', $status, [ [ 'site_id', '=', $site_id ], [ 'app_module', '=', $app_module ], [ 'config_key', '=', 'ALIOSS_CONFIG' ] ]);
|
||||
return $res;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取阿里云上传配置
|
||||
*/
|
||||
public function getAliossConfig($site_id = 1, $app_module = 'shop')
|
||||
{
|
||||
$config = new ConfigModel();
|
||||
$res = $config->getConfig([ [ 'site_id', '=', $site_id ], [ 'app_module', '=', $app_module ], [ 'config_key', '=', 'ALIOSS_CONFIG' ] ]);
|
||||
return $res;
|
||||
}
|
||||
|
||||
/**
|
||||
* 配置阿里云开关状态
|
||||
* @param $status
|
||||
*/
|
||||
public function modifyConfigIsUse($status, $site_id = 1, $app_module = 'shop')
|
||||
{
|
||||
$config = new ConfigModel();
|
||||
$res = $config->modifyConfigIsUse($status, [ [ 'site_id', '=', $site_id ], [ 'app_module', '=', $app_module ], [ 'config_key', '=', 'ALIOSS_CONFIG' ] ]);
|
||||
return $res;
|
||||
}
|
||||
}
|
||||
@@ -1,56 +1,49 @@
|
||||
<?php
|
||||
/**
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
*/
|
||||
|
||||
namespace addon\alioss\shop\controller;
|
||||
|
||||
use addon\alioss\model\Config as ConfigModel;
|
||||
use app\shop\controller\BaseShop;
|
||||
|
||||
/**
|
||||
* 七牛云上传管理
|
||||
*/
|
||||
class Config extends BaseShop
|
||||
{
|
||||
|
||||
/**
|
||||
* 云上传配置
|
||||
* @return mixed
|
||||
*/
|
||||
public function config()
|
||||
{
|
||||
$config_model = new ConfigModel();
|
||||
if (request()->isJson()) {
|
||||
$bucket = input('bucket', '');
|
||||
$access_key_id = input('access_key_id', '');
|
||||
$access_key_secret = input('access_key_secret', '');
|
||||
$endpoint = input('endpoint', '');
|
||||
$status = input('status', 0);
|
||||
$domain = input('domain', '');
|
||||
$is_domain = input('is_domain', 0);
|
||||
|
||||
$data = array (
|
||||
'bucket' => $bucket,
|
||||
'access_key_id' => $access_key_id,
|
||||
'access_key_secret' => $access_key_secret,
|
||||
'endpoint' => $endpoint,
|
||||
'domain' => $domain,
|
||||
'is_domain' => $is_domain
|
||||
);
|
||||
|
||||
$result = $config_model->setAliossConfig($data, $status, $this->site_id, $this->app_module);
|
||||
return $result;
|
||||
} else {
|
||||
$info_result = $config_model->getAliossConfig($this->site_id, $this->app_module);
|
||||
$info = $info_result['data'];
|
||||
$this->assign('info', $info);
|
||||
return $this->fetch('config/config');
|
||||
}
|
||||
}
|
||||
<?php
|
||||
|
||||
|
||||
namespace addon\alioss\shop\controller;
|
||||
|
||||
use addon\alioss\model\Config as ConfigModel;
|
||||
use app\shop\controller\BaseShop;
|
||||
|
||||
/**
|
||||
* 七牛云上传管理
|
||||
*/
|
||||
class Config extends BaseShop
|
||||
{
|
||||
|
||||
/**
|
||||
* 云上传配置
|
||||
* @return mixed
|
||||
*/
|
||||
public function config()
|
||||
{
|
||||
$config_model = new ConfigModel();
|
||||
if (request()->isJson()) {
|
||||
$bucket = input('bucket', '');
|
||||
$access_key_id = input('access_key_id', '');
|
||||
$access_key_secret = input('access_key_secret', '');
|
||||
$endpoint = input('endpoint', '');
|
||||
$status = input('status', 0);
|
||||
$domain = input('domain', '');
|
||||
$is_domain = input('is_domain', 0);
|
||||
|
||||
$data = array (
|
||||
'bucket' => $bucket,
|
||||
'access_key_id' => $access_key_id,
|
||||
'access_key_secret' => $access_key_secret,
|
||||
'endpoint' => $endpoint,
|
||||
'domain' => $domain,
|
||||
'is_domain' => $is_domain
|
||||
);
|
||||
|
||||
$result = $config_model->setAliossConfig($data, $status, $this->site_id, $this->app_module);
|
||||
return $result;
|
||||
} else {
|
||||
$info_result = $config_model->getAliossConfig($this->site_id, $this->app_module);
|
||||
$info = $info_result['data'];
|
||||
$this->assign('info', $info);
|
||||
return $this->fetch('config/config');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,38 +1,31 @@
|
||||
<?php
|
||||
/**
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
*/
|
||||
return [
|
||||
|
||||
// 自定义模板页面类型,格式:[ 'title' => '页面类型名称', 'name' => '页面标识', 'path' => '页面路径', 'value' => '页面数据,json格式' ]
|
||||
'template' => [],
|
||||
|
||||
// 后台自定义组件——装修
|
||||
'util' => [],
|
||||
|
||||
// 自定义页面路径
|
||||
'link' => [],
|
||||
|
||||
// 自定义图标库
|
||||
'icon_library' => [],
|
||||
|
||||
// uni-app 组件,格式:[ 'name' => '组件名称/文件夹名称', 'path' => '文件路径/目录路径' ],多个逗号隔开,自定义组件名称前缀必须是diy-,也可以引用第三方组件
|
||||
'component' => [],
|
||||
|
||||
// uni-app 页面,多个逗号隔开
|
||||
'pages' => [],
|
||||
|
||||
// 模板信息,格式:'title' => '模板名称', 'name' => '模板标识', 'cover' => '模板封面图', 'preview' => '模板预览图', 'desc' => '模板描述'
|
||||
'info' => [],
|
||||
|
||||
// 主题风格配色,格式可以自由定义扩展,【在uni-app中通过:this.themeStyle... 获取定义的颜色字段,例如:this.themeStyle.main_color】
|
||||
'theme' => [],
|
||||
|
||||
// 自定义页面数据,格式:[ 'title' => '页面名称', 'name' => "页面标识", 'value' => [页面数据,json格式] ]
|
||||
'data' => []
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
// 自定义模板页面类型,格式:[ 'title' => '页面类型名称', 'name' => '页面标识', 'path' => '页面路径', 'value' => '页面数据,json格式' ]
|
||||
'template' => [],
|
||||
|
||||
// 后台自定义组件——装修
|
||||
'util' => [],
|
||||
|
||||
// 自定义页面路径
|
||||
'link' => [],
|
||||
|
||||
// 自定义图标库
|
||||
'icon_library' => [],
|
||||
|
||||
// uni-app 组件,格式:[ 'name' => '组件名称/文件夹名称', 'path' => '文件路径/目录路径' ],多个逗号隔开,自定义组件名称前缀必须是diy-,也可以引用第三方组件
|
||||
'component' => [],
|
||||
|
||||
// uni-app 页面,多个逗号隔开
|
||||
'pages' => [],
|
||||
|
||||
// 模板信息,格式:'title' => '模板名称', 'name' => '模板标识', 'cover' => '模板封面图', 'preview' => '模板预览图', 'desc' => '模板描述'
|
||||
'info' => [],
|
||||
|
||||
// 主题风格配色,格式可以自由定义扩展,【在uni-app中通过:this.themeStyle... 获取定义的颜色字段,例如:this.themeStyle.main_color】
|
||||
'theme' => [],
|
||||
|
||||
// 自定义页面数据,格式:[ 'title' => '页面名称', 'name' => "页面标识", 'value' => [页面数据,json格式] ]
|
||||
'data' => []
|
||||
];
|
||||
@@ -1,50 +1,43 @@
|
||||
<?php
|
||||
/**
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
*/
|
||||
return [
|
||||
'bind' => [
|
||||
|
||||
],
|
||||
|
||||
'listen' => [
|
||||
//支付异步回调
|
||||
'PayNotify' => [
|
||||
'addon\alipay\event\PayNotify'
|
||||
],
|
||||
//支付方式,后台查询
|
||||
'PayType' => [
|
||||
'addon\alipay\event\PayType'
|
||||
],
|
||||
//支付,前台应用
|
||||
'Pay' => [
|
||||
'addon\alipay\event\Pay'
|
||||
],
|
||||
'PayClose' => [
|
||||
'addon\alipay\event\PayClose'
|
||||
],
|
||||
'PayRefund' => [
|
||||
'addon\alipay\event\PayRefund'
|
||||
],
|
||||
'PayTransfer' => [
|
||||
'addon\alipay\event\PayTransfer'
|
||||
],
|
||||
'TransferType' => [
|
||||
'addon\alipay\event\TransferType'
|
||||
],
|
||||
'AuthcodePay' => [
|
||||
'addon\alipay\event\AuthcodePay'
|
||||
],
|
||||
'PayOrderQuery' => [
|
||||
'addon\alipay\event\PayOrderQuery'
|
||||
],
|
||||
],
|
||||
|
||||
'subscribe' => [
|
||||
],
|
||||
];
|
||||
<?php
|
||||
|
||||
return [
|
||||
'bind' => [
|
||||
|
||||
],
|
||||
|
||||
'listen' => [
|
||||
//支付异步回调
|
||||
'PayNotify' => [
|
||||
'addon\alipay\event\PayNotify'
|
||||
],
|
||||
//支付方式,后台查询
|
||||
'PayType' => [
|
||||
'addon\alipay\event\PayType'
|
||||
],
|
||||
//支付,前台应用
|
||||
'Pay' => [
|
||||
'addon\alipay\event\Pay'
|
||||
],
|
||||
'PayClose' => [
|
||||
'addon\alipay\event\PayClose'
|
||||
],
|
||||
'PayRefund' => [
|
||||
'addon\alipay\event\PayRefund'
|
||||
],
|
||||
'PayTransfer' => [
|
||||
'addon\alipay\event\PayTransfer'
|
||||
],
|
||||
'TransferType' => [
|
||||
'addon\alipay\event\TransferType'
|
||||
],
|
||||
'AuthcodePay' => [
|
||||
'addon\alipay\event\AuthcodePay'
|
||||
],
|
||||
'PayOrderQuery' => [
|
||||
'addon\alipay\event\PayOrderQuery'
|
||||
],
|
||||
],
|
||||
|
||||
'subscribe' => [
|
||||
],
|
||||
];
|
||||
|
||||
@@ -1,21 +1,13 @@
|
||||
<?php
|
||||
/**
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
*/
|
||||
return [
|
||||
'name' => 'alipay',
|
||||
'title' => '支付宝支付',
|
||||
'description' => '支付宝支付功能',
|
||||
'type' => 'system', //插件类型 system :系统插件(自动安装), promotion:营销插件 tool:工具插件
|
||||
'status' => 1,
|
||||
'author' => '',
|
||||
'version' => '5.3.1',
|
||||
'version_no' => '525231212001',
|
||||
'content' => '',
|
||||
<?php
|
||||
|
||||
return [
|
||||
'name' => 'alipay',
|
||||
'title' => '支付宝支付',
|
||||
'description' => '支付宝支付功能',
|
||||
'type' => 'system', //插件类型 system :系统插件(自动安装), promotion:营销插件 tool:工具插件
|
||||
'status' => 1,
|
||||
'author' => '',
|
||||
'version' => '5.3.1',
|
||||
'version_no' => '525231212001',
|
||||
'content' => '',
|
||||
];
|
||||
@@ -1,23 +1,16 @@
|
||||
<?php
|
||||
/**
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
*/
|
||||
return [
|
||||
[
|
||||
'name' => 'ALI_PAY_CONFIG',
|
||||
'title' => '支付宝支付编辑',
|
||||
'url' => 'alipay://shop/pay/config',
|
||||
'parent' => 'CONFIG_PAY',
|
||||
'is_show' => 0,
|
||||
'is_control' => 1,
|
||||
'is_icon' => 0,
|
||||
'picture' => '',
|
||||
'picture_select' => '',
|
||||
'sort' => 1,
|
||||
],
|
||||
];
|
||||
<?php
|
||||
|
||||
return [
|
||||
[
|
||||
'name' => 'ALI_PAY_CONFIG',
|
||||
'title' => '支付宝支付编辑',
|
||||
'url' => 'alipay://shop/pay/config',
|
||||
'parent' => 'CONFIG_PAY',
|
||||
'is_show' => 0,
|
||||
'is_control' => 1,
|
||||
'is_icon' => 0,
|
||||
'picture' => '',
|
||||
'picture_select' => '',
|
||||
'sort' => 1,
|
||||
],
|
||||
];
|
||||
|
||||
@@ -1,27 +1,18 @@
|
||||
<?php
|
||||
/**
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
*/
|
||||
|
||||
|
||||
namespace addon\alipay\event;
|
||||
|
||||
/**
|
||||
* 应用安装
|
||||
*/
|
||||
class Install
|
||||
{
|
||||
/**
|
||||
* 执行安装
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
return success();
|
||||
}
|
||||
<?php
|
||||
|
||||
|
||||
namespace addon\alipay\event;
|
||||
|
||||
/**
|
||||
* 应用安装
|
||||
*/
|
||||
class Install
|
||||
{
|
||||
/**
|
||||
* 执行安装
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
return success();
|
||||
}
|
||||
}
|
||||
@@ -1,33 +1,26 @@
|
||||
<?php
|
||||
/**
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
*/
|
||||
|
||||
namespace addon\alipay\event;
|
||||
|
||||
use addon\alipay\model\Pay as PayModel;
|
||||
|
||||
/**
|
||||
* 生成支付
|
||||
*/
|
||||
class Pay
|
||||
{
|
||||
/**
|
||||
* 支付方式及配置
|
||||
*/
|
||||
public function handle($param)
|
||||
{
|
||||
if ($param[ "pay_type" ] == "alipay") {
|
||||
if (in_array($param[ "app_type" ], [ "h5", "app", "pc", "aliapp", 'wechat' ])) {
|
||||
$pay_model = new PayModel($param[ 'site_id' ], $param[ "app_type" ] == 'aliapp');
|
||||
$res = $pay_model->pay($param);
|
||||
return $res;
|
||||
}
|
||||
}
|
||||
}
|
||||
<?php
|
||||
|
||||
|
||||
namespace addon\alipay\event;
|
||||
|
||||
use addon\alipay\model\Pay as PayModel;
|
||||
|
||||
/**
|
||||
* 生成支付
|
||||
*/
|
||||
class Pay
|
||||
{
|
||||
/**
|
||||
* 支付方式及配置
|
||||
*/
|
||||
public function handle($param)
|
||||
{
|
||||
if ($param[ "pay_type" ] == "alipay") {
|
||||
if (in_array($param[ "app_type" ], [ "h5", "app", "pc", "aliapp", 'wechat' ])) {
|
||||
$pay_model = new PayModel($param[ 'site_id' ], $param[ "app_type" ] == 'aliapp');
|
||||
$res = $pay_model->pay($param);
|
||||
return $res;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,39 +1,32 @@
|
||||
<?php
|
||||
/**
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
*/
|
||||
|
||||
namespace addon\alipay\event;
|
||||
|
||||
use addon\alipay\model\Pay as PayModel;
|
||||
|
||||
/**
|
||||
* 关闭支付
|
||||
*/
|
||||
class PayClose
|
||||
{
|
||||
/**
|
||||
* 关闭支付
|
||||
* @param $params
|
||||
* @return \addon\alipay\model\multitype|array
|
||||
*/
|
||||
public function handle($params)
|
||||
{
|
||||
// if ($params["pay_type"] == "alipay") {
|
||||
try {
|
||||
$pay_model = new PayModel($params[ 'site_id' ]);
|
||||
$result = $pay_model->close($params);
|
||||
return $result;
|
||||
} catch (\Exception $e) {
|
||||
return error(-1, $e->getMessage());
|
||||
} catch (\Throwable $e) {
|
||||
return error(-1, $e->getMessage());
|
||||
}
|
||||
// }
|
||||
}
|
||||
<?php
|
||||
|
||||
|
||||
namespace addon\alipay\event;
|
||||
|
||||
use addon\alipay\model\Pay as PayModel;
|
||||
|
||||
/**
|
||||
* 关闭支付
|
||||
*/
|
||||
class PayClose
|
||||
{
|
||||
/**
|
||||
* 关闭支付
|
||||
* @param $params
|
||||
* @return \addon\alipay\model\multitype|array
|
||||
*/
|
||||
public function handle($params)
|
||||
{
|
||||
if ($params["pay_type"] == "alipay") {
|
||||
try {
|
||||
$pay_model = new PayModel($params[ 'site_id' ]);
|
||||
$result = $pay_model->close($params);
|
||||
return $result;
|
||||
} catch (\Exception $e) {
|
||||
return error(-1, $e->getMessage());
|
||||
} catch (\Throwable $e) {
|
||||
return error(-1, $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,41 +1,36 @@
|
||||
<?php
|
||||
/**
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
*/
|
||||
|
||||
namespace addon\alipay\event;
|
||||
|
||||
use addon\alipay\model\Pay as PayModel;
|
||||
use app\model\system\Pay as PayCommon;
|
||||
|
||||
/**
|
||||
* 支付回调
|
||||
*/
|
||||
class PayNotify
|
||||
{
|
||||
/**
|
||||
* 支付方式及配置
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
if (isset($_POST[ 'out_trade_no' ])) {
|
||||
$out_trade_no = $_POST[ 'out_trade_no' ];
|
||||
$pay = new PayCommon();
|
||||
$pay_info = $pay->getPayInfo($out_trade_no)[ 'data' ];
|
||||
if (empty($pay_info)) return false;
|
||||
|
||||
if ($_POST[ 'total_amount' ] != $pay_info[ 'pay_money' ]) {
|
||||
return false;
|
||||
}
|
||||
$mch_info = empty($pay_info[ 'mch_info' ]) ? [] : json_decode($pay_info[ 'mch_info' ], true);
|
||||
|
||||
$pay_model = new PayModel($pay_info[ 'site_id' ], $mch_info[ 'is_aliapp' ] ?? 0);
|
||||
$pay_model->payNotify();
|
||||
}
|
||||
}
|
||||
<?php
|
||||
|
||||
|
||||
namespace addon\alipay\event;
|
||||
|
||||
use addon\alipay\model\Pay as PayModel;
|
||||
use app\model\system\Pay as PayCommon;
|
||||
|
||||
/**
|
||||
* 支付回调
|
||||
*/
|
||||
class PayNotify
|
||||
{
|
||||
/**
|
||||
* 支付方式及配置
|
||||
*/
|
||||
public function handle($param)
|
||||
{
|
||||
if ($param[ "pay_type" ] != "alipay") return false;
|
||||
|
||||
if (isset($_POST[ 'out_trade_no' ])) {
|
||||
$out_trade_no = $_POST[ 'out_trade_no' ];
|
||||
$pay = new PayCommon();
|
||||
$pay_info = $pay->getPayInfo($out_trade_no)[ 'data' ];
|
||||
if (empty($pay_info)) return false;
|
||||
|
||||
if ($_POST[ 'total_amount' ] != $pay_info[ 'pay_money' ]) {
|
||||
return false;
|
||||
}
|
||||
$mch_info = empty($pay_info[ 'mch_info' ]) ? [] : json_decode($pay_info[ 'mch_info' ], true);
|
||||
|
||||
$pay_model = new PayModel($pay_info[ 'site_id' ], $mch_info[ 'is_aliapp' ] ?? 0);
|
||||
$pay_model->payNotify();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,34 +1,27 @@
|
||||
<?php
|
||||
/**
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
*/
|
||||
|
||||
namespace addon\alipay\event;
|
||||
|
||||
use addon\alipay\model\Config as ConfigModel;
|
||||
use addon\alipay\model\Pay as PayModel;
|
||||
use app\model\system\Pay;
|
||||
|
||||
/**
|
||||
* 查询支付结果
|
||||
*/
|
||||
class PayOrderQuery
|
||||
{
|
||||
public function handle(array $params)
|
||||
{
|
||||
$pay_info = ( new Pay() )->getInfo([ [ 'id', '=', $params[ 'relate_id' ] ] ])[ 'data' ];
|
||||
if (!empty($pay_info)) {
|
||||
$config_model = new ConfigModel();
|
||||
$pay_config = $config_model->getPayConfig($pay_info[ 'site_id' ])[ 'data' ][ 'value' ];
|
||||
if (!empty($pay_config) && $pay_config[ 'pay_status' ] != 2) {
|
||||
$pay_common = new PayModel($pay_info[ 'site_id' ]);
|
||||
$pay_common->orderQuery($pay_info);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
<?php
|
||||
|
||||
|
||||
namespace addon\alipay\event;
|
||||
|
||||
use addon\alipay\model\Config as ConfigModel;
|
||||
use addon\alipay\model\Pay as PayModel;
|
||||
use app\model\system\Pay;
|
||||
|
||||
/**
|
||||
* 查询支付结果
|
||||
*/
|
||||
class PayOrderQuery
|
||||
{
|
||||
public function handle(array $params)
|
||||
{
|
||||
$pay_info = ( new Pay() )->getInfo([ [ 'id', '=', $params[ 'relate_id' ] ] ])[ 'data' ];
|
||||
if (!empty($pay_info)) {
|
||||
$config_model = new ConfigModel();
|
||||
$pay_config = $config_model->getPayConfig($pay_info[ 'site_id' ])[ 'data' ][ 'value' ];
|
||||
if (!empty($pay_config) && $pay_config[ 'pay_status' ] != 2) {
|
||||
$pay_common = new PayModel($pay_info[ 'site_id' ]);
|
||||
$pay_common->orderQuery($pay_info);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,33 +1,26 @@
|
||||
<?php
|
||||
/**
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
*/
|
||||
|
||||
namespace addon\alipay\event;
|
||||
|
||||
use addon\alipay\model\Pay as PayModel;
|
||||
|
||||
/**
|
||||
* 原路退款
|
||||
*/
|
||||
class PayRefund
|
||||
{
|
||||
/**
|
||||
* 关闭支付
|
||||
*/
|
||||
public function handle($params)
|
||||
{
|
||||
if ($params[ "pay_info" ][ "pay_type" ] == "alipay") {
|
||||
$mch_info = empty($params[ 'pay_info' ][ 'mch_info' ]) ? [] : json_decode($params[ 'pay_info' ][ 'mch_info' ], true);
|
||||
|
||||
$pay_model = new PayModel($params[ 'site_id' ], $mch_info[ 'is_aliapp' ] ?? 0);
|
||||
$result = $pay_model->refund($params);
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
<?php
|
||||
|
||||
|
||||
namespace addon\alipay\event;
|
||||
|
||||
use addon\alipay\model\Pay as PayModel;
|
||||
|
||||
/**
|
||||
* 原路退款
|
||||
*/
|
||||
class PayRefund
|
||||
{
|
||||
/**
|
||||
* 关闭支付
|
||||
*/
|
||||
public function handle($params)
|
||||
{
|
||||
if ($params[ "pay_info" ][ "pay_type" ] == "alipay") {
|
||||
$mch_info = empty($params[ 'pay_info' ][ 'mch_info' ]) ? [] : json_decode($params[ 'pay_info' ][ 'mch_info' ], true);
|
||||
|
||||
$pay_model = new PayModel($params[ 'site_id' ], $mch_info[ 'is_aliapp' ] ?? 0);
|
||||
$result = $pay_model->refund($params);
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,42 +1,35 @@
|
||||
<?php
|
||||
/**
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
*/
|
||||
|
||||
namespace addon\alipay\event;
|
||||
|
||||
use addon\alipay\model\Pay;
|
||||
use addon\alipay\model\Config;
|
||||
|
||||
class PayTransfer
|
||||
{
|
||||
public function handle(array $params)
|
||||
{
|
||||
if ($params[ 'transfer_type' ] == 'alipay') {
|
||||
$pay = new Pay($params[ 'site_id' ]);
|
||||
|
||||
$config_model = new Config();
|
||||
$config_result = $config_model->getPayConfig($params[ 'site_id' ]);
|
||||
$config = $config_result[ "data" ];
|
||||
if (!empty($config[ 'value' ])) {
|
||||
$config_info = $config[ "value" ];
|
||||
$countersign_type = $config_info['countersign_type'] ?? 0;
|
||||
if ($countersign_type == 0) {
|
||||
$res = $pay->payTransfer($params);
|
||||
return $res;
|
||||
} else {
|
||||
$res = $pay->payNewTransfer($params);
|
||||
return $res;
|
||||
}
|
||||
} else {
|
||||
$res = $pay->payTransfer($params);
|
||||
return $res;
|
||||
}
|
||||
}
|
||||
}
|
||||
<?php
|
||||
|
||||
|
||||
namespace addon\alipay\event;
|
||||
|
||||
use addon\alipay\model\Pay;
|
||||
use addon\alipay\model\Config;
|
||||
|
||||
class PayTransfer
|
||||
{
|
||||
public function handle(array $params)
|
||||
{
|
||||
if ($params[ 'transfer_type' ] == 'alipay') {
|
||||
$pay = new Pay($params[ 'site_id' ]);
|
||||
|
||||
$config_model = new Config();
|
||||
$config_result = $config_model->getPayConfig($params[ 'site_id' ]);
|
||||
$config = $config_result[ "data" ];
|
||||
if (!empty($config[ 'value' ])) {
|
||||
$config_info = $config[ "value" ];
|
||||
$countersign_type = $config_info['countersign_type'] ?? 0;
|
||||
if ($countersign_type == 0) {
|
||||
$res = $pay->payTransfer($params);
|
||||
return $res;
|
||||
} else {
|
||||
$res = $pay->payNewTransfer($params);
|
||||
return $res;
|
||||
}
|
||||
} else {
|
||||
$res = $pay->payTransfer($params);
|
||||
return $res;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user