chore: remove plugin from repo, add plugin guide, docs + AGPLv3 license
- mc-report-plugin removed from git tracking (kept locally), gitignored; plugin code no longer distributed (old x-external-key auth, outdated) - docs/PLUGIN-GUIDE.md: Java plugin integration guide using new ID+Secret -> SESSION auth (session mgmt, submit ticket, track, bans) - README: updated external API table (SESSION), dynamic sources, removed obsolete plugin commands/tech row, new dir structure, doc links - INSTALL: drop restart requirement, add upgrade-migration/log/sources sections, license section - LICENSE: GNU AGPL v3 full text - verified: 24 checks (git tracking, docs consistency, license integrity)
This commit is contained in:
4
.gitignore
vendored
4
.gitignore
vendored
@@ -1,10 +1,10 @@
|
||||
node_modules/
|
||||
# 本地运行配置与上传文件(含数据库密码,严禁入库)
|
||||
data/
|
||||
# 日志与临时文件
|
||||
*.log
|
||||
*.tmp.js
|
||||
*.tmp.sh
|
||||
*.bak
|
||||
# 会话记录
|
||||
session-*.md
|
||||
# MC 插件代码不随仓库分发(见 docs/PLUGIN-GUIDE.md 示例自行实现)
|
||||
mc-report-plugin/
|
||||
|
||||
77
INSTALL.md
77
INSTALL.md
@@ -3,7 +3,7 @@
|
||||
## 环境要求
|
||||
|
||||
- Node.js ≥ 18 / npm ≥ 9
|
||||
- MySQL ≥ 5.7(或 MariaDB ≥ 10.2)
|
||||
- MySQL ≥ 5.7(或 MariaDB ≥ 10.2)
|
||||
|
||||
## 快速开始
|
||||
|
||||
@@ -16,17 +16,17 @@ node backend/server.js
|
||||
|
||||
### 安装向导
|
||||
|
||||
**第1步:数据库配置**
|
||||
- MySQL 主机(默认 localhost)
|
||||
- 端口(默认 3306)
|
||||
**第1步:数据库配置**
|
||||
- MySQL 主机(默认 localhost)
|
||||
- 端口(默认 3306)
|
||||
- 用户名 / 密码
|
||||
- 数据库名(不存在则自动创建)
|
||||
- 数据库名(不存在则自动创建)
|
||||
|
||||
**第2步:创建服主账号**
|
||||
**第2步:创建服主账号**
|
||||
- 站点名称、站点地址
|
||||
- 服主用户名、密码、邮箱
|
||||
|
||||
安装完成后 **重启服务器** 加载业务路由。
|
||||
> 安装完成后**无需重启**,页面自动刷新并进入登录。
|
||||
|
||||
## 配置文件 `data/config.json`
|
||||
|
||||
@@ -38,11 +38,12 @@ node backend/server.js
|
||||
"db_pass": "",
|
||||
"db_name": "mc_report",
|
||||
"jwt_secret": "(安装时随机64位)",
|
||||
"api_key": "(安装时随机48位)",
|
||||
"external_api_key": "(安装时随机48位)"
|
||||
"api_key": "(安装时随机48位)"
|
||||
}
|
||||
```
|
||||
|
||||
> `data/` 目录不入库(git 忽略),包含 config.json 与上传文件,请做好备份。
|
||||
|
||||
## 生产部署
|
||||
|
||||
```bash
|
||||
@@ -72,17 +73,49 @@ server {
|
||||
}
|
||||
```
|
||||
|
||||
## 升级迁移
|
||||
|
||||
服务启动时自动执行幂等迁移(`backend/db.js` 的 `migrateAdditions`),老库无需手工操作:
|
||||
|
||||
- `user_identities` 表创建 + 从 `users` 主身份回填(多来源身份)
|
||||
- `sources` 表创建并预置 `netease`/`skin`(动态来源)
|
||||
- `api_clients` / `api_sessions` 表创建(外部 API SESSION 鉴权)
|
||||
- `server_groups.alias`、`tickets/bans.server_name` 列(按子服归属)
|
||||
- `email_logs` / `system_logs` 表创建(日志)
|
||||
- `users.source` ENUM → VARCHAR、`game_uid` 可空(去除网易 UID 依赖)
|
||||
|
||||
升级后建议重启一次并清浏览器缓存(HTML 中注入的 api_key 会随重装刷新)。
|
||||
|
||||
## 邮件配置
|
||||
|
||||
后台 → 系统设置 → SMTP 填写。未配置时注册用户自动激活。
|
||||
后台 → 系统设置 → SMTP 填写。未配置时注册用户自动激活(dev 模式验证码直接显示在前端)。
|
||||
|
||||
## Webhook
|
||||
## Webhook 通知
|
||||
|
||||
后台 → 通知配置 → 添加 Discord Webhook URL → 选择触发事件(工单创建/认领/转交/更新)。
|
||||
后台 → 通知配置 → 添加 Webhook URL(Discord/企微/QQ/飞书均可)→ 选择触发事件(工单创建/认领/转交/更新)。
|
||||
|
||||
## 日志排查
|
||||
|
||||
后台 → 系统日志(owner/admin):
|
||||
|
||||
- **邮件日志**:收件人/模板/状态(成功/失败)/错误信息
|
||||
- **系统日志**:级别/来源(webhook、mailer、server 等)/消息/详情
|
||||
|
||||
配合 `pm2 logs mc-report` 可完整定位问题。
|
||||
|
||||
## 外部 API(机器人/插件)
|
||||
|
||||
后台 → 外部API → 创建客户端(ID 16 位数字 + Secret 32 位随机,secret 仅显示一次)→ `POST /api/external/auth/session` 换取 SESSION → 其余接口带 `Authorization: Bearer <SESSION>`。
|
||||
|
||||
接口清单与插件对接示例见 [docs/EXTERNAL-API.md](docs/EXTERNAL-API.md)、[docs/PLUGIN-GUIDE.md](docs/PLUGIN-GUIDE.md)。
|
||||
|
||||
## 来源管理
|
||||
|
||||
后台 → 来源管理(owner):增删/停用/排序来源(如 网易端/皮肤站/正版/离线),不设默认。已被身份数据使用的来源不能删除,只能停用。
|
||||
|
||||
## 数据库
|
||||
|
||||
MySQL,安装时自动建表。备份:
|
||||
MySQL,安装时自动建表。备份:
|
||||
|
||||
```bash
|
||||
mysqldump -u root -p mc_report > backup.sql
|
||||
@@ -90,21 +123,29 @@ mysqldump -u root -p mc_report > backup.sql
|
||||
|
||||
## 目录
|
||||
|
||||
```
|
||||
```text
|
||||
mc-report/
|
||||
├── backend/
|
||||
│ ├── server.js
|
||||
│ ├── db.js
|
||||
│ ├── logger.js
|
||||
│ ├── mailer.js
|
||||
│ ├── webhook.js
|
||||
│ ├── middleware/{auth,security,upload}.js
|
||||
│ └── routes/{auth,tickets,install,external,users,settings,export,polls,features,notifications,captcha,uploads}.js
|
||||
├── public/ # 前端 SPA(16 页面)
|
||||
├── mc-report-plugin/ # Paper 1.20+ 插件
|
||||
│ └── routes/{auth,tickets,install,external,sources,users,logs,settings,export,polls,features,notifications,captcha,uploads}.js
|
||||
├── public/ # 前端 SPA
|
||||
├── docs/ # API / 外部API / 插件指南
|
||||
├── nginx.inc
|
||||
├── data/config.json
|
||||
├── data/uploads/
|
||||
├── package.json
|
||||
├── README.md
|
||||
└── INSTALL.md
|
||||
├── INSTALL.md
|
||||
└── LICENSE # AGPLv3
|
||||
```
|
||||
|
||||
## 许可证
|
||||
|
||||
本项目采用 **GNU Affero General Public License v3.0(AGPLv3)**,详见 [LICENSE](LICENSE)。
|
||||
|
||||
> 使用/修改/分发本项目请遵守 AGPLv3:修改后的源码必须开源,且通过网络提供服务的修改版本需向用户提供源码。
|
||||
|
||||
640
LICENSE
Normal file
640
LICENSE
Normal file
@@ -0,0 +1,640 @@
|
||||
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 people have made generous contributions to the wide range of
|
||||
software distributed through that system in reliance on consistent
|
||||
application of that system; it is up to the author/donor to decide if
|
||||
he or she is willing to distribute software through any other system
|
||||
and a licensee cannot impose that choice.
|
||||
|
||||
This License distinguishes two kinds of what we call "works".
|
||||
A "work based on the Program" means either the Program or any
|
||||
derivative work under copyright law: that is to say, a work containing
|
||||
the Program or a portion of it, either verbatim or with modifications
|
||||
and/or translated into another language. (Hereinafter, translation is
|
||||
included without limitation in the term "modification".) Each licensee
|
||||
is addressed as "you".
|
||||
|
||||
"The Program" below refers to any such work, and its "work based on
|
||||
the Program" means either the Program or a derivative work as defined
|
||||
above.
|
||||
|
||||
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/>.
|
||||
129
README.md
129
README.md
@@ -13,17 +13,19 @@
|
||||
| **接单制** | 管理员一人一单,可转单(带原因),处理备注(玩家不可见) |
|
||||
| **投票** | 分组/子服管理,定时/不定时投票,盲投制,管理员 1.5 票权 |
|
||||
| **更新内容** | Issues 风格,分状态(待定/计划中/已完成),讨论区,按时间/热度排序,分组筛选 |
|
||||
| **通知** | SMTP 邮件 + Discord Webhook(SSRF 防护),邮件模板自定义(编辑变量) |
|
||||
| **通知** | SMTP 邮件 + Webhook(Discord/企微/QQ/飞书,SSRF 防护),邮件模板自定义 |
|
||||
| **日志** | 邮件日志 + 系统日志(Webhook 失败/服务器错误),后台可视化排查 |
|
||||
| **导出** | CSV(UTF-8 BOM),导出工单/被举报人统计/操作日志(仅服主) |
|
||||
| **插件** | Paper 1.20+,统一 `/report` 命令,游戏内注册/登录/举报/追踪 |
|
||||
| **外部 API** | ID+Secret → SESSION 鉴权;工单提交/追踪、封禁拉取/新增、按子服过滤(机器人/插件对接) |
|
||||
| **找回密码** | 邮箱验证,1小时有效重置链接 |
|
||||
| **多来源身份** | 一个账号可同时绑定网易端 + 皮肤站身份,提交工单时选择身份 |
|
||||
| **多来源身份** | 来源动态管理(后台增删),一个账号可绑定多个来源身份,无默认来源 |
|
||||
| **安装向导** | Web 页面配置 MySQL 连接 + 创建服主账号(完成后无需重启) |
|
||||
|
||||
## API 文档
|
||||
|
||||
- **完整接口文档**:[docs/API.md](docs/API.md)(认证/工单/封禁/用户/设置/通知/投票/更新/导出/外部插件 API)
|
||||
- **第三方外部 API(机器人/插件)**:[docs/EXTERNAL-API.md](docs/EXTERNAL-API.md)(ID+Secret 鉴权、提交工单、进度追踪、封禁拉取、按子服拉数据)
|
||||
- **完整接口文档**:[docs/API.md](docs/API.md)(认证/工单/封禁/用户/设置/通知/投票/更新/导出/外部 API)
|
||||
- **第三方外部 API(机器人/插件)**:[docs/EXTERNAL-API.md](docs/EXTERNAL-API.md)(ID+Secret 换取 SESSION、提交工单、进度追踪、封禁拉取、按子服拉数据)
|
||||
- **MC 插件对接示例**:[docs/PLUGIN-GUIDE.md](docs/PLUGIN-GUIDE.md)(Java 实现:换取 SESSION、提交举报、同步封禁)
|
||||
|
||||
## 快速开始
|
||||
|
||||
@@ -45,12 +47,12 @@ node backend/server.js
|
||||
|----|------|
|
||||
| 后端 | Node.js + Express + mysql2 |
|
||||
| 前端 | Vanilla JS SPA(Hash路由)+ Inter 字体 |
|
||||
| 数据库 | MySQL ≥ 5.7 |
|
||||
| 认证 | JWT(256 位随机密钥,72h 有效期) |
|
||||
| 安全 | Helmet 7 + Rate Limit(分路由限速)+ XSS 过滤 + CSP + API Key 常量时间比对 + SSRF 防护 |
|
||||
| 数据库 | MySQL ≥ 5.7 / MariaDB ≥ 10.2 |
|
||||
| 认证 | Web: JWT(256 位随机密钥,72h 有效期);外部 API: ID+Secret → SESSION(24h,单会话) |
|
||||
| 安全 | Helmet 7 + Rate Limit(分路由限速)+ XSS 过滤 + CSP + SSRF 防护 |
|
||||
| 验证码 | SVG 混淆图片(svg-captcha,4 位字符 + 噪点 + 颜色变形) |
|
||||
| 文件上传 | Multer + MIME + 魔数字节校验,UUID 命名,web 外存储 |
|
||||
| 插件 | Paper API 1.20.4 + java.net.HttpURLConnection |
|
||||
| 许可证 | AGPLv3 |
|
||||
|
||||
## 角色权限矩阵
|
||||
|
||||
@@ -87,36 +89,39 @@ node backend/server.js
|
||||
|
||||
## 注册来源与多身份
|
||||
|
||||
| 来源 | 标识 | UID 含义 |
|
||||
|------|------|---------|
|
||||
| 网易端 | `netease` | 网易 UID |
|
||||
| 皮肤站 | `skin` | 皮肤站 UID / Minecraft UUID |
|
||||
来源在后台「来源管理」页**动态维护**(可增删/停用/排序,不设默认)。初始预置:
|
||||
|
||||
插件注册默认 `skin`。
|
||||
| 来源 | 标识 | 说明 |
|
||||
|------|------|------|
|
||||
| 网易端 | `netease` | 网易端身份 |
|
||||
| 皮肤站 | `skin` | 皮肤站身份 |
|
||||
|
||||
**多身份绑定**:一个账号可同时绑定网易端 + 皮肤站两个身份(控制台「我的身份」或 `POST /api/auth/identities` 管理)。提交工单时可选择使用哪个身份;`user_identities` 表由启动时自动迁移回填,`users` 表主身份字段保持不变(完全兼容旧数据)。
|
||||
**多身份绑定**:一个账号可绑定多个来源身份(控制台「我的身份」或 `POST /api/auth/identities` 管理)。提交工单时可选择使用哪个身份;`user_identities` 表由启动时自动迁移回填,`users` 表主身份字段保持不变(完全兼容旧数据)。
|
||||
|
||||
## 工单提交流程
|
||||
|
||||
1. 填写标题 + 游戏名 + UID(登录后自动填充)
|
||||
2. 举报:选目标玩家 + 原因(内置选项或自定义)+ 可投诉管理员
|
||||
3. 建议:填写建议内容
|
||||
4. 申诉:填写封禁申诉理由
|
||||
5. 上传附件(可选,图片/视频 ≤50MB,最多5个)
|
||||
6. 提交 → 生成 tracking_token(Cookie 留存)
|
||||
1. 填写标题 + 游戏名(登录后自动填充)
|
||||
2. 举报:选目标玩家 + 原因(内置选项或自定义)+ 可投诉管理员
|
||||
3. 建议:填写建议内容
|
||||
4. 申诉:填写封禁申诉理由
|
||||
5. 上传附件(可选,图片/视频 ≤50MB,最多5个)
|
||||
6. 提交 → 生成 tracking_token(Cookie 留存)
|
||||
|
||||
## 外部 API
|
||||
|
||||
| 端点 | 用途 | 认证 |
|
||||
|------|------|------|
|
||||
| `POST /api/external/auth/register` | 插件注册 | x-external-key |
|
||||
| `POST /api/external/auth/login` | 插件登录 | x-external-key |
|
||||
| `GET /api/external/my-tickets` | 我的工单(JWT) | x-external-key + Bearer |
|
||||
| `GET /api/external/my-tickets/:id` | 工单详情(JWT) | x-external-key + Bearer |
|
||||
| `POST /api/external/tickets` | 提交工单 | x-external-key |
|
||||
| `GET /api/external/all-tickets` | 全部工单 | x-external-key |
|
||||
| `GET /api/external/reported-players` | 被举报统计 | x-external-key |
|
||||
| `GET /api/external/stats` | 统计 | x-external-key |
|
||||
| `POST /api/external/auth/session` | ID+Secret 换取 SESSION | x-api-client-id + x-api-secret |
|
||||
| `POST /api/external/auth/register` | 插件注册 | Bearer SESSION |
|
||||
| `POST /api/external/tickets` | 提交工单(带 server 可选) | Bearer SESSION |
|
||||
| `GET /api/external/tickets/track` | 按追踪码查进度 | Bearer SESSION |
|
||||
| `GET /api/external/all-tickets` | 工单列表(可按子服过滤) | Bearer SESSION |
|
||||
| `GET/POST /api/external/bans` | 拉取/新增封禁 | Bearer SESSION |
|
||||
| `GET /api/external/reported-players` | 被举报统计 | Bearer SESSION |
|
||||
| `GET /api/external/servers` | 服务器列表(含别名) | Bearer SESSION |
|
||||
| `GET /api/external/stats` | 统计 | Bearer SESSION |
|
||||
|
||||
完整说明见 [docs/EXTERNAL-API.md](docs/EXTERNAL-API.md);MC 插件对接示例见 [docs/PLUGIN-GUIDE.md](docs/PLUGIN-GUIDE.md)。
|
||||
|
||||
## 部署
|
||||
|
||||
@@ -131,48 +136,52 @@ pm2 start backend/server.js --name mc-report
|
||||
proxy_pass http://127.0.0.1:3100;
|
||||
```
|
||||
|
||||
## 插件命令
|
||||
完整部署步骤见 [INSTALL.md](INSTALL.md)。
|
||||
|
||||
```
|
||||
/report reg <密码> <邮箱> [用户名] 注册(默认用户=游戏名)
|
||||
/report login <密码> [用户名] 登录
|
||||
/report report [玩家] <原因> 举报
|
||||
/report suggest <内容> 建议
|
||||
/report track [id] 追踪工单
|
||||
```
|
||||
## MC 插件对接
|
||||
|
||||
配置 `plugins/MCReport/config.yml` 填写 `api_url` 和 `external_key`。
|
||||
仓库不内置插件代码,按 [docs/PLUGIN-GUIDE.md](docs/PLUGIN-GUIDE.md) 用 Java 自行实现即可:
|
||||
|
||||
- `POST /api/external/auth/session`:ID+Secret 换取 SESSION(自动续期)
|
||||
- `POST /api/external/tickets`:游戏内提交举报/建议/申诉(带子服)
|
||||
- `GET /api/external/tickets/track`:玩家查询处理进度(tracking_token)
|
||||
- `GET/POST /api/external/bans`:同步本服封禁
|
||||
|
||||
## 目录结构
|
||||
|
||||
```
|
||||
```text
|
||||
├── backend/
|
||||
│ ├── server.js # Express 入口
|
||||
│ ├── db.js # MySQL 连接池 + 建表 + 迁移
|
||||
│ ├── mailer.js # SMTP 邮件(缓存 transporter)
|
||||
│ ├── webhook.js # Discord Webhook(SSRF 防护 + 10s 超时)
|
||||
│ ├── server.js # Express 入口(动态加载业务路由)
|
||||
│ ├── db.js # MySQL 连接池 + 建表 + 幂等迁移
|
||||
│ ├── logger.js # 邮件/系统日志(写库, 失败不影响主流程)
|
||||
│ ├── mailer.js # SMTP 邮件(自动记录日志)
|
||||
│ ├── webhook.js # Webhook 推送(SSRF 防护 + 10s 超时)
|
||||
│ ├── middleware/
|
||||
│ │ ├── auth.js # JWT(惰性缓存 secret)
|
||||
│ │ ├── security.js # 限速 + XSS 递归 + API Key 常量时间比对
|
||||
│ │ └── upload.js # 文件上传(仅读 256B 头验证魔数)
|
||||
│ │ ├── auth.js # JWT(Web 鉴权)
|
||||
│ │ ├── security.js # 限速 + XSS + API Key 校验
|
||||
│ │ └── upload.js # 文件上传(魔数校验)
|
||||
│ └── routes/
|
||||
│ ├── auth.js # 注册/登录/验证/找回密码
|
||||
│ ├── tickets.js # 工单核心(自动关闭 + 状态流转)
|
||||
│ ├── install.js # 安装向导(含 api_key 生成)
|
||||
│ ├── external.js # 外部 API(插件 + Java 服务器)
|
||||
│ ├── users.js # 用户管理(含 source 字段)
|
||||
│ ├── settings.js # 站点设置 + 邮件模板编辑
|
||||
│ ├── export.js # CSV 导出(服主专享)
|
||||
│ ├── polls.js # 投票(分组/子服/定时)
|
||||
│ ├── features.js # 更新内容(Issues 风格 + 讨论)
|
||||
│ ├── notifications.js # Webhook 配置
|
||||
│ ├── auth.js # 注册/登录/验证/找回密码/多身份
|
||||
│ ├── tickets.js # 工单核心(状态流转)
|
||||
│ ├── install.js # 安装向导
|
||||
│ ├── external.js # 外部 API(SESSION 鉴权)
|
||||
│ ├── sources.js # 动态来源管理
|
||||
│ ├── users.js # 用户管理
|
||||
│ ├── logs.js # 日志查询
|
||||
│ ├── settings.js # 站点设置 + UUID 查询
|
||||
│ ├── export.js # CSV 导出
|
||||
│ ├── polls.js # 投票 + 服务器分组
|
||||
│ ├── features.js # 更新内容
|
||||
│ ├── notifications.js # 通知配置
|
||||
│ ├── captcha.js # SVG 验证码
|
||||
│ └── uploads.js # 附件下载(鉴权)
|
||||
├── public/ # 前端 SPA (16 页面)
|
||||
├── mc-report-plugin/ # Paper 1.20+ 插件 (Maven)
|
||||
│ └── uploads.js # 附件下载
|
||||
├── public/ # 前端 SPA
|
||||
├── docs/ # 文档(API / 外部API / 插件指南)
|
||||
├── nginx.inc # Nginx 额外配置
|
||||
├── data/ # config.json + uploads/
|
||||
├── data/ # config.json + uploads/ (git 忽略)
|
||||
├── README.md
|
||||
├── INSTALL.md
|
||||
├── LICENSE # AGPLv3
|
||||
└── package.json
|
||||
```
|
||||
|
||||
|
||||
148
docs/PLUGIN-GUIDE.md
Normal file
148
docs/PLUGIN-GUIDE.md
Normal file
@@ -0,0 +1,148 @@
|
||||
# MC 插件对接指南(外部 API)
|
||||
|
||||
本文面向 Bukkit/Paper/Spigot 插件开发者,演示如何用 Java 对接 MC Report 外部 API(提交举报、同步封禁、查询进度)。完整接口定义见 [EXTERNAL-API.md](EXTERNAL-API.md)。
|
||||
|
||||
> 仓库不再内置插件代码 —— 按本指南即可自行实现;凭据(ID+Secret)在站点后台「外部API」页创建。
|
||||
|
||||
---
|
||||
|
||||
## 一、鉴权流程(Java 实现)
|
||||
|
||||
外部 API 用 **ID + Secret 换取 SESSION**,之后所有请求带 `Authorization: Bearer <SESSION>`。
|
||||
|
||||
```java
|
||||
public class McReportApi {
|
||||
private final String baseUrl; // 如 https://mc.example.com/api/external
|
||||
private final String clientId; // 后台创建: 16 位数字
|
||||
private final String clientSecret; // 后台创建: 32 位随机串
|
||||
private String sessionToken;
|
||||
private long sessionExpiresAt; // 到期时间戳(ms)
|
||||
|
||||
public McReportApi(String baseUrl, String clientId, String clientSecret) {
|
||||
this.baseUrl = baseUrl;
|
||||
this.clientId = clientId;
|
||||
this.clientSecret = clientSecret;
|
||||
}
|
||||
|
||||
/** 获取有效 SESSION, 过期则自动换新 */
|
||||
public synchronized String session() throws Exception {
|
||||
if (sessionToken != null && System.currentTimeMillis() < sessionExpiresAt) {
|
||||
return sessionToken;
|
||||
}
|
||||
JSONObject body = new JSONObject();
|
||||
HttpURLConnection conn = open("POST", "/auth/session", null);
|
||||
conn.setRequestProperty("x-api-client-id", clientId);
|
||||
conn.setRequestProperty("x-api-secret", clientSecret);
|
||||
conn.setDoOutput(true);
|
||||
conn.getOutputStream().write(body.toString().getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
JSONObject resp = readJson(conn);
|
||||
sessionToken = resp.getString("session_token");
|
||||
sessionExpiresAt = System.currentTimeMillis() + resp.getLong("expires_in") * 1000L - 60_000L; // 提前1分钟过期
|
||||
return sessionToken;
|
||||
}
|
||||
|
||||
private HttpURLConnection open(String method, String path, String token) throws Exception {
|
||||
HttpURLConnection conn = (HttpURLConnection) new URL(baseUrl + path).openConnection();
|
||||
conn.setRequestMethod(method);
|
||||
conn.setConnectTimeout(5000);
|
||||
conn.setReadTimeout(10000);
|
||||
conn.setRequestProperty("Content-Type", "application/json");
|
||||
conn.setRequestProperty("User-Agent", "MCReportPlugin/1.0");
|
||||
if (token != null) conn.setRequestProperty("Authorization", "Bearer " + token);
|
||||
return conn;
|
||||
}
|
||||
|
||||
private JSONObject readJson(HttpURLConnection conn) throws Exception {
|
||||
int code = conn.getResponseCode();
|
||||
InputStream in = code >= 200 && code < 300 ? conn.getInputStream() : conn.getErrorStream();
|
||||
String text = new String(in.readAllBytes(), StandardCharsets.UTF_8);
|
||||
JSONObject json = new JSONObject(text);
|
||||
if (code >= 400) throw new RuntimeException(json.optString("error", "HTTP " + code));
|
||||
return json;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 二、提交举报工单
|
||||
|
||||
```java
|
||||
/** 玩家在游戏内执行 /report 命令时调用 */
|
||||
public JSONObject submitReport(String reporterName, String targetName, String reason,
|
||||
String serverName, String description) throws Exception {
|
||||
String token = session();
|
||||
JSONObject body = new JSONObject();
|
||||
body.put("type", "report");
|
||||
body.put("title", "游戏内举报: " + targetName);
|
||||
body.put("reporter_game_name", reporterName);
|
||||
body.put("target_game_name", targetName);
|
||||
body.put("reason", reason);
|
||||
body.put("description", description);
|
||||
body.put("server", serverName); // 子服别名或「分组/子服」
|
||||
|
||||
HttpURLConnection conn = open("POST", "/tickets", token);
|
||||
conn.setDoOutput(true);
|
||||
conn.getOutputStream().write(body.toString().getBytes(StandardCharsets.UTF_8));
|
||||
JSONObject resp = readJson(conn);
|
||||
// resp: {"id": 123, "tracking_token": "..."}
|
||||
return resp;
|
||||
}
|
||||
```
|
||||
|
||||
**返回的 `tracking_token` 请保存**(例如写进玩家持久数据),玩家可用它查询处理进度:
|
||||
|
||||
```java
|
||||
public JSONObject trackTicket(String trackingToken) throws Exception {
|
||||
return readJson(open("GET", "/tickets/track?token=" + URLEncoder.encode(trackingToken, "UTF-8"), session()));
|
||||
}
|
||||
```
|
||||
|
||||
## 三、拉取/同步封禁
|
||||
|
||||
```java
|
||||
/** 定时任务: 每 5 分钟同步一次本服生效封禁 */
|
||||
public JSONArray fetchActiveBans(String serverName) throws Exception {
|
||||
String token = session();
|
||||
String path = "/bans?status=active&limit=500"
|
||||
+ (serverName != null && !serverName.isEmpty()
|
||||
? "&server=" + URLEncoder.encode(serverName, "UTF-8") : "");
|
||||
return readJson(open("GET", path, token)).getJSONArray("data");
|
||||
}
|
||||
|
||||
/** 服务器内封禁玩家后同步到系统 */
|
||||
public JSONObject createBan(String playerName, String reason, String type,
|
||||
String duration, String serverName) throws Exception {
|
||||
JSONObject body = new JSONObject();
|
||||
body.put("player_name", playerName);
|
||||
body.put("reason", reason);
|
||||
body.put("type", type); // ban / mute / warn
|
||||
body.put("duration", duration); // 如 "7d", 空=永久
|
||||
body.put("server", serverName);
|
||||
return readJson(open("POST", "/bans", session())); // 需带 body
|
||||
}
|
||||
```
|
||||
|
||||
> 注意:`POST /bans` 需设置 `setDoOutput(true)` 并写入 body(与 submitReport 相同写法)。
|
||||
|
||||
## 四、配置
|
||||
|
||||
`plugins/<你的插件>/config.yml`:
|
||||
|
||||
```yaml
|
||||
# MC Report 外部 API 配置
|
||||
api_url: "https://mc.example.com/api/external"
|
||||
client_id: "1829473056482917" # 后台「外部API」创建
|
||||
client_secret: "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6"
|
||||
server_name: "survival" # 本服别名(后台服务器管理配置)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 常见问题
|
||||
|
||||
| 问题 | 处理 |
|
||||
|------|------|
|
||||
| `401` | ID/Secret 错误或客户端被停用;检查后台「外部API」 |
|
||||
| `401 SESSION 无效或已过期` | 重新调用 `/auth/session` 换取(示例代码已自动处理) |
|
||||
| `400 待处理工单已达上限` | 同一玩家待处理工单最多 5 个 |
|
||||
| 换服/改名 | 用 `server` 参数按子服隔离数据;玩家身份以 `game_name` 为准 |
|
||||
@@ -1,53 +0,0 @@
|
||||
# MC Report Plugin - Paper 1.20.4
|
||||
|
||||
游戏内举报/注册/登录插件
|
||||
|
||||
## 编译
|
||||
|
||||
```bash
|
||||
cd mc-report-plugin
|
||||
mvn clean package
|
||||
```
|
||||
|
||||
输出 `target/mc-report-plugin-1.0.0.jar`
|
||||
|
||||
## 安装
|
||||
|
||||
1. 将 jar 放入服务器 `plugins/` 目录
|
||||
2. 重启服务器
|
||||
3. 编辑 `plugins/MCReport/config.yml`:
|
||||
|
||||
```yaml
|
||||
api_url: "http://localhost:3100/api/external"
|
||||
external_key: "你的external_api_key"
|
||||
```
|
||||
|
||||
external_api_key 在 Web 安装时自动生成,位于 `data/config.json`
|
||||
|
||||
## 权限
|
||||
|
||||
| 节点 | 默认 | 说明 |
|
||||
|------|------|------|
|
||||
| `SeaStudio.Report.use` | true | 使用举报命令 |
|
||||
| `SeaStudio.Report.admin` | op | 管理员标识 |
|
||||
|
||||
LP 命令:
|
||||
```
|
||||
lp group admin permission set SeaStudio.Report.admin true
|
||||
```
|
||||
|
||||
## 命令
|
||||
|
||||
| 命令 | 说明 |
|
||||
|------|------|
|
||||
| `/report reg <密码> <邮箱> [用户名]` | 注册(默认用户名=游戏名, 来源=皮肤站) |
|
||||
| `/report login <密码> [用户名]` | 登录 |
|
||||
| `/report report [玩家] <原因>` | 举报(自动检测管理员投诉) |
|
||||
| `/report suggest <内容>` | 建议 |
|
||||
| `/report track [id]` | 追踪工单 |
|
||||
|
||||
## 依赖
|
||||
|
||||
- Spigot / Paper 1.8 ~ 1.21+(全版本通用)
|
||||
- Java 17+
|
||||
- Maven 3.8+
|
||||
@@ -1,47 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<groupId>com.mcreport</groupId>
|
||||
<artifactId>mc-report-plugin</artifactId>
|
||||
<version>1.0.0</version>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<properties>
|
||||
<maven.compiler.source>17</maven.compiler.source>
|
||||
<maven.compiler.target>17</maven.compiler.target>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
</properties>
|
||||
|
||||
<repositories>
|
||||
<repository>
|
||||
<id>spigotmc-repo</id>
|
||||
<url>https://hub.spigotmc.org/nexus/content/repositories/snapshots/</url>
|
||||
</repository>
|
||||
</repositories>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.spigotmc</groupId>
|
||||
<artifactId>spigot-api</artifactId>
|
||||
<version>1.13.2-R0.1-SNAPSHOT</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>3.11.0</version>
|
||||
<configuration>
|
||||
<source>17</source>
|
||||
<target>17</target>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
@@ -1,102 +0,0 @@
|
||||
package com.mcreport.plugin;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
|
||||
import java.io.OutputStream;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URI;
|
||||
import java.net.URL;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
public class ApiClient {
|
||||
|
||||
private final String apiUrl;
|
||||
private final String externalKey;
|
||||
private final Gson gson = new Gson();
|
||||
|
||||
public ApiClient(String apiUrl, String externalKey) {
|
||||
this.apiUrl = apiUrl;
|
||||
this.externalKey = externalKey;
|
||||
}
|
||||
|
||||
public JsonObject request(String method, String path, JsonObject body, String token) {
|
||||
try {
|
||||
URL url = new URI(apiUrl + path).toURL();
|
||||
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
|
||||
conn.setRequestMethod(method);
|
||||
conn.setRequestProperty("Content-Type", "application/json");
|
||||
conn.setRequestProperty("x-external-key", externalKey);
|
||||
if (token != null) conn.setRequestProperty("Authorization", "Bearer " + token);
|
||||
conn.setConnectTimeout(5000);
|
||||
conn.setReadTimeout(5000);
|
||||
|
||||
if (body != null) {
|
||||
conn.setDoOutput(true);
|
||||
try (OutputStream os = conn.getOutputStream()) {
|
||||
byte[] input = gson.toJson(body).getBytes(StandardCharsets.UTF_8);
|
||||
os.write(input);
|
||||
}
|
||||
}
|
||||
|
||||
int code = conn.getResponseCode();
|
||||
String response = code >= 200 && code < 300
|
||||
? new String(conn.getInputStream().readAllBytes(), StandardCharsets.UTF_8)
|
||||
: new String(conn.getErrorStream().readAllBytes(), StandardCharsets.UTF_8);
|
||||
conn.disconnect();
|
||||
|
||||
JsonObject json = JsonParser.parseString(response).getAsJsonObject();
|
||||
json.addProperty("_status", code);
|
||||
return json;
|
||||
} catch (Exception e) {
|
||||
JsonObject err = new JsonObject();
|
||||
err.addProperty("error", e.getMessage());
|
||||
err.addProperty("_status", 500);
|
||||
return err;
|
||||
}
|
||||
}
|
||||
|
||||
public JsonObject login(String username, String password) {
|
||||
JsonObject body = new JsonObject();
|
||||
body.addProperty("username", username);
|
||||
body.addProperty("password", password);
|
||||
return request("POST", "/auth/login", body, null);
|
||||
}
|
||||
|
||||
public JsonObject register(String username, String password, String email, String gameName, String gameUid) {
|
||||
JsonObject body = new JsonObject();
|
||||
body.addProperty("username", username);
|
||||
body.addProperty("password", password);
|
||||
body.addProperty("email", email);
|
||||
body.addProperty("game_name", gameName);
|
||||
body.addProperty("game_uid", gameUid);
|
||||
body.addProperty("source", "skin");
|
||||
return request("POST", "/auth/register", body, null);
|
||||
}
|
||||
|
||||
public JsonObject submitTicket(String token, String type, String title,
|
||||
String reporterName, String reporterUid,
|
||||
String targetName, String targetUid,
|
||||
String reason, String description, boolean isAdminComplaint) {
|
||||
JsonObject body = new JsonObject();
|
||||
body.addProperty("type", type);
|
||||
body.addProperty("title", title);
|
||||
body.addProperty("reporter_game_name", reporterName);
|
||||
body.addProperty("reporter_game_uid", reporterUid);
|
||||
if (targetName != null && !targetName.isEmpty()) body.addProperty("target_game_name", targetName);
|
||||
if (targetUid != null) body.addProperty("target_game_uid", targetUid);
|
||||
if (reason != null && !reason.isEmpty()) body.addProperty("reason", reason);
|
||||
if (description != null && !description.isEmpty()) body.addProperty("description", description);
|
||||
if (isAdminComplaint) body.addProperty("is_admin_complaint", "1");
|
||||
return request("POST", "/tickets", body, token);
|
||||
}
|
||||
|
||||
public JsonObject getMyTickets(String token) {
|
||||
return request("GET", "/my-tickets?limit=10", null, token);
|
||||
}
|
||||
|
||||
public JsonObject getTicket(String token, String id) {
|
||||
return request("GET", "/my-tickets/" + id, null, token);
|
||||
}
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
package com.mcreport.plugin;
|
||||
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.plugin.java.JavaPlugin;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
public final class MCReportPlugin extends JavaPlugin {
|
||||
|
||||
private ApiClient apiClient;
|
||||
private final Map<UUID, String> sessions = new HashMap<>();
|
||||
private final Map<UUID, String> loginNames = new HashMap<>();
|
||||
private List<String> adminPerms;
|
||||
|
||||
@Override
|
||||
public void onEnable() {
|
||||
saveDefaultConfig();
|
||||
String apiUrl = getConfig().getString("api_url");
|
||||
String externalKey = getConfig().getString("external_key");
|
||||
apiClient = new ApiClient(apiUrl, externalKey);
|
||||
adminPerms = getConfig().getStringList("admin_permissions");
|
||||
|
||||
getCommand("report").setExecutor(new ReportCommand(this));
|
||||
|
||||
getLogger().info("MC举报系统插件已启用");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisable() {
|
||||
getLogger().info("MC举报系统插件已卸载");
|
||||
}
|
||||
|
||||
public ApiClient getApiClient() { return apiClient; }
|
||||
public List<String> getAdminPerms() { return adminPerms; }
|
||||
|
||||
public String getToken(Player player) {
|
||||
return sessions.get(player.getUniqueId());
|
||||
}
|
||||
|
||||
public void setToken(Player player, String token) {
|
||||
sessions.put(player.getUniqueId(), token);
|
||||
}
|
||||
|
||||
public void removeToken(Player player) {
|
||||
sessions.remove(player.getUniqueId());
|
||||
loginNames.remove(player.getUniqueId());
|
||||
}
|
||||
|
||||
public void setLoginName(Player player, String name) { loginNames.put(player.getUniqueId(), name); }
|
||||
public String getLoginName(Player player) { return loginNames.getOrDefault(player.getUniqueId(), player.getName()); }
|
||||
|
||||
public boolean isOnlineAdmin(Player target) {
|
||||
if (target.isOp()) return true;
|
||||
for (String perm : adminPerms) {
|
||||
if (target.hasPermission(perm)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -1,213 +0,0 @@
|
||||
package com.mcreport.plugin;
|
||||
|
||||
import com.google.gson.JsonArray;
|
||||
import com.google.gson.JsonObject;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.command.Command;
|
||||
import org.bukkit.command.CommandExecutor;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
public class ReportCommand implements CommandExecutor {
|
||||
|
||||
private final MCReportPlugin plugin;
|
||||
|
||||
public ReportCommand(MCReportPlugin plugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
|
||||
if (!(sender instanceof Player player)) {
|
||||
sender.sendMessage("§cPlayer only");
|
||||
return true;
|
||||
}
|
||||
|
||||
if (args.length == 0) {
|
||||
showHelp(player);
|
||||
return true;
|
||||
}
|
||||
|
||||
switch (args[0].toLowerCase()) {
|
||||
case "reg": case "register": return handleRegister(player, args);
|
||||
case "login": return handleLogin(player, args);
|
||||
case "report": case "r": return handleSubmit(player, args, "report");
|
||||
case "suggest": case "s": return handleSubmit(player, args, "suggestion");
|
||||
case "track": case "t": return handleTrack(player, args);
|
||||
default: showHelp(player); return true;
|
||||
}
|
||||
}
|
||||
|
||||
private void showHelp(Player player) {
|
||||
if (plugin.getToken(player) == null) {
|
||||
player.sendMessage("§6===== MCReport =====");
|
||||
player.sendMessage("§e/report reg <password> <email> [username] §7- Register");
|
||||
player.sendMessage("§e/report login <password> [username] §7- Login");
|
||||
} else {
|
||||
player.sendMessage("§6===== MCReport =====");
|
||||
player.sendMessage("§e/report report [player] <reason> §7- Report a player");
|
||||
player.sendMessage("§e/report suggest <content> §7- Submit suggestion");
|
||||
player.sendMessage("§e/report track [id] §7- List/view tickets");
|
||||
}
|
||||
}
|
||||
|
||||
private boolean handleLogin(Player player, String[] args) {
|
||||
if (args.length < 2) { player.sendMessage("§cUsage: /report login <password> [username]"); return true; }
|
||||
String pwd = args[1];
|
||||
String username = args.length > 2 ? args[2] : player.getName();
|
||||
|
||||
player.sendMessage("§7Logging in...");
|
||||
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
|
||||
JsonObject res = plugin.getApiClient().login(username, pwd);
|
||||
int status = res.get("_status").getAsInt();
|
||||
if (status == 200) {
|
||||
plugin.setToken(player, res.get("token").getAsString());
|
||||
plugin.setLoginName(player, username);
|
||||
player.sendMessage("§aLogged in as §e" + username);
|
||||
} else {
|
||||
player.sendMessage("§cLogin failed: " + res.get("error").getAsString());
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean handleRegister(Player player, String[] args) {
|
||||
if (args.length < 3) { player.sendMessage("§cUsage: /report reg <password> <email> [username]"); return true; }
|
||||
String pwd = args[1];
|
||||
String email = args[2];
|
||||
String username = args.length > 3 ? args[3] : player.getName();
|
||||
|
||||
if (pwd.length() < 6) { player.sendMessage("§cPassword must be at least 6 characters"); return true; }
|
||||
|
||||
player.sendMessage("§7Registering...");
|
||||
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
|
||||
JsonObject res = plugin.getApiClient().register(
|
||||
username, pwd, email,
|
||||
player.getName(), player.getUniqueId().toString()
|
||||
);
|
||||
int status = res.get("_status").getAsInt();
|
||||
if (status == 201) {
|
||||
player.sendMessage("§a" + res.get("message").getAsString());
|
||||
player.sendMessage("§6Username: §e" + username + " §6Game: §e" + player.getName());
|
||||
} else {
|
||||
player.sendMessage("§cRegister failed: " + res.get("error").getAsString());
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean handleSubmit(Player player, String[] args, String type) {
|
||||
if (plugin.getToken(player) == null) { player.sendMessage("§cLogin first: /report login <password>"); return true; }
|
||||
|
||||
int contentStart = 1;
|
||||
String targetName = null;
|
||||
String targetUid = null;
|
||||
boolean isAdminComplaint = false;
|
||||
|
||||
if (type.equals("report") && args.length > 1) {
|
||||
Player target = Bukkit.getPlayer(args[1]);
|
||||
if (target != null && target.isOnline()) {
|
||||
targetName = target.getName();
|
||||
targetUid = target.getUniqueId().toString();
|
||||
contentStart = 2;
|
||||
isAdminComplaint = plugin.isOnlineAdmin(target);
|
||||
}
|
||||
}
|
||||
|
||||
if (args.length <= contentStart) {
|
||||
player.sendMessage("§cPlease provide " + (type.equals("report")?"a reason":"content"));
|
||||
return true;
|
||||
}
|
||||
|
||||
String content = String.join(" ", Arrays.copyOfRange(args, contentStart, args.length));
|
||||
String title = targetName != null
|
||||
? "In-game report: " + targetName
|
||||
: type.equals("suggestion")
|
||||
? "In-game suggestion: " + player.getName()
|
||||
: "In-game report: " + player.getName();
|
||||
|
||||
player.sendMessage("§7Submitting...");
|
||||
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
|
||||
JsonObject res = plugin.getApiClient().submitTicket(
|
||||
plugin.getToken(player), type, title,
|
||||
player.getName(), player.getUniqueId().toString(),
|
||||
targetName, targetUid,
|
||||
type.equals("report")?content:null,
|
||||
type.equals("suggestion")?content:null,
|
||||
isAdminComplaint
|
||||
);
|
||||
if (res.get("_status").getAsInt() == 200 || res.get("_status").getAsInt() == 201) {
|
||||
player.sendMessage("§aSubmitted! Ticket §e#" + res.get("id").getAsInt());
|
||||
player.sendMessage("§7Use §e/report track §7to check progress");
|
||||
} else {
|
||||
player.sendMessage("§cFailed: " + res.get("error").getAsString());
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean handleTrack(Player player, String[] args) {
|
||||
if (plugin.getToken(player) == null) { player.sendMessage("§cLogin first"); return true; }
|
||||
|
||||
player.sendMessage("§7Loading...");
|
||||
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
|
||||
if (args.length > 1) {
|
||||
JsonObject res = plugin.getApiClient().getTicket(plugin.getToken(player), args[1]);
|
||||
showTicketDetail(player, res);
|
||||
} else {
|
||||
JsonObject res = plugin.getApiClient().getMyTickets(plugin.getToken(player));
|
||||
showTicketList(player, res);
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
private void showTicketList(Player player, JsonObject res) {
|
||||
if (res.get("_status").getAsInt() != 200 || !res.has("list")) {
|
||||
player.sendMessage("§cFailed to load tickets");
|
||||
return;
|
||||
}
|
||||
JsonArray list = res.getAsJsonArray("list");
|
||||
if (list.size() == 0) {
|
||||
player.sendMessage("§7No tickets yet");
|
||||
return;
|
||||
}
|
||||
player.sendMessage("§6===== Your Tickets (last " + list.size() + ") =====");
|
||||
for (int i = 0; i < list.size(); i++) {
|
||||
JsonObject t = list.get(i).getAsJsonObject();
|
||||
String type = t.get("type").getAsString();
|
||||
String typeLabel = type.equals("report")?"雷":type.equals("suggestion")?"灯":type.equals("appeal")?"锤":"?";
|
||||
String status = t.get("status").getAsString();
|
||||
String statusColor = "";
|
||||
switch (status) {
|
||||
case "pending": statusColor = "§e"; break;
|
||||
case "processing": statusColor = "§d"; break;
|
||||
case "resolved": statusColor = "§a"; break;
|
||||
case "rejected": statusColor = "§c"; break;
|
||||
case "closed": statusColor = "§7"; break;
|
||||
case "awaiting_info": statusColor = "§6"; break;
|
||||
case "appealing": statusColor = "§c"; break;
|
||||
}
|
||||
player.sendMessage(" §e#" + t.get("id").getAsInt() + " §7" + statusColor + status + " §f" + t.get("title").getAsString().substring(0, Math.min(30, t.get("title").getAsString().length())));
|
||||
}
|
||||
player.sendMessage("§7Use §e/report track <id> §7for details");
|
||||
}
|
||||
|
||||
private void showTicketDetail(Player player, JsonObject res) {
|
||||
if (res.get("_status").getAsInt() != 200 || res.has("error")) {
|
||||
player.sendMessage("§cTicket not found");
|
||||
return;
|
||||
}
|
||||
JsonObject t = res;
|
||||
player.sendMessage("§6===== Ticket #" + t.get("id").getAsInt() + " =====");
|
||||
player.sendMessage("§eTitle: §f" + t.get("title").getAsString());
|
||||
player.sendMessage("§eStatus: §f" + t.get("status").getAsString());
|
||||
player.sendMessage("§eType: §f" + t.get("type").getAsString());
|
||||
if (t.has("assigned_to") && !t.get("assigned_to").isJsonNull()) {
|
||||
player.sendMessage("§eHandler: §f#" + t.get("assigned_to").getAsInt());
|
||||
}
|
||||
player.sendMessage("§eDate: §7" + t.get("created_at").getAsString());
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
# MC举报系统 - 插件配置
|
||||
api_url: "http://localhost:3100/api/external"
|
||||
external_key: ""
|
||||
|
||||
# 管理权限检测 - 拥有以下任一权限的在线玩家视为管理员
|
||||
admin_permissions:
|
||||
- "op"
|
||||
- "SeaStudio.Report.admin"
|
||||
|
||||
# 踢出提示
|
||||
appeal_url: "http://localhost:3100/#/home/appeal"
|
||||
@@ -1,25 +0,0 @@
|
||||
name: MCReport
|
||||
version: 1.0.0
|
||||
main: com.mcreport.plugin.MCReportPlugin
|
||||
author: Sea-Studio
|
||||
description: MC Report System Plugin
|
||||
|
||||
commands:
|
||||
report:
|
||||
description: Game reporting & account management
|
||||
usage: |
|
||||
§6===== MCReport =====
|
||||
§e/report reg <password> <email> [username] §7- Register
|
||||
§e/report login <password> [username] §7- Login
|
||||
§e/report report [player] <reason> §7- Report player
|
||||
§e/report suggest <content> §7- Submit suggestion
|
||||
§e/report track [id] §7- Track tickets
|
||||
aliases: [jubao,举报]
|
||||
|
||||
permissions:
|
||||
SeaStudio.Report.use:
|
||||
description: Use report functions
|
||||
default: true
|
||||
SeaStudio.Report.admin:
|
||||
description: Identifies as report admin
|
||||
default: op
|
||||
Reference in New Issue
Block a user