97 Commits
main ... dev

Author SHA1 Message Date
f42b4a1036 chore(addon/aikefu): 更新配置UI 2025-12-06 17:14:17 +08:00
7d3d71e0e3 chore(addon/aikefu): 更新文档说明及功能 2025-12-06 16:57:33 +08:00
6a44d27fd3 test(addon/aikefu): 还原原先的kefu.app 方便测试 2025-12-06 16:54:34 +08:00
a90a081973 test(addon/aikefu): 还原原先的kefu.app 方便测试 2025-12-06 16:52:44 +08:00
4736273902 test(addon/aikefu): 还原原先的kefu.app 方便测试 2025-12-06 16:45:44 +08:00
fba01f4909 test(addon/aikefu): 还原原先的kefu.app 方便测试 2025-12-06 16:44:37 +08:00
42aa934493 test(addon/aikefu): 还原原先的kefu.app 方便测试 2025-12-06 16:42:45 +08:00
fe73fdd5bd test(addon/aikefu): 还原原先的kefu.app 方便测试 2025-12-06 16:37:37 +08:00
c112f02fcc chore(addon/aikefu): 更新文档说明及功能 2025-12-06 16:32:29 +08:00
4d2467ae36 chore(addon/aikefu): 更新文档说明及功能 2025-12-06 16:24:55 +08:00
0b6e6914fd chore(addon/aikefu): 变更API暴漏的端点 2025-12-06 16:12:12 +08:00
0f76b61152 fix(addon/aikefu): 使用ThinkPHP Model来处理数据 2025-12-06 15:13:24 +08:00
6cfff15c62 fix(addon/aikefu): 使用curl来发送请求 2025-12-06 14:55:10 +08:00
6d3887ec06 fix(addon/aikefu): 使用uniacid方便以后迁移,唯一值 2025-12-06 14:47:26 +08:00
5af0b07775 fix(addon/aikefu): 支持外部传入site_id 等参数 2025-12-06 14:34:17 +08:00
8ae10dd2c3 fix(addon/aikefu): 获得当前配置有问题 2025-12-06 14:16:13 +08:00
17c1ce2cc6 chore(addon/aikefu): 更新控制器及更新事件 2025-12-06 14:10:27 +08:00
a209dc8080 chore(addon/aikefu): 更新控制器及更新事件 2025-12-06 14:06:12 +08:00
1d4fff13a1 chore(addon/aikefu): 增加统一的事件配置 2025-12-06 13:52:45 +08:00
a811e36635 chore(addon/aikefu): 调整事件名称 2025-12-06 13:41:37 +08:00
d8a0dd5d31 chore(addon/aikefu): 调整config的配置内容 2025-12-06 13:25:15 +08:00
0ff979917c chore(addon/aikefu): 调整config的配置内容 2025-12-06 12:52:30 +08:00
fc5615a9c7 chore(addon/aikefu): 更新获取配置及保存逻辑 2025-12-06 11:59:53 +08:00
cdcd9eeffa chore(addon/aikefu): update html 2025-12-06 11:51:16 +08:00
c0da89735c chore(db): 升级数据包含新增的智能客服插件sql脚本 2025-12-06 11:43:34 +08:00
fc34d83692 chore(addon/aikefu): 更新info 2025-12-06 10:15:40 +08:00
8ceb252d79 feat(addon/aikefu): 新增AI智能客服插件 2025-12-06 10:09:08 +08:00
8da4563435 chore(docker): update supervisord.conf 2025-12-06 09:30:42 +08:00
045e6ab3df chore(addon): 支付相关判断是否平台开通配置 2025-12-05 17:37:12 +08:00
5591e17446 chore(docker): fix supervisord.conf 2025-12-05 17:27:48 +08:00
8f783fd765 fix(docker): 针对已经存在的容器或后期新建的容器权限设置更新 2025-12-05 17:01:50 +08:00
6b41e46f30 chore(addon): weapp 插件与niushopV5部分代码同步比较 2025-12-05 16:04:46 +08:00
ff89fdf5e9 chore(addon): 新增wechat插件 2025-12-05 15:52:21 +08:00
402c425575 chore(src): 更新代码注释 2025-12-05 15:05:54 +08:00
ee2785f972 chore(addon/huawiepay): 增强huaweipay 的预下单及单元测试 2025-12-05 15:05:20 +08:00
bbb0271a5e chore(scripts): 更新生成私钥和公钥的脚本处理 2025-12-05 15:04:10 +08:00
75ff4bb0a4 chore(docker): 更新supervisord.conf 针对Docker容器重启后,设置权限 2025-12-05 15:03:38 +08:00
776f0ed029 fix(addon): alipay 及 wechatpay 没有判断支付类型 2025-12-04 11:19:14 +08:00
eef56291eb fix(addon/wechatpay): 修复微信支付回调错误 2025-12-04 11:05:08 +08:00
8e159edf1d chore(addon/huaweipay): mch_id -> merc_no 2025-12-04 11:00:24 +08:00
1793e4b2aa fix(pay): 修复PayNotify 没有正确的传回参数 2025-12-04 10:32:42 +08:00
a793ed541b chore(addon/huaweipay): 更新huaweipay 2025-12-04 10:32:00 +08:00
09ed1bd427 chore(vendor): 依赖 Vendor 升级测试 2025-12-04 08:39:14 +08:00
ff666975da chore(docker): 更新升级sql,把华为支付及线下支付添加到数据库中 2025-12-03 15:50:20 +08:00
b4403cedd9 chore(addon): 增强针对插件列表的处理 2025-12-03 15:45:08 +08:00
98d2eb8a2a chore(addon/huaweipay): 变更华为支付的测试方法内容 2025-12-03 15:34:19 +08:00
ae5f56c16f chore(event): 更新event 默认的初始化路由等操作 2025-12-03 15:33:33 +08:00
d374034694 chore(docker): 增加升级数据库脚本 2025-12-03 15:07:23 +08:00
b0f399c814 chore(docker): 暂时存储upgrade.sql 2025-12-03 14:51:21 +08:00
8489ef35cb chore(docker): 更新初始化脚本 2025-12-03 14:40:51 +08:00
7d8c2d4e37 chore(event): 使用 log_write 记录日志 2025-12-03 11:47:42 +08:00
ec60eee8fe chore(event): 更新InitAddon, 跟踪初始化事件监听 2025-12-03 11:31:55 +08:00
dac15250a2 chore(event): 更新InitAddon,要求手动添加了addon,也会更新缓存,添加到监听事件中 2025-12-03 11:12:13 +08:00
fec0198537 chore(docker): 更新数据库初始化脚本 2025-12-03 10:48:53 +08:00
f5ac4d10c0 chore(addon/huaweipay): 支持证书文本填写模式 2025-12-03 10:37:39 +08:00
ca07a6cea5 chore(doc): 添加华为支付插件说明文档 2025-12-03 09:42:54 +08:00
b5d89aef72 fix(docker): 只处理 runtime 及 upload目录 2025-12-03 09:02:24 +08:00
d151d45e99 fix(sms): 显示出验证码 2025-12-03 08:56:35 +08:00
3525af81bf chore(docker): update dockerfile 2025-12-03 08:52:06 +08:00
01c86ce0a3 chore(docker): 更新初始化脚本 2025-12-03 08:47:20 +08:00
e5e619a241 chore(docker): 更新PHP Dockerfile 2025-12-03 08:45:35 +08:00
d9b10c1621 chore(docker): update .gitignore 2025-12-03 08:34:11 +08:00
c6e72e5b79 chore(sms): 更新sms 2025-12-02 18:05:39 +08:00
980effc420 fix(docker): 去除不用的目录 2025-12-02 18:01:11 +08:00
e4040a27e7 chore(src): 还原src\app\model\upload\Upload.php 2025-12-02 17:31:37 +08:00
23170dcc3f fix(docker): 修复PHP进程使用的是Web服务用户,而没有相应权限创建目录的问题 2025-12-02 17:29:26 +08:00
64c92857a6 chore(src): 更新上传的判断路径的逻辑 2025-12-02 17:19:41 +08:00
4346bfebb7 chore(src): 使用绝对路径来判断权限 2025-12-02 17:18:00 +08:00
c24271c075 chore(src): 检查路径的权限 2025-12-02 17:15:02 +08:00
526982c431 chore(src): 更新上传错误,检查更全面 2025-12-02 17:12:05 +08:00
c161bc55e5 chore(src): 针对上传权限,提供更多有价值的信息 2025-12-02 17:07:35 +08:00
eb79ad260c chore(src): 所有代码上传 2025-12-02 15:36:42 +08:00
ce8e59902c chore(docker): 更新本地docker-compose.local.yml配置 2025-12-02 15:17:52 +08:00
b8ed400d12 chore(docker): 更新.gitignore 2025-12-02 15:08:54 +08:00
5f48980d31 chore(docker): 根据不同环境区分docker容器及网络,完全隔离 2025-12-02 15:00:06 +08:00
b81e7b8b1b chore(docker): 更新.gitignore文件 2025-12-02 14:01:16 +08:00
a468c0919d chore(docker): 更新.gitignore文件 2025-12-02 14:00:17 +08:00
1ae5b46523 chore(docker): 更新.env.development 2025-12-02 11:33:38 +08:00
ecac45a74a chore(docker): 更新数据库初始化脚本 2025-12-02 11:32:22 +08:00
51c4cb7767 chore(src): 将database.php 纳入代码管理中 2025-12-02 11:24:40 +08:00
ae7cfebb44 chore(docker): 更新docker配置 2025-12-02 11:10:00 +08:00
e4ccbbcbd1 chore(docker): 更新环境变量 2025-12-02 10:46:21 +08:00
bdfcd1cedb chore(docker): 更新docker的网络设置 2025-12-02 10:21:43 +08:00
6a7b465944 chore(docker): 更新supervisord.conf 2025-12-02 10:19:06 +08:00
41ac96630c chore(env): 更新.env.development文件配置 2025-12-02 10:04:24 +08:00
3a8fbc3e1b chore(docker): 更新nignx volumes 设置 2025-12-02 09:55:49 +08:00
e3e57ee154 chore(docker): 更新挂载点 2025-12-02 09:53:42 +08:00
0883c1318b chore(docker): 使用挂载点,确保目录能够正常被读写 2025-12-02 09:47:34 +08:00
fe79d04343 chore(docker): 更新docker-compose.yml 的volumes 使用主机目录持久化 2025-12-02 09:37:14 +08:00
2857283558 chore(docker): 为dev准备部署条件 2025-12-02 09:28:19 +08:00
981779126c chore(addon-huaweipay): 支持华为支付参数配置 2025-12-01 15:28:38 +08:00
39ce5882cb chore(scripts): 新增生成私有公有证书的nodejs脚本 2025-12-01 15:27:30 +08:00
e51f6c6544 chore(addon): 添加线下支付及华为支付基本配置 2025-12-01 11:36:51 +08:00
3a7f510e19 chore(db): 比较于niushop的差异 2025-11-29 17:51:17 +08:00
1c9e72e28d chore: 新增replace_comments.py 脚本,使用python replace_comments.py --path ./src来去掉版权注释及空注释 2025-11-29 16:45:45 +08:00
9d79b2585e chore(docs): 添加GIT分支管理及发布流程 2025-11-29 10:45:24 +08:00
1fc9a39ffe chore(docker): 指定容器内目录是可读可写的 2025-11-29 10:39:30 +08:00
4502 changed files with 597796 additions and 113102 deletions

27
.env.development Normal file
View File

@@ -0,0 +1,27 @@
# 项目配置, 请根据实际情况修改
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=9105
XDEBUG_POST=9108
# 数据库配置
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
NGINX_SSL_PORT=8052

View File

@@ -1,24 +1,26 @@
# 项目配置, 请根据实际情况修改
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
PHP_FPM_PORT=9100
XDEBUG_POST=9103
# 数据库配置
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
NGINX_SSL_PORT=8012

27
.env.local Normal file
View File

@@ -0,0 +1,27 @@
# 项目配置, 请根据实际情况修改
PROJECT_NAME=newshop
# ThinkPHP 6.x 配置, 请根据实际情况修改
APP_ENV=local
# PHP/PHP-FPM 配置
PHP_VERSION=7.4
PHP_FPM_VERSION=7.4-fpm
PHP_FPM_PORT=9100
XDEBUG_POST=9103
# 数据库配置
MYSQL_ROOT_HOST=%
MYSQL_DATABASE=shop_dev
MYSQL_USER=shop_mallnew
MYSQL_PASSWORD=shop_mallnew
MYSQL_PORT=3316
# Redis 配置
REDIS_PASSWORD=luckyshop123!@#
REDIS_PORT=6399
# Nginx 配置
NGINX_PORT=8010
NGINX_SSL_PORT=8012

3
.gitignore vendored
View File

@@ -18,6 +18,9 @@ __pycache__
.idea
.vscode
# 环境变量
.env
# 源码结构
debug.txt
.travis.yml

43
README.md Normal file
View File

@@ -0,0 +1,43 @@
# 在线商城PHP项目
## Docker 部署
```bash
cp .env.example .env.development
```
**注意**
- 在同一目录下面,执行 `docker-compose` 命令时,需要指定项目名称。用来区分不同的环境。如 `shop_local``shop_dev` 等。
- 本地部署时,需要将 `APP_ENV` 设置为 `local`
- 开发环境部署时,需要将 `APP_ENV` 设置为 `development`
## 环境变量
- `APP_ENV`: 应用环境,默认值为 `development`
## 开发环境-local 部署
```bash
# 本地部署时,需要将 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 部署
```bash
# 默认使用 docker-compose.yml 文件
docker-compose --project-name shop_development --env-file .env.development up -d
# docker-compose down 命令,用来停止并删除容器
docker-compose --project-name shop_development down -v
```

126
docker-compose.local.yml Normal file
View File

@@ -0,0 +1,126 @@
# 特别说明本地local环境方便操作所以未使用的统一的docker-compose.yml文件只保留了local环境的配置
x-shared-env: &shared-api-env
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:-rootpassword}
MYSQL_ROOT_HOST: ${MYSQL_ROOT_HOST:-'%'} # 允许root从任何主机连接
MYSQL_DATABASE: ${MYSQL_DATABASE:-shop_mallnew}
MYSQL_USER: ${MYSQL_USER:-shop_mallnew}
MYSQL_PASSWORD: ${MYSQL_PASSWORD:-shop_mallnew}
REDIS_PASSWORD: ${REDIS_PASSWORD:-luckyshop123!@#}
REDIS_PORT: ${REDIS_PORT:-6379}
# 将服务归类到目录 A 中
services:
php-fpm:
build:
context: ./docker/php
dockerfile: Dockerfile
container_name: ${PROJECT_NAME}_php
restart: always
extra_hosts:
- "host.docker.internal:host-gateway" # 支持主机名解析
environment:
PHP_ENV: ${PHP_ENV:-development}
# 环境变量, APP_ENV 应用于 ThinkPHP 6.x 框架, .env.local 要想启用,需要在项目根目录下创建 .env.local 文件,并将 APP_ENV 设置为 local
# 同理,如果要启用开发环境,则将 APP_ENV 设置为 development如果要启用生产环境则将 APP_ENV 设置为 production
# 不然ThinkPHP 6.x 系列,会只加载 .env 文件,而不会加载 .env.local 文件,导致 .env.local 文件中的配置不会生效
APP_ENV: ${APP_ENV:-local}
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
volumes:
- ./:/var/www/all_source
- ./src:/var/www/html
# 更新下载源列表以加速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'"]
interval: 30s
timeout: 10s
retries: 3
start_period: 60s
networks:
- sass-platform-net
labels:
- "com.docker.compose.project.working_dir=${PROJECT_NAME}"
nginx:
build:
context: ./docker/nginx
dockerfile: Dockerfile
container_name: ${PROJECT_NAME}_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
# 创建临时目录
- /var/www/server/nginx/proxy_temp_dir
- /var/www/server/nginx/proxy_cache_dir
depends_on:
- php-fpm
networks:
- sass-platform-net
labels:
- "com.docker.compose.project.working_dir=${PROJECT_NAME}"
db:
image: mysql:5.7.44
container_name: ${PROJECT_NAME}_mysql
environment:
<<: *shared-api-env
volumes:
- mysql_db_data:/var/lib/mysql
- ./docker/mysql/init:/docker-entrypoint-initdb.d
- ./docker/mysql/my.cnf:/etc/mysql/conf.d/custom.cnf
ports:
- ${MYSQL_PORT:-3306}:3306
networks:
- sass-platform-net
restart: unless-stopped
command:
- --character-set-server=utf8mb4
- --collation-server=utf8mb4_unicode_ci
- --innodb_buffer_pool_size=256M
labels:
- "com.docker.compose.project.working_dir=${PROJECT_NAME}"
# Redis 服务(可选)
redis:
image: redis:8.2
container_name: ${PROJECT_NAME}_redis
environment:
REDIS_PASSWORD: ${REDIS_PASSWORD:-luckyshop123!@#}
REDISCLI_AUTH: ${REDIS_PASSWORD:-luckyshop123!@#}
ports:
- "${REDIS_PORT:-6379}:6379"
volumes:
- redis_data:/data
- ./docker/redis/redis.conf:/etc/redis/redis.conf
networks:
- sass-platform-net
restart: unless-stopped
labels:
- "com.docker.compose.project.working_dir=${PROJECT_NAME}"
volumes:
mysql_db_data:
redis_data:
xdebug_logs:
networks:
sass-platform-net:
driver: bridge

View File

@@ -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" # 支持主机名解析
@@ -49,18 +49,19 @@ 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 +73,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 +94,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 +112,32 @@ 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:
name: ${PROJECT_NAME}_${APP_ENV}_redis_data
driver: local
driver_opts:
type: none
o: bind
device: ./docker/redis_data/${APP_ENV}
xdebug_logs:
name: ${PROJECT_NAME}_${APP_ENV}_xdebug_logs
driver: local
driver_opts:
type: none
o: bind
device: ./docker/xdebug_logs/${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

10
docker/mysql_db_data/.gitignore vendored Normal file
View File

@@ -0,0 +1,10 @@
# 忽略目录下所有文件和子目录
*
# 忽略所有子目录
*/
# 但不忽略 .gitkeep 文件
!.gitkeep
# 不忽略 .gitignore 文件自身
!.gitignore
# 不忽略 development/.gitkeep 文件
!development/.gitkeep

View File

View File

@@ -27,6 +27,7 @@ RUN apt-get update && apt-get install -y \
libfreetype6-dev \
libjpeg62-turbo-dev \
libpng-dev \
iputils-ping \
&& rm -rf /var/lib/apt/lists/*
# 安装 PHP 扩展
@@ -66,21 +67,9 @@ RUN echo "zend_extension=xdebug.so" > /usr/local/etc/php/conf.d/xdebug.ini
# 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
############ 查看 cron 进程
## 查看 cron 进程
# ps aux | grep "think cron:schedule"
@@ -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"]

59
docker/php/entrypoint.sh Normal file
View File

@@ -0,0 +1,59 @@
#!/bin/bash
set -e
# 设置全局umask
umask 0002
echo "=== ThinkPHP Docker权限初始化 ==="
# 修复目录所有权和权限
fix_directory_permissions() {
local dir=$1
echo "修复目录权限: $dir"
# 确保目录存在
mkdir -p "$dir"
# 设置所有权
chown -R www-data:www-data "$dir"
# 设置权限
chmod -R 775 "$dir"
# 设置setgid权限
chmod g+s "$dir"
# 尝试设置ACL如果支持
if command -v setfacl >/dev/null 2>&1; then
setfacl -d -m u:www-data:rwx -m u:root:rwx "$dir" 2>/dev/null || true
setfacl -Rm u:www-data:rwx "$dir" 2>/dev/null || true
fi
echo "$dir 权限设置完成"
}
# 处理所有需要权限的目录
directories=("runtime" "upload")
for dir in "${directories[@]}"; do
fix_directory_permissions "/var/www/html/$dir"
done
# 验证权限
echo "=== 权限验证 ==="
echo "当前用户: $(whoami)"
echo "当前UID: $(id -u), GID: $(id -g)"
echo "当前umask: $(umask)"
# 测试写入权限
sudo -u www-data mkdir -p /var/www/html/runtime/test_dir 2>/dev/null && \
echo "✅ runtime目录新建子目录测试通过" || \
echo "❌ runtime目录新建子目录失败"
sudo -u www-data mkdir -p /var/www/html/upload/test_dir 2>/dev/null && \
echo "✅ upload目录新建子目录测试通过" || \
echo "❌ upload目录新建子目录失败"
echo "=== 启动应用 ==="
# 执行原有的启动命令
exec "$@"

View File

@@ -6,25 +6,40 @@ logfile_backups=10
loglevel=info
pidfile=/var/run/supervisord.pid
[program:chmod]
command=/bin/bash -c "while true; do chmod -R 775 /var/www/html/runtime/ /var/www/html/upload/ 2>/dev/null || true; sleep 30; done"
autostart=true
autorestart=true
stopasgroup=true
killasgroup=true
[program:php-fpm]
command=php-fpm
autostart=true
autorestart=true
startretries=3
startsecs=1
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
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

10
docker/redis_data/.gitignore vendored Normal file
View File

@@ -0,0 +1,10 @@
# 忽略目录下所有文件和子目录
*
# 忽略所有子目录
*/
# 但不忽略 .gitkeep 文件
!.gitkeep
# 不忽略 .gitignore 文件自身
!.gitignore
# 不忽略 development/.gitkeep 文件
!development/.gitkeep

View File

10
docker/xdebug_logs/.gitignore vendored Normal file
View File

@@ -0,0 +1,10 @@
# 忽略目录下所有文件和子目录
*
# 忽略所有子目录
*/
# 但不忽略 .gitkeep 文件
!.gitkeep
# 不忽略 .gitignore 文件自身
!.gitignore
# 不忽略 development/.gitkeep 文件
!development/.gitkeep

View File

128
docs/GIT_REALEASE.md Normal file
View 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 成熟度),我可以给出更定制化的建议!

View File

@@ -0,0 +1,26 @@
# 华为支付插件
## Demo
```
安卓快应用ID115644647
安卓快应用包名com.jieganfsj.fivegshop
安卓快应用名称:秸秆粉碎机
商户名称:徐州明文机械有限公司
商户号102751500028
开发者ID10086000901972225
支付ID10086000901972225
公钥:
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=
```
支付密钥用于交易过程中的签名认证,请妥善保管,谨防泄露,签名认证规范参考开发指南服务端开发。

665
docs/api_kefu.md Normal file
View File

@@ -0,0 +1,665 @@
# 智能客服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`
**请求方式**GET
**请求参数**
| 参数名 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| uniacid | int | 是 | 站点ID |
| check_type | string | 否 | 检查类型full完整、basic基础、ai_serviceAI服务默认full |
**响应示例**
```json
{
"code": 0,
"message": "healthy",
"data": {
"status": "healthy",
"check_id": "health_63a8f9c1234abcd",
"timestamp": "2023-12-25 10:30:45",
"total_checks": 4,
"passed_checks": 4,
"failed_checks": 0,
"response_time_ms": 156.78,
"components": {
"database": {
"status": "healthy",
"message": "数据库连接正常",
"response_time_ms": 12.34,
"details": {
"connection": "success",
"query_test": "passed"
}
},
"ai_service_config": {
"status": "healthy",
"message": "AI服务配置正常",
"response_time_ms": 8.56,
"details": {
"configured": true,
"complete": true,
"enabled": true,
"base_url": "https://api.example.com"
}
},
"ai_service_connection": {
"status": "healthy",
"message": "AI服务连接正常",
"response_time_ms": 45.67,
"details": {
"http_status": 200,
"url": "https://api.example.com"
}
},
"system_resources": {
"status": "healthy",
"message": "系统资源正常",
"response_time_ms": 2.34,
"details": {
"php_version": "8.1.0",
"memory_usage": "45.67 MB",
"memory_limit": "512.00 MB",
"memory_usage_percent": "8.92%",
"max_execution_time": "30s"
}
}
},
"warnings": [],
"errors": []
}
}
```
### 2. 获取服务配置信息
**接口地址**`/api/kefu/info`
**请求方式**GET
**请求参数**
| 参数名 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| uniacid | int | 否 | 站点ID |
| member_id | int | 否 | 会员ID |
| token | string | 否 | 访问令牌 |
**响应示例**
```json
{
"code": 0,
"message": "success",
"data": {
"service_info": {
"name": "智能客服",
"version": "1.0.0",
"enabled": true,
"status": "enabled"
},
"features": {
"chat": true,
"chat_stream": true,
"conversation_management": true,
"history_management": true
},
"limits": {
"max_message_length": 4000,
"max_conversation_history": 100,
"rate_limit": {
"requests_per_minute": 60,
"requests_per_hour": 1000
}
},
"endpoints": {
"chat": "/api/kefu/chat",
"chat_stream": "/api/kefu/chatStream",
"create_conversation": "/api/kefu/createConversation",
"get_history": "/api/kefu/getHistory",
"clear_conversation": "/api/kefu/clearConversation",
"health": "/api/kefu/health",
"info": "/api/kefu/info"
},
"api_config": {
"base_url": "https://api.dify.ai/v1",
"chat_endpoint": "/chat-messages",
"supports_streaming": true,
"authentication": "bearer_token"
},
"client_info": {
"user_agent": "Mozilla/5.0...",
"ip": "192.168.1.100",
"timestamp": 1703505845
},
"server_info": {
"php_version": "8.1.0",
"server_time": "2023-12-25 10:30:45",
"timezone": "Asia/Shanghai"
},
"user_stats": {
"can_use_service": true,
"member_id": 123,
"site_id": 1
}
}
}
```
### 3. 智能客服聊天接口
**接口地址**`/api/kefu/chat`
**请求方式**POST
**请求参数**
| 参数名 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| uniacid | int | 是 | 站点ID |
| message | string | 是 | 用户输入的消息内容 |
| user_id | string | 否 | 用户ID默认使用当前登录会员ID |
| conversation_id | string | 否 | 会话ID第一次聊天可不传系统会自动创建 |
| stream | bool | 否 | 是否使用流式响应默认false |
| member_id | int | 否 | 会员ID |
| token | string | 否 | 访问令牌 |
**响应示例**
```json
{
"code": 0,
"message": "success",
"data": {
"conversation_id": "conv_123456789",
"reply": "您好,我是智能客服,有什么可以帮助您的?",
"message_id": "msg_123456789",
"finish_reason": "stop",
"usage": {
"prompt_tokens": 10,
"completion_tokens": 20,
"total_tokens": 30
}
}
}
```
### 4. 智能客服流式聊天接口
**接口地址**`/api/kefu/chatStream`
**请求方式**POST
**请求参数**:同 `/api/kefu/chat` 接口
**响应格式**Server-Sent Events (SSE)
**响应示例**
```javascript
// 开始事件
event: start
data: {
"id": "unique_id",
"event": "start",
"timestamp": 1703505845,
"data": {
"request_id": "stream_123",
"message": "开始处理请求"
}
}
// 内容块事件
event: message
data: {
"id": "unique_id",
"event": "message",
"timestamp": 1703505845,
"data": {
"content": "您",
"conversation_id": "conv_123",
"finished": false
}
}
// 完成事件
event: complete
data: {
"id": "unique_id",
"event": "complete",
"timestamp": 1703505845,
"data": {
"conversation_id": "conv_123",
"message_id": "msg_456",
"usage": {},
"finish_reason": "stop"
}
}
// 结束事件
event: end
data: {
"id": "unique_id",
"event": "end",
"timestamp": 1703505845,
"data": {
"request_id": "stream_123",
"status": "completed"
}
}
```
### 5. 创建新会话
**接口地址**`/api/kefu/createConversation`
**请求方式**POST
**请求参数**
| 参数名 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| uniacid | int | 是 | 站点ID |
| user_id | string | 否 | 用户ID默认使用当前登录会员ID |
| member_id | int | 否 | 会员ID |
| token | string | 否 | 访问令牌 |
**响应示例**
```json
{
"code": 0,
"message": "success",
"data": {
"conversation_id": "conv_123456789",
"name": "智能客服会话",
"created_at": "2023-12-25 10:30:45"
}
}
```
### 6. 获取会话历史
**接口地址**`/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": 1,
"role": "user",
"content": "你好",
"create_time": "2023-12-25 10:30:45"
},
{
"id": 2,
"role": "assistant",
"content": "您好,我是智能客服,有什么可以帮助您的?",
"create_time": "2023-12-25 10:30:46"
}
],
"total": 2,
"limit": 20,
"offset": 0
}
}
```
### 7. 清除会话历史
**接口地址**`/api/kefu/clearConversation`
**请求方式**POST
**请求参数**
| 参数名 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| uniacid | int | 是 | 站点ID |
| conversation_id | string | 否 | 会话ID与user_id二选一 |
| user_id | string | 否 | 用户ID用于清除该用户所有会话与conversation_id二选一 |
| member_id | int | 否 | 会员ID |
| token | string | 否 | 访问令牌 |
**响应示例**
```json
{
"code": 0,
"message": "success",
"data": {
"deleted_messages": 15,
"deleted_conversations": 3
}
}
```
## 四、前端调用示例
### Uniapp调用示例
```javascript
// 引入请求封装(根据项目实际情况调整)
import { request } from '@/utils/request';
// 1. 获取服务配置信息
async function getAIInfo() {
try {
const res = await request({
url: '/api/kefu/info',
method: 'GET',
data: {
uniacid: 1
}
});
if (res.code === 0) {
console.log('AI服务状态:', res.data.service_info);
console.log('可用功能:', res.data.features);
return res.data;
} else {
console.error('获取配置失败:', res.message);
return null;
}
} catch (error) {
console.error('获取配置请求失败:', error);
return null;
}
}
// 2. 智能客服聊天(普通模式)
async function chatWithAI(message, conversationId = '') {
try {
const res = await request({
url: '/api/kefu/chat',
method: 'POST',
data: {
uniacid: 1,
message: message,
conversation_id: conversationId
// user_id: 'your-user-id', // 可选
// stream: false // 可选
}
});
if (res.code === 0) {
return res.data;
} else {
console.error('聊天失败:', res.message);
return null;
}
} catch (error) {
console.error('聊天请求失败:', error);
return null;
}
}
// 3. 智能客服聊天(流式模式)
async function chatWithAIStream(message, conversationId = '', onMessage, onComplete, onError) {
try {
const response = await uni.request({
url: '/api/kefu/chatStream',
method: 'POST',
data: {
uniacid: 1,
message: message,
conversation_id: conversationId
},
responseType: 'text'
});
// 处理流式响应
if (response.statusCode === 200) {
const lines = response.data.split('\n');
for (const line of lines) {
if (line.startsWith('data: ')) {
try {
const data = JSON.parse(line.substring(6));
if (data.event === 'message' && onMessage) {
onMessage(data.data);
} else if (data.event === 'complete' && onComplete) {
onComplete(data.data);
} else if (data.event === 'error' && onError) {
onError(data.data);
}
} catch (e) {
console.warn('解析流式数据失败:', e);
}
}
}
}
} catch (error) {
console.error('流式聊天请求失败:', error);
if (onError) onError({ error: error.message });
}
}
// 4. 创建新会话
async function createNewConversation() {
try {
const res = await request({
url: '/api/kefu/createConversation',
method: 'POST',
data: {
uniacid: 1
// user_id: 'your-user-id', // 可选
}
});
if (res.code === 0) {
return res.data.conversation_id;
} else {
console.error('创建会话失败:', res.message);
return null;
}
} catch (error) {
console.error('创建会话请求失败:', error);
return null;
}
}
// 5. 获取会话历史
async function getChatHistory(conversationId, limit = 20, offset = 0) {
try {
const res = await request({
url: '/api/kefu/getHistory',
method: 'POST',
data: {
uniacid: 1,
conversation_id: conversationId,
limit: limit,
offset: offset
// user_id: 'your-user-id', // 可选
}
});
if (res.code === 0) {
return res.data;
} else {
console.error('获取历史记录失败:', res.message);
return null;
}
} catch (error) {
console.error('获取历史记录请求失败:', error);
return null;
}
}
// 6. 清除会话历史
async function clearConversation(conversationId = '', userId = '') {
try {
const res = await request({
url: '/api/kefu/clearConversation',
method: 'POST',
data: {
uniacid: 1,
conversation_id: conversationId,
user_id: userId
}
});
if (res.code === 0) {
return res.data;
} else {
console.error('清除会话失败:', res.message);
return null;
}
} catch (error) {
console.error('清除会话请求失败:', error);
return null;
}
}
// 7. 健康检查
async function checkHealth(checkType = 'full') {
try {
const res = await request({
url: '/api/kefu/health',
method: 'GET',
data: {
uniacid: 1,
check_type: checkType
}
});
if (res.code === 0) {
return res.data;
} else {
console.error('健康检查失败:', res.message);
return null;
}
} catch (error) {
console.error('健康检查请求失败:', error);
return null;
}
}
```
## 五、使用流程
1. **初始化检查**:小程序端启动时,调用`health``info`接口检查服务状态
2. **创建会话**:进入客服页面时,调用`createConversation`接口创建新会话或使用本地存储的会话ID
3. **发送消息**:用户输入消息后,调用`chat``chatStream`接口发送消息,获取机器人回复
4. **显示消息**:将用户消息和机器人回复显示在聊天界面
5. **加载历史记录**:需要时调用`getHistory`接口加载历史消息
6. **维护会话**保持会话ID用于后续消息交流
7. **清理数据**:根据用户需求调用`clearConversation`接口清理历史数据
## 六、注意事项
1. **必填参数**:所有接口都需要`uniacid`站点ID参数
2. **事件驱动**:后端采用事件驱动架构,所有业务逻辑通过事件处理器执行
3. **安全性**请确保Dify API密钥的安全性不要泄露给前端
4. **用户标识**建议对用户ID进行加密处理避免直接使用敏感信息
5. **流式支持**:推荐使用`chatStream`接口获得更好的用户体验
6. **会话管理**:建议实现会话管理机制,定期清理过期会话
7. **频率限制**:建议添加请求频率限制,防止恶意请求
8. **生产环境**在生产环境中建议关闭DEBUG模式
## 七、测试建议
1. **基础检查**:首先调用`health`接口检查系统状态
2. **配置验证**:调用`info`接口验证配置信息
3. **接口测试**使用Postman或类似工具测试各个API接口
4. **流式测试**:测试`chatStream`接口的流式响应
5. **完整流程**:在小程序端集成并测试完整流程
6. **边界测试**:模拟不同场景下的用户输入,测试机器人回复效果
7. **压力测试**:测试接口在高并发情况下的表现
## 八、常见问题
### 1. 接口返回400错误
**原因**:缺少必填参数`uniacid`或参数格式错误
**解决方法**确保请求中包含有效的站点ID
### 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
## 九、性能优化建议
1. **缓存配置**:可对`info`接口返回的配置信息进行客户端缓存
2. **连接复用**HTTP请求使用连接池减少建立连接的开销
3. **压缩传输**启用gzip压缩减少传输数据量
4. **分页加载**:历史记录使用分页加载,避免一次性加载大量数据
5. **CDN加速**静态资源使用CDN加速访问
6. **监控告警**:建立接口性能监控和告警机制

