Compare commits

..

2 Commits

Author SHA1 Message Date
irving
385ceeecb6 adjust logic, avoide race condition
Some checks failed
Build and Push Backend / docker (push) Failing after 6s
2025-10-18 20:56:55 -04:00
irving
584780a812 feat: 店员收益明细支持条件筛选并返回订单信息 2025-10-18 20:48:39 -04:00
284 changed files with 1539 additions and 24618 deletions

View File

@@ -134,71 +134,6 @@ mvn spotless:apply compile
mvn spotless:apply checkstyle:check compile
```
## 数据库迁移
仓库根目录下提供 `flyway/` 配置与 `flyway.sh` 工具脚本,用于在不同环境执行迁移。
### Profile 与配置
| Profile | 配置文件 | 用途 |
|------------|-----------------------|------------------------------|
| `dev` | `flyway/dev.conf` | 本地 `play-with` 开发库 |
| `staging` | `flyway/staging.conf` | 生产克隆 / 本地 staging 库 |
| `api-test` | `flyway/api-test.conf`| API 集成测试数据库 |
| `prod` | `flyway/prod.conf` | 线上生产库 |
### 示例
```bash
# 校验本地 schema
./flyway.sh validate --profile dev
# 对 API 测试库执行迁移
./flyway.sh migrate --profile api-test
# 修复 staging 库
./flyway.sh repair --profile staging
```
当对 `prod` profile 执行 `migrate``repair` 时,脚本会连续两次提示“你备份数据库了吗?”以避免误操作,输入 `yes` 才会继续。
## API 集成测试指南
`play-admin` 模块内提供了基于 `apitest` Profile 的端到端测试套件。为了稳定跑通所有 API 场景,请按以下步骤准备环境:
1. **准备数据库**
默认连接信息为 `jdbc:mysql://127.0.0.1:33306/peipei_apitest`,账号密码均为 `apitest`。可通过以下命令初始化:
```sql
CREATE DATABASE IF NOT EXISTS peipei_apitest CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
CREATE USER IF NOT EXISTS 'apitest'@'%' IDENTIFIED BY 'apitest';
GRANT ALL PRIVILEGES ON peipei_apitest.* TO 'apitest'@'%';
FLUSH PRIVILEGES;
```
若端口或凭证不同,请同步修改 `play-admin/src/main/resources/application-apitest.yml`。
2. **准备 Redis必需**
测试依赖 Redis 记录幂等与缓存信息。可以执行 `docker compose up -d redis`(路径:`docker/docker-compose.yml`)快速启一个实例,默认映射端口为 `36379`。
3. **执行测试**
在仓库根目录运行:
```bash
mvn -pl play-admin -am test
```
如需探查单个用例,可指定测试类:
```bash
mvn -pl play-admin -Dtest=WxCustomRandomOrderApiTest test
```
4. **自动数据播种**
激活 `apitest` Profile 时,`ApiTestDataSeeder` 会自动创建默认租户、顾客、店员、商品、礼物、优惠券等基线数据,并在每次启动时重置关键计数,因此多次执行结果一致。如果需要彻底清理,可直接清空数据库后重新运行测试。
按照上述流程,即可可靠地复现订单、优惠券、礼物等核心链路的 API 行为。
## 部署说明
### Docker 构建和推送
@@ -335,4 +270,4 @@ mvn clean install
✅ 模块间配置一致
✅ Spotless 代码格式化已配置
✅ Checkstyle 代码规范检查已配置
✅ VS Code Java 配置已设置
✅ VS Code Java 配置已设置

View File

@@ -1,18 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
DB_HOST="primary"
DB_PORT="3306"
DB_NAME="play-with"
DB_USER="root"
DB_PASSWORD="123456"
stamp="$(date +%F)"
backup_dir="yunpei/backup/dev/${stamp}"
mkdir -p "${backup_dir}"
echo "[backup] dumping ${DB_NAME} from ${DB_HOST}:${DB_PORT} -> ${backup_dir}/dev.sql.gz"
mysqldump -h "${DB_HOST}" -P "${DB_PORT}" -u "${DB_USER}" -p"${DB_PASSWORD}" "${DB_NAME}" \
| gzip > "${backup_dir}/dev.sql.gz"
echo "[backup] done"

View File

