rk
17 小时以前 33caf2bb79bb3c561916c91ae386ec772411e2e8
server/services/src/main/java/com/doumee/service/business/impl/ShopInfoServiceImpl.java
@@ -14,6 +14,7 @@
import com.doumee.core.utils.Utils;
import com.doumee.biz.system.AreasBiz;
import com.doumee.biz.system.SystemDictDataBiz;
import com.doumee.core.utils.aliyun.AliSmsService;
import com.doumee.dao.business.MemberMapper;
import com.doumee.dao.business.MultifileMapper;
import com.doumee.dao.business.OrdersMapper;
@@ -25,16 +26,27 @@
import com.doumee.dao.business.model.Orders;
import com.doumee.dao.business.model.PricingRule;
import com.doumee.dao.business.model.ShopInfo;
import com.doumee.config.xyy.XyyConfig;
import com.doumee.config.xyy.dto.AddPrinterRequest;
import com.doumee.config.xyy.dto.AddPrinterRequestItem;
import com.doumee.config.xyy.dto.DelPrinterRequest;
import com.doumee.config.xyy.vo.ObjectRestResponse;
import com.doumee.config.xyy.vo.PrinterResult;
import com.doumee.dao.dto.*;
import com.doumee.dao.system.model.SystemDictData;
import com.doumee.dao.system.SystemUserMapper;
import com.doumee.dao.system.model.SystemUser;
import com.doumee.dao.vo.ShopDetailVO;
import com.doumee.dao.vo.ShopCenterVO;
import com.doumee.dao.vo.ShopLoginVO;
import com.doumee.dao.vo.ShopNearbyVO;
import com.doumee.dao.vo.ShopSalesStatsVO;
import com.doumee.dao.vo.ShopWebDetailVO;
import com.doumee.service.business.AreasService;
import com.doumee.service.business.ShopInfoService;
import com.doumee.dao.business.SmsrecordMapper;
import com.doumee.dao.business.model.Smsrecord;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.RandomStringUtils;
@@ -51,6 +63,7 @@
 * @author rk
 * @date 2026/04/08
 */
@Slf4j
@Service
public class ShopInfoServiceImpl implements ShopInfoService {
@@ -83,6 +96,15 @@
    @Autowired
    private AreasService areasService;
    @Autowired
    private SmsrecordMapper smsrecordMapper;
    @Autowired
    private PrintService printService;
    @Autowired
    private XyyConfig xyyConfig;
    @Override
    public Integer create(ShopInfo shopInfo) {
        shopInfoMapper.insert(shopInfo);
@@ -197,6 +219,9 @@
        if (pageWrap.getModel().getAreaId() != null) {
            queryWrapper.lambda().eq(ShopInfo::getAreaId, pageWrap.getModel().getAreaId());
        }
        if (pageWrap.getModel().getAddress() != null) {
            queryWrapper.lambda().like(ShopInfo::getAddress, pageWrap.getModel().getAddress());
        }
        if (pageWrap.getModel().getAuditStatus() != null) {
            queryWrapper.lambda().eq(ShopInfo::getAuditStatus, pageWrap.getModel().getAuditStatus());
        }
@@ -216,13 +241,7 @@
        if (pageWrap.getModel().getOpenid() != null) {
            queryWrapper.lambda().like(ShopInfo::getOpenid, pageWrap.getModel().getOpenid());
        }
        for (PageWrap.SortData sortData : pageWrap.getSorts()) {
            if (sortData.getDirection().equalsIgnoreCase(PageWrap.DESC)) {
                queryWrapper.orderByDesc(sortData.getProperty());
            } else {
                queryWrapper.orderByAsc(sortData.getProperty());
            }
        }
        queryWrapper.lambda().orderByDesc(ShopInfo::getId);
        return PageData.from(shopInfoMapper.selectPage(page, queryWrapper));
    }
@@ -239,10 +258,105 @@
    public void applyShop(ShopApplyDTO request) {
        Member member = memberMapper.selectById(request.getMemberId());
        // 根据类型校验附件
        validateCompanyTypeFields(request);
        // 2. 根据类型校验附件
        Date now = new Date();
        // 查询该会员最新的变更版本记录
        QueryWrapper<ShopInfo> changeQw = new QueryWrapper<>();
        changeQw.lambda()
                .eq(ShopInfo::getRegionMemberId, member.getId())
                .eq(ShopInfo::getVersionType, Constants.ONE)
                .eq(ShopInfo::getDeleted, Constants.ZERO)
                .orderByDesc(ShopInfo::getCreateTime)
                .last("limit 1");
        ShopInfo changeVersion = shopInfoMapper.selectOne(changeQw);
        if (changeVersion == null) {
            // 首次申请:创建正式版本 + 变更版本
            checkTelephoneUnique(request.getTelephone(), null);
            String rawPassword = generateDefaultPassword(request.getTelephone());
            String salt = RandomStringUtils.randomAlphabetic(6);
            String encryptedPassword = Utils.Secure.encryptPassword(rawPassword, salt);
            // 正式版本
            ShopInfo official = new ShopInfo();
            applyFieldsFromRequest(official, request, member, encryptedPassword, salt, now);
            official.setVersionType(Constants.ZERO);
            official.setAuditStatus(Constants.ZERO);
            official.setStatus(Constants.ZERO);
            official.setDeleted(Constants.ZERO);
            official.setCreateTime(now);
            official.setUpdateTime(now);
            official.setRegionMemberId(member.getId());
            setDepositAmountFromPricingRule(official);
            setDefaultDeliveryRange(official);
            shopInfoMapper.insert(official);
            // 保存正式版本附件
            saveShopAttachments(official.getId(), request, now);
            // 创建变更版本(拷贝正式版本数据)
            createChangeVersion(official, official.getId(), now);
        } else if (Constants.equalsInteger(changeVersion.getAuditStatus(), Constants.ONE)) {
            // 审批通过待支付押金
            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "门店已审批通过,请完成押金支付");
        } else if (Constants.equalsInteger(changeVersion.getAuditStatus(), Constants.THREE)) {
            // 最新变更版本已支付押金(status=3):生成新的变更版本
            Integer relationShopId = changeVersion.getRelationShopId();
            // 校验手机号唯一性(排除正式版本和变更版本)
            checkTelephoneUnique(request.getTelephone(), relationShopId);
            String rawPassword = generateDefaultPassword(request.getTelephone());
            String salt = RandomStringUtils.randomAlphabetic(6);
            String encryptedPassword = Utils.Secure.encryptPassword(rawPassword, salt);
            ShopInfo newChange = new ShopInfo();
            applyFieldsFromRequest(newChange, request, member, encryptedPassword, salt, now);
            newChange.setVersionType(Constants.ONE);
            newChange.setRelationShopId(relationShopId);
            newChange.setAuditStatus(Constants.ZERO);
            newChange.setStatus(Constants.ZERO);
            newChange.setDeleted(Constants.ZERO);
            newChange.setCreateTime(now);
            newChange.setUpdateTime(now);
            newChange.setRegionMemberId(member.getId());
            setDepositAmountFromPricingRule(newChange);
            shopInfoMapper.insert(newChange);
            // 保存新变更版本附件
            saveShopAttachments(newChange.getId(), request, now);
        } else {
            // 最新变更版本 status=0(待审批) 或 2(被驳回):直接更新
            Integer relationShopId = changeVersion.getRelationShopId();
            // 校验手机号唯一性
            checkTelephoneUnique(request.getTelephone(), relationShopId);
            String rawPassword = generateDefaultPassword(request.getTelephone());
            String salt = RandomStringUtils.randomAlphabetic(6);
            String encryptedPassword = Utils.Secure.encryptPassword(rawPassword, salt);
            applyFieldsFromRequest(changeVersion, request, member, encryptedPassword, salt, now);
            changeVersion.setAuditStatus(Constants.ZERO);
            changeVersion.setAuditRemark(null);
            changeVersion.setAuditTime(null);
            changeVersion.setAuditUserId(null);
            changeVersion.setUpdateTime(now);
            setDepositAmountFromPricingRule(changeVersion);
            shopInfoMapper.updateById(changeVersion);
            // 删除旧附件 + 保存新附件
            deleteShopAttachments(changeVersion.getId());
            saveShopAttachments(changeVersion.getId(), request, now);
        }
    }
    private void validateCompanyTypeFields(ShopApplyDTO request) {
        if (Constants.equalsInteger(request.getCompanyType(), Constants.ZERO)) {
            // 个人类型:必须上传劳动合同和社保证明
            if (CollectionUtils.isEmpty(request.getLaborContractImgs())) {
                throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "个人类型必须上传有效劳动合同");
            }
@@ -250,7 +364,6 @@
                throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "个人类型必须上传社保缴纳证明");
            }
        } else if (Constants.equalsInteger(request.getCompanyType(), Constants.ONE)) {
            // 企业类型:必须填写法人信息和营业执照
            if (StringUtils.isBlank(request.getBusinessImg())) {
                throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "企业类型必须上传营业执照");
            }
