增加获取域名续费价格的api端点及实现域名续费的对话框

This commit is contained in:
jingrow 2025-08-02 23:29:37 +08:00
parent 895d19a47e
commit 1e2728b0a9
2 changed files with 542 additions and 164 deletions

View File

@ -1,110 +1,230 @@
<template>
<div class="p-6">
<div class="mb-6">
<h2 class="text-xl font-semibold text-gray-900 mb-2">域名续费</h2>
<p class="text-sm text-gray-600">
为域名 <span class="font-medium">{{ domainDoc?.domain }}</span> 续费
</p>
</div>
<Dialog :options="{ title: '域名续费', size: 'lg' }" v-model="show">
<!-- 第一步选择续费周期和支付方式 -->
<template v-if="!showPaymentProcessing" #body-content>
<div class="p-4 sm:p-6">
<div class="mb-6">
<p class="mt-1 text-sm text-gray-600">
域名 <span class="font-medium">{{ domainDoc?.domain }}</span>
</p>
</div>
<div class="space-y-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">续费时长</label>
<select v-model="renewalPeriod" class="w-full border rounded px-3 py-2">
<option v-for="period in renewalPeriods" :key="period.value" :value="period.value">
{{ period.label }}
</option>
</select>
</div>
<div v-if="domainPrice" class="border-t border-gray-200 pt-4">
<div class="flex justify-between items-center">
<div class="text-sm text-gray-600">年度费用</div>
<div class="font-medium">
¥ {{ domainPrice }}
<span class="text-gray-500 text-sm">(年付)</span>
<div class="mb-6">
<label class="block text-sm font-medium text-gray-700 mb-2">
续费时长
</label>
<div class="flex flex-wrap gap-3">
<button
v-for="period in renewalPeriods"
:key="period.years"
@click="renewalPeriod = period.years"
:class="[
'px-4 py-2 text-sm font-medium rounded-md border',
renewalPeriod === period.years
? 'bg-blue-50 border-blue-500 text-blue-700'
: 'border-gray-300 text-gray-700 hover:bg-gray-50'
]"
>
{{ period.name }}
<span v-if="period.discount" class="ml-1 text-green-600">-{{ period.discount }}%</span>
</button>
</div>
</div>
<div class="flex justify-between items-center mt-2">
<div class="text-sm text-gray-600">续费时长</div>
<div class="font-medium">{{ renewalPeriod }} </div>
</div>
<div class="flex justify-between items-center mt-2 text-lg font-bold">
<div>总计</div>
<div>¥ {{ getTotalAmount() }}</div>
</div>
</div>
<div class="border-t border-gray-200 pt-4">
<label class="block text-sm font-medium text-gray-700 mb-3">
选择支付方式
</label>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<button
class="p-4 border rounded-lg flex flex-col items-center justify-center hover:bg-gray-50 transition-all"
:class="{'border-blue-500 border-2 shadow-sm': selectedPaymentMethod === 'balance', 'border-gray-200': selectedPaymentMethod !== 'balance'}"
@click="selectedPaymentMethod = 'balance'"
>
<span class="text-gray-800 font-medium">余额支付</span>
</button>
<button
class="p-4 border rounded-lg hover:bg-gray-50 transition-all"
:class="{'border-blue-500 border-2 shadow-sm': selectedPaymentMethod === 'alipay', 'border-gray-200': selectedPaymentMethod !== 'alipay'}"
@click="selectedPaymentMethod = 'alipay'"
>
<div class="flex flex-col items-center">
<div class="mb-2">
<AlipayLogo class="h-8" />
</div>
<div class="mb-6 border-t border-gray-200 pt-4">
<div class="flex justify-between items-center">
<div class="text-sm text-gray-600">年度费用</div>
<div class="font-medium">
<span v-if="isPriceLoading" class="text-gray-400">获取中...</span>
<span v-else-if="domainPrice">¥ {{ domainPrice }}</span>
<span class="text-gray-500 text-sm"> (年付)</span>
</div>
</button>
<button
class="p-4 border rounded-lg hover:bg-gray-50 transition-all"
:class="{'border-blue-500 border-2 shadow-sm': selectedPaymentMethod === 'wechatpay', 'border-gray-200': selectedPaymentMethod !== 'wechatpay'}"
@click="selectedPaymentMethod = 'wechatpay'"
>
<div class="flex flex-col items-center">
<div class="mb-2">
<WeChatPayLogo class="h-8" />
</div>
</div>
<div class="flex justify-between items-center mt-2">
<div class="text-sm text-gray-600">续费时长</div>
<div class="font-medium">{{ renewalPeriod }} </div>
</div>
<div v-if="discountPercentage > 0" class="flex justify-between items-center mt-2">
<div class="text-sm text-green-600">折扣</div>
<div class="font-medium text-green-600">-{{ discountPercentage }}%</div>
</div>
<div class="flex justify-between items-center mt-2 text-lg font-bold">
<div>总计</div>
<div>
<span v-if="isPriceLoading" class="text-gray-400">获取中...</span>
<span v-else-if="domainPrice">¥ {{ totalAmount }}</span>
</div>
</div>
</div>
<div class="mb-6 border-t border-gray-200 pt-4">
<label class="block text-sm font-medium text-gray-700 mb-3">
选择支付方式
</label>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mt-2">
<button
class="p-4 border rounded-lg flex flex-col items-center justify-center hover:bg-gray-50 transition-all"
:class="{'border-blue-500 border-2 shadow-sm': selectedPaymentMethod === 'balance', 'border-gray-200': selectedPaymentMethod !== 'balance'}"
@click="selectedPaymentMethod = 'balance'"
>
<span class="text-gray-800 font-medium">余额支付</span>
</button>
<button
class="p-4 border rounded-lg hover:bg-gray-50 transition-all"
:class="{'border-blue-500 border-2 shadow-sm': selectedPaymentMethod === 'alipay', 'border-gray-200': selectedPaymentMethod !== 'alipay'}"
@click="selectedPaymentMethod = 'alipay'"
>
<div class="flex flex-col items-center">
<div class="mb-2">
<AlipayLogo class="h-8" />
</div>
</div>
</button>
<button
class="p-4 border rounded-lg hover:bg-gray-50 transition-all"
:class="{'border-blue-500 border-2 shadow-sm': selectedPaymentMethod === 'wechatpay', 'border-gray-200': selectedPaymentMethod !== 'wechatpay'}"
@click="selectedPaymentMethod = 'wechatpay'"
>
<div class="flex flex-col items-center">
<div class="mb-2">
<WeChatPayLogo class="h-8" />
</div>
</div>
</button>
</div>
</div>
<div v-if="error" class="mt-4 p-3 bg-red-50 text-red-700 rounded-md text-sm">
{{ error }}
</div>
</div>
</template>
<!-- 第二步处理支付 -->
<template v-else #body-content>
<div class="p-4 sm:p-6">
<!-- 加载中状态 -->
<div v-if="isProcessingPayment" class="text-center py-8">
<div class="h-12 w-12 mx-auto animate-spin text-blue-600 mb-4 flex items-center justify-center">
<i class="fe fe-loader text-3xl"></i>
</div>
<p class="text-gray-700 text-lg">正在处理支付请稍候...</p>
</div>
<!-- 支付成功状态 -->
<div v-else-if="paymentSuccess" class="text-center py-8">
<div class="flex justify-center mb-6">
<div class="rounded-full bg-green-100 p-3">
<svg xmlns="http://www.w3.org/2000/svg" class="h-12 w-12 text-green-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
</svg>
</div>
</div>
<h3 class="text-xl font-medium text-gray-900 mb-2">续费成功</h3>
<p class="text-gray-600 mb-2">
您的域名已成功续费 {{ renewalPeriod }}
</p>
</div>
<!-- 微信支付状态 -->
<div v-else-if="selectedPaymentMethod === 'wechatpay' && paymentQrCode" class="text-center">
<div class="relative mb-6">
<div class="flex items-center justify-center">
<WeChatPayLogo class="h-10" />
</div>
</div>
<div class="text-center mb-6">
<div class="text-sm text-gray-600 mb-2">扫一扫付款</div>
<div class="text-3xl font-bold text-[#FF0036]">{{ order?.total_amount }} </div>
</div>
<div class="flex justify-center">
<div class="qrcode-container bg-white p-4 border border-gray-100 shadow-sm rounded-lg">
<img
:src="paymentQrCodeImage"
alt="微信支付二维码"
class="qrcode-image"
/>
</div>
</div>
<div class="mt-6 text-center">
<p class="text-gray-600">
请使用微信扫描二维码完成支付
</p>
<p class="text-gray-500 text-sm mt-2">二维码有效期 15 分钟</p>
</div>
</div>
<!-- 支付宝支付状态 -->
<div v-else-if="selectedPaymentMethod === 'alipay' && paymentUrl" class="text-center py-6">
<div class="mb-6">
<AlipayLogo class="h-10 mx-auto" />
</div>
<h3 class="text-lg font-medium text-gray-900 mb-4">请在新页面完成支付宝支付</h3>
<p class="text-gray-600 mb-6">
如果没有自动跳转请点击下方按钮打开支付页面
</p>
<div class="space-y-4">
<button
class="w-full px-6 py-3 bg-[#1677FF] text-white rounded-lg hover:bg-[#0E5FD8] transition-colors shadow-sm"
@click="window.open(paymentUrl, '_blank')"
>
打开支付页面
</button>
<p class="text-gray-600 text-sm mt-4">
支付完成后请稍等片刻系统会自动刷新页面
</p>
</div>
</div>
<div v-if="error" class="mt-4 p-3 bg-red-50 text-red-700 rounded-md text-sm">
{{ error }}
</div>
</div>
</template>
<template #actions>
<div class="w-full">
<button v-if="!showPaymentProcessing && !paymentSuccess"
type="button"
class="w-full px-4 py-2 bg-[#1fc76f] border border-transparent rounded-md text-sm font-medium text-white hover:bg-[#19b862] focus:outline-none disabled:bg-gray-300 disabled:cursor-not-allowed"
@click="createRenewalOrder"
:disabled="isLoading || isPriceLoading || !domainPrice"
>
{{ isLoading ? '处理中...' : (isPriceLoading ? '获取价格中...' : '确认续费') }}
</button>
<div class="flex justify-between w-full" v-else>
<button
type="button"
class="px-4 py-2 bg-[#1fc76f] border border-transparent rounded-md text-sm font-medium text-white hover:bg-[#19b862] focus:outline-none ml-auto"
@click="cancel"
v-if="paymentSuccess"
>
关闭
</button>
</div>
</div>
<div v-if="error" class="p-3 bg-red-50 text-red-700 rounded-md text-sm">
{{ error }}
</div>
</div>
<div class="flex justify-end gap-3 mt-6">
<button
@click="$emit('close')"
class="px-4 py-2 text-gray-700 bg-gray-100 rounded-md hover:bg-gray-200 transition-colors"
>
取消
</button>
<button
@click="processRenewal"
:disabled="!selectedPaymentMethod || isLoading"
class="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 disabled:bg-gray-300 transition-colors"
>
{{ isLoading ? '处理中...' : '确认续费' }}
</button>
</div>
</div>
</template>
</Dialog>
</template>
<script>
import { toast } from 'vue-sonner';
import { Dialog } from 'jingrow-ui';
import { DashboardError } from '../utils/error';
import AlipayLogo from '../logo/AlipayLogo.vue';
import WeChatPayLogo from '../logo/WeChatPayLogo.vue';
export default {
name: 'JsiteDomainRenewalDialog',
components: {
Dialog,
AlipayLogo,
WeChatPayLogo
},
@ -115,108 +235,297 @@ export default {
},
domainDoc: {
type: Object,
required: true
},
onSuccess: {
type: Function,
default: () => {}
default: null
}
},
emits: ['success'],
data() {
return {
renewalPeriod: 1,
selectedPaymentMethod: null,
show: true,
renewalPeriod: 1, // 1
selectedPaymentMethod: null, //
order: null,
showPaymentProcessing: false,
paymentSuccess: false,
error: null,
isProcessingPayment: false,
paymentUrl: null,
paymentQrCode: null,
paymentQrCodeImage: null,
checkInterval: null,
domainPrice: null,
renewalPeriods: [
{ value: 1, label: '1年' },
{ value: 2, label: '2年' },
{ value: 3, label: '3年' },
{ value: 5, label: '5年' },
{ value: 10, label: '10年' }
],
isLoading: false,
error: null
{ years: 1, name: '1年', discount: 0 },
{ years: 2, name: '2年', discount: 0 },
{ years: 3, name: '3年', discount: 0 },
{ years: 5, name: '5年', discount: 0 },
{ years: 10, name: '10年', discount: 0 }
]
};
},
mounted() {
this.getDomainPrice();
},
methods: {
async getDomainPrice() {
try {
const response = await this.$resources.getDomainPrice.submit({
domain: this.domainDoc.domain,
year: 1
});
if (response && response.data && response.data.price) {
this.domainPrice = response.data.price;
} else {
this.domainPrice = this.domainDoc.price || 50;
}
} catch (error) {
this.domainPrice = this.domainDoc.price || 50;
}
computed: {
discountPercentage() {
const period = this.renewalPeriods.find(p => p.years === this.renewalPeriod);
return period ? period.discount : 0;
},
getTotalAmount() {
const yearlyPrice = this.domainPrice || 0;
return (yearlyPrice * this.renewalPeriod).toFixed(2);
totalAmount() {
if (!this.domainPrice) return '';
const basePrice = this.domainPrice * this.renewalPeriod;
const discount = basePrice * (this.discountPercentage / 100);
return (basePrice - discount).toFixed(2);
},
async processRenewal() {
if (!this.selectedPaymentMethod) {
this.error = '请选择支付方式';
return;
}
this.error = null;
this.isLoading = true;
try {
const response = await this.$resources.renewDomain.submit({
domain: this.domainDoc.domain,
period: this.renewalPeriod,
payment_method: this.selectedPaymentMethod
});
if (response.success) {
toast.success('续费请求已提交');
this.$emit('close');
this.onSuccess(response);
} else {
this.error = response.message || '续费失败';
}
} catch (error) {
this.error = error.message || '续费失败';
} finally {
this.isLoading = false;
}
isLoading() {
return this.$resources.createRenewalOrder.loading;
},
isPriceLoading() {
return this.$resources.getDomainPrice.loading;
}
},
resources: {
getDomainPrice() {
createRenewalOrder() {
return {
url: 'jcloud.api.domain_west.get_west_domain_price',
onSuccess(response) {
if (response && response.data && response.data.price) {
this.domainPrice = response.data.price;
url: 'jcloud.api.domain_west.create_domain_renew_order',
validate() {
if (!this.domain) {
throw new DashboardError('缺少域名信息');
}
},
onSuccess(data) {
if (!data.success) {
this.error = data.message || '创建续费订单失败';
return;
}
//
this.order = data.order;
this.showPaymentProcessing = true;
//
this.processPayment();
},
onError(error) {
this.domainPrice = this.domainDoc.price || 50;
this.error = error.message || '创建续费订单失败';
}
};
},
renewDomain() {
processBalancePayment() {
return {
url: 'jcloud.api.domain_west.west_domain_renew',
url: 'jcloud.api.billing.process_balance_payment_for_domain_order',
params: {},
validate() {
if (!this.order || !this.order.order_id) {
throw new DashboardError('缺少订单信息');
}
},
onSuccess(response) {
return response;
if (response.status === "Error" || response.success === false) {
toast.error(response.message || '支付失败,请确保余额充足');
this.error = response.message || '余额不足';
this.isProcessingPayment = false;
return;
}
//
this.$emit('success', {
order: this.order,
renewal_period: this.renewalPeriod,
message: response.message || '支付成功'
});
this.isProcessingPayment = false;
this.paymentSuccess = true;
},
onError(error) {
throw error;
this.error = error.message || '余额支付处理失败';
this.isProcessingPayment = false;
}
};
},
processAlipayPayment() {
return {
url: 'jcloud.api.billing.process_alipay_order',
params: {},
validate() {
if (!this.order || !this.order.order_id) {
throw new DashboardError('缺少订单信息');
}
},
onSuccess(response) {
this.paymentUrl = response.payment_url;
window.open(response.payment_url, '_blank');
toast.success('支付页面已在新窗口打开');
//
this.startPaymentCheck();
},
onError(error) {
this.error = error.message || '支付宝支付处理失败';
this.isProcessingPayment = false;
}
};
},
processWechatPayment() {
return {
url: 'jcloud.api.billing.process_wechatpay_order',
params: {},
validate() {
if (!this.order || !this.order.order_id) {
throw new DashboardError('缺少订单信息');
}
},
onSuccess(response) {
this.paymentQrCode = response.payment_url;
this.paymentQrCodeImage = response.qr_code_image || null;
//
this.startPaymentCheck();
},
onError(error) {
this.error = error.message || '微信支付处理失败';
this.isProcessingPayment = false;
}
};
},
checkPaymentStatus() {
return {
url: 'jcloud.api.billing.check_order_payment_status',
params: {},
validate() {
if (!this.order || !this.order.order_id) {
throw new DashboardError('缺少订单信息');
}
},
onSuccess(data) {
if (data && data.status === '已支付') {
//
this.stopPaymentCheck();
//
this.$emit('success', {
order: this.order,
renewal_period: this.renewalPeriod,
message: '支付成功'
});
this.paymentSuccess = true;
}
}
};
},
getDomainPrice() {
return {
url: 'jcloud.api.domain_west.get_west_domain_renew_price',
params: {
domain: this.domainDoc?.domain
},
onSuccess(data) {
if (data.data?.price) {
this.domainPrice = data.data.price;
}
},
onError(error) {
console.warn('获取域名续费价格失败:', error.message);
}
};
}
},
async mounted() {
//
if (this.domainDoc?.domain) {
await this.$resources.getDomainPrice.submit();
}
},
methods: {
cancel() {
this.stopPaymentCheck();
this.show = false;
},
createRenewalOrder() {
this.error = null;
this.$resources.createRenewalOrder.submit({
domain: this.domain,
renewal_years: this.renewalPeriod
});
},
processPayment() {
this.isProcessingPayment = true;
this.error = null;
if (!this.order || !this.order.order_id) {
this.error = '订单信息不完整,请重试';
this.isProcessingPayment = false;
return;
}
if (this.selectedPaymentMethod === 'balance') {
this.$resources.processBalancePayment.submit({
order_id: this.order.order_id
});
} else if (this.selectedPaymentMethod === 'alipay') {
this.$resources.processAlipayPayment.submit({
order_id: this.order.order_id
});
} else if (this.selectedPaymentMethod === 'wechatpay') {
this.$resources.processWechatPayment.submit({
order_id: this.order.order_id
});
}
},
startPaymentCheck() {
this.isProcessingPayment = false;
this.checkInterval = setInterval(() => {
this.$resources.checkPaymentStatus.submit({
order_id: this.order.order_id
});
}, 3000);
// 15
setTimeout(() => {
this.stopPaymentCheck();
}, 900000);
},
stopPaymentCheck() {
if (this.checkInterval) {
clearInterval(this.checkInterval);
this.checkInterval = null;
}
}
},
beforeUnmount() {
this.stopPaymentCheck();
}
};
</script>
</script>
<style scoped>
.qrcode-container {
height: 250px;
width: 250px;
margin: 0 auto;
padding: 5px;
border: 1px solid #e5e7eb;
border-radius: 12px;
transition: all 0.3s ease;
}
.qrcode-image {
height: 100%;
width: 100%;
object-fit: contain;
}
/* 添加渐入动画 */
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
</style>

View File

@ -124,6 +124,21 @@ class WestDomain:
}
return self._make_request('/info/?act=getprice', 'POST', body_params=body_params)
def get_domain_renew_price(self, domain: str, year: int = 1) -> Dict[str, Any]:
"""
获取域名续费价格
Args:
domain: 域名
year: 续费年限
"""
body_params = {
'act': 'getrenprice',
'domain': domain,
'year': year,
}
return self._make_request('/domain/', 'POST', body_params=body_params)
def query_domain(self, domain: str, suffix: str = '.com') -> Dict[str, Any]:
"""
域名查询
@ -480,9 +495,19 @@ def get_west_domain_price(domain: str, year: int = 1):
return {"status": "error", "message": "API查询失败"}
data = response.get("data", {})
original_price = data.get("buyprice", 0)
# 统一增加10%的利润参考aliyun_server_light.py的方法
if original_price and original_price > 0:
# 确保10%利润率且价格为整数
adjusted_price = int(original_price / (1 - 0.1))
else:
adjusted_price = original_price
return {
"data": {
"price": data.get("buyprice", 0),
"price": adjusted_price,
"original_price": original_price, # 保留原价用于参考
"domain": domain,
"year": year
}
@ -493,6 +518,50 @@ def get_west_domain_price(domain: str, year: int = 1):
return {"status": "error", "message": "域名价格查询响应解析失败"}
@jingrow.whitelist()
def get_west_domain_renew_price(domain: str, year: int = 1):
"""获取域名续费价格"""
client = get_west_client()
if not client:
return {"status": "error", "message": "API客户端初始化失败"}
if not domain:
return {"status": "error", "message": "缺少域名参数"}
response = client.get_domain_renew_price(domain, year)
if response.get("status") == "error":
return response
try:
# 直接检查响应格式
if response.get("result") != 200:
return {"status": "error", "message": "API查询失败"}
data = response.get("data", {})
original_price = data.get("price", 0)
# 统一增加10%的利润参考aliyun_server_light.py的方法
if original_price and original_price > 0:
# 确保10%利润率且价格为整数
adjusted_price = int(original_price / (1 - 0.1))
else:
adjusted_price = original_price
return {
"data": {
"price": adjusted_price,
"domain": domain,
"year": year,
"ispremium": data.get("ispremium", "n")
}
}
except Exception as e:
jingrow.log_error("域名续费价格查询响应解析失败", error=str(e))
return {"status": "error", "message": "域名续费价格查询响应解析失败"}
@jingrow.whitelist()
def west_domain_register(domain: str, regyear: int = 1, dns_host1: str = "ns1.myhostadmin.net",
dns_host2: str = "ns2.myhostadmin.net", c_sysid: str = None,