Compare commits
2 Commits
master
...
385ceeecb6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
385ceeecb6 | ||
|
|
584780a812 |
67
README.md
67
README.md
@@ -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 配置已设置
|
||||
2861
apitest.out
2861
apitest.out
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||
@@ -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"
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
@@ -1,2 +0,0 @@
|
||||
GRANT SELECT ON performance_schema.* TO 'apitest'@'%';
|
||||
FLUSH PRIVILEGES;
|
||||
@@ -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`)后数据会一起删除,保证每次测试环境一致。
|
||||
@@ -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"
|
||||
@@ -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:
|
||||
|
||||
92
flyway.sh
92
flyway.sh
@@ -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"
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -1,177 +0,0 @@
|
||||
# Earnings Adjustments & Withdrawal Reject — Expected Behavior
|
||||
|
||||
This document defines the intended behavior for:
|
||||
|
||||
- Admin-created **earnings adjustments** (positive or negative earning lines)
|
||||
- Admin **withdrawal reject** (cancel a withdrawal request and release reserved earning lines)
|
||||
- **Authorization** rules (permission + group leader scope + cross-tenant isolation)
|
||||
|
||||
## Concepts
|
||||
|
||||
### Earnings Line
|
||||
An `earnings line` is an immutable money movement entry for a clerk. Amount can be positive or negative.
|
||||
|
||||
### Adjustment
|
||||
An `adjustment` is an admin-originated earnings line, designed to support future extensibility (many “reasons”, auditability, idempotency, async apply).
|
||||
|
||||
Key semantics:
|
||||
|
||||
- It **creates exactly one** earnings line when applied.
|
||||
- The created earnings line uses:
|
||||
- `earningType = ADJUSTMENT`
|
||||
- `sourceType = ADJUSTMENT`
|
||||
- `sourceId = adjustmentId`
|
||||
- `orderId = null`
|
||||
- `amount` can be positive or negative
|
||||
- `unlockTime = effectiveTime` (adjustments are effective at their “unlock” time)
|
||||
|
||||
### Withdrawal Reject
|
||||
Admin reject is a cancel operation that:
|
||||
|
||||
- marks the withdrawal request as canceled/rejected
|
||||
- releases reserved `withdrawing` earnings lines back to `available` / `frozen`
|
||||
|
||||
## Authorization Model (New Endpoints)
|
||||
|
||||
Authorization is **two-layer**:
|
||||
|
||||
1) **Action-level permission**: does the user have permission to call the endpoint?
|
||||
2) **Object-level scope**: can the user act on the target clerk / request?
|
||||
|
||||
### Permission Strings
|
||||
|
||||
- Create adjustment: `withdraw:adjustment:create`
|
||||
- Read/poll adjustment status: `withdraw:adjustment:read`
|
||||
- Reject withdrawal request: `withdraw:request:reject`
|
||||
|
||||
### Group Leader Scope
|
||||
|
||||
If the current user is **not** `superAdmin`, they can only act on clerks that belong to a group where:
|
||||
|
||||
- `clerk.groupId = group.id`
|
||||
- `group.sysUserId = currentUserId`
|
||||
|
||||
If this scope check fails, the endpoint returns **HTTP 403**.
|
||||
|
||||
### Super Admin Bypass
|
||||
|
||||
If `superAdmin == true`, the user bypasses permission checks and scope checks for these new endpoints.
|
||||
|
||||
### Cross-Tenant Isolation
|
||||
|
||||
All operations are tenant-scoped.
|
||||
|
||||
- If `X-Tenant` does not match the target entity’s `tenantId`, the API returns **HTTP 404** (do not leak existence across tenants).
|
||||
|
||||
## Admin Earnings Adjustments API
|
||||
|
||||
### Create Adjustment
|
||||
|
||||
`POST /admin/earnings/adjustments`
|
||||
|
||||
Headers:
|
||||
|
||||
- `Idempotency-Key: <uuid>` (required)
|
||||
- `X-Tenant: <tenantId>` (required)
|
||||
|
||||
Body:
|
||||
|
||||
```json
|
||||
{
|
||||
"clerkId": "clerk-id",
|
||||
"amount": "20.00",
|
||||
"reasonType": "MANUAL",
|
||||
"reasonDescription": "text",
|
||||
"effectiveTime": "2026-01-01T12:00:00" // optional
|
||||
}
|
||||
```
|
||||
|
||||
Validation rules:
|
||||
|
||||
- `Idempotency-Key` required
|
||||
- `tenantId` required
|
||||
- `clerkId` required
|
||||
- `amount` must be non-zero (positive = reward-like, negative = punishment-like)
|
||||
- `reasonType` required (currently hard-coded enum values, extend later)
|
||||
- `reasonDescription` required, non-blank
|
||||
|
||||
Idempotency behavior:
|
||||
|
||||
- Same `tenantId + Idempotency-Key` with the **same request body** returns the **same** `adjustmentId`.
|
||||
- Same `tenantId + Idempotency-Key` with a **different request body** returns **HTTP 409**.
|
||||
|
||||
Response behavior:
|
||||
|
||||
- Always returns **HTTP 202 Accepted** on success (request is “in-progress”).
|
||||
- Includes `Location: /admin/earnings/adjustments/idempotency/{Idempotency-Key}` for polling.
|
||||
|
||||
Response example:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 202,
|
||||
"message": "请求处理中",
|
||||
"data": {
|
||||
"adjustmentId": "adj-uuid",
|
||||
"idempotencyKey": "same-key",
|
||||
"status": "PROCESSING"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Poll Adjustment Status
|
||||
|
||||
`GET /admin/earnings/adjustments/idempotency/{key}`
|
||||
|
||||
Behavior:
|
||||
|
||||
- If not found in this tenant: **HTTP 404**
|
||||
- If found:
|
||||
- returns **HTTP 200**
|
||||
- `status` is one of:
|
||||
- `PROCESSING`: accepted but not yet applied
|
||||
- `APPLIED`: earnings line has been created
|
||||
- `FAILED`: apply failed (and should be visible for operator debugging)
|
||||
|
||||
Stress / eventual consistency note:
|
||||
|
||||
- Under load (DB latency / executor backlog), polling may stay in `PROCESSING` longer, but must not create duplicate earnings lines.
|
||||
|
||||
## Withdrawal Reject API
|
||||
|
||||
### Reject Withdrawal Request
|
||||
|
||||
`POST /admin/withdraw/requests/{id}/reject`
|
||||
|
||||
Body:
|
||||
|
||||
```json
|
||||
{ "reason": "text (optional)" }
|
||||
```
|
||||
|
||||
Behavior:
|
||||
|
||||
- If request does not exist in this tenant: **HTTP 404**
|
||||
- If request is already canceled/rejected: return **HTTP 200** (idempotent)
|
||||
- If request is `success`: return **HTTP 400** (cannot reject a successful payout)
|
||||
- Otherwise:
|
||||
- request status transitions to `canceled` (or `rejected` depending on legacy naming)
|
||||
- all earnings lines with:
|
||||
- `withdrawalId = requestId`
|
||||
- `status = withdrawing`
|
||||
are released:
|
||||
- `withdrawalId` set to `null`
|
||||
- if `unlockTime > now` -> `status = frozen`
|
||||
- else -> `status = available`
|
||||
|
||||
## Stats: includeAdjustments toggle
|
||||
|
||||
The statistics endpoint supports a toggle `includeAdjustments`:
|
||||
|
||||
- when `includeAdjustments = false` (default): only order-derived earnings contribute
|
||||
- when `includeAdjustments = true`: adjustment earnings lines (`sourceType=ADJUSTMENT`) are included in the revenue sum
|
||||
|
||||
Time-window behavior:
|
||||
|
||||
- adjustment inclusion is based on `unlockTime` window (equivalent to `effectiveTime`)
|
||||
|
||||
@@ -1,359 +0,0 @@
|
||||
# WeChat Subsystem — Characterization / Integration Test Matrix
|
||||
|
||||
This document is a **behavior pin** (characterization) test matrix for the current WeChat-related subsystem.
|
||||
The goal is to lock observable behavior (HTTP + DB + Redis + external calls) so later refactoring can be done safely.
|
||||
|
||||
Repo root: `/Volumes/main/code/yunpei/peipei-backend`
|
||||
|
||||
## Source Inventory (entry points)
|
||||
|
||||
### Controllers
|
||||
|
||||
- `/Volumes/main/code/yunpei/peipei-backend/play-admin/src/main/java/com/starry/admin/modules/weichat/controller/WxOauthController.java`
|
||||
- `/Volumes/main/code/yunpei/peipei-backend/play-admin/src/main/java/com/starry/admin/modules/weichat/controller/WxPlayController.java`
|
||||
- `/Volumes/main/code/yunpei/peipei-backend/play-admin/src/main/java/com/starry/admin/modules/weichat/controller/WxCommonController.java`
|
||||
- `/Volumes/main/code/yunpei/peipei-backend/play-admin/src/main/java/com/starry/admin/modules/weichat/controller/WxCustomController.java`
|
||||
- `/Volumes/main/code/yunpei/peipei-backend/play-admin/src/main/java/com/starry/admin/modules/weichat/controller/WxClerkController.java`
|
||||
- `/Volumes/main/code/yunpei/peipei-backend/play-admin/src/main/java/com/starry/admin/modules/weichat/controller/WxOrderInfoController.java`
|
||||
- `/Volumes/main/code/yunpei/peipei-backend/play-admin/src/main/java/com/starry/admin/modules/weichat/controller/WxCouponController.java`
|
||||
- `/Volumes/main/code/yunpei/peipei-backend/play-admin/src/main/java/com/starry/admin/modules/weichat/controller/WxClerkMediaController.java`
|
||||
- `/Volumes/main/code/yunpei/peipei-backend/play-admin/src/main/java/com/starry/admin/modules/weichat/controller/WxBlindBoxController.java`
|
||||
- `/Volumes/main/code/yunpei/peipei-backend/play-admin/src/main/java/com/starry/admin/modules/weichat/controller/WxPkController.java`
|
||||
- `/Volumes/main/code/yunpei/peipei-backend/play-admin/src/main/java/com/starry/admin/modules/weichat/controller/WxGiftController.java`
|
||||
- `/Volumes/main/code/yunpei/peipei-backend/play-admin/src/main/java/com/starry/admin/modules/weichat/controller/WxClerkCommodityController.java`
|
||||
- `/Volumes/main/code/yunpei/peipei-backend/play-admin/src/main/java/com/starry/admin/modules/weichat/controller/WxCommodityController.java`
|
||||
- `/Volumes/main/code/yunpei/peipei-backend/play-admin/src/main/java/com/starry/admin/modules/weichat/controller/WxLevelController.java`
|
||||
- `/Volumes/main/code/yunpei/peipei-backend/play-admin/src/main/java/com/starry/admin/modules/weichat/controller/WxShopController.java`
|
||||
- `/Volumes/main/code/yunpei/peipei-backend/play-admin/src/main/java/com/starry/admin/modules/weichat/controller/WxClerkWagesController.java`
|
||||
- `/Volumes/main/code/yunpei/peipei-backend/play-admin/src/main/java/com/starry/admin/modules/weichat/controller/WxPersonnelGroupInfoController.java`
|
||||
- `/Volumes/main/code/yunpei/peipei-backend/play-admin/src/main/java/com/starry/admin/modules/weichat/controller/WxPlayOrderRankingController.java`
|
||||
- `/Volumes/main/code/yunpei/peipei-backend/play-admin/src/main/java/com/starry/admin/modules/withdraw/controller/WxWithdrawController.java`
|
||||
- `/Volumes/main/code/yunpei/peipei-backend/play-admin/src/main/java/com/starry/admin/modules/withdraw/controller/WxWithdrawPayeeController.java`
|
||||
|
||||
### Services / Cross-cutting
|
||||
|
||||
- `/Volumes/main/code/yunpei/peipei-backend/play-admin/src/main/java/com/starry/admin/modules/weichat/service/WxOauthService.java`
|
||||
- `/Volumes/main/code/yunpei/peipei-backend/play-admin/src/main/java/com/starry/admin/modules/weichat/service/WxCustomMpService.java`
|
||||
- `/Volumes/main/code/yunpei/peipei-backend/play-admin/src/main/java/com/starry/admin/modules/weichat/service/WxTokenService.java`
|
||||
- `/Volumes/main/code/yunpei/peipei-backend/play-admin/src/main/java/com/starry/admin/modules/weichat/service/WxCustomUserService.java`
|
||||
- `/Volumes/main/code/yunpei/peipei-backend/play-admin/src/main/java/com/starry/admin/modules/weichat/service/WxGiftOrderService.java`
|
||||
- `/Volumes/main/code/yunpei/peipei-backend/play-admin/src/main/java/com/starry/admin/modules/weichat/service/WxBlindBoxOrderService.java`
|
||||
- `/Volumes/main/code/yunpei/peipei-backend/play-admin/src/main/java/com/starry/admin/modules/weichat/service/MediaUploadService.java`
|
||||
- `/Volumes/main/code/yunpei/peipei-backend/play-admin/src/main/java/com/starry/admin/common/security/filter/JwtAuthenticationTokenFilter.java`
|
||||
- `/Volumes/main/code/yunpei/peipei-backend/play-admin/src/main/java/com/starry/admin/common/aspect/CustomUserLoginAspect.java`
|
||||
- `/Volumes/main/code/yunpei/peipei-backend/play-admin/src/main/java/com/starry/admin/common/aspect/ClerkUserLoginAspect.java`
|
||||
- `/Volumes/main/code/yunpei/peipei-backend/play-admin/src/main/java/com/starry/admin/modules/weichat/service/NotificationSender.java`
|
||||
- `/Volumes/main/code/yunpei/peipei-backend/play-admin/src/main/java/com/starry/admin/modules/weichat/service/MockNotificationSender.java`
|
||||
|
||||
### DB schema touchpoints
|
||||
|
||||
- `/Volumes/main/code/yunpei/peipei-backend/play-admin/src/main/resources/db/migration/V1__init_schema.sql`
|
||||
- `/Volumes/main/code/yunpei/peipei-backend/play-admin/src/main/resources/db/migration/V17__create_media.sql`
|
||||
|
||||
Key fields to pin:
|
||||
|
||||
- `sys_tenant`: `tenant_key`, `app_id`, `secret`, `mch_id`, `mch_key`, `*_template_id`, `profitsharing_rate`
|
||||
- `play_custom_user_info`: `openid`, `unionid`, `wei_chat_code`, `token`
|
||||
- `play_clerk_user_info`: `openid` (NOT NULL), `wei_chat_code`, `token`, `online_state`, `onboarding_state`, `listing_state`, `clerk_state`
|
||||
- `play_order_info`: `order_type`, `pay_method`, `pay_state`, `place_type`, `reward_type`, `wei_chat_code`, `profit_sharing_amount`
|
||||
- `play_media` / `play_clerk_media_asset`: `status`, `usage`, `review_state`, `deleted`, `order_index` + unique constraint `uk_clerk_usage_media`
|
||||
|
||||
## Test Harness Guidelines
|
||||
|
||||
### Required runtime surfaces to observe
|
||||
|
||||
1) **HTTP**: status code + response schema (normalized) for every `/wx/**` route.
|
||||
2) **DB**: before/after snapshots of rows touched by the route (focus tables above).
|
||||
3) **Redis**: keys under:
|
||||
- `TENANT_INFO:*`
|
||||
- `login_codes:*`
|
||||
- PK keys under `PkRedisKeyConstants` (if enabled)
|
||||
4) **External calls**: record interactions with:
|
||||
- `WxMpService` / WeChat MP template message service
|
||||
- `WxPayService` (unified order + profitsharing)
|
||||
- `IOssFileService`
|
||||
- `SmsUtils`
|
||||
- background tasks like `OverdueOrderHandlerTask`
|
||||
|
||||
### Profiles
|
||||
|
||||
- Prefer running integration tests under `apitest` profile (seeded DB + mock notifier).
|
||||
- For tests that must hit real async behavior, use deterministic executors in test config (or replace `ThreadPoolTaskExecutor` with inline executor via `@MockBean`).
|
||||
|
||||
### Normalization rules (avoid flaky tests)
|
||||
|
||||
- Normalize dynamic fields: timestamps, random IDs (UUID), and generated nonces.
|
||||
- For money: always compare as `BigDecimal` with explicit scale/rounding.
|
||||
- For lists: assert stable ordering only when the endpoint guarantees ordering.
|
||||
|
||||
---
|
||||
|
||||
## 1) Gateway / Tenant / AuthN (Filter + AOP)
|
||||
|
||||
| ID | Surface | Route / Component | Scenario | Setup | Mocks | Assertions |
|
||||
|---:|---|---|---|---|---|---|
|
||||
| GW-001 | HTTP | `JwtAuthenticationTokenFilter` | `/wx/**` without `tenantkey` header on a login-required route | none | none | response is error (pin: code/body shape + status) |
|
||||
| GW-002 | HTTP | `JwtAuthenticationTokenFilter` | `/wx/**` without `tenantkey` on a no-login whitelist route | none | none | response is error (pin: JSON body written by filter) |
|
||||
| GW-003 | HTTP | `JwtAuthenticationTokenFilter` | `/wx/pay/jsCallback` bypasses auth and does not require `tenantkey` | none | none | handler executes and returns success string |
|
||||
| GW-004 | HTTP/Redis | filter `getTenantId` | with `tenantkey` only (no token) | tenant exists | `ISysTenantService` real or stub | `SecurityUtils.getTenantId()` set; request proceeds |
|
||||
| GW-005 | HTTP/Redis | filter `getTenantId` | with clerk token | seed clerk with token + tenantId | none | tenantId resolved from DB; `TENANT_INFO:{userId}` read does not break flow |
|
||||
| GW-006 | HTTP/Redis | filter `getTenantId` | with custom token | seed customer with token + tenantId | none | tenantId resolved from DB |
|
||||
| AOP-001 | HTTP | `@CustomUserLogin` endpoints | missing `customusertoken` header | none | none | 401 `token为空` |
|
||||
| AOP-002 | HTTP | `@ClerkUserLogin` endpoints | missing `clerkusertoken` header | none | none | 401 `token为空` |
|
||||
| AOP-003 | HTTP | `CustomUserLoginAspect` | invalid token format/signature | none | none | 401 `获取用户信息异常` |
|
||||
| AOP-004 | HTTP | `CustomUserLoginAspect` | token ok but DB token mismatch | seed user with different token | none | 401 `token异常` |
|
||||
| AOP-005 | HTTP | `ClerkUserLoginAspect` | `ensureClerkSessionIsValid` rejects | seed clerk state invalid | none | 401 message matches current `CustomException` message |
|
||||
| TOK-001 | unit/contract | `WxTokenService` | token generated and parsed (with/without `Bearer `) | config secret + expireTime | none | same userId extracted |
|
||||
| TOK-002 | unit/contract | `WxTokenService` | expired token fails | short expireTime | none | parsing throws; upstream maps to 401 |
|
||||
|
||||
---
|
||||
|
||||
## 2) WeChat OAuth (WxOauthController + WxOauthService)
|
||||
|
||||
| ID | Surface | Route | Scenario | Setup | Mocks | Assertions |
|
||||
|---:|---|---|---|---|---|---|
|
||||
| OAUTH-001 | HTTP | `POST /wx/oauth2/getConfigAddress` | `url` omitted -> uses default callback | valid tenant | mock `WxMpService` signature | signature returned; default URL pinned |
|
||||
| OAUTH-002 | HTTP | `POST /wx/oauth2/getConfigAddress` | `url` provided -> overrides default | valid tenant | mock signature | uses provided URL |
|
||||
| OAUTH-003 | HTTP | `POST /wx/oauth2/getClerkLoginAddress` | returns auth URL with scope `snsapi_userinfo` | valid tenant | mock OAuth2 service build URL | response contains expected scope |
|
||||
| OAUTH-004 | HTTP | `POST /wx/oauth2/getCustomLoginAddress` | same as clerk | valid tenant | mock OAuth2 service | response contains expected scope |
|
||||
| OAUTH-005 | HTTP/DB/Redis | `POST /wx/oauth2/custom/login` | success -> returns token payload + persists token | seed tenant + seeded customer openid | mock `WxMpService.getOAuth2Service().getAccessToken/getUserInfo` | response includes `tokenValue/tokenName/loginDate`; DB token updated; Redis `TENANT_INFO:{id}` set |
|
||||
| OAUTH-006 | HTTP | `POST /wx/oauth2/custom/login` | upstream WeChat oauth fails -> unauthorized | none | mock `WxMpService.getOAuth2Service().getAccessToken` to throw | response `code=401` (and `success=false`) |
|
||||
| OAUTH-007 | HTTP/DB/Redis | `POST /wx/oauth2/clerk/login` | success -> token persisted + tenant cached; `pcData` present | seed a clerk with `sysUserId=""` to avoid PC login dependency | mock `WxMpService.getOAuth2Service().getAccessToken/getUserInfo` | response includes `pcData.token/role` (empty strings); DB token updated; Redis `TENANT_INFO:{id}` set |
|
||||
| OAUTH-008 | HTTP/DB | `GET /wx/oauth2/custom/logout` | token invalidated | seed user + token | none | DB token set to `empty`; subsequent access 401 |
|
||||
| OAUTH-009 | service/DB | `WxOauthService.customUserLogin` | first login creates new user with registrationTime | empty DB | mock MP OAuth2 | row inserted with expected fields |
|
||||
| OAUTH-010 | service/DB | `WxOauthService.clerkUserLogin` | deleted clerk restored | seed clerk deleted=true | mock MP OAuth2 | deleted=false; default states filled; token/online reset |
|
||||
| OAUTH-011 | HTTP | `GET /wx/oauth2/checkSubscribe` | returns boolean subscribe state | logged-in customer | mock `WxMpService.getUserService().userInfo` | response `data` is `true/false` |
|
||||
|
||||
---
|
||||
|
||||
## 3) WeChat Pay Recharge + Callback + Profit Sharing (WxPlayController)
|
||||
|
||||
| ID | Surface | Route | Scenario | Setup | Mocks | Assertions |
|
||||
|---:|---|---|---|---|---|---|
|
||||
| PAY-001 | HTTP | `GET /wx/pay/custom/getCustomPaymentAmount` | money empty -> 500 | logged-in customer | none | error message pinned |
|
||||
| PAY-002 | HTTP | `GET /wx/pay/custom/getCustomPaymentAmount` | money valid -> BigDecimal returned | logged-in customer | none | response number pinned (scale/rounding) |
|
||||
| PAY-003 | HTTP/DB | `GET /wx/pay/custom/createOrder` | money < 1 -> 500 | logged-in customer | none | error message pinned |
|
||||
| PAY-004 | HTTP/DB | `GET /wx/pay/custom/createOrder` | creates recharge order + returns JSAPI pay params | logged-in customer + tenant with mch config | mock `WxPayService.unifiedOrder` and `SignUtils` inputs via capturing request | order row created (type/pay_state); response has required fields; unifiedOrder request fields pinned |
|
||||
| PAY-005 | HTTP | `GET /wx/pay/custom/createOrder` | subscribe check fails -> error | customer openid + tenant | mock `WxCustomMpService.checkSubscribeThrowsExp` to throw | HTTP error pinned |
|
||||
| PAY-006 | HTTP/DB | `POST/GET /wx/pay/jsCallback` | invalid XML -> still returns success string, no DB changes | existing DB | none | response equals success; no order updates |
|
||||
| PAY-007 | HTTP/DB | `/wx/pay/jsCallback` | unknown out_trade_no -> no changes | none | none | no DB changes |
|
||||
| PAY-008 | HTTP/DB | `/wx/pay/jsCallback` | order_type!=0 OR pay_state!=0 -> no reprocessing | seed paid/non-recharge order | none | no balance change; no state change |
|
||||
| PAY-009 | HTTP/DB/Redis | `/wx/pay/jsCallback` | happy path updates pay_state and balance | seed recharge order pay_state=0 + tenant attach | mock `customAccountBalanceRecharge` if needed or assert real side-effects | pay_state becomes `1`; balance updated; template message called |
|
||||
| PAY-010 | HTTP/DB | `/wx/pay/jsCallback` | replay same callback twice is idempotent | seed recharge order | none | balance does not double-add; profit_sharing_amount not duplicated |
|
||||
| PAY-011 | HTTP/DB | profitSharing | profitsharing_rate<=0 -> no call and no write | tenant rate=0 | mock WxPay profitSharing service | no API call; no DB update |
|
||||
| PAY-012 | HTTP/DB | profitSharing | rate>0 and computed amount=0 -> no call | tiny amount | mock | no call |
|
||||
| PAY-013 | HTTP/DB | profitSharing | rate>0 -> writes profit_sharing_amount | tenant rate set | mock profitSharing result | DB field set to expected BigDecimal |
|
||||
|
||||
---
|
||||
|
||||
## 4) WeChat MP Notifications (WxCustomMpService)
|
||||
|
||||
| ID | Surface | Component | Scenario | Setup | Mocks | Assertions |
|
||||
|---:|---|---|---|---|---|---|
|
||||
| MP-001 | unit/contract | `proxyWxMpService` | missing tenantId -> CustomException | tenantId unset | none | exception message pinned |
|
||||
| MP-002 | unit/contract | `getWxPay` | tenant missing mch_id -> CustomException | tenant has empty mchId | none | message pinned |
|
||||
| MP-003 | integration | `sendCreateOrderMessage` | template data fields mapping | tenant has templateId + tenantKey | mock template msg service | `short_thing5` label resolved; URL pinned |
|
||||
| MP-004 | integration | `sendCreateOrderMessageBatch` | filters offboarded/delisted clerks | clerk list mix | deterministic executor + mock template | only eligible clerks called |
|
||||
| MP-005 | integration | `sendBalanceMessage` | sends recharge success template | order + tenant + customer | mock template | data keys pinned |
|
||||
| MP-006 | integration | `sendOrderFinishMessage` | only placeType "1"/"2" triggers | orders with other placeType | mock template | no calls when not matched |
|
||||
| MP-007 | integration | async wrappers | exceptions inside async do not bubble | throw in underlying send | deterministic executor | caller does not fail |
|
||||
| MP-008 | integration | subscribe checks | subscribe=false -> ServiceException message pinned | mock `WxMpUser.subscribe=false` | mock user service | message pinned |
|
||||
|
||||
---
|
||||
|
||||
## 5) Common WeChat Tools (WxCommonController + WxFileUtils)
|
||||
|
||||
| ID | Surface | Route | Scenario | Setup | Mocks | Assertions |
|
||||
|---:|---|---|---|---|---|---|
|
||||
| COM-001 | HTTP | `GET /wx/common/area/tree` | returns area tree | tenantKey only | mock area service or real | response schema pinned |
|
||||
| COM-002 | HTTP | `GET /wx/common/setting/info` | returns global UI config (NOT tenant-scoped) | call with two different `X-Tenant` values | real service | both responses share same `data.id` |
|
||||
| COM-003 | HTTP | `POST /wx/common/file/upload` | uploads to OSS returns URL | multipart file | mock `IOssFileService.upload` | returned URL pinned |
|
||||
| COM-004 | HTTP | `GET /wx/common/audio/upload` | mediaId empty -> error | none | none | error message pinned |
|
||||
| COM-005 | HTTP | `GET /wx/common/audio/upload` | successful path uploads mp3 | provide accessToken + temp audio stream | mock `WxAccessTokenService`, stub `WxFileUtils.getTemporaryMaterial` (via wrapper or test seam), mock OSS | returns URL; temp files cleaned |
|
||||
| COM-006 | unit/contract | `WxFileUtils.audioConvert2Mp3` | invalid source -> throws | empty file | none | error message pinned |
|
||||
|
||||
---
|
||||
|
||||
## 6) Customer (WxCustomController) — Orders, Gifts, Complaints, Follow, Leave Msg
|
||||
|
||||
| ID | Surface | Route | Scenario | Setup | Mocks | Assertions |
|
||||
|---:|---|---|---|---|---|---|
|
||||
| CUS-001 | HTTP | `GET /wx/custom/queryClerkDetailedById` | not logged in still works | tenantKey only | none | response pinned (no crash) |
|
||||
| CUS-002 | HTTP/DB | `GET /wx/custom/queryById` | returns clerkState=1 when openid matches clerk | seed custom+clerk share openid | none | response `clerkState` pinned |
|
||||
| CUS-003 | HTTP/DB | `POST /wx/custom/updateHideLevelState` | ignores client id and uses session id | logged-in custom | none | DB update on session row only |
|
||||
| CUS-004 | HTTP/DB | `POST /wx/custom/order/reward` | creates completed reward order | logged-in custom + balance | none | order row fields pinned; ledger/balance delta pinned |
|
||||
| CUS-005 | HTTP/DB | `POST /wx/custom/order/gift` | calls WxGiftOrderService and increments gift counts | logged-in custom + gift seeded | none | order created; both gift counters incremented |
|
||||
| CUS-006 | HTTP/DB | `POST /wx/custom/order/commodity` | creates specified order pending | logged-in custom + clerk + commodity | mock pricing if needed | order fields pinned |
|
||||
| CUS-007 | HTTP/DB | `POST /wx/custom/order/random` | success triggers notification + overdue task | logged-in custom + clerks eligible | mock `WxCustomMpService`, mock `OverdueOrderHandlerTask` | expected calls + order fields pinned |
|
||||
| CUS-008 | HTTP/DB | `POST /wx/custom/order/random` | insufficient balance fails and no order created | set balance=0 | none | error code pinned; no DB insert |
|
||||
| CUS-009 | HTTP | `POST /wx/custom/order/queryByPage` | only returns self purchaserBy | seed orders for multiple users | none | result contains only self |
|
||||
| CUS-010 | HTTP/DB | `GET /wx/custom/order/end` | state transition invoked | seed order | none | order status moved (pin) |
|
||||
| CUS-011 | HTTP/DB | `POST /wx/custom/order/cancellation` | pins cancellation refund record (images ignored) | seed pending/accepted order + send non-empty images | none | order canceled; refund record has `refundReason`; `images` stays null; order `refundReason` stays null |
|
||||
| CUS-012 | HTTP/DB | `POST /wx/custom/order/evaluate/add` | non-purchaser cannot evaluate | order purchaser different | none | error message pinned |
|
||||
| CUS-013 | HTTP | `GET /wx/custom/order/evaluate/queryByOrderId` | not evaluated -> error | seed order no eval | none | `当前订单未评价` |
|
||||
| CUS-014 | HTTP/DB | `POST /wx/custom/order/complaint/add` | non-purchaser cannot complain | order purchaser different | none | error message pinned |
|
||||
| CUS-015 | HTTP/DB | `POST /wx/custom/leave/add` | creates leave msg; response message pinned | logged-in custom | none | DB insert; response message currently `"取消成功"` |
|
||||
| CUS-016 | HTTP | `GET /wx/custom/leave/queryPermission` | permission true/false schema pinned | set conditions | none | response JSON has `permission` boolean and `msg` |
|
||||
| CUS-017 | HTTP/DB | `POST /wx/custom/followState/update` | idempotency and correctness | seed follow relation | none | follow state pinned |
|
||||
| CUS-018 | HTTP | `POST /wx/custom/follow/queryByPage` | paging/filters pinned | seed relations | none | page schema pinned |
|
||||
|
||||
---
|
||||
|
||||
## 7) Clerk (WxClerkController) — Apply, Profile Review, Order Ops, Privacy Rules
|
||||
|
||||
| ID | Surface | Route | Scenario | Setup | Mocks | Assertions |
|
||||
|---:|---|---|---|---|---|---|
|
||||
| CLK-001 | HTTP | `POST /wx/clerk/user/queryPerformanceInfo` | date normalize behavior pinned | seed orders | none | output stable for same input |
|
||||
| CLK-002 | HTTP | `GET /wx/clerk/user/queryLevelInfo` | hardcoded `levelAndRanking` list pinned | logged-in clerk | none | list size/content pinned |
|
||||
| CLK-003 | HTTP/Redis | `POST /wx/clerk/user/sendCode` | writes redis key with TTL and returns code | logged-in clerk | none | redis key format + TTL; response contains code |
|
||||
| CLK-004 | HTTP/Redis/DB | `POST /wx/clerk/user/bindCode` | wrong code -> error | seed redis code | none | `验证码错误` |
|
||||
| CLK-005 | HTTP/Redis/DB | `POST /wx/clerk/user/bindCode` | success updates phone and clears redis | seed redis code | none | DB phone updated; redis deleted |
|
||||
| CLK-006 | HTTP/DB | `POST /wx/clerk/user/add` | already clerk -> error | seed clerkState=1 | none | message pinned |
|
||||
| CLK-007 | HTTP/DB | `POST /wx/clerk/user/add` | already has pending review -> error | seed reviewState=0 | none | message pinned |
|
||||
| CLK-008 | HTTP | `POST /wx/clerk/user/add` | subscribe required | mock subscribe=false | mock `WxCustomMpService.checkSubscribeThrowsExp` | error message pinned |
|
||||
| CLK-009 | HTTP/DB | `POST /wx/clerk/user/updateNickname` | creates data review row with correct type and content | logged-in clerk | none | DB insert pinned |
|
||||
| CLK-010 | HTTP/DB | `POST /wx/clerk/user/updateAlbum` | empty album -> error | logged-in clerk | none | `最少上传一张照片` |
|
||||
| CLK-011 | HTTP/DB | `POST /wx/clerk/user/updateAlbum` | invalid new media -> error | seed play_media with mismatched owner/tenant/status | none | error message pinned |
|
||||
| CLK-012 | HTTP/DB | `POST /wx/clerk/user/updateAlbum` | legacy IDs not found -> should not fail (current behavior) | album includes missing IDs | none | request succeeds; review content contains legacy strings |
|
||||
| CLK-013 | HTTP/DB | `GET /wx/clerk/order/queryById` | privacy: non-owner clears weiChatCode | seed order acceptBy other clerk | none | weiChatCode empty |
|
||||
| CLK-014 | HTTP/DB | `GET /wx/clerk/order/queryById` | canceled order clears weiChatCode | seed orderStatus=4 | none | weiChatCode empty |
|
||||
| CLK-015 | HTTP | `GET /wx/clerk/order/accept` | subscribe required | mock subscribe=false | mock `WxCustomMpService` | fails |
|
||||
| CLK-016 | HTTP/DB | `GET /wx/clerk/order/start` | state transition pinned | seed order | none | state updated |
|
||||
| CLK-017 | HTTP/DB | `POST /wx/clerk/order/complete` | sysUserId missing -> error | seed clerk no sysUserId | none | message pinned |
|
||||
| CLK-018 | HTTP/DB | `POST /wx/clerk/order/complete` | permission resolution pinned (admin vs group leader) | seed sysUser mapping | none | correct operatorType chosen or error |
|
||||
| CLK-019 | HTTP/DB | `POST /wx/clerk/order/cancellation` | cancellation state pinned | seed order | none | status/cancel fields updated |
|
||||
| CLK-020 | HTTP | `POST /wx/clerk/user/queryEvaluateByPage` | forces hidden=VISIBLE | seed hidden evaluations | none | response excludes hidden |
|
||||
|
||||
---
|
||||
|
||||
## 8) Orders (WxOrderInfoController) — Continuation + Random Order Masking
|
||||
|
||||
| ID | Surface | Route | Scenario | Setup | Mocks | Assertions |
|
||||
|---:|---|---|---|---|---|---|
|
||||
| ORD-001 | HTTP/DB | `POST /wx/order/clerk/continue` | non-owner cannot continue | order acceptBy != clerk | none | message pinned |
|
||||
| ORD-002 | HTTP/DB | `POST /wx/order/clerk/continue` | second continuation blocked | seed continue record | none | message pinned |
|
||||
| ORD-003 | HTTP | `GET /wx/order/clerk/selectRandomOrderById` | masking for non-owner pinned | seed random order accepted by other | none | fields blanked as implemented |
|
||||
| ORD-004 | HTTP/DB | `POST /wx/order/custom/updateReviewState` | reviewedState != 0 -> error | seed reviewedState=1 | none | `续单已处理` |
|
||||
| ORD-005 | HTTP/DB | `POST /wx/order/custom/continueListByPage` | customId forced to session | seed multiple users | none | only self results |
|
||||
|
||||
---
|
||||
|
||||
## 9) Coupons (WxCouponController)
|
||||
|
||||
| ID | Surface | Route | Scenario | Setup | Mocks | Assertions |
|
||||
|---:|---|---|---|---|---|---|
|
||||
| CP-001 | HTTP | `GET /wx/coupon/custom/obtainCoupon` | id empty -> error message pinned | logged-in custom | none | error message pinned |
|
||||
| CP-002 | HTTP/DB | obtain coupon | not eligible -> returns reason | seed coupon restrictions | none | response contains reason |
|
||||
| CP-003 | HTTP/DB | obtain coupon | eligible -> claim succeeds | seed coupon inventory | none | coupon_details inserted; response success |
|
||||
| CP-004 | HTTP | query all | whitelist hides coupons from non-whitelisted | seed coupon whitelist | none | coupon not present |
|
||||
| CP-005 | HTTP | query by order | clerkId+levelId both empty -> error | none | none | message pinned |
|
||||
| CP-006 | HTTP/DB | query by order | unavailable coupon includes reasonForUnavailableUse | seed coupon restrictions | none | available=0 and reason set |
|
||||
| CP-007 | HTTP/DB | query by order | exceptions inside loop are swallowed (current behavior) | seed one broken coupon | none | endpoint still returns 200 with remaining coupons |
|
||||
|
||||
---
|
||||
|
||||
## 10) Media (WxClerkMediaController + MediaUploadService)
|
||||
|
||||
| ID | Surface | Route | Scenario | Setup | Mocks | Assertions |
|
||||
|---:|---|---|---|---|---|---|
|
||||
| MED-001 | HTTP | `POST /wx/clerk/media/upload` | missing file -> error | logged-in clerk | none | message pinned |
|
||||
| MED-002 | HTTP/DB | upload image | creates `play_media` + `play_clerk_media_asset` | image multipart | mock OSS | DB rows inserted; kind=image; owner=clerk |
|
||||
| MED-003 | HTTP/DB | upload video too large | exceeds 30MB -> error | video > limit | none | error message pinned |
|
||||
| MED-004 | HTTP/DB | upload video too long | duration>45s -> error | long video | none | message pinned |
|
||||
| MED-005 | HTTP/DB | `PUT /wx/clerk/media/order` | distinct mediaIds and ordering pinned | seed assets | none | order_index updates pinned; duplicates removed |
|
||||
| MED-006 | HTTP/DB | `DELETE /wx/clerk/media/{id}` | marks `review_state=rejected` and sets `play_media.status=rejected` (asset `deleted` stays `0` currently) | seed media + asset | none | asset `review_state` becomes rejected; `play_media.status` becomes rejected; `asset.deleted` remains `0` |
|
||||
| MED-007 | HTTP | `GET /wx/clerk/media/list` | returns only draft/pending/rejected | seed assets states | none | filtering pinned |
|
||||
| MED-008 | HTTP | `GET /wx/clerk/media/approved` | returns only approved | seed assets | none | filtering pinned |
|
||||
| MED-009 | DB constraint | `uk_clerk_usage_media` | duplicate submit behavior pinned | seed duplicate row | none | error or ignore (pin current) |
|
||||
|
||||
---
|
||||
|
||||
## 11) Blind Box (WxBlindBoxController + WxBlindBoxOrderService)
|
||||
|
||||
| ID | Surface | Route | Scenario | Setup | Mocks | Assertions |
|
||||
|---:|---|---|---|---|---|---|
|
||||
| BB-001 | HTTP | `GET /wx/blind-box/config/list` | not logged in -> error | none | none | message pinned |
|
||||
| BB-002 | HTTP | list configs | only active configs for tenant | seed configs | none | list content pinned |
|
||||
| BB-003 | HTTP/DB | `POST /wx/blind-box/order/purchase` | tenant mismatch -> “not found” (TenantLine current behavior) | config tenant != user tenant | none | message pinned (`盲盒不存在`) |
|
||||
| BB-004 | HTTP/DB | purchase | creates completed order + reward | seed config + balance | none | order type pinned; reward row created |
|
||||
| BB-005 | HTTP/DB | `POST /wx/blind-box/reward/{id}/dispatch` | reward not found -> error | none | none | message pinned |
|
||||
| BB-006 | HTTP/DB | dispatch | status transition pinned | seed reward | none | status updated; response view pinned |
|
||||
| BB-007 | HTTP | `GET /wx/blind-box/reward/list` | status filter pinned | seed rewards | none | filter results pinned |
|
||||
| BB-008 | HTTP/DB | `POST /wx/blind-box/order/purchase` | insufficient balance -> error and no new order | customer balance < config price | none | message pinned; no new order inserted |
|
||||
|
||||
---
|
||||
|
||||
## 12) PK (WxPkController)
|
||||
|
||||
| ID | Surface | Route | Scenario | Setup | Mocks | Assertions |
|
||||
|---:|---|---|---|---|---|---|
|
||||
| PK-001 | HTTP | `GET /wx/pk/clerk/live` | clerkId missing -> error | none | none | message pinned |
|
||||
| PK-002 | HTTP | live | no pk -> inactive dto | seed none | none | returns inactive dto |
|
||||
| PK-003 | HTTP | live | pk exists but status!=IN_PROGRESS -> inactive | seed pk | none | inactive |
|
||||
| PK-004 | HTTP/Redis | upcoming | tenant missing -> error | no tenant context | none | message pinned |
|
||||
| PK-005 | HTTP/Redis | upcoming | redis hit/miss produces stable behavior | seed redis keys | none | response pinned |
|
||||
| PK-006 | HTTP | schedule/history | limit/page normalization pinned | none | none | safeLimit behavior pinned |
|
||||
|
||||
---
|
||||
|
||||
## 13) Shop + Articles + Misc
|
||||
|
||||
| ID | Surface | Route | Scenario | Setup | Mocks | Assertions |
|
||||
|---:|---|---|---|---|---|---|
|
||||
| SHOP-001 | HTTP | `GET /wx/shop/custom/getShopHomeCarouseInfo` | returns carousel list | seed carousel | none | mapping pinned |
|
||||
| SHOP-002 | HTTP/DB | `GET /wx/shop/clerk/readShopArticleInfo` | visitsNumber increments | seed article | none | visitsNumber +1 persisted |
|
||||
| ART-001 | HTTP/DB | `POST /wx/article/clerk/add` | creates article with clerkId from session | logged-in clerk | none | DB insert pinned |
|
||||
| ART-002 | HTTP/DB | `GET /wx/article/clerk/deleteById` | deletes clerk article + custom article links | seed both | none | rows removed/soft-deleted pinned |
|
||||
| ART-003 | HTTP/DB | `POST /wx/article/custom/updateGreedState` | pins current toggle behavior (not strictly idempotent) | seed article | none | at least one row flips to `endorseState=0` after toggle |
|
||||
| ART-004 | HTTP/DB | `POST /wx/article/custom/updateFollowState` | same for follow | seed article | none | record updated |
|
||||
|
||||
---
|
||||
|
||||
## 14) Wages + Withdraw (WxClerkWagesController + WxWithdrawController)
|
||||
|
||||
| ID | Surface | Route | Scenario | Setup | Mocks | Assertions |
|
||||
|---:|---|---|---|---|---|---|
|
||||
| WAGE-001 | HTTP/DB | `GET /wx/wages/clerk/queryUnsettledWages` | sums orders correctly | seed settlement_state=0 orders | none | totals pinned |
|
||||
| WAGE-002 | HTTP | `GET /wx/wages/clerk/queryCurrentPeriodWages` | missing wages row returns constructed zeros | no wages row | none | response has zeros and dates set |
|
||||
| WAGE-003 | HTTP | `POST /wx/wages/clerk/queryHistoricalWages` | current hard-coded page meta pinned | seed some rows | none | total=5,size=10,pages=1 pinned |
|
||||
| WD-001 | HTTP | `GET /wx/withdraw/balance` | returns available/pending/nextUnlock | seed earnings lines | none | values pinned |
|
||||
| WD-002 | HTTP | `GET /wx/withdraw/earnings` | time parsing supports multiple formats | seed earnings | none | filter correctness pinned |
|
||||
| WD-003 | HTTP/DB | `POST /wx/withdraw/requests` | amount<=0 error | none | none | message pinned |
|
||||
| WD-004 | HTTP/DB | create request | creates withdrawal request and reserves lines | seed available lines | none | statuses pinned |
|
||||
| WD-005 | HTTP | request logs | non-owner forbidden | seed request other clerk | none | `无权查看` |
|
||||
| PAYEE-001 | HTTP | `GET /wx/withdraw/payee` | no profile returns null data (current behavior) | none | none | response pinned |
|
||||
| PAYEE-002 | HTTP/DB | `POST /wx/withdraw/payee` | missing qrCodeUrl -> error | none | none | message pinned |
|
||||
| PAYEE-003 | HTTP/DB | upsert defaulting | defaults channel/displayName | seed clerk | none | stored defaults pinned |
|
||||
| PAYEE-004 | HTTP/DB | confirm requires qrCodeUrl | no profile | none | error message pinned |
|
||||
|
||||
---
|
||||
|
||||
## Coverage Checklist (for “done”)
|
||||
|
||||
- Every `@RequestMapping("/wx...")` route has at least:
|
||||
- 1 happy-path integration test
|
||||
- 1 auth/tenant gating test (where applicable)
|
||||
- 1 validation failure test (where applicable)
|
||||
- 1 DB side-effect snapshot assertion for routes that mutate state
|
||||
- WeChat Pay callback has:
|
||||
- replay/idempotency tests
|
||||
- malformed XML test
|
||||
- missing attach test
|
||||
- Media pipeline has:
|
||||
- type detection + size/duration limits pinned
|
||||
- DB uniqueness behavior pinned
|
||||
|
||||
## Test Case ID Naming Convention (important)
|
||||
|
||||
- In this doc, case IDs are written like `OAUTH-007`.
|
||||
- In Java test method names, we normalize them as `OAUTH_007` because `-` is not valid in Java identifiers.
|
||||
|
||||
## Implemented Coverage (current)
|
||||
|
||||
- `/Volumes/main/code/yunpei/peipei-backend/play-admin/src/test/java/com/starry/admin/api/WxOauthControllerApiTest.java`: `OAUTH-001..008`, `OAUTH-011`
|
||||
- `/Volumes/main/code/yunpei/peipei-backend/play-admin/src/test/java/com/starry/admin/modules/weichat/service/WxOauthServiceTest.java`: `OAUTH-009`, `OAUTH-010`
|
||||
- `/Volumes/main/code/yunpei/peipei-backend/play-admin/src/test/java/com/starry/admin/api/WxPayControllerApiTest.java`: `PAY-001..011`
|
||||
- `/Volumes/main/code/yunpei/peipei-backend/play-admin/src/test/java/com/starry/admin/api/WxAuthAspectApiTest.java`: `AOP-001..005`
|
||||
- `/Volumes/main/code/yunpei/peipei-backend/play-admin/src/test/java/com/starry/admin/modules/weichat/service/WxTokenServiceTest.java`: `TOK-001..002`
|
||||
- `/Volumes/main/code/yunpei/peipei-backend/play-admin/src/test/java/com/starry/admin/api/WxClerkMediaControllerEndpointsApiTest.java`: `MED-005..008` (note: `MED-006` currently keeps `asset.deleted=0`)
|
||||
- `/Volumes/main/code/yunpei/peipei-backend/play-admin/src/test/java/com/starry/admin/api/WxCommonControllerAudioUploadApiTest.java`: covers audio upload behavior (see `MED-*` section for alignment)
|
||||
@@ -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` 不會被清除,舊客端仍能看到舊資料。
|
||||
@@ -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>
|
||||
@@ -25,10 +24,6 @@
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-websocket</artifactId>
|
||||
</dependency>
|
||||
<!-- Flyway -->
|
||||
<dependency>
|
||||
<groupId>org.flywaydb</groupId>
|
||||
@@ -163,42 +158,6 @@
|
||||
<artifactId>mockito-junit-jupiter</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.mockito</groupId>
|
||||
<artifactId>mockito-inline</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>
|
||||
|
||||
@@ -229,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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,754 +0,0 @@
|
||||
package com.starry.admin.common.apitest;
|
||||
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
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.PlayClerkLevelInfoEntity;
|
||||
import com.starry.admin.modules.clerk.module.entity.PlayClerkUserInfoEntity;
|
||||
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.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.mapper.PlayCommodityInfoMapper;
|
||||
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.mapper.SysMenuMapper;
|
||||
import com.starry.admin.modules.system.mapper.SysUserMapper;
|
||||
import com.starry.admin.modules.system.module.entity.SysMenuEntity;
|
||||
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.constant.UserConstants;
|
||||
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 java.util.Optional;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.boot.CommandLineRunner;
|
||||
import org.springframework.context.annotation.Profile;
|
||||
import org.springframework.dao.DuplicateKeyException;
|
||||
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_CUSTOMER_OPEN_ID = "openid-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");
|
||||
public static final BigDecimal E2E_CUSTOMER_BALANCE = new BigDecimal("1000.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 SysUserMapper sysUserMapper;
|
||||
private final SysMenuMapper sysMenuMapper;
|
||||
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 PlayClerkUserInfoMapper clerkUserInfoMapper;
|
||||
private final PlayCommodityInfoMapper commodityInfoMapper;
|
||||
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,
|
||||
SysUserMapper sysUserMapper,
|
||||
SysMenuMapper sysMenuMapper,
|
||||
IPlayPersonnelGroupInfoService personnelGroupInfoService,
|
||||
IPlayClerkLevelInfoService clerkLevelInfoService,
|
||||
IPlayClerkUserInfoService clerkUserInfoService,
|
||||
IPlayCommodityInfoService commodityInfoService,
|
||||
IPlayCommodityAndLevelInfoService commodityAndLevelInfoService,
|
||||
IPlayGiftInfoService giftInfoService,
|
||||
IPlayClerkCommodityService clerkCommodityService,
|
||||
PlayClerkUserInfoMapper clerkUserInfoMapper,
|
||||
PlayCommodityInfoMapper commodityInfoMapper,
|
||||
IPlayClerkGiftInfoService playClerkGiftInfoService,
|
||||
IPlayCustomUserInfoService customUserInfoService,
|
||||
IPlayCustomGiftInfoService playCustomGiftInfoService,
|
||||
PlayClerkGiftInfoMapper playClerkGiftInfoMapper,
|
||||
PlayCustomGiftInfoMapper playCustomGiftInfoMapper,
|
||||
PasswordEncoder passwordEncoder,
|
||||
WxTokenService wxTokenService) {
|
||||
this.tenantPackageService = tenantPackageService;
|
||||
this.tenantService = tenantService;
|
||||
this.sysUserService = sysUserService;
|
||||
this.sysUserMapper = sysUserMapper;
|
||||
this.sysMenuMapper = sysMenuMapper;
|
||||
this.personnelGroupInfoService = personnelGroupInfoService;
|
||||
this.clerkLevelInfoService = clerkLevelInfoService;
|
||||
this.clerkUserInfoService = clerkUserInfoService;
|
||||
this.commodityInfoService = commodityInfoService;
|
||||
this.commodityAndLevelInfoService = commodityAndLevelInfoService;
|
||||
this.giftInfoService = giftInfoService;
|
||||
this.clerkCommodityService = clerkCommodityService;
|
||||
this.clerkUserInfoMapper = clerkUserInfoMapper;
|
||||
this.commodityInfoMapper = commodityInfoMapper;
|
||||
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) {
|
||||
seedPcTenantWagesMenu();
|
||||
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 seedPcTenantWagesMenu() {
|
||||
// Minimal menu tree for pc-tenant E2E: /play/clerk/wages -> play/clerk/wages/index.vue
|
||||
// This is apitest-only; prod/dev menus are managed by ops/admin tooling.
|
||||
SysMenuEntity playRoot = ensureMenu(
|
||||
"陪聊管理",
|
||||
"PlayManage",
|
||||
0L,
|
||||
UserConstants.TYPE_DIR,
|
||||
"/play",
|
||||
UserConstants.LAYOUT,
|
||||
50);
|
||||
|
||||
SysMenuEntity clerkDir = ensureMenu(
|
||||
"店员管理",
|
||||
"ClerkManage",
|
||||
playRoot.getMenuId(),
|
||||
UserConstants.TYPE_DIR,
|
||||
"clerk",
|
||||
"",
|
||||
1);
|
||||
|
||||
ensureMenu(
|
||||
"收益管理",
|
||||
"ClerkWages",
|
||||
clerkDir.getMenuId(),
|
||||
UserConstants.TYPE_MENU,
|
||||
"wages",
|
||||
"play/clerk/wages/index",
|
||||
1);
|
||||
}
|
||||
|
||||
private SysMenuEntity ensureMenu(
|
||||
String menuName,
|
||||
String menuCode,
|
||||
Long parentId,
|
||||
String menuType,
|
||||
String path,
|
||||
String component,
|
||||
Integer sort) {
|
||||
Optional<SysMenuEntity> existing = sysMenuMapper.selectList(Wrappers.<SysMenuEntity>lambdaQuery()
|
||||
.eq(SysMenuEntity::getDeleted, false)
|
||||
.eq(SysMenuEntity::getParentId, parentId)
|
||||
.eq(SysMenuEntity::getMenuCode, menuCode)
|
||||
.last("limit 1"))
|
||||
.stream()
|
||||
.findFirst();
|
||||
if (existing.isPresent()) {
|
||||
SysMenuEntity current = existing.get();
|
||||
boolean changed = false;
|
||||
if (!Objects.equals(current.getPath(), path)) {
|
||||
current.setPath(path);
|
||||
changed = true;
|
||||
}
|
||||
if (!Objects.equals(current.getComponent(), component)) {
|
||||
current.setComponent(component);
|
||||
changed = true;
|
||||
}
|
||||
if (changed) {
|
||||
current.setUpdatedBy("apitest-seed");
|
||||
current.setUpdatedTime(new Date());
|
||||
sysMenuMapper.updateById(current);
|
||||
log.info("Updated apitest sys_menu '{}' path='{}' component='{}'", menuName, path, component);
|
||||
}
|
||||
return current;
|
||||
}
|
||||
|
||||
SysMenuEntity entity = new SysMenuEntity();
|
||||
entity.setMenuName(menuName);
|
||||
entity.setMenuCode(menuCode);
|
||||
entity.setIcon("el-icon-menu");
|
||||
entity.setPermission("");
|
||||
entity.setMenuLevel(parentId == 0 ? 1L : 2L);
|
||||
entity.setSort(sort);
|
||||
entity.setParentId(parentId);
|
||||
entity.setMenuType(menuType);
|
||||
entity.setStatus(0);
|
||||
entity.setRemark(menuName);
|
||||
entity.setPath(path);
|
||||
entity.setComponent(component);
|
||||
entity.setRouterQuery("");
|
||||
entity.setIsFrame(0);
|
||||
entity.setVisible(1);
|
||||
entity.setDeleted(Boolean.FALSE);
|
||||
entity.setCreatedBy("apitest-seed");
|
||||
entity.setCreatedTime(new Date());
|
||||
entity.setUpdatedBy("apitest-seed");
|
||||
entity.setUpdatedTime(new Date());
|
||||
sysMenuMapper.insert(entity);
|
||||
log.info("Inserted apitest sys_menu '{}' path='{}' parentId={}", menuName, path, parentId);
|
||||
return entity;
|
||||
}
|
||||
|
||||
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) {
|
||||
boolean changed = false;
|
||||
if (tenant.getAppId() == null || tenant.getAppId().isEmpty()) {
|
||||
tenant.setAppId("wx-apitest-appid");
|
||||
changed = true;
|
||||
}
|
||||
if (tenant.getSecret() == null || tenant.getSecret().isEmpty()) {
|
||||
tenant.setSecret("wx-apitest-secret");
|
||||
changed = true;
|
||||
}
|
||||
if (tenant.getMchId() == null || tenant.getMchId().isEmpty()) {
|
||||
tenant.setMchId("wx-apitest-mchid");
|
||||
changed = true;
|
||||
}
|
||||
if (tenant.getMchKey() == null || tenant.getMchKey().isEmpty()) {
|
||||
tenant.setMchKey("wx-apitest-mchkey");
|
||||
changed = true;
|
||||
}
|
||||
if (changed) {
|
||||
tenantService.updateById(tenant);
|
||||
log.info("API test tenant {} already exists, wechat config refreshed", DEFAULT_TENANT_ID);
|
||||
return;
|
||||
}
|
||||
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.setAppId("wx-apitest-appid");
|
||||
entity.setSecret("wx-apitest-secret");
|
||||
entity.setMchId("wx-apitest-mchid");
|
||||
entity.setMchKey("wx-apitest-mchkey");
|
||||
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 = sysUserMapper.selectUserByUserNameAndTenantId(
|
||||
DEFAULT_ADMIN_USERNAME, DEFAULT_TENANT_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());
|
||||
try {
|
||||
personnelGroupInfoService.save(entity);
|
||||
log.info("Inserted API test personnel group {}", DEFAULT_GROUP_ID);
|
||||
} catch (DuplicateKeyException duplicateKeyException) {
|
||||
log.info("API test personnel group {} already inserted by another test context", 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);
|
||||
try {
|
||||
clerkLevelInfoService.save(entity);
|
||||
log.info("Inserted API test clerk level {}", DEFAULT_CLERK_LEVEL_ID);
|
||||
} catch (DuplicateKeyException duplicateKeyException) {
|
||||
log.info("API test clerk level {} already inserted by another test context", DEFAULT_CLERK_LEVEL_ID);
|
||||
}
|
||||
}
|
||||
|
||||
private PlayCommodityInfoEntity seedCommodityHierarchy() {
|
||||
PlayCommodityInfoEntity parent = commodityInfoService.getById(DEFAULT_COMMODITY_PARENT_ID);
|
||||
if (parent == null) {
|
||||
PlayCommodityInfoEntity existingParent = commodityInfoMapper
|
||||
.selectByIdIncludingDeleted(DEFAULT_COMMODITY_PARENT_ID);
|
||||
if (existingParent != null) {
|
||||
commodityInfoMapper.restoreCommodity(DEFAULT_COMMODITY_PARENT_ID, DEFAULT_TENANT_ID);
|
||||
parent = commodityInfoService.getById(DEFAULT_COMMODITY_PARENT_ID);
|
||||
if (parent == null) {
|
||||
parent = existingParent;
|
||||
}
|
||||
}
|
||||
}
|
||||
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) {
|
||||
PlayCommodityInfoEntity existingChild = commodityInfoMapper
|
||||
.selectByIdIncludingDeleted(DEFAULT_COMMODITY_ID);
|
||||
if (existingChild != null) {
|
||||
commodityInfoMapper.restoreCommodity(DEFAULT_COMMODITY_ID, DEFAULT_TENANT_ID);
|
||||
child = commodityInfoService.getById(DEFAULT_COMMODITY_ID);
|
||||
if (child == null) {
|
||||
child = existingChild;
|
||||
}
|
||||
}
|
||||
}
|
||||
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.update(Wrappers.<PlayClerkUserInfoEntity>lambdaUpdate()
|
||||
.eq(PlayClerkUserInfoEntity::getId, DEFAULT_CLERK_ID)
|
||||
.set(PlayClerkUserInfoEntity::getDeleted, Boolean.FALSE)
|
||||
.set(PlayClerkUserInfoEntity::getOnboardingState, OnboardingStatus.ACTIVE.getCode())
|
||||
.set(PlayClerkUserInfoEntity::getListingState, ListingStatus.LISTED.getCode())
|
||||
.set(PlayClerkUserInfoEntity::getDisplayState, "1")
|
||||
.set(PlayClerkUserInfoEntity::getRandomOrderState, "1")
|
||||
.set(PlayClerkUserInfoEntity::getClerkState, ClerkRoleStatus.CLERK.getCode())
|
||||
.set(PlayClerkUserInfoEntity::getOnlineState, "1")
|
||||
.set(PlayClerkUserInfoEntity::getAvatar, "https://example.com/avatar.png")
|
||||
.set(PlayClerkUserInfoEntity::getToken, clerkToken));
|
||||
log.info("API test clerk {} already exists, state refreshed", DEFAULT_CLERK_ID);
|
||||
return;
|
||||
}
|
||||
PlayClerkUserInfoEntity existing = clerkUserInfoMapper.selectByIdIncludingDeleted(DEFAULT_CLERK_ID);
|
||||
if (existing != null) {
|
||||
clerkUserInfoService.update(Wrappers.<PlayClerkUserInfoEntity>lambdaUpdate()
|
||||
.eq(PlayClerkUserInfoEntity::getId, DEFAULT_CLERK_ID)
|
||||
.set(PlayClerkUserInfoEntity::getDeleted, Boolean.FALSE)
|
||||
.set(PlayClerkUserInfoEntity::getToken, clerkToken));
|
||||
log.info("API test clerk {} restored from deleted state", 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);
|
||||
String commodityName = DEFAULT_COMMODITY_PARENT_NAME;
|
||||
PlayCommodityInfoEntity parent = commodityInfoService.getById(DEFAULT_COMMODITY_PARENT_ID);
|
||||
if (parent != null && parent.getItemName() != null) {
|
||||
commodityName = parent.getItemName();
|
||||
}
|
||||
|
||||
if (mapping != null) {
|
||||
clerkCommodityService.update(Wrappers.<PlayClerkCommodityEntity>lambdaUpdate()
|
||||
.eq(PlayClerkCommodityEntity::getId, DEFAULT_CLERK_COMMODITY_ID)
|
||||
.set(PlayClerkCommodityEntity::getCommodityId, DEFAULT_COMMODITY_PARENT_ID)
|
||||
.set(PlayClerkCommodityEntity::getCommodityName, commodityName)
|
||||
.set(PlayClerkCommodityEntity::getEnablingState, "1"));
|
||||
log.info("API test clerk commodity {} already exists, state refreshed", DEFAULT_CLERK_COMMODITY_ID);
|
||||
return;
|
||||
}
|
||||
|
||||
PlayClerkCommodityEntity entity = new PlayClerkCommodityEntity();
|
||||
entity.setId(DEFAULT_CLERK_COMMODITY_ID);
|
||||
entity.setTenantId(DEFAULT_TENANT_ID);
|
||||
entity.setClerkId(DEFAULT_CLERK_ID);
|
||||
entity.setCommodityId(DEFAULT_COMMODITY_PARENT_ID);
|
||||
entity.setCommodityName(commodityName);
|
||||
entity.setEnablingState("1");
|
||||
entity.setSort(1);
|
||||
try {
|
||||
clerkCommodityService.save(entity);
|
||||
} catch (DuplicateKeyException duplicateKeyException) {
|
||||
log.info(
|
||||
"API test clerk commodity {} already inserted by another test context",
|
||||
DEFAULT_CLERK_COMMODITY_ID);
|
||||
return;
|
||||
}
|
||||
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");
|
||||
try {
|
||||
giftInfoService.save(entity);
|
||||
} catch (DuplicateKeyException duplicateKeyException) {
|
||||
log.info("API test gift {} already inserted by another test context", DEFAULT_GIFT_ID);
|
||||
return;
|
||||
}
|
||||
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(DEFAULT_CUSTOMER_OPEN_ID);
|
||||
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);
|
||||
try {
|
||||
customUserInfoService.save(entity);
|
||||
} catch (DuplicateKeyException duplicateKeyException) {
|
||||
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 inserted by another test context", DEFAULT_CUSTOMER_ID);
|
||||
return;
|
||||
}
|
||||
log.info("Inserted API test customer {}", DEFAULT_CUSTOMER_ID);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -42,10 +42,7 @@ public class PermissionService {
|
||||
}
|
||||
LoginUser loginUser = SecurityUtils.getLoginUser();
|
||||
if (loginUser == null || CollectionUtils.isEmpty(loginUser.getPermissions())) {
|
||||
return loginUser != null && loginUser.getUser() != null && SecurityUtils.isAdmin(loginUser.getUser());
|
||||
}
|
||||
if (loginUser.getUser() != null && SecurityUtils.isAdmin(loginUser.getUser())) {
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
return hasPermissions(loginUser.getPermissions(), permission);
|
||||
}
|
||||
@@ -73,13 +70,7 @@ public class PermissionService {
|
||||
return false;
|
||||
}
|
||||
LoginUser loginUser = SecurityUtils.getLoginUser();
|
||||
if (loginUser == null) {
|
||||
return false;
|
||||
}
|
||||
if (loginUser.getUser() != null && SecurityUtils.isAdmin(loginUser.getUser())) {
|
||||
return true;
|
||||
}
|
||||
if (CollectionUtils.isEmpty(loginUser.getPermissions())) {
|
||||
if (loginUser == null || CollectionUtils.isEmpty(loginUser.getPermissions())) {
|
||||
return false;
|
||||
}
|
||||
Set<String> authorities = loginUser.getPermissions();
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
package com.starry.admin.common.config;
|
||||
|
||||
import com.starry.admin.modules.weichat.constant.WebSocketConstant;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
|
||||
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
|
||||
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
|
||||
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
|
||||
|
||||
/**
|
||||
* WebSocket 配置,基于 STOMP 的简单消息代理。
|
||||
*/
|
||||
@Configuration
|
||||
@EnableWebSocketMessageBroker
|
||||
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
|
||||
|
||||
private static final String APPLICATION_DESTINATION_PREFIX = "/app";
|
||||
private static final String TOPIC_DESTINATION_PREFIX = "/topic";
|
||||
private static final String PK_ENDPOINT = "/ws/pk";
|
||||
|
||||
@Override
|
||||
public void configureMessageBroker(MessageBrokerRegistry registry) {
|
||||
registry.enableSimpleBroker(TOPIC_DESTINATION_PREFIX);
|
||||
registry.setApplicationDestinationPrefixes(APPLICATION_DESTINATION_PREFIX);
|
||||
registry.setUserDestinationPrefix(WebSocketConstant.USER_DESTINATION_PREFIX);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void registerStompEndpoints(StompEndpointRegistry registry) {
|
||||
registry.addEndpoint(PK_ENDPOINT).setAllowedOriginPatterns("*");
|
||||
}
|
||||
}
|
||||
@@ -8,11 +8,7 @@ import com.starry.common.utils.StringUtils;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.http.converter.HttpMessageNotReadableException;
|
||||
import org.springframework.security.access.AccessDeniedException;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.validation.BindingResult;
|
||||
import org.springframework.validation.FieldError;
|
||||
import org.springframework.web.HttpRequestMethodNotSupportedException;
|
||||
@@ -28,17 +24,6 @@ import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||
@RestControllerAdvice
|
||||
public class GlobalExceptionHandler {
|
||||
private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);
|
||||
public static final String PARAMETER_FORMAT_ERROR = "请求参数格式异常";
|
||||
|
||||
@ExceptionHandler(AccessDeniedException.class)
|
||||
public ResponseEntity<R> handleAccessDenied(AccessDeniedException e) {
|
||||
return ResponseEntity.status(HttpStatus.FORBIDDEN).body(R.error(403, e.getMessage()));
|
||||
}
|
||||
|
||||
@ExceptionHandler(AuthenticationException.class)
|
||||
public ResponseEntity<R> handleAuthentication(AuthenticationException e) {
|
||||
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(R.error(401, e.getMessage()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 业务异常
|
||||
@@ -47,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());
|
||||
@@ -102,20 +85,20 @@ public class GlobalExceptionHandler {
|
||||
|
||||
@ExceptionHandler(MismatchedInputException.class)
|
||||
public R mismatchedInputException(MismatchedInputException e) {
|
||||
log.error(PARAMETER_FORMAT_ERROR, e);
|
||||
return R.error(PARAMETER_FORMAT_ERROR);
|
||||
log.error("请求参数格式异常", e);
|
||||
return R.error("请求参数格式异常");
|
||||
}
|
||||
|
||||
@ExceptionHandler(HttpMessageNotReadableException.class)
|
||||
public R httpMessageNotReadableException(HttpMessageNotReadableException e) {
|
||||
log.error(PARAMETER_FORMAT_ERROR, e);
|
||||
return R.error(PARAMETER_FORMAT_ERROR);
|
||||
log.error("请求参数格式异常", e);
|
||||
return R.error("请求参数格式异常");
|
||||
}
|
||||
|
||||
@ExceptionHandler(MissingServletRequestParameterException.class)
|
||||
public R missingServletRequestParameterException(MissingServletRequestParameterException e) {
|
||||
log.error(PARAMETER_FORMAT_ERROR, e);
|
||||
return R.error(PARAMETER_FORMAT_ERROR);
|
||||
log.error("请求参数格式异常", e);
|
||||
return R.error("请求参数格式异常");
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -128,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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -1,127 +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.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.stream.Collectors;
|
||||
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;
|
||||
private static final String PERMISSIONS_HEADER = "X-Test-Permissions";
|
||||
private static final String SUPER_ADMIN_HEADER = "X-Test-Super-Admin";
|
||||
|
||||
public ApiTestAuthenticationFilter(ApiTestSecurityProperties properties) {
|
||||
this.properties = properties;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
|
||||
throws ServletException, IOException {
|
||||
Map<String, Object> originalContext = new ConcurrentHashMap<>(CustomSecurityContextHolder.getLocalMap());
|
||||
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);
|
||||
applyOverridesFromHeaders(request, loginUser);
|
||||
|
||||
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.setLocalMap(originalContext);
|
||||
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;
|
||||
}
|
||||
|
||||
private void applyOverridesFromHeaders(HttpServletRequest request, LoginUser loginUser) {
|
||||
if (loginUser == null || loginUser.getUser() == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
String superAdmin = request.getHeader(SUPER_ADMIN_HEADER);
|
||||
if (StringUtils.hasText(superAdmin)) {
|
||||
loginUser.getUser().setSuperAdmin(Boolean.parseBoolean(superAdmin));
|
||||
}
|
||||
|
||||
String permissionsHeader = request.getHeader(PERMISSIONS_HEADER);
|
||||
if (!StringUtils.hasText(permissionsHeader)) {
|
||||
return;
|
||||
}
|
||||
|
||||
Set<String> perms = Arrays.stream(permissionsHeader.split(","))
|
||||
.map(String::trim)
|
||||
.filter(StringUtils::hasText)
|
||||
.collect(Collectors.toSet());
|
||||
loginUser.setPermissions(perms);
|
||||
CustomSecurityContextHolder.setPermission(String.join(",", perms));
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -1,25 +1,24 @@
|
||||
package com.starry.admin.common.task;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
|
||||
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.module.entity.PlayClerkWagesDetailsInfoEntity;
|
||||
import com.starry.admin.modules.clerk.module.entity.PlayClerkWagesInfoEntity;
|
||||
import com.starry.admin.modules.clerk.service.IPlayClerkUserInfoService;
|
||||
import com.starry.admin.modules.clerk.service.IPlayClerkWagesDetailsInfoService;
|
||||
import com.starry.admin.modules.clerk.service.IPlayClerkWagesInfoService;
|
||||
import com.starry.admin.modules.order.module.constant.OrderConstant;
|
||||
import com.starry.admin.modules.order.module.entity.PlayOrderInfoEntity;
|
||||
import com.starry.admin.modules.order.service.IPlayOrderInfoService;
|
||||
import com.starry.admin.modules.system.module.entity.SysTenantEntity;
|
||||
import com.starry.admin.modules.system.service.ISysTenantService;
|
||||
import com.starry.admin.utils.SecurityUtils;
|
||||
import com.starry.common.utils.IdUtils;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
import javax.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
@@ -129,13 +128,14 @@ public class ClerkWagesSettlementTask {
|
||||
finalAmount = finalAmount.add(orderInfo.getFinalAmount());
|
||||
orderNumber++;
|
||||
estimatedRevenue = estimatedRevenue.add(orderInfo.getEstimatedRevenue());
|
||||
if ("0".equals(orderInfo.getFirstOrder())) {
|
||||
orderContinueNumber++;
|
||||
orderContinueMoney = orderContinueMoney.add(orderInfo.getFinalAmount());
|
||||
}
|
||||
if ("1".equals(orderInfo.getOrdersExpiredState())) {
|
||||
ordersExpiredNumber++;
|
||||
}
|
||||
}
|
||||
ContinuationMetrics continuationMetrics = calculateContinuationMetrics(orderInfoEntities);
|
||||
orderContinueNumber = continuationMetrics.count;
|
||||
orderContinueMoney = continuationMetrics.money;
|
||||
|
||||
PlayClerkWagesInfoEntity wagesInfo = new PlayClerkWagesInfoEntity();
|
||||
wagesInfo.setId(wagesId);
|
||||
@@ -158,51 +158,4 @@ public class ClerkWagesSettlementTask {
|
||||
|
||||
playClerkWagesInfoService.saveOrUpdate(wagesInfo);
|
||||
}
|
||||
|
||||
private ContinuationMetrics calculateContinuationMetrics(List<PlayOrderInfoEntity> orders) {
|
||||
List<PlayOrderInfoEntity> completedOrders = orders.stream()
|
||||
.filter(order -> OrderConstant.OrderStatus.COMPLETED.getCode().equals(order.getOrderStatus()))
|
||||
.collect(Collectors.toList());
|
||||
if (completedOrders.isEmpty()) {
|
||||
return new ContinuationMetrics(0, BigDecimal.ZERO);
|
||||
}
|
||||
int continuedCount = 0;
|
||||
BigDecimal continuedMoney = BigDecimal.ZERO;
|
||||
for (PlayOrderInfoEntity order : completedOrders) {
|
||||
String customerId = order.getPurchaserBy();
|
||||
if (StrUtil.isBlank(customerId)) {
|
||||
throw new CustomException("订单缺少顾客信息,无法统计续单");
|
||||
}
|
||||
OrderConstant.OrderRelationType relationType = normalizeRelationType(order);
|
||||
if (relationType == OrderConstant.OrderRelationType.CONTINUED) {
|
||||
continuedCount++;
|
||||
continuedMoney = continuedMoney.add(order.getFinalAmount());
|
||||
}
|
||||
}
|
||||
return new ContinuationMetrics(continuedCount, continuedMoney);
|
||||
}
|
||||
|
||||
private OrderConstant.OrderRelationType normalizeRelationType(PlayOrderInfoEntity order) {
|
||||
OrderConstant.OrderRelationType relationType = order.getOrderRelationType();
|
||||
if (relationType == null) {
|
||||
throw new CustomException("订单关系类型不能为空");
|
||||
}
|
||||
if (OrderConstant.PlaceType.RANDOM.getCode().equals(order.getPlaceType())) {
|
||||
return OrderConstant.OrderRelationType.FIRST;
|
||||
}
|
||||
if (relationType == OrderConstant.OrderRelationType.NEUTRAL) {
|
||||
return OrderConstant.OrderRelationType.FIRST;
|
||||
}
|
||||
return relationType;
|
||||
}
|
||||
|
||||
private static final class ContinuationMetrics {
|
||||
private final int count;
|
||||
private final BigDecimal money;
|
||||
|
||||
private ContinuationMetrics(int count, BigDecimal money) {
|
||||
this.count = count;
|
||||
this.money = money;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
package com.starry.admin.common.task;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.starry.admin.common.exception.CustomException;
|
||||
import com.starry.admin.modules.clerk.module.entity.PlayClerkRankingInfoEntity;
|
||||
import com.starry.admin.modules.clerk.module.entity.PlayClerkUserInfoEntity;
|
||||
import com.starry.admin.modules.clerk.service.IPlayClerkRankingInfoService;
|
||||
import com.starry.admin.modules.clerk.service.IPlayClerkUserInfoService;
|
||||
import com.starry.admin.modules.order.module.constant.OrderConstant;
|
||||
import com.starry.admin.modules.order.module.entity.PlayOrderInfoEntity;
|
||||
import com.starry.admin.modules.order.service.IPlayOrderInfoService;
|
||||
import com.starry.admin.modules.system.module.entity.SysTenantEntity;
|
||||
@@ -23,7 +20,6 @@ import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
import javax.annotation.Resource;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Component;
|
||||
@@ -138,13 +134,14 @@ public class OrderRankingSettlementTask {
|
||||
for (PlayOrderInfoEntity orderInfoEntity : orderInfoEntities) {
|
||||
customIds.add(orderInfoEntity.getAcceptBy());
|
||||
orderMoney = orderMoney.add(orderInfoEntity.getOrderMoney());
|
||||
if ("0".equals(orderInfoEntity.getFirstOrder())) {
|
||||
orderContinueNumber++;
|
||||
orderContinueMoney = orderContinueMoney.add(orderInfoEntity.getOrderMoney());
|
||||
}
|
||||
if ("1".equals(orderInfoEntity.getOrdersExpiredState())) {
|
||||
ordersExpiredNumber++;
|
||||
}
|
||||
}
|
||||
ContinuationMetrics continuationMetrics = calculateContinuationMetrics(orderInfoEntities);
|
||||
orderContinueNumber = continuationMetrics.count;
|
||||
orderContinueMoney = continuationMetrics.money;
|
||||
BigDecimal orderContinueProportion = orderNumber == 0 ? BigDecimal.ZERO : new BigDecimal(ordersExpiredNumber).divide(new BigDecimal(orderNumber), 4, RoundingMode.HALF_UP).add(new BigDecimal(100));
|
||||
BigDecimal averageUnitPrice = customIds.isEmpty() ? BigDecimal.ZERO : orderMoney.divide(new BigDecimal(customIds.size()), 4, RoundingMode.HALF_UP);
|
||||
PlayClerkRankingInfoEntity rankingInfo = new PlayClerkRankingInfoEntity();
|
||||
@@ -173,51 +170,4 @@ public class OrderRankingSettlementTask {
|
||||
return rankingInfo;
|
||||
}
|
||||
|
||||
private ContinuationMetrics calculateContinuationMetrics(List<PlayOrderInfoEntity> orders) {
|
||||
List<PlayOrderInfoEntity> completedOrders = orders.stream()
|
||||
.filter(order -> OrderConstant.OrderStatus.COMPLETED.getCode().equals(order.getOrderStatus()))
|
||||
.collect(Collectors.toList());
|
||||
if (completedOrders.isEmpty()) {
|
||||
return new ContinuationMetrics(0, BigDecimal.ZERO);
|
||||
}
|
||||
int continuedCount = 0;
|
||||
BigDecimal continuedMoney = BigDecimal.ZERO;
|
||||
for (PlayOrderInfoEntity order : completedOrders) {
|
||||
String customerId = order.getPurchaserBy();
|
||||
if (StrUtil.isBlank(customerId)) {
|
||||
throw new CustomException("订单缺少顾客信息,无法统计续单");
|
||||
}
|
||||
OrderConstant.OrderRelationType relationType = normalizeRelationType(order);
|
||||
if (relationType == OrderConstant.OrderRelationType.CONTINUED) {
|
||||
continuedCount++;
|
||||
continuedMoney = continuedMoney.add(order.getOrderMoney());
|
||||
}
|
||||
}
|
||||
return new ContinuationMetrics(continuedCount, continuedMoney);
|
||||
}
|
||||
|
||||
private OrderConstant.OrderRelationType normalizeRelationType(PlayOrderInfoEntity order) {
|
||||
OrderConstant.OrderRelationType relationType = order.getOrderRelationType();
|
||||
if (relationType == null) {
|
||||
throw new CustomException("订单关系类型不能为空");
|
||||
}
|
||||
if (OrderConstant.PlaceType.RANDOM.getCode().equals(order.getPlaceType())) {
|
||||
return OrderConstant.OrderRelationType.FIRST;
|
||||
}
|
||||
if (relationType == OrderConstant.OrderRelationType.NEUTRAL) {
|
||||
return OrderConstant.OrderRelationType.FIRST;
|
||||
}
|
||||
return relationType;
|
||||
}
|
||||
|
||||
private static final class ContinuationMetrics {
|
||||
private final int count;
|
||||
private final BigDecimal money;
|
||||
|
||||
private ContinuationMetrics(int count, BigDecimal money) {
|
||||
this.count = count;
|
||||
this.money = money;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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> {
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -1,86 +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)
|
||||
.orderRelationType(OrderConstant.OrderRelationType.UNASSIGNED)
|
||||
.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;
|
||||
}
|
||||
}
|
||||
@@ -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("盲盒奖池库存不足,请稍后再试");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -3,10 +3,6 @@ package com.starry.admin.modules.clerk.controller;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.starry.admin.modules.clerk.module.entity.PlayClerkPkEntity;
|
||||
import com.starry.admin.modules.clerk.service.IPlayClerkPkService;
|
||||
import com.starry.admin.modules.pk.dto.PkScoreBoardDto;
|
||||
import com.starry.admin.modules.pk.dto.PlayClerkPkForceStartRequest;
|
||||
import com.starry.admin.modules.pk.service.ClerkPkLifecycleService;
|
||||
import com.starry.admin.modules.pk.service.IPkScoreboardService;
|
||||
import com.starry.common.annotation.Log;
|
||||
import com.starry.common.enums.BusinessType;
|
||||
import com.starry.common.result.R;
|
||||
@@ -17,13 +13,7 @@ import io.swagger.annotations.ApiParam;
|
||||
import io.swagger.annotations.ApiResponse;
|
||||
import io.swagger.annotations.ApiResponses;
|
||||
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.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
/**
|
||||
* 店员pkController
|
||||
@@ -38,12 +28,6 @@ public class PlayClerkPkController {
|
||||
@Resource
|
||||
private IPlayClerkPkService playClerkPkService;
|
||||
|
||||
@Resource
|
||||
private IPkScoreboardService pkScoreboardService;
|
||||
|
||||
@Resource
|
||||
private ClerkPkLifecycleService clerkPkLifecycleService;
|
||||
|
||||
/**
|
||||
* 查询店员pk列表
|
||||
*/
|
||||
@@ -67,52 +51,6 @@ public class PlayClerkPkController {
|
||||
return R.ok(playClerkPkService.selectPlayClerkPkById(id));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取店员PK实时比分
|
||||
*/
|
||||
@ApiOperation(value = "获取PK实时比分", notes = "根据ID获取店员PK当前比分")
|
||||
@ApiImplicitParam(name = "id", value = "PK记录ID", required = true, paramType = "path", dataType = "String", example = "1")
|
||||
@ApiResponses({@ApiResponse(code = 200, message = "操作成功", response = PkScoreBoardDto.class)})
|
||||
@GetMapping(value = "/{id}/scoreboard")
|
||||
public R getScoreboard(@PathVariable("id") String id) {
|
||||
PkScoreBoardDto scoreboard = pkScoreboardService.getScoreboard(id);
|
||||
return R.ok(scoreboard);
|
||||
}
|
||||
|
||||
/**
|
||||
* 手动开始PK
|
||||
*/
|
||||
@ApiOperation(value = "开始PK", notes = "将指定PK从待开始状态切换为进行中")
|
||||
@ApiImplicitParam(name = "id", value = "PK记录ID", required = true, paramType = "path", dataType = "String", example = "1")
|
||||
@PostMapping(value = "/{id}/start")
|
||||
public R startPk(@PathVariable("id") String id) {
|
||||
clerkPkLifecycleService.startPk(id);
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
/**
|
||||
* 手动结束并结算PK
|
||||
*/
|
||||
@ApiOperation(value = "结束PK并结算", notes = "将指定PK标记为已完成,并写入最终比分和胜者信息")
|
||||
@ApiImplicitParam(name = "id", value = "PK记录ID", required = true, paramType = "path", dataType = "String", example = "1")
|
||||
@PostMapping(value = "/{id}/finish")
|
||||
public R finishPk(@PathVariable("id") String id) {
|
||||
clerkPkLifecycleService.finishPk(id);
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
/**
|
||||
* 强制开始PK(无需排期,便于人工触发)
|
||||
*/
|
||||
@ApiOperation(value = "强制开始PK", notes = "人工触发PK开始并直接进入进行中状态")
|
||||
@ApiResponses({@ApiResponse(code = 200, message = "操作成功")})
|
||||
@Log(title = "店员pk", businessType = BusinessType.INSERT)
|
||||
@PostMapping(value = "/force-start")
|
||||
public R forceStart(@ApiParam(value = "强制开始请求", required = true)
|
||||
@RequestBody PlayClerkPkForceStartRequest request) {
|
||||
return R.ok(clerkPkLifecycleService.forceStart(request));
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增店员pk
|
||||
*/
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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> {
|
||||
|
||||
}
|
||||
@@ -1,12 +1,7 @@
|
||||
package com.starry.admin.modules.clerk.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.InterceptorIgnore;
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.starry.admin.modules.clerk.module.entity.PlayClerkPkEntity;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
|
||||
/**
|
||||
* 店员pkMapper接口
|
||||
@@ -16,47 +11,4 @@ import org.apache.ibatis.annotations.Select;
|
||||
*/
|
||||
public interface PlayClerkPkMapper extends BaseMapper<PlayClerkPkEntity> {
|
||||
|
||||
@InterceptorIgnore(tenantLine = "1")
|
||||
@Select("SELECT * FROM play_clerk_pk "
|
||||
+ "WHERE status = #{status} "
|
||||
+ "AND pk_begin_time >= #{beginTime} "
|
||||
+ "AND pk_begin_time <= #{endTime}")
|
||||
List<PlayClerkPkEntity> selectUpcomingByStatus(
|
||||
@Param("status") String status,
|
||||
@Param("beginTime") Date beginTime,
|
||||
@Param("endTime") Date endTime);
|
||||
|
||||
@Select("<script>"
|
||||
+ "SELECT * FROM play_clerk_pk "
|
||||
+ "WHERE tenant_id = #{tenantId} "
|
||||
+ " AND status = #{status} "
|
||||
+ " AND ("
|
||||
+ " (clerk_a = #{clerkAId} AND clerk_b = #{clerkBId}) "
|
||||
+ " OR (clerk_a = #{clerkBId} AND clerk_b = #{clerkAId})"
|
||||
+ " ) "
|
||||
+ "ORDER BY pk_begin_time DESC "
|
||||
+ "LIMIT #{limit}"
|
||||
+ "</script>")
|
||||
List<PlayClerkPkEntity> selectRecentFinishedBetweenClerks(
|
||||
@Param("tenantId") String tenantId,
|
||||
@Param("clerkAId") String clerkAId,
|
||||
@Param("clerkBId") String clerkBId,
|
||||
@Param("status") String status,
|
||||
@Param("limit") int limit);
|
||||
|
||||
@Select("<script>"
|
||||
+ "SELECT * FROM play_clerk_pk "
|
||||
+ "WHERE tenant_id = #{tenantId} "
|
||||
+ " AND status = #{status} "
|
||||
+ " AND pk_begin_time >= #{beginTime} "
|
||||
+ " AND (clerk_a = #{clerkId} OR clerk_b = #{clerkId}) "
|
||||
+ "ORDER BY pk_begin_time ASC "
|
||||
+ "LIMIT #{limit}"
|
||||
+ "</script>")
|
||||
List<PlayClerkPkEntity> selectUpcomingForClerk(
|
||||
@Param("tenantId") String tenantId,
|
||||
@Param("clerkId") String clerkId,
|
||||
@Param("status") String status,
|
||||
@Param("beginTime") Date beginTime,
|
||||
@Param("limit") int limit);
|
||||
}
|
||||
|
||||
@@ -1,11 +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.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
|
||||
/**
|
||||
* 店员Mapper接口
|
||||
@@ -15,11 +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();
|
||||
|
||||
@InterceptorIgnore(tenantLine = "true")
|
||||
@Select("SELECT id, tenant_id, deleted FROM play_clerk_user_info WHERE id = #{id} LIMIT 1")
|
||||
PlayClerkUserInfoEntity selectByIdIncludingDeleted(@Param("id") String id);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -87,39 +87,4 @@ public class PlayClerkPkEntity extends BaseEntity<PlayClerkPkEntity> {
|
||||
*/
|
||||
private String status;
|
||||
|
||||
/**
|
||||
* 排期设置ID
|
||||
*/
|
||||
private String settingId;
|
||||
|
||||
/**
|
||||
* 店员A得分
|
||||
*/
|
||||
private java.math.BigDecimal clerkAScore;
|
||||
|
||||
/**
|
||||
* 店员B得分
|
||||
*/
|
||||
private java.math.BigDecimal clerkBScore;
|
||||
|
||||
/**
|
||||
* 店员A订单数
|
||||
*/
|
||||
private Integer clerkAOrderCount;
|
||||
|
||||
/**
|
||||
* 店员B订单数
|
||||
*/
|
||||
private Integer clerkBOrderCount;
|
||||
|
||||
/**
|
||||
* 获胜店员ID
|
||||
*/
|
||||
private String winnerClerkId;
|
||||
|
||||
/**
|
||||
* 是否已结算(1:是;0:否)
|
||||
*/
|
||||
private Integer settled;
|
||||
|
||||
}
|
||||
|
||||
@@ -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<>();
|
||||
|
||||
/**
|
||||
* 个性签名
|
||||
*/
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
/**
|
||||
* 审核内容
|
||||
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
|
||||
@@ -68,6 +68,4 @@ public class PlayClerkLevelEditVo {
|
||||
@ApiModelProperty(value = "样式图片URL", example = "https://example.com/style.jpg", notes = "等级样式图片URL")
|
||||
private String styleImageUrl;
|
||||
|
||||
private Long orderNumber;
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -3,10 +3,6 @@ package com.starry.admin.modules.clerk.service;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.starry.admin.modules.clerk.module.entity.PlayClerkPkEntity;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* 店员pkService接口
|
||||
@@ -68,15 +64,4 @@ public interface IPlayClerkPkService extends IService<PlayClerkPkEntity> {
|
||||
* @return 结果
|
||||
*/
|
||||
int deletePlayClerkPkById(String id);
|
||||
|
||||
/**
|
||||
* 查询某个店员在指定时间是否存在进行中的 PK。
|
||||
*
|
||||
* @param clerkId 店员ID
|
||||
* @param occurredAt 发生时间
|
||||
* @return 存在则返回 PK 记录,否则返回空
|
||||
*/
|
||||
Optional<PlayClerkPkEntity> findActivePkForClerk(String clerkId, LocalDateTime occurredAt);
|
||||
|
||||
List<PlayClerkPkEntity> selectUpcomingForClerk(String tenantId, String clerkId, Date beginTime, int limit);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 中现在可能是媒资 ID(mediaId)或历史 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -7,25 +7,16 @@ 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.starry.admin.common.PageBuilder;
|
||||
import com.starry.admin.common.exception.CustomException;
|
||||
import com.starry.admin.modules.clerk.mapper.PlayClerkPkMapper;
|
||||
import com.starry.admin.modules.clerk.module.entity.ClerkPkEnum;
|
||||
import com.starry.admin.modules.clerk.module.entity.PlayClerkPkEntity;
|
||||
import com.starry.admin.modules.clerk.module.entity.PlayClerkUserInfoEntity;
|
||||
import com.starry.admin.modules.clerk.service.IPlayClerkPkService;
|
||||
import com.starry.admin.modules.clerk.service.IPlayClerkUserInfoService;
|
||||
import com.starry.admin.modules.pk.enums.PkLifecycleErrorCode;
|
||||
import com.starry.admin.modules.pk.redis.PkRedisKeyConstants;
|
||||
import com.starry.common.utils.IdUtils;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneId;
|
||||
import java.util.Arrays;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import javax.annotation.Resource;
|
||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
@@ -42,8 +33,6 @@ public class PlayClerkPkServiceImpl extends ServiceImpl<PlayClerkPkMapper, PlayC
|
||||
private PlayClerkPkMapper playClerkPkMapper;
|
||||
@Resource
|
||||
private IPlayClerkUserInfoService clerkUserInfoService;
|
||||
@Resource
|
||||
private StringRedisTemplate stringRedisTemplate;
|
||||
|
||||
/**
|
||||
* 查询店员pk
|
||||
@@ -66,15 +55,8 @@ public class PlayClerkPkServiceImpl extends ServiceImpl<PlayClerkPkMapper, PlayC
|
||||
*/
|
||||
@Override
|
||||
public IPage<PlayClerkPkEntity> selectPlayClerkPkByPage(PlayClerkPkEntity playClerkPk) {
|
||||
Page<PlayClerkPkEntity> page = PageBuilder.build();
|
||||
LambdaQueryWrapper<PlayClerkPkEntity> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(StrUtil.isNotBlank(playClerkPk.getStatus()), PlayClerkPkEntity::getStatus, playClerkPk.getStatus());
|
||||
wrapper.eq(StrUtil.isNotBlank(playClerkPk.getClerkA()), PlayClerkPkEntity::getClerkA, playClerkPk.getClerkA());
|
||||
wrapper.eq(StrUtil.isNotBlank(playClerkPk.getClerkB()), PlayClerkPkEntity::getClerkB, playClerkPk.getClerkB());
|
||||
wrapper.eq(StrUtil.isNotBlank(playClerkPk.getSettingId()), PlayClerkPkEntity::getSettingId,
|
||||
playClerkPk.getSettingId());
|
||||
wrapper.orderByDesc(PlayClerkPkEntity::getPkBeginTime);
|
||||
return this.baseMapper.selectPage(page, wrapper);
|
||||
Page<PlayClerkPkEntity> page = new Page<>(1, 10);
|
||||
return this.baseMapper.selectPage(page, new LambdaQueryWrapper<>());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -116,11 +98,7 @@ public class PlayClerkPkServiceImpl extends ServiceImpl<PlayClerkPkMapper, PlayC
|
||||
}
|
||||
|
||||
playClerkPk.setStatus(ClerkPkEnum.TO_BE_STARTED.name());
|
||||
boolean saved = save(playClerkPk);
|
||||
if (saved) {
|
||||
scheduleStart(playClerkPk);
|
||||
}
|
||||
return saved;
|
||||
return save(playClerkPk);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -158,49 +136,4 @@ public class PlayClerkPkServiceImpl extends ServiceImpl<PlayClerkPkMapper, PlayC
|
||||
public int deletePlayClerkPkById(String id) {
|
||||
return playClerkPkMapper.deleteById(id);
|
||||
}
|
||||
|
||||
private void scheduleStart(PlayClerkPkEntity pk) {
|
||||
if (pk == null || pk.getPkBeginTime() == null || pk.getId() == null) {
|
||||
throw new CustomException(PkLifecycleErrorCode.REQUEST_INVALID.getMessage());
|
||||
}
|
||||
if (StrUtil.isBlank(pk.getTenantId())) {
|
||||
throw new CustomException(PkLifecycleErrorCode.TENANT_MISSING.getMessage());
|
||||
}
|
||||
String scheduleKey = PkRedisKeyConstants.startScheduleKey(pk.getTenantId());
|
||||
long startEpochSeconds = pk.getPkBeginTime().toInstant().getEpochSecond();
|
||||
stringRedisTemplate.opsForZSet().add(scheduleKey, pk.getId(), startEpochSeconds);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<PlayClerkPkEntity> findActivePkForClerk(String clerkId, LocalDateTime occurredAt) {
|
||||
if (StrUtil.isBlank(clerkId) || occurredAt == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
Date eventTime =
|
||||
Date.from(occurredAt.atZone(ZoneId.systemDefault()).toInstant());
|
||||
LambdaQueryWrapper<PlayClerkPkEntity> wrapper = Wrappers.lambdaQuery(PlayClerkPkEntity.class)
|
||||
.in(PlayClerkPkEntity::getStatus,
|
||||
Arrays.asList(ClerkPkEnum.TO_BE_STARTED.name(), ClerkPkEnum.IN_PROGRESS.name()))
|
||||
.and(w -> w.eq(PlayClerkPkEntity::getClerkA, clerkId)
|
||||
.or()
|
||||
.eq(PlayClerkPkEntity::getClerkB, clerkId))
|
||||
.le(PlayClerkPkEntity::getPkBeginTime, eventTime)
|
||||
.ge(PlayClerkPkEntity::getPkEndTime, eventTime);
|
||||
PlayClerkPkEntity entity = this.getOne(wrapper, false);
|
||||
return Optional.ofNullable(entity);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<PlayClerkPkEntity> selectUpcomingForClerk(String tenantId, String clerkId, Date beginTime,
|
||||
int limit) {
|
||||
if (StrUtil.isBlank(tenantId) || StrUtil.isBlank(clerkId) || beginTime == null || limit <= 0) {
|
||||
throw new CustomException(PkLifecycleErrorCode.REQUEST_INVALID.getMessage());
|
||||
}
|
||||
return playClerkPkMapper.selectUpcomingForClerk(
|
||||
tenantId,
|
||||
clerkId,
|
||||
ClerkPkEnum.TO_BE_STARTED.name(),
|
||||
beginTime,
|
||||
limit);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,46 +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.constant.OrderConstant;
|
||||
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;
|
||||
@@ -52,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;
|
||||
@@ -64,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;
|
||||
@@ -82,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
|
||||
@@ -94,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;
|
||||
@@ -146,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);
|
||||
|
||||
}
|
||||
|
||||
@@ -171,7 +134,8 @@ public class PlayClerkUserInfoServiceImpl extends ServiceImpl<PlayClerkUserInfoM
|
||||
/**
|
||||
* 查询店员
|
||||
*
|
||||
* @param id 店员主键
|
||||
* @param id
|
||||
* 店员主键
|
||||
* @return 店员
|
||||
*/
|
||||
@Override
|
||||
@@ -186,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("你已被下架,没有权限访问");
|
||||
}
|
||||
@@ -221,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();
|
||||
@@ -293,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
|
||||
@@ -314,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())) {
|
||||
@@ -345,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);
|
||||
|
||||
}
|
||||
@@ -387,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);
|
||||
@@ -399,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
|
||||
@@ -457,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());
|
||||
}
|
||||
@@ -502,26 +406,19 @@ 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");
|
||||
int orderContinueNumber = 0;
|
||||
int orderNumber = 0;
|
||||
for (PlayOrderInfoEntity orderInfo : playOrderInfoService.queryBySettlementOrder(record.getId(), "")) {
|
||||
OrderConstant.OrderRelationType relationType = orderInfo.getOrderRelationType();
|
||||
if (relationType == null) {
|
||||
throw new CustomException("订单关系类型不能为空");
|
||||
}
|
||||
if (OrderConstant.PlaceType.RANDOM.getCode().equals(orderInfo.getPlaceType())) {
|
||||
relationType = OrderConstant.OrderRelationType.FIRST;
|
||||
}
|
||||
if (relationType == OrderConstant.OrderRelationType.NEUTRAL) {
|
||||
relationType = OrderConstant.OrderRelationType.FIRST;
|
||||
}
|
||||
if (relationType == OrderConstant.OrderRelationType.CONTINUED) {
|
||||
if ("0".equals(orderInfo.getFirstOrder())) {
|
||||
orderContinueNumber++;
|
||||
}
|
||||
orderNumber++;
|
||||
@@ -534,7 +431,6 @@ public class PlayClerkUserInfoServiceImpl extends ServiceImpl<PlayClerkUserInfoM
|
||||
record.setOrderContinueNumber(String.valueOf(orderContinueNumber));
|
||||
}
|
||||
|
||||
attachMediaToAdminVos(page.getRecords());
|
||||
return page;
|
||||
}
|
||||
|
||||
@@ -546,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");
|
||||
@@ -559,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
|
||||
@@ -603,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
|
||||
@@ -634,7 +499,8 @@ public class PlayClerkUserInfoServiceImpl extends ServiceImpl<PlayClerkUserInfoM
|
||||
/**
|
||||
* 删除店员信息
|
||||
*
|
||||
* @param id 店员主键
|
||||
* @param id
|
||||
* 店员主键
|
||||
* @return 结果
|
||||
*/
|
||||
@Override
|
||||
@@ -648,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();
|
||||
@@ -669,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;
|
||||
@@ -679,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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
/**
|
||||
* 批量删除顾客和礼物关系
|
||||
*
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -172,11 +172,4 @@ public interface IPlayCustomUserInfoService extends IService<PlayCustomUserInfoE
|
||||
* @author admin
|
||||
**/
|
||||
void saveOrderInfo(PlayOrderInfoEntity entity);
|
||||
|
||||
/**
|
||||
* 处理订单完成后的顾客统计更新。
|
||||
*
|
||||
* @param entity 完成的订单实体
|
||||
*/
|
||||
void handleOrderCompletion(PlayOrderInfoEntity entity);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量删除顾客和礼物关系
|
||||
*
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,17 +16,13 @@ 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.constant.OrderConstant;
|
||||
import com.starry.admin.modules.order.module.entity.PlayOrderInfoEntity;
|
||||
import com.starry.admin.modules.order.service.impl.PlayOrderInfoServiceImpl;
|
||||
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;
|
||||
@@ -53,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<>();
|
||||
@@ -170,17 +163,7 @@ public class PlayCustomUserInfoServiceImpl extends ServiceImpl<PlayCustomUserInf
|
||||
if (orderInfo.getId() == null) {
|
||||
continue;
|
||||
}
|
||||
OrderConstant.OrderRelationType relationType = orderInfo.getOrderRelationType();
|
||||
if (relationType == null) {
|
||||
throw new CustomException("订单关系类型不能为空");
|
||||
}
|
||||
if (OrderConstant.PlaceType.RANDOM.getCode().equals(orderInfo.getPlaceType())) {
|
||||
relationType = OrderConstant.OrderRelationType.FIRST;
|
||||
}
|
||||
if (relationType == OrderConstant.OrderRelationType.NEUTRAL) {
|
||||
relationType = OrderConstant.OrderRelationType.FIRST;
|
||||
}
|
||||
if (relationType == OrderConstant.OrderRelationType.CONTINUED) {
|
||||
if ("0".equals(orderInfo.getFirstOrder())) {
|
||||
orderContinueNumber++;
|
||||
}
|
||||
orderNumber++;
|
||||
@@ -386,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();
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user