This commit is contained in:
dzc committed 2025-09-20 10:24:38 +08:00
commit ffe3c0f04c
85 files changed
+5959 -122

No files matched your search

+21
View File
@@ -0,0 +1,21 @@
-- 商户用户绑定表
CREATE TABLE `mall_merchant_user` (
`id` bigint(20) NOT NULL COMMENT '主键ID',
`merchant_id` bigint(20) NOT NULL COMMENT '商户ID',
`user_id` bigint(20) NOT NULL COMMENT '用户ID',
`user_name` varchar(50) DEFAULT NULL COMMENT '用户名称',
`user_phone` varchar(20) DEFAULT NULL COMMENT '用户手机号',
`role_type` tinyint(2) NOT NULL DEFAULT '1' COMMENT '角色类型(1:管理员 2:普通用户)',
`status` tinyint(2) NOT NULL DEFAULT '1' COMMENT '状态(1:启用 0:禁用)',
`create_by` varchar(50) DEFAULT NULL COMMENT '创建者',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_by` varchar(50) DEFAULT NULL COMMENT '更新者',
`update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`del_flag` char(1) DEFAULT '0' COMMENT '是否删除(0=正常 1=删除)',
`tenant_id` bigint(20) DEFAULT NULL COMMENT '租户编号',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_merchant_user` (`merchant_id`, `user_id`, `del_flag`),
KEY `idx_user_id` (`user_id`),
KEY `idx_merchant_id` (`merchant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商户用户绑定表';
+127
View File
@@ -0,0 +1,127 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Excel货号提取工具
从Excel文件中提取货号并生成删除SQL
"""
import pandas as pd
import sys
from pathlib import Path
def extract_product_codes_and_generate_sql(excel_file_path):
"""
从Excel文件提取货号并生成删除SQL
"""
try:
# 读取Excel文件
print(f"正在读取Excel文件: {excel_file_path}")
# 尝试读取第一个工作表
df = pd.read_excel(excel_file_path, sheet_name=0)
print(f"Excel文件包含 {len(df)} 行数据")
print("列名:", list(df.columns))
# 寻找货号列
product_code_column = None
possible_columns = ['货号', 'productCode', 'product_code', '商品编码', '编码']
for col in df.columns:
if any(keyword in str(col) for keyword in possible_columns):
product_code_column = col
break
if product_code_column is None:
print("未找到货号列,请检查Excel文件格式")
print("可用的列:", list(df.columns))
return
print(f"找到货号列: {product_code_column}")
# 提取货号
product_codes = df[product_code_column].dropna().astype(str).tolist()
product_codes = [code.strip() for code in product_codes if code.strip()]
print(f"共提取到 {len(product_codes)} 个货号")
if not product_codes:
print("没有找到有效的货号数据")
return
# 生成SQL语句
generate_delete_sql(product_codes)
except Exception as e:
print(f"处理Excel文件时出错: {str(e)}")
def generate_delete_sql(product_codes):
"""
生成删除SQL语句
"""
# 格式化货号列表
formatted_codes = [f"'{code}'" for code in product_codes]
codes_str = ',\n '.join(formatted_codes)
sql_template = f"""
-- 根据货号删除商品及相关数据的SQL语句
-- 总共 {len(product_codes)} 个货号
-- 1. 首先查询要删除的商品ID (用于确认)
SELECT id, product_code, product_name
FROM sm_erp_spu
WHERE product_code IN (
{codes_str}
);
-- 2. 删除相关的辅助条码数据
DELETE FROM sm_erp_auxiliary_code
WHERE spu_id IN (
SELECT id FROM sm_erp_spu WHERE product_code IN (
{codes_str}
)
);
-- 3. 删除相关的单位数据
DELETE FROM sm_erp_unit
WHERE spu_id IN (
SELECT id FROM sm_erp_spu WHERE product_code IN (
{codes_str}
)
);
-- 4. 删除相关的库存数据
DELETE FROM sm_erp_stock
WHERE erp_spu_id IN (
SELECT id FROM sm_erp_spu WHERE product_code IN (
{codes_str}
)
);
-- 5. 最后删除商品主表数据
DELETE FROM sm_erp_spu
WHERE product_code IN (
{codes_str}
);
-- 货号列表:
{chr(10).join([f"-- {i+1}. {code}" for i, code in enumerate(product_codes)])}
"""
# 保存到文件
with open('delete_products.sql', 'w', encoding='utf-8') as f:
f.write(sql_template)
print("SQL语句已生成并保存到 delete_products.sql 文件")
print("\n" + "="*50)
print(sql_template)
if __name__ == "__main__":
# Excel文件路径
excel_file = "pjcloud-supermarket/pjcloud-supermarket-biz/青少年宫.111xls.xls"
if not Path(excel_file).exists():
print(f"Excel文件不存在: {excel_file}")
sys.exit(1)
extract_product_codes_and_generate_sql(excel_file)
@@ -77,5 +77,6 @@
<groupId>io.swagger.core.v3</groupId>
<artifactId>swagger-annotations-jakarta</artifactId>
</dependency>
</dependencies>
</project>
@@ -110,4 +110,6 @@ public interface ErrorCodes {
*/
String APP_USER_USERINFO_EMPTY = "app.user.userInfo.empty";
}
@@ -0,0 +1,111 @@
package com.bsd.pjcloud.integrated.constant;
/**
* 商城相关常量
*
* @author pjcloud
* @date 2025-01-27
*/
public interface IntegratedConstants {
/**
* 会议保洁/会务人员用户类型
*/
interface MeetingUserType {
/**
* 启用状态
*/
String BJ_USER = "1";
/**
* 禁用状态
*/
String HW_USER = "2";
}
/**
* 角色相关常量
*/
interface RoleCode {
/**
* 共富商户用户角色
*/
String MERCHANT_USER = "MALL_MCH";
/**
* 合作商家商户校色
*/
String MCH_USER = "MCH_USER";
/**
* 超市管理员角色
*/
String SUPERMARKET_ADMIN = "SUPERMARKET_ADMIN";
}
/**
* 商户相关常量
*/
interface Merchant {
/**
* 商户状态 - 正常
*/
String STATUS_NORMAL = "1";
/**
* 商户状态 - 停用
*/
String STATUS_DISABLED = "0";
}
/**
* 删除标记
*/
interface DelFlag {
/**
* 正常
*/
String NORMAL = "0";
/**
* 删除
*/
String DELETED = "1";
}
/**
* 会议室状态
*/
interface MeetingRoomStatus {
/**
* 维护中
*/
String MAINTENANCE = "1";
/**
* 可用
*/
String AVAILABLE = "0";
}
/**
* 会务服务需求
*/
interface NeedService {
/**
* 不需要
*/
String NO = "0";
/**
* 需要
*/
String YES = "1";
}
}
@@ -22,4 +22,10 @@ public class MeetingBookingSaveDto {
@Schema(description = "结束时间,格式HH:mm")
private String endTime;
@Schema(description = "是否需要会务服务(0=不需要 1=需要)")
private String needService;
@Schema(description = "桌子摆放形态")
private String deskLayout;
}
@@ -0,0 +1,29 @@
package com.bsd.pjcloud.integrated.vo.clean;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
* 保洁用户下拉列表VO
*
* @author zjf
* @date 2025-01-27
*/
@Data
@Schema(description = "保洁用户下拉列表")
public class CleanUserDropdownVo {
@Schema(description = "id")
private Long id;
@Schema(description = "姓名")
private String name;
@Schema(description = "手机号")
private String phone;
@Schema(description = "1保洁阿姨,2会务人员")
private String type;
}
@@ -49,4 +49,10 @@ public class MeetingBookingPageVo {
@Schema(description = "审核原因")
private String checkReason;
@Schema(description = "是否需要会务服务(0=不需要 1=需要)")
private String needService;
@Schema(description = "桌子摆放形态")
private String deskLayout;
}
@@ -0,0 +1,198 @@
package com.bsd.pjcloud.integrated.vo.meeting.app;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
/**
* 会议预约详情VO
*
* @author zjf
* @date 2025-01-17
*/
@Data
@Schema(description = "会议预约详情VO")
public class MeetingBookingDetailVo {
/**
* 预约ID
*/
@Schema(description = "预约ID")
private Long id;
/**
* 会议主题
*/
@Schema(description = "会议主题")
private String title;
/**
* 会议室ID
*/
@Schema(description = "会议室ID")
private Long roomId;
/**
* 会议室名称
*/
@Schema(description = "会议室名称")
private String roomName;
/**
* 会议室位置
*/
@Schema(description = "会议室位置")
private String roomLocation;
/**
* 会议室容量
*/
@Schema(description = "会议室容量")
private Integer roomCapacity;
/**
* 会议室设备
*/
@Schema(description = "会议室设备")
private String roomEquipment;
/**
* 会议室图片URL
*/
@Schema(description = "会议室图片URL")
private String roomPicUrl;
/**
* 会议室描述
*/
@Schema(description = "会议室描述")
private String roomDescription;
/**
* 会议室状态
*/
@Schema(description = "会议室状态")
private String roomStatus;
/**
* 会议室状态描述
*/
@Schema(description = "会议室状态描述")
private String roomStatusText;
/**
* 会议说明
*/
@Schema(description = "会议说明")
private String description;
/**
* 审批状态
*/
@Schema(description = "审批状态")
private String checkStatus;
/**
* 审批状态描述
*/
@Schema(description = "审批状态描述")
private String checkStatusText;
/**
* 审批原因
*/
@Schema(description = "审批原因")
private String checkReason;
/**
* 申请人ID
*/
@Schema(description = "申请人ID")
private Long applicantId;
/**
* 申请人姓名
*/
@Schema(description = "申请人姓名")
private String applicantName;
/**
* 会议负责人
*/
@Schema(description = "会议负责人")
private String assigneeName;
/**
* 预约日期
*/
@Schema(description = "预约日期")
@JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
private LocalDate bookingDate;
/**
* 开始时间
*/
@Schema(description = "开始时间")
@JsonFormat(pattern = "HH:mm", timezone = "GMT+8")
private LocalTime startTime;
/**
* 结束时间
*/
@Schema(description = "结束时间")
@JsonFormat(pattern = "HH:mm", timezone = "GMT+8")
private LocalTime endTime;
/**
* 会议时长(分钟)
*/
@Schema(description = "会议时长(分钟)")
private Integer duration;
/**
* 是否需要会务服务(0=不需要 1=需要)
*/
@Schema(description = "是否需要会务服务(0=不需要 1=需要)")
private String needService;
/**
* 是否需要会务服务描述
*/
@Schema(description = "是否需要会务服务描述")
private String needServiceText;
/**
* 桌子摆放形态
*/
@Schema(description = "桌子摆放形态")
private String deskLayout;
/**
* 桌子摆放形态描述
*/
@Schema(description = "桌子摆放形态描述")
private String deskLayoutText;
/**
* 是否确认结束
*/
@Schema(description = "是否确认结束")
private Integer isConfirmEnd;
/**
* 创建人
*/
@Schema(description = "创建人")
private String createBy;
/**
* 创建时间
*/
@Schema(description = "创建时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private LocalDateTime createTime;
}
@@ -0,0 +1,158 @@
package com.bsd.pjcloud.integrated.vo.meeting.app;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
/**
* 我的预约列表VO
*
* @author zjf
* @date 2025-01-17
*/
@Data
@Schema(description = "我的预约列表VO")
public class MeetingBookingListVo {
/**
* id
*/
@Schema(description = "预约ID")
private Long id;
/**
* 会议主题
*/
@Schema(description = "会议主题")
private String title;
/**
* 会议室id
*/
@Schema(description = "会议室ID")
private Long roomId;
/**
* 会议室名称
*/
@Schema(description = "会议室名称")
private String roomName;
/**
* 会议说明
*/
@Schema(description = "会议说明")
private String description;
/**
* 审批状态
*/
@Schema(description = "审批状态")
private String checkStatus;
/**
* 审批状态描述
*/
@Schema(description = "审批状态描述")
private String checkStatusText;
/**
* 部门id
*/
@Schema(description = "部门ID")
private Long deptId;
/**
* 部门名称
*/
@Schema(description = "部门名称")
private String deptName;
/**
* 申请人
*/
@Schema(description = "申请人ID")
private Long applicantId;
/**
* 申请人姓名
*/
@Schema(description = "申请人姓名")
private String applicantName;
/**
* 会议负责人
*/
@Schema(description = "会议负责人")
private String assigneeName;
/**
* 预约日期
*/
@Schema(description = "预约日期")
@JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
private LocalDate bookingDate;
/**
* 开始时间
*/
@Schema(description = "开始时间")
@JsonFormat(pattern = "HH:mm", timezone = "GMT+8")
private LocalTime startTime;
/**
* 结束时间
*/
@Schema(description = "结束时间")
@JsonFormat(pattern = "HH:mm", timezone = "GMT+8")
private LocalTime endTime;
/**
* 会议时长(分钟)
*/
@Schema(description = "会议时长(分钟)")
private Integer duration;
/**
* 是否需要会务服务(0=不需要 1=需要)
*/
@Schema(description = "是否需要会务服务(0=不需要 1=需要)")
private String needService;
/**
* 桌子摆放形态
*/
@Schema(description = "桌子摆放形态")
private String deskLayout;
/**
* 审批原因
*/
@Schema(description = "审批原因")
private String checkReason;
/**
* 是否确认结束
*/
@Schema(description = "是否确认结束")
private Integer isConfirmEnd;
/**
* 创建时间
*/
@Schema(description = "创建时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private LocalDateTime createTime;
/**
* 更新时间
*/
@Schema(description = "更新时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private LocalDateTime updateTime;
}
@@ -30,4 +30,10 @@ public class MeetingBookingReqVo {
@Schema(description = "参会人员ID列表")
private List<Long> attendees;
@Schema(description = "桌子摆放形态")
private String deskLayout;
@Schema(description = "是否需要会务服务")
private String needService;
}
@@ -0,0 +1,124 @@
package com.bsd.pjcloud.integrated.vo.visitor.app;
import com.bsd.pjcloud.integrated.dto.visitor.app.VisitorsCarAppDTO;
import com.bsd.pjcloud.integrated.dto.visitor.app.VisitorsPeopleAppDTO;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.time.LocalDateTime;
import java.util.List;
/**
* 访客表
*
* @author zjf
* @date 2025-04-09 15:22:47
*/
@Data
@Schema(description = "app邀约详情vo")
public class VisitorsAppYYDetailVo {
/**
* 姓名
*/
@Schema(description="姓名")
@NotNull
private String name;
/**
* 手机号
*/
@NotNull
@Schema(description="手机号")
private String mobile;
/**
* 身份证号
*/
@Schema(description="身份证号")
private String idCard;
/**
* 描述
*/
@Schema(description="描述")
private String reasonValue;
/**
* 到访单位
*/
@Schema(description="到访单位id")
private Long deptId;
/**
* 到访单位
*/
@Schema(description="到访单位")
private String deptName;
/**
* 接待人
*/
@Schema(description="接待人")
private String userName;
/**
* 公司名称
*/
@Schema(description="公司名称")
private String companyName;
/**
* 随行人员数量
*/
@Schema(description="随行人员数量")
private Integer peopleNumber;
/**
* 到访时间
*/
@Schema(description="到访时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private LocalDateTime vStartDate;
/**
* 到访时间
*/
@Schema(description="到访时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private LocalDateTime vEndDate;
/**
* 是否驾驶车辆0 否 1是
*/
@Schema(description="是否驾驶车辆0 否 1是")
private String isDrive;
/**
* 是否驾驶车辆0 否 1是
*/
@Schema(description="是否有随行人员 0 否 1是")
private String isOtherPeople;
@Schema(description="接待人id")
private Long receiveId;
@Schema(description="车牌号集合")
private List<VisitorsCarAppDTO> carList;
@Schema(description="随行人员集合")
private List <VisitorsPeopleAppDTO> otherPeople;
// @Schema(description="邀约id")
// private Long yyId;
}
@@ -0,0 +1,230 @@
# 会议室预约流程动态审批人实现示例
## 📋 概述
本文档展示如何在会议室预约的save方法中实现根据会议室管理员字段动态确定审批人的功能。
## 🔧 核心修改
### 1. 添加必要的依赖注入
```java
@RestController
@RequiredArgsConstructor
public class MeetingBookingController {
private final MeetingBookingService meetingBookingService;
private final MeetingRoomService meetingRoomService;
private final RemoteFlowApiFlowService remoteFlowApiFlowService;
private final RemoteUserService remoteUserService; // 新增:用于获取用户信息
}
```
### 2. 修改save方法中的流程启动部分
```java
@PostMapping
@Transactional
public R save(@RequestBody MeetingBookingSaveDto dto) {
// ... 前面的验证逻辑保持不变 ...
// 7. 发起流程
var processByBusinessKey = remoteFlowApiFlowService.getProcessByBusinessKey(IntegratedFlowConstant.INTEGRATED_MEETING_BOOKING);
if (!processByBusinessKey.isOk() || processByBusinessKey.getData() == null) {
throw new ServiceException("流程配置获取失败: " + processByBusinessKey.getMsg());
}
Process process = processByBusinessKey.getData();
// 构建流程参数
MeetingFlowDto meetingFlowDto = new MeetingFlowDto();
meetingFlowDto.setRoomName(room.getRoomName());
meetingFlowDto.setApplicantName(SecurityUtils.getUser().getUsername());
meetingFlowDto.setMeetingTime(bookingDate + " " + startTime + "-" + endTime);
Map<String, Object> stringObjectMap = BeanUtil.beanToMap(meetingFlowDto);
// 🔥 关键:动态设置审批人 - 根据会议室管理员
List<NodeUser> approvers = determineApproversByRoomAdmin(room);
if (!approvers.isEmpty()) {
// 注意:这里的 "approval_node_1" 需要根据实际流程设计中的节点ID调整
stringObjectMap.put("approval_node_1_assignee_select", approvers);
}
ProcessInstanceParamDto processInstanceParamDto = new ProcessInstanceParamDto();
processInstanceParamDto.setFlowId(process.getFlowId());
processInstanceParamDto.setParamMap(stringObjectMap);
var stringR = remoteFlowApiFlowService.startProcessInstance(processInstanceParamDto);
// ... 后续逻辑保持不变 ...
}
```
### 3. 实现动态审批人确定方法
```java
/**
* 根据会议室管理员确定审批人
* @param room 会议室信息
* @return 审批人列表
*/
private List<NodeUser> determineApproversByRoomAdmin(MeetingRoomEntity room) {
List<NodeUser> approvers = new ArrayList<>();
// 1. 优先使用会议室指定的管理员
if (room.getAdminId() != null) {
try {
// 获取管理员用户信息
R<List<SysUser>> userResult = remoteUserService.getUserById(Arrays.asList(room.getAdminId()));
if (userResult.isOk() && !userResult.getData().isEmpty()) {
SysUser adminUser = userResult.getData().get(0);
NodeUser approver = NodeUser.builder()
.id(adminUser.getUserId())
.name(adminUser.getName())
.type("user")
.selected(true)
.avatar(adminUser.getAvatar())
.build();
approvers.add(approver);
}
} catch (Exception e) {
log.error("获取会议室管理员信息失败: {}", e.getMessage());
}
}
// 2. 如果会议室没有管理员,使用部门经理
if (approvers.isEmpty() && room.getDeptId() != null) {
approvers.addAll(getDeptManagerAsApprovers(room.getDeptId()));
}
// 3. 如果仍然没有审批人,使用默认审批人
if (approvers.isEmpty()) {
approvers.addAll(getDefaultApprovers());
}
return approvers;
}
/**
* 获取部门经理作为审批人
*/
private List<NodeUser> getDeptManagerAsApprovers(Long deptId) {
List<NodeUser> approvers = new ArrayList<>();
try {
// 这里需要根据实际的部门服务接口获取部门经理
// 示例:假设有部门服务可以获取部门经理
/*
R<SysDept> deptResult = remoteDeptService.getDeptById(deptId);
if (deptResult.isOk() && deptResult.getData() != null) {
SysDept dept = deptResult.getData();
Long managerId = dept.getManagerId();
if (managerId != null) {
R<List<SysUser>> userResult = remoteUserService.getUserById(Arrays.asList(managerId));
if (userResult.isOk() && !userResult.getData().isEmpty()) {
SysUser manager = userResult.getData().get(0);
NodeUser approver = NodeUser.builder()
.id(manager.getUserId())
.name(manager.getName())
.type("user")
.selected(true)
.avatar(manager.getAvatar())
.build();
approvers.add(approver);
}
}
}
*/
} catch (Exception e) {
log.error("获取部门经理信息失败: {}", e.getMessage());
}
return approvers;
}
/**
* 获取默认审批人
*/
private List<NodeUser> getDefaultApprovers() {
List<NodeUser> approvers = new ArrayList<>();
try {
// 设置系统管理员作为默认审批人
Long systemAdminId = 1L; // 需要替换为实际的系统管理员ID
NodeUser defaultApprover = NodeUser.builder()
.id(systemAdminId)
.name("系统管理员")
.type("user")
.selected(true)
.build();
approvers.add(defaultApprover);
} catch (Exception e) {
log.error("获取默认审批人失败: {}", e.getMessage());
}
return approvers;
}
```
## 🎯 关键要点
### 1. 流程节点ID配置
```java
// 这个key的格式是:{节点ID}_assignee_select
stringObjectMap.put("approval_node_1_assignee_select", approvers);
```
- `approval_node_1` 需要替换为实际流程设计中的审批节点ID
- 节点ID可以在流程设计器中查看
### 2. NodeUser对象构建
```java
NodeUser approver = NodeUser.builder()
.id(adminUser.getUserId()) // 用户ID
.name(adminUser.getName()) // 用户姓名
.type("user") // 用户类型,固定为"user"
.selected(true) // 是否选中
.avatar(adminUser.getAvatar()) // 用户头像(可选)
.build();
```
### 3. 审批人优先级
1. **会议室管理员** - 最高优先级
2. **部门经理** - 次优先级
3. **默认审批人** - 兜底方案
### 4. 错误处理
- 所有获取用户信息的操作都有异常处理
- 确保即使获取审批人失败,流程也能正常启动
- 使用日志记录错误信息,便于排查问题
## 🔄 流程设计配置
在流程设计器中,需要将审批节点配置为:
- **分配类型**: `SELF_SELECT` (发起人自选)
- **节点ID**: 与代码中的key保持一致
- **多实例模式**: 根据需要选择会签/或签/顺签
## 📝 使用示例
```java
// 会议室预约时,系统会自动:
// 1. 检查会议室是否有管理员
// 2. 如果有管理员,使用管理员作为审批人
// 3. 如果没有管理员,使用部门经理
// 4. 如果都没有,使用系统管理员
// 流程启动后,审批人会收到待办任务
// 审批人可以在流程中看到会议预约的详细信息
```
## ⚠️ 注意事项
1. **节点ID一致性**: 确保代码中的节点ID与流程设计中的节点ID一致
2. **用户服务可用性**: 确保用户服务接口可用,避免获取用户信息失败
3. **权限控制**: 确保选择的审批人有相应的审批权限
4. **测试验证**: 建议在测试环境先验证流程是否正常工作
## 🚀 扩展功能
可以根据业务需求进一步扩展:
1. **多级审批**: 根据会议室等级设置多级审批
2. **时间限制**: 根据预约时间长度确定审批人
3. **部门规则**: 根据部门特殊规则确定审批人
4. **用户角色**: 根据用户角色确定审批人
@@ -0,0 +1,137 @@
# 移动端摆放形式选择接口使用说明
## 📋 概述
本文档介绍移动端预约课程时选择摆放形式的接口使用方法。
## 🌐 接口信息
### 获取摆放形式列表接口
- **接口地址**: `GET /meetingDeskLayout/app/list`
- **请求方式**: GET
- **认证方式**: 无需认证(移动端公开接口)
- **数据格式**: JSON
## 📡 响应格式
### 成功响应
```json
{
"code": 0,
"msg": "操作成功",
"data": [
{
"id": 1,
"layoutName": "U型摆放",
"layoutImage": "https://example.com/images/u-layout.jpg",
"capacityMin": 8,
"capacityMax": 12,
"description": "适合小组讨论的U型摆放方式"
},
{
"id": 2,
"layoutName": "课桌式摆放",
"layoutImage": "https://example.com/images/classroom-layout.jpg",
"capacityMin": 20,
"capacityMax": 50,
"description": "传统课桌式摆放,适合培训课程"
},
{
"id": 3,
"layoutName": "圆桌式摆放",
"layoutImage": "https://example.com/images/round-table-layout.jpg",
"capacityMin": 6,
"capacityMax": 10,
"description": "圆桌式摆放,促进互动交流"
}
]
}
```
### 字段说明
| 字段名 | 类型 | 说明 |
|--------|------|------|
| id | Long | 摆放形式ID |
| layoutName | String | 摆放形式名称 |
| layoutImage | String | 摆放形式示意图URL |
| capacityMin | Integer | 最小容纳人数 |
| capacityMax | Integer | 最大容纳人数 |
| description | String | 摆放形式描述 |
## 🔍 业务逻辑说明
1. **数据过滤**: 只返回状态为"可用"(isAvailable = "0")的摆放形式
2. **排序规则**: 按照sortOrder字段升序排列
3. **数据精简**: 只返回移动端需要的核心字段,减少数据传输量
## 📱 使用示例
### JavaScript 调用示例
```javascript
fetch('/meetingDeskLayout/app/list', {
method: 'GET',
headers: {
'Content-Type': 'application/json'
}
})
.then(response => response.json())
.then(data => {
if (data.code === 0) {
// 处理摆放形式数据
const layouts = data.data;
layouts.forEach(layout => {
console.log(`摆放形式: ${layout.layoutName}, 容纳人数: ${layout.capacityMin}-${layout.capacityMax}人`);
});
}
})
.catch(error => {
console.error('获取摆放形式失败:', error);
});
```
### React Native 调用示例
```javascript
const fetchDeskLayouts = async () => {
try {
const response = await fetch('/meetingDeskLayout/app/list');
const result = await response.json();
if (result.code === 0) {
return result.data;
} else {
throw new Error(result.msg);
}
} catch (error) {
console.error('获取摆放形式失败:', error);
return [];
}
};
```
### cURL 调用示例
```bash
curl -X GET "http://your-domain/meetingDeskLayout/app/list" \
-H "Content-Type: application/json"
```
## 🎯 应用场景
1. **课程预约**: 用户在预约课程时选择适合的摆放形式
2. **会议室配置**: 根据参会人数推荐合适的摆放形式
3. **活动策划**: 根据活动类型选择合适的桌椅摆放方式
## ⚠️ 注意事项
1. **数据一致性**: 接口返回的数据与后台管理系统的摆放形式数据保持一致
2. **图片加载**: layoutImage字段可能为空,前端需要处理图片加载失败的情况
3. **容量限制**: 建议根据实际参会人数选择合适的摆放形式
4. **缓存策略**: 建议前端对摆放形式数据进行适当缓存,减少重复请求
## 🔄 版本更新
- **v1.0.0** (2025-01-27): 初始版本,提供基本的摆放形式选择功能
@@ -19,6 +19,7 @@ import com.bsd.pjcloud.integrated.entity.VisitorsEntity;
import com.bsd.pjcloud.integrated.service.VisitorUserService;
import com.bsd.pjcloud.integrated.service.VisitorsService;
import com.bsd.pjcloud.integrated.vo.visitor.app.VisitorsAppVo;
import com.bsd.pjcloud.integrated.vo.visitor.app.VisitorsAppYYDetailVo;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.tags.Tag;
@@ -107,6 +108,20 @@ public class AppVisitorController {
return R.ok(visitorsService.getByToken(token));
}
/**
* 邀约详情
*
* @param token
* @return
*/
@Operation(summary = "通过token查询邀约详情" , description = "通过token查询邀约详情" )
@GetMapping("/get/visitorsYYDetail")
public R<VisitorsAppYYDetailVo> getVisitorsYYDetail(@RequestParam("token") String token) {
return R.ok(visitorsService.getVisitorsYYDetail(token));
}
@PostMapping("/app/receiveYY")
@SysLog("访客接收邀约更新状态" )
@Operation(summary = "访客接收邀约" , description = "访客接收邀约" )
@@ -7,10 +7,12 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.bsd.pjcloud.common.core.util.R;
import com.bsd.pjcloud.common.core.util.BeanUtils;
import com.bsd.pjcloud.common.log.annotation.SysLog;
import com.bsd.pjcloud.common.security.util.SecurityUtils;
import com.bsd.pjcloud.integrated.entity.CleanUserEntity;
import com.bsd.pjcloud.integrated.service.CleanUserService;
import com.bsd.pjcloud.integrated.vo.clean.CleanUserDropdownVo;
import org.springframework.security.access.prepost.PreAuthorize;
import com.bsd.pjcloud.common.excel.annotation.ResponseExcel;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
@@ -52,6 +54,7 @@ public class CleanUserController {
LambdaQueryWrapper<CleanUserEntity> wrapper = Wrappers.lambdaQuery();
wrapper.eq(StrUtil.isNotBlank(cleanUser.getName()),CleanUserEntity::getName,cleanUser.getName());
wrapper.eq(StrUtil.isNotBlank(cleanUser.getPhone()),CleanUserEntity::getPhone,cleanUser.getPhone());
wrapper.eq(CleanUserEntity::getType, cleanUser.getType());
return R.ok(cleanUserService.page(page, wrapper));
}
@@ -105,6 +108,15 @@ public class CleanUserController {
@DeleteMapping
@PreAuthorize("@pms.hasPermission('integrated_cleanUser_del')" )
public R removeById(@RequestBody Long[] ids) {
// 验证要删除的记录是否属于指定的type
LambdaQueryWrapper<CleanUserEntity> wrapper = Wrappers.lambdaQuery();
wrapper.in(CleanUserEntity::getId, CollUtil.toList(ids));
List<CleanUserEntity> validRecords = cleanUserService.list(wrapper);
if (validRecords.size() != ids.length) {
return R.failed("部分记录不属于指定类型,无法删除");
}
return R.ok(cleanUserService.removeBatchByIds(CollUtil.toList(ids)));
}
@@ -113,13 +125,18 @@ public class CleanUserController {
* 导出excel 表格
* @param cleanUser 查询条件
* @param ids 导出指定ID
* @param type 类型:1保洁阿姨,2会务人员
* @return excel 文件流
*/
@ResponseExcel
@GetMapping("/export")
@PreAuthorize("@pms.hasPermission('integrated_cleanUser_export')" )
public List<CleanUserEntity> export(CleanUserEntity cleanUser,Long[] ids) {
return cleanUserService.list(Wrappers.lambdaQuery(cleanUser).in(ArrayUtil.isNotEmpty(ids), CleanUserEntity::getId, ids));
public List<CleanUserEntity> export(CleanUserEntity cleanUser, Long[] ids,
@RequestParam(value = "type", required = false) String type) {
LambdaQueryWrapper<CleanUserEntity> wrapper = Wrappers.lambdaQuery(cleanUser);
wrapper.in(ArrayUtil.isNotEmpty(ids), CleanUserEntity::getId, ids);
wrapper.eq(StrUtil.isNotBlank(type), CleanUserEntity::getType, type);
return cleanUserService.list(wrapper);
}
@@ -130,12 +147,13 @@ public class CleanUserController {
@Operation(summary = "获取保洁/会务人员用户下拉列表", description = "获取保洁/会务人员用户下拉列表")
@GetMapping("/dropdown")
// @PreAuthorize("@pms.hasPermission('integrated_cleanUser_view')")
public R getCleanUserDropdown(@RequestParam("type")String type) {
public R<List<CleanUserDropdownVo>> getCleanUserDropdown(@RequestParam(value = "type",required = false)String type) {
LambdaQueryWrapper<CleanUserEntity> wrapper = Wrappers.lambdaQuery();
wrapper.eq(CleanUserEntity::getType, type);
wrapper.select(CleanUserEntity::getId, CleanUserEntity::getName, CleanUserEntity::getPhone);
wrapper.eq(type != null,CleanUserEntity::getType, type);
// wrapper.select(CleanUserEntity::getId, CleanUserEntity::getName, CleanUserEntity::getPhone);
wrapper.orderByAsc(CleanUserEntity::getName);
List<CleanUserEntity> list = cleanUserService.list(wrapper);
return R.ok(list);
List<CleanUserDropdownVo> voList = BeanUtils.toBean(list, CleanUserDropdownVo.class);
return R.ok(voList);
}
}
@@ -24,6 +24,7 @@ import io.swagger.v3.oas.annotations.Operation;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import com.bsd.pjcloud.integrated.vo.feedback.FeedbackScreenVo;
import com.bsd.pjcloud.integrated.vo.FeedbackTypeVO;
import java.util.stream.Collectors;
import java.util.HashMap;
@@ -185,8 +186,20 @@ public class FeedbackController {
@Operation(summary = "获取反馈类型" , description = "获取反馈类型" )
@GetMapping("/app/getFeedbackType" )
public R getFeedbackType() {
return R.ok(remoteDictService.getDictByType("feedback_type"));
public R<List<FeedbackTypeVO>> getFeedbackType() {
R<List<SysDictItem>> dictResult = remoteDictService.getDictByType("feedback_type");
if (!dictResult.isOk() || dictResult.getData() == null) {
return R.failed("获取反馈类型失败");
}
List<FeedbackTypeVO> voList = dictResult.getData().stream().map(item -> {
FeedbackTypeVO vo = new FeedbackTypeVO();
vo.setId(item.getId());
vo.setLabel(item.getLabel());
return vo;
}).collect(Collectors.toList());
return R.ok(voList);
}
/**
@@ -25,6 +25,7 @@ import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import com.bsd.pjcloud.integrated.dto.MaintenanceOrderAppCreateDTO;
import com.bsd.pjcloud.integrated.vo.maintenance.MaintenanceOrderScreenVo;
import com.bsd.pjcloud.integrated.vo.MaintenanceTypeVO;
import com.bsd.pjcloud.admin.api.entity.SysDictItem;
import java.util.Map;
@@ -140,7 +141,7 @@ public class MaintenanceOrderController {
@PostMapping("/app/apply")
@Operation(summary = "App端-维修申请新增", description = "App端用户提交维修申请")
@SysLog("App端-维修申请新增")
@PreAuthorize("@pms.hasPermission('integrated_maintenanceOrder_add')")
// @PreAuthorize("@pms.hasPermission('integrated_maintenanceOrder_add')")
public R appApply(@RequestBody MaintenanceOrderAppCreateDTO dto) {
PjcloudUser user = SecurityUtils.getUser();
Long userId = user.getId();
@@ -158,7 +159,7 @@ public class MaintenanceOrderController {
*/
@GetMapping("/app/my")
@Operation(summary = "App端-我的维修分页列表", description = "App端用户分页查询自己的维修单")
@PreAuthorize("@pms.hasPermission('integrated_maintenanceOrder_view')")
// @PreAuthorize("@pms.hasPermission('integrated_maintenanceOrder_view')")
public R myMaintenanceOrders(@ParameterObject Page page) {
Long userId = SecurityUtils.getUser().getId();
LambdaQueryWrapper<MaintenanceOrderEntity> wrapper = Wrappers.lambdaQuery();
@@ -173,7 +174,7 @@ public class MaintenanceOrderController {
*/
@GetMapping("/app/my/{id}")
@Operation(summary = "App端-我的维修单详情", description = "App端用户查看自己维修单详情")
@PreAuthorize("@pms.hasPermission('integrated_maintenanceOrder_view')")
// @PreAuthorize("@pms.hasPermission('integrated_maintenanceOrder_view')")
public R myMaintenanceOrderDetail(@PathVariable("id") Long id) {
Long userId = SecurityUtils.getUser().getId();
MaintenanceOrderEntity entity = maintenanceOrderService.getById(id);
@@ -214,4 +215,22 @@ public class MaintenanceOrderController {
}).collect(Collectors.toList());
return R.ok(voList);
}
@Operation(summary = "获取维修类型" , description = "获取维修类型" )
@GetMapping("/app/getFeedbackType" )
public R<List<MaintenanceTypeVO>> getFeedbackType() {
R<List<SysDictItem>> dictResult = remoteDictService.getDictByType("maintenance_type");
if (!dictResult.isOk() || dictResult.getData() == null) {
return R.failed("获取维修类型失败");
}
List<MaintenanceTypeVO> voList = dictResult.getData().stream().map(item -> {
MaintenanceTypeVO vo = new MaintenanceTypeVO();
vo.setId(item.getId());
vo.setLabel(item.getLabel());
return vo;
}).collect(Collectors.toList());
return R.ok(voList);
}
}
@@ -100,6 +100,8 @@ public class MeetingBookingController {
vo.setCreateTime(e.getCreateTime());
vo.setCreateBy(e.getCreateBy());
vo.setDescription(e.getDescription());
vo.setNeedService(e.getNeedService());
vo.setDeskLayout(e.getDeskLayout());
return vo;
}).toList();
@@ -140,6 +142,8 @@ public class MeetingBookingController {
meetingBooking.setBookingDate(LocalDate.parse(dto.getBookingDate()));
meetingBooking.setStartTime(LocalTime.parse(dto.getStartTime()));
meetingBooking.setEndTime(LocalTime.parse(dto.getEndTime()));
meetingBooking.setNeedService(dto.getNeedService());
meetingBooking.setDeskLayout(dto.getDeskLayout());
// 2. 验证会议室是否存在
MeetingRoomEntity room = meetingRoomService.getById(meetingBooking.getRoomId());
if (room == null) {
@@ -184,7 +188,8 @@ public class MeetingBookingController {
// 7. 发起流程
var processByBusinessKey = remoteFlowApiFlowService.getProcessByBusinessKey(IntegratedFlowConstant.INTEGRATED_MEETING_BOOKING);
if (!processByBusinessKey.isOk() || processByBusinessKey.getData() == null) {
throw new ServiceException("流程配置获取失败: " + processByBusinessKey.getMsg());
// throw new ServiceException("流程配置获取失败: " + processByBusinessKey.getMsg());
return R.failed("流程配置获取失败");
}
Process process = processByBusinessKey.getData();
MeetingFlowDto meetingFlowDto = new MeetingFlowDto();
@@ -254,9 +259,9 @@ public class MeetingBookingController {
/**
* 会议开始之前,通知阿姨需要对会议室桌面和桌椅拜访进行整理
*/
@PostMapping("/app/notice-visit-desk/{bookingId}")
public R<Boolean> noticeVisitDesk(@PathVariable("bookingId") Long bookingId) {
meetingBookingService.noticeVisitDesk(bookingId);
return R.ok();
}
// @PostMapping("/app/notice-visit-desk/{bookingId}")
// public R<Boolean> noticeVisitDesk(@PathVariable("bookingId") Long bookingId) {
// meetingBookingService.noticeVisitDesk(bookingId);
// return R.ok();
// }
}
@@ -0,0 +1,143 @@
package com.bsd.pjcloud.integrated.controller;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.collection.CollUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.bsd.pjcloud.common.core.util.R;
import com.bsd.pjcloud.common.log.annotation.SysLog;
import com.bsd.pjcloud.integrated.entity.MeetingDeskLayoutEntity;
import com.bsd.pjcloud.integrated.service.MeetingDeskLayoutService;
import com.bsd.pjcloud.integrated.vo.MeetingDeskLayoutAppVO;
import com.bsd.pjcloud.common.core.util.BeanUtils;
import org.springframework.security.access.prepost.PreAuthorize;
import com.bsd.pjcloud.common.excel.annotation.ResponseExcel;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import org.springdoc.core.annotations.ParameterObject;
import org.springframework.http.HttpHeaders;
import io.swagger.v3.oas.annotations.tags.Tag;
import io.swagger.v3.oas.annotations.Operation;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Objects;
/**
* 摆放形式
*
* @author zjf
* @date 2025-09-10 12:56:45
*/
@RestController
@RequiredArgsConstructor
@RequestMapping("/meetingDeskLayout" )
@Tag(description = "meetingDeskLayout" , name = "摆放形式管理" )
@SecurityRequirement(name = HttpHeaders.AUTHORIZATION)
public class MeetingDeskLayoutController {
private final MeetingDeskLayoutService meetingDeskLayoutService;
/**
* 分页查询
* @param page 分页对象
* @param meetingDeskLayout 摆放形式
* @return
*/
@Operation(summary = "分页查询" , description = "分页查询" )
@GetMapping("/page" )
@PreAuthorize("@pms.hasPermission('integrated_meetingDeskLayout_view')" )
public R getMeetingDeskLayoutPage(@ParameterObject Page page, @ParameterObject MeetingDeskLayoutEntity meetingDeskLayout) {
LambdaQueryWrapper<MeetingDeskLayoutEntity> wrapper = Wrappers.lambdaQuery();
return R.ok(meetingDeskLayoutService.page(page, wrapper));
}
/**
* 通过id查询摆放形式
* @param id id
* @return R
*/
@Operation(summary = "通过id查询" , description = "通过id查询" )
@GetMapping("/{id}" )
@PreAuthorize("@pms.hasPermission('integrated_meetingDeskLayout_view')" )
public R getById(@PathVariable("id" ) Long id) {
return R.ok(meetingDeskLayoutService.getById(id));
}
/**
* 新增摆放形式
* @param meetingDeskLayout 摆放形式
* @return R
*/
@Operation(summary = "新增摆放形式" , description = "新增摆放形式" )
@SysLog("新增摆放形式" )
@PostMapping
@PreAuthorize("@pms.hasPermission('integrated_meetingDeskLayout_add')" )
public R save(@RequestBody MeetingDeskLayoutEntity meetingDeskLayout) {
return R.ok(meetingDeskLayoutService.save(meetingDeskLayout));
}
/**
* 修改摆放形式
* @param meetingDeskLayout 摆放形式
* @return R
*/
@Operation(summary = "修改摆放形式" , description = "修改摆放形式" )
@SysLog("修改摆放形式" )
@PutMapping
@PreAuthorize("@pms.hasPermission('integrated_meetingDeskLayout_edit')" )
public R updateById(@RequestBody MeetingDeskLayoutEntity meetingDeskLayout) {
return R.ok(meetingDeskLayoutService.updateById(meetingDeskLayout));
}
/**
* 通过id删除摆放形式
* @param ids id列表
* @return R
*/
@Operation(summary = "通过id删除摆放形式" , description = "通过id删除摆放形式" )
@SysLog("通过id删除摆放形式" )
@DeleteMapping
@PreAuthorize("@pms.hasPermission('integrated_meetingDeskLayout_del')" )
public R removeById(@RequestBody Long[] ids) {
return R.ok(meetingDeskLayoutService.removeBatchByIds(CollUtil.toList(ids)));
}
/**
* 导出excel 表格
* @param meetingDeskLayout 查询条件
* @param ids 导出指定ID
* @return excel 文件流
*/
@ResponseExcel
@GetMapping("/export")
@PreAuthorize("@pms.hasPermission('integrated_meetingDeskLayout_export')" )
public List<MeetingDeskLayoutEntity> export(MeetingDeskLayoutEntity meetingDeskLayout,Long[] ids) {
return meetingDeskLayoutService.list(Wrappers.lambdaQuery(meetingDeskLayout).in(ArrayUtil.isNotEmpty(ids), MeetingDeskLayoutEntity::getId, ids));
}
//----------------------------------APP---------------------------------*/
/**
* 移动端获取可用的摆放形式列表
* @return R
*/
@Operation(summary = "移动端获取摆放形式列表", description = "移动端预约课程时选择摆放形式")
@GetMapping("/app/list")
public R<List<MeetingDeskLayoutAppVO>> getAppLayoutList() {
LambdaQueryWrapper<MeetingDeskLayoutEntity> wrapper = Wrappers.lambdaQuery();
// 只查询可用的摆放形式
wrapper.eq(MeetingDeskLayoutEntity::getIsAvailable, "0");
// 按排序字段排序
wrapper.orderByAsc(MeetingDeskLayoutEntity::getSortOrder);
List<MeetingDeskLayoutEntity> entityList = meetingDeskLayoutService.list(wrapper);
List<MeetingDeskLayoutAppVO> voList = BeanUtils.toBean(entityList, MeetingDeskLayoutAppVO.class);
return R.ok(voList);
}
}
@@ -20,15 +20,20 @@ import com.bsd.pjcloud.flow.task.dto.ProcessNodeRecordParamDto;
import com.bsd.pjcloud.flow.task.entity.Process;
import com.bsd.pjcloud.flow.task.utils.GetCheckReasonUtils;
import com.bsd.pjcloud.flow.task.vo.CheckStatusAndReasonVo;
import com.bsd.pjcloud.integrated.constant.IntegratedConstants;
import com.bsd.pjcloud.integrated.constant.IntegratedFlowConstant;
import com.bsd.pjcloud.integrated.dto.flow.MeetingFlowDto;
import com.bsd.pjcloud.integrated.dto.meeting.app.MeetingPageDto;
import com.bsd.pjcloud.integrated.entity.MeetingBookingEntity;
import com.bsd.pjcloud.integrated.entity.MeetingEquipmentEntity;
import com.bsd.pjcloud.integrated.entity.MeetingRoomEntity;
import com.bsd.pjcloud.integrated.entity.MeetingRoomEquipmentEntity;
import com.bsd.pjcloud.integrated.service.MeetingBookingService;
import com.bsd.pjcloud.integrated.service.MeetingEquipmentService;
import com.bsd.pjcloud.integrated.service.MeetingRoomEquipmentService;
import com.bsd.pjcloud.integrated.service.MeetingRoomService;
import com.bsd.pjcloud.integrated.vo.meeting.app.MeetingBookingDetailVo;
import com.bsd.pjcloud.integrated.vo.meeting.app.MeetingBookingListVo;
import com.bsd.pjcloud.integrated.vo.meeting.app.MeetingBookingReqVo;
import com.bsd.pjcloud.integrated.vo.meeting.app.MeetingRoomPageVo;
import com.bsd.pjcloud.integrated.vo.meeting.app.TimeSlotVo;
@@ -70,6 +75,8 @@ public class MeetingRoomController {
private final MeetingRoomEquipmentService meetingRoomEquipmentService;
private final MeetingEquipmentService meetingEquipmentService;
private final MeetingBookingService meetingBookingService;
private final RemoteFlowApiFlowService remoteFlowApiFlowService;
@@ -315,12 +322,14 @@ public class MeetingRoomController {
booking.setBookingDate(bookingDate);
booking.setStartTime(startTime);
booking.setEndTime(endTime);
booking.setDeskLayout(reqVo.getDeskLayout());
booking.setNeedService(reqVo.getNeedService());
//发起流程
R<Process> processByBusinessKey = remoteFlowApiFlowService.getProcessByBusinessKey(IntegratedFlowConstant.INTEGRATED_MEETING_BOOKING);
if (!processByBusinessKey.isOk() || processByBusinessKey.getData() == null) {
throw new ServiceException("流程配置获取失败: " + processByBusinessKey.getMsg());
// throw new ServiceException("流程配置获取失败: " + processByBusinessKey.getMsg());
return R.failed("流程配置获取失败");
}
Process process = processByBusinessKey.getData();
@@ -348,19 +357,108 @@ public class MeetingRoomController {
* @param checkStatus 审核状态(可选),传空查全部
*/
@GetMapping("/app/my-bookings")
public R<Page<MeetingBookingEntity>> myBookings(@ParameterObject Page page, @RequestParam(value = "checkStatus", required = false) String checkStatus) {
public R<Page<MeetingBookingListVo>> myBookings(@ParameterObject Page page, @RequestParam(value = "checkStatus", required = false) String checkStatus) {
Long userId = SecurityUtils.getUser().getId();
LambdaQueryWrapper<MeetingBookingEntity> wrapper = Wrappers.<MeetingBookingEntity>lambdaQuery()
.eq(MeetingBookingEntity::getApplicantId, userId);
if (checkStatus != null && !checkStatus.isEmpty()) {
wrapper.eq(MeetingBookingEntity::getCheckStatus, checkStatus);
}
wrapper.orderByDesc(MeetingBookingEntity::getBookingDate)
wrapper.orderByDesc(MeetingBookingEntity::getCreateTime)
.orderByDesc(MeetingBookingEntity::getBookingDate)
.orderByDesc(MeetingBookingEntity::getStartTime);
Page<MeetingBookingEntity> result = meetingBookingService.page(page, wrapper);
return R.ok(result);
Page<MeetingBookingEntity> entityPage = meetingBookingService.page(page, wrapper);
// 批量查会议室名称,避免N+1
List<Long> roomIds = entityPage.getRecords().stream().map(MeetingBookingEntity::getRoomId).distinct().toList();
Map<Long, String> roomNameMap = new HashMap<>();
if (!roomIds.isEmpty()) {
List<MeetingRoomEntity> rooms = meetingRoomService.listByIds(roomIds);
for (MeetingRoomEntity room : rooms) {
roomNameMap.put(room.getId(), room.getRoomName());
}
}
// 转VO
List<MeetingBookingListVo> voList = entityPage.getRecords().stream().map(e -> {
MeetingBookingListVo vo = new MeetingBookingListVo();
vo.setId(e.getId());
vo.setTitle(e.getTitle());
vo.setRoomId(e.getRoomId());
vo.setRoomName(roomNameMap.get(e.getRoomId()));
vo.setDescription(e.getDescription());
vo.setCheckStatus(e.getCheckStatus());
vo.setCheckStatusText(getCheckStatusText(e.getCheckStatus()));
vo.setApplicantId(e.getApplicantId());
vo.setApplicantName(SecurityUtils.getUser().getUsername());
vo.setAssigneeName(e.getAssigneeName());
vo.setBookingDate(e.getBookingDate());
vo.setStartTime(e.getStartTime());
vo.setEndTime(e.getEndTime());
vo.setDuration(calculateDuration(e.getStartTime(), e.getEndTime()));
vo.setNeedService(e.getNeedService());
vo.setDeskLayout(e.getDeskLayout());
vo.setCheckReason(e.getCheckReason());
vo.setIsConfirmEnd(e.getIsConfirmEnd());
vo.setCreateTime(e.getCreateTime());
return vo;
}).toList();
Page<MeetingBookingListVo> voPage = new Page<>(entityPage.getCurrent(), entityPage.getSize(), entityPage.getTotal());
voPage.setRecords(voList);
return R.ok(voPage);
}
/**
* 获取审核状态描述
*/
private String getCheckStatusText(String checkStatus) {
if (checkStatus == null) return "";
switch (checkStatus) {
case "1": return "审核中";
case "2": return "审核通过";
case "3": return "审核拒绝";
case "4": return "已取消";
case "5": return "取消";
case "0": return "已到访";
default: return "未知状态";
}
}
/**
* 计算会议时长(分钟)
*/
private Integer calculateDuration(LocalTime startTime, LocalTime endTime) {
if (startTime == null || endTime == null) return 0;
return (int) java.time.Duration.between(startTime, endTime).toMinutes();
}
/**
* 获取会议室状态描述
*/
private String getRoomStatusText(String status) {
if (status == null) return "";
switch (status) {
case IntegratedConstants.MeetingRoomStatus.MAINTENANCE: return "维护中";
case IntegratedConstants.MeetingRoomStatus.AVAILABLE: return "可用";
default: return "未知状态";
}
}
/**
* 获取是否需要会务服务描述
*/
private String getNeedServiceText(String needService) {
if (needService == null) return "";
switch (needService) {
case IntegratedConstants.NeedService.NO: return "不需要";
case IntegratedConstants.NeedService.YES: return "需要";
default: return "未知";
}
}
/**
* 取消预约
*/
@@ -384,7 +482,7 @@ public class MeetingRoomController {
* 预约详情
*/
@GetMapping("/app/booking-detail/{bookingId}")
public R<MeetingBookingEntity> bookingDetail(@PathVariable("bookingId") Long bookingId) {
public R<MeetingBookingDetailVo> bookingDetail(@PathVariable("bookingId") Long bookingId) {
Long userId = SecurityUtils.getUser().getId();
MeetingBookingEntity booking = meetingBookingService.getById(bookingId);
if (booking == null) {
@@ -393,7 +491,82 @@ public class MeetingRoomController {
if (!Objects.equals(booking.getApplicantId(), userId)) {
return R.failed("无权查看他人预约详情");
}
return R.ok(booking);
// 查询会议室详细信息
MeetingRoomEntity room = meetingRoomService.getById(booking.getRoomId());
if (room == null) {
return R.failed("会议室不存在");
}
// 查询会议室设备信息
List<MeetingRoomEquipmentEntity> equipmentList = meetingRoomEquipmentService.list(
Wrappers.<MeetingRoomEquipmentEntity>lambdaQuery()
.eq(MeetingRoomEquipmentEntity::getRoomId, booking.getRoomId())
);
// 构建设备信息字符串
StringBuilder equipmentInfo = new StringBuilder();
if (!equipmentList.isEmpty()) {
// 获取所有设备ID
List<Long> equipmentIds = equipmentList.stream()
.map(MeetingRoomEquipmentEntity::getEquipId)
.distinct()
.toList();
// 批量查询设备信息
Map<Long, String> equipmentNameMap = new HashMap<>();
if (!equipmentIds.isEmpty()) {
List<MeetingEquipmentEntity> equipments = meetingEquipmentService.listByIds(equipmentIds);
for (MeetingEquipmentEntity equipment : equipments) {
equipmentNameMap.put(equipment.getId(), equipment.getName());
}
}
// 构建设备信息字符串
for (MeetingRoomEquipmentEntity roomEquipment : equipmentList) {
if (equipmentInfo.length() > 0) {
equipmentInfo.append("、");
}
String equipmentName = equipmentNameMap.get(roomEquipment.getEquipId());
if (equipmentName != null) {
equipmentInfo.append(equipmentName)
.append("(").append(roomEquipment.getQuantity()).append("台)");
}
}
}
// 转换为VO
MeetingBookingDetailVo vo = new MeetingBookingDetailVo();
vo.setId(booking.getId());
vo.setTitle(booking.getTitle());
vo.setRoomId(booking.getRoomId());
vo.setRoomName(room.getRoomName());
vo.setRoomLocation(room.getLocation());
vo.setRoomCapacity(room.getCapacity());
vo.setRoomEquipment(equipmentInfo.toString());
vo.setRoomPicUrl(room.getPicUrl());
vo.setRoomDescription(room.getDescription());
vo.setRoomStatus(room.getStatus());
vo.setRoomStatusText(getRoomStatusText(room.getStatus()));
vo.setDescription(booking.getDescription());
vo.setCheckStatus(booking.getCheckStatus());
vo.setCheckStatusText(getCheckStatusText(booking.getCheckStatus()));
vo.setCheckReason(booking.getCheckReason());
vo.setApplicantId(booking.getApplicantId());
vo.setApplicantName(SecurityUtils.getUser().getUsername());
vo.setAssigneeName(booking.getAssigneeName());
vo.setBookingDate(booking.getBookingDate());
vo.setStartTime(booking.getStartTime());
vo.setEndTime(booking.getEndTime());
vo.setDuration(calculateDuration(booking.getStartTime(), booking.getEndTime()));
vo.setNeedService(booking.getNeedService());
vo.setNeedServiceText(getNeedServiceText(booking.getNeedService()));
vo.setDeskLayout(booking.getDeskLayout());
vo.setDeskLayoutText(booking.getDeskLayout());
vo.setIsConfirmEnd(booking.getIsConfirmEnd());
vo.setCreateBy(booking.getCreateBy());
vo.setCreateTime(booking.getCreateTime());
return R.ok(vo);
}
@@ -408,10 +581,23 @@ public class MeetingRoomController {
System.out.println(processNodeRecordParamDto.getData());
LambdaQueryWrapper<MeetingBookingEntity> boxesBookingEntityLambdaQueryWrapper = new LambdaQueryWrapper<>();
boxesBookingEntityLambdaQueryWrapper.eq(MeetingBookingEntity::getProcessInstanceId, processInstanceId);
MeetingBookingEntity one = meetingBookingService.getOne(boxesBookingEntityLambdaQueryWrapper);
if (null== one){
return R.failed("流程不存在");
}
CheckStatusAndReasonVo checkReason = GetCheckReasonUtils.getCheckReason(processNodeRecordParamDto);
meetingBookingEntity.setCheckStatus(checkReason.getCheckStatus());
meetingBookingEntity.setCheckReason(checkReason.getCheckReason());
meetingBookingService.update(meetingBookingEntity,boxesBookingEntityLambdaQueryWrapper);
Long id = one.getId();
meetingBookingEntity.setId(id);
meetingBookingService.updateById(meetingBookingEntity);
//发送通知短信,告知会议打扫
meetingBookingService.noticeVisitDesk(one);
//告知预约人
meetingBookingService.noticeVisitor(one);
return R.ok();
}
@@ -143,4 +143,16 @@ public class MeetingBookingEntity extends Model<MeetingBookingEntity> {
@Schema(description = "审批原因")
private String checkReason;
/**
* 是否需要会务服务(0=不需要 1=需要)
*/
@Schema(description = "是否需要会务服务(0=不需要 1=需要)")
private String needService;
/**
* 桌子摆放形态
*/
@Schema(description = "桌子摆放形态")
private String deskLayout;
}
@@ -0,0 +1,133 @@
package com.bsd.pjcloud.integrated.entity;
import com.baomidou.mybatisplus.annotation.*;
import com.baomidou.mybatisplus.extension.activerecord.Model;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import com.bsd.pjcloud.common.core.util.TenantTable;
import java.time.LocalDateTime;
/**
* 摆放形式
*
* @author zjf
* @date 2025-09-10 12:56:45
*/
@Data
@TenantTable
@TableName("meeting_desk_layout")
@EqualsAndHashCode(callSuper = true)
@Schema(description = "摆放形式")
public class MeetingDeskLayoutEntity extends Model<MeetingDeskLayoutEntity> {
/**
* 编号
*/
@TableId(type = IdType.ASSIGN_ID)
@Schema(description="编号")
private Long id;
/**
* 摆放形式名称
*/
@Schema(description="摆放形式名称")
private String layoutName;
/**
* 摆放形式编码
*/
@Schema(description="摆放形式编码")
private String layoutCode;
/**
* 摆放形式描述
*/
@Schema(description="摆放形式描述")
private String description;
/**
* 最小容纳人数
*/
@Schema(description="最小容纳人数")
private Integer capacityMin;
/**
* 最大容纳人数
*/
@Schema(description="最大容纳人数")
private Integer capacityMax;
/**
* 摆放形式示意图
*/
@Schema(description="摆放形式示意图")
private String layoutImage;
/**
* 布置所需时间(分钟)
*/
@Schema(description="布置所需时间(分钟)")
private Integer setupTime;
/**
* 是否可用(0=可用 1=不可用)
*/
@Schema(description="是否可用(0=可用 1=不可用)")
private String isAvailable;
/**
* 排序
*/
@Schema(description="排序")
private Integer sortOrder;
/**
* 备注
*/
@Schema(description="备注")
private String remark;
/**
* 创建人
*/
@TableField(fill = FieldFill.INSERT)
@Schema(description="创建人")
private String createBy;
/**
* 创建时间
*/
@TableField(fill = FieldFill.INSERT)
@Schema(description="创建时间")
private LocalDateTime createTime;
/**
* 修改人
*/
@TableField(fill = FieldFill.INSERT_UPDATE)
@Schema(description="修改人")
private String updateBy;
/**
* 修改时间
*/
@TableField(fill = FieldFill.INSERT_UPDATE)
@Schema(description="修改时间")
private LocalDateTime updateTime;
/**
* 删除标记
*/
@TableLogic
@TableField(fill = FieldFill.INSERT)
@Schema(description="删除标记")
private String delFlag;
/**
* 租户ID
*/
@Schema(description="租户ID")
private Long tenantId;
}
@@ -16,7 +16,7 @@ import java.time.LocalDateTime;
*/
@Data
@TenantTable
@TableName("meeting__room_equipment")
@TableName("meeting_room_equipment")
@EqualsAndHashCode(callSuper = true)
@Schema(description = "设备会议室关联表")
public class MeetingRoomEquipmentEntity extends Model<MeetingRoomEquipmentEntity> {
@@ -0,0 +1,11 @@
package com.bsd.pjcloud.integrated.mapper;
import com.bsd.pjcloud.common.data.datascope.PjcloudBaseMapper;
import com.bsd.pjcloud.integrated.entity.MeetingDeskLayoutEntity;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface MeetingDeskLayoutMapper extends PjcloudBaseMapper<MeetingDeskLayoutEntity> {
}
@@ -27,8 +27,14 @@ public interface MeetingBookingService extends IService<MeetingBookingEntity> {
/**
* 通知阿姨会议前整理会议室
* @param bookingId
* @param
* @return
*/
void noticeVisitDesk(Long bookingId);
void noticeVisitDesk(MeetingBookingEntity meetingBookingEntity);
/**
* 通知会议预约人
* @param
*/
void noticeVisitor(MeetingBookingEntity meetingBookingEntity);
}
@@ -0,0 +1,8 @@
package com.bsd.pjcloud.integrated.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.bsd.pjcloud.integrated.entity.MeetingDeskLayoutEntity;
public interface MeetingDeskLayoutService extends IService<MeetingDeskLayoutEntity> {
}
@@ -9,6 +9,7 @@ import com.bsd.pjcloud.integrated.dto.visitor.app.VisitorsReceiveAppDTO;
import com.bsd.pjcloud.integrated.dto.visitor.app.VisitorsYYAppDTO;
import com.bsd.pjcloud.integrated.entity.VisitorsEntity;
import com.bsd.pjcloud.integrated.vo.visitor.app.VisitorsAppVo;
import com.bsd.pjcloud.integrated.vo.visitor.app.VisitorsAppYYDetailVo;
import java.util.List;
@@ -86,4 +87,12 @@ public interface VisitorsService extends IService<VisitorsEntity> {
Object receiveYY(VisitorsReceiveAppDTO visitorsReceiveAppDTO);
/**
* 获取邀约详情
* @param token
* @return
*/
VisitorsAppYYDetailVo getVisitorsYYDetail(String token);
}
@@ -66,41 +66,52 @@ public class MeetingBookingServiceImpl extends ServiceImpl<MeetingBookingMapper,
return updated;
}
/**
* 通知整理会议室
* @param
*/
@Override
public void noticeVisitDesk(Long bookingId) {
Long currentUserId = SecurityUtils.getUser().getId();
public void noticeVisitDesk(MeetingBookingEntity meetingBookingEntity) {
// Long currentUserId = SecurityUtils.getUser().getId();
//更新会议预约结束状态
MeetingBookingEntity booking = new MeetingBookingEntity();
if (!booking.getApplicantId().equals(currentUserId)){
throw new RuntimeException("无权限操作");
}
// MeetingBookingEntity booking = new MeetingBookingEntity();
// if (!booking.getApplicantId().equals(currentUserId)){
// throw new RuntimeException("无权限操作");
// }
//查询会议室阿姨信息
MeetingRoomEntity meetingRoomEntity = meetingRoomMapper.selectById(booking.getRoomId());
MeetingRoomEntity meetingRoomEntity = meetingRoomMapper.selectById(meetingBookingEntity.getRoomId());
Long cleanUserId = meetingRoomEntity.getCleanUserId();
if (cleanUserId != null){
CleanUserEntity cleanUserEntity = cleanUserMapper.selectById(cleanUserId);
//发送短信通知阿姨
Map<String, String> stringStringHashMap = new HashMap<>();
stringStringHashMap.put("meetingName", meetingRoomEntity.getRoomName()+",地址:"+meetingRoomEntity.getLocation());
stringStringHashMap.put("meetingDate", booking.getBookingDate().format(DateTimeFormatter.ofPattern("yyyy-MM-dd")));
stringStringHashMap.put("deskType", "1");
String format = meetingBookingEntity.getBookingDate().format(DateTimeFormatter.ofPattern("yyyy-MM-dd"));
stringStringHashMap.put("meetingDate", format);
stringStringHashMap.put("deskType", meetingBookingEntity.getDeskLayout()==null?"无要求":meetingBookingEntity.getDeskLayout());
reminderThirdPartySmsService.sendParamsMsg(cleanUserEntity.getPhone(), "30758", stringStringHashMap, WebUtils.getIP());
}else {
System.out.println("没有设置阿姨,无法发送短信");
//没有设置阿姨,无法发送短信
}
}
private void sendConfirmEndMeetingSms(VisitorUserEntity visitorUserEntity, VisitorsEntity dataVisitors) {
Map<String, String> stringStringHashMap = new HashMap<>();
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
stringStringHashMap.put("createTime", visitorUserEntity.getCreateTime().format(formatter));
stringStringHashMap.put("visitorName", visitorUserEntity.getRealName()==null?visitorUserEntity.getPhone():visitorUserEntity.getRealName());
stringStringHashMap.put("visitorPhone", visitorUserEntity.getPhone());
stringStringHashMap.put("visitorTime", dataVisitors.getVStartDate() + "至" + dataVisitors.getVEndDate());
stringStringHashMap.put("visitorLocal", dataVisitors.getDeptName());
@Override
public void noticeVisitor(MeetingBookingEntity meetingBookingEntity) {
reminderThirdPartySmsService.sendParamsMsg(visitorUserEntity.getPhone(), "20658", stringStringHashMap, WebUtils.getIP());
}
// private void sendConfirmEndMeetingSms(VisitorUserEntity visitorUserEntity, VisitorsEntity dataVisitors) {
// Map<String, String> stringStringHashMap = new HashMap<>();
// DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
// stringStringHashMap.put("createTime", visitorUserEntity.getCreateTime().format(formatter));
// stringStringHashMap.put("visitorName", visitorUserEntity.getRealName()==null?visitorUserEntity.getPhone():visitorUserEntity.getRealName());
// stringStringHashMap.put("visitorPhone", visitorUserEntity.getPhone());
// stringStringHashMap.put("visitorTime", dataVisitors.getVStartDate() + "至" + dataVisitors.getVEndDate());
// stringStringHashMap.put("visitorLocal", dataVisitors.getDeptName());
//
//
// reminderThirdPartySmsService.sendParamsMsg(visitorUserEntity.getPhone(), "30757", stringStringHashMap, WebUtils.getIP());
// }
}
@@ -0,0 +1,16 @@
package com.bsd.pjcloud.integrated.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.bsd.pjcloud.integrated.entity.MeetingDeskLayoutEntity;
import com.bsd.pjcloud.integrated.mapper.MeetingDeskLayoutMapper;
import com.bsd.pjcloud.integrated.service.MeetingDeskLayoutService;
import org.springframework.stereotype.Service;
/**
* 摆放形式
*
* @author zjf
* @date 2025-09-10 12:56:45
*/
@Service
public class MeetingDeskLayoutServiceImpl extends ServiceImpl<MeetingDeskLayoutMapper, MeetingDeskLayoutEntity> implements MeetingDeskLayoutService {
}
@@ -1,6 +1,7 @@
package com.bsd.pjcloud.integrated.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
@@ -36,10 +37,7 @@ import com.bsd.pjcloud.integrated.mapper.VisitorsMapper;
import com.bsd.pjcloud.integrated.mapper.VisitorsPeopleMapper;
import com.bsd.pjcloud.integrated.service.VisitorUserService;
import com.bsd.pjcloud.integrated.service.VisitorsService;
import com.bsd.pjcloud.integrated.vo.visitor.app.VisitorsAppPageVo;
import com.bsd.pjcloud.integrated.vo.visitor.app.VisitorsAppVo;
import com.bsd.pjcloud.integrated.vo.visitor.app.VisitorsCarAppVo;
import com.bsd.pjcloud.integrated.vo.visitor.app.VisitorsPeopleAppVo;
import com.bsd.pjcloud.integrated.vo.visitor.app.*;
import com.bsd.pjcloud.thirdparty.api.feign.RemoterThirdPartySmsService;
import com.bsd.pjcloud.thirdparty.api.feign.RemoterVisitorService;
import com.bsd.pjcloud.thirdparty.dto.visitor.AppointmentInfo;
@@ -285,7 +283,7 @@ public class VisitorsServiceImpl extends ServiceImpl<VisitorsMapper, VisitorsEnt
bean.setDeptId(deptId);
R<SysDept> deptByDeptId = remoteDeptService.getDeptByDeptId(deptId);
bean.setDeptName(deptByDeptId.getData().getName());
bean.setUserName(SecurityUtils.getUser().getName());
String uniqueToken = UUID.randomUUID().toString();
String token = uniqueToken.replace("-", "");
bean.setToken(token);
@@ -511,5 +509,41 @@ public class VisitorsServiceImpl extends ServiceImpl<VisitorsMapper, VisitorsEnt
}
@Override
public VisitorsAppYYDetailVo getVisitorsYYDetail(String token) {
VisitorsEntity visitorsEntity = baseMapper.selectOne(
new QueryWrapper<VisitorsEntity>()
.eq("token", token)
.eq("type", VisitorTypeEnum.ZDYQ.getType()));
if(visitorsEntity == null){
throw new ServiceException("数据不存在");
}
// 转换为VO对象
VisitorsAppYYDetailVo result = BeanUtils.toBean(visitorsEntity, VisitorsAppYYDetailVo.class);
// 查询车辆信息
List<VisitorsCarEntity> visitorsCarEntities = visitorsCarMapper.selectList(
new LambdaQueryWrapper<VisitorsCarEntity>().eq(VisitorsCarEntity::getVisitorsId, visitorsEntity.getId())
);
if (!CollectionUtils.isEmpty(visitorsCarEntities)) {
List<VisitorsCarAppDTO> carList = BeanUtils.toBean(visitorsCarEntities, VisitorsCarAppDTO.class);
result.setCarList(carList);
}
// 查询随行人员信息
List<VisitorsPeopleEntity> visitorsPeopleEntities = visitorsPeopleMapper.selectList(
new LambdaQueryWrapper<VisitorsPeopleEntity>().eq(VisitorsPeopleEntity::getVisitorsId, visitorsEntity.getId())
);
if (!CollectionUtils.isEmpty(visitorsPeopleEntities)) {
List<VisitorsPeopleAppDTO> otherPeople = BeanUtils.toBean(visitorsPeopleEntities, VisitorsPeopleAppDTO.class);
result.setOtherPeople(otherPeople);
}
//设置邀约者的信息
// 设置邀约ID
// result.setYyId(visitorsEntity.getId());
return result;
}
}
@@ -0,0 +1,31 @@
package com.bsd.pjcloud.integrated.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serializable;
/**
* 反馈类型VO
*
* @author zjf
* @date 2025-01-27
*/
@Data
@Schema(description = "反馈类型VO")
public class FeedbackTypeVO implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 字典项ID
*/
@Schema(description = "字典项ID")
private Long id;
/**
* 标签名称
*/
@Schema(description = "标签名称")
private String label;
}
@@ -0,0 +1,31 @@
package com.bsd.pjcloud.integrated.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serializable;
/**
* 维修类型VO
*
* @author zjf
* @date 2025-01-27
*/
@Data
@Schema(description = "维修类型VO")
public class MaintenanceTypeVO implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 字典项ID
*/
@Schema(description = "字典项ID")
private Long id;
/**
* 标签名称
*/
@Schema(description = "标签名称")
private String label;
}
@@ -0,0 +1,58 @@
package com.bsd.pjcloud.integrated.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serializable;
/**
* 移动端摆放形式选择VO
*
* @author zjf
* @date 2025-01-27
*/
@Data
@Schema(description = "移动端摆放形式选择VO")
public class MeetingDeskLayoutAppVO implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 编号
*/
@Schema(description = "编号")
private Long id;
/**
* 摆放形式名称
*/
@Schema(description = "摆放形式名称")
private String layoutName;
/**
* 摆放形式示意图
*/
@Schema(description = "摆放形式示意图")
private String layoutImage;
/**
* 最小容纳人数
*/
@Schema(description = "最小容纳人数")
private Integer capacityMin;
/**
* 最大容纳人数
*/
@Schema(description = "最大容纳人数")
private Integer capacityMax;
/**
* 摆放形式描述
*/
@Schema(description = "摆放形式描述")
private String description;
}
@@ -24,6 +24,8 @@
<result property="processInstanceId" column="process_instance_id"/>
<result property="checkReason" column="check_reason"/>
<result property="isConfirmEnd" column="is_confirm_end"/>
<result property="needService" column="need_service"/>
<result property="deskLayout" column="desk_layout"/>
</resultMap>
<select id="findByRoomIdAndDate" resultType="com.bsd.pjcloud.integrated.entity.MeetingBookingEntity">
SELECT * FROM meeting_booking WHERE room_id = #{roomId} AND start_time >= #{date} AND start_time &lt;= #{date}
@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.bsd.pjcloud.integrated.mapper.MeetingDeskLayoutMapper">
<resultMap id="meetingDeskLayoutMap" type="com.bsd.pjcloud.integrated.entity.MeetingDeskLayoutEntity">
<id property="id" column="id"/>
<result property="layoutName" column="layout_name"/>
<result property="layoutCode" column="layout_code"/>
<result property="description" column="description"/>
<result property="capacityMin" column="capacity_min"/>
<result property="capacityMax" column="capacity_max"/>
<result property="layoutImage" column="layout_image"/>
<result property="setupTime" column="setup_time"/>
<result property="isAvailable" column="is_available"/>
<result property="sortOrder" column="sort_order"/>
<result property="remark" column="remark"/>
<result property="createBy" column="create_by"/>
<result property="createTime" column="create_time"/>
<result property="updateBy" column="update_by"/>
<result property="updateTime" column="update_time"/>
<result property="delFlag" column="del_flag"/>
<result property="tenantId" column="tenant_id"/>
</resultMap>
</mapper>
@@ -106,6 +106,10 @@ public class SmErpInventoryRecordsExcelDto {
@ExcelProperty("货商名称")
private String supplierName;
@Schema(description="零售价(元)")
@ExcelProperty("零售价(元)")
private Integer retailPrice;
@Schema(description="单价")
@ExcelProperty("单价(元)")
private Integer unitPrice;
@@ -71,7 +71,7 @@ public class SmErpInventoryRecordsSummaryExcelDto {
*/
@Schema(description="入库总金额")
@ExcelProperty("总金额(元)")
private Integer totalAmount;
private Double totalAmount;
/**
* 创建人
@@ -99,8 +99,12 @@ public class SmErpInventoryRecordsSummaryExcelDto {
@ExcelProperty("货商名称")
private String supplierName;
@Schema(description="汇总单价(元)")
@ExcelProperty("汇总单价(元)")
@Schema(description="零售价(元)")
@ExcelProperty("零售价(元)")
private Double retailPrice;
@Schema(description="汇总成本单价(元)")
@ExcelProperty("汇总成本单价(元)")
private Double averageUnitPrice;
@Schema(description="期初库存")
@@ -58,7 +58,7 @@ public class MallCategoryEntity extends Model<MallCategoryEntity> {
* 状态(0正常,1下架)
*/
@Schema(description="状态(0正常,1下架)")
private Integer status;
private String status;
/**
* 分类图片
@@ -113,4 +113,10 @@ public class MallCategoryEntity extends Model<MallCategoryEntity> {
*/
@Schema(description="租户编号")
private Long tenantId;
/**
* 商户ID
*/
@Schema(description="商户ID")
private Long merchantId;
}
@@ -89,13 +89,13 @@ public class ShopSkuEntity extends Model<ShopSkuEntity> {
* 进价/单位分
*/
@Schema(description="进价/单位分")
@JsonDeserialize(using = MoneyDeserializer.class) // 元 -> 分
// @JsonDeserialize(using = MoneyDeserializer.class) // 元 -> 分
private Integer purchasePrice;
/**
* 零售价/单位分
*/
@Schema(description="零售价/单位分")
@JsonDeserialize(using = MoneyDeserializer.class) // 元 -> 分
// @JsonDeserialize(using = MoneyDeserializer.class) // 元 -> 分
private Integer retailPrice;
/**
* spuID
@@ -106,7 +106,7 @@ public class ShopSkuEntity extends Model<ShopSkuEntity> {
* 促销价
*/
@Schema(description="促销价")
@JsonDeserialize(using = MoneyDeserializer.class) // 元 -> 分
// @JsonDeserialize(using = MoneyDeserializer.class) // 元 -> 分
private Integer promotionPrice;
/**
* 库存
@@ -0,0 +1,348 @@
# 商品新增接口使用说明
## 商品创建接口
### 接口地址
```
POST /mallProduct
```
### 请求参数
支持两种请求体类型:
#### 1. 包含SKU的创建(使用MallProductCreateDTO)
##### 多SKU模式示例(priceType = 1)
```json
{
"merchantId": 1,
"categoryId": 100,
"title": "苹果手机",
"subTitle": "最新款智能手机",
"brand": "Apple",
"coverImage": "https://example.com/cover.jpg",
"carouselImages": "https://example.com/img1.jpg,https://example.com/img2.jpg",
"priceType": 1,
"originalPrice": 9999.00,
"unit": "台",
"status": 1,
"saleStartTime": "2025-01-27T00:00:00",
"saleEndTime": "2025-12-31T23:59:59",
"description": "这是一款高性能的智能手机",
"deptId": 1,
"skuList": [
{
"skuCode": "IPHONE15-128-BLACK",
"skuName": "iPhone 15 128GB 黑色",
"price": 5999.00,
"costPrice": 4500.00,
"stock": 100,
"barcode": "1234567890123",
"weightG": 171,
"volumeCm3": 200,
"status": 1,
"deptId": 1
},
{
"skuCode": "IPHONE15-256-BLACK",
"skuName": "iPhone 15 256GB 黑色",
"price": 6999.00,
"costPrice": 5200.00,
"stock": 50,
"barcode": "1234567890124",
"weightG": 171,
"volumeCm3": 200,
"status": 1,
"deptId": 1
}
]
}
```
##### 单价SPU模式示例(priceType = 2)
```json
{
"merchantId": 1,
"categoryId": 100,
"title": "苹果手机",
"subTitle": "最新款智能手机",
"brand": "Apple",
"coverImage": "https://example.com/cover.jpg",
"carouselImages": "https://example.com/img1.jpg,https://example.com/img2.jpg",
"priceType": 2,
"spuPrice": 5999.00,
"originalPrice": 9999.00,
"unit": "台",
"status": 1,
"saleStartTime": "2025-01-27T00:00:00",
"saleEndTime": "2025-12-31T23:59:59",
"description": "这是一款高性能的智能手机",
"deptId": 1
}
```
#### 2. 普通SPU创建(使用MallProductEntity)
```json
{
"merchantId": 1,
"categoryId": 100,
"title": "苹果手机",
"subTitle": "最新款智能手机",
"brand": "Apple",
"coverImage": "https://example.com/cover.jpg",
"carouselImages": "https://example.com/img1.jpg,https://example.com/img2.jpg",
"priceType": 2,
"spuPrice": 5999.00,
"originalPrice": 9999.00,
"unit": "台",
"status": 1,
"saleStartTime": "2025-01-27T00:00:00",
"saleEndTime": "2025-12-31T23:59:59",
"description": "这是一款高性能的智能手机"
}
```
### 响应示例
#### 成功响应
```json
{
"code": 0,
"msg": "商品创建成功",
"data": null
}
```
#### 失败响应
```json
{
"code": 1,
"msg": "商品创建失败:SKU编码不能为空",
"data": null
}
```
### 字段说明
#### SPU字段
- `merchantId`: 商户ID(必填)
- `categoryId`: 类目ID(必填)
- `title`: 商品标题(必填)
- `subTitle`: 副标题
- `brand`: 品牌
- `coverImage`: 主图
- `carouselImages`: SPU轮播图URL(逗号分隔)
- `priceType`: 定价方式(1=多SKU,2=单价SPU)(必填)
- `spuPrice`: SPU售价(当priceType=2时有效)
- `originalPrice`: 划线价
- `unit`: 计量单位
- `status`: 状态(1=上架,0=下架)
- `saleStartTime`: 开售时间
- `saleEndTime`: 停售时间
- `description`: 详情描述
- `deptId`: 部门ID
#### SKU字段(当priceType=1时必填)
- `skuCode`: SKU编码(必填)
- `skuName`: SKU名称(必填)
- `price`: 售价(必填)
- `costPrice`: 成本价
- `stock`: 库存(必填)
- `barcode`: 条码
- `weightG`: 重量(克)
- `volumeCm3`: 体积(立方厘米)
- `status`: 状态(1=启用,0=停用)
- `deptId`: 部门ID
### 业务逻辑说明
1. **多SKU模式(priceType = 1)**:
- 必须提供skuList
- 系统会自动计算总库存(所有SKU库存之和)
- 每个SKU都会保存到数据库
2. **单价SPU模式(priceType = 2)**:
- 不需要提供skuList
- 使用spuPrice作为商品价格
- 总库存设置为0(可根据业务需求调整)
3. **事务处理**:
- 整个创建过程在一个事务中执行
- 如果任何步骤失败,所有操作都会回滚
4. **数据验证**:
- 使用JSR-303验证注解进行参数校验
- 必填字段验证
- 业务逻辑验证
### 权限要求
需要 `supermarket_mallProduct_add` 权限
## 商品编辑接口
### 接口地址
```
PUT /mallProduct
```
### 请求参数
支持两种请求体类型:
#### 1. 包含SKU的编辑(使用MallProductCreateDTO)
```json
{
"id": 1,
"merchantId": 1,
"categoryId": 100,
"title": "苹果手机(已更新)",
"subTitle": "最新款智能手机",
"brand": "Apple",
"coverImage": "https://example.com/cover.jpg",
"carouselImages": "https://example.com/img1.jpg,https://example.com/img2.jpg",
"priceType": 1,
"originalPrice": 9999.00,
"unit": "台",
"status": 1,
"saleStartTime": "2025-01-27T00:00:00",
"saleEndTime": "2025-12-31T23:59:59",
"description": "这是一款高性能的智能手机",
"skuList": [
{
"skuCode": "IPHONE15-128-BLACK-UPDATED",
"skuName": "iPhone 15 128GB 黑色(已更新)",
"price": 5999.00,
"costPrice": 4500.00,
"stock": 100,
"barcode": "1234567890123",
"weightG": 171,
"volumeCm3": 200,
"status": 1
}
]
}
```
#### 2. 普通SPU编辑(使用MallProductEntity)
```json
{
"id": 1,
"merchantId": 1,
"categoryId": 100,
"title": "苹果手机(已更新)",
"subTitle": "最新款智能手机",
"brand": "Apple",
"coverImage": "https://example.com/cover.jpg",
"carouselImages": "https://example.com/img1.jpg,https://example.com/img2.jpg",
"priceType": 2,
"spuPrice": 5999.00,
"originalPrice": 9999.00,
"unit": "台",
"status": 1,
"saleStartTime": "2025-01-27T00:00:00",
"saleEndTime": "2025-12-31T23:59:59",
"description": "这是一款高性能的智能手机"
}
```
### 响应示例
```json
{
"code": 0,
"msg": "商品更新成功",
"data": null
}
```
### 权限要求
需要 `supermarket_mallProduct_edit` 权限
## 商品详情查询接口
### 接口地址
```
GET /mallProduct/{id}
```
### 请求参数
- `id`: 商品ID(路径参数)
- `withSkus`: 是否包含SKU信息(查询参数,可选,默认false)
### 使用示例
#### 1. 查询基本信息(不包含SKU)
```
GET /mallProduct/1
```
#### 2. 查询完整信息(包含SKU)
```
GET /mallProduct/1?withSkus=true
```
### 响应示例
#### 基本信息响应
```json
{
"code": 0,
"msg": "操作成功",
"data": {
"id": 1,
"merchantId": 1,
"categoryId": 100,
"title": "苹果手机",
"subTitle": "最新款智能手机",
"brand": "Apple",
"coverImage": "https://example.com/cover.jpg",
"carouselImages": "https://example.com/img1.jpg,https://example.com/img2.jpg",
"priceType": 1,
"originalPrice": 9999.00,
"unit": "台",
"status": 1,
"saleStartTime": "2025-01-27T00:00:00",
"saleEndTime": "2025-12-31T23:59:59",
"description": "这是一款高性能的智能手机"
}
}
```
#### 完整信息响应(withSkus=true)
```json
{
"code": 0,
"msg": "操作成功",
"data": {
"id": 1,
"merchantId": 1,
"categoryId": 100,
"title": "苹果手机",
"subTitle": "最新款智能手机",
"brand": "Apple",
"coverImage": "https://example.com/cover.jpg",
"carouselImages": "https://example.com/img1.jpg,https://example.com/img2.jpg",
"priceType": 1,
"originalPrice": 9999.00,
"unit": "台",
"status": 1,
"saleStartTime": "2025-01-27T00:00:00",
"saleEndTime": "2025-12-31T23:59:59",
"description": "这是一款高性能的智能手机",
"skuList": [
{
"skuCode": "IPHONE15-128-BLACK",
"skuName": "iPhone 15 128GB 黑色",
"price": 5999.00,
"costPrice": 4500.00,
"stock": 100,
"barcode": "1234567890123",
"weightG": 171,
"volumeCm3": 200,
"status": 1
}
]
}
}
```
### 权限要求
需要 `supermarket_mallProduct_view` 权限
@@ -0,0 +1,308 @@
# 下拉选择接口使用说明
## 概述
为了方便后台管理系统使用下拉选择组件,我们提供了一系列统一的下拉选择接口。这些接口返回标准化的选项数据格式,便于前端组件使用。
## 统一返回格式
所有下拉选择接口都返回以下格式的数据:
```json
{
"code": 0,
"msg": "操作成功",
"data": [
{
"value": 1,
"label": "选项名称",
"disabled": false,
"extra": "扩展信息"
}
]
}
```
### 字段说明
- `value`: 选项值,可以是数字、字符串等类型
- `label`: 选项显示名称
- `disabled`: 是否禁用,默认为false
- `extra`: 扩展信息,可以包含额外的数据
## 接口列表
### 1. 商户下拉选择
#### 接口地址
```
GET /selectOption/merchant?keyWord=关键字
```
#### 请求参数
- `keyWord` (可选): 关键字,支持商户名称和手机号模糊查询
#### 权限要求
需要 `supermarket_mallMerchant_view` 权限
#### 使用示例
```
# 获取所有商户
GET /selectOption/merchant
# 按商户名称搜索
GET /selectOption/merchant?keyWord=农场
# 按手机号搜索
GET /selectOption/merchant?keyWord=138
# 按关键字搜索(商户名称或手机号)
GET /selectOption/merchant?keyWord=张三
```
#### 响应示例
```json
{
"code": 0,
"msg": "操作成功",
"data": [
{
"value": 1,
"label": "张三农场",
"disabled": false,
"extra": "13800138000"
},
{
"value": 2,
"label": "李四果园",
"disabled": false,
"extra": "13900139000"
}
]
}
```
### 2. 分类下拉选择
#### 接口地址
```
GET /selectOption/category
```
#### 权限要求
需要 `supermarket_mallCategory_view` 权限
#### 响应示例
```json
{
"code": 0,
"msg": "操作成功",
"data": [
{
"value": 1,
"label": "水果",
"disabled": false,
"extra": 0
},
{
"value": 2,
"label": "蔬菜",
"disabled": false,
"extra": 0
},
{
"value": 3,
"label": "苹果",
"disabled": false,
"extra": 1
}
]
}
```
### 3. 分类下拉选择(树形结构)
#### 接口地址
```
GET /selectOption/category/tree
```
#### 权限要求
需要 `supermarket_mallCategory_view` 权限
#### 响应示例
```json
{
"code": 0,
"msg": "操作成功",
"data": [
{
"value": 1,
"label": "水果",
"disabled": false,
"extra": 0
},
{
"value": 3,
"label": " └─ 苹果",
"disabled": false,
"extra": 1
},
{
"value": 2,
"label": "蔬菜",
"disabled": false,
"extra": 0
}
]
}
```
## 传统接口
除了统一的下拉选择接口,原有的控制器也提供了下拉选择接口:
### 商户下拉选择(传统接口)
```
GET /mallMerchant/selectList
```
### 分类下拉选择(传统接口)
```
GET /mallCategory/selectList
```
## 前端使用示例
### Vue.js + Element Plus
```javascript
// 获取商户下拉选择
async getMerchantOptions(keyword = '') {
try {
const params = keyword ? { keyWord: keyword } : {};
const response = await this.$http.get('/selectOption/merchant', { params });
this.merchantOptions = response.data.data.map(item => ({
value: item.value,
label: item.label,
disabled: item.disabled
}));
} catch (error) {
console.error('获取商户选项失败:', error);
}
}
// 搜索商户
async searchMerchants(keyword) {
await this.getMerchantOptions(keyword);
}
// 获取分类下拉选择
async getCategoryOptions() {
try {
const response = await this.$http.get('/selectOption/category');
this.categoryOptions = response.data.data.map(item => ({
value: item.value,
label: item.label,
disabled: item.disabled
}));
} catch (error) {
console.error('获取分类选项失败:', error);
}
}
```
### HTML模板
```html
<template>
<div>
<!-- 商户选择(支持搜索) -->
<el-select
v-model="form.merchantId"
placeholder="请选择商户"
filterable
remote
:remote-method="searchMerchants"
:loading="merchantLoading">
<el-option
v-for="item in merchantOptions"
:key="item.value"
:label="item.label"
:value="item.value"
:disabled="item.disabled">
<span style="float: left">{{ item.label }}</span>
<span style="float: right; color: #8492a6; font-size: 13px">{{ item.extra }}</span>
</el-option>
</el-select>
<!-- 分类选择 -->
<el-select v-model="form.categoryId" placeholder="请选择分类">
<el-option
v-for="item in categoryOptions"
:key="item.value"
:label="item.label"
:value="item.value"
:disabled="item.disabled">
</el-option>
</el-select>
</div>
</template>
<script>
export default {
data() {
return {
form: {
merchantId: null,
categoryId: null
},
merchantOptions: [],
categoryOptions: [],
merchantLoading: false
}
},
mounted() {
this.getMerchantOptions();
this.getCategoryOptions();
},
methods: {
// 搜索商户
async searchMerchants(keyword) {
this.merchantLoading = true;
try {
await this.getMerchantOptions(keyword);
} finally {
this.merchantLoading = false;
}
}
}
}
</script>
```
## 扩展说明
### 添加新的下拉选择类型
如果需要添加新的下拉选择类型,可以:
1. 在 `SelectOptionService` 中添加新的方法
2. 在 `SelectOptionController` 中添加对应的接口
3. 按照统一的格式返回数据
### 自定义扩展信息
`extra` 字段可以包含任何额外的信息,例如:
- 联系电话
- 父级ID
- 状态信息
- 其他业务相关数据
前端可以根据需要解析和使用这些扩展信息。
## 注意事项
1. 所有接口都需要相应的权限验证
2. 只返回启用状态的数据
3. 数据按照指定的排序规则返回
4. 建议前端对接口返回的数据进行缓存,避免频繁请求
@@ -0,0 +1,98 @@
package com.bsd.pjcloud.supermarket.constant;
/**
* 商城相关常量
*
* @author pjcloud
* @date 2025-01-27
*/
public interface MallConstants {
/**
* 状态相关常量
*/
interface Status {
/**
* 启用状态
*/
String ENABLED = "0";
/**
* 禁用状态
*/
String DISABLED = "1";
/**
* 上架状态
*/
String ON_SALE = "1";
/**
* 下架状态
*/
String OFF_SALE = "0";
}
/**
* 角色相关常量
*/
interface RoleCode {
/**
* 商户用户角色
*/
String MERCHANT_USER = "MALL_MCH";
/**
* 超市管理员角色
*/
String SUPERMARKET_ADMIN = "SUPERMARKET_ADMIN";
}
/**
* 商品相关常量
*/
interface Product {
/**
* 定价方式 - 多SKU
*/
Integer PRICE_TYPE_MULTI_SKU = 1;
/**
* 定价方式 - 单价SPU
*/
Integer PRICE_TYPE_SINGLE_SPU = 2;
}
/**
* 商户相关常量
*/
interface Merchant {
/**
* 商户状态 - 正常
*/
String STATUS_NORMAL = "1";
/**
* 商户状态 - 停用
*/
String STATUS_DISABLED = "0";
}
/**
* 删除标记
*/
interface DelFlag {
/**
* 正常
*/
String NORMAL = "0";
/**
* 删除
*/
String DELETED = "1";
}
}
@@ -0,0 +1,688 @@
package com.bsd.pjcloud.supermarket.controller;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import com.alibaba.nacos.common.utils.CollectionUtils;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.bsd.pjcloud.admin.api.entity.SysRole;
import com.bsd.pjcloud.admin.api.feign.RemoteRoleService;
import com.bsd.pjcloud.common.core.util.BeanUtils;
import com.bsd.pjcloud.common.core.util.R;
import com.bsd.pjcloud.common.security.util.SecurityUtils;
import com.bsd.pjcloud.supermarket.dto.MallProductCreateDTO;
import com.bsd.pjcloud.supermarket.entity.MallCategoryEntity;
import com.bsd.pjcloud.supermarket.entity.MallMerchantEntity;
import com.bsd.pjcloud.supermarket.entity.MallProductEntity;
import com.bsd.pjcloud.supermarket.entity.MallProductSkuEntity;
import com.bsd.pjcloud.supermarket.exception.BusinessException;
import com.bsd.pjcloud.supermarket.service.MallCategoryService;
import com.bsd.pjcloud.supermarket.service.MallMerchantService;
import com.bsd.pjcloud.supermarket.service.MallMerchantUserService;
import com.bsd.pjcloud.supermarket.service.MallProductService;
import com.bsd.pjcloud.supermarket.service.MallProductSkuService;
import com.bsd.pjcloud.supermarket.vo.mall.MallCategoryTreeVo;
import com.bsd.pjcloud.supermarket.vo.app.MallProductListAppVo;
import com.bsd.pjcloud.supermarket.vo.app.MallProductDetailAppVo;
import com.bsd.pjcloud.supermarket.vo.app.MallProductSkuAppVo;
import com.bsd.pjcloud.supermarket.vo.app.MallMerchantListAppVo;
import com.bsd.pjcloud.supermarket.dto.SkuStockChangeDTO;
import com.bsd.pjcloud.supermarket.constant.MallConstants;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springdoc.core.annotations.ParameterObject;
import org.springframework.web.bind.annotation.*;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
/**
* 移动端商城接口
*
* @author zjf
* @date 2025-01-27
*/
@RestController
@RequestMapping("/app/mall")
@Tag(name = "移动端商城", description = "移动端商城接口")
@RequiredArgsConstructor
public class MallAppController {
private final MallCategoryService mallCategoryService;
private final MallMerchantService mallMerchantService;
private final MallMerchantUserService mallMerchantUserService;
private final MallProductService mallProductService;
private final MallProductSkuService mallProductSkuService;
private final RemoteRoleService remoteRoleService;
// ==================== 分类相关接口 ====================
/**
* 获取分类树结构
*/
@GetMapping("/category/tree")
@Operation(summary = "获取分类树", description = "获取所有分类的树结构")
public R<List<MallCategoryTreeVo>> getCategoryTree(@RequestParam(required = false) Long merchantId) {
LambdaQueryWrapper<MallCategoryEntity> wrapper = Wrappers.lambdaQuery();
wrapper.eq(MallCategoryEntity::getStatus, MallConstants.Status.ENABLED); // 只查启用状态
// 添加商户ID过滤条件
wrapper.eq(merchantId != null, MallCategoryEntity::getMerchantId, merchantId);
List<MallCategoryEntity> all = mallCategoryService.list(wrapper);
List<MallCategoryTreeVo> tree = buildCategoryTree(all, 0L);
return R.ok(tree);
}
/**
* 根据父级ID获取子分类
*/
@GetMapping("/category/listByParent/{parentId}")
@Operation(summary = "根据父级ID获取子分类", description = "根据父级ID获取子分类列表")
public R<List<MallCategoryEntity>> getCategoryListByParent(@PathVariable("parentId") Long parentId, @RequestParam(required = false) Long merchantId) {
LambdaQueryWrapper<MallCategoryEntity> wrapper = Wrappers.lambdaQuery();
wrapper.eq(MallCategoryEntity::getParentId, parentId);
wrapper.eq(MallCategoryEntity::getStatus, MallConstants.Status.ENABLED);
// 添加商户ID过滤条件
wrapper.eq(merchantId != null, MallCategoryEntity::getMerchantId, merchantId);
List<MallCategoryEntity> list = mallCategoryService.list(wrapper);
return R.ok(list);
}
// ==================== 商户相关接口 ====================
/**
* 获取商户列表
*/
@GetMapping("/merchant/list")
@Operation(summary = "获取商户列表", description = "获取所有启用的商户列表,包含特色商品")
public R<List<MallMerchantListAppVo>> getMerchantList(@RequestParam(defaultValue = "3") Integer productLimit) {
// 查询所有启用的商户
List<MallMerchantEntity> merchants = mallMerchantService.list(
Wrappers.<MallMerchantEntity>lambdaQuery()
.eq(MallMerchantEntity::getStatus, MallConstants.Status.ENABLED)
.orderByAsc(MallMerchantEntity::getName)
);
if (merchants.isEmpty()) {
return R.ok(List.of());
}
// 获取商户ID列表
List<Long> merchantIds = merchants.stream()
.map(MallMerchantEntity::getId)
.collect(Collectors.toList());
// 查询每个商户的特色商品(按创建时间倒序,取前N个)
LocalDateTime now = LocalDateTime.now();
List<MallProductEntity> products = mallProductService.list(
Wrappers.<MallProductEntity>lambdaQuery()
.in(MallProductEntity::getMerchantId, merchantIds)
.eq(MallProductEntity::getStatus, MallConstants.Status.ENABLED) // 只查上架商品
.and(wrapper -> wrapper
.isNull(MallProductEntity::getSaleStartTime)
.or()
.le(MallProductEntity::getSaleStartTime, now)
)
.and(wrapper -> wrapper
.isNull(MallProductEntity::getSaleEndTime)
.or()
.ge(MallProductEntity::getSaleEndTime, now)
)
.orderByDesc(MallProductEntity::getCreateTime)
);
// 按商户ID分组商品
Map<Long, List<MallProductEntity>> productMap = products.stream()
.collect(Collectors.groupingBy(MallProductEntity::getMerchantId));
// 获取商品ID列表,查询SKU信息
List<Long> productIds = products.stream()
.map(MallProductEntity::getId)
.collect(Collectors.toList());
List<MallProductSkuEntity> skuList = productIds.isEmpty() ? List.of() :
mallProductSkuService.list(
Wrappers.<MallProductSkuEntity>lambdaQuery()
.in(MallProductSkuEntity::getProductId, productIds)
);
// 按商品ID分组SKU
Map<Long, List<MallProductSkuEntity>> skuMap = skuList.stream()
.collect(Collectors.groupingBy(MallProductSkuEntity::getProductId));
// 转换为VO
List<MallMerchantListAppVo> result = merchants.stream().map(merchant -> {
MallMerchantListAppVo vo = BeanUtils.toBean(merchant, MallMerchantListAppVo.class);
// 获取该商户的商品列表
List<MallProductEntity> merchantProducts = productMap.getOrDefault(merchant.getId(), List.of());
// 设置商品总数
vo.setProductCount(merchantProducts.size());
// 获取特色商品(限制数量)
List<MallProductListAppVo> featuredProducts = merchantProducts.stream()
.limit(productLimit)
.map(product -> {
MallProductListAppVo productVo = BeanUtils.toBean(product, MallProductListAppVo.class);
List<MallProductSkuEntity> skus = skuMap.getOrDefault(product.getId(), List.of());
// 计算最低价格
productVo.setMinPrice(skus.stream()
.map(MallProductSkuEntity::getPrice)
.filter(Objects::nonNull)
.min(Integer::compareTo)
.orElse(0));
// 计算总库存
productVo.setTotalStock(skus.stream()
.map(MallProductSkuEntity::getStock)
.filter(Objects::nonNull)
.mapToDouble(Double::doubleValue)
.sum());
return productVo;
})
.collect(Collectors.toList());
vo.setFeaturedProducts(featuredProducts);
// 设置商户营业时间
if (merchant.getBusinessHours() != null && !merchant.getBusinessHours().trim().isEmpty()) {
vo.setBusinessHours(merchant.getBusinessHours());
} else {
// 默认商户营业时间
vo.setBusinessHours("10:00-22:00");
}
return vo;
}).collect(Collectors.toList());
return R.ok(result);
}
/**
* 获取商户详情
*/
@GetMapping("/merchant/{id}")
@Operation(summary = "获取商户详情", description = "根据ID获取商户详情")
public R<MallMerchantEntity> getMerchantDetail(@PathVariable("id") Long id) {
MallMerchantEntity merchant = mallMerchantService.getById(id);
if (merchant == null) {
return R.failed("商户不存在");
}
return R.ok(merchant);
}
// ==================== 商品相关接口 ====================
/**
* 商品分页查询
*/
@GetMapping("/product/page")
@Operation(summary = "商品分页查询", description = "移动端商品分页查询")
public R<IPage<MallProductListAppVo>> getProductPage(
@ParameterObject Page page,
@RequestParam(required = false) Long categoryId,
@RequestParam(required = false) String keyword,
@RequestParam Long merchantId
) {
LambdaQueryWrapper<MallProductEntity> wrapper = Wrappers.lambdaQuery();
wrapper.eq(categoryId != null, MallProductEntity::getCategoryId, categoryId);
wrapper.like(StrUtil.isNotBlank(keyword), MallProductEntity::getTitle, keyword);
wrapper.eq(MallProductEntity::getMerchantId, merchantId);
wrapper.eq(MallProductEntity::getStatus, MallConstants.Status.ENABLED); // 只查上架商品
IPage<MallProductEntity> productPage = mallProductService.page(page, wrapper);
// 获取商品ID列表
List<Long> productIds = productPage.getRecords().stream()
.map(MallProductEntity::getId)
.collect(Collectors.toList());
// 查询SKU信息
List<MallProductSkuEntity> skuList = productIds.isEmpty() ? List.of() :
mallProductSkuService.list(
Wrappers.<MallProductSkuEntity>lambdaQuery()
.in(MallProductSkuEntity::getProductId, productIds)
);
// 按商品ID分组SKU
Map<Long, List<MallProductSkuEntity>> skuMap = skuList.stream()
.collect(Collectors.groupingBy(MallProductSkuEntity::getProductId));
// 转换为VO
IPage<MallProductListAppVo> voPage = productPage.convert(product -> {
MallProductListAppVo vo = BeanUtils.toBean(product, MallProductListAppVo.class);
List<MallProductSkuEntity> skus = skuMap.getOrDefault(product.getId(), List.of());
// 计算最低价格
vo.setMinPrice(skus.stream()
.map(MallProductSkuEntity::getPrice)
.filter(Objects::nonNull)
.min(Integer::compareTo)
.orElse(0));
// 计算总库存
vo.setTotalStock(skus.stream()
.map(MallProductSkuEntity::getStock)
.filter(Objects::nonNull)
.mapToDouble(Double::doubleValue)
.sum());
return vo;
});
return R.ok(voPage);
}
/**
* 商品详情
*/
@GetMapping("/product/{id}")
@Operation(summary = "商品详情", description = "根据ID获取商品详情")
public R<MallProductDetailAppVo> getProductDetail(@PathVariable("id") Long id) {
MallProductEntity product = mallProductService.getById(id);
if (product == null) {
return R.failed("商品不存在");
}
MallProductDetailAppVo vo = BeanUtils.toBean(product, MallProductDetailAppVo.class);
// 查询SKU列表
List<MallProductSkuEntity> skuList = mallProductSkuService.list(
Wrappers.<MallProductSkuEntity>lambdaQuery()
.eq(MallProductSkuEntity::getProductId, id)
);
vo.setSkuList(skuList.stream()
.map(sku -> BeanUtils.toBean(sku, MallProductSkuAppVo.class))
.collect(Collectors.toList()));
return R.ok(vo);
}
/**
* 批量获取商品详情
*/
@GetMapping("/product/batch")
@Operation(summary = "批量获取商品详情", description = "根据商品ID列表批量获取商品详情")
public R<List<MallProductDetailAppVo>> getProductBatch(@RequestParam List<Long> ids) {
List<MallProductEntity> products = mallProductService.listByIds(ids);
List<MallProductDetailAppVo> result = new java.util.ArrayList<>();
for (MallProductEntity product : products) {
MallProductDetailAppVo vo = BeanUtils.toBean(product, MallProductDetailAppVo.class);
// 查询SKU列表
List<MallProductSkuEntity> skuList = mallProductSkuService.list(
Wrappers.<MallProductSkuEntity>lambdaQuery()
.eq(MallProductSkuEntity::getProductId, product.getId())
);
vo.setSkuList(skuList.stream()
.map(sku -> BeanUtils.toBean(sku, MallProductSkuAppVo.class))
.collect(Collectors.toList()));
result.add(vo);
}
return R.ok(result);
}
// ==================== 商户商品管理接口 ====================
/**
* 商户创建商品
*/
@PostMapping("/merchant/product/create")
@Operation(summary = "商户创建商品", description = "移动端商户创建商品,支持包含SKU")
public R<String> createProduct(@RequestBody MallProductCreateDTO productCreateDTO) {
try {
Long userMerchantId = validateMerchant();
// 自动设置商户ID
productCreateDTO.setMerchantId(userMerchantId);
// 创建商品
boolean success = mallProductService.createProductWithSkus(productCreateDTO);
if (success) {
return R.ok("商品创建成功");
} else {
return R.failed("商品创建失败");
}
} catch (Exception e) {
return R.failed("商品创建失败:" + e.getMessage());
}
}
/**
* 商户更新商品
*/
@PutMapping("/merchant/product/update")
@Operation(summary = "商户更新商品", description = "移动端商户更新商品,支持包含SKU")
public R<String> updateProduct(@RequestBody MallProductCreateDTO productCreateDTO) {
try {
//验证商户
Long merchantId = validateMerchant();
//验证商品数据
validateProductPermission(productCreateDTO.getId(),merchantId);
// 验证商品ID
if (productCreateDTO.getId() == null) {
return R.failed("商品ID不能为空");
}
// 更新商品
boolean success = mallProductService.updateProductWithSkus(productCreateDTO);
if (success) {
return R.ok("商品更新成功");
} else {
return R.failed("商品更新失败");
}
} catch (Exception e) {
return R.failed("商品更新失败:" + e.getMessage());
}
}
/**
* 验证商户权限和状态
* @return 商户id
*/
private Long validateMerchant() {
// 获取当前用户ID
Long currentUserId = SecurityUtils.getUser().getId();
if (currentUserId == null) {
throw new BusinessException("用户未登录");
}
Long merchantIdByUserId=null;
// 验证用户是否有商户角色
List<Long> roleIds = SecurityUtils.getRoleIds();
R<List<SysRole>> mallMchUserLists = remoteRoleService.findRolesByCodes(Arrays.asList(MallConstants.RoleCode.MERCHANT_USER));
if (mallMchUserLists.isOk()) {
List<SysRole> list = mallMchUserLists.getData();
if (CollectionUtils.isEmpty(list) || CollUtil.isEmpty(roleIds) || !roleIds.contains(list.get(0).getRoleId())) {
throw new BusinessException("请分配商户角色");
}
//有商户角色,然后查询自己绑定的商户信息
merchantIdByUserId = mallMerchantUserService.getMerchantIdByUserId(currentUserId);
if (merchantIdByUserId == null){
throw new BusinessException("当前用户未绑定商户");
}
} else {
throw new BusinessException("基础用户服务异常");
}
return merchantIdByUserId;
}
/**
* 验证是否有处理该条商品数据的权限
*/
private void validateProductPermission(Long productId, Long merchantId) {
MallProductEntity mallProduct = mallProductService.getById(productId);
if (mallProduct == null) {
throw new BusinessException("商品不存在");
}
if (!merchantId.equals(mallProduct.getMerchantId())) {
throw new BusinessException("无权限处理此商品");
}
}
/**
* 商户获取自己的商品列表
*/
@GetMapping("/merchant/product/list")
@Operation(summary = "商户商品列表", description = "移动端商户获取自己的商品列表")
public R<IPage<MallProductListAppVo>> getMerchantProductList(
@ParameterObject Page page,
@RequestParam(required = false) Long merchantId,
@RequestParam(required = false) Long categoryId,
@RequestParam(required = false) String keyword,
@RequestParam(required = false) Integer status
) {
try {
Long targetMerchantId = validateMerchant();
LambdaQueryWrapper<MallProductEntity> wrapper = Wrappers.lambdaQuery();
wrapper.eq(MallProductEntity::getMerchantId, targetMerchantId);
wrapper.eq(categoryId != null, MallProductEntity::getCategoryId, categoryId);
wrapper.like(StrUtil.isNotBlank(keyword), MallProductEntity::getTitle, keyword);
wrapper.eq(status != null, MallProductEntity::getStatus, status);
IPage<MallProductEntity> productPage = mallProductService.page(page, wrapper);
// 获取商品ID列表
List<Long> productIds = productPage.getRecords().stream()
.map(MallProductEntity::getId)
.collect(Collectors.toList());
// 查询SKU信息
List<MallProductSkuEntity> skuList = productIds.isEmpty() ? List.of() :
mallProductSkuService.list(
Wrappers.<MallProductSkuEntity>lambdaQuery()
.in(MallProductSkuEntity::getProductId, productIds)
);
// 按商品ID分组SKU
Map<Long, List<MallProductSkuEntity>> skuMap = skuList.stream()
.collect(Collectors.groupingBy(MallProductSkuEntity::getProductId));
// 转换为VO
IPage<MallProductListAppVo> voPage = productPage.convert(product -> {
MallProductListAppVo vo = BeanUtils.toBean(product, MallProductListAppVo.class);
List<MallProductSkuEntity> skus = skuMap.getOrDefault(product.getId(), List.of());
// 计算最低价格
vo.setMinPrice(skus.stream()
.map(MallProductSkuEntity::getPrice)
.filter(Objects::nonNull)
.min(Integer::compareTo)
.orElse(0));
// 计算总库存
vo.setTotalStock(skus.stream()
.map(MallProductSkuEntity::getStock)
.filter(Objects::nonNull)
.mapToDouble(Double::doubleValue)
.sum());
return vo;
});
return R.ok(voPage);
} catch (Exception e) {
return R.failed("获取商品列表失败:" + e.getMessage());
}
}
/**
* 商户删除商品
*/
@DeleteMapping("/merchant/product/{id}")
@Operation(summary = "商户删除商品", description = "移动端商户删除自己的商品")
public R<String> deleteProduct(@PathVariable("id") Long id) {
try {
Long l = validateMerchant();
validateProductPermission(id, l);
// 删除商品(逻辑删除)
boolean success = mallProductService.removeById(id);
if (success) {
return R.ok("商品删除成功");
} else {
return R.failed("商品删除失败");
}
} catch (Exception e) {
return R.failed("商品删除失败:" + e.getMessage());
}
}
// ==================== 商户分类管理接口 ====================
/**
* 商户创建分类
*/
@PostMapping("/merchant/category/create")
@Operation(summary = "移动端创建商品分类", description = "移动创建商品分类")
public R<String> createCategory(@RequestBody MallCategoryEntity category) {
try {
Long merchantId = validateMerchant();
// 自动设置商户ID
category.setMerchantId(merchantId);
category.setDeptId(SecurityUtils.getUser().getDeptId());
// 创建分类
boolean success = mallCategoryService.save(category);
if (success) {
return R.ok("商品分类创建成功");
} else {
return R.failed("商品分类创建失败");
}
} catch (Exception e) {
return R.failed("商品分类创建失败:" + e.getMessage());
}
}
/**
* 商户更新分类
*/
@PutMapping("/merchant/category/update")
@Operation(summary = "更新商品分类", description = "移动端更新商品分类")
public R<String> updateCategory(@RequestBody MallCategoryEntity category) {
try {
Long merchantId = validateMerchant();
// 验证分类权限
validateCategoryPermission(category.getId(), merchantId);
// 验证分类ID
if (category.getId() == null) {
return R.failed("分类ID不能为空");
}
// 更新分类
boolean success = mallCategoryService.updateById(category);
if (success) {
return R.ok("分类更新成功");
} else {
return R.failed("分类更新失败");
}
} catch (Exception e) {
return R.failed("分类更新失败:" + e.getMessage());
}
}
/**
* 商户删除分类
*/
@DeleteMapping("/merchant/category/{id}")
@Operation(summary = "删除商品分类", description = "移动端删除商户自己的商品分类")
public R<String> deleteCategory(@PathVariable("id") Long id) {
try {
Long merchantId = validateMerchant();
validateCategoryPermission(id, merchantId);
// 删除分类(逻辑删除)
boolean success = mallCategoryService.removeById(id);
if (success) {
return R.ok("分类删除成功");
} else {
return R.failed("分类删除失败");
}
} catch (Exception e) {
return R.failed("分类删除失败:" + e.getMessage());
}
}
/**
* 商户获取自己的分类列表
*/
@GetMapping("/merchant/category/list")
@Operation(summary = "商品分类列表", description = "移动端获取自己商品分类列表")
public R<List<MallCategoryEntity>> getMerchantCategoryList(@RequestParam(required = false) Long parentId) {
try {
Long merchantId = validateMerchant();
LambdaQueryWrapper<MallCategoryEntity> wrapper = Wrappers.lambdaQuery();
wrapper.eq(MallCategoryEntity::getMerchantId, merchantId);
wrapper.eq(parentId != null, MallCategoryEntity::getParentId, parentId);
wrapper.orderByAsc(MallCategoryEntity::getSort);
List<MallCategoryEntity> list = mallCategoryService.list(wrapper);
return R.ok(list);
} catch (Exception e) {
return R.failed("获取分类列表失败:" + e.getMessage());
}
}
/**
* 验证是否有处理该条分类数据的权限
*/
private void validateCategoryPermission(Long categoryId, Long merchantId) {
MallCategoryEntity category = mallCategoryService.getById(categoryId);
if (category == null) {
throw new BusinessException("分类不存在");
}
if (!merchantId.equals(category.getMerchantId())) {
throw new BusinessException("无权限处理此分类");
}
}
// ==================== 库存相关接口 ====================
/**
* 批量增减库存
*/
@PostMapping("/stock/deduct")
@Operation(summary = "批量增减库存", description = "正加负减,任一失败全失败")
public R<Boolean> deductStock(@RequestBody SkuStockChangeDTO changes) {
boolean result = mallProductSkuService.deductStockBatch(changes);
return result ? R.ok(true) : R.failed("库存不足或扣减失败");
}
/**
* 检查商品库存
*/
@GetMapping("/stock/check")
@Operation(summary = "检查商品库存", description = "检查指定SKU的库存是否充足")
public R<Boolean> checkStock(@RequestParam("skuId") Long skuId, @RequestParam("incrCount") Integer incrCount) {
MallProductSkuEntity sku = mallProductSkuService.getById(skuId);
if (sku == null) {
return R.failed("商品SKU不存在");
}
if (sku.getStock() < Math.abs(incrCount)) {
return R.failed("库存不足");
}
return R.ok(true);
}
// ==================== 私有方法 ====================
/**
* 构建分类树
*/
private List<MallCategoryTreeVo> buildCategoryTree(List<MallCategoryEntity> all, Long parentId) {
List<MallCategoryTreeVo> result = new java.util.ArrayList<>();
for (MallCategoryEntity cat : all) {
if (Objects.equals(cat.getParentId(), parentId)) {
MallCategoryTreeVo vo = BeanUtils.toBean(cat, MallCategoryTreeVo.class);
vo.setChildren(buildCategoryTree(all, cat.getId()));
result.add(vo);
}
}
return result;
}
}
@@ -52,6 +52,8 @@ public class MallCategoryController {
public R getMallCategoryPage(@ParameterObject Page page, @ParameterObject MallCategoryEntity MallCategory) {
LambdaQueryWrapper<MallCategoryEntity> wrapper = Wrappers.lambdaQuery();
wrapper.eq(StrUtil.isNotBlank(MallCategory.getName()),MallCategoryEntity::getName,MallCategory.getName());
// 添加商户ID过滤条件
wrapper.eq(MallCategory.getMerchantId() != null, MallCategoryEntity::getMerchantId, MallCategory.getMerchantId());
return R.ok(MallCategoryService.pageScope(page, wrapper));
}
@@ -79,6 +81,13 @@ public class MallCategoryController {
@PreAuthorize("@pms.hasPermission('supermarket_mallCategory_add')" )
public R save(@RequestBody MallCategoryEntity MallCategory) {
MallCategory.setDeptId(SecurityUtils.getUser().getDeptId());
// 设置商户ID
if (MallCategory.getMerchantId() == null) {
return R.failed("商户ID不能为空");
}
// 如果前端没有传商户ID,可以从当前用户信息中获取
// 这里需要根据实际业务逻辑来设置,可能需要从用户关联的商户信息中获取
// MallCategory.setMerchantId(getCurrentUserMerchantId());
return R.ok(MallCategoryService.save(MallCategory));
}
@@ -119,7 +128,11 @@ public class MallCategoryController {
@GetMapping("/export")
@PreAuthorize("@pms.hasPermission('supermarket_mallCategory_export')" )
public List<MallCategoryEntity> export(MallCategoryEntity MallCategory,Long[] ids) {
return MallCategoryService.list(Wrappers.lambdaQuery(MallCategory).in(ArrayUtil.isNotEmpty(ids), MallCategoryEntity::getId, ids));
LambdaQueryWrapper<MallCategoryEntity> wrapper = Wrappers.lambdaQuery(MallCategory);
wrapper.in(ArrayUtil.isNotEmpty(ids), MallCategoryEntity::getId, ids);
// 添加商户ID过滤条件
wrapper.eq(MallCategory.getMerchantId() != null, MallCategoryEntity::getMerchantId, MallCategory.getMerchantId());
return MallCategoryService.list(wrapper);
}
/**
@@ -127,8 +140,11 @@ public class MallCategoryController {
*/
@GetMapping("/tree")
@Operation(summary = "分类树", description = "获取所有分类的树结构")
public R<List<MallCategoryTreeVo>> tree() {
List<MallCategoryEntity> all = MallCategoryService.listScope();
public R<List<MallCategoryTreeVo>> tree(@RequestParam(required = false) Long merchantId) {
LambdaQueryWrapper<MallCategoryEntity> wrapper = Wrappers.lambdaQuery();
// 添加商户ID过滤条件
wrapper.eq(merchantId != null, MallCategoryEntity::getMerchantId, merchantId);
List<MallCategoryEntity> all = MallCategoryService.list(wrapper);
List<MallCategoryTreeVo> tree = buildCategoryTree(all, 0L);
return R.ok(tree);
}
@@ -150,9 +166,15 @@ public class MallCategoryController {
*/
@GetMapping("/listByParent/{parentId}")
@Operation(summary = "根据父级ID查子分类", description = "根据父级ID查子分类")
public R<List<MallCategoryEntity>> listByParent(@PathVariable("parentId") Long parentId) {
List<MallCategoryEntity> list = MallCategoryService.list(Wrappers.<MallCategoryEntity>lambdaQuery().eq(MallCategoryEntity::getParentId, parentId));
public R<List<MallCategoryEntity>> listByParent(@PathVariable("parentId") Long parentId, @RequestParam(required = false) Long merchantId) {
LambdaQueryWrapper<MallCategoryEntity> wrapper = Wrappers.lambdaQuery();
wrapper.eq(MallCategoryEntity::getParentId, parentId);
// 添加商户ID过滤条件
wrapper.eq(merchantId != null, MallCategoryEntity::getMerchantId, merchantId);
List<MallCategoryEntity> list = MallCategoryService.list(wrapper);
return R.ok(list);
}
}
@@ -120,4 +120,20 @@ public class MallMerchantController {
public List<MallMerchantEntity> export(MallMerchantEntity mallMerchant,Long[] ids) {
return mallMerchantService.list(Wrappers.lambdaQuery(mallMerchant).in(ArrayUtil.isNotEmpty(ids), MallMerchantEntity::getId, ids));
}
/**
* 获取商户下拉选择列表
* @return R
*/
@Operation(summary = "获取商户下拉选择列表" , description = "获取商户下拉选择列表" )
@GetMapping("/selectList")
@PreAuthorize("@pms.hasPermission('supermarket_mallMerchant_view')" )
public R getSelectList() {
LambdaQueryWrapper<MallMerchantEntity> wrapper = Wrappers.lambdaQuery();
wrapper.select(MallMerchantEntity::getId, MallMerchantEntity::getName, MallMerchantEntity::getContactName, MallMerchantEntity::getContactPhone);
wrapper.eq(MallMerchantEntity::getStatus, 1); // 只查询启用状态的商户
wrapper.orderByAsc(MallMerchantEntity::getName);
List<MallMerchantEntity> list = mallMerchantService.list(wrapper);
return R.ok(list);
}
}
@@ -0,0 +1,307 @@
package com.bsd.pjcloud.supermarket.controller;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.bsd.pjcloud.admin.api.feign.RemoteUserService;
import com.bsd.pjcloud.admin.api.vo.UserSimpleVo;
import com.bsd.pjcloud.common.core.util.R;
import com.bsd.pjcloud.common.log.annotation.SysLog;
import com.bsd.pjcloud.supermarket.dto.MerchantUserBindDTO;
import com.bsd.pjcloud.supermarket.dto.MerchantUserSingleBindDTO;
import com.bsd.pjcloud.supermarket.entity.MallMerchantUserEntity;
import com.bsd.pjcloud.supermarket.service.MallMerchantUserService;
import com.bsd.pjcloud.supermarket.constant.MallConstants;
import com.bsd.pjcloud.supermarket.vo.MallMerchantUserVo;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springdoc.core.annotations.ParameterObject;
import org.springframework.http.HttpHeaders;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import jakarta.validation.Valid;
import java.util.List;
/**
* 商户用户绑定管理
*
* @author zjf
* @date 2025-01-27
*/
@RestController
@RequiredArgsConstructor
@RequestMapping("/mallMerchantUser")
@Tag(description = "mallMerchantUser", name = "商户用户绑定管理")
@SecurityRequirement(name = HttpHeaders.AUTHORIZATION)
public class MallMerchantUserController {
private final MallMerchantUserService mallMerchantUserService;
private final RemoteUserService remoteUserService;
/**
* 分页查询商户用户绑定关系
*/
@Operation(summary = "分页查询", description = "分页查询商户用户绑定关系")
@GetMapping("/page")
@PreAuthorize("@pms.hasPermission('supermarket_mallMerchant_view')" )
public R<Page<MallMerchantUserEntity>> getPage(
@ParameterObject Page page,
@RequestParam(required = false) Long merchantId,
@RequestParam(required = false) Long userId,
@RequestParam(required = false) Integer roleType,
@RequestParam(required = false) Integer status
) {
LambdaQueryWrapper<MallMerchantUserEntity> wrapper = Wrappers.lambdaQuery();
wrapper.eq(merchantId != null, MallMerchantUserEntity::getMerchantId, merchantId)
.eq(userId != null, MallMerchantUserEntity::getUserId, userId)
.eq(roleType != null, MallMerchantUserEntity::getRoleType, roleType)
.eq(status != null, MallMerchantUserEntity::getStatus, status)
.orderByDesc(MallMerchantUserEntity::getCreateTime);
Page<MallMerchantUserEntity> result = mallMerchantUserService.page(page, wrapper);
return R.ok(result);
}
/**
* 根据商户ID获取绑定的用户列表
*/
@Operation(summary = "获取商户用户列表", description = "根据商户ID获取绑定的用户列表")
@GetMapping("/merchant/{merchantId}/users")
@PreAuthorize("@pms.hasPermission('supermarket_mallMerchant_view')" )
public R<List<MallMerchantUserVo>> getMerchantUsers(@PathVariable("merchantId") Long merchantId) {
List<MallMerchantUserEntity> users = mallMerchantUserService.getMerchantUsers(merchantId);
// 转换为VO对象
List<MallMerchantUserVo> userVos = users.stream().map(entity -> {
MallMerchantUserVo vo = new MallMerchantUserVo();
vo.setId(entity.getId());
vo.setMerchantId(entity.getMerchantId());
vo.setUserId(entity.getUserId());
vo.setName(entity.getUserName());
vo.setPhone(entity.getUserPhone());
vo.setStatus(entity.getStatus());
vo.setStatusDesc("1".equals(entity.getStatus()) ? "启用" : "0".equals(entity.getStatus()) ? "禁用" : "未知");
return vo;
}).toList();
return R.ok(userVos);
}
/**
* 根据用户ID获取绑定的商户信息
*/
@Operation(summary = "获取用户商户信息", description = "根据用户ID获取绑定的商户信息")
@GetMapping("/user/{userId}/merchant")
@PreAuthorize("@pms.hasPermission('supermarket_mallMerchant_view')" )
public R<MallMerchantUserEntity> getUserMerchant(@PathVariable("userId") Long userId) {
MallMerchantUserEntity merchantUser = mallMerchantUserService.getByUserId(userId);
return R.ok(merchantUser);
}
/**
* 单个用户绑定到商户
*/
@Operation(summary = "单个用户绑定", description = "单个用户绑定到商户")
@PostMapping("/bind")
@PreAuthorize("@pms.hasPermission('supermarket_mallMerchant_add')" )
@SysLog("商户用户绑定")
public R<String> bindUser(@Valid @RequestBody MerchantUserSingleBindDTO bindDTO) {
try {
boolean success = mallMerchantUserService.bindUserToMerchant(
bindDTO.getMerchantId(),
bindDTO.getUserId(),
bindDTO.getUserName(),
bindDTO.getUserPhone(),
bindDTO.getRoleType()
);
if (success) {
return R.ok("用户绑定成功");
} else {
return R.failed("用户绑定失败");
}
} catch (Exception e) {
return R.failed("用户绑定失败:" + e.getMessage());
}
}
/**
* 批量绑定用户到商户
*/
@Operation(summary = "批量绑定用户", description = "批量绑定用户到商户")
@PostMapping("/batchBind")
@PreAuthorize("@pms.hasPermission('supermarket_mallMerchant_add')" )
@SysLog("批量商户用户绑定")
public R<String> batchBindUsers(@Valid @RequestBody MerchantUserBindDTO bindDTO) {
// 添加调试日志
System.out.println("接收到的数据: " + bindDTO);
System.out.println("merchantId: " + bindDTO.getMerchantId());
System.out.println("userIds: " + bindDTO.getUserIds());
System.out.println("roleType: " + bindDTO.getRoleType());
try {
// 将String类型的userIds转换为Long类型
List<Long> userIds = bindDTO.getUserIds().stream()
.map(Long::valueOf)
.collect(java.util.stream.Collectors.toList());
int successCount = mallMerchantUserService.batchBindUsersToMerchant(
bindDTO.getMerchantId(),
userIds,
bindDTO.getRoleType()
);
int totalCount = bindDTO.getUserIds().size();
if (successCount == totalCount) {
return R.ok(String.format("成功绑定%d个用户", successCount));
} else if (successCount > 0) {
return R.ok(String.format("部分绑定成功,成功%d个,失败%d个", successCount, totalCount - successCount));
} else {
return R.failed("批量绑定失败");
}
} catch (Exception e) {
return R.failed("批量绑定失败:" + e.getMessage());
}
}
/**
* 解绑单个用户
*/
@Operation(summary = "解绑用户", description = "解绑单个用户")
@DeleteMapping("/unbind")
@PreAuthorize("@pms.hasPermission('supermarket_mallMerchant_add')" )
@SysLog("商户用户解绑")
public R<String> unbindUser(@RequestBody MerchantUserBindDTO bindDTO) {
try {
// 从userIds中取第一个用户ID
if (bindDTO.getUserIds() == null || bindDTO.getUserIds().isEmpty()) {
return R.failed("用户ID不能为空");
}
Long userId = Long.valueOf(bindDTO.getUserIds().get(0));
boolean success = mallMerchantUserService.unbindUserFromMerchant(bindDTO.getMerchantId(), userId);
if (success) {
return R.ok("用户解绑成功");
} else {
return R.failed("用户解绑失败");
}
} catch (Exception e) {
return R.failed("用户解绑失败:" + e.getMessage());
}
}
/**
* 批量解绑用户
*/
@Operation(summary = "批量解绑用户", description = "批量解绑用户")
@DeleteMapping("/batchUnbind")
@PreAuthorize("@pms.hasPermission('supermarket_mallMerchant_add')" )
@SysLog("批量商户用户解绑")
public R<String> batchUnbindUsers(@RequestBody MerchantUserBindDTO bindDTO) {
try {
// 将String类型的userIds转换为Long类型
List<Long> userIds = bindDTO.getUserIds().stream()
.map(Long::valueOf)
.collect(java.util.stream.Collectors.toList());
int successCount = mallMerchantUserService.batchUnbindUsersFromMerchant(bindDTO.getMerchantId(), userIds);
if (successCount > 0) {
return R.ok(String.format("成功解绑%d个用户", successCount));
} else {
return R.failed("批量解绑失败");
}
} catch (Exception e) {
return R.failed("批量解绑失败:" + e.getMessage());
}
}
/**
* 更新用户角色类型
*/
@Operation(summary = "更新用户角色", description = "更新用户在商户中的角色类型")
@PutMapping("/updateRole")
@PreAuthorize("@pms.hasPermission('supermarket_mallMerchant_add')" )
@SysLog("更新商户用户角色")
public R<String> updateUserRole(
@RequestParam Long userId,
@RequestParam Integer roleType
) {
try {
MallMerchantUserEntity merchantUser = mallMerchantUserService.getByUserId(userId);
if (merchantUser == null) {
return R.failed("用户绑定关系不存在");
}
merchantUser.setRoleType(roleType);
boolean success = mallMerchantUserService.updateById(merchantUser);
if (success) {
return R.ok("角色更新成功");
} else {
return R.failed("角色更新失败");
}
} catch (Exception e) {
return R.failed("角色更新失败:" + e.getMessage());
}
}
/**
* 启用/禁用用户绑定
*/
@Operation(summary = "启用/禁用绑定", description = "启用或禁用用户绑定关系")
@PutMapping("/updateStatus")
@PreAuthorize("@pms.hasPermission('supermarket_mallMerchant_add')" )
@SysLog("更新商户用户状态")
public R<String> updateUserStatus(
@RequestParam Long userId,
@RequestParam String status
) {
try {
MallMerchantUserEntity merchantUser = mallMerchantUserService.getByUserId(userId);
if (merchantUser == null) {
return R.failed("用户绑定关系不存在");
}
merchantUser.setStatus(status);
boolean success = mallMerchantUserService.updateById(merchantUser);
if (success) {
return R.ok("状态更新成功");
} else {
return R.failed("状态更新失败");
}
} catch (Exception e) {
return R.failed("状态更新失败:" + e.getMessage());
}
}
/**
* 获取商户可绑定用户列表
*/
@Operation(summary = "获取可绑定用户列表", description = "获取商户可绑定用户列表")
@GetMapping("/merchant/users")
@PreAuthorize("@pms.hasPermission('supermarket_mallMerchant_add')" )
public R<List<UserSimpleVo>> getMerchantBindableUsers(){
R<List<UserSimpleVo>> userListByRoleCode = remoteUserService.getUserListByRoleCode(MallConstants.RoleCode.MERCHANT_USER);
if (!userListByRoleCode.isOk()){
return R.failed(userListByRoleCode.getMsg());
}
List<UserSimpleVo> data = userListByRoleCode.getData();
return R.ok(data);
}
/**
* 测试JSON反序列化
*/
@Operation(summary = "测试JSON反序列化", description = "测试JSON反序列化")
@PostMapping("/testJson")
public R<String> testJson(@RequestBody MerchantUserBindDTO bindDTO) {
return R.ok("JSON反序列化成功: " + bindDTO);
}
}
@@ -10,6 +10,8 @@ import com.bsd.pjcloud.common.core.util.R;
import com.bsd.pjcloud.common.log.annotation.SysLog;
import com.bsd.pjcloud.supermarket.entity.MallProductEntity;
import com.bsd.pjcloud.supermarket.service.MallProductService;
import com.bsd.pjcloud.supermarket.dto.MallProductCreateDTO;
import jakarta.validation.Valid;
import org.springframework.security.access.prepost.PreAuthorize;
import com.bsd.pjcloud.common.excel.annotation.ResponseExcel;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
@@ -36,7 +38,7 @@ import java.util.Objects;
@SecurityRequirement(name = HttpHeaders.AUTHORIZATION)
public class MallProductController {
private final MallProductService mallProductService;
private final MallProductService mallProductService;
/**
* 分页查询
@@ -58,39 +60,67 @@ public class MallProductController {
/**
* 通过id查询共富商城商品信息
* @param id id
* @param withSkus 是否包含SKU信息
* @return R
*/
@Operation(summary = "通过id查询" , description = "通过id查询" )
@Operation(summary = "通过id查询" , description = "通过id查询商品信息,支持包含SKU" )
@GetMapping("/{id}" )
@PreAuthorize("@pms.hasPermission('supermarket_mallProduct_view')" )
public R getById(@PathVariable("id" ) Long id) {
return R.ok(mallProductService.getById(id));
public R getById(@PathVariable("id" ) Long id, @RequestParam(value = "withSkus", defaultValue = "true") Boolean withSkus) {
try {
if (withSkus) {
MallProductCreateDTO productDTO = mallProductService.getProductWithSkus(id);
return R.ok(productDTO);
} else {
return R.ok(mallProductService.getById(id));
}
} catch (Exception e) {
return R.failed("获取商品详情失败:" + e.getMessage());
}
}
/**
* 新增共富商城商品信息
* @param mallProduct 共富商城商品信息
* 新增商品
* @param productCreateDTO 商品创建DTO
* @return R
*/
@Operation(summary = "新增共富商城商品信息" , description = "新增共富商城商品信息" )
@SysLog("新增共富商城商品信息" )
@Operation(summary = "新增商品" , description = "新增商品信息,支持包含SKU" )
@SysLog("新增商品" )
@PostMapping
@PreAuthorize("@pms.hasPermission('supermarket_mallProduct_add')" )
public R save(@RequestBody MallProductEntity mallProduct) {
return R.ok(mallProductService.save(mallProduct));
public R save(@Valid @RequestBody MallProductCreateDTO productCreateDTO) {
try {
boolean success = mallProductService.createProductWithSkus(productCreateDTO);
if (success) {
return R.ok("商品创建成功");
} else {
return R.failed("商品创建失败");
}
} catch (Exception e) {
return R.failed("商品创建失败:" + e.getMessage());
}
}
/**
* 修改共富商城商品信息
* @param mallProduct 共富商城商品信息
* 修改商品
* @param productCreateDTO 商品创建DTO
* @return R
*/
@Operation(summary = "修改共富商城商品信息" , description = "修改共富商城商品信息" )
@SysLog("修改共富商城商品信息" )
@Operation(summary = "修改商品" , description = "修改商品信息,支持包含SKU" )
@SysLog("修改商品" )
@PutMapping
@PreAuthorize("@pms.hasPermission('supermarket_mallProduct_edit')" )
public R updateById(@RequestBody MallProductEntity mallProduct) {
return R.ok(mallProductService.updateById(mallProduct));
public R updateById(@RequestBody MallProductCreateDTO productCreateDTO) {
try {
boolean success = mallProductService.updateProductWithSkus(productCreateDTO);
if (success) {
return R.ok("商品更新成功");
} else {
return R.failed("商品更新失败");
}
} catch (Exception e) {
return R.failed("商品更新失败:" + e.getMessage());
}
}
/**
@@ -0,0 +1,70 @@
package com.bsd.pjcloud.supermarket.controller;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.bsd.pjcloud.common.core.util.R;
import com.bsd.pjcloud.supermarket.dto.SelectOptionDTO;
import com.bsd.pjcloud.supermarket.entity.MallMerchantEntity;
import com.bsd.pjcloud.supermarket.service.SelectOptionService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpHeaders;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* 下拉选择控制器
*
* @author zjf
* @date 2025-01-27
*/
@RestController
@RequiredArgsConstructor
@RequestMapping("/selectOption")
@Tag(description = "selectOption", name = "下拉选择管理")
@SecurityRequirement(name = HttpHeaders.AUTHORIZATION)
public class SelectOptionController {
private final SelectOptionService selectOptionService;
/**
* 获取商户下拉选择列表
* @return R
*/
@Operation(summary = "获取商户下拉选择列表", description = "获取商户下拉选择列表")
@GetMapping("/merchant")
@PreAuthorize("@pms.hasPermission('supermarket_mallMerchant_view')")
public R<List<SelectOptionDTO>> getMerchantSelectList(@RequestParam(value = "keyWord",required = false)String keyWord) {
List<SelectOptionDTO> list = selectOptionService.getMerchantSelectList(keyWord);
return R.ok(list);
}
/**
* 获取分类下拉选择列表
* @return R
*/
@Operation(summary = "获取分类下拉选择列表", description = "获取分类下拉选择列表")
@GetMapping("/category")
@PreAuthorize("@pms.hasPermission('supermarket_mallCategory_view')")
public R<List<SelectOptionDTO>> getCategorySelectList() {
List<SelectOptionDTO> list = selectOptionService.getCategorySelectList();
return R.ok(list);
}
/**
* 获取分类下拉选择列表(树形结构)
* @return R
*/
@Operation(summary = "获取分类下拉选择列表(树形结构)", description = "获取分类下拉选择列表(树形结构)")
@GetMapping("/category/tree")
@PreAuthorize("@pms.hasPermission('supermarket_mallCategory_view')")
public R<List<SelectOptionDTO>> getCategoryTreeSelectList() {
List<SelectOptionDTO> list = selectOptionService.getCategoryTreeSelectList();
return R.ok(list);
}
}
@@ -127,6 +127,9 @@ public class ShopSkuController {
@Operation(summary = "sku详情" , description = "根据多个skuid,查询多个sku详情" )
@GetMapping("/selectListAll" )
public R<List<ShopSkuEntity>> getSelectListAll(@RequestParam List<Long> ids) {
if (Objects.isNull(ids) || ids.isEmpty()) {
return R.ok();
}
List<ShopSkuEntity> shopSkuEntities = shopSkuService.listByIds(ids);
return R.ok(shopSkuEntities);
}
@@ -59,6 +59,7 @@ public class SmErpSupplierController {
wrapper.like(StrUtil.isNotBlank(smErpSupplier.getSupplierName()),SmErpSupplierEntity::getSupplierName,smErpSupplier.getSupplierName());
wrapper.eq(StrUtil.isNotBlank(smErpSupplier.getPhone()),SmErpSupplierEntity::getPhone,smErpSupplier.getPhone());
wrapper.like(StrUtil.isNotBlank(smErpSupplier.getLinkName()),SmErpSupplierEntity::getLinkName,smErpSupplier.getLinkName());
wrapper.eq(null!=smErpSupplier.getSupplierId(),SmErpSupplierEntity::getSupplierId,smErpSupplier.getSupplierId());
return R.ok(smErpSupplierService.pageScope(page, wrapper));
}
@@ -0,0 +1,132 @@
package com.bsd.pjcloud.supermarket.dto;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;
/**
* 共富商城商品创建DTO(包含SPU和SKU信息)
*
* @author zjf
* @date 2025-01-27
*/
@Data
@Schema(description = "共富商城商品创建DTO")
public class MallProductCreateDTO {
/**
* 主键(编辑时使用)
*/
@Schema(description = "主键")
private Long id;
/**
* 商户ID
*/
@NotNull(message = "商户ID不能为空")
@Schema(description = "商户ID")
private Long merchantId;
/**
* 类目ID
*/
@NotNull(message = "类目ID不能为空")
@Schema(description = "类目ID")
private Long categoryId;
/**
* 商品标题
*/
@NotBlank(message = "商品标题不能为空")
@Schema(description = "商品标题")
private String title;
/**
* 副标题
*/
@Schema(description = "副标题")
private String subTitle;
/**
* 品牌
*/
@Schema(description = "品牌")
private String brand;
/**
* 主图
*/
@Schema(description = "主图")
private String coverImage;
/**
* SPU轮播图URL(逗号分隔,按顺序)
*/
@Schema(description = "SPU轮播图URL(逗号分隔,按顺序)")
private String carouselImages;
/**
* 定价方式(1多SKU 2单价SPU)
*/
@NotNull(message = "定价方式不能为空")
@Schema(description = "定价方式(1多SKU 2单价SPU)")
private Integer priceType;
/**
* SPU售价(当price_type=2有效)
*/
@Schema(description = "SPU售价(当price_type=2有效)")
private Integer spuPrice;
/**
* 划线价
*/
@Schema(description = "划线价")
private Integer originalPrice;
/**
* 计量单位
*/
@Schema(description = "计量单位")
private String unit;
/**
* 状态(1上架 0下架)
*/
@Schema(description = "状态(1上架 0下架)")
private String status;
/**
* 开售时间
*/
@Schema(description = "开售时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime saleStartTime;
/**
* 停售时间
*/
@Schema(description = "停售时间")
private LocalDateTime saleEndTime;
/**
* 详情描述(HTML/文本)
*/
@Schema(description = "详情描述(HTML/文本)")
private String description;
/**
* SKU列表(当priceType=1时必填)
*/
@Valid
@Schema(description = "SKU列表")
private List<MallProductSkuCreateDTO> skuList;
}
@@ -0,0 +1,79 @@
package com.bsd.pjcloud.supermarket.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.math.BigDecimal;
/**
* 共富商城商品SKU创建DTO
*
* @author zjf
* @date 2025-01-27
*/
@Data
@Schema(description = "共富商城商品SKU创建DTO")
public class MallProductSkuCreateDTO {
/**
* SKU编码
*/
@NotBlank(message = "SKU编码不能为空")
@Schema(description = "SKU编码")
private String skuCode;
/**
* SKU名称(规格组合)
*/
@NotBlank(message = "SKU名称不能为空")
@Schema(description = "SKU名称(规格组合)")
private String skuName;
/**
* 售价
*/
@NotNull(message = "售价不能为空")
@Schema(description = "售价")
private Integer price;
/**
* 成本价
*/
@Schema(description = "成本价")
private Integer costPrice;
/**
* 库存
*/
@NotNull(message = "库存不能为空")
@Schema(description = "库存")
private Double stock;
/**
* 条码
*/
@Schema(description = "条码")
private String barcode;
/**
* 重量(克)
*/
@Schema(description = "重量(克)")
private Integer weightG;
/**
* 体积(立方厘米)
*/
@Schema(description = "体积(立方厘米)")
private Integer volumeCm3;
/**
* 状态(1启用 0停用)
*/
@Schema(description = "状态(1启用 0停用)")
private String status;
}
@@ -0,0 +1,30 @@
package com.bsd.pjcloud.supermarket.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import jakarta.validation.constraints.NotNull;
import java.util.List;
/**
* 商户用户绑定DTO
*
* @author zjf
* @date 2025-01-27
*/
@Data
@Schema(description = "商户用户绑定DTO")
public class MerchantUserBindDTO {
@NotNull(message = "商户ID不能为空")
@Schema(description = "商户ID")
private Long merchantId;
@NotNull(message = "用户ID列表不能为空")
@Schema(description = "用户ID列表")
private List<String> userIds;
@Schema(description = "角色类型(1:管理员 2:普通用户)", example = "1")
private Integer roleType = 1;
}
@@ -0,0 +1,35 @@
package com.bsd.pjcloud.supermarket.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import jakarta.validation.constraints.NotNull;
/**
* 单个商户用户绑定DTO
*
* @author zjf
* @date 2025-01-27
*/
@Data
@Schema(description = "单个商户用户绑定DTO")
public class MerchantUserSingleBindDTO {
@NotNull(message = "商户ID不能为空")
@Schema(description = "商户ID")
private Long merchantId;
@NotNull(message = "用户ID不能为空")
@Schema(description = "用户ID")
private Long userId;
@Schema(description = "用户名称")
private String userName;
@Schema(description = "用户手机号")
private String userPhone;
@Schema(description = "角色类型(1:管理员 2:普通用户)", example = "1")
private Integer roleType = 1;
}
@@ -0,0 +1,67 @@
package com.bsd.pjcloud.supermarket.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
* 下拉选择选项DTO
*
* @author zjf
* @date 2025-01-27
*/
@Data
@Schema(description = "下拉选择选项DTO")
public class SelectOptionDTO {
/**
* 选项值
*/
@Schema(description = "选项值")
private Object value;
/**
* 选项标签
*/
@Schema(description = "选项标签")
private String label;
/**
* 是否禁用
*/
@Schema(description = "是否禁用")
private Boolean disabled = false;
/**
* 扩展信息
*/
@Schema(description = "扩展信息")
private Object extra;
public SelectOptionDTO() {
}
public SelectOptionDTO(Object value, String label) {
this.value = value;
this.label = label;
}
public SelectOptionDTO(Object value, String label, Boolean disabled) {
this.value = value;
this.label = label;
this.disabled = disabled;
}
public SelectOptionDTO(Object value, String label, Object extra) {
this.value = value;
this.label = label;
this.extra = extra;
}
public SelectOptionDTO(Object value, String label, Boolean disabled, Object extra) {
this.value = value;
this.label = label;
this.disabled = disabled;
this.extra = extra;
}
}
@@ -63,7 +63,7 @@ public class MallMerchantEntity extends Model<MallMerchantEntity> {
* 状态(1启用 0停用)
*/
@Schema(description="状态(1启用 0停用)")
private Integer status;
private String status;
/**
* 营业执照号
@@ -89,6 +89,12 @@ public class MallMerchantEntity extends Model<MallMerchantEntity> {
@Schema(description="备注")
private String remark;
/**
* 营业时间(格式:HH:mm-HH:mm,如:10:30-22:00)
*/
@Schema(description="营业时间(格式:HH:mm-HH:mm)")
private String businessHours;
/**
* 部门ID
*/
@@ -0,0 +1,109 @@
package com.bsd.pjcloud.supermarket.entity;
import com.baomidou.mybatisplus.annotation.*;
import com.baomidou.mybatisplus.extension.activerecord.Model;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import com.bsd.pjcloud.common.core.util.TenantTable;
import java.time.LocalDateTime;
/**
* 商户用户绑定表
*
* @author zjf
* @date 2025-01-27
*/
@Data
@TenantTable
@TableName("mall_merchant_user")
@EqualsAndHashCode(callSuper = true)
@Schema(description = "商户用户绑定表")
public class MallMerchantUserEntity extends Model<MallMerchantUserEntity> {
/**
* 主键ID
*/
@TableId(type = IdType.ASSIGN_ID)
@Schema(description = "主键ID")
private Long id;
/**
* 商户ID
*/
@Schema(description = "商户ID")
private Long merchantId;
/**
* 用户ID
*/
@Schema(description = "用户ID")
private Long userId;
/**
* 用户名称
*/
@Schema(description = "用户名称")
private String userName;
/**
* 用户手机号
*/
@Schema(description = "用户手机号")
private String userPhone;
/**
* 角色类型(1:管理员 2:普通用户)
*/
@Schema(description = "角色类型(1:管理员 2:普通用户)")
private Integer roleType;
/**
* 状态(1:启用 0:禁用)
*/
@Schema(description = "状态(1:启用 0:禁用)")
private String status;
/**
* 创建者
*/
@TableField(fill = FieldFill.INSERT)
@Schema(description = "创建者")
private String createBy;
/**
* 创建时间
*/
@TableField(fill = FieldFill.INSERT)
@Schema(description = "创建时间")
private LocalDateTime createTime;
/**
* 更新者
*/
@TableField(fill = FieldFill.INSERT_UPDATE)
@Schema(description = "更新者")
private String updateBy;
/**
* 更新时间
*/
@TableField(fill = FieldFill.INSERT_UPDATE)
@Schema(description = "更新时间")
private LocalDateTime updateTime;
/**
* 是否删除(0=正常 1=删除)
*/
@TableLogic
@TableField(fill = FieldFill.INSERT)
@Schema(description = "是否删除(0=正常 1=删除)")
private String delFlag;
/**
* 租户编号
*/
@Schema(description = "租户编号")
private Long tenantId;
}
@@ -79,22 +79,22 @@ public class MallProductEntity extends Model<MallProductEntity> {
private Integer priceType;
/**
* SPU售价(当price_type=2有效)
* SPU售价(当price_type=2有效,单位:分)
*/
@Schema(description="SPU售价(当price_type=2有效)")
private BigDecimal spuPrice;
@Schema(description="SPU售价(当price_type=2有效,单位:分)")
private Integer spuPrice;
/**
* 划线价
* 划线价(单位:分)
*/
@Schema(description="划线价")
private BigDecimal originalPrice;
@Schema(description="划线价(单位:分)")
private Integer originalPrice;
/**
* 总库存(冗余)
*/
@Schema(description="总库存(冗余)")
private Integer totalStock;
private Double totalStock;
/**
* 计量单位
@@ -106,7 +106,7 @@ public class MallProductEntity extends Model<MallProductEntity> {
* 状态(1上架 0下架)
*/
@Schema(description="状态(1上架 0下架)")
private Integer status;
private String status;
/**
* 开售时间
@@ -49,22 +49,22 @@ public class MallProductSkuEntity extends Model<MallProductSkuEntity> {
private String skuName;
/**
* 售价
* 售价(单位:分)
*/
@Schema(description="售价")
private BigDecimal price;
@Schema(description="售价(单位:分)")
private Integer price;
/**
* 成本价
* 成本价(单位:分)
*/
@Schema(description="成本价")
private BigDecimal costPrice;
@Schema(description="成本价(单位:分)")
private Integer costPrice;
/**
* 库存
*/
@Schema(description="库存")
private Integer stock;
private Double stock;
/**
* 条码
@@ -88,7 +88,7 @@ public class MallProductSkuEntity extends Model<MallProductSkuEntity> {
* 状态(1启用 0停用)
*/
@Schema(description="状态(1启用 0停用)")
private Integer status;
private String status;
/**
* 部门ID
@@ -0,0 +1,11 @@
package com.bsd.pjcloud.supermarket.exception;
public class BusinessException extends RuntimeException{
public BusinessException(String message) {
super(message);
}
public BusinessException(String message, Throwable cause) {
super(message, cause);
}
}
@@ -0,0 +1,11 @@
package com.bsd.pjcloud.supermarket.mapper;
import com.bsd.pjcloud.common.data.datascope.PjcloudBaseMapper;
import com.bsd.pjcloud.supermarket.entity.MallMerchantUserEntity;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface MallMerchantUserMapper extends PjcloudBaseMapper<MallMerchantUserEntity> {
}
@@ -3,9 +3,18 @@ package com.bsd.pjcloud.supermarket.mapper;
import com.bsd.pjcloud.common.data.datascope.PjcloudBaseMapper;
import com.bsd.pjcloud.supermarket.entity.MallProductSkuEntity;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Update;
@Mapper
public interface MallProductSkuMapper extends PjcloudBaseMapper<MallProductSkuEntity> {
/**
* 更新库存
* @param skuId SKU ID
* @param stockChange 库存变化量(正数增加,负数减少)
* @return 影响行数
*/
@Update("UPDATE mall_product_sku SET stock = stock + #{stockChange} WHERE id = #{skuId} AND stock >= #{stockChange}")
int updateStock(@Param("skuId") Long skuId, @Param("stockChange") Integer stockChange);
}
@@ -0,0 +1,81 @@
package com.bsd.pjcloud.supermarket.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.bsd.pjcloud.supermarket.entity.MallMerchantUserEntity;
import java.util.List;
public interface MallMerchantUserService extends IService<MallMerchantUserEntity> {
/**
* 根据用户ID获取商户信息
* @param userId 用户ID
* @return 商户用户绑定信息
*/
MallMerchantUserEntity getByUserId(Long userId);
/**
* 根据用户ID获取商户ID
* @param userId 用户ID
* @return 商户ID
*/
Long getMerchantIdByUserId(Long userId);
/**
* 根据商户ID获取用户列表
* @param merchantId 商户ID
* @return 用户列表
*/
List<MallMerchantUserEntity> getUsersByMerchantId(Long merchantId);
/**
* 绑定用户到商户
* @param merchantId 商户ID
* @param userId 用户ID
* @param userName 用户名称
* @param userPhone 用户手机号
* @param roleType 角色类型
* @return 是否成功
*/
boolean bindUserToMerchant(Long merchantId, Long userId, String userName, String userPhone, Integer roleType);
/**
* 解绑用户和商户
* @param merchantId 商户ID
* @param userId 用户ID
* @return 是否成功
*/
boolean unbindUserFromMerchant(Long merchantId, Long userId);
/**
* 验证用户是否有权限操作指定商户
* @param userId 用户ID
* @param merchantId 商户ID
* @return 是否有权限
*/
boolean hasPermission(Long userId, Long merchantId);
/**
* 批量绑定用户到商户
* @param merchantId 商户ID
* @param userIds 用户ID列表
* @param roleType 角色类型
* @return 绑定成功的数量
*/
int batchBindUsersToMerchant(Long merchantId, List<Long> userIds, Integer roleType);
/**
* 批量解绑用户和商户
* @param merchantId 商户ID
* @param userIds 用户ID列表
* @return 解绑成功的数量
*/
int batchUnbindUsersFromMerchant(Long merchantId, List<Long> userIds);
/**
* 根据商户ID获取绑定的用户列表(包含用户信息)
* @param merchantId 商户ID
* @return 用户绑定信息列表
*/
List<MallMerchantUserEntity> getMerchantUsers(Long merchantId);
}
@@ -2,7 +2,28 @@ package com.bsd.pjcloud.supermarket.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.bsd.pjcloud.supermarket.entity.MallProductEntity;
import com.bsd.pjcloud.supermarket.dto.MallProductCreateDTO;
public interface MallProductService extends IService<MallProductEntity> {
/**
* 创建商品(包含SPU和SKU)
* @param productCreateDTO 商品创建DTO
* @return 是否成功
*/
boolean createProductWithSkus(MallProductCreateDTO productCreateDTO);
/**
* 更新商品(包含SPU和SKU)
* @param productCreateDTO 商品更新DTO
* @return 是否成功
*/
boolean updateProductWithSkus(MallProductCreateDTO productCreateDTO);
/**
* 获取商品详情(包含SPU和SKU)
* @param id 商品ID
* @return 商品详情DTO
*/
MallProductCreateDTO getProductWithSkus(Long id);
}
@@ -2,7 +2,21 @@ package com.bsd.pjcloud.supermarket.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.bsd.pjcloud.supermarket.entity.MallProductSkuEntity;
import com.bsd.pjcloud.supermarket.dto.SkuStockChangeDTO;
public interface MallProductSkuService extends IService<MallProductSkuEntity> {
/**
* 修改库存(乐观锁)
* 正数:增加库存,负数:扣减库存
* @param skuId sku主键
* @param num 增减数量(正数加,负数减)
* @return 是否成功
*/
boolean deductStock(Long skuId, Double num);
/**
* 批量增减库存(正数增加,负数扣减),任一失败则返回false
*/
boolean deductStockBatch(SkuStockChangeDTO changes);
}
@@ -0,0 +1,103 @@
package com.bsd.pjcloud.supermarket.service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.bsd.pjcloud.supermarket.constant.MallConstants;
import com.bsd.pjcloud.supermarket.dto.SelectOptionDTO;
import com.bsd.pjcloud.supermarket.entity.MallCategoryEntity;
import com.bsd.pjcloud.supermarket.entity.MallMerchantEntity;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.stream.Collectors;
/**
* 下拉选择服务
*
* @author zjf
* @date 2025-01-27
*/
@Service
@RequiredArgsConstructor
public class SelectOptionService {
private final MallMerchantService mallMerchantService;
private final MallCategoryService mallCategoryService;
/**
* 获取商户下拉选择列表
* @param keyword 关键字,支持商户名称和手机号模糊查询
* @return 商户下拉选择列表
*/
public List<SelectOptionDTO> getMerchantSelectList(String keyword) {
LambdaQueryWrapper<MallMerchantEntity> wrapper = Wrappers.lambdaQuery();
wrapper.select(MallMerchantEntity::getId, MallMerchantEntity::getName, MallMerchantEntity::getContactName, MallMerchantEntity::getContactPhone);
wrapper.eq(MallMerchantEntity::getStatus, 0); // 只查询启用状态的商户
// 如果关键字不为空,添加模糊查询条件
if (keyword != null && !keyword.trim().isEmpty()) {
wrapper.and(w -> w.like(MallMerchantEntity::getName, keyword.trim())
.or()
.like(MallMerchantEntity::getContactPhone, keyword.trim()));
}
wrapper.orderByAsc(MallMerchantEntity::getName);
return mallMerchantService.list(wrapper)
.stream()
.map(merchant -> new SelectOptionDTO(
merchant.getId(),
merchant.getName(),
false,
merchant.getContactPhone() // 扩展信息:联系电话
))
.collect(Collectors.toList());
}
/**
* 获取分类下拉选择列表
* @return 分类下拉选择列表
*/
public List<SelectOptionDTO> getCategorySelectList() {
return mallCategoryService.listScope()
.stream()
.filter(category -> category.getStatus() == MallConstants.Status.ENABLED) // 只返回启用状态的分类
.map(category -> new SelectOptionDTO(
category.getId(),
category.getName(),
false,
category.getParentId() // 扩展信息:父级ID
))
.collect(Collectors.toList());
}
/**
* 获取分类下拉选择列表(树形结构)
* @return 分类下拉选择列表
*/
public List<SelectOptionDTO> getCategoryTreeSelectList() {
List<MallCategoryEntity> allCategories = mallCategoryService.listScope();
return buildCategorySelectTree(allCategories, 0L, "");
}
/**
* 构建分类选择树
* @param allCategories 所有分类
* @param parentId 父级ID
* @param prefix 前缀
* @return 分类选择列表
*/
private List<SelectOptionDTO> buildCategorySelectTree(List<MallCategoryEntity> allCategories, Long parentId, String prefix) {
return allCategories.stream()
.filter(category -> category.getStatus() == MallConstants.Status.ENABLED &&
(parentId == null ? category.getParentId() == null : parentId.equals(category.getParentId())))
.map(category -> {
String label = prefix + category.getName();
SelectOptionDTO option = new SelectOptionDTO(category.getId(), label);
option.setExtra(category.getParentId());
return option;
})
.collect(Collectors.toList());
}
}
@@ -0,0 +1,149 @@
package com.bsd.pjcloud.supermarket.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.bsd.pjcloud.supermarket.entity.MallMerchantUserEntity;
import com.bsd.pjcloud.supermarket.mapper.MallMerchantUserMapper;
import com.bsd.pjcloud.supermarket.service.MallMerchantUserService;
import com.bsd.pjcloud.supermarket.constant.MallConstants;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* 商户用户绑定服务实现
*
* @author zjf
* @date 2025-01-27
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class MallMerchantUserServiceImpl extends ServiceImpl<MallMerchantUserMapper, MallMerchantUserEntity> implements MallMerchantUserService {
@Override
public MallMerchantUserEntity getByUserId(Long userId) {
LambdaQueryWrapper<MallMerchantUserEntity> wrapper = Wrappers.lambdaQuery();
wrapper.eq(MallMerchantUserEntity::getUserId, userId)
.eq(MallMerchantUserEntity::getStatus, MallConstants.Status.ENABLED);
return this.getOne(wrapper);
}
@Override
public Long getMerchantIdByUserId(Long userId) {
MallMerchantUserEntity merchantUser = getByUserId(userId);
return merchantUser != null ? merchantUser.getMerchantId() : null;
}
@Override
public List<MallMerchantUserEntity> getUsersByMerchantId(Long merchantId) {
LambdaQueryWrapper<MallMerchantUserEntity> wrapper = Wrappers.lambdaQuery();
wrapper.eq(MallMerchantUserEntity::getMerchantId, merchantId)
.eq(MallMerchantUserEntity::getStatus, MallConstants.Status.ENABLED);
return this.list(wrapper);
}
@Override
public boolean bindUserToMerchant(Long merchantId, Long userId, String userName, String userPhone, Integer roleType) {
// 先检查是否已经绑定
MallMerchantUserEntity existing = getByUserId(userId);
if (existing != null) {
// 如果已经绑定,更新信息
existing.setMerchantId(merchantId);
existing.setUserName(userName);
existing.setUserPhone(userPhone);
existing.setRoleType(roleType);
existing.setStatus(MallConstants.Status.ENABLED);
return this.updateById(existing);
} else {
// 创建新的绑定关系
MallMerchantUserEntity merchantUser = new MallMerchantUserEntity();
merchantUser.setMerchantId(merchantId);
merchantUser.setUserId(userId);
merchantUser.setUserName(userName);
merchantUser.setUserPhone(userPhone);
merchantUser.setRoleType(roleType);
merchantUser.setStatus(MallConstants.Status.ENABLED);
return this.save(merchantUser);
}
}
@Override
public boolean unbindUserFromMerchant(Long merchantId, Long userId) {
LambdaQueryWrapper<MallMerchantUserEntity> wrapper = Wrappers.lambdaQuery();
wrapper.eq(MallMerchantUserEntity::getMerchantId, merchantId)
.eq(MallMerchantUserEntity::getUserId, userId);
return this.remove(wrapper);
}
@Override
public boolean hasPermission(Long userId, Long merchantId) {
LambdaQueryWrapper<MallMerchantUserEntity> wrapper = Wrappers.lambdaQuery();
wrapper.eq(MallMerchantUserEntity::getUserId, userId)
.eq(MallMerchantUserEntity::getMerchantId, merchantId)
.eq(MallMerchantUserEntity::getStatus, MallConstants.Status.ENABLED);
return this.count(wrapper) > 0;
}
@Override
public int batchBindUsersToMerchant(Long merchantId, List<Long> userIds, Integer roleType) {
if (merchantId == null || userIds == null || userIds.isEmpty()) {
return 0;
}
int successCount = 0;
for (Long userId : userIds) {
try {
// 检查是否已经绑定
MallMerchantUserEntity existing = getByUserId(userId);
if (existing != null) {
// 如果已经绑定,更新商户和角色
existing.setMerchantId(merchantId);
existing.setRoleType(roleType);
existing.setStatus(MallConstants.Status.ENABLED);
if (this.updateById(existing)) {
successCount++;
}
} else {
// 创建新的绑定关系
MallMerchantUserEntity merchantUser = new MallMerchantUserEntity();
merchantUser.setMerchantId(merchantId);
merchantUser.setUserId(userId);
merchantUser.setRoleType(roleType);
merchantUser.setStatus(MallConstants.Status.ENABLED);
if (this.save(merchantUser)) {
successCount++;
}
}
} catch (Exception e) {
// 记录日志但继续处理其他用户
log.error("绑定用户{}到商户{}失败: {}", userId, merchantId, e.getMessage());
}
}
return successCount;
}
@Override
public int batchUnbindUsersFromMerchant(Long merchantId, List<Long> userIds) {
if (merchantId == null || userIds == null || userIds.isEmpty()) {
return 0;
}
LambdaQueryWrapper<MallMerchantUserEntity> wrapper = Wrappers.lambdaQuery();
wrapper.eq(MallMerchantUserEntity::getMerchantId, merchantId)
.in(MallMerchantUserEntity::getUserId, userIds);
return this.remove(wrapper) ? userIds.size() : 0;
}
@Override
public List<MallMerchantUserEntity> getMerchantUsers(Long merchantId) {
LambdaQueryWrapper<MallMerchantUserEntity> wrapper = Wrappers.lambdaQuery();
wrapper.eq(MallMerchantUserEntity::getMerchantId, merchantId)
.eq(MallMerchantUserEntity::getStatus, MallConstants.Status.ENABLED)
.orderByDesc(MallMerchantUserEntity::getCreateTime);
return this.list(wrapper);
}
}
@@ -1,10 +1,26 @@
package com.bsd.pjcloud.supermarket.service.impl;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.bsd.pjcloud.supermarket.entity.MallProductEntity;
import com.bsd.pjcloud.supermarket.entity.MallProductSkuEntity;
import com.bsd.pjcloud.supermarket.mapper.MallProductMapper;
import com.bsd.pjcloud.supermarket.service.MallProductService;
import com.bsd.pjcloud.supermarket.service.MallProductSkuService;
import com.bsd.pjcloud.supermarket.dto.MallProductCreateDTO;
import com.bsd.pjcloud.supermarket.dto.MallProductSkuCreateDTO;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* 共富商城商品信息
*
@@ -12,5 +28,338 @@ import org.springframework.stereotype.Service;
* @date 2025-09-02 17:47:01
*/
@Service
@RequiredArgsConstructor
public class MallProductServiceImpl extends ServiceImpl<MallProductMapper, MallProductEntity> implements MallProductService {
private final MallProductSkuService mallProductSkuService;
@Override
@Transactional(rollbackFor = Exception.class)
public boolean createProductWithSkus(MallProductCreateDTO productCreateDTO) {
// 业务验证
validateProductData(productCreateDTO);
// 1. 创建SPU
MallProductEntity product = new MallProductEntity();
product.setMerchantId(productCreateDTO.getMerchantId());
product.setCategoryId(productCreateDTO.getCategoryId());
product.setTitle(productCreateDTO.getTitle());
product.setSubTitle(productCreateDTO.getSubTitle());
product.setBrand(productCreateDTO.getBrand());
product.setCoverImage(productCreateDTO.getCoverImage());
product.setCarouselImages(productCreateDTO.getCarouselImages());
product.setPriceType(productCreateDTO.getPriceType());
product.setSpuPrice(productCreateDTO.getSpuPrice());
product.setOriginalPrice(productCreateDTO.getOriginalPrice());
product.setUnit(productCreateDTO.getUnit());
product.setStatus(productCreateDTO.getStatus());
product.setSaleStartTime(productCreateDTO.getSaleStartTime());
product.setSaleEndTime(productCreateDTO.getSaleEndTime());
product.setDescription(productCreateDTO.getDescription());
// 2. 保存SPU
boolean spuSaved = this.save(product);
if (!spuSaved) {
throw new RuntimeException("保存商品SPU失败");
}
// 3. 处理SKU
if (productCreateDTO.getPriceType() == 1 && CollUtil.isNotEmpty(productCreateDTO.getSkuList())) {
// 多SKU模式,保存SKU列表
List<MallProductSkuEntity> skuList = productCreateDTO.getSkuList().stream()
.map(skuDTO -> {
MallProductSkuEntity sku = new MallProductSkuEntity();
sku.setProductId(product.getId());
sku.setSkuCode(skuDTO.getSkuCode());
sku.setSkuName(skuDTO.getSkuName());
sku.setPrice(skuDTO.getPrice());
sku.setCostPrice(skuDTO.getCostPrice());
sku.setStock(skuDTO.getStock());
sku.setBarcode(skuDTO.getBarcode());
sku.setWeightG(skuDTO.getWeightG());
sku.setVolumeCm3(skuDTO.getVolumeCm3());
sku.setStatus(skuDTO.getStatus());
return sku;
})
.collect(Collectors.toList());
boolean skuSaved = mallProductSkuService.saveBatch(skuList);
if (!skuSaved) {
throw new RuntimeException("保存商品SKU失败");
}
// 计算总库存
Double totalStock = skuList.stream()
.mapToDouble(sku -> sku.getStock() != null ? sku.getStock() : 0D)
.sum();
product.setTotalStock(totalStock);
this.updateById(product);
} else if (productCreateDTO.getPriceType() == 2) {
// 单价SPU模式,设置总库存为0(或根据业务需求设置)
product.setTotalStock(0D);
this.updateById(product);
} else {
// 没有SKU信息,设置总库存为0
product.setTotalStock(0D);
this.updateById(product);
}
return true;
}
@Override
@Transactional(rollbackFor = Exception.class)
public boolean updateProductWithSkus(MallProductCreateDTO productCreateDTO) {
// 业务验证
validateProductData(productCreateDTO);
// 1. 更新SPU
MallProductEntity product = this.getById(productCreateDTO.getId());
if (product == null) {
throw new RuntimeException("商品不存在");
}
// 更新SPU字段
product.setMerchantId(productCreateDTO.getMerchantId());
product.setCategoryId(productCreateDTO.getCategoryId());
product.setTitle(productCreateDTO.getTitle());
product.setSubTitle(productCreateDTO.getSubTitle());
product.setBrand(productCreateDTO.getBrand());
product.setCoverImage(productCreateDTO.getCoverImage());
product.setCarouselImages(productCreateDTO.getCarouselImages());
product.setPriceType(productCreateDTO.getPriceType());
product.setSpuPrice(productCreateDTO.getSpuPrice());
product.setOriginalPrice(productCreateDTO.getOriginalPrice());
product.setUnit(productCreateDTO.getUnit());
product.setStatus(productCreateDTO.getStatus());
product.setSaleStartTime(productCreateDTO.getSaleStartTime());
product.setSaleEndTime(productCreateDTO.getSaleEndTime());
product.setDescription(productCreateDTO.getDescription());
// 2. 保存SPU
boolean spuUpdated = this.updateById(product);
if (!spuUpdated) {
throw new RuntimeException("更新商品SPU失败");
}
// 3. 处理SKU
if (productCreateDTO.getPriceType() == 1 && CollUtil.isNotEmpty(productCreateDTO.getSkuList())) {
// 多SKU模式,精确处理SKU的增删改
updateSkusForProduct(product.getId(), productCreateDTO.getSkuList());
// 计算总库存
List<MallProductSkuEntity> currentSkus = mallProductSkuService.list(
Wrappers.<MallProductSkuEntity>lambdaQuery()
.eq(MallProductSkuEntity::getProductId, product.getId())
);
Double totalStock = currentSkus.stream()
.mapToDouble(sku -> sku.getStock() != null ? sku.getStock() : 0D)
.sum();
product.setTotalStock(totalStock);
this.updateById(product);
} else {
// 单价SPU模式或没有SKU信息,删除所有SKU
LambdaQueryWrapper<MallProductSkuEntity> deleteWrapper = Wrappers.lambdaQuery();
deleteWrapper.eq(MallProductSkuEntity::getProductId, product.getId());
mallProductSkuService.remove(deleteWrapper);
product.setTotalStock(0D);
this.updateById(product);
}
return true;
}
/**
* 精确更新商品的SKU列表(增删改)
* @param productId 商品ID
* @param newSkuList 新的SKU列表
*/
private void updateSkusForProduct(Long productId, List<MallProductSkuCreateDTO> newSkuList) {
// 1. 获取数据库中现有的SKU列表
List<MallProductSkuEntity> existingSkus = mallProductSkuService.list(
Wrappers.<MallProductSkuEntity>lambdaQuery()
.eq(MallProductSkuEntity::getProductId, productId)
);
// 2. 按SKU编码分组现有SKU
Map<String, MallProductSkuEntity> existingSkuMap = existingSkus.stream()
.collect(Collectors.toMap(
MallProductSkuEntity::getSkuCode,
sku -> sku,
(existing, replacement) -> existing // 如果有重复,保留第一个
));
// 3. 按SKU编码分组新SKU
Map<String, MallProductSkuCreateDTO> newSkuMap = newSkuList.stream()
.collect(Collectors.toMap(
MallProductSkuCreateDTO::getSkuCode,
sku -> sku,
(existing, replacement) -> replacement // 如果有重复,保留最后一个
));
// 4. 找出需要删除的SKU(在数据库中存在,但在新列表中不存在)
List<Long> skuIdsToDelete = existingSkus.stream()
.filter(sku -> !newSkuMap.containsKey(sku.getSkuCode()))
.map(MallProductSkuEntity::getId)
.collect(Collectors.toList());
// 5. 找出需要新增的SKU(在新列表中存在,但在数据库中不存在)
List<MallProductSkuEntity> skusToAdd = newSkuList.stream()
.filter(skuDTO -> !existingSkuMap.containsKey(skuDTO.getSkuCode()))
.map(skuDTO -> {
MallProductSkuEntity sku = new MallProductSkuEntity();
sku.setProductId(productId);
sku.setSkuCode(skuDTO.getSkuCode());
sku.setSkuName(skuDTO.getSkuName());
sku.setPrice(skuDTO.getPrice());
sku.setCostPrice(skuDTO.getCostPrice());
sku.setStock(skuDTO.getStock());
sku.setBarcode(skuDTO.getBarcode());
sku.setWeightG(skuDTO.getWeightG());
sku.setVolumeCm3(skuDTO.getVolumeCm3());
sku.setStatus(skuDTO.getStatus());
return sku;
})
.collect(Collectors.toList());
// 6. 找出需要更新的SKU(在数据库和新列表中都存在,但内容不同)
List<MallProductSkuEntity> skusToUpdate = newSkuList.stream()
.filter(skuDTO -> existingSkuMap.containsKey(skuDTO.getSkuCode()))
.map(skuDTO -> {
MallProductSkuEntity existingSku = existingSkuMap.get(skuDTO.getSkuCode());
// 更新字段
existingSku.setSkuName(skuDTO.getSkuName());
existingSku.setPrice(skuDTO.getPrice());
existingSku.setCostPrice(skuDTO.getCostPrice());
existingSku.setStock(skuDTO.getStock());
existingSku.setBarcode(skuDTO.getBarcode());
existingSku.setWeightG(skuDTO.getWeightG());
existingSku.setVolumeCm3(skuDTO.getVolumeCm3());
existingSku.setStatus(skuDTO.getStatus());
return existingSku;
})
.collect(Collectors.toList());
// 7. 执行删除操作
if (!skuIdsToDelete.isEmpty()) {
mallProductSkuService.removeByIds(skuIdsToDelete);
}
// 8. 执行新增操作
if (!skusToAdd.isEmpty()) {
boolean addResult = mallProductSkuService.saveBatch(skusToAdd);
if (!addResult) {
throw new RuntimeException("新增SKU失败");
}
}
// 9. 执行更新操作
if (!skusToUpdate.isEmpty()) {
boolean updateResult = mallProductSkuService.updateBatchById(skusToUpdate);
if (!updateResult) {
throw new RuntimeException("更新SKU失败");
}
}
}
@Override
public MallProductCreateDTO getProductWithSkus(Long id) {
// 1. 获取SPU信息
MallProductEntity product = this.getById(id);
if (product == null) {
throw new RuntimeException("商品不存在");
}
// 2. 转换为DTO
MallProductCreateDTO dto = new MallProductCreateDTO();
dto.setId(product.getId());
dto.setMerchantId(product.getMerchantId());
dto.setCategoryId(product.getCategoryId());
dto.setTitle(product.getTitle());
dto.setSubTitle(product.getSubTitle());
dto.setBrand(product.getBrand());
dto.setCoverImage(product.getCoverImage());
dto.setCarouselImages(product.getCarouselImages());
dto.setPriceType(product.getPriceType());
dto.setSpuPrice(product.getSpuPrice());
dto.setOriginalPrice(product.getOriginalPrice());
dto.setUnit(product.getUnit());
dto.setStatus(product.getStatus());
dto.setSaleStartTime(product.getSaleStartTime());
dto.setSaleEndTime(product.getSaleEndTime());
dto.setDescription(product.getDescription());
// 3. 获取SKU信息
if (product.getPriceType() == 1) {
LambdaQueryWrapper<MallProductSkuEntity> wrapper = Wrappers.lambdaQuery();
wrapper.eq(MallProductSkuEntity::getProductId, id);
List<MallProductSkuEntity> skuList = mallProductSkuService.list(wrapper);
List<MallProductSkuCreateDTO> skuDTOList = skuList.stream()
.map(sku -> {
MallProductSkuCreateDTO skuDTO = new MallProductSkuCreateDTO();
skuDTO.setSkuCode(sku.getSkuCode());
skuDTO.setSkuName(sku.getSkuName());
skuDTO.setPrice(sku.getPrice());
skuDTO.setCostPrice(sku.getCostPrice());
skuDTO.setStock(sku.getStock());
skuDTO.setBarcode(sku.getBarcode());
skuDTO.setWeightG(sku.getWeightG());
skuDTO.setVolumeCm3(sku.getVolumeCm3());
skuDTO.setStatus(sku.getStatus());
return skuDTO;
})
.collect(Collectors.toList());
dto.setSkuList(skuDTOList);
}
return dto;
}
/**
* 商品业务数据验证
* @param productCreateDTO 商品创建DTO
*/
private void validateProductData(MallProductCreateDTO productCreateDTO) {
// 多SKU模式验证
if (productCreateDTO.getPriceType() == 1) {
if (CollUtil.isEmpty(productCreateDTO.getSkuList())) {
throw new RuntimeException("多SKU模式下必须提供SKU列表");
}
// 验证SKU编码在当前请求中的唯一性
long distinctSkuCodes = productCreateDTO.getSkuList().stream()
.map(MallProductSkuCreateDTO::getSkuCode)
.distinct()
.count();
if (distinctSkuCodes != productCreateDTO.getSkuList().size()) {
throw new RuntimeException("SKU编码不能重复");
}
// 验证SKU编码全局唯一性(排除当前商品)
List<String> skuCodes = productCreateDTO.getSkuList().stream()
.map(MallProductSkuCreateDTO::getSkuCode)
.collect(Collectors.toList());
LambdaQueryWrapper<MallProductSkuEntity> wrapper = Wrappers.lambdaQuery();
wrapper.in(MallProductSkuEntity::getSkuCode, skuCodes);
if (productCreateDTO.getId() != null) {
// 编辑时排除当前商品的SKU
wrapper.ne(MallProductSkuEntity::getProductId, productCreateDTO.getId());
}
long existingCount = mallProductSkuService.count(wrapper);
if (existingCount > 0) {
throw new RuntimeException("SKU编码已存在,请使用不同的编码");
}
}
// 单价SPU模式验证
if (productCreateDTO.getPriceType() == 2) {
if (productCreateDTO.getSpuPrice() == null || productCreateDTO.getSpuPrice() <= 0) {
throw new RuntimeException("单价SPU模式下必须设置有效的SPU价格");
}
}
}
}
@@ -1,10 +1,14 @@
package com.bsd.pjcloud.supermarket.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.bsd.pjcloud.supermarket.entity.MallProductSkuEntity;
import com.bsd.pjcloud.supermarket.mapper.MallProductSkuMapper;
import com.bsd.pjcloud.supermarket.service.MallProductSkuService;
import com.bsd.pjcloud.supermarket.dto.SkuStockChangeDTO;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
/**
* 共富商城商品SKU
*
@@ -13,4 +17,46 @@ import org.springframework.stereotype.Service;
*/
@Service
public class MallProductSkuServiceImpl extends ServiceImpl<MallProductSkuMapper, MallProductSkuEntity> implements MallProductSkuService {
/**
* 修改库存(乐观锁)
* 正数:增加库存,负数:扣减库存
*/
@Override
public boolean deductStock(Long skuId, Double num) {
for (int i = 0; i < 3; i++) { // 最多重试3次
MallProductSkuEntity sku = this.getById(skuId);
if (sku == null) return false;
// 扣减时判断库存是否足够
if (num < 0 && sku.getStock() < Math.abs(num)) return false;
LambdaQueryWrapper<MallProductSkuEntity> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(MallProductSkuEntity::getId, skuId);
MallProductSkuEntity currentSku = baseMapper.selectOne(wrapper);
if (currentSku == null) {
return false;
}
// 直接更新库存
int updated = baseMapper.updateStock(skuId, num.intValue());
if (updated > 0) return true;
}
return false;
}
@Override
@Transactional(rollbackFor = Exception.class)
public boolean deductStockBatch(SkuStockChangeDTO changes) {
if (changes == null || changes.getItems() == null || changes.getItems().isEmpty()) {
return true;
}
for (SkuStockChangeDTO.Item item : changes.getItems()) {
if (item == null || item.getId() == null || item.getIncrCount() == null) return false;
boolean ok = deductStock(item.getId(), item.getIncrCount());
if (!ok) {
throw new RuntimeException("扣减库存失败,skuId=" + item.getId());
}
}
return true;
}
}
@@ -39,6 +39,8 @@ import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.DoubleStream;
@@ -167,14 +169,23 @@ public class SmErpInventoryRecordsServiceImpl extends ServiceImpl<SmErpInventory
Double totalNum = smErpInventoryRecordsItemEntityList.stream().filter(Objects::nonNull).mapToDouble(itemEntity -> itemEntity.getPackageNum() * itemEntity.getQuantity()).sum();
int totalRealRrice = smErpInventoryRecordsItemEntityList.stream().filter(Objects::nonNull).mapToInt(SmErpCostPriceVo::getTotalAmount).sum();
double costPrice = (totalRealRrice / 100) / totalNum;
double costPrice = totalRealRrice / totalNum;
SmErpSpuEntity smErpSpuEntity = new SmErpSpuEntity();
smErpSpuEntity.setCostPrice(costPrice);
smErpSpuEntity.setId(erpSpuId);
// 更新库存成本价
smErpSpuMapper.updateById(smErpSpuEntity);
//更新实际进价到商品包装单位里
SmErpUnitEntity smErpUnitEntity = new SmErpUnitEntity();
smErpUnitEntity.setPurchasePrice(item.getRealPrice());
smErpUnitEntity.setId(item.getUnitId());
smErpUnitMapper.updateById(smErpUnitEntity);
});
}
return insert;
}
@@ -221,21 +232,40 @@ public class SmErpInventoryRecordsServiceImpl extends ServiceImpl<SmErpInventory
List<SmErpInventoryRecordsItemEntity> cgPlanItemEntities = smErpInventoryRecordsItemMapper.selectList(smErpCgPlanItemEntityLambdaQueryWrapper);
// 按 inventoryId 分组并计算每个分组的 totalAmount 总和
Map<Long, Integer> inventoryTotalAmountMap = cgPlanItemEntities.stream()
// Map<Long, Integer> inventoryTotalAmountMap = cgPlanItemEntities.stream()
// .collect(Collectors.groupingBy(
// SmErpInventoryRecordsItemEntity::getInventoryId,
// Collectors.summingInt(item ->
// (int) (Math.abs(item.getQuantity()) * (item.getRealPrice()))
// )
// );
Map<Long, List<SmErpInventoryRecordsItemEntity>> smerpInventoryRecordsItemEntityMap= cgPlanItemEntities.stream()
.collect(Collectors.groupingBy(
SmErpInventoryRecordsItemEntity::getInventoryId,
Collectors.summingInt(item ->
(int) (Math.abs(item.getQuantity()) * item.getUnitPrice())
)
));
SmErpInventoryRecordsItemEntity::getInventoryId));
// 将计算出的 totalAmount 总和设置回 pageData 的 records 中
records.forEach(record -> {
Integer totalAmount = inventoryTotalAmountMap.getOrDefault(record.getId(), 0);
record.setTotalAmount(totalAmount);
// if
// Integer totalAmount = inventoryTotalAmountMap.getOrDefault(record.getId(), 0);
List<SmErpInventoryRecordsItemEntity> smErpInventoryRecordsItemEntityList = smerpInventoryRecordsItemEntityMap.get(record.getId());
// record.setTotalAmount(totalAmount);
//todo 这里有一个问题就是出入库编辑的时候,没有更新主表的totalAmount,所以一直用的子表的totalAmount,子表的totalAmount又存在为空的情况,涉及到多个业务
//存在脏数据,判断下,防止空数据查询报错
if (!CollectionUtils.isEmpty(smErpInventoryRecordsItemEntityList)) {
record.setTotalAmount(smErpInventoryRecordsItemEntityList.stream().mapToInt(item -> item.getTotalAmount()==null?0:item.getTotalAmount()).sum());
}else {
record.setTotalAmount(0);
}
});
Set<Long> erpSpuIds = cgPlanItemEntities.stream().map(SmErpInventoryRecordsItemEntity::getErpSpuId).collect(Collectors.toSet());
List<SmErpSpuEntity> smErpSpuEntities = smErpSpuMapper.selectList(new LambdaQueryWrapper<SmErpSpuEntity>().in(SmErpSpuEntity::getId, erpSpuIds));
List<SmErpSpuEntity> smErpSpuEntities=new ArrayList<>();
//存在脏数据,判断下,防止空数据查询报错
if (!CollectionUtils.isEmpty(erpSpuIds)){
smErpSpuEntities = smErpSpuMapper.selectList(new LambdaQueryWrapper<SmErpSpuEntity>().in(SmErpSpuEntity::getId, erpSpuIds));
}
Map<Long,SmErpSpuEntity> spuMap = smErpSpuEntities.stream().collect(Collectors.toMap(SmErpSpuEntity::getId, spu -> spu));
@@ -468,7 +498,6 @@ public class SmErpInventoryRecordsServiceImpl extends ServiceImpl<SmErpInventory
Set<Long> units = items.stream().map(s -> s.getUnitId()).collect(Collectors.toSet());
List<SmErpUnitEntity> smErpUnitEntities = smErpUnitMapper.selectList(new LambdaQueryWrapper<SmErpUnitEntity>().in(SmErpUnitEntity::getId, units));
Map<Long, SmErpUnitEntity> unitMap = smErpUnitEntities.stream().collect(Collectors.toMap(SmErpUnitEntity::getId, unit -> unit));
for (InventoryRecordEditItemDTO item : items){
SmErpInventoryRecordsItemEntity smErpInventoryRecordsItemEntity = new SmErpInventoryRecordsItemEntity();
org.springframework.beans.BeanUtils.copyProperties(item,smErpInventoryRecordsItemEntity);
@@ -996,6 +1025,7 @@ public class SmErpInventoryRecordsServiceImpl extends ServiceImpl<SmErpInventory
excelDto.setUnit(unit.getUnit());
excelDto.setSpec(unit.getSpec());
excelDto.setPackageNum(unit.getPackageNum());
excelDto.setRetailPrice(unit.getRetailPrice() / 100);
}
} else {
// 如果没有指定单位,获取基础单位
@@ -1007,6 +1037,7 @@ public class SmErpInventoryRecordsServiceImpl extends ServiceImpl<SmErpInventory
excelDto.setUnit(baseUnit.getUnit());
excelDto.setSpec(baseUnit.getSpec());
excelDto.setPackageNum(baseUnit.getPackageNum());
excelDto.setRetailPrice(baseUnit.getRetailPrice() / 100);
}
}
}
@@ -1161,6 +1192,9 @@ public class SmErpInventoryRecordsServiceImpl extends ServiceImpl<SmErpInventory
dto.setUnit(baseUnit.getUnit());
dto.setSpec(baseUnit.getSpec());
dto.setPackageNum(baseUnit.getPackageNum());
// 零售价转换为元并保留两位小数
BigDecimal retailPrice = new BigDecimal(baseUnit.getRetailPrice()).divide(new BigDecimal("100"), 2, RoundingMode.HALF_UP);
dto.setRetailPrice(retailPrice.doubleValue());
}
} catch (Exception e) {
// 忽略包装单位信息获取失败
@@ -1187,7 +1221,7 @@ public class SmErpInventoryRecordsServiceImpl extends ServiceImpl<SmErpInventory
summaryDto.setTotalReceiveAmount(0);
}
if (summaryDto.getTotalAmount() == null) {
summaryDto.setTotalAmount(0);
summaryDto.setTotalAmount(0.0);
}
// 获取当前商品的包装单位信息,用于数量转换
@@ -1214,22 +1248,23 @@ public class SmErpInventoryRecordsServiceImpl extends ServiceImpl<SmErpInventory
}
summaryDto.setTotalReceiveAmount(summaryDto.getTotalReceiveAmount() + (int) convertedQuantity);
summaryDto.setTotalAmount(summaryDto.getTotalAmount() + item.getTotalAmount());
// 将商品明细的总金额(分)转换为元并累加,使用BigDecimal确保精度
BigDecimal itemAmount = new BigDecimal(item.getTotalAmount()).divide(new BigDecimal("100"), 2, RoundingMode.HALF_UP);
BigDecimal currentTotal = new BigDecimal(summaryDto.getTotalAmount().toString());
summaryDto.setTotalAmount(currentTotal.add(itemAmount).doubleValue());
// 计算平均单价
// 计算平均单价,保留两位小数
if (summaryDto.getTotalReceiveAmount() > 0) {
double averagePrice = (double) summaryDto.getTotalAmount() / summaryDto.getTotalReceiveAmount() / 100.0; // 转换为元
summaryDto.setAverageUnitPrice(averagePrice);
BigDecimal totalAmount = new BigDecimal(summaryDto.getTotalAmount().toString());
BigDecimal totalQuantity = new BigDecimal(summaryDto.getTotalReceiveAmount());
BigDecimal averagePrice = totalAmount.divide(totalQuantity, 2, RoundingMode.HALF_UP);
summaryDto.setAverageUnitPrice(averagePrice.doubleValue());
}
}
}
// 转换总金额为元
for (SmErpInventoryRecordsSummaryExcelDto summaryDto : summaryMap.values()) {
if (summaryDto.getTotalAmount() != null) {
summaryDto.setTotalAmount(summaryDto.getTotalAmount() / 100);
}
}
// totalAmount在累加时已经转换为元,无需额外处理
return new ArrayList<>(summaryMap.values());
}
@@ -0,0 +1,86 @@
package com.bsd.pjcloud.supermarket.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
/**
* 商户用户绑定信息VO
*
* @author pjcloud
* @date 2025-01-27
*/
@Data
@Schema(description = "商户用户绑定信息")
public class MallMerchantUserVo {
/**
* 主键ID
*/
@Schema(description = "主键ID")
private Long id;
/**
* 商户ID
*/
@Schema(description = "商户ID")
private Long merchantId;
/**
* 用户ID
*/
@Schema(description = "用户ID")
private Long userId;
/**
* 用户名称
*/
@Schema(description = "用户名称")
private String name;
/**
* 用户手机号
*/
@Schema(description = "用户手机号")
private String phone;
/**
* 角色类型(1:管理员 2:普通用户)
*/
@Schema(description = "角色类型(1:管理员 2:普通用户)")
private Integer roleType;
/**
* 角色类型描述
*/
@Schema(description = "角色类型描述")
private String roleTypeDesc;
/**
* 状态(1:启用 0:禁用)
*/
@Schema(description = "状态(1:启用 0:禁用)")
private String status;
/**
* 状态描述
*/
@Schema(description = "状态描述")
private String statusDesc;
/**
* 创建时间
*/
@Schema(description = "创建时间")
private LocalDateTime createTime;
/**
* 更新时间
*/
@Schema(description = "更新时间")
private LocalDateTime updateTime;
}
@@ -0,0 +1,101 @@
package com.bsd.pjcloud.supermarket.vo.app;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.util.List;
/**
* 移动端商户列表VO
*
* @author zjf
* @date 2025-01-27
*/
@Data
@Schema(description = "移动端商户列表VO")
public class MallMerchantListAppVo {
/**
* 商户ID
*/
@Schema(description = "商户ID")
private Long id;
/**
* 商户名称
*/
@Schema(description = "商户名称")
private String name;
/**
* 商户简称
*/
@Schema(description = "商户简称")
private String shortName;
/**
* 联系人
*/
@Schema(description = "联系人")
private String contactName;
/**
* 联系电话
*/
@Schema(description = "联系电话")
private String contactPhone;
/**
* 邮箱
*/
@Schema(description = "邮箱")
private String email;
/**
* 状态(1启用 0停用)
*/
@Schema(description = "状态")
private String status;
/**
* 营业执照号
*/
@Schema(description = "营业执照号")
private String licenseNo;
/**
* 详细地址
*/
@Schema(description = "详细地址")
private String address;
/**
* 商户图片URL(逗号分隔)
*/
@Schema(description = "商户图片URL")
private String images;
/**
* 备注
*/
@Schema(description = "备注")
private String remark;
/**
* 特色商品列表(最多显示3个)
*/
@Schema(description = "特色商品列表")
private List<MallProductListAppVo> featuredProducts;
/**
* 商品总数
*/
@Schema(description = "商品总数")
private Integer productCount;
/**
* 商户营业时间(格式:HH:mm-HH:mm,如:10:30-22:00)
*/
@Schema(description = "商户营业时间")
private String businessHours;
}
@@ -0,0 +1,128 @@
package com.bsd.pjcloud.supermarket.vo.app;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;
/**
* 移动端商品详情VO
*
* @author zjf
* @date 2025-01-27
*/
@Data
@Schema(description = "移动端商品详情VO")
public class MallProductDetailAppVo {
/**
* 商品ID
*/
@Schema(description = "商品ID")
private Long id;
/**
* 商户ID
*/
@Schema(description = "商户ID")
private Long merchantId;
/**
* 类目ID
*/
@Schema(description = "类目ID")
private Long categoryId;
/**
* 商品标题
*/
@Schema(description = "商品标题")
private String title;
/**
* 副标题
*/
@Schema(description = "副标题")
private String subTitle;
/**
* 品牌
*/
@Schema(description = "品牌")
private String brand;
/**
* 主图
*/
@Schema(description = "主图")
private String coverImage;
/**
* 轮播图
*/
@Schema(description = "轮播图")
private String carouselImages;
/**
* 定价方式(1多SKU 2单价SPU)
*/
@Schema(description = "定价方式")
private Integer priceType;
/**
* SPU售价(单位:分)
*/
@Schema(description = "SPU售价(单位:分)")
private Integer spuPrice;
/**
* 划线价(单位:分)
*/
@Schema(description = "划线价(单位:分)")
private Integer originalPrice;
/**
* 总库存
*/
@Schema(description = "总库存")
private Double totalStock;
/**
* 计量单位
*/
@Schema(description = "计量单位")
private String unit;
/**
* 状态
*/
@Schema(description = "状态")
private Integer status;
/**
* 开售时间
*/
@Schema(description = "开售时间")
private LocalDateTime saleStartTime;
/**
* 停售时间
*/
@Schema(description = "停售时间")
private LocalDateTime saleEndTime;
/**
* 详情描述
*/
@Schema(description = "详情描述")
private String description;
/**
* SKU列表
*/
@Schema(description = "SKU列表")
private List<MallProductSkuAppVo> skuList;
}
@@ -0,0 +1,109 @@
package com.bsd.pjcloud.supermarket.vo.app;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
/**
* 移动端商品列表VO
*
* @author zjf
* @date 2025-01-27
*/
@Data
@Schema(description = "移动端商品列表VO")
public class MallProductListAppVo {
/**
* 商品ID
*/
@Schema(description = "商品ID")
private Long id;
/**
* 商户ID
*/
@Schema(description = "商户ID")
private Long merchantId;
/**
* 类目ID
*/
@Schema(description = "类目ID")
private Long categoryId;
/**
* 商品标题
*/
@Schema(description = "商品标题")
private String title;
/**
* 副标题
*/
@Schema(description = "副标题")
private String subTitle;
/**
* 品牌
*/
@Schema(description = "品牌")
private String brand;
/**
* 主图
*/
@Schema(description = "主图")
private String coverImage;
/**
* 轮播图
*/
@Schema(description = "轮播图")
private String carouselImages;
/**
* 定价方式(1多SKU 2单价SPU)
*/
@Schema(description = "定价方式")
private Integer priceType;
/**
* SPU售价(单位:分)
*/
@Schema(description = "SPU售价(单位:分)")
private Integer spuPrice;
/**
* 划线价(单位:分)
*/
@Schema(description = "划线价(单位:分)")
private Integer originalPrice;
/**
* 总库存
*/
@Schema(description = "总库存")
private Double totalStock;
/**
* 计量单位
*/
@Schema(description = "计量单位")
private String unit;
/**
* 状态
*/
@Schema(description = "状态")
private Integer status;
/**
* 最低价格(从SKU中计算,单位:分)
*/
@Schema(description = "最低价格(单位:分)")
private Integer minPrice;
}
@@ -0,0 +1,84 @@
package com.bsd.pjcloud.supermarket.vo.app;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
/**
* 移动端商品SKU VO
*
* @author zjf
* @date 2025-01-27
*/
@Data
@Schema(description = "移动端商品SKU VO")
public class MallProductSkuAppVo {
/**
* SKU ID
*/
@Schema(description = "SKU ID")
private Long id;
/**
* 商品ID
*/
@Schema(description = "商品ID")
private Long productId;
/**
* SKU编码
*/
@Schema(description = "SKU编码")
private String skuCode;
/**
* SKU名称
*/
@Schema(description = "SKU名称")
private String skuName;
/**
* 售价(单位:分)
*/
@Schema(description = "售价(单位:分)")
private Integer price;
/**
* 成本价(单位:分)
*/
@Schema(description = "成本价(单位:分)")
private Integer costPrice;
/**
* 库存
*/
@Schema(description = "库存")
private Double stock;
/**
* 条码
*/
@Schema(description = "条码")
private String barcode;
/**
* 重量(克)
*/
@Schema(description = "重量(克)")
private Integer weightG;
/**
* 体积(立方厘米)
*/
@Schema(description = "体积(立方厘米)")
private Integer volumeCm3;
/**
* 状态
*/
@Schema(description = "状态")
private Integer status;
}
@@ -18,5 +18,6 @@
<result property="updateTime" column="update_time"/>
<result property="delFlag" column="del_flag"/>
<result property="tenantId" column="tenant_id"/>
<result property="merchantId" column="merchant_id"/>
</resultMap>
</mapper>
@@ -0,0 +1,100 @@
package com.bsd.pjcloud.supermarket.service;
import com.bsd.pjcloud.supermarket.dto.MallProductCreateDTO;
import com.bsd.pjcloud.supermarket.dto.MallProductSkuCreateDTO;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ActiveProfiles;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.Arrays;
/**
* 商品服务测试类
*
* @author zjf
* @date 2025-01-27
*/
@SpringBootTest
@ActiveProfiles("test")
public class MallProductServiceTest {
@Autowired
private MallProductService mallProductService;
@Test
public void testCreateProductWithSkus() {
// 创建测试数据
MallProductCreateDTO productCreateDTO = new MallProductCreateDTO();
productCreateDTO.setMerchantId(1L);
productCreateDTO.setCategoryId(100L);
productCreateDTO.setTitle("测试商品");
productCreateDTO.setSubTitle("测试副标题");
productCreateDTO.setBrand("测试品牌");
productCreateDTO.setCoverImage("https://example.com/cover.jpg");
productCreateDTO.setCarouselImages("https://example.com/img1.jpg,https://example.com/img2.jpg");
productCreateDTO.setPriceType(1); // 多SKU模式
productCreateDTO.setOriginalPrice(1);
productCreateDTO.setUnit("台");
productCreateDTO.setStatus("0");
productCreateDTO.setSaleStartTime(LocalDateTime.now());
productCreateDTO.setSaleEndTime(LocalDateTime.now().plusYears(1));
productCreateDTO.setDescription("测试商品描述");
// 创建SKU列表
MallProductSkuCreateDTO sku1 = new MallProductSkuCreateDTO();
sku1.setSkuCode("TEST-SKU-001");
sku1.setSkuName("测试SKU 1");
sku1.setPrice(1);
sku1.setCostPrice(1);
sku1.setStock(1D);
sku1.setBarcode("1234567890123");
sku1.setWeightG(200);
sku1.setVolumeCm3(300);
sku1.setStatus("1");
MallProductSkuCreateDTO sku2 = new MallProductSkuCreateDTO();
sku2.setSkuCode("TEST-SKU-002");
sku2.setSkuName("测试SKU 2");
sku2.setPrice(1);
sku2.setCostPrice(1);
sku2.setStock(50D);
sku2.setBarcode("1234567890124");
sku2.setWeightG(250);
sku2.setVolumeCm3(350);
sku2.setStatus("1");
productCreateDTO.setSkuList(Arrays.asList(sku1, sku2));
// 执行测试
boolean result = mallProductService.createProductWithSkus(productCreateDTO);
assert result : "商品创建应该成功";
}
@Test
public void testCreateProductWithSinglePrice() {
// 创建测试数据 - 单价SPU模式
MallProductCreateDTO productCreateDTO = new MallProductCreateDTO();
productCreateDTO.setMerchantId(1L);
productCreateDTO.setCategoryId(100L);
productCreateDTO.setTitle("测试商品-单价模式");
productCreateDTO.setSubTitle("测试副标题");
productCreateDTO.setBrand("测试品牌");
productCreateDTO.setCoverImage("https://example.com/cover.jpg");
productCreateDTO.setCarouselImages("https://example.com/img1.jpg");
productCreateDTO.setPriceType(2); // 单价SPU模式
productCreateDTO.setSpuPrice(1);
productCreateDTO.setOriginalPrice(1);
productCreateDTO.setUnit("台");
productCreateDTO.setStatus("1");
productCreateDTO.setSaleStartTime(LocalDateTime.now());
productCreateDTO.setSaleEndTime(LocalDateTime.now().plusYears(1));
productCreateDTO.setDescription("测试商品描述");
// 执行测试
boolean result = mallProductService.createProductWithSkus(productCreateDTO);
assert result : "商品创建应该成功";
}
}
@@ -140,6 +140,10 @@ sms:
content: "阿姨您好!{s20}已完成会议,麻烦您前去做好整洁清理工作。"
params: [ "meetingName" ]
31550:
content: "您于{s20}预约的{s20}会议室,现已通过审核,请提前做好会议安排。"
params: [ "meetingName", "meetingDate" ]
settings:
default-ext-code: ""
timeout-seconds: 300
@@ -19,16 +19,21 @@
package com.bsd.pjcloud.admin.api.feign;
import cn.hutool.core.collection.CollUtil;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.bsd.pjcloud.admin.api.entity.SysRole;
import com.bsd.pjcloud.admin.api.entity.SysUserRole;
import com.bsd.pjcloud.common.core.constant.ServiceNameConstants;
import com.bsd.pjcloud.common.core.util.R;
import com.bsd.pjcloud.common.feign.annotation.NoToken;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.util.CollectionUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import java.util.List;
import java.util.stream.Collectors;
/**
* @author pjcloud
@@ -52,4 +57,12 @@ public interface RemoteRoleService {
@PostMapping("/role/by-codes")
R<List<SysRole>> findRolesByCodes(@RequestBody List<String> codes);
/**
* 是否是共富商城商户角色
* @return
*/
@GetMapping("/role/isMallMchUser")
R<Boolean> isMallMchUser();
}
@@ -23,6 +23,7 @@ import com.bsd.pjcloud.admin.api.dto.UserDTO;
import com.bsd.pjcloud.admin.api.dto.UserDeptSimpleInfo;
import com.bsd.pjcloud.admin.api.dto.UserInfo;
import com.bsd.pjcloud.admin.api.entity.SysUser;
import com.bsd.pjcloud.admin.api.vo.UserSimpleVo;
import com.bsd.pjcloud.admin.api.vo.UserVO;
import com.bsd.pjcloud.common.core.constant.ServiceNameConstants;
import com.bsd.pjcloud.common.core.util.R;
@@ -193,4 +194,13 @@ public interface RemoteUserService {
@GetMapping("/user/get-name-or-mobile")
R<List<UserDeptSimpleInfo>> getUserByNameOrMobile(@RequestParam("keyword") String keyword);
/**
* 根据角色编码查询用户列表
* @param code
* @return
*/
@GetMapping("/user/getUserListByRoleCode")
R<List<UserSimpleVo>> getUserListByRoleCode(@RequestParam("code") String code);
}
@@ -0,0 +1,12 @@
package com.bsd.pjcloud.admin.api.vo;
import lombok.Data;
@Data
public class UserSimpleVo {
private Long userId;
// private String username;
private String name;
private String phone;
}
+6
View File
@@ -220,6 +220,12 @@
<groupId>com.bsd</groupId>
<artifactId>pjcloud-flow-task-api</artifactId>
</dependency>
<dependency>
<groupId>com.bsd</groupId>
<artifactId>pjcloud-integrated-api</artifactId>
<version>5.7.0</version>
<scope>compile</scope>
</dependency>
</dependencies>
@@ -41,6 +41,7 @@ import com.bsd.pjcloud.common.security.annotation.HasPermission;
import com.bsd.pjcloud.common.security.annotation.Inner;
import com.bsd.pjcloud.common.security.service.PjcloudUser;
import com.bsd.pjcloud.common.security.util.SecurityUtils;
import com.bsd.pjcloud.integrated.constant.IntegratedConstants;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
@@ -182,7 +183,23 @@ public class SysRoleController {
Long id = SecurityUtils.getUser().getId();
List<SysUserRole> sysUserRoles = sysUserRoleService.list(Wrappers.<SysUserRole>lambdaQuery().eq(SysUserRole::getUserId, id));
List<Long> roleIds = sysUserRoles.stream().map(sysUserRole -> sysUserRole.getRoleId()).collect(Collectors.toList());
List<SysRole> list = sysRoleService.list(Wrappers.<SysRole>lambdaQuery().eq(SysRole::getRoleCode, "MCH_USER"));
List<SysRole> list = sysRoleService.list(Wrappers.<SysRole>lambdaQuery().eq(SysRole::getRoleCode, IntegratedConstants.RoleCode.MCH_USER));
if (CollectionUtils.isEmpty(list) || CollUtil.isEmpty(roleIds) || !roleIds.contains(list.get(0).getRoleId())) {
return R.ok(false);
}
return R.ok(true);
}
/**
* 是否是共富商户角色
* @return
*/
@GetMapping("/isMallMchUser")
public R<Boolean> isMallMchUser() {
Long id = SecurityUtils.getUser().getId();
List<SysUserRole> sysUserRoles = sysUserRoleService.list(Wrappers.<SysUserRole>lambdaQuery().eq(SysUserRole::getUserId, id));
List<Long> roleIds = sysUserRoles.stream().map(sysUserRole -> sysUserRole.getRoleId()).collect(Collectors.toList());
List<SysRole> list = sysRoleService.list(Wrappers.<SysRole>lambdaQuery().eq(SysRole::getRoleCode, IntegratedConstants.RoleCode.MERCHANT_USER));
if (CollectionUtils.isEmpty(list) || CollUtil.isEmpty(roleIds) || !roleIds.contains(list.get(0).getRoleId())) {
return R.ok(false);
}
@@ -29,9 +29,13 @@ import com.bsd.pjcloud.admin.api.dto.UserDTO;
import com.bsd.pjcloud.admin.api.dto.UserDeptSimpleInfo;
import com.bsd.pjcloud.admin.api.dto.UserInfo;
import com.bsd.pjcloud.admin.api.entity.SysDept;
import com.bsd.pjcloud.admin.api.entity.SysRole;
import com.bsd.pjcloud.admin.api.entity.SysUser;
import com.bsd.pjcloud.admin.api.entity.SysUserRole;
import com.bsd.pjcloud.admin.api.vo.*;
import com.bsd.pjcloud.admin.service.SysDeptService;
import com.bsd.pjcloud.admin.service.SysRoleService;
import com.bsd.pjcloud.admin.service.SysUserRoleService;
import com.bsd.pjcloud.admin.service.SysUserService;
import com.bsd.pjcloud.common.core.constant.CommonConstants;
import com.bsd.pjcloud.common.core.exception.ErrorCodes;
@@ -87,6 +91,10 @@ public class SysUserController {
private final RemoterVisitorService remoterVisitorService;
private final SysDeptService sysDeptService;
private final SysRoleService sysRoleService;
private final SysUserRoleService sysUserRoleService;
/* ************************************* start 回调 start ************************************* */
@Inner(value = false)
@@ -524,4 +532,28 @@ public class SysUserController {
}
return R.ok(true);
}
/**
* 根据角色编码获取用户列表
*/
@GetMapping("/getUserListByRoleCode")
public R<List<UserSimpleVo>> getUserListByRoleCode(@RequestParam("code") String code) {
SysRole sysRole = sysRoleService.getOne(Wrappers.<SysRole>lambdaQuery().eq(SysRole::getRoleCode, code));
if (sysRole == null) {
return R.failed("不存在共富商城商户角色的用户!");
}
List<SysUserRole> list = sysUserRoleService.list(Wrappers.<SysUserRole>lambdaQuery().eq(SysUserRole::getRoleId, sysRole.getRoleId()));
List<Long> userIds = list.stream().map(sysUserRole -> sysUserRole.getUserId()).collect(Collectors.toList());
List<SysUser> listData = userService.list(Wrappers.<SysUser>lambdaQuery().in(SysUser::getUserId, userIds));
//转换成vo,只返回id+手机号+姓名,安全考虑
List<UserSimpleVo> userVOList = listData.stream().map(sysUser -> {
UserSimpleVo userVO = new UserSimpleVo();
userVO.setUserId(sysUser.getUserId());
userVO.setPhone(sysUser.getPhone());
userVO.setName(sysUser.getName());
// userVO.setUsername(sysUser.getUsername());
return userVO;
}).collect(Collectors.toList());
return R.ok(userVOList);
}
}