feat(payment): add complete payment system with multi-provider support
Add a full payment and subscription system supporting EasyPay (Alipay/WeChat), Stripe, and direct Alipay/WeChat Pay providers with multi-instance load balancing.
This commit is contained in:
@@ -48,6 +48,7 @@ export const useAdminSettingsStore = defineStore('adminSettings', () => {
|
||||
const opsMonitoringEnabled = ref(readCachedBool('ops_monitoring_enabled_cached', true))
|
||||
const opsRealtimeMonitoringEnabled = ref(readCachedBool('ops_realtime_monitoring_enabled_cached', true))
|
||||
const opsQueryModeDefault = ref(readCachedString('ops_query_mode_default_cached', 'auto'))
|
||||
const paymentEnabled = ref(readCachedBool('payment_enabled_cached', false))
|
||||
const customMenuItems = ref<CustomMenuItem[]>([])
|
||||
|
||||
async function fetch(force = false): Promise<void> {
|
||||
@@ -56,7 +57,10 @@ export const useAdminSettingsStore = defineStore('adminSettings', () => {
|
||||
|
||||
loading.value = true
|
||||
try {
|
||||
const settings = await adminAPI.settings.getSettings()
|
||||
const [settings, paymentConfigResp] = await Promise.all([
|
||||
adminAPI.settings.getSettings(),
|
||||
adminAPI.payment.getConfig()
|
||||
])
|
||||
opsMonitoringEnabled.value = settings.ops_monitoring_enabled ?? true
|
||||
writeCachedBool('ops_monitoring_enabled_cached', opsMonitoringEnabled.value)
|
||||
|
||||
@@ -68,6 +72,9 @@ export const useAdminSettingsStore = defineStore('adminSettings', () => {
|
||||
|
||||
customMenuItems.value = Array.isArray(settings.custom_menu_items) ? settings.custom_menu_items : []
|
||||
|
||||
paymentEnabled.value = paymentConfigResp.data?.enabled ?? false
|
||||
writeCachedBool('payment_enabled_cached', paymentEnabled.value)
|
||||
|
||||
loaded.value = true
|
||||
} catch (err) {
|
||||
// Keep cached/default value: do not "flip" the UI based on a transient fetch failure.
|
||||
@@ -90,6 +97,12 @@ export const useAdminSettingsStore = defineStore('adminSettings', () => {
|
||||
loaded.value = true
|
||||
}
|
||||
|
||||
function setPaymentEnabledLocal(value: boolean) {
|
||||
paymentEnabled.value = value
|
||||
writeCachedBool('payment_enabled_cached', value)
|
||||
loaded.value = true
|
||||
}
|
||||
|
||||
function setOpsQueryModeDefaultLocal(value: string) {
|
||||
opsQueryModeDefault.value = value || 'auto'
|
||||
writeCachedString('ops_query_mode_default_cached', opsQueryModeDefault.value)
|
||||
@@ -126,10 +139,12 @@ export const useAdminSettingsStore = defineStore('adminSettings', () => {
|
||||
opsMonitoringEnabled,
|
||||
opsRealtimeMonitoringEnabled,
|
||||
opsQueryModeDefault,
|
||||
paymentEnabled,
|
||||
customMenuItems,
|
||||
fetch,
|
||||
setOpsMonitoringEnabledLocal,
|
||||
setOpsRealtimeMonitoringEnabledLocal,
|
||||
setPaymentEnabledLocal,
|
||||
setOpsQueryModeDefaultLocal
|
||||
}
|
||||
})
|
||||
|
||||
@@ -329,6 +329,7 @@ export const useAppStore = defineStore('app', () => {
|
||||
hide_ccs_import_button: false,
|
||||
purchase_subscription_enabled: false,
|
||||
purchase_subscription_url: '',
|
||||
payment_enabled: false,
|
||||
custom_menu_items: [],
|
||||
custom_endpoints: [],
|
||||
linuxdo_oauth_enabled: false,
|
||||
|
||||
@@ -9,6 +9,7 @@ export { useAdminSettingsStore } from './adminSettings'
|
||||
export { useSubscriptionStore } from './subscriptions'
|
||||
export { useOnboardingStore } from './onboarding'
|
||||
export { useAnnouncementStore } from './announcements'
|
||||
export { usePaymentStore } from './payment'
|
||||
|
||||
// Re-export types for convenience
|
||||
export type { User, LoginRequest, RegisterRequest, AuthResponse } from '@/types'
|
||||
|
||||
101
frontend/src/stores/payment.ts
Normal file
101
frontend/src/stores/payment.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
/**
|
||||
* Payment Store
|
||||
* Manages payment configuration, current order state, and subscription plans
|
||||
*/
|
||||
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
import { paymentAPI } from '@/api/payment'
|
||||
import type { PaymentConfig, PaymentOrder, SubscriptionPlan, CreateOrderRequest } from '@/types/payment'
|
||||
|
||||
export const usePaymentStore = defineStore('payment', () => {
|
||||
// ==================== State ====================
|
||||
|
||||
/** Payment configuration from backend */
|
||||
const config = ref<PaymentConfig | null>(null)
|
||||
/** Currently active order (for payment flow) */
|
||||
const currentOrder = ref<PaymentOrder | null>(null)
|
||||
/** Available subscription plans */
|
||||
const plans = ref<SubscriptionPlan[]>([])
|
||||
|
||||
const configLoading = ref(false)
|
||||
const configLoaded = ref(false)
|
||||
|
||||
// ==================== Actions ====================
|
||||
|
||||
/** Fetch payment configuration */
|
||||
async function fetchConfig(force = false): Promise<PaymentConfig | null> {
|
||||
if (configLoaded.value && !force) return config.value
|
||||
if (configLoading.value) return config.value
|
||||
|
||||
configLoading.value = true
|
||||
try {
|
||||
const response = await paymentAPI.getConfig()
|
||||
config.value = response.data
|
||||
configLoaded.value = true
|
||||
return config.value
|
||||
} catch (error: unknown) {
|
||||
console.error('[payment] Failed to fetch config:', error)
|
||||
return null
|
||||
} finally {
|
||||
configLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** Fetch available subscription plans */
|
||||
async function fetchPlans(): Promise<SubscriptionPlan[]> {
|
||||
try {
|
||||
const response = await paymentAPI.getPlans()
|
||||
// Backend returns features as newline-separated string; parse to array
|
||||
plans.value = (response.data || []).map((p: Omit<SubscriptionPlan, 'features'> & { features: string | string[] }) => ({
|
||||
...p,
|
||||
features: typeof p.features === 'string'
|
||||
? p.features.split('\n').map((f: string) => f.trim()).filter(Boolean)
|
||||
: (p.features || []),
|
||||
}))
|
||||
return plans.value
|
||||
} catch (error: unknown) {
|
||||
console.error('[payment] Failed to fetch plans:', error)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
/** Create a new order and set it as current */
|
||||
async function createOrder(params: CreateOrderRequest) {
|
||||
const response = await paymentAPI.createOrder(params)
|
||||
return response.data
|
||||
}
|
||||
|
||||
/** Poll order status by ID */
|
||||
async function pollOrderStatus(orderId: number): Promise<PaymentOrder | null> {
|
||||
try {
|
||||
const response = await paymentAPI.getOrder(orderId)
|
||||
const order = response.data
|
||||
if (currentOrder.value?.id === orderId) {
|
||||
currentOrder.value = order
|
||||
}
|
||||
return order
|
||||
} catch (error: unknown) {
|
||||
console.error('[payment] Failed to poll order status:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/** Clear current order state */
|
||||
function clearCurrentOrder() {
|
||||
currentOrder.value = null
|
||||
}
|
||||
|
||||
return {
|
||||
config,
|
||||
currentOrder,
|
||||
plans,
|
||||
configLoading,
|
||||
configLoaded,
|
||||
fetchConfig,
|
||||
fetchPlans,
|
||||
createOrder,
|
||||
pollOrderStatus,
|
||||
clearCurrentOrder
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user