View 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()

File diff suppressed because it is too large Load Diff

11578
docs/db/niushop_database.sql Normal file

File diff suppressed because it is too large Load Diff

View 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()

170
replace_comments.py Normal file
View 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()

View 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);

View File

@@ -1,25 +1,22 @@
APP_DEBUG = true
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
DEBUG = true
[RRDATABASE]
HOSTNAME = host.docker.internal
[redis]
HOST = 127.0.0.1
HOST = redis
PORT = 6379
PASSWORD = ''
PASSWORD = 'luckyshop123!@#'
EXPIRY = 604800

View File

@@ -6,7 +6,7 @@ DEFAULT_TIMEZONE = Asia/Shanghai
default_lang = zh-cn
[DATABASE]
TYPE = mysql
HOSTNAME = newshop_mysql
HOSTNAME = db
DATABASE = shop_dev
USERNAME = shop_mallnew
PASSWORD = shop_mallnew
@@ -14,9 +14,9 @@ HOSTPORT = 3306
CHARSET = utf8
DEBUG = true
[RRDATABASE]
HOSTNAME = 192.168.2.64
HOSTNAME = redis
[redis]
HOST = newshop_redis
HOST = redis
PORT = 6379
PASSWORD = 'luckyshop123!@#'
EXPIRY = 604800

View File

@@ -0,0 +1,313 @@
<?php
namespace addon\aikefu\api\controller;
use addon\aikefu\model\Config as KefuConfigModel;
use addon\aikefu\model\Conversation as KefuConversationModel;
use addon\aikefu\model\Message as KefuMessageModel;
use app\api\controller\BaseApi;
class Kefu extends BaseApi
{
/**
* 封装curl请求方法
* @param string $url 请求URL
* @param string $method 请求方法
* @param array $data 请求数据
* @param array $headers 请求头
* @return string 响应内容
*/
private function curlRequest($url, $method = 'GET', $data = [], $headers = [])
{
$ch = curl_init();
// 设置URL
curl_setopt($ch, CURLOPT_URL, $url);
// 设置请求方法
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
// 设置POST数据
if ($method === 'POST' && !empty($data)) {
curl_setopt($ch, CURLOPT_POSTFIELDS, is_array($data) ? json_encode($data) : $data);
}
// 设置请求头
if (!empty($headers)) {
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
}
// 设置返回值
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
curl_setopt($ch, CURLOPT_TIMEOUT, 30);
// 执行请求
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
// 关闭连接
curl_close($ch);
if ($response === false) {
throw new \Exception('Curl请求失败');
}
if ($httpCode >= 400) {
throw new \Exception('HTTP请求失败状态码' . $httpCode);
}
return $response;
}
/**
* 为事件调用初始化属性
* @param array $data 事件数据
*/
public function initializeForEvent($data)
{
if (!empty($data['site_id'])) {
$this->site_id = $data['site_id'] ?? 0;
}
if (!empty($data['member_id'])) {
$this->member_id = $data['member_id'] ?? 0;
}
if (!empty($data['token'])) {
$this->token = $data['token'] ?? '';
}
$this->params = [
'message' => $data['message'] ?? '',
'user_id' => $data['user_id'] ?? '',
'conversation_id' => $data['conversation_id'] ?? '',
'stream' => $data['stream'] ?? false,
];
}
/**
* 智能客服聊天接口
* @return \think\response\Json
*/
public function chat()
{
// 获取请求参数
$message = $this->params['message'] ?? '';
$user_id = $this->params['user_id'] ?? $this->member_id;
$conversation_id = $this->params['conversation_id'] ?? '';
$stream = $this->params['stream'] ?? false;
// 验证参数
if (empty($message)) {
return $this->response($this->error('请输入消息内容'));
}
try {
// 获取智能客服配置
$kefu_config_model = new KefuConfigModel();
$config_info = $kefu_config_model->getConfig($this->site_id)['data']['value'] ?? [];
if (empty($config_info) || $config_info['status'] != 1) {
return $this->response($this->error('智能客服暂未启用'));
}
$config = $config_info;
$apiKey = $config['api_key'];
$baseUrl = $config['base_url'];
$chatEndpoint = $config['chat_endpoint'];
// 构建请求数据
$requestData = [
'inputs' => [],
'query' => $message,
'response_mode' => $stream ? 'streaming' : 'blocking',
'user' => $user_id,
];
// 如果有会话ID添加到请求中
if (!empty($conversation_id)) {
$requestData['conversation_id'] = $conversation_id;
}
// 构建请求头
$headers = [
'Authorization: Bearer ' . $apiKey,
'Content-Type: application/json',
];
// 发送请求到Dify API
$url = $baseUrl . $chatEndpoint;
$response = $this->curlRequest($url, 'POST', $requestData, $headers);
// 解析响应
$result = json_decode($response, true);
if (json_last_error() !== JSON_ERROR_NONE) {
return $this->response($this->error('解析响应失败'));
}
// 保存消息记录
$kefu_message_model = new KefuMessageModel();
$kefu_conversation_model = new KefuConversationModel();
// 保存用户消息
$kefu_message_model->addMessage([
'site_id' => $this->site_id,
'user_id' => $user_id,
'conversation_id' => $result['conversation_id'] ?? $conversation_id,
'message_id' => $result['message_id'] ?? '',
'role' => 'user',
'content' => $message,
]);
// 保存机器人回复
$kefu_message_model->addMessage([
'site_id' => $this->site_id,
'user_id' => $user_id,
'conversation_id' => $result['conversation_id'] ?? $conversation_id,
'message_id' => $result['id'] ?? '',
'role' => 'assistant',
'content' => $result['answer'] ?? '',
]);
// 更新会话状态或创建新会话
$conversation_info = $kefu_conversation_model->getConversationInfo([
['site_id', '=', $this->site_id],
['conversation_id', '=', $result['conversation_id'] ?? $conversation_id],
]);
if (empty($conversation_info['data'])) {
// 创建新会话
$kefu_conversation_model->addConversation([
'site_id' => $this->site_id,
'user_id' => $user_id,
'conversation_id' => $result['conversation_id'] ?? '',
'name' => '智能客服会话',
]);
} else {
// 更新会话状态
$kefu_conversation_model->updateConversation([
'status' => 1,
], [
['id', '=', $conversation_info['data']['id']],
]);
}
// 返回成功响应
return $this->response($this->success([
'conversation_id' => $result['conversation_id'] ?? '',
'reply' => $result['answer'] ?? '',
'message_id' => $result['message_id'] ?? '',
'finish_reason' => $result['finish_reason'] ?? '',
'usage' => $result['usage'] ?? [],
]));
} catch (\Exception $e) {
return $this->response($this->error('请求失败:' . $e->getMessage()));
}
}
/**
* 获取会话历史
* @return \think\response\Json
*/
public function getHistory()
{
// 获取请求参数
$conversation_id = $this->params['conversation_id'] ?? '';
$user_id = $this->params['user_id'] ?? $this->member_id;
$limit = $this->params['limit'] ?? 20;
$offset = $this->params['offset'] ?? 0;
// 验证参数
if (empty($conversation_id)) {
return $this->response($this->error('会话ID不能为空'));
}
try {
// 获取会话历史记录
$kefu_message_model = new KefuMessageModel();
$message_list = $kefu_message_model->getMessageList([
['site_id', '=', $this->site_id],
['user_id', '=', $user_id],
['conversation_id', '=', $conversation_id],
], 'id, role, content, create_time', 'create_time asc', $limit, $offset);
// 返回成功响应
return $this->response($this->success([
'messages' => $message_list['data'] ?? [],
'total' => $message_list['total'] ?? 0,
'limit' => $limit,
'offset' => $offset,
]));
} catch (\Exception $e) {
return $this->response($this->error('请求失败:' . $e->getMessage()));
}
}
/**
* 创建新会话
* @return \think\response\Json
*/
public function createConversation()
{
// 获取请求参数
$user_id = $this->params['user_id'] ?? $this->member_id;
try {
// 获取智能客服配置
$kefu_config_model = new KefuConfigModel();
$config_info = $kefu_config_model->getConfig($this->site_id)['data']['value'] ?? [];
if (empty($config_info) || $config_info['status'] != 1) {
return $this->response($this->error('智能客服暂未启用'));
}
$config = $config_info;
$apiKey = $config['api_key'];
$baseUrl = $config['base_url'];
// 构建请求数据
$requestData = [
'name' => '智能客服会话',
'user' => $user_id,
];
// 构建请求头
$headers = [
'Authorization: Bearer ' . $apiKey,
'Content-Type: application/json',
];
// 发送请求到Dify API
$url = $baseUrl . '/conversations';
$response = $this->curlRequest($url, 'POST', $requestData, $headers);
// 解析响应
$result = json_decode($response, true);
if (json_last_error() !== JSON_ERROR_NONE) {
return $this->response($this->error('解析响应失败'));
}
// 保存会话记录
$kefu_conversation_model = new KefuConversationModel();
$kefu_conversation_model->addConversation([
'site_id' => $this->site_id,
'user_id' => $user_id,
'conversation_id' => $result['id'] ?? '',
'name' => $result['name'] ?? '智能客服会话',
]);
// 返回成功响应
return $this->response($this->success([
'conversation_id' => $result['id'] ?? '',
'name' => $result['name'] ?? '',
'created_at' => $result['created_at'] ?? '',
]));
} catch (\Exception $e) {
return $this->response($this->error('请求失败:' . $e->getMessage()));
}
}
}

View File

@@ -0,0 +1,36 @@
<?php
/**
* 智能客服扩展事件配置
*/
return [
'bind' => [
],
'listen' => [
'KefuChat' => [
'addon\aikefu\event\KefuChat'
],
'KefuCreateConversation' => [
'addon\aikefu\event\KefuCreateConversation'
],
'KefuGetHistory' => [
'addon\aikefu\event\KefuGetHistory'
],
'KefuClearConversation' => [
'addon\aikefu\event\KefuClearConversation'
],
'KefuHealthCheck' => [
'addon\aikefu\event\KefuHealthCheck'
],
'KefuChatStream' => [
'addon\aikefu\event\KefuChatStream'
],
'KefuGetInfo' => [
'addon\aikefu\event\KefuGetInfo'
],
],
'subscribe' => [
],
];

View 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' => '',
];

View File