@@ -264,98 +377,43 @@
                throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "企业类型必须填写法人身份证号码");
            }
        }
    }
        Date now = new Date();
        String rawPassword = generateDefaultPassword(request.getTelephone());
        String salt = RandomStringUtils.randomAlphabetic(6);
        String encryptedPassword = Utils.Secure.encryptPassword(rawPassword, salt);
    private void applyFieldsFromRequest(ShopInfo shop, ShopApplyDTO request, Member member,
                                        String password, String salt, Date now) {
        shop.setCompanyType(request.getCompanyType());
        shop.setName(request.getName());
        shop.setTelephone(request.getTelephone());
        shop.setLinkName(request.getLinkName());
        shop.setLinkPhone(request.getLinkPhone());
        shop.setIdcard(request.getIdcard());
        shop.setAreaId(request.getAreaId());
        shop.setLongitude(request.getLongitude());
        shop.setLatitude(request.getLatitude());
        shop.setAddress(request.getAddress());
        shop.setIdcardImg(request.getIdcardImg());
        shop.setIdcardImgBack(request.getIdcardImgBack());
        shop.setBusinessImg(request.getBusinessImg());
        shop.setLegalPersonName(request.getLegalPersonName());
        shop.setLegalPersonPhone(request.getLegalPersonPhone());
        shop.setLegalPersonCard(request.getLegalPersonCard());
        shop.setPassword(password);
        shop.setSalt(salt);
        shop.setAliAccount(request.getAliAccount());
        shop.setAliName(request.getAliName());
        shop.setRevenueShareConfig(buildRevenueShareConfig(request.getLocalDeposit(), request.getRemoteDeposit(), request.getRemoteTake()));
        shop.setOpenid(member.getOpenid());
    }
        // 3. 查询该会员是否已有门店记录
        QueryWrapper<ShopInfo> existQw = new QueryWrapper<>();
        existQw.lambda()
                .eq(ShopInfo::getRegionMemberId, member.getId())
                .eq(ShopInfo::getDeleted, Constants.ZERO)
                .last("limit 1");
        ShopInfo existing = shopInfoMapper.selectOne(existQw);
    private void saveShopAttachments(Integer shopId, ShopApplyDTO request, Date now) {
        saveMultifileList(shopId, Constants.FileType.STORE_FRONT.getKey(), request.getStoreFrontImgs(), now);
        saveMultifileList(shopId, Constants.FileType.STORE_INTERIOR.getKey(), request.getStoreInteriorImgs(), now);
        saveMultifileList(shopId, Constants.FileType.OTHER_MATERIAL.getKey(), request.getOtherMaterialImgs(), now);
        saveMultifileList(shopId, Constants.FileType.LABOR_CONTRACT.getKey(), request.getLaborContractImgs(), now);
        saveMultifileList(shopId, Constants.FileType.SOCIAL_SECURITY.getKey(), request.getSocialSecurityImgs(), now);
    }
        Integer shopId;
        if (existing != null) {
            // 校验状态:只有待审批(0)和被驳回(2)可修改
            if (!Constants.equalsInteger(existing.getAuditStatus(), Constants.ZERO)
                    && !Constants.equalsInteger(existing.getAuditStatus(), Constants.TWO)) {
                throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "当前门店状态不允许修改");
            }
            // 校验openid匹配:当前登录会员的openid必须与门店的openid一致
            if (existing.getOpenid() != null && !existing.getOpenid().equals(member.getOpenid())) {
                throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "无权限操作当前门店信息");
            }
            // 校验手机号唯一性(排除自身)
            if (!request.getTelephone().equals(existing.getTelephone())) {
                checkTelephoneUnique(request.getTelephone(), existing.getId());
            }
            // 更新
            existing.setCompanyType(request.getCompanyType());
            existing.setName(request.getName());
            existing.setTelephone(request.getTelephone());
            existing.setLinkName(request.getLinkName());
            existing.setLinkPhone(request.getLinkPhone());
            existing.setIdcard(request.getIdcard());
            existing.setAreaId(request.getAreaId());
            existing.setLongitude(request.getLongitude());
            existing.setLatitude(request.getLatitude());
            existing.setAddress(request.getAddress());
            existing.setIdcardImg(request.getIdcardImg());
            existing.setIdcardImgBack(request.getIdcardImgBack());
            existing.setBusinessImg(request.getBusinessImg());
            existing.setLegalPersonName(request.getLegalPersonName());
            existing.setLegalPersonPhone(request.getLegalPersonPhone());
            existing.setLegalPersonCard(request.getLegalPersonCard());
            existing.setPassword(encryptedPassword);
            existing.setSalt(salt);
            existing.setAliAccount(request.getAliAccount());
            existing.setAliName(request.getAliName());
            existing.setUpdateTime(now);
            existing.setAuditRemark(null);
            existing.setAuditTime(null);
            existing.setAuditUserId(null);
            existing.setAuditStatus(Constants.ZERO);
            shopInfoMapper.updateById(existing);
            shopId = existing.getId();
        } else {
            // 1. 校验门店手机号唯一性(shop_info.telephone)
            checkTelephoneUnique(request.getTelephone(), null);
            // 新建
            ShopInfo shopInfo = new ShopInfo();
            shopInfo.setCompanyType(request.getCompanyType());
            shopInfo.setName(request.getName());
            shopInfo.setTelephone(request.getTelephone());
            shopInfo.setLinkName(request.getLinkName());
            shopInfo.setLinkPhone(request.getLinkPhone());
            shopInfo.setIdcard(request.getIdcard());
            shopInfo.setAreaId(request.getAreaId());
            shopInfo.setLongitude(request.getLongitude());
            shopInfo.setLatitude(request.getLatitude());
            shopInfo.setAddress(request.getAddress());
            shopInfo.setIdcardImg(request.getIdcardImg());
            shopInfo.setIdcardImgBack(request.getIdcardImgBack());
            shopInfo.setBusinessImg(request.getBusinessImg());
            shopInfo.setLegalPersonName(request.getLegalPersonName());
            shopInfo.setLegalPersonPhone(request.getLegalPersonPhone());
            shopInfo.setLegalPersonCard(request.getLegalPersonCard());
            shopInfo.setPassword(encryptedPassword);
            shopInfo.setSalt(salt);
            shopInfo.setAliAccount(request.getAliAccount());
            shopInfo.setAliName(request.getAliName());
            shopInfo.setStatus(Constants.ZERO);
            shopInfo.setDeleted(Constants.ZERO);
            shopInfo.setCreateTime(now);
            shopInfo.setUpdateTime(now);
            shopInfo.setRegionMemberId(member.getId());
            shopInfoMapper.insert(shopInfo);
            shopId = shopInfo.getId();
        }
        // 4. 删除旧附件记录
    private void deleteShopAttachments(Integer shopId) {
        multifileMapper.delete(new QueryWrapper<Multifile>().lambda()
                .eq(Multifile::getObjId, shopId)
                .in(Multifile::getObjType,
@@ -364,13 +422,6 @@
                        Constants.FileType.OTHER_MATERIAL.getKey(),
                        Constants.FileType.LABOR_CONTRACT.getKey(),
                        Constants.FileType.SOCIAL_SECURITY.getKey()));
        // 5. 保存新附件记录
        saveMultifileList(shopId, Constants.FileType.STORE_FRONT.getKey(), request.getStoreFrontImgs(), now);
        saveMultifileList(shopId, Constants.FileType.STORE_INTERIOR.getKey(), request.getStoreInteriorImgs(), now);
        saveMultifileList(shopId, Constants.FileType.OTHER_MATERIAL.getKey(), request.getOtherMaterialImgs(), now);
        saveMultifileList(shopId, Constants.FileType.LABOR_CONTRACT.getKey(), request.getLaborContractImgs(), now);
        saveMultifileList(shopId, Constants.FileType.SOCIAL_SECURITY.getKey(), request.getSocialSecurityImgs(), now);
    }
    @Override
