feat(admin): 添加商户登录密码修改功能并完善优惠券金额验证
- 在AdminBusinessService中添加商户登录密码更新逻辑,包括长度验证和哈希存储
- 在MerchantUpdateRequest中添加loginPassword字段用于接收新密码
- 在CouponCreateRequest和CouponUpdateRequest中添加金额最小值验证注解
- 更新前端表单验证逻辑以检查优惠券金额的有效性
- 修改商户创建和编辑页面的密码相关表单项显示逻辑
- 集成汇付支付网关响应解析优化,支持更多状态码和字段映射
- 移除多个页面中的演示数据开关和相关逻辑代码
- 更新应用配置文件中的汇付支付参数为生产环境配置
- 修复订单详情页面的数据加载逻辑,移除演示模式
- 更新结算服务接口定义以包含审核和支付时间字段
- 优化CSS样式以改善优惠券管理界面的视觉效果
| | |
| | | |
| | | // Toggle homepage mock metrics independently from the login bypass. |
| | | // When disabled, the dashboard requests the authenticated merchant summary API. |
| | | export const useDashboardDemoDataInDevelopment = true |
| | | export const useDashboardDemoDataInDevelopment = false |
| | | |
| | | // 商户中心、商品与营业时间的演示数据开关,与登录跳过和首页经营数据开关相互独立。 |
| | | export const useMerchantManagementDemoDataInDevelopment = true |
| | | // Toggle merchant-center, product, and business-hour demo data independently from login bypass and dashboard demo data. |
| | | export const useMerchantManagementDemoDataInDevelopment = false |
| | | |
| | | // 后台提供微信小程序合法 HTTPS 域名后,在对应环境填写完整基础地址。 |
| | | const developmentBaseUrl = 'http://192.168.110.13:8080' |
| | | //const developmentBaseUrl = 'http://192.168.110.13:8080' |
| | | const developmentBaseUrl = 'http://221.182.45.100:11451' |
| | | const stagingBaseUrl = '' |
| | | const productionBaseUrl = '' |
| | | |
| | |
| | | "quickapp": {}, |
| | | /* 小程序特有相关 */ |
| | | "mp-weixin": { |
| | | "appid": "wxfb87c6ef164c2922", |
| | | "appid": "wxe91f1af7638aa5dd", |
| | | "setting": { |
| | | "urlCheck": false |
| | | }, |
| | |
| | | </template> |
| | | |
| | | <script setup lang="uts"> |
| | | import { apiEnvironment, useMerchantManagementDemoDataInDevelopment } from '../../config/api.uts' |
| | | import { hasActiveSession } from '../../services/session.uts' |
| | | import { MerchantProfile, loadDemoMerchantProfile, loadMerchantProfile, updateDemoMerchantBusinessStatus, updateMerchantBusinessStatus } from '../../services/merchant-management.uts' |
| | | import { MerchantProfile, loadMerchantProfile, updateMerchantBusinessStatus } from '../../services/merchant-management.uts' |
| | | |
| | | const useDemoData = apiEnvironment == 'development' && useMerchantManagementDemoDataInDevelopment |
| | | const merchant = ref<MerchantProfile>(loadDemoMerchantProfile()) |
| | | const merchant = ref<MerchantProfile>({ merchantId: '', storeId: '', merchantName: '', storeName: '', address: '', coverUrl: '', businessStatus: '', onlineProductCount: 0 }) |
| | | const loadState = ref('loading') |
| | | const traceId = ref('') |
| | | const businessUpdating = ref(false) |
| | |
| | | } |
| | | loadState.value = 'loading' |
| | | traceId.value = '' |
| | | if (useDemoData) { |
| | | merchant.value = loadDemoMerchantProfile() |
| | | loadState.value = 'success' |
| | | return |
| | | } |
| | | if (!hasActiveSession()) { |
| | | loadState.value = 'error' |
| | | return |
| | |
| | | if (businessUpdating.value || loadState.value != 'success') return |
| | | const nextStatus = isBusinessOpen.value ? 'closed' : 'open' |
| | | businessUpdating.value = true |
| | | if (useDemoData) { |
| | | merchant.value = updateDemoMerchantBusinessStatus(nextStatus) |
| | | businessUpdating.value = false |
| | | uni.showToast({ title: nextStatus == 'open' ? '已切换为营业中' : '已切换为休息中', icon: 'none' }) |
| | | return |
| | | } |
| | | updateMerchantBusinessStatus(nextStatus, (result: MerchantProfile, _traceId: string) => { |
| | | merchant.value.businessStatus = result.businessStatus |
| | | businessUpdating.value = false |
| | |
| | | <script setup lang="uts"> |
| | | import { revokeOrderVerification } from '../../services/orders.uts' |
| | | import { loadMerchantOrderDetail, MerchantOrderDetail } from '../../services/orders.uts' |
| | | import { apiEnvironment } from '../../config/api.uts' |
| | | import { getLocalOrderDetail } from '../../config/order-demo-data.uts' |
| | | import { formatErrorMessage } from '../../services/diagnostics.uts' |
| | | |
| | | const orderId = ref('') |
| | |
| | | const canRevokeVerification = ref(false) |
| | | const revokeExpiredMessage = ref('已过可撤销时间') |
| | | const revokeSubmitting = ref(false) |
| | | const statusTitle = ref('待支付') |
| | | const statusDesc = ref('订单已生成,等待用户完成支付后即可继续核销。') |
| | | const statusTitle = ref('订单加载中') |
| | | const statusDesc = ref('正在获取订单详情。') |
| | | const statusIcon = ref('/static/icons/lucide/clock-3.svg') |
| | | const orderNo = ref('155464871234') |
| | | const productAmount = ref('¥256.00') |
| | | const talentDiscount = ref('-¥128.00') |
| | | const productDiscount = ref('-¥0.00') |
| | | const paidAmount = ref('¥128.00') |
| | | const subsidyAmount = ref('¥128.00') |
| | | const createdAt = ref('2026-07-27 10:26:15') |
| | | const verificationAt = ref('待核销') |
| | | const customerName = ref('刘先生') |
| | | const customerPhone = ref('183***4568') |
| | | const useDemoData = (): boolean => apiEnvironment == 'development' && uni.getStorageSync('merchant-orders-demo-data') != false |
| | | const orderNo = ref('') |
| | | const productAmount = ref('') |
| | | const talentDiscount = ref('') |
| | | const productDiscount = ref('') |
| | | const paidAmount = ref('') |
| | | const subsidyAmount = ref('') |
| | | const createdAt = ref('') |
| | | const verificationAt = ref('') |
| | | const customerName = ref('') |
| | | const customerPhone = ref('') |
| | | const formatCents = (value: number): string => '¥' + (value / 100).toFixed(2) |
| | | |
| | | const applyRemoteOrder = (detail: MerchantOrderDetail) => { |
| | |
| | | onLoad((options: any) => { |
| | | orderId.value = options.id != null ? options.id as string : '' |
| | | orderNo.value = options.orderNo != null ? options.orderNo as string : orderNo.value |
| | | if (!useDemoData() && orderNo.value.length > 0) { |
| | | loadMerchantOrderDetail(orderNo.value, (detail: MerchantOrderDetail, _traceId: string) => applyRemoteOrder(detail), (error: any) => { |
| | | uni.showToast({ title: error.message != null ? error.message : '订单详情加载失败', icon: 'none' }) |
| | | }) |
| | | if (orderNo.value.length == 0) { |
| | | uni.showToast({ title: '订单参数缺失', icon: 'none' }) |
| | | return |
| | | } |
| | | const detail = getLocalOrderDetail(orderId.value) |
| | | isStoreCompleted.value = detail.category == 'store' && detail.status == 'completed' |
| | | isCompleted.value = detail.status == 'completed' |
| | | orderNo.value = detail.orderNo |
| | | productAmount.value = detail.productAmount |
| | | talentDiscount.value = detail.talentDiscount |
| | | productDiscount.value = detail.productDiscount |
| | | paidAmount.value = detail.paidAmount |
| | | subsidyAmount.value = detail.subsidyAmount |
| | | createdAt.value = detail.createdAt |
| | | verificationAt.value = detail.verificationAt |
| | | customerName.value = detail.customerName |
| | | customerPhone.value = detail.customerPhone |
| | | canRevokeVerification.value = detail.canRevoke |
| | | if (detail.revokeReason.length > 0) revokeExpiredMessage.value = detail.revokeReason |
| | | |
| | | if (isCompleted.value) { |
| | | statusTitle.value = '已完成' |
| | | statusDesc.value = '订单已完成核销,相关权益已结算。' |
| | | statusIcon.value = '/static/icons/lucide/badge-check.svg' |
| | | } |
| | | loadMerchantOrderDetail(orderNo.value, (detail: MerchantOrderDetail, _traceId: string) => applyRemoteOrder(detail), (error: any) => { |
| | | statusTitle.value = '订单加载失败' |
| | | statusDesc.value = error.message != null ? error.message : '请返回订单列表后重试。' |
| | | uni.showToast({ title: error.message != null ? error.message : '订单详情加载失败', icon: 'none' }) |
| | | }) |
| | | }) |
| | | |
| | | const goBack = () => { |
| | |
| | | const submitRevokeVerification = () => { |
| | | if (revokeSubmitting.value) return |
| | | revokeSubmitting.value = true |
| | | if (useDemoData()) { |
| | | setTimeout(() => { |
| | | revokeSubmitting.value = false |
| | | isCompleted.value = false |
| | | canRevokeVerification.value = false |
| | | statusTitle.value = '待核销' |
| | | statusDesc.value = '核销已撤销,订单已退回待核销状态。' |
| | | statusIcon.value = '/static/icons/lucide/clock-3.svg' |
| | | verificationAt.value = '待核销' |
| | | uni.showToast({ title: '撤销成功', icon: 'success' }) |
| | | }, 300) |
| | | return |
| | | } |
| | | const idempotencyKey = 'revoke-verification-' + orderId.value + '-' + Date.now() |
| | | revokeOrderVerification(orderId.value, idempotencyKey, (result: any, traceId: string) => { |
| | | revokeSubmitting.value = false |
| | |
| | | <view class="page"> |
| | | <view class="header"> |
| | | <view class="status-safe-space"></view> |
| | | <view class="title-row"><text class="page-title">订单管理</text><view v-if="isDevelopment" class="demo-control"><text class="demo-control-text">演示数据</text><switch class="demo-switch" :checked="demoDataEnabled" color="#FF7924" @change="toggleDemoData"></switch></view></view> |
| | | <view class="title-row"><text class="page-title">订单管理</text></view> |
| | | <view class="category-tabs"> |
| | | <view class="category-tab" :class="activeCategory == 'group' ? 'active' : ''" @tap="selectCategory('group')"><text>团购</text></view> |
| | | <view class="category-tab" :class="activeCategory == 'store' ? 'active' : ''" @tap="selectCategory('store')"><text>到店消费</text></view> |
| | |
| | | |
| | | <script setup lang="uts"> |
| | | import { getOrderStatusText, getPendingStatusByCategory } from '../../config/order.uts' |
| | | import { loadDemoMerchantOrders, loadMerchantBusinessDate, loadMerchantOrders, MerchantOrder, MerchantOrderPage, MerchantOrderQuery } from '../../services/orders.uts' |
| | | import { apiEnvironment } from '../../config/api.uts' |
| | | import { loadMerchantBusinessDate, loadMerchantOrders, MerchantOrder, MerchantOrderPage, MerchantOrderQuery } from '../../services/orders.uts' |
| | | import { hasActiveSession } from '../../services/session.uts' |
| | | |
| | | const activeCategory = ref('group') |
| | |
| | | const currentPage = ref(1) |
| | | const loadMoreStep = ref(0) |
| | | const orders = ref<Array<MerchantOrder>>([]) |
| | | const isDevelopment = apiEnvironment == 'development' |
| | | const demoDataEnabled = ref(isDevelopment && uni.getStorageSync('merchant-orders-demo-data') != false) |
| | | const dateFilterApplied = ref(false) |
| | | const pageSize = 3 |
| | | let cancelOrderRequest: (() => void) | null = null |
| | |
| | | loadMoreState.value = 'loading' |
| | | } |
| | | const query = buildOrderQuery(page) |
| | | if (demoDataEnabled.value) { |
| | | setTimeout(() => { |
| | | applyOrderPage(loadDemoMerchantOrders(query), append) |
| | | }, 180) |
| | | return |
| | | } |
| | | if (!hasActiveSession()) { |
| | | showOrderError('请登录后加载真实订单', '', append) |
| | | return |
| | |
| | | } |
| | | |
| | | const reloadOrders = () => requestOrders(1, false) |
| | | |
| | | const toggleDemoData = (event: any) => { |
| | | demoDataEnabled.value = event.detail != null && event.detail.value == true |
| | | uni.setStorageSync('merchant-orders-demo-data', demoDataEnabled.value) |
| | | reloadOrders() |
| | | } |
| | | |
| | | const formatAmount = (amount: number): string => (amount / 100).toFixed(2) |
| | | const formatDateTime = (value: string): string => value.length > 0 ? value.replace('T', ' ') : '—' |
| | |
| | | } |
| | | |
| | | onLoad(() => { |
| | | if (!demoDataEnabled.value && hasActiveSession()) loadMerchantBusinessDate((value: string) => applyBusinessDate(value), () => {}) |
| | | if (hasActiveSession()) loadMerchantBusinessDate((value: string) => applyBusinessDate(value), () => {}) |
| | | reloadOrders() |
| | | }) |
| | | |
| | |
| | | </template> |
| | | |
| | | <script setup lang="uts"> |
| | | import { apiEnvironment } from '../../config/api.uts' |
| | | import { getLocalOrderDetail } from '../../config/order-demo-data.uts' |
| | | import { confirmMerchantOrderVerification, loadMerchantOrderDetail, MerchantOrderDetail } from '../../services/orders.uts' |
| | | |
| | | const orderId = ref('') |
| | |
| | | const submitting = ref(false) |
| | | const loadState = ref('loading') |
| | | const loadError = ref('订单详情加载失败') |
| | | const useDemoData = (): boolean => apiEnvironment == 'development' && uni.getStorageSync('merchant-orders-demo-data') != false |
| | | |
| | | const formatAmount = (amount: number): string => (amount / 100).toFixed(2) |
| | | const formatDateTime = (value: string): string => value.length > 0 ? value.replace('T', ' ') : '—' |
| | |
| | | |
| | | const loadOrder = () => { |
| | | loadState.value = 'loading' |
| | | if (useDemoData()) { |
| | | const detail = getLocalOrderDetail(orderId.value) |
| | | applyOrder({ id: orderId.value, orderNo: detail.orderNo, category: detail.category, status: detail.status, statusText: detail.status == 'completed' ? '已完成' : '待核销', title: '特色酸菜鱼', customerName: detail.customerName, customerMobile: detail.customerPhone, cardLevel: '卓越人才卡', orderAmount: Number(detail.productAmount.replace(/[^0-9.]/g, '')) * 100, payAmount: Number(detail.paidAmount.replace(/[^0-9.]/g, '')) * 100, subsidyAmount: Number(detail.subsidyAmount.replace(/[^0-9.]/g, '')) * 100, createdAt: detail.createdAt, verifyTime: detail.status == 'completed' ? detail.verificationAt : '', benefitName: '人才专属权益', merchantName: '', canRevoke: false, revokeDeadline: '', revokeReason: '' }) |
| | | return |
| | | } |
| | | if (orderNo.value.length == 0) { loadError.value = '订单参数缺失'; loadState.value = 'error'; return } |
| | | loadMerchantOrderDetail(orderNo.value, (order: MerchantOrderDetail, _traceId: string) => applyOrder(order), (error: any) => { loadError.value = error.message != null ? error.message : '订单详情加载失败'; loadState.value = 'error' }) |
| | | } |
| | |
| | | const selectedId = ref('') |
| | | const error = ref('') |
| | | const submitting = ref(false) |
| | | const requestKey = ref('') |
| | | const availableText = computed(() => (available.value / 100).toFixed(2)) |
| | | const selected = computed(() => cards.value.find((item) => item.id == selectedId.value) || null) |
| | | const goBack = () => uni.navigateBack() |
| | | const clearError = () => { error.value = '' } |
| | | const chooseBank = () => uni.navigateTo({ url: '/pages/bank-card-select/bank-card-select?selected=' + encodeURIComponent(selectedId.value) }) |
| | | const clearError = () => { error.value = ''; requestKey.value = '' } |
| | | const chooseBank = () => { requestKey.value = ''; uni.navigateTo({ url: '/pages/bank-card-select/bank-card-select?selected=' + encodeURIComponent(selectedId.value) }) } |
| | | const load = () => { |
| | | loadWithdrawalSummary((data: any) => { available.value = Number(data.availableAmountCents || 0) }, () => { error.value = '可提金额加载失败,请稍后重试' }) |
| | | loadBankCards((items: Array<SettlementBankCard>) => { cards.value = items }, () => { error.value = '银行卡加载失败,请稍后重试' }) |
| | |
| | | uni.showModal({ title: '确认提现', content: '确认提现 ¥' + (cents / 100).toFixed(2) + ' 至' + selectedCard.bankName + '吗?', confirmText: '确认提现', confirmColor: '#FF6B2C', success: (result: any) => { |
| | | if (!result.confirm) return |
| | | submitting.value = true |
| | | createWithdrawal(selectedCard.id, cents, 'withdrawal-' + Date.now(), () => { submitting.value = false; uni.redirectTo({ url: '/pages/withdrawal-success/withdrawal-success' }) }, (requestError: any) => { submitting.value = false; error.value = requestError.message || '提现提交失败,请重试' }) |
| | | if (requestKey.value.length == 0) requestKey.value = 'withdrawal-' + selectedCard.id + '-' + cents + '-' + Date.now() |
| | | createWithdrawal(selectedCard.id, cents, requestKey.value, (created: any) => { submitting.value = false; requestKey.value = ''; uni.redirectTo({ url: '/pages/withdrawal-success/withdrawal-success?withdrawalNo=' + encodeURIComponent(created.withdrawalNo) }) }, (requestError: any) => { submitting.value = false; error.value = requestError.message || '提现提交失败,请重试' }) |
| | | } }) |
| | | } |
| | | onShow(() => load()) |
| | |
| | | <template> |
| | | <view class="page"><view class="nav"><view @tap="goBack">返回</view><text>商户提现</text><view></view></view><view class="balance"><text>当前可提</text><text class="amount">¥{{amount}}</text><view class="button" @tap="startWithdrawal">提现</view></view><view class="heading"><text>提现记录</text><text>共 {{records.length}} 条</text></view><scroll-view class="scroll" scroll-y="true"><view v-if="loading" class="state">正在加载</view><view v-else-if="records.length == 0" class="state">暂无提现记录</view><view v-for="item in records" :key="item.id" class="record"><view><text>提现</text><text class="money">¥{{format(item.amountCents)}}</text></view><view class="record-bottom"><text>{{formatTime(item.createdAt)}} {{item.cardMasked}}</text><text :class="'status ' + item.status">{{statusText(item.status)}}</text></view><text v-if="item.reason.length > 0" class="reason">{{item.reason}}</text></view></scroll-view></view> |
| | | <view class="page"><view class="nav"><view @tap="goBack">返回</view><text>商户提现</text><view></view></view><view class="balance"><text>当前可提</text><text class="amount">¥{{amount}}</text><view class="button" @tap="startWithdrawal">提现</view></view><view class="heading"><text>提现记录</text><text>共 {{records.length}} 条</text></view><scroll-view class="scroll" scroll-y="true"><view v-if="loading" class="state">正在加载</view><view v-else-if="records.length == 0" class="state">暂无提现记录</view><view v-for="item in records" :key="item.id" class="record"><view><text>提现</text><text class="money">¥{{format(item.amountCents)}}</text></view><view class="record-bottom"><text>{{displayTime(item)}} {{item.cardMasked}}</text><text :class="'status ' + item.status">{{statusText(item.status)}}</text></view><text v-if="item.reason.length > 0" class="reason">{{item.reason}}</text></view></scroll-view></view> |
| | | </template> |
| | | <script setup lang="uts"> |
| | | import { loadOnboardingStatus, loadWithdrawalSummary, loadWithdrawals, SettlementWithdrawal } from '../../services/settlement.uts' |
| | | const loading = ref(true); const available = ref(0); const onboardingStatus = ref('unsubmitted'); const records = ref<Array<SettlementWithdrawal>>([]) |
| | | const amount = computed(() => (available.value / 100).toFixed(2)); const format = (cents: number): string => (cents / 100).toFixed(2); const formatTime = (value: string): string => value.replace('T', ' ') |
| | | const statusText = (value: string): string => value == 'paid' ? '已到账' : value == 'rejected' ? '已驳回' : value == 'failed' ? '提现失败' : '审核中' |
| | | const statusText = (value: string): string => value == 'paid' ? '已到账' : value == 'processing' ? '到账处理中' : value == 'rejected' ? '已驳回' : value == 'failed' ? '提现失败' : '待审核' |
| | | const displayTime = (item: SettlementWithdrawal): string => item.status == 'paid' && item.paidAt.length > 0 ? '到账 ' + formatTime(item.paidAt) : formatTime(item.createdAt) |
| | | const goBack = () => uni.navigateBack() |
| | | const startWithdrawal = () => { if (onboardingStatus.value == 'approved') { uni.navigateTo({ url: '/pages/withdrawal-detail/withdrawal-detail' }); return }; if (onboardingStatus.value == 'pending') { uni.navigateTo({ url: '/pages/merchant-onboarding-pending/merchant-onboarding-pending' }); return }; uni.navigateTo({ url: '/pages/merchant-onboarding/merchant-onboarding' }) } |
| | | const load = () => { loading.value = true; loadOnboardingStatus((item) => { onboardingStatus.value = item.status }, () => {}); loadWithdrawalSummary((data: any) => { available.value = Number(data.availableAmountCents || 0) }, () => {}); loadWithdrawals(1, 50, (data) => { records.value = data.records; loading.value = false }, (error: any) => { loading.value = false; uni.showToast({ title: error.message || '提现记录加载失败', icon: 'none' }) }) } |
| | |
| | | |
| | | export type SettlementOnboarding = { id: string, applicationNo: string, status: string, reason: string, submittedAt: string } |
| | | export type SettlementBankCard = { id: string, bankName: string, branchName: string, cardMasked: string, status: string, reason: string, createdAt: string } |
| | | export type SettlementWithdrawal = { id: string, withdrawalNo: string, amountCents: number, status: string, reason: string, bankName: string, cardMasked: string, createdAt: string } |
| | | export type SettlementWithdrawal = { id: string, withdrawalNo: string, amountCents: number, status: string, reason: string, bankName: string, cardMasked: string, createdAt: string, reviewedAt: string, paidAt: string } |
| | | export type SettlementPage<T> = { records: Array<T>, page: number, size: number, total: number } |
| | | |
| | | const stringValue = (value: any): string => value == null ? '' : '' + value |
| | | const numberValue = (value: any): number => value == null ? 0 : Number(value) |
| | | const onboarding = (value: any): SettlementOnboarding => ({ id: stringValue(value != null ? value.id : ''), applicationNo: stringValue(value != null ? value.applicationNo : ''), status: stringValue(value != null ? value.status : 'unsubmitted'), reason: stringValue(value != null ? value.reason : ''), submittedAt: stringValue(value != null ? value.submittedAt : '') }) |
| | | const bankCard = (value: any): SettlementBankCard => ({ id: stringValue(value != null ? value.id : ''), bankName: stringValue(value != null ? value.bankName : ''), branchName: stringValue(value != null ? value.branchName : ''), cardMasked: stringValue(value != null ? value.cardMasked : ''), status: stringValue(value != null ? value.status : 'pending'), reason: stringValue(value != null ? value.reason : ''), createdAt: stringValue(value != null ? value.createdAt : '') }) |
| | | const withdrawal = (value: any): SettlementWithdrawal => ({ id: stringValue(value != null ? value.id : ''), withdrawalNo: stringValue(value != null ? value.withdrawalNo : ''), amountCents: numberValue(value != null ? value.amountCents : 0), status: stringValue(value != null ? value.status : 'pending'), reason: stringValue(value != null ? value.reason : ''), bankName: stringValue(value != null ? value.bankName : ''), cardMasked: stringValue(value != null ? value.cardMasked : ''), createdAt: stringValue(value != null ? value.createdAt : '') }) |
| | | const withdrawal = (value: any): SettlementWithdrawal => ({ id: stringValue(value != null ? value.id : ''), withdrawalNo: stringValue(value != null ? value.withdrawalNo : ''), amountCents: numberValue(value != null ? value.amountCents : 0), status: stringValue(value != null ? value.status : 'pending'), reason: stringValue(value != null ? value.reason : ''), bankName: stringValue(value != null ? value.bankName : ''), cardMasked: stringValue(value != null ? value.cardMasked : ''), createdAt: stringValue(value != null ? value.createdAt : ''), reviewedAt: stringValue(value != null ? value.reviewedAt : ''), paidAt: stringValue(value != null ? value.paidAt : '') }) |
| | | |
| | | export const loadOnboardingStatus = (success: (value: SettlementOnboarding) => void, error: (value: HttpError) => void): (() => void) => request<any>({ path: getApiEndpoint('merchantOnboardingStatus') }, (data: any) => success(onboarding(data)), error) |
| | | export const submitOnboarding = (payload: any, key: string, success: (value: SettlementOnboarding) => void, error: (value: HttpError) => void): (() => void) => request<any>({ path: getApiEndpoint('merchantOnboarding'), method: 'POST', data: payload, idempotencyKey: key }, (data: any) => success(onboarding(data)), error) |
| | |
| | | .merchant-coupon-edit-form .el-select__wrapper, |
| | | .merchant-coupon-edit-form .el-range-editor.el-input__wrapper { |
| | | background: #ffffff; |
| | | box-shadow: 0 0 0 1px #d9dee8 inset; |
| | | } |
| | | |
| | | .merchant-coupons-filter .el-input__wrapper:hover, |
| | | .merchant-coupons-filter .el-input__wrapper.is-focus, |
| | | .merchant-coupons-filter .el-select__wrapper:hover, |
| | | .merchant-coupons-filter .el-select__wrapper.is-focused, |
| | | .merchant-coupon-edit-form .el-input__wrapper:hover, |
| | | .merchant-coupon-edit-form .el-input__wrapper.is-focus, |
| | | .merchant-coupon-edit-form .el-select__wrapper:hover, |
| | | .merchant-coupon-edit-form .el-select__wrapper.is-focused, |
| | | .merchant-coupon-edit-form .el-date-editor.el-input__wrapper:hover, |
| | | .merchant-coupon-edit-form .el-date-editor.el-input__wrapper.is-focus { |
| | | box-shadow: 0 0 0 1px #b8c2d1 inset; |
| | | } |
| | | |
| | | .merchant-coupons-filter .coupon-created-time-picker { |
| | |
| | | width: 200px; |
| | | } |
| | | |
| | | .coupon-amount-control { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 8px; |
| | | } |
| | | |
| | | .coupon-amount-control .el-input-number { |
| | | width: 200px; |
| | | } |
| | | |
| | | .coupon-amount-control > span { |
| | | color: #334155; |
| | | line-height: 28px; |
| | | white-space: nowrap; |
| | | } |
| | | |
| | | .merchant-coupon-edit-form .coupon-valid-time-picker { |
| | | width: 300px; |
| | | } |
| | |
| | | ElMessage.warning('请输入优惠券名称') |
| | | return |
| | | } |
| | | const couponAmount = Number(couponEditForm.couponAmount) |
| | | if (couponEditForm.couponAmount === '' || !Number.isFinite(couponAmount) || couponAmount <= 0) { |
| | | ElMessage.warning('请输入大于0的优惠券金额') |
| | | return |
| | | } |
| | | if (couponEditForm.useRequirement === 'full' && couponEditForm.fullAmount === '') { |
| | | ElMessage.warning('请输入使用门槛') |
| | | return |
| | |
| | | ElMessage.warning('请输入排序') |
| | | return |
| | | } |
| | | if (requiresMerchantOperator.value) { |
| | | const updatingMerchantPassword = isEditMode.value && accountForm.loginPassword.length > 0 |
| | | if (updatingMerchantPassword) { |
| | | if (accountForm.loginPassword.length < 6 || accountForm.loginPassword.length > 64) { |
| | | ElMessage.warning('登录密码长度须为6至64位') |
| | | return |
| | | } |
| | | if (!accountForm.confirmPassword) { |
| | | ElMessage.warning('请输入确认密码') |
| | | return |
| | | } |
| | | } |
| | | if (requiresMerchantOperator.value) { |
| | | if (!accountForm.loginAccount.trim()) { |
| | | ElMessage.warning('请输入登录账号') |
| | | return |
| | |
| | | return |
| | | } |
| | | } |
| | | if (accountForm.loginPassword !== accountForm.confirmPassword) { |
| | | if ((requiresMerchantOperator.value || updatingMerchantPassword) && accountForm.loginPassword !== accountForm.confirmPassword) { |
| | | ElMessage.warning('两次输入的密码不一致') |
| | | return |
| | | } |
| | |
| | | latitude: Number(form.latitude || 0) || undefined, |
| | | canLoginMerchantApp: requiresMerchantOperator.value ? 1 : Number(form.canLoginMerchantApp ?? 1), |
| | | loginAccount: !isEditMode.value ? accountForm.loginAccount : undefined, |
| | | loginPassword: !isEditMode.value ? accountForm.loginPassword : undefined, |
| | | loginPassword: !isEditMode.value || updatingMerchantPassword ? accountForm.loginPassword : undefined, |
| | | groupBuyingEnabled: form.groupBuyingEnabled, |
| | | usageInstructions: form.usageInstructions, |
| | | rightsText: form.rightsText, |
| | |
| | | <el-switch v-model="form.canLoginMerchantApp" :active-value="1" :inactive-value="0" /> |
| | | </el-form-item> |
| | | <el-form-item label="登录账号" :error="accountTaken ? '登录账号已存在,请更换' : ''"> |
| | | <el-input v-model="accountForm.loginAccount" maxlength="32" :readonly="isEditMode" :placeholder="isEditMode ? '未配置登录账号' : '仅支持英文和数字'" @input="normalizeLoginAccount" @blur="checkLoginAccountAvailable" /> |
| | | <el-input v-model="accountForm.loginAccount" maxlength="32" :disabled="isEditMode" :placeholder="isEditMode ? '未配置登录账号' : '仅支持英文和数字'" @input="normalizeLoginAccount" @blur="checkLoginAccountAvailable" /> |
| | | </el-form-item> |
| | | <el-form-item v-if="!isEditMode" label="登录密码"> |
| | | <el-input v-model="accountForm.loginPassword" type="password" show-password /> |
| | | <el-form-item label="登录密码"> |
| | | <el-input v-model="accountForm.loginPassword" type="password" show-password :placeholder="isEditMode ? '留空则不修改,6至64位' : ''" /> |
| | | </el-form-item> |
| | | <el-form-item v-if="!isEditMode" label="确认密码"> |
| | | <el-input v-model="accountForm.confirmPassword" type="password" show-password /> |
| | | <el-form-item label="确认密码"> |
| | | <el-input v-model="accountForm.confirmPassword" type="password" show-password :placeholder="isEditMode ? '再次输入新密码' : ''" /> |
| | | </el-form-item> |
| | | </el-form> |
| | | </section> |
| | |
| | | <el-input v-model="couponEditForm.couponName" placeholder="请输入优惠券名称" /> |
| | | </div> |
| | | |
| | | <div class="coupon-edit-row coupon-amount-row"> |
| | | <label>优惠券金额</label> |
| | | <div class="coupon-amount-control"><el-input-number v-model="couponEditForm.couponAmount" :min="0.01" :precision="2" :step="1" controls-position="right" placeholder="请输入优惠券金额" /><span>元</span></div> |
| | | </div> |
| | | |
| | | <div class="coupon-edit-row coupon-requirement-row"> |
| | | <label>使用要求</label> |
| | | <el-radio-group v-model="couponEditForm.useRequirement"> |
| | |
| | | <section class="table-panel"> |
| | | <el-table v-loading="loading" :data="rows" border class="settlement-table" empty-text="暂无数据"> |
| | | <template v-if="kind === 'withdrawals'"> |
| | | <el-table-column prop="withdrawalNo" label="提现Id" min-width="156" /> |
| | | <el-table-column prop="withdrawalNo" label="提现id" min-width="156" /> |
| | | <el-table-column prop="merchantName" label="商家名称" min-width="130" show-overflow-tooltip /> |
| | | <el-table-column prop="contactMobile" label="联系方式" min-width="118" /> |
| | | <el-table-column prop="merchantType" label="商家类型" min-width="110" /> |
| | |
| | | <el-table-column prop="cardMasked" label="提现卡号" min-width="150" /> |
| | | </template> |
| | | <template v-else-if="kind === 'onboardings'"> |
| | | <el-table-column prop="applicationNo" label="进件申请Id" min-width="156" /> |
| | | <el-table-column prop="applicationNo" label="进件id" min-width="156" /> |
| | | <el-table-column prop="merchantName" label="商家名称" min-width="150" show-overflow-tooltip /> |
| | | <el-table-column prop="contactMobile" label="联系方式" min-width="118" /> |
| | | <el-table-column prop="merchantType" label="商家类型" min-width="110" /> |
| | |
| | | <el-table-column prop="subjectType" label="主体类型" min-width="105" /> |
| | | </template> |
| | | <template v-else> |
| | | <el-table-column prop="id" label="申请Id" min-width="100" /> |
| | | <el-table-column prop="id" label="申请id" min-width="100" /> |
| | | <el-table-column prop="merchantName" label="商家名称" min-width="150" show-overflow-tooltip /> |
| | | <el-table-column prop="contactMobile" label="联系方式" min-width="118" /> |
| | | <el-table-column prop="merchantType" label="商家类型" min-width="110" /> |
| | |
| | | ensureUpdated(updated); |
| | | Long storeId = findOrCreateDefaultStore(id, request.merchantName(), request.address()); |
| | | syncMerchantBusinessHours(id, storeId, request.businessHours()); |
| | | if (request.loginPassword() != null && !request.loginPassword().isBlank()) { |
| | | if (request.loginPassword().length() < 6 || request.loginPassword().length() > 64) { |
| | | throw new BusinessException(ErrorCode.BAD_REQUEST, "商户登录密码长度须为6至64位"); |
| | | } |
| | | int passwordUpdated = jdbcTemplate.update(""" |
| | | update merchant_operator |
| | | set password_hash = ? |
| | | where merchant_id = ? and role_code = 'owner' and deleted = 0 |
| | | """, MerchantPasswordHasher.encode(request.loginPassword()), id); |
| | | if (passwordUpdated == 0) throw new BusinessException(ErrorCode.CONFLICT, "该商家未配置商户端登录账号"); |
| | | } |
| | | return merchant(id); |
| | | } |
| | | |
| | |
| | | import java.util.List; |
| | | |
| | | import jakarta.validation.constraints.NotBlank; |
| | | import jakarta.validation.constraints.DecimalMin; |
| | | import jakarta.validation.constraints.NotNull; |
| | | import jakarta.validation.constraints.Size; |
| | | |
| | |
| | | @NotBlank |
| | | String conditionType, |
| | | BigDecimal conditionAmount, |
| | | @NotNull |
| | | @DecimalMin(value = "0.01") |
| | | BigDecimal couponAmount, |
| | | List<String> talentLevels, |
| | | String validStart, |
| | |
| | | import java.util.List; |
| | | |
| | | import jakarta.validation.constraints.NotBlank; |
| | | import jakarta.validation.constraints.DecimalMin; |
| | | import jakarta.validation.constraints.NotNull; |
| | | import jakarta.validation.constraints.Size; |
| | | |
| | |
| | | @NotBlank |
| | | String conditionType, |
| | | BigDecimal conditionAmount, |
| | | @NotNull |
| | | @DecimalMin(value = "0.01") |
| | | BigDecimal couponAmount, |
| | | List<String> talentLevels, |
| | | String validStart, |
| | |
| | | @Size(max = 64) |
| | | String serviceLabel, |
| | | List<@Valid MerchantBusinessHourRequest> businessHours, |
| | | Byte canLoginMerchantApp |
| | | Byte canLoginMerchantApp, |
| | | @Size(max = 64) String loginPassword |
| | | ) { |
| | | } |
| | |
| | | package com.talentcard.server.business.merchant; |
| | | |
| | | import com.talentcard.server.business.DatabaseRows; |
| | | import com.alibaba.fastjson.JSON; |
| | | import com.talentcard.server.business.payment.huifu.HuifuPaymentGatewayException; |
| | | import com.talentcard.server.business.payment.huifu.HuifuWithdrawalCommand; |
| | | import com.talentcard.server.business.payment.huifu.HuifuWithdrawalGateway; |
| | |
| | | private long number(Object v) { return v instanceof Number n ? n.longValue() : 0; } |
| | | private record PreparedWithdrawal(String withdrawalNo, long amountCents, String huifuId, String tokenNo) {} |
| | | @SuppressWarnings("unchecked") private record ProviderResult(boolean success, boolean failed, String code, String message, String providerNo) { |
| | | static ProviderResult from(Map<String, Object> response) { Map<String,Object> d=response.get("data") instanceof Map<?,?> map?(Map<String,Object>)map:response; String code=value(d,"sub_resp_code","resp_code","code"), status=value(d,"trans_status","trans_stat"), message=value(d,"sub_resp_desc","resp_desc","message"), no=value(d,"hf_seq_id"); boolean accepted="00000000".equals(code)||"10000".equals(code)||"0".equals(code); boolean ok=accepted&&("S".equalsIgnoreCase(status)||"SUCCESS".equalsIgnoreCase(status)); boolean pending="P".equalsIgnoreCase(status)||"PROCESSING".equalsIgnoreCase(status)||status.isBlank(); return new ProviderResult(ok,!ok&&!pending,code,message,no); } |
| | | static ProviderResult from(Map<String, Object> response) { |
| | | Object raw = response.get("data"); |
| | | Map<String, Object> d = raw instanceof Map<?, ?> map ? (Map<String, Object>) map |
| | | : raw instanceof String json && !json.isBlank() ? JSON.parseObject(json, Map.class) : response; |
| | | String code = value(d, "sub_resp_code", "resp_code", "code"); |
| | | String status = value(d, "trans_stat", "trans_status"); |
| | | String message = value(d, "sub_resp_desc", "resp_desc", "message"); |
| | | String no = value(d, "hf_seq_id", "bank_order_no"); |
| | | boolean accepted = "00000000".equals(code) || "10000".equals(code) || "0".equals(code) || "SUCCESS".equalsIgnoreCase(code); |
| | | boolean ok = accepted && ("S".equalsIgnoreCase(status) || "SUCCESS".equalsIgnoreCase(status) || "PAID".equalsIgnoreCase(status)); |
| | | boolean pending = "P".equalsIgnoreCase(status) || "PROCESSING".equalsIgnoreCase(status) || "PENDING".equalsIgnoreCase(status) || status.isBlank(); |
| | | return new ProviderResult(ok, !ok && !pending, code, message, no); |
| | | } |
| | | static String value(Map<String,Object> data,String...keys){for(String key:keys)if(data.get(key)!=null)return String.valueOf(data.get(key)).trim();return "";} |
| | | } |
| | | } |
| | |
| | | private void store(MerchantPrincipal principal) { if (rows("select id from merchant_store where id = ? and merchant_id = ? and status = 1 and deleted = 0", principal.storeId(), principal.merchantId()).isEmpty()) throw new BusinessException(ErrorCode.FORBIDDEN, "当前门店不可操作"); } |
| | | private Map<String, Object> onboarding(Map<String, Object> row) { return Map.of("id", String.valueOf(row.get("id")), "applicationNo", row.get("applicationNo"), "status", row.get("status"), "reason", clean(row.get("rejectReason")), "submittedAt", String.valueOf(row.get("createdAt"))); } |
| | | private Map<String, Object> bankCard(Map<String, Object> row) { return Map.of("id", String.valueOf(row.get("id")), "bankName", row.get("bankName"), "branchName", row.get("branchName"), "cardMasked", row.get("cardMasked"), "status", row.get("status"), "reason", clean(row.get("rejectReason")), "createdAt", String.valueOf(row.get("createdAt"))); } |
| | | private Map<String, Object> withdrawal(Map<String, Object> row) { Map<String, Object> value = new LinkedHashMap<>(); value.put("id", String.valueOf(row.get("id"))); value.put("withdrawalNo", row.get("withdrawalNo")); value.put("amountCents", row.get("amountCents")); value.put("status", row.get("status")); value.put("reason", clean(row.get("failureReason"))); value.put("bankName", clean(row.get("bankName"))); value.put("cardMasked", clean(row.get("cardMasked"))); value.put("createdAt", String.valueOf(row.get("createdAt"))); return value; } |
| | | private Map<String, Object> withdrawal(Map<String, Object> row) { Map<String, Object> value = new LinkedHashMap<>(); value.put("id", String.valueOf(row.get("id"))); value.put("withdrawalNo", row.get("withdrawalNo")); value.put("amountCents", row.get("amountCents")); value.put("status", row.get("status")); value.put("reason", clean(row.get("failureReason"))); value.put("bankName", clean(row.get("bankName"))); value.put("cardMasked", clean(row.get("cardMasked"))); value.put("createdAt", String.valueOf(row.get("createdAt"))); value.put("reviewedAt", clean(row.get("reviewedAt"))); value.put("paidAt", clean(row.get("paidAt"))); return value; } |
| | | private Map<String, Object> branch(String id, String branchName, String bankName) { return Map.of("id", id, "branchName", branchName, "bankName", bankName); } |
| | | private List<Map<String, Object>> rows(String sql, Object... args) { return DatabaseRows.normalize(jdbc.queryForList(sql, args)); } |
| | | private Long number(String value) { try { return Long.valueOf(value); } catch (Exception ignored) { throw new BusinessException(ErrorCode.BAD_REQUEST, "编号无效"); } } |
| | |
| | | payment: |
| | | huifu: |
| | | # Keep disabled until the Huifu test/production product, merchant and keys are provisioned. |
| | | enabled: ${HUIFU_ENABLED:false} |
| | | environment: ${HUIFU_ENVIRONMENT:test} |
| | | system-id: ${HUIFU_SYSTEM_ID:} |
| | | product-id: ${HUIFU_PRODUCT_ID:} |
| | | merchant-id: ${HUIFU_MERCHANT_ID:} |
| | | mini-program-app-id: ${HUIFU_MINI_PROGRAM_APP_ID:} |
| | | private-key: ${HUIFU_PRIVATE_KEY:} |
| | | platform-public-key: ${HUIFU_PLATFORM_PUBLIC_KEY:} |
| | | notify-url: ${HUIFU_NOTIFY_URL:} |
| | | withdrawal-product-id: ${HUIFU_WITHDRAWAL_PRODUCT_ID:} |
| | | withdrawal-notify-url: ${HUIFU_WITHDRAWAL_NOTIFY_URL:} |
| | | withdrawal-into-account-date-type: ${HUIFU_WITHDRAWAL_INTO_ACCOUNT_DATE_TYPE:D0} |
| | | enabled: true |
| | | environment: prod |
| | | system-id: 6666000212191071 |
| | | product-id: KAZX |
| | | merchant-id: 6666000212191071 |
| | | mini-program-app-id: wx6ae2cadfea28fce1 |
| | | private-key: MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCcsR3R3nUFCv/4l6vczGk6j4UgZhuMm/fTs2MQQqFkIuoISd38TOiIMaLS3KLYL+D+qhUCps56svOK/3M/kdyXfPsBphJyXB5Ar7il8pJ6BDa38StzTrlRm+IXX+4diJvqxUimetjpx0OfnqRkNYQWc/ZVeoEdQbITqLyxogy0R3rkR/wnOTwxHBbkCYsDCxSon4W0SoM5fLs3o947INbPUbUsf0vL5l6NiqlV9YsZ3qAWvvPoPX4HI1s5maMiVKfUwe27P3ARW9n6n3tadBzbB6lrmAEcFoihXpZjNN9UaRKjYfYjTS9JRkDqRyYGhz5cHp9GthEksMvhLP84oEanAgMBAAECggEAKV6d5+Ya8TzpPiIVTxNtCPgRhu9OgL5BHVS0Z/ufjselEJEoGc/YaPTp71AvcnKZrcMMDwopY73RjTZKoFMLcbeesd296xGlLyD12GKGB88K/1+eQDBdv00v0UoTnRqPR9nUh5fRo09n7EG/V3HFsQPm2sSgSDArKwOW5jNJtWLsGIVCW8gg3RQ9YlZ5HW85neBnjBFnrMA9S/hxwZD336M8CiViIa0v9o5CS7qxAqXMzzoOC7Vr3hZ/X0i/5GrSH9DRO7ougSmP4F/zCEc2dViA0rOJ6m3OuS0osh5FulihOBOjI5uLETEt4K/7PuV9xOd4oAz0OBpJ2bhPf3YpGQKBgQDQdEcmnlULa3jTSrbr0vYA3iMpcls/8F8+I/QymMXHAUeVyc8XKuGzblx+/n3x8ZoAvdVg1ZAO5wgLh/P0BEL74iWhfHedjt8xcdk2R5U0RuOnx1TUTKrmLKRZ5gStNFnGlK4yVwpiNqHn/cr+gYYeZSPARFVi00WdXbqVbEEf2wKBgQDAbmo4uHrmHN5GYvt8lVmT9aha1IpKOOLenjd+Y/vKGJcvHzOVCHnOh8OGO/4p7CxOq0sshER713jW2cFGO3xpFEFpIc45pDQ8Ut0XuSoOjKtwPzid8vKX7A1If6FDLMNy7bYR1qqnIJNJnN/f3lsksX+es7QTuRShhA0X0mnEJQKBgBE5OlPHi1FZPO+AYeLUWH0z3YXkLAP5cXUp9Vhk5RqPUYHf3duVMHbp7HyVvIRMKSRciw/oNXH1xuVlA1QrMfmr8t4H8b4z+t8UABWLDpn2LYNEZg7wksJX9/Bxn/tLswtEZ6xDItmAmBoaDza+Lv2975lITCHRQRRClVMZUbw3AoGAVFrSs6SjM2aqRZ60ccuipgrFT3PmX10Kjt46o2w+/zvCYoTB/u3lP5UiMCDybPw7/Ax4+r3Mq6EsNrrkT95ZzlJzzhtkpL4Hq/4DL5gbCJ+/s4fzUMIkeFahCAvy7FieIS51ZuJ8kqDkEBv1w9RcjfxFXpOWOCMlGRyjBLNXbAUCgYEAjcTEuXctlGEjtO1MBaHc4hkkpiPG7gxV/Bq1XXLszv4LTaOfDEpjGGBpVYtb3URYrif/HzX8T1KHELvPziuqP2ghaJQzbztep1TGQ48A5VpOvHdA/w/+k4JJl0PyKBPrFXiHIw1if7rnvOLt27TymwXSaJquSz7Sz2+wXFxzKJk= |
| | | platform-public-key: ${HUIFU_PLATFORM_PUBLIC_KEY:MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAgxOuXt1UEdZNPk6sntqiIlcs7e+0fZ3CVgKMV2Qv4ixwKmha5etL5WyIONmjoGEHePHpanYkXvm0kHkbbDkjpyHDFdEshuP1eGx5tRUTISRvXY/iFuD3xyeWSsvvjW6hS29OvqVCPcnBUC7jcRj15dJS2IpYR0iDGNS+/M0TyOFwRQME07At+qO+MSjKu4aRIjlglukjD7YbE2s35uV066nsBUCZUNpGPuju1pieUW4zyoIW6K70dyXCQy61oRI6JmA4JRCOJV1BE80N3GrLI4Pbq1C3ZBTgOPi5RWYNq1hfu807BDkPLc3dJKYt22Ea1jPJsg6e/B9Xbzh6JaUNAQIDAQAB} |
| | | notify-url: ${HUIFU_NOTIFY_URL:http://221.182.45.100:11451/api/payments/huifu/notify} |
| | | withdrawal-product-id: ${HUIFU_WITHDRAWAL_PRODUCT_ID:KAZX} |
| | | withdrawal-notify-url: ${HUIFU_WITHDRAWAL_NOTIFY_URL:http://221.182.45.100:11451/api/payments/huifu/withdrawal-notify} |
| | | delay-account: ${HUIFU_DELAY_ACCOUNT:false} |
| | | reconciliation: |
| | | enabled: ${HUIFU_RECONCILIATION_ENABLED:false} |
| | | fixed-delay-ms: ${HUIFU_RECONCILIATION_FIXED_DELAY_MS:300000} |
| | | pending-after-minutes: ${HUIFU_RECONCILIATION_PENDING_AFTER_MINUTES:5} |
| | | wechat: |
| | | mini-program: |
| | | # The app id must be the same mini-program that invokes uni.login and uni.requestPayment. |
| | | app-id: ${WECHAT_MINI_PROGRAM_APP_ID:} |
| | | app-id: ${WECHAT_MINI_PROGRAM_APP_ID:wx6ae2cadfea28fce1} |
| | | # Keep this only in the deployment environment. It is required to exchange a wx.login code for an OpenID. |
| | | app-secret: ${WECHAT_MINI_PROGRAM_APP_SECRET:} |
| | | app-secret: ${WECHAT_MINI_PROGRAM_APP_SECRET:7bf743b066523bbb2d6de80decabc15b} |
| | | |
| | | management: |
| | | endpoints: |