Files
2026-09-04 19:46:58 +08:00

223 lines
7.5 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package main
import (
"bufio"
"errors"
"fmt"
"os"
"strconv"
"strings"
)
// Config 全部来自 config.env(KEY=VALUE)或同名环境变量(环境变量优先)。
type Config struct {
Listen string
BaseURL string
DBPath string
AdminPassword string // 主站登录口令(路径留空 + 此口令)
SiteTitle string
RepoURL string
SourceURL string // 本站自己的开源仓库,页脚展示(留空不显示)
AuthorGitHub string // 页脚作者 GitHub 链接
AuthorX string // 页脚作者 X 链接
// 主站资料(首次启动写入数据库,之后在管理页改)
MainName string
MainSlogan string
MainStory string
MainAvatar string
MainX string
// 收款:配置 BPG_URL 则主站走 BinancePayTool 网关自动确认,子站也能绑定自己的币安 Key;否则主站走直接转账
BPGURL string
BPGKey string
MainReceiveLink string
MainPayID string
Currency string
PresetAmounts []string
MinAmountE8 int64
MaxAmountE8 int64
OrderTTL int64 // 秒
SubsitesEnabled bool
SubsiteReview bool // 新子站默认未收录(不进榜、noindex),主站后台收录
TrustProxy bool
TrustHeader string // 反代传真实 IP 的头:X-Forwarded-For(取最后一跳)或 CF-Connecting-IP
// X(推特):发推验证 / 头像抓取的接口基址,测试时可指向 mock
XTweetAPI string
XAvatarAPI string
AvatarDir string
CoinsPerDay int64 // 每人每站每天可丢的钢镚数
CoinsIPCap int64 // 每 IP 每站每天上限(防脚本)
CoinsIPTotal int64 // 每 IP 每天全站总上限,0 = 不限
CoinExp int64 // 一个钢镚 = 多少 EXP
MoneyExp int64 // 1 个币 = 多少 EXP(默认 10,即 0.1 U = 1 EXP)
}
func loadConfig(path string) (*Config, error) {
vals := map[string]string{}
if f, err := os.Open(path); err == nil {
sc := bufio.NewScanner(f)
for sc.Scan() {
line := strings.TrimSpace(sc.Text())
if line == "" || strings.HasPrefix(line, "#") {
continue
}
k, v, ok := strings.Cut(line, "=")
if !ok {
continue
}
v = strings.TrimSpace(v)
if len(v) >= 2 && (v[0] == '"' && v[len(v)-1] == '"' || v[0] == '\'' && v[len(v)-1] == '\'') {
v = v[1 : len(v)-1]
}
vals[strings.TrimSpace(k)] = v
}
f.Close()
} else if !os.IsNotExist(err) {
return nil, err
}
get := func(k, def string) string {
if v := os.Getenv(k); v != "" {
return v
}
if v, ok := vals[k]; ok && v != "" {
return v
}
return def
}
getBool := func(k string, def bool) bool {
v := strings.ToLower(get(k, ""))
if v == "" {
return def
}
return v == "1" || v == "true" || v == "yes" || v == "on"
}
getInt := func(k string, def int64) (int64, error) {
v := get(k, "")
if v == "" {
return def, nil
}
n, err := strconv.ParseInt(v, 10, 64)
if err != nil {
return 0, fmt.Errorf("%s 须为整数: %q", k, v)
}
return n, nil
}
var err error
c := &Config{
Listen: get("LISTEN", "127.0.0.1:8124"),
BaseURL: strings.TrimRight(get("BASE_URL", "http://127.0.0.1:8124"), "/"),
DBPath: get("DB_PATH", "./beggar.db"),
AdminPassword: get("ADMIN_PASSWORD", get("ADMIN_TOKEN", "")),
SiteTitle: get("SITE_TITLE", "赛博要饭"),
RepoURL: get("REPO_URL", "https://github.com/qianyubtc/BinancePayTool"),
SourceURL: get("SOURCE_URL", "https://github.com/qianyubtc/newbeggar"),
AuthorGitHub: get("AUTHOR_GITHUB", "https://github.com/qianyubtc"),
AuthorX: get("AUTHOR_X", "https://x.com/qianyuwing"),
MainName: get("MAIN_NAME", "站长"),
MainSlogan: get("MAIN_SLOGAN", "行行好,赏口饭吃 🙏"),
MainStory: get("MAIN_STORY", ""),
MainAvatar: get("MAIN_AVATAR", "🥣"),
MainX: get("MAIN_X", ""),
BPGURL: strings.TrimRight(get("BPG_URL", ""), "/"),
BPGKey: get("BPG_KEY", ""),
MainReceiveLink: get("MAIN_RECEIVE_LINK", ""),
MainPayID: get("MAIN_PAY_ID", ""),
Currency: strings.ToUpper(get("CURRENCY", "USDT")),
SubsitesEnabled: getBool("SUBSITES_ENABLED", true),
SubsiteReview: getBool("SUBSITE_REVIEW", false),
TrustProxy: getBool("TRUST_PROXY", false),
TrustHeader: get("TRUST_PROXY_HEADER", "X-Forwarded-For"),
XTweetAPI: strings.TrimRight(get("X_TWEET_API", "https://cdn.syndication.twimg.com"), "/"),
XAvatarAPI: strings.TrimRight(get("X_AVATAR_API", "https://unavatar.io"), "/"),
AvatarDir: get("AVATAR_DIR", "./avatars"),
}
if c.CoinsPerDay, err = getInt("COINS_PER_DAY", 1); err != nil {
return nil, err
}
if c.CoinsIPCap, err = getInt("COINS_IP_CAP", 3); err != nil {
return nil, err
}
if c.CoinsIPTotal, err = getInt("COINS_IP_TOTAL", 30); err != nil {
return nil, err
}
if c.CoinsPerDay < 1 || c.CoinsPerDay > 100 || c.CoinsIPCap < c.CoinsPerDay {
return nil, errors.New("COINS_PER_DAY / COINS_IP_CAP 不合理")
}
if c.CoinsIPTotal < 0 || (c.CoinsIPTotal > 0 && c.CoinsIPTotal < c.CoinsIPCap) {
return nil, errors.New("COINS_IP_TOTAL 不能小于 COINS_IP_CAP(0 表示不限)")
}
if c.CoinExp, err = getInt("COIN_EXP", 1); err != nil {
return nil, err
}
if c.MoneyExp, err = getInt("MONEY_EXP", 10); err != nil {
return nil, err
}
if c.CoinExp < 0 || c.CoinExp > 1000 || c.MoneyExp < 1 || c.MoneyExp > 1000 {
return nil, errors.New("COIN_EXP / MONEY_EXP 不合理(COIN_EXP 0–1000,MONEY_EXP 1–1000)")
}
if !strings.HasPrefix(c.BaseURL, "http://") && !strings.HasPrefix(c.BaseURL, "https://") {
return nil, errors.New("BASE_URL 须以 http:// 或 https:// 开头")
}
for _, p := range c.PresetAmounts {
if e8, _ := parseAmountE8(p, 8); e8 < c.MinAmountE8 || e8 > c.MaxAmountE8 {
return nil, fmt.Errorf("PRESET_AMOUNTS 里的 %s 不在 MIN_AMOUNT–MAX_AMOUNT 之间", p)
}
}
if len(c.AdminPassword) < 8 {
return nil, errors.New("ADMIN_PASSWORD 未设置或太短(≥8 字符),可用 -gen-key 生成一个填入 config.env")
}
if c.BPGURL == "" && c.MainReceiveLink == "" && c.MainPayID == "" {
return nil, errors.New("主站至少配置一种收款方式:BPG_URL+BPG_KEY(网关自动确认)或 MAIN_RECEIVE_LINK / MAIN_PAY_ID(直接转账)")
}
if c.BPGURL != "" && c.BPGKey == "" {
return nil, errors.New("配置了 BPG_URL 就必须配置 BPG_KEY")
}
if c.MainReceiveLink != "" {
l, err := validateBinanceLink(c.MainReceiveLink)
if err != nil {
return nil, fmt.Errorf("MAIN_RECEIVE_LINK: %w", err)
}
c.MainReceiveLink = l
}
if c.MainX != "" {
x, err := validateXLink(c.MainX)
if err != nil {
return nil, fmt.Errorf("MAIN_X: %w", err)
}
c.MainX = x
}
for _, p := range strings.Split(get("PRESET_AMOUNTS", "0.1,1"), ",") {
p = strings.TrimSpace(p)
if p == "" {
continue
}
e8, err := parseAmountE8(p, 2)
if err != nil {
return nil, fmt.Errorf("PRESET_AMOUNTS 含非法金额 %q", p)
}
c.PresetAmounts = append(c.PresetAmounts, fmtE8(e8))
}
if c.MinAmountE8, err = parseAmountE8(get("MIN_AMOUNT", "0.1"), 8); err != nil {
return nil, fmt.Errorf("MIN_AMOUNT: %w", err)
}
if c.MaxAmountE8, err = parseAmountE8(get("MAX_AMOUNT", "10000"), 8); err != nil {
return nil, fmt.Errorf("MAX_AMOUNT: %w", err)
}
if c.MinAmountE8 <= 0 || c.MaxAmountE8 < c.MinAmountE8 {
return nil, errors.New("MIN_AMOUNT / MAX_AMOUNT 不合理")
}
if c.OrderTTL, err = getInt("ORDER_TTL", 900); err != nil {
return nil, err
}
if c.OrderTTL < 120 || c.OrderTTL > 86400 {
return nil, errors.New("ORDER_TTL 须在 120–86400 秒之间")
}
return c, nil
}