@@ -384,12 +435,25 @@
    @Override
    public ShopDetailVO getMyShop(Integer memberId) {
        // 查询最新的变更版本
        QueryWrapper<ShopInfo> qw = new QueryWrapper<>();
        qw.lambda()
                .eq(ShopInfo::getRegionMemberId, memberId)
                .eq(ShopInfo::getVersionType, Constants.ONE)
                .eq(ShopInfo::getDeleted, Constants.ZERO)
                .orderByDesc(ShopInfo::getCreateTime)
                .last("limit 1");
        ShopInfo shopInfo = shopInfoMapper.selectOne(qw);
        if (shopInfo == null) {
            // 无变更版本则查正式版本
            QueryWrapper<ShopInfo> officialQw = new QueryWrapper<>();
            officialQw.lambda()
                    .eq(ShopInfo::getRegionMemberId, memberId)
                    .eq(ShopInfo::getVersionType, Constants.ZERO)
                    .eq(ShopInfo::getDeleted, Constants.ZERO)
                    .last("limit 1");
            shopInfo = shopInfoMapper.selectOne(officialQw);
        }
        if (shopInfo == null) {
            return null;
        }
@@ -406,55 +470,166 @@
                || (auditDTO.getAuditStatus() != 0 && auditDTO.getAuditStatus() != 1)) {
            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "审批状态参数错误");
        }
        ShopInfo shopInfo = shopInfoMapper.selectById(auditDTO.getId());
        if (shopInfo == null || Constants.equalsInteger(shopInfo.getDeleted(), Constants.ONE)) {
        // 审批的是变更版本
        ShopInfo changeVersion = shopInfoMapper.selectById(auditDTO.getId());
        if (changeVersion == null || Constants.equalsInteger(changeVersion.getDeleted(), Constants.ONE)) {
            throw new BusinessException(ResponseStatus.DATA_EMPTY);
        }
        if (!Constants.equalsInteger(shopInfo.getAuditStatus(), Constants.ZERO)) {
        if (!Constants.equalsInteger(changeVersion.getAuditStatus(), Constants.ZERO)) {
            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "当前状态不允许审批");
        }
        Date now = new Date();
        // auditDTO.auditStatus: 0=通过 → auditStatus=1, 1=驳回 → auditStatus=2
        // auditDTO.auditStatus: 0=通过 → 1, 1=驳回 → 2
        Integer newAuditStatus = Constants.equalsInteger(auditDTO.getAuditStatus(), Constants.ZERO) ? Constants.ONE : Constants.TWO;
        shopInfo.setAuditStatus(newAuditStatus);
        shopInfo.setAuditTime(now);
        shopInfo.setAuditRemark(auditDTO.getAuditRemark());
        shopInfo.setAuditUserId(auditDTO.getAuditUser());
        shopInfo.setUpdateTime(now);
        // 审批通过时,校验城市pricing_rule配置,读取押金金额
        if (Constants.equalsInteger(newAuditStatus, Constants.ONE)) {
            // 1. 解析门店所在城市
            Areas area = areasBiz.resolveArea(shopInfo.getAreaId());
            if (area == null || area.getParentId() == null) {
                throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "门店区划信息异常,无法确定所在城市");
            }
            Integer cityId = area.getParentId();
            // 2. 校验 pricing_rule 配置(城市开通在押金支付完成后处理)
            Areas cityArea = areasService.findById(cityId, Constants.ONE);
            if (cityArea == null) {
                throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "城市信息不存在");
        // 查找正式版本
        Integer officialId = changeVersion.getRelationShopId();
        ShopInfo official = officialId != null ? shopInfoMapper.selectById(officialId) : null;
        boolean hasPaidOfficial = official != null
                && Constants.equalsInteger(official.getAuditStatus(), Constants.THREE);
        if (!hasPaidOfficial) {
            // 场景1:不存在auditStatus=3的正式版本(首次审批)
            changeVersion.setAuditStatus(newAuditStatus);
            changeVersion.setAuditTime(now);
            changeVersion.setAuditRemark(auditDTO.getAuditRemark());
            changeVersion.setAuditUserId(auditDTO.getAuditUser());
            changeVersion.setUpdateTime(now);
            if (Constants.equalsInteger(newAuditStatus, Constants.ONE)) {
                validateCityAndSetDeposit(changeVersion);
            }
            if (!Constants.equalsInteger(cityArea.getStatus(), Constants.ONE)) {
                List<String> errors = validateCityPricingRules(cityId);
                if (!errors.isEmpty()) {
                    throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(),
                            "城市[" + cityArea.getName() + "]尚未开通,定价规则未配置完整:" + String.join(";", errors));
            shopInfoMapper.updateById(changeVersion);
            // 同步更新正式版本审批状态
            if (official != null) {
                official.setAuditStatus(newAuditStatus);
                official.setAuditTime(now);
                official.setAuditRemark(auditDTO.getAuditRemark());
                official.setAuditUserId(auditDTO.getAuditUser());
                official.setUpdateTime(now);
                if (Constants.equalsInteger(newAuditStatus, Constants.ONE)) {
                    official.setDepositAmount(changeVersion.getDepositAmount());
                }
                shopInfoMapper.updateById(official);
            }
            // 3. 从PricingRule读取押金金额
            PricingRule pricingRule = pricingRuleMapper.selectOne(new QueryWrapper<PricingRule>().lambda()
                    .eq(PricingRule::getDeleted, Constants.ZERO)
                    .eq(PricingRule::getType, Constants.THREE)
                    .eq(PricingRule::getFieldA, String.valueOf(shopInfo.getCompanyType()))
                    .last("limit 1"));
            if (pricingRule != null && StringUtils.isNotBlank(pricingRule.getFieldB())) {
                shopInfo.setDepositAmount(Long.parseLong(pricingRule.getFieldB()));
            // 短信通知
            sendAuditSms(changeVersion, newAuditStatus, auditDTO.getAuditRemark());
        } else {
            // 场景2:存在auditStatus=3的正式版本(变更审批)
            if (Constants.equalsInteger(newAuditStatus, Constants.ONE)) {
                // 审批通过:变更版本直接标记auditStatus=3,同步数据到正式版本
                changeVersion.setAuditStatus(Constants.THREE);
                changeVersion.setAuditTime(now);
                changeVersion.setAuditRemark(auditDTO.getAuditRemark());
                changeVersion.setAuditUserId(auditDTO.getAuditUser());
                changeVersion.setUpdateTime(now);
                shopInfoMapper.updateById(changeVersion);
                // 同步变更版本数据到正式版本
                syncChangeToOfficial(changeVersion, official, now);
            } else {
                // 审批驳回:仅标记变更版本状态
                changeVersion.setAuditStatus(Constants.TWO);
                changeVersion.setAuditTime(now);
                changeVersion.setAuditRemark(auditDTO.getAuditRemark());
                changeVersion.setAuditUserId(auditDTO.getAuditUser());
                changeVersion.setUpdateTime(now);
                shopInfoMapper.updateById(changeVersion);
            }
        }
        shopInfoMapper.updateById(shopInfo);
    }
    private void validateCityAndSetDeposit(ShopInfo shopInfo) {
        Areas area = areasBiz.resolveArea(shopInfo.getAreaId());
        if (area == null || area.getParentId() == null) {
            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "门店区划信息异常,无法确定所在城市");
        }
        Integer cityId = area.getParentId();
        Areas cityArea = areasService.findById(cityId, Constants.ONE);
        if (cityArea == null) {
            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "城市信息不存在");
        }
        if (!Constants.equalsInteger(cityArea.getStatus(), Constants.ONE)) {
            List<String> errors = validateCityPricingRules(cityId);
            if (!errors.isEmpty()) {
                throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(),
                        "城市[" + cityArea.getName() + "]尚未开通,定价规则未配置完整:" + String.join(";", errors));
            }
        }
        setDepositAmountFromPricingRule(shopInfo);
    }
    private void syncChangeToOfficial(ShopInfo changeVersion, ShopInfo official, Date now) {
        official.setCompanyType(changeVersion.getCompanyType());
        official.setName(changeVersion.getName());
        official.setTelephone(changeVersion.getTelephone());
        official.setLinkName(changeVersion.getLinkName());
        official.setLinkPhone(changeVersion.getLinkPhone());
        official.setIdcard(changeVersion.getIdcard());
        official.setAreaId(changeVersion.getAreaId());
        official.setLongitude(changeVersion.getLongitude());
        official.setLatitude(changeVersion.getLatitude());
        official.setAddress(changeVersion.getAddress());
        official.setIdcardImg(changeVersion.getIdcardImg());
        official.setIdcardImgBack(changeVersion.getIdcardImgBack());
        official.setBusinessImg(changeVersion.getBusinessImg());
        official.setLegalPersonName(changeVersion.getLegalPersonName());
        official.setLegalPersonPhone(changeVersion.getLegalPersonPhone());
        official.setLegalPersonCard(changeVersion.getLegalPersonCard());
        official.setPassword(changeVersion.getPassword());
        official.setSalt(changeVersion.getSalt());
        official.setAliAccount(changeVersion.getAliAccount());
        official.setAliName(changeVersion.getAliName());
        official.setRevenueShareConfig(changeVersion.getRevenueShareConfig());
        official.setDepositAmount(changeVersion.getDepositAmount());
        official.setUpdateTime(now);
        shopInfoMapper.updateById(official);
        // 同步附件:先删正式版本旧附件,再从变更版本拷贝
        deleteShopAttachments(official.getId());
        List<Multifile> changeFiles = multifileMapper.selectList(new QueryWrapper<Multifile>().lambda()
                .eq(Multifile::getObjId, changeVersion.getId())
                .eq(Multifile::getIsdeleted, Constants.ZERO)
                .in(Multifile::getObjType,
                        Constants.FileType.STORE_FRONT.getKey(),
                        Constants.FileType.STORE_INTERIOR.getKey(),
                        Constants.FileType.OTHER_MATERIAL.getKey(),
                        Constants.FileType.LABOR_CONTRACT.getKey(),
                        Constants.FileType.SOCIAL_SECURITY.getKey()));
        for (Multifile f : changeFiles) {
            Multifile copy = new Multifile();
            copy.setCreator(f.getCreator());
            copy.setCreateDate(now);
            copy.setIsdeleted(Constants.ZERO);
            copy.setName(f.getName());
            copy.setInfo(f.getInfo());
            copy.setObjId(official.getId());
            copy.setType(f.getType());
            copy.setObjType(f.getObjType());
            copy.setFileurl(f.getFileurl());
            copy.setSortnum(f.getSortnum());
            multifileMapper.insert(copy);
        }
    }
    private void sendAuditSms(ShopInfo shopInfo, Integer newAuditStatus, String auditRemark) {
        if (Constants.equalsInteger(newAuditStatus, Constants.ONE)) {
            String depositMoney = shopInfo.getDepositAmount() != null
                    ? new java.math.BigDecimal(shopInfo.getDepositAmount())
                        .divide(new java.math.BigDecimal(100), 2, java.math.RoundingMode.HALF_UP).toPlainString() : "0";
            sendSmsNotify(shopInfo.getTelephone(),
                    Constants.SmsNotify.SHOP_AUTH_APPROVED_DEPOSIT,
                    "storeName", shopInfo.getName(),
                    "money", depositMoney);
        } else if (Constants.equalsInteger(newAuditStatus, Constants.TWO)) {
            sendSmsNotify(shopInfo.getTelephone(),
                    Constants.SmsNotify.SHOP_AUTH_REJECTED,
                    "storeName", shopInfo.getName(),
                    "reason", auditRemark != null ? auditRemark : "");
        }
    }
    @Override
