shinnosuke
2026-08-19 c920c574d9587ef798d0c7f82d574e3e30bfde3a
又又更新
8个文件已修改
143 ■■■■■ 已修改文件
talent-card-admin/src/api/admin.ts 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-admin/src/style.css 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-admin/src/utils/excel.ts 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-admin/src/views/MerchantCreateView.vue 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-admin/src/views/TalentCreateView.vue 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-admin/src/views/TalentDetailView.vue 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-server/src/main/java/com/talentcard/server/business/admin/AdminBusinessService.java 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-server/src/main/java/com/talentcard/server/business/merchant/MerchantManagementService.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-admin/src/api/admin.ts
@@ -337,10 +337,25 @@
  return http.get<ServiceApplication, ServiceApplication>(`/api/admin/service-appointment-applications/${id}`)
}
export function updateServiceApplicationStatus(id: number | string, status: string, handlerName = '管理员') {
export function updateServiceApplicationStatus(
  id: number | string,
  status: string,
  handlerName = '管理员',
  remark = '',
  rejectReason = '',
) {
  return http.patch<ServiceApplication, ServiceApplication>(`/api/admin/service-appointment-applications/${id}/status`, {
    status,
    handlerName,
    remark,
    rejectReason,
  })
}
export function exportServiceApplications(params: ServiceApplicationQuery = {}) {
  return http.get<Blob, Blob>('/api/admin/service-appointment-applications/export', {
    params,
    responseType: 'blob',
  })
}
@@ -527,6 +542,10 @@
  return http.get<PageResult<AnyRecord>, PageResult<AnyRecord>>('/api/admin/display-services', { params })
}
export function getDisplayServiceDetail(serviceId: number | string) {
  return http.get<AnyRecord, AnyRecord>(`/api/admin/display-services/${serviceId}`)
}
export function getDisplayServiceCategories() {
  return http.get<AnyRecord[], AnyRecord[]>('/api/admin/display-services/categories')
}
talent-card-admin/src/style.css
@@ -4447,7 +4447,7 @@
  gap: 16px 52px;
  min-height: 82px;
  padding: 18px;
  background: #f2f3f5;
  background: transparent;
  font-size: 13px;
}
@@ -4462,7 +4462,7 @@
  gap: 34px;
  min-height: 136px;
  padding: 8px 24px 18px;
  background: #f2f3f5;
  background: transparent;
}
.talent-card-stack {
@@ -4493,7 +4493,7 @@
.service-staff-panel {
  min-height: 54px;
  padding: 14px 18px;
  background: #f2f3f5;
  background: transparent;
}
.service-staff-panel .el-select {
@@ -4593,10 +4593,10 @@
.identity-edit-card {
  min-height: 92px;
  padding: 16px 14px;
  background: #f2f3f5;
  background: transparent;
}
.identity-edit-card .el-form {
.identity-edit-form {
  display: grid;
  grid-template-columns: repeat(3, minmax(220px, 1fr));
  gap: 12px 50px;
@@ -4609,6 +4609,28 @@
.identity-edit-card .el-input,
.identity-edit-card .el-select {
  width: 180px;
}
/* 新增页与详情页采用同样的整行信息面板,避免被表单标签列压缩。 */
.talent-create-page .create-card-form-item,
.talent-create-page .service-staff-form-item {
  margin: 0;
}
.talent-create-page .create-card-form-item > .el-form-item__content,
.talent-create-page .service-staff-form-item > .el-form-item__content {
  display: block;
  margin-left: 0 !important;
}
.talent-create-page .create-card-form-item .card-manage-panel,
.talent-create-page .service-staff-form-item .service-staff-panel {
  width: 100%;
  box-sizing: border-box;
}
.talent-create-page .service-staff-form-item .service-staff-panel {
  min-height: 54px;
}
.create-card-preview {
@@ -5107,7 +5129,7 @@
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
  .identity-edit-card .el-form {
  .identity-edit-form {
    grid-template-columns: repeat(2, minmax(220px, 1fr));
  }
@@ -5273,7 +5295,7 @@
    width: min(100%, 220px);
  }
  .identity-edit-card .el-form {
  .identity-edit-form {
    grid-template-columns: 1fr;
  }
}
talent-card-admin/src/utils/excel.ts
@@ -21,3 +21,14 @@
  XLSX.utils.book_append_sheet(workbook, sheet, '数据列表')
  XLSX.writeFile(workbook, `${filename}.xlsx`)
}
export function saveBlob(blob: Blob, filename: string) {
  const url = URL.createObjectURL(blob)
  const link = document.createElement('a')
  link.href = url
  link.download = filename
  document.body.appendChild(link)
  link.click()
  link.remove()
  URL.revokeObjectURL(url)
}
talent-card-admin/src/views/MerchantCreateView.vue
@@ -91,7 +91,6 @@
const isSpouseEmploymentMerchant = computed(() => normalizedMerchantType.value === '配偶推荐就业')
const isApprovalMerchant = computed(() => normalizedMerchantType.value === '行政审批')
const isServiceMerchant = computed(() => isFoodMerchant.value || normalizedMerchantType.value === '景区服务')
const requiresFoodMerchantSettings = computed(() => !isEditMode.value && isFoodMerchant.value)
const supportsStoreConsumption = computed(() => [
  '酒店服务', '景区服务', '餐饮服务', '口腔治疗保健', '免费体检',
].includes(normalizedMerchantType.value) || (isCarWashMerchant.value && form.miniProgramJumpType === 'normal'))
@@ -250,12 +249,6 @@
  contactMobile: [{ required: true, message: '请输入联系电话', trigger: 'blur' }],
  address: isApartmentMerchant.value ? [] : [{ required: true, message: '请输入商家地址', trigger: 'blur' }],
  coverUrl: [{ required: true, message: '请上传商家封面图', trigger: 'change' }],
  businessHours: [{
    validator: (_rule, _value, callback) => isApprovalMerchant.value || hasValidBusinessHours()
      ? callback()
      : callback(new Error('请选择有效的营业时间')),
    trigger: 'change',
  }],
  sortNo: [{
    validator: (_rule, value, callback) => {
      const text = String(value ?? '').trim()
@@ -1158,15 +1151,10 @@
  businessHours.splice(index, 1)
}
function hasValidBusinessHours() {
  return businessHours.length > 0 && businessHours.every((item) => {
    const [startTime, endTime] = item.value
    return Boolean(startTime && endTime && startTime !== endTime)
  })
}
function businessHoursPayload() {
  return businessHours.map((item) => ({ startTime: item.value[0], endTime: item.value[1] }))
  return businessHours
    .map((item) => ({ startTime: item.value[0], endTime: item.value[1] }))
    .filter((item) => item.startTime && item.endTime && item.startTime !== item.endTime)
}
function resetBusinessHours(hours?: Array<{ startTime: string; endTime: string }>) {
@@ -2270,7 +2258,7 @@
              <el-input v-model="form.createdAt" readonly />
            </div>
            <el-form-item v-if="!isApprovalMerchant" class="merchant-hours-block" label="营业时间" prop="businessHours" required>
            <el-form-item v-if="!isApprovalMerchant" class="merchant-hours-block" label="营业时间" prop="businessHours">
              <div class="merchant-hours-list">
                <div v-for="(item, index) in businessHours" :key="item.id" class="merchant-hour-row">
                  <el-time-picker
talent-card-admin/src/views/TalentCreateView.vue
@@ -45,7 +45,7 @@
    { pattern: /^[0-9A-Z]{18}$/, message: '身份证号须为18位数字或大写英文字母', trigger: 'blur' },
  ],
  contactAddress: [{ required: true, message: '请输入联系地址', trigger: 'blur' }],
  workAddress: [{ required: true, message: '请输入工作地址', trigger: 'blur' }],
  workUnit: [{ required: true, message: '请输入工作单位', trigger: 'blur' }],
  gender: [{ required: true, message: '请选择性别', trigger: 'change' }],
  talentCardNo: [{
    validator: (_rule, value, callback) => talentLevelConfirmed.value && String(value || '').trim()
@@ -108,7 +108,8 @@
  saving.value = true
  try {
    const talent = await createTalent(form)
    // 后端仍保留 workAddress 历史字段;新建时同步写入,保证新旧端均可回显。
    const talent = await createTalent({ ...form, workAddress: form.workUnit })
    await updateTalentServiceStaff(talent.id, serviceStaff.value)
    ElMessage.success('持卡人已保存')
    router.push('/talents')
@@ -127,7 +128,7 @@
<template>
  <div class="talent-detail-page talent-create-page">
    <el-form ref="formRef" :model="form" :rules="rules" label-width="104px">
    <section class="detail-block create-mobile-summary">
    <section class="detail-block user-summary create-mobile-summary">
      <el-form-item label="手机号码:" prop="mobile" required>
          <el-input
            v-model="form.mobile"
@@ -166,8 +167,8 @@
          <el-form-item label="联系地址:" prop="contactAddress" required>
            <el-input v-model="form.contactAddress" placeholder="请输入" />
          </el-form-item>
          <el-form-item label="工作地址:" prop="workAddress" required>
            <el-input v-model="form.workAddress" placeholder="请输入" />
          <el-form-item label="工作单位:" prop="workUnit" required>
            <el-input v-model="form.workUnit" placeholder="请输入工作单位" />
          </el-form-item>
          <el-form-item label="性别:" prop="gender" required>
            <el-select v-model="form.gender" placeholder="选择">
@@ -181,7 +182,7 @@
    <section class="detail-block">
      <h3>人才卡管理</h3>
      <el-form-item label="人才等级/卡号" prop="talentCardNo" required>
      <el-form-item prop="talentCardNo" required class="create-card-form-item" label-width="0">
        <div class="card-manage-panel">
          <div class="talent-card-stack">
            <button type="button" class="talent-card-preview create-card-preview" @click="openCardDialog">
@@ -198,7 +199,7 @@
    <section class="detail-block">
      <h3>专属服务人员</h3>
      <el-form-item label="专属服务人员" prop="serviceStaff" class="service-staff-form-item" required>
      <el-form-item prop="serviceStaff" class="service-staff-form-item" required label-width="0">
      <div class="service-staff-panel">
        <el-select v-model="serviceStaff" clearable placeholder="请选择专属服务人员" @change="formRef?.validateField('serviceStaff')">
          <el-option
talent-card-admin/src/views/TalentDetailView.vue
@@ -103,8 +103,8 @@
    name: talent.value?.name || '',
    idCardNo: talent.value?.idCardNo || '',
    gender: talent.value?.gender,
    workUnit: talent.value?.workUnit || '',
    workAddress: talent.value?.workAddress || '',
    workUnit: talent.value?.workUnit || talent.value?.workAddress || '',
    workAddress: talent.value?.workAddress || talent.value?.workUnit || '',
    contactAddress: talent.value?.contactAddress || '',
  }
  certificationError.value = ''
@@ -130,7 +130,8 @@
  if (!talent.value?.id) return
  certificationSaving.value = true
  try {
    talent.value = await updateTalentCertification(talent.value.id, form)
    // 统一以 workUnit 为准,同时同步历史 workAddress 字段。
    talent.value = await updateTalentCertification(talent.value.id, { ...form, workAddress: form.workUnit })
    certificationDialogVisible.value = false
    ElMessage.success('认证信息已更新')
  } finally {
@@ -226,8 +227,8 @@
          <strong>{{ mask(talent?.idCardNo, 6, 4) }}</strong>
        </div>
        <div>
          <span>工作地址:</span>
          <strong>{{ talent?.workAddress || talent?.workUnit || '-' }}</strong>
          <span>工作单位:</span>
          <strong>{{ talent?.workUnit || talent?.workAddress || '-' }}</strong>
        </div>
        <div>
          <span>联系地址:</span>
@@ -312,10 +313,6 @@
          <label>
            <span>工作单位</span>
            <el-input v-model="certificationForm.workUnit" maxlength="128" placeholder="请输入工作单位" />
          </label>
          <label>
            <span>工作地址</span>
            <el-input v-model="certificationForm.workAddress" maxlength="255" placeholder="请输入工作地址" />
          </label>
          <label>
            <span>联系地址</span>
talent-card-server/src/main/java/com/talentcard/server/business/admin/AdminBusinessService.java
@@ -429,11 +429,6 @@
        if (!Set.of("景区服务", "餐饮服务").contains(request.merchantType())) {
            return;
        }
        if (request.businessHours() == null || request.businessHours().isEmpty()
                || request.businessHours().stream().anyMatch(item -> item.startTime() == null || item.endTime() == null
                        || item.startTime().isBlank() || item.endTime().isBlank() || item.startTime().equals(item.endTime()))) {
            throw new BusinessException(ErrorCode.BAD_REQUEST, "请选择有效的营业时间");
        }
        if (request.loginAccount() == null || !request.loginAccount().trim().matches("^[A-Za-z0-9]{1,32}$")) {
            throw new BusinessException(ErrorCode.BAD_REQUEST, "商户登录账号只能包含英文字母和数字");
        }
@@ -463,13 +458,13 @@
    private void syncMerchantBusinessHours(Long merchantId, Long storeId, List<MerchantBusinessHourRequest> businessHours) {
        if (businessHours == null) return;
        jdbcTemplate.update("delete from merchant_business_hours where merchant_id = ? and store_id = ?", merchantId, storeId);
        if (businessHours.isEmpty()) {
            throw new BusinessException(ErrorCode.BAD_REQUEST, "请至少配置一个营业时间段");
            return;
        }
        if (businessHours.stream().anyMatch(item -> item.startTime().equals(item.endTime()))) {
            throw new BusinessException(ErrorCode.BAD_REQUEST, "营业开始时间和结束时间不能相同");
        }
        jdbcTemplate.update("delete from merchant_business_hours where merchant_id = ? and store_id = ?", merchantId, storeId);
        for (int index = 0; index < businessHours.size(); index++) {
            MerchantBusinessHourRequest item = businessHours.get(index);
            jdbcTemplate.update("""
@@ -981,15 +976,19 @@
                where s.status = 1 and s.deleted = 0 and h.merchant_id in (""" + placeholders + ")", merchantIds.toArray()));
        LocalTime now = LocalTime.now(ZoneId.of("Asia/Shanghai"));
        Set<Long> openMerchantIds = new HashSet<>();
        Set<Long> merchantsWithBusinessHours = new HashSet<>();
        for (Map<String, Object> period : periods) {
            Long merchantId = Long.valueOf(String.valueOf(period.get("merchantId")));
            merchantsWithBusinessHours.add(merchantId);
            if (isWithinBusinessPeriod(now, String.valueOf(period.get("startTime")), String.valueOf(period.get("endTime")))) {
                openMerchantIds.add(Long.valueOf(String.valueOf(period.get("merchantId"))));
                openMerchantIds.add(merchantId);
            }
        }
        for (Map<String, Object> merchant : merchants) {
            Long merchantId = Long.valueOf(String.valueOf(merchant.get("id")));
            boolean enabled = "1".equals(String.valueOf(merchant.get("status")));
            merchant.put("businessStatus", enabled && openMerchantIds.contains(merchantId) ? "营业中" : "休息中");
            boolean open = !merchantsWithBusinessHours.contains(merchantId) || openMerchantIds.contains(merchantId);
            merchant.put("businessStatus", enabled && open ? "营业中" : "休息中");
        }
    }
talent-card-server/src/main/java/com/talentcard/server/business/merchant/MerchantManagementService.java
@@ -329,6 +329,14 @@
        if (!Long.valueOf(1).equals(number(merchant.get("status"))) || "closed".equals(businessStatus(merchant.get("businessStatus")))) {
            return false;
        }
        Long configuredPeriods = jdbc.queryForObject("""
                select count(*)
                from merchant_business_hours
                where merchant_id = ? and store_id = ?
                """, Long.class, merchant.get("id"), storeId);
        if (configuredPeriods == null || configuredPeriods == 0) {
            return true;
        }
        Long activePeriods = jdbc.queryForObject("""
                select count(*)
                from merchant_business_hours