优化客服权限
This commit is contained in:
@@ -1,16 +0,0 @@
|
|||||||
{
|
|
||||||
"enabled": true,
|
|
||||||
"sources": [
|
|
||||||
{
|
|
||||||
"key": "default",
|
|
||||||
"label": "默认账号",
|
|
||||||
"enabled": true,
|
|
||||||
"baseUrl": "https://example.com",
|
|
||||||
"username": "",
|
|
||||||
"password": "",
|
|
||||||
"phone": "",
|
|
||||||
"deviceId": "08bc9d8c-fd15-48ea-bc00-8d754076cafc",
|
|
||||||
"deviceType": 1
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
{
|
|
||||||
"enabled": true,
|
|
||||||
"channels": {
|
|
||||||
"bark": {
|
|
||||||
"enabled": true,
|
|
||||||
"serverUrl": "https://api.day.app",
|
|
||||||
"recipients": []
|
|
||||||
},
|
|
||||||
"wpush": {
|
|
||||||
"enabled": true,
|
|
||||||
"recipients": []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
{
|
|
||||||
"enabled": true,
|
|
||||||
"jobs": [
|
|
||||||
{
|
|
||||||
"id": "cloudtentacles-health",
|
|
||||||
"type": "cloudtentacles_health",
|
|
||||||
"enabled": false,
|
|
||||||
"intervalSeconds": 300,
|
|
||||||
"cooldownSeconds": 1800,
|
|
||||||
"config": {
|
|
||||||
"assetThreshold": 500,
|
|
||||||
"accounts": [
|
|
||||||
{
|
|
||||||
"sourceKey": "default",
|
|
||||||
"label": "默认账号",
|
|
||||||
"enabled": true,
|
|
||||||
"assetThreshold": 500
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -81,7 +81,7 @@ router.post(
|
|||||||
|
|
||||||
router.post(
|
router.post(
|
||||||
'/kuaishou-industry/vouchers/check-available',
|
'/kuaishou-industry/vouchers/check-available',
|
||||||
requireAdminRoles(['admin', 'operator']),
|
requireAdminRoles(['admin', 'operator', 'support']),
|
||||||
createJsonHandler(
|
createJsonHandler(
|
||||||
(req) =>
|
(req) =>
|
||||||
checkAdminKuaishouIndustryVoucherAvailable(
|
checkAdminKuaishouIndustryVoucherAvailable(
|
||||||
@@ -98,7 +98,7 @@ router.post(
|
|||||||
|
|
||||||
router.post(
|
router.post(
|
||||||
'/kuaishou-industry/vouchers/reverse',
|
'/kuaishou-industry/vouchers/reverse',
|
||||||
requireAdminRoles(['admin', 'operator']),
|
requireAdminRoles(['admin', 'operator', 'support']),
|
||||||
createJsonHandler(
|
createJsonHandler(
|
||||||
(req) =>
|
(req) =>
|
||||||
reverseAdminKuaishouIndustryVoucher(req.body as AdminKuaishouIndustryVoucherReverseRouteBody),
|
reverseAdminKuaishouIndustryVoucher(req.body as AdminKuaishouIndustryVoucherReverseRouteBody),
|
||||||
@@ -113,7 +113,7 @@ router.post(
|
|||||||
|
|
||||||
router.post(
|
router.post(
|
||||||
'/kuaishou-industry/vouchers/consume',
|
'/kuaishou-industry/vouchers/consume',
|
||||||
requireAdminRoles(['admin', 'operator']),
|
requireAdminRoles(['admin', 'operator', 'support']),
|
||||||
createJsonHandler(
|
createJsonHandler(
|
||||||
(req) =>
|
(req) =>
|
||||||
consumeAdminKuaishouIndustryVoucherByCode(
|
consumeAdminKuaishouIndustryVoucherByCode(
|
||||||
@@ -130,7 +130,7 @@ router.post(
|
|||||||
|
|
||||||
router.post(
|
router.post(
|
||||||
'/kuaishou-industry/vouchers/resend-code',
|
'/kuaishou-industry/vouchers/resend-code',
|
||||||
requireAdminRoles(['admin', 'operator']),
|
requireAdminRoles(['admin', 'operator', 'support']),
|
||||||
createJsonHandler(
|
createJsonHandler(
|
||||||
(req) =>
|
(req) =>
|
||||||
resendAdminKuaishouIndustryVoucherCode(
|
resendAdminKuaishouIndustryVoucherCode(
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ router.get('/orders/:orderId', createJsonHandler(
|
|||||||
|
|
||||||
router.post(
|
router.post(
|
||||||
'/orders/:orderId/kuaishou-industry/resend-code',
|
'/orders/:orderId/kuaishou-industry/resend-code',
|
||||||
requireAdminRoles(['admin', 'operator']),
|
requireAdminRoles(['admin', 'operator', 'support']),
|
||||||
createJsonHandler(
|
createJsonHandler(
|
||||||
(req) =>
|
(req) =>
|
||||||
resendAdminOrderKuaishouIndustryVoucherCodes(
|
resendAdminOrderKuaishouIndustryVoucherCodes(
|
||||||
|
|||||||
@@ -10,9 +10,9 @@ import fulfillmentRoutingRouter from "./platform-config/fulfillment-routing.js";
|
|||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
|
router.use("/platform-config", kuaishouIndustryRouter);
|
||||||
router.use("/platform-config", requireAdminRoles(["admin"]));
|
router.use("/platform-config", requireAdminRoles(["admin"]));
|
||||||
router.use("/platform-config", notificationsRouter);
|
router.use("/platform-config", notificationsRouter);
|
||||||
router.use("/platform-config", kuaishouIndustryRouter);
|
|
||||||
router.use("/platform-config", kuaishouFeifeiRouter);
|
router.use("/platform-config", kuaishouFeifeiRouter);
|
||||||
router.use("/platform-config", ninetyoneRouter);
|
router.use("/platform-config", ninetyoneRouter);
|
||||||
router.use("/platform-config", fulfillmentRoutingRouter);
|
router.use("/platform-config", fulfillmentRoutingRouter);
|
||||||
|
|||||||
@@ -10,13 +10,14 @@ import type {
|
|||||||
AdminKuaishouIndustryAuthorizationCodeRouteBody,
|
AdminKuaishouIndustryAuthorizationCodeRouteBody,
|
||||||
AdminKuaishouIndustrySourceConfigRouteBody,
|
AdminKuaishouIndustrySourceConfigRouteBody,
|
||||||
} from '../../../types/admin/route-inputs.js'
|
} from '../../../types/admin/route-inputs.js'
|
||||||
import { createJsonHandler } from '../session.js'
|
import { createJsonHandler, requireAdminRoles } from '../session.js'
|
||||||
import type { JsonRecord } from '../../../types/json.js'
|
import type { JsonRecord } from '../../../types/json.js'
|
||||||
|
|
||||||
const router = Router()
|
const router = Router()
|
||||||
|
|
||||||
router.get(
|
router.get(
|
||||||
'/kuaishou-industry-source',
|
'/kuaishou-industry-source',
|
||||||
|
requireAdminRoles(['admin', 'operator', 'support']),
|
||||||
createJsonHandler(() => getAdminKuaishouIndustrySourceConfig(), {
|
createJsonHandler(() => getAdminKuaishouIndustrySourceConfig(), {
|
||||||
successMessage: 'ok',
|
successMessage: 'ok',
|
||||||
errorMessage: '读取快手行业电子凭证配置失败',
|
errorMessage: '读取快手行业电子凭证配置失败',
|
||||||
@@ -26,6 +27,7 @@ router.get(
|
|||||||
|
|
||||||
router.post(
|
router.post(
|
||||||
'/kuaishou-industry-source',
|
'/kuaishou-industry-source',
|
||||||
|
requireAdminRoles(['admin']),
|
||||||
createJsonHandler(
|
createJsonHandler(
|
||||||
(req) =>
|
(req) =>
|
||||||
updateAdminKuaishouIndustrySourceConfig(
|
updateAdminKuaishouIndustrySourceConfig(
|
||||||
@@ -57,6 +59,7 @@ router.post(
|
|||||||
|
|
||||||
router.post(
|
router.post(
|
||||||
'/kuaishou-industry-source/refresh-token',
|
'/kuaishou-industry-source/refresh-token',
|
||||||
|
requireAdminRoles(['admin']),
|
||||||
createJsonHandler((req) => refreshAdminKuaishouIndustryAccessToken(req.body as JsonRecord), {
|
createJsonHandler((req) => refreshAdminKuaishouIndustryAccessToken(req.body as JsonRecord), {
|
||||||
successMessage: '快手行业电子凭证 accessToken 已刷新',
|
successMessage: '快手行业电子凭证 accessToken 已刷新',
|
||||||
errorMessage: '刷新快手行业电子凭证 accessToken 失败',
|
errorMessage: '刷新快手行业电子凭证 accessToken 失败',
|
||||||
@@ -80,6 +83,7 @@ router.post(
|
|||||||
|
|
||||||
router.post(
|
router.post(
|
||||||
'/kuaishou-industry-source/exchange-code',
|
'/kuaishou-industry-source/exchange-code',
|
||||||
|
requireAdminRoles(['admin']),
|
||||||
createJsonHandler(
|
createJsonHandler(
|
||||||
(req) =>
|
(req) =>
|
||||||
exchangeAdminKuaishouIndustryAuthorizationCode(
|
exchangeAdminKuaishouIndustryAuthorizationCode(
|
||||||
|
|||||||
@@ -149,7 +149,7 @@ router.post(
|
|||||||
|
|
||||||
router.post(
|
router.post(
|
||||||
'/tasks/:taskId/kuaishou-industry/resend-code',
|
'/tasks/:taskId/kuaishou-industry/resend-code',
|
||||||
requireAdminRoles(['admin', 'operator']),
|
requireAdminRoles(['admin', 'operator', 'support']),
|
||||||
createJsonHandler(
|
createJsonHandler(
|
||||||
(req) =>
|
(req) =>
|
||||||
resendAdminTaskKuaishouIndustryVoucherCode(getTaskId(req), req.adminSession || null),
|
resendAdminTaskKuaishouIndustryVoucherCode(getTaskId(req), req.adminSession || null),
|
||||||
@@ -164,7 +164,7 @@ router.post(
|
|||||||
|
|
||||||
router.post(
|
router.post(
|
||||||
'/tasks/:taskId/kuaishou-industry/consume',
|
'/tasks/:taskId/kuaishou-industry/consume',
|
||||||
requireAdminRoles(['admin', 'operator']),
|
requireAdminRoles(['admin', 'operator', 'support']),
|
||||||
createJsonHandler(
|
createJsonHandler(
|
||||||
(req) =>
|
(req) =>
|
||||||
consumeAdminTaskKuaishouIndustryVoucher(
|
consumeAdminTaskKuaishouIndustryVoucher(
|
||||||
|
|||||||
@@ -154,7 +154,7 @@ export async function getAdminOrderDetail(
|
|||||||
kuaishouIndustryVouchers: kuaishouIndustryVouchers.map(mapAdminKuaishouIndustryVoucher),
|
kuaishouIndustryVouchers: kuaishouIndustryVouchers.map(mapAdminKuaishouIndustryVoucher),
|
||||||
operations: {
|
operations: {
|
||||||
canResendKuaishouIndustryVoucherCode:
|
canResendKuaishouIndustryVoucherCode:
|
||||||
viewerContext.canManageTaskLifecycle && kuaishouIndustryVouchers.length > 0,
|
viewerContext.canOperateKuaishouIndustryVoucher && kuaishouIndustryVouchers.length > 0,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -360,9 +360,9 @@ export async function getAdminTaskDetail(
|
|||||||
String(task.executor_key || '').trim() === 'kuaishou_ct_assisted' &&
|
String(task.executor_key || '').trim() === 'kuaishou_ct_assisted' &&
|
||||||
canReturnKuaishouCloudFulfillmentStatus(task.task_status),
|
canReturnKuaishouCloudFulfillmentStatus(task.task_status),
|
||||||
canResendKuaishouIndustryVoucherCode:
|
canResendKuaishouIndustryVoucherCode:
|
||||||
viewerContext.canManageTaskLifecycle && hasKuaishouIndustryVoucher,
|
viewerContext.canOperateKuaishouIndustryVoucher && hasKuaishouIndustryVoucher,
|
||||||
canConsumeKuaishouIndustryVoucher:
|
canConsumeKuaishouIndustryVoucher:
|
||||||
viewerContext.canManageTaskLifecycle &&
|
viewerContext.canOperateKuaishouIndustryVoucher &&
|
||||||
hasKuaishouIndustryVoucher &&
|
hasKuaishouIndustryVoucher &&
|
||||||
kuaishouIndustryVoucherStatus !== 'CONSUMED' &&
|
kuaishouIndustryVoucherStatus !== 'CONSUMED' &&
|
||||||
kuaishouIndustryVoucherSendCallbackStatus === 'success',
|
kuaishouIndustryVoucherSendCallbackStatus === 'success',
|
||||||
|
|||||||
@@ -39,6 +39,13 @@ test('canViewerCloseTask allows support to close active task but not redeemed ta
|
|||||||
assert.equal(canViewerCloseTask({ task_status: 'redeemed' }, viewerContext), false)
|
assert.equal(canViewerCloseTask({ task_status: 'redeemed' }, viewerContext), false)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('createAdminViewerContext lets support operate vouchers without lifecycle permissions', () => {
|
||||||
|
const viewerContext = createAdminViewerContext({ role: 'support' })
|
||||||
|
|
||||||
|
assert.equal(viewerContext.canOperateKuaishouIndustryVoucher, true)
|
||||||
|
assert.equal(viewerContext.canManageTaskLifecycle, false)
|
||||||
|
})
|
||||||
|
|
||||||
test('mapKuaishouCloudFulfillmentContext exposes cloud account display labels', () => {
|
test('mapKuaishouCloudFulfillmentContext exposes cloud account display labels', () => {
|
||||||
const flow = mapKuaishouCloudFulfillmentContext(
|
const flow = mapKuaishouCloudFulfillmentContext(
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ export type AdminViewerContext = {
|
|||||||
canViewSensitiveTaskData: boolean
|
canViewSensitiveTaskData: boolean
|
||||||
canManageTaskLifecycle: boolean
|
canManageTaskLifecycle: boolean
|
||||||
canOperateAssistedTask: boolean
|
canOperateAssistedTask: boolean
|
||||||
|
canOperateKuaishouIndustryVoucher: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
type RedeemResolutionAttempt = {
|
type RedeemResolutionAttempt = {
|
||||||
@@ -370,6 +371,7 @@ export function createAdminViewerContext(
|
|||||||
canViewSensitiveTaskData: role === 'admin' || role === 'operator',
|
canViewSensitiveTaskData: role === 'admin' || role === 'operator',
|
||||||
canManageTaskLifecycle: role === 'admin' || role === 'operator',
|
canManageTaskLifecycle: role === 'admin' || role === 'operator',
|
||||||
canOperateAssistedTask: role === 'admin' || role === 'operator' || role === 'support',
|
canOperateAssistedTask: role === 'admin' || role === 'operator' || role === 'support',
|
||||||
|
canOperateKuaishouIndustryVoucher: role === 'admin' || role === 'operator' || role === 'support',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -369,7 +369,7 @@ export async function resendAdminTaskKuaishouIndustryVoucherCode(
|
|||||||
const now = nowIso()
|
const now = nowIso()
|
||||||
const viewerContext = createAdminViewerContext(session)
|
const viewerContext = createAdminViewerContext(session)
|
||||||
|
|
||||||
if (!viewerContext.canManageTaskLifecycle) {
|
if (!viewerContext.canOperateKuaishouIndustryVoucher) {
|
||||||
throw createHttpError('当前账号没有权限重发电子凭证发码回调', {
|
throw createHttpError('当前账号没有权限重发电子凭证发码回调', {
|
||||||
statusCode: 403,
|
statusCode: 403,
|
||||||
errorCode: 'admin_task_kuaishou_industry_resend_forbidden',
|
errorCode: 'admin_task_kuaishou_industry_resend_forbidden',
|
||||||
@@ -446,7 +446,7 @@ export async function resendAdminOrderKuaishouIndustryVoucherCodes(
|
|||||||
const viewerContext = createAdminViewerContext(session)
|
const viewerContext = createAdminViewerContext(session)
|
||||||
const now = nowIso()
|
const now = nowIso()
|
||||||
|
|
||||||
if (!viewerContext.canManageTaskLifecycle) {
|
if (!viewerContext.canOperateKuaishouIndustryVoucher) {
|
||||||
throw createHttpError('当前账号没有权限重发电子凭证发码回调', {
|
throw createHttpError('当前账号没有权限重发电子凭证发码回调', {
|
||||||
statusCode: 403,
|
statusCode: 403,
|
||||||
errorCode: 'admin_order_kuaishou_industry_resend_forbidden',
|
errorCode: 'admin_order_kuaishou_industry_resend_forbidden',
|
||||||
@@ -519,7 +519,7 @@ export async function consumeAdminTaskKuaishouIndustryVoucher(
|
|||||||
const now = nowIso()
|
const now = nowIso()
|
||||||
const viewerContext = createAdminViewerContext(session)
|
const viewerContext = createAdminViewerContext(session)
|
||||||
|
|
||||||
if (!viewerContext.canManageTaskLifecycle) {
|
if (!viewerContext.canOperateKuaishouIndustryVoucher) {
|
||||||
throw createHttpError('当前账号没有权限执行电子凭证核销', {
|
throw createHttpError('当前账号没有权限执行电子凭证核销', {
|
||||||
statusCode: 403,
|
statusCode: 403,
|
||||||
errorCode: 'admin_task_kuaishou_industry_consume_forbidden',
|
errorCode: 'admin_task_kuaishou_industry_consume_forbidden',
|
||||||
|
|||||||
Generated
-30
@@ -995,9 +995,6 @@
|
|||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1015,9 +1012,6 @@
|
|||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1035,9 +1029,6 @@
|
|||||||
"ppc64"
|
"ppc64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1055,9 +1046,6 @@
|
|||||||
"s390x"
|
"s390x"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1075,9 +1063,6 @@
|
|||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1095,9 +1080,6 @@
|
|||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1991,9 +1973,6 @@
|
|||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -2015,9 +1994,6 @@
|
|||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -2039,9 +2015,6 @@
|
|||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -2063,9 +2036,6 @@
|
|||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import {
|
|||||||
Tag,
|
Tag,
|
||||||
Typography,
|
Typography,
|
||||||
} from 'antd'
|
} from 'antd'
|
||||||
import type { TableColumnsType } from 'antd'
|
import type { TableColumnsType, TabsProps } from 'antd'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import type { Dayjs } from 'dayjs'
|
import type { Dayjs } from 'dayjs'
|
||||||
import { useEffect, useMemo, useState } from 'react'
|
import { useEffect, useMemo, useState } from 'react'
|
||||||
@@ -47,6 +47,7 @@ import type {
|
|||||||
AdminKuaishouIndustryVoucher,
|
AdminKuaishouIndustryVoucher,
|
||||||
AdminKuaishouIndustryVoucherToolPayload,
|
AdminKuaishouIndustryVoucherToolPayload,
|
||||||
} from '@/types/admin'
|
} from '@/types/admin'
|
||||||
|
import { hasAdminRole } from '@/utils/admin-auth'
|
||||||
import { formatAdminDateTime } from '@/utils/admin-time'
|
import { formatAdminDateTime } from '@/utils/admin-time'
|
||||||
import { stringifyDisplayJson } from '@/utils/date-time'
|
import { stringifyDisplayJson } from '@/utils/date-time'
|
||||||
|
|
||||||
@@ -78,6 +79,7 @@ const DEFAULT_ETICKET_TYPE = 'DINING_OPEN_TICKET'
|
|||||||
|
|
||||||
export default function AdminKuaishouIndustryPage() {
|
export default function AdminKuaishouIndustryPage() {
|
||||||
const [activeTab, setActiveTab] = useState<IndustryTab>('vouchers')
|
const [activeTab, setActiveTab] = useState<IndustryTab>('vouchers')
|
||||||
|
const canManageRefund = hasAdminRole('operator')
|
||||||
const [voucherLoading, setVoucherLoading] = useState(false)
|
const [voucherLoading, setVoucherLoading] = useState(false)
|
||||||
const [actionLoading, setActionLoading] = useState('')
|
const [actionLoading, setActionLoading] = useState('')
|
||||||
const [vouchers, setVouchers] = useState<AdminKuaishouIndustryVoucher[]>([])
|
const [vouchers, setVouchers] = useState<AdminKuaishouIndustryVoucher[]>([])
|
||||||
@@ -475,30 +477,39 @@ export default function AdminKuaishouIndustryPage() {
|
|||||||
showSuccess('已填入退款操作区')
|
showSuccess('已填入退款操作区')
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
const tabItems: TabsProps['items'] = [
|
||||||
<div className="page-stack kuaishou-industry-page">
|
|
||||||
<PageHeader title="快手电子凭证" description="行业电子凭证接口工具与售后处理" />
|
|
||||||
|
|
||||||
<Tabs
|
|
||||||
activeKey={activeTab}
|
|
||||||
onChange={(key) => setActiveTab(key as IndustryTab)}
|
|
||||||
items={[
|
|
||||||
{
|
{
|
||||||
key: 'vouchers',
|
key: 'vouchers',
|
||||||
label: '券码操作',
|
label: '券码操作',
|
||||||
children: renderVoucherTab(),
|
children: renderVoucherTab(),
|
||||||
},
|
},
|
||||||
|
...(canManageRefund
|
||||||
|
? [
|
||||||
{
|
{
|
||||||
key: 'refunds',
|
key: 'refunds',
|
||||||
label: '售后退款',
|
label: '售后退款',
|
||||||
children: renderRefundTab(),
|
children: renderRefundTab(),
|
||||||
},
|
},
|
||||||
|
]
|
||||||
|
: []),
|
||||||
{
|
{
|
||||||
key: 'result',
|
key: 'result',
|
||||||
label: '接口结果',
|
label: '接口结果',
|
||||||
children: renderResultTab(),
|
children: renderResultTab(),
|
||||||
},
|
},
|
||||||
]}
|
]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="page-stack kuaishou-industry-page">
|
||||||
|
<PageHeader
|
||||||
|
title="快手电子凭证"
|
||||||
|
description={canManageRefund ? '行业电子凭证接口工具与售后处理' : '行业电子凭证接口工具'}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Tabs
|
||||||
|
activeKey={activeTab}
|
||||||
|
onChange={(key) => setActiveTab(key as IndustryTab)}
|
||||||
|
items={tabItems}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -87,6 +87,7 @@ export default function AdminTaskDetailPage() {
|
|||||||
})
|
})
|
||||||
const detail = query.data?.data
|
const detail = query.data?.data
|
||||||
const canManageTaskLifecycle = hasAdminRole('operator')
|
const canManageTaskLifecycle = hasAdminRole('operator')
|
||||||
|
const canOperateIndustryVoucher = hasAdminRole('support')
|
||||||
const canCloseTasks = hasAdminRole('support')
|
const canCloseTasks = hasAdminRole('support')
|
||||||
const claimTokenStatus = String(detail?.claimToken?.status || '').trim()
|
const claimTokenStatus = String(detail?.claimToken?.status || '').trim()
|
||||||
const isExternalClaimLink = claimTokenStatus.toLowerCase() === 'external'
|
const isExternalClaimLink = claimTokenStatus.toLowerCase() === 'external'
|
||||||
@@ -266,6 +267,7 @@ export default function AdminTaskDetailPage() {
|
|||||||
<TaskActionPanel
|
<TaskActionPanel
|
||||||
detail={resolvedDetail}
|
detail={resolvedDetail}
|
||||||
canManageTaskLifecycle={canManageTaskLifecycle}
|
canManageTaskLifecycle={canManageTaskLifecycle}
|
||||||
|
canOperateIndustryVoucher={canOperateIndustryVoucher}
|
||||||
canCloseTasks={canCloseTasks}
|
canCloseTasks={canCloseTasks}
|
||||||
actionLoadingKey={actionLoadingKey}
|
actionLoadingKey={actionLoadingKey}
|
||||||
onRetry={() =>
|
onRetry={() =>
|
||||||
@@ -490,6 +492,7 @@ export default function AdminTaskDetailPage() {
|
|||||||
function TaskActionPanel({
|
function TaskActionPanel({
|
||||||
detail,
|
detail,
|
||||||
canManageTaskLifecycle,
|
canManageTaskLifecycle,
|
||||||
|
canOperateIndustryVoucher,
|
||||||
canCloseTasks,
|
canCloseTasks,
|
||||||
actionLoadingKey,
|
actionLoadingKey,
|
||||||
onRetry,
|
onRetry,
|
||||||
@@ -504,6 +507,7 @@ function TaskActionPanel({
|
|||||||
}: {
|
}: {
|
||||||
detail: AdminTaskDetail
|
detail: AdminTaskDetail
|
||||||
canManageTaskLifecycle: boolean
|
canManageTaskLifecycle: boolean
|
||||||
|
canOperateIndustryVoucher: boolean
|
||||||
canCloseTasks: boolean
|
canCloseTasks: boolean
|
||||||
actionLoadingKey: string
|
actionLoadingKey: string
|
||||||
onRetry: () => void
|
onRetry: () => void
|
||||||
@@ -607,7 +611,7 @@ function TaskActionPanel({
|
|||||||
</section>
|
</section>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{canManageTaskLifecycle && hasIndustryVoucherActions ? (
|
{canOperateIndustryVoucher && hasIndustryVoucherActions ? (
|
||||||
<section className="task-action-group">
|
<section className="task-action-group">
|
||||||
<Typography.Text type="secondary">电子凭证</Typography.Text>
|
<Typography.Text type="secondary">电子凭证</Typography.Text>
|
||||||
<Space wrap>
|
<Space wrap>
|
||||||
@@ -648,7 +652,7 @@ function TaskActionPanel({
|
|||||||
</section>
|
</section>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{!canManageTaskLifecycle && !canCloseTasks && !hasCloudActions ? (
|
{!canManageTaskLifecycle && !canOperateIndustryVoucher && !canCloseTasks && !hasCloudActions ? (
|
||||||
<Typography.Text type="secondary">当前账号没有可执行的任务操作</Typography.Text>
|
<Typography.Text type="secondary">当前账号没有可执行的任务操作</Typography.Text>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Executable
+364
@@ -0,0 +1,364 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -Eeuo pipefail
|
||||||
|
|
||||||
|
# Mac 本机 Docker 开发环境启动脚本。
|
||||||
|
# 用法:
|
||||||
|
# bash deploy/mac-dev.sh
|
||||||
|
# bash deploy/mac-dev.sh --reset-db
|
||||||
|
|
||||||
|
PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
ENV_FILE="${PROJECT_ROOT}/.env"
|
||||||
|
ENV_EXAMPLE="${PROJECT_ROOT}/.env.mac-docker.example"
|
||||||
|
COMPOSE_FILE="${PROJECT_ROOT}/docker-compose.dev.yml"
|
||||||
|
BACKEND_DATA_DIR="${PROJECT_ROOT}/apps/backend/data"
|
||||||
|
READY_TIMEOUT="${READY_TIMEOUT:-120}"
|
||||||
|
HEALTH_URL="${DEV_HEALTH_URL:-http://localhost/health/ready}"
|
||||||
|
RESET_DB=false
|
||||||
|
RESET_DB_CONFIRM="${RESET_DB_CONFIRM:-}"
|
||||||
|
COMPOSE_CMD=(docker compose -f "${COMPOSE_FILE}")
|
||||||
|
|
||||||
|
log() {
|
||||||
|
printf '\033[1;34m[mac-dev]\033[0m %s\n' "$*"
|
||||||
|
}
|
||||||
|
|
||||||
|
warn() {
|
||||||
|
printf '\033[1;33m[mac-dev]\033[0m %s\n' "$*"
|
||||||
|
}
|
||||||
|
|
||||||
|
fail() {
|
||||||
|
printf '\033[1;31m[mac-dev]\033[0m %s\n' "$*" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
print_usage() {
|
||||||
|
cat <<'EOF'
|
||||||
|
用法:
|
||||||
|
bash deploy/mac-dev.sh
|
||||||
|
bash deploy/mac-dev.sh --reset-db
|
||||||
|
|
||||||
|
参数:
|
||||||
|
--reset-db 删除 dev Compose PostgreSQL 数据卷 postgres_dev_data,并用当前迁移重新初始化数据库。
|
||||||
|
--yes 跳过 --reset-db 的交互确认,也可使用环境变量 RESET_DB_CONFIRM=1。
|
||||||
|
-h, --help 显示帮助。
|
||||||
|
|
||||||
|
环境变量:
|
||||||
|
READY_TIMEOUT 等待容器和健康检查的秒数,默认 120。
|
||||||
|
DEV_HEALTH_URL 本机健康检查地址,默认 http://localhost/health/ready。
|
||||||
|
|
||||||
|
注意:
|
||||||
|
--reset-db 会删除本机开发库里的订单、任务、后台用户、审计日志和迁移记录。
|
||||||
|
它不会删除 apps/backend/data 下的平台 JSON 配置,也不会删除 node_modules 缓存 volume。
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
parse_args() {
|
||||||
|
while (($# > 0)); do
|
||||||
|
case "$1" in
|
||||||
|
--reset-db)
|
||||||
|
RESET_DB=true
|
||||||
|
;;
|
||||||
|
--yes)
|
||||||
|
RESET_DB_CONFIRM=1
|
||||||
|
;;
|
||||||
|
-h|--help)
|
||||||
|
print_usage
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
fail "未知参数:$1。执行 bash deploy/mac-dev.sh --help 查看用法。"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
shift
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
read_env_value() {
|
||||||
|
local key="$1"
|
||||||
|
awk -F= -v key="${key}" '
|
||||||
|
$0 !~ /^[[:space:]]*#/ && $1 == key {
|
||||||
|
value = substr($0, index($0, "=") + 1)
|
||||||
|
gsub(/^[[:space:]]+|[[:space:]]+$/, "", value)
|
||||||
|
gsub(/^"|"$/, "", value)
|
||||||
|
gsub(/^'\''|'\''$/, "", value)
|
||||||
|
print value
|
||||||
|
exit
|
||||||
|
}
|
||||||
|
' "${ENV_FILE}"
|
||||||
|
}
|
||||||
|
|
||||||
|
ensure_docker() {
|
||||||
|
command -v docker >/dev/null 2>&1 || fail "未找到 docker,请先安装并启动 Docker Desktop for Mac。"
|
||||||
|
docker compose version >/dev/null 2>&1 || fail "未找到 docker compose 插件,请升级 Docker Desktop。"
|
||||||
|
command -v curl >/dev/null 2>&1 || fail "未找到 curl,请先安装 curl。"
|
||||||
|
}
|
||||||
|
|
||||||
|
ensure_env_file() {
|
||||||
|
if [[ -f "${ENV_FILE}" ]]; then
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
[[ -f "${ENV_EXAMPLE}" ]] || fail "未找到本机开发环境模板:${ENV_EXAMPLE}"
|
||||||
|
cp "${ENV_EXAMPLE}" "${ENV_FILE}"
|
||||||
|
log "未找到 .env,已从 .env.mac-docker.example 生成本机开发配置。"
|
||||||
|
}
|
||||||
|
|
||||||
|
validate_env_file() {
|
||||||
|
[[ "${READY_TIMEOUT}" =~ ^[1-9][0-9]*$ ]] || fail "READY_TIMEOUT 必须是正整数秒数,当前值:${READY_TIMEOUT}"
|
||||||
|
|
||||||
|
validate_database_url_for_dev_compose
|
||||||
|
}
|
||||||
|
|
||||||
|
validate_database_url_for_dev_compose() {
|
||||||
|
local database_url
|
||||||
|
database_url="$(read_env_value DATABASE_URL)"
|
||||||
|
|
||||||
|
if [[ -z "${database_url}" ]]; then
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "${database_url}" == *"@localhost:"* \
|
||||||
|
|| "${database_url}" == *"@127.0.0.1:"* \
|
||||||
|
|| "${database_url}" == *"@[::1]:"* ]]; then
|
||||||
|
fail ".env 的 DATABASE_URL 当前指向本机地址。Docker dev 后端容器内的 localhost 不是 postgres 容器,请改为 postgres 服务名。"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
validate_reset_database_target() {
|
||||||
|
local database_url
|
||||||
|
database_url="$(read_env_value DATABASE_URL)"
|
||||||
|
|
||||||
|
if [[ -n "${database_url}" && "${database_url}" != *"@postgres:"* ]]; then
|
||||||
|
fail "--reset-db 只支持重置本脚本管理的 dev Compose PostgreSQL。当前 DATABASE_URL 未指向 postgres 服务,请手动确认外部数据库后再处理。"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
confirm_reset_database() {
|
||||||
|
if [[ "${RESET_DB}" != "true" ]]; then
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
validate_reset_database_target
|
||||||
|
|
||||||
|
warn "即将重置 Mac 本机开发 PostgreSQL 数据库:会删除订单、任务、后台用户、审计日志和所有迁移记录。"
|
||||||
|
warn "不会删除 apps/backend/data 下的平台 JSON 配置,也不会删除 node_modules 缓存 volume。"
|
||||||
|
|
||||||
|
if [[ "${RESET_DB_CONFIRM}" == "1" ]]; then
|
||||||
|
warn "检测到 RESET_DB_CONFIRM=1 或 --yes,跳过交互确认。"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ! -t 0 ]]; then
|
||||||
|
fail "当前不是交互式终端。若确认要重置数据库,请执行:RESET_DB_CONFIRM=1 bash deploy/mac-dev.sh --reset-db"
|
||||||
|
fi
|
||||||
|
|
||||||
|
local answer
|
||||||
|
printf '请输入 RESET 确认重置数据库:'
|
||||||
|
read -r answer
|
||||||
|
[[ "${answer}" == "RESET" ]] || fail "未确认重置数据库,已停止。"
|
||||||
|
}
|
||||||
|
|
||||||
|
ensure_data_dir() {
|
||||||
|
mkdir -p "${BACKEND_DATA_DIR}"
|
||||||
|
}
|
||||||
|
|
||||||
|
check_compose_config() {
|
||||||
|
log "检查 Docker Compose dev 配置"
|
||||||
|
"${COMPOSE_CMD[@]}" config >/dev/null || fail "Docker Compose dev 配置校验失败。"
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve_compose_project_name() {
|
||||||
|
local project_name
|
||||||
|
project_name="$(read_env_value COMPOSE_PROJECT_NAME || true)"
|
||||||
|
|
||||||
|
if [[ -z "${project_name}" ]]; then
|
||||||
|
project_name="$(basename "${PROJECT_ROOT}")"
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf '%s\n' "${project_name}"
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve_postgres_volume_name() {
|
||||||
|
local container_id volume_name project_name expected_volume
|
||||||
|
|
||||||
|
container_id="$("${COMPOSE_CMD[@]}" ps -aq postgres 2>/dev/null | head -n 1 || true)"
|
||||||
|
if [[ -n "${container_id}" ]]; then
|
||||||
|
volume_name="$(
|
||||||
|
docker inspect -f '{{range .Mounts}}{{if eq .Destination "/var/lib/postgresql/data"}}{{if eq .Type "volume"}}{{.Name}}{{end}}{{end}}{{end}}' "${container_id}" 2>/dev/null || true
|
||||||
|
)"
|
||||||
|
if [[ -n "${volume_name}" ]]; then
|
||||||
|
printf '%s\n' "${volume_name}"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
project_name="$(resolve_compose_project_name)"
|
||||||
|
expected_volume="${project_name}_postgres_dev_data"
|
||||||
|
if docker volume inspect "${expected_volume}" >/dev/null 2>&1; then
|
||||||
|
printf '%s\n' "${expected_volume}"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
volume_name="$(
|
||||||
|
docker volume ls --format '{{.Name}}' \
|
||||||
|
| awk '$0 ~ /(^|_)postgres_dev_data$/ { print; exit }' || true
|
||||||
|
)"
|
||||||
|
if [[ -n "${volume_name}" ]]; then
|
||||||
|
printf '%s\n' "${volume_name}"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf '%s\n' "${expected_volume}"
|
||||||
|
}
|
||||||
|
|
||||||
|
reset_database() {
|
||||||
|
if [[ "${RESET_DB}" != "true" ]]; then
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
local volume_name
|
||||||
|
volume_name="$(resolve_postgres_volume_name)"
|
||||||
|
|
||||||
|
warn "开始重置本机开发数据库,将删除 Docker volume:${volume_name}"
|
||||||
|
"${COMPOSE_CMD[@]}" stop web frontend backend postgres >/dev/null 2>&1 || true
|
||||||
|
"${COMPOSE_CMD[@]}" rm -f -s postgres >/dev/null 2>&1 || true
|
||||||
|
|
||||||
|
if docker volume inspect "${volume_name}" >/dev/null 2>&1; then
|
||||||
|
docker volume rm "${volume_name}" >/dev/null || fail "删除 PostgreSQL 开发数据卷失败:${volume_name}"
|
||||||
|
log "已删除 PostgreSQL 开发数据卷:${volume_name}"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
warn "未找到 PostgreSQL 开发数据卷:${volume_name},将按空库继续启动。"
|
||||||
|
}
|
||||||
|
|
||||||
|
wait_for_service() {
|
||||||
|
local service="$1"
|
||||||
|
local label="$2"
|
||||||
|
local attempt container_id status health_status
|
||||||
|
|
||||||
|
log "等待 ${label} 就绪"
|
||||||
|
|
||||||
|
for ((attempt = 1; attempt <= READY_TIMEOUT; attempt++)); do
|
||||||
|
container_id="$("${COMPOSE_CMD[@]}" ps -q "${service}" 2>/dev/null || true)"
|
||||||
|
if [[ -n "${container_id}" ]]; then
|
||||||
|
status="$(docker inspect -f '{{.State.Status}}' "${container_id}" 2>/dev/null || true)"
|
||||||
|
health_status="$(docker inspect -f '{{if .State.Health}}{{.State.Health.Status}}{{end}}' "${container_id}" 2>/dev/null || true)"
|
||||||
|
|
||||||
|
if [[ -n "${health_status}" && "${health_status}" == "healthy" ]]; then
|
||||||
|
log "${label} 已就绪"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -z "${health_status}" && "${status}" == "running" ]]; then
|
||||||
|
log "${label} 已启动"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "${status}" == "exited" || "${status}" == "dead" ]]; then
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
|
||||||
|
print_diagnostics
|
||||||
|
fail "${label} 启动超时,请根据上面的日志排查。"
|
||||||
|
}
|
||||||
|
|
||||||
|
start_dev_stack() {
|
||||||
|
log "启动 Mac 本机 Docker 开发环境"
|
||||||
|
if "${COMPOSE_CMD[@]}" up -d --build; then
|
||||||
|
wait_for_service postgres "PostgreSQL"
|
||||||
|
wait_for_service backend "后端"
|
||||||
|
wait_for_service frontend "前端"
|
||||||
|
wait_for_service web "Web 入口"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
print_diagnostics
|
||||||
|
fail "Mac 本机 Docker 开发环境启动失败,请根据上面的日志排查。"
|
||||||
|
}
|
||||||
|
|
||||||
|
wait_for_health() {
|
||||||
|
log "等待开发入口健康检查:${HEALTH_URL}"
|
||||||
|
|
||||||
|
for ((attempt = 1; attempt <= READY_TIMEOUT; attempt++)); do
|
||||||
|
if curl -fsS "${HEALTH_URL}" >/dev/null 2>&1; then
|
||||||
|
log "开发环境已就绪"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
|
||||||
|
print_diagnostics
|
||||||
|
fail "开发环境已启动但健康检查未通过,请根据日志排查。"
|
||||||
|
}
|
||||||
|
|
||||||
|
print_applied_migrations() {
|
||||||
|
log "已应用数据库迁移"
|
||||||
|
|
||||||
|
local sql migrations
|
||||||
|
sql='SELECT COALESCE(filename, name) FROM schema_migrations ORDER BY COALESCE(filename, name);'
|
||||||
|
|
||||||
|
if migrations="$("${COMPOSE_CMD[@]}" exec -T postgres sh -lc 'psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" -Atc "$1"' sh "${sql}" 2>&1)"; then
|
||||||
|
printf '%s\n' "${migrations:-"(无迁移记录)"}"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
warn "读取迁移记录失败:"
|
||||||
|
printf '%s\n' "${migrations}"
|
||||||
|
}
|
||||||
|
|
||||||
|
print_diagnostics() {
|
||||||
|
warn "当前 dev 容器状态:"
|
||||||
|
"${COMPOSE_CMD[@]}" ps || true
|
||||||
|
|
||||||
|
warn "后端最近日志:"
|
||||||
|
local backend_logs
|
||||||
|
backend_logs="$("${COMPOSE_CMD[@]}" logs --tail=120 backend 2>&1 || true)"
|
||||||
|
printf '%s\n' "${backend_logs}"
|
||||||
|
|
||||||
|
if printf '%s\n' "${backend_logs}" | grep -q 'password authentication failed for user "postgres"'; then
|
||||||
|
warn "检测到 PostgreSQL 密码认证失败。通常是 postgres_dev_data 已用旧密码初始化,而 .env 改成了新密码。"
|
||||||
|
warn "无本地数据保留需求时可执行:RESET_DB_CONFIRM=1 bash deploy/mac-dev.sh --reset-db"
|
||||||
|
fi
|
||||||
|
|
||||||
|
warn "前端最近日志:"
|
||||||
|
"${COMPOSE_CMD[@]}" logs --tail=80 frontend || true
|
||||||
|
|
||||||
|
warn "后端 ready 接口:"
|
||||||
|
"${COMPOSE_CMD[@]}" exec -T backend sh -lc 'curl -fsS http://127.0.0.1:3000/health/ready || true' || true
|
||||||
|
|
||||||
|
print_applied_migrations || true
|
||||||
|
}
|
||||||
|
|
||||||
|
print_endpoints() {
|
||||||
|
log "常用入口"
|
||||||
|
printf '%s\n' " 前端、后台、领取页:http://localhost"
|
||||||
|
printf '%s\n' " 后端健康检查:http://localhost/health"
|
||||||
|
printf '%s\n' " 后端 ready 检查:http://localhost/health/ready"
|
||||||
|
}
|
||||||
|
|
||||||
|
main() {
|
||||||
|
parse_args "$@"
|
||||||
|
|
||||||
|
cd "${PROJECT_ROOT}"
|
||||||
|
|
||||||
|
ensure_docker
|
||||||
|
ensure_env_file
|
||||||
|
validate_env_file
|
||||||
|
confirm_reset_database
|
||||||
|
ensure_data_dir
|
||||||
|
check_compose_config
|
||||||
|
reset_database
|
||||||
|
start_dev_stack
|
||||||
|
wait_for_health
|
||||||
|
print_applied_migrations
|
||||||
|
|
||||||
|
log "Mac 本机开发环境启动完成"
|
||||||
|
"${COMPOSE_CMD[@]}" ps
|
||||||
|
print_endpoints
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$@"
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
店铺订单取消
|
||||||
|
对订单列表中的店铺订单发起取消。仅需传平台单号(platform_order_no)。取消规则比商户后台更严格:仅待绑定可取消,充值中不可「申请取消」。
|
||||||
|
|
||||||
|
#接口地址
|
||||||
|
code
|
||||||
|
POST /api/open/v1/orders/cancel
|
||||||
|
#请求方式
|
||||||
|
POST
|
||||||
|
|
||||||
|
#请求参数
|
||||||
|
参数名 类型 必填 说明
|
||||||
|
platform_order_no string 是 店铺平台单号
|
||||||
|
#响应参数
|
||||||
|
| 参数名 | 类型 | 说明 | | --- | --- | --- | --- | | data.order.order_no | string | 平台内部单号 | | data.order.platform_order_no | string | 平台单号 | | data.order.recharge_status | number | 充值状态码 | | data.order.recharge_status_label | string | 充值状态中文 | | data.order.is_verifi | number | 核销状态(卡券单时有值) | | data.order.updated_at | string | 更新时间 |
|
||||||
|
|
||||||
|
#响应示例
|
||||||
|
json
|
||||||
|
{
|
||||||
|
"code": 10000,
|
||||||
|
"message": "订单已取消",
|
||||||
|
"data": {
|
||||||
|
"order": {
|
||||||
|
"order_no": "202601011200001234",
|
||||||
|
"platform_order_no": "2617600561780683",
|
||||||
|
"recharge_status": 60,
|
||||||
|
"recharge_status_label": "已取消"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#常见失败
|
||||||
|
message 说明
|
||||||
|
店铺订单不存在 单号不存在或非店铺订单
|
||||||
|
仅待绑定状态的订单可取消 订单已进入充值中等后续状态
|
||||||
|
#说明
|
||||||
|
鉴权与签名同 鉴权与签名。
|
||||||
|
仅待绑定状态且走供货充值履约的店铺订单可取消;充值中、失败、人工等状态不可取消。
|
||||||
|
取消后立即完成并退积分(按平台配置)。
|
||||||
|
与商户端 POST /api/merchant/orders/cancel 相比:开放 API 不支持充值中「申请取消」,也不支持失败/人工介入等状态的取消;商户后台在上述状态仍可操作(充值中为软取消)。
|
||||||
Reference in New Issue
Block a user