| | |
| | | <view class="page"> |
| | | <view class="navigation-bar"><view class="back-button" @tap="goBack"><image class="back-icon" src="/static/icons/lucide/back-left.svg" mode="aspectFit"></image></view><text class="page-title">商户提现</text></view> |
| | | <scroll-view class="form-scroll" scroll-y="true" :show-scrollbar="false" :enhanced="false"> |
| | | <view class="available-row"><text class="available-label">当前可提</text><text class="available-value">¥{{availableText}}</text></view> |
| | | <view class="form-card"> |
| | | <view class="available-row"><text class="available-label">当前可提</text><text class="available-value">¥{{availableText}}</text></view> |
| | | <view class="field-block"> |
| | | <view class="field-row"> |
| | | <view class="field-row amount-field-row"> |
| | | <text class="field-label">提现金额</text> |
| | | <view class="amount-input-shell" :class="error.length > 0 ? 'field-invalid' : ''"> |
| | | <text class="amount-prefix">¥</text> |
| | | <input class="amount-input" v-model="amount" type="digit" maxlength="10" placeholder="请输入" placeholder-class="input-placeholder" @input="clearError" /> |
| | | <text class="amount-prefix">¥</text> |
| | | <input class="amount-input" v-model="amount" type="digit" maxlength="10" placeholder="请输入提现金额" placeholder-class="input-placeholder" @input="clearError" /> |
| | | </view> |
| | | </view> |
| | | <text v-if="error.length > 0" class="field-error">{{error}}</text> |
| | | </view> |
| | | <view class="field-divider"></view> |
| | | <view class="field-block bank-field-block"> |
| | | <view class="field-row bank-field-row"> |
| | | <text class="field-label">提现银行卡</text> |
| | |
| | | <view class="select-bank-button" @tap="chooseBank"><text>{{selected == null ? '选择' : '更换'}}</text></view> |
| | | </view> |
| | | </view> |
| | | <view class="submit-button" :class="submitting ? 'submit-disabled' : ''" @tap="submit"><text>{{submitting ? '提交中' : '发起提现'}}</text></view> |
| | | </view> |
| | | <view class="submit-button" :class="submitting ? 'submit-disabled' : ''" @tap="submit"><text>{{submitting ? '提交中' : '发起提现'}}</text></view> |
| | | <view class="safe-space"></view> |
| | | </scroll-view> |
| | | </view> |
| | |
| | | page { background-color: #F4F6F9; }.page { display: flex; flex: 1; width: 100%; height: 100vh; flex-direction: column; overflow: hidden; background-color: #F4F6F9; }.navigation-bar { display: flex; height: 174rpx; padding: 68rpx 38rpx 0; flex: none; flex-direction: row; align-items: center; position: relative; box-sizing: border-box; }.back-button { display: flex; width: 64rpx; height: 64rpx; justify-content: center; align-items: center; position: absolute; left: 38rpx; top: 89rpx; z-index: 2; }.back-icon { width: 38rpx; height: 38rpx; transform: rotate(180deg); }.page-title { width: 100%; color: #202B3E; font-size: 34rpx; line-height: 48rpx; text-align: center; font-weight: 700; }.form-scroll { flex: 1; height: 0; padding: 0 34rpx; box-sizing: border-box; }.available-row { display: flex; height: 72rpx; padding: 0 12rpx; align-items: center; }.available-label { color: #7D8797; font-size: 25rpx; }.available-value { margin-left: 12rpx; color: #2C3748; font-size: 27rpx; font-weight: 700; }.form-card { padding: 6rpx 28rpx; border-radius: 16rpx; background-color: #FFFFFF; box-shadow: 0 8rpx 24rpx rgba(49,55,66,.05); }.field-block { display: flex; flex-direction: column; }.bank-field-block { margin-top: 0; }.field-row { display: flex; min-height: 104rpx; align-items: center; }.bank-field-row { align-items: center; }.field-label { width: 178rpx; padding-top: 0; flex: none; color: #5F6978; font-size: 27rpx; font-weight: 400; }.amount-input-shell { display: flex; height: 70rpx; padding: 0 18rpx; flex: 1; align-items: center; border: 1rpx solid #E3E7ED; border-radius: 8rpx; background-color: #FFFFFF; box-sizing: border-box; }.amount-prefix { color: #8B94A2; font-size: 27rpx; font-weight: 400; }.amount-input { height: 68rpx; margin-left: 8rpx; flex: 1; color: #2C3748; font-size: 27rpx; }.input-placeholder { color: #B5BDC9; font-size: 25rpx; }.field-invalid { border-color: #F0746D; background-color: #FFF9F8; }.field-error { margin: 0 0 14rpx 178rpx; color: #E95B55; font-size: 22rpx; }.field-divider { height: 1rpx; margin-left: 178rpx; background-color: #EEF0F3; }.bank-value { display: flex; min-width: 0; flex: 1; }.bank-placeholder { color: #B5BDC9; font-size: 25rpx; }.selected-bank-copy { display: flex; min-width: 0; flex: 1; flex-direction: column; }.selected-bank-name { color: #2C3748; font-size: 25rpx; }.selected-card-number { margin-top: 4rpx; color: #8A93A1; font-size: 22rpx; }.select-bank-button { display: flex; width: 84rpx; height: 52rpx; padding: 0; margin: 0 0 0 14rpx; flex: none; align-items: center; justify-content: center; border: 1rpx solid #58A5F8; border-radius: 8rpx; color: #318CF0; background-color: #FFFFFF; font-size: 23rpx; font-weight: 400; }.submit-button { display: flex; width: 360rpx; height: 72rpx; margin: 34rpx auto 0; align-items: center; justify-content: center; border-radius: 8rpx; color: #FFFFFF; background-color: #409EFF; font-size: 27rpx; font-weight: 500; box-shadow: none; }.submit-disabled { opacity: .58; }.safe-space { height: 70rpx; } |
| | | .back-icon { transform: none; } |
| | | .submit-button { width: 100%; margin-left: 0; margin-right: 0; } |
| | | |
| | | /* Match the withdrawal prototype: balance above the card and vertically stacked fields. */ |
| | | .form-scroll { padding-top: 0; } |
| | | .available-row { height: 128rpx; padding: 20rpx 0 24rpx; flex-direction: column; justify-content: center; align-items: center; box-sizing: border-box; } |
| | | .available-label { font-size: 25rpx; line-height: 36rpx; } |
| | | .available-value { margin: 2rpx 0 0; color: #E87535; font-size: 30rpx; line-height: 42rpx; } |
| | | .form-card { padding: 54rpx 28rpx 34rpx; border-radius: 28rpx; } |
| | | .amount-field-row { min-height: 0; flex-direction: column; align-items: stretch; } |
| | | .amount-field-row .field-label { width: auto; margin-bottom: 10rpx; text-align: center; color: #273345; font-size: 30rpx; line-height: 42rpx; font-weight: 700; } |
| | | .amount-input-shell { width: 100%; height: 160rpx; padding: 10rpx 18rpx 0; flex: none; flex-direction: column; align-items: center; box-sizing: border-box; } |
| | | .amount-prefix { line-height: 34rpx; } |
| | | .amount-input { width: 100%; height: 100rpx; margin: 0; flex: none; text-align: center; } |
| | | .input-placeholder { text-align: center; } |
| | | .field-divider { display: none; } |
| | | .bank-field-block { margin-top: 56rpx; } |
| | | .bank-field-row { min-height: 0; flex-direction: column; align-items: flex-start; } |
| | | .bank-field-row .field-label { width: auto; margin-bottom: 18rpx; color: #273345; font-size: 30rpx; line-height: 42rpx; font-weight: 700; } |
| | | .bank-value { width: 100%; flex: none; } |
| | | .bank-placeholder { font-size: 25rpx; } |
| | | .bank-placeholder { display: none; } |
| | | .select-bank-button { width: 240rpx; height: 76rpx; margin: 18rpx 0 0; border-color: #F0D1B9; border-radius: 38rpx; color: #D97643; background-color: #FFF9F4; font-size: 28rpx; } |
| | | .submit-button { width: 100%; height: 88rpx; margin: 62rpx 0 0; border-radius: 44rpx; color: #FFFFFF; background-image: linear-gradient(90deg, #FF7625 0%, #FF9630 100%); font-size: 29rpx; line-height: 41rpx; font-weight: 700; box-shadow: 0 12rpx 24rpx rgba(255, 107, 36, 0.20); } |
| | | .safe-space { height: 70rpx; } |
| | | </style> |
| | |
| | | padding-left: 12px; |
| | | } |
| | | |
| | | .merchant-settings-form .created-time-row > label { |
| | | box-sizing: border-box; |
| | | padding-left: 12px; |
| | | } |
| | | |
| | | .merchant-settings-form .merchant-form-row > label, |
| | | .merchant-settings-form .merchant-hours-block > label { |
| | | box-sizing: border-box; |
| | | padding-left: 12px; |
| | | } |
| | | |
| | | .merchant-form-row.recommend-row .el-switch { |
| | | justify-self: start; |
| | | } |
| | |
| | | <h3>商家信息</h3> |
| | | |
| | | <el-form ref="merchantFormRef" :model="form" :rules="merchantRules" class="merchant-settings-form" label-width="58px" label-position="left"> |
| | | <div class="merchant-create-content" :class="{ 'service-merchant-content': isServiceMerchant || isApartmentMerchant || isHotelMerchant || isCarWashMerchant || isHealthCheckMerchant || isParkingMerchant || isChildcareMerchant || isFinancialMerchant || isMovieMerchant || isSpouseEmploymentMerchant || isApprovalMerchant }"> |
| | | <div class="merchant-create-left" :style="isApartmentMerchant || isFoodMerchant || isHotelMerchant || isCarWashMerchant || isHealthCheckMerchant || isParkingMerchant || isChildcareMerchant || isFinancialMerchant || isMovieMerchant || isSpouseEmploymentMerchant || isApprovalMerchant ? 'flex: 1' : ''"> |
| | | <div class="merchant-create-content" :class="{ 'service-merchant-content': isServiceMerchant || isApartmentMerchant || isHotelMerchant || isCarWashMerchant || isHealthCheckMerchant || isParkingMerchant || isChildcareMerchant || isFinancialMerchant || isMovieMerchant || isSpouseEmploymentMerchant || isApprovalMerchant || usesSimplifiedMerchantSettings }"> |
| | | <div class="merchant-create-left" :style="isApartmentMerchant || isFoodMerchant || isHotelMerchant || isCarWashMerchant || isHealthCheckMerchant || isParkingMerchant || isChildcareMerchant || isFinancialMerchant || isMovieMerchant || isSpouseEmploymentMerchant || isApprovalMerchant || usesSimplifiedMerchantSettings ? 'flex: 1' : ''"> |
| | | <el-form-item class="merchant-form-row three-columns" label="商家名称" prop="merchantName" required> |
| | | <el-input v-model="form.merchantName" /> |
| | | </el-form-item> |
| | |
| | | </el-form-item> |
| | | </div> |
| | | |
| | | <div v-if="!isServiceMerchant && !isApartmentMerchant && !isHotelMerchant && !isCarWashMerchant && !isHealthCheckMerchant && !isParkingMerchant && !isChildcareMerchant && !isFinancialMerchant && !isMovieMerchant && !isSpouseEmploymentMerchant && !isApprovalMerchant" class="merchant-create-middle"> |
| | | <div v-if="!isServiceMerchant && !isApartmentMerchant && !isHotelMerchant && !isCarWashMerchant && !isHealthCheckMerchant && !isParkingMerchant && !isChildcareMerchant && !isFinancialMerchant && !isMovieMerchant && !isSpouseEmploymentMerchant && !isApprovalMerchant && !usesSimplifiedMerchantSettings" class="merchant-create-middle"> |
| | | <el-form-item class="merchant-form-row" label="联系电话" prop="contactMobile" required> |
| | | <el-input v-model="form.contactMobile" /> |
| | | </el-form-item> |
| | |
| | | </template> |
| | | </div> |
| | | |
| | | <div class="merchant-create-right" :style="isApartmentMerchant || isFoodMerchant || isHotelMerchant || isCarWashMerchant || isHealthCheckMerchant || isParkingMerchant || isChildcareMerchant || isFinancialMerchant || isMovieMerchant || isSpouseEmploymentMerchant || isApprovalMerchant ? 'flex: 1' : ''"> |
| | | <el-form-item v-if="isServiceMerchant || isApartmentMerchant || isHotelMerchant || isCarWashMerchant || isHealthCheckMerchant || isParkingMerchant || isChildcareMerchant || isFinancialMerchant || isMovieMerchant || isSpouseEmploymentMerchant || isApprovalMerchant" class="merchant-form-row service-contact-row" label="联系电话" prop="contactMobile" required> |
| | | <div class="merchant-create-right" :style="isApartmentMerchant || isFoodMerchant || isHotelMerchant || isCarWashMerchant || isHealthCheckMerchant || isParkingMerchant || isChildcareMerchant || isFinancialMerchant || isMovieMerchant || isSpouseEmploymentMerchant || isApprovalMerchant || usesSimplifiedMerchantSettings ? 'flex: 1' : ''"> |
| | | <el-form-item v-if="isServiceMerchant || isApartmentMerchant || isHotelMerchant || isCarWashMerchant || isHealthCheckMerchant || isParkingMerchant || isChildcareMerchant || isFinancialMerchant || isMovieMerchant || isSpouseEmploymentMerchant || isApprovalMerchant || usesSimplifiedMerchantSettings" class="merchant-form-row service-contact-row" label="联系电话" prop="contactMobile" required> |
| | | <el-input v-model="form.contactMobile" placeholder="请输入联系电话" /> |
| | | </el-form-item> |
| | | |
| | | <div v-if="hasExistingMerchant" class="merchant-form-row"> |
| | | <div v-if="hasExistingMerchant" class="merchant-form-row created-time-row"> |
| | | <label>创建时间</label> |
| | | <el-input v-model="form.createdAt" readonly /> |
| | | </div> |
| | |
| | | const merchantType = product.value?.merchantType || '' |
| | | return merchantType === '酒店服务' || merchantType === '酒店' |
| | | }) |
| | | const hideTalentBenefitPanel = computed(() => { |
| | | const merchantType = String(product.value?.merchantType || '').trim().toLowerCase() |
| | | return ['酒店服务', '酒店', '医院服务', '医院', '医疗服务', '医疗', 'hotel', 'hospital', 'medical', 'health_check'].includes(merchantType) |
| | | }) |
| | | const productAmountText = computed(() => { |
| | | const salePrice = product.value?.salePrice |
| | | const originalPrice = product.value?.originalPrice |
| | |
| | | <img v-if="previewUrl" :src="previewUrl" alt="商品图片预览" class="product-audit-preview-image" /> |
| | | </el-dialog> |
| | | |
| | | <section class="audit-detail-panel talent-benefit-panel"> |
| | | <section v-if="!hideTalentBenefitPanel" class="audit-detail-panel talent-benefit-panel"> |
| | | <h3>人才权益</h3> |
| | | <div class="benefit-config-grid"> |
| | | <div v-for="row in benefitRows" :key="row.level" class="benefit-config-row"> |
| | |
| | | result.put("name", result.get("ruleName")); |
| | | result.put("discountRate", configuredDiscountRate(right.get("discount"))); |
| | | result.put("subsidyEnabled", booleanValue(right.get("governmentSubsidy")) ? 1 : 0); |
| | | result.put("merchantDetail", right.get("detail")); |
| | | result.put("consumeDesc", right.get("consumeDesc")); |
| | | result.put("benefitText", configuredBenefitText(right)); |
| | | return result; |
| | |
| | | .orElse(""); |
| | | if (configuredDescription.isBlank()) { |
| | | configuredDescription = configuredRules(merchantId, cardLevel).stream() |
| | | .map(rule -> string(rule.get("merchantDetail"))) |
| | | .filter(value -> !value.isBlank()) |
| | | .findFirst() |
| | | .orElse(""); |
| | | } |
| | | if (configuredDescription.isBlank()) { |
| | | configuredDescription = configuredRules(merchantId, cardLevel).stream() |
| | | .map(rule -> string(rule.get("benefitText"))) |
| | | .filter(value -> !value.isBlank()) |
| | | .findFirst() |
| | |
| | | import org.springframework.jdbc.core.JdbcTemplate; |
| | | import org.springframework.stereotype.Service; |
| | | import org.springframework.transaction.annotation.Transactional; |
| | | import tools.jackson.core.type.TypeReference; |
| | | import tools.jackson.databind.ObjectMapper; |
| | | |
| | | @Service |
| | | public class MerchantWorkService { |
| | | private static final long REVOCATION_HOURS = 3; |
| | | private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); |
| | | private static final TypeReference<List<Map<String, Object>>> RIGHTS_TYPE = new TypeReference<>() {}; |
| | | |
| | | private final JdbcTemplate jdbcTemplate; |
| | | private final ManualConsumptionService manualConsumptionService; |
| | |
| | | select o.id, o.order_no, o.talent_id, tu.name as talent_name, tu.mobile as talent_mobile, tu.gender, |
| | | o.card_id, tc.card_no, coalesce(nullif(o.card_level_snapshot, ''), tc.card_level) as card_level, |
| | | o.benefit_id, b.benefit_name, |
| | | o.merchant_id, m.merchant_name, o.product_id, o.product_name_snapshot, p.rights_config, |
| | | o.merchant_id, m.merchant_name, m.merchant_rights_config, |
| | | o.product_id, o.product_name_snapshot, p.rights_config, |
| | | o.order_type, o.order_amount, o.base_pay_amount, o.coupon_amount, |
| | | o.subsidy_amount, o.pay_amount, o.order_status, o.verify_status, o.created_at, |
| | | vr.verify_time, vr.status as verify_record_status |
| | |
| | | |
| | | private String currentBenefitText(Map<String, Object> order, String category) { |
| | | String cardLevel = text(order.get("cardLevel")); |
| | | String merchantDetail = merchantBenefitDetail(order.get("merchantRightsConfig"), cardLevel); |
| | | if (!merchantDetail.isBlank()) return merchantDetail; |
| | | if ("store".equals(category)) { |
| | | Long merchantId = number(order.get("merchantId")); |
| | | return merchantId == null ? "以当前商户到店消费规则为准" : manualConsumptionService.benefitTextForMerchant(merchantId, cardLevel); |
| | |
| | | return configured.isBlank() ? "以当前订单权益规则为准" : configured; |
| | | } |
| | | |
| | | private String merchantBenefitDetail(Object rawConfig, String cardLevel) { |
| | | if (rawConfig == null || cardLevel.isBlank()) return ""; |
| | | try { |
| | | List<Map<String, Object>> configs = rawConfig instanceof List<?> |
| | | ? ((List<?>) rawConfig).stream().filter(Map.class::isInstance) |
| | | .map(item -> (Map<String, Object>) item).toList() |
| | | : OBJECT_MAPPER.readValue(String.valueOf(rawConfig), RIGHTS_TYPE); |
| | | for (Map<String, Object> config : configs) { |
| | | if (!booleanValue(config.get("enabled")) || !sameCardLevel(config.get("level"), cardLevel)) continue; |
| | | String detail = text(config.get("detail")); |
| | | if (!detail.isBlank()) return detail; |
| | | } |
| | | } catch (Exception ignored) { |
| | | // Invalid legacy configuration should fall back to the structured rule text. |
| | | } |
| | | return ""; |
| | | } |
| | | |
| | | private boolean sameCardLevel(Object configured, String actual) { |
| | | return normalizeCardLevel(configured).equals(normalizeCardLevel(actual)); |
| | | } |
| | | |
| | | private String normalizeCardLevel(Object value) { |
| | | String raw = text(value).toUpperCase(); |
| | | if (raw.contains("卓越") || "EXCELLENT".equals(raw)) return "EXCELLENT"; |
| | | if (raw.contains("杰出") || "OUTSTANDING".equals(raw)) return "OUTSTANDING"; |
| | | if (raw.contains("菁英") || "ELITE".equals(raw)) return "ELITE"; |
| | | return raw; |
| | | } |
| | | |
| | | private boolean booleanValue(Object value) { |
| | | return value instanceof Boolean bool ? bool : "1".equals(text(value)) || "true".equalsIgnoreCase(text(value)); |
| | | } |
| | | |
| | | private String productBenefitText(ProductPriceCalculator.ProductRight right) { |
| | | String discountText = discountText(right.discount()); |
| | | if (!right.free()) return discountText.isBlank() ? "以商品配置为准" : "按市价" + discountText; |
| | |
| | | %s as talent_level, |
| | | o.card_id, tc.card_no, tc.card_type, |
| | | o.merchant_id, m.merchant_name, m.merchant_type, |
| | | o.benefit_id, b.benefit_name, o.order_type, |
| | | o.benefit_id, b.benefit_name, |
| | | coalesce(nullif(o.product_name_snapshot, ''), p.product_name, b.benefit_name) as product_name, |
| | | o.order_type, |
| | | o.order_amount, o.subsidy_amount, o.pay_amount, |
| | | o.order_status, o.verify_status, o.created_at, o.updated_at as pay_time |
| | | from benefit_order o |
| | | left join talent_user tu on tu.id = o.talent_id |
| | | left join talent_card tc on tc.id = o.card_id |
| | | left join benefit b on b.id = o.benefit_id |
| | | left join merchant_product p on p.id = o.product_id and p.deleted = 0 |
| | | left join merchant m on m.id = o.merchant_id |
| | | """.formatted(ORDER_CARD_LEVEL_SQL) + where + " order by o.id desc limit ? offset ?", pageArgs.toArray()); |
| | | Long total = jdbcTemplate.queryForObject(""" |