diff --git a/backend/package-lock.json b/backend/package-lock.json index 07a003b..ca09586 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "@element-plus/icons-vue": "^2.3.2", "@turbodocx/html-to-docx": "^1.22.2", + "@types/qrcode": "^1.5.6", "@umoteam/editor": "^10.2.1", "axios": "^1.13.1", "chart": "^0.1.2", @@ -2455,7 +2456,6 @@ "version": "24.10.14", "resolved": "https://registry.npmmirror.com/@types/node/-/node-24.10.14.tgz", "integrity": "sha512-OowOUbD1lBCOFIPOZ8xnMIhgqA4sCutMiYOmPHL1PTLt5+y1XA+g2+yC9OOyz8p+deMZqPZLxfMjYIfrKsPeFg==", - "dev": true, "license": "MIT", "dependencies": { "undici-types": "~7.16.0" @@ -2467,6 +2467,15 @@ "integrity": "sha512-VWDCbrLeVXJM9fihYodcLiIv0ku+AlOa/TQ1SvYOaBuyrSKgEcro95LJyIsJ4vSo6BXIxOKxiJAat04CmST9Fw==", "license": "MIT" }, + "node_modules/@types/qrcode": { + "version": "1.5.6", + "resolved": "https://registry.npmmirror.com/@types/qrcode/-/qrcode-1.5.6.tgz", + "integrity": "sha512-te7NQcV2BOvdj2b1hCAHzAoMNuj65kNBMz0KBaxM6c3VGBOhU0dURQKOtH8CFNI/dsKkwlv32p26qYQTWoB5bw==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/raf": { "version": "3.4.3", "resolved": "https://registry.npmmirror.com/@types/raf/-/raf-3.4.3.tgz", @@ -8193,7 +8202,6 @@ "version": "7.16.0", "resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-7.16.0.tgz", "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", - "dev": true, "license": "MIT" }, "node_modules/unimport": { diff --git a/backend/package.json b/backend/package.json index 46de7bd..d4c6ab9 100644 --- a/backend/package.json +++ b/backend/package.json @@ -11,6 +11,7 @@ "dependencies": { "@element-plus/icons-vue": "^2.3.2", "@turbodocx/html-to-docx": "^1.22.2", + "@types/qrcode": "^1.5.6", "@umoteam/editor": "^10.2.1", "axios": "^1.13.1", "chart": "^0.1.2", diff --git a/backend/src/api/product.js b/backend/src/api/product.js index b3740b8..fb5ea8e 100644 --- a/backend/src/api/product.js +++ b/backend/src/api/product.js @@ -54,3 +54,33 @@ export function getPaymentChannels() { method: "get", }); } + +/************************************************* + * 租户端「套餐购买」相关接口 + *************************************************/ + +/** 可购买套餐列表(仅启用的套餐) */ +export function getPackageList(params) { + return request({ + url: "/backend/package/list", + method: "get", + params, + }); +} + +/** 按套餐编码取详情 */ +export function getPackageDetailByCode(code) { + return request({ + url: `/backend/package/detail/${code}`, + method: "get", + }); +} + +/** 套餐下单(channel 为空表示线下/待平台开通) */ +export function createPackageOrder(data) { + return request({ + url: "/backend/package/order/create", + method: "post", + data, + }); +} diff --git a/backend/src/router/dynamicRoutes.js b/backend/src/router/dynamicRoutes.js index fc82a52..61a218a 100644 --- a/backend/src/router/dynamicRoutes.js +++ b/backend/src/router/dynamicRoutes.js @@ -59,6 +59,7 @@ function nestedMenuToRoute(menu, layoutAbsPath) { title: menu.title, icon: menu.icon, id: menu.id, + code: menu.code || menu.module_code, componentPath: menu.component_path, }; @@ -142,6 +143,7 @@ export function convertMenusToRoutes(menus, parentPath = '') { title: menu.title, icon: menu.icon, id: menu.id, + code: menu.code || menu.module_code, componentPath: menu.component_path, }; diff --git a/backend/src/router/index.js b/backend/src/router/index.js index a94d8c3..5d0e4a3 100644 --- a/backend/src/router/index.js +++ b/backend/src/router/index.js @@ -164,6 +164,13 @@ const staticRoutes = [ component: () => import("@/views/product/index.vue"), meta: { requiresAuth: true, title: "产品购买", isStandalone: true } }, + // 套餐购买列表页:独立于 Main 框架展示 + { + path: "/packages", + name: "PackageList", + component: () => import("@/views/package/list.vue"), + meta: { requiresAuth: true, title: "套餐购买", isStandalone: true } + }, { path: "/:pathMatch(.*)*", name: "NotFound", diff --git a/backend/src/views/home/index.vue b/backend/src/views/home/index.vue index 55637f9..210aff8 100644 --- a/backend/src/views/home/index.vue +++ b/backend/src/views/home/index.vue @@ -89,7 +89,7 @@ @@ -99,9 +99,9 @@ type="primary" link class="buy-btn" - @click.stop="handleBuyPackage(module)" + @click.stop="handleBuyProduct(module)" > - 购买套餐 + 购买 @@ -134,7 +134,7 @@ @@ -144,9 +144,9 @@ type="primary" link class="buy-btn" - @click.stop="handleBuyPackage(module)" + @click.stop="handleBuyProduct(module)" > - 购买套餐 + 购买 @@ -179,7 +179,7 @@ @@ -189,9 +189,9 @@ type="primary" link class="buy-btn" - @click.stop="handleBuyPackage(module)" + @click.stop="handleBuyProduct(module)" > - 购买套餐 + 购买 @@ -227,7 +227,6 @@ import { Moon, OfficeBuilding, Lock, - Lock, } from "@element-plus/icons-vue"; import { getTenantList } from "@/api/modules"; @@ -346,27 +345,11 @@ const tenantName = computed(() => authStore.user?.tenant_name || ""); // 当前租户生效套餐名称(用于未开通提示) const packageName = ref(""); -// 套餐购买链接(平台端配置,为空时点击「购买」仅提示联系管理员) -const purchaseUrl = ref(""); - -// 套餐未开通模块:不跳转,提示购买套餐后使用 -function showLockedTip(module: ModuleItem) { - const pkg = packageName.value ? `「${packageName.value}」` : "当前"; - ElMessageBox.alert( - `${pkg}套餐未包含「${module.name}」功能模块,请购买或升级套餐后使用。如有疑问请联系平台管理员。`, - "功能未开通", - { - confirmButtonText: "我知道了", - type: "warning", - customClass: "module-locked-tip" - } - ).catch(() => {}); -} // 处理导航跳转 function handleNavigate(module: ModuleItem) { if (module.enabled === false) { - ElMessage.warning(`「${module.name}」暂未开通,请先购买对应套餐`); + ElMessage.warning(`「${module.name}」暂未开通,请先购买对应功能`); return; } @@ -378,10 +361,10 @@ function handleNavigate(module: ModuleItem) { if (targetPath) router.push(targetPath); } -// 未开通功能 → 引导购买套餐 -function handleBuyPackage(module: ModuleItem) { - ElMessage.info(`购买「${module.name}」套餐功能 —— 请联系管理员或前往平台套餐中心`); - // TODO: 未来接入购买页面后 router.push(...) 到对应套餐购买界面 +// 未开通功能 → 跳转单品购买详情页(对应平台的产品定价配置) +function handleBuyProduct(module: ModuleItem) { + const code = module.code || module.path.replace(/^\/+/, "").replace(/\//g, "-"); + router.push(`/product/${code}`); } // 刷新菜单 diff --git a/backend/src/views/locked/index.vue b/backend/src/views/locked/index.vue index 9825a49..54067b0 100644 --- a/backend/src/views/locked/index.vue +++ b/backend/src/views/locked/index.vue @@ -6,9 +6,10 @@
功能未开通
- 当前套餐未包含功能模块,请联系管理员升级套餐后使用。 + 当前未开通功能,请购买或联系管理员开通后使用。
+ 立即购买 返回首页
@@ -27,6 +28,15 @@ const moduleName = computed(() => route.meta?.title || ""); const backToHome = () => { router.push("/home"); }; + +const goBuyProduct = () => { + const code = route.meta?.code || route.path.replace(/^\/+/, "").split("/")[0] || ""; + if (code) { + router.push(`/product/${code}`); + } else { + router.push("/home"); + } +}; \ No newline at end of file diff --git a/go/controllers/backend_modules.go b/go/controllers/backend_modules.go index c7f8221..77f81c8 100644 --- a/go/controllers/backend_modules.go +++ b/go/controllers/backend_modules.go @@ -7,7 +7,6 @@ import ( "server/models" "server/pkg/jwtutil" "server/services" - "server/services" beego "github.com/beego/beego/v2/server/web" ) @@ -59,8 +58,6 @@ type moduleWithEnabled struct { // 返回当前 backend 账号可见的模块(status=1 且 is_show=1),并根据租户绑定的套餐标记每个模块是否已开通。 // 前端可据此排序(已开通在前)、置灰未开通卡片、阻止跳转。 func (c *BackendModulesController) GetTenantList() { - claims, err := c.backendModulesClaims() - if err != nil { claims, err := c.backendModulesClaims() if err != nil { c.jsonErr(401, 401, err.Error()) @@ -68,7 +65,6 @@ func (c *BackendModulesController) GetTenantList() { } 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). diff --git a/go/controllers/backend_product_purchase.go b/go/controllers/backend_product_purchase.go index 268b4f0..2f2e247 100644 --- a/go/controllers/backend_product_purchase.go +++ b/go/controllers/backend_product_purchase.go @@ -310,3 +310,218 @@ func (c *BackendProductPurchaseController) GetMyOrders() { } c.jsonOK(map[string]interface{}{"list": rows, "total": len(rows)}, "获取成功") } + +// ============================ 套餐购买 ============================ + +// GetPackageList GET /backend/package/list +// 租户端可购买套餐列表(仅启用的套餐) +func (c *BackendProductPurchaseController) GetPackageList() { + if _, err := c.claims(); err != nil { + c.jsonErr(401, 401, err.Error()) + return + } + var rows []models.SystemTenantPackage + if _, err := models.Orm.QueryTable(new(models.SystemTenantPackage)). + Filter("delete_time__isnull", true). + Filter("status", 1). + OrderBy("sort", "id"). + All(&rows); err != nil { + c.jsonErr(500, 500, "获取失败:"+err.Error()) + return + } + + ids := make([]uint64, 0, len(rows)) + for _, r := range rows { + ids = append(ids, r.ID) + } + moduleMap := loadPackageModules(ids) + + list := make([]map[string]interface{}, 0, len(rows)) + for _, r := range rows { + modules := moduleMap[r.ID] + if modules == nil { + modules = []tenantPackageModuleItem{} + } + list = append(list, map[string]interface{}{ + "id": r.ID, + "name": r.Name, + "code": r.Code, + "description": r.Description, + "price": r.Price, + "duration_days": r.DurationDays, + "user_quota": r.UserQuota, + "extra_user_price": r.ExtraUserPrice, + "is_default": r.IsDefault, + "sort": r.Sort, + "status": r.Status, + "remark": r.Remark, + "modules": modules, + "module_codes": moduleCodes(modules), + "create_time": r.CreateTime, + "update_time": r.UpdateTime, + }) + } + c.jsonOK(map[string]interface{}{"list": list, "total": len(list)}, "获取成功") +} + +// GetPackageDetailByCode GET /backend/package/detail/:code +// 按套餐编码获取详情(租户端购买页使用) +func (c *BackendProductPurchaseController) GetPackageDetailByCode() { + if _, err := c.claims(); err != nil { + c.jsonErr(401, 401, err.Error()) + return + } + code := strings.TrimSpace(c.Ctx.Input.Param(":code")) + if code == "" { + c.jsonErr(400, 400, "套餐编码不能为空") + return + } + var row models.SystemTenantPackage + if err := models.Orm.QueryTable(new(models.SystemTenantPackage)). + Filter("code", code). + Filter("delete_time__isnull", true). + Filter("status", 1). + One(&row); err != nil { + c.jsonErr(404, 404, "套餐不存在或已下架") + return + } + modules := loadPackageModules([]uint64{row.ID})[row.ID] + if modules == nil { + modules = []tenantPackageModuleItem{} + } + c.jsonOK(map[string]interface{}{ + "id": row.ID, + "name": row.Name, + "code": row.Code, + "description": row.Description, + "price": row.Price, + "duration_days": row.DurationDays, + "user_quota": row.UserQuota, + "extra_user_price": row.ExtraUserPrice, + "is_default": row.IsDefault, + "sort": row.Sort, + "status": row.Status, + "remark": row.Remark, + "modules": modules, + "module_codes": moduleCodes(modules), + "create_time": row.CreateTime, + "update_time": row.UpdateTime, + }, "获取成功") +} + +type createPackageOrderPayload struct { + PackageCode string `json:"package_code"` + Channel string `json:"channel"` + PayType string `json:"pay_type"` + ReturnURL string `json:"return_url"` +} + +// CreatePackageOrder POST /backend/package/order/create +// 创建套餐购买订单;channel 非空时同时创建支付单并返回拉起支付参数。 +// 订单绑定 PackageID,支付成功后自动为租户开通对应套餐。 +func (c *BackendProductPurchaseController) CreatePackageOrder() { + claims, err := c.claims() + if err != nil { + c.jsonErr(401, 401, err.Error()) + return + } + tid := uint64(claims.TenantId) + if tid == 0 { + c.jsonErr(401, 401, "未获取到租户信息") + return + } + + body, _ := io.ReadAll(c.Ctx.Request.Body) + var p createPackageOrderPayload + if err := json.Unmarshal(body, &p); err != nil { + c.jsonErr(400, 400, "参数错误") + return + } + p.PackageCode = strings.TrimSpace(p.PackageCode) + p.Channel = strings.TrimSpace(p.Channel) + if p.PackageCode == "" { + c.jsonErr(400, 400, "套餐不能为空") + return + } + + var pkg models.SystemTenantPackage + if err := models.Orm.QueryTable(new(models.SystemTenantPackage)). + Filter("code", p.PackageCode). + Filter("delete_time__isnull", true). + Filter("status", 1). + One(&pkg); err != nil { + c.jsonErr(404, 404, "套餐不存在或已下架") + return + } + if pkg.Price <= 0 { + c.jsonErr(400, 400, "该套餐未配置价格,请联系平台") + return + } + + now := time.Now() + order := &models.PlatformProductOrder{ + OrderNo: "PO" + strconv.FormatInt(now.UnixNano(), 10), + Tid: tid, + TenantName: c.tenantName(tid), + ProductID: 0, // 套餐购买不关联产品 + ProductCode: pkg.Code, + ProductName: pkg.Name, + Amount: pkg.Price, + PackageID: pkg.ID, + Status: models.ProductOrderStatusPending, + UpdateTime: &now, + } + + // 未选择支付渠道:仅落单,等待平台确认(线下转账/试用审批场景) + if p.Channel == "" { + if _, err := models.Orm.Insert(order); err != nil { + c.jsonErr(500, 500, "下单失败:"+err.Error()) + return + } + c.jsonOK(map[string]interface{}{ + "order_no": order.OrderNo, + "pay_no": "", + "amount": order.Amount, + "offline": true, + }, "下单成功,请等待平台开通") + return + } + + amountFen := int64(order.Amount*100 + 0.5) + subject := pkg.Name + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + payOrder, params, err := payment.CreatePayment(ctx, payment.CreateInput{ + OutTradeNo: order.OrderNo, + OrderType: models.CommissionOrderTypeModuleShop, + TenantID: strconv.FormatUint(tid, 10), + TenantName: order.TenantName, + Amount: amountFen, + Channel: p.Channel, + Subject: subject, + ReturnURL: strings.TrimSpace(p.ReturnURL), + ClientIP: c.Ctx.Input.IP(), + PayType: p.PayType, + }) + if err != nil { + c.jsonErr(400, 400, "创建支付单失败:"+err.Error()) + return + } + + order.PayNo = payOrder.PayNo + order.Channel = p.Channel + if _, err := models.Orm.Insert(order); err != nil { + c.jsonErr(500, 500, "下单失败:"+err.Error()) + return + } + c.jsonOK(map[string]interface{}{ + "order_no": order.OrderNo, + "pay_no": payOrder.PayNo, + "amount": order.Amount, + "channel": p.Channel, + "pay_params": params, + "offline": false, + }, "下单成功") +} + diff --git a/go/routers/backend/backend.go b/go/routers/backend/backend.go index 6e12dc8..db5c932 100644 --- a/go/routers/backend/backend.go +++ b/go/routers/backend/backend.go @@ -22,6 +22,11 @@ func RegisterProductRoutes() { beego.Router("/backend/product/order/create", &controllers.BackendProductPurchaseController{}, "post:CreateOrder") beego.Router("/backend/product/order/status", &controllers.BackendProductPurchaseController{}, "get:GetOrderStatus") beego.Router("/backend/product/order/list", &controllers.BackendProductPurchaseController{}, "get:GetMyOrders") + + // 套餐购买路由 + beego.Router("/backend/package/list", &controllers.BackendProductPurchaseController{}, "get:GetPackageList") + beego.Router("/backend/package/detail/:code", &controllers.BackendProductPurchaseController{}, "get:GetPackageDetailByCode") + beego.Router("/backend/package/order/create", &controllers.BackendProductPurchaseController{}, "post:CreatePackageOrder") } // RegisterPaymentRoutes 注册租户端支付路由(下单页 / 收银台)。 diff --git a/platform/src/api/payment.js b/platform/src/api/payment.js index adddc48..1bb69b3 100644 --- a/platform/src/api/payment.js +++ b/platform/src/api/payment.js @@ -20,9 +20,8 @@ const COMMISSION = "/platform/commission"; /** * 创建支付单 - * TODO: 后端实现 POST /api/payment/create - * 约定入参:{ out_trade_no, tenant_id, order_type, amount(分), channel, subject, return_url, client_ip } - * 约定返回:{ pay_no, channel, pay_params } pay_params 交给前端拉起渠道支付 + * + * 该接口仅供租户端收银台使用;平台端不直接创建支付单。 * @param {Object} data 下单参数 * @returns {Promise} */ @@ -35,17 +34,16 @@ export function createPayment(data) { } /** - * 查询支付单(本地库) - * TODO: 后端实现 GET /api/payment/query/:payNo + * 手动查询支付单并同步渠道状态 * @param {string} payNo 支付单号 * @param {Object} [params] { sync: true } 时同步渠道最新状态后再返回 * @returns {Promise} */ export function queryPayment(payNo, params = {}) { return request({ - url: `${PAY}/query/${payNo}`, - method: "get", - params, + url: `${PAY}/orders/${payNo}/query`, + method: "post", + data: params, }); } @@ -301,6 +299,23 @@ export function toggleCommissionRule(id, status) { /* ================= 对账 ================= */ +export function runReconcile(data) { + return request({ + url: `${PAY}/reconcile/run`, + method: "post", + data, + }); +} + +export function importReconcileBill(data) { + return request({ + url: `${PAY}/reconcile/import`, + method: "post", + data, + timeout: 0, + }); +} + /** * 对账差异列表 * TODO: 后端实现 GET /api/payment/reconcile diff --git a/platform/src/views/basicSettings/pricing/components/edit.vue b/platform/src/views/basicSettings/pricing/components/edit.vue index cd2209f..88f8e71 100644 --- a/platform/src/views/basicSettings/pricing/components/edit.vue +++ b/platform/src/views/basicSettings/pricing/components/edit.vue @@ -25,15 +25,17 @@ - - 元 +
+ + 元 +
@@ -338,10 +340,21 @@ const handleSubmit = async () => { width: 100%; } +.price-input-wrapper { + display: flex; + align-items: center; + width: 100%; + + .full { + flex: 1; + } +} + .suffix { margin-left: 8px; font-size: 13px; color: var(--el-text-color-secondary); + white-space: nowrap; } .feature-box { diff --git a/platform/src/views/basicSettings/pricing/index.vue b/platform/src/views/basicSettings/pricing/index.vue index 1ff0c33..9dfecbb 100644 --- a/platform/src/views/basicSettings/pricing/index.vue +++ b/platform/src/views/basicSettings/pricing/index.vue @@ -128,7 +128,7 @@ diff --git a/platform/src/views/system/payment/commission/rules.vue b/platform/src/views/system/payment/commission/rules.vue index 17d4990..ad064dd 100644 --- a/platform/src/views/system/payment/commission/rules.vue +++ b/platform/src/views/system/payment/commission/rules.vue @@ -196,14 +196,7 @@ import { useRouter } from 'vue-router' import { ElMessage, ElMessageBox } from 'element-plus' import type { FormInstance, FormRules } from 'element-plus' import { Plus, Edit, Delete, Tickets } from '@element-plus/icons-vue' -// TODO: 接口联调时打开 -// import { -// getCommissionRules, -// saveCommissionRule, -// updateCommissionRule, -// deleteCommissionRule, -// toggleCommissionRule -// } from '@/api/payment' +import { getCommissionRules, saveCommissionRule, updateCommissionRule, deleteCommissionRule, toggleCommissionRule } from '@/api/payment' const router = useRouter() const loading = ref(false) @@ -233,51 +226,6 @@ interface RuleRow { remark: string } -const ALL_RULES: RuleRow[] = [ - { - id: 1, - rule_name: '渠道推广-平台使用费 5%', - calc_type: 'percent', - value: 5, - order_type: 'platform_usage', - status: 1, - remark: '默认规则,适用于全部签约渠道伙伴' - }, - { - id: 2, - rule_name: '服务商-模块购买固定 50 元', - calc_type: 'fixed', - value: 5000, - order_type: 'module_shop', - status: 1, - remark: '每笔模块购买订单固定发放' - }, - { - id: 3, - rule_name: '阶梯激励-年度大单', - calc_type: 'tiered', - // 阶梯配置入库为 yz_platform_commission_rule.tier_json,接口读出时反序列化为 steps 数组 - value: 0, - steps: [ - { min: 0, max: 1000000, rate: 3 }, - { min: 1000000, max: 5000000, rate: 5 }, - { min: 5000000, max: null, rate: 8 } - ], - order_type: 'all', - status: 1, - remark: '按订单金额区间取比例' - }, - { - id: 4, - rule_name: '老规则-暂停使用', - calc_type: 'percent', - value: 3, - order_type: 'service_fee', - status: 0, - remark: '2025 年规则,已停用' - } -] - const tableData = ref([]) const calcTypeLabel = (type: string) => @@ -314,19 +262,19 @@ function valueText(row: RuleRow) { async function fetchList() { loading.value = true try { - // TODO: 接口联通后替换为真实请求 - // const res = await getCommissionRules({ page: page.current, pageSize: page.size, keyword: filters.keyword }) - // tableData.value = res.data.list || [] - // page.total = res.data.total || 0 - - const filtered = ALL_RULES.filter((item) => - filters.keyword ? item.rule_name.includes(filters.keyword) : true - ) - page.total = filtered.length - const from = (page.current - 1) * page.size - tableData.value = filtered.slice(from, from + page.size) + const res: any = await getCommissionRules({ + page: page.current, + pageSize: page.size, + keyword: filters.keyword + }) + if (res?.code === 200) { + tableData.value = res.data?.list || [] + page.total = Number(res.data?.total || 0) + } else { + ElMessage.error(res?.msg || '获取佣金规则失败') + } } catch (error: any) { - ElMessage.error(error?.message || '获取佣金规则失败') + ElMessage.error(error?.response?.data?.msg || error?.message || '获取佣金规则失败') } finally { loading.value = false } @@ -506,10 +454,11 @@ async function handleSubmit() { })) } - // TODO: 接口联通后替换为真实请求 - // if (isEdit.value && form.id) await updateCommissionRule(form.id, payload) - // else await saveCommissionRule(payload) - await new Promise((resolve) => setTimeout(resolve, 400)) + if (isEdit.value && form.id) { + await updateCommissionRule(form.id, payload) + } else { + await saveCommissionRule(payload) + } ElMessage.success(isEdit.value ? '规则已更新' : '规则已新增') dialogVisible.value = false await fetchList() @@ -522,9 +471,18 @@ async function handleSubmit() { async function handleStatusChange(row: RuleRow, val: boolean | string | number) { const next = val ? 1 : 0 - // TODO: 接口联通后替换为 toggleCommissionRule(row.id, next) - row.status = next - ElMessage.success(`规则已${next ? '启用' : '停用'}`) + try { + const res: any = await toggleCommissionRule(row.id, next) + if (res?.code !== 200) { + ElMessage.error(res?.msg || '更新规则状态失败') + await fetchList() + return + } + ElMessage.success(`规则已${next ? '启用' : '停用'}`) + } catch (error: any) { + ElMessage.error(error?.response?.data?.msg || error?.message || '更新规则状态失败') + await fetchList() + } } async function handleDelete(row: RuleRow) { @@ -536,11 +494,15 @@ async function handleDelete(row: RuleRow) { return } try { - // TODO: 接口联通后替换为 deleteCommissionRule(row.id) + const res: any = await deleteCommissionRule(row.id) + if (res?.code !== 200) { + ElMessage.error(res?.msg || '删除失败') + return + } ElMessage.success('删除成功') await fetchList() } catch (error: any) { - ElMessage.error(error?.message || '删除失败') + ElMessage.error(error?.response?.data?.msg || error?.message || '删除失败') } } diff --git a/platform/src/views/system/payment/orders/detail.vue b/platform/src/views/system/payment/orders/detail.vue index 80da306..0ec4997 100644 --- a/platform/src/views/system/payment/orders/detail.vue +++ b/platform/src/views/system/payment/orders/detail.vue @@ -125,8 +125,7 @@ import { useRoute, useRouter } from 'vue-router' import { ElMessage, ElMessageBox } from 'element-plus' import { Refresh } from '@element-plus/icons-vue' import StatusTag from '../components/StatusTag.vue' -// TODO: 接口联调时打开 -// import { getOrderDetail, queryPayment, refundPayment } from '@/api/payment' +import { getOrderDetail, queryPayment, refundPayment } from '@/api/payment' const route = useRoute() const router = useRouter() @@ -194,69 +193,25 @@ const orderTypeLabel = (type: string) => orderTypeMap[type] || type || '-' /* ---------------- 数据加载 ---------------- */ async function loadDetail() { + const targetPayNo = payNo.value || outTradeNoQuery.value + if (!targetPayNo) { + ElMessage.error('缺少支付单号') + goBack() + return + } + loading.value = true try { - // TODO: 接口联通后替换为真实请求(后端支持按 payNo 或 outTradeNo 查询) - // const res = await getOrderDetail(payNo.value || outTradeNoQuery.value) - // if (res.success) Object.assign(detail, res.data) - Object.assign(detail, { - pay_no: payNo.value || 'P202609150009', - out_trade_no: outTradeNoQuery.value || 'ORD202609150001', - status: 'paid', - amount: 299900, - currency: 'CNY', - channel: 'wechat', - channel_trade_no: '4200001234202609150987654321', - create_time: '2026-09-15 10:23:12', - paid_at: '2026-09-15 10:24:31', - out_trade_no: 'ORD202609150001', - order_type: 'platform_usage', - tenant_name: '杭州云泽科技有限公司', - tenant_id: 'T10001', - order_amount: 299900, - order_status: 'paid', - subject: '平台使用费 - 2026 年度(标准版)', - merchant_no: '1620888999', - notify_at: '2026-09-15 10:24:33', - notify_url: `${window.location.origin}/api/payment/callback/wechat`, - last_query_at: '2026-09-15 10:25:02', - callback_raw: JSON.stringify( - { - id: 'evt_20260915102431001', - event_type: 'TRANSACTION.SUCCESS', - resource: { - out_trade_no: 'ORD202609150001', - transaction_id: '4200001234202609150987654321', - trade_state: 'SUCCESS', - amount: { total: 299900, payer_total: 299900, currency: 'CNY' } - } - }, - null, - 2 - ), - records: [ - { - to_status: 'created', - create_time: '2026-09-15 10:23:12', - operator: '租户端下单页', - remark: '创建支付单,渠道:微信支付(Native 扫码)' - }, - { - to_status: 'paying', - create_time: '2026-09-15 10:23:35', - operator: '渠道回调', - remark: '用户扫码完成付款,渠道返回支付中' - }, - { - to_status: 'paid', - create_time: '2026-09-15 10:24:31', - operator: '渠道回调', - remark: '支付成功,订单金额 299900 分,已同步业务订单状态为已支付' - } - ] as RecordItem[] - }) + const res: any = await getOrderDetail(targetPayNo) + if (res?.code === 200 && res.data) { + Object.assign(detail, res.data, { + records: res.data.records || [] + }) + } else { + ElMessage.error(res?.msg || '获取支付单详情失败') + } } catch (error: any) { - ElMessage.error(error?.message || '获取支付单详情失败') + ElMessage.error(error?.response?.data?.msg || error?.message || '获取支付单详情失败') } finally { loading.value = false } @@ -266,16 +221,18 @@ async function loadDetail() { /** 手动查询渠道状态:主动调用渠道查询接口并回写本地状态(兜底回调丢失) */ async function handleQueryChannel() { + if (!detail.pay_no) return querying.value = true try { - // TODO: 接口联通后替换为真实请求 - // const res = await queryPayment(payNo.value, { sync: true }) - await new Promise((resolve) => setTimeout(resolve, 600)) - detail.last_query_at = new Date().toLocaleString('zh-CN') - ElMessage.success('渠道状态已查询:SUCCESS(本地状态已同步)') + const res: any = await queryPayment(detail.pay_no, { sync: true }) + if (res?.code !== 200) { + ElMessage.error(res?.msg || '查询渠道状态失败') + return + } + ElMessage.success('渠道状态已查询并同步') await loadDetail() } catch (error: any) { - ElMessage.error(error?.message || '查询渠道状态失败') + ElMessage.error(error?.response?.data?.msg || error?.message || '查询渠道状态失败') } finally { querying.value = false } @@ -301,18 +258,19 @@ async function handleManualRefund() { } try { - // TODO: 接口联通后替换为真实请求 - // await refundPayment({ pay_no: detail.pay_no, amount: detail.amount, reason }) - detail.status = 'refunding' - detail.records.unshift({ - to_status: 'refunding', - create_time: new Date().toLocaleString('zh-CN'), - operator: '平台管理员', - remark: `手动原路退回:${reason}` + const res: any = await refundPayment({ + pay_no: detail.pay_no, + amount: Number(detail.amount), + reason }) + if (res?.code !== 200) { + ElMessage.error(res?.msg || '退回失败') + return + } ElMessage.success('退回申请已提交,请以渠道退回结果为准') + await loadDetail() } catch (error: any) { - ElMessage.error(error?.message || '退回失败') + ElMessage.error(error?.response?.data?.msg || error?.message || '退回失败') } } diff --git a/platform/src/views/system/payment/orders/index.vue b/platform/src/views/system/payment/orders/index.vue index a25f777..0d0631c 100644 --- a/platform/src/views/system/payment/orders/index.vue +++ b/platform/src/views/system/payment/orders/index.vue @@ -126,8 +126,8 @@ import { useRouter } from 'vue-router' import { ElMessage } from 'element-plus' import { Refresh } from '@element-plus/icons-vue' import StatusTag from '../components/StatusTag.vue' -// TODO: 接口联调时打开 -// import { getOrders } from '@/api/payment' +import { getOrders } from '@/api/payment' +import { getTenantList } from '@/api/tenant' const router = useRouter() const loading = ref(false) @@ -167,35 +167,30 @@ const statusOptions = [ { label: '已退款', value: 'refunded' } ] -const tenantOptions = ref([ - { id: 'T10001', name: '杭州云泽科技有限公司' }, - { id: 'T10002', name: '宁波海曙贸易有限公司' }, - { id: 'T10003', name: '上海弘远信息技术有限公司' }, - { id: 'T10004', name: '深圳市博通电子商务有限公司' } -]) +const tenantOptions = ref<{ id: string | number; name: string }[]>([]) const channelName = (channel: string) => channelOptions.find((i) => i.value === channel)?.label || channel -/** 租户远程搜索:TODO 替换为 getTenantList({ keyword }) */ async function searchTenant(keyword: string) { tenantLoading.value = true try { - const all = [ - { id: 'T10001', name: '杭州云泽科技有限公司' }, - { id: 'T10002', name: '宁波海曙贸易有限公司' }, - { id: 'T10003', name: '上海弘远信息技术有限公司' }, - { id: 'T10004', name: '深圳市博通电子商务有限公司' }, - { id: 'T10005', name: '北京中科智联科技有限公司' }, - { id: 'T10006', name: '成都天府软件有限公司' } - ] - tenantOptions.value = keyword ? all.filter((i) => i.name.includes(keyword)) : all + const res: any = await getTenantList({ page: 1, pageSize: 50, keyword }) + if (res?.code === 200) { + const list = res.data?.list || [] + tenantOptions.value = list.map((item: any) => ({ + id: item.id, + name: item.name || item.tenant_name || item.company_name || String(item.id) + })) + } + } catch (error: any) { + ElMessage.error(error?.response?.data?.msg || error?.message || '获取租户列表失败') } finally { tenantLoading.value = false } } -/* ---------------- Mock 数据(本地筛选 + 分页) ---------------- */ +/* ---------------- 列表数据 ---------------- */ interface OrderRow { pay_no: string @@ -208,68 +203,28 @@ interface OrderRow { create_time: string } -const ALL_ORDERS: OrderRow[] = Array.from({ length: 26 }).map((_, index) => { - const channels = ['wechat', 'alipay', 'unionpay', 'cloudpay'] - const statuses = ['paid', 'paid', 'paid', 'pending', 'failed', 'closed', 'refunded'] - const tenants = [ - { id: 'T10001', name: '杭州云泽科技有限公司' }, - { id: 'T10002', name: '宁波海曙贸易有限公司' }, - { id: 'T10003', name: '上海弘远信息技术有限公司' }, - { id: 'T10004', name: '深圳市博通电子商务有限公司' }, - { id: 'T10005', name: '北京中科智联科技有限公司' }, - { id: 'T10006', name: '成都天府软件有限公司' } - ] - const amounts = [99900, 199900, 299900, 499900, 599900] - const tenant = tenants[index % tenants.length] - const day = String(15 - Math.floor(index / 6)).padStart(2, '0') - const hour = String(20 - (index % 12)).padStart(2, '0') - const minute = String((index * 7) % 60).padStart(2, '0') - - return { - pay_no: `P202609${day}${String(1000 + index)}`, - out_trade_no: `ORD202609${day}${String(2000 + index)}`, - tenant_id: tenant.id, - tenant_name: tenant.name, - amount: amounts[index % amounts.length], - channel: channels[index % channels.length], - status: statuses[index % statuses.length], - create_time: `2026-09-${day} ${hour}:${minute}:00` - } -}) - const tableData = ref([]) async function fetchList() { loading.value = true try { - // TODO: 接口联通后替换为真实请求 - // const res = await getOrders({ - // page: page.current, - // pageSize: page.size, - // tenant_id: filters.tenant_id, - // channel: filters.channel, - // status: filters.status, - // start_time: filters.dateRange?.[0], - // end_time: filters.dateRange?.[1] - // }) - // tableData.value = res.data.list || [] - // page.total = res.data.total || 0 - - const [start, end] = filters.dateRange || [] - const filtered = ALL_ORDERS.filter((item) => { - if (filters.tenant_id && item.tenant_id !== filters.tenant_id) return false - if (filters.channel && item.channel !== filters.channel) return false - if (filters.status && item.status !== filters.status) return false - const date = item.create_time.slice(0, 10) - if (start && date < start) return false - if (end && date > end) return false - return true + const res: any = await getOrders({ + page: page.current, + pageSize: page.size, + tenant_id: filters.tenant_id, + channel: filters.channel, + status: filters.status, + start_time: filters.dateRange?.[0], + end_time: filters.dateRange?.[1] }) - page.total = filtered.length - const from = (page.current - 1) * page.size - tableData.value = filtered.slice(from, from + page.size) + if (res?.code === 200) { + tableData.value = res.data?.list || [] + page.total = Number(res.data?.total || 0) + } else { + ElMessage.error(res?.msg || '获取支付订单失败') + } } catch (error: any) { - ElMessage.error(error?.message || '获取支付订单失败') + ElMessage.error(error?.response?.data?.msg || error?.message || '获取支付订单失败') } finally { loading.value = false } diff --git a/platform/src/views/system/payment/reconcile/index.vue b/platform/src/views/system/payment/reconcile/index.vue index 6a29bc7..8852f82 100644 --- a/platform/src/views/system/payment/reconcile/index.vue +++ b/platform/src/views/system/payment/reconcile/index.vue @@ -151,8 +151,7 @@ import { reactive, ref, onMounted } from 'vue' import { ElMessage, ElMessageBox } from 'element-plus' import { Refresh } from '@element-plus/icons-vue' import StatusTag from '../components/StatusTag.vue' -// TODO: 接口联调时打开 -// import { getReconcileList, markReconcileHandled } from '@/api/payment' +import { getReconcileList, markReconcileHandled } from '@/api/payment' const loading = ref(false) @@ -182,7 +181,7 @@ const channelMap: Record = { } const channelLabel = (channel: string) => channelMap[channel] || channel || '-' -/* ---------------- Mock 数据(本地筛选 + 分页) ---------------- */ +/* ---------------- 列表数据 ---------------- */ interface ReconcileRow { id: number @@ -197,134 +196,27 @@ interface ReconcileRow { remark: string } -const ALL_DIFFS: ReconcileRow[] = [ - { - id: 1, - channel: 'wechat', - bill_date: '2026-09-14', - local_trade_no: '', - channel_trade_no: '4200001234202609140011223344', - local_amount: null, - channel_amount: 299900, - diff_type: 'long', - handle_status: 'unhandled', - remark: '' - }, - { - id: 2, - channel: 'alipay', - bill_date: '2026-09-14', - local_trade_no: 'P202609140012', - channel_trade_no: '', - local_amount: 99900, - channel_amount: null, - diff_type: 'short', - handle_status: 'unhandled', - remark: '' - }, - { - id: 3, - channel: 'wechat', - bill_date: '2026-09-13', - local_trade_no: 'P202609130031', - channel_trade_no: '4200001234202609130055667788', - local_amount: 199900, - channel_amount: 199900, - diff_type: 'status_mismatch', - handle_status: 'handled', - remark: '渠道已成功,本地回调丢失,已手动查询同步为已支付' - }, - { - id: 4, - channel: 'unionpay', - bill_date: '2026-09-13', - local_trade_no: 'P202609130028', - channel_trade_no: 'UP2026091300286547', - local_amount: 499900, - channel_amount: 499900, - diff_type: 'status_mismatch', - handle_status: 'handling', - remark: '已联系收单机构核实,等待回复' - }, - { - id: 5, - channel: 'alipay', - bill_date: '2026-09-12', - local_trade_no: '', - channel_trade_no: '2026091222001445123456789012', - local_amount: null, - channel_amount: 99900, - diff_type: 'long', - handle_status: 'ignored', - remark: '渠道账单重复记录(已确认同一笔订单),无需处理' - }, - { - id: 6, - channel: 'wechat', - bill_date: '2026-09-12', - local_trade_no: 'P202609120009', - channel_trade_no: '', - local_amount: 299900, - channel_amount: null, - diff_type: 'short', - handle_status: 'handled', - remark: '用户超时未支付,本地已置为已关闭' - }, - { - id: 7, - channel: 'cloudpay', - bill_date: '2026-09-11', - local_trade_no: 'P202609110004', - channel_trade_no: 'UP2026091100043321', - local_amount: 599900, - channel_amount: 599900, - diff_type: 'status_mismatch', - handle_status: 'handled', - remark: '银联通道时报状态延迟,次日后台查询已一致' - }, - { - id: 8, - channel: 'wechat', - bill_date: '2026-09-11', - local_trade_no: 'P202609110002', - channel_trade_no: '4200001234202609110099001122', - local_amount: 199900, - channel_amount: 199800, - diff_type: 'short', - handle_status: 'unhandled', - remark: '' - } -] - const tableData = ref([]) async function fetchList() { loading.value = true try { - // TODO: 接口联通后替换为真实请求 - // const res = await getReconcileList({ - // page: page.current, - // pageSize: page.size, - // channel: filters.channel, - // bill_date: filters.bill_date, - // diff_type: filters.diff_type, - // handle_status: filters.handle_status - // }) - // tableData.value = res.data.list || [] - // page.total = res.data.total || 0 - - const filtered = ALL_DIFFS.filter((item) => { - if (filters.channel && item.channel !== filters.channel) return false - if (filters.bill_date && item.bill_date !== filters.bill_date) return false - if (filters.diff_type && item.diff_type !== filters.diff_type) return false - if (filters.handle_status && item.handle_status !== filters.handle_status) return false - return true + const res: any = await getReconcileList({ + page: page.current, + pageSize: page.size, + channel: filters.channel, + bill_date: filters.bill_date, + diff_type: filters.diff_type, + handle_status: filters.handle_status }) - page.total = filtered.length - const from = (page.current - 1) * page.size - tableData.value = filtered.slice(from, from + page.size) + if (res?.code === 200) { + tableData.value = res.data?.list || [] + page.total = Number(res.data?.total || 0) + } else { + ElMessage.error(res?.msg || '获取对账差异失败') + } } catch (error: any) { - ElMessage.error(error?.message || '获取对账差异失败') + ElMessage.error(error?.response?.data?.msg || error?.message || '获取对账差异失败') } finally { loading.value = false } @@ -347,13 +239,18 @@ async function handleMarkHandled(row: ReconcileRow) { } try { - // TODO: 接口联通后替换为 markReconcileHandled(row.id, { handle_type: 'manual', remark }) - row.handle_status = 'handled' - row.remark = remark + const res: any = await markReconcileHandled(row.id, { + handle_status: 'handled', + remark + }) + if (res?.code !== 200) { + ElMessage.error(res?.msg || '标记失败') + return + } ElMessage.success('已标记为已处理') await fetchList() } catch (error: any) { - ElMessage.error(error?.message || '标记失败') + ElMessage.error(error?.response?.data?.msg || error?.message || '标记失败') } }