ai批量插用户订单数据

This commit is contained in:
zjf committed 2026-09-28 15:05:45 +08:00
1 parent aee1fb1647
commit 645bfa21ad
15 files changed
+559

No files matched your search

+2
View File
@@ -5,6 +5,8 @@
<runConfiguration name="PjcloudAdminApplication" type="Spring Boot" />
<runConfiguration name="CanteenApp" type="Spring Boot" />
<runConfiguration name="ThirdPartyApp" type="Spring Boot" />
<runConfiguration name="WalletApp" type="Spring Boot" />
<runConfiguration name="HardApp" type="Spring Boot" />
<method v="2" />
</configuration>
</component>
@@ -67,4 +67,20 @@ public interface RemoteHardOrderService {
R<List<com.bsd.pjcloud.hard.api.vo.order.DailyMenuNetSoldVO>> listDailyMenuNetSold(
@RequestBody com.bsd.pjcloud.hard.api.dto.order.remote.DailyMenuNetSoldQueryDTO dto);
/** AI 喂数:精简写入演示订单,返回 orderId */
@NoToken
@PostMapping("/hard-order/inner/ai-feed-create")
R<Long> aiFeedCreateOrder(@RequestBody com.bsd.pjcloud.hard.api.dto.order.remote.AiFeedOrderCreateDTO dto);
/** AI 喂数:批量写入演示订单 */
@NoToken
@PostMapping("/hard-order/inner/ai-feed-create-batch")
R<com.bsd.pjcloud.hard.api.dto.order.remote.AiFeedOrderBatchResultDTO> aiFeedCreateOrdersBatch(
@RequestBody java.util.List<com.bsd.pjcloud.hard.api.dto.order.remote.AiFeedOrderCreateDTO> dtos);
/** AI 喂数清理:软删 AIFEED- 订单 */
@NoToken
@PostMapping("/hard-order/inner/ai-feed-delete")
R<Integer> aiFeedDeleteOrders(@RequestBody List<Long> orderIds);
}
@@ -37,6 +37,7 @@ import com.bsd.pjcloud.hard.api.dto.order.app.HardAppCreateOrderDto;
import com.bsd.pjcloud.hard.api.dto.order.app.HardAppCreateReserveOrdersDto;
import com.bsd.pjcloud.hard.api.dto.order.app.HardAppCreateShopOrderDto;
import com.bsd.pjcloud.hard.api.dto.order.app.HardAppCreateTakeOutOrderDto;
import com.bsd.pjcloud.hard.api.dto.order.remote.AiFeedOrderCreateDTO;
import com.bsd.pjcloud.hard.api.dto.order.remote.HardUpdateShopStatusDto;
import com.bsd.pjcloud.hard.api.dto.order.remote.HardUpdateTakeOutStatusDto;
import com.bsd.pjcloud.hard.api.dto.order.remote.DailyMenuNetSoldQueryDTO;
@@ -99,6 +100,7 @@ public class HardOrderController {
private final RemoteMchService mchService;
private final RemoteUserService userService;
private final AiFeedOrderService aiFeedOrderService;
/* ************************************* start 远程调用 start ************************************* */
@Inner
@@ -153,6 +155,33 @@ public class HardOrderController {
public R<List<DailyMenuNetSoldVO>> listDailyMenuNetSold(@RequestBody DailyMenuNetSoldQueryDTO dto) {
return R.ok(hardwareOrderService.listDailyMenuNetSold(dto));
}
@Inner
@Operation(summary = "AI喂数创建演示订单", description = "精简写入 hard_order/hard_car,不做完整库存链路")
@PostMapping("/inner/ai-feed-create")
public R<Long> aiFeedCreateOrder(@RequestBody AiFeedOrderCreateDTO dto) {
try {
return R.ok(aiFeedOrderService.createDemoOrder(dto));
}
catch (IllegalArgumentException ex) {
return R.failed(ex.getMessage());
}
}
@Inner
@Operation(summary = "AI喂数批量创建演示订单")
@PostMapping("/inner/ai-feed-create-batch")
public R<com.bsd.pjcloud.hard.api.dto.order.remote.AiFeedOrderBatchResultDTO> aiFeedCreateOrdersBatch(
@RequestBody List<AiFeedOrderCreateDTO> dtos) {
return R.ok(aiFeedOrderService.createDemoOrdersBatch(dtos));
}
@Inner
@Operation(summary = "AI喂数删除演示订单", description = "仅软删 orderNo 以 AIFEED- 开头的订单")
@PostMapping("/inner/ai-feed-delete")
public R<Integer> aiFeedDeleteOrders(@RequestBody List<Long> orderIds) {
return R.ok(aiFeedOrderService.softDeleteByIds(orderIds));
}
/* ************************************* end 远程调用 ************************************* */
/* ************************************* start app start ************************************* */
@@ -309,4 +309,23 @@ public interface RemoteUserService {
@GetMapping("/user/getUserListByRoleCode")
R<List<UserSimpleVo>> getUserListByRoleCode(@RequestParam("code") String code);
/**
* AI 喂数:创建普通用户(手机号=账号,姓名=昵称,角色 GENERAL_USER + WALLET_AUTO_CREATE)。
* 手机号已存在则返回已有 userId。
*/
@NoToken
@PostMapping("/user/inner/ai-feed-create")
R<Long> aiFeedCreateUser(@RequestBody com.bsd.pjcloud.admin.api.dto.AiFeedUserCreateDTO dto);
/** AI 喂数:批量建用户 */
@NoToken
@PostMapping("/user/inner/ai-feed-create-batch")
R<com.bsd.pjcloud.admin.api.dto.AiFeedUserBatchResultDTO> aiFeedCreateUsersBatch(
@RequestBody java.util.List<com.bsd.pjcloud.admin.api.dto.AiFeedUserCreateDTO> users);
/** AI 喂数清理:软删演示用户 */
@NoToken
@PostMapping("/user/inner/ai-feed-delete")
R<Boolean> aiFeedDeleteUsers(@RequestBody List<Long> userIds);
}
@@ -761,6 +761,44 @@ public class SysUserController {
return R.ok(userService.list(Wrappers.<SysUser>lambdaQuery().in(SysUser::getUserId, userIds)));
}
/**
* AI 喂数:按手机号创建普通用户;已存在则直接返回 userId。
* 角色固定 GENERAL_USER + WALLET_AUTO_CREATE;不异步开钱包。
*/
@Inner
@PostMapping("/inner/ai-feed-create")
public R<Long> aiFeedCreateUser(@RequestBody com.bsd.pjcloud.admin.api.dto.AiFeedUserCreateDTO dto) {
if (dto == null || StrUtil.isBlank(dto.getPhone()) || StrUtil.isBlank(dto.getName()) || dto.getDeptId() == null) {
return R.failed("phone/name/deptId 不能为空");
}
try {
return R.ok(userService.aiFeedCreateOrGetUser(dto.getPhone(), dto.getName(), dto.getDeptId()));
}
catch (Exception ex) {
return R.failed(StrUtil.blankToDefault(ex.getMessage(), "创建用户失败"));
}
}
/** AI 喂数:批量建用户 */
@Inner
@PostMapping("/inner/ai-feed-create-batch")
public R<com.bsd.pjcloud.admin.api.dto.AiFeedUserBatchResultDTO> aiFeedCreateUsersBatch(
@RequestBody List<com.bsd.pjcloud.admin.api.dto.AiFeedUserCreateDTO> users) {
try {
return R.ok(userService.aiFeedCreateUsersBatch(users));
}
catch (Exception ex) {
return R.failed(StrUtil.blankToDefault(ex.getMessage(), "批量创建用户失败"));
}
}
/** AI 喂数清理:软删演示用户(含角色/身份/钱包,跳过余额校验) */
@Inner
@PostMapping("/inner/ai-feed-delete")
public R<Boolean> aiFeedDeleteUsers(@RequestBody List<Long> userIds) {
return userService.aiFeedDeleteUsers(userIds);
}
/**
* 按用户 ID 批量查询用户、部门及身份协议编码。
*
@@ -92,4 +92,10 @@ public interface SysUserMapper extends PjcloudBaseMapper<SysUser> {
* 用户在/非编人员统计
*/
List<UserStaffingTotalVo> staffingListByDept(@Param("dto") UserDTO dto, DataScope dataScope);
/**
* AI 喂数:多行 INSERT 用户(自行带 tenant_id,忽略租户插件改写)
*/
@com.baomidou.mybatisplus.annotation.InterceptorIgnore(tenantLine = "true")
int insertAiFeedBatch(@Param("list") List<SysUser> list);
}
@@ -21,7 +21,11 @@ package com.bsd.pjcloud.admin.mapper;
import com.bsd.pjcloud.admin.api.entity.SysUserRole;
import com.bsd.pjcloud.common.data.datascope.PjcloudBaseMapper;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* <p>
@@ -34,4 +38,15 @@ import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface SysUserRoleMapper extends PjcloudBaseMapper<SysUserRole> {
/** AI 喂数:多行插入用户角色 */
@Insert("""
<script>
INSERT INTO sys_user_role (user_id, role_id) VALUES
<foreach collection="list" item="r" separator=",">
(#{r.userId}, #{r.roleId})
</foreach>
</script>
""")
int insertAiFeedBatch(@Param("list") List<SysUserRole> list);
}
@@ -159,6 +159,26 @@ public interface SysUserService extends IService<SysUser> {
*/
R<Boolean> lockUser(String username);
/**
* AI 喂数清理:软删用户及关联角色/身份/关系/钱包(不校验余额与流水,演示数据专用)。
*
* @param userIds 用户 ID 列表
* @return 是否成功
*/
R<Boolean> aiFeedDeleteUsers(List<Long> userIds);
/**
* AI 喂数专用:按手机号创建用户(独立实现,不走 saveUser/doSaveUser)。
* 固定 GENERAL_USER + WALLET_AUTO_CREATE;已存在则返回原 userId;不发异步开钱包事件。
*/
Long aiFeedCreateOrGetUser(String phone, String name, Long deptId);
/**
* AI 喂数批量建用户(独立实现)。同批复用同一密码哈希,已存在手机号直接复用。
*/
com.bsd.pjcloud.admin.api.dto.AiFeedUserBatchResultDTO aiFeedCreateUsersBatch(
java.util.List<com.bsd.pjcloud.admin.api.dto.AiFeedUserCreateDTO> users);
/**
* 修改密码
*
@@ -38,6 +38,8 @@ import com.bsd.pjcloud.admin.api.constant.UserExpireEnum;
import com.bsd.pjcloud.admin.api.constant.UserStaffingTypeEnum;
import com.bsd.pjcloud.admin.api.constant.UserStateEnum;
import com.bsd.pjcloud.admin.api.constant.user.UserIdentityTypeEnum;
import com.bsd.pjcloud.admin.api.dto.AiFeedUserBatchResultDTO;
import com.bsd.pjcloud.admin.api.dto.AiFeedUserCreateDTO;
import com.bsd.pjcloud.admin.api.dto.ForgotPasswordDTO;
import com.bsd.pjcloud.admin.api.dto.SysUserFaceDto;
import com.bsd.pjcloud.admin.api.dto.UserDTO;
@@ -215,6 +217,129 @@ public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> impl
}
}
/** 演示用户统一初始密码(仅喂数路径),整批只 BCrypt 一次 */
private static final String AI_FEED_DEFAULT_PASSWORD = "AiFeed@123456";
/**
* AI 喂数专用建用户(独立实现,不改动 {@link #doSaveUser} 业务路径)。
* 固定 GENERAL_USER + WALLET_AUTO_CREATE;不发异步开钱包事件(钱包由喂数侧同步开通)。
*/
@Override
@Transactional(rollbackFor = Exception.class)
public Long aiFeedCreateOrGetUser(String phone, String name, Long deptId) {
AiFeedUserCreateDTO dto = new AiFeedUserCreateDTO();
dto.setPhone(phone);
dto.setName(name);
dto.setDeptId(deptId);
AiFeedUserBatchResultDTO batch = aiFeedCreateUsersBatch(List.of(dto));
if (batch == null || CollUtil.isEmpty(batch.getItems()) || batch.getItems().get(0).getUserId() == null) {
throw new IllegalArgumentException("创建用户失败");
}
return batch.getItems().get(0).getUserId();
}
/**
* AI 喂数批量建用户:一次查已存在手机号,整批共用密码哈希,用户/角色一次性 saveBatch。
* name 为空时仅按手机号查已有用户(订单行引用已导入用户),不新建。
*/
@Override
@Transactional(rollbackFor = Exception.class)
public AiFeedUserBatchResultDTO aiFeedCreateUsersBatch(List<AiFeedUserCreateDTO> users) {
AiFeedUserBatchResultDTO result = new AiFeedUserBatchResultDTO();
if (CollUtil.isEmpty(users)) {
return result;
}
// 同批内手机号去重,后者覆盖前者姓名/部门
Map<String, AiFeedUserCreateDTO> unique = new LinkedHashMap<>();
for (AiFeedUserCreateDTO u : users) {
if (u == null || StrUtil.isBlank(u.getPhone())) {
continue;
}
AiFeedUserCreateDTO copy = new AiFeedUserCreateDTO();
copy.setPhone(StrUtil.trim(u.getPhone()));
copy.setName(StrUtil.isBlank(u.getName()) ? null : StrUtil.trim(u.getName()));
copy.setDeptId(u.getDeptId());
unique.put(copy.getPhone(), copy);
}
if (unique.isEmpty()) {
return result;
}
List<String> phones = new ArrayList<>(unique.keySet());
Map<String, SysUser> existMap = baseMapper.selectList(Wrappers.<SysUser>lambdaQuery().in(SysUser::getPhone, phones))
.stream()
.collect(Collectors.toMap(SysUser::getPhone, u -> u, (a, b) -> a));
List<SysUser> toCreate = new ArrayList<>();
List<AiFeedUserBatchResultDTO.Item> pendingCreated = new ArrayList<>();
LocalDateTime now = LocalDateTime.now();
for (AiFeedUserCreateDTO u : unique.values()) {
AiFeedUserBatchResultDTO.Item item = new AiFeedUserBatchResultDTO.Item();
item.setPhone(u.getPhone());
SysUser exist = existMap.get(u.getPhone());
if (exist != null) {
item.setUserId(exist.getUserId());
item.setCreated(Boolean.FALSE);
result.getItems().add(item);
continue;
}
if (StrUtil.isBlank(u.getName()) || u.getDeptId() == null) {
continue;
}
SysUser sysUser = new SysUser();
// 预分配雪花 ID,便于随后批量写角色关系
sysUser.setUserId(com.baomidou.mybatisplus.core.toolkit.IdWorker.getId());
sysUser.setUsername(u.getPhone());
sysUser.setPhone(u.getPhone());
sysUser.setName(u.getName());
sysUser.setNickname(u.getName());
sysUser.setDeptId(u.getDeptId());
sysUser.setLockFlag(UserStateEnum.NORMAL.getCode());
sysUser.setProcessStatus(TaskCheckStatusEnum.RESULT.getCode());
sysUser.setProcessTime(now);
sysUser.setCreateBy(u.getPhone());
sysUser.setUpdateBy(u.getPhone());
sysUser.setPasswordModifyTime(now);
sysUser.setPasswordExpireFlag(UserExpireEnum.EXPIRE.getCode());
toCreate.add(sysUser);
item.setUserId(sysUser.getUserId());
item.setCreated(Boolean.TRUE);
pendingCreated.add(item);
}
if (!toCreate.isEmpty()) {
List<Long> roleIds = getNonAdminNewUserRoleIds();
String encodedPwd = ENCODER.encode(AI_FEED_DEFAULT_PASSWORD);
Long tenantId = TenantContextHolder.getTenantId();
LocalDateTime createTime = LocalDateTime.now();
for (SysUser sysUser : toCreate) {
sysUser.setPassword(encodedPwd);
sysUser.setDelFlag(CommonConstants.STATUS_NORMAL);
sysUser.setTenantId(tenantId);
sysUser.setCreateTime(createTime);
sysUser.setUpdateTime(createTime);
}
// 多行 INSERT,避免无 rewriteBatchedStatements 时 saveBatch 仍接近逐条写库
for (List<SysUser> chunk : CollUtil.split(toCreate, 200)) {
baseMapper.insertAiFeedBatch(chunk);
}
List<SysUserRole> roles = new ArrayList<>(toCreate.size() * Math.max(roleIds.size(), 1));
for (SysUser sysUser : toCreate) {
for (Long roleId : roleIds) {
SysUserRole userRole = new SysUserRole();
userRole.setUserId(sysUser.getUserId());
userRole.setRoleId(roleId);
roles.add(userRole);
}
}
if (!roles.isEmpty()) {
for (List<SysUserRole> chunk : CollUtil.split(roles, 500)) {
sysUserRoleMapper.insertAiFeedBatch(chunk);
}
}
result.getItems().addAll(pendingCreated);
}
return result;
}
/**
* 解析当前操作人的角色上下文,供一次或一批用户创建复用。
*
@@ -970,6 +1095,51 @@ public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> impl
return R.ok();
}
/**
* AI 喂数清理:软删演示用户。跳过钱包余额/流水校验,避免充值后无法清理。
*/
@Override
@Transactional(rollbackFor = Exception.class)
public R<Boolean> aiFeedDeleteUsers(List<Long> userIds) {
if (CollUtil.isEmpty(userIds)) {
return R.ok(true);
}
List<Long> distinctIds = userIds.stream().filter(Objects::nonNull).distinct().toList();
if (CollUtil.isEmpty(distinctIds)) {
return R.ok(true);
}
List<SysUser> userList = baseMapper.selectBatchIds(distinctIds);
if (CollUtil.isEmpty(userList)) {
return R.ok(true);
}
Cache cache = cacheManager.getCache(CacheConstants.USER_DETAILS);
for (SysUser sysUser : userList) {
if (cache != null && StrUtil.isNotBlank(sysUser.getUsername())) {
cache.evict(sysUser.getUsername());
}
}
List<Long> existIds = userList.stream().map(SysUser::getUserId).toList();
sysUserRoleMapper.delete(Wrappers.<SysUserRole>lambdaQuery().in(SysUserRole::getUserId, existIds));
sysUserRelationService.deleteUserRelationByUserIds(existIds);
sysUserIdentityService.deleteUserIdentityByUserIds(existIds);
this.removeBatchByIds(existIds);
try {
remoteWalletService.delWallet(existIds);
}
catch (Exception ex) {
log.warn("AI喂数清理钱包失败 userIds={}", existIds, ex);
}
for (Long userId : existIds) {
try {
remoteWalletService.userChange(userId, Boolean.TRUE);
}
catch (Exception ex) {
log.warn("AI喂数清理钱包同步失败 userId={}", userId, ex);
}
}
return R.ok(true);
}
@Override
@CacheEvict(value = CacheConstants.USER_DETAILS, key = "#userDto.username")
public R changePassword(UserDTO userDto) {
@@ -562,4 +562,22 @@
order by a.dept_id
</select>
<!-- AI 喂数多行插入:一次 SQL 写入多用户 -->
<insert id="insertAiFeedBatch">
INSERT INTO sys_user (
user_id, username, password, phone, name, nickname, dept_id,
lock_flag, password_expire_flag, password_modify_time,
process_status, process_time, create_by, update_by, create_time, update_time,
del_flag, tenant_id
) VALUES
<foreach collection="list" item="u" separator=",">
(
#{u.userId}, #{u.username}, #{u.password}, #{u.phone}, #{u.name}, #{u.nickname}, #{u.deptId},
#{u.lockFlag}, #{u.passwordExpireFlag}, #{u.passwordModifyTime},
#{u.processStatus}, #{u.processTime}, #{u.createBy}, #{u.updateBy}, #{u.createTime}, #{u.updateTime},
#{u.delFlag}, #{u.tenantId}
)
</foreach>
</insert>
</mapper>
@@ -25,6 +25,17 @@ public interface RemoteWalletService {
@GetMapping("/wallet-manage/get-or-create-wallet")
R<WalletManageEntity> getOrCreateWallet(@RequestParam Long userId, @RequestParam(required = false) WalletTypeEnum walletTypeCode);
/** AI 喂数:确保饭卡钱包(免角色校验、免绑卡) */
@NoToken
@GetMapping("/wallet-manage/inner/ai-feed-ensure-eat")
R<WalletManageEntity> aiFeedEnsureEatWallet(@RequestParam Long userId);
/** AI 喂数:批量开饭卡并设定余额 */
@NoToken
@PostMapping("/wallet-manage/inner/ai-feed-ensure-eat-batch")
R<com.bsd.pjcloud.wallet.api.dto.AiFeedWalletEnsureResultDTO> aiFeedEnsureEatWalletsBatch(
@RequestBody java.util.List<com.bsd.pjcloud.wallet.api.dto.AiFeedWalletEnsureDTO> reqs);
@NoToken
@GetMapping("/wallet-manage/get-wallet")
R<WalletManageEntity> getWallet(@RequestParam Long userId, @RequestParam WalletTypeEnum walletTypeCode);
@@ -80,6 +80,25 @@ public class WalletManageController {
return R.ok(wallet);
}
/** AI 喂数:确保饭卡钱包(免角色校验、免绑卡) */
@Inner
@GetMapping("/inner/ai-feed-ensure-eat")
public R<WalletManageEntity> aiFeedEnsureEatWallet(@RequestParam Long userId) {
WalletManageEntity wallet = walletManageService.aiFeedEnsureEatWallet(userId);
if (ObjectUtil.isEmpty(wallet)) {
return R.failed("开通饭卡钱包失败");
}
return R.ok(wallet);
}
/** AI 喂数:批量开饭卡并设定余额 */
@Inner
@PostMapping("/inner/ai-feed-ensure-eat-batch")
public R<com.bsd.pjcloud.wallet.api.dto.AiFeedWalletEnsureResultDTO> aiFeedEnsureEatWalletsBatch(
@RequestBody List<com.bsd.pjcloud.wallet.api.dto.AiFeedWalletEnsureDTO> reqs) {
return R.ok(walletManageService.aiFeedEnsureEatWalletsBatch(reqs));
}
@Inner
@GetMapping("/get-wallet-by-wallet-id")
public R getWalletByWalletTypeId(@RequestParam Long userId, @RequestParam Long walletTypeId) {
@@ -25,6 +25,27 @@ import java.util.List;
@Mapper
public interface WalletManageMapper extends PjcloudBaseMapper<WalletManageEntity> {
/**
* AI 喂数:多行插入饭卡钱包(自行带 tenant_id)
*/
@com.baomidou.mybatisplus.annotation.InterceptorIgnore(tenantLine = "true")
@org.apache.ibatis.annotations.Insert("""
<script>
INSERT INTO wallet_manage (
id, user_id, wallet_type_id, dept_id, balance, total_recharge, total_expense, freeze_price,
create_by, update_by, create_time, update_time, del_flag, tenant_id
) VALUES
<foreach collection="list" item="w" separator=",">
(
#{w.id}, #{w.userId}, #{w.walletTypeId}, #{w.deptId}, #{w.balance}, #{w.totalRecharge},
#{w.totalExpense}, #{w.freezePrice}, #{w.createBy}, #{w.updateBy}, #{w.createTime}, #{w.updateTime},
#{w.delFlag}, #{w.tenantId}
)
</foreach>
</script>
""")
int insertAiFeedBatch(@Param("list") List<WalletManageEntity> list);
/**
* 构建钱包账户通用筛选条件,并以钱包主键作为最终排序条件保证分片导出顺序稳定。
*
@@ -55,6 +55,20 @@ public interface WalletManageService extends IService<WalletManageEntity> {
*/
WalletManageEntity getOrCreateWallet(@NotNull Long userId, @NotNull WalletTypeEnum walletTypeCode);
/**
* AI 喂数:确保饭卡钱包存在。跳过 WALLET_AUTO_CREATE 角色校验,且不自动绑卡(避免抢卡锁拖慢导入)。
*
* @param userId 用户 ID
* @return 饭卡钱包;用户不存在或类型未配置时返回 null
*/
WalletManageEntity aiFeedEnsureEatWallet(@NotNull Long userId);
/**
* AI 喂数批量:确保饭卡并直接设定余额(不走充值流水,演示加速)。
*/
com.bsd.pjcloud.wallet.api.dto.AiFeedWalletEnsureResultDTO aiFeedEnsureEatWalletsBatch(
java.util.List<com.bsd.pjcloud.wallet.api.dto.AiFeedWalletEnsureDTO> reqs);
/**
* 获取钱包(没有缓存)
*/
@@ -60,7 +60,9 @@ import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
import java.time.LocalDateTime;
import java.util.*;
import java.util.stream.Collectors;
/**
* 钱包管理
@@ -218,6 +220,165 @@ public class WalletManageServiceImpl extends ServiceImpl<WalletManageMapper, Wal
return baseMapper.selectOne(walletQuery);
}
/**
* AI 喂数专用:只确保饭卡钱包行存在,不做角色校验、不抢卡绑卡。
* <p>历史演示导入可能留下同用户多条饭卡钱包;此处用 list+取最早一条,
* 避免调用业务 {@link #getWallet} 的 selectOne 在重复数据上直接报错。</p>
*/
@Override
public WalletManageEntity aiFeedEnsureEatWallet(Long userId) {
if (userId == null) {
return null;
}
WalletTypeEntity eatType = walletTypeMapper.selectOne(new LambdaQueryWrapper<WalletTypeEntity>()
.eq(WalletTypeEntity::getUniqueCode, WalletTypeEnum.EAT.getCode())
.last("LIMIT 1"));
if (ObjectUtil.isEmpty(eatType)) {
log.warn("|{}|AI喂数开通饭卡失败,userId:{},EAT钱包类型未配置", TAG, userId);
return null;
}
WalletManageEntity existing = findFirstEatWallet(userId, eatType.getId());
if (ObjectUtil.isNotEmpty(existing)) {
return existing;
}
UserInfo user = userService.getInnerByUserId(userId).getData();
if (ObjectUtil.isEmpty(user) || ObjectUtil.isEmpty(user.getSysUser())) {
log.warn("|{}|AI喂数开通饭卡失败,userId:{},用户不存在", TAG, userId);
return null;
}
Long deptId = user.getSysUser().getDeptId();
WalletManageEntity insertObj = new WalletManageEntity();
insertObj.setUserId(userId);
insertObj.setWalletTypeId(eatType.getId());
insertObj.setDeptId(deptId);
try {
baseMapper.insert(insertObj);
return insertObj;
}
catch (Exception ex) {
WalletManageEntity again = findFirstEatWallet(userId, eatType.getId());
if (ObjectUtil.isNotEmpty(again)) {
return again;
}
log.warn("|{}|AI喂数开通饭卡失败,userId:{},msg:{}", TAG, userId, ex.getMessage());
return null;
}
}
/** AI 喂数内部:同用户多条饭卡时取最早一条,不抛 TooManyResultsException */
private WalletManageEntity findFirstEatWallet(Long userId, Long eatTypeId) {
List<WalletManageEntity> list = baseMapper.selectList(new LambdaQueryWrapper<WalletManageEntity>()
.eq(WalletManageEntity::getUserId, userId)
.eq(WalletManageEntity::getWalletTypeId, eatTypeId)
.orderByAsc(WalletManageEntity::getId)
.last("LIMIT 1"));
return ObjectUtil.isEmpty(list) ? null : list.get(0);
}
/**
* AI 喂数批量开饭卡:一次查类型、一次查出已有钱包,缺失则插入,并按需直接写余额。
*/
@Override
public com.bsd.pjcloud.wallet.api.dto.AiFeedWalletEnsureResultDTO aiFeedEnsureEatWalletsBatch(
List<com.bsd.pjcloud.wallet.api.dto.AiFeedWalletEnsureDTO> reqs) {
com.bsd.pjcloud.wallet.api.dto.AiFeedWalletEnsureResultDTO result =
new com.bsd.pjcloud.wallet.api.dto.AiFeedWalletEnsureResultDTO();
if (ObjectUtil.isEmpty(reqs)) {
return result;
}
WalletTypeEntity eatType = walletTypeMapper.selectOne(new LambdaQueryWrapper<WalletTypeEntity>()
.eq(WalletTypeEntity::getUniqueCode, WalletTypeEnum.EAT.getCode())
.last("LIMIT 1"));
if (ObjectUtil.isEmpty(eatType)) {
log.warn("|{}|AI喂数批量开饭卡失败,EAT钱包类型未配置", TAG);
return result;
}
List<Long> userIds = reqs.stream()
.filter(Objects::nonNull)
.map(com.bsd.pjcloud.wallet.api.dto.AiFeedWalletEnsureDTO::getUserId)
.filter(Objects::nonNull)
.distinct()
.toList();
if (userIds.isEmpty()) {
return result;
}
Map<Long, WalletManageEntity> walletByUser = baseMapper.selectList(new LambdaQueryWrapper<WalletManageEntity>()
.in(WalletManageEntity::getUserId, userIds)
.eq(WalletManageEntity::getWalletTypeId, eatType.getId())
.orderByAsc(WalletManageEntity::getId))
.stream()
.collect(Collectors.toMap(WalletManageEntity::getUserId, w -> w, (a, b) -> a));
Map<Long, Integer> balanceByUser = new HashMap<>();
Map<Long, Long> deptByUser = new HashMap<>();
for (com.bsd.pjcloud.wallet.api.dto.AiFeedWalletEnsureDTO req : reqs) {
if (req == null || req.getUserId() == null) {
continue;
}
if (req.getBalanceFen() != null) {
balanceByUser.put(req.getUserId(), Math.max(req.getBalanceFen(), 0));
}
if (req.getDeptId() != null) {
deptByUser.put(req.getUserId(), req.getDeptId());
}
}
List<WalletManageEntity> toInsert = new ArrayList<>();
List<WalletManageEntity> toUpdate = new ArrayList<>();
Long tenantId = TenantContextHolder.getTenantId();
LocalDateTime now = LocalDateTime.now();
for (Long userId : userIds) {
WalletManageEntity wallet = walletByUser.get(userId);
if (wallet == null) {
wallet = new WalletManageEntity();
wallet.setId(com.baomidou.mybatisplus.core.toolkit.IdWorker.getId());
wallet.setUserId(userId);
wallet.setWalletTypeId(eatType.getId());
wallet.setDeptId(deptByUser.get(userId));
Integer bal = balanceByUser.get(userId);
wallet.setBalance(bal == null ? 0 : bal);
wallet.setTotalRecharge(bal != null && bal > 0 ? bal : 0);
wallet.setTotalExpense(0);
wallet.setFreezePrice(0);
wallet.setDelFlag(com.bsd.pjcloud.common.core.constant.CommonConstants.STATUS_NORMAL);
wallet.setTenantId(tenantId);
wallet.setCreateBy("ai-feed");
wallet.setUpdateBy("ai-feed");
wallet.setCreateTime(now);
wallet.setUpdateTime(now);
toInsert.add(wallet);
walletByUser.put(userId, wallet);
}
else if (balanceByUser.containsKey(userId)) {
Integer bal = balanceByUser.get(userId);
wallet.setBalance(bal == null ? 0 : bal);
toUpdate.add(wallet);
}
}
if (!toInsert.isEmpty()) {
// 多行 INSERT,一次 SQL 写入,避免 saveBatch 在无 rewriteBatchedStatements 时仍接近逐条
for (List<WalletManageEntity> chunk : cn.hutool.core.collection.CollUtil.split(toInsert, 200)) {
baseMapper.insertAiFeedBatch(chunk);
}
}
// 已有饭卡不再逐条改余额(喂数侧只给新建卡写 balance)
if (!toUpdate.isEmpty()) {
this.updateBatchById(toUpdate, 100);
}
for (Long userId : userIds) {
WalletManageEntity wallet = walletByUser.get(userId);
if (wallet == null || wallet.getId() == null) {
continue;
}
com.bsd.pjcloud.wallet.api.dto.AiFeedWalletEnsureResultDTO.Item item =
new com.bsd.pjcloud.wallet.api.dto.AiFeedWalletEnsureResultDTO.Item();
item.setUserId(userId);
item.setWalletId(wallet.getId());
result.getItems().add(item);
}
return result;
}
@Override
public WalletManageEntity getWallet(Long userId, WalletTypeEnum walletTypeCode) {
WalletTypeEntity walletType = walletTypeService.getByUniqueCode(walletTypeCode);