| talent-card-admin/src/style.css | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| talent-card-admin/src/views/MerchantCreateView.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| talent-card-admin/src/views/MerchantView.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| talent-card-admin/src/views/PlatformSettingsView.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| talent-card-admin/src/views/StaffManagementView.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| talent-card-server/src/main/java/com/talentcard/server/business/admin/AdminBusinessService.java | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| talent-card-server/src/main/java/com/talentcard/server/business/admin/AdminStaffRequest.java | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 |
talent-card-admin/src/style.css
@@ -343,21 +343,6 @@ max-width: 100%; } /* Keep required and optional label text aligned; the required marker sits outside the text flow. */ .admin-main .el-form-item__label { position: relative; box-sizing: border-box; padding-left: 12px; text-align: left; justify-content: flex-start; } .admin-main .el-form-item.is-required:not(.is-no-asterisk) > .el-form-item__label::before { position: absolute; left: 0; margin-right: 0; } .main-breadcrumb { display: flex; align-items: center; @@ -1377,6 +1362,26 @@ width: 104px; } .merchant-consume-row .merchant-discount-input { position: relative; width: 104px; } .merchant-consume-row .merchant-discount-input .el-input { width: 104px; } .merchant-consume-row .merchant-discount-input .field-error { position: absolute; top: calc(100% + 4px); left: 0; width: max-content; color: #f56c6c; font-size: 12px; line-height: 16px; white-space: nowrap; } .hotel-voucher-row { grid-template-columns: 76px 42px 104px 24px 64px 104px 96px 42px; } @@ -1765,8 +1770,6 @@ .product-image-row label, .product-detail-row label, .product-notice-field label { box-sizing: border-box; padding-left: 12px; color: #334155; font-size: 12px; line-height: 28px; @@ -3397,6 +3400,8 @@ /* 角色管理筛选项与标题左边缘对齐。 */ .role-management-filter-form .el-form-item__label { flex: 0 0 42px; width: 42px !important; justify-content: flex-start; } @@ -4235,6 +4240,11 @@ line-height: 32px; } .platform-settings-form .platform-mini-program-link .el-form-item__label { font-size: 12px; white-space: nowrap; } .platform-settings-form .el-input { width: 178px; } talent-card-admin/src/views/MerchantCreateView.vue
@@ -88,7 +88,9 @@ const isParkingMerchant = computed(() => normalizedMerchantType.value === '停车服务') const isChildcareMerchant = computed(() => normalizedMerchantType.value === '子女托管') const isFinancialMerchant = computed(() => normalizedMerchantType.value === '金融服务') const isMovieMerchant = computed(() => normalizedMerchantType.value === '观影服务') const isMovieMerchant = computed(() => [ '观影服务', 'cinema', 'theatre', '电影院', '影院', '影城', '剧院', ].includes(normalizedMerchantType.value)) const isSpouseEmploymentMerchant = computed(() => normalizedMerchantType.value === '配偶推荐就业') const isApprovalMerchant = computed(() => normalizedMerchantType.value === '行政审批') const isServiceMerchant = computed(() => isFoodMerchant.value || normalizedMerchantType.value === '景区服务') @@ -966,6 +968,7 @@ } const rightsConfigs = reactive<MerchantRightConfig[]>(defaultMerchantRights()) const merchantDiscountErrors = reactive<Record<string, string>>({}) const rightExampleImage = 'https://images.unsplash.com/photo-1559847844-5315695dadae?auto=format&fit=crop&w=640&q=80' @@ -1688,6 +1691,33 @@ return amount > 0 && amount <= 10 } function validMerchantDiscount(value: unknown) { const text = String(value ?? '').trim() if (!/^(?:[0-9]|10)(?:\.\d)?$/.test(text)) return false const amount = Number(text) return amount > 0 && amount <= 10 } function normalizeMerchantDiscount(item: MerchantRightConfig) { let text = String(item.discount ?? '').replace(/[^\d.]/g, '') const dotIndex = text.indexOf('.') if (dotIndex >= 0) { text = `${text.slice(0, dotIndex + 1)}${text.slice(dotIndex + 1).replace(/\./g, '').slice(0, 1)}` } if (text.startsWith('.')) text = `0${text}` item.discount = text delete merchantDiscountErrors[item.level] } function validateMerchantDiscount(item: MerchantRightConfig) { if (validMerchantDiscount(item.discount)) { delete merchantDiscountErrors[item.level] return true } merchantDiscountErrors[item.level] = '\u8bf7\u8f93\u5165\u5927\u4e8e0\u4e14\u4e0d\u8d85\u8fc710\u7684\u4e00\u4f4d\u5c0f\u6570' return false } function validateProductExceedDiscount(item: { level: string, exceedDiscount: unknown }) { const key = item.level if (validExceedDiscount(item.exceedDiscount)) { @@ -2006,9 +2036,12 @@ async function saveMerchant() { const merchantValid = await merchantFormRef.value?.validate().catch(() => false) if (!merchantValid) return if (isServiceMerchant.value && rightsConfigs.some((item) => item.storeConsume && !validExceedDiscount(item.discount))) { ElMessage.warning('折扣请输入大于 0 且不超过 10 的一位小数') return if (isServiceMerchant.value) { const invalidDiscount = rightsConfigs.filter((item) => item.storeConsume && !validateMerchantDiscount(item)) if (invalidDiscount.length > 0) { ElMessage.warning('\u6298\u6263\u8bf7\u8f93\u5165\u5927\u4e8e0\u4e14\u4e0d\u8d85\u8fc710\u7684\u4e00\u4f4d\u5c0f\u6570') return } } const updatingMerchantPassword = isEditMode.value && accountForm.loginPassword.length > 0 const accountValid = await accountFormRef.value?.validate().catch(() => false) @@ -2523,7 +2556,16 @@ <strong>到店消费</strong> <el-switch v-model="item.storeConsume" /> <span>折扣</span> <el-input v-model="item.discount" placeholder="请输入" /> <div class="merchant-discount-input"> <el-input v-model="item.discount" placeholder="请输入" :class="{ 'is-error': merchantDiscountErrors[item.level] }" @input="normalizeMerchantDiscount(item)" @blur="validateMerchantDiscount(item)" /> <span v-if="merchantDiscountErrors[item.level]" class="field-error">{{ merchantDiscountErrors[item.level] }}</span> </div> <span>折</span> <strong>是否政府补贴</strong> <el-switch v-model="item.governmentSubsidy" /> talent-card-admin/src/views/MerchantView.vue
@@ -249,7 +249,7 @@ <el-form class="merchant-filter-form" :model="filters" label-width="74px"> <el-form-item label="商家"> <el-input v-model="filters.keyword" placeholder="请输入商家名称,支持模糊查询" clearable @keyup.enter="search" /> <el-input v-model="filters.keyword" placeholder="请输入商家名称" clearable @keyup.enter="search" /> </el-form-item> <el-form-item label="联系电话"> <el-input v-model="filters.contactMobile" placeholder="请输入商家联系电话" clearable /> talent-card-admin/src/views/PlatformSettingsView.vue
@@ -49,7 +49,7 @@ <span v-if="form.videoName || form.homeVideoUrl" class="platform-setting-file">{{ form.videoName || form.homeVideoUrl }}</span> </el-form-item> <el-form-item label="首页小程序跳转链接"> <el-form-item class="platform-mini-program-link" label="首页小程序跳转链接"> <el-input v-model="form.miniProgramUrl" placeholder="请输入" /> </el-form-item> talent-card-admin/src/views/StaffManagementView.vue
@@ -36,6 +36,14 @@ const accountPattern = /^[A-Za-z0-9]+$/; const rules = computed<FormRules>(() => ({ displayName: [{ required: true, message: "请输入人员名称", trigger: "blur" }], mobile: [{ validator: (_rule, value, callback) => { const mobile = String(value || '').trim() if (!mobile || /^1\d{10}$/.test(mobile)) return callback() callback(new Error('请输入正确的联系电话')) }, trigger: 'blur', }], account: [ { required: true, message: "请输入登录账号", trigger: "blur" }, { @@ -328,7 +336,7 @@ ><el-form ref="formRef" :model="form" :rules="rules" label-width="92px" ><el-form-item label="人员名称" prop="displayName" required ><el-input v-model="form.displayName" /></el-form-item ><el-form-item label="联系电话" ><el-form-item label="联系电话" prop="mobile" ><el-input v-model="form.mobile" inputmode="numeric" talent-card-server/src/main/java/com/talentcard/server/business/admin/AdminBusinessService.java
@@ -238,11 +238,9 @@ StringBuilder countSql = new StringBuilder("select count(*) from merchant m where m.deleted = 0"); List<Object> args = new ArrayList<>(); if (keyword != null && !keyword.isBlank()) { sql.append(" and (m.merchant_name like ? or m.contact_mobile like ? or m.address like ?)"); countSql.append(" and (m.merchant_name like ? or m.contact_mobile like ? or m.address like ?)"); sql.append(" and m.merchant_name like ?"); countSql.append(" and m.merchant_name like ?"); String like = "%" + keyword.trim() + "%"; args.add(like); args.add(like); args.add(like); } if (contactMobile != null && !contactMobile.isBlank()) { @@ -330,8 +328,8 @@ s.external_mini_program_path as car_wash_external_mini_program_path, s.external_web_url as car_wash_external_web_url, (select o.account from merchant_operator o where o.merchant_id = merchant.id and o.role_code = 'owner' and o.deleted = 0 order by o.id asc limit 1) as login_account where o.merchant_id = merchant.id and o.deleted = 0 order by case when o.role_code = 'owner' then 0 else 1 end, o.id asc limit 1) as login_account from merchant left join transport_car_wash_service s on s.merchant_id = merchant.id and s.deleted = 0 where merchant.deleted = 0 and merchant.id = ? @@ -361,6 +359,7 @@ MerchantType merchantType = requireMerchantType(request.merchantType()); validateMerchantAddress(merchantType.code(), request.address()); validateRequiredMerchantCreateFields(request); validateMerchantRightsConfig(merchantType.code(), request.merchantRightsConfig()); jdbcTemplate.update(""" insert into merchant(merchant_name, merchant_type, merchant_type_code, contact_name, contact_mobile, address, logo_url, cover_url, business_status, longitude, latitude, can_login_merchant_app, group_buying_enabled, @@ -392,8 +391,10 @@ syncPremiumTopicMerchant(id, merchantType, request.sortNo()); Long storeId = createDefaultStore(id, request.merchantName(), request.address()); syncMerchantBusinessHours(id, storeId, request.businessHours()); if (hasMerchantAppAccess(request)) { String account = request.loginAccount().trim(); if (hasMerchantAppAccess(request) || (request.loginAccount() != null && !request.loginAccount().isBlank()) || (request.loginPassword() != null && !request.loginPassword().isBlank())) { String account = request.loginAccount() == null ? "" : request.loginAccount().trim(); if (!account.matches("^[A-Za-z0-9]{1,32}$")) { throw new BusinessException(ErrorCode.BAD_REQUEST, "商户登录账号只能包含英文字母和数字"); } @@ -436,6 +437,20 @@ } if (request.loginPassword() == null || request.loginPassword().length() < 6 || request.loginPassword().length() > 64) { throw new BusinessException(ErrorCode.BAD_REQUEST, "商户登录密码长度须为6至64位"); } } private void validateMerchantRightsConfig(String merchantType, List<Map<String, Object>> rightsConfig) { if (!Set.of("food", "scenic").contains(MerchantTypeCatalog.canonicalCode(merchantType)) || rightsConfig == null) return; for (Map<String, Object> right : rightsConfig) { Object storeConsume = right.get("storeConsume"); if (!Boolean.TRUE.equals(storeConsume) && !"true".equalsIgnoreCase(String.valueOf(storeConsume))) continue; String discount = String.valueOf(right.getOrDefault("discount", "")).trim(); if (!discount.matches("^(?:[0-9]|10)(?:\\.\\d)?$") || new BigDecimal(discount).compareTo(BigDecimal.ZERO) <= 0 || new BigDecimal(discount).compareTo(BigDecimal.TEN) > 0) { throw new BusinessException(ErrorCode.BAD_REQUEST, "折扣请输入大于0且不超过10的一位小数"); } } } @@ -484,6 +499,7 @@ public Map<String, Object> updateMerchant(Long id, MerchantUpdateRequest request) { MerchantType merchantType = resolveMerchantTypeForUpdate(id, request.merchantType()); validateMerchantAddress(merchantType.code(), request.address()); validateMerchantRightsConfig(merchantType.code(), request.merchantRightsConfig()); int updated = jdbcTemplate.update(""" update merchant set merchant_name = ?, merchant_type = ?, merchant_type_code = ?, contact_name = ?, contact_mobile = ?, talent-card-server/src/main/java/com/talentcard/server/business/admin/AdminStaffRequest.java
@@ -1,12 +1,17 @@ package com.talentcard.server.business.admin; import jakarta.validation.constraints.*; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Pattern; import jakarta.validation.constraints.Size; public record AdminStaffRequest( @NotBlank @Size(max=32) @Pattern(regexp="[A-Za-z0-9]+", message="登录账号只能包含英文和数字") String account, @Size(max=64) String password, @NotBlank @Size(max=64) String displayName, @Pattern(regexp="^$|^1\\d{10}$", message="联系电话格式不正确") String mobile, @NotBlank @Size(max=32) String roleCode, @NotBlank @Size(max=64) String positionName, @Size(max=64) String customerWechat, @NotBlank @Size(max = 32) @Pattern(regexp = "[A-Za-z0-9]+", message = "登录账号只能包含英文和数字") String account, @Size(max = 64) String password, @NotBlank @Size(max = 64) String displayName, @Pattern(regexp = "^$|^1\\d{10}$", message = "请输入正确的联系电话") String mobile, @NotBlank @Size(max = 32) String roleCode, @NotBlank @Size(max = 64) String positionName, @Size(max = 64) String customerWechat, @NotNull Byte status ) {}