@@ -554,6 +729,7 @@
        shopInfo.setLegalPersonCard(request.getLegalPersonCard());
        shopInfo.setAliAccount(request.getAliAccount());
        shopInfo.setAliName(request.getAliName());
        shopInfo.setRevenueShareConfig(buildRevenueShareConfig(request.getLocalDeposit(), request.getRemoteDeposit(), request.getRemoteTake()));
        shopInfo.setUpdateTime(now);
        shopInfoMapper.updateById(shopInfo);
@@ -585,6 +761,81 @@
            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "手机号格式异常,无法生成默认密码");
        }
        return telephone.substring(telephone.length() - 6) + "@123456";
    }
    private String buildRevenueShareConfig(Double localDeposit, Double remoteDeposit, Double remoteTake) {
        if (localDeposit == null && remoteDeposit == null && remoteTake == null) {
            return null;
        }
        JSONObject config = new JSONObject();
        if (localDeposit != null) {
            config.put("localDeposit", localDeposit);
        }
        if (remoteDeposit != null) {
            config.put("remoteDeposit", remoteDeposit);
        }
        if (remoteTake != null) {
            config.put("remoteTake", remoteTake);
        }
        return config.toJSONString();
    }
    private void createChangeVersion(ShopInfo origin, Integer originShopId, Date now) {
        ShopInfo changeShop = new ShopInfo();
        changeShop.setCompanyType(origin.getCompanyType());
        changeShop.setName(origin.getName());
        changeShop.setTelephone(origin.getTelephone());
        changeShop.setLinkName(origin.getLinkName());
        changeShop.setLinkPhone(origin.getLinkPhone());
        changeShop.setIdcard(origin.getIdcard());
        changeShop.setAreaId(origin.getAreaId());
        changeShop.setLongitude(origin.getLongitude());
        changeShop.setLatitude(origin.getLatitude());
        changeShop.setAddress(origin.getAddress());
        changeShop.setIdcardImg(origin.getIdcardImg());
        changeShop.setIdcardImgBack(origin.getIdcardImgBack());
        changeShop.setBusinessImg(origin.getBusinessImg());
        changeShop.setLegalPersonName(origin.getLegalPersonName());
        changeShop.setLegalPersonPhone(origin.getLegalPersonPhone());
        changeShop.setLegalPersonCard(origin.getLegalPersonCard());
        changeShop.setAliAccount(origin.getAliAccount());
        changeShop.setAliName(origin.getAliName());
        changeShop.setDepositAmount(origin.getDepositAmount());
        changeShop.setRevenueShareConfig(origin.getRevenueShareConfig());
        changeShop.setVersionType(Constants.ONE);
        changeShop.setRelationShopId(originShopId);
        changeShop.setRegionMemberId(origin.getRegionMemberId());
        changeShop.setOpenid(origin.getOpenid());
        changeShop.setStatus(Constants.ZERO);
        changeShop.setDeleted(Constants.ZERO);
        changeShop.setCreateTime(now);
        changeShop.setUpdateTime(now);
        shopInfoMapper.insert(changeShop);
        // 拷贝附件到变更版本
        List<Multifile> originFiles = multifileMapper.selectList(new QueryWrapper<Multifile>().lambda()
                .eq(Multifile::getObjId, originShopId)
                .eq(Multifile::getIsdeleted, Constants.ZERO)
                .in(Multifile::getObjType,
                        Constants.FileType.STORE_FRONT.getKey(),
                        Constants.FileType.STORE_INTERIOR.getKey(),
                        Constants.FileType.OTHER_MATERIAL.getKey(),
                        Constants.FileType.LABOR_CONTRACT.getKey(),
                        Constants.FileType.SOCIAL_SECURITY.getKey()));
        for (Multifile f : originFiles) {
            Multifile copy = new Multifile();
            copy.setCreator(f.getCreator());
            copy.setCreateDate(now);
            copy.setIsdeleted(Constants.ZERO);
            copy.setName(f.getName());
            copy.setInfo(f.getInfo());
            copy.setObjId(changeShop.getId());
            copy.setType(f.getType());
            copy.setObjType(f.getObjType());
            copy.setFileurl(f.getFileurl());
            copy.setSortnum(f.getSortnum());
            multifileMapper.insert(copy);
        }
    }
    private void checkTelephoneUnique(String telephone, Integer excludeId) {
@@ -660,6 +911,21 @@
        vo.setAliAccount(shopInfo.getAliAccount());
        vo.setAliName(shopInfo.getAliName());
        vo.setDepositAmount(shopInfo.getDepositAmount());
        vo.setDeliveryRange(shopInfo.getDeliveryArea());
        vo.setVersionType(shopInfo.getVersionType());
        vo.setRelationShopId(shopInfo.getRelationShopId());
        // 解析收益比例配置
        if (StringUtils.isNotBlank(shopInfo.getRevenueShareConfig())) {
            try {
                JSONObject config = JSONObject.parseObject(shopInfo.getRevenueShareConfig());
                if (config != null) {
                    vo.setLocalDeposit(config.getDouble("localDeposit"));
                    vo.setRemoteDeposit(config.getDouble("remoteDeposit"));
                    vo.setRemoteTake(config.getDouble("remoteTake"));
                }
            } catch (Exception ignored) {
            }
        }
        // 拼接图片前缀
        String imgPrefix = "";
@@ -731,6 +997,13 @@
            }
        }
        // 门店头像:优先使用 coverImg,为空则取门头照第一张
        if (StringUtils.isNotBlank(shopInfo.getCoverImg())) {
            vo.setShopAvatar(imgPrefix + shopInfo.getCoverImg());
        } else if (!CollectionUtils.isEmpty(vo.getStoreFrontImgUrls())) {
            vo.setShopAvatar(vo.getStoreFrontImgUrls().get(0));
        }
        return vo;
    }
