记得上下班打卡 | git大法好,push需谨慎

Commit 931332ff authored by wangyifan's avatar wangyifan

Merge branch 'dev-v1.7.3-pintuan' into 'master'

Dev v1.7.3 pintuan、归因

See merge request !438
parents 8d25b760 e3d3471d
---
description: LiquidNet Bus V1 项目代码规范与最佳实践,适用于所有 Java 文件的编写、审查和重构
globs: "**/*.java"
alwaysApply: false
---
# LiquidNet Bus V1 代码规范与最佳实践
## 1. 架构与目录规范
### 1.1 顶层模块职责
| 模块 | 职责 | 何时修改 |
|------|------|---------|
| `liquidnet-bus-common` | 公共基础设施(工具类、缓存、异常、MQ、Feign、MyBatis、Web) | 新增通用工具或中间件封装 |
| `liquidnet-bus-do` | 数据对象层:Entity、Mapper DAO | 新增/修改数据库表映射 |
| `liquidnet-bus-api` | API 契约层:DTO 入参、常量、枚举(供 Feign 调用方依赖) | 新增对外接口契约 |
| `liquidnet-bus-feign` | Feign 声明式 HTTP 客户端 | 新增跨服务调用 |
| `liquidnet-bus-service` | 核心业务实现(含 consumer 消费者) | 编写业务逻辑 |
| `liquidnet-bus-support` | 基础设施:Eureka、Zuul、Hystrix、Sleuth | 基础设施调整 |
| `liquidnet-bus-client` | 客户端应用:admin 后台(RuoYi)+ job 定时任务 | 管理后台/定时任务 |
| `liquidnet-bus-config` | YAML 配置集中管理 | 配置变更 |
### 1.2 服务内部分层
```
com.liquidnet.service.{服务名}
├── config/ -- 配置类(@Configuration)
├── controller/ -- REST 控制器(@RestController)
├── interceptor/ -- 拦截器
├── service/ -- 服务接口(I 前缀)
│ └── impl/ -- 服务实现
│ ├── inner/ -- 内部服务(MQ 消费 / Job / 跨服务调用)
│ ├── manage/ -- 后台管理服务
│ └── helper/ -- 辅助服务
└── utils/ -- 服务专属工具类
```
### 1.3 DTO/VO/Entity 分布规则
| 类型 | 放置位置 | 说明 |
|------|---------|------|
| Entity | `liquidnet-bus-do/{服务}-do/entity/` | 与数据库表一一对应 |
| DTO(入参/对外) | `liquidnet-bus-api/{服务}-api/dto/` | 供 Feign 调用方使用 |
| DTO(内部) | `liquidnet-bus-do/{服务}-do/dto/` | 仅内部使用 |
| VO(出参) | `liquidnet-bus-service/{服务}-impl/dto/vo/` | 返回给前端的数据 |
| Param(入参) | `liquidnet-bus-service/{服务}-impl/dto/param/` | Controller 接收参数 |
### 1.4 新增微服务流程
1. 在 `liquidnet-bus-do` 下创建 `{服务名}-do` 子模块,定义 Entity/Mapper
2. 在 `liquidnet-bus-api` 下创建 `{服务名}-api` 子模块,定义 DTO/常量/枚举
3. 在 `liquidnet-bus-feign` 下创建 `{服务名}-feign` 子模块,声明 Feign 客户端
4. 在 `liquidnet-bus-service` 下创建 `{服务名}-impl` 子模块,实现业务逻辑
5. 在 `liquidnet-bus-config/liquidnet-config/` 下创建 `{服务名}.yml` 及环境配置
6. 在父 POM 中注册新子模块
### 1.5 文件放置速查表
> 以 **adam** 服务为例,列出所有常见文件类型的放置路径,便于快速定位。
| 文件类型 | 放置路径(以 adam 服务为例) | 说明 |
|---------|---------------------------|------|
| Controller | `liquidnet-bus-service/liquidnet-service-adam/liquidnet-service-adam-impl/src/main/java/com/liquidnet/service/adam/controller/` | REST 接口入口 |
| Service 接口 | `liquidnet-bus-service/liquidnet-service-adam/liquidnet-service-adam-impl/src/main/java/com/liquidnet/service/adam/service/` | I 前缀命名,如 `IAdamUserService` |
| Service 实现 | `liquidnet-bus-service/liquidnet-service-adam/liquidnet-service-adam-impl/src/main/java/com/liquidnet/service/adam/service/impl/` | 命名如 `AdamUserServiceImpl` |
| Service 内部实现 | `...service/impl/inner/` | MQ 消费 / Job / 跨服务调用场景 |
| Service 后台管理 | `...service/impl/manage/` | 后台管理逻辑 |
| Service 辅助 | `...service/impl/helper/` | 辅助服务,抽取公共逻辑 |
| Entity | `liquidnet-bus-do/liquidnet-service-adam-do/src/main/java/com/liquidnet/service/adam/entity/` | 数据库表映射,`@TableName` |
| Mapper 接口 | `liquidnet-bus-do/liquidnet-service-adam-do/src/main/java/com/liquidnet/service/adam/mapper/` | 继承 `BaseMapper<T>` |
| Mapper XML | `liquidnet-bus-do/liquidnet-service-adam-do/src/main/resources/com.liquidnet.service.adam.mapper/` | SQL 映射文件 |
| DAO 查询对象 | `liquidnet-bus-do/liquidnet-service-adam-do/src/main/java/com/liquidnet/service/adam/dao/` | 复杂查询投影对象 |
| DTO(对外) | `liquidnet-bus-api/liquidnet-service-adam-api/src/main/java/com/liquidnet/service/adam/dto/` | Feign 调用方依赖,跨服务传输 |
| DTO(内部) | `liquidnet-bus-do/liquidnet-service-adam-do/src/main/java/com/liquidnet/service/adam/dto/` | 仅内部传输,不对外暴露 |
| VO(出参) | `liquidnet-bus-service/liquidnet-service-adam/liquidnet-service-adam-impl/src/main/java/com/liquidnet/service/adam/dto/vo/` | 返回给前端的数据封装 |
| Param(入参) | `liquidnet-bus-service/liquidnet-service-adam/liquidnet-service-adam-impl/src/main/java/com/liquidnet/service/adam/dto/param/` | Controller 接收参数 |
| 常量 / 枚举 | `liquidnet-bus-api/liquidnet-service-adam-api/src/main/java/com/liquidnet/service/adam/constant/` | 如 `AdamEnum`、`AdamRedisConst` |
| 拦截器 | `liquidnet-bus-service/liquidnet-service-adam/liquidnet-service-adam-impl/src/main/java/com/liquidnet/service/adam/interceptor/` | 服务专属拦截器 |
| 配置类 | `liquidnet-bus-service/liquidnet-service-adam/liquidnet-service-adam-impl/src/main/java/com/liquidnet/service/adam/config/` | `@Configuration` 标注 |
| 工具类 | `liquidnet-bus-service/liquidnet-service-adam/liquidnet-service-adam-impl/src/main/java/com/liquidnet/service/adam/utils/` | 服务专属工具 |
| Feign 客户端 | `liquidnet-bus-feign/liquidnet-api-feign-adam/src/main/java/com/liquidnet/service/feign/adam/rsc/` | 声明式 HTTP 客户端接口 |
| Feign Fallback | `liquidnet-bus-feign/liquidnet-api-feign-adam/src/main/java/com/liquidnet/service/feign/adam/fallback/` | 降级实现 |
| Redis 工具 | `liquidnet-bus-common/liquidnet-common-cache/src/main/java/com/liquidnet/common/cache/redis/` | 命名 `Redis{服务名}Util` |
| 公共工具类 | `liquidnet-bus-common/liquidnet-common-base/src/main/java/com/liquidnet/commons/lang/util/` | 通用工具,所有服务共享 |
| 全局异常基类 | `liquidnet-bus-common/liquidnet-common-exception/liquidnet-common-exception-base/` | 异常定义 |
| 异常处理器 | `liquidnet-bus-common/liquidnet-common-exception/liquidnet-common-exception-handler-service/` | `@ControllerAdvice` 统一处理 |
| 全局拦截器 | `liquidnet-bus-common/liquidnet-common-web/src/main/java/com/liquidnet/common/web/` | 跨服务拦截器 |
| 错误码配置 | `liquidnet-bus-common/liquidnet-common-service-base/src/main/resources/errors.properties` | 错误码映射文件 |
| YAML 配置 | `liquidnet-bus-config/liquidnet-config/` | 所有配置文件集中管理 |
| Consumer 消费者 | `liquidnet-bus-service/liquidnet-service-consumer-all/` | MQ 消费端 |
| Swagger 配置 | 各服务 impl 的 `config/` 包下 | 如 `SwaggerConfig` |
#### 文件放置决策流程图
```
新增文件 → 是什么类型?
├── 数据库表映射(Entity / Mapper / DAO)→ liquidnet-bus-do/{服务}-do/
├── 对外 API 契约(DTO / 常量 / 枚举)→ liquidnet-bus-api/{服务}-api/
├── Feign 客户端 → liquidnet-bus-feign/{服务}-feign/
├── 业务逻辑 → liquidnet-bus-service/{服务}-impl/
│ ├── Controller → controller/
│ ├── Service 接口 → service/
│ ├── Service 实现 → service/impl/
│ │ ├── MQ 消费 / Job → service/impl/inner/
│ │ ├── 后台管理 → service/impl/manage/
│ │ └── 辅助逻辑 → service/impl/helper/
│ ├── 入参 / 出参 → dto/param/ 或 dto/vo/
│ ├── 拦截器 → interceptor/
│ ├── 配置类 → config/
│ └── 工具类 → utils/
├── 通用工具 → liquidnet-bus-common/liquidnet-common-base/
├── Redis 工具 → liquidnet-bus-common/liquidnet-common-cache/
├── 全局拦截器 / 过滤器 → liquidnet-bus-common/liquidnet-common-web/
├── 异常定义 → liquidnet-bus-common/liquidnet-common-exception/
├── YAML 配置 → liquidnet-bus-config/
└── 定时任务 → liquidnet-bus-client/liquidnet-client-job/
```
---
## 2. 命名与编码风格
### 2.1 类命名规范
| 类型 | 命名模式 | 正确示例 |
|------|---------|---------|
| Controller | `{Module}{Entity}Controller` | `AdamUserController` |
| Service 接口 | `I{Entity}Service` | `IAdamUserService` |
| Service 实现 | `{Entity}ServiceImpl` | `AdamUserServiceImpl` |
| Mapper | `{Entity}Mapper` | `LiquidnetStellarUserMapper` |
| Entity/DO | 驼峰命名 | `AdamRealName` |
| DTO/Param | `{Entity}Param` | `AdamThirdPartParam` |
| VO | `{Entity}Vo` | `AdamUserInfoVo` |
| 异常 | `{Name}Exception` | `LiquidnetServiceException` |
| 拦截器 | `{Name}Interceptor` | `GlobalAuthorityInterceptor` |
| Feign 客户端 | `Feign{Module}{Name}Client` | `FeignAdamBaseClient` |
| Redis 工具 | `Redis{服务名}Util` | `RedisAdamUtil` |
| 配置类 | `{Name}Config` | `SwaggerConfig` |
### 2.2 方法命名规范
```java
// ✅ 查询类
getUserById(), queryUserList(), findUserByPhone()
// ✅ 新增类
register(), addUser(), bindPhone()
// ✅ 更新类
edit(), editUserInfo(), updateStatus()
// ✅ 删除类
close(), unBindPhone(), deleteUser()
// ✅ 校验类
checkUserExists(), checkPermission(), validateToken()
// ❌ 禁止无意义命名
handle(), process(), doSomething() // 除非在极小作用域内
```
### 2.3 变量与常量
```java
// ✅ 常量:UPPER_SNAKE_CASE
public static final String TOKEN_ILLEGAL = "token_illegal";
public static final int MAX_RETRY_COUNT = 3;
// ✅ 变量:camelCase
String userName = currentUtil.getUserName();
int retryCount = 0;
// ❌ 禁止
public static final String token_illegal = "token_illegal"; // 常量未大写
String UserName = "test"; // 变量首字母大写
```
### 2.4 注解使用规范
```java
// ✅ Controller 注解
@RestController
@RequestMapping("/adam/user")
@Api(tags = "用户管理")
public class AdamUserController {
@Autowired
private IAdamUserService adamUserService;
@PostMapping("/get")
@ApiOperation("获取用户信息")
public ResponseDto<AdamUserInfoVo> getUser(@RequestBody @Valid AdamUserParam param) {
return adamUserService.getUserById(param);
}
}
// ✅ Service 注解
@Service
@Slf4j
public class AdamUserServiceImpl implements IAdamUserService {
// ...
}
// ✅ Entity 注解(Lombok)
@Data
@TableName("adam_user")
public class AdamUser {
@TableId(type = IdType.INPUT)
private Long id;
private String userName;
}
```
**Lombok 使用规则**:
- Entity/DTO/VO/Param 使用 `@Data`
- Service 实现类使用 `@Slf4j`,禁止手动 `new Logger`
- 构造器注入优先使用 `@RequiredArgsConstructor`,兼容项目内已有的 `@Autowired`
---
## 3. API 与数据流规范
### 3.1 Controller 编写规范
```java
// ✅ 正确:Controller 只做参数接收和转发,不写业务逻辑
@RestController
@RequestMapping("/kylin/show")
@Api(tags = "演出管理")
@Slf4j
public class KylinShowController {
@Autowired
private IKylinShowService kylinShowService;
@PostMapping("/detail")
@ApiOperation("演出详情")
public ResponseDto<KylinShowVo> getShowDetail(@RequestBody @Valid KylinShowParam param) {
return kylinShowService.getShowDetail(param);
}
}
// ❌ 错误:Controller 中编写业务逻辑
@PostMapping("/detail")
public ResponseDto getDetail(@RequestBody KylinShowParam param) {
// 禁止在 Controller 中写业务逻辑!
KylinShow show = showMapper.selectById(param.getId());
if (show.getStatus() == 1) {
show.setViews(show.getViews() + 1);
showMapper.updateById(show);
}
return ResponseDto.success(show);
}
```
**核心规则**:
- 所有返回值必须使用 `ResponseDto<T>` 封装
- 参数必须使用 `@Valid` 校验
- 禁止在 Controller 中注入 Mapper 或直接操作数据库
### 3.2 Service 编写规范
```java
// ✅ 正确:接口 + 实现分离,日志完善
@Service
@Slf4j
public class AdamUserServiceImpl implements IAdamUserService {
@Autowired
private LiquidnetStellarUserMapper userMapper;
@Override
public ResponseDto<AdamUserInfoVo> getUserById(AdamUserParam param) {
log.info("getUserById param: {}", JsonUtils.toJson(param));
AdamUser user = userMapper.selectById(param.getUserId());
if (user == null) {
return ResponseDto.failure(ErrorMapping.get("10011"));
}
AdamUserInfoVo vo = BeanConverterUtil.convert(user, AdamUserInfoVo.class);
return ResponseDto.success(vo);
}
}
// ❌ 错误:缺少日志、硬编码错误信息
@Override
public ResponseDto getUserById(AdamUserParam param) {
AdamUser user = userMapper.selectById(param.getUserId());
if (user == null) {
return ResponseDto.failure("10011", "用户不存在"); // 禁止硬编码错误信息
}
return ResponseDto.success(user); // 禁止直接返回 Entity
}
```
### 3.3 统一返回值 `ResponseDto<T>`
```java
// ✅ 成功
return ResponseDto.success(data);
// ✅ 失败(通过 ErrorMapping)
return ResponseDto.failure(ErrorMapping.get("10011"));
// ✅ 失败(自定义 code + message,仅在特殊场景)
return ResponseDto.failure(code, message);
// ❌ 禁止
return data; // 未封装
return new HashMap<>(); // 返回原始 Map
return ResponseEntity.ok(data); // 不使用 ResponseDto
```
### 3.4 错误码与异常处理
```java
// ✅ 正确:通过 ErrorMapping 获取错误码,抛出业务异常
ErrorMessage errorMessage = ErrorMapping.get("10011");
throw new LiquidnetServiceException(errorMessage.getCode(), errorMessage.getMessage());
// ✅ 正确:Feign 调用异常
throw new LiquidnetFeignException(code, message);
// ❌ 禁止
throw new RuntimeException("出错了"); // 禁止原生异常
throw new Exception("用户不存在"); // 禁止通用 Exception
throw new LiquidnetServiceException("10011"); // 禁止硬编码,应通过 ErrorMapping
```
**错误码前缀规范**:
| 前缀 | 业务域 |
|------|--------|
| SYS | 系统级 |
| SEV | 服务级 |
| PAY | 支付(dragon) |
| ACC | 账户(adam) |
| BAK | 后台 |
| TAK | 任务 |
| USR | 用户 |
### 3.5 Mapper/DAO 规范(仅用于查询)
> **核心原则**:Mapper 在本项目中 **仅用于数据查询**,所有数据写入(INSERT/UPDATE/DELETE)必须通过 SqlMapping + Redis Stream 异步完成。
```java
// ✅ Mapper 仅用于查询场景
AdamUser user = userMapper.selectById(userId);
List<AdamUser> users = userMapper.selectList(
new LambdaQueryWrapper<AdamUser>()
.eq(AdamUser::getStatus, 1)
.orderByDesc(AdamUser::getCreateTime)
);
// ✅ 复杂查询使用 XML 映射
@Mapper
public interface KylinShowMapper extends BaseMapper<KylinShow> {
List<KylinShowVo> selectShowWithTicket(@Param("showId") Long showId);
}
// ❌ 禁止通过 Mapper 执行写操作(INSERT/UPDATE/DELETE)
userMapper.insert(user); // 禁止!应通过 SqlMapping + MQ
userMapper.updateById(user); // 禁止!应通过 SqlMapping + MQ
userMapper.deleteById(userId); // 禁止!应通过 SqlMapping + MQ
// ❌ 禁止不检查 BaseMapper 就直接写自定义查询
@Select("SELECT * FROM adam_user WHERE id = #{id}")
AdamUser findById(Long id); // selectById 已提供,禁止重复
```
**Mapper 使用边界**:
- **允许**:`selectById`、`selectList`、`selectOne`、`selectCount`、`selectPage` 等查询方法
- **禁止**:`insert`、`update`、`updateById`、`delete`、`deleteById` 等写入方法
- 写入操作统一走 **SqlMapping + Redis Stream** 异步通道(见 3.6 节)
### 3.6 数据写入架构:SqlMapping + Redis Stream(核心规范)
> **本项目最核心的架构约定**:所有数据写入操作(INSERT / UPDATE / DELETE)不直接调用 Mapper,而是通过 `SqlMapping` 封装 SQL 语句,再经 `queueUtils.sendMsgByRedis()` 投递到 Redis Stream,由 consumer 端异步消费并执行写库。本地服务 **不使用 `@Transactional`**。
#### 3.6.1 架构流程
```
业务 Service
├── 1. 写入 Redis 缓存(保证接口即时响应)
├── 2. 构建 SqlMapping(封装 SQL + 参数)
└── 3. queueUtils.sendMsgByRedis(MQConst.XXX, sqlMapping)
Redis Stream(消息队列)
Consumer 消费者(liquidnet-bus-service 中的 consumer 模块)
└── 解析 SqlMapping → 执行实际 SQL → 写入 MySQL
```
#### 3.6.2 SqlMapping 构建规范
```java
// ===================== INSERT 示例 =====================
// 场景:新用户注册,写入 adam_user 表
SqlMapping sqlMapping = new SqlMapping();
// ⚠️ SQL 中所有动态值都用 ? 占位符,绝对不拼接变量
sqlMapping.setSql("INSERT INTO adam_user (id, user_name, phone, status, create_time) VALUES (?, ?, ?, ?, ?)");
List<Object> params = new ArrayList<>();
params.add(GenSnowFlowerUtil.nextId()); // id:雪花算法生成全局唯一ID
params.add(userParam.getUserName()); // user_name:来自入参
params.add(userParam.getPhone()); // phone:来自入参
params.add(1); // status:1=正常
params.add(DateUtil.getNowDate()); // create_time:当前时间
sqlMapping.setParams(params);
// 发送到 Redis Stream,由 consumer 异步执行 INSERT
queueUtils.sendMsgByRedis(MQConst.ADAM_USER_REGISTER, sqlMapping);
// ===================== UPDATE 示例 =====================
// 场景:修改用户状态
SqlMapping updateMapping = new SqlMapping();
updateMapping.setSql("UPDATE adam_user SET status = ?, update_time = ? WHERE id = ?");
List<Object> updateParams = new ArrayList<>();
updateParams.add(newStatus); // 第1个 ? → 新状态值
updateParams.add(DateUtil.getNowDate()); // 第2个 ? → 更新时间
updateParams.add(userId); // 第3个 ? → 用户ID
updateMapping.setParams(updateParams);
queueUtils.sendMsgByRedis(MQConst.ADAM_USER_UPDATE, updateMapping);
// ===================== DELETE(逻辑删除)示例 =====================
// 场景:关闭用户账号(逻辑删除:将 status 置为 0,而非物理删除)
SqlMapping deleteMapping = new SqlMapping();
deleteMapping.setSql("UPDATE adam_user SET status = 0, update_time = ? WHERE id = ?");
List<Object> delParams = new ArrayList<>();
delParams.add(DateUtil.getNowDate()); // 第1个 ? → 更新时间
delParams.add(userId); // 第2个 ? → 用户ID
deleteMapping.setParams(delParams);
queueUtils.sendMsgByRedis(MQConst.ADAM_USER_CLOSE, deleteMapping);
```
#### 3.6.3 缓存 + MQ 组合写入(标准模式)
```java
// ====================================================================
// ✅ 标准写入流程示例(可直接复制作为模板)
// 整体思路:先校验 → 生成ID → 写缓存 → 发MQ异步写库
// ====================================================================
public ResponseDto registerUser(AdamUserParam param) {
// ▶ STEP 1:入参日志记录,方便排查问题
log.info("registerUser param: {}", JsonUtils.toJson(param));
// ▶ STEP 2:业务校验(查询类用 Mapper)
ErrorMessage errMsg = checkUserExists(param.getPhone());
if (errMsg != null) {
return ResponseDto.failure(errMsg); // 校验不通过,直接返回错误
}
// ▶ STEP 3:生成全局唯一ID(雪花算法)
Long userId = GenSnowFlowerUtil.nextId();
// ▶ STEP 4:写入 Redis 缓存
// 目的:前端可以立即读取,不必等待 MQ 写库完成
// Key 命名规范:{服务名}:{业务}:{标识}
// 过期时间:必须设置,用 RedisKeyExpireConst 常量
String cacheKey = "adam:user:" + userId;
Map<String, Object> cacheData = new HashMap<>();
cacheData.put("id", userId);
cacheData.put("userName", param.getUserName());
cacheData.put("phone", param.getPhone());
redisAdamUtil.set(cacheKey, JsonUtils.toJson(cacheData), RedisKeyExpireConst.HOUR_24);
// ▶ STEP 5:构建 SqlMapping,封装 SQL + 参数
// ⚠️ 铁律:SQL 必须用 ? 占位符,禁止拼接变量
SqlMapping sqlMapping = new SqlMapping();
sqlMapping.setSql("INSERT INTO adam_user (id, user_name, phone, status, create_time) VALUES (?, ?, ?, ?, ?)");
List<Object> sqlParams = new ArrayList<>();
sqlParams.add(userId); // 第1个 ? → id
sqlParams.add(param.getUserName()); // 第2个 ? → user_name
sqlParams.add(param.getPhone()); // 第3个 ? → phone
sqlParams.add(1); // 第4个 ? → status(1=正常)
sqlParams.add(DateUtil.getNowDate()); // 第5个 ? → create_time
sqlMapping.setParams(sqlParams);
// ▶ STEP 6:发送到 Redis Stream 消息队列
// consumer 端会异步消费这条消息,执行实际 SQL 写入 MySQL
// 队列常量必须在 MQConst 中定义,禁止硬编码字符串
queueUtils.sendMsgByRedis(MQConst.ADAM_USER_REGISTER, sqlMapping);
// ▶ 返回成功(前端拿 userId 即可,DB 已异步写入)
return ResponseDto.success(userId);
}
```
#### 3.6.4 批量写入
```java
// ✅ 批量操作:构建多个 SqlMapping 逐条发送
for (AdamUserParam param : userList) {
SqlMapping sqlMapping = new SqlMapping();
sqlMapping.setSql("INSERT INTO adam_user (id, user_name, phone) VALUES (?, ?, ?)");
List<Object> params = new ArrayList<>();
params.add(GenSnowFlowerUtil.nextId());
params.add(param.getUserName());
params.add(param.getPhone());
sqlMapping.setParams(params);
queueUtils.sendMsgByRedis(MQConst.ADAM_USER_REGISTER, sqlMapping);
}
```
#### 3.6.5 禁止的写入方式
```java
// ❌ 禁止直接调用 Mapper 写库
userMapper.insert(user); // 禁止!
userMapper.updateById(user); // 禁止!
userMapper.deleteById(userId); // 禁止!
// ❌ 禁止使用 @Transactional 本地事务
@Transactional
public void createUser(AdamUser user) {
userMapper.insert(user); // 禁止!
logMapper.insert(operationLog); // 禁止!
}
// ❌ 禁止使用 Spring JDBC 直接写库
jdbcTemplate.update("INSERT INTO ...", params); // 禁止!
// ❌ 禁止绕过 MQ 直接拼接 SQL 执行
connection.createStatement().executeUpdate(sql); // 禁止!
```
#### 3.6.6 MQ 队列常量规范
```java
// ✅ 队列常量必须在 MQConst 中定义,按业务域分组前缀
public static final String ADAM_USER_REGISTER = "adam_user_register";
public static final String ADAM_USER_UPDATE = "adam_user_update";
public static final String KYLIN_SHOW_CREATE = "kylin_show_create";
public static final String DRAGON_PAY_CALLBACK = "dragon_pay_callback";
public static final String GOBLIN_ORDER_CREATE = "goblin_order_create";
// ❌ 禁止在业务代码中硬编码队列名
queueUtils.sendMsgByRedis("adam_user_register_queue", sqlMapping); // 禁止!
// ✅ 新增队列常量流程:
// 1. 在 MQConst 中按业务域前缀新增常量
// 2. 在对应 consumer 模块中新增消费方法监听该队列
// 3. 业务 Service 中引用该常量发送消息
```
#### 3.6.7 关键注意事项
- **写入不保证即时落库**:由于异步写入,前端读取应优先读 Redis 缓存
- **缓存与 DB 一致性**:consumer 写库成功后会更新/清理缓存,业务端无需额外处理
- **幂等性**:consumer 消费时需做幂等校验,防止 MQ 重试导致重复写入
- **日志**:发送 MQ 消息前后应打印关键日志,便于排查异步问题
#### 3.6.8 SQL 防注入规范(强制)
> **铁律**:SqlMapping 中的 SQL **必须使用 `?` 参数化占位符**,所有动态值通过 `setParams()` 传入。绝对禁止在 SQL 字符串中拼接任何变量或用户输入。
```java
// ═══════════════════ 正确写法:参数化占位符 ═══════════════════
// ✅ SELECT:查询用户(值通过 params 传入,不拼接)
SqlMapping sqlMapping = new SqlMapping();
sqlMapping.setSql("SELECT * FROM adam_user WHERE phone = ? AND status = ?");
List<Object> params = new ArrayList<>();
params.add(userPhone); // 第1个 ? → phone 值
params.add(status); // 第2个 ? → status 值
sqlMapping.setParams(params);
// ✅ INSERT:插入用户(所有字段都用 ? 占位符)
sqlMapping.setSql("INSERT INTO adam_user (id, user_name, phone) VALUES (?, ?, ?)");
List<Object> insertParams = new ArrayList<>();
insertParams.add(GenSnowFlowerUtil.nextId()); // id
insertParams.add(userName); // user_name
insertParams.add(phone); // phone
sqlMapping.setParams(insertParams);
// ✅ UPDATE:含 LIKE 查询时,通配符 % 拼在参数值中,而非 SQL 中
sqlMapping.setSql("UPDATE kylin_show SET title = ? WHERE title LIKE ?");
List<Object> updateParams = new ArrayList<>();
updateParams.add(newTitle); // 第1个 ? → 新标题
updateParams.add("%" + keyword + "%"); // 第2个 ? → LIKE 值(% 在这里拼接)
sqlMapping.setParams(updateParams);
// ═══════════════════ 禁止写法:字符串拼接(SQL 注入风险) ═══════════════════
// ❌ 致命错误:直接拼接用户输入变量
sqlMapping.setSql("SELECT * FROM adam_user WHERE phone = '" + userPhone + "'");
// ❌ 致命错误:INSERT 拼接变量
sqlMapping.setSql("INSERT INTO adam_user (id, user_name) VALUES (" + userId + ", '" + name + "')");
// ❌ 致命错误:UPDATE 拼接变量
sqlMapping.setSql("UPDATE adam_user SET status = " + status + " WHERE id = " + userId);
// ❌ 致命错误:DELETE 拼接变量
sqlMapping.setSql("DELETE FROM adam_user WHERE id = " + userId);
// ❌ 即使变量来自内部逻辑(非用户输入),也必须参数化
Long userId = CurrentUtil.getUid();
sqlMapping.setSql("SELECT * FROM adam_user WHERE id = " + userId); // 禁止!
// ✅ 正确写法:
sqlMapping.setSql("SELECT * FROM adam_user WHERE id = ?");
sqlMapping.setParams(Arrays.asList(userId));
```
**防注入自查清单**:
- SQL 字符串中不得出现 `" + variable + "` 形式的拼接
- 所有动态值(包括 ID、状态码、字符串、日期)必须通过 `params` 列表传入
- LIKE 查询的通配符 `%` 应拼接到参数值中,而非 SQL 字符串中
- 新增 SqlMapping 时,Code Review 必须检查 SQL 是否完全参数化
### 3.7 第 3 节总结:DO / DON’T
#### ✅ 必须做
- [ ] **写数据走 MQ**:`SqlMapping` + `queueUtils.sendMsgByRedis()` 异步写库
- [ ] **SQL 参数化**:`setSql()` 中用 `?` 占位符,值通过 `setParams()` 传入
- [ ] **查询用 Mapper**:`selectById` / `selectList` + `LambdaQueryWrapper`
- [ ] **返回值封装**:统一用 `ResponseDto.success(data)` / `ResponseDto.failure(msg)`
- [ ] **抛统一异常**:`new LiquidnetServiceException(msg.getCode(), msg.getMessage())`
- [ ] **错误码走映射**:`ErrorMapping.get("错误码")` 获取
- [ ] **Controller 只做转发**:接收参数 → 调用 Service → 返回结果
- [ ] **Service 接口+实现**:`IXxxService` + `XxxServiceImpl` + `@Slf4j`
#### ❌ 绝对禁止
- [ ] **Mapper 写库**:`mapper.insert()` / `mapper.updateById()` / `mapper.deleteById()`
- [ ] **SQL 拼接变量**:`"WHERE id = " + userId`(SQL 注入风险)
- [ ] **@Transactional**:项目不用本地事务
- [ ] **裸返数据**:直接返回 Entity / Map / `ResponseEntity`
- [ ] **原生异常**:`new RuntimeException()` / `new Exception()`
- [ ] **硬编码错误码**:`ResponseDto.failure("10011", "用户不存在")`
- [ ] **Controller 写业务**:在 Controller 中注入 Mapper、写 if/else 逻辑
---
## 4. 公共工具使用指南
### 4.1 核心工具类速查表
| 工具类 | 用途 | 典型用法 |
|--------|------|---------|
| `CurrentUtil` | 当前请求上下文 | `CurrentUtil.getToken()`, `CurrentUtil.getUid()`, `CurrentUtil.getIp()` |
| `JsonUtils` | JSON 序列化(Jackson) | `JsonUtils.toJson(obj)`, `JsonUtils.fromJson(json, Clazz.class)` |
| `BeanConverterUtil` | Bean 转换 | `BeanConverterUtil.convert(source, TargetClass.class)` |
| `BeanUtil` | Bean 反射/属性操作 | `BeanUtil.getProperty(obj, "name")` |
| `CollectionUtil` | 集合操作 | `CollectionUtil.isEmpty(list)`, `CollectionUtil.isNotEmpty(list)` |
| `DateUtil` | 日期处理(573 行,功能全面) | `DateUtil.format(date)`, `DateUtil.parse(str)` |
| `StringUtil` | 字符串工具 | `StringUtil.isEmpty(str)`, `StringUtil.isNotBlank(str)` |
| `HttpUtil` | HTTP 请求(RestTemplate + OkHttp) | `HttpUtil.get(url)`, `HttpUtil.post(url, body)` |
| `AESUtil` | AES 加解密 | `AESUtil.encrypt(plainText)`, `AESUtil.decrypt(cipherText)` |
| `MD5Utils` | MD5 哈希 | `MD5Utils.md5(text)` |
| `GenSnowFlowerUtil` | 雪花算法 ID 生成 | `GenSnowFlowerUtil.nextId()` |
| `IDGenerator` | ID 生成器 | `IDGenerator.generate()` |
| `SensitizeUtil` | 数据脱敏 | `SensitizeUtil.phone("13800138000")` → `138****8000` |
| `QRCodeUtil` | 二维码生成 | `QRCodeUtil.generate(content, width, height)` |
| `FilesUtils` | 文件操作 | 文件上传/下载/读取 |
| `EncodeUtil` | 编码工具 | URL/Base64 编解码 |
| `RandomUtil` | 随机数 | `RandomUtil.nextInt(min, max)` |
| `ValidationUtil` | 校验工具 | 参数校验 |
| `VersionCompareUtil` | 版本号比较 | `VersionCompareUtil.compare(v1, v2)` |
| `IDCardUtil` | 身份证校验 | `IDCardUtil.validate(idCard)` |
| `EmojiFilterUtil` | Emoji 过滤 | `EmojiFilterUtil.filter(str)` |
### 4.2 公共组件
| 组件 | 用途 |
|------|------|
| `AbstractRedisUtil` | Redis 操作基类(832 行),所有 Redis{服务名}Util 继承此类 |
| `PagedResult<T>` | 分页结果封装 |
| `ResponseDto<T>` | 统一 API 响应封装 |
| `JwtValidator` | JWT 令牌验证 |
| `IKeywordsFilter` / `KeywordsACFilter` | 敏感词过滤(AC 自动机) |
| `CommonConst` | 全局基础常量接口 |
| `LnsEnum` | 环境/开关枚举 |
| `LnsRegex` | 正则表达式常量 |
| `MQConst` | MQ 队列常量(按业务域) |
| `RedisKeyExpireConst` | Redis Key 过期时间常量 |
| `ErrorMapping` | 错误码映射(从 errors.properties 加载) |
| `ErrorCode` | 核心错误码枚举 |
### 4.3 禁止重复造轮子
以下场景 **必须** 使用现有工具类,禁止自行实现:
| 场景 | 必须使用 |
|------|---------|
| HTTP 请求 | `HttpUtil` 或 Feign 客户端 |
| JSON 序列化 | `JsonUtils`(禁止用 Gson 直接调) |
| 日期处理 | `DateUtil` |
| Bean 转换 | `BeanConverterUtil` |
| 集合判空 | `CollectionUtil.isEmpty()` |
| 字符串判空 | `StringUtil.isEmpty()` / `StringUtil.isNotBlank()` |
| ID 生成 | `GenSnowFlowerUtil` / `IDGenerator` |
| 数据脱敏 | `SensitizeUtil` |
| Redis 操作 | `Redis{服务名}Util`(继承 `AbstractRedisUtil`) |
| 敏感词过滤 | `KeywordsACFilter` |
---
## 5. 避坑指南(Anti-Patterns)
### 5.1 网络请求
```java
// ❌ 禁止使用原生 HttpClient / OkHttp 直接调用
HttpClient client = HttpClient.newHttpClient();
client.send(request, BodyHandlers.ofString());
// ❌ 禁止引入 Axios 等前端 HTTP 库
// ✅ 使用项目封装的 HttpUtil
String result = HttpUtil.get(url);
String result = HttpUtil.post(url, jsonBody);
// ✅ 跨服务调用使用 Feign
@Autowired
private FeignAdamBaseClient feignAdamBaseClient;
ResponseDto<UserDto> result = feignAdamBaseClient.getUser(userId);
```
### 5.2 Redis 操作
```java
// ❌ 禁止直接操作 Redis 连接
@Autowired
private StringRedisTemplate redisTemplate;
redisTemplate.opsForValue().set(key, value);
// ✅ 必须通过 Redis{服务名}Util 操作
@Autowired
private RedisAdamUtil redisAdamUtil;
redisAdamUtil.set(key, value, RedisKeyExpireConst.HOUR_1);
```
### 5.3 异常处理
```java
// ❌ 禁止直接 new 原生异常
throw new RuntimeException("参数错误");
throw new Exception("系统异常");
throw new IllegalArgumentException("非法参数");
// ✅ 必须使用项目统一异常
ErrorMessage errorMessage = ErrorMapping.get("10011");
throw new LiquidnetServiceException(errorMessage.getCode(), errorMessage.getMessage());
// ✅ Feign 调用场景
throw new LiquidnetFeignException(code, message);
```
### 5.4 事务与写入
> 本项目采用 Redis + MQ 异步写库架构,**禁止任何本地事务操作**。
```java
// ═══════════════════ 禁止写法 ═══════════════════
// ❌ 禁止使用 @Transactional(项目不用本地事务)
@Transactional
public void createOrder(OrderParam param) {
orderMapper.insert(order); // 禁止!Mapper 不允许写库
stockMapper.updateStock(stockId); // 禁止!Mapper 不允许写库
}
// ❌ 禁止通过 Mapper 执行写操作
userMapper.insert(user); // 禁止!
orderMapper.updateById(order); // 禁止!
// ═══════════════════ 正确写法 ═══════════════════
// ✅ 所有写入必须通过 SqlMapping + Redis Stream
public void createOrder(OrderParam param) {
// 1. 生成ID
Long orderId = GenSnowFlowerUtil.nextId();
// 2. 构建 SqlMapping(SQL 必须用 ? 占位符)
SqlMapping sqlMapping = new SqlMapping();
sqlMapping.setSql("INSERT INTO orders (id, user_id, amount, status, create_time) VALUES (?, ?, ?, ?, ?)");
List<Object> params = new ArrayList<>();
params.add(orderId); // id
params.add(CurrentUtil.getUid()); // user_id:从当前请求上下文获取
params.add(orderParam.getAmount()); // amount:订单金额
params.add(1); // status:1=待支付
params.add(DateUtil.getNowDate()); // create_time
sqlMapping.setParams(params);
// 3. 先写缓存(前端可立即查询)
redisUtil.set("order:" + orderId, orderCache, RedisKeyExpireConst.HOUR_24);
// 4. 发 MQ 异步写库(consumer 端执行实际 INSERT)
queueUtils.sendMsgByRedis(MQConst.ORDER_CREATE, sqlMapping);
}
```
### 5.5 错误码
```java
// ❌ 禁止硬编码错误码和错误信息
return ResponseDto.failure("10011", "用户不存在");
throw new LiquidnetServiceException("10011", "用户不存在");
// ✅ 必须通过 ErrorMapping 获取
ErrorMessage msg = ErrorMapping.get("10011");
return ResponseDto.failure(msg);
throw new LiquidnetServiceException(msg.getCode(), msg.getMessage());
```
### 5.6 Controller 职责
```java
// ❌ 禁止在 Controller 中写业务逻辑
@PostMapping("/create")
public ResponseDto create(@RequestBody UserParam param) {
User user = new User();
user.setName(param.getName());
user.setPhone(param.getPhone());
userMapper.insert(user); // 禁止直接操作 Mapper
return ResponseDto.success(user);
}
// ✅ Controller 只做转发
@PostMapping("/create")
@ApiOperation("创建用户")
public ResponseDto<Long> create(@RequestBody @Valid UserParam param) {
return userService.createUser(param);
}
```
### 5.7 返回值封装
```java
// ❌ 禁止不封装直接返回
@GetMapping("/user")
public AdamUser getUser() {
return userMapper.selectById(id);
}
// ❌ 禁止返回 Entity(应转换为 VO)
return ResponseDto.success(userMapper.selectById(id));
// ✅ 正确封装 + 转换 VO
ResponseDto<AdamUserInfoVo> vo = ResponseDto.success(
BeanConverterUtil.convert(user, AdamUserInfoVo.class)
);
return vo;
```
### 5.8 日志规范
```java
// ❌ 禁止使用 System.out / e.printStackTrace()
System.out.println("用户ID: " + userId);
e.printStackTrace();
// ❌ 禁止手动创建 Logger
private static final Logger log = LoggerFactory.getLogger(XxxServiceImpl.class);
// ✅ 使用 @Slf4j + 占位符
@Slf4j
public class XxxServiceImpl {
public void doSomething(Long userId) {
log.info("doSomething userId: {}", userId);
try {
// ...
} catch (Exception e) {
log.error("doSomething error, userId: {}", userId, e);
}
}
}
```
### 5.9 第 5 节总结:DO / DON’T
#### ✅ 必须做
- [ ] **HTTP 用封装工具**:`HttpUtil.get(url)` / `HttpUtil.post(url, body)` 或 Feign
- [ ] **Redis 用专属工具**:`Redis{服务名}Util.set(key, value, expire)`
- [ ] **抛统一异常**:`new LiquidnetServiceException(msg.getCode(), msg.getMessage())`
- [ ] **写库走 MQ**:`SqlMapping` + `queueUtils.sendMsgByRedis()` 异步写入
- [ ] **错误码走映射**:`ErrorMapping.get("错误码")` 获取
- [ ] **Controller 只做转发**:接收参数 → 调用 Service → 返回结果
- [ ] **返回值封装**:`ResponseDto<T>` + VO 转换
- [ ] **日志用 @Slf4j**:`log.info("xxx: {}", value)` + `log.error("xxx", e)`
#### ❌ 绝对禁止
- [ ] **原生 HTTP 客户端**:`HttpClient` / `OkHttpClient` 直接调用
- [ ] **直接注入 RedisTemplate**:`@Autowired StringRedisTemplate`
- [ ] **原生异常**:`new RuntimeException()` / `new Exception()` / `new IllegalArgumentException()`
- [ ] **Mapper 写库**:`mapper.insert()` / `mapper.updateById()`
- [ ] **@Transactional**:项目不用本地事务
- [ ] **硬编码错误码**:`ResponseDto.failure("10011", "用户不存在")`
- [ ] **Controller 写业务**:注入 Mapper、写 if/else 逻辑
- [ ] **裸返数据**:直接返回 Entity / Map
- [ ] **System.out / printStackTrace**:`System.out.println()` / `e.printStackTrace()`
- [ ] **手动创建 Logger**:`LoggerFactory.getLogger(Xxx.class)`
---
## 6. 微服务通信规范
### 6.1 Feign 调用规范
```java
// ═══════════════════ Feign 客户端定义 ═══════════════════
// ✅ Feign 客户端命名规则:Feign{Module}{Name}Client
// 定义在 liquidnet-bus-feign/{服务名}-feign/ 模块中
@FeignClient(name = "liquidnet-service-adam", fallbackFactory = FeignAdamBaseClientFallback.class)
public interface FeignAdamBaseClient {
@PostMapping("/adam/base/getUser")
ResponseDto<AdamUserDto> getUser(@RequestParam("userId") Long userId);
}
// ═══════════════════ Fallback 降级实现 ═══════════════════
// ✅ Fallback 必须实现,保证服务不可用时给出友好响应
@Component
@Slf4j
public class FeignAdamBaseClientFallback implements FallbackFactory<FeignAdamBaseClient> {
@Override
public FeignAdamBaseClient create(Throwable cause) {
// 记录异常原因,便于排查
log.error("FeignAdamBaseClient fallback, cause: {}", cause.getMessage());
// 返回系统级错误,而非抛出异常
return userId -> ResponseDto.failure(ErrorCode.SYS_ERROR_SYS.getCode(), "服务暂不可用");
}
}
// ═══════════════════ 调用示例 ═══════════════════
// ✅ 安全头注入由 SecuringRequestInterceptor 自动处理,无需手动传 Token
@Autowired
private FeignAdamBaseClient feignAdamBaseClient;
public void someBusinessMethod(Long userId) {
ResponseDto<AdamUserDto> result = feignAdamBaseClient.getUser(userId);
if ("0".equals(result.getCode())) {
AdamUserDto user = result.getData();
// 处理业务逻辑...
}
}
```
**规则**:
- Feign 接口定义在 `liquidnet-bus-feign/{服务名}-feign/` 模块中
- 入参/出参类型定义在 `liquidnet-bus-api/{服务名}-api/` 模块中
- 必须提供 Fallback 或 FallbackFactory 实现降级
- 返回值统一使用 `ResponseDto<T>`
### 6.2 MQ 消息规范
```java
// ✅ 使用 MQConst 中定义的队列常量
queueUtils.sendMsgByRedis(MQConst.ADAM_USER_REGISTER, sqlMapping);
// ✅ 队列命名遵循业务域前缀:adam_ / kylin_ / goblin_ / dragon_ 等
// ✅ 消息体使用 SqlMapping 封装 SQL 操作
// ❌ 禁止硬编码队列名
queueUtils.sendMsgByRedis("adam_user_register_queue", data); // 禁止!
// ❌ 禁止在 MQ 消费者中抛出未捕获异常
```
**双 MQ 架构**:
- **Redis Stream**(主要):业务数据写入、常规异步处理
- **RabbitMQ**(辅助):特定场景消息队列
### 6.3 Redis 缓存使用规范
```java
// ✅ Key 命名规范:{服务名}:{业务}:{标识}
String key = "adam:user:" + userId;
redisAdamUtil.set(key, userInfo, RedisKeyExpireConst.HOUR_1);
// ✅ 过期时间使用 RedisKeyExpireConst 常量
redisAdamUtil.set(key, value, RedisKeyExpireConst.MINUTE_30);
redisAdamUtil.set(key, value, RedisKeyExpireConst.HOUR_1);
redisAdamUtil.set(key, value, RedisKeyExpireConst.DAY_7);
// ✅ 缓存更新策略:先更新缓存,再发 MQ 写库
redisAdamUtil.set(key, newValue, expire);
queueUtils.sendMsgByRedis(queueName, sqlMapping);
// ❌ 禁止不设过期时间(防止内存泄漏)
redisAdamUtil.set(key, value); // 无过期时间,禁止!
// ❌ 禁止 Key 命名无规范
redisAdamUtil.set("test123", value, expire); // 无业务含义,禁止!
```
### 6.4 第 6 节总结:DO / DON’T
#### ✅ 必须做
- [ ] **Feign 命名规范**:`Feign{Module}{Name}Client`,如 `FeignAdamBaseClient`
- [ ] **Feign 必须降级**:实现 `FallbackFactory`,返回 `ResponseDto.failure()`
- [ ] **MQ 用常量队列**:`queueUtils.sendMsgByRedis(MQConst.XXX, sqlMapping)`
- [ ] **MQ 消费幂等**:consumer 端做幂等校验 + 异常捕获
- [ ] **Redis Key 规范**:`{服务名}:{业务}:{标识}` + `RedisKeyExpireConst` 过期时间
- [ ] **缓存优先写**:先写 Redis 缓存,再发 MQ 异步写库
- [ ] **Feign 返回值**:统一用 `ResponseDto<T>` 封装
#### ❌ 绝对禁止
- [ ] **Feign 无降级**:不提供 Fallback / FallbackFactory
- [ ] **手动传 Token**:`SecuringRequestInterceptor` 已自动处理安全头
- [ ] **硬编码队列名**:`queueUtils.sendMsgByRedis("adam_user_register", data)`
- [ ] **消费端抛异常**:consumer 中未捕获异常导致消息丢失
- [ ] **Redis 无过期时间**:`redisUtil.set(key, value)` 不设置 expire
- [ ] **Redis Key 无含义**:`redisUtil.set("test123", value, expire)`
- [ ] **只写库不写缓存**:跳过 Redis 缓存直接发 MQ
---
## 7. 拦截器/过滤器体系
### 7.1 层级职责
| 层级 | 组件 | 职责 | 何时修改 |
|------|------|------|---------|
| 网关层 | `GlobalAuthFilter`(Zuul) | 全局 JWT 鉴权、路由过滤 | 网关规则变更 |
| 服务层 | `GlobalLogTrackInterceptor` | 请求耗时、链路追踪、X-Server 响应头 | 监控需求 |
| 服务层 | `GlobalAuthorityInterceptor` | JWT 鉴权 + SSO + Token 校验 | 通用鉴权逻辑 |
| 服务层 | `AdamAuthorityInterceptor` | adam 服务专属鉴权 | adam 鉴权规则 |
| 服务层 | `KylinAuthorityInterceptor` | kylin 服务专属鉴权 | kylin 鉴权规则 |
| Feign 层 | `SecuringRequestInterceptor` | Feign 调用安全头注入 | Feign 安全策略 |
### 7.2 响应头规范
- `X-Server` 响应头由 `GlobalLogTrackInterceptor` 从 `POD_NAME` 环境变量获取并注入
- 复用全局拦截器处理响应头,禁止在业务代码中手动设置
---
## 8. 配置管理规范
### 8.1 配置文件命名
```
liquidnet-bus-config/liquidnet-config/
├── application.yml -- 全局公共
├── application-common-service.yml -- 服务公共配置
├── application-{env}.yml -- 环境公共(dev/test/prod)
├── liquidnet-service-{服务名}.yml -- 服务基础配置
├── liquidnet-service-{服务名}-{env}.yml -- 服务 + 环境配置
├── liquidnet-client-admin-web.yml -- admin 客户端
└── liquidnet-client-job.yml -- 定时任务
```
### 8.2 配置规则
- 所有配置集中在 `liquidnet-bus-config` 模块管理
- 新增服务必须创建对应的 `{服务名}.yml` 和各环境配置
- 敏感信息(密码、密钥)禁止明文写入配置文件
- 环境区分:`dev` / `test` / `test2` / `yace` / `prod`
---
## 快速参考 / 每日速查
> 本节为日常开发速查卡片,可直接复制模板代码使用。
### 写数据(INSERT / UPDATE / DELETE)
```
① 校验参数 → ② 生成ID → ③ 写Redis缓存 → ④ 构建SqlMapping(必须用?占位符) → ⑤ queueUtils.sendMsgByRedis(MQConst.XXX, sqlMapping)
```
```java
// 一句话模板:
SqlMapping sql = new SqlMapping();
sql.setSql("INSERT INTO 表 (字段...) VALUES (?, ?, ?)"); // 必须用 ? 占位符
List<Object> params = new ArrayList<>();
params.add(值1); params.add(值2); params.add(值3);
sql.setParams(params);
queueUtils.sendMsgByRedis(MQConst.队列常量, sql);
```
### 查数据(SELECT)
```java
// 用 Mapper 查询,优先用 BaseMapper 自带方法
AdamUser user = userMapper.selectById(userId);
List<AdamUser> list = userMapper.selectList(new LambdaQueryWrapper<AdamUser>().eq(...));
```
### Controller 模板
```java
@RestController @RequestMapping("/{服务}/{模块}") @Api(tags = "XXX管理")
public class XxxController {
@Autowired private IXxxService xxxService;
@PostMapping("/get") @ApiOperation("查询")
public ResponseDto<XxxVo> getXxx(@RequestBody @Valid XxxParam param) {
return xxxService.getXxx(param); // 只做转发,不写业务逻辑
}
}
```
### 返回与异常
```java
return ResponseDto.success(data); // 成功
return ResponseDto.failure(ErrorMapping.get("错误码")); // 失败
throw new LiquidnetServiceException(msg.getCode(), msg.getMessage()); // 抛异常
```
### 10 条铁律
| # | 规则 | 说明 |
|---|------|------|
| 1 | **写数据走 MQ** | INSERT/UPDATE/DELETE 必须用 SqlMapping + Redis Stream,禁止 Mapper 写库 |
| 2 | **SQL 必须参数化** | SqlMapping.setSql() 中必须用 `?` 占位符,禁止拼接变量 |
| 3 | **禁止 @Transactional** | 项目不用本地事务,用缓存 + MQ 异步写库 |
| 4 | **返回值必须封装** | 统一用 `ResponseDto<T>`,禁止裸返 Entity / Map |
| 5 | **异常用统一类** | 用 `LiquidnetServiceException` + `ErrorMapping`,禁止 `new RuntimeException` |
| 6 | **Controller 只做转发** | 禁止在 Controller 中写业务逻辑或注入 Mapper |
| 7 | **Redis 用专属工具** | 必须用 `Redis{服务名}Util`,禁止直接注入 RedisTemplate |
| 8 | **HTTP 用 HttpUtil / Feign** | 禁止原生 HttpClient / OkHttp |
| 9 | **日志用 @Slf4j** | 禁止 System.out / e.printStackTrace() / 手动 Logger |
| 10 | **错误码走 ErrorMapping** | 禁止硬编码错误码字符串 |
-- 演出票退款原因配置表 + 退款单归因字段
CREATE TABLE IF NOT EXISTS `kylin_order_refund_reasons` (
`mid` int(11) NOT NULL AUTO_INCREMENT,
`reason_id` varchar(32) NOT NULL DEFAULT '' COMMENT '业务ID',
`name` varchar(100) NOT NULL DEFAULT '' COMMENT '退款理由名称',
`need_detail` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否需要二级补充输入 0否 1是',
`placeholder` varchar(200) NOT NULL DEFAULT '' COMMENT '二级输入placeholder',
`is_applied` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否已应用 0未应用 1应用中',
`sort` int(11) NOT NULL DEFAULT '0' COMMENT '排序,越大越靠前',
`is_deleted` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否删除 0未删除 1已删除',
`created_at` datetime DEFAULT NULL,
`updated_at` datetime DEFAULT NULL,
PRIMARY KEY (`mid`),
UNIQUE KEY `uk_reason_id` (`reason_id`),
KEY `idx_is_applied_sort` (`is_applied`, `sort`),
KEY `idx_is_deleted` (`is_deleted`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='演出票退款原因配置';
-- 已建表环境补逻辑删除字段(若已存在可忽略报错)
ALTER TABLE `kylin_order_refund_reasons`
ADD COLUMN `is_deleted` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否删除 0未删除 1已删除' AFTER `sort`;
ALTER TABLE `kylin_order_refund_reasons`
ADD KEY `idx_is_deleted` (`is_deleted`);
ALTER TABLE `kylin_order_refunds`
ADD COLUMN `reason_id` varchar(32) DEFAULT NULL COMMENT '退款原因配置ID' AFTER `reason`,
ADD COLUMN `reason_detail` varchar(200) DEFAULT NULL COMMENT '退款原因二级补充说明' AFTER `reason_id`;
-- 演出票退款原因管理 - 菜单与权限
-- 挂载在:正在映画 → 退款管理 下面(与「订单退款列表」「演出退款管理」同级)
SET @zhengzaiRootId = (
SELECT menu_id
FROM sys_menu
WHERE menu_name = '正在映画'
AND parent_id = 0
LIMIT 1
);
SET @refundManageId = (
SELECT menu_id
FROM sys_menu
WHERE menu_name = '退款管理'
AND parent_id = @zhengzaiRootId
LIMIT 1
);
INSERT INTO sys_menu (menu_name, parent_id, order_num, url, target, menu_type, visible, perms, icon, create_by, create_time, update_by, update_time, remark)
VALUES ('退款原因管理', @refundManageId, 2, '/kylin/refundReasons', 'menuItem', 'C', '0', 'kylin:refundReasons:list', '#', 'admin', NOW(), '', NULL, '演出票退款原因配置');
SET @menuId = LAST_INSERT_ID();
INSERT INTO sys_menu (menu_name, parent_id, order_num, url, target, menu_type, visible, perms, icon, create_by, create_time, update_by, update_time, remark)
VALUES
('退款原因查询', @menuId, 1, '#', '', 'F', '0', 'kylin:refundReasons:list', '#', 'admin', NOW(), '', NULL, ''),
('退款原因新增', @menuId, 2, '#', '', 'F', '0', 'kylin:refundReasons:create', '#', 'admin', NOW(), '', NULL, ''),
('退款原因修改', @menuId, 3, '#', '', 'F', '0', 'kylin:refundReasons:update', '#', 'admin', NOW(), '', NULL, ''),
('退款原因删除', @menuId, 4, '#', '', 'F', '0', 'kylin:refundReasons:delete', '#', 'admin', NOW(), '', NULL, ''),
('退款原因应用', @menuId, 5, '#', '', 'F', '0', 'kylin:refundReasons:apply', '#', 'admin', NOW(), '', NULL, '');
package com.liquidnet.service.kylin.constant;
/**
* 演出票拼团状态常量,覆盖活动、订单、成员三类状态机。
*/
public class GroupBuyStatusConst {
private GroupBuyStatusConst() {
}
/**
* 拼团活动状态:草稿 → 待启动 → 进行中 ⇄ 已关闭 → 已结束。
*/
public static class ActivityStatus {
/** 草稿(未提交) */
public static final Integer DRAFT = 0;
/** 待启动(手动/定时) */
public static final Integer PENDING = 1;
/** 进行中(用户可发起/加入拼团) */
public static final Integer ACTIVE = 2;
/** 已关闭(B端手动关停,可重新开启) */
public static final Integer CLOSED = 3;
/** 已结束(到结束时间,终态) */
public static final Integer ENDED = 4;
private ActivityStatus() {
}
}
/**
* 启动方式。
*/
public static class LaunchType {
/** 立即启动:保存后活动立即生效 */
public static final Integer IMMEDIATE = 1;
/** 手动启动:保存后状态=待启动,B端手动点启动 */
public static final Integer MANUAL = 2;
/** 定时启动:保存后状态=待启动,到定时时间自动激活 */
public static final Integer SCHEDULED = 3;
private LaunchType() {
}
}
/**
* 优惠类型。
*/
public static class DiscountType {
/** 固定价格(元) */
public static final Integer FIXED_PRICE = 1;
/** 折扣(1-100,如85表示85折) */
public static final Integer DISCOUNT = 2;
/** 满减(元) */
public static final Integer REDUCE = 3;
private DiscountType() {
}
}
/**
* 拼团订单状态:预占 → 拼单中 → 拼成 / 失败 / 已取消。
*/
public static class OrderStatus {
/** 待支付发起(团长已创建拼团但尚未下单) */
public static final Integer PREPARE = -1;
/** 拼单中(团长已支付,等待成员加入) */
public static final Integer PENDING = 0;
/** 拼成(达到目标人数) */
public static final Integer SUCCESS = 1;
/** 失败(24h超时未成团) */
public static final Integer FAILED = 2;
/** 已取消(团长手动取消) */
public static final Integer CANCELLED = 3;
private OrderStatus() {
}
}
/**
* 公开/私密标记。
*/
public static class PublicFlag {
/** 私密(不进公开轮播,仅通过 shareToken 加入) */
public static final Integer PRIVATE = 0;
/** 公开(进入轮播列表) */
public static final Integer PUBLIC = 1;
private PublicFlag() {
}
}
/**
* 拼团成员状态:下单后待支付 → 已加入 → 已退款。
*/
public static class MemberStatus {
/** 待支付(已下单但未支付/支付中) */
public static final Integer PENDING_PAY = -1;
/** 已加入(支付成功) */
public static final Integer JOINED = 0;
/** 已退款(拼单中退款/拼成后退款) */
public static final Integer REFUNDED = 1;
/** 已取消(支付超时/用户取消订单) */
public static final Integer CANCELLED = 2;
private MemberStatus() {
}
}
}
......@@ -36,6 +36,16 @@ public class KylinRedisConst {
public static final String ORDER_REFUND_BY_ORDER_ID = "kylin:order:refund:orderId:";
public static final String ORDER_REFUND = "kylin:order:refund:orderRefundsId:";
//拼团相关开始
public static final String GROUP_BUY_ACTIVITY = "kylin:groupBuy:activity:id:";
public static final String GROUP_BUY_ORDER = "kylin:groupBuy:order:id:";
public static final String GROUP_BUY_ORDER_LOCK = "kylin:groupBuy:lock:";
public static final String GROUP_BUY_ORDER_SHARE = "kylin:groupBuy:order:share:";
/** 拼团短信总开关:无 key 或非 0/false 则发送;SET 为 0/false 关停 */
public static final String GROUP_BUY_SMS_ENABLED = "kylin:groupBuy:sms:enabled";
/** 拼团 1h 提醒防重:key + groupOrderId */
public static final String GROUP_BUY_REMIND_1H = "kylin:groupBuy:remind1h:";
public static final String CAMERA_DEVICES = "kylin:cameraDevices:list";
public static final String CAMERA_DEVICES_FIELD = "kylin:cameraDevices:fieldId:";
public static final String CAMERA_DEVICE_GB_INFO = "kylin:cameraDevices:gbId:";
......@@ -96,6 +106,11 @@ public class KylinRedisConst {
public static final String KYLIN_EXPRESS_MODULES = "kylin:express:modules";
/**
* 已应用的演出票退款原因列表
*/
public static final String KYLIN_ORDER_REFUND_REASONS_APPLIED = "kylin:order:refund:reasons:applied";
public static final String REDIS_KEY_KYLIN_PERFORM_SUBSCRIBE = "kylin:performSubscribe:uid:";
public static final String REDIS_KEY_KYLIN_PERFORMANCE_SUBSCRIBE = "kylin:performance:subscribe:"; //演出预约
......
package com.liquidnet.service.kylin.dto.param;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import javax.validation.Valid;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.List;
/**
* 创建拼团活动请求参数(不含 activityId)。
*/
@Data
@ApiModel(value = "KylinGroupBuyActivityCreateParam", description = "创建拼团活动")
public class KylinGroupBuyActivityCreateParam implements Serializable {
private static final long serialVersionUID = 1L;
@ApiModelProperty(value = "活动名称", required = true)
@NotNull(message = "活动名称不能为空")
private String name;
@ApiModelProperty(value = "关联演出ID", required = true)
@NotNull(message = "关联演出ID不能为空")
private String performanceId;
@ApiModelProperty(value = "成团人数", required = true)
@NotNull(message = "成团人数不能为空")
private Integer minGroupSize;
@ApiModelProperty(value = "每人限购", required = true)
@NotNull(message = "每人限购不能为空")
private Integer maxBuyPerUser;
@ApiModelProperty(value = "活动开始时间,格式 yyyy-MM-dd HH:mm:ss", required = true, example = "2026-07-17 00:00:00")
@NotNull(message = "活动开始时间不能为空")
private String startTime;
@ApiModelProperty(value = "活动结束时间,格式 yyyy-MM-dd HH:mm:ss", required = true, example = "2026-12-31 23:59:59")
@NotNull(message = "活动结束时间不能为空")
private String endTime;
@ApiModelProperty(value = "启动方式 1立即 2手动 3定时", required = true)
@NotNull(message = "启动方式不能为空")
private Integer launchType;
@ApiModelProperty(value = "定时启动时间,格式 yyyy-MM-dd HH:mm:ss(launchType=3 时必填)", example = "2026-07-18 10:00:00")
private String scheduledTime;
@ApiModelProperty(value = "活动说明")
private String description;
@Valid
@NotEmpty(message = "票种优惠不能为空")
@ApiModelProperty(value = "票种优惠配置", required = true)
private List<DiscountParam> discounts;
@Data
@ApiModel(value = "KylinGroupBuyActivityDiscountParam", description = "拼团票种优惠")
public static class DiscountParam implements Serializable {
private static final long serialVersionUID = 1L;
@ApiModelProperty(value = "场次ID", required = true)
@NotNull(message = "场次ID不能为空")
private String timeId;
@ApiModelProperty(value = "票种ID", required = true)
@NotNull(message = "票种ID不能为空")
private String ticketId;
@ApiModelProperty(value = "优惠类型 1固定价格 2折扣 3满减", required = true)
@NotNull(message = "优惠类型不能为空")
private Integer discountType;
@ApiModelProperty(value = "优惠值", required = true)
@NotNull(message = "优惠值不能为空")
private BigDecimal discountValue;
}
}
package com.liquidnet.service.kylin.dto.param;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import javax.validation.constraints.NotNull;
/**
* 编辑拼团活动请求参数(必传 activityId)。
*/
@Data
@EqualsAndHashCode(callSuper = true)
@ApiModel(value = "KylinGroupBuyActivityUpdateParam", description = "编辑拼团活动")
public class KylinGroupBuyActivityUpdateParam extends KylinGroupBuyActivityCreateParam {
private static final long serialVersionUID = 1L;
@ApiModelProperty(value = "活动ID", required = true)
@NotNull(message = "活动ID不能为空")
private String activityId;
}
package com.liquidnet.service.kylin.dto.param;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;
import java.io.Serializable;
import java.math.BigDecimal;
@Data
public class KylinGroupBuyCreateParam implements Serializable {
private static final long serialVersionUID = 1L;
@ApiModelProperty(value = "拼团活动ID")
@NotNull(message = "拼团活动ID不能为空")
private String activityId;
@ApiModelProperty(value = "演出ID")
@NotNull(message = "演出ID不能为空")
private String performanceId;
@ApiModelProperty(value = "场次ID")
@NotNull(message = "场次ID不能为空")
private String timeId;
@ApiModelProperty(value = "票种ID")
@NotNull(message = "票种ID不能为空")
private String ticketId;
@ApiModelProperty(value = "购票张数")
@NotNull(message = "购票张数不能为空")
@Min(value = 1, message = "购票张数不能小于1")
private Integer ticketCount;
@ApiModelProperty(value = "是否公开 1公开 0私密,默认公开")
private Integer isPublic;
@ApiModelProperty(value = "前端展示拼团单价,仅用于服务端比对")
private BigDecimal groupBuyDisplayPrice;
}
package com.liquidnet.service.kylin.dto.param;
import com.liquidnet.service.kylin.entity.KylinOrderRefundReasons;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import org.springframework.beans.BeanUtils;
import java.time.LocalDateTime;
import java.util.List;
/**
* 退款原因配置参数
*/
@Data
@ApiModel("退款原因配置参数")
public class OrderRefundReasonParam {
@ApiModelProperty(value = "原因业务ID")
private String reasonId;
@ApiModelProperty(value = "退款理由名称")
private String name;
@ApiModelProperty(value = "是否需要二级补充输入 0否 1是")
private Integer needDetail;
@ApiModelProperty(value = "二级输入placeholder")
private String placeholder;
@ApiModelProperty(value = "是否已应用 0未应用 1应用中")
private Integer isApplied;
@ApiModelProperty(value = "排序")
private Integer sort;
@ApiModelProperty(value = "批量操作ID列表")
private List<String> ids;
public KylinOrderRefundReasons getFields(String reasonId, LocalDateTime createdAt) {
KylinOrderRefundReasons entity = new KylinOrderRefundReasons();
BeanUtils.copyProperties(this, entity);
if (reasonId != null) {
entity.setReasonId(reasonId);
}
if (createdAt != null) {
entity.setCreatedAt(createdAt);
}
if (entity.getNeedDetail() == null) {
entity.setNeedDetail(0);
}
if (entity.getPlaceholder() == null) {
entity.setPlaceholder("");
}
if (entity.getIsApplied() == null) {
entity.setIsApplied(0);
}
if (entity.getIsDeleted() == null) {
entity.setIsDeleted(0);
}
if (entity.getSort() == null) {
entity.setSort(0);
}
if (entity.getNeedDetail() != null && entity.getNeedDetail() == 0) {
entity.setPlaceholder("");
}
entity.setUpdatedAt(LocalDateTime.now());
return entity;
}
}
package com.liquidnet.service.kylin.dto.param;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
/**
* 退款原因配置搜索参数
*/
@Data
@ApiModel("退款原因配置搜索参数")
public class OrderRefundReasonSearchParam {
@ApiModelProperty(value = "退款理由名称")
private String name;
@ApiModelProperty(value = "是否已应用 0未应用 1应用中")
private Integer isApplied;
@ApiModelProperty(value = "页码")
private Integer pageNum = 1;
@ApiModelProperty(value = "每页数量")
private Integer pageSize = 10;
}
......@@ -71,4 +71,17 @@ public class PayOrderParam {
private String referrerApp;
@ApiModelProperty(value = "粉丝俱乐部侧用户id")
private String referrerUserId;
@ApiModelProperty(value = "拼团订单ID,加入拼团时传")
private String groupOrderId;
@ApiModelProperty(value = "拼团活动ID,发起拼团时传")
private String groupBuyActivityId;
@ApiModelProperty(value = "是否公开拼团 1公开 0私密,发起时传,默认1")
private Integer isPublic;
@ApiModelProperty(value = "分享token,私密团加入时传")
private String shareToken;
}
......@@ -23,6 +23,16 @@ public class RefundApplyParam implements Serializable {
private String reason;
/**
* 退款原因配置ID
*/
private String reasonId;
/**
* 退款原因二级补充说明
*/
private String reasonDetail;
private BigDecimal RefundPriceExpress;
private List<String> ticketEntityIds;
......
......@@ -23,6 +23,16 @@ public class RefundBatchApplyParam implements Serializable {
private String reason;
/**
* 退款原因配置ID
*/
private String reasonId;
/**
* 退款原因二级补充说明
*/
private String reasonDetail;
private Integer status;
private Integer refundStatus;
......
package com.liquidnet.service.kylin.dto.vo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.io.Serializable;
/**
* 退款原因配置 VO
*/
@Data
@ApiModel("退款原因配置")
public class OrderRefundReasonVo implements Serializable, Cloneable {
private static final long serialVersionUID = 1L;
@ApiModelProperty(value = "原因业务ID")
private String reasonId;
@ApiModelProperty(value = "退款理由名称")
private String name;
@ApiModelProperty(value = "是否需要二级补充输入 0否 1是")
private Integer needDetail;
@ApiModelProperty(value = "二级输入placeholder")
private String placeholder;
@ApiModelProperty(value = "是否已应用 0未应用 1应用中")
private Integer isApplied;
@ApiModelProperty(value = "应用状态名称")
private String isAppliedName;
@ApiModelProperty(value = "排序")
private Integer sort;
@ApiModelProperty(value = "创建时间")
private String createdAt;
@ApiModelProperty(value = "修改时间")
private String updatedAt;
private static final OrderRefundReasonVo OBJ = new OrderRefundReasonVo();
public static OrderRefundReasonVo getNew() {
try {
return (OrderRefundReasonVo) OBJ.clone();
} catch (CloneNotSupportedException e) {
return new OrderRefundReasonVo();
}
}
}
package com.liquidnet.service.kylin.dto.vo.groupbuy;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.io.Serializable;
/**
* 拼团成员简要信息(头像+ID)。
*/
@Data
public class GroupBuyMemberUserInfoVo implements Serializable {
private static final long serialVersionUID = 1L;
@ApiModelProperty(value = "用户ID")
private String userId;
@ApiModelProperty(value = "头像URL")
private String avatar;
@ApiModelProperty(value = "昵称")
private String nickname;
@ApiModelProperty(value = "订单ID")
private String orderTicketsId;
}
package com.liquidnet.service.kylin.dto.vo.groupbuy;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.io.Serializable;
import java.util.List;
@Data
public class KylinGroupBuyActivityVo implements Serializable {
private static final long serialVersionUID = 1L;
@ApiModelProperty(value = "活动ID")
private String activityId;
@ApiModelProperty(value = "活动名称")
private String name;
@ApiModelProperty(value = "演出ID")
private String performanceId;
@ApiModelProperty(value = "成团人数")
private Integer minGroupSize;
@ApiModelProperty(value = "每人限购")
private Integer maxBuyPerUser;
@ApiModelProperty(value = "活动开始时间")
private String startTime;
@ApiModelProperty(value = "活动结束时间")
private String endTime;
@ApiModelProperty(value = "启动方式 1立即 2手动 3定时")
private Integer launchType;
@ApiModelProperty(value = "定时启动时间")
private String scheduledTime;
@ApiModelProperty(value = "状态 0草稿 1待启动 2进行中 3已关闭 4已结束")
private Integer status;
@ApiModelProperty(value = "活动说明")
private String description;
@ApiModelProperty(value = "创建时间")
private String createdAt;
@ApiModelProperty(value = "更新时间")
private String updatedAt;
@ApiModelProperty(value = "优惠配置")
private List<KylinGroupBuyDiscountVo> discounts;
@ApiModelProperty(value = "关联演出名称(列表查询时填充)")
private String performanceName;
@ApiModelProperty(value = "拼单数量(列表查询时填充)")
private Long orderCount;
}
package com.liquidnet.service.kylin.dto.vo.groupbuy;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
@Data
public class KylinGroupBuyDiscountVo implements Serializable {
private static final long serialVersionUID = 1L;
@ApiModelProperty(value = "优惠ID")
private String discountId;
@ApiModelProperty(value = "活动ID")
private String activityId;
@ApiModelProperty(value = "演出ID")
private String performanceId;
@ApiModelProperty(value = "场次ID")
private String timeId;
@ApiModelProperty(value = "票种ID")
private String ticketId;
@ApiModelProperty(value = "优惠类型 1固定价格 2折扣 3满减")
private Integer discountType;
@ApiModelProperty(value = "优惠值")
private BigDecimal discountValue;
@ApiModelProperty(value = "创建时间")
private String createdAt;
}
package com.liquidnet.service.kylin.dto.vo.groupbuy;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.io.Serializable;
@Data
public class KylinGroupBuyMemberVo implements Serializable {
private static final long serialVersionUID = 1L;
@ApiModelProperty(value = "成员ID")
private String memberId;
@ApiModelProperty(value = "拼团订单ID")
private String groupOrderId;
@ApiModelProperty(value = "用户ID")
private String userId;
@ApiModelProperty(value = "订单ID")
private String orderTicketsId;
@ApiModelProperty(value = "购票张数")
private Integer ticketCount;
@ApiModelProperty(value = "状态 -1待支付 0已加入 1已退款")
private Integer status;
@ApiModelProperty(value = "加入时间")
private String joinTime;
@ApiModelProperty(value = "创建时间")
private String createdAt;
@ApiModelProperty(value = "昵称")
private String nickname;
@ApiModelProperty(value = "头像")
private String avatar;
}
package com.liquidnet.service.kylin.dto.vo.groupbuy;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.io.Serializable;
import java.util.List;
/**
* 我的拼团列表项(/my 用)。
*/
@Data
public class KylinGroupBuyMyGroupVo implements Serializable {
private static final long serialVersionUID = 1L;
@ApiModelProperty(value = "拼团订单ID")
private String groupOrderId;
@ApiModelProperty(value = "活动ID")
private String activityId;
@ApiModelProperty(value = "团长用户ID")
private String leaderUserId;
@ApiModelProperty(value = "目标成团人数")
private Integer targetCount;
@ApiModelProperty(value = "当前参团人数")
private Integer currentCount;
@ApiModelProperty(value = "状态 0拼单中 1拼成 2失败 3已取消")
private Integer status;
@ApiModelProperty(value = "过期时间")
private String expiresAt;
@ApiModelProperty(value = "是否团长 1是 0否")
private Integer isLeader;
@ApiModelProperty(value = "演出标题")
private String performanceTitle;
@ApiModelProperty(value = "演出图片")
private String performanceImg;
@ApiModelProperty(value = "票种名称")
private String ticketTitle;
@ApiModelProperty(value = "场次名称")
private String timeTitle;
@ApiModelProperty(value = "成员头像列表")
private List<GroupBuyMemberUserInfoVo> memberBriefs;
}
package com.liquidnet.service.kylin.dto.vo.groupbuy;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.io.Serializable;
/**
* 拼单轮播卡片(/pending 用)。
*/
@Data
public class KylinGroupBuyOrderCardVo implements Serializable {
private static final long serialVersionUID = 1L;
@ApiModelProperty(value = "拼团订单ID")
private String groupOrderId;
@ApiModelProperty(value = "活动ID")
private String activityId;
@ApiModelProperty(value = "团长用户ID")
private String leaderUserId;
@ApiModelProperty(value = "目标成团人数")
private Integer targetCount;
@ApiModelProperty(value = "当前参团人数")
private Integer currentCount;
@ApiModelProperty(value = "分享token")
private String shareToken;
}
package com.liquidnet.service.kylin.dto.vo.groupbuy;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.List;
/**
* 拼团详情(/detail 用)。
*/
@Data
public class KylinGroupBuyOrderDetailVo implements Serializable {
private static final long serialVersionUID = 1L;
@ApiModelProperty(value = "拼团订单ID")
private String groupOrderId;
@ApiModelProperty(value = "活动ID")
private String activityId;
@ApiModelProperty(value = "团长用户ID")
private String leaderUserId;
@ApiModelProperty(value = "目标成团人数")
private Integer targetCount;
@ApiModelProperty(value = "当前参团人数")
private Integer currentCount;
@ApiModelProperty(value = "状态 0拼单中 1拼成 2失败 3已取消")
private Integer status;
@ApiModelProperty(value = "是否公开 1公开 0私密")
private Integer isPublic;
@ApiModelProperty(value = "分享token,分享/私密团加入时携带")
private String shareToken;
@ApiModelProperty(value = "过期时间")
private String expiresAt;
@ApiModelProperty(value = "创建时间")
private String createdAt;
@ApiModelProperty(value = "演出ID")
private String performanceId;
@ApiModelProperty(value = "演出名称")
private String performanceName;
@ApiModelProperty(value = "场次ID")
private String timeId;
@ApiModelProperty(value = "场次名称")
private String timeTitle;
@ApiModelProperty(value = "票种ID")
private String ticketId;
@ApiModelProperty(value = "票种名称")
private String ticketName;
@ApiModelProperty(value = "原价")
private BigDecimal originalPrice;
@ApiModelProperty(value = "拼团单价")
private BigDecimal groupBuyPrice;
@ApiModelProperty(value = "当前用户是否团长 1是 0否")
private Integer isLeader;
@ApiModelProperty(value = "拼团成员信息")
private List<GroupBuyMemberUserInfoVo> members;
}
package com.liquidnet.service.kylin.dto.vo.groupbuy;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.io.Serializable;
import java.util.List;
@Data
public class KylinGroupBuyOrderVo implements Serializable {
private static final long serialVersionUID = 1L;
@ApiModelProperty(value = "拼团订单ID")
private String groupOrderId;
@ApiModelProperty(value = "活动ID")
private String activityId;
@ApiModelProperty(value = "团长用户ID")
private String leaderUserId;
@ApiModelProperty(value = "团长订单ID")
private String orderTicketsId;
@ApiModelProperty(value = "场次ID")
private String timeId;
@ApiModelProperty(value = "票种ID")
private String ticketId;
@ApiModelProperty(value = "目标成团人数")
private Integer targetCount;
@ApiModelProperty(value = "当前参团人数")
private Integer currentCount;
@ApiModelProperty(value = "状态 0拼单中 1拼成 2失败 3已取消")
private Integer status;
@ApiModelProperty(value = "是否公开 1公开 0私密")
private Integer isPublic;
@ApiModelProperty(value = "分享token")
private String shareToken;
@ApiModelProperty(value = "过期时间")
private String expiresAt;
@ApiModelProperty(value = "创建时间")
private String createdAt;
@ApiModelProperty(value = "更新时间")
private String updatedAt;
@ApiModelProperty(value = "是否已发送到期前1小时提醒 1已发 0/null未发")
private Integer remind1hSent;
@ApiModelProperty(value = "成员")
private List<KylinGroupBuyMemberVo> members;
}
package com.liquidnet.service.kylin.dto.vo.groupbuy;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.List;
/**
* 管理端拼团订单详情 VO(与列表 VO 分离)。
*/
@Data
public class KylinGroupBuyPartnerOrderDetailVo implements Serializable {
private static final long serialVersionUID = 1L;
@ApiModelProperty(value = "拼团订单号")
private String groupOrderId;
@ApiModelProperty(value = "活动ID")
private String activityId;
@ApiModelProperty(value = "拼团活动名称")
private String activityName;
@ApiModelProperty(value = "订单状态 0拼团中 1拼团成功 2拼团失败 -1待支付发起 3已取消")
private Integer status;
@ApiModelProperty(value = "订单金额(团长票单实付)")
private BigDecimal orderAmount;
@ApiModelProperty(value = "创建时间")
private String createdAt;
@ApiModelProperty(value = "当前参团人数")
private Integer currentCount;
@ApiModelProperty(value = "成团目标人数")
private Integer targetCount;
@ApiModelProperty(value = "参团成员")
private List<KylinGroupBuyPartnerOrderMemberVo> members;
@ApiModelProperty(value = "发起人用户ID")
private String leaderUserId;
@ApiModelProperty(value = "发起人昵称")
private String leaderNickname;
@ApiModelProperty(value = "发起人头像")
private String leaderAvatar;
}
package com.liquidnet.service.kylin.dto.vo.groupbuy;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
/**
* 管理端拼团订单列表 VO(与详情 VO 分离)。
*/
@Data
public class KylinGroupBuyPartnerOrderListVo implements Serializable {
private static final long serialVersionUID = 1L;
@ApiModelProperty(value = "拼团订单号 groupOrderId")
private String groupOrderId;
@ApiModelProperty(value = "拼团活动名称")
private String activityName;
@ApiModelProperty(value = "发起人用户ID")
private String leaderUserId;
@ApiModelProperty(value = "发起人昵称")
private String leaderNickname;
@ApiModelProperty(value = "发起人头像")
private String leaderAvatar;
@ApiModelProperty(value = "当前参团人数")
private Integer currentCount;
@ApiModelProperty(value = "成团目标人数")
private Integer targetCount;
@ApiModelProperty(value = "订单金额(团长票单实付)")
private BigDecimal orderAmount;
@ApiModelProperty(value = "创建时间 yyyy-MM-dd HH:mm:ss")
private String createdAt;
@ApiModelProperty(value = "状态 0拼团中 1拼团成功 2拼团失败;列表可能出现 -1待支付发起 3已取消")
private Integer status;
}
package com.liquidnet.service.kylin.dto.vo.groupbuy;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.io.Serializable;
/**
* 管理端拼团订单详情-成员项。
*/
@Data
public class KylinGroupBuyPartnerOrderMemberVo implements Serializable {
private static final long serialVersionUID = 1L;
@ApiModelProperty(value = "用户ID")
private String userId;
@ApiModelProperty(value = "昵称")
private String nickname;
@ApiModelProperty(value = "头像")
private String avatar;
@ApiModelProperty(value = "是否团长 1是 0否")
private Integer isLeader;
@ApiModelProperty(value = "成员状态 -1待支付 0已加入 1已退款 2已取消")
private Integer status;
}
package com.liquidnet.service.kylin.dto.vo.groupbuy;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.io.Serializable;
/**
* 管理端拼团订单统计。
*/
@Data
public class KylinGroupBuyPartnerOrderStatsVo implements Serializable {
private static final long serialVersionUID = 1L;
@ApiModelProperty(value = "订单总数(含待支付发起、已取消)")
private Long total;
@ApiModelProperty(value = "拼团中数量 status=0")
private Long pending;
@ApiModelProperty(value = "拼团成功数量 status=1")
private Long success;
@ApiModelProperty(value = "拼团失败数量 status=2")
private Long failed;
}
package com.liquidnet.service.kylin.dto.vo.groupbuy;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
@Data
public class KylinGroupBuyPriceVo implements Serializable {
private static final long serialVersionUID = 1L;
@ApiModelProperty(value = "活动ID")
private String activityId;
@ApiModelProperty(value = "场次ID")
private String timeId;
@ApiModelProperty(value = "票种ID")
private String ticketId;
@ApiModelProperty(value = "原价")
private BigDecimal originalPrice;
@ApiModelProperty(value = "拼团单价")
private BigDecimal groupBuyPrice;
@ApiModelProperty(value = "优惠类型")
private Integer discountType;
@ApiModelProperty(value = "优惠值")
private BigDecimal discountValue;
}
package com.liquidnet.service.kylin.dto.vo.groupbuy;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.io.Serializable;
/**
* 刷新分享链接响应(/refreshShare 用)。
*/
@Data
public class KylinGroupBuyRefreshShareVo implements Serializable {
private static final long serialVersionUID = 1L;
@ApiModelProperty(value = "拼团订单ID")
private String groupOrderId;
@ApiModelProperty(value = "新邀请token")
private String shareToken;
}
......@@ -49,6 +49,8 @@ public class KylinOrderTicketVo implements Serializable, Cloneable {
private String referrerApp;
@ApiModelProperty(position = 19, value = "粉丝俱乐部侧用户id")
private String referrerUserId;
@ApiModelProperty(position = 19, value = "拼团订单ID")
private String groupOrderId;
@ApiModelProperty(position = 20, value = "下单版本")
private String orderVersion;
@ApiModelProperty(position = 20, value = "数量")
......
......@@ -91,6 +91,12 @@ public class KylinOrderRefundsAdminVo implements Serializable,Cloneable {
@ApiModelProperty(value = "退款原因")
private String reason;
@ApiModelProperty(value = "退款原因配置ID")
private String reasonId;
@ApiModelProperty(value = "退款原因二级补充说明")
private String reasonDetail;
@ApiModelProperty(value = "审核人id")
private String auditorId;
......
......@@ -89,6 +89,12 @@ public class KylinOrderRefundsVo implements Serializable,Cloneable {
@ApiModelProperty(value = "退款原因")
private String reason;
@ApiModelProperty(value = "退款原因配置ID")
private String reasonId;
@ApiModelProperty(value = "退款原因二级补充说明")
private String reasonDetail;
@ApiModelProperty(value = "审核人id")
private String auditorId;
......
package com.liquidnet.service.kylin.service;
/**
* 拼团短信通知(成功 / 超时失败 / 到期前 1h)。
* 主挂载点在 order(成功)与 platform(失败、1h);本接口供 kylin 侧复用。
*/
public interface GroupBuyNotifyService {
void notifySuccess(String groupOrderId);
void notifyFailed(String groupOrderId);
void notifyRemind1h(String groupOrderId);
}
package com.liquidnet.service.kylin.service;
import com.liquidnet.service.base.ResponseDto;
import com.liquidnet.service.kylin.dto.vo.groupbuy.KylinGroupBuyActivityVo;
import com.liquidnet.service.kylin.dto.vo.groupbuy.KylinGroupBuyPriceVo;
import java.util.List;
public interface IKylinGroupBuyActivityService {
ResponseDto<List<KylinGroupBuyActivityVo>> activities(String performanceId);
ResponseDto<KylinGroupBuyPriceVo> price(String activityId, String timeId, String ticketId);
}
package com.liquidnet.service.kylin.service;
import com.github.pagehelper.PageInfo;
import com.liquidnet.service.base.ResponseDto;
import com.liquidnet.service.kylin.dto.vo.groupbuy.KylinGroupBuyMyGroupVo;
import com.liquidnet.service.kylin.dto.vo.groupbuy.KylinGroupBuyOrderCardVo;
import com.liquidnet.service.kylin.dto.vo.groupbuy.KylinGroupBuyOrderDetailVo;
import com.liquidnet.service.kylin.dto.vo.groupbuy.KylinGroupBuyRefreshShareVo;
import java.util.List;
public interface IKylinGroupBuyOrderService {
ResponseDto<List<KylinGroupBuyOrderCardVo>> pending(String activityId, String timeId, String ticketId);
ResponseDto<PageInfo<KylinGroupBuyMyGroupVo>> my(Integer page, Integer size);
ResponseDto<KylinGroupBuyOrderDetailVo> detail(String groupOrderId, String shareToken);
ResponseDto<Boolean> cancel(String groupOrderId);
ResponseDto<KylinGroupBuyRefreshShareVo> refreshShare(String groupOrderId);
}
package com.liquidnet.service.kylin.service;
import com.liquidnet.service.kylin.dto.vo.OrderRefundReasonVo;
import java.util.List;
/**
* 退款原因(App/H5)
*/
public interface IKylinOrderRefundReasonService {
/**
* 已应用退款理由列表(Redis 缓存)
*/
List<OrderRefundReasonVo> listApplied();
/**
* 按 reasonId 从已应用列表取配置;不存在返回 null
*/
OrderRefundReasonVo getAppliedByReasonId(String reasonId);
}
package com.liquidnet.service.kylin.service.admin;
import com.github.pagehelper.PageInfo;
import com.liquidnet.service.kylin.dto.param.OrderRefundReasonParam;
import com.liquidnet.service.kylin.dto.param.OrderRefundReasonSearchParam;
import com.liquidnet.service.kylin.dto.vo.OrderRefundReasonVo;
import com.liquidnet.service.kylin.entity.KylinOrderRefundReasons;
import java.util.List;
/**
* 退款原因配置管理
*/
public interface IOrderRefundReasonAdminService {
Boolean create(OrderRefundReasonParam param);
Boolean update(OrderRefundReasonParam param);
OrderRefundReasonVo detail(String reasonId);
Boolean delete(List<String> reasonIds);
PageInfo<KylinOrderRefundReasons> reasonList(OrderRefundReasonSearchParam param);
/**
* 应用 / 取消应用
*
* @param reasonId 原因ID
* @param isApplied 1应用 0取消应用
*/
Boolean apply(String reasonId, Integer isApplied);
/**
* 已应用列表(读缓存)
*/
List<OrderRefundReasonVo> listApplied();
}
package com.liquidnet.service.kylin.service.partner;
import com.github.pagehelper.PageInfo;
import com.liquidnet.service.base.ResponseDto;
import com.liquidnet.service.kylin.dto.param.KylinGroupBuyActivityCreateParam;
import com.liquidnet.service.kylin.dto.param.KylinGroupBuyActivityUpdateParam;
import com.liquidnet.service.kylin.dto.vo.groupbuy.KylinGroupBuyActivityVo;
import com.liquidnet.service.kylin.dto.vo.groupbuy.KylinGroupBuyPartnerOrderDetailVo;
import com.liquidnet.service.kylin.dto.vo.groupbuy.KylinGroupBuyPartnerOrderListVo;
import com.liquidnet.service.kylin.dto.vo.groupbuy.KylinGroupBuyPartnerOrderStatsVo;
import javax.servlet.http.HttpServletResponse;
import java.util.Map;
public interface IKylinGroupBuyPartnerService {
ResponseDto<PageInfo<KylinGroupBuyActivityVo>> list(String name, Integer status, Integer page, Integer size);
ResponseDto<Map<String, Long>> stats();
ResponseDto<KylinGroupBuyActivityVo> detail(String activityId);
ResponseDto<KylinGroupBuyActivityVo> create(KylinGroupBuyActivityCreateParam param);
ResponseDto<KylinGroupBuyActivityVo> update(KylinGroupBuyActivityUpdateParam param);
ResponseDto<Boolean> delete(String activityId);
ResponseDto<Boolean> toggle(String activityId, Integer targetStatus);
/**
* 拼团订单顶部统计:总数 + 拼团中/成功/失败。
*/
ResponseDto<KylinGroupBuyPartnerOrderStatsVo> orderStats();
/**
* 拼团订单分页列表。status 仅允许 0/1/2 或空(空=全部含 -1/3)。
*/
ResponseDto<PageInfo<KylinGroupBuyPartnerOrderListVo>> orderList(String keyword, Integer status,
Integer page, Integer size);
/**
* 拼团订单详情。
*/
ResponseDto<KylinGroupBuyPartnerOrderDetailVo> orderDetail(String groupOrderId);
/**
* 导出拼团订单(列同列表,筛选同列表)。
*/
void exportOrders(String keyword, Integer status, HttpServletResponse response);
}
package com.liquidnet.service.kylin.utils;
import com.liquidnet.service.kylin.dto.vo.OrderRefundReasonVo;
import lombok.Getter;
import org.apache.commons.lang3.StringUtils;
import java.util.List;
/**
* 退款原因展示文案与校验工具
*/
public final class OrderRefundReasonUtils {
private OrderRefundReasonUtils() {
}
/**
* 校验已应用退款原因,并组装落库字段
*
* @param reasonId 原因配置 ID
* @param reasonDetail 二级补充说明
* @param appliedList 已应用原因列表(可从缓存取)
*/
public static ResolvedRefundReason resolve(String reasonId, String reasonDetail, List<OrderRefundReasonVo> appliedList) {
if (StringUtils.isBlank(reasonId)) {
return ResolvedRefundReason.fail("请选择退款原因");
}
OrderRefundReasonVo matched = findByReasonId(reasonId, appliedList);
if (matched == null) {
return ResolvedRefundReason.fail("退款原因无效或未应用");
}
return resolve(matched, reasonDetail);
}
/**
* 已拿到配置项时的校验与组装
*/
public static ResolvedRefundReason resolve(OrderRefundReasonVo matched, String reasonDetail) {
if (matched == null) {
return ResolvedRefundReason.fail("退款原因无效或未应用");
}
String detail = StringUtils.trimToEmpty(reasonDetail);
boolean needDetail = matched.getNeedDetail() != null && matched.getNeedDetail() == 1;
if (needDetail && StringUtils.isBlank(detail)) {
return ResolvedRefundReason.fail("请填写补充说明");
}
if (!needDetail) {
detail = null;
} else {
detail = truncate(detail, 200);
}
String snapshot = buildReasonSnapshot(matched.getName(), detail);
return ResolvedRefundReason.ok(matched.getReasonId(), detail, snapshot);
}
public static OrderRefundReasonVo findByReasonId(String reasonId, List<OrderRefundReasonVo> appliedList) {
if (StringUtils.isBlank(reasonId) || appliedList == null) {
return null;
}
for (OrderRefundReasonVo item : appliedList) {
if (reasonId.equals(item.getReasonId())) {
return item;
}
}
return null;
}
/**
* 组装 reason 快照:无二级写 name;有二级写 name:detail,截断至 200
*/
public static String buildReasonSnapshot(String name, String reasonDetail) {
String base = StringUtils.defaultString(name, "");
if (StringUtils.isBlank(reasonDetail)) {
return truncate(base, 200);
}
return truncate(base + ":" + reasonDetail.trim(), 200);
}
public static String truncate(String value, int maxLen) {
if (value == null) {
return "";
}
if (value.length() <= maxLen) {
return value;
}
return value.substring(0, maxLen);
}
@Getter
public static final class ResolvedRefundReason {
private final boolean success;
private final String errorMsg;
private final String reasonId;
private final String reasonDetail;
private final String reasonSnapshot;
private ResolvedRefundReason(boolean success, String errorMsg, String reasonId, String reasonDetail, String reasonSnapshot) {
this.success = success;
this.errorMsg = errorMsg;
this.reasonId = reasonId;
this.reasonDetail = reasonDetail;
this.reasonSnapshot = reasonSnapshot;
}
public static ResolvedRefundReason ok(String reasonId, String reasonDetail, String reasonSnapshot) {
return new ResolvedRefundReason(true, null, reasonId, reasonDetail, reasonSnapshot);
}
public static ResolvedRefundReason fail(String errorMsg) {
return new ResolvedRefundReason(false, errorMsg, null, null, null);
}
}
}
......@@ -10,7 +10,8 @@ public class SlimeAuthorizationConst {
SALES("4", "销售统计"),
CHECK("5", "验票"),
GRANT("6", "授权"),
AUDIT("7", "审核");
AUDIT("7", "审核"),
GROUP_BUY("8", "拼团管理");
private final String id;
private final String name;
......
package com.liquidnet.client.admin.web.controller.zhengzai.kylin;
import com.github.pagehelper.PageInfo;
import com.liquidnet.client.admin.common.annotation.Log;
import com.liquidnet.client.admin.common.core.controller.BaseController;
import com.liquidnet.client.admin.common.core.domain.AjaxResult;
import com.liquidnet.client.admin.common.core.page.TableDataInfo;
import com.liquidnet.client.admin.common.enums.BusinessType;
import com.liquidnet.client.admin.zhengzai.kylin.service.impl.KylinOrderRefundReasonAdminServiceImpl;
import com.liquidnet.service.kylin.dto.param.OrderRefundReasonParam;
import com.liquidnet.service.kylin.dto.param.OrderRefundReasonSearchParam;
import com.liquidnet.service.kylin.dto.vo.OrderRefundReasonVo;
import com.liquidnet.service.kylin.entity.KylinOrderRefundReasons;
import org.apache.shiro.authz.annotation.Logical;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* 退款原因管理
*/
@Controller
@RequestMapping("/kylin/refundReasons")
public class KylinOrderRefundReasonsAdminController extends BaseController {
private String prefix = "zhengzai/kylin/refundReasons";
@Autowired
private KylinOrderRefundReasonAdminServiceImpl refundReasonAdminService;
@RequiresPermissions("kylin:refundReasons:list")
@GetMapping()
public String list() {
return prefix + "/refundReasons";
}
@RequiresPermissions("kylin:refundReasons:list")
@PostMapping("list")
@ResponseBody
public TableDataInfo listData(OrderRefundReasonSearchParam param) {
PageInfo<KylinOrderRefundReasons> result = refundReasonAdminService.reasonList(param);
return getDataTable(result.getList());
}
@GetMapping("/create")
public String create() {
return prefix + "/create";
}
@Log(title = "创建退款原因", businessType = BusinessType.INSERT)
@RequiresPermissions("kylin:refundReasons:create")
@PostMapping("create")
@ResponseBody
public AjaxResult createSave(OrderRefundReasonParam param) {
Boolean res = refundReasonAdminService.create(param);
return res ? success() : error("添加退款原因失败");
}
@GetMapping("/update/{reasonId}")
public String update(@PathVariable("reasonId") String reasonId, ModelMap mmap) {
OrderRefundReasonVo vo = refundReasonAdminService.detail(reasonId);
if (vo == null || (vo.getIsApplied() != null && vo.getIsApplied() == 1)) {
mmap.put("errorMsg", "应用中的退款原因不可编辑,请先取消应用");
}
mmap.put("RefundReasonVo", vo);
return prefix + "/update";
}
@Log(title = "修改退款原因", businessType = BusinessType.UPDATE)
@RequiresPermissions("kylin:refundReasons:update")
@PostMapping("update")
@ResponseBody
public AjaxResult updateSave(OrderRefundReasonParam param) {
Boolean res = refundReasonAdminService.update(param);
return res ? success() : error("修改失败(应用中不可编辑,或数据不存在)");
}
@Log(title = "删除退款原因", businessType = BusinessType.DELETE)
@RequiresPermissions("kylin:refundReasons:delete")
@PostMapping("delete")
@ResponseBody
public AjaxResult delete(OrderRefundReasonParam param) {
List<String> ids = param.getIds();
Boolean result = refundReasonAdminService.delete(ids);
return result ? success("删除成功") : error("删除失败(应用中的原因需先取消应用,或数据不存在)");
}
@Log(title = "应用/取消应用退款原因", businessType = BusinessType.UPDATE)
@RequiresPermissions("kylin:refundReasons:apply")
@PutMapping("apply")
@ResponseBody
public AjaxResult apply(@RequestParam("reasonId") String reasonId,
@RequestParam("isApplied") Integer isApplied) {
Boolean result = refundReasonAdminService.apply(reasonId, isApplied);
return result ? success() : error("操作失败");
}
/**
* 已应用列表(供申请页下拉,走缓存)
*/
@RequiresPermissions(value = {"kylin:refund:apply", "kylin:refundBatch:apply", "kylin:refundReasons:list"}, logical = Logical.OR)
@GetMapping("appliedList")
@ResponseBody
public AjaxResult appliedList() {
List<OrderRefundReasonVo> list = refundReasonAdminService.listApplied();
return AjaxResult.success(list);
}
}
......@@ -34,9 +34,17 @@
</div>
</div>
<div class="form-group">
<label class="col-sm-2 control-label layui-required is-required">备注</label>
<label class="col-sm-2 control-label layui-required is-required">退款原因</label>
<div class="col-sm-10">
<textarea name="reason" maxlength="500" class="form-control layui-required" rows="3"></textarea>
<select name="reasonId" id="reasonId" class="form-control layui-required" required>
<option value="">请选择退款原因</option>
</select>
</div>
</div>
<div class="form-group" id="reasonDetailGroup" style="display:none;">
<label class="col-sm-2 control-label layui-required is-required">补充说明:</label>
<div class="col-sm-10">
<textarea name="reasonDetail" id="reasonDetail" maxlength="200" class="form-control" rows="3"></textarea>
</div>
</div>
</form>
......@@ -44,6 +52,38 @@
<th:block th:include="include :: footer"/>
<script type="text/javascript">
var prefix = ctx + "kylin/refund";
var reasonPrefix = ctx + "kylin/refundReasons";
function loadAppliedReasons() {
$.ajax({
url: reasonPrefix + "/appliedList",
type: "get",
dataType: "json",
success: function (result) {
if (result.code == 0 && result.data) {
var $sel = $("#reasonId");
$sel.find("option:not(:first)").remove();
for (var i = 0; i < result.data.length; i++) {
var item = result.data[i];
$sel.append('<option value="' + item.reasonId + '" data-need-detail="' + item.needDetail + '" data-placeholder="' + (item.placeholder || '') + '">' + item.name + '</option>');
}
}
}
});
}
function onReasonChange() {
var $opt = $("#reasonId option:selected");
var needDetail = $opt.data("need-detail");
var placeholder = $opt.data("placeholder") || "请填写补充说明";
if (needDetail == 1) {
$("#reasonDetailGroup").show();
$("#reasonDetail").attr("placeholder", placeholder);
} else {
$("#reasonDetailGroup").hide();
$("#reasonDetail").val("");
}
}
$("#form-post-add").validate({
onkeyup: false,
......@@ -174,7 +214,8 @@
});
function submitHandler() {
var reason = $('#form-post-add').find("[name='reason']").val();
var reasonId = $('#form-post-add').find("[name='reasonId']").val();
var reasonDetail = $('#form-post-add').find("[name='reasonDetail']").val();
var orderTicketsId = $('#form-post-add').find("[name='orderTicketsId']").val();
var rows = $.common.isEmpty(table.options.uniqueId) ? $.table.selectFirstColumns() : $.table.selectColumns(table.options.uniqueId);
if (!orderTicketsId) {
......@@ -185,8 +226,13 @@
layer.msg("请选择需要退款的入场人订单");
return false;
}
if (!reason) {
layer.msg("请填写备注");
if (!reasonId) {
layer.msg("请选择退款原因");
return false;
}
var $opt = $("#reasonId option:selected");
if ($opt.data("need-detail") == 1 && !$.common.trim(reasonDetail)) {
layer.msg("请填写补充说明");
return false;
}
if ($.validate.form()) {
......@@ -204,6 +250,11 @@
}
}
$(function () {
loadAppliedReasons();
$("#reasonId").on("change", onReasonChange);
});
</script>
</body>
</html>
......@@ -147,7 +147,7 @@
<div class="form-control-static" th:text="${KylinOrderRefundsAdminVo.applicantName}"></div>
<label class="col-sm-2 control-label">申请时间:</label>
<div class="form-control-static" th:text="${KylinOrderRefundsAdminVo.applicantAt}"></div>
<label class="col-sm-2 control-label">申请备注</label>
<label class="col-sm-2 control-label">退款原因</label>
<div class="form-control-static" th:text="${KylinOrderRefundsAdminVo.reason}"></div>
</div>
<div class="form-group">
......
......@@ -368,6 +368,19 @@
field: 'priceExpress',
title: '退快递费金额'
},
{
field: 'reason',
title: '退款原因',
formatter: function (value) {
if (!value) {
return '';
}
if (value.length > 30) {
return '<span title="' + value.replace(/"/g, '&quot;') + '">' + value.substring(0, 30) + '...</span>';
}
return value;
}
},
{
field: 'statusName',
title: '退款状态'
......
......@@ -27,9 +27,17 @@
</div>
</div>
<div class="form-group">
<label class="col-sm-3 control-label layui-required is-required">备注</label>
<label class="col-sm-3 control-label layui-required is-required">退款原因</label>
<div class="col-sm-8">
<textarea name="reason" maxlength="500" class="form-control layui-required" rows="3"></textarea>
<select name="reasonId" id="reasonId" class="form-control layui-required" required>
<option value="">请选择退款原因</option>
</select>
</div>
</div>
<div class="form-group" id="reasonDetailGroup" style="display:none;">
<label class="col-sm-3 control-label layui-required is-required">补充说明:</label>
<div class="col-sm-8">
<textarea name="reasonDetail" id="reasonDetail" maxlength="200" class="form-control" rows="3"></textarea>
</div>
</div>
</form>
......@@ -37,7 +45,38 @@
<th:block th:include="include :: footer" />
<script type="text/javascript">
var prefix = ctx + "kylin/refundBatch";
var reasonPrefix = ctx + "kylin/refundReasons";
function loadAppliedReasons() {
$.ajax({
url: reasonPrefix + "/appliedList",
type: "get",
dataType: "json",
success: function (result) {
if (result.code == 0 && result.data) {
var $sel = $("#reasonId");
$sel.find("option:not(:first)").remove();
for (var i = 0; i < result.data.length; i++) {
var item = result.data[i];
$sel.append('<option value="' + item.reasonId + '" data-need-detail="' + item.needDetail + '" data-placeholder="' + (item.placeholder || '') + '">' + item.name + '</option>');
}
}
}
});
}
function onReasonChange() {
var $opt = $("#reasonId option:selected");
var needDetail = $opt.data("need-detail");
var placeholder = $opt.data("placeholder") || "请填写补充说明";
if (needDetail == 1) {
$("#reasonDetailGroup").show();
$("#reasonDetail").attr("placeholder", placeholder);
} else {
$("#reasonDetailGroup").hide();
$("#reasonDetail").val("");
}
}
$("#form-post-add").validate({
onkeyup: false,
rules:{
......@@ -136,14 +175,20 @@
});
function submitHandler() {
var reason = $('#form-post-add').find("[name='reason']").val();
var reasonId = $('#form-post-add').find("[name='reasonId']").val();
var reasonDetail = $('#form-post-add').find("[name='reasonDetail']").val();
var targetId = $('#form-post-add').find("[name='targetId']").val();
if (!targetId) {
layer.msg("演出id有误");
return false;
}
if (!reason) {
layer.msg("请填写备注");
if (!reasonId) {
layer.msg("请选择退款原因");
return false;
}
var $opt = $("#reasonId option:selected");
if ($opt.data("need-detail") == 1 && !$.common.trim(reasonDetail)) {
layer.msg("请填写补充说明");
return false;
}
if ($.validate.form()) {
......@@ -151,6 +196,11 @@
$.operate.save(prefix + "/apply", data);
}
}
$(function () {
loadAppliedReasons();
$("#reasonId").on("change", onReasonChange);
});
</script>
</body>
</html>
<!DOCTYPE html>
<html lang="zh" xmlns:th="http://www.thymeleaf.org">
<head>
<th:block th:include="include :: header('添加退款原因')"/>
</head>
<body class="white-bg">
<div class="wrapper wrapper-content animated fadeInRight ibox-content">
<form class="form-horizontal m" id="form-reason-add">
<div class="form-group">
<label class="col-sm-3 control-label is-required">退款理由名称:</label>
<div class="col-sm-8">
<input class="form-control" type="text" name="name" id="name"
maxlength="100" placeholder="展示给用户的文案" required>
</div>
</div>
<div class="form-group">
<label class="col-sm-3 control-label is-required">是否需补充说明:</label>
<div class="col-sm-8">
<div class="radio check-box">
<label>
<input type="radio" value="0" name="needDetail" checked> 否(仅选择)
</label>
</div>
<div class="radio check-box">
<label>
<input type="radio" value="1" name="needDetail"> 是(选择后需输入)
</label>
</div>
</div>
</div>
<div class="form-group" id="placeholderGroup" style="display:none;">
<label class="col-sm-3 control-label is-required">二级输入 placeholder:</label>
<div class="col-sm-8">
<input class="form-control" type="text" name="placeholder" id="placeholder"
maxlength="200" placeholder="请输入补充说明的提示文案">
</div>
</div>
<div class="form-group">
<label class="col-sm-3 control-label">排序:</label>
<div class="col-sm-8">
<input class="form-control" type="number" name="sort" id="sort" value="0">
<span class="help-block m-b-none"><i class="fa fa-info-circle"></i> 数值越大越靠前</span>
</div>
</div>
</form>
</div>
<th:block th:include="include :: footer"/>
<script type="text/javascript">
var prefix = ctx + "kylin/refundReasons";
function togglePlaceholderGroup() {
if ($('input[name="needDetail"]:checked').val() == '1') {
$('#placeholderGroup').show();
} else {
$('#placeholderGroup').hide();
$('#placeholder').val('');
}
}
function submitHandler() {
var name = $('#name').val().trim();
if (!name) {
$.modal.alertWarning("请填写退款理由名称");
return false;
}
var needDetail = $('input[name="needDetail"]:checked').val();
if (needDetail == '1' && !$('#placeholder').val().trim()) {
$.modal.alertWarning("请填写二级输入 placeholder");
return false;
}
if ($.validate.form()) {
var data = $('#form-reason-add').serializeArray();
$.operate.save(prefix + "/create", data);
}
}
$(function () {
// 本页 radio 走 iCheck,必须用 ifChecked,否则选「是」不会展示 placeholder
$('input[name="needDetail"]').on('ifChecked change', togglePlaceholderGroup);
togglePlaceholderGroup();
});
</script>
</body>
</html>
<!DOCTYPE html>
<html lang="zh" xmlns:th="http://www.thymeleaf.org" xmlns:shiro="http://www.pollix.at/thymeleaf/shiro">
<head>
<th:block th:include="include :: header('退款原因列表')"/>
<style>
.btn-disabled-edit {
color: #ccc !important;
cursor: not-allowed !important;
pointer-events: none;
}
</style>
</head>
<body class="gray-bg">
<div class="container-div">
<div class="row">
<div class="col-sm-12 search-collapse">
<form id="formId">
<div class="select-list">
<ul>
<li>
<label>名称:</label>
<input type="text" name="name"/>
</li>
<li>
<label>应用状态:</label>
<select name="isApplied">
<option value="">全部</option>
<option value="1">应用中</option>
<option value="0">未应用</option>
</select>
</li>
<li>
<a class="btn btn-primary btn-rounded btn-sm" onclick="$.table.search()"><i
class="fa fa-search"></i>&nbsp;搜索</a>
<a class="btn btn-warning btn-rounded btn-sm" onclick="$.form.reset()"><i
class="fa fa-refresh"></i>&nbsp;重置</a>
</li>
</ul>
</div>
</form>
</div>
<div class="btn-group-sm" id="toolbar" role="group">
<a class="btn btn-success" onclick="$.operate.add()" shiro:hasPermission="kylin:refundReasons:create">
<i class="fa fa-plus"></i> 添加
</a>
</div>
<div class="col-sm-12 select-table table-bordered">
<table id="bootstrap-table"></table>
</div>
</div>
</div>
<th:block th:include="include :: footer"/>
<script th:inline="javascript">
var updateFlag = [[${@permission.hasPermi('kylin:refundReasons:update')}]];
var removeFlag = [[${@permission.hasPermi('kylin:refundReasons:delete')}]];
var applyFlag = [[${@permission.hasPermi('kylin:refundReasons:apply')}]];
var prefix = ctx + "kylin/refundReasons";
$(function () {
var options = {
url: prefix + "/list",
updateUrl: prefix + "/update/{id}",
createUrl: prefix + "/create",
removeUrl: prefix + "/delete",
modalName: "退款原因",
columns: [
{
field: 'name',
title: '退款理由'
},
{
field: 'needDetail',
title: '需补充说明',
formatter: function (value) {
return value == 1 ? '是' : '否';
}
},
{
field: 'placeholder',
title: '二级输入placeholder'
},
{
field: 'isApplied',
title: '应用状态',
formatter: function (value) {
if (value == 1) {
return '<span class="badge badge-primary">应用中</span>';
}
return '<span class="badge badge-warning">未应用</span>';
}
},
{
field: 'sort',
title: '排序'
},
{
field: 'updatedAt',
title: '更新时间',
formatter: function (value) {
if (!value) {
return '';
}
var date = new Date(value);
return date.getFullYear() + '-' +
String(date.getMonth() + 1).padStart(2, '0') + '-' +
String(date.getDate()).padStart(2, '0') + ' ' +
String(date.getHours()).padStart(2, '0') + ':' +
String(date.getMinutes()).padStart(2, '0') + ':' +
String(date.getSeconds()).padStart(2, '0');
}
},
{
title: '操作',
align: 'center',
formatter: function (value, row) {
var actions = [];
if (row.isApplied == 1) {
actions.push('<a class="btn btn-success btn-xs btn-disabled-edit" href="javascript:void(0)" title="请先取消应用"><i class="fa fa-edit"></i>编辑</a> ');
actions.push('<a class="btn btn-warning btn-xs ' + applyFlag + '" href="javascript:void(0)" onclick="toggleApply(\'' + row.reasonId + '\', 0)"><i class="fa fa-ban"></i>取消应用</a> ');
} else {
actions.push('<a class="btn btn-success btn-xs ' + updateFlag + '" href="javascript:void(0)" onclick="$.operate.edit(\'' + row.reasonId + '\')"><i class="fa fa-edit"></i>编辑</a> ');
actions.push('<a class="btn btn-primary btn-xs ' + applyFlag + '" href="javascript:void(0)" onclick="toggleApply(\'' + row.reasonId + '\', 1)"><i class="fa fa-check"></i>应用</a> ');
actions.push('<a class="btn btn-danger btn-xs ' + removeFlag + '" href="javascript:void(0)" onclick="removeReason(\'' + row.reasonId + '\')"><i class="fa fa-remove"></i>删除</a> ');
}
return actions.join('');
}
}]
};
$.table.init(options);
});
function removeReason(id) {
$.operate.remove(id, "确认删除该退款原因吗?");
}
function toggleApply(reasonId, isApplied) {
var title = isApplied == 1 ? '应用' : '取消应用';
$.modal.confirm('确定要' + title + '该退款原因吗?', function () {
$.ajax({
type: 'put',
url: prefix + '/apply',
data: {reasonId: reasonId, isApplied: isApplied},
success: function (e) {
if (e.code == 0) {
$.modal.msgSuccess(title + '成功');
$("button[name=refresh]").click();
} else {
$.modal.alertError(e.msg || (title + '失败'));
}
}
});
});
}
</script>
</body>
</html>
<!DOCTYPE html>
<html lang="zh" xmlns:th="http://www.thymeleaf.org">
<head>
<th:block th:include="include :: header('修改退款原因')"/>
</head>
<body class="white-bg">
<div class="wrapper wrapper-content animated fadeInRight ibox-content">
<div th:if="${errorMsg != null}" class="alert alert-warning" th:text="${errorMsg}"></div>
<form class="form-horizontal m" id="form-reason-edit" th:if="${RefundReasonVo != null and RefundReasonVo.isApplied != 1}">
<input type="hidden" name="reasonId" th:value="${RefundReasonVo.reasonId}">
<div class="form-group">
<label class="col-sm-3 control-label is-required">退款理由名称:</label>
<div class="col-sm-8">
<input class="form-control" type="text" name="name" id="name"
maxlength="100" placeholder="展示给用户的文案"
th:value="${RefundReasonVo.name}" required>
</div>
</div>
<div class="form-group">
<label class="col-sm-3 control-label is-required">是否需补充说明:</label>
<div class="col-sm-8">
<div class="radio check-box">
<label>
<input type="radio" value="0" name="needDetail"
th:checked="${RefundReasonVo.needDetail == null or RefundReasonVo.needDetail == 0}"> 否(仅选择)
</label>
</div>
<div class="radio check-box">
<label>
<input type="radio" value="1" name="needDetail"
th:checked="${RefundReasonVo.needDetail == 1}"> 是(选择后需输入)
</label>
</div>
</div>
</div>
<div class="form-group" id="placeholderGroup"
th:style="${RefundReasonVo.needDetail == 1} ? '' : 'display:none;'">
<label class="col-sm-3 control-label is-required">二级输入 placeholder:</label>
<div class="col-sm-8">
<input class="form-control" type="text" name="placeholder" id="placeholder"
maxlength="200" placeholder="请输入补充说明的提示文案"
th:value="${RefundReasonVo.placeholder}">
</div>
</div>
<div class="form-group">
<label class="col-sm-3 control-label">排序:</label>
<div class="col-sm-8">
<input class="form-control" type="number" name="sort" id="sort"
th:value="${RefundReasonVo.sort}">
<span class="help-block m-b-none"><i class="fa fa-info-circle"></i> 数值越大越靠前</span>
</div>
</div>
</form>
</div>
<th:block th:include="include :: footer"/>
<script type="text/javascript">
var prefix = ctx + "kylin/refundReasons";
function togglePlaceholderGroup() {
if ($('input[name="needDetail"]:checked').val() == '1') {
$('#placeholderGroup').show();
} else {
$('#placeholderGroup').hide();
$('#placeholder').val('');
}
}
function submitHandler() {
if ($('#form-reason-edit').length === 0) {
$.modal.alertWarning("应用中的退款原因不可编辑");
return false;
}
var name = $('#name').val().trim();
if (!name) {
$.modal.alertWarning("请填写退款理由名称");
return false;
}
var needDetail = $('input[name="needDetail"]:checked').val();
if (needDetail == '1' && !$('#placeholder').val().trim()) {
$.modal.alertWarning("请填写二级输入 placeholder");
return false;
}
if ($.validate.form()) {
var data = $('#form-reason-edit').serializeArray();
$.operate.save(prefix + "/update", data);
}
}
$(function () {
// 本页 radio 走 iCheck,必须用 ifChecked,否则选「是」不会展示 placeholder
$('input[name="needDetail"]').on('ifChecked change', togglePlaceholderGroup);
togglePlaceholderGroup();
});
</script>
</body>
</html>
package com.liquidnet.client.admin.zhengzai.kylin.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;
import com.liquidnet.client.admin.zhengzai.kylin.utils.DataUtils;
import com.liquidnet.commons.lang.util.BeanUtil;
import com.liquidnet.commons.lang.util.DateUtil;
import com.liquidnet.commons.lang.util.IDGenerator;
import com.liquidnet.service.kylin.dto.param.OrderRefundReasonParam;
import com.liquidnet.service.kylin.dto.param.OrderRefundReasonSearchParam;
import com.liquidnet.service.kylin.dto.vo.OrderRefundReasonVo;
import com.liquidnet.service.kylin.entity.KylinOrderRefundReasons;
import com.liquidnet.service.kylin.mapper.KylinOrderRefundReasonsMapper;
import com.liquidnet.service.kylin.service.admin.IOrderRefundReasonAdminService;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* 退款原因配置管理
*/
@Slf4j
@Service
public class KylinOrderRefundReasonAdminServiceImpl
extends ServiceImpl<KylinOrderRefundReasonsMapper, KylinOrderRefundReasons>
implements IOrderRefundReasonAdminService {
@Autowired
private KylinOrderRefundReasonsMapper reasonsMapper;
@Autowired
private DataUtils dataUtils;
@Override
public Boolean create(OrderRefundReasonParam param) {
try {
if (StringUtils.isBlank(param.getName())) {
return false;
}
String reasonId = IDGenerator.nextSnowId();
LocalDateTime now = LocalDateTime.now();
KylinOrderRefundReasons entity = param.getFields(reasonId, now);
// 新建默认未应用、未删除
entity.setIsApplied(0);
entity.setIsDeleted(0);
reasonsMapper.insert(entity);
refreshAppliedCache();
return true;
} catch (Exception e) {
log.error("创建退款原因失败", e);
return false;
}
}
@Override
public Boolean update(OrderRefundReasonParam param) {
try {
if (StringUtils.isBlank(param.getReasonId()) || StringUtils.isBlank(param.getName())) {
return false;
}
KylinOrderRefundReasons existing = reasonsMapper.selectOne(
new QueryWrapper<KylinOrderRefundReasons>()
.eq("reason_id", param.getReasonId())
.eq("is_deleted", 0)
);
if (existing == null) {
return false;
}
// 应用中不可编辑
if (existing.getIsApplied() != null && existing.getIsApplied() == 1) {
return false;
}
KylinOrderRefundReasons entity = param.getFields(null, null);
// 不允许通过 update 直接改应用态/删除态
entity.setIsApplied(null);
entity.setIsDeleted(null);
entity.setReasonId(null);
entity.setCreatedAt(null);
entity.setUpdatedAt(LocalDateTime.now());
reasonsMapper.update(entity, new UpdateWrapper<KylinOrderRefundReasons>()
.eq("reason_id", param.getReasonId())
.eq("is_deleted", 0));
refreshAppliedCache();
return true;
} catch (Exception e) {
log.error("修改退款原因失败", e);
return false;
}
}
@Override
public OrderRefundReasonVo detail(String reasonId) {
KylinOrderRefundReasons data = reasonsMapper.selectOne(
new QueryWrapper<KylinOrderRefundReasons>()
.eq("reason_id", reasonId)
.eq("is_deleted", 0)
);
if (data == null) {
return null;
}
return toVo(data);
}
@Override
public PageInfo<KylinOrderRefundReasons> reasonList(OrderRefundReasonSearchParam param) {
try {
PageHelper.startPage(param.getPageNum(), param.getPageSize());
Map<String, Object> paramMap = BeanUtil.convertBeanToMap(param);
// convertBeanToMap(isAll) 会把 null 变成 "",isApplied=0 需保留为整型
if (param.getIsApplied() != null) {
paramMap.put("isApplied", param.getIsApplied());
} else {
paramMap.remove("isApplied");
}
List<KylinOrderRefundReasons> list = reasonsMapper.searchList(paramMap);
if (list != null) {
for (KylinOrderRefundReasons item : list) {
item.setIsAppliedName(formatAppliedName(item.getIsApplied()));
}
}
return new PageInfo<>(list);
} catch (Exception e) {
log.error("获取退款原因列表失败", e);
return new PageInfo<>();
}
}
@Override
public Boolean delete(List<String> reasonIds) {
try {
if (reasonIds == null || reasonIds.isEmpty()) {
return false;
}
List<KylinOrderRefundReasons> list = reasonsMapper.selectList(
new QueryWrapper<KylinOrderRefundReasons>()
.in("reason_id", reasonIds)
.eq("is_deleted", 0)
);
if (list == null || list.isEmpty()) {
return false;
}
for (KylinOrderRefundReasons item : list) {
if (item.getIsApplied() != null && item.getIsApplied() == 1) {
return false;
}
}
// 逻辑删除,保留 reason_id 供历史退款单关联
KylinOrderRefundReasons update = new KylinOrderRefundReasons();
update.setIsDeleted(1);
update.setUpdatedAt(LocalDateTime.now());
reasonsMapper.update(update, new UpdateWrapper<KylinOrderRefundReasons>()
.in("reason_id", reasonIds)
.eq("is_deleted", 0));
refreshAppliedCache();
return true;
} catch (Exception e) {
log.error("删除退款原因失败", e);
return false;
}
}
@Override
public Boolean apply(String reasonId, Integer isApplied) {
try {
if (StringUtils.isBlank(reasonId) || isApplied == null || (isApplied != 0 && isApplied != 1)) {
return false;
}
KylinOrderRefundReasons existing = reasonsMapper.selectOne(
new QueryWrapper<KylinOrderRefundReasons>()
.eq("reason_id", reasonId)
.eq("is_deleted", 0)
);
if (existing == null) {
return false;
}
KylinOrderRefundReasons update = new KylinOrderRefundReasons();
update.setIsApplied(isApplied);
update.setUpdatedAt(LocalDateTime.now());
reasonsMapper.update(update, new UpdateWrapper<KylinOrderRefundReasons>()
.eq("reason_id", reasonId)
.eq("is_deleted", 0));
refreshAppliedCache();
return true;
} catch (Exception e) {
log.error("应用/取消应用退款原因失败", e);
return false;
}
}
@Override
public List<OrderRefundReasonVo> listApplied() {
List<OrderRefundReasonVo> cached = dataUtils.getOrderRefundReasonsApplied();
if (cached != null) {
return cached;
}
return refreshAppliedCache();
}
private List<OrderRefundReasonVo> refreshAppliedCache() {
List<KylinOrderRefundReasons> list = reasonsMapper.listApplied();
List<OrderRefundReasonVo> voList = new ArrayList<>();
if (list != null) {
for (KylinOrderRefundReasons item : list) {
voList.add(toVo(item));
}
}
dataUtils.setOrderRefundReasonsApplied(voList);
return voList;
}
private OrderRefundReasonVo toVo(KylinOrderRefundReasons data) {
OrderRefundReasonVo vo = new OrderRefundReasonVo();
BeanUtils.copyProperties(data, vo);
vo.setIsAppliedName(formatAppliedName(data.getIsApplied()));
if (data.getCreatedAt() != null) {
vo.setCreatedAt(DateUtil.Formatter.yyyyMMddHHmmss.format(data.getCreatedAt()));
}
if (data.getUpdatedAt() != null) {
vo.setUpdatedAt(DateUtil.Formatter.yyyyMMddHHmmss.format(data.getUpdatedAt()));
}
return vo;
}
private String formatAppliedName(Integer isApplied) {
if (isApplied != null && isApplied == 1) {
return "应用中";
}
return "未应用";
}
}
......@@ -31,6 +31,7 @@ import com.liquidnet.service.kylin.dto.vo.returns.RefundOrderDetailsVo;
import com.liquidnet.service.kylin.entity.*;
import com.liquidnet.service.kylin.mapper.*;
import com.liquidnet.service.kylin.service.IKylinOrderRefundsService;
import com.liquidnet.service.kylin.utils.OrderRefundReasonUtils;
import com.mongodb.BasicDBObject;
import com.mongodb.client.result.UpdateResult;
import lombok.extern.slf4j.Slf4j;
......@@ -89,6 +90,9 @@ public class KylinOrderRefundsServiceImpl extends ServiceImpl<KylinOrderRefundsM
@Autowired
private KylinOrderRefundPicMapper kylinOrderRefundPicMapper;
@Autowired
private KylinOrderRefundReasonAdminServiceImpl refundReasonAdminService;
@Autowired
MongoTemplate mongoTemplate;
@Autowired
......@@ -96,6 +100,10 @@ public class KylinOrderRefundsServiceImpl extends ServiceImpl<KylinOrderRefundsM
public ResponseDto refundApply(RefundApplyParam refundApplyParam, String authId, String authName) {
try {
ResponseDto reasonCheck = fillRefundReason(refundApplyParam);
if (reasonCheck != null) {
return reasonCheck;
}
String orderTicketsId = refundApplyParam.getOrderTicketsId();
List<String> ticketEntityIds = refundApplyParam.getTicketEntityIds();
List<BigDecimal> entitiesPrice = refundApplyParam.getEntitiesPrice();
......@@ -574,4 +582,21 @@ public class KylinOrderRefundsServiceImpl extends ServiceImpl<KylinOrderRefundsM
}
return orderCode;
}
/**
* 校验并填充退款原因快照(从已应用配置缓存)
*
* @return 失败时返回 ResponseDto;成功返回 null
*/
private ResponseDto fillRefundReason(RefundApplyParam param) {
OrderRefundReasonUtils.ResolvedRefundReason resolved = OrderRefundReasonUtils.resolve(
param.getReasonId(), param.getReasonDetail(), refundReasonAdminService.listApplied());
if (!resolved.isSuccess()) {
return ResponseDto.failure(resolved.getErrorMsg());
}
param.setReasonId(resolved.getReasonId());
param.setReasonDetail(resolved.getReasonDetail());
param.setReason(resolved.getReasonSnapshot());
return null;
}
}
......@@ -52,7 +52,7 @@ public class KylinRefundExecuteServiceImpl {
private KylinOrderRefundsServiceImpl kylinOrderRefundsServiceImpl;
@Async
public void refundBatchApply(String targetId, String refundBatchId, String reason, String authId, String authName) {
public void refundBatchApply(String targetId, String refundBatchId, String reason, String reasonId, String reasonDetail, String authId, String authName) {
RefundApplyParam refundApplyParam = new RefundApplyParam();
int count;
int limitNum = 10;
......@@ -70,6 +70,8 @@ public class KylinRefundExecuteServiceImpl {
refundApplyParam.setOrderRefundBatchesId(refundBatchId);
refundApplyParam.setReason(reason);
refundApplyParam.setReasonId(reasonId);
refundApplyParam.setReasonDetail(reasonDetail);
refundApplyParam.setOrderTicketsId(order.getOrderTicketsId());
refundApplyParam.setTicketEntityIds(ticketEntityIds);
if (null != order.getPriceExpress()) {
......
......@@ -21,6 +21,7 @@ import com.liquidnet.service.kylin.mapper.KylinOrderRefundBatchesMapper;
import com.liquidnet.service.kylin.mapper.KylinOrderRefundsMapper;
import com.liquidnet.service.kylin.mapper.KylinOrderTicketsMapper;
import com.liquidnet.service.kylin.mapper.KylinPerformancesMapper;
import com.liquidnet.service.kylin.utils.OrderRefundReasonUtils;
import com.liquidnet.service.dragon.support.WepayAppletPaySupport;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
......@@ -65,7 +66,21 @@ public class KylinRefundPerformancesAdminServiceImpl {
@Autowired
RedisDataSourceUtil redisDataSourceUtil;
@Autowired
private KylinOrderRefundReasonAdminServiceImpl refundReasonAdminService;
public ResponseDto refundBatchApply(RefundBatchApplyParam refundBatchApplyParam) {
OrderRefundReasonUtils.ResolvedRefundReason resolved = OrderRefundReasonUtils.resolve(
refundBatchApplyParam.getReasonId(),
refundBatchApplyParam.getReasonDetail(),
refundReasonAdminService.listApplied());
if (!resolved.isSuccess()) {
return ResponseDto.failure(resolved.getErrorMsg());
}
refundBatchApplyParam.setReasonId(resolved.getReasonId());
refundBatchApplyParam.setReasonDetail(resolved.getReasonDetail());
refundBatchApplyParam.setReason(resolved.getReasonSnapshot());
String targetId = refundBatchApplyParam.getTargetId();
// 查询需要退的金额和数量
String[] paymentTypeAlipay = WepayAppletPaySupport.alipayPaymentTypesForRefundStatis();
......@@ -122,7 +137,8 @@ public class KylinRefundPerformancesAdminServiceImpl {
// 开始执行批量提交审核 分批处理退款申请
String authId = ShiroUtils.getUserId().toString();
String authName = ShiroUtils.getLoginName();
kylinRefundExecuteServiceImpl.refundBatchApply(targetId, refundBatchId, refundBatchApplyParam.getReason(), authId, authName);
kylinRefundExecuteServiceImpl.refundBatchApply(targetId, refundBatchId, refundBatchApplyParam.getReason(),
refundBatchApplyParam.getReasonId(), refundBatchApplyParam.getReasonDetail(), authId, authName);
return ResponseDto.success();
} else {
return ResponseDto.failure("操作失败");
......
......@@ -172,6 +172,8 @@ public class KylinRefundsStatusServiceImpl {
kylinOrderRefunds.setApplicantName(authName);
kylinOrderRefunds.setApplicantAt(LocalDateTime.now());
kylinOrderRefunds.setReason(reason);
kylinOrderRefunds.setReasonId(refundApplyParam.getReasonId());
kylinOrderRefunds.setReasonDetail(refundApplyParam.getReasonDetail());
kylinOrderRefunds.setCreatedAt(LocalDateTime.now());
int rows = kylinOrderRefundsMapper.insert(kylinOrderRefunds);
// 添加缓存
......
......@@ -10,6 +10,7 @@ import com.liquidnet.service.base.constant.RedisKeyExpireConst;
import com.liquidnet.service.candy.param.BackCouponParam;
import com.liquidnet.service.kylin.constant.KylinRedisConst;
import com.liquidnet.service.kylin.dto.vo.KylinExpressModuleVo;
import com.liquidnet.service.kylin.dto.vo.OrderRefundReasonVo;
import com.liquidnet.service.kylin.dto.vo.admin.OrderRefundAddress;
import com.liquidnet.service.kylin.dto.vo.admin.OrderRefundPoundage;
import com.liquidnet.service.kylin.dto.vo.express.KylinOrderExpressVo;
......@@ -464,6 +465,23 @@ public class DataUtils {
redisDataSourceUtil.getRedisKylinUtil().set(redisKey, voList);
}
public void setOrderRefundReasonsApplied(List<OrderRefundReasonVo> voList) {
redisDataSourceUtil.getRedisKylinUtil().set(KylinRedisConst.KYLIN_ORDER_REFUND_REASONS_APPLIED, voList);
}
@SuppressWarnings("unchecked")
public List<OrderRefundReasonVo> getOrderRefundReasonsApplied() {
Object obj = redisDataSourceUtil.getRedisKylinUtil().get(KylinRedisConst.KYLIN_ORDER_REFUND_REASONS_APPLIED);
if (obj == null) {
return null;
}
return (List<OrderRefundReasonVo>) obj;
}
public void delOrderRefundReasonsApplied() {
redisDataSourceUtil.getRedisKylinUtil().del(KylinRedisConst.KYLIN_ORDER_REFUND_REASONS_APPLIED);
}
/**
* 获取 票种快递票模板关联vo [第三方非线上]
*
......
......@@ -77,6 +77,12 @@ public class SmsMessage implements Serializable, Cloneable {
return this;
}
/** 无变量模板时传空对象 {},避免 templateParam 为 null */
public SmsMessage setEmptyTemplateParam() {
this.templateParam = JsonUtils.OM().createObjectNode();
return this;
}
public String toJson() {
return JsonUtils.toJson(this);
}
......
......@@ -17,6 +17,9 @@ public class RedisKeyExpireConst {
// kylin:order:list:userId 过期时间为30天
public static final long KYLIN_ORDER_LIST_USERID_EXPIRE = 30 * 24 * 60 * 60;
// kylin:groupBuy 过期时间为30天
public static final long KYLIN_GROUP_BUY_EXPIRE = 30 * 24 * 60 * 60;
// adam:info:enters 过期时间为30天
public static final long ADAM_INFO_ENTERS_EXPIRE = 30 * 24 * 60 * 60;
......
......@@ -82,6 +82,24 @@ public class SmsEnum {
* - 模版内容:欢迎加入登登登VIP大家庭。请微信关注“摩登天空服务号”,回复VIP,获取专属小助理客服服务了,回T退订
*/
SMS_232908714,
/**
* 拼团成功
* - 恭喜拼团成功,您购买的是「${name}」演出的「${time}」场次「${ticket}」票种,祝您观演愉快
* - {"name":"...","time":"...","ticket":"..."}
*/
SMS_510540063,
/**
* 拼团失败/超时未成团
* - 实在抱歉,您的拼团订单在「${hours}」小时内无人拼单,拼团失败,已支付金额会在 1-3 个工作日原路退回,请注意查收。
* - {"hours":"24"}
*/
SMS_510615071,
/**
* 拼团倒计时剩 1 小时提醒
* - 您的拼团订单还有 1 小时到期,快转发朋友一起拼单吧,逾期未成团,订单将自动取消。
* - {}
*/
SMS_510285138,
;
}
......
......@@ -163,6 +163,9 @@ global-auth:
- ${liquidnet.info.context}/performance/artists/**
# 艺人详情 C 端(无需登录)
- ${liquidnet.info.context}/artist/**
# 拼团
- ${liquidnet.info.context}/groupBuy/detail
- ${liquidnet.info.context}/groupBuy/activities
oncheck-url-pattern:
- ${liquidnet.info.context}/order/details
- ${liquidnet.info.context}/order/transfer*
......
package com.liquidnet.service.kylin.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serializable;
import java.time.LocalDateTime;
@Data
@EqualsAndHashCode(callSuper = false)
public class KylinGroupBuyActivity implements Serializable, Cloneable {
private static final long serialVersionUID = 1L;
@TableId(value = "mid", type = IdType.AUTO)
private Integer mid;
private String activityId;
private String name;
private String performanceId;
private Integer minGroupSize;
private Integer maxBuyPerUser;
private LocalDateTime startTime;
private LocalDateTime endTime;
private Integer launchType;
private LocalDateTime scheduledTime;
private Integer status;
private String description;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
private static final KylinGroupBuyActivity obj = new KylinGroupBuyActivity();
public static KylinGroupBuyActivity getNew() {
try {
return (KylinGroupBuyActivity) obj.clone();
} catch (CloneNotSupportedException e) {
return new KylinGroupBuyActivity();
}
}
}
package com.liquidnet.service.kylin.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@Data
@EqualsAndHashCode(callSuper = false)
public class KylinGroupBuyDiscount implements Serializable, Cloneable {
private static final long serialVersionUID = 1L;
@TableId(value = "mid", type = IdType.AUTO)
private Integer mid;
private String discountId;
private String activityId;
private String performanceId;
private String timeId;
private String ticketId;
private Integer discountType;
private BigDecimal discountValue;
private LocalDateTime createdAt;
private static final KylinGroupBuyDiscount obj = new KylinGroupBuyDiscount();
public static KylinGroupBuyDiscount getNew() {
try {
return (KylinGroupBuyDiscount) obj.clone();
} catch (CloneNotSupportedException e) {
return new KylinGroupBuyDiscount();
}
}
}
package com.liquidnet.service.kylin.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serializable;
import java.time.LocalDateTime;
@Data
@EqualsAndHashCode(callSuper = false)
public class KylinGroupBuyMember implements Serializable, Cloneable {
private static final long serialVersionUID = 1L;
@TableId(value = "mid", type = IdType.AUTO)
private Integer mid;
private String memberId;
private String groupOrderId;
private String userId;
private String orderTicketsId;
private Integer ticketCount;
private Integer status;
private LocalDateTime joinTime;
private LocalDateTime createdAt;
private static final KylinGroupBuyMember obj = new KylinGroupBuyMember();
public static KylinGroupBuyMember getNew() {
try {
return (KylinGroupBuyMember) obj.clone();
} catch (CloneNotSupportedException e) {
return new KylinGroupBuyMember();
}
}
}
package com.liquidnet.service.kylin.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serializable;
import java.time.LocalDateTime;
@Data
@EqualsAndHashCode(callSuper = false)
public class KylinGroupBuyOrder implements Serializable, Cloneable {
private static final long serialVersionUID = 1L;
@TableId(value = "mid", type = IdType.AUTO)
private Integer mid;
private String groupOrderId;
private String activityId;
private String leaderUserId;
private String orderTicketsId;
private Integer targetCount;
private Integer currentCount;
private Integer status;
private Integer isPublic;
private String shareToken;
private LocalDateTime expiresAt;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
private static final KylinGroupBuyOrder obj = new KylinGroupBuyOrder();
public static KylinGroupBuyOrder getNew() {
try {
return (KylinGroupBuyOrder) obj.clone();
} catch (CloneNotSupportedException e) {
return new KylinGroupBuyOrder();
}
}
}
package com.liquidnet.service.kylin.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* 演出票退款原因配置表
*/
@Data
@EqualsAndHashCode(callSuper = false)
public class KylinOrderRefundReasons implements Serializable, Cloneable {
private static final long serialVersionUID = 1L;
@TableId(value = "mid", type = IdType.AUTO)
private Integer mid;
/**
* 业务ID
*/
private String reasonId;
/**
* 退款理由名称
*/
private String name;
/**
* 是否需要二级补充输入 0否 1是
*/
private Integer needDetail;
/**
* 二级输入 placeholder
*/
private String placeholder;
/**
* 是否已应用 0未应用 1应用中
*/
private Integer isApplied;
/**
* 是否删除 0未删除 1已删除
*/
private Integer isDeleted;
/**
* 排序,越大越靠前
*/
private Integer sort;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
@TableField(exist = false)
private String isAppliedName;
private static final KylinOrderRefundReasons OBJ = new KylinOrderRefundReasons();
public static KylinOrderRefundReasons getNew() {
try {
return (KylinOrderRefundReasons) OBJ.clone();
} catch (CloneNotSupportedException e) {
return new KylinOrderRefundReasons();
}
}
}
......@@ -106,10 +106,20 @@ public class KylinOrderRefunds implements Serializable, Cloneable{
private LocalDateTime applicantAt;
/**
* 退款原因
* 退款原因(展示快照)
*/
private String reason;
/**
* 退款原因配置ID
*/
private String reasonId;
/**
* 退款原因二级补充说明
*/
private String reasonDetail;
/**
* 审核人id
*/
......
package com.liquidnet.service.kylin.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.liquidnet.service.kylin.entity.KylinGroupBuyActivity;
public interface KylinGroupBuyActivityMapper extends BaseMapper<KylinGroupBuyActivity> {
}
package com.liquidnet.service.kylin.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.liquidnet.service.kylin.entity.KylinGroupBuyDiscount;
public interface KylinGroupBuyDiscountMapper extends BaseMapper<KylinGroupBuyDiscount> {
}
package com.liquidnet.service.kylin.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.liquidnet.service.kylin.entity.KylinGroupBuyMember;
public interface KylinGroupBuyMemberMapper extends BaseMapper<KylinGroupBuyMember> {
}
package com.liquidnet.service.kylin.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.liquidnet.service.kylin.entity.KylinGroupBuyOrder;
public interface KylinGroupBuyOrderMapper extends BaseMapper<KylinGroupBuyOrder> {
}
package com.liquidnet.service.kylin.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.liquidnet.service.kylin.entity.KylinOrderRefundReasons;
import java.util.List;
import java.util.Map;
/**
* 演出票退款原因配置 Mapper
*/
public interface KylinOrderRefundReasonsMapper extends BaseMapper<KylinOrderRefundReasons> {
List<KylinOrderRefundReasons> searchList(Map<String, Object> params);
List<KylinOrderRefundReasons> listApplied();
}
<?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.liquidnet.service.kylin.mapper.KylinOrderRefundReasonsMapper">
<select id="searchList" parameterType="java.util.Map" resultType="com.liquidnet.service.kylin.entity.KylinOrderRefundReasons">
SELECT
mid,
reason_id,
name,
need_detail,
placeholder,
is_applied,
sort,
is_deleted,
created_at,
updated_at
FROM kylin_order_refund_reasons
<where>
AND is_deleted = 0
<if test="name != null and name != ''">
AND name LIKE CONCAT('%', #{name}, '%')
</if>
<!-- isApplied=0 时不能写 != '',OGNL 会把 0 当成空 -->
<if test="isApplied != null">
AND is_applied = #{isApplied}
</if>
</where>
ORDER BY sort DESC, mid DESC
</select>
<select id="listApplied" resultType="com.liquidnet.service.kylin.entity.KylinOrderRefundReasons">
SELECT
mid,
reason_id,
name,
need_detail,
placeholder,
is_applied,
sort,
is_deleted,
created_at,
updated_at
FROM kylin_order_refund_reasons
WHERE is_applied = 1
AND is_deleted = 0
ORDER BY sort DESC, mid DESC
</select>
</mapper>
......@@ -54,4 +54,13 @@ public interface FeignPlatformTaskClient {
@GetMapping("platform/refund/failRefund")
ResponseDto<Boolean> failRefund();
@PostMapping("platform/groupBuy/timeout")
ResponseDto<Boolean> groupBuyTimeout();
@PostMapping("platform/groupBuy/activitySchedule")
ResponseDto<Boolean> groupBuyActivitySchedule();
@PostMapping("platform/groupBuy/remind1h")
ResponseDto<Boolean> groupBuyRemind1h();
}
......@@ -9,13 +9,18 @@ import com.liquidnet.commons.lang.util.HttpUtil;
import com.liquidnet.commons.lang.util.JsonUtils;
import com.liquidnet.service.base.SqlMapping;
import com.liquidnet.service.base.constant.MQConst;
import com.liquidnet.service.base.constant.RedisKeyExpireConst;
import com.liquidnet.service.consumer.order.utils.GoblinNftUtils;
import com.liquidnet.service.consumer.order.utils.KylinOrderUtils;
import com.liquidnet.service.goblin.constant.GoblinRedisConst;
import com.liquidnet.service.goblin.constant.GoblinStatusConst;
import com.liquidnet.service.goblin.dto.vo.*;
import com.liquidnet.service.goblin.param.BackCouponParam;
import com.liquidnet.service.kylin.constant.GroupBuyStatusConst;
import com.liquidnet.service.kylin.constant.KylinRedisConst;
import com.liquidnet.service.kylin.constant.KylinTableStatusConst;
import com.liquidnet.service.kylin.dto.vo.groupbuy.KylinGroupBuyMemberVo;
import com.liquidnet.service.kylin.dto.vo.groupbuy.KylinGroupBuyOrderVo;
import com.liquidnet.service.kylin.dto.vo.mongo.KylinOrderTicketEntitiesVo;
import com.liquidnet.service.kylin.dto.vo.mongo.KylinOrderTicketVo;
import com.liquidnet.service.kylin.entity.KylinOrderCoupons;
......@@ -386,8 +391,17 @@ public abstract class AbstractOrderCloseReceiver implements StreamListener<Strin
sqlDataOrderRelation.add(new Object[]{
now, kylinOrderTicketVo.getOrderTicketsId(),
});
sendMsgByRedis(MQConst.KylinQueue.SQL_ORDER_PAY.getKey(),
SqlMapping.gets(sqls, sqlDataOrder, sqlDataOrderStatus, sqlDataOrderRelation));
// 拼团:Mongo + Redis + MySQL(MQ) 回收未支付占坑
LinkedList<Object[]> gbMember = CollectionUtil.linkedListObjectArr();
LinkedList<Object[]> gbOrder = CollectionUtil.linkedListObjectArr();
boolean hasGroupBuySql = appendGroupBuyCloseSql(kylinOrderTicketVo, now, sqls, gbMember, gbOrder);
if (hasGroupBuySql) {
sendMsgByRedis(MQConst.KylinQueue.SQL_ORDER_PAY.getKey(),
SqlMapping.gets(sqls, sqlDataOrder, sqlDataOrderStatus, sqlDataOrderRelation, gbMember, gbOrder));
} else {
sendMsgByRedis(MQConst.KylinQueue.SQL_ORDER_PAY.getKey(),
SqlMapping.gets(sqls, sqlDataOrder, sqlDataOrderStatus, sqlDataOrderRelation));
}
}
return true;
} catch (Exception e) {
......@@ -396,6 +410,107 @@ public abstract class AbstractOrderCloseReceiver implements StreamListener<Strin
}
}
/**
* 关单回收拼团占坑:先改 Mongo/Redis,再组装 MySQL SQL。
* @return 是否追加了拼团 SQL
*/
private boolean appendGroupBuyCloseSql(KylinOrderTicketVo ticket, LocalDateTime now,
LinkedList<String> sqls,
LinkedList<Object[]> gbMember,
LinkedList<Object[]> gbOrder) {
KylinGroupBuyOrderVo group = findGroupBuyForClose(ticket);
if (group == null || CollectionUtil.isEmpty(group.getMembers())) {
return false;
}
String lockKey = KylinRedisConst.GROUP_BUY_ORDER_LOCK.concat(group.getGroupOrderId());
if (!redisDataSourceUtil.getRedisKylinUtil().lock(lockKey, 1, 15)) {
log.warn("关单拼团获取锁失败 orderTicketsId={}, groupOrderId={}",
ticket.getOrderTicketsId(), group.getGroupOrderId());
return false;
}
try {
group = mongoTemplate.findOne(
Query.query(Criteria.where("groupOrderId").is(group.getGroupOrderId())),
KylinGroupBuyOrderVo.class, KylinGroupBuyOrderVo.class.getSimpleName());
if (group == null || CollectionUtil.isEmpty(group.getMembers())) {
return false;
}
KylinGroupBuyMemberVo member = null;
for (KylinGroupBuyMemberVo m : group.getMembers()) {
if (ticket.getOrderTicketsId().equals(m.getOrderTicketsId())) {
member = m;
break;
}
}
if (member == null || !GroupBuyStatusConst.MemberStatus.PENDING_PAY.equals(member.getStatus())) {
return false;
}
boolean isLeaderPrepare = GroupBuyStatusConst.OrderStatus.PREPARE.equals(group.getStatus())
&& Objects.equals(group.getLeaderUserId(), ticket.getUserId());
member.setStatus(GroupBuyStatusConst.MemberStatus.CANCELLED);
String nowStr = DateUtil.Formatter.yyyyMMddHHmmss.format(now);
if (isLeaderPrepare) {
group.setStatus(GroupBuyStatusConst.OrderStatus.CANCELLED);
group.setCurrentCount(0);
} else if (GroupBuyStatusConst.OrderStatus.PENDING.equals(group.getStatus())
|| GroupBuyStatusConst.OrderStatus.PREPARE.equals(group.getStatus())) {
int count = group.getCurrentCount() == null ? 0 : group.getCurrentCount();
group.setCurrentCount(Math.max(0, count - 1));
}
group.setUpdatedAt(nowStr);
mongoTemplate.remove(Query.query(Criteria.where("groupOrderId").is(group.getGroupOrderId())),
KylinGroupBuyOrderVo.class, KylinGroupBuyOrderVo.class.getSimpleName());
mongoTemplate.insert(group, KylinGroupBuyOrderVo.class.getSimpleName());
if (isLeaderPrepare) {
redisDataSourceUtil.getRedisKylinUtil().del(KylinRedisConst.GROUP_BUY_ORDER.concat(group.getGroupOrderId()));
if (StringUtils.isNotBlank(group.getShareToken())) {
redisDataSourceUtil.getRedisKylinUtil().del(KylinRedisConst.GROUP_BUY_ORDER_SHARE.concat(group.getShareToken()));
}
} else {
redisDataSourceUtil.getRedisKylinUtil().set(KylinRedisConst.GROUP_BUY_ORDER.concat(group.getGroupOrderId()),
group, RedisKeyExpireConst.KYLIN_GROUP_BUY_EXPIRE);
if (StringUtils.isNotBlank(group.getShareToken())) {
redisDataSourceUtil.getRedisKylinUtil().set(KylinRedisConst.GROUP_BUY_ORDER_SHARE.concat(group.getShareToken()),
group, RedisKeyExpireConst.KYLIN_GROUP_BUY_EXPIRE);
}
}
sqls.add(SqlMapping.get("kylin_group_buy_member.cancel_by_order_tickets"));
gbMember.add(new Object[]{ticket.getOrderTicketsId()});
if (isLeaderPrepare) {
sqls.add(SqlMapping.get("kylin_group_buy_order.cancel_if_prepare"));
gbOrder.add(new Object[]{now, group.getGroupOrderId()});
} else {
sqls.add(SqlMapping.get("kylin_group_buy_order.decr_count_if_pending"));
gbOrder.add(new Object[]{now, group.getGroupOrderId()});
}
return true;
} finally {
redisDataSourceUtil.getRedisKylinUtil().uLock(lockKey);
}
}
private KylinGroupBuyOrderVo findGroupBuyForClose(KylinOrderTicketVo ticket) {
if (StringUtils.isNotBlank(ticket.getGroupOrderId())) {
Object cached = redisDataSourceUtil.getRedisKylinUtil()
.get(KylinRedisConst.GROUP_BUY_ORDER.concat(ticket.getGroupOrderId()));
if (cached instanceof KylinGroupBuyOrderVo) {
return (KylinGroupBuyOrderVo) cached;
}
KylinGroupBuyOrderVo byId = mongoTemplate.findOne(
Query.query(Criteria.where("groupOrderId").is(ticket.getGroupOrderId())),
KylinGroupBuyOrderVo.class, KylinGroupBuyOrderVo.class.getSimpleName());
if (byId != null) {
return byId;
}
}
return mongoTemplate.findOne(
Query.query(Criteria.where("members.orderTicketsId").is(ticket.getOrderTicketsId())),
KylinGroupBuyOrderVo.class, KylinGroupBuyOrderVo.class.getSimpleName());
}
private UpdateResult updateGoblinStoreOrderVo(String orderId, GoblinStoreOrderVo data) {
BasicDBObject object = cloneBasicDBObject().append("$set", mongoConverter.convertToMongoType(data));
return mongoTemplate.getCollection(GoblinStoreOrderVo.class.getSimpleName()).updateOne(
......
......@@ -9,4 +9,13 @@ goblin_activity.code.admin=UPDATE goblin_nft_ex_code SET admin_uid = ? where co
#---- 关闭票务订单
kylin_order_tickets.close=UPDATE kylin_order_tickets SET updated_at = ? where order_tickets_id = ?
kylin_order_ticket_status.close=UPDATE kylin_order_ticket_status SET status = ? , updated_at = ? where order_id = ?
kylin_order_ticket_relations.close=UPDATE kylin_order_ticket_relations SET updated_at = ? where order_id = ?
\ No newline at end of file
kylin_order_ticket_relations.close=UPDATE kylin_order_ticket_relations SET updated_at = ? where order_id = ?
# ------------------------演出票拼团-未支付关单----------------------------
kylin_group_buy_member.cancel_by_order_tickets=UPDATE kylin_group_buy_member SET status=2 WHERE order_tickets_id=? AND status=-1
kylin_group_buy_order.cancel_if_prepare=UPDATE kylin_group_buy_order SET status=3, current_count=0, updated_at=? WHERE group_order_id=? AND status=-1
kylin_group_buy_order.decr_count_if_pending=UPDATE kylin_group_buy_order SET current_count=current_count-1, updated_at=? WHERE group_order_id=? AND status=0 AND current_count>0
kylin_group_buy_order.delete=DELETE FROM kylin_group_buy_order WHERE group_order_id = ?
kylin_group_buy_order.insert=INSERT INTO kylin_group_buy_order (group_order_id,activity_id,leader_user_id,order_tickets_id,target_count,current_count,status,is_public,share_token,expires_at,created_at,updated_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?)
kylin_group_buy_member.delete_by_group=DELETE FROM kylin_group_buy_member WHERE group_order_id = ?
kylin_group_buy_member.insert=INSERT INTO kylin_group_buy_member (member_id,group_order_id,user_id,order_tickets_id,ticket_count,status,join_time,created_at) VALUES (?,?,?,?,?,?,?,?)
\ No newline at end of file
......@@ -122,4 +122,34 @@ public class KylinTaskHandler {
}
}
@XxlJob(value = "sev-platform:groupBuyTimeout")
public void groupBuyTimeout() {
try {
XxlJobHelper.handleSuccess("结果:" + feignPlatformTaskClient.groupBuyTimeout().getData());
} catch (Exception e) {
XxlJobHelper.log(e);
XxlJobHelper.handleFail();
}
}
@XxlJob(value = "sev-platform:groupBuyActivitySchedule")
public void groupBuyActivitySchedule() {
try {
XxlJobHelper.handleSuccess("结果:" + feignPlatformTaskClient.groupBuyActivitySchedule().getData());
} catch (Exception e) {
XxlJobHelper.log(e);
XxlJobHelper.handleFail();
}
}
@XxlJob(value = "sev-platform:groupBuyRemind1h")
public void groupBuyRemind1h() {
try {
XxlJobHelper.handleSuccess("结果:" + feignPlatformTaskClient.groupBuyRemind1h().getData());
} catch (Exception e) {
XxlJobHelper.log(e);
XxlJobHelper.handleFail();
}
}
}
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for kylin_group_buy_activity
-- ----------------------------
CREATE TABLE IF NOT EXISTS `kylin_group_buy_activity` (
`mid` int NOT NULL AUTO_INCREMENT,
`activity_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '拼团活动ID',
`name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '活动名称',
`performance_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '演出ID',
`min_group_size` int NOT NULL COMMENT '成团人数',
`max_buy_per_user` int NOT NULL COMMENT '每人限购',
`start_time` datetime NOT NULL COMMENT '活动开始时间',
`end_time` datetime NOT NULL COMMENT '活动结束时间',
`launch_type` tinyint NOT NULL COMMENT '启动方式 1立即 2手动 3定时',
`scheduled_time` datetime DEFAULT NULL COMMENT '定时启动时间',
`status` tinyint NOT NULL COMMENT '状态 0草稿 1待启动 2进行中 3已关闭 4已结束',
`description` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '活动说明',
`created_at` datetime NOT NULL COMMENT '创建时间',
`updated_at` datetime NOT NULL COMMENT '更新时间',
PRIMARY KEY (`mid`) USING BTREE,
UNIQUE KEY `uk_activity_id` (`activity_id`) USING BTREE,
KEY `idx_performance_status` (`performance_id`, `status`) USING BTREE,
KEY `idx_status_schedule` (`status`, `launch_type`, `scheduled_time`) USING BTREE,
KEY `idx_status_end_time` (`status`, `end_time`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=Dynamic COMMENT='演出票拼团活动';
-- ----------------------------
-- Table structure for kylin_group_buy_discount
-- ----------------------------
CREATE TABLE IF NOT EXISTS `kylin_group_buy_discount` (
`mid` int NOT NULL AUTO_INCREMENT,
`discount_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '优惠ID',
`activity_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '拼团活动ID',
`performance_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '演出ID',
`time_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '场次ID',
`ticket_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '票种ID',
`discount_type` tinyint NOT NULL COMMENT '优惠类型 1固定价格 2折扣 3满减',
`discount_value` decimal(10,2) NOT NULL COMMENT '优惠值',
`created_at` datetime NOT NULL COMMENT '创建时间',
PRIMARY KEY (`mid`) USING BTREE,
UNIQUE KEY `uk_discount_id` (`discount_id`) USING BTREE,
UNIQUE KEY `uk_activity_ticket` (`activity_id`, `time_id`, `ticket_id`) USING BTREE,
KEY `idx_performance_ticket` (`performance_id`, `time_id`, `ticket_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=Dynamic COMMENT='演出票拼团优惠配置';
-- ----------------------------
-- Table structure for kylin_group_buy_order
-- ----------------------------
CREATE TABLE IF NOT EXISTS `kylin_group_buy_order` (
`mid` int NOT NULL AUTO_INCREMENT,
`group_order_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '拼团订单ID',
`activity_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '拼团活动ID',
`leader_user_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '团长用户ID',
`order_tickets_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '团长票务订单ID',
`target_count` int NOT NULL COMMENT '目标成团人数',
`current_count` int NOT NULL DEFAULT 0 COMMENT '当前参团人数',
`status` tinyint NOT NULL COMMENT '状态 -1待支付发起单 0拼单中 1拼成 2失败 3已取消',
`is_public` tinyint NOT NULL DEFAULT 1 COMMENT '是否公开 1公开 0私密',
`share_token` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '分享token',
`expires_at` datetime DEFAULT NULL COMMENT '拼团过期时间',
`created_at` datetime NOT NULL COMMENT '创建时间',
`updated_at` datetime NOT NULL COMMENT '更新时间',
PRIMARY KEY (`mid`) USING BTREE,
UNIQUE KEY `uk_group_order_id` (`group_order_id`) USING BTREE,
UNIQUE KEY `uk_share_token` (`share_token`) USING BTREE,
KEY `idx_activity_status` (`activity_id`, `status`) USING BTREE,
KEY `idx_status_expires` (`status`, `expires_at`) USING BTREE,
KEY `idx_leader_status` (`leader_user_id`, `status`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=Dynamic COMMENT='演出票拼团订单';
-- ----------------------------
-- Table structure for kylin_group_buy_member
-- ----------------------------
CREATE TABLE IF NOT EXISTS `kylin_group_buy_member` (
`mid` int NOT NULL AUTO_INCREMENT,
`member_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '成员ID',
`group_order_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '拼团订单ID',
`user_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '用户ID',
`order_tickets_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '票务订单ID',
`ticket_count` int NOT NULL COMMENT '购票张数',
`status` tinyint NOT NULL DEFAULT -1 COMMENT '状态 -1待支付 0已加入 1已退款 2已取消',
`join_time` datetime NOT NULL COMMENT '加入时间',
`created_at` datetime NOT NULL COMMENT '创建时间',
PRIMARY KEY (`mid`) USING BTREE,
UNIQUE KEY `uk_member_id` (`member_id`) USING BTREE,
UNIQUE KEY `uk_order_tickets_id` (`order_tickets_id`) USING BTREE,
KEY `idx_group_order_id` (`group_order_id`) USING BTREE,
KEY `idx_user_id` (`user_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=Dynamic COMMENT='演出票拼团成员';
-- 条件状态迁移示例:
-- UPDATE kylin_group_buy_order SET status = 1, updated_at = NOW()
-- WHERE group_order_id = ? AND status = 0 AND current_count >= target_count;
-- UPDATE kylin_group_buy_order SET status = 2, updated_at = NOW()
-- WHERE group_order_id = ? AND status = 0 AND expires_at < NOW();
-- UPDATE kylin_group_buy_order SET status = 3, updated_at = NOW()
-- WHERE group_order_id = ? AND status = 0;
SET FOREIGN_KEY_CHECKS = 1;
......@@ -793,6 +793,28 @@ alter table kylin_order_refunds add ticket_audit_name varchar(100) NOT NULL DE
alter table kylin_order_refunds add ticket_audit_at timestamp NULL DEFAULT NULL COMMENT '票务审核时间' after ticket_audit_name;
alter table kylin_order_refunds add ticket_audit_remark varchar(200) NOT NULL DEFAULT '' COMMENT '票务审核备至' after ticket_audit_at;
alter table kylin_order_refunds add reason_id varchar(32) DEFAULT NULL COMMENT '退款原因配置ID' after reason;
alter table kylin_order_refunds add reason_detail varchar(200) DEFAULT NULL COMMENT '退款原因二级补充说明' after reason_id;
# 演出票退款原因配置表
drop TABLE if exists `kylin_order_refund_reasons`;
CREATE TABLE `kylin_order_refund_reasons`
(
`mid` int(11) NOT NULL AUTO_INCREMENT,
`reason_id` varchar(32) NOT NULL DEFAULT '' COMMENT '业务ID',
`name` varchar(100) NOT NULL DEFAULT '' COMMENT '退款理由名称',
`need_detail` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否需要二级补充输入 0否 1是',
`placeholder` varchar(200) NOT NULL DEFAULT '' COMMENT '二级输入placeholder',
`is_applied` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否已应用 0未应用 1应用中',
`sort` int(11) NOT NULL DEFAULT '0' COMMENT '排序,越大越靠前',
`created_at` datetime DEFAULT NULL,
`updated_at` datetime DEFAULT NULL,
PRIMARY KEY (`mid`),
UNIQUE KEY `uk_reason_id` (`reason_id`),
KEY `idx_is_applied_sort` (`is_applied`, `sort`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4 COMMENT ='演出票退款原因配置';
drop TABLE if exists `kylin_order_refund_entities`;
CREATE TABLE `kylin_order_refund_entities`
(
......
......@@ -55,6 +55,13 @@
<version>1.0-SNAPSHOT</version>
<scope>compile</scope>
</dependency>
<!-- SmsProcessor / SmsEnum:拼团短信 -->
<dependency>
<groupId>com.liquidnet</groupId>
<artifactId>liquidnet-common-sms</artifactId>
<version>1.0-SNAPSHOT</version>
<scope>compile</scope>
</dependency>
</dependencies>
<build>
......
......@@ -8,6 +8,7 @@ import com.liquidnet.service.adam.dto.vo.AdamUserMemberVo;
import com.liquidnet.service.base.ResponseDto;
import com.liquidnet.service.kylin.service.IKylinConsmerService;
import com.liquidnet.service.kylin.service.IKylinPerformancesService;
import com.liquidnet.service.kylin.service.impl.KylinOrderTicketsRefundServiceImpl;
import com.liquidnet.service.kylin.service.impl.KylinPerformancesServiceImpl;
import com.liquidnet.service.kylin.utils.DataUtils;
import io.swagger.annotations.Api;
......@@ -39,6 +40,9 @@ public class InnerController {
@Autowired
private KylinPerformancesServiceImpl kylinPerformancesService;
@Autowired
private KylinOrderTicketsRefundServiceImpl kylinOrderTicketsRefundService;
@GetMapping("cache/member/{uid}")
@ApiOperation("获取会员相关数据")
public void cacheMember(@NotBlank @PathVariable(value = "uid") String uid) {
......@@ -81,4 +85,17 @@ public class InnerController {
return ResponseDto.success(map);
}
/**
* 系统全额退款(拼团超时等平台任务调用)。走 /inner/**,无需登录态。
*/
@PostMapping("orderRefund/systemFullRefund")
@ApiOperation("系统全额退款")
public ResponseDto systemFullRefund(
@RequestParam("orderId") @NotBlank(message = "订单ID不能为空") String orderId,
@RequestParam("orderTicketEntitiesId") @NotBlank(message = "票单ID不能为空") String orderTicketEntitiesId,
@RequestParam(value = "reason", required = false) String reason
) {
return kylinOrderTicketsRefundService.systemFullRefund(orderId, orderTicketEntitiesId, reason);
}
}
package com.liquidnet.service.kylin.controller;
import com.github.pagehelper.PageInfo;
import com.liquidnet.service.base.ResponseDto;
import com.liquidnet.service.kylin.dto.vo.groupbuy.*;
import com.liquidnet.service.kylin.service.IKylinGroupBuyActivityService;
import com.liquidnet.service.kylin.service.IKylinGroupBuyOrderService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.validation.constraints.NotNull;
import java.util.List;
@Api(tags = "前端-拼团")
@RestController
@RequestMapping("/groupBuy")
@Validated
public class KylinGroupBuyController {
@Autowired
private IKylinGroupBuyActivityService activityService;
@Autowired
private IKylinGroupBuyOrderService orderService;
@GetMapping("/activities")
@ApiOperation("演出下进行中的拼团活动")
public ResponseDto<List<KylinGroupBuyActivityVo>> activities(
@RequestParam("performanceId") @NotNull(message = "演出ID不能为空") String performanceId) {
return activityService.activities(performanceId);
}
@GetMapping("/orders/pending")
@ApiOperation("待成团公开拼单")
public ResponseDto<List<KylinGroupBuyOrderCardVo>> pending(
@RequestParam("activityId") @NotNull(message = "活动ID不能为空") String activityId,
@RequestParam("timeId") @NotNull(message = "场次ID不能为空") String timeId,
@RequestParam("ticketId") @NotNull(message = "票种ID不能为空") String ticketId) {
return orderService.pending(activityId, timeId, ticketId);
}
@GetMapping("/my")
@ApiOperation("我的拼团列表")
public ResponseDto<PageInfo<KylinGroupBuyMyGroupVo>> my(
@RequestParam(value = "page", required = false, defaultValue = "1") Integer page,
@RequestParam(value = "size", required = false, defaultValue = "20") Integer size) {
return orderService.my(page, size);
}
@GetMapping("/detail")
@ApiOperation("拼团详情")
public ResponseDto<KylinGroupBuyOrderDetailVo> detail(@RequestParam(value = "groupOrderId", required = false) String groupOrderId,
@RequestParam(value = "shareToken", required = false) String shareToken) {
return orderService.detail(groupOrderId, shareToken);
}
@PostMapping("/cancel")
@ApiOperation("团长取消拼团")
public ResponseDto<Boolean> cancel(@RequestParam("groupOrderId") @NotNull(message = "拼团订单ID不能为空") String groupOrderId) {
return orderService.cancel(groupOrderId);
}
@PostMapping("/refreshShare")
@ApiOperation("刷新邀请链接")
public ResponseDto<KylinGroupBuyRefreshShareVo> refreshShare(
@RequestParam("groupOrderId") @NotNull(message = "拼团订单ID不能为空") String groupOrderId) {
return orderService.refreshShare(groupOrderId);
}
@GetMapping("/price")
@ApiOperation("查询拼团价")
public ResponseDto<KylinGroupBuyPriceVo> price(
@RequestParam("activityId") @NotNull(message = "活动ID不能为空") String activityId,
@RequestParam("timeId") @NotNull(message = "场次ID不能为空") String timeId,
@RequestParam("ticketId") @NotNull(message = "票种ID不能为空") String ticketId) {
return activityService.price(activityId, timeId, ticketId);
}
}
package com.liquidnet.service.kylin.controller;
import com.liquidnet.service.base.ResponseDto;
import com.liquidnet.service.kylin.dto.vo.OrderRefundReasonVo;
import com.liquidnet.service.kylin.service.impl.KylinOrderRefundReasonServiceImpl;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
/**
* 退款原因(App/H5)
*/
@Api(tags = "前端-退款原因")
@RestController
@RequestMapping("orderRefundReason")
@Validated
public class KylinOrderRefundReasonController {
@Autowired
private KylinOrderRefundReasonServiceImpl refundReasonService;
@GetMapping("appliedList")
@ApiOperation("已应用退款理由列表")
@ApiResponse(code = 200, message = "接口返回对象参数")
public ResponseDto<List<OrderRefundReasonVo>> appliedList() {
return ResponseDto.success(refundReasonService.listApplied());
}
}
......@@ -63,7 +63,9 @@ public class KylinOrderTicketsRefundController {
@ApiImplicitParams({
@ApiImplicitParam(type = "form", dataType = "String", name = "orderId", value = "订单ID", required = true),
@ApiImplicitParam(type = "form", dataType = "String", name = "orderTicketEntitiesId", value = "订单入场人ID", required = true),
@ApiImplicitParam(type = "form", dataType = "String", name = "reason", value = "退款申请备注", required = false),
@ApiImplicitParam(type = "form", dataType = "String", name = "reasonId", value = "退款原因配置ID", required = true),
@ApiImplicitParam(type = "form", dataType = "String", name = "reasonDetail", value = "退款原因补充说明"),
@ApiImplicitParam(type = "form", dataType = "String", name = "reason", value = "退款申请备注(兼容旧版,优先使用reasonId)", required = false),
@ApiImplicitParam(type = "form", dataType = "String", name = "picList", value = "图片"),
@ApiImplicitParam(type = "form", dataType = "String", name = "ticketNum", value = "纸质票号"),
@ApiImplicitParam(type = "form", dataType = "Integer", name = "sendExpressType", value = "快递方式 1上门取件 2自主发货"),
......@@ -71,12 +73,16 @@ public class KylinOrderTicketsRefundController {
@ApiImplicitParam(type = "form", dataType = "String", name = "expressContacts", value = "发货人姓名"),
@ApiImplicitParam(type = "form", dataType = "String", name = "expressPhone", value = "发货人联系方式"),
@ApiImplicitParam(type = "form", dataType = "String", name = "sendExpressAddress", value = "取件地址"),
@ApiImplicitParam(type = "form", dataType = "String", name = "appointmentTime", value = "预约取件时间")
@ApiImplicitParam(type = "form", dataType = "String", name = "appointmentTime", value = "预约取件时间"),
@ApiImplicitParam(type = "form", dataType = "Boolean", name = "skipCharges", value = "系统退款免手续费"),
@ApiImplicitParam(type = "form", dataType = "Boolean", name = "autoApproved", value = "系统退款自动审批")
})
public ResponseDto sendOrderRefund(
@RequestParam(value = "orderId", required = true) @NotNull(message = "订单ID不能为空") String orderId,
@RequestParam(value = "orderTicketEntitiesId", required = true) @NotNull(message = "票单ID不能为空") String orderTicketEntitiesId,
@RequestParam(value = "reason", required = false) @NotNull(message = "退款原因不能为空") String reason,
@RequestParam(value = "reasonId", required = false) String reasonId,
@RequestParam(value = "reasonDetail", required = false) String reasonDetail,
@RequestParam(value = "reason", required = false) String reason,
@RequestParam(value = "picList", required = false) String picList,
@RequestParam(value = "ticketNum", required = false) String ticketNum,
......@@ -86,11 +92,16 @@ public class KylinOrderTicketsRefundController {
@RequestParam(value = "expressContacts", required = false) String expressContacts,
@RequestParam(value = "expressPhone", required = false) String expressPhone,
@RequestParam(value = "sendExpressAddress", required = false) String sendExpressAddress,
@RequestParam(value = "appointmentTime", required = false) String appointmentTime
@RequestParam(value = "appointmentTime", required = false) String appointmentTime,
@RequestParam(value = "skipCharges", required = false) Boolean skipCharges,
@RequestParam(value = "autoApproved", required = false) Boolean autoApproved
) {
// 退款
boolean systemRefund = Boolean.TRUE.equals(skipCharges) && Boolean.TRUE.equals(autoApproved);
if (systemRefund) {
return kylinOrderTicketsRefundService.systemFullRefund(orderId, orderTicketEntitiesId, reason);
}
return kylinOrderTicketsRefundService.sendOrderRefund(
orderId, orderTicketEntitiesId, reason, picList, ticketNum,
orderId, orderTicketEntitiesId, reasonId, reasonDetail, reason, picList, ticketNum,
sendExpressType, expressNumber, expressContacts, expressPhone, sendExpressAddress, appointmentTime
);
}
......
package com.liquidnet.service.kylin.service.impl;
import com.liquidnet.service.kylin.dto.vo.groupbuy.KylinGroupBuyOrderVo;
import com.liquidnet.service.kylin.service.GroupBuyNotifyService;
import com.liquidnet.service.kylin.utils.GroupBuySmsSender;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.stereotype.Service;
@Service
@Slf4j
public class GroupBuyNotifyServiceImpl implements GroupBuyNotifyService {
@Autowired
private MongoTemplate mongoTemplate;
@Autowired
private GroupBuySmsSender groupBuySmsSender;
@Override
public void notifySuccess(String groupOrderId) {
KylinGroupBuyOrderVo group = load(groupOrderId);
if (group == null) {
return;
}
groupBuySmsSender.notifySuccess(group);
}
@Override
public void notifyFailed(String groupOrderId) {
KylinGroupBuyOrderVo group = load(groupOrderId);
if (group == null) {
return;
}
groupBuySmsSender.notifyFailed(group);
}
@Override
public void notifyRemind1h(String groupOrderId) {
KylinGroupBuyOrderVo group = load(groupOrderId);
if (group == null) {
return;
}
groupBuySmsSender.notifyRemind1h(group);
}
private KylinGroupBuyOrderVo load(String groupOrderId) {
if (groupOrderId == null) {
return null;
}
return mongoTemplate.findOne(
Query.query(Criteria.where("groupOrderId").is(groupOrderId)),
KylinGroupBuyOrderVo.class, KylinGroupBuyOrderVo.class.getSimpleName());
}
}
package com.liquidnet.service.kylin.service.impl;
import com.liquidnet.common.cache.redis.util.RedisUtil;
import com.liquidnet.service.base.ErrorMapping;
import com.liquidnet.service.base.ResponseDto;
import com.liquidnet.service.base.constant.RedisKeyExpireConst;
import com.liquidnet.service.kylin.constant.GroupBuyStatusConst;
import com.liquidnet.service.kylin.constant.KylinRedisConst;
import com.liquidnet.service.kylin.dto.vo.groupbuy.KylinGroupBuyActivityVo;
import com.liquidnet.service.kylin.dto.vo.groupbuy.KylinGroupBuyDiscountVo;
import com.liquidnet.service.kylin.dto.vo.groupbuy.KylinGroupBuyPriceVo;
import com.liquidnet.service.kylin.dto.vo.middle.KylinTicketTimesVo;
import com.liquidnet.service.kylin.dto.vo.middle.KylinTicketVo;
import com.liquidnet.service.kylin.dto.vo.mongo.KylinPerformanceVo;
import com.liquidnet.service.kylin.service.IKylinGroupBuyActivityService;
import com.liquidnet.service.kylin.utils.DataUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.util.List;
@Service
@Slf4j
public class KylinGroupBuyActivityServiceImpl implements IKylinGroupBuyActivityService {
@Autowired
private MongoTemplate mongoTemplate;
@Autowired
private DataUtils dataUtils;
@Autowired
private RedisUtil redisUtil;
@Override
public ResponseDto<List<KylinGroupBuyActivityVo>> activities(String performanceId) {
Query query = Query.query(Criteria.where("performanceId").is(performanceId)
.and("status").is(GroupBuyStatusConst.ActivityStatus.ACTIVE));
List<KylinGroupBuyActivityVo> list = mongoTemplate.find(query,
KylinGroupBuyActivityVo.class, KylinGroupBuyActivityVo.class.getSimpleName());
return ResponseDto.success(list);
}
@Override
public ResponseDto<KylinGroupBuyPriceVo> price(String activityId, String timeId, String ticketId) {
KylinGroupBuyActivityVo activity = getActiveActivity(activityId);
if (activity == null) {
return ResponseDto.failure("拼团活动不存在或未进行中");
}
KylinGroupBuyDiscountVo discount = findDiscount(activity, timeId, ticketId);
if (discount == null) {
return ResponseDto.failure("该票档不支持拼团");
}
BigDecimal originalPrice = getOriginalPrice(activity.getPerformanceId(), timeId, ticketId);
if (originalPrice == null) {
return ResponseDto.failure(ErrorMapping.get("20004"));
}
KylinGroupBuyPriceVo vo = new KylinGroupBuyPriceVo();
vo.setActivityId(activityId);
vo.setTimeId(timeId);
vo.setTicketId(ticketId);
vo.setOriginalPrice(originalPrice);
vo.setDiscountType(discount.getDiscountType());
vo.setDiscountValue(discount.getDiscountValue());
vo.setGroupBuyPrice(discount.getDiscountValue());
return ResponseDto.success(vo);
}
public KylinGroupBuyActivityVo getActiveActivity(String activityId) {
if (activityId == null) {
return null;
}
String redisKey = KylinRedisConst.GROUP_BUY_ACTIVITY.concat(activityId);
Object obj = redisUtil.get(redisKey);
if (obj != null) {
KylinGroupBuyActivityVo activity = (KylinGroupBuyActivityVo) obj;
return GroupBuyStatusConst.ActivityStatus.ACTIVE.equals(activity.getStatus()) ? activity : null;
}
Query query = Query.query(Criteria.where("activityId").is(activityId)
.and("status").is(GroupBuyStatusConst.ActivityStatus.ACTIVE));
KylinGroupBuyActivityVo activity = mongoTemplate.findOne(query, KylinGroupBuyActivityVo.class,
KylinGroupBuyActivityVo.class.getSimpleName());
if (activity != null) {
redisUtil.set(redisKey, activity, RedisKeyExpireConst.KYLIN_GROUP_BUY_EXPIRE);
}
return activity;
}
public KylinGroupBuyDiscountVo findDiscount(KylinGroupBuyActivityVo activity, String timeId, String ticketId) {
if (activity == null || activity.getDiscounts() == null) {
return null;
}
for (KylinGroupBuyDiscountVo discount : activity.getDiscounts()) {
if (timeId.equals(discount.getTimeId()) && ticketId.equals(discount.getTicketId())) {
return discount;
}
}
return null;
}
public BigDecimal getOriginalPrice(String performanceId, String timeId, String ticketId) {
KylinPerformanceVo performanceVo = dataUtils.getPerformanceVo(performanceId);
if (performanceVo == null || performanceVo.getTicketTimeList() == null) {
return null;
}
for (KylinTicketTimesVo timeVo : performanceVo.getTicketTimeList()) {
if (!timeId.equals(timeVo.getTimeId()) || timeVo.getTicketList() == null) {
continue;
}
for (KylinTicketVo ticketVo : timeVo.getTicketList()) {
if (ticketId.equals(ticketVo.getTicketsId())) {
return ticketVo.getPrice();
}
}
}
return null;
}
}
package com.liquidnet.service.kylin.service.impl;
import com.github.pagehelper.PageInfo;
import com.liquidnet.common.cache.redis.util.RedisUtil;
import com.liquidnet.commons.lang.util.*;
import com.liquidnet.service.adam.constant.AdamRedisConst;
import com.liquidnet.service.adam.dto.vo.AdamUserInfoVo;
import com.liquidnet.service.base.ResponseDto;
import com.liquidnet.service.base.SqlMapping;
import com.liquidnet.service.base.constant.MQConst;
import com.liquidnet.service.base.constant.RedisKeyExpireConst;
import com.liquidnet.service.kylin.constant.GroupBuyStatusConst;
import com.liquidnet.service.kylin.constant.KylinRedisConst;
import com.liquidnet.service.kylin.dto.vo.groupbuy.*;
import com.liquidnet.service.kylin.dto.vo.middle.KylinTicketTimesVo;
import com.liquidnet.service.kylin.dto.vo.middle.KylinTicketVo;
import com.liquidnet.service.kylin.dto.vo.mongo.KylinOrderTicketEntitiesVo;
import com.liquidnet.service.kylin.dto.vo.mongo.KylinOrderTicketVo;
import com.liquidnet.service.kylin.dto.vo.mongo.KylinPerformanceVo;
import com.liquidnet.service.kylin.service.IKylinGroupBuyOrderService;
import com.liquidnet.service.kylin.utils.DataUtils;
import com.liquidnet.service.kylin.utils.QueueUtils;
import com.mongodb.BasicDBObject;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Sort;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import java.time.LocalDateTime;
import java.util.*;
@Service
@Slf4j
public class KylinGroupBuyOrderServiceImpl implements IKylinGroupBuyOrderService {
@Autowired
private MongoTemplate mongoTemplate;
@Autowired
private DataUtils dataUtils;
@Autowired
private KylinGroupBuyActivityServiceImpl activityService;
@Autowired
private KylinOrderTicketsRefundServiceImpl refundService;
@Autowired
private RedisUtil redisUtil;
@Autowired
private QueueUtils queueUtils;
@Override
public ResponseDto<List<KylinGroupBuyOrderCardVo>> pending(String activityId, String timeId, String ticketId) {
Query query = Query.query(Criteria.where("activityId").is(activityId)
.and("ticketId").is(ticketId)
.and("timeId").is(timeId)
.and("status").is(GroupBuyStatusConst.OrderStatus.PENDING)
.and("isPublic").is(GroupBuyStatusConst.PublicFlag.PUBLIC));
query.with(Sort.by(Sort.Direction.ASC, "createdAt")).limit(3);
List<KylinGroupBuyOrderVo> orders = mongoTemplate.find(query,
KylinGroupBuyOrderVo.class, KylinGroupBuyOrderVo.class.getSimpleName());
List<KylinGroupBuyOrderCardVo> result = new ArrayList<>();
for (KylinGroupBuyOrderVo o : orders) {
if (o.getCurrentCount() >= o.getTargetCount()) continue;
if (isExpired(o)) continue;
KylinGroupBuyOrderCardVo card = new KylinGroupBuyOrderCardVo();
card.setGroupOrderId(o.getGroupOrderId());
card.setActivityId(o.getActivityId());
card.setLeaderUserId(o.getLeaderUserId());
card.setTargetCount(o.getTargetCount());
card.setCurrentCount(o.getCurrentCount());
card.setShareToken(o.getShareToken());
result.add(card);
}
return ResponseDto.success(result);
}
@Override
public ResponseDto<PageInfo<KylinGroupBuyMyGroupVo>> my(Integer page, Integer size) {
String uid = CurrentUtil.getCurrentUid();
Criteria criteria = new Criteria().orOperator(
Criteria.where("leaderUserId").is(uid),
Criteria.where("members.userId").is(uid));
Query countQuery = Query.query(criteria);
long total = mongoTemplate.count(countQuery, KylinGroupBuyOrderVo.class,
KylinGroupBuyOrderVo.class.getSimpleName());
Query dataQuery = Query.query(criteria).with(Sort.by(Sort.Direction.DESC, "createdAt"));
if (page != null && size != null && page > 0 && size > 0) {
dataQuery.skip((long) (page - 1) * size).limit(size);
}
List<KylinGroupBuyOrderVo> orders = mongoTemplate.find(dataQuery,
KylinGroupBuyOrderVo.class, KylinGroupBuyOrderVo.class.getSimpleName());
List<KylinGroupBuyMyGroupVo> result = new ArrayList<>();
Map<String, String> avatarCache = new HashMap<>();
Map<String, KylinGroupBuyActivityVo> activityCache = new HashMap<>();
Map<String, KylinPerformanceVo> performanceCache = new HashMap<>();
for (KylinGroupBuyOrderVo o : orders) {
KylinGroupBuyMyGroupVo vo = new KylinGroupBuyMyGroupVo();
vo.setGroupOrderId(o.getGroupOrderId());
vo.setActivityId(o.getActivityId());
vo.setLeaderUserId(o.getLeaderUserId());
vo.setTargetCount(o.getTargetCount());
vo.setCurrentCount(o.getCurrentCount());
vo.setStatus(o.getStatus());
vo.setExpiresAt(o.getExpiresAt());
vo.setIsLeader(uid.equals(o.getLeaderUserId()) ? 1 : 0);
fillMyGroupPerformanceInfo(o, vo, activityCache, performanceCache);
if (o.getMembers() != null) {
List<GroupBuyMemberUserInfoVo> memberUserList = new ArrayList<>();
for (KylinGroupBuyMemberVo m : o.getMembers()) {
// 仅展示已加入(拼团成功入团)成员,过滤待支付/已取消/已退款
if (!GroupBuyStatusConst.MemberStatus.JOINED.equals(m.getStatus())) {
continue;
}
GroupBuyMemberUserInfoVo b = new GroupBuyMemberUserInfoVo();
b.setUserId(m.getUserId());
b.setAvatar(resolveAvatar(m.getUserId(), m.getAvatar(), avatarCache));
b.setNickname(m.getNickname());
b.setOrderTicketsId(m.getOrderTicketsId());
memberUserList.add(b);
}
vo.setMemberBriefs(memberUserList);
}
result.add(vo);
}
PageInfo<KylinGroupBuyMyGroupVo> pageInfo = new PageInfo<>(result);
pageInfo.setTotal(total);
return ResponseDto.success(pageInfo);
}
@Override
public ResponseDto<KylinGroupBuyOrderDetailVo> detail(String groupOrderId, String shareToken) {
KylinGroupBuyOrderVo order = getOrderForJoin(groupOrderId, shareToken);
if (order == null) return ResponseDto.failure("拼团不存在");
KylinGroupBuyActivityVo activity = findActivity(order.getActivityId());
if (null == activity) {
return ResponseDto.failure("拼团活动不存在");
}
KylinPerformanceVo performance = dataUtils.getPerformanceVo(order.getActivityId() != null ? activity.getPerformanceId() : null);
KylinGroupBuyOrderDetailVo vo = new KylinGroupBuyOrderDetailVo();
vo.setGroupOrderId(order.getGroupOrderId());
vo.setActivityId(order.getActivityId());
vo.setLeaderUserId(order.getLeaderUserId());
vo.setTargetCount(order.getTargetCount());
vo.setCurrentCount(order.getCurrentCount());
vo.setStatus(order.getStatus());
vo.setIsPublic(order.getIsPublic());
vo.setShareToken(order.getShareToken());
vo.setExpiresAt(order.getExpiresAt());
vo.setCreatedAt(order.getCreatedAt());
vo.setTimeId(order.getTimeId());
vo.setTicketId(order.getTicketId());
String currentUid = CurrentUtil.getCurrentUid();
vo.setIsLeader(StringUtil.isNotBlank(currentUid) && currentUid.equals(order.getLeaderUserId()) ? 1 : 0);
if (performance != null) {
vo.setPerformanceName(performance.getTitle());
fillTicketInfo(performance, order.getTimeId(), order.getTicketId(), vo);
}
if (activity != null) {
vo.setPerformanceId(activity.getPerformanceId());
KylinGroupBuyDiscountVo discount = activityService.findDiscount(activity, order.getTimeId(), order.getTicketId());
vo.setGroupBuyPrice(discount != null ? discount.getDiscountValue() : null);
}
if (order.getMembers() != null) {
Map<String, String> avatarCache = new HashMap<>();
List<GroupBuyMemberUserInfoVo> members = new ArrayList<>();
for (KylinGroupBuyMemberVo m : order.getMembers()) {
if (!GroupBuyStatusConst.MemberStatus.JOINED.equals(m.getStatus())) {
continue;
}
GroupBuyMemberUserInfoVo mu = new GroupBuyMemberUserInfoVo();
mu.setUserId(m.getUserId());
mu.setNickname(m.getNickname());
mu.setAvatar(resolveAvatar(m.getUserId(), m.getAvatar(), avatarCache));
mu.setOrderTicketsId(m.getOrderTicketsId());
members.add(mu);
}
vo.setMembers(members);
}
return ResponseDto.success(vo);
}
@Override
public ResponseDto<Boolean> cancel(String groupOrderId) {
String uid = CurrentUtil.getCurrentUid();
KylinGroupBuyOrderVo order = findOrder(groupOrderId);
if (order == null) return ResponseDto.failure("拼团不存在");
if (!uid.equals(order.getLeaderUserId())) return ResponseDto.failure("仅团长可取消拼团");
if (GroupBuyStatusConst.OrderStatus.SUCCESS.equals(order.getStatus()))
return ResponseDto.failure("拼团已成,不可取消");
if (!GroupBuyStatusConst.OrderStatus.PENDING.equals(order.getStatus()))
return ResponseDto.failure("当前状态不可取消");
String lockKey = KylinRedisConst.GROUP_BUY_ORDER_LOCK.concat(groupOrderId);
if (!redisUtil.lock(lockKey, 1, 15)) {
return ResponseDto.failure("拼团处理中,请稍后重试");
}
List<KylinGroupBuyMemberVo> joinedToRefund = new ArrayList<>();
try {
// 持锁后回源,仅 PENDING → CANCELLED
order = mongoTemplate.findOne(
Query.query(Criteria.where("groupOrderId").is(groupOrderId)),
KylinGroupBuyOrderVo.class, KylinGroupBuyOrderVo.class.getSimpleName());
if (order == null) return ResponseDto.failure("拼团不存在");
if (!GroupBuyStatusConst.OrderStatus.PENDING.equals(order.getStatus())) {
return ResponseDto.failure("当前状态不可取消");
}
order.setStatus(GroupBuyStatusConst.OrderStatus.CANCELLED);
order.setUpdatedAt(now());
if (!CollectionUtils.isEmpty(order.getMembers())) {
for (KylinGroupBuyMemberVo member : order.getMembers()) {
if (GroupBuyStatusConst.MemberStatus.JOINED.equals(member.getStatus())) {
joinedToRefund.add(member);
member.setStatus(GroupBuyStatusConst.MemberStatus.REFUNDED);
} else if (GroupBuyStatusConst.MemberStatus.PENDING_PAY.equals(member.getStatus())) {
member.setStatus(GroupBuyStatusConst.MemberStatus.CANCELLED);
}
}
}
order.setCurrentCount(0);
saveOrder(order);
} finally {
redisUtil.uLock(lockKey);
}
for (KylinGroupBuyMemberVo member : joinedToRefund) {
refundJoinedMember(member.getOrderTicketsId(), "团长取消拼团,全额退款");
}
return ResponseDto.success(true);
}
@Override
public ResponseDto<KylinGroupBuyRefreshShareVo> refreshShare(String groupOrderId) {
String uid = CurrentUtil.getCurrentUid();
KylinGroupBuyOrderVo order = findOrder(groupOrderId);
if (order == null) return ResponseDto.failure("拼团不存在");
if (!uid.equals(order.getLeaderUserId())) return ResponseDto.failure("仅团长可刷新邀请链接");
if (!GroupBuyStatusConst.OrderStatus.PENDING.equals(order.getStatus())
&& !GroupBuyStatusConst.OrderStatus.PREPARE.equals(order.getStatus())) {
return ResponseDto.failure("当前状态不可刷新邀请链接");
}
redisUtil.del(KylinRedisConst.GROUP_BUY_ORDER_SHARE.concat(order.getShareToken()));
order.setShareToken(IDGenerator.nextSnowId());
order.setUpdatedAt(now());
saveOrder(order);
KylinGroupBuyRefreshShareVo vo = new KylinGroupBuyRefreshShareVo();
vo.setGroupOrderId(groupOrderId);
vo.setShareToken(order.getShareToken());
return ResponseDto.success(vo);
}
// ==================== private helpers ====================
private KylinGroupBuyOrderVo getOrderForJoin(String groupOrderId, String shareToken) {
if (shareToken != null) {
Object v = redisUtil.get(KylinRedisConst.GROUP_BUY_ORDER_SHARE.concat(shareToken));
if (v != null) return (KylinGroupBuyOrderVo) v;
} else if (groupOrderId != null) {
Object v = redisUtil.get(KylinRedisConst.GROUP_BUY_ORDER.concat(groupOrderId));
if (v != null) return (KylinGroupBuyOrderVo) v;
}
Criteria c = shareToken != null ? Criteria.where("shareToken").is(shareToken)
: Criteria.where("groupOrderId").is(groupOrderId);
KylinGroupBuyOrderVo order = mongoTemplate.findOne(Query.query(c),
KylinGroupBuyOrderVo.class, KylinGroupBuyOrderVo.class.getSimpleName());
cacheOrder(order);
return order;
}
private KylinGroupBuyOrderVo findOrder(String groupOrderId) {
if (groupOrderId == null) return null;
Object obj = redisUtil.get(KylinRedisConst.GROUP_BUY_ORDER.concat(groupOrderId));
if (obj != null) return (KylinGroupBuyOrderVo) obj;
KylinGroupBuyOrderVo order = mongoTemplate.findOne(
Query.query(Criteria.where("groupOrderId").is(groupOrderId)),
KylinGroupBuyOrderVo.class, KylinGroupBuyOrderVo.class.getSimpleName());
cacheOrder(order);
return order;
}
private KylinGroupBuyActivityVo findActivity(String activityId) {
Object obj = redisUtil.get(KylinRedisConst.GROUP_BUY_ACTIVITY.concat(activityId));
if (obj != null) return (KylinGroupBuyActivityVo) obj;
KylinGroupBuyActivityVo activity = mongoTemplate.findOne(
Query.query(Criteria.where("activityId").is(activityId)),
KylinGroupBuyActivityVo.class, KylinGroupBuyActivityVo.class.getSimpleName());
if (activity != null) redisUtil.set(KylinRedisConst.GROUP_BUY_ACTIVITY.concat(activityId),
activity, RedisKeyExpireConst.KYLIN_GROUP_BUY_EXPIRE);
return activity;
}
private void saveOrder(KylinGroupBuyOrderVo order) {
clearOrderCache(order);
mongoTemplate.getCollection(KylinGroupBuyOrderVo.class.getSimpleName()).updateOne(
Query.query(Criteria.where("groupOrderId").is(order.getGroupOrderId())).getQueryObject(),
new BasicDBObject().append("$set", mongoTemplate.getConverter().convertToMongoType(order)));
sendReplaceOrderSql(order, MQConst.KylinQueue.SQL_ORDER_PAY.getKey());
cacheOrder(order);
}
private void sendReplaceOrderSql(KylinGroupBuyOrderVo order, String queueKey) {
if (order.getOrderTicketsId() == null) return;
LinkedList<String> sqls = CollectionUtil.linkedListString();
LinkedList<Object[]> od = CollectionUtil.linkedListObjectArr();
LinkedList<Object[]> oi = CollectionUtil.linkedListObjectArr();
LinkedList<Object[]> md = CollectionUtil.linkedListObjectArr();
LinkedList<Object[]> mi = CollectionUtil.linkedListObjectArr();
sqls.add(SqlMapping.get("kylin_group_buy_order.delete"));
od.add(new Object[]{order.getGroupOrderId()});
sqls.add(SqlMapping.get("kylin_group_buy_order.insert"));
oi.add(orderInsertObject(order));
sqls.add(SqlMapping.get("kylin_group_buy_member.delete_by_group"));
md.add(new Object[]{order.getGroupOrderId()});
if (!CollectionUtils.isEmpty(order.getMembers())) {
sqls.add(SqlMapping.get("kylin_group_buy_member.insert"));
for (KylinGroupBuyMemberVo m : order.getMembers()) mi.add(memberInsertObject(m));
queueUtils.sendMsgByRedis(queueKey, SqlMapping.gets(sqls, od, oi, md, mi));
} else {
queueUtils.sendMsgByRedis(queueKey, SqlMapping.gets(sqls, od, oi, md));
}
}
private Object[] orderInsertObject(KylinGroupBuyOrderVo vo) {
return new Object[]{vo.getGroupOrderId(), vo.getActivityId(), vo.getLeaderUserId(),
vo.getOrderTicketsId(), vo.getTargetCount(), vo.getCurrentCount(), vo.getStatus(),
vo.getIsPublic(), vo.getShareToken(),
vo.getExpiresAt() == null ? null : DateUtil.Formatter.yyyyMMddHHmmss.parse(vo.getExpiresAt()),
DateUtil.Formatter.yyyyMMddHHmmss.parse(vo.getCreatedAt()),
DateUtil.Formatter.yyyyMMddHHmmss.parse(vo.getUpdatedAt())};
}
private Object[] memberInsertObject(KylinGroupBuyMemberVo vo) {
return new Object[]{vo.getMemberId(), vo.getGroupOrderId(), vo.getUserId(),
vo.getOrderTicketsId(), vo.getTicketCount(), vo.getStatus(),
DateUtil.Formatter.yyyyMMddHHmmss.parse(vo.getJoinTime()),
DateUtil.Formatter.yyyyMMddHHmmss.parse(vo.getCreatedAt())};
}
private void cacheOrder(KylinGroupBuyOrderVo order) {
if (order == null) return;
redisUtil.set(KylinRedisConst.GROUP_BUY_ORDER.concat(order.getGroupOrderId()), order,
RedisKeyExpireConst.KYLIN_GROUP_BUY_EXPIRE);
redisUtil.set(KylinRedisConst.GROUP_BUY_ORDER_SHARE.concat(order.getShareToken()), order,
RedisKeyExpireConst.KYLIN_GROUP_BUY_EXPIRE);
}
private void clearOrderCache(KylinGroupBuyOrderVo order) {
if (order == null) return;
redisUtil.del(KylinRedisConst.GROUP_BUY_ORDER.concat(order.getGroupOrderId()));
redisUtil.del(KylinRedisConst.GROUP_BUY_ORDER_SHARE.concat(order.getShareToken()));
}
private boolean isExpired(KylinGroupBuyOrderVo order) {
return order.getExpiresAt() != null
&& DateUtil.Formatter.yyyyMMddHHmmss.parse(order.getExpiresAt()).isBefore(LocalDateTime.now());
}
private long restSeconds(KylinGroupBuyOrderVo order) {
if (order.getExpiresAt() == null) return 0L;
return Math.max(0L, java.time.Duration.between(LocalDateTime.now(),
DateUtil.Formatter.yyyyMMddHHmmss.parse(order.getExpiresAt())).getSeconds());
}
private void refundJoinedMember(String orderTicketsId, String reason) {
KylinOrderTicketVo ticketVo = dataUtils.getOrderTicketVo(orderTicketsId);
if (ticketVo == null || CollectionUtils.isEmpty(ticketVo.getEntitiesVoList())) {
return;
}
for (KylinOrderTicketEntitiesVo entity : ticketVo.getEntitiesVoList()) {
try {
refundService.systemFullRefund(ticketVo.getOrderTicketsId(),
entity.getOrderTicketEntitiesId(), reason);
} catch (Exception e) {
log.error("拼团散团退款失败 orderTicketsId={}, entityId={}",
orderTicketsId, entity.getOrderTicketEntitiesId(), e);
}
}
}
private String now() {
return DateUtil.Formatter.yyyyMMddHHmmss.format(LocalDateTime.now());
}
/**
* 快照头像为空时,从 adam 用户缓存回填(不回写 Mongo)。
*/
private String resolveAvatar(String userId, String avatar, Map<String, String> cache) {
if (StringUtil.isNotBlank(avatar)) {
return avatar;
}
if (StringUtil.isBlank(userId)) {
return "";
}
if (cache != null && cache.containsKey(userId)) {
return cache.get(userId);
}
String filled = "";
try {
Object obj = redisUtil.get(AdamRedisConst.INFO_USER.concat(userId));
if (obj instanceof AdamUserInfoVo) {
String fromCache = ((AdamUserInfoVo) obj).getAvatar();
filled = fromCache == null ? "" : fromCache;
}
} catch (Exception e) {
log.warn("拼团回填头像失败 userId={}", userId, e);
}
if (cache != null) {
cache.put(userId, filled);
}
return filled;
}
private void fillTicketInfo(KylinPerformanceVo p, String timeId, String ticketId,
KylinGroupBuyOrderDetailVo vo) {
if (p.getTicketTimeList() == null) return;
for (KylinTicketTimesVo t : p.getTicketTimeList()) {
if (!timeId.equals(t.getTimeId())) continue;
vo.setTimeTitle(t.getTitle());
if (t.getTicketList() == null) return;
for (KylinTicketVo tk : t.getTicketList()) {
if (ticketId.equals(tk.getTicketsId())) {
vo.setTicketName(tk.getTitle());
vo.setOriginalPrice(tk.getPrice());
return;
}
}
return;
}
}
/**
* 我的拼团列表:活动 → 演出 → 场次/票种名称、封面。
*/
private void fillMyGroupPerformanceInfo(KylinGroupBuyOrderVo order, KylinGroupBuyMyGroupVo vo,
Map<String, KylinGroupBuyActivityVo> activityCache,
Map<String, KylinPerformanceVo> performanceCache) {
if (order == null || vo == null || StringUtil.isBlank(order.getActivityId())) {
return;
}
KylinGroupBuyActivityVo activity = activityCache.get(order.getActivityId());
if (activity == null) {
activity = findActivity(order.getActivityId());
if (activity != null) {
activityCache.put(order.getActivityId(), activity);
}
}
if (activity == null || StringUtil.isBlank(activity.getPerformanceId())) {
return;
}
KylinPerformanceVo performance = performanceCache.get(activity.getPerformanceId());
if (performance == null) {
performance = dataUtils.getPerformanceVo(activity.getPerformanceId());
if (performance != null) {
performanceCache.put(activity.getPerformanceId(), performance);
}
}
if (performance == null) {
return;
}
vo.setPerformanceTitle(performance.getTitle());
vo.setPerformanceImg(performance.getImgPoster());
if (StringUtil.isBlank(order.getTimeId()) || performance.getTicketTimeList() == null) {
return;
}
for (KylinTicketTimesVo t : performance.getTicketTimeList()) {
if (!order.getTimeId().equals(t.getTimeId())) {
continue;
}
vo.setTimeTitle(t.getTitle());
if (StringUtil.isBlank(order.getTicketId()) || t.getTicketList() == null) {
return;
}
for (KylinTicketVo tk : t.getTicketList()) {
if (order.getTicketId().equals(tk.getTicketsId())) {
vo.setTicketTitle(tk.getTitle());
return;
}
}
return;
}
}
}
package com.liquidnet.service.kylin.service.impl;
import com.liquidnet.common.cache.redis.util.RedisUtil;
import com.liquidnet.service.kylin.constant.KylinRedisConst;
import com.liquidnet.service.kylin.dto.vo.OrderRefundReasonVo;
import com.liquidnet.service.kylin.entity.KylinOrderRefundReasons;
import com.liquidnet.service.kylin.mapper.KylinOrderRefundReasonsMapper;
import com.liquidnet.service.kylin.service.IKylinOrderRefundReasonService;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
/**
* 退款原因(App/H5)
*/
@Slf4j
@Service
public class KylinOrderRefundReasonServiceImpl implements IKylinOrderRefundReasonService {
@Autowired
private RedisUtil redisUtil;
@Autowired
private KylinOrderRefundReasonsMapper reasonsMapper;
@Override
@SuppressWarnings("unchecked")
public List<OrderRefundReasonVo> listApplied() {
Object cached = redisUtil.get(KylinRedisConst.KYLIN_ORDER_REFUND_REASONS_APPLIED);
if (cached != null) {
return (List<OrderRefundReasonVo>) cached;
}
List<KylinOrderRefundReasons> list = reasonsMapper.listApplied();
List<OrderRefundReasonVo> voList = new ArrayList<>();
if (list != null) {
for (KylinOrderRefundReasons item : list) {
OrderRefundReasonVo vo = new OrderRefundReasonVo();
BeanUtils.copyProperties(item, vo);
voList.add(vo);
}
}
redisUtil.set(KylinRedisConst.KYLIN_ORDER_REFUND_REASONS_APPLIED, voList);
return voList;
}
@Override
public OrderRefundReasonVo getAppliedByReasonId(String reasonId) {
if (StringUtils.isBlank(reasonId)) {
return null;
}
List<OrderRefundReasonVo> list = listApplied();
if (list == null) {
return null;
}
for (OrderRefundReasonVo item : list) {
if (reasonId.equals(item.getReasonId())) {
return item;
}
}
return null;
}
}
......@@ -64,6 +64,8 @@ public class KylinOrderTicketsRefundServiceImpl {
private QueueUtils queueUtils;
@Autowired
private KylinRefundsStatusServiceImpl refundsStatusService;
@Autowired
private KylinOrderRefundReasonServiceImpl refundReasonService;
@Value("${liquidnet.express.shunfeng.custid}")
private String custid;
......@@ -170,7 +172,22 @@ public class KylinOrderTicketsRefundServiceImpl {
}
}
public ResponseDto sendOrderRefund(String orderId, String orderTicketEntitiesId, String reason, String picList, String ticketNum, Integer sendExpressType, String expressNumber, String expressContacts, String expressPhone, String sendExpressAddress, String appointmentTime) {
public ResponseDto sendOrderRefund(String orderId, String orderTicketEntitiesId, String reasonId, String reasonDetail, String reason, String picList, String ticketNum, Integer sendExpressType, String expressNumber, String expressContacts, String expressPhone, String sendExpressAddress, String appointmentTime) {
return sendOrderRefund(orderId, orderTicketEntitiesId, reasonId, reasonDetail, reason, picList, ticketNum,
sendExpressType, expressNumber, expressContacts, expressPhone, sendExpressAddress, appointmentTime,
false, false);
}
/**
* 系统全额退(拼团散团/超时):跳过票主登录态与退票窗口,免手续费并触发自动退款。
* 原因按自由文本兼容写入(无 reasonId)。
*/
public ResponseDto systemFullRefund(String orderId, String orderTicketEntitiesId, String reason) {
return sendOrderRefund(orderId, orderTicketEntitiesId, null, null, reason, "", "",
0, "", "", "", "", "", true, true);
}
public ResponseDto sendOrderRefund(String orderId, String orderTicketEntitiesId, String reasonId, String reasonDetail, String reason, String picList, String ticketNum, Integer sendExpressType, String expressNumber, String expressContacts, String expressPhone, String sendExpressAddress, String appointmentTime, boolean skipCharges, boolean autoApproved) {
boolean isLock = dataUtils.setRefundLock(orderTicketEntitiesId);
if (!isLock) {
return ResponseDto.failure("正在处理,请勿重复操作~");
......@@ -178,11 +195,39 @@ public class KylinOrderTicketsRefundServiceImpl {
KylinOrderTicketEntitiesVo orderTicketEntitiesVo = null;
try {
String uid = CurrentUtil.getCurrentUid();
String resolvedReason;
String resolvedReasonId = null;
String resolvedReasonDetail = null;
if (StringUtils.isNotBlank(reasonId)) {
OrderRefundReasonUtils.ResolvedRefundReason resolved = OrderRefundReasonUtils.resolve(
refundReasonService.getAppliedByReasonId(reasonId), reasonDetail);
if (!resolved.isSuccess()) {
return ResponseDto.failure(resolved.getErrorMsg());
}
resolvedReasonId = resolved.getReasonId();
resolvedReasonDetail = resolved.getReasonDetail();
resolvedReason = resolved.getReasonSnapshot();
} else if (StringUtils.isNotBlank(reason)) {
// 兼容旧版自由文本 / 系统退款文案
resolvedReason = OrderRefundReasonUtils.truncate(reason, 200);
} else {
return ResponseDto.failure("请选择退款原因");
}
String uid = null;
try {
uid = CurrentUtil.getCurrentUid();
} catch (Exception ignore) {
// 系统退款可能无登录态
}
KylinOrderTicketVo orderTicketVo = dataUtils.getOrderTicketVo(orderId);
List<KylinOrderRefundsVo> kylinOrderRefundsVoBaseList = dataUtils.getOrderRefundVoByOrderId(orderTicketVo.getOrderTicketsId());
if (!SuperAccountUtils.isSuperAccount(uid, orderTicketVo.getUserId())) {
return ResponseDto.failure(ErrorMapping.get("20003"));
if (!autoApproved) {
if (uid == null || !SuperAccountUtils.isSuperAccount(uid, orderTicketVo.getUserId())) {
return ResponseDto.failure(ErrorMapping.get("20003"));
}
} else if (uid == null) {
uid = "system";
}
// if (uid.equals("809406") || uid.equals("773650")) {
// } else {
......@@ -209,18 +254,20 @@ public class KylinOrderTicketsRefundServiceImpl {
}
}
if (null != performanceVo.getIsCanRefund() && performanceVo.getIsCanRefund() == 1) {
LocalDateTime refundOpenDate = DateUtil.Formatter.yyyyMMddHHmmss.parse(performanceVo.getRefundOpenTime());
LocalDateTime refundCloseDate = DateUtil.Formatter.yyyyMMddHHmmss.parse(performanceVo.getRefundCloseTime());
if (LocalDateTime.now().isAfter(refundOpenDate) && LocalDateTime.now().isBefore(refundCloseDate)
//&& ticketVo.getCounts() == 1
) {
if (!autoApproved) {
if (null != performanceVo.getIsCanRefund() && performanceVo.getIsCanRefund() == 1) {
LocalDateTime refundOpenDate = DateUtil.Formatter.yyyyMMddHHmmss.parse(performanceVo.getRefundOpenTime());
LocalDateTime refundCloseDate = DateUtil.Formatter.yyyyMMddHHmmss.parse(performanceVo.getRefundCloseTime());
if (LocalDateTime.now().isAfter(refundOpenDate) && LocalDateTime.now().isBefore(refundCloseDate)
//&& ticketVo.getCounts() == 1
) {
} else {
return ResponseDto.failure("暂不支持退款");
}
} else {
return ResponseDto.failure("暂不支持退款");
}
} else {
return ResponseDto.failure("暂不支持退款");
}
List<KylinOrderTicketEntitiesVo> entitiesVos = orderTicketVo.getEntitiesVoList();
......@@ -240,9 +287,12 @@ public class KylinOrderTicketsRefundServiceImpl {
BigDecimal singlePrice = orderTicketVo.getPriceActual().subtract(orderTicketVo.getPriceExpress()).divide(BigDecimal.valueOf(orderTicketVo.getNumber()), 2, BigDecimal.ROUND_HALF_UP);
// 手续费处理 TODO 修改了手续费根据票单算
BigDecimal chargesRatio = getChargesRatio(orderTicketVo.getUseStart(), performanceVo.getIsRefundPoundage(dataUtils.getRefundPoundage(1, performanceVo.getPerformancesId()).size()), performanceVo.getPerformancesId());
BigDecimal priceCharges = singlePrice.multiply(chargesRatio).setScale(2, BigDecimal.ROUND_HALF_UP);
refundSinglePrice = refundSinglePrice.subtract(priceCharges);
BigDecimal priceCharges = BigDecimal.ZERO;
if (!skipCharges) {
BigDecimal chargesRatio = getChargesRatio(orderTicketVo.getUseStart(), performanceVo.getIsRefundPoundage(dataUtils.getRefundPoundage(1, performanceVo.getPerformancesId()).size()), performanceVo.getPerformancesId());
priceCharges = singlePrice.multiply(chargesRatio).setScale(2, BigDecimal.ROUND_HALF_UP);
refundSinglePrice = refundSinglePrice.subtract(priceCharges);
}
if (refundSinglePrice.compareTo(BigDecimal.ZERO) <= 0) {
return ResponseDto.failure("申请金额不得小于0哦~");
}
......@@ -265,16 +315,20 @@ public class KylinOrderTicketsRefundServiceImpl {
}
}
Map token = CurrentUtil.getTokenClaims();
String username = StringUtils.defaultString(((String) token.get("nickname")), "");
String orderRefundId = refundsStatusService.userOrderTicketRefunding(orderTicketVo, refundSinglePrice, priceExpress, priceCharges, orderTicketEntitiesId, reason, picList, ticketNum, uid, username, kylinOrderRefundsVoBaseList.size(), sendExpressType);
String username = "system";
if (!autoApproved) {
Map token = CurrentUtil.getTokenClaims();
username = StringUtils.defaultString(((String) token.get("nickname")), "");
}
Integer expressTypeParam = sendExpressType == null ? 0 : sendExpressType;
String orderRefundId = refundsStatusService.userOrderTicketRefunding(orderTicketVo, refundSinglePrice, priceExpress, priceCharges, orderTicketEntitiesId, resolvedReason, resolvedReasonId, resolvedReasonDetail, picList, ticketNum, uid, username, kylinOrderRefundsVoBaseList.size(), expressTypeParam);
if (!orderRefundId.isEmpty()) {
// 退款申请成功 异步去快递下单
if (orderTicketVo.getGetTicketType().equals("express") && sendExpressType > 0) {// 快递票
expressPlace(sendExpressType, expressNumber, expressContacts, expressPhone, sendExpressAddress, appointmentTime, orderTicketVo, performanceVo, orderRefundId);
if (orderTicketVo.getGetTicketType().equals("express") && expressTypeParam > 0) {// 快递票
expressPlace(expressTypeParam, expressNumber, expressContacts, expressPhone, sendExpressAddress, appointmentTime, orderTicketVo, performanceVo, orderRefundId);
}
//电子票自动退款
if (orderTicketVo.getGetTicketType().equals("electronic")){
//电子票自动退款;系统退款一律触发自动审批
if (autoApproved || orderTicketVo.getGetTicketType().equals("electronic")){
try {
MultiValueMap<String, String> params = new LinkedMultiValueMap();
params.add("orderRefundsId", orderRefundId);
......
......@@ -56,6 +56,8 @@ public class KylinRefundsStatusServiceImpl {
BigDecimal priceCharges,
String orderEntitiesId,
String reason,
String reasonId,
String reasonDetail,
String picList,
String ticketNum,
String uid,
......@@ -120,6 +122,8 @@ public class KylinRefundsStatusServiceImpl {
kylinOrderRefunds.setApplicantName(username);
kylinOrderRefunds.setApplicantAt(time);
kylinOrderRefunds.setReason(reason);
kylinOrderRefunds.setReasonId(reasonId);
kylinOrderRefunds.setReasonDetail(reasonDetail);
kylinOrderRefunds.setCreatedAt(time);
KylinOrderRefundsVo orderRefundsVo = KylinOrderRefundsVo.getNew();
......@@ -182,7 +186,7 @@ public class KylinRefundsStatusServiceImpl {
kylinOrderRefunds.getOrderRefundsId(), kylinOrderRefunds.getOrderTicketsId(), kylinOrderRefunds.getOrderRefundCode(),
kylinOrderRefunds.getPrice(), kylinOrderRefunds.getPriceExpress(), kylinOrderRefunds.getPriceCharges(), ticketNum, kylinOrderRefunds.getStatus(),
kylinOrderRefunds.getType(), kylinOrderRefunds.getApplicantId(), kylinOrderRefunds.getApplicantName(),
kylinOrderRefunds.getApplicantAt(), kylinOrderRefunds.getReason(), kylinOrderRefunds.getCreatedAt()
kylinOrderRefunds.getApplicantAt(), kylinOrderRefunds.getReason(), kylinOrderRefunds.getReasonId(), kylinOrderRefunds.getReasonDetail(), kylinOrderRefunds.getCreatedAt()
});
sqlsDataD.add(new Object[]{
kylinOrderRefundEntities.getOrderRefundsEntitiesId(), kylinOrderRefundEntities.getOrderRefundsId(), kylinOrderRefundEntities.getRefundPrice(),
......@@ -332,9 +336,9 @@ public class KylinRefundsStatusServiceImpl {
});
sqlsDataC.add(new Object[]{
kylinOrderRefunds.getOrderRefundsId(), kylinOrderRefunds.getOrderTicketsId(), kylinOrderRefunds.getOrderRefundCode(),
kylinOrderRefunds.getPrice(), kylinOrderRefunds.getPriceExpress(), 0, kylinOrderRefunds.getStatus(),
kylinOrderRefunds.getPrice(), kylinOrderRefunds.getPriceExpress(), 0, null, kylinOrderRefunds.getStatus(),
kylinOrderRefunds.getType(), kylinOrderRefunds.getApplicantId(), kylinOrderRefunds.getApplicantName(),
kylinOrderRefunds.getApplicantAt(), kylinOrderRefunds.getReason(), kylinOrderRefunds.getCreatedAt()
kylinOrderRefunds.getApplicantAt(), kylinOrderRefunds.getReason(), null, null, kylinOrderRefunds.getCreatedAt()
});
sqlsDataD.add(new Object[]{
kylinOrderRefundEntities.getOrderRefundsEntitiesId(), kylinOrderRefundEntities.getOrderRefundsId(), kylinOrderRefundEntities.getRefundPrice(),
......
package com.liquidnet.service.kylin.utils;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.liquidnet.common.cache.redis.util.RedisUtil;
import com.liquidnet.common.sms.constant.SmsEnum;
import com.liquidnet.commons.lang.util.DateUtil;
import com.liquidnet.commons.lang.util.JsonUtils;
import com.liquidnet.service.base.SmsMessage;
import com.liquidnet.service.base.constant.MQConst;
import com.liquidnet.service.base.constant.RedisKeyExpireConst;
import com.liquidnet.service.kylin.constant.GroupBuyStatusConst;
import com.liquidnet.service.kylin.constant.KylinRedisConst;
import com.liquidnet.service.kylin.dto.vo.groupbuy.KylinGroupBuyActivityVo;
import com.liquidnet.service.kylin.dto.vo.groupbuy.KylinGroupBuyMemberVo;
import com.liquidnet.service.kylin.dto.vo.groupbuy.KylinGroupBuyOrderVo;
import com.liquidnet.service.kylin.dto.vo.middle.KylinTicketTimesVo;
import com.liquidnet.service.kylin.dto.vo.middle.KylinTicketVo;
import com.liquidnet.service.kylin.dto.vo.mongo.KylinOrderTicketVo;
import com.liquidnet.service.kylin.dto.vo.mongo.KylinPerformanceVo;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* 拼团短信(kylin):入队 {@link MQConst.KylinQueue#SMS_NOTICE}(与购票短信一致,异步)。
*/
@Component
@Slf4j
public class GroupBuySmsSender {
@Autowired
private QueueUtils queueUtils;
@Autowired
private RedisUtil redisUtil;
@Autowired
private MongoTemplate mongoTemplate;
@Autowired
private DataUtils dataUtils;
public void notifySuccess(KylinGroupBuyOrderVo group) {
if (group == null || !isSmsEnabled()) {
return;
}
List<String> phones = collectPhones(group, true, false);
if (phones.isEmpty()) {
return;
}
ObjectNode params = buildSuccessParams(group);
for (String phone : phones) {
enqueueSms(phone, SmsEnum.ADTemplate.SMS_510540063.name(), params,
group.getGroupOrderId(), "success");
}
}
public void notifyFailed(KylinGroupBuyOrderVo group) {
if (group == null || !isSmsEnabled()) {
return;
}
List<String> phones = collectPhones(group, true, false);
if (phones.isEmpty()) {
return;
}
ObjectNode params = JsonUtils.OM().createObjectNode();
params.put("hours", resolveHours(group));
for (String phone : phones) {
enqueueSms(phone, SmsEnum.ADTemplate.SMS_510615071.name(), params,
group.getGroupOrderId(), "failed");
}
}
public void notifyRemind1h(KylinGroupBuyOrderVo group) {
if (group == null || !isSmsEnabled()) {
return;
}
List<String> phones = collectPhones(group, false, true);
if (phones.isEmpty()) {
return;
}
ObjectNode empty = JsonUtils.OM().createObjectNode();
for (String phone : phones) {
enqueueSms(phone, SmsEnum.ADTemplate.SMS_510285138.name(), empty,
group.getGroupOrderId(), "remind1h");
}
redisUtil.set(KylinRedisConst.GROUP_BUY_REMIND_1H.concat(group.getGroupOrderId()), "1",
RedisKeyExpireConst.KYLIN_GROUP_BUY_EXPIRE);
}
private boolean isSmsEnabled() {
Object v = redisUtil.get(KylinRedisConst.GROUP_BUY_SMS_ENABLED);
if (v == null) {
return true;
}
String s = String.valueOf(v).trim();
return !("0".equals(s) || "false".equalsIgnoreCase(s));
}
private List<String> collectPhones(KylinGroupBuyOrderVo group, boolean joinedOnly, boolean includePendingPay) {
Set<String> phones = new HashSet<>();
if (group == null || CollectionUtils.isEmpty(group.getMembers())) {
return new ArrayList<>();
}
for (KylinGroupBuyMemberVo m : group.getMembers()) {
if (m == null || StringUtils.isBlank(m.getOrderTicketsId())
|| m.getOrderTicketsId().startsWith("ROBOT_")) {
continue;
}
Integer st = m.getStatus();
boolean ok = joinedOnly
? GroupBuyStatusConst.MemberStatus.JOINED.equals(st)
: GroupBuyStatusConst.MemberStatus.JOINED.equals(st)
|| (includePendingPay && GroupBuyStatusConst.MemberStatus.PENDING_PAY.equals(st));
if (!ok) {
continue;
}
String mobile = resolveMobile(m.getOrderTicketsId());
if (StringUtils.isNotBlank(mobile)) {
phones.add(mobile.trim());
}
}
return new ArrayList<>(phones);
}
private String resolveMobile(String orderTicketsId) {
Object cached = redisUtil.get(KylinRedisConst.ORDER.concat(orderTicketsId));
if (cached instanceof KylinOrderTicketVo) {
return ((KylinOrderTicketVo) cached).getUserMobile();
}
KylinOrderTicketVo ticket = mongoTemplate.findOne(
Query.query(Criteria.where("orderTicketsId").is(orderTicketsId)),
KylinOrderTicketVo.class, KylinOrderTicketVo.class.getSimpleName());
return ticket == null ? null : ticket.getUserMobile();
}
private ObjectNode buildSuccessParams(KylinGroupBuyOrderVo group) {
ObjectNode node = JsonUtils.OM().createObjectNode();
String name = "";
String time = "";
String ticket = "";
try {
KylinGroupBuyActivityVo activity = mongoTemplate.findOne(
Query.query(Criteria.where("activityId").is(group.getActivityId())),
KylinGroupBuyActivityVo.class, KylinGroupBuyActivityVo.class.getSimpleName());
if (activity != null && StringUtils.isNotBlank(activity.getPerformanceId())) {
KylinPerformanceVo p = dataUtils.getPerformanceVo(activity.getPerformanceId());
if (p != null) {
name = p.getTitle() == null ? "" : p.getTitle();
if (p.getTicketTimeList() != null && group.getTimeId() != null) {
for (KylinTicketTimesVo t : p.getTicketTimeList()) {
if (!group.getTimeId().equals(t.getTimeId())) {
continue;
}
time = t.getTitle() == null ? "" : t.getTitle();
if (t.getTicketList() != null && group.getTicketId() != null) {
for (KylinTicketVo tk : t.getTicketList()) {
if (group.getTicketId().equals(tk.getTicketsId())) {
ticket = tk.getTitle() == null ? "" : tk.getTitle();
break;
}
}
}
break;
}
}
}
}
} catch (Exception e) {
log.warn("拼团成功短信变量组装失败 groupOrderId={}", group.getGroupOrderId(), e);
}
node.put("name", name);
node.put("time", time);
node.put("ticket", ticket);
return node;
}
private String resolveHours(KylinGroupBuyOrderVo group) {
try {
if (StringUtils.isNotBlank(group.getCreatedAt()) && StringUtils.isNotBlank(group.getExpiresAt())) {
LocalDateTime created = DateUtil.Formatter.yyyyMMddHHmmss.parse(group.getCreatedAt());
LocalDateTime expires = DateUtil.Formatter.yyyyMMddHHmmss.parse(group.getExpiresAt());
long hours = Duration.between(created, expires).toHours();
return String.valueOf(hours < 1 ? 1 : hours);
}
} catch (Exception e) {
log.warn("拼团失败短信 hours 解析失败 groupOrderId={}", group.getGroupOrderId(), e);
}
return "24";
}
private void enqueueSms(String phone, String templateCode, ObjectNode params, String groupOrderId, String scene) {
try {
SmsMessage msg = SmsMessage.builder()
.setPhone(phone)
.setSignName(SmsEnum.ADSignName.M02.getVal())
.setTemplateCode(templateCode);
if (params == null || params.size() == 0) {
msg.setEmptyTemplateParam();
} else {
Iterator<Map.Entry<String, JsonNode>> it = params.fields();
while (it.hasNext()) {
Map.Entry<String, JsonNode> e = it.next();
msg.setTemplateParam(e.getKey(), e.getValue() == null ? "" : e.getValue().asText(""));
}
}
queueUtils.sendMsgByRedis(MQConst.KylinQueue.SMS_NOTICE.getKey(), msg.toJson());
} catch (Exception e) {
log.error("拼团短信入队异常 scene={} groupOrderId={} phone={}", scene, groupOrderId, phone, e);
}
}
}
......@@ -16,7 +16,7 @@ kylin_order_ticket_entities.synPay=UPDATE kylin_order_ticket_entities SET is_pay
# ------------------------\u7528\u6237\u53D1\u8D77\u8BA2\u5355\u9000\u6B3E----------------------------
kylin_order_ticket_status.refund=UPDATE kylin_order_ticket_status SET status = ? ,updated_at = ? WHERE order_id = ? and (updated_at <= ? or created_at = ? or updated_at is null)
kylin_order_ticket_entities.refund=UPDATE kylin_order_ticket_entities SET is_payment = ? ,updated_at = ? WHERE order_ticket_entities_id = ? and (updated_at <= ? or created_at = ? or updated_at is null)
kylin_order_refund.refund=INSERT INTO kylin_order_refunds (order_refunds_id,order_tickets_id,order_refund_code,price,price_express,price_charges,ticket_num,status,type,applicant_id,applicant_name,applicant_at,reason,created_at) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?)
kylin_order_refund.refund=INSERT INTO kylin_order_refunds (order_refunds_id,order_tickets_id,order_refund_code,price,price_express,price_charges,ticket_num,status,type,applicant_id,applicant_name,applicant_at,reason,reason_id,reason_detail,created_at) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
kylin_order_refund_entities.refund=INSERT INTO kylin_order_refund_entities (order_refunds_entities_id,order_refunds_id,refund_price,order_ticket_entities_id,created_at) VALUES(?,?,?,?,?)
kylin_order_refund_pic.refund=INSERT INTO kylin_order_refund_pic (refund_pic_id,order_refunds_id,pic_url,created_at) VALUES (?,?,?,?)
kylin_order_refund.refundOrderExpress=INSERT INTO kylin_order_express (order_express_id,performances_id,order_tickets_id,order_refunds_id,order_express_code,mailno,express_type,filter_result,remark,express_status,send_type,send_express_type,express_contacts,express_phone,send_express_address,appointment_time,pro_name,cargo_type_code,limit_type_code,express_type_code,goods_value_total,goods_number,created_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
......@@ -42,4 +42,14 @@ kylin_ar_ticket.update =update kylin_ticket_active set state=?,redeem_uid= ?,red
kylin_order_ticket_entities.update_status_bystation_pad=UPDATE kylin_order_ticket_entities SET `status`=?,check_client=?,updated_at=?, comment=? WHERE order_ticket_entities_id=?
# ------------------------ \u6F14\u51FA\u9884\u7EA6 ----------------------------
kylin_performance_subscribe.insert=INSERT INTO kylin_performance_subscribe(performance_id,ticket_times_id,tickets_id,user_id,type,device_tokens,push_range,push_time,push_title,is_push)VALUES(?,?,?,?,?,?,?,?,?,?)
kylin_performance_subscribe.delete=DELETE FROM kylin_performance_subscribe WHERE performance_id= ? and ticket_times_id = ? and user_id = ?;
\ No newline at end of file
kylin_performance_subscribe.delete=DELETE FROM kylin_performance_subscribe WHERE performance_id= ? and ticket_times_id = ? and user_id = ?;
# ------------------------\u6F14\u51FA\u7968\u62FC\u56E2----------------------------
kylin_group_buy_order.delete=DELETE FROM kylin_group_buy_order WHERE group_order_id = ?
kylin_group_buy_order.insert=INSERT INTO kylin_group_buy_order (group_order_id,activity_id,leader_user_id,order_tickets_id,target_count,current_count,status,is_public,share_token,expires_at,created_at,updated_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?)
kylin_group_buy_order.update_status=UPDATE kylin_group_buy_order SET status = ?, updated_at = ? WHERE group_order_id = ? AND status = ?
kylin_group_buy_member.delete_by_group=DELETE FROM kylin_group_buy_member WHERE group_order_id = ?
kylin_group_buy_member.insert=INSERT INTO kylin_group_buy_member (member_id,group_order_id,user_id,order_tickets_id,ticket_count,status,join_time,created_at) VALUES (?,?,?,?,?,?,?,?)
kylin_group_buy_member.cancel_by_order_tickets=UPDATE kylin_group_buy_member SET status=2 WHERE order_tickets_id=? AND status=-1
kylin_group_buy_order.cancel_if_prepare=UPDATE kylin_group_buy_order SET status=3, current_count=0, updated_at=? WHERE group_order_id=? AND status=-1
kylin_group_buy_order.decr_count_if_pending=UPDATE kylin_group_buy_order SET current_count=current_count-1, updated_at=? WHERE group_order_id=? AND status=0 AND current_count>0
\ No newline at end of file
......@@ -26,6 +26,11 @@
<groupId>com.liquidnet</groupId>
<artifactId>liquidnet-common-web</artifactId>
</dependency>
<dependency>
<groupId>com.liquidnet</groupId>
<artifactId>liquidnet-common-sms</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>
</project>
package com.liquidnet.service.order.controller;
import com.liquidnet.service.base.ErrorMapping;
import com.liquidnet.service.base.ResponseDto;
import com.liquidnet.service.base.codec.annotation.DecryptAndVerify;
import com.liquidnet.service.base.codec.vo.EncryptedReq;
......@@ -60,12 +59,8 @@ public class KylinOrderTicketsController {
@ApiOperation("再次支付")
@ApiResponse(code = 200, message = "接口返回对象参数")
public ResponseDto<PayInnerResultVo> payAgain(@RequestBody @Valid PayAgainParam payAgainParam) {
PayInnerResultVo vo = orderTicketsOrderService.payAgain(payAgainParam).getData();
if (null == vo) {
return ResponseDto.failure(ErrorMapping.get("20024"));
} else {
return ResponseDto.success(vo);
}
// 直接透传 Service 结果,避免失败时 data=null 被误报为「订单不存在」
return orderTicketsOrderService.payAgain(payAgainParam);
}
@PostMapping("syncOrder")
......
......@@ -82,6 +82,8 @@ public class KylinOrderTicketsServiceImpl implements IKylinOrderTicketsOrderServ
private TaobaoTicketUtils taobaoTicketUtils;
@Autowired
private OrderRefundOvertimeServiceImpl orderRefundsCallbackService;
@Autowired
private GroupBuyOrderHelper groupBuyOrderHelper;
ArrayList<Integer> canBuyStatus = new ArrayList() {{
add(6);
......@@ -367,6 +369,13 @@ public class KylinOrderTicketsServiceImpl implements IKylinOrderTicketsOrderServ
return ResponseDto.failure(ErrorMapping.get("20015"));//入场人数量错误
}
// 拼团:库存前校验(禁券 + 发起/加入)
ResponseDto<Boolean> groupBuyCheck = groupBuyOrderHelper.validateBeforeOrder(payOrderParam, uid);
if (!groupBuyCheck.isSuccess()) {
orderUtils.backAdvanceCoupon(payOrderParam.getAdvanceCode(), uid);
return ResponseDto.failure(groupBuyCheck.getMessage());
}
// 判断库存
int surplusGeneral = orderUtils.changeSurplus(isPay, payOrderParam.getTicketId(), -payOrderParam.getNumber());
......@@ -522,18 +531,32 @@ public class KylinOrderTicketsServiceImpl implements IKylinOrderTicketsOrderServ
orderTickets.setCouponType("no");
}
if (isMember) {
orderTickets.setPriceTotal(ticketData.getMemberPrice().multiply(BigDecimal.valueOf(payOrderParam.getNumber())).add(payOrderParam.getIsExpress() == 1 ? priceExpress : BigDecimal.valueOf(0)));
boolean isGroupBuy = groupBuyOrderHelper.isGroupBuyOrder(payOrderParam);
BigDecimal groupUnitPrice = groupBuyOrderHelper.resolveUnitPrice(payOrderParam, ticketData.getPrice());
BigDecimal expressAmount = payOrderParam.getIsExpress() == 1 ? priceExpress : BigDecimal.valueOf(0);
if (isGroupBuy) {
if (groupUnitPrice == null) {
throw new Exception("拼团价计算失败");
}
// 拼团单:应付=原价×数量;实付=拼团价×数量(直接算);优惠=应付-实付
BigDecimal originalTotal = ticketData.getPrice().multiply(BigDecimal.valueOf(payOrderParam.getNumber())).add(expressAmount);
BigDecimal groupActual = groupUnitPrice.multiply(BigDecimal.valueOf(payOrderParam.getNumber())).add(expressAmount);
orderTickets.setPriceTotal(originalTotal);
orderTickets.setPriceActual(groupActual);
orderTickets.setPriceVoucher(originalTotal.subtract(groupActual).max(BigDecimal.ZERO));
} else if (isMember) {
orderTickets.setPriceTotal(ticketData.getMemberPrice().multiply(BigDecimal.valueOf(payOrderParam.getNumber())).add(expressAmount));
orderTickets.setPriceVoucher(BigDecimal.ZERO);
} else {
orderTickets.setPriceTotal(ticketData.getPrice().multiply(BigDecimal.valueOf(payOrderParam.getNumber())).add(payOrderParam.getIsExpress() == 1 ? priceExpress : BigDecimal.valueOf(0)));
orderTickets.setPriceTotal(ticketData.getPrice().multiply(BigDecimal.valueOf(payOrderParam.getNumber())).add(expressAmount));
orderTickets.setPriceVoucher(BigDecimal.ZERO);
}
orderTickets.setPriceVoucher(BigDecimal.ZERO);
//券的使用逻辑
//券的使用逻辑(拼团禁券,价格已在上方直接赋值)
sqls.add(SqlMapping.get("kylin_order_coupon.add"));
LinkedList<Object[]> sqlsDataE = ObjectUtil.cloneLinkedListObj();
ArrayList<KylinOrderCoupons> couponsList = new ArrayList();
if (advanceMap != null) {
if (!isGroupBuy && advanceMap != null) {
BigDecimal priceVoucher = (BigDecimal) advanceMap.get("voucher");
Integer typeVoucher = (Integer) advanceMap.get("type");
KylinOrderCoupons couponsAdvance = KylinOrderCoupons.getNew();
......@@ -548,7 +571,7 @@ public class KylinOrderTicketsServiceImpl implements IKylinOrderTicketsOrderServ
});
}
if (payOrderParam.getVoucherCode() != null) {
if (!isGroupBuy && payOrderParam.getVoucherCode() != null) {
HashMap<String, Object> hashMap = orderUtils.useCoupon(payOrderParam.getVoucherCode(), content, orderTickets.getPriceTotal().subtract(priceExpress), payOrderParam.getPerformanceId(), payOrderParam.getTimeId(), payOrderParam.getTicketId(),performanceData.getType());
if (null != hashMap) {
BigDecimal priceVoucher = (BigDecimal) hashMap.get("voucher");
......@@ -574,8 +597,11 @@ public class KylinOrderTicketsServiceImpl implements IKylinOrderTicketsOrderServ
dataUtils.setOrderCoupon(orderTicketId, couponsList);
}
BigDecimal finalPrice = orderTickets.getPriceTotal().subtract(orderTickets.getPriceVoucher());
orderTickets.setPriceActual(finalPrice.compareTo(BigDecimal.ZERO) <= 0 ? BigDecimal.ZERO : finalPrice);
// 普通单:实付=应付-优惠;拼团单实付已在上方直接赋值,不再重算
if (!isGroupBuy) {
BigDecimal finalPrice = orderTickets.getPriceTotal().subtract(orderTickets.getPriceVoucher());
orderTickets.setPriceActual(finalPrice.compareTo(BigDecimal.ZERO) <= 0 ? BigDecimal.ZERO : finalPrice);
}
orderTickets.setPriceExpress(priceExpress);
orderTickets.setPriceRefund(BigDecimal.valueOf(0.0));
orderTickets.setRefundNumber(0);
......@@ -820,6 +846,14 @@ public class KylinOrderTicketsServiceImpl implements IKylinOrderTicketsOrderServ
orderTicketVo.setOverdueAt(orderTickets.getCreatedAt().plusMinutes(performanceData.getPayCountdownMinute()).format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
orderTicketVo.setCreatedAt(orderTickets.getCreatedAt().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
orderTicketVo.setChangeDate(orderTickets.getCreatedAt());
// 拼团:写团 + member(-1),票单冗余 groupOrderId
if (isGroupBuy) {
String nickname = StringUtils.defaultString((String) token.get("nickname"), "");
String avatar = StringUtils.defaultString((String) token.get("avatar"), "");
String groupOrderId = groupBuyOrderHelper.createOrJoinOnOrder(
payOrderParam, uid, orderTicketId, payOrderParam.getNumber(), nickname, avatar);
orderTicketVo.setGroupOrderId(groupOrderId);
}
mongoTemplate.insert(orderTicketVo, KylinOrderTicketVo.class.getSimpleName());
orderUtils.resetOrderListVo(uid, 1, orderTicketVo.getOrderTicketsId(), orderTicketVo);
......@@ -869,6 +903,11 @@ public class KylinOrderTicketsServiceImpl implements IKylinOrderTicketsOrderServ
if (checkVo.getStatus() == 1) {
return ResponseDto.failure(ErrorMapping.get("20027"));
}
ResponseDto<Boolean> groupBuyPayAgainCheck = groupBuyOrderHelper.validateBeforePayAgain(orderTicketData, uid);
if (!groupBuyPayAgainCheck.isSuccess()) {
log.error("[payAgain] group buy msg: {}", groupBuyPayAgainCheck.getMessage());
return ResponseDto.failure(groupBuyPayAgainCheck.getMessage());
}
if (!orderTicketData.getPayType().equals("no")) {
if (orderTicketData.getEntitiesVoList().size() == 0) {
......@@ -1169,6 +1208,17 @@ public class KylinOrderTicketsServiceImpl implements IKylinOrderTicketsOrderServ
// innerService.reportO(orderTicketData, "摩登天空");
//大麦回调
sycDamaiOrder(orderTickets.getOrderTicketsId(), orderTicketData.getIsMember());
// 拼团:按票单确认成员 / 开团 / 成团;团已取消则自动退
try {
KylinOrderTicketVo paidTicket = dataUtils.getOrderTicketVo(orderTicketData.getOrderTicketsId());
int gbResult = groupBuyOrderHelper.confirmPaidMember(paidTicket);
if (gbResult == 2) {
orderRefundsCallbackService.refundApply(orderTicketData.getOrderTicketsId(),
syncOrderParam.getPaymentId(), syncOrderParam.getPaymentType());
}
} catch (Exception gbEx) {
log.error("拼团支付确认失败 orderTicketsId={}", orderTicketData.getOrderTicketsId(), gbEx);
}
}
log.info(UserPathDto.setData("订单支付成功回调", syncOrderParam, ""));
return "success";
......
package com.liquidnet.service.order.utils;
import com.liquidnet.common.cache.redis.util.RedisUtil;
import com.liquidnet.commons.lang.util.CollectionUtil;
import com.liquidnet.commons.lang.util.DateUtil;
import com.liquidnet.commons.lang.util.IDGenerator;
import com.liquidnet.commons.lang.util.StringUtil;
import com.liquidnet.service.base.ResponseDto;
import com.liquidnet.service.base.SqlMapping;
import com.liquidnet.service.base.constant.MQConst;
import com.liquidnet.service.base.constant.RedisKeyExpireConst;
import com.liquidnet.service.kylin.constant.GroupBuyStatusConst;
import com.liquidnet.service.kylin.constant.KylinRedisConst;
import com.liquidnet.service.kylin.dto.param.PayOrderParam;
import com.liquidnet.service.kylin.dto.vo.groupbuy.KylinGroupBuyActivityVo;
import com.liquidnet.service.kylin.dto.vo.groupbuy.KylinGroupBuyDiscountVo;
import com.liquidnet.service.kylin.dto.vo.groupbuy.KylinGroupBuyMemberVo;
import com.liquidnet.service.kylin.dto.vo.groupbuy.KylinGroupBuyOrderVo;
import com.liquidnet.service.kylin.dto.vo.mongo.KylinOrderTicketVo;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
/**
* 拼团下单/支付确认辅助:写 Mongo+Redis,MySQL 走 MQ。
*/
@Component
@Slf4j
public class GroupBuyOrderHelper {
@Autowired
private MongoTemplate mongoTemplate;
@Autowired
private RedisUtil redisUtil;
@Autowired
private QueueUtils queueUtils;
@Autowired
private GroupBuySmsSender groupBuySmsSender;
public boolean isGroupBuyOrder(PayOrderParam param) {
return param != null && (StringUtils.isNotBlank(param.getGroupBuyActivityId())
|| StringUtils.isNotBlank(param.getGroupOrderId()));
}
/**
* 库存前校验:禁券 + 发起/加入校验。失败返回 failure。
*/
public ResponseDto<Boolean> validateBeforeOrder(PayOrderParam param, String uid) {
if (!isGroupBuyOrder(param)) {
return ResponseDto.success(true);
}
if (StringUtils.isNotBlank(param.getVoucherCode())
|| StringUtils.isNotBlank(param.getAdvanceCode())
|| (param.getVoucherType() != null && param.getVoucherType().equals(3))) {
return ResponseDto.failure("拼团订单不支持优惠券/兑换券/优先购券");
}
if (StringUtils.isNotBlank(param.getGroupBuyActivityId()) && StringUtils.isNotBlank(param.getGroupOrderId())) {
return ResponseDto.failure("发起与加入参数不能同时传");
}
if (StringUtils.isNotBlank(param.getGroupBuyActivityId())) {
return validateCreate(param, uid);
}
return validateJoin(param, uid);
}
/**
* 再次支付前置校验:票单已绑定团,只判断能否继续付,不重建团。
* 非拼团票单直接通过。
*/
public ResponseDto<Boolean> validateBeforePayAgain(KylinOrderTicketVo ticket, String uid) {
if (ticket == null || StringUtils.isBlank(ticket.getGroupOrderId())) {
return ResponseDto.success(true);
}
KylinGroupBuyOrderVo group = findGroup(ticket.getGroupOrderId());
if (group == null) {
return ResponseDto.failure("拼团不存在");
}
KylinGroupBuyMemberVo member = findMember(group, ticket.getOrderTicketsId());
if (member == null) {
return ResponseDto.failure("拼团成员不存在");
}
Integer groupStatus = group.getStatus();
if (GroupBuyStatusConst.OrderStatus.CANCELLED.equals(groupStatus)) {
return ResponseDto.failure("拼团已取消,无法支付");
}
if (!GroupBuyStatusConst.MemberStatus.PENDING_PAY.equals(member.getStatus())) {
return ResponseDto.failure("拼团成员状态不可支付");
}
if (GroupBuyStatusConst.OrderStatus.FAILED.equals(groupStatus)) {
return ResponseDto.failure("拼团已失败,无法支付");
}
if (GroupBuyStatusConst.OrderStatus.SUCCESS.equals(groupStatus)) {
return ResponseDto.failure("拼团已结束,无法支付");
}
if (GroupBuyStatusConst.OrderStatus.PREPARE.equals(groupStatus)) {
if (!uid.equals(group.getLeaderUserId()) || !uid.equals(member.getUserId())) {
return ResponseDto.failure("拼团当前不可支付");
}
return ResponseDto.success(true);
}
if (GroupBuyStatusConst.OrderStatus.PENDING.equals(groupStatus)) {
if (group.getExpiresAt() != null
&& DateUtil.Formatter.yyyyMMddHHmmss.parse(group.getExpiresAt()).isBefore(LocalDateTime.now())) {
return ResponseDto.failure("拼团已过期");
}
return ResponseDto.success(true);
}
return ResponseDto.failure("拼团当前不可支付");
}
private ResponseDto<Boolean> validateCreate(PayOrderParam param, String uid) {
KylinGroupBuyActivityVo activity = getActiveActivity(param.getGroupBuyActivityId());
if (activity == null) {
return ResponseDto.failure("拼团活动不存在或未进行中");
}
if (!activity.getPerformanceId().equals(param.getPerformanceId())) {
return ResponseDto.failure("演出与拼团活动不匹配");
}
KylinGroupBuyDiscountVo discount = findDiscount(activity, param.getTimeId(), param.getTicketId());
if (discount == null) {
return ResponseDto.failure("该票档不支持拼团");
}
if (param.getNumber() == null || param.getNumber() < 1
|| param.getNumber() > activity.getMaxBuyPerUser()) {
return ResponseDto.failure("超过拼团限购数量");
}
Query exists = Query.query(Criteria.where("activityId").is(param.getGroupBuyActivityId())
.and("leaderUserId").is(uid)
.and("status").in(GroupBuyStatusConst.OrderStatus.PREPARE, GroupBuyStatusConst.OrderStatus.PENDING));
if (mongoTemplate.exists(exists, KylinGroupBuyOrderVo.class, KylinGroupBuyOrderVo.class.getSimpleName())) {
return ResponseDto.failure("同一活动只能发起一次拼团");
}
return ResponseDto.success(true);
}
private ResponseDto<Boolean> validateJoin(PayOrderParam param, String uid) {
KylinGroupBuyOrderVo group = findGroup(param.getGroupOrderId());
if (group == null) {
return ResponseDto.failure("拼团不存在");
}
if (!GroupBuyStatusConst.OrderStatus.PENDING.equals(group.getStatus())) {
return ResponseDto.failure("拼团当前不可加入");
}
if (group.getExpiresAt() != null
&& DateUtil.Formatter.yyyyMMddHHmmss.parse(group.getExpiresAt()).isBefore(LocalDateTime.now())) {
return ResponseDto.failure("拼团已过期");
}
if (group.getCurrentCount() != null && group.getTargetCount() != null
&& group.getCurrentCount() >= group.getTargetCount()) {
return ResponseDto.failure("拼团人数已满");
}
if (GroupBuyStatusConst.PublicFlag.PRIVATE.equals(group.getIsPublic())) {
if (StringUtils.isBlank(param.getShareToken()) || !param.getShareToken().equals(group.getShareToken())) {
return ResponseDto.failure("邀请链接无效");
}
}
if (!param.getTimeId().equals(group.getTimeId()) || !param.getTicketId().equals(group.getTicketId())) {
return ResponseDto.failure("场次或票档与拼团不一致");
}
KylinGroupBuyActivityVo activity = getActiveActivity(group.getActivityId());
if (activity == null) {
return ResponseDto.failure("拼团活动已结束");
}
if (param.getNumber() == null || param.getNumber() < 1
|| param.getNumber() > activity.getMaxBuyPerUser()) {
return ResponseDto.failure("超过拼团限购数量");
}
if (!CollectionUtils.isEmpty(group.getMembers())) {
for (KylinGroupBuyMemberVo m : group.getMembers()) {
if (uid.equals(m.getUserId())
&& (GroupBuyStatusConst.MemberStatus.PENDING_PAY.equals(m.getStatus())
|| GroupBuyStatusConst.MemberStatus.JOINED.equals(m.getStatus()))) {
return ResponseDto.failure("您已在该拼团中");
}
}
}
return ResponseDto.success(true);
}
/**
* 拼团单价;非拼团返回 null。
*/
public BigDecimal resolveUnitPrice(PayOrderParam param, BigDecimal originalPrice) {
if (!isGroupBuyOrder(param) || originalPrice == null) {
return null;
}
KylinGroupBuyActivityVo activity;
if (StringUtils.isNotBlank(param.getGroupBuyActivityId())) {
activity = getActiveActivity(param.getGroupBuyActivityId());
} else {
KylinGroupBuyOrderVo group = findGroup(param.getGroupOrderId());
activity = group == null ? null : getActiveActivity(group.getActivityId());
}
if (activity == null) {
return null;
}
KylinGroupBuyDiscountVo discount = findDiscount(activity, param.getTimeId(), param.getTicketId());
if (discount == null) {
return null;
}
return calculatePrice(originalPrice, discount);
}
public BigDecimal calculatePrice(BigDecimal originalPrice, KylinGroupBuyDiscountVo discount) {
if (originalPrice == null || discount == null || discount.getDiscountValue() == null) {
return null;
}
BigDecimal value = discount.getDiscountValue();
if (GroupBuyStatusConst.DiscountType.FIXED_PRICE.equals(discount.getDiscountType())) {
return value.max(BigDecimal.ZERO).setScale(2, RoundingMode.HALF_UP);
}
if (GroupBuyStatusConst.DiscountType.DISCOUNT.equals(discount.getDiscountType())) {
return originalPrice.multiply(value).divide(BigDecimal.valueOf(100), 2, RoundingMode.HALF_UP)
.max(BigDecimal.ZERO);
}
if (GroupBuyStatusConst.DiscountType.REDUCE.equals(discount.getDiscountType())) {
return originalPrice.subtract(value).max(BigDecimal.ZERO).setScale(2, RoundingMode.HALF_UP);
}
return null;
}
/**
* 下单成功后写团 + member(-1),回填 ticket.groupOrderId,发 MQ。
*/
public String createOrJoinOnOrder(PayOrderParam param, String uid, String orderTicketsId,
Integer ticketCount, String nickname, String avatar) {
if (!isGroupBuyOrder(param)) {
return null;
}
String now = now();
if (StringUtils.isNotBlank(param.getGroupBuyActivityId())) {
return createPrepareGroup(param, uid, orderTicketsId, ticketCount, nickname, avatar, now);
}
return joinPendingGroup(param, uid, orderTicketsId, ticketCount, nickname, avatar, now);
}
private String createPrepareGroup(PayOrderParam param, String uid, String orderTicketsId,
Integer ticketCount, String nickname, String avatar, String now) {
KylinGroupBuyActivityVo activity = getActiveActivity(param.getGroupBuyActivityId());
if (activity == null) {
throw new IllegalStateException("拼团活动不存在或未进行中");
}
String groupOrderId = IDGenerator.nextSnowId();
KylinGroupBuyMemberVo member = buildMember(groupOrderId, uid, orderTicketsId, ticketCount,
nickname, avatar, now);
KylinGroupBuyOrderVo group = new KylinGroupBuyOrderVo();
group.setGroupOrderId(groupOrderId);
group.setActivityId(activity.getActivityId());
group.setLeaderUserId(uid);
group.setOrderTicketsId(orderTicketsId);
group.setTimeId(param.getTimeId());
group.setTicketId(param.getTicketId());
group.setTargetCount(activity.getMinGroupSize());
group.setCurrentCount(1);
group.setStatus(GroupBuyStatusConst.OrderStatus.PREPARE);
group.setIsPublic(param.getIsPublic() == null ? GroupBuyStatusConst.PublicFlag.PUBLIC : param.getIsPublic());
group.setShareToken(IDGenerator.nextSnowId());
group.setCreatedAt(now);
group.setUpdatedAt(now);
List<KylinGroupBuyMemberVo> members = new ArrayList<>();
members.add(member);
group.setMembers(members);
mongoTemplate.insert(group, KylinGroupBuyOrderVo.class.getSimpleName());
cacheGroup(group);
sendReplaceGroupSql(group);
return groupOrderId;
}
private String joinPendingGroup(PayOrderParam param, String uid, String orderTicketsId,
Integer ticketCount, String nickname, String avatar, String now) {
String groupOrderId = param.getGroupOrderId();
String lockKey = KylinRedisConst.GROUP_BUY_ORDER_LOCK.concat(groupOrderId);
if (!redisUtil.lock(lockKey, 1, 15)) {
throw new IllegalStateException("拼团火爆,请稍后重试");
}
try {
// 持锁后回源 Mongo,避免并发读到脏缓存
KylinGroupBuyOrderVo group = loadGroupFromMongo(groupOrderId);
if (group == null || !GroupBuyStatusConst.OrderStatus.PENDING.equals(group.getStatus())) {
throw new IllegalStateException("拼团当前不可加入");
}
if (group.getExpiresAt() != null
&& DateUtil.Formatter.yyyyMMddHHmmss.parse(group.getExpiresAt()).isBefore(LocalDateTime.now())) {
throw new IllegalStateException("拼团已过期");
}
if (group.getCurrentCount() != null && group.getTargetCount() != null
&& group.getCurrentCount() >= group.getTargetCount()) {
throw new IllegalStateException("拼团人数已满");
}
if (GroupBuyStatusConst.PublicFlag.PRIVATE.equals(group.getIsPublic())) {
if (StringUtils.isBlank(param.getShareToken()) || !param.getShareToken().equals(group.getShareToken())) {
throw new IllegalStateException("邀请链接无效");
}
}
if (!CollectionUtils.isEmpty(group.getMembers())) {
for (KylinGroupBuyMemberVo m : group.getMembers()) {
if (uid.equals(m.getUserId())
&& (GroupBuyStatusConst.MemberStatus.PENDING_PAY.equals(m.getStatus())
|| GroupBuyStatusConst.MemberStatus.JOINED.equals(m.getStatus()))) {
throw new IllegalStateException("您已在该拼团中");
}
}
} else {
group.setMembers(new ArrayList<>());
}
KylinGroupBuyMemberVo member = buildMember(group.getGroupOrderId(), uid, orderTicketsId, ticketCount,
nickname, avatar, now);
group.getMembers().add(member);
group.setCurrentCount((group.getCurrentCount() == null ? 0 : group.getCurrentCount()) + 1);
group.setUpdatedAt(now);
saveGroup(group);
return group.getGroupOrderId();
} finally {
redisUtil.uLock(lockKey);
}
}
private KylinGroupBuyOrderVo loadGroupFromMongo(String groupOrderId) {
if (StringUtils.isBlank(groupOrderId)) {
return null;
}
return mongoTemplate.findOne(
Query.query(Criteria.where("groupOrderId").is(groupOrderId)),
KylinGroupBuyOrderVo.class, KylinGroupBuyOrderVo.class.getSimpleName());
}
/**
* 支付成功:按 orderTicketsId 确认成员;团长则开团;满员则成团。
* @return 0无拼团 1成功 2需退款
*/
public int confirmPaidMember(KylinOrderTicketVo ticket) {
if (ticket == null || StringUtil.isEmpty(ticket.getOrderTicketsId())) {
return 0;
}
KylinGroupBuyOrderVo located = findGroupByOrderTicketsId(ticket.getOrderTicketsId());
if (located == null) {
return 0;
}
String lockKey = KylinRedisConst.GROUP_BUY_ORDER_LOCK.concat(located.getGroupOrderId());
if (!redisUtil.lock(lockKey, 1, 15)) {
log.warn("拼团支付确认获取锁失败 groupOrderId={}", located.getGroupOrderId());
return 0;
}
try {
KylinGroupBuyOrderVo group = loadGroupFromMongo(located.getGroupOrderId());
if (group == null) {
return 0;
}
KylinGroupBuyMemberVo member = findMember(group, ticket.getOrderTicketsId());
if (member == null) {
return 0;
}
if (GroupBuyStatusConst.MemberStatus.JOINED.equals(member.getStatus())) {
return 1;
}
Integer groupStatus = group.getStatus();
String now = now();
if (GroupBuyStatusConst.OrderStatus.CANCELLED.equals(groupStatus)
|| GroupBuyStatusConst.OrderStatus.FAILED.equals(groupStatus)
|| (GroupBuyStatusConst.OrderStatus.SUCCESS.equals(groupStatus)
&& GroupBuyStatusConst.MemberStatus.PENDING_PAY.equals(member.getStatus()))) {
member.setStatus(GroupBuyStatusConst.MemberStatus.REFUNDED);
if (group.getCurrentCount() != null && group.getCurrentCount() > 0) {
group.setCurrentCount(group.getCurrentCount() - 1);
}
group.setUpdatedAt(now);
saveGroup(group);
return 2;
}
if (GroupBuyStatusConst.OrderStatus.PREPARE.equals(groupStatus)
&& ticket.getUserId().equals(group.getLeaderUserId())) {
group.setStatus(GroupBuyStatusConst.OrderStatus.PENDING);
group.setExpiresAt(DateUtil.Formatter.yyyyMMddHHmmss.format(LocalDateTime.now().plusHours(24)));
member.setStatus(GroupBuyStatusConst.MemberStatus.JOINED);
group.setUpdatedAt(now);
saveGroup(group);
return 1;
}
if (GroupBuyStatusConst.OrderStatus.PENDING.equals(groupStatus)) {
member.setStatus(GroupBuyStatusConst.MemberStatus.JOINED);
boolean becameSuccess = false;
if (group.getCurrentCount() != null && group.getTargetCount() != null
&& group.getCurrentCount() >= group.getTargetCount()) {
group.setStatus(GroupBuyStatusConst.OrderStatus.SUCCESS);
becameSuccess = true;
}
group.setUpdatedAt(now);
saveGroup(group);
if (becameSuccess) {
try {
groupBuySmsSender.notifySuccess(group);
} catch (Exception e) {
log.error("拼团成功短信通知失败 groupOrderId={}", group.getGroupOrderId(), e);
}
}
return 1;
}
return 0;
} finally {
redisUtil.uLock(lockKey);
}
}
public KylinGroupBuyActivityVo getActiveActivity(String activityId) {
if (activityId == null) {
return null;
}
Object obj = redisUtil.get(KylinRedisConst.GROUP_BUY_ACTIVITY.concat(activityId));
if (obj != null) {
KylinGroupBuyActivityVo activity = (KylinGroupBuyActivityVo) obj;
return GroupBuyStatusConst.ActivityStatus.ACTIVE.equals(activity.getStatus()) ? activity : null;
}
KylinGroupBuyActivityVo activity = mongoTemplate.findOne(
Query.query(Criteria.where("activityId").is(activityId)
.and("status").is(GroupBuyStatusConst.ActivityStatus.ACTIVE)),
KylinGroupBuyActivityVo.class, KylinGroupBuyActivityVo.class.getSimpleName());
if (activity != null) {
redisUtil.set(KylinRedisConst.GROUP_BUY_ACTIVITY.concat(activityId), activity,
RedisKeyExpireConst.KYLIN_GROUP_BUY_EXPIRE);
}
return activity;
}
public KylinGroupBuyDiscountVo findDiscount(KylinGroupBuyActivityVo activity, String timeId, String ticketId) {
if (activity == null || activity.getDiscounts() == null) {
return null;
}
for (KylinGroupBuyDiscountVo discount : activity.getDiscounts()) {
if (timeId.equals(discount.getTimeId()) && ticketId.equals(discount.getTicketId())) {
return discount;
}
}
return null;
}
public KylinGroupBuyOrderVo findGroup(String groupOrderId) {
if (StringUtils.isBlank(groupOrderId)) {
return null;
}
Object obj = redisUtil.get(KylinRedisConst.GROUP_BUY_ORDER.concat(groupOrderId));
if (obj != null) {
return (KylinGroupBuyOrderVo) obj;
}
KylinGroupBuyOrderVo group = mongoTemplate.findOne(
Query.query(Criteria.where("groupOrderId").is(groupOrderId)),
KylinGroupBuyOrderVo.class, KylinGroupBuyOrderVo.class.getSimpleName());
cacheGroup(group);
return group;
}
public KylinGroupBuyOrderVo findGroupByOrderTicketsId(String orderTicketsId) {
KylinGroupBuyOrderVo group = mongoTemplate.findOne(
Query.query(Criteria.where("members.orderTicketsId").is(orderTicketsId)),
KylinGroupBuyOrderVo.class, KylinGroupBuyOrderVo.class.getSimpleName());
if (group != null) {
cacheGroup(group);
}
return group;
}
private KylinGroupBuyMemberVo findMember(KylinGroupBuyOrderVo group, String orderTicketsId) {
if (group == null || CollectionUtils.isEmpty(group.getMembers())) {
return null;
}
for (KylinGroupBuyMemberVo m : group.getMembers()) {
if (orderTicketsId.equals(m.getOrderTicketsId())) {
return m;
}
}
return null;
}
private KylinGroupBuyMemberVo buildMember(String groupOrderId, String uid, String orderTicketsId,
Integer ticketCount, String nickname, String avatar, String now) {
KylinGroupBuyMemberVo member = new KylinGroupBuyMemberVo();
member.setMemberId(IDGenerator.nextSnowId());
member.setGroupOrderId(groupOrderId);
member.setUserId(uid);
member.setOrderTicketsId(orderTicketsId);
member.setTicketCount(ticketCount);
member.setStatus(GroupBuyStatusConst.MemberStatus.PENDING_PAY);
member.setJoinTime(now);
member.setCreatedAt(now);
member.setNickname(nickname == null ? "" : nickname);
member.setAvatar(avatar == null ? "" : avatar);
return member;
}
private void saveGroup(KylinGroupBuyOrderVo group) {
clearGroupCache(group);
mongoTemplate.remove(Query.query(Criteria.where("groupOrderId").is(group.getGroupOrderId())),
KylinGroupBuyOrderVo.class, KylinGroupBuyOrderVo.class.getSimpleName());
mongoTemplate.insert(group, KylinGroupBuyOrderVo.class.getSimpleName());
cacheGroup(group);
sendReplaceGroupSql(group);
}
private void cacheGroup(KylinGroupBuyOrderVo group) {
if (group == null) {
return;
}
redisUtil.set(KylinRedisConst.GROUP_BUY_ORDER.concat(group.getGroupOrderId()), group,
RedisKeyExpireConst.KYLIN_GROUP_BUY_EXPIRE);
if (StringUtils.isNotBlank(group.getShareToken())) {
redisUtil.set(KylinRedisConst.GROUP_BUY_ORDER_SHARE.concat(group.getShareToken()), group,
RedisKeyExpireConst.KYLIN_GROUP_BUY_EXPIRE);
}
}
private void clearGroupCache(KylinGroupBuyOrderVo group) {
if (group == null) {
return;
}
redisUtil.del(KylinRedisConst.GROUP_BUY_ORDER.concat(group.getGroupOrderId()));
if (StringUtils.isNotBlank(group.getShareToken())) {
redisUtil.del(KylinRedisConst.GROUP_BUY_ORDER_SHARE.concat(group.getShareToken()));
}
}
private void sendReplaceGroupSql(KylinGroupBuyOrderVo group) {
LinkedList<String> sqls = CollectionUtil.linkedListString();
LinkedList<Object[]> orderDel = CollectionUtil.linkedListObjectArr();
LinkedList<Object[]> orderIns = CollectionUtil.linkedListObjectArr();
LinkedList<Object[]> memberDel = CollectionUtil.linkedListObjectArr();
LinkedList<Object[]> memberIns = CollectionUtil.linkedListObjectArr();
sqls.add(SqlMapping.get("kylin_group_buy_order.delete"));
orderDel.add(new Object[]{group.getGroupOrderId()});
sqls.add(SqlMapping.get("kylin_group_buy_order.insert"));
orderIns.add(orderInsertObject(group));
sqls.add(SqlMapping.get("kylin_group_buy_member.delete_by_group"));
memberDel.add(new Object[]{group.getGroupOrderId()});
if (!CollectionUtils.isEmpty(group.getMembers())) {
sqls.add(SqlMapping.get("kylin_group_buy_member.insert"));
for (KylinGroupBuyMemberVo m : group.getMembers()) {
memberIns.add(memberInsertObject(m));
}
queueUtils.sendMsgByRedis(MQConst.KylinQueue.SQL_ORDER_CREATE.getKey(),
SqlMapping.gets(sqls, orderDel, orderIns, memberDel, memberIns));
} else {
queueUtils.sendMsgByRedis(MQConst.KylinQueue.SQL_ORDER_CREATE.getKey(),
SqlMapping.gets(sqls, orderDel, orderIns, memberDel));
}
}
private Object[] orderInsertObject(KylinGroupBuyOrderVo vo) {
return new Object[]{
vo.getGroupOrderId(), vo.getActivityId(), vo.getLeaderUserId(), vo.getOrderTicketsId(),
vo.getTargetCount(), vo.getCurrentCount(), vo.getStatus(), vo.getIsPublic(), vo.getShareToken(),
vo.getExpiresAt() == null ? null : DateUtil.Formatter.yyyyMMddHHmmss.parse(vo.getExpiresAt()),
DateUtil.Formatter.yyyyMMddHHmmss.parse(vo.getCreatedAt()),
DateUtil.Formatter.yyyyMMddHHmmss.parse(vo.getUpdatedAt())
};
}
private Object[] memberInsertObject(KylinGroupBuyMemberVo vo) {
return new Object[]{
vo.getMemberId(), vo.getGroupOrderId(), vo.getUserId(), vo.getOrderTicketsId(),
vo.getTicketCount(), vo.getStatus(),
DateUtil.Formatter.yyyyMMddHHmmss.parse(vo.getJoinTime()),
DateUtil.Formatter.yyyyMMddHHmmss.parse(vo.getCreatedAt())
};
}
private String now() {
return DateUtil.Formatter.yyyyMMddHHmmss.format(LocalDateTime.now());
}
}
package com.liquidnet.service.order.utils;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.liquidnet.common.cache.redis.util.RedisUtil;
import com.liquidnet.common.sms.constant.SmsEnum;
import com.liquidnet.commons.lang.util.JsonUtils;
import com.liquidnet.service.base.SmsMessage;
import com.liquidnet.service.base.constant.MQConst;
import com.liquidnet.service.kylin.constant.GroupBuyStatusConst;
import com.liquidnet.service.kylin.constant.KylinRedisConst;
import com.liquidnet.service.kylin.dto.vo.groupbuy.KylinGroupBuyActivityVo;
import com.liquidnet.service.kylin.dto.vo.groupbuy.KylinGroupBuyMemberVo;
import com.liquidnet.service.kylin.dto.vo.groupbuy.KylinGroupBuyOrderVo;
import com.liquidnet.service.kylin.dto.vo.middle.KylinTicketTimesVo;
import com.liquidnet.service.kylin.dto.vo.middle.KylinTicketVo;
import com.liquidnet.service.kylin.dto.vo.mongo.KylinOrderTicketVo;
import com.liquidnet.service.kylin.dto.vo.mongo.KylinPerformanceVo;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* 拼团成功短信:入队 {@link MQConst.KylinQueue#SMS_NOTICE}(与购票短信一致,异步)。
*/
@Component
@Slf4j
public class GroupBuySmsSender {
@Autowired
private QueueUtils queueUtils;
@Autowired
private RedisUtil redisUtil;
@Autowired
private MongoTemplate mongoTemplate;
@Autowired
private DataUtils dataUtils;
public void notifySuccess(KylinGroupBuyOrderVo group) {
if (group == null || !isSmsEnabled()) {
return;
}
List<String> phones = collectPhones(group, true, false);
if (phones.isEmpty()) {
log.info("拼团成功短信无有效手机号 groupOrderId={}", group.getGroupOrderId());
return;
}
ObjectNode params = buildSuccessParams(group);
for (String phone : phones) {
enqueueSms(phone, SmsEnum.ADTemplate.SMS_510540063.name(), params,
group.getGroupOrderId(), "success");
}
}
private boolean isSmsEnabled() {
Object v = redisUtil.get(KylinRedisConst.GROUP_BUY_SMS_ENABLED);
if (v == null) {
return true;
}
String s = String.valueOf(v).trim();
return !("0".equals(s) || "false".equalsIgnoreCase(s));
}
private List<String> collectPhones(KylinGroupBuyOrderVo group, boolean joinedOnly, boolean includePendingPay) {
Set<String> phones = new HashSet<>();
if (group == null || CollectionUtils.isEmpty(group.getMembers())) {
return new ArrayList<>();
}
for (KylinGroupBuyMemberVo m : group.getMembers()) {
if (m == null || StringUtils.isBlank(m.getOrderTicketsId())) {
continue;
}
if (m.getOrderTicketsId().startsWith("ROBOT_")) {
continue;
}
Integer st = m.getStatus();
boolean ok = GroupBuyStatusConst.MemberStatus.JOINED.equals(st)
|| (includePendingPay && GroupBuyStatusConst.MemberStatus.PENDING_PAY.equals(st));
if (joinedOnly) {
ok = GroupBuyStatusConst.MemberStatus.JOINED.equals(st);
}
if (!ok) {
continue;
}
String mobile = resolveMobile(m.getOrderTicketsId());
if (StringUtils.isNotBlank(mobile)) {
phones.add(mobile.trim());
}
}
return new ArrayList<>(phones);
}
private String resolveMobile(String orderTicketsId) {
Object cached = redisUtil.get(KylinRedisConst.ORDER.concat(orderTicketsId));
if (cached instanceof KylinOrderTicketVo) {
return ((KylinOrderTicketVo) cached).getUserMobile();
}
KylinOrderTicketVo ticket = mongoTemplate.findOne(
Query.query(Criteria.where("orderTicketsId").is(orderTicketsId)),
KylinOrderTicketVo.class, KylinOrderTicketVo.class.getSimpleName());
return ticket == null ? null : ticket.getUserMobile();
}
private ObjectNode buildSuccessParams(KylinGroupBuyOrderVo group) {
ObjectNode node = JsonUtils.OM().createObjectNode();
String name = "";
String time = "";
String ticket = "";
try {
KylinGroupBuyActivityVo activity = mongoTemplate.findOne(
Query.query(Criteria.where("activityId").is(group.getActivityId())),
KylinGroupBuyActivityVo.class, KylinGroupBuyActivityVo.class.getSimpleName());
if (activity != null && StringUtils.isNotBlank(activity.getPerformanceId())) {
KylinPerformanceVo p = dataUtils.getPerformanceVo(activity.getPerformanceId());
if (p != null) {
name = p.getTitle() == null ? "" : p.getTitle();
String[] titles = resolveTimeTicketTitle(p, group.getTimeId(), group.getTicketId());
time = titles[0];
ticket = titles[1];
}
}
} catch (Exception e) {
log.warn("拼团成功短信组装变量失败 groupOrderId={}", group.getGroupOrderId(), e);
}
node.put("name", name);
node.put("time", time);
node.put("ticket", ticket);
return node;
}
private String[] resolveTimeTicketTitle(KylinPerformanceVo p, String timeId, String ticketId) {
String timeTitle = "";
String ticketTitle = "";
if (p.getTicketTimeList() == null || timeId == null) {
return new String[]{timeTitle, ticketTitle};
}
for (KylinTicketTimesVo t : p.getTicketTimeList()) {
if (!timeId.equals(t.getTimeId())) {
continue;
}
timeTitle = t.getTitle() == null ? "" : t.getTitle();
if (t.getTicketList() != null && ticketId != null) {
for (KylinTicketVo tk : t.getTicketList()) {
if (ticketId.equals(tk.getTicketsId())) {
ticketTitle = tk.getTitle() == null ? "" : tk.getTitle();
break;
}
}
}
break;
}
return new String[]{timeTitle, ticketTitle};
}
private void enqueueSms(String phone, String templateCode, ObjectNode params, String groupOrderId, String scene) {
try {
SmsMessage msg = SmsMessage.builder()
.setPhone(phone)
.setSignName(SmsEnum.ADSignName.M02.getVal())
.setTemplateCode(templateCode);
if (params == null || params.size() == 0) {
msg.setEmptyTemplateParam();
} else {
Iterator<Map.Entry<String, com.fasterxml.jackson.databind.JsonNode>> it = params.fields();
while (it.hasNext()) {
Map.Entry<String, com.fasterxml.jackson.databind.JsonNode> e = it.next();
msg.setTemplateParam(e.getKey(), e.getValue() == null ? "" : e.getValue().asText(""));
}
}
queueUtils.sendMsgByRedis(MQConst.KylinQueue.SMS_NOTICE.getKey(), msg.toJson());
} catch (Exception e) {
log.error("拼团短信入队异常 scene={} groupOrderId={} phone={}", scene, groupOrderId, phone, e);
}
}
}
......@@ -79,3 +79,9 @@ dragon_orders.updateNotifyStatus=update dragon_orders t set t.notify_status = ?
#---- \u85CF\u54C1\u8BA2\u5355\u5173\u8054\u85CF\u54C1ID
goblin_nft_order.update_artwork=UPDATE goblin_nft_order SET artwork_id=? WHERE order_id=?
#----
# ------------------------演出票拼团----------------------------
kylin_group_buy_order.delete=DELETE FROM kylin_group_buy_order WHERE group_order_id = ?
kylin_group_buy_order.insert=INSERT INTO kylin_group_buy_order (group_order_id,activity_id,leader_user_id,order_tickets_id,target_count,current_count,status,is_public,share_token,expires_at,created_at,updated_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?)
kylin_group_buy_member.delete_by_group=DELETE FROM kylin_group_buy_member WHERE group_order_id = ?
kylin_group_buy_member.insert=INSERT INTO kylin_group_buy_member (member_id,group_order_id,user_id,order_tickets_id,ticket_count,status,join_time,created_at) VALUES (?,?,?,?,?,?,?,?)
......@@ -153,6 +153,11 @@
<scope>system</scope>
<systemPath>${project.basedir}/lib/taobao-sdk-java-auto_1600401599540-20210607.jar</systemPath>
</dependency>
<dependency>
<groupId>com.liquidnet</groupId>
<artifactId>liquidnet-common-sms</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>
<build>
......
package com.liquidnet.service.platform.controller.kylin.task;
import com.liquidnet.service.base.ResponseDto;
import com.liquidnet.service.platform.service.impl.kylin.PlatformGroupBuyTaskServiceImpl;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@Api(tags = "前端-拼团任务")
@RestController
@RequestMapping("groupBuy")
@Validated
public class PlatformGroupBuyTaskController {
@Autowired
private PlatformGroupBuyTaskServiceImpl groupBuyTaskService;
@PostMapping("timeout")
@ApiOperation("拼团超时退款任务")
public ResponseDto<Boolean> timeout() {
return ResponseDto.success(groupBuyTaskService.timeout());
}
@PostMapping("activitySchedule")
@ApiOperation("拼团活动启动/结束任务")
public ResponseDto<Boolean> activitySchedule() {
return ResponseDto.success(groupBuyTaskService.activitySchedule());
}
@PostMapping("remind1h")
@ApiOperation("拼团到期前1小时提醒任务")
public ResponseDto<Boolean> remind1h() {
return ResponseDto.success(groupBuyTaskService.remind1h());
}
}
package com.liquidnet.service.platform.service.impl.kylin;
import com.liquidnet.common.cache.redis.util.RedisDataSourceUtil;
import com.liquidnet.commons.lang.util.DateUtil;
import com.liquidnet.commons.lang.util.HttpUtil;
import com.liquidnet.service.base.SqlMapping;
import com.liquidnet.service.base.constant.MQConst;
import com.liquidnet.service.base.constant.RedisKeyExpireConst;
import com.liquidnet.service.kylin.constant.GroupBuyStatusConst;
import com.liquidnet.service.kylin.constant.KylinRedisConst;
import com.liquidnet.service.kylin.dto.vo.groupbuy.KylinGroupBuyActivityVo;
import com.liquidnet.service.kylin.dto.vo.groupbuy.KylinGroupBuyMemberVo;
import com.liquidnet.service.kylin.dto.vo.groupbuy.KylinGroupBuyOrderVo;
import com.liquidnet.service.kylin.dto.vo.mongo.KylinOrderTicketEntitiesVo;
import com.liquidnet.service.kylin.dto.vo.mongo.KylinOrderTicketVo;
import com.liquidnet.service.platform.utils.GroupBuyPlatformHelper;
import com.liquidnet.service.platform.utils.GroupBuySmsSender;
import com.liquidnet.service.platform.utils.QueueUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.core.query.Update;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
@Service
@Slf4j
public class PlatformGroupBuyTaskServiceImpl {
@Autowired
private MongoTemplate mongoTemplate;
@Autowired
private RedisDataSourceUtil redisDataSourceUtil;
@Autowired
private QueueUtils queueUtils;
@Autowired
private GroupBuyPlatformHelper groupBuyPlatformHelper;
@Autowired
private GroupBuySmsSender groupBuySmsSender;
@Value("${liquidnet.service.kylin.url}")
private String kylinUrl;
public boolean timeout() {
String now = now();
Query query = Query.query(Criteria.where("status").is(GroupBuyStatusConst.OrderStatus.PENDING)
.and("expiresAt").lt(now));
List<KylinGroupBuyOrderVo> orders = mongoTemplate.find(query,
KylinGroupBuyOrderVo.class, KylinGroupBuyOrderVo.class.getSimpleName());
int processed = 0;
int skipped = 0;
for (KylinGroupBuyOrderVo order : orders) {
String groupOrderId = order.getGroupOrderId();
if (!groupBuyPlatformHelper.tryLock(groupOrderId)) {
log.warn("拼团超时任务获取锁失败 groupOrderId={}", groupOrderId);
skipped++;
continue;
}
try {
KylinGroupBuyOrderVo fresh = groupBuyPlatformHelper.loadFromMongo(groupOrderId);
if (fresh == null || !GroupBuyStatusConst.OrderStatus.PENDING.equals(fresh.getStatus())) {
skipped++;
continue;
}
if (fresh.getExpiresAt() == null
|| !DateUtil.Formatter.yyyyMMddHHmmss.parse(fresh.getExpiresAt()).isBefore(LocalDateTime.now())) {
skipped++;
continue;
}
fresh.setStatus(GroupBuyStatusConst.OrderStatus.FAILED);
fresh.setUpdatedAt(now);
// 失败短信需在 JOINED 改成 REFUNDED 之前发送
try {
groupBuySmsSender.notifyFailed(fresh);
} catch (Exception e) {
log.error("拼团失败短信通知失败 groupOrderId={}", groupOrderId, e);
}
List<String> joinedOrderTicketIds = new ArrayList<>();
if (!CollectionUtils.isEmpty(fresh.getMembers())) {
for (KylinGroupBuyMemberVo member : fresh.getMembers()) {
if (GroupBuyStatusConst.MemberStatus.PENDING_PAY.equals(member.getStatus())) {
member.setStatus(GroupBuyStatusConst.MemberStatus.CANCELLED);
} else if (GroupBuyStatusConst.MemberStatus.JOINED.equals(member.getStatus())) {
joinedOrderTicketIds.add(member.getOrderTicketsId());
member.setStatus(GroupBuyStatusConst.MemberStatus.REFUNDED);
}
}
}
fresh.setCurrentCount(0);
groupBuyPlatformHelper.saveGroup(fresh);
for (String orderTicketsId : joinedOrderTicketIds) {
log.info("[timeout] 拼团超时,订单退款: {}", orderTicketsId);
refundJoinedTicket(orderTicketsId, "拼团超时未成团,全额退款");
}
processed++;
} catch (Exception e) {
log.error("拼团超时处理失败 groupOrderId={}", groupOrderId, e);
} finally {
groupBuyPlatformHelper.unlock(groupOrderId);
}
}
log.info("拼团超时任务结束 now={}, hit={}, processed={}, skipped={}",
now, orders == null ? 0 : orders.size(), processed, skipped);
return true;
}
/**
* 到期前 1 小时提醒:每个团只发一次(remind1hSent + Redis 防重)。
*/
public boolean remind1h() {
LocalDateTime nowLdt = LocalDateTime.now();
String now = DateUtil.Formatter.yyyyMMddHHmmss.format(nowLdt);
String deadline = DateUtil.Formatter.yyyyMMddHHmmss.format(nowLdt.plusHours(1));
Query query = Query.query(Criteria.where("status").is(GroupBuyStatusConst.OrderStatus.PENDING)
.and("expiresAt").gt(now).lte(deadline)
.and("remind1hSent").ne(1));
List<KylinGroupBuyOrderVo> orders = mongoTemplate.find(query,
KylinGroupBuyOrderVo.class, KylinGroupBuyOrderVo.class.getSimpleName());
int processed = 0;
int skipped = 0;
for (KylinGroupBuyOrderVo order : orders) {
String groupOrderId = order.getGroupOrderId();
if (!groupBuyPlatformHelper.tryLock(groupOrderId)) {
log.warn("拼团1h提醒获取锁失败 groupOrderId={}", groupOrderId);
skipped++;
continue;
}
try {
KylinGroupBuyOrderVo fresh = groupBuyPlatformHelper.loadFromMongo(groupOrderId);
if (fresh == null || !GroupBuyStatusConst.OrderStatus.PENDING.equals(fresh.getStatus())) {
skipped++;
continue;
}
if (groupBuySmsSender.isRemind1hAlreadySent(fresh)) {
skipped++;
continue;
}
if (fresh.getExpiresAt() == null) {
skipped++;
continue;
}
LocalDateTime expiresAt = DateUtil.Formatter.yyyyMMddHHmmss.parse(fresh.getExpiresAt());
if (!expiresAt.isAfter(nowLdt) || expiresAt.isAfter(nowLdt.plusHours(1))) {
skipped++;
continue;
}
try {
groupBuySmsSender.notifyRemind1h(fresh);
} catch (Exception e) {
log.error("拼团1h提醒短信失败 groupOrderId={}", groupOrderId, e);
}
fresh.setRemind1hSent(1);
fresh.setUpdatedAt(now);
groupBuyPlatformHelper.saveGroup(fresh);
processed++;
} catch (Exception e) {
log.error("拼团1h提醒处理失败 groupOrderId={}", groupOrderId, e);
} finally {
groupBuyPlatformHelper.unlock(groupOrderId);
}
}
log.info("拼团1h提醒任务结束 now={}, deadline={}, hit={}, processed={}, skipped={}",
now, deadline, orders == null ? 0 : orders.size(), processed, skipped);
return true;
}
public boolean activitySchedule() {
String now = now();
List<KylinGroupBuyActivityVo> dueStartList = mongoTemplate.find(Query.query(Criteria.where("status").is(GroupBuyStatusConst.ActivityStatus.PENDING)
.and("launchType").is(GroupBuyStatusConst.LaunchType.SCHEDULED)
.and("scheduledTime").lte(now)),
KylinGroupBuyActivityVo.class, KylinGroupBuyActivityVo.class.getSimpleName());
mongoTemplate.updateMulti(Query.query(Criteria.where("status").is(GroupBuyStatusConst.ActivityStatus.PENDING)
.and("launchType").is(GroupBuyStatusConst.LaunchType.SCHEDULED)
.and("scheduledTime").lte(now)),
Update.update("status", GroupBuyStatusConst.ActivityStatus.ACTIVE).set("updatedAt", now),
"KylinGroupBuyActivityVo");
for (KylinGroupBuyActivityVo activity : dueStartList) {
activity.setStatus(GroupBuyStatusConst.ActivityStatus.ACTIVE);
activity.setUpdatedAt(now);
sendActivityStatusSql(activity.getActivityId(), GroupBuyStatusConst.ActivityStatus.PENDING,
GroupBuyStatusConst.ActivityStatus.ACTIVE, LocalDateTime.now());
cacheActivity(activity);
}
List<KylinGroupBuyActivityVo> dueEndList = mongoTemplate.find(Query.query(Criteria.where("status")
.in(GroupBuyStatusConst.ActivityStatus.ACTIVE, GroupBuyStatusConst.ActivityStatus.CLOSED)
.and("endTime").lte(now)),
KylinGroupBuyActivityVo.class, KylinGroupBuyActivityVo.class.getSimpleName());
mongoTemplate.updateMulti(Query.query(Criteria.where("status")
.in(GroupBuyStatusConst.ActivityStatus.ACTIVE, GroupBuyStatusConst.ActivityStatus.CLOSED)
.and("endTime").lte(now)),
Update.update("status", GroupBuyStatusConst.ActivityStatus.ENDED).set("updatedAt", now),
"KylinGroupBuyActivityVo");
for (KylinGroupBuyActivityVo activity : dueEndList) {
Integer fromStatus = activity.getStatus();
activity.setStatus(GroupBuyStatusConst.ActivityStatus.ENDED);
activity.setUpdatedAt(now);
sendActivityStatusSql(activity.getActivityId(), fromStatus,
GroupBuyStatusConst.ActivityStatus.ENDED, LocalDateTime.now());
cacheActivity(activity);
}
log.info("拼团活动调度结束 now={}, started={}, ended={}", now,
dueStartList.size(),
dueEndList.size());
return true;
}
private void cacheActivity(KylinGroupBuyActivityVo activity) {
if (activity == null || activity.getActivityId() == null) {
return;
}
String key = KylinRedisConst.GROUP_BUY_ACTIVITY.concat(activity.getActivityId());
redisDataSourceUtil.getRedisKylinUtil().del(key);
redisDataSourceUtil.getRedisKylinUtil().set(key, activity, RedisKeyExpireConst.KYLIN_GROUP_BUY_EXPIRE);
}
private void sendActivityStatusSql(String activityId, Integer fromStatus, Integer toStatus, LocalDateTime updatedAt) {
queueUtils.sendMsgByRedis(MQConst.SlimeQueue.SQL_SLIME_FIELD.getKey(),
SqlMapping.get("kylin_group_buy_activity.update_status", toStatus, updatedAt, activityId, fromStatus));
}
private void refundJoinedTicket(String orderTicketsId, String reason) {
KylinOrderTicketVo orderTicketVo = mongoTemplate.findOne(
Query.query(Criteria.where("orderTicketsId").is(orderTicketsId)),
KylinOrderTicketVo.class, KylinOrderTicketVo.class.getSimpleName());
if (orderTicketVo == null) {
return;
}
List<KylinOrderTicketEntitiesVo> entities = orderTicketVo.getEntitiesVoList();
if (CollectionUtils.isEmpty(entities)) {
entities = mongoTemplate.find(
Query.query(Criteria.where("orderId").is(orderTicketVo.getOrderTicketsId())),
KylinOrderTicketEntitiesVo.class, KylinOrderTicketEntitiesVo.class.getSimpleName());
}
if (CollectionUtils.isEmpty(entities)) {
return;
}
for (KylinOrderTicketEntitiesVo entity : entities) {
autoRefund(orderTicketVo.getOrderTicketsId(), entity.getOrderTicketEntitiesId(), reason);
}
}
private void autoRefund(String orderId, String orderTicketEntitiesId, String reason) {
try {
MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
params.add("orderId", orderId);
params.add("orderTicketEntitiesId", orderTicketEntitiesId);
params.add("reason", reason);
MultiValueMap<String, String> headers = new LinkedMultiValueMap<>();
headers.add("Accept", "application/json;charset=UTF-8");
// 走 kylin /inner/**(鉴权白名单),避免 C 端 sendOrderRefunds 无登录态被拒
String resp = HttpUtil.post(kylinUrl + "/inner/orderRefund/systemFullRefund", params, headers);
log.info("拼团自动退款返回 orderId={}, entityId={}, resp={}", orderId, orderTicketEntitiesId, resp);
// ResponseDto 成功码为 "0"
if (resp == null || !(resp.contains("\"code\":\"0\"") || resp.contains("\"code\": \"0\""))) {
log.error("拼团自动退款业务失败 orderId={}, entityId={}, resp={}", orderId, orderTicketEntitiesId, resp);
}
} catch (Exception e) {
log.error("拼团自动退款失败 orderId={}, entityId={}", orderId, orderTicketEntitiesId, e);
}
}
private String now() {
return DateUtil.Formatter.yyyyMMddHHmmss.format(LocalDateTime.now());
}
}
......@@ -22,6 +22,7 @@ import com.liquidnet.service.kylin.mapper.KylinOrderTicketStatusMapper;
import com.liquidnet.service.kylin.mapper.KylinOrderTicketsMapper;
import com.liquidnet.service.kylin.service.IKylinOrderRefundsService;
import com.liquidnet.service.platform.utils.DataUtils;
import com.liquidnet.service.platform.utils.GroupBuyPlatformHelper;
import com.mongodb.BasicDBObject;
import com.mongodb.client.result.UpdateResult;
import lombok.extern.slf4j.Slf4j;
......@@ -80,6 +81,8 @@ public class OrderRefundsCallbackServiceImpl extends ServiceImpl<KylinOrderRefun
private MongoConverter mongoConverter;
@Autowired
private DataUtils dataUtils;
@Autowired
private GroupBuyPlatformHelper groupBuyPlatformHelper;
public String refundCallback(RefundCallbackParam refundCallbackParam) {
log.info("refundCallback订单退款回调参数: [RefundCallbackParam={}]", refundCallbackParam);
......@@ -112,6 +115,11 @@ public class OrderRefundsCallbackServiceImpl extends ServiceImpl<KylinOrderRefun
if (1 == status) { // 退款成功
boolean res = kylinRefundsStatusServiceImpl.orderTicketRefunded(refundCallbackParam, refundInfo);
if (res) {
try {
groupBuyPlatformHelper.onTicketRefunded(refundInfo.getOrderTicketsId());
} catch (Exception e) {
log.error("拼团退款回写异常 orderTicketsId={}", refundInfo.getOrderTicketsId(), e);
}
return "success";
} else {
return "fail";
......
package com.liquidnet.service.platform.utils;
import com.liquidnet.common.cache.redis.util.RedisDataSourceUtil;
import com.liquidnet.commons.lang.util.CollectionUtil;
import com.liquidnet.commons.lang.util.DateUtil;
import com.liquidnet.service.base.SqlMapping;
import com.liquidnet.service.base.constant.MQConst;
import com.liquidnet.service.base.constant.RedisKeyExpireConst;
import com.liquidnet.service.kylin.constant.GroupBuyStatusConst;
import com.liquidnet.service.kylin.constant.KylinRedisConst;
import com.liquidnet.service.kylin.dto.vo.groupbuy.KylinGroupBuyMemberVo;
import com.liquidnet.service.kylin.dto.vo.groupbuy.KylinGroupBuyOrderVo;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import java.time.LocalDateTime;
import java.util.LinkedList;
import java.util.function.Consumer;
/**
* platform 侧拼团:锁、退款回写、Mongo/Redis/MySQL 快照同步。
*/
@Component
@Slf4j
public class GroupBuyPlatformHelper {
private static final int LOCK_SECONDS = 15;
@Autowired
private MongoTemplate mongoTemplate;
@Autowired
private RedisDataSourceUtil redisDataSourceUtil;
@Autowired
private QueueUtils queueUtils;
public boolean tryLock(String groupOrderId) {
if (StringUtils.isBlank(groupOrderId)) {
return false;
}
return redisDataSourceUtil.getRedisKylinUtil().lock(KylinRedisConst.GROUP_BUY_ORDER_LOCK.concat(groupOrderId), 1, LOCK_SECONDS);
}
public void unlock(String groupOrderId) {
if (StringUtils.isBlank(groupOrderId)) {
return;
}
redisDataSourceUtil.getRedisKylinUtil().uLock(KylinRedisConst.GROUP_BUY_ORDER_LOCK.concat(groupOrderId));
}
public void withGroupLock(String groupOrderId, Runnable action) {
if (!tryLock(groupOrderId)) {
throw new IllegalStateException("拼团处理中,请稍后重试");
}
try {
action.run();
} finally {
unlock(groupOrderId);
}
}
public boolean withGroupLockQuiet(String groupOrderId, Consumer<KylinGroupBuyOrderVo> action) {
if (!tryLock(groupOrderId)) {
log.warn("拼团锁获取失败 groupOrderId={}", groupOrderId);
return false;
}
try {
KylinGroupBuyOrderVo group = loadFromMongo(groupOrderId);
if (group == null) {
return false;
}
action.accept(group);
return true;
} finally {
unlock(groupOrderId);
}
}
/**
* 票单退款成功:JOINED → REFUNDED;PENDING 团则 count--。幂等。
*/
public void onTicketRefunded(String orderTicketsId) {
if (StringUtils.isBlank(orderTicketsId)) {
return;
}
KylinGroupBuyOrderVo group = mongoTemplate.findOne(
Query.query(Criteria.where("members.orderTicketsId").is(orderTicketsId)),
KylinGroupBuyOrderVo.class, KylinGroupBuyOrderVo.class.getSimpleName());
if (group == null || CollectionUtils.isEmpty(group.getMembers())) {
return;
}
String groupOrderId = group.getGroupOrderId();
if (!tryLock(groupOrderId)) {
log.warn("退款回写拼团获取锁失败 orderTicketsId={}, groupOrderId={}", orderTicketsId, groupOrderId);
return;
}
try {
group = loadFromMongo(groupOrderId);
if (group == null || CollectionUtils.isEmpty(group.getMembers())) {
return;
}
KylinGroupBuyMemberVo member = null;
for (KylinGroupBuyMemberVo m : group.getMembers()) {
if (orderTicketsId.equals(m.getOrderTicketsId())) {
member = m;
break;
}
}
if (member == null) {
return;
}
if (GroupBuyStatusConst.MemberStatus.REFUNDED.equals(member.getStatus())
|| GroupBuyStatusConst.MemberStatus.CANCELLED.equals(member.getStatus())) {
return;
}
// 仅已支付成员回写;未支付占坑由关单/散团处理
if (!GroupBuyStatusConst.MemberStatus.JOINED.equals(member.getStatus())) {
return;
}
member.setStatus(GroupBuyStatusConst.MemberStatus.REFUNDED);
if (GroupBuyStatusConst.OrderStatus.PENDING.equals(group.getStatus())) {
int count = group.getCurrentCount() == null ? 0 : group.getCurrentCount();
group.setCurrentCount(Math.max(0, count - 1));
}
group.setUpdatedAt(now());
saveGroup(group);
log.info("拼团退款回写完成 orderTicketsId={}, groupOrderId={}, groupStatus={}, currentCount={}",
orderTicketsId, groupOrderId, group.getStatus(), group.getCurrentCount());
} catch (Exception e) {
log.error("拼团退款回写失败 orderTicketsId={}", orderTicketsId, e);
} finally {
unlock(groupOrderId);
}
}
public KylinGroupBuyOrderVo loadFromMongo(String groupOrderId) {
if (StringUtils.isBlank(groupOrderId)) {
return null;
}
return mongoTemplate.findOne(
Query.query(Criteria.where("groupOrderId").is(groupOrderId)),
KylinGroupBuyOrderVo.class, KylinGroupBuyOrderVo.class.getSimpleName());
}
public void saveGroup(KylinGroupBuyOrderVo group) {
if (group == null) {
return;
}
clearCache(group);
mongoTemplate.remove(Query.query(Criteria.where("groupOrderId").is(group.getGroupOrderId())),
KylinGroupBuyOrderVo.class, KylinGroupBuyOrderVo.class.getSimpleName());
mongoTemplate.insert(group, KylinGroupBuyOrderVo.class.getSimpleName());
cache(group);
sendReplaceSql(group);
}
public void cache(KylinGroupBuyOrderVo group) {
if (group == null) {
return;
}
redisDataSourceUtil.getRedisKylinUtil().set(KylinRedisConst.GROUP_BUY_ORDER.concat(group.getGroupOrderId()), group,
RedisKeyExpireConst.KYLIN_GROUP_BUY_EXPIRE);
if (StringUtils.isNotBlank(group.getShareToken())) {
redisDataSourceUtil.getRedisKylinUtil().set(KylinRedisConst.GROUP_BUY_ORDER_SHARE.concat(group.getShareToken()), group,
RedisKeyExpireConst.KYLIN_GROUP_BUY_EXPIRE);
}
}
public void clearCache(KylinGroupBuyOrderVo group) {
if (group == null) {
return;
}
redisDataSourceUtil.getRedisKylinUtil().del(KylinRedisConst.GROUP_BUY_ORDER.concat(group.getGroupOrderId()));
if (StringUtils.isNotBlank(group.getShareToken())) {
redisDataSourceUtil.getRedisKylinUtil().del(KylinRedisConst.GROUP_BUY_ORDER_SHARE.concat(group.getShareToken()));
}
}
public void sendReplaceSql(KylinGroupBuyOrderVo group) {
LinkedList<String> sqls = CollectionUtil.linkedListString();
LinkedList<Object[]> od = CollectionUtil.linkedListObjectArr();
LinkedList<Object[]> oi = CollectionUtil.linkedListObjectArr();
LinkedList<Object[]> md = CollectionUtil.linkedListObjectArr();
LinkedList<Object[]> mi = CollectionUtil.linkedListObjectArr();
sqls.add(SqlMapping.get("kylin_group_buy_order.delete"));
od.add(new Object[]{group.getGroupOrderId()});
sqls.add(SqlMapping.get("kylin_group_buy_order.insert"));
oi.add(orderInsertObject(group));
sqls.add(SqlMapping.get("kylin_group_buy_member.delete_by_group"));
md.add(new Object[]{group.getGroupOrderId()});
if (!CollectionUtils.isEmpty(group.getMembers())) {
sqls.add(SqlMapping.get("kylin_group_buy_member.insert"));
for (KylinGroupBuyMemberVo m : group.getMembers()) {
mi.add(memberInsertObject(m));
}
queueUtils.sendMsgByRedis(MQConst.KylinQueue.SQL_ORDER_OVERTIME_REFUND.getKey(),
SqlMapping.gets(sqls, od, oi, md, mi));
} else {
queueUtils.sendMsgByRedis(MQConst.KylinQueue.SQL_ORDER_OVERTIME_REFUND.getKey(),
SqlMapping.gets(sqls, od, oi, md));
}
}
private Object[] orderInsertObject(KylinGroupBuyOrderVo vo) {
return new Object[]{
vo.getGroupOrderId(), vo.getActivityId(), vo.getLeaderUserId(), vo.getOrderTicketsId(),
vo.getTargetCount(), vo.getCurrentCount(), vo.getStatus(), vo.getIsPublic(), vo.getShareToken(),
vo.getExpiresAt() == null ? null : DateUtil.Formatter.yyyyMMddHHmmss.parse(vo.getExpiresAt()),
DateUtil.Formatter.yyyyMMddHHmmss.parse(vo.getCreatedAt()),
DateUtil.Formatter.yyyyMMddHHmmss.parse(vo.getUpdatedAt())
};
}
private Object[] memberInsertObject(KylinGroupBuyMemberVo vo) {
return new Object[]{
vo.getMemberId(), vo.getGroupOrderId(), vo.getUserId(), vo.getOrderTicketsId(),
vo.getTicketCount(), vo.getStatus(),
DateUtil.Formatter.yyyyMMddHHmmss.parse(vo.getJoinTime()),
DateUtil.Formatter.yyyyMMddHHmmss.parse(vo.getCreatedAt())
};
}
private String now() {
return DateUtil.Formatter.yyyyMMddHHmmss.format(LocalDateTime.now());
}
}
package com.liquidnet.service.platform.utils;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.liquidnet.common.cache.redis.util.RedisDataSourceUtil;
import com.liquidnet.common.sms.constant.SmsEnum;
import com.liquidnet.commons.lang.util.DateUtil;
import com.liquidnet.commons.lang.util.JsonUtils;
import com.liquidnet.service.base.SmsMessage;
import com.liquidnet.service.base.constant.MQConst;
import com.liquidnet.service.base.constant.RedisKeyExpireConst;
import com.liquidnet.service.kylin.constant.GroupBuyStatusConst;
import com.liquidnet.service.kylin.constant.KylinRedisConst;
import com.liquidnet.service.kylin.dto.vo.groupbuy.KylinGroupBuyMemberVo;
import com.liquidnet.service.kylin.dto.vo.groupbuy.KylinGroupBuyOrderVo;
import com.liquidnet.service.kylin.dto.vo.mongo.KylinOrderTicketVo;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* 拼团失败 / 1h 提醒短信:入队 {@link MQConst.KylinQueue#SMS_NOTICE}(与购票短信一致,异步)。
*/
@Component
@Slf4j
public class GroupBuySmsSender {
@Autowired
private QueueUtils queueUtils;
@Autowired
private RedisDataSourceUtil redisDataSourceUtil;
@Autowired
private MongoTemplate mongoTemplate;
public void notifyFailed(KylinGroupBuyOrderVo group) {
if (group == null || !isSmsEnabled()) {
return;
}
List<String> phones = collectPhones(group, true, false);
if (phones.isEmpty()) {
log.info("拼团失败短信无有效手机号 groupOrderId={}", group.getGroupOrderId());
return;
}
ObjectNode params = JsonUtils.OM().createObjectNode();
params.put("hours", resolveHours(group));
for (String phone : phones) {
enqueueSms(phone, SmsEnum.ADTemplate.SMS_510615071.name(), params,
group.getGroupOrderId(), "failed");
}
}
public void notifyRemind1h(KylinGroupBuyOrderVo group) {
if (group == null || !isSmsEnabled()) {
return;
}
List<String> phones = collectPhones(group, false, true);
if (phones.isEmpty()) {
log.info("拼团1h提醒短信无有效手机号 groupOrderId={}", group.getGroupOrderId());
return;
}
ObjectNode empty = JsonUtils.OM().createObjectNode();
for (String phone : phones) {
enqueueSms(phone, SmsEnum.ADTemplate.SMS_510285138.name(), empty,
group.getGroupOrderId(), "remind1h");
}
redisDataSourceUtil.getRedisKylinUtil().set(KylinRedisConst.GROUP_BUY_REMIND_1H.concat(group.getGroupOrderId()), "1",
RedisKeyExpireConst.KYLIN_GROUP_BUY_EXPIRE);
}
public boolean isRemind1hAlreadySent(KylinGroupBuyOrderVo group) {
if (group == null) {
return true;
}
if (Integer.valueOf(1).equals(group.getRemind1hSent())) {
return true;
}
Object v = redisDataSourceUtil.getRedisKylinUtil().get(KylinRedisConst.GROUP_BUY_REMIND_1H.concat(group.getGroupOrderId()));
return v != null && ("1".equals(String.valueOf(v)) || "true".equalsIgnoreCase(String.valueOf(v)));
}
private boolean isSmsEnabled() {
Object v = redisDataSourceUtil.getRedisKylinUtil().get(KylinRedisConst.GROUP_BUY_SMS_ENABLED);
if (v == null) {
return true;
}
String s = String.valueOf(v).trim();
return !("0".equals(s) || "false".equalsIgnoreCase(s));
}
private List<String> collectPhones(KylinGroupBuyOrderVo group, boolean joinedOnly, boolean includePendingPay) {
Set<String> phones = new HashSet<>();
if (group == null || CollectionUtils.isEmpty(group.getMembers())) {
return new ArrayList<>();
}
for (KylinGroupBuyMemberVo m : group.getMembers()) {
if (m == null || StringUtils.isBlank(m.getOrderTicketsId())) {
continue;
}
if (m.getOrderTicketsId().startsWith("ROBOT_")) {
continue;
}
Integer st = m.getStatus();
boolean ok;
if (joinedOnly) {
ok = GroupBuyStatusConst.MemberStatus.JOINED.equals(st);
} else {
ok = GroupBuyStatusConst.MemberStatus.JOINED.equals(st)
|| (includePendingPay && GroupBuyStatusConst.MemberStatus.PENDING_PAY.equals(st));
}
if (!ok) {
continue;
}
String mobile = resolveMobile(m.getOrderTicketsId());
if (StringUtils.isNotBlank(mobile)) {
phones.add(mobile.trim());
}
}
return new ArrayList<>(phones);
}
private String resolveMobile(String orderTicketsId) {
Object cached = redisDataSourceUtil.getRedisKylinUtil().get(KylinRedisConst.ORDER.concat(orderTicketsId));
if (cached instanceof KylinOrderTicketVo) {
return ((KylinOrderTicketVo) cached).getUserMobile();
}
KylinOrderTicketVo ticket = mongoTemplate.findOne(
Query.query(Criteria.where("orderTicketsId").is(orderTicketsId)),
KylinOrderTicketVo.class, KylinOrderTicketVo.class.getSimpleName());
return ticket == null ? null : ticket.getUserMobile();
}
private String resolveHours(KylinGroupBuyOrderVo group) {
try {
if (StringUtils.isNotBlank(group.getCreatedAt()) && StringUtils.isNotBlank(group.getExpiresAt())) {
LocalDateTime created = DateUtil.Formatter.yyyyMMddHHmmss.parse(group.getCreatedAt());
LocalDateTime expires = DateUtil.Formatter.yyyyMMddHHmmss.parse(group.getExpiresAt());
long hours = Duration.between(created, expires).toHours();
if (hours < 1) {
hours = 1;
}
return String.valueOf(hours);
}
} catch (Exception e) {
log.warn("拼团失败短信 hours 解析失败 groupOrderId={}", group.getGroupOrderId(), e);
}
return "24";
}
private void enqueueSms(String phone, String templateCode, ObjectNode params, String groupOrderId, String scene) {
try {
SmsMessage msg = SmsMessage.builder()
.setPhone(phone)
.setSignName(SmsEnum.ADSignName.M02.getVal())
.setTemplateCode(templateCode);
if (params == null || params.size() == 0) {
msg.setEmptyTemplateParam();
} else {
Iterator<Map.Entry<String, JsonNode>> it = params.fields();
while (it.hasNext()) {
Map.Entry<String, JsonNode> e = it.next();
msg.setTemplateParam(e.getKey(), e.getValue() == null ? "" : e.getValue().asText(""));
}
}
queueUtils.sendMsgByRedis(MQConst.KylinQueue.SMS_NOTICE.getKey(), msg.toJson());
} catch (Exception e) {
log.error("拼团短信入队异常 scene={} groupOrderId={} phone={}", scene, groupOrderId, phone, e);
}
}
}
......@@ -4,3 +4,11 @@ goblin_order.close.sku=UPDATE goblin_order_sku SET status = ? ,updated_at = ? WH
# ------------------------????----------------------------
kylin_camera_record.insert=INSERT INTO kylin_camera_record (camera_record_id, camera_id, person_num) VALUES (?,?,?)
# ------------------------演出票拼团----------------------------
kylin_group_buy_order.update_status=UPDATE kylin_group_buy_order SET status = ?, updated_at = ? WHERE group_order_id = ? AND status = ?
kylin_group_buy_activity.update_status=UPDATE kylin_group_buy_activity SET status = ?, updated_at = ? WHERE activity_id = ? AND status = ?
kylin_group_buy_order.delete=DELETE FROM kylin_group_buy_order WHERE group_order_id = ?
kylin_group_buy_order.insert=INSERT INTO kylin_group_buy_order (group_order_id,activity_id,leader_user_id,order_tickets_id,target_count,current_count,status,is_public,share_token,expires_at,created_at,updated_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?)
kylin_group_buy_member.delete_by_group=DELETE FROM kylin_group_buy_member WHERE group_order_id = ?
kylin_group_buy_member.insert=INSERT INTO kylin_group_buy_member (member_id,group_order_id,user_id,order_tickets_id,ticket_count,status,join_time,created_at) VALUES (?,?,?,?,?,?,?,?)
......@@ -50,6 +50,10 @@
<version>1.0-SNAPSHOT</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
</dependency>
</dependencies>
<build>
......
package com.liquidnet.service.slime.controller;
import com.github.pagehelper.PageInfo;
import com.liquidnet.service.base.ResponseDto;
import com.liquidnet.service.kylin.dto.param.KylinGroupBuyActivityCreateParam;
import com.liquidnet.service.kylin.dto.param.KylinGroupBuyActivityUpdateParam;
import com.liquidnet.service.kylin.dto.vo.groupbuy.KylinGroupBuyActivityVo;
import com.liquidnet.service.kylin.dto.vo.groupbuy.KylinGroupBuyPartnerOrderDetailVo;
import com.liquidnet.service.kylin.dto.vo.groupbuy.KylinGroupBuyPartnerOrderListVo;
import com.liquidnet.service.kylin.dto.vo.groupbuy.KylinGroupBuyPartnerOrderStatsVo;
import com.liquidnet.service.kylin.service.partner.IKylinGroupBuyPartnerService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletResponse;
import javax.validation.Valid;
import javax.validation.constraints.NotNull;
import java.util.Map;
@Api(tags = "第三方-拼团", position = 1005)
@RestController
@RequestMapping("partner/groupBuy")
@Validated
public class KylinGroupBuyPartnerController {
@Autowired
private IKylinGroupBuyPartnerService groupBuyPartnerService;
@GetMapping("")
@ApiOperation(value = "拼团活动列表", position = 1)
@ApiResponse(response = KylinGroupBuyActivityVo.class, code = 200, message = "接口返回对象参数")
public ResponseDto<PageInfo<KylinGroupBuyActivityVo>> list(
@RequestParam(value = "name", required = false) String name,
@RequestParam(value = "status", required = false) Integer status,
@RequestParam(value = "page", required = false, defaultValue = "1") Integer page,
@RequestParam(value = "size", required = false, defaultValue = "20") Integer size) {
return groupBuyPartnerService.list(name, status, page, size);
}
@GetMapping("/stats")
@ApiOperation(value = "拼团活动统计", position = 8)
public ResponseDto<Map<String, Long>> stats() {
return groupBuyPartnerService.stats();
}
@GetMapping("/details")
@ApiOperation(value = "拼团活动详情", position = 2)
public ResponseDto<KylinGroupBuyActivityVo> detail(
@RequestParam("activityId") @NotNull(message = "活动ID不能为空") String activityId) {
return groupBuyPartnerService.detail(activityId);
}
@PostMapping("")
@ApiOperation(value = "创建拼团活动", position = 3)
public ResponseDto<KylinGroupBuyActivityVo> create(@RequestBody @Valid KylinGroupBuyActivityCreateParam param) {
return groupBuyPartnerService.create(param);
}
@PutMapping("")
@ApiOperation(value = "编辑拼团活动", position = 4)
public ResponseDto<KylinGroupBuyActivityVo> update(@RequestBody @Valid KylinGroupBuyActivityUpdateParam param) {
return groupBuyPartnerService.update(param);
}
@DeleteMapping("")
@ApiOperation(value = "删除拼团活动", position = 5)
public ResponseDto<Boolean> delete(@RequestParam("activityId") @NotNull(message = "活动ID不能为空") String activityId) {
return groupBuyPartnerService.delete(activityId);
}
@PostMapping("/toggle")
@ApiOperation(value = "启停拼团活动", position = 6)
public ResponseDto<Boolean> toggle(@RequestParam("activityId") @NotNull(message = "活动ID不能为空") String activityId,
@RequestParam("targetStatus") @NotNull(message = "目标状态不能为空") Integer targetStatus) {
return groupBuyPartnerService.toggle(activityId, targetStatus);
}
@GetMapping("/orders/stats")
@ApiOperation(value = "拼团订单统计", position = 10)
@ApiResponse(response = KylinGroupBuyPartnerOrderStatsVo.class, code = 200, message = "接口返回对象参数")
public ResponseDto<KylinGroupBuyPartnerOrderStatsVo> orderStats() {
return groupBuyPartnerService.orderStats();
}
@GetMapping("/orders")
@ApiOperation(value = "拼团订单列表", position = 11)
@ApiResponse(response = KylinGroupBuyPartnerOrderListVo.class, code = 200, message = "接口返回对象参数")
public ResponseDto<PageInfo<KylinGroupBuyPartnerOrderListVo>> orderList(
@RequestParam(value = "keyword", required = false) String keyword,
@RequestParam(value = "status", required = false) Integer status,
@RequestParam(value = "page", required = false, defaultValue = "1") Integer page,
@RequestParam(value = "size", required = false, defaultValue = "20") Integer size) {
return groupBuyPartnerService.orderList(keyword, status, page, size);
}
@GetMapping("/orders/details")
@ApiOperation(value = "拼团订单详情", position = 12)
@ApiResponse(response = KylinGroupBuyPartnerOrderDetailVo.class, code = 200, message = "接口返回对象参数")
public ResponseDto<KylinGroupBuyPartnerOrderDetailVo> orderDetail(
@RequestParam("groupOrderId") @NotNull(message = "拼团订单号不能为空") String groupOrderId) {
return groupBuyPartnerService.orderDetail(groupOrderId);
}
@GetMapping("/orders/export")
@ApiOperation(value = "导出拼团订单", position = 13)
public void exportOrders(
@RequestParam(value = "keyword", required = false) String keyword,
@RequestParam(value = "status", required = false) Integer status,
HttpServletResponse response) {
groupBuyPartnerService.exportOrders(keyword, status, response);
}
}
package com.liquidnet.service.slime.service.impl;
import com.github.pagehelper.PageInfo;
import com.liquidnet.common.cache.redis.util.RedisUtil;
import com.liquidnet.commons.lang.util.CollectionUtil;
import com.liquidnet.commons.lang.util.DateUtil;
import com.liquidnet.commons.lang.util.IDGenerator;
import com.liquidnet.service.base.ResponseDto;
import com.liquidnet.service.base.SqlMapping;
import com.liquidnet.service.base.constant.MQConst;
import com.liquidnet.service.base.constant.RedisKeyExpireConst;
import com.liquidnet.service.kylin.constant.GroupBuyStatusConst;
import com.liquidnet.service.kylin.constant.KylinRedisConst;
import com.liquidnet.service.kylin.dto.param.KylinGroupBuyActivityCreateParam;
import com.liquidnet.service.kylin.dto.param.KylinGroupBuyActivityUpdateParam;
import com.liquidnet.service.kylin.dto.vo.groupbuy.KylinGroupBuyActivityVo;
import com.liquidnet.service.kylin.dto.vo.groupbuy.KylinGroupBuyDiscountVo;
import com.liquidnet.service.kylin.dto.vo.groupbuy.KylinGroupBuyMemberVo;
import com.liquidnet.service.kylin.dto.vo.groupbuy.KylinGroupBuyOrderVo;
import com.liquidnet.service.kylin.dto.vo.groupbuy.KylinGroupBuyPartnerOrderDetailVo;
import com.liquidnet.service.kylin.dto.vo.groupbuy.KylinGroupBuyPartnerOrderListVo;
import com.liquidnet.service.kylin.dto.vo.groupbuy.KylinGroupBuyPartnerOrderMemberVo;
import com.liquidnet.service.kylin.dto.vo.groupbuy.KylinGroupBuyPartnerOrderStatsVo;
import com.liquidnet.service.kylin.dto.vo.middle.KylinTicketTimesVo;
import com.liquidnet.service.kylin.dto.vo.middle.KylinTicketVo;
import com.liquidnet.service.kylin.dto.vo.mongo.KylinOrderTicketVo;
import com.liquidnet.service.kylin.dto.vo.mongo.KylinPerformanceVo;
import com.liquidnet.service.kylin.service.partner.IKylinGroupBuyPartnerService;
import com.liquidnet.service.slime.util.GroupBuyOrderExcelUtil;
import com.liquidnet.service.slime.util.ObjectUtil;
import com.liquidnet.service.slime.util.QueueUtil;
import com.liquidnet.service.slime.util.RedisSlimeUtils;
import com.mongodb.BasicDBObject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Sort;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import javax.servlet.http.HttpServletResponse;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
@Service
public class KylinGroupBuyPartnerServiceImpl implements IKylinGroupBuyPartnerService {
@Autowired
private MongoTemplate mongoTemplate;
@Autowired
private RedisSlimeUtils redisSlimeUtils;
@Autowired
private RedisUtil redisUtil;
@Autowired
private QueueUtil queueUtil;
@Override
public ResponseDto<PageInfo<KylinGroupBuyActivityVo>> list(String name, Integer status, Integer page, Integer size) {
Criteria criteria = new Criteria();
if (status != null) {
criteria.and("status").is(status);
}
if (name != null && !name.trim().isEmpty()) {
criteria.and("name").regex(".*" + name.trim() + ".*", "i");
}
Query countQuery = Query.query(criteria);
long total = mongoTemplate.count(countQuery, KylinGroupBuyActivityVo.class, KylinGroupBuyActivityVo.class.getSimpleName());
Query dataQuery = Query.query(criteria).with(Sort.by(Sort.Direction.DESC, "createdAt"));
if (page != null && size != null && page > 0 && size > 0) {
dataQuery.skip((long) (page - 1) * size).limit(size);
}
List<KylinGroupBuyActivityVo> list = mongoTemplate.find(dataQuery,
KylinGroupBuyActivityVo.class, KylinGroupBuyActivityVo.class.getSimpleName());
if (!list.isEmpty()) {
List<String> activityIds = list.stream().map(KylinGroupBuyActivityVo::getActivityId).collect(Collectors.toList());
Map<String, Long> orderCountMap = new HashMap<>();
for (String activityId : activityIds) {
long count = mongoTemplate.count(Query.query(Criteria.where("activityId").is(activityId)),
KylinGroupBuyOrderVo.class.getSimpleName());
orderCountMap.put(activityId, count);
}
for (KylinGroupBuyActivityVo vo : list) {
vo.setOrderCount(orderCountMap.getOrDefault(vo.getActivityId(), 0L));
KylinPerformanceVo performanceVo = redisSlimeUtils.getPerformanceVo(vo.getPerformanceId());
vo.setPerformanceName(performanceVo != null ? performanceVo.getTitle() : null);
}
}
PageInfo<KylinGroupBuyActivityVo> pageInfo = new PageInfo<>(list);
pageInfo.setTotal(total);
return ResponseDto.success(pageInfo);
}
@Override
public ResponseDto<Map<String, Long>> stats() {
Map<String, Long> result = new HashMap<>();
String collection = KylinGroupBuyActivityVo.class.getSimpleName();
result.put("totalActivities", mongoTemplate.count(Query.query(new Criteria()), collection));
result.put("activeCount", mongoTemplate.count(
Query.query(Criteria.where("status").is(GroupBuyStatusConst.ActivityStatus.ACTIVE)), collection));
result.put("endedCount", mongoTemplate.count(
Query.query(Criteria.where("status").is(GroupBuyStatusConst.ActivityStatus.ENDED)), collection));
result.put("orderCount", mongoTemplate.count(Query.query(new Criteria()),
KylinGroupBuyOrderVo.class.getSimpleName()));
return ResponseDto.success(result);
}
@Override
public ResponseDto<KylinGroupBuyActivityVo> detail(String activityId) {
KylinGroupBuyActivityVo vo = findActivity(activityId);
return vo == null ? ResponseDto.failure("拼团活动不存在") : ResponseDto.success(vo);
}
@Override
public ResponseDto<KylinGroupBuyActivityVo> create(KylinGroupBuyActivityCreateParam param) {
ResponseDto<Boolean> validate = validateParam(param);
if (!validate.isSuccess()) {
return ResponseDto.failure(validate.getMessage());
}
String now = now();
KylinGroupBuyActivityVo vo = buildActivity(param, now);
vo.setActivityId(IDGenerator.nextSnowId());
vo.setCreatedAt(now);
vo.setUpdatedAt(now);
vo.setDiscounts(buildDiscounts(param, vo.getActivityId(), now));
mongoTemplate.insert(vo, KylinGroupBuyActivityVo.class.getSimpleName());
sendReplaceActivitySql(vo);
cacheActivity(vo);
return ResponseDto.success();
}
@Override
public ResponseDto<KylinGroupBuyActivityVo> update(KylinGroupBuyActivityUpdateParam param) {
KylinGroupBuyActivityVo old = findActivity(param.getActivityId());
if (old == null) {
return ResponseDto.failure("拼团活动不存在");
}
ResponseDto<Boolean> validate = validateParam(param);
if (!validate.isSuccess()) {
return ResponseDto.failure(validate.getMessage());
}
String now = now();
KylinGroupBuyActivityVo vo = buildActivity(param, now);
vo.setActivityId(old.getActivityId());
vo.setStatus(old.getStatus());
vo.setCreatedAt(old.getCreatedAt());
vo.setUpdatedAt(now);
vo.setDiscounts(buildDiscounts(param, old.getActivityId(), now));
replaceActivity(vo);
sendReplaceActivitySql(vo);
return ResponseDto.success();
}
@Override
public ResponseDto<Boolean> delete(String activityId) {
KylinGroupBuyActivityVo old = findActivity(activityId);
if (old == null) {
return ResponseDto.failure("拼团活动不存在");
}
if (GroupBuyStatusConst.ActivityStatus.ACTIVE.equals(old.getStatus())) {
return ResponseDto.failure("进行中的拼团活动不可删除");
}
mongoTemplate.remove(Query.query(Criteria.where("activityId").is(activityId)),
KylinGroupBuyActivityVo.class, KylinGroupBuyActivityVo.class.getSimpleName());
sendDeleteActivitySql(activityId);
clearActivityCache(old);
return ResponseDto.success(true);
}
@Override
public ResponseDto<Boolean> toggle(String activityId, Integer targetStatus) {
KylinGroupBuyActivityVo old = findActivity(activityId);
if (old == null) {
return ResponseDto.failure("拼团活动不存在");
}
if (!GroupBuyStatusConst.ActivityStatus.ACTIVE.equals(targetStatus)
&& !GroupBuyStatusConst.ActivityStatus.CLOSED.equals(targetStatus)) {
return ResponseDto.failure("仅支持启用或关闭活动");
}
Integer fromStatus = old.getStatus();
if (GroupBuyStatusConst.ActivityStatus.ACTIVE.equals(targetStatus)
&& !GroupBuyStatusConst.ActivityStatus.PENDING.equals(fromStatus)
&& !GroupBuyStatusConst.ActivityStatus.CLOSED.equals(fromStatus)) {
return ResponseDto.failure("仅待启动或已关闭的活动可启用");
}
if (GroupBuyStatusConst.ActivityStatus.CLOSED.equals(targetStatus)
&& !GroupBuyStatusConst.ActivityStatus.ACTIVE.equals(fromStatus)) {
return ResponseDto.failure("仅进行中的活动可关闭");
}
old.setStatus(targetStatus);
old.setUpdatedAt(now());
replaceActivity(old);
sendUpdateActivityStatusSql(old.getActivityId(), fromStatus, targetStatus,
DateUtil.Formatter.yyyyMMddHHmmss.parse(old.getUpdatedAt()));
return ResponseDto.success(true);
}
private static final int EXPORT_MAX_SIZE = 10000;
@Override
public ResponseDto<KylinGroupBuyPartnerOrderStatsVo> orderStats() {
String collection = KylinGroupBuyOrderVo.class.getSimpleName();
KylinGroupBuyPartnerOrderStatsVo result = new KylinGroupBuyPartnerOrderStatsVo();
result.setTotal(mongoTemplate.count(Query.query(new Criteria()), collection));
result.setPending(mongoTemplate.count(
Query.query(Criteria.where("status").is(GroupBuyStatusConst.OrderStatus.PENDING)), collection));
result.setSuccess(mongoTemplate.count(
Query.query(Criteria.where("status").is(GroupBuyStatusConst.OrderStatus.SUCCESS)), collection));
result.setFailed(mongoTemplate.count(
Query.query(Criteria.where("status").is(GroupBuyStatusConst.OrderStatus.FAILED)), collection));
return ResponseDto.success(result);
}
@Override
public ResponseDto<PageInfo<KylinGroupBuyPartnerOrderListVo>> orderList(String keyword, Integer status,
Integer page, Integer size) {
ResponseDto<Boolean> statusCheck = validateOrderListStatus(status);
if (!statusCheck.isSuccess()) {
return ResponseDto.failure(statusCheck.getMessage());
}
Criteria criteria = buildOrderListCriteria(keyword, status);
String collection = KylinGroupBuyOrderVo.class.getSimpleName();
long total = mongoTemplate.count(Query.query(criteria), collection);
Query dataQuery = Query.query(criteria).with(Sort.by(Sort.Direction.DESC, "createdAt"));
int pageNo = page == null || page < 1 ? 1 : page;
int pageSize = size == null || size < 1 ? 20 : size;
dataQuery.skip((long) (pageNo - 1) * pageSize).limit(pageSize);
List<KylinGroupBuyOrderVo> orders = mongoTemplate.find(dataQuery, KylinGroupBuyOrderVo.class, collection);
List<KylinGroupBuyPartnerOrderListVo> list = toPartnerOrderListVos(orders);
PageInfo<KylinGroupBuyPartnerOrderListVo> pageInfo = new PageInfo<>(list);
pageInfo.setTotal(total);
pageInfo.setPageNum(pageNo);
pageInfo.setPageSize(pageSize);
return ResponseDto.success(pageInfo);
}
@Override
public ResponseDto<KylinGroupBuyPartnerOrderDetailVo> orderDetail(String groupOrderId) {
if (!StringUtils.hasText(groupOrderId)) {
return ResponseDto.failure("拼团订单号不能为空");
}
KylinGroupBuyOrderVo order = mongoTemplate.findOne(
Query.query(Criteria.where("groupOrderId").is(groupOrderId)),
KylinGroupBuyOrderVo.class, KylinGroupBuyOrderVo.class.getSimpleName());
if (order == null) {
return ResponseDto.failure("拼团订单不存在");
}
return ResponseDto.success(toPartnerOrderDetailVo(order));
}
@Override
public void exportOrders(String keyword, Integer status, HttpServletResponse response) {
ResponseDto<Boolean> statusCheck = validateOrderListStatus(status);
if (!statusCheck.isSuccess()) {
throw new IllegalArgumentException(statusCheck.getMessage());
}
Criteria criteria = buildOrderListCriteria(keyword, status);
Query query = Query.query(criteria).with(Sort.by(Sort.Direction.DESC, "createdAt")).limit(EXPORT_MAX_SIZE);
List<KylinGroupBuyOrderVo> orders = mongoTemplate.find(query,
KylinGroupBuyOrderVo.class, KylinGroupBuyOrderVo.class.getSimpleName());
List<KylinGroupBuyPartnerOrderListVo> rows = toPartnerOrderListVos(orders);
try {
GroupBuyOrderExcelUtil.export(rows, response);
} catch (Exception e) {
throw new IllegalStateException("导出拼团订单失败", e);
}
}
private ResponseDto<Boolean> validateOrderListStatus(Integer status) {
if (status == null) {
return ResponseDto.success(true);
}
if (GroupBuyStatusConst.OrderStatus.PENDING.equals(status)
|| GroupBuyStatusConst.OrderStatus.SUCCESS.equals(status)
|| GroupBuyStatusConst.OrderStatus.FAILED.equals(status)) {
return ResponseDto.success(true);
}
return ResponseDto.failure("状态仅支持:0拼团中、1拼团成功、2拼团失败");
}
private Criteria buildOrderListCriteria(String keyword, Integer status) {
List<Criteria> andList = new ArrayList<>();
if (status != null) {
andList.add(Criteria.where("status").is(status));
}
if (StringUtils.hasText(keyword)) {
String escaped = escapeRegex(keyword.trim());
andList.add(new Criteria().orOperator(
Criteria.where("groupOrderId").regex(escaped, "i"),
Criteria.where("members.nickname").regex(escaped, "i"),
Criteria.where("leaderUserId").regex(escaped, "i")
));
}
if (andList.isEmpty()) {
return new Criteria();
}
if (andList.size() == 1) {
return andList.get(0);
}
return new Criteria().andOperator(andList.toArray(new Criteria[0]));
}
private List<KylinGroupBuyPartnerOrderListVo> toPartnerOrderListVos(List<KylinGroupBuyOrderVo> orders) {
List<KylinGroupBuyPartnerOrderListVo> result = new ArrayList<>();
if (CollectionUtils.isEmpty(orders)) {
return result;
}
Map<String, String> activityNameMap = loadActivityNameMap(orders);
Map<String, BigDecimal> amountMap = loadLeaderAmountMap(orders);
for (KylinGroupBuyOrderVo order : orders) {
KylinGroupBuyPartnerOrderListVo vo = new KylinGroupBuyPartnerOrderListVo();
vo.setGroupOrderId(order.getGroupOrderId());
vo.setActivityName(activityNameMap.get(order.getActivityId()));
vo.setLeaderUserId(order.getLeaderUserId());
KylinGroupBuyMemberVo leader = findLeaderMember(order);
if (leader != null) {
vo.setLeaderNickname(leader.getNickname());
vo.setLeaderAvatar(leader.getAvatar());
}
vo.setCurrentCount(order.getCurrentCount());
vo.setTargetCount(order.getTargetCount());
vo.setOrderAmount(amountMap.get(order.getGroupOrderId()));
vo.setCreatedAt(order.getCreatedAt());
vo.setStatus(order.getStatus());
result.add(vo);
}
return result;
}
private KylinGroupBuyPartnerOrderDetailVo toPartnerOrderDetailVo(KylinGroupBuyOrderVo order) {
KylinGroupBuyPartnerOrderDetailVo vo = new KylinGroupBuyPartnerOrderDetailVo();
vo.setGroupOrderId(order.getGroupOrderId());
vo.setActivityId(order.getActivityId());
KylinGroupBuyActivityVo activity = findActivity(order.getActivityId());
vo.setActivityName(activity == null ? null : activity.getName());
vo.setStatus(order.getStatus());
vo.setOrderAmount(resolveLeaderAmount(order));
vo.setCreatedAt(order.getCreatedAt());
vo.setCurrentCount(order.getCurrentCount());
vo.setTargetCount(order.getTargetCount());
vo.setLeaderUserId(order.getLeaderUserId());
List<KylinGroupBuyPartnerOrderMemberVo> members = new ArrayList<>();
if (!CollectionUtils.isEmpty(order.getMembers())) {
for (KylinGroupBuyMemberVo m : order.getMembers()) {
KylinGroupBuyPartnerOrderMemberVo item = new KylinGroupBuyPartnerOrderMemberVo();
item.setUserId(m.getUserId());
item.setNickname(m.getNickname());
item.setAvatar(m.getAvatar());
item.setStatus(m.getStatus());
boolean isLeader = order.getLeaderUserId() != null && order.getLeaderUserId().equals(m.getUserId());
item.setIsLeader(isLeader ? 1 : 0);
members.add(item);
}
}
vo.setMembers(members);
KylinGroupBuyMemberVo leader = findLeaderMember(order);
if (leader != null) {
vo.setLeaderNickname(leader.getNickname());
vo.setLeaderAvatar(leader.getAvatar());
}
return vo;
}
private Map<String, String> loadActivityNameMap(List<KylinGroupBuyOrderVo> orders) {
Set<String> activityIds = new HashSet<>();
for (KylinGroupBuyOrderVo order : orders) {
if (StringUtils.hasText(order.getActivityId())) {
activityIds.add(order.getActivityId());
}
}
Map<String, String> map = new HashMap<>();
if (activityIds.isEmpty()) {
return map;
}
List<KylinGroupBuyActivityVo> activities = mongoTemplate.find(
Query.query(Criteria.where("activityId").in(activityIds)),
KylinGroupBuyActivityVo.class, KylinGroupBuyActivityVo.class.getSimpleName());
for (KylinGroupBuyActivityVo activity : activities) {
map.put(activity.getActivityId(), activity.getName());
}
return map;
}
private Map<String, BigDecimal> loadLeaderAmountMap(List<KylinGroupBuyOrderVo> orders) {
Map<String, String> groupToTicket = new HashMap<>();
Set<String> ticketIds = new HashSet<>();
for (KylinGroupBuyOrderVo order : orders) {
String ticketId = resolveLeaderOrderTicketsId(order);
if (StringUtils.hasText(ticketId)) {
groupToTicket.put(order.getGroupOrderId(), ticketId);
ticketIds.add(ticketId);
}
}
Map<String, BigDecimal> ticketAmount = new HashMap<>();
if (!ticketIds.isEmpty()) {
List<KylinOrderTicketVo> tickets = mongoTemplate.find(
Query.query(Criteria.where("orderTicketsId").in(ticketIds)),
KylinOrderTicketVo.class, KylinOrderTicketVo.class.getSimpleName());
for (KylinOrderTicketVo ticket : tickets) {
ticketAmount.put(ticket.getOrderTicketsId(), ticket.getPriceActual());
}
for (String ticketId : ticketIds) {
if (!ticketAmount.containsKey(ticketId)) {
Object cached = redisUtil.get(KylinRedisConst.ORDER.concat(ticketId));
if (cached instanceof KylinOrderTicketVo) {
ticketAmount.put(ticketId, ((KylinOrderTicketVo) cached).getPriceActual());
}
}
}
}
Map<String, BigDecimal> result = new HashMap<>();
for (Map.Entry<String, String> e : groupToTicket.entrySet()) {
result.put(e.getKey(), ticketAmount.get(e.getValue()));
}
return result;
}
private BigDecimal resolveLeaderAmount(KylinGroupBuyOrderVo order) {
String ticketId = resolveLeaderOrderTicketsId(order);
if (!StringUtils.hasText(ticketId)) {
return null;
}
Object cached = redisUtil.get(KylinRedisConst.ORDER.concat(ticketId));
if (cached instanceof KylinOrderTicketVo) {
return ((KylinOrderTicketVo) cached).getPriceActual();
}
KylinOrderTicketVo ticket = mongoTemplate.findOne(
Query.query(Criteria.where("orderTicketsId").is(ticketId)),
KylinOrderTicketVo.class, KylinOrderTicketVo.class.getSimpleName());
return ticket == null ? null : ticket.getPriceActual();
}
private String resolveLeaderOrderTicketsId(KylinGroupBuyOrderVo order) {
if (order == null) {
return null;
}
if (StringUtils.hasText(order.getOrderTicketsId())) {
return order.getOrderTicketsId();
}
KylinGroupBuyMemberVo leader = findLeaderMember(order);
return leader == null ? null : leader.getOrderTicketsId();
}
private KylinGroupBuyMemberVo findLeaderMember(KylinGroupBuyOrderVo order) {
if (order == null || !StringUtils.hasText(order.getLeaderUserId())
|| CollectionUtils.isEmpty(order.getMembers())) {
return null;
}
for (KylinGroupBuyMemberVo m : order.getMembers()) {
if (order.getLeaderUserId().equals(m.getUserId())) {
return m;
}
}
return null;
}
/** Mongo $regex 不支持 \\Q..\\E,手工转义特殊字符。 */
private static String escapeRegex(String input) {
return Pattern.compile("([\\\\.*+?^${}()|\\[\\]])").matcher(input).replaceAll("\\\\$1");
}
private ResponseDto<Boolean> validateParam(KylinGroupBuyActivityCreateParam param) {
LocalDateTime startTime;
LocalDateTime endTime;
try {
startTime = DateUtil.Formatter.yyyyMMddHHmmss.parse(param.getStartTime());
endTime = DateUtil.Formatter.yyyyMMddHHmmss.parse(param.getEndTime());
} catch (Exception e) {
return ResponseDto.failure("开始/结束时间格式错误,需为 yyyy-MM-dd HH:mm:ss");
}
if (endTime.isBefore(startTime) || endTime.isEqual(startTime)) {
return ResponseDto.failure("活动结束时间必须晚于开始时间");
}
if (GroupBuyStatusConst.LaunchType.SCHEDULED.equals(param.getLaunchType())) {
if (param.getScheduledTime() == null || param.getScheduledTime().trim().isEmpty()) {
return ResponseDto.failure("定时启动必须填写启动时间");
}
try {
DateUtil.Formatter.yyyyMMddHHmmss.parse(param.getScheduledTime());
} catch (Exception e) {
return ResponseDto.failure("定时启动时间格式错误,需为 yyyy-MM-dd HH:mm:ss");
}
}
KylinPerformanceVo performanceVo = redisSlimeUtils.getPerformanceVo(param.getPerformanceId());
if (performanceVo == null || CollectionUtils.isEmpty(performanceVo.getTicketTimeList())) {
return ResponseDto.failure("演出不存在");
}
if (param.getMinGroupSize() == null || param.getMinGroupSize() < 2) {
return ResponseDto.failure("成团人数不能小于2");
}
if (param.getMaxBuyPerUser() == null || param.getMaxBuyPerUser() < 1) {
return ResponseDto.failure("限购数量不能小于1");
}
if (!CollectionUtils.isEmpty(param.getDiscounts())) {
for (KylinGroupBuyActivityCreateParam.DiscountParam discount : param.getDiscounts()) {
if (!ticketExists(performanceVo, discount.getTimeId(), discount.getTicketId())) {
return ResponseDto.failure("优惠配置的场次或票档不存在");
}
if (discount.getDiscountValue() == null || discount.getDiscountValue().compareTo(BigDecimal.ZERO) <= 0) {
return ResponseDto.failure("优惠值必须大于0");
}
}
}
return ResponseDto.success(true);
}
private boolean ticketExists(KylinPerformanceVo performanceVo, String timeId, String ticketId) {
for (KylinTicketTimesVo timeVo : performanceVo.getTicketTimeList()) {
if (!timeId.equals(timeVo.getTimeId()) || CollectionUtils.isEmpty(timeVo.getTicketList())) {
continue;
}
for (KylinTicketVo ticketVo : timeVo.getTicketList()) {
if (ticketId.equals(ticketVo.getTicketsId())) {
return true;
}
}
}
return false;
}
private KylinGroupBuyActivityVo buildActivity(KylinGroupBuyActivityCreateParam param, String now) {
KylinGroupBuyActivityVo vo = new KylinGroupBuyActivityVo();
vo.setName(param.getName());
vo.setPerformanceId(param.getPerformanceId());
vo.setMinGroupSize(param.getMinGroupSize());
vo.setMaxBuyPerUser(param.getMaxBuyPerUser());
vo.setStartTime(param.getStartTime());
vo.setEndTime(param.getEndTime());
vo.setLaunchType(param.getLaunchType());
if (param.getScheduledTime() != null && !param.getScheduledTime().trim().isEmpty()) {
vo.setScheduledTime(param.getScheduledTime());
}
vo.setDescription(param.getDescription());
if (GroupBuyStatusConst.LaunchType.IMMEDIATE.equals(param.getLaunchType())) {
vo.setStatus(GroupBuyStatusConst.ActivityStatus.ACTIVE);
} else {
vo.setStatus(GroupBuyStatusConst.ActivityStatus.PENDING);
}
vo.setUpdatedAt(now);
return vo;
}
private List<KylinGroupBuyDiscountVo> buildDiscounts(KylinGroupBuyActivityCreateParam param, String activityId, String now) {
List<KylinGroupBuyDiscountVo> list = new ArrayList<>();
for (KylinGroupBuyActivityCreateParam.DiscountParam discountParam : param.getDiscounts()) {
KylinGroupBuyDiscountVo vo = new KylinGroupBuyDiscountVo();
vo.setDiscountId(IDGenerator.nextSnowId());
vo.setActivityId(activityId);
vo.setPerformanceId(param.getPerformanceId());
vo.setTimeId(discountParam.getTimeId());
vo.setTicketId(discountParam.getTicketId());
vo.setDiscountType(discountParam.getDiscountType());
vo.setDiscountValue(discountParam.getDiscountValue());
vo.setCreatedAt(now);
list.add(vo);
}
return list;
}
private KylinGroupBuyActivityVo findActivity(String activityId) {
Object obj = redisUtil.get(KylinRedisConst.GROUP_BUY_ACTIVITY.concat(activityId));
if (obj != null) {
return (KylinGroupBuyActivityVo) obj;
}
KylinGroupBuyActivityVo activity = mongoTemplate.findOne(Query.query(Criteria.where("activityId").is(activityId)),
KylinGroupBuyActivityVo.class, KylinGroupBuyActivityVo.class.getSimpleName());
cacheActivity(activity);
return activity;
}
private void replaceActivity(KylinGroupBuyActivityVo activity) {
clearActivityCache(activity);
Query query = Query.query(Criteria.where("activityId").is(activity.getActivityId()));
BasicDBObject updateObj = ObjectUtil.cloneBasicDBObject()
.append("$set", mongoTemplate.getConverter().convertToMongoType(activity));
mongoTemplate.getCollection(KylinGroupBuyActivityVo.class.getSimpleName())
.updateOne(query.getQueryObject(), updateObj);
cacheActivity(activity);
}
private void sendReplaceActivitySql(KylinGroupBuyActivityVo activity) {
LinkedList<String> sqls = CollectionUtil.linkedListString();
LinkedList<Object[]> activityDel = CollectionUtil.linkedListObjectArr();
LinkedList<Object[]> activityInsert = CollectionUtil.linkedListObjectArr();
LinkedList<Object[]> discountDel = CollectionUtil.linkedListObjectArr();
LinkedList<Object[]> discountInsert = CollectionUtil.linkedListObjectArr();
sqls.add(SqlMapping.get("kylin_group_buy_activity.delete"));
activityDel.add(new Object[]{activity.getActivityId()});
sqls.add(SqlMapping.get("kylin_group_buy_activity.insert"));
activityInsert.add(activityInsertObject(activity));
sqls.add(SqlMapping.get("kylin_group_buy_discount.delete_by_activity"));
discountDel.add(new Object[]{activity.getActivityId()});
if (!CollectionUtils.isEmpty(activity.getDiscounts())) {
sqls.add(SqlMapping.get("kylin_group_buy_discount.insert"));
for (KylinGroupBuyDiscountVo discount : activity.getDiscounts()) {
discountInsert.add(discountInsertObject(discount));
}
queueUtil.sendMsgByRedis(MQConst.SlimeQueue.SQL_SLIME_FIELD.getKey(),
SqlMapping.gets(sqls, activityDel, activityInsert, discountDel, discountInsert));
} else {
queueUtil.sendMsgByRedis(MQConst.SlimeQueue.SQL_SLIME_FIELD.getKey(),
SqlMapping.gets(sqls, activityDel, activityInsert, discountDel));
}
}
private void sendDeleteActivitySql(String activityId) {
LinkedList<String> sqls = CollectionUtil.linkedListString();
LinkedList<Object[]> activityDel = CollectionUtil.linkedListObjectArr();
LinkedList<Object[]> discountDel = CollectionUtil.linkedListObjectArr();
sqls.add(SqlMapping.get("kylin_group_buy_activity.delete"));
activityDel.add(new Object[]{activityId});
sqls.add(SqlMapping.get("kylin_group_buy_discount.delete_by_activity"));
discountDel.add(new Object[]{activityId});
queueUtil.sendMsgByRedis(MQConst.SlimeQueue.SQL_SLIME_FIELD.getKey(),
SqlMapping.gets(sqls, activityDel, discountDel));
}
private void sendUpdateActivityStatusSql(String activityId, Integer fromStatus, Integer toStatus, LocalDateTime updatedAt) {
queueUtil.sendMsgByRedis(MQConst.SlimeQueue.SQL_SLIME_FIELD.getKey(),
SqlMapping.get("kylin_group_buy_activity.update_status",
toStatus, updatedAt, activityId, fromStatus));
}
private Object[] activityInsertObject(KylinGroupBuyActivityVo vo) {
return new Object[]{
vo.getActivityId(), vo.getName(), vo.getPerformanceId(), vo.getMinGroupSize(), vo.getMaxBuyPerUser(),
DateUtil.Formatter.yyyyMMddHHmmss.parse(vo.getStartTime()),
DateUtil.Formatter.yyyyMMddHHmmss.parse(vo.getEndTime()),
vo.getLaunchType(),
vo.getScheduledTime() == null ? null : DateUtil.Formatter.yyyyMMddHHmmss.parse(vo.getScheduledTime()),
vo.getStatus(), vo.getDescription(),
DateUtil.Formatter.yyyyMMddHHmmss.parse(vo.getCreatedAt()),
DateUtil.Formatter.yyyyMMddHHmmss.parse(vo.getUpdatedAt())
};
}
private Object[] discountInsertObject(KylinGroupBuyDiscountVo vo) {
return new Object[]{
vo.getDiscountId(), vo.getActivityId(), vo.getPerformanceId(), vo.getTimeId(), vo.getTicketId(),
vo.getDiscountType(), vo.getDiscountValue(), DateUtil.Formatter.yyyyMMddHHmmss.parse(vo.getCreatedAt())
};
}
private void cacheActivity(KylinGroupBuyActivityVo activity) {
if (activity == null) {
return;
}
redisUtil.set(KylinRedisConst.GROUP_BUY_ACTIVITY.concat(activity.getActivityId()), activity, RedisKeyExpireConst.KYLIN_GROUP_BUY_EXPIRE);
}
private void clearActivityCache(KylinGroupBuyActivityVo activity) {
if (activity == null) {
return;
}
redisUtil.del(KylinRedisConst.GROUP_BUY_ACTIVITY.concat(activity.getActivityId()));
}
private String now() {
return DateUtil.Formatter.yyyyMMddHHmmss.format(LocalDateTime.now());
}
}
package com.liquidnet.service.slime.util;
import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.support.ExcelTypeEnum;
import com.liquidnet.commons.lang.util.DateUtil;
import com.liquidnet.service.kylin.constant.GroupBuyStatusConst;
import com.liquidnet.service.kylin.dto.vo.groupbuy.KylinGroupBuyPartnerOrderListVo;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.util.CollectionUtils;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* 拼团订单列表导出(EasyExcel,对齐 goblin exportOrderList)。
*/
public final class GroupBuyOrderExcelUtil {
private GroupBuyOrderExcelUtil() {
}
public static void export(List<KylinGroupBuyPartnerOrderListVo> rows, HttpServletResponse response) throws IOException {
if (CollectionUtils.isEmpty(rows)) {
return;
}
ServletOutputStream outputStream = null;
try {
String fileName = DateUtil.Formatter.yyyyMMddHHmmssTrim.format(LocalDateTime.now())
.concat(new String(("拼团订单").getBytes("gb2312"), StandardCharsets.ISO_8859_1));
response.setHeader(HttpHeaders.CONTENT_DISPOSITION,
"attachment;filename=".concat(fileName).concat(ExcelTypeEnum.XLSX.getValue()));
response.setContentType(MediaType.MULTIPART_FORM_DATA_VALUE);
response.setCharacterEncoding(StandardCharsets.UTF_8.name());
outputStream = response.getOutputStream();
EasyExcel.write(outputStream)
.head(buildHead())
.sheet("拼团订单")
.doWrite(buildRows(rows));
} finally {
if (outputStream != null) {
try {
outputStream.close();
} catch (Exception ignored) {
}
}
}
}
private static List<List<String>> buildHead() {
List<List<String>> head = new ArrayList<>();
head.add(Collections.singletonList("订单号"));
head.add(Collections.singletonList("拼团活动"));
head.add(Collections.singletonList("发起人"));
head.add(Collections.singletonList("参团人数"));
head.add(Collections.singletonList("成团人数"));
head.add(Collections.singletonList("订单金额"));
head.add(Collections.singletonList("创建时间"));
head.add(Collections.singletonList("状态"));
return head;
}
private static List<List<String>> buildRows(List<KylinGroupBuyPartnerOrderListVo> list) {
List<List<String>> rows = new ArrayList<>(list.size());
for (KylinGroupBuyPartnerOrderListVo vo : list) {
List<String> row = new ArrayList<>(8);
row.add(nullToEmpty(vo.getGroupOrderId()));
row.add(nullToEmpty(vo.getActivityName()));
row.add(nullToEmpty(vo.getLeaderNickname()));
row.add(formatProgress(vo.getCurrentCount(), vo.getTargetCount()));
row.add(vo.getTargetCount() == null ? "" : vo.getTargetCount() + "人");
row.add(formatAmount(vo.getOrderAmount()));
row.add(nullToEmpty(vo.getCreatedAt()));
row.add(statusText(vo.getStatus()));
rows.add(row);
}
return rows;
}
private static String formatProgress(Integer current, Integer target) {
int c = current == null ? 0 : current;
int t = target == null ? 0 : target;
return c + "/" + t + "人";
}
private static String formatAmount(BigDecimal amount) {
if (amount == null) {
return "";
}
return amount.setScale(2, RoundingMode.HALF_UP).toPlainString();
}
private static String statusText(Integer status) {
if (status == null) {
return "";
}
if (GroupBuyStatusConst.OrderStatus.PENDING.equals(status)) {
return "拼团中";
}
if (GroupBuyStatusConst.OrderStatus.SUCCESS.equals(status)) {
return "拼团成功";
}
if (GroupBuyStatusConst.OrderStatus.FAILED.equals(status)) {
return "拼团失败";
}
if (GroupBuyStatusConst.OrderStatus.PREPARE.equals(status)) {
return "待支付发起";
}
if (GroupBuyStatusConst.OrderStatus.CANCELLED.equals(status)) {
return "已取消";
}
return String.valueOf(status);
}
private static String nullToEmpty(String s) {
return s == null ? "" : s;
}
}
......@@ -73,3 +73,10 @@ kylin_ticket_relations.insert=INSERT INTO kylin_ticket_relations (ticket_relatio
kylin_ticket_express_module.del=DELETE FROM kylin_ticket_express_module WHERE ticket_id = ? and created_at < ?
kylin_ticket_express_module.insert=INSERT INTO kylin_ticket_express_module (ticket_id,produce_code,created_at,updated_at) VALUES (?,?,?,?)
# ---- 演出票拼团 ----
kylin_group_buy_activity.delete=DELETE FROM kylin_group_buy_activity WHERE activity_id = ?
kylin_group_buy_activity.insert=INSERT INTO kylin_group_buy_activity (activity_id,name,performance_id,min_group_size,max_buy_per_user,start_time,end_time,launch_type,scheduled_time,status,description,created_at,updated_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)
kylin_group_buy_activity.update_status=UPDATE kylin_group_buy_activity SET status = ?, updated_at = ? WHERE activity_id = ? AND status = ?
kylin_group_buy_discount.delete_by_activity=DELETE FROM kylin_group_buy_discount WHERE activity_id = ?
kylin_group_buy_discount.insert=INSERT INTO kylin_group_buy_discount (discount_id,activity_id,performance_id,time_id,ticket_id,discount_type,discount_value,created_at) VALUES (?,?,?,?,?,?,?,?)
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment