shinnosuke
2026-08-04 c8d12c37b3f78e41aafef1ccb3e380573308c34a
feat(admin): 添加商户登录密码修改功能并完善优惠券金额验证

- 在AdminBusinessService中添加商户登录密码更新逻辑,包括长度验证和哈希存储
- 在MerchantUpdateRequest中添加loginPassword字段用于接收新密码
- 在CouponCreateRequest和CouponUpdateRequest中添加金额最小值验证注解
- 更新前端表单验证逻辑以检查优惠券金额的有效性
- 修改商户创建和编辑页面的密码相关表单项显示逻辑
- 集成汇付支付网关响应解析优化,支持更多状态码和字段映射
- 移除多个页面中的演示数据开关和相关逻辑代码
- 更新应用配置文件中的汇付支付参数为生产环境配置
- 修复订单详情页面的数据加载逻辑,移除演示模式
- 更新结算服务接口定义以包含审核和支付时间字段
- 优化CSS样式以改善优惠券管理界面的视觉效果
19个文件已修改
287 ■■■■■ 已修改文件
rencaika_merchant/config/api.uts 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rencaika_merchant/manifest.json 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rencaika_merchant/pages/merchant/merchant.uvue 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rencaika_merchant/pages/order-detail/order-detail.uvue 72 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rencaika_merchant/pages/orders/orders.uvue 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rencaika_merchant/pages/verification-detail/verification-detail.uvue 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rencaika_merchant/pages/withdrawal-detail/withdrawal-detail.uvue 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rencaika_merchant/pages/withdrawal/withdrawal.uvue 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rencaika_merchant/services/settlement.uts 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-admin/src/style.css 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-admin/src/views/MerchantCreateView.vue 37 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-admin/src/views/MerchantSettlementView.vue 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-server/src/main/java/com/talentcard/server/business/admin/AdminBusinessService.java 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-server/src/main/java/com/talentcard/server/business/admin/CouponCreateRequest.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-server/src/main/java/com/talentcard/server/business/admin/CouponUpdateRequest.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-server/src/main/java/com/talentcard/server/business/admin/MerchantUpdateRequest.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-server/src/main/java/com/talentcard/server/business/merchant/HuifuMerchantWithdrawalService.java 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-server/src/main/java/com/talentcard/server/business/merchant/MerchantSettlementService.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-server/src/main/resources/application.yml 31 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rencaika_merchant/config/api.uts
@@ -9,13 +9,14 @@
// 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 = ''
rencaika_merchant/manifest.json
@@ -9,7 +9,7 @@
    "quickapp": {},
    /* 小程序特有相关 */
    "mp-weixin": {
        "appid": "wxfb87c6ef164c2922",
        "appid": "wxe91f1af7638aa5dd",
        "setting": {
            "urlCheck": false
        },
rencaika_merchant/pages/merchant/merchant.uvue
@@ -37,12 +37,10 @@
</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)
@@ -56,11 +54,6 @@
        }
        loadState.value = 'loading'
        traceId.value = ''
        if (useDemoData) {
            merchant.value = loadDemoMerchantProfile()
            loadState.value = 'success'
            return
        }
        if (!hasActiveSession()) {
            loadState.value = 'error'
            return
@@ -81,12 +74,6 @@
        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
rencaika_merchant/pages/order-detail/order-detail.uvue
@@ -37,8 +37,6 @@
<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('')
@@ -47,20 +45,19 @@
    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) => {
@@ -86,33 +83,15 @@
    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 = () => {
@@ -132,19 +111,6 @@
    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
rencaika_merchant/pages/orders/orders.uvue
@@ -2,7 +2,7 @@
    <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>
@@ -146,8 +146,7 @@
<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')
@@ -164,8 +163,6 @@
    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
@@ -262,12 +259,6 @@
            loadMoreState.value = 'loading'
        }
        const query = buildOrderQuery(page)
        if (demoDataEnabled.value) {
            setTimeout(() => {
                applyOrderPage(loadDemoMerchantOrders(query), append)
            }, 180)
            return
        }
        if (!hasActiveSession()) {
            showOrderError('请登录后加载真实订单', '', append)
            return
@@ -282,12 +273,6 @@
    }
    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', ' ') : '—'
@@ -503,7 +488,7 @@
    }
    onLoad(() => {
        if (!demoDataEnabled.value && hasActiveSession()) loadMerchantBusinessDate((value: string) => applyBusinessDate(value), () => {})
        if (hasActiveSession()) loadMerchantBusinessDate((value: string) => applyBusinessDate(value), () => {})
        reloadOrders()
    })
