|
|
@@ -0,0 +1,187 @@
|
|
|
+package com.ruoyi.app.stall.qualification;
|
|
|
+
|
|
|
+import com.ruoyi.common.exception.ServiceException;
|
|
|
+import com.ruoyi.common.utils.MessageUtils;
|
|
|
+import com.ruoyi.system.domain.*;
|
|
|
+import com.ruoyi.system.mapper.InfoUserMapper;
|
|
|
+import com.ruoyi.system.mapper.PosStoreMapper;
|
|
|
+import com.ruoyi.system.mapper.StallQualificationMapper;
|
|
|
+import org.springframework.stereotype.Service;
|
|
|
+import org.springframework.transaction.annotation.Transactional;
|
|
|
+import java.util.Date;
|
|
|
+import java.util.List;
|
|
|
+import java.util.Objects;
|
|
|
+import static com.ruoyi.system.domain.StallQualificationApplication.*;
|
|
|
+
|
|
|
+/** 摊位级材料和审核事务;不复用账号 auditStatus,不影响已产生订单的履约。 */
|
|
|
+@Service
|
|
|
+public class StallQualificationService {
|
|
|
+ private final StallQualificationMapper mapper;
|
|
|
+ private final InfoUserMapper users;
|
|
|
+ private final PosStoreMapper stores;
|
|
|
+ private final StallQualificationFileService files;
|
|
|
+ public StallQualificationService(StallQualificationMapper mapper, InfoUserMapper users,
|
|
|
+ PosStoreMapper stores, StallQualificationFileService files) {
|
|
|
+ this.mapper = mapper; this.users = users; this.stores = stores; this.files = files;
|
|
|
+ }
|
|
|
+
|
|
|
+ public Long ownerStoreId(Long userId) {
|
|
|
+ InfoUser user = requireUser(userId, "4");
|
|
|
+ PosStore store = user.getStoreId() == null ? null : stores.selectPosStoreById(user.getStoreId());
|
|
|
+ requireStall(store);
|
|
|
+ return user.getStoreId();
|
|
|
+ }
|
|
|
+ private InfoUser requireUser(Long userId, String type) {
|
|
|
+ InfoUser user = userId == null ? null : users.selectInfoUserByUserId(userId);
|
|
|
+ if (user == null || !type.equals(user.getUserType()) || !"0".equals(user.getStatus()) || !"0".equals(user.getDelFlag())) {
|
|
|
+ throw error("merchant.store.access.denied");
|
|
|
+ }
|
|
|
+ return user;
|
|
|
+ }
|
|
|
+ private void requireStall(PosStore store) {
|
|
|
+ if (store == null || !Integer.valueOf(1).equals(store.getIsStall()) || !"0".equals(store.getDelFlag())) {
|
|
|
+ throw error("merchant.store.access.denied");
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 读操作不创建草稿,未填写的存量摊位明确返回 version=0、空申请。 */
|
|
|
+ @Transactional(readOnly = true)
|
|
|
+ public QualificationView ownerView(Long userId) {
|
|
|
+ Long storeId = ownerStoreId(userId);
|
|
|
+ return view(stores.selectPosStoreById(storeId), mapper.selectByStoreId(storeId));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Transactional(rollbackFor = Exception.class)
|
|
|
+ public QualificationView save(Long userId, QualificationSaveRequest request, boolean submit) {
|
|
|
+ Long storeId = ownerStoreId(userId);
|
|
|
+ if (request == null || request.getVersion() == null || request.getVersion() < 0) { throw error("stall.qualification.version.required"); }
|
|
|
+ PosStore store = stores.selectPosStoreById(storeId);
|
|
|
+ // 唯一键初始化 + 主体行锁:多个摊主账号、首次并发提交也只会产生一个待审核版本。
|
|
|
+ mapper.ensureMaster(storeId);
|
|
|
+ StallQualification qualification = mapper.lockByStoreId(storeId);
|
|
|
+ if (qualification == null || !Objects.equals(qualification.getVersion(), request.getVersion())) { throw conflict(); }
|
|
|
+ StallQualificationApplication previous = qualification.getCurrentApplicationId() == null ? null
|
|
|
+ : mapper.selectApplication(qualification.getCurrentApplicationId());
|
|
|
+ if (previous != null && Integer.valueOf(PENDING).equals(previous.getStatus())) { throw error("stall.qualification.pending"); }
|
|
|
+ StallQualificationApplication application = new StallQualificationApplication();
|
|
|
+ application.setQualificationId(qualification.getId()); application.setStoreId(storeId);
|
|
|
+ application.setResponsibleName(text(request.getResponsibleName(), 100));
|
|
|
+ application.setContactPhone(text(request.getContactPhone(), 32));
|
|
|
+ application.setIdentityNumber(text(request.getIdentityNumber(), 64));
|
|
|
+ application.setFoodRegistrationNumber(text(request.getFoodRegistrationNumber(), 64));
|
|
|
+ application.setIdentityImageUrls(images(storeId, request.getIdentityImageUrls()));
|
|
|
+ application.setStallImageUrls(images(storeId, request.getStallImageUrls()));
|
|
|
+ application.setFoodRegistrationImageUrls(images(storeId, request.getFoodRegistrationImageUrls()));
|
|
|
+ application.setStallNameSnapshot(store.getPosName()); application.setStallAddressSnapshot(store.getAddress());
|
|
|
+ application.setNightMarketIdSnapshot(store.getNightMarketId());
|
|
|
+ application.setStatus(submit ? PENDING : DRAFT);
|
|
|
+ application.setVersion(0L); application.setCreateTime(new Date()); application.setUpdateTime(new Date());
|
|
|
+ if (submit) {
|
|
|
+ requireMaterials(application);
|
|
|
+ application.setSubmittedBy(userId); application.setSubmittedAt(new Date());
|
|
|
+ }
|
|
|
+ if (previous != null && Integer.valueOf(DRAFT).equals(previous.getStatus())) {
|
|
|
+ application.setId(previous.getId()); application.setApplicationVersion(previous.getApplicationVersion());
|
|
|
+ application.setVersion(previous.getVersion()); application.setCreateTime(previous.getCreateTime());
|
|
|
+ if (mapper.updateDraft(application) != 1) { throw conflict(); }
|
|
|
+ application.setVersion(application.getVersion() + 1);
|
|
|
+ } else {
|
|
|
+ // 已提交/已审核材料保持不可变;通过后修改或驳回重提,都建立下一个材料版本。
|
|
|
+ application.setApplicationVersion(previous == null ? 1 : previous.getApplicationVersion() + 1);
|
|
|
+ if (mapper.insertApplication(application) != 1 || application.getId() == null) { throw conflict(); }
|
|
|
+ }
|
|
|
+ qualification.setCurrentApplicationId(application.getId());
|
|
|
+ bumpMaster(qualification);
|
|
|
+ QualificationView view = view(store, qualification);
|
|
|
+ view.setCurrentApplication(application);
|
|
|
+ return view;
|
|
|
+ }
|
|
|
+
|
|
|
+ public StallQualificationApplication applicationForOwner(Long userId, Long id) {
|
|
|
+ Long storeId = ownerStoreId(userId);
|
|
|
+ StallQualificationApplication application = mapper.selectApplication(id);
|
|
|
+ if (application == null || !storeId.equals(application.getStoreId())) { throw error("merchant.store.access.denied"); }
|
|
|
+ return application;
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 夜市主仅看到进度,材料和身份字段从查询源头就不返回。 */
|
|
|
+ public List<StallQualificationSummary> marketProgress(Long userId) {
|
|
|
+ requireUser(userId, "3"); return mapper.selectMarketProgress(userId);
|
|
|
+ }
|
|
|
+
|
|
|
+ public StallQualificationApplication adminApplication(Long id) {
|
|
|
+ StallQualificationApplication application = id == null ? null : mapper.selectApplication(id);
|
|
|
+ if (application == null) { throw error("stall.qualification.not.found"); }
|
|
|
+ return application;
|
|
|
+ }
|
|
|
+ public List<StallQualificationSummary> adminList(int pageNum, int pageSize, Integer status, Long storeId) {
|
|
|
+ checkPage(pageNum, pageSize, status);
|
|
|
+ return mapper.selectApplications(status, storeId, (pageNum - 1) * pageSize, pageSize);
|
|
|
+ }
|
|
|
+ public long adminCount(Integer status, Long storeId) { return mapper.countApplications(status, storeId); }
|
|
|
+ private void checkPage(int pageNum, int pageSize, Integer status) {
|
|
|
+ if (pageNum < 1 || pageSize < 1 || pageSize > 100 || (long) (pageNum - 1) * pageSize > Integer.MAX_VALUE
|
|
|
+ || (status != null && (status < DRAFT || status > REJECTED))) { throw error("stall.qualification.input.invalid"); }
|
|
|
+ }
|
|
|
+
|
|
|
+ @Transactional(rollbackFor = Exception.class)
|
|
|
+ public void review(Long reviewerId, Long id, QualificationReviewRequest request) {
|
|
|
+ if (request == null || request.getVersion() == null || request.getApproved() == null) { throw error("stall.qualification.input.invalid"); }
|
|
|
+ String reason = text(request.getRejectReason(), 500);
|
|
|
+ if (!request.getApproved() && reason.isEmpty()) { throw error("stall.qualification.reason.required"); }
|
|
|
+ StallQualificationApplication initial = adminApplication(id);
|
|
|
+ // 和保存/提交使用同一主体锁;锁后重读防止读到另一个管理员操作前的旧状态。
|
|
|
+ StallQualification qualification = mapper.lockByStoreId(initial.getStoreId());
|
|
|
+ StallQualificationApplication application = adminApplication(id);
|
|
|
+ if (qualification == null || !Objects.equals(id, qualification.getCurrentApplicationId())
|
|
|
+ || !Integer.valueOf(PENDING).equals(application.getStatus())
|
|
|
+ || !Objects.equals(application.getVersion(), request.getVersion())) { throw conflict(); }
|
|
|
+ requireStall(stores.selectPosStoreById(application.getStoreId()));
|
|
|
+ application.setStatus(request.getApproved() ? APPROVED : REJECTED);
|
|
|
+ application.setReviewedBy(reviewerId); application.setReviewedAt(new Date());
|
|
|
+ application.setRejectReason(request.getApproved() ? null : reason);
|
|
|
+ if (mapper.reviewApplication(application) != 1) { throw conflict(); }
|
|
|
+ // 新申请驳回不能抹掉旧有效版本;审核通过只切换有效指针,不自动替商家开店。
|
|
|
+ if (request.getApproved()) { qualification.setApprovedApplicationId(id); }
|
|
|
+ bumpMaster(qualification);
|
|
|
+ }
|
|
|
+
|
|
|
+ private QualificationView view(PosStore store, StallQualification qualification) {
|
|
|
+ QualificationView view = new QualificationView();
|
|
|
+ Long storeId = store.getId().longValue();
|
|
|
+ view.setStoreId(storeId); view.setStallName(store.getPosName()); view.setStallAddress(store.getAddress());
|
|
|
+ view.setNightMarketId(store.getNightMarketId()); view.setVersion(qualification == null ? 0L : qualification.getVersion());
|
|
|
+ if (qualification != null) {
|
|
|
+ if (qualification.getCurrentApplicationId() != null) { view.setCurrentApplication(mapper.selectApplication(qualification.getCurrentApplicationId())); }
|
|
|
+ if (qualification.getApprovedApplicationId() != null) { view.setApprovedApplication(mapper.selectApplication(qualification.getApprovedApplicationId())); }
|
|
|
+ }
|
|
|
+ view.setHistory(mapper.selectHistory(storeId)); return view;
|
|
|
+ }
|
|
|
+ private void bumpMaster(StallQualification qualification) {
|
|
|
+ if (mapper.updateMaster(qualification) != 1) { throw conflict(); }
|
|
|
+ qualification.setVersion(qualification.getVersion() + 1);
|
|
|
+ }
|
|
|
+ private List<String> images(Long storeId, List<String> references) {
|
|
|
+ if (references == null) { return List.of(); }
|
|
|
+ if (references.size() > 5) { throw error("stall.qualification.input.invalid"); }
|
|
|
+ for (String reference : references) { files.requireReference(storeId, reference); }
|
|
|
+ return List.copyOf(references);
|
|
|
+ }
|
|
|
+ private void requireMaterials(StallQualificationApplication application) {
|
|
|
+ if (application.getResponsibleName().isEmpty() || application.getContactPhone().isEmpty()
|
|
|
+ || application.getIdentityNumber().isEmpty() || application.getFoodRegistrationNumber().isEmpty()
|
|
|
+ || application.getIdentityImageUrls().isEmpty() || application.getStallImageUrls().isEmpty()
|
|
|
+ || application.getFoodRegistrationImageUrls().isEmpty()
|
|
|
+ || application.getStallNameSnapshot() == null || application.getStallNameSnapshot().isBlank()
|
|
|
+ || application.getStallAddressSnapshot() == null || application.getStallAddressSnapshot().isBlank()) {
|
|
|
+ throw error("stall.qualification.materials.required");
|
|
|
+ }
|
|
|
+ }
|
|
|
+ private String text(String value, int max) {
|
|
|
+ String normalized = value == null ? "" : value.trim();
|
|
|
+ if (normalized.length() > max) { throw error("stall.qualification.input.invalid"); }
|
|
|
+ return normalized;
|
|
|
+ }
|
|
|
+ private ServiceException conflict() { return error("stall.qualification.conflict"); }
|
|
|
+ private ServiceException error(String key) { return new ServiceException(MessageUtils.message(key)); }
|
|
|
+}
|