diff --git a/.env.docker b/.env.docker index ef9a63b14..af3a54dea 100644 --- a/.env.docker +++ b/.env.docker @@ -1,13 +1,14 @@ APP_NAME=DooTask APP_ENV=local APP_KEY= -APP_DEBUG=true +APP_DEBUG=false APP_SCHEME=auto APP_URL=http://localhost APP_ID= APP_IPPR= APP_PORT=2222 +APP_DEV_PORT= LOG_CHANNEL=stack LOG_LEVEL=debug @@ -53,6 +54,9 @@ PUSHER_APP_KEY= PUSHER_APP_SECRET= PUSHER_APP_CLUSTER=mt1 +JUKE_KEY_JOKE= +JUKE_KEY_SOUP= + MIX_PUSHER_APP_KEY="${PUSHER_APP_KEY}" MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}" diff --git a/.github/workflows/electron-generic.yml b/.github/workflows/electron-generic.yml deleted file mode 100644 index c318fda58..000000000 --- a/.github/workflows/electron-generic.yml +++ /dev/null @@ -1,75 +0,0 @@ -name: Build Generic - -on: - push: - tags: - - 'v*' - -jobs: - build-mac: - runs-on: macos-latest - environment: build - - if: startsWith(github.event.ref, 'refs/tags/v') - steps: - - name: Checkout - uses: actions/checkout@v2 - - - name: Create changelog text - id: changelog - uses: loopwerk/tag-changelog@v1 - with: - token: ${{ secrets.GH_PAT }} - exclude_types: other,chore,build - - - name: Use Node.js 16.x - uses: actions/setup-node@v1 - with: - node-version: 16.x - - - name: Build for MacOS - env: - APPLEID: ${{ secrets.APPLEID }} - APPLEIDPASS: ${{ secrets.APPLEIDPASS }} - CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }} - CSC_LINK: ${{ secrets.CSC_LINK }} - PROVIDER: "generic" - RELEASE_BODY: ${{ steps.changelog.outputs.changes }} - run: ./cmd electron build-mac - - build-win: - runs-on: windows-latest - environment: build - - if: startsWith(github.event.ref, 'refs/tags/v') - steps: - - name: Checkout - uses: actions/checkout@v2 - - - name: Create changelog text - id: changelog - uses: loopwerk/tag-changelog@v1 - with: - token: ${{ secrets.GH_PAT }} - exclude_types: other,chore,build - - - name: Use Node.js 16.x - uses: actions/setup-node@v1 - with: - node-version: 16.x - - - name: Build for Windows - shell: powershell - env: - PROVIDER: "generic" - RELEASE_BODY: ${{ steps.changelog.outputs.changes }} - run: | - npm install - cd electron - npm install - cd ../ - mkdir -p ./electron/public - cp ./electron/index.html ./electron/public/index.html - npx mix --production -- --env --electron - node ./electron/build.js build-win - diff --git a/.github/workflows/electron-main.yml b/.github/workflows/electron-main.yml deleted file mode 100644 index 5ed7119f1..000000000 --- a/.github/workflows/electron-main.yml +++ /dev/null @@ -1,85 +0,0 @@ -name: Build Main - -on: - push: - tags: - - 'v*' - -jobs: - release: - runs-on: ubuntu-latest - - if: startsWith(github.event.ref, 'refs/tags/v') - steps: - - name: Checkout - uses: actions/checkout@v2 - - - name: Create changelog text - id: changelog - uses: loopwerk/tag-changelog@v1 - with: - token: ${{ secrets.GH_PAT }} - exclude_types: other,chore,build - - - name: Create release - uses: actions/create-release@latest - env: - GITHUB_TOKEN: ${{ secrets.GH_PAT }} - with: - tag_name: ${{ github.ref }} - release_name: Release ${{ github.ref }} - body: ${{ steps.changelog.outputs.changes }} - - build-mac: - runs-on: macos-latest - environment: build - - if: startsWith(github.event.ref, 'refs/tags/v') - steps: - - name: Checkout - uses: actions/checkout@v2 - - - name: Use Node.js 16.x - uses: actions/setup-node@v1 - with: - node-version: 16.x - - - name: Build for MacOS - env: - APPLEID: ${{ secrets.APPLEID }} - APPLEIDPASS: ${{ secrets.APPLEIDPASS }} - CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }} - CSC_LINK: ${{ secrets.CSC_LINK }} - GH_TOKEN: ${{ secrets.GH_PAT }} - EP_PRE_RELEASE: true - run: ./cmd electron build-mac - - build-win: - runs-on: windows-latest - environment: build - - if: startsWith(github.event.ref, 'refs/tags/v') - steps: - - name: Checkout - uses: actions/checkout@v2 - - - name: Use Node.js 16.x - uses: actions/setup-node@v1 - with: - node-version: 16.x - - - name: Build for Windows - shell: powershell - env: - GH_TOKEN: ${{ secrets.GH_PAT }} - EP_PRE_RELEASE: true - run: | - npm install - cd electron - npm install - cd ../ - mkdir -p ./electron/public - cp ./electron/index.html ./electron/public/index.html - npx mix --production -- --env --electron - node ./electron/build.js build-win - diff --git a/.github/workflows/publish-desktop.yml b/.github/workflows/publish-desktop.yml new file mode 100644 index 000000000..a8c1511b4 --- /dev/null +++ b/.github/workflows/publish-desktop.yml @@ -0,0 +1,33 @@ +name: Publish Desktop + +on: + push: + tags: + - 'v*' + +jobs: + build: + runs-on: macos-latest + environment: build + + if: startsWith(github.event.ref, 'refs/tags/v') + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Use Node.js 16.x + uses: actions/setup-node@v1 + with: + node-version: 16.x + + - name: Build + env: + APPLEID: ${{ secrets.APPLEID }} + APPLEIDPASS: ${{ secrets.APPLEIDPASS }} + CSC_LINK: ${{ secrets.CSC_LINK }} + CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }} + DP_KEY: ${{ secrets.DP_KEY }} + GH_TOKEN: ${{ secrets.GH_TOKEN }} + GH_REPOSITORY: ${{ github.repository }} + run: ./cmd electron all + diff --git a/.gitignore b/.gitignore index 826c00caa..eb3f4b4a4 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ /public/.well-known /public/.user.ini /storage/*.key +/config/LICENSE /vendor /build /tmp @@ -14,7 +15,6 @@ .vscode .vagrant .phpunit.result.cache -CHANGELOG.md Homestead.json Homestead.yaml npm-debug.log diff --git a/.gitmodules b/.gitmodules index 32d75439b..670933a56 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,6 @@ [submodule "resources/drawio"] path = resources/drawio url = https://github.com/jgraph/drawio.git +[submodule "resources/mobile"] + path = resources/mobile + url = https://github.com/kuaifan/dootask-app.git diff --git a/.gitpod.yml b/.gitpod.yml new file mode 100644 index 000000000..f2ba8ab41 --- /dev/null +++ b/.gitpod.yml @@ -0,0 +1,13 @@ +# This configuration file was automatically generated by Gitpod. +# Please adjust to your needs (see https://www.gitpod.io/docs/config-gitpod-file) +# and commit this file to your remote git repository to share the goodness with others. + +tasks: + - init: sudo ./cmd install + command: ./cmd dev + +ports: + - port: 2222 + visibility: public + - port: 22222 + visibility: public diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 000000000..53d771fe6 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,1959 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +## [0.25.39] + +### Bug Fixes + +- 无法浏览表情图片 + +### Features + +- 更多emoji表情回复 + +### Performance + +- 优化录音效果 +- 优化缓存 +- 优化语音播放 + +## [0.25.11] + +### Performance + +- 优化输入草稿 +- 搜索会员默认机器人排在最后 + +## [0.25.7] + +### Bug Fixes + +- 操作菜单导致的页面错位 + +## [0.25.0] + +### Bug Fixes + +- 列表模式下重命名文件名称导致其他文件重命名的情况 +- 桌面端开启子窗口消息数倍数增长的问题 + +### Performance + +- 优化emoji表情分类 +- Android表情输入框跟键盘同时出现的情况 +- 检测文件名称包含特殊字符 +- 通过#发送任务显示项目内已完成任务 +- 优化通讯录数量 +- 优化token到期时间 +- 管理员通讯录显示新帐号 +- 管理员可以移除全员群人员 +- 优化图片浏览 + +## [0.24.85] + +### Bug Fixes + +- 安装时机器人也进入全员群 +- 移出任务后还在项目里但看不到任务 + +### Performance + +- 网络不好时发送消息顺序问题 +- 移动端搜索消息支持滑动取消搜索 +- 优化 doo 模块 +- 新增doo模块 +- 新增doo模块 +- 新增doo模块 +- 优化数据删除 + +## [0.24.58] + +### Bug Fixes + +- 客户端无法关闭窗口的情况 +- 仅显示我的文件时无法创建文件 +- 重命名别人共享的文件后不见了 + +### Features + +- 搜索会话消息 + +### Performance + +- 新增管理机器人菜单 +- 整理请求外部接口 +- 可以通过ID搜索任务 +- 会员选择框支持搜索拼音 +- 文件消息新增显示文件菜单 +- 优化链接识别 +- 隐藏共享文件改为仅显示我的 +- 文件名称显示两行 +- 子任务允许多个负责人 +- 优化搜索 +- 搜索消息禁止右键 +- 重写项目和会话接口数据 +- 重写更新和删除方法 +- 优化数据同步 + +## [0.24.30] + +### Bug Fixes + +- 思维导图快捷键保存 + +### Features + +- 支持发送匿名消息 + +### Performance + +- 优化表情回复 +- 消息快捷发送菜单 +- 优化消息类型分类 +- 文件列表支持隐藏共享文件 +- 群组支持修改头像 +- 优化阅读消息 +- 点击会话消息头像@ +- 通讯录显示部门负责人 +- 加载更多消息safari兼容性 +- 优化签到数据 +- 点击头像进入对话 +- 优化开发执行脚本 +- 网络恢复后重新标记已读失败的信息 +- Dialog loading +- 优化再次点击抖动 +- 优化ipad表单显示 +- 工作包括周报模板添加下周拟定计划项 + +## [0.23.86] + +### Bug Fixes + +- App cross domain +- 桌面端已知bug +- 从任务窗口发送聊天输入缓存的问题 +- 桌面端新窗口打开任务无法发起聊天的问题 +- Ldap一处报错 + +### Performance + +- 优化文件分享链接 +- 非工作日不推送签到提醒 +- 设置免打扰后被@也推送通知 +- 任务完成通知流程状态 +- 优化会话列表数据加载 + +## [0.23.62] + +### Bug Fixes + +- 没有后缀名无法下载文件的问题 +- 打卡提醒失效 +- 移动端在任务提醒打开任务无法聊天的问题 + +### Performance + +- 优化ws连接机制 +- Office、图表、文本国际化 +- 优化移动端设置 +- 再次点击消息图标闪动未读对话 +- 优化已读标记 +- 移动端优化 +- 消息接口支持@邮箱 +- 支持gitpod + +## [0.23.46] + +### Bug Fixes + +- 编辑消息@丢失的问题 +- 修复已知bug + +### Features + +- 新增临时帐号功能 + +### Performance + +- 兼容ipad app样式 +- 添加上班签到提醒消息 +- 完善临时帐号权限 +- 设待办快速选择人员 +- 优化消息数量显示 +- 优化阅读消息 +- 对话顶部提示 +- 样式兼容 +- 优化api国际化 +- 优化国际化 +- 优化ws重连规则 +- 优化翻译 +- 优化更新日志生成 +- 优化首页 + +## [0.22.99] + +### Bug Fixes + +- 移动端应用内通知标题溢出的问题 + +### Performance + +- 机器人支持webhook +- 优化输入框功能提示 +- 优化任务修改时间通知 +- 导出所有超期任务 + +## [0.22.88] + +### Bug Fixes + +- LDAP Exception + +### Features + +- 二维码登录 + +### Performance + +- 优化删除数据 + +## [0.22.84] + +### Bug Fixes + +- 栏目内添加任务应该直接归属此栏目 + +### Features + +- 新增ldap帐号 + +### Performance + +- 优化会话删除 +- 优化表情输入 +- 优化根据会员筛选任务 +- Drawio文件支持导出pdf文件 +- 优化任务提醒 +- 优化state数据结构 +- 聊天设置待办时可快速选择 +- 完善LDAP +- 优化移动端(pad) +- 优化消息列表数据 + +## [0.22.66] + +### Bug Fixes + +- 任务首次发消息消失的情况 + +### Features + +- 项目面板支持根据成员筛选任务 + +### Performance + +- 优化消息对话框loading +- 优化未读消息提示 +- 优化移动端打开会话 +- 回复/引用机器人消息图标移位的问题 +- 会话顶部提示剩余未读消息 +- 角标最大显示999 + +## [0.22.56] + +### Bug Fixes + +- 导出签到最多只导出20个的问题 +- 截图快捷键的报错 +- 跨月签到记录不显示的问题 + +### Features + +- 上班打开每日开心/下班打卡心灵鸡汤 +- 上班打卡新增每日开心 + +### Performance + +- 导出签到/任务统计名字新增序号 +- 解决桌面端跨域cookie无法携带的问题 + +## [0.22.46] + +### Bug Fixes + +- 跨日/周写工作报告导致的覆盖问题 + +### Performance + +- 优化查看汇报详情loading +- 我的工作汇报列表显示汇报对象 +- 工作汇报可留空汇报对象 +- 优化签到打卡提醒 +- 升级office套件 + +## [0.22.40] + +### Bug Fixes + +- 时间快选 + +### Performance + +- 优化签到通知 +- 任务时间修改提醒 +- 优化导出快速选择 +- 优化修改员工mac地址备注 +- 工作报告模板新增项目名称 +- 优化修改文件名称相同的情况 +- 优化任务APP/邮件提醒 +- 优化主题跟随系统 +- 优化对话列表加载速度 + +## [0.22.22] + +### Bug Fixes + +- 聊天页面出现滚动溢出的问题 +- 会话置顶失效 +- 标记已读失败 +- 因机器人首次安装失败 + +### Features + +- 免打扰会话取消邮件通知 +- 免打扰导致推送角标数量不对 +- 消息会话支持免打扰 + +### Performance + +- 签到成功通知 +- 优化引用机器人消息看不到机器人图标的问题 +- 导出签到/任务统计会员数增加到最多可选100个 +- 会员选择支持全选列表 +- 个人签到设置显示最近签到数据 +- 定时清理异步任务记录 +- 聊天消息长大超过5000转文件发送 + +## [0.22.0] + +### Bug Fixes + +- 下载文件出现文件损坏的情况 +- 清空已完成上传列表 + +### Features + +- 新增机器人 + +### Performance + +- 机器人支持静默推送 +- 优化签到数据 + +## [0.21.96] + +### Performance + +- 优化签到数据结构 + +## [0.21.90] + +### Features + +- 签到功能 + +### Performance + +- Mac地址已存在检查 +- 限制截图快捷键 +- 查看我自己的签到数据 +- 优化导出签到 +- Update office manifest +- 优化打开个人会话速度 +- 优化任务超时提醒文案 +- 优化导出统计 +- 完善签到功能 +- 完善签到功能 +- 优化缓存 +- 优化缓存 +- 缓存迁移 +- 优化本地数据 + +## [0.21.68] + +### Bug Fixes + +- 客户端打开出现报错 +- 客户端提交截图空格报错的问题 +- 上传文件没有读取权限 + +### Features + +- 添加考勤接口 + +### Performance + +- 文件右键菜单直接发送至会话 +- 优化删除或归档项目后数量更新 +- 消息搜索支持会员结果 +- 优化设置菜单 +- 取消universal版本编译 +- 优化客户端通知,Mac支持快速回复 +- 群聊天点击头像进入个人对话 +- 优化会话保留 +- 修改搜索成员文案 +- 聊天和文件模块不限制上传类型 +- 消息列表进行搜索时,条件过长,显示的无结果文案无法完全显示 +- 优化全局表格滚动条 + +## [0.21.42] + +### Performance + +- 优化滚动条 +- 优化消息数量 +- 优化网络错误提示框 +- 网络错误不清空仪表盘数据 + +## [0.21.32] + +### Performance + +- 优化消息&符号 +- 优化移动端网络错误提示 + +## [0.21.26] + +### Bug Fixes + +- 回复数量增长错误的问题 + +### Performance + +- 客户端新增截图快捷键 +- 截图dev +- 优化国际化提升访问速度 + +## [0.21.15] + +### Bug Fixes + +- Safari浏览器兼容性 +- 对话窗口js报错 + +### Performance + +- 添加小兔子工作中表情 + +## [0.21.7] + +### Bug Fixes + +- 链接消息处理问题 + +### Performance + +- 头像标签部门过长显示优化 +- 聊天使用~符号分享文件 +- 修改任务时间添加备注 + +## [0.21.0] + +### Bug Fixes + +- 转让群主后窗口不关闭的问题 +- 通知消息显示UserAvatar + +### Performance + +- @结果相同时避免刷新 +- 离职移交部门 +- 离职后退出所有群 +- 升级onlyoffice + +## [0.20.90] + +### Features + +- 新增部门功能 + +### Performance + +- 个人群支持转让群主 +- 头像显示部门 +- 支持选择已有群为创建部门群 +- 优化表情发送后搜索关键词逻辑 +- 优化搜索表情 +- 支持搜索在线表情 +- 完善部门群组功能 +- 选择器的优化 +- Task进程添加执行记录 + +## [0.20.71] + +### Bug Fixes + +- 未聊天过的任务无法发送聊天表情 +- 离职仍受到推送的问题 +- 任务详情无法右键的问题 + +### Features + +- 聊天支持联想表情 + +### Performance + +- 优化编辑器对象销毁的问题 + +## [0.20.65] + +### Bug Fixes + +- Android进入会议没有声音的问题 +- IOS点击发送图片表情偶尔不显示的情况 + +### Performance + +- 优化会议聊天 +- Win通知标题 +- 主窗口可以单独关闭到后台 +- 会议支持最小化窗口 +- 优化录音、优化会议 + +## [0.20.51] + +### Bug Fixes + +- 搜索文件选择在上层文件夹中显示时如果已经当前文件夹时没有反应的问题 +- 离职员工仍可以接收到邮件的问题 +- 首次聊天因网络问题聊天记录清空的情况 +- 编译已发送的消息中含有任务信息时的未定义问题 +- 新安装出现无法打开其他人员会话的问题 + +### Features + +- 新增任务过期app推送提醒 + +### Performance + +- 优化客户端图片浏览器 +- 聊天内容图片支持下载 +- 优化隐私政策弹窗 +- 自己可以转为任务协助人员 +- 升级element/view-design +- 优化任务队列 + +## [0.20.35] + +### Bug Fixes + +- Umeng mi push + +### Performance + +- 升级election框架 + +## [0.20.23] + +### Performance + +- 搜索排序 + +## [0.20.20] + +### Features + +- 工作报告支持批量标记已读 + +### Performance + +- 操作离职隐藏退出群通知 +- 调整文件表格列表重命名输入框尺寸 +- 群内鼠标悬停成员头像显示聊天按钮 +- 优化消息已读 +- 文件分享链接显示文件名称 + +## [0.20.12] + +### Performance + +- 优化通知 + +## [0.20.5] + +### Performance + +- 优化聊天页面cpu占用 + +## [0.20.3] + +### Bug Fixes + +- 聊天、任务中的md文件预览无法滚动 +- 修改工作报告弹出多次成功提示的问题 +- 安装数据库初始化失败 +- 消息已读 + +### Features + +- Window客户端任务栏闪烁 + +### Performance + +- 升级框架内核 +- 优化消息发送失败 + +## [0.19.95] + +### Bug Fixes + +- 无法添加任务的问题 + +### Features + +- 消息粘贴excel内容自动转成图片 + +### Performance + +- 优化发送图片出现空白的情况 +- 消息发送失败支持再次编辑 +- 对话支持拼音搜索 +- 新增注册自动进入全员群开关 +- 移动客户端群消息通知加上群名称 +- 消息菜单新增复制图片、链接功能 + +## [0.19.75] + +### Bug Fixes + +- 无法下载转发文件的问题 +- 无法操作离职的问题 +- 编辑@消息的问题 +- 删除账号-提示文案修改 +- 删除账号-提示文案修改 +- 修改邮箱-”发送验证码“倒计时未结束修改 +- 删除账户必填加星号;邮箱验证码可以多发送 +- 修改/删除账号接口无权限问题修改;根据env文件'SYSTEM_SETTING'变量判断是否能修改/删除账号 + +### Performance + +- 按录音时停止正在播放的 +- 优化消息列表 +- 优化移除群成员与打开成员对话冲突的情况 +- 优化国际化 +- 优化删除成员 +- 优化编辑带有图片的消息 +- 支持搜索共享文件 +- 优化发消息时有时候出现空白需要滚动才出现内容的情况 + +## [0.19.40] + +### Bug Fixes + +- 音频/视频都不选时无法进入会议的情况 +- 修改邮箱-校验邮箱去掉前后空格 + +### Features + +- 新增删除账户功能 +- 新增修改邮箱功能 + +## [0.19.26] + +### Bug Fixes + +- 待办数量与实际的数量不一致 + +### Performance + +- 角标提示待办跟@一起 +- 移动端任务打开聊天按钮优化 +- 支持转发给最近聊天 +- 可以通过群成员点击打开对话 +- 展示消息回应详情 +- 优化通知类消息字符长度 +- 去除通知里的  + +## [0.19.10] + +### Bug Fixes + +- 任务列表无法修改优先级的问题 + +### Performance + +- 客户端窗口激活自动获取聊天焦点 +- 个人对话窗口支持拨打电话 +- 新增联系电话 +- [notice|tag|todo]类型的消息静默推送 +- 只给一个月内登录App的帐号推送 +- 显示待办消息数量 +- 待办消息支持指定成员 +- 支持查看待办完成情况 + +## [0.18.80] + +### Bug Fixes + +- 任务详情不出现聊天的情况 + +### Features + +- 新增待办消息功能 + +### Performance + +- 优化抖动提示 +- 消息新增#我协助的任务 + +## [0.18.71] + +### Bug Fixes + +- 移动文件夹内文件所有者不变的问题 +- 通知消息一直未读的情况 + +### Performance + +- 回复或修改消息发送时立即隐藏引用显示 +- 搜索对话可以搜索远程的对话 + +## [0.18.58] + +### Bug Fixes + +- 移动文件所有者错误 +- 无法通过项目点击聊天的情况 + +### Features + +- 新增全员群组 +- 支持编辑已发送的消息 + +### Performance + +- 所有项目列表支持筛选个人项目 +- 调整消息标签位置 +- 添加邮件忽略功能 + +## [0.18.44] + +### Bug Fixes + +- 任务聊天出现空白的情况 +- 新建文件不显示的问题 + +### Features + +- 新增消息类型筛选 +- 新增标注消息功能 + +### Performance + +- 优化协助任务的更新 +- 管理员新增修改成员邮箱功能 + +## [0.18.22] + +### Bug Fixes + +- 输入框粘贴后出错的问题 +- 任务重复周期 + +### Performance + +- 支持查看回复列表 +- 优化消息分页加载 +- 添加消息回复量 + +## [0.18.1] + +### Features + +- 新增任务重复周期 + +### Performance + +- 仪表盘列表新增显示协助的任务 + +## [0.17.98] + +### Bug Fixes + +- 任务成员应该禁止退出任务群聊 +- 撤回消息导致未读数错误的问题 +- 部分长按菜单移位的问题 +- 无法点击图片预览的问题 + +### Features + +- 支持通过拼音搜索联系人 + +### Performance + +- 优化@其他成员在线状态 +- 仅(群聊)且(是群主或没有群主)才可以@成员以外的人 +- 优化pdf浏览方式 +- 支持@群聊以外成员 +- 项目群、任务群可添加成员 + +## [0.17.75] + +### Features + +- 支持搜索历史消息 + +### Performance + +- 优化文件操作菜单样式 +- 文件浏览支持滑动返回上一个文件夹 +- 桌面客户端出现无法关闭窗口的情况 +- 优化触发回复页面滚动 +- 优化对话详情页 + +## [0.17.53] + +### Features + +- 新增回复消息功能 + +### Performance + +- 取消置顶标签 +- 优化移动客户端滚动穿透 +- 优化消息列表 + +## [0.17.30] + +### Performance + +- 默认使用文字头像 +- 使用系统浏览器打开新窗口链接 + +## [0.17.20] + +### Bug Fixes + +- 员工删除后对话还存在的问题 + +### Performance + +- 优化通讯录刷新机制 +- 通知自动关闭 +- 优化excel菜单 + +## [0.17.7] + +### Performance + +- 优化文件重名的问题 +- 优化图片预览缩放 +- 优化预览文件 +- 优化同时加载同个任务 + +## [0.16.85] + +### Bug Fixes + +- Win客户端升级签名报错的问题 +- 文件md、text互转时文件格式没有变的问题 +- 移动客户端访问本站链接出现需要登录的情况 +- 不是任务负责人不能通过小窗口发送任务消息的问题 +- 桌面客户端任务独立窗口无法操作任务状态的问题 + +### Performance + +- 优化键盘关闭 +- 优化office右上角菜单按钮重叠的问题 +- 优化录音效果 +- 移动端只读文件 +- 优化任务窗口输入框草稿 +- 头像显示已离职效果 +- 文件文本编辑支持command+s保存 +- 长文本消息的处理 +- 客户端新窗口皮肤不统一的问题 +- 流程图支持搜索远程图标 + +## [0.16.62] + +### Performance + +- 升级office插件 + +## [0.16.60] + +### Bug Fixes + +- 修复任务窗口无法发送表情的问题 + +### Performance + +- 优化消息已读未读 +- 预览图片尺寸的优化 +- 新窗口打开任务时保持日志显示状态 +- 优化首页加载失败的情况 +- 文字发送太长转成文件发送 +- 任务详情窗口尺寸 +- 优化全局任务操作菜单 + +## [0.16.22] + +### Features + +- 新增消息回复表情功能 + +## [0.15.83] + +### Features + +- 添加会议功能 + +### Performance + +- 优化移动端图片预览 +- 移动端长按菜单 + +## [0.15.60] + +### Bug Fixes + +- 文件共享人数太多内容溢出 +- 聊天内容加载中刷新导致无法再继续加载的情况 +- 对话列表点击任务状态标签无法打开对话 +- 任务弹窗无法发送语音 +- 焦点会超出输入框的情况 +- 获取首字母失败的情况 + +### Features + +- 支持发送录音 +- 对话窗口新增会员最后在线时间 + +### Performance + +- 触摸返回中禁止滚动消息列表 +- 撤回语音消息时停止正在播放 +- 保留粘贴的a标签 +- 支持会话自己 +- 聊天内容链接可点击 +- 优化搜索加载提示 +- 项目-任务状态的数量,实时更新数据 +- 优化聊天窗口样式 +- 移动端聊天窗口返回按钮显示未读信息数 +- 优化加载状态 +- 客户端本地通知 +- 聊天输入框草稿 +- Ws重连后重新获取会员基本信息 +- 聊天窗口样式 +- 优化信息邮件格式 +- 优化移交个人项目 + +## [0.14.94] + +### Bug Fixes + +- 邮件通知消息未读对象可能会出错的情况 + +### Performance + +- 优化适配ipad +- 优化客户端生命周期重连ws机制 +- 优化更新对话列表机制 +- 7天内显示时间m-d H:i +- 消息也推送给在其它地方登录的自己 + +## [0.14.72] + +### Performance + +- 聊天输入框支持粘贴文件 +- 优化UserAvatar组件 +- 上传或发送图片太大时压缩显示 +- 仪表盘任务数量、最近打开的任务 +- 优化消息移动端打开动画效果 + +## [0.14.62] + +### Bug Fixes + +- 未读消息邮件头像不显示的问题 +- 修复手机客户端无法预览文件的问题 +- 客户端选择sso登录输入相同地址时提交无反应的问题 +- 推送收到的群组名称为空的情况 +- 任务开始邮件提醒错误的问题 +- Ios键盘遮挡输入框的问题 + +### Performance + +- 未读消息邮件提醒,提醒时把所有未读消息都加上,而不是只提示指定时间的 +- 优化modal内滚动会传播给其他组件的问题 +- 优化任务过多加载卡的情况 +- 点击聊天输入框窗口跳动 +- 支持上传plist格式文件 +- DrawerOverlay 使用 Model +- 手机客户端登录页优化sso登录样式 +- 优化手机客户端登录页切换主题提示 +- 优化消息列表 +- 优化移动端 + +## [0.14.8] + +### Features + +- 邮件通知未读消息 + +### Performance + +- 优化聊天输入框计算样式 +- 优化正则表达式 + +## [0.13.98] + +### Bug Fixes + +- 修复共享文件移动到共享文件夹内共享属性错乱的问题 + +### Performance + +- 移交项目和任务时记录被移交对象 +- 共享的文件禁止移动到另一个共享文件夹内 +- 优化自定义sso登录 + +## [0.13.88] + +### Bug Fixes + +- 同时删除多个任务负责人或协助人员任务动态显示错误的问题 +- 修复 ETooltip 组件 disabled 取消后错位的问题 +- 添加任务窗口选择其他项目无效的问题 + +### Performance + +- 优化文件历史查看 +- 查看文件修改历史时文本编辑器、图表点击编辑历史窗口不隐藏 + +## [0.13.78] + +### Bug Fixes + +- 修复上传文件夹不立即显示的问题 + +### Performance + +- 优化任务详情右键预览图片 + +## [0.13.74] + +### Bug Fixes + +- 修复已打开文件需刷新网页才显示最新内容的情况 + +### Features + +- 新增查看文件历史版本 + +### Performance + +- 文件打开保存机制 +- 客户端升级日志 + +## [0.13.63] + +### Bug Fixes + +- 修复打开pdf因为文件名内容出错的问题 + +### Features + +- 新增聊天选择内容粗体、斜体、删除线、序号等工具 + +### Performance + +- 发送消息未设置昵称的优化 +- 优化共享文件夹图标 +- 优化重复共享提示 +- 优化聊天窗口群聊已读列表 +- 优化任务窗口 + +## [0.13.48] + +### Performance + +- 优化暗黑模式 +- 客户端填写周报后保存关闭窗口 +- 文件浏览保存排序 + +## [0.13.0] + +### Performance + +- 指定mariadb:10.7.3解决部分出现初始化数据库失败的情况 +- 上传限制改为1G + +## [0.12.95] + +### Bug Fixes + +- 工作包括编辑内容不正确的问题 +- 会员选择框偶尔出现默认值错误的情况 +- 部分客户端登录页面报错的问题 +- 退出登录仍出现未读数的情况 + +### Performance + +- 搜索后支持快速取消筛选 +- 优化工作汇报的搜索 +- 新增邮件发送测试 +- 优化用户邮箱验证 +- 优化任务到期前后邮件提醒 +- 消息已完成图标布局优化 +- 优化websocket连接机制 + +## [0.12.77] + +### Features + +- 客户端新增系统托盘图标 + +### Performance + +- 文件权限提示点击确定返回主目录 + +## [0.12.66] + +### Bug Fixes + +- 已完成任务还可以拖动排序的问题 +- 移动端不显示甘特图的问题 + +### Performance + +- 优化聊天窗口显示头像 +- 优化聊天窗口滚动 +- 取消任务群聊发送图片同步到任务附件 +- 优化项目页面聊天窗口 +- 优化网络重连聊天机制 +- 文件查看图片直接弹窗浏览 +- 文件共享成员支持分享链接 + +## [0.12.40] + +### Bug Fixes + +- 设置分页10条每页无效的问题 + +### Performance + +- 优化提示此文件夹内已有共享文件夹 +- 支持上传golang文件 +- 文件新增pids(上级ID递归)字段 + +## [0.12.23] + +### Bug Fixes + +- 客户端打开不自动登录的问题 + +### Performance + +- 优化一些前端 + +## [0.12.19] + +### Bug Fixes + +- 修复新增项目成员无法通过邮箱搜索的问题 +- Public客户端打开空白的情况 +- 所有可搜索列表在非第1页搜索时不返回第1页的问题 +- 查看已发送的工作汇报,汇报对象需横向显示 + +### Performance + +- 优化工作量配色 +- 优化路由重复提示的报错 + +## [0.12.8] + +### Performance + +- 任务详细描述右键新增预览图片 +- 优化图片预览,优化与弹窗esc按键冲突的问题 + +## [0.11.96] + +### Bug Fixes + +- 项目-任务列表tab优化 +- 项目-任务列表tab修改,甘特图数据优化 +- 项目--任务列表删除主任务后,子任务仍显示问题 +- 项目任务甘特图图标及优化移动逻辑 +- 项目任务列表甘特图鼠标悬停图标或文案时样式变为手指 +- 项目任务列表甘特图去掉'已完成'任务及样式优化 +- 项目--删除任务详情删除前有聊天记录显示异常修改 +- 修改项目任务列表甘特图时长及颜色显示不对问题 +- 官网首页修改 +- 修改图片 +- 首页重写 + +### Features + +- 项目任务新增一个甘特图展示选项 +- 客户端登录,新增工作报告、修改工作报告、查看工作报告,全部直接在新窗口打开 + +### Performance + +- 图片预览使用当前页组件,支持多图 +- 优化工作报告前端 +- 优化甘特图 +- 优化任务列表切换显示 +- 更新icon图标库 +- 更新icon图标库 +- 已删除任务详情任务描述改为只读 +- 已删除任务操作文案及显示优化 +- 项目--删除任务查看详情页功能 +- 首页兼容暗黑模式及文案和查询优化 +- 优化消息标记已读/未读 + +## [0.10.30] + +### Bug Fixes + +- 优化验证邮箱页面文案 +- 修正验证邮箱页面文案 +- 修复自定义SSO自动升级版本出错的问题 +- 添加任务时开始时间和结束时间为同一天可能发生报错 +- 拖动任务列表排序后会自动还原的情况 +- 任务第二次邮件提醒判断错误修改 +- '最近打开任务'数据没有根据用户区分问题修改 +- 去掉调试信息 + +### Features + +- 在项目设置里新增一个“已删除任务”菜单 +- 优化表格分页样式 +- 优化TableAction组件 +- 头像加载失败时显示名称首字 +- 消息右键对话新增:标记已读、标记未读 +- 添加“最近打开的任务” + +### Performance + +- 修改验证邮件有效期改回24小时 +- 优化邮件设置页面样式 +- 修改验证邮件有效期为10分钟方便测试(验证完后改为24小时) +- 【邮箱验证】链接过期的提示文案为:链接已失效,请重新登录/注册 +- 任务修改计划时间需要重置任务邮件提醒日志 +- 任务附件过多时仅显示最新50个 +- 消息已读/未读人员优化 +- 消息添加'未读标记'字段 +- 添加任务默认选中自己,如果不选则添加无负责人任务 +- 优化消息未读数 +- 优化置顶后数据请求 +- 优化未读信息数 +- 优化首页仪表盘样式 +- 优化关闭任务独立窗口点击取消后没有自动获取焦点 +- 邮箱验证优化流程提示 +- 任务提醒缩短邮件通知时间区间 + +## [0.10.21] + +### Bug Fixes + +- 清空子任务的时间报错闪现的问题 +- 任务提醒时间格式错误修改 +- 任务统计导出完成时间为空时,不应出现实际完成用时 +- 客户端任务独立窗口修改详情后没有同步到主窗口的问题 +- 客户端任务独立窗口无法按command+s保存任务的问题 +- 任务提醒时间区间错误修改 +- 任务列表缺少参数报错修改 +- 导出任务统计修改 +- 登录账号密码错误时提示修改 + +### Features + +- 任务到期提醒开启邮件通知 + +### Performance + +- 项目、消息对话置顶后要滚动到可以看到它的位置 +- 再次点击滚动到未读条目 +- 优化新窗口打开的任务保存机制 +- 优化新消息等列表滚动 +- 完成任务时任务暂时继续显示,直到路由发生改变 +- 优化仪表盘使用sticky方式 +- 邮箱发送失败提示优化 +- 任务提醒时间区间放大,防止定时器出现意外发送不到问题 +- 网络不好连续按回车导致重复添加子任务 +- 优化任务详情数据结构 +- 补全系统设置中的未翻译地方 +- 修改导出报表文案 + +## [0.10.5] + +### Bug Fixes + +- Md编辑器出现toc混乱的情况 +- 邮箱验证模板页'你'改为'您' +- 邮箱设置异常报错修改 +- 报表导出任务没有流程日志判断优化 +- 修复登录页设置下拉显示不全的情况 +- 处理回滚后异常代码 +- 处理回滚后异常代码 +- 【系统设置】邮件设置提前小时数双向绑定无效问题修改 + +### Features + +- 完成邮箱验证 +- 邮箱验证部分 +- 管理员系统设置新增:新增邮件设置 + +### Performance + +- 优化仪表盘角标数 +- 优化客户端任务详情按command+s保存 +- 优化文件重命名,支持按esc取消编辑 +- 任务详情打开操作菜单时按esc任务窗口隐藏了但是菜单还看见 +- 上传文件名称过程显示错位的问题 +- 退出登录返回登录页而不是注册页 +- 报表导出列及剩余天数优化 +- 网络异常的情况下需提示网络异常而不是系统出错 +- 任务详情当任务倒计时结束时显示"超期未完成"标签 +- 优化脚本,支持部分服务器是docker compose命令 +- 优化已读回执 +- 补全任务'测试'状态样式 +- 导出报表调整 +- 倒计时刚到到达0时会显示自定义才继续显示计时,且未显示超时标签 +- 鼠标滑动至仪表盘中的待完成任务卡片时,卡片周围未显示光晕,且未显示为手指样式 +- 优化任务详细描述显示 +- 仪表盘当前激活的卡片不明显优化 +- 置顶样式优化 + +## [0.9.83] + +### Bug Fixes + +- 修复无法预览pdf文件 +- 首页360浏览器图片函数摆错修改 +- 无法浏览聊天图片的问题 +- 回滚代码后功能完善 +- 项目列表处于置顶选中状态显示选中样式 + +### Features + +- 导出任务功能 +- 文件支持拖动到列表上传 + +### Performance + +- 记录任务工作流变化 +- 优化修改工作流的过程 +- 优化任务排序 +- 支持nodejs16+ +- 首页判断是否登录优先于判断是否需要启动首页 +- 优化首页判断时间一闪而过问题 +- 添加首页文字翻译 + +## [0.9.68] + +### Bug Fixes + +- 登录页重复填写sso地址无法保存的问题 +- 无法移动共享文件夹内创建的文件 +- 客户端无法编辑office文件 +- 客户端无法下载文件 +- 合并异常代码修改 +- 项目、消息置顶样式修改 +- 修复客户端任务新窗口无法修改任务等级 +- 修复邮箱大写报错的问题 +- 项目列表滚动'置顶'框隐藏 +- 【文件】右键多选所有文件复选框显示,取消所有选中消失 +- 消息:列表滚动隐藏'置顶'文案 +- 文件:列表模式右键后已选内容会错乱修复 +- 消息:列表滚动右键Y轴值判断错误修复 +- 消息:列表滚动在任意位置右键菜单错位问题修复 +- 为引入组件报错 +- 【文件】流程图只读接入新组件及删除旧组件引入代码 + +### Features + +- 首页启动设置 +- 项目列表添加置顶功能 +- 支持文本、图表、思维导图下载上传 + +### Performance + +- 文件、聊天文件、任务文件预览优化(支持预览drawio、mind等) +- 调整消息置顶标识位置 +- 消息列表详情增加'置顶'标识 +- 项目列表置顶优化 +- 项目列表置顶优化 +- 【文件】剪切后加'取消剪切'按钮 +- 消息会话右键时隐藏滚动条 +- 页面高度足够时只滚动项目部分 +- 客户端版本更新提示关闭 +- 除了任务状态,任务创建人和协助人权限与负责人的保持一致 +- 议仪表盘添加'待完成任务'选项 +- 创建者及协助人可以修改任务但不能修改任务状态 +- 优化点击右键时选中框缺少右侧线条 +- 客户端修改文件未保存关闭窗口前提示 +- 修改项目及消息置顶样式 +- 任务创建人和协助人可修改任务内容和详情,但不可修改任务状态 +- 修改任务时间日志 +- 任务聊天中发送图片时,回车可确定发送 +- 更新流程图表 +- 【文件】多个选择剪切功能与右键剪切重复,数据处理应该合拼;方格列表默认不显示复选框,右键菜单新增一个多选菜单 +- 优化注册提示 +- 消息列表需支持多个置顶 + +## [0.8.22] + +### Bug Fixes + +- 修复复制文件内容为空的问题 +- 流程图预览暗黑模式下看不见文字的问题 +- 【消息】置顶会话在子类tab中排序错误修改 + +### Features + +- 【消息】列表增加点击右键置顶该聊天功能 + +### Performance + +- 没有时间还显示时间倒计时的问题 +- 优化修改任务时间记录 +- 优化文件重命名 +- 主任务归档时同步子任务归档 +- 【注册】校验参数是否合法顺序优化 +- 【消息】列表置顶会话加背景颜色 +- 【消息】列表取消置顶 + +## [0.8.14] + +### Bug Fixes + +- 取消(完成状态)变为待测试(改变状态),如果有状态负责人应该把状态负责人加上 +- 【文件】流程图新组件添加缺失图片 +- 【文件】流程图新组件复选框在360浏览器中显示异常修改 +- 已完成子任务还出现时间跳动的情况 +- 【文件】流程图去掉ctr+s提示框 +- 【文件】新版流程图右侧及底部被隐藏问题修改 + +### Performance + +- 会员选择输入框不刷新的情况 +- 未完成状态禁止归档 +- 【文件】流程图新组件添加切换皮肤主题功能 +- 优化iPad兼容 +- 优化右下角、登录页主题设置 + +## [0.8.2] + +### Bug Fixes + +- 修复聊天mp4文件无法预览的问题 + +### Features + +- 更新流程图组件 + +### Performance + +- 单条消息最长2000个字符,超过自动分割发送,总最长20000 +- 去掉刷新提示及前端报错 +- 下载、查看任务文件权限改为所有项目成员 +- 任务详细描述取消文件上传 +- 优化任务详情拖动发送文件 +- 优化通知 + +## [0.7.94] + +### Bug Fixes + +- 修复文件上传一直出现loading的情况 +- 已存在的任务新添加负责人不出现在任务群聊里 +- 初次安装失败的情况 +- 客户端偶尔出现无法打开文件的情况 + +### Features + +- 添加项目支持默认工作流 +- 新增自定义添加项目时的项目模板 + +### Performance + +- 兼容iPad端 +- 手动切换账号提示“项目不存在或不在成员列表内”的情况 +- 任务详情 +- 该文件版本已经改变了。该页面将被重新加载 +- 点击切换语言一级菜单出现的兼容问题 +- 上传文件夹 +- 该文件版本已经改变了。该页面将被重新加载 +- 团队管理新增身份筛选项 +- 任务文件支持更多格式上传 + +## [0.7.73] + +### Bug Fixes + +- 客户端编辑文件不显示协助成员 +- 添加任务时选择任务组无效 +- 工作报告弹窗被遮挡的问题 +- 客户端任务窗口首次聊天失败的问题 +- 文件右键打开失效 +- 修复工作汇报正文被图片撑开页面的问题 + +### Features + +- 文件支持批量移动 + +### Performance + +- 优化项目筛选工作流 +- 优化撤回消息 +- 优化修改工作流未保存关闭提示 +- 查看工作报告图片显示不全的问题 +- 优化UserInput组件 +- 开放文件夹移动功能 +- 优化文件模块用户体验 +- 文件增加取消选择按钮 + +## [0.7.27] + +### Bug Fixes + +- 移动端无法上传任务文件的问题 +- 任务中没有聊天记录时,发送图片无法成功 +- 修复消息撤回文字提示在第一条时会被顶部遮住的问题 +- 修复个人对话为空时无法重复打开该对话的问题 +- 修复消息撤回文字提示在第一条时会被顶部遮住的问题 +- MacOS客户端首次不加载角标的问题 +- 工作流列表接口用作筛选时不用传多余参数 + +### Documentation + +- 补充消息撤回功能API文档 + +### Features + +- 工作流程负责人新增剔除模式(改变任务负责人并保留操作状态的人员) +- 文件网格模式支持批量删除文件 +- 文件表格支持批量删除文件 + +### Performance + +- 聊天消息附件支持预览 +- 仪表盘鼠标滑过时间显示完整时间 +- 聊天页面图片尺寸缩小至180px +- 头像上传图片浏览组件增加空提醒 +- MacOS角标首次不显示的问题 +- 添加任务时如果自己不是任务负责人可选择加入协助人员列表 +- 文件列表模式右键重命名无效 + +## [0.7.13] + +### Bug Fixes + +- 主任务负责人无法修改子任务负责人的问题 +- 设置主任务时间跟子任务没有交集时子任务时间改为跟主任务一致 +- 搜索后,点击查看已归档的任务,仍然显示这个搜索框 +- 修复归档任务无法查看日志的问题 +- 动态中点击重置按钮后流程的字体颜色和背景颜色并未正确显示 +- 修复任务负责人选择弹窗不随页面滚动的问题 +- 客户端无法下载聊天文件的问题 +- 修复添加任务变更栏目后无效果的问题 + +### Features + +- 新增文本文件编辑功能 +- 支持文件下载 +- 加入【项目管理】任务列表流程筛选被回滚功能 +- 聊天消息撤回-后端实现 +- 聊天消息撤回-前端实现 + +### Performance + +- 任务群聊中拖拽文件或者照片时并未有确认窗口 +- 待处理时进入动态中点击重置,重置提示中的参数为空,点击确定后,提示流程不存在或者删除 +- 添加子任务时输入框为空,回车时提示的图标错误 +- 上传文件夹应该保持目录结构 +- 支持xmind,rp格式文件上传 +- 注册页面增加密码格式提醒 +- 消息撤回后直接删除消息 + +## [0.6.97] + +### Bug Fixes + +- 修改主任务时间时未设置时间的子任务没有同步修改 +- Flow文件路径引用错误导致无法编译的问题 +- 修复工作汇报选择接收人组件在调整窗体大小时发生位移的问题 +- 修复周报点击编辑关闭后再点击"新建汇报"按钮导致用户列表无法重新加载的问题 +- 【工作汇报】修复新增今天的周报,提示要覆盖昨天日报的问题 +- 【工作汇报】收到的汇报中,未读时发送人删除掉汇报对象,汇报对象的未读数量仍然未减少 + +### Features + +- 聊天粘贴发送文件、图片时预览确认 + +### Performance + +- 任务详情窗口截止时间24小时倒计时 +- 修改任务或修改项目后同步对话信息 +- 支持查看已归档任务详情 +- 消息列表显示任务基本状态 +- 优化消息对话排序 +- 隐藏无聊天内容的对话 +- 消息页再次点击类型定位到未读消息 +- 调整周报签名的生成方法 + +## [0.6.80] + +### Performance + +- 仅客户端或Chrome浏览器支持主题功能 + +## [0.6.79] + +### Bug Fixes + +- 【工作报告】新增报告初始化数据不导入已有时间点数据 +- 【工作报告】详情页面前端报错修改 +- 【工作报告】填写内容--汇报类型切换提交提示错误问题 +- 发送图片显示错误 +- 项目负责人还原已经回档的自己不是任务负责人的任务时,无法还原,且会提示错误 + +### Performance + +- 工作报告优化 +- 规范代码 +- 工作报告优化 +- 共享文件删除、移动改为仅限所有者或创建者操作 + +## [0.6.38] + +### Bug Fixes + +- 无法读取 /www/.env:没有那个文件或目录 +- 最大只能上传64M的问题 + +### Features + +- 添加暗黑模式 + +### Performance + +- 优化安装脚本 + +## [0.6.31] + +### Performance + +- 新增工作流初始化数据 +- 优化客户端自动更新 + +## [0.6.26] + +### Bug Fixes + +- Websocket获取链接失败 +- 【工作报告】填写'提交'按钮位置被挤压修改 + +### Features + +- 【工作报告】功能 + +### Performance + +- 工作汇报样式 + +## [0.6.20] + +### Bug Fixes + +- 查看日历页面部分任务会不见 + +## [0.6.13] + +### Bug Fixes + +- 任务流转会出错 + +### Performance + +- 客户端更新提示 + +## [0.6.6] + +### Bug Fixes + +- 修复客户端旧版本更新任务出现已完成列表中有状态为“进行中”的情况 + +### Features + +- 工作流程状态支持仅限状态负责人修改 + +### Performance + +- 发送文件ws返回错误地址 +- 优化任务日志的显示 +- 项目页面点击搜索按钮再点击弹出的搜索框窗会隐藏 + +## [0.5.94] + +### Features + +- 任务流转自动负责人支持转让模式 + +### Performance + +- 任务日志刷新 +- Action中错误提醒弱化 +- 手机版无法选择日期范围的问题 +- 手机版无法聊天,输入页面跳动 + +## [0.5.84] + +### Features + +- 添加工作流 + +### Performance + +- 个人设置显示版本号 +- 完成任务暂时继续显示在我的列表 +- 客户端自动下载新版本更新 +- 设置子任务时间时,如果主任务没有时间则自动设置 +- 任务操作菜单组件化 +- 调整仅项目负责人可以删除任务列表 +- 领取任务需要设置计划时间 + +## [0.5.59] + +### Bug Fixes + +- 其他人员添加任务会临时出现在自己的列表中 + +### Performance + +- 任务版本如果只有一个负责人时显示负责人的名字 + +## [0.5.58] + +### Bug Fixes + +- 主任务被删除或归档时子任务应该也同步 + +### Features + +- 创建任务窗口添加小时钟 +- 限制个人最多500个未完成任务 +- 限制项目最多100个人参与 +- 思维导图支持无极缩放 +- 自动归档已完成任务 + +### Performance + +- 日历任务缓存 +- 日历选择时间添加任务 +- 归档任务可以搜索 +- 归档任务列表显示完成时间 +- 项目版面成员显示 +- 子任务默认起始时间与主任务一致 +- 禁用会员记录禁用时间 +- 安装项目时支持自定义端口 +- 任务详情窗口光标在任务描述可使用ctrl+s(command+s)保存 +- 文件快捷键保存 + +## [0.5.17] + +### Bug Fixes + +- 任务有负责人后仅限项目或任务负责人修改 + +### Performance + +- 未被领取的任务标记完成之前先领取 +- 快速添加任务0天的描述 +- 客户端标题优化 + +## [0.5.8] + +### Performance + +- Office文件预览 +- 文件预览不能滚动的问题 + +## [0.5.0] + +### Bug Fixes + +- 只读文件也能修改文件 + +### Performance + +- 优化文件权限 +- 初始化数据 + +## [0.4.99] + +### Bug Fixes + +- 修复首页内容部分间距问题 +- 修复英文版本登录按钮无法点击 +- 修复首页代码格式化 + +### Features + +- 文件分享查看链接 +- 新增主动退出共享文件 +- 共享给所有人 +- 文件支持只读、读/写细化设置 + +## [0.4.86] + +### Bug Fixes + +- 聊天窗口不自动滚动的问题 + +### Features + +- 客户端子窗口数据同步到主窗口 +- 任务新窗口打开 + +### Performance + +- Windows客户端无法关闭的情况 +- 任务等级支持设置0天表示默认不限时 +- 客户端子窗口 +- 任务窗口 + +## [0.4.78] + +### Bug Fixes + +- 清除缓存导致自定义服务器失败的问题 +- Office无法修改的问题 +- 邀请加入项目数据库迁移文件 +- 系统设置保存后邀请码消失 + +### Features + +- 客户端支持自定义服务器 + +### Performance + +- 链接说明 +- 发布窗口小屏幕不支持右键(隐藏右键提示) + +### Styling + +- 任务描述溢出的问题 +- 优化客户端更新窗口滚动 +- 发布任务出口小屏幕下方按钮不居中 + +## [0.4.63] + +### Bug Fixes + +- 任务详情窗口不显示子任务 + +### Features + +- 注册邀请码模式 +- 通过链接邀请加入项目 +- 文件上传进度 + +## [0.4.58] + +### Bug Fixes + +- 全局loading +- 图片媒体文件无法预览 + +### Features + +- 新注册自动创建个人项目 + +### Performance + +- 安装composer install失败时尝试使用国内源再试一次 +- 优化office加载提示 +- 按需加载富文本静态资源 + +### Styling + +- 登录成功加载效果 + +## [0.4.50] + +### Bug Fixes + +- 客户端新窗口 +- Window客户端弹出错误 +- 任务窗口详情描述内容溢出 +- 任务聊天窗口领取任务按钮错位 +- 任务级别设置等级描述 +- 手机版隐藏客户端下载按钮 + +### Documentation + +- Docker Compose v2.0+ + +### Features + +- 客户端新窗口打开文件 +- 任务创建窗口自动选择上次添加的项目 + +### Performance + +- 客户端新窗口打开文件 +- 客户端窗口标题 +- 领取任务流程 +- 到期时间格式化 +- 小屏幕弹窗底部按钮铺全 +- 仪表盘完成任务 +- 添加任务窗口 +- 添加任务弹窗 + +### Styling + +- 客户端文件窗口样式 +- Iview +- Iview +- 排序箭头颜色 + +## [0.4.28] + +### Bug Fixes + +- 按到期时间排序没有时间应该排到最后 +- 分页 #31 +- 任务负责人无法修改子任务的问题 + +### Documentation + +- 添加QQ群号 + +### Features + +- Mac客户端角标提示加上仪表盘内的任务数 +- 客户端快捷键关闭侧滑窗 +- 客户端快捷键关闭窗口先关网页内的弹窗 +- 项目任务支持优先级、到期时间排序 +- 仪表盘徽标数 +- 协助的任务 +- 记住最后登录账号 + +### Performance + +- 客户端关闭 +- 客户端在项目页面支持快捷键添加任务 +- 已完成任务的显示 +- 任务详细描述、添加任务 +- 仪表盘 +- 时间范围任务 +- 今天任务、我的任务 +- 日历日、周视图选择添加任务会进入all day +- 清除缓存 + diff --git a/README.md b/README.md index fcd14a2cd..44373ba34 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,16 @@ cd dootask ./cmd port 2222 ``` +### Change App Url + +```bash +# This URL only affects the email reply. +./cmd url {Your domain url} + +# example: +./cmd url https://domain.com +``` + ### Stop server ```bash @@ -106,7 +116,8 @@ git pull ./cmd mysql recovery ``` -If 502 after the upgrade please run `./cmd restart` restart the service. +* Please try again if the upgrade fails across a large version. +* If 502 after the upgrade please run `./cmd restart` restart the service. ## Transfer diff --git a/README_CLIENT.md b/README_CLIENT.md new file mode 100644 index 000000000..7b54c7a99 --- /dev/null +++ b/README_CLIENT.md @@ -0,0 +1,21 @@ +# 客户端说明 + +## 1、App客户端 + +#### 1.1、说明 +目录 `resources/mobile`,使用`eeui.app`框架,遵从eeui的开发文档进行打包开发app + +#### 1.2、编译App +1. 在项目目录执行 `./cmd appbuild` 编译 +2. 进入 `resources/mobile` eeui框架内打包Android或iOS应用 + + +## 2、PC/Mac客户端 + +#### 2.1、说明 +目录 `electron`,使用`electron`框架,遵从electron的开发文档进行打包客户端 + +#### 2.2、编译客户端 +在项目目录执行 `./cmd electron` 根据提示编译 + + diff --git a/README_CN.md b/README_CN.md index 11f993fb9..0797ff044 100644 --- a/README_CN.md +++ b/README_CN.md @@ -45,6 +45,16 @@ cd dootask ./cmd port 2222 ``` +### 更换URL + +```bash +# 此地址仅影响邮件回复功能 +./cmd url {域名地址} + +# 例如: +./cmd url https://domain.com +``` + ### 停止服务 ```bash @@ -107,7 +117,8 @@ git pull ./cmd mysql recovery ``` -如果升级后出现502请运行 `./cmd restart` 重启服务即可。 +* 跨越大版本升级失败时请重试执行一次。 +* 如果升级后出现502请运行 `./cmd restart` 重启服务即可。 ## 迁移项目 diff --git a/README_PUBLISH.md b/README_PUBLISH.md new file mode 100644 index 000000000..fb1f1ee9f --- /dev/null +++ b/README_PUBLISH.md @@ -0,0 +1,26 @@ +# 发布说明 + +## 发布前 + +1. 添加环境变量 `APPLEID`、`APPLEIDPASS` 用于公证 +2. 添加环境变量 `CSC_LINK`、`CSC_KEY_PASSWORD` 用于签名 +3. 添加环境变量 `GH_TOKEN`、`GH_REPOSITORY` 用于发布到GitHub +4. 添加环境变量 `DP_KEY` 用于发布到私有服务器 + +## 通过 GitHub Actions 发布 + +1. 执行 `npm run version` 生成版本 +2. 执行 `npm run build` 编译前端 +3. 执行 `git commit` 提交并推送 +4. 添加并推送标签 + +## 本地发布 + +1. 执行 `npm run version` 生成版本 +2. 执行 `npm run build` 编译前端 +3. 执行 `./cmd electron` 相关操作 + +## 编译App + +1. 执行 `./cmd appbuild` 或 `./cmd appbuild setting` 编译 +2. 进入 `resources/mobile` eeui框架内打包Android或iOS应用 diff --git a/_ide_helper.php b/_ide_helper.php index 9f6005a2e..4d6bba4fd 100644 --- a/_ide_helper.php +++ b/_ide_helper.php @@ -1,9 +1,10 @@ runningUnitTests(); + } + /** + * Determine if the application is running with debug mode enabled. + * + * @return bool + * @static + */ + public static function hasDebugModeEnabled() + { + /** @var \Illuminate\Foundation\Application $instance */ + return $instance->hasDebugModeEnabled(); } /** * Register all of the configured providers. @@ -564,6 +576,7 @@ /** * {@inheritdoc} * + * @return \Symfony\Component\HttpFoundation\Response * @static */ public static function handle($request, $type = 1, $catch = true) @@ -699,7 +712,7 @@ * @param int $code * @param string $message * @param array $headers - * @return void + * @return \Illuminate\Foundation\never * @throws \Symfony\Component\HttpKernel\Exception\HttpException * @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException * @static @@ -707,7 +720,7 @@ public static function abort($code, $message = '', $headers = []) { /** @var \Illuminate\Foundation\Application $instance */ - $instance->abort($code, $message, $headers); + return $instance->abort($code, $message, $headers); } /** * Register a terminating callback with the application. @@ -937,6 +950,7 @@ * `has($id)` returning true does not mean that `get($id)` will not throw an exception. * It does however mean that `get($id)` will not throw a `NotFoundExceptionInterface`. * + * @return bool * @param string $id Identifier of the entry to look for. * @return bool * @static @@ -1088,6 +1102,32 @@ { //Method inherited from \Illuminate\Container\Container /** @var \Illuminate\Foundation\Application $instance */ $instance->singletonIf($abstract, $concrete); + } + /** + * Register a scoped binding in the container. + * + * @param string $abstract + * @param \Closure|string|null $concrete + * @return void + * @static + */ + public static function scoped($abstract, $concrete = null) + { //Method inherited from \Illuminate\Container\Container + /** @var \Illuminate\Foundation\Application $instance */ + $instance->scoped($abstract, $concrete); + } + /** + * Register a scoped binding if it hasn't already been registered. + * + * @param string $abstract + * @param \Closure|string|null $concrete + * @return void + * @static + */ + public static function scopedIf($abstract, $concrete = null) + { //Method inherited from \Illuminate\Container\Container + /** @var \Illuminate\Foundation\Application $instance */ + $instance->scopedIf($abstract, $concrete); } /** * "Extend" an abstract type in the container. @@ -1239,6 +1279,7 @@ /** * Finds an entry of the container by its identifier and returns it. * + * @return mixed * @param string $id Identifier of the entry to look for. * @throws NotFoundExceptionInterface No entry was found for **this** identifier. * @throws ContainerExceptionInterface Error while retrieving the entry. @@ -1360,6 +1401,17 @@ { //Method inherited from \Illuminate\Container\Container /** @var \Illuminate\Foundation\Application $instance */ $instance->forgetInstances(); + } + /** + * Clear all of the scoped instances from the container. + * + * @return void + * @static + */ + public static function forgetScopedInstances() + { //Method inherited from \Illuminate\Container\Container + /** @var \Illuminate\Foundation\Application $instance */ + $instance->forgetScopedInstances(); } /** * Get the globally available instance of the container. @@ -1935,7 +1987,7 @@ * * @param string $password * @param string $attribute - * @return bool|null + * @return \App\Models\User|null * @throws \Illuminate\Auth\AuthenticationException * @static */ @@ -1999,6 +2051,18 @@ { /** @var \Illuminate\Auth\SessionGuard $instance */ return $instance->viaRemember(); + } + /** + * Set the number of minutes the remember me cookie should be valid for. + * + * @param int $minutes + * @return \Illuminate\Auth\SessionGuard + * @static + */ + public static function setRememberDuration($minutes) + { + /** @var \Illuminate\Auth\SessionGuard $instance */ + return $instance->setRememberDuration($minutes); } /** * Get the cookie creator instance used by the guard. @@ -2103,6 +2167,17 @@ { /** @var \Illuminate\Auth\SessionGuard $instance */ return $instance->setRequest($request); + } + /** + * Get the timebox instance used by the guard. + * + * @return \Illuminate\Support\Timebox + * @static + */ + public static function getTimebox() + { + /** @var \Illuminate\Auth\SessionGuard $instance */ + return $instance->getTimebox(); } /** * Determine if the current user is authenticated. If not, throw an exception. @@ -2207,6 +2282,16 @@ public static function hasMacro($name) { return \Illuminate\Auth\SessionGuard::hasMacro($name); + } + /** + * Flush the existing macros. + * + * @return void + * @static + */ + public static function flushMacros() + { + \Illuminate\Auth\SessionGuard::flushMacros(); } } @@ -2262,6 +2347,30 @@ { /** @var \Illuminate\View\Compilers\BladeCompiler $instance */ return $instance->compileString($value); + } + /** + * Evaluate and render a Blade string to HTML. + * + * @param string $string + * @param array $data + * @param bool $deleteCachedView + * @return string + * @static + */ + public static function render($string, $data = [], $deleteCachedView = false) + { + return \Illuminate\View\Compilers\BladeCompiler::render($string, $data, $deleteCachedView); + } + /** + * Render a component instance to HTML. + * + * @param \Illuminate\View\Component $component + * @return string + * @static + */ + public static function renderComponent($component) + { + return \Illuminate\View\Compilers\BladeCompiler::renderComponent($component); } /** * Strip the parentheses from the given expression. @@ -2588,6 +2697,19 @@ { /** @var \Illuminate\View\Compilers\BladeCompiler $instance */ return $instance->compileEndOnce(); + } + /** + * Add a handler to be executed before echoing a given class. + * + * @param string|callable $class + * @param callable|null $handler + * @return void + * @static + */ + public static function stringable($class, $handler = null) + { + /** @var \Illuminate\View\Compilers\BladeCompiler $instance */ + $instance->stringable($class, $handler); } /** * Compile Blade echos into valid PHP. @@ -2600,6 +2722,18 @@ { /** @var \Illuminate\View\Compilers\BladeCompiler $instance */ return $instance->compileEchos($value); + } + /** + * Apply the echo handler for the value if it exists. + * + * @param string $value + * @return string + * @static + */ + public static function applyEchoHandler($value) + { + /** @var \Illuminate\View\Compilers\BladeCompiler $instance */ + return $instance->applyEchoHandler($value); } } @@ -2961,6 +3095,17 @@ { /** @var \Illuminate\Support\Testing\Fakes\BusFake $instance */ $instance->assertNotDispatched($command, $callback); + } + /** + * Assert that no jobs were dispatched. + * + * @return void + * @static + */ + public static function assertNothingDispatched() + { + /** @var \Illuminate\Support\Testing\Fakes\BusFake $instance */ + $instance->assertNothingDispatched(); } /** * Assert if a job was explicitly dispatched synchronously based on a truth-test callback. @@ -3076,6 +3221,18 @@ { /** @var \Illuminate\Support\Testing\Fakes\BusFake $instance */ $instance->assertBatched($callback); + } + /** + * Assert the number of batches that have been dispatched. + * + * @param int $count + * @return void + * @static + */ + public static function assertBatchCount($count) + { + /** @var \Illuminate\Support\Testing\Fakes\BusFake $instance */ + $instance->assertBatchCount($count); } /** * Get all of the jobs matching a truth-test callback. @@ -3346,6 +3503,7 @@ /** * Obtains multiple cache items by their unique keys. * + * @return \Illuminate\Cache\iterable * @param \Psr\SimpleCache\iterable $keys A list of keys that can obtained in a single operation. * @param mixed $default Default value to return for keys that do not exist. * @return \Psr\SimpleCache\iterable A list of key => value pairs. Cache keys that do not exist or are stale will have $default as value. @@ -3389,6 +3547,7 @@ /** * Persists data in the cache, uniquely referenced by a key with an optional expiration TTL time. * + * @return bool * @param string $key The key of the item to store. * @param mixed $value The value of the item to store, must be serializable. * @param null|int|\DateInterval $ttl Optional. The TTL value of this item. If no value is sent and @@ -3420,6 +3579,7 @@ /** * Persists a set of key => value pairs in the cache, with an optional TTL. * + * @return bool * @param \Psr\SimpleCache\iterable $values A list of key => value pairs for a multiple-set operation. * @param null|int|\DateInterval $ttl Optional. The TTL value of this item. If no value is sent and * the driver supports TTL then the library may set a default value @@ -3492,7 +3652,7 @@ * Get an item from the cache, or execute the given Closure and store the result. * * @param string $key - * @param \DateTimeInterface|\DateInterval|int|null $ttl + * @param \Closure|\DateTimeInterface|\DateInterval|int|null $ttl * @param \Closure $callback * @return mixed * @static @@ -3543,6 +3703,7 @@ /** * Delete an item from the cache by its unique key. * + * @return bool * @param string $key The unique cache key of the item to delete. * @return bool True if the item was successfully removed. False if there was an error. * @throws \Psr\SimpleCache\InvalidArgumentException @@ -3557,6 +3718,7 @@ /** * Deletes multiple cache items in a single operation. * + * @return bool * @param \Psr\SimpleCache\iterable $keys A list of string-based keys to be deleted. * @return bool True if the items were successfully removed. False if there was an error. * @throws \Psr\SimpleCache\InvalidArgumentException @@ -3572,6 +3734,7 @@ /** * Wipes clean the entire cache's keys. * + * @return bool * @return bool True on success and false on failure. * @static */ @@ -3745,6 +3908,16 @@ public static function hasMacro($name) { return \Illuminate\Cache\Repository::hasMacro($name); + } + /** + * Flush the existing macros. + * + * @return void + * @static + */ + public static function flushMacros() + { + \Illuminate\Cache\Repository::flushMacros(); } /** * Dynamically handle calls to the class. @@ -4221,6 +4394,16 @@ public static function hasMacro($name) { return \Illuminate\Cookie\CookieJar::hasMacro($name); + } + /** + * Flush the existing macros. + * + * @return void + * @static + */ + public static function flushMacros() + { + \Illuminate\Cookie\CookieJar::flushMacros(); } } @@ -4345,7 +4528,7 @@ * @method static \Illuminate\Support\Carbon now($tz = null) * @method static \Illuminate\Support\Carbon parse($time = null, $tz = null) * @method static \Illuminate\Support\Carbon setHumanDiffOptions($humanDiffOptions) - * @method static \Illuminate\Support\Carbon setTestNow($testNow = null) + * @method static void setTestNow($testNow = null) * @method static \Illuminate\Support\Carbon setUtf8($utf8) * @method static \Illuminate\Support\Carbon today($tz = null) * @method static \Illuminate\Support\Carbon tomorrow($tz = null) @@ -4473,6 +4656,22 @@ { /** @var \Illuminate\Database\DatabaseManager $instance */ return $instance->connection($name); + } + /** + * Register a custom Doctrine type. + * + * @param string $class + * @param string $name + * @param string $type + * @return void + * @throws \Doctrine\DBAL\DBALException + * @throws \RuntimeException + * @static + */ + public static function registerDoctrineType($class, $name, $type) + { + /** @var \Illuminate\Database\DatabaseManager $instance */ + $instance->registerDoctrineType($class, $name, $type); } /** * Disconnect from the given database and remove from local cache. @@ -4890,6 +5089,18 @@ { //Method inherited from \Illuminate\Database\Connection /** @var \Illuminate\Database\MySqlConnection $instance */ $instance->logQuery($query, $bindings, $time); + } + /** + * Register a hook to be run just before a database query is executed. + * + * @param \Closure $callback + * @return \Illuminate\Database\MySqlConnection + * @static + */ + public static function beforeExecuting($callback) + { //Method inherited from \Illuminate\Database\Connection + /** @var \Illuminate\Database\MySqlConnection $instance */ + return $instance->beforeExecuting($callback); } /** * Register a database query listener with the connection. @@ -4914,6 +5125,17 @@ { //Method inherited from \Illuminate\Database\Connection /** @var \Illuminate\Database\MySqlConnection $instance */ return $instance->raw($value); + } + /** + * Determine if the database connection has modified any database records. + * + * @return bool + * @static + */ + public static function hasModifiedRecords() + { //Method inherited from \Illuminate\Database\Connection + /** @var \Illuminate\Database\MySqlConnection $instance */ + return $instance->hasModifiedRecords(); } /** * Indicate if any records have been modified. @@ -4926,6 +5148,18 @@ { //Method inherited from \Illuminate\Database\Connection /** @var \Illuminate\Database\MySqlConnection $instance */ $instance->recordsHaveBeenModified($value); + } + /** + * Set the record modification state. + * + * @param bool $value + * @return \Illuminate\Database\MySqlConnection + * @static + */ + public static function setRecordModificationState($value) + { //Method inherited from \Illuminate\Database\Connection + /** @var \Illuminate\Database\MySqlConnection $instance */ + return $instance->setRecordModificationState($value); } /** * Reset the record modification state. @@ -4937,6 +5171,18 @@ { //Method inherited from \Illuminate\Database\Connection /** @var \Illuminate\Database\MySqlConnection $instance */ $instance->forgetRecordModificationState(); + } + /** + * Indicate that the connection should use the write PDO connection for reads. + * + * @param bool $value + * @return \Illuminate\Database\MySqlConnection + * @static + */ + public static function useWriteConnectionWhenReading($value = true) + { //Method inherited from \Illuminate\Database\Connection + /** @var \Illuminate\Database\MySqlConnection $instance */ + return $instance->useWriteConnectionWhenReading($value); } /** * Is Doctrine available? @@ -5581,7 +5827,7 @@ /** * Register an event listener with the dispatcher. * - * @param \Closure|string $listener + * @param \Closure|string|array $listener * @param bool $wildcard * @return \Closure * @static @@ -5674,6 +5920,16 @@ public static function hasMacro($name) { return \Illuminate\Events\Dispatcher::hasMacro($name); + } + /** + * Flush the existing macros. + * + * @return void + * @static + */ + public static function flushMacros() + { + \Illuminate\Events\Dispatcher::flushMacros(); } /** * Assert if an event has a listener attached to it. @@ -5900,6 +6156,20 @@ { /** @var \Illuminate\Filesystem\Filesystem $instance */ $instance->replace($path, $content); + } + /** + * Replace a given string within a given file. + * + * @param array|string $search + * @param array|string $replace + * @param string $path + * @return void + * @static + */ + public static function replaceInFile($search, $replace, $path) + { + /** @var \Illuminate\Filesystem\Filesystem $instance */ + $instance->replaceInFile($search, $replace, $path); } /** * Prepend to a file. @@ -6344,6 +6614,16 @@ public static function hasMacro($name) { return \Illuminate\Filesystem\Filesystem::hasMacro($name); + } + /** + * Flush the existing macros. + * + * @return void + * @static + */ + public static function flushMacros() + { + \Illuminate\Filesystem\Filesystem::flushMacros(); } } @@ -6364,6 +6644,36 @@ { /** @var \Illuminate\Auth\Access\Gate $instance */ return $instance->has($ability); + } + /** + * Perform an on-demand authorization check. Throw an authorization exception if the condition or callback is false. + * + * @param \Illuminate\Auth\Access\Response|\Closure|bool $condition + * @param string|null $message + * @param string|null $code + * @return \Illuminate\Auth\Access\Response + * @throws \Illuminate\Auth\Access\AuthorizationException + * @static + */ + public static function allowIf($condition, $message = null, $code = null) + { + /** @var \Illuminate\Auth\Access\Gate $instance */ + return $instance->allowIf($condition, $message, $code); + } + /** + * Perform an on-demand authorization check. Throw an authorization exception if the condition or callback is true. + * + * @param \Illuminate\Auth\Access\Response|\Closure|bool $condition + * @param string|null $message + * @param string|null $code + * @return \Illuminate\Auth\Access\Response + * @throws \Illuminate\Auth\Access\AuthorizationException + * @static + */ + public static function denyIf($condition, $message = null, $code = null) + { + /** @var \Illuminate\Auth\Access\Gate $instance */ + return $instance->denyIf($condition, $message, $code); } /** * Define a new ability. @@ -6812,7 +7122,7 @@ * @method static \Illuminate\Http\Client\PendingRequest contentType(string $contentType) * @method static \Illuminate\Http\Client\PendingRequest dd() * @method static \Illuminate\Http\Client\PendingRequest dump() - * @method static \Illuminate\Http\Client\PendingRequest retry(int $times, int $sleep = 0) + * @method static \Illuminate\Http\Client\PendingRequest retry(int $times, int $sleep = 0, ?callable $when = null) * @method static \Illuminate\Http\Client\PendingRequest sink(string|resource $to) * @method static \Illuminate\Http\Client\PendingRequest stub(callable $callback) * @method static \Illuminate\Http\Client\PendingRequest timeout(int $seconds) @@ -6994,6 +7304,17 @@ { /** @var \Illuminate\Http\Client\Factory $instance */ return $instance->recorded($callback); + } + /** + * Get the current event dispatcher implementation. + * + * @return \Illuminate\Contracts\Events\Dispatcher|null + * @static + */ + public static function getDispatcher() + { + /** @var \Illuminate\Http\Client\Factory $instance */ + return $instance->getDispatcher(); } /** * Register a custom macro. @@ -7030,6 +7351,16 @@ public static function hasMacro($name) { return \Illuminate\Http\Client\Factory::hasMacro($name); + } + /** + * Flush the existing macros. + * + * @return void + * @static + */ + public static function flushMacros() + { + \Illuminate\Http\Client\Factory::flushMacros(); } /** * Dynamically handle calls to the class. @@ -7291,6 +7622,17 @@ { //Method inherited from \Illuminate\Support\NamespacedItemResolver /** @var \Illuminate\Translation\Translator $instance */ $instance->setParsedKey($key, $parsed); + } + /** + * Flush the cache of parsed keys. + * + * @return void + * @static + */ + public static function flushParsedKeys() + { //Method inherited from \Illuminate\Support\NamespacedItemResolver + /** @var \Illuminate\Translation\Translator $instance */ + $instance->flushParsedKeys(); } /** * Register a custom macro. @@ -7327,18 +7669,42 @@ public static function hasMacro($name) { return \Illuminate\Translation\Translator::hasMacro($name); + } + /** + * Flush the existing macros. + * + * @return void + * @static + */ + public static function flushMacros() + { + \Illuminate\Translation\Translator::flushMacros(); } } /** * * + * @method static \Illuminate\Log\Logger withContext(array $context = []) + * @method static \Illuminate\Log\Logger withoutContext() * @method static void write(string $level, string $message, array $context = []) * @method static void listen(\Closure $callback) * @see \Illuminate\Log\Logger */ class Log { /** + * Build an on-demand log channel. + * + * @param array $config + * @return \Psr\Log\LoggerInterface + * @static + */ + public static function build($config) + { + /** @var \Illuminate\Log\LogManager $instance */ + return $instance->build($config); + } + /** * Create a new, on-demand aggregate logger instance. * * @param array $channels @@ -7374,22 +7740,11 @@ { /** @var \Illuminate\Log\LogManager $instance */ return $instance->driver($driver); - } - /** - * - * - * @return array - * @static - */ - public static function getChannels() - { - /** @var \Illuminate\Log\LogManager $instance */ - return $instance->getChannels(); } /** * Get the default log driver name. * - * @return string + * @return string|null * @static */ public static function getDefaultDriver() @@ -7433,6 +7788,17 @@ { /** @var \Illuminate\Log\LogManager $instance */ return $instance->forgetChannel($driver); + } + /** + * Get all of the resolved log channels. + * + * @return array + * @static + */ + public static function getChannels() + { + /** @var \Illuminate\Log\LogManager $instance */ + return $instance->getChannels(); } /** * System is unusable. @@ -7568,6 +7934,10 @@ /** * * + * @method static void alwaysFrom(string $address, string|null $name = null) + * @method static void alwaysReplyTo(string $address, string|null $name = null) + * @method static void alwaysReturnPath(string $address) + * @method static void alwaysTo(string $address, string|null $name = null) * @method static mixed laterOn(string $queue, \DateTimeInterface|\DateInterval|int $delay, \Illuminate\Contracts\Mail\Mailable|string|array $view) * @method static mixed queueOn(string $queue, \Illuminate\Contracts\Mail\Mailable|string|array $view) * @method static void plain(string $view, array $data, $callback) @@ -7707,11 +8077,24 @@ { /** @var \Illuminate\Support\Testing\Fakes\MailFake $instance */ $instance->assertSent($mailable, $callback); + } + /** + * Determine if a mailable was not sent or queued to be sent based on a truth-test callback. + * + * @param string|\Closure $mailable + * @param callable|null $callback + * @return void + * @static + */ + public static function assertNotOutgoing($mailable, $callback = null) + { + /** @var \Illuminate\Support\Testing\Fakes\MailFake $instance */ + $instance->assertNotOutgoing($mailable, $callback); } /** * Determine if a mailable was not sent based on a truth-test callback. * - * @param string $mailable + * @param string|\Closure $mailable * @param callable|null $callback * @return void * @static @@ -7720,6 +8103,17 @@ { /** @var \Illuminate\Support\Testing\Fakes\MailFake $instance */ $instance->assertNotSent($mailable, $callback); + } + /** + * Assert that no mailables were sent or queued to be sent. + * + * @return void + * @static + */ + public static function assertNothingOutgoing() + { + /** @var \Illuminate\Support\Testing\Fakes\MailFake $instance */ + $instance->assertNothingOutgoing(); } /** * Assert that no mailables were sent. @@ -7748,7 +8142,7 @@ /** * Determine if a mailable was not queued based on a truth-test callback. * - * @param string $mailable + * @param string|\Closure $mailable * @param callable|null $callback * @return void * @static @@ -7772,7 +8166,7 @@ /** * Get all of the mailables matching a truth-test callback. * - * @param string $mailable + * @param string|\Closure $mailable * @param callable|null $callback * @return \Illuminate\Support\Collection * @static @@ -7797,7 +8191,7 @@ /** * Get all of the queued mailables matching a truth-test callback. * - * @param string $mailable + * @param string|\Closure $mailable * @param callable|null $callback * @return \Illuminate\Support\Collection * @static @@ -8071,6 +8465,20 @@ { //Method inherited from \Illuminate\Support\Manager /** @var \Illuminate\Notifications\ChannelManager $instance */ return $instance->forgetDrivers(); + } + /** + * Assert if a notification was sent on-demand based on a truth-test callback. + * + * @param string|\Closure $notification + * @param callable|null $callback + * @return void + * @throws \Exception + * @static + */ + public static function assertSentOnDemand($notification, $callback = null) + { + /** @var \Illuminate\Support\Testing\Fakes\NotificationFake $instance */ + $instance->assertSentOnDemand($notification, $callback); } /** * Assert if a notification was sent based on a truth-test callback. @@ -8086,6 +8494,19 @@ { /** @var \Illuminate\Support\Testing\Fakes\NotificationFake $instance */ $instance->assertSentTo($notifiable, $notification, $callback); + } + /** + * Assert if a notification was sent on-demand a number of times. + * + * @param string $notification + * @param int $times + * @return void + * @static + */ + public static function assertSentOnDemandTimes($notification, $times = 1) + { + /** @var \Illuminate\Support\Testing\Fakes\NotificationFake $instance */ + $instance->assertSentOnDemandTimes($notification, $times); } /** * Assert if a notification was sent a number of times. @@ -8126,6 +8547,19 @@ { /** @var \Illuminate\Support\Testing\Fakes\NotificationFake $instance */ $instance->assertNothingSent(); + } + /** + * Assert the total amount of times a notification was sent. + * + * @param string $notification + * @param int $expectedCount + * @return void + * @static + */ + public static function assertSentTimes($notification, $expectedCount) + { + /** @var \Illuminate\Support\Testing\Fakes\NotificationFake $instance */ + $instance->assertSentTimes($notification, $expectedCount); } /** * Assert the total amount of times a notification was sent. @@ -8133,6 +8567,7 @@ * @param int $expectedCount * @param string $notification * @return void + * @deprecated Use the assertSentTimes method instead * @static */ public static function assertTimesSent($expectedCount, $notification) @@ -8202,6 +8637,16 @@ public static function hasMacro($name) { return \Illuminate\Support\Testing\Fakes\NotificationFake::hasMacro($name); + } + /** + * Flush the existing macros. + * + * @return void + * @static + */ + public static function flushMacros() + { + \Illuminate\Support\Testing\Fakes\NotificationFake::flushMacros(); } } @@ -8258,7 +8703,6 @@ /** * * - * @method static void popUsing(string $workerName, callable $callback) * @see \Illuminate\Queue\QueueManager * @see \Illuminate\Queue\Queue */ @@ -8561,7 +9005,7 @@ /** * Push a new job onto the queue. * - * @param string $job + * @param string|object $job * @param mixed $data * @param string|null $queue * @return mixed @@ -8590,7 +9034,7 @@ * Push a new job onto the queue after a delay. * * @param \DateTimeInterface|\DateInterval|int $delay - * @param string $job + * @param string|object $job * @param mixed $data * @param string|null $queue * @return mixed @@ -8605,7 +9049,7 @@ * Push a new job onto the queue. * * @param string $queue - * @param string $job + * @param string|object $job * @param mixed $data * @return mixed * @static @@ -8620,7 +9064,7 @@ * * @param string $queue * @param \DateTimeInterface|\DateInterval|int $delay - * @param string $job + * @param string|object $job * @param mixed $data * @return mixed * @static @@ -9086,6 +9530,16 @@ public static function hasMacro($name) { return \Illuminate\Routing\Redirector::hasMacro($name); + } + /** + * Flush the existing macros. + * + * @return void + * @static + */ + public static function flushMacros() + { + \Illuminate\Routing\Redirector::flushMacros(); } } @@ -9172,6 +9626,18 @@ { /** @var \Illuminate\Http\Request $instance */ return $instance->fullUrlWithQuery($query); + } + /** + * Get the full URL for the request without the given query string parameters. + * + * @param array|string $query + * @return string + * @static + */ + public static function fullUrlWithoutQuery($keys) + { + /** @var \Illuminate\Http\Request $instance */ + return $instance->fullUrlWithoutQuery($keys); } /** * Get the current path info for the request. @@ -9343,6 +9809,18 @@ { /** @var \Illuminate\Http\Request $instance */ return $instance->merge($input); + } + /** + * Merge new input into the request's input, but only when that key is missing from the request. + * + * @param array $input + * @return \Illuminate\Http\Request + * @static + */ + public static function mergeIfMissing($input) + { + /** @var \Illuminate\Http\Request $instance */ + return $instance->mergeIfMissing($input); } /** * Replace the input for the current request. @@ -9410,6 +9888,7 @@ /** * Clones a request and overrides some of its parameters. * + * @return static * @param array $query The GET parameters * @param array $request The POST parameters * @param array $attributes The request attributes (parameters parsed from the PATH_INFO, ...) @@ -9705,7 +10184,7 @@ /** * Gets the list of trusted proxies. * - * @return array An array of trusted proxies + * @return array * @static */ public static function getTrustedProxies() @@ -9737,7 +10216,7 @@ /** * Gets the list of trusted host patterns. * - * @return array An array of trusted host patterns + * @return array * @static */ public static function getTrustedHosts() @@ -9750,7 +10229,7 @@ * It builds a normalized query string, where keys/value pairs are alphabetized, * have consistent escaping and unneeded delimiters are removed. * - * @return string A normalized query string for the Request + * @return string * @static */ public static function normalizeQueryString($qs) @@ -9777,7 +10256,7 @@ /** * Checks whether support for the _method request parameter is enabled. * - * @return bool True when the _method request parameter is enabled, false otherwise + * @return bool * @static */ public static function getHttpMethodParameterOverride() @@ -9803,7 +10282,8 @@ * like whether the session is started or not. It is just a way to check if this Request * is associated with a Session instance. * - * @return bool true when the Request contains a Session object, false otherwise + * @param bool $skipIfUninitialized When true, ignores factories injected by `setSessionFactory` + * @return bool * @static */ public static function hasSession() @@ -9825,6 +10305,7 @@ * * * @internal + * @param \Symfony\Component\HttpFoundation\callable(): SessionInterface $factory * @static */ public static function setSessionFactory($factory) @@ -9841,7 +10322,7 @@ * * Use this method carefully; you should use getClientIp() instead. * - * @return array The client IP addresses + * @return array * @see getClientIp() * @static */ @@ -9863,7 +10344,7 @@ * ("Client-Ip" for instance), configure it via the $trustedHeaderSet * argument of the Request::setTrustedProxies() method instead. * - * @return string|null The client IP address + * @return string|null * @see getClientIps() * @see https://wikipedia.org/wiki/X-Forwarded-For * @static @@ -9957,7 +10438,7 @@ * * The "X-Forwarded-Port" header must contain the client port. * - * @return int|string can be a string if fetched from the server bag + * @return int|string|null Can be a string if fetched from the server bag * @static */ public static function getPort() @@ -9990,7 +10471,7 @@ /** * Gets the user info. * - * @return string A user name and, optionally, scheme-specific information about how to gain authorization to access the server + * @return string|null A user name if any and, optionally, scheme-specific information about how to gain authorization to access the server * @static */ public static function getUserInfo() @@ -10028,7 +10509,7 @@ * If the URL was called with basic authentication, the user * and the password are not added to the generated string. * - * @return string The scheme and HTTP host + * @return string * @static */ public static function getSchemeAndHttpHost() @@ -10039,7 +10520,7 @@ /** * Generates a normalized URI (URL) for the Request. * - * @return string A normalized URI (URL) for the Request + * @return string * @see getQueryString() * @static */ @@ -10052,7 +10533,7 @@ * Generates a normalized URI for the given path. * * @param string $path A path to use instead of the current one - * @return string The normalized URI for the path + * @return string * @static */ public static function getUriForPath($path) @@ -10075,7 +10556,7 @@ * - "/a/b/c/other" -> "other" * - "/a/x/y" -> "../../x/y" * - * @return string The relative target path + * @return string * @static */ public static function getRelativeUriForPath($path) @@ -10089,7 +10570,7 @@ * It builds a normalized query string, where keys/value pairs are alphabetized * and have consistent escaping. * - * @return string|null A normalized query string for the Request + * @return string|null * @static */ public static function getQueryString() @@ -10151,7 +10632,7 @@ * * The method is always an uppercased string. * - * @return string The request method + * @return string * @see getRealMethod() * @static */ @@ -10163,7 +10644,7 @@ /** * Gets the "real" request method. * - * @return string The request method + * @return string * @see getMethod() * @static */ @@ -10175,7 +10656,7 @@ /** * Gets the mime type associated with the format. * - * @return string|null The associated mime type (null if not found) + * @return string|null * @static */ public static function getMimeType($format) @@ -10186,7 +10667,7 @@ /** * Gets the mime types associated with the format. * - * @return array The associated mime types + * @return array * @static */ public static function getMimeTypes($format) @@ -10196,7 +10677,7 @@ /** * Gets the format associated with the mime type. * - * @return string|null The format (null if not found) + * @return string|null * @static */ public static function getFormat($mimeType) @@ -10225,7 +10706,7 @@ * * $default * * @see getPreferredFormat - * @return string|null The request format + * @return string|null * @static */ public static function getRequestFormat($default = 'html') @@ -10246,7 +10727,7 @@ /** * Gets the format associated with the request. * - * @return string|null The format (null if no content type is present) + * @return string|null * @static */ public static function getContentType() @@ -10335,7 +10816,7 @@ * Checks whether the method is cacheable or not. * * @see https://tools.ietf.org/html/rfc7231#section-4.2.3 - * @return bool True for GET and HEAD, false otherwise + * @return bool * @static */ public static function isMethodCacheable() @@ -10364,7 +10845,7 @@ * Returns the request body content. * * @param bool $asResource If true, a resource will be returned - * @return string|resource The request body content or a resource to read the body stream + * @return string|resource * @static */ public static function getContent($asResource = false) @@ -10375,7 +10856,7 @@ /** * Gets the Etags. * - * @return array The entity tags + * @return array * @static */ public static function getETags() @@ -10413,7 +10894,7 @@ * Returns the preferred language. * * @param string[] $locales An array of ordered available locales - * @return string|null The preferred locale + * @return string|null * @static */ public static function getPreferredLanguage($locales = null) @@ -10422,9 +10903,9 @@ return $instance->getPreferredLanguage($locales); } /** - * Gets a list of languages acceptable by the client browser. + * Gets a list of languages acceptable by the client browser ordered in the user browser preferences. * - * @return array Languages ordered in the user browser preferences + * @return array * @static */ public static function getLanguages() @@ -10433,9 +10914,9 @@ return $instance->getLanguages(); } /** - * Gets a list of charsets acceptable by the client browser. + * Gets a list of charsets acceptable by the client browser in preferable order. * - * @return array List of charsets in preferable order + * @return array * @static */ public static function getCharsets() @@ -10444,9 +10925,9 @@ return $instance->getCharsets(); } /** - * Gets a list of encodings acceptable by the client browser. + * Gets a list of encodings acceptable by the client browser in preferable order. * - * @return array List of encodings in preferable order + * @return array * @static */ public static function getEncodings() @@ -10455,9 +10936,9 @@ return $instance->getEncodings(); } /** - * Gets a list of content types acceptable by the client browser. + * Gets a list of content types acceptable by the client browser in preferable order. * - * @return array List of content types in preferable order + * @return array * @static */ public static function getAcceptableContentTypes() @@ -10472,7 +10953,7 @@ * It is known to work with common JavaScript frameworks: * * @see https://wikipedia.org/wiki/List_of_Ajax_frameworks#JavaScript - * @return bool true if the request is an XMLHttpRequest, false otherwise + * @return bool * @static */ public static function isXmlHttpRequest() @@ -10497,7 +10978,7 @@ * This can be useful to determine whether or not to trust the * contents of a proxy-specific header. * - * @return bool true if the request came from a trusted proxy, false otherwise + * @return bool * @static */ public static function isFromTrustedProxy() @@ -10768,13 +11249,14 @@ * * @param string $key * @param callable $callback + * @param callable|null $default * @return $this|mixed * @static */ - public static function whenHas($key, $callback) + public static function whenHas($key, $callback, $default = null) { /** @var \Illuminate\Http\Request $instance */ - return $instance->whenHas($key, $callback); + return $instance->whenHas($key, $callback, $default); } /** * Determine if the request contains a non-empty value for an input item. @@ -10817,13 +11299,14 @@ * * @param string $key * @param callable $callback + * @param callable|null $default * @return $this|mixed * @static */ - public static function whenFilled($key, $callback) + public static function whenFilled($key, $callback, $default = null) { /** @var \Illuminate\Http\Request $instance */ - return $instance->whenFilled($key, $callback); + return $instance->whenFilled($key, $callback, $default); } /** * Determine if the request is missing a given input item key. @@ -10887,6 +11370,32 @@ { /** @var \Illuminate\Http\Request $instance */ return $instance->boolean($key, $default); + } + /** + * Retrieve input from the request as a Carbon instance. + * + * @param string $key + * @param string|null $format + * @param string|null $tz + * @return \Illuminate\Support\Carbon|null + * @static + */ + public static function date($key, $format = null, $tz = null) + { + /** @var \Illuminate\Http\Request $instance */ + return $instance->date($key, $format, $tz); + } + /** + * Retrieve input from the request as a collection. + * + * @param array|string|null $key + * @return \Illuminate\Support\Collection + * @static + */ + public static function collect($key = null) + { + /** @var \Illuminate\Http\Request $instance */ + return $instance->collect($key); } /** * Get a subset containing the provided keys with values from the input data. @@ -11002,7 +11511,7 @@ /** * Dump the request items and end the script. * - * @param array|mixed $keys + * @param mixed $keys * @return void * @static */ @@ -11014,7 +11523,7 @@ /** * Dump the items. * - * @param array $keys + * @param mixed $keys * @return \Illuminate\Http\Request * @static */ @@ -11058,6 +11567,16 @@ public static function hasMacro($name) { return \Illuminate\Http\Request::hasMacro($name); + } + /** + * Flush the existing macros. + * + * @return void + * @static + */ + public static function flushMacros() + { + \Illuminate\Http\Request::flushMacros(); } /** * @@ -11116,7 +11635,7 @@ /** * Create a new response instance. * - * @param string $content + * @param mixed $content * @param int $status * @param array $headers * @return \Illuminate\Http\Response @@ -11353,6 +11872,16 @@ public static function hasMacro($name) { return \Illuminate\Routing\ResponseFactory::hasMacro($name); + } + /** + * Flush the existing macros. + * + * @return void + * @static + */ + public static function flushMacros() + { + \Illuminate\Routing\ResponseFactory::flushMacros(); } } @@ -11360,12 +11889,15 @@ * * * @method static \Illuminate\Routing\RouteRegistrar as(string $value) + * @method static \Illuminate\Routing\RouteRegistrar controller(string $controller) * @method static \Illuminate\Routing\RouteRegistrar domain(string $value) * @method static \Illuminate\Routing\RouteRegistrar middleware(array|string|null $middleware) * @method static \Illuminate\Routing\RouteRegistrar name(string $value) * @method static \Illuminate\Routing\RouteRegistrar namespace(string|null $value) * @method static \Illuminate\Routing\RouteRegistrar prefix(string $prefix) + * @method static \Illuminate\Routing\RouteRegistrar scopeBindings() * @method static \Illuminate\Routing\RouteRegistrar where(array $where) + * @method static \Illuminate\Routing\RouteRegistrar withoutMiddleware(array|string $middleware) * @see \Illuminate\Routing\Router */ class Route { @@ -12214,6 +12746,16 @@ public static function hasMacro($name) { return \Illuminate\Routing\Router::hasMacro($name); + } + /** + * Flush the existing macros. + * + * @return void + * @static + */ + public static function flushMacros() + { + \Illuminate\Routing\Router::flushMacros(); } /** * Dynamically handle calls to the class. @@ -12518,8 +13060,6 @@ * @param string $name * @param string $type * @return void - * @throws \Doctrine\DBAL\DBALException - * @throws \RuntimeException * @static */ public static function registerCustomDoctrineType($class, $name, $type) @@ -13262,6 +13802,18 @@ { /** @var \Illuminate\Filesystem\FilesystemManager $instance */ return $instance->cloud(); + } + /** + * Build an on-demand disk. + * + * @param string|array $config + * @return \Illuminate\Filesystem\FilesystemAdapter + * @static + */ + public static function build($config) + { + /** @var \Illuminate\Filesystem\FilesystemManager $instance */ + return $instance->build($config); } /** * Create an instance of the local driver. @@ -13382,6 +13934,18 @@ { /** @var \Illuminate\Filesystem\FilesystemManager $instance */ return $instance->extend($driver, $callback); + } + /** + * Set the application instance used by the manager. + * + * @param \Illuminate\Contracts\Foundation\Application $app + * @return \Illuminate\Filesystem\FilesystemManager + * @static + */ + public static function setApplication($app) + { + /** @var \Illuminate\Filesystem\FilesystemManager $instance */ + return $instance->setApplication($app); } /** * Assert that the given file exists. @@ -13490,7 +14054,7 @@ * Write the contents of a file. * * @param string $path - * @param string|resource $contents + * @param \Psr\Http\Message\StreamInterface|\Illuminate\Http\File|\Illuminate\Http\UploadedFile|string|resource $contents * @param mixed $options * @return bool * @static @@ -13825,6 +14389,78 @@ { /** @var \Illuminate\Filesystem\FilesystemAdapter $instance */ return $instance->getDriver(); + } + /** + * Define a custom temporary URL builder callback. + * + * @param \Closure $callback + * @return void + * @static + */ + public static function buildTemporaryUrlsUsing($callback) + { + /** @var \Illuminate\Filesystem\FilesystemAdapter $instance */ + $instance->buildTemporaryUrlsUsing($callback); + } + /** + * Register a custom macro. + * + * @param string $name + * @param object|callable $macro + * @return void + * @static + */ + public static function macro($name, $macro) + { + \Illuminate\Filesystem\FilesystemAdapter::macro($name, $macro); + } + /** + * Mix another object into the class. + * + * @param object $mixin + * @param bool $replace + * @return void + * @throws \ReflectionException + * @static + */ + public static function mixin($mixin, $replace = true) + { + \Illuminate\Filesystem\FilesystemAdapter::mixin($mixin, $replace); + } + /** + * Checks if macro is registered. + * + * @param string $name + * @return bool + * @static + */ + public static function hasMacro($name) + { + return \Illuminate\Filesystem\FilesystemAdapter::hasMacro($name); + } + /** + * Flush the existing macros. + * + * @return void + * @static + */ + public static function flushMacros() + { + \Illuminate\Filesystem\FilesystemAdapter::flushMacros(); + } + /** + * Dynamically handle calls to the class. + * + * @param string $method + * @param array $parameters + * @return mixed + * @throws \BadMethodCallException + * @static + */ + public static function macroCall($method, $parameters) + { + /** @var \Illuminate\Filesystem\FilesystemAdapter $instance */ + return $instance->macroCall($method, $parameters); } } @@ -14311,6 +14947,16 @@ public static function hasMacro($name) { return \Illuminate\Routing\UrlGenerator::hasMacro($name); + } + /** + * Flush the existing macros. + * + * @return void + * @static + */ + public static function flushMacros() + { + \Illuminate\Routing\UrlGenerator::flushMacros(); } } @@ -14405,6 +15051,17 @@ { /** @var \Illuminate\Validation\Factory $instance */ $instance->replacer($rule, $replacer); + } + /** + * Indicate that unvalidated array keys should be excluded, even if the parent array was validated. + * + * @return void + * @static + */ + public static function excludeUnvalidatedArrayKeys() + { + /** @var \Illuminate\Validation\Factory $instance */ + $instance->excludeUnvalidatedArrayKeys(); } /** * Set the Validator instance resolver. @@ -14540,6 +15197,21 @@ { /** @var \Illuminate\View\Factory $instance */ return $instance->renderWhen($condition, $view, $data, $mergeData); + } + /** + * Get the rendered content of the view based on the negation of a given condition. + * + * @param bool $condition + * @param string $view + * @param \Illuminate\Contracts\Support\Arrayable|array $data + * @param array $mergeData + * @return string + * @static + */ + public static function renderUnless($condition, $view, $data = [], $mergeData = []) + { + /** @var \Illuminate\View\Factory $instance */ + return $instance->renderUnless($condition, $view, $data, $mergeData); } /** * Get the rendered contents of a partial from a loop. @@ -14899,6 +15571,16 @@ public static function hasMacro($name) { return \Illuminate\View\Factory::hasMacro($name); + } + /** + * Flush the existing macros. + * + * @return void + * @static + */ + public static function flushMacros() + { + \Illuminate\View\Factory::flushMacros(); } /** * Start a component rendering process. @@ -14936,20 +15618,33 @@ { /** @var \Illuminate\View\Factory $instance */ return $instance->renderComponent(); + } + /** + * Get an item from the component data that exists above the current component. + * + * @param string $key + * @param mixed $default + * @return mixed|null + * @static + */ + public static function getConsumableComponentData($key, $default = null) + { + /** @var \Illuminate\View\Factory $instance */ + return $instance->getConsumableComponentData($key, $default); } /** * Start the slot rendering process. * * @param string $name * @param string|null $content + * @param array $attributes * @return void - * @throws \InvalidArgumentException * @static */ - public static function slot($name, $content = null) + public static function slot($name, $content = null, $attributes = []) { /** @var \Illuminate\View\Factory $instance */ - $instance->slot($name, $content); + $instance->slot($name, $content, $attributes); } /** * Save the slot content for rendering. @@ -15111,7 +15806,7 @@ return \Illuminate\View\Factory::parentPlaceholder($section); } /** - * Check if the section exists. + * Check if section exists. * * @param string $name * @return bool @@ -15354,11 +16049,12 @@ * @param string $fileName * @param string|null $writerType * @param mixed $withHeadings + * @param array $responseHeaders * @static */ - public static function downloadExcel($fileName, $writerType = null, $withHeadings = false) + public static function downloadExcel($fileName, $writerType = null, $withHeadings = false, $responseHeaders = []) { - return \Illuminate\Support\Collection::downloadExcel($fileName, $writerType, $withHeadings); + return \Illuminate\Support\Collection::downloadExcel($fileName, $writerType, $withHeadings, $responseHeaders); } /** * @@ -15424,6 +16120,16 @@ { /** @var \Facade\FlareClient\Flare $instance */ return $instance->filterExceptionsUsing($filterExceptionsCallable); + } + /** + * + * + * @static + */ + public static function filterReportsUsing($filterReportsCallable) + { + /** @var \Facade\FlareClient\Flare $instance */ + return $instance->filterReportsUsing($filterReportsCallable); } /** * @@ -15748,9 +16454,9 @@ * @param string|null $fileName * @param string $writerType * @param array $headers + * @return \Symfony\Component\HttpFoundation\BinaryFileResponse * @throws \PhpOffice\PhpSpreadsheet\Exception * @throws \PhpOffice\PhpSpreadsheet\Writer\Exception - * @return \Symfony\Component\HttpFoundation\BinaryFileResponse * @static */ public static function download($export, $fileName, $writerType = null, $headers = []) @@ -15766,9 +16472,9 @@ * @param string|null $disk * @param string $writerType * @param mixed $diskOptions + * @return bool * @throws \PhpOffice\PhpSpreadsheet\Exception * @throws \PhpOffice\PhpSpreadsheet\Writer\Exception - * @return bool * @static */ public static function store($export, $filePath, $diskName = null, $writerType = null, $diskOptions = []) @@ -15948,6 +16654,18 @@ { /** @var \Maatwebsite\Excel\Fakes\ExcelFake $instance */ return $instance->assertQueuedWithChain($chain); + } + /** + * + * + * @param string $classname + * @param callable|null $callback + * @static + */ + public static function assertExportedInRaw($classname, $callback = null) + { + /** @var \Maatwebsite\Excel\Fakes\ExcelFake $instance */ + return $instance->assertExportedInRaw($classname, $callback); } /** * @@ -15975,7 +16693,7 @@ class Madzipper { /** * Create a new zip Archive if the file does not exists - * opens a zip archive if the file exists + * opens a zip archive if the file exists. * * @param $pathToFile string The file to open * @param \Madnest\Madzipper\Repositories\RepositoryInterface|string $type The type of the archive, defaults to zip, possible are zip, phar @@ -15991,11 +16709,11 @@ return $instance->make($pathToFile, $type); } /** - * Create a new zip archive or open an existing one + * Create a new zip archive or open an existing one. * - * @param $pathToFile + * @param string $pathToFile * @throws \Exception - * @return \Madnest\Madzipper\Madzipper + * @return self * @static */ public static function zip($pathToFile) @@ -16004,11 +16722,11 @@ return $instance->zip($pathToFile); } /** - * Create a new phar file or open one + * Create a new phar file or open one. * - * @param $pathToFile + * @param string $pathToFile * @throws \Exception - * @return \Madnest\Madzipper\Madzipper + * @return self * @static */ public static function phar($pathToFile) @@ -16017,11 +16735,11 @@ return $instance->phar($pathToFile); } /** - * Create a new rar file or open one + * Create a new rar file or open one. * - * @param $pathToFile + * @param string $pathToFile * @throws \Exception - * @return \Madnest\Madzipper\Madzipper + * @return self * @static */ public static function rar($pathToFile) @@ -16032,18 +16750,19 @@ /** * Extracts the opened zip archive to the specified location
* you can provide an array of files and folders and define if they should be a white list - * or a black list to extract. By default this method compares file names using "string starts with" logic + * or a black list to extract. By default this method compares file names using "string starts with" logic. * * @param $path string The path to extract to * @param array $files An array of files * @param int $methodFlags The Method the files should be treated * @throws \Exception + * @return void * @static */ public static function extractTo($path, $files = [], $methodFlags = 2) { /** @var \Madnest\Madzipper\Madzipper $instance */ - return $instance->extractTo($path, $files, $methodFlags); + $instance->extractTo($path, $files, $methodFlags); } /** * Extracts matching files/folders from the opened zip archive to the specified location. @@ -16060,7 +16779,7 @@ return $instance->extractMatchingRegex($extractToPath, $regex); } /** - * Gets the content of a single file if available + * Gets the content of a single file if available. * * @param $filePath string The full path (including all folders) of the file in the zip * @throws \Exception @@ -16086,7 +16805,7 @@ return $instance->add($pathToAdd, $fileName); } /** - * Add an empty directory + * Add an empty directory. * * @param $dirName * @return \Madzipper @@ -16098,7 +16817,7 @@ return $instance->addEmptyDir($dirName); } /** - * Add a file to the zip using its contents + * Add a file to the zip using its contents. * * @param $filename string The name of the file to create * @param $content string The file contents @@ -16122,7 +16841,7 @@ return $instance->getStatus(); } /** - * Remove a file or array of files and folders from the zip archive + * Remove a file or array of files and folders from the zip archive. * * @param $fileToRemove array|string The path/array to the files in the zip * @return \Madnest\Madzipper\Madzipper Madzipper instance @@ -16145,7 +16864,7 @@ return $instance->getFilePath(); } /** - * Sets the password to be used for decompressing + * Sets the password to be used for decompressing. * * @param $password * @return bool @@ -16157,7 +16876,7 @@ return $instance->usePassword($password); } /** - * Closes the zip file and frees all handles + * Closes the zip file and frees all handles. * * @static */ @@ -16191,7 +16910,7 @@ return $instance->home(); } /** - * Deletes the archive file + * Deletes the archive file. * * @static */ @@ -16201,7 +16920,7 @@ return $instance->delete(); } /** - * Get the type of the Archive + * Get the type of the Archive. * * @return string * @static @@ -16212,7 +16931,7 @@ return $instance->getArchiveType(); } /** - * Get the current internal folder pointer + * Get the current internal folder pointer. * * @return string * @static @@ -16223,7 +16942,7 @@ return $instance->getCurrentFolderPath(); } /** - * Checks if a file is present in the archive + * Checks if a file is present in the archive. * * @param $fileInArchive * @return bool @@ -16257,7 +16976,7 @@ return $instance->getFileHandler(); } /** - * Gets the path to the internal folder + * Gets the path to the internal folder. * * @return string * @static @@ -16268,7 +16987,7 @@ return $instance->getInternalPath(); } /** - * List all files that are within the archive + * List all files that are within the archive. * * @param string|null $regexFilter regular expression to filter returned files/folders. See @link http://php.net/manual/en/reference.pcre.pattern.syntax.php * @throws \RuntimeException @@ -16360,6 +17079,172 @@ } +} + + namespace Orangehill\Iseed\Facades { + /** + * + * + */ + class Iseed { + /** + * + * + * @static + */ + public static function readStubFile($file) + { + /** @var \Orangehill\Iseed\Iseed $instance */ + return $instance->readStubFile($file); + } + /** + * Generates a seed file. + * + * @param string $table + * @param string $prefix + * @param string $suffix + * @param string $database + * @param int $max + * @param string $prerunEvent + * @param string $postunEvent + * @return bool + * @throws Orangehill\Iseed\TableNotFoundException + * @static + */ + public static function generateSeed($table, $prefix = null, $suffix = null, $database = null, $max = 0, $chunkSize = 0, $exclude = null, $prerunEvent = null, $postrunEvent = null, $dumpAuto = true, $indexed = true, $orderBy = null, $direction = 'ASC') + { + /** @var \Orangehill\Iseed\Iseed $instance */ + return $instance->generateSeed($table, $prefix, $suffix, $database, $max, $chunkSize, $exclude, $prerunEvent, $postrunEvent, $dumpAuto, $indexed, $orderBy, $direction); + } + /** + * Get a seed folder path + * + * @return string + * @static + */ + public static function getSeedPath() + { + /** @var \Orangehill\Iseed\Iseed $instance */ + return $instance->getSeedPath(); + } + /** + * Get the Data + * + * @param string $table + * @return Array + * @static + */ + public static function getData($table, $max, $exclude = null, $orderBy = null, $direction = 'ASC') + { + /** @var \Orangehill\Iseed\Iseed $instance */ + return $instance->getData($table, $max, $exclude, $orderBy, $direction); + } + /** + * Repacks data read from the database + * + * @param array|object $data + * @return array + * @static + */ + public static function repackSeedData($data) + { + /** @var \Orangehill\Iseed\Iseed $instance */ + return $instance->repackSeedData($data); + } + /** + * Checks if a database table exists + * + * @param string $table + * @return boolean + * @static + */ + public static function hasTable($table) + { + /** @var \Orangehill\Iseed\Iseed $instance */ + return $instance->hasTable($table); + } + /** + * Generates a seed class name (also used as a filename) + * + * @param string $table + * @param string $prefix + * @param string $suffix + * @return string + * @static + */ + public static function generateClassName($table, $prefix = null, $suffix = null) + { + /** @var \Orangehill\Iseed\Iseed $instance */ + return $instance->generateClassName($table, $prefix, $suffix); + } + /** + * Get the path to the stub file. + * + * @return string + * @static + */ + public static function getStubPath() + { + /** @var \Orangehill\Iseed\Iseed $instance */ + return $instance->getStubPath(); + } + /** + * Populate the place-holders in the seed stub. + * + * @param string $class + * @param string $stub + * @param string $table + * @param string $data + * @param int $chunkSize + * @param string $prerunEvent + * @param string $postunEvent + * @return string + * @static + */ + public static function populateStub($class, $stub, $table, $data, $chunkSize = null, $prerunEvent = null, $postrunEvent = null, $indexed = true) + { + /** @var \Orangehill\Iseed\Iseed $instance */ + return $instance->populateStub($class, $stub, $table, $data, $chunkSize, $prerunEvent, $postrunEvent, $indexed); + } + /** + * Create the full path name to the seed file. + * + * @param string $name + * @param string $path + * @return string + * @static + */ + public static function getPath($name, $path) + { + /** @var \Orangehill\Iseed\Iseed $instance */ + return $instance->getPath($name, $path); + } + /** + * Cleans the iSeed section + * + * @return bool + * @static + */ + public static function cleanSection() + { + /** @var \Orangehill\Iseed\Iseed $instance */ + return $instance->cleanSection(); + } + /** + * Updates the DatabaseSeeder file's run method (kudoz to: https://github.com/JeffreyWay/Laravel-4-Generators) + * + * @param string $className + * @return bool + * @static + */ + public static function updateDatabaseSeederRunMethod($className) + { + /** @var \Orangehill\Iseed\Iseed $instance */ + return $instance->updateDatabaseSeederRunMethod($className); + } + + } + } namespace Illuminate\Http { @@ -16417,6 +17302,27 @@ } +} + + namespace Illuminate\Database\Query { + /** + * + * + */ + class Builder { + /** + * + * + * @see \App\Providers\AppServiceProvider::boot() + * @static + */ + public static function rawSql() + { + return \Illuminate\Database\Query\Builder::rawSql(); + } + + } + } @@ -16549,7 +17455,7 @@ namespace { * @param mixed $operator * @param mixed $value * @param string $boolean - * @return \Illuminate\Database\Eloquent\Model|static + * @return \Illuminate\Database\Eloquent\Model|static|null * @static */ public static function firstWhere($column, $operator = null, $value = null, $boolean = 'and') @@ -16781,6 +17687,20 @@ namespace { return $instance->value($column); } + /** + * Get a single column's value from the first result of the query or throw an exception. + * + * @param string|\Illuminate\Database\Query\Expression $column + * @return mixed + * @throws \Illuminate\Database\Eloquent\ModelNotFoundException + * @static + */ + public static function valueOrFail($column) + { + /** @var \Illuminate\Database\Eloquent\Builder $instance */ + return $instance->valueOrFail($column); + } + /** * Execute the query as a "select" statement. * @@ -16885,9 +17805,8 @@ namespace { * @param int|null $perPage * @param array $columns * @param string $cursorName - * @param string|null $cursor - * @return \Illuminate\Contracts\Pagination\Paginator - * @throws \Illuminate\Pagination\CursorPaginationException + * @param \Illuminate\Pagination\Cursor|string|null $cursor + * @return \Illuminate\Contracts\Pagination\CursorPaginator * @static */ public static function cursorPaginate($perPage = null, $columns = [], $cursorName = 'cursor', $cursor = null) @@ -17425,6 +18344,131 @@ namespace { return $instance->orWhereDoesntHaveMorph($relation, $types, $callback); } + /** + * Add a basic where clause to a relationship query. + * + * @param string $relation + * @param \Closure|string|array|\Illuminate\Database\Query\Expression $column + * @param mixed $operator + * @param mixed $value + * @return \Illuminate\Database\Eloquent\Builder|static + * @static + */ + public static function whereRelation($relation, $column, $operator = null, $value = null) + { + /** @var \Illuminate\Database\Eloquent\Builder $instance */ + return $instance->whereRelation($relation, $column, $operator, $value); + } + + /** + * Add an "or where" clause to a relationship query. + * + * @param string $relation + * @param \Closure|string|array|\Illuminate\Database\Query\Expression $column + * @param mixed $operator + * @param mixed $value + * @return \Illuminate\Database\Eloquent\Builder|static + * @static + */ + public static function orWhereRelation($relation, $column, $operator = null, $value = null) + { + /** @var \Illuminate\Database\Eloquent\Builder $instance */ + return $instance->orWhereRelation($relation, $column, $operator, $value); + } + + /** + * Add a polymorphic relationship condition to the query with a where clause. + * + * @param \Illuminate\Database\Eloquent\Relations\MorphTo|string $relation + * @param string|array $types + * @param \Closure|string|array|\Illuminate\Database\Query\Expression $column + * @param mixed $operator + * @param mixed $value + * @return \Illuminate\Database\Eloquent\Builder|static + * @static + */ + public static function whereMorphRelation($relation, $types, $column, $operator = null, $value = null) + { + /** @var \Illuminate\Database\Eloquent\Builder $instance */ + return $instance->whereMorphRelation($relation, $types, $column, $operator, $value); + } + + /** + * Add a polymorphic relationship condition to the query with an "or where" clause. + * + * @param \Illuminate\Database\Eloquent\Relations\MorphTo|string $relation + * @param string|array $types + * @param \Closure|string|array|\Illuminate\Database\Query\Expression $column + * @param mixed $operator + * @param mixed $value + * @return \Illuminate\Database\Eloquent\Builder|static + * @static + */ + public static function orWhereMorphRelation($relation, $types, $column, $operator = null, $value = null) + { + /** @var \Illuminate\Database\Eloquent\Builder $instance */ + return $instance->orWhereMorphRelation($relation, $types, $column, $operator, $value); + } + + /** + * Add a morph-to relationship condition to the query. + * + * @param \Illuminate\Database\Eloquent\Relations\MorphTo|string $relation + * @param \Illuminate\Database\Eloquent\Model|string $model + * @return \Illuminate\Database\Eloquent\Builder|static + * @static + */ + public static function whereMorphedTo($relation, $model, $boolean = 'and') + { + /** @var \Illuminate\Database\Eloquent\Builder $instance */ + return $instance->whereMorphedTo($relation, $model, $boolean); + } + + /** + * Add a morph-to relationship condition to the query with an "or where" clause. + * + * @param \Illuminate\Database\Eloquent\Relations\MorphTo|string $relation + * @param \Illuminate\Database\Eloquent\Model|string $model + * @return \Illuminate\Database\Eloquent\Builder|static + * @static + */ + public static function orWhereMorphedTo($relation, $model) + { + /** @var \Illuminate\Database\Eloquent\Builder $instance */ + return $instance->orWhereMorphedTo($relation, $model); + } + + /** + * Add a "belongs to" relationship where clause to the query. + * + * @param \Illuminate\Database\Eloquent\Model $related + * @param string $relationship + * @param string $boolean + * @return \Illuminate\Database\Eloquent\Builder|static + * @throws \RuntimeException + * @static + */ + public static function whereBelongsTo($related, $relationshipName = null, $boolean = 'and') + { + /** @var \Illuminate\Database\Eloquent\Builder $instance */ + return $instance->whereBelongsTo($related, $relationshipName, $boolean); + } + + /** + * Add an "BelongsTo" relationship with an "or where" clause to the query. + * + * @param \Illuminate\Database\Eloquent\Model $related + * @param string $relationship + * @return \Illuminate\Database\Eloquent\Builder|static + * @throws \RuntimeException + * @static + */ + public static function orWhereBelongsTo($related, $relationshipName = null) + { + /** @var \Illuminate\Database\Eloquent\Builder $instance */ + return $instance->orWhereBelongsTo($related, $relationshipName); + } + /** * Add subselect queries to include an aggregate value for a relationship. * @@ -17535,18 +18579,6 @@ namespace { return $instance->mergeConstraintsFrom($from); } - /** - * Explains the query. - * - * @return \Illuminate\Support\Collection - * @static - */ - public static function explain() - { - /** @var \Illuminate\Database\Eloquent\Builder $instance */ - return $instance->explain(); - } - /** * Chunk the results of the query. * @@ -17639,7 +18671,7 @@ namespace { /** * Query lazily, by chunking the results of a query by comparing IDs. * - * @param int $count + * @param int $chunkSize * @param string|null $column * @param string|null $alias * @return \Illuminate\Support\LazyCollection @@ -17652,6 +18684,22 @@ namespace { return $instance->lazyById($chunkSize, $column, $alias); } + /** + * Query lazily, by chunking the results of a query by comparing IDs in descending order. + * + * @param int $chunkSize + * @param string|null $column + * @param string|null $alias + * @return \Illuminate\Support\LazyCollection + * @throws \InvalidArgumentException + * @static + */ + public static function lazyByIdDesc($chunkSize = 1000, $column = null, $alias = null) + { + /** @var \Illuminate\Database\Eloquent\Builder $instance */ + return $instance->lazyByIdDesc($chunkSize, $column, $alias); + } + /** * Execute the query and get the first result. * @@ -17680,26 +18728,11 @@ namespace { return $instance->baseSole($columns); } - /** - * Apply the callback's query changes if the given "value" is true. - * - * @param mixed $value - * @param callable $callback - * @param callable|null $default - * @return mixed|$this - * @static - */ - public static function when($value, $callback, $default = null) - { - /** @var \Illuminate\Database\Eloquent\Builder $instance */ - return $instance->when($value, $callback, $default); - } - /** * Pass the query to a given callback. * * @param callable $callback - * @return \Illuminate\Database\Eloquent\Builder|static + * @return $this|mixed * @static */ public static function tap($callback) @@ -17709,12 +18742,27 @@ namespace { } /** - * Apply the callback's query changes if the given "value" is false. + * Apply the callback if the given "value" is truthy. * * @param mixed $value * @param callable $callback * @param callable|null $default - * @return mixed|$this + * @return $this|mixed + * @static + */ + public static function when($value, $callback, $default = null) + { + /** @var \Illuminate\Database\Eloquent\Builder $instance */ + return $instance->when($value, $callback, $default); + } + + /** + * Apply the callback if the given "value" is falsy. + * + * @param mixed $value + * @param callable $callback + * @param callable|null $default + * @return $this|mixed * @static */ public static function unless($value, $callback, $default = null) @@ -17723,6 +18771,17 @@ namespace { return $instance->unless($value, $callback, $default); } + /** + * + * + * @see \App\Providers\AppServiceProvider::boot() + * @static + */ + public static function rawSql() + { + return \Illuminate\Database\Eloquent\Builder::rawSql(); + } + /** * Set the columns to be selected. * @@ -17810,6 +18869,7 @@ namespace { /** * Force the query to only return distinct results. * + * @param mixed $distinct * @return \Illuminate\Database\Query\Builder * @static */ @@ -18242,7 +19302,7 @@ namespace { /** * Add an "or where null" clause to the query. * - * @param string $column + * @param string|array $column * @return \Illuminate\Database\Query\Builder * @static */ @@ -18400,7 +19460,7 @@ namespace { /** * Add a "where date" statement to the query. * - * @param string $column + * @param \Illuminate\Database\Query\Expression|string $column * @param string $operator * @param \DateTimeInterface|string|null $value * @param string $boolean @@ -18799,6 +19859,35 @@ namespace { return $instance->dynamicWhere($method, $parameters); } + /** + * Add a "where fulltext" clause to the query. + * + * @param string|string[] $columns + * @param string $value + * @param string $boolean + * @return \Illuminate\Database\Query\Builder + * @static + */ + public static function whereFullText($columns, $value, $options = [], $boolean = 'and') + { + /** @var \Illuminate\Database\Query\Builder $instance */ + return $instance->whereFullText($columns, $value, $options, $boolean); + } + + /** + * Add a "or where fulltext" clause to the query. + * + * @param string|string[] $columns + * @param string $value + * @return \Illuminate\Database\Query\Builder + * @static + */ + public static function orWhereFullText($columns, $value, $options = []) + { + /** @var \Illuminate\Database\Query\Builder $instance */ + return $instance->orWhereFullText($columns, $value, $options); + } + /** * Add a "group by" clause to the query. * @@ -18905,7 +19994,7 @@ namespace { /** * Add an "order by" clause to the query. * - * @param \Closure|\Illuminate\Database\Query\Builder|\Illuminate\Database\Query\Expression|string $column + * @param \Closure|\Illuminate\Database\Eloquent\Builder|\Illuminate\Database\Query\Builder|\Illuminate\Database\Query\Expression|string $column * @param string $direction * @return \Illuminate\Database\Query\Builder * @throws \InvalidArgumentException @@ -18920,7 +20009,7 @@ namespace { /** * Add a descending "order by" clause to the query. * - * @param string $column + * @param \Closure|\Illuminate\Database\Eloquent\Builder|\Illuminate\Database\Query\Builder|\Illuminate\Database\Query\Expression|string $column * @return \Illuminate\Database\Query\Builder * @static */ @@ -19056,7 +20145,7 @@ namespace { /** * Remove all existing orders and optionally add a new order. * - * @param string|null $column + * @param \Closure|\Illuminate\Database\Query\Builder|\Illuminate\Database\Query\Expression|string|null $column * @param string $direction * @return \Illuminate\Database\Query\Builder * @static @@ -19405,6 +20494,19 @@ namespace { return $instance->insertUsing($columns, $query); } + /** + * Update records in a PostgreSQL database using the update from syntax. + * + * @param array $values + * @return int + * @static + */ + public static function updateFrom($values) + { + /** @var \Illuminate\Database\Query\Builder $instance */ + return $instance->updateFrom($values); + } + /** * Insert or update a record matching the attributes, and fill it with values. * @@ -19498,6 +20600,19 @@ namespace { return $instance->addBinding($value, $type); } + /** + * Cast the given binding value. + * + * @param mixed $value + * @return mixed + * @static + */ + public static function castBinding($value) + { + /** @var \Illuminate\Database\Query\Builder $instance */ + return $instance->castBinding($value); + } + /** * Merge an array of bindings into our bindings. * @@ -19601,13 +20716,25 @@ namespace { /** * Die and dump the current SQL and bindings. * - * @return void + * @return \Illuminate\Database\Query\never * @static */ public static function dd() { /** @var \Illuminate\Database\Query\Builder $instance */ - $instance->dd(); + return $instance->dd(); + } + + /** + * Explains the query. + * + * @return \Illuminate\Support\Collection + * @static + */ + public static function explain() + { + /** @var \Illuminate\Database\Query\Builder $instance */ + return $instance->explain(); } /** @@ -19637,6 +20764,17 @@ namespace { \Illuminate\Database\Query\Builder::mixin($mixin, $replace); } + /** + * Flush the existing macros. + * + * @return void + * @static + */ + public static function flushMacros() + { + \Illuminate\Database\Query\Builder::flushMacros(); + } + /** * Dynamically handle calls to the class. * @@ -19679,6 +20817,7 @@ namespace { class Excel extends \Maatwebsite\Excel\Facades\Excel {} class Madzipper extends \Madnest\Madzipper\Facades\Madzipper {} class Captcha extends \Mews\Captcha\Facades\Captcha {} + class Iseed extends \Orangehill\Iseed\Facades\Iseed {} } diff --git a/app/Exceptions/Handler.php b/app/Exceptions/Handler.php index e17af7d63..9ce03c8fc 100644 --- a/app/Exceptions/Handler.php +++ b/app/Exceptions/Handler.php @@ -67,7 +67,13 @@ class Handler extends ExceptionHandler public function report(Throwable $e) { if ($e instanceof ApiException) { - Log::error($e->getMessage(), ['exception' => ' at ' . $e->getFile() .':' . $e->getLine()]); + if ($e->getCode() !== -1) { + Log::error($e->getMessage(), [ + 'code' => $e->getCode(), + 'data' => $e->getData(), + 'exception' => ' at ' . $e->getFile() . ':' . $e->getLine() + ]); + } } else { parent::report($e); } diff --git a/app/Helpers/Function.php b/app/Helpers/Function.php index 13f4fc583..15b3936cc 100644 --- a/app/Helpers/Function.php +++ b/app/Helpers/Function.php @@ -15,3 +15,10 @@ if (!function_exists('seeders_at')) { return date("Y-m-d H:i:s", $time); } } + +if (!function_exists('md5s')) { + function md5s($val, $len = 16) + { + return substr(md5($val), 32 - $len); + } +} diff --git a/app/Http/Controllers/Api/DialogController.php b/app/Http/Controllers/Api/DialogController.php index 232294771..56342755c 100755 --- a/app/Http/Controllers/Api/DialogController.php +++ b/app/Http/Controllers/Api/DialogController.php @@ -2,18 +2,24 @@ namespace App\Http\Controllers\Api; +use App\Models\AbstractModel; +use App\Models\Deleted; use App\Models\File; +use App\Models\FileContent; use App\Models\ProjectTask; use App\Models\ProjectTaskFile; use App\Models\User; use App\Models\WebSocketDialog; use App\Models\WebSocketDialogMsg; use App\Models\WebSocketDialogMsgRead; +use App\Models\WebSocketDialogMsgTodo; use App\Models\WebSocketDialogUser; use App\Module\Base; +use App\Module\TimeRange; use Carbon\Carbon; +use DB; +use Redirect; use Request; -use Response; /** * @apiDefine dialog @@ -30,8 +36,12 @@ class DialogController extends AbstractController * @apiGroup dialog * @apiName lists * + * @apiParam {String} [timerange] 时间范围(如:1678248944,1678248944) + * - 第一个时间: 读取在这个时间之后更新的数据 + * - 第二个时间: 读取在这个时间之后删除的数据ID(第1页附加返回数据: deleted_id) + * * @apiParam {Number} [page] 当前页,默认:1 - * @apiParam {Number} [pagesize] 每页显示数量,默认:100,最大:200 + * @apiParam {Number} [pagesize] 每页显示数量,默认:50,最大:100 * * @apiSuccess {Number} ret 返回状态码(1正确、0错误) * @apiSuccess {String} msg 返回信息(错误描述) @@ -41,21 +51,109 @@ class DialogController extends AbstractController { $user = User::auth(); // - $list = WebSocketDialog::select(['web_socket_dialogs.*', 'u.top_at', 'u.mark_unread']) + $timerange = TimeRange::parse(Request::input()); + // + $builder = WebSocketDialog::select(['web_socket_dialogs.*', 'u.top_at', 'u.mark_unread', 'u.silence', 'u.updated_at as user_at']) ->join('web_socket_dialog_users as u', 'web_socket_dialogs.id', '=', 'u.dialog_id') + ->where('u.userid', $user->userid); + if ($timerange->updated) { + $builder->where('u.updated_at', '>', $timerange->updated); + } + $list = $builder + ->orderByDesc('u.top_at') + ->orderByDesc('web_socket_dialogs.last_at') + ->paginate(Base::getPaginate(100, 50)); + $list->transform(function (WebSocketDialog $item) use ($user) { + return $item->formatData($user->userid); + }); + // + $data = $list->toArray(); + if ($list->currentPage() === 1) { + $data['deleted_id'] = Deleted::ids('dialog', $user->userid, $timerange->deleted); + } + // + return Base::retSuccess('success', $data); + } + + /** + * @api {get} api/dialog/search 02. 搜索会话 + * + * @apiDescription 根据消息关键词搜索相关会话,需要token身份 + * @apiVersion 1.0.0 + * @apiGroup dialog + * @apiName search + * + * @apiParam {String} key 消息关键词 + * + * @apiSuccess {Number} ret 返回状态码(1正确、0错误) + * @apiSuccess {String} msg 返回信息(错误描述) + * @apiSuccess {Object} data 返回数据 + */ + public function search() + { + $user = User::auth(); + // + $key = trim(Request::input('key')); + if (empty($key)) { + return Base::retError('请输入搜索关键词'); + } + // 搜索会话 + $dialogs = WebSocketDialog::select(['web_socket_dialogs.*', 'u.top_at', 'u.mark_unread', 'u.silence', 'u.updated_at as user_at']) + ->join('web_socket_dialog_users as u', 'web_socket_dialogs.id', '=', 'u.dialog_id') + ->where('web_socket_dialogs.name', 'LIKE', "%{$key}%") ->where('u.userid', $user->userid) ->orderByDesc('u.top_at') ->orderByDesc('web_socket_dialogs.last_at') - ->paginate(Base::getPaginate(200, 100)); - $list->transform(function (WebSocketDialog $item) use ($user) { - return WebSocketDialog::formatData($item, $user->userid); + ->take(20) + ->get(); + $dialogs->transform(function (WebSocketDialog $item) use ($user) { + return $item->formatData($user->userid); }); + $list = $dialogs->toArray(); + // 搜索联系人 + if (count($list) < 20 && Base::judgeClientVersion("0.21.60")) { + $users = User::select(User::$basicField) + ->where(function ($query) use ($key) { + if (str_contains($key, "@")) { + $query->where("email", "like", "%{$key}%"); + } else { + $query->where("nickname", "like", "%{$key}%")->orWhere("pinyin", "like", "%{$key}%"); + } + })->orderBy('userid') + ->take(20 - count($list)) + ->get(); + $users->transform(function (User $item) { + return [ + 'id' => 'u:' . $item->userid, + 'type' => 'user', + 'name' => $item->nickname, + 'dialog_user' => $item, + 'last_msg' => null, + ]; + }); + $list = array_merge($list, $users->toArray()); + } + // 搜索消息会话 + if (count($list) < 20) { + $msgs = WebSocketDialog::select(['web_socket_dialogs.*', 'u.top_at', 'u.mark_unread', 'u.silence', 'u.updated_at as user_at', 'm.id as search_msg_id']) + ->join('web_socket_dialog_users as u', 'web_socket_dialogs.id', '=', 'u.dialog_id') + ->join('web_socket_dialog_msgs as m', 'web_socket_dialogs.id', '=', 'm.dialog_id') + ->where('u.userid', $user->userid) + ->where('m.key', 'LIKE', "%{$key}%") + ->orderByDesc('m.id') + ->take(20 - count($list)) + ->get(); + $msgs->transform(function (WebSocketDialog $item) use ($user) { + return $item->formatData($user->userid); + }); + $list = array_merge($list, $msgs->toArray()); + } // return Base::retSuccess('success', $list); } /** - * @api {get} api/dialog/one 02. 获取单个会话信息 + * @api {get} api/dialog/one 03. 获取单个会话信息 * * @apiDescription 需要token身份 * @apiVersion 1.0.0 @@ -74,20 +172,170 @@ class DialogController extends AbstractController // $dialog_id = intval(Request::input('dialog_id')); // - $item = WebSocketDialog::select(['web_socket_dialogs.*', 'u.top_at', 'u.mark_unread']) + $item = WebSocketDialog::select(['web_socket_dialogs.*', 'u.top_at', 'u.mark_unread', 'u.silence', 'u.updated_at as user_at']) ->join('web_socket_dialog_users as u', 'web_socket_dialogs.id', '=', 'u.dialog_id') ->where('web_socket_dialogs.id', $dialog_id) ->where('u.userid', $user->userid) ->first(); - if ($item) { - $item = WebSocketDialog::formatData($item, $user->userid); + if (empty($item)) { + return Base::retError('会话不存在或已被删除', ['dialog_id' => $dialog_id], -4003); } - // - return Base::retSuccess('success', $item); + return Base::retSuccess('success', $item->formatData($user->userid)); } /** - * @api {get} api/dialog/msg/user 03. 打开会话 + * @api {get} api/dialog/user 04. 获取会话成员 + * + * @apiDescription 需要token身份 + * @apiVersion 1.0.0 + * @apiGroup dialog + * @apiName user + * + * @apiParam {Number} dialog_id 会话ID + * @apiParam {Number} [getuser] 获取会员详情(1: 返回会员昵称、邮箱等基本信息,0: 默认不返回) + * + * @apiSuccess {Number} ret 返回状态码(1正确、0错误) + * @apiSuccess {String} msg 返回信息(错误描述) + * @apiSuccess {Object} data 返回数据 + */ + public function user() + { + User::auth(); + // + $dialog_id = intval(Request::input('dialog_id')); + $getuser = intval(Request::input('getuser', 0)); + // + $dialog = WebSocketDialog::checkDialog($dialog_id); + // + $data = $dialog->dialogUser->toArray(); + if ($getuser === 1) { + $array = []; + foreach ($data as $item) { + $res = User::userid2basic($item['userid']); + if ($res) { + $array[] = array_merge($item, $res->toArray()); + } + } + $data = $array; + } + // + $array = []; + foreach ($data as $item) { + if ($item['userid'] > 0) { + $array[] = $item; + } + } + return Base::retSuccess('success', $array); + } + + /** + * @api {get} api/dialog/todo 05. 获取会话待办 + * + * @apiDescription 需要token身份 + * @apiVersion 1.0.0 + * @apiGroup dialog + * @apiName todo + * + * @apiParam {Number} dialog_id 会话ID + * + * @apiSuccess {Number} ret 返回状态码(1正确、0错误) + * @apiSuccess {String} msg 返回信息(错误描述) + * @apiSuccess {Object} data 返回数据 + */ + public function todo() + { + $user = User::auth(); + // + $dialog_id = intval(Request::input('dialog_id')); + // + WebSocketDialog::checkDialog($dialog_id); + // + $list = WebSocketDialogMsgTodo::whereDialogId($dialog_id)->whereUserid($user->userid)->whereDoneAt(null)->orderByDesc('id')->take(50)->get(); + return Base::retSuccess("success", $list); + } + + /** + * @api {get} api/dialog/top 06. 会话置顶 + * + * @apiDescription 需要token身份 + * @apiVersion 1.0.0 + * @apiGroup dialog + * @apiName top + * + * @apiParam {Number} dialog_id 会话ID + * + * @apiSuccess {Number} ret 返回状态码(1正确、0错误) + * @apiSuccess {String} msg 返回信息(错误描述) + * @apiSuccess {Object} data 返回数据 + */ + public function top() + { + $user = User::auth(); + $dialogId = intval(Request::input('dialog_id')); + $dialogUser = WebSocketDialogUser::whereUserid($user->userid)->whereDialogId($dialogId)->first(); + if (!$dialogUser) { + return Base::retError("会话不存在"); + } + $dialogUser->top_at = $dialogUser->top_at ? null : Carbon::now(); + $dialogUser->save(); + return Base::retSuccess("success", [ + 'id' => $dialogUser->dialog_id, + 'top_at' => $dialogUser->top_at?->toDateTimeString(), + ]); + } + + /** + * @api {get} api/dialog/tel 07. 获取对方联系电话 + * + * @apiDescription 需要token身份 + * @apiVersion 1.0.0 + * @apiGroup dialog + * @apiName tel + * + * @apiParam {Number} dialog_id 会话ID + * + * @apiSuccess {Number} ret 返回状态码(1正确、0错误) + * @apiSuccess {String} msg 返回信息(错误描述) + * @apiSuccess {Object} data 返回数据 + */ + public function tel() + { + $user = User::auth(); + // + $dialog_id = intval(Request::input('dialog_id')); + // + $dialog = WebSocketDialog::checkDialog($dialog_id); + if ($dialog->type !== 'user') { + return Base::retError("会话类型错误"); + } + $dialogUser = $dialog->dialogUser->where('userid', '!=', $user->userid)->first(); + if (empty($dialogUser)) { + return Base::retError("会话对象不存在"); + } + $callUser = User::find($dialogUser->userid); + if (empty($callUser) || empty($callUser->tel)) { + return Base::retError("对方未设置联系电话"); + } + if ($user->isTemp()) { + return Base::retError("无法查看联系电话"); + } + // + $add = null; + $res = WebSocketDialogMsg::sendMsg(null, $dialog->id, 'notice', [ + 'notice' => $user->nickname . " 查看了 " . $callUser->nickname . " 的联系电话" + ]); + if (Base::isSuccess($res)) { + $add = $res['data']; + } + // + return Base::retSuccess("success", [ + 'tel' => $callUser->tel, + 'add' => $add ?: null + ]); + } + + /** + * @api {get} api/dialog/open/user 08. 打开会话 * * @apiDescription 需要token身份 * @apiVersion 1.0.0 @@ -105,100 +353,309 @@ class DialogController extends AbstractController $user = User::auth(); // $userid = intval(Request::input('userid')); - if ($userid == $user->userid) { - return Base::retError('不能对话自己'); + if (empty($userid)) { + return Base::retError('错误的会话'); } // - $dialog = WebSocketDialog::checkUserDialog($user->userid, $userid); + $dialog = WebSocketDialog::checkUserDialog($user, $userid); if (empty($dialog)) { return Base::retError('打开会话失败'); } - $data = WebSocketDialog::formatData(WebSocketDialog::find($dialog->id), $user->userid); - if (empty($data)) { - return Base::retError('打开会话错误'); - } + $data = WebSocketDialog::find($dialog->id)?->formatData($user->userid); return Base::retSuccess('success', $data); } /** - * @api {get} api/dialog/msg/lists 04. 获取消息列表 + * @api {get} api/dialog/msg/list 09. 获取消息列表 * * @apiDescription 需要token身份 * @apiVersion 1.0.0 * @apiGroup dialog - * @apiName msg__lists + * @apiName msg__list * * @apiParam {Number} dialog_id 对话ID + * @apiParam {Number} [msg_id] 消息ID + * @apiParam {Number} [position_id] 此消息ID前后的数据 + * @apiParam {Number} [prev_id] 此消息ID之前的数据 + * @apiParam {Number} [next_id] 此消息ID之后的数据 + * - position_id、prev_id、next_id 只有一个有效,优先循序为:position_id > prev_id > next_id + * @apiParam {String} [msg_type] 消息类型 + * - tag: 标记 + * - link: 链接 + * - text: 文本 + * - image: 图片 + * - file: 文件 + * - record: 录音 + * - meeting: 会议 * - * @apiParam {Number} [page] 当前页,默认:1 - * @apiParam {Number} [pagesize] 每页显示数量,默认:50,最大:100 + * @apiParam {Number} [take] 获取条数,默认:50,最大:100 * * @apiSuccess {Number} ret 返回状态码(1正确、0错误) * @apiSuccess {String} msg 返回信息(错误描述) * @apiSuccess {Object} data 返回数据 */ - public function msg__lists() + public function msg__list() { $user = User::auth(); // $dialog_id = intval(Request::input('dialog_id')); + $msg_id = intval(Request::input('msg_id')); + $position_id = intval(Request::input('position_id')); + $prev_id = intval(Request::input('prev_id')); + $next_id = intval(Request::input('next_id')); + $msg_type = trim(Request::input('msg_type')); + $take = Base::getPaginate(100, 50, 'take'); + $data = []; // $dialog = WebSocketDialog::checkDialog($dialog_id); + $reDialog = true; // - $list = WebSocketDialogMsg::whereDialogId($dialog_id)->orderByDesc('id')->paginate(Base::getPaginate(100, 50)); - $list->transform(function (WebSocketDialogMsg $item) use ($user) { - $item->is_read = $item->userid === $user->userid || WebSocketDialogMsgRead::whereMsgId($item->id)->whereUserid($user->userid)->value('read_at'); - return $item; - }); + $builder = WebSocketDialogMsg::select([ + 'web_socket_dialog_msgs.*', + 'read.mention', + 'read.read_at', + ])->leftJoin('web_socket_dialog_msg_reads as read', function ($leftJoin) use ($user) { + $leftJoin + ->on('read.userid', '=', DB::raw($user->userid)) + ->on('read.msg_id', '=', 'web_socket_dialog_msgs.id'); + })->where('web_socket_dialog_msgs.dialog_id', $dialog_id); // + if ($msg_type) { + if ($msg_type === 'tag') { + $builder->where('tag', '>', 0); + } elseif ($msg_type === 'link') { + $builder->whereLink(1); + } elseif (in_array($msg_type, ['text', 'image', 'file', 'record', 'meeting'])) { + $builder->whereMtype($msg_type); + } else { + return Base::retError('参数错误'); + } + $reDialog = false; + } + if ($msg_id > 0) { + $builder->whereReplyId($msg_id); + $reDialog = false; + } + // + if ($position_id > 0) { + $array = $builder->clone() + ->where('web_socket_dialog_msgs.id', '>=', $position_id) + ->orderBy('web_socket_dialog_msgs.id') + ->take(intval($take / 2)) + ->get(); + $prev_id = intval($array->last()?->id); + } + // + $cloner = $builder->clone(); + if ($prev_id > 0) { + $cloner->where('web_socket_dialog_msgs.id', '<=', $prev_id)->orderByDesc('web_socket_dialog_msgs.id'); + $reDialog = false; + } elseif ($next_id > 0) { + $cloner->where('web_socket_dialog_msgs.id', '>=', $next_id)->orderBy('web_socket_dialog_msgs.id'); + $reDialog = false; + } else { + $cloner->orderByDesc('web_socket_dialog_msgs.id'); + } + $list = $cloner->take($take)->get()->sortByDesc('id', SORT_NUMERIC)->values(); + // + if ($list->isNotEmpty()) { + $list->transform(function (WebSocketDialogMsg $item) { + $item->next_id = 0; + $item->prev_id = 0; + return $item; + }); + $first = $list->first(); + $first->next_id = intval($builder->clone() + ->where('web_socket_dialog_msgs.id', '>', $first->id) + ->orderBy('web_socket_dialog_msgs.id') + ->value('id')); + $last = $list->last(); + $last->prev_id = intval($builder->clone() + ->where('web_socket_dialog_msgs.id', '<', $last->id) + ->orderByDesc('web_socket_dialog_msgs.id') + ->value('id')); + } + $data['list'] = $list; + $data['time'] = Base::time(); + // 记录当前打开的任务对话 if ($dialog->type == 'group' && $dialog->group_type == 'task') { $user->task_dialog_id = $dialog->id; $user->save(); } - //去掉标记未读 + // 去掉标记未读 $isMarkDialogUser = WebSocketDialogUser::whereDialogId($dialog->id)->whereUserid($user->userid)->whereMarkUnread(1)->first(); if ($isMarkDialogUser) { $isMarkDialogUser->mark_unread = 0; $isMarkDialogUser->save(); } // - $data = $list->toArray(); - if ($list->currentPage() === 1) { - $data['dialog'] = WebSocketDialog::formatData($dialog, $user->userid); + if ($reDialog) { + $data['dialog'] = $dialog->formatData($user->userid, true); + $data['todo'] = $data['dialog']->todo_num > 0 ? WebSocketDialogMsgTodo::whereDialogId($dialog->id)->whereUserid($user->userid)->whereDoneAt(null)->orderByDesc('id')->take(50)->get() : []; } return Base::retSuccess('success', $data); } /** - * @api {get} api/dialog/msg/unread 05. 获取未读消息数量 + * @api {get} api/dialog/msg/search 10. 搜索消息位置 + * + * @apiDescription 需要token身份 + * @apiVersion 1.0.0 + * @apiGroup dialog + * @apiName msg__search + * + * @apiParam {Number} dialog_id 对话ID + * @apiParam {String} key 搜索关键词 + * + * @apiSuccess {Number} ret 返回状态码(1正确、0错误) + * @apiSuccess {String} msg 返回信息(错误描述) + * @apiSuccess {Object} data 返回数据 + */ + public function msg__search() + { + User::auth(); + // + $dialog_id = intval(Request::input('dialog_id')); + $key = trim(Request::input('key')); + // + if (empty($key)) { + return Base::retError('关键词不能为空'); + } + // + WebSocketDialog::checkDialog($dialog_id); + // + $data = WebSocketDialogMsg::whereDialogId($dialog_id) + ->where('key', 'LIKE', "%{$key}%") + ->take(200) + ->pluck('id'); + return Base::retSuccess('success', [ + 'data' => $data + ]); + } + + /** + * @api {get} api/dialog/msg/one 10. 获取单条消息 + * + * @apiDescription 需要token身份 + * @apiVersion 1.0.0 + * @apiGroup dialog + * @apiName msg__one + * + * @apiParam {Number} msg_id 消息ID + * + * @apiSuccess {Number} ret 返回状态码(1正确、0错误) + * @apiSuccess {String} msg 返回信息(错误描述) + * @apiSuccess {Object} data 返回数据 + */ + public function msg__one() + { + User::auth(); + // + $msg_id = intval(Request::input('msg_id')); + // + $msg = WebSocketDialogMsg::whereId($msg_id)->first(); + if (empty($msg)) { + return Base::retError("消息不存在或已被删除"); + } + WebSocketDialog::checkDialog($msg->dialog_id); + // + return Base::retSuccess('success', $msg); + } + + /** + * @api {get} api/dialog/msg/read 11. 已读聊天消息 + * + * @apiDescription 需要token身份 + * @apiVersion 1.0.0 + * @apiGroup dialog + * @apiName msg__read + * + * @apiParam {Number} id 消息ID(组) + * + * @apiSuccess {Number} ret 返回状态码(1正确、0错误) + * @apiSuccess {String} msg 返回信息(错误描述) + * @apiSuccess {Object} data 返回数据 + */ + public function msg__read() + { + $user = User::auth(); + // + $id = Request::input('id'); + $ids = Base::explodeInt($id); + // + $dialogIds = []; + WebSocketDialogMsg::whereIn('id', $ids)->chunkById(20, function($list) use ($user, &$dialogIds) { + /** @var WebSocketDialogMsg $item */ + foreach ($list as $item) { + $item->readSuccess($user->userid); + $dialogIds[$item->dialog_id] = $item->dialog_id; + } + }); + // + $data = []; + $dialogUsers = WebSocketDialogUser::with(['webSocketDialog'])->whereUserid($user->userid)->whereIn('dialog_id', array_values($dialogIds))->get(); + foreach ($dialogUsers as $dialogUser) { + if (!$dialogUser->webSocketDialog) { + continue; + } + $dialogUser->updated_at = Carbon::now(); + $dialogUser->save(); + // + $dialogUser->webSocketDialog->generateUnread($user->userid); + $data[] = [ + 'id' => $dialogUser->webSocketDialog->id, + 'unread' => $dialogUser->webSocketDialog->unread, + 'mention' => $dialogUser->webSocketDialog->mention, + 'user_at' => Carbon::parse($dialogUser->updated_at)->toDateTimeString('millisecond'), + 'user_ms' => Carbon::parse($dialogUser->updated_at)->valueOf() + ]; + } + return Base::retSuccess('success', $data); + } + + /** + * @api {get} api/dialog/msg/unread 12. 获取未读消息数据 * * @apiDescription 需要token身份 * @apiVersion 1.0.0 * @apiGroup dialog * @apiName msg__unread * - * @apiParam {Number} [dialog_id] 对话ID,留空获取总未读消息数量 + * @apiParam {Number} dialog_id 对话ID * * @apiSuccess {Number} ret 返回状态码(1正确、0错误) * @apiSuccess {String} msg 返回信息(错误描述) * @apiSuccess {Object} data 返回数据 + * @apiSuccessExample {json} data: + { + "id": 43, + "unread": 308, + "mention": 11, + "user_at": "2020-12-12 00:00:00.000", + "user_ms": 1677558147167, + } */ public function msg__unread() { $dialog_id = intval(Request::input('dialog_id')); // - $builder = WebSocketDialogMsgRead::whereUserid(User::userid())->whereReadAt(null); - if ($dialog_id > 0) { - $builder->whereDialogId($dialog_id); + $dialogUser = WebSocketDialogUser::with(['webSocketDialog'])->whereDialogId($dialog_id)->whereUserid(User::userid())->first(); + if (empty($dialogUser?->webSocketDialog)) { + return Base::retError('会话不存在'); } - $unread = $builder->count(); + $dialogUser->webSocketDialog->generateUnread($dialogUser->userid); + // return Base::retSuccess('success', [ - 'unread' => $unread, + 'id' => $dialogUser->webSocketDialog->id, + 'unread' => $dialogUser->webSocketDialog->unread, + 'mention' => $dialogUser->webSocketDialog->mention, + 'user_at' => Carbon::parse($dialogUser->updated_at)->toDateTimeString('millisecond'), + 'user_ms' => Carbon::parse($dialogUser->updated_at)->valueOf() ]); } /** - * @api {post} api/dialog/msg/sendtext 06. 发送消息 + * @api {post} api/dialog/msg/sendtext 13. 发送消息 * * @apiDescription 需要token身份 * @apiVersion 1.0.0 @@ -207,6 +664,11 @@ class DialogController extends AbstractController * * @apiParam {Number} dialog_id 对话ID * @apiParam {String} text 消息内容 + * @apiParam {Number} [update_id] 更新消息ID(优先大于reply_id) + * @apiParam {Number} [reply_id] 回复ID + * @apiParam {String} [silence] 是否静默发送 + * - no: 正常发送(默认) + * - yes: 静默发送 * * @apiSuccess {Number} ret 返回状态码(1正确、0错误) * @apiSuccess {String} msg 返回信息(错误描述) @@ -214,57 +676,135 @@ class DialogController extends AbstractController */ public function msg__sendtext() { - Base::checkClientVersion('0.8.1'); + Base::checkClientVersion('0.19.0'); $user = User::auth(); // - $chat_nickname = Base::settingFind('system', 'chat_nickname'); - if ($chat_nickname == 'required') { - $nickname = User::select(['nickname as nickname_original'])->whereUserid($user->userid)->value('nickname_original'); - if (empty($nickname)) { - return Base::retError('请设置昵称', [], -2); + if (!$user->bot) { + $chatInformation = Base::settingFind('system', 'chat_information'); + if ($chatInformation == 'required') { + if (empty($user->getRawOriginal('nickname'))) { + return Base::retError('请设置昵称', [], -2); + } + if (empty($user->getRawOriginal('tel'))) { + return Base::retError('请设置联系电话', [], -3); + } } } // $dialog_id = Base::getPostInt('dialog_id'); + $update_id = Base::getPostInt('update_id'); + $reply_id = Base::getPostInt('reply_id'); $text = trim(Base::getPostValue('text')); - // - if (mb_strlen($text) < 1) { - return Base::retError('消息内容不能为空'); - } elseif (mb_strlen($text) > 20000) { - return Base::retError('消息内容最大不能超过20000字'); - } + $silence = trim(Base::getPostValue('silence')) === 'yes'; // WebSocketDialog::checkDialog($dialog_id); // - if (mb_strlen($text) > 2000) { - $array = mb_str_split($text, 2000); + if ($update_id > 0) { + $action = "update-$update_id"; + } elseif ($reply_id > 0) { + $action = "reply-$reply_id"; } else { - $array = [$text]; + $action = ""; } // - $list = []; - foreach ($array as $item) { - $res = WebSocketDialogMsg::sendMsg($dialog_id, 'text', ['text' => $item], $user->userid); - if (Base::isSuccess($res)) { - $list[] = $res['data']; + $text = WebSocketDialogMsg::formatMsg($text, $dialog_id); + $strlen = mb_strlen($text); + $noimglen = mb_strlen(preg_replace("/]*?>/i", "", $text)); + if ($strlen < 1) { + return Base::retError('消息内容不能为空'); + } + if ($noimglen > 200000) { + return Base::retError('消息内容最大不能超过200000字'); + } + if ($noimglen > 5000) { + // 内容过长转成文件发送 + $path = "uploads/chat/" . date("Ym") . "/" . $dialog_id . "/"; + Base::makeDir(public_path($path)); + $path = $path . md5($text) . ".htm"; + $file = public_path($path); + file_put_contents($file, $text); + $size = filesize(public_path($path)); + if (empty($size)) { + return Base::retError('消息发送保存失败'); } + $fileData = [ + 'name' => "LongText-{$strlen}.htm", + 'size' => $size, + 'file' => $file, + 'path' => $path, + 'url' => Base::fillUrl($path), + 'thumb' => '', + 'width' => -1, + 'height' => -1, + 'ext' => 'htm', + ]; + return WebSocketDialogMsg::sendMsg($action, $dialog_id, 'file', $fileData, $user->userid, false, false, $silence); } // - return Base::retSuccess('发送成功', $list); + return WebSocketDialogMsg::sendMsg($action, $dialog_id, 'text', ['text' => $text], $user->userid, false, false, $silence); } /** - * @api {post} api/dialog/msg/sendfile 07. 文件上传 + * @api {post} api/dialog/msg/sendrecord 14. 发送语音 + * + * @apiDescription 需要token身份 + * @apiVersion 1.0.0 + * @apiGroup dialog + * @apiName msg__sendrecord + * + * @apiParam {Number} dialog_id 对话ID + * @apiParam {Number} [reply_id] 回复ID + * @apiParam {String} base64 语音base64 + * @apiParam {Number} duration 语音时长(毫秒) + * + * @apiSuccess {Number} ret 返回状态码(1正确、0错误) + * @apiSuccess {String} msg 返回信息(错误描述) + * @apiSuccess {Object} data 返回数据 + */ + public function msg__sendrecord() + { + $user = User::auth(); + // + $dialog_id = Base::getPostInt('dialog_id'); + $reply_id = Base::getPostInt('reply_id'); + // + WebSocketDialog::checkDialog($dialog_id); + // + $action = $reply_id > 0 ? "reply-$reply_id" : ""; + $path = "uploads/chat/" . date("Ym") . "/" . $dialog_id . "/"; + $base64 = Base::getPostValue('base64'); + $duration = Base::getPostInt('duration'); + if ($duration < 600) { + return Base::retError('说话时间太短'); + } + $data = Base::record64save([ + "base64" => $base64, + "path" => $path, + ]); + if (Base::isError($data)) { + return Base::retError($data['msg']); + } else { + $recordData = $data['data']; + $recordData['size'] *= 1024; + $recordData['duration'] = $duration; + return WebSocketDialogMsg::sendMsg($action, $dialog_id, 'record', $recordData, $user->userid); + } + } + + /** + * @api {post} api/dialog/msg/sendfile 15. 文件上传 * * @apiDescription 需要token身份 * @apiVersion 1.0.0 * @apiGroup dialog * @apiName msg__sendfile * - * @apiParam {Number} dialog_id 对话ID - * @apiParam {String} [filename] post-文件名称 - * @apiParam {String} [image64] post-base64图片(二选一) - * @apiParam {File} [files] post-文件对象(二选一) + * @apiParam {Number} dialog_id 对话ID + * @apiParam {Number} [reply_id] 回复ID + * @apiParam {Number} [image_attachment] 图片是否也存到附件 + * @apiParam {String} [filename] post-文件名称 + * @apiParam {String} [image64] post-base64图片(二选一) + * @apiParam {File} [files] post-文件对象(二选一) * * @apiSuccess {Number} ret 返回状态码(1正确、0错误) * @apiSuccess {String} msg 返回信息(错误描述) @@ -275,9 +815,12 @@ class DialogController extends AbstractController $user = User::auth(); // $dialog_id = Base::getPostInt('dialog_id'); + $reply_id = Base::getPostInt('reply_id'); + $image_attachment = Base::getPostInt('image_attachment'); // $dialog = WebSocketDialog::checkDialog($dialog_id); // + $action = $reply_id > 0 ? "reply-$reply_id" : ""; $path = "uploads/chat/" . date("Ym") . "/" . $dialog_id . "/"; $image64 = Base::getPostValue('image64'); $fileName = Base::getPostValue('filename'); @@ -303,8 +846,8 @@ class DialogController extends AbstractController $fileData['thumb'] = Base::unFillUrl($fileData['thumb']); $fileData['size'] *= 1024; // - if ($dialog->type === 'group' && $dialog->group_type === 'task') { // 任务群聊保存文件 - if (!in_array($fileData['ext'], File::localExt)) { // 如果是图片不保存 + if ($dialog->type === 'group' && $dialog->group_type === 'task') { // 任务群组保存文件 + if ($image_attachment || !in_array($fileData['ext'], File::imageExt)) { // 如果是图片不保存 $task = ProjectTask::whereDialogId($dialog->id)->first(); if ($task) { $file = ProjectTaskFile::createInstance([ @@ -322,7 +865,7 @@ class DialogController extends AbstractController } } // - $result = WebSocketDialogMsg::sendMsg($dialog_id, 'file', $fileData, $user->userid); + $result = WebSocketDialogMsg::sendMsg($action, $dialog_id, 'file', $fileData, $user->userid); if (Base::isSuccess($result)) { if (isset($task)) { $result['data']['task_id'] = $task->id; @@ -333,7 +876,130 @@ class DialogController extends AbstractController } /** - * @api {get} api/dialog/msg/readlist 08. 获取消息阅读情况 + * @api {get} api/dialog/msg/sendfileid 16. 通过文件ID发送文件 + * + * @apiDescription 需要token身份 + * @apiVersion 1.0.0 + * @apiGroup dialog + * @apiName msg__sendfileid + * + * @apiParam {Number} file_id 消息ID + * @apiParam {Array} dialogids 转发给的对话ID + * @apiParam {Array} userids 转发给的成员ID + * + * @apiSuccess {Number} ret 返回状态码(1正确、0错误) + * @apiSuccess {String} msg 返回信息(错误描述) + * @apiSuccess {Object} data 返回数据 + */ + public function msg__sendfileid() + { + $user = User::auth(); + // + $file_id = intval(Request::input("file_id")); + $dialogids = Request::input('dialogids'); + $userids = Request::input('userids'); + // + if (empty($dialogids) && empty($userids)) { + return Base::retError("请选择转发对话或成员"); + } + // + $file = File::permissionFind($file_id, $user); + $fileLink = $file->getShareLink($user->userid); + $fileMsg = "~{$file->getNameAndExt()}"; + // + return AbstractModel::transaction(function() use ($user, $fileMsg, $userids, $dialogids) { + $msgs = []; + $already = []; + if ($dialogids) { + if (!is_array($dialogids)) { + $dialogids = [$dialogids]; + } + foreach ($dialogids as $dialogid) { + $res = WebSocketDialogMsg::sendMsg(null, $dialogid, 'text', ['text' => $fileMsg], $user->userid); + if (Base::isSuccess($res)) { + $msgs[] = $res['data']; + $already[] = $dialogid; + } + } + } + if ($userids) { + if (!is_array($userids)) { + $userids = [$userids]; + } + foreach ($userids as $userid) { + if (!User::whereUserid($userid)->exists()) { + continue; + } + $dialog = WebSocketDialog::checkUserDialog($user, $userid); + if ($dialog && !in_array($dialog->id, $already)) { + $res = WebSocketDialogMsg::sendMsg(null, $dialog->id, 'text', ['text' => $fileMsg], $user->userid); + if (Base::isSuccess($res)) { + $msgs[] = $res['data']; + } + } + } + } + return Base::retSuccess('发送成功', [ + 'msgs' => $msgs + ]); + }); + } + + /** + * @api {post} api/dialog/msg/sendanon 16. 发送匿名消息 + * + * @apiDescription 需要token身份 + * @apiVersion 1.0.0 + * @apiGroup dialog + * @apiName msg__sendanon + * + * @apiParam {Number} userid 对方会员ID + * @apiParam {String} text 消息内容 + * + * @apiSuccess {Number} ret 返回状态码(1正确、0错误) + * @apiSuccess {String} msg 返回信息(错误描述) + * @apiSuccess {Object} data 返回数据 + */ + public function msg__sendanon() + { + User::auth(); + // + $userid = Base::getPostInt('userid'); + $text = trim(Base::getPostValue('text')); + // + $anonMessage = Base::settingFind('system', 'anon_message', 'open'); + if ($anonMessage != 'open') { + return Base::retError("匿名消息功能暂停使用"); + } + // + $toUser = User::whereUserid($userid)->first(); + if (empty($toUser) || $toUser->bot) { + return Base::retError("匿名消息仅允许发送给个人"); + } + if ($toUser->isDisable()) { + return Base::retError("对方已离职"); + } + $strlen = mb_strlen($text); + if ($strlen < 1) { + return Base::retError('消息内容不能为空'); + } + if ($strlen > 2000) { + return Base::retError('消息内容最大不能超过2000字'); + } + // + $botUser = User::botGetOrCreate('anon-msg'); + if (empty($botUser)) { + return Base::retError('匿名机器人不存在'); + } + $dialog = WebSocketDialog::checkUserDialog($botUser, $toUser->userid); + if (empty($dialog)) { + return Base::retError('匿名机器人会话不存在'); + } + return WebSocketDialogMsg::sendMsg(null, $dialog->id, 'text', ['text' => "

{$text}

"], $botUser->userid, true); + } + + /** + * @api {get} api/dialog/msg/readlist 17. 获取消息阅读情况 * * @apiDescription 需要token身份 * @apiVersion 1.0.0 @@ -362,7 +1028,7 @@ class DialogController extends AbstractController } /** - * @api {get} api/dialog/msg/detail 09. 消息详情 + * @api {get} api/dialog/msg/detail 18. 消息详情 * * @apiDescription 需要token身份 * @apiVersion 1.0.0 @@ -410,14 +1076,17 @@ class DialogController extends AbstractController } /** - * @api {get} api/dialog/msg/download 10. 文件下载 + * @api {get} api/dialog/msg/download 19. 文件下载 * * @apiDescription 需要token身份 * @apiVersion 1.0.0 * @apiGroup dialog * @apiName msg__download * - * @apiParam {Number} msg_id 消息ID + * @apiParam {Number} msg_id 消息ID + * @apiParam {String} down 直接下载 + * - yes: 下载(默认) + * - preview: 转预览地址 * * @apiSuccess {Number} ret 返回状态码(1正确、0错误) * @apiSuccess {String} msg 返回信息(错误描述) @@ -428,6 +1097,7 @@ class DialogController extends AbstractController User::auth(); // $msg_id = intval(Request::input('msg_id')); + $down = Request::input('down', 'yes'); // $msg = WebSocketDialogMsg::whereId($msg_id)->first(); if (empty($msg)) { @@ -438,11 +1108,18 @@ class DialogController extends AbstractController } $array = Base::json2array($msg->getRawOriginal('msg')); // - return Response::download(public_path($array['path']), $array['name']); + if ($down === 'preview') { + return Redirect::to(FileContent::toPreviewUrl($array)); + } + // + $filePath = public_path($array['path']); + return Base::streamDownload(function() use ($filePath) { + echo file_get_contents($filePath); + }, $array['name']); } /** - * @api {get} api/dialog/msg/withdraw 11. 聊天消息撤回 + * @api {get} api/dialog/msg/withdraw 20. 聊天消息撤回 * * @apiDescription 消息撤回限制24小时内,需要token身份 * @apiVersion 1.0.0 @@ -463,58 +1140,95 @@ class DialogController extends AbstractController if (empty($msg)) { return Base::retError("消息不存在或已被删除"); } - $msg->deleteMsg(); + $msg->withdrawMsg(); return Base::retSuccess("success"); } /** - * @api {get} api/dialog/top 12. 会话置顶 - * - * @apiDescription 需要token身份 - * @apiVersion 1.0.0 - * @apiGroup dialog - * @apiName top - * - * @apiParam {Number} dialog_id 会话ID - * - * @apiSuccess {Number} ret 返回状态码(1正确、0错误) - * @apiSuccess {String} msg 返回信息(错误描述) - * @apiSuccess {Object} data 返回数据 - */ - public function top() - { - $user = User::auth(); - $dialogId = intval(Request::input('dialog_id')); - $dialogUser = WebSocketDialogUser::whereUserid($user->userid)->whereDialogId($dialogId)->first(); - if (!$dialogUser) { - return Base::retError("会话不存在"); - } - $dialogUser->top_at = $dialogUser->top_at ? null : Carbon::now(); - $dialogUser->save(); - return Base::retSuccess("success", [ - 'id' => $dialogUser->dialog_id, - 'top_at' => $dialogUser->top_at?->toDateTimeString(), - ]); - } - - /** - * @api {get} api/dialog/msg/mark 13. 消息标记操作 + * @api {get} api/dialog/msg/mark 21. 消息标记操作 * * @apiDescription 需要token身份 * @apiVersion 1.0.0 * @apiGroup dialog * @apiName msg__mark * - * @apiParam {Number} dialog_id 消息ID - * @apiParam {String} type 类型 - * - read - * - unread + * @apiParam {Number} dialog_id 会话ID + * @apiParam {String} type 类型 + * - read: 已读 + * - unread: 未读 + * @apiParam {Number} [after_msg_id] 仅标记已读指定之后(含)的消息 * * @apiSuccess {Number} ret 返回状态码(1正确、0错误) * @apiSuccess {String} msg 返回信息(错误描述) * @apiSuccess {Object} data 返回数据 */ public function msg__mark() + { + $user = User::auth(); + // + $dialog_id = intval(Request::input('dialog_id')); + $type = Request::input('type'); + $after_msg_id = intval(Request::input('after_msg_id')); + // + $dialogUser = WebSocketDialogUser::with(['webSocketDialog'])->whereDialogId($dialog_id)->whereUserid($user->userid)->first(); + if (empty($dialogUser?->webSocketDialog)) { + return Base::retError('会话不存在'); + } + switch ($type) { + case 'read': + $builder = WebSocketDialogMsgRead::whereDialogId($dialog_id)->whereUserid($user->userid)->whereReadAt(null); + if ($after_msg_id > 0) { + $builder->where('msg_id', '>=', $after_msg_id); + } + $builder->chunkById(100, function ($list) { + WebSocketDialogMsgRead::onlyMarkRead($list); + }); + // + $dialogUser->webSocketDialog->generateUnread($user->userid); + $data = [ + 'id' => $dialogUser->webSocketDialog->id, + 'unread' => $dialogUser->webSocketDialog->unread, + 'mention' => $dialogUser->webSocketDialog->mention, + 'mark_unread' => 0, + ]; + break; + + case 'unread': + $data = [ + 'id' => $dialogUser->webSocketDialog->id, + 'mark_unread' => 1, + ]; + break; + + default: + return Base::retError("参数错误"); + } + $dialogUser->mark_unread = $data['mark_unread']; + $dialogUser->save(); + return Base::retSuccess("success", array_merge($data, [ + 'user_at' => Carbon::parse($dialogUser->updated_at)->toDateTimeString('millisecond'), + 'user_ms' => Carbon::parse($dialogUser->updated_at)->valueOf(), + ])); + } + + /** + * @api {get} api/dialog/msg/silence 22. 消息免打扰 + * + * @apiDescription 需要token身份 + * @apiVersion 1.0.0 + * @apiGroup dialog + * @apiName msg__silence + * + * @apiParam {Number} dialog_id 会话ID + * @apiParam {String} type 类型 + * - set + * - cancel + * + * @apiSuccess {Number} ret 返回状态码(1正确、0错误) + * @apiSuccess {String} msg 返回信息(错误描述) + * @apiSuccess {Object} data 返回数据 + */ + public function msg__silence() { $user = User::auth(); $dialogId = intval(Request::input('dialog_id')); @@ -523,29 +1237,565 @@ class DialogController extends AbstractController if (!$dialogUser) { return Base::retError("会话不存在"); } + // + $dialogData = WebSocketDialog::find($dialogId); + if (empty($dialogData)) { + return Base::retError("会话不存在"); + } + if ($dialogData->type === 'group' && $dialogData->group_type !== 'user') { + return Base::retError("此会话不允许设置免打扰"); + } + // switch ($type) { - case 'read': + case 'set': + $data['silence'] = 0; WebSocketDialogMsgRead::whereUserid($user->userid) ->whereReadAt(null) ->whereDialogId($dialogId) ->chunkById(100, function ($list) { WebSocketDialogMsgRead::onlyMarkRead($list); }); - $dialogUser->mark_unread = 0; + $dialogUser->silence = 1; $dialogUser->save(); break; - case 'unread': - $dialogUser->mark_unread = 1; + case 'cancel': + $dialogUser->silence = 0; $dialogUser->save(); break; default: return Base::retError("参数错误"); } - return Base::retSuccess("success", [ + $data = [ 'id' => $dialogId, - 'mark_unread' => $dialogUser->mark_unread, + 'silence' => $dialogUser->silence, + ]; + return Base::retSuccess("success", $data); + } + + /** + * @api {get} api/dialog/msg/forward 23. 转发消息给 + * + * @apiDescription 需要token身份 + * @apiVersion 1.0.0 + * @apiGroup dialog + * @apiName msg__forward + * + * @apiParam {Number} msg_id 消息ID + * @apiParam {Array} dialogids 转发给的对话ID + * @apiParam {Array} userids 转发给的成员ID + * + * @apiSuccess {Number} ret 返回状态码(1正确、0错误) + * @apiSuccess {String} msg 返回信息(错误描述) + * @apiSuccess {Object} data 返回数据 + */ + public function msg__forward() + { + $user = User::auth(); + // + $msg_id = intval(Request::input("msg_id")); + $dialogids = Request::input('dialogids'); + $userids = Request::input('userids'); + // + if (empty($dialogids) && empty($userids)) { + return Base::retError("请选择转发对话或成员"); + } + // + $msg = WebSocketDialogMsg::whereId($msg_id)->first(); + if (empty($msg)) { + return Base::retError("消息不存在或已被删除"); + } + WebSocketDialog::checkDialog($msg->dialog_id); + // + return $msg->forwardMsg($dialogids, $userids, $user); + } + + /** + * @api {get} api/dialog/msg/emoji 24. emoji回复 + * + * @apiDescription 需要token身份 + * @apiVersion 1.0.0 + * @apiGroup dialog + * @apiName msg__emoji + * + * @apiParam {Number} msg_id 消息ID + * @apiParam {String} symbol 回复或取消的emoji表情 + * + * @apiSuccess {Number} ret 返回状态码(1正确、0错误) + * @apiSuccess {String} msg 返回信息(错误描述) + * @apiSuccess {Object} data 返回数据 + */ + public function msg__emoji() + { + $user = User::auth(); + // + $msg_id = intval(Request::input("msg_id")); + $symbol = Request::input("symbol"); + // + if (!preg_match("/^[\u{d800}-\u{dbff}]|[\u{dc00}-\u{dfff}]$/", $symbol)) { + return Base::retError("参数错误"); + } + // + $msg = WebSocketDialogMsg::whereId($msg_id)->first(); + if (empty($msg)) { + return Base::retError("消息不存在或已被删除"); + } + WebSocketDialog::checkDialog($msg->dialog_id); + // + return $msg->emojiMsg($symbol, $user->userid); + } + + /** + * @api {get} api/dialog/msg/tag 25. 标注/取消标注 + * + * @apiDescription 需要token身份 + * @apiVersion 1.0.0 + * @apiGroup dialog + * @apiName msg__tag + * + * @apiParam {Number} msg_id 消息ID + * + * @apiSuccess {Number} ret 返回状态码(1正确、0错误) + * @apiSuccess {String} msg 返回信息(错误描述) + * @apiSuccess {Object} data 返回数据 + */ + public function msg__tag() + { + $user = User::auth(); + // + $msg_id = intval(Request::input("msg_id")); + // + $msg = WebSocketDialogMsg::whereId($msg_id)->first(); + if (empty($msg)) { + return Base::retError("消息不存在或已被删除"); + } + WebSocketDialog::checkDialog($msg->dialog_id); + // + return $msg->toggleTagMsg($user->userid); + } + + /** + * @api {get} api/dialog/msg/todo 26. 设待办/取消待办 + * + * @apiDescription 需要token身份 + * @apiVersion 1.0.0 + * @apiGroup dialog + * @apiName msg__todo + * + * @apiParam {Number} msg_id 消息ID + * @apiParam {String} type 设待办对象 + * - all: 会话全部成员(默认) + * - user: 会话指定成员 + * @apiParam {Array} userids 会员ID组(type=user有效,格式: [userid1, userid2, userid3]) + * + * @apiSuccess {Number} ret 返回状态码(1正确、0错误) + * @apiSuccess {String} msg 返回信息(错误描述) + * @apiSuccess {Object} data 返回数据 + */ + public function msg__todo() + { + $user = User::auth(); + // + $msg_id = intval(Request::input("msg_id")); + $type = trim(Request::input("type", "all")); + $userids = Request::input('userids'); + // + if ($type === 'user') { + if (empty($userids)) { + return Base::retError("选择指定成员"); + } + } else { + $userids = []; + } + // + $msg = WebSocketDialogMsg::whereId($msg_id)->first(); + if (empty($msg)) { + return Base::retError("消息不存在或已被删除"); + } + WebSocketDialog::checkDialog($msg->dialog_id); + // + return $msg->toggleTodoMsg($user->userid, $userids); + } + + /** + * @api {get} api/dialog/msg/todolist 27. 获取消息待办情况 + * + * @apiDescription 需要token身份 + * @apiVersion 1.0.0 + * @apiGroup dialog + * @apiName msg__todolist + * + * @apiParam {Number} msg_id 消息ID + * + * @apiSuccess {Number} ret 返回状态码(1正确、0错误) + * @apiSuccess {String} msg 返回信息(错误描述) + * @apiSuccess {Object} data 返回数据 + */ + public function msg__todolist() + { + User::auth(); + // + $msg_id = intval(Request::input('msg_id')); + // + $msg = WebSocketDialogMsg::whereId($msg_id)->first(); + if (empty($msg)) { + return Base::retError("消息不存在或已被删除"); + } + WebSocketDialog::checkDialog($msg->dialog_id); + // + $todo = WebSocketDialogMsgTodo::whereMsgId($msg_id)->get(); + return Base::retSuccess('success', $todo ?: []); + } + + /** + * @api {get} api/dialog/msg/done 28. 完成待办 + * + * @apiDescription 需要token身份 + * @apiVersion 1.0.0 + * @apiGroup dialog + * @apiName msg__done + * + * @apiParam {Number} id 待办数据ID + * + * @apiSuccess {Number} ret 返回状态码(1正确、0错误) + * @apiSuccess {String} msg 返回信息(错误描述) + * @apiSuccess {Object} data 返回数据 + */ + public function msg__done() + { + $user = User::auth(); + // + $id = intval(Request::input("id")); + // + $add = []; + $todo = WebSocketDialogMsgTodo::whereId($id)->whereUserid($user->userid)->first(); + if ($todo && empty($todo->done_at)) { + $todo->done_at = Carbon::now(); + $todo->save(); + // + $msg = WebSocketDialogMsg::find($todo->msg_id); + if ($msg) { + $res = WebSocketDialogMsg::sendMsg(null, $todo->dialog_id, 'todo', [ + 'action' => 'done', + 'data' => [ + 'id' => $msg->id, + 'type' => $msg->type, + 'msg' => $msg->quoteTextMsg(), + ] + ]); + if (Base::isSuccess($res)) { + $add = $res['data']; + } + } + } + // + return Base::retSuccess("待办已完成", [ + 'add' => $add ?: null + ]); + } + + /** + * @api {get} api/dialog/group/add 29. 新增群组 + * + * @apiDescription 需要token身份 + * @apiVersion 1.0.0 + * @apiGroup dialog + * @apiName group__add + * + * @apiParam {String} [avatar] 群头像 + * @apiParam {String} [chat_name] 群名称 + * @apiParam {Array} userids 群成员,格式: [userid1, userid2, userid3] + * + * @apiSuccess {Number} ret 返回状态码(1正确、0错误) + * @apiSuccess {String} msg 返回信息(错误描述) + * @apiSuccess {Object} data 返回数据 + */ + public function group__add() + { + $user = User::auth(); + // + $avatar = Request::input('avatar'); + $avatar = $avatar ? Base::unFillUrl(is_array($avatar) ? $avatar[0]['path'] : $avatar) : ''; + $chatName = trim(Request::input('chat_name')); + $userids = Request::input('userids'); + // + if (!is_array($userids)) { + return Base::retError('请选择群成员'); + } + $userids = array_merge([$user->userid], $userids); + $userids = array_values(array_filter(array_unique($userids))); + if (count($userids) < 2) { + return Base::retError('群成员至少2人'); + } + // + if (empty($chatName)) { + $array = []; + foreach ($userids as $userid) { + $array[] = User::userid2nickname($userid); + if (count($array) >= 8 || strlen(implode(", ", $array)) > 100) { + $array[] = "..."; + break; + } + } + $chatName = implode(", ", $array); + } + if ($user->isTemp()) { + return Base::retError('无法创建群组'); + } + $dialog = WebSocketDialog::createGroup($chatName, $userids, 'user', $user->userid); + if (empty($dialog)) { + return Base::retError('创建群组失败'); + } + if ($avatar) { + $dialog->avatar = $avatar; + $dialog->save(); + } + $data = WebSocketDialog::find($dialog->id)?->formatData($user->userid); + $userids = array_values(array_diff($userids, [$user->userid])); + $dialog->pushMsg("groupAdd", null, $userids); + return Base::retSuccess('创建成功', $data); + } + + /** + * @api {get} api/dialog/group/edit 30. 修改群组 + * + * @apiDescription 需要token身份 + * @apiVersion 1.0.0 + * @apiGroup dialog + * @apiName group__edit + * + * @apiParam {Number} dialog_id 会话ID + * @apiParam {String} [avatar] 群头像 + * @apiParam {String} [chat_name] 群名称 + * @apiParam {Number} [admin] 系统管理员操作(1:只判断是不是系统管理员,否则判断是否群管理员) + * + * @apiSuccess {Number} ret 返回状态码(1正确、0错误) + * @apiSuccess {String} msg 返回信息(错误描述) + * @apiSuccess {Object} data 返回数据 + */ + public function group__edit() + { + $user = User::auth(); + // + $dialog_id = intval(Request::input('dialog_id')); + $admin = intval(Request::input('admin')); + // + if ($admin === 1) { + $user->checkAdmin(); + $dialog = WebSocketDialog::find($dialog_id); + if (empty($dialog)) { + return Base::retError('对话不存在或已被删除', ['dialog_id' => $dialog_id], -4003); + } + } else { + $dialog = WebSocketDialog::checkDialog($dialog_id, true); + } + // + $data = ['id' => $dialog->id]; + $array = []; + if (Request::exists('avatar')) { + $avatar = Request::input('avatar'); + $avatar = $avatar ? Base::unFillUrl(is_array($avatar) ? $avatar[0]['path'] : $avatar) : ''; + $data['avatar'] = Base::fillUrl($array['avatar'] = $avatar); + } + if (Request::exists('chat_name') && $dialog->group_type === 'user') { + $chatName = trim(Request::input('chat_name')); + if (mb_strlen($chatName) < 2) { + return Base::retError('群名称至少2个字'); + } + if (mb_strlen($chatName) > 100) { + return Base::retError('群名称最长限制100个字'); + } + $data['name'] = $array['name'] = $chatName; + } + // + if ($array) { + $dialog->updateInstance($array); + $dialog->save(); + WebSocketDialogUser::whereDialogId($dialog->id)->change(['updated_at' => Carbon::now()->toDateTimeString('millisecond')]); + } + // + return Base::retSuccess('修改成功', $data); + } + + /** + * @api {get} api/dialog/group/adduser 31. 添加群成员 + * + * @apiDescription 需要token身份 + * - 有群主时:只有群主可以邀请 + * - 没有群主时:群内成员都可以邀请 + * @apiVersion 1.0.0 + * @apiGroup dialog + * @apiName group__adduser + * + * @apiParam {Number} dialog_id 会话ID + * @apiParam {Array} userids 新增的群成员,格式: [userid1, userid2, userid3] + * + * @apiSuccess {Number} ret 返回状态码(1正确、0错误) + * @apiSuccess {String} msg 返回信息(错误描述) + * @apiSuccess {Object} data 返回数据 + */ + public function group__adduser() + { + $user = User::auth(); + // + $dialog_id = intval(Request::input('dialog_id')); + $userids = Request::input('userids'); + // + if (!is_array($userids)) { + return Base::retError('请选择群成员'); + } + // + $dialog = WebSocketDialog::checkDialog($dialog_id, "auto"); + // + $dialog->checkGroup(); + $dialog->joinGroup($userids, $user->userid); + $dialog->pushMsg("groupJoin", null, $userids); + return Base::retSuccess('添加成功'); + } + + /** + * @api {get} api/dialog/group/deluser 32. 移出(退出)群成员 + * + * @apiDescription 需要token身份 + * - 只有群主、邀请人可以踢人 + * - 群主、任务人员、项目人员不可被踢或退出 + * @apiVersion 1.0.0 + * @apiGroup dialog + * @apiName group__adduser + * + * @apiParam {Number} dialog_id 会话ID + * @apiParam {Array} [userids] 移出的群成员,格式: [userid1, userid2, userid3] + * - 留空表示自己退出 + * - 有值表示移出,仅限群主操作 + * + * @apiSuccess {Number} ret 返回状态码(1正确、0错误) + * @apiSuccess {String} msg 返回信息(错误描述) + * @apiSuccess {Object} data 返回数据 + */ + public function group__deluser() + { + $user = User::auth(); + // + $dialog_id = intval(Request::input('dialog_id')); + $userids = Request::input('userids'); + // + $type = 'remove'; + if (empty($userids)) { + $type = 'exit'; + $userids = [$user->userid]; + } + // + if (!is_array($userids)) { + return Base::retError('请选择群成员'); + } + // + $dialog = WebSocketDialog::checkDialog($dialog_id); + // + $dialog->checkGroup(); + $dialog->exitGroup($userids, $type); + $dialog->pushMsg("groupExit", null, $userids); + return Base::retSuccess($type === 'remove' ? '移出成功' : '退出成功'); + } + + /** + * @api {get} api/dialog/group/transfer 33. 转让群组 + * + * @apiDescription 需要token身份 + * - 只有群主且是个人类型群可以解散 + * @apiVersion 1.0.0 + * @apiGroup dialog + * @apiName group__transfer + * + * @apiParam {Number} dialog_id 会话ID + * @apiParam {Number} userid 新的群主 + * + * @apiSuccess {Number} ret 返回状态码(1正确、0错误) + * @apiSuccess {String} msg 返回信息(错误描述) + * @apiSuccess {Object} data 返回数据 + */ + public function group__transfer() + { + $user = User::auth(); + // + $dialog_id = intval(Request::input('dialog_id')); + $userid = intval(Request::input('userid')); + // + if ($userid === $user->userid) { + return Base::retError('你已经是群主'); + } + if (!User::whereUserid($userid)->exists()) { + return Base::retError('请选择有效的新群主'); + } + // + $dialog = WebSocketDialog::checkDialog($dialog_id, true); + // + $dialog->checkGroup('user'); + $dialog->owner_id = $userid; + if ($dialog->save()) { + $dialog->joinGroup($userid, 0); + $dialog->pushMsg("groupUpdate", [ + 'id' => $dialog->id, + 'owner_id' => $dialog->owner_id, + ]); + } + return Base::retSuccess('转让成功'); + } + + /** + * @api {get} api/dialog/group/disband 34. 解散群组 + * + * @apiDescription 需要token身份 + * - 只有群主且是个人类型群可以解散 + * @apiVersion 1.0.0 + * @apiGroup dialog + * @apiName group__disband + * + * @apiParam {Number} dialog_id 会话ID + * + * @apiSuccess {Number} ret 返回状态码(1正确、0错误) + * @apiSuccess {String} msg 返回信息(错误描述) + * @apiSuccess {Object} data 返回数据 + */ + public function group__disband() + { + User::auth(); + // + $dialog_id = intval(Request::input('dialog_id')); + // + $dialog = WebSocketDialog::checkDialog($dialog_id, true); + // + $dialog->checkGroup('user'); + $dialog->deleteDialog(); + return Base::retSuccess('解散成功'); + } + + /** + * @api {get} api/dialog/group/searchuser 35. 搜索个人群(仅限管理员) + * + * @apiDescription 需要token身份,用于创建部门搜索个人群组 + * @apiVersion 1.0.0 + * @apiGroup dialog + * @apiName group__searchuser + * + * @apiParam {String} key 关键词 + * + * @apiSuccess {Number} ret 返回状态码(1正确、0错误) + * @apiSuccess {String} msg 返回信息(错误描述) + * @apiSuccess {Object} data 返回数据 + */ + public function group__searchuser() + { + User::auth('admin'); + // + $key = trim(Request::input('key')); + // + $builder = WebSocketDialog::whereType('group')->whereGroupType('user'); + if ($key) { + $builder->where('name', 'like', "%{$key}%"); + } + return Base::retSuccess('success', [ + 'list' => $builder->take(20)->get() ]); } } diff --git a/app/Http/Controllers/Api/FileController.php b/app/Http/Controllers/Api/FileController.php index 1f279e405..97565726b 100755 --- a/app/Http/Controllers/Api/FileController.php +++ b/app/Http/Controllers/Api/FileController.php @@ -13,6 +13,7 @@ use App\Module\Base; use App\Module\Ihttp; use Carbon\Carbon; use Illuminate\Support\Facades\DB; +use Redirect; use Request; /** @@ -44,11 +45,12 @@ class FileController extends AbstractController $pid = intval($data['pid']); // $permission = 1000; + $userids = $user->isTemp() ? [$user->userid] : [0, $user->userid]; + $builder = File::wherePid($pid); if ($pid > 0) { - File::permissionFind($pid, 0, $permission); - $builder = File::wherePid($pid); + File::permissionFind($pid, $userids, 0, $permission); } else { - $builder = File::whereUserid($user->userid); + $builder->whereUserid($user->userid); } // $array = $builder->take(500)->get()->toArray(); @@ -65,9 +67,25 @@ class FileController extends AbstractController } $pid = $file->pid; $temp = $file->toArray(); - $temp['permission'] = $file->getPermission($user->userid); + $temp['permission'] = $file->getPermission($userids); $array[] = $temp; } + // 去除没有权限的文件 + $isUnset = false; + foreach ($array as $index1 => $item1) { + if ($item1['permission'] === -1) { + foreach ($array as $index2 => $item2) { + if ($item2['pid'] === $item1['id']) { + $array[$index2]['pid'] = 0; + } + } + $isUnset = true; + unset($array[$index1]); + } + } + if ($isUnset) { + $array = array_values($array); + } } else { // 获取共享相关 DB::statement("SET SQL_MODE=''"); @@ -75,10 +93,7 @@ class FileController extends AbstractController $list = File::select(["files.*", DB::raw("MAX({$pre}file_users.permission) as permission")]) ->join('file_users', 'files.id', '=', 'file_users.file_id') ->where('files.userid', '!=', $user->userid) - ->where(function ($query) use ($user) { - $query->where('file_users.userid', 0); - $query->orWhere('file_users.userid', $user->userid); - }) + ->whereIn('file_users.userid', $userids) ->groupBy('files.id') ->take(100) ->get(); @@ -92,7 +107,7 @@ class FileController extends AbstractController } // 图片直接返回预览地址 foreach ($array as &$item) { - File::handleImageUrl($item); + $item = File::handleImageUrl($item); } return Base::retSuccess('success', $array); } @@ -119,13 +134,18 @@ class FileController extends AbstractController // $permission = 0; if (Base::isNumber($id)) { - User::auth(); - $file = File::permissionFind(intval($id), 0, $permission); + $user = User::auth(); + $file = File::permissionFind(intval($id), $user, 0, $permission); } elseif ($id) { $fileLink = FileLink::whereCode($id)->first(); $file = $fileLink?->file; if (empty($file)) { - return Base::retError('链接不存在'); + $msg = '文件链接不存在'; + $data = File::code2IdName($id); + if ($data) { + $msg = "【{$data->name}】 {$msg}"; + } + return Base::retError($msg, $data); } } else { return Base::retError('参数错误'); @@ -144,6 +164,7 @@ class FileController extends AbstractController * @apiGroup file * @apiName search * + * @apiParam {String} [link] 通过分享地址搜索(如:https://t.hitosea.com/single/file/ODcwOCwzOSxpa0JBS2lmVQ==) * @apiParam {String} [key] 关键词 * * @apiSuccess {Number} ret 返回状态码(1正确、0错误) @@ -154,15 +175,54 @@ class FileController extends AbstractController { $user = User::auth(); // + $link = trim(Request::input('link')); $key = trim(Request::input('key')); - if (empty($key)) { - return Base::retError('请输入关键词'); + $id = 0; + $take = 50; + if (preg_match("/\/single\/file\/(.*?)$/i", $link, $match)) { + $id = intval(FileLink::whereCode($match[1])->value('file_id')); + $take = 1; + } + // 搜索自己的 + $builder = File::whereUserid($user->userid); + if ($id) { + $builder->where("id", $id); + } + if ($key) { + $builder->where("name", "like", "%{$key}%"); + } + $array = $builder->take($take)->get()->toArray(); + // 搜索共享的 + $take = $take - count($array); + if ($take > 0 && ($id || $key)) { + $builder = File::whereIn('pshare', function ($queryA) use ($user) { + $queryA->select('files.id') + ->from('files') + ->join('file_users', 'files.id', '=', 'file_users.file_id') + ->where('files.userid', '!=', $user->userid) + ->where(function ($queryB) use ($user) { + $queryB->whereIn('file_users.userid', [0, $user->userid]); + }); + }); + if ($id) { + $builder->where("id", $id); + } + if ($key) { + $builder->where("name", "like", "%{$key}%"); + } + $list = $builder->take($take)->get(); + if ($list->isNotEmpty()) { + foreach ($list as $file) { + $temp = $file->toArray(); + if ($file->pshare === $file->id) { + $temp['pid'] = 0; + } + $array[] = $temp; + } + } } // - $builder = File::whereUserid($user->userid)->where("name", "like", "%{$key}%"); - $list = $builder->take(50)->get(); - // - return Base::retSuccess('success', $list); + return Base::retSuccess('success', $array); } /** @@ -195,15 +255,24 @@ class FileController extends AbstractController } elseif (mb_strlen($name) > 32) { return Base::retError('文件名称最多只能设置32个字'); } + $tmpName = preg_replace("/[\\\\\/:*?\"<>|]/", '', $name); + if ($tmpName != $name) { + return Base::retError("文件名称不能包含这些字符:\/:*?\"<>|"); + } // if ($id > 0) { // 修改 - $file = File::permissionFind($id, 1); + $file = File::permissionFind($id, $user, 1); // $file->name = $name; + $file->handleDuplicateName(); $file->save(); - $file->pushMsg('update', $file); - return Base::retSuccess('修改成功', $file); + $data = [ + 'id' => $file->id, + 'name' => $file->name, + ]; + $file->pushMsg('update', $data); + return Base::retSuccess('修改成功', $data); } else { // 添加 if (!in_array($type, [ @@ -240,7 +309,7 @@ class FileController extends AbstractController if (File::wherePid($pid)->count() >= 300) { return Base::retError('每个文件夹里最多只能创建300个文件或文件夹'); } - $row = File::permissionFind($pid, 1); + $row = File::permissionFind($pid, $user, 1); $userid = $row->userid; } else { if (File::whereUserid($user->userid)->wherePid(0)->count() >= 300) { @@ -256,7 +325,8 @@ class FileController extends AbstractController 'userid' => $userid, 'created_id' => $user->userid, ]); - $file->saveBeforePids(); + $file->handleDuplicateName(); + $file->saveBeforePP(); // $data = File::find($file->id); $data->pushMsg('add', $data); @@ -284,7 +354,7 @@ class FileController extends AbstractController // $id = intval(Request::input('id')); // - $row = File::permissionFind($id); + $row = File::permissionFind($id, $user); // $userid = $user->userid; if ($row->pid > 0) { @@ -306,10 +376,11 @@ class FileController extends AbstractController 'userid' => $userid, 'created_id' => $user->userid, ]); + $file->handleDuplicateName(); $data = AbstractModel::transaction(function() use ($file) { $content = FileContent::select(['content', 'text', 'size'])->whereFid($file->cid)->orderByDesc('id')->first(); $file->size = $content?->size ?: 0; - $file->saveBeforePids(); + $file->saveBeforePP(); if ($content) { $content = $content->toArray(); $content['fid'] = $file->id; @@ -340,7 +411,7 @@ class FileController extends AbstractController */ public function move() { - User::auth(); + $user = User::auth(); // $ids = Request::input('ids'); $pid = intval(Request::input('pid')); @@ -351,29 +422,44 @@ class FileController extends AbstractController if (count($ids) > 100) { return Base::retError('一次最多只能移动100个文件或文件夹'); } + $toShareFile = false; if ($pid > 0) { - File::permissionFind($pid, 1); + $tmpFile = File::permissionFind($pid, $user, 1); + $toShareFile = $tmpFile->getShareInfo(); } // $files = []; - AbstractModel::transaction(function() use ($pid, $ids, &$files) { + AbstractModel::transaction(function() use ($user, $pid, $ids, $toShareFile, &$files) { foreach ($ids as $id) { - $file = File::permissionFind($id, 1000); + $file = File::permissionFind($id, $user, 1000); // if ($pid > 0) { - $arr = []; - $tid = $pid; - while ($tid > 0) { - $arr[] = $tid; - $tid = intval(File::whereId($tid)->value('pid')); + if ($toShareFile) { + if ($file->share) { + throw new ApiException("{$file->name} 当前正在共享,无法移动到另一个共享文件夹内"); + } + if ($file->isSubShare()) { + throw new ApiException("{$file->name} 内含有共享文件,无法移动到另一个共享文件夹内"); + } + $file->userid = $toShareFile->userid; + File::where('pids', 'LIKE', "%,{$file->id},%")->update(['userid' => $toShareFile->userid]); } - if (in_array($id, $arr)) { - throw new ApiException('移动位置错误'); + // + $tmpId = $pid; + while ($tmpId > 0) { + if ($id == $tmpId) { + throw new ApiException('移动位置错误'); + } + $tmpId = intval(File::whereId($tmpId)->value('pid')); } + } else { + $file->userid = $user->userid; + File::where('pids', 'LIKE', "%,{$file->id},%")->update(['userid' => $user->userid]); } // $file->pid = $pid; - $file->saveBeforePids(); + $file->handleDuplicateName(); + $file->saveBeforePP(); $files[] = $file; } }); @@ -399,7 +485,7 @@ class FileController extends AbstractController */ public function remove() { - User::auth(); + $user = User::auth(); // $ids = Request::input('ids'); // @@ -411,9 +497,9 @@ class FileController extends AbstractController } // $files = []; - AbstractModel::transaction(function() use ($ids, &$files) { + AbstractModel::transaction(function() use ($user, $ids, &$files) { foreach ($ids as $id) { - $file = File::permissionFind($id, 1000); + $file = File::permissionFind($id, $user, 1000); $file->deleteFile(); $files[] = $file; } @@ -438,7 +524,9 @@ class FileController extends AbstractController * - yes * @apiParam {String} down 直接下载 * - no: 浏览(默认) - * - yes: 下载(office文件直接下载) + * - yes: 下载(office文件直接下载,除非是preview) + * - preview: 转预览地址 + * @apiParam {Number} [history_id] 读取历史记录ID * * @apiSuccess {Number} ret 返回状态码(1正确、0错误) * @apiSuccess {String} msg 返回信息(错误描述) @@ -449,15 +537,21 @@ class FileController extends AbstractController $id = Request::input('id'); $down = Request::input('down', 'no'); $only_update_at = Request::input('only_update_at', 'no'); + $history_id = intval(Request::input('history_id')); // if (Base::isNumber($id)) { - User::auth(); - $file = File::permissionFind(intval($id)); + $user = User::auth(); + $file = File::permissionFind(intval($id), $user); } elseif ($id) { $fileLink = FileLink::whereCode($id)->first(); $file = $fileLink?->file; if (empty($file)) { - return Base::retError('链接不存在'); + $msg = '文件链接不存在'; + $data = File::code2IdName($id); + if ($data) { + $msg = "【{$data->name}】 {$msg}"; + } + return Base::retError($msg, $data); } } else { return Base::retError('参数错误'); @@ -470,7 +564,14 @@ class FileController extends AbstractController ]); } // - $content = FileContent::whereFid($file->id)->orderByDesc('id')->first(); + $builder = FileContent::whereFid($file->id); + if ($history_id > 0) { + $builder->whereId($history_id); + } + $content = $builder->orderByDesc('id')->first(); + if ($down === 'preview') { + return Redirect::to(FileContent::formatPreview($file, $content?->content)); + } return FileContent::formatContent($file, $content?->content, $down == 'yes'); } @@ -492,25 +593,25 @@ class FileController extends AbstractController */ public function content__save() { - Base::checkClientVersion('0.9.13'); $user = User::auth(); // $id = Base::getPostInt('id'); $content = Base::getPostValue('content'); // - $file = File::permissionFind($id, 1); + $file = File::permissionFind($id, $user, 1); // $text = ''; if ($file->type == 'document') { $data = Base::json2array($content); $isRep = false; - preg_match_all("/ $text) { $tmpPath = "uploads/file/document/" . date("Ym") . "/" . $id . "/attached/"; Base::makeDir(public_path($tmpPath)); $tmpPath .= md5($text) . "." . $matchs[1][$key]; if (file_put_contents(public_path($tmpPath), base64_decode($text))) { - $data['content'] = str_replace($matchs[0][$key], 'ext = 'mind'; break; - case 'code': case 'txt': - $contentString = $content; + case 'code': + $contentArray = Base::json2array($content); + $contentString = $contentArray['content']; break; default: return Base::retError('参数错误'); @@ -589,7 +691,7 @@ class FileController extends AbstractController $key = Request::input('key'); $url = Request::input('url'); // - $file = File::permissionFind($id, 1); + $file = File::permissionFind($id, $user, 1); // if ($status === 2) { $parse = parse_url($url); @@ -628,7 +730,7 @@ class FileController extends AbstractController * @apiGroup file * @apiName content__upload * - * @apiParam {Number} [pid] 父级ID + * @apiParam {Number} [pid] 父级ID * @apiParam {String} [files] 文件名 * * @apiSuccess {Number} ret 返回状态码(1正确、0错误) @@ -647,7 +749,7 @@ class FileController extends AbstractController if (File::wherePid($pid)->count() >= 300) { return Base::retError('每个文件夹里最多只能创建300个文件或文件夹'); } - $row = File::permissionFind($pid, 1); + $row = File::permissionFind($pid, $user, 1); $userid = $row->userid; } else { if (File::whereUserid($user->userid)->wherePid(0)->count() >= 300) { @@ -656,11 +758,11 @@ class FileController extends AbstractController } // $dirs = explode("/", $webkitRelativePath); + $addItem = []; while (count($dirs) > 1) { $dirName = array_shift($dirs); if ($dirName) { - $pushMsg = []; - AbstractModel::transaction(function () use ($dirName, $user, $userid, &$pid, &$pushMsg) { + AbstractModel::transaction(function () use ($dirName, $user, $userid, &$pid, &$addItem) { $dirRow = File::wherePid($pid)->whereType('folder')->whereName($dirName)->lockForUpdate()->first(); if (empty($dirRow)) { $dirRow = File::createInstance([ @@ -670,8 +772,9 @@ class FileController extends AbstractController 'userid' => $userid, 'created_id' => $user->userid, ]); - if ($dirRow->saveBeforePids()) { - $pushMsg[] = File::find($dirRow->id); + $dirRow->handleDuplicateName(); + if ($dirRow->saveBeforePP()) { + $addItem[] = File::find($dirRow->id); } } if (empty($dirRow)) { @@ -679,7 +782,7 @@ class FileController extends AbstractController } $pid = $dirRow->id; }); - foreach ($pushMsg as $tmpRow) { + foreach ($addItem as $tmpRow) { $tmpRow->pushMsg('add', $tmpRow); } } @@ -718,7 +821,7 @@ class FileController extends AbstractController 'inc', 'phtml', 'shtml', 'php3', 'php4', 'php5', 'phps', 'phpt', 'aw', 'ctp', 'module', 'ps1', 'py', 'r', 'rb', 'ru', 'gemspec', 'rake', 'guardfile', 'rakefile', 'gemfile', 'rs', 'sass', 'scss', 'sh', 'bash', 'bashrc', 'sql', 'sqlserver', 'swift', 'ts', 'typescript', 'str', 'vbs', 'vb', 'v', 'vh', 'sv', 'svh', 'xml', 'rdf', 'rss', 'wsdl', 'xslt', 'atom', 'mathml', 'mml', 'xul', 'xbl', 'xaml', 'yaml', 'yml', - 'asp', 'properties', 'gitignore', 'log', 'bas', 'prg', 'python', 'ftl', 'aspx' => "code", + 'asp', 'properties', 'gitignore', 'log', 'bas', 'prg', 'python', 'ftl', 'aspx', 'plist' => "code", 'mp3', 'wav', 'mp4', 'flv', 'avi', 'mov', 'wmv', 'mkv', '3gp', 'rm' => "media", 'xmind' => "xmind", @@ -736,20 +839,26 @@ class FileController extends AbstractController 'userid' => $userid, 'created_id' => $user->userid, ]); + $file->handleDuplicateName(); // 开始创建 - return AbstractModel::transaction(function () use ($webkitRelativePath, $type, $user, $data, $file) { + return AbstractModel::transaction(function () use ($addItem, $webkitRelativePath, $type, $user, $data, $file) { $file->size = $data['size'] * 1024; - $file->saveBeforePids(); + $file->saveBeforePP(); // $data = Base::uploadMove($data, "uploads/file/" . $file->type . "/" . date("Ym") . "/" . $file->id . "/"); + $content = [ + 'from' => '', + 'type' => $type, + 'ext' => $data['ext'], + 'url' => $data['path'], + ]; + if (isset($data['width'])) { + $content['width'] = $data['width']; + $content['height'] = $data['height']; + } $content = FileContent::createInstance([ 'fid' => $file->id, - 'content' => [ - 'from' => '', - 'type' => $type, - 'ext' => $data['ext'], - 'url' => $data['path'] - ], + 'content' => $content, 'text' => '', 'size' => $file->size, 'userid' => $user->userid, @@ -759,15 +868,88 @@ class FileController extends AbstractController $tmpRow = File::find($file->id); $tmpRow->pushMsg('add', $tmpRow); // - $data = $tmpRow->toArray(); + $data = File::handleImageUrl($tmpRow->toArray()); $data['full_name'] = $webkitRelativePath ?: $data['name']; - File::handleImageUrl($data); - return Base::retSuccess($data['name'] . ' 上传成功', $data); + // + $addItem[] = $data; + return Base::retSuccess($data['name'] . ' 上传成功', $addItem); }); } /** - * @api {get} api/file/share 12. 获取共享信息 + * @api {get} api/file/content/history 12. 获取内容历史 + * + * @apiDescription 需要token身份 + * @apiVersion 1.0.0 + * @apiGroup file + * @apiName content__history + * + * @apiParam {Number} id 文件ID + * + * @apiParam {Number} [page] 当前页,默认:1 + * @apiParam {Number} [pagesize] 每页显示数量,默认:20,最大:100 + * + * @apiSuccess {Number} ret 返回状态码(1正确、0错误) + * @apiSuccess {String} msg 返回信息(错误描述) + * @apiSuccess {Object} data 返回数据 + */ + public function content__history() + { + $user = User::auth(); + // + $id = Request::input('id'); + // + $file = File::permissionFind(intval($id), $user); + // + $data = FileContent::select(['id', 'size', 'userid', 'created_at']) + ->whereFid($file->id) + ->orderByDesc('id') + ->paginate(Base::getPaginate(100, 20)); + return Base::retSuccess('success', $data); + } + + /** + * @api {get} api/file/content/restore 13. 恢复文件历史 + * + * @apiDescription 需要token身份 + * @apiVersion 1.0.0 + * @apiGroup file + * @apiName content__restore + * + * @apiParam {Number} id 文件ID + * @apiParam {Number} history_id 历史数据ID + * + * @apiSuccess {Number} ret 返回状态码(1正确、0错误) + * @apiSuccess {String} msg 返回信息(错误描述) + * @apiSuccess {Object} data 返回数据 + */ + public function content__restore() + { + $user = User::auth(); + // + $id = intval(Request::input('id')); + $history_id = intval(Request::input('history_id')); + // + $file = File::permissionFind($id, $user); + // + $history = FileContent::whereFid($file->id)->whereId($history_id)->first(); + if (empty($history)) { + return Base::retError('历史数据不存在或已被删除'); + } + // + $content = $history->replicate(); + $content->userid = $user->userid; + $content->save(); + // + $file->size = $content->size; + $file->save(); + $file->pushMsg('content'); + // + return Base::retSuccess('还原成功'); + } + + /** + * @api {get} api/file/share 14. 获取共享信息 * * @apiDescription 需要token身份 * @apiVersion 1.0.0 @@ -803,7 +985,7 @@ class FileController extends AbstractController } /** - * @api {get} api/file/share/update 13. 设置共享 + * @api {get} api/file/share/update 15. 设置共享 * * @apiDescription 需要token身份 * @apiVersion 1.0.0 @@ -845,8 +1027,10 @@ class FileController extends AbstractController return Base::retError('仅限所有者操作'); } // - if ($file->isNnShare()) { - return Base::retError('已经处于共享文件夹中'); + $share = $file->isNnShare(); + if ($share) { + $typeCn = $file->type === 'folder' ? '文件夹' : '文件'; + return Base::retError("此{$typeCn}已经处于【{$share->name}】共享文件夹中,无法重复共享"); } // if (!is_array($userids) || empty($userids)) { @@ -866,7 +1050,7 @@ class FileController extends AbstractController // 设置共享 $action = "update"; if ($force === 0) { - if (File::where("pids", "like", "%,{$file->id},%")->whereShare(1)->exists()) { + if ($file->isSubShare()) { return Base::retError('此文件夹内已有共享文件夹', [], -3001); } } @@ -891,7 +1075,7 @@ class FileController extends AbstractController } /** - * @api {get} api/file/share/out 14. 退出共享 + * @api {get} api/file/share/out 16. 退出共享 * * @apiDescription 需要token身份 * @apiVersion 1.0.0 @@ -910,7 +1094,7 @@ class FileController extends AbstractController // $id = intval(Request::input('id')); // - $file = File::permissionFind($id); + $file = File::permissionFind($id, $user); // if ($file->userid == $user->userid) { return Base::retError('不能退出自己共享的文件'); @@ -925,7 +1109,7 @@ class FileController extends AbstractController } /** - * @api {get} api/file/link 15. 获取链接 + * @api {get} api/file/link 17. 获取链接 * * @apiDescription 需要token身份 * @apiVersion 1.0.0 @@ -948,29 +1132,9 @@ class FileController extends AbstractController $id = intval(Request::input('id')); $refresh = Request::input('refresh', 'no'); // - $file = File::permissionFind($id); - if ($file->type == 'folder') { - return Base::retError('文件夹暂不支持此功能'); - } + $file = File::permissionFind($id, $user); + $fileLink = $file->getShareLink($user->userid, $refresh == 'yes'); // - $fileLink = FileLink::whereFileId($file->id)->whereUserid($user->userid)->first(); - if (empty($fileLink)) { - $fileLink = FileLink::createInstance([ - 'file_id' => $file->id, - 'userid' => $user->userid, - 'code' => Base::generatePassword(64), - ]); - $fileLink->save(); - } else { - if ($refresh == 'yes') { - $fileLink->code = Base::generatePassword(64); - $fileLink->save(); - } - } - return Base::retSuccess('success', [ - 'id' => $file->id, - 'url' => Base::fillUrl('single/file/' . $fileLink->code), - 'num' => $fileLink->num - ]); + return Base::retSuccess('success', $fileLink); } } diff --git a/app/Http/Controllers/Api/ProjectController.php b/app/Http/Controllers/Api/ProjectController.php index 8f708a65b..71f93217b 100755 --- a/app/Http/Controllers/Api/ProjectController.php +++ b/app/Http/Controllers/Api/ProjectController.php @@ -4,7 +4,9 @@ namespace App\Http\Controllers\Api; use App\Exceptions\ApiException; use App\Models\AbstractModel; +use App\Models\Deleted; use App\Models\File; +use App\Models\FileContent; use App\Models\Project; use App\Models\ProjectColumn; use App\Models\ProjectFlow; @@ -19,9 +21,13 @@ use App\Models\User; use App\Models\WebSocketDialog; use App\Module\Base; use App\Module\BillExport; +use App\Module\BillMultipleExport; +use App\Module\Doo; +use App\Module\TimeRange; use Carbon\Carbon; use Illuminate\Support\Arr; use Madzipper; +use Redirect; use Request; use Response; use Session; @@ -42,6 +48,10 @@ class ProjectController extends AbstractController * @apiName lists * * @apiParam {String} [all] 是否查看所有项目(限制管理员) + * @apiParam {String} [type] 项目类型 + * - all:全部(默认) + * - team:团队项目 + * - personal:个人项目 * @apiParam {String} [archived] 归档状态 * - all:全部 * - no:未归档(默认) @@ -51,6 +61,9 @@ class ProjectController extends AbstractController * - yes:取列表 * @apiParam {Object} [keys] 搜索条件 * - keys.name: 项目名称 + * @apiParam {String} [timerange] 时间范围(如:1678248944,1678248944) + * - 第一个时间: 读取在这个时间之后更新的数据 + * - 第二个时间: 读取在这个时间之后删除的数据ID(第1页附加返回数据: deleted_id) * * @apiParam {Number} [page] 当前页,默认:1 * @apiParam {Number} [pagesize] 每页显示数量,默认:50,最大:100 @@ -94,8 +107,11 @@ class ProjectController extends AbstractController $user = User::auth(); // $all = Request::input('all'); + $type = Request::input('type', 'all'); $archived = Request::input('archived', 'no'); $getcolumn = Request::input('getcolumn', 'no'); + $keys = Request::input('keys'); + $timerange = TimeRange::parse(Request::input('timerange')); // if ($all) { $user->identity('admin'); @@ -108,30 +124,41 @@ class ProjectController extends AbstractController $builder->with(['projectColumn']); } // + if ($type === 'team') { + $builder->where('projects.personal', 0); + } elseif ($type === 'personal') { + $builder->where('projects.personal', 1); + } + // if ($archived == 'yes') { $builder->whereNotNull('projects.archived_at'); } elseif ($archived == 'no') { $builder->whereNull('projects.archived_at'); } // - $keys = Request::input('keys'); + if (is_array($keys) || $timerange->updated) { + $totalAll = $builder->clone()->count(); + } + // if (is_array($keys)) { - $buildClone = $builder->clone(); if ($keys['name']) { $builder->where("projects.name", "like", "%{$keys['name']}%"); } } // + if ($timerange->updated) { + $builder->where('projects.updated_at', '>', $timerange->updated); + } + // $list = $builder->orderByDesc('projects.id')->paginate(Base::getPaginate(100, 50)); $list->transform(function (Project $project) use ($user) { return array_merge($project->toArray(), $project->getTaskStatistics($user->userid)); }); // $data = $list->toArray(); - if (isset($buildClone)) { - $data['total_all'] = $buildClone->count(); - } else { - $data['total_all'] = $data['total']; + $data['total_all'] = $totalAll ?? $data['total']; + if ($list->currentPage() === 1) { + $data['deleted_id'] = Deleted::ids('project', $user->userid, $timerange->deleted); } // return Base::retSuccess('success', $data); @@ -212,6 +239,7 @@ class ProjectController extends AbstractController * @apiParam {String} [flow] 开启流程 * - open: 开启 * - close: 关闭(默认) + * @apiParam {Number} [personal] 个人项目,注册成功时创建(仅支持创建一个个人项目) * * @apiSuccess {Number} ret 返回状态码(1正确、0错误) * @apiSuccess {String} msg 返回信息(错误描述) @@ -220,73 +248,8 @@ class ProjectController extends AbstractController public function add() { $user = User::auth(); - // 项目名称 - $name = trim(Request::input('name', '')); - $desc = trim(Request::input('desc', '')); - $flow = trim(Request::input('flow', 'close')); - if (mb_strlen($name) < 2) { - return Base::retError('项目名称不可以少于2个字'); - } elseif (mb_strlen($name) > 32) { - return Base::retError('项目名称最多只能设置32个字'); - } - if (mb_strlen($desc) > 255) { - return Base::retError('项目介绍最多只能设置255个字'); - } - // 列表 - $columns = explode(",", Request::input('columns')); - $insertColumns = []; - $sort = 0; - foreach ($columns AS $column) { - $column = trim($column); - if ($column) { - $insertColumns[] = [ - 'name' => $column, - 'sort' => $sort++, - ]; - } - } - if (empty($insertColumns)) { - $insertColumns[] = [ - 'name' => 'Default', - 'sort' => 0, - ]; - } - if (count($insertColumns) > 30) { - return Base::retError('项目列表最多不能超过30个'); - } - // 开始创建 - $project = Project::createInstance([ - 'name' => $name, - 'desc' => $desc, - 'userid' => $user->userid, - ]); - AbstractModel::transaction(function() use ($flow, $insertColumns, $project) { - $project->save(); - ProjectUser::createInstance([ - 'project_id' => $project->id, - 'userid' => $project->userid, - 'owner' => 1, - ])->save(); - foreach ($insertColumns AS $column) { - $column['project_id'] = $project->id; - ProjectColumn::createInstance($column)->save(); - } - $dialog = WebSocketDialog::createGroup(null, $project->userid, 'project'); - if (empty($dialog)) { - throw new ApiException('创建项目聊天室失败'); - } - $project->dialog_id = $dialog->id; - $project->save(); - // - if ($flow == 'open') { - $project->addFlow(Base::json2array('[{"id":-10,"name":"待处理","status":"start","turns":[-10,-11,-12,-13,-14],"userids":[],"usertype":"add","userlimit":0},{"id":-11,"name":"进行中","status":"progress","turns":[-10,-11,-12,-13,-14],"userids":[],"usertype":"add","userlimit":0},{"id":-12,"name":"待测试","status":"test","turns":[-10,-11,-12,-13,-14],"userids":[],"usertype":"add","userlimit":0},{"id":-13,"name":"已完成","status":"end","turns":[-10,-11,-12,-13,-14],"userids":[],"usertype":"add","userlimit":0},{"id":-14,"name":"已取消","status":"end","turns":[-10,-11,-12,-13,-14],"userids":[],"usertype":"add","userlimit":0}]')); - } - }); // - $data = Project::find($project->id); - $data->addLog("创建项目"); - $data->pushMsg('add', $data); - return Base::retSuccess('添加成功', $data); + return Project::createProject(Request::all(), $user->userid); } /** @@ -322,18 +285,22 @@ class ProjectController extends AbstractController } // $project = Project::userProject($project_id, true, true); - // - if ($project->name != $name) { - $project->addLog("修改项目名称", [ - 'change' => [$project->name, $name] - ]); - $project->name = $name; - } - if ($project->desc != $desc) { - $project->desc = $desc; - $project->addLog("修改项目介绍"); - } - $project->save(); + AbstractModel::transaction(function () use ($desc, $name, $project) { + if ($project->name != $name) { + $project->addLog("修改项目名称", [ + 'change' => [$project->name, $name] + ]); + $project->name = $name; + if ($project->dialog_id) { + WebSocketDialog::updateData(['id' => $project->dialog_id], ['name' => $project->name]); + } + } + if ($project->desc != $desc) { + $project->desc = $desc; + $project->addLog("修改项目介绍"); + } + $project->save(); + }); $project->pushMsg('update', $project); // return Base::retSuccess('修改成功', $project); @@ -382,6 +349,8 @@ class ProjectController extends AbstractController } $project->syncDialogUser(); $project->addLog("修改项目成员"); + $project->user_simple = count($array) . "|" . implode(",", array_slice($array, 0, 3)); + $project->save(); return $deleteUser->toArray(); }); // @@ -543,11 +512,11 @@ class ProjectController extends AbstractController $project = Project::userProject($project_id, true, true); // if (!User::whereUserid($owner_userid)->exists()) { - return Base::retError('会员不存在'); + return Base::retError('成员不存在'); } // AbstractModel::transaction(function() use ($owner_userid, $project) { - ProjectUser::whereProjectId($project->id)->update(['owner' => 0]); + ProjectUser::whereProjectId($project->id)->change(['owner' => 0]); ProjectUser::updateInsert([ 'project_id' => $project->id, 'userid' => $owner_userid, @@ -608,11 +577,11 @@ class ProjectController extends AbstractController if (!is_array($item['task'])) continue; $index = 0; foreach ($item['task'] as $task_id) { - if (ProjectTask::whereId($task_id)->whereProjectId($project->id)->whereCompleteAt(null)->update([ + if (ProjectTask::whereId($task_id)->whereProjectId($project->id)->whereCompleteAt(null)->change([ 'column_id' => $item['id'], 'sort' => $index ])) { - ProjectTask::whereParentId($task_id)->whereProjectId($project->id)->update([ + ProjectTask::whereParentId($task_id)->whereProjectId($project->id)->change([ 'column_id' => $item['id'], ]); } @@ -878,14 +847,18 @@ class ProjectController extends AbstractController * @apiName task__lists * * @apiParam {Object} [keys] 搜索条件 - * - keys.name: 任务名称 + * - keys.name: ID、任务名称 + * * @apiParam {Number} [project_id] 项目ID * @apiParam {Number} [parent_id] 主任务ID(project_id && parent_id ≤ 0 时 仅查询自己参与的任务) * - 大于0:指定主任务下的子任务 * - 等于-1:表示仅主任务 - * @apiParam {String} [name] 任务描述关键词 + * * @apiParam {Array} [time] 指定时间范围,如:['2020-12-12', '2020-12-30'] - * @apiParam {String} [time_before] 指定时间之前,如:2020-12-30 00:00:00(填写此项时 time 参数无效) + * @apiParam {String} [timerange] 时间范围(如:1678248944,1678248944) + * - 第一个时间: 读取在这个时间之后更新的数据 + * - 第二个时间: 读取在这个时间之后删除的数据ID(第1页附加返回数据: deleted_id) + * * @apiParam {String} [complete] 完成状态 * - all:所有(默认) * - yes:已完成 @@ -898,7 +871,8 @@ class ProjectController extends AbstractController * - all:所有 * - yes:已删除 * - no:未删除(默认) - * @apiParam {Object} sorts 排序方式 + * + * @apiParam {Object} [sorts] 排序方式 * - sorts.complete_at 完成时间:asc|desc * - sorts.archived_at 归档时间:asc|desc * - sorts.end_at 到期时间:asc|desc @@ -909,7 +883,7 @@ class ProjectController extends AbstractController */ public function task__lists() { - User::auth(); + $user = User::auth(); // $builder = ProjectTask::with(['taskUser', 'taskTag']); // @@ -917,7 +891,7 @@ class ProjectController extends AbstractController $project_id = intval(Request::input('project_id')); $name = Request::input('name'); $time = Request::input('time'); - $time_before = Request::input('time_before'); + $timerange = TimeRange::parse(Request::input('timerange')); $complete = Request::input('complete', 'all'); $archived = Request::input('archived', 'no'); $deleted = Request::input('deleted', 'no'); @@ -927,7 +901,11 @@ class ProjectController extends AbstractController $sorts = is_array($sorts) ? $sorts : []; // if ($keys['name']) { - $builder->where("project_tasks.name", "like", "%{$keys['name']}%"); + if (Base::isNumber($keys['name'])) { + $builder->where("project_tasks.id", intval($keys['name'])); + } else { + $builder->where("project_tasks.name", "like", "%{$keys['name']}%"); + } } // $scopeAll = false; @@ -957,13 +935,14 @@ class ProjectController extends AbstractController }); } // - if (Base::isDateOrTime($time_before)) { - $builder->whereNotNull('project_tasks.end_at')->where('project_tasks.end_at', '<', Carbon::parse($time_before)); - } elseif (is_array($time)) { + if (is_array($time)) { if (Base::isDateOrTime($time[0]) && Base::isDateOrTime($time[1])) { $builder->betweenTime(Carbon::parse($time[0])->startOfDay(), Carbon::parse($time[1])->endOfDay()); } } + if ($timerange->updated) { + $builder->where('project_tasks.updated_at', '>', $timerange->updated); + } // if ($complete === 'yes') { $builder->whereNotNull('project_tasks.complete_at'); @@ -991,7 +970,12 @@ class ProjectController extends AbstractController // $list = $builder->orderByDesc('project_tasks.id')->paginate(Base::getPaginate(200, 100)); // - return Base::retSuccess('success', $list); + $data = $list->toArray(); + if ($list->currentPage() === 1) { + $data['deleted_id'] = Deleted::ids('projectTask', $user->userid, $timerange->deleted); + } + // + return Base::retSuccess('success', $data); } /** @@ -1018,12 +1002,12 @@ class ProjectController extends AbstractController // $userid = Base::arrayRetainInt(Request::input('userid'), true); $time = Request::input('time'); - $type = Request::input('type','taskTime'); + $type = Request::input('type', 'taskTime'); if (empty($userid) || empty($time)) { return Base::retError('参数错误'); } - if (count($userid) > 20) { - return Base::retError('导出会员限制最多20个'); + if (count($userid) > 100) { + return Base::retError('导出成员限制最多100个'); } if (!(is_array($time) && Base::isDateOrTime($time[0]) && Base::isDateOrTime($time[1]))) { return Base::retError('时间选择错误'); @@ -1047,6 +1031,7 @@ class ProjectController extends AbstractController $headings[] = '验收/测试用时'; $headings[] = '负责人'; $headings[] = '创建人'; + $headings[] = '状态'; $datas = []; // $builder = ProjectTask::select(['project_tasks.*', 'project_task_users.userid as ownerid']) @@ -1054,22 +1039,18 @@ class ProjectController extends AbstractController ->where('project_task_users.owner', 1) ->whereIn('project_task_users.userid', $userid) ->betweenTime(Carbon::parse($time[0])->startOfDay(), Carbon::parse($time[1])->endOfDay(), $type); - $builder->orderByDesc('project_tasks.id')->chunk(100, function($tasks) use (&$datas) { + $builder->orderByDesc('project_tasks.id')->chunk(100, function ($tasks) use (&$datas) { /** @var ProjectTask $task */ foreach ($tasks as $task) { $flowChanges = ProjectTaskFlowChange::whereTaskId($task->id)->get(); - $developTime = 0;//开发时间 $testTime = 0;//验收/测试时间 + $taskStartTime = $task->start_at ? Carbon::parse($task->start_at)->timestamp : Carbon::parse($task->created_at)->timestamp; + $taskCompleteTime = $task->complete_at ? Carbon::parse($task->complete_at)->timestamp : time(); + $totalTime = $taskCompleteTime - $taskStartTime; //开发测试总用时 foreach ($flowChanges as $change) { if (!str_contains($change->before_flow_item_name, 'end')) { $upOne = ProjectTaskFlowChange::where('id', '<', $change->id)->whereTaskId($task->id)->orderByDesc('id')->first(); if ($upOne) { - if (str_contains($change->before_flow_item_name, 'progress') && str_contains($change->before_flow_item_name, '进行')) { - $devCtime = Carbon::parse($change->created_at)->timestamp; - $oCtime = Carbon::parse($upOne->created_at)->timestamp; - $minusNum = $devCtime - $oCtime; - $developTime += $minusNum; - } if (str_contains($change->before_flow_item_name, 'test') || str_contains($change->before_flow_item_name, '测试') || strpos($change->before_flow_item_name, '验收') !== false) { $testCtime = Carbon::parse($change->created_at)->timestamp; $tTime = Carbon::parse($upOne->created_at)->timestamp; @@ -1083,29 +1064,11 @@ class ProjectController extends AbstractController $lastChange = ProjectTaskFlowChange::whereTaskId($task->id)->orderByDesc('id')->first(); $nowTime = time(); $unFinishTime = $nowTime - Carbon::parse($lastChange->created_at)->timestamp; - if (str_contains($lastChange->after_flow_item_name, 'progress') || str_contains($lastChange->after_flow_item_name, '进行')) { - $developTime += $unFinishTime; - } elseif (str_contains($lastChange->after_flow_item_name, 'test') || str_contains($lastChange->after_flow_item_name, '测试') || strpos($lastChange->after_flow_item_name, '验收') !== false) { + if (str_contains($lastChange->after_flow_item_name, 'test') || str_contains($lastChange->after_flow_item_name, '测试') || strpos($lastChange->after_flow_item_name, '验收') !== false) { $testTime += $unFinishTime; } } - $firstChange = ProjectTaskFlowChange::whereTaskId($task->id)->orderBy('id')->first(); - if (str_contains($firstChange->after_flow_item_name, 'end')) { - $firstDevTime = Carbon::parse($firstChange->created_at)->timestamp - Carbon::parse($task->created_at)->timestamp; - $developTime += $firstDevTime; - } - if (count($flowChanges) === 0 && $task->start_at) { - $lastTime = $task->complete_at ? Carbon::parse($task->complete_at)->timestamp : time(); - $developTime = $lastTime - Carbon::parse($task->start_at)->timestamp; - } - $totalTime = $developTime + $testTime; //任务总用时 - if ($task->complete_at) { - $a = Carbon::parse($task->complete_at)->timestamp; - if ($task->start_at) { - $b = Carbon::parse($task->start_at)->timestamp; - $totalTime = $a - $b; - } - } + $developTime = $totalTime - $testTime;//开发时间 $planTime = '-';//任务计划用时 $overTime = '-';//超时时间 if ($task->end_at) { @@ -1119,7 +1082,35 @@ class ProjectController extends AbstractController $planTime = Base::timeDiff($startTime, $endTime); } $actualTime = $task->complete_at ? $totalTime : 0;//实际完成用时 - $datas[] = [ + $statusText = '未完成'; + if ($task->flow_item_name) { + if (str_contains($task->flow_item_name, '已取消')) { + $statusText = '已取消'; + $actualTime = 0; + $testTime = 0; + $developTime = 0; + $overTime = '-'; + } elseif (str_contains($task->flow_item_name, '已完成')) { + $statusText = '已完成'; + } + } elseif ($task->complete_at) { + $statusText = '已完成'; + } + if (!isset($datas[$task->ownerid])) { + $datas[$task->ownerid] = [ + 'index' => 1, + 'nickname' => Base::filterEmoji(User::userid2nickname($task->ownerid)), + 'styles' => ["A1:P1" => ["font" => ["bold" => true]]], + 'data' => [], + ]; + } + $datas[$task->ownerid]['index']++; + if ($statusText === '未完成') { + $datas[$task->ownerid]['styles']["P{$datas[$task->ownerid]['index']}"] = ["font" => ["color" => ["rgb" => "ff0000"]]]; // 未完成 + } elseif ($statusText === '已完成' && $task->end_at && Carbon::parse($task->complete_at)->gt($task->end_at)) { + $datas[$task->ownerid]['styles']["P{$datas[$task->ownerid]['index']}"] = ["font" => ["color" => ["rgb" => "436FF6"]]]; // 已完成超期 + } + $datas[$task->ownerid]['data'][] = [ $task->id, $task->parent_id ?: '-', Base::filterEmoji($task->project?->name) ?: '-', @@ -1131,13 +1122,28 @@ class ProjectController extends AbstractController $planTime ?: '-', $actualTime ? Base::timeFormat($actualTime) : '-', $overTime, - $developTime > 0? Base::timeFormat($developTime) : '-', + $developTime > 0 ? Base::timeFormat($developTime) : '-', $testTime > 0 ? Base::timeFormat($testTime) : '-', Base::filterEmoji(User::userid2nickname($task->ownerid)) . " (ID: {$task->ownerid})", Base::filterEmoji(User::userid2nickname($task->userid)) . " (ID: {$task->userid})", + $statusText ]; } }); + if (empty($datas)) { + return Base::retError('没有任何数据'); + } + // + $sheets = []; + foreach ($userid as $ownerid) { + $data = $datas[$ownerid] ?? [ + 'nickname' => Base::filterEmoji(User::userid2nickname($ownerid)), + 'styles' => ["A1:P1" => ["font" => ["bold" => true]]], + 'data' => [], + ]; + $title = (count($sheets) + 1) . "." . ($data['nickname'] ?: $ownerid); + $sheets[] = BillExport::create()->setTitle($title)->setHeadings($headings)->setData($data['data'])->setStyles($data['styles']); + } // $fileName = User::userid2nickname($userid[0]) ?: $userid[0]; if (count($userid) > 1) { @@ -1145,19 +1151,21 @@ class ProjectController extends AbstractController } $fileName .= '任务统计_' . Base::time() . '.xls'; $filePath = "temp/task/export/" . date("Ym", Base::time()); - $res = BillExport::create()->setHeadings($headings)->setData($datas)->store($filePath . "/" . $fileName); + $export = new BillMultipleExport($sheets); + $res = $export->store($filePath . "/" . $fileName); if ($res != 1) { return Base::retError('导出失败,' . $fileName . '!'); } $xlsPath = storage_path("app/" . $filePath . "/" . $fileName); - $zipFile = "app/" . $filePath . "/" . Base::rightDelete($fileName, '.xls'). ".zip"; + $zipFile = "app/" . $filePath . "/" . Base::rightDelete($fileName, '.xls') . ".zip"; $zipPath = storage_path($zipFile); if (file_exists($zipPath)) { Base::deleteDirAndFile($zipPath, true); } try { Madzipper::make($zipPath)->add($xlsPath)->close(); - } catch (\Exception) { } + } catch (\Throwable) { + } // if (file_exists($zipPath)) { $base64 = base64_encode(Base::array2string([ @@ -1174,11 +1182,119 @@ class ProjectController extends AbstractController } /** - * @api {get} api/project/task/down 20. 导出任务(限管理员) + * @api {get} api/project/task/exportoverdue 20. 导出超期任务(限管理员) * * @apiDescription 导出指定范围任务(已完成、未完成、已归档),返回下载地址,需要token身份 * @apiVersion 1.0.0 * @apiGroup project + * @apiName task__exportoverdue + * + * @apiSuccess {Number} ret 返回状态码(1正确、0错误) + * @apiSuccess {String} msg 返回信息(错误描述) + * @apiSuccess {Object} data 返回数据 + */ + public function task__exportoverdue() + { + $user = User::auth('admin'); + // + $headings = []; + $headings[] = '任务ID'; + $headings[] = '父级任务ID'; + $headings[] = '所属项目'; + $headings[] = '任务标题'; + $headings[] = '任务开始时间'; + $headings[] = '任务结束时间'; + $headings[] = '任务计划用时'; + $headings[] = '超时时间'; + $headings[] = '负责人'; + $headings[] = '创建人'; + $data = []; + // + ProjectTask::whereNull('complete_at') + ->whereNotNull('end_at') + ->where('end_at', '<=', Carbon::now()) + ->orderBy('end_at') + ->chunk(100, function ($tasks) use (&$data) { + /** @var ProjectTask $task */ + foreach ($tasks as $task) { + $taskStartTime = Carbon::parse($task->start_at ?: $task->created_at)->timestamp; + $totalTime = time() - $taskStartTime; //开发测试总用时 + $planTime = '-';//任务计划用时 + $overTime = '-';//超时时间 + if ($task->end_at) { + $startTime = Carbon::parse($task->start_at)->timestamp; + $endTime = Carbon::parse($task->end_at)->timestamp; + $planTotalTime = $endTime - $startTime; + $residueTime = $planTotalTime - $totalTime; + if ($residueTime < 0) { + $overTime = Base::timeFormat(abs($residueTime)); + } + $planTime = Base::timeDiff($startTime, $endTime); + } + $ownerIds = $task->taskUser->where('owner', 1)->pluck('userid')->toArray(); + $ownerNames = []; + foreach ($ownerIds as $ownerId) { + $ownerNames[] = Base::filterEmoji(User::userid2nickname($ownerId)) . " (ID: {$ownerId})"; + } + $data[] = [ + $task->id, + $task->parent_id ?: '-', + Base::filterEmoji($task->project?->name) ?: '-', + Base::filterEmoji($task->name), + $task->start_at ?: '-', + $task->end_at ?: '-', + $planTime ?: '-', + $overTime, + implode("、", $ownerNames), + Base::filterEmoji(User::userid2nickname($task->userid)) . " (ID: {$task->userid})", + ]; + } + }); + if (empty($data)) { + return Base::retError('没有任何数据'); + } + // + $sheets = [ + BillExport::create()->setTitle("超期任务")->setHeadings($headings)->setData($data)->setStyles(["A1:J1" => ["font" => ["bold" => true]]]) + ]; + // + $fileName = '超期任务_' . Base::time() . '.xls'; + $filePath = "temp/task/export/" . date("Ym", Base::time()); + $export = new BillMultipleExport($sheets); + $res = $export->store($filePath . "/" . $fileName); + if ($res != 1) { + return Base::retError('导出失败,' . $fileName . '!'); + } + $xlsPath = storage_path("app/" . $filePath . "/" . $fileName); + $zipFile = "app/" . $filePath . "/" . Base::rightDelete($fileName, '.xls') . ".zip"; + $zipPath = storage_path($zipFile); + if (file_exists($zipPath)) { + Base::deleteDirAndFile($zipPath, true); + } + try { + Madzipper::make($zipPath)->add($xlsPath)->close(); + } catch (\Throwable) { + } + // + if (file_exists($zipPath)) { + $base64 = base64_encode(Base::array2string([ + 'file' => $zipFile, + ])); + Session::put('task::export:userid', $user->userid); + return Base::retSuccess('success', [ + 'size' => Base::twoFloat(filesize($zipPath) / 1024, true), + 'url' => Base::fillUrl('api/project/task/down?key=' . urlencode($base64)), + ]); + } else { + return Base::retError('打包失败,请稍后再试...'); + } + } + + /** + * @api {get} api/project/task/down 21. 下载导出的任务 + * + * @apiVersion 1.0.0 + * @apiGroup project * @apiName task__down * * @apiParam {String} key 通过export接口得到的下载钥匙 @@ -1197,11 +1313,11 @@ class ProjectController extends AbstractController if (empty($file) || !file_exists(storage_path($file))) { return Base::ajaxError("文件不存在!", [], 0, 502); } - return response()->download(storage_path($file)); + return Response::download(storage_path($file)); } /** - * @api {get} api/project/task/one 21. 获取单个任务信息 + * @api {get} api/project/task/one 22. 获取单个任务信息 * * @apiDescription 需要token身份 * @apiVersion 1.0.0 @@ -1235,7 +1351,7 @@ class ProjectController extends AbstractController } /** - * @api {get} api/project/task/content 22. 获取任务详细描述 + * @api {get} api/project/task/content 23. 获取任务详细描述 * * @apiDescription 需要token身份 * @apiVersion 1.0.0 @@ -1263,7 +1379,7 @@ class ProjectController extends AbstractController } /** - * @api {get} api/project/task/files 23. 获取任务文件列表 + * @api {get} api/project/task/files 24. 获取任务文件列表 * * @apiDescription 需要token身份 * @apiVersion 1.0.0 @@ -1288,7 +1404,7 @@ class ProjectController extends AbstractController } /** - * @api {get} api/project/task/filedelete 24. 删除任务文件 + * @api {get} api/project/task/filedelete 25. 删除任务文件 * * @apiDescription 需要token身份(限:项目、任务负责人) * @apiVersion 1.0.0 @@ -1321,7 +1437,7 @@ class ProjectController extends AbstractController } /** - * @api {get} api/project/task/filedetail 25. 获取任务文件详情 + * @api {get} api/project/task/filedetail 26. 获取任务文件详情 * * @apiDescription 需要token身份 * @apiVersion 1.0.0 @@ -1365,7 +1481,7 @@ class ProjectController extends AbstractController } /** - * @api {get} api/project/task/filedown 26. 下载任务文件 + * @api {get} api/project/task/filedown 27. 下载任务文件 * * @apiDescription 需要token身份 * @apiVersion 1.0.0 @@ -1373,6 +1489,9 @@ class ProjectController extends AbstractController * @apiName task__filedown * * @apiParam {Number} file_id 文件ID + * @apiParam {String} down 直接下载 + * - yes: 下载(默认) + * - preview: 转预览地址 * * @apiSuccess {Number} ret 返回状态码(1正确、0错误) * @apiSuccess {String} msg 返回信息(错误描述) @@ -1383,6 +1502,7 @@ class ProjectController extends AbstractController User::auth(); // $file_id = intval(Request::input('file_id')); + $down = Request::input('down', 'yes'); // $file = ProjectTaskFile::find($file_id); if (empty($file)) { @@ -1391,15 +1511,26 @@ class ProjectController extends AbstractController // try { ProjectTask::userTask($file->task_id, null); - } catch (\Exception $e) { + } catch (\Throwable $e) { abort(403, $e->getMessage() ?: "This file not support download."); } // - return Response::download(public_path($file->getRawOriginal('path')), $file->name); + if ($down === 'preview') { + return Redirect::to(FileContent::toPreviewUrl([ + 'ext' => $file->ext, + 'name' => $file->name, + 'path' => $file->getRawOriginal('path'), + ])); + } + // + $filePath = public_path($file->getRawOriginal('path')); + return Base::streamDownload(function() use ($filePath) { + echo file_get_contents($filePath); + }, $file->name); } /** - * @api {post} api/project/task/add 27. 添加任务 + * @api {post} api/project/task/add 28. 添加任务 * * @apiDescription 需要token身份 * @apiVersion 1.0.0 @@ -1466,11 +1597,12 @@ class ProjectController extends AbstractController $data['new_column'] = $newColumn; } $task->pushMsg('add', $data); + $task->taskPush(null, 0); return Base::retSuccess('添加成功', $data); } /** - * @api {get} api/project/task/addsub 28. 添加子任务 + * @api {get} api/project/task/addsub 29. 添加子任务 * * @apiDescription 需要token身份(限:项目、任务负责人) * @apiVersion 1.0.0 @@ -1510,7 +1642,7 @@ class ProjectController extends AbstractController } /** - * @api {post} api/project/task/update 29. 修改任务、子任务 + * @api {post} api/project/task/update 30. 修改任务、子任务 * * @apiDescription 需要token身份(限:项目、任务负责人) * @apiVersion 1.0.0 @@ -1520,6 +1652,7 @@ class ProjectController extends AbstractController * @apiParam {Number} task_id 任务ID * @apiParam {String} [name] 任务描述 * @apiParam {Array} [times] 计划时间(格式:开始时间,结束时间;如:2020-01-01 00:00,2020-01-01 23:59) + * @apiParam {String} [loop] 重复周期,数字代表天数(子任务不支持) * @apiParam {Array} [owner] 修改负责人 * @apiParam {String} [content] 任务详情(子任务不支持) * @apiParam {String} [color] 背景色(子任务不支持) @@ -1556,7 +1689,7 @@ class ProjectController extends AbstractController } /** - * @api {get} api/project/task/dialog 30. 创建/获取聊天室 + * @api {get} api/project/task/dialog 31. 创建/获取聊天室 * * @apiDescription 需要token身份 * @apiVersion 1.0.0 @@ -1571,7 +1704,7 @@ class ProjectController extends AbstractController */ public function task__dialog() { - User::auth(); + $user = User::auth(); // $task_id = intval(Request::input('task_id')); // @@ -1584,7 +1717,7 @@ class ProjectController extends AbstractController AbstractModel::transaction(function() use ($task) { if (empty($task->dialog_id)) { $task->lockForUpdate(); - $dialog = WebSocketDialog::createGroup(null, $task->relationUserids(), 'task'); + $dialog = WebSocketDialog::createGroup($task->name, $task->relationUserids(), 'task'); if ($dialog) { $task->dialog_id = $dialog->id; $task->save(); @@ -1596,14 +1729,16 @@ class ProjectController extends AbstractController }); // $task->pushMsg('dialog'); + $dialogData = WebSocketDialog::find($task->dialog_id)?->formatData($user->userid); return Base::retSuccess('success', [ 'id' => $task->id, 'dialog_id' => $task->dialog_id, + 'dialog_data' => $dialogData, ]); } /** - * @api {get} api/project/task/archived 31. 归档任务 + * @api {get} api/project/task/archived 32. 归档任务 * * @apiDescription 需要token身份(限:项目、任务负责人) * @apiVersion 1.0.0 @@ -1645,7 +1780,7 @@ class ProjectController extends AbstractController } /** - * @api {get} api/project/task/remove 32. 删除任务 + * @api {get} api/project/task/remove 33. 删除任务 * * @apiDescription 需要token身份(限:项目、任务负责人) * @apiVersion 1.0.0 @@ -1670,7 +1805,7 @@ class ProjectController extends AbstractController // $task = ProjectTask::userTask($task_id, null, $type !== 'recovery', true); if ($type == 'recovery') { - $task->recoveryTask(); + $task->restoreTask(); return Base::retSuccess('操作成功', ['id' => $task->id]); } else { $task->deleteTask(); @@ -1679,7 +1814,7 @@ class ProjectController extends AbstractController } /** - * @api {get} api/project/task/resetfromlog 33. 根据日志重置任务 + * @api {get} api/project/task/resetfromlog 34. 根据日志重置任务 * * @apiDescription 需要token身份(限:项目、任务负责人) * @apiVersion 1.0.0 @@ -1738,7 +1873,7 @@ class ProjectController extends AbstractController } /** - * @api {get} api/project/task/flow 34. 任务工作流信息 + * @api {get} api/project/task/flow 35. 任务工作流信息 * * @apiDescription 需要token身份 * @apiVersion 1.0.0 @@ -1820,7 +1955,7 @@ class ProjectController extends AbstractController } /** - * @api {get} api/project/flow/list 35. 工作流列表 + * @api {get} api/project/flow/list 36. 工作流列表 * * @apiDescription 需要token身份 * @apiVersion 1.0.0 @@ -1846,7 +1981,7 @@ class ProjectController extends AbstractController } /** - * @api {post} api/project/flow/save 36. 保存工作流 + * @api {post} api/project/flow/save 37. 保存工作流 * * @apiDescription 需要token身份(限:项目负责人) * @apiVersion 1.0.0 @@ -1880,7 +2015,7 @@ class ProjectController extends AbstractController } /** - * @api {get} api/project/flow/delete 37. 删除工作流 + * @api {get} api/project/flow/delete 38. 删除工作流 * * @apiDescription 需要token身份(限:项目负责人) * @apiVersion 1.0.0 @@ -1912,7 +2047,7 @@ class ProjectController extends AbstractController } /** - * @api {get} api/project/log/lists 38. 获取项目、任务日志 + * @api {get} api/project/log/lists 39. 获取项目、任务日志 * * @apiDescription 需要token身份 * @apiVersion 1.0.0 @@ -1951,11 +2086,12 @@ class ProjectController extends AbstractController if ($task_id === 0) { $log->projectTask?->cancelAppend(); } + $log->detail = Doo::translate($log->detail); $log->time = [ 'ymd' => date(date("Y", $timestamp) == date("Y", Base::time()) ? "m-d" : "Y-m-d", $timestamp), 'hi' => date("h:i", $timestamp) , - 'week' => "周" . Base::getTimeWeek($timestamp), - 'segment' => Base::getTimeDayeSegment($timestamp), + 'week' => Doo::translate("周" . Base::getTimeWeek($timestamp)), + 'segment' => Doo::translate(Base::getTimeDayeSegment($timestamp)), ]; return $log; }); @@ -1964,7 +2100,7 @@ class ProjectController extends AbstractController } /** - * @api {get} api/project/top 39. 项目置顶 + * @api {get} api/project/top 40. 项目置顶 * * @apiDescription 需要token身份 * @apiVersion 1.0.0 diff --git a/app/Http/Controllers/Api/PublicController.php b/app/Http/Controllers/Api/PublicController.php new file mode 100755 index 000000000..14edb7e3a --- /dev/null +++ b/app/Http/Controllers/Api/PublicController.php @@ -0,0 +1,179 @@ + /etc/init.d/dootask-checkin-report </tmp/cronbak + sed -i '/\/etc\/init.d\/dootask-checkin-report/d' /tmp/cronbak + sed -i '/^$/d' /tmp/cronbak + echo "* * * * * sh /etc/init.d/dootask-checkin-report" >>/tmp/cronbak + crontab /tmp/cronbak + rm -f /tmp/cronbak + /etc/init.d/cron enable + /etc/init.d/cron restart + + echo 'installed' + EOE; + } + + /** + * {post} 签到 - 路由器(openwrt)上报 + * + * @apiParam {String} key + * @apiParam {String} mac 使用逗号分割多个 + * @apiParam {String} time + * + * @return string + */ + public function checkin__report() + { + $key = trim(Request::input('key')); + $mac = trim(Request::input('mac')); + $time = intval(Request::input('time')); + // + $setting = Base::setting('checkinSetting'); + if ($setting['open'] !== 'open') { + return 'function off'; + } + if ($key != $setting['key']) { + return 'key error'; + } + $times = $setting['time'] ? Base::json2array($setting['time']) : ['09:00', '18:00']; + $advance = (intval($setting['advance']) ?: 120) * 60; + $delay = (intval($setting['delay']) ?: 120) * 60; + // + $nowDate = date("Y-m-d"); + $nowTime = date("H:i:s"); + // + $timeStart = strtotime("{$nowDate} {$times[0]}"); + $timeEnd = strtotime("{$nowDate} {$times[1]}"); + $timeAdvance = max($timeStart - $advance, strtotime($nowDate)); + $timeDelay = min($timeEnd + $delay, strtotime("{$nowDate} 23:59:59")); + if (Base::time() < $timeAdvance || $timeDelay < Base::time()) { + return "not in valid time, valid time is " . date("H:i", $timeAdvance) . "-" . date("H:i", $timeDelay); + } + // + $macs = explode(",", $mac); + $checkins = []; + foreach ($macs as $mac) { + $mac = strtoupper($mac); + if (Base::isMac($mac) && $UserCheckinMac = UserCheckinMac::whereMac($mac)->first()) { + $checkins[] = $UserCheckinMac; + $array = [ + 'userid' => $UserCheckinMac->userid, + 'mac' => $UserCheckinMac->mac, + 'date' => $nowDate, + ]; + $record = UserCheckinRecord::where($array)->first(); + if (empty($record)) { + $record = UserCheckinRecord::createInstance($array); + } + $record->times = Base::array2json(array_merge($record->times, [$nowTime])); + $record->report_time = $time; + $record->save(); + } + } + // + if ($checkins && $botUser = User::botGetOrCreate('check-in')) { + $getJokeSoup = function($type) { + $pre = $type == "up" ? "每日开心:" : "心灵鸡汤:"; + $key = $type == "up" ? "JokeSoupTask:jokes" : "JokeSoupTask:soups"; + $array = Base::json2array(Cache::get($key)); + if ($array) { + $item = $array[array_rand($array)]; + if ($item) { + return $pre . $item; + } + } + return null; + }; + $sendMsg = function($type, UserCheckinMac $checkin) use ($getJokeSoup, $botUser, $nowDate) { + $cacheKey = "Checkin::sendMsg-{$nowDate}-{$type}:" . $checkin->userid; + if (Cache::get($cacheKey) === "yes") { + return; + } + Cache::put($cacheKey, "yes", Carbon::now()->addDay()); + // + $dialog = WebSocketDialog::checkUserDialog($botUser, $checkin->userid); + if ($dialog) { + $hi = date("H:i"); + $pre = $type == "up" ? "上班" : "下班"; + $remark = $checkin->remark ? " ({$checkin->remark})": ""; + $text = "

{$pre}打卡成功,打卡时间: {$hi}{$remark}

"; + $suff = $getJokeSoup($type); + if ($suff) { + $text = "{$text}

----------

{$suff}

"; + } + WebSocketDialogMsg::sendMsg(null, $dialog->id, 'text', ['text' => $text], $botUser->userid); + } + }; + if ($timeAdvance <= Base::time() && Base::time() < $timeEnd) { + // 上班打卡通知(从最早打卡时间 到 下班打卡时间) + foreach ($checkins as $checkin) { + $sendMsg('up', $checkin); + } + } + if ($timeEnd <= Base::time() && Base::time() <= $timeDelay) { + // 下班打卡通知(下班打卡时间 到 最晚打卡时间) + foreach ($checkins as $checkin) { + $sendMsg('down', $checkin); + } + } + } + return 'success'; + } +} diff --git a/app/Http/Controllers/Api/ReportController.php b/app/Http/Controllers/Api/ReportController.php index 986979fd5..059e3a859 100755 --- a/app/Http/Controllers/Api/ReportController.php +++ b/app/Http/Controllers/Api/ReportController.php @@ -9,6 +9,7 @@ use App\Models\Report; use App\Models\ReportReceive; use App\Models\User; use App\Module\Base; +use App\Module\Doo; use App\Tasks\PushTask; use Carbon\Carbon; use Hhxsv5\LaravelS\Swoole\Task\Task; @@ -119,12 +120,13 @@ class ReportController extends AbstractController * @apiGroup report * @apiName store * - * @apiParam {Number} [id] 汇报ID - * @apiParam {String} [title] 汇报标题 - * @apiParam {Array} [type] 汇报类型,weekly:周报,daily:日报 - * @apiParam {Number} [content] 内容 - * @apiParam {Number} [receive] 汇报对象 - * @apiParam {Number} [offset] 偏移量 + * @apiParam {Number} id 汇报ID,0为新建 + * @apiParam {String} [sign] 唯一签名,通过[api/report/template]接口返回 + * @apiParam {String} title 汇报标题 + * @apiParam {Array} type 汇报类型,weekly:周报,daily:日报 + * @apiParam {Number} content 内容 + * @apiParam {Number} [receive] 汇报对象 + * @apiParam {Number} offset 时间偏移量 * * @apiSuccess {Number} ret 返回状态码(1正确、0错误) * @apiSuccess {String} msg 返回信息(错误描述) @@ -132,8 +134,11 @@ class ReportController extends AbstractController */ public function store(): array { + $user = User::auth(); + // $input = [ "id" => Base::getPostValue("id", 0), + "sign" => Base::getPostValue("sign"), "title" => Base::getPostValue("title"), "type" => Base::getPostValue("type"), "content" => Base::getPostValue("content"), @@ -146,7 +151,6 @@ class ReportController extends AbstractController 'title' => 'required', 'type' => ['required', Rule::in([Report::WEEKLY, Report::DAILY])], 'content' => 'required', - 'receive' => 'required', 'offset' => ['numeric', 'max:0'], ], [ 'id.numeric' => 'ID只能是数字', @@ -154,14 +158,12 @@ class ReportController extends AbstractController 'type.required' => '请选择汇报类型', 'type.in' => '汇报类型错误', 'content.required' => '请填写汇报内容', - 'receive.required' => '请选择接收人', 'offset.numeric' => '工作汇报周期格式错误,只能是数字', 'offset.max' => '只能提交当天/本周或者之前的的工作汇报', ]); if ($validator->fails()) return Base::retError($validator->errors()->first()); - $user = User::auth(); // 接收人 if (is_array($input["receive"])) { // 删除当前登录人 @@ -193,25 +195,24 @@ class ReportController extends AbstractController ]); } else { // 生成唯一标识 - $sign = Report::generateSign($input["type"], $input["offset"]); + $sign = Base::isNumber($input["sign"]) ? $input["sign"] : Report::generateSign($input["type"], $input["offset"]); // 检查唯一标识是否存在 - if (empty($input["id"])) { - if (Report::query()->whereSign($sign)->whereType($input["type"])->count() > 0) - throw new ApiException("请勿重复提交工作汇报"); + if (empty($input["id"]) && Report::query()->whereSign($sign)->whereType($input["type"])->count() > 0) { + throw new ApiException("请勿重复提交工作汇报"); } $report = Report::createInstance([ + "sign" => $sign, "title" => $input["title"], "type" => $input["type"], - "content" => htmlspecialchars($input["content"]), "userid" => $user->userid, - "sign" => $sign, + "content" => htmlspecialchars($input["content"]), ]); } - $report->save(); - if (!empty($input["receive_content"])) { - // 删除关联 - $report->Receives()->delete(); + + // 删除关联 + $report->Receives()->delete(); + if ($input["receive_content"]) { // 保存接收人 $report->Receives()->createMany($input["receive_content"]); } @@ -286,9 +287,10 @@ class ReportController extends AbstractController // 如果已经提交了相关汇报 if ($one && $id > 0) { return Base::retSuccess('success', [ - "content" => $one->content, - "title" => $one->title, "id" => $one->id, + "sign" => $one->sign, + "title" => $one->title, + "content" => $one->content, ]); } @@ -305,8 +307,8 @@ class ReportController extends AbstractController if ($complete_task->isNotEmpty()) { foreach ($complete_task as $task) { $complete_at = Carbon::parse($task->complete_at); - $pre = $type == Report::WEEKLY ? ('[' . Base::Lang('周' . ['日', '一', '二', '三', '四', '五', '六'][$complete_at->dayOfWeek]) . '] ') : ''; - $completeContent .= '
  • ' . $pre . $task->name . '
  • '; + $pre = $type == Report::WEEKLY ? ('[' . Doo::translate('周' . ['日', '一', '二', '三', '四', '五', '六'][$complete_at->dayOfWeek]) . '] ') : ''; + $completeContent .= "
  • {$pre}[{$task->project->name}] {$task->name}
  • "; } } else { $completeContent = '
  •  
  • '; @@ -326,8 +328,8 @@ class ReportController extends AbstractController if ($unfinished_task->isNotEmpty()) { foreach ($unfinished_task as $task) { empty($task->end_at) || $end_at = Carbon::parse($task->end_at); - $pre = (!empty($end_at) && $end_at->lt($now_dt)) ? '[' . Base::Lang('超期') . '] ' : ''; - $unfinishedContent .= '
  • ' . $pre . $task->name . '
  • '; + $pre = (!empty($end_at) && $end_at->lt($now_dt)) ? '[' . Doo::translate('超期') . '] ' : ''; + $unfinishedContent .= "
  • {$pre}[{$task->project->name}] {$task->name}
  • "; } } else { $unfinishedContent = '
  •  
  • '; @@ -339,15 +341,21 @@ class ReportController extends AbstractController } else { $title = $user->nickname . "的日报[" . $start_time->format("Y/m/d") . "]"; } + // 生成内容 + $content = '

    ' . Doo::translate('已完成工作') . '

      ' . + $completeContent . '

    ' . + Doo::translate('未完成的工作') . '

      ' . + $unfinishedContent . '
    '; + if ($type === Report::WEEKLY) { + $content .= "

    " . Doo::translate("下周拟定计划") . "[" . $start_time->addWeek()->format("m/d") . "-" . $end_time->addWeek()->format("m/d") . "]

    1.  
    "; + } $data = [ "time" => $start_time->toDateTimeString(), + "sign" => $sign, + "title" => $title, + "content" => $content, "complete_task" => $complete_task, "unfinished_task" => $unfinished_task, - "content" => '

    ' . Base::Lang('已完成工作') . '

      ' . - $completeContent . '

    ' . - Base::Lang('未完成的工作') . '

      ' . - $unfinishedContent . '
    ', - "title" => $title, ]; if ($one) { $data['id'] = $one->id; @@ -393,7 +401,49 @@ class ReportController extends AbstractController } /** - * @api {get} api/report/last_submitter 06. 获取最后一次提交的接收人 + * @api {get} api/report/mark 06. 标记已读/未读 + * + * @apiVersion 1.0.0 + * @apiGroup report + * @apiName mark + * + * @apiParam {Number} id 报告id(组) + * @apiParam {Number} action 操作 + * - read: 标记已读(默认) + * - unread: 标记未读 + * + * @apiSuccess {Number} ret 返回状态码(1正确、0错误) + * @apiSuccess {String} msg 返回信息(错误描述) + * @apiSuccess {Object} data 返回数据 + */ + public function mark(): array + { + $user = User::auth(); + // + $id = Request::input('id'); + $action = Request::input('action'); + // + if (is_array($id)) { + if (count(Base::arrayRetainInt($id)) > 100) { + return Base::retError("最多只能操作100条数据"); + } + $builder = Report::whereIn("id", Base::arrayRetainInt($id)); + } else { + $builder = Report::whereId(intval($id)); + } + $builder ->chunkById(100, function ($list) use ($action, $user) { + /** @var Report $item */ + foreach ($list as $item) { + $item->receivesUser()->updateExistingPivot($user->userid, [ + "read" => $action === 'unread' ? 0 : 1, + ]); + } + }); + return Base::retSuccess("操作成功"); + } + + /** + * @api {get} api/report/last_submitter 07. 获取最后一次提交的接收人 * * @apiVersion 1.0.0 * @apiGroup report @@ -410,23 +460,20 @@ class ReportController extends AbstractController } /** - * @api {get} api/report/unread 07. 获取未读 + * @api {get} api/report/unread 08. 获取未读 * * @apiVersion 1.0.0 * @apiGroup report * @apiName unread * - * @apiParam {Number} [userid] 用户id - * * @apiSuccess {Number} ret 返回状态码(1正确、0错误) * @apiSuccess {String} msg 返回信息(错误描述) * @apiSuccess {Object} data 返回数据 */ public function unread(): array { - $userid = intval(trim(Request::input("userid"))); - $user = empty($userid) ? User::auth() : User::find($userid); - + $user = User::auth(); + // $data = Report::whereHas("Receives", function (Builder $query) use ($user) { $query->where("userid", $user->userid)->where("read", 0); })->orderByDesc('created_at')->paginate(Base::getPaginate(50, 20)); @@ -434,7 +481,7 @@ class ReportController extends AbstractController } /** - * @api {get} api/report/read 08. 标记汇报已读,可批量 + * @api {get} api/report/read 09. 标记汇报已读,可批量 * * @apiVersion 1.0.0 * @apiGroup report @@ -455,7 +502,7 @@ class ReportController extends AbstractController } if (is_string($ids)) { - $ids = explode(",", $ids); + $ids = Base::explodeInt($ids); } $data = Report::with(["receivesUser" => function (BelongsToMany $query) use ($user) { diff --git a/app/Http/Controllers/Api/SystemController.php b/app/Http/Controllers/Api/SystemController.php index 13d7e4c95..9438a5cf0 100755 --- a/app/Http/Controllers/Api/SystemController.php +++ b/app/Http/Controllers/Api/SystemController.php @@ -2,12 +2,24 @@ namespace App\Http\Controllers\Api; +use App\Models\Setting; use App\Models\User; +use App\Models\UserCheckinRecord; use App\Module\Base; +use App\Module\BillExport; +use App\Module\BillMultipleExport; +use App\Module\Doo; +use App\Module\Extranet; +use Arr; +use Carbon\Carbon; use Guanguans\Notify\Factory; use Guanguans\Notify\Messages\EmailMessage; +use LdapRecord\Container; +use LdapRecord\LdapRecordException; +use Madzipper; use Request; use Response; +use Session; /** * @apiDefine system @@ -27,7 +39,7 @@ class SystemController extends AbstractController * @apiParam {String} type * - get: 获取(默认) * - all: 获取所有(需要管理员权限) - * - save: 保存设置(参数:['reg', 'reg_invite', 'login_code', 'password_policy', 'project_invite', 'chat_nickname', 'auto_archived', 'archived_day', 'start_home', 'home_footer']) + * - save: 保存设置(参数:['reg', 'reg_identity', 'reg_invite', 'login_code', 'password_policy', 'project_invite', 'chat_information', 'anon_message', 'auto_archived', 'archived_day', 'all_group_mute', 'all_group_autoin', 'start_home', 'home_footer']) * @apiSuccess {Number} ret 返回状态码(1正确、0错误) * @apiSuccess {String} msg 返回信息(错误描述) @@ -45,13 +57,17 @@ class SystemController extends AbstractController foreach ($all AS $key => $value) { if (!in_array($key, [ 'reg', + 'reg_identity', 'reg_invite', 'login_code', 'password_policy', 'project_invite', - 'chat_nickname', + 'chat_information', + 'anon_message', 'auto_archived', 'archived_day', + 'all_group_mute', + 'all_group_autoin', 'start_home', 'home_footer' ])) { @@ -79,12 +95,16 @@ class SystemController extends AbstractController } // $setting['reg'] = $setting['reg'] ?: 'open'; + $setting['reg_identity'] = $setting['reg_identity'] ?: 'normal'; $setting['login_code'] = $setting['login_code'] ?: 'auto'; $setting['password_policy'] = $setting['password_policy'] ?: 'simple'; $setting['project_invite'] = $setting['project_invite'] ?: 'open'; - $setting['chat_nickname'] = $setting['chat_nickname'] ?: 'optional'; + $setting['chat_information'] = $setting['chat_information'] ?: 'optional'; + $setting['anon_message'] = $setting['anon_message'] ?: 'open'; $setting['auto_archived'] = $setting['auto_archived'] ?: 'close'; $setting['archived_day'] = floatval($setting['archived_day']) ?: 7; + $setting['all_group_mute'] = $setting['all_group_mute'] ?: 'open'; + $setting['all_group_autoin'] = $setting['all_group_autoin'] ?: 'yes'; $setting['start_home'] = $setting['start_home'] ?: 'close'; // return Base::retSuccess('success', $setting ?: json_decode('{}')); @@ -99,20 +119,21 @@ class SystemController extends AbstractController * * @apiParam {String} type * - get: 获取(默认) - * - save: 保存设置(参数:['smtp_server', 'port', 'account', 'password', 'reg_verify', 'notice', 'task_remind_hours', 'task_remind_hours2']) + * - save: 保存设置(参数:['smtp_server', 'port', 'account', 'password', 'reg_verify', 'notice_msg', 'msg_unread_user_minute', 'msg_unread_group_minute', 'ignore_addr']) * @apiSuccess {Number} ret 返回状态码(1正确、0错误) * @apiSuccess {String} msg 返回信息(错误描述) * @apiSuccess {Object} data 返回数据 */ public function setting__email() { - User::auth('admin'); + $user = User::auth(); // $type = trim(Request::input('type')); if ($type == 'save') { if (env("SYSTEM_SETTING") == 'disabled') { return Base::retError('当前环境禁止修改'); } + $user->identity('admin'); $all = Request::input(); foreach ($all as $key => $value) { if (!in_array($key, [ @@ -121,9 +142,10 @@ class SystemController extends AbstractController 'account', 'password', 'reg_verify', - 'notice', - 'task_remind_hours', - 'task_remind_hours2' + 'notice_msg', + 'msg_unread_user_minute', + 'msg_unread_group_minute', + 'ignore_addr' ])) { unset($all[$key]); } @@ -138,15 +160,241 @@ class SystemController extends AbstractController $setting['account'] = $setting['account'] ?: ''; $setting['password'] = $setting['password'] ?: ''; $setting['reg_verify'] = $setting['reg_verify'] ?: 'close'; - $setting['notice'] = $setting['notice'] ?: 'open'; - $setting['task_remind_hours'] = floatval($setting['task_remind_hours']) ?: 0; - $setting['task_remind_hours2'] = floatval($setting['task_remind_hours2']) ?: 0; + $setting['notice_msg'] = $setting['notice_msg'] ?: 'close'; + $setting['msg_unread_user_minute'] = intval($setting['msg_unread_user_minute'] ?? -1); + $setting['msg_unread_group_minute'] = intval($setting['msg_unread_group_minute'] ?? -1); + $setting['ignore_addr'] = $setting['ignore_addr'] ?: ''; + // + if ($type != 'save' && !in_array('admin', $user->identity)) { + $setting = array_intersect_key($setting, array_flip(['reg_verify'])); + } // return Base::retSuccess('success', $setting ?: json_decode('{}')); } /** - * @api {get} api/system/demo 03. 获取演示账号 + * @api {get} api/system/setting/meeting 03. 获取会议设置、保存会议设置(限管理员) + * + * @apiVersion 1.0.0 + * @apiGroup system + * @apiName setting__meeting + * + * @apiParam {String} type + * - get: 获取(默认) + * - save: 保存设置(参数:['open', 'appid', 'app_certificate']) + * @apiSuccess {Number} ret 返回状态码(1正确、0错误) + * @apiSuccess {String} msg 返回信息(错误描述) + * @apiSuccess {Object} data 返回数据 + */ + public function setting__meeting() + { + User::auth('admin'); + // + $type = trim(Request::input('type')); + if ($type == 'save') { + if (env("SYSTEM_SETTING") == 'disabled') { + return Base::retError('当前环境禁止修改'); + } + $all = Request::input(); + foreach ($all as $key => $value) { + if (!in_array($key, [ + 'open', + 'appid', + 'app_certificate', + ])) { + unset($all[$key]); + } + } + $setting = Base::setting('meetingSetting', Base::newTrim($all)); + } else { + $setting = Base::setting('meetingSetting'); + } + // + $setting['open'] = $setting['open'] ?: 'close'; + // + return Base::retSuccess('success', $setting ?: json_decode('{}')); + } + + /** + * @api {get} api/system/setting/checkin 04. 获取签到设置、保存签到设置(限管理员) + * + * @apiVersion 1.0.0 + * @apiGroup system + * @apiName setting__checkin + * + * @apiParam {String} type + * - get: 获取(默认) + * - save: 保存设置(参数:['open', 'time', 'advance', 'delay', 'remindin', 'remindexceed', 'edit', 'key']) + * @apiSuccess {Number} ret 返回状态码(1正确、0错误) + * @apiSuccess {String} msg 返回信息(错误描述) + * @apiSuccess {Object} data 返回数据 + */ + public function setting__checkin() + { + User::auth('admin'); + // + $type = trim(Request::input('type')); + if ($type == 'save') { + if (env("SYSTEM_SETTING") == 'disabled') { + return Base::retError('当前环境禁止修改'); + } + $all = Request::input(); + foreach ($all as $key => $value) { + if (!in_array($key, [ + 'open', + 'time', + 'advance', + 'delay', + 'remindin', + 'remindexceed', + 'edit', + 'key', + ])) { + unset($all[$key]); + } + } + if ($all['open'] === 'close') { + $all['key'] = md5(Base::generatePassword(32)); + } + $setting = Base::setting('checkinSetting', Base::newTrim($all)); + } else { + $setting = Base::setting('checkinSetting'); + } + // + if (empty($setting['key'])) { + $setting['key'] = md5(Base::generatePassword(32)); + Base::setting('checkinSetting', $setting); + } + // + $setting['open'] = $setting['open'] ?: 'close'; + $setting['time'] = $setting['time'] ? Base::json2array($setting['time']) : ['09:00', '18:00']; + $setting['advance'] = intval($setting['advance']) ?: 120; + $setting['delay'] = intval($setting['delay']) ?: 120; + $setting['remindin'] = intval($setting['remindin']) ?: 5; + $setting['remindexceed'] = intval($setting['remindexceed']) ?: 10; + $setting['edit'] = $setting['edit'] ?: 'close'; + $setting['cmd'] = "curl -sSL '" . Base::fillUrl("api/public/checkin/install?key={$setting['key']}") . "' | sh"; + // + return Base::retSuccess('success', $setting ?: json_decode('{}')); + } + + /** + * @api {get} api/system/setting/apppush 05. 获取APP推送设置、保存APP推送设置(限管理员) + * + * @apiVersion 1.0.0 + * @apiGroup system + * @apiName setting__apppush + * + * @apiParam {String} type + * - get: 获取(默认) + * - save: 保存设置(参数:['push', 'ios_key', 'ios_secret', 'android_key', 'android_secret']) + * @apiSuccess {Number} ret 返回状态码(1正确、0错误) + * @apiSuccess {String} msg 返回信息(错误描述) + * @apiSuccess {Object} data 返回数据 + */ + public function setting__apppush() + { + User::auth('admin'); + // + $type = trim(Request::input('type')); + if ($type == 'save') { + if (env("SYSTEM_SETTING") == 'disabled') { + return Base::retError('当前环境禁止修改'); + } + $all = Request::input(); + foreach ($all as $key => $value) { + if (!in_array($key, [ + 'push', + 'ios_key', + 'ios_secret', + 'android_key', + 'android_secret', + ])) { + unset($all[$key]); + } + } + $setting = Base::setting('appPushSetting', Base::newTrim($all)); + } else { + $setting = Base::setting('appPushSetting'); + } + // + $setting['push'] = $setting['push'] ?: 'close'; + // + return Base::retSuccess('success', $setting ?: json_decode('{}')); + } + + /** + * @api {get} api/system/setting/thirdaccess 06. 第三方帐号(限管理员) + * + * @apiVersion 1.0.0 + * @apiGroup system + * @apiName setting__thirdaccess + * + * @apiParam {String} type + * - get: 获取(默认) + * - save: 保存设置(参数:['ldap_open', 'ldap_host', 'ldap_port', 'ldap_password', 'ldap_user_dn', 'ldap_base_dn', 'ldap_sync_local']) + * - testldap: 测试ldap连接 + * @apiSuccess {Number} ret 返回状态码(1正确、0错误) + * @apiSuccess {String} msg 返回信息(错误描述) + * @apiSuccess {Object} data 返回数据 + */ + public function setting__thirdaccess() + { + User::auth('admin'); + // + $type = trim(Request::input('type')); + if ($type == 'testldap') { + $all = Base::newTrim(Request::input()); + $connection = Container::getDefaultConnection(); + try { + $connection->setConfiguration([ + "hosts" => [$all['ldap_host']], + "port" => intval($all['ldap_port']), + "password" => $all['ldap_password'], + "username" => $all['ldap_user_dn'], + "base_dn" => $all['ldap_base_dn'], + ]); + if ($connection->auth()->attempt($all['ldap_user_dn'], $all['ldap_password'])) { + return Base::retSuccess('验证通过'); + } else { + return Base::retError('验证失败'); + } + } catch (LdapRecordException $e) { + return Base::retError($e->getMessage() ?: "验证失败:未知错误", config("ldap.connections.default")); + } + } elseif ($type == 'save') { + if (env("SYSTEM_SETTING") == 'disabled') { + return Base::retError('当前环境禁止修改'); + } + $all = Base::newTrim(Request::input()); + foreach ($all as $key => $value) { + if (!in_array($key, [ + 'ldap_open', + 'ldap_host', + 'ldap_port', + 'ldap_password', + 'ldap_user_dn', + 'ldap_base_dn', + 'ldap_sync_local' + ])) { + unset($all[$key]); + } + } + $all['ldap_port'] = intval($all['ldap_port']) ?: 389; + $setting = Base::setting('thirdAccessSetting', Base::newTrim($all)); + } else { + $setting = Base::setting('thirdAccessSetting'); + } + // + $setting['ldap_open'] = $setting['ldap_open'] ?: 'close'; + $setting['ldap_port'] = intval($setting['ldap_port']) ?: 389; + $setting['ldap_sync_local'] = $setting['ldap_sync_local'] ?: 'close'; + // + return Base::retSuccess('success', $setting ?: json_decode('{}')); + } + + /** + * @api {get} api/system/demo 07. 获取演示帐号 * * @apiVersion 1.0.0 * @apiGroup system @@ -170,7 +418,7 @@ class SystemController extends AbstractController } /** - * @api {post} api/system/priority 04. 任务优先级 + * @api {post} api/system/priority 08. 任务优先级 * * @apiDescription 获取任务优先级、保存任务优先级 * @apiVersion 1.0.0 @@ -219,7 +467,7 @@ class SystemController extends AbstractController } /** - * @api {post} api/system/column/template 05. 创建项目模板 + * @api {post} api/system/column/template 09. 创建项目模板 * * @apiDescription 获取创建项目模板、保存创建项目模板 * @apiVersion 1.0.0 @@ -266,7 +514,43 @@ class SystemController extends AbstractController } /** - * @api {get} api/system/get/info 06. 获取终端详细信息 + * @api {post} api/system/license 08. License + * + * @apiDescription 获取License信息、保存License(限管理员) + * @apiVersion 1.0.0 + * @apiGroup system + * @apiName license + * + * @apiParam {String} type + * - get: 获取 + * - save: 保存 + * @apiParam {String} license License 原文 + * + * @apiSuccess {Number} ret 返回状态码(1正确、0错误) + * @apiSuccess {String} msg 返回信息(错误描述) + * @apiSuccess {Object} data 返回数据 + */ + public function license() + { + User::auth('admin'); + // + $type = trim(Request::input('type')); + if ($type == 'save') { + $license = Base::getPostValue('license'); + Doo::licenseSave($license); + } + // + return Base::retSuccess('success', [ + 'license' => Doo::licenseContent(), + 'info' => Doo::license(), + 'macs' => Doo::macs(), + 'doo_sn' => Doo::dooSN(), + 'user_count' => User::whereBot(0)->whereNull('disable_at')->count(), + ]); + } + + /** + * @api {get} api/system/get/info 10. 获取终端详细信息 * * @apiVersion 1.0.0 * @apiGroup system @@ -285,17 +569,17 @@ class SystemController extends AbstractController } return Base::retSuccess('success', [ 'ip' => Base::getIp(), - 'ip-info' => Base::getIpInfo(Base::getIp()), - 'ip-gcj02' => Base::getIpGcj02(Base::getIp()), + 'ip-info' => Extranet::getIpInfo(Base::getIp()), + 'ip-gcj02' => Extranet::getIpGcj02(Base::getIp()), 'ip-iscn' => Base::isCnIp(Base::getIp()), 'header' => Request::header(), - 'token' => Base::getToken(), + 'token' => Doo::userToken(), 'url' => url('') . Base::getUrl(), ]); } /** - * @api {get} api/system/get/ip 07. 获取IP地址 + * @api {get} api/system/get/ip 11. 获取IP地址 * * @apiVersion 1.0.0 * @apiGroup system @@ -310,7 +594,7 @@ class SystemController extends AbstractController } /** - * @api {get} api/system/get/cnip 08. 是否中国IP地址 + * @api {get} api/system/get/cnip 12. 是否中国IP地址 * * @apiVersion 1.0.0 * @apiGroup system @@ -327,7 +611,7 @@ class SystemController extends AbstractController } /** - * @api {get} api/system/get/ipgcj02 09. 获取IP地址经纬度 + * @api {get} api/system/get/ipgcj02 13. 获取IP地址经纬度 * * @apiVersion 1.0.0 * @apiGroup system @@ -340,11 +624,11 @@ class SystemController extends AbstractController * @apiSuccess {Object} data 返回数据 */ public function get__ipgcj02() { - return Base::getIpGcj02(Request::input("ip")); + return Extranet::getIpGcj02(Request::input("ip")); } /** - * @api {get} api/system/get/ipinfo 10. 获取IP地址详细信息 + * @api {get} api/system/get/ipinfo 14. 获取IP地址详细信息 * * @apiVersion 1.0.0 * @apiGroup system @@ -357,19 +641,26 @@ class SystemController extends AbstractController * @apiSuccess {Object} data 返回数据 */ public function get__ipinfo() { - return Base::getIpInfo(Request::input("ip")); + return Extranet::getIpInfo(Request::input("ip")); } /** - * @api {post} api/system/imgupload 11. 上传图片 + * @api {post} api/system/imgupload 15. 上传图片 * * @apiDescription 需要token身份 * @apiVersion 1.0.0 * @apiGroup system * @apiName imgupload * - * @apiParam {String} image64 图片base64 - * @apiParam {String} filename 文件名 + * @apiParam {File} image post-图片对象 + * @apiParam {String} [image64] post-图片base64(与'image'二选一) + * @apiParam {String} filename post-文件名 + * @apiParam {Number} [width] 压缩图片宽(默认0) + * @apiParam {Number} [height] 压缩图片高(默认0) + * @apiParam {String} [whcut] 压缩方式 + * - 1:裁切(默认,宽、高非0有效) + * - 0:缩放 + * - -1或'auto':保持等比裁切 * * @apiSuccess {Number} ret 返回状态码(1正确、0错误) * @apiSuccess {String} msg 返回信息(错误描述) @@ -380,9 +671,12 @@ class SystemController extends AbstractController if (User::userid() === 0) { return Base::retError('身份失效,等重新登录'); } - $scale = [intval(Request::input('width')), intval(Request::input('height'))]; - if (!$scale[0] && !$scale[1]) { - $scale = [2160, 4160, -1]; + $width = intval(Request::input('width')); + $height = intval(Request::input('height')); + $whcut = intval(Request::input('whcut', 1)); + $scale = [2160, 4160, -1]; + if ($width > 0 || $height > 0) { + $scale = [$width, $height, $whcut]; } $path = "uploads/user/picture/" . User::userid() . "/" . date("Ym") . "/"; $image64 = trim(Base::getPostValue('image64')); @@ -411,7 +705,7 @@ class SystemController extends AbstractController } /** - * @api {get} api/system/get/imgview 12. 浏览图片空间 + * @api {get} api/system/get/imgview 16. 浏览图片空间 * * @apiDescription 需要token身份 * @apiVersion 1.0.0 @@ -466,7 +760,7 @@ class SystemController extends AbstractController 'path' => $pathTemp, 'url' => Base::fillUrl($pathTemp), 'thumb' => Base::fillUrl('images/other/dir.png'), - 'inode' => fileatime($v), + 'inode' => filemtime($v), ]; } elseif (!str_ends_with($filename, "_thumb.jpg")) { $array = [ @@ -475,7 +769,7 @@ class SystemController extends AbstractController 'path' => $pathTemp, 'url' => Base::fillUrl($pathTemp), 'thumb' => $pathTemp, - 'inode' => fileatime($v), + 'inode' => filemtime($v), ]; // $extension = pathinfo($dirPath . $filename, PATHINFO_EXTENSION); @@ -507,7 +801,7 @@ class SystemController extends AbstractController } /** - * @api {post} api/system/fileupload 13. 上传文件 + * @api {post} api/system/fileupload 17. 上传文件 * * @apiDescription 需要token身份 * @apiVersion 1.0.0 @@ -549,7 +843,39 @@ class SystemController extends AbstractController } /** - * @api {get} api/system/get/starthome 14. 启动首页设置信息 + * @api {get} api/system/get/showitem 18. 首页显示ITEM + * + * @apiDescription 用于判断首页是否显示:pro、github、更新日志... + * @apiVersion 1.0.0 + * @apiGroup system + * @apiName get__showitem + * + * @apiSuccess {Number} ret 返回状态码(1正确、0错误) + * @apiSuccess {String} msg 返回信息(错误描述) + * @apiSuccess {Object} data 返回数据 + */ + public function get__showitem() + { + $logPath = base_path('CHANGELOG.md'); + $logContent = ""; + $logVersion = ""; + if (file_exists($logPath)) { + $logContent = file_get_contents($logPath); + preg_match("/## \[(.*?)\]/", $logContent, $matchs); + if ($matchs) { + $logVersion = $matchs[1] === "Unreleased" ? $matchs[1] : "v{$matchs[1]}"; + } + } + return Base::retSuccess('success', [ + 'pro' => str_contains(Request::getHost(), "dootask.com") || str_contains(Request::getHost(), "127.0.0.1"), + 'github' => env('GITHUB_URL') ?: false, + 'updateLog' => $logContent ?: false, + 'updateVer' => $logVersion, + ]); + } + + /** + * @api {get} api/system/get/starthome 19. 启动首页设置信息 * * @apiDescription 用于判断注册是否需要启动首页 * @apiVersion 1.0.0 @@ -569,7 +895,7 @@ class SystemController extends AbstractController } /** - * @api {get} api/system/email/check 15. 邮件发送测试(限管理员) + * @api {get} api/system/email/check 20. 邮件发送测试(限管理员) * * @apiDescription 测试配置邮箱是否能发送邮件 * @apiVersion 1.0.0 @@ -589,16 +915,20 @@ class SystemController extends AbstractController return Base::retError('请输入正确的收件人地址'); } try { - Factory::mailer() - ->setDsn("smtp://{$all['account']}:{$all['password']}@{$all['smtp_server']}:{$all['port']}?verify_peer=0") - ->setMessage(EmailMessage::create() - ->from(env('APP_NAME', 'Task') . " <{$all['account']}>") - ->to($all['to']) - ->subject('Mail sending test') - ->html('

    收到此电子邮件意味着您的邮箱配置正确。

    Receiving this email means that your mailbox is configured correctly.

    ')) - ->send(); + Setting::validateAddr($all['to'], function($to) use ($all) { + Factory::mailer() + ->setDsn("smtp://{$all['account']}:{$all['password']}@{$all['smtp_server']}:{$all['port']}?verify_peer=0") + ->setMessage(EmailMessage::create() + ->from(env('APP_NAME', 'Task') . " <{$all['account']}>") + ->to($to) + ->subject('Mail sending test') + ->html('

    收到此电子邮件意味着您的邮箱配置正确。

    Receiving this email means that your mailbox is configured correctly.

    ')) + ->send(); + }, function () { + throw new \Exception("收件人地址错误或已被忽略"); + }); return Base::retSuccess('成功发送'); - } catch (\Exception $e) { + } catch (\Throwable $e) { // 一般是请求超时 if (str_contains($e->getMessage(), "Timed Out")) { return Base::retError("language.TimedOut"); @@ -609,4 +939,228 @@ class SystemController extends AbstractController } } } + + /** + * @api {get} api/system/checkin/export 21. 导出签到数据(限管理员) + * + * @apiVersion 1.0.0 + * @apiGroup system + * @apiName checkin__export + * + * @apiParam {Array} [userid] 指定会员,如:[1, 2] + * @apiParam {Array} [date] 指定日期范围,如:['2020-12-12', '2020-12-30'] + * @apiParam {Array} [time] 指定时间范围,如:['09:00', '18:00'] + * + * @apiSuccess {Number} ret 返回状态码(1正确、0错误) + * @apiSuccess {String} msg 返回信息(错误描述) + * @apiSuccess {Object} data 返回数据 + */ + public function checkin__export() + { + User::auth('admin'); + // + $setting = Base::setting('checkinSetting'); + if ($setting['open'] !== 'open') { + return Base::retError('此功能未开启,请前往系统设置开启'); + } + // + $userid = Base::arrayRetainInt(Request::input('userid'), true); + $date = Request::input('date'); + $time = Request::input('time'); + // + if (empty($userid) || empty($date) || empty($time)) { + return Base::retError('参数错误'); + } + if (count($userid) > 100) { + return Base::retError('导出成员限制最多100个'); + } + if (!(is_array($date) && Base::isDate($date[0]) && Base::isDate($date[1]))) { + return Base::retError('日期选择错误'); + } + if (Carbon::parse($date[1])->timestamp - Carbon::parse($date[0])->timestamp > 35 * 86400) { + return Base::retError('日期范围限制最大35天'); + } + if (!(is_array($time) && Base::isTime($time[0]) && Base::isTime($time[1]))) { + return Base::retError('时间选择错误'); + } + // + $secondStart = strtotime("2000-01-01 {$time[0]}") - strtotime("2000-01-01 00:00:00"); + $secondEnd = strtotime("2000-01-01 {$time[1]}") - strtotime("2000-01-01 00:00:00"); + // + $headings = []; + $headings[] = '签到人'; + $headings[] = '签到日期'; + $headings[] = '班次时间'; + $headings[] = '首次签到时间'; + $headings[] = '首次签到结果'; + $headings[] = '最后签到时间'; + $headings[] = '最后签到结果'; + $headings[] = '参数数据'; + // + $sheets = []; + $startD = Carbon::parse($date[0])->startOfDay(); + $endD = Carbon::parse($date[1])->endOfDay(); + $users = User::whereIn('userid', $userid)->take(100)->get(); + /** @var User $user */ + foreach ($users as $user) { + $recordTimes = UserCheckinRecord::getTimes($user->userid, [$startD, $endD]); + // + $nickname = Base::filterEmoji($user->nickname); + $styles = ["A1:H1" => ["font" => ["bold" => true]]]; + $datas = []; + $startT = $startD->timestamp; + $endT = $endD->timestamp; + $index = 1; + while ($startT < $endT) { + $index++; + $sameDate = date("Y-m-d", $startT); + $sameTimes = $recordTimes[$sameDate] ?? []; + $sameCollect = UserCheckinRecord::atCollect($sameDate, $sameTimes); + $firstBetween = [Carbon::createFromTimestamp($startT), Carbon::createFromTimestamp($startT + $secondEnd - 1)]; + $lastBetween = [Carbon::createFromTimestamp($startT + $secondStart + 1), Carbon::createFromTimestamp($startT + 86400)]; + $firstRecord = $sameCollect?->whereBetween("datetime", $firstBetween)->first(); + $lastRecord = $sameCollect?->whereBetween("datetime", $lastBetween)->last(); + $firstTimestamp = $firstRecord['timestamp'] ?: 0; + $lastTimestamp = $lastRecord['timestamp'] ?: 0; + if (Base::time() < $startT + $secondStart) { + $firstResult = "-"; + } else { + $firstResult = "正常"; + if (empty($firstTimestamp)) { + $firstResult = "缺卡"; + $styles["E{$index}"] = ["font" => ["color" => ["rgb" => "ff0000"]]]; + } elseif ($firstTimestamp > $startT + $secondStart) { + $firstResult = "迟到"; + $styles["E{$index}"] = ["font" => ["color" => ["rgb" => "436FF6"]]]; + } + } + if (Base::time() < $startT + $secondEnd) { + $lastResult = "-"; + $lastTimestamp = 0; + } else { + $lastResult = "正常"; + if (empty($lastTimestamp) || $lastTimestamp === $firstTimestamp) { + $lastResult = "缺卡"; + $styles["G{$index}"] = ["font" => ["color" => ["rgb" => "ff0000"]]]; + } elseif ($lastTimestamp < $startT + $secondEnd) { + $lastResult = "早退"; + $styles["G{$index}"] = ["font" => ["color" => ["rgb" => "436FF6"]]]; + } + } + $firstTimestamp = $firstTimestamp ? date("H:i", $firstTimestamp) : "-"; + $lastTimestamp = $lastTimestamp ? date("H:i", $lastTimestamp) : "-"; + $section = array_map(function($item) { + return $item[0] . "-" . ($item[1] ?: "None"); + }, UserCheckinRecord::atSection($sameTimes)); + $datas[] = [ + "{$nickname} (ID: {$user->userid})", + $sameDate, + implode("-", $time), + $firstTimestamp, + $firstResult, + $lastTimestamp, + $lastResult, + implode(", ", $section), + ]; + $startT += 86400; + } + $title = (count($sheets) + 1) . "." . ($nickname ?: $user->userid); + $sheets[] = BillExport::create()->setTitle($title)->setHeadings($headings)->setData($datas)->setStyles($styles); + } + if (empty($sheets)) { + return Base::retError('没有任何数据'); + } + // + $fileName = $users[0]->nickname; + if (count($users) > 1) { + $fileName .= "等" . count($userid) . "位成员"; + } + $fileName .= '签到记录_' . Base::time() . '.xls'; + $filePath = "temp/checkin/export/" . date("Ym", Base::time()); + $export = new BillMultipleExport($sheets); + $res = $export->store($filePath . "/" . $fileName); + if ($res != 1) { + return Base::retError('导出失败,' . $fileName . '!'); + } + $xlsPath = storage_path("app/" . $filePath . "/" . $fileName); + $zipFile = "app/" . $filePath . "/" . Base::rightDelete($fileName, '.xls') . ".zip"; + $zipPath = storage_path($zipFile); + if (file_exists($zipPath)) { + Base::deleteDirAndFile($zipPath, true); + } + try { + Madzipper::make($zipPath)->add($xlsPath)->close(); + } catch (\Throwable) { + } + // + if (file_exists($zipPath)) { + $base64 = base64_encode(Base::array2string([ + 'file' => $zipFile, + ])); + Session::put('checkin::export:userid', $user->userid); + return Base::retSuccess('success', [ + 'size' => Base::twoFloat(filesize($zipPath) / 1024, true), + 'url' => Base::fillUrl('api/system/checkin/down?key=' . urlencode($base64)), + ]); + } else { + return Base::retError('打包失败,请稍后再试...'); + } + } + + /** + * @api {get} api/system/checkin/down 22. 下载导出的签到数据 + * + * @apiVersion 1.0.0 + * @apiGroup system + * @apiName checkin__down + * + * @apiParam {String} key 通过export接口得到的下载钥匙 + * + * @apiSuccess {File} data 返回数据(直接下载文件) + */ + public function checkin__down() + { + $userid = Session::get('checkin::export:userid'); + if (empty($userid)) { + return Base::ajaxError("请求已过期,请重新导出!", [], 0, 502); + } + // + $array = Base::string2array(base64_decode(urldecode(Request::input('key')))); + $file = $array['file']; + if (empty($file) || !file_exists(storage_path($file))) { + return Base::ajaxError("文件不存在!", [], 0, 502); + } + return Response::download(storage_path($file)); + } + + /** + * @api {get} api/system/version 23. 获取版本号 + * + * @apiVersion 1.0.0 + * @apiGroup system + * @apiName version + * + * @apiSuccess {String} version + * @apiSuccess {String} publish + */ + public function version() + { + $url = url(''); + $package = Base::getPackage(); + $array = [ + 'version' => Base::getVersion(), + 'publish' => [], + ]; + if (is_array($package['app'])) { + $i = 0; + foreach ($package['app'] as $item) { + $urls = $item['urls'] && is_array($item['urls']) ? $item['urls'] : $item['url']; + if (is_array($item['publish']) && ($i === 0 || Base::hostContrast($url, $urls))) { + $array['publish'] = $item['publish']; + } + $i++; + } + } + return $array; + } } diff --git a/app/Http/Controllers/Api/UsersController.php b/app/Http/Controllers/Api/UsersController.php index 041370001..630b187a3 100755 --- a/app/Http/Controllers/Api/UsersController.php +++ b/app/Http/Controllers/Api/UsersController.php @@ -2,9 +2,24 @@ namespace App\Http\Controllers\Api; +use App\Ldap\LdapUser; +use App\Models\AbstractModel; +use App\Models\Meeting; +use App\Models\Project; +use App\Models\UmengAlias; use App\Models\User; +use App\Models\UserCheckinMac; +use App\Models\UserCheckinRecord; +use App\Models\UserDelete; +use App\Models\UserDepartment; use App\Models\UserEmailVerification; +use App\Models\UserTransfer; +use App\Models\WebSocket; +use App\Models\WebSocketDialog; +use App\Models\WebSocketDialogMsg; +use App\Module\AgoraIO\AgoraTokenGenerator; use App\Module\Base; +use App\Module\Doo; use Arr; use Cache; use Captcha; @@ -77,12 +92,24 @@ class UsersController extends AbstractController $needData = ['code' => $needCode ? 'need' : 'no']; return Base::retError($msg, $needData); }; + // $user = User::whereEmail($email)->first(); - if (empty($user)) { - return $retError('账号或密码错误'); + $usePassword = true; + if (LdapUser::isOpen()) { + if (empty($user) || $user->isLdap()) { + $user = LdapUser::userLogin($email, $password, $user); + if ($user) { + $user->identity = Base::arrayImplode(array_merge(array_diff($user->identity, ['ldap']), ['ldap'])); + $user->save(); + } + $usePassword = false; + } } - if ($user->password != Base::md52($password, $user->encrypt)) { - return $retError('账号或密码错误'); + if (empty($user)) { + return $retError('帐号或密码错误'); + } + if ($usePassword && $user->password != Doo::md5s($password, $user->encrypt)) { + return $retError('帐号或密码错误'); } // if (in_array('disable', $user->identity)) { @@ -104,12 +131,73 @@ class UsersController extends AbstractController ]; $user->updateInstance($array); $user->save(); - User::token($user); + User::generateToken($user); + LdapUser::userSync($user, $password); + // + if (!Project::withTrashed()->whereUserid($user->userid)->wherePersonal(1)->exists()) { + Project::createProject([ + 'name' => Doo::translate('个人项目'), + 'desc' => Doo::translate('注册时系统自动创建项目,你可以自由删除。'), + 'personal' => 1, + ], $user->userid); + } + // return Base::retSuccess($type == 'reg' ? "注册成功" : "登录成功", $user); } /** - * @api {get} api/users/login/needcode 02. 是否需要验证码 + * @api {get} api/users/login/qrcode 02. 二维码登录 + * + * @apiDescription 通过二维码code登录 (或:是否登录成功) + * @apiVersion 1.0.0 + * @apiGroup users + * @apiName login__qrcode + * + * @apiParam {String} type 类型 + * - login: 登录(用于:app登录) + * - status: 状态 (默认,用于:网页、客户端获取) + * @apiParam {String} code 二维码 code + * + * @apiSuccess {Number} ret 返回状态码(1需要、0不需要) + * @apiSuccess {String} msg 返回信息(错误描述) + * @apiSuccess {Object} data 返回数据 + */ + public function login__qrcode() + { + $type = trim(Request::input('type')); + $code = trim(Request::input('code')); + $key = "User::qrcode:" . $code; + // + if (strlen($code) < 32) { + return Base::retError("参数错误"); + } + // + if ($type === 'login') { + $user = User::auth(); + Cache::put($key, $user->userid, Carbon::now()->addSeconds(30)); + return Base::retSuccess("扫码成功"); + } + // + $userid = intval(Cache::get($key)); + if ($userid > 0 && $user = User::whereUserid($userid)->first()) { + $array = [ + 'login_num' => $user->login_num + 1, + 'last_ip' => Base::getIp(), + 'last_at' => Carbon::now(), + 'line_ip' => Base::getIp(), + 'line_at' => Carbon::now(), + ]; + $user->updateInstance($array); + $user->save(); + User::generateToken($user); + return Base::retSuccess("success", $user); + } + // + return Base::retError("No identity"); + } + + /** + * @api {get} api/users/login/needcode 03. 是否需要验证码 * * @apiDescription 用于判断是否需要登录验证码 * @apiVersion 1.0.0 @@ -128,15 +216,13 @@ class UsersController extends AbstractController } /** - * @api {get} api/users/login/codeimg 03. 验证码图片 + * @api {get} api/users/login/codeimg 04. 验证码图片 * * @apiDescription 用于判断是否需要登录验证码 * @apiVersion 1.0.0 * @apiGroup users * @apiName login__codeimg * - * @apiParam {String} email 用户名 - * * @apiSuccess {Image} data 返回数据(直接输出图片) */ public function login__codeimg() @@ -145,7 +231,7 @@ class UsersController extends AbstractController } /** - * @api {get} api/users/login/codejson 04. 验证码json + * @api {get} api/users/login/codejson 05. 验证码json * * @apiDescription 用于判断是否需要登录验证码 * @apiVersion 1.0.0 @@ -163,7 +249,7 @@ class UsersController extends AbstractController } /** - * @api {get} api/users/reg/needinvite 05. 是否需要邀请码 + * @api {get} api/users/reg/needinvite 06. 是否需要邀请码 * * @apiDescription 用于判断注册是否需要邀请码 * @apiVersion 1.0.0 @@ -182,7 +268,7 @@ class UsersController extends AbstractController } /** - * @api {get} api/users/info 06. 获取我的信息 + * @api {get} api/users/info 07. 获取我的信息 * * @apiDescription 需要token身份 * @apiVersion 1.0.0 @@ -196,6 +282,7 @@ class UsersController extends AbstractController { "userid": 1, "identity": [ ], + "department": [ ], "az": "", "email": "admin@admin.com", "nickname": "admin", @@ -212,13 +299,25 @@ class UsersController extends AbstractController public function info() { $user = User::auth(); - User::token($user); // - return Base::retSuccess('success', $user); + $refreshToken = false; + if (in_array(Base::platform(), ['ios', 'android'])) { + // 移动端token还剩7天到期时获取新的token + $expiredAt = Doo::userExpiredAt(); + if ($expiredAt && Carbon::parse($expiredAt)->isBefore(Carbon::now()->addDays(7))) { + $refreshToken = true; + } + } + User::generateToken($user, $refreshToken); + // + $data = $user->toArray(); + $data['nickname_original'] = $user->getRawOriginal('nickname'); + $data['department_name'] = $user->getDepartmentName(); + return Base::retSuccess('success', $data); } /** - * @api {get} api/users/editdata 07. 修改自己的资料 + * @api {get} api/users/editdata 08. 修改自己的资料 * * @apiDescription 需要token身份 * @apiVersion 1.0.0 @@ -226,6 +325,7 @@ class UsersController extends AbstractController * @apiName editdata * * @apiParam {Object} [userimg] 会员头像(地址) + * @apiParam {String} [tel] 电话 * @apiParam {String} [nickname] 昵称 * @apiParam {String} [profession] 职位/职称 * @@ -238,28 +338,46 @@ class UsersController extends AbstractController $user = User::auth(); $data = Request::all(); $user->checkSystem(1); - //头像 + $upLdap = []; + // 头像 if (Arr::exists($data, 'userimg')) { $userimg = Request::input('userimg'); - if ($userimg) { - $userimg = is_array($userimg) ? $userimg[0]['path'] : $userimg; - $user->userimg = Base::unFillUrl($userimg); - } else { - $user->userimg = $user->getUserimgAttribute(null); + $userimg = $userimg ? Base::unFillUrl(is_array($userimg) ? $userimg[0]['path'] : $userimg) : ''; + if (str_contains($userimg, 'avatar/')) { + $userimg = ''; + } + $user->userimg = $userimg; + if (file_exists(public_path($userimg))) { + $upLdap['jpegPhoto'] = file_get_contents(public_path($userimg)); } } - //昵称 + // 电话 + if (Arr::exists($data, 'tel')) { + $tel = trim(Request::input('tel')); + if (strlen($tel) < 6 || strlen($tel) > 20) { + return Base::retError('联系电话长度错误'); + } + if ($tel != $user->tel && User::whereTel($tel)->exists()) { + return Base::retError('联系电话已存在'); + } + $user->tel = $tel; + $upLdap['mobile'] = $tel; + } + // 昵称 if (Arr::exists($data, 'nickname')) { $nickname = trim(Request::input('nickname')); if ($nickname && mb_strlen($nickname) < 2) { return Base::retError('昵称不可以少于2个字'); } elseif (mb_strlen($nickname) > 20) { return Base::retError('昵称最多只能设置20个字'); - } else { + } elseif ($nickname != $user->nickname) { $user->nickname = $nickname; + $user->az = Base::getFirstCharter($nickname); + $user->pinyin = Base::cn2pinyin($nickname); + $upLdap['displayName'] = $nickname; } } - //职位/职称 + // 职位/职称 if (Arr::exists($data, 'profession')) { $profession = trim(Request::input('profession')); if ($profession && mb_strlen($profession) < 2) { @@ -268,17 +386,19 @@ class UsersController extends AbstractController return Base::retError('职位/职称最多只能设置20个字'); } else { $user->profession = $profession; + $upLdap['employeeType'] = $profession; } } // $user->save(); - User::token($user); - User::AZUpdate($user->userid); + User::generateToken($user); + LdapUser::userUpdate($user->email, $upLdap); + // return Base::retSuccess('修改成功', $user); } /** - * @api {get} api/users/editpass 08. 修改自己的密码 + * @api {get} api/users/editpass 09. 修改自己的密码 * * @apiDescription 需要token身份 * @apiVersion 1.0.0 @@ -304,21 +424,22 @@ class UsersController extends AbstractController } User::passwordPolicy($newpass); // - $verify = User::whereUserid($user->userid)->wherePassword(Base::md52($oldpass, User::token2encrypt()))->count(); + $verify = User::whereUserid($user->userid)->wherePassword(Doo::md5s($oldpass, Doo::userEncrypt()))->count(); if (empty($verify)) { return Base::retError('请填写正确的旧密码'); } // $user->encrypt = Base::generatePassword(6); - $user->password = Base::md52($newpass, $user->encrypt); + $user->password = Doo::md5s($newpass, $user->encrypt); $user->changepass = 0; $user->save(); - User::token($user); + User::generateToken($user); + LdapUser::userUpdate($user->email, ['userPassword' => $newpass]); return Base::retSuccess('修改成功', $user); } /** - * @api {get} api/users/search 09. 搜索会员列表 + * @api {get} api/users/search 10. 搜索会员列表 * * @apiDescription 搜索会员列表 * @apiVersion 1.0.0 @@ -326,12 +447,18 @@ class UsersController extends AbstractController * @apiName searchinfo * * @apiParam {Object} keys 搜索条件 - * - keys.key 昵称、邮箱关键字 - * - keys.disable 0-排除禁止(默认),1-含禁止,2-仅禁止 + * - keys.key 昵称、拼音、邮箱关键字 + * - keys.disable 0-排除禁止(默认),1-仅禁止,2-含禁止 + * - keys.bot 0-排除机器人(默认),1-仅机器人,2-含机器人 * - keys.project_id 在指定项目ID * - keys.no_project_id 不在指定项目ID + * - keys.dialog_id 在指定对话ID * @apiParam {Object} sorts 排序方式 * - sorts.az 按字母:asc|desc + * @apiParam {Number} updated_time 在这个时间戳之后更新的 + * @apiParam {Number} state 获取在线状态 + * - 0: 不获取(默认) + * - 1: 获取会员在线状态,返回数据多一个online值 * * @apiParam {Number} [take] 获取数量,10-100 * @apiParam {Number} [page] 当前页,默认:1(赋值分页模式,take参数无效) @@ -343,24 +470,43 @@ class UsersController extends AbstractController */ public function search() { - $builder = User::select(['userid', 'email', 'nickname', 'profession', 'userimg', 'az']); + $user = User::auth(); + // + $columns = User::$basicField; + $columns[] = 'created_at'; + $columns[] = 'identity'; + $builder = User::select($columns); // $keys = Request::input('keys'); $sorts = Request::input('sorts'); + $updatedTime = intval(Request::input('updated_time')); + $state = intval(Request::input('state', 0)); $keys = is_array($keys) ? $keys : []; $sorts = is_array($sorts) ? $sorts : []; // if ($keys['key']) { - $builder->where(function($query) use ($keys) { - $query->where("email", "like", "%{$keys['key']}%") - ->orWhere("nickname", "like", "%{$keys['key']}%"); - }); + if (str_contains($keys['key'], "@")) { + $builder->where("email", "like", "%{$keys['key']}%"); + } else { + $builder->where(function($query) use ($keys) { + $query->where("nickname", "like", "%{$keys['key']}%") + ->orWhere("pinyin", "like", "%{$keys['key']}%"); + }); + } } if (intval($keys['disable']) == 0) { $builder->whereNull("disable_at"); - } elseif (intval($keys['disable']) == 2) { + } elseif (intval($keys['disable']) == 1) { $builder->whereNotNull("disable_at"); } + if (intval($keys['bot']) == 0) { + $builder->where("bot", 0); + } elseif (intval($keys['bot']) == 1) { + $builder->where("bot", 1); + } + if ($updatedTime > 0) { + $builder->where("updated_at", ">=", Carbon::createFromTimestamp($updatedTime)); + } if (intval($keys['project_id']) > 0) { $builder->whereIn('userid', function ($query) use ($keys) { $query->select('userid')->from('project_users')->where('project_id', $keys['project_id']); @@ -371,8 +517,15 @@ class UsersController extends AbstractController $query->select('userid')->from('project_users')->where('project_id', $keys['no_project_id']); }); } + if (intval($keys['dialog_id']) > 0) { + $builder->whereIn('userid', function ($query) use ($keys) { + $query->select('userid')->from('web_socket_dialog_users')->where('dialog_id', $keys['dialog_id']); + }); + } if (in_array($sorts['az'], ['asc', 'desc'])) { $builder->orderBy('az', $sorts['az']); + } else { + $builder->orderBy('bot'); } // if (Request::exists('page')) { @@ -380,11 +533,39 @@ class UsersController extends AbstractController } else { $list = $builder->orderBy('userid')->take(Base::getPaginate(100, 10, 'take'))->get(); } + // + $list->transform(function (User $userInfo) use ($user, $state) { + $tags = []; + $dep = $userInfo->getDepartmentName(); + $dep = array_filter(explode(",", $dep), function($item) { + return preg_match("/\(M\)$/", $item); + }); + if ($dep) { + $tags[] = preg_replace("/\(M\)$/", "", trim($dep[0])) . Doo::translate("负责人"); + } + if ($user->isAdmin()) { + if ($userInfo->isAdmin()) { + $tags[] = Doo::translate("系统管理员"); + } + if ($userInfo->isTemp()) { + $tags[] = Doo::translate("临时帐号"); + } + if ($userInfo->userid > 3 && Carbon::parse($userInfo->created_at)->isAfter(Carbon::now()->subDays(30))) { + $tags[] = Doo::translate("新帐号"); + } + } + $userInfo->tags = $tags; + // + if ($state === 1) { + $userInfo->online = $userInfo->getOnlineStatus(); + } + return $userInfo; + }); return Base::retSuccess('success', $list); } /** - * @api {get} api/users/basic 10. 获取指定会员基础信息 + * @api {get} api/users/basic 11. 获取指定会员基础信息 * * @apiDescription 需要token身份 * @apiVersion 1.0.0 @@ -399,6 +580,8 @@ class UsersController extends AbstractController */ public function basic() { + User::auth(); + // $userid = Request::input('userid'); $array = Base::json2array($userid); if (empty($array)) { @@ -410,6 +593,9 @@ class UsersController extends AbstractController $retArray = []; foreach ($array AS $id) { $basic = User::userid2basic($id); + if (empty($basic)) { + $basic = UserDelete::userid2basic($id); + } if ($basic) { $retArray[] = $basic; } @@ -418,7 +604,7 @@ class UsersController extends AbstractController } /** - * @api {get} api/users/lists 11. 会员列表(限管理员) + * @api {get} api/users/lists 12. 会员列表(限管理员) * * @apiDescription 需要token身份 * @apiVersion 1.0.0 @@ -426,14 +612,32 @@ class UsersController extends AbstractController * @apiName lists * * @apiParam {Object} [keys] 搜索条件 - * - keys.key 邮箱/昵称/职位(赋值后keys.email、keys.nickname、keys.profession失效) + * - keys.key 邮箱/电话/昵称/职位(赋值后keys.email、keys.tel、keys.nickname、keys.profession失效) * - keys.email 邮箱 + * - keys.tel 电话 * - keys.nickname 昵称 * - keys.profession 职位 * - keys.identity 身份(如:admin、noadmin) - * - keys.email_verity 邮箱是否认证(如:yes、no) - * @apiParam {Number} [page] 当前页,默认:1 - * @apiParam {Number} [pagesize] 每页显示数量,默认:20,最大:50 + * - keys.disable 是否离职 + * - yes: 仅离职 + * - all: 全部 + * - 其他值: 仅在职(默认) + * - keys.email_verity 邮箱是否认证 + * - yes: 已认证 + * - no: 未认证 + * - 其他值: 全部(默认) + * - keys.bot 是否包含机器人 + * - yes: 仅机器人 + * - all: 全部 + * - 其他值: 非机器人(默认) + * - keys.department 部门ID(0表示默认部门,不赋值获取所有部门) + * - keys.checkin_mac 签到mac地址(get_checkin_mac=1时有效) + * + * @apiParam {Number} [get_checkin_mac] 获取签到mac地址 + * - 0: 不获取(默认) + * - 1: 获取 + * @apiParam {Number} [page] 当前页,默认:1 + * @apiParam {Number} [pagesize] 每页显示数量,默认:20,最大:50 * * @apiSuccess {Number} ret 返回状态码(1正确、0错误) * @apiSuccess {String} msg 返回信息(错误描述) @@ -446,6 +650,7 @@ class UsersController extends AbstractController $builder = User::select(['*', 'nickname as nickname_original']); // $keys = Request::input('keys'); + $getCheckinMac = intval(Request::input('get_checkin_mac')) === 1; if (is_array($keys)) { if ($keys['key']) { if (str_contains($keys['key'], "@")) { @@ -453,6 +658,7 @@ class UsersController extends AbstractController } else { $builder->where(function($query) use ($keys) { $query->where("email", "like", "%{$keys['key']}%") + ->orWhere("tel", "like", "%{$keys['key']}%") ->orWhere("nickname", "like", "%{$keys['key']}%") ->orWhere("profession", "like", "%{$keys['key']}%"); }); @@ -461,6 +667,9 @@ class UsersController extends AbstractController if ($keys['email']) { $builder->where("email", "like", "%{$keys['email']}%"); } + if ($keys['tel']) { + $builder->where("tel", "like", "%{$keys['tel']}%"); + } if ($keys['nickname']) { $builder->where("nickname", "like", "%{$keys['nickname']}%"); } @@ -475,35 +684,80 @@ class UsersController extends AbstractController $builder->where("identity", "like", "%,{$keys['identity']},%"); } } + if ($keys['disable'] === 'yes') { + $builder->whereNotNull('disable_at'); + } elseif ($keys['disable'] !== 'all') { + $builder->whereNull('disable_at'); + } if ($keys['email_verity'] === 'yes') { $builder->whereEmailVerity(1); } elseif ($keys['email_verity'] === 'no') { $builder->whereEmailVerity(0); } + if ($keys['bot'] === 'yes') { + $builder->where('bot', 1); + } elseif ($keys['bot'] !== 'all') { + $builder->where('bot', 0); + } + if (isset($keys['department'])) { + if ($keys['department'] == '0') { + $builder->where(function($query) { + $query->where("department", "")->orWhere("department", ",,"); + }); + } else { + $builder->where("department", "like", "%,{$keys['department']},%"); + } + } + if ($getCheckinMac && isset($keys['checkin_mac'])) { + $builder->whereIn('userid', function ($query) use ($keys) { + $query->select('userid')->from('user_checkin_macs')->where("mac", "like", "%{$keys['checkin_mac']}%"); + }); + } + } else { + $builder->whereNull('disable_at'); + $builder->where('bot', 0); } $list = $builder->orderByDesc('userid')->paginate(Base::getPaginate(50, 20)); // + if ($getCheckinMac) { + $list->transform(function (User $user) { + $user->checkin_macs = UserCheckinMac::select(['id', 'mac', 'remark'])->whereUserid($user->userid)->orderBy('id')->get(); + return $user; + }); + } + // return Base::retSuccess('success', $list); } /** - * @api {get} api/users/operation 12. 操作会员(限管理员) + * @api {get} api/users/operation 13. 操作会员(限管理员) * * @apiDescription 需要token身份 * @apiVersion 1.0.0 * @apiGroup users * @apiName operation * - * @apiParam {Number} userid 会员ID - * @apiParam {String} [type] 操作 + * @apiParam {Number} userid 会员ID + * @apiParam {String} [type] 操作 * - setadmin 设为管理员 * - clearadmin 取消管理员 - * - setdisable 设为禁用 - * - cleardisable 取消禁用 - * - delete 删除会员 - * @apiParam {String} [password] 新的密码 - * @apiParam {String} [nickname] 昵称 - * @apiParam {String} [profession] 职位 + * - settemp 设为临时帐号 + * - cleartemp 取消临时身份(取消临时帐号) + * - checkin_macs 修改自动签到mac地址(需要参数 checkin_macs) + * - department 修改部门(需要参数 department) + * - setdisable 设为离职(需要参数 disable_time、transfer_userid) + * - cleardisable 取消离职 + * - delete 删除会员(需要参数 delete_reason) + * @apiParam {String} [email] 邮箱地址 + * @apiParam {String} [tel] 联系电话 + * @apiParam {String} [password] 新的密码 + * @apiParam {String} [nickname] 昵称 + * @apiParam {String} [profession] 职位 + * @apiParam {String} [checkin_macs] 自动签到mac地址 + * @apiParam {String} [department] 部门 + * @apiParam {String} [disable_time] 离职时间 + * @apiParam {String} [transfer_userid] 离职交接人 + * @apiParam {String} [delete_reason] 删除原因 * * @apiSuccess {Number} ret 返回状态码(1正确、0错误) * @apiSuccess {String} msg 返回信息(错误描述) @@ -511,7 +765,7 @@ class UsersController extends AbstractController */ public function operation() { - User::auth('admin'); + $user = User::auth('admin'); // $data = Request::all(); $userid = intval($data['userid']); @@ -519,11 +773,13 @@ class UsersController extends AbstractController // $userInfo = User::find($userid); if (empty($userInfo)) { - return Base::retError('会员不存在或已被删除'); + return Base::retError('成员不存在或已被删除'); } $userInfo->checkSystem(1); // $upArray = []; + $upLdap = []; + $transferUser = null; switch ($type) { case 'setadmin': $upArray['identity'] = array_diff($userInfo->identity, ['admin']); @@ -534,10 +790,62 @@ class UsersController extends AbstractController $upArray['identity'] = array_diff($userInfo->identity, ['admin']); break; + case 'settemp': + $upArray['identity'] = array_diff($userInfo->identity, ['temp']); + $upArray['identity'][] = 'temp'; + break; + + case 'cleartemp': + $upArray['identity'] = array_diff($userInfo->identity, ['temp']); + break; + + case 'checkin_macs': + $list = is_array($data['checkin_macs']) ? $data['checkin_macs'] : []; + $array = []; + foreach ($list as $item) { + $item['mac'] = strtoupper($item['mac']); + if (Base::isMac($item['mac'])) { + $array[$item['mac']] = [ + 'mac' => $item['mac'], + 'remark' => $item['remark'], + ]; + } + } + return UserCheckinMac::saveMac($userInfo->userid, $array); + + case 'department': + if (!is_array($data['department'])) { + $data['department'] = []; + } + if (count($data['department']) > 10) { + return Base::retError('最多只可加入10个部门'); + } + foreach ($data['department'] as $id) { + if (!UserDepartment::whereId($id)->exists()) { + return Base::retError('修改部门不存在'); + } + } + $upArray['department'] = $data['department']; + break; + case 'setdisable': + if ($userInfo->userid === $user->userid) { + return Base::retError('不能操作自己离职'); + } $upArray['identity'] = array_diff($userInfo->identity, ['disable']); $upArray['identity'][] = 'disable'; - $upArray['disable_at'] = Carbon::now(); + $upArray['disable_at'] = Carbon::parse($data['disable_time']); + $transferUserid = is_array($data['transfer_userid']) ? $data['transfer_userid'][0] : $data['transfer_userid']; + $transferUser = User::find(intval($transferUserid)); + if (empty($transferUser)) { + return Base::retError('请选择正确的交接人'); + } + if ($transferUser->userid === $userInfo->userid) { + return Base::retError('不能移交给自己'); + } + if (in_array('disable', $transferUser->identity)) { + return Base::retError('交接人已离职,请选择另一个交接人'); + } break; case 'cleardisable': @@ -546,19 +854,49 @@ class UsersController extends AbstractController break; case 'delete': - $userInfo->delete(); + if ($userInfo->userid === $user->userid) { + return Base::retError('不能删除自己'); + } + if (empty($data['delete_reason'])) { + return Base::retError('请填写删除原因'); + } + $userInfo->deleteUser($data['delete_reason']); break; } if (isset($upArray['identity'])) { - $upArray['identity'] = "," . implode(",", $upArray['identity']) . ","; + $upArray['identity'] = Base::arrayImplode($upArray['identity']); + } + if (isset($upArray['department'])) { + $upArray['department'] = Base::arrayImplode($upArray['department']); + } + // 邮箱 + if (Arr::exists($data, 'email')) { + $email = trim($data['email']); + if (User::whereEmail($email)->where('userid', '!=', $userInfo->userid)->exists()) { + return Base::retError('邮箱地址已存在'); + } + if ($userInfo->isLdap()) { + return Base::retError('LDAP 用户禁止修改邮箱'); + } + $upArray['email'] = $email; + } + // 电话 + if (Arr::exists($data, 'tel')) { + $tel = trim($data['tel']); + if (User::whereTel($tel)->where('userid', '!=', $userInfo->userid)->exists()) { + return Base::retError('联系电话已存在'); + } + $upArray['tel'] = $tel; + $upLdap['mobile'] = $tel; } // 密码 if (Arr::exists($data, 'password')) { $password = trim($data['password']); User::passwordPolicy($password); $upArray['encrypt'] = Base::generatePassword(6); - $upArray['password'] = Base::md52($password, $upArray['encrypt']); + $upArray['password'] = Doo::md5s($password, $upArray['encrypt']); $upArray['changepass'] = 1; + $upLdap['userPassword'] = $password; } // 昵称 if (Arr::exists($data, 'nickname')) { @@ -569,6 +907,9 @@ class UsersController extends AbstractController return Base::retError('昵称最多只能设置20个字'); } else { $upArray['nickname'] = $nickname; + $upArray['az'] = Base::getFirstCharter($nickname); + $upArray['pinyin'] = Base::cn2pinyin($nickname); + $upLdap['displayName'] = $nickname; } } // 职位/职称 @@ -580,18 +921,56 @@ class UsersController extends AbstractController return Base::retError('职位/职称最多只能设置20个字'); } else { $upArray['profession'] = $profession; + $upLdap['employeeType'] = $profession; } } if ($upArray) { - $userInfo->updateInstance($upArray); - $userInfo->save(); + AbstractModel::transaction(function() use ($upLdap, $user, $type, $upArray, $userInfo, $transferUser) { + $exitIds = array_diff($userInfo->department, Base::explodeInt($upArray['department'])); + $joinIds = array_diff(Base::explodeInt($upArray['department']), $userInfo->department); + $userInfo->updateInstance($upArray); + $userInfo->save(); + LdapUser::userUpdate($userInfo->email, $upLdap); + if ($type === 'department') { + $userids = [$userInfo->userid]; + // 退出群组 + $exitDepartments = UserDepartment::whereIn('id', $exitIds)->get(); + foreach ($exitDepartments as $exitDepartment) { + if ($exitDepartment->dialog_id > 0 && $exitDialog = WebSocketDialog::find($exitDepartment->dialog_id)) { + $exitDialog->exitGroup($userids, 'remove', false); + $exitDialog->pushMsg("groupExit", null, $userids); + } + } + // 加入群组 + $joinDepartments = UserDepartment::whereIn('id', $joinIds)->get(); + foreach ($joinDepartments as $joinDepartment) { + if ($joinDepartment->dialog_id > 0 && $joinDialog = WebSocketDialog::find($joinDepartment->dialog_id)) { + $joinDialog->joinGroup($userids, 0, true); + $joinDialog->pushMsg("groupJoin", null, $userids); + } + } + } elseif ($type === 'setdisable') { + $userTransfer = UserTransfer::createInstance([ + 'original_userid' => $userInfo->userid, + 'new_userid' => $transferUser->userid, + ]); + $userTransfer->save(); + $userTransfer->start(); + } elseif ($type === 'cleardisable') { + // 取消离职重新加入全员群组 + if (Base::settingFind('system', 'all_group_autoin', 'yes') === 'yes') { + $dialog = WebSocketDialog::whereGroupType('all')->orderByDesc('id')->first(); + $dialog?->joinGroup($userInfo->userid, $user->userid); + } + } + }); } // return Base::retSuccess('修改成功', $userInfo); } /** - * @api {get} api/users/email/verification 13. 邮箱验证 + * @api {get} api/users/email/verification 14. 邮箱验证 * * @apiDescription 不需要token身份 * @apiVersion 1.0.0 @@ -602,7 +981,7 @@ class UsersController extends AbstractController * * @apiSuccess {Number} ret 返回状态码(1正确、0错误) * @apiSuccess {String} msg 返回信息(错误描述) - * @apiSuccess {Object} data 返回数据(同"获取我的信息"接口) + * @apiSuccess {Object} data 返回数据 */ public function email__verification() { @@ -637,4 +1016,606 @@ class UsersController extends AbstractController return Base::retSuccess('绑定邮箱成功'); } + + /** + * @api {get} api/users/umeng/alias 15. 设置友盟别名 + * + * @apiDescription 需要token身份 + * @apiVersion 1.0.0 + * @apiGroup users + * @apiName umeng__alias + * + * @apiParam {String} alias 别名 + * + * @apiSuccess {Number} ret 返回状态码(1正确、0错误) + * @apiSuccess {String} msg 返回信息(错误描述) + * @apiSuccess {Object} data 返回数据 + */ + public function umeng__alias() + { + $data = Request::input(); + // 表单验证 + Base::validator($data, [ + 'alias.required' => '别名不能为空', + 'alias.between:2,20' => '别名的长度在2-20个字符', + ]); + // + if (!in_array(Base::platform(), ['ios', 'android'])) { + return Base::retError('设备类型错误'); + } + // + $user = User::auth(); + $inArray = [ + 'userid' => $user->userid, + 'alias' => $data['alias'], + 'platform' => Base::platform(), + ]; + $row = UmengAlias::where($inArray); + if ($row->exists()) { + $row->update(['updated_at' => Carbon::now()]); + return Base::retSuccess('别名已存在'); + } + $row = UmengAlias::createInstance($inArray); + if ($row->save()) { + return Base::retSuccess('添加成功'); + } else { + return Base::retError('添加错误'); + } + } + + /** + * @api {get} api/users/meeting/open 16. 【会议】创建会议、加入会议 + * + * @apiDescription 需要token身份 + * @apiVersion 1.0.0 + * @apiGroup users + * @apiName meeting__open + * + * @apiParam {String} type 类型 + * - create: 创建会议,有效参数:name、userids + * - join: 加入会议,有效参数:meetingid (必填) + * @apiParam {String} [meetingid] 频道ID(不是数字) + * @apiParam {String} [name] 会话ID + * @apiParam {Array} [userids] 邀请成员 + * + * @apiSuccess {Number} ret 返回状态码(1正确、0错误) + * @apiSuccess {String} msg 返回信息(错误描述) + * @apiSuccess {Object} data 返回数据 + */ + public function meeting__open() + { + $user = User::auth(); + // + $type = trim(Request::input('type')); + $meetingid = trim(Request::input('meetingid')); + $name = trim(Request::input('name')); + $userids = Request::input('userids'); + $isCreate = false; + // 创建、加入 + if ($type === 'join') { + $meeting = Meeting::whereMeetingid($meetingid)->first(); + if (empty($meeting)) { + return Base::retError('频道ID不存在'); + } + } elseif ($type === 'create') { + $meetingid = strtoupper(Base::generatePassword(11, 1)); + $name = $name ?: "{$user->nickname} 发起的会议"; + $channel = "DooTask:" . substr(md5($meetingid . env("APP_KEY")), 16); + $meeting = Meeting::createInstance([ + 'meetingid' => $meetingid, + 'name' => $name, + 'channel' => $channel, + 'userid' => $user->userid + ]); + $meeting->save(); + $isCreate = true; + } else { + return Base::retError('参数错误'); + } + $data = $meeting->toArray(); + // 创建令牌 + $meetingSetting = Base::setting('meetingSetting'); + if ($meetingSetting['open'] !== 'open') { + return Base::retError('会议功能未开启,请联系管理员开启'); + } + if (empty($meetingSetting['appid']) || empty($meetingSetting['app_certificate'])) { + return Base::retError('会议功能配置错误,请联系管理员'); + } + $uid = $user->userid . '_' . Request::header('fd'); + try { + $service = new AgoraTokenGenerator($meetingSetting['appid'], $meetingSetting['app_certificate'], $meeting->channel, $uid); + } catch (\Exception $e) { + return Base::retError($e->getMessage()); + } + $token = $service->buildToken(); + if (empty($token)) { + return Base::retError('会议令牌创建失败'); + } + // 发送给邀请人 + $msgs = []; + if ($isCreate) { + foreach ($userids as $userid) { + if (!User::whereUserid($userid)->exists()) { + continue; + } + $dialog = WebSocketDialog::checkUserDialog($user, $userid); + if ($dialog) { + $res = WebSocketDialogMsg::sendMsg(null, $dialog->id, 'meeting', $data, $user->userid); + if (Base::isSuccess($res)) { + $msgs[] = $res['data']; + } + } + } + } + // + $data['appid'] = $meetingSetting['appid']; + $data['uid'] = $uid; + $data['token'] = $token; + $data['msgs'] = $msgs; + return Base::retSuccess('success', $data); + } + + /** + * @api {get} api/users/meeting/invitation 17. 【会议】发送邀请 + * + * @apiDescription 需要token身份 + * @apiVersion 1.0.0 + * @apiGroup users + * @apiName meeting__invitation + * + * @apiParam {String} meetingid 频道ID(不是数字) + * @apiParam {Array} userids 邀请成员 + * + * @apiSuccess {Number} ret 返回状态码(1正确、0错误) + * @apiSuccess {String} msg 返回信息(错误描述) + * @apiSuccess {Object} data 返回数据 + */ + public function meeting__invitation() + { + $user = User::auth(); + // + $meetingid = trim(Request::input('meetingid')); + $userids = Request::input('userids'); + // + $meeting = Meeting::whereMeetingid($meetingid)->first(); + if (empty($meeting)) { + return Base::retError('频道ID不存在'); + } + $data = $meeting->toArray(); + // 发送给邀请人 + $msgs = []; + foreach ($userids as $userid) { + if (!User::whereUserid($userid)->exists()) { + continue; + } + $dialog = WebSocketDialog::checkUserDialog($user, $userid); + if ($dialog) { + $res = WebSocketDialogMsg::sendMsg(null, $dialog->id, 'meeting', $data, $user->userid); + if (Base::isSuccess($res)) { + $msgs[] = $res['data']; + } + } + } + // + $data['msgs'] = $msgs; + return Base::retSuccess('发送邀请成功', $data); + } + + /** + * @api {get} api/users/email/send 18. 发送邮箱验证码 + * + * @apiDescription 需要token身份 + * @apiVersion 1.0.0 + * @apiGroup users + * @apiName email__send + * + * @apiParam {Number} type 邮件类型 + * @apiParam {String} email 邮箱地址 + * + * @apiSuccess {Number} ret 返回状态码(1正确、0错误) + * @apiSuccess {String} msg 返回信息(错误描述) + * @apiSuccess {Object} data 返回数据 + */ + public function email__send() + { + $user = User::auth(); + // + $type = Request::input('type', 2); + $email = Request::input('email'); + if (!$email) { + return Base::retError('请输入新邮箱地址'); + } + if (!Base::isEmail($email)) { + return Base::retError('邮箱地址错误'); + } + if ($user->email == $email && $type == 2) { + return Base::retError('不能与旧邮箱一致'); + } + if ($user->email != $email && $type == 3) { + return Base::retError('与当前登录邮箱不一致'); + } + if (User::where('userid', '<>', $user->userid)->whereEmail($email)->exists()) { + return Base::retError('邮箱地址已存在'); + } + UserEmailVerification::userEmailSend($user, $type, $email); + return Base::retSuccess('发送成功'); + } + + /** + * @api {get} api/users/email/edit 19. 修改邮箱 + * + * @apiDescription 需要token身份 + * @apiVersion 1.0.0 + * @apiGroup users + * @apiName edit__email + * + * @apiParam {String} newEmail 新邮箱地址 + * @apiParam {String} code 邮箱验证码 + * + * @apiSuccess {Number} ret 返回状态码(1正确、0错误) + * @apiSuccess {String} msg 返回信息(错误描述) + * @apiSuccess {Object} data 返回数据 + */ + public function email__edit() + { + $user = User::auth(); + // + $user->checkSystem(); + // + if ($user->isLdap()) { + return Base::retError('LDAP 用户禁止修改邮箱'); + } + // + $newEmail = trim(Request::input('newEmail')); + $code = trim(Request::input('code')); + if (!$newEmail) { + return Base::retError('请输入新邮箱地址'); + } + if (!Base::isEmail($newEmail)) { + return Base::retError('邮箱地址错误'); + } + + $isRegVerify = Base::settingFind('emailSetting', 'reg_verify') === 'open'; + if ($isRegVerify) { + UserEmailVerification::verify($newEmail, $code, 2); + } + + $user->email = $newEmail; + $user->save(); + User::generateToken($user); + return Base::retSuccess('修改成功', $user); + } + + /** + * @api {get} api/users/delete/account 20. 删除帐号 + * + * @apiDescription 需要token身份 + * @apiVersion 1.0.0 + * @apiGroup users + * @apiName delete__account + * + * @apiParam {String} email 帐号邮箱 + * @apiParam {String} code 邮箱验证码 + * @apiParam {String} reason 注销理由 + * @apiParam {String} password 登录密码 + * @apiParam {Number} type 类型 + * - warning: 提交校验 + * - confirm: 确认删除 + * + * @apiSuccess {Number} ret 返回状态码(1正确、0错误) + * @apiSuccess {String} msg 返回信息(错误描述) + * @apiSuccess {Object} data 返回数据 + */ + public function delete__account() + { + $user = User::auth(); + // + $user->checkSystem(1); + // + $email = Request::input('email'); + $code = Request::input('code'); + $reason = Request::input('reason'); + $password = Request::input('password'); + $type = Request::input('type'); + if (!$email) { + return Base::retError('请输入新邮箱地址'); + } + if (!Base::isEmail($email)) { + return Base::retError('邮箱地址错误'); + } + if ($user->email != $email) { + return Base::retError('与当前登录邮箱不一致'); + } + + $isRegVerify = Base::settingFind('emailSetting', 'reg_verify') === 'open'; + if ($isRegVerify) { + UserEmailVerification::verify($email, $code, 3); + } else { + if (!$password) { + return Base::retError('请输入登录密码'); + } + if ($user->password != Doo::md5s($password, $user->encrypt)) { + return Base::retError('密码错误'); + } + } + if ($type == 'confirm') { + if ($user->deleteUser($reason)) { + return Base::retSuccess('删除成功', $user); + } else { + return Base::retError('删除失败'); + } + } + return Base::retSuccess('success', $user); + } + + /** + * @api {get} api/users/department/list 21. 部门列表(限管理员) + * + * @apiDescription 需要token身份 + * @apiVersion 1.0.0 + * @apiGroup users + * @apiName department__list + * + * @apiSuccess {Number} ret 返回状态码(1正确、0错误) + * @apiSuccess {String} msg 返回信息(错误描述) + * @apiSuccess {Object} data 返回数据 + */ + public function department__list() + { + User::auth('admin'); + // + return Base::retSuccess('success', UserDepartment::orderBy('id')->get()); + } + + /** + * @api {get} api/users/department/add 22. 新建、修改部门(限管理员) + * + * @apiDescription 需要token身份 + * @apiVersion 1.0.0 + * @apiGroup users + * @apiName department__add + * + * @apiParam {Number} [id] 部门id,留空为创建部门 + * @apiParam {String} name 部门名称 + * @apiParam {Number} [parent_id] 上级部门ID + * @apiParam {Number} owner_userid 部门负责人ID + * @apiParam {String} [dialog_group] 部门群(仅创建部门时有效) + * - new: 创建(默认) + * - use: 使用现有群 + * @apiParam {Number} [dialog_useid] 使用现有群ID(dialog_group=use时有效) + * + * @apiSuccess {Number} ret 返回状态码(1正确、0错误) + * @apiSuccess {String} msg 返回信息(错误描述) + * @apiSuccess {Object} data 返回数据 + */ + public function department__add() + { + User::auth('admin'); + // + $id = intval(Request::input('id')); + $name = trim(Request::input('name')); + $parent_id = intval(Request::input('parent_id')); + $owner_userid = intval(Request::input('owner_userid')); + $dialog_group = trim(Request::input('dialog_group')); + $dialog_useid = $dialog_group === 'use' ? intval(Request::input('dialog_useid')) : 0; + // + if (mb_strlen($name) < 2 || mb_strlen($name) > 20) { + return Base::retError('部门名称长度限制2-20个字'); + } + if (preg_match('/[\Q~!@#$%^&*()+-_=.:?<>,\E]/', $name)) { + return Base::retError('部门名称不能包含特殊符号'); + } + if (str_contains($name, '(M)')) { + return Base::retError('部门名称不能包含:(M)'); + } + // + if ($id > 0) { + $userDepartment = UserDepartment::find($id); + if (empty($userDepartment)) { + return Base::retError('部门不存在或已被删除'); + } + } else { + if (UserDepartment::count() > 200) { + return Base::retError('最多只能创建200个部门'); + } + $userDepartment = UserDepartment::createInstance(); + } + // + if ($parent_id > 0) { + $parentDepartment = UserDepartment::find($parent_id); + if (empty($parentDepartment)) { + return Base::retError('上级部门不存在或已被删除'); + } + if ($parentDepartment->parent_id > 0) { + return Base::retError('上级部门层级错误'); + } + if (UserDepartment::whereParentId($parent_id)->count() > 20) { + return Base::retError('每个部门最多只能创建20个子部门'); + } + if ($id > 0 && UserDepartment::whereParentId($id)->exists()) { + return Base::retError('含有子部门无法修改上级部门'); + } + } + if (empty($owner_userid) || !User::whereUserid($owner_userid)->exists()) { + return Base::retError('请选择正确的部门负责人'); + } + // + $userDepartment->saveDepartment([ + 'name' => $name, + 'parent_id' => $parent_id, + 'owner_userid' => $owner_userid, + ], $dialog_useid); + Cache::forever("UserDepartment::rand", Base::generatePassword()); + // + return Base::retSuccess($parent_id > 0 ? '保存成功' : '新建成功'); + } + + /** + * @api {get} api/users/department/del 23. 删除部门(限管理员) + * + * @apiDescription 需要token身份 + * @apiVersion 1.0.0 + * @apiGroup users + * @apiName department__del + * + * @apiParam {Number} id 部门id + * + * @apiSuccess {Number} ret 返回状态码(1正确、0错误) + * @apiSuccess {String} msg 返回信息(错误描述) + * @apiSuccess {Object} data 返回数据 + */ + public function department__del() + { + User::auth('admin'); + // + $id = intval(Request::input('id')); + // + $userDepartment = UserDepartment::find($id); + if (empty($userDepartment)) { + return Base::retError('部门不存在或已被删除'); + } + $userDepartment->deleteDepartment(); + Cache::forever("UserDepartment::rand", Base::generatePassword()); + // + return Base::retSuccess('删除成功'); + } + + /** + * @api {get} api/users/checkin/get 24. 获取签到设置 + * + * @apiDescription 需要token身份 + * @apiVersion 1.0.0 + * @apiGroup users + * @apiName checkin__get + * + * @apiSuccess {Number} ret 返回状态码(1正确、0错误) + * @apiSuccess {String} msg 返回信息(错误描述) + * @apiSuccess {Object} data 返回数据 + */ + public function checkin__get() + { + $user = User::auth(); + // + $list = UserCheckinMac::whereUserid($user->userid)->orderBy('id')->get(); + // + return Base::retSuccess('success', $list); + } + + /** + * @api {post} api/users/checkin/save 25. 保存签到设置 + * + * @apiDescription 需要token身份 + * @apiVersion 1.0.0 + * @apiGroup users + * @apiName checkin__save + * + * @apiParam {Array} list 优先级数据,格式:[{mac,remark}] + * + * @apiSuccess {Number} ret 返回状态码(1正确、0错误) + * @apiSuccess {String} msg 返回信息(错误描述) + * @apiSuccess {Object} data 返回数据 + */ + public function checkin__save() + { + $user = User::auth(); + // + $setting = Base::setting('checkinSetting'); + if ($setting['open'] !== 'open') { + return Base::retError('此功能未开启,请联系管理员开启'); + } + if ($setting['edit'] !== 'open') { + return Base::retError('未开放修改权限,请联系管理员'); + } + // + $list = Base::getPostValue('list'); + $array = []; + if (empty($list) || !is_array($list)) { + return Base::retError('参数错误'); + } + foreach ($list AS $item) { + $item = Base::newTrim($item); + if (Base::isMac($item['mac'])) { + $mac = strtoupper($item['mac']); + $array[$mac] = [ + 'mac' => $mac, + 'remark' => substr($item['remark'], 0, 50), + ]; + } + } + if (count($array) > 3) { + return Base::retError('最多只能添加3个MAC地址'); + } + // + return UserCheckinMac::saveMac($user->userid, $array); + } + + /** + * @api {get} api/users/checkin/list 26. 获取签到数据 + * + * @apiDescription 需要token身份 + * @apiVersion 1.0.0 + * @apiGroup users + * @apiName checkin__list + * + * @apiParam {String} ym 年-月(如:2020-01) + * @apiParam {Number} [before] 取月份之前的数据(单位:月数,最大3) + * + * @apiSuccess {Number} ret 返回状态码(1正确、0错误) + * @apiSuccess {String} msg 返回信息(错误描述) + * @apiSuccess {Object} data 返回数据 + */ + public function checkin__list() + { + $user = User::auth(); + // + $ym = trim(Request::input('ym')); + $start = Carbon::parse(date("Y-m-01 00:00:00", strtotime($ym))); + $end = (clone $start)->addMonth()->subSecond(); + // + $before = min(3, intval(Request::input('before'))); + if ($before > 0) { + $start = $start->subMonths($before); + } + // + $recordTimes = UserCheckinRecord::getTimes($user->userid, [$start, $end]); + $array = []; + $startT = $start->timestamp; + $endT = $end->timestamp; + while ($startT < $endT) { + $sameDate = date("Y-m-d", $startT); + $sameTimes = $recordTimes[$sameDate] ?? []; + if ($sameTimes) { + $array[] = [ + 'date' => $sameDate, + 'section' => UserCheckinRecord::atSection($sameTimes), + ]; + } + $startT += 86400; + } + // + return Base::retSuccess('success', $array); + } + + /** + * @api {get} api/users/socket/status 27. 获取socket状态 + * + * @apiDescription 需要token身份 + * @apiVersion 1.0.0 + * @apiGroup users + * @apiName socket__status + * + * @apiParam {String} [fd] + * + * @apiSuccess {Number} ret 返回状态码(1正确、0错误) + * @apiSuccess {String} msg 返回信息(错误描述) + * @apiSuccess {Object} data 返回数据 + */ + public function socket__status() + { + $row = WebSocket::select(['id', 'fd', 'userid', 'updated_at'])->whereFd(Base::headerOrInput('fd'))->first(); + if (empty($row)) { + return Base::retError('error'); + } + return Base::retSuccess('success', $row); + } } diff --git a/app/Http/Controllers/IndexController.php b/app/Http/Controllers/IndexController.php index c92425182..ee80fa817 100755 --- a/app/Http/Controllers/IndexController.php +++ b/app/Http/Controllers/IndexController.php @@ -2,14 +2,25 @@ namespace App\Http\Controllers; +use App\Models\File; use App\Module\Base; +use App\Module\Extranet; +use App\Module\RandomColor; +use App\Tasks\AppPushTask; use App\Tasks\AutoArchivedTask; +use App\Tasks\CheckinRemindTask; +use App\Tasks\DeleteBotMsgTask; use App\Tasks\DeleteTmpTask; -use App\Tasks\OverdueRemindEmailTask; +use App\Tasks\EmailNoticeTask; +use App\Tasks\JokeSoupTask; +use App\Tasks\LoopTask; use Arr; +use Cache; use Hhxsv5\LaravelS\Swoole\Task\Task; +use LasseRafn\InitialAvatarGenerator\InitialAvatar; use Redirect; use Request; +use Response; /** @@ -25,6 +36,10 @@ class IndexController extends InvokeController if ($action) { $app .= "__" . $action; } + if ($app === 'manifest.txt') { + $app = 'manifest'; + $child = 'txt'; + } if (!method_exists($this, $app)) { $app = method_exists($this, $method) ? $method : 'main'; } @@ -33,45 +48,121 @@ class IndexController extends InvokeController /** * 首页 - * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View + * @return \Illuminate\Http\Response */ public function main() { - $hash = 'no'; - $path = public_path('js/hash'); - if (file_exists($path)) { - $hash = trim(file_get_contents(public_path('js/hash'))); - if (strlen($hash) > 16) { - $hash = 'long'; - } + $hotFile = public_path('hot'); + $manifestFile = public_path('manifest.json'); + if (file_exists($hotFile)) { + $array = Base::json2array(file_get_contents($hotFile)); + $style = null; + $script = preg_replace("/^(\/\/(.*?))(:\d+)?\//i", "$1:" . $array['APP_DEV_PORT'] . "/", asset_main("resources/assets/js/app.js")); + } else { + $array = Base::json2array(file_get_contents($manifestFile)); + $style = asset_main($array['resources/assets/js/app.js']['css'][0]); + $script = asset_main($array['resources/assets/js/app.js']['file']); } - return view('main', [ + return response()->view('main', [ 'version' => Base::getVersion(), - 'hash' => $hash - ]); + 'style' => $style, + 'script' => $script, + ])->header('Link', "<" . url('manifest.txt') . ">; rel=\"prefetch\""); + } + + /** + * Manifest + * @param $child + * @return \Illuminate\Contracts\Foundation\Application|\Illuminate\Contracts\Routing\ResponseFactory|\Illuminate\Http\Response|string + */ + public function manifest($child = '') + { + if (empty($child)) { + $murl = url('manifest.txt'); + return response($murl)->header('Link', "<{$murl}>; rel=\"prefetch\""); + } + $array = [ + "office/web-apps/apps/api/documents/api.js?hash=" . Base::getVersion(), + "office/7.3.2-8/web-apps/vendor/requirejs/require.js", + "office/7.3.2-8/web-apps/apps/api/documents/api.js", + "office/7.3.2-8/sdkjs/common/AllFonts.js", + "office/7.3.2-8/web-apps/vendor/xregexp/xregexp-all-min.js", + "office/7.3.2-8/web-apps/vendor/sockjs/sockjs.min.js", + "office/7.3.2-8/web-apps/vendor/jszip/jszip.min.js", + "office/7.3.2-8/web-apps/vendor/jszip-utils/jszip-utils.min.js", + "office/7.3.2-8/sdkjs/common/libfont/wasm/fonts.js", + "office/7.3.2-8/sdkjs/common/Charts/ChartStyles.js", + "office/7.3.2-8/sdkjs/slide/themes//themes.js", + + "office/7.3.2-8/web-apps/apps/presentationeditor/main/app.js", + "office/7.3.2-8/sdkjs/slide/sdk-all-min.js", + "office/7.3.2-8/sdkjs/slide/sdk-all.js", + + "office/7.3.2-8/web-apps/apps/documenteditor/main/app.js", + "office/7.3.2-8/sdkjs/word/sdk-all-min.js", + "office/7.3.2-8/sdkjs/word/sdk-all.js", + + "office/7.3.2-8/web-apps/apps/spreadsheeteditor/main/app.js", + "office/7.3.2-8/sdkjs/cell/sdk-all-min.js", + "office/7.3.2-8/sdkjs/cell/sdk-all.js", + ]; + foreach ($array as &$item) { + $item = url($item); + } + return implode(PHP_EOL, $array); } /** * 获取版本号 - * @return array + * @return \Illuminate\Http\RedirectResponse */ public function version() { - $url = url(''); - $package = Base::getPackage(); - $array = [ - 'version' => Base::getVersion(), - 'publish' => Arr::get($package, 'app.0.publish'), - ]; - if (is_array($package['app'])) { - foreach ($package['app'] as $item) { - if (is_array($item['publish']) && Base::hostContrast($url, $item['url'])) { - $array['publish'] = $item['publish']; - break; - } - } + return Redirect::to(Base::fillUrl('api/system/version'), 301); + } + + /** + * 头像 + * @return \Illuminate\Contracts\Foundation\Application|\Illuminate\Contracts\Routing\ResponseFactory|\Illuminate\Http\Response + */ + public function avatar() + { + $segment = Request::segment(2); + if ($segment && preg_match('/.*?\.png$/i', $segment)) { + $name = substr($segment, 0, -4); + } else { + $name = Request::input('name', 'H'); } - return $array; + $size = Request::input('size', 128); + $color = Request::input('color'); + $background = Request::input('background'); + // + if (preg_match('/^[\x{4e00}-\x{9fa5}]+$/u', $name)) { + $name = mb_substr($name, mb_strlen($name) - 2); + } + if (empty($color)) { + $color = '#ffffff'; + $cacheKey = "avatarBackgroundColor::" . md5($name); + $background = Cache::rememberForever($cacheKey, function() { + return RandomColor::one(['luminosity' => 'dark']); + }); + } + // + $avatar = new InitialAvatar(); + $content = $avatar->name($name) + ->size($size) + ->color($color) + ->background($background) + ->fontSize(0.35) + ->autoFont() + ->generate() + ->stream('png', 100); + // + return response($content) + ->header('Pragma', 'public') + ->header('Cache-Control', 'max-age=1814400') + ->header('Content-type', 'image/png') + ->header('Expires', gmdate('D, d M Y H:i:s \G\M\T', time() + 1814400)); } /** @@ -93,13 +184,24 @@ class IndexController extends InvokeController // 限制内网访问 return "Forbidden Access"; } + // 自动归档 + Task::deliver(new AutoArchivedTask()); + // 邮件通知 + Task::deliver(new EmailNoticeTask()); + // App推送 + Task::deliver(new AppPushTask()); // 删除过期的临时表数据 Task::deliver(new DeleteTmpTask('wg_tmp_msgs', 1)); + Task::deliver(new DeleteTmpTask('task_worker', 12)); Task::deliver(new DeleteTmpTask('tmp', 24)); - // 自动归档任务 - Task::deliver(new AutoArchivedTask()); - // 任务到期邮件提醒 - Task::deliver(new OverdueRemindEmailTask()); + // 删除机器人消息 + Task::deliver(new DeleteBotMsgTask()); + // 周期任务 + Task::deliver(new LoopTask()); + // 签到提醒 + Task::deliver(new CheckinRemindTask()); + // 获取笑话/心灵鸡汤 + Task::deliver(new JokeSoupTask()); return "success"; } @@ -109,21 +211,31 @@ class IndexController extends InvokeController */ public function desktop__publish($name = '') { + $publishVersion = Request::header('publish-version'); $latestFile = public_path("uploads/desktop/latest"); - $genericVersion = Request::header('generic-version'); + $latestVersion = file_exists($latestFile) ? trim(file_get_contents($latestFile)) : "0.0.1"; + if (strtolower($name) === 'latest') { + $name = $latestVersion; + } // 上传 - if (preg_match("/^\d+\.\d+\.\d+$/", $genericVersion)) { - $genericPath = "uploads/desktop/" . $genericVersion . "/"; - $res = Base::upload([ - "file" => Request::file('file'), - "type" => 'desktop', - "path" => $genericPath, - "fileName" => true - ]); - if (Base::isSuccess($res)) { - file_put_contents($latestFile, $genericVersion); + if (preg_match("/^\d+\.\d+\.\d+$/", $publishVersion)) { + $publishKey = Request::header('publish-key'); + if ($publishKey !== env('APP_KEY')) { + return Base::retError("key error"); + } + if (version_compare($publishVersion, $latestVersion) > -1) { // 限制上传版本必须 ≥ 当前版本 + $publishPath = "uploads/desktop/{$publishVersion}/"; + $res = Base::upload([ + "file" => Request::file('file'), + "type" => 'desktop', + "path" => $publishPath, + "fileName" => true + ]); + if (Base::isSuccess($res)) { + file_put_contents($latestFile, $publishVersion); + } + return $res; } - return $res; } // 列表 if (preg_match("/^\d+\.\d+\.\d+$/", $name)) { @@ -132,29 +244,119 @@ class IndexController extends InvokeController $lists = Base::readDir($dirPath); $files = []; foreach ($lists as $file) { + if (str_ends_with($file, '.yml') || str_ends_with($file, '.yaml')) { + continue; + } $fileName = Base::leftDelete($file, $dirPath); $files[] = [ 'name' => substr($fileName, 1), - 'time' => date("Y-m-d H:i:s", fileatime($file)), + 'time' => date("Y-m-d H:i:s", filemtime($file)), 'size' => Base::readableBytes(filesize($file)), 'url' => Base::fillUrl($path . $fileName), ]; } + // + $path = "uploads/android"; + $dirPath = public_path($path); + $lists = Base::readDir($dirPath); + $apkFile = null; + foreach ($lists as $file) { + if (!str_ends_with($file, '.apk')) { + continue; + } + if ($apkFile && strtotime($apkFile['time']) > filemtime($file)) { + continue; + } + $fileName = Base::leftDelete($file, $dirPath); + $apkFile = [ + 'name' => substr($fileName, 1), + 'time' => date("Y-m-d H:i:s", filemtime($file)), + 'size' => Base::readableBytes(filesize($file)), + 'url' => Base::fillUrl($path . $fileName), + ]; + } + if ($apkFile) { + $files = array_merge([$apkFile], $files); + } return view('desktop', ['version' => $name, 'files' => $files]); } // 下载 if ($name && file_exists($latestFile)) { - $genericVersion = file_get_contents($latestFile); - if (preg_match("/^\d+\.\d+\.\d+$/", $genericVersion)) { - $filePath = public_path("uploads/desktop/{$genericVersion}/{$name}"); + $publishVersion = file_get_contents($latestFile); + if (preg_match("/^\d+\.\d+\.\d+$/", $publishVersion)) { + $filePath = public_path("uploads/desktop/{$publishVersion}/{$name}"); if (file_exists($filePath)) { - return response()->download($filePath); + return Response::download($filePath); } } } return abort(404); } + /** + * Drawio 图标搜索 + * @return array|mixed + */ + public function drawio__iconsearch() + { + $query = trim(Request::input('q')); + $page = trim(Request::input('p')); + $size = trim(Request::input('c')); + return Extranet::drawioIconSearch($query, $page, $size); + } + + /** + * 预览文件 + * @return array|mixed + */ + public function online__preview() + { + $key = trim(Request::input('key')); + // + $data = parse_url($key); + $path = Arr::get($data, 'path'); + $file = public_path($path); + // + if (file_exists($file)) { + parse_str($data['query'], $query); + $name = Arr::get($query, 'name'); + $ext = strtolower(Arr::get($query, 'ext')); + $userAgent = strtolower(Request::server('HTTP_USER_AGENT')); + if ($ext === 'pdf' + && (str_contains($userAgent, 'electron') || str_contains($userAgent, 'chrome'))) { + return Response::download($file, $name, [ + 'Content-Type' => 'application/pdf' + ], 'inline'); + } + // + if (in_array($ext, File::localExt)) { + $url = Base::fillUrl($path); + } else { + $url = 'http://' . env('APP_IPPR') . '.3/' . $path; + } + if ($ext !== 'pdf') { + $url = Base::urlAddparameter($url, [ + 'fullfilename' => $name . '.' . $ext + ]); + } + $toUrl = Base::fillUrl("fileview/onlinePreview?url=" . urlencode(base64_encode($url))); + return Redirect::to($toUrl, 301); + } + return abort(404); + } + + /** + * 设置语言和皮肤 + * @return \Illuminate\Contracts\Foundation\Application|\Illuminate\Contracts\View\Factory|\Illuminate\Contracts\View\View + */ + public function setting__theme_language() + { + return view('setting', [ + 'theme' => Request::input('theme'), + 'language' => Request::input('language') + ]); + } + /** * 提取所有中文 * @return array|string @@ -216,9 +418,6 @@ class IndexController extends InvokeController $list = array_merge(Base::readDir(app_path()), Base::readDir(resource_path())); $array = []; foreach ($list as $item) { - if (Base::rightExists($item, "language.all.js")) { - continue; - } if (Base::rightExists($item, ".php") || Base::rightExists($item, ".vue") || Base::rightExists($item, ".js")) { $content = file_get_contents($item); preg_match_all("/(['\"])(.*?)[\u{4e00}-\u{9fa5}\u{FE30}-\u{FFA0}]+([\s\S]((?!\n).)*)\\1/u", $content, $matchs); diff --git a/app/Http/Middleware/VerifyCsrfToken.php b/app/Http/Middleware/VerifyCsrfToken.php index 60cdfc939..7970dff29 100644 --- a/app/Http/Middleware/VerifyCsrfToken.php +++ b/app/Http/Middleware/VerifyCsrfToken.php @@ -24,6 +24,9 @@ class VerifyCsrfToken extends Middleware // 保存创建项目列表模板 'api/system/column/template/', + // License 设置 + 'api/system/license/', + // 添加任务 'api/project/task/add/', @@ -36,9 +39,15 @@ class VerifyCsrfToken extends Middleware // 聊天发文本 'api/dialog/msg/sendtext/', + // 聊天发语音 + 'api/dialog/msg/sendrecord/', + // 聊天发文件 'api/dialog/msg/sendfile/', + // 聊天发匿名消息 + 'api/dialog/msg/sendanon/', + // 保存文件内容 'api/file/content/save/', @@ -51,6 +60,12 @@ class VerifyCsrfToken extends Middleware // 保存汇报 'api/report/store/', + // 签到设置 + 'api/users/checkin/save/', + + // 签到上报 + 'api/public/checkin/report/', + // 发布桌面端 'desktop/publish/', ]; diff --git a/app/Http/Middleware/WebApi.php b/app/Http/Middleware/WebApi.php index 77168c009..c66b3a1d6 100644 --- a/app/Http/Middleware/WebApi.php +++ b/app/Http/Middleware/WebApi.php @@ -4,6 +4,7 @@ namespace App\Http\Middleware; @error_reporting(E_ALL & ~E_NOTICE & ~E_WARNING); +use App\Module\Doo; use Closure; use Request; @@ -31,6 +32,7 @@ class WebApi if (in_array(strtolower($APP_SCHEME), ['https', 'on', 'ssl', '1', 'true', 'yes'], true)) { $request->setTrustedProxies([$request->getClientIp()], $request::HEADER_X_FORWARDED_PROTO); } + Doo::load(); return $next($request); } diff --git a/app/Ldap/LdapUser.php b/app/Ldap/LdapUser.php new file mode 100644 index 000000000..71e3de2fe --- /dev/null +++ b/app/Ldap/LdapUser.php @@ -0,0 +1,249 @@ +jpegPhoto && is_array($this->jpegPhoto) ? $this->jpegPhoto[0] : null; + } + + /** + * @return mixed|null + */ + public function getDisplayName() + { + $nickname = $this->displayName ?: $this->uid; + return is_array($nickname) ? $nickname[0] : $nickname; + } + + /** + * @return LdapUser + */ + public static function static(): LdapUser + { + return new static; + } + + /** + * 服务是否打开 + * @return bool + */ + public static function isOpen(): bool + { + return Base::settingFind('thirdAccessSetting', 'ldap_open') === 'open'; + } + + /** + * 同步本地是否打开 + * @return bool + */ + public static function isSyncLocal(): bool + { + return Base::settingFind('thirdAccessSetting', 'ldap_sync_local') === 'open'; + } + + /** + * 初始化配置 + * @return bool + */ + public static function initConfig() + { + if (is_bool(self::$init)) { + return self::$init; + } + // + $setting = Base::setting('thirdAccessSetting'); + if ($setting['ldap_open'] !== 'open') { + return self::$init = false; + } + // + $connection = Container::getDefaultConnection(); + try { + $connection->setConfiguration([ + "hosts" => [$setting['ldap_host']], + "port" => intval($setting['ldap_port']), + "base_dn" => $setting['ldap_base_dn'], + "username" => $setting['ldap_user_dn'], + "password" => $setting['ldap_password'], + ]); + return self::$init = true; + } catch (ConfigurationException $e) { + info($e->getMessage()); + return self::$init = false; + } + } + + /** + * 获取 + * @param $username + * @param $password + * @return Model|null + */ + public static function userFirst($username, $password): ?Model + { + if (!self::initConfig()) { + return null; + } + try { + return self::static() + ->where([ + 'cn' => $username, + 'userPassword' => $password + ])->first(); + } catch (\Exception) { + return null; + } + } + + /** + * 登录 + * @param $username + * @param $password + * @param User|null $user + * @return User|mixed|null + */ + public static function userLogin($username, $password, $user = null) + { + if (!self::initConfig()) { + return null; + } + $row = self::userFirst($username, $password); + if (!$row) { + return null; + } + if (empty($user)) { + $user = User::reg($username, $password); + } + if ($user) { + $userimg = $row->getPhoto(); + if ($userimg) { + $path = "uploads/user/ldap/"; + Base::makeDir(public_path($path)); + file_put_contents(public_path("{$path}{$user->userid}.jpeg"), $userimg); + $user->userimg = "{$path}{$user->userid}.jpeg"; + } + $user->nickname = $row->getDisplayName(); + $user->save(); + } + return $user; + } + + /** + * 同步 + * @param User $user + * @param $password + * @return void + */ + public static function userSync(User $user, $password) + { + if ($user->isLdap()) { + return; + } + // + if (!self::initConfig()) { + return; + } + // + if (self::isSyncLocal()) { + $row = self::userFirst($user->email, $password); + if ($row) { + return; + } + try { + $userimg = public_path($user->getRawOriginal('userimg')); + if (file_exists($userimg)) { + $userimg = file_get_contents($userimg); + } else { + $userimg = ''; + } + self::static()->create([ + 'cn' => $user->email, + 'gidNumber' => 0, + 'homeDirectory' => '/home/ldap/dootask/' . env("APP_NAME"), + 'sn' => $user->email, + 'uid' => $user->email, + 'uidNumber' => $user->userid, + 'userPassword' => $password, + 'displayName' => $user->nickname, + 'jpegPhoto' => $userimg, + ]); + $user->identity = Base::arrayImplode(array_merge(array_diff($user->identity, ['ldap']), ['ldap'])); + $user->save(); + } catch (LdapRecordException $e) { + info("[LDAP] sync fail: " . $e->getMessage()); + } + } + } + + /** + * 更新 + * @param $username + * @param $array + * @return void + */ + public static function userUpdate($username, $array) + { + if (empty($array)) { + return; + } + if (!self::initConfig()) { + return; + } + try { + $row = self::static() + ->where([ + 'cn' => $username, + ])->first(); + $row?->update($array); + } catch (\Exception $e) { + info("[LDAP] update fail: " . $e->getMessage()); + } + } + + /** + * 删除 + * @param $username + * @return void + */ + public static function userDelete($username) + { + if (!self::initConfig()) { + return; + } + try { + $row = self::static() + ->where([ + 'cn' => $username, + ])->first(); + $row?->delete(); + } catch (\Exception $e) { + info("[LDAP] delete fail: " . $e->getMessage()); + } + } +} diff --git a/app/Models/AbstractModel.php b/app/Models/AbstractModel.php index 7174ffe97..6e6c85e23 100644 --- a/app/Models/AbstractModel.php +++ b/app/Models/AbstractModel.php @@ -21,7 +21,10 @@ use Illuminate\Support\Facades\DB; * @method static \Illuminate\Database\Eloquent\Model|object|static|null cancelHidden() * @method static \Illuminate\Database\Eloquent\Builder|static with($relations) * @method static \Illuminate\Database\Query\Builder|static select($columns = []) + * @method static \Illuminate\Database\Query\Builder|static whereIn($column, $values, $boolean = 'and', $not = false) * @method static \Illuminate\Database\Query\Builder|static whereNotIn($column, $values, $boolean = 'and') + * @method int change(array $array) + * @method int remove() * @mixin \Eloquent */ class AbstractModel extends Model @@ -38,6 +41,42 @@ class AbstractModel extends Model protected $appendattrs = []; + /** + * 通过模型修改数据 + * @param AbstractModel $builder + * @param $array + * @return int + */ + protected function scopeChange($builder, $array) + { + $line = 0; + $rows = $builder->get(); + foreach ($rows as $row) { + $row->updateInstance($array); + if ($row->save()) { + $line++; + } + } + return $line; + } + + /** + * 通过模型删除数据 + * @param AbstractModel $builder + * @return int + */ + protected function scopeRemove($builder) + { + $line = 0; + $rows = $builder->get(); + foreach ($rows as $row) { + if ($row->delete()) { + $line++; + } + } + return $line; + } + /** * 保存数据忽略错误 * @return bool @@ -46,7 +85,7 @@ class AbstractModel extends Model { try { return $this->save(); - } catch (\Exception $e) { + } catch (\Throwable) { return false; } } diff --git a/app/Models/Deleted.php b/app/Models/Deleted.php new file mode 100644 index 000000000..26e8d494b --- /dev/null +++ b/app/Models/Deleted.php @@ -0,0 +1,88 @@ + $type, + 'userid' => $userid + ])->where('created_at', '>=', Carbon::parse($time))->pluck('did')->toArray(); + } + + /** + * 忘记(恢复或添加数据时删除记录) + * @param $type + * @param $id + * @param $userid + * @return void + */ + public static function forget($type, $id, $userid): void + { + if (is_array($userid)) { + self::where([ + 'type' => $type, + 'did' => $id, + ])->whereIn('userid', $userid)->delete(); + } else { + self::where([ + 'type' => $type, + 'did' => $id, + 'userid' => $userid, + ])->delete(); + } + } + + /** + * 记录(删除数据时添加记录) + * @param $type + * @param $id + * @param $userid + * @return void + */ + public static function record($type, $id, $userid): void + { + $array = is_array($userid) ? $userid : [$userid]; + foreach ($array as $value) { + self::updateInsert([ + 'type' => $type, + 'did' => $id, + 'userid' => $value, + ]); + } + } +} diff --git a/app/Models/File.php b/app/Models/File.php index 97df338a2..48830485f 100644 --- a/app/Models/File.php +++ b/app/Models/File.php @@ -13,8 +13,8 @@ use Request; * App\Models\File * * @property int $id - * @property string|null $pids 上级ID递归 * @property int|null $pid 上级ID + * @property string|null $pids 上级ID递归 * @property int|null $cid 复制ID * @property string|null $name 名称 * @property string|null $type 类型 @@ -22,6 +22,7 @@ use Request; * @property int|null $size 大小(B) * @property int|null $userid 拥有者ID * @property int|null $share 是否共享 + * @property int|null $pshare 所属分享ID * @property int|null $created_id 创建者 * @property \Illuminate\Support\Carbon|null $created_at * @property \Illuminate\Support\Carbon|null $updated_at @@ -39,6 +40,7 @@ use Request; * @method static \Illuminate\Database\Eloquent\Builder|File whereName($value) * @method static \Illuminate\Database\Eloquent\Builder|File wherePid($value) * @method static \Illuminate\Database\Eloquent\Builder|File wherePids($value) + * @method static \Illuminate\Database\Eloquent\Builder|File wherePshare($value) * @method static \Illuminate\Database\Eloquent\Builder|File whereShare($value) * @method static \Illuminate\Database\Eloquent\Builder|File whereSize($value) * @method static \Illuminate\Database\Eloquent\Builder|File whereType($value) @@ -63,7 +65,7 @@ class File extends AbstractModel 'inc', 'phtml', 'shtml', 'php3', 'php4', 'php5', 'phps', 'phpt', 'aw', 'ctp', 'module', 'ps1', 'py', 'r', 'rb', 'ru', 'gemspec', 'rake', 'guardfile', 'rakefile', 'gemfile', 'rs', 'sass', 'scss', 'sh', 'bash', 'bashrc', 'sql', 'sqlserver', 'swift', 'ts', 'typescript', 'str', 'vbs', 'vb', 'v', 'vh', 'sv', 'svh', 'xml', 'rdf', 'rss', 'wsdl', 'xslt', 'atom', 'mathml', 'mml', 'xul', 'xbl', 'xaml', 'yaml', 'yml', - 'asp', 'properties', 'gitignore', 'log', 'bas', 'prg', 'python', 'ftl', 'aspx' + 'asp', 'properties', 'gitignore', 'log', 'bas', 'prg', 'python', 'ftl', 'aspx', 'plist' ]; /** @@ -94,21 +96,18 @@ class File extends AbstractModel /** * 是否有访问权限 - * @param $userid + * @param array $userids * @return int -1:没有权限,0:访问权限,1:读写权限,1000:所有者或创建者 */ - public function getPermission($userid) + public function getPermission(array $userids) { - if ($userid == $this->userid || $userid == $this->created_id) { + if (in_array($this->userid, $userids) || in_array($this->created_id, $userids)) { // ① 自己的文件夹 或 自己创建的文件夹 return 1000; } $row = $this->getShareInfo(); if ($row) { - $fileUser = FileUser::whereFileId($row->id)->where(function ($query) use ($userid) { - $query->where('userid', 0); - $query->orWhere('userid', $userid); - })->orderByDesc('permission')->first(); + $fileUser = FileUser::whereFileId($row->id)->whereIn('userid', $userids)->orderByDesc('permission')->first(); if ($fileUser) { // ② 在指定共享成员内 return $fileUser->permission; @@ -142,7 +141,7 @@ class File extends AbstractModel /** * 是否处于共享文件夹内(不含自身) - * @return bool + * @return File|false */ public function isNnShare() { @@ -153,13 +152,22 @@ class File extends AbstractModel break; } if ($row->share) { - return true; + return $row; } $pid = $row->pid; } return false; } + /** + * 目录内是否存在共享文件或文件夹 + * @return bool + */ + public function isSubShare() + { + return $this->type == 'folder' && File::where("pids", "like", "%,{$this->id},%")->whereShare(1)->exists(); + } + /** * 设置/关闭 共享(同时遍历取消里面的共享) * @param $share @@ -174,6 +182,7 @@ class File extends AbstractModel AbstractModel::transaction(function () use ($share) { $this->share = $share; $this->save(); + File::where("pids", "like", "%,{$this->id},%")->update(['pshare' => $share ? $this->id : 0]); if ($share === 0) { FileUser::deleteFileAll($this->id, $this->userid); } @@ -189,16 +198,50 @@ class File extends AbstractModel } /** - * 保存前更新pids + * 处理重名 + * @return void + */ + public function handleDuplicateName() + { + $builder = self::wherePid($this->pid)->whereUserid($this->userid)->whereExt($this->ext); + $exist = $builder->clone()->whereName($this->name)->exists(); + if (!$exist) { + return; // 未重名,不需要处理 + } + // 发现重名,自动重命名 + $nextNum = 2; + if (preg_match("/(.*?)(\s+\(\d+\))*$/", $this->name)) { + $preName = preg_replace("/(.*?)(\s+\(\d+\))*$/", "$1", $this->name); + $nextNum = $builder->clone()->where("name", "LIKE", "{$preName}%")->count() + 1; + } + $newName = "{$this->name} ({$nextNum})"; + if ($builder->clone()->whereName($newName)->exists()) { + $nextNum = rand(100, 9999); + $newName = "{$this->name} ({$nextNum})"; + } + $this->name = $newName; + } + + /** + * 保存前更新pids/pshare * @return bool */ - public function saveBeforePids() + public function saveBeforePP() { $pid = $this->pid; + $pshare = $this->share ? $this->id : 0; $array = []; while ($pid > 0) { $array[] = $pid; - $pid = intval(self::whereId($pid)->value('pid')); + $file = self::select(['id', 'pid', 'share'])->find($pid); + if ($file) { + $pid = $file->pid; + if ($file->share) { + $pshare = $file->id; + } + } else { + $pid = 0; + } } $opids = $this->pids; if ($array) { @@ -207,6 +250,7 @@ class File extends AbstractModel } else { $this->pids = ''; } + $this->pshare = $pshare; if (!$this->save()) { return false; } @@ -215,7 +259,7 @@ class File extends AbstractModel self::wherePid($this->id)->chunkById(100, function ($lists) { /** @var self $item */ foreach ($lists as $item) { - $item->saveBeforePids(); + $item->saveBeforePP(); } }); } @@ -243,6 +287,29 @@ class File extends AbstractModel return true; } + /** + * 获取文件分享链接 + * @param $userid + * @param $refresh + * @return array + */ + public function getShareLink($userid, $refresh = false) + { + if ($this->type == 'folder') { + throw new ApiException('文件夹不支持分享'); + } + return FileLink::generateLink($this->id, $userid, $refresh); + } + + /** + * 获取文件名称加后缀 + * @return string|null + */ + public function getNameAndExt() + { + return $this->ext ? "{$this->name}.{$this->ext}" : $this->name; + } + /** * 推送消息 * @param $action @@ -257,19 +324,7 @@ class File extends AbstractModel ]; } // - if ($userid === null) { - $userid = [$this->userid]; - if ($this->share == 1) { - $builder = WebSocket::select(['userid']); - if ($action == 'content') { - $builder->wherePath('file/content/' . $this->id); - } - $userid = array_merge($userid, $builder->pluck('userid')->toArray()); - } elseif ($this->share == 2) { - $userid = array_merge($userid, FileUser::whereFileId($this->id)->pluck('userid')->toArray()); - } - $userid = array_values(array_filter(array_unique($userid))); - } + $userid = $this->pushUserid($action, $userid); if (empty($userid)) { return; } @@ -293,35 +348,102 @@ class File extends AbstractModel } /** - * 处理返回图片地址 - * @param $item - * @return void + * 获取推送会员 + * @param $action + * @param $userid + * @return array|int[]|mixed|null[] */ - public static function handleImageUrl(&$item) + public function pushUserid($action, $userid = null) { + $wherePath = "/manage/file"; + if ($userid === null) { + $array = [$this->userid]; + if ($action == 'add' && $this->pid == 0) { + return $array; + } + if ($action == 'content') { + $wherePath = "/single/file/{$this->id}"; + } elseif ($this->pid > 0) { + $wherePath = "/manage/file/{$this->pid}"; + } else { + $tmpArray = FileUser::whereFileId($this->id)->pluck('userid')->toArray(); + if (empty($tmpArray)) { + return $array; + } + if (!in_array(0, $tmpArray)) { + return $tmpArray; + } + } + $tmpArray = WebSocket::wherePath($wherePath)->pluck('userid')->toArray(); + if (empty($tmpArray)) { + return $array; + } + $array = array_values(array_filter(array_unique(array_merge($array, $tmpArray)))); + } else { + $array = is_array($userid) ? $userid : [$userid]; + if (in_array(0, $array)) { + return WebSocket::wherePath($wherePath)->pluck('userid')->toArray(); + } + } + return $array; + } + + /** + * code获取文件ID、名称 + * @param $code + * @return File + */ + public static function code2IdName($code) { + $arr = explode(",", base64_decode($code)); + if (empty($arr)) { + return null; + } + $fileId = intval($arr[0]); + if (empty($fileId)) { + return null; + } + return File::select(['id', 'name'])->find($fileId); + } + + + /** + * 处理返回图片地址 + * @param array $item + * @return array + */ + public static function handleImageUrl($item) { if (in_array($item['ext'], self::imageExt) ) { $content = Base::json2array(FileContent::whereFid($item['id'])->orderByDesc('id')->value('content')); if ($content) { $item['image_url'] = Base::fillUrl($content['url']); + $item['image_width'] = intval($content['width']); + $item['image_height'] = intval($content['height']); } } + return $item; } /** * 获取文件并检测权限 - * @param $id - * @param int $limit 要求权限: 0-访问权限、1-读写权限、1000-所有者或创建者 - * @param $permission + * @param int $id + * @param User|array|int $user 要求权限的用户,如:[0, 1] + * @param int $limit 要求权限: 0-访问权限、1-读写权限、1000-所有者或创建者 + * @param int $permission * @return File */ - public static function permissionFind($id, $limit = 0, &$permission = -1) + public static function permissionFind(int $id, $user, int $limit = 0, int &$permission = -1) { $file = File::find($id); if (empty($file)) { throw new ApiException('文件不存在或已被删除'); } // - $permission = $file->getPermission(User::userid()); + if ($user instanceof User) { + $userids = $user->isTemp() ? [$user->userid] : [0, $user->userid]; + } else { + $userids = is_array($user) ? $user : [$user]; + } + $permission = $file->getPermission($userids); if ($permission < $limit) { $msg = match ($limit) { 1000 => '仅限所有者或创建者操作', @@ -340,11 +462,10 @@ class File extends AbstractModel */ public static function formatFileData(array $data) { + $fileName = $data['name']; $filePath = $data['path']; $fileSize = $data['size']; $fileExt = $data['ext']; - $fileDotExt = '.' . $fileExt; - $fileName = Base::rightDelete($data['name'], $fileDotExt) . $fileDotExt; $publicPath = public_path($filePath); // switch ($fileExt) { @@ -376,27 +497,27 @@ class File extends AbstractModel if (in_array($fileExt, self::codeExt) && $fileSize < 2 * 1024 * 1024) { // 文本预览,限制2M内的文件 - $data['content'] = file_get_contents($publicPath) ?: 'Content deleted'; + $data['content'] = [ + 'content' => file_get_contents($publicPath) ?: 'Content deleted', + ]; $data['file_mode'] = 'code'; } elseif (in_array($fileExt, File::officeExt)) { // office预览 - $data['content'] = ''; + $data['content'] = json_decode('{}'); $data['file_mode'] = 'office'; } else { // 其他预览 - if (in_array($fileExt, File::localExt)) { - $url = Base::fillUrl($filePath); - } else { - $url = 'http://' . env('APP_IPPR') . '.3/' . $filePath; - } + $name = Base::rightDelete($fileName, ".{$fileExt}") . ".{$fileExt}"; $data['content'] = [ 'preview' => true, - 'url' => base64_encode(Base::urlAddparameter($url, [ - 'fullfilename' => $fileName + 'name' => $name, + 'key' => urlencode(Base::urlAddparameter($filePath, [ + 'name' => $name, + 'ext' => $fileExt ])), ]; $data['file_mode'] = 'preview'; @@ -405,4 +526,60 @@ class File extends AbstractModel } return $data; } + + /** + * 移交文件 + * @param $originalUserid + * @param $newUserid + * @return void + */ + public static function transfer($originalUserid, $newUserid) + { + if (!self::whereUserid($originalUserid)->exists()) { + return; + } + + // 创建一个文件夹存放移交的文件 + $name = User::userid2nickname($originalUserid) ?: ('ID:' . $originalUserid); + $file = File::createInstance([ + 'pid' => 0, + 'name' => "【{$name}】移交的文件", + 'type' => "folder", + 'ext' => "", + 'userid' => $newUserid, + 'created_id' => 0, + ]); + $file->handleDuplicateName(); + $file->saveBeforePP(); + + // 移交文件 + self::whereUserid($originalUserid)->chunkById(100, function($list) use ($file, $newUserid) { + /** @var self $item */ + foreach ($list as $item) { + if ($item->pid === 0) { + $item->pid = $file->id; + } + $item->userid = $newUserid; + $item->saveBeforePP(); + } + }); + + // 移交文件权限 + FileUser::whereUserid($originalUserid)->chunkById(100, function ($list) use ($newUserid) { + /** @var FileUser $item */ + foreach ($list as $item) { + $row = FileUser::whereFileId($item->file_id)->whereUserid($newUserid)->first(); + if ($row) { + // 已存在则删除原数据,判断改变已存在的数据 + $row->permission = max($row->permission, $item->permission); + $row->save(); + $item->delete(); + } else { + // 不存在则改变原数据 + $item->userid = $newUserid; + $item->save(); + } + } + }); + } } diff --git a/app/Models/FileContent.php b/app/Models/FileContent.php index df86e574a..0bddedf14 100644 --- a/app/Models/FileContent.php +++ b/app/Models/FileContent.php @@ -5,7 +5,6 @@ namespace App\Models; use App\Module\Base; use Illuminate\Database\Eloquent\SoftDeletes; -use Response; /** * App\Models\FileContent @@ -40,12 +39,52 @@ class FileContent extends AbstractModel { use SoftDeletes; + /** + * 转预览地址 + * @param array $array + * @return string + */ + public static function toPreviewUrl($array) + { + $fileExt = $array['ext']; + $fileName = $array['name']; + $filePath = $array['path']; + $name = Base::rightDelete($fileName, ".{$fileExt}") . ".{$fileExt}"; + $key = urlencode(Base::urlAddparameter($filePath, [ + 'name' => $name, + 'ext' => $fileExt + ])); + return Base::fillUrl("online/preview/{$name}?key={$key}"); + } + + /** + * 转预览地址 + * @param File $file + * @param $content + * @return string + */ + public static function formatPreview($file, $content) + { + $content = Base::json2array($content ?: []); + $filePath = $content['url']; + if (in_array($file->type, ['word', 'excel', 'ppt'])) { + if (empty($content)) { + $filePath = 'assets/office/empty.' . str_replace(['word', 'excel', 'ppt'], ['docx', 'xlsx', 'pptx'], $file->type); + } + } + return self::toPreviewUrl([ + 'ext' => $file->ext, + 'name' => $file->name, + 'path' => $filePath, + ]); + } + /** * 获取格式内容(或下载) * @param File $file * @param $content * @param $download - * @return array|\Symfony\Component\HttpFoundation\BinaryFileResponse + * @return array|\Symfony\Component\HttpFoundation\StreamedResponse */ public static function formatContent($file, $content, $download = false) { @@ -53,9 +92,13 @@ class FileContent extends AbstractModel $content = Base::json2array($content ?: []); if (in_array($file->type, ['word', 'excel', 'ppt'])) { if (empty($content)) { - return Response::download(resource_path('assets/statics/office/empty.' . str_replace(['word', 'excel', 'ppt'], ['docx', 'xlsx', 'pptx'], $file->type)), $name); + $filePath = public_path('assets/office/empty.' . str_replace(['word', 'excel', 'ppt'], ['docx', 'xlsx', 'pptx'], $file->type)); + } else { + $filePath = public_path($content['url']); } - return Response::download(public_path($content['url']), $name); + return Base::streamDownload(function() use ($filePath) { + echo file_get_contents($filePath); + }, $name); } if (empty($content)) { $content = match ($file->type) { @@ -84,7 +127,9 @@ class FileContent extends AbstractModel if ($download) { $filePath = public_path($path); if (isset($filePath)) { - return Response::download($filePath, $name); + return Base::streamDownload(function() use ($filePath) { + echo file_get_contents($filePath); + }, $name); } else { abort(403, "This file not support download."); } diff --git a/app/Models/FileLink.php b/app/Models/FileLink.php index 27a56e1f1..027b6f660 100644 --- a/app/Models/FileLink.php +++ b/app/Models/FileLink.php @@ -2,6 +2,8 @@ namespace App\Models; +use App\Module\Base; + /** * App\Models\FileLink * @@ -34,4 +36,35 @@ class FileLink extends AbstractModel { return $this->hasOne(File::class, 'id', 'file_id'); } + + /** + * 生成链接 + * @param $fileId + * @param $userid + * @param $refresh + * @return array + */ + public static function generateLink($fileId, $userid, $refresh = false) + { + $fileLink = FileLink::whereFileId($fileId)->whereUserid($userid)->first(); + if (empty($fileLink)) { + $fileLink = FileLink::createInstance([ + 'file_id' => $fileId, + 'userid' => $userid, + 'code' => base64_encode("{$fileId},{$userid}," . Base::generatePassword()), + ]); + $fileLink->save(); + } else { + if ($refresh == 'yes') { + $fileLink->code = base64_encode("{$fileId},{$userid}," . Base::generatePassword()); + $fileLink->save(); + } + } + return [ + 'id' => $fileId, + 'url' => Base::fillUrl('single/file/' . $fileLink->code), + 'code' => $fileLink->code, + 'num' => $fileLink->num + ]; + } } diff --git a/app/Models/Meeting.php b/app/Models/Meeting.php new file mode 100644 index 000000000..92ba18bbc --- /dev/null +++ b/app/Models/Meeting.php @@ -0,0 +1,34 @@ + $this->dialog_id, 'userid' => $userid, + ], [ + 'important' => 1 ]); } - WebSocketDialogUser::whereDialogId($this->dialog_id)->whereNotIn('userid', $userids)->delete(); + WebSocketDialogUser::whereDialogId($this->dialog_id)->whereNotIn('userid', $userids)->whereImportant(1)->remove(); }); } @@ -244,8 +251,8 @@ class Project extends AbstractModel $this->archived_at = null; $this->archived_userid = User::userid(); $this->addLog("项目取消归档"); - $this->pushMsg('add', $this); - ProjectTask::whereProjectId($this->id)->whereArchivedFollow(1)->update([ + $this->pushMsg('recovery', $this); + ProjectTask::whereProjectId($this->id)->whereArchivedFollow(1)->change([ 'archived_at' => null, 'archived_follow' => 0 ]); @@ -255,7 +262,7 @@ class Project extends AbstractModel $this->archived_userid = User::userid(); $this->addLog("项目归档"); $this->pushMsg('archived'); - ProjectTask::whereProjectId($this->id)->whereArchivedAt(null)->update([ + ProjectTask::whereProjectId($this->id)->whereArchivedAt(null)->change([ 'archived_at' => $archived_at, 'archived_follow' => 1 ]); @@ -434,7 +441,7 @@ class Project extends AbstractModel }); // foreach ($upTaskList as $id => $value) { - ProjectTask::whereFlowItemId($id)->update([ + ProjectTask::whereFlowItemId($id)->change([ 'flow_item_name' => $value ]); } @@ -461,6 +468,93 @@ class Project extends AbstractModel }); } + /** + * 创建项目 + * @param $params + * - name 项目名称 + * - desc + * - flow + * - personal + * - columns + * @return array + */ + public static function createProject($params, $userid) + { + $name = trim(Arr::get($params, 'name', '')); + $desc = trim(Arr::get($params, 'desc', '')); + $flow = trim(Arr::get($params, 'flow', 'close')); + $isPersonal = intval(Arr::get($params, 'personal')); + if (mb_strlen($name) < 2) { + return Base::retError('项目名称不可以少于2个字'); + } elseif (mb_strlen($name) > 32) { + return Base::retError('项目名称最多只能设置32个字'); + } + if (mb_strlen($desc) > 255) { + return Base::retError('项目介绍最多只能设置255个字'); + } + // 列表 + $columns = explode(",", Arr::get($params, 'columns')); + $insertColumns = []; + $sort = 0; + foreach ($columns AS $column) { + $column = trim($column); + if ($column) { + $insertColumns[] = [ + 'name' => $column, + 'sort' => $sort++, + ]; + } + } + if (empty($insertColumns)) { + $insertColumns[] = [ + 'name' => 'Default', + 'sort' => 0, + ]; + } + if (count($insertColumns) > 30) { + return Base::retError('项目列表最多不能超过30个'); + } + // 开始创建 + $project = Project::createInstance([ + 'name' => $name, + 'desc' => $desc, + 'userid' => $userid, + ]); + if ($isPersonal) { + if (Project::whereUserid($userid)->wherePersonal(1)->exists()) { + return Base::retError('个人项目已存在,无须重复创建'); + } + $project->personal = 1; + } + AbstractModel::transaction(function() use ($flow, $insertColumns, $project) { + $project->save(); + ProjectUser::createInstance([ + 'project_id' => $project->id, + 'userid' => $project->userid, + 'owner' => 1, + ])->save(); + foreach ($insertColumns AS $column) { + $column['project_id'] = $project->id; + ProjectColumn::createInstance($column)->save(); + } + $dialog = WebSocketDialog::createGroup($project->name, $project->userid, 'project'); + if (empty($dialog)) { + throw new ApiException('创建项目聊天室失败'); + } + $project->dialog_id = $dialog->id; + $project->save(); + // + if ($flow == 'open') { + $project->addFlow(Base::json2array('[{"id":-10,"name":"待处理","status":"start","turns":[-10,-11,-12,-13,-14],"userids":[],"usertype":"add","userlimit":0},{"id":-11,"name":"进行中","status":"progress","turns":[-10,-11,-12,-13,-14],"userids":[],"usertype":"add","userlimit":0},{"id":-12,"name":"待测试","status":"test","turns":[-10,-11,-12,-13,-14],"userids":[],"usertype":"add","userlimit":0},{"id":-13,"name":"已完成","status":"end","turns":[-10,-11,-12,-13,-14],"userids":[],"usertype":"add","userlimit":0},{"id":-14,"name":"已取消","status":"end","turns":[-10,-11,-12,-13,-14],"userids":[],"usertype":"add","userlimit":0}]')); + } + }); + // + $data = Project::find($project->id); + $data->addLog("创建项目"); + $data->pushMsg('add', $data); + return Base::retSuccess('添加成功', $data); + } + /** * 获取项目信息(用于判断会员是否存在项目内) * @param int $project_id diff --git a/app/Models/ProjectFlowItem.php b/app/Models/ProjectFlowItem.php index 853e4efe8..e03c78ff7 100644 --- a/app/Models/ProjectFlowItem.php +++ b/app/Models/ProjectFlowItem.php @@ -81,7 +81,7 @@ class ProjectFlowItem extends AbstractModel */ public function deleteFlowItem() { - ProjectTask::whereFlowItemId($this->id)->update([ + ProjectTask::whereFlowItemId($this->id)->change([ 'flow_item_id' => 0, 'flow_item_name' => "", ]); diff --git a/app/Models/ProjectTask.php b/app/Models/ProjectTask.php index 891ea1171..44fb6912c 100644 --- a/app/Models/ProjectTask.php +++ b/app/Models/ProjectTask.php @@ -7,12 +7,9 @@ use App\Module\Base; use App\Tasks\PushTask; use Arr; use Carbon\Carbon; -use Config; use DB; -use Exception; use Hhxsv5\LaravelS\Swoole\Task\Task; use Illuminate\Database\Eloquent\SoftDeletes; -use Mail; use Request; /** @@ -39,6 +36,8 @@ use Request; * @property string|null $p_name 优先级名称 * @property string|null $p_color 优先级颜色 * @property int|null $sort 排序(ASC) + * @property string|null $loop 重复周期 + * @property string|null $loop_at 下一次重复时间 * @property \Illuminate\Support\Carbon|null $created_at * @property \Illuminate\Support\Carbon|null $updated_at * @property \Illuminate\Support\Carbon|null $deleted_at @@ -81,6 +80,8 @@ use Request; * @method static \Illuminate\Database\Eloquent\Builder|ProjectTask whereFlowItemId($value) * @method static \Illuminate\Database\Eloquent\Builder|ProjectTask whereFlowItemName($value) * @method static \Illuminate\Database\Eloquent\Builder|ProjectTask whereId($value) + * @method static \Illuminate\Database\Eloquent\Builder|ProjectTask whereLoop($value) + * @method static \Illuminate\Database\Eloquent\Builder|ProjectTask whereLoopAt($value) * @method static \Illuminate\Database\Eloquent\Builder|ProjectTask whereName($value) * @method static \Illuminate\Database\Eloquent\Builder|ProjectTask wherePColor($value) * @method static \Illuminate\Database\Eloquent\Builder|ProjectTask wherePLevel($value) @@ -303,6 +304,7 @@ class ProjectTask extends AbstractModel 'project_tasks.*', 'project_task_users.owner' ]) + ->selectRaw("1 AS assist") ->join('project_task_users', 'project_tasks.id', '=', 'project_task_users.task_id') ->where('project_task_users.userid', $userid); if ($owner !== null) { @@ -529,7 +531,7 @@ class ProjectTask extends AbstractModel { AbstractModel::transaction(function () use ($data, &$updateMarking) { // 判断版本 - Base::checkClientVersion('0.6.0'); + Base::checkClientVersion('0.19.0'); // 主任务 $mainTask = $this->parent_id > 0 ? self::find($this->parent_id) : null; // 工作流 @@ -626,7 +628,7 @@ class ProjectTask extends AbstractModel if ($this->complete_at) { throw new ApiException('任务已完成'); } - $this->completeTask(Carbon::now()); + $this->completeTask(Carbon::now(), isset($newFlowItem) ? $newFlowItem->name : null); } else { // 标记未完成 if (!$this->complete_at) { @@ -647,10 +649,13 @@ class ProjectTask extends AbstractModel 'change' => [$this->name, $data['name']] ]); $this->name = $data['name']; + if ($this->dialog_id) { + WebSocketDialog::updateData(['id' => $this->dialog_id], ['name' => $this->name]); + } } // 负责人 if (Arr::exists($data, 'owner')) { - $count = $this->taskUser->where('owner', 1)->count(); + $older = $this->taskUser->where('owner', 1)->pluck('userid')->toArray(); $array = []; $owner = is_array($data['owner']) ? $data['owner'] : [$data['owner']]; if (count($owner) > 10) { @@ -669,20 +674,18 @@ class ProjectTask extends AbstractModel 'owner' => 1, ]); $array[] = $uid; - if ($this->parent_id) { - break; // 子任务只能是一个负责人 - } } if ($array) { - if ($count == 0 && count($array) == 1 && $array[0] == User::userid()) { + if (count($older) == 0 && count($array) == 1 && $array[0] == User::userid()) { $this->addLog("认领{任务}"); } else { $this->addLog("修改{任务}负责人", ['userid' => $array]); } + $this->taskPush(array_values(array_diff($array, $older)), 0); } $rows = ProjectTaskUser::whereTaskId($this->id)->whereOwner(1)->whereNotIn('userid', $array)->get(); if ($rows->isNotEmpty()) { - $this->addLog("删除{任务}负责人", ['userid' => $rows->implode('userid', ',')]); + $this->addLog("删除{任务}负责人", ['userid' => $rows->pluck('userid')]); foreach ($rows as $row) { $row->delete(); } @@ -697,7 +700,7 @@ class ProjectTask extends AbstractModel $this->start_at = null; $this->end_at = null; $times = $data['times']; - list($start, $end) = is_string($times) ? explode(",", $times) : (is_array($times) ? $times : []); + list($start, $end, $desc) = is_string($times) ? explode(",", $times) : (is_array($times) ? $times : []); if (Base::isDate($start) && Base::isDate($end) && $start != $end) { $start_at = Carbon::parse($start); $end_at = Carbon::parse($end); @@ -758,15 +761,38 @@ class ProjectTask extends AbstractModel }); } $newStringAt = $this->start_at ? ($this->start_at->toDateTimeString() . '~' . $this->end_at->toDateTimeString()) : ''; - $this->addLog("修改{任务}时间", [ + $newDesc = $desc ? "(备注:{$desc})" : ""; + $this->addLog("修改{任务}时间" . $newDesc, [ 'change' => [$oldStringAt, $newStringAt] ]); - - //修改计划时间需要重置任务邮件提醒日志 - ProjectTaskMailLog::whereTaskId($this->id)->delete(); + $this->taskPush(null, 3, $newDesc); } - // 以下紧顶级任务可修改 + // 以下仅顶级任务可修改 if ($this->parent_id === 0) { + // 重复周期 + $loopAt = $this->loop_at; + $loopDesc = $this->loopDesc(); + if (Arr::exists($data, 'loop')) { + $this->loop = $data['loop']; + if (!$this->refreshLoop()) { + throw new ApiException('重复周期选择错误'); + } + } elseif (Arr::exists($data, 'times')) { + // 更新任务时间也要更新重复周期 + $this->refreshLoop(); + } + $oldLoop = $loopAt ? Carbon::parse($loopAt)->toDateTimeString() : null; + $newLoop = $this->loop_at ? Carbon::parse($this->loop_at)->toDateTimeString() : null; + if ($oldLoop != $newLoop) { + $this->addLog("修改{任务}下个周期", [ + 'change' => [$oldLoop, $newLoop] + ]); + } + if ($loopDesc != $this->loopDesc()) { + $this->addLog("修改{任务}重复周期", [ + 'change' => [$loopDesc, $this->loopDesc()] + ]); + } // 协助人员 if (Arr::exists($data, 'assist')) { $array = []; @@ -793,7 +819,7 @@ class ProjectTask extends AbstractModel } $rows = ProjectTaskUser::whereTaskId($this->id)->whereOwner(0)->whereNotIn('userid', $array)->get(); if ($rows->isNotEmpty()) { - $this->addLog("删除{任务}协助人员", ['userid' => $rows->implode('userid', ',')]); + $this->addLog("删除{任务}协助人员", ['userid' => $rows->pluck('userid')]); foreach ($rows as $row) { $row->delete(); } @@ -860,6 +886,141 @@ class ProjectTask extends AbstractModel return true; } + /** + * 刷新重复周期时间 + * @param bool $save 是否执行保存 + * @return bool + */ + public function refreshLoop($save = false) + { + $success = true; + if ($this->start_at) { + $base = Carbon::parse($this->start_at); + if ($base->lt(Carbon::today())) { + // 如果任务开始时间小于今天则基数时间为今天 + $base = Carbon::parse(date("Y-m-d {$base->toTimeString()}")); + } + } else { + // 未设置任务时间时基数时间为今天 + $base = Carbon::today(); + } + switch ($this->loop) { + case "day": + $this->loop_at = $base->addDay(); + break; + case "weekdays": + $this->loop_at = $base->addWeekday(); + break; + case "week": + $this->loop_at = $base->addWeek(); + break; + case "twoweeks": + $this->loop_at = $base->addWeeks(2); + break; + case "month": + $this->loop_at = $base->addMonth(); + break; + case "year": + $this->loop_at = $base->addYear(); + break; + case "never": + $this->loop_at = null; + break; + default: + if (Base::isNumber($this->loop)) { + $this->loop_at = $base->addDays($this->loop); + } else { + $this->loop_at = null; + $success = false; + } + break; + } + if ($success && $save) { + $this->save(); + } + return $success; + } + + /** + * 获取周期描述 + * @return string + */ + public function loopDesc() { + $loopDesc = "从不"; + switch ($this->loop) { + case "day": + $loopDesc = "每天"; + break; + case "weekdays": + $loopDesc = "每个工作日"; + break; + case "week": + $loopDesc = "每周"; + break; + case "twoweeks": + $loopDesc = "每两周"; + break; + case "month": + $loopDesc = "每月"; + break; + case "year": + $loopDesc = "每年"; + break; + default: + if (Base::isNumber($this->loop)) { + $loopDesc = "每{$this->loop}天"; + } + break; + } + return $loopDesc; + } + + /** + * 复制任务 + * @return self + */ + public function copyTask() + { + if ($this->parent_id > 0) { + throw new ApiException('子任务禁止复制'); + } + return AbstractModel::transaction(function() { + // 复制任务 + $task = $this->replicate(); + $task->dialog_id = 0; + $task->archived_at = null; + $task->archived_userid = 0; + $task->archived_follow = 0; + $task->complete_at = null; + $task->created_at = Carbon::now(); + $task->save(); + // 复制任务内容 + if ($this->content) { + $tmp = $this->content->replicate(); + $tmp->task_id = $task->id; + $tmp->created_at = Carbon::now(); + $tmp->save(); + } + // 复制任务附件 + foreach ($this->taskFile as $taskFile) { + $tmp = $taskFile->replicate(); + $tmp->task_id = $task->id; + $tmp->created_at = Carbon::now(); + $tmp->save(); + } + // 复制任务成员 + foreach ($this->taskUser as $taskUser) { + $tmp = $taskUser->replicate(); + $tmp->task_id = $task->id; + $tmp->task_pid = $task->id; + $tmp->created_at = Carbon::now(); + $tmp->save(); + } + // + return $task; + }); + } + /** * 同步项目成员至聊天室 */ @@ -879,9 +1040,11 @@ class ProjectTask extends AbstractModel WebSocketDialogUser::updateInsert([ 'dialog_id' => $this->dialog_id, 'userid' => $userid, + ], [ + 'important' => 1 ]); } - WebSocketDialogUser::whereDialogId($this->dialog_id)->whereNotIn('userid', $userids)->delete(); + WebSocketDialogUser::whereDialogId($this->dialog_id)->whereNotIn('userid', $userids)->whereImportant(1)->remove(); }); } @@ -929,11 +1092,18 @@ class ProjectTask extends AbstractModel /** * 权限版本 - * @param int $level 1-负责人,2-协助人/负责人,3-创建人/协助人/负责人 + * @param int $level + * 1:负责人 + * 2:协助人/负责人 + * 3:创建人/协助人/负责人 + * 4:任务群聊成员/3 * @return bool */ public function permission($level = 1) { + if ($level >= 4) { + return $this->permission(3) || $this->existDialogUser(); + } if ($level >= 3 && $this->isCreater()) { return true; } @@ -943,6 +1113,15 @@ class ProjectTask extends AbstractModel return $this->isOwner(); } + /** + * 判断是否在任务对话里 + * @return bool + */ + public function existDialogUser() + { + return $this->dialog_id && WebSocketDialogUser::whereDialogId($this->dialog_id)->whereUserid(User::userid())->exists(); + } + /** * 判断是否创建者 * @return bool @@ -998,15 +1177,22 @@ class ProjectTask extends AbstractModel /** * 标记已完成、未完成 * @param Carbon|null $complete_at 完成时间 + * @param String $complete_name 已完成名称(留空为:已完成) * @return bool */ - public function completeTask($complete_at) + public function completeTask($complete_at, $complete_name = null) { - AbstractModel::transaction(function () use ($complete_at) { + AbstractModel::transaction(function () use ($complete_at, $complete_name) { + $addMsg = $this->parent_id == 0 && $this->dialog_id > 0; if ($complete_at === null) { // 标记未完成 $this->complete_at = null; $this->addLog("标记{任务}未完成"); + if ($addMsg) { + WebSocketDialogMsg::sendMsg(null, $this->dialog_id, 'notice', [ + 'notice' => "标记任务未完成" + ], 0, true, true); + } } else { // 标记已完成 if ($this->parent_id == 0) { @@ -1017,8 +1203,16 @@ class ProjectTask extends AbstractModel if (!$this->hasOwner()) { throw new ApiException('请先领取任务'); } + if (empty($complete_name)) { + $complete_name = '已完成'; + } $this->complete_at = $complete_at; - $this->addLog("标记{任务}已完成"); + $this->addLog("标记{任务}{$complete_name}"); + if ($addMsg) { + WebSocketDialogMsg::sendMsg(null, $this->dialog_id, 'notice', [ + 'notice' => "标记任务{$complete_name}" + ], 0, true, true); + } } $this->save(); }); @@ -1063,12 +1257,12 @@ class ProjectTask extends AbstractModel $this->archived_follow = 0; $this->addLog($logText, [], $userid); } - $this->pushMsg('update', [ + $this->pushMsg($archived_at === null ? 'recovery' : 'archived', [ 'id' => $this->id, 'archived_at' => $this->archived_at, 'archived_userid' => $this->archived_userid, ]); - self::whereParentId($this->id)->update([ + self::whereParentId($this->id)->change([ 'archived_at' => $this->archived_at, 'archived_userid' => $this->archived_userid, 'archived_follow' => $this->archived_follow, @@ -1090,7 +1284,7 @@ class ProjectTask extends AbstractModel $dialog = WebSocketDialog::find($this->dialog_id); $dialog?->deleteDialog(); } - self::whereParentId($this->id)->delete(); + self::whereParentId($this->id)->remove(); $this->deleted_userid = User::userid(); $this->save(); $this->addLog("删除{任务}"); @@ -1107,12 +1301,12 @@ class ProjectTask extends AbstractModel * @param bool $pushMsg 是否推送 * @return bool */ - public function recoveryTask($pushMsg = true) + public function restoreTask($pushMsg = true) { AbstractModel::transaction(function () { if ($this->dialog_id) { $dialog = WebSocketDialog::withTrashed()->find($this->dialog_id); - $dialog?->recoveryDialog(); + $dialog?->restoreDialog(); } self::whereParentId($this->id)->withTrashed()->restore(); $this->addLog("还原{任务}"); @@ -1175,28 +1369,62 @@ class ProjectTask extends AbstractModel $data = $data->toArray(); } // - $array = [$userid, []]; + $userids = []; if ($userid === null) { - $array[0] = $this->project->relationUserids(); + $userids = $this->project->relationUserids(); } elseif (!is_array($userid)) { - $array[0] = [$userid]; + $userids = [$userid]; } // - if (isset($data['owner'])) { - $owners = ProjectTaskUser::whereTaskId($data['id'])->whereOwner(1)->pluck('userid')->toArray(); - $array = [array_intersect($array[0], $owners), array_diff($array[0], $owners)]; - } - foreach ($array as $index => $item) { - if ($index > 0) { - $data['owner'] = 0; + $array = []; + if (empty($data['parent_id'])) { + if (Arr::exists($data, 'owner') || Arr::exists($data, 'assist')) { + $taskUser = ProjectTaskUser::select(['userid', 'owner'])->whereTaskId($data['id'])->get(); + // 负责人 + $owners = $taskUser->where('owner', 1)->pluck('userid')->toArray(); + $owners = array_intersect($userids, $owners); + if ($owners) { + $array[] = [ + 'userid' => array_values($owners), + 'data' => array_merge($data, [ + 'owner' => 1, + 'assist' => 1, + ]) + ]; + } + // 协助人 + $assists = $taskUser->pluck('userid')->toArray(); + $assists = array_intersect($userids, array_diff($assists, $owners)); + if ($assists) { + $array[] = [ + 'userid' => array_values($assists), + 'data' => array_merge($data, [ + 'owner' => 0, + 'assist' => 1, + ]) + ]; + } + // 项目成员(其他人) + $userids = array_diff($userids, $owners, $assists); + $data = array_merge($data, [ + 'owner' => 0, + 'assist' => 0, + ]); } + } + $array[] = [ + 'userid' => array_values($userids), + 'data' => $data + ]; + // + foreach ($array as $item) { $params = [ 'ignoreFd' => Request::header('fd'), 'userid' => array_values($item), 'msg' => [ 'type' => 'projectTask', 'action' => $action, - 'data' => $data, + 'data' => $item['data'], ] ]; $task = new PushTask($params, false); @@ -1204,6 +1432,62 @@ class ProjectTask extends AbstractModel } } + /** + * 任务提醒 + * @param $userids + * @param int $type 0-新任务、1-即将超时、2-已超时、3-修改时间 + * @param string $suffix 描述后缀 + * @return void + */ + public function taskPush($userids, int $type, string $suffix = "") + { + if ($userids === null) { + $userids = $this->taskUser->pluck('userid')->toArray(); + } + if (empty($userids)) { + return; + } + $owners = $this->taskUser->pluck('owner', 'userid')->toArray(); + $receivers = User::whereIn('userid', $userids)->whereNull('disable_at')->get(); + if (empty($receivers)) { + return; + } + + $botUser = User::botGetOrCreate('task-alert'); + if (empty($botUser)) { + return; + } + + $taskHtml = "id}\">#{$this->name}"; + $text = match ($type) { + 1 => "您的任务 {$taskHtml} 即将超时。", + 2 => "您的任务 {$taskHtml} 已经超时。", + 3 => "您的任务 {$taskHtml} 时间已修改。", + default => "您有一个新任务 {$taskHtml}。", + }; + + /** @var User $user */ + foreach ($receivers as $receiver) { + $data = [ + 'type' => $type, + 'userid' => $receiver->userid, + 'task_id' => $this->id, + ]; + if (in_array($type, [1, 2]) && ProjectTaskPushLog::where($data)->exists()) { + continue; + } + // + $replace = $owners[$receiver->userid] ? "您负责的任务" : "您协助的任务"; + $dialog = WebSocketDialog::checkUserDialog($botUser, $receiver->userid); + if ($dialog) { + ProjectTaskPushLog::createInstance($data)->save(); + WebSocketDialogMsg::sendMsg(null, $dialog->id, 'text', [ + 'text' => str_replace("您的任务", $replace, $text) . $suffix + ], $botUser->userid); + } + } + } + /** * 获取任务 * @param $task_id @@ -1211,7 +1495,15 @@ class ProjectTask extends AbstractModel */ public static function oneTask($task_id) { - return self::with(['taskUser', 'taskTag'])->allData()->where("project_tasks.id", intval($task_id))->first(); + $data = self::with(['taskUser', 'taskTag'])->allData()->where("project_tasks.id", intval($task_id))->first(); + if ($data && $data->parent_id === 0) { + if ($data->owner || ProjectTaskUser::select(['owner'])->whereTaskId($data->id)->whereUserid(User::userid())->exists()) { + $data->assist = 1; + } else { + $data->assist = 0; + } + } + return $data; } /** @@ -1219,7 +1511,10 @@ class ProjectTask extends AbstractModel * @param int $task_id * @param bool $archived true:仅限未归档, false:仅限已归档, null:不限制 * @param bool $trashed true:仅限未删除, false:仅限已删除, null:不限制 - * @param int|bool $permission 0|false:不限制, 1|true:限制项目负责人、任务负责人、协助人员及任务创建者, 2:已有负责人才限制true (子任务时如果是主任务负责人也可以) + * @param int|bool $permission + * - 0|false 限制:项目成员、任务成员、任务群聊成员(任务成员 = 任务创建人+任务协助人+任务负责人) + * - 1|true 限制:项目负责人、任务成员 + * - 2 已有负责人才限制true (子任务时如果是主任务负责人也可以) * @param array $with * @return self */ @@ -1245,20 +1540,21 @@ class ProjectTask extends AbstractModel // try { $project = Project::userProject($task->project_id); - } catch (Exception $e) { - if ($task->owner === null) { + } catch (\Throwable $e) { + if ($task->owner !== null || (!$permission && $task->permission(4))) { + $project = Project::find($task->project_id); + if (empty($project)) { + throw new ApiException('项目不存在或已被删除', [ 'task_id' => $task_id ], -4002); + } + } else { throw new ApiException($e->getMessage(), [ 'task_id' => $task_id ], -4002); } - $project = Project::find($task->project_id); - if (empty($project)) { - throw new ApiException('项目不存在或已被删除', [ 'task_id' => $task_id ], -4002); - } } // - if ($permission === 2) { + if ($permission >= 2) { $permission = $task->hasOwner() ? 1 : 0; } - if (($permission === 1 || $permission === true) && !$project->owner && !$task->permission(3)) { + if ($permission && !$project->owner && !$task->permission(3)) { throw new ApiException('仅限项目负责人、任务负责人、协助人员或任务创建者操作'); } // diff --git a/app/Models/ProjectTaskContent.php b/app/Models/ProjectTaskContent.php index 97ab0a176..9e1809b1a 100644 --- a/app/Models/ProjectTaskContent.php +++ b/app/Models/ProjectTaskContent.php @@ -61,13 +61,14 @@ class ProjectTaskContent extends AbstractModel { $path = 'uploads/task/content/' . date("Ym") . '/' . $task_id . '/'; // - preg_match_all("/ $text) { $tmpPath = $path . 'attached/'; Base::makeDir(public_path($tmpPath)); $tmpPath .= md5($text) . "." . $matchs[1][$key]; if (file_put_contents(public_path($tmpPath), base64_decode($text))) { - $content = str_replace($matchs[0][$key], 'ext)); } + + /** + * 宽 + * @return int + */ + public function getWidthAttribute() + { + $this->generateSizeData(); + return $this->appendattrs['width']; + } + + /** + * 高 + * @return int + */ + public function getHeightAttribute() + { + $this->generateSizeData(); + return $this->appendattrs['height']; + } + + /** + * 生成尺寸数据 + */ + private function generateSizeData() + { + if (!isset($this->appendattrs['width'])) { + $width = -1; + $height = -1; + if (in_array($this->ext, ['jpg', 'jpeg', 'gif', 'png'])) { + $path = public_path($this->getRawOriginal('path')); + [$width, $height] = Cache::remember("File::size-" . md5($path), now()->addDays(7), function () use ($path) { + $width = -1; + $height = -1; + if (file_exists($path)) { + $paramet = getimagesize($path); + $width = $paramet[0]; + $height = $paramet[1]; + } + return [$width, $height]; + }); + } + $this->appendattrs['width'] = $width; + $this->appendattrs['height'] = $height; + } + } } diff --git a/app/Models/ProjectTaskMailLog.php b/app/Models/ProjectTaskPushLog.php similarity index 55% rename from app/Models/ProjectTaskMailLog.php rename to app/Models/ProjectTaskPushLog.php index 2b5cbb78e..626ff4e54 100644 --- a/app/Models/ProjectTaskMailLog.php +++ b/app/Models/ProjectTaskPushLog.php @@ -8,37 +8,31 @@ namespace App\Models; use Illuminate\Database\Eloquent\SoftDeletes; /** - * App\Models\ProjectTaskMailLog + * App\Models\ProjectTaskPushLog * * @property int $id * @property int|null $userid 用户id * @property int|null $task_id 任务id - * @property string|null $email 电子邮箱 - * @property int|null $type 提醒类型:1第一次任务提醒,2第二次任务超期提醒 - * @property int|null $is_send 邮件发送是否成功:0否,1是 - * @property string|null $send_error 邮件发送错误详情 + * @property int|null $type 提醒类型:0 任务开始提醒,1 距离到期提醒,2到期超时提醒 * @property \Illuminate\Support\Carbon|null $created_at * @property \Illuminate\Support\Carbon|null $updated_at * @property \Illuminate\Support\Carbon|null $deleted_at - * @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskMailLog newModelQuery() - * @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskMailLog newQuery() - * @method static \Illuminate\Database\Query\Builder|ProjectTaskMailLog onlyTrashed() - * @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskMailLog query() - * @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskMailLog whereCreatedAt($value) - * @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskMailLog whereDeletedAt($value) - * @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskMailLog whereEmail($value) - * @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskMailLog whereId($value) - * @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskMailLog whereIsSend($value) - * @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskMailLog whereSendError($value) - * @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskMailLog whereTaskId($value) - * @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskMailLog whereType($value) - * @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskMailLog whereUpdatedAt($value) - * @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskMailLog whereUserid($value) - * @method static \Illuminate\Database\Query\Builder|ProjectTaskMailLog withTrashed() - * @method static \Illuminate\Database\Query\Builder|ProjectTaskMailLog withoutTrashed() + * @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskPushLog newModelQuery() + * @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskPushLog newQuery() + * @method static \Illuminate\Database\Query\Builder|ProjectTaskPushLog onlyTrashed() + * @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskPushLog query() + * @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskPushLog whereCreatedAt($value) + * @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskPushLog whereDeletedAt($value) + * @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskPushLog whereId($value) + * @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskPushLog whereTaskId($value) + * @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskPushLog whereType($value) + * @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskPushLog whereUpdatedAt($value) + * @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskPushLog whereUserid($value) + * @method static \Illuminate\Database\Query\Builder|ProjectTaskPushLog withTrashed() + * @method static \Illuminate\Database\Query\Builder|ProjectTaskPushLog withoutTrashed() * @mixin \Eloquent */ -class ProjectTaskMailLog extends AbstractModel +class ProjectTaskPushLog extends AbstractModel { use SoftDeletes; diff --git a/app/Models/ProjectTaskUser.php b/app/Models/ProjectTaskUser.php index d3c642a71..3a1d36211 100644 --- a/app/Models/ProjectTaskUser.php +++ b/app/Models/ProjectTaskUser.php @@ -13,6 +13,7 @@ namespace App\Models; * @property int|null $owner 是否任务负责人 * @property \Illuminate\Support\Carbon|null $created_at * @property \Illuminate\Support\Carbon|null $updated_at + * @property-read \App\Models\ProjectTask|null $projectTask * @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskUser newModelQuery() * @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskUser newQuery() * @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskUser query() @@ -29,4 +30,45 @@ namespace App\Models; class ProjectTaskUser extends AbstractModel { + /** + * @return \Illuminate\Database\Eloquent\Relations\HasOne + */ + public function projectTask(): \Illuminate\Database\Eloquent\Relations\HasOne + { + return $this->hasOne(ProjectTask::class, 'id', 'task_id'); + } + + /** + * 移交任务身份 + * @param $originalUserid + * @param $newUserid + * @return void + */ + public static function transfer($originalUserid, $newUserid) + { + self::whereUserid($originalUserid)->chunk(100, function ($list) use ($originalUserid, $newUserid) { + $tastIds = []; + /** @var self $item */ + foreach ($list as $item) { + $row = self::whereTaskId($item->task_id)->whereUserid($newUserid)->first(); + if ($row) { + // 已存在则删除原数据,判断改变已存在的数据 + $row->owner = max($row->owner, $item->owner); + $row->save(); + $item->delete(); + } else { + // 不存在则改变原数据 + $item->userid = $newUserid; + $item->save(); + } + if ($item->projectTask) { + $item->projectTask->addLog("移交{任务}身份", ['userid' => [$originalUserid, ' => ',$newUserid]]); + if (!in_array($item->task_pid, $tastIds)) { + $tastIds[] = $item->task_pid; + $item->projectTask->syncDialogUser(); + } + } + } + }); + } } diff --git a/app/Models/ProjectUser.php b/app/Models/ProjectUser.php index 904ed2033..96eb0145d 100644 --- a/app/Models/ProjectUser.php +++ b/app/Models/ProjectUser.php @@ -2,8 +2,6 @@ namespace App\Models; -use App\Module\Base; - /** * App\Models\ProjectUser * @@ -38,6 +36,41 @@ class ProjectUser extends AbstractModel return $this->hasOne(Project::class, 'id', 'project_id'); } + /** + * 移交项目身份 + * @param $originalUserid + * @param $newUserid + * @return void + */ + public static function transfer($originalUserid, $newUserid) + { + self::whereUserid($originalUserid)->chunkById(100, function ($list) use ($originalUserid, $newUserid) { + /** @var self $item */ + foreach ($list as $item) { + $row = self::whereProjectId($item->project_id)->whereUserid($newUserid)->first(); + if ($row) { + // 已存在则删除原数据,判断改变已存在的数据 + $row->owner = max($row->owner, $item->owner); + $row->save(); + $item->delete(); + } else { + // 不存在则改变原数据 + $item->userid = $newUserid; + $item->save(); + } + if ($item->project) { + if ($item->project->personal) { + $name = User::userid2nickname($originalUserid) ?: ('ID:' . $originalUserid); + $item->project->name = "【{$name}】{$item->project->name}"; + $item->project->save(); + } + $item->project->addLog("移交项目身份", ['userid' => [$originalUserid, ' => ', $newUserid]]); + $item->project->syncDialogUser(); + } + } + }); + } + /** * 退出项目 */ @@ -47,15 +80,13 @@ class ProjectUser extends AbstractModel ->whereUserid($this->userid) ->chunk(100, function ($list) { $tastIds = []; + /** @var ProjectTaskUser $item */ foreach ($list as $item) { + $item->delete(); if (!in_array($item->task_pid, $tastIds)) { $tastIds[] = $item->task_pid; + $item->projectTask?->syncDialogUser(); } - $item->delete(); - } - $tasks = ProjectTask::whereIn('id', $tastIds)->get(); - foreach ($tasks as $task) { - $task->syncDialogUser(); } }); $this->delete(); diff --git a/app/Models/Setting.php b/app/Models/Setting.php index a337cfc3f..dea33845f 100644 --- a/app/Models/Setting.php +++ b/app/Models/Setting.php @@ -2,6 +2,8 @@ namespace App\Models; +use App\Module\Base; + /** * App\Models\Setting * @@ -24,5 +26,40 @@ namespace App\Models; */ class Setting extends AbstractModel { - + /** + * 验证邮箱地址(过滤忽略地址) + * @param $array + * @param \Closure $resultClosure + * @param \Closure|null $emptyClosure + * @return array|mixed + */ + public static function validateAddr($array, $resultClosure, $emptyClosure = null) + { + if (!is_array($array)) { + $array = [$array]; + } + $ignoreAddr = Base::settingFind('emailSetting', 'ignore_addr'); + $ignoreAddr = explode("\n", $ignoreAddr); + $ignoreArray = ['admin@dootask.com', 'test@dootask.com']; + foreach ($ignoreAddr as $item) { + if (Base::isEmail($item)) { + $ignoreArray[] = trim($item); + } + } + if ($ignoreArray) { + $array = array_diff($array, $ignoreArray); + } + if ($array) { + if ($resultClosure instanceof \Closure) { + foreach ($array as $value) { + $resultClosure($value); + } + } + } else { + if ($emptyClosure instanceof \Closure) { + $emptyClosure(); + } + } + return $array; + } } diff --git a/app/Models/TaskWorker.php b/app/Models/TaskWorker.php new file mode 100644 index 000000000..bdb9bef16 --- /dev/null +++ b/app/Models/TaskWorker.php @@ -0,0 +1,37 @@ + $setting['ios_key'], + 'appMasterSecret' => $setting['ios_secret'], + 'production_mode' => true, + ]; + } + if ($setting['android_key']) { + $config['Android'] = [ + 'appKey' => $setting['android_key'], + 'appMasterSecret' => $setting['android_secret'], + 'production_mode' => true, + ]; + } + return $config; + } + + /** + * 推送消息 + * @param string $alias + * @param string $platform + * @param array $array [title, subtitle, body, description, extra, seconds, badge] + * @return array|false + */ + public static function pushMsgToAlias($alias, $platform, $array) + { + $config = self::getPushConfig(); + if ($config === false) { + return false; + } + // + $title = $array['title'] ?: ''; // 标题 + $subtitle = $array['subtitle'] ?: ''; // 副标题(iOS) + $body = $array['body'] ?: ''; // 通知内容 + $description = $array['description'] ?: 'no description'; // 描述 + $extra = is_array($array['extra']) ? $array['extra'] : []; // 额外参数 + $seconds = intval($array['seconds']) ?: 86400; // 有效时间(单位:秒) + $badge = intval($array['badge']) ?: 0; // 角标数(iOS) + // + switch ($platform) { + case 'ios': + if (!isset($config['iOS'])) { + return false; + } + $ios = new IOS($config); + return $ios->send([ + 'description' => $description, + 'payload' => array_merge([ + 'aps' => [ + 'alert' => [ + 'title' => $title, + 'subtitle' => $subtitle, + 'body' => $body, + ], + 'sound' => 'default', + 'badge' => $badge, + ], + ], $extra), + 'type' => 'customizedcast', + 'alias_type' => 'userid', + 'alias' => $alias, + 'policy' => [ + 'expire_time' => Carbon::now()->addSeconds($seconds)->toDateTimeString(), + ], + ]); + + case 'android': + if (!isset($config['Android'])) { + return false; + } + $android = new Android($config); + return $android->send([ + 'description' => $description, + 'payload' => array_merge([ + 'display_type' => 'notification', + 'body' => [ + 'ticker' => $title, + 'text' => $body, + 'title' => $title, + 'after_open' => 'go_app', + 'play_sound' => true, + ], + ], $extra), + 'type' => 'customizedcast', + 'alias_type' => 'userid', + 'alias' => $alias, + 'mipush' => true, + 'mi_activity' => 'app.eeui.umeng.activity.MfrMessageActivity', + 'policy' => [ + 'expire_time' => Carbon::now()->addSeconds($seconds)->toDateTimeString(), + ] + ]); + + default: + return false; + } + } + + /** + * 推送给指定会员 + * @param array|int $userid + * @param array $array + * @return void + */ + public static function pushMsgToUserid($userid, $array) + { + $builder = self::select(['id', 'platform', 'alias', 'userid'])->where('updated_at', '>', Carbon::now()->subMonth()); + if (is_array($userid)) { + $builder->whereIn('userid', $userid); + } elseif (Base::isNumber($userid)) { + $builder->whereUserid($userid); + } + $builder + ->orderByDesc('id') + ->chunkById(100, function ($datas) use ($array) { + $uids = $datas->groupBy('userid'); + foreach ($uids as $uid => $rows) { + $array['badge'] = WebSocketDialogMsgRead::whereUserid($uid)->whereSilence(0)->whereReadAt(null)->count(); + $lists = $rows->groupBy('platform'); + foreach ($lists as $platform => $list) { + $alias = $list->pluck('alias')->implode(','); + self::pushMsgToAlias($alias, $platform, $array); + } + } + }); + } +} diff --git a/app/Models/User.php b/app/Models/User.php index 750aa481e..626a9e4e7 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -5,6 +5,7 @@ namespace App\Models; use App\Exceptions\ApiException; use App\Module\Base; +use App\Module\Doo; use Cache; use Carbon\Carbon; @@ -13,8 +14,11 @@ use Carbon\Carbon; * * @property int $userid * @property array $identity 身份 + * @property array $department 所属部门 * @property string|null $az A-Z + * @property string|null $pinyin 拼音(主要用于搜索) * @property string|null $email 邮箱 + * @property string|null $tel 联系电话 * @property string $nickname 昵称 * @property string|null $profession 职位/职称 * @property string $userimg 头像 @@ -28,8 +32,9 @@ use Carbon\Carbon; * @property string|null $line_at 最后在线时间(接口) * @property int|null $task_dialog_id 最后打开的任务会话ID * @property string|null $created_ip 注册IP - * @property string|null $disable_at 禁用时间 + * @property string|null $disable_at 禁用时间(离职时间) * @property int|null $email_verity 邮箱是否已验证 + * @property int|null $bot 是否机器人 * @property \Illuminate\Support\Carbon|null $created_at * @property \Illuminate\Support\Carbon|null $updated_at * @method static \Database\Factories\UserFactory factory(...$parameters) @@ -37,9 +42,11 @@ use Carbon\Carbon; * @method static \Illuminate\Database\Eloquent\Builder|User newQuery() * @method static \Illuminate\Database\Eloquent\Builder|User query() * @method static \Illuminate\Database\Eloquent\Builder|User whereAz($value) + * @method static \Illuminate\Database\Eloquent\Builder|User whereBot($value) * @method static \Illuminate\Database\Eloquent\Builder|User whereChangepass($value) * @method static \Illuminate\Database\Eloquent\Builder|User whereCreatedAt($value) * @method static \Illuminate\Database\Eloquent\Builder|User whereCreatedIp($value) + * @method static \Illuminate\Database\Eloquent\Builder|User whereDepartment($value) * @method static \Illuminate\Database\Eloquent\Builder|User whereDisableAt($value) * @method static \Illuminate\Database\Eloquent\Builder|User whereEmail($value) * @method static \Illuminate\Database\Eloquent\Builder|User whereEmailVerity($value) @@ -52,8 +59,10 @@ use Carbon\Carbon; * @method static \Illuminate\Database\Eloquent\Builder|User whereLoginNum($value) * @method static \Illuminate\Database\Eloquent\Builder|User whereNickname($value) * @method static \Illuminate\Database\Eloquent\Builder|User wherePassword($value) + * @method static \Illuminate\Database\Eloquent\Builder|User wherePinyin($value) * @method static \Illuminate\Database\Eloquent\Builder|User whereProfession($value) * @method static \Illuminate\Database\Eloquent\Builder|User whereTaskDialogId($value) + * @method static \Illuminate\Database\Eloquent\Builder|User whereTel($value) * @method static \Illuminate\Database\Eloquent\Builder|User whereUpdatedAt($value) * @method static \Illuminate\Database\Eloquent\Builder|User whereUserid($value) * @method static \Illuminate\Database\Eloquent\Builder|User whereUserimg($value) @@ -64,22 +73,14 @@ class User extends AbstractModel protected $primaryKey = 'userid'; protected $hidden = [ - 'disable_at', 'updated_at', ]; - /** - * 更新数据校验 - * @param array $param - */ - public function updateInstance(array $param) - { - parent::updateInstance($param); - // - if (isset($param['line_at']) && $this->userid) { - Cache::put("User::online:" . $this->userid, time(), Carbon::now()->addSeconds(30)); - } - } + // 默认头像类型:auto自动生成,system系统默认 + public static $defaultAvatarMode = 'auto'; + + // 基本信息的字段 + public static $basicField = ['userid', 'email', 'nickname', 'profession', 'department', 'userimg', 'bot', 'az', 'pinyin', 'line_at', 'disable_at']; /** * 昵称 @@ -98,11 +99,7 @@ class User extends AbstractModel */ public function getUserimgAttribute($value) { - if ($value) { - return Base::fillUrl($value); - } - $name = ($this->userid - 1) % 21 + 1; - return url("images/avatar/default_{$name}.png"); + return self::getAvatar($this->userid, $value, $this->email, $this->nickname); } /** @@ -118,13 +115,47 @@ class User extends AbstractModel return array_filter(is_array($value) ? $value : explode(",", trim($value, ","))); } + /** + * 部门 + * @param $value + * @return array + */ + public function getDepartmentAttribute($value) + { + if (empty($value)) { + return []; + } + return array_filter(is_array($value) ? $value : Base::explodeInt($value)); + } + + /** + * 获取所属部门名称 + * @return string + */ + public function getDepartmentName() + { + if (empty($this->department)) { + return ""; + } + $key = "UserDepartment::" . md5(Cache::get("UserDepartment::rand") . '-' . implode(',' , $this->department)); + $list = Cache::remember($key, now()->addMonth(), function() { + $list = UserDepartment::select(['id', 'owner_userid', 'name'])->whereIn('id', $this->department)->take(10)->get(); + return $list->toArray(); + }); + $array = []; + foreach ($list as $item) { + $array[] = $item['name'] . ($item['owner_userid'] === $this->userid ? '(M)' : ''); + } + return implode(', ', $array); + } + /** * 是否在线 * @return bool */ public function getOnlineStatus() { - $online = intval(Cache::get("User::online:" . $this->userid, 0)); + $online = $this->bot || Cache::get("User::online:" . $this->userid) === "on"; if ($online) { return true; } @@ -132,9 +163,45 @@ class User extends AbstractModel } /** - * 判断是否管理员 + * 返回是否LDAP用户 + * @return bool + */ + public function isLdap() + { + return in_array('ldap', $this->identity); + } + + /** + * 返回是否临时帐号 + * @return bool + */ + public function isTemp() + { + return in_array('temp', $this->identity); + } + + /** + * 返回是否禁用帐号(离职) + * @return bool + */ + public function isDisable() + { + return in_array('disable', $this->identity); + } + + /** + * 返回是否管理员 + * @return bool */ public function isAdmin() + { + return in_array('admin', $this->identity); + } + + /** + * 判断是否管理员 + */ + public function checkAdmin() { $this->identity('admin'); } @@ -169,6 +236,36 @@ class User extends AbstractModel } } + /** + * 删除会员 + * @param $reason + * @return bool|null + */ + public function deleteUser($reason) + { + return AbstractModel::transaction(function () use ($reason) { + // 删除原因 + $userDelete = UserDelete::createInstance([ + 'operator' => User::userid(), + 'userid' => $this->userid, + 'email' => $this->email, + 'reason' => $reason, + 'cache' => array_merge($this->getRawOriginal(), [ + 'department_name' => $this->getDepartmentName() + ]) + ]); + $userDelete->save(); + // 删除未读 + WebSocketDialogMsgRead::whereUserid($this->userid)->delete(); + // 删除待办 + WebSocketDialogMsgTodo::whereUserid($this->userid)->delete(); + // 删除邮箱验证记录 + UserEmailVerification::whereEmail($this->email)->delete(); + // + return $this->delete(); + }); + } + /** ***************************************************************************************** */ /** ***************************************************************************************** */ /** ***************************************************************************************** */ @@ -182,105 +279,47 @@ class User extends AbstractModel */ public static function reg($email, $password, $other = []) { - //邮箱 + // 邮箱 if (!Base::isEmail($email)) { throw new ApiException('请输入正确的邮箱地址'); } - if (User::email2userid($email) > 0) { + $user = self::whereEmail($email)->first(); + if ($user) { $isRegVerify = Base::settingFind('emailSetting', 'reg_verify') === 'open'; - $user = self::whereUserid(User::email2userid($email))->first(); if ($isRegVerify && $user->email_verity === 0) { UserEmailVerification::userEmailSend($user); - throw new ApiException('您的账号已注册过,请验证邮箱', ['code' => 'email']); + throw new ApiException('您的帐号已注册过,请验证邮箱', ['code' => 'email']); } throw new ApiException('邮箱地址已存在'); } - //密码 + // 密码 self::passwordPolicy($password); - //开始注册 - $encrypt = Base::generatePassword(6); - $inArray = [ - 'encrypt' => $encrypt, - 'email' => $email, - 'password' => Base::md52($password, $encrypt), - 'created_ip' => Base::getIp(), - ]; + // 开始注册 + $user = Doo::userCreate($email, $password); if ($other) { - $inArray = array_merge($inArray, $other); + $user->updateInstance($other); + } + $user->az = Base::getFirstCharter($user->nickname); + $user->pinyin = Base::cn2pinyin($user->nickname); + $user->created_ip = Base::getIp(); + if ($user->save()) { + $setting = Base::setting('system'); + $reg_identity = $setting['reg_identity'] ?: 'normal'; + $all_group_autoin = $setting['all_group_autoin'] ?: 'yes'; + // 注册临时身份 + if ($reg_identity === 'temp') { + $user->identity = Base::arrayImplode(array_merge(array_diff($user->identity, ['temp']), ['temp'])); + $user->save(); + } + // 加入全员群组 + if ($all_group_autoin === 'yes') { + $dialog = WebSocketDialog::whereGroupType('all')->orderByDesc('id')->first(); + $dialog?->joinGroup($user->userid, 0); + } } - $user = User::createInstance($inArray); - $user->save(); - User::AZUpdate($user->userid); return $user->find($user->userid); } - /** - * 邮箱获取userid - * @param $email - * @return int - */ - public static function email2userid($email) - { - if (empty($email)) { - return 0; - } - return intval(self::whereEmail($email)->value('userid')); - } - - /** - * token获取会员userid - * @return int - */ - public static function token2userid() - { - return self::authFind('userid', Base::getToken()); - } - - /** - * token获取会员邮箱 - * @return int - */ - public static function token2email() - { - return self::authFind('email', Base::getToken()); - } - - /** - * token获取encrypt - * @return mixed|string - */ - public static function token2encrypt() - { - return self::authFind('encrypt', Base::getToken()); - } - - /** - * 获取token身份信息 - * @param $find - * @param null $token - * @return array|mixed|string - */ - public static function authFind($find, $token = null) - { - if ($token === null) { - $token = Base::getToken(); - } - list($userid, $email, $encrypt, $timestamp) = explode("#$", base64_decode($token) . "#$#$#$#$"); - $array = [ - 'userid' => intval($userid), - 'email' => $email ?: '', - 'encrypt' => $encrypt ?: '', - 'timestamp' => intval($timestamp), - ]; - if (isset($array[$find])) { - return $array[$find]; - } - if ($find == 'all') { - return $array; - } - return ''; - } - /** * 获取我的ID * @return int @@ -316,8 +355,7 @@ class User extends AbstractModel { $user = self::authInfo(); if (!$user) { - $authorization = Base::getToken(); - if ($authorization) { + if (Base::headerOrInput('token')) { throw new ApiException('身份已失效,请重新登录', [], -1); } else { throw new ApiException('请登录后继续...', [], -1); @@ -342,57 +380,49 @@ class User extends AbstractModel if (isset($_A["__static_auth"])) { return $_A["__static_auth"]; } - $authorization = Base::getToken(); - if ($authorization) { - $authInfo = self::authFind('all', $authorization); - if ($authInfo['userid'] > 0) { - $loginValid = floatval(Base::settingFind('system', 'loginValid')) ?: 720; - $loginValid *= 3600; - if ($authInfo['timestamp'] + $loginValid > time()) { - $row = self::whereUserid($authInfo['userid'])->whereEmail($authInfo['email'])->whereEncrypt($authInfo['encrypt'])->first(); - if ($row) { - $upArray = []; - if (Base::getIp() && $row->line_ip != Base::getIp()) { - $upArray['line_ip'] = Base::getIp(); - } - if (Carbon::parse($row->line_at)->addSeconds(30)->lt(Carbon::now())) { - $upArray['line_at'] = Carbon::now(); - } - if ($upArray) { - $row->updateInstance($upArray); - $row->save(); - } - return $_A["__static_auth"] = $row; - } - } + if (Doo::userId() > 0 + && !Doo::userExpired() + && $user = self::whereUserid(Doo::userId())->whereEmail(Doo::userEmail())->whereEncrypt(Doo::userEncrypt())->first()) { + $upArray = []; + if (Base::getIp() && $user->line_ip != Base::getIp()) { + $upArray['line_ip'] = Base::getIp(); } + if (Carbon::parse($user->line_at)->addSeconds(30)->lt(Carbon::now())) { + $upArray['line_at'] = Carbon::now(); + } + if ($upArray) { + $user->updateInstance($upArray); + $user->save(); + } + return $_A["__static_auth"] = $user; } return $_A["__static_auth"] = false; } /** - * 生成token + * 生成 token * @param self $userinfo + * @param bool $refresh 获取新的token * @return string */ - public static function token($userinfo) + public static function generateToken($userinfo, $refresh = false) { - $userinfo->token = base64_encode($userinfo->userid . '#$' . $userinfo->email . '#$' . $userinfo->encrypt . '#$' . time() . '#$' . Base::generatePassword(6)); + if (!$refresh) { + if (Doo::userId() != $userinfo->userid + || Doo::userEmail() != $userinfo->email + || Doo::userEncrypt() != $userinfo->encrypt) { + $refresh = true; + } + } + if ($refresh) { + $days = $userinfo->bot ? 0 : max(1, intval(Base::settingFind('system', 'token_valid_days', 15))); + $token = Doo::tokenEncode($userinfo->userid, $userinfo->email, $userinfo->encrypt, $days); + } else { + $token = Doo::userToken(); + } unset($userinfo->encrypt); unset($userinfo->password); - return $userinfo->token; - } - - /** - * 判断用户权限(身份) - * @param $identity - * @param $userIdentity - * @return bool - */ - public static function identityRaw($identity, $userIdentity) - { - $userIdentity = is_array($userIdentity) ? $userIdentity : explode(",", trim($userIdentity, ",")); - return $identity && in_array($identity, $userIdentity); + return $userinfo->token = $token; } /** @@ -410,10 +440,10 @@ class User extends AbstractModel if (isset($_A["__static_userid2basic_" . $userid])) { return $_A["__static_userid2basic_" . $userid]; } - $fields = ['userid', 'email', 'nickname', 'profession', 'userimg']; - $userInfo = self::whereUserid($userid)->select($fields)->first(); + $userInfo = self::whereUserid($userid)->select(User::$basicField)->first(); if ($userInfo) { $userInfo->online = $userInfo->getOnlineStatus(); + $userInfo->department_name = $userInfo->getDepartmentName(); } return $_A["__static_userid2basic_" . $userid] = ($userInfo ?: []); } @@ -426,21 +456,7 @@ class User extends AbstractModel */ public static function userid2nickname($userid) { - $basic = self::userid2basic($userid); - return $basic ? $basic->nickname : ''; - } - - /** - * 更新首字母 - * @param $userid - */ - public static function AZUpdate($userid) - { - $row = self::whereUserid($userid)->first(); - if ($row) { - $row->az = Base::getFirstCharter($row->nickname); - $row->save(); - } + return self::userid2basic($userid)?->nickname ?: ''; } /** @@ -467,6 +483,42 @@ class User extends AbstractModel } } + /** + * 获取头像 + * @param $userid + * @param $userimg + * @param $email + * @param $nickname + * @return string + */ + public static function getAvatar($userid, $userimg, $email, $nickname) + { + // 自定义头像 + if ($userimg && !str_contains($userimg, 'avatar/')) { + return Base::fillUrl($userimg); + } + // 机器人头像 + switch ($email) { + case 'system-msg@bot.system': + return url("images/avatar/default_system.png"); + case 'task-alert@bot.system': + return url("images/avatar/default_task.png"); + case 'check-in@bot.system': + return url("images/avatar/default_checkin.png"); + case 'anon-msg@bot.system': + return url("images/avatar/default_anon.png"); + case 'bot-manager@bot.system': + return url("images/avatar/default_bot.png"); + } + // 生成文字头像 + if (self::$defaultAvatarMode === 'auto') { + return url("avatar/" . urlencode($nickname) . ".png"); + } + // 系统默认头像 + $name = ($userid - 1) % 21 + 1; + return url("images/avatar/default_{$name}.png"); + } + /** * 检测密码策略是否符合 * @param $password @@ -497,4 +549,60 @@ class User extends AbstractModel } } } + + /** + * 获取机器人或创建 + * @param $key + * @param $update + * @param $userid + * @return self|null + */ + public static function botGetOrCreate($key, $update = [], $userid = 0) + { + $email = "{$key}@bot.system"; + $botUser = self::whereEmail($email)->first(); + if (empty($botUser)) { + $botUser = Doo::userCreate($email, Base::generatePassword(32)); + if (empty($botUser)) { + return null; + } + $botUser->updateInstance([ + 'created_ip' => Base::getIp(), + ]); + $botUser->save(); + if ($userid > 0) { + UserBot::createInstance([ + 'userid' => $userid, + 'bot_id' => $botUser->userid, + ])->save(); + } + // + switch ($key) { + case 'system-msg': + $update['nickname'] = '系统消息'; + break; + case 'task-alert': + $update['nickname'] = '任务提醒'; + break; + case 'check-in': + $update['nickname'] = '签到打卡'; + break; + case 'anon-msg': + $update['nickname'] = '匿名消息'; + break; + case 'bot-manager': + $update['nickname'] = '机器人管理'; + break; + } + } + if ($update) { + $botUser->updateInstance($update); + if (isset($update['nickname'])) { + $botUser->az = Base::getFirstCharter($botUser->nickname); + $botUser->pinyin = Base::cn2pinyin($botUser->nickname); + } + $botUser->save(); + } + return $botUser; + } } diff --git a/app/Models/UserBot.php b/app/Models/UserBot.php new file mode 100644 index 000000000..9444711fc --- /dev/null +++ b/app/Models/UserBot.php @@ -0,0 +1,127 @@ + [ + [ + 'key' => 'checkin', + 'label' => Doo::translate('我要签到') + ], [ + 'key' => 'it', + 'label' => Doo::translate('IT资讯') + ], [ + 'key' => '36ke', + 'label' => Doo::translate('36氪') + ], [ + 'key' => '60s', + 'label' => Doo::translate('60s读世界') + ], [ + 'key' => 'joke', + 'label' => Doo::translate('开心笑话') + ], [ + 'key' => 'soup', + 'label' => Doo::translate('心灵鸡汤') + ] + ], + 'anon-msg@bot.system' => [ + [ + 'key' => 'help', + 'label' => Doo::translate('使用说明') + ], [ + 'key' => 'privacy', + 'label' => Doo::translate('隐私说明') + ], + ], + 'bot-manager@bot.system' => [ + [ + 'key' => '/help', + 'label' => Doo::translate('帮助指令') + ], [ + 'key' => '/api', + 'label' => Doo::translate('Api接口文档') + ], [ + 'key' => '/list', + 'label' => Doo::translate('我的机器人') + ], + ], + default => [], + }; + + } + + /** + * 签到机器人 + * @param $command + * @param $userid + * @return string + */ + public static function checkinBotQuickMsg($command, $userid) + { + if (Cache::get("UserBot::checkinBotQuickMsg:{$userid}") === "yes") { + return "操作频繁!"; + } + Cache::put("UserBot::checkinBotQuickMsg:{$userid}", "yes", Carbon::now()->addSecond()); + // + $text = match ($command) { + "checkin" => "暂未开放手动签到。", + default => Extranet::checkinBotQuickMsg($command), + }; + return $text ?: '维护中...'; + } + + /** + * 隐私机器人 + * @param $command + * @return string + */ + public static function anonBotQuickMsg($command) + { + return match ($command) { + "help" => "使用说明:打开你想要发匿名消息的个人对话,点击输入框右边的 ⊕ 号,选择 匿名消息 即可输入你想要发送的匿名消息内容。", + "privacy" => "匿名消息将通过 匿名消息(机器人) 发送给对方,不会记录你的身份信息。", + default => '', + }; + } +} diff --git a/app/Models/UserCheckinMac.php b/app/Models/UserCheckinMac.php new file mode 100644 index 000000000..7b2288e50 --- /dev/null +++ b/app/Models/UserCheckinMac.php @@ -0,0 +1,66 @@ +where('userid', '!=', $userid)->exists()) { + throw new ApiException("{$item['mac']} 已被其他成员设置"); + } + $update = []; + if ($item['remark']) { + $update = [ + 'remark' => $item['remark'] + ]; + } + $row = self::updateInsert([ + 'userid' => $userid, + 'mac' => $item['mac'] + ], $update); + if ($row) { + $ids[] = $row->id; + $list[] = $row; + } + } + self::whereUserid($userid)->whereNotIn('id', $ids)->delete(); + // + return Base::retSuccess('修改成功', $list); + }); + } +} diff --git a/app/Models/UserCheckinRecord.php b/app/Models/UserCheckinRecord.php new file mode 100644 index 000000000..86967f781 --- /dev/null +++ b/app/Models/UserCheckinRecord.php @@ -0,0 +1,134 @@ +whereBetween('created_at', $betweenTimes)->orderBy('id')->get(); + /** @var self $record */ + foreach ($records as $record) { + $times = array_map(function ($time) { + return preg_replace("/(\d+):(\d+):\d+$/", "$1:$2", $time); + }, $record->times); + if (isset($array[$record->date])) { + $array[$record->date] = array_merge($array[$record->date], $times); + } else { + $array[$record->date] = $times; + } + } + // + foreach ($array as $date => $times) { + $times = array_values(array_filter(array_unique($times))); + $inOrder = []; + foreach ($times as $key => $time) { + $inOrder[$key] = strtotime("2022-01-01 {$time}"); + } + array_multisort($inOrder, SORT_ASC, $times); + $array[$date] = $times; + } + // + return $array; + } + + /** + * 时间收集 + * @param string $data + * @param array $times + * @return \Illuminate\Support\Collection + */ + public static function atCollect($data, $times) + { + $sameTimes = array_map(function($time) use ($data) { + return [ + "datetime" => "{$data} {$time}", + "timestamp" => strtotime("{$data} {$time}") + ]; + }, $times); + return collect($sameTimes); + } + + /** + * 签到时段 + * @param array $times + * @param int $diff 多长未签到算失效(秒) + * @return array + */ + public static function atSection($times, $diff = 3600) + { + $start = ""; + $end = ""; + $array = []; + foreach ($times as $time) { + $time = preg_replace("/(\d+):(\d+):\d+$/", "$1:$2", $time); + if (empty($start)) { + $start = $time; + continue; + } + if (empty($end)) { + $end = $time; + continue; + } + if (strtotime("2022-01-01 {$time}") - strtotime("2022-01-01 {$end}") > $diff) { + $array[] = [$start, $end]; + $start = $time; + $end = ""; + continue; + } + $end = $time; + } + if ($start) { + $array[] = [$start, $end]; + } + return $array; + } +} diff --git a/app/Models/UserDelete.php b/app/Models/UserDelete.php new file mode 100644 index 000000000..71c9ec395 --- /dev/null +++ b/app/Models/UserDelete.php @@ -0,0 +1,71 @@ +first(); + if (empty($row) || empty($row->cache)) { + return null; + } + $cache = $row->cache; + $cache = array_intersect_key($cache, array_flip(array_merge(User::$basicField, ['department_name']))); + $cache['delete_at'] = $row->created_at->format($row->dateFormat ?: 'Y-m-d H:i:s'); + return $cache; + } +} diff --git a/app/Models/UserDepartment.php b/app/Models/UserDepartment.php new file mode 100644 index 000000000..e6349f991 --- /dev/null +++ b/app/Models/UserDepartment.php @@ -0,0 +1,152 @@ +owner_userid) { + $oldUser = User::find($this->owner_userid); + $newUser = User::find($data['owner_userid']); + } + $this->updateInstance($data); + // + if ($this->dialog_id > 0) { + // 已有群 + $dialog = WebSocketDialog::find($this->dialog_id); + if ($dialog) { + $dialog->name = $this->name; + $dialog->owner_id = $this->owner_userid; + if ($dialog->save()) { + $dialog->joinGroup($this->owner_userid, 0, true); + $dialog->pushMsg("groupUpdate", [ + 'id' => $dialog->id, + 'name' => $dialog->name, + 'owner_id' => $dialog->owner_id, + ]); + } + } + } elseif ($dialogUseid > 0) { + // 使用现有群 + $dialog = WebSocketDialog::whereType('group')->whereGroupType('user')->find($dialogUseid); + if (empty($dialog)) { + throw new ApiException("选择现有聊天群不存在"); + } + $dialog->name = $this->name; + $dialog->owner_id = $this->owner_userid; + $dialog->group_type = 'department'; + if ($dialog->save()) { + $dialog->joinGroup($this->owner_userid, 0, true); + $dialog->pushMsg("groupUpdate", [ + 'id' => $dialog->id, + 'name' => $dialog->name, + 'owner_id' => $dialog->owner_id, + 'group_type' => $dialog->group_type, + ]); + WebSocketDialogMsg::sendMsg(null, $dialog->id, 'notice', [ + 'notice' => User::nickname() . " 将此群改为部门群" + ], User::userid(), true, true); + } + $this->dialog_id = $dialog->id; + } else { + // 创建群 + $dialog = WebSocketDialog::createGroup($this->name, [$this->owner_userid], 'department', $this->owner_userid); + if (empty($dialog)) { + throw new ApiException("创建群组失败"); + } + $this->dialog_id = $dialog->id; + } + $this->save(); + // + if ($oldUser) { + $oldUser->department = array_diff($oldUser->department, [$this->id]); + $oldUser->department = "," . implode(",", $oldUser->department) . ","; + $oldUser->save(); + } + if ($newUser) { + $newUser->department = array_diff($newUser->department, [$this->id]); + $newUser->department = array_merge($newUser->department, [$this->id]); + $newUser->department = "," . implode(",", $newUser->department) . ","; + $newUser->save(); + } + }); + } + + /** + * 删除部门 + * @return void + */ + public function deleteDepartment() { + // 删除子部门 + $list = self::whereParentId($this->id)->get(); + foreach ($list as $item) { + $item->deleteDepartment(); + } + // 移出成员 + User::where("department", "like", "%,{$this->id},%")->chunk(100, function($items) { + /** @var User $user */ + foreach ($items as $user) { + $user->department = array_diff($user->department, [$this->id]); + $user->department = "," . implode(",", $user->department) . ","; + $user->save(); + } + }); + // 解散群组 + $dialog = WebSocketDialog::find($this->dialog_id); + if ($dialog) { + $dialog->deleteDialog(); + } + // + $this->delete(); + } + + /** + * 移交部门身份 + * @param $originalUserid + * @param $newUserid + * @return void + */ + public static function transfer($originalUserid, $newUserid) + { + self::whereOwnerUserid($originalUserid)->chunkById(100, function ($list) use ($originalUserid, $newUserid) { + /** @var self $item */ + foreach ($list as $item) { + $item->saveDepartment([ + 'owner_userid' => $newUserid, + ]); + } + }); + } +} diff --git a/app/Models/UserEmailVerification.php b/app/Models/UserEmailVerification.php index 5adfb80d8..221836a1b 100644 --- a/app/Models/UserEmailVerification.php +++ b/app/Models/UserEmailVerification.php @@ -16,6 +16,7 @@ use Guanguans\Notify\Messages\EmailMessage; * @property string|null $code 验证参数 * @property string|null $email 电子邮箱 * @property int|null $status 0-未验证,1-已验证 + * @property int|null $type 邮件类型:1-邮箱认证,2-修改邮箱 * @property \Illuminate\Support\Carbon|null $created_at * @property \Illuminate\Support\Carbon|null $updated_at * @method static \Illuminate\Database\Eloquent\Builder|UserEmailVerification newModelQuery() @@ -26,6 +27,7 @@ use Guanguans\Notify\Messages\EmailMessage; * @method static \Illuminate\Database\Eloquent\Builder|UserEmailVerification whereEmail($value) * @method static \Illuminate\Database\Eloquent\Builder|UserEmailVerification whereId($value) * @method static \Illuminate\Database\Eloquent\Builder|UserEmailVerification whereStatus($value) + * @method static \Illuminate\Database\Eloquent\Builder|UserEmailVerification whereType($value) * @method static \Illuminate\Database\Eloquent\Builder|UserEmailVerification whereUpdatedAt($value) * @method static \Illuminate\Database\Eloquent\Builder|UserEmailVerification whereUserid($value) * @mixin \Eloquent @@ -36,38 +38,54 @@ class UserEmailVerification extends AbstractModel /** * 发验证邮箱 * @param User $user + * @param int $type + * @param null $email */ - public static function userEmailSend(User $user) + public static function userEmailSend(User $user, $type = 1, $email = null) { - $res = self::whereUserid($user->userid)->where('created_at', '>', Carbon::now()->subMinutes(30))->first(); - if ($res) return; + $email = $type == 1 ? $user->email : $email; + $res = self::whereEmail($email)->where('created_at', '>', Carbon::now()->subMinutes(30))->whereType($type)->first(); + if ($res && $type == 1) return; //删除 - self::whereUserid($user->userid)->delete(); - $userEmailVerification = self::createInstance([ + self::whereUserid($email)->delete(); + $code = $type == 1 ? Base::generatePassword(64) : rand(100000, 999999); + $row = self::createInstance([ 'userid' => $user->userid, - 'email' => $user->email, - 'code' => Base::generatePassword(64), + 'email' => $email, + 'code' => $code, 'status' => 0, + 'type' => $type ]); - $userEmailVerification->save(); - + $row->save(); $setting = Base::setting('emailSetting'); - $url = Base::fillUrl('single/valid/email') . '?code=' . $userEmailVerification->code; try { - if (!Base::isEmail($user->email)) { - throw new \Exception("User email '{$user->email}' address error"); + if (!Base::isEmail($email)) { + throw new \Exception("User email '{$email}' address error"); + } + switch ($type) { + case 2: + $subject = env('APP_NAME') . "修改邮箱验证"; + $content = "

    {$user->nickname} 您好,您正在修改 " . env('APP_NAME') . " 的邮箱,验证码如下。请在30分钟内输入验证码

    $code

    如果不是本人操作,您的帐号可能存在风险,请及时修改密码!

    "; + break; + case 3: + $subject = env('APP_NAME') . "注销帐号验证"; + $content = "

    {$user->nickname} 您好,您正在注销 " . env('APP_NAME') . " 的帐号,验证码如下。请在30分钟内输入验证码

    $code

    如果不是本人操作,您的帐号可能存在风险,请及时修改密码!

    "; + break; + default: + $url = Base::fillUrl('single/valid/email') . '?code=' . $row->code; + $subject = env('APP_NAME') . "绑定邮箱验证"; + $content = "

    {$user->nickname} 您好,您正在绑定 " . env('APP_NAME') . " 的邮箱,请于30分钟之内点击以下链接完成验证 :

    {$url}

    "; + break; } - $subject = env('APP_NAME') . " 绑定邮箱验证"; - $content = "

    {$user->nickname} 您好,您正在绑定 " . env('APP_NAME') . " 的邮箱,请于30分钟之内点击以下链接完成验证 :

    {$url}

    "; Factory::mailer() ->setDsn("smtp://{$setting['account']}:{$setting['password']}@{$setting['smtp_server']}:{$setting['port']}?verify_peer=0") ->setMessage(EmailMessage::create() ->from(env('APP_NAME', 'Task') . " <{$setting['account']}>") - ->to($user->email) + ->to($email) ->subject($subject) ->html($content)) ->send(); - } catch (\Exception $e) { + } catch (\Throwable $e) { if (str_contains($e->getMessage(), "Timed Out")) { throw new ApiException("language.TimedOut"); } elseif ($e->getCode() === 550) { @@ -77,4 +95,39 @@ class UserEmailVerification extends AbstractModel } } } + + /** + * 校验验证码 + * @param $email + * @param $code + * @param int $type + * @return bool + */ + public static function verify($email, $code, $type = 1) + { + if (!$code) { + throw new ApiException('请输入验证码'); + } + /** @var UserEmailVerification $emailVerify */ + $emailVerify = self::whereEmail($email)->whereType($type)->orderByDesc('id')->first(); + + if (empty($emailVerify) || $emailVerify->code != $code) { + throw new ApiException('验证码错误'); + } + + $oldTime = Carbon::parse($emailVerify->created_at)->timestamp; + $time = Base::Time(); + + // 30分钟失效 + if (abs($time - $oldTime) > 1800) { + throw new ApiException('验证码已失效'); + } + + self::whereEmail($email)->whereCode($code)->whereType($type)->update([ + 'status' => 1 + ]); + + return true; + } + } diff --git a/app/Models/UserTransfer.php b/app/Models/UserTransfer.php new file mode 100644 index 000000000..7dd11d222 --- /dev/null +++ b/app/Models/UserTransfer.php @@ -0,0 +1,71 @@ +original_userid, $this->new_userid); + // 移交项目身份 + ProjectUser::transfer($this->original_userid, $this->new_userid); + // 移交任务身份 + ProjectTaskUser::transfer($this->original_userid, $this->new_userid); + // 移交文件 + File::transfer($this->original_userid, $this->new_userid); + // 离职移出群组 + WebSocketDialog::select(['web_socket_dialogs.*']) + ->join('web_socket_dialog_users as u', 'web_socket_dialogs.id', '=', 'u.dialog_id') + ->where('web_socket_dialogs.type', 'group') + ->where('u.userid', $this->original_userid) + ->orderByDesc('web_socket_dialogs.id') + ->chunk(100, function($list) { + /** @var WebSocketDialog $dialog */ + foreach ($list as $dialog) { + // 离职员工退出群 + $dialog->exitGroup($this->original_userid, 'remove', false, false); + if ($dialog->owner_id === $this->original_userid) { + // 如果是群主则把交接人设为群主 + $dialog->owner_id = $this->new_userid; + if ($dialog->save()) { + $dialog->joinGroup($this->new_userid, 0); + $dialog->pushMsg("groupUpdate", [ + 'id' => $dialog->id, + 'owner_id' => $dialog->owner_id, + ]); + } + } + } + }); + } +} diff --git a/app/Models/WebSocketDialog.php b/app/Models/WebSocketDialog.php index cf8d7bc80..f4ebd19bf 100644 --- a/app/Models/WebSocketDialog.php +++ b/app/Models/WebSocketDialog.php @@ -3,7 +3,12 @@ namespace App\Models; use App\Exceptions\ApiException; +use App\Module\Base; +use App\Module\Doo; +use App\Tasks\PushTask; +use Cache; use Carbon\Carbon; +use Hhxsv5\LaravelS\Swoole\Task\Task; use Illuminate\Database\Eloquent\SoftDeletes; /** @@ -13,7 +18,9 @@ use Illuminate\Database\Eloquent\SoftDeletes; * @property string|null $type 对话类型 * @property string|null $group_type 聊天室类型 * @property string|null $name 对话名称 + * @property string $avatar 头像(群) * @property string|null $last_at 最后消息时间 + * @property int|null $owner_id 群主用户ID * @property \Illuminate\Support\Carbon|null $created_at * @property \Illuminate\Support\Carbon|null $updated_at * @property \Illuminate\Support\Carbon|null $deleted_at @@ -23,12 +30,14 @@ use Illuminate\Database\Eloquent\SoftDeletes; * @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialog newQuery() * @method static \Illuminate\Database\Query\Builder|WebSocketDialog onlyTrashed() * @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialog query() + * @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialog whereAvatar($value) * @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialog whereCreatedAt($value) * @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialog whereDeletedAt($value) * @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialog whereGroupType($value) * @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialog whereId($value) * @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialog whereLastAt($value) * @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialog whereName($value) + * @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialog whereOwnerId($value) * @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialog whereType($value) * @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialog whereUpdatedAt($value) * @method static \Illuminate\Database\Query\Builder|WebSocketDialog withTrashed() @@ -39,6 +48,16 @@ class WebSocketDialog extends AbstractModel { use SoftDeletes; + /** + * 头像地址 + * @param $value + * @return string + */ + public function getAvatarAttribute($value) + { + return $value ? Base::fillUrl($value) : $value; + } + /** * @return \Illuminate\Database\Eloquent\Relations\HasMany */ @@ -47,6 +66,236 @@ class WebSocketDialog extends AbstractModel return $this->hasMany(WebSocketDialogUser::class, 'dialog_id', 'id'); } + /** + * 格式化对话 + * @param int $userid 会员ID + * @param bool $hasData + * @return $this + */ + public function formatData($userid, $hasData = false) + { + $dialogUserFun = function ($key, $default = null) use ($userid) { + $data = Cache::remember("Dialog::formatData-{$this->id}-{$userid}", now()->addSeconds(10), function () use ($userid) { + return WebSocketDialogUser::whereDialogId($this->id)->whereUserid($userid)->first()?->toArray(); + }); + return $data[$key] ?? $default; + }; + // + $time = Carbon::parse($this->user_at ?? $dialogUserFun('updated_at')); + $this->top_at = $this->top_at ?? $dialogUserFun('top_at'); + $this->user_at = $time->toDateTimeString('millisecond'); + $this->user_ms = $time->valueOf(); + // + if (isset($this->search_msg_id)) { + // 最后消息 (搜索预览消息) + $this->last_msg = WebSocketDialogMsg::whereDialogId($this->id)->find($this->search_msg_id); + $this->last_at = $this->last_msg?->created_at; + } else { + // 未读信息 + $this->generateUnread($userid, $hasData); + // 未读标记 + $this->mark_unread = $this->mark_unread ?? $dialogUserFun('mark_unread'); + // 是否免打扰 + $this->silence = $this->silence ?? $dialogUserFun('silence'); + // 对话人数 + $this->people = WebSocketDialogUser::whereDialogId($this->id)->count(); + // 有待办 + $this->todo_num = WebSocketDialogMsgTodo::whereDialogId($this->id)->whereUserid($userid)->whereDoneAt(null)->count(); + // 最后消息 + $this->last_msg = WebSocketDialogMsg::whereDialogId($this->id)->orderByDesc('id')->first(); + } + // 对方信息 + $this->pinyin = Base::cn2pinyin($this->name); + $this->quick_msgs = []; + $this->dialog_user = null; + $this->group_info = null; + $this->bot = 0; + switch ($this->type) { + case "user": + $dialog_user = WebSocketDialogUser::whereDialogId($this->id)->where('userid', '!=', $userid)->first(); + if ($dialog_user->userid === 0) { + $dialog_user->userid = $userid; + } + $basic = User::userid2basic($dialog_user->userid); + if ($basic) { + $this->name = $basic->nickname; + $this->bot = $basic->bot; + $this->quick_msgs = UserBot::quickMsgs($basic->email); + } else { + $this->name = 'non-existent'; + $this->dialog_delete = 1; + } + $this->dialog_user = $dialog_user; + break; + case "group": + switch ($this->group_type) { + case 'project': + $this->group_info = Project::withTrashed()->select(['id', 'name', 'archived_at', 'deleted_at'])->whereDialogId($this->id)->first()?->cancelAppend()->cancelHidden(); + if ($this->group_info) { + $this->name = $this->group_info->name; + } else { + $this->name = '[Delete]'; + $this->dialog_delete = 1; + } + break; + case 'task': + $this->group_info = ProjectTask::withTrashed()->select(['id', 'name', 'complete_at', 'archived_at', 'deleted_at'])->whereDialogId($this->id)->first()?->cancelAppend()->cancelHidden(); + if ($this->group_info) { + $this->name = $this->group_info->name; + } else { + $this->name = '[Delete]'; + $this->dialog_delete = 1; + } + break; + case 'all': + $this->name = Doo::translate('全体成员'); + $this->all_group_mute = Base::settingFind('system', 'all_group_mute'); + break; + } + break; + } + if ($hasData === true) { + $msgBuilder = WebSocketDialogMsg::whereDialogId($this->id); + $this->has_tag = $msgBuilder->clone()->where('tag', '>', 0)->exists(); + $this->has_image = $msgBuilder->clone()->whereMtype('image')->exists(); + $this->has_file = $msgBuilder->clone()->whereMtype('file')->exists(); + $this->has_link = $msgBuilder->clone()->whereLink(1)->exists(); + } + return $this; + } + + /** + * 生成未读数据 + * @param $userid + * @param $positionData + * @return $this + */ + public function generateUnread($userid, $positionData = false) + { + $builder = WebSocketDialogMsgRead::whereDialogId($this->id)->whereUserid($userid)->whereReadAt(null); + $this->unread = $builder->count(); + $this->mention = $this->unread > 0 ? $builder->clone()->whereMention(1)->count() : 0; + if ($positionData) { + $array = []; + // @我的消息 + if ($this->mention > 0 + && $mention_id = intval($builder->clone()->whereMention(1)->orderByDesc('msg_id')->value('msg_id'))) { + $array[] = [ + 'msg_id' => $mention_id, + 'label' => Doo::translate('@我的消息'), + ]; + } + // 最早一条未读消息 + if ($this->unread > 0 + && $first_id = intval($builder->clone()->orderBy('msg_id')->value('msg_id'))) { + $array[] = [ + 'msg_id' => $first_id, + 'label' => '{UNREAD}' + ]; + } + // + $this->position_msgs = $array; + } + return $this; + } + + /** + * 加入聊天室 + * @param int|array $userid 加入的会员ID或会员ID组 + * @param int $inviter 邀请人 + * @param bool|null $important 重要人员(null不修改、bool修改) + * @return bool + */ + public function joinGroup($userid, $inviter, $important = null) + { + AbstractModel::transaction(function () use ($important, $inviter, $userid) { + foreach (is_array($userid) ? $userid : [$userid] as $value) { + if ($value > 0) { + $updateData = [ + 'inviter' => $inviter, + ]; + if (is_bool($important)) { + $updateData['important'] = $important ? 1 : 0; + } + $isInsert = false; + WebSocketDialogUser::updateInsert([ + 'dialog_id' => $this->id, + 'userid' => $value, + ], $updateData, [], $isInsert); + if ($isInsert) { + WebSocketDialogMsg::sendMsg(null, $this->id, 'notice', [ + 'notice' => User::userid2nickname($value) . " 已加入群组" + ], $inviter, true, true); + } + } + } + }); + $this->pushMsg("groupUpdate", [ + 'id' => $this->id, + 'people' => WebSocketDialogUser::whereDialogId($this->id)->count() + ]); + return true; + } + + /** + * 退出聊天室 + * @param int|array $userid 退出的会员ID或会员ID组 + * @param string $type exit|remove + * @param bool $checkDelete 是否检查删除 + * @param bool $pushMsg 是否推送消息 + */ + public function exitGroup($userid, $type = 'exit', $checkDelete = true, $pushMsg = true) + { + $typeDesc = $type === 'remove' ? '移出' : '退出'; + AbstractModel::transaction(function () use ($pushMsg, $checkDelete, $typeDesc, $type, $userid) { + $builder = WebSocketDialogUser::whereDialogId($this->id); + if (is_array($userid)) { + $builder->whereIn('userid', $userid); + } else { + $builder->whereUserid($userid); + } + $builder->chunkById(100, function($list) use ($pushMsg, $checkDelete, $typeDesc, $type) { + /** @var WebSocketDialogUser $item */ + foreach ($list as $item) { + if ($checkDelete) { + if ($type === 'remove') { + // 移出时:如果是全员群仅允许管理员操作,其他群仅群主或邀请人可以操作 + if ($this->group_type === 'all') { + User::auth("admin"); + } elseif (!in_array(User::userid(), [$this->owner_id, $item->inviter])) { + throw new ApiException('只有群主或邀请人可以移出成员'); + } + } + if ($item->userid == $this->owner_id) { + throw new ApiException('群主不可' . $typeDesc); + } + if ($item->important) { + throw new ApiException('部门成员、项目人员或任务人员不可' . $typeDesc); + } + } + // + $item->delete(); + // + if ($pushMsg) { + if ($type === 'remove') { + $notice = User::nickname() . " 将 " . User::userid2nickname($item->userid) . " 移出群组"; + } else { + $notice = User::userid2nickname($item->userid) . " 退出群组"; + } + WebSocketDialogMsg::sendMsg(null, $this->id, 'notice', [ + 'notice' => $notice + ], User::userid(), true, true); + } + } + }); + }); + // + $this->pushMsg("groupUpdate", [ + 'id' => $this->id, + 'people' => WebSocketDialogUser::whereDialogId($this->id)->count() + ]); + } + /** * 删除会话 * @return bool @@ -61,6 +310,7 @@ class WebSocketDialog extends AbstractModel }); $this->delete(); }); + $this->pushMsg("groupDelete"); return true; } @@ -68,18 +318,127 @@ class WebSocketDialog extends AbstractModel * 还原会话 * @return bool */ - public function recoveryDialog() + public function restoreDialog() { $this->restore(); + $this->pushMsg("groupRestore"); return true; } + /** + * 检查群组类型 + * @param string|array|null $groupType + * @return void + */ + public function checkGroup($groupType = null) + { + if ($this->type !== 'group') { + throw new ApiException('仅限群组操作'); + } + if ($groupType) { + $groupTypes = is_array($groupType) ? $groupType : [$groupType]; + if (!in_array($this->group_type, $groupTypes)) { + throw new ApiException('操作的群组类型错误'); + } + } + } + + /** + * 检查禁言 + * @param $userid + * @return void + */ + public function checkMute($userid) + { + if ($this->group_type === 'all') { + $allGroupMute = Base::settingFind('system', 'all_group_mute'); + switch ($allGroupMute) { + case 'all': + throw new ApiException('当前会话全员禁言'); + case 'user': + if (!User::find($userid)?->checkAdmin()) { + throw new ApiException('当前会话禁言'); + } + } + } + } + + /** + * 获取群组名称 + * @return mixed|string|null + */ + public function getGroupName() + { + if (!isset($this->appendattrs['groupName'])) { + $name = $this->name; + if ($this->type == "group") { + switch ($this->group_type) { + case 'project': + $name = \DB::table('projects')->where('dialog_id', $this->id)->value('name'); + break; + case 'task': + $name = \DB::table('project_tasks')->where('dialog_id', $this->id)->value('name'); + break; + case 'all': + $name = Doo::translate('全体成员'); + break; + } + } + $this->appendattrs['groupName'] = $name; + } + return $this->appendattrs['groupName']; + } + + /** + * 推送消息 + * @param $action + * @param array $data 发送内容,默认为[id=>会话ID] + * @param array $userid 指定会员,默认为群组所有成员 + * @return void + */ + public function pushMsg($action, $data = null, $userid = null) + { + if ($data === null) { + $data = ['id' => $this->id]; + } + // + if ($userid === null) { + $userid = $this->dialogUser->pluck('userid')->toArray(); + } + // + $params = [ + 'userid' => $userid, + 'msg' => [ + 'type' => 'dialog', + 'mode' => $action, + 'data' => $data, + ] + ]; + $task = new PushTask($params, false); + Task::deliver($task); + } + + /** + * 更新对话最后消息时间 + * @return WebSocketDialogMsg|\Illuminate\Database\Eloquent\Builder|\Illuminate\Database\Eloquent\Model|\Illuminate\Database\Query\Builder|object|null + */ + public function updateMsgLastAt() + { + $lastMsg = WebSocketDialogMsg::whereDialogId($this->id)->orderByDesc('id')->first(); + if ($lastMsg) { + $this->last_at = $lastMsg->created_at; + $this->save(); + } + return $lastMsg; + } + /** * 获取对话(同时检验对话身份) * @param $dialog_id + * @param bool|string $checkOwner 是否校验群组身份,'auto'时有群主为true无群主为false * @return self */ - public static function checkDialog($dialog_id) + public static function checkDialog($dialog_id, $checkOwner = false) { $dialog = WebSocketDialog::find($dialog_id); if (empty($dialog)) { @@ -87,7 +446,14 @@ class WebSocketDialog extends AbstractModel } // $userid = User::userid(); - if ($dialog->type === 'group' && $dialog->group_type === 'task') { + if ($checkOwner === 'auto') { + $checkOwner = $dialog->owner_id > 0; + } + if ($checkOwner === true && $dialog->owner_id != $userid) { + throw new ApiException('仅限群主操作'); + } + // + if ($dialog->group_type === 'task') { // 任务群对话校验是否在项目内 $project_id = intval(ProjectTask::whereDialogId($dialog->id)->value('project_id')); if ($project_id > 0) { @@ -97,50 +463,7 @@ class WebSocketDialog extends AbstractModel } } if (!WebSocketDialogUser::whereDialogId($dialog->id)->whereUserid($userid)->exists()) { - throw new ApiException('不在成员列表内'); - } - return $dialog; - } - - /** - * 格式化对话 - * @param WebSocketDialog $dialog - * @param int $userid 会员ID - * @return self|null - */ - public static function formatData(WebSocketDialog $dialog, $userid) - { - if (empty($dialog)) { - return null; - } - // 最后消息 - $last_msg = WebSocketDialogMsg::whereDialogId($dialog->id)->orderByDesc('id')->first(); - $dialog->last_msg = $last_msg; - // 未读信息 - $dialog->unread = WebSocketDialogMsgRead::whereDialogId($dialog->id)->whereUserid($userid)->whereReadAt(null)->count(); - $dialog->mark_unread = $dialog->mark_unread ?? WebSocketDialogUser::whereDialogId($dialog->id)->whereUserid($userid)->value('mark_unread'); - // 对话人数 - $builder = WebSocketDialogUser::whereDialogId($dialog->id); - $dialog->people = $builder->count(); - // 对方信息 - $dialog->dialog_user = null; - $dialog->group_info = null; - $dialog->top_at = $dialog->top_at ?? WebSocketDialogUser::whereDialogId($dialog->id)->whereUserid($userid)->value('top_at'); - switch ($dialog->type) { - case "user": - $dialog_user = $builder->where('userid', '!=', $userid)->first(); - $dialog->name = User::userid2nickname($dialog_user->userid); - $dialog->dialog_user = $dialog_user; - break; - case "group": - if ($dialog->group_type === 'project') { - $dialog->group_info = Project::withTrashed()->select(['id', 'name', 'archived_at', 'deleted_at'])->whereDialogId($dialog->id)->first()?->cancelAppend()->cancelHidden(); - $dialog->name = $dialog->group_info ? $dialog->group_info->name : ''; - } elseif ($dialog->group_type === 'task') { - $dialog->group_info = ProjectTask::withTrashed()->select(['id', 'name', 'complete_at', 'archived_at', 'deleted_at'])->whereDialogId($dialog->id)->first()?->cancelAppend()->cancelHidden(); - $dialog->name = $dialog->group_info ? $dialog->group_info->name : ''; - } - break; + throw new ApiException('不在成员列表内', ['dialog_id' => $dialog_id], -4003); } return $dialog; } @@ -148,17 +471,20 @@ class WebSocketDialog extends AbstractModel /** * 创建聊天室 * @param string $name 聊天室名称 - * @param int|array $userid 加入的会员ID或会员ID组 + * @param int|array $userid 加入的会员ID(组) * @param string $group_type 聊天室类型 + * @param int $owner_id 群主会员ID * @return self|null */ - public static function createGroup($name, $userid, $group_type = '') + public static function createGroup($name, $userid, $group_type = '', $owner_id = 0) { - return AbstractModel::transaction(function () use ($userid, $group_type, $name) { + return AbstractModel::transaction(function () use ($owner_id, $userid, $group_type, $name) { $dialog = self::createInstance([ 'type' => 'group', 'name' => $name ?: '', 'group_type' => $group_type, + 'owner_id' => $owner_id, + 'last_at' => in_array($group_type, ['user', 'department', 'all']) ? Carbon::now() : null, ]); $dialog->save(); foreach (is_array($userid) ? $userid : [$userid] as $value) { @@ -166,6 +492,7 @@ class WebSocketDialog extends AbstractModel WebSocketDialogUser::createInstance([ 'dialog_id' => $dialog->id, 'userid' => $value, + 'important' => !in_array($group_type, ['user', 'all']) ])->save(); } } @@ -173,80 +500,44 @@ class WebSocketDialog extends AbstractModel }); } - /** - * 加入聊天室 - * @param int $dialog_id 会话ID(即 聊天室ID) - * @param int|array $userid 加入的会员ID或会员ID组 - * @return bool - */ - public static function joinGroup($dialog_id, $userid) - { - $dialog = self::whereId($dialog_id)->whereType('group')->first(); - if (empty($dialog)) { - return false; - } - AbstractModel::transaction(function () use ($dialog, $userid) { - foreach (is_array($userid) ? $userid : [$userid] as $value) { - if ($value > 0) { - WebSocketDialogUser::createInstance([ - 'dialog_id' => $dialog->id, - 'userid' => $value, - ])->save(); - } - } - }); - return true; - } - - /** - * 退出聊天室 - * @param int $dialog_id 会话ID(即 聊天室ID) - * @param int|array $userid 加入的会员ID或会员ID组 - * @return bool - */ - public static function exitGroup($dialog_id, $userid) - { - if (is_array($userid)) { - WebSocketDialogUser::whereDialogId($dialog_id)->whereIn('userid', $userid)->delete(); - } else { - WebSocketDialogUser::whereDialogId($dialog_id)->whereUserid($userid)->delete(); - } - return true; - } - /** * 获取会员对话(没有自动创建) - * @param int $userid 会员ID - * @param int $userid2 另一个会员ID + * @param User $user 发起会话的会员 + * @param int $receiver 另一个会员ID * @return self|null */ - public static function checkUserDialog($userid, $userid2) + public static function checkUserDialog($user, $receiver) { + if ($user->userid == $receiver) { + $receiver = 0; + } $dialogUser = self::select(['web_socket_dialogs.*']) ->join('web_socket_dialog_users as u1', 'web_socket_dialogs.id', '=', 'u1.dialog_id') ->join('web_socket_dialog_users as u2', 'web_socket_dialogs.id', '=', 'u2.dialog_id') - ->where('u1.userid', $userid) - ->where('u2.userid', $userid2) + ->where('u1.userid', $user->userid) + ->where('u2.userid', $receiver) ->where('web_socket_dialogs.type', 'user') ->first(); if ($dialogUser) { return $dialogUser; } - return AbstractModel::transaction(function () use ($userid2, $userid) { + if ($receiver > 0 && $user->isTemp()) { + throw new ApiException('无法发起会话'); + } + return AbstractModel::transaction(function () use ($receiver, $user) { $dialog = self::createInstance([ 'type' => 'user', ]); $dialog->save(); WebSocketDialogUser::createInstance([ 'dialog_id' => $dialog->id, - 'userid' => $userid, + 'userid' => $user->userid, ])->save(); WebSocketDialogUser::createInstance([ 'dialog_id' => $dialog->id, - 'userid' => $userid2, + 'userid' => $receiver, ])->save(); return $dialog; }); } - } diff --git a/app/Models/WebSocketDialogMsg.php b/app/Models/WebSocketDialogMsg.php index b62f5c67c..a28450fd5 100644 --- a/app/Models/WebSocketDialogMsg.php +++ b/app/Models/WebSocketDialogMsg.php @@ -2,11 +2,12 @@ namespace App\Models; -use App\Exceptions\ApiException; +use Carbon\Carbon; +use App\Models\User; use App\Module\Base; use App\Tasks\PushTask; +use App\Exceptions\ApiException; use App\Tasks\WebSocketDialogMsgTask; -use Carbon\Carbon; use Hhxsv5\LaravelS\Swoole\Task\Task; use Illuminate\Database\Eloquent\SoftDeletes; @@ -15,15 +16,26 @@ use Illuminate\Database\Eloquent\SoftDeletes; * * @property int $id * @property int|null $dialog_id 对话ID + * @property string|null $dialog_type 对话类型 * @property int|null $userid 发送会员ID * @property string|null $type 消息类型 + * @property string|null $mtype 消息类型(用于搜索) * @property array|mixed $msg 详细消息 + * @property array|mixed $emoji emoji回复 + * @property string|null $key 搜索关键词 * @property int|null $read 已阅数量 * @property int|null $send 发送数量 + * @property int|null $tag 标注会员ID + * @property int|null $todo 设为待办会员ID + * @property int|null $link 是否存在链接 + * @property int|null $modify 是否编辑 + * @property int|null $reply_num 有多少条回复 + * @property int|null $reply_id 回复ID * @property \Illuminate\Support\Carbon|null $created_at * @property \Illuminate\Support\Carbon|null $updated_at * @property \Illuminate\Support\Carbon|null $deleted_at * @property-read int|mixed $percentage + * @property-read \App\Models\WebSocketDialogMsg|null $reply_data * @property-read \App\Models\WebSocketDialog|null $webSocketDialog * @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg newModelQuery() * @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg newQuery() @@ -32,10 +44,20 @@ use Illuminate\Database\Eloquent\SoftDeletes; * @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereCreatedAt($value) * @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereDeletedAt($value) * @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereDialogId($value) + * @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereDialogType($value) + * @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereEmoji($value) * @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereId($value) + * @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereKey($value) + * @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereLink($value) + * @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereModify($value) * @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereMsg($value) + * @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereMtype($value) * @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereRead($value) + * @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereReplyId($value) + * @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereReplyNum($value) * @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereSend($value) + * @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereTag($value) + * @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereTodo($value) * @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereType($value) * @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereUpdatedAt($value) * @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereUserid($value) @@ -49,9 +71,11 @@ class WebSocketDialogMsg extends AbstractModel protected $appends = [ 'percentage', + 'reply_data', ]; protected $hidden = [ + 'key', 'updated_at', ]; @@ -75,6 +99,21 @@ class WebSocketDialogMsg extends AbstractModel return $this->appendattrs['percentage']; } + /** + * 回复消息详情 + * @return WebSocketDialogMsg|null + */ + public function getReplyDataAttribute() + { + if (!isset($this->appendattrs['reply_data'])) { + $this->appendattrs['reply_data'] = null; + if ($this->reply_id > 0) { + $this->appendattrs['reply_data'] = self::find($this->reply_id, ['id', 'userid', 'type', 'msg'])?->cancelAppend() ?: null; + } + } + return $this->appendattrs['reply_data']; + } + /** * 消息格式化 * @param $value @@ -90,10 +129,25 @@ class WebSocketDialogMsg extends AbstractModel $value['type'] = in_array($value['ext'], ['jpg', 'jpeg', 'png', 'gif']) ? 'img' : 'file'; $value['path'] = Base::fillUrl($value['path']); $value['thumb'] = Base::fillUrl($value['thumb'] ?: Base::extIcon($value['ext'])); + } else if ($this->type === 'record') { + $value['path'] = Base::fillUrl($value['path']); } return $value; } + /** + * emoji回复格式化 + * @param $value + * @return array|mixed + */ + public function getEmojiAttribute($value) + { + if (is_array($value)) { + return $value; + } + return Base::json2array($value); + } + /** * 获取占比 * @param bool|int $increment 是否新增阅读数 @@ -157,11 +211,237 @@ class WebSocketDialogMsg extends AbstractModel return true; } + /** + * emoji回复 + * @param $symbol + * @param int $sender 发送的会员ID + * @return mixed + */ + public function emojiMsg($symbol, $sender) + { + $exist = false; + $array = $this->emoji; + foreach ($array as $index => &$item) { + if ($item['symbol'] === $symbol) { + if (in_array($sender, $item['userids'])) { + // 已存在 去除 + $item['userids'] = array_values(array_diff($item['userids'], [$sender])); + if (empty($item['userids'])) { + unset($array[$index]); + $array = array_values($array); + } + } else { + // 未存在 添加 + array_unshift($item['userids'], $sender); + } + $exist = true; + break; + } + } + if (!$exist) { + array_unshift($array, [ + 'symbol' => $symbol, + 'userids' => [$sender] + ]); + } + // + $this->emoji = Base::array2json($array); + $this->save(); + $resData = [ + 'id' => $this->id, + 'emoji' => $array, + ]; + // + $dialog = WebSocketDialog::find($this->dialog_id); + $dialog?->pushMsg('update', $resData); + // + return Base::retSuccess('success', $resData); + } + + /** + * 标注、取消标注 + * @param int $sender 标注的会员ID + * @return mixed + */ + public function toggleTagMsg($sender) + { + if (in_array($this->type, ['tag', 'todo', 'notice'])) { + return Base::retError('此消息不支持标注'); + } + $before = $this->tag; + $this->tag = $before ? 0 : $sender; + $this->save(); + $resData = [ + 'id' => $this->id, + 'tag' => $this->tag, + ]; + // + $data = [ + 'update' => $resData + ]; + $res = self::sendMsg(null, $this->dialog_id, 'tag', [ + 'action' => $this->tag ? 'add' : 'remove', + 'data' => [ + 'id' => $this->id, + 'type' => $this->type, + 'msg' => $this->quoteTextMsg(), + ] + ], $sender); + if (Base::isSuccess($res)) { + $data['add'] = $res['data']; + $dialog = WebSocketDialog::find($this->dialog_id); + $dialog->pushMsg('update', $resData); + } else { + $this->tag = $before; + $this->save(); + } + // + return Base::retSuccess($this->tag ? '标注成功' : '取消成功', $data); + } + + /** + * 设待办、取消待办 + * @param int $sender 设待办的会员ID + * @param array $userids 设置给指定会员 + * @return mixed + */ + public function toggleTodoMsg($sender, $userids = []) + { + if (in_array($this->type, ['tag', 'todo', 'notice'])) { + return Base::retError('此消息不支持设待办'); + } + if ($this->todo && $this->todo != $sender) { + return Base::retError('仅支持设此待办人员【' . User::userid2nickname($this->todo) . '】取消'); + } + $before = $this->todo; + $this->todo = $before ? 0 : $sender; + $this->save(); + $resData = [ + 'id' => $this->id, + 'todo' => $this->todo, + ]; + // + $data = [ + 'update' => $resData + ]; + $res = self::sendMsg(null, $this->dialog_id, 'todo', [ + 'action' => $this->todo ? 'add' : 'remove', + 'data' => [ + 'id' => $this->id, + 'type' => $this->type, + 'msg' => $this->quoteTextMsg(), + 'userids' => implode(",", $userids), + ] + ], $sender); + if (Base::isSuccess($res)) { + $data['add'] = $res['data']; + $dialog = WebSocketDialog::find($this->dialog_id); + $dialog->pushMsg('update', array_merge($resData, ['dialog_id' => $this->dialog_id])); + // + if ($this->todo) { + $useridList = $dialog->dialogUser->pluck('userid')->toArray(); + foreach ($useridList as $userid) { + if ($userids && !in_array($userid, $userids)) { + continue; + } + if (empty($userid)) { + continue; + } + WebSocketDialogMsgTodo::createInstance([ + 'dialog_id' => $this->dialog_id, + 'msg_id' => $this->id, + 'userid' => $userid, + ])->saveOrIgnore(); + } + } else { + WebSocketDialogMsgTodo::whereMsgId($this->id)->delete(); + } + } else { + $this->todo = $before; + $this->save(); + } + // + return Base::retSuccess($this->todo ? '设置成功' : '取消成功', $data); + } + + /** + * 转发消息 + * @param array|int $dialogids + * @param array|int $userids + * @param User $user 发送的会员 + * @return mixed + */ + public function forwardMsg($dialogids, $userids, $user) + { + return AbstractModel::transaction(function() use ($dialogids, $user, $userids) { + $originalMsg = Base::json2array($this->getRawOriginal('msg')); + $msgs = []; + $already = []; + if ($dialogids) { + if (!is_array($dialogids)) { + $dialogids = [$dialogids]; + } + foreach ($dialogids as $dialogid) { + $res = self::sendMsg(null, $dialogid, $this->type, $originalMsg, $user->userid); + if (Base::isSuccess($res)) { + $msgs[] = $res['data']; + $already[] = $dialogid; + } + } + } + if ($userids) { + if (!is_array($userids)) { + $userids = [$userids]; + } + foreach ($userids as $userid) { + if (!User::whereUserid($userid)->exists()) { + continue; + } + $dialog = WebSocketDialog::checkUserDialog($user, $userid); + if ($dialog && !in_array($dialog->id, $already)) { + $res = self::sendMsg(null, $dialog->id, $this->type, $originalMsg, $user->userid); + if (Base::isSuccess($res)) { + $msgs[] = $res['data']; + } + } + } + } + return Base::retSuccess('转发成功', [ + 'msgs' => $msgs + ]); + }); + } + /** * 删除消息 + * @param array|int $ids * @return void */ - public function deleteMsg() + public static function deleteMsgs($ids) { + $ids = Base::arrayRetainInt(is_array($ids) ? $ids : [$ids], true); + AbstractModel::transaction(function() use ($ids) { + $dialogIds = WebSocketDialogMsg::select('dialog_id')->whereIn("id", $ids)->distinct()->get()->pluck('dialog_id'); + $replyIds = WebSocketDialogMsg::select('reply_id')->whereIn("id", $ids)->distinct()->get()->pluck('reply_id'); + // + WebSocketDialogMsgRead::whereIn('msg_id', $ids)->whereNull('read_at')->delete(); // 未阅读记录不需要软删除,直接删除即可 + WebSocketDialogMsgTodo::whereIn('msg_id', $ids)->delete(); + self::whereIn('id', $ids)->delete(); + // + $dialogDatas = WebSocketDialog::whereIn('id', $dialogIds)->get(); + foreach ($dialogDatas as $dialogData) { + $dialogData->updateMsgLastAt(); + } + foreach ($replyIds as $id) { + self::whereId($id)->update(['reply_num' => self::whereReplyId($id)->count()]); + } + }); + } + + /** + * 撤回消息 + * @return void + */ + public function withdrawMsg() { $send_dt = Carbon::parse($this->created_at)->addDay(); if ($send_dt->lt(Carbon::now())) { @@ -171,16 +451,17 @@ class WebSocketDialogMsg extends AbstractModel $deleteRead = WebSocketDialogMsgRead::whereMsgId($this->id)->whereNull('read_at')->delete(); // 未阅读记录不需要软删除,直接删除即可 $this->delete(); // - $last_msg = null; - if ($this->webSocketDialog) { - $last_msg = WebSocketDialogMsg::whereDialogId($this->dialog_id)->orderByDesc('id')->first(); - $this->webSocketDialog->last_at = $last_msg->created_at; - $this->webSocketDialog->save(); + if ($this->reply_id > 0) { + self::whereId($this->reply_id)->decrement('reply_num'); } // - $dialog = WebSocketDialog::find($this->dialog_id); - if ($dialog) { - $userids = $dialog->dialogUser->pluck('userid')->toArray(); + $dialogData = $this->webSocketDialog; + if ($dialogData) { + foreach ($dialogData->dialogUser as $dialogUser) { + $dialogUser->updated_at = Carbon::now(); + $dialogUser->save(); + } + $userids = $dialogData->dialogUser->pluck('userid')->toArray(); PushTask::push([ 'userid' => $userids, 'msg' => [ @@ -189,43 +470,407 @@ class WebSocketDialogMsg extends AbstractModel 'data' => [ 'id' => $this->id, 'dialog_id' => $this->dialog_id, - 'last_msg' => $last_msg, + 'last_msg' => $dialogData->updateMsgLastAt(), 'update_read' => $deleteRead ? 1 : 0 ], ] ]); } + // + WebSocketDialogMsgTodo::whereMsgId($this->id)->delete(); }); } /** - * 发送消息 - * @param int $dialog_id 会话ID(即 聊天室ID) - * @param string $type 消息类型 - * @param array $msg 发送的消息 - * @param int $sender 发送的会员ID(默认自己,0为系统) + * 预览消息 + * @param bool $preserveHtml 保留html格式 + * @param null|array $data + * @return string + */ + public function previewMsg($preserveHtml = false, $data = null) + { + if ($data === null) { + $data = [ + 'type' => $this->type, + 'msg' => $this->msg, + ]; + } + switch ($data['type']) { + case 'text': + return $this->previewTextMsg($data['msg']['text'], $preserveHtml); + case 'record': + return "[语音]"; + case 'meeting': + return "[会议] ${$data['msg']['name']}"; + case 'file': + if ($data['msg']['type'] == 'img') { + return "[图片]"; + } + return "[文件] {$data['msg']['name']}"; + case 'tag': + $action = $data['msg']['action'] === 'remove' ? '取消标注' : '标注'; + return "[{$action}] {$this->previewMsg(false, $data['msg']['data'])}"; + case 'todo': + $action = $data['msg']['action'] === 'remove' ? '取消待办' : ($data['msg']['action'] === 'done' ? '完成' : '设待办'); + return "[{$action}] {$this->previewMsg(false, $data['msg']['data'])}"; + case 'notice': + return $data['msg']['notice']; + default: + return "[未知的消息]"; + } + } + + /** + * 生成关键词 + * @return string + */ + public function generateMsgKey() + { + return match ($this->type) { + 'text' => str_replace(" ", " ", strip_tags($this->msg['text'])), + 'meeting', 'file' => $this->msg['name'], + default => '', + }; + } + + /** + * 返回引用消息(如果是文本消息则截取) + * @param int $strlen + * @return array|mixed + */ + public function quoteTextMsg($strlen = 30) + { + $msg = $this->msg; + if ($this->type === 'text') { + $msg['text'] = $this->previewTextMsg($msg['text']); + if (mb_strlen($msg['text']) > $strlen) { + $msg['text'] = mb_substr($msg['text'], 0, $strlen - 3) . "..."; + } + } + return $msg; + } + + /** + * 返回文本预览消息 + * @param $text + * @param bool $preserveHtml 保留html格式 + * @return string|string[]|null + */ + private function previewTextMsg($text, $preserveHtml = false) + { + if (!$text) return ''; + $text = preg_replace("/]*?alt=\"(\S+)\"[^>]*?>/", "[$1]", $text); + $text = preg_replace("/]*?>/", "[动画表情]", $text); + $text = preg_replace("/]*?>/", "[图片]", $text); + if (!$preserveHtml) { + $text = strip_tags($text); + $text = str_replace([" ", "&", "<", ">"], [" ", "&", "<", ">"], $text); + } + return $text; + } + + /** + * 处理文本消息内容,用于发送前 + * @param $text + * @param $dialog_id + * @return mixed|string|string[] + */ + public static function formatMsg($text, $dialog_id) + { + @ini_set("pcre.backtrack_limit", 999999999); + // 基础处理 + $text = preg_replace("/<(\/[a-zA-Z]+)\s*>/s", "<$1>", $text); + // 图片 [:IMAGE:className:width:height:src:alt:] + preg_match_all("/(<\/img>)*/s", $text, $matchs); + foreach ($matchs[2] as $key => $base64) { + $imagePath = "uploads/chat/" . date("Ym") . "/" . $dialog_id . "/"; + Base::makeDir(public_path($imagePath)); + $imagePath .= md5s($base64) . "." . $matchs[1][$key]; + if (file_put_contents(public_path($imagePath), base64_decode($base64))) { + $imageSize = getimagesize(public_path($imagePath)); + if (Base::imgThumb(public_path($imagePath), public_path($imagePath) . "_thumb.jpg", 320, 0)) { + $imagePath .= "_thumb.jpg"; + } + $text = str_replace($matchs[0][$key], "[:IMAGE:browse:{$imageSize[0]}:{$imageSize[1]}:{$imagePath}::]", $text); + } + } + // 表情图片 + preg_match_all("//s", $text, $matchs); + foreach ($matchs[1] as $key => $str) { + preg_match("/data-asset=\"(.*?)\"/", $str, $matchAsset); + preg_match("/data-name=\"(.*?)\"/", $str, $matchName); + $imageSize = null; + $imagePath = ""; + $imageName = ""; + if ($matchAsset[1] === "emosearch") { + preg_match("/src=\"(.*?)\"/", $str, $matchSrc); + if ($matchSrc) { + $srcMd5 = md5($matchSrc[1]); + $imagePath = "uploads/emosearch/" . substr($srcMd5, 0, 2) . "/" . substr($srcMd5, 32 - 2) . "/"; + Base::makeDir(public_path($imagePath)); + $imagePath .= md5s($matchSrc[1]); + if (file_exists(public_path($imagePath))) { + $imageSize = getimagesize(public_path($imagePath)); + } else { + $image = file_get_contents($matchSrc[1]); + if ($image && file_put_contents(public_path($imagePath), $image)) { + $imageSize = getimagesize(public_path($imagePath)); + // 添加后缀 + if ($imageSize && !str_contains($imagePath, '.')) { + preg_match("/^image\/(png|jpg|jpeg|gif)$/", $imageSize['mime'], $matchMine); + if ($matchMine) { + $imageNewPath = $imagePath . "." . $matchMine[1]; + if (rename(public_path($imagePath), public_path($imageNewPath))) { + $imagePath = $imageNewPath; + } + } + } + } + } + } + } elseif (file_exists(public_path($matchAsset[1]))) { + $imagePath = $matchAsset[1]; + $imageName = $matchName[1]; + $imageSize = getimagesize(public_path($matchAsset[1])); + } + if ($imageSize) { + $text = str_replace($matchs[0][$key], "[:IMAGE:emoticon:{$imageSize[0]}:{$imageSize[1]}:{$imagePath}:{$imageName}:]", $text); + } else { + $text = str_replace($matchs[0][$key], "[:IMAGE:browse:90:90:images/other/imgerr.jpg::]", $text); + } + } + // 其他网络图片 + preg_match_all("/]*?src=([\"'])(.*?\.(png|jpg|jpeg|gif))\\1[^>]*?>/is", $text, $matchs); + foreach ($matchs[2] as $key => $str) { + if (str_starts_with($str, "{{RemoteURL}}")) { + $imagePath = Base::leftDelete($str, "{{RemoteURL}}"); + $imagePath = Base::rightDelete($imagePath, "_thumb.jpg"); + } else { + $imagePath = "uploads/chat/" . date("Ym") . "/" . $dialog_id . "/"; + Base::makeDir(public_path($imagePath)); + $imagePath .= md5s($str) . "." . $matchs[3][$key]; + } + if (file_exists(public_path($imagePath))) { + $imageSize = getimagesize(public_path($imagePath)); + if (Base::imgThumb(public_path($imagePath), public_path($imagePath) . "_thumb.jpg", 320, 0)) { + $imagePath .= "_thumb.jpg"; + } + $text = str_replace($matchs[0][$key], "[:IMAGE:browse:{$imageSize[0]}:{$imageSize[1]}:{$imagePath}::]", $text); + } else { + $image = file_get_contents($str); + if (empty($image)) { + $text = str_replace($matchs[0][$key], "[:IMAGE:browse:90:90:images/other/imgerr.jpg::]", $text); + } else if (file_put_contents(public_path($imagePath), $image)) { + $imageSize = getimagesize(public_path($imagePath)); + if (Base::imgThumb(public_path($imagePath), public_path($imagePath) . "_thumb.jpg", 320, 0)) { + $imagePath .= "_thumb.jpg"; + } + $text = str_replace($matchs[0][$key], "[:IMAGE:browse:{$imageSize[0]}:{$imageSize[1]}:{$imagePath}::]", $text); + } + } + } + // @成员、#任务、~文件 + preg_match_all("/.*?<\/span>.*?<\/span>.*?<\/span>/s", $text, $matchs); + foreach ($matchs[1] as $key => $str) { + preg_match("/data-denotation-char=\"(.*?)\"/", $str, $matchChar); + preg_match("/data-id=\"(.*?)\"/", $str, $matchId); + preg_match("/data-value=\"(.*?)\"/", $str, $matchValye); + $keyId = $matchId[1]; + if ($matchChar[1] === "~") { + if (Base::isNumber($keyId)) { + $file = File::permissionFind($keyId, User::auth()); + if ($file->type == 'folder') { + throw new ApiException('文件夹不支持分享'); + } + $fileLink = $file->getShareLink(User::userid()); + $keyId = $fileLink['code']; + } else { + preg_match("/\/single\/file\/(.*?)$/i", $keyId, $match); + if ($match && strlen($match[1]) >= 8) { + $keyId = $match[1]; + } else { + throw new ApiException('文件分享错误'); + } + } + } + $text = str_replace($matchs[0][$key], "[:{$matchChar[1]}:{$keyId}:{$matchValye[1]}:]", $text); + } + // 处理快捷消息 + preg_match_all("/]*?data-quick-key=([\"'])(.*?)\\1[^>]*?>(.*?)<\/span>/is", $text, $matchs); + foreach ($matchs[0] as $key => $str) { + $quickKey = $matchs[2][$key]; + $quickLabel = $matchs[3][$key]; + if ($quickKey && $quickLabel) { + $quickKey = str_replace(":", "", $quickKey); + $quickLabel = str_replace(":", "", $quickLabel); + $text = str_replace($str, "[:QUICK:{$quickKey}:{$quickLabel}:]", $text); + } + } + // 处理链接标签 + preg_match_all("/]*?href=([\"'])(.*?)\\1[^>]*?>(.*?)<\/a>/is", $text, $matchs); + foreach ($matchs[0] as $key => $str) { + $herf = $matchs[2][$key]; + $title = $matchs[3][$key] ?: $herf; + preg_match("/\/single\/file\/(.*?)$/i", strip_tags($title), $match); + if ($match && strlen($match[1]) >= 8) { + $file = File::select(['files.id', 'files.name', 'files.ext'])->join('file_links as L', 'files.id', '=', 'L.file_id')->where('L.code', $match[1])->first(); + if ($file && $file->name) { + $name = $file->ext ? "{$file->name}.{$file->ext}" : $file->name; + $text = str_replace($str, "[:~:{$match[1]}:{$name}:]", $text); + continue; + } + } + $herf = base64_encode($herf); + $title = base64_encode($title); + $text = str_replace($str, "[:LINK:{$herf}:{$title}:]", $text); + } + // 文件分享链接 + preg_match_all("/(https*:\/\/)((\w|=|\?|\.|\/|&|-|:|\+|%|;|#|@|,|!)+)/i", $text, $matchs); + if ($matchs) { + foreach ($matchs[0] as $str) { + preg_match("/\/single\/file\/(.*?)$/i", $str, $match); + if ($match && strlen($match[1]) >= 8) { + $file = File::select(['files.id', 'files.name', 'files.ext'])->join('file_links as L', 'files.id', '=', 'L.file_id')->where('L.code', $match[1])->first(); + if ($file && $file->name) { + $name = $file->ext ? "{$file->name}.{$file->ext}" : $file->name; + $text = str_replace($str, "[:~:{$match[1]}:{$name}:]", $text); + } + } + } + } + // 过滤标签 + $text = strip_tags($text, '