mirror of
https://github.com/perfect-panel/ppanel-web.git
synced 2026-08-29 05:52:08 -04:00
Initial
This commit is contained in:
@@ -0,0 +1 @@
|
|||||||
|
/target
|
||||||
Generated
+5535
File diff suppressed because it is too large
Load Diff
+47
@@ -0,0 +1,47 @@
|
|||||||
|
[workspace]
|
||||||
|
members = ["crates/*"]
|
||||||
|
|
||||||
|
[package]
|
||||||
|
name = "ppanel-backend"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
asynq = { version = "0.1", features = ["json"] }
|
||||||
|
anyhow = "1"
|
||||||
|
async-trait = "0.1"
|
||||||
|
axum = "0.8"
|
||||||
|
chrono = "0.4"
|
||||||
|
base64 = "0.22"
|
||||||
|
hex = "0.4"
|
||||||
|
jwt = { path = "crates/jwt" }
|
||||||
|
oauth = { path = "crates/oauth" }
|
||||||
|
password = { path = "crates/password" }
|
||||||
|
rand = "0.8"
|
||||||
|
redis = { version = "0.27", features = ["tokio-comp", "connection-manager"] }
|
||||||
|
result = { path = "crates/result" }
|
||||||
|
payment = { path = "crates/payment" }
|
||||||
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
serde_json = "1"
|
||||||
|
serde_urlencoded = "0.7"
|
||||||
|
serde_yaml = "0.9"
|
||||||
|
sqlx = { version = "0.9", features = ["runtime-tokio", "postgres", "mysql", "any", "chrono", "uuid", "migrate", "derive", "macros"] }
|
||||||
|
tokio = { version = "1", features = ["full"] }
|
||||||
|
tower-http = { version = "0.6", features = ["trace"] }
|
||||||
|
tracing = "0.1"
|
||||||
|
tracing-appender = "0.2"
|
||||||
|
tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] }
|
||||||
|
tracing-opentelemetry = "0.28"
|
||||||
|
opentelemetry = { version = "0.27", features = ["trace"] }
|
||||||
|
opentelemetry_sdk = { version = "0.27", features = ["rt-tokio", "trace"] }
|
||||||
|
opentelemetry-otlp = { version = "0.27", features = ["grpc-tonic", "http-proto", "trace"] }
|
||||||
|
opentelemetry-stdout = { version = "0.27", features = ["trace"] }
|
||||||
|
opentelemetry-semantic-conventions = "0.27"
|
||||||
|
uuid = { version = "1", features = ["v4", "v5", "serde"] }
|
||||||
|
md5 = "0.7"
|
||||||
|
urlencoding = "2"
|
||||||
|
gtmpl = "0.7"
|
||||||
|
gtmpl_value = "0.5"
|
||||||
|
sms = { path = "crates/sms" }
|
||||||
|
email = { path = "crates/email" }
|
||||||
|
reqwest = { version = "0.12", features = ["json"] }
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
[package]
|
||||||
|
name = "email"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
lettre = { version = "0.11", default-features = false, features = ["tokio1", "builder", "smtp-transport", "tokio1-native-tls"] }
|
||||||
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
serde_json = "1"
|
||||||
|
tokio = { version = "1", features = ["full"] }
|
||||||
|
tracing = "0.1"
|
||||||
|
thiserror = "2"
|
||||||
|
async-trait = "0.1"
|
||||||
|
anyhow = "1"
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
pub mod manager;
|
||||||
|
pub mod platform;
|
||||||
|
pub mod sender;
|
||||||
|
pub mod smtp;
|
||||||
|
pub mod template;
|
||||||
|
pub mod worker;
|
||||||
|
|
||||||
|
pub use manager::{get_global_manager, set_global_manager, WorkerManager};
|
||||||
|
pub use platform::{get_supported_platforms, Platform, PlatformInfo};
|
||||||
|
pub use sender::{new_sender, EmailError, Sender};
|
||||||
|
pub use worker::{ErrorInfo, Worker, WorkerStatus};
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use tokio::sync::RwLock;
|
||||||
|
use tokio::time::{sleep, Duration};
|
||||||
|
|
||||||
|
use crate::sender::Sender;
|
||||||
|
use crate::worker::{TaskInfo, Worker};
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
pub trait TaskRepo: Send + Sync {
|
||||||
|
async fn find_one(&self, id: i64) -> Result<TaskInfo, anyhow::Error>;
|
||||||
|
async fn update(&self, data: &TaskInfo) -> Result<(), anyhow::Error>;
|
||||||
|
async fn update_status(&self, id: i64, status: i16) -> Result<(), anyhow::Error>;
|
||||||
|
fn is_cancelled(&self, id: i64) -> bool;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct WorkerManager {
|
||||||
|
repo: Arc<dyn TaskRepo>,
|
||||||
|
sender: Arc<dyn Sender>,
|
||||||
|
workers: RwLock<HashMap<i64, WorkerHandle>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct WorkerHandle {
|
||||||
|
worker: Arc<Worker>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WorkerManager {
|
||||||
|
pub fn new(repo: Arc<dyn TaskRepo>, sender: Arc<dyn Sender>) -> Arc<Self> {
|
||||||
|
let manager = Arc::new(WorkerManager {
|
||||||
|
repo,
|
||||||
|
sender,
|
||||||
|
workers: RwLock::new(HashMap::new()),
|
||||||
|
});
|
||||||
|
|
||||||
|
let mgr = manager.clone();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
loop {
|
||||||
|
sleep(Duration::from_secs(60)).await;
|
||||||
|
mgr.check_workers().await;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
manager
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn add_worker(&self, id: i64) {
|
||||||
|
let mut workers = self.workers.write().await;
|
||||||
|
if workers.contains_key(&id) {
|
||||||
|
tracing::info!(
|
||||||
|
"Batch Send Email: Worker already exists, task_id={}",
|
||||||
|
id
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let worker = Arc::new(Worker::new(id, self.repo.clone(), self.sender.clone()));
|
||||||
|
let handle = WorkerHandle {
|
||||||
|
worker: worker.clone(),
|
||||||
|
};
|
||||||
|
workers.insert(id, handle);
|
||||||
|
|
||||||
|
tracing::info!(
|
||||||
|
"Batch Send Email: Added new worker, task_id={}",
|
||||||
|
id
|
||||||
|
);
|
||||||
|
|
||||||
|
tokio::spawn(async move {
|
||||||
|
worker.start().await;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_worker(&self, id: i64) -> Option<Arc<Worker>> {
|
||||||
|
let workers = self.workers.read().await;
|
||||||
|
workers.get(&id).map(|h| h.worker.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn remove_worker(&self, id: i64) {
|
||||||
|
let mut workers = self.workers.write().await;
|
||||||
|
if workers.remove(&id).is_some() {
|
||||||
|
tracing::info!(
|
||||||
|
"Batch Send Email: Removed worker, task_id={}",
|
||||||
|
id
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
tracing::error!(
|
||||||
|
"Batch Send Email: Worker not found for removal, task_id={}",
|
||||||
|
id
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn check_workers(&self) {
|
||||||
|
let mut workers = self.workers.write().await;
|
||||||
|
let mut to_remove = Vec::new();
|
||||||
|
|
||||||
|
for (&id, handle) in workers.iter() {
|
||||||
|
if handle.worker.is_running().await as i16 == 2 {
|
||||||
|
to_remove.push(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for id in to_remove {
|
||||||
|
workers.remove(&id);
|
||||||
|
tracing::info!(
|
||||||
|
"Batch Send Email: Removed completed worker, task_id={}",
|
||||||
|
id
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static MANAGER: std::sync::OnceLock<Arc<WorkerManager>> = std::sync::OnceLock::new();
|
||||||
|
|
||||||
|
pub fn set_global_manager(manager: Arc<WorkerManager>) -> Result<(), Arc<WorkerManager>> {
|
||||||
|
MANAGER.set(manager)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_global_manager() -> Option<&'static Arc<WorkerManager>> {
|
||||||
|
MANAGER.get()
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum Platform {
|
||||||
|
Smtp,
|
||||||
|
Unsupported,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for Platform {
|
||||||
|
type Err = ();
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
match s {
|
||||||
|
"smtp" => Ok(Platform::Smtp),
|
||||||
|
_ => Ok(Platform::Unsupported),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Platform {
|
||||||
|
pub fn as_str(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Platform::Smtp => "smtp",
|
||||||
|
Platform::Unsupported => "unsupported",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize)]
|
||||||
|
pub struct PlatformInfo {
|
||||||
|
pub platform: String,
|
||||||
|
pub platform_url: String,
|
||||||
|
pub platform_field_description: HashMap<String, String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_supported_platforms() -> Vec<PlatformInfo> {
|
||||||
|
let mut desc = HashMap::new();
|
||||||
|
desc.insert("host".into(), "host".into());
|
||||||
|
desc.insert("port".into(), "port".into());
|
||||||
|
desc.insert("user".into(), "user".into());
|
||||||
|
desc.insert("pass".into(), "pass".into());
|
||||||
|
desc.insert("from".into(), "from".into());
|
||||||
|
desc.insert("reply_to".into(), "reply_to".into());
|
||||||
|
desc.insert("ssl".into(), "ssl".into());
|
||||||
|
|
||||||
|
vec![PlatformInfo {
|
||||||
|
platform: "smtp".into(),
|
||||||
|
platform_url: String::new(),
|
||||||
|
platform_field_description: desc,
|
||||||
|
}]
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use crate::platform::Platform;
|
||||||
|
use crate::smtp::{SmtpClient, SmtpConfig};
|
||||||
|
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum EmailError {
|
||||||
|
#[error("SMTP transport error: {0}")]
|
||||||
|
SmtpTransport(#[from] lettre::transport::smtp::Error),
|
||||||
|
#[error("Message build error: {0}")]
|
||||||
|
MessageBuild(String),
|
||||||
|
#[error("Unsupported platform: {0}")]
|
||||||
|
UnsupportedPlatform(String),
|
||||||
|
#[error("Config parse error: {0}")]
|
||||||
|
ConfigParse(#[from] serde_json::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
pub trait Sender: Send + Sync {
|
||||||
|
async fn send(&self, to: &[String], subject: &str, body: &str) -> Result<(), EmailError>;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new_sender(
|
||||||
|
platform: &str,
|
||||||
|
config: &str,
|
||||||
|
site_name: &str,
|
||||||
|
) -> Result<Box<dyn Sender>, EmailError> {
|
||||||
|
match Platform::from_str(platform).unwrap_or(Platform::Unsupported) {
|
||||||
|
Platform::Smtp => {
|
||||||
|
let mut cfg: SmtpConfig = serde_json::from_str(config)?;
|
||||||
|
cfg.site_name = site_name.to_string();
|
||||||
|
Ok(Box::new(SmtpClient::new(cfg)))
|
||||||
|
}
|
||||||
|
_ => Err(EmailError::UnsupportedPlatform(platform.to_string())),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
use lettre::message::header::ContentType;
|
||||||
|
use lettre::transport::smtp::authentication::Credentials;
|
||||||
|
use lettre::transport::smtp::client::{Tls, TlsParameters};
|
||||||
|
use lettre::{AsyncSmtpTransport, AsyncTransport, Message, Tokio1Executor};
|
||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
use crate::sender::EmailError;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
|
pub struct SmtpConfig {
|
||||||
|
pub host: String,
|
||||||
|
pub port: u16,
|
||||||
|
pub user: String,
|
||||||
|
pub pass: String,
|
||||||
|
pub from: String,
|
||||||
|
pub reply_to: Option<String>,
|
||||||
|
pub ssl: bool,
|
||||||
|
#[serde(default)]
|
||||||
|
pub site_name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct SmtpClient {
|
||||||
|
config: SmtpConfig,
|
||||||
|
mailer: AsyncSmtpTransport<Tokio1Executor>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SmtpClient {
|
||||||
|
pub fn new(config: SmtpConfig) -> Self {
|
||||||
|
let creds = Credentials::new(config.user.clone(), config.pass.clone());
|
||||||
|
|
||||||
|
let tls_params = TlsParameters::new(config.host.clone())
|
||||||
|
.expect("failed to build TLS parameters");
|
||||||
|
|
||||||
|
let tls = if config.ssl {
|
||||||
|
Tls::Wrapper(tls_params)
|
||||||
|
} else {
|
||||||
|
Tls::Required(tls_params)
|
||||||
|
};
|
||||||
|
|
||||||
|
let mailer = AsyncSmtpTransport::<Tokio1Executor>::relay(&config.host)
|
||||||
|
.expect("failed to build SMTP relay")
|
||||||
|
.port(config.port)
|
||||||
|
.credentials(creds)
|
||||||
|
.tls(tls)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
SmtpClient { config, mailer }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl crate::sender::Sender for SmtpClient {
|
||||||
|
async fn send(&self, to: &[String], subject: &str, body: &str) -> Result<(), EmailError> {
|
||||||
|
let site_name = if self.config.site_name.is_empty() {
|
||||||
|
self.config.from.clone()
|
||||||
|
} else {
|
||||||
|
self.config.site_name.clone()
|
||||||
|
};
|
||||||
|
|
||||||
|
let from_header = format!("{} <{}>", site_name, self.config.from);
|
||||||
|
|
||||||
|
let from_addr: lettre::message::Mailbox = from_header
|
||||||
|
.parse()
|
||||||
|
.map_err(|e: lettre::address::AddressError| {
|
||||||
|
EmailError::MessageBuild(e.to_string())
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let mut builder = Message::builder().from(from_addr);
|
||||||
|
|
||||||
|
if let Some(ref reply_to) = self.config.reply_to {
|
||||||
|
let reply_addr: lettre::message::Mailbox = reply_to
|
||||||
|
.parse()
|
||||||
|
.map_err(|e: lettre::address::AddressError| {
|
||||||
|
EmailError::MessageBuild(e.to_string())
|
||||||
|
})?;
|
||||||
|
builder = builder.reply_to(reply_addr);
|
||||||
|
}
|
||||||
|
|
||||||
|
for addr in to {
|
||||||
|
let to_addr: lettre::message::Mailbox = addr
|
||||||
|
.parse()
|
||||||
|
.map_err(|e: lettre::address::AddressError| {
|
||||||
|
EmailError::MessageBuild(e.to_string())
|
||||||
|
})?;
|
||||||
|
builder = builder.to(to_addr);
|
||||||
|
}
|
||||||
|
|
||||||
|
let message = builder
|
||||||
|
.subject(subject)
|
||||||
|
.header(ContentType::TEXT_HTML)
|
||||||
|
.body(body.to_string())
|
||||||
|
.map_err(|e| EmailError::MessageBuild(e.to_string()))?;
|
||||||
|
|
||||||
|
self.mailer.send(message).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,175 @@
|
|||||||
|
pub const DEFAULT_EMAIL_VERIFY_TEMPLATE: &str = r#"<!doctype html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>
|
||||||
|
{{if eq .Type 1}}注册验证码 / Registration Verification Code{{else}}重置密码验证码 / Password
|
||||||
|
Reset Verification Code{{end}}
|
||||||
|
</title>
|
||||||
|
<style>
|
||||||
|
body { color: black; }
|
||||||
|
.container { border-radius: 5px; width: 500px; margin: 20px auto 0; border: 1px solid #cce7ff; background-color: #f0f8ff; padding: 25px 30px; }
|
||||||
|
.header { text-align: center; display: flex; align-items: center; justify-content: center; }
|
||||||
|
.logo { width: 56px; height: 56px; object-fit: cover; margin-right: 10px; }
|
||||||
|
.site-name { font-size: 18px; font-weight: bold; margin: 0; }
|
||||||
|
.content { margin: 10px 0; font-size: 14px; }
|
||||||
|
.greeting { font-weight: 700; margin: 5px 0; }
|
||||||
|
.highlight { margin: 0 2px; font-weight: 700; color: #007bff; }
|
||||||
|
.code-container { margin: 25px 0; width: 100%; background-color: #e6f2ff; height: 60px; line-height: 60px; text-align: center; font-size: 32px; font-weight: 700; color: #007bff; }
|
||||||
|
.code { letter-spacing: 5pt; }
|
||||||
|
.footer { border-top: #99ccff 1px solid; margin-top: 20px; padding-top: 5px; font-size: 12px; font-weight: 700; color: #777; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="header">
|
||||||
|
<img src="{{.SiteLogo}}" class="logo" />
|
||||||
|
<p class="site-name">{{.SiteName}}</p>
|
||||||
|
</div>
|
||||||
|
<div class="content">
|
||||||
|
<p class="greeting">Hi, 尊敬的用户 / Dear User</p>
|
||||||
|
<p>
|
||||||
|
{{if eq .Type 1}} 感谢您注册!您的验证码是(请于<span class="highlight">{{.Expire}}</span>分钟内使用):
|
||||||
|
<br />
|
||||||
|
Thank you for registering! Your verification code is (please use it within <span class="highlight">{{.Expire}}</span> minutes): {{else}}
|
||||||
|
您正在重置密码。您的验证码是(请于<span class="highlight">{{.Expire}}</span>分钟内使用):
|
||||||
|
<br />
|
||||||
|
You are resetting your password. Your verification code is (please use it within <span class="highlight">{{.Expire}}</span> minutes): {{end}}
|
||||||
|
</p>
|
||||||
|
<div class="code-container">
|
||||||
|
<span class="code">{{.Code}}</span>
|
||||||
|
</div>
|
||||||
|
<p>
|
||||||
|
如果您未请求此验证码,请忽略此邮件。<br />If you did not request this code, please ignore this email.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="footer">此为系统邮件,请勿回复 / This is a system email, please do not reply</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>"#;
|
||||||
|
|
||||||
|
pub const DEFAULT_MAINTENANCE_EMAIL_TEMPLATE: &str = r#"<!doctype html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>系统维护通知 / System Maintenance Notice</title>
|
||||||
|
<style>
|
||||||
|
body { color: black; }
|
||||||
|
.container { border-radius: 5px; width: 500px; margin: 20px auto 0; border: 1px solid #cce7ff; background-color: #f0f8ff; padding: 25px 30px; }
|
||||||
|
.header { text-align: center; display: flex; align-items: center; justify-content: center; }
|
||||||
|
.logo { width: 56px; height: 56px; object-fit: cover; margin-right: 10px; }
|
||||||
|
.site-name { font-size: 18px; font-weight: bold; margin: 0; }
|
||||||
|
.content { margin: 20px 0; font-size: 14px; }
|
||||||
|
.greeting { font-weight: 700; margin: 5px 0; }
|
||||||
|
.highlight { margin: 0 2px; font-weight: 700; color: #007bff; }
|
||||||
|
.footer { border-top: #99ccff 1px solid; margin-top: 20px; padding-top: 5px; font-size: 12px; font-weight: 700; color: #777; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="header">
|
||||||
|
<img src="{{.SiteLogo}}" class="logo" />
|
||||||
|
<p class="site-name">{{.SiteName}}</p>
|
||||||
|
</div>
|
||||||
|
<div class="content">
|
||||||
|
<p class="greeting">Hi, 尊敬的用户 / Dear User</p>
|
||||||
|
<p>
|
||||||
|
我们计划在<span class="highlight">{{.MaintenanceDate}}</span>进行系统维护,预计维护时间为<span class="highlight">{{.MaintenanceTime}}</span>。在此期间,您可能会遇到服务中断或无法访问的情况。
|
||||||
|
<br />
|
||||||
|
We will be performing system maintenance on <span class="highlight">{{.MaintenanceDate}}</span>, and the expected maintenance period is <span class="highlight">{{.MaintenanceTime}}</span>. During this time, you may experience service interruptions or unavailability.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
维护完成后,系统将自动恢复。如果您有任何问题,请随时联系我们的支持团队。
|
||||||
|
<br />
|
||||||
|
The system will resume automatically once the maintenance is completed. If you have any questions, please feel free to contact our support team.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="footer">此为系统邮件,请勿回复 / This is a system email, please do not reply</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>"#;
|
||||||
|
|
||||||
|
pub const DEFAULT_EXPIRATION_EMAIL_TEMPLATE: &str = r#"<!doctype html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>服务到期通知 / Service Expiration Notice</title>
|
||||||
|
<style>
|
||||||
|
body { color: black; }
|
||||||
|
.container { border-radius: 5px; width: 500px; margin: 20px auto 0; border: 1px solid #cce7ff; background-color: #f0f8ff; padding: 25px 30px; }
|
||||||
|
.header { text-align: center; display: flex; align-items: center; justify-content: center; }
|
||||||
|
.logo { width: 56px; height: 56px; object-fit: cover; margin-right: 10px; }
|
||||||
|
.site-name { font-size: 18px; font-weight: bold; margin: 0; }
|
||||||
|
.content { margin: 20px 0; font-size: 14px; }
|
||||||
|
.greeting { font-weight: 700; margin: 5px 0; }
|
||||||
|
.highlight { margin: 0 2px; font-weight: 700; color: #007bff; }
|
||||||
|
.footer { border-top: #99ccff 1px solid; margin-top: 20px; padding-top: 5px; font-size: 12px; font-weight: 700; color: #777; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="header">
|
||||||
|
<img src="{{.SiteLogo}}" class="logo" />
|
||||||
|
<p class="site-name">{{.SiteName}}</p>
|
||||||
|
</div>
|
||||||
|
<div class="content">
|
||||||
|
<p class="greeting">Hi, 尊敬的用户 / Dear User</p>
|
||||||
|
<p>
|
||||||
|
您的服务即将在<span class="highlight">{{.ExpireDate}}</span>到期,请及时续费以保证服务不间断。
|
||||||
|
<br />
|
||||||
|
Your service is set to expire on <span class="highlight">{{.ExpireDate}}</span>. Please renew your subscription to avoid service interruptions.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
如需帮助,请联系客服团队。感谢您的支持!
|
||||||
|
<br />
|
||||||
|
If you need assistance, please contact our support team. Thank you for your continued support!
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="footer">此为系统邮件,请勿回复 / This is a system email, please do not reply</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>"#;
|
||||||
|
|
||||||
|
pub const DEFAULT_TRAFFIC_EXCEED_EMAIL_TEMPLATE: &str = r#"<!doctype html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>流量用尽通知 / Traffic Exhausted Notice</title>
|
||||||
|
<style>
|
||||||
|
.container { border-radius: 5px; width: 500px; margin: 20px auto 0; border: 1px solid #cce7ff; background-color: #f0f8ff; padding: 25px 30px; }
|
||||||
|
.header { text-align: center; display: flex; align-items: center; justify-content: center; }
|
||||||
|
.logo { width: 56px; height: 56px; object-fit: cover; margin-right: 10px; }
|
||||||
|
.site-name { font-size: 18px; font-weight: bold; margin: 0; }
|
||||||
|
.content { margin: 20px 0; font-size: 14px; }
|
||||||
|
.greeting { font-weight: 700; margin: 5px 0; }
|
||||||
|
.highlight { color: #007bff; }
|
||||||
|
.footer { border-top: #99ccff 1px solid; margin-top: 20px; padding-top: 5px; font-size: 12px; font-weight: 700; color: #777; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="header">
|
||||||
|
<img src="{{.SiteLogo}}" class="logo" />
|
||||||
|
<p class="site-name">{{.SiteName}}</p>
|
||||||
|
</div>
|
||||||
|
<div class="content">
|
||||||
|
<p class="greeting">Hi, 尊敬的用户 / Dear User</p>
|
||||||
|
<p>
|
||||||
|
您的流量已经用尽,请及时购买流量以继续使用我们的服务。
|
||||||
|
<br />
|
||||||
|
Your traffic has been exhausted. Please purchase additional traffic to continue using our service.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
如需帮助,请联系客服团队。感谢您的支持!
|
||||||
|
<br />
|
||||||
|
If you need assistance, please contact our support team. Thank you for your continued support!
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="footer">此为系统邮件,请勿回复 / This is a system email, please do not reply</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>"#;
|
||||||
@@ -0,0 +1,263 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use tokio::sync::Mutex;
|
||||||
|
use tokio::time::sleep;
|
||||||
|
|
||||||
|
use crate::sender::Sender;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct ErrorInfo {
|
||||||
|
pub error: String,
|
||||||
|
pub email: String,
|
||||||
|
pub time: i64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum WorkerStatus {
|
||||||
|
Idle = 0,
|
||||||
|
Running = 1,
|
||||||
|
Completed = 2,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
|
pub struct EmailScope {
|
||||||
|
#[serde(rename = "type")]
|
||||||
|
pub type_: i16,
|
||||||
|
#[serde(default)]
|
||||||
|
pub register_start_time: i64,
|
||||||
|
#[serde(default)]
|
||||||
|
pub register_end_time: i64,
|
||||||
|
#[serde(default)]
|
||||||
|
pub recipients: Vec<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub additional: Vec<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub scheduled: i64,
|
||||||
|
#[serde(default)]
|
||||||
|
pub interval: i16,
|
||||||
|
#[serde(default)]
|
||||||
|
pub limit: i64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
|
pub struct EmailContent {
|
||||||
|
pub subject: String,
|
||||||
|
pub content: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct TaskInfo {
|
||||||
|
pub id: i64,
|
||||||
|
pub type_: i16,
|
||||||
|
pub scope: String,
|
||||||
|
pub content: String,
|
||||||
|
pub status: i16,
|
||||||
|
pub errors: String,
|
||||||
|
pub total: i64,
|
||||||
|
pub current: i64,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Worker {
|
||||||
|
id: i64,
|
||||||
|
repo: Arc<dyn crate::manager::TaskRepo>,
|
||||||
|
sender: Arc<dyn Sender>,
|
||||||
|
status: Arc<Mutex<WorkerStatus>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Worker {
|
||||||
|
pub fn new(
|
||||||
|
id: i64,
|
||||||
|
repo: Arc<dyn crate::manager::TaskRepo>,
|
||||||
|
sender: Arc<dyn Sender>,
|
||||||
|
) -> Self {
|
||||||
|
Worker {
|
||||||
|
id,
|
||||||
|
repo,
|
||||||
|
sender,
|
||||||
|
status: Arc::new(Mutex::new(WorkerStatus::Idle)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn id(&self) -> i64 {
|
||||||
|
self.id
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn is_running(&self) -> WorkerStatus {
|
||||||
|
*self.status.lock().await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn start(&self) {
|
||||||
|
let task_info = match self.repo.find_one(self.id).await {
|
||||||
|
Ok(t) => t,
|
||||||
|
Err(e) => {
|
||||||
|
tracing::error!(
|
||||||
|
"Batch Send Email: Failed to find task, task_id={}, error={}",
|
||||||
|
self.id,
|
||||||
|
e
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if task_info.status != 0 {
|
||||||
|
tracing::error!(
|
||||||
|
"Batch Send Email: Task already completed or in progress, task_id={}",
|
||||||
|
self.id
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let scope: EmailScope = match serde_json::from_str(&task_info.scope) {
|
||||||
|
Ok(s) => s,
|
||||||
|
Err(e) => {
|
||||||
|
tracing::error!(
|
||||||
|
"Batch Send Email: Failed to parse task scope, task_id={}, error={}",
|
||||||
|
self.id,
|
||||||
|
e
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if scope.recipients.is_empty() && scope.additional.is_empty() {
|
||||||
|
tracing::error!(
|
||||||
|
"Batch Send Email: No recipients or additional emails provided, task_id={}",
|
||||||
|
self.id
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let content: EmailContent = match serde_json::from_str(&task_info.content) {
|
||||||
|
Ok(c) => c,
|
||||||
|
Err(e) => {
|
||||||
|
tracing::error!(
|
||||||
|
"Batch Send Email: Failed to parse task content, task_id={}, error={}",
|
||||||
|
self.id,
|
||||||
|
e
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut status = self.status.lock().await;
|
||||||
|
*status = WorkerStatus::Running;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut recipients = scope.recipients.clone();
|
||||||
|
recipients.extend(scope.additional.clone());
|
||||||
|
remove_duplicates_and_empty(&mut recipients);
|
||||||
|
|
||||||
|
if recipients.is_empty() {
|
||||||
|
tracing::error!(
|
||||||
|
"Batch Send Email: No valid recipients found, task_id={}",
|
||||||
|
self.id
|
||||||
|
);
|
||||||
|
let mut status = self.status.lock().await;
|
||||||
|
*status = WorkerStatus::Completed;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let interval = if scope.interval == 0 {
|
||||||
|
Duration::from_secs(1)
|
||||||
|
} else {
|
||||||
|
Duration::from_secs(scope.interval as u64)
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut errors: Vec<ErrorInfo> = Vec::new();
|
||||||
|
let mut count: i64 = 0;
|
||||||
|
|
||||||
|
for recipient in &recipients {
|
||||||
|
if self.repo.is_cancelled(self.id) {
|
||||||
|
tracing::info!(
|
||||||
|
"Batch Send Email: Worker stopped by cancellation, task_id={}",
|
||||||
|
self.id
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if task_info.status == 0 {
|
||||||
|
// mark as in-progress via repo
|
||||||
|
let _ = self.repo.update_status(self.id, 1).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Err(e) = self
|
||||||
|
.sender
|
||||||
|
.send(std::slice::from_ref(recipient), &content.subject, &content.content)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
tracing::error!(
|
||||||
|
"Batch Send Email: Failed to send email, task_id={}, recipient={}, error={}",
|
||||||
|
self.id,
|
||||||
|
recipient,
|
||||||
|
e
|
||||||
|
);
|
||||||
|
errors.push(ErrorInfo {
|
||||||
|
error: e.to_string(),
|
||||||
|
email: recipient.clone(),
|
||||||
|
time: std::time::SystemTime::now()
|
||||||
|
.duration_since(std::time::UNIX_EPOCH)
|
||||||
|
.unwrap()
|
||||||
|
.as_secs() as i64,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
count += 1;
|
||||||
|
|
||||||
|
let mut updated = task_info.clone();
|
||||||
|
updated.current = count;
|
||||||
|
updated.errors = serde_json::to_string(&errors).unwrap_or_default();
|
||||||
|
|
||||||
|
if let Err(e) = self.repo.update(&updated).await {
|
||||||
|
tracing::error!(
|
||||||
|
"Batch Send Email: Failed to update task progress, task_id={}, error={}",
|
||||||
|
self.id,
|
||||||
|
e
|
||||||
|
);
|
||||||
|
let mut status = self.status.lock().await;
|
||||||
|
*status = WorkerStatus::Completed;
|
||||||
|
}
|
||||||
|
|
||||||
|
sleep(interval).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut status = self.status.lock().await;
|
||||||
|
*status = WorkerStatus::Completed;
|
||||||
|
|
||||||
|
let mut finalized = task_info.clone();
|
||||||
|
finalized.status = 2;
|
||||||
|
finalized.current = count;
|
||||||
|
finalized.errors = serde_json::to_string(&errors).unwrap_or_default();
|
||||||
|
|
||||||
|
match self.repo.update(&finalized).await {
|
||||||
|
Ok(_) => {
|
||||||
|
tracing::info!(
|
||||||
|
"Batch Send Email: Task completed successfully, task_id={}, total_sent={}",
|
||||||
|
self.id,
|
||||||
|
count
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
tracing::error!(
|
||||||
|
"Batch Send Email: Failed to finalize task, task_id={}, error={}",
|
||||||
|
self.id,
|
||||||
|
e
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn remove_duplicates_and_empty(items: &mut Vec<String>) {
|
||||||
|
let mut seen = std::collections::HashSet::new();
|
||||||
|
items.retain(|item| {
|
||||||
|
if item.is_empty() || seen.contains(item) {
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
seen.insert(item.clone());
|
||||||
|
true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
[package]
|
||||||
|
name = "ip"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
reqwest = { version = "0.12", features = ["json", "brotli", "gzip"] }
|
||||||
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
serde_json = "1"
|
||||||
|
thiserror = "2"
|
||||||
|
tracing = "0.1"
|
||||||
|
tokio = { version = "1", features = ["net"] }
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
use std::net::IpAddr;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
const IPINFO: &str = "ipinfo.io";
|
||||||
|
const IPAPI: &str = "ipapi.co";
|
||||||
|
const IPBASE: &str = "api.ipbase.com";
|
||||||
|
const IPWHOIS: &str = "ipwhois.app";
|
||||||
|
|
||||||
|
const SERVICES: &[&str] = &[IPBASE, IPAPI, IPWHOIS, IPINFO];
|
||||||
|
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum IpError {
|
||||||
|
#[error("DNS resolution failed for {0}")]
|
||||||
|
DnsResolutionFailed(String),
|
||||||
|
#[error("HTTP request failed: {0}")]
|
||||||
|
Http(#[from] reqwest::Error),
|
||||||
|
#[error("JSON parsing failed: {0}")]
|
||||||
|
Json(#[from] serde_json::Error),
|
||||||
|
#[error("all geolocation services failed")]
|
||||||
|
AllServicesFailed,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn resolve_ip(input: &str) -> Result<Vec<String>, IpError> {
|
||||||
|
if let Ok(ip) = input.parse::<IpAddr>() {
|
||||||
|
return Ok(vec![ip.to_string()]);
|
||||||
|
}
|
||||||
|
|
||||||
|
let addrs = tokio::net::lookup_host(input).await.map_err(|_| {
|
||||||
|
IpError::DnsResolutionFailed(input.to_string())
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let ips: Vec<String> = addrs.map(|sa| sa.ip().to_string()).collect();
|
||||||
|
if ips.is_empty() {
|
||||||
|
return Err(IpError::DnsResolutionFailed(input.to_string()));
|
||||||
|
}
|
||||||
|
Ok(ips)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_region_by_ip(ip: &str) -> Result<GeoLocationResponse, IpError> {
|
||||||
|
let client = new_http_client();
|
||||||
|
|
||||||
|
for service in SERVICES {
|
||||||
|
match fetch_geolocation(&client, service, ip).await {
|
||||||
|
Ok(resp) => return Ok(resp),
|
||||||
|
Err(e) => {
|
||||||
|
tracing::error!("Failed to fetch geolocation from {}: {:?}", service, e);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(IpError::AllServicesFailed)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn new_http_client() -> reqwest::Client {
|
||||||
|
reqwest::Client::builder()
|
||||||
|
.timeout(Duration::from_secs(10))
|
||||||
|
.user_agent("Mozilla/5.0 (X11; Linux x86_64; rv:134.0) Gecko/20100101 Firefox/134.0")
|
||||||
|
.build()
|
||||||
|
.expect("Failed to build reqwest::Client")
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn fetch_geolocation(
|
||||||
|
client: &reqwest::Client,
|
||||||
|
service: &str,
|
||||||
|
ip: &str,
|
||||||
|
) -> Result<GeoLocationResponse, IpError> {
|
||||||
|
let api_url = match service {
|
||||||
|
IPINFO => format!("https://ipinfo.io/{}/json", ip),
|
||||||
|
IPAPI => format!("https://ipapi.co/{}/json", ip),
|
||||||
|
IPBASE => format!("https://api.ipbase.com/v1/json/{}", ip),
|
||||||
|
IPWHOIS => format!("https://ipwhois.app/json/{}", ip),
|
||||||
|
_ => unreachable!(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let resp = client
|
||||||
|
.get(&api_url)
|
||||||
|
.header("Host", service)
|
||||||
|
.header("Accept", "application/json, text/html, application/xhtml+xml, */*;q=0.8")
|
||||||
|
.header("Accept-Language", "en-US,en;q=0.5")
|
||||||
|
.header("Accept-Encoding", "gzip, deflate, br, zstd")
|
||||||
|
.header("Connection", "keep-alive")
|
||||||
|
.header("Upgrade-Insecure-Requests", "1")
|
||||||
|
.send()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let bytes = resp.bytes().await?;
|
||||||
|
let mut location: GeoLocationResponse = serde_json::from_slice(&bytes)?;
|
||||||
|
|
||||||
|
if location.country.is_empty() {
|
||||||
|
location.country = location.country_name.clone();
|
||||||
|
}
|
||||||
|
|
||||||
|
if !location.loc.is_empty() {
|
||||||
|
if let Some((lat, lon)) = location.loc.split_once(',') {
|
||||||
|
location.latitude = lat.trim().to_string();
|
||||||
|
location.longitude = lon.trim().to_string();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(location)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Clone, Deserialize)]
|
||||||
|
#[serde(default)]
|
||||||
|
pub struct GeoLocationResponse {
|
||||||
|
pub country: String,
|
||||||
|
pub country_name: String,
|
||||||
|
pub region: String,
|
||||||
|
pub city: String,
|
||||||
|
pub latitude: String,
|
||||||
|
pub longitude: String,
|
||||||
|
pub loc: String,
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
[package]
|
||||||
|
name = "jwt"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
jsonwebtoken = "9"
|
||||||
|
serde = { version = "1", features = ["derive"] }
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, Validation};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct Claims {
|
||||||
|
pub exp: i64,
|
||||||
|
pub iat: i64,
|
||||||
|
#[serde(rename = "UserId")]
|
||||||
|
pub user_id: i64,
|
||||||
|
#[serde(rename = "SessionId")]
|
||||||
|
pub session_id: String,
|
||||||
|
#[serde(rename = "LoginType")]
|
||||||
|
pub login_type: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Claims {
|
||||||
|
pub fn new(user_id: i64, session_id: String, login_type: String) -> (Self, i64) {
|
||||||
|
let now = chrono_now();
|
||||||
|
let seconds = 604800;
|
||||||
|
(
|
||||||
|
Self {
|
||||||
|
iat: now,
|
||||||
|
exp: now + seconds,
|
||||||
|
user_id,
|
||||||
|
session_id,
|
||||||
|
login_type,
|
||||||
|
},
|
||||||
|
seconds,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn chrono_now() -> i64 {
|
||||||
|
std::time::SystemTime::now()
|
||||||
|
.duration_since(std::time::UNIX_EPOCH)
|
||||||
|
.unwrap()
|
||||||
|
.as_secs() as i64
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn generate_token(claims: &Claims, secret: &str) -> Result<String, jsonwebtoken::errors::Error> {
|
||||||
|
encode(
|
||||||
|
&Header::default(),
|
||||||
|
claims,
|
||||||
|
&EncodingKey::from_secret(secret.as_bytes()),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn validate_token(
|
||||||
|
token: &str,
|
||||||
|
secret: &str,
|
||||||
|
) -> Result<Claims, jsonwebtoken::errors::Error> {
|
||||||
|
let token_data = decode::<Claims>(
|
||||||
|
token,
|
||||||
|
&DecodingKey::from_secret(secret.as_bytes()),
|
||||||
|
&Validation::default(),
|
||||||
|
)?;
|
||||||
|
Ok(token_data.claims)
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
[package]
|
||||||
|
name = "oauth"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
arctic-oauth = { version = "0.3.0", features = ["apple", "google"] }
|
||||||
|
base64 = "0.22"
|
||||||
|
hex = "0.4"
|
||||||
|
hmac = "0.12"
|
||||||
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
serde_json = "1"
|
||||||
|
sha2 = "0.10"
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
|
pub struct GoogleConfig {
|
||||||
|
pub client_id: String,
|
||||||
|
pub client_secret: String,
|
||||||
|
pub redirect_url: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
|
pub struct AppleConfig {
|
||||||
|
pub team_id: String,
|
||||||
|
pub key_id: String,
|
||||||
|
pub client_id: String,
|
||||||
|
pub client_secret: String,
|
||||||
|
pub redirect_url: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
|
pub struct TelegramConfig {
|
||||||
|
pub bot_token: String,
|
||||||
|
pub enable_notify: Option<bool>,
|
||||||
|
pub webhook_domain: Option<String>,
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
use std::fmt;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum OAuthError {
|
||||||
|
Config(String),
|
||||||
|
Telegram(String),
|
||||||
|
Arctic(arctic_oauth::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for OAuthError {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
OAuthError::Config(msg) => write!(f, "OAuth config error: {}", msg),
|
||||||
|
OAuthError::Telegram(msg) => write!(f, "Telegram OAuth error: {}", msg),
|
||||||
|
OAuthError::Arctic(e) => write!(f, "OAuth error: {}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::error::Error for OAuthError {}
|
||||||
|
|
||||||
|
impl From<arctic_oauth::Error> for OAuthError {
|
||||||
|
fn from(e: arctic_oauth::Error) -> Self {
|
||||||
|
OAuthError::Arctic(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
//! OAuth 2.0 authentication library wrapping `arctic-oauth` with
|
||||||
|
//! config types matching the database schema, plus Telegram Login support.
|
||||||
|
//!
|
||||||
|
//! # Providers
|
||||||
|
//!
|
||||||
|
//! | Provider | Standard OAuth 2.0 | PKCE | Tokens |
|
||||||
|
//! |-----------|-------------------|----------|---------------|
|
||||||
|
//! | Google | ✅ (arctic-oauth) | Required | access + refresh + id_token |
|
||||||
|
//! | Apple | ✅ (arctic-oauth) | None | access + id_token |
|
||||||
|
//! | Telegram | ⚠️ Custom HMAC | N/A | N/A (stateless) |
|
||||||
|
|
||||||
|
pub mod config;
|
||||||
|
pub mod error;
|
||||||
|
pub mod telegram;
|
||||||
|
|
||||||
|
// Re-export arctic-oauth core types + providers
|
||||||
|
pub use arctic_oauth::{
|
||||||
|
create_code_challenge, decode_id_token, generate_code_verifier, generate_state,
|
||||||
|
Apple, AppleOptions, Google, GoogleOptions, OAuth2Tokens,
|
||||||
|
};
|
||||||
|
pub use arctic_oauth::{CodeChallengeMethod, Error as ArcticError};
|
||||||
|
pub use config::{AppleConfig, GoogleConfig, TelegramConfig};
|
||||||
|
pub use error::OAuthError;
|
||||||
|
pub use telegram::{parse_and_validate_auth_data, parse_base64_and_validate, validate_auth_data, AuthData};
|
||||||
|
|
||||||
|
/// Unified user info extracted from any OAuth provider.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct OAuthUserInfo {
|
||||||
|
pub open_id: String,
|
||||||
|
pub email: Option<String>,
|
||||||
|
pub name: Option<String>,
|
||||||
|
pub picture: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OAuthUserInfo {
|
||||||
|
pub fn from_google(tokens: &OAuth2Tokens) -> Result<Self, OAuthError> {
|
||||||
|
let id_token = tokens.id_token()?;
|
||||||
|
let claims = decode_id_token(id_token)?;
|
||||||
|
let open_id = claims
|
||||||
|
.get("sub")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.unwrap_or("")
|
||||||
|
.to_string();
|
||||||
|
let email = claims.get("email").and_then(|v| v.as_str()).map(String::from);
|
||||||
|
let name = claims.get("name").and_then(|v| v.as_str()).map(String::from);
|
||||||
|
let picture = claims
|
||||||
|
.get("picture")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.map(String::from);
|
||||||
|
Ok(Self {
|
||||||
|
open_id,
|
||||||
|
email,
|
||||||
|
name,
|
||||||
|
picture,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_apple(tokens: &OAuth2Tokens) -> Result<Self, OAuthError> {
|
||||||
|
let id_token = tokens.id_token()?;
|
||||||
|
let claims = decode_id_token(id_token)?;
|
||||||
|
let open_id = claims
|
||||||
|
.get("sub")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.unwrap_or("")
|
||||||
|
.to_string();
|
||||||
|
let email = claims.get("email").and_then(|v| v.as_str()).map(String::from);
|
||||||
|
let name = None;
|
||||||
|
let picture = None;
|
||||||
|
Ok(Self {
|
||||||
|
open_id,
|
||||||
|
email,
|
||||||
|
name,
|
||||||
|
picture,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_telegram(data: &AuthData) -> Self {
|
||||||
|
let open_id = data.id.to_string();
|
||||||
|
let name = Some(
|
||||||
|
[data.first_name.as_deref(), data.last_name.as_deref()]
|
||||||
|
.into_iter()
|
||||||
|
.flatten()
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(" "),
|
||||||
|
)
|
||||||
|
.filter(|s| !s.is_empty());
|
||||||
|
Self {
|
||||||
|
open_id,
|
||||||
|
email: None,
|
||||||
|
name,
|
||||||
|
picture: data.photo_url.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn open_id(&self) -> &str {
|
||||||
|
&self.open_id
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
use base64::Engine;
|
||||||
|
use hmac::{Hmac, Mac};
|
||||||
|
use serde::Deserialize;
|
||||||
|
use sha2::{Digest, Sha256};
|
||||||
|
|
||||||
|
use crate::error::OAuthError;
|
||||||
|
|
||||||
|
const AUTH_DATE_TTL_SECS: i64 = 86400;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
|
pub struct AuthData {
|
||||||
|
pub id: i64,
|
||||||
|
pub first_name: Option<String>,
|
||||||
|
pub last_name: Option<String>,
|
||||||
|
pub username: Option<String>,
|
||||||
|
pub photo_url: Option<String>,
|
||||||
|
pub auth_date: i64,
|
||||||
|
pub hash: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type TelegramUserInfo = AuthData;
|
||||||
|
|
||||||
|
fn check_string(data: &AuthData) -> String {
|
||||||
|
let mut pairs: Vec<(String, String)> = Vec::new();
|
||||||
|
|
||||||
|
pairs.push(("id".to_string(), data.id.to_string()));
|
||||||
|
if let Some(v) = &data.first_name {
|
||||||
|
pairs.push(("first_name".to_string(), v.clone()));
|
||||||
|
}
|
||||||
|
if let Some(v) = &data.last_name {
|
||||||
|
pairs.push(("last_name".to_string(), v.clone()));
|
||||||
|
}
|
||||||
|
if let Some(v) = &data.username {
|
||||||
|
pairs.push(("username".to_string(), v.clone()));
|
||||||
|
}
|
||||||
|
if let Some(v) = &data.photo_url {
|
||||||
|
pairs.push(("photo_url".to_string(), v.clone()));
|
||||||
|
}
|
||||||
|
pairs.push(("auth_date".to_string(), data.auth_date.to_string()));
|
||||||
|
|
||||||
|
pairs.sort_by(|a, b| a.0.cmp(&b.0));
|
||||||
|
|
||||||
|
pairs
|
||||||
|
.iter()
|
||||||
|
.map(|(k, v)| format!("{}={}", k, v))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join("\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn validate_auth_data(data: &AuthData, bot_token: &[u8]) -> Result<(), OAuthError> {
|
||||||
|
if bot_token.is_empty() {
|
||||||
|
return Err(OAuthError::Telegram(
|
||||||
|
"telegram bot token is not provided".into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let now = std::time::SystemTime::now()
|
||||||
|
.duration_since(std::time::UNIX_EPOCH)
|
||||||
|
.map_err(|e| OAuthError::Telegram(format!("system clock error: {}", e)))?
|
||||||
|
.as_secs() as i64;
|
||||||
|
|
||||||
|
if now - data.auth_date > AUTH_DATE_TTL_SECS {
|
||||||
|
return Err(OAuthError::Telegram("auth date is expired".into()));
|
||||||
|
}
|
||||||
|
|
||||||
|
let check_str = check_string(data);
|
||||||
|
|
||||||
|
let key = sha2::Sha256::digest(bot_token);
|
||||||
|
let mut mac = Hmac::<Sha256>::new_from_slice(&key)
|
||||||
|
.map_err(|e| OAuthError::Telegram(format!("HMAC key error: {}", e)))?;
|
||||||
|
mac.update(check_str.as_bytes());
|
||||||
|
let computed = hex::encode(mac.finalize().into_bytes());
|
||||||
|
|
||||||
|
if data.hash != computed {
|
||||||
|
return Err(OAuthError::Telegram("hash is not valid".into()));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_and_validate_auth_data(
|
||||||
|
json_bytes: &[u8],
|
||||||
|
bot_token: &[u8],
|
||||||
|
) -> Result<AuthData, OAuthError> {
|
||||||
|
let data: AuthData = serde_json::from_slice(json_bytes)
|
||||||
|
.map_err(|e| OAuthError::Telegram(format!("json parse error: {}", e)))?;
|
||||||
|
validate_auth_data(&data, bot_token)?;
|
||||||
|
Ok(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_base64_and_validate(
|
||||||
|
base64_str: &str,
|
||||||
|
bot_token: &[u8],
|
||||||
|
) -> Result<AuthData, OAuthError> {
|
||||||
|
let decoded = base64::engine::general_purpose::STANDARD
|
||||||
|
.decode(base64_str.as_bytes())
|
||||||
|
.or_else(|_| {
|
||||||
|
base64::engine::general_purpose::URL_SAFE_NO_PAD.decode(base64_str.as_bytes())
|
||||||
|
})
|
||||||
|
.map_err(|e| OAuthError::Telegram(format!("base64 decode error: {}", e)))?;
|
||||||
|
|
||||||
|
parse_and_validate_auth_data(&decoded, bot_token)
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
[package]
|
||||||
|
name = "password"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
pbkdf2 = "0.12"
|
||||||
|
sha2 = "0.10"
|
||||||
|
md5 = "0.7"
|
||||||
|
bcrypt = "0.15"
|
||||||
|
hex = "0.4"
|
||||||
|
rand = "0.8"
|
||||||
@@ -0,0 +1,231 @@
|
|||||||
|
//! 密码加密与验证工具。
|
||||||
|
//!
|
||||||
|
//! 从 `/root/project-moth/server/pkg/tool/encryption.go` 迁移而来。
|
||||||
|
//! 提供密码哈希(PBKDF2-SHA512)、MD5 编码以及多算法验证。
|
||||||
|
|
||||||
|
use pbkdf2::pbkdf2_hmac;
|
||||||
|
use sha2::{Digest, Sha256, Sha512};
|
||||||
|
|
||||||
|
const PBKDF2_SALT_LEN: usize = 16;
|
||||||
|
const PBKDF2_ITERATIONS: u32 = 100;
|
||||||
|
const PBKDF2_KEY_LEN: usize = 32;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum PasswordError {
|
||||||
|
HashError(String),
|
||||||
|
ParseError(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for PasswordError {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
PasswordError::HashError(msg) => write!(f, "Hash error: {}", msg),
|
||||||
|
PasswordError::ParseError(msg) => write!(f, "Parse error: {}", msg),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::error::Error for PasswordError {}
|
||||||
|
|
||||||
|
/// 使用 PBKDF2-SHA512 对密码进行加盐编码。
|
||||||
|
///
|
||||||
|
/// 返回格式化字符串:`$pbkdf2-sha512$<salt>$<hash>`。
|
||||||
|
///
|
||||||
|
/// # 示例
|
||||||
|
/// ```
|
||||||
|
/// let encoded = password::encode_password("mypassword").unwrap();
|
||||||
|
/// assert!(encoded.starts_with("$pbkdf2-sha512$"));
|
||||||
|
/// ```
|
||||||
|
pub fn encode_password(password: &str) -> Result<String, PasswordError> {
|
||||||
|
use rand::Rng;
|
||||||
|
|
||||||
|
// 生成随机盐值
|
||||||
|
let mut rng = rand::thread_rng();
|
||||||
|
let salt: Vec<u8> = (0..PBKDF2_SALT_LEN).map(|_| rng.gen()).collect();
|
||||||
|
|
||||||
|
// 使用 PBKDF2-HMAC-SHA512 对密码进行哈希
|
||||||
|
let mut key = vec![0u8; PBKDF2_KEY_LEN];
|
||||||
|
pbkdf2_hmac::<Sha512>(password.as_bytes(), &salt, PBKDF2_ITERATIONS, &mut key);
|
||||||
|
|
||||||
|
// 将盐值和密钥编码为十六进制
|
||||||
|
let salt_hex = hex::encode(&salt);
|
||||||
|
let key_hex = hex::encode(&key);
|
||||||
|
|
||||||
|
Ok(format!("$pbkdf2-sha512${}${}", salt_hex, key_hex))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 验证密码与编码后的密码哈希是否匹配。
|
||||||
|
///
|
||||||
|
/// 预期格式:`$pbkdf2-sha512$<salt>$<hash>`。
|
||||||
|
///
|
||||||
|
/// # 示例
|
||||||
|
/// ```
|
||||||
|
/// let encoded = password::encode_password("mypassword").unwrap();
|
||||||
|
/// assert!(password::verify_password("mypassword", &encoded));
|
||||||
|
/// assert!(!password::verify_password("wrongpass", &encoded));
|
||||||
|
/// ```
|
||||||
|
pub fn verify_password(password: &str, encoded: &str) -> bool {
|
||||||
|
let parts: Vec<&str> = encoded.split('$').collect();
|
||||||
|
if parts.len() < 4 || parts[1] != "pbkdf2-sha512" {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let salt_hex = parts[2];
|
||||||
|
let expected_hash_hex = parts[3];
|
||||||
|
|
||||||
|
// 从十六进制解码盐值
|
||||||
|
let salt = match hex::decode(salt_hex) {
|
||||||
|
Ok(s) => s,
|
||||||
|
Err(_) => return false,
|
||||||
|
};
|
||||||
|
|
||||||
|
// 计算哈希值
|
||||||
|
let mut key = vec![0u8; PBKDF2_KEY_LEN];
|
||||||
|
pbkdf2_hmac::<Sha512>(password.as_bytes(), &salt, PBKDF2_ITERATIONS, &mut key);
|
||||||
|
|
||||||
|
// 与期望的哈希值进行比较
|
||||||
|
let computed_hash_hex = hex::encode(&key);
|
||||||
|
computed_hash_hex == expected_hash_hex
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 计算输入字符串的 MD5 哈希值。
|
||||||
|
///
|
||||||
|
/// # 参数
|
||||||
|
/// * `s` - 输入字符串
|
||||||
|
/// * `uppercase` - 若为 true 则返回大写十六进制,否则返回小写
|
||||||
|
///
|
||||||
|
/// # 示例
|
||||||
|
/// ```
|
||||||
|
/// let hash = password::md5_encode("hello", false);
|
||||||
|
/// assert_eq!(hash, "5d41402abc4b2a76b9719d911017c592");
|
||||||
|
/// ```
|
||||||
|
pub fn md5_encode(s: &str, uppercase: bool) -> String {
|
||||||
|
let digest = md5::compute(s.as_bytes());
|
||||||
|
let result = format!("{:x}", digest);
|
||||||
|
if uppercase {
|
||||||
|
result.to_uppercase()
|
||||||
|
} else {
|
||||||
|
result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 使用多种算法验证密码。
|
||||||
|
///
|
||||||
|
/// 支持的算法:
|
||||||
|
/// - `"md5"`:简单 MD5 哈希
|
||||||
|
/// - `"sha256"`:简单 SHA-256 哈希
|
||||||
|
/// - `"md5salt"`:MD5(密码 + 盐值)
|
||||||
|
/// - `"sha256salt"`:SHA-256(密码 + 盐值),由 SSPanel 使用
|
||||||
|
/// - `"default"`:PBKDF2-SHA512(PPanel 默认)
|
||||||
|
/// - `"bcrypt"`:Bcrypt 哈希
|
||||||
|
///
|
||||||
|
/// # 参数
|
||||||
|
/// * `algo` - 算法名称
|
||||||
|
/// * `salt` - 盐值字符串(用于 `*salt` 算法)
|
||||||
|
/// * `password` - 明文密码
|
||||||
|
/// * `hash` - 期望的哈希值
|
||||||
|
///
|
||||||
|
/// # 示例
|
||||||
|
/// ```
|
||||||
|
/// let result = password::multi_password_verify("md5", "", "hello", "5d41402abc4b2a76b9719d911017c592");
|
||||||
|
/// assert!(result);
|
||||||
|
/// ```
|
||||||
|
pub fn multi_password_verify(algo: &str, salt: &str, password: &str, hash: &str) -> bool {
|
||||||
|
match algo {
|
||||||
|
"md5" => {
|
||||||
|
let digest = md5::compute(password.as_bytes());
|
||||||
|
let computed = format!("{:x}", digest);
|
||||||
|
computed == hash
|
||||||
|
}
|
||||||
|
"sha256" => {
|
||||||
|
let mut hasher = Sha256::new();
|
||||||
|
hasher.update(password.as_bytes());
|
||||||
|
let computed = hex::encode(hasher.finalize());
|
||||||
|
computed == hash
|
||||||
|
}
|
||||||
|
"md5salt" => {
|
||||||
|
let input = format!("{}{}", password, salt);
|
||||||
|
let digest = md5::compute(input.as_bytes());
|
||||||
|
let computed = format!("{:x}", digest);
|
||||||
|
computed == hash
|
||||||
|
}
|
||||||
|
"sha256salt" => {
|
||||||
|
let input = format!("{}{}", password, salt);
|
||||||
|
let mut hasher = Sha256::new();
|
||||||
|
hasher.update(input.as_bytes());
|
||||||
|
let computed = hex::encode(hasher.finalize());
|
||||||
|
computed == hash
|
||||||
|
}
|
||||||
|
"default" => verify_password(password, hash),
|
||||||
|
"bcrypt" => bcrypt::verify(password, hash).unwrap_or(false),
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_encode_and_verify_password() {
|
||||||
|
let password = "test_password_123";
|
||||||
|
let encoded = encode_password(password).unwrap();
|
||||||
|
|
||||||
|
eprintln!("Encoded password: {}", encoded);
|
||||||
|
|
||||||
|
assert!(encoded.starts_with("$pbkdf2-sha512$"));
|
||||||
|
assert!(verify_password(password, &encoded));
|
||||||
|
assert!(!verify_password("wrong_password", &encoded));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_md5_encode() {
|
||||||
|
let input = "hello";
|
||||||
|
let lowercase = md5_encode(input, false);
|
||||||
|
let uppercase = md5_encode(input, true);
|
||||||
|
|
||||||
|
assert_eq!(lowercase, "5d41402abc4b2a76b9719d911017c592");
|
||||||
|
assert_eq!(uppercase, "5D41402ABC4B2A76B9719D911017C592");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_multi_password_verify_md5() {
|
||||||
|
let password = "hello";
|
||||||
|
let hash = "5d41402abc4b2a76b9719d911017c592";
|
||||||
|
assert!(multi_password_verify("md5", "", password, hash));
|
||||||
|
assert!(!multi_password_verify("md5", "", "wrong", hash));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_multi_password_verify_sha256() {
|
||||||
|
let password = "hello";
|
||||||
|
let hash = "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824";
|
||||||
|
assert!(multi_password_verify("sha256", "", password, hash));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_multi_password_verify_md5salt() {
|
||||||
|
let password = "hello";
|
||||||
|
let salt = "world";
|
||||||
|
// MD5("helloworld") = fc5e038d38a57032085441e7fe7010b0
|
||||||
|
let hash = "fc5e038d38a57032085441e7fe7010b0";
|
||||||
|
assert!(multi_password_verify("md5salt", salt, password, hash));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_multi_password_verify_bcrypt() {
|
||||||
|
let password = "test123";
|
||||||
|
// 预先为 "test123" 生成的 bcrypt 哈希值
|
||||||
|
let hash = bcrypt::hash(password, 4).unwrap();
|
||||||
|
assert!(multi_password_verify("bcrypt", "", password, &hash));
|
||||||
|
assert!(!multi_password_verify("bcrypt", "", "wrong", &hash));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_multi_password_verify_default() {
|
||||||
|
let password = "test123";
|
||||||
|
let encoded = encode_password(password).unwrap();
|
||||||
|
assert!(multi_password_verify("default", "", password, &encoded));
|
||||||
|
assert!(!multi_password_verify("default", "", "wrong", &encoded));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
[package]
|
||||||
|
name = "payment"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
alipay_sdk_rust = "1"
|
||||||
|
stripe = { version = "0.40", package = "async-stripe", features = ["runtime-tokio-hyper"] }
|
||||||
|
md5 = "0.7"
|
||||||
|
reqwest = { version = "0.12", features = ["json"] }
|
||||||
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
serde_json = "1"
|
||||||
|
serde_urlencoded = "0.7"
|
||||||
|
thiserror = "2"
|
||||||
|
tracing = "0.1"
|
||||||
|
url = "2"
|
||||||
@@ -0,0 +1,162 @@
|
|||||||
|
use alipay_sdk_rust::{
|
||||||
|
biz::{BizContenter, TradePrecreateBiz, TradeQueryBiz},
|
||||||
|
pay::{Payer, PayClient},
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::error::PaymentError;
|
||||||
|
use crate::types::Cents;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Config {
|
||||||
|
pub app_id: String,
|
||||||
|
pub private_key: String,
|
||||||
|
pub public_key: String,
|
||||||
|
pub invoice_name: String,
|
||||||
|
pub notify_url: String,
|
||||||
|
pub sandbox: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub enum OrderStatus {
|
||||||
|
Success,
|
||||||
|
Pending,
|
||||||
|
Closed,
|
||||||
|
Finished,
|
||||||
|
Error(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Notification {
|
||||||
|
pub order_no: String,
|
||||||
|
pub amount: Cents,
|
||||||
|
pub status: OrderStatus,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Provider {
|
||||||
|
client: Box<dyn Payer>,
|
||||||
|
config: Config,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Provider {
|
||||||
|
pub fn new(config: Config) -> Result<Self, PaymentError> {
|
||||||
|
let api_url = if config.sandbox {
|
||||||
|
"https://openapi-sandbox.dl.alipaydev.com/gateway.do"
|
||||||
|
} else {
|
||||||
|
"https://openapi.dl.alipaydev.com/gateway.do"
|
||||||
|
};
|
||||||
|
|
||||||
|
let app_id = config.app_id.clone();
|
||||||
|
let private_key = config.private_key.clone();
|
||||||
|
let public_key = config.public_key.clone();
|
||||||
|
|
||||||
|
let client: Box<dyn Payer> = Box::new(
|
||||||
|
PayClient::builder()
|
||||||
|
.api_url(api_url)
|
||||||
|
.app_id(&app_id)
|
||||||
|
.private_key(&private_key)
|
||||||
|
.public_key(&public_key)
|
||||||
|
.sign_type_rsa2()
|
||||||
|
.charset_utf8()
|
||||||
|
.format_json()
|
||||||
|
.version_1_0()
|
||||||
|
.build()
|
||||||
|
.map_err(|e| PaymentError::Config(format!("Alipay config error: {e}")))?,
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(Provider { client, config })
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn pre_create_trade(&self, order_no: &str, amount: Cents) -> Result<String, PaymentError> {
|
||||||
|
let mut biz = TradePrecreateBiz::new();
|
||||||
|
biz.set_out_trade_no(order_no.to_string().into());
|
||||||
|
biz.set_total_amount(amount.to_yuan_string().into());
|
||||||
|
biz.set_subject(self.config.invoice_name.clone().into());
|
||||||
|
biz.set("notify_url", self.config.notify_url.clone().into());
|
||||||
|
|
||||||
|
let resp = self
|
||||||
|
.client
|
||||||
|
.trade_precreate(&biz)
|
||||||
|
.map_err(|e| PaymentError::Alipay(e.to_string()))?;
|
||||||
|
|
||||||
|
if resp.response.code.as_deref() != Some("10000") {
|
||||||
|
return Err(PaymentError::Alipay(
|
||||||
|
resp.response
|
||||||
|
.sub_msg
|
||||||
|
.unwrap_or_else(|| "unknown alipay error".into()),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
resp.response
|
||||||
|
.qr_code
|
||||||
|
.ok_or_else(|| PaymentError::Alipay("QR code not returned".into()))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn query_trade(&self, order_no: &str) -> Result<OrderStatus, PaymentError> {
|
||||||
|
let mut biz = TradeQueryBiz::new();
|
||||||
|
biz.set_out_trade_no(order_no.to_string().into());
|
||||||
|
|
||||||
|
let resp = self
|
||||||
|
.client
|
||||||
|
.trade_query(&biz)
|
||||||
|
.map_err(|e| PaymentError::Alipay(e.to_string()))?;
|
||||||
|
|
||||||
|
match resp.response.trade_status.as_deref() {
|
||||||
|
Some("TRADE_SUCCESS") | Some("TRADE_FINISHED") => Ok(OrderStatus::Success),
|
||||||
|
Some("WAIT_BUYER_PAY") => Ok(OrderStatus::Pending),
|
||||||
|
Some("TRADE_CLOSED") => Ok(OrderStatus::Closed),
|
||||||
|
Some(s) => Ok(OrderStatus::Error(s.into())),
|
||||||
|
None => Ok(OrderStatus::Error("no trade status".into())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn decode_notification(&self, body: &[u8]) -> Result<Notification, PaymentError> {
|
||||||
|
let verified = self
|
||||||
|
.client
|
||||||
|
.async_verify_sign(body)
|
||||||
|
.map_err(|e| PaymentError::Alipay(format!("notification verify failed: {e}")))?;
|
||||||
|
|
||||||
|
if !verified {
|
||||||
|
return Err(PaymentError::Alipay(
|
||||||
|
"notification sign verification failed".into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let parsed: serde_json::Value = serde_json::from_slice(body)
|
||||||
|
.map_err(|e| PaymentError::Alipay(format!("invalid notify body: {e}")))?;
|
||||||
|
|
||||||
|
let response = parsed
|
||||||
|
.as_object()
|
||||||
|
.and_then(|m| m.values().next())
|
||||||
|
.and_then(|v| v.as_object())
|
||||||
|
.ok_or_else(|| PaymentError::Alipay("cannot parse notification".into()))?;
|
||||||
|
|
||||||
|
let trade_status = response
|
||||||
|
.get("trade_status")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.unwrap_or("");
|
||||||
|
let out_trade_no = response
|
||||||
|
.get("out_trade_no")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.unwrap_or("");
|
||||||
|
let total_amount = response
|
||||||
|
.get("total_amount")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.unwrap_or("0");
|
||||||
|
|
||||||
|
let status = match trade_status {
|
||||||
|
"TRADE_SUCCESS" => OrderStatus::Success,
|
||||||
|
"WAIT_BUYER_PAY" => OrderStatus::Pending,
|
||||||
|
"TRADE_CLOSED" => OrderStatus::Closed,
|
||||||
|
"TRADE_FINISHED" => OrderStatus::Finished,
|
||||||
|
s => OrderStatus::Error(s.into()),
|
||||||
|
};
|
||||||
|
|
||||||
|
let amount = Cents::from_yuan(total_amount)
|
||||||
|
.map_err(|e| PaymentError::Alipay(format!("Invalid amount: {e}")))?;
|
||||||
|
|
||||||
|
Ok(Notification {
|
||||||
|
order_no: out_trade_no.into(),
|
||||||
|
amount,
|
||||||
|
status,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,146 @@
|
|||||||
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
|
use crate::error::PaymentError;
|
||||||
|
use crate::types::Cents;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Config {
|
||||||
|
pub pid: String,
|
||||||
|
pub url: String,
|
||||||
|
pub key: String,
|
||||||
|
pub pay_type: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Order {
|
||||||
|
pub name: String,
|
||||||
|
pub order_no: String,
|
||||||
|
pub amount: Cents,
|
||||||
|
pub sign_type: String,
|
||||||
|
pub notify_url: String,
|
||||||
|
pub return_url: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Provider {
|
||||||
|
pub pid: String,
|
||||||
|
pub url: String,
|
||||||
|
pub key: String,
|
||||||
|
pub pay_type: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Provider {
|
||||||
|
pub fn new(config: Config) -> Self {
|
||||||
|
Provider {
|
||||||
|
pid: config.pid,
|
||||||
|
url: config.url,
|
||||||
|
key: config.key,
|
||||||
|
pay_type: config.pay_type,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn params_map(&self, order: &Order) -> BTreeMap<&str, String> {
|
||||||
|
let mut m = BTreeMap::new();
|
||||||
|
m.insert("pid", self.pid.clone());
|
||||||
|
m.insert("type", self.pay_type.clone());
|
||||||
|
m.insert("out_trade_no", order.order_no.clone());
|
||||||
|
m.insert("money", order.amount.to_yuan_string());
|
||||||
|
m.insert("name", order.name.clone());
|
||||||
|
m.insert("notify_url", order.notify_url.clone());
|
||||||
|
m.insert("return_url", order.return_url.clone());
|
||||||
|
m
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_sign(&self, params: &BTreeMap<&str, String>) -> String {
|
||||||
|
let query: String = params
|
||||||
|
.iter()
|
||||||
|
.filter(|(k, v)| !v.is_empty() && **k != "sign" && **k != "sign_type")
|
||||||
|
.map(|(k, v)| format!("{k}={v}"))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join("&");
|
||||||
|
let text = format!("{}{}", query, self.key);
|
||||||
|
format!("{:x}", md5::compute(text))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn create_pay_url(&self, order: &Order) -> Result<String, PaymentError> {
|
||||||
|
let params = self.params_map(order);
|
||||||
|
let sign = self.create_sign(¶ms);
|
||||||
|
|
||||||
|
let mut base_url = url::Url::parse(&self.url)
|
||||||
|
.map_err(|_| PaymentError::Config("invalid EPay URL".into()))?;
|
||||||
|
base_url = base_url
|
||||||
|
.join("/submit.php")
|
||||||
|
.map_err(|_| PaymentError::Config("invalid EPay path".into()))?;
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut pairs = base_url.query_pairs_mut();
|
||||||
|
for (k, v) in ¶ms {
|
||||||
|
pairs.append_pair(k, v);
|
||||||
|
}
|
||||||
|
pairs.append_pair("sign", &sign);
|
||||||
|
pairs.append_pair("sign_type", "MD5");
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(base_url.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn verify_sign(&self, params: &std::collections::HashMap<String, String>) -> bool {
|
||||||
|
let mut sorted = BTreeMap::new();
|
||||||
|
for (k, v) in params {
|
||||||
|
sorted.insert(k.as_str(), v.clone());
|
||||||
|
}
|
||||||
|
let expected = params.get("sign").cloned().unwrap_or_default();
|
||||||
|
self.create_sign(&sorted) == expected
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn query_order_status(&self, order_no: &str) -> Result<bool, PaymentError> {
|
||||||
|
let query_url = format!(
|
||||||
|
"{}/api.php?act=order&pid={}&out_trade_no={}",
|
||||||
|
self.url, self.pid, order_no
|
||||||
|
);
|
||||||
|
let resp = reqwest::get(&query_url).await?;
|
||||||
|
let body: serde_json::Value = resp.json().await?;
|
||||||
|
let status = body.get("status").and_then(|v| v.as_i64()).unwrap_or(0);
|
||||||
|
Ok(status == 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_epay_sign() {
|
||||||
|
let provider = Provider::new(Config {
|
||||||
|
pid: "1654".into(),
|
||||||
|
url: "http://127.0.0.1".into(),
|
||||||
|
key: "LbTabbB580zWyhXhyyww7wwvy5u8k0wl".into(),
|
||||||
|
pay_type: "alipay".into(),
|
||||||
|
});
|
||||||
|
|
||||||
|
let order = Order {
|
||||||
|
name: "product".into(),
|
||||||
|
order_no: "202412152115078262977262254".into(),
|
||||||
|
amount: Cents(1000),
|
||||||
|
sign_type: "MD5".into(),
|
||||||
|
notify_url: "".into(),
|
||||||
|
return_url: "".into(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let url = provider.create_pay_url(&order).unwrap();
|
||||||
|
assert!(url.contains("sign="));
|
||||||
|
assert!(url.contains("sign_type=MD5"));
|
||||||
|
|
||||||
|
// Verify sign from callback params (matches Go test data)
|
||||||
|
let params = std::collections::HashMap::from([
|
||||||
|
("pid".into(), "1654".into()),
|
||||||
|
("trade_no".into(), "2024121521150860990".into()),
|
||||||
|
("out_trade_no".into(), "202412152115078262977262254".into()),
|
||||||
|
("type".into(), "alipay".into()),
|
||||||
|
("name".into(), "product".into()),
|
||||||
|
("money".into(), "10".into()),
|
||||||
|
("trade_status".into(), "TRADE_SUCCESS".into()),
|
||||||
|
("sign".into(), "d3181f18ebdf9821f0ab6ee93faa82d1".into()),
|
||||||
|
("sign_type".into(), "MD5".into()),
|
||||||
|
]);
|
||||||
|
assert!(provider.verify_sign(¶ms));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
#[derive(Error, Debug)]
|
||||||
|
pub enum PaymentError {
|
||||||
|
#[error("Stripe error: {0}")]
|
||||||
|
Stripe(#[from] stripe::StripeError),
|
||||||
|
|
||||||
|
#[error("Stripe webhook error: {0}")]
|
||||||
|
StripeWebhook(String),
|
||||||
|
|
||||||
|
#[error("Alipay error: {0}")]
|
||||||
|
Alipay(String),
|
||||||
|
|
||||||
|
#[error("EPay error: {0}")]
|
||||||
|
EPay(String),
|
||||||
|
|
||||||
|
#[error("HTTP request failed: {0}")]
|
||||||
|
Http(#[from] reqwest::Error),
|
||||||
|
|
||||||
|
#[error("URL parse error: {0}")]
|
||||||
|
Url(#[from] url::ParseError),
|
||||||
|
|
||||||
|
#[error("Serialization error: {0}")]
|
||||||
|
Serde(#[from] serde_json::Error),
|
||||||
|
|
||||||
|
#[error("Invalid configuration: {0}")]
|
||||||
|
Config(String),
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
pub mod alipay;
|
||||||
|
pub mod epay;
|
||||||
|
pub mod error;
|
||||||
|
pub mod platform;
|
||||||
|
pub mod stripe;
|
||||||
|
pub mod types;
|
||||||
|
|
||||||
|
pub use error::PaymentError;
|
||||||
|
pub use platform::{get_supported_platforms, Platform, PlatformInfo};
|
||||||
|
pub use types::{Cents, Notification, Order, PaymentSheet, User};
|
||||||
@@ -0,0 +1,146 @@
|
|||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum Platform {
|
||||||
|
Stripe,
|
||||||
|
AlipayF2F,
|
||||||
|
EPay,
|
||||||
|
Balance,
|
||||||
|
CryptoSaaS,
|
||||||
|
Unsupported,
|
||||||
|
}
|
||||||
|
|
||||||
|
const PLATFORM_NAMES: &[(&str, Platform)] = &[
|
||||||
|
("CryptoSaaS", Platform::CryptoSaaS),
|
||||||
|
("Stripe", Platform::Stripe),
|
||||||
|
("AlipayF2F", Platform::AlipayF2F),
|
||||||
|
("EPay", Platform::EPay),
|
||||||
|
("balance", Platform::Balance),
|
||||||
|
];
|
||||||
|
|
||||||
|
impl Platform {
|
||||||
|
pub fn from_str(s: &str) -> Self {
|
||||||
|
for &(name, platform) in PLATFORM_NAMES {
|
||||||
|
if name == s {
|
||||||
|
return platform;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Platform::Unsupported
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn as_str(&self) -> &'static str {
|
||||||
|
for &(name, platform) in PLATFORM_NAMES {
|
||||||
|
if platform == *self {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"unsupported"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for Platform {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
write!(f, "{}", self.as_str())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::str::FromStr for Platform {
|
||||||
|
type Err = ();
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
match Self::from_str(s) {
|
||||||
|
Platform::Unsupported => Err(()),
|
||||||
|
p => Ok(p),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize)]
|
||||||
|
pub struct PlatformInfo {
|
||||||
|
pub platform: String,
|
||||||
|
pub platform_url: String,
|
||||||
|
pub platform_field_description: std::collections::HashMap<String, String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_supported_platforms() -> Vec<PlatformInfo> {
|
||||||
|
vec![
|
||||||
|
PlatformInfo {
|
||||||
|
platform: "Stripe".into(),
|
||||||
|
platform_url: "https://stripe.com".into(),
|
||||||
|
platform_field_description: {
|
||||||
|
let mut m = std::collections::HashMap::new();
|
||||||
|
m.insert("public_key".into(), "Publishable key".into());
|
||||||
|
m.insert("secret_key".into(), "Secret key".into());
|
||||||
|
m.insert("webhook_secret".into(), "Webhook secret".into());
|
||||||
|
m.insert("payment".into(), "Payment Method, only supported card/alipay/wechat_pay".into());
|
||||||
|
m
|
||||||
|
},
|
||||||
|
},
|
||||||
|
PlatformInfo {
|
||||||
|
platform: "AlipayF2F".into(),
|
||||||
|
platform_url: "https://alipay.com".into(),
|
||||||
|
platform_field_description: {
|
||||||
|
let mut m = std::collections::HashMap::new();
|
||||||
|
m.insert("app_id".into(), "App ID".into());
|
||||||
|
m.insert("private_key".into(), "Private Key".into());
|
||||||
|
m.insert("public_key".into(), "Public Key".into());
|
||||||
|
m.insert("invoice_name".into(), "Invoice Name".into());
|
||||||
|
m.insert("sandbox".into(), "Sandbox Mode".into());
|
||||||
|
m
|
||||||
|
},
|
||||||
|
},
|
||||||
|
PlatformInfo {
|
||||||
|
platform: "EPay".into(),
|
||||||
|
platform_url: String::new(),
|
||||||
|
platform_field_description: {
|
||||||
|
let mut m = std::collections::HashMap::new();
|
||||||
|
m.insert("pid".into(), "PID".into());
|
||||||
|
m.insert("url".into(), "URL".into());
|
||||||
|
m.insert("key".into(), "Key".into());
|
||||||
|
m.insert("type".into(), "Type".into());
|
||||||
|
m
|
||||||
|
},
|
||||||
|
},
|
||||||
|
PlatformInfo {
|
||||||
|
platform: "CryptoSaaS".into(),
|
||||||
|
platform_url: "https://t.me/CryptoSaaSBot".into(),
|
||||||
|
platform_field_description: {
|
||||||
|
let mut m = std::collections::HashMap::new();
|
||||||
|
m.insert("endpoint".into(), "API Endpoint".into());
|
||||||
|
m.insert("account_id".into(), "Account ID".into());
|
||||||
|
m.insert("secret_key".into(), "Secret Key".into());
|
||||||
|
m
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
use std::fmt;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_platform_parse() {
|
||||||
|
assert_eq!(Platform::from_str("Stripe"), Platform::Stripe);
|
||||||
|
assert_eq!(Platform::from_str("AlipayF2F"), Platform::AlipayF2F);
|
||||||
|
assert_eq!(Platform::from_str("EPay"), Platform::EPay);
|
||||||
|
assert_eq!(Platform::from_str("balance"), Platform::Balance);
|
||||||
|
assert_eq!(Platform::from_str("CryptoSaaS"), Platform::CryptoSaaS);
|
||||||
|
assert_eq!(Platform::from_str("unknown"), Platform::Unsupported);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_platform_display() {
|
||||||
|
assert_eq!(Platform::Stripe.to_string(), "Stripe");
|
||||||
|
assert_eq!(Platform::Unsupported.to_string(), "unsupported");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_get_supported_platforms() {
|
||||||
|
let platforms = get_supported_platforms();
|
||||||
|
assert_eq!(platforms.len(), 4);
|
||||||
|
assert!(platforms.iter().any(|p| p.platform == "Stripe"));
|
||||||
|
assert!(platforms.iter().any(|p| p.platform == "EPay"));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,171 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use stripe::{
|
||||||
|
Client as StripeClient, CreateCustomer, CreateEphemeralKey, CreatePaymentIntent,
|
||||||
|
CreateWebhookEndpoint, Customer, CustomerSearchParams, EphemeralKey, EventFilter,
|
||||||
|
EventObject, Expandable, PaymentIntent, PaymentIntentId, PaymentIntentStatus,
|
||||||
|
PaymentMethod, PaymentMethodId, Webhook, WebhookEndpoint,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::error::PaymentError;
|
||||||
|
use crate::types::{Cents, Notification, Order, PaymentSheet, User};
|
||||||
|
|
||||||
|
pub const API_VERSION: &str = "2024-04-10";
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Config {
|
||||||
|
pub public_key: String,
|
||||||
|
pub secret_key: String,
|
||||||
|
pub webhook_secret: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Provider {
|
||||||
|
client: StripeClient,
|
||||||
|
config: Config,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Provider {
|
||||||
|
pub fn new(config: Config) -> Self {
|
||||||
|
let client = StripeClient::new(&config.secret_key);
|
||||||
|
Provider { client, config }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn create_payment_sheet(
|
||||||
|
&self,
|
||||||
|
order: &Order,
|
||||||
|
user: &User,
|
||||||
|
) -> Result<PaymentSheet, PaymentError> {
|
||||||
|
let customer = self.find_or_create_customer(user).await?;
|
||||||
|
|
||||||
|
let mut ek_params = CreateEphemeralKey::new();
|
||||||
|
ek_params.customer = Some(customer.id.clone());
|
||||||
|
let ek = EphemeralKey::create(&self.client, ek_params).await?;
|
||||||
|
|
||||||
|
let mut metadata = HashMap::new();
|
||||||
|
metadata.insert("order_no".to_string(), order.order_no.clone());
|
||||||
|
metadata.insert("user_id".to_string(), user.user_id.to_string());
|
||||||
|
metadata.insert("subscribe".to_string(), order.subscribe.clone());
|
||||||
|
|
||||||
|
let currency = stripe::Currency::from_str(&order.currency)
|
||||||
|
.map_err(|e| PaymentError::Config(format!("invalid currency: {e}")))?;
|
||||||
|
|
||||||
|
let mut pi_params = CreatePaymentIntent::new(order.amount.0, currency);
|
||||||
|
pi_params.customer = Some(customer.id.clone());
|
||||||
|
pi_params.payment_method_types = Some(vec![order.payment.clone()]);
|
||||||
|
pi_params.metadata = Some(metadata);
|
||||||
|
|
||||||
|
let pi = PaymentIntent::create(&self.client, pi_params).await?;
|
||||||
|
|
||||||
|
Ok(PaymentSheet {
|
||||||
|
client_secret: pi.client_secret.unwrap_or_default(),
|
||||||
|
ephemeral_key: ek.secret.unwrap_or_default(),
|
||||||
|
customer: customer.id.to_string(),
|
||||||
|
publishable_key: self.config.public_key.clone(),
|
||||||
|
trade_no: pi.id.to_string(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn find_or_create_customer(&self, user: &User) -> Result<Customer, PaymentError> {
|
||||||
|
if let Some(customer) = self.search_customer(user).await? {
|
||||||
|
return Ok(customer);
|
||||||
|
}
|
||||||
|
self.create_customer(user).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn search_customer(&self, user: &User) -> Result<Option<Customer>, PaymentError> {
|
||||||
|
let query = if !user.email.is_empty() {
|
||||||
|
format!("email:'{}'", user.email)
|
||||||
|
} else {
|
||||||
|
format!("metadata['user_id']:'{}'", user.user_id)
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut params = CustomerSearchParams::new();
|
||||||
|
params.query = query;
|
||||||
|
|
||||||
|
let result = Customer::search(&self.client, params).await?;
|
||||||
|
Ok(result.data.into_iter().next())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn create_customer(&self, user: &User) -> Result<Customer, PaymentError> {
|
||||||
|
let mut params = CreateCustomer::new();
|
||||||
|
if !user.email.is_empty() {
|
||||||
|
params.email = Some(&user.email);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut metadata = HashMap::new();
|
||||||
|
metadata.insert("user_id".to_string(), user.user_id.to_string());
|
||||||
|
params.metadata = Some(metadata);
|
||||||
|
|
||||||
|
Ok(Customer::create(&self.client, params).await?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn query_order_status(&self, trade_no: &str) -> Result<bool, PaymentError> {
|
||||||
|
let id = PaymentIntentId::from_str(trade_no)
|
||||||
|
.map_err(|_| PaymentError::Config("invalid PaymentIntent ID".into()))?;
|
||||||
|
let intent = PaymentIntent::retrieve(&self.client, &id, &[]).await?;
|
||||||
|
Ok(intent.status == PaymentIntentStatus::Succeeded)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_notify(
|
||||||
|
&self,
|
||||||
|
payload: &[u8],
|
||||||
|
signature: &str,
|
||||||
|
) -> Result<Notification, PaymentError> {
|
||||||
|
let payload_str =
|
||||||
|
std::str::from_utf8(payload).map_err(|e| PaymentError::StripeWebhook(e.to_string()))?;
|
||||||
|
|
||||||
|
let event = Webhook::construct_event(payload_str, signature, &self.config.webhook_secret)
|
||||||
|
.map_err(|e| PaymentError::StripeWebhook(e.to_string()))?;
|
||||||
|
|
||||||
|
let pi = match event.data.object {
|
||||||
|
EventObject::PaymentIntent(pi) => pi,
|
||||||
|
_ => {
|
||||||
|
return Err(PaymentError::StripeWebhook(
|
||||||
|
"unexpected event object type".into(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let order_no = pi.metadata.get("order_no").cloned().unwrap_or_default();
|
||||||
|
let user_id: i64 = pi
|
||||||
|
.metadata
|
||||||
|
.get("user_id")
|
||||||
|
.and_then(|v| v.parse().ok())
|
||||||
|
.unwrap_or(0);
|
||||||
|
|
||||||
|
let method = match pi.payment_method {
|
||||||
|
Some(Expandable::Object(ref pm)) => Some(pm.type_.to_string()),
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Notification {
|
||||||
|
event_type: event.type_.to_string(),
|
||||||
|
order_no,
|
||||||
|
trade_no: pi.id.to_string(),
|
||||||
|
user_id,
|
||||||
|
amount: Cents(pi.amount),
|
||||||
|
method,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn retrieve_payment_method(
|
||||||
|
&self,
|
||||||
|
id: &str,
|
||||||
|
) -> Result<PaymentMethod, PaymentError> {
|
||||||
|
let pm_id = PaymentMethodId::from_str(id)
|
||||||
|
.map_err(|_| PaymentError::Config("invalid PaymentMethod ID".into()))?;
|
||||||
|
Ok(PaymentMethod::retrieve(&self.client, &pm_id, &[]).await?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn create_webhook_endpoint(
|
||||||
|
&self,
|
||||||
|
url: &str,
|
||||||
|
) -> Result<WebhookEndpoint, PaymentError> {
|
||||||
|
let params = CreateWebhookEndpoint::new(
|
||||||
|
vec![EventFilter::PaymentIntentSucceeded, EventFilter::PaymentIntentPaymentFailed],
|
||||||
|
url,
|
||||||
|
);
|
||||||
|
Ok(WebhookEndpoint::create(&self.client, params).await?)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::fmt;
|
||||||
|
use std::ops::{Div, Mul};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
#[serde(transparent)]
|
||||||
|
pub struct Cents(pub i64);
|
||||||
|
|
||||||
|
impl Cents {
|
||||||
|
pub fn from_yuan(s: &str) -> Result<Self, std::num::ParseFloatError> {
|
||||||
|
let yuan: f64 = s.parse()?;
|
||||||
|
Ok(Cents((yuan * 100.0).round() as i64))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_yuan_f64(&self) -> f64 {
|
||||||
|
self.0 as f64 / 100.0
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_yuan_string(&self) -> String {
|
||||||
|
format!("{:.2}", self.to_yuan_f64())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for Cents {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
write!(f, "{}", self.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Mul<i64> for Cents {
|
||||||
|
type Output = Cents;
|
||||||
|
fn mul(self, rhs: i64) -> Cents {
|
||||||
|
Cents(self.0 * rhs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Div<i64> for Cents {
|
||||||
|
type Output = Cents;
|
||||||
|
fn div(self, rhs: i64) -> Cents {
|
||||||
|
Cents(self.0 / rhs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_cents_from_yuan() {
|
||||||
|
let c = Cents::from_yuan("10.00").unwrap();
|
||||||
|
assert_eq!(c.0, 1000);
|
||||||
|
assert_eq!(c.to_yuan_string(), "10.00");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_cents_from_yuan_rounding() {
|
||||||
|
let c = Cents::from_yuan("9.99").unwrap();
|
||||||
|
assert_eq!(c.0, 999);
|
||||||
|
assert_eq!(c.to_yuan_string(), "9.99");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_cents_display() {
|
||||||
|
let c = Cents(100);
|
||||||
|
assert_eq!(c.to_string(), "100");
|
||||||
|
assert_eq!(c.to_yuan_f64(), 1.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct Order {
|
||||||
|
pub order_no: String,
|
||||||
|
pub amount: Cents,
|
||||||
|
pub currency: String,
|
||||||
|
pub payment: String,
|
||||||
|
pub subscribe: String,
|
||||||
|
pub name: String,
|
||||||
|
pub notify_url: String,
|
||||||
|
pub return_url: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct User {
|
||||||
|
pub user_id: i64,
|
||||||
|
pub email: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Notification {
|
||||||
|
pub event_type: String,
|
||||||
|
pub order_no: String,
|
||||||
|
pub trade_no: String,
|
||||||
|
pub user_id: i64,
|
||||||
|
pub amount: Cents,
|
||||||
|
pub method: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize)]
|
||||||
|
pub struct PaymentSheet {
|
||||||
|
pub client_secret: String,
|
||||||
|
pub ephemeral_key: String,
|
||||||
|
pub customer: String,
|
||||||
|
pub publishable_key: String,
|
||||||
|
pub trade_no: String,
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
[package]
|
||||||
|
name = "result"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
anyhow = "1"
|
||||||
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
serde_json = "1"
|
||||||
|
axum = "0.8"
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
tokio = { version = "1", features = ["macros", "rt"] }
|
||||||
@@ -0,0 +1,120 @@
|
|||||||
|
// Code-carrying error type.
|
||||||
|
//
|
||||||
|
// Ported from the Go package `xerr` (errors.go). A `CodeError` carries a
|
||||||
|
// numeric error code (shown to the front end) and a human-readable message,
|
||||||
|
// and can travel through an error chain so handlers can recover the code via
|
||||||
|
// [`find_code_error`].
|
||||||
|
|
||||||
|
use std::fmt;
|
||||||
|
use std::sync::LazyLock;
|
||||||
|
|
||||||
|
use crate::error_code::{map_err_msg, ERROR};
|
||||||
|
|
||||||
|
/// An error that carries a machine-readable code and a message.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct CodeError {
|
||||||
|
err_code: u32,
|
||||||
|
err_msg: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CodeError {
|
||||||
|
/// Creates a `CodeError` whose message is looked up from [`map_err_msg`].
|
||||||
|
///
|
||||||
|
/// Mirrors Go `xerr.NewErrCode`.
|
||||||
|
pub fn new_err_code(err_code: u32) -> Self {
|
||||||
|
Self {
|
||||||
|
err_code,
|
||||||
|
err_msg: map_err_msg(err_code).to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a `CodeError` with an explicit code and message.
|
||||||
|
///
|
||||||
|
/// Mirrors Go `xerr.NewErrCodeMsg`.
|
||||||
|
pub fn new_err_code_msg(err_code: u32, err_msg: impl Into<String>) -> Self {
|
||||||
|
Self {
|
||||||
|
err_code,
|
||||||
|
err_msg: err_msg.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a `CodeError` for an unspecified failure (`ERROR` code).
|
||||||
|
///
|
||||||
|
/// Mirrors Go `xerr.NewErrMsg`.
|
||||||
|
pub fn new_err_msg(err_msg: impl Into<String>) -> Self {
|
||||||
|
Self {
|
||||||
|
err_code: ERROR,
|
||||||
|
err_msg: err_msg.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the error code shown to the front end.
|
||||||
|
pub fn get_err_code(&self) -> u32 {
|
||||||
|
self.err_code
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the error message shown to the front end.
|
||||||
|
pub fn get_err_msg(&self) -> &str {
|
||||||
|
&self.err_msg
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for CodeError {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
// Preserve the original Go format (note the full-width comma).
|
||||||
|
write!(f, "ErrCode:{},ErrMsg:{}", self.err_code, self.err_msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::error::Error for CodeError {}
|
||||||
|
|
||||||
|
/// Sentinel error for "304 Not Modified".
|
||||||
|
///
|
||||||
|
/// Mirrors Go `xerr.StatusNotModified`.
|
||||||
|
pub static STATUS_NOT_MODIFIED: LazyLock<SimpleError> =
|
||||||
|
LazyLock::new(|| SimpleError("304 Not Modified".to_string()));
|
||||||
|
|
||||||
|
/// A plain string-backed error, used for sentinel values such as
|
||||||
|
/// [`STATUS_NOT_MODIFIED`].
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct SimpleError(pub String);
|
||||||
|
|
||||||
|
impl fmt::Display for SimpleError {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
f.write_str(&self.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::error::Error for SimpleError {}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn new_err_code_maps_message() {
|
||||||
|
let e = CodeError::new_err_code(crate::error_code::INVALID_PARAMS);
|
||||||
|
assert_eq!(e.get_err_code(), 400);
|
||||||
|
assert_eq!(e.get_err_msg(), "Param Error");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn new_err_code_msg_is_explicit() {
|
||||||
|
let e = CodeError::new_err_code_msg(123, "custom");
|
||||||
|
assert_eq!(e.get_err_code(), 123);
|
||||||
|
assert_eq!(e.get_err_msg(), "custom");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn new_err_msg_uses_error_code() {
|
||||||
|
let e = CodeError::new_err_msg("boom");
|
||||||
|
assert_eq!(e.get_err_code(), ERROR);
|
||||||
|
assert_eq!(e.get_err_msg(), "boom");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn display_preserves_go_format() {
|
||||||
|
let e = CodeError::new_err_code_msg(400, "Param Error");
|
||||||
|
assert_eq!(e.to_string(), "ErrCode:400,ErrMsg:Param Error");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,230 @@
|
|||||||
|
// Error codes and their human-readable messages.
|
||||||
|
//
|
||||||
|
// Ported from the Go package `xerr` (errCode.go + errMsg.go). The first three
|
||||||
|
// digits identify the business area; the last three identify the specific
|
||||||
|
// error within that area.
|
||||||
|
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::sync::LazyLock;
|
||||||
|
|
||||||
|
/// General error codes.
|
||||||
|
pub const SUCCESS: u32 = 200;
|
||||||
|
pub const ERROR: u32 = 500;
|
||||||
|
|
||||||
|
/// Database errors.
|
||||||
|
pub const DATABASE_QUERY_ERROR: u32 = 10001;
|
||||||
|
pub const DATABASE_UPDATE_ERROR: u32 = 10002;
|
||||||
|
pub const DATABASE_INSERT_ERROR: u32 = 10003;
|
||||||
|
pub const DATABASE_DELETED_ERROR: u32 = 10004;
|
||||||
|
|
||||||
|
/// User errors.
|
||||||
|
pub const USER_EXIST: u32 = 20001;
|
||||||
|
pub const USER_NOT_EXIST: u32 = 20002;
|
||||||
|
pub const USER_PASSWORD_ERROR: u32 = 20003;
|
||||||
|
pub const USER_DISABLED: u32 = 20004;
|
||||||
|
pub const INSUFFICIENT_BALANCE: u32 = 20005;
|
||||||
|
pub const STOP_REGISTER: u32 = 20006;
|
||||||
|
pub const TELEGRAM_NOT_BOUND: u32 = 20007;
|
||||||
|
pub const USER_NOT_BIND_OAUTH: u32 = 20008;
|
||||||
|
pub const INVITE_CODE_ERROR: u32 = 20009;
|
||||||
|
pub const USER_COMMISSION_NOT_ENOUGH: u32 = 20010;
|
||||||
|
|
||||||
|
/// Node errors.
|
||||||
|
pub const NODE_EXIST: u32 = 30001;
|
||||||
|
pub const NODE_NOT_EXIST: u32 = 30002;
|
||||||
|
pub const NODE_GROUP_EXIST: u32 = 30003;
|
||||||
|
pub const NODE_GROUP_NOT_EXIST: u32 = 30004;
|
||||||
|
pub const NODE_GROUP_NOT_EMPTY: u32 = 30005;
|
||||||
|
|
||||||
|
/// Request errors.
|
||||||
|
pub const INVALID_PARAMS: u32 = 400;
|
||||||
|
pub const TOO_MANY_REQUESTS: u32 = 401;
|
||||||
|
pub const ERROR_TOKEN_EMPTY: u32 = 40002;
|
||||||
|
pub const ERROR_TOKEN_INVALID: u32 = 40003;
|
||||||
|
pub const ERROR_TOKEN_EXPIRE: u32 = 40004;
|
||||||
|
pub const INVALID_ACCESS: u32 = 40005;
|
||||||
|
pub const INVALID_CIPHERTEXT: u32 = 40006;
|
||||||
|
pub const SECRET_IS_EMPTY: u32 = 40007;
|
||||||
|
|
||||||
|
/// Coupon errors.
|
||||||
|
pub const COUPON_NOT_EXIST: u32 = 50001;
|
||||||
|
pub const COUPON_ALREADY_USED: u32 = 50002;
|
||||||
|
pub const COUPON_NOT_APPLICABLE: u32 = 50003;
|
||||||
|
pub const COUPON_INSUFFICIENT_USAGE: u32 = 50004;
|
||||||
|
pub const COUPON_EXPIRED: u32 = 50005;
|
||||||
|
pub const COUPON_DISABLED: u32 = 50006;
|
||||||
|
|
||||||
|
/// Subscribe errors.
|
||||||
|
pub const SUBSCRIBE_EXPIRED: u32 = 60001;
|
||||||
|
pub const SUBSCRIBE_NOT_AVAILABLE: u32 = 60002;
|
||||||
|
pub const USER_SUBSCRIBE_EXIST: u32 = 60003;
|
||||||
|
pub const SUBSCRIBE_IS_USED_ERROR: u32 = 60004;
|
||||||
|
pub const SINGLE_SUBSCRIBE_MODE_EXCEEDS_LIMIT: u32 = 60005;
|
||||||
|
pub const SUBSCRIBE_QUOTA_LIMIT: u32 = 60006;
|
||||||
|
pub const SUBSCRIBE_OUT_OF_STOCK: u32 = 60007;
|
||||||
|
|
||||||
|
/// Order errors.
|
||||||
|
pub const ORDER_NOT_EXIST: u32 = 61001;
|
||||||
|
pub const PAYMENT_METHOD_NOT_FOUND: u32 = 61002;
|
||||||
|
pub const ORDER_STATUS_ERROR: u32 = 61003;
|
||||||
|
pub const INSUFFICIENT_OF_PERIOD: u32 = 61004;
|
||||||
|
pub const EXIST_AVAILABLE_TRAFFIC: u32 = 61005;
|
||||||
|
|
||||||
|
/// Auth errors.
|
||||||
|
pub const VERIFY_CODE_ERROR: u32 = 70001;
|
||||||
|
|
||||||
|
/// Equipment errors.
|
||||||
|
pub const QUEUE_ENQUEUE_ERROR: u32 = 80001;
|
||||||
|
|
||||||
|
/// System errors.
|
||||||
|
pub const DEBUG_MODE_ERROR: u32 = 90001;
|
||||||
|
pub const SEND_SMS_ERROR: u32 = 90002;
|
||||||
|
pub const SMS_NOT_ENABLED: u32 = 90003;
|
||||||
|
pub const EMAIL_NOT_ENABLED: u32 = 90004;
|
||||||
|
pub const GET_AUTHENTICATOR_ERROR: u32 = 90005;
|
||||||
|
pub const AUTHENTICATOR_NOT_SUPPORTED_ERROR: u32 = 90006;
|
||||||
|
pub const TELEPHONE_AREA_CODE_IS_EMPTY: u32 = 90007;
|
||||||
|
pub const TODAY_SEND_COUNT_EXCEEDS_LIMIT: u32 = 90015;
|
||||||
|
pub const PASSWORD_IS_EMPTY: u32 = 90008;
|
||||||
|
pub const AREA_CODE_IS_EMPTY: u32 = 90009;
|
||||||
|
pub const PASSWORD_OR_VERIFICATION_CODE_REQUIRED: u32 = 90010;
|
||||||
|
pub const EMAIL_EXIST: u32 = 90011;
|
||||||
|
pub const TELEPHONE_EXIST: u32 = 90012;
|
||||||
|
pub const DEVICE_EXIST: u32 = 90013;
|
||||||
|
pub const TELEPHONE_ERROR: u32 = 90014;
|
||||||
|
pub const DEVICE_NOT_EXIST: u32 = 90017;
|
||||||
|
pub const USERID_NOT_MATCH: u32 = 90018;
|
||||||
|
|
||||||
|
/// Mapping of error code -> default message.
|
||||||
|
static MESSAGES: LazyLock<HashMap<u32, &'static str>> = LazyLock::new(|| {
|
||||||
|
let mut m = HashMap::new();
|
||||||
|
// General
|
||||||
|
m.insert(SUCCESS, "Success");
|
||||||
|
m.insert(ERROR, "Internal Server Error");
|
||||||
|
// Request / parameter
|
||||||
|
m.insert(TOO_MANY_REQUESTS, "Too Many Requests");
|
||||||
|
m.insert(INVALID_PARAMS, "Param Error");
|
||||||
|
m.insert(ERROR_TOKEN_EMPTY, "User token is empty");
|
||||||
|
m.insert(ERROR_TOKEN_INVALID, "User token is invalid");
|
||||||
|
m.insert(ERROR_TOKEN_EXPIRE, "User token is expired");
|
||||||
|
m.insert(SECRET_IS_EMPTY, "Secret is empty");
|
||||||
|
m.insert(INVALID_ACCESS, "Invalid access");
|
||||||
|
m.insert(INVALID_CIPHERTEXT, "Invalid ciphertext");
|
||||||
|
// Database
|
||||||
|
m.insert(DATABASE_QUERY_ERROR, "Database query error");
|
||||||
|
m.insert(DATABASE_UPDATE_ERROR, "Database update error");
|
||||||
|
m.insert(DATABASE_INSERT_ERROR, "Database insert error");
|
||||||
|
m.insert(DATABASE_DELETED_ERROR, "Database deleted error");
|
||||||
|
// User
|
||||||
|
m.insert(USER_EXIST, "User already exists");
|
||||||
|
m.insert(USER_NOT_EXIST, "User does not exist");
|
||||||
|
m.insert(USER_PASSWORD_ERROR, "User password error");
|
||||||
|
m.insert(USER_DISABLED, "User disabled");
|
||||||
|
m.insert(INSUFFICIENT_BALANCE, "Insufficient balance");
|
||||||
|
m.insert(STOP_REGISTER, "Stop register");
|
||||||
|
m.insert(TELEGRAM_NOT_BOUND, "Telegram not bound ");
|
||||||
|
m.insert(USER_NOT_BIND_OAUTH, "User not bind oauth method");
|
||||||
|
m.insert(INVITE_CODE_ERROR, "Invite code error");
|
||||||
|
// Node
|
||||||
|
m.insert(NODE_EXIST, "Node already exists");
|
||||||
|
m.insert(NODE_NOT_EXIST, "Node does not exist");
|
||||||
|
m.insert(NODE_GROUP_EXIST, "Node group already exists");
|
||||||
|
m.insert(NODE_GROUP_NOT_EXIST, "Node group does not exist");
|
||||||
|
m.insert(NODE_GROUP_NOT_EMPTY, "Node group is not empty");
|
||||||
|
// Coupon
|
||||||
|
m.insert(COUPON_NOT_EXIST, "Coupon does not exist");
|
||||||
|
m.insert(COUPON_ALREADY_USED, "Coupon has already been used");
|
||||||
|
m.insert(COUPON_NOT_APPLICABLE, "Coupon does not match the order or conditions");
|
||||||
|
m.insert(COUPON_INSUFFICIENT_USAGE, "Coupon has insufficient remaining uses");
|
||||||
|
m.insert(COUPON_EXPIRED, "Coupon is expired");
|
||||||
|
m.insert(COUPON_DISABLED, "Coupon is disabled");
|
||||||
|
// Subscribe
|
||||||
|
m.insert(SUBSCRIBE_EXPIRED, "Subscribe is expired");
|
||||||
|
m.insert(SUBSCRIBE_NOT_AVAILABLE, "Subscribe is not available");
|
||||||
|
m.insert(USER_SUBSCRIBE_EXIST, "User has subscription");
|
||||||
|
m.insert(SUBSCRIBE_IS_USED_ERROR, "Subscribe is used");
|
||||||
|
m.insert(
|
||||||
|
SINGLE_SUBSCRIBE_MODE_EXCEEDS_LIMIT,
|
||||||
|
"Single subscribe mode exceeds limit",
|
||||||
|
);
|
||||||
|
m.insert(SUBSCRIBE_QUOTA_LIMIT, "Subscribe quota limit");
|
||||||
|
m.insert(SUBSCRIBE_OUT_OF_STOCK, "Subscribe out of stock");
|
||||||
|
// Auth
|
||||||
|
m.insert(VERIFY_CODE_ERROR, "Verify code error");
|
||||||
|
// Equipment
|
||||||
|
m.insert(QUEUE_ENQUEUE_ERROR, " Queue enqueue error");
|
||||||
|
// System
|
||||||
|
m.insert(DEBUG_MODE_ERROR, "Debug mode is enabled");
|
||||||
|
m.insert(GET_AUTHENTICATOR_ERROR, "Unsupported login method");
|
||||||
|
m.insert(
|
||||||
|
AUTHENTICATOR_NOT_SUPPORTED_ERROR,
|
||||||
|
"The authenticator does not support this method",
|
||||||
|
);
|
||||||
|
m.insert(TELEPHONE_AREA_CODE_IS_EMPTY, "Telephone area code is empty");
|
||||||
|
m.insert(
|
||||||
|
TODAY_SEND_COUNT_EXCEEDS_LIMIT,
|
||||||
|
"This account has reached the limit of sending times today",
|
||||||
|
);
|
||||||
|
m.insert(SMS_NOT_ENABLED, "Telephone login is not enabled");
|
||||||
|
m.insert(EMAIL_NOT_ENABLED, "Email function is not enabled yet");
|
||||||
|
m.insert(
|
||||||
|
PASSWORD_OR_VERIFICATION_CODE_REQUIRED,
|
||||||
|
"Password or verification code required",
|
||||||
|
);
|
||||||
|
m.insert(EMAIL_EXIST, "Email already exists");
|
||||||
|
m.insert(TELEPHONE_EXIST, "Telephone already exists");
|
||||||
|
m.insert(DEVICE_EXIST, "device exists");
|
||||||
|
m.insert(PASSWORD_IS_EMPTY, "password is empty");
|
||||||
|
m.insert(TELEPHONE_ERROR, "telephone number error");
|
||||||
|
m.insert(DEVICE_NOT_EXIST, "Device does not exist");
|
||||||
|
m.insert(USERID_NOT_MATCH, "Userid not match");
|
||||||
|
// Order
|
||||||
|
m.insert(ORDER_NOT_EXIST, "Order does not exist");
|
||||||
|
m.insert(PAYMENT_METHOD_NOT_FOUND, "Payment method not found");
|
||||||
|
m.insert(ORDER_STATUS_ERROR, "Order status error");
|
||||||
|
m.insert(INSUFFICIENT_OF_PERIOD, "Insufficient number of period");
|
||||||
|
m
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Returns the default message for an error code.
|
||||||
|
///
|
||||||
|
/// Mirrors Go `xerr.MapErrMsg`: falls back to `"Internal Server Error"` when the
|
||||||
|
/// code is unknown.
|
||||||
|
pub fn map_err_msg(err_code: u32) -> &'static str {
|
||||||
|
MESSAGES
|
||||||
|
.get(&err_code)
|
||||||
|
.copied()
|
||||||
|
.unwrap_or("Internal Server Error")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns `true` when the code is a known, registered error code.
|
||||||
|
///
|
||||||
|
/// Mirrors Go `xerr.IsCodeErr`.
|
||||||
|
pub fn is_code_err(err_code: u32) -> bool {
|
||||||
|
MESSAGES.contains_key(&err_code)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn maps_known_codes() {
|
||||||
|
assert_eq!(map_err_msg(SUCCESS), "Success");
|
||||||
|
assert_eq!(map_err_msg(ERROR), "Internal Server Error");
|
||||||
|
assert_eq!(map_err_msg(INVALID_PARAMS), "Param Error");
|
||||||
|
assert_eq!(map_err_msg(ORDER_NOT_EXIST), "Order does not exist");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn unknown_code_falls_back() {
|
||||||
|
assert_eq!(map_err_msg(999_999), "Internal Server Error");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn is_code_err_recognizes_registered_codes() {
|
||||||
|
assert!(is_code_err(SUCCESS));
|
||||||
|
assert!(is_code_err(INVALID_PARAMS));
|
||||||
|
assert!(!is_code_err(999_999));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,236 @@
|
|||||||
|
// Response envelopes and HTTP result construction.
|
||||||
|
//
|
||||||
|
// Ported from the Go package `result` (responseBean.go + httpResult.go). The Go
|
||||||
|
// package wraps responses in `ResponseSuccessBean` / `ResponseErrorBean` and
|
||||||
|
// writes them through a Hertz `*Context`; in axum the equivalent is producing a
|
||||||
|
// type that implements `IntoResponse`, so those beans are exposed directly.
|
||||||
|
|
||||||
|
use axum::http::StatusCode;
|
||||||
|
use axum::response::{IntoResponse, Response};
|
||||||
|
use axum::Json;
|
||||||
|
use serde::Serialize;
|
||||||
|
use std::error::Error as StdError;
|
||||||
|
|
||||||
|
use crate::code_error::CodeError;
|
||||||
|
use crate::error_code;
|
||||||
|
|
||||||
|
/// Envelope returned for a successful request.
|
||||||
|
///
|
||||||
|
/// Mirrors Go `result.ResponseSuccessBean`. `data` is omitted from the JSON
|
||||||
|
/// payload when `None` (the `omitempty`-style `skip_serializing_if`).
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
pub struct ResponseSuccessBean<T = serde_json::Value> {
|
||||||
|
pub code: u32,
|
||||||
|
pub msg: String,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub data: Option<T>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> ResponseSuccessBean<T> {
|
||||||
|
pub fn new(data: Option<T>) -> Self {
|
||||||
|
Self {
|
||||||
|
code: 200,
|
||||||
|
msg: "success".to_string(),
|
||||||
|
data,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Marker for an empty (null) data payload, kept for parity with the Go
|
||||||
|
/// `NullJson` type.
|
||||||
|
pub struct NullJson;
|
||||||
|
|
||||||
|
/// Envelope returned for a failed request.
|
||||||
|
///
|
||||||
|
/// Mirrors Go `result.ResponseErrorBean`.
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
pub struct ResponseErrorBean {
|
||||||
|
pub code: u32,
|
||||||
|
pub msg: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Builds a success envelope wrapping `data`.
|
||||||
|
///
|
||||||
|
/// Mirrors Go `result.Success`.
|
||||||
|
pub fn success<T>(data: T) -> ResponseSuccessBean<T> {
|
||||||
|
ResponseSuccessBean::new(Some(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Builds an error envelope for the given code and message.
|
||||||
|
///
|
||||||
|
/// Mirrors Go `result.Error`.
|
||||||
|
pub fn error(err_code: u32, err_msg: impl Into<String>) -> ResponseErrorBean {
|
||||||
|
ResponseErrorBean {
|
||||||
|
code: err_code,
|
||||||
|
msg: err_msg.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A structured HTTP response, identical in intent to the Go `HTTPResult`:
|
||||||
|
/// an HTTP status code paired with the serialized body. Axum's `Response`
|
||||||
|
/// subsumes the original `(StatusCode, body)` pair.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct HttpResult {
|
||||||
|
pub status_code: StatusCode,
|
||||||
|
pub body: Response,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Constructs an `HttpResult` from a fallible response, normalizing the body
|
||||||
|
/// into a success or error envelope.
|
||||||
|
///
|
||||||
|
/// Mirrors Go `result.BuildHTTPResult`:
|
||||||
|
/// - On success: HTTP 200 with a `ResponseSuccessBean` body.
|
||||||
|
/// - On error: HTTP 200 with a `ResponseErrorBean` body, the code/message
|
||||||
|
/// recovered from any nested `CodeError`, defaulting to `ERROR` /
|
||||||
|
/// `"Internal Server Error"` for plain errors.
|
||||||
|
pub fn build_http_result<T>(resp: Option<T>, err: Option<anyhow::Error>) -> HttpResult
|
||||||
|
where
|
||||||
|
T: Serialize,
|
||||||
|
{
|
||||||
|
if let Some(err) = err {
|
||||||
|
let (code, msg) = recover_code_and_msg(&err);
|
||||||
|
return HttpResult {
|
||||||
|
status_code: StatusCode::OK,
|
||||||
|
body: error(code, msg).into_response_body(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
HttpResult {
|
||||||
|
status_code: StatusCode::OK,
|
||||||
|
body: Json(ResponseSuccessBean::new(resp)).into_response_body(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Constructs an `HttpResult` for a parameter-validation failure.
|
||||||
|
///
|
||||||
|
/// Mirrors Go `result.BuildParamErrorResult`: HTTP 200 with a `ResponseErrorBean`
|
||||||
|
/// whose code is `INVALID_PARAMS` and whose message is the raw error text.
|
||||||
|
pub fn build_param_error_result(err: &dyn StdError) -> HttpResult {
|
||||||
|
HttpResult {
|
||||||
|
status_code: StatusCode::OK,
|
||||||
|
body: error(error_code::INVALID_PARAMS, err.to_string()).into_response_body(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Emits an HTTP response built from a fallible result.
|
||||||
|
///
|
||||||
|
/// Mirrors Go `result.HttpResult(ctx, resp, err)` (which wrote the body via the
|
||||||
|
/// Hertz context). Here it simply renders the constructed `HttpResult`.
|
||||||
|
pub fn http_result<T>(resp: Option<T>, err: Option<anyhow::Error>) -> Response
|
||||||
|
where
|
||||||
|
T: Serialize,
|
||||||
|
{
|
||||||
|
build_http_result(resp, err).into_response()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Emits an HTTP response for a parameter-validation failure.
|
||||||
|
///
|
||||||
|
/// Mirrors Go `result.ParamErrorResult(ctx, err)`. The Go version also logged the
|
||||||
|
/// error onto the Hertz error chain (`ctx.Error`); in axum that side effect is
|
||||||
|
/// the caller's responsibility (e.g. a tracing call), so only the response is
|
||||||
|
/// produced here.
|
||||||
|
pub fn param_error_result(err: &dyn StdError) -> Response {
|
||||||
|
build_param_error_result(err).into_response()
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntoResponse for HttpResult {
|
||||||
|
fn into_response(self) -> Response {
|
||||||
|
self.body
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- helpers --------------------------------------------------------------
|
||||||
|
|
||||||
|
/// Re-implementation of Go's `errors.As(errors.Cause(err), &e)` chain walk: the
|
||||||
|
/// first `CodeError` found while unwrapping `anyhow::Error` wins.
|
||||||
|
fn recover_code_and_msg(err: &anyhow::Error) -> (u32, String) {
|
||||||
|
for cause in err.chain() {
|
||||||
|
if let Some(code_err) = cause.downcast_ref::<CodeError>() {
|
||||||
|
return (code_err.get_err_code(), code_err.get_err_msg().to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(error_code::ERROR, "Internal Server Error".to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extension so the `(StatusCode, Json<T>)` rendering can be captured as the
|
||||||
|
/// inner `Response` body of an `HttpResult`.
|
||||||
|
trait IntoResponseBody {
|
||||||
|
fn into_response_body(self) -> Response;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Serialize> IntoResponseBody for Json<T> {
|
||||||
|
fn into_response_body(self) -> Response {
|
||||||
|
(StatusCode::OK, self).into_response()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntoResponseBody for ResponseErrorBean {
|
||||||
|
fn into_response_body(self) -> Response {
|
||||||
|
(StatusCode::OK, Json(self)).into_response()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use axum::body::to_bytes;
|
||||||
|
use crate::code_error::CodeError;
|
||||||
|
use crate::error_code;
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn build_http_result_success() {
|
||||||
|
let result = build_http_result(Some("ok"), None);
|
||||||
|
assert_eq!(result.status_code, StatusCode::OK);
|
||||||
|
let bytes = to_bytes(result.body.into_body(), usize::MAX).await.unwrap();
|
||||||
|
let v: serde_json::Value = serde_json::from_slice(&bytes).unwrap();
|
||||||
|
assert_eq!(v["code"], 200);
|
||||||
|
assert_eq!(v["msg"], "success");
|
||||||
|
assert_eq!(v["data"], "ok");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn build_http_result_code_error() {
|
||||||
|
let err = anyhow::Error::new(CodeError::new_err_code(error_code::INVALID_PARAMS));
|
||||||
|
let result = build_http_result::<()>(None, Some(err));
|
||||||
|
let bytes = to_bytes(result.body.into_body(), usize::MAX).await.unwrap();
|
||||||
|
let v: serde_json::Value = serde_json::from_slice(&bytes).unwrap();
|
||||||
|
assert_eq!(v["code"], 400);
|
||||||
|
assert_eq!(v["msg"], "Param Error");
|
||||||
|
assert!(v.get("data").is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn build_http_result_generic_error() {
|
||||||
|
let err = anyhow::Error::msg("boom");
|
||||||
|
let result = build_http_result::<()>(None, Some(err));
|
||||||
|
let bytes = to_bytes(result.body.into_body(), usize::MAX).await.unwrap();
|
||||||
|
let v: serde_json::Value = serde_json::from_slice(&bytes).unwrap();
|
||||||
|
assert_eq!(v["code"], 500);
|
||||||
|
assert_eq!(v["msg"], "Internal Server Error");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn build_param_error_result_works() {
|
||||||
|
let err = anyhow::Error::msg("bad param");
|
||||||
|
let result = build_param_error_result(err.as_ref());
|
||||||
|
let bytes = to_bytes(result.body.into_body(), usize::MAX).await.unwrap();
|
||||||
|
let v: serde_json::Value = serde_json::from_slice(&bytes).unwrap();
|
||||||
|
assert_eq!(v["code"], 400);
|
||||||
|
assert_eq!(v["msg"], "bad param");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn success_envelope_shape() {
|
||||||
|
let bean = success(serde_json::json!({"x": 1}));
|
||||||
|
assert_eq!(bean.code, 200);
|
||||||
|
assert_eq!(bean.msg, "success");
|
||||||
|
assert_eq!(bean.data.as_ref().unwrap()["x"], 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn error_envelope_shape() {
|
||||||
|
let bean = error(401, "Too Many Requests");
|
||||||
|
assert_eq!(bean.code, 401);
|
||||||
|
assert_eq!(bean.msg, "Too Many Requests");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
// Response envelopes and HTTP result construction.
|
||||||
|
//
|
||||||
|
// Ported from the Go package `result` together with its `xerr` dependency:
|
||||||
|
// error codes live in [`error_code`], the code-carrying error in
|
||||||
|
// [`code_error`], and the response beans / `HttpResult` in [`http_result`].
|
||||||
|
|
||||||
|
pub mod code_error;
|
||||||
|
pub mod error_code;
|
||||||
|
pub mod http_result;
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
[package]
|
||||||
|
name = "sms"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
async-trait = "0.1"
|
||||||
|
anyhow = "1"
|
||||||
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
serde_json = "1"
|
||||||
|
base64 = "0.22"
|
||||||
|
tokio = { version = "1", features = ["full"] }
|
||||||
|
reqwest = { version = "0.12", features = ["json"] }
|
||||||
|
hmac = "0.12"
|
||||||
|
sha1 = "0.10"
|
||||||
|
sha2 = "0.10"
|
||||||
|
md5 = "0.7"
|
||||||
|
chrono = "0.4"
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
/// Unified config struct covering all 4 providers.
|
||||||
|
/// Fields not used by a given provider are simply ignored.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||||
|
pub struct SmsConfig {
|
||||||
|
// ── AlibabaCloud ────────────────────────────────────────────────
|
||||||
|
/// AccessKeyId
|
||||||
|
#[serde(default)]
|
||||||
|
pub access: String,
|
||||||
|
/// AccessKeySecret / MD5Key / AuthToken / Password
|
||||||
|
#[serde(default)]
|
||||||
|
pub secret: String,
|
||||||
|
/// AlibabaCloud: SignName
|
||||||
|
#[serde(default)]
|
||||||
|
pub sign_name: String,
|
||||||
|
/// AlibabaCloud: Endpoint (default: dysmsapi.ap-southeast-1.aliyuncs.com)
|
||||||
|
#[serde(default)]
|
||||||
|
pub endpoint: String,
|
||||||
|
/// AlibabaCloud: TemplateCode
|
||||||
|
#[serde(default)]
|
||||||
|
pub template_code: String,
|
||||||
|
|
||||||
|
// ── Smsbao / Abosend / Twilio ────────────────────────────────────
|
||||||
|
/// Go template string, e.g. "Your code is {{.code}}"
|
||||||
|
#[serde(default)]
|
||||||
|
pub template: String,
|
||||||
|
|
||||||
|
// ── Abosend ─────────────────────────────────────────────────────
|
||||||
|
/// Override API base URL (default: https://smsapi.abosend.com)
|
||||||
|
#[serde(default)]
|
||||||
|
pub api_domain: String,
|
||||||
|
|
||||||
|
// ── Twilio ───────────────────────────────────────────────────────
|
||||||
|
/// Sending phone number (e.g. "+12015551234")
|
||||||
|
#[serde(default)]
|
||||||
|
pub phone_number: String,
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
use crate::config::SmsConfig;
|
||||||
|
use crate::platform::Platform;
|
||||||
|
use crate::providers::{abosend::AbosendSender, alibabacloud::AlibabaCloudSender, smsbao::SmsbaoSender, twilio::TwilioSender};
|
||||||
|
use crate::sender::Sender;
|
||||||
|
|
||||||
|
pub fn create_sender(platform: Platform, config: SmsConfig) -> Box<dyn Sender> {
|
||||||
|
match platform {
|
||||||
|
Platform::AlibabaCloud => Box::new(AlibabaCloudSender::new(config)),
|
||||||
|
Platform::Smsbao => Box::new(SmsbaoSender::new(config)),
|
||||||
|
Platform::Abosend => Box::new(AbosendSender::new(config)),
|
||||||
|
Platform::Twilio => Box::new(TwilioSender::new(config)),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
pub mod config;
|
||||||
|
pub mod factory;
|
||||||
|
pub mod platform;
|
||||||
|
pub mod providers;
|
||||||
|
pub mod sender;
|
||||||
|
|
||||||
|
pub use config::SmsConfig;
|
||||||
|
pub use factory::create_sender;
|
||||||
|
pub use platform::Platform;
|
||||||
|
pub use sender::Sender;
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub enum Platform {
|
||||||
|
AlibabaCloud,
|
||||||
|
Smsbao,
|
||||||
|
Abosend,
|
||||||
|
Twilio,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Platform {
|
||||||
|
pub fn from_str(s: &str) -> Option<Self> {
|
||||||
|
match s {
|
||||||
|
"AlibabaCloud" => Some(Platform::AlibabaCloud),
|
||||||
|
"smsbao" => Some(Platform::Smsbao),
|
||||||
|
"abosend" => Some(Platform::Abosend),
|
||||||
|
"twilio" => Some(Platform::Twilio),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for Platform {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
let s = match self {
|
||||||
|
Platform::AlibabaCloud => "AlibabaCloud",
|
||||||
|
Platform::Smsbao => "smsbao",
|
||||||
|
Platform::Abosend => "abosend",
|
||||||
|
Platform::Twilio => "twilio",
|
||||||
|
};
|
||||||
|
write!(f, "{}", s)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
use anyhow::Context;
|
||||||
|
use reqwest::Client;
|
||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
use crate::config::SmsConfig;
|
||||||
|
use crate::sender::Sender;
|
||||||
|
|
||||||
|
const BASE_URL: &str = "https://smsapi.abosend.com";
|
||||||
|
|
||||||
|
pub struct AbosendSender {
|
||||||
|
config: SmsConfig,
|
||||||
|
client: Client,
|
||||||
|
base_url: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AbosendSender {
|
||||||
|
pub fn new(config: SmsConfig) -> Self {
|
||||||
|
let base_url = if config.api_domain.is_empty() {
|
||||||
|
BASE_URL.to_string()
|
||||||
|
} else {
|
||||||
|
config.api_domain.clone()
|
||||||
|
};
|
||||||
|
Self {
|
||||||
|
config,
|
||||||
|
client: Client::new(),
|
||||||
|
base_url,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct AbosendResponse {
|
||||||
|
code: i32,
|
||||||
|
message: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Render a Go-style template string: replace `{{.code}}` with `code`.
|
||||||
|
fn render_template(template: &str, code: &str) -> String {
|
||||||
|
template.replace("{{.code}}", code)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Compute MD5 hex string (lowercase).
|
||||||
|
fn md5_hex(input: &str) -> String {
|
||||||
|
let digest = md5::compute(input.as_bytes());
|
||||||
|
format!("{:x}", digest)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl Sender for AbosendSender {
|
||||||
|
async fn send(&self, area: &str, phone: &str, code: &str, _expire: u32) -> anyhow::Result<()> {
|
||||||
|
let content = render_template(&self.config.template, code);
|
||||||
|
|
||||||
|
// rand is a 6-digit numeric string; we derive it from current time nanos
|
||||||
|
let rand_num = format!(
|
||||||
|
"{:06}",
|
||||||
|
std::time::SystemTime::now()
|
||||||
|
.duration_since(std::time::UNIX_EPOCH)
|
||||||
|
.unwrap_or_default()
|
||||||
|
.subsec_nanos()
|
||||||
|
% 1_000_000
|
||||||
|
);
|
||||||
|
|
||||||
|
// sign = md5(orgCode + content + rand + md5key), uppercase
|
||||||
|
let sign_input = format!(
|
||||||
|
"{}{}{}{}",
|
||||||
|
self.config.access, content, rand_num, self.config.secret
|
||||||
|
);
|
||||||
|
let sign = md5_hex(&sign_input).to_uppercase();
|
||||||
|
|
||||||
|
let body = serde_json::json!({
|
||||||
|
"orgCode": self.config.access,
|
||||||
|
"mobileArea": format!("+{}", area),
|
||||||
|
"mobiles": format!("{}{}", area, phone),
|
||||||
|
"content": content,
|
||||||
|
"rand": rand_num,
|
||||||
|
"sign": sign,
|
||||||
|
});
|
||||||
|
|
||||||
|
let url = format!("{}/v2/api/sendSMS", self.base_url);
|
||||||
|
let resp = self
|
||||||
|
.client
|
||||||
|
.post(&url)
|
||||||
|
.json(&body)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.context("Abosend: HTTP request failed")?;
|
||||||
|
|
||||||
|
let status = resp.status();
|
||||||
|
if !status.is_success() {
|
||||||
|
anyhow::bail!("Abosend: HTTP {}", status);
|
||||||
|
}
|
||||||
|
|
||||||
|
let result: AbosendResponse = resp
|
||||||
|
.json()
|
||||||
|
.await
|
||||||
|
.context("Abosend: failed to parse response")?;
|
||||||
|
|
||||||
|
if result.code != 200 {
|
||||||
|
anyhow::bail!(
|
||||||
|
"Abosend: send failed, code={}, message={}",
|
||||||
|
result.code,
|
||||||
|
result.message
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,179 @@
|
|||||||
|
use anyhow::Context;
|
||||||
|
use reqwest::Client;
|
||||||
|
use serde::Deserialize;
|
||||||
|
use serde_json::json;
|
||||||
|
|
||||||
|
use crate::config::SmsConfig;
|
||||||
|
use crate::sender::Sender;
|
||||||
|
|
||||||
|
const DEFAULT_ENDPOINT: &str = "dysmsapi.ap-southeast-1.aliyuncs.com";
|
||||||
|
|
||||||
|
pub struct AlibabaCloudSender {
|
||||||
|
config: SmsConfig,
|
||||||
|
client: Client,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AlibabaCloudSender {
|
||||||
|
pub fn new(config: SmsConfig) -> Self {
|
||||||
|
Self {
|
||||||
|
config,
|
||||||
|
client: Client::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Minimal response body from Dysmsapi SendSms
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct SendSmsResponse {
|
||||||
|
#[serde(rename = "Code")]
|
||||||
|
code: String,
|
||||||
|
#[serde(rename = "Message")]
|
||||||
|
message: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build the canonical query string and HMAC-SHA1 signature required by
|
||||||
|
/// Alibaba Cloud's Dysmsapi (RPC-style, API version 2017-05-25).
|
||||||
|
///
|
||||||
|
/// Reference:
|
||||||
|
/// https://help.aliyun.com/document_detail/101341.html
|
||||||
|
fn sign_request(
|
||||||
|
access_key_id: &str,
|
||||||
|
access_key_secret: &str,
|
||||||
|
params: &mut Vec<(String, String)>,
|
||||||
|
) -> anyhow::Result<String> {
|
||||||
|
use base64::{engine::general_purpose::STANDARD, Engine};
|
||||||
|
use hmac::{Hmac, Mac};
|
||||||
|
use sha1::Sha1;
|
||||||
|
#[allow(unused_imports)]
|
||||||
|
use sha2::Sha256;
|
||||||
|
|
||||||
|
// Common system parameters
|
||||||
|
let nonce = uuid_v4_simple();
|
||||||
|
let timestamp = chrono::Utc::now().format("%Y-%m-%dT%H:%M:%SZ").to_string();
|
||||||
|
|
||||||
|
params.push(("AccessKeyId".to_string(), access_key_id.to_string()));
|
||||||
|
params.push(("Format".to_string(), "JSON".to_string()));
|
||||||
|
params.push(("SignatureMethod".to_string(), "HMAC-SHA1".to_string()));
|
||||||
|
params.push(("SignatureNonce".to_string(), nonce));
|
||||||
|
params.push(("SignatureVersion".to_string(), "1.0".to_string()));
|
||||||
|
params.push(("Timestamp".to_string(), timestamp));
|
||||||
|
params.push(("Version".to_string(), "2017-05-25".to_string()));
|
||||||
|
|
||||||
|
// Sort by key
|
||||||
|
params.sort_by(|a, b| a.0.cmp(&b.0));
|
||||||
|
|
||||||
|
// Percent-encode each key=value pair then join with &
|
||||||
|
let canonical = params
|
||||||
|
.iter()
|
||||||
|
.map(|(k, v)| {
|
||||||
|
format!(
|
||||||
|
"{}={}",
|
||||||
|
percent_encode(k),
|
||||||
|
percent_encode(v)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join("&");
|
||||||
|
|
||||||
|
let string_to_sign = format!(
|
||||||
|
"GET&{}&{}",
|
||||||
|
percent_encode("/"),
|
||||||
|
percent_encode(&canonical)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Sign with HMAC-SHA1 using "<secret>&" as key
|
||||||
|
let signing_key = format!("{}&", access_key_secret);
|
||||||
|
let mut mac = Hmac::<Sha1>::new_from_slice(signing_key.as_bytes())
|
||||||
|
.context("HMAC-SHA1 init failed")?;
|
||||||
|
mac.update(string_to_sign.as_bytes());
|
||||||
|
let signature = STANDARD.encode(mac.finalize().into_bytes());
|
||||||
|
|
||||||
|
params.push(("Signature".to_string(), signature.clone()));
|
||||||
|
|
||||||
|
// Rebuild final query
|
||||||
|
let query = params
|
||||||
|
.iter()
|
||||||
|
.map(|(k, v)| format!("{}={}", percent_encode(k), percent_encode(v)))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join("&");
|
||||||
|
|
||||||
|
Ok(query)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn percent_encode(s: &str) -> String {
|
||||||
|
let mut encoded = String::new();
|
||||||
|
for b in s.bytes() {
|
||||||
|
match b {
|
||||||
|
b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9'
|
||||||
|
| b'-' | b'_' | b'.' | b'~' => {
|
||||||
|
encoded.push(b as char);
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
encoded.push_str(&format!("%{:02X}", b));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
encoded
|
||||||
|
}
|
||||||
|
|
||||||
|
fn uuid_v4_simple() -> String {
|
||||||
|
// Generate a UUID-like random string without external dep
|
||||||
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
|
let t = SystemTime::now()
|
||||||
|
.duration_since(UNIX_EPOCH)
|
||||||
|
.unwrap_or_default()
|
||||||
|
.subsec_nanos();
|
||||||
|
format!("{:08x}-{:04x}-4{:03x}", t, t >> 16, t & 0xfff)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl Sender for AlibabaCloudSender {
|
||||||
|
async fn send(&self, area: &str, phone: &str, code: &str, _expire: u32) -> anyhow::Result<()> {
|
||||||
|
let endpoint = if self.config.endpoint.is_empty() {
|
||||||
|
DEFAULT_ENDPOINT
|
||||||
|
} else {
|
||||||
|
&self.config.endpoint
|
||||||
|
};
|
||||||
|
|
||||||
|
let template_param = json!({ "code": code }).to_string();
|
||||||
|
let phone_number = format!("{}{}", area, phone);
|
||||||
|
|
||||||
|
let mut params: Vec<(String, String)> = vec![
|
||||||
|
("Action".to_string(), "SendSms".to_string()),
|
||||||
|
("PhoneNumbers".to_string(), phone_number),
|
||||||
|
("SignName".to_string(), self.config.sign_name.clone()),
|
||||||
|
("TemplateCode".to_string(), self.config.template_code.clone()),
|
||||||
|
("TemplateParam".to_string(), template_param),
|
||||||
|
];
|
||||||
|
|
||||||
|
let query = sign_request(&self.config.access, &self.config.secret, &mut params)?;
|
||||||
|
let url = format!("https://{}/{}", endpoint, query);
|
||||||
|
|
||||||
|
let resp = self
|
||||||
|
.client
|
||||||
|
.get(&url)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.context("AlibabaCloud: HTTP request failed")?;
|
||||||
|
|
||||||
|
let status = resp.status();
|
||||||
|
let body = resp.text().await.context("AlibabaCloud: failed to read body")?;
|
||||||
|
|
||||||
|
if !status.is_success() {
|
||||||
|
anyhow::bail!("AlibabaCloud: HTTP {} — {}", status, body);
|
||||||
|
}
|
||||||
|
|
||||||
|
let result: SendSmsResponse =
|
||||||
|
serde_json::from_str(&body).context("AlibabaCloud: failed to parse response")?;
|
||||||
|
|
||||||
|
if result.code != "OK" {
|
||||||
|
anyhow::bail!(
|
||||||
|
"AlibabaCloud: SendSms failed, code={}, message={}",
|
||||||
|
result.code,
|
||||||
|
result.message
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
pub mod alibabacloud;
|
||||||
|
pub mod abosend;
|
||||||
|
pub mod smsbao;
|
||||||
|
pub mod twilio;
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
use anyhow::Context;
|
||||||
|
use reqwest::Client;
|
||||||
|
|
||||||
|
use crate::config::SmsConfig;
|
||||||
|
use crate::sender::Sender;
|
||||||
|
|
||||||
|
const BASE_URL: &str = "https://api.smsbao.com";
|
||||||
|
|
||||||
|
pub struct SmsbaoSender {
|
||||||
|
config: SmsConfig,
|
||||||
|
client: Client,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SmsbaoSender {
|
||||||
|
pub fn new(config: SmsConfig) -> Self {
|
||||||
|
Self {
|
||||||
|
config,
|
||||||
|
client: Client::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Render a Go-style template: replace `{{.code}}` with `code`.
|
||||||
|
fn render_template(template: &str, code: &str) -> String {
|
||||||
|
template.replace("{{.code}}", code)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// MD5 hex (lowercase).
|
||||||
|
fn md5_hex(input: &str) -> String {
|
||||||
|
let digest = md5::compute(input.as_bytes());
|
||||||
|
format!("{:x}", digest)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Map SMSBao numeric response body to an error description.
|
||||||
|
fn parse_smsbao_error(body: &str) -> anyhow::Result<()> {
|
||||||
|
match body.trim() {
|
||||||
|
"0" => Ok(()),
|
||||||
|
"30" => anyhow::bail!("SMSBao: password error"),
|
||||||
|
"40" => anyhow::bail!("SMSBao: account not found"),
|
||||||
|
"41" => anyhow::bail!("SMSBao: insufficient balance"),
|
||||||
|
"43" => anyhow::bail!("SMSBao: IP address restrictions"),
|
||||||
|
"50" => anyhow::bail!("SMSBao: content contains sensitive words"),
|
||||||
|
"51" => anyhow::bail!("SMSBao: mobile number is incorrect"),
|
||||||
|
other => anyhow::bail!("SMSBao: unknown error code: {}", other),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl Sender for SmsbaoSender {
|
||||||
|
async fn send(&self, area: &str, phone: &str, code: &str, _expire: u32) -> anyhow::Result<()> {
|
||||||
|
let content = render_template(&self.config.template, code);
|
||||||
|
let password_md5 = md5_hex(&self.config.secret);
|
||||||
|
|
||||||
|
// Domestic (China, area == "86") → /sms, just mobile number
|
||||||
|
// International → /wsms, prepend +area to number
|
||||||
|
let (api_path, mobile) = if area == "86" {
|
||||||
|
("/sms", phone.to_string())
|
||||||
|
} else {
|
||||||
|
("/wsms", format!("+{}{}", area, phone))
|
||||||
|
};
|
||||||
|
|
||||||
|
let url = format!("{}{}", BASE_URL, api_path);
|
||||||
|
let resp = self
|
||||||
|
.client
|
||||||
|
.get(&url)
|
||||||
|
.query(&[
|
||||||
|
("u", self.config.access.as_str()),
|
||||||
|
("p", &password_md5),
|
||||||
|
("m", &mobile),
|
||||||
|
("c", &content),
|
||||||
|
])
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.context("SMSBao: HTTP request failed")?;
|
||||||
|
|
||||||
|
let status = resp.status();
|
||||||
|
let body = resp.text().await.context("SMSBao: failed to read body")?;
|
||||||
|
|
||||||
|
if !status.is_success() {
|
||||||
|
anyhow::bail!("SMSBao: HTTP {} — {}", status, body);
|
||||||
|
}
|
||||||
|
|
||||||
|
parse_smsbao_error(&body)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
use anyhow::Context;
|
||||||
|
use base64::{engine::general_purpose::STANDARD, Engine};
|
||||||
|
use reqwest::Client;
|
||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
use crate::config::SmsConfig;
|
||||||
|
use crate::sender::Sender;
|
||||||
|
|
||||||
|
pub struct TwilioSender {
|
||||||
|
config: SmsConfig,
|
||||||
|
client: Client,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TwilioSender {
|
||||||
|
pub fn new(config: SmsConfig) -> Self {
|
||||||
|
Self {
|
||||||
|
config,
|
||||||
|
client: Client::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Render Go-style template: replace `{{.code}}` with `code`.
|
||||||
|
fn render_template(template: &str, code: &str) -> String {
|
||||||
|
template.replace("{{.code}}", code)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Twilio Messages API response (partial — only error fields matter)
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct TwilioMessageResponse {
|
||||||
|
error_code: Option<i32>,
|
||||||
|
error_message: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl Sender for TwilioSender {
|
||||||
|
async fn send(&self, area: &str, phone: &str, code: &str, _expire: u32) -> anyhow::Result<()> {
|
||||||
|
let to = format!("+{}{}", area, phone);
|
||||||
|
let body_text = render_template(&self.config.template, code);
|
||||||
|
|
||||||
|
// Twilio REST API: POST /2010-04-01/Accounts/{AccountSid}/Messages.json
|
||||||
|
// Auth: HTTP Basic (AccountSid : AuthToken)
|
||||||
|
let account_sid = &self.config.access;
|
||||||
|
let auth_token = &self.config.secret;
|
||||||
|
let url = format!(
|
||||||
|
"https://api.twilio.com/2010-04-01/Accounts/{}/Messages.json",
|
||||||
|
account_sid
|
||||||
|
);
|
||||||
|
|
||||||
|
let credentials = STANDARD.encode(format!("{}:{}", account_sid, auth_token));
|
||||||
|
|
||||||
|
let resp = self
|
||||||
|
.client
|
||||||
|
.post(&url)
|
||||||
|
.header("Authorization", format!("Basic {}", credentials))
|
||||||
|
.form(&[
|
||||||
|
("To", to.as_str()),
|
||||||
|
("From", self.config.phone_number.as_str()),
|
||||||
|
("Body", body_text.as_str()),
|
||||||
|
])
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.context("Twilio: HTTP request failed")?;
|
||||||
|
|
||||||
|
let status = resp.status();
|
||||||
|
let raw = resp.text().await.context("Twilio: failed to read body")?;
|
||||||
|
|
||||||
|
if !status.is_success() {
|
||||||
|
anyhow::bail!("Twilio: HTTP {} — {}", status, raw);
|
||||||
|
}
|
||||||
|
|
||||||
|
let result: TwilioMessageResponse =
|
||||||
|
serde_json::from_str(&raw).context("Twilio: failed to parse response")?;
|
||||||
|
|
||||||
|
if let Some(err_code) = result.error_code {
|
||||||
|
let msg = result.error_message.unwrap_or_default();
|
||||||
|
anyhow::bail!("Twilio: send failed, error_code={}, message={}", err_code, msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
#[async_trait::async_trait]
|
||||||
|
pub trait Sender: Send + Sync {
|
||||||
|
async fn send(&self, area: &str, phone: &str, code: &str, expire: u32) -> anyhow::Result<()>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,594 @@
|
|||||||
|
-- migrate:up
|
||||||
|
-- 000001_init_schema.up.sql
|
||||||
|
SET FOREIGN_KEY_CHECKS = 0;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `ads`
|
||||||
|
(
|
||||||
|
`id` bigint NOT NULL AUTO_INCREMENT,
|
||||||
|
`title` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Ads title',
|
||||||
|
`type` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Ads type',
|
||||||
|
`content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci COMMENT 'Ads content',
|
||||||
|
`target_url` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT '' COMMENT 'Ads target url',
|
||||||
|
`start_time` datetime DEFAULT NULL COMMENT 'Ads start time',
|
||||||
|
`end_time` datetime DEFAULT NULL COMMENT 'Ads end time',
|
||||||
|
`status` tinyint(1) DEFAULT '0' COMMENT 'Ads status,0 disable,1 enable',
|
||||||
|
`created_at` datetime(3) DEFAULT NULL COMMENT 'Create Time',
|
||||||
|
`updated_at` datetime(3) DEFAULT NULL COMMENT 'Update Time',
|
||||||
|
PRIMARY KEY (`id`)
|
||||||
|
) ENGINE = InnoDB
|
||||||
|
DEFAULT CHARSET = utf8mb4
|
||||||
|
COLLATE = utf8mb4_general_ci;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `announcement`
|
||||||
|
(
|
||||||
|
`id` bigint NOT NULL AUTO_INCREMENT,
|
||||||
|
`title` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Title',
|
||||||
|
`content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci COMMENT 'Content',
|
||||||
|
`show` tinyint(1) NOT NULL DEFAULT '0' COMMENT 'Show',
|
||||||
|
`pinned` tinyint(1) NOT NULL DEFAULT '0' COMMENT 'Pinned',
|
||||||
|
`popup` tinyint(1) NOT NULL DEFAULT '0' COMMENT 'Popup',
|
||||||
|
`created_at` datetime(3) DEFAULT NULL COMMENT 'Create Time',
|
||||||
|
`updated_at` datetime(3) DEFAULT NULL COMMENT 'Update Time',
|
||||||
|
PRIMARY KEY (`id`)
|
||||||
|
) ENGINE = InnoDB
|
||||||
|
DEFAULT CHARSET = utf8mb4
|
||||||
|
COLLATE = utf8mb4_general_ci;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `application`
|
||||||
|
(
|
||||||
|
`id` bigint NOT NULL AUTO_INCREMENT,
|
||||||
|
`name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '应用名称',
|
||||||
|
`icon` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '应用图标',
|
||||||
|
`description` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci COMMENT '更新描述',
|
||||||
|
`subscribe_type` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '订阅类型',
|
||||||
|
`created_at` datetime(3) DEFAULT NULL COMMENT '创建时间',
|
||||||
|
`updated_at` datetime(3) DEFAULT NULL COMMENT '更新时间',
|
||||||
|
PRIMARY KEY (`id`)
|
||||||
|
) ENGINE = InnoDB
|
||||||
|
DEFAULT CHARSET = utf8mb4
|
||||||
|
COLLATE = utf8mb4_general_ci;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `application_config`
|
||||||
|
(
|
||||||
|
`id` bigint NOT NULL AUTO_INCREMENT,
|
||||||
|
`app_id` bigint NOT NULL DEFAULT '0' COMMENT 'App id',
|
||||||
|
`encryption_key` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci COMMENT 'Encryption Key',
|
||||||
|
`encryption_method` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT 'Encryption Method',
|
||||||
|
`domains` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci,
|
||||||
|
`startup_picture` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci,
|
||||||
|
`startup_picture_skip_time` bigint NOT NULL DEFAULT '0' COMMENT 'Startup Picture Skip Time',
|
||||||
|
`created_at` datetime(3) DEFAULT NULL COMMENT 'Create Time',
|
||||||
|
`updated_at` datetime(3) DEFAULT NULL COMMENT 'Update Time',
|
||||||
|
PRIMARY KEY (`id`)
|
||||||
|
) ENGINE = InnoDB
|
||||||
|
DEFAULT CHARSET = utf8mb4
|
||||||
|
COLLATE = utf8mb4_general_ci;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `application_version`
|
||||||
|
(
|
||||||
|
`id` bigint NOT NULL AUTO_INCREMENT,
|
||||||
|
`url` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '应用地址',
|
||||||
|
`version` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '应用版本',
|
||||||
|
`platform` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '应用平台',
|
||||||
|
`is_default` tinyint(1) NOT NULL DEFAULT '0' COMMENT '默认版本',
|
||||||
|
`description` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci COMMENT '更新描述',
|
||||||
|
`application_id` bigint DEFAULT NULL COMMENT '所属应用',
|
||||||
|
`created_at` datetime(3) DEFAULT NULL COMMENT '创建时间',
|
||||||
|
`updated_at` datetime(3) DEFAULT NULL COMMENT '更新时间',
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
KEY `fk_application_application_versions` (`application_id`)
|
||||||
|
) ENGINE = InnoDB
|
||||||
|
DEFAULT CHARSET = utf8mb4
|
||||||
|
COLLATE = utf8mb4_general_ci;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `auth_method`
|
||||||
|
(
|
||||||
|
`id` bigint NOT NULL AUTO_INCREMENT,
|
||||||
|
`method` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'method',
|
||||||
|
`config` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT 'OAuth Configuration',
|
||||||
|
`enabled` tinyint(1) NOT NULL DEFAULT '0' COMMENT 'Is Enabled',
|
||||||
|
`created_at` datetime(3) DEFAULT NULL COMMENT 'Create Time',
|
||||||
|
`updated_at` datetime(3) DEFAULT NULL COMMENT 'Update Time',
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE KEY `uni_auth_method` (`method`)
|
||||||
|
) ENGINE = InnoDB
|
||||||
|
DEFAULT CHARSET = utf8mb4
|
||||||
|
COLLATE = utf8mb4_general_ci;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `coupon`
|
||||||
|
(
|
||||||
|
`id` bigint NOT NULL AUTO_INCREMENT,
|
||||||
|
`name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Coupon Name',
|
||||||
|
`code` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Coupon Code',
|
||||||
|
`count` bigint NOT NULL DEFAULT '0' COMMENT 'Count Limit',
|
||||||
|
`type` tinyint(1) NOT NULL DEFAULT '1' COMMENT 'Coupon Type: 1: Percentage 2: Fixed Amount',
|
||||||
|
`discount` bigint NOT NULL DEFAULT '0' COMMENT 'Coupon Discount',
|
||||||
|
`start_time` bigint NOT NULL DEFAULT '0' COMMENT 'Start Time',
|
||||||
|
`expire_time` bigint NOT NULL DEFAULT '0' COMMENT 'Expire Time',
|
||||||
|
`user_limit` bigint NOT NULL DEFAULT '0' COMMENT 'User Limit',
|
||||||
|
`subscribe` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Subscribe Limit',
|
||||||
|
`used_count` bigint NOT NULL DEFAULT '0' COMMENT 'Used Count',
|
||||||
|
`enable` tinyint(1) NOT NULL DEFAULT '1' COMMENT 'Enable',
|
||||||
|
`created_at` datetime(3) DEFAULT NULL COMMENT 'Create Time',
|
||||||
|
`updated_at` datetime(3) DEFAULT NULL COMMENT 'Update Time',
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE KEY `uni_coupon_code` (`code`)
|
||||||
|
) ENGINE = InnoDB
|
||||||
|
DEFAULT CHARSET = utf8mb4
|
||||||
|
COLLATE = utf8mb4_general_ci;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `document`
|
||||||
|
(
|
||||||
|
`id` bigint NOT NULL AUTO_INCREMENT,
|
||||||
|
`title` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Document Title',
|
||||||
|
`content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci COMMENT 'Document Content',
|
||||||
|
`tags` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Document Tags',
|
||||||
|
`show` tinyint(1) NOT NULL DEFAULT '1' COMMENT 'Show',
|
||||||
|
`created_at` datetime(3) DEFAULT NULL COMMENT 'Create Time',
|
||||||
|
`updated_at` datetime(3) DEFAULT NULL COMMENT 'Update Time',
|
||||||
|
PRIMARY KEY (`id`)
|
||||||
|
) ENGINE = InnoDB
|
||||||
|
DEFAULT CHARSET = utf8mb4
|
||||||
|
COLLATE = utf8mb4_general_ci;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `message_log`
|
||||||
|
(
|
||||||
|
`id` bigint NOT NULL AUTO_INCREMENT,
|
||||||
|
`type` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT 'email' COMMENT 'Message Type',
|
||||||
|
`platform` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT 'smtp' COMMENT 'Platform',
|
||||||
|
`to` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT 'To',
|
||||||
|
`subject` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Subject',
|
||||||
|
`content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci COMMENT 'Content',
|
||||||
|
`status` tinyint(1) NOT NULL DEFAULT '0' COMMENT 'Status',
|
||||||
|
`created_at` datetime(3) DEFAULT NULL COMMENT 'Create Time',
|
||||||
|
`updated_at` datetime(3) DEFAULT NULL COMMENT 'Update Time',
|
||||||
|
PRIMARY KEY (`id`)
|
||||||
|
) ENGINE = InnoDB
|
||||||
|
DEFAULT CHARSET = utf8mb4
|
||||||
|
COLLATE = utf8mb4_general_ci;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `order`
|
||||||
|
(
|
||||||
|
`id` bigint NOT NULL AUTO_INCREMENT,
|
||||||
|
`parent_id` bigint DEFAULT NULL COMMENT 'Parent Order Id',
|
||||||
|
`user_id` bigint NOT NULL DEFAULT '0' COMMENT 'User Id',
|
||||||
|
`order_no` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Order No',
|
||||||
|
`type` tinyint(1) NOT NULL DEFAULT '1' COMMENT 'Order Type: 1: Subscribe, 2: Renewal, 3: ResetTraffic, 4: Recharge',
|
||||||
|
`quantity` bigint NOT NULL DEFAULT '1' COMMENT 'Quantity',
|
||||||
|
`price` bigint NOT NULL DEFAULT '0' COMMENT 'Original price',
|
||||||
|
`amount` bigint NOT NULL DEFAULT '0' COMMENT 'Order Amount',
|
||||||
|
`gift_amount` bigint NOT NULL DEFAULT '0' COMMENT 'User Gift Amount',
|
||||||
|
`discount` bigint NOT NULL DEFAULT '0' COMMENT 'Discount Amount',
|
||||||
|
`coupon` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT 'Coupon',
|
||||||
|
`coupon_discount` bigint NOT NULL DEFAULT '0' COMMENT 'Coupon Discount Amount',
|
||||||
|
`commission` bigint NOT NULL DEFAULT '0' COMMENT 'Order Commission',
|
||||||
|
`payment_id` bigint NOT NULL DEFAULT '-1' COMMENT 'Payment Id',
|
||||||
|
`method` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Payment Method',
|
||||||
|
`fee_amount` bigint NOT NULL DEFAULT '0' COMMENT 'Fee Amount',
|
||||||
|
`trade_no` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT 'Trade No',
|
||||||
|
`status` tinyint(1) NOT NULL DEFAULT '1' COMMENT 'Order Status: 1: Pending, 2: Paid, 3:Close, 4: Failed, 5:Finished',
|
||||||
|
`subscribe_id` bigint NOT NULL DEFAULT '0' COMMENT 'Subscribe Id',
|
||||||
|
`subscribe_token` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT 'Renewal Subscribe Token',
|
||||||
|
`is_new` tinyint(1) NOT NULL DEFAULT '0' COMMENT 'Is New Order',
|
||||||
|
`created_at` datetime(3) DEFAULT NULL COMMENT 'Create Time',
|
||||||
|
`updated_at` datetime(3) DEFAULT NULL COMMENT 'Update Time',
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE KEY `uni_order_order_no` (`order_no`)
|
||||||
|
) ENGINE = InnoDB
|
||||||
|
DEFAULT CHARSET = utf8mb4
|
||||||
|
COLLATE = utf8mb4_general_ci;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `payment`
|
||||||
|
(
|
||||||
|
`id` bigint NOT NULL AUTO_INCREMENT,
|
||||||
|
`name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Payment Name',
|
||||||
|
`platform` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT 'Payment Platform',
|
||||||
|
`description` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci COMMENT 'Payment Description',
|
||||||
|
`icon` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT '' COMMENT 'Payment Icon',
|
||||||
|
`domain` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT '' COMMENT 'Notification Domain',
|
||||||
|
`config` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT 'Payment Configuration',
|
||||||
|
`fee_mode` tinyint(1) NOT NULL DEFAULT '0' COMMENT 'Fee Mode: 0: No Fee 1: Percentage 2: Fixed Amount 3: Percentage + Fixed Amount',
|
||||||
|
`fee_percent` bigint DEFAULT '0' COMMENT 'Fee Percentage',
|
||||||
|
`fee_amount` bigint DEFAULT '0' COMMENT 'Fixed Fee Amount',
|
||||||
|
`enable` tinyint(1) NOT NULL DEFAULT '0' COMMENT 'Is Enabled',
|
||||||
|
`token` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT 'Payment Token',
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE KEY `uni_payment_token` (`token`)
|
||||||
|
) ENGINE = InnoDB
|
||||||
|
DEFAULT CHARSET = utf8mb4
|
||||||
|
COLLATE = utf8mb4_general_ci;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `server`
|
||||||
|
(
|
||||||
|
`id` bigint NOT NULL AUTO_INCREMENT,
|
||||||
|
`name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Node Name',
|
||||||
|
`tags` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Tags',
|
||||||
|
`country` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Country',
|
||||||
|
`city` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'City',
|
||||||
|
`latitude` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'latitude',
|
||||||
|
`longitude` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'longitude',
|
||||||
|
`server_addr` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Server Address',
|
||||||
|
`relay_mode` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT 'none' COMMENT 'Relay Mode',
|
||||||
|
`relay_node` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci COMMENT 'Relay Node',
|
||||||
|
`speed_limit` bigint NOT NULL DEFAULT '0' COMMENT 'Speed Limit',
|
||||||
|
`traffic_ratio` decimal(4, 2) NOT NULL DEFAULT '0.00' COMMENT 'Traffic Ratio',
|
||||||
|
`group_id` bigint DEFAULT NULL COMMENT 'Group ID',
|
||||||
|
`protocol` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Protocol',
|
||||||
|
`config` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci COMMENT 'Config',
|
||||||
|
`enable` tinyint(1) NOT NULL DEFAULT '1' COMMENT 'Enabled',
|
||||||
|
`sort` bigint NOT NULL DEFAULT '0' COMMENT 'Sort',
|
||||||
|
`last_reported_at` datetime(3) DEFAULT NULL COMMENT 'Last Reported Time',
|
||||||
|
`created_at` datetime(3) DEFAULT NULL COMMENT 'Creation Time',
|
||||||
|
`updated_at` datetime(3) DEFAULT NULL COMMENT 'Update Time',
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
KEY `idx_group_id` (`group_id`)
|
||||||
|
) ENGINE = InnoDB
|
||||||
|
DEFAULT CHARSET = utf8mb4
|
||||||
|
COLLATE = utf8mb4_general_ci;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `server_group`
|
||||||
|
(
|
||||||
|
`id` bigint NOT NULL AUTO_INCREMENT,
|
||||||
|
`name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Group Name',
|
||||||
|
`description` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT '' COMMENT 'Group Description',
|
||||||
|
`created_at` datetime(3) DEFAULT NULL COMMENT 'Creation Time',
|
||||||
|
`updated_at` datetime(3) DEFAULT NULL COMMENT 'Update Time',
|
||||||
|
PRIMARY KEY (`id`)
|
||||||
|
) ENGINE = InnoDB
|
||||||
|
DEFAULT CHARSET = utf8mb4
|
||||||
|
COLLATE = utf8mb4_general_ci;
|
||||||
|
|
||||||
|
-- if `sms` not exist, create it
|
||||||
|
CREATE TABLE IF NOT EXISTS `sms`
|
||||||
|
(
|
||||||
|
`id` bigint NOT NULL AUTO_INCREMENT,
|
||||||
|
`content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci,
|
||||||
|
`platform` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
|
||||||
|
`area_code` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
|
||||||
|
`telephone` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
|
||||||
|
`status` tinyint(1) DEFAULT '1',
|
||||||
|
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (`id`)
|
||||||
|
) ENGINE = InnoDB
|
||||||
|
DEFAULT CHARSET = utf8mb4
|
||||||
|
COLLATE = utf8mb4_general_ci;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `subscribe`
|
||||||
|
(
|
||||||
|
`id` bigint NOT NULL AUTO_INCREMENT,
|
||||||
|
`name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Subscribe Name',
|
||||||
|
`description` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci COMMENT 'Subscribe Description',
|
||||||
|
`unit_price` bigint NOT NULL DEFAULT '0' COMMENT 'Unit Price',
|
||||||
|
`unit_time` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Unit Time',
|
||||||
|
`discount` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci COMMENT 'Discount',
|
||||||
|
`replacement` bigint NOT NULL DEFAULT '0' COMMENT 'Replacement',
|
||||||
|
`inventory` bigint NOT NULL DEFAULT '0' COMMENT 'Inventory',
|
||||||
|
`traffic` bigint NOT NULL DEFAULT '0' COMMENT 'Traffic',
|
||||||
|
`speed_limit` bigint NOT NULL DEFAULT '0' COMMENT 'Speed Limit',
|
||||||
|
`device_limit` bigint NOT NULL DEFAULT '0' COMMENT 'Device Limit',
|
||||||
|
`quota` bigint NOT NULL DEFAULT '0' COMMENT 'Quota',
|
||||||
|
`group_id` bigint DEFAULT NULL COMMENT 'Group Id',
|
||||||
|
`server_group` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT 'Server Group',
|
||||||
|
`server` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT 'Server',
|
||||||
|
`show` tinyint(1) NOT NULL DEFAULT '0' COMMENT 'Show portal page',
|
||||||
|
`sell` tinyint(1) NOT NULL DEFAULT '0' COMMENT 'Sell',
|
||||||
|
`sort` bigint NOT NULL DEFAULT '0' COMMENT 'Sort',
|
||||||
|
`deduction_ratio` bigint DEFAULT '0' COMMENT 'Deduction Ratio',
|
||||||
|
`allow_deduction` tinyint(1) DEFAULT '1' COMMENT 'Allow deduction',
|
||||||
|
`reset_cycle` bigint DEFAULT '0' COMMENT 'Reset Cycle: 0: No Reset, 1: 1st, 2: Monthly, 3: Yearly',
|
||||||
|
`renewal_reset` tinyint(1) DEFAULT '0' COMMENT 'Renew Reset',
|
||||||
|
`created_at` datetime(3) DEFAULT NULL COMMENT 'Create Time',
|
||||||
|
`updated_at` datetime(3) DEFAULT NULL COMMENT 'Update Time',
|
||||||
|
PRIMARY KEY (`id`)
|
||||||
|
) ENGINE = InnoDB
|
||||||
|
DEFAULT CHARSET = utf8mb4
|
||||||
|
COLLATE = utf8mb4_general_ci;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `subscribe_group`
|
||||||
|
(
|
||||||
|
`id` bigint NOT NULL AUTO_INCREMENT,
|
||||||
|
`name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Group Name',
|
||||||
|
`description` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci COMMENT 'Group Description',
|
||||||
|
`created_at` datetime(3) DEFAULT NULL COMMENT 'Create Time',
|
||||||
|
`updated_at` datetime(3) DEFAULT NULL COMMENT 'Update Time',
|
||||||
|
PRIMARY KEY (`id`)
|
||||||
|
) ENGINE = InnoDB
|
||||||
|
DEFAULT CHARSET = utf8mb4
|
||||||
|
COLLATE = utf8mb4_general_ci;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `subscribe_type`
|
||||||
|
(
|
||||||
|
`id` bigint NOT NULL AUTO_INCREMENT,
|
||||||
|
`name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '订阅类型',
|
||||||
|
`mark` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '订阅标识',
|
||||||
|
`created_at` datetime(3) DEFAULT NULL COMMENT '创建时间',
|
||||||
|
`updated_at` datetime(3) DEFAULT NULL COMMENT '更新时间',
|
||||||
|
PRIMARY KEY (`id`)
|
||||||
|
) ENGINE = InnoDB
|
||||||
|
DEFAULT CHARSET = utf8mb4
|
||||||
|
COLLATE = utf8mb4_general_ci;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `system`
|
||||||
|
(
|
||||||
|
`id` bigint NOT NULL AUTO_INCREMENT,
|
||||||
|
`category` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Category',
|
||||||
|
`key` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Key Name',
|
||||||
|
`value` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT 'Key Value',
|
||||||
|
`type` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Type',
|
||||||
|
`desc` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT 'Description',
|
||||||
|
`created_at` datetime(3) DEFAULT NULL COMMENT 'Creation Time',
|
||||||
|
`updated_at` datetime(3) DEFAULT NULL COMMENT 'Update Time',
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE KEY `uni_system_key` (`key`),
|
||||||
|
KEY `index_key` (`key`)
|
||||||
|
) ENGINE = InnoDB
|
||||||
|
DEFAULT CHARSET = utf8mb4
|
||||||
|
COLLATE = utf8mb4_general_ci;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `ticket`
|
||||||
|
(
|
||||||
|
`id` bigint NOT NULL AUTO_INCREMENT,
|
||||||
|
`title` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Title',
|
||||||
|
`description` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci COMMENT 'Description',
|
||||||
|
`user_id` bigint NOT NULL DEFAULT '0' COMMENT 'UserId',
|
||||||
|
`status` tinyint(1) NOT NULL DEFAULT '1' COMMENT 'Status',
|
||||||
|
`created_at` datetime(3) DEFAULT NULL COMMENT 'Create Time',
|
||||||
|
`updated_at` datetime(3) DEFAULT NULL COMMENT 'Update Time',
|
||||||
|
PRIMARY KEY (`id`)
|
||||||
|
) ENGINE = InnoDB
|
||||||
|
DEFAULT CHARSET = utf8mb4
|
||||||
|
COLLATE = utf8mb4_general_ci;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `ticket_follow`
|
||||||
|
(
|
||||||
|
`id` bigint NOT NULL AUTO_INCREMENT,
|
||||||
|
`ticket_id` bigint NOT NULL DEFAULT '0' COMMENT 'TicketId',
|
||||||
|
`from` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'From',
|
||||||
|
`type` tinyint(1) NOT NULL DEFAULT '1' COMMENT 'Type: 1 text, 2 image',
|
||||||
|
`content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci COMMENT 'Content',
|
||||||
|
`created_at` datetime(3) DEFAULT NULL COMMENT 'Create Time',
|
||||||
|
PRIMARY KEY (`id`)
|
||||||
|
) ENGINE = InnoDB
|
||||||
|
DEFAULT CHARSET = utf8mb4
|
||||||
|
COLLATE = utf8mb4_general_ci;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `traffic_log`
|
||||||
|
(
|
||||||
|
`id` bigint NOT NULL AUTO_INCREMENT,
|
||||||
|
`server_id` bigint NOT NULL COMMENT 'Server ID',
|
||||||
|
`user_id` bigint NOT NULL COMMENT 'User ID',
|
||||||
|
`subscribe_id` bigint NOT NULL COMMENT 'Subscription ID',
|
||||||
|
`download` bigint DEFAULT '0' COMMENT 'Download Traffic',
|
||||||
|
`upload` bigint DEFAULT '0' COMMENT 'Upload Traffic',
|
||||||
|
`timestamp` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT 'Traffic Log Time',
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
KEY `idx_subscribe_id` (`subscribe_id`),
|
||||||
|
KEY `idx_server_id` (`server_id`),
|
||||||
|
KEY `idx_user_id` (`user_id`)
|
||||||
|
) ENGINE = InnoDB
|
||||||
|
DEFAULT CHARSET = utf8mb4
|
||||||
|
COLLATE = utf8mb4_general_ci;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `user`
|
||||||
|
(
|
||||||
|
`id` bigint NOT NULL AUTO_INCREMENT,
|
||||||
|
`password` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT 'User Password',
|
||||||
|
`avatar` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci COMMENT 'User Avatar',
|
||||||
|
`balance` bigint DEFAULT '0' COMMENT 'User Balance',
|
||||||
|
`telegram` bigint DEFAULT NULL COMMENT 'Telegram Account',
|
||||||
|
`refer_code` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT '' COMMENT 'Referral Code',
|
||||||
|
`referer_id` bigint DEFAULT NULL COMMENT 'Referrer ID',
|
||||||
|
`commission` bigint DEFAULT '0' COMMENT 'Commission',
|
||||||
|
`gift_amount` bigint DEFAULT '0' COMMENT 'User Gift Amount',
|
||||||
|
`enable` tinyint(1) NOT NULL DEFAULT '1' COMMENT 'Is Account Enabled',
|
||||||
|
`is_admin` tinyint(1) NOT NULL DEFAULT '0' COMMENT 'Is Admin',
|
||||||
|
`valid_email` tinyint(1) NOT NULL DEFAULT '0' COMMENT 'Is Email Verified',
|
||||||
|
`enable_email_notify` tinyint(1) NOT NULL DEFAULT '0' COMMENT 'Enable Email Notifications',
|
||||||
|
`enable_telegram_notify` tinyint(1) NOT NULL DEFAULT '0' COMMENT 'Enable Telegram Notifications',
|
||||||
|
`enable_balance_notify` tinyint(1) NOT NULL DEFAULT '0' COMMENT 'Enable Balance Change Notifications',
|
||||||
|
`enable_login_notify` tinyint(1) NOT NULL DEFAULT '0' COMMENT 'Enable Login Notifications',
|
||||||
|
`enable_subscribe_notify` tinyint(1) NOT NULL DEFAULT '0' COMMENT 'Enable Subscription Notifications',
|
||||||
|
`enable_trade_notify` tinyint(1) NOT NULL DEFAULT '0' COMMENT 'Enable Trade Notifications',
|
||||||
|
`created_at` datetime(3) DEFAULT NULL COMMENT 'Creation Time',
|
||||||
|
`updated_at` datetime(3) DEFAULT NULL COMMENT 'Update Time',
|
||||||
|
`deleted_at` datetime(3) DEFAULT NULL COMMENT 'Deletion Time',
|
||||||
|
`is_del` bigint unsigned DEFAULT NULL COMMENT '1: Normal 0: Deleted',
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
KEY `idx_referer` (`referer_id`)
|
||||||
|
) ENGINE = InnoDB
|
||||||
|
DEFAULT CHARSET = utf8mb4
|
||||||
|
COLLATE = utf8mb4_general_ci;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `user_auth_methods`
|
||||||
|
(
|
||||||
|
`id` bigint NOT NULL AUTO_INCREMENT,
|
||||||
|
`user_id` bigint NOT NULL COMMENT 'User ID',
|
||||||
|
`auth_type` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT 'Auth Type 1: apple 2: google 3: github 4: facebook 5: telegram 6: email 7: phone',
|
||||||
|
`auth_identifier` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT 'Auth Identifier',
|
||||||
|
`verified` tinyint(1) NOT NULL DEFAULT '0' COMMENT 'Is Verified',
|
||||||
|
`created_at` datetime(3) DEFAULT NULL COMMENT 'Creation Time',
|
||||||
|
`updated_at` datetime(3) DEFAULT NULL COMMENT 'Update Time',
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE KEY `idx_auth_identifier` (`auth_identifier`),
|
||||||
|
KEY `idx_user_id` (`user_id`)
|
||||||
|
) ENGINE = InnoDB
|
||||||
|
DEFAULT CHARSET = utf8mb4
|
||||||
|
COLLATE = utf8mb4_general_ci;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `user_balance_log`
|
||||||
|
(
|
||||||
|
`id` bigint NOT NULL AUTO_INCREMENT,
|
||||||
|
`user_id` bigint NOT NULL COMMENT 'User ID',
|
||||||
|
`amount` bigint NOT NULL COMMENT 'Amount',
|
||||||
|
`type` tinyint(1) NOT NULL COMMENT 'Type: 1: Recharge 2: Withdraw 3: Payment 4: Refund 5: Reward',
|
||||||
|
`order_id` bigint DEFAULT NULL COMMENT 'Order ID',
|
||||||
|
`balance` bigint NOT NULL COMMENT 'Balance',
|
||||||
|
`created_at` datetime(3) DEFAULT NULL COMMENT 'Creation Time',
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
KEY `idx_user_id` (`user_id`)
|
||||||
|
) ENGINE = InnoDB
|
||||||
|
DEFAULT CHARSET = utf8mb4
|
||||||
|
COLLATE = utf8mb4_general_ci;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `user_commission_log`
|
||||||
|
(
|
||||||
|
`id` bigint NOT NULL AUTO_INCREMENT,
|
||||||
|
`user_id` bigint NOT NULL COMMENT 'User ID',
|
||||||
|
`order_no` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT 'Order No.',
|
||||||
|
`amount` bigint NOT NULL COMMENT 'Amount',
|
||||||
|
`created_at` datetime(3) DEFAULT NULL COMMENT 'Creation Time',
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
KEY `idx_user_id` (`user_id`)
|
||||||
|
) ENGINE = InnoDB
|
||||||
|
DEFAULT CHARSET = utf8mb4
|
||||||
|
COLLATE = utf8mb4_general_ci;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `user_device`
|
||||||
|
(
|
||||||
|
`id` bigint NOT NULL AUTO_INCREMENT,
|
||||||
|
`user_id` bigint NOT NULL COMMENT 'User ID',
|
||||||
|
`subscribe_id` bigint DEFAULT NULL COMMENT 'Subscribe ID',
|
||||||
|
`ip` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT 'Device Ip.',
|
||||||
|
`Identifier` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT 'Device Identifier.',
|
||||||
|
`user_agent` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT 'Device User Agent.',
|
||||||
|
`online` tinyint(1) NOT NULL DEFAULT '0' COMMENT 'Online',
|
||||||
|
`enabled` tinyint(1) NOT NULL DEFAULT '1' COMMENT 'EnableDeviceNumber',
|
||||||
|
`created_at` datetime(3) DEFAULT NULL COMMENT 'Creation Time',
|
||||||
|
`updated_at` datetime(3) DEFAULT NULL COMMENT 'Update Time',
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
KEY `idx_user_id` (`user_id`)
|
||||||
|
) ENGINE = InnoDB
|
||||||
|
DEFAULT CHARSET = utf8mb4
|
||||||
|
COLLATE = utf8mb4_general_ci;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `user_gift_amount_log`
|
||||||
|
(
|
||||||
|
`id` bigint NOT NULL AUTO_INCREMENT,
|
||||||
|
`user_id` bigint NOT NULL COMMENT 'User ID',
|
||||||
|
`user_subscribe_id` bigint DEFAULT NULL COMMENT 'Deduction User Subscribe ID',
|
||||||
|
`order_no` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT 'Order No.',
|
||||||
|
`type` tinyint(1) NOT NULL COMMENT 'Type: 1: Increase 2: Reduce',
|
||||||
|
`amount` bigint NOT NULL COMMENT 'Amount',
|
||||||
|
`balance` bigint NOT NULL COMMENT 'Balance',
|
||||||
|
`remark` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT '' COMMENT 'Remark',
|
||||||
|
`created_at` datetime(3) DEFAULT NULL COMMENT 'Creation Time',
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
KEY `idx_user_id` (`user_id`)
|
||||||
|
) ENGINE = InnoDB
|
||||||
|
DEFAULT CHARSET = utf8mb4
|
||||||
|
COLLATE = utf8mb4_general_ci;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `user_login_log`
|
||||||
|
(
|
||||||
|
`id` bigint NOT NULL AUTO_INCREMENT,
|
||||||
|
`user_id` bigint NOT NULL COMMENT 'User ID',
|
||||||
|
`login_ip` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT 'Login IP',
|
||||||
|
`user_agent` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT 'UserAgent',
|
||||||
|
`success` tinyint(1) NOT NULL DEFAULT '0' COMMENT 'Login Success',
|
||||||
|
`created_at` datetime(3) DEFAULT NULL COMMENT 'Creation Time',
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
KEY `idx_user_id` (`user_id`)
|
||||||
|
) ENGINE = InnoDB
|
||||||
|
DEFAULT CHARSET = utf8mb4
|
||||||
|
COLLATE = utf8mb4_general_ci;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `user_subscribe`
|
||||||
|
(
|
||||||
|
`id` bigint NOT NULL AUTO_INCREMENT,
|
||||||
|
`user_id` bigint NOT NULL COMMENT 'User ID',
|
||||||
|
`order_id` bigint NOT NULL COMMENT 'Order ID',
|
||||||
|
`subscribe_id` bigint NOT NULL COMMENT 'Subscription ID',
|
||||||
|
`start_time` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT 'Subscription Start Time',
|
||||||
|
`expire_time` datetime(3) DEFAULT NULL COMMENT 'Subscription Expire Time',
|
||||||
|
`traffic` bigint DEFAULT '0' COMMENT 'Traffic',
|
||||||
|
`download` bigint DEFAULT '0' COMMENT 'Download Traffic',
|
||||||
|
`upload` bigint DEFAULT '0' COMMENT 'Upload Traffic',
|
||||||
|
`token` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT '' COMMENT 'Token',
|
||||||
|
`uuid` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT '' COMMENT 'UUID',
|
||||||
|
`status` tinyint(1) DEFAULT '0' COMMENT 'Subscription Status: 0: Pending 1: Active 2: Finished 3: Expired 4: Deducted',
|
||||||
|
`created_at` datetime(3) DEFAULT NULL COMMENT 'Creation Time',
|
||||||
|
`updated_at` datetime(3) DEFAULT NULL COMMENT 'Update Time',
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE KEY `uni_user_subscribe_token` (`token`),
|
||||||
|
UNIQUE KEY `uni_user_subscribe_uuid` (`uuid`),
|
||||||
|
KEY `idx_user_id` (`user_id`),
|
||||||
|
KEY `idx_order_id` (`order_id`),
|
||||||
|
KEY `idx_subscribe_id` (`subscribe_id`),
|
||||||
|
KEY `idx_token` (`token`),
|
||||||
|
KEY `idx_uuid` (`uuid`)
|
||||||
|
) ENGINE = InnoDB
|
||||||
|
DEFAULT CHARSET = utf8mb4
|
||||||
|
COLLATE = utf8mb4_general_ci;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `user_subscribe_log`
|
||||||
|
(
|
||||||
|
`id` bigint NOT NULL AUTO_INCREMENT,
|
||||||
|
`user_id` bigint NOT NULL COMMENT 'User ID',
|
||||||
|
`user_subscribe_id` bigint NOT NULL COMMENT 'User Subscribe ID',
|
||||||
|
`token` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT 'Token',
|
||||||
|
`ip` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT 'IP',
|
||||||
|
`user_agent` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT 'UserAgent',
|
||||||
|
`created_at` datetime(3) DEFAULT NULL COMMENT 'Creation Time',
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
KEY `idx_user_id` (`user_id`),
|
||||||
|
KEY `idx_user_subscribe_id` (`user_subscribe_id`)
|
||||||
|
) ENGINE = InnoDB
|
||||||
|
DEFAULT CHARSET = utf8mb4
|
||||||
|
COLLATE = utf8mb4_general_ci;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `server_rule_group`
|
||||||
|
(
|
||||||
|
`id` bigint NOT NULL AUTO_INCREMENT,
|
||||||
|
`name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Rule Group Name',
|
||||||
|
`icon` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci COMMENT 'Rule Group Icon',
|
||||||
|
`tags` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci COMMENT 'Selected Node Tags',
|
||||||
|
`description` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT '' COMMENT 'Rule Group Description',
|
||||||
|
`enable` tinyint(1) NOT NULL DEFAULT '1' COMMENT 'Rule Group Enable',
|
||||||
|
`created_at` datetime(3) DEFAULT NULL COMMENT 'Creation Time',
|
||||||
|
`updated_at` datetime(3) DEFAULT NULL COMMENT 'Update Time',
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE KEY `unique_name` (`name`)
|
||||||
|
) ENGINE = InnoDB
|
||||||
|
DEFAULT CHARSET = utf8mb4
|
||||||
|
COLLATE = utf8mb4_general_ci;
|
||||||
|
|
||||||
|
|
||||||
|
SET FOREIGN_KEY_CHECKS = 1;
|
||||||
|
|
||||||
|
-- migrate:down
|
||||||
|
-- 000001_init_schema.down.sql
|
||||||
|
SET FOREIGN_KEY_CHECKS = 0;
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS `user_subscribe_log`;
|
||||||
|
DROP TABLE IF EXISTS `user_subscribe`;
|
||||||
|
DROP TABLE IF EXISTS `user_login_log`;
|
||||||
|
DROP TABLE IF EXISTS `user_gift_amount_log`;
|
||||||
|
DROP TABLE IF EXISTS `user_device`;
|
||||||
|
DROP TABLE IF EXISTS `user_commission_log`;
|
||||||
|
DROP TABLE IF EXISTS `user_balance_log`;
|
||||||
|
DROP TABLE IF EXISTS `user_auth_methods`;
|
||||||
|
DROP TABLE IF EXISTS `user`;
|
||||||
|
DROP TABLE IF EXISTS `traffic_log`;
|
||||||
|
DROP TABLE IF EXISTS `ticket_follow`;
|
||||||
|
DROP TABLE IF EXISTS `ticket`;
|
||||||
|
DROP TABLE IF EXISTS `system`;
|
||||||
|
DROP TABLE IF EXISTS `subscribe_type`;
|
||||||
|
DROP TABLE IF EXISTS `subscribe_group`;
|
||||||
|
DROP TABLE IF EXISTS `subscribe`;
|
||||||
|
DROP TABLE IF EXISTS `sms`;
|
||||||
|
DROP TABLE IF EXISTS `server_rule_group`;
|
||||||
|
DROP TABLE IF EXISTS `server_group`;
|
||||||
|
DROP TABLE IF EXISTS `server`;
|
||||||
|
DROP TABLE IF EXISTS `payment`;
|
||||||
|
DROP TABLE IF EXISTS `order`;
|
||||||
|
DROP TABLE IF EXISTS `message_log`;
|
||||||
|
DROP TABLE IF EXISTS `document`;
|
||||||
|
DROP TABLE IF EXISTS `coupon`;
|
||||||
|
DROP TABLE IF EXISTS `auth_method`;
|
||||||
|
DROP TABLE IF EXISTS `application_version`;
|
||||||
|
DROP TABLE IF EXISTS `application_config`;
|
||||||
|
DROP TABLE IF EXISTS `application`;
|
||||||
|
DROP TABLE IF EXISTS `announcement`;
|
||||||
|
DROP TABLE IF EXISTS `ads`;
|
||||||
|
|
||||||
|
SET FOREIGN_KEY_CHECKS = 1;
|
||||||
@@ -0,0 +1,150 @@
|
|||||||
|
-- migrate:up
|
||||||
|
-- 000002_init_data.up.sql
|
||||||
|
SET FOREIGN_KEY_CHECKS = 0;
|
||||||
|
|
||||||
|
-- auth_method
|
||||||
|
INSERT IGNORE INTO `auth_method` (`id`, `method`, `config`, `enabled`, `created_at`, `updated_at`)
|
||||||
|
VALUES (1, 'email',
|
||||||
|
'{"platform":"smtp","platform_config":{"host":"","port":0,"user":"","pass":"","from":"","ssl":false},"enable_verify":false,"enable_notify":false,"enable_domain_suffix":false,"domain_suffix_list":"","verify_email_template":"","expiration_email_template":"","maintenance_email_template":"","traffic_exceed_email_template":""}',
|
||||||
|
1, '2025-04-22 14:25:16.642', '2025-04-22 14:25:16.642'),
|
||||||
|
(2, 'mobile',
|
||||||
|
'{"platform":"AlibabaCloud","platform_config":{"access":"","secret":"","sign_name":"","endpoint":"","template_code":""},"enable_whitelist":false,"whitelist":[]}',
|
||||||
|
0, '2025-04-22 14:25:16.642', '2025-04-22 14:25:16.642'),
|
||||||
|
(3, 'apple', '{"team_id":"","key_id":"","client_id":"","client_secret":"","redirect_url":""}', 0,
|
||||||
|
'2025-04-22 14:25:16.642', '2025-04-22 14:25:16.642'),
|
||||||
|
(4, 'google', '{"client_id":"","client_secret":"","redirect_url":""}', 0, '2025-04-22 14:25:16.642',
|
||||||
|
'2025-04-22 14:25:16.642'),
|
||||||
|
(5, 'github', '{"client_id":"","client_secret":"","redirect_url":""}', 0, '2025-04-22 14:25:16.642',
|
||||||
|
'2025-04-22 14:25:16.642'),
|
||||||
|
(6, 'facebook', '{"client_id":"","client_secret":"","redirect_url":""}', 0, '2025-04-22 14:25:16.642',
|
||||||
|
'2025-04-22 14:25:16.642'),
|
||||||
|
(7, 'telegram', '{"bot_token":"","enable_notify":false,"webhook_domain":""}', 0, '2025-04-22 14:25:16.642',
|
||||||
|
'2025-04-22 14:25:16.642'),
|
||||||
|
(8, 'device', '{"show_ads":false,"only_real_device":false,"enable_security":false,"security_secret":""}', 0,
|
||||||
|
'2025-04-22 14:25:16.642', '2025-04-22 14:25:16.642');
|
||||||
|
|
||||||
|
-- payment
|
||||||
|
INSERT IGNORE INTO `payment` (`id`, `name`, `platform`, `description`, `icon`, `domain`, `config`, `fee_mode`,
|
||||||
|
`fee_percent`, `fee_amount`, `enable`, `token`)
|
||||||
|
VALUES (-1, 'Balance', 'balance', '', '', '', '', 0, 0, 0, 1, '');
|
||||||
|
|
||||||
|
-- subscribe_type
|
||||||
|
INSERT IGNORE INTO `subscribe_type` (`id`, `name`, `mark`, `created_at`, `updated_at`)
|
||||||
|
VALUES (1, 'Clash', 'Clash', '2025-04-22 14:25:16.648', '2025-04-22 14:25:16.648'),
|
||||||
|
(2, 'Hiddify', 'Hiddify', '2025-04-22 14:25:16.648', '2025-04-22 14:25:16.648'),
|
||||||
|
(3, 'Loon', 'Loon', '2025-04-22 14:25:16.648', '2025-04-22 14:25:16.648'),
|
||||||
|
(4, 'NekoBox', 'NekoBox', '2025-04-22 14:25:16.648', '2025-04-22 14:25:16.648'),
|
||||||
|
(5, 'NekoRay', 'NekoRay', '2025-04-22 14:25:16.648', '2025-04-22 14:25:16.648'),
|
||||||
|
(6, 'Netch', 'Netch', '2025-04-22 14:25:16.648', '2025-04-22 14:25:16.648'),
|
||||||
|
(7, 'Quantumult', 'Quantumult', '2025-04-22 14:25:16.648', '2025-04-22 14:25:16.648'),
|
||||||
|
(8, 'Shadowrocket', 'Shadowrocket', '2025-04-22 14:25:16.648', '2025-04-22 14:25:16.648'),
|
||||||
|
(9, 'SingBox', ' SingBox', '2025-04-22 14:25:16.648', '2025-04-22 14:25:16.648'),
|
||||||
|
(10, 'Surfboard', 'Surfboard', '2025-04-22 14:25:16.648', '2025-04-22 14:25:16.648'),
|
||||||
|
(11, 'Surge', 'Surge', '2025-04-22 14:25:16.648', '2025-04-22 14:25:16.648'),
|
||||||
|
(12, 'V2box', 'V2box', '2025-04-22 14:25:16.648', '2025-04-22 14:25:16.648'),
|
||||||
|
(13, 'V2rayN', 'V2rayN', '2025-04-22 14:25:16.648', '2025-04-22 14:25:16.648'),
|
||||||
|
(14, 'V2rayNg', 'V2rayNg', '2025-04-22 14:25:16.648', '2025-04-22 14:25:16.648');
|
||||||
|
|
||||||
|
-- system
|
||||||
|
INSERT IGNORE INTO `system` (`id`, `category`, `key`, `value`, `type`, `desc`, `created_at`, `updated_at`)
|
||||||
|
VALUES (1, 'site', 'SiteLogo', '/favicon.svg', 'string', 'Site Logo', '2025-04-22 14:25:16.637',
|
||||||
|
'2025-04-22 14:25:16.637'),
|
||||||
|
(2, 'site', 'SiteName', 'Perfect Panel', 'string', 'Site Name', '2025-04-22 14:25:16.637',
|
||||||
|
'2025-04-22 14:25:16.637'),
|
||||||
|
(3, 'site', 'SiteDesc',
|
||||||
|
'PPanel is a pure, professional, and perfect open-source proxy panel tool, designed to be your ideal choice for learning and practical use.',
|
||||||
|
'string', 'Site Description', '2025-04-22 14:25:16.637', '2025-04-22 14:25:16.637'),
|
||||||
|
(4, 'site', 'Host', '', 'string', 'Site Host', '2025-04-22 14:25:16.637', '2025-04-22 14:25:16.637'),
|
||||||
|
(5, 'site', 'Keywords', 'Perfect Panel,PPanel', 'string', 'Site Keywords', '2025-04-22 14:25:16.637',
|
||||||
|
'2025-04-22 14:25:16.637'),
|
||||||
|
(6, 'site', 'CustomHTML', '', 'string', 'Custom HTML', '2025-04-22 14:25:16.637', '2025-04-22 14:25:16.637'),
|
||||||
|
(7, 'tos', 'TosContent', 'Welcome to use Perfect Panel', 'string', 'Terms of Service', '2025-04-22 14:25:16.637',
|
||||||
|
'2025-04-22 14:25:16.637'),
|
||||||
|
(8, 'tos', 'PrivacyPolicy', '', 'string', 'PrivacyPolicy', '2025-04-22 14:25:16.637', '2025-04-22 14:25:16.637'),
|
||||||
|
(9, 'ad', 'WebAD', 'false', 'bool', 'Display ad on the web', '2025-04-22 14:25:16.637',
|
||||||
|
'2025-04-22 14:25:16.637'),
|
||||||
|
(10, 'subscribe', 'SingleModel', 'false', 'bool', '是否单订阅模式', '2025-04-22 14:25:16.639',
|
||||||
|
'2025-04-22 14:25:16.639'),
|
||||||
|
(11, 'subscribe', 'SubscribePath', '/api/subscribe', 'string', '订阅路径', '2025-04-22 14:25:16.639',
|
||||||
|
'2025-04-22 14:25:16.639'),
|
||||||
|
(12, 'subscribe', 'SubscribeDomain', '', 'string', '订阅域名', '2025-04-22 14:25:16.639',
|
||||||
|
'2025-04-22 14:25:16.639'),
|
||||||
|
(13, 'subscribe', 'PanDomain', 'false', 'bool', '是否使用泛域名', '2025-04-22 14:25:16.639',
|
||||||
|
'2025-04-22 14:25:16.639'),
|
||||||
|
(14, 'verify', 'TurnstileSiteKey', '', 'string', 'TurnstileSiteKey', '2025-04-22 14:25:16.639',
|
||||||
|
'2025-04-22 14:25:16.639'),
|
||||||
|
(15, 'verify', 'TurnstileSecret', '', 'string', 'TurnstileSecret', '2025-04-22 14:25:16.639',
|
||||||
|
'2025-04-22 14:25:16.639'),
|
||||||
|
(16, 'verify', 'EnableLoginVerify', 'false', 'bool', 'is enable login verify', '2025-04-22 14:25:16.639',
|
||||||
|
'2025-04-22 14:25:16.639'),
|
||||||
|
(17, 'verify', 'EnableRegisterVerify', 'false', 'bool', 'is enable register verify', '2025-04-22 14:25:16.639',
|
||||||
|
'2025-04-22 14:25:16.639'),
|
||||||
|
(18, 'verify', 'EnableResetPasswordVerify', 'false', 'bool', 'is enable reset password verify',
|
||||||
|
'2025-04-22 14:25:16.639', '2025-04-22 14:25:16.639'),
|
||||||
|
(19, 'server', 'NodeSecret', '12345678', 'string', 'node secret', '2025-04-22 14:25:16.640',
|
||||||
|
'2025-04-22 14:25:16.640'),
|
||||||
|
(20, 'server', 'NodePullInterval', '10', 'int', 'node pull interval', '2025-04-22 14:25:16.640',
|
||||||
|
'2025-04-22 14:25:16.640'),
|
||||||
|
(21, 'server', 'NodePushInterval', '60', 'int', 'node push interval', '2025-04-22 14:25:16.640',
|
||||||
|
'2025-04-22 14:25:16.640'),
|
||||||
|
(22, 'server', 'NodeMultiplierConfig', '[]', 'string', 'node multiplier config', '2025-04-22 14:25:16.640',
|
||||||
|
'2025-04-22 14:25:16.640'),
|
||||||
|
(23, 'invite', 'ForcedInvite', 'false', 'bool', 'Forced invite', '2025-04-22 14:25:16.640',
|
||||||
|
'2025-04-22 14:25:16.640'),
|
||||||
|
(24, 'invite', 'ReferralPercentage', '20', 'int', 'Referral percentage', '2025-04-22 14:25:16.640',
|
||||||
|
'2025-04-22 14:25:16.640'),
|
||||||
|
(25, 'invite', 'OnlyFirstPurchase', 'false', 'bool', 'Only first purchase', '2025-04-22 14:25:16.640',
|
||||||
|
'2025-04-22 14:25:16.640'),
|
||||||
|
(26, 'register', 'StopRegister', 'false', 'bool', 'is stop register', '2025-04-22 14:25:16.640',
|
||||||
|
'2025-04-22 14:25:16.640'),
|
||||||
|
(27, 'register', 'EnableTrial', 'false', 'bool', 'is enable trial', '2025-04-22 14:25:16.640',
|
||||||
|
'2025-04-22 14:25:16.640'),
|
||||||
|
(28, 'register', 'TrialSubscribe', '', 'int', 'Trial subscription', '2025-04-22 14:25:16.640',
|
||||||
|
'2025-04-22 14:25:16.640'),
|
||||||
|
(29, 'register', 'TrialTime', '24', 'int', 'Trial time', '2025-04-22 14:25:16.640', '2025-04-22 14:25:16.640'),
|
||||||
|
(30, 'register', 'TrialTimeUnit', 'Hour', 'string', 'Trial time unit', '2025-04-22 14:25:16.640',
|
||||||
|
'2025-04-22 14:25:16.640'),
|
||||||
|
(31, 'register', 'EnableIpRegisterLimit', 'false', 'bool', 'is enable IP register limit',
|
||||||
|
'2025-04-22 14:25:16.640', '2025-04-22 14:25:16.640'),
|
||||||
|
(32, 'register', 'IpRegisterLimit', '3', 'int', 'IP Register Limit', '2025-04-22 14:25:16.640',
|
||||||
|
'2025-04-22 14:25:16.640'),
|
||||||
|
(33, 'register', 'IpRegisterLimitDuration', '64', 'int', 'IP Register Limit Duration (minutes)',
|
||||||
|
'2025-04-22 14:25:16.640', '2025-04-22 14:25:16.640'),
|
||||||
|
(34, 'currency', 'Currency', 'USD', 'string', 'Currency', '2025-04-22 14:25:16.641', '2025-04-22 14:25:16.641'),
|
||||||
|
(35, 'currency', 'CurrencySymbol', '$', 'string', 'Currency Symbol', '2025-04-22 14:25:16.641',
|
||||||
|
'2025-04-22 14:25:16.641'),
|
||||||
|
(36, 'currency', 'CurrencyUnit', 'USD', 'string', 'Currency Unit', '2025-04-22 14:25:16.641',
|
||||||
|
'2025-04-22 14:25:16.641'),
|
||||||
|
(37, 'currency', 'AccessKey', '', 'string', 'Exchangerate Access Key', '2025-04-22 14:25:16.641',
|
||||||
|
'2025-04-22 14:25:16.641'),
|
||||||
|
(38, 'verify_code', 'VerifyCodeExpireTime', '300', 'int', 'Verify code expire time', '2025-04-22 14:25:16.641',
|
||||||
|
'2025-04-22 14:25:16.641'),
|
||||||
|
(39, 'verify_code', 'VerifyCodeLimit', '15', 'int', 'limits of verify code', '2025-04-22 14:25:16.641',
|
||||||
|
'2025-04-22 14:25:16.641'),
|
||||||
|
(40, 'verify_code', 'VerifyCodeInterval', '60', 'int', 'Interval of verify code', '2025-04-22 14:25:16.641',
|
||||||
|
'2025-04-22 14:25:16.641'),
|
||||||
|
(41, 'system', 'Version', '0.2.0(02002)', 'string', 'System Version', '2025-04-22 14:25:16.642',
|
||||||
|
'2025-04-22 14:25:16.642');
|
||||||
|
SET FOREIGN_KEY_CHECKS = 1;
|
||||||
|
-- migrate:down
|
||||||
|
-- 000002_init_data.down.sql
|
||||||
|
SET
|
||||||
|
FOREIGN_KEY_CHECKS = 0;
|
||||||
|
|
||||||
|
DELETE
|
||||||
|
FROM `auth_method`
|
||||||
|
WHERE `id` IN (1, 2, 3, 4, 5, 6, 7, 8);
|
||||||
|
DELETE
|
||||||
|
FROM `payment`
|
||||||
|
WHERE `id` = -1;
|
||||||
|
DELETE
|
||||||
|
FROM `subscribe_type`
|
||||||
|
WHERE `id` IN (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14);
|
||||||
|
DELETE
|
||||||
|
FROM `system`
|
||||||
|
WHERE `id` IN
|
||||||
|
(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30,
|
||||||
|
31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41);
|
||||||
|
|
||||||
|
SET
|
||||||
|
FOREIGN_KEY_CHECKS = 1;
|
||||||
@@ -0,0 +1,146 @@
|
|||||||
|
-- migrate:up
|
||||||
|
-- 2025-04-22 16:16:00
|
||||||
|
-- Purpose: Update payment table
|
||||||
|
-- Author: PPanel Team, 2025-04-21
|
||||||
|
|
||||||
|
SET FOREIGN_KEY_CHECKS = 0;
|
||||||
|
|
||||||
|
-- Alter the order table to add a payment_id column (if not exists)
|
||||||
|
SET @column_exists = (SELECT COUNT(*)
|
||||||
|
FROM INFORMATION_SCHEMA.COLUMNS
|
||||||
|
WHERE TABLE_SCHEMA = DATABASE()
|
||||||
|
AND TABLE_NAME = 'order'
|
||||||
|
AND COLUMN_NAME = 'payment_id');
|
||||||
|
SET @sql = IF(@column_exists = 0,
|
||||||
|
'ALTER TABLE `order` ADD COLUMN `payment_id` bigint NOT NULL DEFAULT \'-1\' COMMENT \'Payment Id\' AFTER `commission`',
|
||||||
|
'SELECT 1');
|
||||||
|
PREPARE stmt FROM @sql;
|
||||||
|
EXECUTE stmt;
|
||||||
|
DEALLOCATE PREPARE stmt;
|
||||||
|
|
||||||
|
-- Alter the payment table to add a platform column (if not exists)
|
||||||
|
SET @column_exists = (SELECT COUNT(*)
|
||||||
|
FROM INFORMATION_SCHEMA.COLUMNS
|
||||||
|
WHERE TABLE_SCHEMA = DATABASE()
|
||||||
|
AND TABLE_NAME = 'payment'
|
||||||
|
AND COLUMN_NAME = 'platform');
|
||||||
|
SET @sql = IF(@column_exists = 0,
|
||||||
|
'ALTER TABLE `payment` ADD COLUMN `platform` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT \'Payment Platform\' AFTER `name`',
|
||||||
|
'SELECT 1');
|
||||||
|
PREPARE stmt FROM @sql;
|
||||||
|
EXECUTE stmt;
|
||||||
|
DEALLOCATE PREPARE stmt;
|
||||||
|
|
||||||
|
-- Drop the mark column from the payment table (only if exists)
|
||||||
|
SET @column_exists = (SELECT COUNT(*)
|
||||||
|
FROM INFORMATION_SCHEMA.COLUMNS
|
||||||
|
WHERE TABLE_SCHEMA = DATABASE()
|
||||||
|
AND TABLE_NAME = 'payment'
|
||||||
|
AND COLUMN_NAME = 'mark');
|
||||||
|
SET @sql = IF(@column_exists > 0,
|
||||||
|
'ALTER TABLE `payment` DROP COLUMN `mark`',
|
||||||
|
'SELECT 1');
|
||||||
|
PREPARE stmt FROM @sql;
|
||||||
|
EXECUTE stmt;
|
||||||
|
DEALLOCATE PREPARE stmt;
|
||||||
|
|
||||||
|
-- Alter the payment table to add a description column (if not exists)
|
||||||
|
SET @column_exists = (SELECT COUNT(*)
|
||||||
|
FROM INFORMATION_SCHEMA.COLUMNS
|
||||||
|
WHERE TABLE_SCHEMA = DATABASE()
|
||||||
|
AND TABLE_NAME = 'payment'
|
||||||
|
AND COLUMN_NAME = 'description');
|
||||||
|
SET @sql = IF(@column_exists = 0,
|
||||||
|
'ALTER TABLE `payment` ADD COLUMN `description` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci COMMENT \'Payment Description\' AFTER `platform`',
|
||||||
|
'SELECT 1');
|
||||||
|
PREPARE stmt FROM @sql;
|
||||||
|
EXECUTE stmt;
|
||||||
|
DEALLOCATE PREPARE stmt;
|
||||||
|
|
||||||
|
-- Alter the payment table to add a token column (if not exists)
|
||||||
|
SET @column_exists = (SELECT COUNT(*)
|
||||||
|
FROM INFORMATION_SCHEMA.COLUMNS
|
||||||
|
WHERE TABLE_SCHEMA = DATABASE()
|
||||||
|
AND TABLE_NAME = 'payment'
|
||||||
|
AND COLUMN_NAME = 'token');
|
||||||
|
SET @sql = IF(@column_exists = 0,
|
||||||
|
'ALTER TABLE `payment` ADD COLUMN `token` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT \'Payment Token\' AFTER `description`',
|
||||||
|
'SELECT 1');
|
||||||
|
PREPARE stmt FROM @sql;
|
||||||
|
EXECUTE stmt;
|
||||||
|
DEALLOCATE PREPARE stmt;
|
||||||
|
|
||||||
|
SET FOREIGN_KEY_CHECKS = 1;
|
||||||
|
-- migrate:down
|
||||||
|
-- migrations/02003_update_payment.down.sql
|
||||||
|
-- Purpose: Revert updates to payment and order tables
|
||||||
|
-- Author: PPanel Team, 2025-04-21
|
||||||
|
|
||||||
|
SET FOREIGN_KEY_CHECKS = 0;
|
||||||
|
|
||||||
|
-- Drop payment_id column from order table (if exists)
|
||||||
|
SET @column_exists = (SELECT COUNT(*)
|
||||||
|
FROM INFORMATION_SCHEMA.COLUMNS
|
||||||
|
WHERE TABLE_SCHEMA = DATABASE()
|
||||||
|
AND TABLE_NAME = 'order'
|
||||||
|
AND COLUMN_NAME = 'payment_id');
|
||||||
|
SET @sql = IF(@column_exists > 0,
|
||||||
|
'ALTER TABLE `order` DROP COLUMN `payment_id`',
|
||||||
|
'SELECT 1');
|
||||||
|
PREPARE stmt FROM @sql;
|
||||||
|
EXECUTE stmt;
|
||||||
|
DEALLOCATE PREPARE stmt;
|
||||||
|
|
||||||
|
-- Drop platform column from payment table (if exists)
|
||||||
|
SET @column_exists = (SELECT COUNT(*)
|
||||||
|
FROM INFORMATION_SCHEMA.COLUMNS
|
||||||
|
WHERE TABLE_SCHEMA = DATABASE()
|
||||||
|
AND TABLE_NAME = 'payment'
|
||||||
|
AND COLUMN_NAME = 'platform');
|
||||||
|
SET @sql = IF(@column_exists > 0,
|
||||||
|
'ALTER TABLE `payment` DROP COLUMN `platform`',
|
||||||
|
'SELECT 1');
|
||||||
|
PREPARE stmt FROM @sql;
|
||||||
|
EXECUTE stmt;
|
||||||
|
DEALLOCATE PREPARE stmt;
|
||||||
|
|
||||||
|
-- Drop description column from payment table (if exists)
|
||||||
|
SET @column_exists = (SELECT COUNT(*)
|
||||||
|
FROM INFORMATION_SCHEMA.COLUMNS
|
||||||
|
WHERE TABLE_SCHEMA = DATABASE()
|
||||||
|
AND TABLE_NAME = 'payment'
|
||||||
|
AND COLUMN_NAME = 'description');
|
||||||
|
SET @sql = IF(@column_exists > 0,
|
||||||
|
'ALTER TABLE `payment` DROP COLUMN `description`',
|
||||||
|
'SELECT 1');
|
||||||
|
PREPARE stmt FROM @sql;
|
||||||
|
EXECUTE stmt;
|
||||||
|
DEALLOCATE PREPARE stmt;
|
||||||
|
|
||||||
|
-- Drop token column from payment table (if exists)
|
||||||
|
SET @column_exists = (SELECT COUNT(*)
|
||||||
|
FROM INFORMATION_SCHEMA.COLUMNS
|
||||||
|
WHERE TABLE_SCHEMA = DATABASE()
|
||||||
|
AND TABLE_NAME = 'payment'
|
||||||
|
AND COLUMN_NAME = 'token');
|
||||||
|
SET @sql = IF(@column_exists > 0,
|
||||||
|
'ALTER TABLE `payment` DROP COLUMN `token`',
|
||||||
|
'SELECT 1');
|
||||||
|
PREPARE stmt FROM @sql;
|
||||||
|
EXECUTE stmt;
|
||||||
|
DEALLOCATE PREPARE stmt;
|
||||||
|
|
||||||
|
-- Optionally restore mark column (if needed, adjust definition as per original schema)
|
||||||
|
SET @column_exists = (SELECT COUNT(*)
|
||||||
|
FROM INFORMATION_SCHEMA.COLUMNS
|
||||||
|
WHERE TABLE_SCHEMA = DATABASE()
|
||||||
|
AND TABLE_NAME = 'payment'
|
||||||
|
AND COLUMN_NAME = 'mark');
|
||||||
|
SET @sql = IF(@column_exists = 0,
|
||||||
|
'ALTER TABLE `payment` ADD COLUMN `mark` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT \'Payment Mark\' AFTER `name`',
|
||||||
|
'SELECT 1');
|
||||||
|
PREPARE stmt FROM @sql;
|
||||||
|
EXECUTE stmt;
|
||||||
|
DEALLOCATE PREPARE stmt;
|
||||||
|
|
||||||
|
SET FOREIGN_KEY_CHECKS = 1;
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
-- migrate:up
|
||||||
|
-- migrations/02003_rebuild_rule.up.sql
|
||||||
|
-- Purpose: rebuilding server rule table
|
||||||
|
-- Author: PPanel Team, 2025-04-21
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS `server_rule_group`;
|
||||||
|
|
||||||
|
CREATE TABLE `server_rule_group`
|
||||||
|
(
|
||||||
|
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||||
|
`name` VARCHAR(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Rule Group Name',
|
||||||
|
`icon` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci COMMENT 'Rule Group Icon',
|
||||||
|
`tags` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci COMMENT 'Selected Node Tags',
|
||||||
|
`rules` MEDIUMTEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci COMMENT 'Rules',
|
||||||
|
`enable` TINYINT(1) NOT NULL DEFAULT 1 COMMENT 'Rule Group Enable',
|
||||||
|
`created_at` DATETIME(3) COMMENT 'Creation Time',
|
||||||
|
`updated_at` DATETIME(3) COMMENT 'Update Time',
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE KEY `uni_server_rule_group_name` (`name`),
|
||||||
|
INDEX `idx_enable` (`enable`)
|
||||||
|
) ENGINE = InnoDB
|
||||||
|
DEFAULT CHARSET = utf8mb4
|
||||||
|
COLLATE = utf8mb4_general_ci;
|
||||||
|
-- migrate:down
|
||||||
|
-- migrations/02003_rebuild_rule.up.sql
|
||||||
|
-- Purpose: Back rebuilding server rule table
|
||||||
|
-- Author: PPanel Team, 2025-04-21
|
||||||
|
DROP TABLE IF EXISTS server_rule_group;
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
-- migrate:up
|
||||||
|
-- migrations/02005_create_user_device_online_record.up.sql
|
||||||
|
-- Purpose: Create table for tracking user device online records
|
||||||
|
-- Author: PPanel Team, 2025-04-22
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `user_device_online_record`
|
||||||
|
(
|
||||||
|
`id` BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
`user_id` BIGINT NOT NULL COMMENT 'User ID',
|
||||||
|
`identifier` VARCHAR(255) NOT NULL COMMENT 'Device Identifier',
|
||||||
|
`online_time` DATETIME COMMENT 'Online Time',
|
||||||
|
`offline_time` DATETIME COMMENT 'Offline Time',
|
||||||
|
`online_seconds` BIGINT COMMENT 'Offline Seconds',
|
||||||
|
`duration_days` BIGINT COMMENT 'Duration Days',
|
||||||
|
`created_at` DATETIME COMMENT 'Creation Time'
|
||||||
|
) ENGINE = InnoDB
|
||||||
|
DEFAULT CHARSET = utf8mb4
|
||||||
|
COLLATE = utf8mb4_general_ci;
|
||||||
|
|
||||||
|
|
||||||
|
-- User subscribe table migration for adding finished_at column
|
||||||
|
|
||||||
|
SET @column_exists = (SELECT COUNT(*)
|
||||||
|
FROM INFORMATION_SCHEMA.COLUMNS
|
||||||
|
WHERE TABLE_SCHEMA = DATABASE()
|
||||||
|
AND TABLE_NAME = 'user_subscribe'
|
||||||
|
AND COLUMN_NAME = 'finished_at');
|
||||||
|
|
||||||
|
SET @sql = IF(@column_exists = 0,
|
||||||
|
'ALTER TABLE `user_subscribe` ADD COLUMN `finished_at` DATETIME NULL COMMENT ''Subscribe Finished Time'' AFTER `expire_time`',
|
||||||
|
'SELECT 1'
|
||||||
|
);
|
||||||
|
|
||||||
|
PREPARE stmt FROM @sql;
|
||||||
|
EXECUTE stmt;
|
||||||
|
DEALLOCATE PREPARE stmt;
|
||||||
|
|
||||||
|
|
||||||
|
-- Application config table migration for adding Link column
|
||||||
|
|
||||||
|
SET @column_exists = (SELECT COUNT(*)
|
||||||
|
FROM INFORMATION_SCHEMA.COLUMNS
|
||||||
|
WHERE TABLE_SCHEMA = DATABASE()
|
||||||
|
AND TABLE_NAME = 'application_config'
|
||||||
|
AND COLUMN_NAME = 'invitation_link');
|
||||||
|
|
||||||
|
SET @sql = IF(@column_exists = 0,
|
||||||
|
'ALTER TABLE `application_config` ADD COLUMN `invitation_link` TEXT NULL DEFAULT NULL COMMENT ''Invitation Link'' AFTER `startup_picture_skip_time`',
|
||||||
|
'SELECT 1'
|
||||||
|
);
|
||||||
|
|
||||||
|
PREPARE stmt FROM @sql;
|
||||||
|
EXECUTE stmt;
|
||||||
|
DEALLOCATE PREPARE stmt;
|
||||||
|
|
||||||
|
-- Application config table migration for adding kr_website_id column
|
||||||
|
SET @column_exists = (SELECT COUNT(*)
|
||||||
|
FROM INFORMATION_SCHEMA.COLUMNS
|
||||||
|
WHERE TABLE_SCHEMA = DATABASE()
|
||||||
|
AND TABLE_NAME = 'application_config'
|
||||||
|
AND COLUMN_NAME = 'kr_website_id');
|
||||||
|
|
||||||
|
SET @sql = IF(@column_exists = 0,
|
||||||
|
'ALTER TABLE `application_config` ADD COLUMN `kr_website_id` VARCHAR(255) NULL DEFAULT NULL COMMENT ''KR Website ID'' AFTER `invitation_link`',
|
||||||
|
'SELECT 1'
|
||||||
|
);
|
||||||
|
|
||||||
|
PREPARE stmt FROM @sql;
|
||||||
|
EXECUTE stmt;
|
||||||
|
DEALLOCATE PREPARE stmt;
|
||||||
|
|
||||||
|
-- migrate:down
|
||||||
|
-- migrations/02004_create_user_device_online_record.down.sql
|
||||||
|
-- Purpose: Drop user device online record table
|
||||||
|
-- Author: PPanel Team, 2025-04-22
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS `user_device_online_record`;
|
||||||
|
|
||||||
|
-- User subscribe table migration for removing finished_at column
|
||||||
|
SET @column_exists = (SELECT COUNT(*)
|
||||||
|
FROM INFORMATION_SCHEMA.COLUMNS
|
||||||
|
WHERE TABLE_SCHEMA = DATABASE()
|
||||||
|
AND TABLE_NAME = 'user_subscribe'
|
||||||
|
AND COLUMN_NAME = 'finished_at');
|
||||||
|
SET @sql = IF(@column_exists > 0,
|
||||||
|
'ALTER TABLE `user_subscribe` DROP COLUMN `finished_at`',
|
||||||
|
'SELECT 1'
|
||||||
|
);
|
||||||
|
PREPARE stmt FROM @sql;
|
||||||
|
EXECUTE stmt;
|
||||||
|
DEALLOCATE PREPARE stmt;
|
||||||
|
|
||||||
|
-- Application config table migration for removing invitation_link column
|
||||||
|
|
||||||
|
SET @column_exists = (SELECT COUNT(*)
|
||||||
|
FROM INFORMATION_SCHEMA.COLUMNS
|
||||||
|
WHERE TABLE_SCHEMA = DATABASE()
|
||||||
|
AND TABLE_NAME = 'application_config'
|
||||||
|
AND COLUMN_NAME = 'invitation_link');
|
||||||
|
|
||||||
|
SET @sql = IF(@column_exists > 0,
|
||||||
|
'ALTER TABLE `application_config` DROP COLUMN `invitation_link`',
|
||||||
|
'SELECT 1'
|
||||||
|
);
|
||||||
|
|
||||||
|
PREPARE stmt FROM @sql;
|
||||||
|
EXECUTE stmt;
|
||||||
|
DEALLOCATE PREPARE stmt;
|
||||||
|
|
||||||
|
-- Application config table migration for removing kr_website_id column
|
||||||
|
SET @column_exists = (SELECT COUNT(*)
|
||||||
|
FROM INFORMATION_SCHEMA.COLUMNS
|
||||||
|
WHERE TABLE_SCHEMA = DATABASE()
|
||||||
|
AND TABLE_NAME = 'application_config'
|
||||||
|
AND COLUMN_NAME = 'kr_website_id');
|
||||||
|
|
||||||
|
SET @sql = IF(@column_exists > 0,
|
||||||
|
'ALTER TABLE `application_config` DROP COLUMN `kr_website_id`',
|
||||||
|
'SELECT 1'
|
||||||
|
);
|
||||||
|
|
||||||
|
PREPARE stmt FROM @sql;
|
||||||
|
EXECUTE stmt;
|
||||||
|
DEALLOCATE PREPARE stmt;
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
-- migrate:up
|
||||||
|
-- migrations/02008_create_user_reset_subscribe_log.up.sql
|
||||||
|
-- Purpose: Create user_reset_subscribe_log table
|
||||||
|
-- Author: PPanel Team, 2025-04-22
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `user_reset_subscribe_log`
|
||||||
|
(
|
||||||
|
`id` BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
`user_id` BIGINT NOT NULL COMMENT 'User ID',
|
||||||
|
`type` TINYINT(1) NOT NULL COMMENT 'Type: 1: Auto 2: Advance 3: Paid',
|
||||||
|
`order_no` VARCHAR(255) DEFAULT NULL COMMENT 'Order No.',
|
||||||
|
`user_subscribe_id` BIGINT NOT NULL COMMENT 'User Subscribe ID',
|
||||||
|
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'Creation Time',
|
||||||
|
INDEX `idx_user_id` (`user_id`),
|
||||||
|
INDEX `idx_user_subscribe_id` (`user_subscribe_id`)
|
||||||
|
) ENGINE = InnoDB
|
||||||
|
DEFAULT CHARSET = utf8mb4
|
||||||
|
COLLATE = utf8mb4_general_ci;
|
||||||
|
|
||||||
|
-- migrate:down
|
||||||
|
-- migrations/02008_create_user_reset_subscribe_log.down.sql
|
||||||
|
-- Purpose: Drop user_reset_subscribe_log table
|
||||||
|
-- Author: PPanel Team, 2025-04-22
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS `user_reset_subscribe_log`;
|
||||||
|
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
-- migrate:up
|
||||||
|
ALTER TABLE `server_rule_group`
|
||||||
|
ADD COLUMN `default` TINYINT(1) NOT NULL DEFAULT 0 COMMENT 'Is Default Group',
|
||||||
|
ADD COLUMN `type` VARCHAR(100) NOT NULL DEFAULT '' COMMENT 'Rule Group Type';
|
||||||
|
-- migrate:down
|
||||||
|
ALTER TABLE `server_rule_group`
|
||||||
|
DROP COLUMN `default`,
|
||||||
|
DROP COLUMN `type`;
|
||||||
|
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
-- migrate:up
|
||||||
|
DROP TABLE IF EXISTS `email_task`;
|
||||||
|
CREATE TABLE `email_task` (
|
||||||
|
`id` bigint NOT NULL AUTO_INCREMENT COMMENT 'ID',
|
||||||
|
`subject` varchar(255) COLLATE utf8mb4_general_ci NOT NULL COMMENT 'Email Subject',
|
||||||
|
`content` text COLLATE utf8mb4_general_ci NOT NULL COMMENT 'Email Content',
|
||||||
|
`recipient` text COLLATE utf8mb4_general_ci NOT NULL COMMENT 'Email Recipient',
|
||||||
|
`scope` varchar(50) COLLATE utf8mb4_general_ci NOT NULL COMMENT 'Email Scope',
|
||||||
|
`register_start_time` datetime(3) DEFAULT NULL COMMENT 'Register Start Time',
|
||||||
|
`register_end_time` datetime(3) DEFAULT NULL COMMENT 'Register End Time',
|
||||||
|
`additional` text COLLATE utf8mb4_general_ci COMMENT 'Additional Information',
|
||||||
|
`scheduled` datetime(3) NOT NULL COMMENT 'Scheduled Time',
|
||||||
|
`interval` tinyint unsigned NOT NULL COMMENT 'Interval in Seconds',
|
||||||
|
`limit` bigint unsigned NOT NULL COMMENT 'Daily send limit',
|
||||||
|
`status` tinyint unsigned NOT NULL COMMENT 'Daily Status',
|
||||||
|
`errors` text COLLATE utf8mb4_general_ci NOT NULL COMMENT 'Errors',
|
||||||
|
`total` bigint unsigned NOT NULL DEFAULT '0' COMMENT 'Total Number',
|
||||||
|
`current` bigint unsigned NOT NULL DEFAULT '0' COMMENT 'Current Number',
|
||||||
|
`created_at` datetime(3) DEFAULT NULL COMMENT 'Creation Time',
|
||||||
|
`updated_at` datetime(3) DEFAULT NULL COMMENT 'Update Time',
|
||||||
|
PRIMARY KEY (`id`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
|
||||||
|
|
||||||
|
SET FOREIGN_KEY_CHECKS = 1;
|
||||||
|
|
||||||
|
-- migrate:down
|
||||||
|
DROP TABLE IF EXISTS `email_task`;
|
||||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,7 @@
|
|||||||
|
-- migrate:up
|
||||||
|
INSERT IGNORE INTO `system` (`id`, `category`, `key`, `value`, `type`, `desc`, `created_at`, `updated_at`)
|
||||||
|
VALUES
|
||||||
|
(42, 'subscribe', 'UserAgentLimit', 'false', 'bool', 'User Agent Limit', '2025-04-22 14:25:16.637', '2025-04-22 14:25:16.637'),
|
||||||
|
(43, 'subscribe', 'UserAgentList', '', 'string', 'User Agent List', '2025-04-22 14:25:16.637','2025-04-22 14:25:16.637');
|
||||||
|
-- migrate:down
|
||||||
|
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
-- migrate:up
|
||||||
|
DROP TABLE IF EXISTS `application`;
|
||||||
|
DROP TABLE IF EXISTS `application_version`;
|
||||||
|
DROP TABLE IF EXISTS `application_config`;
|
||||||
|
-- migrate:down
|
||||||
|
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
-- migrate:up
|
||||||
|
DROP TABLE IF EXISTS `user_balance_log`;
|
||||||
|
DROP TABLE IF EXISTS `user_commission_log`;
|
||||||
|
DROP TABLE IF EXISTS `user_gift_amount_log`;
|
||||||
|
DROP TABLE IF EXISTS `user_login_log`;
|
||||||
|
DROP TABLE IF EXISTS `user_reset_subscribe_log`;
|
||||||
|
DROP TABLE IF EXISTS `user_subscribe_log`;
|
||||||
|
DROP TABLE IF EXISTS `message_log`;
|
||||||
|
DROP TABLE IF EXISTS `system_logs`;
|
||||||
|
CREATE TABLE `system_logs` (
|
||||||
|
`id` bigint NOT NULL AUTO_INCREMENT,
|
||||||
|
`type` tinyint(1) NOT NULL DEFAULT '0' COMMENT 'Log Type: 1: Email Message 2: Mobile Message 3: Subscribe 4: Subscribe Traffic 5: Server Traffic 6: Login 7: Register 8: Balance 9: Commission 10: Reset Subscribe 11: Gift',
|
||||||
|
`date` varchar(20) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT 'Log Date',
|
||||||
|
`object_id` bigint NOT NULL DEFAULT '0' COMMENT 'Object ID',
|
||||||
|
`content` text COLLATE utf8mb4_general_ci NOT NULL COMMENT 'Log Content',
|
||||||
|
`created_at` datetime(3) DEFAULT NULL COMMENT 'Create Time',
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
KEY `idx_type` (`type`),
|
||||||
|
KEY `idx_object_id` (`object_id`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
|
||||||
|
-- migrate:down
|
||||||
|
CREATE TABLE IF NOT EXISTS `user_balance_log`
|
||||||
|
(
|
||||||
|
`id` bigint NOT NULL AUTO_INCREMENT,
|
||||||
|
`user_id` bigint NOT NULL COMMENT 'User ID',
|
||||||
|
`amount` bigint NOT NULL COMMENT 'Amount',
|
||||||
|
`type` tinyint(1) NOT NULL COMMENT 'Type: 1: Recharge 2: Withdraw 3: Payment 4: Refund 5: Reward',
|
||||||
|
`order_id` bigint DEFAULT NULL COMMENT 'Order ID',
|
||||||
|
`balance` bigint NOT NULL COMMENT 'Balance',
|
||||||
|
`created_at` datetime(3) DEFAULT NULL COMMENT 'Creation Time',
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
KEY `idx_user_id` (`user_id`)
|
||||||
|
) ENGINE = InnoDB
|
||||||
|
DEFAULT CHARSET = utf8mb4
|
||||||
|
COLLATE = utf8mb4_general_ci;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `user_commission_log`
|
||||||
|
(
|
||||||
|
`id` bigint NOT NULL AUTO_INCREMENT,
|
||||||
|
`user_id` bigint NOT NULL COMMENT 'User ID',
|
||||||
|
`order_no` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT 'Order No.',
|
||||||
|
`amount` bigint NOT NULL COMMENT 'Amount',
|
||||||
|
`created_at` datetime(3) DEFAULT NULL COMMENT 'Creation Time',
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
KEY `idx_user_id` (`user_id`)
|
||||||
|
) ENGINE = InnoDB
|
||||||
|
DEFAULT CHARSET = utf8mb4
|
||||||
|
COLLATE = utf8mb4_general_ci;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `user_gift_amount_log`
|
||||||
|
(
|
||||||
|
`id` bigint NOT NULL AUTO_INCREMENT,
|
||||||
|
`user_id` bigint NOT NULL COMMENT 'User ID',
|
||||||
|
`user_subscribe_id` bigint DEFAULT NULL COMMENT 'Deduction User Subscribe ID',
|
||||||
|
`order_no` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT 'Order No.',
|
||||||
|
`type` tinyint(1) NOT NULL COMMENT 'Type: 1: Increase 2: Reduce',
|
||||||
|
`amount` bigint NOT NULL COMMENT 'Amount',
|
||||||
|
`balance` bigint NOT NULL COMMENT 'Balance',
|
||||||
|
`remark` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT '' COMMENT 'Remark',
|
||||||
|
`created_at` datetime(3) DEFAULT NULL COMMENT 'Creation Time',
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
KEY `idx_user_id` (`user_id`)
|
||||||
|
) ENGINE = InnoDB
|
||||||
|
DEFAULT CHARSET = utf8mb4
|
||||||
|
COLLATE = utf8mb4_general_ci;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `user_login_log`
|
||||||
|
(
|
||||||
|
`id` bigint NOT NULL AUTO_INCREMENT,
|
||||||
|
`user_id` bigint NOT NULL COMMENT 'User ID',
|
||||||
|
`login_ip` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT 'Login IP',
|
||||||
|
`user_agent` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT 'UserAgent',
|
||||||
|
`success` tinyint(1) NOT NULL DEFAULT '0' COMMENT 'Login Success',
|
||||||
|
`created_at` datetime(3) DEFAULT NULL COMMENT 'Creation Time',
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
KEY `idx_user_id` (`user_id`)
|
||||||
|
) ENGINE = InnoDB
|
||||||
|
DEFAULT CHARSET = utf8mb4
|
||||||
|
COLLATE = utf8mb4_general_ci;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `user_reset_subscribe_log`
|
||||||
|
(
|
||||||
|
`id` BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
`user_id` BIGINT NOT NULL COMMENT 'User ID',
|
||||||
|
`type` TINYINT(1) NOT NULL COMMENT 'Type: 1: Auto 2: Advance 3: Paid',
|
||||||
|
`order_no` VARCHAR(255) DEFAULT NULL COMMENT 'Order No.',
|
||||||
|
`user_subscribe_id` BIGINT NOT NULL COMMENT 'User Subscribe ID',
|
||||||
|
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'Creation Time',
|
||||||
|
INDEX `idx_user_id` (`user_id`),
|
||||||
|
INDEX `idx_user_subscribe_id` (`user_subscribe_id`)
|
||||||
|
) ENGINE = InnoDB
|
||||||
|
DEFAULT CHARSET = utf8mb4
|
||||||
|
COLLATE = utf8mb4_general_ci;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `user_subscribe_log`
|
||||||
|
(
|
||||||
|
`id` bigint NOT NULL AUTO_INCREMENT,
|
||||||
|
`user_id` bigint NOT NULL COMMENT 'User ID',
|
||||||
|
`user_subscribe_id` bigint NOT NULL COMMENT 'User Subscribe ID',
|
||||||
|
`token` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT 'Token',
|
||||||
|
`ip` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT 'IP',
|
||||||
|
`user_agent` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT 'UserAgent',
|
||||||
|
`created_at` datetime(3) DEFAULT NULL COMMENT 'Creation Time',
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
KEY `idx_user_id` (`user_id`),
|
||||||
|
KEY `idx_user_subscribe_id` (`user_subscribe_id`)
|
||||||
|
) ENGINE = InnoDB
|
||||||
|
DEFAULT CHARSET = utf8mb4
|
||||||
|
COLLATE = utf8mb4_general_ci;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `message_log`
|
||||||
|
(
|
||||||
|
`id` bigint NOT NULL AUTO_INCREMENT,
|
||||||
|
`type` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT 'email' COMMENT 'Message Type',
|
||||||
|
`platform` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT 'smtp' COMMENT 'Platform',
|
||||||
|
`to` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT 'To',
|
||||||
|
`subject` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Subject',
|
||||||
|
`content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci COMMENT 'Content',
|
||||||
|
`status` tinyint(1) NOT NULL DEFAULT '0' COMMENT 'Status',
|
||||||
|
`created_at` datetime(3) DEFAULT NULL COMMENT 'Create Time',
|
||||||
|
`updated_at` datetime(3) DEFAULT NULL COMMENT 'Update Time',
|
||||||
|
PRIMARY KEY (`id`)
|
||||||
|
) ENGINE = InnoDB
|
||||||
|
DEFAULT CHARSET = utf8mb4
|
||||||
|
COLLATE = utf8mb4_general_ci;
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS `system_logs`;
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
-- migrate:up
|
||||||
|
CREATE TABLE IF NOT EXISTS `servers` (
|
||||||
|
`id` bigint NOT NULL AUTO_INCREMENT,
|
||||||
|
`name` varchar(100) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Server Name',
|
||||||
|
`country` varchar(128) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Country',
|
||||||
|
`city` varchar(128) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'City',
|
||||||
|
`ratio` decimal(4,2) NOT NULL DEFAULT '0.00' COMMENT 'Traffic Ratio',
|
||||||
|
`address` varchar(100) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Server Address',
|
||||||
|
`sort` bigint NOT NULL DEFAULT '0' COMMENT 'Sort',
|
||||||
|
`protocols` text COLLATE utf8mb4_general_ci COMMENT 'Protocol',
|
||||||
|
`last_reported_at` datetime(3) DEFAULT NULL COMMENT 'Last Reported Time',
|
||||||
|
`created_at` datetime(3) DEFAULT NULL COMMENT 'Creation Time',
|
||||||
|
`updated_at` datetime(3) DEFAULT NULL COMMENT 'Update Time',
|
||||||
|
PRIMARY KEY (`id`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `nodes` (
|
||||||
|
`id` bigint NOT NULL AUTO_INCREMENT,
|
||||||
|
`name` varchar(100) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Node Name',
|
||||||
|
`tags` varchar(255) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Tags',
|
||||||
|
`port` smallint unsigned NOT NULL DEFAULT '0' COMMENT 'Connect Port',
|
||||||
|
`address` varchar(255) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Connect Address',
|
||||||
|
`server_id` bigint NOT NULL DEFAULT '0' COMMENT 'Server ID',
|
||||||
|
`protocol` varchar(100) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Protocol',
|
||||||
|
`enabled` tinyint(1) NOT NULL DEFAULT '1' COMMENT 'Enabled',
|
||||||
|
`created_at` datetime(3) DEFAULT NULL COMMENT 'Creation Time',
|
||||||
|
`updated_at` datetime(3) DEFAULT NULL COMMENT 'Update Time',
|
||||||
|
PRIMARY KEY (`id`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
|
||||||
|
|
||||||
|
-- migrate:down
|
||||||
|
DROP TABLE IF EXISTS `nodes`;
|
||||||
|
DROP TABLE IF EXISTS `servers`;
|
||||||
|
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
-- migrate:up
|
||||||
|
ALTER TABLE `subscribe`
|
||||||
|
ADD COLUMN `nodes` VARCHAR(255) NOT NULL DEFAULT '' COMMENT 'Node IDs',
|
||||||
|
ADD COLUMN `node_tags` VARCHAR(255) NOT NULL DEFAULT '' COMMENT 'Node Tags',
|
||||||
|
DROP COLUMN `server`,
|
||||||
|
DROP COLUMN `server_group`;
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS `server_rule_group`;
|
||||||
|
|
||||||
|
-- migrate:down
|
||||||
|
ALTER TABLE `subscribe`
|
||||||
|
DROP COLUMN `nodes`,
|
||||||
|
DROP COLUMN `node_tags`,
|
||||||
|
ADD COLUMN `server` VARCHAR(255) NOT NULL DEFAULT '' COMMENT 'Server',
|
||||||
|
ADD COLUMN `server_group` VARCHAR(255) NOT NULL DEFAULT '' COMMENT 'Server Group';
|
||||||
|
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
-- migrate:up
|
||||||
|
INSERT IGNORE INTO `system` (`category`, `key`, `value`, `type`, `desc`, `created_at`, `updated_at`)
|
||||||
|
VALUES
|
||||||
|
('log', 'AutoClear', 'true', 'bool', 'Auto Clear Log', '2025-04-22 14:25:16.637', '2025-04-22 14:25:16.637'),
|
||||||
|
('log', 'ClearDays', '7', 'int', 'Clear Days', '2025-04-22 14:25:16.637','2025-04-22 14:25:16.637');
|
||||||
|
-- migrate:down
|
||||||
|
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
-- migrate:up
|
||||||
|
ALTER TABLE `user`
|
||||||
|
ADD COLUMN `referral_percentage` TINYINT UNSIGNED NOT NULL DEFAULT 0
|
||||||
|
COMMENT 'Referral Percentage'
|
||||||
|
AFTER `commission`,
|
||||||
|
ADD COLUMN `only_first_purchase` TINYINT(1) NOT NULL DEFAULT 1
|
||||||
|
COMMENT 'Only First Purchase'
|
||||||
|
AFTER `referral_percentage`;
|
||||||
|
|
||||||
|
-- migrate:down
|
||||||
|
ALTER TABLE `user`
|
||||||
|
DROP COLUMN `referral_percentage`,
|
||||||
|
DROP COLUMN `only_first_purchase`;
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
-- migrate:up
|
||||||
|
ALTER TABLE `nodes`
|
||||||
|
ADD COLUMN `sort` INT UNSIGNED NOT NULL DEFAULT 0
|
||||||
|
COMMENT 'Sort' AFTER `enabled`;
|
||||||
|
-- migrate:down
|
||||||
|
ALTER TABLE `nodes`
|
||||||
|
DROP COLUMN `sort`;
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
-- migrate:up
|
||||||
|
CREATE INDEX idx_traffic_log_time_user_sub ON traffic_log (timestamp, user_id, subscribe_id);
|
||||||
|
|
||||||
|
-- migrate:down
|
||||||
|
DROP INDEX idx_traffic_log_time_user_sub ON traffic_log;
|
||||||
|
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
-- migrate:up
|
||||||
|
DROP TABLE IF EXISTS `subscribe_type`;
|
||||||
|
DROP TABLE IF EXISTS `sms`;
|
||||||
|
-- migrate:down
|
||||||
|
DROP TABLE IF EXISTS `subscribe_type`;
|
||||||
|
DROP TABLE IF EXISTS `sms`;
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
-- migrate:up
|
||||||
|
ALTER TABLE `subscribe`
|
||||||
|
DROP COLUMN `group_id`,
|
||||||
|
ADD COLUMN `language` VARCHAR(255) NOT NULL DEFAULT ''
|
||||||
|
COMMENT 'Language'
|
||||||
|
AFTER `name`;
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS `subscribe_group`;
|
||||||
|
-- migrate:down
|
||||||
|
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
-- migrate:up
|
||||||
|
DROP TABLE IF EXISTS `email_task`;
|
||||||
|
CREATE TABLE `task` (
|
||||||
|
`id` bigint NOT NULL AUTO_INCREMENT COMMENT 'ID',
|
||||||
|
`type` tinyint NOT NULL COMMENT 'Task Type',
|
||||||
|
`scope` text COLLATE utf8mb4_general_ci COMMENT 'Task Scope',
|
||||||
|
`content` text COLLATE utf8mb4_general_ci COMMENT 'Task Content',
|
||||||
|
`status` tinyint NOT NULL DEFAULT '0' COMMENT 'Task Status: 0: Pending, 1: In Progress, 2: Completed, 3: Failed',
|
||||||
|
`errors` text COLLATE utf8mb4_general_ci COMMENT 'Task Errors',
|
||||||
|
`total` bigint unsigned NOT NULL DEFAULT '0' COMMENT 'Total Number',
|
||||||
|
`current` bigint unsigned NOT NULL DEFAULT '0' COMMENT 'Current Number',
|
||||||
|
`created_at` datetime(3) DEFAULT NULL COMMENT 'Creation Time',
|
||||||
|
`updated_at` datetime(3) DEFAULT NULL COMMENT 'Update Time',
|
||||||
|
PRIMARY KEY (`id`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
|
||||||
|
-- migrate:down
|
||||||
|
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
-- migrate:up
|
||||||
|
INSERT
|
||||||
|
IGNORE INTO `system` (`category`, `key`, `value`, `type`, `desc`, `created_at`, `updated_at`)
|
||||||
|
VALUE
|
||||||
|
('server', 'TrafficReportThreshold', '0', 'int', 'Traffic report threshold', '2025-04-22 14:25:16.637','2025-04-22 14:25:16.637'),
|
||||||
|
('server', 'IPStrategy', '', 'string', 'IP Strategy', '2025-04-22 14:25:16.637','2025-04-22 14:25:16.637'),
|
||||||
|
('server', 'DNS', '', 'string', 'DNS', '2025-04-22 14:25:16.637','2025-04-22 14:25:16.637'),
|
||||||
|
('server', 'Block', '', 'string', 'Block', '2025-04-22 14:25:16.637','2025-04-22 14:25:16.637'),
|
||||||
|
('server', 'Outbound', '', 'string', 'Proxy Outbound', '2025-04-22 14:25:16.637','2025-04-22 14:25:16.637');
|
||||||
|
-- migrate:down
|
||||||
|
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
-- migrate:up
|
||||||
|
-- 只有当 ads 表中不存在 description 字段时才添加
|
||||||
|
SET
|
||||||
|
@col_exists := (
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM INFORMATION_SCHEMA.COLUMNS
|
||||||
|
WHERE TABLE_SCHEMA = DATABASE()
|
||||||
|
AND TABLE_NAME = 'ads'
|
||||||
|
AND COLUMN_NAME = 'description'
|
||||||
|
);
|
||||||
|
|
||||||
|
SET
|
||||||
|
@query := IF(
|
||||||
|
@col_exists = 0,
|
||||||
|
'ALTER TABLE `ads` ADD COLUMN `description` VARCHAR(255) DEFAULT '''' COMMENT ''Description'';',
|
||||||
|
'SELECT "Column `description` already exists"'
|
||||||
|
);
|
||||||
|
|
||||||
|
PREPARE stmt FROM @query;
|
||||||
|
EXECUTE stmt;
|
||||||
|
DEALLOCATE PREPARE stmt;
|
||||||
|
|
||||||
|
-- migrate:down
|
||||||
|
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
-- migrate:up
|
||||||
|
-- 添加 algo 列(如果不存在)
|
||||||
|
SET @dbname = DATABASE();
|
||||||
|
SET @tablename = 'user';
|
||||||
|
SET @colname = 'algo';
|
||||||
|
SET @sql = (
|
||||||
|
SELECT IF(
|
||||||
|
COUNT(*) = 0,
|
||||||
|
'ALTER TABLE `user` ADD COLUMN `algo` VARCHAR(20) NOT NULL DEFAULT ''default'' COMMENT ''Encryption Algorithm'' AFTER `password`;',
|
||||||
|
'SELECT "Column `algo` already exists";'
|
||||||
|
)
|
||||||
|
FROM information_schema.COLUMNS
|
||||||
|
WHERE TABLE_SCHEMA = @dbname
|
||||||
|
AND TABLE_NAME = @tablename
|
||||||
|
AND COLUMN_NAME = @colname
|
||||||
|
);
|
||||||
|
PREPARE stmt FROM @sql;
|
||||||
|
EXECUTE stmt;
|
||||||
|
DEALLOCATE PREPARE stmt;
|
||||||
|
|
||||||
|
-- 添加 salt 列(如果不存在)
|
||||||
|
SET @colname = 'salt';
|
||||||
|
SET @sql = (
|
||||||
|
SELECT IF(
|
||||||
|
COUNT(*) = 0,
|
||||||
|
'ALTER TABLE `user` ADD COLUMN `salt` VARCHAR(20) NOT NULL DEFAULT ''default'' COMMENT ''Password Salt'' AFTER `algo`;',
|
||||||
|
'SELECT "Column `salt` already exists";'
|
||||||
|
)
|
||||||
|
FROM information_schema.COLUMNS
|
||||||
|
WHERE TABLE_SCHEMA = @dbname
|
||||||
|
AND TABLE_NAME = @tablename
|
||||||
|
AND COLUMN_NAME = @colname
|
||||||
|
);
|
||||||
|
PREPARE stmt FROM @sql;
|
||||||
|
EXECUTE stmt;
|
||||||
|
DEALLOCATE PREPARE stmt;
|
||||||
|
|
||||||
|
-- migrate:down
|
||||||
|
ALTER TABLE `user`
|
||||||
|
DROP COLUMN `algo`,
|
||||||
|
DROP COLUMN `salt`;
|
||||||
|
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
-- migrate:up
|
||||||
|
INSERT INTO `system` (`category`, `key`, `value`, `type`, `desc`, `created_at`, `updated_at`)
|
||||||
|
SELECT 'site', 'CustomData', '{
|
||||||
|
"kr_website_id": ""
|
||||||
|
}', 'string', 'Custom Data', '2025-04-22 14:25:16.637', '2025-10-14 15:47:19.187'
|
||||||
|
WHERE NOT EXISTS (
|
||||||
|
SELECT 1 FROM `system` WHERE `category` = 'site' AND `key` = 'CustomData'
|
||||||
|
);
|
||||||
|
|
||||||
|
-- migrate:down
|
||||||
|
INSERT INTO `system` (`category`, `key`, `value`, `type`, `desc`, `created_at`, `updated_at`)
|
||||||
|
SELECT 'site', 'CustomData', '{
|
||||||
|
"kr_website_id": ""
|
||||||
|
}', 'string', 'Custom Data', '2025-04-22 14:25:16.637', '2025-10-14 15:47:19.187'
|
||||||
|
WHERE NOT EXISTS (
|
||||||
|
SELECT 1 FROM `system` WHERE `category` = 'site' AND `key` = 'CustomData'
|
||||||
|
);
|
||||||
|
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
-- migrate:up
|
||||||
|
ALTER TABLE traffic_log ADD INDEX idx_timestamp (timestamp);
|
||||||
|
|
||||||
|
-- migrate:down
|
||||||
|
ALTER TABLE traffic_log DROP INDEX idx_timestamp;
|
||||||
|
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
-- migrate:up
|
||||||
|
ALTER TABLE `user_subscribe`
|
||||||
|
ADD COLUMN `note` VARCHAR(500) NOT NULL DEFAULT ''
|
||||||
|
COMMENT 'User note for subscription'
|
||||||
|
AFTER `status`;
|
||||||
|
|
||||||
|
-- migrate:down
|
||||||
|
ALTER TABLE `user_subscribe`
|
||||||
|
DROP COLUMN `note`;
|
||||||
|
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
-- migrate:up
|
||||||
|
ALTER TABLE `user`
|
||||||
|
ADD COLUMN `rules` TEXT NULL
|
||||||
|
COMMENT 'User rules for subscription'
|
||||||
|
AFTER `created_at`;
|
||||||
|
|
||||||
|
-- migrate:down
|
||||||
|
ALTER TABLE `user`
|
||||||
|
DROP COLUMN IF EXISTS `rules`;
|
||||||
|
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
-- migrate:up
|
||||||
|
CREATE TABLE IF NOT EXISTS `withdrawals` (
|
||||||
|
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT 'Primary Key',
|
||||||
|
`user_id` BIGINT NOT NULL COMMENT 'User ID',
|
||||||
|
`amount` BIGINT NOT NULL COMMENT 'Withdrawal Amount',
|
||||||
|
`content` TEXT COMMENT 'Withdrawal Content',
|
||||||
|
`status` TINYINT(1) NOT NULL DEFAULT 0 COMMENT 'Withdrawal Status',
|
||||||
|
`reason` VARCHAR(500) NOT NULL DEFAULT '' COMMENT 'Rejection Reason',
|
||||||
|
`created_at` DATETIME NOT NULL COMMENT 'Creation Time',
|
||||||
|
`updated_at` DATETIME NOT NULL COMMENT 'Update Time',
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
KEY `idx_user_id` (`user_id`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
|
INSERT IGNORE INTO `system` (`category`, `key`, `value`, `type`, `desc`, `created_at`, `updated_at`)
|
||||||
|
VALUES
|
||||||
|
('invite', 'WithdrawalMethod', '', 'string', 'withdrawal method', '2025-04-22 14:25:16.637', '2025-04-22 14:25:16.637');
|
||||||
|
-- migrate:down
|
||||||
|
DROP TABLE IF EXISTS `withdrawals`;
|
||||||
|
|
||||||
|
DELETE FROM `system`
|
||||||
|
WHERE `category` = 'invite'
|
||||||
|
AND `key` = 'WithdrawalMethod';
|
||||||
|
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
-- migrate:up
|
||||||
|
DROP TABLE IF EXISTS `server`;
|
||||||
|
-- migrate:down
|
||||||
|
CREATE TABLE IF NOT EXISTS `server`
|
||||||
|
(
|
||||||
|
`id` bigint NOT NULL AUTO_INCREMENT,
|
||||||
|
`name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Node Name',
|
||||||
|
`tags` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Tags',
|
||||||
|
`country` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Country',
|
||||||
|
`city` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'City',
|
||||||
|
`latitude` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'latitude',
|
||||||
|
`longitude` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'longitude',
|
||||||
|
`server_addr` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Server Address',
|
||||||
|
`relay_mode` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT 'none' COMMENT 'Relay Mode',
|
||||||
|
`relay_node` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci COMMENT 'Relay Node',
|
||||||
|
`speed_limit` bigint NOT NULL DEFAULT '0' COMMENT 'Speed Limit',
|
||||||
|
`traffic_ratio` decimal(4, 2) NOT NULL DEFAULT '0.00' COMMENT 'Traffic Ratio',
|
||||||
|
`group_id` bigint DEFAULT NULL COMMENT 'Group ID',
|
||||||
|
`protocol` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Protocol',
|
||||||
|
`config` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci COMMENT 'Config',
|
||||||
|
`enable` tinyint(1) NOT NULL DEFAULT '1' COMMENT 'Enabled',
|
||||||
|
`sort` bigint NOT NULL DEFAULT '0' COMMENT 'Sort',
|
||||||
|
`last_reported_at` datetime(3) DEFAULT NULL COMMENT 'Last Reported Time',
|
||||||
|
`created_at` datetime(3) DEFAULT NULL COMMENT 'Creation Time',
|
||||||
|
`updated_at` datetime(3) DEFAULT NULL COMMENT 'Update Time',
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
KEY `idx_group_id` (`group_id`)
|
||||||
|
) ENGINE = InnoDB
|
||||||
|
DEFAULT CHARSET = utf8mb4
|
||||||
|
COLLATE = utf8mb4_general_ci;
|
||||||
|
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
-- migrate:up
|
||||||
|
ALTER TABLE `subscribe`
|
||||||
|
ADD COLUMN `show_original_price` TINYINT(1) NOT NULL DEFAULT 0 COMMENT 'display the original price: 0 not display, 1 display' AFTER `created_at`;
|
||||||
|
|
||||||
|
-- migrate:down
|
||||||
|
ALTER TABLE `subscribe`
|
||||||
|
DROP COLUMN `show_original_price`;
|
||||||
|
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
-- migrate:up
|
||||||
|
DROP TABLE IF EXISTS `server_group`;
|
||||||
|
-- migrate:down
|
||||||
|
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
-- migrate:up
|
||||||
|
-- Update the `subscribe` table to set `inventory` to -1 where it is currently 0
|
||||||
|
UPDATE `subscribe`
|
||||||
|
SET `inventory` = -1
|
||||||
|
WHERE `inventory` = 0;
|
||||||
|
-- migrate:down
|
||||||
|
|
||||||
|
-- This migration script reverts the inventory values in the 'subscribe' table
|
||||||
|
UPDATE `subscribe`
|
||||||
|
SET `inventory` = 0
|
||||||
|
WHERE `inventory` = -1;
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
-- migrate:up
|
||||||
|
CREATE INDEX idx_type_date ON system_logs (type, date);
|
||||||
|
|
||||||
|
-- migrate:down
|
||||||
|
DROP INDEX idx_type_date ON system_logs;
|
||||||
|
|
||||||
@@ -0,0 +1,266 @@
|
|||||||
|
-- migrate:up
|
||||||
|
SET @index_exists = (SELECT COUNT(1)
|
||||||
|
FROM INFORMATION_SCHEMA.STATISTICS
|
||||||
|
WHERE TABLE_SCHEMA = DATABASE()
|
||||||
|
AND TABLE_NAME = 'order'
|
||||||
|
AND INDEX_NAME = 'idx_order_trade_no');
|
||||||
|
SET @sql = IF(@index_exists = 0,
|
||||||
|
'ALTER TABLE `order` ADD INDEX `idx_order_trade_no` (`trade_no`(191))',
|
||||||
|
'SELECT 1');
|
||||||
|
PREPARE stmt FROM @sql;
|
||||||
|
EXECUTE stmt;
|
||||||
|
DEALLOCATE PREPARE stmt;
|
||||||
|
|
||||||
|
SET @index_exists = (SELECT COUNT(1)
|
||||||
|
FROM INFORMATION_SCHEMA.STATISTICS
|
||||||
|
WHERE TABLE_SCHEMA = DATABASE()
|
||||||
|
AND TABLE_NAME = 'order'
|
||||||
|
AND INDEX_NAME = 'idx_order_coupon');
|
||||||
|
SET @sql = IF(@index_exists = 0,
|
||||||
|
'ALTER TABLE `order` ADD INDEX `idx_order_coupon` (`coupon`(191))',
|
||||||
|
'SELECT 1');
|
||||||
|
PREPARE stmt FROM @sql;
|
||||||
|
EXECUTE stmt;
|
||||||
|
DEALLOCATE PREPARE stmt;
|
||||||
|
|
||||||
|
SET @index_exists = (SELECT COUNT(1)
|
||||||
|
FROM INFORMATION_SCHEMA.STATISTICS
|
||||||
|
WHERE TABLE_SCHEMA = DATABASE()
|
||||||
|
AND TABLE_NAME = 'user'
|
||||||
|
AND INDEX_NAME = 'idx_user_refer_code');
|
||||||
|
SET @sql = IF(@index_exists = 0,
|
||||||
|
'ALTER TABLE `user` ADD INDEX `idx_user_refer_code` (`refer_code`)',
|
||||||
|
'SELECT 1');
|
||||||
|
PREPARE stmt FROM @sql;
|
||||||
|
EXECUTE stmt;
|
||||||
|
DEALLOCATE PREPARE stmt;
|
||||||
|
|
||||||
|
SET @index_exists = (SELECT COUNT(1)
|
||||||
|
FROM INFORMATION_SCHEMA.STATISTICS
|
||||||
|
WHERE TABLE_SCHEMA = DATABASE()
|
||||||
|
AND TABLE_NAME = 'coupon'
|
||||||
|
AND INDEX_NAME = 'idx_coupon_name');
|
||||||
|
SET @sql = IF(@index_exists = 0,
|
||||||
|
'ALTER TABLE `coupon` ADD INDEX `idx_coupon_name` (`name`(191))',
|
||||||
|
'SELECT 1');
|
||||||
|
PREPARE stmt FROM @sql;
|
||||||
|
EXECUTE stmt;
|
||||||
|
DEALLOCATE PREPARE stmt;
|
||||||
|
|
||||||
|
SET @index_exists = (SELECT COUNT(1)
|
||||||
|
FROM INFORMATION_SCHEMA.STATISTICS
|
||||||
|
WHERE TABLE_SCHEMA = DATABASE()
|
||||||
|
AND TABLE_NAME = 'payment'
|
||||||
|
AND INDEX_NAME = 'idx_payment_name');
|
||||||
|
SET @sql = IF(@index_exists = 0,
|
||||||
|
'ALTER TABLE `payment` ADD INDEX `idx_payment_name` (`name`)',
|
||||||
|
'SELECT 1');
|
||||||
|
PREPARE stmt FROM @sql;
|
||||||
|
EXECUTE stmt;
|
||||||
|
DEALLOCATE PREPARE stmt;
|
||||||
|
|
||||||
|
SET @index_exists = (SELECT COUNT(1)
|
||||||
|
FROM INFORMATION_SCHEMA.STATISTICS
|
||||||
|
WHERE TABLE_SCHEMA = DATABASE()
|
||||||
|
AND TABLE_NAME = 'servers'
|
||||||
|
AND INDEX_NAME = 'idx_servers_name');
|
||||||
|
SET @sql = IF(@index_exists = 0,
|
||||||
|
'ALTER TABLE `servers` ADD INDEX `idx_servers_name` (`name`)',
|
||||||
|
'SELECT 1');
|
||||||
|
PREPARE stmt FROM @sql;
|
||||||
|
EXECUTE stmt;
|
||||||
|
DEALLOCATE PREPARE stmt;
|
||||||
|
|
||||||
|
SET @index_exists = (SELECT COUNT(1)
|
||||||
|
FROM INFORMATION_SCHEMA.STATISTICS
|
||||||
|
WHERE TABLE_SCHEMA = DATABASE()
|
||||||
|
AND TABLE_NAME = 'servers'
|
||||||
|
AND INDEX_NAME = 'idx_servers_address');
|
||||||
|
SET @sql = IF(@index_exists = 0,
|
||||||
|
'ALTER TABLE `servers` ADD INDEX `idx_servers_address` (`address`)',
|
||||||
|
'SELECT 1');
|
||||||
|
PREPARE stmt FROM @sql;
|
||||||
|
EXECUTE stmt;
|
||||||
|
DEALLOCATE PREPARE stmt;
|
||||||
|
|
||||||
|
SET @index_exists = (SELECT COUNT(1)
|
||||||
|
FROM INFORMATION_SCHEMA.STATISTICS
|
||||||
|
WHERE TABLE_SCHEMA = DATABASE()
|
||||||
|
AND TABLE_NAME = 'nodes'
|
||||||
|
AND INDEX_NAME = 'idx_nodes_name');
|
||||||
|
SET @sql = IF(@index_exists = 0,
|
||||||
|
'ALTER TABLE `nodes` ADD INDEX `idx_nodes_name` (`name`)',
|
||||||
|
'SELECT 1');
|
||||||
|
PREPARE stmt FROM @sql;
|
||||||
|
EXECUTE stmt;
|
||||||
|
DEALLOCATE PREPARE stmt;
|
||||||
|
|
||||||
|
SET @index_exists = (SELECT COUNT(1)
|
||||||
|
FROM INFORMATION_SCHEMA.STATISTICS
|
||||||
|
WHERE TABLE_SCHEMA = DATABASE()
|
||||||
|
AND TABLE_NAME = 'nodes'
|
||||||
|
AND INDEX_NAME = 'idx_nodes_address');
|
||||||
|
SET @sql = IF(@index_exists = 0,
|
||||||
|
'ALTER TABLE `nodes` ADD INDEX `idx_nodes_address` (`address`(191))',
|
||||||
|
'SELECT 1');
|
||||||
|
PREPARE stmt FROM @sql;
|
||||||
|
EXECUTE stmt;
|
||||||
|
DEALLOCATE PREPARE stmt;
|
||||||
|
|
||||||
|
SET @index_exists = (SELECT COUNT(1)
|
||||||
|
FROM INFORMATION_SCHEMA.STATISTICS
|
||||||
|
WHERE TABLE_SCHEMA = DATABASE()
|
||||||
|
AND TABLE_NAME = 'nodes'
|
||||||
|
AND INDEX_NAME = 'idx_nodes_tags');
|
||||||
|
SET @sql = IF(@index_exists = 0,
|
||||||
|
'ALTER TABLE `nodes` ADD INDEX `idx_nodes_tags` (`tags`(191))',
|
||||||
|
'SELECT 1');
|
||||||
|
PREPARE stmt FROM @sql;
|
||||||
|
EXECUTE stmt;
|
||||||
|
DEALLOCATE PREPARE stmt;
|
||||||
|
|
||||||
|
SET @index_exists = (SELECT COUNT(1)
|
||||||
|
FROM INFORMATION_SCHEMA.STATISTICS
|
||||||
|
WHERE TABLE_SCHEMA = DATABASE()
|
||||||
|
AND TABLE_NAME = 'nodes'
|
||||||
|
AND INDEX_NAME = 'idx_nodes_port');
|
||||||
|
SET @sql = IF(@index_exists = 0,
|
||||||
|
'ALTER TABLE `nodes` ADD INDEX `idx_nodes_port` (`port`)',
|
||||||
|
'SELECT 1');
|
||||||
|
PREPARE stmt FROM @sql;
|
||||||
|
EXECUTE stmt;
|
||||||
|
DEALLOCATE PREPARE stmt;
|
||||||
|
|
||||||
|
-- migrate:down
|
||||||
|
SET @index_exists = (SELECT COUNT(1)
|
||||||
|
FROM INFORMATION_SCHEMA.STATISTICS
|
||||||
|
WHERE TABLE_SCHEMA = DATABASE()
|
||||||
|
AND TABLE_NAME = 'nodes'
|
||||||
|
AND INDEX_NAME = 'idx_nodes_port');
|
||||||
|
SET @sql = IF(@index_exists > 0,
|
||||||
|
'ALTER TABLE `nodes` DROP INDEX `idx_nodes_port`',
|
||||||
|
'SELECT 1');
|
||||||
|
PREPARE stmt FROM @sql;
|
||||||
|
EXECUTE stmt;
|
||||||
|
DEALLOCATE PREPARE stmt;
|
||||||
|
|
||||||
|
SET @index_exists = (SELECT COUNT(1)
|
||||||
|
FROM INFORMATION_SCHEMA.STATISTICS
|
||||||
|
WHERE TABLE_SCHEMA = DATABASE()
|
||||||
|
AND TABLE_NAME = 'nodes'
|
||||||
|
AND INDEX_NAME = 'idx_nodes_tags');
|
||||||
|
SET @sql = IF(@index_exists > 0,
|
||||||
|
'ALTER TABLE `nodes` DROP INDEX `idx_nodes_tags`',
|
||||||
|
'SELECT 1');
|
||||||
|
PREPARE stmt FROM @sql;
|
||||||
|
EXECUTE stmt;
|
||||||
|
DEALLOCATE PREPARE stmt;
|
||||||
|
|
||||||
|
SET @index_exists = (SELECT COUNT(1)
|
||||||
|
FROM INFORMATION_SCHEMA.STATISTICS
|
||||||
|
WHERE TABLE_SCHEMA = DATABASE()
|
||||||
|
AND TABLE_NAME = 'nodes'
|
||||||
|
AND INDEX_NAME = 'idx_nodes_address');
|
||||||
|
SET @sql = IF(@index_exists > 0,
|
||||||
|
'ALTER TABLE `nodes` DROP INDEX `idx_nodes_address`',
|
||||||
|
'SELECT 1');
|
||||||
|
PREPARE stmt FROM @sql;
|
||||||
|
EXECUTE stmt;
|
||||||
|
DEALLOCATE PREPARE stmt;
|
||||||
|
|
||||||
|
SET @index_exists = (SELECT COUNT(1)
|
||||||
|
FROM INFORMATION_SCHEMA.STATISTICS
|
||||||
|
WHERE TABLE_SCHEMA = DATABASE()
|
||||||
|
AND TABLE_NAME = 'nodes'
|
||||||
|
AND INDEX_NAME = 'idx_nodes_name');
|
||||||
|
SET @sql = IF(@index_exists > 0,
|
||||||
|
'ALTER TABLE `nodes` DROP INDEX `idx_nodes_name`',
|
||||||
|
'SELECT 1');
|
||||||
|
PREPARE stmt FROM @sql;
|
||||||
|
EXECUTE stmt;
|
||||||
|
DEALLOCATE PREPARE stmt;
|
||||||
|
|
||||||
|
SET @index_exists = (SELECT COUNT(1)
|
||||||
|
FROM INFORMATION_SCHEMA.STATISTICS
|
||||||
|
WHERE TABLE_SCHEMA = DATABASE()
|
||||||
|
AND TABLE_NAME = 'servers'
|
||||||
|
AND INDEX_NAME = 'idx_servers_address');
|
||||||
|
SET @sql = IF(@index_exists > 0,
|
||||||
|
'ALTER TABLE `servers` DROP INDEX `idx_servers_address`',
|
||||||
|
'SELECT 1');
|
||||||
|
PREPARE stmt FROM @sql;
|
||||||
|
EXECUTE stmt;
|
||||||
|
DEALLOCATE PREPARE stmt;
|
||||||
|
|
||||||
|
SET @index_exists = (SELECT COUNT(1)
|
||||||
|
FROM INFORMATION_SCHEMA.STATISTICS
|
||||||
|
WHERE TABLE_SCHEMA = DATABASE()
|
||||||
|
AND TABLE_NAME = 'servers'
|
||||||
|
AND INDEX_NAME = 'idx_servers_name');
|
||||||
|
SET @sql = IF(@index_exists > 0,
|
||||||
|
'ALTER TABLE `servers` DROP INDEX `idx_servers_name`',
|
||||||
|
'SELECT 1');
|
||||||
|
PREPARE stmt FROM @sql;
|
||||||
|
EXECUTE stmt;
|
||||||
|
DEALLOCATE PREPARE stmt;
|
||||||
|
|
||||||
|
SET @index_exists = (SELECT COUNT(1)
|
||||||
|
FROM INFORMATION_SCHEMA.STATISTICS
|
||||||
|
WHERE TABLE_SCHEMA = DATABASE()
|
||||||
|
AND TABLE_NAME = 'payment'
|
||||||
|
AND INDEX_NAME = 'idx_payment_name');
|
||||||
|
SET @sql = IF(@index_exists > 0,
|
||||||
|
'ALTER TABLE `payment` DROP INDEX `idx_payment_name`',
|
||||||
|
'SELECT 1');
|
||||||
|
PREPARE stmt FROM @sql;
|
||||||
|
EXECUTE stmt;
|
||||||
|
DEALLOCATE PREPARE stmt;
|
||||||
|
|
||||||
|
SET @index_exists = (SELECT COUNT(1)
|
||||||
|
FROM INFORMATION_SCHEMA.STATISTICS
|
||||||
|
WHERE TABLE_SCHEMA = DATABASE()
|
||||||
|
AND TABLE_NAME = 'coupon'
|
||||||
|
AND INDEX_NAME = 'idx_coupon_name');
|
||||||
|
SET @sql = IF(@index_exists > 0,
|
||||||
|
'ALTER TABLE `coupon` DROP INDEX `idx_coupon_name`',
|
||||||
|
'SELECT 1');
|
||||||
|
PREPARE stmt FROM @sql;
|
||||||
|
EXECUTE stmt;
|
||||||
|
DEALLOCATE PREPARE stmt;
|
||||||
|
|
||||||
|
SET @index_exists = (SELECT COUNT(1)
|
||||||
|
FROM INFORMATION_SCHEMA.STATISTICS
|
||||||
|
WHERE TABLE_SCHEMA = DATABASE()
|
||||||
|
AND TABLE_NAME = 'user'
|
||||||
|
AND INDEX_NAME = 'idx_user_refer_code');
|
||||||
|
SET @sql = IF(@index_exists > 0,
|
||||||
|
'ALTER TABLE `user` DROP INDEX `idx_user_refer_code`',
|
||||||
|
'SELECT 1');
|
||||||
|
PREPARE stmt FROM @sql;
|
||||||
|
EXECUTE stmt;
|
||||||
|
DEALLOCATE PREPARE stmt;
|
||||||
|
|
||||||
|
SET @index_exists = (SELECT COUNT(1)
|
||||||
|
FROM INFORMATION_SCHEMA.STATISTICS
|
||||||
|
WHERE TABLE_SCHEMA = DATABASE()
|
||||||
|
AND TABLE_NAME = 'order'
|
||||||
|
AND INDEX_NAME = 'idx_order_coupon');
|
||||||
|
SET @sql = IF(@index_exists > 0,
|
||||||
|
'ALTER TABLE `order` DROP INDEX `idx_order_coupon`',
|
||||||
|
'SELECT 1');
|
||||||
|
PREPARE stmt FROM @sql;
|
||||||
|
EXECUTE stmt;
|
||||||
|
DEALLOCATE PREPARE stmt;
|
||||||
|
|
||||||
|
SET @index_exists = (SELECT COUNT(1)
|
||||||
|
FROM INFORMATION_SCHEMA.STATISTICS
|
||||||
|
WHERE TABLE_SCHEMA = DATABASE()
|
||||||
|
AND TABLE_NAME = 'order'
|
||||||
|
AND INDEX_NAME = 'idx_order_trade_no');
|
||||||
|
SET @sql = IF(@index_exists > 0,
|
||||||
|
'ALTER TABLE `order` DROP INDEX `idx_order_trade_no`',
|
||||||
|
'SELECT 1');
|
||||||
|
PREPARE stmt FROM @sql;
|
||||||
|
EXECUTE stmt;
|
||||||
|
DEALLOCATE PREPARE stmt;
|
||||||
|
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
-- migrate:up
|
||||||
|
CREATE TABLE IF NOT EXISTS `server_config_overrides`
|
||||||
|
(
|
||||||
|
`id` bigint NOT NULL AUTO_INCREMENT,
|
||||||
|
`server_id` bigint NOT NULL COMMENT 'Server ID',
|
||||||
|
`ip_strategy` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT 'IP strategy override, NULL means inherit',
|
||||||
|
`dns` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci COMMENT 'DNS override, NULL means inherit',
|
||||||
|
`block` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci COMMENT 'Block override, NULL means inherit',
|
||||||
|
`outbound` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci COMMENT 'Outbound override, NULL means inherit',
|
||||||
|
`created_at` datetime(3) DEFAULT NULL COMMENT 'Creation Time',
|
||||||
|
`updated_at` datetime(3) DEFAULT NULL COMMENT 'Update Time',
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE KEY `uni_server_config_overrides_server_id` (`server_id`)
|
||||||
|
) ENGINE = InnoDB
|
||||||
|
DEFAULT CHARSET = utf8mb4
|
||||||
|
COLLATE = utf8mb4_general_ci;
|
||||||
|
|
||||||
|
-- migrate:down
|
||||||
|
DROP TABLE IF EXISTS `server_config_overrides`;
|
||||||
|
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
-- migrate:up
|
||||||
|
SET @column_exists = (
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM INFORMATION_SCHEMA.COLUMNS
|
||||||
|
WHERE TABLE_SCHEMA = DATABASE()
|
||||||
|
AND TABLE_NAME = 'payment'
|
||||||
|
AND COLUMN_NAME = 'sort'
|
||||||
|
);
|
||||||
|
|
||||||
|
SET @sql = IF(
|
||||||
|
@column_exists = 0,
|
||||||
|
'ALTER TABLE `payment` ADD COLUMN `sort` bigint NOT NULL DEFAULT 0 COMMENT ''Sort'' AFTER `fee_amount`',
|
||||||
|
'SELECT 1'
|
||||||
|
);
|
||||||
|
|
||||||
|
PREPARE stmt FROM @sql;
|
||||||
|
EXECUTE stmt;
|
||||||
|
DEALLOCATE PREPARE stmt;
|
||||||
|
|
||||||
|
UPDATE `payment`
|
||||||
|
SET `sort` = `id`
|
||||||
|
WHERE `sort` = 0;
|
||||||
|
|
||||||
|
-- migrate:down
|
||||||
|
SET @column_exists = (
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM INFORMATION_SCHEMA.COLUMNS
|
||||||
|
WHERE TABLE_SCHEMA = DATABASE()
|
||||||
|
AND TABLE_NAME = 'payment'
|
||||||
|
AND COLUMN_NAME = 'sort'
|
||||||
|
);
|
||||||
|
|
||||||
|
SET @sql = IF(
|
||||||
|
@column_exists > 0,
|
||||||
|
'ALTER TABLE `payment` DROP COLUMN `sort`',
|
||||||
|
'SELECT 1'
|
||||||
|
);
|
||||||
|
|
||||||
|
PREPARE stmt FROM @sql;
|
||||||
|
EXECUTE stmt;
|
||||||
|
DEALLOCATE PREPARE stmt;
|
||||||
|
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
-- migrate:up
|
||||||
|
INSERT INTO `system` (`category`, `key`, `value`, `type`, `desc`, `created_at`, `updated_at`)
|
||||||
|
SELECT 'subscribe', 'ShowTutorial', 'true', 'bool', 'Show tutorial section on the user document page', '2025-04-22 14:25:16.639', '2025-04-22 14:25:16.639'
|
||||||
|
WHERE NOT EXISTS (
|
||||||
|
SELECT 1 FROM `system` WHERE `category` = 'subscribe' AND `key` = 'ShowTutorial'
|
||||||
|
);
|
||||||
|
|
||||||
|
-- migrate:down
|
||||||
|
DELETE FROM `system` WHERE `category` = 'subscribe' AND `key` = 'ShowTutorial';
|
||||||
|
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
-- migrate:up
|
||||||
|
-- MySQL datetime type does not have timezone support.
|
||||||
|
-- The Go code fix (serverPushStatusLogic.go, serverPushUserTrafficLogic.go)
|
||||||
|
-- removing .UTC() is sufficient for MySQL environments.
|
||||||
|
SELECT 1;
|
||||||
|
|
||||||
|
-- migrate:down
|
||||||
|
SELECT 1;
|
||||||
|
|
||||||
@@ -0,0 +1,494 @@
|
|||||||
|
-- migrate:up
|
||||||
|
-- 000001_init_schema.up.sql
|
||||||
|
CREATE TABLE IF NOT EXISTS "ads"
|
||||||
|
(
|
||||||
|
"id" BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL,
|
||||||
|
"title" varchar(255) NOT NULL DEFAULT '',
|
||||||
|
"type" varchar(255) NOT NULL DEFAULT '',
|
||||||
|
"content" text,
|
||||||
|
"target_url" varchar(512) DEFAULT '',
|
||||||
|
"start_time" TIMESTAMP DEFAULT NULL,
|
||||||
|
"end_time" TIMESTAMP DEFAULT NULL,
|
||||||
|
"status" SMALLINT DEFAULT '0',
|
||||||
|
"created_at" TIMESTAMP(3) DEFAULT NULL,
|
||||||
|
"updated_at" TIMESTAMP(3) DEFAULT NULL,
|
||||||
|
PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
CREATE TABLE IF NOT EXISTS "announcement"
|
||||||
|
(
|
||||||
|
"id" BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL,
|
||||||
|
"title" varchar(255) NOT NULL DEFAULT '',
|
||||||
|
"content" text,
|
||||||
|
"show" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
"pinned" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
"popup" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
"created_at" TIMESTAMP(3) DEFAULT NULL,
|
||||||
|
"updated_at" TIMESTAMP(3) DEFAULT NULL,
|
||||||
|
PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
CREATE TABLE IF NOT EXISTS "application"
|
||||||
|
(
|
||||||
|
"id" BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL,
|
||||||
|
"name" varchar(255) NOT NULL DEFAULT '',
|
||||||
|
"icon" text NOT NULL,
|
||||||
|
"description" text,
|
||||||
|
"subscribe_type" varchar(50) NOT NULL DEFAULT '',
|
||||||
|
"created_at" TIMESTAMP(3) DEFAULT NULL,
|
||||||
|
"updated_at" TIMESTAMP(3) DEFAULT NULL,
|
||||||
|
PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
CREATE TABLE IF NOT EXISTS "application_config"
|
||||||
|
(
|
||||||
|
"id" BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL,
|
||||||
|
"app_id" bigint NOT NULL DEFAULT '0',
|
||||||
|
"encryption_key" text,
|
||||||
|
"encryption_method" varchar(255) DEFAULT NULL,
|
||||||
|
"domains" text,
|
||||||
|
"startup_picture" text,
|
||||||
|
"startup_picture_skip_time" bigint NOT NULL DEFAULT '0',
|
||||||
|
"created_at" TIMESTAMP(3) DEFAULT NULL,
|
||||||
|
"updated_at" TIMESTAMP(3) DEFAULT NULL,
|
||||||
|
PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
CREATE TABLE IF NOT EXISTS "application_version"
|
||||||
|
(
|
||||||
|
"id" BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL,
|
||||||
|
"url" varchar(255) NOT NULL DEFAULT '',
|
||||||
|
"version" varchar(255) NOT NULL DEFAULT '',
|
||||||
|
"platform" varchar(50) NOT NULL DEFAULT '',
|
||||||
|
"is_default" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
"description" text,
|
||||||
|
"application_id" bigint DEFAULT NULL,
|
||||||
|
"created_at" TIMESTAMP(3) DEFAULT NULL,
|
||||||
|
"updated_at" TIMESTAMP(3) DEFAULT NULL,
|
||||||
|
PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS "application_version_fk_application_application_versions" ON "application_version" ("application_id");
|
||||||
|
CREATE TABLE IF NOT EXISTS "auth_method"
|
||||||
|
(
|
||||||
|
"id" BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL,
|
||||||
|
"method" varchar(255) NOT NULL DEFAULT '',
|
||||||
|
"config" text NOT NULL,
|
||||||
|
"enabled" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
"created_at" TIMESTAMP(3) DEFAULT NULL,
|
||||||
|
"updated_at" TIMESTAMP(3) DEFAULT NULL,
|
||||||
|
PRIMARY KEY ("id"),
|
||||||
|
CONSTRAINT "uni_auth_method" UNIQUE ("method")
|
||||||
|
);
|
||||||
|
CREATE TABLE IF NOT EXISTS "coupon"
|
||||||
|
(
|
||||||
|
"id" BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL,
|
||||||
|
"name" varchar(255) NOT NULL DEFAULT '',
|
||||||
|
"code" varchar(255) NOT NULL DEFAULT '',
|
||||||
|
"count" bigint NOT NULL DEFAULT '0',
|
||||||
|
"type" SMALLINT NOT NULL DEFAULT '1',
|
||||||
|
"discount" bigint NOT NULL DEFAULT '0',
|
||||||
|
"start_time" bigint NOT NULL DEFAULT '0',
|
||||||
|
"expire_time" bigint NOT NULL DEFAULT '0',
|
||||||
|
"user_limit" bigint NOT NULL DEFAULT '0',
|
||||||
|
"subscribe" varchar(255) NOT NULL DEFAULT '',
|
||||||
|
"used_count" bigint NOT NULL DEFAULT '0',
|
||||||
|
"enable" BOOLEAN NOT NULL DEFAULT true,
|
||||||
|
"created_at" TIMESTAMP(3) DEFAULT NULL,
|
||||||
|
"updated_at" TIMESTAMP(3) DEFAULT NULL,
|
||||||
|
PRIMARY KEY ("id"),
|
||||||
|
CONSTRAINT "uni_coupon_code" UNIQUE ("code")
|
||||||
|
);
|
||||||
|
CREATE TABLE IF NOT EXISTS "document"
|
||||||
|
(
|
||||||
|
"id" BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL,
|
||||||
|
"title" varchar(255) NOT NULL DEFAULT '',
|
||||||
|
"content" text,
|
||||||
|
"tags" varchar(255) NOT NULL DEFAULT '',
|
||||||
|
"show" BOOLEAN NOT NULL DEFAULT true,
|
||||||
|
"created_at" TIMESTAMP(3) DEFAULT NULL,
|
||||||
|
"updated_at" TIMESTAMP(3) DEFAULT NULL,
|
||||||
|
PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
CREATE TABLE IF NOT EXISTS "message_log"
|
||||||
|
(
|
||||||
|
"id" BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL,
|
||||||
|
"type" varchar(50) NOT NULL DEFAULT 'email',
|
||||||
|
"platform" varchar(50) NOT NULL DEFAULT 'smtp',
|
||||||
|
"to" text NOT NULL,
|
||||||
|
"subject" varchar(255) NOT NULL DEFAULT '',
|
||||||
|
"content" text,
|
||||||
|
"status" SMALLINT NOT NULL DEFAULT '0',
|
||||||
|
"created_at" TIMESTAMP(3) DEFAULT NULL,
|
||||||
|
"updated_at" TIMESTAMP(3) DEFAULT NULL,
|
||||||
|
PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
CREATE TABLE IF NOT EXISTS "order"
|
||||||
|
(
|
||||||
|
"id" BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL,
|
||||||
|
"parent_id" bigint DEFAULT NULL,
|
||||||
|
"user_id" bigint NOT NULL DEFAULT '0',
|
||||||
|
"order_no" varchar(255) NOT NULL DEFAULT '',
|
||||||
|
"type" SMALLINT NOT NULL DEFAULT '1',
|
||||||
|
"quantity" bigint NOT NULL DEFAULT '1',
|
||||||
|
"price" bigint NOT NULL DEFAULT '0',
|
||||||
|
"amount" bigint NOT NULL DEFAULT '0',
|
||||||
|
"gift_amount" bigint NOT NULL DEFAULT '0',
|
||||||
|
"discount" bigint NOT NULL DEFAULT '0',
|
||||||
|
"coupon" varchar(255) DEFAULT NULL,
|
||||||
|
"coupon_discount" bigint NOT NULL DEFAULT '0',
|
||||||
|
"commission" bigint NOT NULL DEFAULT '0',
|
||||||
|
"payment_id" bigint NOT NULL DEFAULT '-1',
|
||||||
|
"method" varchar(255) NOT NULL DEFAULT '',
|
||||||
|
"fee_amount" bigint NOT NULL DEFAULT '0',
|
||||||
|
"trade_no" varchar(255) DEFAULT NULL,
|
||||||
|
"status" SMALLINT NOT NULL DEFAULT '1',
|
||||||
|
"subscribe_id" bigint NOT NULL DEFAULT '0',
|
||||||
|
"subscribe_token" varchar(255) DEFAULT NULL,
|
||||||
|
"is_new" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
"created_at" TIMESTAMP(3) DEFAULT NULL,
|
||||||
|
"updated_at" TIMESTAMP(3) DEFAULT NULL,
|
||||||
|
PRIMARY KEY ("id"),
|
||||||
|
CONSTRAINT "uni_order_order_no" UNIQUE ("order_no")
|
||||||
|
);
|
||||||
|
CREATE TABLE IF NOT EXISTS "payment"
|
||||||
|
(
|
||||||
|
"id" BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL,
|
||||||
|
"name" varchar(100) NOT NULL DEFAULT '',
|
||||||
|
"platform" varchar(100) NOT NULL,
|
||||||
|
"description" text,
|
||||||
|
"icon" varchar(255) DEFAULT '',
|
||||||
|
"domain" varchar(255) DEFAULT '',
|
||||||
|
"config" text NOT NULL,
|
||||||
|
"fee_mode" SMALLINT NOT NULL DEFAULT '0',
|
||||||
|
"fee_percent" bigint DEFAULT '0',
|
||||||
|
"fee_amount" bigint DEFAULT '0',
|
||||||
|
"enable" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
"token" varchar(255) DEFAULT NULL,
|
||||||
|
PRIMARY KEY ("id"),
|
||||||
|
CONSTRAINT "uni_payment_token" UNIQUE ("token")
|
||||||
|
);
|
||||||
|
CREATE TABLE IF NOT EXISTS "server"
|
||||||
|
(
|
||||||
|
"id" BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL,
|
||||||
|
"name" varchar(100) NOT NULL DEFAULT '',
|
||||||
|
"tags" varchar(128) NOT NULL DEFAULT '',
|
||||||
|
"country" varchar(128) NOT NULL DEFAULT '',
|
||||||
|
"city" varchar(128) NOT NULL DEFAULT '',
|
||||||
|
"latitude" varchar(128) NOT NULL DEFAULT '',
|
||||||
|
"longitude" varchar(128) NOT NULL DEFAULT '',
|
||||||
|
"server_addr" varchar(100) NOT NULL DEFAULT '',
|
||||||
|
"relay_mode" varchar(20) NOT NULL DEFAULT 'none',
|
||||||
|
"relay_node" text,
|
||||||
|
"speed_limit" bigint NOT NULL DEFAULT '0',
|
||||||
|
"traffic_ratio" decimal(4, 2) NOT NULL DEFAULT '0.00',
|
||||||
|
"group_id" bigint DEFAULT NULL,
|
||||||
|
"protocol" varchar(20) NOT NULL DEFAULT '',
|
||||||
|
"config" text,
|
||||||
|
"enable" SMALLINT NOT NULL DEFAULT '1',
|
||||||
|
"sort" bigint NOT NULL DEFAULT '0',
|
||||||
|
"last_reported_at" TIMESTAMP(3) DEFAULT NULL,
|
||||||
|
"created_at" TIMESTAMP(3) DEFAULT NULL,
|
||||||
|
"updated_at" TIMESTAMP(3) DEFAULT NULL,
|
||||||
|
PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS "server_idx_group_id" ON "server" ("group_id");
|
||||||
|
CREATE TABLE IF NOT EXISTS "server_group"
|
||||||
|
(
|
||||||
|
"id" BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL,
|
||||||
|
"name" varchar(100) NOT NULL DEFAULT '',
|
||||||
|
"description" varchar(255) DEFAULT '',
|
||||||
|
"created_at" TIMESTAMP(3) DEFAULT NULL,
|
||||||
|
"updated_at" TIMESTAMP(3) DEFAULT NULL,
|
||||||
|
PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
-- if "sms" not exist, create it
|
||||||
|
CREATE TABLE IF NOT EXISTS "sms"
|
||||||
|
(
|
||||||
|
"id" BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL,
|
||||||
|
"content" text,
|
||||||
|
"platform" varchar(64) DEFAULT NULL,
|
||||||
|
"area_code" varchar(64) DEFAULT NULL,
|
||||||
|
"telephone" varchar(64) DEFAULT NULL,
|
||||||
|
"status" SMALLINT DEFAULT '1',
|
||||||
|
"created_at" timestamp NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
CREATE TABLE IF NOT EXISTS "subscribe"
|
||||||
|
(
|
||||||
|
"id" BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL,
|
||||||
|
"name" varchar(255) NOT NULL DEFAULT '',
|
||||||
|
"description" text,
|
||||||
|
"unit_price" bigint NOT NULL DEFAULT '0',
|
||||||
|
"unit_time" varchar(255) NOT NULL DEFAULT '',
|
||||||
|
"discount" text,
|
||||||
|
"replacement" bigint NOT NULL DEFAULT '0',
|
||||||
|
"inventory" bigint NOT NULL DEFAULT '0',
|
||||||
|
"traffic" bigint NOT NULL DEFAULT '0',
|
||||||
|
"speed_limit" bigint NOT NULL DEFAULT '0',
|
||||||
|
"device_limit" bigint NOT NULL DEFAULT '0',
|
||||||
|
"quota" bigint NOT NULL DEFAULT '0',
|
||||||
|
"group_id" bigint DEFAULT NULL,
|
||||||
|
"server_group" varchar(255) DEFAULT NULL,
|
||||||
|
"server" varchar(255) DEFAULT NULL,
|
||||||
|
"show" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
"sell" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
"sort" bigint NOT NULL DEFAULT '0',
|
||||||
|
"deduction_ratio" bigint DEFAULT '0',
|
||||||
|
"allow_deduction" BOOLEAN DEFAULT true,
|
||||||
|
"reset_cycle" bigint DEFAULT '0',
|
||||||
|
"renewal_reset" BOOLEAN DEFAULT false,
|
||||||
|
"created_at" TIMESTAMP(3) DEFAULT NULL,
|
||||||
|
"updated_at" TIMESTAMP(3) DEFAULT NULL,
|
||||||
|
PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
CREATE TABLE IF NOT EXISTS "subscribe_group"
|
||||||
|
(
|
||||||
|
"id" BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL,
|
||||||
|
"name" varchar(255) NOT NULL DEFAULT '',
|
||||||
|
"description" text,
|
||||||
|
"created_at" TIMESTAMP(3) DEFAULT NULL,
|
||||||
|
"updated_at" TIMESTAMP(3) DEFAULT NULL,
|
||||||
|
PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
CREATE TABLE IF NOT EXISTS "subscribe_type"
|
||||||
|
(
|
||||||
|
"id" BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL,
|
||||||
|
"name" varchar(50) NOT NULL DEFAULT '',
|
||||||
|
"mark" varchar(255) NOT NULL DEFAULT '',
|
||||||
|
"created_at" TIMESTAMP(3) DEFAULT NULL,
|
||||||
|
"updated_at" TIMESTAMP(3) DEFAULT NULL,
|
||||||
|
PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
CREATE TABLE IF NOT EXISTS "system"
|
||||||
|
(
|
||||||
|
"id" BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL,
|
||||||
|
"category" varchar(100) NOT NULL DEFAULT '',
|
||||||
|
"key" varchar(100) NOT NULL DEFAULT '',
|
||||||
|
"value" text NOT NULL,
|
||||||
|
"type" varchar(50) NOT NULL DEFAULT '',
|
||||||
|
"desc" text NOT NULL,
|
||||||
|
"created_at" TIMESTAMP(3) DEFAULT NULL,
|
||||||
|
"updated_at" TIMESTAMP(3) DEFAULT NULL,
|
||||||
|
PRIMARY KEY ("id"),
|
||||||
|
CONSTRAINT "uni_system_key" UNIQUE ("key")
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS "system_index_key" ON "system" ("key");
|
||||||
|
CREATE TABLE IF NOT EXISTS "ticket"
|
||||||
|
(
|
||||||
|
"id" BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL,
|
||||||
|
"title" varchar(255) NOT NULL DEFAULT '',
|
||||||
|
"description" text,
|
||||||
|
"user_id" bigint NOT NULL DEFAULT '0',
|
||||||
|
"status" SMALLINT NOT NULL DEFAULT '1',
|
||||||
|
"created_at" TIMESTAMP(3) DEFAULT NULL,
|
||||||
|
"updated_at" TIMESTAMP(3) DEFAULT NULL,
|
||||||
|
PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
CREATE TABLE IF NOT EXISTS "ticket_follow"
|
||||||
|
(
|
||||||
|
"id" BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL,
|
||||||
|
"ticket_id" bigint NOT NULL DEFAULT '0',
|
||||||
|
"from" varchar(255) NOT NULL DEFAULT '',
|
||||||
|
"type" SMALLINT NOT NULL DEFAULT '1',
|
||||||
|
"content" text,
|
||||||
|
"created_at" TIMESTAMP(3) DEFAULT NULL,
|
||||||
|
PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
CREATE TABLE IF NOT EXISTS "traffic_log"
|
||||||
|
(
|
||||||
|
"id" BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL,
|
||||||
|
"server_id" bigint NOT NULL,
|
||||||
|
"user_id" bigint NOT NULL,
|
||||||
|
"subscribe_id" bigint NOT NULL,
|
||||||
|
"download" bigint DEFAULT '0',
|
||||||
|
"upload" bigint DEFAULT '0',
|
||||||
|
"timestamp" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||||
|
PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS "traffic_log_idx_subscribe_id" ON "traffic_log" ("subscribe_id");
|
||||||
|
CREATE INDEX IF NOT EXISTS "traffic_log_idx_server_id" ON "traffic_log" ("server_id");
|
||||||
|
CREATE INDEX IF NOT EXISTS "traffic_log_idx_user_id" ON "traffic_log" ("user_id");
|
||||||
|
CREATE TABLE IF NOT EXISTS "user"
|
||||||
|
(
|
||||||
|
"id" BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL,
|
||||||
|
"password" varchar(100) NOT NULL,
|
||||||
|
"avatar" text,
|
||||||
|
"balance" bigint DEFAULT '0',
|
||||||
|
"telegram" bigint DEFAULT NULL,
|
||||||
|
"refer_code" varchar(20) DEFAULT '',
|
||||||
|
"referer_id" bigint DEFAULT NULL,
|
||||||
|
"commission" bigint DEFAULT '0',
|
||||||
|
"gift_amount" bigint DEFAULT '0',
|
||||||
|
"enable" BOOLEAN NOT NULL DEFAULT true,
|
||||||
|
"is_admin" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
"valid_email" SMALLINT NOT NULL DEFAULT '0',
|
||||||
|
"enable_email_notify" SMALLINT NOT NULL DEFAULT '0',
|
||||||
|
"enable_telegram_notify" SMALLINT NOT NULL DEFAULT '0',
|
||||||
|
"enable_balance_notify" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
"enable_login_notify" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
"enable_subscribe_notify" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
"enable_trade_notify" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
"created_at" TIMESTAMP(3) DEFAULT NULL,
|
||||||
|
"updated_at" TIMESTAMP(3) DEFAULT NULL,
|
||||||
|
"deleted_at" TIMESTAMP(3) DEFAULT NULL,
|
||||||
|
"is_del" BIGINT DEFAULT NULL,
|
||||||
|
PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS "user_idx_referer" ON "user" ("referer_id");
|
||||||
|
CREATE TABLE IF NOT EXISTS "user_auth_methods"
|
||||||
|
(
|
||||||
|
"id" BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL,
|
||||||
|
"user_id" bigint NOT NULL,
|
||||||
|
"auth_type" varchar(255) NOT NULL,
|
||||||
|
"auth_identifier" varchar(255) NOT NULL,
|
||||||
|
"verified" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
"created_at" TIMESTAMP(3) DEFAULT NULL,
|
||||||
|
"updated_at" TIMESTAMP(3) DEFAULT NULL,
|
||||||
|
PRIMARY KEY ("id"),
|
||||||
|
CONSTRAINT "idx_auth_identifier" UNIQUE ("auth_identifier")
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS "user_auth_methods_idx_user_id" ON "user_auth_methods" ("user_id");
|
||||||
|
CREATE TABLE IF NOT EXISTS "user_balance_log"
|
||||||
|
(
|
||||||
|
"id" BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL,
|
||||||
|
"user_id" bigint NOT NULL,
|
||||||
|
"amount" bigint NOT NULL,
|
||||||
|
"type" SMALLINT NOT NULL,
|
||||||
|
"order_id" bigint DEFAULT NULL,
|
||||||
|
"balance" bigint NOT NULL,
|
||||||
|
"created_at" TIMESTAMP(3) DEFAULT NULL,
|
||||||
|
PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS "user_balance_log_idx_user_id" ON "user_balance_log" ("user_id");
|
||||||
|
CREATE TABLE IF NOT EXISTS "user_commission_log"
|
||||||
|
(
|
||||||
|
"id" BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL,
|
||||||
|
"user_id" bigint NOT NULL,
|
||||||
|
"order_no" varchar(191) DEFAULT NULL,
|
||||||
|
"amount" bigint NOT NULL,
|
||||||
|
"created_at" TIMESTAMP(3) DEFAULT NULL,
|
||||||
|
PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS "user_commission_log_idx_user_id" ON "user_commission_log" ("user_id");
|
||||||
|
CREATE TABLE IF NOT EXISTS "user_device"
|
||||||
|
(
|
||||||
|
"id" BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL,
|
||||||
|
"user_id" bigint NOT NULL,
|
||||||
|
"subscribe_id" bigint DEFAULT NULL,
|
||||||
|
"ip" varchar(191) DEFAULT NULL,
|
||||||
|
"identifier" varchar(191) DEFAULT NULL,
|
||||||
|
"user_agent" varchar(64) DEFAULT NULL,
|
||||||
|
"online" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
"enabled" BOOLEAN NOT NULL DEFAULT true,
|
||||||
|
"created_at" TIMESTAMP(3) DEFAULT NULL,
|
||||||
|
"updated_at" TIMESTAMP(3) DEFAULT NULL,
|
||||||
|
PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS "user_device_idx_user_id" ON "user_device" ("user_id");
|
||||||
|
CREATE TABLE IF NOT EXISTS "user_gift_amount_log"
|
||||||
|
(
|
||||||
|
"id" BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL,
|
||||||
|
"user_id" bigint NOT NULL,
|
||||||
|
"user_subscribe_id" bigint DEFAULT NULL,
|
||||||
|
"order_no" varchar(191) DEFAULT NULL,
|
||||||
|
"type" SMALLINT NOT NULL,
|
||||||
|
"amount" bigint NOT NULL,
|
||||||
|
"balance" bigint NOT NULL,
|
||||||
|
"remark" varchar(255) DEFAULT '',
|
||||||
|
"created_at" TIMESTAMP(3) DEFAULT NULL,
|
||||||
|
PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS "user_gift_amount_log_idx_user_id" ON "user_gift_amount_log" ("user_id");
|
||||||
|
CREATE TABLE IF NOT EXISTS "user_login_log"
|
||||||
|
(
|
||||||
|
"id" BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL,
|
||||||
|
"user_id" bigint NOT NULL,
|
||||||
|
"login_ip" varchar(255) NOT NULL,
|
||||||
|
"user_agent" text NOT NULL,
|
||||||
|
"success" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
"created_at" TIMESTAMP(3) DEFAULT NULL,
|
||||||
|
PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS "user_login_log_idx_user_id" ON "user_login_log" ("user_id");
|
||||||
|
CREATE TABLE IF NOT EXISTS "user_subscribe"
|
||||||
|
(
|
||||||
|
"id" BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL,
|
||||||
|
"user_id" bigint NOT NULL,
|
||||||
|
"order_id" bigint NOT NULL,
|
||||||
|
"subscribe_id" bigint NOT NULL,
|
||||||
|
"start_time" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||||
|
"expire_time" TIMESTAMP(3) DEFAULT NULL,
|
||||||
|
"traffic" bigint DEFAULT '0',
|
||||||
|
"download" bigint DEFAULT '0',
|
||||||
|
"upload" bigint DEFAULT '0',
|
||||||
|
"token" varchar(255) DEFAULT '',
|
||||||
|
"uuid" varchar(255) DEFAULT '',
|
||||||
|
"status" SMALLINT DEFAULT '0',
|
||||||
|
"created_at" TIMESTAMP(3) DEFAULT NULL,
|
||||||
|
"updated_at" TIMESTAMP(3) DEFAULT NULL,
|
||||||
|
PRIMARY KEY ("id"),
|
||||||
|
CONSTRAINT "uni_user_subscribe_token" UNIQUE ("token"),
|
||||||
|
CONSTRAINT "uni_user_subscribe_uuid" UNIQUE ("uuid")
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS "user_subscribe_idx_user_id" ON "user_subscribe" ("user_id");
|
||||||
|
CREATE INDEX IF NOT EXISTS "user_subscribe_idx_order_id" ON "user_subscribe" ("order_id");
|
||||||
|
CREATE INDEX IF NOT EXISTS "user_subscribe_idx_subscribe_id" ON "user_subscribe" ("subscribe_id");
|
||||||
|
CREATE INDEX IF NOT EXISTS "user_subscribe_idx_token" ON "user_subscribe" ("token");
|
||||||
|
CREATE INDEX IF NOT EXISTS "user_subscribe_idx_uuid" ON "user_subscribe" ("uuid");
|
||||||
|
CREATE TABLE IF NOT EXISTS "user_subscribe_log"
|
||||||
|
(
|
||||||
|
"id" BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL,
|
||||||
|
"user_id" bigint NOT NULL,
|
||||||
|
"user_subscribe_id" bigint NOT NULL,
|
||||||
|
"token" varchar(255) NOT NULL,
|
||||||
|
"ip" varchar(255) NOT NULL,
|
||||||
|
"user_agent" text NOT NULL,
|
||||||
|
"created_at" TIMESTAMP(3) DEFAULT NULL,
|
||||||
|
PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS "user_subscribe_log_idx_user_id" ON "user_subscribe_log" ("user_id");
|
||||||
|
CREATE INDEX IF NOT EXISTS "user_subscribe_log_idx_user_subscribe_id" ON "user_subscribe_log" ("user_subscribe_id");
|
||||||
|
CREATE TABLE IF NOT EXISTS "server_rule_group"
|
||||||
|
(
|
||||||
|
"id" BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL,
|
||||||
|
"name" varchar(100) NOT NULL DEFAULT '',
|
||||||
|
"icon" text,
|
||||||
|
"tags" text,
|
||||||
|
"description" varchar(255) DEFAULT '',
|
||||||
|
"enable" BOOLEAN NOT NULL DEFAULT true,
|
||||||
|
"created_at" TIMESTAMP(3) DEFAULT NULL,
|
||||||
|
"updated_at" TIMESTAMP(3) DEFAULT NULL,
|
||||||
|
PRIMARY KEY ("id"),
|
||||||
|
CONSTRAINT "unique_name" UNIQUE ("name")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- migrate:down
|
||||||
|
-- 000001_init_schema.down.sql
|
||||||
|
DROP TABLE IF EXISTS "user_subscribe_log";
|
||||||
|
DROP TABLE IF EXISTS "user_subscribe";
|
||||||
|
DROP TABLE IF EXISTS "user_login_log";
|
||||||
|
DROP TABLE IF EXISTS "user_gift_amount_log";
|
||||||
|
DROP TABLE IF EXISTS "user_device";
|
||||||
|
DROP TABLE IF EXISTS "user_commission_log";
|
||||||
|
DROP TABLE IF EXISTS "user_balance_log";
|
||||||
|
DROP TABLE IF EXISTS "user_auth_methods";
|
||||||
|
DROP TABLE IF EXISTS "user";
|
||||||
|
DROP TABLE IF EXISTS "traffic_log";
|
||||||
|
DROP TABLE IF EXISTS "ticket_follow";
|
||||||
|
DROP TABLE IF EXISTS "ticket";
|
||||||
|
DROP TABLE IF EXISTS "system";
|
||||||
|
DROP TABLE IF EXISTS "subscribe_type";
|
||||||
|
DROP TABLE IF EXISTS "subscribe_group";
|
||||||
|
DROP TABLE IF EXISTS "subscribe";
|
||||||
|
DROP TABLE IF EXISTS "sms";
|
||||||
|
DROP TABLE IF EXISTS "server_rule_group";
|
||||||
|
DROP TABLE IF EXISTS "server_group";
|
||||||
|
DROP TABLE IF EXISTS "server";
|
||||||
|
DROP TABLE IF EXISTS "payment";
|
||||||
|
DROP TABLE IF EXISTS "order";
|
||||||
|
DROP TABLE IF EXISTS "message_log";
|
||||||
|
DROP TABLE IF EXISTS "document";
|
||||||
|
DROP TABLE IF EXISTS "coupon";
|
||||||
|
DROP TABLE IF EXISTS "auth_method";
|
||||||
|
DROP TABLE IF EXISTS "application_version";
|
||||||
|
DROP TABLE IF EXISTS "application_config";
|
||||||
|
DROP TABLE IF EXISTS "application";
|
||||||
|
DROP TABLE IF EXISTS "announcement";
|
||||||
|
DROP TABLE IF EXISTS "ads";
|
||||||
|
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
-- migrate:up
|
||||||
|
-- 000002_init_data.up.sql
|
||||||
|
-- auth_method
|
||||||
|
INSERT INTO "auth_method" ("id", "method", "config", "enabled", "created_at", "updated_at")
|
||||||
|
VALUES
|
||||||
|
(1, 'email', '{"platform":"smtp","platform_config":{"host":"","port":0,"user":"","pass":"","from":"","ssl":false},"enable_verify":false,"enable_notify":false,"enable_domain_suffix":false,"domain_suffix_list":"","verify_email_template":"","expiration_email_template":"","maintenance_email_template":"","traffic_exceed_email_template":""}', true, '2025-04-22 14:25:16.642', '2025-04-22 14:25:16.642'),
|
||||||
|
(2, 'mobile', '{"platform":"AlibabaCloud","platform_config":{"access":"","secret":"","sign_name":"","endpoint":"","template_code":""},"enable_whitelist":false,"whitelist":[]}', false, '2025-04-22 14:25:16.642', '2025-04-22 14:25:16.642'),
|
||||||
|
(3, 'apple', '{"team_id":"","key_id":"","client_id":"","client_secret":"","redirect_url":""}', false, '2025-04-22 14:25:16.642', '2025-04-22 14:25:16.642'),
|
||||||
|
(4, 'google', '{"client_id":"","client_secret":"","redirect_url":""}', false, '2025-04-22 14:25:16.642', '2025-04-22 14:25:16.642'),
|
||||||
|
(5, 'github', '{"client_id":"","client_secret":"","redirect_url":""}', false, '2025-04-22 14:25:16.642', '2025-04-22 14:25:16.642'),
|
||||||
|
(6, 'facebook', '{"client_id":"","client_secret":"","redirect_url":""}', false, '2025-04-22 14:25:16.642', '2025-04-22 14:25:16.642'),
|
||||||
|
(7, 'telegram', '{"bot_token":"","enable_notify":false,"webhook_domain":""}', false, '2025-04-22 14:25:16.642', '2025-04-22 14:25:16.642'),
|
||||||
|
(8, 'device', '{"show_ads":false,"only_real_device":false,"enable_security":false,"security_secret":""}', false, '2025-04-22 14:25:16.642', '2025-04-22 14:25:16.642');
|
||||||
|
-- payment
|
||||||
|
INSERT INTO "payment" ("id", "name", "platform", "description", "icon", "domain", "config", "fee_mode",
|
||||||
|
"fee_percent", "fee_amount", "enable", "token")
|
||||||
|
VALUES
|
||||||
|
(-1, 'Balance', 'balance', '', '', '', '', 0, 0, 0, true, '');
|
||||||
|
-- subscribe_type
|
||||||
|
INSERT INTO "subscribe_type" ("id", "name", "mark", "created_at", "updated_at")
|
||||||
|
VALUES (1, 'Clash', 'Clash', '2025-04-22 14:25:16.648', '2025-04-22 14:25:16.648'),
|
||||||
|
(2, 'Hiddify', 'Hiddify', '2025-04-22 14:25:16.648', '2025-04-22 14:25:16.648'),
|
||||||
|
(3, 'Loon', 'Loon', '2025-04-22 14:25:16.648', '2025-04-22 14:25:16.648'),
|
||||||
|
(4, 'NekoBox', 'NekoBox', '2025-04-22 14:25:16.648', '2025-04-22 14:25:16.648'),
|
||||||
|
(5, 'NekoRay', 'NekoRay', '2025-04-22 14:25:16.648', '2025-04-22 14:25:16.648'),
|
||||||
|
(6, 'Netch', 'Netch', '2025-04-22 14:25:16.648', '2025-04-22 14:25:16.648'),
|
||||||
|
(7, 'Quantumult', 'Quantumult', '2025-04-22 14:25:16.648', '2025-04-22 14:25:16.648'),
|
||||||
|
(8, 'Shadowrocket', 'Shadowrocket', '2025-04-22 14:25:16.648', '2025-04-22 14:25:16.648'),
|
||||||
|
(9, 'SingBox', ' SingBox', '2025-04-22 14:25:16.648', '2025-04-22 14:25:16.648'),
|
||||||
|
(10, 'Surfboard', 'Surfboard', '2025-04-22 14:25:16.648', '2025-04-22 14:25:16.648'),
|
||||||
|
(11, 'Surge', 'Surge', '2025-04-22 14:25:16.648', '2025-04-22 14:25:16.648'),
|
||||||
|
(12, 'V2box', 'V2box', '2025-04-22 14:25:16.648', '2025-04-22 14:25:16.648'),
|
||||||
|
(13, 'V2rayN', 'V2rayN', '2025-04-22 14:25:16.648', '2025-04-22 14:25:16.648'),
|
||||||
|
(14, 'V2rayNg', 'V2rayNg', '2025-04-22 14:25:16.648', '2025-04-22 14:25:16.648');
|
||||||
|
-- system
|
||||||
|
INSERT INTO "system" ("id", "category", "key", "value", "type", "desc", "created_at", "updated_at")
|
||||||
|
VALUES (1, 'site', 'SiteLogo', '/favicon.svg', 'string', 'Site Logo', '2025-04-22 14:25:16.637',
|
||||||
|
'2025-04-22 14:25:16.637'),
|
||||||
|
(2, 'site', 'SiteName', 'Perfect Panel', 'string', 'Site Name', '2025-04-22 14:25:16.637',
|
||||||
|
'2025-04-22 14:25:16.637'),
|
||||||
|
(3, 'site', 'SiteDesc',
|
||||||
|
'PPanel is a pure, professional, and perfect open-source proxy panel tool, designed to be your ideal choice for learning and practical use.',
|
||||||
|
'string', 'Site Description', '2025-04-22 14:25:16.637', '2025-04-22 14:25:16.637'),
|
||||||
|
(4, 'site', 'Host', '', 'string', 'Site Host', '2025-04-22 14:25:16.637', '2025-04-22 14:25:16.637'),
|
||||||
|
(5, 'site', 'Keywords', 'Perfect Panel,PPanel', 'string', 'Site Keywords', '2025-04-22 14:25:16.637',
|
||||||
|
'2025-04-22 14:25:16.637'),
|
||||||
|
(6, 'site', 'CustomHTML', '', 'string', 'Custom HTML', '2025-04-22 14:25:16.637', '2025-04-22 14:25:16.637'),
|
||||||
|
(7, 'tos', 'TosContent', 'Welcome to use Perfect Panel', 'string', 'Terms of Service', '2025-04-22 14:25:16.637',
|
||||||
|
'2025-04-22 14:25:16.637'),
|
||||||
|
(8, 'tos', 'PrivacyPolicy', '', 'string', 'PrivacyPolicy', '2025-04-22 14:25:16.637', '2025-04-22 14:25:16.637'),
|
||||||
|
(9, 'ad', 'WebAD', 'false', 'bool', 'Display ad on the web', '2025-04-22 14:25:16.637',
|
||||||
|
'2025-04-22 14:25:16.637'),
|
||||||
|
(10, 'subscribe', 'SingleModel', 'false', 'bool', '是否单订阅模式', '2025-04-22 14:25:16.639',
|
||||||
|
'2025-04-22 14:25:16.639'),
|
||||||
|
(11, 'subscribe', 'SubscribePath', '/api/subscribe', 'string', '订阅路径', '2025-04-22 14:25:16.639',
|
||||||
|
'2025-04-22 14:25:16.639'),
|
||||||
|
(12, 'subscribe', 'SubscribeDomain', '', 'string', '订阅域名', '2025-04-22 14:25:16.639',
|
||||||
|
'2025-04-22 14:25:16.639'),
|
||||||
|
(13, 'subscribe', 'PanDomain', 'false', 'bool', '是否使用泛域名', '2025-04-22 14:25:16.639',
|
||||||
|
'2025-04-22 14:25:16.639'),
|
||||||
|
(14, 'verify', 'TurnstileSiteKey', '', 'string', 'TurnstileSiteKey', '2025-04-22 14:25:16.639',
|
||||||
|
'2025-04-22 14:25:16.639'),
|
||||||
|
(15, 'verify', 'TurnstileSecret', '', 'string', 'TurnstileSecret', '2025-04-22 14:25:16.639',
|
||||||
|
'2025-04-22 14:25:16.639'),
|
||||||
|
(16, 'verify', 'EnableLoginVerify', 'false', 'bool', 'is enable login verify', '2025-04-22 14:25:16.639',
|
||||||
|
'2025-04-22 14:25:16.639'),
|
||||||
|
(17, 'verify', 'EnableRegisterVerify', 'false', 'bool', 'is enable register verify', '2025-04-22 14:25:16.639',
|
||||||
|
'2025-04-22 14:25:16.639'),
|
||||||
|
(18, 'verify', 'EnableResetPasswordVerify', 'false', 'bool', 'is enable reset password verify',
|
||||||
|
'2025-04-22 14:25:16.639', '2025-04-22 14:25:16.639'),
|
||||||
|
(19, 'server', 'NodeSecret', '12345678', 'string', 'node secret', '2025-04-22 14:25:16.640',
|
||||||
|
'2025-04-22 14:25:16.640'),
|
||||||
|
(20, 'server', 'NodePullInterval', '10', 'int', 'node pull interval', '2025-04-22 14:25:16.640',
|
||||||
|
'2025-04-22 14:25:16.640'),
|
||||||
|
(21, 'server', 'NodePushInterval', '60', 'int', 'node push interval', '2025-04-22 14:25:16.640',
|
||||||
|
'2025-04-22 14:25:16.640'),
|
||||||
|
(22, 'server', 'NodeMultiplierConfig', '[]', 'string', 'node multiplier config', '2025-04-22 14:25:16.640',
|
||||||
|
'2025-04-22 14:25:16.640'),
|
||||||
|
(23, 'invite', 'ForcedInvite', 'false', 'bool', 'Forced invite', '2025-04-22 14:25:16.640',
|
||||||
|
'2025-04-22 14:25:16.640'),
|
||||||
|
(24, 'invite', 'ReferralPercentage', '20', 'int', 'Referral percentage', '2025-04-22 14:25:16.640',
|
||||||
|
'2025-04-22 14:25:16.640'),
|
||||||
|
(25, 'invite', 'OnlyFirstPurchase', 'false', 'bool', 'Only first purchase', '2025-04-22 14:25:16.640',
|
||||||
|
'2025-04-22 14:25:16.640'),
|
||||||
|
(26, 'register', 'StopRegister', 'false', 'bool', 'is stop register', '2025-04-22 14:25:16.640',
|
||||||
|
'2025-04-22 14:25:16.640'),
|
||||||
|
(27, 'register', 'EnableTrial', 'false', 'bool', 'is enable trial', '2025-04-22 14:25:16.640',
|
||||||
|
'2025-04-22 14:25:16.640'),
|
||||||
|
(28, 'register', 'TrialSubscribe', '', 'int', 'Trial subscription', '2025-04-22 14:25:16.640',
|
||||||
|
'2025-04-22 14:25:16.640'),
|
||||||
|
(29, 'register', 'TrialTime', '24', 'int', 'Trial time', '2025-04-22 14:25:16.640', '2025-04-22 14:25:16.640'),
|
||||||
|
(30, 'register', 'TrialTimeUnit', 'Hour', 'string', 'Trial time unit', '2025-04-22 14:25:16.640',
|
||||||
|
'2025-04-22 14:25:16.640'),
|
||||||
|
(31, 'register', 'EnableIpRegisterLimit', 'false', 'bool', 'is enable IP register limit',
|
||||||
|
'2025-04-22 14:25:16.640', '2025-04-22 14:25:16.640'),
|
||||||
|
(32, 'register', 'IpRegisterLimit', '3', 'int', 'IP Register Limit', '2025-04-22 14:25:16.640',
|
||||||
|
'2025-04-22 14:25:16.640'),
|
||||||
|
(33, 'register', 'IpRegisterLimitDuration', '64', 'int', 'IP Register Limit Duration (minutes)',
|
||||||
|
'2025-04-22 14:25:16.640', '2025-04-22 14:25:16.640'),
|
||||||
|
(34, 'currency', 'Currency', 'USD', 'string', 'Currency', '2025-04-22 14:25:16.641', '2025-04-22 14:25:16.641'),
|
||||||
|
(35, 'currency', 'CurrencySymbol', '$', 'string', 'Currency Symbol', '2025-04-22 14:25:16.641',
|
||||||
|
'2025-04-22 14:25:16.641'),
|
||||||
|
(36, 'currency', 'CurrencyUnit', 'USD', 'string', 'Currency Unit', '2025-04-22 14:25:16.641',
|
||||||
|
'2025-04-22 14:25:16.641'),
|
||||||
|
(37, 'currency', 'AccessKey', '', 'string', 'Exchangerate Access Key', '2025-04-22 14:25:16.641',
|
||||||
|
'2025-04-22 14:25:16.641'),
|
||||||
|
(38, 'verify_code', 'VerifyCodeExpireTime', '300', 'int', 'Verify code expire time', '2025-04-22 14:25:16.641',
|
||||||
|
'2025-04-22 14:25:16.641'),
|
||||||
|
(39, 'verify_code', 'VerifyCodeLimit', '15', 'int', 'limits of verify code', '2025-04-22 14:25:16.641',
|
||||||
|
'2025-04-22 14:25:16.641'),
|
||||||
|
(40, 'verify_code', 'VerifyCodeInterval', '60', 'int', 'Interval of verify code', '2025-04-22 14:25:16.641',
|
||||||
|
'2025-04-22 14:25:16.641'),
|
||||||
|
(41, 'system', 'Version', '0.2.0(02002)', 'string', 'System Version', '2025-04-22 14:25:16.642',
|
||||||
|
'2025-04-22 14:25:16.642');
|
||||||
|
SELECT setval(pg_get_serial_sequence('"auth_method"', 'id'), COALESCE((SELECT MAX("id") FROM "auth_method"), 1), true);
|
||||||
|
SELECT setval(pg_get_serial_sequence('"subscribe_type"', 'id'), COALESCE((SELECT MAX("id") FROM "subscribe_type"), 1), true);
|
||||||
|
SELECT setval(pg_get_serial_sequence('"system"', 'id'), COALESCE((SELECT MAX("id") FROM "system"), 1), true);
|
||||||
|
|
||||||
|
-- migrate:down
|
||||||
|
-- 000002_init_data.down.sql
|
||||||
|
DELETE
|
||||||
|
FROM "auth_method"
|
||||||
|
WHERE "id" IN (1, 2, 3, 4, 5, 6, 7, 8);
|
||||||
|
DELETE
|
||||||
|
FROM "payment"
|
||||||
|
WHERE "id" = -1;
|
||||||
|
DELETE
|
||||||
|
FROM "subscribe_type"
|
||||||
|
WHERE "id" IN (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14);
|
||||||
|
DELETE
|
||||||
|
FROM "system"
|
||||||
|
WHERE "id" IN
|
||||||
|
(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30,
|
||||||
|
31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41);
|
||||||
|
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
-- migrate:up
|
||||||
|
-- PostgreSQL version of payment/order compatibility migration.
|
||||||
|
ALTER TABLE "order" ADD COLUMN IF NOT EXISTS "payment_id" BIGINT NOT NULL DEFAULT -1;
|
||||||
|
ALTER TABLE "payment" ADD COLUMN IF NOT EXISTS "platform" VARCHAR(100) NOT NULL DEFAULT '';
|
||||||
|
ALTER TABLE "payment" DROP COLUMN IF EXISTS "mark";
|
||||||
|
ALTER TABLE "payment" ADD COLUMN IF NOT EXISTS "description" TEXT;
|
||||||
|
ALTER TABLE "payment" ADD COLUMN IF NOT EXISTS "token" VARCHAR(255) DEFAULT NULL;
|
||||||
|
|
||||||
|
-- migrate:down
|
||||||
|
ALTER TABLE "order" DROP COLUMN IF EXISTS "payment_id";
|
||||||
|
ALTER TABLE "payment" DROP COLUMN IF EXISTS "platform";
|
||||||
|
ALTER TABLE "payment" DROP COLUMN IF EXISTS "description";
|
||||||
|
ALTER TABLE "payment" DROP COLUMN IF EXISTS "token";
|
||||||
|
ALTER TABLE "payment" ADD COLUMN IF NOT EXISTS "mark" VARCHAR(255) DEFAULT NULL;
|
||||||
|
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
-- migrate:up
|
||||||
|
-- migrations/02003_rebuild_rule.up.sql
|
||||||
|
-- Purpose: rebuilding server rule table
|
||||||
|
-- Author: PPanel Team, 2025-04-21
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS "server_rule_group";
|
||||||
|
CREATE TABLE "server_rule_group"
|
||||||
|
(
|
||||||
|
"id" BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL,
|
||||||
|
"name" VARCHAR(64) NOT NULL DEFAULT '',
|
||||||
|
"icon" VARCHAR(255),
|
||||||
|
"tags" TEXT,
|
||||||
|
"rules" TEXT,
|
||||||
|
"enable" BOOLEAN NOT NULL DEFAULT true,
|
||||||
|
"created_at" TIMESTAMP(3),
|
||||||
|
"updated_at" TIMESTAMP(3),
|
||||||
|
PRIMARY KEY ("id"),
|
||||||
|
CONSTRAINT "uni_server_rule_group_name" UNIQUE ("name")
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS "server_rule_group_idx_enable" ON "server_rule_group" ("enable");
|
||||||
|
|
||||||
|
-- migrate:down
|
||||||
|
-- migrations/02003_rebuild_rule.up.sql
|
||||||
|
-- Purpose: Back rebuilding server rule table
|
||||||
|
-- Author: PPanel Team, 2025-04-21
|
||||||
|
DROP TABLE IF EXISTS server_rule_group;
|
||||||
|
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
-- migrate:up
|
||||||
|
CREATE TABLE IF NOT EXISTS "user_device_online_record"
|
||||||
|
(
|
||||||
|
"id" BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||||
|
"user_id" BIGINT NOT NULL,
|
||||||
|
"identifier" VARCHAR(255) NOT NULL,
|
||||||
|
"online_time" TIMESTAMP,
|
||||||
|
"offline_time" TIMESTAMP,
|
||||||
|
"online_seconds" BIGINT,
|
||||||
|
"duration_days" BIGINT,
|
||||||
|
"created_at" TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
ALTER TABLE "user_subscribe" ADD COLUMN IF NOT EXISTS "finished_at" TIMESTAMP NULL;
|
||||||
|
ALTER TABLE "application_config" ADD COLUMN IF NOT EXISTS "invitation_link" TEXT NULL DEFAULT NULL;
|
||||||
|
ALTER TABLE "application_config" ADD COLUMN IF NOT EXISTS "kr_website_id" VARCHAR(255) NULL DEFAULT NULL;
|
||||||
|
|
||||||
|
-- migrate:down
|
||||||
|
DROP TABLE IF EXISTS "user_device_online_record";
|
||||||
|
ALTER TABLE "user_subscribe" DROP COLUMN IF EXISTS "finished_at";
|
||||||
|
ALTER TABLE "application_config" DROP COLUMN IF EXISTS "invitation_link";
|
||||||
|
ALTER TABLE "application_config" DROP COLUMN IF EXISTS "kr_website_id";
|
||||||
|
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
-- migrate:up
|
||||||
|
-- migrations/02008_create_user_reset_subscribe_log.up.sql
|
||||||
|
-- Purpose: Create user_reset_subscribe_log table
|
||||||
|
-- Author: PPanel Team, 2025-04-22
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS "user_reset_subscribe_log"
|
||||||
|
(
|
||||||
|
"id" BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL PRIMARY KEY,
|
||||||
|
"user_id" BIGINT NOT NULL,
|
||||||
|
"type" SMALLINT NOT NULL,
|
||||||
|
"order_no" VARCHAR(255) DEFAULT NULL,
|
||||||
|
"user_subscribe_id" BIGINT NOT NULL,
|
||||||
|
"created_at" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS "user_reset_subscribe_log_idx_user_id" ON "user_reset_subscribe_log" ("user_id");
|
||||||
|
CREATE INDEX IF NOT EXISTS "user_reset_subscribe_log_idx_user_subscribe_id" ON "user_reset_subscribe_log" ("user_subscribe_id");
|
||||||
|
|
||||||
|
-- migrate:down
|
||||||
|
-- migrations/02008_create_user_reset_subscribe_log.down.sql
|
||||||
|
-- Purpose: Drop user_reset_subscribe_log table
|
||||||
|
-- Author: PPanel Team, 2025-04-22
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS "user_reset_subscribe_log";
|
||||||
|
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
-- migrate:up
|
||||||
|
ALTER TABLE "server_rule_group"
|
||||||
|
ADD COLUMN "default" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
ADD COLUMN "type" VARCHAR(100) NOT NULL DEFAULT '';
|
||||||
|
|
||||||
|
-- migrate:down
|
||||||
|
ALTER TABLE "server_rule_group"
|
||||||
|
DROP COLUMN "default",
|
||||||
|
DROP COLUMN "type";
|
||||||
|
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
-- migrate:up
|
||||||
|
DROP TABLE IF EXISTS "email_task";
|
||||||
|
CREATE TABLE "email_task" (
|
||||||
|
"id" BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL,
|
||||||
|
"subject" varchar(255) NOT NULL,
|
||||||
|
"content" text NOT NULL,
|
||||||
|
"recipient" text NOT NULL,
|
||||||
|
"scope" varchar(50) NOT NULL,
|
||||||
|
"register_start_time" TIMESTAMP(3) DEFAULT NULL,
|
||||||
|
"register_end_time" TIMESTAMP(3) DEFAULT NULL,
|
||||||
|
"additional" text,
|
||||||
|
"scheduled" TIMESTAMP(3) NOT NULL,
|
||||||
|
"interval" SMALLINT NOT NULL,
|
||||||
|
"limit" BIGINT NOT NULL,
|
||||||
|
"status" SMALLINT NOT NULL,
|
||||||
|
"errors" text NOT NULL,
|
||||||
|
"total" BIGINT NOT NULL DEFAULT '0',
|
||||||
|
"current" BIGINT NOT NULL DEFAULT '0',
|
||||||
|
"created_at" TIMESTAMP(3) DEFAULT NULL,
|
||||||
|
"updated_at" TIMESTAMP(3) DEFAULT NULL,
|
||||||
|
PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- migrate:down
|
||||||
|
DROP TABLE IF EXISTS "email_task";
|
||||||
|
|
||||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,9 @@
|
|||||||
|
-- migrate:up
|
||||||
|
INSERT INTO "system" ("id", "category", "key", "value", "type", "desc", "created_at", "updated_at")
|
||||||
|
VALUES
|
||||||
|
(42, 'subscribe', 'UserAgentLimit', 'false', 'bool', 'User Agent Limit', '2025-04-22 14:25:16.637', '2025-04-22 14:25:16.637'),
|
||||||
|
(43, 'subscribe', 'UserAgentList', '', 'string', 'User Agent List', '2025-04-22 14:25:16.637','2025-04-22 14:25:16.637') ON CONFLICT DO NOTHING;
|
||||||
|
SELECT setval(pg_get_serial_sequence('"system"', 'id'), COALESCE((SELECT MAX("id") FROM "system"), 1), true);
|
||||||
|
|
||||||
|
-- migrate:down
|
||||||
|
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
-- migrate:up
|
||||||
|
DROP TABLE IF EXISTS "application";
|
||||||
|
DROP TABLE IF EXISTS "application_version";
|
||||||
|
DROP TABLE IF EXISTS "application_config";
|
||||||
|
|
||||||
|
-- migrate:down
|
||||||
|
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
-- migrate:up
|
||||||
|
DROP TABLE IF EXISTS "user_balance_log";
|
||||||
|
DROP TABLE IF EXISTS "user_commission_log";
|
||||||
|
DROP TABLE IF EXISTS "user_gift_amount_log";
|
||||||
|
DROP TABLE IF EXISTS "user_login_log";
|
||||||
|
DROP TABLE IF EXISTS "user_reset_subscribe_log";
|
||||||
|
DROP TABLE IF EXISTS "user_subscribe_log";
|
||||||
|
DROP TABLE IF EXISTS "message_log";
|
||||||
|
DROP TABLE IF EXISTS "system_logs";
|
||||||
|
CREATE TABLE "system_logs" (
|
||||||
|
"id" BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL,
|
||||||
|
"type" SMALLINT NOT NULL DEFAULT '0',
|
||||||
|
"date" varchar(20) DEFAULT NULL,
|
||||||
|
"object_id" bigint NOT NULL DEFAULT '0',
|
||||||
|
"content" text NOT NULL,
|
||||||
|
"created_at" TIMESTAMP(3) DEFAULT NULL,
|
||||||
|
PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS "system_logs_idx_type" ON "system_logs" ("type");
|
||||||
|
CREATE INDEX IF NOT EXISTS "system_logs_idx_object_id" ON "system_logs" ("object_id");
|
||||||
|
|
||||||
|
-- migrate:down
|
||||||
|
CREATE TABLE IF NOT EXISTS "user_balance_log"
|
||||||
|
(
|
||||||
|
"id" BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL,
|
||||||
|
"user_id" bigint NOT NULL,
|
||||||
|
"amount" bigint NOT NULL,
|
||||||
|
"type" SMALLINT NOT NULL,
|
||||||
|
"order_id" bigint DEFAULT NULL,
|
||||||
|
"balance" bigint NOT NULL,
|
||||||
|
"created_at" TIMESTAMP(3) DEFAULT NULL,
|
||||||
|
PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS "user_balance_log_idx_user_id" ON "user_balance_log" ("user_id");
|
||||||
|
CREATE TABLE IF NOT EXISTS "user_commission_log"
|
||||||
|
(
|
||||||
|
"id" BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL,
|
||||||
|
"user_id" bigint NOT NULL,
|
||||||
|
"order_no" varchar(191) DEFAULT NULL,
|
||||||
|
"amount" bigint NOT NULL,
|
||||||
|
"created_at" TIMESTAMP(3) DEFAULT NULL,
|
||||||
|
PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS "user_commission_log_idx_user_id" ON "user_commission_log" ("user_id");
|
||||||
|
CREATE TABLE IF NOT EXISTS "user_gift_amount_log"
|
||||||
|
(
|
||||||
|
"id" BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL,
|
||||||
|
"user_id" bigint NOT NULL,
|
||||||
|
"user_subscribe_id" bigint DEFAULT NULL,
|
||||||
|
"order_no" varchar(191) DEFAULT NULL,
|
||||||
|
"type" SMALLINT NOT NULL,
|
||||||
|
"amount" bigint NOT NULL,
|
||||||
|
"balance" bigint NOT NULL,
|
||||||
|
"remark" varchar(255) DEFAULT '',
|
||||||
|
"created_at" TIMESTAMP(3) DEFAULT NULL,
|
||||||
|
PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS "user_gift_amount_log_idx_user_id" ON "user_gift_amount_log" ("user_id");
|
||||||
|
CREATE TABLE IF NOT EXISTS "user_login_log"
|
||||||
|
(
|
||||||
|
"id" BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL,
|
||||||
|
"user_id" bigint NOT NULL,
|
||||||
|
"login_ip" varchar(255) NOT NULL,
|
||||||
|
"user_agent" text NOT NULL,
|
||||||
|
"success" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
"created_at" TIMESTAMP(3) DEFAULT NULL,
|
||||||
|
PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS "user_login_log_idx_user_id" ON "user_login_log" ("user_id");
|
||||||
|
CREATE TABLE IF NOT EXISTS "user_reset_subscribe_log"
|
||||||
|
(
|
||||||
|
"id" BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL PRIMARY KEY,
|
||||||
|
"user_id" BIGINT NOT NULL,
|
||||||
|
"type" SMALLINT NOT NULL,
|
||||||
|
"order_no" VARCHAR(255) DEFAULT NULL,
|
||||||
|
"user_subscribe_id" BIGINT NOT NULL,
|
||||||
|
"created_at" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS "user_reset_subscribe_log_idx_user_id" ON "user_reset_subscribe_log" ("user_id");
|
||||||
|
CREATE INDEX IF NOT EXISTS "user_reset_subscribe_log_idx_user_subscribe_id" ON "user_reset_subscribe_log" ("user_subscribe_id");
|
||||||
|
CREATE TABLE IF NOT EXISTS "user_subscribe_log"
|
||||||
|
(
|
||||||
|
"id" BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL,
|
||||||
|
"user_id" bigint NOT NULL,
|
||||||
|
"user_subscribe_id" bigint NOT NULL,
|
||||||
|
"token" varchar(255) NOT NULL,
|
||||||
|
"ip" varchar(255) NOT NULL,
|
||||||
|
"user_agent" text NOT NULL,
|
||||||
|
"created_at" TIMESTAMP(3) DEFAULT NULL,
|
||||||
|
PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS "user_subscribe_log_idx_user_id" ON "user_subscribe_log" ("user_id");
|
||||||
|
CREATE INDEX IF NOT EXISTS "user_subscribe_log_idx_user_subscribe_id" ON "user_subscribe_log" ("user_subscribe_id");
|
||||||
|
CREATE TABLE IF NOT EXISTS "message_log"
|
||||||
|
(
|
||||||
|
"id" BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL,
|
||||||
|
"type" varchar(50) NOT NULL DEFAULT 'email',
|
||||||
|
"platform" varchar(50) NOT NULL DEFAULT 'smtp',
|
||||||
|
"to" text NOT NULL,
|
||||||
|
"subject" varchar(255) NOT NULL DEFAULT '',
|
||||||
|
"content" text,
|
||||||
|
"status" SMALLINT NOT NULL DEFAULT '0',
|
||||||
|
"created_at" TIMESTAMP(3) DEFAULT NULL,
|
||||||
|
"updated_at" TIMESTAMP(3) DEFAULT NULL,
|
||||||
|
PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
DROP TABLE IF EXISTS "system_logs";
|
||||||
|
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
-- migrate:up
|
||||||
|
CREATE TABLE IF NOT EXISTS "servers" (
|
||||||
|
"id" BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL,
|
||||||
|
"name" varchar(100) NOT NULL DEFAULT '',
|
||||||
|
"country" varchar(128) NOT NULL DEFAULT '',
|
||||||
|
"city" varchar(128) NOT NULL DEFAULT '',
|
||||||
|
"ratio" decimal(4,2) NOT NULL DEFAULT '0.00',
|
||||||
|
"address" varchar(100) NOT NULL DEFAULT '',
|
||||||
|
"sort" bigint NOT NULL DEFAULT '0',
|
||||||
|
"protocols" text,
|
||||||
|
"last_reported_at" TIMESTAMP(3) DEFAULT NULL,
|
||||||
|
"created_at" TIMESTAMP(3) DEFAULT NULL,
|
||||||
|
"updated_at" TIMESTAMP(3) DEFAULT NULL,
|
||||||
|
PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
CREATE TABLE IF NOT EXISTS "nodes" (
|
||||||
|
"id" BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL,
|
||||||
|
"name" varchar(100) NOT NULL DEFAULT '',
|
||||||
|
"tags" varchar(255) NOT NULL DEFAULT '',
|
||||||
|
"port" INTEGER NOT NULL DEFAULT '0',
|
||||||
|
"address" varchar(255) NOT NULL DEFAULT '',
|
||||||
|
"server_id" bigint NOT NULL DEFAULT '0',
|
||||||
|
"protocol" varchar(100) NOT NULL DEFAULT '',
|
||||||
|
"enabled" BOOLEAN NOT NULL DEFAULT true,
|
||||||
|
"created_at" TIMESTAMP(3) DEFAULT NULL,
|
||||||
|
"updated_at" TIMESTAMP(3) DEFAULT NULL,
|
||||||
|
PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- migrate:down
|
||||||
|
DROP TABLE IF EXISTS "nodes";
|
||||||
|
DROP TABLE IF EXISTS "servers";
|
||||||
|
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
-- migrate:up
|
||||||
|
ALTER TABLE "subscribe"
|
||||||
|
ADD COLUMN "nodes" VARCHAR(255) NOT NULL DEFAULT '' ,
|
||||||
|
ADD COLUMN "node_tags" VARCHAR(255) NOT NULL DEFAULT '' ,
|
||||||
|
DROP COLUMN "server",
|
||||||
|
DROP COLUMN "server_group";
|
||||||
|
DROP TABLE IF EXISTS "server_rule_group";
|
||||||
|
|
||||||
|
-- migrate:down
|
||||||
|
ALTER TABLE "subscribe"
|
||||||
|
DROP COLUMN "nodes",
|
||||||
|
DROP COLUMN "node_tags",
|
||||||
|
ADD COLUMN "server" VARCHAR(255) NOT NULL DEFAULT '' ,
|
||||||
|
ADD COLUMN "server_group" VARCHAR(255) NOT NULL DEFAULT '';
|
||||||
|
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
-- migrate:up
|
||||||
|
INSERT INTO "system" ("category", "key", "value", "type", "desc", "created_at", "updated_at")
|
||||||
|
VALUES
|
||||||
|
('log', 'AutoClear', 'true', 'bool', 'Auto Clear Log', '2025-04-22 14:25:16.637', '2025-04-22 14:25:16.637'),
|
||||||
|
('log', 'ClearDays', '7', 'int', 'Clear Days', '2025-04-22 14:25:16.637','2025-04-22 14:25:16.637') ON CONFLICT DO NOTHING;
|
||||||
|
|
||||||
|
-- migrate:down
|
||||||
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user