BusinessLicenseService.java 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642
  1. package com.xuekairui.user.service;
  2. import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
  3. import com.xuekairui.common.BusinessException;
  4. import com.xuekairui.common.ErrorCode;
  5. import com.xuekairui.common.event.ActivityEventRedisService;
  6. import com.xuekairui.user.dto.BusinessLicenseReviewResponse;
  7. import com.xuekairui.user.entity.BusinessLicense;
  8. import com.xuekairui.user.entity.User;
  9. import com.xuekairui.user.enums.LicenseStatus;
  10. import com.xuekairui.user.enums.MembershipLevel;
  11. import com.xuekairui.user.enums.OperationSource;
  12. import com.xuekairui.user.enums.OperationType;
  13. import com.xuekairui.user.enums.OperatorRole;
  14. import com.xuekairui.user.mapper.BusinessLicenseMapper;
  15. import com.xuekairui.user.mapper.UserMapper;
  16. import lombok.RequiredArgsConstructor;
  17. import lombok.extern.slf4j.Slf4j;
  18. import org.springframework.stereotype.Service;
  19. import org.springframework.transaction.annotation.Transactional;
  20. import org.springframework.transaction.support.TransactionSynchronization;
  21. import org.springframework.transaction.support.TransactionSynchronizationManager;
  22. import java.time.LocalDateTime;
  23. import java.util.HashMap;
  24. import java.util.List;
  25. import java.util.Map;
  26. /**
  27. * 入驻信息服务(封装所有 BusinessLicenseMapper 操作 + 审核业务逻辑)
  28. * <p>
  29. * 用户侧:提交/查询入驻信息
  30. * 运营侧:列表查询、审核通过(赠送会员)、驳回
  31. *
  32. * @author ProPrice Team
  33. * @since 2026-07-09
  34. */
  35. @Slf4j
  36. @Service
  37. @RequiredArgsConstructor
  38. public class BusinessLicenseService {
  39. private final BusinessLicenseMapper businessLicenseMapper;
  40. private final MembershipService membershipService;
  41. private final ActivityEventRedisService activityEventRedisService;
  42. private final OperationAuditLogService auditLogService;
  43. private final UserMapper userMapper;
  44. private final ShopResourceSyncService shopResourceSyncService;
  45. // ======================== 查询方法 ========================
  46. /**
  47. * 按审核状态 + 手机号关键词查询列表(不传则查全部),LEFT JOIN 查提交者信息,一次 SQL
  48. */
  49. public List<BusinessLicenseReviewResponse> listByReviewStatus(String reviewStatus, String keyword) {
  50. if (reviewStatus != null && reviewStatus.isBlank()) {
  51. reviewStatus = null;
  52. }
  53. if (keyword != null && keyword.isBlank()) {
  54. keyword = null;
  55. }
  56. return businessLicenseMapper.selectReviewList(reviewStatus, keyword);
  57. }
  58. /**
  59. * 根据 ID 查询
  60. */
  61. public BusinessLicense getById(Long id) {
  62. return businessLicenseMapper.selectById(id);
  63. }
  64. /**
  65. * 根据用户 ID 查询入驻信息
  66. */
  67. public BusinessLicense getByUserId(Long userId) {
  68. return businessLicenseMapper.selectByUserId(userId);
  69. }
  70. /**
  71. * 批量查询用户入驻状态,返回 userId → reviewStatus
  72. * 未提交的用户不在 Map 中(视为 NOT_SUBMITTED)
  73. */
  74. public Map<Long, String> getLicenseStatusByUserIds(List<Long> userIds) {
  75. if (userIds == null || userIds.isEmpty()) {
  76. return Map.of();
  77. }
  78. List<BusinessLicense> licenses = businessLicenseMapper.selectList(
  79. new LambdaQueryWrapper<BusinessLicense>()
  80. .in(BusinessLicense::getUserId, userIds));
  81. Map<Long, String> result = new HashMap<>();
  82. for (BusinessLicense l : licenses) {
  83. result.put(l.getUserId(), l.getReviewStatus());
  84. }
  85. return result;
  86. }
  87. /**
  88. * 插入新入驻信息(通用方法,供同步等场景使用)
  89. * <p>必须包含 userId,否则拒绝写入,避免产生无主入驻记录。
  90. */
  91. public void insert(BusinessLicense license) {
  92. if (license.getUserId() == null) {
  93. throw new BusinessException(ErrorCode.PARAM_ERROR, "入驻信息缺少提交用户ID,无法写入");
  94. }
  95. businessLicenseMapper.insert(license);
  96. }
  97. /**
  98. * 更新入驻信息(通用方法,供同步等场景使用)
  99. */
  100. public void updateById(BusinessLicense license) {
  101. businessLicenseMapper.updateById(license);
  102. }
  103. /**
  104. * 检查用户是否已有其他审核通过的入驻信息(避免重复赠送会员)
  105. */
  106. private boolean hasOtherApprovedLicense(Long userId, Long excludeLicenseId) {
  107. Long count = businessLicenseMapper.selectCount(
  108. new LambdaQueryWrapper<BusinessLicense>()
  109. .eq(BusinessLicense::getUserId, userId)
  110. .eq(BusinessLicense::getReviewStatus, LicenseStatus.APPROVED.name())
  111. .ne(BusinessLicense::getId, excludeLicenseId));
  112. return count != null && count > 0;
  113. }
  114. // ======================== 用户侧:提交入驻信息 ========================
  115. /**
  116. * 提交或更新入驻信息(用户端)
  117. */
  118. @Transactional
  119. public void saveOrUpdate(Long userId, BusinessLicenseSaveRequest request) {
  120. BusinessLicense existing = getByUserId(userId);
  121. final BusinessLicense targetLicense;
  122. if (existing == null) {
  123. BusinessLicense license = BusinessLicense.builder()
  124. .userId(userId)
  125. .storeName(request.getStoreName())
  126. .terminalType(request.getTerminalType())
  127. .province(request.getProvince())
  128. .city(request.getCity())
  129. .district(request.getDistrict())
  130. .storeAddress(request.getStoreAddress())
  131. .contactPerson(request.getContactPerson())
  132. .contactPhone(request.getContactPhone())
  133. .licenseImageUrl(request.getBusinessLicenseUrl())
  134. .drugLicenseUrl(request.getDrugLicenseUrl())
  135. .creditCode(request.getCreditCode())
  136. .medicalDeviceClass2Url(request.getMedicalDeviceClass2Url())
  137. .medicalDeviceClass3Url(request.getMedicalDeviceClass3Url())
  138. .reviewStatus(LicenseStatus.PENDING.name())
  139. .showVerifiedBadge(false)
  140. .build();
  141. businessLicenseMapper.insert(license);
  142. targetLicense = license;
  143. log.info("用户{}提交入驻信息:id={}", userId, license.getId());
  144. } else {
  145. existing.setStoreName(request.getStoreName());
  146. existing.setTerminalType(request.getTerminalType());
  147. existing.setProvince(request.getProvince());
  148. existing.setCity(request.getCity());
  149. existing.setDistrict(request.getDistrict());
  150. existing.setStoreAddress(request.getStoreAddress());
  151. existing.setContactPerson(request.getContactPerson());
  152. existing.setContactPhone(request.getContactPhone());
  153. existing.setLicenseImageUrl(request.getBusinessLicenseUrl());
  154. existing.setDrugLicenseUrl(request.getDrugLicenseUrl());
  155. if (request.getCreditCode() != null) {
  156. existing.setCreditCode(request.getCreditCode());
  157. }
  158. if (request.getMedicalDeviceClass2Url() != null) {
  159. existing.setMedicalDeviceClass2Url(request.getMedicalDeviceClass2Url());
  160. }
  161. if (request.getMedicalDeviceClass3Url() != null) {
  162. existing.setMedicalDeviceClass3Url(request.getMedicalDeviceClass3Url());
  163. }
  164. existing.setReviewStatus(LicenseStatus.PENDING.name());
  165. businessLicenseMapper.updateById(existing);
  166. targetLicense = existing;
  167. log.info("用户{}更新入驻信息:id={}", userId, existing.getId());
  168. }
  169. // 事务提交后同步客户入驻信息到第三方系统(智价云药店版),失败不阻塞主流程
  170. User submitter = userMapper.selectById(userId);
  171. if (submitter != null) {
  172. TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
  173. @Override
  174. public void afterCommit() {
  175. syncShopResourceToThirdParty(submitter, targetLicense);
  176. }
  177. });
  178. }
  179. }
  180. // ======================== 运营侧:管理员新增入驻 ========================
  181. /**
  182. * 管理员后台新增入驻信息(代客户填写)
  183. * <p>
  184. * 业务规则:
  185. * <ul>
  186. * <li>必须选择一个未提交过入驻信息的客户(t_business_license 中无该 userId 记录)</li>
  187. * <li>必填基础信息 + 营业执照 + 药品经营许可;二类/三类医疗器械至少上传一个</li>
  188. * <li>管理员手动创建的记录状态为 PENDING,需再操作一次审核确认才转为 APPROVED</li>
  189. * <li>审核确认通过后赠送 30 天高级会员并同步第三方(由 approve 方法处理)</li>
  190. * </ul>
  191. *
  192. * @param request 入驻信息(含 userId)
  193. * @param reviewerId 操作管理员 ID
  194. */
  195. @Transactional
  196. public void createByAdmin(BusinessLicenseSaveRequest request, Long reviewerId) {
  197. Long userId = request.getUserId();
  198. if (userId == null) {
  199. throw new BusinessException(ErrorCode.PARAM_ERROR, "必须选择一个客户");
  200. }
  201. // 校验客户存在
  202. User submitter = userMapper.selectById(userId);
  203. if (submitter == null) {
  204. throw new BusinessException(ErrorCode.NOT_FOUND, "客户(ID=" + userId + ")不存在");
  205. }
  206. // 校验该客户未提交过入驻信息
  207. BusinessLicense existing = getByUserId(userId);
  208. if (existing != null) {
  209. throw new BusinessException(ErrorCode.BUSINESS_ERROR,
  210. "该客户已提交过入驻信息,无法重复新增,请使用编辑功能");
  211. }
  212. // 校验资质必填项
  213. validateRequiredQualifications(request);
  214. // 构建入驻记录,状态为 PENDING,需管理员再操作一次审核确认
  215. BusinessLicense license = BusinessLicense.builder()
  216. .userId(userId)
  217. .storeName(request.getStoreName())
  218. .terminalType(request.getTerminalType())
  219. .province(request.getProvince())
  220. .city(request.getCity())
  221. .district(request.getDistrict())
  222. .storeAddress(request.getStoreAddress())
  223. .contactPerson(request.getContactPerson())
  224. .contactPhone(request.getContactPhone())
  225. .licenseImageUrl(request.getBusinessLicenseUrl())
  226. .drugLicenseUrl(request.getDrugLicenseUrl())
  227. .creditCode(request.getCreditCode())
  228. .medicalDeviceClass2Url(request.getMedicalDeviceClass2Url())
  229. .medicalDeviceClass3Url(request.getMedicalDeviceClass3Url())
  230. .reviewStatus(LicenseStatus.PENDING.name())
  231. .showVerifiedBadge(false)
  232. .syncSource("MANUAL")
  233. .status(0)
  234. .build();
  235. businessLicenseMapper.insert(license);
  236. log.info("管理员{}新增入驻信息(待审核确认): licenseId={}, userId={}",
  237. reviewerId, license.getId(), userId);
  238. // 审计日志:管理员新增,记录变更后快照
  239. String userName = submitter.getNickname() != null
  240. ? submitter.getNickname() : String.valueOf(userId);
  241. String storeName = license.getStoreName() != null ? license.getStoreName() : "未知药店";
  242. auditLogService.logSuccess(
  243. reviewerId != null ? reviewerId : 0L,
  244. OperatorRole.ADMIN,
  245. OperationType.LICENSE_CREATE,
  246. "LICENSE:" + license.getId(),
  247. license.getId(),
  248. null,
  249. Map.of("userId", userId, "userName", userName, "storeName", storeName,
  250. "reviewStatus", LicenseStatus.PENDING.name(), "syncSource", "MANUAL"),
  251. String.format("管理员新增客户「%s」(药店:%s)的入驻信息,待审核确认", userName, storeName),
  252. OperationSource.ADMIN_CREATE);
  253. }
  254. /**
  255. * 校验资质必填项
  256. * <p>
  257. * 营业执照、药品经营许可必填;二类/三类医疗器械至少上传一个。
  258. */
  259. private void validateRequiredQualifications(BusinessLicenseSaveRequest request) {
  260. if (isBlank(request.getBusinessLicenseUrl())) {
  261. throw new BusinessException(ErrorCode.PARAM_ERROR, "营业执照图片不能为空");
  262. }
  263. if (isBlank(request.getDrugLicenseUrl())) {
  264. throw new BusinessException(ErrorCode.PARAM_ERROR, "药品经营许可证不能为空");
  265. }
  266. boolean hasClass2 = !isBlank(request.getMedicalDeviceClass2Url());
  267. boolean hasClass3 = !isBlank(request.getMedicalDeviceClass3Url());
  268. if (!hasClass2 && !hasClass3) {
  269. throw new BusinessException(ErrorCode.PARAM_ERROR,
  270. "必须上传二类或三类医疗器械资质(二选一)");
  271. }
  272. }
  273. private static boolean isBlank(String s) {
  274. return s == null || s.isBlank();
  275. }
  276. // ======================== 运营侧:管理员编辑入驻 ========================
  277. /**
  278. * 管理员编辑入驻信息(可修改客户信息、替换资质图片,保存后即时生效)
  279. * <p>
  280. * 业务规则:
  281. * <ul>
  282. * <li>可编辑全部入驻数据(含已审核 APPROVED),不受 PENDING 限制</li>
  283. * <li>必填项约束同样生效:营业执照、药品经营许可必填,二类/三类至少一个</li>
  284. * <li>资质图片覆盖原内容(非空即覆盖,空值保留原值)</li>
  285. * <li>记录变更前/变更后快照,便于追溯</li>
  286. * <li>事务提交后同步到第三方系统,失败不阻塞主流程</li>
  287. * </ul>
  288. *
  289. * @param licenseId 入驻信息 ID
  290. * @param request 编辑后的入驻信息
  291. * @param reviewerId 操作管理员 ID
  292. */
  293. @Transactional
  294. public void editByAdmin(Long licenseId, BusinessLicenseSaveRequest request, Long reviewerId) {
  295. BusinessLicense license = getById(licenseId);
  296. if (license == null) {
  297. throw new BusinessException(ErrorCode.NOT_FOUND, "入驻信息记录不存在");
  298. }
  299. // 校验资质必填项
  300. validateRequiredQualifications(request);
  301. // 变更前快照
  302. Map<String, Object> beforeData = buildSnapshot(license);
  303. Long userId = license.getUserId();
  304. User submitter = userMapper.selectById(userId);
  305. if (submitter == null) {
  306. throw new BusinessException(ErrorCode.NOT_FOUND, "客户(ID=" + userId + ")不存在");
  307. }
  308. // 覆盖更新所有字段(资质图片非空即覆盖)
  309. license.setStoreName(request.getStoreName());
  310. license.setTerminalType(request.getTerminalType());
  311. license.setProvince(request.getProvince());
  312. license.setCity(request.getCity());
  313. license.setDistrict(request.getDistrict());
  314. license.setStoreAddress(request.getStoreAddress());
  315. license.setContactPerson(request.getContactPerson());
  316. license.setContactPhone(request.getContactPhone());
  317. license.setLicenseImageUrl(request.getBusinessLicenseUrl());
  318. license.setDrugLicenseUrl(request.getDrugLicenseUrl());
  319. if (request.getCreditCode() != null) {
  320. license.setCreditCode(request.getCreditCode());
  321. }
  322. if (request.getMedicalDeviceClass2Url() != null) {
  323. license.setMedicalDeviceClass2Url(request.getMedicalDeviceClass2Url());
  324. }
  325. if (request.getMedicalDeviceClass3Url() != null) {
  326. license.setMedicalDeviceClass3Url(request.getMedicalDeviceClass3Url());
  327. }
  328. license.setReviewerId(reviewerId);
  329. license.setReviewTime(LocalDateTime.now());
  330. businessLicenseMapper.updateById(license);
  331. log.info("管理员{}编辑入驻信息: licenseId={}, userId={}", reviewerId, licenseId, userId);
  332. // 变更后快照
  333. Map<String, Object> afterData = buildSnapshot(license);
  334. // 事务提交后同步到第三方系统
  335. TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
  336. @Override
  337. public void afterCommit() {
  338. syncShopResourceToThirdParty(submitter, license);
  339. }
  340. });
  341. // 审计日志:管理员编辑,记录变更前/后快照
  342. String userName = submitter.getNickname() != null
  343. ? submitter.getNickname() : String.valueOf(userId);
  344. String storeName = license.getStoreName() != null ? license.getStoreName() : "未知药店";
  345. auditLogService.logSuccess(
  346. reviewerId != null ? reviewerId : 0L,
  347. OperatorRole.ADMIN,
  348. OperationType.LICENSE_EDIT,
  349. "LICENSE:" + license.getId(),
  350. license.getId(),
  351. beforeData,
  352. afterData,
  353. String.format("管理员编辑客户「%s」(药店:%s)的入驻信息", userName, storeName),
  354. OperationSource.ADMIN_CREATE);
  355. }
  356. /**
  357. * 构建入驻信息快照(用于审计日志的变更前/后对比)
  358. */
  359. private Map<String, Object> buildSnapshot(BusinessLicense license) {
  360. Map<String, Object> snapshot = new HashMap<>();
  361. snapshot.put("userId", license.getUserId());
  362. snapshot.put("storeName", license.getStoreName());
  363. snapshot.put("terminalType", license.getTerminalType());
  364. snapshot.put("province", license.getProvince());
  365. snapshot.put("city", license.getCity());
  366. snapshot.put("district", license.getDistrict());
  367. snapshot.put("storeAddress", license.getStoreAddress());
  368. snapshot.put("contactPerson", license.getContactPerson());
  369. snapshot.put("contactPhone", license.getContactPhone());
  370. snapshot.put("creditCode", license.getCreditCode());
  371. snapshot.put("businessLicenseUrl", license.getLicenseImageUrl());
  372. snapshot.put("drugLicenseUrl", license.getDrugLicenseUrl());
  373. snapshot.put("medicalDeviceClass2Url", license.getMedicalDeviceClass2Url());
  374. snapshot.put("medicalDeviceClass3Url", license.getMedicalDeviceClass3Url());
  375. snapshot.put("reviewStatus", license.getReviewStatus());
  376. snapshot.put("showVerifiedBadge", license.getShowVerifiedBadge());
  377. return snapshot;
  378. }
  379. // ======================== 运营侧:审核操作 ========================
  380. /**
  381. * 审核通过
  382. * <p>
  383. * 更新审核状态为 APPROVED,展示认证标识。首次审核通过时赠送 30 天高级会员,
  384. * 并推送 Redis 事件供活动模块消费(邀请有礼:被邀请人资质通过后发邀请人奖励)。
  385. *
  386. * @param licenseId 入驻信息 ID
  387. * @param reviewerId 审核人 ID
  388. */
  389. @Transactional
  390. public void approve(Long licenseId, Long reviewerId) {
  391. BusinessLicense license = validateReviewable(licenseId);
  392. // 提交用户必须存在,否则拒绝审核
  393. Long userId = license.getUserId();
  394. User submitter = userMapper.selectById(userId);
  395. if (submitter == null) {
  396. throw new BusinessException(ErrorCode.NOT_FOUND, "提交用户(ID=" + userId + ")不存在,无法审核");
  397. }
  398. license.setReviewStatus(LicenseStatus.APPROVED.name());
  399. license.setReviewerId(reviewerId);
  400. license.setReviewTime(LocalDateTime.now());
  401. license.setShowVerifiedBadge(true);
  402. businessLicenseMapper.updateById(license);
  403. // 首次审核通过,赠送 30 天高级会员
  404. if (!hasOtherApprovedLicense(userId, licenseId)) {
  405. membershipService.grantOrExtendMembership(
  406. userId,
  407. MembershipLevel.PRO,
  408. 30,
  409. "LICENSE",
  410. "入驻信息审核通过赠送30天高级会员");
  411. log.info("入驻信息审核通过,赠送用户{} 30天高级会员", userId);
  412. // 事务提交后推送 Redis 事件,供活动模块消费
  413. TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
  414. @Override
  415. public void afterCommit() {
  416. try {
  417. activityEventRedisService.pushLicenseApproved(userId, license.getId(), "BUSINESS_LICENSE");
  418. } catch (Exception e) {
  419. log.warn("存入入驻审核通过事件失败,不影响主流程: userId={}", userId, e);
  420. }
  421. }
  422. });
  423. }
  424. // 事务提交后同步客户入驻信息到第三方系统(智价云药店版),失败不阻塞主流程
  425. TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
  426. @Override
  427. public void afterCommit() {
  428. syncShopResourceToThirdParty(submitter, license);
  429. }
  430. });
  431. // 审计日志:审核通过,记录客户信息
  432. String userName = submitter.getNickname() != null
  433. ? submitter.getNickname() : String.valueOf(userId);
  434. String storeName = license.getStoreName() != null ? license.getStoreName() : "未知药店";
  435. auditLogService.logSuccess(
  436. reviewerId != null ? reviewerId : 0L,
  437. OperatorRole.ADMIN,
  438. OperationType.LICENSE_APPROVE,
  439. "LICENSE:" + license.getId(),
  440. license.getId(),
  441. null,
  442. Map.of("userId", userId, "userName", userName, "storeName", storeName),
  443. String.format("审核通过客户「%s」(药店:%s)的入驻申请", userName, storeName),
  444. OperationSource.ADMIN_CREATE);
  445. log.info("入驻审核通过: licenseId={}, userId={}, userName={}, reviewerId={}",
  446. licenseId, userId, userName, reviewerId);
  447. }
  448. /**
  449. * 同步客户入驻信息到第三方系统(智价云药店版)
  450. * <p>
  451. * 将客户手机号 + 店铺信息推送到第三方接口,第三方返回该客户的访问令牌。
  452. * 在入驻新增、审核驳回、审核通过三个环节均触发同步,确保第三方实时获取入驻状态变更。
  453. * 同步成功后将第三方 user_id 回写到入驻记录的 externalId 字段,并标记 status=1。
  454. * 第三方不可用或同步失败时标记 status=2,不影响本地主流程,
  455. * 后续可通过 status 筛选失败记录进行补推。
  456. */
  457. private void syncShopResourceToThirdParty(User submitter, BusinessLicense license) {
  458. try {
  459. ShopResourceSyncService.SyncResult result =
  460. shopResourceSyncService.syncShopResource(submitter, license);
  461. if (result != null && result.getUserId() != null) {
  462. license.setExternalId(result.getUserId());
  463. license.setStatus(1);
  464. businessLicenseMapper.updateById(license);
  465. log.info("同步客户入驻信息到第三方成功: licenseId={}, externalUserId={}, newUser={}",
  466. license.getId(), result.getUserId(), result.getNewUser());
  467. } else {
  468. license.setStatus(2);
  469. businessLicenseMapper.updateById(license);
  470. log.warn("同步客户入驻信息到第三方未成功(第三方未返回user_id),标记为2: licenseId={}",
  471. license.getId());
  472. }
  473. } catch (Exception e) {
  474. license.setStatus(2);
  475. businessLicenseMapper.updateById(license);
  476. log.warn("同步客户入驻信息到第三方失败,标记为2,不影响主流程: licenseId={}, error={}",
  477. license.getId(), e.getMessage());
  478. }
  479. }
  480. /**
  481. * 审核驳回
  482. * <p>
  483. * 业务规则:
  484. * <ul>
  485. * <li>只能对 PENDING(新提交/未审核通过)的记录驳回,APPROVED 不允许驳回</li>
  486. * <li>驳回原因必填</li>
  487. * <li>驳回后状态为 REJECTED,客户端/后台客户详情同步移除该条入驻展示</li>
  488. * <li>用户可重新上传提交(saveOrUpdate 会将状态改回 PENDING)</li>
  489. * <li>记录变更前/后快照,便于追溯</li>
  490. * <li>事务提交后同步到第三方系统,失败不阻塞主流程</li>
  491. * </ul>
  492. *
  493. * @param licenseId 入驻信息 ID
  494. * @param reviewerId 审核人 ID
  495. * @param rejectReason 驳回原因(必填)
  496. */
  497. @Transactional
  498. public void reject(Long licenseId, Long reviewerId, String rejectReason) {
  499. // 驳回原因必填
  500. if (rejectReason == null || rejectReason.isBlank()) {
  501. throw new BusinessException(ErrorCode.PARAM_ERROR, "驳回原因不能为空");
  502. }
  503. BusinessLicense license = validateReviewable(licenseId);
  504. // 审核通过后不允许驳回
  505. if (LicenseStatus.APPROVED.name().equals(license.getReviewStatus())) {
  506. throw new BusinessException(ErrorCode.BAD_REQUEST, "该入驻信息已审核通过,不允许驳回");
  507. }
  508. // 变更前快照
  509. Map<String, Object> beforeData = buildSnapshot(license);
  510. // 提交用户必须存在
  511. Long userId = license.getUserId();
  512. User submitter = userMapper.selectById(userId);
  513. if (submitter == null) {
  514. throw new BusinessException(ErrorCode.NOT_FOUND, "提交用户(ID=" + userId + ")不存在,无法审核");
  515. }
  516. license.setReviewStatus(LicenseStatus.REJECTED.name());
  517. license.setReviewerId(reviewerId);
  518. license.setReviewTime(LocalDateTime.now());
  519. license.setShowVerifiedBadge(false);
  520. license.setRejectReason(rejectReason);
  521. businessLicenseMapper.updateById(license);
  522. // 变更后快照
  523. Map<String, Object> afterData = buildSnapshot(license);
  524. // 审计日志:审核驳回,记录变更前/后快照
  525. String userName = submitter.getNickname() != null
  526. ? submitter.getNickname() : String.valueOf(userId);
  527. String storeName = license.getStoreName() != null ? license.getStoreName() : "未知药店";
  528. auditLogService.logSuccess(
  529. reviewerId != null ? reviewerId : 0L,
  530. OperatorRole.ADMIN,
  531. OperationType.LICENSE_REJECT,
  532. "LICENSE:" + license.getId(),
  533. license.getId(),
  534. beforeData,
  535. afterData,
  536. String.format("驳回客户「%s」(药店:%s)的入驻申请,原因:%s",
  537. userName, storeName, rejectReason),
  538. OperationSource.ADMIN_CREATE);
  539. log.info("入驻审核驳回: licenseId={}, userId={}, userName={}, reviewerId={}, reason={}",
  540. licenseId, userId, userName, reviewerId, rejectReason);
  541. // 事务提交后同步客户入驻信息到第三方系统(智价云药店版),失败不阻塞主流程
  542. TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
  543. @Override
  544. public void afterCommit() {
  545. syncShopResourceToThirdParty(submitter, license);
  546. }
  547. });
  548. }
  549. /**
  550. * 校验入驻信息是否可审核(存在 + 状态为 PENDING)
  551. */
  552. private BusinessLicense validateReviewable(Long licenseId) {
  553. BusinessLicense license = getById(licenseId);
  554. if (license == null) {
  555. throw new BusinessException(ErrorCode.NOT_FOUND, "入驻信息记录不存在");
  556. }
  557. if (!LicenseStatus.PENDING.name().equals(license.getReviewStatus())) {
  558. throw new BusinessException(ErrorCode.BAD_REQUEST, "该入驻信息已审核,无法重复操作");
  559. }
  560. return license;
  561. }
  562. // ======================== 请求 DTO ========================
  563. /**
  564. * 入驻信息提交请求参数(用于 Service 层,避免依赖 Controller 层 DTO)
  565. */
  566. @lombok.Data
  567. @lombok.Builder
  568. @lombok.NoArgsConstructor
  569. @lombok.AllArgsConstructor
  570. public static class BusinessLicenseSaveRequest {
  571. /** 客户用户ID(管理员新增时必填,用户自主提交时由 Service 注入) */
  572. private Long userId;
  573. private String storeName;
  574. private String terminalType;
  575. private String province;
  576. private String city;
  577. private String district;
  578. private String storeAddress;
  579. private String contactPerson;
  580. private String contactPhone;
  581. private String businessLicenseUrl;
  582. private String drugLicenseUrl;
  583. private String creditCode;
  584. private String medicalDeviceClass2Url;
  585. private String medicalDeviceClass3Url;
  586. }
  587. }