@@ -752,6 +1025,7 @@
        qw.lambda()
                .eq(ShopInfo::getDeleted, Constants.ZERO)
                .eq(ShopInfo::getStatus, Constants.ZERO)
                .eq(ShopInfo::getVersionType, Constants.ZERO)
                .eq(ShopInfo::getAuditStatus, Constants.THREE);
        // 门店营业类型筛选
@@ -761,7 +1035,8 @@
        // 门店名称模糊查询
        if (StringUtils.isNotBlank(dto.getName())) {
            qw.lambda().like(ShopInfo::getName, dto.getName());
            qw.lambda().and(q -> q.like(ShopInfo::getName, dto.getName()).or()
                    .like(ShopInfo::getAddress, dto.getName()));
        }
        // 城市筛选(areaId是区县,需匹配其parentId等于城市ID)
@@ -825,7 +1100,7 @@
    @Override
    public ShopWebDetailVO getShopWebDetail(ShopDetailQueryDTO dto) {
        ShopInfo shop = shopInfoMapper.selectById(dto.getId());
        if (Objects.isNull(shop) || Constants.equalsInteger(shop.getDeleted(), Constants.ONE)) {
        if (Objects.isNull(shop) || Constants.equalsInteger(shop.getDeleted(), Constants.ONE) || Constants.equalsInteger(shop.getVersionType(), Constants.ONE)) {
            throw new BusinessException(ResponseStatus.DATA_EMPTY);
        }
@@ -838,6 +1113,7 @@
        vo.setFeeStandard(shop.getFeeStandard());
        vo.setLatitude(shop.getLatitude());
        vo.setLongitude(shop.getLongitude());
        vo.setLinkPhone(shop.getLinkPhone());
        // 门头照 + 内部照 全路径集合
        String imgPrefix = getShopPrefix();
@@ -880,8 +1156,8 @@
    }
    @Override
    public ShopInfoMaintainDTO getShopMaintainInfo(Integer memberId) {
        ShopInfo shop = shopInfoMapper.selectById(memberId);
    public ShopInfoMaintainDTO getShopMaintainInfo(Integer shopId) {
        ShopInfo shop = shopInfoMapper.selectById(shopId);
        if (Objects.isNull(shop) || Constants.equalsInteger(shop.getDeleted(), Constants.ONE)) {
            return null;
        }
@@ -893,6 +1169,10 @@
        dto.setDeliveryArea(shop.getDeliveryArea());
        dto.setShopHours(shop.getShopHours());
        dto.setBusinessType(shop.getBusinessType());
        // 头像全路径
        if (StringUtils.isNotBlank(shop.getCoverImg())) {
            dto.setCoverImgUrl(getShopPrefix() + shop.getCoverImg());
        }
        return dto;
    }
@@ -970,7 +1250,7 @@
    @Override
    public ShopCenterVO getShopCenterInfo(Integer shopId) {
        ShopInfo shop = shopInfoMapper.selectById(shopId);
        if (shop == null || Constants.equalsInteger(shop.getDeleted(), Constants.ONE)) {
        if (shop == null || Constants.equalsInteger(shop.getDeleted(), Constants.ONE) || Constants.equalsInteger(shop.getVersionType(), Constants.ONE)) {
            throw new BusinessException(ResponseStatus.DATA_EMPTY);
        }
        ShopCenterVO vo = new ShopCenterVO();
@@ -996,6 +1276,117 @@
                .eq(Orders::getDeleted, Constants.ZERO)
                .in(Orders::getStatus, Constants.OrderStatus.delivering.getStatus(), Constants.OrderStatus.arrived.getStatus()));
        vo.setWaitReceiveCount(waitReceiveCount.intValue());
        // 支付宝提现账号
        vo.setAliAccount(shop.getAliAccount());
        vo.setAliName(shop.getAliName());
        return vo;
    }
    @Override
    public ShopSalesStatsVO getShopSalesStats(Integer shopId, Integer period) {
        // 计算时间范围 0=今日 1=本月 2=上月
        Calendar cal = Calendar.getInstance();
        Date startTime;
        Date endTime;
        if (Constants.equalsInteger(period, 2)) {
            // 上月:上月1号00:00:00 ~ 本月1号00:00:00
            cal.set(Calendar.DAY_OF_MONTH, 1);
            cal.set(Calendar.HOUR_OF_DAY, 0);
            cal.set(Calendar.MINUTE, 0);
            cal.set(Calendar.SECOND, 0);
            cal.set(Calendar.MILLISECOND, 0);
            endTime = cal.getTime();
            cal.add(Calendar.MONTH, -1);
            startTime = cal.getTime();
        } else if (Constants.equalsInteger(period, 1)) {
            // 本月:本月1号00:00:00 ~ 下月1号00:00:00
            cal.set(Calendar.DAY_OF_MONTH, 1);
            cal.set(Calendar.HOUR_OF_DAY, 0);
            cal.set(Calendar.MINUTE, 0);
            cal.set(Calendar.SECOND, 0);
            cal.set(Calendar.MILLISECOND, 0);
            startTime = cal.getTime();
            cal.add(Calendar.MONTH, 1);
            endTime = cal.getTime();
        } else {
            // 今日:今天00:00:00 ~ 明天00:00:00
            cal.set(Calendar.HOUR_OF_DAY, 0);
            cal.set(Calendar.MINUTE, 0);
            cal.set(Calendar.SECOND, 0);
            cal.set(Calendar.MILLISECOND, 0);
            startTime = cal.getTime();
            cal.add(Calendar.DAY_OF_MONTH, 1);
            endTime = cal.getTime();
        }
        ShopSalesStatsVO vo = new ShopSalesStatsVO();
        // 1. 销售额 + 订单数:按订单创建时间,存件门店或取件门店是本门店
        // 存件门店
        List<Orders> depositSalesOrders = ordersMapper.selectList(new QueryWrapper<Orders>().lambda()
                .eq(Orders::getDeleted, Constants.ZERO)
                .notIn(Orders::getStatus,Constants.OrderStatus.cancelled.getStatus(),Constants.OrderStatus.waitPay.getStatus())
                .ge(Orders::getCreateTime, startTime)
                .lt(Orders::getCreateTime, endTime)
                .eq(Orders::getDepositShopId, shopId));
        // 取件门店
        List<Orders> takeSalesOrders = ordersMapper.selectList(new QueryWrapper<Orders>().lambda()
                .eq(Orders::getDeleted, Constants.ZERO)
                .eq(Orders::getType,Constants.ONE)
                .notIn(Orders::getStatus,Constants.OrderStatus.cancelled.getStatus(),Constants.OrderStatus.waitPay.getStatus())
                .ge(Orders::getCreateTime, startTime)
                .lt(Orders::getCreateTime, endTime)
                .eq(Orders::getTakeShopId, shopId));
        long salesAmount = depositSalesOrders.stream().mapToLong(o -> o.getTotalAmount() != null ? o.getTotalAmount() : 0L).sum()
                + takeSalesOrders.stream().mapToLong(o -> o.getTotalAmount() != null ? o.getTotalAmount() : 0L).sum();
        vo.setSalesAmount(salesAmount);
        vo.setOrderCount(depositSalesOrders.size() + takeSalesOrders.size());
        // 2. 结算利润:按结算时间,根据门店角色取depositShopFee或takeShopFee
        // 存件门店 = 本门店 的订单,取 depositShopFee
        List<Orders> depositSettleOrders = ordersMapper.selectList(new QueryWrapper<Orders>().lambda()
                .eq(Orders::getDeleted, Constants.ZERO)
                .eq(Orders::getSettlementStatus, Constants.ONE)
                .ge(Orders::getSettlementTime, startTime)
                .lt(Orders::getSettlementTime, endTime)
                .eq(Orders::getDepositShopId, shopId));
        // 取件门店 = 本门店 的订单,取 takeShopFee
        List<Orders> takeSettleOrders = ordersMapper.selectList(new QueryWrapper<Orders>().lambda()
                .eq(Orders::getDeleted, Constants.ZERO)
                .eq(Orders::getSettlementStatus, Constants.ONE)
                .ge(Orders::getSettlementTime, startTime)
                .lt(Orders::getSettlementTime, endTime)
                .eq(Orders::getTakeShopId, shopId));
        long depositFee = depositSettleOrders.stream()
                .mapToLong(o -> o.getDepositShopFee() != null ? o.getDepositShopFee() : 0L).sum();
        long takeFee = takeSettleOrders.stream()
                .mapToLong(o -> o.getTakeShopFee() != null ? o.getTakeShopFee() : 0L).sum();
        vo.setSettlementProfit(depositFee + takeFee);
        // 3. 在库订单数
        // 3.1 就地订单:存件门店=本门店,type=0,status in (2已寄存, 5待取件)
        Long localStorageCount = ordersMapper.selectCount(new QueryWrapper<Orders>().lambda()
                .eq(Orders::getDeleted, Constants.ZERO)
                .eq(Orders::getDepositShopId, shopId)
                .eq(Orders::getType, Constants.ZERO)
                .in(Orders::getStatus,
                        Constants.OrderStatus.deposited.getStatus(),
                        Constants.OrderStatus.arrived.getStatus()));
        // 3.2 异地订单:存件门店=本门店,type=1,status=2已寄存
        Long remoteStorageCount = ordersMapper.selectCount(new QueryWrapper<Orders>().lambda()
                .eq(Orders::getDeleted, Constants.ZERO)
                .eq(Orders::getDepositShopId, shopId)
                .eq(Orders::getType, Constants.ONE)
                .eq(Orders::getStatus, Constants.OrderStatus.deposited.getStatus()));
        // 3.3 取件门店=本门店,异地订单,status = 5待取件
        Long takeStorageCount = ordersMapper.selectCount(new QueryWrapper<Orders>().lambda()
                .eq(Orders::getDeleted, Constants.ZERO)
                .eq(Orders::getType,Constants.ONE)
                .eq(Orders::getTakeShopId, shopId)
                .eq(Orders::getStatus, Constants.OrderStatus.arrived.getStatus()));
        vo.setStorageCount(localStorageCount.intValue() + remoteStorageCount.intValue() + takeStorageCount.intValue());
        return vo;
    }
@@ -1008,48 +1399,51 @@
    public ShopLoginVO shopPasswordLogin(ShopLoginDTO dto){
        if(StringUtils.isBlank(dto.getTelephone())
                || StringUtils.isBlank(dto.getPassword())
                || StringUtils.isBlank(dto.getOpenid())
        ){
            throw new BusinessException(ResponseStatus.NOT_ALLOWED.getCode(),"用户名或密码不能为空");
        }
        ShopInfo shop = shopInfoMapper.selectOne(new QueryWrapper<ShopInfo>().lambda().eq(ShopInfo::getTelephone, dto.getTelephone())
                .eq(ShopInfo::getDeleted,Constants.ZERO)
                .eq(ShopInfo::getVersionType,Constants.ZERO)
                .last("limit 1")
        );
        if(shop==null){
            throw new BusinessException(ResponseStatus.ACCOUNT_INCORRECT);
            throw new BusinessException(ResponseStatus.NOT_ALLOWED.getCode(),"请核对登录手机号是否正确");
        }
        if(Constants.equalsInteger(shop.getStatus(),Constants.ONE)){
            throw new BusinessException(ResponseStatus.NOT_ALLOWED.getCode(),"门店已禁用,请联系管理员");
        }
        if(!Constants.equalsInteger(shop.getAuditStatus(),Constants.THREE)){
            if(Constants.equalsInteger(shop.getAuditStatus(),Constants.ONE)){
                throw new BusinessException(ResponseStatus.NOT_ALLOWED.getCode(),"请完成审批流程 - 支付押金");
            }
            throw new BusinessException(ResponseStatus.NOT_ALLOWED.getCode(),"门店审批流程未完成,请完成审批流程");
        }
        //加密密码
        String pwd = Utils.Secure.encryptPassword( dto.getPassword(), shop.getSalt());
        if(!pwd.equals(shop.getPassword())){
            throw new BusinessException(ResponseStatus.ACCOUNT_INCORRECT);
        }
        // 更新当前登录会员的openid到门店
        if(StringUtils.isNotBlank(dto.getOpenid())){
            shopInfoMapper.update(null,new UpdateWrapper<ShopInfo>().lambda()
                    .set(ShopInfo::getOpenid,dto.getOpenid())
                    .eq(ShopInfo::getId,shop.getId())
            );
            // 清空其他门店的同一openid,保证唯一
            shopInfoMapper.update(null,new UpdateWrapper<ShopInfo>().lambda()
                    .set(ShopInfo::getOpenid,null)
                    .eq(ShopInfo::getOpenid,dto.getOpenid())
                    .ne(ShopInfo::getId,shop.getId())
            );
            shop.setOpenid(dto.getOpenid());
        if(Objects.nonNull(dto.getMemberId())){
            Member member = memberMapper.selectById(dto.getMemberId());
            if(Objects.nonNull(member)){
                memberMapper.update(new UpdateWrapper<Member>().lambda()
                        .set(Member::getLoginShopId,shop.getId())
                        .eq(Member::getId,member.getId())
                );
                shop.setMemberId(member.getId());
            }else{
                throw new BusinessException(ResponseStatus.NOT_ALLOWED.getCode(),"会员信息异常,请联系管理员");
            }
        }
        // 创建token(generateTokenForRedis 已自动清除该用户旧token,保证唯一有效)
        String token = JwtTokenUtil.generateTokenForRedis(shop.getId(), Constants.TWO, JSONObject.toJSONString(shop), redisTemplate);
        String token = JwtTokenUtil.generateShopTokenForRedis(shop.getMemberId(), JSONObject.toJSONString(shop), redisTemplate);
        // 构建响应
        ShopLoginVO vo = new ShopLoginVO();
        vo.setToken(token);
        vo.setShopId(shop.getId());
        vo.setShopName(shop.getName());
        vo.setCompanyType(shop.getCompanyType());
        // 所属城市名称
        Areas area = areasBiz.resolveArea(shop.getAreaId());
        if (area != null) {
@@ -1061,18 +1455,25 @@
    @Override
    public ShopLoginVO shopSilentLogin(Integer memberId) {
        Member member = memberMapper.selectById(memberId);
        if(Objects.isNull(member)||StringUtils.isBlank(member.getOpenid())){
        if(Objects.isNull(member)){
            throw new BusinessException(ResponseStatus.NOT_ALLOWED.getCode(), "当前登录会员身份异常,请联系管理员!");
        }
        if (Objects.isNull(member.getLoginShopId())) {
            return null;
        }
        ShopInfo shop = shopInfoMapper.selectOne(new QueryWrapper<ShopInfo>().lambda()
                .eq(ShopInfo::getOpenid, member.getOpenid())
                .eq(ShopInfo::getId, member.getLoginShopId())
                .eq(ShopInfo::getVersionType,Constants.ZERO)
                .eq(ShopInfo::getDeleted, Constants.ZERO)
                .last("limit 1"));
        if (shop == null) {
            return null;
        }
        if(Constants.equalsInteger(shop.getStatus(),Constants.ONE)){
            throw new BusinessException(ResponseStatus.NOT_ALLOWED.getCode(),"门店已禁用,请联系管理员");
        }
        // 创建token(generateTokenForRedis 已自动清除该用户旧token,保证唯一有效)
        String token = JwtTokenUtil.generateTokenForRedis(shop.getId(), Constants.TWO, JSONObject.toJSONString(shop), redisTemplate);
        String token = JwtTokenUtil.generateShopTokenForRedis(member.getId(), JSONObject.toJSONString(shop), redisTemplate);
        ShopLoginVO vo = new ShopLoginVO();
        vo.setToken(token);
@@ -1174,4 +1575,155 @@
        return errors;
    }
    /**
     * 从PricingRule读取押金金额并赋值到门店记录
     */
    private void setDepositAmountFromPricingRule(ShopInfo shopInfo) {
        if (shopInfo.getCompanyType() == null) {
            return;
        }
        Areas areas = areasService.findById(shopInfo.getAreaId());
        PricingRule pricingRule = pricingRuleMapper.selectOne(new QueryWrapper<PricingRule>().lambda()
                .eq(PricingRule::getDeleted, Constants.ZERO)
                .eq(PricingRule::getType, Constants.THREE)
                .eq(PricingRule::getFieldA, String.valueOf(Constants.equalsInteger(shopInfo.getCompanyType(),Constants.ZERO)?Constants.ONE:Constants.ZERO))
                .eq(PricingRule::getCityId, areas.getParentId())
                .last("limit 1"));
        if (pricingRule != null && StringUtils.isNotBlank(pricingRule.getFieldB())) {
            shopInfo.setDepositAmount(Long.parseLong(pricingRule.getFieldB()));
        }
    }
    /**
     * 从运营配置读取默认配送范围并赋值到门店记录
     */
    private void setDefaultDeliveryRange(ShopInfo shopInfo) {
        SystemDictData data = systemDictDataBiz.queryByCode(Constants.OPERATION_CONFIG, Constants.OP_DEFAULT_DELIVERY_RANGE);
        if (data != null && StringUtils.isNotBlank(data.getCode())) {
            shopInfo.setDeliveryArea(new java.math.BigDecimal(data.getCode()));
        }
    }
    /**
     * 发送短信通知(失败不影响主业务)
     */
    private void sendSmsNotify(String phone, Constants.SmsNotify smsNotify, String... paramPairs) {
        if (StringUtils.isBlank(phone)) {
            return;
        }
        if (!smsNotify.isEnabled()) {
            return;
        }
        String content = smsNotify.format(paramPairs);
        try {
            JSONObject templateParam = new JSONObject();
            for (int i = 0; i < paramPairs.length - 1; i += 2) {
                templateParam.put(paramPairs[i], paramPairs[i + 1]);
            }
            String error = AliSmsService.sendSms(phone, smsNotify.getTemplateCode(),
                    templateParam.toJSONString());
            Smsrecord smsRecord = new Smsrecord();
            smsRecord.setPhone(phone);
            smsRecord.setContent(content);
            smsRecord.setType(Constants.ONE);
            smsRecord.setStatus(error == null ? Constants.ONE : Constants.ZERO);
            if (error != null) {
                smsRecord.setRemark(error);
            }
            smsRecord.setCreateTime(new Date());
            smsRecord.setDeleted(Constants.ZERO);
            smsrecordMapper.insert(smsRecord);
        } catch (Exception e) {
            log.error("短信发送异常: phone={}, template={}, error={}", phone, smsNotify.name(), e.getMessage());
        }
    }
    @Override
    public void changePassword(Integer shopId, String oldPassword, String newPassword, String token) {
        if (StringUtils.isBlank(oldPassword)) {
            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "旧密码不能为空");
        }
        if (StringUtils.isBlank(newPassword)) {
            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "新密码不能为空");
        }
        // 校验新密码必须同时包含字母和数字
        boolean hasLetter = newPassword.chars().anyMatch(Character::isLetter);
        boolean hasDigit = newPassword.chars().anyMatch(Character::isDigit);
        if (!hasLetter || !hasDigit) {
            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "密码必须同时包含字母和数字");
        }
        // 查询门店
        ShopInfo shopInfo = shopInfoMapper.selectById(shopId);
        if (shopInfo == null || Constants.equalsInteger(shopInfo.getDeleted(), Constants.ONE)) {
            throw new BusinessException(ResponseStatus.DATA_EMPTY);
        }
        // 校验旧密码
        String oldEncryptPwd = Utils.Secure.encryptPassword(oldPassword, shopInfo.getSalt());
        if (!oldEncryptPwd.equals(shopInfo.getPassword())) {
            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "旧密码错误");
        }
        // 重新生成salt,加密新密码并更新
        String salt = RandomStringUtils.randomAlphabetic(6);
        shopInfo.setPassword(Utils.Secure.encryptPassword(newPassword, salt));
        shopInfo.setSalt(salt);
        shopInfoMapper.updateById(shopInfo);
        // 清除token,强制重新登录
        if (StringUtils.isNotBlank(token)) {
            redisTemplate.delete(token);
        }
    }
    @Override
    public void maintainPrinterSn(ShopPrinterDTO dto) {
        ShopInfo shop = shopInfoMapper.selectById(dto.getId());
        if (shop == null || Constants.equalsInteger(shop.getDeleted(), Constants.ONE)) {
            throw new BusinessException(ResponseStatus.DATA_EMPTY);
        }
        String oldSn = shop.getPrinterSn();
        String newSn = dto.getPrinterSn();
        boolean hasNewSn = StringUtils.isNotBlank(newSn);
        boolean hasOldSn = StringUtils.isNotBlank(oldSn);
        if (!hasNewSn) {
            // 未传打印机SN → 删除逻辑
            if (hasOldSn) {
                delPrinterFromXyy(oldSn);
                shop.setPrinterSn(null);
                shopInfoMapper.updateById(shop);
            }
            return;
        }
        // 传了打印机SN → 如果旧绑定存在,先删除
        if (hasOldSn) {
            delPrinterFromXyy(oldSn);
        }
        // 绑定新打印机
        AddPrinterRequest addReq = new AddPrinterRequest();
        xyyConfig.createRequestHeader(addReq);
        addReq.setItems(new AddPrinterRequestItem[]{new AddPrinterRequestItem() {{
            setSn(newSn);
            setName(shop.getName());
        }}});
        ObjectRestResponse<PrinterResult> addResp = printService.addPrinters(addReq);
        if (addResp.getCode() != 0) {
            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "绑定打印机失败:" + addResp.getMsg());
        }
        log.info("绑定成功:{}", newSn);
        shop.setPrinterSn(newSn);
        shopInfoMapper.updateById(shop);
    }
    private void delPrinterFromXyy(String sn) {
        DelPrinterRequest delReq = new DelPrinterRequest();
        xyyConfig.createRequestHeader(delReq);
        delReq.setSnlist(new String[]{sn});
        ObjectRestResponse<PrinterResult> delResp = printService.delPrinters(delReq);
        if (delResp.getCode() != 0) {
            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "解绑打印机失败:" + delResp.getMsg());
        }
        log.info("解绑成功:{}", sn);
    }
}