@@ -77,20 +77,6 @@ fi
TIMESTAMP=$(TZ='Asia/Shanghai' date +"%Y-%m-%d-%Hh-%Mm")
echo -e "${YELLOW}构建时间戳 (UTC+8): ${TIMESTAMP}${NC}"
# 获取 Git 提交信息用于镜像元数据
if git rev-parse HEAD >/dev/null 2>&1; then
COMMIT_HASH=$(git rev-parse HEAD)
COMMIT_MESSAGE=$(git log -1 --pretty=%s | tr -d '\n')
COMMIT_MESSAGE=${COMMIT_MESSAGE//\"/\'}
COMMIT_MESSAGE=${COMMIT_MESSAGE//\$/\\$}
else
COMMIT_HASH="unknown"
COMMIT_MESSAGE="unknown"
fi
echo -e "${YELLOW}Git 提交: ${COMMIT_HASH}${NC}"
echo -e "${YELLOW}提交说明: ${COMMIT_MESSAGE}${NC}"
# 镜像名称和标签
IMAGE_NAME="peipei-backend"
VERSION_TAG="${TIMESTAMP}-${TARGET_ARCH}"
@@ -138,8 +124,6 @@ if docker buildx build \
--load \
--cache-from="type=local,src=${CACHE_DIR}" \
--cache-to="type=local,dest=${CACHE_DIR}" \
--label "org.opencontainers.image.revision=${COMMIT_HASH}" \
--label "org.opencontainers.image.commit-message=${COMMIT_MESSAGE}" \
-f docker/Dockerfile \
-t "${IMAGE_NAME}:${VERSION_TAG}" \
-t "${IMAGE_NAME}:${LATEST_TAG}" \
@@ -155,9 +139,6 @@ if [[ "$BUILD_SUCCESS" == "true" ]]; then
echo -e "${GREEN}镜像标签:${NC}"
echo -e " - ${IMAGE_NAME}:${VERSION_TAG}"
echo -e " - ${IMAGE_NAME}:${LATEST_TAG}"
echo -e "${GREEN}镜像元数据:${NC}"
echo -e " - org.opencontainers.image.revision=${COMMIT_HASH}"
echo -e " - org.opencontainers.image.commit-message=${COMMIT_MESSAGE}"
echo -e "\n${YELLOW}镜像信息:${NC}"
docker images | grep -E "^${IMAGE_NAME}\s"

View File

@@ -1,75 +1,6 @@
#!/usr/bin/env bash
# Docker deployment script with safety checks
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$SCRIPT_DIR"
prompt_yes_no() {
local prompt="$1"
local choice_hint="${2:-[y/N]}"
local answer
read -r -p "$prompt $choice_hint " answer || true
answer="${answer:-}"
answer="$(printf '%s' "$answer" | tr '[:upper:]' '[:lower:]')"
[[ "$answer" == "y" || "$answer" == "yes" ]]
}
echo "=== 部署前检查开始 ==="
if ! git -C "$SCRIPT_DIR" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
echo "错误: 当前目录不在 Git 仓库内,无法继续。"
exit 1
fi
CURRENT_BRANCH=$(git -C "$SCRIPT_DIR" rev-parse --abbrev-ref HEAD)
if [[ "$CURRENT_BRANCH" != "master" ]]; then
echo "错误: 当前分支为 '$CURRENT_BRANCH'。仅允许在 master 分支上部署。"
exit 1
fi
if prompt_yes_no "你跑测试了吗?确认已跑请输入 y"; then
echo "已确认测试执行完毕。"
else
if prompt_yes_no "需要我帮你跑测试吗?"; then
echo "开始执行测试: mvn test"
if ! mvn test; then
echo "测试未通过,部署流程终止。"
exit 1
fi
echo "测试通过。"
else
echo "错误: 未执行测试,部署流程终止。"
exit 1
fi
fi
if ! prompt_yes_no "你备份数据库了吗?确认已备份请输入 y"; then
echo "请先完成数据库备份,再运行部署脚本。"
exit 1
fi
if ! prompt_yes_no "你 commit 了吗?确认已提交请输入 y"; then
echo "请在提交后再运行部署脚本。"
exit 1
fi
if ! git -C "$SCRIPT_DIR" diff --quiet; then
echo "错误: 检测到未暂存的本地修改,请处理后再试。"
exit 1
fi
if [[ -n "$(git -C "$SCRIPT_DIR" ls-files --others --exclude-standard)" ]]; then
echo "错误: 检测到未跟踪的文件,请清理或加入版本控制后再试。"
exit 1
fi
if ! git -C "$SCRIPT_DIR" diff --cached --quiet; then
echo "错误: 检测到未提交的暂存修改,请提交后再试。"
exit 1
fi
echo "部署前检查通过。"
#!/bin/sh
# Docker deployment script
set -e
# Get current time and format it
current_time=$(date +"%Y-%m-%d %H:%M:%S")

View File

@@ -25,14 +25,11 @@ FROM --platform=$TARGETPLATFORM eclipse-temurin:11-jre AS runtime
RUN groupadd -g 1001 appgroup && useradd -u 1001 -g appgroup -m -s /usr/sbin/nologin appuser
WORKDIR /app
RUN mkdir -p /app/log
COPY --from=build /workspace/play-admin/target/*.jar /app/app.jar
RUN chown -R appuser:appgroup /app
USER appuser
ENV JAVA_OPTS="-Xms2g -Xmx2g" \
LOG_DIR="/app/log" \
SPRING_PROFILES_ACTIVE="test" \
TZ="Asia/Shanghai" \
LANG="C.UTF-8"

View File

@@ -1,27 +0,0 @@
version: "3.9"
services:
mysql-apitest:
image: mysql:8.0.24
container_name: peipei-mysql-apitest
restart: unless-stopped
environment:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: peipei_apitest
MYSQL_USER: apitest
MYSQL_PASSWORD: apitest
ports:
- "33306:3306"
volumes:
- ./apitest-mysql/init:/docker-entrypoint-initdb.d:ro
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1", "-uroot", "-proot"]
interval: 10s
timeout: 5s
retries: 10
command:
- "--default-authentication-plugin=mysql_native_password"
- "--lower_case_table_names=1"
- "--explicit_defaults_for_timestamp=1"
- "--character-set-server=utf8mb4"
- "--collation-server=utf8mb4_unicode_ci"

View File

@@ -1,2 +0,0 @@
GRANT SELECT ON performance_schema.* TO 'apitest'@'%';
FLUSH PRIVILEGES;

View File

@@ -1,10 +0,0 @@
# API Test MySQL Seed Files
将初始化 schema 和种子数据的 SQL 文件放在此目录下,文件会在 `mysql-apitest` 容器启动时自动执行。
推荐约定:
- `000-schema.sql`:创建数据库/表结构(可复用 Flyway 生成的整库脚本)。
- `100-seed-*.sql`:插入基础租户、用户、商品、优惠券等测试数据。
- `900-cleanup.sql`:可选的清理脚本,用于重置状态。
容器销毁(`docker-compose down -v`)后数据会一起删除,保证每次测试环境一致。

View File

@@ -1,32 +0,0 @@
version: "3.9"
services:
mysql-apitest:
image: mysql:8.0
container_name: peipei-mysql-apitest
restart: unless-stopped
environment:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: peipei_apitest
MYSQL_USER: apitest
MYSQL_PASSWORD: apitest
ports:
- "33306:3306"
command:
- "--default-authentication-plugin=mysql_native_password"
- "--lower_case_table_names=1"
- "--explicit_defaults_for_timestamp=1"
- "--character-set-server=utf8mb4"
- "--collation-server=utf8mb4_general_ci"
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1", "-uroot", "-proot"]
interval: 10s
timeout: 5s
retries: 10
redis-apitest:
image: redis:7-alpine
container_name: peipei-redis-apitest
restart: unless-stopped
command: ["redis-server", "--appendonly", "no"]
ports:
- "36379:6379"

View File

@@ -1,20 +1,9 @@
services:
redis:
image: redis:7-alpine
container_name: peipei-redis
ports:
- "36379:6379"
command: ["redis-server", "--save", "", "--appendonly", "no"]
restart: unless-stopped
networks:
- peipei-network
version: "3.8"
services:
peipei-backend:
image: docker-registry.julyhaven.com/peipei/backend:latest
container_name: peipei-backend
platform: linux/amd64
depends_on:
- redis
ports:
- "7003:7002"
environment:

View File

@@ -1,92 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
CONFIG_DIR="$SCRIPT_DIR/flyway"
usage() {
cat <<USAGE
Usage: $(basename "$0") <migrate|validate|repair> --profile <api-test|dev|staging|prod>
Examples:
./flyway.sh validate --profile dev
./flyway.sh migrate --profile api-test
USAGE
exit 1
}
if [[ $# -lt 2 ]]; then
usage
fi
action="$1"
shift
case "$action" in
migrate|validate|repair) ;;
*) echo "[ERROR] Unsupported action: $action" >&2; usage ;;
esac
profile=""
while [[ $# -gt 0 ]]; do
case "$1" in
--profile|-p)
[[ $# -ge 2 ]] || usage
profile="$2"
shift 2
;;
*)
echo "[ERROR] Unknown argument: $1" >&2
usage
;;
esac
done
if [[ -z "$profile" ]]; then
usage
fi
case "$profile" in
api-test|apitest)
profile="api-test"
config_file="$CONFIG_DIR/api-test.conf"
;;
dev)
config_file="$CONFIG_DIR/dev.conf"
;;
staging)
config_file="$CONFIG_DIR/staging.conf"
;;
prod)
config_file="$CONFIG_DIR/prod.conf"
;;
*)
echo "[ERROR] Unknown profile: $profile" >&2
usage
;;
esac
if [[ ! -f "$config_file" ]]; then
echo "[ERROR] Config file not found: $config_file" >&2
exit 1
fi
confirm_backup() {
local prompt="$1"
read -r -p "$prompt (yes/no): " reply
if [[ "$reply" != "yes" ]]; then
echo "操作已取消"
exit 1
fi
}
if [[ "$profile" == "prod" && ( "$action" == "migrate" || "$action" == "repair" ) ]]; then
confirm_backup "你备份数据库了吗?"
confirm_backup "你真的备份了吗?"
fi
exec mvn \
-f "$SCRIPT_DIR/pom.xml" \
-pl play-admin \
-DskipTests \
"flyway:$action" \
"-Dflyway.configFiles=$config_file"

View File

@@ -1,10 +0,0 @@
flyway.url=jdbc:mysql://127.0.0.1:33306/peipei_apitest?useUnicode=true&characterEncoding=utf8&characterSetResults=utf8
flyway.user=apitest
flyway.password=apitest
flyway.locations=classpath:db/migration
flyway.table=admin_flyway_schema_history
flyway.baselineOnMigrate=true
flyway.baselineVersion=1
flyway.cleanDisabled=true
flyway.outOfOrder=false
flyway.validateOnMigrate=false

View File

@@ -1,10 +0,0 @@
flyway.url=jdbc:mysql://127.0.0.1:3307/play-with?useUnicode=true&characterEncoding=utf8&characterSetResults=utf8
flyway.user=root
flyway.password=root
flyway.locations=classpath:db/migration
flyway.table=admin_flyway_schema_history
flyway.baselineOnMigrate=true
flyway.baselineVersion=1
flyway.cleanDisabled=true
flyway.outOfOrder=false
flyway.validateOnMigrate=false

View File

@@ -1,10 +0,0 @@
flyway.url=jdbc:mysql://122.51.20.105:3306/play-with?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&allowMultiQueries=true&rewriteBatchedStatements=true
flyway.user=root
flyway.password=KdaKRZ2trpdhNePa
flyway.locations=classpath:db/migration
flyway.table=admin_flyway_schema_history
flyway.baselineOnMigrate=true
flyway.baselineVersion=1
flyway.cleanDisabled=true
flyway.outOfOrder=false
flyway.validateOnMigrate=true

View File

@@ -1,10 +0,0 @@
flyway.url=jdbc:mysql://127.0.0.1:3307/play-with?useUnicode=true&characterEncoding=utf8&characterSetResults=utf8
flyway.user=root
flyway.password=root
flyway.locations=classpath:db/migration
flyway.table=admin_flyway_schema_history
flyway.baselineOnMigrate=true
flyway.baselineVersion=1
flyway.cleanDisabled=true
flyway.outOfOrder=false
flyway.validateOnMigrate=false

View File

@@ -1,24 +0,0 @@
## 媒資/相簿相容性:手動驗證清單
1. **WeChat 店員端 - 個資頁預覽**
- 登入店員帳號進入「我的資料」,確認相簿縮圖顯示為合併後的 `mediaList + album`,不應出現重覆 URL。
- 點擊「照片」進入管理介面,確認 legacy album 中的圖片仍存在;上傳新媒資後應立即出現於列表。
2. **媒資上傳與排序**
- 上傳圖片與影片各一,測試格式/大小超限的錯誤提示與成功上傳後的狀態。
- 排序、刪除媒資並提交審核,確認前端列表與預覽更新,且不會重複顯示相同 URL。
3. **後台店員列表**
- 在管理端店員列表中,確認每位店員的照片區塊都展示合併後的媒資清單(舊 album + 新媒資)。
- 點擊圖片預覽,確認輪播順序正確、無重覆 URL。
4. **後台店員審核詳情**
- 查看一筆含多張舊相簿照片的申請,確認圖片區塊已改用 `mediaGallery`,兼容新舊媒資。
- 點擊照片預覽,確認圖片來源為合併後的清單。
5. **API 回應驗證**
- 呼叫 `/clerk/user/list` 或 WeChat 前端使用的 API檢查 `album` 欄位仍保留原值,`mediaList` 會包含新媒資並附帶 legacy URL無重複
- 若資料仍未遷移,確保 `mediaList` 仍會帶上舊 `album` 的 URL。
6. **媒資審核流程**
- 走一次「上傳 → 排序 → 提交 → 審核」流程,確認審核通過後 `mediaList` 只保留 Approved 的媒資,但 `album` 不會被清除,舊客端仍能看到舊資料。

View File

@@ -16,7 +16,6 @@
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<spring.profiles.active>test</spring.profiles.active>
</properties>
<dependencies>
@@ -159,37 +158,6 @@
<artifactId>mockito-junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.jayway.jsonpath</groupId>
<artifactId>json-path</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
@@ -220,9 +188,6 @@
<version>3.0.0-M7</version>
<configuration>
<useSystemClassLoader>false</useSystemClassLoader>
<systemPropertyVariables>
<spring.profiles.active>${spring.profiles.active}</spring.profiles.active>
</systemPropertyVariables>
</configuration>
</plugin>
<plugin>

View File

@@ -1,21 +0,0 @@
package com.starry.admin.common;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
public class PageBuilder {
public static final String PAGE_NUM = "pageNum";
public static final String PAGE_SIZE = "pageSize";
public static <T> Page<T> build() {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
Integer pageNum = Integer.valueOf(attributes.getRequest().getParameter(PAGE_NUM));
Integer pageSize = Integer.valueOf(attributes.getRequest().getParameter(PAGE_SIZE));
return new Page<>(pageNum, pageSize);
}
}

View File

@@ -1,526 +0,0 @@
package com.starry.admin.common.apitest;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.starry.admin.modules.clerk.module.entity.PlayClerkCommodityEntity;
import com.starry.admin.modules.clerk.module.entity.PlayClerkLevelInfoEntity;
import com.starry.admin.modules.clerk.module.entity.PlayClerkUserInfoEntity;
import com.starry.admin.modules.clerk.service.IPlayClerkCommodityService;
import com.starry.admin.modules.clerk.service.IPlayClerkLevelInfoService;
import com.starry.admin.modules.clerk.service.IPlayClerkUserInfoService;
import com.starry.admin.modules.custom.mapper.PlayCustomGiftInfoMapper;
import com.starry.admin.modules.custom.module.entity.PlayCustomGiftInfoEntity;
import com.starry.admin.modules.custom.module.entity.PlayCustomUserInfoEntity;
import com.starry.admin.modules.custom.service.IPlayCustomGiftInfoService;
import com.starry.admin.modules.custom.service.IPlayCustomUserInfoService;
import com.starry.admin.modules.personnel.module.entity.PlayPersonnelGroupInfoEntity;
import com.starry.admin.modules.personnel.service.IPlayPersonnelGroupInfoService;
import com.starry.admin.modules.shop.mapper.PlayClerkGiftInfoMapper;
import com.starry.admin.modules.shop.module.entity.PlayClerkGiftInfoEntity;
import com.starry.admin.modules.shop.module.entity.PlayCommodityAndLevelInfoEntity;
import com.starry.admin.modules.shop.module.entity.PlayCommodityInfoEntity;
import com.starry.admin.modules.shop.module.entity.PlayGiftInfoEntity;
import com.starry.admin.modules.shop.service.IPlayClerkGiftInfoService;
import com.starry.admin.modules.shop.service.IPlayCommodityAndLevelInfoService;
import com.starry.admin.modules.shop.service.IPlayCommodityInfoService;
import com.starry.admin.modules.shop.service.IPlayGiftInfoService;
import com.starry.admin.modules.system.module.entity.SysTenantEntity;
import com.starry.admin.modules.system.module.entity.SysTenantPackageEntity;
import com.starry.admin.modules.system.module.entity.SysUserEntity;
import com.starry.admin.modules.system.service.ISysTenantPackageService;
import com.starry.admin.modules.system.service.ISysTenantService;
import com.starry.admin.modules.system.service.SysUserService;
import com.starry.admin.modules.weichat.service.WxTokenService;
import com.starry.admin.utils.SecurityUtils;
import com.starry.common.context.CustomSecurityContextHolder;
import com.starry.common.utils.IdUtils;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.Date;
import java.util.Objects;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.CommandLineRunner;
import org.springframework.context.annotation.Profile;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
@Component
@Profile("apitest")
public class ApiTestDataSeeder implements CommandLineRunner {
private static final Logger log = LoggerFactory.getLogger(ApiTestDataSeeder.class);
public static final String DEFAULT_PACKAGE_ID = "pkg-basic";
public static final String DEFAULT_TENANT_ID = "tenant-apitest";
public static final String DEFAULT_TENANT_KEY = "tenant-key-apitest";
public static final String DEFAULT_TENANT_NAME = "API Test Tenant";
public static final String DEFAULT_ADMIN_USER_ID = "user-apitest-admin";
public static final String DEFAULT_ADMIN_USERNAME = "apitest-admin";
public static final String DEFAULT_GROUP_ID = "group-basic";
public static final String DEFAULT_CLERK_LEVEL_ID = "lvl-basic";
public static final String DEFAULT_CLERK_ID = "clerk-apitest";
public static final String DEFAULT_CLERK_OPEN_ID = "openid-clerk-apitest";
public static final String DEFAULT_COMMODITY_PARENT_ID = "svc-parent";
public static final String DEFAULT_COMMODITY_PARENT_NAME = "语音陪聊服务";
public static final String DEFAULT_COMMODITY_ID = "svc-basic";
public static final String DEFAULT_CLERK_COMMODITY_ID = "clerk-svc-basic";
public static final String DEFAULT_CUSTOMER_ID = "customer-apitest";
public static final String DEFAULT_GIFT_ID = "gift-basic";
public static final String DEFAULT_GIFT_NAME = "API测试礼物";
public static final BigDecimal DEFAULT_COMMODITY_PRICE = new BigDecimal("120.00");
private static final String GIFT_TYPE_REGULAR = "1";
private static final String GIFT_STATE_ACTIVE = "0";
private static final BigDecimal DEFAULT_CUSTOMER_BALANCE = new BigDecimal("200.00");
private static final BigDecimal DEFAULT_CUSTOMER_RECHARGE = DEFAULT_CUSTOMER_BALANCE;
private final ISysTenantPackageService tenantPackageService;
private final ISysTenantService tenantService;
private final SysUserService sysUserService;
private final IPlayPersonnelGroupInfoService personnelGroupInfoService;
private final IPlayClerkLevelInfoService clerkLevelInfoService;
private final IPlayClerkUserInfoService clerkUserInfoService;
private final IPlayCommodityInfoService commodityInfoService;
private final IPlayCommodityAndLevelInfoService commodityAndLevelInfoService;
private final IPlayGiftInfoService giftInfoService;
private final IPlayClerkCommodityService clerkCommodityService;
private final IPlayClerkGiftInfoService playClerkGiftInfoService;
private final IPlayCustomUserInfoService customUserInfoService;
private final IPlayCustomGiftInfoService playCustomGiftInfoService;
private final PlayClerkGiftInfoMapper playClerkGiftInfoMapper;
private final PlayCustomGiftInfoMapper playCustomGiftInfoMapper;
private final PasswordEncoder passwordEncoder;
private final WxTokenService wxTokenService;
public ApiTestDataSeeder(
ISysTenantPackageService tenantPackageService,
ISysTenantService tenantService,
SysUserService sysUserService,
IPlayPersonnelGroupInfoService personnelGroupInfoService,
IPlayClerkLevelInfoService clerkLevelInfoService,
IPlayClerkUserInfoService clerkUserInfoService,
IPlayCommodityInfoService commodityInfoService,
IPlayCommodityAndLevelInfoService commodityAndLevelInfoService,
IPlayGiftInfoService giftInfoService,
IPlayClerkCommodityService clerkCommodityService,
IPlayClerkGiftInfoService playClerkGiftInfoService,
IPlayCustomUserInfoService customUserInfoService,
IPlayCustomGiftInfoService playCustomGiftInfoService,
PlayClerkGiftInfoMapper playClerkGiftInfoMapper,
PlayCustomGiftInfoMapper playCustomGiftInfoMapper,
PasswordEncoder passwordEncoder,
WxTokenService wxTokenService) {
this.tenantPackageService = tenantPackageService;
this.tenantService = tenantService;
this.sysUserService = sysUserService;
this.personnelGroupInfoService = personnelGroupInfoService;
this.clerkLevelInfoService = clerkLevelInfoService;
this.clerkUserInfoService = clerkUserInfoService;
this.commodityInfoService = commodityInfoService;
this.commodityAndLevelInfoService = commodityAndLevelInfoService;
this.giftInfoService = giftInfoService;
this.clerkCommodityService = clerkCommodityService;
this.playClerkGiftInfoService = playClerkGiftInfoService;
this.customUserInfoService = customUserInfoService;
this.playCustomGiftInfoService = playCustomGiftInfoService;
this.playClerkGiftInfoMapper = playClerkGiftInfoMapper;
this.playCustomGiftInfoMapper = playCustomGiftInfoMapper;
this.passwordEncoder = passwordEncoder;
this.wxTokenService = wxTokenService;
}
@Override
@Transactional
public void run(String... args) {
seedTenantPackage();
seedTenant();
String originalTenant = SecurityUtils.getTenantId();
try {
SecurityUtils.setTenantId(DEFAULT_TENANT_ID);
seedAdminUser();
seedPersonnelGroup();
seedClerkLevel();
PlayCommodityInfoEntity commodity = seedCommodityHierarchy();
seedCommodityPricing(commodity);
seedClerk();
seedClerkCommodity();
seedGift();
resetGiftCounters();
seedCustomer();
} finally {
if (Objects.nonNull(originalTenant)) {
SecurityUtils.setTenantId(originalTenant);
}
CustomSecurityContextHolder.remove();
}
}
private void seedTenantPackage() {
long existing = tenantPackageService.count(Wrappers.<SysTenantPackageEntity>lambdaQuery()
.eq(SysTenantPackageEntity::getPackageId, DEFAULT_PACKAGE_ID));
if (existing > 0) {
log.info("API test tenant package {} already exists", DEFAULT_PACKAGE_ID);
return;
}
SysTenantPackageEntity entity = new SysTenantPackageEntity();
entity.setPackageId(DEFAULT_PACKAGE_ID);
entity.setPackageName("API测试基础套餐");
entity.setStatus("0");
entity.setMenuIds("[]");
entity.setRemarks("Seeded for API integration tests");
tenantPackageService.save(entity);
log.info("Inserted API test tenant package {}", DEFAULT_PACKAGE_ID);
}
private void seedTenant() {
SysTenantEntity tenant = tenantService.getById(DEFAULT_TENANT_ID);
if (tenant != null) {
log.info("API test tenant {} already exists", DEFAULT_TENANT_ID);
return;
}
SysTenantEntity entity = new SysTenantEntity();
entity.setTenantId(DEFAULT_TENANT_ID);
entity.setTenantName(DEFAULT_TENANT_NAME);
entity.setTenantType("0");
entity.setTenantStatus("0");
entity.setTenantCode("apitest");
entity.setTenantKey(DEFAULT_TENANT_KEY);
entity.setPackageId(DEFAULT_PACKAGE_ID);
entity.setTenantTime(new Date(System.currentTimeMillis() + 365L * 24 * 3600 * 1000));
entity.setUserName(DEFAULT_ADMIN_USERNAME);
entity.setUserPwd(passwordEncoder.encode("apitest-secret"));
entity.setPhone("13800000000");
entity.setEmail("apitest@example.com");
entity.setAddress("API Test Street 1");
tenantService.save(entity);
log.info("Inserted API test tenant {}", DEFAULT_TENANT_ID);
}
private void seedAdminUser() {
SysUserEntity existing = sysUserService.getById(DEFAULT_ADMIN_USER_ID);
if (existing != null) {
log.info("API test admin user {} already exists", DEFAULT_ADMIN_USER_ID);
return;
}
SysUserEntity admin = new SysUserEntity();
admin.setUserId(DEFAULT_ADMIN_USER_ID);
admin.setUserCode(DEFAULT_ADMIN_USERNAME);
admin.setPassWord(passwordEncoder.encode("apitest-secret"));
admin.setRealName("API Test Admin");
admin.setUserNickname("API Admin");
admin.setStatus(0);
admin.setUserType(1);
admin.setTenantId(DEFAULT_TENANT_ID);
admin.setMobile("13800000000");
admin.setAddTime(LocalDateTime.now());
admin.setSuperAdmin(Boolean.TRUE);
sysUserService.save(admin);
log.info("Inserted API test admin user {}", DEFAULT_ADMIN_USER_ID);
}
private void seedPersonnelGroup() {
PlayPersonnelGroupInfoEntity group = personnelGroupInfoService.getById(DEFAULT_GROUP_ID);
if (group != null) {
log.info("API test personnel group {} already exists", DEFAULT_GROUP_ID);
return;
}
PlayPersonnelGroupInfoEntity entity = new PlayPersonnelGroupInfoEntity();
entity.setId(DEFAULT_GROUP_ID);
entity.setTenantId(DEFAULT_TENANT_ID);
entity.setSysUserId(DEFAULT_ADMIN_USER_ID);
entity.setSysUserCode(DEFAULT_ADMIN_USERNAME);
entity.setGroupName("测试小组");
entity.setLeaderName("API Admin");
entity.setAddTime(LocalDateTime.now());
personnelGroupInfoService.save(entity);
log.info("Inserted API test personnel group {}", DEFAULT_GROUP_ID);
}
private void seedClerkLevel() {
PlayClerkLevelInfoEntity level = clerkLevelInfoService.getById(DEFAULT_CLERK_LEVEL_ID);
if (level != null) {
log.info("API test clerk level {} already exists", DEFAULT_CLERK_LEVEL_ID);
return;
}
PlayClerkLevelInfoEntity entity = new PlayClerkLevelInfoEntity();
entity.setId(DEFAULT_CLERK_LEVEL_ID);
entity.setTenantId(DEFAULT_TENANT_ID);
entity.setName("基础等级");
entity.setLevel(1);
entity.setFirstRegularRatio(60);
entity.setNotFirstRegularRatio(50);
entity.setFirstRandomRadio(55);
entity.setNotFirstRandomRadio(45);
entity.setFirstRewardRatio(40);
entity.setNotFirstRewardRatio(35);
entity.setOrderNumber(1L);
clerkLevelInfoService.save(entity);
log.info("Inserted API test clerk level {}", DEFAULT_CLERK_LEVEL_ID);
}
private PlayCommodityInfoEntity seedCommodityHierarchy() {
PlayCommodityInfoEntity parent = commodityInfoService.getById(DEFAULT_COMMODITY_PARENT_ID);
if (parent == null) {
parent = new PlayCommodityInfoEntity();
parent.setId(DEFAULT_COMMODITY_PARENT_ID);
parent.setTenantId(DEFAULT_TENANT_ID);
parent.setPId("00");
parent.setItemType("service-category");
parent.setItemName(DEFAULT_COMMODITY_PARENT_NAME);
parent.setEnableStace("1");
parent.setSort(1);
commodityInfoService.save(parent);
log.info("Inserted API test commodity parent {}", DEFAULT_COMMODITY_PARENT_ID);
} else {
boolean parentNeedsUpdate = false;
if (!"00".equals(parent.getPId())) {
parent.setPId("00");
parentNeedsUpdate = true;
}
if (!"service-category".equals(parent.getItemType())) {
parent.setItemType("service-category");
parentNeedsUpdate = true;
}
if (!DEFAULT_TENANT_ID.equals(parent.getTenantId())) {
parent.setTenantId(DEFAULT_TENANT_ID);
parentNeedsUpdate = true;
}
if (!"1".equals(parent.getEnableStace())) {
parent.setEnableStace("1");
parentNeedsUpdate = true;
}
if (parentNeedsUpdate) {
commodityInfoService.updateById(parent);
log.info("Normalized API test commodity parent {}", DEFAULT_COMMODITY_PARENT_ID);
}
}
PlayCommodityInfoEntity child = commodityInfoService.getById(DEFAULT_COMMODITY_ID);
if (child != null) {
boolean childNeedsUpdate = false;
if (!DEFAULT_COMMODITY_PARENT_ID.equals(child.getPId())) {
child.setPId(DEFAULT_COMMODITY_PARENT_ID);
childNeedsUpdate = true;
}
if (!"service".equals(child.getItemType())) {
child.setItemType("service");
childNeedsUpdate = true;
}
if (!DEFAULT_TENANT_ID.equals(child.getTenantId())) {
child.setTenantId(DEFAULT_TENANT_ID);
childNeedsUpdate = true;
}
if (!"1".equals(child.getEnableStace())) {
child.setEnableStace("1");
childNeedsUpdate = true;
}
if (childNeedsUpdate) {
commodityInfoService.updateById(child);
log.info("Normalized API test commodity {}", DEFAULT_COMMODITY_ID);
}
log.info("API test commodity {} already exists", DEFAULT_COMMODITY_ID);
return child;
}
child = new PlayCommodityInfoEntity();
child.setId(DEFAULT_COMMODITY_ID);
child.setTenantId(DEFAULT_TENANT_ID);
child.setPId(DEFAULT_COMMODITY_PARENT_ID);
child.setItemType("service");
child.setItemName("60分钟语音陪聊");
child.setServiceDuration("60min");
child.setEnableStace("1");
child.setSort(1);
commodityInfoService.save(child);
log.info("Inserted API test commodity {}", DEFAULT_COMMODITY_ID);
return child;
}
private void seedCommodityPricing(PlayCommodityInfoEntity commodity) {
if (commodity == null) {
return;
}
PlayCommodityAndLevelInfoEntity existing = commodityAndLevelInfoService.lambdaQuery()
.eq(PlayCommodityAndLevelInfoEntity::getCommodityId, commodity.getId())
.eq(PlayCommodityAndLevelInfoEntity::getLevelId, DEFAULT_CLERK_LEVEL_ID)
.one();
if (existing != null) {
log.info("API test commodity pricing for {} already exists", commodity.getId());
return;
}
PlayCommodityAndLevelInfoEntity price = new PlayCommodityAndLevelInfoEntity();
price.setId(IdUtils.getUuid());
price.setTenantId(DEFAULT_TENANT_ID);
price.setCommodityId(commodity.getId());
price.setLevelId(DEFAULT_CLERK_LEVEL_ID);
price.setPrice(DEFAULT_COMMODITY_PRICE);
price.setSort(1L);
commodityAndLevelInfoService.save(price);
log.info("Inserted API test commodity pricing for {}", commodity.getId());
}
private void seedClerk() {
PlayClerkUserInfoEntity clerk = clerkUserInfoService.getById(DEFAULT_CLERK_ID);
String clerkToken = wxTokenService.createWxUserToken(DEFAULT_CLERK_ID);
if (clerk != null) {
clerkUserInfoService.updateTokenById(DEFAULT_CLERK_ID, clerkToken);
log.info("API test clerk {} already exists", DEFAULT_CLERK_ID);
return;
}
PlayClerkUserInfoEntity entity = new PlayClerkUserInfoEntity();
entity.setId(DEFAULT_CLERK_ID);
entity.setTenantId(DEFAULT_TENANT_ID);
entity.setSysUserId(DEFAULT_ADMIN_USER_ID);
entity.setOpenid(DEFAULT_CLERK_OPEN_ID);
entity.setNickname("小测官");
entity.setGroupId(DEFAULT_GROUP_ID);
entity.setLevelId(DEFAULT_CLERK_LEVEL_ID);
entity.setFixingLevel("1");
entity.setSex("2");
entity.setPhone("13900000001");
entity.setWeiChatCode("apitest-clerk");
entity.setAvatar("https://example.com/avatar.png");
entity.setAccountBalance(BigDecimal.ZERO);
entity.setOnboardingState("1");
entity.setListingState("1");
entity.setDisplayState("1");
entity.setOnlineState("1");
entity.setRandomOrderState("1");
entity.setClerkState("1");
entity.setEntryTime(LocalDateTime.now());
entity.setToken(clerkToken);
clerkUserInfoService.save(entity);
log.info("Inserted API test clerk {}", DEFAULT_CLERK_ID);
}
private void seedClerkCommodity() {
PlayClerkCommodityEntity mapping = clerkCommodityService.getById(DEFAULT_CLERK_COMMODITY_ID);
if (mapping != null) {
log.info("API test clerk commodity {} already exists", DEFAULT_CLERK_COMMODITY_ID);
return;
}
String commodityName = DEFAULT_COMMODITY_PARENT_NAME;
PlayCommodityInfoEntity parent = commodityInfoService.getById(DEFAULT_COMMODITY_PARENT_ID);
if (parent != null && parent.getItemName() != null) {
commodityName = parent.getItemName();
}
PlayClerkCommodityEntity entity = new PlayClerkCommodityEntity();
entity.setId(DEFAULT_CLERK_COMMODITY_ID);
entity.setTenantId(DEFAULT_TENANT_ID);
entity.setClerkId(DEFAULT_CLERK_ID);
entity.setCommodityId(DEFAULT_COMMODITY_ID);
entity.setCommodityName(commodityName);
entity.setEnablingState("1");
entity.setSort(1);
clerkCommodityService.save(entity);
log.info("Inserted API test clerk commodity link {}", DEFAULT_CLERK_COMMODITY_ID);
}
private void seedGift() {
PlayGiftInfoEntity gift = giftInfoService.getById(DEFAULT_GIFT_ID);
if (gift != null) {
log.info("API test gift {} already exists", DEFAULT_GIFT_ID);
return;
}
PlayGiftInfoEntity entity = new PlayGiftInfoEntity();
entity.setId(DEFAULT_GIFT_ID);
entity.setTenantId(DEFAULT_TENANT_ID);
entity.setHistory("0");
entity.setName(DEFAULT_GIFT_NAME);
entity.setType(GIFT_TYPE_REGULAR);
entity.setUrl("https://example.com/apitest/gift.png");
entity.setPrice(new BigDecimal("15.00"));
entity.setUnit("CNY");
entity.setState(GIFT_STATE_ACTIVE);
entity.setListingTime(LocalDateTime.now());
entity.setRemark("Seeded gift for API tests");
giftInfoService.save(entity);
log.info("Inserted API test gift {}", DEFAULT_GIFT_ID);
}
private void resetGiftCounters() {
int customerReset = playCustomGiftInfoMapper.resetGiftCount(DEFAULT_TENANT_ID, DEFAULT_CUSTOMER_ID, DEFAULT_GIFT_ID);
if (customerReset == 0) {
PlayCustomGiftInfoEntity entity = new PlayCustomGiftInfoEntity();
entity.setId(IdUtils.getUuid());
entity.setTenantId(DEFAULT_TENANT_ID);
entity.setCustomId(DEFAULT_CUSTOMER_ID);
entity.setGiffId(DEFAULT_GIFT_ID);
entity.setGiffNumber(0L);
try {
playCustomGiftInfoService.save(entity);
} catch (org.springframework.dao.DuplicateKeyException duplicateKeyException) {
playCustomGiftInfoMapper.resetGiftCount(DEFAULT_TENANT_ID, DEFAULT_CUSTOMER_ID, DEFAULT_GIFT_ID);
}
}
int clerkReset = playClerkGiftInfoMapper.resetGiftCount(DEFAULT_TENANT_ID, DEFAULT_CLERK_ID, DEFAULT_GIFT_ID);
if (clerkReset == 0) {
PlayClerkGiftInfoEntity entity = new PlayClerkGiftInfoEntity();
entity.setId(IdUtils.getUuid());
entity.setTenantId(DEFAULT_TENANT_ID);
entity.setClerkId(DEFAULT_CLERK_ID);
entity.setGiffId(DEFAULT_GIFT_ID);
entity.setGiffNumber(0L);
try {
playClerkGiftInfoService.save(entity);
} catch (org.springframework.dao.DuplicateKeyException duplicateKeyException) {
playClerkGiftInfoMapper.resetGiftCount(DEFAULT_TENANT_ID, DEFAULT_CLERK_ID, DEFAULT_GIFT_ID);
}
}
}
private void seedCustomer() {
PlayCustomUserInfoEntity customer = customUserInfoService.getById(DEFAULT_CUSTOMER_ID);
String token = wxTokenService.createWxUserToken(DEFAULT_CUSTOMER_ID);
if (customer != null) {
customUserInfoService.updateTokenById(DEFAULT_CUSTOMER_ID, token);
customUserInfoService.lambdaUpdate()
.set(PlayCustomUserInfoEntity::getAccountBalance, DEFAULT_CUSTOMER_BALANCE)
.set(PlayCustomUserInfoEntity::getAccumulatedRechargeAmount, DEFAULT_CUSTOMER_RECHARGE)
.set(PlayCustomUserInfoEntity::getAccumulatedConsumptionAmount, BigDecimal.ZERO)
.set(PlayCustomUserInfoEntity::getAccountState, "1")
.set(PlayCustomUserInfoEntity::getSubscribeState, "1")
.set(PlayCustomUserInfoEntity::getPurchaseState, "1")
.set(PlayCustomUserInfoEntity::getMobilePhoneState, "1")
.set(PlayCustomUserInfoEntity::getLastLoginTime, new Date())
.eq(PlayCustomUserInfoEntity::getId, DEFAULT_CUSTOMER_ID)
.update();
log.info("API test customer {} already exists, state refreshed", DEFAULT_CUSTOMER_ID);
return;
}
PlayCustomUserInfoEntity entity = new PlayCustomUserInfoEntity();
entity.setId(DEFAULT_CUSTOMER_ID);
entity.setTenantId(DEFAULT_TENANT_ID);
entity.setOpenid("openid-customer-apitest");
entity.setUnionid("unionid-customer-apitest");
entity.setNickname("测试顾客");
entity.setSex(1);
entity.setPhone("13700000002");
entity.setWeiChatCode("apitest-customer");
entity.setAccountBalance(DEFAULT_CUSTOMER_BALANCE);
entity.setAccumulatedRechargeAmount(DEFAULT_CUSTOMER_RECHARGE);
entity.setAccumulatedConsumptionAmount(BigDecimal.ZERO);
entity.setAccountState("1");
entity.setSubscribeState("1");
entity.setPurchaseState("1");
entity.setMobilePhoneState("1");
entity.setRegistrationTime(new Date());
entity.setLastLoginTime(new Date());
entity.setToken(token);
customUserInfoService.save(entity);
log.info("Inserted API test customer {}", DEFAULT_CUSTOMER_ID);
}
}

View File

@@ -1,7 +1,6 @@
package com.starry.admin.common.aspect;
import com.starry.admin.common.conf.ThreadLocalRequestDetail;
import com.starry.admin.common.exception.CustomException;
import com.starry.admin.common.exception.ServiceException;
import com.starry.admin.modules.clerk.module.entity.PlayClerkUserInfoEntity;
import com.starry.admin.modules.clerk.service.impl.PlayClerkUserInfoServiceImpl;
@@ -57,12 +56,6 @@ public class ClerkUserLoginAspect {
if (Objects.isNull(entity)) {
throw new ServiceException("未查询到有效用户", HttpStatus.UNAUTHORIZED);
}
try {
clerkUserInfoService.ensureClerkSessionIsValid(entity);
} catch (CustomException e) {
log.warn("Clerk token rejected due to status change, clerkId={} message={}", entity.getId(), e.getMessage());
throw new ServiceException(e.getMessage(), HttpStatus.UNAUTHORIZED);
}
if (!userToken.equals(entity.getToken())) {
throw new ServiceException("token异常", HttpStatus.UNAUTHORIZED);
}

View File

@@ -0,0 +1,19 @@
package com.starry.admin.common.conf;
import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceBuilder;
import javax.sql.DataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
@Configuration
public class DataSourceConfig {
// For flyway only
@Bean(name = "primaryDataSource")
@Primary
public DataSource dataSource() {
return DruidDataSourceBuilder.create().build();
}
}

View File

@@ -32,10 +32,8 @@ public class GlobalExceptionHandler {
public R handleServiceException(ServiceException e, HttpServletRequest request) {
if ("token异常".equals(e.getMessage()) || "token为空".equals(e.getMessage())) {
log.error("用户token异常");
} else if (log.isDebugEnabled()) {
log.debug("业务异常", e);
} else {
log.warn("业务异常: {}", e.getMessage());
log.error(e.getMessage(), e);
}
Integer code = e.getCode();
return StringUtils.isNotNull(code) ? R.error(code, e.getMessage()) : R.error(e.getMessage());
@@ -113,11 +111,10 @@ public class GlobalExceptionHandler {
public R customException(CustomException e) {
if ("token异常".equals(e.getMessage()) || "token为空".equals(e.getMessage())) {
log.error("用户token异常");
} else if (log.isDebugEnabled()) {
log.debug("业务异常", e);
} else {
log.warn("业务异常: {}", e.getMessage());
log.error(e.getMessage(), e);
}
return R.error(e.getMessage());
}
}

View File

@@ -10,11 +10,9 @@ import com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerIntercept
import com.starry.admin.common.mybatis.handler.MyTenantLineHandler;
import javax.sql.DataSource;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.autoconfigure.flyway.FlywayDataSource;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
@@ -32,8 +30,6 @@ public class MybatisPlusConfig {
* @return dataSource
*/
@Bean(name = "dataSource")
@Primary
@FlywayDataSource
@ConfigurationProperties(prefix = "spring.datasource.druid")
public DataSource dataSource() {
return DruidDataSourceBuilder.create().build();

View File

@@ -1,49 +0,0 @@
package com.starry.admin.common.security.config;
import com.starry.admin.common.security.filter.ApiTestAuthenticationFilter;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
@Profile("apitest")
@EnableConfigurationProperties(ApiTestSecurityProperties.class)
public class ApiTestSecurityConfig extends WebSecurityConfigurerAdapter {
private final ApiTestSecurityProperties properties;
public ApiTestSecurityConfig(ApiTestSecurityProperties properties) {
this.properties = properties;
}
@Bean
public ApiTestAuthenticationFilter apiTestAuthenticationFilter() {
return new ApiTestAuthenticationFilter(properties);
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.formLogin().disable()
.logout().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
.authorizeRequests().anyRequest().authenticated().and()
.addFilterBefore(apiTestAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
}
}

View File

@@ -1,73 +0,0 @@
package com.starry.admin.common.security.config;
import java.util.ArrayList;
import java.util.List;
import org.springframework.boot.context.properties.ConfigurationProperties;
@ConfigurationProperties(prefix = "apitest.security")
public class ApiTestSecurityProperties {
private String tenantHeader = "X-Tenant";
private String userHeader = "X-Test-User";
private final Defaults defaults = new Defaults();
public String getTenantHeader() {
return tenantHeader;
}
public void setTenantHeader(String tenantHeader) {
this.tenantHeader = tenantHeader;
}
public String getUserHeader() {
return userHeader;
}
public void setUserHeader(String userHeader) {
this.userHeader = userHeader;
}
public Defaults getDefaults() {
return defaults;
}
public static class Defaults {
private String tenantId = "tenant-apitest";
private String userId = "apitest-user";
private List<String> roles = new ArrayList<>();
private List<String> permissions = new ArrayList<>();
public String getTenantId() {
return tenantId;
}
public void setTenantId(String tenantId) {
this.tenantId = tenantId;
}
public String getUserId() {
return userId;
}
public void setUserId(String userId) {
this.userId = userId;
}
public List<String> getRoles() {
return roles;
}
public void setRoles(List<String> roles) {
this.roles = roles;
}
public List<String> getPermissions() {
return permissions;
}
public void setPermissions(List<String> permissions) {
this.permissions = permissions;
}
}
}

View File

@@ -12,7 +12,6 @@ import java.util.Set;
import javax.annotation.Resource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
@@ -32,7 +31,6 @@ import org.springframework.security.web.authentication.UsernamePasswordAuthentic
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
@Profile("!apitest")
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {
@Resource

View File

@@ -1,96 +0,0 @@
package com.starry.admin.common.security.filter;
import com.starry.admin.common.domain.LoginUser;
import com.starry.admin.common.security.config.ApiTestSecurityProperties;
import com.starry.admin.modules.system.module.entity.SysUserEntity;
import com.starry.common.constant.SecurityConstants;
import com.starry.common.context.CustomSecurityContextHolder;
import java.io.IOException;
import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.Set;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.http.HttpStatus;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
public class ApiTestAuthenticationFilter extends OncePerRequestFilter {
private final ApiTestSecurityProperties properties;
public ApiTestAuthenticationFilter(ApiTestSecurityProperties properties) {
this.properties = properties;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
String requestedUser = request.getHeader(properties.getUserHeader());
String requestedTenant = request.getHeader(properties.getTenantHeader());
String userId = StringUtils.hasText(requestedUser) ? requestedUser : properties.getDefaults().getUserId();
String tenantId = StringUtils.hasText(requestedTenant) ? requestedTenant : properties.getDefaults().getTenantId();
if (!StringUtils.hasText(userId) || !StringUtils.hasText(tenantId)) {
response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.setContentType("application/json");
response.getWriter().write("{\"code\":401,\"message\":\"Missing test user or tenant header\"}");
response.getWriter().flush();
return;
}
try {
LoginUser loginUser = buildLoginUser(userId, tenantId);
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(loginUser, null,
Collections.emptyList());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
CustomSecurityContextHolder.set(SecurityConstants.DETAILS_USER_ID, userId);
CustomSecurityContextHolder.set(SecurityConstants.DETAILS_USERNAME, userId);
CustomSecurityContextHolder.setTenantId(tenantId);
CustomSecurityContextHolder.setPermission(String.join(",", loginUser.getPermissions()));
filterChain.doFilter(request, response);
} finally {
CustomSecurityContextHolder.remove();
SecurityContextHolder.clearContext();
}
}
private LoginUser buildLoginUser(String userId, String tenantId) {
SysUserEntity sysUser = new SysUserEntity();
sysUser.setUserId(userId);
sysUser.setUserCode(userId);
sysUser.setRealName(userId);
sysUser.setTenantId(tenantId);
sysUser.setSuperAdmin(Boolean.FALSE);
sysUser.setStatus(0);
LoginUser loginUser = new LoginUser();
loginUser.setUser(sysUser);
loginUser.setUserId(userId);
loginUser.setUserName(userId);
loginUser.setToken("apitest-" + userId + "-" + tenantId);
loginUser.setLoginTime(System.currentTimeMillis());
loginUser.setExpireTime(System.currentTimeMillis() + 3600_000);
loginUser.setTenantEndDate(new Date(System.currentTimeMillis() + 3600_000));
loginUser.setTenantStatus(0);
Set<String> roles = new HashSet<>(properties.getDefaults().getRoles());
Set<String> permissions = new HashSet<>(properties.getDefaults().getPermissions());
loginUser.setRoles(roles);
loginUser.setPermissions(permissions);
loginUser.setCurrentRole(roles.stream().findFirst().orElse(null));
return loginUser;
}
}

View File

@@ -74,8 +74,8 @@ public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
*/
Set<String> noLoginPathRequired = new HashSet<>(Arrays.asList("/wx/common/area/tree", "/wx/common/file/upload",
"/wx/common/audio/upload", "/wx/oauth2/getConfigAddress", "/wx/clerk/user/queryByPage",
"/wx/clerk/user/queryGiftById", "/wx/clerk/user/queryPriceById", "/wx/clerk/user/queryTrendsById",
"/wx/clerk/user/queryEvaluateById"));
"wx/clerk/user/queryGiftById", "/wx/clerk/user/queryPriceById", "/wx/clerk/user/queryTrendsById",
"wx/clerk/user/queryEvaluateById"));
@Override
protected void doFilterInternal(@NotNull HttpServletRequest httpServletRequest,

View File

@@ -1,14 +0,0 @@
package com.starry.admin.modules.blindbox.config;
import java.time.Clock;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class BlindBoxConfiguration {
@Bean
public Clock systemClock() {
return Clock.systemDefaultZone();
}
}

View File

@@ -1,108 +0,0 @@
package com.starry.admin.modules.blindbox.controller;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.starry.admin.common.exception.CustomException;
import com.starry.admin.modules.blindbox.module.constant.BlindBoxConfigStatus;
import com.starry.admin.modules.blindbox.module.entity.BlindBoxConfigEntity;
import com.starry.admin.modules.blindbox.service.BlindBoxConfigService;
import com.starry.admin.utils.SecurityUtils;
import com.starry.common.result.R;
import com.starry.common.utils.IdUtils;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import java.math.BigDecimal;
import java.util.List;
import javax.annotation.Resource;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
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;
@Api(tags = "盲盒配置管理")
@RestController
@RequestMapping("/play/blind-box/config")
public class BlindBoxConfigController {
@Resource
private BlindBoxConfigService blindBoxConfigService;
@ApiOperation("查询盲盒列表")
@GetMapping
public R list(@RequestParam(required = false) Integer status) {
String tenantId = SecurityUtils.getTenantId();
LambdaQueryWrapper<BlindBoxConfigEntity> query = Wrappers.lambdaQuery(BlindBoxConfigEntity.class)
.eq(BlindBoxConfigEntity::getTenantId, tenantId)
.eq(BlindBoxConfigEntity::getDeleted, Boolean.FALSE)
.orderByDesc(BlindBoxConfigEntity::getCreatedTime);
if (status != null) {
query.eq(BlindBoxConfigEntity::getStatus, status);
}
List<BlindBoxConfigEntity> configs = blindBoxConfigService.list(query);
return R.ok(configs);
}
@ApiOperation("盲盒详情")
@GetMapping("/{id}")
public R detail(@PathVariable String id) {
BlindBoxConfigEntity entity = blindBoxConfigService.requireById(id);
return R.ok(entity);
}
@ApiOperation("新增盲盒")
@PostMapping
public R create(@RequestBody BlindBoxConfigEntity body) {
if (StrUtil.isBlank(body.getName())) {
throw new CustomException("盲盒名称不能为空");
}
validatePrice(body.getPrice());
body.setId(IdUtils.getUuid());
body.setTenantId(SecurityUtils.getTenantId());
body.setDeleted(Boolean.FALSE);
if (body.getStatus() == null) {
body.setStatus(BlindBoxConfigStatus.ENABLED.getCode());
}
blindBoxConfigService.save(body);
return R.ok(body.getId());
}
@ApiOperation("更新盲盒")
@PutMapping("/{id}")
public R update(@PathVariable String id, @RequestBody BlindBoxConfigEntity body) {
validatePrice(body.getPrice());
BlindBoxConfigEntity existing = blindBoxConfigService.requireById(id);
if (!SecurityUtils.getTenantId().equals(existing.getTenantId())) {
throw new CustomException("无权操作该盲盒");
}
existing.setName(body.getName());
existing.setCoverUrl(body.getCoverUrl());
existing.setDescription(body.getDescription());
existing.setPrice(body.getPrice());
existing.setStatus(body.getStatus());
blindBoxConfigService.updateById(existing);
return R.ok();
}
@ApiOperation("删除盲盒")
@DeleteMapping("/{id}")
public R delete(@PathVariable String id) {
BlindBoxConfigEntity existing = blindBoxConfigService.requireById(id);
if (!SecurityUtils.getTenantId().equals(existing.getTenantId())) {
throw new CustomException("无权操作该盲盒");
}
blindBoxConfigService.removeById(id);
return R.ok();
}
private void validatePrice(BigDecimal price) {
if (price == null || price.compareTo(BigDecimal.ZERO) <= 0) {
throw new CustomException("盲盒价格必须大于0");
}
}
}

View File

@@ -1,77 +0,0 @@
package com.starry.admin.modules.blindbox.controller;
import com.starry.admin.common.exception.CustomException;
import com.starry.admin.modules.blindbox.module.dto.BlindBoxPoolImportRow;
import com.starry.admin.modules.blindbox.module.dto.BlindBoxPoolUpsertRequest;
import com.starry.admin.modules.blindbox.service.BlindBoxPoolAdminService;
import com.starry.admin.utils.ExcelUtils;
import com.starry.common.result.R;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import java.io.IOException;
import java.util.List;
import javax.annotation.Resource;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
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 org.springframework.web.multipart.MultipartFile;
@Api(tags = "盲盒奖池管理")
@RestController
@RequestMapping("/play/blind-box/pool")
public class BlindBoxPoolController {
@Resource
private BlindBoxPoolAdminService blindBoxPoolAdminService;
@ApiOperation("查询盲盒奖池列表")
@GetMapping
public R list(@RequestParam String blindBoxId) {
return R.ok(blindBoxPoolAdminService.list(blindBoxId));
}
@ApiOperation("导入盲盒奖池配置")
@PostMapping("/{blindBoxId}/import")
public R importPool(@PathVariable String blindBoxId, @RequestParam("file") MultipartFile file)
throws IOException {
if (file == null || file.isEmpty()) {
throw new CustomException("上传文件不能为空");
}
List<?> rows = ExcelUtils.importEasyExcel(file.getInputStream(), BlindBoxPoolImportRow.class);
@SuppressWarnings("unchecked")
List<BlindBoxPoolImportRow> importRows = (List<BlindBoxPoolImportRow>) rows;
blindBoxPoolAdminService.replacePool(blindBoxId, importRows);
return R.ok();
}
@ApiOperation("删除盲盒奖池项")
@DeleteMapping("/{id}")
public R remove(@PathVariable Long id) {
blindBoxPoolAdminService.removeById(id);
return R.ok();
}
@ApiOperation("新增盲盒奖池项")
@PostMapping
public R create(@RequestBody BlindBoxPoolUpsertRequest body) {
return R.ok(blindBoxPoolAdminService.create(body != null ? body.getBlindBoxId() : null, body));
}
@ApiOperation("更新盲盒奖池项")
@PutMapping("/{id}")
public R update(@PathVariable Long id, @RequestBody BlindBoxPoolUpsertRequest body) {
return R.ok(blindBoxPoolAdminService.update(id, body));
}
@ApiOperation("查询可用中奖礼物")
@GetMapping("/gifts")
public R giftOptions(@RequestParam(required = false) String keyword) {
return R.ok(blindBoxPoolAdminService.listGiftOptions(keyword));
}
}

View File

@@ -1,9 +0,0 @@
package com.starry.admin.modules.blindbox.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.starry.admin.modules.blindbox.module.entity.BlindBoxConfigEntity;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface BlindBoxConfigMapper extends BaseMapper<BlindBoxConfigEntity> {
}

View File

@@ -1,54 +0,0 @@
package com.starry.admin.modules.blindbox.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.starry.admin.modules.blindbox.module.dto.BlindBoxCandidate;
import com.starry.admin.modules.blindbox.module.entity.BlindBoxPoolEntity;
import java.time.LocalDateTime;
import java.util.List;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Update;
@Mapper
public interface BlindBoxPoolMapper extends BaseMapper<BlindBoxPoolEntity> {
@Select({
"SELECT id AS poolId,",
" tenant_id AS tenantId,",
" blind_box_id AS blindBoxId,",
" reward_gift_id AS rewardGiftId,",
" reward_price AS rewardPrice,",
" weight,",
" remaining_stock AS remainingStock",
"FROM blind_box_pool",
"WHERE tenant_id = #{tenantId}",
" AND blind_box_id = #{blindBoxId}",
" AND status = 1",
" AND deleted = 0",
" AND (valid_from IS NULL OR valid_from <= #{now})",
" AND (valid_to IS NULL OR valid_to >= #{now})",
" AND (remaining_stock IS NULL OR remaining_stock > 0)"
})
List<BlindBoxCandidate> listActiveEntries(
@Param("tenantId") String tenantId,
@Param("blindBoxId") String blindBoxId,
@Param("now") LocalDateTime now);
@Update({
"UPDATE blind_box_pool",
"SET remaining_stock = CASE",
" WHEN remaining_stock IS NULL THEN NULL",
" ELSE remaining_stock - 1",
" END",
"WHERE tenant_id = #{tenantId}",
" AND id = #{poolId}",
" AND reward_gift_id = #{rewardGiftId}",
" AND deleted = 0",
" AND (remaining_stock IS NULL OR remaining_stock > 0)"
})
int consumeRewardStock(
@Param("tenantId") String tenantId,
@Param("poolId") Long poolId,
@Param("rewardGiftId") String rewardGiftId);
}

View File

@@ -1,46 +0,0 @@
package com.starry.admin.modules.blindbox.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.starry.admin.modules.blindbox.module.entity.BlindBoxRewardEntity;
import java.time.LocalDateTime;
import java.util.List;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Update;
@Mapper
public interface BlindBoxRewardMapper extends BaseMapper<BlindBoxRewardEntity> {
@Select("SELECT * FROM blind_box_reward WHERE id = #{id} FOR UPDATE")
BlindBoxRewardEntity lockByIdForUpdate(@Param("id") String id);
@Update({
"UPDATE blind_box_reward",
"SET status = 'USED',",
" used_order_id = #{orderId},",
" used_clerk_id = #{clerkId},",
" used_time = #{usedTime},",
" version = version + 1",
"WHERE id = #{id}",
" AND status = 'UNUSED'"
})
int markUsed(
@Param("id") String id,
@Param("clerkId") String clerkId,
@Param("orderId") String orderId,
@Param("usedTime") LocalDateTime usedTime);
@Select({
"SELECT * FROM blind_box_reward",
"WHERE tenant_id = #{tenantId}",
" AND customer_id = #{customerId}",
" AND deleted = 0",
" AND (#{status} IS NULL OR status = #{status})",
"ORDER BY created_time DESC"
})
List<BlindBoxRewardEntity> listByCustomer(
@Param("tenantId") String tenantId,
@Param("customerId") String customerId,
@Param("status") String status);
}

View File

@@ -1,19 +0,0 @@
package com.starry.admin.modules.blindbox.module.constant;
/**
* 盲盒(配置)上下架状态。
*/
public enum BlindBoxConfigStatus {
ENABLED(1),
DISABLED(0);
private final int code;
BlindBoxConfigStatus(int code) {
this.code = code;
}
public int getCode() {
return code;
}
}

View File

@@ -1,19 +0,0 @@
package com.starry.admin.modules.blindbox.module.constant;
/**
* 盲盒奖池项启停状态。
*/
public enum BlindBoxPoolStatus {
ENABLED(1),
DISABLED(0);
private final int code;
BlindBoxPoolStatus(int code) {
this.code = code;
}
public int getCode() {
return code;
}
}

View File

@@ -1,17 +0,0 @@
package com.starry.admin.modules.blindbox.module.constant;
public enum BlindBoxRewardStatus {
UNUSED("UNUSED"),
USED("USED"),
REFUNDED("REFUNDED");
private final String code;
BlindBoxRewardStatus(String code) {
this.code = code;
}
public String getCode() {
return code;
}
}

View File

@@ -1,46 +0,0 @@
package com.starry.admin.modules.blindbox.module.dto;
import java.math.BigDecimal;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 盲盒抽奖候选项,封装奖池记录必要信息。
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class BlindBoxCandidate {
/**
* 奖池记录主键。
*/
private Long poolId;
private String tenantId;
private String blindBoxId;
private String rewardGiftId;
private BigDecimal rewardPrice;
private int weight;
/**
* 剩余库存null 表示不限量。
*/
private Integer remainingStock;
public static BlindBoxCandidate of(
Long poolId,
String tenantId,
String blindBoxId,
String rewardGiftId,
BigDecimal rewardPrice,
int weight,
Integer remainingStock) {
return new BlindBoxCandidate(poolId, tenantId, blindBoxId, rewardGiftId, rewardPrice, weight, remainingStock);
}
}

View File

@@ -1,23 +0,0 @@
package com.starry.admin.modules.blindbox.module.dto;
import java.math.BigDecimal;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 盲盒奖池可选礼物选项。
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class BlindBoxGiftOption {
private String id;
private String name;
private BigDecimal price;
private String imageUrl;
}

View File

@@ -1,33 +0,0 @@
package com.starry.admin.modules.blindbox.module.dto;
import com.alibaba.excel.annotation.ExcelProperty;
import java.math.BigDecimal;
import lombok.Data;
/**
* 盲盒奖池导入模板行。
*/
@Data
public class BlindBoxPoolImportRow {
@ExcelProperty("中奖礼物名称")
private String rewardGiftName;
@ExcelProperty("权重")
private Integer weight;
@ExcelProperty("初始库存")
private Integer remainingStock;
@ExcelProperty("生效时间")
private String validFrom;
@ExcelProperty("失效时间")
private String validTo;
@ExcelProperty("状态")
private Integer status;
@ExcelProperty("奖品售价(可选)")
private BigDecimal overrideRewardPrice;
}

View File

@@ -1,28 +0,0 @@
package com.starry.admin.modules.blindbox.module.dto;
import com.fasterxml.jackson.annotation.JsonFormat;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import lombok.Data;
@Data
public class BlindBoxPoolUpsertRequest {
private String blindBoxId;
private String rewardGiftId;
private BigDecimal rewardPrice;
private Integer weight;
private Integer remainingStock;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private LocalDateTime validFrom;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private LocalDateTime validTo;
private Integer status;
}

View File

@@ -1,34 +0,0 @@
package com.starry.admin.modules.blindbox.module.dto;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import lombok.Data;
/**
* 盲盒奖池前端展示对象。
*/
@Data
public class BlindBoxPoolView {
private Long id;
private String blindBoxId;
private String blindBoxName;
private String rewardGiftId;
private String rewardGiftName;
private BigDecimal rewardPrice;
private Integer weight;
private Integer remainingStock;
private LocalDateTime validFrom;
private LocalDateTime validTo;
private Integer status;
}

View File

@@ -1,30 +0,0 @@
package com.starry.admin.modules.blindbox.module.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import com.starry.common.domain.BaseEntity;
import java.math.BigDecimal;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 盲盒配置实体。
*/
@Data
@EqualsAndHashCode(callSuper = false)
@TableName("blind_box_config")
public class BlindBoxConfigEntity extends BaseEntity<BlindBoxConfigEntity> {
private String id;
private String tenantId;
private String name;
private String coverUrl;
private String description;
private BigDecimal price;
private Integer status;
}

View File

@@ -1,45 +0,0 @@
package com.starry.admin.modules.blindbox.module.entity;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.starry.common.domain.BaseEntity;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.springframework.format.annotation.DateTimeFormat;
/**
* 盲盒奖池配置实体。
*/
@Data
@EqualsAndHashCode(callSuper = false)
@TableName("blind_box_pool")
public class BlindBoxPoolEntity extends BaseEntity<BlindBoxPoolEntity> {
private Long id;
private String tenantId;
@TableField("blind_box_id")
private String blindBoxId;
private String rewardGiftId;
private BigDecimal rewardPrice;
private Integer weight;
private Integer remainingStock;
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private LocalDateTime validFrom;
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private LocalDateTime validTo;
private Integer status;
}

View File

@@ -1,44 +0,0 @@
package com.starry.admin.modules.blindbox.module.entity;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.starry.common.domain.BaseEntity;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.springframework.format.annotation.DateTimeFormat;
@Data
@EqualsAndHashCode(callSuper = false)
@TableName("blind_box_reward")
public class BlindBoxRewardEntity extends BaseEntity<BlindBoxRewardEntity> {
private String id;
private String tenantId;
private String customerId;
@TableField("blind_box_id")
private String blindBoxId;
private String rewardGiftId;
private BigDecimal rewardPrice;
private BigDecimal boxPrice;
private BigDecimal subsidyAmount;
private Integer rewardStockSnapshot;
private String seed;
private String status;
private String createdByOrder;
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private LocalDateTime expiresAt;
private String usedOrderId;
private String usedClerkId;
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private LocalDateTime usedTime;
}

View File

@@ -1,12 +0,0 @@
package com.starry.admin.modules.blindbox.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.starry.admin.modules.blindbox.module.entity.BlindBoxConfigEntity;
import java.util.List;
public interface BlindBoxConfigService extends IService<BlindBoxConfigEntity> {
BlindBoxConfigEntity requireById(String id);
List<BlindBoxConfigEntity> listActiveByTenant(String tenantId);
}

View File

@@ -1,85 +0,0 @@
package com.starry.admin.modules.blindbox.service;
import com.starry.admin.common.exception.CustomException;
import com.starry.admin.modules.blindbox.module.entity.BlindBoxRewardEntity;
import com.starry.admin.modules.order.module.constant.OrderConstant;
import com.starry.admin.modules.order.module.dto.CommodityInfo;
import com.starry.admin.modules.order.module.dto.OrderCreationContext;
import com.starry.admin.modules.order.module.dto.OrderPlacementCommand;
import com.starry.admin.modules.order.module.dto.OrderPlacementResult;
import com.starry.admin.modules.order.module.dto.PaymentInfo;
import com.starry.admin.modules.order.service.IOrderLifecycleService;
import com.starry.admin.modules.order.service.IPlayOrderInfoService;
import com.starry.admin.modules.shop.module.entity.PlayGiftInfoEntity;
import com.starry.admin.modules.shop.service.IPlayClerkGiftInfoService;
import com.starry.admin.modules.shop.service.IPlayGiftInfoService;
import com.starry.common.utils.IdUtils;
import java.math.BigDecimal;
import java.util.Collections;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@RequiredArgsConstructor
public class BlindBoxDispatchService {
private final IOrderLifecycleService orderLifecycleService;
private final IPlayOrderInfoService orderInfoService;
private final IPlayGiftInfoService giftInfoService;
private final IPlayClerkGiftInfoService clerkGiftInfoService;
@Transactional(rollbackFor = Exception.class)
public OrderPlacementResult dispatchRewardOrder(BlindBoxRewardEntity reward, String clerkId) {
PlayGiftInfoEntity giftInfo = giftInfoService.selectPlayGiftInfoById(reward.getRewardGiftId());
if (giftInfo == null) {
throw new CustomException("奖励礼物不存在或已下架");
}
BigDecimal rewardPrice = reward.getRewardPrice();
PaymentInfo paymentInfo = PaymentInfo.builder()
.orderMoney(rewardPrice)
.finalAmount(rewardPrice)
.discountAmount(BigDecimal.ZERO)
.couponIds(Collections.emptyList())
.payMethod(OrderConstant.PayMethod.BALANCE.getCode())
.paymentSource(OrderConstant.PaymentSource.BLIND_BOX)
.build();
CommodityInfo commodityInfo = CommodityInfo.builder()
.commodityId(giftInfo.getId())
.commodityType(OrderConstant.CommodityType.GIFT)
.commodityPrice(giftInfo.getPrice())
.commodityName(giftInfo.getName())
.commodityNumber("1")
.serviceDuration("")
.build();
OrderCreationContext context = OrderCreationContext.builder()
.orderId(IdUtils.getUuid())
.orderNo(orderInfoService.getOrderNo())
.orderStatus(OrderConstant.OrderStatus.COMPLETED)
.orderType(OrderConstant.OrderType.GIFT)
.placeType(OrderConstant.PlaceType.REWARD)
.rewardType(OrderConstant.RewardType.GIFT)
.paymentSource(OrderConstant.PaymentSource.BLIND_BOX)
.sourceRewardId(reward.getId())
.paymentInfo(paymentInfo)
.commodityInfo(commodityInfo)
.purchaserBy(reward.getCustomerId())
.acceptBy(clerkId)
.creatorActor(OrderConstant.OrderActor.CUSTOMER)
.creatorId(reward.getCustomerId())
.remark("盲盒奖励兑现")
.build();
OrderPlacementResult result = orderLifecycleService.placeOrder(OrderPlacementCommand.builder()
.orderContext(context)
.balanceOperationAction("盲盒奖励兑现")
.build());
if (clerkId != null) {
clerkGiftInfoService.incrementGiftCount(clerkId, giftInfo.getId(), reward.getTenantId(), 1);
}
return result;
}
}

View File

@@ -1,22 +0,0 @@
package com.starry.admin.modules.blindbox.service;
import com.starry.admin.common.exception.CustomException;
import com.starry.admin.modules.blindbox.mapper.BlindBoxPoolMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@RequiredArgsConstructor
public class BlindBoxInventoryService {
private final BlindBoxPoolMapper blindBoxPoolMapper;
@Transactional(rollbackFor = Exception.class)
public void reserveRewardStock(String tenantId, Long poolId, String rewardGiftId) {
int affected = blindBoxPoolMapper.consumeRewardStock(tenantId, poolId, rewardGiftId);
if (affected <= 0) {
throw new CustomException("盲盒奖池库存不足,请稍后再试");
}
}
}

View File

@@ -1,403 +0,0 @@
package com.starry.admin.modules.blindbox.service;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.starry.admin.common.exception.CustomException;
import com.starry.admin.modules.blindbox.mapper.BlindBoxPoolMapper;
import com.starry.admin.modules.blindbox.module.constant.BlindBoxPoolStatus;
import com.starry.admin.modules.blindbox.module.dto.BlindBoxGiftOption;
import com.starry.admin.modules.blindbox.module.dto.BlindBoxPoolImportRow;
import com.starry.admin.modules.blindbox.module.dto.BlindBoxPoolUpsertRequest;
import com.starry.admin.modules.blindbox.module.dto.BlindBoxPoolView;
import com.starry.admin.modules.blindbox.module.entity.BlindBoxConfigEntity;
import com.starry.admin.modules.blindbox.module.entity.BlindBoxPoolEntity;
import com.starry.admin.modules.shop.mapper.PlayGiftInfoMapper;
import com.starry.admin.modules.shop.module.constant.GiftHistory;
import com.starry.admin.modules.shop.module.constant.GiftState;
import com.starry.admin.modules.shop.module.constant.GiftType;
import com.starry.admin.modules.shop.module.entity.PlayGiftInfoEntity;
import com.starry.admin.utils.SecurityUtils;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
@Service
@RequiredArgsConstructor
public class BlindBoxPoolAdminService {
private static final DateTimeFormatter DATE_TIME_FORMATTER =
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
private final BlindBoxPoolMapper blindBoxPoolMapper;
private final BlindBoxConfigService blindBoxConfigService;
private final PlayGiftInfoMapper playGiftInfoMapper;
public List<BlindBoxPoolView> list(String blindBoxId) {
String tenantId = SecurityUtils.getTenantId();
if (StrUtil.isBlank(tenantId) || StrUtil.isBlank(blindBoxId)) {
return Collections.emptyList();
}
BlindBoxConfigEntity config = blindBoxConfigService.requireById(blindBoxId);
if (!tenantId.equals(config.getTenantId())) {
throw new CustomException("盲盒不存在或已被移除");
}
LambdaQueryWrapper<BlindBoxPoolEntity> query = Wrappers.lambdaQuery(BlindBoxPoolEntity.class)
.eq(BlindBoxPoolEntity::getTenantId, tenantId)
.eq(BlindBoxPoolEntity::getBlindBoxId, blindBoxId)
.eq(BlindBoxPoolEntity::getDeleted, Boolean.FALSE)
.orderByAsc(BlindBoxPoolEntity::getId);
List<BlindBoxPoolEntity> entities = blindBoxPoolMapper.selectList(query);
if (CollUtil.isEmpty(entities)) {
return Collections.emptyList();
}
Set<String> rewardIds = entities.stream()
.map(BlindBoxPoolEntity::getRewardGiftId)
.collect(Collectors.toSet());
List<PlayGiftInfoEntity> gifts = playGiftInfoMapper.selectBatchIds(rewardIds);
Map<String, PlayGiftInfoEntity> giftMap = gifts.stream()
.collect(Collectors.toMap(PlayGiftInfoEntity::getId, Function.identity()));
return entities.stream()
.map(entity -> toView(entity, config, giftMap.get(entity.getRewardGiftId())))
.collect(Collectors.toList());
}
public List<BlindBoxGiftOption> listGiftOptions(String keyword) {
String tenantId = SecurityUtils.getTenantId();
if (StrUtil.isBlank(tenantId)) {
return Collections.emptyList();
}
LambdaQueryWrapper<PlayGiftInfoEntity> query = Wrappers.lambdaQuery(PlayGiftInfoEntity.class)
.eq(PlayGiftInfoEntity::getTenantId, tenantId)
.eq(PlayGiftInfoEntity::getHistory, GiftHistory.CURRENT.getCode())
.eq(PlayGiftInfoEntity::getState, GiftState.ACTIVE.getCode())
.eq(PlayGiftInfoEntity::getType, GiftType.NORMAL.getCode())
.eq(PlayGiftInfoEntity::getDeleted, Boolean.FALSE);
if (StrUtil.isNotBlank(keyword)) {
query.like(PlayGiftInfoEntity::getName, keyword.trim());
}
query.orderByAsc(PlayGiftInfoEntity::getName);
List<PlayGiftInfoEntity> gifts = playGiftInfoMapper.selectList(query);
if (CollUtil.isEmpty(gifts)) {
return Collections.emptyList();
}
return gifts.stream()
.map(gift -> new BlindBoxGiftOption(gift.getId(), gift.getName(), gift.getPrice(), gift.getUrl()))
.collect(Collectors.toList());
}
@Transactional(rollbackFor = Exception.class)
public BlindBoxPoolView create(String blindBoxId, BlindBoxPoolUpsertRequest request) {
if (request == null) {
throw new CustomException("参数不能为空");
}
String tenantId = requireTenantId();
String targetBlindBoxId = StrUtil.isNotBlank(blindBoxId) ? blindBoxId : request.getBlindBoxId();
if (StrUtil.isBlank(targetBlindBoxId)) {
throw new CustomException("盲盒ID不能为空");
}
BlindBoxConfigEntity config = blindBoxConfigService.requireById(targetBlindBoxId);
if (!tenantId.equals(config.getTenantId())) {
throw new CustomException("盲盒不存在或已被移除");
}
PlayGiftInfoEntity rewardGift = requireRewardGift(tenantId, request.getRewardGiftId());
validateTimeRange(request.getValidFrom(), request.getValidTo());
Integer weight = requirePositiveWeight(request.getWeight(), rewardGift.getName());
Integer remainingStock = normalizeRemainingStock(request.getRemainingStock(), rewardGift.getName());
Integer status = resolveStatus(request.getStatus());
BigDecimal rewardPrice = resolveRewardPrice(request.getRewardPrice(), rewardGift.getPrice());
String operatorId = currentUserIdSafely();
BlindBoxPoolEntity entity = new BlindBoxPoolEntity();
entity.setTenantId(tenantId);
entity.setBlindBoxId(targetBlindBoxId);
entity.setRewardGiftId(rewardGift.getId());
entity.setRewardPrice(rewardPrice);
entity.setWeight(weight);
entity.setRemainingStock(remainingStock);
entity.setValidFrom(request.getValidFrom());
entity.setValidTo(request.getValidTo());
entity.setStatus(status);
entity.setDeleted(Boolean.FALSE);
entity.setCreatedBy(operatorId);
entity.setUpdatedBy(operatorId);
blindBoxPoolMapper.insert(entity);
return toView(entity, config, rewardGift);
}
@Transactional(rollbackFor = Exception.class)
public BlindBoxPoolView update(Long id, BlindBoxPoolUpsertRequest request) {
if (id == null) {
throw new CustomException("记录ID不能为空");
}
if (request == null) {
throw new CustomException("参数不能为空");
}
String tenantId = requireTenantId();
BlindBoxPoolEntity existing = blindBoxPoolMapper.selectById(id);
if (existing == null || Boolean.TRUE.equals(existing.getDeleted())) {
throw new CustomException("记录不存在或已删除");
}
if (!tenantId.equals(existing.getTenantId())) {
throw new CustomException("无权操作该记录");
}
String targetBlindBoxId = StrUtil.isNotBlank(request.getBlindBoxId())
? request.getBlindBoxId()
: existing.getBlindBoxId();
BlindBoxConfigEntity config = blindBoxConfigService.requireById(targetBlindBoxId);
if (!tenantId.equals(config.getTenantId())) {
throw new CustomException("盲盒不存在或已被移除");
}
PlayGiftInfoEntity rewardGift = requireRewardGiftForUpdate(tenantId, request.getRewardGiftId());
validateTimeRange(request.getValidFrom(), request.getValidTo());
Integer weight = requirePositiveWeight(request.getWeight(), rewardGift.getName());
Integer remainingStock = normalizeRemainingStock(request.getRemainingStock(), rewardGift.getName());
Integer status = resolveStatus(request.getStatus());
BigDecimal rewardPrice = resolveRewardPrice(request.getRewardPrice(), rewardGift.getPrice());
String operatorId = currentUserIdSafely();
existing.setBlindBoxId(targetBlindBoxId);
existing.setRewardGiftId(rewardGift.getId());
existing.setRewardPrice(rewardPrice);
existing.setWeight(weight);
existing.setRemainingStock(remainingStock);
existing.setValidFrom(request.getValidFrom());
existing.setValidTo(request.getValidTo());
existing.setStatus(status);
existing.setUpdatedBy(operatorId);
blindBoxPoolMapper.updateById(existing);
return toView(existing, config, rewardGift);
}
@Transactional(rollbackFor = Exception.class)
public void replacePool(String blindBoxId, List<BlindBoxPoolImportRow> rows) {
if (StrUtil.isBlank(blindBoxId)) {
throw new CustomException("盲盒ID不能为空");
}
if (CollUtil.isEmpty(rows)) {
throw new CustomException("导入数据不能为空");
}
String tenantId = SecurityUtils.getTenantId();
if (StrUtil.isBlank(tenantId)) {
throw new CustomException("租户信息缺失");
}
BlindBoxConfigEntity config = blindBoxConfigService.requireById(blindBoxId);
if (!tenantId.equals(config.getTenantId())) {
throw new CustomException("盲盒不存在或已被移除");
}
List<String> rewardNames = rows.stream()
.map(BlindBoxPoolImportRow::getRewardGiftName)
.filter(StrUtil::isNotBlank)
.distinct()
.collect(Collectors.toList());
if (CollUtil.isEmpty(rewardNames)) {
throw new CustomException("导入数据缺少中奖礼物名称");
}
LambdaQueryWrapper<PlayGiftInfoEntity> rewardQuery = Wrappers.lambdaQuery(PlayGiftInfoEntity.class)
.eq(PlayGiftInfoEntity::getTenantId, tenantId)
.eq(PlayGiftInfoEntity::getHistory, GiftHistory.CURRENT.getCode())
.eq(PlayGiftInfoEntity::getState, GiftState.ACTIVE.getCode())
.eq(PlayGiftInfoEntity::getType, GiftType.NORMAL.getCode())
.in(PlayGiftInfoEntity::getName, rewardNames);
List<PlayGiftInfoEntity> rewardGifts = playGiftInfoMapper.selectList(rewardQuery);
Map<String, List<PlayGiftInfoEntity>> rewardsByName = rewardGifts.stream()
.collect(Collectors.groupingBy(PlayGiftInfoEntity::getName));
List<String> duplicateNames = rewardsByName.entrySet().stream()
.filter(entry -> entry.getValue().size() > 1)
.map(Map.Entry::getKey)
.collect(Collectors.toList());
if (CollUtil.isNotEmpty(duplicateNames)) {
throw new CustomException("存在同名礼物,无法区分:" + String.join("", duplicateNames));
}
Map<String, PlayGiftInfoEntity> rewardMap = rewardsByName.entrySet().stream()
.collect(Collectors.toMap(Map.Entry::getKey, entry -> entry.getValue().get(0)));
String operatorId = currentUserIdSafely();
List<BlindBoxPoolEntity> toInsert = new ArrayList<>(rows.size());
for (BlindBoxPoolImportRow row : rows) {
if (StrUtil.isBlank(row.getRewardGiftName())) {
continue;
}
PlayGiftInfoEntity rewardGift = rewardMap.get(row.getRewardGiftName());
if (rewardGift == null) {
throw new CustomException("中奖礼物不存在: " + row.getRewardGiftName());
}
Integer weight = row.getWeight();
if (weight == null || weight <= 0) {
throw new CustomException("礼物 " + row.getRewardGiftName() + " 权重必须为正整数");
}
Integer remainingStock = row.getRemainingStock();
if (remainingStock != null && remainingStock < 0) {
throw new CustomException("礼物 " + row.getRewardGiftName() + " 库存不能为负数");
}
Integer status = row.getStatus() == null
? BlindBoxPoolStatus.ENABLED.getCode()
: row.getStatus();
BlindBoxPoolEntity entity = new BlindBoxPoolEntity();
entity.setTenantId(tenantId);
entity.setBlindBoxId(blindBoxId);
entity.setRewardGiftId(rewardGift.getId());
entity.setRewardPrice(resolveRewardPrice(row.getOverrideRewardPrice(), rewardGift.getPrice()));
entity.setWeight(weight);
entity.setRemainingStock(remainingStock);
entity.setValidFrom(parseDateTime(row.getValidFrom()));
entity.setValidTo(parseDateTime(row.getValidTo()));
entity.setStatus(status);
entity.setDeleted(Boolean.FALSE);
entity.setCreatedBy(operatorId);
entity.setUpdatedBy(operatorId);
toInsert.add(entity);
}
if (CollUtil.isEmpty(toInsert)) {
throw new CustomException("有效导入数据为空");
}
LambdaQueryWrapper<BlindBoxPoolEntity> deleteWrapper = Wrappers.lambdaQuery(BlindBoxPoolEntity.class)
.eq(BlindBoxPoolEntity::getTenantId, tenantId)
.eq(BlindBoxPoolEntity::getBlindBoxId, blindBoxId);
blindBoxPoolMapper.delete(deleteWrapper);
for (BlindBoxPoolEntity entity : toInsert) {
blindBoxPoolMapper.insert(entity);
}
}
@Transactional(rollbackFor = Exception.class)
public void removeById(Long id) {
String tenantId = SecurityUtils.getTenantId();
LambdaQueryWrapper<BlindBoxPoolEntity> wrapper = Wrappers.lambdaQuery(BlindBoxPoolEntity.class)
.eq(BlindBoxPoolEntity::getTenantId, tenantId)
.eq(BlindBoxPoolEntity::getId, id);
int deleted = blindBoxPoolMapper.delete(wrapper);
if (deleted == 0) {
throw new CustomException("记录不存在或已删除");
}
}
private BlindBoxPoolView toView(BlindBoxPoolEntity entity, BlindBoxConfigEntity config,
PlayGiftInfoEntity rewardGift) {
BlindBoxPoolView view = new BlindBoxPoolView();
view.setId(entity.getId());
view.setBlindBoxId(entity.getBlindBoxId());
view.setBlindBoxName(config.getName());
view.setRewardGiftId(entity.getRewardGiftId());
view.setRewardGiftName(rewardGift != null ? rewardGift.getName() : entity.getRewardGiftId());
view.setRewardPrice(entity.getRewardPrice());
view.setWeight(entity.getWeight());
view.setRemainingStock(entity.getRemainingStock());
view.setValidFrom(entity.getValidFrom());
view.setValidTo(entity.getValidTo());
view.setStatus(entity.getStatus());
return view;
}
private LocalDateTime parseDateTime(String value) {
if (!StringUtils.hasText(value)) {
return null;
}
try {
return LocalDateTime.parse(value.trim(), DATE_TIME_FORMATTER);
} catch (DateTimeParseException ex) {
throw new CustomException("日期格式应为 yyyy-MM-dd HH:mm:ss: " + value);
}
}
private BigDecimal resolveRewardPrice(BigDecimal overrideRewardPrice, BigDecimal defaultPrice) {
return overrideRewardPrice != null ? overrideRewardPrice : defaultPrice;
}
private String requireTenantId() {
String tenantId = SecurityUtils.getTenantId();
if (StrUtil.isBlank(tenantId)) {
throw new CustomException("租户信息缺失");
}
return tenantId;
}
private PlayGiftInfoEntity requireRewardGift(String tenantId, String rewardGiftId) {
return requireRewardGift(tenantId, rewardGiftId, true);
}
private PlayGiftInfoEntity requireRewardGiftForUpdate(String tenantId, String rewardGiftId) {
return requireRewardGift(tenantId, rewardGiftId, false);
}
private PlayGiftInfoEntity requireRewardGift(String tenantId, String rewardGiftId, boolean strictAvailability) {
if (StrUtil.isBlank(rewardGiftId)) {
throw new CustomException("请选择中奖礼物");
}
PlayGiftInfoEntity gift = playGiftInfoMapper.selectById(rewardGiftId);
if (gift == null
|| !tenantId.equals(gift.getTenantId())
|| !GiftType.NORMAL.getCode().equals(gift.getType())
|| Boolean.TRUE.equals(gift.getDeleted())) {
throw new CustomException("中奖礼物不存在或已下架");
}
if (strictAvailability) {
if (!GiftHistory.CURRENT.getCode().equals(gift.getHistory())
|| !GiftState.ACTIVE.getCode().equals(gift.getState())) {
throw new CustomException("中奖礼物不存在或已下架");
}
}
return gift;
}
private Integer requirePositiveWeight(Integer weight, String giftName) {
if (weight == null || weight <= 0) {
String name = StrUtil.isBlank(giftName) ? "" : giftName;
throw new CustomException(StrUtil.isBlank(name)
? "奖池权重必须为正整数"
: "礼物 " + name + " 权重必须为正整数");
}
return weight;
}
private Integer normalizeRemainingStock(Integer remainingStock, String giftName) {
if (remainingStock == null) {
return null;
}
if (remainingStock < 0) {
String name = StrUtil.isBlank(giftName) ? "" : giftName;
throw new CustomException(StrUtil.isBlank(name)
? "库存不能为负数"
: "礼物 " + name + " 库存不能为负数");
}
return remainingStock;
}
private Integer resolveStatus(Integer status) {
if (status == null) {
return BlindBoxPoolStatus.ENABLED.getCode();
}
if (status.equals(BlindBoxPoolStatus.ENABLED.getCode())
|| status.equals(BlindBoxPoolStatus.DISABLED.getCode())) {
return status;
}
throw new CustomException("状态参数非法");
}
private void validateTimeRange(LocalDateTime validFrom, LocalDateTime validTo) {
if (validFrom != null && validTo != null && validFrom.isAfter(validTo)) {
throw new CustomException("生效时间不能晚于失效时间");
}
}
private String currentUserIdSafely() {
try {
return SecurityUtils.getUserId();
} catch (RuntimeException ex) {
return null;
}
}
}

View File

@@ -1,176 +0,0 @@
package com.starry.admin.modules.blindbox.service;
import com.starry.admin.common.exception.CustomException;
import com.starry.admin.modules.blindbox.mapper.BlindBoxPoolMapper;
import com.starry.admin.modules.blindbox.mapper.BlindBoxRewardMapper;
import com.starry.admin.modules.blindbox.module.constant.BlindBoxRewardStatus;
import com.starry.admin.modules.blindbox.module.dto.BlindBoxCandidate;
import com.starry.admin.modules.blindbox.module.entity.BlindBoxConfigEntity;
import com.starry.admin.modules.blindbox.module.entity.BlindBoxRewardEntity;
import com.starry.admin.modules.order.module.dto.OrderPlacementResult;
import com.starry.common.utils.IdUtils;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.Clock;
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.ThreadLocalRandom;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;
@Service
public class BlindBoxService {
private final BlindBoxPoolMapper poolMapper;
private final BlindBoxRewardMapper rewardMapper;
private final BlindBoxInventoryService inventoryService;
private final BlindBoxDispatchService dispatchService;
private final BlindBoxConfigService configService;
private final Clock clock;
private final Duration rewardValidity;
private final RandomAdapter randomAdapter;
@Autowired
public BlindBoxService(
BlindBoxPoolMapper poolMapper,
BlindBoxRewardMapper rewardMapper,
BlindBoxInventoryService inventoryService,
BlindBoxDispatchService dispatchService,
BlindBoxConfigService configService) {
this(poolMapper, rewardMapper, inventoryService, dispatchService, configService, Clock.systemDefaultZone(), Duration.ofDays(30), new DefaultRandomAdapter());
}
public BlindBoxService(
BlindBoxPoolMapper poolMapper,
BlindBoxRewardMapper rewardMapper,
BlindBoxInventoryService inventoryService,
BlindBoxDispatchService dispatchService,
BlindBoxConfigService configService,
Clock clock,
Duration rewardValidity,
RandomAdapter randomAdapter) {
this.poolMapper = Objects.requireNonNull(poolMapper, "poolMapper");
this.rewardMapper = Objects.requireNonNull(rewardMapper, "rewardMapper");
this.inventoryService = Objects.requireNonNull(inventoryService, "inventoryService");
this.dispatchService = Objects.requireNonNull(dispatchService, "dispatchService");
this.configService = Objects.requireNonNull(configService, "configService");
this.clock = Objects.requireNonNull(clock, "clock");
this.rewardValidity = Objects.requireNonNull(rewardValidity, "rewardValidity");
this.randomAdapter = Objects.requireNonNull(randomAdapter, "randomAdapter");
}
@Transactional(rollbackFor = Exception.class)
public BlindBoxRewardEntity drawReward(
String tenantId,
String orderId,
String customerId,
String blindBoxId,
String seed) {
LocalDateTime now = LocalDateTime.now(clock);
BlindBoxConfigEntity config = configService.requireById(blindBoxId);
if (!tenantId.equals(config.getTenantId())) {
throw new CustomException("盲盒不存在或已下架");
}
List<BlindBoxCandidate> candidates = poolMapper.listActiveEntries(tenantId, blindBoxId, now);
if (CollectionUtils.isEmpty(candidates)) {
throw new CustomException("盲盒奖池暂无可用奖励");
}
BlindBoxCandidate selected = selectCandidate(candidates);
inventoryService.reserveRewardStock(tenantId, selected.getPoolId(), selected.getRewardGiftId());
BlindBoxRewardEntity entity = new BlindBoxRewardEntity();
entity.setId(IdUtils.getUuid());
entity.setTenantId(tenantId);
entity.setCustomerId(customerId);
entity.setBlindBoxId(blindBoxId);
entity.setRewardGiftId(selected.getRewardGiftId());
entity.setRewardPrice(normalizeMoney(selected.getRewardPrice()));
entity.setBoxPrice(normalizeMoney(config.getPrice()));
entity.setSubsidyAmount(entity.getRewardPrice().subtract(entity.getBoxPrice()));
Integer remainingStock = selected.getRemainingStock();
entity.setRewardStockSnapshot(remainingStock == null ? null : Math.max(remainingStock - 1, 0));
entity.setSeed(seed);
entity.setStatus(BlindBoxRewardStatus.UNUSED.getCode());
entity.setCreatedByOrder(orderId);
entity.setExpiresAt(now.plus(rewardValidity));
entity.setCreatedTime(java.sql.Timestamp.valueOf(now));
entity.setUpdatedTime(java.sql.Timestamp.valueOf(now));
rewardMapper.insert(entity);
return entity;
}
@Transactional(rollbackFor = Exception.class)
public OrderPlacementResult dispatchReward(String rewardId, String clerkId, String customerId) {
BlindBoxRewardEntity reward = rewardMapper.lockByIdForUpdate(rewardId);
if (reward == null) {
throw new CustomException("盲盒奖励不存在");
}
if (customerId != null && !customerId.equals(reward.getCustomerId())) {
throw new CustomException("无权操作该盲盒奖励");
}
LocalDateTime now = LocalDateTime.now(clock);
if (!BlindBoxRewardStatus.UNUSED.getCode().equals(reward.getStatus())) {
throw new CustomException("盲盒奖励已使用");
}
if (reward.getExpiresAt() != null && reward.getExpiresAt().isBefore(now)) {
throw new CustomException("盲盒奖励已过期");
}
OrderPlacementResult result = dispatchService.dispatchRewardOrder(reward, clerkId);
String orderId = result != null && result.getOrder() != null ? result.getOrder().getId() : null;
int affected = rewardMapper.markUsed(rewardId, clerkId, orderId, now);
if (affected <= 0) {
throw new CustomException("盲盒奖励已使用");
}
reward.setStatus(BlindBoxRewardStatus.USED.getCode());
reward.setUsedClerkId(clerkId);
reward.setUsedOrderId(orderId);
reward.setUsedTime(now);
return result;
}
private BlindBoxCandidate selectCandidate(List<BlindBoxCandidate> candidates) {
int totalWeight = candidates.stream()
.mapToInt(BlindBoxCandidate::getWeight)
.sum();
if (totalWeight <= 0) {
throw new CustomException("盲盒奖池权重配置异常");
}
double threshold = randomAdapter.nextDouble() * totalWeight;
int cumulative = 0;
for (BlindBoxCandidate candidate : candidates) {
cumulative += Math.max(candidate.getWeight(), 0);
if (threshold < cumulative) {
return candidate;
}
}
return candidates.get(candidates.size() - 1);
}
private BigDecimal normalizeMoney(BigDecimal value) {
return value == null ? BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP)
: value.setScale(2, RoundingMode.HALF_UP);
}
public java.util.List<BlindBoxRewardEntity> listRewards(String tenantId, String customerId, String status) {
return rewardMapper.listByCustomer(tenantId, customerId, status);
}
public interface RandomAdapter {
double nextDouble();
}
private static class DefaultRandomAdapter implements RandomAdapter {
@Override
public double nextDouble() {
return ThreadLocalRandom.current().nextDouble();
}
}
}

View File

@@ -1,34 +0,0 @@
package com.starry.admin.modules.blindbox.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.starry.admin.common.exception.CustomException;
import com.starry.admin.modules.blindbox.mapper.BlindBoxConfigMapper;
import com.starry.admin.modules.blindbox.module.constant.BlindBoxConfigStatus;
import com.starry.admin.modules.blindbox.module.entity.BlindBoxConfigEntity;
import com.starry.admin.modules.blindbox.service.BlindBoxConfigService;
import java.util.List;
import org.springframework.stereotype.Service;
@Service
public class BlindBoxConfigServiceImpl
extends ServiceImpl<BlindBoxConfigMapper, BlindBoxConfigEntity>
implements BlindBoxConfigService {
@Override
public BlindBoxConfigEntity requireById(String id) {
BlindBoxConfigEntity entity = getById(id);
if (entity == null) {
throw new CustomException("盲盒不存在");
}
return entity;
}
@Override
public List<BlindBoxConfigEntity> listActiveByTenant(String tenantId) {
return lambdaQuery()
.eq(BlindBoxConfigEntity::getTenantId, tenantId)
.eq(BlindBoxConfigEntity::getStatus, BlindBoxConfigStatus.ENABLED.getCode())
.eq(BlindBoxConfigEntity::getDeleted, Boolean.FALSE)
.list();
}
}

View File

@@ -1,9 +1,7 @@
package com.starry.admin.modules.clerk.controller;
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson2.JSONObject;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.starry.admin.common.exception.CustomException;
import com.starry.admin.modules.clerk.module.entity.PlayClerkUserInfoEntity;
import com.starry.admin.modules.clerk.module.entity.PlayClerkUserQueryVo;
import com.starry.admin.modules.clerk.module.entity.PlayClerkUserReturnVo;
@@ -218,9 +216,6 @@ public class PlayClerkUserInfoController {
@PostMapping(value = "/update")
public R update(@ApiParam(value = "店员信息", required = true) @Validated @RequestBody PlayClerkUserEditVo vo) {
PlayClerkUserInfoEntity entity = ConvertUtil.entityToVo(vo, PlayClerkUserInfoEntity.class);
if (StrUtil.isNotBlank(vo.getSex())) {
throw new CustomException("性别修改需要提交资料审核");
}
boolean success = playClerkUserInfoService.update(entity);
if (success) {
return R.ok();
@@ -238,9 +233,6 @@ public class PlayClerkUserInfoController {
@PostMapping(value = "/updateState")
public R updateState(
@ApiParam(value = "店员状态信息", required = true) @Validated @RequestBody PlayClerkUserStateEditVo vo) {
if (StrUtil.isNotBlank(vo.getSex())) {
throw new CustomException("性别修改需要提交资料审核");
}
PlayClerkUserInfoEntity entity = ConvertUtil.entityToVo(vo, PlayClerkUserInfoEntity.class);
boolean success = playClerkUserInfoService.update(entity);
if (success) {

View File

@@ -1,30 +0,0 @@
package com.starry.admin.modules.clerk.enums;
import lombok.Getter;
@Getter
public enum ClerkMediaReviewState {
DRAFT("draft"),
PENDING("pending"),
APPROVED("approved"),
REJECTED("rejected");
private final String code;
ClerkMediaReviewState(String code) {
this.code = code;
}
public static ClerkMediaReviewState fromCode(String code) {
if (code == null || code.isEmpty()) {
return DRAFT;
}
for (ClerkMediaReviewState state : values()) {
if (state.code.equalsIgnoreCase(code) || state.name().equalsIgnoreCase(code)) {
return state;
}
}
return DRAFT;
}
}

View File

@@ -1,32 +0,0 @@
package com.starry.admin.modules.clerk.enums;
import lombok.Getter;
@Getter
public enum ClerkMediaUsage {
PROFILE("profile"),
AVATAR("avatar"),
MOMENTS("moments"),
VOICE_INTRO("voice_intro"),
PROMO("promo"),
OTHER("other");
private final String code;
ClerkMediaUsage(String code) {
this.code = code;
}
public static ClerkMediaUsage fromCode(String code) {
if (code == null || code.isEmpty()) {
return PROFILE;
}
for (ClerkMediaUsage usage : values()) {
if (usage.code.equalsIgnoreCase(code) || usage.name().equalsIgnoreCase(code)) {
return usage;
}
}
return PROFILE;
}
}

View File

@@ -1,8 +0,0 @@
package com.starry.admin.modules.clerk.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.starry.admin.modules.clerk.module.entity.PlayClerkMediaAssetEntity;
public interface PlayClerkMediaAssetMapper extends BaseMapper<PlayClerkMediaAssetEntity> {
}

View File

@@ -1,10 +1,7 @@
package com.starry.admin.modules.clerk.mapper;
import com.baomidou.mybatisplus.annotation.InterceptorIgnore;
import com.github.yulichang.base.MPJBaseMapper;
import com.starry.admin.modules.clerk.module.entity.PlayClerkUserInfoEntity;
import java.util.List;
import org.apache.ibatis.annotations.Select;
/**
* 店员Mapper接口
@@ -14,7 +11,4 @@ import org.apache.ibatis.annotations.Select;
*/
public interface PlayClerkUserInfoMapper extends MPJBaseMapper<PlayClerkUserInfoEntity> {
@InterceptorIgnore(tenantLine = "true")
@Select("SELECT id, tenant_id, album FROM play_clerk_user_info WHERE deleted = 0 AND album IS NOT NULL")
List<PlayClerkUserInfoEntity> selectAllWithAlbumIgnoringTenant();
}

View File

@@ -36,7 +36,7 @@ public class PlayClerkDataReviewInfoEntity extends BaseEntity<PlayClerkDataRevie
private String clerkId;
/**
* 资料类型[0:昵称;1:头像;2:相册;3:录音;4:性别]
* 资料类型[0:昵称;1:头像;2:相册;3:录音]
*/
private String dataType;

View File

@@ -1,7 +1,5 @@
package com.starry.admin.modules.clerk.module.entity;
import com.baomidou.mybatisplus.annotation.FieldStrategy;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import com.starry.common.domain.BaseEntity;
import lombok.Data;
@@ -71,7 +69,4 @@ public class PlayClerkLevelInfoEntity extends BaseEntity<PlayClerkLevelInfoEntit
private Integer styleType;
private String styleImageUrl;
@TableField(updateStrategy = FieldStrategy.IGNORED)
private Long orderNumber;
}

View File

@@ -1,40 +0,0 @@
package com.starry.admin.modules.clerk.module.entity;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.starry.common.domain.BaseEntity;
import java.time.LocalDateTime;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Data
@EqualsAndHashCode(callSuper = false)
@TableName(value = "play_clerk_media_asset")
public class PlayClerkMediaAssetEntity extends BaseEntity<PlayClerkMediaAssetEntity> {
@TableId
private String id;
private String clerkId;
/**
* 租戶 ID供 TenantLine 過濾
*/
private String tenantId;
private String mediaId;
@TableField("`usage`")
private String usage;
private String reviewState;
private Integer orderIndex;
private LocalDateTime submittedTime;
private String reviewRecordId;
private String note;
}

View File

@@ -3,7 +3,6 @@ package com.starry.admin.modules.clerk.module.entity;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.starry.admin.modules.order.module.entity.PlayOrderInfoEntity;
import com.starry.admin.modules.weichat.entity.clerk.MediaVo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import java.math.BigDecimal;
@@ -95,12 +94,6 @@ public class PlayClerkUserReturnVo {
@ApiModelProperty(value = "相册列表", notes = "店员相册图片URL列表")
private List<String> album = new ArrayList<>();
/**
* 媒资列表
*/
@ApiModelProperty(value = "媒资列表", notes = "结构化媒资数据")
private List<MediaVo> mediaList = new ArrayList<>();
/**
* 个性签名
*/

View File

@@ -16,8 +16,7 @@ public enum ClerkDataType {
NICKNAME("0", "昵称"),
AVATAR("1", "头像"),
PHOTO_ALBUM("2", "相册"),
RECORDING("3", "录音"),
GENDER("4", "性别");
RECORDING("3", "录音");
private final String code;
private final String description;

View File

@@ -1,50 +0,0 @@
package com.starry.admin.modules.clerk.module.enums;
import cn.hutool.core.util.StrUtil;
/**
* 员工状态【1是陪聊0不是陪聊】
*/
public enum ClerkRoleStatus {
CLERK("1"),
NON_CLERK("0");
private final String code;
ClerkRoleStatus(String code) {
this.code = code;
}
public String getCode() {
return code;
}
public boolean matches(String value) {
return StrUtil.equals(code, value);
}
public boolean isClerk() {
return this == CLERK;
}
public static ClerkRoleStatus fromCode(String value) {
if (CLERK.matches(value)) {
return CLERK;
}
if (NON_CLERK.matches(value)) {
return NON_CLERK;
}
return NON_CLERK;
}
public static boolean isClerk(String value) {
return fromCode(value).isClerk();
}
public static boolean transitionedToNonClerk(String newValue, String originalValue) {
if (StrUtil.isBlank(newValue)) {
return false;
}
return !isClerk(newValue) && isClerk(originalValue);
}
}

View File

@@ -1,54 +0,0 @@
package com.starry.admin.modules.clerk.module.enums;
import cn.hutool.core.util.StrUtil;
/**
* 上架状态【1上架0下架】
*/
public enum ListingStatus {
LISTED("1"),
DELISTED("0");
private final String code;
ListingStatus(String code) {
this.code = code;
}
public String getCode() {
return code;
}
public boolean matches(String value) {
return StrUtil.equals(code, value);
}
public boolean isListed() {
return this == LISTED;
}
public static ListingStatus fromCode(String value) {
if (LISTED.matches(value)) {
return LISTED;
}
if (DELISTED.matches(value)) {
return DELISTED;
}
return LISTED;
}
public static boolean isListed(String value) {
return fromCode(value).isListed();
}
public static boolean isDelisted(String value) {
return !isListed(value);
}
public static boolean transitionedToDelisted(String newValue, String originalValue) {
if (StrUtil.isBlank(newValue)) {
return false;
}
return isDelisted(newValue) && !isDelisted(originalValue);
}
}

View File

@@ -1,54 +0,0 @@
package com.starry.admin.modules.clerk.module.enums;
import cn.hutool.core.util.StrUtil;
/**
* 在职状态1在职0离职
*/
public enum OnboardingStatus {
ACTIVE("1"),
OFFBOARDED("0");
private final String code;
OnboardingStatus(String code) {
this.code = code;
}
public String getCode() {
return code;
}
public boolean matches(String value) {
return StrUtil.equals(code, value);
}
public boolean isActive() {
return this == ACTIVE;
}
public static OnboardingStatus fromCode(String value) {
if (ACTIVE.matches(value)) {
return ACTIVE;
}
if (OFFBOARDED.matches(value)) {
return OFFBOARDED;
}
return ACTIVE;
}
public static boolean isActive(String value) {
return fromCode(value).isActive();
}
public static boolean isOffboarded(String value) {
return !isActive(value);
}
public static boolean transitionedToOffboarded(String newValue, String originalValue) {
if (StrUtil.isBlank(newValue)) {
return false;
}
return isOffboarded(newValue) && !isOffboarded(originalValue);
}
}

View File

@@ -25,9 +25,9 @@ public class PlayClerkDataReviewQueryVo extends BasePageEntity {
private String clerkId;
/**
* 资料类型[0:昵称;1:头像;2:相册;3:录音;4:性别]
* 资料类型[0:昵称;1:头像;2:相册;3:录音]
*/
@ApiModelProperty(value = "资料类型", example = "1", notes = "0:昵称;1:头像;2:相册;3:录音;4:性别")
@ApiModelProperty(value = "资料类型", example = "1", notes = "0:昵称;1:头像;2:相册;3:录音")
private String dataType;
/**

View File

@@ -1,7 +1,7 @@
package com.starry.admin.modules.clerk.module.vo;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.starry.admin.modules.clerk.module.enums.ClerkDataType;
import com.starry.common.enums.ClerkReviewState;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import java.time.LocalDateTime;
@@ -43,15 +43,15 @@ public class PlayClerkDataReviewReturnVo {
private String clerkAvatar;
/**
* 资料类型[0:昵称;1:头像;2:相册;3:录音;4:性别]
* 资料类型[0:昵称;1:头像;2:相册;3:录音]
*/
@ApiModelProperty(value = "资料类型", example = "1", notes = "0:昵称;1:头像;2:相册;3:录音;4:性别")
@ApiModelProperty(value = "资料类型", example = "1", notes = "0:昵称;1:头像;2:相册;3:录音")
private String dataType;
/**
* 资料类型枚举(新增字段,用于类型安全)
*/
@ApiModelProperty(value = "资料类型枚举", example = "AVATAR", notes = "NICKNAME:昵称, AVATAR:头像, PHOTO_ALBUM:相册, RECORDING:录音, GENDER:性别")
@ApiModelProperty(value = "资料类型枚举", example = "AVATAR", notes = "NICKNAME:昵称, AVATAR:头像, PHOTO_ALBUM:相册, RECORDING:录音")
private ClerkDataType dataTypeEnum;
/**
@@ -60,27 +60,12 @@ public class PlayClerkDataReviewReturnVo {
@ApiModelProperty(value = "资料内容", example = "[\"https://example.com/photo1.jpg\"]", notes = "资料内容,根据资料类型有不同格式")
private List<String> dataContent;
/**
* 媒资对应的视频地址(仅当资料类型为头像/相册且为视频时有值,顺序与 dataContent 一一对应)
*/
@ApiModelProperty(
value = "媒资视频地址列表",
example = "[\"https://example.com/video1.mp4\"]",
notes = "仅当资料类型为头像/相册且为视频时有值,顺序与 dataContent 一一对应")
private List<String> mediaVideoUrls;
/**
* 审核状态0未审核:1审核通过2审核不通过
*/
@ApiModelProperty(value = "审核状态", example = "0", notes = "0未审核:1审核通过2审核不通过")
private String reviewState;
/**
* 审核状态枚举
*/
@ApiModelProperty(value = "审核状态枚举", example = "APPROVED", notes = "PENDING:未审核, APPROVED:审核通过, REJECTED:审核不通过")
private ClerkReviewState reviewStateEnum;
/**
* 资料添加时间
*/
@@ -138,29 +123,4 @@ public class PlayClerkDataReviewReturnVo {
return dataTypeEnum;
}
public void setReviewState(String reviewState) {
this.reviewState = reviewState;
try {
this.reviewStateEnum = ClerkReviewState.fromCode(reviewState);
} catch (Exception e) {
this.reviewStateEnum = null;
}
}
public void setReviewStateEnum(ClerkReviewState reviewStateEnum) {
this.reviewStateEnum = reviewStateEnum;
this.reviewState = reviewStateEnum != null ? reviewStateEnum.getCode() : null;
}
public ClerkReviewState getReviewStateEnum() {
if (reviewStateEnum == null && reviewState != null) {
try {
reviewStateEnum = ClerkReviewState.fromCode(reviewState);
} catch (Exception e) {
// ignore invalid code
}
}
return reviewStateEnum;
}
}

View File

@@ -1,11 +1,10 @@
package com.starry.admin.modules.clerk.module.vo;
import com.starry.common.enums.ClerkReviewState;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import java.time.LocalDateTime;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Pattern;
import lombok.Data;
/**
@@ -22,14 +21,10 @@ public class PlayClerkDataReviewStateEditVo {
/**
* 审核状态0未审核:1审核通过2审核不通过
*/
@NotNull(message = "审核状态不能为空")
@ApiModelProperty(
value = "审核状态",
required = true,
example = "1",
allowableValues = "1,2",
notes = "1审核通过2审核不通过")
private ClerkReviewState reviewState;
@NotBlank(message = "审核状态不能为空")
@Pattern(regexp = "[12]", message = "审核状态必须为1或2")
@ApiModelProperty(value = "审核状态", required = true, example = "1", notes = "1审核通过2审核不通过")
private String reviewState;
/**
* 审核内容

View File

@@ -55,7 +55,4 @@ public class PlayClerkLevelAddVo {
@ApiModelProperty(value = "非首次随机单比例", example = "65", notes = "非首次随机单提成比例范围0-100%")
private Integer notFirstRandomRadio;
@ApiModelProperty(value = "排序号", example = "1", notes = "越小的等级在列表越靠前")
private Long orderNumber;
}

View File

@@ -68,6 +68,4 @@ public class PlayClerkLevelEditVo {
@ApiModelProperty(value = "样式图片URL", example = "https://example.com/style.jpg", notes = "等级样式图片URL")
private String styleImageUrl;
private Long orderNumber;
}

View File

@@ -37,10 +37,4 @@ public class PlayClerkUserReviewStateEditVo {
*/
@ApiModelProperty(value = "审核时间", hidden = true, notes = "系统自动生成,无需传入")
private LocalDateTime reviewTime = LocalDateTime.now();
/**
* 店员分组ID
*/
@ApiModelProperty(value = "店员分组ID", required = false, notes = "审核通过时可选,指定店员所属分组")
private String groupId;
}

View File

@@ -1,25 +0,0 @@
package com.starry.admin.modules.clerk.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.starry.admin.modules.clerk.enums.ClerkMediaReviewState;
import com.starry.admin.modules.clerk.enums.ClerkMediaUsage;
import com.starry.admin.modules.clerk.module.entity.PlayClerkMediaAssetEntity;
import java.util.Collection;
import java.util.List;
public interface IPlayClerkMediaAssetService extends IService<PlayClerkMediaAssetEntity> {
PlayClerkMediaAssetEntity linkDraftAsset(String tenantId, String clerkId, String mediaId, ClerkMediaUsage usage);
void submitWithOrder(String clerkId, ClerkMediaUsage usage, List<String> mediaIds);
void reorder(String clerkId, ClerkMediaUsage usage, List<String> mediaIds);
void softDelete(String clerkId, String mediaId);
List<PlayClerkMediaAssetEntity> listByState(String clerkId, ClerkMediaUsage usage, Collection<ClerkMediaReviewState> states);
List<PlayClerkMediaAssetEntity> listActiveByUsage(String clerkId, ClerkMediaUsage usage);
void applyReviewDecision(String clerkId, ClerkMediaUsage usage, List<String> approvedValues, String reviewRecordId, String note);
}

View File

@@ -190,41 +190,6 @@ public interface IPlayClerkUserInfoService extends IService<PlayClerkUserInfoEnt
*/
IPage<PlayClerkUserInfoResultVo> selectPlayClerkUserInfoByPage(PlayClerkUserInfoQueryVo vo);
/**
* 构建面向顾客的店员详情视图对象(包含媒资与兼容相册)。
*
* @param clerkId
* 店员ID
* @param customUserId
* 顾客ID可为空用于标记关注状态
* @return 店员详情视图对象
*/
PlayClerkUserInfoResultVo buildCustomerDetail(String clerkId, String customUserId);
/**
* 确认店员处于可用状态,否则抛出异常
*
* @param clerkUserInfoEntity
* 店员信息
*/
void ensureClerkIsActive(PlayClerkUserInfoEntity clerkUserInfoEntity);
/**
* 确认店员登录态仍然有效(未被离职/下架/删除),否则抛出异常
*
* @param clerkUserInfoEntity
* 店员信息
*/
void ensureClerkSessionIsValid(PlayClerkUserInfoEntity clerkUserInfoEntity);
/**
* 使店员当前登录态失效
*
* @param clerkId
* 店员ID
*/
void invalidateClerkSession(String clerkId);
/**
* 新增店员
*
@@ -263,12 +228,5 @@ public interface IPlayClerkUserInfoService extends IService<PlayClerkUserInfoEnt
List<PlayClerkUserInfoEntity> simpleList();
/**
* 查询存在相册字段数据的店员(忽略租户隔离)
*
* @return 店员集合
*/
List<PlayClerkUserInfoEntity> listWithAlbumIgnoringTenant();
JSONObject getPcData(PlayClerkUserInfoEntity entity);
}

View File

@@ -1,6 +1,5 @@
package com.starry.admin.modules.clerk.service.impl;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
@@ -8,34 +7,21 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.github.yulichang.wrapper.MPJLambdaWrapper;
import com.starry.admin.common.exception.CustomException;
import com.starry.admin.modules.clerk.enums.ClerkMediaUsage;
import com.starry.admin.modules.clerk.mapper.PlayClerkDataReviewInfoMapper;
import com.starry.admin.modules.clerk.module.entity.PlayClerkDataReviewInfoEntity;
import com.starry.admin.modules.clerk.module.entity.PlayClerkUserInfoEntity;
import com.starry.admin.modules.clerk.module.entity.PlayClerkUserReviewInfoEntity;
import com.starry.admin.modules.clerk.module.enums.ClerkDataType;
import com.starry.admin.modules.clerk.module.vo.PlayClerkDataReviewQueryVo;
import com.starry.admin.modules.clerk.module.vo.PlayClerkDataReviewReturnVo;
import com.starry.admin.modules.clerk.module.vo.PlayClerkDataReviewStateEditVo;
import com.starry.admin.modules.clerk.service.IPlayClerkDataReviewInfoService;
import com.starry.admin.modules.clerk.service.IPlayClerkMediaAssetService;
import com.starry.admin.modules.clerk.service.IPlayClerkUserInfoService;
import com.starry.admin.modules.media.entity.PlayMediaEntity;
import com.starry.admin.modules.media.enums.MediaKind;
import com.starry.admin.modules.media.enums.MediaOwnerType;
import com.starry.admin.modules.media.enums.MediaStatus;
import com.starry.admin.modules.media.service.IPlayMediaService;
import com.starry.common.enums.ClerkReviewState;
import com.starry.common.utils.IdUtils;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import javax.annotation.Resource;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Service;
/**
@@ -56,12 +42,6 @@ public class PlayClerkDataReviewInfoServiceImpl
@Resource
private IPlayClerkUserInfoService playClerkUserInfoService;
@Resource
private IPlayClerkMediaAssetService clerkMediaAssetService;
@Resource
private IPlayMediaService mediaService;
/**
* 查询店员资料审核
*
@@ -127,11 +107,8 @@ public class PlayClerkDataReviewInfoServiceImpl
lambdaQueryWrapper.between(PlayClerkDataReviewInfoEntity::getAddTime, vo.getAddTime().get(0),
vo.getAddTime().get(1));
}
IPage<PlayClerkDataReviewReturnVo> page = this.baseMapper.selectJoinPage(
new Page<>(vo.getPageNum(), vo.getPageSize()), PlayClerkDataReviewReturnVo.class,
lambdaQueryWrapper);
enrichDataContentWithMediaPreview(page);
return page;
return this.baseMapper.selectJoinPage(new Page<>(vo.getPageNum(), vo.getPageSize()),
PlayClerkDataReviewReturnVo.class, lambdaQueryWrapper);
}
/**
@@ -152,168 +129,27 @@ public class PlayClerkDataReviewInfoServiceImpl
return save(playClerkDataReviewInfo);
}
/**
* 为头像 / 相册审核记录补充可预览的 URL。
*
* <p>dataContent 中现在可能是媒资 IDmediaId或历史 URL这里做一次向前兼容
* <ul>
* <li>如果是 mediaId则解析到 play_media 记录,并返回封面或原始 URL</li>
* <li>如果查不到媒资,则保留原值。</li>
* </ul>
* 这样 PC 端审核页面始终可以正确预览图片/视频。</p>
*/
private void enrichDataContentWithMediaPreview(IPage<PlayClerkDataReviewReturnVo> page) {
if (page == null || page.getRecords() == null || page.getRecords().isEmpty()) {
return;
}
for (PlayClerkDataReviewReturnVo row : page.getRecords()) {
ClerkDataType type = row.getDataTypeEnum();
if (type == null) {
continue;
}
if (type == ClerkDataType.AVATAR || type == ClerkDataType.PHOTO_ALBUM) {
List<String> content = row.getDataContent();
if (CollectionUtil.isEmpty(content)) {
continue;
}
List<String> previewUrls = new ArrayList<>();
List<String> videoUrls = new ArrayList<>();
for (String value : content) {
if (StrUtil.isBlank(value)) {
continue;
}
MediaPreviewPair pair = resolvePreviewPair(value);
if (pair == null || StrUtil.isBlank(pair.getPreviewUrl())) {
continue;
}
previewUrls.add(pair.getPreviewUrl());
videoUrls.add(pair.getVideoUrl());
}
row.setDataContent(previewUrls);
row.setMediaVideoUrls(videoUrls);
}
}
}
private MediaPreviewPair resolvePreviewPair(String value) {
if (StrUtil.isBlank(value)) {
return null;
}
PlayMediaEntity media = mediaService.getById(value);
if (media == null) {
MediaPreviewPair fallback = new MediaPreviewPair();
fallback.setPreviewUrl(value);
fallback.setVideoUrl(null);
return fallback;
}
MediaPreviewPair pair = new MediaPreviewPair();
if (MediaKind.VIDEO.getCode().equals(media.getKind())) {
String coverUrl = StrUtil.isNotBlank(media.getCoverUrl()) ? media.getCoverUrl() : media.getUrl();
pair.setPreviewUrl(coverUrl);
pair.setVideoUrl(media.getUrl());
} else {
pair.setPreviewUrl(media.getUrl());
pair.setVideoUrl(null);
}
return pair;
}
@Override
public void updateDataReviewState(PlayClerkDataReviewStateEditVo vo) {
PlayClerkDataReviewInfoEntity entity = this.selectPlayClerkDataReviewInfoById(vo.getId());
ClerkReviewState reviewState = vo.getReviewState();
entity.setReviewState(reviewState != null ? reviewState.getCode() : null);
entity.setReviewCon(vo.getReviewCon());
entity.setReviewTime(vo.getReviewTime() != null ? vo.getReviewTime() : LocalDateTime.now());
BeanUtils.copyProperties(vo, entity);
this.update(entity);
if (ClerkReviewState.APPROVED.equals(reviewState)) {
if ("1".equals(vo.getReviewState())) {
PlayClerkUserInfoEntity userInfo = new PlayClerkUserInfoEntity();
userInfo.setId(entity.getClerkId());
if ("0".equals(entity.getDataType())) {
userInfo.setNickname(entity.getDataContent().isEmpty() ? null : entity.getDataContent().get(0));
}
if ("1".equals(entity.getDataType())) {
userInfo.setAvatar(entity.getDataContent().get(0));
}
if ("2".equals(entity.getDataType())) {
userInfo.setAlbum(new ArrayList<>());
synchronizeApprovedAlbumMedia(entity);
userInfo.setAlbum(entity.getDataContent());
}
if ("3".equals(entity.getDataType())) {
userInfo.setAudio(entity.getDataContent().get(0));
}
if ("4".equals(entity.getDataType())) {
userInfo.setSex(entity.getDataContent().isEmpty() ? null : entity.getDataContent().get(0));
}
playClerkUserInfoService.update(userInfo);
}
}
private void synchronizeApprovedAlbumMedia(PlayClerkDataReviewInfoEntity reviewInfo) {
PlayClerkUserInfoEntity clerkInfo = playClerkUserInfoService.getById(reviewInfo.getClerkId());
if (clerkInfo == null) {
throw new CustomException("店员信息不存在,无法同步媒资");
}
List<String> rawContent = reviewInfo.getDataContent();
List<String> sanitized = CollectionUtil.isEmpty(rawContent)
? Collections.emptyList()
: rawContent.stream().filter(StrUtil::isNotBlank).map(String::trim).distinct()
.collect(Collectors.toList());
List<String> resolvedMediaIds = new ArrayList<>();
for (String value : sanitized) {
PlayMediaEntity media = resolveMediaEntity(clerkInfo, value);
if (media == null) {
continue;
}
clerkMediaAssetService.linkDraftAsset(clerkInfo.getTenantId(), clerkInfo.getId(), media.getId(),
ClerkMediaUsage.PROFILE);
resolvedMediaIds.add(media.getId());
}
clerkMediaAssetService.applyReviewDecision(clerkInfo.getId(), ClerkMediaUsage.PROFILE, resolvedMediaIds,
reviewInfo.getId(), reviewInfo.getReviewCon());
}
private PlayMediaEntity resolveMediaEntity(PlayClerkUserInfoEntity clerkInfo, String value) {
if (StrUtil.isBlank(value)) {
return null;
}
PlayMediaEntity media = mediaService.getById(value);
if (media != null) {
return media;
}
media = mediaService.lambdaQuery()
.eq(PlayMediaEntity::getOwnerType, MediaOwnerType.CLERK)
.eq(PlayMediaEntity::getOwnerId, clerkInfo.getId())
.eq(PlayMediaEntity::getUrl, value)
.last("limit 1")
.one();
if (media != null) {
return media;
}
return createMediaFromLegacyUrl(clerkInfo, value);
}
private PlayMediaEntity createMediaFromLegacyUrl(PlayClerkUserInfoEntity clerkInfo, String url) {
PlayMediaEntity media = new PlayMediaEntity();
media.setId(IdUtils.getUuid());
media.setTenantId(clerkInfo.getTenantId());
media.setOwnerType(MediaOwnerType.CLERK);
media.setOwnerId(clerkInfo.getId());
media.setKind(MediaKind.IMAGE.getCode());
media.setStatus(MediaStatus.READY.getCode());
media.setUrl(url);
Map<String, Object> metadata = new HashMap<>();
metadata.put("legacySource", "album_review");
media.setMetadata(metadata);
mediaService.normalizeAndSave(media);
media.setStatus(MediaStatus.READY.getCode());
mediaService.updateById(media);
return media;
}
/**
* 修改店员资料审核
*
@@ -349,28 +185,4 @@ public class PlayClerkDataReviewInfoServiceImpl
public int deletePlayClerkDataReviewInfoById(String id) {
return playClerkDataReviewInfoMapper.deleteById(id);
}
/**
* 简单的预览地址 / 视频地址对,避免在主体逻辑中使用 Map 或魔法下标。
*/
private static class MediaPreviewPair {
private String previewUrl;
private String videoUrl;
String getPreviewUrl() {
return previewUrl;
}
void setPreviewUrl(String previewUrl) {
this.previewUrl = previewUrl;
}
String getVideoUrl() {
return videoUrl;
}
void setVideoUrl(String videoUrl) {
this.videoUrl = videoUrl;
}
}
}

View File

@@ -43,7 +43,6 @@ public class PlayClerkLevelInfoServiceImpl extends ServiceImpl<PlayClerkLevelInf
entity.setFirstRegularRatio(45);
entity.setNotFirstRegularRatio(50);
entity.setLevel(1);
entity.setOrderNumber(1L);
entity.setStyleType(entity.getLevel());
entity.setTenantId(sysTenantEntity.getTenantId());
this.baseMapper.insert(entity);
@@ -65,7 +64,6 @@ public class PlayClerkLevelInfoServiceImpl extends ServiceImpl<PlayClerkLevelInf
entity.setFirstRegularRatio(45);
entity.setNotFirstRegularRatio(50);
entity.setLevel(1);
entity.setOrderNumber(1L);
entity.setStyleType(1);
this.baseMapper.insert(entity);
return entity;
@@ -118,9 +116,6 @@ public class PlayClerkLevelInfoServiceImpl extends ServiceImpl<PlayClerkLevelInf
}
playClerkLevelInfo.setCreatedTime(new Date());
playClerkLevelInfo.setStyleType(playClerkLevelInfo.getLevel());
if (playClerkLevelInfo.getOrderNumber() == null) {
playClerkLevelInfo.setOrderNumber(playClerkLevelInfo.getLevel().longValue());
}
return save(playClerkLevelInfo);
}

View File

@@ -1,280 +0,0 @@
package com.starry.admin.modules.clerk.service.impl;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.starry.admin.common.exception.CustomException;
import com.starry.admin.modules.clerk.enums.ClerkMediaReviewState;
import com.starry.admin.modules.clerk.enums.ClerkMediaUsage;
import com.starry.admin.modules.clerk.mapper.PlayClerkMediaAssetMapper;
import com.starry.admin.modules.clerk.module.entity.PlayClerkMediaAssetEntity;
import com.starry.admin.modules.clerk.service.IPlayClerkMediaAssetService;
import com.starry.admin.modules.media.entity.PlayMediaEntity;
import com.starry.admin.modules.media.service.IPlayMediaService;
import com.starry.common.utils.IdUtils;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
import javax.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class PlayClerkMediaAssetServiceImpl extends ServiceImpl<PlayClerkMediaAssetMapper, PlayClerkMediaAssetEntity>
implements IPlayClerkMediaAssetService {
@Resource
private IPlayMediaService mediaService;
@Override
@Transactional(rollbackFor = Exception.class)
public PlayClerkMediaAssetEntity linkDraftAsset(String tenantId, String clerkId, String mediaId,
ClerkMediaUsage usage) {
LambdaQueryWrapper<PlayClerkMediaAssetEntity> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(PlayClerkMediaAssetEntity::getClerkId, clerkId)
.eq(StrUtil.isNotBlank(tenantId), PlayClerkMediaAssetEntity::getTenantId, tenantId)
.eq(PlayClerkMediaAssetEntity::getUsage, usage.getCode())
.eq(PlayClerkMediaAssetEntity::getMediaId, mediaId);
PlayClerkMediaAssetEntity existing = this.getOne(wrapper, false);
if (existing != null) {
if (StrUtil.isBlank(existing.getTenantId()) && StrUtil.isNotBlank(tenantId)) {
existing.setTenantId(tenantId);
}
if (Boolean.TRUE.equals(existing.getDeleted())) {
existing.setDeleted(false);
}
existing.setReviewState(ClerkMediaReviewState.DRAFT.getCode());
if (existing.getOrderIndex() == null) {
existing.setOrderIndex(resolveNextOrderIndex(clerkId, usage));
}
this.updateById(existing);
return existing;
}
PlayClerkMediaAssetEntity entity = new PlayClerkMediaAssetEntity();
entity.setId(IdUtils.getUuid());
entity.setClerkId(clerkId);
entity.setTenantId(tenantId);
entity.setMediaId(mediaId);
entity.setUsage(usage.getCode());
entity.setReviewState(ClerkMediaReviewState.DRAFT.getCode());
entity.setOrderIndex(resolveNextOrderIndex(clerkId, usage));
entity.setDeleted(false);
this.save(entity);
return entity;
}
@Override
@Transactional(rollbackFor = Exception.class)
public void submitWithOrder(String clerkId, ClerkMediaUsage usage, List<String> mediaIds) {
List<String> ordered = distinctMediaIds(mediaIds);
List<PlayClerkMediaAssetEntity> assets = listActiveByUsage(clerkId, usage);
if (CollectionUtil.isEmpty(assets)) {
return;
}
Map<String, PlayClerkMediaAssetEntity> assetsByMediaId = assets.stream()
.collect(Collectors.toMap(PlayClerkMediaAssetEntity::getMediaId, item -> item));
List<PlayClerkMediaAssetEntity> updates = new ArrayList<>();
int order = 0;
for (String mediaId : ordered) {
PlayClerkMediaAssetEntity asset = assetsByMediaId.get(mediaId);
if (asset == null) {
continue;
}
asset.setOrderIndex(order++);
asset.setReviewState(ClerkMediaReviewState.PENDING.getCode());
asset.setSubmittedTime(LocalDateTime.now());
updates.add(asset);
}
Set<String> keepSet = ordered.stream().collect(Collectors.toSet());
for (PlayClerkMediaAssetEntity asset : assets) {
if (!keepSet.contains(asset.getMediaId())) {
asset.setReviewState(ClerkMediaReviewState.REJECTED.getCode());
asset.setOrderIndex(0);
updates.add(asset);
}
}
if (CollectionUtil.isNotEmpty(updates)) {
this.updateBatchById(updates);
}
}
@Override
@Transactional(rollbackFor = Exception.class)
public void reorder(String clerkId, ClerkMediaUsage usage, List<String> mediaIds) {
List<String> ordered = distinctMediaIds(mediaIds);
if (CollectionUtil.isEmpty(ordered)) {
return;
}
List<PlayClerkMediaAssetEntity> assets = listActiveByUsage(clerkId, usage);
if (CollectionUtil.isEmpty(assets)) {
return;
}
Map<String, PlayClerkMediaAssetEntity> assetsByMediaId = assets.stream()
.collect(Collectors.toMap(PlayClerkMediaAssetEntity::getMediaId, item -> item));
List<PlayClerkMediaAssetEntity> updates = new ArrayList<>();
int order = 0;
for (String mediaId : ordered) {
PlayClerkMediaAssetEntity asset = assetsByMediaId.get(mediaId);
if (asset == null) {
continue;
}
if (!Objects.equals(asset.getOrderIndex(), order)) {
asset.setOrderIndex(order);
updates.add(asset);
}
order++;
}
if (CollectionUtil.isNotEmpty(updates)) {
this.updateBatchById(updates);
}
}
@Override
@Transactional(rollbackFor = Exception.class)
public void softDelete(String clerkId, String mediaId) {
LambdaQueryWrapper<PlayClerkMediaAssetEntity> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(PlayClerkMediaAssetEntity::getClerkId, clerkId)
.eq(PlayClerkMediaAssetEntity::getMediaId, mediaId)
.eq(PlayClerkMediaAssetEntity::getDeleted, false);
PlayClerkMediaAssetEntity asset = this.getOne(wrapper, false);
if (asset == null) {
return;
}
asset.setDeleted(true);
asset.setReviewState(ClerkMediaReviewState.REJECTED.getCode());
this.updateById(asset);
}
@Override
public List<PlayClerkMediaAssetEntity> listByState(String clerkId, ClerkMediaUsage usage,
Collection<ClerkMediaReviewState> states) {
LambdaQueryWrapper<PlayClerkMediaAssetEntity> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(PlayClerkMediaAssetEntity::getClerkId, clerkId)
.eq(PlayClerkMediaAssetEntity::getUsage, usage.getCode())
.eq(PlayClerkMediaAssetEntity::getDeleted, false)
.orderByAsc(PlayClerkMediaAssetEntity::getOrderIndex)
.orderByDesc(PlayClerkMediaAssetEntity::getCreatedTime);
if (CollectionUtil.isNotEmpty(states)) {
wrapper.in(PlayClerkMediaAssetEntity::getReviewState,
states.stream().map(ClerkMediaReviewState::getCode).collect(Collectors.toList()));
}
return this.list(wrapper);
}
@Override
public List<PlayClerkMediaAssetEntity> listActiveByUsage(String clerkId, ClerkMediaUsage usage) {
LambdaQueryWrapper<PlayClerkMediaAssetEntity> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(PlayClerkMediaAssetEntity::getClerkId, clerkId)
.eq(PlayClerkMediaAssetEntity::getUsage, usage.getCode())
.eq(PlayClerkMediaAssetEntity::getDeleted, false)
.orderByAsc(PlayClerkMediaAssetEntity::getOrderIndex)
.orderByDesc(PlayClerkMediaAssetEntity::getCreatedTime);
return this.list(wrapper);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void applyReviewDecision(String clerkId, ClerkMediaUsage usage, List<String> approvedValues,
String reviewRecordId, String note) {
List<PlayClerkMediaAssetEntity> assets = listActiveByUsage(clerkId, usage);
if (CollectionUtil.isEmpty(assets)) {
return;
}
List<String> normalized = distinctMediaIds(approvedValues);
Map<String, PlayClerkMediaAssetEntity> byMediaId = assets.stream()
.collect(Collectors.toMap(PlayClerkMediaAssetEntity::getMediaId, item -> item));
Map<String, PlayClerkMediaAssetEntity> byUrl = buildAssetByUrlMap(assets);
List<PlayClerkMediaAssetEntity> updates = new ArrayList<>();
Set<String> approvedAssetIds = new java.util.HashSet<>();
int order = 0;
for (String value : normalized) {
PlayClerkMediaAssetEntity asset = byMediaId.get(value);
if (asset == null) {
asset = byUrl.get(value);
}
if (asset == null) {
continue;
}
asset.setReviewState(ClerkMediaReviewState.APPROVED.getCode());
asset.setOrderIndex(order++);
asset.setReviewRecordId(reviewRecordId);
if (StrUtil.isNotBlank(note)) {
asset.setNote(note);
}
updates.add(asset);
approvedAssetIds.add(asset.getId());
}
for (PlayClerkMediaAssetEntity asset : assets) {
if (approvedAssetIds.contains(asset.getId())) {
continue;
}
asset.setReviewState(ClerkMediaReviewState.REJECTED.getCode());
asset.setReviewRecordId(reviewRecordId);
updates.add(asset);
}
if (CollectionUtil.isNotEmpty(updates)) {
this.updateBatchById(updates);
}
}
private int resolveNextOrderIndex(String clerkId, ClerkMediaUsage usage) {
LambdaQueryWrapper<PlayClerkMediaAssetEntity> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(PlayClerkMediaAssetEntity::getClerkId, clerkId)
.eq(PlayClerkMediaAssetEntity::getUsage, usage.getCode())
.eq(PlayClerkMediaAssetEntity::getDeleted, false)
.orderByDesc(PlayClerkMediaAssetEntity::getOrderIndex)
.last("limit 1");
PlayClerkMediaAssetEntity last = this.getOne(wrapper, false);
if (last == null || last.getOrderIndex() == null) {
return 0;
}
return last.getOrderIndex() + 1;
}
private List<String> distinctMediaIds(List<String> mediaIds) {
if (CollectionUtil.isEmpty(mediaIds)) {
return Collections.emptyList();
}
return mediaIds.stream()
.filter(StrUtil::isNotBlank)
.map(String::trim)
.distinct()
.collect(Collectors.toList());
}
private Map<String, PlayClerkMediaAssetEntity> buildAssetByUrlMap(List<PlayClerkMediaAssetEntity> assets) {
if (CollectionUtil.isEmpty(assets)) {
return Collections.emptyMap();
}
List<String> mediaIds = assets.stream().map(PlayClerkMediaAssetEntity::getMediaId).collect(Collectors.toList());
List<PlayMediaEntity> mediaList = mediaService.listByIds(mediaIds);
if (CollectionUtil.isEmpty(mediaList)) {
return Collections.emptyMap();
}
Map<String, String> mediaIdToUrl = mediaList.stream()
.filter(item -> StrUtil.isNotBlank(item.getUrl()))
.collect(Collectors.toMap(PlayMediaEntity::getId, PlayMediaEntity::getUrl, (left, right) -> left));
Map<String, PlayClerkMediaAssetEntity> map = new HashMap<>();
for (PlayClerkMediaAssetEntity asset : assets) {
String url = mediaIdToUrl.get(asset.getMediaId());
if (StrUtil.isNotBlank(url)) {
map.put(url, asset);
}
}
return map;
}
}

View File

@@ -1,45 +1,30 @@
package com.starry.admin.modules.clerk.service.impl;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson2.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.github.yulichang.wrapper.MPJLambdaWrapper;
import com.starry.admin.common.component.JwtToken;
import com.starry.admin.common.domain.LoginUser;
import com.starry.admin.common.exception.CustomException;
import com.starry.admin.modules.clerk.enums.ClerkMediaReviewState;
import com.starry.admin.modules.clerk.enums.ClerkMediaUsage;
import com.starry.admin.modules.clerk.mapper.PlayClerkUserInfoMapper;
import com.starry.admin.modules.clerk.module.entity.PlayClerkCommodityEntity;
import com.starry.admin.modules.clerk.module.entity.PlayClerkDataReviewInfoEntity;
import com.starry.admin.modules.clerk.module.entity.PlayClerkLevelInfoEntity;
import com.starry.admin.modules.clerk.module.entity.PlayClerkMediaAssetEntity;
import com.starry.admin.modules.clerk.module.entity.PlayClerkUserInfoEntity;
import com.starry.admin.modules.clerk.module.entity.PlayClerkUserQueryVo;
import com.starry.admin.modules.clerk.module.entity.PlayClerkUserReturnVo;
import com.starry.admin.modules.clerk.module.entity.PlayClerkUserReviewInfoEntity;
import com.starry.admin.modules.clerk.module.enums.ClerkRoleStatus;
import com.starry.admin.modules.clerk.module.enums.ListingStatus;
import com.starry.admin.modules.clerk.module.enums.OnboardingStatus;
import com.starry.admin.modules.clerk.module.vo.PlayClerkCommodityQueryVo;
import com.starry.admin.modules.clerk.module.vo.PlayClerkUnsettledWagesInfoQueryVo;
import com.starry.admin.modules.clerk.module.vo.PlayClerkUnsettledWagesInfoReturnVo;
import com.starry.admin.modules.clerk.service.IPlayClerkCommodityService;
import com.starry.admin.modules.clerk.service.IPlayClerkDataReviewInfoService;
import com.starry.admin.modules.clerk.service.IPlayClerkLevelInfoService;
import com.starry.admin.modules.clerk.service.IPlayClerkMediaAssetService;
import com.starry.admin.modules.clerk.service.IPlayClerkUserInfoService;
import com.starry.admin.modules.custom.entity.PlayCustomFollowInfoEntity;
import com.starry.admin.modules.custom.service.IPlayCustomFollowInfoService;
import com.starry.admin.modules.media.entity.PlayMediaEntity;
import com.starry.admin.modules.media.enums.MediaStatus;
import com.starry.admin.modules.media.service.IPlayMediaService;
import com.starry.admin.modules.order.module.entity.PlayOrderInfoEntity;
import com.starry.admin.modules.order.service.IPlayOrderInfoService;
import com.starry.admin.modules.personnel.module.entity.PlayPersonnelAdminInfoEntity;
@@ -51,9 +36,7 @@ import com.starry.admin.modules.personnel.service.IPlayPersonnelGroupInfoService
import com.starry.admin.modules.personnel.service.IPlayPersonnelWaiterInfoService;
import com.starry.admin.modules.statistics.module.vo.PlayClerkPerformanceInfoQueryVo;
import com.starry.admin.modules.system.service.LoginService;
import com.starry.admin.modules.weichat.assembler.ClerkMediaAssembler;
import com.starry.admin.modules.weichat.entity.PlayClerkUserLoginResponseVo;
import com.starry.admin.modules.weichat.entity.clerk.MediaVo;
import com.starry.admin.modules.weichat.entity.clerk.PlayClerkUserInfoQueryVo;
import com.starry.admin.modules.weichat.entity.clerk.PlayClerkUserInfoResultVo;
import com.starry.admin.utils.SecurityUtils;
@@ -63,13 +46,10 @@ import com.starry.common.utils.StringUtils;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
import javax.annotation.Resource;
import org.springframework.stereotype.Service;
@@ -81,11 +61,9 @@ import org.springframework.stereotype.Service;
* @since 2024-03-30
*/
@Service
public class PlayClerkUserInfoServiceImpl extends ServiceImpl<PlayClerkUserInfoMapper, PlayClerkUserInfoEntity> implements IPlayClerkUserInfoService {
private static final String OFFBOARD_MESSAGE = "你已离职,需要复职请联系店铺管理员";
private static final String DELISTED_MESSAGE = "你已被下架,没有权限访问";
private static final String INVALID_CLERK_MESSAGE = "你不是有效店员,无法执行该操作";
public class PlayClerkUserInfoServiceImpl extends ServiceImpl<PlayClerkUserInfoMapper, PlayClerkUserInfoEntity>
implements
IPlayClerkUserInfoService {
@Resource
private PlayClerkUserInfoMapper playClerkUserInfoMapper;
@Resource
@@ -93,14 +71,8 @@ public class PlayClerkUserInfoServiceImpl extends ServiceImpl<PlayClerkUserInfoM
@Resource
private IPlayClerkCommodityService playClerkCommodityService;
@Resource
private IPlayClerkDataReviewInfoService playClerkDataReviewInfoService;
@Resource
private IPlayCustomFollowInfoService customFollowInfoService;
@Resource
private IPlayClerkMediaAssetService clerkMediaAssetService;
@Resource
private IPlayMediaService mediaService;
@Resource
private IPlayBalanceDetailsInfoService playBalanceDetailsInfoService;
@Resource
private IPlayOrderInfoService playOrderInfoService;
@@ -145,18 +117,10 @@ public class PlayClerkUserInfoServiceImpl extends ServiceImpl<PlayClerkUserInfoM
MPJLambdaWrapper<PlayClerkUserInfoEntity> lambdaWrapper = new MPJLambdaWrapper<>();
lambdaWrapper.selectAll(PlayClerkLevelInfoEntity.class);
lambdaWrapper.selectAs(PlayClerkUserInfoEntity::getLevelId, "levelId");
lambdaWrapper.leftJoin(PlayClerkLevelInfoEntity.class, PlayClerkLevelInfoEntity::getId, PlayClerkUserInfoEntity::getLevelId);
lambdaWrapper.leftJoin(PlayClerkLevelInfoEntity.class, PlayClerkLevelInfoEntity::getId,
PlayClerkUserInfoEntity::getLevelId);
lambdaWrapper.eq(PlayClerkUserInfoEntity::getId, clerkId);
PlayClerkLevelInfoEntity levelInfo = this.baseMapper.selectJoinOne(PlayClerkLevelInfoEntity.class, lambdaWrapper);
if (levelInfo != null) {
return levelInfo;
}
PlayClerkUserInfoEntity clerk = this.baseMapper.selectById(clerkId);
if (clerk == null || StringUtils.isBlank(clerk.getLevelId())) {
return null;
}
return playClerkLevelInfoService.getById(clerk.getLevelId());
return this.baseMapper.selectJoinOne(PlayClerkLevelInfoEntity.class, lambdaWrapper);
}
@@ -170,7 +134,8 @@ public class PlayClerkUserInfoServiceImpl extends ServiceImpl<PlayClerkUserInfoM
/**
* 查询店员
*
* @param id 店员主键
* @param id
* 店员主键
* @return 店员
*/
@Override
@@ -185,33 +150,29 @@ public class PlayClerkUserInfoServiceImpl extends ServiceImpl<PlayClerkUserInfoM
@Override
public PlayClerkUserLoginResponseVo getVo(PlayClerkUserInfoEntity userInfo) {
PlayClerkUserLoginResponseVo result = ConvertUtil.entityToVo(userInfo, PlayClerkUserLoginResponseVo.class);
List<PlayClerkDataReviewInfoEntity> pendingReviews = playClerkDataReviewInfoService.queryByClerkId(userInfo.getId(), "0");
if (pendingReviews != null && !pendingReviews.isEmpty()) {
Set<String> pendingTypes = pendingReviews.stream().map(PlayClerkDataReviewInfoEntity::getDataType).filter(StrUtil::isNotBlank).collect(Collectors.toSet());
if (pendingTypes.contains("0")) {
result.setNicknameAllowEdit(false);
}
if (pendingTypes.contains("1")) {
result.setAvatarAllowEdit(false);
}
if (pendingTypes.contains("2")) {
result.setAlbumAllowEdit(false);
}
if (pendingTypes.contains("3")) {
result.setAudioAllowEdit(false);
}
if (pendingTypes.contains("4")) {
result.setSexAllowEdit(false);
}
}
// List<PlayClerkDataReviewInfoEntity> list =
// playClerkDataReviewInfoService.queryByClerkId(userInfo.getId(),"0");
// // 判断头像、音频、相册是否可以编辑,如果存在未审核的数据,则不允许编辑
// Map<String, PlayClerkDataReviewInfoEntity> map =
// list.stream().collect(Collectors.toMap(PlayClerkDataReviewInfoEntity::getDataType,
// account -> account, (entity1, entity2) -> entity1));
// if (map.containsKey("1")) {
// result.setAvatarAllowEdit(false);
// }
// if (map.containsKey("2")) {
// result.setAlbumAllowEdit(false);
// }
// if (map.containsKey("3")) {
// result.setAudioAllowEdit(false);
// }
// 是店员之后,判断是否可以登录
if (ClerkRoleStatus.isClerk(result.getClerkState())) {
if ("1".equals(result.getClerkState())) {
// 设置店员是否运行登录
if (OnboardingStatus.isOffboarded(userInfo.getOnboardingState())) {
if ("0".equals(userInfo.getOnboardingState())) {
result.setAllowLogin("1");
result.setDisableLoginReason("你已离职,需要复职请联系店铺管理员");
}
if (ListingStatus.isDelisted(userInfo.getListingState())) {
if ("0".equals(userInfo.getListingState())) {
result.setAllowLogin("1");
result.setDisableLoginReason("你已被下架,没有权限访问");
}
@@ -220,64 +181,26 @@ public class PlayClerkUserInfoServiceImpl extends ServiceImpl<PlayClerkUserInfoM
// 如果存在未审批的申请,或者当前已经是店员-可以申请陪聊
PlayClerkUserReviewInfoEntity entity = playClerkUserReviewInfoService.queryByClerkId(userInfo.getId(), "0");
if (entity != null || ClerkRoleStatus.isClerk(result.getClerkState())) {
if (entity != null || "1".equals(result.getClerkState())) {
result.setClerkAllowEdit(false);
}
// 查询店员服务项目
List<PlayClerkCommodityEntity> clerkCommodityEntities = playClerkCommodityService.selectCommodityTypeByUser(userInfo.getId(), "");
List<PlayClerkCommodityEntity> clerkCommodityEntities = playClerkCommodityService
.selectCommodityTypeByUser(userInfo.getId(), "");
List<PlayClerkCommodityQueryVo> playClerkCommodityQueryVos = new ArrayList<>();
for (PlayClerkCommodityEntity clerkCommodityEntity : clerkCommodityEntities) {
playClerkCommodityQueryVos.add(new PlayClerkCommodityQueryVo(clerkCommodityEntity.getCommodityName(), clerkCommodityEntity.getEnablingState()));
playClerkCommodityQueryVos.add(new PlayClerkCommodityQueryVo(clerkCommodityEntity.getCommodityName(),
clerkCommodityEntity.getEnablingState()));
}
result.setCommodity(playClerkCommodityQueryVos);
result.setArea(userInfo.getProvince() + "-" + userInfo.getCity());
result.setPcData(this.getPcData(userInfo));
result.setLevelInfo(playClerkLevelInfoService.selectPlayClerkLevelInfoById(userInfo.getLevelId()));
List<MediaVo> mediaList = loadMediaForClerk(userInfo.getId(), true);
result.setMediaList(mergeLegacyAlbum(userInfo.getAlbum(), mediaList));
result.setAlbum(CollectionUtil.isEmpty(userInfo.getAlbum()) ? new ArrayList<>() : new ArrayList<>(userInfo.getAlbum()));
return result;
}
@Override
public void ensureClerkIsActive(PlayClerkUserInfoEntity clerkUserInfoEntity) {
ensureClerkSessionIsValid(clerkUserInfoEntity);
if (!ClerkRoleStatus.isClerk(clerkUserInfoEntity.getClerkState())) {
invalidateClerkSession(clerkUserInfoEntity.getId());
throw new CustomException(INVALID_CLERK_MESSAGE);
}
}
@Override
public void ensureClerkSessionIsValid(PlayClerkUserInfoEntity clerkUserInfoEntity) {
if (Objects.isNull(clerkUserInfoEntity)) {
throw new CustomException("店员不存在");
}
if (Boolean.TRUE.equals(clerkUserInfoEntity.getDeleted())) {
invalidateClerkSession(clerkUserInfoEntity.getId());
throw new CustomException(INVALID_CLERK_MESSAGE);
}
if (OnboardingStatus.isOffboarded(clerkUserInfoEntity.getOnboardingState())) {
invalidateClerkSession(clerkUserInfoEntity.getId());
throw new CustomException(OFFBOARD_MESSAGE);
}
if (ListingStatus.isDelisted(clerkUserInfoEntity.getListingState())) {
invalidateClerkSession(clerkUserInfoEntity.getId());
throw new CustomException(DELISTED_MESSAGE);
}
}
@Override
public void invalidateClerkSession(String clerkId) {
if (StrUtil.isBlank(clerkId)) {
return;
}
LambdaUpdateWrapper<PlayClerkUserInfoEntity> wrapper = Wrappers.lambdaUpdate(PlayClerkUserInfoEntity.class).eq(PlayClerkUserInfoEntity::getId, clerkId).set(PlayClerkUserInfoEntity::getToken, "empty").set(PlayClerkUserInfoEntity::getOnlineState, "0");
this.baseMapper.update(null, wrapper);
}
@Override
public void updateTokenById(String id, String token) {
PlayClerkUserInfoEntity entity = new PlayClerkUserInfoEntity();
@@ -292,17 +215,21 @@ public class PlayClerkUserInfoServiceImpl extends ServiceImpl<PlayClerkUserInfoM
}
@Override
public void updateAccountBalanceById(String userId, BigDecimal balanceBeforeOperation, BigDecimal balanceAfterOperation, String operationType, String operationAction, BigDecimal balanceMoney, String orderId) {
public void updateAccountBalanceById(String userId, BigDecimal balanceBeforeOperation,
BigDecimal balanceAfterOperation, String operationType, String operationAction, BigDecimal balanceMoney,
String orderId) {
// 修改用户余额
this.baseMapper.updateById(new PlayClerkUserInfoEntity(userId, balanceAfterOperation));
// 记录余额变更记录
playBalanceDetailsInfoService.insertBalanceDetailsInfo("0", userId, balanceBeforeOperation, balanceAfterOperation, operationType, operationAction, balanceMoney, BigDecimal.ZERO, orderId);
playBalanceDetailsInfoService.insertBalanceDetailsInfo("0", userId, balanceBeforeOperation,
balanceAfterOperation, operationType, operationAction, balanceMoney, BigDecimal.ZERO, orderId);
}
/**
* 查询店员列表
*
* @param vo 店员查询对象
* @param vo
* 店员查询对象
* @return 店员
*/
@Override
@@ -313,10 +240,12 @@ public class PlayClerkUserInfoServiceImpl extends ServiceImpl<PlayClerkUserInfoM
// 查询不隐藏的
lambdaQueryWrapper.eq(PlayClerkUserInfoEntity::getDisplayState, "1");
// 查询主表全部字段
lambdaQueryWrapper.selectAll(PlayClerkUserInfoEntity.class).selectAs(PlayClerkUserInfoEntity::getCity, "address");
lambdaQueryWrapper.selectAll(PlayClerkUserInfoEntity.class).selectAs(PlayClerkUserInfoEntity::getCity,
"address");
// 等级表
lambdaQueryWrapper.selectAs(PlayClerkLevelInfoEntity::getName, "levelName");
lambdaQueryWrapper.leftJoin(PlayClerkLevelInfoEntity.class, PlayClerkLevelInfoEntity::getId, PlayClerkUserInfoEntity::getLevelId);
lambdaQueryWrapper.leftJoin(PlayClerkLevelInfoEntity.class, PlayClerkLevelInfoEntity::getId,
PlayClerkUserInfoEntity::getLevelId);
// 服务项目表
if (StrUtil.isNotBlank(vo.getNickname())) {
@@ -344,40 +273,18 @@ public class PlayClerkUserInfoServiceImpl extends ServiceImpl<PlayClerkUserInfoM
lambdaQueryWrapper.eq(PlayClerkUserInfoEntity::getOnboardingState, vo.getOnboardingState());
}
// 排序:非空的等级排序号优先,值越小越靠前;同一排序号在线状态优先
lambdaQueryWrapper
.orderByDesc(PlayClerkUserInfoEntity::getOnlineState)
.orderByDesc(PlayClerkUserInfoEntity::getPinToTopState)
.orderByAsc(true, "CASE WHEN t1.order_number IS NULL THEN 1 ELSE 0 END")
.orderByAsc(PlayClerkLevelInfoEntity::getOrderNumber)
.orderByAsc(PlayClerkUserInfoEntity::getCreatedTime)
.orderByAsc(PlayClerkUserInfoEntity::getNickname)
.orderByAsc(PlayClerkUserInfoEntity::getId);
// 排序:置顶状态优先,在线用户其次,最后按创建时间倒序
lambdaQueryWrapper.orderByDesc(PlayClerkUserInfoEntity::getPinToTopState)
.orderByDesc(PlayClerkUserInfoEntity::getOnlineState)
.orderByDesc(PlayClerkUserInfoEntity::getCreatedTime);
IPage<PlayClerkUserInfoResultVo> pageResult = this.baseMapper.selectJoinPage(page,
PlayClerkUserInfoResultVo.class, lambdaQueryWrapper);
if (pageResult != null && pageResult.getRecords() != null) {
List<PlayClerkUserInfoResultVo> deduped = new ArrayList<>();
Set<String> seen = new HashSet<>();
for (PlayClerkUserInfoResultVo record : pageResult.getRecords()) {
String id = record.getId();
if (id == null || !seen.add(id)) {
continue;
}
deduped.add(record);
}
pageResult.setRecords(deduped);
}
if (pageResult != null) {
attachMediaToResultVos(pageResult.getRecords(), false);
}
return pageResult;
return this.baseMapper.selectJoinPage(page, PlayClerkUserInfoResultVo.class, lambdaQueryWrapper);
}
@Override
public List<PlayClerkUserInfoEntity> listAll() {
LambdaQueryWrapper<PlayClerkUserInfoEntity> lambdaQueryWrapper = new LambdaQueryWrapper<>();
lambdaQueryWrapper.eq(PlayClerkUserInfoEntity::getClerkState, ClerkRoleStatus.CLERK.getCode());
lambdaQueryWrapper.eq(PlayClerkUserInfoEntity::getClerkState, "1");
return this.baseMapper.selectList(lambdaQueryWrapper);
}
@@ -386,8 +293,9 @@ public class PlayClerkUserInfoServiceImpl extends ServiceImpl<PlayClerkUserInfoM
public IPage<PlayClerkUnsettledWagesInfoReturnVo> listUnsettledWagesByPage(PlayClerkUnsettledWagesInfoQueryVo vo) {
MPJLambdaWrapper<PlayClerkUserInfoEntity> lambdaQueryWrapper = new MPJLambdaWrapper<>();
// 查询所有店员
lambdaQueryWrapper.selectAs(PlayClerkUserInfoEntity::getNickname, "clerkNickname").selectAs(PlayClerkUserInfoEntity::getId, "clerkId");
lambdaQueryWrapper.eq(PlayClerkUserInfoEntity::getClerkState, ClerkRoleStatus.CLERK.getCode());
lambdaQueryWrapper.selectAs(PlayClerkUserInfoEntity::getNickname, "clerkNickname")
.selectAs(PlayClerkUserInfoEntity::getId, "clerkId");
lambdaQueryWrapper.eq(PlayClerkUserInfoEntity::getClerkState, "1");
// 加入组员的筛选
List<String> clerkIdList = playClerkGroupInfoService.getValidClerkIdList(SecurityUtils.getLoginUser(), null);
lambdaQueryWrapper.in(PlayClerkUserInfoEntity::getId, clerkIdList);
@@ -398,11 +306,14 @@ public class PlayClerkUserInfoServiceImpl extends ServiceImpl<PlayClerkUserInfoM
lambdaQueryWrapper.eq(PlayClerkUserInfoEntity::getListingState, vo.getListingState());
}
// 查询店员订单信息
lambdaQueryWrapper.selectCollection(PlayOrderInfoEntity.class, PlayClerkUnsettledWagesInfoReturnVo::getOrderInfoEntities);
lambdaQueryWrapper.leftJoin(PlayOrderInfoEntity.class, PlayOrderInfoEntity::getAcceptBy, PlayClerkUserInfoEntity::getId);
lambdaQueryWrapper.selectCollection(PlayOrderInfoEntity.class,
PlayClerkUnsettledWagesInfoReturnVo::getOrderInfoEntities);
lambdaQueryWrapper.leftJoin(PlayOrderInfoEntity.class, PlayOrderInfoEntity::getAcceptBy,
PlayClerkUserInfoEntity::getId);
lambdaQueryWrapper.eq(PlayOrderInfoEntity::getOrderSettlementState, "0");
return this.baseMapper.selectJoinPage(new Page<>(vo.getPageNum(), vo.getPageSize()), PlayClerkUnsettledWagesInfoReturnVo.class, lambdaQueryWrapper);
return this.baseMapper.selectJoinPage(new Page<>(vo.getPageNum(), vo.getPageSize()),
PlayClerkUnsettledWagesInfoReturnVo.class, lambdaQueryWrapper);
}
@Override
@@ -456,12 +367,6 @@ public class PlayClerkUserInfoServiceImpl extends ServiceImpl<PlayClerkUserInfoM
if (StrUtil.isNotBlank(vo.getLevelId())) {
lambdaQueryWrapper.eq(PlayClerkUserInfoEntity::getLevelId, vo.getLevelId());
}
if (StrUtil.isNotBlank(vo.getListingState())) {
lambdaQueryWrapper.eq(PlayClerkUserInfoEntity::getListingState, vo.getListingState());
}
if (StrUtil.isNotBlank(vo.getRandomOrderState())) {
lambdaQueryWrapper.eq(PlayClerkUserInfoEntity::getRandomOrderState, vo.getRandomOrderState());
}
if (StrUtil.isNotBlank(vo.getProvince())) {
lambdaQueryWrapper.eq(PlayClerkUserInfoEntity::getProvince, vo.getProvince());
}
@@ -501,9 +406,12 @@ public class PlayClerkUserInfoServiceImpl extends ServiceImpl<PlayClerkUserInfoM
lambdaQueryWrapper.in(PlayClerkUserInfoEntity::getId, clerkIdList);
// 排序:置顶状态优先,在线用户其次,最后按创建时间倒序
lambdaQueryWrapper.orderByDesc(PlayClerkUserInfoEntity::getPinToTopState).orderByDesc(PlayClerkUserInfoEntity::getOnlineState).orderByDesc(PlayClerkUserInfoEntity::getCreatedTime);
lambdaQueryWrapper.orderByDesc(PlayClerkUserInfoEntity::getPinToTopState)
.orderByDesc(PlayClerkUserInfoEntity::getOnlineState)
.orderByDesc(PlayClerkUserInfoEntity::getCreatedTime);
IPage<PlayClerkUserReturnVo> page = this.baseMapper.selectJoinPage(new Page<>(vo.getPageNum(), vo.getPageSize()), PlayClerkUserReturnVo.class, lambdaQueryWrapper);
IPage<PlayClerkUserReturnVo> page = this.baseMapper.selectJoinPage(
new Page<>(vo.getPageNum(), vo.getPageSize()), PlayClerkUserReturnVo.class, lambdaQueryWrapper);
for (PlayClerkUserReturnVo record : page.getRecords()) {
BigDecimal orderTotalAmount = new BigDecimal("0");
@@ -523,7 +431,6 @@ public class PlayClerkUserInfoServiceImpl extends ServiceImpl<PlayClerkUserInfoM
record.setOrderContinueNumber(String.valueOf(orderContinueNumber));
}
attachMediaToAdminVos(page.getRecords());
return page;
}
@@ -535,8 +442,10 @@ public class PlayClerkUserInfoServiceImpl extends ServiceImpl<PlayClerkUserInfoM
if (StrUtil.isNotBlank(customUserId)) {
LambdaQueryWrapper<PlayCustomFollowInfoEntity> lambdaQueryWrapper = new LambdaQueryWrapper<>();
lambdaQueryWrapper.eq(PlayCustomFollowInfoEntity::getCustomId, customUserId);
List<PlayCustomFollowInfoEntity> customFollowInfoEntities = customFollowInfoService.list(lambdaQueryWrapper);
customFollows = customFollowInfoEntities.stream().collect(Collectors.toMap(PlayCustomFollowInfoEntity::getClerkId, PlayCustomFollowInfoEntity::getFollowState));
List<PlayCustomFollowInfoEntity> customFollowInfoEntities = customFollowInfoService
.list(lambdaQueryWrapper);
customFollows = customFollowInfoEntities.stream().collect(Collectors
.toMap(PlayCustomFollowInfoEntity::getClerkId, PlayCustomFollowInfoEntity::getFollowState));
}
for (PlayClerkUserInfoResultVo record : voPage.getRecords()) {
record.setFollowState(customFollows.containsKey(record.getId()) ? "1" : "0");
@@ -548,37 +457,11 @@ public class PlayClerkUserInfoServiceImpl extends ServiceImpl<PlayClerkUserInfoM
return voPage;
}
@Override
public PlayClerkUserInfoResultVo buildCustomerDetail(String clerkId, String customUserId) {
PlayClerkUserInfoEntity entity = this.baseMapper.selectById(clerkId);
if (entity == null) {
throw new CustomException("店员不存在");
}
PlayClerkUserInfoResultVo vo = ConvertUtil.entityToVo(entity, PlayClerkUserInfoResultVo.class);
vo.setAddress(entity.getCity());
vo.setCommodity(playClerkCommodityService.getClerkCommodityList(vo.getId(), "1"));
String followState = "0";
if (StrUtil.isNotBlank(customUserId)) {
LambdaQueryWrapper<PlayCustomFollowInfoEntity> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(PlayCustomFollowInfoEntity::getCustomId, customUserId)
.eq(PlayCustomFollowInfoEntity::getClerkId, clerkId);
PlayCustomFollowInfoEntity followInfo = customFollowInfoService.getOne(wrapper, false);
if (followInfo != null && "1".equals(followInfo.getFollowState())) {
followState = "1";
}
}
vo.setFollowState(followState);
List<MediaVo> mediaList = loadMediaForClerk(clerkId, false);
vo.setMediaList(mergeLegacyAlbum(entity.getAlbum(), mediaList));
return vo;
}
/**
* 新增店员
*
* @param playClerkUserInfo 店员
* @param playClerkUserInfo
* 店员
* @return 结果
*/
@Override
@@ -592,27 +475,20 @@ public class PlayClerkUserInfoServiceImpl extends ServiceImpl<PlayClerkUserInfoM
/**
* 修改店员
*
* @param playClerkUserInfo 店员
* @param playClerkUserInfo
* 店员
* @return 结果
*/
@Override
public boolean update(PlayClerkUserInfoEntity playClerkUserInfo) {
boolean inspectStatus = StringUtils.isNotBlank(playClerkUserInfo.getId()) && (StrUtil.isNotBlank(playClerkUserInfo.getOnboardingState()) || StrUtil.isNotBlank(playClerkUserInfo.getListingState()) || StrUtil.isNotBlank(playClerkUserInfo.getClerkState()));
PlayClerkUserInfoEntity beforeUpdate = null;
if (inspectStatus) {
beforeUpdate = this.baseMapper.selectById(playClerkUserInfo.getId());
}
boolean updated = updateById(playClerkUserInfo);
if (updated && inspectStatus && beforeUpdate != null) {
handleStatusSideEffects(playClerkUserInfo, beforeUpdate);
}
return updated;
return updateById(playClerkUserInfo);
}
/**
* 批量删除店员
*
* @param ids 需要删除的店员主键
* @param ids
* 需要删除的店员主键
* @return 结果
*/
@Override
@@ -623,7 +499,8 @@ public class PlayClerkUserInfoServiceImpl extends ServiceImpl<PlayClerkUserInfoM
/**
* 删除店员信息
*
* @param id 店员主键
* @param id
* 店员主键
* @return 结果
*/
@Override
@@ -637,16 +514,13 @@ public class PlayClerkUserInfoServiceImpl extends ServiceImpl<PlayClerkUserInfoM
LambdaQueryWrapper<PlayClerkUserInfoEntity> lambdaQueryWrapper = new LambdaQueryWrapper<>();
lambdaQueryWrapper.eq(PlayClerkUserInfoEntity::getDeleted, 0);
lambdaQueryWrapper.in(PlayClerkUserInfoEntity::getId, clerkIdList);
lambdaQueryWrapper.select(PlayClerkUserInfoEntity::getId, PlayClerkUserInfoEntity::getNickname, PlayClerkUserInfoEntity::getAvatar, PlayClerkUserInfoEntity::getTypeId, PlayClerkUserInfoEntity::getGroupId, PlayClerkUserInfoEntity::getPhone);
lambdaQueryWrapper.select(PlayClerkUserInfoEntity::getId, PlayClerkUserInfoEntity::getNickname,
PlayClerkUserInfoEntity::getAvatar, PlayClerkUserInfoEntity::getTypeId,
PlayClerkUserInfoEntity::getGroupId, PlayClerkUserInfoEntity::getPhone);
lambdaQueryWrapper.orderByDesc(PlayClerkUserInfoEntity::getId);
return this.baseMapper.selectList(lambdaQueryWrapper);
}
@Override
public List<PlayClerkUserInfoEntity> listWithAlbumIgnoringTenant() {
return playClerkUserInfoMapper.selectAllWithAlbumIgnoringTenant();
}
@Override
public JSONObject getPcData(PlayClerkUserInfoEntity entity) {
JSONObject data = new JSONObject();
@@ -658,7 +532,8 @@ public class PlayClerkUserInfoServiceImpl extends ServiceImpl<PlayClerkUserInfoM
LoginUser loginUserInfo = loginService.getLoginUserInfo(entity.getSysUserId());
Map<String, Object> tokenMap = jwtToken.createToken(loginUserInfo);
data.fluentPut("token", tokenMap.get("token"));
PlayPersonnelAdminInfoEntity adminInfoEntity = playPersonnelAdminInfoService.selectByUserId(entity.getSysUserId());
PlayPersonnelAdminInfoEntity adminInfoEntity = playPersonnelAdminInfoService
.selectByUserId(entity.getSysUserId());
if (Objects.nonNull(adminInfoEntity)) {
data.fluentPut("role", "operator");
return data;
@@ -668,113 +543,12 @@ public class PlayClerkUserInfoServiceImpl extends ServiceImpl<PlayClerkUserInfoM
data.fluentPut("role", "leader");
return data;
}
PlayPersonnelWaiterInfoEntity waiterInfoEntity = playClerkWaiterInfoService.selectByUserId(entity.getSysUserId());
PlayPersonnelWaiterInfoEntity waiterInfoEntity = playClerkWaiterInfoService
.selectByUserId(entity.getSysUserId());
if (Objects.nonNull(waiterInfoEntity)) {
data.fluentPut("role", "waiter");
return data;
}
return data;
}
private void handleStatusSideEffects(PlayClerkUserInfoEntity updatedPayload, PlayClerkUserInfoEntity beforeUpdate) {
if (beforeUpdate == null) {
return;
}
if (OnboardingStatus.transitionedToOffboarded(updatedPayload.getOnboardingState(), beforeUpdate.getOnboardingState()) || ListingStatus.transitionedToDelisted(updatedPayload.getListingState(), beforeUpdate.getListingState()) || ClerkRoleStatus.transitionedToNonClerk(updatedPayload.getClerkState(), beforeUpdate.getClerkState())) {
invalidateClerkSession(beforeUpdate.getId());
}
}
private void attachMediaToResultVos(List<PlayClerkUserInfoResultVo> records, boolean includePending) {
if (CollectionUtil.isEmpty(records)) {
return;
}
Map<String, List<MediaVo>> mediaMap = resolveMediaByAssets(
records.stream().map(PlayClerkUserInfoResultVo::getId).collect(Collectors.toList()), includePending);
for (PlayClerkUserInfoResultVo record : records) {
List<MediaVo> mediaList = new ArrayList<>(mediaMap.getOrDefault(record.getId(), Collections.emptyList()));
record.setMediaList(mergeLegacyAlbum(record.getAlbum(), mediaList));
}
}
private void attachMediaToAdminVos(List<PlayClerkUserReturnVo> records) {
if (CollectionUtil.isEmpty(records)) {
return;
}
Map<String, List<MediaVo>> mediaMap = resolveMediaByAssets(
records.stream().map(PlayClerkUserReturnVo::getId).collect(Collectors.toList()), true);
for (PlayClerkUserReturnVo record : records) {
List<MediaVo> mediaList = new ArrayList<>(mediaMap.getOrDefault(record.getId(), Collections.emptyList()));
record.setMediaList(mergeLegacyAlbum(record.getAlbum(), mediaList));
}
}
private List<MediaVo> loadMediaForClerk(String clerkId, boolean includePending) {
Map<String, List<MediaVo>> mediaMap = resolveMediaByAssets(Collections.singletonList(clerkId), includePending);
return new ArrayList<>(mediaMap.getOrDefault(clerkId, Collections.emptyList()));
}
private Map<String, List<MediaVo>> resolveMediaByAssets(List<String> clerkIds, boolean includePending) {
if (CollectionUtil.isEmpty(clerkIds)) {
return Collections.emptyMap();
}
List<ClerkMediaReviewState> targetStates = includePending
? Arrays.asList(ClerkMediaReviewState.APPROVED, ClerkMediaReviewState.PENDING,
ClerkMediaReviewState.DRAFT, ClerkMediaReviewState.REJECTED)
: Collections.singletonList(ClerkMediaReviewState.APPROVED);
List<PlayClerkMediaAssetEntity> assets = clerkMediaAssetService.lambdaQuery()
.in(PlayClerkMediaAssetEntity::getClerkId, clerkIds)
.eq(PlayClerkMediaAssetEntity::getUsage, ClerkMediaUsage.PROFILE.getCode())
.eq(PlayClerkMediaAssetEntity::getDeleted, false)
.in(CollectionUtil.isNotEmpty(targetStates), PlayClerkMediaAssetEntity::getReviewState,
targetStates.stream().map(ClerkMediaReviewState::getCode).collect(Collectors.toList()))
.orderByAsc(PlayClerkMediaAssetEntity::getOrderIndex)
.orderByDesc(PlayClerkMediaAssetEntity::getCreatedTime)
.list();
if (CollectionUtil.isEmpty(assets)) {
Map<String, List<MediaVo>> empty = new HashMap<>();
clerkIds.forEach(id -> empty.put(id, Collections.emptyList()));
return empty;
}
List<String> mediaIds = assets.stream().map(PlayClerkMediaAssetEntity::getMediaId).distinct()
.collect(Collectors.toList());
Map<String, PlayMediaEntity> mediaById = CollectionUtil.isEmpty(mediaIds)
? Collections.emptyMap()
: mediaService.listByIds(mediaIds).stream()
.collect(Collectors.toMap(PlayMediaEntity::getId, item -> item, (left, right) -> left));
Map<String, List<PlayClerkMediaAssetEntity>> groupedAssets = assets.stream()
.collect(Collectors.groupingBy(PlayClerkMediaAssetEntity::getClerkId));
Map<String, List<MediaVo>> result = new HashMap<>(groupedAssets.size());
groupedAssets.forEach((clerkId, assetList) -> result.put(clerkId, ClerkMediaAssembler.toVoList(assetList, mediaById)));
clerkIds.forEach(id -> result.computeIfAbsent(id, key -> Collections.emptyList()));
return result;
}
static List<MediaVo> mergeLegacyAlbum(List<String> legacyAlbum, List<MediaVo> destination) {
if (CollectionUtil.isEmpty(legacyAlbum)) {
return destination;
}
Set<String> existingUrls = destination.stream()
.map(MediaVo::getUrl)
.filter(StrUtil::isNotBlank)
.collect(Collectors.toSet());
for (String url : legacyAlbum) {
if (StrUtil.isBlank(url) || !existingUrls.add(url)) {
continue;
}
MediaVo legacyVo = new MediaVo();
legacyVo.setId(url);
legacyVo.setUrl(url);
legacyVo.setUsage(ClerkMediaUsage.PROFILE.getCode());
legacyVo.setStatus(MediaStatus.READY.getCode());
legacyVo.setReviewState(ClerkMediaReviewState.APPROVED.getCode());
destination.add(legacyVo);
}
return destination;
}
}

View File

@@ -1,6 +1,5 @@
package com.starry.admin.modules.clerk.service.impl;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
@@ -17,8 +16,7 @@ import com.starry.admin.modules.clerk.module.vo.PlayClerkUserReviewStateEditVo;
import com.starry.admin.modules.clerk.service.IPlayClerkCommodityService;
import com.starry.admin.modules.clerk.service.IPlayClerkUserInfoService;
import com.starry.admin.modules.clerk.service.IPlayClerkUserReviewInfoService;
import com.starry.admin.modules.order.module.constant.OrderConstant;
import com.starry.admin.modules.weichat.service.NotificationSender;
import com.starry.admin.modules.weichat.service.WxCustomMpService;
import com.starry.common.utils.IdUtils;
import java.util.Arrays;
import javax.annotation.Resource;
@@ -45,7 +43,7 @@ public class PlayClerkUserReviewInfoServiceImpl
@Resource
private IPlayClerkCommodityService clerkCommodityService;
@Resource
private NotificationSender notificationSender;
private WxCustomMpService wxCustomMpService;
@Override
public PlayClerkUserReviewInfoEntity queryByClerkId(String clerkId, String reviewState) {
@@ -93,19 +91,11 @@ public class PlayClerkUserReviewInfoServiceImpl
lambdaQueryWrapper.like(PlayClerkUserReviewInfoEntity::getPhone, vo.getPhone());
}
if (StrUtil.isNotBlank(vo.getReviewState())) {
lambdaQueryWrapper.eq(PlayClerkUserReviewInfoEntity::getReviewState, vo.getReviewState());
lambdaQueryWrapper.like(PlayClerkUserReviewInfoEntity::getReviewState, vo.getReviewState());
}
if (StrUtil.isNotBlank(vo.getWeiChatCode())) {
lambdaQueryWrapper.like(PlayClerkUserReviewInfoEntity::getWeiChatCode, vo.getWeiChatCode());
}
if (CollUtil.isNotEmpty(vo.getAddTime())) {
if (vo.getAddTime().size() >= 2) {
lambdaQueryWrapper.between(PlayClerkUserReviewInfoEntity::getAddTime, vo.getAddTime().get(0),
vo.getAddTime().get(1));
} else if (vo.getAddTime().size() == 1) {
lambdaQueryWrapper.ge(PlayClerkUserReviewInfoEntity::getAddTime, vo.getAddTime().get(0));
}
}
// 加入组员的筛选
// List<String> clerkIdList =
// playClerkGroupInfoService.getValidClerkIdList(SecurityUtils.getLoginUser(),
@@ -173,11 +163,8 @@ public class PlayClerkUserReviewInfoServiceImpl
userInfo.setClerkState("1");
userInfo.setId(entity.getClerkId());
userInfo.setAlbum(entity.getAlbum());
if (StrUtil.isNotBlank(vo.getGroupId())) {
userInfo.setGroupId(vo.getGroupId());
}
if(OrderConstant.Gender.UNKNOWN.getCode().equals(entity.getSex())){
userInfo.setSex(OrderConstant.Gender.FEMALE.getCode());
if(entity.getSex().equals("0")){
userInfo.setSex("2");
}
playClerkUserInfoService.update(userInfo);
clerkCommodityService.initClerkCommodity(userInfo.getId());
@@ -186,7 +173,7 @@ public class PlayClerkUserReviewInfoServiceImpl
this.update(entity);
// 发送消息
notificationSender.sendCheckMessage(entity, userInfo, vo.getReviewState());
wxCustomMpService.sendCheckMessage(entity, userInfo, vo.getReviewState());
}
/**

View File

@@ -1,132 +0,0 @@
package com.starry.admin.modules.clerk.task;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.util.StrUtil;
import com.starry.admin.modules.clerk.enums.ClerkMediaUsage;
import com.starry.admin.modules.clerk.module.entity.PlayClerkUserInfoEntity;
import com.starry.admin.modules.clerk.service.IPlayClerkMediaAssetService;
import com.starry.admin.modules.clerk.service.IPlayClerkUserInfoService;
import com.starry.admin.modules.media.entity.PlayMediaEntity;
import com.starry.admin.modules.media.enums.MediaKind;
import com.starry.admin.modules.media.enums.MediaOwnerType;
import com.starry.admin.modules.media.enums.MediaStatus;
import com.starry.admin.modules.media.service.IPlayMediaService;
import com.starry.admin.utils.SecurityUtils;
import com.starry.common.utils.IdUtils;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
/**
* 一次性迁移旧相册数据到媒资表。启用方式:启动时配置
* {@code clerk.media.migration-enabled=true}。
*/
@Component
@ConditionalOnProperty(prefix = "clerk.media", name = "migration-enabled", havingValue = "true")
@RequiredArgsConstructor
@Slf4j
public class ClerkAlbumMigrationRunner implements ApplicationRunner {
private final IPlayClerkUserInfoService clerkUserInfoService;
private final IPlayMediaService mediaService;
private final IPlayClerkMediaAssetService clerkMediaAssetService;
@Override
@Transactional(rollbackFor = Exception.class)
public void run(ApplicationArguments args) {
log.info("[ClerkAlbumMigration] start migration from legacy album column");
List<PlayClerkUserInfoEntity> candidates = clerkUserInfoService.listWithAlbumIgnoringTenant();
if (CollectionUtil.isEmpty(candidates)) {
log.info("[ClerkAlbumMigration] no clerk records with legacy album found, skip");
return;
}
AtomicInteger migratedOwners = new AtomicInteger();
AtomicInteger migratedMedia = new AtomicInteger();
String originalTenantId = SecurityUtils.getTenantId();
for (PlayClerkUserInfoEntity clerk : candidates) {
String tenantId = StrUtil.blankToDefault(clerk.getTenantId(), originalTenantId);
SecurityUtils.setTenantId(tenantId);
try {
List<String> album = clerk.getAlbum();
if (CollectionUtil.isEmpty(album)) {
continue;
}
List<String> sanitizedAlbum = album.stream()
.filter(StrUtil::isNotBlank)
.map(String::trim)
.distinct()
.collect(Collectors.toList());
if (CollectionUtil.isEmpty(sanitizedAlbum)) {
continue;
}
List<String> approvedMediaIds = new ArrayList<>();
for (String value : sanitizedAlbum) {
PlayMediaEntity media = resolveMediaEntity(clerk, value);
if (media == null) {
continue;
}
media.setStatus(MediaStatus.READY.getCode());
mediaService.updateById(media);
clerkMediaAssetService.linkDraftAsset(clerk.getTenantId(), clerk.getId(), media.getId(),
ClerkMediaUsage.PROFILE);
approvedMediaIds.add(media.getId());
}
clerkMediaAssetService.applyReviewDecision(clerk.getId(), ClerkMediaUsage.PROFILE, approvedMediaIds,
null, null);
migratedOwners.incrementAndGet();
migratedMedia.addAndGet(approvedMediaIds.size());
log.info("[ClerkAlbumMigration] processed {} media for clerk {}", approvedMediaIds.size(),
clerk.getId());
} finally {
SecurityUtils.setTenantId(originalTenantId);
}
}
log.info("[ClerkAlbumMigration] completed, owners migrated: {}, media migrated: {}", migratedOwners.get(),
migratedMedia.get());
}
private PlayMediaEntity resolveMediaEntity(PlayClerkUserInfoEntity clerk, String value) {
if (StrUtil.isBlank(value)) {
return null;
}
PlayMediaEntity byId = mediaService.getById(value);
if (byId != null) {
return byId;
}
PlayMediaEntity byUrl = mediaService.lambdaQuery()
.eq(PlayMediaEntity::getOwnerType, MediaOwnerType.CLERK)
.eq(PlayMediaEntity::getOwnerId, clerk.getId())
.eq(PlayMediaEntity::getUrl, value)
.last("limit 1")
.one();
if (byUrl != null) {
return byUrl;
}
PlayMediaEntity media = new PlayMediaEntity();
media.setId(IdUtils.getUuid());
media.setTenantId(clerk.getTenantId());
media.setOwnerType(MediaOwnerType.CLERK);
media.setOwnerId(clerk.getId());
media.setKind(MediaKind.IMAGE.getCode());
media.setStatus(MediaStatus.READY.getCode());
media.setUrl(value);
Map<String, Object> metadata = new HashMap<>();
metadata.put("legacySource", "album_migration");
media.setMetadata(metadata);
mediaService.normalizeAndSave(media);
return media;
}
}

View File

@@ -1,44 +0,0 @@
package com.starry.admin.modules.common.controller;
import cn.hutool.core.util.StrUtil;
import com.starry.admin.common.exception.CustomException;
import com.starry.admin.common.oss.service.IOssFileService;
import com.starry.admin.utils.SecurityUtils;
import com.starry.common.result.R;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import java.io.IOException;
import javax.annotation.Resource;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
/**
* 后台通用能力接口
*/
@Api(tags = "后台通用接口", description = "提供后台常用的公共能力,例如文件上传")
@RestController
@RequestMapping("/common")
public class CommonController {
@Resource
private IOssFileService ossFileService;
@ApiOperation(value = "上传文件", notes = "上传文件到 OSS 并返回访问地址")
@PostMapping("/upload")
public R upload(@ApiParam(value = "上传文件", required = true) @RequestParam("file") MultipartFile file)
throws IOException {
if (file == null || file.isEmpty()) {
throw new CustomException("上传文件不能为空");
}
String tenantId = SecurityUtils.getTenantId();
if (StrUtil.isBlank(tenantId)) {
throw new CustomException("租户信息缺失,请重新登录");
}
String fileUrl = ossFileService.upload(file.getInputStream(), tenantId, file.getOriginalFilename());
return R.ok(fileUrl);
}
}

View File

@@ -2,8 +2,6 @@ package com.starry.admin.modules.custom.mapper;
import com.github.yulichang.base.MPJBaseMapper;
import com.starry.admin.modules.custom.module.entity.PlayCustomGiftInfoEntity;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Update;
/**
* 顾客和礼物关系Mapper接口
@@ -13,25 +11,4 @@ import org.apache.ibatis.annotations.Update;
*/
public interface PlayCustomGiftInfoMapper extends MPJBaseMapper<PlayCustomGiftInfoEntity> {
/**
* 原子递增顾客礼物数量
*
* @param customId 顾客ID
* @param giftId 礼物ID
* @param tenantId 租户ID
* @param delta 增量
* @return 受影响行数
*/
@Update("UPDATE play_custom_gift_info "
+ "SET giff_number = giff_number + #{delta} "
+ "WHERE custom_id = #{customId} AND giff_id = #{giftId} "
+ "AND (tenant_id = #{tenantId} OR tenant_id IS NULL)")
int incrementGiftCount(@Param("customId") String customId, @Param("giftId") String giftId,
@Param("tenantId") String tenantId, @Param("delta") long delta);
@Update("UPDATE play_custom_gift_info SET giff_number = 0, deleted = 0 "
+ "WHERE tenant_id = #{tenantId} AND custom_id = #{customId} AND giff_id = #{giftId}")
int resetGiftCount(@Param("tenantId") String tenantId, @Param("customId") String customId,
@Param("giftId") String giftId);
}

View File

@@ -2,10 +2,6 @@ package com.starry.admin.modules.custom.mapper;
import com.github.yulichang.base.MPJBaseMapper;
import com.starry.admin.modules.custom.module.entity.PlayCustomUserInfoEntity;
import java.math.BigDecimal;
import java.util.Date;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Update;
/**
* 顾客Mapper接口
@@ -15,18 +11,4 @@ import org.apache.ibatis.annotations.Update;
*/
public interface PlayCustomUserInfoMapper extends MPJBaseMapper<PlayCustomUserInfoEntity> {
@Update({
"<script>",
"UPDATE play_custom_user_info",
"SET accumulated_consumption_amount = COALESCE(accumulated_consumption_amount, 0) + #{consumptionDelta},",
" last_purchase_time = #{completionTime},",
" first_purchase_time = CASE WHEN first_purchase_time IS NULL THEN #{completionTime} ELSE first_purchase_time END",
" <if test='weiChatCode != null and weiChatCode != \"\"'>, wei_chat_code = #{weiChatCode}</if>",
"WHERE id = #{userId}",
"</script>"
})
int applyOrderCompletionUpdate(@Param("userId") String userId,
@Param("consumptionDelta") BigDecimal consumptionDelta,
@Param("completionTime") Date completionTime,
@Param("weiChatCode") String weiChatCode);
}

View File

@@ -31,7 +31,7 @@ public interface IPlayCustomGiftInfoService extends IService<PlayCustomGiftInfoE
* 顾客IF
* @return 顾客已点亮礼物列表
*/
List<PlayCustomGiftInfoEntity> selectByCustomId(String customId, String tenantId);
List<PlayCustomGiftInfoEntity> selectBtyCustomId(String customId);
/**
* 查询顾客和礼物关系
@@ -69,16 +69,6 @@ public interface IPlayCustomGiftInfoService extends IService<PlayCustomGiftInfoE
*/
boolean update(PlayCustomGiftInfoEntity playCustomGiftInfo);
/**
* 原子递增顾客礼物数量,无记录时按增量初始化
*
* @param customId 顾客ID
* @param giftId 礼物ID
* @param tenantId 租户ID
* @param delta 增量
*/
void incrementGiftCount(String customId, String giftId, String tenantId, long delta);
/**
* 批量删除顾客和礼物关系
*

View File

@@ -3,7 +3,6 @@ package com.starry.admin.modules.custom.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.starry.admin.modules.custom.module.entity.PlayCustomLevelInfoEntity;
import com.starry.admin.modules.system.module.entity.SysTenantEntity;
import java.math.BigDecimal;
import java.util.List;
/**
@@ -101,13 +100,4 @@ public interface IPlayCustomLevelInfoService extends IService<PlayCustomLevelInf
* 删除最大等级
*/
void delMaxLevelByLevel(Integer level);
/**
* 根据累计消费金额匹配顾客等级。
*
* @param tenantId 租户ID
* @param totalConsumption 累计消费金额
* @return 匹配到的等级,未匹配则返回 {@code null}
*/
PlayCustomLevelInfoEntity matchLevelByConsumption(String tenantId, BigDecimal totalConsumption);
}

View File

@@ -172,11 +172,4 @@ public interface IPlayCustomUserInfoService extends IService<PlayCustomUserInfoE
* @author admin
**/
void saveOrderInfo(PlayOrderInfoEntity entity);
/**
* 处理订单完成后的顾客统计更新。
*
* @param entity 完成的订单实体
*/
void handleOrderCompletion(PlayOrderInfoEntity entity);
}

View File

@@ -12,7 +12,6 @@ import com.starry.common.utils.IdUtils;
import java.util.Arrays;
import java.util.List;
import javax.annotation.Resource;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.stereotype.Service;
/**
@@ -44,12 +43,9 @@ public class PlayCustomGiftInfoServiceImpl extends ServiceImpl<PlayCustomGiftInf
* @return 店员活动礼物列表
*/
@Override
public List<PlayCustomGiftInfoEntity> selectByCustomId(String customId, String tenantId) {
public List<PlayCustomGiftInfoEntity> selectBtyCustomId(String customId) {
LambdaQueryWrapper<PlayCustomGiftInfoEntity> lambdaQueryWrapper = new LambdaQueryWrapper<>();
lambdaQueryWrapper.eq(PlayCustomGiftInfoEntity::getCustomId, customId);
if (StrUtil.isNotBlank(tenantId)) {
lambdaQueryWrapper.eq(PlayCustomGiftInfoEntity::getTenantId, tenantId);
}
return this.baseMapper.selectList(lambdaQueryWrapper);
}
@@ -105,39 +101,6 @@ public class PlayCustomGiftInfoServiceImpl extends ServiceImpl<PlayCustomGiftInf
return updateById(playCustomGiftInfo);
}
/**
* 原子递增顾客礼物数量,无记录时按增量初始化
*
* @param customId 顾客ID
* @param giftId 礼物ID
* @param tenantId 租户ID
* @param delta 增量
*/
@Override
public void incrementGiftCount(String customId, String giftId, String tenantId, long delta) {
if (delta <= 0) {
throw new IllegalArgumentException("delta must be positive");
}
int updated = playCustomGiftInfoMapper.incrementGiftCount(customId, giftId, tenantId, delta);
if (updated == 0) {
PlayCustomGiftInfoEntity entity = new PlayCustomGiftInfoEntity();
entity.setId(IdUtils.getUuid());
entity.setCustomId(customId);
entity.setTenantId(tenantId);
entity.setGiffId(giftId);
entity.setGiffNumber(delta);
boolean inserted = false;
try {
inserted = this.save(entity);
} catch (DuplicateKeyException ex) {
// ignore and retry update below
}
if (!inserted) {
playCustomGiftInfoMapper.incrementGiftCount(customId, giftId, tenantId, delta);
}
}
}
/**
* 批量删除顾客和礼物关系
*

View File

@@ -9,10 +9,8 @@ import com.starry.admin.modules.custom.module.entity.PlayCustomLevelInfoEntity;
import com.starry.admin.modules.custom.service.IPlayCustomLevelInfoService;
import com.starry.admin.modules.system.module.entity.SysTenantEntity;
import com.starry.common.utils.IdUtils;
import java.math.BigDecimal;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import javax.annotation.Resource;
import org.springframework.stereotype.Service;
@@ -155,39 +153,4 @@ public class PlayCustomLevelInfoServiceImpl extends ServiceImpl<PlayCustomLevelI
queryWrapper.eq(PlayCustomLevelInfoEntity::getLevel, level);
this.baseMapper.delete(queryWrapper);
}
@Override
public PlayCustomLevelInfoEntity matchLevelByConsumption(String tenantId, BigDecimal totalConsumption) {
BigDecimal consumption = Objects.requireNonNullElse(totalConsumption, BigDecimal.ZERO);
LambdaQueryWrapper<PlayCustomLevelInfoEntity> lambdaQueryWrapper = new LambdaQueryWrapper<>();
lambdaQueryWrapper.orderByAsc(PlayCustomLevelInfoEntity::getLevel);
if (StrUtil.isNotBlank(tenantId)) {
lambdaQueryWrapper.eq(PlayCustomLevelInfoEntity::getTenantId, tenantId);
}
List<PlayCustomLevelInfoEntity> levels = this.baseMapper.selectList(lambdaQueryWrapper);
if (levels == null || levels.isEmpty()) {
return null;
}
PlayCustomLevelInfoEntity matched = null;
for (PlayCustomLevelInfoEntity level : levels) {
BigDecimal threshold = parseConsumptionAmount(level.getConsumptionAmount());
if (consumption.compareTo(threshold) >= 0) {
matched = level;
} else {
break;
}
}
return matched != null ? matched : levels.get(0);
}
private BigDecimal parseConsumptionAmount(String rawValue) {
if (StrUtil.isBlank(rawValue)) {
return BigDecimal.ZERO;
}
try {
return new BigDecimal(rawValue.trim());
} catch (NumberFormatException ex) {
return BigDecimal.ZERO;
}
}
}

View File

@@ -16,7 +16,6 @@ import com.starry.admin.modules.custom.module.vo.PlayCustomRankingQueryVo;
import com.starry.admin.modules.custom.module.vo.PlayCustomRankingReturnVo;
import com.starry.admin.modules.custom.module.vo.PlayCustomUserQueryVo;
import com.starry.admin.modules.custom.module.vo.PlayCustomUserReturnVo;
import com.starry.admin.modules.custom.service.IPlayCustomLevelInfoService;
import com.starry.admin.modules.custom.service.IPlayCustomUserInfoService;
import com.starry.admin.modules.order.module.entity.PlayOrderInfoEntity;
import com.starry.admin.modules.order.service.impl.PlayOrderInfoServiceImpl;
@@ -24,8 +23,6 @@ import com.starry.admin.modules.personnel.service.IPlayBalanceDetailsInfoService
import com.starry.common.utils.IdUtils;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.*;
import java.util.stream.Collectors;
import javax.annotation.Resource;
@@ -52,9 +49,6 @@ public class PlayCustomUserInfoServiceImpl extends ServiceImpl<PlayCustomUserInf
@Resource
private IPlayBalanceDetailsInfoService playBalanceDetailsInfoService;
@Resource
private IPlayCustomLevelInfoService playCustomLevelInfoService;
@Override
public PlayCustomUserInfoEntity selectByOpenid(String openId) {
LambdaQueryWrapper<PlayCustomUserInfoEntity> lambdaQueryWrapper = new LambdaQueryWrapper<>();
@@ -375,50 +369,6 @@ public class PlayCustomUserInfoServiceImpl extends ServiceImpl<PlayCustomUserInf
return baseMapper.selectList(wrapper);
}
@Override
public void handleOrderCompletion(PlayOrderInfoEntity entity) {
if (entity == null || StrUtil.isBlank(entity.getPurchaserBy())) {
return;
}
PlayCustomUserInfoEntity userInfo = playCustomUserInfoMapper.selectById(entity.getPurchaserBy());
if (userInfo == null) {
log.warn("handleOrderCompletion skipped, userId={} missing, orderId={}", entity.getPurchaserBy(), entity.getId());
return;
}
BigDecimal finalAmount = Objects.requireNonNullElse(entity.getFinalAmount(), BigDecimal.ZERO);
Date completionTime = resolveCompletionTime(entity.getOrderEndTime());
int affected = playCustomUserInfoMapper.applyOrderCompletionUpdate(
userInfo.getId(),
finalAmount,
completionTime,
entity.getWeiChatCode());
if (affected == 0) {
log.warn("handleOrderCompletion update skipped for userId={}, orderId={}", userInfo.getId(), entity.getId());
return;
}
PlayCustomUserInfoEntity latest = playCustomUserInfoMapper.selectById(userInfo.getId());
if (latest == null) {
return;
}
PlayCustomLevelInfoEntity matchedLevel = playCustomLevelInfoService
.matchLevelByConsumption(latest.getTenantId(), latest.getAccumulatedConsumptionAmount());
if (matchedLevel != null && !StrUtil.equals(matchedLevel.getId(), latest.getLevelId())) {
this.update(Wrappers.<PlayCustomUserInfoEntity>lambdaUpdate()
.eq(PlayCustomUserInfoEntity::getId, latest.getId())
.set(PlayCustomUserInfoEntity::getLevelId, matchedLevel.getId()));
log.info("顾客{}消费累计达到{},自动调整等级为{}", latest.getId(), latest.getAccumulatedConsumptionAmount(), matchedLevel.getName());
}
}
private Date resolveCompletionTime(LocalDateTime orderEndTime) {
LocalDateTime time = orderEndTime != null ? orderEndTime : LocalDateTime.now();
return Date.from(time.atZone(ZoneId.systemDefault()).toInstant());
}
@Override
public void saveOrderInfo(PlayOrderInfoEntity entity) {
String id = entity.getPurchaserBy();

View File

@@ -1,92 +0,0 @@
package com.starry.admin.modules.media.entity;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
import java.util.Date;
import java.util.Map;
import lombok.Data;
/**
* 媒资表 play_media
*
* <p>存储各类业务(店员、顾客等)的图片/视频。</p>
*/
@Data
@TableName(value = "play_media", autoResultMap = true)
public class PlayMediaEntity {
@TableId
private String id;
/**
* 租户ID
*/
private String tenantId;
/**
* 归属业务类型,例如 clerk/custom/order
*/
private String ownerType;
/**
* 归属业务主键例如店员ID
*/
private String ownerId;
/**
* 媒资类型 image / video
*/
private String kind;
/**
* 媒资状态 uploaded / processing / ready / approved / rejected
*/
private String status;
/**
* 资源地址
*/
private String url;
/**
* 视频封面地址
*/
private String coverUrl;
/**
* 时长(毫秒)
*/
private Long durationMs;
/**
* 媒资宽度
*/
private Integer width;
/**
* 媒资高度
*/
private Integer height;
/**
* 文件大小(字节)
*/
private Long sizeBytes;
/**
* 排序序号,从 0 开始
*/
private Integer orderIndex;
/**
* 扩展字段
*/
@TableField(typeHandler = JacksonTypeHandler.class)
private Map<String, Object> metadata;
private Date createdTime;
private Date updatedTime;
}

View File

@@ -1,33 +0,0 @@
package com.starry.admin.modules.media.enums;
import lombok.Getter;
@Getter
public enum MediaKind {
IMAGE("image"),
VIDEO("video");
private final String code;
MediaKind(String code) {
this.code = code;
}
public static boolean isVideo(String value) {
return VIDEO.code.equalsIgnoreCase(value);
}
public static boolean isImage(String value) {
return IMAGE.code.equalsIgnoreCase(value);
}
public static MediaKind fromCode(String value) {
for (MediaKind kind : values()) {
if (kind.code.equalsIgnoreCase(value)) {
return kind;
}
}
throw new IllegalArgumentException("Unsupported media kind: " + value);
}
}

View File

@@ -1,9 +0,0 @@
package com.starry.admin.modules.media.enums;
public final class MediaOwnerType {
private MediaOwnerType() {
}
public static final String CLERK = "clerk";
}

View File

@@ -1,22 +0,0 @@
package com.starry.admin.modules.media.enums;
import lombok.Getter;
@Getter
public enum MediaStatus {
UPLOADED("uploaded"),
PROCESSING("processing"),
READY("ready"),
APPROVED("approved"),
REJECTED("rejected");
private final String code;
MediaStatus(String code) {
this.code = code;
}
public static boolean isTerminal(String value) {
return APPROVED.code.equalsIgnoreCase(value) || REJECTED.code.equalsIgnoreCase(value);
}
}

View File

@@ -1,8 +0,0 @@
package com.starry.admin.modules.media.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.starry.admin.modules.media.entity.PlayMediaEntity;
public interface PlayMediaMapper extends BaseMapper<PlayMediaEntity> {
}

View File

@@ -1,21 +0,0 @@
package com.starry.admin.modules.media.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.starry.admin.modules.media.entity.PlayMediaEntity;
import java.util.Collection;
import java.util.List;
public interface IPlayMediaService extends IService<PlayMediaEntity> {
List<PlayMediaEntity> listByOwner(String ownerType, String ownerId);
List<PlayMediaEntity> listByOwner(String ownerType, String ownerId, Collection<String> statuses);
List<PlayMediaEntity> listApprovedByOwner(String ownerType, String ownerId);
PlayMediaEntity normalizeAndSave(PlayMediaEntity entity);
void updateOrder(String ownerType, String ownerId, List<String> orderedIds);
void softDelete(String ownerType, String ownerId, String mediaId);
}

View File

@@ -1,136 +0,0 @@
package com.starry.admin.modules.media.service.impl;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.starry.admin.common.exception.CustomException;
import com.starry.admin.modules.media.entity.PlayMediaEntity;
import com.starry.admin.modules.media.enums.MediaOwnerType;
import com.starry.admin.modules.media.enums.MediaStatus;
import com.starry.admin.modules.media.mapper.PlayMediaMapper;
import com.starry.admin.modules.media.service.IPlayMediaService;
import java.util.*;
import java.util.stream.Collectors;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class PlayMediaServiceImpl extends ServiceImpl<PlayMediaMapper, PlayMediaEntity>
implements IPlayMediaService {
@Override
public List<PlayMediaEntity> listByOwner(String ownerType, String ownerId) {
return listByOwner(ownerType, ownerId, null);
}
@Override
public List<PlayMediaEntity> listByOwner(String ownerType, String ownerId, Collection<String> statuses) {
LambdaQueryWrapper<PlayMediaEntity> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(PlayMediaEntity::getOwnerType, ownerType)
.eq(PlayMediaEntity::getOwnerId, ownerId)
.orderByAsc(PlayMediaEntity::getOrderIndex)
.orderByDesc(PlayMediaEntity::getCreatedTime);
if (CollectionUtil.isNotEmpty(statuses)) {
wrapper.in(PlayMediaEntity::getStatus, statuses);
}
return this.list(wrapper);
}
@Override
public List<PlayMediaEntity> listApprovedByOwner(String ownerType, String ownerId) {
return listByOwner(ownerType, ownerId, Collections.singleton(MediaStatus.READY.getCode()));
}
@Override
@Transactional(rollbackFor = Exception.class)
public PlayMediaEntity normalizeAndSave(PlayMediaEntity entity) {
Assert.notNull(entity, "媒资信息不能为空");
Assert.isTrue(StrUtil.isNotBlank(entity.getOwnerId()), "媒资归属ID不能为空");
// ownerType 默认 clerk
if (StrUtil.isBlank(entity.getOwnerType())) {
entity.setOwnerType(MediaOwnerType.CLERK);
}
if (entity.getOrderIndex() == null) {
entity.setOrderIndex(resolveNextOrderIndex(entity.getOwnerType(), entity.getOwnerId()));
}
if (StrUtil.isBlank(entity.getStatus())) {
entity.setStatus(MediaStatus.UPLOADED.getCode());
}
boolean saved = this.save(entity);
if (!saved) {
throw new CustomException("媒资保存失败");
}
return entity;
}
@Override
@Transactional(rollbackFor = Exception.class)
public void updateOrder(String ownerType, String ownerId, List<String> orderedIds) {
List<PlayMediaEntity> mediaList = listByOwner(ownerType, ownerId);
if (CollectionUtil.isEmpty(mediaList)) {
return;
}
Map<String, PlayMediaEntity> mediaById = mediaList.stream()
.collect(Collectors.toMap(PlayMediaEntity::getId, item -> item));
Set<String> keepSet = new LinkedHashSet<>();
if (CollectionUtil.isNotEmpty(orderedIds)) {
keepSet.addAll(orderedIds);
}
List<PlayMediaEntity> updates = new ArrayList<>();
int index = 0;
for (String mediaId : keepSet) {
PlayMediaEntity entity = mediaById.get(mediaId);
if (entity == null) {
throw new CustomException("媒资不存在或已被删除");
}
entity.setOrderIndex(index++);
if (MediaStatus.REJECTED.getCode().equals(entity.getStatus())) {
entity.setStatus(MediaStatus.READY.getCode());
}
updates.add(entity);
}
// 其他未保留的标记为 rejected
for (PlayMediaEntity entity : mediaList) {
if (!keepSet.contains(entity.getId())
&& !MediaStatus.REJECTED.getCode().equals(entity.getStatus())) {
entity.setStatus(MediaStatus.REJECTED.getCode());
entity.setOrderIndex(0);
updates.add(entity);
}
}
if (CollectionUtil.isNotEmpty(updates)) {
this.updateBatchById(updates);
}
}
@Override
@Transactional(rollbackFor = Exception.class)
public void softDelete(String ownerType, String ownerId, String mediaId) {
PlayMediaEntity entity = this.getById(mediaId);
if (entity == null) {
return;
}
if (!ownerType.equals(entity.getOwnerType()) || !ownerId.equals(entity.getOwnerId())) {
throw new CustomException("无权删除该媒资");
}
entity.setStatus(MediaStatus.REJECTED.getCode());
entity.setOrderIndex(0);
this.updateById(entity);
}
private int resolveNextOrderIndex(String ownerType, String ownerId) {
LambdaQueryWrapper<PlayMediaEntity> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(PlayMediaEntity::getOwnerType, ownerType)
.eq(PlayMediaEntity::getOwnerId, ownerId)
.ne(PlayMediaEntity::getStatus, MediaStatus.REJECTED.getCode())
.orderByDesc(PlayMediaEntity::getOrderIndex)
.last("limit 1");
PlayMediaEntity last = this.getOne(wrapper, false);
if (last == null || last.getOrderIndex() == null) {
return 0;
}
return last.getOrderIndex() + 1;
}
}

View File

@@ -4,18 +4,15 @@ import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.starry.admin.common.exception.CustomException;
import com.starry.admin.modules.clerk.module.entity.PlayClerkUserInfoEntity;
import com.starry.admin.modules.clerk.service.IPlayClerkUserInfoService;
import com.starry.admin.modules.order.module.constant.OrderConstant.OperatorType;
import com.starry.admin.modules.order.module.constant.OrderConstant.OrderTriggerSource;
import com.starry.admin.modules.order.module.dto.OrderRefundContext;
import com.starry.admin.modules.order.module.dto.OrderRevocationContext;
import com.starry.admin.modules.custom.module.entity.PlayCustomUserInfoEntity;
import com.starry.admin.modules.custom.service.IPlayCustomUserInfoService;
import com.starry.admin.modules.order.module.entity.PlayOrderInfoEntity;
import com.starry.admin.modules.order.module.vo.*;
import com.starry.admin.modules.order.service.IOrderLifecycleService;
import com.starry.admin.modules.order.service.IPlayOrderInfoService;
import com.starry.admin.modules.order.service.IPlayOrderRefundInfoService;
import com.starry.admin.modules.shop.module.vo.PlayCommodityInfoVo;
import com.starry.admin.modules.shop.service.IPlayCommodityInfoService;
import com.starry.admin.modules.weichat.service.WxCustomMpService;
import com.starry.admin.modules.withdraw.service.IEarningsService;
import com.starry.admin.utils.SecurityUtils;
import com.starry.common.annotation.Log;
import com.starry.common.context.CustomSecurityContextHolder;
@@ -29,7 +26,6 @@ import io.swagger.annotations.ApiResponse;
import io.swagger.annotations.ApiResponses;
import java.math.BigDecimal;
import java.util.List;
import java.util.Optional;
import javax.annotation.Resource;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
@@ -57,13 +53,12 @@ public class PlayOrderInfoController {
private IPlayCommodityInfoService playCommodityInfoService;
@Resource
private IOrderLifecycleService orderLifecycleService;
private IPlayOrderRefundInfoService playOrderRefundInfoService;
@Resource
private IPlayCustomUserInfoService customUserInfoService;
@Resource
private IPlayClerkUserInfoService clerkUserInfoService;
@Resource
private IEarningsService earningsService;
/**
* 分页查询订单列表
*/
@@ -82,14 +77,7 @@ public class PlayOrderInfoController {
public R sendNotice(String orderId) {
PlayOrderInfoEntity orderInfo = orderInfoService.selectOrderInfoById(orderId);
List<PlayClerkUserInfoEntity> clerkList = clerkUserInfoService.list(Wrappers.lambdaQuery(PlayClerkUserInfoEntity.class).isNotNull(PlayClerkUserInfoEntity::getOpenid).eq(PlayClerkUserInfoEntity::getClerkState, "1").eq(PlayClerkUserInfoEntity::getSex, orderInfo.getSex()));
wxCustomMpService.sendCreateOrderMessageBatch(
clerkList,
orderInfo.getOrderNo(),
orderInfo.getOrderMoney().toString(),
orderInfo.getCommodityName(),
orderId,
orderInfo.getPlaceType(),
orderInfo.getRewardType());
wxCustomMpService.sendCreateOrderMessageBatch(clerkList, orderInfo.getOrderNo(), orderInfo.getOrderMoney().toString(), orderInfo.getCommodityName(), orderId);
return R.ok();
}
@@ -101,69 +89,41 @@ public class PlayOrderInfoController {
// @PreAuthorize("@customSs.hasPermission('order:order:update')")
@PostMapping("/orderRefund")
public R orderRefund(@ApiParam(value = "退款信息", required = true) @Validated @RequestBody PlayOrderRefundAddVo vo) {
OrderRefundContext context = new OrderRefundContext();
context.setOrderId(vo.getOrderId());
context.setRefundAmount(vo.getRefundAmount());
context.setRefundReason(vo.getRefundReason());
context.setOperatorType(OperatorType.ADMIN.getCode());
context.setOperatorId(SecurityUtils.getUserId());
context.withTriggerSource(OrderTriggerSource.ADMIN_API);
orderLifecycleService.refundOrder(context);
PlayOrderInfoEntity orderInfo = orderInfoService.selectOrderInfoById(vo.getOrderId());
if (orderInfo.getFinalAmount().compareTo(vo.getRefundAmount()) < 0) {
throw new CustomException("退款金额不能大于支付金额");
}
if ("3".equals(orderInfo.getOrderStatus())) {
throw new CustomException("【已完成】的订单无法操作退款");
}
if ("4".equals(orderInfo.getOrderStatus())) {
throw new CustomException("【已取消】的订单无法操作退款");
}
if ("1".equals(orderInfo.getRefundType())) {
throw new CustomException("每个订单只能退款一次~");
}
PlayOrderInfoEntity updateOrderInfo = new PlayOrderInfoEntity();
updateOrderInfo.setId(orderInfo.getId());
updateOrderInfo.setRefundType("1");
// 订单退款,订单状态变为已取消
updateOrderInfo.setOrderStatus("4");
updateOrderInfo.setRefundAmount(vo.getRefundAmount());
// 修改订单状态
orderInfoService.update(updateOrderInfo);
// 记录退款信息
String refundType = orderInfo.getFinalAmount().compareTo(vo.getRefundAmount()) == 0 ? "0" : "1";
PlayCustomUserInfoEntity customUserInfo = customUserInfoService.getById(orderInfo.getPurchaserBy());
customUserInfoService.updateAccountBalanceById(customUserInfo.getId(), customUserInfo.getAccountBalance(),
customUserInfo.getAccountBalance().add(orderInfo.getOrderMoney()), "3", "订单退款",
orderInfo.getOrderMoney(), BigDecimal.ZERO, vo.getOrderId());
playOrderRefundInfoService.add(orderInfo.getId(), orderInfo.getPurchaserBy(), orderInfo.getAcceptBy(),
orderInfo.getPayMethod(), refundType, vo.getRefundAmount(), vo.getRefundReason(), "2",
SecurityUtils.getUserId(), "0", "0");
return R.ok("退款成功");
}
@ApiOperation(value = "撤销已完成订单", notes = "管理员操作撤销,支持可选退款与收益处理")
@PostMapping("/revokeCompleted")
public R revokeCompleted(@Validated @RequestBody PlayOrderRevocationVo vo) {
OrderRevocationContext context = new OrderRevocationContext();
context.setOrderId(vo.getOrderId());
context.setRefundToCustomer(vo.isRefundToCustomer());
context.setRefundAmount(vo.getRefundAmount());
context.setRefundReason(vo.getRefundReason());
context.setDeductClerkEarnings(vo.isDeductClerkEarnings());
context.setEarningsAdjustAmount(vo.getDeductAmount());
context.setOperatorType(OperatorType.ADMIN.getCode());
context.setOperatorId(SecurityUtils.getUserId());
context.withTriggerSource(OrderTriggerSource.ADMIN_API);
orderLifecycleService.revokeCompletedOrder(context);
return R.ok("撤销成功");
}
@ApiOperation(value = "撤销限额", notes = "查询指定订单可退金额与可扣回收益")
@GetMapping("/{id}/revocationLimits")
public R getRevocationLimits(@PathVariable("id") String id) {
PlayOrderInfoEntity order = orderInfoService.selectOrderInfoById(id);
BigDecimal maxRefundAmount = Optional.ofNullable(order.getFinalAmount()).orElse(BigDecimal.ZERO);
BigDecimal maxDeductAmount = BigDecimal.ZERO;
if (order.getAcceptBy() != null) {
maxDeductAmount = Optional.ofNullable(earningsService.getRemainingEarningsForOrder(order.getId(), order.getAcceptBy()))
.orElse(BigDecimal.ZERO);
}
if (maxDeductAmount.compareTo(BigDecimal.ZERO) < 0) {
maxDeductAmount = BigDecimal.ZERO;
}
PlayOrderRevocationLimitsVo limitsVo = new PlayOrderRevocationLimitsVo();
limitsVo.setOrderId(order.getId());
limitsVo.setMaxRefundAmount(maxRefundAmount);
limitsVo.setMaxDeductAmount(maxDeductAmount);
limitsVo.setDefaultDeductAmount(maxDeductAmount);
limitsVo.setDeductible(order.getAcceptBy() != null);
return R.ok(limitsVo);
}
/**
* 管理后台强制取消进行中订单
*/
@ApiOperation(value = "强制取消订单", notes = "管理员强制取消已接单或服务中的订单")
@ApiResponses({@ApiResponse(code = 200, message = "操作成功"), @ApiResponse(code = 500, message = "操作失败")})
@PostMapping("/forceCancel")
public R forceCancel(@ApiParam(value = "取消参数", required = true) @Validated @RequestBody PlayOrderForceCancelVo vo) {
orderInfoService.forceCancelOngoingOrder("2", CustomSecurityContextHolder.getUserId(), vo.getOrderId(),
vo.getRefundAmount(), vo.getRefundReason(), vo.getImages());
return R.ok("操作成功");
}
/**
* 更换店员
*/
@@ -179,15 +139,8 @@ public class PlayOrderInfoController {
PlayOrderInfoEntity orderInfo = orderInfoService.selectOrderInfoById(vo.getOrderId());
PlayCommodityInfoVo commodityInfo = playCommodityInfoService.queryCommodityInfo(orderInfo.getCommodityId(),
clerkUserInfo.getLevelId());
wxCustomMpService.sendCreateOrderMessage(
clerkUserInfo.getTenantId(),
clerkUserInfo.getOpenid(),
orderInfo.getOrderNo(),
orderInfo.getOrderMoney().toString(),
commodityInfo.getCommodityName(),
vo.getOrderId(),
orderInfo.getPlaceType(),
orderInfo.getRewardType());
wxCustomMpService.sendCreateOrderMessage(clerkUserInfo.getTenantId(), clerkUserInfo.getOpenid(),
orderInfo.getOrderNo(), orderInfo.getOrderMoney().toString(), commodityInfo.getCommodityName(), vo.getOrderId());
return R.ok("操作成功");
}

View File

@@ -8,10 +8,9 @@ import com.starry.admin.common.exception.CustomException;
import com.starry.admin.modules.clerk.module.entity.PlayClerkUserInfoEntity;
import com.starry.admin.modules.clerk.service.IPlayClerkUserInfoService;
import com.starry.admin.modules.order.mapper.PlayOrderInfoMapper;
import com.starry.admin.modules.order.module.dto.OrderCompletionContext;
import com.starry.admin.modules.order.module.entity.PlayOrderInfoEntity;
import com.starry.admin.modules.order.service.IOrderLifecycleService;
import com.starry.admin.modules.weichat.service.WxCustomMpService;
import com.starry.common.redis.RedisCache;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.Date;
@@ -19,6 +18,7 @@ import java.util.List;
import javax.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
@@ -33,8 +33,6 @@ public class OrderJob {
@Resource
private IPlayClerkUserInfoService clerkUserInfoService;
@Resource
private IOrderLifecycleService orderLifecycleService;
@Resource
public RedisTemplate<String, Object> redisTemplate;
@@ -62,10 +60,11 @@ public class OrderJob {
// 判断与开始时间相比较,如果大于服务时长,则修改订单状态为已完成
if (ca.getOrderStartTime().plusMinutes(serviceDuration).isBefore(LocalDateTime.now())) {
orderLifecycleService.completeOrder(
ca.getId(),
OrderCompletionContext.scheduler("auto finish by duration")
.withForceNotify(true));
PlayOrderInfoEntity entity2 = new PlayOrderInfoEntity(ca.getId(), "3");
entity2.setOrderEndTime(LocalDateTime.now());
this.orderInfoMapper.updateById(entity2);
// 发送消息
wxCustomMpService.sendOrderFinishMessage(ca);
}
} catch (Exception e) {
@@ -96,14 +95,7 @@ public class OrderJob {
redisTemplate.opsForValue().set("order_notice_" + orderInfo.getId(), "1", 30, java.util.concurrent.TimeUnit.MINUTES);
List<PlayClerkUserInfoEntity> clerkList = clerkUserInfoService.list(Wrappers.lambdaQuery(PlayClerkUserInfoEntity.class).isNotNull(PlayClerkUserInfoEntity::getOpenid).eq(PlayClerkUserInfoEntity::getClerkState, "1")
.eq(PlayClerkUserInfoEntity::getSex, orderInfo.getSex()).eq(PlayClerkUserInfoEntity::getTenantId, orderInfo.getTenantId()));
wxCustomMpService.sendCreateOrderMessageBatch(
clerkList,
orderInfo.getOrderNo(),
orderInfo.getOrderMoney().toString(),
orderInfo.getCommodityName(),
orderInfo.getId(),
orderInfo.getPlaceType(),
orderInfo.getRewardType());
wxCustomMpService.sendCreateOrderMessageBatch(clerkList, orderInfo.getOrderNo(), orderInfo.getOrderMoney().toString(), orderInfo.getCommodityName(), orderInfo.getId());
} catch (Exception e) {
log.error(e.getMessage(), e);

View File

@@ -1,54 +0,0 @@
package com.starry.admin.modules.order.listener;
import com.starry.admin.common.exception.CustomException;
import com.starry.admin.modules.custom.module.entity.PlayCustomUserInfoEntity;
import com.starry.admin.modules.custom.service.IPlayCustomUserInfoService;
import com.starry.admin.modules.order.module.constant.OrderConstant.BalanceOperationType;
import com.starry.admin.modules.order.module.entity.PlayOrderInfoEntity;
import com.starry.admin.modules.order.module.event.OrderRevocationEvent;
import java.math.BigDecimal;
import java.util.Optional;
import org.springframework.stereotype.Component;
import org.springframework.transaction.event.TransactionPhase;
import org.springframework.transaction.event.TransactionalEventListener;
@Component
public class OrderRevocationBalanceListener {
private final IPlayCustomUserInfoService customUserInfoService;
public OrderRevocationBalanceListener(IPlayCustomUserInfoService customUserInfoService) {
this.customUserInfoService = customUserInfoService;
}
@TransactionalEventListener(phase = TransactionPhase.BEFORE_COMMIT)
public void handle(OrderRevocationEvent event) {
if (event == null || event.getContext() == null || event.getOrderSnapshot() == null) {
return;
}
if (!event.getContext().isRefundToCustomer()) {
return;
}
BigDecimal refundAmount = Optional.ofNullable(event.getContext().getRefundAmount()).orElse(BigDecimal.ZERO);
if (refundAmount.compareTo(BigDecimal.ZERO) <= 0) {
return;
}
PlayOrderInfoEntity order = event.getOrderSnapshot();
PlayCustomUserInfoEntity customer = customUserInfoService.getById(order.getPurchaserBy());
if (customer == null) {
throw new CustomException("顾客信息不存在");
}
BigDecimal currentBalance = Optional.ofNullable(customer.getAccountBalance()).orElse(BigDecimal.ZERO);
customUserInfoService.updateAccountBalanceById(
customer.getId(),
currentBalance,
currentBalance.add(refundAmount),
BalanceOperationType.REFUND.getCode(),
"已完成订单撤销退款",
refundAmount,
BigDecimal.ZERO,
order.getId());
}
}

View File

@@ -1,55 +0,0 @@
package com.starry.admin.modules.order.listener;
import com.starry.admin.common.exception.CustomException;
import com.starry.admin.modules.order.module.dto.OrderRevocationContext;
import com.starry.admin.modules.order.module.entity.PlayOrderInfoEntity;
import com.starry.admin.modules.order.module.event.OrderRevocationEvent;
import com.starry.admin.modules.withdraw.service.IEarningsService;
import java.math.BigDecimal;
import java.util.Optional;
import org.springframework.stereotype.Component;
import org.springframework.transaction.event.TransactionPhase;
import org.springframework.transaction.event.TransactionalEventListener;
@Component
public class OrderRevocationEarningsListener {
private final IEarningsService earningsService;
public OrderRevocationEarningsListener(IEarningsService earningsService) {
this.earningsService = earningsService;
}
@TransactionalEventListener(phase = TransactionPhase.BEFORE_COMMIT)
public void handle(OrderRevocationEvent event) {
if (event == null || event.getContext() == null || event.getOrderSnapshot() == null) {
return;
}
OrderRevocationContext context = event.getContext();
if (!context.isDeductClerkEarnings()) {
return;
}
createCounterLine(event);
}
private void createCounterLine(OrderRevocationEvent event) {
OrderRevocationContext context = event.getContext();
if (context == null) {
return;
}
PlayOrderInfoEntity order = event.getOrderSnapshot();
String targetClerkId = order.getAcceptBy();
if (targetClerkId == null || targetClerkId.trim().isEmpty()) {
throw new CustomException("需要指定收益冲销目标账号");
}
BigDecimal amount = context.getEarningsAdjustAmount();
if (amount == null || amount.compareTo(BigDecimal.ZERO) <= 0) {
amount = Optional.ofNullable(order.getEstimatedRevenue()).orElse(BigDecimal.ZERO);
if (amount.compareTo(BigDecimal.ZERO) <= 0) {
return;
}
}
earningsService.createCounterLine(order.getId(), order.getTenantId(), targetClerkId, amount, context.getOperatorId());
}
}

View File

@@ -1,9 +0,0 @@
package com.starry.admin.modules.order.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.starry.admin.modules.order.module.entity.PlayOrderLogInfoEntity;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface PlayOrderLogInfoMapper extends BaseMapper<PlayOrderLogInfoEntity> {
}

View File

@@ -19,8 +19,7 @@ public class OrderConstant {
ACCEPTED("1", "已接单(待开始)"),
IN_PROGRESS("2", "已开始(服务中)"),
COMPLETED("3", "已完成"),
CANCELLED("4", "已取消"),
REVOKED("5", "已撤销");
CANCELLED("4", "已取消");
private final String code;
private final String description;
@@ -48,9 +47,7 @@ public class OrderConstant {
REFUND("-1", "退款订单"),
RECHARGE("0", "充值订单"),
WITHDRAWAL("1", "提现订单"),
NORMAL("2", "普通订单"),
GIFT("3", "礼物订单"),
BLIND_BOX_PURCHASE("4", "盲盒购买订单");
NORMAL("2", "普通订单");
private final String code;
private final String description;
@@ -70,31 +67,6 @@ public class OrderConstant {
}
}
@Getter
public enum PaymentSource {
BALANCE("BALANCE", "余额扣款"),
WX_PAY("WX_PAY", "微信支付"),
ALI_PAY("ALI_PAY", "支付宝支付"),
BLIND_BOX("BLIND_BOX", "盲盒奖励抵扣");
private final String code;
private final String description;
PaymentSource(String code, String description) {
this.code = code;
this.description = description;
}
public static PaymentSource fromCode(String code) {
for (PaymentSource source : values()) {
if (source.code.equals(code)) {
return source;
}
}
throw new IllegalArgumentException("Unknown payment source code: " + code);
}
}
/**
* 下单类型枚举
*/
@@ -208,301 +180,15 @@ public class OrderConstant {
}
}
/**
* 操作人类型枚举
*/
@Getter
public enum OperatorType {
CUSTOMER("0", "顾客"),
CLERK("1", "店员"),
ADMIN("2", "管理员"),
GROUP_LEADER("3", "组长");
private final String code;
private final String description;
OperatorType(String code, String description) {
this.code = code;
this.description = description;
}
public static OperatorType fromCode(String code) {
for (OperatorType type : values()) {
if (type.code.equals(code)) {
return type;
}
}
throw new IllegalArgumentException("Unknown operator type code: " + code);
}
}
@Getter
public enum OrderActor {
CUSTOMER,
CLERK,
GROUP_LEADER,
ADMIN,
SYSTEM;
}
// 排除历史记录常量
public static final String EXCLUDE_HISTORY_NO = "0";
public static final String EXCLUDE_HISTORY_YES = "1";
@Getter
public enum OrderRefundFlag {
NOT_REFUNDED("0"),
REFUNDED("1");
private final String code;
OrderRefundFlag(String code) {
this.code = code;
}
public static OrderRefundFlag fromCode(String code) {
for (OrderRefundFlag flag : values()) {
if (flag.code.equals(code)) {
return flag;
}
}
throw new IllegalArgumentException("Unknown order refund flag code: " + code);
}
}
@Getter
public enum OrderRefundRecordType {
FULL("0"),
PARTIAL("1");
private final String code;
OrderRefundRecordType(String code) {
this.code = code;
}
public static OrderRefundRecordType fromCode(String code) {
for (OrderRefundRecordType type : values()) {
if (type.code.equals(code)) {
return type;
}
}
throw new IllegalArgumentException("Unknown refund record type code: " + code);
}
}
@Getter
public enum OrderRefundState {
PROCESSING("0"),
SUCCESS("1"),
CLOSED("2"),
ABNORMAL("-1");
private final String code;
OrderRefundState(String code) {
this.code = code;
}
public static OrderRefundState fromCode(String code) {
for (OrderRefundState state : values()) {
if (state.code.equals(code)) {
return state;
}
}
throw new IllegalArgumentException("Unknown refund state code: " + code);
}
}
@Getter
public enum ReviewRequirement {
NOT_REQUIRED("0"),
REQUIRED("1");
private final String code;
ReviewRequirement(String code) {
this.code = code;
}
public static ReviewRequirement fromCode(String code) {
for (ReviewRequirement requirement : values()) {
if (requirement.code.equals(code)) {
return requirement;
}
}
throw new IllegalArgumentException("Unknown review requirement code: " + code);
}
}
@Getter
public enum BalanceOperationType {
RECHARGE("0"),
CONSUME("1"),
SERVICE("2"),
REFUND("3");
private final String code;
BalanceOperationType(String code) {
this.code = code;
}
public static BalanceOperationType fromCode(String code) {
for (BalanceOperationType type : values()) {
if (type.code.equals(code)) {
return type;
}
}
throw new IllegalArgumentException("Unknown balance operation type code: " + code);
}
}
@Getter
public enum YesNoFlag {
NO("0"),
YES("1");
private final String code;
YesNoFlag(String code) {
this.code = code;
}
public static YesNoFlag fromCode(String code) {
for (YesNoFlag flag : values()) {
if (flag.code.equals(code)) {
return flag;
}
}
throw new IllegalArgumentException("Unknown yes/no flag code: " + code);
}
}
@Getter
public enum OrderSettlementState {
NOT_SETTLED("0"),
SETTLED("1");
private final String code;
OrderSettlementState(String code) {
this.code = code;
}
public static OrderSettlementState fromCode(String code) {
for (OrderSettlementState state : values()) {
if (state.code.equals(code)) {
return state;
}
}
throw new IllegalArgumentException("Unknown settlement state code: " + code);
}
}
@Getter
public enum OrdersExpiredState {
NOT_EXPIRED("0"),
EXPIRED("1");
private final String code;
OrdersExpiredState(String code) {
this.code = code;
}
public static OrdersExpiredState fromCode(String code) {
for (OrdersExpiredState state : values()) {
if (state.code.equals(code)) {
return state;
}
}
throw new IllegalArgumentException("Unknown orders expired state code: " + code);
}
}
@Getter
public enum PayMethod {
BALANCE("0"),
WECHAT("1"),
ALIPAY("2"),
BANK_CARD("3"),
OTHER("4");
private final String code;
PayMethod(String code) {
this.code = code;
}
public static PayMethod fromCode(String code) {
for (PayMethod method : values()) {
if (method.code.equals(code)) {
return method;
}
}
throw new IllegalArgumentException("Unknown pay method code: " + code);
}
}
@Getter
public enum OrderTriggerSource {
/**
* 未标记来源的兜底,通常用于兼容历史数据
*/
UNKNOWN("unknown"),
/**
* 运营或客服后台人工处理触发
*/
MANUAL("manual"),
/**
* 微信顾客端(小程序/公众号)下单触发
*/
WX_CUSTOMER("wx_customer"),
/**
* 微信店员端操作触发
*/
WX_CLERK("wx_clerk"),
/**
* 微信店员端管理能力触发(组长/运营)
*/
WX_CLERK_MGMT("wx_clerk_mgmt"),
/**
* 管理后台控制台界面发起
*/
ADMIN_CONSOLE("admin_console"),
/**
* 管理后台开放接口调用
*/
ADMIN_API("admin_api"),
/**
* 打赏单自动生成的订单流程
*/
REWARD_ORDER("reward_order"),
/**
* 定时任务/调度器触发
*/
SCHEDULER("scheduler"),
/**
* 平台内部系统逻辑触发
*/
SYSTEM("system");
private final String code;
OrderTriggerSource(String code) {
this.code = code;
}
public static OrderTriggerSource fromCode(String code) {
for (OrderTriggerSource source : values()) {
if (source.code.equals(code)) {
return source;
}
}
throw new IllegalArgumentException("Unknown order trigger source code: " + code);
}
}
// Legacy constants for backward compatibility - consider deprecating
@Deprecated
public final static String ORDER_STATUS_0 = "0";
@Deprecated
public final static String ORDER_STATUS_1 = "1";
@Deprecated
public final static String ORDER_STATUS_2 = "2";
@Deprecated
public final static String ORDER_STATUS_3 = "3";
@Deprecated
public final static String ORDER_STATUS_4 = "4";
}

Some files were not shown because too many files have changed in this diff Show More