@@ -0,0 +1,42 @@
-- 智能客服插件安装脚本
-- 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_unicode_ci COMMENT='智能客服会话表';
-- 创建智能客服消息表
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 '消息内容',
`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_unicode_ci COMMENT='智能客服消息表';
-- 修改表字符集utf8mb4_unicode_ci 兼容emoji表情
ALTER TABLE lucky_aikefu_message MODIFY content TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
ALTER TABLE lucky_aikefu_conversation CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
ALTER TABLE lucky_aikefu_message CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

View File

@@ -0,0 +1,4 @@
-- 智能客服插件卸载脚本
-- 删除智能客服相关表(配置信息存储在系统配置表中,无需单独删除)
DROP TABLE IF EXISTS `lucky_aikefu_message`;
DROP TABLE IF EXISTS `lucky_aikefu_conversation`;

View 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);
}
}

View File

@@ -0,0 +1,39 @@
<?php
namespace addon\aikefu\event;
use addon\aikefu\api\controller\Kefu as KefuApi;
/**
* 客服聊天
*/
class KefuChat
{
/**
* 处理智能客服聊天事件
* @param array $data 事件数据
* @return array
*/
public function handle($data)
{
try {
// 创建addon的KefuApi实例
$kefu_api = new KefuApi();
// 调用初始化方法设置属性
$kefu_api->initializeForEvent($data);
// 调用addon的chat方法
$response = $kefu_api->chat();
// 返回响应数据
return json_decode($response->getContent(), true);
} catch (\Exception $e) {
return [
'code' => -1,
'message' => '聊天失败:' . $e->getMessage(),
'data' => []
];
}
}
}

View File

@@ -0,0 +1,235 @@
<?php
namespace addon\aikefu\event;
use addon\aikefu\model\Config as KefuConfigModel;
use addon\aikefu\model\Conversation as KefuConversationModel;
use addon\aikefu\model\Message as KefuMessageModel;
/**
* 智能客服流式聊天
*/
class KefuChatStream
{
/**
* 处理智能客服流式聊天事件
* @param array $data 事件数据
* @return array
*/
public function handle($data)
{
$message = $data['message'] ?? '';
$user_id = $data['user_id'] ?? '';
$conversation_id = $data['conversation_id'] ?? '';
$site_id = $data['site_id'] ?? 0;
$request_id = $data['request_id'] ?? '';
try {
// 验证参数
if (empty($message)) {
return [
[
'type' => 'error',
'message' => '消息内容不能为空'
]
];
}
// 获取智能客服配置
$kefu_config_model = new KefuConfigModel();
$config_info = $kefu_config_model->getConfig($site_id)['data']['value'] ?? [];
if (empty($config_info) || $config_info['status'] != 1) {
return [
[
'type' => 'error',
'message' => '智能客服暂未启用'
]
];
}
$config = $config_info;
$apiKey = $config['api_key'];
$baseUrl = $config['base_url'];
$chatEndpoint = $config['chat_endpoint'];
// 构建请求数据
$requestData = [
'inputs' => [],
'query' => $message,
'response_mode' => 'streaming',
'user' => $user_id,
];
if (!empty($conversation_id)) {
$requestData['conversation_id'] = $conversation_id;
}
// 构建请求头
$headers = [
'Authorization: Bearer ' . $apiKey,
'Content-Type: application/json',
'Accept: text/event-stream',
];
// 发送流式请求到Dify API
$url = $baseUrl . $chatEndpoint;
$result = $this->executeStreamRequest($url, $requestData, $headers);
return $result;
} catch (\Exception $e) {
return [
[
'type' => 'error',
'message' => '请求失败:' . $e->getMessage()
]
];
}
}
/**
* 执行流式请求
*/
private function executeStreamRequest($url, $requestData, $headers)
{
$ch = curl_init();
// 设置curl选项
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($requestData));
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_WRITEFUNCTION, [$this, 'streamCallback']);
curl_setopt($ch, CURLOPT_TIMEOUT, 120); // 设置较长的超时时间
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
// 执行请求
$result = [];
$this->stream_buffer = '';
$this->conversation_id = '';
$this->current_message_id = '';
$response = curl_exec($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$error = curl_error($ch);
curl_close($ch);
if ($response === false || !empty($error)) {
return [
[
'type' => 'error',
'message' => '请求失败:' . $error
]
];
}
if ($http_code >= 400) {
return [
[
'type' => 'error',
'message' => '服务端错误HTTP状态码' . $http_code
]
];
}
return $this->parseStreamResponse($this->stream_buffer);
}
/**
* 流式回调函数
*/
private function streamCallback($ch, $data)
{
$this->stream_buffer .= $data;
return strlen($data);
}
/**
* 解析流式响应
*/
private function parseStreamResponse($response)
{
$result = [];
$lines = explode("\n", $response);
$conversation_id = '';
$message_id = '';
$buffer = '';
foreach ($lines as $line) {
$line = trim($line);
if (empty($line)) {
continue;
}
if (strpos($line, 'data: ') === 0) {
$data_str = substr($line, 6);
if ($data_str === '[DONE]') {
// 流结束,发送完成事件
$result[] = [
'type' => 'complete',
'conversation_id' => $conversation_id,
'message_id' => $message_id,
'content' => $buffer,
'finished' => true
];
break;
}
$data = json_decode($data_str, true);
if (json_last_error() === JSON_ERROR_NONE && isset($data)) {
// 提取会话ID和消息ID
if (isset($data['conversation_id'])) {
$conversation_id = $data['conversation_id'];
}
if (isset($data['message_id'])) {
$message_id = $data['message_id'];
}
// 处理内容块
if (isset($data['answer'])) {
$buffer .= $data['answer'];
$result[] = [
'type' => 'chunk',
'content' => $data['answer'],
'conversation_id' => $conversation_id,
'message_id' => $message_id,
'finished' => false
];
}
// 检查是否结束
if (isset($data['finish_reason']) && $data['finish_reason'] !== 'null') {
$result[] = [
'type' => 'complete',
'conversation_id' => $conversation_id,
'message_id' => $message_id,
'content' => $buffer,
'finish_reason' => $data['finish_reason'],
'usage' => $data['usage'] ?? [],
'finished' => true
];
break;
}
}
}
}
// 如果没有收到[DONE]信号,确保发送完成事件
if (empty($result) || end($result)['type'] !== 'complete') {
$result[] = [
'type' => 'complete',
'conversation_id' => $conversation_id,
'message_id' => $message_id,
'content' => $buffer,
'finished' => true
];
}
return $result;
}
}

View 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' => []
];
}
}
}

View File

@@ -0,0 +1,81 @@
<?php
namespace addon\aikefu\event;
use addon\aikefu\api\controller\Kefu as KefuApi;
/**
* 处理智能客服创建会话事件
*/
class KefuCreateConversation
{
/**
* 处理智能客服创建会话事件
* @param array $data 事件数据
* @return array
*/
public function handle($data)
{
try {
// 创建addon的KefuApi实例
$kefu_api = new KefuApi();
// 调用初始化方法设置属性
$kefu_api->initializeForEvent($data);
// 调用addon的createConversation方法
$response = $kefu_api->createConversation();
// 返回响应数据
return json_decode($response->getContent(), true);
} catch (\Exception $e) {
return [
'code' => -1,
'message' => '创建会话失败:' . $e->getMessage(),
'data' => []
];
}
}
/**
* 处理智能客服获取历史消息事件
* @param array $data 事件数据
* @return array
*/
public function handleKefuGetHistory($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' => []
];
}
}
/**
* 事件监听映射
* @return array
*/
public function subscribe()
{
return [
'KefuChat' => 'handleKefuChat',
'KefuCreateConversation' => 'handleKefuCreateConversation',
'KefuGetHistory' => 'handleKefuGetHistory',
];
}
}

View 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' => []
];
}
}
}

View File

@@ -0,0 +1,127 @@
<?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',
'chat_stream' => '/api/kefu/chatStream',
'create_conversation' => '/api/kefu/createConversation',
'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()
]
];
}
}
}

View 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;
}
}

View 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);
}
}

File diff suppressed because one or more lines are too long

View 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;
}
}

View 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);
}
}

View File

@@ -0,0 +1,208 @@
<?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
* @return array
*/
public function getConversationMessages($site_id, $conversation_id, $limit = 20, $offset = 0)
{
$condition = [
['site_id', '=', $site_id],
['conversation_id', '=', $conversation_id]
];
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]);
}
}

View File

@@ -0,0 +1,230 @@
<?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", "");//Dify API密钥
$base_url = input("base_url", "https://api.dify.ai/v1");//API基础地址
$chat_endpoint = input("chat_endpoint", "/chat-messages");//聊天接口端点
$status = input("status", 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", 1);
$limit = input("limit", 10);
$user_id = input("user_id", "");
$status = input("status", "");
$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);
return $this->success($conversation_list);
}
/**
* 获取会话信息
* @return \think\response\Json
*/
public function getConversationInfo()
{
$conversation_id = input("conversation_id", "");
if (empty($conversation_id)) {
return $this->error('会话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 $this->error('会话不存在');
}
return $this->success($conversation_info);
}
/**
* 结束会话
* @return \think\response\Json
*/
public function endConversation()
{
$id = input("id", "");
if (empty($id)) {
return $this->error('会话ID不能为空');
}
$kefu_conversation_model = new KefuConversationModel();
$result = $kefu_conversation_model->updateConversation(
['status' => 0],
[
['id', '=', $id],
['site_id', '=', $this->site_id]
]
);
return $this->success($result);
}
/**
* 删除会话
* @return \think\response\Json
*/
public function deleteConversation()
{
$id = input("id", "");
if (empty($id)) {
return $this->error('会话ID不能为空');
}
$kefu_conversation_model = new KefuConversationModel();
$kefu_message_model = new KefuMessageModel();
// 开启事务
Db::startTrans();
try {
// 删除会话关联的消息
$conversation_info = $kefu_conversation_model->getConversationInfo([
['id', '=', $id],
['site_id', '=', $this->site_id]
]);
if (!empty($conversation_info)) {
$kefu_message_model->deleteMessage([
['site_id', '=', $this->site_id],
['conversation_id', '=', $conversation_info['conversation_id']]
]);
}
// 删除会话
$result = $kefu_conversation_model->deleteConversation([
['id', '=', $id],
['site_id', '=', $this->site_id]
]);
// 提交事务
Db::commit();
return $this->success($result);
} catch (\Exception $e) {
// 回滚事务
Db::rollback();
return $this->error($e->getMessage());
}
}
/**
* 消息管理列表
* @return \think\response\View
*/
public function message()
{
$conversation_id = input("conversation_id", "");
View::assign("conversation_id", $conversation_id);
return $this->fetch("kefu/message");
}
/**
* 获取消息列表
* @return \think\response\Json
*/
public function getMessageList()
{
$page = input("page", 1);
$limit = input("limit", 50);
$conversation_id = input("conversation_id", "");
if (empty($conversation_id)) {
return $this->error('会话ID不能为空');
}
$kefu_message_model = new KefuMessageModel();
$condition = [
['site_id', '=', $this->site_id],
['conversation_id', '=', $conversation_id]
];
$message_list = $kefu_message_model->getMessageList($condition, '*', 'create_time asc', $page, $limit);
return $this->success($message_list);
}
}

View 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">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>

View File

@@ -0,0 +1,214 @@
<style>
.search-box {
padding: 10px 0;
border-bottom: 1px solid #eee;
margin-bottom: 15px;
}
.search-item {
display: inline-block;
margin-right: 15px;
vertical-align: middle;
}
.search-item label {
display: inline-block;
width: 80px;
text-align: right;
margin-right: 10px;
vertical-align: middle;
}
.search-item input,
.search-item select {
width: 150px;
display: inline-block;
vertical-align: middle;
}
.layui-btn-container {
margin-bottom: 15px;
text-align: right;
}
.layui-btn-sm {
margin-left: 5px;
}
.status-active {
color: #52c41a;
}
.status-inactive {
color: #faad14;
}
</style>
<div class="layui-card-body">
<!-- 搜索区域 -->
<div class="search-box">
<form class="layui-form" id="searchForm">
<div class="search-item">
<label for="user_id">用户ID</label>
<input type="text" name="user_id" id="user_id" placeholder="请输入用户ID" class="layui-input">
</div>
<div class="search-item">
<label for="status">状态</label>
<select name="status" id="status" class="layui-select">
<option value="">全部</option>
<option value="1">活跃</option>
<option value="0">已结束</option>
</select>
</div>
<div class="search-item">
<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>
</form>
</div>
<!-- 表格区域 -->
<table class="layui-table" id="conversationTable" lay-filter="conversationTable"></table>
</div>
<script type="text/html" id="toolbarDemo">
<div class="layui-btn-container">
<!-- 可以在这里添加按钮 -->
</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>
<script type="text/html" id="barDemo">
<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>
</script>
<script>
layui.use(['table', 'form', 'layer'], function() {
var table = layui.table;
var form = layui.form;
var layer = layui.layer;
// 渲染表格
var tableIns = table.render({
elem: '#conversationTable',
url: ns.url("aikefu://shop/kefu/getConversationList"),
method: 'POST',
toolbar: '#toolbarDemo',
defaultToolbar: ['filter', 'exports', 'print'],
title: '会话管理',
cols: [[
{field: 'id', title: 'ID', width: 80, align: 'center', fixed: 'left'},
{field: 'conversation_id', title: '会话ID', width: 200, align: 'center'},
{field: 'user_id', title: '用户ID', width: 150, 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: '暂无会话数据'
}
});
// 搜索按钮点击事件
$('#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: 2,
title: '消息记录',
content: ns.url("aikefu://shop/kefu/message", {conversation_id: data.conversation_id}),
area: ['90%', '90%']
});
} 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);
});
}
});
});
</script>

View File

@@ -0,0 +1,38 @@
<div class="layui-fluid">
<div class="layui-card">
<div class="layui-card-header">智能客服管理</div>
<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>

View File

@@ -0,0 +1,274 @@
<style>
.message-list {
max-height: 600px;
overflow-y: auto;
padding: 20px;
background-color: #f5f7fa;
border-radius: 8px;
margin-bottom: 20px;
border: 1px solid #e6e6e6;
}
.message-item {
margin-bottom: 20px;
display: flex;
align-items: flex-start;
}
.message-item.user {
justify-content: flex-end;
}
.message-item.assistant {
justify-content: flex-start;
}
.message-avatar {
width: 40px;
height: 40px;
border-radius: 50%;
margin: 0 10px;
}
.message-content {
max-width: 70%;
padding: 12px 16px;
border-radius: 18px;
word-wrap: break-word;
line-height: 1.5;
}
.message-item.user .message-content {
background-color: #1E9FFF;
color: white;
border-bottom-right-radius: 4px;
}
.message-item.assistant .message-content {
background-color: white;
color: #333;
border-bottom-left-radius: 4px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
}
.message-time {
font-size: 12px;
color: #909399;
margin-top: 5px;
text-align: center;
}
.message-role {
font-size: 12px;
color: #909399;
margin-bottom: 5px;
}
.conversation-info {
margin-bottom: 20px;
padding: 15px;
background-color: white;
border-radius: 8px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
border: 1px solid #e6e6e6;
}
.conversation-info h3 {
margin: 0 0 10px 0;
font-size: 16px;
color: #333;
font-weight: bold;
}
.conversation-info p {
margin: 5px 0;
font-size: 14px;
color: #606266;
}
.status-active {
color: #52c41a;
}
.status-inactive {
color: #faad14;
}
.search-box {
margin-bottom: 20px;
padding: 15px;
background-color: white;
border-radius: 8px;
border: 1px solid #e6e6e6;
}
.search-item {
margin-right: 15px;
}
.search-item label {
display: inline-block;
width: 80px;
text-align: right;
margin-right: 10px;
}
.layui-btn-sm {
margin-left: 5px;
}
</style>
<div class="layui-card-body">
<!-- 搜索区域 -->
<div class="search-box">
<form class="layui-form" id="searchForm">
<div class="search-item">
<label for="conversation_id">会话ID</label>
<input type="text" name="conversation_id" id="conversation_id" placeholder="请输入会话ID" value="{$conversation_id ?? ''}" class="layui-input" style="width: 200px; display: inline-block;">
</div>
<div class="search-item">
<button type="button" class="layui-btn layui-btn-primary" id="searchBtn">搜索</button>
</div>
</form>
</div>
<!-- 会话信息 -->
<div id="conversationInfo" class="conversation-info">
<!-- 会话信息将通过JavaScript动态加载 -->
</div>
<!-- 消息列表 -->
<div class="message-list" id="messageList">
<!-- 消息列表将通过JavaScript动态加载 -->
</div>
<!-- 分页 -->
<div class="layui-fixbar" id="pagination"></div>
</div>
<script>
layui.use(['laypage', 'layer'], function() {
var laypage = layui.laypage;
var layer = layui.layer;
// 分页参数
var page = 1;
var limit = 50;
var total = 0;
var conversation_id = $('#conversation_id').val();
// 加载会话信息
function loadConversationInfo() {
if (!conversation_id) {
$('#conversationInfo').html('<h3>会话详情</h3><p>请输入会话ID进行搜索</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>';
$('#conversationInfo').html(html);
} else {
$('#conversationInfo').html('<h3>会话详情</h3><p>未找到会话信息</p>');
}
},
error: function() {
$('#conversationInfo').html('<h3>会话详情</h3><p>加载会话信息失败</p>');
}
});
}
// 加载消息列表
function loadMessageList() {
if (!conversation_id) {
$('#messageList').html('<div style="text-align: center; padding: 50px; color: #999;">请输入会话ID进行搜索</div>');
return;
}
$.ajax({
url: ns.url("aikefu://shop/kefu/getMessageList"),
type: 'POST',
data: {
page: page,
limit: limit,
conversation_id: conversation_id
},
dataType: 'json',
success: function(res) {
if (res.code === 0) {
var list = res.data.list;
total = res.data.total;
var html = '';
if (list.length > 0) {
list.forEach(function(item) {
var role = item.role === 'user' ? '用户' : '机器人';
var roleClass = item.role === 'user' ? 'user' : 'assistant';
var avatar = item.role === 'user' ? '/__STATIC__/admin/img/user.png' : '/__STATIC__/admin/img/robot.png';
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 style="text-align: center; padding: 50px; color: #999;">暂无消息记录</div>';
}
$('#messageList').html(html);
// 滚动到底部
$('#messageList').scrollTop($('#messageList')[0].scrollHeight);
// 渲染分页
renderPagination();
} else {
layer.msg('加载失败:' + res.message, {icon: 2});
}
},
error: function() {
layer.msg('请求失败,请稍后重试', {icon: 2});
}
});
}
// 渲染分页
function renderPagination() {
if (total <= limit) {
$('#pagination').html('');
return;
}
laypage.render({
elem: 'pagination',
count: total,
limit: limit,
curr: page,
layout: ['prev', 'page', 'next', 'count', 'skip'],
jump: function(obj, first) {
if (!first) {
page = obj.curr;
loadMessageList();
}
}
});
}
// 搜索按钮点击事件
$('#searchBtn').click(function() {
conversation_id = $('#conversation_id').val();
page = 1;
loadConversationInfo();
loadMessageList();
});
// 初始化加载
if (conversation_id) {
loadConversationInfo();
loadMessageList();
}
});
</script>

View File

@@ -1,38 +1,30 @@
<?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' => []
];

View File

@@ -1,21 +1,12 @@
<?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' => '',
];

View File

@@ -1,39 +1,31 @@
<?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;
}
}
}
}

View File

@@ -1,27 +1,19 @@
<?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;
}
}

View File

@@ -1,26 +1,18 @@
<?php
/**
*/
namespace addon\alioss\event;
/**
* 应用安装
*/
class Install
{
/**
* 执行安装
*/
public function handle()
{
return success();
}
<?php
namespace addon\alioss\event;
/**
* 应用安装
*/
class Install
{
/**
* 执行安装
*/
public function handle()
{
return success();
}
}

View File

@@ -1,33 +1,25 @@
<?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;
}
}

View File

@@ -1,31 +1,23 @@
<?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;
}
}

View File

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

View File

@@ -1,146 +1,138 @@
<?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());
}
}
}
}

View File

@@ -1,56 +1,48 @@
<?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;
}
}

View File

@@ -1,56 +1,48 @@
<?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');
}
}
}

View File

@@ -1,38 +1,30 @@
<?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' => []
];

View File

@@ -1,50 +1,42 @@
<?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' => [
],
];

View File

@@ -1,21 +1,12 @@
<?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' => '',
];

View File

@@ -1,23 +1,15 @@
<?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,
],
];

View File

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

View File

@@ -1,33 +1,25 @@
<?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;
}
}
}
}

View File

@@ -1,39 +1,31 @@
<?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());
}
}
}
}

View File

@@ -1,41 +1,35 @@
<?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();
}
}
}

View File

@@ -1,34 +1,26 @@
<?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);
}
}
}
}

View File

@@ -1,33 +1,25 @@
<?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;
}
}
}

View File

@@ -1,42 +1,34 @@
<?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;
}
}
}
}

View File

@@ -1,51 +1,43 @@
<?php
/**
*/
namespace addon\alipay\event;
use addon\alipay\model\Config;
/**
* 支付方式 (后台调用)
*/
class PayType
{
/**
* 支付方式及配置
*/
public function handle($param)
{
$app_type = $param['app_type'] ?? '';
if (!empty($app_type)) {
if (!in_array($app_type, [ "h5", "app", "pc", "aliapp", 'wechat' ])) {
return '';
}
if ($app_type != 'aliapp') {
$config_model = new Config();
$config_result = $config_model->getPayConfig($param[ 'site_id' ]);
$config = $config_result[ "data" ][ "value" ] ?? [];
$pay_status = $config[ "pay_status" ] ?? 0;
if ($pay_status == 0) {
return '';
}
}
}
$info = array (
"pay_type" => "alipay",
"pay_type_name" => "支付宝支付",
"edit_url" => "alipay://shop/pay/config",
"shop_url" => "alipay://shop/pay/config",
"logo" => "addon/alipay/icon.png",
"desc" => "支付宝网站(www.alipay.com) 是国内先进的网上支付平台。"
);
return $info;
}
<?php
namespace addon\alipay\event;
use addon\alipay\model\Config;
/**
* 支付方式 (后台调用)
*/
class PayType
{
/**
* 支付方式及配置
*/
public function handle($param)
{
$app_type = $param['app_type'] ?? '';
if (!empty($app_type)) {
if (!in_array($app_type, [ "h5", "app", "pc", "aliapp", 'wechat' ])) {
return '';
}
if ($app_type != 'aliapp') {
$config_model = new Config();
$config_result = $config_model->getPayConfig($param[ 'site_id' ]);
$config = $config_result[ "data" ][ "value" ] ?? [];
$pay_status = $config[ "pay_status" ] ?? 0;
if ($pay_status == 0) {
return '';
}
}
}
$info = array (
"pay_type" => "alipay",
"pay_type_name" => "支付宝支付",
"edit_url" => "alipay://shop/pay/config",
"shop_url" => "alipay://shop/pay/config",
"logo" => "addon/alipay/icon.png",
"desc" => "支付宝网站(www.alipay.com) 是国内先进的网上支付平台。"
);
return $info;
}
}

View File

@@ -1,39 +1,31 @@
<?php
/**
*/
namespace addon\alipay\event;
use addon\alipay\model\Config;
class TransferType
{
public function handle(array $param)
{
$app_type = $param['app_type'] ?? '';
if (!empty($app_type)) {
if (!in_array($app_type, [ "h5", "app", "pc", "aliapp" ])) {
return '';
}
$config_model = new Config();
$config_result = $config_model->getPayConfig($param[ 'site_id' ]);
$config = $config_result[ "data" ][ "value" ] ?? [];
$transfer_status = $config[ "transfer_status" ] ?? 0;
if ($transfer_status == 0) {
return '';
}
}
$info = array (
"type" => "alipay",
"type_name" => "支付宝",
);
return $info;
}
<?php
namespace addon\alipay\event;
use addon\alipay\model\Config;
class TransferType
{
public function handle(array $param)
{
$app_type = $param['app_type'] ?? '';
if (!empty($app_type)) {
if (!in_array($app_type, [ "h5", "app", "pc", "aliapp" ])) {
return '';
}
$config_model = new Config();
$config_result = $config_model->getPayConfig($param[ 'site_id' ]);
$config = $config_result[ "data" ][ "value" ] ?? [];
$transfer_status = $config[ "transfer_status" ] ?? 0;
if ($transfer_status == 0) {
return '';
}
}
$info = array (
"type" => "alipay",
"type_name" => "支付宝",
);
return $info;
}
}

View File

@@ -1,26 +1,17 @@
<?php
/**
*/
namespace addon\alipay\event;
/**
* 应用卸载
*/
class UnInstall
{
/**
* 执行卸载
*/
public function handle()
{
return error(-1, "系统插件不得删除");
}
<?php
namespace addon\alipay\event;
/**
* 应用卸载
*/
class UnInstall
{
/**
* 执行卸载
*/
public function handle()
{
return error(-1, "系统插件不得删除");
}
}

View File

@@ -1,96 +1,88 @@
<?php
/**
*/
namespace addon\alipay\model;
use app\model\system\Config as ConfigModel;
use app\model\BaseModel;
/**
* 支付宝支付配置
*/
class Config extends BaseModel
{
private $encrypt = '******';
/**
* 设置支付配置
* @param $data
* @param int $site_id
* @param string $app_module
* @return array
*/
public function setPayConfig($data, $site_id = 0, $app_module = 'shop')
{
$config = new ConfigModel();
// 未加密前的数据
$original_config = $this->getPayConfig($site_id)[ 'data' ][ 'value' ];
// 检测数据是否发生变化,如果没有变化,则保持未加密前的数据
if (!empty($data[ 'private_key' ]) && $data[ 'private_key' ] == $this->encrypt) {
$data[ 'private_key' ] = $original_config[ 'private_key' ]; // 应用私钥
}
if (!empty($data[ 'public_key' ]) && $data[ 'public_key' ] == $this->encrypt) {
$data[ 'public_key' ] = $original_config[ 'public_key' ]; // 应用公钥
}
if (!empty($data[ 'alipay_public_key' ]) && $data[ 'alipay_public_key' ] == $this->encrypt) {
$data[ 'alipay_public_key' ] = $original_config[ 'alipay_public_key' ]; // 支付宝公钥
}
if (!empty($data[ 'public_key_crt' ]) && $data[ 'public_key_crt' ] == $this->encrypt) {
$data[ 'public_key_crt' ] = $original_config[ 'public_key_crt' ]; // 应用公钥证书
}
if (!empty($data[ 'alipay_public_key_crt' ]) && $data[ 'alipay_public_key_crt' ] == $this->encrypt) {
$data[ 'alipay_public_key_crt' ] = $original_config[ 'alipay_public_key_crt' ]; // 支付宝公钥证书
}
if (!empty($data[ 'alipay_with_crt' ]) && $data[ 'alipay_with_crt' ] == $this->encrypt) {
$data[ 'alipay_with_crt' ] = $original_config[ 'alipay_with_crt' ]; // 支付宝根证书
}
$res = $config->setConfig($data, '支付宝支付配置', 1, [ [ 'site_id', '=', $site_id ], [ 'app_module', '=', $app_module ], [ 'config_key', '=', 'ALI_PAY_CONFIG' ] ]);
return $res;
}
/**
* 获取支付配置
* @param int $site_id
* @param string $app_module
* @param bool $need_encrypt 是否需要加密数据true加密、false不加密
* @return array
*/
public function getPayConfig($site_id = 0, $app_module = 'shop', $need_encrypt = false)
{
$config = new ConfigModel();
$res = $config->getConfig([ [ 'site_id', '=', $site_id ], [ 'app_module', '=', $app_module ], [ 'config_key', '=', 'ALI_PAY_CONFIG' ] ]);
if ($need_encrypt) {
// 加密敏感信息
if (!empty($res[ 'data' ][ 'value' ][ 'private_key' ])) {
$res[ 'data' ][ 'value' ][ 'private_key' ] = $this->encrypt; // 应用私钥
}
if (!empty($res[ 'data' ][ 'value' ][ 'public_key' ])) {
$res[ 'data' ][ 'value' ][ 'public_key' ] = $this->encrypt; // 应用公钥
}
if (!empty($res[ 'data' ][ 'value' ][ 'alipay_public_key' ])) {
$res[ 'data' ][ 'value' ][ 'alipay_public_key' ] = $this->encrypt; // 支付宝公钥
}
if (!empty($res[ 'data' ][ 'value' ][ 'public_key_crt' ])) {
$res[ 'data' ][ 'value' ][ 'public_key_crt' ] = $this->encrypt; // 应用公钥证书
}
if (!empty($res[ 'data' ][ 'value' ][ 'alipay_public_key_crt' ])) {
$res[ 'data' ][ 'value' ][ 'alipay_public_key_crt' ] = $this->encrypt; // 支付宝公钥证书
}
if (!empty($res[ 'data' ][ 'value' ][ 'alipay_with_crt' ])) {
$res[ 'data' ][ 'value' ][ 'alipay_with_crt' ] = $this->encrypt; // 支付宝根证书
}
}
return $res;
}
<?php
namespace addon\alipay\model;
use app\model\system\Config as ConfigModel;
use app\model\BaseModel;
/**
* 支付宝支付配置
*/
class Config extends BaseModel
{
private $encrypt = '******';
/**
* 设置支付配置
* @param $data
* @param int $site_id
* @param string $app_module
* @return array
*/
public function setPayConfig($data, $site_id = 0, $app_module = 'shop')
{
$config = new ConfigModel();
// 未加密前的数据
$original_config = $this->getPayConfig($site_id)[ 'data' ][ 'value' ];
// 检测数据是否发生变化,如果没有变化,则保持未加密前的数据
if (!empty($data[ 'private_key' ]) && $data[ 'private_key' ] == $this->encrypt) {
$data[ 'private_key' ] = $original_config[ 'private_key' ]; // 应用私钥
}
if (!empty($data[ 'public_key' ]) && $data[ 'public_key' ] == $this->encrypt) {
$data[ 'public_key' ] = $original_config[ 'public_key' ]; // 应用公钥
}
if (!empty($data[ 'alipay_public_key' ]) && $data[ 'alipay_public_key' ] == $this->encrypt) {
$data[ 'alipay_public_key' ] = $original_config[ 'alipay_public_key' ]; // 支付宝公钥
}
if (!empty($data[ 'public_key_crt' ]) && $data[ 'public_key_crt' ] == $this->encrypt) {
$data[ 'public_key_crt' ] = $original_config[ 'public_key_crt' ]; // 应用公钥证书
}
if (!empty($data[ 'alipay_public_key_crt' ]) && $data[ 'alipay_public_key_crt' ] == $this->encrypt) {
$data[ 'alipay_public_key_crt' ] = $original_config[ 'alipay_public_key_crt' ]; // 支付宝公钥证书
}
if (!empty($data[ 'alipay_with_crt' ]) && $data[ 'alipay_with_crt' ] == $this->encrypt) {
$data[ 'alipay_with_crt' ] = $original_config[ 'alipay_with_crt' ]; // 支付宝根证书
}
$res = $config->setConfig($data, '支付宝支付配置', 1, [ [ 'site_id', '=', $site_id ], [ 'app_module', '=', $app_module ], [ 'config_key', '=', 'ALI_PAY_CONFIG' ] ]);
return $res;
}
/**
* 获取支付配置
* @param int $site_id
* @param string $app_module
* @param bool $need_encrypt 是否需要加密数据true加密、false不加密
* @return array
*/
public function getPayConfig($site_id = 0, $app_module = 'shop', $need_encrypt = false)
{
$config = new ConfigModel();
$res = $config->getConfig([ [ 'site_id', '=', $site_id ], [ 'app_module', '=', $app_module ], [ 'config_key', '=', 'ALI_PAY_CONFIG' ] ]);
if ($need_encrypt) {
// 加密敏感信息
if (!empty($res[ 'data' ][ 'value' ][ 'private_key' ])) {
$res[ 'data' ][ 'value' ][ 'private_key' ] = $this->encrypt; // 应用私钥
}
if (!empty($res[ 'data' ][ 'value' ][ 'public_key' ])) {
$res[ 'data' ][ 'value' ][ 'public_key' ] = $this->encrypt; // 应用公钥
}
if (!empty($res[ 'data' ][ 'value' ][ 'alipay_public_key' ])) {
$res[ 'data' ][ 'value' ][ 'alipay_public_key' ] = $this->encrypt; // 支付宝公钥
}
if (!empty($res[ 'data' ][ 'value' ][ 'public_key_crt' ])) {
$res[ 'data' ][ 'value' ][ 'public_key_crt' ] = $this->encrypt; // 应用公钥证书
}
if (!empty($res[ 'data' ][ 'value' ][ 'alipay_public_key_crt' ])) {
$res[ 'data' ][ 'value' ][ 'alipay_public_key_crt' ] = $this->encrypt; // 支付宝公钥证书
}
if (!empty($res[ 'data' ][ 'value' ][ 'alipay_with_crt' ])) {
$res[ 'data' ][ 'value' ][ 'alipay_with_crt' ] = $this->encrypt; // 支付宝根证书
}
}
return $res;
}
}

View File

@@ -1,486 +1,478 @@
<?php
/**
*/
namespace addon\alipay\model;
use addon\alipay\data\sdk\AopClient;
use addon\alipay\data\sdk\request\AlipayFundTransToaccountTransferRequest;
use addon\alipay\data\sdk\request\AlipayTradeAppPayRequest;
use addon\alipay\data\sdk\request\AlipayTradeCloseRequest;
use addon\alipay\data\sdk\request\AlipayTradeCreateRequest;
use addon\alipay\data\sdk\request\AlipayTradePagePayRequest;
use addon\alipay\data\sdk\request\AlipayTradeRefundRequest;
use addon\alipay\data\sdk\request\AlipayTradeWapPayRequest;
use addon\alipay\data\sdk\request\AlipayTradePrecreateRequest;
use addon\alipay\data\sdk\request\AlipayTradePayRequest;
use addon\alipay\data\sdk\request\AlipayTradeQueryRequest;
use app\model\BaseModel;
use app\model\system\Cron;
use app\model\system\Pay as PayCommon;
use addon\alipay\data\sdk\request\AlipayFundTransUniTransferRequest;
use addon\alipay\data\sdk\AopCertClient;
use app\model\system\Pay as PayModel;
use addon\aliapp\model\Config as AliappConfig;
use think\facade\Log;
/**
* 支付宝支付配置
*/
class Pay extends BaseModel
{
public $aop;
private $is_aliapp = 0;
/**
*
* @param $site_id
* @param int $is_aliapp 是否是小程序
*/
function __construct($site_id, $is_aliapp = 0)
{
$this->is_aliapp = $is_aliapp;
try {
// 获取支付宝支付参数(统一支付到平台账户)
if ($is_aliapp) {
$config_info = ( new AliappConfig() )->getAliappConfig($site_id)[ 'data' ][ 'value' ];
} else {
$config_info = ( new Config() )->getPayConfig($site_id)[ 'data' ][ 'value' ];
}
if (!empty($config_info)) {
$countersign_type = $config_info[ 'countersign_type' ] ?? 0;
if ($countersign_type == 1) {
$appCertPath = $config_info[ "public_key_crt" ] ?? "";
$alipayCertPath = $config_info[ "alipay_public_key_crt" ] ?? "";
$rootCertPath = $config_info[ "alipay_with_crt" ] ?? "";
$this->aop = new AopCertClient();
//调用getPublicKey从支付宝公钥证书中提取公钥
$this->aop->alipayrsaPublicKey = $this->aop->getPublicKey($alipayCertPath);
//是否校验自动下载的支付宝公钥证书,如果开启校验要保证支付宝根证书在有效期内
$this->aop->isCheckAlipayPublicCert = false;
//调用getCertSN获取证书序列号
$this->aop->appCertSN = $this->aop->getCertSN($appCertPath);
//调用getRootCertSN获取支付宝根证书序列号
$this->aop->alipayRootCertSN = $this->aop->getRootCertSN($rootCertPath);
} else {
// 获取支付宝支付参数(统一支付到平台账户)
$this->aop = new AopClient();
$this->aop->alipayrsaPublicKey = $config_info[ 'public_key' ] ?? "";
$this->aop->alipayPublicKey = $config_info[ 'alipay_public_key' ] ?? "";
}
$this->aop->appId = $config_info[ "app_id" ] ?? "";
$this->aop->rsaPrivateKey = $config_info[ 'private_key' ] ?? "";
$this->aop->gatewayUrl = 'https://openapi.alipay.com/gateway.do';
$this->aop->apiVersion = '1.0';
$this->aop->signType = 'RSA2';
$this->aop->postCharset = 'UTF-8';
$this->aop->format = 'json';
}
// else{
// return $this->error('', '支付宝支付未配置');
// }
} catch (\Exception $e) {
return $this->error('', '支付宝配置错误');
}
}
/**
* 生成支付
* @param $param
* @return array
*/
public function pay($param)
{
//构造要请求的参数数组,无需改动
$parameter = array (
"out_trade_no" => $param[ "out_trade_no" ],
"subject" => str_sub($param[ "pay_body" ], 15),
"total_amount" => (float) $param[ "pay_money" ],
"body" => str_sub($param[ "pay_body" ], 60),
"product_code" => 'FAST_INSTANT_TRADE_PAY',
);
switch ( $param[ "app_type" ] ) {
case "h5":
$request = new AlipayTradeWapPayRequest();
break;
case "pc":
$request = new AlipayTradePagePayRequest();
break;
case "app":
$request = new AlipayTradeAppPayRequest();
break;
case 'wechat':
$request = new AlipayTradeWapPayRequest();
break;
case 'cashier':
$request = new AlipayTradePrecreateRequest();
break;
case 'aliapp':
$parameter[ 'product_code' ] = 'FACE_TO_FACE_PAYMENT';
$member_info = model('member')->getInfo([ [ "member_id", "=", $param[ "member_id" ] ] ], 'ali_openid');
if (empty($member_info)) return $this->error(-1, '未获取到会员信息');
$parameter[ 'buyer_id' ] = $member_info[ 'ali_openid' ];
$request = new AlipayTradeCreateRequest();
break;
}
$parameter = json_encode($parameter);
$request->setBizContent($parameter);
$request->SetReturnUrl($param[ "return_url" ]);
$request->SetNotifyUrl($param[ "notify_url" ]);
///绑定商户数据
$pay_model = new PayModel();
$pay_model->bindMchPay($param[ "out_trade_no" ], [ "is_aliapp" => $this->is_aliapp ]);
try {
if ($param[ "app_type" ] == 'h5' || $param[ "app_type" ] == 'wechat' || $param[ "app_type" ] == 'pc') {
$result = $this->aop->pageExecute($request, 'get');
return $this->success([
'type' => 'url',
'data' => $result
]);
} elseif ($param[ "app_type" ] == 'app') {
$result = $this->aop->sdkExecute($request);
if (strpos(get_class($this->aop), 'AopClient') !== false) {
return $this->success([
'type' => 'url',
'data' => $result
]);
}
} else {
$result = $this->aop->execute($request);
}
if ($result === false) return $this->error('', '支付宝发起支付失败');
} catch (\Exception $e) {
return $this->error('', $e->getMessage());
}
$responseNode = str_replace(".", "_", $request->getApiMethodName()) . "_response";
$resultCode = $result->$responseNode->code;
if (!empty($resultCode) && $resultCode == 10000) {
switch ( $param[ "app_type" ] ) {
case 'cashier':
return $this->success([
'type' => 'qrcode',
'data' => [
'qrcode' => $result->$responseNode->qr_code
]
]);
break;
case 'aliapp':
return $this->success([
'type' => 'data',
'data' => [
'orderInfo' => $result->$responseNode->trade_no
]
]);
break;
default:
return $this->success();
}
} else {
return $this->error("", $result->$responseNode->sub_msg);
}
}
/**
* 支付关闭
* @param $param
* @return array
* @throws \think\Exception
*/
public function close($param)
{
$parameter = array (
"out_trade_no" => $param[ "out_trade_no" ]
);
// 建立请求
$request = new AlipayTradeCloseRequest();
$request->setBizContent(json_encode($parameter));
$result = $this->aop->execute($request);
$responseNode = str_replace(".", "_", $request->getApiMethodName()) . "_response";
$resultCode = $result->$responseNode->code;
if (!empty($resultCode) && $resultCode == 10000) {
return $this->success();
} else {
$sub_code = $result->$responseNode->sub_code;
$data = [];
if(in_array($sub_code, ['ACQ.TRADE_STATUS_ERROR', 'ACQ.REASON_TRADE_STATUS_INVALID', 'ACQ.REASON_ILLEGAL_STATUS'])){
$pay_order_result = $this->get($param[ "out_trade_no" ]);
if(!empty($pay_order_result) && $pay_order_result['code'] >= 0){
if($pay_order_result['data']['trade_status'] == 'TRADE_SUCCESS' || $pay_order_result['data']['trade_status'] == 'TRADE_FINISHED'){
$data['is_paid'] = 1;
}
}
}
return $this->error($data, $result->$responseNode->sub_msg);
}
}
/**
* 支付宝支付原路返回
* @param array $param 支付参数
* @return array
* @throws \think\Exception
*/
public function refund($param)
{
$pay_info = $param[ "pay_info" ];
$refund_no = $param[ "refund_no" ];
$out_trade_no = $pay_info[ "trade_no" ] ?? '';
$refund_fee = $param[ "refund_fee" ];
$parameter = array (
'trade_no' => $out_trade_no,
'refund_amount' => sprintf("%.2f", $refund_fee),
'out_request_no' => $refund_no
);
// 建立请求
$request = new AlipayTradeRefundRequest ();
$request->setBizContent(json_encode($parameter));
$result = $this->aop->execute($request);
$responseNode = str_replace(".", "_", $request->getApiMethodName()) . "_response";
$resultCode = $result->$responseNode->code;
if (!empty($resultCode) && $resultCode == 10000) {
return $this->success();
} else {
return $this->error("", $result->$responseNode->sub_msg);
}
}
/**
* 支付宝转账
* @param $param
* @return array
*/
public function payTransfer($param)
{
try {
$config_model = new Config();
$config_result = $config_model->getPayConfig($param[ 'site_id' ]);
if ($config_result[ 'code' ] < 0) return $config_result;
$config = $config_result[ 'data' ][ 'value' ];
if (empty($config)) return $this->error([], '未配置支付宝支付');
if (!$config[ 'transfer_status' ]) return $this->error([], '未启用支付宝转账');
$parameter = [
'out_biz_no' => $param[ 'out_trade_no' ],
'payee_type' => 'ALIPAY_LOGONID',
'payee_account' => $param[ "account_number" ],
'amount' => sprintf("%.2f", $param[ 'amount' ]),
'payee_real_name' => $param[ "real_name" ],
'remark' => $param[ "desc" ]
];
// 建立请求
$request = new AlipayFundTransToaccountTransferRequest();
$request->setBizContent(json_encode($parameter));
$result = $this->aop->execute($request);
$responseNode = str_replace(".", "_", $request->getApiMethodName()) . "_response";
$resultCode = $result->$responseNode->code;
if (!empty($resultCode) && $resultCode == 10000) {
return $this->success([
'out_trade_no' => $result->$responseNode->out_biz_no, // 商户交易号
'payment_no' => $result->$responseNode->order_id, // 微信付款单号
'payment_time' => date_to_time($result->$responseNode->pay_date) // 付款成功时间
]);
} else {
return $this->error([], $result->$responseNode->sub_msg);
}
} catch (\Exception $e) {
return $this->error([], $e->getMessage());
}
}
/**
* 异步完成支付
* @param $param
*/
public function payNotify()
{
// Log::write('pay_notifiy_log:alipay:'.json_encode(input()), 'notice');
try {
$res = $this->aop->rsaCheckV1($_POST, $this->aop->alipayrsaPublicKey, $this->aop->signType);
if ($res) { // 验证成功
$out_trade_no = $_POST[ 'out_trade_no' ];
// 支付宝交易号
$trade_no = $_POST[ 'trade_no' ];
// 交易状态
$trade_status = $_POST[ 'trade_status' ];
$pay_common = new PayCommon();
if ($trade_status == "TRADE_SUCCESS") {
$retval = $pay_common->onlinePay($out_trade_no, "alipay", $trade_no, "alipay");
}
echo "success";
} else {
// 验证失败
echo "fail";
}
} catch (\Exception $e) {
echo "fail";
}
}
public function payNewTransfer($param)
{
try {
$config_model = new Config();
$config_result = $config_model->getPayConfig($param[ 'site_id' ]);
if ($config_result[ 'code' ] < 0) return $config_result;
$config = $config_result[ 'data' ][ 'value' ];
if (empty($config)) return $this->error([], '未配置支付宝支付');
if (!$config[ 'transfer_status' ]) return $this->error([], '未启用支付宝转账');
$parameter = [
'out_biz_no' => $param[ 'out_trade_no' ],
'trans_amount' => sprintf("%.2f", $param[ 'amount' ]),
'product_code' => 'TRANS_ACCOUNT_NO_PWD',
'biz_scene' => 'DIRECT_TRANSFER',
'order_title' => '支付宝转账',
'remark' => $param[ "desc" ],
'payee_info' => [
'identity' => $param[ "account_number" ],
'identity_type' => "ALIPAY_LOGON_ID",
'name' => $param[ "real_name" ]
]
];
// 建立请求
$request = new AlipayFundTransUniTransferRequest();
$request->setBizContent(json_encode($parameter));
$result = $this->aop->execute($request);
$responseNode = str_replace(".", "_", $request->getApiMethodName()) . "_response";
$resultCode = $result->$responseNode->code;
if (!empty($resultCode) && $resultCode == 10000) {
return $this->success([
'out_trade_no' => $result->$responseNode->out_biz_no, // 商户交易号
'payment_no' => $result->$responseNode->order_id, // 微信付款单号
'payment_time' => date_to_time($result->$responseNode->trans_date) // 付款成功时间
]);
} else {
return $this->error([], $result->$responseNode->sub_msg);
}
} catch (\Exception $e) {
return $this->error([], $e->getMessage());
}
}
/**
* 付款码支付
* @param $param
* @return array|mixed|void
*/
public function micropay($param)
{
try {
//构造要请求的参数数组,无需改动
$parameter = array (
"out_trade_no" => $param[ "out_trade_no" ],
"subject" => str_sub($param[ "pay_body" ], 15),
"total_amount" => (float) $param[ "pay_money" ],
"scene" => "bar_code",
"auth_code" => $param[ 'auth_code' ],
);
$parameter = json_encode($parameter);
$request = new AlipayTradePayRequest();
$request->setBizContent($parameter);
$result = $this->aop->execute($request);
$responseNode = str_replace(".", "_", $request->getApiMethodName()) . "_response";
$resultCode = $result->$responseNode->code;
Log::write('支付宝—付款码支付result' . json_encode($result));
Log::write('支付宝—付款码支付resultCode' . json_encode($resultCode));
if (!empty($resultCode)) {
if ($resultCode == 10000) {
$pay_common = new PayModel();
return $res = $pay_common->onlinePay($param[ 'out_trade_no' ], 'alipay', $result->$responseNode->trade_no, 'alipay');
} else if ($resultCode == 10003) {
// 等待用户付款
( new Cron() )->addCron(1, 0, "查询付款码支付结果", "PayOrderQuery", time() + 3, $param[ 'id' ]);
}
} else {
return $this->error([], $result->$responseNode->sub_msg);
}
} catch (\Exception $e) {
return $this->error([], $e->getMessage());
}
}
// todo 查询交易信息【AlipayTradeQueryRequest】 https://opendocs.alipay.com/open/194/106039?pathHash=5b8cf9e6
public function orderQuery($param)
{
try {
//构造要请求的参数数组,无需改动
$parameter = array (
"out_trade_no" => $param[ "out_trade_no" ],
);
$parameter = json_encode($parameter);
$request = new AlipayTradeQueryRequest();
$request->setBizContent($parameter);
$result = $this->aop->execute($request);
$responseNode = str_replace(".", "_", $request->getApiMethodName()) . "_response";
$resultCode = $result->$responseNode->code;
Log::write('alipay_orderQuery' . json_encode($result));
Log::write('alipay_orderQuery_$resultCode' . json_encode($resultCode));
if (!empty($resultCode) && $resultCode == 10000) {
if ($result->$responseNode->trade_status == 'TRADE_SUCCESS') {
$pay_common = new PayModel();
return $res = $pay_common->onlinePay($param[ 'out_trade_no' ], 'alipay', $result->$responseNode->trade_no, 'alipay');
} else {
( new Cron() )->addCron(1, 0, "查询付款码支付结果", "PayOrderQuery", time() + 3, $param[ 'id' ]);
}
} else {
return $this->error([], $result->$responseNode->sub_msg);
}
} catch (\Exception $e) {
return $this->error([], $e->getMessage());
}
}
/**
* 查询订单信息
* @param $out_trade_no
* @return array
* @throws \think\Exception
*/
public function get($out_trade_no)
{
$parameter = array (
"out_trade_no" => $out_trade_no
);
// 建立请求
$request = new AlipayTradeQueryRequest();
$request->setBizContent(json_encode($parameter));
$result = $this->aop->execute($request);
$responseNode = str_replace(".", "_", $request->getApiMethodName()) . "_response";
$resultCode = $result->$responseNode->code;
if (!empty($resultCode) && $resultCode == 10000) {
return $this->success(json_decode(json_encode($result->$responseNode), true));
} else {
return $this->error([], $result->$responseNode->sub_msg);
}
}
<?php
namespace addon\alipay\model;
use addon\alipay\data\sdk\AopClient;
use addon\alipay\data\sdk\request\AlipayFundTransToaccountTransferRequest;
use addon\alipay\data\sdk\request\AlipayTradeAppPayRequest;
use addon\alipay\data\sdk\request\AlipayTradeCloseRequest;
use addon\alipay\data\sdk\request\AlipayTradeCreateRequest;
use addon\alipay\data\sdk\request\AlipayTradePagePayRequest;
use addon\alipay\data\sdk\request\AlipayTradeRefundRequest;
use addon\alipay\data\sdk\request\AlipayTradeWapPayRequest;
use addon\alipay\data\sdk\request\AlipayTradePrecreateRequest;
use addon\alipay\data\sdk\request\AlipayTradePayRequest;
use addon\alipay\data\sdk\request\AlipayTradeQueryRequest;
use app\model\BaseModel;
use app\model\system\Cron;
use app\model\system\Pay as PayCommon;
use addon\alipay\data\sdk\request\AlipayFundTransUniTransferRequest;
use addon\alipay\data\sdk\AopCertClient;
use app\model\system\Pay as PayModel;
use addon\aliapp\model\Config as AliappConfig;
use think\facade\Log;
/**
* 支付宝支付配置
*/
class Pay extends BaseModel
{
public $aop;
private $is_aliapp = 0;
/**
*
* @param $site_id
* @param int $is_aliapp 是否是小程序
*/
function __construct($site_id, $is_aliapp = 0)
{
$this->is_aliapp = $is_aliapp;
try {
// 获取支付宝支付参数(统一支付到平台账户)
if ($is_aliapp) {
$config_info = ( new AliappConfig() )->getAliappConfig($site_id)[ 'data' ][ 'value' ];
} else {
$config_info = ( new Config() )->getPayConfig($site_id)[ 'data' ][ 'value' ];
}
if (!empty($config_info)) {
$countersign_type = $config_info[ 'countersign_type' ] ?? 0;
if ($countersign_type == 1) {
$appCertPath = $config_info[ "public_key_crt" ] ?? "";
$alipayCertPath = $config_info[ "alipay_public_key_crt" ] ?? "";
$rootCertPath = $config_info[ "alipay_with_crt" ] ?? "";
$this->aop = new AopCertClient();
//调用getPublicKey从支付宝公钥证书中提取公钥
$this->aop->alipayrsaPublicKey = $this->aop->getPublicKey($alipayCertPath);
//是否校验自动下载的支付宝公钥证书,如果开启校验要保证支付宝根证书在有效期内
$this->aop->isCheckAlipayPublicCert = false;
//调用getCertSN获取证书序列号
$this->aop->appCertSN = $this->aop->getCertSN($appCertPath);
//调用getRootCertSN获取支付宝根证书序列号
$this->aop->alipayRootCertSN = $this->aop->getRootCertSN($rootCertPath);
} else {
// 获取支付宝支付参数(统一支付到平台账户)
$this->aop = new AopClient();
$this->aop->alipayrsaPublicKey = $config_info[ 'public_key' ] ?? "";
$this->aop->alipayPublicKey = $config_info[ 'alipay_public_key' ] ?? "";
}
$this->aop->appId = $config_info[ "app_id" ] ?? "";
$this->aop->rsaPrivateKey = $config_info[ 'private_key' ] ?? "";
$this->aop->gatewayUrl = 'https://openapi.alipay.com/gateway.do';
$this->aop->apiVersion = '1.0';
$this->aop->signType = 'RSA2';
$this->aop->postCharset = 'UTF-8';
$this->aop->format = 'json';
}
// else{
// return $this->error('', '支付宝支付未配置');
// }
} catch (\Exception $e) {
return $this->error('', '支付宝配置错误');
}
}
/**
* 生成支付
* @param $param
* @return array
*/
public function pay($param)
{
//构造要请求的参数数组,无需改动
$parameter = array (
"out_trade_no" => $param[ "out_trade_no" ],
"subject" => str_sub($param[ "pay_body" ], 15),
"total_amount" => (float) $param[ "pay_money" ],
"body" => str_sub($param[ "pay_body" ], 60),
"product_code" => 'FAST_INSTANT_TRADE_PAY',
);
switch ( $param[ "app_type" ] ) {
case "h5":
$request = new AlipayTradeWapPayRequest();
break;
case "pc":
$request = new AlipayTradePagePayRequest();
break;
case "app":
$request = new AlipayTradeAppPayRequest();
break;
case 'wechat':
$request = new AlipayTradeWapPayRequest();
break;
case 'cashier':
$request = new AlipayTradePrecreateRequest();
break;
case 'aliapp':
$parameter[ 'product_code' ] = 'FACE_TO_FACE_PAYMENT';
$member_info = model('member')->getInfo([ [ "member_id", "=", $param[ "member_id" ] ] ], 'ali_openid');
if (empty($member_info)) return $this->error(-1, '未获取到会员信息');
$parameter[ 'buyer_id' ] = $member_info[ 'ali_openid' ];
$request = new AlipayTradeCreateRequest();
break;
}
$parameter = json_encode($parameter);
$request->setBizContent($parameter);
$request->SetReturnUrl($param[ "return_url" ]);
$request->SetNotifyUrl($param[ "notify_url" ]);
///绑定商户数据
$pay_model = new PayModel();
$pay_model->bindMchPay($param[ "out_trade_no" ], [ "is_aliapp" => $this->is_aliapp ]);
try {
if ($param[ "app_type" ] == 'h5' || $param[ "app_type" ] == 'wechat' || $param[ "app_type" ] == 'pc') {
$result = $this->aop->pageExecute($request, 'get');
return $this->success([
'type' => 'url',
'data' => $result
]);
} elseif ($param[ "app_type" ] == 'app') {
$result = $this->aop->sdkExecute($request);
if (strpos(get_class($this->aop), 'AopClient') !== false) {
return $this->success([
'type' => 'url',
'data' => $result
]);
}
} else {
$result = $this->aop->execute($request);
}
if ($result === false) return $this->error('', '支付宝发起支付失败');
} catch (\Exception $e) {
return $this->error('', $e->getMessage());
}
$responseNode = str_replace(".", "_", $request->getApiMethodName()) . "_response";
$resultCode = $result->$responseNode->code;
if (!empty($resultCode) && $resultCode == 10000) {
switch ( $param[ "app_type" ] ) {
case 'cashier':
return $this->success([
'type' => 'qrcode',
'data' => [
'qrcode' => $result->$responseNode->qr_code
]
]);
break;
case 'aliapp':
return $this->success([
'type' => 'data',
'data' => [
'orderInfo' => $result->$responseNode->trade_no
]
]);
break;
default:
return $this->success();
}
} else {
return $this->error("", $result->$responseNode->sub_msg);
}
}
/**
* 支付关闭
* @param $param
* @return array
* @throws \think\Exception
*/
public function close($param)
{
$parameter = array (
"out_trade_no" => $param[ "out_trade_no" ]
);
// 建立请求
$request = new AlipayTradeCloseRequest();
$request->setBizContent(json_encode($parameter));
$result = $this->aop->execute($request);
$responseNode = str_replace(".", "_", $request->getApiMethodName()) . "_response";
$resultCode = $result->$responseNode->code;
if (!empty($resultCode) && $resultCode == 10000) {
return $this->success();
} else {
$sub_code = $result->$responseNode->sub_code;
$data = [];
if(in_array($sub_code, ['ACQ.TRADE_STATUS_ERROR', 'ACQ.REASON_TRADE_STATUS_INVALID', 'ACQ.REASON_ILLEGAL_STATUS'])){
$pay_order_result = $this->get($param[ "out_trade_no" ]);
if(!empty($pay_order_result) && $pay_order_result['code'] >= 0){
if($pay_order_result['data']['trade_status'] == 'TRADE_SUCCESS' || $pay_order_result['data']['trade_status'] == 'TRADE_FINISHED'){
$data['is_paid'] = 1;
}
}
}
return $this->error($data, $result->$responseNode->sub_msg);
}
}
/**
* 支付宝支付原路返回
* @param array $param 支付参数
* @return array
* @throws \think\Exception
*/
public function refund($param)
{
$pay_info = $param[ "pay_info" ];
$refund_no = $param[ "refund_no" ];
$out_trade_no = $pay_info[ "trade_no" ] ?? '';
$refund_fee = $param[ "refund_fee" ];
$parameter = array (
'trade_no' => $out_trade_no,
'refund_amount' => sprintf("%.2f", $refund_fee),
'out_request_no' => $refund_no
);
// 建立请求
$request = new AlipayTradeRefundRequest ();
$request->setBizContent(json_encode($parameter));
$result = $this->aop->execute($request);
$responseNode = str_replace(".", "_", $request->getApiMethodName()) . "_response";
$resultCode = $result->$responseNode->code;
if (!empty($resultCode) && $resultCode == 10000) {
return $this->success();
} else {
return $this->error("", $result->$responseNode->sub_msg);
}
}
/**
* 支付宝转账
* @param $param
* @return array
*/
public function payTransfer($param)
{
try {
$config_model = new Config();
$config_result = $config_model->getPayConfig($param[ 'site_id' ]);
if ($config_result[ 'code' ] < 0) return $config_result;
$config = $config_result[ 'data' ][ 'value' ];
if (empty($config)) return $this->error([], '未配置支付宝支付');
if (!$config[ 'transfer_status' ]) return $this->error([], '未启用支付宝转账');
$parameter = [
'out_biz_no' => $param[ 'out_trade_no' ],
'payee_type' => 'ALIPAY_LOGONID',
'payee_account' => $param[ "account_number" ],
'amount' => sprintf("%.2f", $param[ 'amount' ]),
'payee_real_name' => $param[ "real_name" ],
'remark' => $param[ "desc" ]
];
// 建立请求
$request = new AlipayFundTransToaccountTransferRequest();
$request->setBizContent(json_encode($parameter));
$result = $this->aop->execute($request);
$responseNode = str_replace(".", "_", $request->getApiMethodName()) . "_response";
$resultCode = $result->$responseNode->code;
if (!empty($resultCode) && $resultCode == 10000) {
return $this->success([
'out_trade_no' => $result->$responseNode->out_biz_no, // 商户交易号
'payment_no' => $result->$responseNode->order_id, // 微信付款单号
'payment_time' => date_to_time($result->$responseNode->pay_date) // 付款成功时间
]);
} else {
return $this->error([], $result->$responseNode->sub_msg);
}
} catch (\Exception $e) {
return $this->error([], $e->getMessage());
}
}
/**
* 异步完成支付
* @param $param
*/
public function payNotify()
{
// Log::write('pay_notifiy_log:alipay:'.json_encode(input()), 'notice');
try {
$res = $this->aop->rsaCheckV1($_POST, $this->aop->alipayrsaPublicKey, $this->aop->signType);
if ($res) { // 验证成功
$out_trade_no = $_POST[ 'out_trade_no' ];
// 支付宝交易号
$trade_no = $_POST[ 'trade_no' ];
// 交易状态
$trade_status = $_POST[ 'trade_status' ];
$pay_common = new PayCommon();
if ($trade_status == "TRADE_SUCCESS") {
$retval = $pay_common->onlinePay($out_trade_no, "alipay", $trade_no, "alipay");
}
echo "success";
} else {
// 验证失败
echo "fail";
}
} catch (\Exception $e) {
echo "fail";
}
}
public function payNewTransfer($param)
{
try {
$config_model = new Config();
$config_result = $config_model->getPayConfig($param[ 'site_id' ]);
if ($config_result[ 'code' ] < 0) return $config_result;
$config = $config_result[ 'data' ][ 'value' ];
if (empty($config)) return $this->error([], '未配置支付宝支付');
if (!$config[ 'transfer_status' ]) return $this->error([], '未启用支付宝转账');
$parameter = [
'out_biz_no' => $param[ 'out_trade_no' ],
'trans_amount' => sprintf("%.2f", $param[ 'amount' ]),
'product_code' => 'TRANS_ACCOUNT_NO_PWD',
'biz_scene' => 'DIRECT_TRANSFER',
'order_title' => '支付宝转账',
'remark' => $param[ "desc" ],
'payee_info' => [
'identity' => $param[ "account_number" ],
'identity_type' => "ALIPAY_LOGON_ID",
'name' => $param[ "real_name" ]
]
];
// 建立请求
$request = new AlipayFundTransUniTransferRequest();
$request->setBizContent(json_encode($parameter));
$result = $this->aop->execute($request);
$responseNode = str_replace(".", "_", $request->getApiMethodName()) . "_response";
$resultCode = $result->$responseNode->code;
if (!empty($resultCode) && $resultCode == 10000) {
return $this->success([
'out_trade_no' => $result->$responseNode->out_biz_no, // 商户交易号
'payment_no' => $result->$responseNode->order_id, // 微信付款单号
'payment_time' => date_to_time($result->$responseNode->trans_date) // 付款成功时间
]);
} else {
return $this->error([], $result->$responseNode->sub_msg);
}
} catch (\Exception $e) {
return $this->error([], $e->getMessage());
}
}
/**
* 付款码支付
* @param $param
* @return array|mixed|void
*/
public function micropay($param)
{
try {
//构造要请求的参数数组,无需改动
$parameter = array (
"out_trade_no" => $param[ "out_trade_no" ],
"subject" => str_sub($param[ "pay_body" ], 15),
"total_amount" => (float) $param[ "pay_money" ],
"scene" => "bar_code",
"auth_code" => $param[ 'auth_code' ],
);
$parameter = json_encode($parameter);
$request = new AlipayTradePayRequest();
$request->setBizContent($parameter);
$result = $this->aop->execute($request);
$responseNode = str_replace(".", "_", $request->getApiMethodName()) . "_response";
$resultCode = $result->$responseNode->code;
Log::write('支付宝—付款码支付result' . json_encode($result));
Log::write('支付宝—付款码支付resultCode' . json_encode($resultCode));
if (!empty($resultCode)) {
if ($resultCode == 10000) {
$pay_common = new PayModel();
return $res = $pay_common->onlinePay($param[ 'out_trade_no' ], 'alipay', $result->$responseNode->trade_no, 'alipay');
} else if ($resultCode == 10003) {
// 等待用户付款
( new Cron() )->addCron(1, 0, "查询付款码支付结果", "PayOrderQuery", time() + 3, $param[ 'id' ]);
}
} else {
return $this->error([], $result->$responseNode->sub_msg);
}
} catch (\Exception $e) {
return $this->error([], $e->getMessage());
}
}
// todo 查询交易信息【AlipayTradeQueryRequest】 https://opendocs.alipay.com/open/194/106039?pathHash=5b8cf9e6
public function orderQuery($param)
{
try {
//构造要请求的参数数组,无需改动
$parameter = array (
"out_trade_no" => $param[ "out_trade_no" ],
);
$parameter = json_encode($parameter);
$request = new AlipayTradeQueryRequest();
$request->setBizContent($parameter);
$result = $this->aop->execute($request);
$responseNode = str_replace(".", "_", $request->getApiMethodName()) . "_response";
$resultCode = $result->$responseNode->code;
Log::write('alipay_orderQuery' . json_encode($result));
Log::write('alipay_orderQuery_$resultCode' . json_encode($resultCode));
if (!empty($resultCode) && $resultCode == 10000) {
if ($result->$responseNode->trade_status == 'TRADE_SUCCESS') {
$pay_common = new PayModel();
return $res = $pay_common->onlinePay($param[ 'out_trade_no' ], 'alipay', $result->$responseNode->trade_no, 'alipay');
} else {
( new Cron() )->addCron(1, 0, "查询付款码支付结果", "PayOrderQuery", time() + 3, $param[ 'id' ]);
}
} else {
return $this->error([], $result->$responseNode->sub_msg);
}
} catch (\Exception $e) {
return $this->error([], $e->getMessage());
}
}
/**
* 查询订单信息
* @param $out_trade_no
* @return array
* @throws \think\Exception
*/
public function get($out_trade_no)
{
$parameter = array (
"out_trade_no" => $out_trade_no
);
// 建立请求
$request = new AlipayTradeQueryRequest();
$request->setBizContent(json_encode($parameter));
$result = $this->aop->execute($request);
$responseNode = str_replace(".", "_", $request->getApiMethodName()) . "_response";
$resultCode = $result->$responseNode->code;
if (!empty($resultCode) && $resultCode == 10000) {
return $this->success(json_decode(json_encode($result->$responseNode), true));
} else {
return $this->error([], $result->$responseNode->sub_msg);
}
}
}

View File

@@ -1,94 +1,86 @@
<?php
/**
*/
namespace addon\alipay\shop\controller;
use addon\alipay\model\Config as ConfigModel;
use app\shop\controller\BaseShop;
use think\facade\Config;
use app\model\upload\Upload;
/**
* 支付宝 控制器
*/
class Pay extends BaseShop
{
public function config()
{
$config_model = new ConfigModel();
if (request()->isJson()) {
$app_id = input("app_id", "");//支付宝应用ID (支付宝分配给开发者的应用ID)
$private_key = input("private_key", "");//应用私钥
$public_key = input("public_key", "");//应用公钥
$alipay_public_key = input("alipay_public_key", "");//支付宝公钥
$app_type = input("app_type", "");//支持端口 如web app
$pay_status = input("pay_status", 0);//支付启用状态
$refund_status = input("refund_status", 0);//退款启用状态
$transfer_status = input("transfer_status", 0);//转账启用状态
$public_key_crt = input("public_key_crt", "");
$alipay_public_key_crt = input("alipay_public_key_crt", "");
$alipay_with_crt = input("alipay_with_crt", "");
$countersign_type = input("countersign_type", 0);//加签模式
$data = array (
"app_id" => $app_id,
"private_key" => $private_key,
"public_key" => $public_key,
"alipay_public_key" => $alipay_public_key,
"refund_status" => $refund_status,
"pay_status" => $pay_status,
"transfer_status" => $transfer_status,
"app_type" => $app_type,
"public_key_crt" => $public_key_crt,
"alipay_public_key_crt" => $alipay_public_key_crt,
"alipay_with_crt" => $alipay_with_crt,
"countersign_type" => $countersign_type
);
$result = $config_model->setPayConfig($data, $this->site_id, $this->app_module);
return $result;
} else {
$info = $config_model->getPayConfig($this->site_id, $this->app_module, true)[ 'data' ][ 'value' ];
if (!empty($info)) {
$app_type_arr = [];
if (!empty($info[ 'app_type' ])) {
$app_type_arr = explode(',', $info[ 'app_type' ]);
}
$info[ 'app_type_arr' ] = $app_type_arr;
if (empty($info[ 'countersign_type' ])) {
$info[ 'countersign_type' ] = 0;
}
}
$this->assign("info", $info);
$this->assign("app_type", Config::get("app_type"));
return $this->fetch("pay/config");
}
}
/**
* 上传微信支付证书
*/
public function uploadAlipayCrt()
{
$upload_model = new Upload();
$site_id = request()->siteid();
$name = input("name", "");
$extend_type = [ 'crt' ];
$param = array (
"name" => "file",
"extend_type" => $extend_type
);
$site_id = max($site_id, 0);
$result = $upload_model->setPath("common/alipay/crt/" . $site_id . "/")->file($param);
return $result;
}
<?php
namespace addon\alipay\shop\controller;
use addon\alipay\model\Config as ConfigModel;
use app\shop\controller\BaseShop;
use think\facade\Config;
use app\model\upload\Upload;
/**
* 支付宝 控制器
*/
class Pay extends BaseShop
{
public function config()
{
$config_model = new ConfigModel();
if (request()->isJson()) {
$app_id = input("app_id", "");//支付宝应用ID (支付宝分配给开发者的应用ID)
$private_key = input("private_key", "");//应用私钥
$public_key = input("public_key", "");//应用公钥
$alipay_public_key = input("alipay_public_key", "");//支付宝公钥
$app_type = input("app_type", "");//支持端口 如web app
$pay_status = input("pay_status", 0);//支付启用状态
$refund_status = input("refund_status", 0);//退款启用状态
$transfer_status = input("transfer_status", 0);//转账启用状态
$public_key_crt = input("public_key_crt", "");
$alipay_public_key_crt = input("alipay_public_key_crt", "");
$alipay_with_crt = input("alipay_with_crt", "");
$countersign_type = input("countersign_type", 0);//加签模式
$data = array (
"app_id" => $app_id,
"private_key" => $private_key,
"public_key" => $public_key,
"alipay_public_key" => $alipay_public_key,
"refund_status" => $refund_status,
"pay_status" => $pay_status,
"transfer_status" => $transfer_status,
"app_type" => $app_type,
"public_key_crt" => $public_key_crt,
"alipay_public_key_crt" => $alipay_public_key_crt,
"alipay_with_crt" => $alipay_with_crt,
"countersign_type" => $countersign_type
);
$result = $config_model->setPayConfig($data, $this->site_id, $this->app_module);
return $result;
} else {
$info = $config_model->getPayConfig($this->site_id, $this->app_module, true)[ 'data' ][ 'value' ];
if (!empty($info)) {
$app_type_arr = [];
if (!empty($info[ 'app_type' ])) {
$app_type_arr = explode(',', $info[ 'app_type' ]);
}
$info[ 'app_type_arr' ] = $app_type_arr;
if (empty($info[ 'countersign_type' ])) {
$info[ 'countersign_type' ] = 0;
}
}
$this->assign("info", $info);
$this->assign("app_type", Config::get("app_type"));
return $this->fetch("pay/config");
}
}
/**
* 上传微信支付证书
*/
public function uploadAlipayCrt()
{
$upload_model = new Upload();
$site_id = request()->siteid();
$name = input("name", "");
$extend_type = [ 'crt' ];
$param = array (
"name" => "file",
"extend_type" => $extend_type
);
$site_id = max($site_id, 0);
$result = $upload_model->setPath("common/alipay/crt/" . $site_id . "/")->file($param);
return $result;
}
}

View File

@@ -1,48 +1,40 @@
<?php
/**
*/
return [
// 自定义模板页面类型,格式:[ 'title' => '页面类型名称', 'name' => '页面标识', 'path' => '页面路径', 'value' => '页面数据json格式' ]
'template' => [],
// 后台自定义组件——装修
'util' => [],
// 自定义页面路径
'link' => [
[
'name' => 'CASES_INFO',
'title' => '案例展示',
'parent' => 'BASICS_LINK',
'wap_url' => '/pages_tool/cases/index',
'web_url' => '',
'sort' => 0
]
],
// 自定义图标库
'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' => [
[
'name' => 'CASES_INFO',
'title' => '案例展示',
'parent' => 'BASICS_LINK',
'wap_url' => '/pages_tool/cases/index',
'web_url' => '',
'sort' => 0
]
],
// 自定义图标库
'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' => []
];

View File

@@ -1,21 +1,12 @@
<?php
/**
*/
return [
'name' => 'cases',
'title' => '案例展示',
'description' => '展示案例信息',
'type' => 'tool', //插件类型 system :系统插件(自动安装), promotion:扩展营销插件 tool:工具插件
'status' => 1,
'author' => '',
'version' => '1.0.0',
'version_no' => '2025051923121',
'content' => '',
<?php
return [
'name' => 'cases',
'title' => '案例展示',
'description' => '展示案例信息',
'type' => 'tool', //插件类型 system :系统插件(自动安装), promotion:扩展营销插件 tool:工具插件
'status' => 1,
'author' => '',
'version' => '1.0.0',
'version_no' => '2025051923121',
'content' => '',
];

View File

@@ -1,25 +1,17 @@
<?php
/**
*/
namespace addon\cases\event;
/**
* 应用安装
*/
class Install
{
/**
* 执行安装
*/
public function handle()
{
return success();
}
<?php
namespace addon\cases\event;
/**
* 应用安装
*/
class Install
{
/**
* 执行安装
*/
public function handle()
{
return success();
}
}

View File

@@ -1,30 +1,21 @@
<?php
/**
*/
namespace addon\cases\event;
/**
* 应用卸载
*/
class UnInstall
{
/**
* 执行卸载
*/
public function handle()
{
try {
return error('', "系统插件不允许删除");
} catch (\Exception $e) {
return error('', $e->getMessage());
}
}
<?php
namespace addon\cases\event;
/**
* 应用卸载
*/
class UnInstall
{
/**
* 执行卸载
*/
public function handle()
{
try {
return error('', "系统插件不允许删除");
} catch (\Exception $e) {
return error('', $e->getMessage());
}
}
}

View File

@@ -1,351 +1,343 @@
<?php
/**
*/
namespace addon\coupon\api\controller;
use app\api\controller\BaseApi;
use addon\coupon\model\Coupon as CouponModel;
use addon\coupon\model\CouponType as CouponTypeModel;
use addon\coupon\model\MemberCoupon;
use think\facade\Db;
/**
* 优惠券
*/
class Coupon extends BaseApi
{
/**
* 优惠券类型信息
*/
public function typeinfo()
{
$coupon_type_id = $this->params['coupon_type_id'] ?? 0;
if (empty($coupon_type_id)) {
return $this->response($this->error('', 'REQUEST_COUPON_TYPE_ID'));
}
$app_type = $this->params['app_type'] ?? 'h5';
$coupon_model = new CouponModel();
$condition = [
[ 'coupon_type_id', '=', $coupon_type_id ],
[ 'is_show', '=', 1 ],
[ 'site_id', '=', $this->site_id ]
];
$coupon_type_model = new CouponTypeModel();
$qrcode = $coupon_type_model->qrcode($coupon_type_id, $app_type, $this->site_id)[ 'data' ];
$info = $coupon_model->getCouponTypeInfo($condition);
if (!empty($info[ 'data' ]) && !empty($qrcode)) {
$info[ 'data' ][ 'qrcode' ] = $qrcode[ 'path' ];
}
return $this->response($info);
}
/**
* 列表信息
*/
public function memberpage()
{
$token = $this->checkToken();
if ($token[ 'code' ] < 0) return $this->response($token);
$page = $this->params['page'] ?? 1;
$page_size = $this->params['page_size'] ?? PAGE_LIST_ROWS;
$state = $this->params['state'] ?? 1;//优惠券状态 1已领用未使用 2已使用 3已过期
$coupon_model = new CouponModel();
$condition = [
[ 'npc.member_id', '=', $token[ 'data' ][ 'member_id' ] ],
[ 'npc.state', '=', $state ]
];
//按类型筛选
$type = $this->params['type'] ?? '';
$related_id = $this->params['related_id'] ?? 0;
switch ( $type ) {
case 'reward'://满减
$condition[] = ['npc.type', '=', 'reward'];
break;
case 'discount'://折扣
$condition[] = ['npc.type', '=', 'discount'];
break;
case 'no_threshold'://无门槛
$condition[] = ['npc.at_least', '=', 0 ];
break;
}
if (!empty($related_id)) {
$condition[] = [ 'related_id', '=', $related_id ];
}
$list = $coupon_model->getMemberCouponPageList($condition, $page, $page_size);
return $this->response($list);
}
/**
* 优惠券类型列表
*/
public function typelists()
{
$num = $this->params['num'] ?? 0;
$coupon_type_id_arr = $this->params['coupon_type_id_arr'] ?? '';//coupon_type_id数组
$can_receive = $this->params['can_receive'] ?? 0;// 是否只查询可领取的
$token = $this->checkToken();
$coupon_model = new CouponModel();
$condition = [
[ 'status', '=', 1 ],
[ 'is_show', '=', 1 ],
[ 'site_id', '=', $this->site_id ]
];
//按类型查询
$type = $this->params['type'] ?? '';
switch ( $type ) {
case 'reward'://满减
$condition[] = ['type', '=', 'reward'];
break;
case 'discount'://折扣
$condition[] = ['type', '=', 'discount'];
break;
case 'no_threshold'://无门槛
$condition[] = ['at_least', '=', 0 ];
break;
}
if (!empty($coupon_type_id_arr)) {
$condition[] = [ 'coupon_type_id', 'in', $coupon_type_id_arr ];
}
$field = 'coupon_type_id,type,site_id,coupon_name,money,discount,max_fetch,at_least,end_time,image,validity_type,fixed_term,status,is_show,goods_type,discount_limit,count,lead_count,IF(count < 0 or count - lead_count > 0, 1, 0) as is_remain';
if ($can_receive == 1) {
$condition[] = [ [ 'count', '<>', Db::raw('lead_count') ] ];
}
$order = Db::raw('IF(count < 0 or count - lead_count > 0, 1, 0) DESC,sort ASC');
$list = $coupon_model->getCouponTypeList($condition, $field, $order, $num);
if (!empty($list[ 'data' ]) && $this->member_id) {
foreach ($list[ 'data' ] as $k => $v) {
$list[ 'data' ][ $k ][ 'member_coupon_num' ] = $coupon_model->getCouponCount([
[ 'get_type', '=', 2 ],
[ 'member_id', '=', $this->member_id ],
[ 'coupon_type_id', '=', $v[ 'coupon_type_id' ] ]
])[ 'data' ];
}
}
return $this->response($list);
}
/**
* 优惠券类型分页列表
*/
public function typepagelists()
{
$page = $this->params['page'] ?? 1;
$page_size = $this->params['page_size'] ?? PAGE_LIST_ROWS;
$coupon_type_id_arr = $this->params['coupon_type_id_arr'] ?? '';//coupon_type_id数组
$can_receive = $this->params['can_receive'] ?? 0;// 是否只查询可领取的
$token = $this->checkToken();
$coupon_model = new CouponModel();
$condition = [
[ 'status', '=', 1 ],
[ 'is_show', '=', 1 ],
[ 'site_id', '=', $this->site_id ]
];
//按类型查询
$type = $this->params['type'] ?? '';
switch ( $type ) {
case 'reward'://满减
$condition[] = ['type', '=', 'reward'];
break;
case 'discount'://折扣
$condition[] = ['type', '=', 'discount'];
break;
case 'no_threshold'://无门槛
$condition[] = ['at_least', '=', 0 ];
break;
}
if (!empty($coupon_type_id_arr)) {
$condition[] = [ 'coupon_type_id', 'in', $coupon_type_id_arr ];
}
$field = 'coupon_type_id,type,site_id,coupon_name,money,discount,max_fetch,at_least,end_time,image,validity_type,fixed_term,status,is_show,goods_type,discount_limit,count,lead_count,IF(count < 0 or count - lead_count > 0, 1, 0) as is_remain';
if ($can_receive == 1) {
$condition[] = [ [ 'count', '<>', Db::raw('lead_count') ] ];
}
if ($this->member_id) {
$prefix = config('database.connections.mysql.prefix');
$field .= ', (select count(coupon_id) from ' . $prefix . 'promotion_coupon pc where pc.coupon_type_id = ct.coupon_type_id and pc.get_type=2 and pc.member_id=' . $this->member_id . ') as member_coupon_num';
}
$order = Db::raw('IF(count < 0 or count - lead_count > 0, 1, 0) DESC,sort ASC');
$list = $coupon_model->getCouponTypePageList($condition, $page, $page_size, $order, $field, 'ct');
return $this->response($list);
}
/**
* 获取优惠券
*/
public function receive()
{
$token = $this->checkToken();
if ($token[ 'code' ] < 0) return $this->response($token);
$site_id = $this->site_id;
$coupon_type_id = $this->params['coupon_type_id'] ?? 0;
$get_type = $this->params['get_type'] ?? 2;//获取方式:1订单2.直接领取3.活动领取
if (empty($coupon_type_id)) {
return $this->response($this->error('', 'REQUEST_COUPON_TYPE_ID'));
}
$coupon_model = new CouponModel();
$res = $coupon_model->receiveCoupon($coupon_type_id, $site_id, $token[ 'data' ][ 'member_id' ], $get_type);
$res[ 'data' ] = [];
//判断一下用户是否拥有当前优惠券
$coupon = $coupon_model->getCouponInfo([ [ 'coupon_type_id', '=', $coupon_type_id ], [ 'site_id', '=', $site_id ], [ 'member_id', '=', $token[ 'data' ][ 'member_id' ] ] ], 'coupon_id')[ 'data' ];
$res[ 'data' ][ 'is_exist' ] = empty($coupon) ? 0 : 1;
return $this->response($res);
}
/**
* 会员优惠券数量
* @return string
*/
public function num()
{
$token = $this->checkToken();
if ($token[ 'code' ] < 0) return $this->response($token);
$state = $this->params[ 'state' ] ?? 1;
$coupon_model = new MemberCoupon();
$count = $coupon_model->getMemberCouponNum($token[ 'data' ][ 'member_id' ], $state);
return $this->response($count);
}
/**
* 是否可以领取
*/
public function receivedNum()
{
$token = $this->checkToken();
if ($token[ 'code' ] < 0) return $this->response($token);
$coupon_type_id = $this->params['coupon_type_id'] ?? 0;
$coupon_model = new MemberCoupon();
$res = $coupon_model->receivedNum($coupon_type_id, $this->member_id);
return $this->response($res);
}
/**
* 查询商品可用的优惠券
* @param int $id
* @return false|string
*/
public function goodsCoupon($id = 0)
{
$this->checkToken();
$coupon_model = new CouponModel();
$goods_id = $this->params[ 'goods_id' ] ?? 0;
if (!empty($id)) {
$goods_id = $id;
}
// 查询全部商品参与
$condition = [
[ 'site_id', '=', $this->site_id ],
[ 'status', '=', 1 ],
[ 'is_show', '=', 1 ],
[ 'goods_type', '=', 1 ]
];
$field = 'count,lead_count,coupon_type_id,coupon_type_id as type_id,type,site_id,coupon_name,money,discount,max_fetch,at_least,end_time,validity_type,fixed_term,goods_type,discount_limit';
if ($this->member_id) {
$prefix = config('database.connections.mysql.prefix');
$field .= ',(select count(coupon_id) from ' . $prefix . 'promotion_coupon pc where pc.coupon_type_id = type_id and pc.get_type=2 and pc.member_id=' . $this->member_id . ') as member_coupon_num';
}
$list = $coupon_model->getCouponTypeList($condition, $field, 'money desc', null, 'ct');
// 查询指定商品参与
$goods_condition = [
[ 'site_id', '=', $this->site_id ],
[ 'status', '=', 1 ],
[ 'is_show', '=', 1 ],
[ 'goods_type', '=', 2 ],
[ 'goods_ids', 'like', "%,$goods_id,%" ]
];
$goods_coupon = $coupon_model->getCouponTypeList($goods_condition, $field, 'money desc', null, 'ct');
if (!empty($goods_coupon[ 'data' ])) {
$list[ 'data' ] = array_merge($list[ 'data' ], $goods_coupon[ 'data' ]);
}
// 查询指定商品不参与
$not_goods_condition = [
[ 'site_id', '=', $this->site_id ],
[ 'status', '=', 1 ],
[ 'is_show', '=', 1 ],
[ 'goods_type', '=', 3 ],
[ 'goods_ids', 'not like', "%,$goods_id,%" ]
];
$not_goods_coupon = $coupon_model->getCouponTypeList($not_goods_condition, $field, 'money desc', null, 'ct');
if (!empty($not_goods_coupon[ 'data' ])) {
$list[ 'data' ] = array_merge($list[ 'data' ], $not_goods_coupon[ 'data' ]);
}
if ($list[ 'data' ] && $this->member_id) {
foreach ($list[ 'data' ] as $k => $v) {
// 已抢光
if ($v[ 'count' ] == $v[ 'lead_count' ]) {
unset($list[ 'data' ][ $k ]);
} elseif ($v[ 'max_fetch' ] != 0 && $v[ 'member_coupon_num' ] > 0 && $v[ 'member_coupon_num' ] >= $v[ 'max_fetch' ]) {
// 已领取
unset($list[ 'data' ][ $k ]);
}
}
$list[ 'data' ] = array_values($list[ 'data' ]);
}
return $this->response($list);
}
/**
* 查询优惠券通过优惠券类型id
*/
public function couponById()
{
$id = $this->params[ 'id' ] ?? 0;
$coupon_model = new CouponModel();
$condition = [
[ 'site_id', '=', $this->site_id ],
[ 'status', '=', 1 ],
[ 'coupon_type_id', 'in', $id ]
];
$list = $coupon_model->getCouponTypeList($condition, 'coupon_type_id,type,site_id,coupon_name,money,discount,max_fetch,at_least,end_time,validity_type,fixed_term,goods_type,discount_limit', 'money desc', '');
return $this->response($list);
}
<?php
namespace addon\coupon\api\controller;
use app\api\controller\BaseApi;
use addon\coupon\model\Coupon as CouponModel;
use addon\coupon\model\CouponType as CouponTypeModel;
use addon\coupon\model\MemberCoupon;
use think\facade\Db;
/**
* 优惠券
*/
class Coupon extends BaseApi
{
/**
* 优惠券类型信息
*/
public function typeinfo()
{
$coupon_type_id = $this->params['coupon_type_id'] ?? 0;
if (empty($coupon_type_id)) {
return $this->response($this->error('', 'REQUEST_COUPON_TYPE_ID'));
}
$app_type = $this->params['app_type'] ?? 'h5';
$coupon_model = new CouponModel();
$condition = [
[ 'coupon_type_id', '=', $coupon_type_id ],
[ 'is_show', '=', 1 ],
[ 'site_id', '=', $this->site_id ]
];
$coupon_type_model = new CouponTypeModel();
$qrcode = $coupon_type_model->qrcode($coupon_type_id, $app_type, $this->site_id)[ 'data' ];
$info = $coupon_model->getCouponTypeInfo($condition);
if (!empty($info[ 'data' ]) && !empty($qrcode)) {
$info[ 'data' ][ 'qrcode' ] = $qrcode[ 'path' ];
}
return $this->response($info);
}
/**
* 列表信息
*/
public function memberpage()
{
$token = $this->checkToken();
if ($token[ 'code' ] < 0) return $this->response($token);
$page = $this->params['page'] ?? 1;
$page_size = $this->params['page_size'] ?? PAGE_LIST_ROWS;
$state = $this->params['state'] ?? 1;//优惠券状态 1已领用未使用 2已使用 3已过期
$coupon_model = new CouponModel();
$condition = [
[ 'npc.member_id', '=', $token[ 'data' ][ 'member_id' ] ],
[ 'npc.state', '=', $state ]
];
//按类型筛选
$type = $this->params['type'] ?? '';
$related_id = $this->params['related_id'] ?? 0;
switch ( $type ) {
case 'reward'://满减
$condition[] = ['npc.type', '=', 'reward'];
break;
case 'discount'://折扣
$condition[] = ['npc.type', '=', 'discount'];
break;
case 'no_threshold'://无门槛
$condition[] = ['npc.at_least', '=', 0 ];
break;
}
if (!empty($related_id)) {
$condition[] = [ 'related_id', '=', $related_id ];
}
$list = $coupon_model->getMemberCouponPageList($condition, $page, $page_size);
return $this->response($list);
}
/**
* 优惠券类型列表
*/
public function typelists()
{
$num = $this->params['num'] ?? 0;
$coupon_type_id_arr = $this->params['coupon_type_id_arr'] ?? '';//coupon_type_id数组
$can_receive = $this->params['can_receive'] ?? 0;// 是否只查询可领取的
$token = $this->checkToken();
$coupon_model = new CouponModel();
$condition = [
[ 'status', '=', 1 ],
[ 'is_show', '=', 1 ],
[ 'site_id', '=', $this->site_id ]
];
//按类型查询
$type = $this->params['type'] ?? '';
switch ( $type ) {
case 'reward'://满减
$condition[] = ['type', '=', 'reward'];
break;
case 'discount'://折扣
$condition[] = ['type', '=', 'discount'];
break;
case 'no_threshold'://无门槛
$condition[] = ['at_least', '=', 0 ];
break;
}
if (!empty($coupon_type_id_arr)) {
$condition[] = [ 'coupon_type_id', 'in', $coupon_type_id_arr ];
}
$field = 'coupon_type_id,type,site_id,coupon_name,money,discount,max_fetch,at_least,end_time,image,validity_type,fixed_term,status,is_show,goods_type,discount_limit,count,lead_count,IF(count < 0 or count - lead_count > 0, 1, 0) as is_remain';
if ($can_receive == 1) {
$condition[] = [ [ 'count', '<>', Db::raw('lead_count') ] ];
}
$order = Db::raw('IF(count < 0 or count - lead_count > 0, 1, 0) DESC,sort ASC');
$list = $coupon_model->getCouponTypeList($condition, $field, $order, $num);
if (!empty($list[ 'data' ]) && $this->member_id) {
foreach ($list[ 'data' ] as $k => $v) {
$list[ 'data' ][ $k ][ 'member_coupon_num' ] = $coupon_model->getCouponCount([
[ 'get_type', '=', 2 ],
[ 'member_id', '=', $this->member_id ],
[ 'coupon_type_id', '=', $v[ 'coupon_type_id' ] ]
])[ 'data' ];
}
}
return $this->response($list);
}
/**
* 优惠券类型分页列表
*/
public function typepagelists()
{
$page = $this->params['page'] ?? 1;
$page_size = $this->params['page_size'] ?? PAGE_LIST_ROWS;
$coupon_type_id_arr = $this->params['coupon_type_id_arr'] ?? '';//coupon_type_id数组
$can_receive = $this->params['can_receive'] ?? 0;// 是否只查询可领取的
$token = $this->checkToken();
$coupon_model = new CouponModel();
$condition = [
[ 'status', '=', 1 ],
[ 'is_show', '=', 1 ],
[ 'site_id', '=', $this->site_id ]
];
//按类型查询
$type = $this->params['type'] ?? '';
switch ( $type ) {
case 'reward'://满减
$condition[] = ['type', '=', 'reward'];
break;
case 'discount'://折扣
$condition[] = ['type', '=', 'discount'];
break;
case 'no_threshold'://无门槛
$condition[] = ['at_least', '=', 0 ];
break;
}
if (!empty($coupon_type_id_arr)) {
$condition[] = [ 'coupon_type_id', 'in', $coupon_type_id_arr ];
}
$field = 'coupon_type_id,type,site_id,coupon_name,money,discount,max_fetch,at_least,end_time,image,validity_type,fixed_term,status,is_show,goods_type,discount_limit,count,lead_count,IF(count < 0 or count - lead_count > 0, 1, 0) as is_remain';
if ($can_receive == 1) {
$condition[] = [ [ 'count', '<>', Db::raw('lead_count') ] ];
}
if ($this->member_id) {
$prefix = config('database.connections.mysql.prefix');
$field .= ', (select count(coupon_id) from ' . $prefix . 'promotion_coupon pc where pc.coupon_type_id = ct.coupon_type_id and pc.get_type=2 and pc.member_id=' . $this->member_id . ') as member_coupon_num';
}
$order = Db::raw('IF(count < 0 or count - lead_count > 0, 1, 0) DESC,sort ASC');
$list = $coupon_model->getCouponTypePageList($condition, $page, $page_size, $order, $field, 'ct');
return $this->response($list);
}
/**
* 获取优惠券
*/
public function receive()
{
$token = $this->checkToken();
if ($token[ 'code' ] < 0) return $this->response($token);
$site_id = $this->site_id;
$coupon_type_id = $this->params['coupon_type_id'] ?? 0;
$get_type = $this->params['get_type'] ?? 2;//获取方式:1订单2.直接领取3.活动领取
if (empty($coupon_type_id)) {
return $this->response($this->error('', 'REQUEST_COUPON_TYPE_ID'));
}
$coupon_model = new CouponModel();
$res = $coupon_model->receiveCoupon($coupon_type_id, $site_id, $token[ 'data' ][ 'member_id' ], $get_type);
$res[ 'data' ] = [];
//判断一下用户是否拥有当前优惠券
$coupon = $coupon_model->getCouponInfo([ [ 'coupon_type_id', '=', $coupon_type_id ], [ 'site_id', '=', $site_id ], [ 'member_id', '=', $token[ 'data' ][ 'member_id' ] ] ], 'coupon_id')[ 'data' ];
$res[ 'data' ][ 'is_exist' ] = empty($coupon) ? 0 : 1;
return $this->response($res);
}
/**
* 会员优惠券数量
* @return string
*/
public function num()
{
$token = $this->checkToken();
if ($token[ 'code' ] < 0) return $this->response($token);
$state = $this->params[ 'state' ] ?? 1;
$coupon_model = new MemberCoupon();
$count = $coupon_model->getMemberCouponNum($token[ 'data' ][ 'member_id' ], $state);
return $this->response($count);
}
/**
* 是否可以领取
*/
public function receivedNum()
{
$token = $this->checkToken();
if ($token[ 'code' ] < 0) return $this->response($token);
$coupon_type_id = $this->params['coupon_type_id'] ?? 0;
$coupon_model = new MemberCoupon();
$res = $coupon_model->receivedNum($coupon_type_id, $this->member_id);
return $this->response($res);
}
/**
* 查询商品可用的优惠券
* @param int $id
* @return false|string
*/
public function goodsCoupon($id = 0)
{
$this->checkToken();
$coupon_model = new CouponModel();
$goods_id = $this->params[ 'goods_id' ] ?? 0;
if (!empty($id)) {
$goods_id = $id;
}
// 查询全部商品参与
$condition = [
[ 'site_id', '=', $this->site_id ],
[ 'status', '=', 1 ],
[ 'is_show', '=', 1 ],
[ 'goods_type', '=', 1 ]
];
$field = 'count,lead_count,coupon_type_id,coupon_type_id as type_id,type,site_id,coupon_name,money,discount,max_fetch,at_least,end_time,validity_type,fixed_term,goods_type,discount_limit';
if ($this->member_id) {
$prefix = config('database.connections.mysql.prefix');
$field .= ',(select count(coupon_id) from ' . $prefix . 'promotion_coupon pc where pc.coupon_type_id = type_id and pc.get_type=2 and pc.member_id=' . $this->member_id . ') as member_coupon_num';
}
$list = $coupon_model->getCouponTypeList($condition, $field, 'money desc', null, 'ct');
// 查询指定商品参与
$goods_condition = [
[ 'site_id', '=', $this->site_id ],
[ 'status', '=', 1 ],
[ 'is_show', '=', 1 ],
[ 'goods_type', '=', 2 ],
[ 'goods_ids', 'like', "%,$goods_id,%" ]
];
$goods_coupon = $coupon_model->getCouponTypeList($goods_condition, $field, 'money desc', null, 'ct');
if (!empty($goods_coupon[ 'data' ])) {
$list[ 'data' ] = array_merge($list[ 'data' ], $goods_coupon[ 'data' ]);
}
// 查询指定商品不参与
$not_goods_condition = [
[ 'site_id', '=', $this->site_id ],
[ 'status', '=', 1 ],
[ 'is_show', '=', 1 ],
[ 'goods_type', '=', 3 ],
[ 'goods_ids', 'not like', "%,$goods_id,%" ]
];
$not_goods_coupon = $coupon_model->getCouponTypeList($not_goods_condition, $field, 'money desc', null, 'ct');
if (!empty($not_goods_coupon[ 'data' ])) {
$list[ 'data' ] = array_merge($list[ 'data' ], $not_goods_coupon[ 'data' ]);
}
if ($list[ 'data' ] && $this->member_id) {
foreach ($list[ 'data' ] as $k => $v) {
// 已抢光
if ($v[ 'count' ] == $v[ 'lead_count' ]) {
unset($list[ 'data' ][ $k ]);
} elseif ($v[ 'max_fetch' ] != 0 && $v[ 'member_coupon_num' ] > 0 && $v[ 'member_coupon_num' ] >= $v[ 'max_fetch' ]) {
// 已领取
unset($list[ 'data' ][ $k ]);
}
}
$list[ 'data' ] = array_values($list[ 'data' ]);
}
return $this->response($list);
}
/**
* 查询优惠券通过优惠券类型id
*/
public function couponById()
{
$id = $this->params[ 'id' ] ?? 0;
$coupon_model = new CouponModel();
$condition = [
[ 'site_id', '=', $this->site_id ],
[ 'status', '=', 1 ],
[ 'coupon_type_id', 'in', $id ]
];
$list = $coupon_model->getCouponTypeList($condition, 'coupon_type_id,type,site_id,coupon_name,money,discount,max_fetch,at_least,end_time,validity_type,fixed_term,goods_type,discount_limit', 'money desc', '');
return $this->response($list);
}
}

View File

@@ -1,67 +1,59 @@
<?php
/**
*/
return [
// 自定义模板页面类型,格式:[ 'title' => '页面类型名称', 'name' => '页面标识', 'path' => '页面路径', 'value' => '页面数据json格式' ]
'template' => [],
// 后台自定义组件——装修
'util' => [
[
'name' => 'Coupon',
'title' => '优惠券',
'type' => 'PROMOTION',
'value' => '{"style":1,"sources":"initial","styleName":"风格一","couponIds":[],"count":6,"previewList":[],"nameColor":"","moneyColor":"#FFFFFF","limitColor":"#FFFFFF","btnStyle":{"maxLen": 4,"textColor":"#FFFFFF","bgColor":"","text":"立即领取","aroundRadius":0,"isBgColor":false,"isAroundRadius":false},"isName":false,"couponBgColor":"","couponBgUrl":"","couponType":"img","ifNeedBg":true}',
'sort' => '30000',
'support_diy_view' => '',
'max_count' => 0,
'icon' => 'iconfont iconyouhuiquan',
],
],
// 自定义页面路径
'link' => [
[
'name' => 'COUPON_LIST',
'title' => '优惠券',
'parent' => 'MARKETING_LINK',
'wap_url' => '',
'web_url' => '',
'sort' => 0,
'child_list' => [
[
'name' => 'COUPON_PREFECTURE',
'title' => '优惠券专区',
'wap_url' => '/pages_tool/goods/coupon',
'web_url' => '',
'sort' => 0
]
]
],
],
// 自定义图标库
'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' => [
[
'name' => 'Coupon',
'title' => '优惠券',
'type' => 'PROMOTION',
'value' => '{"style":1,"sources":"initial","styleName":"风格一","couponIds":[],"count":6,"previewList":[],"nameColor":"","moneyColor":"#FFFFFF","limitColor":"#FFFFFF","btnStyle":{"maxLen": 4,"textColor":"#FFFFFF","bgColor":"","text":"立即领取","aroundRadius":0,"isBgColor":false,"isAroundRadius":false},"isName":false,"couponBgColor":"","couponBgUrl":"","couponType":"img","ifNeedBg":true}',
'sort' => '30000',
'support_diy_view' => '',
'max_count' => 0,
'icon' => 'iconfont iconyouhuiquan',
],
],
// 自定义页面路径
'link' => [
[
'name' => 'COUPON_LIST',
'title' => '优惠券',
'parent' => 'MARKETING_LINK',
'wap_url' => '',
'web_url' => '',
'sort' => 0,
'child_list' => [
[
'name' => 'COUPON_PREFECTURE',
'title' => '优惠券专区',
'wap_url' => '/pages_tool/goods/coupon',
'web_url' => '',
'sort' => 0
]
]
],
],
// 自定义图标库
'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' => []
];

View File

@@ -1,21 +1,12 @@
<?php
/**
*/
return [
'name' => 'coupon',
'title' => '优惠券',
'description' => '会员优惠券功能',
'type' => 'system', //插件类型 system :系统插件(自动安装), promotion:扩展营销插件 tool:工具插件
'status' => 1,
'author' => '',
'version' => '5.3.1',
'version_no' => '525231212001',
'content' => '',
<?php
return [
'name' => 'coupon',
'title' => '优惠券',
'description' => '会员优惠券功能',
'type' => 'system', //插件类型 system :系统插件(自动安装), promotion:扩展营销插件 tool:工具插件
'status' => 1,
'author' => '',
'version' => '5.3.1',
'version_no' => '525231212001',
'content' => '',
];

View File

@@ -1,55 +1,46 @@
<?php
/**
*/
namespace addon\coupon\dict;
/**
* 订单公共属性
*/
class CouponDict
{
const normal = 1;
const used = 2;
const expire = 3;
const close = 4;
/**
* 优惠券状态
* @param $status
* @return string|string[]
*/
public static function getStatus($status = ''){
$list = [
self::normal => '待使用',
self::used => '已使用',
self::expire => '已过期',
self::close => '已关闭',
];
if($status) return $list[$status] ?? '';
return $list;
}
const all = 1;
const selected = 2;
const selected_out = 3;
public static function getGoodsType($type = ''){
$list = [
self::all => '全部商品参与',
self::selected => '指定商品参与',
self::selected_out => '指定不参与商品'
];
if($type) return $list[$type] ?? '';
return $list;
}
}
<?php
namespace addon\coupon\dict;
/**
* 订单公共属性
*/
class CouponDict
{
const normal = 1;
const used = 2;
const expire = 3;
const close = 4;
/**
* 优惠券状态
* @param $status
* @return string|string[]
*/
public static function getStatus($status = ''){
$list = [
self::normal => '待使用',
self::used => '已使用',
self::expire => '已过期',
self::close => '已关闭',
];
if($status) return $list[$status] ?? '';
return $list;
}
const all = 1;
const selected = 2;
const selected_out = 3;
public static function getGoodsType($type = ''){
$list = [
self::all => '全部商品参与',
self::selected => '指定商品参与',
self::selected_out => '指定不参与商品'
];
if($type) return $list[$type] ?? '';
return $list;
}
}

View File

@@ -1,27 +1,19 @@
<?php
/**
*/
namespace addon\coupon\event;
use addon\coupon\model\Coupon;
/**
* 启动活动
*/
class CronCouponEnd
{
public function handle($params = [])
{
$coupon = new Coupon();
$res = $coupon->cronCouponEnd();
return $res;
}
<?php
namespace addon\coupon\event;
use addon\coupon\model\Coupon;
/**
* 启动活动
*/
class CronCouponEnd
{
public function handle($params = [])
{
$coupon = new Coupon();
$res = $coupon->cronCouponEnd();
return $res;
}
}

View File

@@ -1,27 +1,19 @@
<?php
/**
*/
namespace addon\coupon\event;
use addon\coupon\model\CouponType;
/**
* 优惠券定时结束
*/
class CronCouponTypeEnd
{
public function handle($params = [])
{
$coupon = new CouponType();
$res = $coupon->couponCronEnd($params[ 'relate_id' ]);
return $res;
}
<?php
namespace addon\coupon\event;
use addon\coupon\model\CouponType;
/**
* 优惠券定时结束
*/
class CronCouponTypeEnd
{
public function handle($params = [])
{
$coupon = new CouponType();
$res = $coupon->couponCronEnd($params[ 'relate_id' ]);
return $res;
}
}

View File

@@ -1,38 +1,29 @@
<?php
/**
*/
namespace addon\coupon\event;
use app\model\system\Cron;
/**
* 应用安装
*/
class Install
{
/**
* 执行安装
*/
public function handle()
{
try {
execute_sql('addon/coupon/data/install.sql');
$cron = new Cron();
$cron->deleteCron([ ['event', '=', 'CronCouponEnd'] ]);
$cron->addCron(2, 1, '优惠券过期自动关闭', 'CronCouponEnd', time(), 0);
return success();
} catch (\Exception $e) {
return error('', $e->getMessage());
}
}
<?php
namespace addon\coupon\event;
use app\model\system\Cron;
/**
* 应用安装
*/
class Install
{
/**
* 执行安装
*/
public function handle()
{
try {
execute_sql('addon/coupon/data/install.sql');
$cron = new Cron();
$cron->deleteCron([ ['event', '=', 'CronCouponEnd'] ]);
$cron->addCron(2, 1, '优惠券过期自动关闭', 'CronCouponEnd', time(), 0);
return success();
} catch (\Exception $e) {
return error('', $e->getMessage());
}
}
}

View File

@@ -1,46 +1,38 @@
<?php
/**
*/
namespace addon\coupon\event;
/**
* 店铺活动
*/
class ShowPromotion
{
/**
* 活动展示
* @return array
*/
public function handle()
{
$data = [
'shop' => [
[
//插件名称
'name' => 'coupon',
//展示分类根据平台端设置admin平台营销shop店铺营销member:会员营销, tool:应用工具)
'show_type' => 'shop',
//展示主题
'title' => '优惠券',
//展示介绍
'description' => '设置商家优惠券',
//展示图标
'icon' => 'addon/coupon/icon.png',
//跳转链接
'url' => 'coupon://shop/coupon/lists',
]
]
];
return $data;
}
<?php
namespace addon\coupon\event;
/**
* 店铺活动
*/
class ShowPromotion
{
/**
* 活动展示
* @return array
*/
public function handle()
{
$data = [
'shop' => [
[
//插件名称
'name' => 'coupon',
//展示分类根据平台端设置admin平台营销shop店铺营销member:会员营销, tool:应用工具)
'show_type' => 'shop',
//展示主题
'title' => '优惠券',
//展示介绍
'description' => '设置商家优惠券',
//展示图标
'icon' => 'addon/coupon/icon.png',
//跳转链接
'url' => 'coupon://shop/coupon/lists',
]
]
];
return $data;
}
}

View File

@@ -1,32 +1,23 @@
<?php
/**
*/
namespace addon\coupon\event;
/**
* 应用卸载
*/
class UnInstall
{
/**
* 执行卸载
*/
public function handle()
{
try {
return error('', "系统插件不允许删除");
//execute_sql('addon/coupon/data/uninstall.sql');
//return success();
} catch (\Exception $e) {
return error('', $e->getMessage());
}
}
<?php
namespace addon\coupon\event;
/**
* 应用卸载
*/
class UnInstall
{
/**
* 执行卸载
*/
public function handle()
{
try {
return error('', "系统插件不允许删除");
//execute_sql('addon/coupon/data/uninstall.sql');
//return success();
} catch (\Exception $e) {
return error('', $e->getMessage());
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,64 +1,56 @@
<?php
/**
*/
namespace addon\coupon\model;
use app\model\BaseModel;
use app\model\system\Stat;
/**
* 优惠券统计
*/
class CouponStat extends BaseModel
{
/**
* 领取优惠券统计
* @param $params
* @return array
*/
public function addReceiveCouponStat($params)
{
$coupon_id = $params[ 'coupon_id' ];
$site_id = $params[ 'site_id' ] ?? 0;
$order_condition = array (
[ 'coupon_id', '=', $coupon_id ],
[ 'site_id', '=', $site_id ]
);
$info = model('promotion_coupon')->getInfo($order_condition);
if (empty($info))
return $this->error();
$stat_data = array (
'site_id' => $site_id,
'coupon_count' => 1
);
$member_id = $info[ 'member_id' ];
//如果是第一笔订单才能累加下单会员数
$time_region = getDayStartAndEndTime();
$today_start_time = $time_region[ 'start_time' ];
$today_end_time = $time_region[ 'end_time' ];
$today_order_condition = array (
[ 'member_id', '=', $member_id ],
[ 'fetch_time', 'between', [ $today_start_time, $today_end_time ] ],
[ 'coupon_id', '<>', $coupon_id ]
);
$count = model('promotion_coupon')->getCount($today_order_condition);
if ($count == 0) {
$stat_data[ 'coupon_member_count' ] = 1;
}
//发布统计
$stat_model = new Stat();
$result = $stat_model->addShopStat($stat_data);
return $result;
}
<?php
namespace addon\coupon\model;
use app\model\BaseModel;
use app\model\system\Stat;
/**
* 优惠券统计
*/
class CouponStat extends BaseModel
{
/**
* 领取优惠券统计
* @param $params
* @return array
*/
public function addReceiveCouponStat($params)
{
$coupon_id = $params[ 'coupon_id' ];
$site_id = $params[ 'site_id' ] ?? 0;
$order_condition = array (
[ 'coupon_id', '=', $coupon_id ],
[ 'site_id', '=', $site_id ]
);
$info = model('promotion_coupon')->getInfo($order_condition);
if (empty($info))
return $this->error();
$stat_data = array (
'site_id' => $site_id,
'coupon_count' => 1
);
$member_id = $info[ 'member_id' ];
//如果是第一笔订单才能累加下单会员数
$time_region = getDayStartAndEndTime();
$today_start_time = $time_region[ 'start_time' ];
$today_end_time = $time_region[ 'end_time' ];
$today_order_condition = array (
[ 'member_id', '=', $member_id ],
[ 'fetch_time', 'between', [ $today_start_time, $today_end_time ] ],
[ 'coupon_id', '<>', $coupon_id ]
);
$count = model('promotion_coupon')->getCount($today_order_condition);
if ($count == 0) {
$stat_data[ 'coupon_member_count' ] = 1;
}
//发布统计
$stat_model = new Stat();
$result = $stat_model->addShopStat($stat_data);
return $result;
}
}

View File

@@ -1,336 +1,328 @@
<?php
/**
*/
namespace addon\coupon\model;
use app\model\BaseModel;
use app\model\system\Config as ConfigModel;
use app\model\system\Cron;
use app\model\upload\Upload;
/**
* 优惠券活动
*/
class CouponType extends BaseModel
{
//优惠券类型状态
private $coupon_type_status = [
1 => '进行中',
2 => '已结束',
-1 => '已关闭',
];
public function getCouponTypeStatus()
{
return $this->coupon_type_status;
}
/**
* 添加优惠券活动
* @param $data
* @return array
*/
public function addCouponType($data)
{
//只要创建了就是进行中
$data[ 'status' ] = 1;
$data[ 'create_time' ] = time();
//获取商品id
if ($data[ 'goods_type' ] == 1) {//全部商品参与
$data[ 'goods_ids' ] = '';
}
$data[ 'goods_ids' ] = ',' . $data[ 'goods_ids' ] . ',';
$res = model('promotion_coupon_type')->add($data);
if ($data[ 'validity_type' ] == 0) {
$cron = new Cron();
$cron->addCron(1, 1, '优惠券活动定时结束', 'CronCouponTypeEnd', $data[ 'end_time' ], $res);
}
$this->qrcode($res, 'all', $data[ 'site_id' ]);
return $this->success($res);
}
/**
* 编辑优惠券活动
* @param $data
* @param $coupon_type_id
* @return array
*/
public function editCouponType($data, $coupon_type_id)
{
$data[ 'update_time' ] = time();
//获取商品id
if ($data[ 'goods_type' ] == 1) {//全部商品参与
$data[ 'goods_ids' ] = '';
}
$coupon_info = model('promotion_coupon_type')->getInfo([ [ 'coupon_type_id', '=', $coupon_type_id ] ]);
if (!empty($coupon_info[ 'image' ]) && !empty($data[ 'image' ]) && $coupon_info[ 'image' ] != $data[ 'image' ]) {
$upload_model = new Upload();
$upload_model->deletePic($coupon_info[ 'image' ], $coupon_info[ 'site_id' ]);
}
$data[ 'goods_ids' ] = ',' . $data[ 'goods_ids' ] . ',';
$res = model('promotion_coupon_type')->update($data, [ [ 'coupon_type_id', '=', $coupon_type_id ] ]);
model('promotion_coupon')->update([ 'goods_ids' => $data[ 'goods_ids' ], 'goods_type' => $data[ 'goods_type' ] ], [ [ 'coupon_type_id', '=', $coupon_type_id ], [ 'state', '=', 1 ] ]);
$cron = new Cron();
$cron->deleteCron([ [ 'event', '=', 'CronCouponTypeEnd' ], [ 'relate_id', '=', $coupon_type_id ] ]);
if ($data[ 'validity_type' ] == 0) {
$cron->addCron(1, 1, '优惠券活动定时结束', 'CronCouponTypeEnd', $data[ 'end_time' ], $coupon_type_id);
}
return $this->success($res);
}
/**
* 关闭优惠券
* @param $coupon_type_id
* @param $site_id
* @return array
*/
public function closeCouponType($coupon_type_id, $site_id)
{
$res = model('promotion_coupon_type')->update([ 'status' => -1 ], [ [ 'coupon_type_id', '=', $coupon_type_id ], [ 'site_id', '=', $site_id ] ]);
// if ($res) {
// model("promotion_coupon")->update(['state' => 3], [['coupon_type_id', '=', $coupon_type_id], ['site_id', '=', $site_id]]);
// }
$cron = new Cron();
$cron->deleteCron([ [ 'event', '=', 'CronCouponTypeEnd' ], [ 'relate_id', '=', $coupon_type_id ] ]);
return $this->success($res);
}
/**
* 删除优惠券活动
* @param $coupon_type_id
* @param $site_id
* @return array
*/
public function deleteCouponType($coupon_type_id, $site_id)
{
$coupon_info = model('promotion_coupon_type')->getInfo([ [ 'coupon_type_id', '=', $coupon_type_id ] ]);
if ($coupon_info['status'] == 1) return $this->error('', '进行中的优惠卷无法删除,请先关闭');
if (!empty($coupon_info[ 'image' ])) {
$upload_model = new Upload();
$upload_model->deletePic($coupon_info[ 'image' ], $coupon_info[ 'site_id' ]);
}
$res = model('promotion_coupon_type')->delete([ [ 'coupon_type_id', '=', $coupon_type_id ], [ 'site_id', '=', $site_id ] ]);
if ($res) {
model('promotion_coupon')->delete([ [ 'coupon_type_id', '=', $coupon_type_id ] ]);
}
$cron = new Cron();
$cron->deleteCron([ [ 'event', '=', 'CronCouponTypeEnd' ], [ 'relate_id', '=', $coupon_type_id ] ]);
return $this->success($res);
}
/**
* 获取优惠券活动详情
* @param $coupon_type_id
* @param $site_id
* @return array
*/
public function getCouponTypeInfo($coupon_type_id, $site_id)
{
$res = model('promotion_coupon_type')->getList([ [ 'coupon_type_id', 'in', $coupon_type_id ], [ 'site_id', '=', $site_id ] ]);
if (!empty($res)) {
foreach ($res as $k => $v) {
if ($v[ 'goods_type' ] == 2 || $v[ 'goods_type' ] == 3) {
$field[ $k ] = 'goods_id,goods_name,FLOOR(goods_stock) as goods_stock,goods_image,price,sort';
$goods_ids[ $k ] = substr($v[ 'goods_ids' ], '1', '-1');
$goods_list[ $k ] = model('goods')->getList([ [ 'goods_id', 'in', $goods_ids[ $k ] ] ], $field[ $k ]);
}
$res[ $k ][ 'goods_list' ] = $goods_list[$k] ?? [];
$res[ $k ][ 'goods_list_count' ] = count($res[ $k ][ 'goods_list' ]);
}
}
return $this->success($res);
}
/**
* 获取优惠券活动信息
* @param array $where
* @param bool $field
* @param string $alias
* @param null $join
* @param null $data
* @return array
*/
public function getInfo($where = [], $field = true, $alias = 'a', $join = null, $data = null)
{
$res = model('promotion_coupon_type')->getInfo($where, $field, $alias, $join, $data);
return $this->success($res);
}
/**
* 获取优惠券类型列表
* @param array $condition
* @param string $field
* @param string $order
* @param null $limit
* @return array
*/
public function getCouponTypeList($condition = [], $field = '*', $order = 'create_time desc', $limit = null)
{
$res = model('promotion_coupon_type')->getList($condition, $field, $order, '', '', '', $limit);
return $this->success($res);
}
/**
* 获取优惠券活动分页列表
* @param array $condition
* @param int $page
* @param int $page_size
* @param string $order
* @param string $field
* @return array
*/
public function getCouponTypePageList($condition = [], $page = 1, $page_size = PAGE_LIST_ROWS, $order = '', $field = '*')
{
$condition[] = [ 'promotion_type', '=', 0 ];
$list = model('promotion_coupon_type')->pageList($condition, $field, $order, $page, $page_size);
return $this->success($list);
}
/**
* 排序
* @param $coupon_type_id
* @param $sort
* @return array
*/
public function couponSort($coupon_type_id, $sort)
{
$res = model('promotion_coupon_type')->update([ 'sort' => $sort ], [ [ 'coupon_type_id', '=', $coupon_type_id ] ]);
return $this->success($res);
}
/**
* 生成优惠券二维码
* @param $coupon_type_id
* @param string $app_type all为全部
* @param string $type 类型 create创建 get获取
* @return mixed|array
*/
public function qrcode($coupon_type_id, $app_type, $site_id, $type = 'create')
{
$res = event('Qrcode', [
'site_id' => $site_id,
'app_type' => $app_type,
'type' => $type,
'data' => [
'coupon_type_id' => $coupon_type_id
],
'page' => '/pages_tool/goods/coupon_receive',
'qrcode_path' => 'upload/qrcode/coupon',
'qrcode_name' => 'coupon_type_code_' . $coupon_type_id . '_' . $site_id,
], true);
return $res;
}
/**
* 优惠券定时结束
* @param $coupon_type_id
* @return array
*/
public function couponCronEnd($coupon_type_id)
{
$res = model('promotion_coupon_type')->update([ 'status' => 2 ], [ [ 'coupon_type_id', '=', $coupon_type_id ] ]);
return $this->success($res);
}
public function spread($coupon_type_id, $name, $site_id, $type = 'create')
{
$data = [
'site_id' => $site_id,
'app_type' => 'all', // all为全部
'type' => $type, // 类型 create创建 get获取
'data' => [
'coupon_type_id' => $coupon_type_id
],
'page' => '/pages_tool/goods/coupon_receive',
'qrcode_path' => 'upload/qrcode/coupon',
'qrcode_name' => 'coupon_type_code_' . $coupon_type_id . '_' . $site_id,
];
event('Qrcode', $data, true);
$app_type_list = config('app_type');
$path = [];
foreach ($app_type_list as $k => $v) {
switch ( $k ) {
case 'h5':
$wap_domain = getH5Domain();
$path[ $k ][ 'status' ] = 1;
$path[ $k ][ 'url' ] = $wap_domain . $data[ 'page' ] . '?coupon_type_id=' . $coupon_type_id;
$path[ $k ][ 'img' ] = 'upload/qrcode/coupon/coupon_type_code_' . $coupon_type_id . '_' . $site_id . '_' . $k . '.png';
break;
case 'weapp' :
$config = new ConfigModel();
$res = $config->getConfig([ [ 'site_id', '=', $site_id ], [ 'app_module', '=', 'shop' ], [ 'config_key', '=', 'WEAPP_CONFIG' ] ]);
if (!empty($res[ 'data' ])) {
if (empty($res[ 'data' ][ 'value' ][ 'qrcode' ])) {
$path[ $k ][ 'status' ] = 2;
$path[ $k ][ 'message' ] = '未配置微信小程序';
} else {
$path[ $k ][ 'status' ] = 1;
$path[ $k ][ 'img' ] = $res[ 'data' ][ 'value' ][ 'qrcode' ];
}
} else {
$path[ $k ][ 'status' ] = 2;
$path[ $k ][ 'message' ] = '未配置微信小程序';
}
break;
case 'wechat' :
$config = new ConfigModel();
$res = $config->getConfig([ [ 'site_id', '=', $site_id ], [ 'app_module', '=', 'shop' ], [ 'config_key', '=', 'WECHAT_CONFIG' ] ]);
if (!empty($res[ 'data' ])) {
if (empty($res[ 'data' ][ 'value' ][ 'qrcode' ])) {
$path[ $k ][ 'status' ] = 2;
$path[ $k ][ 'message' ] = '未配置微信公众号';
} else {
$path[ $k ][ 'status' ] = 1;
$path[ $k ][ 'img' ] = $res[ 'data' ][ 'value' ][ 'qrcode' ];
}
} else {
$path[ $k ][ 'status' ] = 2;
$path[ $k ][ 'message' ] = '未配置微信公众号';
}
break;
}
}
$return = [
'path' => $path,
'name' => $name,
];
return $this->success($return);
}
public function urlQrcode($page, $qrcode_param, $promotion_type, $app_type, $site_id)
{
$params = [
'site_id' => $site_id,
'data' => $qrcode_param,
'page' => $page,
'promotion_type' => $promotion_type,
'app_type' => $app_type,
'h5_path' => $page . '?coupon_type_id=' . $qrcode_param[ 'coupon_type_id' ],
'qrcode_path' => 'upload/qrcode/coupon',
'qrcode_name' => 'coupon_type_code_' . $promotion_type . '_' . $qrcode_param[ 'coupon_type_id' ] . '_' . $site_id,
];
$solitaire = event('PromotionQrcode', $params, true);
return $this->success($solitaire);
}
<?php
namespace addon\coupon\model;
use app\model\BaseModel;
use app\model\system\Config as ConfigModel;
use app\model\system\Cron;
use app\model\upload\Upload;
/**
* 优惠券活动
*/
class CouponType extends BaseModel
{
//优惠券类型状态
private $coupon_type_status = [
1 => '进行中',
2 => '已结束',
-1 => '已关闭',
];
public function getCouponTypeStatus()
{
return $this->coupon_type_status;
}
/**
* 添加优惠券活动
* @param $data
* @return array
*/
public function addCouponType($data)
{
//只要创建了就是进行中
$data[ 'status' ] = 1;
$data[ 'create_time' ] = time();
//获取商品id
if ($data[ 'goods_type' ] == 1) {//全部商品参与
$data[ 'goods_ids' ] = '';
}
$data[ 'goods_ids' ] = ',' . $data[ 'goods_ids' ] . ',';
$res = model('promotion_coupon_type')->add($data);
if ($data[ 'validity_type' ] == 0) {
$cron = new Cron();
$cron->addCron(1, 1, '优惠券活动定时结束', 'CronCouponTypeEnd', $data[ 'end_time' ], $res);
}
$this->qrcode($res, 'all', $data[ 'site_id' ]);
return $this->success($res);
}
/**
* 编辑优惠券活动
* @param $data
* @param $coupon_type_id
* @return array
*/
public function editCouponType($data, $coupon_type_id)
{
$data[ 'update_time' ] = time();
//获取商品id
if ($data[ 'goods_type' ] == 1) {//全部商品参与
$data[ 'goods_ids' ] = '';
}
$coupon_info = model('promotion_coupon_type')->getInfo([ [ 'coupon_type_id', '=', $coupon_type_id ] ]);
if (!empty($coupon_info[ 'image' ]) && !empty($data[ 'image' ]) && $coupon_info[ 'image' ] != $data[ 'image' ]) {
$upload_model = new Upload();
$upload_model->deletePic($coupon_info[ 'image' ], $coupon_info[ 'site_id' ]);
}
$data[ 'goods_ids' ] = ',' . $data[ 'goods_ids' ] . ',';
$res = model('promotion_coupon_type')->update($data, [ [ 'coupon_type_id', '=', $coupon_type_id ] ]);
model('promotion_coupon')->update([ 'goods_ids' => $data[ 'goods_ids' ], 'goods_type' => $data[ 'goods_type' ] ], [ [ 'coupon_type_id', '=', $coupon_type_id ], [ 'state', '=', 1 ] ]);
$cron = new Cron();
$cron->deleteCron([ [ 'event', '=', 'CronCouponTypeEnd' ], [ 'relate_id', '=', $coupon_type_id ] ]);
if ($data[ 'validity_type' ] == 0) {
$cron->addCron(1, 1, '优惠券活动定时结束', 'CronCouponTypeEnd', $data[ 'end_time' ], $coupon_type_id);
}
return $this->success($res);
}
/**
* 关闭优惠券
* @param $coupon_type_id
* @param $site_id
* @return array
*/
public function closeCouponType($coupon_type_id, $site_id)
{
$res = model('promotion_coupon_type')->update([ 'status' => -1 ], [ [ 'coupon_type_id', '=', $coupon_type_id ], [ 'site_id', '=', $site_id ] ]);
// if ($res) {
// model("promotion_coupon")->update(['state' => 3], [['coupon_type_id', '=', $coupon_type_id], ['site_id', '=', $site_id]]);
// }
$cron = new Cron();
$cron->deleteCron([ [ 'event', '=', 'CronCouponTypeEnd' ], [ 'relate_id', '=', $coupon_type_id ] ]);
return $this->success($res);
}
/**
* 删除优惠券活动
* @param $coupon_type_id
* @param $site_id
* @return array
*/
public function deleteCouponType($coupon_type_id, $site_id)
{
$coupon_info = model('promotion_coupon_type')->getInfo([ [ 'coupon_type_id', '=', $coupon_type_id ] ]);
if ($coupon_info['status'] == 1) return $this->error('', '进行中的优惠卷无法删除,请先关闭');
if (!empty($coupon_info[ 'image' ])) {
$upload_model = new Upload();
$upload_model->deletePic($coupon_info[ 'image' ], $coupon_info[ 'site_id' ]);
}
$res = model('promotion_coupon_type')->delete([ [ 'coupon_type_id', '=', $coupon_type_id ], [ 'site_id', '=', $site_id ] ]);
if ($res) {
model('promotion_coupon')->delete([ [ 'coupon_type_id', '=', $coupon_type_id ] ]);
}
$cron = new Cron();
$cron->deleteCron([ [ 'event', '=', 'CronCouponTypeEnd' ], [ 'relate_id', '=', $coupon_type_id ] ]);
return $this->success($res);
}
/**
* 获取优惠券活动详情
* @param $coupon_type_id
* @param $site_id
* @return array
*/
public function getCouponTypeInfo($coupon_type_id, $site_id)
{
$res = model('promotion_coupon_type')->getList([ [ 'coupon_type_id', 'in', $coupon_type_id ], [ 'site_id', '=', $site_id ] ]);
if (!empty($res)) {
foreach ($res as $k => $v) {
if ($v[ 'goods_type' ] == 2 || $v[ 'goods_type' ] == 3) {
$field[ $k ] = 'goods_id,goods_name,FLOOR(goods_stock) as goods_stock,goods_image,price,sort';
$goods_ids[ $k ] = substr($v[ 'goods_ids' ], '1', '-1');
$goods_list[ $k ] = model('goods')->getList([ [ 'goods_id', 'in', $goods_ids[ $k ] ] ], $field[ $k ]);
}
$res[ $k ][ 'goods_list' ] = $goods_list[$k] ?? [];
$res[ $k ][ 'goods_list_count' ] = count($res[ $k ][ 'goods_list' ]);
}
}
return $this->success($res);
}
/**
* 获取优惠券活动信息
* @param array $where
* @param bool $field
* @param string $alias
* @param null $join
* @param null $data
* @return array
*/
public function getInfo($where = [], $field = true, $alias = 'a', $join = null, $data = null)
{
$res = model('promotion_coupon_type')->getInfo($where, $field, $alias, $join, $data);
return $this->success($res);
}
/**
* 获取优惠券类型列表
* @param array $condition
* @param string $field
* @param string $order
* @param null $limit
* @return array
*/
public function getCouponTypeList($condition = [], $field = '*', $order = 'create_time desc', $limit = null)
{
$res = model('promotion_coupon_type')->getList($condition, $field, $order, '', '', '', $limit);
return $this->success($res);
}
/**
* 获取优惠券活动分页列表
* @param array $condition
* @param int $page
* @param int $page_size
* @param string $order
* @param string $field
* @return array
*/
public function getCouponTypePageList($condition = [], $page = 1, $page_size = PAGE_LIST_ROWS, $order = '', $field = '*')
{
$condition[] = [ 'promotion_type', '=', 0 ];
$list = model('promotion_coupon_type')->pageList($condition, $field, $order, $page, $page_size);
return $this->success($list);
}
/**
* 排序
* @param $coupon_type_id
* @param $sort
* @return array
*/
public function couponSort($coupon_type_id, $sort)
{
$res = model('promotion_coupon_type')->update([ 'sort' => $sort ], [ [ 'coupon_type_id', '=', $coupon_type_id ] ]);
return $this->success($res);
}
/**
* 生成优惠券二维码
* @param $coupon_type_id
* @param string $app_type all为全部
* @param string $type 类型 create创建 get获取
* @return mixed|array
*/
public function qrcode($coupon_type_id, $app_type, $site_id, $type = 'create')
{
$res = event('Qrcode', [
'site_id' => $site_id,
'app_type' => $app_type,
'type' => $type,
'data' => [
'coupon_type_id' => $coupon_type_id
],
'page' => '/pages_tool/goods/coupon_receive',
'qrcode_path' => 'upload/qrcode/coupon',
'qrcode_name' => 'coupon_type_code_' . $coupon_type_id . '_' . $site_id,
], true);
return $res;
}
/**
* 优惠券定时结束
* @param $coupon_type_id
* @return array
*/
public function couponCronEnd($coupon_type_id)
{
$res = model('promotion_coupon_type')->update([ 'status' => 2 ], [ [ 'coupon_type_id', '=', $coupon_type_id ] ]);
return $this->success($res);
}
public function spread($coupon_type_id, $name, $site_id, $type = 'create')
{
$data = [
'site_id' => $site_id,
'app_type' => 'all', // all为全部
'type' => $type, // 类型 create创建 get获取
'data' => [
'coupon_type_id' => $coupon_type_id
],
'page' => '/pages_tool/goods/coupon_receive',
'qrcode_path' => 'upload/qrcode/coupon',
'qrcode_name' => 'coupon_type_code_' . $coupon_type_id . '_' . $site_id,
];
event('Qrcode', $data, true);
$app_type_list = config('app_type');
$path = [];
foreach ($app_type_list as $k => $v) {
switch ( $k ) {
case 'h5':
$wap_domain = getH5Domain();
$path[ $k ][ 'status' ] = 1;
$path[ $k ][ 'url' ] = $wap_domain . $data[ 'page' ] . '?coupon_type_id=' . $coupon_type_id;
$path[ $k ][ 'img' ] = 'upload/qrcode/coupon/coupon_type_code_' . $coupon_type_id . '_' . $site_id . '_' . $k . '.png';
break;
case 'weapp' :
$config = new ConfigModel();
$res = $config->getConfig([ [ 'site_id', '=', $site_id ], [ 'app_module', '=', 'shop' ], [ 'config_key', '=', 'WEAPP_CONFIG' ] ]);
if (!empty($res[ 'data' ])) {
if (empty($res[ 'data' ][ 'value' ][ 'qrcode' ])) {
$path[ $k ][ 'status' ] = 2;
$path[ $k ][ 'message' ] = '未配置微信小程序';
} else {
$path[ $k ][ 'status' ] = 1;
$path[ $k ][ 'img' ] = $res[ 'data' ][ 'value' ][ 'qrcode' ];
}
} else {
$path[ $k ][ 'status' ] = 2;
$path[ $k ][ 'message' ] = '未配置微信小程序';
}
break;
case 'wechat' :
$config = new ConfigModel();
$res = $config->getConfig([ [ 'site_id', '=', $site_id ], [ 'app_module', '=', 'shop' ], [ 'config_key', '=', 'WECHAT_CONFIG' ] ]);
if (!empty($res[ 'data' ])) {
if (empty($res[ 'data' ][ 'value' ][ 'qrcode' ])) {
$path[ $k ][ 'status' ] = 2;
$path[ $k ][ 'message' ] = '未配置微信公众号';
} else {
$path[ $k ][ 'status' ] = 1;
$path[ $k ][ 'img' ] = $res[ 'data' ][ 'value' ][ 'qrcode' ];
}
} else {
$path[ $k ][ 'status' ] = 2;
$path[ $k ][ 'message' ] = '未配置微信公众号';
}
break;
}
}
$return = [
'path' => $path,
'name' => $name,
];
return $this->success($return);
}
public function urlQrcode($page, $qrcode_param, $promotion_type, $app_type, $site_id)
{
$params = [
'site_id' => $site_id,
'data' => $qrcode_param,
'page' => $page,
'promotion_type' => $promotion_type,
'app_type' => $app_type,
'h5_path' => $page . '?coupon_type_id=' . $qrcode_param[ 'coupon_type_id' ],
'qrcode_path' => 'upload/qrcode/coupon',
'qrcode_name' => 'coupon_type_code_' . $promotion_type . '_' . $qrcode_param[ 'coupon_type_id' ] . '_' . $site_id,
];
$solitaire = event('PromotionQrcode', $params, true);
return $this->success($solitaire);
}
}

View File

@@ -1,243 +1,235 @@
<?php
/**
*/
namespace addon\coupon\model;
use addon\coupon\dict\CouponDict;
use app\model\BaseModel;
/**
* 优惠券
*/
class MemberCoupon extends BaseModel
{
/**
* 获取会员已领取优惠券
* @param $member_id
* @param $state
* @param int $site_id
* @param string $order
* @return array
*/
public function getMemberCouponList($member_id, $state, $site_id = 0, $order = "fetch_time desc")
{
$condition = array (
[ "member_id", "=", $member_id ],
[ "state", "=", $state ],
);
if ($site_id > 0) {
$condition[] = [ "site_id", "=", $site_id ];
}
$list = model("promotion_coupon")->getList($condition, "*", $order, '', '', '', 0);
return $this->success($list);
}
/**
* 使用优惠券
* @param $coupon_id
* @param $member_id
* @param int $order_id
* @return array
*/
public function useMemberCoupon($coupon_id, $member_id, $order_id = 0)
{
//优惠券处理方案
$result = model('promotion_coupon')->update([ 'use_order_id' => $order_id, 'state' => 2, 'use_time' => time() ], [ [ 'coupon_id', '=', $coupon_id ], [ "member_id", "=", $member_id ], [ 'state', '=', 1 ] ]);
if ($result === false) {
return $this->error();
}
return $this->success();
}
/**
* 获取会员已领取优惠券数量
* @param $member_id
* @param $state
* @param int $site_id
* @return array
*/
public function getMemberCouponNum($member_id, $state, $site_id = 0)
{
$condition = array (
[ "member_id", "=", $member_id ],
[ "state", "=", $state ],
);
if ($site_id > 0) {
$condition[] = [ "site_id", "=", $site_id ];
}
$num = model("promotion_coupon")->getCount($condition);
return $this->success($num);
}
/**
* 会员是否可领取该优惠券
* @param $coupon_type_id
* @param $member_id
* @return array
*/
public function receivedNum($coupon_type_id, $member_id)
{
$received_num = model('promotion_coupon')->getCount([ [ 'coupon_type_id', '=', $coupon_type_id ], [ 'member_id', '=', $member_id ] ]);
return $this->success($received_num);
}
/**
* 获取编码
*/
public function getCode()
{
return random_keys(8);
}
/**
* 会员批量发放优惠券
* @param $coupon_type_ids
* @param $site_id
* @param $member_id
* @param int $get_type
* @param int $is_stock
* @param int $related_id
* @return array
*/
public function sendCoupon($coupon_type_ids, $site_id, $member_id, $get_type = 4, $is_stock = 0, $related_id = 0)
{
//已选优惠券提交数组
if (!empty($coupon_type_ids)) {
$res = 0;
foreach ($coupon_type_ids as $coupon_type_id) {
$coupon_type_info = model('promotion_coupon_type')->getInfo([ 'coupon_type_id' => $coupon_type_id, 'site_id' => $site_id, 'status' => 1 ]);
if (!empty($coupon_type_info)) {
if ($coupon_type_info[ 'count' ] != -1 || $is_stock == 0) {
if ($coupon_type_info[ 'count' ] == $coupon_type_info[ 'lead_count' ]) {
return $this->error('', '来迟了该优惠券已被领取完了');
}
}
if ($coupon_type_info[ 'max_fetch' ] != 0 && $get_type == 2) {
//限制领取
$member_receive_num = model('promotion_coupon')->getCount([
'coupon_type_id' => $coupon_type_id,
'member_id' => $member_id,
'get_type' => 2
]);
if ($member_receive_num >= $coupon_type_info[ 'max_fetch' ] ) {
return $this->error('', '该优惠券领取已达到上限');
}
}
$data = [
'coupon_type_id' => $coupon_type_id,
'site_id' => $site_id,
'coupon_code' => $this->getCode(),
'member_id' => $member_id,
'money' => $coupon_type_info[ 'money' ],
'state' => 1,
'get_type' => $get_type,
'goods_type' => $coupon_type_info[ 'goods_type' ],
'fetch_time' => time(),
'coupon_name' => $coupon_type_info[ 'coupon_name' ],
'at_least' => $coupon_type_info[ 'at_least' ],
'type' => $coupon_type_info[ 'type' ],
'discount' => $coupon_type_info[ 'discount' ],
'discount_limit' => $coupon_type_info[ 'discount_limit' ],
'goods_ids' => $coupon_type_info[ 'goods_ids' ],
'related_id' => $related_id
];
if ($coupon_type_info[ 'validity_type' ] == 0) {
$data[ 'end_time' ] = $coupon_type_info[ 'end_time' ];
} elseif ($coupon_type_info[ 'validity_type' ] == 1) {
$data[ 'end_time' ] = ( time() + $coupon_type_info[ 'fixed_term' ] * 86400 );
}
$res = model('promotion_coupon')->add($data);
if ($is_stock == 0) {
model('promotion_coupon_type')->setInc([ [ 'coupon_type_id', '=', $coupon_type_id ] ], 'lead_count');
}
}
}
if ($res) {
return $this->success($res);
} else {
return $this->error();
}
} else {
return $this->error();
}
}
/**
* 回收优惠券
* @param array $coupon_list
* @param $site_id
* @return array
*/
public function recoveryCoupon(array $coupon_list, $site_id)
{
$coupon = [];
foreach ($coupon_list as $coupon_item) {
if (isset($coupon[ $coupon_item[ 'coupon_type_id' ] ])) {
$coupon[$coupon_item['coupon_type_id']][] = $coupon_item['coupon_id'];
} else {
$coupon[ $coupon_item[ 'coupon_type_id' ] ] = [ $coupon_item[ 'coupon_id' ] ];
}
}
if (!count($coupon)) return $this->error();
model('promotion_coupon')->startTrans();
try {
foreach ($coupon as $coupon_type_id => $coupon_ids) {
$num = model('promotion_coupon')->delete([ [ 'coupon_id', 'in', $coupon_ids ], [ 'site_id', '=', $site_id ], [ 'state', '=', 1 ] ]);
if ($num) model('promotion_coupon_type')->setDec([ [ 'coupon_type_id', '=', $coupon_type_id ] ], 'lead_count', $num);
}
model('promotion_coupon')->commit();
return $this->success();
} catch (\Exception $e) {
model('promotion_coupon')->rollback();
return $this->error('', '回收失败');
}
}
/**
* 专用于撤回活动赠送的优惠券
* @return void
*/
public function cancelByPromotion($data){
$member_id = $data['member_id'];
$coupon_data = $data['coupon_data'];//优惠券id相关项
$coupon_ids = array_column($coupon_data, 'coupon_type_id');
$member_coupon_list = model('promotion_coupon')->getList([
['member_id', '=', $member_id],
['coupon_type_id', 'in', $coupon_ids],
['state', '=', CouponDict::normal]
], '*');
$member_coupon_type_group_list = [];
foreach($member_coupon_list as $v){
$member_coupon_type_group_list[$v['coupon_type_id']][] = $v['coupon_id'];
}
$cancel_ids = [];
foreach ($coupon_data as $item) {
$coupon_type_id = $item['coupon_type_id'];
$num = $item['num'];
$item_coupon_type_group = $member_coupon_type_group_list[$coupon_type_id] ?? [];
if($item_coupon_type_group){
if(count($item_coupon_type_group) > $num){
$cancel_ids = array_merge($cancel_ids, array_slice($item_coupon_type_group, 0, $num));
}else{
$cancel_ids = array_merge($cancel_ids, $item_coupon_type_group);
}
}
}
model('promotion_coupon')->update(['state' => CouponDict::close], [['coupon_id', 'in', $cancel_ids]]);
return $this->success();
}
<?php
namespace addon\coupon\model;
use addon\coupon\dict\CouponDict;
use app\model\BaseModel;
/**
* 优惠券
*/
class MemberCoupon extends BaseModel
{
/**
* 获取会员已领取优惠券
* @param $member_id
* @param $state
* @param int $site_id
* @param string $order
* @return array
*/
public function getMemberCouponList($member_id, $state, $site_id = 0, $order = "fetch_time desc")
{
$condition = array (
[ "member_id", "=", $member_id ],
[ "state", "=", $state ],
);
if ($site_id > 0) {
$condition[] = [ "site_id", "=", $site_id ];
}
$list = model("promotion_coupon")->getList($condition, "*", $order, '', '', '', 0);
return $this->success($list);
}
/**
* 使用优惠券
* @param $coupon_id
* @param $member_id
* @param int $order_id
* @return array
*/
public function useMemberCoupon($coupon_id, $member_id, $order_id = 0)
{
//优惠券处理方案
$result = model('promotion_coupon')->update([ 'use_order_id' => $order_id, 'state' => 2, 'use_time' => time() ], [ [ 'coupon_id', '=', $coupon_id ], [ "member_id", "=", $member_id ], [ 'state', '=', 1 ] ]);
if ($result === false) {
return $this->error();
}
return $this->success();
}
/**
* 获取会员已领取优惠券数量
* @param $member_id
* @param $state
* @param int $site_id
* @return array
*/
public function getMemberCouponNum($member_id, $state, $site_id = 0)
{
$condition = array (
[ "member_id", "=", $member_id ],
[ "state", "=", $state ],
);
if ($site_id > 0) {
$condition[] = [ "site_id", "=", $site_id ];
}
$num = model("promotion_coupon")->getCount($condition);
return $this->success($num);
}
/**
* 会员是否可领取该优惠券
* @param $coupon_type_id
* @param $member_id
* @return array
*/
public function receivedNum($coupon_type_id, $member_id)
{
$received_num = model('promotion_coupon')->getCount([ [ 'coupon_type_id', '=', $coupon_type_id ], [ 'member_id', '=', $member_id ] ]);
return $this->success($received_num);
}
/**
* 获取编码
*/
public function getCode()
{
return random_keys(8);
}
/**
* 会员批量发放优惠券
* @param $coupon_type_ids
* @param $site_id
* @param $member_id
* @param int $get_type
* @param int $is_stock
* @param int $related_id
* @return array
*/
public function sendCoupon($coupon_type_ids, $site_id, $member_id, $get_type = 4, $is_stock = 0, $related_id = 0)
{
//已选优惠券提交数组
if (!empty($coupon_type_ids)) {
$res = 0;
foreach ($coupon_type_ids as $coupon_type_id) {
$coupon_type_info = model('promotion_coupon_type')->getInfo([ 'coupon_type_id' => $coupon_type_id, 'site_id' => $site_id, 'status' => 1 ]);
if (!empty($coupon_type_info)) {
if ($coupon_type_info[ 'count' ] != -1 || $is_stock == 0) {
if ($coupon_type_info[ 'count' ] == $coupon_type_info[ 'lead_count' ]) {
return $this->error('', '来迟了该优惠券已被领取完了');
}
}
if ($coupon_type_info[ 'max_fetch' ] != 0 && $get_type == 2) {
//限制领取
$member_receive_num = model('promotion_coupon')->getCount([
'coupon_type_id' => $coupon_type_id,
'member_id' => $member_id,
'get_type' => 2
]);
if ($member_receive_num >= $coupon_type_info[ 'max_fetch' ] ) {
return $this->error('', '该优惠券领取已达到上限');
}
}
$data = [
'coupon_type_id' => $coupon_type_id,
'site_id' => $site_id,
'coupon_code' => $this->getCode(),
'member_id' => $member_id,
'money' => $coupon_type_info[ 'money' ],
'state' => 1,
'get_type' => $get_type,
'goods_type' => $coupon_type_info[ 'goods_type' ],
'fetch_time' => time(),
'coupon_name' => $coupon_type_info[ 'coupon_name' ],
'at_least' => $coupon_type_info[ 'at_least' ],
'type' => $coupon_type_info[ 'type' ],
'discount' => $coupon_type_info[ 'discount' ],
'discount_limit' => $coupon_type_info[ 'discount_limit' ],
'goods_ids' => $coupon_type_info[ 'goods_ids' ],
'related_id' => $related_id
];
if ($coupon_type_info[ 'validity_type' ] == 0) {
$data[ 'end_time' ] = $coupon_type_info[ 'end_time' ];
} elseif ($coupon_type_info[ 'validity_type' ] == 1) {
$data[ 'end_time' ] = ( time() + $coupon_type_info[ 'fixed_term' ] * 86400 );
}
$res = model('promotion_coupon')->add($data);
if ($is_stock == 0) {
model('promotion_coupon_type')->setInc([ [ 'coupon_type_id', '=', $coupon_type_id ] ], 'lead_count');
}
}
}
if ($res) {
return $this->success($res);
} else {
return $this->error();
}
} else {
return $this->error();
}
}
/**
* 回收优惠券
* @param array $coupon_list
* @param $site_id
* @return array
*/
public function recoveryCoupon(array $coupon_list, $site_id)
{
$coupon = [];
foreach ($coupon_list as $coupon_item) {
if (isset($coupon[ $coupon_item[ 'coupon_type_id' ] ])) {
$coupon[$coupon_item['coupon_type_id']][] = $coupon_item['coupon_id'];
} else {
$coupon[ $coupon_item[ 'coupon_type_id' ] ] = [ $coupon_item[ 'coupon_id' ] ];
}
}
if (!count($coupon)) return $this->error();
model('promotion_coupon')->startTrans();
try {
foreach ($coupon as $coupon_type_id => $coupon_ids) {
$num = model('promotion_coupon')->delete([ [ 'coupon_id', 'in', $coupon_ids ], [ 'site_id', '=', $site_id ], [ 'state', '=', 1 ] ]);
if ($num) model('promotion_coupon_type')->setDec([ [ 'coupon_type_id', '=', $coupon_type_id ] ], 'lead_count', $num);
}
model('promotion_coupon')->commit();
return $this->success();
} catch (\Exception $e) {
model('promotion_coupon')->rollback();
return $this->error('', '回收失败');
}
}
/**
* 专用于撤回活动赠送的优惠券
* @return void
*/
public function cancelByPromotion($data){
$member_id = $data['member_id'];
$coupon_data = $data['coupon_data'];//优惠券id相关项
$coupon_ids = array_column($coupon_data, 'coupon_type_id');
$member_coupon_list = model('promotion_coupon')->getList([
['member_id', '=', $member_id],
['coupon_type_id', 'in', $coupon_ids],
['state', '=', CouponDict::normal]
], '*');
$member_coupon_type_group_list = [];
foreach($member_coupon_list as $v){
$member_coupon_type_group_list[$v['coupon_type_id']][] = $v['coupon_id'];
}
$cancel_ids = [];
foreach ($coupon_data as $item) {
$coupon_type_id = $item['coupon_type_id'];
$num = $item['num'];
$item_coupon_type_group = $member_coupon_type_group_list[$coupon_type_id] ?? [];
if($item_coupon_type_group){
if(count($item_coupon_type_group) > $num){
$cancel_ids = array_merge($cancel_ids, array_slice($item_coupon_type_group, 0, $num));
}else{
$cancel_ids = array_merge($cancel_ids, $item_coupon_type_group);
}
}
}
model('promotion_coupon')->update(['state' => CouponDict::close], [['coupon_id', 'in', $cancel_ids]]);
return $this->success();
}
}

View File

@@ -1,103 +1,94 @@
<?php
/**
*/
namespace addon\coupon\model\share;
use app\model\share\WchatShareBase as BaseModel;
use app\model\system\Config as ConfigModel;
use app\model\system\Site as SiteModel;
/**
* 分享
*/
class WchatShare extends BaseModel
{
protected $config = [
[
'title' => '领券中心',
'config_key' => 'WCHAT_SHARE_CONFIG_COUPON_LIST',
'path' => [ '/pages_tool/goods/coupon' ],
'method_prefix' => 'couponList',
],
];
/**
* 商品列表分享数据
* @param $param
* @return array
*/
protected function couponListShareData($param)
{
$site_id = $param[ 'site_id' ] ?? 0;
//站点设置
$site_model = new SiteModel();
$site_info = $site_model->getSiteInfo([ [ 'site_id', '=', $site_id ] ])[ 'data' ];
//跳转路径
$link = $this->getShareLink($param);
//获取和替换配置数据
$config_method = preg_replace('/Data$/', 'Config', __FUNCTION__);
$config_data = $this->$config_method($param);
$title = $config_data[ 'value' ][ 'title' ];
$desc = $config_data[ 'value' ][ 'desc' ];
$image_url = $config_data[ 'value' ][ 'imgUrl' ] ?: $site_info[ 'logo_square' ];
$data = [
'title' => $title,
'desc' => $desc,
'link' => $link,
'imgUrl' => $image_url,
];
return [
'permission' => [
'hideOptionMenu' => false,
'hideMenuItems' => [],
],
'data' => $data,//分享内容
];
}
/**
* 商品列表分享配置
* @param $param
* @return array
*/
protected function couponListShareConfig($param)
{
$site_id = $param[ 'site_id' ];
$config = $param[ 'config' ];
$config_model = new ConfigModel();
$data = $config_model->getConfig([
[ 'site_id', '=', $site_id ],
[ 'app_module', '=', 'shop' ],
[ 'config_key', '=', $config[ 'config_key' ] ],
])[ 'data' ];
if (empty($data[ 'value' ])) {
$data[ 'value' ] = [
'title' => '送你一张优惠券',
'desc' => "优惠多多\n好物多多",
'imgUrl' => '',
];
}
if (empty($data[ 'value' ][ 'imgUrl' ])) {
$data[ 'value' ][ 'imgUrl' ] = img('addon/coupon/icon.png');
}
$variable = [];
return [
'value' => $data[ 'value' ],
'variable' => $variable,
];
}
}
<?php
namespace addon\coupon\model\share;
use app\model\share\WchatShareBase as BaseModel;
use app\model\system\Config as ConfigModel;
use app\model\system\Site as SiteModel;
/**
* 分享
*/
class WchatShare extends BaseModel
{
protected $config = [
[
'title' => '领券中心',
'config_key' => 'WCHAT_SHARE_CONFIG_COUPON_LIST',
'path' => [ '/pages_tool/goods/coupon' ],
'method_prefix' => 'couponList',
],
];
/**
* 商品列表分享数据
* @param $param
* @return array
*/
protected function couponListShareData($param)
{
$site_id = $param[ 'site_id' ] ?? 0;
//站点设置
$site_model = new SiteModel();
$site_info = $site_model->getSiteInfo([ [ 'site_id', '=', $site_id ] ])[ 'data' ];
//跳转路径
$link = $this->getShareLink($param);
//获取和替换配置数据
$config_method = preg_replace('/Data$/', 'Config', __FUNCTION__);
$config_data = $this->$config_method($param);
$title = $config_data[ 'value' ][ 'title' ];
$desc = $config_data[ 'value' ][ 'desc' ];
$image_url = $config_data[ 'value' ][ 'imgUrl' ] ?: $site_info[ 'logo_square' ];
$data = [
'title' => $title,
'desc' => $desc,
'link' => $link,
'imgUrl' => $image_url,
];
return [
'permission' => [
'hideOptionMenu' => false,
'hideMenuItems' => [],
],
'data' => $data,//分享内容
];
}
/**
* 商品列表分享配置
* @param $param
* @return array
*/
protected function couponListShareConfig($param)
{
$site_id = $param[ 'site_id' ];
$config = $param[ 'config' ];
$config_model = new ConfigModel();
$data = $config_model->getConfig([
[ 'site_id', '=', $site_id ],
[ 'app_module', '=', 'shop' ],
[ 'config_key', '=', $config[ 'config_key' ] ],
])[ 'data' ];
if (empty($data[ 'value' ])) {
$data[ 'value' ] = [
'title' => '送你一张优惠券',
'desc' => "优惠多多\n好物多多",
'imgUrl' => '',
];
}
if (empty($data[ 'value' ][ 'imgUrl' ])) {
$data[ 'value' ][ 'imgUrl' ] = img('addon/coupon/icon.png');
}
$variable = [];
return [
'value' => $data[ 'value' ],
'variable' => $variable,
];
}
}

View File

@@ -1,91 +1,82 @@
<?php
/**
*/
namespace addon\coupon\model\share;
use app\model\share\WeappShareBase;
use app\model\system\Config as ConfigModel;
/**
* 分享
*/
class WeappShare extends WeappShareBase
{
protected $config = [
[
'title' => '领券中心',
'config_key' => 'WEAPP_SHARE_CONFIG_COUPON_LIST',
'path' => [ '/pages_tool/goods/coupon' ],
'method_prefix' => 'couponList',
],
];
protected $sort = 2;
/***************************** 领券中心 ***************************/
/**
* 首页分享数据
* @param $param
* @return array
*/
protected function couponListShareData($param)
{
//获取和替换配置数据
$config_data = $this->couponListShareConfig($param);
$title = $config_data[ 'value' ][ 'title' ];
$image_url = $config_data[ 'value' ][ 'imageUrl' ] ? img($config_data[ 'value' ][ 'imageUrl' ]) : '';
$path = $this->getSharePath($param);
$data = [
'title' => $title,
'path' => $path,
'imageUrl' => $image_url,
];
return [
'permission' => [
'onShareAppMessage' => true,
'onShareTimeline' => true,
],
'data' => $data,//分享内容
];
}
/**
* 首页分享配置
* @param $param
* @return array
*/
protected function couponListShareConfig($param)
{
$site_id = $param[ 'site_id' ];
$config = $param[ 'config' ];
$config_model = new ConfigModel();
$data = $config_model->getConfig([
[ 'site_id', '=', $site_id ],
[ 'app_module', '=', 'shop' ],
[ 'config_key', '=', $config[ 'config_key' ] ],
])[ 'data' ];
if (empty($data[ 'value' ])) {
$data[ 'value' ] = [
'title' => '送你一张优惠券,快来领取吧',
'imageUrl' => '',
];
}
$variable = [];
return [
'value' => $data[ 'value' ],
'variable' => $variable,
];
}
}
<?php
namespace addon\coupon\model\share;
use app\model\share\WeappShareBase;
use app\model\system\Config as ConfigModel;
/**
* 分享
*/
class WeappShare extends WeappShareBase
{
protected $config = [
[
'title' => '领券中心',
'config_key' => 'WEAPP_SHARE_CONFIG_COUPON_LIST',
'path' => [ '/pages_tool/goods/coupon' ],
'method_prefix' => 'couponList',
],
];
protected $sort = 2;
/***************************** 领券中心 ***************************/
/**
* 首页分享数据
* @param $param
* @return array
*/
protected function couponListShareData($param)
{
//获取和替换配置数据
$config_data = $this->couponListShareConfig($param);
$title = $config_data[ 'value' ][ 'title' ];
$image_url = $config_data[ 'value' ][ 'imageUrl' ] ? img($config_data[ 'value' ][ 'imageUrl' ]) : '';
$path = $this->getSharePath($param);
$data = [
'title' => $title,
'path' => $path,
'imageUrl' => $image_url,
];
return [
'permission' => [
'onShareAppMessage' => true,
'onShareTimeline' => true,
],
'data' => $data,//分享内容
];
}
/**
* 首页分享配置
* @param $param
* @return array
*/
protected function couponListShareConfig($param)
{
$site_id = $param[ 'site_id' ];
$config = $param[ 'config' ];
$config_model = new ConfigModel();
$data = $config_model->getConfig([
[ 'site_id', '=', $site_id ],
[ 'app_module', '=', 'shop' ],
[ 'config_key', '=', $config[ 'config_key' ] ],
])[ 'data' ];
if (empty($data[ 'value' ])) {
$data[ 'value' ] = [
'title' => '送你一张优惠券,快来领取吧',
'imageUrl' => '',
];
}
$variable = [];
return [
'value' => $data[ 'value' ],
'variable' => $variable,
];
}
}

View File

@@ -1,110 +1,102 @@
<?php
/**
*/
namespace addon\coupon\shopapi\controller;
use addon\coupon\model\Coupon as CouponModel;
use addon\coupon\model\CouponType as CouponTypeModel;
use addon\coupon\model\MemberCoupon;
use app\shopapi\controller\BaseApi;
/**
* 优惠券
*/
class Coupon extends BaseApi
{
public function __construct()
{
//执行父类构造函数
parent::__construct();
$token = $this->checkToken();
if ($token[ 'code' ] < 0) {
echo $this->response($token);
exit;
}
}
/**
* 活动列表
*/
public function lists()
{
$coupon_type_model = new CouponTypeModel();
$page = $this->params['page'] ?? 1;
$page_size = $this->params['page_size'] ?? PAGE_LIST_ROWS;
$coupon_name = $this->params['coupon_name'] ?? '';
$status = $this->params['status'] ?? '';
$condition = [];
if ($status !== '') {
$condition[] = [ 'status', '=', $status ];
}
$type = $this->params['type'] ?? '';
if ($type) {
$condition[] = [ 'type', '=', $type ];
}
//类型
$validity_type = $this->params['validity_type'] ?? '';
if ($validity_type) {
$start_time = $this->params['start_time'] ?? '';
$end_time = $this->params['end_time'] ?? '';
switch ( $validity_type ) {
case 1: //固定
$condition[] = [ 'end_time', 'between', [ $start_time, $end_time ] ];
break;
case 2:
$condition[] = [ 'fixed_term', 'between', [ $start_time, $end_time ] ];
break;
}
}
$condition[] = [ 'site_id', '=', $this->site_id ];
$condition[] = [ 'coupon_name', 'like', '%' . $coupon_name . '%' ];
$order = 'create_time desc';
$field = '*';
$res = $coupon_type_model->getCouponTypePageList($condition, $page, $page_size, $order, $field);
//获取优惠券状态
$coupon_type_status_arr = $coupon_type_model->getCouponTypeStatus();
foreach ($res[ 'data' ][ 'list' ] as $key => $val) {
$res[ 'data' ][ 'list' ][ $key ][ 'status_name' ] = $coupon_type_status_arr[ $val[ 'status' ] ];
}
return $this->response($res);
}
/**
* 发放优惠券
*/
public function send()
{
$member_id = $this->params['member_id'] ?? 0;
$coupon_type_ids = $this->params['parent'] ?? '';
$get_type = $this->params['get_type'] ?? 4;
$site_id = $this->site_id;
$parent = $coupon_type_ids;
if (empty($parent)) {
return $this->error('', 'REQUEST_COUPON_TYPE_ID');
}
$parent = explode(',', $parent);
if (count($parent) == 1) {
$coupon_model = new CouponModel();
$res = $coupon_model->receiveCoupon($parent[ 0 ], $site_id, $member_id, $get_type);
} else {
$member_coupon_model = new MemberCoupon();
$res = $member_coupon_model->sendCoupon(explode(',', $coupon_type_ids), $site_id, $member_id, $get_type);
}
return $this->response($res);
}
<?php
namespace addon\coupon\shopapi\controller;
use addon\coupon\model\Coupon as CouponModel;
use addon\coupon\model\CouponType as CouponTypeModel;
use addon\coupon\model\MemberCoupon;
use app\shopapi\controller\BaseApi;
/**
* 优惠券
*/
class Coupon extends BaseApi
{
public function __construct()
{
//执行父类构造函数
parent::__construct();
$token = $this->checkToken();
if ($token[ 'code' ] < 0) {
echo $this->response($token);
exit;
}
}
/**
* 活动列表
*/
public function lists()
{
$coupon_type_model = new CouponTypeModel();
$page = $this->params['page'] ?? 1;
$page_size = $this->params['page_size'] ?? PAGE_LIST_ROWS;
$coupon_name = $this->params['coupon_name'] ?? '';
$status = $this->params['status'] ?? '';
$condition = [];
if ($status !== '') {
$condition[] = [ 'status', '=', $status ];
}
$type = $this->params['type'] ?? '';
if ($type) {
$condition[] = [ 'type', '=', $type ];
}
//类型
$validity_type = $this->params['validity_type'] ?? '';
if ($validity_type) {
$start_time = $this->params['start_time'] ?? '';
$end_time = $this->params['end_time'] ?? '';
switch ( $validity_type ) {
case 1: //固定
$condition[] = [ 'end_time', 'between', [ $start_time, $end_time ] ];
break;
case 2:
$condition[] = [ 'fixed_term', 'between', [ $start_time, $end_time ] ];
break;
}
}
$condition[] = [ 'site_id', '=', $this->site_id ];
$condition[] = [ 'coupon_name', 'like', '%' . $coupon_name . '%' ];
$order = 'create_time desc';
$field = '*';
$res = $coupon_type_model->getCouponTypePageList($condition, $page, $page_size, $order, $field);
//获取优惠券状态
$coupon_type_status_arr = $coupon_type_model->getCouponTypeStatus();
foreach ($res[ 'data' ][ 'list' ] as $key => $val) {
$res[ 'data' ][ 'list' ][ $key ][ 'status_name' ] = $coupon_type_status_arr[ $val[ 'status' ] ];
}
return $this->response($res);
}
/**
* 发放优惠券
*/
public function send()
{
$member_id = $this->params['member_id'] ?? 0;
$coupon_type_ids = $this->params['parent'] ?? '';
$get_type = $this->params['get_type'] ?? 4;
$site_id = $this->site_id;
$parent = $coupon_type_ids;
if (empty($parent)) {
return $this->error('', 'REQUEST_COUPON_TYPE_ID');
}
$parent = explode(',', $parent);
if (count($parent) == 1) {
$coupon_model = new CouponModel();
$res = $coupon_model->receiveCoupon($parent[ 0 ], $site_id, $member_id, $get_type);
} else {
$member_coupon_model = new MemberCoupon();
$res = $member_coupon_model->sendCoupon(explode(',', $coupon_type_ids), $site_id, $member_id, $get_type);
}
return $this->response($res);
}
}

File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More