From 21794bf0908f509d916811e1320082a03d9ab116 Mon Sep 17 00:00:00 2001 From: zjf <1799650030@qq.com> Date: Thu, 18 Jun 2026 16:58:32 +0800 Subject: [PATCH 1/3] =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E7=99=BB=E5=BD=95?= =?UTF-8?q?=EF=BC=8C=E9=87=8D=E7=BD=AE=E5=AF=86=E7=A0=81=EF=BC=8C=E8=B6=85?= =?UTF-8?q?=E5=B8=82=E9=83=A8=E7=BD=B2=E5=AE=8C=E5=96=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/core/constant/CacheConstants.java | 5 + ...jcloudTocMobileUserDetailsServiceImpl.java | 13 +- .../controller/SmErpCategoryController.java | 11 +- .../controller/SmErpSupplierController.java | 5 +- .../controller/SmSpuController.java | 1 + .../service/SmErpCategoryService.java | 5 + .../impl/SmErpCategoryServiceImpl.java | 14 +- .../SmErpInventoryRecordsServiceImpl.java | 361 +++++++++--------- .../service/impl/SmErpSpuServiceImpl.java | 18 + .../impl/SmErpSupplierServiceImpl.java | 15 +- .../admin/api/vo/WxSocialResolveVO.java | 21 + .../SysSocialDetailsController.java | 18 + .../admin/handler/WeChatLoginHandler.java | 15 +- .../admin/handler/WxTicketLoginHandler.java | 64 ++++ .../service/SysSocialDetailsService.java | 11 + .../impl/SysSocialDetailsServiceImpl.java | 42 ++ 16 files changed, 423 insertions(+), 196 deletions(-) create mode 100644 pjcloud-upms/pjcloud-upms-api/src/main/java/com/bsd/pjcloud/admin/api/vo/WxSocialResolveVO.java create mode 100644 pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/handler/WxTicketLoginHandler.java diff --git a/pjcloud-common/pjcloud-common-core/src/main/java/com/bsd/pjcloud/common/core/constant/CacheConstants.java b/pjcloud-common/pjcloud-common-core/src/main/java/com/bsd/pjcloud/common/core/constant/CacheConstants.java index 23937c2f9..8196a831d 100644 --- a/pjcloud-common/pjcloud-common-core/src/main/java/com/bsd/pjcloud/common/core/constant/CacheConstants.java +++ b/pjcloud-common/pjcloud-common-core/src/main/java/com/bsd/pjcloud/common/core/constant/CacheConstants.java @@ -127,6 +127,11 @@ public interface CacheConstants { */ String PROJECT_OAUTH_ACCESS = "token::access_token"; + /** + * App 微信社交登录一次性 ticket(openid 缓存,避免 code 重复使用) + */ + String WX_SOCIAL_TICKET = GLOBALLY + "wx:social:ticket:"; + /* ************************************* start 钱包相关 start ************************************* */ String WALLET_DEFAULT = "wallet_default"; diff --git a/pjcloud-common/pjcloud-common-security/src/main/java/com/bsd/pjcloud/common/security/service/PjcloudTocMobileUserDetailsServiceImpl.java b/pjcloud-common/pjcloud-common-security/src/main/java/com/bsd/pjcloud/common/security/service/PjcloudTocMobileUserDetailsServiceImpl.java index 1b6635ff8..300947b2a 100644 --- a/pjcloud-common/pjcloud-common-security/src/main/java/com/bsd/pjcloud/common/security/service/PjcloudTocMobileUserDetailsServiceImpl.java +++ b/pjcloud-common/pjcloud-common-security/src/main/java/com/bsd/pjcloud/common/security/service/PjcloudTocMobileUserDetailsServiceImpl.java @@ -17,6 +17,7 @@ package com.bsd.pjcloud.common.security.service; import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.StrUtil; import com.bsd.pjcloud.app.api.dto.AppUserInfo; import com.bsd.pjcloud.app.api.entity.AppUser; import com.bsd.pjcloud.app.api.feign.RemoteAppUserService; @@ -27,6 +28,7 @@ import com.bsd.pjcloud.common.core.util.MsgUtils; import com.bsd.pjcloud.common.core.util.R; import com.bsd.pjcloud.common.core.util.RetOps; import com.bsd.pjcloud.common.core.util.WebUtils; +import com.baomidou.mybatisplus.core.toolkit.StringPool; import lombok.RequiredArgsConstructor; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; @@ -129,7 +131,16 @@ public class PjcloudTocMobileUserDetailsServiceImpl implements PjcloudUserDetail } String header = WebUtils.getRequest().getHeader(SecurityConstants.HEADER_TOC); - return SecurityConstants.HEADER_TOC_YES.equals(header) && SecurityConstants.GRANT_MOBILE.equals(grantType); + if (!SecurityConstants.HEADER_TOC_YES.equals(header) || !SecurityConstants.GRANT_MOBILE.equals(grantType)) { + return false; + } + // 仅小程序等 C 端账号走 app-server;微信开放平台 WX/WX_TICKET 绑定在 upms sys_user + String mobile = WebUtils.getRequest().getParameter(SecurityConstants.GRANT_MOBILE); + if (StrUtil.isBlank(mobile) || !mobile.contains(StringPool.AT)) { + return false; + } + String type = mobile.substring(0, mobile.indexOf(StringPool.AT)); + return StrUtil.startWithAny(type, "MINI_WECHAT", "MINI_ALIPAY", "APP-SMS"); } } diff --git a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/controller/SmErpCategoryController.java b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/controller/SmErpCategoryController.java index c1e40b7df..82d7f89b7 100644 --- a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/controller/SmErpCategoryController.java +++ b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/controller/SmErpCategoryController.java @@ -120,7 +120,10 @@ public class SmErpCategoryController { @GetMapping("/export") @PreAuthorize("@pms.hasPermission('supermarket_supermarketCategory_export')" ) public List export(SmErpCategoryEntity supermarketCategory, Long[] ids) { - return smErpCategoryService.list(Wrappers.lambdaQuery(supermarketCategory).in(ArrayUtil.isNotEmpty(ids), SmErpCategoryEntity::getId, ids)); + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(supermarketCategory) + .in(ArrayUtil.isNotEmpty(ids), SmErpCategoryEntity::getId, ids); + smErpCategoryService.applyDefaultSort(wrapper); + return smErpCategoryService.list(wrapper); } @@ -147,8 +150,10 @@ public class SmErpCategoryController { @ApiCryptoAes @GetMapping("/app/simpleAll") public R> getAppSimpleAll(@RequestParam("merchantId")Long merchantId) { - List list = smErpCategoryService. - list(new LambdaQueryWrapper().eq(SmErpCategoryEntity::getMerchantId, merchantId)); + LambdaQueryWrapper wrapper = new LambdaQueryWrapper() + .eq(SmErpCategoryEntity::getMerchantId, merchantId); + smErpCategoryService.applyDefaultSort(wrapper); + List list = smErpCategoryService.list(wrapper); List bean = BeanUtils.toBean(list, SmErpCategorySimpleVo.class); return R.ok(bean); } diff --git a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/controller/SmErpSupplierController.java b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/controller/SmErpSupplierController.java index e9bebd24f..d31651f68 100644 --- a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/controller/SmErpSupplierController.java +++ b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/controller/SmErpSupplierController.java @@ -131,10 +131,9 @@ public class SmErpSupplierController { } /** - * 货商下拉 - * @return R + * 货商下拉(带数据权限 scope,仅返回当前账号可见范围) */ - @Operation(summary = "通过id查询" , description = "通过id查询" ) + @Operation(summary = "货商下拉" , description = "货商下拉,支持名称/拼音/联系人/电话模糊搜索" ) @GetMapping("/selectSimpleAll" ) public R selectSimpleAll(@RequestParam(value = "name",required = false)String name, @RequestParam(value = "supplierId",required = false)Long supplierId) { diff --git a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/controller/SmSpuController.java b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/controller/SmSpuController.java index fd274a919..3876dcd73 100644 --- a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/controller/SmSpuController.java +++ b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/controller/SmSpuController.java @@ -206,6 +206,7 @@ public class SmSpuController { LambdaQueryWrapper smErpCategoryEntityLambdaQueryWrapper = new LambdaQueryWrapper<>(); smErpCategoryEntityLambdaQueryWrapper.in(SmErpCategoryEntity::getId, categoryIds); + smErpCategoryService.applyDefaultSort(smErpCategoryEntityLambdaQueryWrapper); List categoryEntities = smErpCategoryService.list(smErpCategoryEntityLambdaQueryWrapper); List bean = BeanUtils.toBean(categoryEntities, SmErpCategorySimpleVo.class); return R.ok(bean); diff --git a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/SmErpCategoryService.java b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/SmErpCategoryService.java index c557ff49b..6372bbaf4 100644 --- a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/SmErpCategoryService.java +++ b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/SmErpCategoryService.java @@ -23,4 +23,9 @@ public interface SmErpCategoryService extends IService { * @return */ Object pageScope(Page page, LambdaQueryWrapper wrapper); + + /** + * 分类默认排序:sort 升序,id 降序 + */ + void applyDefaultSort(LambdaQueryWrapper wrapper); } \ No newline at end of file diff --git a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/impl/SmErpCategoryServiceImpl.java b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/impl/SmErpCategoryServiceImpl.java index 8ae3ac816..432c153e0 100644 --- a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/impl/SmErpCategoryServiceImpl.java +++ b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/impl/SmErpCategoryServiceImpl.java @@ -19,13 +19,23 @@ import java.util.List; */ @Service public class SmErpCategoryServiceImpl extends ServiceImpl implements SmErpCategoryService { + + @Override + public void applyDefaultSort(LambdaQueryWrapper wrapper) { + wrapper.orderByAsc(SmErpCategoryEntity::getSort) + .orderByDesc(SmErpCategoryEntity::getId); + } + @Override public List listScope() { - return baseMapper.selectListByScope(new LambdaQueryWrapper(), DataScope.of()); + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + applyDefaultSort(wrapper); + return baseMapper.selectListByScope(wrapper, DataScope.of()); } @Override public Object pageScope(Page page, LambdaQueryWrapper wrapper) { + applyDefaultSort(wrapper); return baseMapper.selectPageByScope(page, wrapper, DataScope.of()); } -} \ No newline at end of file +} diff --git a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/impl/SmErpInventoryRecordsServiceImpl.java b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/impl/SmErpInventoryRecordsServiceImpl.java index 037c9ae79..5c14cc762 100644 --- a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/impl/SmErpInventoryRecordsServiceImpl.java +++ b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/impl/SmErpInventoryRecordsServiceImpl.java @@ -207,52 +207,15 @@ public class SmErpInventoryRecordsServiceImpl extends ServiceImpl 0) { - double totalAmount = remainingPurchasePrice * remainingQuantity + newInTotalAmount; - newCostPrice = totalAmount / totalQuantity; - - // 调试日志:打印成本价计算过程 -// System.out.println("========== 成本价计算详情 =========="); -// System.out.println("商品ID: " + erpSpuId); -// System.out.println("剩余数量: " + remainingQuantity); -// System.out.println("剩余商品进货价(基础单位采购价): " + remainingPurchasePrice + " 分/个"); -// System.out.println("剩余总金额: " + (remainingPurchasePrice * remainingQuantity) + " 分"); -// System.out.println("新进数量: " + newInTotalQuantity); -// System.out.println("新进商品进货价(realPrice): " + (newInTotalQuantity > 0 ? (newInTotalAmount / newInTotalQuantity) : 0) + " 分/个"); -// System.out.println("新进总金额: " + newInTotalAmount + " 分"); -// System.out.println("总数量: " + totalQuantity); -// System.out.println("总金额: " + totalAmount + " 分"); -// System.out.println("新成本价: " + newCostPrice + " 分/个"); -// System.out.println("新成本价(保留2位): " + String.format("%.2f", newCostPrice) + " 分/个"); -// System.out.println("===================================="); - } else { - // 如果总数量为0,使用新进商品的平均价格 - if (newInTotalQuantity > 0) { - newCostPrice = newInTotalAmount / newInTotalQuantity; - } - } + syncUnitPurchasePrice(spuItems); + syncSpuAndBaseUnitPurchasePrice(erpSpuId, spuItems); + + double newCostPrice = calcWeightedCostPrice(remainingQuantity, remainingPurchasePrice, newInTotalAmount, newInTotalQuantity); - // 更新商品成本价(double类型会自动保留精度) SmErpSpuEntity updateSpu = new SmErpSpuEntity(); updateSpu.setId(erpSpuId); updateSpu.setCostPrice(newCostPrice); @@ -262,6 +225,116 @@ public class SmErpInventoryRecordsServiceImpl extends ServiceImpl items) { + return items.stream().mapToDouble(this::calcItemBaseQuantity).sum(); + } + + /** 明细行金额(realPrice 为包装单位进价/分,金额=进价×包装数量,与前端一致) */ + private double calcItemBaseAmount(SmErpInventoryRecordsItemEntity item) { + if (item.getTotalAmount() != null) { + return item.getTotalAmount(); + } + if (item.getRealPrice() != null && item.getQuantity() != null) { + return item.getRealPrice() * item.getQuantity(); + } + return 0D; + } + + /** 将入库明细的实际进价换算为基础单位进价(分) */ + private Integer calcBaseUnitPurchasePrice(SmErpInventoryRecordsItemEntity item, SmErpUnitEntity inboundUnit, SmErpUnitEntity baseUnit) { + if (item.getRealPrice() == null) { + return null; + } + if (baseUnit != null && item.getUnitId() != null && item.getUnitId().equals(baseUnit.getId())) { + return item.getRealPrice(); + } + Integer packageNum = inboundUnit != null ? inboundUnit.getPackageNum() : item.getPackageNum(); + if (packageNum == null || packageNum <= 0) { + return item.getRealPrice(); + } + return (int) Math.round((double) item.getRealPrice() / packageNum); + } + + /** 同步 SPU、基础包装单位的进价(取本批入库最后一条明细) */ + private void syncSpuAndBaseUnitPurchasePrice(Long erpSpuId, List spuItems) { + if (CollectionUtils.isEmpty(spuItems)) { + return; + } + SmErpUnitEntity baseUnit = smErpUnitMapper.selectOne( + new LambdaQueryWrapper() + .eq(SmErpUnitEntity::getSpuId, erpSpuId) + .eq(SmErpUnitEntity::getIsBase, true) + .last("LIMIT 1") + ); + SmErpInventoryRecordsItemEntity latestItem = spuItems.get(spuItems.size() - 1); + if (latestItem.getRealPrice() == null) { + return; + } + SmErpUnitEntity inboundUnit = latestItem.getUnitId() != null + ? smErpUnitMapper.selectById(latestItem.getUnitId()) + : null; + Integer basePurchasePrice = calcBaseUnitPurchasePrice(latestItem, inboundUnit, baseUnit); + if (basePurchasePrice == null) { + return; + } + if (baseUnit != null && (inboundUnit == null || !latestItem.getUnitId().equals(baseUnit.getId()))) { + SmErpUnitEntity updateBaseUnit = new SmErpUnitEntity(); + updateBaseUnit.setId(baseUnit.getId()); + updateBaseUnit.setPurchasePrice(basePurchasePrice); + smErpUnitMapper.updateById(updateBaseUnit); + } + SmErpSpuEntity updateSpu = new SmErpSpuEntity(); + updateSpu.setId(erpSpuId); + updateSpu.setPurchasePrice(basePurchasePrice); + smErpSpuMapper.updateById(updateSpu); + } + + /** 计算加权平均成本价(分/基础单位) */ + private double calcWeightedCostPrice(double remainingQuantity, double remainingPurchasePrice, + double newInTotalAmount, double newInTotalQuantity) { + double totalQuantity = remainingQuantity + newInTotalQuantity; + if (totalQuantity > 0) { + return (remainingPurchasePrice * remainingQuantity + newInTotalAmount) / totalQuantity; + } + if (newInTotalQuantity > 0) { + return newInTotalAmount / newInTotalQuantity; + } + return 0D; + } + + private double calcItemsBaseAmount(List items) { + return items.stream().mapToDouble(this::calcItemBaseAmount).sum(); + } + + /** 同步明细实际进价到包装单位主数据 */ + private void syncUnitPurchasePrice(List itemEntities) { + if (CollectionUtils.isEmpty(itemEntities)) { + return; + } + for (SmErpInventoryRecordsItemEntity item : itemEntities) { + if (item.getUnitId() == null || item.getRealPrice() == null) { + continue; + } + SmErpUnitEntity smErpUnitEntity = new SmErpUnitEntity(); + smErpUnitEntity.setId(item.getUnitId()); + smErpUnitEntity.setPurchasePrice(item.getRealPrice()); + smErpUnitMapper.updateById(smErpUnitEntity); + } + } + private String getProcessKey(String inventoryType) { switch (inventoryType) { case "3": @@ -585,15 +658,34 @@ public class SmErpInventoryRecordsServiceImpl extends ServiceImpl smErpInventoryRecordsItemEntity.getId() != null).collect(Collectors.toList()); // 为新增的项目设置inventoryId和deptId + final String recordInventoryType; + if (inventoryRecordEditDTO.getInventoryType() != null) { + recordInventoryType = inventoryRecordEditDTO.getInventoryType(); + } else { + SmErpInventoryRecordsEntity dbRecord = baseMapper.selectById(inventoryRecordEditDTO.getId()); + recordInventoryType = dbRecord != null ? dbRecord.getInventoryType() : null; + } insertItems.forEach(insertItem -> { insertItem.setInventoryId(inventoryRecordEditDTO.getId()); insertItem.setDeptId(deptId); + if (insertItem.getInventoryType() == null) { + insertItem.setInventoryType(recordInventoryType); + } }); //更新的话,查询之前保存了哪些item,然后去库存表删掉从新计算库存 List dataBaseSmErpInventoryRecordsItemEntityList = smErpInventoryRecordsItemMapper .selectList(new LambdaQueryWrapper().eq(SmErpInventoryRecordsItemEntity::getInventoryId, inventoryRecordEditDTO.getId())); + List submittedItemIds = items.stream() + .map(InventoryRecordEditItemDTO::getId) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + List toDeleteItemIds = dataBaseSmErpInventoryRecordsItemEntityList.stream() + .map(SmErpInventoryRecordsItemEntity::getId) + .filter(id -> !submittedItemIds.contains(id)) + .collect(Collectors.toList()); + //这部分是数据库原本添加的库存数据,需要全部减掉,重新添加更新的库存数据进去 dataBaseSmErpInventoryRecordsItemEntityList.forEach(item -> { // 使用保存时的内装数,而不是查询最新的内装数,这样即使内装数改变了也能正确计算 @@ -615,6 +707,10 @@ public class SmErpInventoryRecordsServiceImpl extends ServiceImpl smErpInventoryRecordsItemMapper.updateById(item)); @@ -624,27 +720,6 @@ public class SmErpInventoryRecordsServiceImpl extends ServiceImpl existingCodeIds = smErpInventoryRecordsItemMapper.selectList( - new LambdaQueryWrapper() - .eq(SmErpInventoryRecordsItemEntity::getInventoryId, smErpInventoryRecordsEntity.getId()) - ).stream().map(SmErpInventoryRecordsItemEntity::getId).collect(Collectors.toList()); - - // 收集本次提交的ID(排除新增的) - List submittedCodeIds = updateItems.stream() - .map(SmErpInventoryRecordsItemEntity::getId) - .filter(Objects::nonNull) - .collect(Collectors.toList()); - - // 计算需要删除的ID - List toDeleteCodeIds = existingCodeIds.stream() - .filter(id -> !submittedCodeIds.contains(id)) - .collect(Collectors.toList()); - - // 批量删除失效条码 - if (!toDeleteCodeIds.isEmpty()) { - smErpInventoryRecordsItemMapper.deleteByIds(toDeleteCodeIds); - } - // 重新计算主表的totalAmount List finalItems = smErpInventoryRecordsItemMapper.selectList( new LambdaQueryWrapper() @@ -666,25 +741,20 @@ public class SmErpInventoryRecordsServiceImpl extends ServiceImpl savedItems = smErpInventoryRecordsItemMapper.selectList( new LambdaQueryWrapper() .eq(SmErpInventoryRecordsItemEntity::getInventoryId, inventoryRecordEditDTO.getId()) ); - // 获取涉及的商品ID List currentSpuIds = savedItems.stream() .map(SmErpInventoryRecordsItemEntity::getErpSpuId) .distinct() .collect(Collectors.toList()); if (!CollectionUtils.isEmpty(currentSpuIds)) { - // 获取当前库存信息和商品信息 List stockList = smErpStockService.list( new LambdaQueryWrapper().in(SmErpStockEntity::getErpSpuId, currentSpuIds) ); @@ -697,107 +767,38 @@ public class SmErpInventoryRecordsServiceImpl extends ServiceImpl spuMap = spuList.stream() .collect(Collectors.toMap(SmErpSpuEntity::getId, spu -> spu)); - // 按商品分组,计算每个商品的加权平均成本价 Map> itemsBySpuId = savedItems.stream() .collect(Collectors.groupingBy(SmErpInventoryRecordsItemEntity::getErpSpuId)); + Map> oldItemsBySpuId = dataBaseSmErpInventoryRecordsItemEntityList.stream() + .collect(Collectors.groupingBy(SmErpInventoryRecordsItemEntity::getErpSpuId)); itemsBySpuId.forEach((erpSpuId, spuItems) -> { - // 获取当前库存数量(已经更新后的库存) + List oldSpuItems = oldItemsBySpuId.getOrDefault(erpSpuId, Collections.emptyList()); + double oldBatchAmount = calcItemsBaseAmount(oldSpuItems); + double newBatchAmount = calcItemsBaseAmount(spuItems); + SmErpStockEntity stock = stockMap.get(erpSpuId); - double currentStockQuantity = 0; - - if (stock != null) { - currentStockQuantity = stock.getCurrentStock(); - } + double currentStockQuantity = stock != null && stock.getCurrentStock() != null ? stock.getCurrentStock() : 0D; - // 计算本次编辑的入库数量 - double editInQuantity = spuItems.stream() - .mapToDouble(item -> { - Integer pkgNum = item.getPackageNum(); - if (pkgNum == null) { - SmErpUnitEntity unit = smErpUnitMapper.selectById(item.getUnitId()); - pkgNum = unit != null ? unit.getPackageNum() : 1; - } - return pkgNum * item.getQuantity(); - }) - .sum(); + SmErpSpuEntity spu = spuMap.get(erpSpuId); + double currentCostPrice = spu != null && spu.getCostPrice() != null ? spu.getCostPrice() : 0D; - // 剩余数量 = 当前库存 - 本次入库数量 - double remainingQuantity = currentStockQuantity - editInQuantity; - if (remainingQuantity < 0) { - remainingQuantity = 0; - } + syncUnitPurchasePrice(spuItems); + syncSpuAndBaseUnitPurchasePrice(erpSpuId, spuItems); - // 获取剩余商品的进货价(从基础单位的采购价获取,单位:分) - double remainingPurchasePrice = 0; - LambdaQueryWrapper baseUnitWrapper = new LambdaQueryWrapper<>(); - baseUnitWrapper.eq(SmErpUnitEntity::getSpuId, erpSpuId); - baseUnitWrapper.eq(SmErpUnitEntity::getIsBase, true); - SmErpUnitEntity baseUnit = smErpUnitMapper.selectOne(baseUnitWrapper); - if (baseUnit != null && baseUnit.getPurchasePrice() != null) { - remainingPurchasePrice = baseUnit.getPurchasePrice(); - } - - // 计算新进(编辑后)商品的总金额和总数量 - double newInTotalAmount = 0; - double newInTotalQuantity = 0; - - for (SmErpInventoryRecordsItemEntity item : spuItems) { - // 获取内装数 - Integer pkgNum = item.getPackageNum(); - if (pkgNum == null) { - SmErpUnitEntity unit = smErpUnitMapper.selectById(item.getUnitId()); - pkgNum = unit != null ? unit.getPackageNum() : 1; - } - - // 新进数量 = 包装数量 * 内装数 - double quantity = pkgNum * item.getQuantity(); - newInTotalQuantity += quantity; - - // 新进商品总金额 = 实际进价 * 数量 - if (item.getRealPrice() != null) { - newInTotalAmount += item.getRealPrice() * quantity; - } - - //更新实际进价到商品包装单位里 - if (item.getRealPrice() != null) { - SmErpUnitEntity smErpUnitEntity = new SmErpUnitEntity(); - smErpUnitEntity.setPurchasePrice(item.getRealPrice()); - smErpUnitEntity.setId(item.getUnitId()); - smErpUnitMapper.updateById(smErpUnitEntity); - } - } - - // 计算加权平均成本价 = (剩余商品进货价*剩余数量 + 新进商品进货价*新进数量) / (剩余数量 + 新进数量) - double newCostPrice = 0; - double totalQuantity = remainingQuantity + newInTotalQuantity; - - if (totalQuantity > 0) { - double totalAmount = remainingPurchasePrice * remainingQuantity + newInTotalAmount; - newCostPrice = totalAmount / totalQuantity; - - // 调试日志:打印成本价计算过程(编辑) - System.out.println("========== 成本价计算详情(编辑) =========="); - System.out.println("商品ID: " + erpSpuId); - System.out.println("剩余数量: " + remainingQuantity); - System.out.println("剩余商品进货价(基础单位采购价): " + remainingPurchasePrice + " 分/个"); - System.out.println("剩余总金额: " + (remainingPurchasePrice * remainingQuantity) + " 分"); - System.out.println("新进数量: " + newInTotalQuantity); - System.out.println("新进商品进货价(realPrice): " + (newInTotalQuantity > 0 ? (newInTotalAmount / newInTotalQuantity) : 0) + " 分/个"); - System.out.println("新进总金额: " + newInTotalAmount + " 分"); - System.out.println("总数量: " + totalQuantity); - System.out.println("总金额: " + totalAmount + " 分"); - System.out.println("新成本价: " + newCostPrice + " 分/个"); - System.out.println("新成本价(保留2位): " + String.format("%.2f", newCostPrice) + " 分/个"); - System.out.println("=========================================="); + double newCostPrice; + if (currentStockQuantity <= 0) { + double newBatchQty = calcItemsBaseQuantity(spuItems); + newCostPrice = newBatchQty > 0 ? newBatchAmount / newBatchQty : 0D; } else { - // 如果总数量为0,使用新进商品的平均价格 - if (newInTotalQuantity > 0) { - newCostPrice = newInTotalAmount / newInTotalQuantity; + double inventoryValue = currentCostPrice * currentStockQuantity - oldBatchAmount + newBatchAmount; + newCostPrice = inventoryValue / currentStockQuantity; + if (newCostPrice < 0) { + double newBatchQty = calcItemsBaseQuantity(spuItems); + newCostPrice = newBatchQty > 0 ? newBatchAmount / newBatchQty : 0D; } } - // 更新商品成本价 SmErpSpuEntity updateSpu = new SmErpSpuEntity(); updateSpu.setId(erpSpuId); updateSpu.setCostPrice(newCostPrice); @@ -992,6 +993,15 @@ public class SmErpInventoryRecordsServiceImpl extends ServiceImpl dataBaseSmErpInventoryRecordsItemEntityList = smErpInventoryRecordsItemMapper .selectList(new LambdaQueryWrapper().eq(SmErpInventoryRecordsItemEntity::getInventoryId, pdfUpdateDTO.getId())); + Set submittedSpuIds = items.stream() + .map(PDItemUpdateDTO::getErpSpuId) + .filter(Objects::nonNull) + .collect(Collectors.toSet()); + List toDeleteItemIds = dataBaseSmErpInventoryRecordsItemEntityList.stream() + .filter(item -> !submittedSpuIds.contains(item.getErpSpuId())) + .map(SmErpInventoryRecordsItemEntity::getId) + .collect(Collectors.toList()); + //这部分是数据库原本添加的库存数据,需要全部减掉,重新添加更新的库存数据进去 dataBaseSmErpInventoryRecordsItemEntityList.forEach(item -> { UpdateStockDTO updateStockDTO = new UpdateStockDTO(); @@ -1000,6 +1010,10 @@ public class SmErpInventoryRecordsServiceImpl extends ServiceImpl smErpInventoryRecordsItemMapper.updateById(item)); @@ -1009,27 +1023,6 @@ public class SmErpInventoryRecordsServiceImpl extends ServiceImpl existingCodeIds = smErpInventoryRecordsItemMapper.selectList( - new LambdaQueryWrapper() - .eq(SmErpInventoryRecordsItemEntity::getInventoryId, smErpInventoryRecordsEntity.getId()) - ).stream().map(SmErpInventoryRecordsItemEntity::getId).collect(Collectors.toList()); - - // 收集本次提交的ID(排除新增的) - List submittedCodeIds = updateItems.stream() - .map(SmErpInventoryRecordsItemEntity::getId) - .filter(Objects::nonNull) - .collect(Collectors.toList()); - - // 计算需要删除的ID - List toDeleteCodeIds = existingCodeIds.stream() - .filter(id -> !submittedCodeIds.contains(id)) - .collect(Collectors.toList()); - - // 批量删除失效条码 - if (!toDeleteCodeIds.isEmpty()) { - smErpInventoryRecordsItemMapper.deleteByIds(toDeleteCodeIds); - } - //todo 实际上应该审批后才会入库 //更新库存 smErpStockService.updateStock(updateStockDTOS); @@ -1161,6 +1154,8 @@ public class SmErpInventoryRecordsServiceImpl extends ServiceImpl wrapper = Wrappers.lambdaQuery(); // wrapper.eq(StringUtils.isNotBlank(supermarketSpu.getProductCode()), SmErpSpuEntity::getProductCode,supermarketSpu.getProductCode()); // wrapper.eq(StringUtils.isNotBlank(supermarketSpu.getBarCode()), SmErpSpuEntity::getBarCode,supermarketSpu.getBarCode()); @@ -712,6 +713,23 @@ public class SmErpSpuServiceImpl extends ServiceImpl selectListAllScope(@ParameterObject Page page, SupermarketSpuQueryDTO supermarketSpuQueryDTO) { diff --git a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/impl/SmErpSupplierServiceImpl.java b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/impl/SmErpSupplierServiceImpl.java index 74ff30028..51312d6e9 100644 --- a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/impl/SmErpSupplierServiceImpl.java +++ b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/impl/SmErpSupplierServiceImpl.java @@ -39,10 +39,17 @@ public class SmErpSupplierServiceImpl extends ServiceImpl selectSimpleAllScope(String name,Long supplierId) { - LambdaQueryWrapper wrapper = new LambdaQueryWrapper(); - wrapper.eq(SmErpSupplierEntity::getStatus, "0"); - wrapper.like(StringUtils.isNotBlank(name),SmErpSupplierEntity::getSupplierName,name); - wrapper.eq(supplierId!=null,SmErpSupplierEntity::getSupplierId,supplierId); + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(SmErpSupplierEntity::getStatus, "0"); + if (supplierId != null) { + wrapper.eq(SmErpSupplierEntity::getSupplierId, supplierId); + } else if (StringUtils.isNotBlank(name)) { + wrapper.and(w -> w.like(SmErpSupplierEntity::getSupplierName, name) + .or().like(SmErpSupplierEntity::getPinyinCod, name) + .or().like(SmErpSupplierEntity::getLinkName, name) + .or().like(SmErpSupplierEntity::getPhone, name)); + } + wrapper.orderByAsc(SmErpSupplierEntity::getSupplierName); return baseMapper.selectListByScope(wrapper, DataScope.of()); } diff --git a/pjcloud-upms/pjcloud-upms-api/src/main/java/com/bsd/pjcloud/admin/api/vo/WxSocialResolveVO.java b/pjcloud-upms/pjcloud-upms-api/src/main/java/com/bsd/pjcloud/admin/api/vo/WxSocialResolveVO.java new file mode 100644 index 000000000..976a47897 --- /dev/null +++ b/pjcloud-upms/pjcloud-upms-api/src/main/java/com/bsd/pjcloud/admin/api/vo/WxSocialResolveVO.java @@ -0,0 +1,21 @@ +package com.bsd.pjcloud.admin.api.vo; + +import lombok.Data; + +/** + * App 微信授权解析结果(code 仅兑换一次,后续用 ticket 登录/绑定) + */ +@Data +public class WxSocialResolveVO { + + /** + * 是否已绑定系统账号 + */ + private Boolean bound; + + /** + * 一次性凭证,用于登录或绑定 + */ + private String ticket; + +} diff --git a/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/controller/SysSocialDetailsController.java b/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/controller/SysSocialDetailsController.java index 3b71b8859..4c9033438 100644 --- a/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/controller/SysSocialDetailsController.java +++ b/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/controller/SysSocialDetailsController.java @@ -23,6 +23,7 @@ import com.baomidou.mybatisplus.core.toolkit.Wrappers; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.bsd.pjcloud.admin.api.entity.SysSocialDetails; import com.bsd.pjcloud.admin.service.SysSocialDetailsService; +import com.bsd.pjcloud.admin.api.vo.WxSocialResolveVO; import com.bsd.pjcloud.common.core.util.R; import com.bsd.pjcloud.common.core.util.ValidGroup; import com.bsd.pjcloud.common.log.annotation.SysLog; @@ -150,6 +151,23 @@ public class SysSocialDetailsController { return R.ok(sysSocialDetailsService.bindSocial(state, code)); } + /** + * App 微信:解析 code(一次性),返回绑定状态与 ticket + */ + @Inner(value = false) + @PostMapping("/wx/resolve") + public R resolveWxCode(String code) { + return R.ok(sysSocialDetailsService.resolveWxCode(code)); + } + + /** + * App 微信:使用 ticket 绑定(需已登录) + */ + @PostMapping("/bind-ticket") + public R bindSocialByTicket(String state, String ticket) { + return R.ok(sysSocialDetailsService.bindSocialByTicket(state, ticket)); + } + /** * 导出 */ diff --git a/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/handler/WeChatLoginHandler.java b/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/handler/WeChatLoginHandler.java index 2f417b590..1022b0072 100644 --- a/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/handler/WeChatLoginHandler.java +++ b/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/handler/WeChatLoginHandler.java @@ -18,6 +18,7 @@ package com.bsd.pjcloud.admin.handler; import cn.hutool.http.HttpUtil; +import cn.hutool.json.JSONObject; import cn.hutool.json.JSONUtil; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.core.toolkit.Wrappers; @@ -28,6 +29,7 @@ import com.bsd.pjcloud.admin.mapper.SysSocialDetailsMapper; import com.bsd.pjcloud.admin.service.SysUserService; import com.bsd.pjcloud.common.core.constant.SecurityConstants; import com.bsd.pjcloud.common.core.constant.enums.LoginTypeEnum; +import com.bsd.pjcloud.common.core.exception.CheckedException; import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; @@ -63,7 +65,18 @@ public class WeChatLoginHandler extends AbstractLoginHandler { String result = HttpUtil.get(url); log.debug("微信响应报文:{}", result); - Object obj = JSONUtil.parseObj(result).get("openid"); + JSONObject resultJsonObj = JSONUtil.parseObj(result); + if (resultJsonObj.containsKey("errcode")) { + log.error("微信授权失败:{}", result); + String errMsg = resultJsonObj.getStr("errmsg", "微信授权失败"); + throw new CheckedException("微信授权失败:" + errMsg); + } + + Object obj = resultJsonObj.get("openid"); + if (obj == null) { + log.error("微信响应缺少openid:{}", result); + throw new CheckedException("微信授权失败,未获取到用户标识"); + } return obj.toString(); } diff --git a/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/handler/WxTicketLoginHandler.java b/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/handler/WxTicketLoginHandler.java new file mode 100644 index 000000000..1d41b5813 --- /dev/null +++ b/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/handler/WxTicketLoginHandler.java @@ -0,0 +1,64 @@ +package com.bsd.pjcloud.admin.handler; + +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.bsd.pjcloud.admin.api.dto.UserInfo; +import com.bsd.pjcloud.admin.api.entity.SysUser; +import com.bsd.pjcloud.admin.service.SysUserService; +import com.bsd.pjcloud.common.core.constant.CacheConstants; +import com.bsd.pjcloud.common.core.exception.CheckedException; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Component; + +import java.util.concurrent.TimeUnit; + +/** + * 微信 App 登录 ticket 处理器(mobile=WX_TICKET@{ticket}) + */ +@Slf4j +@Component("WX_TICKET") +@RequiredArgsConstructor +public class WxTicketLoginHandler implements LoginHandler { + + private final StringRedisTemplate redisTemplate; + + private final SysUserService sysUserService; + + private String ticketKey(String ticket) { + return CacheConstants.WX_SOCIAL_TICKET + ticket; + } + + @Override + public Boolean check(String loginStr) { + return true; + } + + @Override + public String identify(String ticket) { + String openid = redisTemplate.opsForValue().get(ticketKey(ticket)); + if (StrUtil.isBlank(openid)) { + throw new CheckedException("微信授权已过期,请重新授权"); + } + redisTemplate.delete(ticketKey(ticket)); + return openid; + } + + @Override + public UserInfo info(String openid) { + SysUser user = sysUserService.getOne(Wrappers.lambdaQuery().eq(SysUser::getWxOpenid, openid)); + if (user == null) { + log.info("微信未绑定:{}", openid); + return null; + } + return sysUserService.findUserInfo(user); + } + + @Override + public UserInfo handle(String loginStr) { + String openid = identify(loginStr); + return info(openid); + } + +} diff --git a/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/service/SysSocialDetailsService.java b/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/service/SysSocialDetailsService.java index b72fa1b26..a74155834 100644 --- a/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/service/SysSocialDetailsService.java +++ b/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/service/SysSocialDetailsService.java @@ -21,6 +21,7 @@ import com.baomidou.mybatisplus.extension.service.IService; import com.bsd.pjcloud.admin.api.dto.UserInfo; import com.bsd.pjcloud.admin.api.entity.SysSocialDetails; import com.bsd.pjcloud.admin.api.vo.SysSocialDetailsVO; +import com.bsd.pjcloud.admin.api.vo.WxSocialResolveVO; import java.util.List; @@ -40,6 +41,16 @@ public interface SysSocialDetailsService extends IService { */ Boolean bindSocial(String state, String code); + /** + * 使用一次性 ticket 绑定(App 微信授权后) + */ + Boolean bindSocialByTicket(String type, String ticket); + + /** + * 解析微信 code(仅兑换一次),返回是否已绑定及 ticket + */ + WxSocialResolveVO resolveWxCode(String code); + /** * 根据入参查询用户信息 * @param inStr diff --git a/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/service/impl/SysSocialDetailsServiceImpl.java b/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/service/impl/SysSocialDetailsServiceImpl.java index 4319928f9..bedb9f034 100644 --- a/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/service/impl/SysSocialDetailsServiceImpl.java +++ b/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/service/impl/SysSocialDetailsServiceImpl.java @@ -24,20 +24,26 @@ import com.bsd.pjcloud.admin.api.dto.UserInfo; import com.bsd.pjcloud.admin.api.entity.SysSocialDetails; import com.bsd.pjcloud.admin.api.entity.SysUser; import com.bsd.pjcloud.admin.api.vo.SysSocialDetailsVO; +import com.bsd.pjcloud.admin.api.vo.WxSocialResolveVO; import com.bsd.pjcloud.admin.handler.LoginHandler; import com.bsd.pjcloud.admin.mapper.SysSocialDetailsMapper; import com.bsd.pjcloud.admin.mapper.SysUserMapper; import com.bsd.pjcloud.admin.service.SysSocialDetailsService; import com.bsd.pjcloud.common.core.constant.CacheConstants; +import com.bsd.pjcloud.common.core.exception.CheckedException; import com.bsd.pjcloud.common.security.util.SecurityUtils; +import cn.hutool.core.util.IdUtil; +import cn.hutool.core.util.StrUtil; import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.BeanUtils; import org.springframework.cache.CacheManager; +import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Service; import java.util.List; import java.util.Map; +import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; /** @@ -56,6 +62,14 @@ public class SysSocialDetailsServiceImpl extends ServiceImpl Date: Fri, 19 Jun 2026 04:36:12 +0800 Subject: [PATCH 2/3] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E5=AF=86=E7=A0=81?= =?UTF-8?q?=E6=8E=A5=E5=8F=A3=E8=A7=A3=E5=AF=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/bsd/pjcloud/admin/controller/SysUserController.java | 1 + 1 file changed, 1 insertion(+) diff --git a/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/controller/SysUserController.java b/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/controller/SysUserController.java index 87c9c5ba5..d2a8fa06d 100644 --- a/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/controller/SysUserController.java +++ b/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/controller/SysUserController.java @@ -334,6 +334,7 @@ public class SysUserController { * @param userDto 用户DTO对象,包含需要修改密码的用户信息 * @return R 返回结果对象,包含修改密码操作的结果信息 */ + @ApiCryptoAes @PutMapping("/personal/password") public R updatePassword(@RequestBody UserDTO userDto) { String username = SecurityUtils.getUser().getUsername(); From 39878e0aa53633c9fb85120c3cbb8a88839c8d0c Mon Sep 17 00:00:00 2001 From: zjf <1799650030@qq.com> Date: Mon, 22 Jun 2026 17:32:13 +0800 Subject: [PATCH 3/3] =?UTF-8?q?=E5=90=8E=E5=8F=B0=E7=99=BB=E5=BD=95?= =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E4=BA=8C=E6=AC=A1=E8=AE=A4=E8=AF=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../support/PcWechatPasswordLoginGate.java | 120 +++++++++++ .../support/PcWechatSecurityProperties.java | 36 ++++ ...sourceOwnerBaseAuthenticationProvider.java | 15 ++ .../main/resources/application-pc-wechat.yml | 6 + .../src/main/resources/application.yml | 1 + .../common/core/constant/CacheConstants.java | 15 ++ .../core/constant/enums/LoginTypeEnum.java | 7 +- .../WechatBindRequiredException.java | 21 ++ .../WechatConfirmRequiredException.java | 21 ++ .../security/util/OAuth2ErrorCodesExpand.java | 6 + .../bsd/pjcloud/admin/api/entity/SysUser.java | 6 + .../admin/api/vo/SysSocialDetailsVO.java | 7 + .../com/bsd/pjcloud/admin/api/vo/UserVO.java | 6 + .../SysSocialDetailsController.java | 30 +++ .../admin/handler/WeChatPcLoginHandler.java | 78 +++++++ .../admin/handler/WxPcTicketLoginHandler.java | 62 ++++++ .../admin/service/PcWechatSocialService.java | 25 +++ .../impl/PcWechatSocialServiceImpl.java | 203 ++++++++++++++++++ .../impl/SysSocialDetailsServiceImpl.java | 34 ++- .../service/impl/SysUserServiceImpl.java | 2 + .../main/resources/mapper/SysUserMapper.xml | 5 + 21 files changed, 701 insertions(+), 5 deletions(-) create mode 100644 pjcloud-auth/src/main/java/com/bsd/pjcloud/auth/support/PcWechatPasswordLoginGate.java create mode 100644 pjcloud-auth/src/main/java/com/bsd/pjcloud/auth/support/PcWechatSecurityProperties.java create mode 100644 pjcloud-auth/src/main/resources/application-pc-wechat.yml create mode 100644 pjcloud-common/pjcloud-common-security/src/main/java/com/bsd/pjcloud/common/security/exception/WechatBindRequiredException.java create mode 100644 pjcloud-common/pjcloud-common-security/src/main/java/com/bsd/pjcloud/common/security/exception/WechatConfirmRequiredException.java create mode 100644 pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/handler/WeChatPcLoginHandler.java create mode 100644 pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/handler/WxPcTicketLoginHandler.java create mode 100644 pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/service/PcWechatSocialService.java create mode 100644 pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/service/impl/PcWechatSocialServiceImpl.java diff --git a/pjcloud-auth/src/main/java/com/bsd/pjcloud/auth/support/PcWechatPasswordLoginGate.java b/pjcloud-auth/src/main/java/com/bsd/pjcloud/auth/support/PcWechatPasswordLoginGate.java new file mode 100644 index 000000000..54eded18a --- /dev/null +++ b/pjcloud-auth/src/main/java/com/bsd/pjcloud/auth/support/PcWechatPasswordLoginGate.java @@ -0,0 +1,120 @@ +package com.bsd.pjcloud.auth.support; + +import cn.hutool.core.util.BooleanUtil; +import cn.hutool.core.util.IdUtil; +import cn.hutool.core.util.StrUtil; +import com.bsd.pjcloud.admin.api.dto.UserInfo; +import com.bsd.pjcloud.admin.api.feign.RemoteUserService; +import com.bsd.pjcloud.common.core.constant.CacheConstants; +import com.bsd.pjcloud.common.core.util.R; +import com.bsd.pjcloud.common.core.util.RetOps; +import com.bsd.pjcloud.common.data.resolver.ParamResolver; +import com.bsd.pjcloud.common.security.exception.WechatBindRequiredException; +import com.bsd.pjcloud.common.security.exception.WechatConfirmRequiredException; +import com.bsd.pjcloud.common.security.service.PjcloudUser; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.security.core.Authentication; +import org.springframework.security.oauth2.server.authorization.client.RegisteredClient; +import org.springframework.stereotype.Component; + +import java.util.concurrent.TimeUnit; + +/** + * PC 后台(pig 客户端)密码登录:可选绑定 / 扫码确认 PC 微信 + */ +@Slf4j +@Component +@RequiredArgsConstructor +public class PcWechatPasswordLoginGate { + + private static final String PC_PASSWORD_CLIENT_ID = "pig"; + + private static final String PARAM_BIND_REQUIRED = "PC_WECHAT_BIND_REQUIRED"; + + private static final String PARAM_CONFIRM_REQUIRED = "PC_WECHAT_CONFIRM_REQUIRED"; + + private static final long TICKET_TTL_MINUTES = 15; + + private final RemoteUserService remoteUserService; + + private final StringRedisTemplate redisTemplate; + + private final PcWechatSecurityProperties pcWechatSecurityProperties; + + public void check(RegisteredClient registeredClient, Authentication authentication) { + if (registeredClient == null || !PC_PASSWORD_CLIENT_ID.equals(registeredClient.getClientId())) { + return; + } + if (!isBindRequired() && !isConfirmRequired()) { + return; + } + Object principal = authentication.getPrincipal(); + if (!(principal instanceof PjcloudUser pjcloudUser)) { + return; + } + R userResult = remoteUserService.getInnerByUserId(pjcloudUser.getId()); + UserInfo userInfo = RetOps.of(userResult).getData().orElse(null); + if (userInfo == null || userInfo.getSysUser() == null) { + return; + } + String wxPcOpenid = userInfo.getSysUser().getWxPcOpenid(); + String ticket = IdUtil.simpleUUID(); + if (StrUtil.isBlank(wxPcOpenid)) { + if (!isBindRequired() && !isConfirmRequired()) { + return; + } + // 未绑定:只要开启了绑定或扫码确认,均须先扫码绑定 + redisTemplate.opsForValue().set(pwdVerifiedKey(ticket), String.valueOf(pjcloudUser.getId()), TICKET_TTL_MINUTES, + TimeUnit.MINUTES); + throw new WechatBindRequiredException(ticket); + } + if (!isConfirmRequired()) { + return; + } + redisTemplate.opsForValue().set(loginConfirmKey(ticket), String.valueOf(pjcloudUser.getId()), TICKET_TTL_MINUTES, + TimeUnit.MINUTES); + throw new WechatConfirmRequiredException(ticket); + } + + private boolean isBindRequired() { + return resolveFlag(PARAM_BIND_REQUIRED, pcWechatSecurityProperties.isBindRequired()); + } + + private boolean isConfirmRequired() { + return resolveFlag(PARAM_CONFIRM_REQUIRED, pcWechatSecurityProperties.isConfirmRequired()); + } + + private boolean resolveFlag(String paramKey, boolean defaultValue) { + try { + String value = ParamResolver.getStr(paramKey); + if (StrUtil.isNotBlank(value)) { + return parseBool(value); + } + } + catch (Exception e) { + log.debug("读取公共参数 {} 失败,使用配置文件默认值", paramKey); + } + return defaultValue; + } + + private boolean parseBool(String value) { + if ("1".equals(value) || "Y".equalsIgnoreCase(value) || "yes".equalsIgnoreCase(value)) { + return true; + } + if ("0".equals(value) || "N".equalsIgnoreCase(value) || "no".equalsIgnoreCase(value)) { + return false; + } + return BooleanUtil.toBoolean(value); + } + + private String pwdVerifiedKey(String ticket) { + return CacheConstants.PC_PWD_VERIFIED + ticket; + } + + private String loginConfirmKey(String ticket) { + return CacheConstants.PC_LOGIN_CONFIRM + ticket; + } + +} diff --git a/pjcloud-auth/src/main/java/com/bsd/pjcloud/auth/support/PcWechatSecurityProperties.java b/pjcloud-auth/src/main/java/com/bsd/pjcloud/auth/support/PcWechatSecurityProperties.java new file mode 100644 index 000000000..a82298bee --- /dev/null +++ b/pjcloud-auth/src/main/java/com/bsd/pjcloud/auth/support/PcWechatSecurityProperties.java @@ -0,0 +1,36 @@ +package com.bsd.pjcloud.auth.support; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.cloud.context.config.annotation.RefreshScope; +import org.springframework.stereotype.Component; + +/** + * PC 后台密码登录微信校验(开发/生产可关) + *

