package com.tcm.training.admin.member.service; import cn.idev.excel.FastExcel; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.fasterxml.jackson.databind.JsonNode; import com.tcm.training.admin.agent.entity.AgentRegion; import com.tcm.training.admin.agent.entity.ProvincialAgent; import com.tcm.training.admin.agent.excel.ClinicAgentImportRow; import com.tcm.training.admin.agent.mapper.ProvincialAgentMapper; import com.tcm.training.admin.app.register.entity.SysRegion; import com.tcm.training.admin.app.register.mapper.SysRegionMapper; import com.tcm.training.admin.common.DateTimes; import com.tcm.training.admin.common.PageResponse; import com.tcm.training.admin.common.PhoneMasker; import com.tcm.training.admin.live.entity.Clinic; import com.tcm.training.admin.live.mapper.ClinicMapper; import com.tcm.training.admin.member.dto.ClinicBankAccountUpdateRequest; import com.tcm.training.admin.member.entity.AppUser; import com.tcm.training.admin.member.excel.ClinicExcelRow; import com.tcm.training.admin.member.mapper.ClinicQueryMapper; import com.tcm.training.admin.member.mapper.UserMapper; import com.tcm.training.admin.member.row.ClinicBoundUserRow; import com.tcm.training.admin.member.row.ClinicDetailRow; import com.tcm.training.admin.member.row.ClinicListRow; import com.tcm.training.admin.member.row.ClinicRegionOptionRow; import com.tcm.training.admin.member.vo.ClinicBoundUserItemVO; import com.tcm.training.admin.member.vo.ClinicDetailVO; import com.tcm.training.admin.member.vo.ClinicListItemVO; import com.tcm.training.admin.member.vo.ClinicRegionOptionVO; import com.tcm.training.admin.utils.VzanHttpUtil; import lombok.extern.slf4j.Slf4j; import org.springframework.jdbc.datasource.DataSourceTransactionManager; import org.springframework.stereotype.Service; import org.springframework.transaction.TransactionDefinition; import org.springframework.transaction.TransactionStatus; import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.support.DefaultTransactionDefinition; import org.springframework.web.multipart.MultipartFile; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.net.URLEncoder; import java.time.LocalDateTime; import java.util.*; @Service @Slf4j public class ClinicAppService { private final ClinicQueryMapper clinicQueryMapper; private final ClinicMapper clinicMapper; private final ProvincialAgentMapper provincialAgentMapper; private final UserMapper userMapper; private final SysRegionMapper sysRegionMapper; private final VzanHttpUtil vzanHttpUtil; private final DataSourceTransactionManager transactionManager; public ClinicAppService(ClinicQueryMapper clinicQueryMapper, ClinicMapper clinicMapper, ProvincialAgentMapper provincialAgentMapper, UserMapper userMapper, SysRegionMapper sysRegionMapper, VzanHttpUtil vzanHttpUtil, DataSourceTransactionManager transactionManager) { this.clinicQueryMapper = clinicQueryMapper; this.clinicMapper = clinicMapper; this.provincialAgentMapper = provincialAgentMapper; this.userMapper = userMapper; this.sysRegionMapper = sysRegionMapper; this.vzanHttpUtil = vzanHttpUtil; this.transactionManager = transactionManager; } public PageResponse page(int page, int pageSize, String regionCode, String name, String phone, LocalDateTime createdFrom, LocalDateTime createdTo, Integer status) { IPage result = clinicQueryMapper.selectClinicPage( new Page(page, pageSize), trimToNull(regionCode), trimToNull(name), trimToNull(phone), createdFrom, createdTo, status ); List list = new ArrayList(); for (ClinicListRow row : result.getRecords()) { list.add(toListItem(row)); } return PageResponse.of(list, result.getTotal(), result.getCurrent(), result.getSize()); } public void downloadClinicImportTemplate(HttpServletResponse response) throws IOException { response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); response.setCharacterEncoding("utf-8"); String filename = URLEncoder.encode("诊所省区代理导入模板", "UTF-8").replace("+", "%20"); response.setHeader("Content-Disposition", "attachment;filename*=utf-8''" + filename + ".xlsx"); FastExcel.write(response.getOutputStream(), ClinicAgentImportRow.class) .sheet("诊所省区代理导入模板") .doWrite(Collections.emptyList()); } @Transactional public void importAgentBindings(MultipartFile file) throws IOException { if (file == null || file.isEmpty()) { throw new IllegalArgumentException("请选择导入文件"); } List rows = FastExcel.read(file.getInputStream()) .head(ClinicAgentImportRow.class).sheet().doReadSync(); if (rows == null || rows.isEmpty()) { throw new IllegalArgumentException("导入文件不能为空"); } Set clinicNames = new LinkedHashSet(); Set agentNames = new LinkedHashSet(); Map bindingByClinicName = new HashMap(); int rowNumber = 1; for (ClinicAgentImportRow row : rows) { rowNumber++; if (row == null || (trimToNull(row.getClinicName()) == null && trimToNull(row.getAgentName()) == null)) continue; String clinicName = trimToNull(row.getClinicName()); String agentName = trimToNull(row.getAgentName()); if (clinicName == null) { throw new IllegalArgumentException("第" + rowNumber + "行诊所不能为空"); } if (agentName == null) { throw new IllegalArgumentException("第" + rowNumber + "行省区代理名称不能为空"); } String previous = bindingByClinicName.put(clinicName, agentName); if (previous != null && !previous.equals(agentName)) { throw new IllegalArgumentException("诊所“" + clinicName + "”存在多个代理绑定"); } clinicNames.add(clinicName); agentNames.add(agentName); } if (bindingByClinicName.isEmpty()) { throw new IllegalArgumentException("导入文件没有可用数据"); } List clinics = clinicMapper.selectList(new LambdaQueryWrapper().in(Clinic::getName, clinicNames)); List agents = provincialAgentMapper.selectList(new LambdaQueryWrapper() .eq(ProvincialAgent::getDeleted, 0).in(ProvincialAgent::getAgentName, agentNames)); Map agentByName = new LinkedHashMap(); for (ProvincialAgent agent : agents) { agentByName.put(agent.getAgentName(), agent); } for (String agentName : agentNames) { if (!agentByName.containsKey(agentName)) { throw new IllegalArgumentException("省区代理不存在:" + agentName); } } Set foundClinicNames = new LinkedHashSet(); for (Clinic clinic : clinics) { foundClinicNames.add(clinic.getName()); ProvincialAgent agent = agentByName.get(bindingByClinicName.get(clinic.getName())); clinic.setAgentId(agent.getId()); clinicMapper.updateById(clinic); } for (String clinicName : clinicNames) { if (!foundClinicNames.contains(clinicName)) { throw new IllegalArgumentException("诊所不存在:" + clinicName); } } } public void export(String regionCode, String name, String phone, LocalDateTime createdFrom, LocalDateTime createdTo, Integer status, HttpServletResponse response) throws IOException { response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); response.setCharacterEncoding("utf-8"); String filename = URLEncoder.encode("诊所列表", "UTF-8").replace("+", "%20"); response.setHeader("Content-Disposition", "attachment;filename*=utf-8''" + filename + ".xlsx"); List rows = clinicQueryMapper.selectClinicList( trimToNull(regionCode), trimToNull(name), trimToNull(phone), createdFrom, createdTo, status ); List exports = new ArrayList(); for (int i = 0; i < rows.size(); i++) { ClinicListRow row = rows.get(i); ClinicExcelRow excel = new ClinicExcelRow(); excel.setSerialNo(i + 1); excel.setName(defaultText(row.getName())); excel.setPhone(defaultText(PhoneMasker.mask(row.getPhone()))); excel.setRegionName(defaultText(row.getRegionName())); excel.setUserCount(row.getUserCount() == null ? 0 : row.getUserCount()); excel.setStatus(statusText(row.getStatus())); excel.setCreatedAt(DateTimes.format(row.getCreatedAt())); exports.add(excel); } FastExcel.write(response.getOutputStream(), ClinicExcelRow.class) .sheet("诊所列表") .doWrite(exports); } public List regionOptions() { List rows = clinicQueryMapper.selectRegionOptions(); List list = new ArrayList(); for (ClinicRegionOptionRow row : rows) { list.add(new ClinicRegionOptionVO(row.getRegionCode(), row.getRegionName())); } return list; } public ClinicDetailVO detail(Long id) { ClinicDetailRow row = clinicQueryMapper.selectClinicDetail(id); if (row == null) { throw new IllegalArgumentException("诊所不存在"); } ClinicDetailVO vo = new ClinicDetailVO(); vo.setId(row.getId()); vo.setAgentId(row.getAgentId()); vo.setName(row.getName()); vo.setStatus(row.getStatus()); vo.setPhone(defaultText(row.getContactPhone())); vo.setAddress(defaultText(row.getAddress())); vo.setProvinceCode(row.getProvinceCode()); vo.setCityCode(row.getCityCode()); vo.setDistrictCode(row.getDistrictCode()); vo.setBusinessLicenseUrl(trimToNull(row.getBusinessLicenseUrl())); vo.setUserCount(row.getUserCount() == null ? 0 : row.getUserCount()); vo.setBankName(defaultText(row.getBankName())); vo.setAccountHolder(defaultText(row.getAccountHolder())); vo.setAccountNumber(defaultText(row.getAccountNumber())); vo.setBranchName(defaultText(row.getBranchName())); vo.setAvatar(row.getAvatar()); vo.setLoginAccount(row.getLoginAccount()); return vo; } public ClinicDetailVO updateBankAccount(Long id, ClinicBankAccountUpdateRequest request) { // 手动开启事务 DefaultTransactionDefinition def = new DefaultTransactionDefinition(); def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED); TransactionStatus status = transactionManager.getTransaction(def); try { Clinic clinic = clinicMapper.selectById(id); if (clinic == null || (clinic.getDeleted() != null && clinic.getDeleted() == 1)) { throw new IllegalArgumentException("诊所不存在"); } String loginAccount = trimToNull(request.getLoginAccount()); AppUser user = userMapper.selectOne(new LambdaQueryWrapper().eq(AppUser::getPhone, loginAccount).eq(AppUser::getDeleted, 0).last("limit 1")); if (user == null) { throw new IllegalArgumentException("登录账号不存在"); } user.setAvatar(trimToNull(request.getAvatar())); user.setPhone(loginAccount); userMapper.updateById(user); Set codes = new LinkedHashSet(); addCode(codes, request.getProvinceCode()); addCode(codes, request.getCityCode()); addCode(codes, request.getDistrictCode()); Map regions = new HashMap(); List regionList = sysRegionMapper.selectList(new LambdaQueryWrapper().in(SysRegion::getCode, codes)); if (!regionList.isEmpty()) { for (SysRegion region : regionList) { regions.put(region.getCode(), region); } } clinic.setName(trimToNull(request.getName())); clinic.setContactPhone(loginAccount); clinic.setProvinceCode(trimToNull(request.getProvinceCode())); clinic.setProvinceName(regionName(request.getProvinceCode(), request.getProvinceName(), regions)); clinic.setCityCode(trimToNull(request.getCityCode())); clinic.setCityName(regionName(request.getCityCode(), request.getCityName(), regions)); clinic.setDistrictCode(trimToNull(request.getDistrictCode())); clinic.setDistrictName(regionName(request.getDistrictCode(), request.getDistrictName(), regions)); clinic.setAddress(trimToNull(request.getAddress())); clinic.setBusinessLicenseUrl(trimToNull(request.getBusinessLicenseUrl())); clinic.setStatus(request.getStatus()); clinic.setAgentId(request.getAgentId()); clinic.setBankName(trimToNull(request.getBankName())); clinic.setAccountHolder(trimToNull(request.getAccountHolder())); clinic.setAccountNumber(trimToNull(request.getAccountNumber())); clinic.setBranchName(trimToNull(request.getBranchName())); clinicMapper.updateById(clinic); // 提交事务 transactionManager.commit(status); log.info("诊所本地数据更新成功,clinicId={}", id); } catch (Exception e) { // 发生异常,回滚事务 transactionManager.rollback(status); log.error("诊所本地数据更新失败,clinicId={}", id, e); throw e; } if (request.getAgentId() == null) { return detail(id); } // 事务提交后,调用微赞接口(不回滚本地事务) try { Clinic clinic = clinicMapper.selectById(id); // 调用微赞创建门店 Integer provinceCode = parseRegionCode(clinic.getProvinceCode(), "省编码"); Integer cityCode = parseRegionCode(clinic.getCityCode(), "市编码"); Integer areaCode = parseRegionCode(clinic.getDistrictCode(), "区县编码"); log.info("调用微赞创建门店,入参 title={}, provinceCode={}, cityCode={}, areaCode={}, address={}, telephone={}", clinic.getName(), provinceCode, cityCode, areaCode, clinic.getAddress(), clinic.getContactPhone()); JsonNode vzanStoreResponse = vzanHttpUtil.createStore(clinic.getName(), provinceCode, cityCode, areaCode, clinic.getAddress(), clinic.getContactPhone()); log.info("微赞创建门店成功,出参={}", vzanStoreResponse); // 查询代理信息获取parentMobile ProvincialAgent agent = provincialAgentMapper.selectById(request.getAgentId()); String parentMobile = agent.getContactInfo(); if (parentMobile == null || parentMobile.trim().isEmpty()) { throw new IllegalArgumentException("代理联系方式为空,agentId=" + request.getAgentId()); } // 调用微赞创建代理 log.info("调用微赞创建代理,入参 agentMobile={}, agentName={}, levelCode=1, storeName={}, parentMobile={}", clinic.getContactPhone(), clinic.getName(), clinic.getName(), parentMobile); JsonNode vzanAgentResponse = vzanHttpUtil.createAgent( clinic.getContactPhone(), // agentMobile传门店的手机号 clinic.getName(), // agentName传门店的名称 1, // levelCode传1 parentMobile, // parentMobile传代理的contact_info clinic.getName() // storeName传门店的name ); log.info("微赞创建代理成功,出参={}", vzanAgentResponse); } catch (Exception e) { // 捕获微赞接口异常,不回滚本地事务 log.error("调用微赞接口失败,但本地数据已保存成功,clinicId={}", id, e); throw new IllegalStateException("调用微赞失败", e); } return detail(id); } private Integer parseRegionCode(String code, String fieldName) { if (code == null || code.trim().isEmpty()) { throw new IllegalArgumentException(fieldName + "不能为空"); } try { return Integer.parseInt(code); } catch (NumberFormatException e) { throw new IllegalArgumentException(fieldName + "格式错误: " + code); } } private void addCode(Set codes, String code) { String value = trimToNull(code); if (value != null) { codes.add(value); } } private String regionName(String code, String fallback, Map regions) { String value = trimToNull(code); SysRegion region = value == null ? null : regions.get(value); return region == null ? trimToNull(fallback) : region.getName(); } public PageResponse clinicUsers(Long clinicId, int page, int pageSize, String nickname, String phone, Integer status, Integer isMember, LocalDateTime registeredFrom, LocalDateTime registeredTo) { requireClinic(clinicId); IPage result = clinicQueryMapper.selectClinicUserPage( new Page(page, pageSize), clinicId, trimToNull(nickname), trimToNull(phone), status, isMember, registeredFrom, registeredTo ); List list = new ArrayList(); for (ClinicBoundUserRow row : result.getRecords()) { ClinicBoundUserItemVO item = new ClinicBoundUserItemVO(); item.setId(row.getId()); item.setNickname(defaultText(row.getNickname())); item.setPhone(row.getPhone() == null ? "-" : PhoneMasker.mask(row.getPhone())); item.setAvatar(trimToNull(row.getAvatar())); item.setLivePoints(row.getLivePoints() == null ? 0 : row.getLivePoints()); item.setIsMember(row.getIsMember() != null && row.getIsMember() == 1); item.setStatus(row.getStatus()); item.setCreatedAt(row.getCreatedAt()); list.add(item); } return PageResponse.of(list, result.getTotal(), result.getCurrent(), result.getSize()); } private String trimToNull(String value) { if (value == null) { return null; } String trimmed = value.trim(); return trimmed.isEmpty() ? null : trimmed; } private String defaultText(String value) { return value == null || value.trim().isEmpty() ? "-" : value; } private ClinicListItemVO toListItem(ClinicListRow row) { ClinicListItemVO item = new ClinicListItemVO(); item.setId(row.getId()); item.setName(defaultText(row.getName())); item.setPhone(row.getPhone() == null ? "-" : PhoneMasker.mask(row.getPhone())); item.setRegionName(defaultText(row.getRegionName())); item.setStatus(row.getStatus()); item.setUserCount(row.getUserCount() == null ? 0 : row.getUserCount()); item.setCreatedAt(row.getCreatedAt()); item.setBusinessLicenseUrl(trimToNull(row.getBusinessLicenseUrl())); return item; } private void requireClinic(Long clinicId) { if (clinicQueryMapper.selectClinicDetail(clinicId) == null) { throw new IllegalArgumentException("诊所不存在"); } } private String statusText(Integer status) { if (status != null && status == 1) { return "启用"; } if (status != null && status == 2) { return "注销"; } return "禁用"; } }