rencaika_merchant/pages/verification-detail/verification-detail.uvue
@@ -26,8 +26,6 @@
</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('')
@@ -46,7 +44,6 @@
    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', ' ') : '—'
@@ -70,11 +67,6 @@
    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' })
    }
rencaika_merchant/pages/withdrawal-detail/withdrawal-detail.uvue
@@ -23,11 +23,12 @@
    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 = '银行卡加载失败,请稍后重试' })
@@ -45,7 +46,8 @@
        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())
rencaika_merchant/pages/withdrawal/withdrawal.uvue
@@ -1,11 +1,12 @@
<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' }) }) }
rencaika_merchant/services/settlement.uts
@@ -3,14 +3,14 @@
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)
talent-card-admin/src/style.css
@@ -1185,6 +1185,20 @@
.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 {
@@ -1561,6 +1575,22 @@
  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;
}
talent-card-admin/src/views/MerchantCreateView.vue
@@ -959,6 +959,11 @@
    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
@@ -1016,7 +1021,18 @@
        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
@@ -1039,7 +1055,7 @@
            return
        }
    }
  if (accountForm.loginPassword !== accountForm.confirmPassword) {
    if ((requiresMerchantOperator.value || updatingMerchantPassword) && accountForm.loginPassword !== accountForm.confirmPassword) {
    ElMessage.warning('两次输入的密码不一致')
    return
  }
@@ -1058,7 +1074,7 @@
        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,
@@ -1279,13 +1295,13 @@
            <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>
@@ -1762,6 +1778,11 @@
              <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">
talent-card-admin/src/views/MerchantSettlementView.vue
@@ -197,7 +197,7 @@
    <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" />
@@ -211,7 +211,7 @@
          <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" />
@@ -219,7 +219,7 @@
          <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" />
talent-card-server/src/main/java/com/talentcard/server/business/admin/AdminBusinessService.java
@@ -459,6 +459,17 @@
        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);
    }
talent-card-server/src/main/java/com/talentcard/server/business/admin/CouponCreateRequest.java
@@ -4,6 +4,7 @@
import java.util.List;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.DecimalMin;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
@@ -16,6 +17,8 @@
        @NotBlank
        String conditionType,
        BigDecimal conditionAmount,
        @NotNull
        @DecimalMin(value = "0.01")
        BigDecimal couponAmount,
        List<String> talentLevels,
        String validStart,
talent-card-server/src/main/java/com/talentcard/server/business/admin/CouponUpdateRequest.java
@@ -4,6 +4,7 @@
import java.util.List;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.DecimalMin;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
@@ -16,6 +17,8 @@
        @NotBlank
        String conditionType,
        BigDecimal conditionAmount,
        @NotNull
        @DecimalMin(value = "0.01")
        BigDecimal couponAmount,
        List<String> talentLevels,
        String validStart,
talent-card-server/src/main/java/com/talentcard/server/business/admin/MerchantUpdateRequest.java
@@ -35,6 +35,7 @@
        @Size(max = 64)
        String serviceLabel,
        List<@Valid MerchantBusinessHourRequest> businessHours,
        Byte canLoginMerchantApp
        Byte canLoginMerchantApp,
        @Size(max = 64) String loginPassword
) {
}
talent-card-server/src/main/java/com/talentcard/server/business/merchant/HuifuMerchantWithdrawalService.java
@@ -1,6 +1,7 @@
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;
@@ -68,7 +69,19 @@
    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 "";}
    }
}
talent-card-server/src/main/java/com/talentcard/server/business/merchant/MerchantSettlementService.java
@@ -112,7 +112,7 @@
    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, "编号无效"); } }
talent-card-server/src/main/resources/application.yml
@@ -52,29 +52,24 @@
  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: