Merge branch 'master' of https://gitea.zianxin.com/zax/biz-api-cloud-zax
This commit is contained in:
commit
d435283d06
35 files changed
+1121
-197
No files matched your search
+120
@@ -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<UserInfo> 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;
|
||||
}
|
||||
|
||||
}
|
||||
+36
@@ -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 后台密码登录微信校验(开发/生产可关)
|
||||
* <p>
|
||||
* 配置示例(pjcloud-auth application.yml 或 Nacos):
|
||||
* <pre>
|
||||
* security:
|
||||
* pc-wechat:
|
||||
* bind-required: false
|
||||
* confirm-required: false
|
||||
* </pre>
|
||||
* 也可在「参数管理」覆盖: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;
|
||||
|
||||
}
|
||||
+15
@@ -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<T extends OA
|
||||
Authentication usernamePasswordAuthentication = authenticationManager
|
||||
.authenticate(usernamePasswordAuthenticationToken);
|
||||
|
||||
if (AuthorizationGrantType.PASSWORD.equals(resouceOwnerBaseAuthentication.getAuthorizationGrantType())) {
|
||||
SpringUtil.getBean(PcWechatPasswordLoginGate.class).check(registeredClient, usernamePasswordAuthentication);
|
||||
}
|
||||
|
||||
Object onlineQuantity = registeredClient.getClientSettings()
|
||||
.getSettings()
|
||||
.get(CommonConstants.ONLINE_QUANTITY);
|
||||
@@ -303,6 +310,14 @@ public abstract class OAuth2ResourceOwnerBaseAuthenticationProvider<T extends OA
|
||||
"User credentials have expired"),
|
||||
""));
|
||||
}
|
||||
if (authenticationException instanceof WechatBindRequiredException bindEx) {
|
||||
return new OAuth2AuthenticationException(
|
||||
new OAuth2Error(OAuth2ErrorCodesExpand.WECHAT_BIND_REQUIRED, bindEx.getTicket(), ""));
|
||||
}
|
||||
if (authenticationException instanceof WechatConfirmRequiredException confirmEx) {
|
||||
return new OAuth2AuthenticationException(
|
||||
new OAuth2Error(OAuth2ErrorCodesExpand.WECHAT_CONFIRM_REQUIRED, confirmEx.getTicket(), ""));
|
||||
}
|
||||
if (authenticationException instanceof ScopeException) {
|
||||
return new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_SCOPE,
|
||||
this.messages.getMessage("AbstractAccessDecisionManager.accessDenied", "invalid_scope"), ""));
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
security:
|
||||
pc-wechat:
|
||||
# PC 后台密码登录:未绑定 PC 微信时是否强制先绑定
|
||||
bind-required: false
|
||||
# PC 后台密码登录:已绑定后是否须微信扫码确认
|
||||
confirm-required: false
|
||||
@@ -16,3 +16,4 @@ spring:
|
||||
import:
|
||||
- optional:nacos:application-@profiles.active@.yml
|
||||
- optional:nacos:${spring.application.name}-@profiles.active@.yml
|
||||
- optional:classpath:application-pc-wechat.yml
|
||||
+20
@@ -127,6 +127,26 @@ public interface CacheConstants {
|
||||
*/
|
||||
String PROJECT_OAUTH_ACCESS = "token::access_token";
|
||||
|
||||
/**
|
||||
* App 微信社交登录一次性 ticket(openid 缓存,避免 code 重复使用)
|
||||
*/
|
||||
String WX_SOCIAL_TICKET = GLOBALLY + "wx:social:ticket:";
|
||||
|
||||
/**
|
||||
* PC 微信社交登录一次性 ticket
|
||||
*/
|
||||
String WX_PC_SOCIAL_TICKET = GLOBALLY + "wx:pc:social:ticket:";
|
||||
|
||||
/**
|
||||
* PC 密码已校验待绑定微信(value=userId)
|
||||
*/
|
||||
String PC_PWD_VERIFIED = GLOBALLY + "pc:pwd:verified:";
|
||||
|
||||
/**
|
||||
* PC 密码已校验待微信扫码确认(value=userId)
|
||||
*/
|
||||
String PC_LOGIN_CONFIRM = GLOBALLY + "pc:login:confirm:";
|
||||
|
||||
/* ************************************* start 钱包相关 start ************************************* */
|
||||
|
||||
String WALLET_DEFAULT = "wallet_default";
|
||||
|
||||
+6
-1
@@ -46,10 +46,15 @@ public enum LoginTypeEnum {
|
||||
QQ("QQ", "QQ登录"),
|
||||
|
||||
/**
|
||||
* 微信登录
|
||||
* 微信登录(移动端 App)
|
||||
*/
|
||||
WECHAT("WX", "微信登录"),
|
||||
|
||||
/**
|
||||
* PC 后台微信扫码(网站应用 snsapi_login)
|
||||
*/
|
||||
WECHAT_PC("WX_PC", "PC微信登录"),
|
||||
|
||||
/**
|
||||
* 微信小程序
|
||||
*/
|
||||
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
package com.bsd.pjcloud.common.security.exception;
|
||||
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
|
||||
/**
|
||||
* PC 后台密码校验通过,但未绑定 PC 微信
|
||||
*/
|
||||
public class WechatBindRequiredException extends AuthenticationException {
|
||||
|
||||
private final String ticket;
|
||||
|
||||
public WechatBindRequiredException(String ticket) {
|
||||
super(ticket);
|
||||
this.ticket = ticket;
|
||||
}
|
||||
|
||||
public String getTicket() {
|
||||
return ticket;
|
||||
}
|
||||
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
package com.bsd.pjcloud.common.security.exception;
|
||||
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
|
||||
/**
|
||||
* PC 后台密码校验通过,须微信扫码确认后签发 token
|
||||
*/
|
||||
public class WechatConfirmRequiredException extends AuthenticationException {
|
||||
|
||||
private final String ticket;
|
||||
|
||||
public WechatConfirmRequiredException(String ticket) {
|
||||
super(ticket);
|
||||
this.ticket = ticket;
|
||||
}
|
||||
|
||||
public String getTicket() {
|
||||
return ticket;
|
||||
}
|
||||
|
||||
}
|
||||
+12
-1
@@ -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");
|
||||
}
|
||||
|
||||
}
|
||||
+6
@@ -24,6 +24,12 @@ public interface OAuth2ErrorCodesExpand {
|
||||
/** 证书过期 */
|
||||
String CREDENTIALS_EXPIRED = "credentials_expired";
|
||||
|
||||
/** PC 后台须绑定微信 */
|
||||
String WECHAT_BIND_REQUIRED = "wechat_bind_required";
|
||||
|
||||
/** PC 后台须微信扫码确认 */
|
||||
String WECHAT_CONFIRM_REQUIRED = "wechat_confirm_required";
|
||||
|
||||
/** scope 为空异常 */
|
||||
String SCOPE_IS_EMPTY = "scope_is_empty";
|
||||
|
||||
|
||||
+8
-3
@@ -120,7 +120,10 @@ public class SmErpCategoryController {
|
||||
@GetMapping("/export")
|
||||
@PreAuthorize("@pms.hasPermission('supermarket_supermarketCategory_export')" )
|
||||
public List<SmErpCategoryEntity> export(SmErpCategoryEntity supermarketCategory, Long[] ids) {
|
||||
return smErpCategoryService.list(Wrappers.lambdaQuery(supermarketCategory).in(ArrayUtil.isNotEmpty(ids), SmErpCategoryEntity::getId, ids));
|
||||
LambdaQueryWrapper<SmErpCategoryEntity> 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<List<SmErpCategorySimpleVo>> getAppSimpleAll(@RequestParam("merchantId")Long merchantId) {
|
||||
List<SmErpCategoryEntity> list = smErpCategoryService.
|
||||
list(new LambdaQueryWrapper<SmErpCategoryEntity>().eq(SmErpCategoryEntity::getMerchantId, merchantId));
|
||||
LambdaQueryWrapper<SmErpCategoryEntity> wrapper = new LambdaQueryWrapper<SmErpCategoryEntity>()
|
||||
.eq(SmErpCategoryEntity::getMerchantId, merchantId);
|
||||
smErpCategoryService.applyDefaultSort(wrapper);
|
||||
List<SmErpCategoryEntity> list = smErpCategoryService.list(wrapper);
|
||||
List<SmErpCategorySimpleVo> bean = BeanUtils.toBean(list, SmErpCategorySimpleVo.class);
|
||||
return R.ok(bean);
|
||||
}
|
||||
|
||||
+2
-3
@@ -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) {
|
||||
|
||||
+1
@@ -206,6 +206,7 @@ public class SmSpuController {
|
||||
|
||||
LambdaQueryWrapper<SmErpCategoryEntity> smErpCategoryEntityLambdaQueryWrapper = new LambdaQueryWrapper<>();
|
||||
smErpCategoryEntityLambdaQueryWrapper.in(SmErpCategoryEntity::getId, categoryIds);
|
||||
smErpCategoryService.applyDefaultSort(smErpCategoryEntityLambdaQueryWrapper);
|
||||
List<SmErpCategoryEntity> categoryEntities = smErpCategoryService.list(smErpCategoryEntityLambdaQueryWrapper);
|
||||
List<SmErpCategorySimpleVo> bean = BeanUtils.toBean(categoryEntities, SmErpCategorySimpleVo.class);
|
||||
return R.ok(bean);
|
||||
|
||||
+5
@@ -23,4 +23,9 @@ public interface SmErpCategoryService extends IService<SmErpCategoryEntity> {
|
||||
* @return
|
||||
*/
|
||||
Object pageScope(Page page, LambdaQueryWrapper<SmErpCategoryEntity> wrapper);
|
||||
|
||||
/**
|
||||
* 分类默认排序:sort 升序,id 降序
|
||||
*/
|
||||
void applyDefaultSort(LambdaQueryWrapper<SmErpCategoryEntity> wrapper);
|
||||
}
|
||||
+12
-2
@@ -19,13 +19,23 @@ import java.util.List;
|
||||
*/
|
||||
@Service
|
||||
public class SmErpCategoryServiceImpl extends ServiceImpl<SmErpCategoryMapper, SmErpCategoryEntity> implements SmErpCategoryService {
|
||||
|
||||
@Override
|
||||
public void applyDefaultSort(LambdaQueryWrapper<SmErpCategoryEntity> wrapper) {
|
||||
wrapper.orderByAsc(SmErpCategoryEntity::getSort)
|
||||
.orderByDesc(SmErpCategoryEntity::getId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<SmErpCategoryEntity> listScope() {
|
||||
return baseMapper.selectListByScope(new LambdaQueryWrapper<SmErpCategoryEntity>(), DataScope.of());
|
||||
LambdaQueryWrapper<SmErpCategoryEntity> wrapper = new LambdaQueryWrapper<>();
|
||||
applyDefaultSort(wrapper);
|
||||
return baseMapper.selectListByScope(wrapper, DataScope.of());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object pageScope(Page page, LambdaQueryWrapper<SmErpCategoryEntity> wrapper) {
|
||||
applyDefaultSort(wrapper);
|
||||
return baseMapper.selectPageByScope(page, wrapper, DataScope.of());
|
||||
}
|
||||
}
|
||||
}
|
||||
+179
-182
@@ -207,52 +207,15 @@ public class SmErpInventoryRecordsServiceImpl extends ServiceImpl<SmErpInventory
|
||||
double newInTotalQuantity = 0;
|
||||
|
||||
for (SmErpInventoryRecordsItemEntity item : spuItems) {
|
||||
// 新进数量 = 包装数量(箱数) * 内装数(个/箱)
|
||||
double quantity = item.getPackageNum() * item.getQuantity();
|
||||
newInTotalQuantity += quantity;
|
||||
|
||||
// 新进商品总金额 = 实际进价(分/个) * 数量(个)
|
||||
if (item.getRealPrice() != null) {
|
||||
newInTotalAmount += item.getRealPrice() * quantity;
|
||||
}
|
||||
|
||||
//更新实际进价到商品包装单位里
|
||||
SmErpUnitEntity smErpUnitEntity = new SmErpUnitEntity();
|
||||
smErpUnitEntity.setPurchasePrice(item.getRealPrice());
|
||||
smErpUnitEntity.setId(item.getUnitId());
|
||||
smErpUnitMapper.updateById(smErpUnitEntity);
|
||||
newInTotalQuantity += calcItemBaseQuantity(item);
|
||||
newInTotalAmount += calcItemBaseAmount(item);
|
||||
}
|
||||
|
||||
// 计算加权平均成本价 = (剩余商品进货价*剩余数量 + 新进商品进货价*新进数量) / (剩余数量 + 新进数量)
|
||||
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("====================================");
|
||||
} 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<SmErpInventory
|
||||
return insert;
|
||||
}
|
||||
|
||||
/** 明细行基准数量(包装数×内装数×行数量) */
|
||||
private double calcItemBaseQuantity(SmErpInventoryRecordsItemEntity item) {
|
||||
Integer pkgNum = item.getPackageNum();
|
||||
if (pkgNum == null && item.getUnitId() != null) {
|
||||
SmErpUnitEntity unit = smErpUnitMapper.selectById(item.getUnitId());
|
||||
pkgNum = unit != null ? unit.getPackageNum() : 1;
|
||||
}
|
||||
if (pkgNum == null) {
|
||||
pkgNum = 1;
|
||||
}
|
||||
return pkgNum * (item.getQuantity() != null ? item.getQuantity() : 0D);
|
||||
}
|
||||
|
||||
private double calcItemsBaseQuantity(List<SmErpInventoryRecordsItemEntity> 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<SmErpInventoryRecordsItemEntity> spuItems) {
|
||||
if (CollectionUtils.isEmpty(spuItems)) {
|
||||
return;
|
||||
}
|
||||
SmErpUnitEntity baseUnit = smErpUnitMapper.selectOne(
|
||||
new LambdaQueryWrapper<SmErpUnitEntity>()
|
||||
.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<SmErpInventoryRecordsItemEntity> items) {
|
||||
return items.stream().mapToDouble(this::calcItemBaseAmount).sum();
|
||||
}
|
||||
|
||||
/** 同步明细实际进价到包装单位主数据 */
|
||||
private void syncUnitPurchasePrice(List<SmErpInventoryRecordsItemEntity> 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<SmErpInventory
|
||||
filter(smErpInventoryRecordsItemEntity -> 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<SmErpInventoryRecordsItemEntity> dataBaseSmErpInventoryRecordsItemEntityList = smErpInventoryRecordsItemMapper
|
||||
.selectList(new LambdaQueryWrapper<SmErpInventoryRecordsItemEntity>().eq(SmErpInventoryRecordsItemEntity::getInventoryId, inventoryRecordEditDTO.getId()));
|
||||
|
||||
List<Long> submittedItemIds = items.stream()
|
||||
.map(InventoryRecordEditItemDTO::getId)
|
||||
.filter(Objects::nonNull)
|
||||
.collect(Collectors.toList());
|
||||
List<Long> 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<SmErpInventory
|
||||
});
|
||||
|
||||
|
||||
if (!toDeleteItemIds.isEmpty()) {
|
||||
smErpInventoryRecordsItemMapper.deleteByIds(toDeleteItemIds);
|
||||
}
|
||||
|
||||
// 批量更新已存在的明细(包括 packageNum 字段)
|
||||
if (!CollectionUtils.isEmpty(updateItems)) {
|
||||
updateItems.forEach(item -> smErpInventoryRecordsItemMapper.updateById(item));
|
||||
@@ -624,27 +720,6 @@ public class SmErpInventoryRecordsServiceImpl extends ServiceImpl<SmErpInventory
|
||||
smErpInventoryRecordsItemMapper.insert(insertItems);
|
||||
}
|
||||
|
||||
List<Long> existingCodeIds = smErpInventoryRecordsItemMapper.selectList(
|
||||
new LambdaQueryWrapper<SmErpInventoryRecordsItemEntity>()
|
||||
.eq(SmErpInventoryRecordsItemEntity::getInventoryId, smErpInventoryRecordsEntity.getId())
|
||||
).stream().map(SmErpInventoryRecordsItemEntity::getId).collect(Collectors.toList());
|
||||
|
||||
// 收集本次提交的ID(排除新增的)
|
||||
List<Long> submittedCodeIds = updateItems.stream()
|
||||
.map(SmErpInventoryRecordsItemEntity::getId)
|
||||
.filter(Objects::nonNull)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
// 计算需要删除的ID
|
||||
List<Long> toDeleteCodeIds = existingCodeIds.stream()
|
||||
.filter(id -> !submittedCodeIds.contains(id))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
// 批量删除失效条码
|
||||
if (!toDeleteCodeIds.isEmpty()) {
|
||||
smErpInventoryRecordsItemMapper.deleteByIds(toDeleteCodeIds);
|
||||
}
|
||||
|
||||
// 重新计算主表的totalAmount
|
||||
List<SmErpInventoryRecordsItemEntity> finalItems = smErpInventoryRecordsItemMapper.selectList(
|
||||
new LambdaQueryWrapper<SmErpInventoryRecordsItemEntity>()
|
||||
@@ -666,25 +741,20 @@ public class SmErpInventoryRecordsServiceImpl extends ServiceImpl<SmErpInventory
|
||||
//更新库存
|
||||
smErpStockService.updateStock(updateStockDTOS);
|
||||
|
||||
//计算库存商品的成本价 = (剩余商品进货价*剩余数量+新进商品进货价*新进数量)/(剩余数量+新进数量)
|
||||
//只有采购入库类型才需要更新成本价
|
||||
if (smErpInventoryRecordsEntity.getInventoryType() != null &&
|
||||
smErpInventoryRecordsEntity.getInventoryType().equals(InventoryTypeEnum.CKRK.getType())){
|
||||
|
||||
// 获取最终保存的商品列表
|
||||
// 采购入库编辑:同步进价并重算成本价(扣除本单旧进价后写入新进价)
|
||||
String inventoryType = recordInventoryType;
|
||||
if (InventoryTypeEnum.CKRK.getType().equals(inventoryType)) {
|
||||
List<SmErpInventoryRecordsItemEntity> savedItems = smErpInventoryRecordsItemMapper.selectList(
|
||||
new LambdaQueryWrapper<SmErpInventoryRecordsItemEntity>()
|
||||
.eq(SmErpInventoryRecordsItemEntity::getInventoryId, inventoryRecordEditDTO.getId())
|
||||
);
|
||||
|
||||
// 获取涉及的商品ID
|
||||
List<Long> currentSpuIds = savedItems.stream()
|
||||
.map(SmErpInventoryRecordsItemEntity::getErpSpuId)
|
||||
.distinct()
|
||||
.collect(Collectors.toList());
|
||||
|
||||
if (!CollectionUtils.isEmpty(currentSpuIds)) {
|
||||
// 获取当前库存信息和商品信息
|
||||
List<SmErpStockEntity> stockList = smErpStockService.list(
|
||||
new LambdaQueryWrapper<SmErpStockEntity>().in(SmErpStockEntity::getErpSpuId, currentSpuIds)
|
||||
);
|
||||
@@ -697,107 +767,38 @@ public class SmErpInventoryRecordsServiceImpl extends ServiceImpl<SmErpInventory
|
||||
Map<Long, SmErpSpuEntity> spuMap = spuList.stream()
|
||||
.collect(Collectors.toMap(SmErpSpuEntity::getId, spu -> spu));
|
||||
|
||||
// 按商品分组,计算每个商品的加权平均成本价
|
||||
Map<Long, List<SmErpInventoryRecordsItemEntity>> itemsBySpuId = savedItems.stream()
|
||||
.collect(Collectors.groupingBy(SmErpInventoryRecordsItemEntity::getErpSpuId));
|
||||
Map<Long, List<SmErpInventoryRecordsItemEntity>> oldItemsBySpuId = dataBaseSmErpInventoryRecordsItemEntityList.stream()
|
||||
.collect(Collectors.groupingBy(SmErpInventoryRecordsItemEntity::getErpSpuId));
|
||||
|
||||
itemsBySpuId.forEach((erpSpuId, spuItems) -> {
|
||||
// 获取当前库存数量(已经更新后的库存)
|
||||
List<SmErpInventoryRecordsItemEntity> 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<SmErpUnitEntity> 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<SmErpInventory
|
||||
List<SmErpInventoryRecordsItemEntity> dataBaseSmErpInventoryRecordsItemEntityList = smErpInventoryRecordsItemMapper
|
||||
.selectList(new LambdaQueryWrapper<SmErpInventoryRecordsItemEntity>().eq(SmErpInventoryRecordsItemEntity::getInventoryId, pdfUpdateDTO.getId()));
|
||||
|
||||
Set<Long> submittedSpuIds = items.stream()
|
||||
.map(PDItemUpdateDTO::getErpSpuId)
|
||||
.filter(Objects::nonNull)
|
||||
.collect(Collectors.toSet());
|
||||
List<Long> 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<SmErpInventory
|
||||
updateStockDTOS.add(updateStockDTO);
|
||||
});
|
||||
|
||||
if (!toDeleteItemIds.isEmpty()) {
|
||||
smErpInventoryRecordsItemMapper.deleteByIds(toDeleteItemIds);
|
||||
}
|
||||
|
||||
// 批量更新已存在的明细
|
||||
if (!CollectionUtils.isEmpty(updateItems)) {
|
||||
updateItems.forEach(item -> smErpInventoryRecordsItemMapper.updateById(item));
|
||||
@@ -1009,27 +1023,6 @@ public class SmErpInventoryRecordsServiceImpl extends ServiceImpl<SmErpInventory
|
||||
smErpInventoryRecordsItemMapper.insert(insertItems);
|
||||
}
|
||||
|
||||
List<Long> existingCodeIds = smErpInventoryRecordsItemMapper.selectList(
|
||||
new LambdaQueryWrapper<SmErpInventoryRecordsItemEntity>()
|
||||
.eq(SmErpInventoryRecordsItemEntity::getInventoryId, smErpInventoryRecordsEntity.getId())
|
||||
).stream().map(SmErpInventoryRecordsItemEntity::getId).collect(Collectors.toList());
|
||||
|
||||
// 收集本次提交的ID(排除新增的)
|
||||
List<Long> submittedCodeIds = updateItems.stream()
|
||||
.map(SmErpInventoryRecordsItemEntity::getId)
|
||||
.filter(Objects::nonNull)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
// 计算需要删除的ID
|
||||
List<Long> 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<SmErpInventory
|
||||
SmErpInventoryRecordsEntity::getInventoryType, smErpInventoryRecords.getInventoryType());
|
||||
wrapper.eq(StringUtils.isNotBlank(smErpInventoryRecords.getOrderType()),
|
||||
SmErpInventoryRecordsEntity::getOrderType, smErpInventoryRecords.getOrderType());
|
||||
wrapper.eq(smErpInventoryRecords.getSupplierId() != null,
|
||||
SmErpInventoryRecordsEntity::getSupplierId, smErpInventoryRecords.getSupplierId());
|
||||
}
|
||||
}
|
||||
wrapper.orderByDesc(SmErpInventoryRecordsEntity::getCreateTime);
|
||||
@@ -1345,6 +1340,8 @@ public class SmErpInventoryRecordsServiceImpl extends ServiceImpl<SmErpInventory
|
||||
SmErpInventoryRecordsEntity::getInventoryType, smErpInventoryRecords.getInventoryType());
|
||||
wrapper.eq(StringUtils.isNotBlank(smErpInventoryRecords.getOrderType()),
|
||||
SmErpInventoryRecordsEntity::getOrderType, smErpInventoryRecords.getOrderType());
|
||||
wrapper.eq(smErpInventoryRecords.getSupplierId() != null,
|
||||
SmErpInventoryRecordsEntity::getSupplierId, smErpInventoryRecords.getSupplierId());
|
||||
}
|
||||
}
|
||||
wrapper.orderByDesc(SmErpInventoryRecordsEntity::getCreateTime);
|
||||
|
||||
+18
@@ -677,6 +677,7 @@ public class SmErpSpuServiceImpl extends ServiceImpl<SmErpSpuMapper, SmErpSpuEnt
|
||||
|
||||
@Override
|
||||
public Object pageScope(Page page, SmErpSpuPageDto smErpSpuPageDto) {
|
||||
normalizeSpuPageQuery(smErpSpuPageDto);
|
||||
// LambdaQueryWrapper<SmErpSpuEntity> 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<SmErpSpuMapper, SmErpSpuEnt
|
||||
return pageData;
|
||||
}
|
||||
|
||||
private void normalizeSpuPageQuery(SmErpSpuPageDto dto) {
|
||||
if (dto == null) {
|
||||
return;
|
||||
}
|
||||
dto.setProductCode(trimToNull(dto.getProductCode()));
|
||||
dto.setBarCode(trimToNull(dto.getBarCode()));
|
||||
dto.setProductName(trimToNull(dto.getProductName()));
|
||||
}
|
||||
|
||||
private String trimToNull(String value) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
String trimmed = value.trim();
|
||||
return StringUtils.isBlank(trimmed) ? null : trimmed;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Page<SpuVo> selectListAllScope(@ParameterObject Page page, SupermarketSpuQueryDTO supermarketSpuQueryDTO) {
|
||||
|
||||
|
||||
+11
-4
@@ -39,10 +39,17 @@ public class SmErpSupplierServiceImpl extends ServiceImpl<SmErpSupplierMapper, S
|
||||
private final SmErpSupplierTypeMapper supplierTypeMapper;
|
||||
@Override
|
||||
public List<SmErpSupplierEntity> selectSimpleAllScope(String name,Long supplierId) {
|
||||
LambdaQueryWrapper<SmErpSupplierEntity> wrapper = new LambdaQueryWrapper<SmErpSupplierEntity>();
|
||||
wrapper.eq(SmErpSupplierEntity::getStatus, "0");
|
||||
wrapper.like(StringUtils.isNotBlank(name),SmErpSupplierEntity::getSupplierName,name);
|
||||
wrapper.eq(supplierId!=null,SmErpSupplierEntity::getSupplierId,supplierId);
|
||||
LambdaQueryWrapper<SmErpSupplierEntity> 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());
|
||||
}
|
||||
|
||||
|
||||
+6
@@ -188,6 +188,12 @@ public class SysUser extends Model<SysUser> implements Serializable {
|
||||
@Schema(description = "微信openid")
|
||||
private String wxOpenid;
|
||||
|
||||
/**
|
||||
* PC 后台微信 openid(网站应用)
|
||||
*/
|
||||
@Schema(description = "PC微信openid")
|
||||
private String wxPcOpenid;
|
||||
|
||||
/**
|
||||
* 企微微信 userid
|
||||
*/
|
||||
|
||||
+7
@@ -29,4 +29,11 @@ public class SysSocialDetailsVO {
|
||||
*/
|
||||
@Schema(description = "扩展字段")
|
||||
private String ext;
|
||||
|
||||
/**
|
||||
* 授权回调地址(PC 微信 qrconnect 使用,不含 #)
|
||||
*/
|
||||
@Schema(description = "回调地址")
|
||||
private String redirectUrl;
|
||||
|
||||
}
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
+21
@@ -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;
|
||||
|
||||
}
|
||||
+48
@@ -22,7 +22,9 @@ 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;
|
||||
import com.bsd.pjcloud.common.core.util.ValidGroup;
|
||||
import com.bsd.pjcloud.common.log.annotation.SysLog;
|
||||
@@ -54,6 +56,8 @@ public class SysSocialDetailsController {
|
||||
|
||||
private final SysSocialDetailsService sysSocialDetailsService;
|
||||
|
||||
private final PcWechatSocialService pcWechatSocialService;
|
||||
|
||||
/**
|
||||
* 社交登录账户简单分页查询
|
||||
* @param page 分页对象
|
||||
@@ -150,6 +154,50 @@ public class SysSocialDetailsController {
|
||||
return R.ok(sysSocialDetailsService.bindSocial(state, code));
|
||||
}
|
||||
|
||||
/**
|
||||
* App 微信:解析 code(一次性),返回绑定状态与 ticket
|
||||
*/
|
||||
@Inner(value = false)
|
||||
@PostMapping("/wx/resolve")
|
||||
public R<WxSocialResolveVO> resolveWxCode(String code) {
|
||||
return R.ok(sysSocialDetailsService.resolveWxCode(code));
|
||||
}
|
||||
|
||||
/**
|
||||
* App 微信:使用 ticket 绑定(需已登录)
|
||||
*/
|
||||
@PostMapping("/bind-ticket")
|
||||
public R<Boolean> bindSocialByTicket(String state, String ticket) {
|
||||
return R.ok(sysSocialDetailsService.bindSocialByTicket(state, ticket));
|
||||
}
|
||||
|
||||
/**
|
||||
* PC 微信:解析 code,返回绑定状态与 ticket
|
||||
*/
|
||||
@Inner(value = false)
|
||||
@PostMapping("/pc/wx/resolve")
|
||||
public R<WxSocialResolveVO> resolveWxPcCode(String code) {
|
||||
return R.ok(pcWechatSocialService.resolveWxPcCode(code));
|
||||
}
|
||||
|
||||
/**
|
||||
* PC 密码已校验、未绑定:扫码绑定并登录
|
||||
*/
|
||||
@Inner(value = false)
|
||||
@PostMapping("/pc/bind-by-pwd-ticket")
|
||||
public R<java.util.Map<String, Object>> bindPcByPwdTicket(String pwdTicket, String code) {
|
||||
return R.ok(pcWechatSocialService.bindByPwdTicket(pwdTicket, code));
|
||||
}
|
||||
|
||||
/**
|
||||
* PC 密码已校验、已绑定:扫码确认登录
|
||||
*/
|
||||
@Inner(value = false)
|
||||
@PostMapping("/pc/confirm-login")
|
||||
public R<java.util.Map<String, Object>> confirmPcLogin(String ticket, String code) {
|
||||
return R.ok(pcWechatSocialService.confirmLogin(ticket, code));
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出
|
||||
*/
|
||||
|
||||
+1
@@ -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();
|
||||
|
||||
+14
-1
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
+78
@@ -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.<SysUser>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;
|
||||
}
|
||||
|
||||
}
|
||||
+62
@@ -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.<SysUser>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);
|
||||
}
|
||||
|
||||
}
|
||||
+64
@@ -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.<SysUser>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);
|
||||
}
|
||||
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
package com.bsd.pjcloud.admin.service;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* PC 后台微信:密码登录后的绑定 / 扫码确认
|
||||
*/
|
||||
public interface PcWechatSocialService {
|
||||
|
||||
/**
|
||||
* 密码已校验、未绑定 PC 微信:扫码绑定并登录
|
||||
*/
|
||||
Map<String, Object> bindByPwdTicket(String pwdTicket, String code);
|
||||
|
||||
/**
|
||||
* 密码已校验、已绑定:扫码确认后登录
|
||||
*/
|
||||
Map<String, Object> confirmLogin(String confirmTicket, String code);
|
||||
|
||||
/**
|
||||
* 解析 PC 微信 code,返回绑定状态与 ticket
|
||||
*/
|
||||
com.bsd.pjcloud.admin.api.vo.WxSocialResolveVO resolveWxPcCode(String code);
|
||||
|
||||
}
|
||||
+11
@@ -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<SysSocialDetails> {
|
||||
*/
|
||||
Boolean bindSocial(String state, String code);
|
||||
|
||||
/**
|
||||
* 使用一次性 ticket 绑定(App 微信授权后)
|
||||
*/
|
||||
Boolean bindSocialByTicket(String type, String ticket);
|
||||
|
||||
/**
|
||||
* 解析微信 code(仅兑换一次),返回是否已绑定及 ticket
|
||||
*/
|
||||
WxSocialResolveVO resolveWxCode(String code);
|
||||
|
||||
/**
|
||||
* 根据入参查询用户信息
|
||||
* @param inStr
|
||||
|
||||
+203
@@ -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<String, LoginHandler> 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<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> 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<String> buildTokenAuthorities(UserInfo userInfo) {
|
||||
Set<String> 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;
|
||||
}
|
||||
|
||||
}
|
||||
+68
@@ -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,21 @@ public class SysSocialDetailsServiceImpl extends ServiceImpl<SysSocialDetailsMap
|
||||
|
||||
private final SysUserMapper sysUserMapper;
|
||||
|
||||
private final StringRedisTemplate redisTemplate;
|
||||
|
||||
private static final long WX_TICKET_TTL_MINUTES = 15;
|
||||
|
||||
private String wxTicketKey(String type, String ticket) {
|
||||
if ("WX_PC".equals(type)) {
|
||||
return CacheConstants.WX_PC_SOCIAL_TICKET + ticket;
|
||||
}
|
||||
return CacheConstants.WX_SOCIAL_TICKET + ticket;
|
||||
}
|
||||
|
||||
private String wxTicketKey(String ticket) {
|
||||
return wxTicketKey("WX", ticket);
|
||||
}
|
||||
|
||||
/**
|
||||
* 绑定社交账号
|
||||
*
|
||||
@@ -76,6 +97,33 @@ public class SysSocialDetailsServiceImpl extends ServiceImpl<SysSocialDetailsMap
|
||||
return Boolean.TRUE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean bindSocialByTicket(String type, String ticket) {
|
||||
String openid = redisTemplate.opsForValue().get(wxTicketKey(type, ticket));
|
||||
if (StrUtil.isBlank(openid)) {
|
||||
throw new CheckedException("微信授权已过期,请重新授权");
|
||||
}
|
||||
LoginHandler loginHandler = loginHandlerMap.get(type);
|
||||
SysUser sysUser = sysUserMapper.selectById(SecurityUtils.getUser().getId());
|
||||
loginHandler.bind(sysUser, openid);
|
||||
cacheManager.getCache(CacheConstants.USER_DETAILS).evict(sysUser.getUsername());
|
||||
redisTemplate.delete(wxTicketKey(type, ticket));
|
||||
return Boolean.TRUE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public WxSocialResolveVO resolveWxCode(String code) {
|
||||
LoginHandler wxHandler = loginHandlerMap.get("WX");
|
||||
String openid = wxHandler.identify(code);
|
||||
UserInfo userInfo = wxHandler.info(openid);
|
||||
String ticket = IdUtil.simpleUUID();
|
||||
redisTemplate.opsForValue().set(wxTicketKey(ticket), openid, WX_TICKET_TTL_MINUTES, TimeUnit.MINUTES);
|
||||
WxSocialResolveVO vo = new WxSocialResolveVO();
|
||||
vo.setBound(userInfo != null);
|
||||
vo.setTicket(ticket);
|
||||
return vo;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据入参查询用户信息
|
||||
*
|
||||
@@ -101,7 +149,27 @@ public class SysSocialDetailsServiceImpl extends ServiceImpl<SysSocialDetailsMap
|
||||
return sysSocialDetailsList.stream().map(sysSocialDetails -> {
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
+2
@@ -907,6 +907,8 @@ public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> impl
|
||||
LambdaUpdateWrapper<SysUser> wrapper = null;
|
||||
if (type.equals("wechat")) {
|
||||
wrapper = Wrappers.<SysUser>lambdaUpdate().set(SysUser::getWxOpenid, null).eq(SysUser::getUserId, user.getId());
|
||||
} else if (type.equals("wechat_pc")) {
|
||||
wrapper = Wrappers.<SysUser>lambdaUpdate().set(SysUser::getWxPcOpenid, null).eq(SysUser::getUserId, user.getId());
|
||||
} else if (type.equals("gitee")) {
|
||||
wrapper = Wrappers.<SysUser>lambdaUpdate().set(SysUser::getGiteeLogin, null).eq(SysUser::getUserId, user.getId());
|
||||
} else if (type.equals("osc")) {
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
<result column="gender" property="gender"/>
|
||||
<result column="avatar" property="avatar"/>
|
||||
<result column="wx_openid" property="wxOpenid"/>
|
||||
<result column="wx_pc_openid" property="wxPcOpenid"/>
|
||||
<result column="qq_openid" property="qqOpenid"/>
|
||||
<result column="gitee_login" property="giteeOpenId"/>
|
||||
<result column="osc_id" property="oscOpenId"/>
|
||||
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user