Compare commits

..

39 Commits

Author SHA1 Message Date
commie d0e34f36bd 22 2026-04-23 07:33:28 +08:00
commie 24273db8dd 11 2026-04-13 20:00:32 +08:00
commie 9393185f25 批量注册用户优化 2026-04-12 09:50:05 +08:00
commie a98d2b56ee fix redis clear 2026-04-12 09:48:57 +08:00
commie 4463f73efb backup: 修复备份命令的用户名密码问题
backup
2026-04-12 09:46:20 +08:00
commie d75808951e 1 2026-04-12 08:26:08 +08:00
commie f0970446f9 修复 2026-04-10 18:04:33 +08:00
commie c092662ebe payment1 2026-04-10 13:31:15 +08:00
commie 6c59e41b32 Merge branch 'main' of http://156.238.245.175:10086/commie/im 2026-04-08 10:10:23 +08:00
commie b0e87b3fb3 翻译 2026-04-08 10:05:25 +08:00
commie d0dc7930ad 添加用户批量注册命令 2026-04-07 02:30:37 +08:00
commie 594aa137ef 修复相册groupID写成了group_id 2026-04-07 01:40:52 +08:00
commie 005c775694 22 2026-04-06 04:35:35 +08:00
commie 26fb271a54 优化朋友圈 2026-04-06 03:36:41 +08:00
commie dd6745fe24 22 2026-04-06 03:10:44 +08:00
commie 7b5d43f0e8 删除 .user.ini 2026-04-06 02:02:14 +08:00
commie ffc1404baa 21 2026-04-06 01:59:22 +08:00
commie d98ac8f146 20 2026-04-04 08:52:59 +08:00
commie 66bcd8061a 19 2026-03-25 02:48:30 +08:00
commie 8704434c36 translation 2026-03-06 02:31:40 +08:00
commie 70c4966aad 18 2026-03-06 02:27:52 +08:00
commie f598cc8157 17 2026-03-02 15:59:46 +08:00
commie 92948fa856 16 2026-03-01 21:05:19 +08:00
commie 0a45a8fbb9 15 2026-03-01 00:24:34 +08:00
commie 873c7cf9c2 14 2026-02-28 19:24:05 +08:00
commie 73f67b4143 13 2026-02-28 16:18:52 +08:00
commie d75fea32f7 12 2026-02-27 13:53:53 +08:00
commie c9c8a120ab 11 2026-02-24 21:02:17 +08:00
commie 6586f27c9e 10 2026-02-21 08:21:05 +08:00
commie 1a7f4bc98a 9 2026-02-15 19:41:56 +08:00
commie 61c5192018 8 2026-01-12 12:42:08 +08:00
commie c153975eed 7 2026-01-08 05:42:44 +08:00
commie 7439a4a794 6 2025-12-25 23:30:14 +08:00
commie 7c1d6d447e 5 2025-12-25 06:02:38 +08:00
commie 20d230f6c8 database 2025-12-24 17:03:10 +08:00
commie b68946fe79 4 2025-12-24 16:59:05 +08:00
commie b52a51c09b 3 2025-11-22 15:31:01 +08:00
commie 9f25a85d07 2 2025-11-21 01:45:26 +08:00
commie f89196c73c 1 2025-11-21 01:42:54 +08:00
3235 changed files with 610015 additions and 124711 deletions
Executable
+38
View File
@@ -0,0 +1,38 @@
[app]
debug = true
[server]
port=8585
domain=www.shun777.com
https=false
[mysql]
host =127.0.0.1
port = 3306
database = imadmin
username = imadmin
password = ejkFmaAXAHXyNXd2
charset = utf8mb4
collation = utf8mb4_general_ci
prefix = wa_
strict = true
engine =
[mongodb]
host = 127.0.0.1
port = 27017
database = imadmin_mongo
username = root
password = n1e5a6s6m7
[redis]
host = 127.0.0.1
port = 6379
database = 0
password = n1e5a6s6m7
prefix = q_
[mcp]
TRANSPORT=http
HOST=127.0.0.1
PORT=8080
PATH=mcp
TIMEOUT=60000
MEMORY_LIMIT=256M
MAX_EXECUTION_TIME=0
+2
View File
@@ -0,0 +1,2 @@
* text=auto eol=lf
*.txt -text
Regular → Executable
+10 -43
View File
@@ -1,45 +1,12 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
node_modules
*.lock
unpackage
package-lock.json
.hbuilderx
uniCloud-aliyun
# plugin
/nativeplugins
# testing
/coverage
# production
/build
/unpackage/cache
/unpackage/debug
/unpackage/dist
/unpackage/resources
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# electron output
/dist
# db
OpenIM_*
# plugin
/nativeplugins
config/site.php
app/command/Test.php
.env
runtime
vendor
public/shunliao.apk
.user.ini
+1
View File
@@ -0,0 +1 @@
-2
View File
@@ -1,2 +0,0 @@
/node_modules/
/nativeplugins/
+232
View File
@@ -0,0 +1,232 @@
---
name: "webman-openim-admin"
description: "专门针对基于webman的openim管理项目,使用了think-cache、think-orm、think-template、redis-queue、webman/event、tinywan/validate等依赖。用于项目的开发、维护和问题排查。"
---
# Webman OpenIM Admin 技能
## 项目概述
本技能专门针对基于 webman 框架开发的 OpenIM 管理项目,该项目使用了以下核心依赖:
- **think-cache**: 缓存管理
- **think-orm**: 数据库 ORM 框架
- **think-template**: 模板引擎
- **redis-queue**: Redis 队列管理
- **webman/event**: 事件系统
- **tinywan/validate**: 数据验证
## 功能特性
### 1. 项目结构分析
- 分析项目目录结构
- 识别核心模块和文件
- 理解依赖关系
### 2. 代码开发与维护
- 基于现有代码风格和模式进行开发
- 提供符合项目规范的代码建议
- 帮助排查和修复常见问题
### 3. 依赖管理
- 分析 composer.json 配置
- 提供依赖版本建议
- 处理依赖冲突问题
### 4. 数据库操作
- 基于 think-orm 的数据库操作指导
- 模型定义和关系映射
- 数据库迁移和种子数据管理
### 5. 缓存策略
- 基于 think-cache 的缓存配置和使用
- 缓存优化建议
- 缓存一致性管理
### 6. 队列管理
- redis-queue 的配置和使用
- 队列任务的创建和监控
- 队列性能优化
### 7. 事件系统
- webman/event 的配置和使用
- 事件监听和触发
- 事件驱动架构设计
### 8. 数据验证
- tinywan/validate 的配置和使用
- 表单验证规则定义
- 自定义验证规则开发
## 触发条件
当用户需要:
- 了解项目结构和依赖
- 开发新功能或修改现有功能
- 排查项目中的问题
- 优化项目性能
- 配置或调整项目依赖
- 进行数据库相关操作
- 实现缓存策略
- 管理队列任务
- 使用事件系统
- 进行数据验证
## 使用示例
### 示例 1: 分析项目结构
```bash
# 查看项目目录结构
ls -la
# 查看 composer.json 了解依赖
cat composer.json
```
### 示例 2: 数据库操作
```php
// 使用 think-orm 进行数据库查询
use think\Model;
class User extends Model
{
protected $table = 'user';
}
// 查询用户列表
$users = User::where('status', 1)->select();
```
### 示例 3: 缓存使用
```php
// 使用 think-cache
use think\facade\Cache;
// 设置缓存
Cache::set('key', 'value', 3600);
// 获取缓存
$value = Cache::get('key');
```
### 示例 4: 队列任务
```php
// 使用 redis-queue
use support\Queue;
// 推送任务
Queue::push('App\\Jobs\\SendEmail', ['email' => 'user@example.com']);
```
### 示例 5: 事件监听
```php
// 使用 webman/event
use support\Event;
// 监听事件
Event::listen('user.registered', function ($user) {
// 处理用户注册事件
});
// 触发事件
Event::trigger('user.registered', $user);
```
### 示例 6: 数据验证
```php
// 使用 tinywan/validate
use Tinywan\Validate\Validate;
$validate = new Validate();
$validate->rule([
'name' => 'require|max:25',
'email' => 'require|email',
]);
if (!$validate->check($data)) {
return $validate->getError();
}
```
## 项目配置建议
1. **composer.json** 配置:
- 保持依赖版本的稳定性
- 定期更新依赖以获取安全补丁
2. **数据库配置**
- 优化数据库连接池设置
- 合理使用索引
- 定期备份数据库
3. **缓存配置**
- 根据业务场景选择合适的缓存策略
- 设置合理的缓存过期时间
- 考虑缓存预热机制
4. **队列配置**
- 合理设置队列 worker 数量
- 监控队列任务执行状态
- 实现失败重试机制
5. **性能优化**
- 启用 OPcache
- 优化数据库查询
- 使用合适的缓存策略
- 合理设计事件系统
## 常见问题与解决方案
1. **依赖冲突**
- 检查 composer.json 中的版本约束
- 使用 `composer update` 解决版本冲突
2. **数据库连接问题**
- 检查数据库配置文件
- 确认数据库服务是否正常运行
3. **缓存失效**
- 检查缓存配置
- 确认缓存服务是否正常
4. **队列任务失败**
- 检查队列配置
- 查看任务执行日志
- 实现失败重试机制
5. **事件不触发**
- 检查事件监听注册
- 确认事件触发代码
6. **验证失败**
- 检查验证规则
- 确认输入数据格式
## 最佳实践
1. **代码组织**
- 遵循 PSR 代码规范
- 合理使用命名空间
- 保持代码结构清晰
2. **安全性**
- 防止 SQL 注入
- 防止 XSS 攻击
- 保护敏感信息
3. **可维护性**
- 编写清晰的注释
- 使用一致的代码风格
- 遵循设计模式
4. **性能**
- 优化数据库查询
- 合理使用缓存
- 减少不必要的计算
5. **扩展性**
- 采用模块化设计
- 依赖注入
- 接口分离
本技能旨在帮助开发者更高效地开发和维护基于 webman 的 OpenIM 管理项目,提供专业的技术支持和最佳实践建议。
Vendored Executable
+3
View File
@@ -0,0 +1,3 @@
{
"php.version": "8.2"
}
-429
View File
@@ -1,429 +0,0 @@
<script>
import {mapGetters,mapActions} from "vuex";
import IMSDK, {IMMethods,MessageType,SessionType,} from "openim-uniapp-polyfill";
import config from "./common/config";
import {getDbDir,toastWithCallback} from "@/util/common.js";
import {conversationSort} from "@/util/imCommon";
import {checkUpgrade} from "@/api/login.js"
import {PageEvents,UpdateMessageTypes} from "@/constant";
export default {
onLaunch: function() {
console.log("App Launch");
// #ifdef H5
console.error(
`暂时不支持运行到 Web,如果需要移动端的 Web 项目,参考 [H5 demo](https://github.com/openimsdk/openim-h5-demo)`
);
return ;
// #endif
// #ifdef MP-WEIXIN
console.error(`暂时不支持运行到小程序端`);
// #endif
this.checkUpdate();
this.setGlobalIMlistener();
this.tryLogin();
},
onShow: function() {
console.log("App Show");
// #ifdef APP
IMSDK.asyncApi(IMSDK.IMMethods.SetAppBackgroundStatus, IMSDK.uuid(), false);
// #endif
},
onHide: function() {
console.log("App Hide");
// #ifdef APP
IMSDK.asyncApi(IMSDK.IMMethods.SetAppBackgroundStatus, IMSDK.uuid(), true);
// #endif
},
computed: {
...mapGetters([
"storeConversationList",
"storeCurrentConversation",
"storeCurrentUserID",
"storeSelfInfo",
"storeRecvFriendApplications",
"storeRecvGroupApplications",
"storeHistoryMessageList",
"storeIsSyncing",
"storeGroupList",
]),
},
methods: {
...mapActions("message", ["pushNewMessage", "updateOneMessage"]),
...mapActions("conversation", ["updateCurrentMemberInGroup"]),
...mapActions("circle", ["getFriendCircleInfo"]),
...mapActions("contact", [
"updateFriendInfo",
"pushNewFriend",
"updateBlackInfo",
"pushNewBlack",
"pushNewGroup",
"updateGroupInfo",
"pushNewRecvFriendApplition",
"updateRecvFriendApplition",
"pushNewSentFriendApplition",
"updateSentFriendApplition",
"pushNewRecvGroupApplition",
"updateRecvGroupApplition",
"pushNewSentGroupApplition",
"updateSentGroupApplition",
]),
setGlobalIMlistener() {
this.$store.dispatch("system/getConfig");
console.log("setGlobalIMlistener");
// init
const kickHander = (message) => {
toastWithCallback(message, () => {
uni.removeStorage({
key: "IMToken",
});
uni.removeStorage({
key: "BusinessToken",
});
uni.$u.route("/pages/login/index");
});
};
IMSDK.subscribe(IMSDK.IMEvents.OnKickedOffline, (data) => {
kickHander("您的账号在其他设备登录,请重新登陆!");
});
IMSDK.subscribe(IMSDK.IMEvents.OnUserTokenExpired, (data) => {
kickHander("您的登录已过期,请重新登陆!");
});
IMSDK.subscribe(IMSDK.IMEvents.OnUserTokenInvalid, (data) => {
kickHander("您的登录已无效,请重新登陆!");
});
// sync
const syncStartHandler = ({data}) => {
this.$store.commit("user/SET_IS_SYNCING", true);
this.$store.commit("user/SET_REINSTALL", data);
};
const syncProgressHandler = ({data}) => {
this.$store.commit("user/SET_PROGRESS", data);
};
const syncFinishHandler = () => {
uni.hideLoading();
this.$store.dispatch("conversation/getConversationList");
this.$store.dispatch("contact/getFriendList");
this.$store.dispatch("contact/getGrouplist");
this.$store.dispatch("conversation/getUnReadCount");
this.$store.commit("user/SET_IS_SYNCING", false);
};
const syncFailedHandler = () => {
uni.hideLoading();
uni.$u.toast("同步消息失败");
this.$store.dispatch("conversation/getConversationList");
this.$store.dispatch("conversation/getUnReadCount");
this.$store.commit("user/SET_IS_SYNCING", false);
};
IMSDK.subscribe(IMSDK.IMEvents.OnSyncServerStart, syncStartHandler);
IMSDK.subscribe(IMSDK.IMEvents.OnSyncServerFinish, syncFinishHandler);
IMSDK.subscribe(IMSDK.IMEvents.OnSyncServerFailed, syncFailedHandler);
IMSDK.subscribe(IMSDK.IMEvents.OnSyncServerProgress, syncProgressHandler);
// self
const selfInfoUpdateHandler = ({data}) => {
this.$store.commit("user/SET_SELF_INFO", {
...this.storeSelfInfo,
...data,
});
};
IMSDK.subscribe(IMSDK.IMEvents.OnSelfInfoUpdated, selfInfoUpdateHandler);
// message
const newMessagesHandler = ({data}) => {
if (this.storeIsSyncing) {
return;
}
data.forEach(this.handleNewMessage);
};
IMSDK.subscribe(IMSDK.IMEvents.OnRecvNewMessages, newMessagesHandler);
// friend
const friendInfoChangeHandler = ({data}) => {
console.log('friendInfoChangeHandler',data);
uni.$emit(IMSDK.IMEvents.OnFriendInfoChanged, {data});
this.updateFriendInfo({friendInfo: data,});
};
const friendAddedHandler = ({data}) => {
this.pushNewFriend(data);
};
const friendDeletedHander = ({data}) => {
this.updateFriendInfo({
friendInfo: data,
isRemove: true,
});
};
IMSDK.subscribe(IMSDK.IMEvents.OnFriendInfoChanged,friendInfoChangeHandler);
IMSDK.subscribe(IMSDK.IMEvents.OnFriendAdded, friendAddedHandler);
IMSDK.subscribe(IMSDK.IMEvents.OnFriendDeleted, friendDeletedHander);
// blacklist
const blackAddedHandler = ({data}) => {
this.pushNewBlack(data);
};
const blackDeletedHandler = ({data}) => {
this.updateBlackInfo({
blackInfo: data,
isRemove: true,
});
};
IMSDK.subscribe(IMSDK.IMEvents.OnBlackAdded, blackAddedHandler);
IMSDK.subscribe(IMSDK.IMEvents.OnBlackDeleted, blackDeletedHandler);
// group
const joinedGroupAddedHandler = ({data}) => {
this.pushNewGroup(data);
};
const joinedGroupDeletedHandler = ({data}) => {
this.updateGroupInfo({
groupInfo: data,
isRemove: true,
});
};
const groupInfoChangedHandler = ({data}) => {
this.updateGroupInfo({
groupInfo: data,
});
};
const groupMemberInfoChangedHandler = ({data}) => {
uni.$emit(IMSDK.IMEvents.OnGroupMemberInfoChanged, {data});
if (data.groupID === this.storeCurrentConversation?.groupID) {
this.updateCurrentMemberInGroup(data);
}
};
IMSDK.subscribe(IMSDK.IMEvents.OnJoinedGroupAdded,joinedGroupAddedHandler);
IMSDK.subscribe(IMSDK.IMEvents.OnJoinedGroupDeleted,joinedGroupDeletedHandler);
IMSDK.subscribe(IMSDK.IMEvents.OnGroupInfoChanged,groupInfoChangedHandler);
IMSDK.subscribe(IMSDK.IMEvents.OnGroupMemberInfoChanged,groupMemberInfoChangedHandler);
// application
const friendApplicationNumHandler = ({data}) => {
const isRecv = data.toUserID === this.storeCurrentUserID;
if (isRecv) {
this.pushNewRecvFriendApplition(data);
} else {
this.pushNewSentFriendApplition(data);
}
};
const friendApplicationAccessHandler = ({data}) => {
const isRecv = data.toUserID === this.storeCurrentUserID;
if (isRecv) {
this.updateRecvFriendApplition({
application: data,
});
} else {
this.updateSentFriendApplition({
application: data,
});
}
};
const groupApplicationNumHandler = ({data}) => {
const isRecv = data.userID !== this.storeCurrentUserID;
if (isRecv) {
this.pushNewRecvGroupApplition(data);
} else {
this.pushNewSentGroupApplition(data);
}
};
const groupApplicationAccessHandler = ({data}) => {
const isRecv = data.userID !== this.storeCurrentUserID;
if (isRecv) {
this.updateRecvGroupApplition({
application: data,
});
} else {
this.updateSentGroupApplition({
application: data,
});
}
};
IMSDK.subscribe(IMSDK.IMEvents.OnFriendApplicationAdded,friendApplicationNumHandler);
IMSDK.subscribe(IMSDK.IMEvents.OnFriendApplicationAccepted,friendApplicationAccessHandler);
IMSDK.subscribe(IMSDK.IMEvents.OnFriendApplicationRejected,friendApplicationAccessHandler);
IMSDK.subscribe(IMSDK.IMEvents.OnGroupApplicationAdded,groupApplicationNumHandler);
IMSDK.subscribe(IMSDK.IMEvents.OnGroupApplicationAccepted,groupApplicationAccessHandler);
IMSDK.subscribe(IMSDK.IMEvents.OnGroupApplicationRejected,groupApplicationAccessHandler);
// conversation
const totalUnreadCountChangedHandler = ({data}) => {
if (this.storeIsSyncing) {
return;
}
this.$store.commit("conversation/SET_UNREAD_COUNT", data);
};
const newConversationHandler = ({data}) => {
if (this.storeIsSyncing) {
return;
}
const result = [...data, ...this.storeConversationList];
this.$store.commit(
"conversation/SET_CONVERSATION_LIST",
conversationSort(result)
);
};
const conversationChangedHandler = ({data}) => {
//console.log('conversationChangedHandler',data);
if (this.storeIsSyncing) {
return;
}
let filterArr = [];
//console.log(data);
const chids = data.map((ch) => ch.conversationID);
filterArr = this.storeConversationList.filter((tc) => !chids.includes(tc.conversationID));
const idx = data.findIndex((c) =>c.conversationID === this.storeCurrentConversation.conversationID);
if (idx !== -1){
this.$store.commit("conversation/SET_CURRENT_CONVERSATION",data[idx]);
}
const result = [...data, ...filterArr];
this.$store.commit("conversation/SET_CONVERSATION_LIST",conversationSort(result));
};
IMSDK.subscribe(IMSDK.IMEvents.OnTotalUnreadMessageCountChanged,totalUnreadCountChangedHandler);
IMSDK.subscribe(IMSDK.IMEvents.OnNewConversation, newConversationHandler);
IMSDK.subscribe(IMSDK.IMEvents.OnConversationChanged,conversationChangedHandler);
},
tryLogin() {
const initStore = () => {
this.$store.dispatch("user/getSelfInfo");
this.$store.dispatch("conversation/getConversationList");
this.$store.dispatch("conversation/getUnReadCount");
this.$store.dispatch("contact/getBlacklist");
this.$store.dispatch("contact/getRecvFriendApplications");
this.$store.dispatch("contact/getSentFriendApplications");
this.$store.dispatch("contact/getRecvGroupApplications");
this.$store.dispatch("contact/getSentGroupApplications");
this.$store.dispatch("contact/getFriendList");
this.$store.dispatch("circle/getFriendCircleInfo");
uni.switchTab({
url: "/pages/conversation/conversationList/index?isRedirect=true"
});
};
getDbDir()
.then(async (path) => {
const flag = await IMSDK.asyncApi(IMMethods.InitSDK, IMSDK.uuid(), {
systemType: "uni-app",
apiAddr: config.getApiUrl(), // SDK的API接口地址。如:http://xxx:10002
wsAddr: config.getWsUrl(), // SDK的websocket地址。如: ws://xxx:10001
dataDir: path, // 数据存储路径
logLevel: 6,
logFilePath: path,
isLogStandardOutput: true,
isExternalExtensions: false,
});
if (!flag) {
plus.navigator.closeSplashscreen();
uni.$u.toast("初始化IMSDK失败!");
return;
}
const status = await IMSDK.asyncApi(IMSDK.IMMethods.GetLoginStatus,IMSDK.uuid());
if (status === 3) {
initStore();
return;
}
const IMToken = uni.getStorageSync("IMToken");
const IMUserID = uni.getStorageSync("IMUserID")+'';
if (IMToken && IMUserID) {
IMSDK.asyncApi(IMSDK.IMMethods.Login, IMSDK.uuid(), {
userID: IMUserID,
token: IMToken,
})
.then(initStore)
.catch((err) => {
console.log(err);
uni.removeStorage({
key: "IMToken",
});
uni.removeStorage({
key: "BusinessToken",
});
plus.navigator.closeSplashscreen();
});
} else {
plus.navigator.closeSplashscreen();
}
})
.catch((err) => {
console.log("get dir failed");
console.log(err);
plus.navigator.closeSplashscreen();
});
},
handleNewMessage(newServerMsg) {
if (this.inCurrentConversation(newServerMsg)) {
if (
newServerMsg.contentType !== MessageType.TypingMessage &&
newServerMsg.contentType !== MessageType.RevokeMessage
) {
newServerMsg.isAppend = true;
this.pushNewMessage(newServerMsg);
setTimeout(() => uni.$emit(PageEvents.ScrollToBottom, true));
uni.$u.debounce(this.markConversationAsRead, 2000);
}
}
},
inCurrentConversation(newServerMsg) {
switch (newServerMsg.sessionType) {
case SessionType.Single:
return (
newServerMsg.sendID === this.storeCurrentConversation.userID ||
(newServerMsg.sendID === this.storeCurrentUserID &&
newServerMsg.recvID === this.storeCurrentConversation.userID)
);
case SessionType.WorkingGroup:
return newServerMsg.groupID === this.storeCurrentConversation.groupID;
case SessionType.Notification:
return newServerMsg.sendID === this.storeCurrentConversation.userID;
default:
return false;
}
},
markConversationAsRead() {
IMSDK.asyncApi(
IMSDK.IMMethods.MarkConversationMessageAsRead,
IMSDK.uuid(),
this.storeCurrentConversation.conversationID
);
},
// 验证是否升级
checkUpdate() {
let system = uni.getSystemInfoSync()
plus.runtime.getProperty(plus.runtime.appid, function(inf) {
checkUpgrade({version:system.appVersion,platform:system.platform,version_wgt:inf.versionCode}).then(res=>{
let skip_version = uni.getStorageSync('skip_version')
if(res && res.version!=skip_version){
uni.$emit('closeWebview')
this.setShow(this.current, false)
router('/pages/common/upgrade?model=' + JSON.stringify(res), '', 'fade-in')
}
})
})
},
},
};
</script>
<style lang="scss">
/*每个页面公共css */
@import "@/uni_modules/uview-ui/index.scss";
@import "@/styles/login.scss";
@import "@/styles/global.scss";
uni-page-body {
height: 100vh;
overflow: hidden;
}
.uni-tabbar .uni-tabbar__icon {
width: 28px !important;
height: 28px !important;
}
</style>
-661
View File
@@ -1,661 +0,0 @@
GNU AFFERO GENERAL PUBLIC LICENSE
Version 3, 19 November 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU Affero General Public License is a free, copyleft license for
software and other kinds of works, specifically designed to ensure
cooperation with the community in the case of network server software.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
our General Public Licenses are intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
Developers that use our General Public Licenses protect your rights
with two steps: (1) assert copyright on the software, and (2) offer
you this License which gives you legal permission to copy, distribute
and/or modify the software.
A secondary benefit of defending all users' freedom is that
improvements made in alternate versions of the program, if they
receive widespread use, become available for other developers to
incorporate. Many developers of free software are heartened and
encouraged by the resulting cooperation. However, in the case of
software used on network servers, this result may fail to come about.
The GNU General Public License permits making a modified version and
letting the public access it on a server without ever releasing its
source code to the public.
The GNU Affero General Public License is designed specifically to
ensure that, in such cases, the modified source code becomes available
to the community. It requires the operator of a network server to
provide the source code of the modified version running there to the
users of that server. Therefore, public use of a modified version, on
a publicly accessible server, gives the public access to the source
code of the modified version.
An older license, called the Affero General Public License and
published by Affero, was designed to accomplish similar goals. This is
a different license, not a version of the Affero GPL, but Affero has
released a new version of the Affero GPL which permits relicensing under
this license.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU Affero General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Remote Network Interaction; Use with the GNU General Public License.
Notwithstanding any other provision of this License, if you modify the
Program, your modified version must prominently offer all users
interacting with it remotely through a computer network (if your version
supports such interaction) an opportunity to receive the Corresponding
Source of your version by providing access to the Corresponding Source
from a network server at no charge, through some standard or customary
means of facilitating copying of software. This Corresponding Source
shall include the Corresponding Source for any work covered by version 3
of the GNU General Public License that is incorporated pursuant to the
following paragraph.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the work with which it is combined will remain governed by version
3 of the GNU General Public License.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU Affero General Public License from time to time. Such new versions
will be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU Affero General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU Affero General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU Affero General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If your software can interact with users remotely through a computer
network, you should also make sure that it provides a way for users to
get its source. For example, if your program is a web application, its
interface could display a "Source" link that leads users to an archive
of the code. There are many ways you could offer source, and different
solutions will be better for different programs; see section 13 for the
specific requirements.
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU AGPL, see
<https://www.gnu.org/licenses/>.
-122
View File
@@ -1,122 +0,0 @@
```
n1e5a6s6m7
```
```
conversation详情
{
"conversationID": "si_100003_100004",
"conversationType": 1,
"userID": "100004",
"groupID": "",
"showName": "1111",
"faceURL": "/static/img/avatar.png",
"recvMsgOpt": 2,
"unreadCount": 0,
"groupAtType": 0,
"latestMsg": "{\"clientMsgID\":\"748ac3ce532afd7dc6638ee13154d5f4\",\"serverMsgID\":\"00ba977273b73af7ae8be68e70b24100\",\"createTime\":1764798647940,\"sendTime\":1764798644531,\"sessionType\":1,\"sendID\":\"100004\",\"recvID\":\"100003\",\"msgFrom\":100,\"contentType\":101,\"senderPlatformID\":2,\"senderNickname\":\"131****1111\",\"senderFaceUrl\":\"/static/img/avatar.png\",\"seq\":17,\"isRead\":true,\"status\":2,\"attachedInfo\":\"null\",\"textElem\":{\"content\":\"馃槆馃槆馃槆馃槆馃槆馃槆馃槆馃槆馃槆馃槆馃槆馃槆馃槆馃槆馃槆馃槆馃槆馃槆馃槆馃槆馃槆\"},\"attachedInfoElem\":{\"groupHasReadInfo\":{\"hasReadCount\":0,\"groupMemberCount\":0},\"isPrivateChat\":false,\"burnDuration\":0,\"hasReadTime\":0,\"isEncryption\":false,\"inEncryptStatus\":false}}",
"latestMsgSendTime": 1764798644531,
"draftText": "",
"draftTextTime": 0,
"isPinned": true,
"isPrivateChat": false,
"burnDuration": 0,
"isNotInGroup": false,
"updateUnreadCountTime": 0,
"attachedInfo": "",
"ex": "",
"maxSeq": 0,
"minSeq": 0,
"msgDestructTime": 0,
"isMsgDestruct": false
}
```
```
group detail
{
"groupID": "1793688611",
"groupName": "5676uy",
"notification": "",
"introduction": "",
"faceURL": "",
"createTime": 1764103081757,
"status": 0,
"creatorUserID": "100003",
"groupType": 2,
"ownerUserID": "100003",
"memberCount": 3,
"ex": "",
"attachedInfo": "",
"needVerification": 0,
"lookMemberInfo": 0,
"applyMemberFriend": 0,
"notificationUpdateTime": 0,
"notificationUserID": ""
}
```
```
group member
{
"groupID": "1793688611",
"userID": "100004",
"nickname": "131****1111",
"faceURL": "/static/img/avatar.png",
"roleLevel": 20,
"joinTime": 1764103081761,
"joinSource": 2,
"inviterUserID": "100003",
"muteEndTime": 0,
"operatorUserID": "100003",
"ex": "",
"attachedInfo": ""
}
```
```
user detail
{
"userID": "100003",
"nickname": "131****2222",
"faceURL": "/static/img/avatar.png",
"createTime": 1764100199726,
"ex": "",
"attachedInfo": "",
"globalRecvMsgOpt": 0,
"id": 100003,
"role_id": 0,
"parent_id": null,
"group_id": 0,
"username": "13122222222",
"sex": "1",
"email": null,
"region": "86",
"mobile": "13122222222",
"level": 1,
"birthday": null,
"bio": null,
"money": "0.0000000000",
"score": 0,
"currency1": "0.0000000000",
"currency2": "0.0000000000",
"currency3": "0.0000000000",
"currency4": "0.0000000000",
"currency5": "0.0000000000",
"currency6": "0.0000000000",
"currency7": "0.0000000000",
"currency8": "0.0000000000",
"currency9": "0.0000000000",
"maxsuccessions": 0,
"successions": 0,
"loginfailure": 0,
"prev_time": null,
"last_time": 1764833943,
"last_ip": "139.202.159.214",
"join_time": 1764100199,
"join_ip": "139.202.158.3",
"token": null,
"invite_code": "WBDFAQSL",
"online": 0,
"status": 1,
"created_at": 1764100199,
"updated_at": 1764833943,
"deleted_at": null
}
```
-131
View File
@@ -1,131 +0,0 @@
import config from "@/common/config";
// 登录
export const businessConfig = (params) =>
uni.$u?.http.post("/common/init", JSON.stringify(params));
// 验证是否升级
export const checkUpgrade = (params) =>
uni.$u?.http.post("/common/checkUpgrade", JSON.stringify(params));
export const businessLogin = (params) =>
uni.$u?.http.post("/common/login", JSON.stringify(params));
export const businessSendSms = (params) =>
uni.$u?.http.post("/common/captcha", JSON.stringify(params));
export const businessVerifyCode = (params) =>
uni.$u?.http.post("/common/verify_captcha", JSON.stringify(params));
export const businessRegister = (params) =>
uni.$u?.http.post("/common/register", JSON.stringify(params));
export const businessReset = (params) =>
uni.$u?.http.post("/common/resetpwd", JSON.stringify(params));
export const businessModify = (params) =>
uni.$u?.http.post(
"/user/change_password",
JSON.stringify({
...params,
}), {
header: {
token: uni.getStorageSync("BusinessToken"),
},
}
);
// 用户信息
export const businessInfoUpdate = (params) =>
uni.$u?.http.post(
"/user/info",
JSON.stringify({...params}),
{
header: {
token: uni.getStorageSync("BusinessToken"),
},
}
);
export const businessGetUserInfo = (userID) =>
uni.$u?.http.post(
"/user/find",
JSON.stringify({userIDs: [userID],}),
{
header: {
token: uni.getStorageSync("BusinessToken"),
},
}
);
export const businessSearchUserInfo = (keyword,searchtype) =>
uni.$u?.http.post(
"/user/search",
JSON.stringify({
keyword,
searchtype:(searchtype? searchtype : 'id'),
page: 1,
limit: 10
}), {
header: {
token: uni.getStorageSync("BusinessToken"),
},
}
);
export const businessSearchUser = (keyword,searchtype) =>
uni.$u?.http.post(
"/friend/search",
JSON.stringify({
keyword,
searchtype:(searchtype? searchtype : 'id'),
page: 1,
limit: 99,
}), {
header: {
token: uni.getStorageSync("BusinessToken"),
},
}
);
export const getArticle = (id,type) => uni.$u?.http.post("/article/detail",JSON.stringify({id,type:(type? type : 'id')}));
export const getFriendCircle = (page=1,limit=10) =>{
return uni.$u?.http.get("/friendcircle/list",JSON.stringify({limit:limit,page:page}));
}
export const getFriendCircleNewcount = () =>{
return uni.$u?.http.get("/friendcircle/newcount");
}
export const getFriendCircleInfo = () =>{
return uni.$u?.http.get("/friendcircle/info");
}
export const upload = (files,data,onProgress) =>{
if(typeof data == 'function'){
onProgress = data;
data = {};
}
console.log(typeof files);
return new Promise((resolve,reject)=>{
var u = uni.uploadFile({
url: config.getRegisterUrl()+"/user/upload", // 仅为示例,非真实的接口地址
filePath: files,
//files:files.length > 1 ? files : files[0],
name: "file",
formData:data,
header:{
token:uni.getStorageSync("BusinessToken"),
},
success({data,errMsg}){
console.log(data);
data = JSON.parse(data);
if(data.code == 0){
resolve(data);
}else{
reject(data.msg);
}
},
fail(res) {
console.log(e);
reject(e);
}
});
u.onProgressUpdate((e)=>{
var res = {
'code' : 99999,
'progress' : e.progress
}
onProgress && onProgress.call(this,res);
})
})
}
+125
View File
@@ -0,0 +1,125 @@
<?php
namespace app\api\controller;
use support\Request;
use app\model\Address as AddressModel;
use hg\apidoc\annotation as Apidoc;
/**
* 提现地址
* @Apidoc\NotParse()
* @Apidoc\NotDebug()
*/
class AddressController extends BaseController{
/**
* 不需要鉴权的方法
* @var array
*/
public $noNeedAuth = ['*'];
/**
* 无需登录及鉴权的方法
* @var array
*/
public $noNeedLogin = [];
/**
* 列表
* @Apidoc\Method("POST")
* @Apidoc\Query("network", type="string", require=true, desc="网络")
* @Apidoc\Query("page", type="int", require=true, desc="页码",default=1)
* @Apidoc\Query("limit", type="int", require=true, desc="分页大小",default=10)
*/
public function list()
{
$limit = (int)input('limit',10);
$page = (int)input('page',1);
$status = (int)input('status',-1);
$network = input('network');
$type = input('type');
$model = AddressModel::where('user_id',\support\Jwt\JwtToken::getCurrentId());
//->where('network','BEP-20');
if($type){
$model = $model->where('status',1);
}
if($network){
$model = $model->where('network',$network);
}
$list = $model->paginate($limit);
return $this->success(__('successful'),$list->toArray());
}
/**
* 创建
* @Apidoc\Method("POST")
* @Apidoc\Param("network", type="string", require=true, desc="网络,BEP-20,TRC-20,ALIPAY,WECHAT",default="BEP-20")
* @Apidoc\Param("address", type="string", require=true, desc="地址")
* @Apidoc\Param("title", type="string", require=true, desc="名称")
* @Apidoc\Param("status", type="string", require=true, desc="状态,可选,1,0,默认1")
*/
public function create()
{
//captcha_verify('image','create_address');
//* @Apidoc\Param("code", type="string", require=true, desc="图形验证码 event=create_address")
//$trade_password = input('trade_password');
//\support\Jwt::verify_trade_password($trade_password);
//* @Apidoc\Param("trade_password", type="string", require=true, desc="交易密码")
$data = [
'network' => input('network','BEP-20'),
'title' => input('title'),
'address' => input('address'),
'img' => input('img'),
'is_default' => input('is_default'),
'status' => input('status',0),
'user_id' => \support\Jwt\JwtToken::getCurrentId()
];
if(!$data['title']){
return $this->error(__('Invalid title'));
}
if(!$data['address']){
return $this->error(__('Invalid address'));
}
AddressModel::create($data);
return $this->success(__('successful'));
}
/**
* 编辑
* @Apidoc\Method("POST")
* @Apidoc\Param("id", type="string", require=true, desc="id")
* @Apidoc\Param("title", type="string", require=true, desc="名称")
* @Apidoc\Param("status", type="string", require=true, desc="状态,可选,1,0,默认1")
*/
public function update()
{
//captcha_verify('image','update_address');
//$trade_password = input('trade_password');
//\support\Jwt::verify_trade_password($trade_password);
$data = [
'id' => input('id',''),
'title' => input('title',''),
//'is_default' => input('is_default'),
'status' => input('status',1),
];
if(!$data['id']){
return $this->error(__('Invalid parameters'));
}
if(!$data['title']){
return $this->error(__('Invalid title'));
}
AddressModel::where('id',$data['id'])->save($data);
return $this->success(__('successful'));
}
/**
* 详情
* @Apidoc\Query("id", type="int", require=true, desc="id")
*/
public function detail(){
$appid = input('id');
$vo = AddressModel::where('id',$appid)->find();
if($vo) {
return $this->success(__('successful'),$vo->toArray());
}else{
return $this->error(__("Address is not exist"));
}
}
}
+110
View File
@@ -0,0 +1,110 @@
<?php
namespace app\api\controller;
use support\Request;
use support\Response;
use hg\apidoc\annotation as Apidoc;
use app\model\User;
use app\model\Album as AlbumModel;
/**
* 群相册
*/
class AlbumController extends BaseController
{
public $noNeedAuth = ['*'];
public $noNeedLogin = [];
/**
* @Apidoc\Title("群相册列表")
* @Apidoc\Method("POST")
* @Apidoc\Param("groupID", type="string", require=true, desc="群ID")
* @Apidoc\Param("offset", type="int", require=false, desc="偏移量,和页码二选一",default=0)
* @Apidoc\Param("page", type="int", require=false, desc="页码",default=1)
* @Apidoc\Param("limit", type="int", require=true, desc="分页大小",default=10)
*/
function list(Request $request): Response
{
$user = \support\Jwt::getUser();
$limit = $request->post('limit',10);
$offset = $request->post('offset',0);
$groupID = $request->post('groupID') ?:0;
if(!$groupID){
return $this->error('groupID is invalid');
}
//$ls = $this->get_user_in_group(groupID);
$query = AlbumModel::where('groupID',$groupID)
->order('id','desc');
if($offset){
$list = $query->where('id','<',$offset)->limit($offset,$limit);
}else{
$list = $query->paginate($limit);
}
$list->each(function($item){
if($item->image){
$item['image_detail'] = \support\think\Db::name('gallery')->where('id',$item->image)->find();
}
return $item;
});
return $this->success('ok',$list);
}
/**
* @Apidoc\Title("创建相册")
* @Apidoc\Method("POST")
* @Apidoc\Param("groupID", type="string", require=true, desc="群ID")
* @Apidoc\Param("title", type="string", require=true, desc="标题")
* @Apidoc\Param("image", type="int", require=false, desc="封面ID")
*/
function create(Request $request): Response
{
$user_id = \support\Jwt\JwtToken::getCurrentId();
$data = [
'userID' => $user_id,
'groupID' => input('groupID'),
'title' => input('title'),
'image' => input('image'),
];
log_alert($data);
if(!$data['groupID']){
return $this->error(__('Invalid parameters'));
}
$result = AlbumModel::create($data);
return $this->success('ok',$result);
}
/**
* @Apidoc\Title("更新")
* @Apidoc\Method("POST")
* @Apidoc\Param("id", type="string", require=true, desc="ID")
* @Apidoc\Param("title", type="string", require=true, desc="标题")
* @Apidoc\Param("image", type="int", require=false, desc="封面ID")
*/
function update(Request $request): Response
{
$id = $request->input('id');
$image = $request->input('image');
$title = $request->input('title');
$album = AlbumModel::find($id);
if($title){
$album->title = $title;
}
if($image){
$album->image = $image;
}
$album->save();
return $this->success('ok',$album);
}
/**
* @Apidoc\Title("删除")
* @Apidoc\Method("POST")
* @Apidoc\Param("id", type="string", require=true, desc="ID")
*/
function delete(Request $request): Response
{
$id = Input('id');
$album = AlbumModel::whereIn('id',condition: $id)->find();
$album->delete();
return $this->success('ok');
}
}
+250
View File
@@ -0,0 +1,250 @@
<?php
namespace app\api\controller;
use app\model\Archives as ArchivesModel;
use app\model\Content;
use support\Request;
use hg\apidoc\annotation as Apidoc;
use support\Jwt\JwtToken;
use support\think\Db;
/**
* 文章模块
*/
class ArticleController extends BaseController
{
public $noNeedLogin = ['*'];
private const CACHE_PREFIX_READ = 'article_read_';
private const CACHE_TTL = 86400;
/**
* 列表
* @Apidoc\Method("GET")
* @Apidoc\Query("category_id", type="int", require=true, desc="分类ID", default=10)
* @Apidoc\Query("page", type="int", require=true, desc="页码", default=1)
* @Apidoc\Query("limit", type="int", require=true, desc="分页大小", default=10)
*/
public function list()
{
$limit = (int)input('limit', 10);
$category_id = (int)input('category_id', 0);
$model = ArchivesModel::where('status', 'normal')->where('type', 'article');
if ($category_id) {
$model = $model->where('category_id', $category_id);
}
$list = $model->order('id', 'desc')->paginate($limit);
$user_id = $this->getCurrentUserId();
$list->each(function ($item) use ($user_id) {
$item->is_read = $user_id ? $this->getReadStatus($item->id, $user_id) : 0;
return $item;
});
return $this->success(__('successful'), $list->toArray());
}
/**
* faq
* @Apidoc\method("GET")
* @Apidoc\Query("page", type="int", require=true, desc="页码", default=1)
* @Apidoc\Query("limit", type="int", require=true, desc="分页大小", default=10)
*/
public function faq()
{
$limit = (int)input('limit', 10);
$list = ArchivesModel::alias('a')
->join('content c', 'a.id = c.id')
->where('a.status', 'normal')
->where('a.type', 'article')
->where('a.category_id', 9)
->field('a.title, a.id, c.content')
->order('a.id', 'desc')
->paginate($limit);
return $this->success(__('successful'), $list->toArray());
}
/**
* 详情
* @Apidoc\Method("GET")
* @Apidoc\Query("id", type="int", require=true, desc="ID")
*/
public function detail()
{
$id = (int)input('id');
if (!$id) {
return $this->error(__("Invalid parameters"));
}
$vo = ArchivesModel::where('id', $id)->find();
if (!$vo) {
return $this->error(__("Article does not exist"));
}
$this->appendContent($vo);
$user_id = $this->getCurrentUserId();
if ($user_id) {
$this->markAsRead($vo->id, $user_id);
}
return $this->success(__('successful'), $vo->toArray());
}
/**
* 获取最新公告
* @Apidoc\Method("GET")
*/
public function last_notie()
{
$vo = ArchivesModel::where('type', 'article')
->where('status', 'normal')
->order('id', 'desc')
->find();
if (!$vo) {
return $this->success(__("successful"), []);
}
$this->appendContent($vo);
$user_id = $this->getCurrentUserId();
if ($user_id) {
$this->markAsRead($vo->id, $user_id);
}
return $this->success(__('successful'), $vo->toArray());
}
/**
* 单页详情
* @Apidoc\Method("GET")
* @Apidoc\Query("id", type="int", require=true, desc="ID")
* @Apidoc\Query("name", type="string", require=true, desc="二选1")
*/
public function singpage()
{
$id = (int)input('id');
$name = input('name');
$vo = null;
if ($name) {
$vo = ArchivesModel::where('name', $name)->find();
} elseif ($id) {
$vo = ArchivesModel::where('id', $id)->find();
}
if (!$vo) {
return $this->error(__("Article does not exist"));
}
$this->appendContent($vo);
return $this->success(__('successful'), $vo->toArray());
}
/**
* 幻灯片
* @Apidoc\Query("id", type="int", require=true, desc="ID")
*/
public function slide()
{
$list = [
['image' => domain() . '/storage/slide/1.jpg', 'title' => ''],
['image' => domain() . '/storage/slide/2.webp', 'title' => ''],
['image' => domain() . '/storage/slide/3.webp', 'title' => ''],
['image' => domain() . '/storage/slide/4.jpg', 'title' => ''],
];
return $this->success(__('successful'), $list);
}
/**
* 设为已读
* @Apidoc\Query("id", type="int", require=true, desc="ID,多个逗号隔开")
*/
public function mask_as_read()
{
$ids = input('id');
$user_id = $this->getCurrentUserId();
if (!$user_id) {
return $this->success(__('successful'));
}
$ids = array_filter(explode(',', $ids));
foreach ($ids as $id) {
$this->markAsRead((int)$id, $user_id);
}
return $this->success(__('successful'));
}
/**
* 获取当前用户ID
*/
protected function getCurrentUserId(): int
{
try {
return (int)JwtToken::getCurrentId();
} catch (\Throwable $e) {
return 0;
}
}
/**
* 获取阅读状态
*/
protected function getReadStatus(int $articleId, int $userId): int
{
return (int)(cache_get($this->getCacheKey($articleId, $userId)) ?: 0);
}
/**
* 标记为已读
*/
protected function markAsRead(int $articleId, int $userId = null): void
{
if (!$articleId) {
return;
}
if (!$userId) {
$userId = $this->getCurrentUserId();
}
if (!$userId) {
return;
}
$cacheKey = $this->getCacheKey($articleId, $userId);
if (!cache_get($cacheKey)) {
Db::name('archives_read')->insert([
'user_id' => $userId,
'source_id' => $articleId,
'value' => 1
]);
cache($cacheKey, 1, self::CACHE_TTL);
}
}
/**
* 追加内容到文章
*/
protected function appendContent(ArchivesModel $article): void
{
$content = Content::where('id', $article->id)->find();
if ($content) {
$article->setAddonData($content->toArray());
}
}
/**
* 生成缓存键
*/
protected function getCacheKey(int $articleId, int $userId): string
{
return self::CACHE_PREFIX_READ . $articleId . '_' . $userId;
}
}
+53
View File
@@ -0,0 +1,53 @@
<?php
namespace app\api\controller;
use app\model\User as UserModel;
use support\Request;
use hg\apidoc\annotation as Apidoc;
/**
* 余额日志
*/
class BalanceLogController extends BaseController{
/**
* 不需要鉴权的方法
* @var array
*/
public $noNeedAuth = ['*'];
/**
* 无需登录及鉴权的方法
* @var array
*/
public $noNeedLogin = [];
/**
* 余额日志
* @Apidoc\Query("currency", type="string", require=true, desc="货币",default="money")
* @Apidoc\Query("type", type="string", require=true, desc="类型")
* @Apidoc\Query("startTime", type="string", require=true, desc="开始时间")
* @Apidoc\Query("endTime", type="string", require=true, desc="结束时间")
* @Apidoc\Query("page", type="int", require=true, desc="页码",default=1)
* @Apidoc\Query("limit", type="int", require=true, desc="分页大小",default=10)
*/
function list(Request $request){
$user_id = \support\Jwt\JwtToken::getCurrentId();
$type = Input('type',0);
$currency = Input('currency','money');
$startTime = Input('startTime');
$endTime = Input('endTime');
$list = \app\model\BalanceLog::queryLogs($user_id,$currency,$type,$startTime,$endTime);
$BalanceTypeList= \app\enum\BalanceType::toArray();
$list->each(function($item)use($BalanceTypeList){
if($item->type == \app\enum\BalanceType::TRANSFER->value && $item->memo){
$item['target'] = UserModel::where('id',$item->memo)->value('username');
$item->memo = \support\Encrypt::userIDencode($item->memo);
}
$item->_type= $item->type;
$item->type= $BalanceTypeList[$item->type];
if(ctype_digit($item->created_at)){
$item->created_at = datetime($item->created_at);
}
return $item;
});
return $this->success(__('successful'),$list);
}
}
+123
View File
@@ -0,0 +1,123 @@
<?php
namespace app\api\controller;
use support\Request;
use support\Response;
use Shopwwi\WebmanFilesystem\FilesystemFactory;
use Shopwwi\WebmanFilesystem\Facade\Storage;
use hg\apidoc\annotation as Apidoc;
use Tinywan\Validate\Facade\Validate;
/**
* 基础控制器
* @Apidoc\NotParse()
* @Apidoc\NotDebug()
*/
class BaseController
{
/**
* 不需要鉴权的方法
* @var array
*/
public $noNeedAuth = [];
/**
* 无需登录及鉴权的方法
* @var array
*/
public $noNeedLogin = [];
function __construct()
{
$this->_init();
}
protected function _init(){
}
/**
* 返回格式化json数据
*
* @param int $code
* @param string $msg
* @param array $data
* @return Response
*/
protected function json(int $code, string $msg = 'ok', array|object|null $data = []): Response
{
return json(['code' => $code, 'data' => $data, 'msg' => __($msg)]);
}
protected function success(string $msg = '成功', array|object|null $data = []): Response
{
return $this->json(0, $msg, $data);
}
protected function fail(string $msg = '失败', array|object|null $data = []): Response
{
return $this->json(1,$msg, $data);
}
protected function error(string $msg = '失败', array|object|null $data = []): Response
{
return $this->json(1,$msg, $data);
}
protected function _upload($request)
{
try{
$user = \support\Jwt::getUser();
}catch(\Exception $e){
$user = ['id'=>0];
}
$savePath = $request->post('savePath','files');
$validate = Validate::rule('savePath', 'alphaNum');
$data = ['savePath' => $savePath];
if (!$validate->check($data)) {
return '参数错误:'.$validate->getError();
}
$savePath = trim($savePath,'/');
//$savePath = 'upload/'.$savePath.'/'.$user['id'];
$savePath = $savePath.'/'.$user['id'];
\support\Log::alert('savePath:'.$savePath);
$mimetype = explode(',',Config('site.upload_mimetype'));
$maxsize = Config('site.upload_maxsize')*1024*1024;
//多文件上传
$files = $request->file();
$result = Storage::adapter('oss')
->path($savePath)
->size($maxsize)
->extYes($mimetype)
->uploads($files,0,$maxsize * count($files),false);
$save_datas = [];
foreach($result as $k=>$fileinfo){
$save_datas[] = [
'user_id' => $user['id'],
'category' => 'default',
'adapter' => $fileinfo->adapter,
'origin_name' => $fileinfo->origin_name,
'file_name' => $fileinfo->file_name,
'size' => $fileinfo->size,
'mime_type' => $fileinfo->mime_type,
'extension' => $fileinfo->extension,
'file_height' => $fileinfo->file_height,
'file_width' => $fileinfo->file_width,
'file_url' => $fileinfo->file_url,
'sha1' => $fileinfo->storage_key ?:sha1_file(public_path($fileinfo->file_name)),
'use_count' => 0,
];
}
$res = \app\model\Files::saveAll($save_datas);
return $res;
}
/**
* @Apidoc\Title("上传")
* @Apidoc\Method("POST")
* @Apidoc\NotParse()
* @Apidoc\NotDebug()
*/
function upload(Request $request,$return = false)
{
$res = $this->_upload($request);
if(is_string($res)){
return $this->fail( $res);
}
return $this->success(__('successful'),$res);
}
}
+124
View File
@@ -0,0 +1,124 @@
<?php
namespace app\api\controller;
use app\model\Card as CardModel;
use app\model\Cdkey as CdkeyModel;
use app\model\User as UserModel;
use support\think\Db;
use hg\apidoc\annotation as Apidoc;
/**
* 卡密模块
* @Apidoc\NotParse()
* @Apidoc\NotDebug()
*/
class CardController extends BaseController{
/**
* 不需要鉴权的方法
* @var array
*/
public $noNeedAuth = ['*'];
public $noNeedLogin = ['detail','list'];
/**
* 列表
* @Apidoc\Query("kw", type="string", require=false, desc="搜索关键字")
* @Apidoc\Query("page", type="int", require=true, desc="页码",default=1)
* @Apidoc\Query("limit", type="int", require=true, desc="分页大小",default=10)
*/
public function list(){
$limit = (int)input('limit',10);
$kw = (string)input('kw');
$model = CardModel::where('status',1);
if($kw){
$model = $model->whereLike('title',$kw);
}
$list = $model->order('id desc')->paginate($limit);
return $this->success(__('successful'),$list->toArray());
}
/**
* 详情
* @Apidoc\Query("id", type="int", require=true, desc="ID")
* @Apidoc\Query("status", type="string", require=true, desc="状态")
* @Apidoc\Query("page", type="int", require=true, desc="页码",default=1)
* @Apidoc\Query("limit", type="int", require=true, desc="分页大小",default=10)
*/
public function detail(){
$limit = (int)input('limit',10);
$id = (int)input('id',0);
$status = (string)input('status','all');
$model = CdkeyModel::where('category_id',$id);
if($status!='all'){
$model = $model->where('status',$status);
}
$list = $model->order('id desc')->paginate($limit);
return $this->success(__('successful'),$list->toArray());
}
/**
* 详情
* @Apidoc\Query("id", type="int", require=true, desc="ID")
*/
public function download(){
$id = (int)input('id',0);
$model = CdkeyModel::where('category_id',$id);
$list = $model->order('id desc')->select();
return $this->success(__('successful'),$list->toArray());
}
/**
* 购买产品
* @Apidoc\Method("POST")
* @Apidoc\Param("title", type="string", require=true, desc="标题")
* @Apidoc\Param("total", type="int", require=true, desc="数量")
* @Apidoc\Param("days", type="int", require=true, desc="金额")
* @Apidoc\Param("trade_password", type="string", require=true, desc="交易密码")
*/
public function create()
{
$user = \support\Jwt::getUser();
$data = [
'user_id' => $user->id,
'title'=> input('title'),
'total' => (int)input('total'),
'days' => input('days'),
'status' => 0
];
if(!$data['title']){
return $this->error(__('Incorrect title'));
}
/** @var CardModel $gift */
if(!in_array($data['days'],[100,200,300,500])){
return $this->error(__('Incorrect amount'));
}
if($data['total'] < 1){
return $this->error(__('Incorrect quantity'));
}
$amount = $data['days'] * $data['total'];
$data['user_id'] = $user->id;
$data['amount'] = $amount;
$data['type'] = 'active';
$data['expires'] = strtotime('2030-12-31');
//验证交易密码
$trade_password = input('trade_password');
\support\Jwt::verify_trade_password($trade_password);
//var_dump($user);
//验证余额
if($data['amount'] < 1){
return $this->error(__('Incorrect amount'));
}
if($data['amount'] > $user->score){
return $this->error(__('Insufficient balance'));
}
Db::startTrans();
try{
$data = CardModel::create($data);
UserModel::score($data['user_id'],-$data['amount'],\app\enum\BalanceType::CDKEY,$data['id']);
Hook('card.create',$data);
Db::commit();
}catch(\Exception $e){
Db::rollback();
return $this->error($e->getMessage());
}
return $this->success(__('successful'));
}
}
+106
View File
@@ -0,0 +1,106 @@
<?php
namespace app\api\controller;
use app\model\User as UserModel;
use app\model\Collection as CollectionModel;
use support\Request;
use support\Response;
use support\think\Db;
use hg\apidoc\annotation as Apidoc;
/**
* 收藏
*/
class CollectionController extends BaseController{
/**
* 不需要鉴权的方法
* @var array
*/
public $noNeedAuth = ['*'];
/**
* 无需登录及鉴权的方法
* @var array
*/
public $noNeedLogin = [];
/**
* @Apidoc\Title("列表")
* @Apidoc\Method("GET")
* @Apidoc\Query("content_type", type="string", require=false, desc="内容类型 enum('text', 'image', 'file', 'video', 'link','audio')")
* @Apidoc\Query("kw", type="string", require=false, desc="关键字")
* @Apidoc\Query("page", type="int", require=true, desc="页码",default=1)
* @Apidoc\Query("limit", type="int", require=true, desc="分页大小",default=10)
*/
function list(Request $request): Response
{
$user = \support\Jwt::getUser();
$page = (int)Input('page', 1);
$content_type = Input('content_type');
$kw = Input('kw');
$limit = (int)Input('limit', 10);
$query = CollectionModel::where('status', 1)
->whereIn('user_id',$user->id)
->order('created_at', 'desc');
if($content_type){
$query->where('content_type',$content_type);
}
if($kw){
$query->whereLike('content','%'.$kw.'%');
}
$list = $query->paginate([
'list_rows' => $limit,
'page' => $page,
]);
return $this->success('ok', $list);
}
/**
* 创建收藏
* @Apidoc\Param("content_type", type="string",require=true, desc="内容类型 enum('text', 'image', 'file', 'video', 'link','audio')")
* @Apidoc\Param("content", type="string",require=true, desc="json结构化收藏内容本体")
* @Apidoc\Param("tags", type="string",require=true, desc="用户自定义标签,多个用逗号隔开,或者使用数组")
* @Apidoc\Param("is_pinned", type="int",require=true, desc="是否置顶")
* @param Request $request
* @return Response
*/
function create(Request $request): Response
{
$user = \support\Jwt::getUser();
$content = $request->post('content');
$content_type = $request->post('content_type', '');
$tags = $request->post('tags', '');
$is_pinned = $request->post('is_pinned', 0);
// 验证内容
if (empty($content_type)) {
return $this->fail(__('The field %field% must be not empty. ',['field'=>'content_type']));
}
if (empty($content)) {
return $this->fail(__('The field %field% must be not empty. ',['field'=>'content']));
}
if(is_array($content)) {
$content = json_encode($content);
}
// 创建朋友圈动态
$collection = CollectionModel::create([
'user_id' => $user->id,
'content_type' => $content_type,
'content' => $content,
'tags' => $tags,
'is_pinned' => $is_pinned
]);
return $this->success('发布成功', ['collection' => $collection]);
}
/**
* 删除收藏
* @Apidoc\Param("id", type="int",require=true, desc="收藏id")
* @param Request $request
* @return Response
*/
function delete(Request $request): Response
{
$user = \support\Jwt::getUser();
$id = $request->post('id');
CollectionModel::where('id',$id)->where('user_id',$user->id)->delete();
return $this->success('删除成功');
}
}
+553
View File
@@ -0,0 +1,553 @@
<?php
namespace app\api\controller;
use Tinywan\Validate\Facade\Validate;
use app\model\User as UserModel;
use support\Request;
use support\Response;
use Webman\Captcha\CaptchaBuilder;
use Webman\Captcha\PhraseBuilder;
use Shopwwi\WebmanFilesystem\FilesystemFactory;
use Shopwwi\WebmanFilesystem\Facade\Storage;
use hg\apidoc\annotation as Apidoc;
use think\facade\Db;
/**
* 公共接口
*/
class CommonController extends BaseController{
/**
* 不需要鉴权的方法
* @var array
*/
public $noNeedAuth = [];
/**
* 无需登录及鉴权的方法
* @var array
*/
public $noNeedLogin = ['*'];
/**
* 加载初始化
*
* @Apidoc\Query("version", type="string", require=true, desc="版本号")
*/
public function init(Request $request)
{
$lang = $request->header('lang','zh-Hans');
locale( $lang);
$config = Config('site');
$disallowFields = [
'api_token','reward_time_limit',
'mail_type','mail_smtp_host','mail_smtp_port','mail_smtp_user','mail_smtp_pass','mail_verify_type','mail_from',
'attachment_category','categorytype','cdkey_category','configgroup','flagtype',
'languages','forbiddenip','fixedpage','admin_login_captcha',
'upload_mimetype','upload_multipart','upload_multiple','upload_thumbstyle','upload_previewtpl','upload_timeout','upload_maxsize',
'yeji_jicha_reward','suanli_rate','agent_expirs_retention','allow_currencys','allow_currency_logs',
'agent_commission_total_rate','agent_commission_layer_rate','differential_commission_total_rate'
];
$config = array_diff_key($config, array_flip($disallowFields));
if(Request()->client != "web"){
$config["steps"] = Config('step');
}
$config['balance_type_list'] = \app\enum\BalanceType::toArray();
$config['recharge_status_list'] = \app\enum\RechargeStatus::toArray();
$config['withdrawl_status_list'] = \app\enum\WithdrawlStatus::toArray();
$config['server_status_list'] = \app\enum\ServerStatus::toArray();
$config['see_point_awards'] = [
[
'name'=>'S1',
'award'=>0.05,
'total'=>50,
],
[
'name'=>'S2',
'award'=>0.1,
'total'=>100,
],
[
'name'=>'S3',
'award'=>0.15,
'total'=>1000,
],
[
'name'=>'S4',
'award'=>0.2,
'total'=>5000,
],
[
'name'=>'S5',
'award'=>0.25,
'total'=>20000,
]
];
//$config['getFriendList'] = $request->IM->friend->getFriendList('100006');
return $this->success(__('successful'), $config);
}
/**
* 验证是否升级
*/
public function checkUpgrade(Request $request)
{
$field = 'id,type,force,source,version,content';
$verUpdate = new \app\model\Version;
$version = Input('version');
$platform = Input('platform');
$version_wgt = Input('version_wgt');
// 查询整包、外链数据
$update_data = $verUpdate->whereIn('type','0,2')
->where('status',1)
->where('version','>', $version)
->where('platform',$platform)
->field($field)
->order('id desc')->find();
if($update_data) {
return $this->success('',$update_data);
}
// 查询WGT数据
$update_wgt_data = $verUpdate->where('type',1)
->where('status',1)
->where('version_wgt','>', $version_wgt)
->where('platform',$platform)
->field($field)->order('id desc')->find();
if($update_wgt_data) {
return $this->success('',$update_wgt_data);
}
return $this->success('',[]);
}
/**
* 注册会员
*
* @Apidoc\Method ("POST")
* @Apidoc\Param("type", type="string",require=true, desc="注册方式:email,mobile")
* @Apidoc\Param("email", type="string",require=true, desc="邮箱")
* @Apidoc\Param("mobile", type="string",require=true, desc="手机号码")
* @Apidoc\Param("password", type="string",require=true, desc="密码")
* @Apidoc\Param("trade_password", type="string",require=true, desc="交易密码")
* @Apidoc\Param("invite_code", type="string",require=true, desc="推荐码")
* @Apidoc\Param("code", type="string",require=true, desc="邮箱验证码,event=register")
*/
public function register()
{
$email = input('email');
$password = input('password');
$trade_password= input( 'trade_password');
$username = input('username');
$mobile = input('mobile');
$invite_code = input('invite_code');
$type = input('type');
if (!in_array($type,Config('site.user_register_way')) ) {
return $this->error(__('Unknown register way'));
}
if ($type == 'email') {
if(!$email || !Validate::is($email, "email")){
return $this->error(__('Email is incorrect'));
}
$username = $email;
unset($mobile);
captcha_verify('email','register',$email,false);
}
if ($type == 'mobile') {
if(!$mobile || !Validate::regex($mobile, "^1\d{10}$")){
return $this->error(__('Mobile is incorrect'));
}
$username = $mobile;
unset($email);
captcha_verify('mobile','register',$mobile,false);
}
if ($type == 'username') {
if(!$username){
return $this->error(__('Username is incorrect'));
}
}
if (!$password) {
return $this->error(__('Invalid parameters'));
}
// if (!$trade_password) {
// return $this->error(__('Invalid trade password'));
// }else{
// $extends['trade_password'] = \plugin\admin\app\common\Util::passwordHash($trade_password);
// }
//邀请码
//$invite_code = 'TEAJXLEE';
$region = Input('region','+86');
$region = str_replace('+','',$region);
$extends = [
'role_id' => 1,
'group_id' => 0,
'region' => $region,
'nickname' => input('nickname'),
'avatar' => '/static/avatar/'.rand(0,17).'.png',
];
if(empty($extends['nickname'])){
if($type == 'mobile'){
$extends['nickname'] = '用户_'.substr($username,7);
}else if($type == 'email'){
$extends['nickname'] = '用户_'.substr(explode('@',$username)[0],7);
}else{
$extends['nickname'] = $username;
}
}
if ($invite_code) {
if(strlen($invite_code) == 12){
//系统生产的一次性推荐吗
$inviteModel = \app\model\Invitecode::where('code',$invite_code)->find();
if(!$inviteModel){
return $this->error(__('错误的邀请码'));
}
$extends['group_id'] = 2;
$extends['role_id'] = 1;
$extends['parent_id'] = 0;
}else{
$inviter_user = UserModel::where('invite_code',$invite_code)->field('group_id,id')->find();
if(!$inviter_user){
return $this->error(__('Invalid invite code'));
}
$extends['parent_id'] = $inviter_user['id'];
}
}else{
//return $this->error(__('Invalid invite code'));
}
// validate(\app\validate\User::class)
// ->scene('edit')
// ->check([
// 'name' => 'thinkphp',
// 'email' => 'thinkphp@qq.com',
// ]);
try {
$user = \support\Jwt::register($username, $password, $email, $mobile, $extends);
if($inviteModel){
$inviteModel->status = 1;
$inviteModel->save();
}
$data = ['userinfo' => $user];
// if ($type == 'email') {
// captcha_verify('email','register',$email,true);
// }else if ($type == 'mobile') {
// captcha_verify('mobile','register',$mobile,true);
// }else{
// captcha_verify('image','register',$mobile,true);
// }
return $this->success(__('Sign up successful'), $data);
} catch (\Exception $e) {
return $this->error($e->getMessage());
}
}
/**
* 登录
* @Apidoc\Method("POST")
* @Apidoc\Param("username", type="string",require=false, desc="用户名登录必填")
* @Apidoc\Param("email", type="string",require=false, desc="邮箱登录必填")
* @Apidoc\Param("mobile", type="string",require=false, desc="手机号登录必填")
* @Apidoc\Param("type", type="string",require=true,default="mobile",desc="登录方式,username,mobile,email")
* @Apidoc\Param("password", type="string",require=false, desc="密码的登录必填")
* @Apidoc\Param("code", type="string",require=false, desc="验证码登录必填")
* @Apidoc\Param("platform", type="string",require=false, desc="平台",default="web")
* @Apidoc\Param("region", type="string",require=false,default="86", desc="区域,手机号登录必填")
*/
public function login(Request $request){
$username = input('username');
$mobile = input('mobile');
$email = input('email');
$password = input('password');
$type = input('type');
if($type == 'mobile'){
if (!$mobile ) {
return $this->fail(__('Invalid username or password'));
}
$username = $mobile;
}else if($type == 'email'){
if (!$email ) {
return $this->fail(__('Invalid username or password'));
}
$username = $email;
}else{
if (!$username ) {
return $this->fail(__('Invalid username or password'));
}
}
try{
if ($password) {
//return $this->fail(__('Invalid username or password'));
$user = \support\Jwt::login($username, $password,$type);
}else{
$user = \support\Jwt::login($username, $password,$type,'code');
}
if($user === false){
return $this->fail(\support\Jwt::getError());
}
//登录成功的事件
$user = Hook("user.login_successed", $user);
return $this->success(__('successful'), $user[0]);
} catch (\Exception $e) {
return $this->error($e->getMessage());
}
}
/**
* 退出登录
* @Apidoc\Method("GET")
*/
public function logout(){
\support\Jwt::logout();
return $this->success(__('successful'));
}
/**
* 重置密码
*
* @Apidoc\Method ("POST")
* @Apidoc\Param("email", type="string",require=true, desc="邮箱")
* @Apidoc\Param("mobile", type="string",require=true, desc="手机号")
* @Apidoc\Param("newpassword", type="string",require=true, desc="新密码")
* @Apidoc\Param("code", type="string",require=true, desc="邮箱验证码,event=resetpwd")
*/
public function resetpwd()
{
$email = input("email");
$mobile = input("mobile");
$newpassword = input("newpassword");
if (!$newpassword) {
return $this->error(__('Invalid parameters'));
}
//验证Token
if (!Validate::check(['newpassword' => $newpassword], ['newpassword' => 'require|regex:\S{6,32}'])) {
return $this->error(__('Password must be 6 to 32 characters'));
}
if (!$mobile && !$email){
try{
$user = \support\Jwt::getUser();
}catch(\Exception $e){
$user = false;
}
if($user){
captcha_verify('mobile','reset_pwd',$user->mobile);
}
}else{
if ($mobile && Validate::regex($mobile, "^1\d{10}$")) {
captcha_verify('mobile','reset_pwd',$mobile);
$region = Input('region','+86');
$region = str_replace('+','',$region);
$user = UserModel::where('region',$region)->where('mobile',$mobile)->find();
}else if ($email && Validate::is($email, "email")) {
captcha_verify('email','reset_pwd',$email);
$user = UserModel::getByEmail($email);
}
}
if (!$user) {
return $this->error(__('Invalid parameters'));
}
//模拟一次登录,需不需要充值登录信息?????
//\support\Jwt::direct($user->id);
try{
UserModel::where('id',$user->id)->save([
'loginfailure' => 0,
'password' => \plugin\admin\app\common\Util::passwordHash($newpassword)
]);
} catch (\Exception $e) {
return $this->error($e->getMessage());
}
return $this->success(__('Reset password successful'));
}
/**
* 重置交易密码
*
* @Apidoc\Method ("POST")
* @Apidoc\Param("email", type="string",require=true, desc="邮箱")
* @Apidoc\Param("newpassword", type="string",require=true, desc="新密码")
* @Apidoc\Param("code", type="string",require=true, desc="邮箱验证码,event=reset_trade_pwd")
* @Apidoc\Param("verify_type", type="string",require=true, desc="验证方式,email,mobile")
*/
public function reset_trade_pwd()
{
$email = input("email");
$mobile = input("mobile");
$verify_type = input("verify_type");
$newpassword = input("newpassword");
if (!$newpassword) {
return $this->error(__('Invalid parameters'));
}
//验证Token
if (!Validate::check(['newpassword' => $newpassword], ['newpassword' => 'require|regex:\S{6,32}'])) {
return $this->error(__('Trade password must be 6-32 characters'));
}
if (!$mobile && !$email){
try{
$user = \support\Jwt::getUser();
}catch(\Exception $e){
$user = false;
}
if($user){
if($verify_type == 'email'){
captcha_verify('email','reset_trade_pwd',$user->email);
}else if($verify_type == 'mobile'){
captcha_verify('mobile','reset_trade_pwd',$user->mobile);
}else{
return $this->error(__('Unknown verify type'));
}
}
}else{
if ($mobile && Validate::regex($mobile, "^1\d{10}$")) {
captcha_verify('mobile','reset_trade_pwd',$mobile);
$user = UserModel::getByMobile($mobile);
}elseif ($email && Validate::is($email, "email")) {
captcha_verify('email','reset_trade_pwd',$email);
$user = UserModel::getByEmail($email);
}
}
if (!$user) {
return $this->error(__('Invalid parameters'));
}
//模拟一次登录,需不需要充值登录信息?????
//\support\Jwt::direct($user->id);
try{
UserModel::where('id',$user->id)->save([
'trade_password' => \plugin\admin\app\common\Util::passwordHash($newpassword)
]);
} catch (\Exception $e) {
return $this->error($e->getMessage());
}
return $this->success(__('Reset Trade password successful'));
}
/**
* 验证码
* @Apidoc\Method ("POST")
* @Apidoc\Param("type", type="string",require=true, desc="GET参数,类型,email:邮箱验证码,image:图片验证码")
* @Apidoc\Param("event", type="string",require=true, desc="事件,regiser:注册,resetpwd:重置密码,withdrawl:提现")
* @Apidoc\Param("email", type="string",require=true, desc="邮箱,可选")
*/
public function captcha(Request $request){
$debug = false;
$request->input('type');
$type = $request->input('type');
$event = $request->input('event');
if($type == 'email'){
$email = $request->input('email');
if(!$email){
try {
$user = \support\Jwt::getUser();
$email = $user->email;
} catch (\Exception $th) {
return $this->error(__('Invalid parameter'));
}
}
$key = 'captcha_'.$event.'_'.$email;
$list = cache($key);
$list = $list ?:[];
$expris = 60;
if(cache('?exp_'.$key)){
if(cache('exp_'.$key)+$expris > time()){
return $this->fail(__('Only one verification code can be sent within %second% seconds',['%second%'=>$expris]));
}
}
$code =\support\Random::numeric(6);
$list[$code] = time();
cache($key,$list);
cache('exp_'.$key,time());
addJob([
'email' => $email,
'title' => __(Config('site.name').' 验证码'),
'event' => $event,
'code' => $code
],'Email');
\support\Log::channel('mail')->alert("邮件验证码:".$code.',邮箱:'.$email);
return $this->success(__('Email sent successfully'),[
'code'=> $debug ? $code : ''
]);
}elseif($type == 'mobile'){
$mobile = $request->input('mobile');
if(!$mobile){
try {
$user = \support\Jwt::getUser();
$mobile = $user->mobile;
} catch (\Exception $th) {
return $this->error(__('Invalid parameter'));
}
}
if (!Validate::regex($mobile, "^1\d{10}$")) {
return $this->error(__('Mobile is incorrect'));
}
$key = 'captcha_'.$event.'_'.$mobile;
$list = cache($key);
$list = $list ?:[];
$expris = 300;
if(cache('?exp_'.$key)){
if(cache('exp_'.$key)+$expris > time()){
return $this->fail(__('Only one verification code can be sent within %second% seconds',['%second%'=>$expris]));
}
}
$code =\support\Random::numeric(6);
$list[$code] = time();
cache($key,$list);
cache('exp_'.$key,time());
addJob([
'mobile' => $mobile,
'event' => $event,
'code' => $code
],'Sms');
\support\Log::channel('mail')->alert("短信验证码:".$code.',手机号:'.$mobile);
return $this->success(__('SMS sent successfully'),[
'code'=> $debug ? $code : ''
]);
}else{
//TODO 图像验证码没有唯一的KEY
$key = 'captcha_'.$event.'_';
//abcdefghjkmnpqrstuvwxyzABCDEFGHJKMNPQRSTUVWXYZ
$builder = new PhraseBuilder(4, '0123456789');
$captcha = new CaptchaBuilder(null, $builder);
$captcha->build(120);
$code = strtolower($captcha->getPhrase());
$list[$code] = time();
cache($key,$list);
if($request->method() =='GET'){
$img_content = $captcha->get();
return response($img_content, 200, ['Content-Type' => 'image/jpeg']);
}else{
$img_content = $captcha->inline();
return json([
'code' => 0,
'msg' => __('successful'),
'data' => $img_content
]);
}
}
}
/**
* 校验验证码
* @Apidoc\Param("type", type="string",require=true, desc="GET参数,类型,email:邮箱验证码,image:图片验证码")
* @Apidoc\Param("event", type="string",require=true, desc="事件,register:注册,resetpwd:重置密码,withdrawl:提现")
* @Apidoc\Param("email", type="string",require=false, desc="邮箱,可选,仅type==email时必填")
* @Apidoc\Param("code", type="string",require=true, desc="验证码")
*/
public function verify_captcha(Request $request): Response
{
$type = $request->input('type');
$email = $request->post('email');
$mobile = $request->input('mobile');
$event = $request->post('event');
try {
if($type == 'email'){
$result = captcha_verify('email', $event , $email,false);
}elseif($type == 'mobile'){
$result = captcha_verify('mobile', $event , $mobile,false);
}else{
$result = captcha_verify('image', $event , '',false);
}
if(!$result){
return $this->fail(__('Captcha is incorrect'));
}
} catch (\Exception $e) {
return $this->fail($e->getMessage());
}
return $this->success(__('successful'));
}
}
+81
View File
@@ -0,0 +1,81 @@
<?php
namespace app\api\controller;
use app\model\User as UserModel;
use app\model\UserRemark as UserRemarkModel;
use app\model\GroupRemark as GroupRemarkModel;
use support\Request;
use support\Response;
use support\think\Db;
use hg\apidoc\annotation as Apidoc;
/**
* 通讯录
*/
class ContactController extends BaseController{
/**
* 不需要鉴权的方法
* @var array
*/
public $noNeedAuth = ['*'];
/**
* 无需登录及鉴权的方法
* @var array
*/
public $noNeedLogin = [];
/**
* @Apidoc\Title("获取用户好友列表")
* @Apidoc\Method("GET")
*/
function get_friend_list(Request $request): Response
{
$current_user = \support\Jwt::getUser();
$user_id = $current_user->id;
$userID = \support\Encrypt::userIDencode($user_id);
$res = $request->IM->friend()->getFriendList($userID,1,10000);
return $this->success('ok',$res['data']['friendsInfo']);
}
/**
* @Apidoc\Title("好友信息")
* @Apidoc\Method("GET")
* @Apidoc\Param("userID", type="string",require=true, desc="用户ID")
*/
function get_friend_info(Request $request): Response
{
$userID = Input('userID');
if(!$userID){
return $this->error('UserID is Empty');
}
$res = \app\model\User::where('userID',$userID)->find();
return $this->success('ok',$res);
}
/**
* @Apidoc\Title("批量查询好友信息")
* @Apidoc\Method("GET")
* @Apidoc\Param("userIDs", type="string",require=true, desc="用户ID列表,逗号分隔")
*/
function get_friends_info(Request $request): Response
{
$userIDs = Input('userIDs');
if(!$userIDs){
return $this->error('UserID is Empty');
}
$res = \app\model\User::whereIn('userID',$userIDs)->select();
return $this->success('ok',$res);
}
/**
* @Apidoc\Title("批量查询好友信息")
* @Apidoc\Method("GET")
* @Apidoc\Param("userIDs", type="string",require=true, desc="用户ID列表,逗号分隔")
*/
function get_friends_roles(Request $request): Response
{
$userIDs = Input('userIDs');
if(!$userIDs){
return $this->error('UserID is Empty');
}
$res = Db::name('user')->whereIn('userID',$userIDs)->column('role_id','userID');
return $this->success('ok',$res);
}
}
+441
View File
@@ -0,0 +1,441 @@
<?php
namespace app\api\controller;
use app\model\FriendCircle;
use Shopwwi\WebmanFilesystem\FilesystemFactory;
use Shopwwi\WebmanFilesystem\Facade\Storage;
use app\model\User as UserModel;
use app\model\Realname as RealnameModel;
use app\model\FriendCircle as FriendCircleModel;
use app\model\FriendCircleLike as FriendCircleLikeModel;
use app\model\FriendCircleComment as FriendCircleCommentModel;
use support\Request;
use support\Response;
use support\think\Db;
use hg\apidoc\annotation as Apidoc;
/**
* 朋友圈
*/
class FriendCircleController extends BaseController{
/**
* 不需要鉴权的方法
* @var array
*/
public $noNeedAuth = ['*'];
/**
* 无需登录及鉴权的方法
* @var array
*/
public $noNeedLogin = [];
/**
* 朋友圈设置
* @return void
*/
function info(Request $request): Response{
$user_id = Input('user_id');
if($user_id){
$user_id = \support\Encrypt::userIDDecode($user_id);
$json= [
'top_unread_items' =>[],
'unread_item_ids' =>[],
'unread_count' =>0,
'settings' => Db::name('user_extend')->where('user_id',$user_id)->findOrEmpty()
];
return $this->success('ok',$json);
}else{
$user = \support\Jwt::getUser();
if (!$user) {
return $this->fail('请先登录');
}
$user_id = $user->id;
$res = $this->newcount($request);
$response = $res->rawBody();
$json = json_decode($response,true);
$json['data']['settings'] = Db::name('user_extend')->where('user_id',$user_id)->findOrEmpty();
// [
// 'bg' => '',
// ];
$top_unread_items = FriendCircleModel::whereIn('id',$json['data']['unread_item_ids'])
->with(['user' => function($query) {
$query->field('id,nickname,avatar');
}])
->order('id', 'desc')
->limit(0,3)
->select();
$json['data']['top_unread_items'] = $top_unread_items ?: [];
$res->withBody(json_encode($json));
return $res;
}
}
/**
* @Apidoc\Title("列表")
* @Apidoc\Method("GET")
* @Apidoc\Query("page", type="int", require=true, desc="页码",default=1)
* @Apidoc\Query("limit", type="int", require=true, desc="分页大小",default=10)
* @Apidoc\Query("user_id", type="int", require=false, desc="用户ID,不传则获取所有")
*/
function list(Request $request): Response
{
$current_user = \support\Jwt::getUser();
$current_user_id = $current_user ? $current_user->id : 0;
$page = (int)Input('page', 1);
$limit = (int)Input('limit', 10);
$user_id = Input('user_id', 0);
if($user_id){
$user_id = \support\Encrypt::userIDDecode($user_id);
}
$query = FriendCircleModel::where('status', 1)
->whereIn('user_id',$this->getFriendUserIds($current_user_id))
->with(['user' => function($query) {
$query->field('id,nickname,avatar');
}])
->order('created_at', 'desc');
// 如果指定了用户ID,只获取该用户的朋友圈
if ($user_id > 0) {
$query->where('user_id', $user_id);
}
$list = $query->paginate([
'list_rows' => $limit,
'page' => $page,
]);
if(!$user_id){
cache('circle_last_read_id_'.$current_user_id,$list[0]['id']);
}
// 处理每条朋友圈数据
$items = $list->items();
$list->each(function($item) use ($current_user_id){
// 获取点赞列表
$likes = Db::name('friend_circle_like')->alias('f')
->join('user u','u.id=f.user_id')
->where('f.circle_id', $item->id)
->field('f.*,u.avatar,u.nickname')
->order('f.created_at', 'desc')
->limit(20)
->select();
$likes = $likes ? $likes->toArray() : [];
// 检查当前用户是否已点赞
$is_liked = false;
if ($current_user_id > 0) {
$is_liked = null !== array_find($likes,function($item)use($current_user_id){
return $item['user_id'] == $current_user_id;
});
// FriendCircleLikeModel::where('circle_id', $item->id)
// ->where('user_id', $current_user_id)
// ->count() > 0;
}
// 获取评论列表(最新10条)
$comments = FriendCircleCommentModel::where('circle_id', $item->id)
->where('status', 1)
->with(['user' => function($query) {
$query->field('id,nickname,avatar');
}, 'replyUser' => function($query) {
$query->field('id,nickname,avatar');
}])
->order('created_at', 'asc')
->limit(10)
->select();
// 格式化数据
$item->is_liked = $is_liked;
$item->likes = $likes;
$item->comments = $comments;
// 处理图片URL
if (!empty($item->files)) {
$files = is_array($item->files) ? $item->files : json_decode($item->files, true);
if (is_array($files)) {
$item->files = array_map(function($file) {
return cdnurl($file);
}, $files);
} else {
$item->files = [];
}
} else {
$item->files = [];
}
// 处理用户头像
if ($item->user && $item->user->avatar) {
$item->user->avatar = cdnurl($item->user->avatar);
}
// 处理点赞用户头像
foreach ($item->likes as $like) {
if ($like->user) {
$like->avatar = cdnurl($like->avatar);
}
}
// 处理评论用户头像
foreach ($item->comments as $comment) {
if ($comment->user && $comment->user->avatar) {
$comment->user->avatar = cdnurl($comment->user->avatar);
}
if ($comment->replyUser && $comment->replyUser->avatar) {
$comment->replyUser->avatar = cdnurl($comment->replyUser->avatar);
}
}
return $item;
});
return $this->success('ok', $list);
}
/**
* @Apidoc\Title("最近更新的数量")
* @Apidoc\Method("POST")
* @Apidoc\Param("last_see", type="string",require=false, desc="最近查看的时间戳")
*/
function newcount(Request $request): Response
{
$user = \support\Jwt::getUser();
if (!$user) {
return $this->fail('请先登录');
}
$user_id = $user->id;
$circle_last_read_id = cache('circle_last_read_id_'.$user_id) ?: 0;
// 统计从上次查看时间到现在新增的朋友圈数量
$unread_item_ids = FriendCircleModel::where('status', 1)
->whereIn('user_id',$this->getFriendUserIds($user_id))
->where('id', '>', $circle_last_read_id)
->order('id', 'desc')
->column('id');
return $this->success('ok', [
'unread_count' => count($unread_item_ids),
'unread_item_ids'=>$unread_item_ids
]);
}
/**
* @Apidoc\Title("发布朋友圈")
* @Apidoc\Method("POST")
* @Apidoc\Param("body", type="string",require=false, desc="内容")
* @Apidoc\Param("files", type="string",require=false, desc="图片列表(JSON数组)")
*/
function create(Request $request): Response
{
$user = \support\Jwt::getUser();
if (!$user) {
return $this->fail('请先登录');
}
$body = $request->post('content', '');
$files = $request->post('files', '');
$address = $request->post('address', '');
$releaseType = $request->post('releaseType', '');
// 验证内容
if (empty($body)) {
return $this->fail('什么内容都木有啊');
}
// 处理图片列表
$files_array = [];
if (!empty($files)) {
if (is_string($files)) {
$files_array = json_decode($files, true);
} elseif (is_array($files)) {
$files_array = $files;
}
if (!is_array($files_array)) {
return $this->fail('图片列表格式错误');
}
// 限制图片数量
if (count($files_array) > 9) {
return $this->fail('最多只能上传9张图片');
}
}
// 创建朋友圈动态
$circle = FriendCircleModel::create([
'user_id' => $user->id,
'releaseType' => $releaseType,
'body' => $body,
'files' => $files_array,
'address' => $address,
'status' => 1,
]);
return $this->success('发布成功', ['id' => $circle->id,'data' => $circle]);
}
/**
* @Apidoc\Title("发表评论")
* @Apidoc\Method("POST")
* @Apidoc\Param("body", type="string",require=true, desc="内容")
* @Apidoc\Param("id", type="int",require=true, desc="朋友圈动态ID")
* @Apidoc\Param("reply_user_id", type="int",require=false, desc="回复的用户ID(回复评论时使用)")
*/
function comment(Request $request): Response
{
$user = \support\Jwt::getUser();
if (!$user) {
return $this->fail('请先登录');
}
$body = $request->post('body', '');
$circle_id = (int)$request->post('id', 0);
$reply_user_id = (int)$request->post('reply_user_id', 0);
if (empty($body)) {
return $this->fail('评论内容不能为空');
}
if ($circle_id <= 0) {
return $this->fail('朋友圈动态ID错误');
}
// 检查朋友圈动态是否存在
$circle = FriendCircleModel::where('id', $circle_id)
->where('status', 1)
->find();
if (!$circle) {
return $this->fail('朋友圈动态不存在');
}
// 如果回复评论,检查被回复的用户是否存在
if ($reply_user_id > 0) {
$reply_user = UserModel::where('id', $reply_user_id)->find();
if (!$reply_user) {
return $this->fail('被回复的用户不存在');
}
}
// 创建评论
$comment = FriendCircleCommentModel::create([
'circle_id' => $circle_id,
'user_id' => $user->id,
'reply_user_id' => $reply_user_id,
'body' => $body,
'status' => 1,
]);
// 更新朋友圈评论数
$circle->comment_count = FriendCircleCommentModel::where('circle_id', $circle_id)
->where('status', 1)
->count();
$circle->save();
$comment->user = Db::name('user')->where('id',$comment->user_id)->find();
$comment->replyUser=null;
if($comment->reply_user_id){
$comment->replyUser = Db::name('user')->where('id',$comment->reply_user_id)->find();
}
return $this->success('评论成功', $comment);
}
/**
* @Apidoc\Title("点赞")
* @Apidoc\Method("POST")
* @Apidoc\Param("id", type="int",require=true, desc="朋友圈动态ID")
*/
function like(Request $request): Response
{
$user = \support\Jwt::getUser();
if (!$user) {
return $this->fail('请先登录');
}
$circle_id = (int)$request->post('id', 0);
if ($circle_id <= 0) {
return $this->fail('朋友圈动态ID错误');
}
// 检查朋友圈动态是否存在
$circle = FriendCircleModel::where('id', $circle_id)
->where('status', 1)
->find();
if (!$circle) {
return $this->fail('朋友圈动态不存在');
}
// 检查是否已点赞
$like = FriendCircleLikeModel::where('circle_id', $circle_id)
->where('user_id', $user->id)
->find();
if ($like) {
// 取消点赞
$like->delete();
$circle->like_count = max(0, $circle->like_count - 1);
$circle->save();
return $this->success('取消点赞成功', ['is_liked' => false]);
} else {
// 添加点赞
FriendCircleLikeModel::create([
'circle_id' => $circle_id,
'user_id' => $user->id,
]);
$circle->like_count = $circle->like_count + 1;
$circle->save();
return $this->success('点赞成功', ['is_liked' => true]);
}
}
protected function getFriendUserIds($user_id):array{
if (!$user_id) {
return [];
}
$cache_key = 'friend_id_list_'.$user_id;
$result = cache($cache_key) ?: [];
if(count($result) === 0){
$res = request()->IM->friend->getFriendList(\support\Encrypt::userIDencode($user_id));
$friendsInfo = $res['friendsInfo'];
foreach($friendsInfo as $k=>$v){
array_push($result,\support\Encrypt::userIDDecode($v['friendUser']['userID']));
}
cache($cache_key,$result,3600);
}
$result[] = $user_id;
return $result;
}
function delete(Request $request): Response{
$id = $request->post('id');
$user = \support\Jwt::getUser();
if (!$user) {
return $this->fail('请先登录');
}
if($id){
FriendCircleModel::where('id',$id)->where('user_id',$user->id)->delete();
}
return $this->success('删除成功');
}
function upload_bg(Request $request){
try {
$user = \support\Jwt::getUser();
if (!$user) {
return $this->fail('请先登录');
}
$res = $this->_upload($request);
if(is_string($res)){
return $this->fail( $res);
}
Db::name('user_extend')->where('user_id',$user->id)->save([
'moments_banner' => $res[0]['file_name']
]);
//$result->ss = cdnurl($result->url);
//P($result);
return $this->success(__('successful'),[
'url'=>$res[0]['file_name']
]);
}catch (\Exception $e){
return $this->error($e->getMessage());
}
}
}
+41
View File
@@ -0,0 +1,41 @@
<?php
namespace app\api\controller;
use app\model\User as UserModel;
use app\model\UserRemark as UserRemarkModel;
use app\model\GroupRemark as GroupRemarkModel;
use support\Request;
use support\Response;
use support\think\Db;
use hg\apidoc\annotation as Apidoc;
/**
* 好友相关
*/
class FriendController extends BaseController{
/**
* 不需要鉴权的方法
* @var array
*/
public $noNeedAuth = ['*'];
/**
* 无需登录及鉴权的方法
* @var array
*/
public $noNeedLogin = [];
/**
* @Apidoc\Title("搜索用户")
* @Apidoc\Method("GET")
* @Apidoc\Param("nickname", type="string",require=true, desc="昵称")
*/
function search(Request $request): Response
{
$keyword = Input('keyword');
$searchtype = Input('searchtype');
if($searchtype =='id'){
}else{
}
return $this->success('ok');
}
}
+120
View File
@@ -0,0 +1,120 @@
<?php
namespace app\api\controller;
use support\Request;
use support\Response;
use hg\apidoc\annotation as Apidoc;
use app\model\User;
use app\model\Gallery as GalleryModel;
use app\model\Album as AlbumModel;
/**
* 相册的相片
*/
class GalleryController extends BaseController
{
public $noNeedAuth = ['*'];
public $noNeedLogin = [];
/**
* @Apidoc\Title("列表")
* @Apidoc\Method("POST")
* @Apidoc\Param("album_id", type="string", require=true, desc="相册ID")
* @Apidoc\Param("offset", type="int", require=false, desc="偏移量,和页码二选一",default=0)
* @Apidoc\Param("page", type="int", require=false, desc="页码",default=1)
* @Apidoc\Param("limit", type="int", require=true, desc="分页大小",default=10)
*/
function list(Request $request): Response
{
$user = \support\Jwt::getUser();
$limit = $request->post('limit',10);
$offset = $request->post('offset',0);
$album_id = $request->post('album_id') ?: 0;
//$ls = $this->get_user_in_group($group_id);
$query = GalleryModel::where('album_id',$album_id)->order('id','desc');
if($offset){
$list = $query->where('id','<',$offset)->limit(0,$limit);
}else{
$list = $query->paginate($limit);
}
return $this->success('ok',$list);
}
/**
* @Apidoc\Title("上传")
* @Apidoc\Method("POST")
* @Apidoc\Param("album_id", type="string", require=true, desc="相册ID",default=0)
* @Apidoc\Param("title", type="string", require=true, desc="标题")
* @Apidoc\Param("url", type="string", require=true, desc="图片")
* @Apidoc\Param("file", type="file", require=true, desc="图片,没有url得时候必传")
*/
function create(Request $request): Response
{
$user_id = \support\Jwt\JwtToken::getCurrentId();
$res = $this->_upload($request);
if(is_string($res)){
return $this->fail( $res);
}
$album_id = $request->post('album_id') ?: 0;
$album = AlbumModel::find($album_id);
if(!$album){
return $this->fail('相册不存在');
}
$insert_data = [];
foreach($res as $item){
$insert_data[] = [
'user_id' => $user_id,
'group_id' => $album->groupID,
'album_id' => $album_id,
'title' => $item['origin_name'],
'url' => $item['file_name'],
];
}
$result = GalleryModel::saveAll($insert_data);
return $this->success('ok',$result[0]);
}
/**
* @Apidoc\Title("更新")
* @Apidoc\Method("POST")
* @Apidoc\Param("id", type="string", require=true, desc="ID")
* @Apidoc\Param("title", type="string", require=true, desc="标题")
* @Apidoc\Param("url", type="string", require=true, desc="图片")
*/
function update(Request $request): Response
{
$id = $request->input('id');
$title = $request->input('title');
$url = $request->input('url');
$album = GalleryModel::find($id);
if($album){
if($title){
$album->title = $title;
}
if($url){
$album->url = $url;
}
$album->save();
}
return $this->success('ok',$album);
}
/**
* @Apidoc\Title("删除")
* @Apidoc\Method("POST")
* @Apidoc\Param("id", type="string", require=true, desc="ID")
*/
function delete(Request $request): Response
{
$ids = Input('ids');
GalleryModel::whereIn('id',$ids)->delete();
return $this->success('ok');
}
/**
* 获取在群里的角色
* @Apidoc\NotParse()
* @Apidoc\NotDebug()
*/
private function get_user_in_group($groupID='',$userID='')
{
$list = request()->IM->group->getGroupMemberList($groupID,$userID);
return $list;
}
}
+160
View File
@@ -0,0 +1,160 @@
<?php
namespace app\api\controller;
use app\model\Gift as GiftModel;
use app\model\GiftOrder as GiftOrderModel;
use app\model\User as UserModel;
use support\think\Db;
use hg\apidoc\annotation as Apidoc;
/**
* 礼品模块
* @Apidoc\NotParse()
* @Apidoc\NotDebug()
*/
class GiftController extends BaseController{
/**
* 不需要鉴权的方法
* @var array
*/
public $noNeedAuth = ['*'];
public $noNeedLogin = ['detail','list'];
/**
* 列表
* @Apidoc\Query("kw", type="string", require=false, desc="搜索关键字")
* @Apidoc\Query("page", type="int", require=true, desc="页码",default=1)
* @Apidoc\Query("limit", type="int", require=true, desc="分页大小",default=10)
*/
public function list(){
$limit = (int)input('limit',10);
$model = GiftModel::where('status',1);
$list = $model->order('weight desc,id asc')->paginate($limit);
return $this->success(__('successful'),$list->toArray());
}
/**
* 详情
* @Apidoc\Query("id", type="int", require=true, desc="ID")
*/
public function detail(){
try{
$user = \support\Jwt::getUser();
}catch(\Exception $e){
$user = ['id'=>0,'role_id'=>0];
}
$appid = input('id');
if(!$appid){
return $this->error(__("Product does not exist"));
}
/** @var GiftModel $gift */
$gift = GiftModel::where('id',$appid)->find();
//->cache(true,86400,'product_detail')
if(!$gift) {
return $this->error(__("Product does not exist"));
}
if($user['id']){
/** @var GiftOrderModel $total_quantity_user */
$total_quantity_user = GiftOrderModel::where('gift_id',$gift->id)->where('user_id',$user['id'])->sum('quantity');
$total_quantity_system = $gift->user_quantity ?: 99999999;
$max_quantity = $total_quantity_system-$total_quantity_user;
$max_quantity= $max_quantity < 0 ? 0: $max_quantity;
$gift->max_quantity = $max_quantity;
$gift->total_quantity_user = $total_quantity_user;
}else{
$gift->total_quantity_user = 0;
$gift->max_quantity = 0;
}
return $this->success(__('successful'),$gift->toArray());
}
/**
* 列表
* @Apidoc\Query("status", type="int", require=false, desc="状态")
* @Apidoc\Query("page", type="int", require=true, desc="页码",default=1)
* @Apidoc\Query("limit", type="int", require=true, desc="分页大小",default=10)
*/
public function record(){
$limit = (int)input('limit',10);
$page = (int)input('page',1);
$status = input('status','all');
$user_id = \support\Jwt\JwtToken::getCurrentId();
$model = GiftOrderModel::with(['gift'])->where('user_id',$user_id);
if($status!='all'){
$model = $model->where('status',$status);
}
$list = $model->order('created_at desc')
->paginate($limit);
return $this->success(__('successful'),$list->toArray());
}
/**
* 购买产品
* @Apidoc\Method("POST")
* @Apidoc\Param("gift_id", type="string", require=true, desc="产品ID")
* @Apidoc\Param("quantity", type="int", require=true, desc="数量")
* @Apidoc\Param("trade_password", type="string", require=true, desc="交易密码")
*/
public function create()
{
$user = \support\Jwt::getUser();
$data = [
'user_id' => $user->id,
'gift_id'=> input('gift_id'),
'quantity' => (int)input('quantity'),
'denomination' => input('denomination',0),
'status' => 0
];
if(!$data['gift_id']){
return $this->error(__('Gift is incorrect'));
}
/** @var GiftModel $gift */
$gift = GiftModel::where('id',$data['gift_id'])->find();
if(!$gift){
return $this->error(__('Gift is incorrect'));
}
// if(!in_array($data['denomination'],$gift->amounts)){
// return $this->error(__('Denomination is incorrect'));
// }
if($data['quantity'] < 1){
return $this->error(__('Quantity is incorrect'));
}
if($gift->stock < $data['quantity']){
return $this->error(__('Gift stock is insufficient'));
}
//$data['amount'] = $data['denomination'] * $data['quantity'];
$data['denomination'] = $gift->price;
$data['amount'] = $gift->price * $data['quantity'];
$total_quantity_user = GiftOrderModel::where('gift_id',$gift->id)->where('user_id',$user['id'])->sum('quantity');
$total_quantity_system = $gift->user_quantity ?: 99999999;
$can_purchase = $total_quantity_system-$total_quantity_user;
$can_purchase= $can_purchase < 0 ? 0: $can_purchase;
if($can_purchase < $data['quantity']){
return $this->error(__('You can only purchase %max_quantity% copies',[
'%max_quantity%' => $can_purchase
]));
}
//验证交易密码
$trade_password = input('trade_password');
\support\Jwt::verify_trade_password($trade_password);
//var_dump($user);
//验证余额
if($data['amount'] > $user->score){
return $this->error(__('Insufficient balance'));
}
Db::startTrans();
try{
$data = GiftOrderModel::create($data);
$gift->stock = $gift->stock - $data['quantity'];
$gift->sales = $gift->sales + $data['quantity'];
$gift->save();
UserModel::score($data['user_id'],-$data['amount'],\app\enum\BalanceType::GIFT_BUY,$data['id']);
Hook('gift.buy',$data);
Db::commit();
}catch(\Exception $e){
Db::rollback();
return $this->error($e->getMessage());
}
return $this->success(__('successful'));
}
}
+127
View File
@@ -0,0 +1,127 @@
<?php
namespace app\api\controller;
use support\Request;
use support\Response;
use hg\apidoc\annotation as Apidoc;
use app\model\User;
use app\model\Gallery as AlbumModel;
/**
* 群组管理
*/
class GroupController extends BaseController
{
public $noNeedAuth = ['*'];
public $noNeedLogin = [];
/**
* @Apidoc\Title("群相片列表")
* @Apidoc\Method("POST")
* @Apidoc\Param("group_id", type="string", require=true, desc="群ID")
* @Apidoc\Param("offset", type="int", require=true, desc="偏移量",default=99999999999999)
* @Apidoc\Param("limit", type="int", require=true, desc="分页大小",default=10)
*/
function album_list(Request $request): Response
{
$user = \support\Jwt::getUser();
$limit = $request->post('limit',10);
$offset = $request->post('offset',0);
$group_id = $request->post('groupID') ?:$request->post('group_id');
//$ls = $this->get_user_in_group($group_id);
$list = AlbumModel::where('group_id',$group_id)
->where('id','<',$offset)
->order('id','desc')
->limit(0,$limit)
->select();
return $this->success('ok',$list);
}
/**
* @Apidoc\Title("上传相片")
* @Apidoc\Method("POST")
* @Apidoc\Param("group_id", type="string", require=true, desc="群ID")
* @Apidoc\Param("title", type="string", require=true, desc="标题")
* @Apidoc\Param("url", type="string", require=true, desc="图片")
*/
function album_create(Request $request): Response
{
$user_id = \support\Jwt\JwtToken::getCurrentId();
$res = $this->_upload($request);
if(is_string($res)){
return $this->fail( $res);
}
$groupID = $request->post('groupID');
$insert_data = [];
foreach($res as $item){
$insert_data[] = [
'user_id' => $user_id,
'group_id' => $groupID,
'title' => $item['origin_name'],
'url' => $item['file_name'],
];
}
$result = AlbumModel::saveAll($insert_data);
return $this->success('ok',$result[0]);
}
/**
* @Apidoc\Title("更新相片")
* @Apidoc\Method("POST")
* @Apidoc\Param("id", type="string", require=true, desc="ID")
* @Apidoc\Param("title", type="string", require=true, desc="标题")
* @Apidoc\Param("url", type="string", require=true, desc="图片")
*/
function album_update(Request $request): Response
{
$id = $request->input('id');
$data = $request->input('data');
$album = AlbumModel::find($id);
$album->update($data);
return $this->success('ok',$album);
}
/**
* @Apidoc\Title("删除相片")
* @Apidoc\Method("POST")
* @Apidoc\Param("id", type="string", require=true, desc="ID")
*/
function album_delete(Request $request): Response
{
$ids = Input('ids');
//$album = AlbumModel::whereIn('id',condition: $ids)->select();
//$album->delete();
AlbumModel::whereIn('id',condition: $ids)->delete();
return $this->success('ok');
}
/**
* 获取再群里的角色
* @Apidoc\NotParse()
* @Apidoc\NotDebug()
*/
private function get_user_in_group($group_id='',$user_id='')
{
$list = request()->IM->group->getGroupMemberList($group_id,$user_id);
return $list;
}
/**
* 头像上传
* @Apidoc\Method("POST")
* @Apidoc\Param("file", type="File", require=true, desc="文件")
*/
public function avatar(Request $request)
{
//单文件上传
$groupID = $request->post('groupID');
if(!$groupID){
return $this->fail(__('Invalid parameter'));
}
$res = $this->_upload($request);
if(is_string($res)){
return $this->fail( $res);
}
$data = [
'groupID' => $groupID,
'faceURL' => $res[0]['file_name'],
];
$list = request()->IM->group->setGroupInfo($data);
return $this->success(__('successful'),$data);
}
}
+17
View File
@@ -0,0 +1,17 @@
<?php
namespace app\api\controller;
use Response;
use support\Request;
use hg\apidoc\annotation as Apidoc;
/**
* 主控制器
* @Apidoc\NotParse()
* @Apidoc\NotDebug()
*/
class IndexController extends BaseController{
public $noNeedLogin=['index','chart'];
public function index(){
return $this->success(__("Unverified address"));
}
}
+32
View File
@@ -0,0 +1,32 @@
<?php
namespace app\api\controller;
use app\model\User as UserModel;
use support\Request;
use support\Response;
use support\think\Db;
use hg\apidoc\annotation as Apidoc;
/**
* 消息控制器
* @Apidoc\NotParse()
* @Apidoc\NotDebug()
*/
class MessageController extends BaseController{
/**
* 购买产品
* @Apidoc\Method("POST")
* @Apidoc\Param("gift_id", type="string", require=true, desc="产品ID")
* @Apidoc\Param("quantity", type="int", require=true, desc="数量")
* @Apidoc\Param("trade_password", type="string", require=true, desc="交易密码")
*/
function delete(Request $request):Response{
$im = $request->IM;
$data = $im->message->sendBusinessNotification('system',\support\Encrypt::userIDencode(100007),[
'contentType' => 101,
'textElem' => [
'content' => '欢迎使用4'.Config('site.name')
]
],'group');
return $this->success('ok');
}
}
+473
View File
@@ -0,0 +1,473 @@
<?php
namespace app\api\controller;
use app\model\User as UserModel;
use app\model\FriendCircle as FriendCircleModel;
use app\model\FriendCircleLike as FriendCircleLikeModel;
use app\model\FriendCircleComment as FriendCircleCommentModel;
use support\Request;
use support\Response;
use support\think\Db;
use hg\apidoc\annotation as Apidoc;
/**
* 新朋友圈
*/
class MomentsController extends BaseController
{
public $noNeedAuth = ['*'];
public $noNeedLogin = [];
public $user_display_fields = 'id,userID,nickname,avatar';
protected function getUserSettings(int $user_id): array
{
$result = Db::name('user_extend')
->where('user_id', $user_id)
->field('moments_allow_view_days,moments_banner')
->findOrEmpty();
return $result;
}
protected function processAvatar($data): void
{
if (is_array($data)) {
if (!empty($data['avatar'])) {
$data['avatar'] = cdnurl($data['avatar']);
}
} elseif (is_object($data) && !empty($data->avatar)) {
$data->avatar = cdnurl($data->avatar);
}
}
function info(Request $request): Response
{
$userID = Input('userID');
if ($userID) {
$user_id = \support\Encrypt::userIDDecode($userID);
$json = [
'top_unread_items' => [],
'unread_item_ids' => [],
'unread_count' => 0,
'settings' => $this->getUserSettings($user_id)
];
return $this->success('ok', $json);
}
$user = \support\Jwt::getUser();
if (!$user) {
return $this->fail('请先登录');
}
$res = $this->newcount($request);
$response = $res->rawBody();
$json = json_decode($response, true);
$json['data']['settings'] = $this->getUserSettings($user->id);
$top_unread_items = FriendCircleModel::whereIn('id', $json['data']['unread_item_ids'])
->with(['user' => function ($query) {
$query->field($this->user_display_fields);
}])
->order('id', 'desc')
->limit(0, 3)
->select();
$json['data']['top_unread_items'] = $top_unread_items ?: [];
$res->withBody(json_encode($json));
return $res;
}
/**
* @Apidoc\Title("列表")
* @Apidoc\Method("GET")
* @Apidoc\Query("userID", type="string", require=false, desc="用户userID,不传则获取所有")
* @Apidoc\Query("page", type="int", require=true, desc="页码", default=1)
* @Apidoc\Query("limit", type="int", require=true, desc="分页大小", default=10)
*/
function list(Request $request): Response
{
$current_user = \support\Jwt::getUser();
$current_user_id = $current_user ? $current_user->id : 0;
$page = (int)Input('page', 1);
$limit = (int)Input('limit', 10);
$userID = Input('userID');
$query = FriendCircleModel::where('status', 1)
->with(['user' => function ($query) {
$query->field($this->user_display_fields);
}])
->order('created_at', 'desc');
if ($userID) {
$user_id = \support\Encrypt::userIDDecode($userID);
$query->where('user_id', $user_id);
} else {
$current_userID = $current_user_id > 0 ? \support\Encrypt::userIDencode($current_user_id) : '';
$friendIds = $this->getFriendUserIds($current_userID);
if (empty($friendIds)) {
return $this->success('ok', []);
}
$query->whereIn('user_id', $friendIds);
}
$list = $query->paginate([
'list_rows' => $limit,
'page' => $page,
]);
if (!$userID && $list->count() > 0) {
cache('circle_last_read_id_' . $current_user_id, $list[0]['id']);
}
$list->each(function ($item) use ($current_user_id) {
$likes = Db::name('friend_circle_like')->alias('f')
->join('user u', 'u.id=f.user_id')
->where('f.circle_id', $item->id)
->field('f.*,u.userID,u.avatar,u.nickname')
->order('f.created_at', 'desc')
->limit(20)
->select();
$likes = $likes ? $likes->toArray() : [];
$is_liked = false;
if ($current_user_id > 0) {
$is_liked = null !== array_find($likes, function ($like) use ($current_user_id) {
return $like['user_id'] == $current_user_id;
});
}
$comments = FriendCircleCommentModel::where('circle_id', $item->id)
->where('status', 1)
->with(['user' => function ($query) {
$query->field($this->user_display_fields);
}, 'replyUser' => function ($query) {
$query->field($this->user_display_fields);
}])
->order('created_at', 'asc')
->limit(10)
->select();
$item->is_liked = $is_liked;
$item->likes = $likes;
$item->comments = $comments;
if (!empty($item->files)) {
$files = is_array($item->files) ? $item->files : json_decode($item->files, true);
$item->files = is_array($files) ? array_map('cdnurl', $files) : [];
} else {
$item->files = [];
}
if ($item->user && $item->user->avatar) {
$item->user->avatar = cdnurl($item->user->avatar);
}
$likes = $item->likes;
if (is_array($likes) || $likes instanceof \Traversable) {
foreach ($likes as &$like) {
if (!empty($like['avatar'])) {
$like['avatar'] = cdnurl($like['avatar']);
}
}
unset($like);
$item->likes = $likes;
}
foreach ($item->comments as $comment) {
if ($comment->user && $comment->user->avatar) {
$comment->user->avatar = cdnurl($comment->user->avatar);
}
if ($comment->replyUser && $comment->replyUser->avatar) {
$comment->replyUser->avatar = cdnurl($comment->replyUser->avatar);
}
}
return $item;
});
return $this->success('ok', $list);
}
/**
* @Apidoc\Title("最近更新的数量")
* @Apidoc\Method("POST")
* @Apidoc\Param("last_see", type="string", require=false, desc="最近查看的时间戳")
*/
function newcount(Request $request): Response
{
$user = \support\Jwt::getUser();
if (!$user) {
return $this->fail('请先登录');
}
$user_id = $user->id;
$circle_last_read_id = cache('circle_last_read_id_' . $user_id) ?: 0;
$userID = \support\Encrypt::userIDencode($user_id);
$unread_item_ids = FriendCircleModel::where('status', 1)
->whereIn('user_id', $this->getFriendUserIds($userID))
->where('id', '>', $circle_last_read_id)
->order('id', 'desc')
->column('id');
return $this->success('ok', [
'unread_count' => count($unread_item_ids),
'unread_item_ids' => $unread_item_ids
]);
}
/**
* @Apidoc\Title("发布朋友圈")
* @Apidoc\Method("POST")
* @Apidoc\Param("body", type="string", require=false, desc="内容")
* @Apidoc\Param("files", type="string", require=false, desc="图片列表(JSON数组)")
*/
function create(Request $request): Response
{
$user = \support\Jwt::getUser();
if (!$user) {
return $this->fail('请先登录');
}
$body = $request->post('content', '');
$files = $request->post('files', '');
$address = $request->post('address', '');
$releaseType = $request->post('releaseType', '');
if (empty($body)) {
return $this->fail('什么内容都木有啊');
}
$files_array = [];
if (!empty($files)) {
if (is_string($files)) {
$files_array = json_decode($files, true);
} elseif (is_array($files)) {
$files_array = $files;
}
if (!is_array($files_array)) {
return $this->fail('图片列表格式错误');
}
if (count($files_array) > 9) {
return $this->fail('最多只能上传9张图片');
}
}
$circle = FriendCircleModel::create([
'user_id' => $user->id,
'releaseType' => $releaseType,
'body' => $body,
'files' => $files_array,
'address' => $address,
'status' => 1,
]);
return $this->success('发布成功', ['id' => $circle->id, 'data' => $circle]);
}
/**
* @Apidoc\Title("发表评论")
* @Apidoc\Method("POST")
* @Apidoc\Param("body", type="string", require=true, desc="内容")
* @Apidoc\Param("id", type="int", require=true, desc="朋友圈动态ID")
* @Apidoc\Param("reply_userID", type="string", require=false, desc="回复的用户userID(回复评论时使用)")
*/
function comment(Request $request): Response
{
$user = \support\Jwt::getUser();
if (!$user) {
return $this->fail('请先登录');
}
$body = $request->post('body', '');
$circle_id = (int)$request->post('id');
$reply_userID = $request->post('reply_userID', '');
if (empty($body)) {
return $this->fail('评论内容不能为空');
}
if ($circle_id <= 0) {
return $this->fail('朋友圈动态ID错误');
}
$circle = FriendCircleModel::where('id', $circle_id)
->where('status', 1)
->find();
if (!$circle) {
return $this->fail('朋友圈动态不存在');
}
$reply_user_id = 0;
if (!empty($reply_userID)) {
$reply_user_id = (int)\support\Encrypt::userIDDecode($reply_userID);
}
if ($reply_user_id > 0) {
$reply_user = UserModel::where('id', $reply_user_id)->find();
if (!$reply_user) {
return $this->fail('被回复的用户不存在');
}
}
$comment = FriendCircleCommentModel::create([
'circle_id' => $circle_id,
'user_id' => $user->id,
'reply_user_id' => $reply_user_id,
'body' => $body,
'status' => 1,
]);
$circle->comment_count = FriendCircleCommentModel::where('circle_id', $circle_id)
->where('status', 1)
->count();
$circle->save();
$comment->user = Db::name('user')->field($this->user_display_fields)->where('id', $comment->user_id)->find();
$comment->replyUser = null;
if ($comment->reply_user_id) {
$comment->replyUser = Db::name('user')->field($this->user_display_fields)->where('id', $comment->reply_user_id)->find();
}
return $this->success('评论成功', $comment);
}
/**
* @Apidoc\Title("点赞")
* @Apidoc\Method("POST")
* @Apidoc\Param("id", type="int", require=true, desc="朋友圈动态ID")
*/
function like(Request $request): Response
{
$user = \support\Jwt::getUser();
if (!$user) {
return $this->fail('请先登录');
}
$circle_id = (int)$request->post('id', 0);
if ($circle_id <= 0) {
return $this->fail('朋友圈动态ID错误');
}
$circle = FriendCircleModel::where('id', $circle_id)
->where('status', 1)
->find();
if (!$circle) {
return $this->fail('朋友圈动态不存在');
}
$like = FriendCircleLikeModel::where('circle_id', $circle_id)
->where('user_id', $user->id)
->find();
if ($like) {
$like->delete();
$circle->like_count = max(0, $circle->like_count - 1);
$circle->save();
return $this->success('取消点赞成功', ['is_liked' => false]);
}
FriendCircleLikeModel::create([
'circle_id' => $circle_id,
'user_id' => $user->id,
]);
$circle->like_count = $circle->like_count + 1;
$circle->save();
return $this->success('点赞成功', ['is_liked' => true]);
}
protected function getFriendUserIds($userID): array
{
if (!$userID) {
return [];
}
$cache_key = 'friend_id_list_' . $userID;
$result = cache($cache_key) ?: [];
if (count($result) === 0) {
$res = request()->IM->friend->getFriendList($userID);
$friendsInfo = $res['friendsInfo'] ?? [];
foreach ($friendsInfo as $v) {
$result[] = \support\Encrypt::userIDDecode($v['friendUser']['userID']);
}
cache($cache_key, $result, 3600);
}
$result[] = \support\Encrypt::userIDDecode($userID);
return $result;
}
/**
* 删除朋友圈
* @Apidoc\Method("POST")
* @Apidoc\Param("id", type="int", require=true, desc="朋友圈动态ID")
* @return Response
*/
function delete(Request $request): Response
{
$id = (int)$request->post('id');
$user = \support\Jwt::getUser();
if (!$user) {
return $this->fail('请先登录');
}
if ($id > 0) {
FriendCircleModel::where('id', $id)->where('user_id', $user->id)->delete();
}
return $this->success('删除成功');
}
/**
* 设置朋友圈背景
* @param Request $request
* @return Response
*/
function upload_bg(Request $request): Response
{
return $this->setBanner($request);
}
/**
* 设置朋友圈背景
* @Apidoc\Method("POST")
* @Apidoc\Param("file", type="File", require=true, desc="文件")
* @return Response
*/
function setBanner(Request $request): Response
{
try {
$user = \support\Jwt::getUser();
if (!$user) {
return $this->fail('请先登录');
}
$res = $this->_upload($request);
if (is_string($res)) {
return $this->fail($res);
}
$exist = Db::name('user_extend')->where('user_id', $user->id)->find();
if ($exist) {
Db::name('user_extend')
->where('user_id', $user->id)
->update(['moments_banner' => $res[0]['file_name']]);
} else {
Db::name('user_extend')->insert([
'user_id' => $user->id,
'moments_banner' => $res[0]['file_name'],
]);
}
return $this->success(__('successful'), [
'url' => $res[0]['file_name']
]);
} catch (\Exception $e) {
return $this->error($e->getMessage());
}
}
}
+190
View File
@@ -0,0 +1,190 @@
<?php
namespace app\api\controller;
use app\model\User as UserModel;
use app\model\Card;
use app\model\Cdkey;
use support\think\Db;
use hg\apidoc\annotation as Apidoc;
use Tinywan\Validate\Facade\Validate;
/**
* 通行证
*/
class PassportController extends BaseController{
/**
* 不需要鉴权的方法
* @var array
*/
public $noNeedAuth = ['*'];
public $noNeedLogin = [];
/**
* 安全验证
* @Apidoc\Method("POST")
* @Apidoc\Param("verify_type", type="string", require=true, desc="验证类型,email或mobile")
* @Apidoc\Param("code", type="string", require=true, desc="验证码,event=verify")
*/
public function security_verify()
{
$user = \support\Jwt::getUser();
$verify_type = input('verify_type');
if($verify_type=='mobile'){
captcha_verify('mobile', 'verify', $user->mobile);
}else if($verify_type == 'email'){
captcha_verify('email', 'verify', $user->email);
}else{
return $this->error(__('Invalid verify type'));
}
return $this->success(__('Security verify successfully'));
}
/**
* 绑定手机号
* @Apidoc\Method("POST")
* @Apidoc\Param("region", type="string", require=true, desc="区域代码")
* @Apidoc\Param("mobile", type="string", require=true, desc="手机号")
* @Apidoc\Param("code", type="string", require=true, desc="验证码,event=bind_mobile")
*/
public function bind_mobile()
{
$user = \support\Jwt::getUser();
$mobile = input('mobile');
$region = input('region','+86');
$region = str_replace('+','',$region);
// 验证手机号格式
if (!$mobile || !Validate::regex($mobile, "^1\d{10}$")) {
return $this->error(__('Incorrect mobile number format'));
}
// 验证手机号唯一性
if (UserModel::where('mobile', $mobile)->where('region',$region)->where('id', '<>', $user->id)->find()) {
return $this->error(__('Mobile number already exists'));
}
// 验证验证码
captcha_verify('mobile', 'bind_mobile', $mobile);
// 更新用户信息
$user->mobile = $mobile;
$user->region = $region;
//$user->mobile_verify = 1;
$user->save();
return $this->success(__('Mobile number bound successfully'));
}
/**
* 绑定邮箱
* @Apidoc\Method("POST")
* @Apidoc\Param("email", type="string", require=true, desc="邮箱")
* @Apidoc\Param("code", type="string", require=true, desc="验证码,event=bind_email")
*/
public function bind_email()
{
$user = \support\Jwt::getUser();
$email = input('email');
// 验证邮箱格式
if (!$email || !Validate::email($email)) {
return $this->error(__('Incorrect email format'));
}
// 验证邮箱唯一性
if (UserModel::where('email', $email)->where('id', '<>', $user->id)->find()) {
return $this->error(__('Email already exists'));
}
captcha_verify('email', 'bind_email', $email);
// 更新用户信息
$user->email = $email;
//$user->email_verify = 1;
$user->save();
return $this->success(__('Email bound successfully'));
}
/**
* 绑定用户名
* @Apidoc\Method("POST")
* @Apidoc\Param("username", type="string", require=true, desc="用户名")
* @Apidoc\Param("verify_type", type="string", require=true, desc="验证类型,email或mobile")
* @Apidoc\Param("code", type="string", require=true, desc="验证码,event=bind_username")
*/
public function bind_username()
{
$user = \support\Jwt::getUser();
$username = input('username');
$verify_type = input('verify_type');
// 验证用户名格式
if (!$username || strlen($username) < 3 || strlen($username) > 20) {
return $this->error(__('Username length must be between 3 and 20 characters'));
}
// 验证用户名唯一性
if (UserModel::where('username', $username)->where('id', '<>', $user->id)->find()) {
return $this->error(__('Username already exists'));
}
if($verify_type == 'mobile'){
captcha_verify('mobile', 'bind_username', $user->mobile);
}else if($verify_type == 'email'){
captcha_verify('email', 'bind_username', $user->email);
}
// 更新用户信息
$user->username = $username;
$user->save();
return $this->success(__('Username bound successfully'));
}
/**
* 解绑手机号
* @Apidoc\Method("POST")
* @Apidoc\Param("code", type="string", require=true, desc="验证码,event=unbind_mobile")
*/
public function unbind_mobile()
{
$user = \support\Jwt::getUser();
if (!$user->mobile) {
return $this->error(__('Mobile number not bound'));
}
// 验证验证码
captcha_verify('mobile', 'unbind_mobile', $user->mobile);
// 更新用户信息
$user->mobile = '';
$user->mobile_verify = 0;
$user->save();
return $this->success(__('Mobile number unbound successfully'));
}
/**
* 解绑邮箱
* @Apidoc\Method("POST")
* @Apidoc\Param("code", type="string", require=true, desc="验证码,event=unbind_email")
*/
public function unbind_email()
{
$user = \support\Jwt::getUser();
if (!$user->email) {
return $this->error(__('Email not bound'));
}
// 验证验证码
captcha_verify('email', 'unbind_email', $user->email);
// 更新用户信息
$user->email = '';
$user->email_verify = 0;
$user->save();
return $this->success(__('Email unbound successfully'));
}
}
+731
View File
@@ -0,0 +1,731 @@
<?php
namespace app\api\controller;
use support\Request;
use support\Response;
use Yansongda\Pay\Pay;
use Yansongda\Pay\Log;
use think\facade\Db;
use support\Log as WebmanLog;
use hg\apidoc\annotation as Apidoc;
use app\enum\Payment\Method;
use app\enum\Payment\Status;
use app\enum\Payment\Type;
/**
* 支付控制器
* @Apidoc\Title("支付管理")
* @Apidoc\Group("payment")
*/
class PaymentController extends BaseController
{
/**
* 支付宝支付下单
* @Apidoc\Title("支付宝支付下单")
* @Apidoc\Url("/api/payment/alipay/order")
* @Apidoc\Method("POST")
* @Apidoc\Param("amount", "float", "支付金额", true, "0.01")
* @Apidoc\Param("order_no", "string", "订单号", true, "20260409001")
* @Apidoc\Param("subject", "string", "订单标题", true, "测试商品")
* @Apidoc\Param("body", "string", "订单描述", false, "测试商品描述")
* @Apidoc\Param("type", "string", "订单类型: recharge(充值), goods(商品), service(服务), other(其他)", false, "goods")
* @Apidoc\Return("success", "object", "成功响应", "pay_url|string|支付链接,order_no|string|订单号")
* @Apidoc\Return("error", "object", "失败响应", "code|int|错误码,msg|string|错误信息")
* @param Request $request
* @return Response
*/
public function alipay_order(Request $request): Response
{
try {
$params = $request->post();
// 验证参数
if (!isset($params['amount']) || !isset($params['order_no']) || !isset($params['subject'])) {
return $this->error('缺少必要参数');
}
$amount = $params['amount'];
$orderNo = $params['order_no'];
$subject = $params['subject'];
$body = $params['body'] ?? $subject;
$type = $params['type'] ?? 'goods';
// 获取支付宝配置
$config = config('payment.alipay.default');
// 构建支付参数
$payOrder = [
'out_trade_no' => $orderNo,
'total_amount' => $amount,
'subject' => $subject,
'body' => $body,
];
// 发起支付
$result = Pay::alipay($config)->web($payOrder);
// 记录支付订单
$this->record_payment_order($orderNo, 'alipay', $amount, $subject, $type);
return $this->success('ok',[
'pay_url' => $result->getTargetUrl(),
'order_no' => $orderNo
]);
} catch (\Exception $e) {
WebmanLog::error('支付宝支付下单失败: ' . $e->getMessage());
return $this->error('支付下单失败: ' . $e->getMessage());
}
}
/**
* 支付宝同步回调
* @Apidoc\Title("支付宝同步回调")
* @Apidoc\Url("/api/payment/alipay/return")
* @Apidoc\Method("GET")
* @Apidoc\Param("out_trade_no", "string", "订单号", true)
* @Apidoc\Param("trade_no", "string", "支付宝交易号", true)
* @Apidoc\Param("trade_status", "string", "交易状态", true)
* @Apidoc\Return("redirect", "string", "跳转到成功或失败页面")
* @param Request $request
* @return Response
*/
public function alipay_return(Request $request): Response
{
try {
$config = config('payment.alipay.default');
$data = $request->all();
// 验证回调数据
$result = Pay::alipay($config)->verify($data);
// 处理支付结果
$orderNo = $result->out_trade_no;
$tradeNo = $result->trade_no;
$status = $result->trade_status;
// 更新订单状态
$this->update_payment_order($orderNo, $tradeNo, $status);
// 跳转到成功页面
return redirect('/api/payment/success?order_no=' . $orderNo);
} catch (\Exception $e) {
WebmanLog::error('支付宝同步回调失败: ' . $e->getMessage());
return redirect('/api/payment/fail?error=' . urlencode($e->getMessage()));
}
}
/**
* 支付宝异步回调
* @Apidoc\Title("支付宝异步回调")
* @Apidoc\Url("/api/payment/alipay/notify")
* @Apidoc\Method("POST")
* @Apidoc\Param("out_trade_no", "string", "订单号", true)
* @Apidoc\Param("trade_no", "string", "支付宝交易号", true)
* @Apidoc\Param("trade_status", "string", "交易状态", true)
* @Apidoc\Return("string", "string", "返回success或fail")
* @param Request $request
* @return string
*/
public function alipay_notify(Request $request): string
{
try {
$config = config('payment.alipay.default');
$data = $request->all();
// 验证回调数据
$result = Pay::alipay($config)->verify($data);
// 处理支付结果
$orderNo = $result->out_trade_no;
$tradeNo = $result->trade_no;
$status = $result->trade_status;
// 更新订单状态
$this->update_payment_order($orderNo, $tradeNo, $status);
// 返回成功
return 'success';
} catch (\Exception $e) {
WebmanLog::error('支付宝异步回调失败: ' . $e->getMessage());
return 'fail';
}
}
/**
* 微信支付下单
* @Apidoc\Title("微信支付下单")
* @Apidoc\Url("/api/payment/wechat/order")
* @Apidoc\Method("POST")
* @Apidoc\Param("amount", "float", "支付金额", true, "0.01")
* @Apidoc\Param("order_no", "string", "订单号", true, "20260409001")
* @Apidoc\Param("body", "string", "订单描述", true, "测试商品描述")
* @Apidoc\Param("trade_type", "string", "交易类型: JSAPI, NATIVE, APP, MWEB", false, "JSAPI")
* @Apidoc\Param("openid", "string", "微信用户openid(JSAPI模式需要)", false, "o123456")
* @Apidoc\Param("type", "string", "订单类型: recharge(充值), goods(商品), service(服务), other(其他)", false, "goods")
* @Apidoc\Return("success", "object", "成功响应", "pay_data|object|支付数据,order_no|string|订单号")
* @Apidoc\Return("error", "object", "失败响应", "code|int|错误码,msg|string|错误信息")
* @param Request $request
* @return Response
*/
public function wechat_order(Request $request): Response
{
try {
$params = $request->post();
// 验证参数
if (!isset($params['amount']) || !isset($params['order_no']) || !isset($params['body'])) {
return $this->error('缺少必要参数');
}
$amount = $params['amount'];
$orderNo = $params['order_no'];
$body = $params['body'];
$tradeType = $params['trade_type'] ?? 'JSAPI';
$openid = $params['openid'] ?? '';
$type = $params['type'] ?? 'goods';
// 获取微信支付配置
$config = config('payment.wechat.default');
// 构建支付参数
$payOrder = [
'out_trade_no' => $orderNo,
'total_fee' => $amount * 100, // 微信支付金额单位为分
'body' => $body,
'trade_type' => $tradeType,
];
// JSAPI支付需要openid
if ($tradeType === 'JSAPI' && $openid) {
$payOrder['openid'] = $openid;
}
// 发起支付
$result = Pay::wechat($config)->order($payOrder);
// 记录支付订单
$this->record_payment_order($orderNo, 'wechat', $amount, $body, $type);
return $this->success('ok',[
'pay_data' => $result->toArray(),
'order_no' => $orderNo
]);
} catch (\Exception $e) {
WebmanLog::error('微信支付下单失败: ' . $e->getMessage());
return $this->error('支付下单失败: ' . $e->getMessage());
}
}
/**
* 微信支付回调
* @Apidoc\Title("微信支付回调")
* @Apidoc\Url("/api/payment/wechat/notify")
* @Apidoc\Method("POST")
* @Apidoc\Param("out_trade_no", "string", "订单号", true)
* @Apidoc\Param("transaction_id", "string", "微信交易号", true)
* @Apidoc\Param("result_code", "string", "业务结果", true)
* @Apidoc\Return("xml", "string", "返回XML格式的成功或失败响应")
* @param Request $request
* @return string
*/
public function wechat_notify(Request $request): string
{
try {
$config = config('payment.wechat.default');
$data = $request->all();
// 验证回调数据
$result = Pay::wechat($config)->verify($data);
// 处理支付结果
$orderNo = $result->out_trade_no;
$tradeNo = $result->transaction_id;
$status = $result->result_code === 'SUCCESS' ? 'SUCCESS' : 'FAIL';
// 更新订单状态
$this->update_payment_order($orderNo, $tradeNo, $status);
// 返回成功
return Pay::wechat($config)->success();
} catch (\Exception $e) {
WebmanLog::error('微信支付回调失败: ' . $e->getMessage());
return Pay::wechat(config('payment.wechat.default'))->fail();
}
}
/**
* 记录支付订单
* @param string $orderNo
* @param string $payType
* @param float $amount
* @param string $subject
* @param string $type
*/
private function record_payment_order(string $orderNo, string $payType, float $amount, string $subject, string $type = 'goods'): void
{
try {
Db::name('payment_order')->insert([
'order_no' => $orderNo,
'pay_type' => $payType,
'type' => $type,
'amount' => $amount,
'subject' => $subject,
'status' => Status::CREATED->value,
'created_at' => time(),
'updated_at' => time()
]);
} catch (\Exception $e) {
WebmanLog::error('记录支付订单失败: ' . $e->getMessage());
}
}
/**
* 更新支付订单状态
* @param string $orderNo
* @param string $tradeNo
* @param string $status
*/
private function update_payment_order(string $orderNo, string $tradeNo, string $status): void
{
try {
// 映射支付状态到我们的状态枚举
$mappedStatus = $this->map_payment_status($status);
Db::name('payment_order')->where('order_no', $orderNo)->update([
'trade_no' => $tradeNo,
'status' => $mappedStatus,
'updated_at' => time()
]);
} catch (\Exception $e) {
WebmanLog::error('更新支付订单状态失败: ' . $e->getMessage());
}
}
/**
* 映射支付状态到枚举
* @param string $status
* @return string
*/
private function map_payment_status(string $status): string
{
// 支付宝状态映射
$alipayStatusMap = [
'TRADE_SUCCESS' => Status::SUCCESS->value,
'TRADE_FINISHED' => Status::COMPLETE->value,
'TRADE_CLOSED' => Status::FAIL->value,
'WAIT_BUYER_PAY' => Status::CREATED->value
];
// 微信支付状态映射
$wechatStatusMap = [
'SUCCESS' => Status::SUCCESS->value,
'FAIL' => Status::FAIL->value,
'REFUND' => Status::REFUNDED->value
];
// 先尝试支付宝映射
if (isset($alipayStatusMap[$status])) {
return $alipayStatusMap[$status];
}
// 再尝试微信映射
if (isset($wechatStatusMap[$status])) {
return $wechatStatusMap[$status];
}
// 默认返回成功
return Status::SUCCESS->value;
}
/**
* 查询支付订单状态
* @Apidoc\Title("查询支付订单状态")
* @Apidoc\Url("/api/payment/order/query")
* @Apidoc\Method("GET")
* @Apidoc\Param("order_no", "string", "订单号", true, "20260409001")
* @Apidoc\Param("pay_type", "string", "支付类型: alipay, wechat", true, "alipay")
* @Apidoc\Return("success", "object", "成功响应", "order|object|订单信息,pay_status|object|支付状态")
* @Apidoc\Return("error", "object", "失败响应", "code|int|错误码,msg|string|错误信息")
* @param Request $request
* @return Response
*/
public function query_order(Request $request): Response
{
try {
$orderNo = $request->get('order_no');
$payType = $request->get('pay_type');
if (!$orderNo || !$payType) {
return $this->error('缺少必要参数');
}
// 查询订单
$order = Db::name('payment_order')->where('order_no', $orderNo)->find();
if (!$order) {
return $this->error('订单不存在');
}
// 根据支付类型查询支付状态
if ($payType === \app\enum\Payment\Method::ALIPAY->value) {
$config = config('payment.alipay.default');
$result = Pay::alipay($config)->find($orderNo);
} elseif ($payType === \app\enum\Payment\Method::WECHAT->value) {
$config = config('payment.wechat.default');
$result = Pay::wechat($config)->find($orderNo);
} else {
return $this->error('不支持的支付类型');
}
return $this->success('ok',[
'order' => $order,
'pay_status' => $result->toArray()
]);
} catch (\Exception $e) {
WebmanLog::error('查询支付订单失败: ' . $e->getMessage());
return $this->error('查询失败: ' . $e->getMessage());
}
}
/**
* 关闭支付订单
* @Apidoc\Title("关闭支付订单")
* @Apidoc\Url("/api/payment/order/close")
* @Apidoc\Method("POST")
* @Apidoc\Param("order_no", "string", "订单号", true, "20260409001")
* @Apidoc\Param("pay_type", "string", "支付类型: alipay, wechat", true, "alipay")
* @Apidoc\Return("success", "object", "成功响应", "msg|string|成功信息")
* @Apidoc\Return("error", "object", "失败响应", "code|int|错误码,msg|string|错误信息")
* @param Request $request
* @return Response
*/
public function close_order(Request $request): Response
{
try {
$orderNo = $request->post('order_no');
$payType = $request->post('pay_type');
if (!$orderNo || !$payType) {
return $this->error('缺少必要参数');
}
// 关闭订单
if ($payType === 'alipay') {
$config = config('payment.alipay.default');
$result = Pay::alipay($config)->close($orderNo);
} elseif ($payType === 'wechat') {
$config = config('payment.wechat.default');
$result = Pay::wechat($config)->close($orderNo);
} else {
return $this->error('不支持的支付类型');
}
// 更新订单状态
Db::name('payment_order')->where('order_no', $orderNo)->update([
'status' => \app\enum\Payment\Status::COMPLETE->value,
'updated_at' => time()
]);
return $this->success('订单关闭成功');
} catch (\Exception $e) {
WebmanLog::error('关闭支付订单失败: ' . $e->getMessage());
return $this->error('关闭失败: ' . $e->getMessage());
}
}
/**
* 退款
* @Apidoc\Title("退款")
* @Apidoc\Url("/api/payment/refund")
* @Apidoc\Method("POST")
* @Apidoc\Param("order_no", "string", "订单号", true, "20260409001")
* @Apidoc\Param("pay_type", "string", "支付类型: alipay, wechat", true, "alipay")
* @Apidoc\Param("amount", "float", "退款金额", true, "0.01")
* @Apidoc\Param("refund_no", "string", "退款单号", false, "20260409001_refund")
* @Apidoc\Param("reason", "string", "退款原因", false, "退款")
* @Apidoc\Return("success", "object", "成功响应", "msg|string|成功信息")
* @Apidoc\Return("error", "object", "失败响应", "code|int|错误码,msg|string|错误信息")
* @param Request $request
* @return Response
*/
public function refund(Request $request): Response
{
try {
$params = $request->post();
if (!isset($params['order_no']) || !isset($params['pay_type']) || !isset($params['amount'])) {
return $this->error('缺少必要参数');
}
$orderNo = $params['order_no'];
$payType = $params['pay_type'];
$amount = $params['amount'];
$refundNo = $params['refund_no'] ?? $orderNo . '_' . time();
$reason = $params['reason'] ?? '退款';
// 发起退款
if ($payType === Method::ALIPAY->value) {
$config = config('payment.alipay.default');
$result = Pay::alipay($config)->refund([
'out_trade_no' => $orderNo,
'refund_amount' => $amount,
'out_request_no' => $refundNo,
'refund_reason' => $reason,
]);
} elseif ($payType === Method::WECHAT->value) {
$config = config('payment.wechat.default');
$result = Pay::wechat($config)->refund([
'out_trade_no' => $orderNo,
'out_refund_no' => $refundNo,
'total_fee' => $amount * 100,
'refund_fee' => $amount * 100,
'refund_desc' => $reason,
]);
} else {
return $this->error('不支持的支付类型');
}
// 记录退款信息
Db::name('payment_refund')->insert([
'order_no' => $orderNo,
'refund_no' => $refundNo,
'pay_type' => $payType,
'amount' => $amount,
'reason' => $reason,
'status' => 'SUCCESS',
'created_at' => time()
]);
// 更新订单状态
Db::name('payment_order')->where('order_no', $orderNo)->update([
'status' => Status::REFUNDED->value,
'updated_at' => time()
]);
return $this->success('退款成功');
} catch (\Exception $e) {
WebmanLog::error('退款失败: ' . $e->getMessage());
return $this->error('退款失败: ' . $e->getMessage());
}
}
/**
* 统一支付接口
* @Apidoc\Title("统一支付接口")
* @Apidoc\Url("/api/payment/unified/order")
* @Apidoc\Method("POST")
* @Apidoc\Param("payment", "string", "支付方式: alipay, wechat", true, "alipay")
* @Apidoc\Param("amount", "float", "支付金额", true, "0.01")
* @Apidoc\Param("order_no", "string", "订单号", true, "20260409001")
* @Apidoc\Param("subject", "string", "订单标题", true, "测试商品")
* @Apidoc\Param("body", "string", "订单描述", false, "测试商品描述")
* @Apidoc\Param("type", "string", "订单类型: recharge(充值), goods(商品), service(服务), other(其他)", false, "goods")
* @Apidoc\Param("trade_type", "string", "交易类型(微信支付需要): JSAPI, NATIVE, APP, MWEB", false, "JSAPI")
* @Apidoc\Param("openid", "string", "微信用户openid(JSAPI模式需要)", false, "o123456")
* @Apidoc\Return("success", "object", "成功响应", "pay_url|string|支付宝支付链接,pay_data|object|微信支付数据,order_no|string|订单号")
* @Apidoc\Return("error", "object", "失败响应", "code|int|错误码,msg|string|错误信息")
* @param Request $request
* @return Response
*/
public function unified_order(Request $request): Response
{
try {
$params = $request->post();
// 验证必要参数
if (!isset($params['payment']) || !isset($params['amount']) || !isset($params['order_no']) || !isset($params['subject'])) {
return $this->error('缺少必要参数');
}
$payment = strtolower($params['payment']);
$amount = $params['amount'];
$orderNo = $params['order_no'];
$subject = $params['subject'];
$body = $params['body'] ?? $subject;
$type = $params['type'] ?? 'goods';
// 根据payment参数选择支付方式
switch ($payment) {
case Method::ALIPAY->value:
// 调用支付宝支付
return $this->alipay_order($request);
case Method::WECHAT->value:
// 调用微信支付
return $this->wechat_order($request);
default:
return $this->error('不支持的支付方式');
}
} catch (\Exception $e) {
WebmanLog::error('统一支付接口失败: ' . $e->getMessage());
return $this->error('支付失败: ' . $e->getMessage());
}
}
/**
* 统一支付回调接口
* @Apidoc\Title("统一支付回调接口")
* @Apidoc\Url("/api/payment/unified/notify")
* @Apidoc\Method("ANY")
* @Apidoc\Param("payment", "string", "支付方式: alipay, wechat", false)
* @Apidoc\Param("out_trade_no", "string", "订单号", true)
* @Apidoc\Param("trade_no|transaction_id", "string", "支付交易号", true)
* @Apidoc\Param("trade_status|result_code", "string", "交易状态", true)
* @Apidoc\Return("string", "string", "返回success/fail或XML格式响应")
* @param Request $request
* @return Response
*/
public function unified_notify(Request $request): Response
{
try {
$payment = $request->get('payment') ?? $request->post('payment');
if (!$payment) {
// 尝试从请求参数中自动识别
$data = $request->all();
if (isset($data['app_id']) && strpos($data['app_id'], '20') === 0) {
$payment = 'alipay';
} elseif (isset($data['mch_id'])) {
$payment = 'wechat';
} else {
return $this->error('无法识别支付方式');
}
}
$payment = strtolower($payment);
// 根据payment参数选择回调处理
switch ($payment) {
case Method::ALIPAY->value:
// 调用支付宝回调
return $this->alipay_notify($request);
case Method::WECHAT->value:
// 调用微信回调
return $this->wechat_notify($request);
default:
return $this->error('不支持的支付方式');
}
} catch (\Exception $e) {
WebmanLog::error('统一支付回调接口失败: ' . $e->getMessage());
return $this->error('回调处理失败: ' . $e->getMessage());
}
}
/**
* 统一支付查询接口
* @Apidoc\Title("统一支付查询接口")
* @Apidoc\Url("/api/payment/unified/query")
* @Apidoc\Method("GET")
* @Apidoc\Param("order_no", "string", "订单号", true, "20260409001")
* @Apidoc\Param("payment", "string", "支付方式: alipay, wechat", true, "alipay")
* @Apidoc\Return("success", "object", "成功响应", "order|object|订单信息,pay_status|object|支付状态")
* @Apidoc\Return("error", "object", "失败响应", "code|int|错误码,msg|string|错误信息")
* @param Request $request
* @return Response
*/
public function unified_query(Request $request): Response
{
try {
$params = $request->all();
// 验证必要参数
if (!isset($params['order_no']) || !isset($params['payment'])) {
return $this->error('缺少必要参数');
}
// 设置pay_type参数
$request->withAddedHeader('pay_type', $params['payment']);
// 调用查询接口
return $this->query_order($request);
} catch (\Exception $e) {
WebmanLog::error('统一支付查询接口失败: ' . $e->getMessage());
return $this->error('查询失败: ' . $e->getMessage());
}
}
/**
* 统一支付关闭接口
* @Apidoc\Title("统一支付关闭接口")
* @Apidoc\Url("/api/payment/unified/close")
* @Apidoc\Method("POST")
* @Apidoc\Param("order_no", "string", "订单号", true, "20260409001")
* @Apidoc\Param("payment", "string", "支付方式: alipay, wechat", true, "alipay")
* @Apidoc\Return("success", "object", "成功响应", "msg|string|成功信息")
* @Apidoc\Return("error", "object", "失败响应", "code|int|错误码,msg|string|错误信息")
* @param Request $request
* @return Response
*/
public function unified_close(Request $request): Response
{
try {
$params = $request->post();
// 验证必要参数
if (!isset($params['order_no']) || !isset($params['payment'])) {
return $this->error('缺少必要参数');
}
// 设置pay_type参数
$request->withAddedHeader('pay_type', $params['payment']);
// 调用关闭接口
return $this->close_order($request);
} catch (\Exception $e) {
WebmanLog::error('统一支付关闭接口失败: ' . $e->getMessage());
return $this->error('关闭失败: ' . $e->getMessage());
}
}
/**
* 统一退款接口
* @Apidoc\Title("统一退款接口")
* @Apidoc\Url("/api/payment/unified/refund")
* @Apidoc\Method("POST")
* @Apidoc\Param("order_no", "string", "订单号", true, "20260409001")
* @Apidoc\Param("payment", "string", "支付方式: alipay, wechat", true, "alipay")
* @Apidoc\Param("amount", "float", "退款金额", true, "0.01")
* @Apidoc\Param("refund_no", "string", "退款单号", false, "20260409001_refund")
* @Apidoc\Param("reason", "string", "退款原因", false, "退款")
* @Apidoc\Return("success", "object", "成功响应", "msg|string|成功信息")
* @Apidoc\Return("error", "object", "失败响应", "code|int|错误码,msg|string|错误信息")
* @param Request $request
* @return Response
*/
public function unified_refund(Request $request): Response
{
try {
$params = $request->post();
// 验证必要参数
if (!isset($params['order_no']) || !isset($params['payment']) || !isset($params['amount'])) {
return $this->error('缺少必要参数');
}
// 设置pay_type参数
$params['pay_type'] = $params['payment'];
$request->withAddedHeader('pay_type', $params['payment']);
// 调用退款接口
return $this->refund($request);
} catch (\Exception $e) {
WebmanLog::error('统一退款接口失败: ' . $e->getMessage());
return $this->error('退款失败: ' . $e->getMessage());
}
}
}
+70
View File
@@ -0,0 +1,70 @@
<?php
namespace app\api\controller;
use app\model\Product as ProductModel;
use app\model\ProductOrder as ProductOrderModel;
use support\think\Db;
use hg\apidoc\annotation as Apidoc;
/**
* 产品模块
* @Apidoc\NotParse()
* @Apidoc\NotDebug()
*/
class ProductController extends BaseController{
/**
* 不需要鉴权的方法
* @var array
*/
public $noNeedAuth = ['*'];
public $noNeedLogin = ['detail','list'];
/**
* 列表
* @Apidoc\Query("kw", type="string", require=false, desc="搜索关键字")
* @Apidoc\Query("step", type="string", require=false, desc="类型,progress,done")
* @Apidoc\Query("billing_cycle", type="int", require=true, desc="周期",default=1)
* @Apidoc\Query("page", type="int", require=true, desc="页码",default=1)
* @Apidoc\Query("limit", type="int", require=true, desc="分页大小",default=10)
*/
public function list(){
$limit = (int)input('limit',10);
$model = ProductModel::where('status',1);
$list = $model->order('weight desc,id asc')->paginate($limit);
return $this->success(__('successful'),$list->toArray());
}
/**
* 详情
* @Apidoc\Query("id", type="int", require=true, desc="ID")
*/
public function detail(){
try{
$user = \support\Jwt::getUser();
}catch(\Exception $e){
$user = ['id'=>0,'role_id'=>0];
}
$appid = input('id');
if(!$appid){
return $this->error(__("Product does not exist"));
}
/** @var ProductModel $product */
$product = ProductModel::where('id',$appid)->find();
//->cache(true,86400,'product_detail')
if(!$product) {
return $this->error(__("Product does not exist"));
}
if($user['id']){
$total_quantity_user = ProductOrderModel::where('product_id',$product->id)->where('user_id',$user['id'])->sum('quantity');
$total_quantity_system = $product->user_quantity ?: 99999999;
$max_quantity = $total_quantity_system-$total_quantity_user;
$max_quantity= $max_quantity < 0 ? 0: $max_quantity;
$product->max_quantity = $max_quantity;
$product->total_quantity_user = $total_quantity_user;
}else{
$product->total_quantity_user = 0;
$product->max_quantity = 0;
}
return $this->success(__('successful'),$product->toArray());
}
}
+146
View File
@@ -0,0 +1,146 @@
<?php
namespace app\api\controller;
use app\model\ProductOrder as ProductOrderModel;
use app\model\Product as ProductModel;
use app\model\User as UserModel;
use support\think\Db;
use hg\apidoc\annotation as Apidoc;
/**
* 我的产品
* @Apidoc\NotParse()
* @Apidoc\NotDebug()
*/
class ProductOrderController extends BaseController{
/**
* 不需要鉴权的方法
* @var array
*/
public $noNeedAuth = ['*'];
public $noNeedLogin = [];
/**
* 列表
* @Apidoc\Query("step", type="int", require=false, desc="工作状态")
* @Apidoc\Query("page", type="int", require=true, desc="页码",default=1)
* @Apidoc\Query("limit", type="int", require=true, desc="分页大小",default=10)
*/
public function list(){
$limit = (int)input('limit',10);
$page = (int)input('page',1);
$step = (int)input('step',0);
$user_id = \support\Jwt\JwtToken::getCurrentId();
$model = ProductOrderModel::with(['product'])->where('user_id',$user_id);
if($step){
$ids = \app\model\WorkRecord::where('user_id',$user_id)
->distinct(true)
->where('status',$step)
->column('order_id');
$model = $model->whereIn('id',$ids);
}
$list = $model->where('status',1)
->order('created_at desc')
->paginate($limit);
$list->each(function($item)use($step){
//$_step=1;
if($step){
$_step = $step;
}else{
$_step = \app\model\WorkRecord::where('order_id',$item->id)
->order('status','asc')
->limit(0,1)
->value('status');
}
$item->step = $_step;
return $item;
});
return $this->success(__('successful'),$list->toArray());
}
/**
* 购买产品
* @Apidoc\Method("POST")
* @Apidoc\Param("product_id", type="string", require=true, desc="产品ID")
* @Apidoc\Param("quantity", type="int", require=true, desc="数量")
* @Apidoc\Param("trade_password", type="string", require=true, desc="交易密码")
*/
public function create()
{
$user = \support\Jwt::getUser();
//验证交易密码
$trade_password = input('trade_password');
\support\Jwt::verify_trade_password($trade_password);
$data = [
'user_id' => $user->id,
'product_id'=> input('product_id'),
'quantity' => (int)input('quantity'),
'accelerate' => input('accelerate'),
'status' => 1
];
if(!$data['product_id']){
return $this->error(__('Product is incorrect'));
}
if($data['quantity'] < 1){
return $this->error(__('Quantity is incorrect'));
}
if($data['product_id'] == 12){
$exsit = ProductOrderModel::where('product_id',$data['product_id'])->where('user_id',$data['user_id'])->count('id');
if($exsit != 0){
return $this->error(__('微量包每个用户只能购买一个'));
}
if($data['quantity'] != 1){
return $this->error(__('微量包每个用户只能购买一个'));
}
}
/** @var ProductModel $product */
$product = ProductModel::where('id',$data['product_id'])->find();
if(!$product){
return $this->error(__('Product is incorrect'));
}
/*
if ($product->start_time > time() || $product->end_time < time()) {
return $this->error(__('Can\'t buy now'));
}
*/
//return $this->success(__('successful'),[$product->start_time]);
$data['price'] = $product['price'];
if($data['accelerate']){
$data['price'] += $product['accelerate_price'];
$data['accelerate_times'] = $product['accelerate_assign_times'];
$data['accelerate_used'] = 0;
}
$data['amount'] = $data['price'] * $data['quantity'];
$data['total'] = $product['total'] * $data['quantity'];
$data['assigned'] = 0;
/*
$total_quantity_user = ProductOrderModel::where('product_id',$product->id)->where('user_id',$user['id'])->sum('quantity');
$total_quantity_system = $product->user_quantity ?: 99999999;
$can_purchase = $total_quantity_system-$total_quantity_user;
$can_purchase= $can_purchase < 0 ? 0: $can_purchase;
if($can_purchase < $data['quantity']){
return $this->error(__('You can only purchase %max_quantity% copies',[
'%max_quantity%' => $can_purchase
]));
}
*/
//var_dump($user);
//验证余额
if($data['amount'] > $user->money){
return $this->error(__('Insufficient balance'));
}
Db::startTrans();
try{
$data = ProductOrderModel::create($data);
$product->sales = $product->sales + $data['quantity'];
$product->save();
UserModel::money($data['user_id'],-$data['amount'],\app\enum\BalanceType::PRODUCT_BUY,$data['id']);
Hook('product.buy',$data);
Db::commit();
}catch(\Exception $e){
Db::rollback();
return $this->error($e->getMessage());
}
return $this->success(__('successful'));
}
}
+482
View File
@@ -0,0 +1,482 @@
<?php
namespace app\api\controller;
use support\Request;
use support\think\Db;
use app\model\Recharge as RechargeModel;
use app\model\User as UserModel;
use hg\apidoc\annotation as Apidoc;
/**
* 充值模块
* @Apidoc\NotParse()
* @Apidoc\NotDebug()
*/
class RechargeController extends BaseController{
/**
* 不需要鉴权的方法
* @var array
*/
public $noNeedAuth = ['*'];
/**
* 无需登录及鉴权的方法
* @var array
*/
public $noNeedLogin = ['notify','watch_list','updateUserBalance'];
/**
* 列表
* @Apidoc\Method("POST")
* @Apidoc\Query("page", type="int", require=true, desc="页码",default=1)
* @Apidoc\Query("limit", type="int", require=true, desc="分页大小",default=10)
*/
public function list()
{
$limit = (int)input('limit',10);
$status = Input('status',null);
if(!is_null($status)){
$list = RechargeModel::where('user_id',\support\Jwt\JwtToken::getCurrentId())->where('status',$status)->order('id desc')->paginate($limit);
}else{
$list = RechargeModel::where('user_id',\support\Jwt\JwtToken::getCurrentId())->order('id desc')->paginate($limit);
}
return $this->success(__('successful'),$list->toArray());
}
/**
* 创建
* @Apidoc\Method("POST")
* @Apidoc\Param("amount", type="string", require=true, desc="金额")
* @Apidoc\Param("network", type="string", require=true, desc="网络")
*/
function create(){
return $this->create1();
}
public function create2()
{
$user = \support\Jwt::getUser();
$data = [
'user_id' => $user->id,
'amount' => input('amount',0),
'network' => input('network',''),
'status' => \app\enum\WithdrawlStatus::CREATED->value
];
//验证最小金额
if($data['amount'] < Config('site.recharge_minimum')){
return $this->error(__('Minimum recharge of %num%',[
'%num%' => Config('site.recharge_minimum')
]));
}
if (!$data['network'] || !in_array($data['network'],['BEP-20','TRC-20','wxpay','alipay','qqpay'])) {
return $this->error(__('Network is incorrect'));
}
if(in_array($data['network'],['BEP-20','TRC-20'])){
$ing_count = RechargeModel::where('user_id',$data['user_id'])->where('status',\app\enum\RechargeStatus::CREATED->value)->count('id');
if($ing_count >= 2){
return $this->error(__('You have reached the limit of uncompleted orders ( %max% ). Please complete the existing orders before trying to create another one.',[
'%max%' => 2
]));
}
Db::startTrans();
try {
/** @var RechargeModel $order */
$order = RechargeModel::create($data);
//$order->notify_url = Request()->domain().'/api/recharge/notify';
$order->notify_url = config('pay.notify_server').'/api/recharge/notify';
$order->out_trade_no = $order->id;
$order->env = 'product';
$order->appid = Config('pay.appid');
$order->created_at = time();
$postdata = $order->toArray();
unset($postdata['id']);
//转换成USDT
$postdata['amount'] = bcdiv($order->amount,Config('site.money_to_usdt_rate'),4);
//折扣
$postdata['amount'] = bcdiv($postdata['amount'],Config('site.usdt_recharge_discount'),4);
$res = post(Config('pay.server').'/recharge/create',$postdata);
\support\Log::alert("create res:".$res);
//cp($res);
$res = json_decode($res,true);
if($res['code'] === 0){
$order->address = $res['data']['address'];
}else{
Db::rollback();
return $this->error(__('Failed to create recharge order, please try again later'));
}
$order->allowField(['address'])->save();
Db::commit();
return $this->success(__('successful'),[
'order' => $order
]);
} catch (\Exception $e) {
Db::rollback();
\support\Log::alert("create error".$e->getMessage());
return $this->error(__('Failed to create recharge order, please try again later'));
}
}else{
$sign = \support\Encrypt::aesencode(json_encode($data));
return $this->success(__('successful'),[
'order' => [
'id' => 0,
'network' => $data['network'],
'amount' => $data['amount'],
"url" => 'http://43.240.74.89:9595/pay/index?sign='.$sign
]
]);
}
}
public function create1()
{
$user = \support\Jwt::getUser();
$data = [
'user_id' => $user->id,
'amount' => input('amount',0),
'network' => input('network',''),
'status' => \app\enum\WithdrawlStatus::CREATED->value
];
//验证最小金额
if($data['amount'] < Config('site.recharge_minimum')){
return $this->error(__('Minimum recharge of %num%',[
'%num%' => Config('site.recharge_minimum')
]));
}
if (!$data['network'] || !in_array($data['network'],['BEP-20','TRC-20','wxpay','alipay','qqpay'])) {
return $this->error(__('Network is incorrect'));
}
if(in_array($data['network'],['BEP-20','TRC-20'])){
$ing_count = RechargeModel::where('user_id',$data['user_id'])->where('status',\app\enum\RechargeStatus::CREATED->value)->count('id');
if($ing_count >= 2){
return $this->error(__('You have reached the limit of uncompleted orders ( %max% ). Please complete the existing orders before trying to create another one.',[
'%max%' => 2
]));
}
}
Db::startTrans();
try {
/** @var RechargeModel $order */
$order = RechargeModel::create($data);
if(in_array($data['network'],['BEP-20','TRC-20'])){
//$order->notify_url = Request()->domain().'/api/recharge/notify';
$order->notify_url = config('pay.notify_server').'/api/recharge/notify';
$order->out_trade_no = $order->id;
$order->env = 'product';
$order->appid = Config('pay.appid');
$order->created_at = time();
$postdata = $order->toArray();
unset($postdata['id']);
//转换成USDT
$postdata['amount'] = bcdiv($order->amount,Config('site.money_to_usdt_rate'),4);
//折扣
$postdata['amount'] = bcmul($postdata['amount'],Config('site.usdt_recharge_discount'),4);
$res = post(Config('pay.server').'/recharge/create',$postdata);
\support\Log::alert("create res:".$res);
//cp($res);
$res = json_decode($res,true);
if($res['code'] === 0){
$order->address = $res['data']['address'];
}else{
Db::rollback();
return $this->error(__('Failed to create recharge order, please try again later'));
}
$order->allowField(['address'])->save();
}else{
$postdata = [
"pid" => '144604',
"type" => $order->network,
"out_trade_no" => $order->id,
"notify_url" => Config('pay.notify_server').'/api/recharge/notify_ok',
"return_url" => Config('pay.notify_server').'/api/recharge/notify_ok',
"name" => 'VIP会员',
"money" => $order->amount,
"device" => "mobile",
"clientip" => request()->getRealIp()
];
$postdata['money'] = bcdiv($postdata['money'] ,Config('site.rmb_recharge_discount'),2);
//\support\Log::alert("create postdata:".json_encode($postdata));
$this->getsign($postdata);
$res = post('https://pay.yf2.cn/mapi.php',$postdata);
\support\Log::alert("create res:".$res);
$res = json_decode($res,true);
if($res['code'] === 1){
if(isset($res['payurl'])){
$order->address = $res['qrcode'];
}elseif(isset($res['qrcode'])){
$order->address = $res['qrcode'];
}elseif(isset($res['urlscheme'])){
$order->address = $res['urlscheme'];
}
}else{
Db::rollback();
\support\Log::error(json_encode($res));
throw new \Exception($res['msg']);
}
$order->allowField(['address'])->save();
}
Db::commit();
return $this->success(__('successful'),[
'order' => $order
]);
} catch (\Exception $e) {
Db::rollback();
\support\Log::alert("create error".$e->getMessage());
return $this->error(__('Failed to create recharge order, please try again later'));
}
}
/**
* 更新
* @Apidoc\Method("POST")
* @Apidoc\Param("id", type="int", require=true, desc="ID")
* @Apidoc\Param("amount", type="string", require=true, desc="金额")
* @Apidoc\Param("network", type="string", require=true, desc="网络")
*/
function update(){
$id = input('id');
$amount = input('amount');
$network = input('network');
$data = [];
if(!$id){
return $this->error(__('Invalid parameters'));
}
/** @var RechargeModel $order */
$order = RechargeModel::where('id',$id)->find();
if(!$order){
return $this->error(__('Order not exsit'));
}
if($amount && $amount != $order->amount){
$data['amount'] = $amount;
//验证最小金额
if($data['amount'] < Config('site.recharge_minimum')){
return $this->error(__('Minimum recharge of %num%',[
'%num%' => Config('site.recharge_minimum')
]));
}
$data['amount'] = bcdiv($data['amount'],Config('site.money_to_usdt_rate'),4);
}
if($network && $network != $order->network){
$data['network'] = $network;
if (!$data['network'] || !in_array($data['network'],['BEP-20','TRC-20'])) {
return $this->error(__('Network is incorrect'));
}
}
if(empty($data)){
return $this->error(__('Invalid parameters'));
}
foreach($data as $field=>$value){
$order->$field = $value;
}
$data['out_trade_no'] = $order->id;
$data['action'] = 'update';
$data['appid'] = Config('pay.appid');
$res = post(Config('pay.server').'/recharge/create',$data);
//\support\Log::alert("update res:".$res);
$res = json_decode($res,true);
//cp($res);
if($res['code'] === 0){
if($order->address != $res['data']['address']){
$order->address = $res['data']['address'];
}
}else{
return $this->error(__('Failed to create recharge order, please try again later'));
}
$order->save();
return $this->success(__('successful'),[
'order' => $order
]);
}
/**
* 详情
* @Apidoc\Query("id", type="string", require=true, desc="ID")
*/
public function detail(){
$appid = input('id');
$vo = RechargeModel::where('id',$appid)->find();
if($vo) {
return $this->success(__('successful'),[
'order' => $vo
]);
}
return $this->error(__("Record does not exist"));
}
/**
* 转账成功异步通知
* @Apidoc\NotParse()
* @Apidoc\NotDebug()
*/
public function notify(){
$data = \support\Encrypt::aesdecode(input('data'));
$data = json_decode($data,true);
/** @var RechargeModel $vo */
$vo = RechargeModel::where('id',$data['out_trade_no'])->find();
if($vo){
if($data['result'] == 'SUCCESS'){
if($vo->status != \app\enum\RechargeStatus::COMPLETE->value){
try{
$vo->status = \app\enum\RechargeStatus::COMPLETE->value;
$vo->txid = $data['txid'];
$vo->real_amount = isset($data['real_amount']) ? $data['real_amount'] : 0;
$vo->transfer_at = $data['transfer_at'] ?: time();
$vo->save();
//消除USDT和人民币的误差,因为USDT支付可能不是一模一样的金额
$money = bcmul($vo->amount ,Config('site.rmb_to_money_rate'));
UserModel::money($vo->user_id,$money,\app\enum\BalanceType::RECHARGE,$vo->id);
Hook('recharge.success',$vo);
}catch(\Exception $e){
log_alert('充值回调失败:'.$e->getMessage());
log_alert($data);
}
}
}else{
$vo->status = \app\enum\RechargeStatus::FAIL->value;
$vo->txid = $data['txid'];
$vo->reason = $data['reason'];
$vo->save();
}
}
return response("SUCCESS");
}
/**
* 人民币转账成功异步通知
* @Apidoc\NotParse()
* @Apidoc\NotDebug()
*/
public function notify_ok(){
$data = Input('post.');
$sign = get_pay_sign($data);
if($sign != $data['sign']){
return response("FAIL");
}
/** @var RechargeModel $vo */
$vo = RechargeModel::where('id',$data['out_trade_no'])->find();
if($vo && isset($data['trade_status'])){
if($data['trade_status'] === 'TRADE_SUCCESS'){
if($vo->status != \app\enum\RechargeStatus::COMPLETE->value){
try{
$vo->status = \app\enum\RechargeStatus::COMPLETE->value;
$vo->txid = isset($data['trade_no']) ? $data['trade_no'] : '';
$vo->from='pay.yf2.cn';
$vo->real_amount = isset($data['money']) ? $data['money'] : 0;
$vo->save();
$money = bcmul($vo->real_amount ,Config('site.rmb_to_money_rate'));
UserModel::money($vo->user_id,$money,\app\enum\BalanceType::RECHARGE,$vo->id);
Hook('recharge.success',$vo);
}catch(\Exception $e){
log_alert('充值回调失败:'.$e->getMessage());
log_alert($data);
return response("FAIL");
}
}
}
}
return response("SUCCESS");
}
/**
* 根据TXID更新用户余额,补单的功能
* @Apidoc\NotParse()
* @Apidoc\NotDebug()
* @return \support\Response
*/
function update_balance_by_txid(){
$user_id = \support\Jwt\JwtToken::getCurrentId();
$data = [
'txid' => input('txid'),
'id' => input('id'),
'status' => \app\enum\WithdrawlStatus::CREATED->value
];
/** @var RechargeModel $vo */
$vo = RechargeModel::where('id',$data['id'])->find();
if($vo->user_id !=$user_id){
return $this->error(__('permission denied'));
}
//验证最小金额
if(!$data['txid']){
return $this->error(__('Invalid parameters'));
}
$res = get(Config('pay.server').'/Util/txid?txid='.$data['txid']);
$res = json_decode($res,true);
if($res['code'] !== 0){
return $this->error($res['msg']);
}
$vo->txid = $data['txid'];
$vo->result = $res['result'];
if($res['result'] == 'SUCCESS'){
$vo->from = $res['from'];
$vo->real_amount = $res['real_amount'];
$vo->pay_time = $res['timestamp'];
$vo->status = \app\enum\RechargeStatus::COMPLETE->value;
}else{
$vo->result = $res['from'];
$vo->reason = $res['real_amount'];
$vo->status = \app\enum\RechargeStatus::FAIL->value;
}
$vo->save();
return $this->success(__('successful'));
}
/**
* 根据监视上报数据更新用户余额
* @Apidoc\NotParse()
* @Apidoc\NotDebug()
*/
function updateUserBalance(Request $request){
$decimal_part = $request->post('decimal_part');
$data = [
'user_id' => 0 ,
'amount' => $request->post('amount'),
'network' => $request->post('chain'),
'address' => $request->post('to'),
'from' => $request->post('from'),
'txid' => $request->post('txid'),
'pay_time' => $request->post('pay_time'),
'created_at' => $request->post('pay_time'),
];
if($data['network'] == 'TRC-20'){
$data['user_id'] = UserModel::where('trc_recharge_address',$data['address'])->where('decimal_part',$decimal_part)->value('id');
}else{
$data['user_id'] = UserModel::where('bep_recharge_address',$data['address'])->where('decimal_part',$decimal_part)->value('id');
}
if(!$data['user_id']){
return $this->success(__('user not exist'));
}
$data['real_amount'] = $data['amount'];
//$data['pay_time'] = time();
$data['status'] = 2;
$vo = RechargeModel::where('txid',$data['txid'])->find();
if($vo){
return $this->success(__('exist'));
}
Db::startTrans();
try{
$idata = RechargeModel::create($data);
UserModel::score($data['user_id'],$data['amount'],\app\enum\BalanceType::RECHARGE,$idata['id'].'');
Db::commit();
Hook('recharge.success',$idata);
return $this->success(__('successful'));
}catch(\Exception $e){
Db::rollback();
return $this->error($e->getMessage());
}
}
/**
* 监视列表
* @Apidoc\NotParse()
* @Apidoc\NotDebug()
*/
function watch_list(){
//$bep = UserModel::distinct(true)->column('LOWER(bep_recharge_address)');
//$trc = UserModel::distinct(true)->column('LOWER(trc_recharge_address)');
$bep = UserModel::distinct(true)->column('bep_recharge_address');
$trc = UserModel::distinct(true)->column('trc_recharge_address');
return $this->success(__('successful'),[
'BEP-20'=>$bep,
'TRC-20'=>$trc,
]);
}
protected function getsign(&$data, $key = "3E7551E3707DFB6B6E8A6E83B01FF437") {
return get_pay_sign($data,$key);
}
}
+295
View File
@@ -0,0 +1,295 @@
<?php
namespace app\api\controller;
use support\Request;
use support\Response;
use app\model\UserSignin as UserSigninModel;
use app\model\UserXuanchuan as UserXuanchuanModel;
use support\Jwt\JwtToken;
use Shopwwi\WebmanFilesystem\FilesystemFactory;
use Shopwwi\WebmanFilesystem\Facade\Storage;
use hg\apidoc\annotation as Apidoc;
/**
* 签到模块
* @Apidoc\NotParse()
* @Apidoc\NotDebug()
*/
class SigninController extends BaseController
{
/**
* 不需要鉴权的方法
* @var array
*/
public $noNeedAuth = ['*'];
/**
* 无需登录及鉴权的方法
* @var array
*/
public $noNeedLogin = [];
/**
* 列表
* @Apidoc\Query("status", type="int", require=true, desc="状态")
* @Apidoc\Query("page", type="int", require=true, desc="页码",default=1)
* @Apidoc\Query("limit", type="int", require=true, desc="分页大小",default=10)
*/
public function list(){
$limit = (int)input('limit',10);
$status = (int)input('status');
$model = UserXuanchuanModel::where('id','>',0);
if($status && $status!='all'){
$model = $model->where('status',$status);
}
$list = $model->order('id','desc')->paginate($limit);
return $this->success(__('successful'),$list);
}
/**
* @Apidoc\Title("用户签到")
* @Apidoc\Method("GET")
*/
public function info(Request $request)
{
$user_id = JwtToken::getCurrentId();
if (!$user_id) {
return $this->error(__('Please login first'));
}
/** @var UserSigninModel $last */
$last = UserSigninModel::where('user_id', $user_id)->order('id','desc')->find();
return $this->success(__('successful'),[
'continuous_days' => $last->continuous_days,
'last_day' => $last->sign_date,
'signed' => $last->sign_date == date('Y-m-d'),
'invite_complete' => cache('invite_'.$user_id.'_'.date('Ymd')),
'pyq_complete' => UserXuanchuanModel::where('user_id',$user_id)->where('type','pyq')->whereTime('created_at','today')->count() > 0,
'group_complete' => UserXuanchuanModel::where('user_id',$user_id)->where('type','group_complete')->whereTime('created_at','today')->count() > 0,
'signinList' => [20,20,20,20,20,20,100]
]);
}
/**
* @Apidoc\Title("用户签到")
* @Apidoc\Method("GET")
*/
public function sign(Request $request)
{
$user = JwtToken::getUser();
if (!$user->realname_verify) {
return $this->error(__('Please complete real-name verification first'));
}
$user_id = $user->id;
$today = date('Y-m-d');
// 检查今天是否已签到
if (UserSigninModel::where('user_id', $user_id)->where('sign_date', $today)->find()) {
return $this->error(__('今日已签到'));
}
// 检查昨天是否签到
$continuous_days = UserSigninModel::where('user_id', $user_id)->count('id');
$continuous_days = $continuous_days ? ($continuous_days + 1) : 1;
// 奖励规则(可自定义/读取配置/数据库)
$reward = $this->getReward($continuous_days);
// 写入签到记录
UserSigninModel::create([
'user_id' => $user_id,
'sign_date' => $today,
'reward' => $reward,
'continuous_days' => $continuous_days,
]);
// 发放奖励(如积分、余额等)
\app\model\User::currency1($user_id, $reward, \app\enum\BalanceType::SIGNIN);
return $this->success(__('successful'),[
'reward' => $reward,
'continuous_days' => $continuous_days
]);
}
/**
* @Apidoc\Title("查询签到状态")
* @Apidoc\Method("GET")
*/
public function status(Request $request)
{
$user_id = JwtToken::getCurrentId();
if (!$user_id) {
return $this->error(__('Please login first'));
}
$today = date('Y-m-d');
$record = UserSigninModel::where('user_id', $user_id)->where('sign_date', $today)->find();
$continuous_days = UserSigninModel::where('user_id', $user_id)->order('id','desc')->value('continuous_days');
return $this->success(__('successful'),[
'signed' => !!$record,
'continuous_days' => $continuous_days ?: 0,
]);
}
/**
* @Apidoc\Title("查询签到记录")
* @Apidoc\Method("GET")
* @Apidoc\Query("page", type="int", require=true, desc="页码",default=1)
* @Apidoc\Query("limit", type="int", require=true, desc="分页大小",default=10)
*/
public function records(Request $request)
{
$user_id = JwtToken::getCurrentId();
if (!$user_id) {
return $this->error(__('Please login first'));
}
$limit = (int)$request->get('limit', 30);
$list = UserSigninModel::where('user_id', $user_id)
->order('id','desc')
->limit($limit)
->select();
return $this->success(__('successful'),$list);
}
/**
* @Apidoc\Title("发布朋友圈")
* @Apidoc\Method("POST")
* @Apidoc\Param("files", type="string",require=true, desc="文件列表")
*/
public function pyq(Request $request)
{
$user = JwtToken::getUser();
if (!$user->realname_verify) {
return $this->error(__('Please complete real-name verification first'));
}
$user_id = $user->id;
$files = Input('files');
if(count($files) != 2) {
return $this->error(__('请上传2张图片'));
}
if(UserXuanchuanModel::where('user_id',$user_id)->where('type','pyq')->whereTime('created_at','today')->count() > 0) {
return $this->error(__('请明日再来'));
}
UserXuanchuanModel::create([
'user_id' => $user_id,
'files' => implode(',',$files),
'type' => 'pyq'
]);
return $this->success(__('successful'));
}
/**
* @Apidoc\Title("发布微信群")
* @Apidoc\Method("POST")
* @Apidoc\Param("files", type="string",require=true, desc="文件")
*/
public function wx(Request $request)
{
$user = JwtToken::getUser();
if (!$user->realname_verify) {
return $this->error(__('Please complete real-name verification first'));
}
$user_id = $user->id;
$files = Input('files');
if(count($files) != 1) {
return $this->error(__('请上传1张图片'));
}
if(UserXuanchuanModel::where('user_id',$user_id)->where('type','group')->whereTime('created_at','today')->count() > 0) {
return $this->error(__('请每日再来'));
}
UserXuanchuanModel::create([
'user_id' => $user_id,
'files' => implode(',',$files),
'type' => 'group'
]);
return $this->success(__('successful'));
}
/**
* @Apidoc\Title("补签")
* @Apidoc\Method("POST")
* @Apidoc\Param("date", type="string",require=true, desc="补签日期")
*/
public function makeUp(Request $request)
{
$user_id = JwtToken::getCurrentId();
if (!$user_id) {
return $this->error(__('Please login first'));
}
$date = $request->post('date');
$today = date('Y-m-d');
if (!$date || !preg_match('/^\d{4}-\d{2}-\d{2}$/', $date)) {
return $this->error(__('日期格式错误'));
}
// 补签日期不能大于今天
if ($date > $today) {
return $this->error(__('补签日期不能大于今天'));
}
// 检查是否已签到
if (UserSigninModel::where('user_id', $user_id)->where('sign_date', $date)->find()) {
return $this->error(__('该日已签到'));
}
// 补签消耗积分或奖励减半(此处以奖励减半为例)
$yesterday = date('Y-m-d', strtotime($date . ' -1 day'));
/** @var UserSigninModel $last */
$last = UserSigninModel::where('user_id', $user_id)->where('sign_date', $yesterday)->find();
$continuous_days = $last ? ($last->continuous_days + 1) : 1;
$reward = floor($this->getReward($continuous_days) / 2); // 奖励减半
UserSigninModel::create([
'user_id' => $user_id,
'sign_date' => $date,
'reward' => $reward,
'continuous_days' => $continuous_days,
]);
\app\model\User::currency1($user_id, $reward, \app\enum\BalanceType::SIGNIN);
return $this->success(__('successful'),['reward' => $reward, 'continuous_days' => $continuous_days]);
}
/**
* @Apidoc\Title("签到统计报表")
* @Apidoc\Method("GET")
*/
public function report(Request $request)
{
$user_id = JwtToken::getCurrentId();
if (!$user_id) {
return $this->error(__('Please login first'));
}
$total = UserSigninModel::where('user_id', $user_id)->count();
$max_continuous = UserSigninModel::where('user_id', $user_id)->max('continuous_days');
$month = date('Y-m');
$month_count = UserSigninModel::where('user_id', $user_id)
->whereLike('sign_date', "$month%")
->count();
return $this->success(__('successful'),[
'total' => $total,
'max_continuous' => $max_continuous,
'month_count' => $month_count,
]);
}
// 奖励规则,可自定义
protected function getReward($continuous_days)
{
$rewards = [20,20,20,20,20,20,100];
$continuous_days = $continuous_days - 1;
$continuous_days = $continuous_days % 7;
return $rewards[$continuous_days];
}
/**
* @Apidoc\Title("上传")
* @Apidoc\Method("POST")
* @Apidoc\Param("file", type="string",require=true, desc="文件")
*/
function upload(Request $request,$return = false)
{
//多文件上传
$files = $request->file();
try {
$result = Storage::adapter('public')
->path('upload/files')
->size(1024*1024*50)
->extYes(['image/jpeg','image/png'])
->uploads($files,0,1024*1024*20,false);
return $this->success(__('successful'),$result);
}catch (\Exception $e){
return $this->error($e->getMessage());
}
}
}
+192
View File
@@ -0,0 +1,192 @@
<?php
namespace app\api\controller;
use app\model\User as UserModel;
use app\model\UserTeam as UserTeamModel;
use app\model\WorkRecord as WorkRecordModel;
use support\Request;
use support\think\Db;
use hg\apidoc\annotation as Apidoc;
/**
* 用户团队
*/
class TeamController extends BaseController{
/**
* 不需要鉴权的方法
* @var array
*/
public $noNeedAuth = ['*'];
/**
* 无需登录及鉴权的方法
* @var array
*/
public $noNeedLogin = [];
/**
* @Apidoc\Title("团队概览")
* @Apidoc\Method("GET")
* @Apidoc\Param("username", type="string",require=true, desc="用户名")
* @Apidoc\Param("nickname", type="string",require=true, desc="密码")
*/
public function index(Request $request){
$user = \support\Jwt::getUserinfo();
$user_id = $user['id'];
$user= Hook('user.profile',$user);
//$team_ids = UserTeamModel::where('ancestor_id',$user_id)->where('depth','>',0)->column('descendant_id');
$result=[
'level' => $user['level'],
'total_count' => cache_get('team_user_count_'.$user_id),//团队总人数
'direct_total' => cache_get('team_direct_total_'.$user_id),//直属团队人数
'vip_total' => cache_get('team_vip_total_'.$user_id),//旗下会员总数
// 'recharge_total' => cache('team_recharge_total_'.$user_id)??0,
// 'withdrawl_total' => cache('team_withdrawl_total_'.$user_id)??0,
// 'income_total' => cache('team_income_total_'.$user_id)??0,
// 'today_income_total' => cache('user_today_income_total_'.$user_id)??0,
// 'promotion_income_total' => cache('user_promotion_income_total_'.$user_id)??0,
// 'consume_total' => cache('team_consume_total_'.$user_id)??0,//团队总业绩
// 'user_sales_reward' => cache('user_sales_reward_'.$user_id)??0,//销售奖
// 'user_output_reward' => cache('user_output_reward_'.$user_id)??0,//产值奖
// 'user_withdrawl_reward' => cache('user_withdrawl_reward'.$user_id)??0,//提现奖
'user' => $user[0]
];
return $this->success(__('successful'),$result);
}
/**
* @Apidoc\Title("团队列表")
* @Apidoc\Method("GET")
* @Apidoc\Param("page", type="int",require=false, desc="页码")
* @Apidoc\Param("limit", type="int",require=false, desc="分页大小")
*/
public function list(Request $request){
$user = \support\Jwt::getUser();
$limit = $request->get('limit',10);
$page = $request->get('page',1);
$kw = $request->get('kw');
// 假设 $user_id 是要查询的用户 ID
// $user = User::find($user['id']); // 查询用户对象
// // 获取该用户的下属团队
// $teamMembers = $user->team()
// ->where('status', 1) // 只查询有效的下属
// ->with('user') // 联合查询 User 模型,获取下属的详细信息
// ->order('depth') // 按照层级深度排序
// ->select();
// foreach ($teamMembers as $member) {
// echo '下属用户ID: ' . $member->descendant_id . ',层级深度: ' . $member->depth . ',状态: ' . $member->status . ',用户名: ' . $member->user->name . ',邮箱: ' . $member->user->email . "\n";
// }
// return $this->success(__('successful'),$result);
// $model = Db::name('user_team')
// ->alias('ut')
// ->join('user wu', 'ut.descendant_id = wu.id')
// ->where('ut.ancestor_id', $user['id'])
// ->where('ut.depth', '<=', 3) // 限制三级内
// ->field('wu.id, wu.username, wu.group, ut.depth')
// ->order('ut.depth ASC, wu.username ASC');
// if($limit == 'all' || $limit >= 999999){
// $result = $model->select();
// }else{
// // 分页处理
// $result = $model->page($page, $limit)->select();
// $total = $model->count(); // 获取总记录数
// $result->each(function ($item) {
// //cache_add('user_recharge_total_'.$item['id'],1);
// //cache_add('user_withdrawl_total_'.$item['id'],1);
// //cache_add('user_income_total_'.$item['id'],1);
// $item['avatar'] = cdnurl($item['avatar'] ?: '/storage/avatar/default.png');
// $item['recharge_total'] = cache('user_recharge_total_'.$item['id']);
// $item['withdrawl_total'] = cache('user_withdrawl_total_'.$item['id']);
// $item['income_total'] = cache('user_income_total_'.$item['id']);
// $item['created_at'] = date('Y-m-d H:i:s', $item['created_at']);
// return $item;
// });
// $result = [
// 'data' => $result,
// 'total' => $total,
// 'current_page' => $page,
// 'last_page' => ceil($total / $limit),
// 'per_page' => $limit,
// ];
// }
$user_id = \support\Jwt\JwtToken::getCurrentId();
$model = UserModel::alias('u')
->where('parent_id',$user_id)
->join('user_extend ue', 'u.id = ue.user_id')
->where('u.parent_id', $user['id'])
//->where('ue.active', 1)
->field('u.id,u.userID, u.username,u.nickname,u.money,u.score,u.role_id,u.avatar, u.created_at')
->order('u.created_at desc');
if($kw){
$model = $model->whereLike("u.username",'%'.$kw.'%');
}
if($limit == 'all' || $limit >= 999999){
$result = $model->select();
// $result = [
// 'data' => $result,
// 'total' => count($result),
// 'current_page' => 1,
// 'last_page' => 1,
// 'per_page' => count($result),
// ];
$result= \think\Paginator::make($result, 99999999999, 1, count($result));
}else{
$result = $model->paginate($limit);
}
$result = $result->toArray();
foreach($result['data'] as $k=>$item){
$result['data'][$k]['avatar'] = cdnurl($item['avatar'] ?: '/storage/avatar/default.png');
//$result['data'][$k]['recharge_total'] = cache('user_recharge_total_'.$item['id'])??0;
//$result['data'][$k]['withdrawl_total'] = cache('user_withdrawl_total_'.$item['id'])??0;
//$result['data'][$k]['withdrawl_reward'] = cache('user_withdrawl_reward_'.$item['id'])??0;
//$result['data'][$k]['income_total'] = cache('user_income_total_'.$item['id'])??0;
//$result['data'][$k]['consume_total'] = cache('user_consume_total_'.$item['id'])??0;
//$result['data'][$k]['created_at'] = date('Y-m-d H:i:s', $item['created_at']);
//return $item;
}
return $this->success(__('successful'),$result);
}
/**
* @Apidoc\NotParse()
* @Apidoc\NotDebug()
* @Apidoc\Title("改变用户等级")
* @Apidoc\Method("POST")
* @Apidoc\Param("id", type="string",require=false, desc="ID")
* @Apidoc\Param("level", type="int",require=false, desc="等级")
*/
function changelevel(Request $request){
$user = \support\Jwt::getUser();
$id = $request->post('id');
$level = $request->post('level');
$id = \support\Encrypt::userIDDecode($id);
if(!$id || !$level){
return $this->error(__('Invalid parameters'));
}
$child_user = UserModel::find($id);
if(!$child_user){
return $this->error(__('Invalid user'));
}
if($child_user->parent_id!=$user->id){
return $this->error(__('Access denied'));
}
if($user->role_id <= $level){
return $this->error(__('It cannot be lower than the user\'s current level'));
}
if($child_user->role_id >= $level){
return $this->error(__('It cannot be lower than the user\'s current level'));
}
$child_user->role_id = $level;
$child_user->save();
return $this->success(__('successful'));
}
}
+164
View File
@@ -0,0 +1,164 @@
<?php
namespace app\api\controller;
use support\Request;
use support\Response;
use support\think\Db;
use hg\apidoc\annotation as Apidoc;
/**
* VIP
*/
class ThaliController extends BaseController{
/**
* 不需要鉴权的方法
* @var array
*/
public $noNeedAuth = ['*'];
/**
* 无需登录及鉴权的方法
* @var array
*/
public $noNeedLogin = ['recent','list'];
/**
* @Apidoc\Title("列表")
* @Apidoc\Method("GET")
* @Apidoc\Param("page", type="int",require=false, desc="页码")
* @Apidoc\Param("limit", type="int",require=false, desc="分页大小")
*/
public function list(Request $request){
$limit = $request->get('limit',10);
$kw = $request->get('kw');
$model = \app\model\Thali::with(['Role'])->where('status',1)->order('id asc');
if($limit == 'all' || $limit >= 999999){
$result = $model->select();
$result= \think\Paginator::make($result, 99999999999, 1, count($result));
}else{
$result = $model->paginate($limit);
}
return $this->success(__('successful'),$result);
}
/**
* @Apidoc\Title("当前角色信息")
* @Apidoc\Method("GET")
* @Apidoc\NotParse()
* @Apidoc\NotDebug()
*/
function detail(){
$user = \support\Jwt::getUser();
$data = \app\model\UserRole::where('id',$user->role_id)->field('name,id,price')->find();
return $this->success(__('successful'),$data);
}
/**
* @Apidoc\Title("最近购买列表")
* @Apidoc\Method("GET")
*/
public function recent(Request $request){
$list = (new \app\model\BalanceLog)->setSuffix('_score')
->where('type',\app\enum\BalanceType::PURCHASE_ROLE->value)
->order('created_at','desc')
->limit(0,10)
->field('user_id,created_at,memo')
->select();
$list = $list->toArray();
$data = [];
$role_list = \app\model\UserRole::where('id', '>',1)
->column('name','id');
foreach($list as $v){
$data[] = [
'username' => \app\model\User::where('id',$v['user_id'])->value('username'),
'created_at' => $v['created_at'],
'v' => $v,
//'role' => $role_list[str_replace('购买用户组:','',$v['memo'])]
'role' => 'K'.str_replace('购买用户组:','',$v['memo'])
];
}
return $this->success(__('successful'),$data);
}
/**
* @Apidoc\Title("购买")
* @Apidoc\Method("POST")
* @Apidoc\Param("id", type="string",require=true, desc="要购买的ID")
* @Apidoc\Param("quantity", type="string",require=true, desc="要购买的数量(单位月)")
* @Apidoc\Param("trade_password", type="string",require=true, desc="交易密码")
*/
function buy(Request $request): Response{
$user = \support\Jwt::getUser();
$id = (int)$request->post('id');
//数量
$quantity = (int)$request->post('quantity',1);
/**
* @var \app\model\Thali $thali
*/
$thali = \app\model\Thali::where('id',$id)->find();
if(!$thali){
return $this->fail(__('Role does not exist'));
}
$role_id = $thali->role_id;
if($user->role_id > $role_id){
return $this->fail(__('Your level is too high to purchase this character'));
}
$price = $thali->price;
if($quantity == 1){
$price = $thali->month_price;
}
if($quantity == 3){
$price = $thali->quarter_price;
}
if($quantity == 12){
$price = $thali->year_price;
}
//新开通
$isNew=false;
if(is_null($user->role_id)){
$isNew = true;
}
//升级
$isUpgrade=true;
//续费
if($user->role_id == $role_id){
$isUpgrade = false;
}
$amount = $price;
if($isUpgrade){
//按那个价格算,目前是按原价,剩余时间不做抵扣
}
//$amount = $price * $quantity;
if($amount <=0){
return $this->fail(__('This character group is not allowed to be sold'));
}
if($user->score < $amount){
return $this->fail(__('Insufficient balance'));
}
\support\Jwt::verify_trade_password($request->post('trade_password'));
$user = \support\Jwt::getUser();
$user->expire_at = ($user->expire_at>time() ? $user->expire_at : time())+86400* $quantity * 30;
if($isUpgrade){
$user->expire_at = (time())+86400* $quantity * 30;
$user->role_id = $role_id;
}
$user->save();
cache('user_role_'.$user->userID,[
'role_id'=>$role_id,'expire_at'=>$user->expire_at
],$user->expire_at-time());
\app\model\User::score($user->id,-$amount,\app\enum\BalanceType::PURCHASE_ROLE,json_encode(['role_id'=>$role_id,'quantity'=>$quantity,'role_name'=>$thali->title]));
cache('user_rights_'.$user->id,null);
if($isNew){
Hook('user.role_up', $user);
}
$data = [
'role_id' => $role_id,
'user_id' => $user->id,
'amount' => $amount,
];
Hook('user.role_buy', $data);
return $this->success(__('successful'),$user);
}
}
+293
View File
@@ -0,0 +1,293 @@
<?php
namespace app\api\controller;
use Shopwwi\WebmanFilesystem\FilesystemFactory;
use Shopwwi\WebmanFilesystem\Facade\Storage;
use app\model\User as UserModel;
use app\model\Realname as RealnameModel;
use support\Request;
use support\Response;
use support\think\Db;
use hg\apidoc\annotation as Apidoc;
/**
* 用户相关
*/
class UserController extends BaseController{
/**
* 不需要鉴权的方法
* @var array
*/
public $noNeedAuth = ['*'];
/**
* 无需登录及鉴权的方法
* @var array
*/
public $noNeedLogin = [];
/**
* @Apidoc\Title("个人资料")
* @Apidoc\Method("GET","POST")
* @Apidoc\Tag("常用")
* @Apidoc\Desc("GET为获取用户信息,POST为修改数据")
* @Apidoc\Param("nickname", type="string",require=true, desc="昵称")
*/
public function profile()
{
$data = \support\Jwt::getUser();
if(Request()->method() == 'POST'){
$nickname = input('nickname');
$gender = input('gender',null);
$faceURL = input('faceURL',null);
$birth = input('birth',null);
$bio = input('bio',null);
$save_data =[];
if($nickname){
$save_data['nickname'] = $nickname;
}
if($gender!=null){
$save_data['sex'] = $gender;
}
if($faceURL){
$save_data['faceURL'] = $faceURL;
}
if($bio){
$save_data['bio'] = $bio;
}
if($birth){
$save_data['birthday'] = datetime($birth/1000,'Y-m-d');
}
if(!empty($save_data)){
\support\Jwt::getUser()->save($save_data);
}
return $this->success(__('successful'));
}
$data = \support\Jwt::getUserInfo($data);
$data= Hook('user.profile',$data);
return $this->success(__('successful'),$data[0]);
}
/**
* @Apidoc\Title("修改密码")
* @Apidoc\Method("POST")
* @Apidoc\Param("password", type="string",require=true, desc="旧密码")
* @Apidoc\Param("newpassword", type="string",require=true, desc="新密码")
* @Apidoc\Param("renewpassword", type="string",require=true, desc="新密码")
*/
public function change_password(){
$password = input('password');
$newpassword = input('newpassword');
$renewpassword = input('renewpassword');
if (!$password || !$newpassword || !$renewpassword) {
return $this->error(__('Invalid parameters'));
}
if ($newpassword !== $renewpassword) {
return $this->error(__('Invalid parameters'));
}
try{
\support\Jwt::changepwd($newpassword,$password);
return $this->success(__('Reset password successful'));
} catch (\Throwable $e) {
return $this->error($e->getMessage());
}
}
/**
* 修改交易密码
* @Apidoc\Method("POST")
* @Apidoc\Param("password", type="string",require=true, desc="旧密码(新设时可用为空)")
* @Apidoc\Param("newpassword", type="string",require=true, desc="新密码")
* @Apidoc\Param("renewpassword", type="string",require=true, desc="新密码")
* @Apidoc\Param("code", type="string",require=true, desc="验证码")
* @Apidoc\Param("verify_type", type="string",require=true, desc="验证方式,email,mobile,password")
*/
public function change_trade_password(){
$user = \support\Jwt::getUser();
$password = input('password');
$newpassword = input('newpassword');
$renewpassword = input('renewpassword');
$verify_type = input('verify_type');
if (!$newpassword || !$renewpassword || $newpassword !== $renewpassword) {
return $this->error(__('Invalid parameters'));
}
if($verify_type == 'email'){
captcha_verify('email','reset_trade_pwd',$user->email);
try{
\support\Jwt::change_trade_pwd($newpassword,'',true);
return $this->success(__('Reset trade password successful'));
} catch (\Throwable $e) {
return $this->error($e->getMessage());
}
}else if($verify_type == 'mobile'){
captcha_verify('mobile','reset_trade_pwd',$user->mobile);
try{
\support\Jwt::change_trade_pwd($newpassword,'',true);
return $this->success(__('Reset trade password successful'));
} catch (\Throwable $e) {
return $this->error($e->getMessage());
}
}else if($verify_type == 'password'){
if (!$password) {
return $this->error(__('Invalid parameters'));
}
try{
\support\Jwt::change_trade_pwd($newpassword,$password);
return $this->success(__('Reset trade password successful'));
} catch (\Throwable $e) {
return $this->error($e->getMessage());
}
}
}
/**
* 根据关键字查询用户列表
* @Apidoc\Method("POST")
* @Apidoc\Param("kw", type="string",require=true, desc="关键字")
*/
function getuserlist(){
$kw = Input('kw');
$user_id = \support\Jwt\JwtToken::getCurrentId();
$list = [];
if($kw){
//$list = User::where('id','<>',\support\Jwt\JwtToken::getCurrentId())->whereLike('nickname|username|email','%'.$kw.'%')->limit(0,10)->order('id asc')->field('id,username')->select();
//$list = User::where('id','<>',\support\Jwt\JwtToken::getCurrentId())->whereLike('username','%'.$kw.'%')->limit(0,10)->order('id asc')->field('id,username,username as name')->select();
$list = UserModel::whereLike('username','%'.$kw.'%')->where('id','<>',$user_id)->limit(0,10)->order('id asc')->field('id,username,username as name')->select();
// foreach($list as $k=>$v){
// }
}
return $this->success(__('successful'),$list);
}
/**
* 头像上传
* @Apidoc\Method("POST")
* @Apidoc\Param("file", type="File", require=true, desc="文件")
*/
public function avatar(Request $request)
{
//单文件上传
$res = $this->_upload($request);
if(is_string($res)){
return $this->fail( $res);
}
$data = [
'avatar' => $res[0]['file_name'],
];
\support\Jwt::getUser()->save($data);
return $this->success(__('successful'),$data);
}
/**
* 设置个人banner
* @Apidoc\Method("POST")
* @Apidoc\Param("file", type="File", require=true, desc="文件")
*/
public function setBanner(Request $request)
{
$user_id = \support\Jwt\JwtToken::getCurrentId();
//单文件上传
$res = $this->_upload($request);
if(is_string($res)){
return $this->fail( $res);
}
$data = [
'profile_banner' => $res[0]['file_name'],
];
Db::name('user_extend')->where('user_id',$user_id)->save($data);
return $this->success(__('successful'),$data);
}
function realname(Request $request): Response
{
/**
* @var UserModel $user
*/
$user = \support\Jwt::getUser();
if($request->method() == 'POST'){
$data = [
'realname' => Input('realname'),
'idcard' => Input('idcard'),
'user_id' => $user->id,
];
if(!$data['realname'] || !$data['idcard']){
return $this->error(__('Invalid parameter'));
}
if($user->realname_verify == 1){
return $this->error(__('You have verified'));
}
if(RealnameModel::where('idcard',$data['idcard'])->where('user_id','<>',$user->id)->count()){
return $this->error(__('ID card already exists'));
}
Db::startTrans();
try {
RealnameModel::create($data);
$user->realname_verify = 1;
$user->save();
if($user->parent_id && cache('invite_'.$user->parent_id.'_'.date('Ymd')) < 1){
\app\model\User::currency1($user->parent_id,40,\app\enum\BalanceType::INVITE_NEW_USER);
cache('invite_'.$user->parent_id.'_'.date('Ymd'),1);
}
Db::commit();
return $this->success('ok',$user);
} catch (\Exception $e) {
Db::rollback();
return $this->error(__($e->getMessage()));
}
}else{
$user->realname = RealnameModel::where('user_id',$user->id)->find();
return $this->success('ok',$user);
}
}
/**
* find
* @Apidoc\Method("POST")
* @Apidoc\Param("userIDs", type="array", require=true, desc="userIDs")
*/
function find(Request $request): Response
{
$ids = Input('userIDs');
if(is_string($ids)){
$ids = explode(',',$ids);
}
//$userIDs = array_map('\support\Encrypt::userIDDecode',$ids);
//$res = $request->IM->user->getUsersInfo($userIDs);
$list = Db::name('user')->alias('u')
->leftJoin('user_extend ue','ue.user_id=u.id')
->field('u.*,ue.profile_banner')
->whereIn('u.userID',$ids)
->paginate(Input('limit',10));
$list->each(function($user){
$data = \support\Jwt::getUserInfo($user);
$data= Hook('user.profile',$data);
return $data[0];
//$user->hidden(['password']);
});
return $this->success('ok',$list);
}
/**
* search
* @Apidoc\Method("POST")
* @Apidoc\Param("keyword", type="string", require=true, desc="关键字")
* @Apidoc\Param("searchtype", type="string", require=true, desc="搜索类型")
*/
function search(Request $request): Response
{
$keyword = Input('keyword');
$searchtype = Input('searchtype');
$fields = 'u.userID,u.avatar,u.username,u.nickname,u.avatar,u.sex,u.email,u.mobile,u.birthday,u.bio,ue.profile_banner';
$model = Db::name('user')->alias('u')
->join('user_extend ue','ue.user_id=u.id')
->field($fields)
->where('status',1);
$model = $model->where('u.userID',$keyword);
// if($searchtype =='id'){
// $model = $model->where('id',$keyword);
// }else{
// $model = $model->whereLike('username|id','%'.$keyword.'%');
// }
$list = $model->paginate(Input('limit',10));
$list->each(function ($item){
$item['id'] = $item['userID'];
return $item;
});
return $this->success('ok',$list);
}
}
+163
View File
@@ -0,0 +1,163 @@
<?php
namespace app\api\controller;
use app\model\User as UserModel;
use hg\apidoc\annotation as Apidoc;
/**
* 验证接口
*/
class ValidateController extends BaseController
{
public $noNeedLogin = '*';
/**
* 检测邮箱是否可用
*
* @Apidoc\Method ("POST")
* @Apidoc\Param("email", type="string",require=true, desc="邮箱")
* @Apidoc\Param("id", type="string",require=true, desc="排除会员ID")
*/
public function check_email_available()
{
$email = input('email');
$id = (int)input('id');
$count = UserModel::where('email', '=', $email)->where('id', '<>', $id)->count();
if ($count > 0) {
return $this->error(__('The mailbox is already occupied'));
}
return $this->success(__('successful'));
}
/**
* 检测用户名
*
* @Apidoc\Method ("POST")
* @Apidoc\Param("username", type="string",require=true, desc="用户名")
* @Apidoc\Param("id", type="string",require=true, desc="排除会员ID")
*/
public function check_username_available()
{
$username = input('username');
$id = (int)input('id');
$count = UserModel::where('username', '=', $username)->where('id', '<>', $id)->count();
if ($count > 0) {
return $this->error(__('Username is already taken'));
}
return $this->success(__('successful'));
}
/**
* 检测昵称
*
* @Apidoc\Method ("POST")
* @Apidoc\Param("nickname", type="string",require=true, desc="昵称")
* @Apidoc\Param("id", type="string",require=true, desc="排除会员ID")
*/
public function check_nickname_available()
{
$nickname = input('nickname');
$id = (int)input('id');
$count = UserModel::where('nickname', '=', $nickname)->where('id', '<>', $id)->count();
if ($count > 0) {
return $this->error(__('Nickname is already taken'));
}
return $this->success(__('successful'));
}
/**
* 检测手机
*
* @Apidoc\Method ("POST")
* @Apidoc\Param("mobile", type="string",require=true, desc="手机号")
* @Apidoc\Param("id", type="string",require=true, desc="排除会员ID")
*/
public function check_mobile_available()
{
$mobile = input('mobile');
$id = (int)input('id');
$count = UserModel::where('mobile', '=', $mobile)->where('id', '<>', $id)->count();
if ($count > 0) {
return $this->error(__('Phone Number is already taken'));
}
return $this->success(__('successful'));
}
/**
* 检测手机是否存在
*
* @Apidoc\Method ("POST")
* @Apidoc\Param("mobile", type="string",require=true, desc="手机号")
*/
public function check_mobile_exist()
{
$mobile = input('mobile');
$count = UserModel::where('mobile', '=', $mobile)->count();
if (!$count) {
return $this->error(__('Mobile number does not exist'));
}
return $this->success(__('successful'));
}
/**
* 检测邮箱是否存在
*
* @Apidoc\Method ("POST")
* @Apidoc\Param("email", type="string",require=true, desc="邮箱")
*/
public function check_email_exist()
{
$email = input('email');
$count = UserModel::where('email', '=', $email)->count();
if (!$count) {
return $this->error(__('Email does not exist'));
}
return $this->success(__('successful'));
}
/**
* 检测手机验证码(弃用)
*
* @Apidoc\Method ("POST")
* @Apidoc\Param("mobile", type="string",require=true, desc="手机号")
* @Apidoc\Param("code", type="string",require=true, desc="验证码")
* @Apidoc\Param("event", type="string",require=true, desc="事件")
*/
protected function check_sms_correct()
{
$mobile = input('mobile');
$captcha = input('captcha');
$event = input('event');
// if (!\app\common\library\Sms::check($mobile, $captcha, $event)) {
// $this->error(__('Incorrect verification code'));
// }
return $this->success(__('successful'));
}
/**
* 检测邮箱验证码
*
* @Apidoc\Method ("POST")
* @Apidoc\Param("email", type="string",require=true, desc="邮箱")
* @Apidoc\Param("code", type="string",require=true, desc="验证码")
* @Apidoc\Param("event", type="string",require=true, desc="事件")
*/
public function check_ems_correct()
{
$email = input('email');
$captcha = input('code');
$event = input('event');
$cache_key = 'captcha_'.$event.'_'.$email;
$list = cache($cache_key);
$list = $list?:[];
if(!isset($list[$captcha])){
return $this->error(__('Incorrect verification code'));
}
if($list[$captcha]+5*60 >= time()){
unset($list[$captcha]);
cache($cache_key,$list);
return $this->error(__('Verification code has expired'));
}
return $this->success(__('successful'));
}
}
+226
View File
@@ -0,0 +1,226 @@
<?php
namespace app\api\controller;
use app\model\User as UserModel;
use support\Request;
use app\model\Cdkey as CdkeyModel;
use support\think\Db;
use hg\apidoc\annotation as Apidoc;
/**
* 钱包接口
*/
class WalletController extends BaseController{
/**
* 不需要鉴权的方法
* @var array
*/
public $noNeedAuth = ['*'];
/**
* 无需登录及鉴权的方法
* @var array
*/
public $noNeedLogin = [];
/**
* 用户货币互换
* @Apidoc\Method("POST")
* @Apidoc\Param("currency", type="string",require=true, desc="货币money_to_score")
* @Apidoc\Param("sendAmount", type="string",require=true, desc="money兑换数量")
* @Apidoc\Param("receiveAmount", type="string",require=true, desc="score兑换数量")
* @Apidoc\Param("trade_password", type="string",require=true, desc="交易密码")
* @Apidoc\Param("code", type="string",require=true, desc="图形验证码(event=exchange)")
*/
public function exchange(){
//return $this->error(__('The system is under maintenance, please wait...'));
$user = \support\Jwt\JwtToken::getUser();
// if(Config('site.trade_password_type') == 'email'){
// captcha_verify('email','exchange',$user['username']);
// }else{
// $trade_password = input('trade_password');
// \support\Jwt::verify_trade_password($trade_password);
// }
$currency_pair = input('currency');
$currencys = explode('_to_', $currency_pair);
$from_currency = $currencys[0];
$to_currency = $currencys[1];
if(!$from_currency || !$to_currency){
return $this->error(__('Invalid parameters'));
}
$sendAmount = (float)input('sendAmount');
$receiveAmount = (float)input('receiveAmount');
$rate = Config('site.'.$currency_pair.'_rate');
if(!$sendAmount || !$receiveAmount || !$rate){
return $this->error(__('Invalid parameters'));
}
$_receiveAmount = intval($sendAmount / $rate);
$_sendAmount = $_receiveAmount * $rate;
if($sendAmount > $user->$from_currency || $receiveAmount <= 0){
return $this->error(__('Invalid parameters').$sendAmount .'<' .$user->$from_currency .'||'. $receiveAmount);
}
Db::startTrans();
try{
UserModel::$from_currency($user->id,-$_sendAmount,\app\enum\BalanceType::EXCHANGE);
UserModel::$to_currency($user->id,$_receiveAmount,\app\enum\BalanceType::EXCHANGE);
Db::commit();
return $this->success(__('Exchange successful'));
}catch(\Exception $e){
Db::rollback();
return $this->error($e->getMessage());
}
}
/**
* 用户间score转账
* @Apidoc\Method("POST")
* @Apidoc\Param("username", type="string",require=true, desc="收款用户/用户ID")
* @Apidoc\Param("amount", type="string",require=true, desc="金额")
* @Apidoc\Param("trade_password", type="string",require=true, desc="交易密码")
* @Apidoc\Param("code", type="string",require=true, desc="图形验证码(event=transfer)")
*/
public function transfer(){
//return $this->error(__('The system is under maintenance, please wait...'));
$user = \support\Jwt::getUser();
$username = input('username');
if(!$username){
return $this->error(__('User is incorrect'));
}
/** @var UserModel $to_user */
if(str_contains($username,'@')){
$to_user = UserModel::where('username',$username)->find();
}else{
$to_user_id = \support\Encrypt::userIDDecode($username);
$to_user = UserModel::where('id',$to_user_id)->find();
}
if(!$to_user){
return $this->error(__('User is incorrect'));
}
if(Config('site.trade_password_type') == 'email'){
//captcha_verify('email','transfer',$to_user['username']);
}else{
$trade_password = input('trade_password');
\support\Jwt::verify_trade_password($trade_password);
}
$amount = (float)input('amount');
if($amount <= 0){
return $this->error(__('Invalid parameters'));
}
if($user->score < $amount){
return $this->error(__('Insufficient balance'));
}
Db::startTrans();
try{
UserModel::score($user->id,-$amount,\app\enum\BalanceType::TRANSFER,$to_user->id);
UserModel::score($to_user->id,$amount,\app\enum\BalanceType::TRANSFER,$user->id);
Db::commit();
return $this->success(__('Transfer successful'));
}catch(\Exception $e){
Db::rollback();
return $this->error($e->getMessage());
}
}
/**
* 根据关键字查询用户列表
* @Apidoc\Method("POST")
* @Apidoc\Param("kw", type="string",require=true, desc="关键字")
*/
function getuserlist(){
$kw = Input('kw');
$user_id = \support\Jwt\JwtToken::getCurrentId();
$list = [];
if($kw){
//$list = User::where('id','<>',\support\Jwt\JwtToken::getCurrentId())->whereLike('nickname|username|email','%'.$kw.'%')->limit(0,10)->order('id asc')->field('id,username')->select();
//$list = User::where('id','<>',\support\Jwt\JwtToken::getCurrentId())->whereLike('username','%'.$kw.'%')->limit(0,10)->order('id asc')->field('id,username,username as name')->select();
$list = UserModel::whereLike('username','%'.$kw.'%')->where('id','<>',$user_id)->limit(0,10)->order('id asc')->field('id,username,username as name')->select();
// foreach($list as $k=>$v){
// }
}
return $this->success(__('successful'),$list);
}
/**
* 本地cdkey兑换
* @Apidoc\Method("POST")
* @Apidoc\Param("cdkey", type="string",require=true, desc="cdkey")
* @Apidoc\Param("trade_password", type="string",require=true, desc="交易密码")
* @Apidoc\Param("code", type="string",require=true, desc="图形验证码(event=cdkeyExchange)")
*/
public function cdkeyExchange_local_cdkey(){
//return $this->error(__('The system is under maintenance, please wait...'));
$user = \support\Jwt\JwtToken::getUser();
// if(Config('site.trade_password_type') == 'email'){
// captcha_verify('email','exchange',$user['username']);
// }else{
// $trade_password = input('trade_password');
// \support\Jwt::verify_trade_password($trade_password);
// }
$cdkey = input('cdkey');
/** @var CdkeyModel $Cdkey */
$Cdkey = CdkeyModel::where('account',$cdkey)->lock(true)->where('is_used',0)->find();
if(!$Cdkey){
return $this->error(__('卡密不存在'));
}
if($Cdkey['type'] == 3){
//不能使用续费激活码
return $this->error(__('卡密不存在'));
}
Db::startTrans();
try{
CdkeyModel::where('id',$Cdkey->id)->save([
'record_id' => $user->id,
'is_used' => 1,
'use_time' => time(),
]);
UserModel::score($user->id,$Cdkey->days,\app\enum\BalanceType::RECHARGE_CARD);
Db::commit();
return $this->success(__('Exchange successful'));
}catch(\Exception $e){
Db::rollback();
return $this->error($e->getMessage());
}
}
/**
* cdkey兑换
* @Apidoc\Method("POST")
* @Apidoc\Param("card_number", type="string",require=true, desc="卡号")
* @Apidoc\Param("password", type="string",require=true, desc="密码")
*/
function cdkey_exchange(){
$user = \support\Jwt\JwtToken::getUser();
$domain = 'http://127.0.0.1:8383';
$data=[
'user_id' => \support\Jwt\JwtToken::getCurrentId(),
'card_number'=> input('card_number'),
'password'=> input('password'),
];
$activeData = [
'app_id' => 8,
'card_number' => $data['card_number'],
'password' => $data['password'],
'type' => 'recharge',
'record_id' => $user->id
];
$remoteResponse = post($domain.'/api/cdkey/redeem',$activeData);
\support\Log::info($remoteResponse);
try{
$remoteResponse = json_decode($remoteResponse,true);
}catch(\Exception $e){
return $this->error($e->getMessage());
}
if($remoteResponse['code'] !== 0){
\support\Log::info(json_encode($remoteResponse));
return $this->error($remoteResponse['msg']);
}
if($remoteResponse['data']['days']){
UserModel::money($user->id,$remoteResponse['data']['days'],\app\enum\BalanceType::RECHARGE_CARD);
return $this->success(__('Exchange successful'));
}
return $this->error($remoteResponse['msg'],$remoteResponse);
}
}
+187
View File
@@ -0,0 +1,187 @@
<?php
namespace app\api\controller;
use app\model\Address as AddressModel;
use app\model\User as UserModel;
use app\model\Withdrawl as WithdrawlModel;
use support\Request;
use support\think\Db;
use hg\apidoc\annotation as Apidoc;
/**
* 提现模块
* @Apidoc\NotParse()
* @Apidoc\NotDebug()
*/
class WithdrawlController extends BaseController{
/**
* 不需要鉴权的方法
* @var array
*/
public $noNeedAuth = ['*'];
/**
* 无需登录及鉴权的方法
* @var array
*/
public $noNeedLogin = ['notify','notify1','recent'];
/**
* 列表
* @Apidoc\Method("GET")
* @Apidoc\Query("status", type="int", require=false, desc="状态")
* @Apidoc\Query("page", type="int", require=true, desc="页码",default=1)
* @Apidoc\Query("limit", type="int", require=true, desc="分页大小",default=10)
*/
public function list()
{
$limit = (int)input('limit',10);
$status = input('status','all');
$model = WithdrawlModel::where('user_id',\support\Jwt\JwtToken::getCurrentId())
->order('id desc');
if($status!='all'){
$model = $model->where('status',$status);
}
$list = $model->paginate($limit);
return $this->success(__('successful'),$list->toArray());
}
/**
* 最近提现
* @Apidoc\Method("GET")
*/
public function recent()
{
$list = WithdrawlModel::with(['user'])->
where('status',\app\enum\WithdrawlStatus::COMPLETE->value)->
order('id desc')->
limit(10)->select();
$list->each(function($item){
$item->user = UserModel::field('username,email')->where('id',$item->user_id)->find();
});
return $this->success(__('successful'),$list->toArray());
}
/**
* 创建
* @Apidoc\Method("POST")
* @Apidoc\Param("amount", type="string", require=true, desc="金额")
* @Apidoc\Param("address_id", type="string", require=true, desc="地址ID,列表选择")
* @Apidoc\Param("trade_password", type="string", require=true, desc="交易密码")
*/
public function create()
{
//return $this->error(__('The system is under maintenance, please wait...'));
//* @Apidoc\Param("code", type="string", require=true, desc="图形验证码(type=withdrawl)")
//captcha_verify('image','withdrawl');
$address_id = input('address_id');
if(!$address_id){
return $this->error(__('Address is incorrect'));
}
/** @var AddressModel $address */
$address = AddressModel::where('id',$address_id)->find();
if(!$address){
return $this->error(__('Address is incorrect'));
}
// if(!$address->status){
// return $this->error(__('Unverified address'));
// }
$user = \support\Jwt::getUser();
if(Config('site.trade_password_type') == 'email'){
captcha_verify('email','withdrawl',$user['username']);
}else{
//验证交易密码
$trade_password = input('trade_password');
\support\Jwt::verify_trade_password($trade_password);
}
$deduction_amount = input('amount',0);
$fee = config('site.withdrawl_fee')[$address['network']];
if($fee < 0.5){
$fee = bcmul( $fee , $deduction_amount,2);
}
$data = [
'user_id' => \support\Jwt\JwtToken::getCurrentId(),
'deduction_amount' => $deduction_amount,
'title' => $address['title'],
'network' => $address['network'],
'address' => $address['address'],
'fee' => $fee,
'type' => 0,
'status' => \app\enum\WithdrawlStatus::CREATED->value
];
//验证最小提现金额
$data['recive_amount'] = $data['deduction_amount'] - $data['fee'];
$withdrawl_minimum = Config('site.withdrawl_minimum')[$data['network']];
if($data['deduction_amount'] < $withdrawl_minimum){
return $this->error(__('Minimum withdrawal of %num%',[
'%num%' => $withdrawl_minimum
]));
}
//var_dump($user);
//验证余额
if($data['deduction_amount'] > $user->money){
return $this->error(__('The amount exceeds the available balance'));
}
//if(WithdrawlModel::whereTime('created_at','-24 hours')->count('id')){
if(WithdrawlModel::whereTime('created_at','today')->where('user_id',$data['user_id'])->count('id')){
return $this->error(__('You can only withdraw once a day.'));
}
if (!$data['network'] || !in_array($data['network'],['BEP-20','TRC-20','ALIPAY','WECHAT'])) {
return $this->error(__('Network is incorrect'));
}
if (!$data['address']) {
return $this->error(__('Address is incorrect'));
}
Db::startTrans();
try{
/** @var WithdrawlModel $data */
$data = WithdrawlModel::create($data);
UserModel::money($data->user_id,-$data->deduction_amount,\app\enum\BalanceType::WITHDRAWAL,$data->id);
Db::commit();
return $this->success(__('successful'),$data);
}catch(\Exception $e){
Db::rollback();
return $this->error($e->getMessage());
}
}
/**
* 详情
* @Apidoc\Query("id", type="string", require=true, desc="ID")
*/
public function detail(){
$appid = input('id');
$vo = WithdrawlModel::where('id',$appid)->find();
if($vo) {
return $this->success(__('successful'),$vo->toArray());
}else{
return $this->error(__("Record does not exist"));
}
}
/**
* 转账成功异步通知
* @Apidoc\NotParse()
* @Apidoc\NotDebug()
*/
public function notify(){
$data = \support\Encrypt::aesdecode(input('data',''));
$data = json_decode($data,true);
/** @var WithdrawlModel $vo */
$vo = WithdrawlModel::where('id',$data['out_trade_no'])->find();
if($vo){
if($data['result'] == 'SUCCESS'){
if($vo->status != \app\enum\WithdrawlStatus::COMPLETE->value){
$vo->status = \app\enum\WithdrawlStatus::COMPLETE->value;
$vo->txid = $data['txid'];
$vo->transfer_at = $data['transfer_at'] ?: time();
$vo->save();
Hook('withdrawl.success',$vo);
}
}else{
$vo->status = \app\enum\WithdrawlStatus::FAIL->value;
$vo->txid = $data['txid'];
$vo->memo = $data['reason'];
$vo->save();
}
}
return response("SUCCESS");
}
}
+149
View File
@@ -0,0 +1,149 @@
<?php
namespace app\api\middleware;
use ReflectionException;
use support\exception\BusinessException;
use Webman\Http\Request;
use Webman\Http\Response;
use Webman\MiddlewareInterface;
use support\Container;
class Auth implements MiddlewareInterface
{
/**
* @param Request $request
* @param callable $handler
* @return Response
* @throws ReflectionException|BusinessException
*/
public function process(Request $request, callable $next): Response
{
$headers = [
'Access-Control-Allow-Credentials' => 'true',
'Access-Control-Allow-Origin' => $request->header('origin', '*'),
'Access-Control-Allow-Methods' => $request->header('access-control-request-method', '*'),
'Access-Control-Allow-Headers' => $request->header('access-control-request-headers', '*'),
];
if($request->method() == 'OPTIONS'){
$response = response('',204,$headers);
return $response;
}
$lang = $request->header('lang','zh-Hans');
locale($lang);
if ($request->controller) {
$request->client = $request->header('client',"web");
// if($request->client=='win' && $request->header('version') < 2.06){
// abort('旧版本不能再使用,请更新到最新版本', 603);
// }
//跨域请求检测
//check_cors_request();
// 检测IP是否允许
//check_ip_allowed();
$request->start_time = microtime();
$controller = Container::get($request->controller);
// 检测是否需要验证登录
if (!\support\Jwt::match($controller->noNeedLogin)) {
//检测是否登录
try {
if (!\support\Jwt::isLogin()) {
return new Response(401,$headers,json_encode([
"code"=>401,
"data"=>[],
"msg"=>__('Please login first')
]));
}
} catch (\Exception $e) {
return new Response(401,$headers,json_encode([
"code"=>401,
"data"=>[],
"msg"=>__('Please login first')
]));
}
$user = \support\Jwt\JwtToken::getUser();
if(!$user['status']){
return new Response(403,$headers,json_encode([
"code"=>403,
"data"=>[],
"msg"=>__('Account is locked')
]));
}
// $key = "debounce_" . $request->path() . "_" . ($user->id ?? 'guest');
// $ttl = 1; // 防抖时间(秒)
// $redishandler = new \Redis;
// $redishandler->connect(
// \support\Env::get('host'),
// (int) \support\Env::get('port'),
// (int) \support\Env::get('timeout'));
// $redishandler->select(12);
// if ($redishandler->setnx($key, 1)) {
// $redishandler->expire($key, $ttl);
// }else{
// return new Response(429,[],__('Too frequent operation'));
// }
// 判断是否需要验证权限
if (!\support\Jwt::match($controller->noNeedAuth)) {
// 判断控制器和方法判断是否有对应权限
$controllername = get_controller_name();
$actionname = strtolower(get_action_name());
$path = str_replace('.', '/', $controllername) . '/' . $actionname;
if (!\support\Jwt::check($path)) {
return new Response(405,$headers,json_encode([
"code"=>405,
"data"=>[],
"msg"=>__('have no permission')
]));
}
}
}
// if($request->client!='web'){
// $data = $request->post('data');
// if($data){
// $data = str_replace('%3D','=',$data);
// $data = str_replace(' ','+',$data);
// //var_dump($data);
// $data = \support\Encrypt::aesdecode($data);
// $data = json_decode($data,true);
// //var_dump($data);
// $request->withBody($data);
// }
// }
$config = Config('site');
$config['debug'] = config('app.debug');
$config['controller'] = $request->controller_name;
$config['action'] = $request->action_name;
$request->_view_vars = array_merge((array) $request->_view_vars,[
'user' => session('admin'),
'config' => $config
]);
$IM = new \support\OpenImSdk\Client([
'host' => config('openim.server'), // OpenIM API地址
'secret' => config('openim.secret'), // OpenIM密钥
]);
$request->IM = $IM;
$response = $next($request);
//cp('auth');
//\support\Log::alert('auth');
$body = str_replace([
'__SELF__'
],[
request()->path()
],$response->rawBody());
// if($request->app=="api" && $request->client!='web'){
// $body = \support\Encrypt::aesencode($body);
// }
$response->withHeaders($headers)->withBody($body)->getStatusCode();
$time = microtime() - $request->start_time;
//echo("响应时间:".$request->uri().':'.$time.PHP_EOL);
//$response = $next($request);
//\support\Log::error($response->rawBody());
return $response;
}
return $next($request);
}
}
+51
View File
@@ -0,0 +1,51 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
namespace app\bootstrap;
use Webman\Bootstrap;
//use support\Db;
use Symfony\Component\VarDumper\Cloner\VarCloner;
use Symfony\Component\VarDumper\Dumper\CliDumper;
use support\think\Db;
class SqlDebug implements Bootstrap
{
/**
* 自定义输出格式,否则输出前面会带有当前文件,无用信息
* @param $var
* @return void
*/
public static function dumpvar($var): void
{
$cloner = new VarCloner();
$dumper = new CliDumper();
$dumper->dump($cloner->cloneVar($var));
}
public static function start($worker)
{
// Is it console environment ?
$is_console = !$worker;
// if ($is_console) {
// return;
// }
if (!Config("app.debug")) return;
Db::listen(function($sql, $runtime, $master) {
if (!Config("app.debug")) return;
if($sql!='select 1' && $sql){
$sql= preg_replace('/db\.[db\.]+/', 'db.', $sql);
\support\Log::alert('['.$runtime.']'.$sql);
}
});
}
}
+807
View File
@@ -0,0 +1,807 @@
<?php
namespace app\command;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Input\InputOption;
class Backup extends Command
{
protected static $defaultName = 'backup';
protected static $defaultDescription = '备份 MongoDB 和 MySQL 数据库';
// 数据源配置
private $dataSources = [
[
'type' => 'mongodb',
'host' => '127.0.0.1',
'port' => 27017,
'database' => 'openim_v3',
'username' => 'openIM',
'password' => 'n1e5a6s6m7',
'useDocker' => true,
'dockerContainerName' => 'mongo' // Docker 容器名称
],
[
'type' => 'mysql',
'host' => '127.0.0.1',
'port' => 3306,
'database' => 'imadmin',
'username' => 'root',
'password' => 'n1e5a6s6m7',
'useDocker' => true,
'dockerContainerName' => 'my_mysql' // Docker 容器名称
],
[
'type' => 'redis',
'host' => '127.0.0.1',
'port' => 16379,
'database' => 0,
'username' => '',
'password' => 'n1e5a6s6m7',
'useDocker' => true,
'dockerContainerName' => 'redis' // Docker 容器名称
],
[
'name' => 'tettt_mongodb',
'type' => 'mongodb',
'host' => '127.0.0.1',
'port' => 27017,
'database' => 'tettt',
'username' => 'commie',
'password' => 'n1e5a6s6m7',
'authSource' => 'admin',
'useDocker' => true,
'dockerContainerName' => 'mongo'
],
];
protected function configure()
{
$this->addOption('backup', 'b', InputOption::VALUE_NONE, '备份数据库');
$this->addOption('restore', 'r', InputOption::VALUE_NONE, '还原数据库');
$this->addOption('clear', 'c', InputOption::VALUE_NONE, '清空 Redis');
$this->addOption('source', 's', InputOption::VALUE_OPTIONAL, '数据源名称 (mongodb, mysql, redis)');
$this->addOption('output', 'o', InputOption::VALUE_OPTIONAL, '备份输出目录', '/backup');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$backup = $input->getOption('backup');
$restore = $input->getOption('restore');
$clear = $input->getOption('clear');
$source = $input->getOption('source');
$outputDir = $input->getOption('output');
// 确保备份目录存在
$mongoDir = base_path($outputDir) . '/mongo';
$mysqlDir = base_path($outputDir) . '/mysql';
if (!is_dir($mongoDir)) {
mkdir($mongoDir, 0755, true);
}
if (!is_dir($mysqlDir)) {
mkdir($mysqlDir, 0755, true);
}
// 显示备份目录
$output->writeln("\n备份目录:");
$output->writeln("- MongoDB: {$mongoDir}");
$output->writeln("- MySQL: {$mysqlDir}");
// 显示环境配置
$output->writeln("\n环境配置:");
foreach ($this->dataSources as $_source) {
$name = $_source['name'] ?? $_source['dockerContainerName'] ?? ucfirst($_source['type']);
if($name == $source){
$source = $_source;
}
$output->writeln("- {$name}: " . ($_source['useDocker'] ? "Docker 容器" : "本地环境"));
}
// 处理命令行选项
if ($backup) {
if(is_array($source)){
if ($source['type'] === 'mongodb') {
$this->backupMongoDB($mongoDir, $output, $source);
}
if ($source['type'] === 'mysql') {
$this->backupMySQL($mysqlDir, $output, $source);
}
} else {
foreach ($this->dataSources as $_source) {
if ($_source['type'] === 'mongodb') {
$this->backupMongoDB($mongoDir, $output, $_source);
} elseif ($_source['type'] === 'mysql') {
$this->backupMySQL($mysqlDir, $output, $_source);
}
}
}
} elseif ($restore) {
if(is_array($source)){
if ($source['type'] === 'mongodb') {
$this->restoreMongoDB($mongoDir, $output, $source);
}
if ($source['type'] === 'mysql') {
$this->restoreMySQL($mysqlDir, $output, $source);
}
} else {
$this->restoreMenu($output, $outputDir);
}
} elseif ($clear) {
$this->clearRedis($output);
} else {
$this->mainMenu($output, $outputDir);
}
$output->writeln("\n✅ 操作完成!");
return self::SUCCESS;
}
/**
* 主菜单
*/
private function mainMenu($output, $outputDir): void
{
while (true) {
$output->writeln("\n================================");
$output->writeln(" 备份工具");
$output->writeln("================================");
$output->writeln("1. 备份数据库");
$output->writeln("2. 还原数据库");
$output->writeln("3. 清空 Redis");
$output->writeln("0. 退出");
$output->write("\n请选择操作 (0-3): ");
$handle = fopen("php://stdin", "r");
$choice = fgets($handle);
fclose($handle);
$choice = trim($choice);
switch ($choice) {
case '1':
$this->backupMenu($output, $outputDir);
break;
case '2':
$this->restoreMenu($output, $outputDir);
break;
case '3':
$this->clearRedis($output);
break;
case '0':
return;
default:
$output->writeln("\n无效选择,请重新输入");
}
}
}
/**
* 备份菜单
*/
private function backupMenu($output, $outputDir): void
{
$output->writeln("\n================================");
$output->writeln(" 备份数据库");
$output->writeln("================================");
foreach ($this->dataSources as $index => $source) {
if ($source['type'] !== 'redis') {
$name = $source['name'] ?? $source['dockerContainerName'] ?? ucfirst($source['type']);
$output->writeln(($index + 1) . ". " . $name . " (" . ($source['useDocker'] ? "Docker 容器" : "本地环境") . ")");
}
}
$output->writeln("0. 返回上一级");
$output->write("\n请选择要备份的数据源 (0-" . count($this->dataSources) . "): ");
$handle = fopen("php://stdin", "r");
$choice = fgets($handle);
fclose($handle);
$choice = trim($choice);
if ($choice === '0') {
return;
}
if (is_numeric($choice) && $choice > 0 && $choice <= count($this->dataSources)) {
$source = $this->dataSources[$choice - 1];
if ($source['type'] === 'mongodb') {
$mongoDir = base_path($outputDir) . '/mongo';
if (!is_dir($mongoDir)) {
mkdir($mongoDir, 0755, true);
}
$this->backupMongoDB($mongoDir, $output, $source);
} elseif ($source['type'] === 'mysql') {
$mysqlDir = base_path($outputDir) . '/mysql';
if (!is_dir($mysqlDir)) {
mkdir($mysqlDir, 0755, true);
}
$this->backupMySQL($mysqlDir, $output, $source);
}
} else {
$output->writeln("\n无效选择,请重新输入");
}
}
/**
* 还原菜单
*/
private function restoreMenu($output, $outputDir): void
{
$output->writeln("\n================================");
$output->writeln(" 还原数据库");
$output->writeln("================================");
foreach ($this->dataSources as $index => $source) {
if ($source['type'] !== 'redis') {
$name = $source['name'] ?? $source['dockerContainerName'] ?? ucfirst($source['type']);
$output->writeln(($index + 1) . ". " . $name . " (" . ($source['useDocker'] ? "Docker 容器" : "本地环境") . ")");
}
}
$output->writeln("0. 返回上一级");
$output->write("\n请选择要还原的数据源 (0-" . count($this->dataSources) . "): ");
$handle = fopen("php://stdin", "r");
$choice = fgets($handle);
fclose($handle);
$choice = trim($choice);
if ($choice === '0') {
return;
}
if (is_numeric($choice) && $choice > 0 && $choice <= count($this->dataSources)) {
$source = $this->dataSources[$choice - 1];
if ($source['type'] === 'mongodb') {
$mongoDir = base_path($outputDir) . '/mongo';
if (!is_dir($mongoDir)) {
mkdir($mongoDir, 0755, true);
}
$this->restoreMongoDB($mongoDir, $output, $source);
} elseif ($source['type'] === 'mysql') {
$mysqlDir = base_path($outputDir) . '/mysql';
if (!is_dir($mysqlDir)) {
mkdir($mysqlDir, 0755, true);
}
$this->restoreMySQL($mysqlDir, $output, $source);
}
} else {
$output->writeln("\n无效选择,请重新输入");
}
}
private function backupMongoDB($backupDir, $output, $dataSource = null): void
{
$name = $dataSource['name'] ?? $dataSource['dockerContainerName'] ?? ucfirst($dataSource['type']);
$output->writeln("\n开始备份 {$name}...");
try {
$mongoSource = $dataSource;
if (!$mongoSource) {
foreach ($this->dataSources as $source) {
if ($source['type'] === 'mongodb') {
$mongoSource = $source;
break;
}
}
}
if (!$mongoSource) {
$output->writeln("❌ 未找到 MongoDB 数据源配置");
return;
}
$host = $mongoSource['host'];
$port = $mongoSource['port'];
$database = $mongoSource['database'];
$useDocker = $mongoSource['useDocker'];
$dockerContainerName = $mongoSource['dockerContainerName'];
$username = $mongoSource['username'] ?? '';
$password = $mongoSource['password'] ?? '';
$authSource = $mongoSource['authSource'] ?? $database;
$username = $mongoSource['username'] ?? '';
$password = $mongoSource['password'] ?? '';
$authSource = $mongoSource['authSource'] ?? $database;
$backupFileName = "{$database}_" . date("Y_m_d_H_i_s") . ".zip";
$backupFilePath = "{$backupDir}/{$backupFileName}";
$tempDir = "/tmp/mongo_backup_" . uniqid();
if (!is_dir($tempDir)) {
mkdir($tempDir, 0755, true);
}
$cmd = $this->getMongoDumpCommand($host, $port, $database, $tempDir, $useDocker, $dockerContainerName, $username, $password, $authSource);
$output->writeln("执行命令: {$cmd}");
exec($cmd, $outputLines, $returnCode);
if ($returnCode === 0) {
// 保存当前工作目录
$currentDir = getcwd();
// 切换到临时目录并压缩
chdir($tempDir);
$zipCmd = "zip -r {$backupFilePath} .";
$output->writeln("创建压缩文件: {$backupFilePath}");
exec($zipCmd, $zipOutput, $zipReturnCode);
// 切换回原来的工作目录
chdir($currentDir);
if ($zipReturnCode === 0) {
$output->writeln("✅ MongoDB 备份成功: {$backupFilePath}");
} else {
$output->writeln("❌ MongoDB 压缩失败");
$output->writeln(implode("\n", $zipOutput));
}
// 清理临时目录
exec("rm -rf {$tempDir}");
} else {
$output->writeln("❌ MongoDB 备份失败");
$output->writeln(implode("\n", $outputLines));
// 清理临时目录
exec("rm -rf {$tempDir}");
}
} catch (\Exception $e) {
$output->writeln("❌ MongoDB 备份失败: " . $e->getMessage());
}
}
private function restoreMongoDB($backupDir, $output, $dataSource = null): void
{
$name = $dataSource['name'] ?? $dataSource['dockerContainerName'] ?? ucfirst($dataSource['type']);
$output->writeln("\n开始还原 {$name}...");
try {
$mongoSource = $dataSource;
if (!$mongoSource) {
foreach ($this->dataSources as $source) {
if ($source['type'] === 'mongodb') {
$mongoSource = $source;
break;
}
}
}
if (!$mongoSource) {
$output->writeln("❌ 未找到 MongoDB 数据源配置");
return;
}
$host = $mongoSource['host'];
$port = $mongoSource['port'];
$database = $mongoSource['database'];
$useDocker = $mongoSource['useDocker'];
$dockerContainerName = $mongoSource['dockerContainerName'];
$username = $mongoSource['username'] ?? '';
$password = $mongoSource['password'] ?? '';
$authSource = $mongoSource['authSource'] ?? $database;
$backupFiles = glob("{$backupDir}/*.zip");
if (empty($backupFiles)) {
$output->writeln("❌ 未找到备份文件");
return;
}
// 按修改时间排序
usort($backupFiles, function ($a, $b) {
return filemtime($b) - filemtime($a);
});
// 显示备份文件列表
$output->writeln("\n可用的备份文件:");
foreach ($backupFiles as $index => $file) {
$fileName = basename($file);
$fileSize = filesize($file) / 1024 / 1024;
$modTime = date("Y-m-d H:i:s", filemtime($file));
$output->writeln(($index + 1) . ". {$fileName} (" . round($fileSize, 2) . " MB, {$modTime})");
}
// 选择备份文件
$output->write("\n请选择要还原的备份文件 (1-" . count($backupFiles) . "): ");
$handle = fopen("php://stdin", "r");
$choice = fgets($handle);
fclose($handle);
$choice = trim($choice);
if (!is_numeric($choice) || $choice < 1 || $choice > count($backupFiles)) {
$output->writeln("\n无效选择");
return;
}
$selectedFile = $backupFiles[$choice - 1];
$output->writeln("\n选择的备份文件: " . basename($selectedFile));
// 生成临时还原目录
$tempDir = "/tmp/mongo_restore_" . uniqid();
if (!is_dir($tempDir)) {
mkdir($tempDir, 0755, true);
}
// 解压备份文件
$unzipCmd = "unzip {$selectedFile} -d {$tempDir}";
$output->writeln("解压备份文件...");
exec($unzipCmd, $unzipOutput, $unzipReturnCode);
if ($unzipReturnCode === 0) {
$dbRestoreDir = $tempDir;
$backupDbName = null;
$subDirs = glob("{$tempDir}/*", GLOB_ONLYDIR);
$output->writeln("解压后的目录: " . implode(", ", array_map('basename', $subDirs)));
if (!empty($subDirs)) {
foreach ($subDirs as $subDir) {
$bsonFiles = glob("{$subDir}/*.bson");
if (!empty($bsonFiles)) {
$dbRestoreDir = $subDir;
$backupDbName = basename($subDir);
$output->writeln("找到备份目录: {$backupDbName}");
break;
}
}
}
$bsonFiles = glob("{$dbRestoreDir}/*.bson");
if (empty($bsonFiles)) {
$output->writeln("❌ 备份文件中没有找到 BSON 数据文件");
$output->writeln("目录内容: " . implode(", ", scandir($dbRestoreDir)));
exec("rm -rf {$tempDir}");
return;
}
$output->writeln("找到 " . count($bsonFiles) . " 个 BSON 文件");
$output->writeln("还原目录: {$dbRestoreDir}");
$output->writeln("备份数据库: {$backupDbName} -> 目标数据库: {$database}");
$cmd = $this->getMongoRestoreCommand($host, $port, $database, $dbRestoreDir, $useDocker, $dockerContainerName, $username, $password, $authSource);
$output->writeln("执行命令: {$cmd}");
exec($cmd, $outputLines, $returnCode);
if ($returnCode === 0) {
$output->writeln("✅ MongoDB 还原成功");
if (!empty($outputLines)) {
$output->writeln(implode("\n", $outputLines));
}
} else {
$output->writeln("❌ MongoDB 还原失败");
$output->writeln(implode("\n", $outputLines));
}
exec("rm -rf {$tempDir}");
} else {
$output->writeln("❌ 解压备份文件失败");
$output->writeln(implode("\n", $unzipOutput));
exec("rm -rf {$tempDir}");
}
} catch (\Exception $e) {
$output->writeln("❌ MongoDB 还原失败: " . $e->getMessage());
}
}
private function backupMySQL($backupDir, $output, $dataSource = null): void
{
$name = $dataSource['name'] ?? $dataSource['dockerContainerName'] ?? ucfirst($dataSource['type']);
$output->writeln("\n开始备份 {$name}...");
try {
$mysqlSource = $dataSource;
if (!$mysqlSource) {
foreach ($this->dataSources as $source) {
if ($source['type'] === 'mysql') {
$mysqlSource = $source;
break;
}
}
}
if (!$mysqlSource) {
$output->writeln("❌ 未找到 MySQL 数据源配置");
return;
}
$host = $mysqlSource['host'];
$port = $mysqlSource['port'];
$database = $mysqlSource['database'];
$username = $mysqlSource['username'];
$password = $mysqlSource['password'];
$useDocker = $mysqlSource['useDocker'];
$dockerContainerName = $mysqlSource['dockerContainerName'];
// 生成备份文件名
$backupFileName = "{$database}_" . date("Y_m_d_H_i_s") . ".sql";
$backupFilePath = "{$backupDir}/{$backupFileName}";
// 构建备份命令
$cmd = $this->getMySqlDumpCommand($host, $port, $database, $username, $password, $backupFilePath, $useDocker, $dockerContainerName);
$output->writeln("执行命令: {$cmd}");
// 执行备份命令
exec($cmd, $outputLines, $returnCode);
if ($returnCode === 0) {
$output->writeln("✅ MySQL 备份成功: {$backupFilePath}");
} else {
$output->writeln("❌ MySQL 备份失败");
$output->writeln(implode("\n", $outputLines));
}
} catch (\Exception $e) {
$output->writeln("❌ MySQL 备份失败: " . $e->getMessage());
}
}
private function restoreMySQL($backupDir, $output, $dataSource = null): void
{
$name = $dataSource['name'] ?? $dataSource['dockerContainerName'] ?? ucfirst($dataSource['type']);
$output->writeln("\n开始还原 {$name}...");
try {
$mysqlSource = $dataSource;
if (!$mysqlSource) {
foreach ($this->dataSources as $source) {
if ($source['type'] === 'mysql') {
$mysqlSource = $source;
break;
}
}
}
if (!$mysqlSource) {
$output->writeln("❌ 未找到 MySQL 数据源配置");
return;
}
$host = $mysqlSource['host'];
$port = $mysqlSource['port'];
$database = $mysqlSource['database'];
$username = $mysqlSource['username'];
$password = $mysqlSource['password'];
$useDocker = $mysqlSource['useDocker'];
$dockerContainerName = $mysqlSource['dockerContainerName'];
// 列出备份文件(支持 SQL 文件和 zip 文件)
$backupFiles = array_merge(
glob("{$backupDir}/*.sql"),
glob("{$backupDir}/*.zip")
);
if (empty($backupFiles)) {
$output->writeln("❌ 未找到备份文件");
return;
}
// 按修改时间排序
usort($backupFiles, function ($a, $b) {
return filemtime($b) - filemtime($a);
});
// 显示备份文件列表
$output->writeln("\n可用的备份文件:");
foreach ($backupFiles as $index => $file) {
$fileName = basename($file);
$fileSize = filesize($file) / 1024 / 1024;
$modTime = date("Y-m-d H:i:s", filemtime($file));
$output->writeln(($index + 1) . ". {$fileName} (" . round($fileSize, 2) . " MB, {$modTime})");
}
// 选择备份文件
$output->write("\n请选择要还原的备份文件 (1-" . count($backupFiles) . "): ");
$handle = fopen("php://stdin", "r");
$choice = fgets($handle);
fclose($handle);
$choice = trim($choice);
if (!is_numeric($choice) || $choice < 1 || $choice > count($backupFiles)) {
$output->writeln("\n无效选择");
return;
}
$selectedFile = $backupFiles[$choice - 1];
$output->writeln("\n选择的备份文件: " . basename($selectedFile));
$sqlFile = $selectedFile;
// 如果是 zip 文件,需要解压
if (pathinfo($selectedFile, PATHINFO_EXTENSION) === 'zip') {
// 生成临时还原目录
$tempDir = "/tmp/mysql_restore_" . uniqid();
if (!is_dir($tempDir)) {
mkdir($tempDir, 0755, true);
}
// 解压备份文件
$unzipCmd = "unzip {$selectedFile} -d {$tempDir}";
$output->writeln("解压备份文件...");
exec($unzipCmd, $unzipOutput, $unzipReturnCode);
if ($unzipReturnCode !== 0) {
$output->writeln("❌ 解压备份文件失败");
$output->writeln(implode("\n", $unzipOutput));
// 清理临时目录
exec("rm -rf {$tempDir}");
return;
}
// 找到解压后的 SQL 文件
$sqlFiles = glob("{$tempDir}/*.sql");
if (empty($sqlFiles)) {
$output->writeln("❌ 未找到 SQL 文件");
// 清理临时目录
exec("rm -rf {$tempDir}");
return;
}
$sqlFile = $sqlFiles[0];
}
// 构建还原命令
$cmd = $this->getMySqlRestoreCommand($host, $port, $database, $username, $password, $sqlFile, $useDocker, $dockerContainerName);
$output->writeln("执行命令: {$cmd}");
// 执行还原命令
exec($cmd, $outputLines, $returnCode);
if ($returnCode === 0) {
$output->writeln("✅ MySQL 还原成功");
} else {
$output->writeln("❌ MySQL 还原失败");
$output->writeln(implode("\n", $outputLines));
}
// 清理临时目录(如果使用了临时目录)
if (pathinfo($selectedFile, PATHINFO_EXTENSION) === 'zip') {
exec("rm -rf {$tempDir}");
}
} catch (\Exception $e) {
$output->writeln("❌ MySQL 还原失败: " . $e->getMessage());
}
}
private function clearRedis($output): void
{
$output->writeln("\n开始清空 Redis...");
try {
// 从数据源配置中获取 Redis 配置
$redisSource = null;
foreach ($this->dataSources as $source) {
if ($source['type'] === 'redis') {
$redisSource = $source;
break;
}
}
if (!$redisSource) {
$output->writeln("❌ 未找到 Redis 数据源配置");
return;
}
if ($redisSource['useDocker']) {
$redisContainer = $redisSource['dockerContainerName'];
if (!$redisContainer) {
$output->writeln("❌ 未指定 Redis Docker 容器名称");
return;
}
$output->writeln("使用 Docker 容器清空 Redis");
$cmd = "docker exec -it {$redisContainer} redis-cli flushall";
$output->writeln("执行命令: {$cmd}");
exec($cmd, $outputLines, $returnCode);
if ($returnCode === 0) {
$output->writeln("✅ Redis 清空成功");
} else {
$output->writeln("❌ Redis 清空失败");
$output->writeln(implode("\n", $outputLines));
}
} else {
$redis = new \Redis();
$host = $redisSource['host'] ?? '127.0.0.1';
$port = $redisSource['port'] ?? 6379;
$password = $redisSource['password'] ?? '';
$output->writeln("连接 Redis: {$host}:{$port}");
if ($redis->connect($host, $port)) {
if (!empty($password)) {
$redis->auth($password);
}
$result = $redis->flushAll();
if ($result) {
$output->writeln("✅ Redis 清空成功");
} else {
$output->writeln("❌ Redis 清空失败");
}
} else {
$output->writeln("❌ 无法连接到 Redis");
}
}
} catch (\Exception $e) {
$output->writeln("❌ Redis 操作失败: " . $e->getMessage());
}
}
private function getMongoDumpCommand($host, $port, $database, $outputDir, $useDocker = false, $dockerContainerName = null, $username = '', $password = '', $authSource = null): string
{
if ($authSource === null) {
$authSource = $database;
}
$authParams = '';
if (!empty($username) && !empty($password)) {
$authParams = "--username {$username} --password {$password} --authenticationDatabase {$authSource}";
}
if ($useDocker) {
if (!$dockerContainerName) {
return "echo '错误:未指定 Docker 容器名称' && exit 1";
}
$port = 27017;
return "docker exec -it {$dockerContainerName} mongodump --host {$host}:{$port} {$authParams} --db {$database} --out /tmp/mongo_backup && docker cp {$dockerContainerName}:/tmp/mongo_backup/{$database} {$outputDir}/";
} else {
return "mongodump --host {$host}:{$port} {$authParams} --db {$database} --out {$outputDir}";
}
}
private function getMongoRestoreCommand($host, $port, $database, $restoreDir, $useDocker = false, $dockerContainerName = null, $username = '', $password = '', $authSource = null): string
{
if ($authSource === null) {
$authSource = $database;
}
$authParams = '';
if (!empty($username) && !empty($password)) {
$authParams = "--username {$username} --password {$password} --authenticationDatabase {$authSource}";
}
if ($useDocker) {
if (!$dockerContainerName) {
return "echo '错误:未指定 Docker 容器名称' && exit 1";
}
$dirName = basename($restoreDir);
return "docker cp {$restoreDir} {$dockerContainerName}:/tmp/ && docker exec -it {$dockerContainerName} mongorestore --host {$host}:{$port} {$authParams} --db {$database} /tmp/{$dirName} && docker exec -it {$dockerContainerName} rm -rf /tmp/{$dirName}";
} else {
return "mongorestore --host {$host}:{$port} {$authParams} --db {$database} {$restoreDir}";
}
}
private function getMySqlDumpCommand($host, $port, $database, $username, $password, $outputFile, $useDocker = false, $dockerContainerName = null): string
{
if ($useDocker) {
// 使用 Docker 容器
if (!$dockerContainerName) {
return "echo '错误:未指定 Docker 容器名称' && exit 1";
}
return "docker exec -it {$dockerContainerName} mysqldump -h {$host} -P {$port} -u {$username} --password={$password} {$database} > {$outputFile}";
} else {
// 不使用 Docker,直接使用本地命令
return "mysqldump -h {$host} -P {$port} -u {$username} --password={$password} {$database} > {$outputFile}";
}
}
private function getMySqlRestoreCommand($host, $port, $database, $username, $password, $sqlFile, $useDocker = false, $dockerContainerName = null): string
{
if ($useDocker) {
// 使用 Docker 容器
if (!$dockerContainerName) {
return "echo '错误:未指定 Docker 容器名称' && exit 1";
}
return "docker cp {$sqlFile} {$dockerContainerName}:/tmp/mysql_restore.sql && docker exec -it {$dockerContainerName} bash -c 'mysql -h {$host} -P {$port} -u {$username} --password={$password} {$database} < /tmp/mysql_restore.sql'";
} else {
// 不使用 Docker,直接使用本地命令
return "mysql -h {$host} -P {$port} -u {$username} --password={$password} {$database} < {$sqlFile}";
}
}
}
+47
View File
@@ -0,0 +1,47 @@
<?php
namespace app\command;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Output\OutputInterface;
use app\model\BalanceLog;
class BalanceLogTask extends Command
{
protected static $defaultName = 'balance:log-task';
protected static $defaultDescription = 'balance:log-task';
protected function configure()
{
}
protected function execute(InputInterface $input, OutputInterface $output):int
{
// 1. 确保索引存在
$output->writeln('Creating indexes...');
$indexResults = BalanceLog::createAllIndexes();
foreach ($indexResults as $currency => $messages) {
$output->writeln("[$currency]");
foreach ($messages as $message) {
$output->writeln(" - $message");
}
}
// 2. 执行数据归档
$output->writeln('Archiving old data...');
$archiveResults = BalanceLog::archiveData(3); // 归档3天前的数据
foreach ($archiveResults as $currency => $result) {
$output->writeln("[$currency]");
$output->writeln(" - Table: {$result['table']}");
$output->writeln(" - Archived: {$result['archived']} records");
foreach ($result['messages'] as $message) {
$output->writeln(" - $message");
}
}
$output->writeln('All tasks completed!');
return self::SUCCESS;
}
}
+60
View File
@@ -0,0 +1,60 @@
<?php
namespace app\command;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class CheckConversation extends Command
{
protected static $defaultName = 'check-conversation';
protected static $defaultDescription = '检查 conversation 记录';
protected function execute(InputInterface $input, OutputInterface $output): int
{
$conversationId = 'sg_2639473367';
$ownerUserId = '83484627';
cp("=== 检查 conversation 表记录 ===");
$convModel = new \app\model\Openim\Conversation();
$conv = $convModel->where('conversation_id', $conversationId)
->where('owner_user_id', $ownerUserId)
->find();
if ($conv) {
cp("找到记录:");
print_r($conv->toArray());
cp("");
if (isset($conv['max_seq'])) {
cp("max_seq: " . $conv['max_seq']);
} else {
cp("max_seq 不存在");
}
if (isset($conv['min_seq'])) {
cp("min_seq: " . $conv['min_seq']);
} else {
cp("min_seq 不存在");
}
} else {
cp("未找到记录");
cp("\n查找同一 conversation_id 的其他记录:");
$allConvs = $convModel->where('conversation_id', $conversationId)->select();
foreach ($allConvs as $c) {
cp("owner_user_id: " . ($c['owner_user_id'] ?? 'null'));
cp(" max_seq: " . ($c['max_seq'] ?? 'null'));
cp(" min_seq: " . ($c['min_seq'] ?? 'null'));
}
}
cp("\n=== 检查 seq 表 ===");
$seqModel = new \app\model\Openim\Seq();
$seq = $seqModel->where('conversation_id', $conversationId)->find();
if ($seq) {
cp("conversation_id: " . $seq['conversation_id']);
cp("max_seq: " . $seq['max_seq']);
cp("min_seq: " . $seq['min_seq']);
}
return 0;
}
}
+60
View File
@@ -0,0 +1,60 @@
<?php
namespace app\command;
use Exception;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use think\db\PDOConnection;
use support\think\Db;
class Clear extends Command
{
protected static $defaultName = 'clear';
protected static $defaultDescription = '数据库缓存';
protected function configure()
{
$this->setDescription('clear database.');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$action = 'all';
if($action == 'all'){
Db::name('address')->where('id','>',0)->delete();
Db::name('recharge')->where('id','>',0)->delete();
Db::name('transfer')->where('id','>',0)->delete();
Db::name('user')->where('id','>',0)->delete();
Db::name('user_extend')->where('user_id','>',0)->delete();
Db::name('user_team')->where('descendant_id|ancestor_id','>',0)->delete();
Db::name('withdrawl')->where('id','>',0)->delete();
Db::name('work_record')->where('id','>',0)->delete();
}else{
$list = \app\model\User::order('id','asc')->select();
foreach($list as $k=>$user){
Db::name('address')->where('user_id',$user->id)->delete();
Db::name('transfer')->where('user_id',$user->id)->delete();
Db::name('recharge')->where('user_id',$user->id)->delete();
Db::name('record')->where('user_id',$user->id)->delete();
Db::name('withdrawl')->where('user_id',$user->id)->delete();
Db::name('user_extend')->where('user_id',$user->id)->delete();
Db::name('user_team')->where('descendant_id|ancestor_id','=',$user->id)->delete();
Db::name('withdrawl')->where('user_id',$user->id)->delete();
Db::name('work_record')->where('user_id',$user->id)->delete();
Db::name('user')->where('id',$user->id)->delete();
}
}
$output->writeln('<info>Succeed!</info>');
return self::SUCCESS;
}
protected function buildModelSchema(string $class): void
{
}
}
+261
View File
@@ -0,0 +1,261 @@
<?php
namespace app\command;
use Exception;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Output\OutputInterface;
use support\think\Db;
use app\model\User as UserModel;
use \think\db\PDOConnection;
use think\db\exception\PDOException;
use think\db\exception\InvalidArgumentException;
class Database extends Command
{
protected static $defaultName = 'Db';
protected static $defaultDescription = 'Database 优化';
/**
* @return void
*/
protected function configure()
{
$this->addOption('action','a', InputOption::VALUE_OPTIONAL, '要做什么操作');
$this->addOption('table','t', InputOption::VALUE_OPTIONAL, '表名');
$this->addOption('domain','ym', InputOption::VALUE_OPTIONAL, 'domain');
$this->addOption('connection','c', InputOption::VALUE_OPTIONAL, '数据库链接名,默认mysql','mysql');
$this->addOption('dir','d', InputOption::VALUE_OPTIONAL, '缓存目录');
$this->addOption('robot_id','rid', InputOption::VALUE_OPTIONAL, 'robot_id');
}
/**
* @param InputInterface $input
* @param OutputInterface $output
* @return int
*/
protected function execute(InputInterface $input, OutputInterface $output): int
{
$action = $input->getOption('action');
if(method_exists($this, $action)){
return $this->$action($input, $output);
}
cp('操作不存在:'.$action);
return 0;
}
function optimize_schema(InputInterface $input, OutputInterface $output)
{
$table = $input->getOption('table');
try {
if ($table) {
$this->cacheTable($table, $input->getOption('connection'));
} else {
$dirs = ((array) $input->getOption('dir')) ?: $this->getDefaultDirs();
foreach ($dirs as $dir) {
$this->cacheModel($dir);
}
}
} catch (\Exception $e) {
$output->write($e->getMessage());
return self::FAILURE;
}
$output->write('Succeed!');
return self::SUCCESS;
}
function prototype(InputInterface $input, OutputInterface $output){
$table = $input->getOption('table');
// 获取表前缀并构建完整表名
$prefix = config('thinkorm.connections.mysql.prefix', '');
$fullTableName = '`' . $prefix . $table . '`';
// 查询表结构
$res = Db::query('SHOW FULL COLUMNS FROM ' . $fullTableName);
if (empty($res)) {
return "// 表 {$table} 不存在或没有字段";
}
$annotations = [];
$annotations[] = '/**';
foreach ($res as $row) {
$field = $row['Field'];
$type = $row['Type'];
$comment = $row['Comment'] ?: '无注释';
// 处理字段类型映射
$phpType = $this->mapMysqlTypeToPhp($type);
// 处理特殊字段
if ($field === 'id') {
$annotations[] = " * @property integer \${$field} 主键(ID) - {$comment}";
} else {
$annotations[] = " * @property {$phpType} \${$field} {$comment}";
}
}
$annotations[] = ' */';
cp( implode("\n", $annotations));
return self::SUCCESS;
}
/**
* 将MySQL字段类型映射到PHP类型
*
* @param string $mysqlType MySQL字段类型
* @return string PHP类型
*/
protected function mapMysqlTypeToPhp($mysqlType)
{
$mysqlType = strtolower($mysqlType);
// 整数类型
if (preg_match('/^(tinyint|smallint|mediumint|int|bigint)/', $mysqlType)) {
// 检查是否为无符号
if (strpos($mysqlType, 'unsigned') !== false) {
return 'integer'; // 无符号整数也返回integer
}
return 'integer';
}
// 浮点类型
if (preg_match('/^(float|double|decimal)/', $mysqlType)) {
return 'float';
}
// 字符串类型
if (preg_match('/^(varchar|char|text|tinytext|mediumtext|longtext|enum|set)/', $mysqlType)) {
return 'string';
}
// 日期时间类型
if (preg_match('/^(date|time|datetime|timestamp|year)/', $mysqlType)) {
return 'string'; // 或者可以返回 '\\DateTime' 如果需要更精确的类型
}
// 二进制类型
if (preg_match('/^(blob|tinyblob|mediumblob|longblob|binary|varbinary)/', $mysqlType)) {
return 'string'; // 或者根据需求返回其他类型
}
// JSON类型
if (strpos($mysqlType, 'json') !== false) {
return 'array'; // 或者 'mixed'
}
// 布尔类型(tinyint(1)通常用作布尔值)
if ($mysqlType === 'tinyint(1)' || $mysqlType === 'boolean' || $mysqlType === 'bool') {
return 'boolean';
}
// 默认返回混合类型
return 'mixed';
}
protected function buildModelSchema(string $class): void
{
$reflect = new \ReflectionClass($class);
if ($reflect->isAbstract() || ! $reflect->isSubclassOf('\think\Model')) {
return;
}
try {
/** @var \think\Model $model */
$model = new $class;
$connection = $model->db()->getConnection();
if ($connection instanceof PDOConnection) {
$table = $model->getTable();
//预读字段信息
$connection->getSchemaInfo($table, true);
}
} catch (Exception $e) {
}
}
protected function buildDataBaseSchema(PDOConnection $connection, array $tables, string $dbName): void
{
foreach ($tables as $table) {
//预读字段信息
$connection->getSchemaInfo("{$dbName}.{$table}", true);
}
}
/**
* 缓存表
*/
private function cacheTable(string $table, ?string $connectionName = null): void
{
$connection = Db::connect($connectionName);
if (! $connection instanceof PDOConnection) {
throw new Exception('only PDO connection support schema cache!');
}
if (str_contains($table, '.')) {
[$dbName, $table] = explode('.', $table);
} else {
$dbName = $connection->getConfig('database');
}
if ($table == '*') {
$table = $connection->getTables($dbName);
}
$this->buildDataBaseSchema($connection, (array) $table, $dbName);
}
/**
* 缓存模型
*/
private function cacheModel(?string $dir = null): void
{
if ($dir) {
$modelDir = app_path('model') . $dir . DIRECTORY_SEPARATOR;
$namespace = 'app\\' . $dir;
} else {
$modelDir = app_path('model').DIRECTORY_SEPARATOR;
$namespace = 'app';
}
if (! is_dir($modelDir)) {
throw new InvalidArgumentException("{$modelDir} directory does not exist");
}
/** @var \SplFileInfo[] $iterator */
$iterator = new \RecursiveIteratorIterator(
new \RecursiveDirectoryIterator($modelDir, \RecursiveDirectoryIterator::SKIP_DOTS),
\RecursiveIteratorIterator::SELF_FIRST
);
foreach ($iterator as $fileInfo) {
$relativePath = substr($fileInfo->getRealPath(), strlen($modelDir));
if (! str_ends_with($relativePath, '.php')) {
continue;
}
// 去除 .php
$relativePath = substr($relativePath, 0, -4);
$class = '\\' . $namespace . '\\model\\' . str_replace('/', '\\', $relativePath);
if (! class_exists($class)) {
continue;
}
$this->buildModelSchema($class);
}
}
/**
* 获取默认目录名
* @return array<int, ?string>
*/
private function getDefaultDirs(): array
{
// 包含默认的模型目录
$dirs = [null];
return $dirs;
}
}
+273
View File
@@ -0,0 +1,273 @@
<?php
namespace app\command;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
class FixOpenimSeq extends Command
{
protected static $defaultName = 'fix:openim:seq';
protected static $defaultDescription = '修复 OpenIM MongoDB seq 相关字段';
private $conversationSeqMap = [];
protected function configure(): void
{
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$this->log("╔════════════════════════════════════════════════════════════╗");
$this->log("║ 开始修复 OpenIM Seq 数据 ║");
$this->log("╚════════════════════════════════════════════════════════════╝");
$this->log("");
// 分析数据(获取会话 seq 信息)
$this->analyzeMsgSeq();
// 执行修复
$this->fixAll();
return 0;
}
private function getConversationId(array $msg): ?string
{
$sessionType = $msg['session_type'] ?? null;
$sendId = $msg['send_id'] ?? '';
$recvId = $msg['recv_id'] ?? '';
$groupId = $msg['group_id'] ?? '';
if ($sessionType === 3 || !empty($groupId)) {
return 'sg_' . $groupId;
}
if ($sessionType === 1 || ($sendId && $recvId)) {
$ids = [(int)$sendId, (int)$recvId];
sort($ids);
return 'si_' . $ids[0] . '_' . $ids[1];
}
return null;
}
private function analyzeMsgSeq(): void
{
$msgCount = \app\model\Openim\Msg::count();
$this->log("分析消息数据...");
$this->log("消息文档数: {$msgCount}");
$processedDocs = 0;
$totalMsgs = 0;
$msgs = \app\model\Openim\Msg::select();
foreach ($msgs as $doc) {
$processedDocs++;
$msgsArray = $doc['msgs'];
if ($msgsArray instanceof \think\model\Collection) {
$msgsArray = $msgsArray->toArray();
}
if (!empty($msgsArray) && is_array($msgsArray)) {
foreach ($msgsArray as $msgItem) {
if (isset($msgItem['msg'])) {
$msg = $msgItem['msg'];
$conversationId = $this->getConversationId($msg);
$seq = $msg['seq'] ?? null;
if ($conversationId && $seq !== null) {
$totalMsgs++;
if (!isset($this->conversationSeqMap[$conversationId])) {
$this->conversationSeqMap[$conversationId] = [
'min_seq' => $seq,
'max_seq' => $seq,
'count' => 0
];
}
$this->conversationSeqMap[$conversationId]['min_seq'] = min($this->conversationSeqMap[$conversationId]['min_seq'], $seq);
$this->conversationSeqMap[$conversationId]['max_seq'] = max($this->conversationSeqMap[$conversationId]['max_seq'], $seq);
$this->conversationSeqMap[$conversationId]['count']++;
}
}
}
}
if ($processedDocs % 100 == 0) {
$this->log("已处理 {$processedDocs}/{$msgCount} 个文档...");
}
}
$this->log("处理完成!共处理 {$processedDocs} 个文档,{$totalMsgs} 条消息");
$this->log("发现 " . count($this->conversationSeqMap) . " 个会话");
$this->log("");
}
private function fixAll(): void
{
$this->log("开始修复数据...");
$this->log("");
if (empty($this->conversationSeqMap)) {
$this->log("错误:缺少会话 seq 数据");
return;
}
$seqFixed = 0;
$seqCreated = 0;
$seqUserFixed = 0;
$conversationFixed = 0;
foreach ($this->conversationSeqMap as $conversationId => $seqInfo) {
// 修复 seq 表
$existing = \app\model\Openim\Seq::where('conversation_id', $conversationId)->find();
if ($existing) {
$max_seq = 0;
if(str_starts_with($conversationId,'sg_')){
$max_seq = ceil($seqInfo['max_seq']/100)*100+1;
}else{
$max_seq = ceil($seqInfo['max_seq']/50)*50+1;
}
$existing->max_seq = $max_seq;
$existing->min_seq = 0;
$existing->save();
$seqFixed++;
} else {
}
// 修复 seq_user 表
\app\model\Openim\SeqUser::where('conversation_id', $conversationId)->update([
'max_seq' => 0,
'min_seq' => 0,
]);
\app\model\Openim\SeqUser::where('conversation_id', $conversationId)
->where('read_seq','<>',$seqInfo['max_seq'])
->update([
'read_seq' => $seqInfo['max_seq'],
]);
// 修复 conversation 表
\app\model\Openim\Conversation::where('min_seq', '>',0)->update([
'max_seq' => 0,
'min_seq' => 0,
]);
}
$this->log("修复完成!");
$this->log("- seq 表: 更新 {$seqFixed} 条,新建 {$seqCreated}");
$this->log("- seq_user 表: 更新 {$seqUserFixed}");
$this->log("- conversation 表: 更新 {$conversationFixed}");
$this->log("");
}
private function log(string $message): void
{
echo $message . "\n";
}
/**
* 重置 seq 相关字段并重新计算
*/
public function fix_seq(): void
{
$this->log("\n═══════════════════════════════════════════════════════════");
$this->log(" 执行 fix_seq 方法 ");
$this->log("═══════════════════════════════════════════════════════════");
// 1. 获取所有会话ID
$conversationIds = [];
// 从 seq 表获取所有会话ID
$seqRecords = \app\model\Openim\Seq::field('conversation_id')->select()->toArray();
foreach ($seqRecords as $record) {
$conversationIds[] = $record['conversation_id'];
}
// 去重
$conversationIds = array_unique($conversationIds);
$totalConversations = count($conversationIds);
$this->log("发现 {$totalConversations} 个会话");
$processed = 0;
foreach ($conversationIds as $conversationId) {
cp('更新:'.$conversationId);
continue;
$processed++;
$this->log("\n处理会话 {$conversationId} ({$processed}/{$totalConversations})");
// 2. 计算变量A
$msgCount = \app\model\Openim\Msg::whereLike('doc_id', "{$conversationId}%")->count();
$multiplier = strpos($conversationId, 'sg_') === 0 ? 100 : 50;
$baseA = $msgCount * $multiplier + 1;
// 确保 A 是 1, 51, 101 等递增格式
$remainder = $baseA % $multiplier;
if ($remainder != 1) {
$baseA = $baseA - $remainder + 1;
}
// 3. 获取最后一条消息的 seq
$lastSeq = 0;
$msgDocs = \app\model\Openim\Msg::whereLike('doc_id', "{$conversationId}%")->select();
foreach ($msgDocs as $doc) {
$msgsArray = $doc['msgs'];
if ($msgsArray instanceof \think\model\Collection) {
$msgsArray = $msgsArray->toArray();
}
if (!empty($msgsArray) && is_array($msgsArray)) {
foreach ($msgsArray as $msgItem) {
if (isset($msgItem['msg']['seq'])) {
$lastSeq = max($lastSeq, (int)$msgItem['msg']['seq']);
}
}
}
}
// 确保 A 大于最后一条消息的 seq
if ($baseA <= $lastSeq) {
$baseA = $lastSeq + 50 - ($lastSeq % 50) + 1;
if ($baseA % 50 != 1) {
$baseA += 1;
}
}
$this->log(" - 消息记录数: {$msgCount}");
$this->log(" - 乘数: {$multiplier}");
$this->log(" - 最后消息 seq: {$lastSeq}");
$this->log(" - 计算变量 A: {$baseA}");
// 4. 更新 seq 表
$seq = \app\model\Openim\Seq::where('conversation_id', $conversationId)->find();
if ($seq) {
$seq->max_seq = $baseA;
$seq->min_seq = 0;
$seq->save();
$this->log(" - 更新 seq 表: max_seq={$baseA}, min_seq=0");
}
// 5. 更新 conversation 表
$conversations = \app\model\Openim\Conversation::where('conversation_id', $conversationId)->select();
foreach ($conversations as $conversation) {
$conversation->max_seq = 0;
$conversation->min_seq = 0;
$conversation->save();
}
$this->log(" - 更新 conversation 表: max_seq=0, min_seq=0");
// 6. 更新 seq_user 表
$seqUsers = \app\model\Openim\SeqUser::where('conversation_id', $conversationId)->select();
foreach ($seqUsers as $seqUser) {
cp('更新:'.$conversationId);
$seqUser->max_seq = 0;
$seqUser->min_seq = 0;
$seqUser->read_seq = $lastSeq;
$seqUser->save();
}
$this->log(" - 更新 seq_user 表: max_seq=0, min_seq=0, read_seq={$lastSeq}");
}
$this->log("\n═══════════════════════════════════════════════════════════");
$this->log(" fix_seq 方法执行完成 ");
$this->log("═══════════════════════════════════════════════════════════");
}
}
+101
View File
@@ -0,0 +1,101 @@
<?php
namespace app\command;
use Exception;
use plugin\admin\app\model\Config;
use support\Request;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Output\OutputInterface;
use support\think\Db;
class Language extends Command
{
protected static $defaultName = 'Language:scan';
protected static $defaultDescription = '自动完成多语言的文件提取';
/**
* @return void
*/
protected function configure()
{
$this->addOption('file','f', InputArgument::OPTIONAL, '只是针对那个文件');
}
/**
* @param InputInterface $input
* @param OutputInterface $output
* @return int
*/
protected function execute(InputInterface $input, OutputInterface $output): int
{
$file = $input->getOption('file');
$file = base_path()."/app/api/controller/ServerController.php";
$dir = new \RecursiveDirectoryIterator(base_path().'/app');
$iterator = new \RecursiveIteratorIterator($dir);
$phpFiles = new \RegexIterator($iterator, '/^.+\.php$/i', \RecursiveRegexIterator::GET_MATCH);
$fnlist = [];
$result = [
'common.php'=>""
];
foreach ($phpFiles as $_file) {
$_file = $_file[0];
$fnlist[] = $_file;
$key = 'common.php';
if(false !==strpos($_file, base_path().'/app/api/controller')){
$key = 'api/'.pathinfo($_file,PATHINFO_BASENAME);
}
if(false !==strpos($_file, base_path().'/app/controller')){
$key = pathinfo($_file,PATHINFO_BASENAME);
}
$key = strtolower(str_replace('Controller','',$key));
//cp($key);
$res = $this->parseOneFile($_file);
$result[$key]=$res;
}
//$res = $this->parseOneFile($file);
//cp($result);
$this->write2file($result);
return 0;
}
function write2file($data=[]){
$langs = ['zh-Hans','en'];
foreach($data as $fn=>$arr){
foreach($langs as $lang){
$lang_path = base_path('/resource/translations/'.$lang.'/');
$_common_arr = require($lang_path.'common.php');
$_arr = [];
if(file_exists($lang_path.$fn)){
$_arr = require($lang_path.$fn);
}
foreach($arr as $ov){
if(!isset($_common_arr[$ov]) && !isset($_arr[$ov])){
$_arr[$ov]=$ov;
}
if(isset($_common_arr[$ov]) && isset($_arr[$ov])){
unset($_arr[$ov]);
}
}
file_put_contents($lang_path.$fn,'<?php'.PHP_EOL.'return '.var_export($_arr,true).';');
//cp('写入文件:'.$lang_path.$fn);
}
}
}
function parseOneFile($fn){
cp('解析文件:',$fn);
if(file_exists($fn)){
$content = file_get_contents($fn);
$matchs = [];
preg_match_all('/__\(([\'"])(.*?)\1\s*(?:,\s*\[.*?\])?\)/',$content,$matchs);
//cp($matchs[2]);
return $matchs[2];
}else{
cp('文件不存在:'.$fn);
}
}
}
+872
View File
@@ -0,0 +1,872 @@
<?php
namespace app\command;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
// 引入内置进度条类
use Symfony\Component\Console\Helper\ProgressBar;
use support\think\Db;
class MigrateMessages extends Command
{
protected static $defaultName = 'migrate:messages';
protected static $defaultDescription = '从老数据库迁移数据到新OpenIM数据库';
private $sdk = null;
private $oldManager = null;
private $newManager = null;
private $retry = 3;
private $delay = 2;
private $backupDir = '/vol3/1000/code/im/admin/backup';
private $currentBackup = null;
private $skipUsers = [];
private $skipGroups = [];
private $stats = [
'users' => ['total' => 0, 'success' => 0, 'failed' => 0],
'groups' => ['total' => 0, 'success' => 0, 'failed' => 0],
'members' => ['total' => 0, 'success' => 0, 'failed' => 0],
'messages' => ['total' => 0, 'success' => 0, 'failed' => 0, 'skipped' => 0],
];
protected function configure(): void
{
$this->addOption('step', 's', InputOption::VALUE_OPTIONAL, '执行步骤: users/groups/members/messages/all', 'all');
$this->addOption('skip-users', null, InputOption::VALUE_OPTIONAL, '跳过的用户ID(逗号分隔)');
$this->addOption('skip-groups', null, InputOption::VALUE_OPTIONAL, '跳过的群ID(逗号分隔)');
$this->addOption('clean', null, InputOption::VALUE_NONE, '清空现有数据后再迁移');
$this->addOption('retry', 'r', InputOption::VALUE_OPTIONAL, '失败重试次数', 3);
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$step = $input->getOption('step');
$skipUsers = $input->getOption('skip-users') ? explode(',', $input->getOption('skip-users')) : [];
$skipGroups = $input->getOption('skip-groups') ? explode(',', $input->getOption('skip-groups')) : [];
$clean = $input->getOption('clean');
$retry = (int)$input->getOption('retry');
// 自动忽略特殊用户
$defaultSkipUsers = ['group_bot', 'official_team', 'system','imAdmin'];
$skipUsers = array_merge($skipUsers, $defaultSkipUsers);
$this->skipUsers = array_unique($skipUsers);
$this->skipGroups = array_unique($skipGroups);
$this->retry = $retry;
$this->log($output, "╔════════════════════════════════════════════════════════════╗");
$this->log($output, "║ OpenIM 数据迁移工具 v2.0 ║");
$this->log($output, "╚════════════════════════════════════════════════════════════╝");
$this->log($output, "");
if ($clean) {
$this->log($output, "🗑️ 清理模式:会清空现有数据");
}
$this->log($output, "📍 执行步骤: {$step}");
$this->log($output, "");
if($step == 'restore'){
$this->restoreMongoDB($output, '/vol3/1000/code/im/admin/backup/openim_v3_groups_20260413141105.json');
return 0;
}
$this->cleanExistingData($output,[]);
try {
$this->initConnections($output);
//return 0 ;
if ($clean) {
$this->cleanExistingData($output,[
'conversation', 'conversation_version', // 会话相关集合
'data_version', // 数据版本集合
'friend', 'friend_request', 'friend_version', // 好友关系相关集合
'group', 'group_join_version','group_member','group_member_version','group_request', // 群组相关集合
'msg','seq','seq_user' // 消息和序列号相关集合
]);
return 0;
}
cache('admin_token_imAdmin',null);
$steps = $step === 'all' ? [
'users',
'friends',
'groups',
//'members',
'messages'
] : [$step];
foreach ($steps as $s) {
// 备份数据
$backupFile = $this->backupMongoDB($output, $s);
try {
switch ($s) {
case 'users':
$this->migrateUsers($output);
break;
case 'friends':
$this->migrateFriends($output);
break;
case 'groups':
$this->migrateGroups($output);
break;
case 'members':
$this->migrateGroupMembers($output);
break;
case 'messages':
$this->migrateMessages($output);
break;
}
} catch (\Exception $e) {
// 遇到错误,回滚数据
if (!empty($backupFile)) {
$this->restoreMongoDB($output, $backupFile);
}
throw $e;
}
}
$this->printStats($output);
return self::SUCCESS;
} catch (\Exception $e) {
$this->log($output, "❌ 错误: " . $e->getMessage());
//$this->log($output, $e->getTraceAsString());
return self::FAILURE;
}
}
private function migrateUsers(OutputInterface $output): void
{
//之前残留了一部分数据,是单向好友,这里没做删除,所以数据大小和之前的不一样,用户重新删除一次就好了
$this->log($output, "");
$this->log($output, "═════════════════ 步骤1: 迁移用户 ═════════════════");
$this->log($output, "清理旧的数据");
$this->cleanExistingData($output,[
'user'
]);
$user_list = (new \app\model\Openim\User())->setOption('connection','tettt')
->whereNotIn('user_id',$this->skipUsers)
->field('user_id,nickname,face_url')
->select();
$user_list = $user_list->toArray();
// 1. 创建进度条(内置核心方法)
$progressBar = new ProgressBar($output, count($user_list));
// 可选:设置进度条样式(字符、长度等)
$progressBar->setBarCharacter('█');
$progressBar->setEmptyBarCharacter('░');
$progressBar->setProgressCharacter('▶');
$progressBar->setBarWidth(400);
// 2. 开始显示
$progressBar->start();
echo sprintf("\r");
while(count($user_list) > 0){
$step = 100;
$user = array_slice($user_list,0,$step);
$user_list = array_slice($user_list,$step);
$this->sdk->user->userRegister($user);
$progressBar->advance($step);
}
// 4. 结束进度条
$progressBar->finish();
}
private function migrateFriends(OutputInterface $output): void
{
//之前残留了一部分数据,是单向好友,这里没做删除,所以数据大小和之前的不一样,用户重新删除一次就好了
$this->log($output, "");
$this->log($output, "═════════════════ 步骤3: 迁移好友 ═════════════════");
$this->log($output, "清理旧的数据");
// $this->cleanExistingData($output,[
// 'conversation', 'conversation_version', // 会话相关集合
// 'data_version', // 数据版本集合
// 'friend', 'friend_request', 'friend_version', // 好友关系相关集合
// 'group', 'group_join_version','group_member','group_member_version','group_request', // 群组相关集合
// 'msg','seq','seq_user' // 消息和序列号相关集合
// ]);
$user_list = (new \app\model\Openim\User())->setOption('connection','tettt')
->whereNotNull('user_id')
->column('user_id');
// 1. 创建进度条(内置核心方法)
$progressBar = new ProgressBar($output, count($user_list));
// 可选:设置进度条样式(字符、长度等)
$progressBar->setBarCharacter('█');
$progressBar->setEmptyBarCharacter('░');
$progressBar->setProgressCharacter('▶');
$progressBar->setBarWidth(400);
// 2. 开始显示
$progressBar->start();
foreach($user_list as $userID){
$friend_list = (new \app\model\Openim\Friend())->setOption('connection','tettt')
->where('owner_user_id',$userID)
->column('friend_user_id');
if(count($friend_list)){
while(count($friend_list)){
$_friend_list = array_slice($friend_list, 0, 500);
$friend_list = array_slice($friend_list, 500);
$this->sdk->friend->importFriend($userID,$_friend_list);
}
}
$progressBar->advance();
}
// 4. 结束进度条
$progressBar->finish();
}
private function migrateGroups(OutputInterface $output): void
{
$this->log($output, "");
$this->log($output, "═════════════════ 步骤2: 迁移群组 ═════════════════");
$this->log($output, "");
$options = [];
$groups = $this->queryOldDb('group', [], $options);
$this->stats['groups']['total'] = count($groups);
$this->log($output, "📊 找到 {$this->stats['groups']['total']} 个群组");
$processed = 0;
// 1. 创建进度条(内置核心方法)
$progressBar = new ProgressBar($output, count($groups));
// 可选:设置进度条样式(字符、长度等)
$progressBar->setBarCharacter('█');
$progressBar->setEmptyBarCharacter('░');
$progressBar->setProgressCharacter('▶');
$progressBar->setBarWidth(400);
// 2. 开始显示
$progressBar->start();
foreach ($groups as $group) {
$processed++;
$groupID = (string)($group['group_id'] ?? $group['groupID'] ?? '');
if (empty($groupID) || in_array($groupID, $this->skipGroups)) {
$this->stats['groups']['failed']++;
continue;
}
$ownerUserID = (string)($group['owner_user_id'] ?? $group['ownerUserID'] ?? $group['creator_user_id'] ?? $group['creatorUserID'] ?? '');
if (empty($ownerUserID)) {
$this->stats['groups']['failed']++;
continue;
}
$groupName = (string)($group['group_name'] ?? $group['groupName'] ?? '');
$faceURL = (string)($group['face_url'] ?? $group['faceURL'] ?? '');
$introduction = (string)($group['introduction'] ?? '');
$notification = (string)($group['notification'] ?? '');
$ex = (string)($group['ex'] ?? '');
// 群组设置字段
$groupType = (int)($group['group_type'] ?? $group['groupType'] ?? 2);
$needVerification = (int)($group['need_verification'] ?? $group['needVerification'] ?? 0);
$lookMemberInfo = (int)($group['look_member_info'] ?? $group['lookMemberInfo'] ?? 0);
$applyMemberFriend = (int)($group['apply_member_friend'] ?? $group['applyMemberFriend'] ?? 0);
$progress = sprintf("[群组 %d/%d]", $processed, $this->stats['groups']['total']);
if ($processed % 20 == 0 || $processed == 1) {
$this->log($output, "{$progress} 处理中...");
}
$this->log($output, "{$progress} 尝试创建群组: {$groupID}, 群主: {$ownerUserID}");
// 管理员信息
$adminUserIDs = (new \app\model\Openim\GroupMember())->setOption('connection','tettt')
->where('group_id',$groupID)
->where('role_level',60)
->column('user_id');
//cp($adminUserIDs );
// 成员信息
$memberUserIDs = (new \app\model\Openim\GroupMember())->setOption('connection','tettt')
->where('group_id',$groupID)
->where('role_level',20)
->column('user_id');
//cp($memberUserIDs );
$memberUserIDs = array_unique($memberUserIDs);
$_memberUserIDs = array_slice($memberUserIDs, 0, 10);
$memberUserIDs = array_slice($memberUserIDs, 10);
try {
$this->sdk->group->createGroup(
$ownerUserID,
$_memberUserIDs,
$adminUserIDs,
$groupName,
$groupID,
$faceURL,
$introduction,
$notification,
$ex,
$groupType,
$needVerification,
$lookMemberInfo,
$applyMemberFriend
);
while(count($memberUserIDs)){
$_memberUserIDs = array_slice($memberUserIDs, 0, 10);
$memberUserIDs = array_slice($memberUserIDs, 10);
try{
$this->sdk->group->inviteUserToGroup($groupID, $_memberUserIDs);
} catch (\Exception $e) {
$this->log($output, "{$progress} ❌ 邀请成员失败: " . $e->getMessage());
}
}
$this->stats['groups']['success']++;
//$this->log($output, "{$progress} ✅ 创建成功");
} catch (\Exception $e) {
$this->stats['groups']['failed']++;
if ($e->getCode() == 1202 || strpos($e->getMessage(), 'GroupIDExisted') !== false) {
$this->log($output, "{$progress} ️ 群组已存在,跳过创建");
$this->stats['groups']['success']++;
continue;
} else {
$this->log($output, "{$progress} ❌ 创建失败: " . $e->getMessage());
}
}
$progressBar->advance();
}
// 4. 结束进度条
$progressBar->finish();
}
private function migrateGroupMembers(OutputInterface $output): void
{
$this->log($output, "");
$this->log($output, "═════════════════ 步骤3: 迁移群成员 ═════════════════");
$this->log($output, "");
$groups = $this->queryOldDb('group', [], ['projection' => ['group_id' => 1, 'groupID' => 1]]);
$groupIDs = [];
foreach ($groups as $g) {
$gid = (string)($g['group_id'] ?? $g['groupID'] ?? '');
if (!empty($gid) && !in_array($gid, $this->skipGroups)) {
$groupIDs[] = $gid;
}
}
$totalMembers = 0;
foreach ($groupIDs as $groupID) {
$members = $this->queryOldDb('group_member', ['group_id' => $groupID]);
$ownerUserID = null;
$adminUserIDs = [];
$memberUserIDs = [];
foreach ($members as $member) {
$userID = (string)($member['user_id'] ?? $member['userID'] ?? '');
if (empty($userID)) continue;
$roleLevel = (int)($member['role_level'] ?? $member['roleLevel'] ?? 0);
if ($roleLevel == 100) {
$ownerUserID = $userID;
} elseif ($roleLevel == 60) {
$adminUserIDs[] = $userID;
} else {
$memberUserIDs[] = $userID;
}
}
if (empty($memberUserIDs) && empty($adminUserIDs)) {
continue;
}
$totalMembers += count($memberUserIDs);
$this->stats['members']['total'] += count($memberUserIDs);
$progress = sprintf("[群 %s 成员 %d]", $groupID, count($memberUserIDs));
$this->log($output, "{$progress} 处理中...");
// 分批邀请,每批最多50人
$batches = array_chunk($memberUserIDs, 50);
foreach ($batches as $batch) {
if (empty($batch)) {
continue;
}
$attempts = 0;
while ($attempts < $this->retry) {
try {
$this->log($output, "{$progress} 邀请成员: " . implode(', ', array_slice($batch, 0, 5)) . (count($batch) > 5 ? '...' : ''));
$result = $this->sdk->group->inviteUserToGroup($groupID, $ownerUserID ?? 'admin', $batch);
$this->log($output, "{$progress} API返回: " . json_encode($result, JSON_UNESCAPED_UNICODE));
if (isset($result['errCode']) && $result['errCode'] != 0) {
// 检查是否是重复键错误
if (strpos($result['errMsg'] ?? '', 'duplicate key') !== false || strpos($result['errMsg'] ?? '', 'DuplicateKey') !== false) {
$this->log($output, "{$progress} ️ 部分成员已存在,跳过");
$this->stats['members']['success'] += count($batch);
} else {
$this->stats['members']['failed'] += count($batch);
$this->log($output, "{$progress} ❌ 邀请失败: " . ($result['errMsg'] ?? '未知错误'));
}
} else {
$this->stats['members']['success'] += count($batch);
$this->log($output, "{$progress} ✅ 邀请成功");
}
break;
} catch (\Exception $e) {
$attempts++;
// 检查是否是重复键错误
if (strpos($e->getMessage(), 'duplicate key') !== false || strpos($e->getMessage(), 'DuplicateKey') !== false) {
$this->log($output, "{$progress} ️ 部分成员已存在,跳过");
$this->stats['members']['success'] += count($batch);
break;
} elseif ($attempts >= $this->retry) {
$this->stats['members']['failed'] += count($batch);
$this->log($output, "{$progress} ❌ 邀请异常: " . $e->getMessage());
} else {
$this->log($output, "{$progress} ⚠️ 邀请失败,第 {$attempts}/{$this->retry} 次重试...");
usleep(100000);
}
}
}
usleep(10000);
}
}
$this->log($output, "📊 共处理 {$totalMembers} 个群成员");
}
private function migrateMessages(OutputInterface $output): void
{
$this->log($output, "");
$this->log($output, "═════════════════ 步骤4: 迁移消息 ═════════════════");
$this->log($output, "");
$pipeline = [
['$unwind' => '$msgs'],
['$match' => ['msgs.msg' => ['$ne' => null]]],
['$sort' => ['msgs.msg.send_time' => 1]],
];
$pipeline[] = ['$project' => ['doc_id' => 1, 'msg' => '$msgs.msg']];
$command = new \MongoDB\Driver\Command([
'aggregate' => 'msg',
'pipeline' => $pipeline,
'cursor' => new \stdClass
]);
$cursor = $this->oldManager->executeCommand('tettt', $command);
$messages = [];
foreach ($cursor as $doc) {
$messages[] = $this->bsonToArray($doc);
}
$this->stats['messages']['total'] = count($messages);
$this->log($output, "📊 找到 {$this->stats['messages']['total']} 条消息");
$processed = 0;
foreach ($messages as $doc) {
$processed++;
$msg = $doc['msg'] ?? [];
if (empty($msg)) {
$this->stats['messages']['skipped']++;
continue;
}
$sendID = (string)($msg['send_id'] ?? $msg['sendID'] ?? '');
$recvID = (string)($msg['recv_id'] ?? $msg['recvID'] ?? '');
$groupID = (string)($msg['group_id'] ?? $msg['groupID'] ?? '');
$contentType = (int)($msg['content_type'] ?? $msg['contentType'] ?? 101);
$sessionType = (int)($msg['session_type'] ?? $msg['sessionType'] ?? 1);
if (in_array($sendID, $this->skipUsers)) {
$this->stats['messages']['skipped']++;
continue;
}
if ($sessionType == 3 && in_array($groupID, $this->skipGroups)) {
$this->stats['messages']['skipped']++;
continue;
}
// 跳过特殊消息类型(如系统通知等)
if (in_array($contentType, [200, 201, 202, 203, 204, 205])) {
$this->stats['messages']['skipped']++;
continue;
}
$progress = sprintf("[消息 %d/%d]", $processed, $this->stats['messages']['total']);
if ($processed % 100 == 0 || $processed == 1) {
$this->log($output, "{$progress} 处理中...");
}
try {
$this->log($output, "{$progress} 发送消息: sendID={$sendID}, recvID={$recvID}, groupID={$groupID}, contentType={$contentType}, sessionType={$sessionType}");
$result = $this->sendMessage($msg);
$this->log($output, "{$progress} API返回: " . json_encode($result, JSON_UNESCAPED_UNICODE));
if ($result['success'] ?? false) {
$this->stats['messages']['success']++;
if ($processed % 100 == 0) {
$this->log($output, "{$progress} ✅ 发送成功");
}
} else {
$this->stats['messages']['failed']++;
$this->log($output, "{$progress} ❌ 发送失败: " . ($result['errMsg'] ?? '未知错误'));
// 遇到NotInGroupYetError时跳过,继续迁移其他消息
if (strpos(($result['errMsg'] ?? ''), 'NotInGroupYetError') === false) {
// 遇到其他错误时退出
throw new \Exception("消息发送失败: " . ($result['errMsg'] ?? '未知错误'));
} else {
$this->log($output, "{$progress} ️ 跳过NotInGroupYetError错误,继续迁移");
}
}
} catch (\Exception $e) {
$this->stats['messages']['failed']++;
$this->log($output, "{$progress} ❌ 发送异常: " . $e->getMessage());
// 遇到NotInGroupYetError异常时跳过,继续迁移其他消息
if (strpos($e->getMessage(), 'NotInGroupYetError') === false) {
// 遇到其他异常时退出
throw $e;
} else {
$this->log($output, "{$progress} ️ 跳过NotInGroupYetError异常,继续迁移");
}
}
if ($this->delay > 0) {
usleep($this->delay * 1000);
}
}
}
private function sendMessage(array $msg): array
{
$sendID = (string)($msg['send_id'] ?? $msg['sendID'] ?? '');
$recvID = (string)($msg['recv_id'] ?? $msg['recvID'] ?? '');
$groupID = (string)($msg['group_id'] ?? $msg['groupID'] ?? '');
$contentType = (int)($msg['content_type'] ?? $msg['contentType'] ?? 101);
$sessionType = (int)($msg['session_type'] ?? $msg['sessionType'] ?? 1);
$sendTime = (int)($msg['send_time'] ?? $msg['sendTime'] ?? 0);
$content = $msg['content'] ?? '';
$ex = (string)($msg['ex'] ?? '');
if (empty($sendID)) {
return ['success' => false, 'errMsg' => 'sendID为空'];
}
$contentData = $this->parseContent($content, $contentType);
// 构建消息数据
$messageData = [
'content' => $contentData,
'contentType' => $contentType,
'sendTime' => $sendTime,
'ex' => $ex,
'isOnlineOnly' => false,
'notOfflinePush' => true
];
// 根据会话类型调用不同的发送方法
if ($sessionType == 1 && !empty($recvID)) {
// 单聊
$result = $this->sdk->message->sendSingleMessage($sendID, $recvID, $messageData);
} elseif (!empty($groupID)) {
// 群聊
$result = $this->sdk->message->sendGroupMessage($sendID, $groupID, $messageData);
} else {
return ['success' => false, 'errMsg' => '缺少必要的参数'];
}
return [
'success' => !($result['errCode'] ?? 0),
'errMsg' => $result['errMsg'] ?? ''
];
}
private function parseContent($content, int $contentType): array
{
if (is_string($content)) {
$decoded = json_decode($content, true);
if (json_last_error() === JSON_ERROR_NONE && is_array($decoded)) {
return $decoded;
}
return ['content' => $content, 'text' => $content];
}
if (is_array($content)) {
return $content;
}
return ['content' => '', 'text' => ''];
}
private function bsonToArray($data): array
{
if ($data instanceof \MongoDB\Model\BSONArray) {
return $data->getArrayCopy();
}
if ($data instanceof \MongoDB\Model\BSONDocument) {
return $data->getArrayCopy();
}
if (is_object($data)) {
return json_decode(json_encode($data), true);
}
return is_array($data) ? $data : [];
}
private function printStats(OutputInterface $output): void
{
$this->log($output, "");
$this->log($output, "╔════════════════════════════════════════════════════════════╗");
$this->log($output, "║ 迁移统计报告 ║");
$this->log($output, "╠════════════════════════════════════════════════════════════╣");
$this->log($output, "║ 用户: 总数 {$this->stats['users']['total']}, 成功 {$this->stats['users']['success']}, 失败 {$this->stats['users']['failed']}");
$this->log($output, "║ 群组: 总数 {$this->stats['groups']['total']}, 成功 {$this->stats['groups']['success']}, 失败 {$this->stats['groups']['failed']}");
$this->log($output, "║ 成员: 总数 {$this->stats['members']['total']}, 成功 {$this->stats['members']['success']}, 失败 {$this->stats['members']['failed']}");
$this->log($output, "║ 消息: 总数 {$this->stats['messages']['total']}, 成功 {$this->stats['messages']['success']}, 失败 {$this->stats['messages']['failed']}, 跳过 {$this->stats['messages']['skipped']}");
$this->log($output, "╚════════════════════════════════════════════════════════════╝");
}
private function log(OutputInterface $output, string $message): void
{
$output->writeln($message);
}
/**
* 获取OpenIM SDK实例
*
* @return object
*/
function getSdk()
{
if ($this->sdk) {
return $this->sdk;
}
$this->sdk = new \support\OpenImSdk\Client([
'host' => config('openim.server'),
'secret' => config('openim.secret'),
]);
return $this->sdk;
}
private function initConnections(OutputInterface $output): void
{
$this->log($output, "正在初始化连接...");
$this->getSdk();
$uri = 'mongodb://commie:n1e5a6s6m7@127.0.0.1:37017/tettt?authSource=admin';
$this->oldManager = new \MongoDB\Driver\Manager($uri);
$this->log($output, "✅ 连接成功");
}
private function cleanExistingData(OutputInterface $output,$collections=[]): void
{
// 记录开始清理数据的日志信息
$this->log($output, "\n═════════════════ 清理现有数据 ═════════════════");
$this->log($output, "");
// 构建新数据库(OpenIM v3)的MongoDB连接URI
$uri = 'mongodb://commie:n1e5a6s6m7@127.0.0.1:37017/openim_v3?authSource=admin';
// 创建MongoDB驱动管理器实例,用于操作新数据库
$this->newManager = new \MongoDB\Driver\Manager($uri);
try {
// 记录开始清理数据的状态
$this->log($output, "正在清理mongodb数据...");
// 定义需要清空的数据集合列表(保留user集合,清空其他所有业务数据)
// 遍历所有需要清理的集合,逐个执行清空操作
foreach ($collections as $collection) {
try {
// 创建批量写入操作对象
$bulk = new \MongoDB\Driver\BulkWrite;
// 添加删除所有文档的操作(空条件表示删除全部)
$bulk->delete([]);
// 执行批量删除操作,指定数据库和集合名称
$this->newManager->executeBulkWrite('openim_v3.' . $collection, $bulk);
// 记录该集合清空成功的日志
$this->log($output, "已清空集合: {$collection}");
} catch (\Exception $e) {
// 单个集合清空失败时记录警告信息,不影响其他集合的清理
$this->log($output, "⚠️ 清空集合 {$collection} 失败: " . $e->getMessage());
}
}
$this->log($output, "正在清理redis数据...");
$redis = new \Redis();
$host = '127.0.0.1';
$port = 16379;
$password = 'n1e5a6s6m7';
$output->writeln("连接 Redis: {$host}:{$port}");
if ($redis->connect($host, $port)) {
if (!empty($password)) {
$redis->auth($password);
}
$result = $redis->flushAll();
if ($result) {
$output->writeln("✅ Redis 清空成功");
} else {
$output->writeln("❌ Redis 清空失败");
}
} else {
$output->writeln("❌ 无法连接到 Redis");
}
// 记录所有数据清理完成的日志
$this->log($output, "✅ 数据清理完成");
} catch (\Exception $e) {
// 捕获整体清理过程中的异常,记录错误但不抛出,确保程序继续执行
$this->log($output, "❌ 清理数据失败: " . $e->getMessage());
// 不抛出异常,继续执行
}
}
private function queryOldDb(string $collection, array $filter = [], array $options = []): array
{
$query = new \MongoDB\Driver\Query($filter, $options);
$cursor = $this->oldManager->executeQuery('tettt.' . $collection, $query);
$result = [];
foreach ($cursor as $doc) {
$result[] = $this->bsonToArray($doc);
}
return $result;
}
/**
* 备份MongoDB数据
* @param OutputInterface $output
* @param string $step
* @return string
*/
private function backupMongoDB(OutputInterface $output, string $step): string
{
$this->log($output, "═════════════════ 备份MongoDB数据 ═════════════════");
// 确保备份目录存在
if (!is_dir($this->backupDir)) {
mkdir($this->backupDir, 0755, true);
}
// 生成备份文件名
$timestamp = date('YmdHis');
$backupFile = "{$this->backupDir}/openim_v3_{$step}_{$timestamp}.json";
try {
// 使用现有的新数据库连接
if (!$this->newManager) {
$uri = 'mongodb://commie:n1e5a6s6m7@127.0.0.1:37017/openim_v3?authSource=admin';
$this->newManager = new \MongoDB\Driver\Manager($uri);
}
// 获取所有集合
$command = new \MongoDB\Driver\Command(['listCollections' => 1]);
$cursor = $this->newManager->executeCommand('openim_v3', $command);
$collections = $cursor->toArray();
//$this->log($output, "找到 " . count($collections) . " 个集合");
$backupData = [];
// 备份每个集合
foreach ($collections as $collection) {
$collectionName = $collection->name;
if (in_array($collectionName, ['system.indexes', 'system.profile'])) {
continue;
}
//$this->log($output, "备份集合: {$collectionName}");
$query = new \MongoDB\Driver\Query([]);
$cursor = $this->newManager->executeQuery('openim_v3.' . $collectionName, $query);
$documents = [];
foreach ($cursor as $doc) {
$document = $this->bsonToArray($doc);
// 处理ObjectId
if (isset($document['_id']) && is_array($document['_id']) && isset($document['_id']['$oid'])) {
$document['_id'] = $document['_id']['$oid'];
}
$documents[] = $document;
}
if (!empty($documents)) {
$backupData[$collectionName] = $documents;
//$this->log($output, " - 备份了 " . count($documents) . " 条记录");
}
}
// 保存备份文件
file_put_contents($backupFile, json_encode($backupData, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT));
$this->log($output, "✅ 备份成功: {$backupFile} (" . filesize($backupFile) . " 字节)");
$this->currentBackup = $backupFile;
return $backupFile;
} catch (\Exception $e) {
$this->log($output, "❌ 备份失败: " . $e->getMessage());
return '';
}
}
/**
* 回滚MongoDB数据
* @param OutputInterface $output
* @param string $backupFile
* @return bool
*/
private function restoreMongoDB(OutputInterface $output, string $backupFile): bool
{
$this->log($output, "═════════════════ 回滚MongoDB数据 ═════════════════");
if (!file_exists($backupFile)) {
$this->log($output, "❌ 备份文件不存在: {$backupFile}");
return false;
}
try {
// 读取备份文件
$backupData = json_decode(file_get_contents($backupFile), true);
if (empty($backupData)) {
$this->log($output, "❌ 备份文件为空");
return false;
}
// 连接到新数据库
$uri = 'mongodb://commie:n1e5a6s6m7@127.0.0.1:37017/openim_v3?authSource=admin';
$manager = new \MongoDB\Driver\Manager($uri);
// 清空所有集合
$this->log($output, "清空现有数据...");
foreach (array_keys($backupData) as $collectionName) {
$bulk = new \MongoDB\Driver\BulkWrite;
$bulk->delete([]);
$manager->executeBulkWrite('openim_v3.' . $collectionName, $bulk);
}
// 恢复数据
foreach ($backupData as $collectionName => $documents) {
$documentCount = count($documents);
$this->log($output, "恢复集合: {$collectionName} ({$documentCount} 条记录)");
$bulk = new \MongoDB\Driver\BulkWrite;
foreach ($documents as $document) {
// 处理_id字段
if (isset($document['_id']) && is_string($document['_id'])) {
// 尝试创建ObjectId
try {
$document['_id'] = new \MongoDB\BSON\ObjectId($document['_id']);
} catch (\Exception $e) {
// 如果不是有效的ObjectId格式,保持原样
}
}
$bulk->insert($document);
}
$result = $manager->executeBulkWrite('openim_v3.' . $collectionName, $bulk);
$this->log($output, "恢复成功: {$result->getInsertedCount()} 条记录");
}
$this->log($output, "✅ 回滚成功");
return true;
} catch (\Exception $e) {
$this->log($output, "❌ 回滚失败: " . $e->getMessage());
return false;
}
}
}
+173
View File
@@ -0,0 +1,173 @@
<?php
namespace app\command;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Output\OutputInterface;
use support\think\Db;
class Min extends Command
{
protected static $defaultName = 'Min';
protected static $defaultDescription = '压缩静态文件';
/**
* 路径和文件名配置
*/
protected $options = [
'cssBaseUrl' => 'public/assets/css/',
'cssBaseName' => '{module}',
'jsBaseUrl' => 'public/assets/js/',
'jsBaseName' => 'require-{module}',
];
/**
* @return void
*/
protected function configure()
{
$this->addOption('module', 'm', InputArgument::REQUIRED, 'module name(frontend or backend),use \'all\' when build all modules', null)
->addOption('resource', 'r', InputArgument::REQUIRED, 'resource name(js or css),use \'all\' when build all resources', null)
->addOption('optimize', 'o', InputArgument::OPTIONAL, 'optimize type(uglify|closure|none)', 'none')
->setDescription('Compress js and css file');
}
/**
* @param InputInterface $input
* @param OutputInterface $output
* @return int
*/
protected function execute(InputInterface $input, OutputInterface $output): int
{
$module = $input->getOption('module') ?: '';
$resource = $input->getOption('resource') ?: '';
$optimize = $input->getOption('optimize') ?: 'none';
if (!$module || !in_array($module, ['frontend', 'backend', 'all'])) {
throw new \Exception('Please input correct module name');
}
if (!$resource || !in_array($resource, ['js', 'css', 'all'])) {
throw new \Exception('Please input correct resource name');
}
$moduleArr = $module == 'all' ? ['frontend', 'backend'] : [$module];
$resourceArr = $resource == 'all' ? ['js', 'css'] : [$resource];
$minPath = __DIR__ . DIRECTORY_SEPARATOR . 'Min' . DIRECTORY_SEPARATOR;
$publicPath = public_path() . DIRECTORY_SEPARATOR;
$tempFile = $minPath . 'temp.js';
$nodeExec = '';
if (!$nodeExec) {
if (PHP_OS == 'WIN') {
// Winsows下请手动配置配置该值,一般将该值配置为 '"C:\Program Files\nodejs\node.exe"',除非你的Node安装路径有变更
$nodeExec = 'C:\Program Files\nodejs\node.exe';
if (file_exists($nodeExec)) {
$nodeExec = '"' . $nodeExec . '"';
} else {
// 如果 '"C:\Program Files\nodejs\node.exe"' 不存在,可能是node安装路径有变更
// 但安装node会自动配置环境变量,直接执行 '"node.exe"' 提高第一次使用压缩打包的成功率
$nodeExec = '"node.exe"';
}
} else {
try {
$nodeExec = exec("which node");
if (!$nodeExec) {
throw new \Exception("node environment not found!please install node first!");
}
} catch (\Exception $e) {
throw new \Exception($e->getMessage());
}
}
}
foreach ($moduleArr as $mod) {
foreach ($resourceArr as $res) {
$data = [
'publicPath' => $publicPath,
'jsBaseName' => str_replace('{module}', $mod, $this->options['jsBaseName']),
'jsBaseUrl' => $this->options['jsBaseUrl'],
'cssBaseName' => str_replace('{module}', $mod, $this->options['cssBaseName']),
'cssBaseUrl' => $this->options['cssBaseUrl'],
'jsBasePath' => str_replace(DIRECTORY_SEPARATOR, '/', public_path() . $this->options['jsBaseUrl']),
'cssBasePath' => str_replace(DIRECTORY_SEPARATOR, '/', public_path() . $this->options['cssBaseUrl']),
'optimize' => $optimize,
'ds' => DIRECTORY_SEPARATOR,
];
//源文件
$from = $data["{$res}BasePath"] . $data["{$res}BaseName"] . '.' . $res;
if (!is_file($from)) {
$output->writeln("{$res} source file not found!file:{$from}");
continue;
}
if ($res == "js") {
$content = file_get_contents($from);
preg_match("/require\.config\(\{[\r\n]?[\n]?+(.*?)[\r\n]?[\n]?}\);/is", $content, $matches);
if (!isset($matches[1])) {
$output->writeln("js config not found!");
continue;
}
$config = preg_replace("/(urlArgs|baseUrl):(.*)\n/", '', $matches[1]);
$config = preg_replace("/('tableexport'):(.*)\,\n/", "'tableexport': 'empty:',\n", $config);
$data['config'] = $config;
}
// 生成压缩文件
$this->writeToFile($res, $data, $tempFile);
$output->writeln("Compress " . $data["{$res}BaseName"] . ".{$res}");
// 执行压缩
$command = "{$nodeExec} \"{$minPath}r.js\" -o \"{$tempFile}\" >> \"{$minPath}node.log\"";
if ($output->isDebug()) {
$output->writeln($command);
}
echo exec($command);
}
}
if (!$output->isDebug()) {
@unlink($tempFile);
}
$output->writeln("Build Successed!");
return self::SUCCESS;
}
/**
* 写入到文件
* @param string $name
* @param array $data
* @param string $pathname
* @return mixed
*/
protected function writeToFile($name, $data, $pathname)
{
$search = $replace = [];
foreach ($data as $k => $v) {
$search[] = "{%{$k}%}";
$replace[] = $v;
}
$stub = file_get_contents($this->getStub($name));
$content = str_replace($search, $replace, $stub);
if (!is_dir(dirname($pathname))) {
mkdir(strtolower(dirname($pathname)), 0755, true);
}
return file_put_contents($pathname, $content);
}
/**
* 获取基础模板
* @param string $name
* @return string
*/
protected function getStub($name)
{
return __DIR__ . DIRECTORY_SEPARATOR . 'Min' . DIRECTORY_SEPARATOR . 'stubs' . DIRECTORY_SEPARATOR . $name . '.stub';
}
}
+6
View File
@@ -0,0 +1,6 @@
({
cssIn: "{%cssBasePath%}{%cssBaseName%}.css",
out: "{%cssBasePath%}{%cssBaseName%}.min.css",
optimizeCss: "default",
optimize: "{%optimize%}"
})
+11
View File
@@ -0,0 +1,11 @@
({
{%config%}
,
optimizeCss: "standard",
optimize: "{%optimize%}", //可使用uglify|closure|none
preserveLicenseComments: false,
removeCombined: false,
baseUrl: "{%jsBasePath%}", //JS文件所在的基础目录
name: "{%jsBaseName%}", //来源文件,不包含后缀
out: "{%jsBasePath%}{%jsBaseName%}.min.js" //目标文件
});
+1620
View File
File diff suppressed because it is too large Load Diff
+158
View File
@@ -0,0 +1,158 @@
<?php
namespace app\command;
use Exception;
use plugin\admin\app\model\Config;
use app\model\User as UserModel;
use support\Request;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Output\OutputInterface;
use support\think\Db;
use support\Mattermost;
use app\model\User;
class Tongji extends Command
{
protected static $defaultName = 'Tongji';
protected static $defaultDescription = 'Tongji';
/**
* @return void
*/
protected function configure()
{
$this->addOption('action','a', InputArgument::OPTIONAL, '要做什么操作');
$this->addOption('user_id','uid', InputArgument::OPTIONAL, 'user_id');
}
/**
* @param InputInterface $input
* @param OutputInterface $output
* @return int
*/
protected function execute(InputInterface $input, OutputInterface $output): int
{
$action = $input->getOption('action');
if(method_exists($this, $action)){
return $this->$action($input, $output);
}
cp('操作不存在:'.$action);
return self::SUCCESS;
}
/**
* 修复充值统计
*/
function recharge(InputInterface $input, OutputInterface $output) {
//购买金额统计
$recharge = \app\model\Recharge::where('status',\app\enum\RechargeStatus::COMPLETE->value)->select();
$recharge_result = [];
$statistics_recharge_times_result = [];
$user_recharge_total_result = [];
$team_recharge_total_result = [];
/**
* @var \app\model\Recharge $vo
*/
foreach($recharge as $vo){
$date = explode(' ',$vo->created_at)[0];
$recharge_result[$date] += abs($vo->amount);
$statistics_recharge_times_result[$date] += 1;
$user_recharge_total_result[$vo->user_id.''] +=abs($vo->amount);
$parent_id = get_parent_id($vo->user_id);
if($parent_id){
//团队提现统计
$team_recharge_total_result[$parent_id.''] +=abs($vo->amount);
}
}
foreach($recharge_result as $date => $value){
cache('statistics_recharge_amount_'.$date,$value);
}
foreach($statistics_recharge_times_result as $date => $value){
cache('statistics_recharge_times_'.$date,$value);
}
foreach($user_recharge_total_result as $user_id => $value){
cache('user_recharge_total_'.$user_id,$value);
}
foreach($team_recharge_total_result as $user_id => $value){
cache('team_recharge_total_'.$user_id,$value);
}
cp($recharge_result);
}
/**
* 修复提现统计
*/
function withdrawl(InputInterface $input, OutputInterface $output) {
//购买金额统计
$withdrawl = \app\model\Withdrawl::where('status',\app\enum\WithdrawlStatus::COMPLETE->value)->select();
$withdrawl_result = [];
$statistics_withdrawl_times_result = [];
$user_withdrawl_total_result = [];
$team_withdrawl_total_result = [];
/**
* @var \app\model\Withdrawl $vo
*/
foreach($withdrawl as $vo){
$date = explode(' ',$vo->created_at)[0];
$withdrawl_result[$date] += abs($vo->recive_amount);
$statistics_withdrawl_times_result[$date] += 1;
$user_withdrawl_total_result[$vo->user_id.''] +=abs($vo->recive_amount);
$parent_id = get_parent_id($vo->user_id);
if($parent_id){
//团队提现统计
$team_withdrawl_total_result[$parent_id.''] +=abs($vo->recive_amount);
}
}
foreach($withdrawl_result as $date => $value){
cache('statistics_withdrawl_amount_'.$date,$value);
}
foreach($statistics_withdrawl_times_result as $date => $value){
cache('statistics_withdrawl_times_'.$date,$value);
}
foreach($user_withdrawl_total_result as $user_id => $value){
cache('user_withdrawl_total_'.$user_id,$value);
}
foreach($team_withdrawl_total_result as $user_id => $value){
cache('team_withdrawl_total_'.$user_id,$value);
}
cp($withdrawl_result);
}
/**
* 修复团队统计数据
* @param InputInterface $input
* @param OutputInterface $output
* @return int
*/
function build_team(InputInterface $input, OutputInterface $output){
$list = Db::name('user')->order('id','asc')->column('id');
//$list = [['id'=>100006]];
foreach($list as $k=>$user_id){
//team_total
$team_user_ids = Db::name('user_team')->where('ancestor_id',$user_id)
->where('depth','>',0)
->order('depth','ASC')
->column('descendant_id');
Db::name('user_extend')->where('user_id',$user_id)->data([
'team_total'=> count($team_user_ids)
])->save();
cache('team_user_count_'.$user_id,count($team_user_ids));
$direct_use_count = Db::name('user')->where('parent_id',$user_id)->count('id');
$vip_user_count = Db::name('user')->whereIn('id',$team_user_ids)->where('role_id','>',1)->count('id');
Db::name('user_extend')->where('user_id',$user_id)->data([
'direct_total'=> $direct_use_count,
'vip_total'=> $vip_user_count
])->save();
cache('team_direct_total_'.$user_id,$direct_use_count);
cache('team_vip_total_'.$user_id,$vip_user_count);
update_user_level($user_id,$vip_user_count);
cp($user_id.'完成');
}
return self::SUCCESS;
}
}
+137
View File
@@ -0,0 +1,137 @@
<?php
namespace app\command;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Output\OutputInterface;
use support\think\Db;
use isszz\hashids\facade\Hashids;
use function GuzzleHttp\json_encode;
class User extends Command
{
protected static $defaultName = 'User';
protected static $defaultDescription = '用户';
public $sdk= null;
/**
* @return void
*/
protected function configure()
{
$this->addOption('user_id','u', InputOption::VALUE_OPTIONAL, 'user_id');
$this->addOption('action','a', InputOption::VALUE_OPTIONAL, '操作类型','test');
}
/**
* @param InputInterface $input
* @param OutputInterface $output
* @return int
*/
protected function execute(InputInterface $input, OutputInterface $output): int
{
$action = $input->getOption('action');
if(method_exists($this, $action)){
return $this->$action($input, $output);
}
cp('操作不存在:'.$action);
return 0;
}
function update_password(InputInterface $input, OutputInterface $output){
$newpassword = \plugin\admin\app\common\Util::passwordHash(MD5('qwe123'));
Db::name('User')->where('id','>',0)->save([
'loginfailure' => 0,
'password' => $newpassword
]);
return 0;
}
function login(InputInterface $input, OutputInterface $output){
$user_id = $input->getOption('user_id');
if(!$user_id){
return false;
}
$user = \support\Jwt::direct($user_id);
//$imToken = $IM->auth->getUserToken($user['userID'],2);
cp('userID:' . $user['id']);
cp('nickname:' . $user['nickname']);
cp('token:' . $user['token']);
//cp('imToken:' . $imToken['token']);
return 0;
}
function otop(InputInterface $input, OutputInterface $output){
$user_id = $input->getOption('user_id');
if(!$user_id){
return false;
}
/**
* @var \plugin\admin\app\model\Admin $admin
*/
$admin = \plugin\admin\app\model\Admin::where('id',$user_id)->find();
if(!$admin){
return false;
}
$totp = \OTPHP\TOTP::create($admin->totp_secret);
cp($totp->now());
return 1;
}
//重建user_team
function build_team(){
Db::name('user_team')->where('ancestor_id','>',0)->delete();
$list = Db::name('user')->field('id,parent_id')->order('id','asc')->select();
foreach($list as $k=>$user){
build_user_team($user);
cp('user_id:'.$user['id']);
}
return 0;
}
function register(InputInterface $input, OutputInterface $output){
$im = $this->getSdk();
try {
for($i=313;$i<333;$i++){
$mobile = (12600000000+$i).'';
$password = \support\Random::build('23456789abcdefghjklmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ',8);
$data = [
'mobile' => $mobile,
'username' => $mobile,
'region' => '86',
'nickname' => $mobile,
'role_id' => 1,
'group_id' => 0,
'password' => \plugin\admin\app\common\Util::passwordHash(md5($password)),
'avatar' => '/static/img/avatar.png',
'created_at' => time(),
'updated_at' => time(),
'status' => 1,
];
$user_id = Db::name('user')->insertGetId($data);
$userID = \support\Encrypt::userIDencode($user_id);
Db::name('user')->where('id',$user_id)->update([
'userID'=>$userID
]);
$im->user->userRegister($userID,$data['nickname'],cdnurl($data['avatar']));
$user = Db::name('user')->where('id',$user_id)->find();
Hook('user.register_successed',$user);
cp($user_id,$data['mobile'],$password);
}
return 0;
} catch (\Exception $e) {
//throw $th;
cp($e->getMessage());
return 1;
}
}
protected function getSdk(){
if($this->sdk){
return $this->sdk;
}
$this->sdk = new \support\OpenImSdk\Client([
'host' => config('openim.server'), // OpenIM API地址
'secret' => config('openim.secret'), // OpenIM密钥
]);
return $this->sdk;
}
}
+37
View File
@@ -0,0 +1,37 @@
<?php
namespace app\command;
use Monolog\Formatter\MongoDBFormatter;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Output\OutputInterface;
use support\think\Db;
class createIndex extends Command
{
protected static $defaultName = 'createIndex';
protected static $defaultDescription = 'createIndex';
/**
* @return void
*/
protected function configure()
{
$this->addArgument('name', InputArgument::OPTIONAL, 'Name description');
}
/**
* @param InputInterface $input
* @param OutputInterface $output
* @return int
*/
protected function execute(InputInterface $input, OutputInterface $output): int
{
\app\model\BalanceLog::createindex();
return 0;
}
}
+46
View File
@@ -0,0 +1,46 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
namespace app\api\exception;
use Throwable;
use Webman\Exception\ExceptionHandler;
use Webman\Http\Request;
use Webman\Http\Response;
/**
* Class Handler
* @package sapp\api\exception
*/
class Handler extends ExceptionHandler
{
public $dontReport = [
\support\exception\BusinessException::class,
];
public function report(Throwable $exception)
{
parent::report($exception);
}
public function render(Request $request, Throwable $exception): Response
{
$code = $exception->getCode();
$json = ['code' => $code ?: 500, 'msg' => __($exception->getMessage())];
return new Response(200, ['Content-Type' => 'application/json'],
json_encode($json, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
}
}
+67
View File
@@ -0,0 +1,67 @@
<?php
namespace app\controller;
use think\Model;
use support\Response;
/**
* 基础控制器
*/
class Base
{
/**
* @var Model
*/
protected $model = null;
/**
* 无需登录及鉴权的方法
* @var array
*/
protected $noNeedLogin = [];
/**
* 需要登录无需鉴权的方法
* @var array
*/
protected $noNeedAuth = [];
/**
* 数据限制
* null 不做限制,任何管理员都可以查看该表的所有数据
* auth 管理员能看到自己以及自己的子管理员插入的数据
* personal 管理员只能看到自己插入的数据
* @var string
*/
protected $dataLimit = null;
/**
* 数据限制字段
*/
protected $dataLimitField = 'admin_id';
/**
* 返回格式化json数据
*
* @param int $code
* @param string $msg
* @param array $data
* @return Response
*/
protected function json(int $code, string $msg = 'ok', array $data = []): Response
{
return json(['code' => $code, 'data' => $data, 'msg' => $msg]);
}
protected function success(string $msg = '成功', array $data = []): Response
{
return $this->json(0, $msg, $data);
}
protected function fail(string $msg = '失败', array $data = []): Response
{
return $this->json(1, $msg, $data);
}
}
+31
View File
@@ -0,0 +1,31 @@
<?php
namespace app\controller;
use Exception;
use support\exception\BusinessException;
use support\Request;
use support\Response;
class CommonController extends Crud
{
/**
* 后台主页
* @param Request $request
* @return Response
* @throws BusinessException|Exception
*/
public function register(Request $request,$code='')
{
if($code){
return view('common/register',[
'config' => Config('site'),
'invite_code' => $code
]);
}else{
return $this->fail('404');
}
}
}
+440
View File
@@ -0,0 +1,440 @@
<?php
namespace app\controller;
use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
use Illuminate\Database\Query\Builder as QueryBuilder;
use support\exception\BusinessException;
use support\think\Model;
use support\Request;
use support\Response;
use support\think\Db;
class Crud extends Base
{
/**
* @var Model
*/
protected $model = null;
/**
* 查询
* @param Request $request
* @return Response
* @throws BusinessException
*/
public function select(Request $request): Response
{
[$where, $format, $limit, $field, $order] = $this->selectInput($request);
$query = $this->doSelect($where, $field, $order);
return $this->doFormat($query, $format, $limit);
}
/**
* 添加
* @param Request $request
* @return Response
* @throws BusinessException
*/
public function insert(Request $request): Response
{
$data = $this->insertInput($request);
$id = $this->doInsert($data);
return $this->success(__('successful'), ['id' => $id]);
}
/**
* 更新
* @param Request $request
* @return Response
* @throws BusinessException
*/
public function update(Request $request): Response
{
[$id, $data] = $this->updateInput($request);
$this->doUpdate($id, $data);
return $this->success(__('successful'));
}
/**
* 删除
* @param Request $request
* @return Response
* @throws BusinessException
*/
public function delete(Request $request): Response
{
$ids = $this->deleteInput($request);
$this->doDelete($ids);
return $this->success(__('successful'));
}
/**
* 查询前置
* @param Request $request
* @return array
* @throws BusinessException
*/
protected function selectInput(Request $request): array
{
$field = $request->get('sort');
$order = $request->get('sortOrder', 'asc');
$format = $request->get('format', 'normal');
$limit = (int)$request->get('limit', $format === 'tree' ? 1000 : 10);
$limit = $limit <= 0 ? 10 : $limit;
$order = $order === 'asc' ? 'asc' : 'desc';
$where = $request->get('filter',[]);
$page = (int)$request->get('page');
$page = $page > 0 ? $page : 1;
$allow_column = [];
//var_dump($this->model->getConnectionName());
//if ($this->model->getConnection()->getDriverName() == 'mongodb') {
if ($this->model->getConnection() != 'mysql') {
} else {
$table = $this->model->getTable();
$allow_column = Db::query("desc `$table`");
if (!$allow_column) {
throw new BusinessException('表不存在');
}
$allow_column = array_column($allow_column, 'Field', 'Field');
if (!in_array($field, $allow_column)) {
$field = null;
}
}
return [$where, $format, $limit, $field, $order, $page];
}
/**
* 指定查询where条件,并没有真正的查询数据库操作
* @param array $where
* @param string|null $field
* @param string $order
* @return Model
*/
protected function doSelect(array $where, string $field = null, string $order = 'desc')
{
$model = $this->model;
foreach ($where as $column => $value) {
$symbol = $value['symbol'];
$value1 = $value['value1'];
$value2 = $value['value2'];
if (is_array($value)) {
if ($symbol === 'like' || $symbol === 'not like') {
$model = $model->where($column, $symbol, "%$value1%");
} elseif (in_array($symbol, ['>', '=', '<', '<>','>=','<='])) {
$model = $model->where($column, $symbol, $value1);
} elseif (($symbol == 'in'|| $symbol == 'not in') && !empty($value1)) {
$valArr = $value1;
if (is_string($value1)) {
$valArr = explode(",", trim($value1));
}
if($symbol == 'in'){
$model = $model->whereIn($column, $valArr);
}else{
$model = $model->whereNotIn($column, $valArr);
}
} elseif ($symbol == 'null') {
$model = $model->whereNull($column);
} elseif ($symbol == 'not null') {
$model = $model->whereNotNull($column);
} elseif ($symbol == 'range' && $$value1 !== '' || $value2 !== '') {
$model = $model->whereBetween($column, [$value1, $value2]);
}
} else {
$model = $model->where($column, $value);
}
}
if ($field) {
$model = $model->order($field, $order);
}
return $model;
}
/**
* 执行真正查询,并返回格式化数据
* @param $query
* @param $format
* @param $limit
* @return Response
*/
protected function doFormat($query, $format, $limit,$fields="*"): Response
{
$methods = [
'select' => 'formatSelect',
'tree' => 'formatTree',
'table_tree' => 'formatTableTree',
'normal' => 'formatNormal',
];
if($limit == 'all'){
$paginator = $query->field($fields)->select();
$total = count($paginator);
$items = $paginator;
}else{
$paginator = $query->field($fields)->paginate($limit);
$total = $paginator->total();
$items = $paginator->items();
}
if (method_exists($this, "afterQuery")) {
$items = call_user_func([$this, "afterQuery"], $items);
}
$format_function = $methods[$format] ?? 'formatNormal';
return call_user_func([$this, $format_function], $items, $total);
}
/**
* 插入前置方法
* @param Request $request
* @return array
* @throws BusinessException
*/
protected function insertInput(Request $request): array
{
$data = $this->inputFilter($request->post());
$password_filed = 'password';
if (isset($data[$password_filed])) {
$data[$password_filed] = password_hash($data[$password_filed],PASSWORD_DEFAULT);
}
return $data;
}
/**
* 执行插入
* @param array $data
* @return mixed|null
*/
protected function doInsert(array $data)
{
$primary_key = $this->model->getPk();
$model_class = get_class($this->model);
$model = $model_class::create($data);
return $primary_key ? $model->$primary_key : null;
}
/**
* 更新前置方法
* @param Request $request
* @return array
* @throws BusinessException
*/
protected function updateInput(Request $request): array
{
$primary_key = $this->model->getPk();
$id = $request->post($primary_key);
$data = $this->inputFilter($request->post());
$model = $this->model->find($id);
if (!$model) {
throw new BusinessException('记录不存在', 2);
}
$password_filed = 'password';
if (isset($data[$password_filed])) {
// 密码为空,则不更新密码
if ($data[$password_filed] === '') {
unset($data[$password_filed]);
} else {
$data[$password_filed] = password_hash($data[$password_filed],PASSWORD_DEFAULT);
}
}
unset($data[$primary_key]);
return [$id, $data];
}
/**
* 执行更新
* @param $id
* @param $data
* @return void
*/
protected function doUpdate($id, $data)
{
$model = $this->model->find($id);
foreach ($data as $key => $val) {
$model->{$key} = $val;
}
$model->save();
}
/**
* 对用户输入表单过滤
* @param array $data
* @return array
* @throws BusinessException
*/
protected function inputFilter(array $data): array
{
$table = config('database.connections.mysql.prefix') . $this->model->getTable();
$allow_column = Db::getFields($this->model->getTable());
if (!$allow_column) {
throw new BusinessException('表不存在', 2);
}
//$columns = array_column($allow_column, 'Type', 'Field');
//echo json_encode($allow_column);
foreach ($data as $col => $item) {
if (!isset($allow_column[$col])) {
unset($data[$col]);
continue;
}
// 非字符串类型传空则为null
if ($item === '' && strpos(strtolower($allow_column[$col]['type']), 'varchar') === false && strpos(strtolower($allow_column[$col]['type']), 'text') === false) {
$data[$col] = null;
}
if (is_array($item)) {
$data[$col] = implode(',', $item);
}
}
if (empty($data['created_at'])) {
unset($data['created_at']);
}
if (empty($data['updated_at'])) {
unset($data['updated_at']);
}
return $data;
}
/**
* 删除前置方法
* @param Request $request
* @return array
* @throws BusinessException
*/
protected function deleteInput(Request $request): array
{
$primary_key = $this->model->getPk();
if (!$primary_key) {
throw new BusinessException('该表无主键,不支持删除');
}
$ids = $request->post('ids', '');
if(!is_array($ids)){
$ids = explode(',',$ids);
}
return $ids;
}
/**
* 执行删除
* @param array $ids
* @return void
*/
protected function doDelete(array $ids)
{
if (!$ids) {
return;
}
$primary_key = $this->model->getPk();
$this->model->whereIn($primary_key, $ids)->delete();
}
/**
* 格式化树
* @param $items
* @return Response
*/
protected function formatTree($items): Response
{
$format_items = [];
//$primary_key = $this->model->getPk();
$primary_key = $this->model->getPk();
foreach ($items as $item) {
$item->name = $this->guessName($item) ?: $item->$primary_key;
$item->value = (string)$item->$primary_key;
$item->id = $item->$primary_key;
//$item->pid = $item->pid;
$format_items[] = $item;
}
return $this->success(__('successful'), $format_items);
}
/**
* 格式化表格树
* @param $items
* @return Response
*/
protected function formatTableTree($items): Response
{
return $this->success(__('successful'), $items);
}
/**
* 格式化下拉列表
* @param $items
* @return Response
*/
protected function formatSelect($items): Response
{
$formatted_items = [];
$primary_key = $this->model->getPk();
foreach ($items as $item) {
$formatted_items[] = [
'name' => $this->guessName($item) ?: $item->$primary_key,
'value' => $item->$primary_key
];
}
return $this->success(__('successful'), $formatted_items);
}
/**
* 通用格式化
* @param $items
* @param $total
* @return Response
*/
protected function formatNormal($items, $total): Response
{
return json(['code' => 0, 'msg' => 'ok', 'count' => $total, 'data' => $items]);
}
/**
* 查询数据库后置方法,可用于修改数据
* @param mixed $items 原数据
* @return mixed 修改后数据
*/
protected function afterQuery($items)
{
return $items;
}
/**
* 猜测记录名称
* @param $item
* @return mixed
*/
protected function guessName($item)
{
return $item->title ?? $item->name ?? $item->nickname ?? $item->username ?? $item->id;
}
function multi(){
$ids = Request()->post('ids');
$params = Request()->post('params');
parse_str($params,$s);
$this->model->whereIn('id', [$ids])->update($s);
return $this->success(__('successful'));
}
/**
* 返回格式化json数据
*
* @param int $code
* @param string $msg
* @param array $data
* @return Response
*/
protected function json(int $code, string $msg = 'ok', array|object $data = []): Response
{
return json(['code' => $code, 'data' => $data, 'msg' => $msg]);
}
protected function success(string $msg = '成功', array|object $data = []): Response
{
return $this->json(0, $msg, $data);
}
protected function fail(string $msg = '失败', array|object $data = []): Response
{
return $this->json(1,$msg, $data);
}
protected function error(string $msg = '失败', array|object $data = []): Response
{
return $this->json(1,$msg, $data);
}
}
+13
View File
@@ -0,0 +1,13 @@
<?php
namespace app\controller;
use support\Request;
class DocController
{
public function index(Request $request)
{
return view("/public/doc/index");
}
}
+101
View File
@@ -0,0 +1,101 @@
<?php
namespace app\controller;
use support\Request;
use support\Log;
use Symfony\Component\Process\Process;
use support\Response;
class GitController
{
private string $secret = 'a66fb7936210d94960ac9b4e0c8bd3ef45f8f3e1';
public function test(Request $request): Response
{
$this->dispatchUpdate('bang_server.sh');
return response('Test webhook executed');
}
public function handle(Request $request): Response
{
// 1. IP白名单验证(仅接受GitHub请求)
$allowedIps = ['110.42.52.196'];
if($request->method() !== 'POST'){
return response('Method Not Allowed', 405);
}
$clientIp = $request->header('x-real-ip', $request->getRealIp());
$isValidIp = false;
foreach ($allowedIps as $range) {
if ($this->ipInRange($clientIp, $range)) {
$isValidIp = true;
break;
}
}
if (!$isValidIp) {
Log::warning("Unauthorized IP: {$clientIp}");
return response('IP not allowed', 403);
}
// 2. 签名验证
$signature = $request->header('x-hub-signature-256');
$payload = $request->rawBody();
$json = json_decode($payload, true);
$script_fn = "";
if($json['repository']['full_name'] == 'commie/wenjuanbang_server')
{
if($json['ref'] == 'refs/heads/main'){
$script_fn = 'bang_server.sh';
}
if($json['ref'] == 'refs/heads/xi'){
$script_fn = 'xi_server.sh';
}
}else if($json['repository']['full_name'] == 'commie/cdkey'){
if($json['ref'] == 'refs/heads/xi'){
$script_fn = 'wjx_cdkey.sh';
}
if($json['ref'] == 'refs/heads/wjb'){
$script_fn = 'wjb_cdkey.sh';
}
}
if(!$script_fn){
return response('Not main branch', 200);
}
if (!$this->verifySignature($payload, $signature)) {
Log::warning("Invalid signature from {$clientIp}");
return response('Invalid signature', 403);
}
// 3. 异步更新
$this->dispatchUpdate($script_fn);
return response('Webhook received successfully');
}
private function ipInRange(string $ip, string $range): bool
{
[$subnet, $bits] = explode('/', $range);
$ip = ip2long($ip);
$subnet = ip2long($subnet);
$mask = -1 << (32 - $bits);
return ($ip & $mask) === ($subnet & $mask);
}
private function verifySignature(string $payload, ?string $signature): bool
{
$computedSignature = 'sha256=' . hash_hmac('sha256', $payload, $this->secret);
return hash_equals($computedSignature, $signature ?? '');
}
private function dispatchUpdate($script_fn): void
{
$scriptPath = base_path('scripts/'.$script_fn);
$outputFile = runtime_path('logs').'/'.$script_fn.'.log';
// 使用su命令切换到您的用户
$command = "bash {$scriptPath} > {$outputFile} 2>&1";
// 后台执行
shell_exec("nohup {$command} &");
}
}
+370
View File
@@ -0,0 +1,370 @@
<?php
namespace app\controller;
use support\Request;
use support\Response;
use think\facade\Db;
use app\model\User as UserModel;
class HookController{
public $sdk = null;
function index(){
return 'ok';
}
function __call($method, $args):Response
{
//log_alert(Input());
return $this->success();
}
//用户注册完成后
function callbackAfterUserRegisterCommand(Request $request): Response
{
$userID= Input('userID');
$nickname= Input('nickname');
$users = Input('users');
foreach($users as $k=>$v){
$this->getSdk()->message->sendBusinessNotification('SystemOfficialTeam',$v['userID'],[
'contentType' => 101,
'textElem' => [
'content' => '欢迎使用'.Config('site.name')
]
]);
}
return $this->success();
}
//在发送单聊消息前的回调
public function callbackBeforeSendSingleMsgCommand(Request $request): Response
{
//log_alert(Input());
// $user_id = Input('sendID');
// $recv_user_id = Input( 'recvID');
// $status = Input('status',1);
// $sessionType = Input('sessionType',null);
// if($status == 1 && $sessionType != 4){
// //$max = 10000000000;//限制消息数量
// $user_rights = get_user_rights($user_id);
// $max = $user_rights['right']['max_send_msg_count'];
// $sended_msg_count = cache('single_msg_count_'.$user_id)??0 + cache('group_msg_count_'.$user_id)??0;
// if($sended_msg_count > $max){
// return $this->error(1002,'超出消息数量限制,请先开通或升级会员');
// }
// }
return $this->success();
}
//发送单聊消息后的回调
public function callbackAfterSendSingleMsgCommand(Request $request): Response
{
$user_id = Input('sendID');
$recv_user_id = Input('recvID');
$status = Input('status',1);
$sessionType = Input('sessionType',null);
if($status == 1 && $sessionType != 4){
$key = '_msg_count_'.$user_id;
if($sessionType == 1){
$key = 'single'.$key;
}
if($sessionType == 2){
$key = 'group'.$key;
}
cache_add($key,1);
}
return $this->success();
}
//发送群聊消息前的回调
public function callbackBeforeSendGroupleMsgCommand(Request $request): Response
{
return $this->success();
}
//发送群聊消息后的回调
public function callbackAfterSendGroupleMsgCommand(Request $request): Response
{
return $this->success();
}
//发送好友申请之前的回调
public function callbackBeforeAddFriendCommand(Request $request): Response{
// $from_user_id = Input('fromUserID');
// $to_user_id = Input('toUserID');
// $handleResult = Input('handleResult');
// $key = 'friend_count_'.$from_user_id;
// $user_rights = get_user_rights($from_user_id);
// $max = isset($user_rights['right']['max_friend_count']) ? $user_rights['right']['max_friend_count'] : -1;
// if(cache($key) > $max){
// return $this->error(1001,'超出好友数量限制,请先开通或升级会员');
// }
return $this->success();
}
//发送好友申请之后的回调
public function callbackAfterAddFriendCommand(Request $request): Response
{
$from_user_id = Input('fromUserID');
$to_user_id = Input('toUserID');
cache_add('friend_count_'.$to_user_id,1);
cache_add('friend_count_'.$from_user_id,1);
return $this->success();
}
//在添加好友对方同意之前的回调
public function callbackBeforeAddFriendAgreeCommand(Request $request): Response
{
// $from_user_id = Input('fromUserID');
// $to_user_id = Input('toUserID');
// $handleResult = Input('handleResult');
// if($handleResult == 1){
// $key = 'friend_count_'.$to_user_id;
// $user_rights = get_user_rights($to_user_id);
// $max = isset($user_rights['right']['max_friend_count']) ? $user_rights['right']['max_friend_count'] : -1;
// if(cache($key) > $max){
// return $this->error(1001,'超出好友数量限制,请先开通或升级会员');
// }
// }
return $this->success();
}
//在添加好友对方同意之后的回调
public function callbackAfterAddFriendAgreeCommand(Request $request):Response
{
return $this->success();
}
//用户在线状态回调
public function callbackAfterUserOnlineCommand(Request $request): Response
{
$user_id = Input('userID');
return $this->success();
}
//用户离线状态回调
public function callbackAfterUserOfflineCommand(Request $request): Response{
$user_id = Input('userID');
return $this->success();
}
//用户删除好友之后得回调
public function callbackAfterDeleteFriendCommand(Request $request): Response {
$friendUserID = Input('friendUserID');
$ownerUserID = Input('ownerUserID');
$sdk = $this->getSdk();
$relation = $sdk->friend->isFriend($friendUserID,$ownerUserID);
if($relation){
if($relation['inUser1Friends']){
$this->getSdk()->friend->deleteFriend($friendUserID,$ownerUserID);
}
if($relation['inUser2Friends']){
$this->getSdk()->friend->deleteFriend($ownerUserID,$friendUserID);
}
}
return $this->success();
}
//在创建群组之前的回调
//执行顺序,callbackBeforeCreateGroupCommand -> callbackBeforeMembersJoinGroupCommand -> callbackAfterCreateGroupCommand
public function callbackbeforeCreateGroupCommand(Request $request): Response
{
return $this->success();
$groupID = Input('groupID');
$creatorUserID = Input('creatorUserID');
$key = 'user_'.$creatorUserID.'_create_group_count';
$user_rights = get_user_rights($creatorUserID);
$max_group_create_count = isset($user_rights['right']['max_group_create_count']) ? $user_rights['right']['max_group_create_count'] : -1;
if(cache($key) > $max_group_create_count){
return $this->error(2001,'超出创建群组数量限制,请先开通或升级会员');
}
$max_group_user = $user_rights['right']['max_group_user_count'];
if(count(Input('initMemberList')) > $max_group_user){
return $this->error(2002,'超出群组成员数量限制,请先开通或升级会员');
}
return $this->success();
}
//在创建群组之后的回调
public function callbackafterCreateGroupCommand(Request $request):Response
{
$groupID = Input('groupID');
$creatorUserID = Input('creatorUserID');
cache_add('user_'.$creatorUserID.'_create_group_count',1);
//增加群组用户数量缓存
cache_add('group_'.$groupID.'_user_count',count(Input('initMemberList')));
return $this->success();
}
//转让群主之后的回调
public function callbackAfterTransferGroupOwnerCommand(Request $request):Response
{
$oldOwnerUserID = Input('oldOwnerUserID');
$newOwnerUserID = Input('newOwnerUserID');
$groupID = Input('groupID');
cache_add('user_'.$oldOwnerUserID.'_create_group_count',-1);
cache_add('user_'.$newOwnerUserID.'_create_group_count',1);
cache_add('group_owner_'.$groupID,$newOwnerUserID);
return $this->success();
}
//解散群组后回调
public function callbackAfterDisMissGroupCommand(Request $request):Response
{
$groupID = Input('groupID');
$ownerID = Input('ownerID');
//减少群主创建群组数量缓存
cache_add('user_'.$ownerID.'_create_group_count',-1);
//删除群组用户数量缓存
cache('group_'.$groupID.'_user_count',null);
//删除群组群主缓存
cache('group_owner_'.$groupID,null);
return $this->success();
}
//用户退出群组的回调
public function callbackAfterQuitGroupCommand(Request $request):Response
{
$groupID = Input('groupID');
$userID = Input('userID');
// //减少用户加入群组数量缓存
// cache_add('user_'.$userID.'_join_group_count',-1);
//减少群组用户数量缓存
cache_add('group_'.$groupID.'_user_count',-1);
return $this->success();
}
//群成员进群之前的回调
public function callbackBeforeMembersJoinGroupCommand(Request $request):Response
{
return $this->success();
$groupID = Input('groupID');
$memberList = Input('memberList');
$ownerID = $this->getGroupOwner($groupID);
if(!$ownerID){
return $this->success();
}
//获取群组当前用户数量
$group_user_count = cache('group_'.$groupID.'_user_count');
if($group_user_count === null){
$group_user_count = 0;
}
//获取群组最大用户数量
$max_group_user = get_user_rights($ownerID)['right']['max_group_user_count'];
if((count($memberList) + $group_user_count) > $max_group_user){
return $this->error(2003,'超出群组成员数量限制,请先开通或升级会员');
}
return $this->success();
}
//踢除群组成员的回调
public function callbackAfterKickGroupCommand(Request $request):Response
{
$groupID = Input('groupID');
$kickedUserIDs = Input('kickedUserIDs');
//减少群组用户数量缓存
cache_add('group_'.$groupID.'_user_count',-count($kickedUserIDs));
// foreach($kickedUserIDs as $kickedUserID){
// //减少用户加入群组数量缓存
// cache_add('user_'.$kickedUserID.'_join_group_count',-1);
// }
return $this->success();
}
//新成员加入群组之后的回调
public function callbackAfterJoinGroupCommand(Request $request):Response
{
$groupID = Input('groupID');
$userID = Input('userID');
// //增加用户加入群组数量缓存
// cache_add('user_'.$userID.'_join_group_count',1);
//增加群组用户数量缓存
cache_add('group_'.$groupID.'_user_count',1);
return $this->success();
}
//邀请新成员加入群组之前的回调
//执行顺序,callbackBeforeInviteJoinGroupCommand -> callbackBeforeMembersJoinGroupCommand
public function callbackBeforeInviteJoinGroupCommand(Request $request):Response
{
return $this->success();
$groupID = Input('groupID');
$invitedUserIDs = Input('invitedUserIDs');
//获取群组当前用户数量
$group_user_count = cache('group_'.$groupID.'_user_count');
if($group_user_count === null){
$group_user_count = 0;
}
//获取群组最大用户数量
$max_group_user = get_user_rights($this->getGroupOwner($groupID))['right']['max_group_user_count'];
if((count($invitedUserIDs) + $group_user_count) > $max_group_user){
return $this->error(2003,'超出群组成员数量限制,请先开通或升级会员');
}
return $this->success();
}
//申请加入群组之前的回调
public function callbackBeforeJoinGroupCommand(Request $request):Response
{
return $this->success();
$groupID = Input('groupID');
$applyID = Input('applyID');
//获取群组当前用户数量
$group_user_count = cache('group_'.$groupID.'_user_count')?:0;
//获取群组最大用户数量
$max_group_user = get_user_rights($this->getGroupOwner($groupID))['right']['max_group_user_count'];
if((1 + $group_user_count) > $max_group_user){
return $this->error(2003,'群组已经满员');
}
// //获取用户加入群组数量限制
// $max_group_join_count = get_user_rights($applyID)['right']['max_group_join_count'];
// //获取用户加入群组数量
// $user_join_group_count = cache('user_'.$applyID.'_join_group_count')?:0;
// if((1 + $user_join_group_count) > $max_group_join_count){
// return $this->error(2004,'超出加入群组数量限制,请先开通或升级会员');
// }
return $this->success();
}
function getGroupOwner($groupID=''){
$result = cache('group_owner_'.$groupID);
if($result){
return $result;
}
$groupsInfo = $this->getSdk()->group->getGroupsInfo([$groupID]);
foreach($groupsInfo['groupInfos'] as $groupInfo){
if($groupInfo['groupID'] == $groupID){
cache('group_owner_'.$groupID,$groupInfo['ownerUserID']);
return $groupInfo['ownerUserID'];
}
}
return '';
}
function result($code,$msg,$nextCode=0){
return json([
"actionCode" => 0,
"errCode" => $code,
"errMsg" => $msg,
"errDlt" => '',
"nextCode"=> $nextCode
]);
}
function success(){
return $this->result(0,"");
}
function error($errCode=0,$errMsg='',$nextCode=1){
return $this->result($errCode,$errMsg,$nextCode);
}
function getSdk(){
if($this->sdk){
return $this->sdk;
}
$this->sdk = new \support\OpenImSdk\Client([
'host' => config('openim.server'), // OpenIM API地址
'secret' => config('openim.secret'), // OpenIM密钥
]);
return $this->sdk;
}
}
+63
View File
@@ -0,0 +1,63 @@
<?php
namespace app\controller;
use support\exception\BusinessException;
use support\Request;
use support\Response;
use Exception;
use app\model\Archives as ArchivesModel;
class IndexController extends Crud
{
/**
* 后台主页
* @param Request $request
* @return Response
* @throws BusinessException|Exception
*/
public function index(Request $request)
{
return view('public/index.html');
}
public function user(Request $request,$code)
{
cp($code);
return 'user';
}
public function group(Request $request,$code)
{
cp($code);
return 'group';
}
public function privacy_policy(Request $request)
{
return $this->siglepage($request);
}
public function aboutus(Request $request)
{
return $this->siglepage($request);
}
function siglepage($request){
$name = $request->action;
if(!$name){
return $this->error(__("Article does not exist"));
}
/** @var ArchivesModel $vo */
$vo = ArchivesModel::where('name',$name)->find();
if(!$vo) {
return $this->error(__("Article does not exist"));
}
$addon = \app\model\Content::where('id', $vo->id)->find()->toArray();
if ($addon) {
$vo->setAddonData($addon);
}
return view('common/siglepage',[
'vo' => $vo
]);
}
}
+672
View File
@@ -0,0 +1,672 @@
<?php
namespace app\controller;
use support\Request;
use support\Response;
/**
* 指标
* 提供Prometheus格式的监控指标
*/
class MetricsController extends Base
{
/**
* 无需登录及鉴权的方法
* @var array
*/
protected $noNeedLogin = ['index'];
/**
* 指标数据存储
*/
protected static $metrics = [];
/**
* 指标类型定义
*/
const TYPE_COUNTER = 'counter';
const TYPE_GAUGE = 'gauge';
const TYPE_HISTOGRAM = 'histogram';
const TYPE_SUMMARY = 'summary';
/**
* 获取指标数据
*
* @param Request $request
* @return Response
*/
public function index(Request $request): Response
{
$metrics = [];
// 系统基础指标
$metrics[] = $this->getSystemMetrics();
// PHP运行指标
$metrics[] = $this->getPhpMetrics();
// 应用业务指标
$metrics[] = $this->getAppMetrics();
// 数据库指标
$metrics[] = $this->getDatabaseMetrics();
// Redis指标
$metrics[] = $this->getRedisMetrics();
// MongoDB指标
$metrics[] = $this->getMongoDBMetrics();
// HTTP请求指标
$metrics[] = $this->getHttpMetrics();
$content = implode("\n", array_filter($metrics));
return response($content, 200, [
'Content-Type' => 'text/plain; charset=utf-8'
]);
}
/**
* 获取系统指标
*/
protected function getSystemMetrics(): string
{
$metrics = [];
// 内存使用
$memoryUsage = memory_get_usage(true);
$memoryPeak = memory_get_peak_usage(true);
$memoryLimit = $this->getBytes(ini_get('memory_limit'));
$metrics[] = $this->formatGauge('webman_memory_usage_bytes', 'Current memory usage in bytes', [], $memoryUsage);
$metrics[] = $this->formatGauge('webman_memory_peak_bytes', 'Peak memory usage in bytes', [], $memoryPeak);
$metrics[] = $this->formatGauge('webman_memory_limit_bytes', 'Memory limit in bytes', [], $memoryLimit);
// CPU负载
if (function_exists('sys_getloadavg')) {
$load = sys_getloadavg();
$metrics[] = $this->formatGauge('webman_system_load1', 'System load average over 1 minute', [], $load[0]);
$metrics[] = $this->formatGauge('webman_system_load5', 'System load average over 5 minutes', [], $load[1]);
$metrics[] = $this->formatGauge('webman_system_load15', 'System load average over 15 minutes', [], $load[2]);
}
// 磁盘使用
$diskTotal = disk_total_space('/');
$diskFree = disk_free_space('/');
$diskUsed = $diskTotal - $diskFree;
$metrics[] = $this->formatGauge('webman_disk_total_bytes', 'Total disk space in bytes', [], $diskTotal);
$metrics[] = $this->formatGauge('webman_disk_free_bytes', 'Free disk space in bytes', [], $diskFree);
$metrics[] = $this->formatGauge('webman_disk_used_bytes', 'Used disk space in bytes', [], $diskUsed);
return implode("\n", $metrics);
}
/**
* 获取PHP指标
*/
protected function getPhpMetrics(): string
{
$metrics = [];
// PHP版本信息
$metrics[] = $this->formatGauge('webman_php_version_info', 'PHP version information', [
'version' => PHP_VERSION,
'sapi' => PHP_SAPI
], 1);
// OPcache指标
if (function_exists('opcache_get_status')) {
$opcache = opcache_get_status(false);
if ($opcache) {
$metrics[] = $this->formatGauge('webman_opcache_enabled', 'OPcache enabled status', [], $opcache['opcache_enabled'] ? 1 : 0);
$metrics[] = $this->formatGauge('webman_opcache_hit_rate', 'OPcache hit rate', [], $opcache['opcache_statistics']['opcache_hit_rate'] ?? 0);
$metrics[] = $this->formatCounter('webman_opcache_hits_total', 'Total OPcache hits', [], $opcache['opcache_statistics']['hits'] ?? 0);
$metrics[] = $this->formatCounter('webman_opcache_misses_total', 'Total OPcache misses', [], $opcache['opcache_statistics']['misses'] ?? 0);
}
}
// 运行时间
if (function_exists('posix_times')) {
$times = posix_times();
$metrics[] = $this->formatCounter('webman_cpu_user_seconds_total', 'Total user CPU time', [], $times['utime'] ?? 0);
$metrics[] = $this->formatCounter('webman_cpu_system_seconds_total', 'Total system CPU time', [], $times['stime'] ?? 0);
}
return implode("\n", $metrics);
}
/**
* 获取应用业务指标
*/
protected function getAppMetrics(): string
{
$metrics = [];
// 应用信息
$metrics[] = $this->formatGauge('webman_app_info', 'Application information', [
'name' => config('app.name', 'webman'),
'version' => config('app.version', '1.0.0'),
'env' => config('app.debug') ? 'development' : 'production'
], 1);
// 启动时间
$metrics[] = $this->formatGauge('webman_start_time_seconds', 'Application start time', [], defined('WEBMAN_START_TIME') ? WEBMAN_START_TIME : time());
// 运行时长
$startTime = defined('WEBMAN_START_TIME') ? WEBMAN_START_TIME : time();
$uptime = time() - $startTime;
$metrics[] = $this->formatCounter('webman_uptime_seconds_total', 'Total uptime in seconds', [], $uptime);
return implode("\n", $metrics);
}
/**
* 获取数据库指标
*/
protected function getDatabaseMetrics(): string
{
$metrics = [];
try {
// 数据库连接信息
$dbConfig = config('database.connections.mysql');
if ($dbConfig) {
$metrics[] = $this->formatGauge('webman_db_config_info', 'Database configuration', [
'host' => $dbConfig['hostname'] ?? 'unknown',
'database' => $dbConfig['database'] ?? 'unknown',
'charset' => $dbConfig['charset'] ?? 'utf8'
], 1);
}
// 数据库连接状态 - 连接成功
$metrics[] = $this->formatGauge('webman_db_up', 'Database connection status', [], 1);
// 尝试获取数据库状态
$db = \think\facade\Db::connect();
// 1. 全局状态指标
$status = $db->query('SHOW GLOBAL STATUS');
$statusMap = [];
foreach ($status as $item) {
$statusMap[$item['Variable_name']] = $item['Value'];
}
// 关键指标
if (isset($statusMap['Threads_connected'])) {
$metrics[] = $this->formatGauge('webman_db_threads_connected', 'Number of currently connected threads', [], (int)$statusMap['Threads_connected']);
}
if (isset($statusMap['Threads_running'])) {
$metrics[] = $this->formatGauge('webman_db_threads_running', 'Number of threads running', [], (int)$statusMap['Threads_running']);
}
if (isset($statusMap['Queries'])) {
$metrics[] = $this->formatCounter('webman_db_queries_total', 'Total number of queries', [], (int)$statusMap['Queries']);
}
if (isset($statusMap['Slow_queries'])) {
$metrics[] = $this->formatCounter('webman_db_slow_queries_total', 'Total number of slow queries', [], (int)$statusMap['Slow_queries']);
}
if (isset($statusMap['Uptime'])) {
$metrics[] = $this->formatCounter('webman_db_uptime_seconds', 'Database uptime in seconds', [], (int)$statusMap['Uptime']);
}
if (isset($statusMap['Innodb_buffer_pool_pages_data'])) {
$metrics[] = $this->formatGauge('webman_db_innodb_buffer_pool_pages_data', 'Number of pages containing data', [], (int)$statusMap['Innodb_buffer_pool_pages_data']);
}
if (isset($statusMap['Innodb_buffer_pool_pages_free'])) {
$metrics[] = $this->formatGauge('webman_db_innodb_buffer_pool_pages_free', 'Number of free pages', [], (int)$statusMap['Innodb_buffer_pool_pages_free']);
}
if (isset($statusMap['Innodb_buffer_pool_pages_total'])) {
$metrics[] = $this->formatGauge('webman_db_innodb_buffer_pool_pages_total', 'Total number of pages', [], (int)$statusMap['Innodb_buffer_pool_pages_total']);
}
if (isset($statusMap['Innodb_row_reads'])) {
$metrics[] = $this->formatCounter('webman_db_innodb_row_reads_total', 'Number of rows read', [], (int)$statusMap['Innodb_row_reads']);
}
if (isset($statusMap['Innodb_row_writes'])) {
$metrics[] = $this->formatCounter('webman_db_innodb_row_writes_total', 'Number of rows written', [], (int)$statusMap['Innodb_row_writes']);
}
if (isset($statusMap['Com_select'])) {
$metrics[] = $this->formatCounter('webman_db_com_select_total', 'Number of SELECT statements', [], (int)$statusMap['Com_select']);
}
if (isset($statusMap['Com_insert'])) {
$metrics[] = $this->formatCounter('webman_db_com_insert_total', 'Number of INSERT statements', [], (int)$statusMap['Com_insert']);
}
if (isset($statusMap['Com_update'])) {
$metrics[] = $this->formatCounter('webman_db_com_update_total', 'Number of UPDATE statements', [], (int)$statusMap['Com_update']);
}
if (isset($statusMap['Com_delete'])) {
$metrics[] = $this->formatCounter('webman_db_com_delete_total', 'Number of DELETE statements', [], (int)$statusMap['Com_delete']);
}
// 2. 数据库大小
$databases = $db->query('SHOW DATABASES WHERE `Database` NOT IN ("information_schema", "mysql", "performance_schema", "sys")');
foreach ($databases as $dbInfo) {
$dbName = $dbInfo['Database'];
$sizeResult = $db->query("SELECT table_schema AS 'database', SUM(data_length + index_length) AS 'size_bytes' FROM information_schema.tables WHERE table_schema = '{$dbName}' GROUP BY table_schema");
if (isset($sizeResult[0]['size_bytes'])) {
$metrics[] = $this->formatGauge('webman_db_database_size_bytes', 'Database size in bytes', [
'database' => $dbName
], (int)$sizeResult[0]['size_bytes']);
}
}
// 3. 表状态
$tables = $db->query('SHOW TABLE STATUS FROM `' . ($dbConfig['database'] ?? 'test') . '`');
foreach ($tables as $table) {
$tableName = $table['Name'];
$metrics[] = $this->formatGauge('webman_db_table_rows', 'Number of rows in table', [
'table' => $tableName
], (int)$table['Rows']);
$metrics[] = $this->formatGauge('webman_db_table_data_length_bytes', 'Data length of table', [
'table' => $tableName
], (int)$table['Data_length']);
$metrics[] = $this->formatGauge('webman_db_table_index_length_bytes', 'Index length of table', [
'table' => $tableName
], (int)$table['Index_length']);
}
} catch (\Exception $e) {
// 数据库连接失败,记录错误
$metrics[] = $this->formatGauge('webman_db_up', 'Database connection status', [], 0);
}
return implode("\n", $metrics);
}
/**
* 获取Redis指标
*/
protected function getRedisMetrics(): string
{
$metrics = [];
try {
$redis = \support\think\Cache::handler();
$info = $redis->info();
// Redis连接状态
$metrics[] = $this->formatGauge('webman_redis_up', 'Redis connection status', [], 1);
// Redis版本
if (isset($info['redis_version'])) {
$metrics[] = $this->formatGauge('webman_redis_version_info', 'Redis version information', [
'version' => $info['redis_version']
], 1);
}
// 内存使用
if (isset($info['used_memory'])) {
$metrics[] = $this->formatGauge('webman_redis_memory_used_bytes', 'Redis memory used in bytes', [], (int)$info['used_memory']);
}
if (isset($info['used_memory_peak'])) {
$metrics[] = $this->formatGauge('webman_redis_memory_peak_bytes', 'Redis peak memory used in bytes', [], (int)$info['used_memory_peak']);
}
if (isset($info['used_memory_rss'])) {
$metrics[] = $this->formatGauge('webman_redis_memory_rss_bytes', 'Redis RSS memory used in bytes', [], (int)$info['used_memory_rss']);
}
if (isset($info['mem_fragmentation_ratio'])) {
$metrics[] = $this->formatGauge('webman_redis_memory_fragmentation_ratio', 'Redis memory fragmentation ratio', [], (float)$info['mem_fragmentation_ratio']);
}
// 连接数
if (isset($info['connected_clients'])) {
$metrics[] = $this->formatGauge('webman_redis_connected_clients', 'Number of connected clients', [], (int)$info['connected_clients']);
}
if (isset($info['client_recent_max_input_buffer'])) {
$metrics[] = $this->formatGauge('webman_redis_client_max_input_buffer_bytes', 'Maximum input buffer size', [], (int)$info['client_recent_max_input_buffer']);
}
if (isset($info['client_recent_max_output_buffer'])) {
$metrics[] = $this->formatGauge('webman_redis_client_max_output_buffer_bytes', 'Maximum output buffer size', [], (int)$info['client_recent_max_output_buffer']);
}
// 命令统计
if (isset($info['total_commands_processed'])) {
$metrics[] = $this->formatCounter('webman_redis_commands_processed_total', 'Total commands processed', [], (int)$info['total_commands_processed']);
}
if (isset($info['instantaneous_ops_per_sec'])) {
$metrics[] = $this->formatGauge('webman_redis_instantaneous_ops_per_sec', 'Instantaneous operations per second', [], (int)$info['instantaneous_ops_per_sec']);
}
// 键数量
if (isset($info['db0'])) {
preg_match('/keys=(\d+)/', $info['db0'], $matches);
if (isset($matches[1])) {
$metrics[] = $this->formatGauge('webman_redis_keys_total', 'Total number of keys', [], (int)$matches[1]);
}
}
// 运行时间
if (isset($info['uptime_in_seconds'])) {
$metrics[] = $this->formatCounter('webman_redis_uptime_seconds', 'Redis uptime in seconds', [], (int)$info['uptime_in_seconds']);
}
// 过期键
if (isset($info['expired_keys'])) {
$metrics[] = $this->formatCounter('webman_redis_expired_keys_total', 'Total number of expired keys', [], (int)$info['expired_keys']);
}
if (isset($info['evicted_keys'])) {
$metrics[] = $this->formatCounter('webman_redis_evicted_keys_total', 'Total number of evicted keys', [], (int)$info['evicted_keys']);
}
// 命中和未命中
if (isset($info['keyspace_hits'])) {
$metrics[] = $this->formatCounter('webman_redis_keyspace_hits_total', 'Total number of keyspace hits', [], (int)$info['keyspace_hits']);
}
if (isset($info['keyspace_misses'])) {
$metrics[] = $this->formatCounter('webman_redis_keyspace_misses_total', 'Total number of keyspace misses', [], (int)$info['keyspace_misses']);
}
// 复制状态
if (isset($info['role'])) {
$metrics[] = $this->formatGauge('webman_redis_role', 'Redis role (master=1, slave=2)', [
'role' => $info['role']
], $info['role'] === 'master' ? 1 : 2);
}
} catch (\Exception $e) {
$metrics[] = $this->formatGauge('webman_redis_up', 'Redis connection status', [], 0);
}
return implode("\n", $metrics);
}
/**
* 获取MongoDB指标
*/
protected function getMongoDBMetrics(): string
{
$metrics = [];
try {
// 检查MongoDB扩展是否安装
if (!class_exists('MongoDB\\Client')) {
$metrics[] = $this->formatGauge('webman_mongodb_up', 'MongoDB connection status', [], 0);
$metrics[] = $this->formatGauge('webman_mongodb_error', 'MongoDB error', [
'error' => 'extension_not_installed'
], 1);
return implode("\n", $metrics);
}
// 尝试连接MongoDB
$mongoClient = new \MongoDB\Client('mongodb://localhost:27017');
// MongoDB连接状态
$metrics[] = $this->formatGauge('webman_mongodb_up', 'MongoDB connection status', [], 1);
// 获取服务器状态
$serverStatus = $mongoClient->selectDatabase('admin')->command(['serverStatus' => 1]);
$status = $serverStatus->toArray()[0];
// 版本信息
if (isset($status['version'])) {
$metrics[] = $this->formatGauge('webman_mongodb_version_info', 'MongoDB version information', [
'version' => $status['version']
], 1);
}
// 连接数
if (isset($status['connections'])) {
$metrics[] = $this->formatGauge('webman_mongodb_connections_current', 'Current number of connections', [], (int)$status['connections']['current']);
$metrics[] = $this->formatGauge('webman_mongodb_connections_available', 'Available number of connections', [], (int)$status['connections']['available']);
}
// 内存使用
if (isset($status['mem'])) {
if (isset($status['mem']['resident'])) {
$metrics[] = $this->formatGauge('webman_mongodb_memory_resident_bytes', 'Resident memory usage in bytes', [], (int)$status['mem']['resident'] * 1024 * 1024);
}
if (isset($status['mem']['virtual'])) {
$metrics[] = $this->formatGauge('webman_mongodb_memory_virtual_bytes', 'Virtual memory usage in bytes', [], (int)$status['mem']['virtual'] * 1024 * 1024);
}
}
// 操作统计
if (isset($status['opcounters'])) {
if (isset($status['opcounters']['insert'])) {
$metrics[] = $this->formatCounter('webman_mongodb_ops_insert_total', 'Total insert operations', [], (int)$status['opcounters']['insert']);
}
if (isset($status['opcounters']['query'])) {
$metrics[] = $this->formatCounter('webman_mongodb_ops_query_total', 'Total query operations', [], (int)$status['opcounters']['query']);
}
if (isset($status['opcounters']['update'])) {
$metrics[] = $this->formatCounter('webman_mongodb_ops_update_total', 'Total update operations', [], (int)$status['opcounters']['update']);
}
if (isset($status['opcounters']['delete'])) {
$metrics[] = $this->formatCounter('webman_mongodb_ops_delete_total', 'Total delete operations', [], (int)$status['opcounters']['delete']);
}
}
// 集合和数据库统计
$databases = $mongoClient->listDatabases();
foreach ($databases as $dbInfo) {
$dbName = $dbInfo->getName();
if (!in_array($dbName, ['admin', 'local', 'config'])) {
$db = $mongoClient->selectDatabase($dbName);
$collections = $db->listCollections();
$collectionCount = 0;
foreach ($collections as $collection) {
$collectionCount++;
$collectionName = $collection->getName();
$stats = $db->command(['collStats' => $collectionName]);
$collStats = $stats->toArray()[0];
if (isset($collStats['count'])) {
$metrics[] = $this->formatGauge('webman_mongodb_collection_documents', 'Number of documents in collection', [
'database' => $dbName,
'collection' => $collectionName
], (int)$collStats['count']);
}
if (isset($collStats['size'])) {
$metrics[] = $this->formatGauge('webman_mongodb_collection_size_bytes', 'Size of collection in bytes', [
'database' => $dbName,
'collection' => $collectionName
], (int)$collStats['size']);
}
}
$metrics[] = $this->formatGauge('webman_mongodb_database_collections', 'Number of collections in database', [
'database' => $dbName
], $collectionCount);
}
}
} catch (\Exception $e) {
$metrics[] = $this->formatGauge('webman_mongodb_up', 'MongoDB connection status', [], 0);
$metrics[] = $this->formatGauge('webman_mongodb_error', 'MongoDB error', [
'error' => 'connection_failed'
], 1);
}
return implode("\n", $metrics);
}
/**
* 获取HTTP请求指标
*/
protected function getHttpMetrics(): string
{
$metrics = [];
// 请求计数器(这里需要从中间件或日志中统计,示例使用静态数据)
$metrics[] = $this->formatCounter('webman_http_requests_total', 'Total HTTP requests', [
'method' => 'GET',
'status' => '200'
], 0);
$metrics[] = $this->formatCounter('webman_http_requests_total', 'Total HTTP requests', [
'method' => 'POST',
'status' => '200'
], 0);
// 请求处理时间(使用Gauge类型)
$metrics[] = $this->formatGauge('webman_http_request_duration_seconds', 'HTTP request duration in seconds', [
'method' => 'GET',
'path' => '/metrics'
], 0);
// 响应大小
$metrics[] = $this->formatCounter('webman_http_response_size_bytes_total', 'Total HTTP response size in bytes', [], 0);
// 请求大小
$metrics[] = $this->formatCounter('webman_http_request_size_bytes_total', 'Total HTTP request size in bytes', [], 0);
return implode("\n", $metrics);
}
/**
* 格式化Counter类型指标
*/
protected function formatCounter(string $name, string $help, array $labels, float $value): string
{
$output = [];
$output[] = "# HELP {$name} {$help}";
$output[] = "# TYPE {$name} counter";
$labelStr = $this->formatLabels($labels);
$output[] = $name . $labelStr . ' ' . $value;
return implode("\n", $output);
}
/**
* 格式化Gauge类型指标
*/
protected function formatGauge(string $name, string $help, array $labels, float $value): string
{
$output = [];
$output[] = "# HELP {$name} {$help}";
$output[] = "# TYPE {$name} gauge";
$labelStr = $this->formatLabels($labels);
$output[] = $name . $labelStr . ' ' . $value;
return implode("\n", $output);
}
/**
* 格式化Histogram类型指标
*/
protected function formatHistogram(string $name, string $help, array $labels, array $buckets): string
{
$output = [];
$output[] = "# HELP {$name} {$help}";
$output[] = "# TYPE {$name} histogram";
// 如果没有提供桶数据,返回空直方图
if (empty($buckets)) {
$labelStr = $this->formatLabels($labels);
$output[] = $name . '_bucket' . $labelStr . ',le="+Inf" 0';
$output[] = $name . '_sum' . $labelStr . ' 0';
$output[] = $name . '_count' . $labelStr . ' 0';
}
return implode("\n", $output);
}
/**
* 格式化标签
*/
protected function formatLabels(array $labels): string
{
if (empty($labels)) {
return '';
}
$pairs = [];
foreach ($labels as $key => $value) {
$pairs[] = $key . '="' . $this->escapeLabel($value) . '"';
}
return '{' . implode(',', $pairs) . '}';
}
/**
* 转义标签值
*/
protected function escapeLabel(string $value): string
{
return str_replace(['\\', '"', "\n"], ['\\\\', '\\"', '\\n'], $value);
}
/**
* 将PHP内存限制字符串转换为字节数
*/
protected function getBytes(string $val): int
{
$val = trim($val);
$last = strtolower($val[strlen($val) - 1]);
$val = (int) $val;
switch ($last) {
case 'g':
$val *= 1024;
case 'm':
$val *= 1024;
case 'k':
$val *= 1024;
}
return $val;
}
/**
* 增加计数器值(供其他控制器调用)
*
* @param string $name
* @param array $labels
* @param float $value
*/
public static function incCounter(string $name, array $labels = [], float $value = 1): void
{
$key = $name . json_encode($labels);
if (!isset(self::$metrics[$key])) {
self::$metrics[$key] = [
'type' => 'counter',
'name' => $name,
'labels' => $labels,
'value' => 0
];
}
self::$metrics[$key]['value'] += $value;
}
/**
* 设置Gauge值(供其他控制器调用)
*
* @param string $name
* @param float $value
* @param array $labels
*/
public static function setGauge(string $name, float $value, array $labels = []): void
{
$key = $name . json_encode($labels);
self::$metrics[$key] = [
'type' => 'gauge',
'name' => $name,
'labels' => $labels,
'value' => $value
];
}
/**
* 记录HTTP请求指标(供中间件调用)
*
* @param string $method
* @param string $path
* @param int $status
* @param float $duration
* @param int $responseSize
*/
public static function recordHttpRequest(string $method, string $path, int $status, float $duration, int $responseSize = 0): void
{
$labels = [
'method' => $method,
'path' => $path,
'status' => (string)$status
];
// 请求总数
self::incCounter('webman_http_requests_total', $labels);
// 响应大小
self::incCounter('webman_http_response_size_bytes_total', $labels, $responseSize);
}
}
+42
View File
@@ -0,0 +1,42 @@
<?php
namespace app\controller;
use app\model\Order;
use app\model\Withdrawl as WithdrawlModel;
use app\model\Address as AddressModel;
use support\exception\BusinessException;
use support\Request;
use support\Response;
use Throwable;
use Web3\Contracts\Types\Address as TypesAddress;
use Workerman\Worker;
class UtilsController extends Crud
{
public function i18n(Request $request): Response
{
$locale = $_GET['locale'];
$key = $_GET['key'];
$langArr=[
'zh_CN',
'zh_TW',
'fi_FI',
'ja_JP',
'ko_KR',
'en_US',
];
foreach($langArr as $lang){
$fn = "public/h5/i18n/".$lang.'.json';
$json = json_decode(file_get_contents($fn), true);
echo $locale,$key;
if(!isset($json[$key])){
$json[$key] = $key;
file_put_contents($fn, json_encode($json, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT));
}
}
return $this->success(__('successful'));
}
}
+107
View File
@@ -0,0 +1,107 @@
<?php
namespace app\enum;
enum BalanceType: int
{
/**
* 充值
*/
case RECHARGE = 100;
/**
* 充值卡密
*/
case RECHARGE_CARD = 101;
/**
* 提现
*/
case WITHDRAWAL = 200;
/**
* 提现退回
*/
case WITHDRAWAL_REJECT = 201;
/**
* 购买卡密
*/
case CDKEY = 202;
/**
* 站内转账
*/
case TRANSFER = 300;
/**
* 兑换
*/
case EXCHANGE = 301;
/**
* 签到
*/
case SIGNIN = 302;
/**
* 邀请新用户注册
*/
case INVITE_NEW_USER = 305;
/**
* 购买产品
*/
case PRODUCT_BUY = 401;
/**
* 购买角色
*/
case PURCHASE_ROLE = 402;
/**
* 购买积分卡
*/
case GIFT_BUY = 407;
/**
* 购买积分卡
*/
case SEE_POINT_AWARD = 901;
/**
* 获取所有类型映射数组
*/
public static function toArray(): array
{
return [
self::RECHARGE->value => __('充值'),
self::RECHARGE_CARD->value => __('充值卡密'),
self::WITHDRAWAL->value => __('提现'),
self::WITHDRAWAL_REJECT->value => __('提现退回'),
self::CDKEY->value => __('购买卡密'),
self::TRANSFER->value => __('站内转账'),
self::EXCHANGE->value => __('兑换'),
self::SIGNIN->value => __('签到'),
self::INVITE_NEW_USER->value => __('邀请新用户注册'),
self::PRODUCT_BUY->value => __('购买产品'),
self::PURCHASE_ROLE->value => __('购买角色'),
self::GIFT_BUY->value => __('购买积分卡'),
self::SEE_POINT_AWARD->value => __('见点奖'),
];
}
/**
* 获取当前类型的描述文本
*/
public function getDescription(): string
{
return self::toArray()[$this->value];
}
/**
* 安全地从值创建枚举实例
*/
public static function tryFromValue(int $value): ?self
{
return self::tryFrom($value);
}
}
+24
View File
@@ -0,0 +1,24 @@
<?php
namespace app\Enum;
trait BaseEnum {
public static function toArray(): array
{
return [
];
}
/**
* 获取当前状态的描述文本
*/
public function getDescription(): string
{
return self::toArray()[$this->value];
}
/**
* 安全地从值创建枚举实例
*/
public static function tryFromValue(int $value): ?self
{
return self::tryFrom($value);
}
}
+39
View File
@@ -0,0 +1,39 @@
<?php
namespace app\enum;
enum OrderStatus: int
{
case CLOSE = 1;
case PAID = 2;
case CLUBS = 3;
case SPADES = 4;
/**
* 获取所有状态映射数组
*/
public static function toArray(): array
{
return [
self::CLOSE->value => __('失败'),
self::PAID->value => __('取消'),
self::CLUBS->value => __('等待支付'),
self::SPADES->value => __('完成'),
];
}
/**
* 获取当前状态的描述文本
*/
public function getDescription(): string
{
return self::toArray()[$this->value];
}
/**
* 安全地从值创建枚举实例
*/
public static function tryFromValue(int $value): ?self
{
return self::tryFrom($value);
}
}
+32
View File
@@ -0,0 +1,32 @@
<?php
namespace app\enum\Payment;
use app\enum\BaseEnum;
/**
* 支付方式
*/
enum Method: string
{
use BaseEnum;
/**
* 微信
*/
case WECHAT = 'weichat';
/**
* 支付宝
*/
case ALIPAY = 'alipay';
/**
* 获取所有状态映射数组
*/
public static function toArray(): array
{
return [
self::WECHAT->value => __('微信'),
self::ALIPAY->value => __('支付宝')
];
}
}
+61
View File
@@ -0,0 +1,61 @@
<?php
namespace app\enum\Payment;
use app\enum\BaseEnum;
/**
* 支付状态常量
*/
enum Status: string
{
/**
* 等待支付
*/
case CREATED = 'created';
/**
* 成功
*/
case SUCCESS = 'success';
/**
* 完成
*/
case COMPLETE = 'complete';
/**
* 失败
*/
case FAIL = 'fail';
/**
* 退款
*/
case REFUNDED = 'refunded';
/**
* 获取所有状态映射数组
*/
public static function toArray(): array
{
return [
self::CREATED->value => __('等待支付'),
self::SUCCESS->value => __('成功'),
self::FAIL->value => __('失败'),
self::COMPLETE->value => __('完成'),
self::REFUNDED->value => __('退款'),
];
}
/**
* 获取当前状态的描述文本
*/
public function getDescription(): string
{
return self::toArray()[$this->value];
}
/**
* 安全地从值创建枚举实例
*/
public static function tryFromValue(int $value): ?self
{
return self::tryFrom($value);
}
}
+42
View File
@@ -0,0 +1,42 @@
<?php
namespace app\enum\Payment;
use app\enum\BaseEnum;
/**
* 支付用途
*/
enum Type: string
{
use BaseEnum;
/**
* 充值
*/
case RECHARGE = 'recharge';
/**
* 商品
*/
case GOODS = 'goods';
/**
* 服务
*/
case SERVICE = 'service';
/**
* 其他
*/
case OTHER = 'other';
/**
* 获取所有状态映射数组
*/
public static function toArray(): array
{
return [
self::RECHARGE->value => __('充值'),
self::GOODS->value => __('商品'),
self::SERVICE->value => __('服务'),
self::OTHER->value => __('其他')
];
}
}
+60
View File
@@ -0,0 +1,60 @@
<?php
namespace app\enum;
enum RechargeStatus: int
{
/**
* 失败
*/
case FAIL = -2;
/**
* 取消
*/
case CANCEL = -1;
/**
* 等待支付
*/
case CREATED = 0;
/**
* 支付完成
*/
case PAID = 1;
/**
* 完成
*/
case COMPLETE = 2;
/**
* 获取所有状态映射数组
*/
public static function toArray(): array
{
return [
self::FAIL->value => __('失败'),
self::CANCEL->value => __('取消'),
self::CREATED->value => __('等待支付'),
self::COMPLETE->value => __('完成'),
];
}
/**
* 获取当前状态的描述文本
*/
public function getDescription(): string
{
return self::toArray()[$this->value];
}
/**
* 安全地从值创建枚举实例
*/
public static function tryFromValue(int $value): ?self
{
return self::tryFrom($value);
}
}
+61
View File
@@ -0,0 +1,61 @@
<?php
namespace app\enum;
enum ServerStatus : int
{
/**
* 等待开始
*/
case WAITING = 0;
/**
* 进行中
*/
case WORKING = 1;
/**
* 审核中
*/
case AUDITING = 2;
/**
* 结算中
*/
case SETTLEMENT = 3;
/**
* 任务完成
*/
case COMPLETE = 4;
/**
* 任务失败
*/
case FAILED = -1;
/**
* 获取所有状态映射数组
*/
public static function toArray(): array
{
return [
self::WAITING->value => __('waiting'),
self::WORKING->value => __('working'),
self::AUDITING->value => __('auditing'),
self::SETTLEMENT->value => __('settlement'),
self::COMPLETE->value => __('complete'),
self::FAILED->value => __('failed'),
];
}
/**
* 获取当前状态的描述文本
*/
public function getDescription(): string
{
return self::toArray()[$this->value];
}
/**
* 安全地从值创建枚举实例
*/
public static function tryFromValue(int $value): ?self
{
return self::tryFrom($value);
}
}
+55
View File
@@ -0,0 +1,55 @@
<?php
namespace app\enum;
enum WithdrawlStatus: int
{
/**
* 失败
*/
case FAIL = -2;
/**
* 驳回
*/
case REJECT = -1;
/**
* 等待审核
*/
case CREATED = 0;
/**
* 转账中
*/
case TRANSFERRING = 1;
/**
* 完成
*/
case COMPLETE = 2;
// 获取所有状态描述映射
public static function toArray(): array
{
return [
self::FAIL->value => __('失败'),
self::REJECT->value => __('驳回'),
self::CREATED->value => __('等待审核'),
self::TRANSFERRING->value => __('转账中'),
self::COMPLETE->value => __('完成'),
];
}
// 获取当前状态的描述
public function getDescription(): string
{
return self::toArray()[$this->value];
}
// 从值创建枚举实例(带安全检测)
public static function tryFromValue(int $value): ?self
{
return self::tryFrom($value);
}
}
+29
View File
@@ -0,0 +1,29 @@
<?php
namespace app\event;
use app\model\User as UserModel;
use support\think\Db;
use Request;
class Card{
function create($row){
$cdkeys = [];
for ($i=0; $i < $row['total']; $i++) {
array_push($cdkeys,[
'type' => $row['type'],
'category_id' => $row['id'],
'account' => \support\Random::uuid(),
'password' => '123456',
'expires' => $row['expires'],
'days' => $row['days'],
'is_used' => 0,
'use_time' => 0,
'status' => 1,
'created_at' => time(),
'updated_at' => time(),
]);
}
$Cdkey = new \app\model\Cdkey;
$Cdkey->saveAll($cdkeys);
return $row;
}
}
+9
View File
@@ -0,0 +1,9 @@
<?php
namespace app\event;
use support\think\Db;
use Request;
class Main{
function index($data=[]){
return $data;
}
}
+151
View File
@@ -0,0 +1,151 @@
<?php
namespace app\event;
use support\think\Db;
use app\model\User as UserModel;
use Request;
/**
* 产品Hook
*/
class Product{
private $debug = false;
private $userinfo=[];
function buy($data=[]){
$questionnaire_count = $data->product->total * $data['quantity']; //问卷总数
UserModel::currency7($data['user_id'],$questionnaire_count,\app\enum\BalanceType::PRODUCT_BUY,'购买产品');
$user = UserModel::find($data["user_id"]);
//设置定时任务发放问卷,马上发放第一天的,然后每隔24小时发放一次,发放到第$data->product->days天
$assign_count = $data->product->assign_count;
addJob([
'action' => 'assign',
'user_id' => $data['user_id'],
'order_id' => $data['id'],
'amount' => $assign_count,
],'Questionnaire');
//$data =
//用户消费统计更新
cache_add('user_consume_total_'.$data['user_id'],$data['amount']);
$parent_id = $this->get_parent_id($data['user_id']);
if($parent_id){
// 销售奖励(直推)
// $reward = bcmul($data['amount'] ,2,0);
// UserModel::score($parent_id ,$reward,\app\enum\BalanceType::SALES_REWARD,$data['id']);
// cache_add('user_sales_reward_'.$parent_id,$reward); //销售奖励
$ancestorIds = Db::name('user_team')->where('descendant_id',$data['user_id'])
->column('ancestor_id');
if(!empty($ancestorIds)){
// 批量累加上级业绩
Db::name('user_extend')->whereIn('user_id',$ancestorIds)->where('user_id','<>',$data['user_id'])->update([
'sales' => Db::raw('sales+'.$data['amount'])
]);
$users = Db::name('user')->whereIn('id',$ancestorIds)->where('group',2)->column('id');
// 销售奖励(渠道)
foreach($users as $uid){
$reward = bcmul($data['amount'] ,8,0);
UserModel::score($uid ,$reward,\app\enum\BalanceType::SALES_REWARD,$data['id']);
cache_add('user_sales_reward_'.$uid,$reward); //销售奖励
}
}
return $data;
// 业绩与等级批量更新(事务内:所有上级的 sales 与 role_id
$this->updateAncestorsSalesAndLevel($data['user_id'],$data['amount']);
//我的用户表有role_id:角色ID,id:用户ID,详细可以查看\app\model\User的属性
//$data['user_id'] //购买人ID
//$data['amount'] //交易金额
//$data['role_id'] //用户角色
//上级user_id查询用$this->get_parent_id($user_id)
//$parent_user_role_id = \app\model\User::where('id',$this->get_parent_id($data['user_id']))->value('role_id');
//用户余额增加使用 User::money(用户ID,增加的金额,\app\enum\BalanceType::SALES_REWARD,$data['id']);
// 分佣规则
//从当前用户
// 代理佣金总和是交易金额的10%
// 极差收益,type=\app\enum\BalanceType::SALES_REWARD
// 极差收益总和是交易金额的20%
// 最多只能10个人分,如果上级用户级别小于上一个分润的人的级别,就跳过他,继续找下一个,始终补满10个人,直到级别等于10或者上级为空的时候才停止
// 每个人分润的比例是(极差收益比例-已经分出去的比例)*极差收益总和
//代码写在这里,不能去掉我的注释
$distributed_users = jicha($data['user_id'],$data['amount'],[0,0.02,0.04,0.06,0.08,0.1]);
foreach($distributed_users as $k=>$v){
UserModel::money($v['user_id'],$v['amount'],\app\enum\BalanceType::SALES_REWARD,$data['id']);
cache_add('user_income_total_'.$v['user_id'],$v['amount']); //收入统计
cache_add('user_sales_reward_'.$v['user_id'],$v['amount']); //销售奖励
}
}
return $data;
}
// 批量更新所有上级的业绩并根据阈值升级角色(单事务)
private function updateAncestorsSalesAndLevel($user_id,$delta_sales){
Db::startTrans();
try{
// 取出所有上级ID
$ancestorIds = Db::name('user_team')->where('descendant_id',$user_id)->column('ancestor_id');
if(empty($ancestorIds)){
Db::commit();
return;
}
// 批量累加上级业绩
Db::name('user_extend')->whereIn('user_id',$ancestorIds)->update([
'sales' => Db::raw('sales+'.$delta_sales)
]);
// 读取更新后的 sales 和当前 role_id
$extends = Db::name('user_extend')->whereIn('user_id',$ancestorIds)->column('sales','user_id');
$roles = Db::name('user')->whereIn('id',$ancestorIds)->column('role_id','id');
$levelArr = [0,5000,10000,50000,100000,200000];
$maxIdx = count($levelArr)-1;
$upgradeMap = [];
foreach($extends as $uid=>$sales){
cache_add('user_consume_reward_'.$uid,$sales);//个人消费统计
cache_add('team_consume_total_'.$uid,$sales); //团队总业绩
// 计算应达的最高等级
$newLevel = 0;
for($i=$maxIdx;$i>=0;$i--){
if($sales >= $levelArr[$i]){ $newLevel = $i; break; }
}
$current = isset($roles[$uid]) ? (int)$roles[$uid] : 0;
if($newLevel > $current){
$upgradeMap[$uid] = $newLevel;
}
}
// 批量升级(按新等级分组,可减少语句数)
if(!empty($upgradeMap)){
$levelToUsers = [];
foreach($upgradeMap as $uid=>$lvl){ $levelToUsers[$lvl][] = $uid; }
foreach($levelToUsers as $lvl=>$uids){
Db::name('user')->whereIn('id',$uids)->where('group',2)->where('role_id','<',$lvl)->update(['role_id'=>$lvl]);
}
}
Db::commit();
}catch(\Throwable $e){
Db::rollback();
throw $e;
}
}
function get_parent_id($user_id){
if($this->debug){
return $this->userinfo[''.$user_id]['parent_id'];
}
return get_parent_id($user_id);
}
function log($str){
$args = func_get_args();
if(is_string($args[0])){
$str = call_user_func_array('sprintf',$args);
if($this->debug){
return print_r($str);
}
log_alert($str);
}else{
$str = json_encode($args);
if($this->debug){
return print_r($str);
}
log_alert($str);
}
}
}
+23
View File
@@ -0,0 +1,23 @@
<?php
namespace app\event;
use support\think\Db;
use Request;
class Recharge{
function success($row=[]){
$data = $row;
if(!is_array($row)){
$data = $data->toArray();
}
cache_add('user_recharge_total_'.$data['user_id'],$data['amount']);
$parent_id = get_parent_id($data['user_id']);
if($parent_id){
//团队提现统计
cache_add('team_recharge_total_'.$parent_id,$data['amount']);
}
//系统每日提现统计
$date = date('Y-m-d');
cache_add('statistics_recharge_times_'.$date,1);
cache_add('statistics_recharge_amount_'.$date,$data['amount']);
return $row;
}
}
+198
View File
@@ -0,0 +1,198 @@
<?php
namespace app\event;
use support\think\Db;
use Request;
use Symfony\Component\Console\Input\Input;
class User{
function register_successed($user)
{
$_user = $user;
if(!is_array($_user)){
$_user = $_user->toArray();
}
$date = date('Y-m-d');
cache_add('statistics_register_'.$date,1);
$saveData = [
//'invite_code' => build_invite_code($_user['id']),
'invite_code' => \support\Encrypt::userIDencode($_user['id']),
'userID' => \support\Encrypt::userIDencode($_user['id'])
];
\app\model\User::where('id',$_user['id'])->update($saveData);
//创建扩展数据
Db::name('user_extend')->replace()->insert([
'user_id' => $_user['id'],
'consume' => 0,
// 'profile_banner' => '',
// 'moments_banner' => '',
// 'moments_allow_view_days'=>0,
]);
//管理直推人数和团队人数
if($_user['parent_id']){
parent_info( $_user['id'],[
'id' => $_user['parent_id'],
'username' => Db::name('user')->where('id',$_user['parent_id'])->value('username')
]);
build_user_team($_user);
//直属团队人数
Db::name('user_extend')->where('user_id',$_user['parent_id'])
->data([
'direct_total'=> Db::raw('direct_total+1')
])->save();
cache_add('team_direct_total_'.$_user['parent_id'],1);
//管理团队人数
$team_user_ids = Db::name('user_team')->where('descendant_id',$_user['id'])
->where('depth','>',0)
->order('depth','ASC')
->column('ancestor_id');
Db::name('user_extend')->whereIn('user_id',$team_user_ids)->data([
'team_total'=> Db::raw('team_total+1')
])->save();
$list = Db::name('user_extend')->whereIn('user_id',$team_user_ids)->field('user_id,team_total')->select();
foreach($list as $v){
cache('team_user_count_'.$v['user_id'],$v['team_total']);
}
}
}
function login_successed($data=[]){
$data = $this->profile($data);
/**
* @var \support\OpenImSdk\Client $IM
*/
$IM = request()->IM;
$imToken = $IM->auth->getUserToken($data['userID'],Input('platform'));
$data['imToken'] = $imToken['token'];
return $data;
}
function profile($user=[]){
$data = $user;
if(!is_array($data)){
$data = $data->toArray();
}
$last_see = $last_see ?? cache('last_see_'.$data['id']);
$count = 0;
$ff = [
'unread_count' => 0,
'userHeadImg' => null,
];
try {
$ff = Db::name('user_extend')->where('user_id',$user['id'])->field('moments_allow_view_days,profile_banner,moments_banner')->find();
$data['moments_allow_view_days'] = $ff['moments_allow_view_days'];
$data['moments_banner'] = $ff['moments_banner'];
$data['profile_banner'] = $ff['profile_banner'];
$ff['userHeadImg'] = $ff['moments_banner'];
} catch (\Exception $e) {
}
$ff['unread_count'] = $count ?:0;
$data['friend_settings'] = $ff;
return $data;
}
function changepwd_successed($data=[]){
return $data;
}
function change_trade_pwd_successed($data=[]){
return $data;
}
function logout_successed($data=[]){
return $data;
}
function delete_successed($data=[]){
return $data;
}
//用户角色组变化
function role_up($user=[]){
$data = $user;
if(!is_array($data)){
$data = $data->toArray();
}
//旗下会员总数
$team_user_ids = Db::name('user_team')->where('descendant_id',$user['id'])
->where('depth','>',0)
->order('depth','ASC')
->column('ancestor_id');
Db::name('user_extend')->whereIn('user_id',$team_user_ids)
->data([
'vip_total'=> Db::raw('vip_total+1')
])
->save();
$list = Db::name('user_extend')->whereIn('user_id',$team_user_ids)->field('user_id,vip_total')->select();
foreach($list as $v){
cache('team_vip_total_'.$v['user_id'],$v['vip_total']);
update_user_level($v['user_id'],$v['vip_total']);
}
// if(!$user->active){
// $user->active = 1;
// $user->save();
// cache_add('team_direct_total_'.$user->parent_id,1);
// }
return $user;
}
//用户角色组变化
function role_buy($data=[])
{
// $data = [
// 'role_id'=>1,
// 'user_id'=>100008,
// 'amount'=>1000
// ];
//addJob($data,'Settlement');
}
/**
* 分润逻辑
*
* @param int $userId 用户ID(充值用户)
* @param float $amount 充值金额
* @param int $orderId 订单ID
* @return bool
*/
function distributeProfit($user_id, $amount, $order_id) {
// 定义分润比例
$commissionRates = Config('site.indirect_referral_award');
// 启动事务
Db::startTrans();
try {
// 查询上三级用户
$ancestors = Db::name('user_team')
->alias('ut')
->join('user wu', 'ut.ancestor_id = wu.id') // 获取上级用户信息
->where('ut.descendant_id', $user_id)
->whereBetween('ut.depth', [1, 3]) // 限制深度为 1 到 3 级
->field('ut.ancestor_id, ut.depth')
->order('ut.depth ASC')
->select();
// 遍历上级用户,计算并记录分润
/** @var \app\model\UserTeam $ancestor */
foreach ($ancestors as $ancestor) {
$depth = $ancestor['depth'];
if (isset($commissionRates[$depth])) {
$commission = $amount * $commissionRates[$depth]; // 计算分润金额
// 插入分润记录
Db::table('z_commission_logs')->insert([
'user_id' => $ancestor['ancestor_id'],
'order_id' => $order_id,
'amount' => $commission,
'created_at' => date('Y-m-d H:i:s'),
]);
}
}
// 提交事务
Db::commit();
return true;
} catch (\Exception $e) {
// 回滚事务
Db::rollback();
throw $e; // 或记录日志以便调试
}
}
}
+75
View File
@@ -0,0 +1,75 @@
<?php
namespace app\event;
use app\model\User as UserModel;
use support\think\Db;
use Request;
class Withdrawl{
function success($row=[]){
$data = $row;
if(!is_array($row)){
$data = $data->toArray();
}
//用户提现统计
cache_add('user_withdrawl_total_'.$data['user_id'],$data['deduction_amount']);
// $parent_id = get_parent_id($data['user_id']);
// if($parent_id){
// //团队提现统计
// cache_add('team_withdrawl_total_'.$parent_id,$data['deduction_amount']);
// //提现奖励
// $distributed_users = jicha($data['user_id'],$data['deduction_amount'],[0,0.01,0.02,0.03,0.05,0.05]);
// foreach($distributed_users as $k=>$v){
// UserModel::money($v['user_id'],$v['amount'],\app\enum\BalanceType::OUTPUT_REWARD,$data['id']);
// cache_add('user_income_total_'.$v['user_id'],$v['amount']);
// cache_add('user_withdrawl_reward_'.$v['user_id'],$v['amount']);
// }
// }
//系统每日提现统计
$date = date('Y-m-d');
cache_add('statistics_withdrawl_times_'.$date,1);
cache_add('statistics_withdrawl_amount_'.$date,$data['deduction_amount']);
//cache_add('withdrawl_pass_total',$data['deduction_amount']);
//cache_add('withdrawl_pass_times',1);
return $row;
}
function reject($row=[]){
$data = $row;
if(!is_array($row)){
$data = $data->toArray();
}
// cache_add('withdrawl_pass_total',-$data['deduction_amount']);
// cache_add('withdrawl_pass_times',-1);
return $row;
}
function created($row=[]){
$data = $row;
if(!is_array($row)){
$data = $data->toArray();
}
return $row;
}
function transfering($row=[]){
$data = $row;
if(!is_array($row)){
$data = $data->toArray();
}
// cache_add('user_withdrawl_total_'.$data['user_id'],$data['deduction_amount']);
// $parent_id = get_parent_id($data['user_id']);
// if($parent_id){
// cache_add('team_withdrawl_total_'.$parent_id,$data['deduction_amount']);
// }
post(Config('pay.server').'/index/withdrawl',[
'appid' => config('pay.appid'),
'amount' => $data['recive_amount'],
'network' => $data['network'],
'out_trade_no' => $data['id'],
'address' => $data['address'],
'notify_url' => config('pay.notify_server').'/api/withdrawl/notify',
//'from_address' => $config['from_address'],
//'private_key' => $config['private_key'],
'env' => 'product'
]);
return $row;
}
}
+601
View File
@@ -0,0 +1,601 @@
<?php
use support\Env;
if (!function_exists('admin_path')) {
function admin_path(){
return '/app/admin';
}
}
if (!function_exists('cache')) {
/**
* 缓存管理
* @param string $name 缓存名称
* @param mixed $value 缓存值
* @param mixed $options 缓存参数
* @param string $tag 缓存标签
* @return mixed
*/
function cache(string $name = null, $value = '', $options = null, $tag = null)
{
if (is_null($name)) {
return '';
}
if ('' === $value) {
// 获取缓存
return str_starts_with($name, '?') ? \support\think\Cache::has(substr($name, 1)) : \support\think\Cache::get($name);
} elseif (is_null($value)) {
// 删除缓存
return \support\think\Cache::delete($name);
}
// 缓存数据
if (is_array($options)) {
$expire = $options['expire'] ?? null; //修复查询缓存无法设置过期时间
} else {
$expire = $options;
}
if (is_null($tag)) {
return \support\think\Cache::set($name, $value, $expire);
} else {
return \support\think\Cache::tag($tag)->set($name, $value, $expire);
}
}
}
if (!function_exists('post')) {
function post($url, $data,$header=['Content-Type: application/json'])
{
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
if($header){
curl_setopt($ch, CURLOPT_HTTPHEADER, $header); // 设置请求头
}
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
//curl_setopt($ch, CURLOPT_HTTPHEADER, array('Content-Type: application/json'));
$response = curl_exec($ch);
curl_close($ch);
return $response;
}
}
if (!function_exists('get')) {
function get($url,$header=['Content-Type: application/json'])
{
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
if($header){
curl_setopt($ch, CURLOPT_HTTPHEADER, $header); // 设置请求头
}
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
curl_close($ch);
return $response;
}
}
if (!function_exists('__')) {
function __(string $name = '', array $parameters = [], ?string $domain = null, ?string $locale = null)
{
// $locale = $locale ?: locale();
// if(!$domain){
// $request = Request();
// $request->app.','.$request->plugin.','.get_controller_name();
// $fn = '/resource/translations/'.$locale.'/'.($request->app ? $request->app .'/' : '').strtolower(get_controller_name());
// if($request->plugin){
// $fn = base_path('plugin').'/'.$request->plugin.$fn;
// }else{
// $fn = base_path($fn);
// }
// $domain = $fn;
// }
return trans($name, $parameters, $domain, $locale);
}
}
/**
* 跨域检测
*/
if (!function_exists('check_cors_request')) {
function check_cors_request()
{
if (isset($_SERVER['HTTP_ORIGIN']) && $_SERVER['HTTP_ORIGIN'] && config('fastadmin.cors_request_domain')) {
$info = parse_url($_SERVER['HTTP_ORIGIN']);
$domainArr = explode(',', config('fastadmin.cors_request_domain'));
$domainArr[] = request()->host(true);
if (in_array("*", $domainArr) || in_array($_SERVER['HTTP_ORIGIN'], $domainArr) || (isset($info['host']) && in_array($info['host'], $domainArr))) {
header("Access-Control-Allow-Origin: " . $_SERVER['HTTP_ORIGIN']);
} else {
abort('跨域检测无效', 403);
}
header('Access-Control-Allow-Credentials: true');
header('Access-Control-Max-Age: 86400');
if ($_SERVER['REQUEST_METHOD'] == 'OPTIONS') {
if (isset($_SERVER['HTTP_ACCESS_CONTROL_REQUEST_METHOD'])) {
header("Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS");
}
if (isset($_SERVER['HTTP_ACCESS_CONTROL_REQUEST_HEADERS'])) {
header("Access-Control-Allow-Headers: {$_SERVER['HTTP_ACCESS_CONTROL_REQUEST_HEADERS']}");
}
abort('', 200);
}
}
}
}
if (!function_exists('check_ip_allowed')) {
/**
* 检测IP是否允许
* @param string $ip IP地址
*/
function check_ip_allowed($ip = null)
{
$ip = is_null($ip) ? getRealIp() : $ip;
$forbiddenipArr = config('site.forbiddenip');
$forbiddenipArr = !$forbiddenipArr ? [] : $forbiddenipArr;
$forbiddenipArr = is_array($forbiddenipArr) ? $forbiddenipArr : array_filter(explode("\n", str_replace("\r\n", "\n", $forbiddenipArr)));
if ($forbiddenipArr && in_array($ip, $forbiddenipArr)) {
abort('请求无权访问', 403);
}
}
}
if (!function_exists('Hook')) {
function Hook(?string $key = null, mixed $default = null)
{
//return \Webman\Event\Event::dispatch($key, $default);//不会自动处理错误
return \Webman\Event\Event::emit($key, $default);//会自动处理错误
}
}
if (!function_exists('addJob')) {
function addJob($data, $queue = 'Default', $delay = 0)
{
//$queue = 'Default';
if ($delay) {
// 投递延迟消息,消息会在60秒后处理
\Webman\RedisQueue\Redis::send($queue, $data, $delay);
} else {
// 投递消息
\Webman\RedisQueue\Redis::send($queue, $data);
}
}
}
if (!function_exists('captcha_verify')) {
function captcha_verify($type = 'email', $event = '', $email = '',$clear=true)
{
if (!$event) {
abort(__('Captcha event is incorrect'));
}
$cache_key = 'captcha_' . $event . '_' . $email;
$expires = 5 * 60; //5分钟
$code = Request()->post('code');
$list = cache($cache_key);
$list = $list ?: [];
if (!isset($list[$code])) {
abort(__('Captcha is incorrect'));
}
if ($list[$code] + $expires < time()) {
unset($list[$code]);
cache($cache_key, $list);
abort(__('Captcha has expired'));
}
if($clear){
unset($list[$code]);
if ($event && $email) {
cache($cache_key, null);
} else {
cache($cache_key, $list);
}
}
return true;
}
}
if (!function_exists('cdnurl')) {
function cdnurl($path = '')
{
if(!$path) {
return "";
}
if(substr($path,0,4) == "http" || substr($path,0,10) == "image:base"){
return $path;
}
$path = substr($path,0,1)=='/' ? $path : '/'.$path;
return Config('site.cdnurl') . $path;
//return $path ? domain() . $path : $path;
}
}
if (!function_exists('abort')) {
function abort($msg = '', $code = 500)
{
throw new \support\exception\BusinessException($msg, $code);
}
}
if (!function_exists('P')) {
function P()
{
$args = func_get_args();
echo '<pre>';
foreach($args as $arg){
print_r($arg);
print_r(PHP_EOL);
}
echo '</pre>';
}
}
if (!function_exists('cp')) {
function cp()
{
$args = func_get_args();
foreach($args as $arg){
print_r($arg);
print("\t");
}
echo "\n";
}
}
if (!function_exists('formatAmount')) {
function formatAmount($amount, $wei = 4)
{
if (!$amount) {
return 0;
}
return round($amount, $wei);
}
}
if (!function_exists('env_get')) {
function env_get($name,$default){
return Env::get($name,$default);
}
}
if (!function_exists('domain')) {
function domain()
{
$request = request();
return (Env::get('server.https')?'https':'http').'://'.($request ? $request->host() : Env::get('server.domain',''));
}
}
if (!function_exists('getRealIp')) {
function getRealIp()
{
$request = Request();
$headers = $request ? $request->header() : [];
$ip = $request ? $request->getRealIp() : '';
if (isset($headers['cf-connecting-ip'])) {
$ip = $headers['cf-connecting-ip'];
}
return $ip;
}
}
if (!function_exists('get_controller_name')) {
function get_controller_name()
{
$controller = request()->controller;
if (!$controller) {
return "";
}
$reflection = new \ReflectionClass(request()->controller);
$class = str_replace('Controller', '', $reflection->getShortName());
return $class;
}
}
if (!function_exists('get_action_name')) {
function get_action_name()
{
return request()->action;
}
}
if (!function_exists('msectime')) {
function msectime()
{
list($msec, $sec) = explode(' ', microtime());
$msectime = (float) sprintf('%.0f', (floatval($msec) + floatval($sec)) * 1000);
return $msectime;
}
}
if (!function_exists('cache_add')) {
function cache_add(string $key, int $value = 1, ?string $tag = null): void
{
static $tagMap = [
'user_recharge_total_' => 'recharge_total',
'user_income_total_' => 'income_total',
'user_consume_total_' => 'consume_total',
'team_user_total_' => 'team_user_total',
'team_direct_total_' => 'team_direct_total',
'team_vip_total_' => 'team_vip_total',
'team_recharge_total_' => 'team_recharge_total',
'team_withdrawl_total_' => 'team_withdrawl_total',
'team_income_total_' => 'team_income_total',
'team_consume_total_' => 'team_consume_total',
];
foreach ($tagMap as $prefix => $cacheTag) {
if (str_starts_with($key, $prefix)) {
$tag = $cacheTag;
break;
}
}
$old_value = cache_get($key);
cache($key, $old_value + $value, null, $tag);
}
}
if (!function_exists('cache_get')) {
function cache_get(string $key, bool $force = false): mixed
{
static $queryMap = [
'team_user_total_' => ['table' => 'user_extend', 'field' => 'team_total'],
'team_direct_total_' => ['table' => 'user_extend', 'field' => 'direct_total'],
'team_vip_total_' => ['table' => 'user_extend', 'field' => 'vip_total'],
];
$ret = cache($key) ?: 0;
if (!$ret || $force) {
$matched = false;
foreach ($queryMap as $prefix => $config) {
if (str_starts_with($key, $prefix)) {
$user_id = substr($key, strlen($prefix));
$ret = \support\think\Db::name($config['table'])
->where('user_id', $user_id)
->value($config['field']);
$matched = true;
break;
}
}
if (!$matched && str_starts_with($key, 'article_read_')) {
$parts = explode('_', substr($key, strlen('article_read_')));
if (count($parts) === 2) {
$ret = \support\think\Db::name('archives_read')
->where('source_id', $parts[0])
->where('user_id', $parts[1])
->value('value');
}
}
cache($key, $ret);
}
return $ret;
}
}
if (!function_exists('build_invite_code')) {
function build_invite_code($id = '')
{
if (empty($id)) {
return '';
}
// 使用一个固定的种子值来增加随机性
$seed = 0x7F4A8C3B;
// 将用户ID转换为数字并加入种子
$num = intval($id) + $seed;
// 使用一个固定的字符集(去掉容易混淆的字符)
$chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ';
$chars_len = strlen($chars);
$code = '';
// 生成8位邀请码
for ($i = 0; $i < 8; $i++) {
// 使用不同的数学运算来打乱数字
$num = ($num * 31 + $seed) % 0x7FFFFFFF;
// 确保每次取模的结果在字符集范围内
$index = ($num % $chars_len + $chars_len) % $chars_len;
$code .= $chars[$index];
}
return $code;
}
}
if (!function_exists('get_parent_id')) {
function get_parent_id($user_id)
{
if (!$user_id) {
return "";
}
$info = parent_info( $user_id);
return $info['id'];
}
}
if (!function_exists('parent_info')) {
function parent_info($user_id,$value=[])
{
if (!$user_id) {
return "";
}
if($value){
cache('user_parent_info_' . $user_id, $value);
return $value;
}
$info = cache('user_parent_info_' . $user_id);
if (!$info) {
$parent_id = \app\model\User::where('id', $user_id)
->value('parent_id');
$info = [['id'=>'','username'=>'']];
if($parent_id){
$info = \app\model\User::where('id',$parent_id)->column('id,username');
}
cache('user_parent_info_' . $user_id, $info[0]);
}
return $info;
}
}
if(!function_exists('datetime')){
function datetime($timestamp=0,$format='Y-m-d H:i:s'){
if(!$timestamp){return '';}
if(strpos($timestamp,'-')===false){
if(!$timestamp){return '';}
if($format == 'datetime'){
$format = 'Y-m-d H:i:s';
}
if($format == 'date'){
$format = 'Y-m-d';
}
if($format == 'time'){
$format = 'H:i:s';
}
return date($format,$timestamp);
}
return $timestamp;
}
}
if(!function_exists('log_alert')){
function log_alert($data='',$channel='default'){
if(!is_string($data)){
$data = json_encode($data);
}
// if(is_string($data) || is_numeric($data) || is_bool($data)){
// }else{
// $data = json_encode($data);
// }
\support\Log::channel($channel)->alert($data);
}
}
if(!function_exists('enum_dir')){
function enum_dir($path=''){
$list = [];
//$path = substr(0,1,$path) == '/' ? $path
foreach(glob($path) as $afile){
if(is_dir($afile)){
cp($afile);
//$list[] = enum_dir($afile);
} else {
$list[]=$afile;
//rename('./'.$afile,'./'.$name);
echo $afile,"\n";
}
}
return $list ;
}
}
if(!function_exists('get_user_rights')){
function get_user_rights($user_id):array{
$user_id = idDecode($user_id);
$key = 'user_rights_'.$user_id;
$result = cache($key);
if(!$result){
$result = \think\facade\Db::name('user_role')->alias('ur')
->join('user u','ur.id = u.role_id')
->where('u.id',$user_id)
->field('ur.name,ur.right')
->find();
$result['right'] = json_decode($result['right'],true);
cache($key,$result,86400);
}
return $result;
}
}
if(!function_exists('array_find')){
function array_find(array $array,callable $callbcak):mixed{
foreach ($array as $key => $value) {
if ($callbcak($value, $key)) {
return $value;
}
}
return null;
}
}
if(!function_exists('__my__template_inputs')){
function __my__template_inputs(&$template, &$vars, &$app, &$plugin){
// cp('__after__template_inputs:');
// cp('template:'.$template);
// cp('app:'.$app);
// cp('plugin:'.$plugin);
$request = request();
if(!$template){
$baseViewPath = $plugin ? base_path() . "/plugin/$plugin/app" : app_path();
$viewPath = $app === '' ? "$baseViewPath/view/" : "$baseViewPath/$app/view/";
$template = strtolower($request->controller_name."/".$request->action_name);
}
if(count(explode('/',$template)) == 1){
$template = strtolower($request->controller_name."/".$template);
}
return [$template, $vars, $app, $plugin];
}
}
if(!function_exists('update_user_level')){
function update_user_level($user_id,$count=0){
$levels = [
0,
50,
100,
1000,
5000,
20000,
];
$level = 0;
foreach($levels as $k=>$v){
if($count>=$v){
$level= $k;
}else{
break;
}
}
\support\think\Db::name('user')->where('id',$user_id)->data(['level'=>$level])->save();
}}
if(!function_exists('build_user_team')){
function build_user_team($user){
// 插入自己的团队关系 (自己是自己的后代)
$teamData = [
[
'ancestor_id' => $user['id'],
'descendant_id' => $user['id'],
'depth' => 0,
'status' => 0,
]
];
// 2. 处理团队关系(如果有推荐人)
if ($user['parent_id']) {
parent_info( $user['id'],[
'id' => $user['parent_id'],
'username' => \support\think\Db::name('user')->where('id',$user['parent_id'])->value('username')
]);
// 获取推荐人所有的上级关系,生成新用户的团队关系
$ancestors = \support\think\Db::name('user_team')
->where('descendant_id', $user['parent_id'])
->select();
/** @var \app\model\UserTeam $ancestor */
// 插入新用户与祖先的关系
foreach ($ancestors as $ancestor) {
$teamData[] = [
'ancestor_id' => $ancestor['ancestor_id'],
'descendant_id' => $user['id'],
'depth' => $ancestor['depth'] + 1,
'status' => 1, // 默认状态为 0,表示无效
];
}
}
// 批量插入关系
try {
if($teamData){
\support\think\Db::name('user_team')->insertAll($teamData);
}
} catch (\Exception $e) {
cp($e->getMessage());
}
}
}
+4234
View File
File diff suppressed because it is too large Load Diff
+60
View File
@@ -0,0 +1,60 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
namespace app\mcp;
use app\mcp\McpService;
use support\Log;
class process
{
public function __construct()
{
}
public function onWorkerStart()
{
try {
$config = config('mcp');
$transport = $config['transport'] ?? 'sse';
$host = $config['host'] ?? '127.0.0.1';
$port = (int)($config['port'] ?? 8080);
$path = $config['path'] ?? 'mcp';
$service = new McpService();
switch ($transport) {
case 'stdio':
Log::channel('mcp')->info('Starting MCP with STDIO transport');
$service->startWithStdio();
break;
case 'http':
Log::channel('mcp')->info("Starting MCP with HTTP transport at http://{$host}:{$port}/{$path}");
$service->startWithHttp($host, $port, $path);
break;
case 'sse':
default:
Log::channel('mcp')->info("Starting MCP with SSE transport at http://{$host}:{$port}/{$path}");
$service->startWithSse($host, $port, $path);
break;
}
} catch (\Throwable $e) {
Log::channel('mcp')->error('MCP process start failed: ' . $e->getMessage(), [
'file' => $e->getFile(),
'line' => $e->getLine(),
]);
}
}
}
+20
View File
@@ -0,0 +1,20 @@
<?php
namespace plugin\admin\app\controller;
use think\facade\Db;
/**
* @ControllerAnnotation('{$description}')
* Class {$controllerClass}
* @package plugin\admin\app\controller
*/
class {$controllerClass} extends Crud
{
protected array \$noNeedLogin = [];
protected array \$noNeedRight = [];
function __construct()
{
$this->model = new \app\model\{:(str_replace('','',$controllerClass)};
}
}
+128
View File
@@ -0,0 +1,128 @@
<?php
namespace app\api\controller;
use think\facade\Db;
use support\Request;
use Tinywan\Validate\Facade\Validate;
use support\Jwt;
use hg\apidoc\annotation as Apidoc;
/**
* @ControllerAnnotation('{$description}')
* Class {$controllerClass}
* @package app\api\controller
*/
class {$controllerClass} extends BaseController
{
/**
* 不需要鉴权的方法
* @var array
*/
protected array \$noNeedRight = [];
/**
* 无需登录及鉴权的方法
* @var array
*/
protected array \$noNeedLogin = [];
function __construct()
{
$this->model = new \app\model\{:(str_replace('','',$controllerClass)};
}
/**
* 列表
* @Apidoc\Method("POST")
* @Apidoc\Query("network", type="string", require=true, desc="网络")
* @Apidoc\Query("page", type="int", require=true, desc="页码",default=1)
* @Apidoc\Query("limit", type="int", require=true, desc="分页大小",default=10)
*/
public function list()
{
$limit = (int)input('limit',10);
$page = (int)input('page',1);
$status = (int)input('status',-1);
$network = input('network');
$type = input('type');
$model = $this->model->where('user_id',\support\Jwt\JwtToken::getCurrentId());
//->where('network','BEP-20');
if($type){
$model = $model->where('status',1);
}
if($network){
$model = $model->where('network',$network);
}
$list = $model->paginate($limit);
return $this->success(__('successful'),$list->toArray());
}
/**
* 创建
* @Apidoc\Method("POST")
* @Apidoc\Param("network", type="string", require=true, desc="网络,BEP-20,TRC-20",default="BEP-20")
* @Apidoc\Param("address", type="string", require=true, desc="地址")
* @Apidoc\Param("title", type="string", require=true, desc="名称")
* @Apidoc\Param("status", type="string", require=true, desc="状态,可选,1,0,默认1")
*/
public function create()
{
//captcha_verify('image','create_address');
//* @Apidoc\Param("code", type="string", require=true, desc="图形验证码 event=create_address")
//$trade_password = input('trade_password');
//\support\Jwt::verify_trade_password($trade_password);
//* @Apidoc\Param("trade_password", type="string", require=true, desc="交易密码")
$data = [
'title' => input('title',''),
'network' => input('network','BEP-20'),
'address' => input('address'),
'status' => input('status',0),
'user_id' => \support\Jwt\JwtToken::getCurrentId()
];
if(!$data['title']){
return $this->error(__('Invalid title'));
}
// || substr($data['address'],0,2)!='0x'
if(!$data['address']){
return $this->error(__('Invalid address'));
}
$this->model->create($data);
return $this->success(__('successful'));
}
/**
* 编辑
* @Apidoc\Method("POST")
* @Apidoc\Param("id", type="string", require=true, desc="id")
* @Apidoc\Param("title", type="string", require=true, desc="名称")
* @Apidoc\Param("status", type="string", require=true, desc="状态,可选,1,0,默认1")
*/
public function update()
{
//captcha_verify('image','update_address');
//$trade_password = input('trade_password');
//\support\Jwt::verify_trade_password($trade_password);
$data = [
'id' => input('id',''),
'title' => input('title',''),
'status' => input('status',1)
];
if(!$data['id']){
return $this->error(__('Invalid parameters'));
}
if(!$data['title']){
return $this->error(__('Invalid title'));
}
$this->model->where('id',$data['id'])->save($data);
return $this->success(__('successful'));
}
/**
* 详情
* @Apidoc\Query("id", type="int", require=true, desc="id")
*/
public function detail(){
$id = input('id');
$vo = $this->model->where('id',$id)->find();
if($vo) {
return $this->success(__('successful'),$vo->toArray());
}else{
return $this->error(__("Record is not exist"));
}
}
}
+43
View File
@@ -0,0 +1,43 @@
<?php
namespace app\controller;
use think\facade\Db;
/**
* @ControllerAnnotation('{$description}')
* Class {$controllerClass}
* @package app\controller
*/
class {$controllerClass} extends Base
{
protected array \$noNeedLogin = [];
protected array \$noNeedRight = [];
function __construct()
{
}
/**
* @NodeAnnotation(title='列表')
* @return mixed
*/
public function index()
{
return view();
}
/**
* @NodeAnnotation(title='添加')
* @return mixed
*/
public function add()
{
}
/**
* @NodeAnnotation(title='编辑')
* @return \\support\\response
*/
public function edit()
{
}
}
+113
View File
@@ -0,0 +1,113 @@
define(['table', 'upload','form'], function (Table,Upload,Form) {
var {$controllerClass} = {
index: function () {
var admin_path = Config.admin_path;
Table.api.init({
extend: {
index_url: '{:admin_path()}/{$controllerLower}/select',
add_url: '{:admin_path()}/{$controllerLower}/insert',
edit_url: '{:admin_path()}/{$controllerLower}/update',
del_url: '{:admin_path()}/{$controllerLower}/delete',
multi_url: '{:admin_path()}/{$controllerLower}/multi',
dragsort_url: '{:admin_path()}/{$controllerLower}/weigh',
table: 'admin',
}
});
var table = $("#table");
var tableOptions = {
url: $.fn.bootstrapTable.defaults.extend.index_url,
pk: 'id',
sortName: 'id',
pagination: false,
commonSearch: false,
search: false,
columns: [
[
{checkbox: true},
{
field: 'id',
title: 'ID',
filter: "number",
sortable: true // 是否排序
},
{
field: 'username',
title: '用户名',
filter: "string",
},
{
field: 'role_name',
title: '角色',
filter: "string",
},
{
field: 'mobile',
title: '手机',
filter: "string",
},
{
field: 'email',
title: '邮箱',
filter: "string"
},
{
field: 'login_at',
title: '最后登录',
filter: "date",
visible:false
},
{
field: 'created_at',
title: '注册时间',
filter: "date",
visible:false
},
{
field: 'status',
title: '状态',
formatter:Table.api.formatter.switch
},
{field: 'operate', title: '操作', table: table, events: Table.api.events.operate, formatter: Table.api.formatter.operate}
]
]
};
// 初始化表格
table.bootstrapTable(tableOptions);
// 为表格绑定事件
Table.api.bindevent(table);
},
update:function(){
Config['upload_url'] = '{:admin_path()}/file/avatar';
var form = $('form');
Form.api.bindevent(form)
this.getRole();
},
insert:function(){
Config['upload_url'] = '{:admin_path()}/file/avatar';
var form = $('form');
Form.api.bindevent(form)
this.getRole();
},
getRole:function(){
Fast.api.ajax({
url: "{:admin_path()}/adminrole/select?format=select",
dataType: "json",
success: function (res) {
var html = "";
var selected=$('#roles').data('value');
for (let index = 0; index < res.data.length; index++) {
const element = res.data[index];
if(selected == element.value){
html+='<option value="'+element.value+'" selected>'+element.name+'</option>';
}else{
html+='<option value="'+element.value+'">'+element.name+'</option>';
}
}
$('#roles').append(html);
}
});
}
};
return {$controllerClass}
});
+21
View File
@@ -0,0 +1,21 @@
{layout name="layout"}
<div class="toolbar" class="toolbar-btn-action">
<a id="btn_add" class="btn btn-primary m-r-5 btn-add" data-url="{:url('insert')}" data-title="新增">
<span class="mdi mdi-plus" aria-hidden="true"></span>新增
</a>
<a id="btn_edit" class="btn btn-success m-r-5 btn-disabled disabled btn-multi" data-params="status=1">
<span class="mdi mdi-check" aria-hidden="true"></span>启用
</a>
<a id="btn_edit" class="btn btn-warning m-r-5 btn-disabled disabled btn-multi" data-params="status=0">
<span class="mdi mdi-block-helper" aria-hidden="true"></span>禁用
</a>
<a id="btn_delete" class="btn btn-danger btn-del btn-disabled disabled">
<span class="mdi mdi-window-close" aria-hidden="true"></span>删除
</a>
</div>
<!-- 数据表格 -->
<div class="card">
<div class="card-body">
<table id="table"></table>
</div>
</div>
+99
View File
@@ -0,0 +1,99 @@
{layout name="layout"}
<div class="card">
<div class="card-body">
<form class="form-horizontal" action="__SELF__" method="post">
<input type="hidden" name="id" value="{$row.id|null}" />
{volist name="fields" id="vo"}
<?php
$fieldName = $field['name'];
$fieldComment = $field['comment'] ?? $fieldName;
$fieldType = $field['type'] ?? 'varchar';
?>
{switch $fieldType}
{case value='select'}
<div class="form-group">
<label class="control-label col-xs-12 col-sm-2">{$fieldComment}</label>
<div class="col-xs-12 col-sm-8 col-md-6">
<select name="{$fieldName}" class="form-control selectpicker">
{\\volist name="$vo.selectOptions" id="cvo"}
<option value="{\\$cvo.id}" {\\if $row['{$fieldName}']== $cvo.id}selected{\\/if}>{\\$cvo.title}</option>
{\\/volist}
</select>
</div>
</div>
{/case}
{case value='radio'}
<div class="form-group">
<label class="control-label col-xs-12 col-sm-2">{$fieldComment}</label>
<div class="col-xs-12 col-sm-8 col-md-6">
{\\volist name="$vo.selectOptions" id="cvo"}
<label class="lyear-radio radio-primary radio-inline">
<input type="radio" name="{$fieldName}" {\\if $row.{$fieldName} == $key} checked{\\/if} value="{\\$key}">
<span>{\\$rvo}</span>
</label>
{\\/volist}
</div>
</div>
{/case}
{case value='time'}
<div class="form-group">
<label for="type" class="control-label col-xs-12 col-sm-2">{$fieldComment}</label>
<div class="col-xs-12 col-sm-8 col-md-6">
<input type="password" name="{$fieldName}" placeholder="请输入{$fieldComment}" value="{\$row.{$fieldName}|null}" class="form-control" />
</div>
</div>
{/case}
{case value='image'}
<div class="form-group">
<label for="type" class="control-label col-xs-12 col-sm-2">{$fieldComment}</label>
<div class="col-xs-12 col-sm-8 col-md-6">
<input id="c-{$fieldName}" class="form-control" size="50" name="{$fieldName}" type="hidden" value="{\$row.{$fieldName}|default=''}" data-tip="image">
<ul class="list-inline clearfix lyear-uploads-pic" data-template="preview" id="p-{$fieldName}">
<li nodelete class="col-xs-4 col-sm-3 col-md-2">
<a class="pic-add faupload" style="height: auto;border: 0;" permission="app.admin.files.upload" id="add-pic-btn" href="#!" title="点击上传" data-input-id="c-{$fieldName}" data-mimetype="image/*" data-multiple="false" data-preview-id="p-{$fieldName}"></a>
<a class="pic-add fachoose" style="height: auto;border: 0;" permission="app.admin.files.list" id="choose-pic-btn" href="#!" title="选择文件" data-input-id="c-{$fieldName}" data-mimetype="image/*" data-multiple="false" data-preview-id="p-{$fieldName}"></a>
</li>
</ul>
</div>
</div>
{/case}
{case value='file'}
<div class="form-group">
<label for="type" class="control-label col-xs-12 col-sm-2">{$fieldComment}</label>
<div class="col-xs-12 col-sm-8 col-md-6">
<input id="c-{$fieldName}" class="form-control" size="50" name="{$fieldName}" type="hidden" value="{\$row.{$fieldName}|default=''}" data-tip="image">
<ul class="list-inline clearfix lyear-uploads-pic" data-template="preview" id="p-{$fieldName}">
<li nodelete class="col-xs-4 col-sm-3 col-md-2">
<a class="pic-add faupload" style="height: auto;border: 0;" permission="app.admin.files.upload" id="add-pic-btn" href="#!" title="点击上传" data-input-id="c-{$fieldName}" data-multiple="false" data-preview-id="p-{$fieldName}"></a>
<a class="pic-add fachoose" style="height: auto;border: 0;" permission="app.admin.files.list" id="choose-pic-btn" href="#!" title="选择文件" data-input-id="c-{$fieldName}" data-multiple="false" data-preview-id="p-{$fieldName}"></a>
</li>
</ul>
</div>
</div>
{/case}
{case value='password'}
<div class="form-group">
<label for="type" class="control-label col-xs-12 col-sm-2">{$fieldComment}</label>
<div class="col-xs-12 col-sm-8 col-md-6">
<input type="password" name="{$fieldName}" placeholder="请输入{$fieldComment}" value="{\$row.{$fieldName}|null}" class="form-control" />
</div>
</div>
{/case}
{default /}
<div class="form-group">
<label for="type" class="control-label col-xs-12 col-sm-2">{$fieldComment}</label>
<div class="col-xs-12 col-sm-8 col-md-6">
<input type="text" name="{$fieldName}" placeholder="请输入{$fieldComment}" value="{\$row.{$fieldName}|null}" class="form-control" />
</div>
</div>
{/switch}
<div class="form-group">
<label class="control-label col-xs-12 col-sm-2"></label>
<div class="col-xs-12 col-sm-8 col-md-6 layer-footer">
<button type="submit" class="btn btn-primary m-r-5">提交</button>
<button type="reset" class="btn btn-warning m-r-5">重置</button>
</div>
</div>
</form>
</div>
</div>
+57
View File
@@ -0,0 +1,57 @@
<?php
namespace app\middleware;
use Override;
use support\Container;
use Webman\MiddlewareInterface;
use Webman\Http\Response;
use Webman\Http\Request;
use Webman\Route;
class ActionHook implements MiddlewareInterface
{
public function process(Request $request, callable $next) : Response
{
if ($request->controller) {
$headers = [
'Access-Control-Allow-Credentials' => 'true',
'Access-Control-Allow-Origin' => $request->header('origin', '*'),
'Access-Control-Allow-Methods' => $request->header('access-control-request-method', '*'),
'Access-Control-Allow-Headers' => $request->header('access-control-request-headers', '*'),
];
if($request->method() == 'OPTIONS'){
$response = response('',204,$headers);
return $response;
}
// 禁止直接访问beforeAction afterAction
if (substr($request->action,0,9) === '__before_' || substr($request->action,0,8) === '__after_') {
$callback = Route::getFallback() ?? function () {
return new Response(404, [], \file_get_contents(public_path() . '/404.html'));
};
$reponse = $callback($request);
return $reponse instanceof Response ? $reponse : \response($reponse);
}
$controller = Container::get($request->controller);
$beforeAction = '__before_'.$request->action.'__';
if (method_exists($controller, $beforeAction)) {
$before_response = call_user_func([$controller, $beforeAction], $request);
if ($before_response instanceof Response) {
return $before_response;
}
}
$response = $next($request);
$afterAction = '__after_'.$request->action.'__';
if (method_exists($controller, $afterAction)) {
$after_response = call_user_func([$controller, $afterAction], $request, $response);
if ($after_response instanceof Response) {
return $after_response;
}
}
if($request->controller == '\\hg\\apidoc\\Controller' && !$response->getHeader('Access-Control-Allow-Methods')){
$response->withHeaders($headers);
}
return $response;
}
return $next($request);
}
}
+76
View File
@@ -0,0 +1,76 @@
<?php
namespace app\middleware;
use app\controller\MetricsController;
use support\Request;
use Webman\Http\Response;
/**
* 指标收集中间件
* 自动记录HTTP请求指标
*/
class MetricsMiddleware
{
/**
* 请求开始时间
*/
protected $startTime;
/**
* 处理请求
*
* @param Request $request
* @param callable $handler
* @return Response
*/
public function process(Request $request, callable $handler): Response
{
// 记录请求开始时间
$this->startTime = microtime(true);
// 处理请求
$response = $handler($request);
// 计算请求处理时间
$duration = microtime(true) - $this->startTime;
// 记录指标
$this->recordMetrics($request, $response, $duration);
return $response;
}
/**
* 记录指标
*
* @param Request $request
* @param Response $response
* @param float $duration
*/
protected function recordMetrics(Request $request, Response $response, float $duration): void
{
try {
$method = $request->method();
$path = $request->path();
$status = $response->getStatusCode();
// 获取响应大小
$responseSize = strlen($response->rawBody());
// 记录HTTP请求指标
MetricsController::recordHttpRequest($method, $path, $status, $duration, $responseSize);
// 记录请求处理时间
MetricsController::setGauge('webman_http_request_duration_seconds', $duration, [
'method' => $method,
'path' => $path,
'status' => (string)$status
]);
} catch (\Exception $e) {
// 记录指标失败不应影响正常请求
error_log('Metrics recording failed: ' . $e->getMessage());
}
}
}
+42
View File
@@ -0,0 +1,42 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
namespace app\middleware;
use Webman\MiddlewareInterface;
use Webman\Http\Response;
use Webman\Http\Request;
/**
* Class StaticFile
* @package app\middleware
*/
class StaticFile implements MiddlewareInterface
{
public function process(Request $request, callable $next): Response
{
// Access to files beginning with. Is prohibited
if (strpos($request->path(), '/.') !== false) {
return response('<h1>403 forbidden</h1>', 403);
}
/** @var Response $response */
$response = $next($request);
// Add cross domain HTTP header
/*$response->withHeaders([
'Access-Control-Allow-Origin' => '*',
'Access-Control-Allow-Credentials' => 'true',
]);*/
return $response;
}
}
+44
View File
@@ -0,0 +1,44 @@
<?php
namespace app\model;
use app\model\Base;
/**
* @property integer $id 主键(ID) - 无注释
* @property integer $user_id 用户ID
* @property string $title 名字
* @property string $network 网络
* @property string $address 账号
* @property string $img 图片
* @property integer $is_default 是否默认
* @property integer $created_at 创建时间
* @property integer $updated_at 更新时间
* @property integer $status 状态
*/
class Address extends Base
{
//protected $name = 'address';
function getNetworkList(){
return [
"BEP-20"=>"BEP-20",
"TRC-20"=>"TRC-20",
"WECHAT"=>"微信",
"ALIPAY"=>"支付宝"
];
}
function getStatusList(){
return [
'0' => '禁用',
'1' => '启用',
];
}
public function user()
{
return $this->belongsTo('User', 'user_id', 'id');//->setEagerlyType(0);
}
}
+70
View File
@@ -0,0 +1,70 @@
<?php
namespace app\model;
/**
* 相册模型
* @property integer $id 主键(ID)
* @property integer $user_id 用户ID
* @property integer $group_id 群组ID
* @property integer $userID 用户ID
* @property integer $groupID 群组ID
* @property string $title 标题
* @property int $image 封面图片ID
* @property int $weigh 排序权重,越小越靠前
* @property integer $created_at 创建时间
* @property integer $updated_at 更新时间
* @property integer $status 状态(0:隐藏 1:正常)
*/
class Album extends Base
{
protected $name = 'album';
protected function getOptions(): array
{
return array_merge(parent::getOptions(), [
'insert' => [
'status' => 1,
],
'append'=>[
'userID',
'groupID'
]
]);
}
public static function onAfterInsert($row){
$changeData = $row->getChangedData();
if(isset($changeData['image'])) {
Files::where('path',$changeData['image'])->inc('use_count');
};
}
public static function onAfterUpdate($row){
$OrgData = $row->getOrigin();
$changeData = $row->getChangedData();
if(isset($OrgData['image']) && $OrgData['image']) {
\support\Log::info('OrgData string');
Files::where('path',$OrgData['image'])->dec('use_count');
};
if(isset($changeData['image']) && $changeData['image']) {
\support\Log::info('changeData string');
Files::where('path',$changeData['image'])->inc('use_count');
};
}
public static function onBeforeDelete($row){
if($row->total>0){
return false;
}
}
public static function onAfterDelete($row){
Files::where('path',$row->image)->dec('use_count');
}
function getGroupIDAttr($v,$row){
return $v?:$row['group_id'];
}
function getUserIDAttr($v,$row){
return $v?:$row['user_id'];
}
}
+107
View File
@@ -0,0 +1,107 @@
<?php
namespace app\model;
use support\think\Db;
use traits\model\SoftDelete;
class Archives extends Base
{
//use SoftDelete;// 表名
protected function getOptions(): array{
return array_merge(parent::getOptions(),[
'append' => [
'status_text'
],
]);
}
public static function onAfterInsert(Archives $row)
{
\support\Log::error(''. json_encode($row));
$pk = $row->getPk();
self::where($pk, $row->$pk)->update(['weigh' => $row[$pk]]);
$changedData = $row->getData();
if (isset($changedData['content'])) {
//在更新成功后刷新副表
$values = array_intersect_key($changedData, array_flip(['content']));
$values['id'] = $row['id'];
//更新副表
Db::name('content')->insert($values, true);
}
}
public static function onAfterUpdate($row)
{
\support\Log::info('onAfterUpdate'.$row->id. json_encode($row->getChangedData()));
$changedData = $row->getChangedData();
if (isset($changedData['content'])) {
//在更新成功后刷新副表
$values = array_intersect_key($row->getData(), array_flip(['content']));
//更新副表
Db::name('content')->where('id',$row->id)->update($values);
}
}
public static function onAfterDelete($row){
//删除副表
Db::name('content')->where("id", $row['id'])->delete();
}
/**
* 批量设置数据
* @param $data
* @return $this
*/
public function setAddonData(string|array|object $data)
{
if (is_object($data)) {
$data = get_object_vars($data);
}
foreach($data as $k=>$v){
$this->$k=$v;
}
return $this;
}
public function getStatusList()
{
return ['1' => '正常', '0' => '隐藏'];
}
public function getStatusTextAttr($value, $data)
{
$value = $value ? $value : (isset($data['status']) ? $data['status'] : '');
$list = $this->getStatusList();
return isset($list[$value]) ? $list[$value] : '';
}
public function getCategoryOptions($type='default'){
return Category::where('status','1')->where('type',$type)->column('id,title');
}
function setCreatedAtAttr($v){
if($v && strpos($v,'-')){
return strtotime($v);
}
return $v;
}
function setUpdatedAtAttr($v){
if($v && strpos($v,'-')){
return strtotime($v);
}
return $v;
}
public function getFlagList()
{
return Config('site.flagtype');
}
public function content()
{
return $this->hasOne('content', 'id', 'id');
// ->bind(['content']);//->setEagerlyType(0);
}
public function category()
{
return $this->belongsTo('Category', 'category_id', 'id');//->setEagerlyType(0);
}
}
+226
View File
@@ -0,0 +1,226 @@
<?php
namespace app\model;
use Symfony\Component\Console\Input\Input;
use think\Model;
use think\facade\Db;
class BalanceLog extends Base
{
// 表结构定义(使用时间戳)
const TABLE_SCHEMA = [
'id' => 'int(11) NOT NULL AUTO_INCREMENT',
'user_id' => 'int(11) NOT NULL',
'currency' => 'varchar(20) NOT NULL',
'amount' => 'decimal(15,2) NOT NULL',
'before' => 'decimal(15,2) NOT NULL',
'after' => 'decimal(15,2) NOT NULL',
'type' => 'varchar(50) NOT NULL',
'created_at' => 'int(11) NOT NULL COMMENT \'UNIX timestamp\'', // 改为整型时间戳
'memo' => 'varchar(255) DEFAULT NULL',
'PRIMARY KEY (`id`)'
];
function getCreatedAtAttr($v){
return $v ? explode('.',$v)[0] : '';
}
protected function getOptions(): array{
return array_merge(parent::getOptions(),[
'connection' => 'mongodb',
// 'append' => [
// 'from_user',
// 'to_user'
// ],
]);
}
public static function create(array|object $data, array $allowField = [], bool $replace = false, string $suffix = ''):\think\model\contract\Modelable
{
$model = new static();
if(isset($data['currency'])){
if(in_array($data['currency'],Config('site.allow_currency_logs'))){
$data['status']=isset($data['status']) ? $data['status']:1;
$data['user_id'] = intval($data['user_id']);
$data['amount'] = floatval($data['amount']);
$data['before'] = floatval($data['before']);
$data['after'] = floatval($data['after']);
$data['type'] = $data['type'] instanceof \app\enum\BalanceType ? $data['type']->value : floatval($data['type']);
$model->setSuffix('_'.strtolower($data['currency']))->allowField($allowField)
->replace($replace)
->save($data, true);
}
}
return $model->fetchModel($model);
}
// 创建所有需要的表索引
public static function createAllIndexes(): array
{
$results = [];
$allow_currency_logs = Config('site.allow_currency_logs');
foreach ($allow_currency_logs as $currency) {
$results[$currency] = self::createTableIndexes($currency);
}
return $results;
}
// 创建索引(适配时间戳查询)
public static function createTableIndexes(string $currency): array
{
$table = self::getTableName($currency);
$results = [];
try {
// 确保归档表存在
if (!self::tableExists($table)) {
self::createTableStructure($table);
}
// 主复合索引(使用时间戳)
if (!self::indexExists($table, 'idx_user_currency_type_created')) {
Db::execute("ALTER TABLE `{$table}` ADD INDEX `idx_user_currency_type_created` (`user_id`, `currency`, `type`, `created_at`)");
$results[] = "Created idx_user_currency_type_created on {$table}";
}
// 时间索引(降序优化)
if (!self::indexExists($table, 'idx_created_at')) {
Db::execute("ALTER TABLE `{$table}` ADD INDEX `idx_created_at` (`created_at` DESC)");
$results[] = "Created idx_created_at on {$table}";
}
} catch (\Throwable $e) {
$results['error'] = "Error on {$table}: " . $e->getMessage();
}
return $results;
}
// 检查索引是否存在
protected static function indexExists(string $table, string $indexName): bool
{
$indexes = Db::query("SHOW INDEX FROM `{$table}` WHERE Key_name = ?", [$indexName]);
return !empty($indexes);
}
// 检查表是否存在
protected static function tableExists(string $table): bool
{
try {
Db::query("SELECT 1 FROM `{$table}` LIMIT 1");
return true;
} catch (\Throwable $e) {
return false;
}
}
// 数据归档方法(可在定时任务中调用)
public static function archiveData(int $days = 3): array
{
$results = [];
$allow_currency_logs = Config('site.allow_currency_logs');
foreach ($allow_currency_logs as $currency) {
$results[$currency] = self::archiveCurrencyData($currency, $days);
}
return $results;
}
// 归档指定货币的数据
protected static function archiveCurrencyData(string $currency, int $days): array
{
$table = self::getTableName($currency);
$archiveTable = $table . '_archive';
$cutoffTimestamp = time() - ($days * 86400); // 转为时间戳计算
$result = [
'table' => $table,
'archived' => 0,
'messages' => []
];
try {
// 确保归档表存在
if (!self::tableExists($archiveTable)) {
self::createTableStructure($archiveTable);
$result['messages'][] = "Created archive table: {$archiveTable}";
}
// 分批归档数据
$totalArchived = 0;
Db::table($table)
->where('created_at', '<=', $cutoffTimestamp)
->chunk(1000, function($logs) use ($archiveTable, $table, &$totalArchived) {
Db::table($archiveTable)->insertAll($logs);
$count = count($logs);
Db::table($table)->whereIn('id', array_column($logs, 'id'))->delete();
$totalArchived += $count;
});
$result['archived'] = $totalArchived;
$result['messages'][] = "Archived {$totalArchived} records from {$table}";
// 优化表
Db::execute("OPTIMIZE TABLE `{$table}`");
$result['messages'][] = "Optimized table: {$table}";
} catch (\Throwable $e) {
$result['error'] = $e->getMessage();
}
return $result;
}
// 查询方法(示例)
public static function queryLogs($userId, $currency, $type = null, $startTime = null, $endTime = null)
{
$model = new static;
$query = $model->setSuffix('_'.strtolower($currency))
//->where('currency', $currency)
->where('user_id', intval($userId))
->order('created_at', 'desc');
if ($type) {
$temp_arr = explode(',', $type); // 得到 ["1", "2", "3", "4"]
$arr = array_map('intval', $temp_arr); // 得到 [1, 2, 3, 4]
$query->whereIn('type', $arr);
}
if ($startTime) {
// 支持传入时间戳或日期字符串
//$startTimestamp = is_numeric($startTime) ? intval($startTime) : strtotime($startTime);
$query->where('created_at', '>=', $startTime);
}
if ($endTime) {
// 支持传入时间戳或日期字符串
//$endTimestamp = is_numeric($endTime) ? intval($endTime) : strtotime($endTime);
$query->where('created_at', '<=', $endTime);
}
$limit = 10;
if(request()){
$limit = input('limit',10);
}
return $query->paginate($limit);
}
// 创建表结构
protected static function createTableStructure(string $table): bool
{
if (self::tableExists($table)) {
return false;
}
$columns = [];
foreach (self::TABLE_SCHEMA as $column => $definition) {
if (strpos($definition, 'PRIMARY KEY') === false) {
$columns[] = "`{$column}` {$definition}";
}
}
$primaryKey = self::TABLE_SCHEMA['PRIMARY KEY'] ?? 'PRIMARY KEY (`id`)';
$sql = "CREATE TABLE `{$table}` (" .
implode(', ', $columns) . ", " .
$primaryKey .
") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci";
Db::execute($sql);
return true;
}
}
+51
View File
@@ -0,0 +1,51 @@
<?php
namespace app\model;
use DateTimeInterface;
use support\think\Model;
class Base extends Model
{
protected function getOptions(): array{
return [
'connection' => 'mysql',
'createTime' => 'created_at',
'updateTime' => 'updated_at',
'deleteTime' => 'deleted_at',
'autoWriteTimestamp' => 'int',
//'dateFormat' => false
// query 自定义数据库查询对象类名(默认为空)
// type 需要自动转换的字段及类型(数组,默认为空)
// autoValidate 是否自动验证(开启后会自动进行数据验证)
// validate 对应验证类名或验证规则(字符串或数组,autoValidate参数开启后有效)
// strict 是否严格区分字段大小写(默认为true)
// disuse 废弃字段(数组,默认为空)
// readonly 只读字段(数组,默认为空)
// hidden 输出隐藏字段(数组,默认为空)
// visible 输出显示字段(数组,默认为空)
// append 输出追加字段(数组,默认为空)
// mapping 字段映射(数组,默认为空)
// autoRelation 自动with关联(数组,默认为空)
// insert 自动新增写入(数组,默认为空)
// update 自动更新写入(数组,默认为空)
// dateFormat 时间输出格式化设置
];
}
/**
* 格式化日期
*
* @param DateTimeInterface $date
* @return string
*/
protected function serializeDate(DateTimeInterface $date)
{
return $date->format('Y-m-d H:i:s');
}
function getStatusList(){
return [
'0' => '隐藏',
'1' => '正常',
];
}
}

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