diff --git a/backend/src/views/home/index.vue b/backend/src/views/home/index.vue index 0fca01d..2f20669 100644 --- a/backend/src/views/home/index.vue +++ b/backend/src/views/home/index.vue @@ -71,6 +71,7 @@ v-for="module in basicModules" :key="module.path" class="module-card" + :class="{ disabled: !module.enabled }" @click="handleNavigate(module)" >
@@ -79,8 +80,27 @@
-

{{ module.description || "暂无描述" }}

+ +
+ @@ -93,6 +113,7 @@ v-for="module in systemModules" :key="module.path" class="module-card" + :class="{ disabled: !module.enabled }" @click="handleNavigate(module)" >
@@ -101,8 +122,27 @@
-

{{ module.description || "暂无描述" }}

+ +
+ @@ -115,6 +155,7 @@ v-for="module in uncategorizedModules" :key="module.path" class="module-card" + :class="{ disabled: !module.enabled }" @click="handleNavigate(module)" >
@@ -123,8 +164,27 @@
-

{{ module.description || "暂无描述" }}

+ +
+ @@ -157,6 +217,7 @@ import { Sunny, Moon, OfficeBuilding, + Lock, } from "@element-plus/icons-vue"; import { getTenantList } from "@/api/modules"; @@ -175,6 +236,7 @@ interface ModuleItem { status: number; type: number; title?: string; + enabled?: boolean; } const router = useRouter(); @@ -267,6 +329,11 @@ const tenantName = computed(() => authStore.user?.tenant_name || ""); // 处理导航跳转 function handleNavigate(module: ModuleItem) { + if (module.enabled === false) { + ElMessage.warning(`「${module.name}」暂未开通,请先购买对应套餐`); + return; + } + const isWebsiteManagement = module.name === "网站管理" || module.path.startsWith("/apps/cms"); const targetPath = isWebsiteManagement ? "/apps/cms/analytics/content" @@ -275,6 +342,12 @@ function handleNavigate(module: ModuleItem) { if (targetPath) router.push(targetPath); } +// 未开通功能 → 引导购买套餐 +function handleBuyPackage(module: ModuleItem) { + ElMessage.info(`购买「${module.name}」套餐功能 —— 请联系管理员或前往平台套餐中心`); + // TODO: 未来接入购买页面后 router.push(...) 到对应套餐购买界面 +} + // 刷新菜单 async function handleRefreshMenus() { refreshLoading.value = true; @@ -325,16 +398,20 @@ async function loadModules() { const list = res.data?.list || []; const filteredList = list .filter((item: ModuleItem) => item.status === 1 && item.is_show === 1) - .sort((a, b) => Number(a.sort) - Number(b.sort)); - moduleList.value = filteredList - .map((item: ModuleItem) => { - const mapped = { - ...item, - type: item.type ?? 0, // 默认为0(未分类) - title: item.name // 添加title字段用于显示分类标题 - }; - return mapped; + .map((item: ModuleItem) => ({ + ...item, + enabled: item.enabled !== false, // 后端返回 enabled,缺失时默认 true + title: item.name, + })) + .sort((a: ModuleItem, b: ModuleItem) => { + // 已开通在前 + if (a.enabled !== b.enabled) return a.enabled ? -1 : 1; + // 同状态内按 sort 升序、id 升序 + const sortDiff = Number(a.sort) - Number(b.sort); + if (sortDiff !== 0) return sortDiff; + return Number(a.id) - Number(b.id); }); + moduleList.value = filteredList; } } catch (error) { console.error("加载模块列表失败:", error); @@ -649,6 +726,71 @@ onMounted(() => { opacity: 0; } } + + // 未开通的套餐模块(disabled 状态) + &.disabled { + cursor: not-allowed; + background: #f7f8fa; + border-color: #e4e7ed; + box-shadow: none; + + .card-title { + color: #909399; + } + + .card-icon-wrapper { + color: #c0c4cc; + } + + .card-divider { + border-bottom-color: #ebeef5; + } + + // hover 禁用所有悬浮效果 + &:hover { + transform: none; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04); + background: #f7f8fa; + + .card-title, + .card-desc { + color: #909399; + } + + .card-icon-wrapper { + color: #c0c4cc; + } + + .card-divider { + opacity: 1; + } + } + + // 锁图标 + 提示文字 + .card-locked { + display: flex; + flex-direction: column; + align-items: center; + gap: 6px; + padding: 6px 0 0; + + .lock-icon { + font-size: 22px; + color: #c0c4cc; + } + + .lock-tip { + font-size: 12px; + color: #909399; + margin: 0; + } + } + + .buy-btn { + margin-top: 6px; + font-size: 12px; + } + } } } @@ -947,6 +1089,32 @@ onMounted(() => { .card-divider { border-color: #3d3d3d; } + + // 暗色主题下未开通卡片 + &.disabled { + background: #222; + border-color: #333; + + .card-title { + color: #666 !important; + } + + .card-icon-wrapper { + color: #555; + } + + .card-divider { + border-color: #2e2e2e; + } + + .card-locked .lock-icon { + color: #555; + } + + .card-locked .lock-tip { + color: #666; + } + } } } diff --git a/go/controllers/backend_modules.go b/go/controllers/backend_modules.go index d2d7e59..c22d842 100644 --- a/go/controllers/backend_modules.go +++ b/go/controllers/backend_modules.go @@ -6,6 +6,7 @@ import ( "server/models" "server/pkg/jwtutil" + "server/services" beego "github.com/beego/beego/v2/server/web" ) @@ -40,15 +41,24 @@ func (c *BackendModulesController) jsonErr(httpStatus, bizCode int, msg string) _ = c.ServeJSON() } +// moduleWithEnabled 带套餐开通标记的模块输出 +type moduleWithEnabled struct { + models.SystemModules + Enabled bool `json:"enabled"` +} + // GetTenantList GET /backend/modules/getTenantList -// 返回当前 backend 账号可见的模块。当前实现:返回 status=1 且 is_show=1 的全部模块。 +// 返回当前 backend 账号可见的模块(status=1 且 is_show=1),并根据租户绑定的套餐标记每个模块是否已开通。 +// 前端可据此排序(已开通在前)、置灰未开通卡片、阻止跳转。 func (c *BackendModulesController) GetTenantList() { - if _, err := c.backendModulesClaims(); err != nil { + claims, err := c.backendModulesClaims() + if err != nil { c.jsonErr(401, 401, err.Error()) return } + var rows []models.SystemModules - _, err := models.Orm.QueryTable(new(models.SystemModules)). + _, err = models.Orm.QueryTable(new(models.SystemModules)). Filter("delete_time__isnull", true). Filter("status", 1). Filter("is_show", 1). @@ -58,12 +68,26 @@ func (c *BackendModulesController) GetTenantList() { c.jsonErr(500, 500, "获取失败:"+err.Error()) return } + + // 查询当前租户套餐包含的模块 code 集合(含默认套餐回退) + tid := uint64(claims.TenantId) + allowedCodes := services.GetTenantModuleCodes(tid) + + enhanced := make([]moduleWithEnabled, 0, len(rows)) + for _, m := range rows { + enabled := true + if len(allowedCodes) > 0 { + enabled = allowedCodes[m.Code] + } + enhanced = append(enhanced, moduleWithEnabled{SystemModules: m, Enabled: enabled}) + } + c.Data["json"] = map[string]interface{}{ "code": 200, "msg": "获取成功", "data": map[string]interface{}{ - "list": rows, - "total": len(rows), + "list": enhanced, + "total": len(enhanced), }, } _ = c.ServeJSON() diff --git a/go/controllers/platform_payment.go b/go/controllers/platform_payment.go index 683348b..e7d1cf0 100644 --- a/go/controllers/platform_payment.go +++ b/go/controllers/platform_payment.go @@ -71,10 +71,23 @@ var channelSecretKeys = map[string][]string{ } func channelRowDTO(row *models.PlatformPaymentChannel) map[string]interface{} { + hasConfig := (row.ConfigJSON != nil && *row.ConfigJSON != "" && *row.ConfigJSON != "{}") || row.MerchantNo != "" + if row.Channel == payment.ChannelCloudPay { + hasConfig = true + } + status := "unconfigured" + if row.Enabled == 1 { + status = "enabled" + } else if hasConfig { + status = "disabled" + } + return map[string]interface{}{ "id": row.ID, "channel": row.Channel, "name": row.Name, "merchant_no": row.MerchantNo, "callback_url": row.CallbackURL, "enabled": row.Enabled == 1, + "status": status, + "has_config": hasConfig, "remark": row.Remark, "last_test_time": row.LastTestTime, "last_test_result": row.LastTestResult, "create_time": row.CreateTime, "update_time": row.UpdateTime, @@ -223,13 +236,29 @@ func (c *PlatformPaymentController) SaveChannel() { extraJSON = string(b) } + merchantNo := strings.TrimSpace(p.MerchantNo) + if merchantNo == "" && merged != nil { + switch channel { + case payment.ChannelPayPal: + merchantNo = merged.Get("client_id") + case payment.ChannelWechat: + merchantNo = merged.Get("mch_id") + case payment.ChannelAlipay: + merchantNo = merged.Get("app_id") + case payment.ChannelUnionPay, payment.ChannelCloudPay: + merchantNo = merged.Get("mer_id") + } + } + now := time.Now() updates := orm.Params{ "config_json": enc, - "merchant_no": strings.TrimSpace(p.MerchantNo), - "remark": strings.TrimSpace(p.Remark), + "merchant_no": merchantNo, "update_time": now, } + if p.Remark != "" { + updates["remark"] = strings.TrimSpace(p.Remark) + } if p.Name != "" { updates["name"] = strings.TrimSpace(p.Name) } diff --git a/platform/src/views/basicSettings/tenantpackage/index.vue b/platform/src/views/basicSettings/tenantpackage/index.vue index 10330c3..94c8d42 100644 --- a/platform/src/views/basicSettings/tenantpackage/index.vue +++ b/platform/src/views/basicSettings/tenantpackage/index.vue @@ -267,7 +267,8 @@ onMounted(refresh); diff --git a/platform/src/views/system/payment/channels/paypal.vue b/platform/src/views/system/payment/channels/paypal.vue index aa16c16..5413a1d 100644 --- a/platform/src/views/system/payment/channels/paypal.vue +++ b/platform/src/views/system/payment/channels/paypal.vue @@ -139,8 +139,7 @@ const form = reactive({ const rules: FormRules = { client_id: [{ required: true, message: '请输入 Client ID', trigger: 'blur' }], - client_secret: [{ required: true, message: '请输入 Client Secret', trigger: 'blur' }], - webhook_id: [{ required: true, message: '请输入 Webhook ID', trigger: 'blur' }] + client_secret: [{ required: true, message: '请输入 Client Secret', trigger: 'blur' }] } /* ---------------- 敏感字段掩码编辑 ---------------- */ @@ -226,7 +225,7 @@ async function handleTest() { }, extra: { env: form.env } }) - ElMessage.success(res?.data?.message || '连接成功:OAuth2 凭证校验通过') + ElMessage.success(res?.data?.message || '连接成功:OAuth2 凭证校验通过(请点击下方「保存配置」使配置生效)') } catch (error: any) { ElMessage.error(error?.response?.data?.msg || error?.message || '连接失败,请检查 Client ID / Secret 与运行环境') } finally { diff --git a/platform/src/views/system/payment/components/ChannelCard.vue b/platform/src/views/system/payment/components/ChannelCard.vue index 1070fa4..5b8a1d8 100644 --- a/platform/src/views/system/payment/components/ChannelCard.vue +++ b/platform/src/views/system/payment/components/ChannelCard.vue @@ -9,7 +9,7 @@
{{ name }}
-
商户号:{{ merchantNo || '未配置' }}
+
{{ idLabel }}:{{ merchantNo || (status === 'unconfigured' ? '未配置' : '已配置') }}
@@ -22,7 +22,16 @@ 停用 - 启用 + + 启用 + @@ -51,6 +60,8 @@ const ICONS: Record = { const props = withDefaults( defineProps<{ + /** 渠道标识,如 wechat / alipay / paypal */ + channel?: string /** 渠道名称,如「微信支付」 */ name: string /** 渠道状态:enabled 已启用 / disabled 已停用 / unconfigured 未配置 */ @@ -63,6 +74,7 @@ const props = withDefaults( desc?: string }>(), { + channel: '', status: 'unconfigured', merchantNo: '', icon: 'PriceTag', @@ -76,6 +88,12 @@ const emit = defineEmits<{ (e: 'disable'): void }>() +const idLabel = computed(() => { + if (props.channel === 'paypal') return 'Client ID' + if (props.channel === 'cloudpay') return '银联商户号' + return '商户号' +}) + const iconComp = computed(() => ICONS[props.icon] || PriceTag) const statusClass = computed(() => { diff --git a/platform/src/views/system/payment/index.vue b/platform/src/views/system/payment/index.vue index 0107573..468d4b5 100644 --- a/platform/src/views/system/payment/index.vue +++ b/platform/src/views/system/payment/index.vue @@ -23,79 +23,15 @@ 渠道状态概览
共 {{ channelList.length }} 个渠道,已启用 {{ enabledCount }} 个 - - 表格 - 卡片 -
- - - - - - - - - - - - - - - - - -
+
('table') /** TODO: 路由 path 需与你配置的菜单 path 保持一致,例如实际为 /payment/orders 时请同步修改 */ const ROUTES = { @@ -212,16 +139,6 @@ const ROUTES = { } as Record } -/** 渠道图标映射(图标名与 ChannelCard 组件保持一致) */ -const CHANNEL_ICONS: Record = { - ChatDotRound, - Wallet, - CreditCard, - Lightning, - Money -} -const channelIcon = (icon: string) => CHANNEL_ICONS[icon] || Money - /* ---------------- 渠道概览 ---------------- */ interface ChannelRow { @@ -233,20 +150,48 @@ interface ChannelRow { remark: string } +const DEFAULT_CHANNEL_META: Record = { + wechat: { + name: '微信支付', + icon: 'ChatDotRound', + remark: 'JSAPI / Native 扫码收款,平台使用费主渠道' + }, + alipay: { + name: '支付宝', + icon: 'Wallet', + remark: '电脑网站支付 / 手机网站支付' + }, + unionpay: { + name: '银联', + icon: 'CreditCard', + remark: '银联全渠道收单,需上传商户证书' + }, + cloudpay: { + name: '云闪付', + icon: 'Lightning', + remark: '复用银联商户参数,仅控制收银台是否展示云闪付标识' + }, + paypal: { + name: 'PayPal', + icon: 'Money', + remark: '面向港澳台及海外租户,当前无跨境需求,默认关闭' + } +} + const channelList = ref([ { channel: 'wechat', name: '微信支付', - status: 'enabled', - merchant_no: '1620888999', + status: 'unconfigured', + merchant_no: '', icon: 'ChatDotRound', remark: 'JSAPI / Native 扫码收款,平台使用费主渠道' }, { channel: 'alipay', name: '支付宝', - status: 'enabled', - merchant_no: '2088123456789012', + status: 'unconfigured', + merchant_no: '', icon: 'Wallet', remark: '电脑网站支付 / 手机网站支付' }, @@ -261,15 +206,15 @@ const channelList = ref([ { channel: 'cloudpay', name: '云闪付', - status: 'disabled', - merchant_no: '898110158000000', + status: 'unconfigured', + merchant_no: '', icon: 'Lightning', remark: '复用银联商户参数,仅控制收银台是否展示云闪付标识' }, { channel: 'paypal', name: 'PayPal', - status: 'disabled', + status: 'unconfigured', merchant_no: '', icon: 'Money', remark: '面向港澳台及海外租户,当前无跨境需求,默认关闭' @@ -331,34 +276,66 @@ interface RecentOrder { const recentOrders = ref([]) -/** 最近 10 条:TODO 换成 getOrders({ page: 1, pageSize: 10 }) */ -function loadRecentOrders() { - recentOrders.value = [ - { pay_no: 'P202609150009', tenant_name: '杭州云泽科技有限公司', amount: 299900, channel: 'wechat', status: 'paid', create_time: '2026-09-15 10:24:31' }, - { pay_no: 'P202609150008', tenant_name: '宁波海曙贸易有限公司', amount: 99900, channel: 'alipay', status: 'paid', create_time: '2026-09-15 09:58:02' }, - { pay_no: 'P202609150007', tenant_name: '上海弘远信息技术有限公司', amount: 199900, channel: 'wechat', status: 'pending', create_time: '2026-09-15 09:31:47' }, - { pay_no: 'P202609150006', tenant_name: '深圳市博通电子商务有限公司', amount: 499900, channel: 'alipay', status: 'paid', create_time: '2026-09-15 09:12:15' }, - { pay_no: 'P202609150005', tenant_name: '北京中科智联科技有限公司', amount: 99900, channel: 'wechat', status: 'failed', create_time: '2026-09-15 08:56:39' }, - { pay_no: 'P202609140021', tenant_name: '成都天府软件有限公司', amount: 299900, channel: 'wechat', status: 'paid', create_time: '2026-09-14 18:42:10' }, - { pay_no: 'P202609140020', tenant_name: '武汉光谷数字科技有限公司', amount: 199900, channel: 'alipay', status: 'closed', create_time: '2026-09-14 17:20:55' }, - { pay_no: 'P202609140019', tenant_name: '西安丝路网络科技有限公司', amount: 99900, channel: 'wechat', status: 'paid', create_time: '2026-09-14 16:05:23' }, - { pay_no: 'P202609140018', tenant_name: '青岛海洋装备有限公司', amount: 599900, channel: 'alipay', status: 'paid', create_time: '2026-09-14 14:48:07' }, - { pay_no: 'P202609140017', tenant_name: '长沙湘江云计算有限公司', amount: 299900, channel: 'wechat', status: 'refunded', create_time: '2026-09-14 11:33:41' } - ] +/** 最近 10 条 */ +async function loadRecentOrders() { + try { + const res: any = await getOrders({ page: 1, pageSize: 10 }) + if (res?.code === 200 && res.data) { + const list = res.data.list || [] + recentOrders.value = list.map((item: any) => ({ + pay_no: item.pay_no, + tenant_name: item.tenant_name || '-', + amount: item.amount, + channel: item.channel, + status: item.status, + create_time: item.create_time + })) + } + } catch (error: any) { + console.error('加载最近交易失败:', error) + } } /* ---------------- 数据加载与操作 ---------------- */ async function fetchChannels() { - // TODO: 接口联通后替换为真实请求 - // const res = await getChannelList() - // if (res.success) channelList.value = res.data.list || res.data || [] + try { + const res: any = await getChannelList() + if (res?.code === 200 && res.data) { + const list = res.data.list || [] + channelList.value = list.map((item: any) => { + const meta = DEFAULT_CHANNEL_META[item.channel] || { + name: item.name, + icon: 'Money', + remark: '' + } + let status: 'enabled' | 'disabled' | 'unconfigured' = 'unconfigured' + if (item.status) { + status = item.status + } else if (item.enabled) { + status = 'enabled' + } else if (item.merchant_no || item.has_config || item.last_test_result) { + status = 'disabled' + } + return { + channel: item.channel, + name: item.name || meta.name, + status, + merchant_no: item.merchant_no || '', + icon: meta.icon || 'Money', + remark: item.remark || meta.remark || '' + } + }) + } + } catch (error: any) { + ElMessage.error(error?.response?.data?.msg || error?.message || '获取渠道列表失败') + } } async function fetchData() { loading.value = true try { - await Promise.all([fetchChannels(), Promise.resolve(loadRecentOrders())]) + await Promise.allSettled([fetchChannels(), loadRecentOrders()]) } catch (error: any) { ElMessage.error(error?.message || '加载支付概览失败') } finally { @@ -390,9 +367,17 @@ function goOrderDetail(row: RecentOrder) { } async function toggleChannelStatus(row: ChannelRow, enable: boolean) { - // TODO: 接口联通后替换为 toggleChannel(row.channel, enable) - row.status = enable ? 'enabled' : 'disabled' - ElMessage.success(`${row.name}已${enable ? '启用' : '停用'}`) + try { + const res: any = await toggleChannel(row.channel, enable) + if (res?.code === 200) { + row.status = enable ? 'enabled' : 'disabled' + ElMessage.success(`${row.name}已${enable ? '启用' : '停用'}`) + } else { + ElMessage.error(res?.msg || '操作失败') + } + } catch (error: any) { + ElMessage.error(error?.response?.data?.msg || error?.message || '操作失败') + } } /* ---------------- 工具函数 ---------------- */ @@ -472,13 +457,15 @@ onMounted(() => { gap: 16px; } -.channel-cell { - display: flex; - align-items: center; - gap: 8px; +@media (max-width: 1200px) { + .channel-card-grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } +} - .channel-icon { - color: #409eff; +@media (max-width: 768px) { + .channel-card-grid { + grid-template-columns: 1fr; } }