这个东西是什么
更现代的游戏社区赞助工具,带 PayPal/Stripe/Discord/CFTools 集成;需要配置支付、Steam/Discord 和数据库后才能完整运行。
预览说明
这个项目不是完整商城前台,不能直接像 PHP 店铺那样点购买。这里给你看源码说明、目录用途和关键代码,方便判断它是不是你要的东西。
README
# Server donation tool
[](https://go2tech.de/discord)
[](https://hub.docker.com/r/droidwiki/server-donation-tool)
[](https://github.com/FlorianSW/server-donation-tool/actions/workflows/test.yml)
[](https://floriansw.github.io/server-donation-tool/)
<p align="center">
<img src="https://github.com/FlorianSW/cftools-server-donation/raw/main/.github/screenshot.png" alt="Main view of the website" width="420px">
</p>
This server donation tool is meant to make it easier for gaming communities to allow their members to donate and earn differnet perks for their donation.
It seamlessly integrates with different platforms in order to automatically provision and setup perks for donators to reduce manual efforts for community admins and moderators.
## Key Features
- Self-services, guided donation on a modern website
- Integrated with PayPal for easy, self-service and secure payments
- Fully configurable packages, including configuring perks a donator earns, and the amount one needs to donate
- Perks available: Priority Queue (based on CFTools Cloud), Discord roles
- Discord notifications
## Installation & Configuration
The `config.full.yml` provides extensive documentation for each configuration option.
With the `config.example.yml`, there is a minimum configuration file, where you just need to replace the placeholder values to get started (it does not contain all the possible configuration values).
Documentation, as well as a getting started guide is [available in the documentation](https://floriansw.github.io/server-donation-tool/).
## Support and Collaboration
If you have a question regarding the use or the setup of this tool, feel free to [join my Discord](https://go2tech.de/discord) and get the `Tools` role in the #get-your-roles-here channel.
I'll be happy to assist you if I can with your question there :)
The initial configuration of the tool might look a bit overwhelming, but I'm happy to help you with whatever problem you're facing, just contact me.
Given you think you found a bug, a missing feature or something that can be improved, I encourage you to open an issue in [this Github repository](https://github.com/FlorianSW/cftools-server-donation).
Please, do not use issues for your questions or for getting support.
Given you're a developer and want to contribute code to this project, feel free to do so.
Simply fork the project, make your code changes and open a pull request against this repository.
I appreciate your help and support and will try to review your pull request as soon as I can.
首页/捐赠控制器代码
import {Request, Response, Router} from 'express';
import {AppConfig} from '../../domain/app-config';
import {DonationType, Login, Package, Price, PriceType} from '../../domain/package';
import {Logger} from 'winston';
import csrf from 'csurf';
import {inject, singleton} from 'tsyringe';
import {UserData} from '../../service/user-data';
interface categoryPackage {
category?: string | undefined,
packages: Package[],
}
@singleton()
export class StartController {
public readonly router: Router = Router();
constructor(
@inject('AppConfig') private readonly config: AppConfig,
@inject('availablePackages') private readonly packages: Package[],
@inject(UserData) private readonly data: UserData,
@inject('Logger') private readonly log: Logger
) {
const csrfProtection = csrf();
this.router.post('/selectPackage', csrfProtection, this.selectPackage.bind(this));
this.router.get('/', csrfProtection, this.startPage.bind(this));
}
private async startPage(req: Request, res: Response) {
req.user = await this.data.onRefresh(req.user);
const packages: categoryPackage[] = [{
packages: this.packages.filter((p) => !this.config.packageCategories.includes(p.category)),
}];
for (let cat of this.config.packageCategories) {
const p = this.packages.filter((p) => p.category === cat);
if (p.length === 0) {
continue
}
packages.push({
category: cat,
packages: p,
});
}
res.render('steps/package_selection', {
withOpenGraph: true,
csrfToken: req.csrfToken(),
requiredLogins: (p: Package): Login[] => {
return Array.from(new Set(p.perks.flatMap((p) => p.requiresLogins())));
},
showDonationTarget: !!this.config.app.community?.donationTarget?.monthly,
availablePackages: packages,
subscribedPackages: req.user?.subscribedPackages || {},
});
}
private price(req: Request, pack: Package): Price {
const price = {
...pack.price
};
if (req.body[`price-${pack.id}`]) {
if (pack.price.type === PriceType.FIXED) {
throw Error('VariablePriceForFixedPackage');
}
let amount = req.body[`price-${pack.id}`].replace(',', '.');
if (Math.sign(amount) !== 1) {
throw new Error('Invalid variable price detected: ' + amount);
}
price.amount = amount;
}
return price;
}
private async selectPackage(req: Request, res: Response) {
const selectedPackage = this.packages.find((p) => p.id === parseInt(req.body.package));
let type = DonationType.OneTime;
if (req.body['perks-for'] === 'subscribe') {
type = DonationType.Subscription;
} else if (req.body['perks-for'] === 'gift') {
type = DonationType.Gift;
} else if (req.body['perks-for'] !== 'donate') {
res.redirect('/');
}
try {
req.session.selectedPackage = {
id: selectedPackage.id,
price: this.price(req, selectedPackage),
perkDetails: {},
type: type,
};
res.redirect('/donate');
} catch (e) {
if (e.message === 'VariablePriceForFixedPackage') {
this.log.warn(`Discord user ${req.user.discord.id} requested variable price for fixed package.`);
res.redirect('/');
} else {
throw e;
}
}
}
}