+ * 配置示例(pjcloud-auth application.yml 或 Nacos): + *

+ * security:
+ *   pc-wechat:
+ *     bind-required: false
+ *     confirm-required: false
+ * 
+ * 也可在「参数管理」覆盖:PC_WECHAT_BIND_REQUIRED、PC_WECHAT_CONFIRM_REQUIRED(true/false 或 1/0) + */ +@Data +@Component +@RefreshScope +@ConfigurationProperties("security.pc-wechat") +public class PcWechatSecurityProperties { + + /** + * 密码登录后未绑定 PC 微信时是否拦截(须先扫码绑定) + */ + private boolean bindRequired = false; + + /** + * 已绑定 PC 微信时,密码登录是否须微信扫码确认 + */ + private boolean confirmRequired = false; + +} diff --git a/pjcloud-auth/src/main/java/com/bsd/pjcloud/auth/support/base/OAuth2ResourceOwnerBaseAuthenticationProvider.java b/pjcloud-auth/src/main/java/com/bsd/pjcloud/auth/support/base/OAuth2ResourceOwnerBaseAuthenticationProvider.java index 359ac0851..afb4a3e28 100644 --- a/pjcloud-auth/src/main/java/com/bsd/pjcloud/auth/support/base/OAuth2ResourceOwnerBaseAuthenticationProvider.java +++ b/pjcloud-auth/src/main/java/com/bsd/pjcloud/auth/support/base/OAuth2ResourceOwnerBaseAuthenticationProvider.java @@ -2,7 +2,10 @@ package com.bsd.pjcloud.auth.support.base; import cn.hutool.core.util.BooleanUtil; import cn.hutool.extra.spring.SpringUtil; +import com.bsd.pjcloud.auth.support.PcWechatPasswordLoginGate; import com.bsd.pjcloud.common.core.constant.CommonConstants; +import com.bsd.pjcloud.common.security.exception.WechatBindRequiredException; +import com.bsd.pjcloud.common.security.exception.WechatConfirmRequiredException; import com.bsd.pjcloud.common.security.service.PjcloudRedisOAuth2AuthorizationService; import com.bsd.pjcloud.common.security.util.OAuth2ErrorCodesExpand; import com.bsd.pjcloud.common.security.util.ScopeException; @@ -148,6 +151,10 @@ public abstract class OAuth2ResourceOwnerBaseAuthenticationProvider implements Serializable { @Schema(description = "微信openid") private String wxOpenid; + /** + * PC 后台微信 openid(网站应用) + */ + @Schema(description = "PC微信openid") + private String wxPcOpenid; + /** * 企微微信 userid */ diff --git a/pjcloud-upms/pjcloud-upms-api/src/main/java/com/bsd/pjcloud/admin/api/vo/SysSocialDetailsVO.java b/pjcloud-upms/pjcloud-upms-api/src/main/java/com/bsd/pjcloud/admin/api/vo/SysSocialDetailsVO.java index 73561d756..00a53ef9c 100644 --- a/pjcloud-upms/pjcloud-upms-api/src/main/java/com/bsd/pjcloud/admin/api/vo/SysSocialDetailsVO.java +++ b/pjcloud-upms/pjcloud-upms-api/src/main/java/com/bsd/pjcloud/admin/api/vo/SysSocialDetailsVO.java @@ -29,4 +29,11 @@ public class SysSocialDetailsVO { */ @Schema(description = "扩展字段") private String ext; + + /** + * 授权回调地址(PC 微信 qrconnect 使用,不含 #) + */ + @Schema(description = "回调地址") + private String redirectUrl; + } diff --git a/pjcloud-upms/pjcloud-upms-api/src/main/java/com/bsd/pjcloud/admin/api/vo/UserVO.java b/pjcloud-upms/pjcloud-upms-api/src/main/java/com/bsd/pjcloud/admin/api/vo/UserVO.java index 9ef93898c..b0e5fd8b1 100644 --- a/pjcloud-upms/pjcloud-upms-api/src/main/java/com/bsd/pjcloud/admin/api/vo/UserVO.java +++ b/pjcloud-upms/pjcloud-upms-api/src/main/java/com/bsd/pjcloud/admin/api/vo/UserVO.java @@ -91,6 +91,12 @@ public class UserVO implements BaseFacesInterface, Serializable { @Schema(description = "微信open id") private String wxOpenid; + /** + * PC 微信 openid + */ + @Schema(description = "PC微信open id") + private String wxPcOpenid; + /** * QQ openid */ diff --git a/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/controller/SysSocialDetailsController.java b/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/controller/SysSocialDetailsController.java index 4c9033438..b035f2868 100644 --- a/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/controller/SysSocialDetailsController.java +++ b/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/controller/SysSocialDetailsController.java @@ -22,6 +22,7 @@ import cn.hutool.core.util.ArrayUtil; import com.baomidou.mybatisplus.core.toolkit.Wrappers; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.bsd.pjcloud.admin.api.entity.SysSocialDetails; +import com.bsd.pjcloud.admin.service.PcWechatSocialService; import com.bsd.pjcloud.admin.service.SysSocialDetailsService; import com.bsd.pjcloud.admin.api.vo.WxSocialResolveVO; import com.bsd.pjcloud.common.core.util.R; @@ -55,6 +56,8 @@ public class SysSocialDetailsController { private final SysSocialDetailsService sysSocialDetailsService; + private final PcWechatSocialService pcWechatSocialService; + /** * 社交登录账户简单分页查询 * @param page 分页对象 @@ -168,6 +171,33 @@ public class SysSocialDetailsController { return R.ok(sysSocialDetailsService.bindSocialByTicket(state, ticket)); } + /** + * PC 微信:解析 code,返回绑定状态与 ticket + */ + @Inner(value = false) + @PostMapping("/pc/wx/resolve") + public R resolveWxPcCode(String code) { + return R.ok(pcWechatSocialService.resolveWxPcCode(code)); + } + + /** + * PC 密码已校验、未绑定:扫码绑定并登录 + */ + @Inner(value = false) + @PostMapping("/pc/bind-by-pwd-ticket") + public R> bindPcByPwdTicket(String pwdTicket, String code) { + return R.ok(pcWechatSocialService.bindByPwdTicket(pwdTicket, code)); + } + + /** + * PC 密码已校验、已绑定:扫码确认登录 + */ + @Inner(value = false) + @PostMapping("/pc/confirm-login") + public R> confirmPcLogin(String ticket, String code) { + return R.ok(pcWechatSocialService.confirmLogin(ticket, code)); + } + /** * 导出 */ diff --git a/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/handler/WeChatPcLoginHandler.java b/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/handler/WeChatPcLoginHandler.java new file mode 100644 index 000000000..9304e13bb --- /dev/null +++ b/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/handler/WeChatPcLoginHandler.java @@ -0,0 +1,78 @@ +/* + * PC 后台微信登录(网站应用 qrconnect,与移动端 WX 分离) + */ +package com.bsd.pjcloud.admin.handler; + +import cn.hutool.http.HttpUtil; +import cn.hutool.json.JSONObject; +import cn.hutool.json.JSONUtil; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.bsd.pjcloud.admin.api.dto.UserInfo; +import com.bsd.pjcloud.admin.api.entity.SysSocialDetails; +import com.bsd.pjcloud.admin.api.entity.SysUser; +import com.bsd.pjcloud.admin.mapper.SysSocialDetailsMapper; +import com.bsd.pjcloud.admin.service.SysUserService; +import com.bsd.pjcloud.common.core.constant.SecurityConstants; +import com.bsd.pjcloud.common.core.constant.enums.LoginTypeEnum; +import com.bsd.pjcloud.common.core.exception.CheckedException; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +@Slf4j +@Component("WX_PC") +@AllArgsConstructor +public class WeChatPcLoginHandler extends AbstractLoginHandler { + + private final SysUserService sysUserService; + + private final SysSocialDetailsMapper sysSocialDetailsMapper; + + @Override + public String identify(String code) { + SysSocialDetails condition = new SysSocialDetails(); + condition.setType(LoginTypeEnum.WECHAT_PC.getType()); + SysSocialDetails socialDetails = sysSocialDetailsMapper.selectOne(new QueryWrapper<>(condition)); + if (socialDetails == null) { + throw new CheckedException("未配置 PC 微信登录,请在后台添加 type=WX_PC 的网站应用"); + } + + String url = String.format(SecurityConstants.WX_AUTHORIZATION_CODE_URL, socialDetails.getAppId(), + socialDetails.getAppSecret(), code); + String result = HttpUtil.get(url); + log.debug("PC微信响应报文:{}", result); + + JSONObject resultJsonObj = JSONUtil.parseObj(result); + if (resultJsonObj.containsKey("errcode")) { + log.error("PC微信授权失败:{}", result); + String errMsg = resultJsonObj.getStr("errmsg", "微信授权失败"); + throw new CheckedException("微信授权失败:" + errMsg); + } + + Object obj = resultJsonObj.get("openid"); + if (obj == null) { + log.error("PC微信响应缺少openid:{}", result); + throw new CheckedException("微信授权失败,未获取到用户标识"); + } + return obj.toString(); + } + + @Override + public UserInfo info(String openId) { + SysUser user = sysUserService.getOne(Wrappers.query().lambda().eq(SysUser::getWxPcOpenid, openId)); + if (user == null) { + log.info("PC微信未绑定:{}", openId); + return null; + } + return sysUserService.findUserInfo(user); + } + + @Override + public Boolean bind(SysUser user, String identify) { + user.setWxPcOpenid(identify); + sysUserService.updateById(user); + return true; + } + +} diff --git a/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/handler/WxPcTicketLoginHandler.java b/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/handler/WxPcTicketLoginHandler.java new file mode 100644 index 000000000..992b31884 --- /dev/null +++ b/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/handler/WxPcTicketLoginHandler.java @@ -0,0 +1,62 @@ +package com.bsd.pjcloud.admin.handler; + +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.bsd.pjcloud.admin.api.dto.UserInfo; +import com.bsd.pjcloud.admin.api.entity.SysUser; +import com.bsd.pjcloud.admin.service.SysUserService; +import com.bsd.pjcloud.common.core.constant.CacheConstants; +import com.bsd.pjcloud.common.core.exception.CheckedException; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Component; + +/** + * PC 微信 ticket 登录(mobile=WX_PC_TICKET@{ticket}) + */ +@Slf4j +@Component("WX_PC_TICKET") +@RequiredArgsConstructor +public class WxPcTicketLoginHandler implements LoginHandler { + + private final StringRedisTemplate redisTemplate; + + private final SysUserService sysUserService; + + private String ticketKey(String ticket) { + return CacheConstants.WX_PC_SOCIAL_TICKET + ticket; + } + + @Override + public Boolean check(String loginStr) { + return true; + } + + @Override + public String identify(String ticket) { + String openid = redisTemplate.opsForValue().get(ticketKey(ticket)); + if (StrUtil.isBlank(openid)) { + throw new CheckedException("微信授权已过期,请重新授权"); + } + redisTemplate.delete(ticketKey(ticket)); + return openid; + } + + @Override + public UserInfo info(String openid) { + SysUser user = sysUserService.getOne(Wrappers.lambdaQuery().eq(SysUser::getWxPcOpenid, openid)); + if (user == null) { + log.info("PC微信未绑定:{}", openid); + return null; + } + return sysUserService.findUserInfo(user); + } + + @Override + public UserInfo handle(String loginStr) { + String openid = identify(loginStr); + return info(openid); + } + +} diff --git a/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/service/PcWechatSocialService.java b/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/service/PcWechatSocialService.java new file mode 100644 index 000000000..646491c6f --- /dev/null +++ b/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/service/PcWechatSocialService.java @@ -0,0 +1,25 @@ +package com.bsd.pjcloud.admin.service; + +import java.util.Map; + +/** + * PC 后台微信:密码登录后的绑定 / 扫码确认 + */ +public interface PcWechatSocialService { + + /** + * 密码已校验、未绑定 PC 微信:扫码绑定并登录 + */ + Map bindByPwdTicket(String pwdTicket, String code); + + /** + * 密码已校验、已绑定:扫码确认后登录 + */ + Map confirmLogin(String confirmTicket, String code); + + /** + * 解析 PC 微信 code,返回绑定状态与 ticket + */ + com.bsd.pjcloud.admin.api.vo.WxSocialResolveVO resolveWxPcCode(String code); + +} diff --git a/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/service/impl/PcWechatSocialServiceImpl.java b/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/service/impl/PcWechatSocialServiceImpl.java new file mode 100644 index 000000000..887f87d20 --- /dev/null +++ b/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/service/impl/PcWechatSocialServiceImpl.java @@ -0,0 +1,203 @@ +package com.bsd.pjcloud.admin.service.impl; + +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.IdUtil; +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.bsd.pjcloud.admin.api.dto.UserInfo; +import com.bsd.pjcloud.admin.api.dto.UserTokenDTO; +import com.bsd.pjcloud.admin.api.entity.SysUser; +import com.bsd.pjcloud.admin.api.feign.RemoteTokenService; +import com.bsd.pjcloud.admin.api.vo.WxSocialResolveVO; +import com.bsd.pjcloud.admin.handler.LoginHandler; +import com.bsd.pjcloud.admin.service.PcWechatSocialService; +import com.bsd.pjcloud.admin.service.SysUserService; +import com.bsd.pjcloud.common.core.constant.CacheConstants; +import com.bsd.pjcloud.common.core.constant.SecurityConstants; +import com.bsd.pjcloud.common.core.exception.CheckedException; +import com.bsd.pjcloud.common.core.util.R; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.cache.CacheManager; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.security.oauth2.core.OAuth2AccessToken; +import org.springframework.security.oauth2.core.OAuth2RefreshToken; +import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AccessTokenAuthenticationToken; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.TimeUnit; + +@Slf4j +@Service +@RequiredArgsConstructor +public class PcWechatSocialServiceImpl implements PcWechatSocialService { + + private static final long TICKET_TTL_MINUTES = 15; + + private final Map loginHandlerMap; + + private final StringRedisTemplate redisTemplate; + + private final SysUserService sysUserService; + + private final RemoteTokenService remoteTokenService; + + private final CacheManager cacheManager; + + private String wxPcTicketKey(String ticket) { + return CacheConstants.WX_PC_SOCIAL_TICKET + ticket; + } + + private String pwdVerifiedKey(String ticket) { + return CacheConstants.PC_PWD_VERIFIED + ticket; + } + + private String loginConfirmKey(String ticket) { + return CacheConstants.PC_LOGIN_CONFIRM + ticket; + } + + @Override + public WxSocialResolveVO resolveWxPcCode(String code) { + LoginHandler wxPcHandler = loginHandlerMap.get("WX_PC"); + String openid = wxPcHandler.identify(code); + UserInfo userInfo = wxPcHandler.info(openid); + String ticket = IdUtil.simpleUUID(); + redisTemplate.opsForValue().set(wxPcTicketKey(ticket), openid, TICKET_TTL_MINUTES, TimeUnit.MINUTES); + WxSocialResolveVO vo = new WxSocialResolveVO(); + vo.setBound(userInfo != null); + vo.setTicket(ticket); + return vo; + } + + @Override + public Map bindByPwdTicket(String pwdTicket, String code) { + String userIdStr = redisTemplate.opsForValue().get(pwdVerifiedKey(pwdTicket)); + if (StrUtil.isBlank(userIdStr)) { + throw new CheckedException("登录已过期,请重新输入账号密码"); + } + LoginHandler wxPcHandler = loginHandlerMap.get("WX_PC"); + String openid = wxPcHandler.identify(code); + UserInfo existing = wxPcHandler.info(openid); + if (existing != null && !existing.getSysUser().getUserId().equals(Long.valueOf(userIdStr))) { + throw new CheckedException("该微信已绑定其他账号"); + } + SysUser user = sysUserService.getById(Long.valueOf(userIdStr)); + if (user == null) { + throw new CheckedException("用户不存在"); + } + wxPcHandler.bind(user, openid); + cacheManager.getCache(CacheConstants.USER_DETAILS).evict(user.getUsername()); + redisTemplate.delete(pwdVerifiedKey(pwdTicket)); + return issueToken(user); + } + + @Override + public Map confirmLogin(String confirmTicket, String code) { + String userIdStr = redisTemplate.opsForValue().get(loginConfirmKey(confirmTicket)); + if (StrUtil.isBlank(userIdStr)) { + throw new CheckedException("登录已过期,请重新输入账号密码"); + } + SysUser user = sysUserService.getById(Long.valueOf(userIdStr)); + if (user == null) { + throw new CheckedException("用户不存在"); + } + if (StrUtil.isBlank(user.getWxPcOpenid())) { + throw new CheckedException("未绑定 PC 微信,请先绑定"); + } + LoginHandler wxPcHandler = loginHandlerMap.get("WX_PC"); + String openid = wxPcHandler.identify(code); + if (!StrUtil.equals(openid, user.getWxPcOpenid())) { + throw new CheckedException("微信账号与绑定不一致,请使用已绑定的微信扫码"); + } + redisTemplate.delete(loginConfirmKey(confirmTicket)); + return issueToken(user); + } + + private Map issueToken(SysUser user) { + UserInfo userInfo = sysUserService.findUserInfo(user); + UserTokenDTO userTokenDTO = new UserTokenDTO(); + userTokenDTO.setSysUser(userInfo.getSysUser()); + userTokenDTO.setUsername(user.getUsername()); + userTokenDTO.setAuthorities(buildTokenAuthorities(userInfo)); + R tokenResult = remoteTokenService.generateToken(userTokenDTO); + if (tokenResult == null || tokenResult.getCode() != 0 || tokenResult.getData() == null) { + log.error("PC微信签发 token 失败: {}", tokenResult); + throw new CheckedException("签发登录令牌失败"); + } + Object data = tokenResult.getData(); + if (data instanceof OAuth2AccessTokenAuthenticationToken authToken) { + OAuth2AccessToken accessToken = authToken.getAccessToken(); + OAuth2RefreshToken refreshToken = authToken.getRefreshToken(); + Map map = new HashMap<>(2); + map.put("access_token", accessToken.getTokenValue()); + if (refreshToken != null) { + map.put("refresh_token", refreshToken.getTokenValue()); + } + return map; + } + if (data instanceof Map map) { + String accessToken = extractTokenValue(map.get("accessToken")); + if (StrUtil.isBlank(accessToken)) { + accessToken = extractTokenValue(map.get("access_token")); + } + String refreshToken = extractTokenValue(map.get("refreshToken")); + if (StrUtil.isBlank(refreshToken)) { + refreshToken = extractTokenValue(map.get("refresh_token")); + } + if (StrUtil.isBlank(accessToken)) { + log.error("PC微信签发 token 解析失败, data={}", map); + throw new CheckedException("签发登录令牌失败"); + } + Map result = new HashMap<>(2); + result.put("access_token", accessToken); + if (StrUtil.isNotBlank(refreshToken)) { + result.put("refresh_token", refreshToken); + } + return result; + } + log.error("PC微信签发 token 未知数据类型: {}", data.getClass()); + throw new CheckedException("签发登录令牌失败"); + } + + /** 与 PjcloudUserDetailsService 一致:角色 ROLE_{id} + 权限标识,否则菜单接口 getRoleIds 为空 */ + private List buildTokenAuthorities(UserInfo userInfo) { + Set authSet = new HashSet<>(); + if (ArrayUtil.isNotEmpty(userInfo.getRoles())) { + Arrays.stream(userInfo.getRoles()).forEach(roleId -> authSet.add(SecurityConstants.ROLE + roleId)); + } + if (ArrayUtil.isNotEmpty(userInfo.getPermissions())) { + authSet.addAll(Arrays.asList(userInfo.getPermissions())); + } + return new ArrayList<>(authSet); + } + + /** Feign 反序列化 OAuth2AccessToken 常为 Map,需取 tokenValue */ + private String extractTokenValue(Object tokenObj) { + if (tokenObj == null) { + return null; + } + if (tokenObj instanceof OAuth2AccessToken accessToken) { + return accessToken.getTokenValue(); + } + if (tokenObj instanceof OAuth2RefreshToken refreshToken) { + return refreshToken.getTokenValue(); + } + if (tokenObj instanceof Map tokenMap) { + Object tokenValue = tokenMap.get("tokenValue"); + if (tokenValue == null) { + tokenValue = tokenMap.get("value"); + } + return tokenValue != null ? String.valueOf(tokenValue) : null; + } + String str = String.valueOf(tokenObj); + return StrUtil.isBlank(str) || str.startsWith("{") ? null : str; + } + +} diff --git a/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/service/impl/SysSocialDetailsServiceImpl.java b/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/service/impl/SysSocialDetailsServiceImpl.java index bedb9f034..a7b63f5e8 100644 --- a/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/service/impl/SysSocialDetailsServiceImpl.java +++ b/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/service/impl/SysSocialDetailsServiceImpl.java @@ -66,10 +66,17 @@ public class SysSocialDetailsServiceImpl extends ServiceImpl { SysSocialDetailsVO newSysSocialDetails = new SysSocialDetailsVO(); BeanUtils.copyProperties(sysSocialDetails, newSysSocialDetails); + if ("WX_PC".equals(sysSocialDetails.getType())) { + newSysSocialDetails.setRedirectUrl(normalizeWxPcRedirectUrl(sysSocialDetails.getRedirectUrl())); + } return newSysSocialDetails; }).collect(Collectors.toList()); } + + /** PC 微信 qrconnect:redirect_uri 不能含 # */ + private static String normalizeWxPcRedirectUrl(String redirectUrl) { + if (StrUtil.isBlank(redirectUrl)) { + return redirectUrl; + } + String url = redirectUrl.trim(); + int hashIdx = url.indexOf('#'); + if (hashIdx >= 0) { + url = url.substring(0, hashIdx); + } + while (url.endsWith("/")) { + url = url.substring(0, url.length() - 1); + } + return url; + } + } diff --git a/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/service/impl/SysUserServiceImpl.java b/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/service/impl/SysUserServiceImpl.java index 55505f7cf..b63c72b95 100644 --- a/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/service/impl/SysUserServiceImpl.java +++ b/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/service/impl/SysUserServiceImpl.java @@ -907,6 +907,8 @@ public class SysUserServiceImpl extends ServiceImpl impl LambdaUpdateWrapper wrapper = null; if (type.equals("wechat")) { wrapper = Wrappers.lambdaUpdate().set(SysUser::getWxOpenid, null).eq(SysUser::getUserId, user.getId()); + } else if (type.equals("wechat_pc")) { + wrapper = Wrappers.lambdaUpdate().set(SysUser::getWxPcOpenid, null).eq(SysUser::getUserId, user.getId()); } else if (type.equals("gitee")) { wrapper = Wrappers.lambdaUpdate().set(SysUser::getGiteeLogin, null).eq(SysUser::getUserId, user.getId()); } else if (type.equals("osc")) { diff --git a/pjcloud-upms/pjcloud-upms-biz/src/main/resources/mapper/SysUserMapper.xml b/pjcloud-upms/pjcloud-upms-biz/src/main/resources/mapper/SysUserMapper.xml index a20dcc142..625362b4c 100644 --- a/pjcloud-upms/pjcloud-upms-biz/src/main/resources/mapper/SysUserMapper.xml +++ b/pjcloud-upms/pjcloud-upms-biz/src/main/resources/mapper/SysUserMapper.xml @@ -33,6 +33,7 @@ + @@ -73,6 +74,7 @@ u.gender, u.avatar, u.wx_openid, + u.wx_pc_openid, u.qq_openid, u.gitee_login, u.osc_id, @@ -107,6 +109,7 @@ u.gender, u.avatar, u.wx_openid, + u.wx_pc_openid, u.qq_openid, u.gitee_login, u.osc_id, @@ -163,6 +166,7 @@ u.gender, u.avatar, u.wx_openid, + u.wx_pc_openid, u.qq_openid, u.gitee_login, u.osc_id, @@ -263,6 +267,7 @@ u.phone, u.avatar, u.wx_openid, + u.wx_pc_openid, u.qq_openid, u.gitee_login, u.osc_id,