Compare commits
41 Commits
d7d7c64c01
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
036e8156d5 | ||
|
|
6497788b64 | ||
|
|
132ac8796c | ||
|
|
f2a7039a41 | ||
|
|
21bbd0386d | ||
|
|
e683ef6863 | ||
|
|
086aa47226 | ||
|
|
8558d203af | ||
|
|
69909a3b83 | ||
|
|
d7754a66af | ||
|
|
dbf1832f75 | ||
|
|
e10b7bd3be | ||
|
|
5331fd75a2 | ||
|
|
5c0de2201c | ||
|
|
29f168dd67 | ||
|
|
48348609a8 | ||
|
|
25554bac84 | ||
|
|
cec5e965f6 | ||
|
|
4cd2950051 | ||
|
|
cc76710858 | ||
|
|
b51aac0cfa | ||
|
|
ee0fc4d1f6 | ||
|
|
9d20040574 | ||
|
|
2f807a2796 | ||
|
|
49867a30dd | ||
|
|
51c4a5438d | ||
|
|
e616dd6a13 | ||
|
|
ed0edf584a | ||
|
|
b9250566fb | ||
|
|
7b6943d391 | ||
|
|
984e33bd94 | ||
|
|
4fdcf6ddbd | ||
|
|
7d07e32271 | ||
|
|
438aef7af7 | ||
|
|
eaee5f5aa6 | ||
|
|
51ec9dd85b | ||
|
|
9868fb1bb9 | ||
|
|
3df1267272 | ||
|
|
5c3fa1e33f | ||
|
|
15f058617a | ||
|
|
29ff0a2637 |
28
README.md
28
README.md
@@ -134,6 +134,34 @@ mvn spotless:apply compile
|
|||||||
mvn spotless:apply checkstyle:check 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 集成测试指南
|
## API 集成测试指南
|
||||||
|
|
||||||
在 `play-admin` 模块内提供了基于 `apitest` Profile 的端到端测试套件。为了稳定跑通所有 API 场景,请按以下步骤准备环境:
|
在 `play-admin` 模块内提供了基于 `apitest` Profile 的端到端测试套件。为了稳定跑通所有 API 场景,请按以下步骤准备环境:
|
||||||
|
|||||||
18
backup-dev-db.sh
Executable file
18
backup-dev-db.sh
Executable file
@@ -0,0 +1,18 @@
|
|||||||
|
#!/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"
|
||||||
@@ -2,7 +2,7 @@ version: "3.9"
|
|||||||
|
|
||||||
services:
|
services:
|
||||||
mysql-apitest:
|
mysql-apitest:
|
||||||
image: mysql:8.0.32
|
image: mysql:8.0.24
|
||||||
container_name: peipei-mysql-apitest
|
container_name: peipei-mysql-apitest
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
environment:
|
environment:
|
||||||
|
|||||||
32
docker/docker-compose-apitest.yml
Normal file
32
docker/docker-compose-apitest.yml
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
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"
|
||||||
92
flyway.sh
Executable file
92
flyway.sh
Executable file
@@ -0,0 +1,92 @@
|
|||||||
|
#!/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"
|
||||||
10
flyway/api-test.conf
Normal file
10
flyway/api-test.conf
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
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,7 +1,7 @@
|
|||||||
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.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.user=root
|
||||||
flyway.password=KdaKRZ2trpdhNePa
|
flyway.password=KdaKRZ2trpdhNePa
|
||||||
lyway.locations=classpath:db/migration
|
flyway.locations=classpath:db/migration
|
||||||
flyway.table=admin_flyway_schema_history
|
flyway.table=admin_flyway_schema_history
|
||||||
flyway.baselineOnMigrate=true
|
flyway.baselineOnMigrate=true
|
||||||
flyway.baselineVersion=1
|
flyway.baselineVersion=1
|
||||||
10
flyway/staging.conf
Normal file
10
flyway/staging.conf
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
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
|
||||||
24
media-migration-to-test.md
Normal file
24
media-migration-to-test.md
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
## 媒資/相簿相容性:手動驗證清單
|
||||||
|
|
||||||
|
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` 不會被清除,舊客端仍能看到舊資料。
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -162,7 +162,7 @@ public class BlindBoxPoolAdminService {
|
|||||||
if (!tenantId.equals(config.getTenantId())) {
|
if (!tenantId.equals(config.getTenantId())) {
|
||||||
throw new CustomException("盲盒不存在或已被移除");
|
throw new CustomException("盲盒不存在或已被移除");
|
||||||
}
|
}
|
||||||
PlayGiftInfoEntity rewardGift = requireRewardGift(tenantId, request.getRewardGiftId());
|
PlayGiftInfoEntity rewardGift = requireRewardGiftForUpdate(tenantId, request.getRewardGiftId());
|
||||||
validateTimeRange(request.getValidFrom(), request.getValidTo());
|
validateTimeRange(request.getValidFrom(), request.getValidTo());
|
||||||
Integer weight = requirePositiveWeight(request.getWeight(), rewardGift.getName());
|
Integer weight = requirePositiveWeight(request.getWeight(), rewardGift.getName());
|
||||||
Integer remainingStock = normalizeRemainingStock(request.getRemainingStock(), rewardGift.getName());
|
Integer remainingStock = normalizeRemainingStock(request.getRemainingStock(), rewardGift.getName());
|
||||||
@@ -326,18 +326,30 @@ public class BlindBoxPoolAdminService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private PlayGiftInfoEntity requireRewardGift(String tenantId, String rewardGiftId) {
|
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)) {
|
if (StrUtil.isBlank(rewardGiftId)) {
|
||||||
throw new CustomException("请选择中奖礼物");
|
throw new CustomException("请选择中奖礼物");
|
||||||
}
|
}
|
||||||
PlayGiftInfoEntity gift = playGiftInfoMapper.selectById(rewardGiftId);
|
PlayGiftInfoEntity gift = playGiftInfoMapper.selectById(rewardGiftId);
|
||||||
if (gift == null
|
if (gift == null
|
||||||
|| !tenantId.equals(gift.getTenantId())
|
|| !tenantId.equals(gift.getTenantId())
|
||||||
|| !GiftHistory.CURRENT.getCode().equals(gift.getHistory())
|
|
||||||
|| !GiftState.ACTIVE.getCode().equals(gift.getState())
|
|
||||||
|| !GiftType.NORMAL.getCode().equals(gift.getType())
|
|| !GiftType.NORMAL.getCode().equals(gift.getType())
|
||||||
|| Boolean.TRUE.equals(gift.getDeleted())) {
|
|| Boolean.TRUE.equals(gift.getDeleted())) {
|
||||||
throw new CustomException("中奖礼物不存在或已下架");
|
throw new CustomException("中奖礼物不存在或已下架");
|
||||||
}
|
}
|
||||||
|
if (strictAvailability) {
|
||||||
|
if (!GiftHistory.CURRENT.getCode().equals(gift.getHistory())
|
||||||
|
|| !GiftState.ACTIVE.getCode().equals(gift.getState())) {
|
||||||
|
throw new CustomException("中奖礼物不存在或已下架");
|
||||||
|
}
|
||||||
|
}
|
||||||
return gift;
|
return gift;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
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,7 +1,10 @@
|
|||||||
package com.starry.admin.modules.clerk.mapper;
|
package com.starry.admin.modules.clerk.mapper;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.InterceptorIgnore;
|
||||||
import com.github.yulichang.base.MPJBaseMapper;
|
import com.github.yulichang.base.MPJBaseMapper;
|
||||||
import com.starry.admin.modules.clerk.module.entity.PlayClerkUserInfoEntity;
|
import com.starry.admin.modules.clerk.module.entity.PlayClerkUserInfoEntity;
|
||||||
|
import java.util.List;
|
||||||
|
import org.apache.ibatis.annotations.Select;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 店员Mapper接口
|
* 店员Mapper接口
|
||||||
@@ -11,4 +14,7 @@ import com.starry.admin.modules.clerk.module.entity.PlayClerkUserInfoEntity;
|
|||||||
*/
|
*/
|
||||||
public interface PlayClerkUserInfoMapper extends MPJBaseMapper<PlayClerkUserInfoEntity> {
|
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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ package com.starry.admin.modules.clerk.module.entity;
|
|||||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||||
import com.starry.admin.modules.order.module.entity.PlayOrderInfoEntity;
|
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.ApiModel;
|
||||||
import io.swagger.annotations.ApiModelProperty;
|
import io.swagger.annotations.ApiModelProperty;
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
@@ -94,6 +95,12 @@ public class PlayClerkUserReturnVo {
|
|||||||
@ApiModelProperty(value = "相册列表", notes = "店员相册图片URL列表")
|
@ApiModelProperty(value = "相册列表", notes = "店员相册图片URL列表")
|
||||||
private List<String> album = new ArrayList<>();
|
private List<String> album = new ArrayList<>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 媒资列表
|
||||||
|
*/
|
||||||
|
@ApiModelProperty(value = "媒资列表", notes = "结构化媒资数据")
|
||||||
|
private List<MediaVo> mediaList = new ArrayList<>();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 个性签名
|
* 个性签名
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -60,6 +60,15 @@ public class PlayClerkDataReviewReturnVo {
|
|||||||
@ApiModelProperty(value = "资料内容", example = "[\"https://example.com/photo1.jpg\"]", notes = "资料内容,根据资料类型有不同格式")
|
@ApiModelProperty(value = "资料内容", example = "[\"https://example.com/photo1.jpg\"]", notes = "资料内容,根据资料类型有不同格式")
|
||||||
private List<String> dataContent;
|
private List<String> dataContent;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 媒资对应的视频地址(仅当资料类型为头像/相册且为视频时有值,顺序与 dataContent 一一对应)
|
||||||
|
*/
|
||||||
|
@ApiModelProperty(
|
||||||
|
value = "媒资视频地址列表",
|
||||||
|
example = "[\"https://example.com/video1.mp4\"]",
|
||||||
|
notes = "仅当资料类型为头像/相册且为视频时有值,顺序与 dataContent 一一对应")
|
||||||
|
private List<String> mediaVideoUrls;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 审核状态(0:未审核:1:审核通过,2:审核不通过)
|
* 审核状态(0:未审核:1:审核通过,2:审核不通过)
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
@@ -190,6 +190,17 @@ public interface IPlayClerkUserInfoService extends IService<PlayClerkUserInfoEnt
|
|||||||
*/
|
*/
|
||||||
IPage<PlayClerkUserInfoResultVo> selectPlayClerkUserInfoByPage(PlayClerkUserInfoQueryVo vo);
|
IPage<PlayClerkUserInfoResultVo> selectPlayClerkUserInfoByPage(PlayClerkUserInfoQueryVo vo);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建面向顾客的店员详情视图对象(包含媒资与兼容相册)。
|
||||||
|
*
|
||||||
|
* @param clerkId
|
||||||
|
* 店员ID
|
||||||
|
* @param customUserId
|
||||||
|
* 顾客ID(可为空,用于标记关注状态)
|
||||||
|
* @return 店员详情视图对象
|
||||||
|
*/
|
||||||
|
PlayClerkUserInfoResultVo buildCustomerDetail(String clerkId, String customUserId);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 确认店员处于可用状态,否则抛出异常
|
* 确认店员处于可用状态,否则抛出异常
|
||||||
*
|
*
|
||||||
@@ -252,5 +263,12 @@ public interface IPlayClerkUserInfoService extends IService<PlayClerkUserInfoEnt
|
|||||||
|
|
||||||
List<PlayClerkUserInfoEntity> simpleList();
|
List<PlayClerkUserInfoEntity> simpleList();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询存在相册字段数据的店员(忽略租户隔离)
|
||||||
|
*
|
||||||
|
* @return 店员集合
|
||||||
|
*/
|
||||||
|
List<PlayClerkUserInfoEntity> listWithAlbumIgnoringTenant();
|
||||||
|
|
||||||
JSONObject getPcData(PlayClerkUserInfoEntity entity);
|
JSONObject getPcData(PlayClerkUserInfoEntity entity);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.starry.admin.modules.clerk.service.impl;
|
package com.starry.admin.modules.clerk.service.impl;
|
||||||
|
|
||||||
|
import cn.hutool.core.collection.CollectionUtil;
|
||||||
import cn.hutool.core.util.StrUtil;
|
import cn.hutool.core.util.StrUtil;
|
||||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||||
@@ -7,20 +8,33 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
|||||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||||
import com.github.yulichang.wrapper.MPJLambdaWrapper;
|
import com.github.yulichang.wrapper.MPJLambdaWrapper;
|
||||||
import com.starry.admin.common.exception.CustomException;
|
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.mapper.PlayClerkDataReviewInfoMapper;
|
||||||
import com.starry.admin.modules.clerk.module.entity.PlayClerkDataReviewInfoEntity;
|
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.PlayClerkUserInfoEntity;
|
||||||
import com.starry.admin.modules.clerk.module.entity.PlayClerkUserReviewInfoEntity;
|
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.PlayClerkDataReviewQueryVo;
|
||||||
import com.starry.admin.modules.clerk.module.vo.PlayClerkDataReviewReturnVo;
|
import com.starry.admin.modules.clerk.module.vo.PlayClerkDataReviewReturnVo;
|
||||||
import com.starry.admin.modules.clerk.module.vo.PlayClerkDataReviewStateEditVo;
|
import com.starry.admin.modules.clerk.module.vo.PlayClerkDataReviewStateEditVo;
|
||||||
import com.starry.admin.modules.clerk.service.IPlayClerkDataReviewInfoService;
|
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.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.enums.ClerkReviewState;
|
||||||
import com.starry.common.utils.IdUtils;
|
import com.starry.common.utils.IdUtils;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
import javax.annotation.Resource;
|
import javax.annotation.Resource;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
@@ -42,6 +56,12 @@ public class PlayClerkDataReviewInfoServiceImpl
|
|||||||
@Resource
|
@Resource
|
||||||
private IPlayClerkUserInfoService playClerkUserInfoService;
|
private IPlayClerkUserInfoService playClerkUserInfoService;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private IPlayClerkMediaAssetService clerkMediaAssetService;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private IPlayMediaService mediaService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 查询店员资料审核
|
* 查询店员资料审核
|
||||||
*
|
*
|
||||||
@@ -107,8 +127,11 @@ public class PlayClerkDataReviewInfoServiceImpl
|
|||||||
lambdaQueryWrapper.between(PlayClerkDataReviewInfoEntity::getAddTime, vo.getAddTime().get(0),
|
lambdaQueryWrapper.between(PlayClerkDataReviewInfoEntity::getAddTime, vo.getAddTime().get(0),
|
||||||
vo.getAddTime().get(1));
|
vo.getAddTime().get(1));
|
||||||
}
|
}
|
||||||
return this.baseMapper.selectJoinPage(new Page<>(vo.getPageNum(), vo.getPageSize()),
|
IPage<PlayClerkDataReviewReturnVo> page = this.baseMapper.selectJoinPage(
|
||||||
PlayClerkDataReviewReturnVo.class, lambdaQueryWrapper);
|
new Page<>(vo.getPageNum(), vo.getPageSize()), PlayClerkDataReviewReturnVo.class,
|
||||||
|
lambdaQueryWrapper);
|
||||||
|
enrichDataContentWithMediaPreview(page);
|
||||||
|
return page;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -129,6 +152,72 @@ public class PlayClerkDataReviewInfoServiceImpl
|
|||||||
return save(playClerkDataReviewInfo);
|
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
|
@Override
|
||||||
public void updateDataReviewState(PlayClerkDataReviewStateEditVo vo) {
|
public void updateDataReviewState(PlayClerkDataReviewStateEditVo vo) {
|
||||||
PlayClerkDataReviewInfoEntity entity = this.selectPlayClerkDataReviewInfoById(vo.getId());
|
PlayClerkDataReviewInfoEntity entity = this.selectPlayClerkDataReviewInfoById(vo.getId());
|
||||||
@@ -147,7 +236,8 @@ public class PlayClerkDataReviewInfoServiceImpl
|
|||||||
userInfo.setAvatar(entity.getDataContent().get(0));
|
userInfo.setAvatar(entity.getDataContent().get(0));
|
||||||
}
|
}
|
||||||
if ("2".equals(entity.getDataType())) {
|
if ("2".equals(entity.getDataType())) {
|
||||||
userInfo.setAlbum(entity.getDataContent());
|
userInfo.setAlbum(new ArrayList<>());
|
||||||
|
synchronizeApprovedAlbumMedia(entity);
|
||||||
}
|
}
|
||||||
if ("3".equals(entity.getDataType())) {
|
if ("3".equals(entity.getDataType())) {
|
||||||
userInfo.setAudio(entity.getDataContent().get(0));
|
userInfo.setAudio(entity.getDataContent().get(0));
|
||||||
@@ -159,6 +249,71 @@ public class PlayClerkDataReviewInfoServiceImpl
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 修改店员资料审核
|
* 修改店员资料审核
|
||||||
*
|
*
|
||||||
@@ -194,4 +349,28 @@ public class PlayClerkDataReviewInfoServiceImpl
|
|||||||
public int deletePlayClerkDataReviewInfoById(String id) {
|
public int deletePlayClerkDataReviewInfoById(String id) {
|
||||||
return playClerkDataReviewInfoMapper.deleteById(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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,280 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.starry.admin.modules.clerk.service.impl;
|
package com.starry.admin.modules.clerk.service.impl;
|
||||||
|
|
||||||
|
import cn.hutool.core.collection.CollectionUtil;
|
||||||
import cn.hutool.core.util.StrUtil;
|
import cn.hutool.core.util.StrUtil;
|
||||||
import com.alibaba.fastjson2.JSONObject;
|
import com.alibaba.fastjson2.JSONObject;
|
||||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
@@ -12,10 +13,13 @@ import com.github.yulichang.wrapper.MPJLambdaWrapper;
|
|||||||
import com.starry.admin.common.component.JwtToken;
|
import com.starry.admin.common.component.JwtToken;
|
||||||
import com.starry.admin.common.domain.LoginUser;
|
import com.starry.admin.common.domain.LoginUser;
|
||||||
import com.starry.admin.common.exception.CustomException;
|
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.mapper.PlayClerkUserInfoMapper;
|
||||||
import com.starry.admin.modules.clerk.module.entity.PlayClerkCommodityEntity;
|
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.PlayClerkDataReviewInfoEntity;
|
||||||
import com.starry.admin.modules.clerk.module.entity.PlayClerkLevelInfoEntity;
|
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.PlayClerkUserInfoEntity;
|
||||||
import com.starry.admin.modules.clerk.module.entity.PlayClerkUserQueryVo;
|
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.PlayClerkUserReturnVo;
|
||||||
@@ -29,9 +33,13 @@ import com.starry.admin.modules.clerk.module.vo.PlayClerkUnsettledWagesInfoRetur
|
|||||||
import com.starry.admin.modules.clerk.service.IPlayClerkCommodityService;
|
import com.starry.admin.modules.clerk.service.IPlayClerkCommodityService;
|
||||||
import com.starry.admin.modules.clerk.service.IPlayClerkDataReviewInfoService;
|
import com.starry.admin.modules.clerk.service.IPlayClerkDataReviewInfoService;
|
||||||
import com.starry.admin.modules.clerk.service.IPlayClerkLevelInfoService;
|
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.clerk.service.IPlayClerkUserInfoService;
|
||||||
import com.starry.admin.modules.custom.entity.PlayCustomFollowInfoEntity;
|
import com.starry.admin.modules.custom.entity.PlayCustomFollowInfoEntity;
|
||||||
import com.starry.admin.modules.custom.service.IPlayCustomFollowInfoService;
|
import com.starry.admin.modules.custom.service.IPlayCustomFollowInfoService;
|
||||||
|
import com.starry.admin.modules.media.entity.PlayMediaEntity;
|
||||||
|
import com.starry.admin.modules.media.enums.MediaStatus;
|
||||||
|
import com.starry.admin.modules.media.service.IPlayMediaService;
|
||||||
import com.starry.admin.modules.order.module.entity.PlayOrderInfoEntity;
|
import com.starry.admin.modules.order.module.entity.PlayOrderInfoEntity;
|
||||||
import com.starry.admin.modules.order.service.IPlayOrderInfoService;
|
import com.starry.admin.modules.order.service.IPlayOrderInfoService;
|
||||||
import com.starry.admin.modules.personnel.module.entity.PlayPersonnelAdminInfoEntity;
|
import com.starry.admin.modules.personnel.module.entity.PlayPersonnelAdminInfoEntity;
|
||||||
@@ -43,7 +51,9 @@ import com.starry.admin.modules.personnel.service.IPlayPersonnelGroupInfoService
|
|||||||
import com.starry.admin.modules.personnel.service.IPlayPersonnelWaiterInfoService;
|
import com.starry.admin.modules.personnel.service.IPlayPersonnelWaiterInfoService;
|
||||||
import com.starry.admin.modules.statistics.module.vo.PlayClerkPerformanceInfoQueryVo;
|
import com.starry.admin.modules.statistics.module.vo.PlayClerkPerformanceInfoQueryVo;
|
||||||
import com.starry.admin.modules.system.service.LoginService;
|
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.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.PlayClerkUserInfoQueryVo;
|
||||||
import com.starry.admin.modules.weichat.entity.clerk.PlayClerkUserInfoResultVo;
|
import com.starry.admin.modules.weichat.entity.clerk.PlayClerkUserInfoResultVo;
|
||||||
import com.starry.admin.utils.SecurityUtils;
|
import com.starry.admin.utils.SecurityUtils;
|
||||||
@@ -53,7 +63,9 @@ import com.starry.common.utils.StringUtils;
|
|||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
@@ -69,9 +81,7 @@ import org.springframework.stereotype.Service;
|
|||||||
* @since 2024-03-30
|
* @since 2024-03-30
|
||||||
*/
|
*/
|
||||||
@Service
|
@Service
|
||||||
public class PlayClerkUserInfoServiceImpl extends ServiceImpl<PlayClerkUserInfoMapper, PlayClerkUserInfoEntity>
|
public class PlayClerkUserInfoServiceImpl extends ServiceImpl<PlayClerkUserInfoMapper, PlayClerkUserInfoEntity> implements IPlayClerkUserInfoService {
|
||||||
implements
|
|
||||||
IPlayClerkUserInfoService {
|
|
||||||
|
|
||||||
private static final String OFFBOARD_MESSAGE = "你已离职,需要复职请联系店铺管理员";
|
private static final String OFFBOARD_MESSAGE = "你已离职,需要复职请联系店铺管理员";
|
||||||
private static final String DELISTED_MESSAGE = "你已被下架,没有权限访问";
|
private static final String DELISTED_MESSAGE = "你已被下架,没有权限访问";
|
||||||
@@ -87,6 +97,10 @@ public class PlayClerkUserInfoServiceImpl extends ServiceImpl<PlayClerkUserInfoM
|
|||||||
@Resource
|
@Resource
|
||||||
private IPlayCustomFollowInfoService customFollowInfoService;
|
private IPlayCustomFollowInfoService customFollowInfoService;
|
||||||
@Resource
|
@Resource
|
||||||
|
private IPlayClerkMediaAssetService clerkMediaAssetService;
|
||||||
|
@Resource
|
||||||
|
private IPlayMediaService mediaService;
|
||||||
|
@Resource
|
||||||
private IPlayBalanceDetailsInfoService playBalanceDetailsInfoService;
|
private IPlayBalanceDetailsInfoService playBalanceDetailsInfoService;
|
||||||
@Resource
|
@Resource
|
||||||
private IPlayOrderInfoService playOrderInfoService;
|
private IPlayOrderInfoService playOrderInfoService;
|
||||||
@@ -131,8 +145,7 @@ public class PlayClerkUserInfoServiceImpl extends ServiceImpl<PlayClerkUserInfoM
|
|||||||
MPJLambdaWrapper<PlayClerkUserInfoEntity> lambdaWrapper = new MPJLambdaWrapper<>();
|
MPJLambdaWrapper<PlayClerkUserInfoEntity> lambdaWrapper = new MPJLambdaWrapper<>();
|
||||||
lambdaWrapper.selectAll(PlayClerkLevelInfoEntity.class);
|
lambdaWrapper.selectAll(PlayClerkLevelInfoEntity.class);
|
||||||
lambdaWrapper.selectAs(PlayClerkUserInfoEntity::getLevelId, "levelId");
|
lambdaWrapper.selectAs(PlayClerkUserInfoEntity::getLevelId, "levelId");
|
||||||
lambdaWrapper.leftJoin(PlayClerkLevelInfoEntity.class, PlayClerkLevelInfoEntity::getId,
|
lambdaWrapper.leftJoin(PlayClerkLevelInfoEntity.class, PlayClerkLevelInfoEntity::getId, PlayClerkUserInfoEntity::getLevelId);
|
||||||
PlayClerkUserInfoEntity::getLevelId);
|
|
||||||
lambdaWrapper.eq(PlayClerkUserInfoEntity::getId, clerkId);
|
lambdaWrapper.eq(PlayClerkUserInfoEntity::getId, clerkId);
|
||||||
PlayClerkLevelInfoEntity levelInfo = this.baseMapper.selectJoinOne(PlayClerkLevelInfoEntity.class, lambdaWrapper);
|
PlayClerkLevelInfoEntity levelInfo = this.baseMapper.selectJoinOne(PlayClerkLevelInfoEntity.class, lambdaWrapper);
|
||||||
if (levelInfo != null) {
|
if (levelInfo != null) {
|
||||||
@@ -157,8 +170,7 @@ public class PlayClerkUserInfoServiceImpl extends ServiceImpl<PlayClerkUserInfoM
|
|||||||
/**
|
/**
|
||||||
* 查询店员
|
* 查询店员
|
||||||
*
|
*
|
||||||
* @param id
|
* @param id 店员主键
|
||||||
* 店员主键
|
|
||||||
* @return 店员
|
* @return 店员
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
@@ -173,13 +185,9 @@ public class PlayClerkUserInfoServiceImpl extends ServiceImpl<PlayClerkUserInfoM
|
|||||||
@Override
|
@Override
|
||||||
public PlayClerkUserLoginResponseVo getVo(PlayClerkUserInfoEntity userInfo) {
|
public PlayClerkUserLoginResponseVo getVo(PlayClerkUserInfoEntity userInfo) {
|
||||||
PlayClerkUserLoginResponseVo result = ConvertUtil.entityToVo(userInfo, PlayClerkUserLoginResponseVo.class);
|
PlayClerkUserLoginResponseVo result = ConvertUtil.entityToVo(userInfo, PlayClerkUserLoginResponseVo.class);
|
||||||
List<PlayClerkDataReviewInfoEntity> pendingReviews = playClerkDataReviewInfoService
|
List<PlayClerkDataReviewInfoEntity> pendingReviews = playClerkDataReviewInfoService.queryByClerkId(userInfo.getId(), "0");
|
||||||
.queryByClerkId(userInfo.getId(), "0");
|
|
||||||
if (pendingReviews != null && !pendingReviews.isEmpty()) {
|
if (pendingReviews != null && !pendingReviews.isEmpty()) {
|
||||||
Set<String> pendingTypes = pendingReviews.stream()
|
Set<String> pendingTypes = pendingReviews.stream().map(PlayClerkDataReviewInfoEntity::getDataType).filter(StrUtil::isNotBlank).collect(Collectors.toSet());
|
||||||
.map(PlayClerkDataReviewInfoEntity::getDataType)
|
|
||||||
.filter(StrUtil::isNotBlank)
|
|
||||||
.collect(Collectors.toSet());
|
|
||||||
if (pendingTypes.contains("0")) {
|
if (pendingTypes.contains("0")) {
|
||||||
result.setNicknameAllowEdit(false);
|
result.setNicknameAllowEdit(false);
|
||||||
}
|
}
|
||||||
@@ -217,18 +225,19 @@ public class PlayClerkUserInfoServiceImpl extends ServiceImpl<PlayClerkUserInfoM
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 查询店员服务项目
|
// 查询店员服务项目
|
||||||
List<PlayClerkCommodityEntity> clerkCommodityEntities = playClerkCommodityService
|
List<PlayClerkCommodityEntity> clerkCommodityEntities = playClerkCommodityService.selectCommodityTypeByUser(userInfo.getId(), "");
|
||||||
.selectCommodityTypeByUser(userInfo.getId(), "");
|
|
||||||
List<PlayClerkCommodityQueryVo> playClerkCommodityQueryVos = new ArrayList<>();
|
List<PlayClerkCommodityQueryVo> playClerkCommodityQueryVos = new ArrayList<>();
|
||||||
for (PlayClerkCommodityEntity clerkCommodityEntity : clerkCommodityEntities) {
|
for (PlayClerkCommodityEntity clerkCommodityEntity : clerkCommodityEntities) {
|
||||||
playClerkCommodityQueryVos.add(new PlayClerkCommodityQueryVo(clerkCommodityEntity.getCommodityName(),
|
playClerkCommodityQueryVos.add(new PlayClerkCommodityQueryVo(clerkCommodityEntity.getCommodityName(), clerkCommodityEntity.getEnablingState()));
|
||||||
clerkCommodityEntity.getEnablingState()));
|
|
||||||
}
|
}
|
||||||
result.setCommodity(playClerkCommodityQueryVos);
|
result.setCommodity(playClerkCommodityQueryVos);
|
||||||
result.setArea(userInfo.getProvince() + "-" + userInfo.getCity());
|
result.setArea(userInfo.getProvince() + "-" + userInfo.getCity());
|
||||||
|
|
||||||
result.setPcData(this.getPcData(userInfo));
|
result.setPcData(this.getPcData(userInfo));
|
||||||
result.setLevelInfo(playClerkLevelInfoService.selectPlayClerkLevelInfoById(userInfo.getLevelId()));
|
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;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -265,10 +274,7 @@ public class PlayClerkUserInfoServiceImpl extends ServiceImpl<PlayClerkUserInfoM
|
|||||||
if (StrUtil.isBlank(clerkId)) {
|
if (StrUtil.isBlank(clerkId)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
LambdaUpdateWrapper<PlayClerkUserInfoEntity> wrapper = Wrappers.lambdaUpdate(PlayClerkUserInfoEntity.class)
|
LambdaUpdateWrapper<PlayClerkUserInfoEntity> wrapper = Wrappers.lambdaUpdate(PlayClerkUserInfoEntity.class).eq(PlayClerkUserInfoEntity::getId, clerkId).set(PlayClerkUserInfoEntity::getToken, "empty").set(PlayClerkUserInfoEntity::getOnlineState, "0");
|
||||||
.eq(PlayClerkUserInfoEntity::getId, clerkId)
|
|
||||||
.set(PlayClerkUserInfoEntity::getToken, "empty")
|
|
||||||
.set(PlayClerkUserInfoEntity::getOnlineState, "0");
|
|
||||||
this.baseMapper.update(null, wrapper);
|
this.baseMapper.update(null, wrapper);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -286,21 +292,17 @@ public class PlayClerkUserInfoServiceImpl extends ServiceImpl<PlayClerkUserInfoM
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void updateAccountBalanceById(String userId, BigDecimal balanceBeforeOperation,
|
public void updateAccountBalanceById(String userId, BigDecimal balanceBeforeOperation, BigDecimal balanceAfterOperation, String operationType, String operationAction, BigDecimal balanceMoney, String orderId) {
|
||||||
BigDecimal balanceAfterOperation, String operationType, String operationAction, BigDecimal balanceMoney,
|
|
||||||
String orderId) {
|
|
||||||
// 修改用户余额
|
// 修改用户余额
|
||||||
this.baseMapper.updateById(new PlayClerkUserInfoEntity(userId, balanceAfterOperation));
|
this.baseMapper.updateById(new PlayClerkUserInfoEntity(userId, balanceAfterOperation));
|
||||||
// 记录余额变更记录
|
// 记录余额变更记录
|
||||||
playBalanceDetailsInfoService.insertBalanceDetailsInfo("0", userId, balanceBeforeOperation,
|
playBalanceDetailsInfoService.insertBalanceDetailsInfo("0", userId, balanceBeforeOperation, balanceAfterOperation, operationType, operationAction, balanceMoney, BigDecimal.ZERO, orderId);
|
||||||
balanceAfterOperation, operationType, operationAction, balanceMoney, BigDecimal.ZERO, orderId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 查询店员列表
|
* 查询店员列表
|
||||||
*
|
*
|
||||||
* @param vo
|
* @param vo 店员查询对象
|
||||||
* 店员查询对象
|
|
||||||
* @return 店员
|
* @return 店员
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
@@ -311,12 +313,10 @@ public class PlayClerkUserInfoServiceImpl extends ServiceImpl<PlayClerkUserInfoM
|
|||||||
// 查询不隐藏的
|
// 查询不隐藏的
|
||||||
lambdaQueryWrapper.eq(PlayClerkUserInfoEntity::getDisplayState, "1");
|
lambdaQueryWrapper.eq(PlayClerkUserInfoEntity::getDisplayState, "1");
|
||||||
// 查询主表全部字段
|
// 查询主表全部字段
|
||||||
lambdaQueryWrapper.selectAll(PlayClerkUserInfoEntity.class).selectAs(PlayClerkUserInfoEntity::getCity,
|
lambdaQueryWrapper.selectAll(PlayClerkUserInfoEntity.class).selectAs(PlayClerkUserInfoEntity::getCity, "address");
|
||||||
"address");
|
|
||||||
// 等级表
|
// 等级表
|
||||||
lambdaQueryWrapper.selectAs(PlayClerkLevelInfoEntity::getName, "levelName");
|
lambdaQueryWrapper.selectAs(PlayClerkLevelInfoEntity::getName, "levelName");
|
||||||
lambdaQueryWrapper.leftJoin(PlayClerkLevelInfoEntity.class, PlayClerkLevelInfoEntity::getId,
|
lambdaQueryWrapper.leftJoin(PlayClerkLevelInfoEntity.class, PlayClerkLevelInfoEntity::getId, PlayClerkUserInfoEntity::getLevelId);
|
||||||
PlayClerkUserInfoEntity::getLevelId);
|
|
||||||
|
|
||||||
// 服务项目表
|
// 服务项目表
|
||||||
if (StrUtil.isNotBlank(vo.getNickname())) {
|
if (StrUtil.isNotBlank(vo.getNickname())) {
|
||||||
@@ -345,11 +345,33 @@ public class PlayClerkUserInfoServiceImpl extends ServiceImpl<PlayClerkUserInfoM
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 排序:非空的等级排序号优先,值越小越靠前;同一排序号在线状态优先
|
// 排序:非空的等级排序号优先,值越小越靠前;同一排序号在线状态优先
|
||||||
lambdaQueryWrapper.orderByAsc(true, "CASE WHEN t1.order_number IS NULL THEN 1 ELSE 0 END")
|
lambdaQueryWrapper
|
||||||
.orderByAsc(PlayClerkLevelInfoEntity::getOrderNumber)
|
.orderByDesc(PlayClerkUserInfoEntity::getOnlineState)
|
||||||
.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);
|
||||||
|
|
||||||
return this.baseMapper.selectJoinPage(page, PlayClerkUserInfoResultVo.class, lambdaQueryWrapper);
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -364,8 +386,7 @@ public class PlayClerkUserInfoServiceImpl extends ServiceImpl<PlayClerkUserInfoM
|
|||||||
public IPage<PlayClerkUnsettledWagesInfoReturnVo> listUnsettledWagesByPage(PlayClerkUnsettledWagesInfoQueryVo vo) {
|
public IPage<PlayClerkUnsettledWagesInfoReturnVo> listUnsettledWagesByPage(PlayClerkUnsettledWagesInfoQueryVo vo) {
|
||||||
MPJLambdaWrapper<PlayClerkUserInfoEntity> lambdaQueryWrapper = new MPJLambdaWrapper<>();
|
MPJLambdaWrapper<PlayClerkUserInfoEntity> lambdaQueryWrapper = new MPJLambdaWrapper<>();
|
||||||
// 查询所有店员
|
// 查询所有店员
|
||||||
lambdaQueryWrapper.selectAs(PlayClerkUserInfoEntity::getNickname, "clerkNickname")
|
lambdaQueryWrapper.selectAs(PlayClerkUserInfoEntity::getNickname, "clerkNickname").selectAs(PlayClerkUserInfoEntity::getId, "clerkId");
|
||||||
.selectAs(PlayClerkUserInfoEntity::getId, "clerkId");
|
|
||||||
lambdaQueryWrapper.eq(PlayClerkUserInfoEntity::getClerkState, ClerkRoleStatus.CLERK.getCode());
|
lambdaQueryWrapper.eq(PlayClerkUserInfoEntity::getClerkState, ClerkRoleStatus.CLERK.getCode());
|
||||||
// 加入组员的筛选
|
// 加入组员的筛选
|
||||||
List<String> clerkIdList = playClerkGroupInfoService.getValidClerkIdList(SecurityUtils.getLoginUser(), null);
|
List<String> clerkIdList = playClerkGroupInfoService.getValidClerkIdList(SecurityUtils.getLoginUser(), null);
|
||||||
@@ -377,14 +398,11 @@ public class PlayClerkUserInfoServiceImpl extends ServiceImpl<PlayClerkUserInfoM
|
|||||||
lambdaQueryWrapper.eq(PlayClerkUserInfoEntity::getListingState, vo.getListingState());
|
lambdaQueryWrapper.eq(PlayClerkUserInfoEntity::getListingState, vo.getListingState());
|
||||||
}
|
}
|
||||||
// 查询店员订单信息
|
// 查询店员订单信息
|
||||||
lambdaQueryWrapper.selectCollection(PlayOrderInfoEntity.class,
|
lambdaQueryWrapper.selectCollection(PlayOrderInfoEntity.class, PlayClerkUnsettledWagesInfoReturnVo::getOrderInfoEntities);
|
||||||
PlayClerkUnsettledWagesInfoReturnVo::getOrderInfoEntities);
|
lambdaQueryWrapper.leftJoin(PlayOrderInfoEntity.class, PlayOrderInfoEntity::getAcceptBy, PlayClerkUserInfoEntity::getId);
|
||||||
lambdaQueryWrapper.leftJoin(PlayOrderInfoEntity.class, PlayOrderInfoEntity::getAcceptBy,
|
|
||||||
PlayClerkUserInfoEntity::getId);
|
|
||||||
lambdaQueryWrapper.eq(PlayOrderInfoEntity::getOrderSettlementState, "0");
|
lambdaQueryWrapper.eq(PlayOrderInfoEntity::getOrderSettlementState, "0");
|
||||||
|
|
||||||
return this.baseMapper.selectJoinPage(new Page<>(vo.getPageNum(), vo.getPageSize()),
|
return this.baseMapper.selectJoinPage(new Page<>(vo.getPageNum(), vo.getPageSize()), PlayClerkUnsettledWagesInfoReturnVo.class, lambdaQueryWrapper);
|
||||||
PlayClerkUnsettledWagesInfoReturnVo.class, lambdaQueryWrapper);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -483,12 +501,9 @@ public class PlayClerkUserInfoServiceImpl extends ServiceImpl<PlayClerkUserInfoM
|
|||||||
lambdaQueryWrapper.in(PlayClerkUserInfoEntity::getId, clerkIdList);
|
lambdaQueryWrapper.in(PlayClerkUserInfoEntity::getId, clerkIdList);
|
||||||
|
|
||||||
// 排序:置顶状态优先,在线用户其次,最后按创建时间倒序
|
// 排序:置顶状态优先,在线用户其次,最后按创建时间倒序
|
||||||
lambdaQueryWrapper.orderByDesc(PlayClerkUserInfoEntity::getPinToTopState)
|
lambdaQueryWrapper.orderByDesc(PlayClerkUserInfoEntity::getPinToTopState).orderByDesc(PlayClerkUserInfoEntity::getOnlineState).orderByDesc(PlayClerkUserInfoEntity::getCreatedTime);
|
||||||
.orderByDesc(PlayClerkUserInfoEntity::getOnlineState)
|
|
||||||
.orderByDesc(PlayClerkUserInfoEntity::getCreatedTime);
|
|
||||||
|
|
||||||
IPage<PlayClerkUserReturnVo> page = this.baseMapper.selectJoinPage(
|
IPage<PlayClerkUserReturnVo> page = this.baseMapper.selectJoinPage(new Page<>(vo.getPageNum(), vo.getPageSize()), PlayClerkUserReturnVo.class, lambdaQueryWrapper);
|
||||||
new Page<>(vo.getPageNum(), vo.getPageSize()), PlayClerkUserReturnVo.class, lambdaQueryWrapper);
|
|
||||||
|
|
||||||
for (PlayClerkUserReturnVo record : page.getRecords()) {
|
for (PlayClerkUserReturnVo record : page.getRecords()) {
|
||||||
BigDecimal orderTotalAmount = new BigDecimal("0");
|
BigDecimal orderTotalAmount = new BigDecimal("0");
|
||||||
@@ -508,6 +523,7 @@ public class PlayClerkUserInfoServiceImpl extends ServiceImpl<PlayClerkUserInfoM
|
|||||||
record.setOrderContinueNumber(String.valueOf(orderContinueNumber));
|
record.setOrderContinueNumber(String.valueOf(orderContinueNumber));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
attachMediaToAdminVos(page.getRecords());
|
||||||
return page;
|
return page;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -519,10 +535,8 @@ public class PlayClerkUserInfoServiceImpl extends ServiceImpl<PlayClerkUserInfoM
|
|||||||
if (StrUtil.isNotBlank(customUserId)) {
|
if (StrUtil.isNotBlank(customUserId)) {
|
||||||
LambdaQueryWrapper<PlayCustomFollowInfoEntity> lambdaQueryWrapper = new LambdaQueryWrapper<>();
|
LambdaQueryWrapper<PlayCustomFollowInfoEntity> lambdaQueryWrapper = new LambdaQueryWrapper<>();
|
||||||
lambdaQueryWrapper.eq(PlayCustomFollowInfoEntity::getCustomId, customUserId);
|
lambdaQueryWrapper.eq(PlayCustomFollowInfoEntity::getCustomId, customUserId);
|
||||||
List<PlayCustomFollowInfoEntity> customFollowInfoEntities = customFollowInfoService
|
List<PlayCustomFollowInfoEntity> customFollowInfoEntities = customFollowInfoService.list(lambdaQueryWrapper);
|
||||||
.list(lambdaQueryWrapper);
|
customFollows = customFollowInfoEntities.stream().collect(Collectors.toMap(PlayCustomFollowInfoEntity::getClerkId, PlayCustomFollowInfoEntity::getFollowState));
|
||||||
customFollows = customFollowInfoEntities.stream().collect(Collectors
|
|
||||||
.toMap(PlayCustomFollowInfoEntity::getClerkId, PlayCustomFollowInfoEntity::getFollowState));
|
|
||||||
}
|
}
|
||||||
for (PlayClerkUserInfoResultVo record : voPage.getRecords()) {
|
for (PlayClerkUserInfoResultVo record : voPage.getRecords()) {
|
||||||
record.setFollowState(customFollows.containsKey(record.getId()) ? "1" : "0");
|
record.setFollowState(customFollows.containsKey(record.getId()) ? "1" : "0");
|
||||||
@@ -534,11 +548,37 @@ public class PlayClerkUserInfoServiceImpl extends ServiceImpl<PlayClerkUserInfoM
|
|||||||
return voPage;
|
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 结果
|
* @return 结果
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
@@ -552,16 +592,12 @@ public class PlayClerkUserInfoServiceImpl extends ServiceImpl<PlayClerkUserInfoM
|
|||||||
/**
|
/**
|
||||||
* 修改店员
|
* 修改店员
|
||||||
*
|
*
|
||||||
* @param playClerkUserInfo
|
* @param playClerkUserInfo 店员
|
||||||
* 店员
|
|
||||||
* @return 结果
|
* @return 结果
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public boolean update(PlayClerkUserInfoEntity playClerkUserInfo) {
|
public boolean update(PlayClerkUserInfoEntity playClerkUserInfo) {
|
||||||
boolean inspectStatus = StringUtils.isNotBlank(playClerkUserInfo.getId())
|
boolean inspectStatus = StringUtils.isNotBlank(playClerkUserInfo.getId()) && (StrUtil.isNotBlank(playClerkUserInfo.getOnboardingState()) || StrUtil.isNotBlank(playClerkUserInfo.getListingState()) || StrUtil.isNotBlank(playClerkUserInfo.getClerkState()));
|
||||||
&& (StrUtil.isNotBlank(playClerkUserInfo.getOnboardingState())
|
|
||||||
|| StrUtil.isNotBlank(playClerkUserInfo.getListingState())
|
|
||||||
|| StrUtil.isNotBlank(playClerkUserInfo.getClerkState()));
|
|
||||||
PlayClerkUserInfoEntity beforeUpdate = null;
|
PlayClerkUserInfoEntity beforeUpdate = null;
|
||||||
if (inspectStatus) {
|
if (inspectStatus) {
|
||||||
beforeUpdate = this.baseMapper.selectById(playClerkUserInfo.getId());
|
beforeUpdate = this.baseMapper.selectById(playClerkUserInfo.getId());
|
||||||
@@ -576,8 +612,7 @@ public class PlayClerkUserInfoServiceImpl extends ServiceImpl<PlayClerkUserInfoM
|
|||||||
/**
|
/**
|
||||||
* 批量删除店员
|
* 批量删除店员
|
||||||
*
|
*
|
||||||
* @param ids
|
* @param ids 需要删除的店员主键
|
||||||
* 需要删除的店员主键
|
|
||||||
* @return 结果
|
* @return 结果
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
@@ -588,8 +623,7 @@ public class PlayClerkUserInfoServiceImpl extends ServiceImpl<PlayClerkUserInfoM
|
|||||||
/**
|
/**
|
||||||
* 删除店员信息
|
* 删除店员信息
|
||||||
*
|
*
|
||||||
* @param id
|
* @param id 店员主键
|
||||||
* 店员主键
|
|
||||||
* @return 结果
|
* @return 结果
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
@@ -603,13 +637,16 @@ public class PlayClerkUserInfoServiceImpl extends ServiceImpl<PlayClerkUserInfoM
|
|||||||
LambdaQueryWrapper<PlayClerkUserInfoEntity> lambdaQueryWrapper = new LambdaQueryWrapper<>();
|
LambdaQueryWrapper<PlayClerkUserInfoEntity> lambdaQueryWrapper = new LambdaQueryWrapper<>();
|
||||||
lambdaQueryWrapper.eq(PlayClerkUserInfoEntity::getDeleted, 0);
|
lambdaQueryWrapper.eq(PlayClerkUserInfoEntity::getDeleted, 0);
|
||||||
lambdaQueryWrapper.in(PlayClerkUserInfoEntity::getId, clerkIdList);
|
lambdaQueryWrapper.in(PlayClerkUserInfoEntity::getId, clerkIdList);
|
||||||
lambdaQueryWrapper.select(PlayClerkUserInfoEntity::getId, PlayClerkUserInfoEntity::getNickname,
|
lambdaQueryWrapper.select(PlayClerkUserInfoEntity::getId, PlayClerkUserInfoEntity::getNickname, PlayClerkUserInfoEntity::getAvatar, PlayClerkUserInfoEntity::getTypeId, PlayClerkUserInfoEntity::getGroupId, PlayClerkUserInfoEntity::getPhone);
|
||||||
PlayClerkUserInfoEntity::getAvatar, PlayClerkUserInfoEntity::getTypeId,
|
|
||||||
PlayClerkUserInfoEntity::getGroupId, PlayClerkUserInfoEntity::getPhone);
|
|
||||||
lambdaQueryWrapper.orderByDesc(PlayClerkUserInfoEntity::getId);
|
lambdaQueryWrapper.orderByDesc(PlayClerkUserInfoEntity::getId);
|
||||||
return this.baseMapper.selectList(lambdaQueryWrapper);
|
return this.baseMapper.selectList(lambdaQueryWrapper);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<PlayClerkUserInfoEntity> listWithAlbumIgnoringTenant() {
|
||||||
|
return playClerkUserInfoMapper.selectAllWithAlbumIgnoringTenant();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public JSONObject getPcData(PlayClerkUserInfoEntity entity) {
|
public JSONObject getPcData(PlayClerkUserInfoEntity entity) {
|
||||||
JSONObject data = new JSONObject();
|
JSONObject data = new JSONObject();
|
||||||
@@ -621,8 +658,7 @@ public class PlayClerkUserInfoServiceImpl extends ServiceImpl<PlayClerkUserInfoM
|
|||||||
LoginUser loginUserInfo = loginService.getLoginUserInfo(entity.getSysUserId());
|
LoginUser loginUserInfo = loginService.getLoginUserInfo(entity.getSysUserId());
|
||||||
Map<String, Object> tokenMap = jwtToken.createToken(loginUserInfo);
|
Map<String, Object> tokenMap = jwtToken.createToken(loginUserInfo);
|
||||||
data.fluentPut("token", tokenMap.get("token"));
|
data.fluentPut("token", tokenMap.get("token"));
|
||||||
PlayPersonnelAdminInfoEntity adminInfoEntity = playPersonnelAdminInfoService
|
PlayPersonnelAdminInfoEntity adminInfoEntity = playPersonnelAdminInfoService.selectByUserId(entity.getSysUserId());
|
||||||
.selectByUserId(entity.getSysUserId());
|
|
||||||
if (Objects.nonNull(adminInfoEntity)) {
|
if (Objects.nonNull(adminInfoEntity)) {
|
||||||
data.fluentPut("role", "operator");
|
data.fluentPut("role", "operator");
|
||||||
return data;
|
return data;
|
||||||
@@ -632,8 +668,7 @@ public class PlayClerkUserInfoServiceImpl extends ServiceImpl<PlayClerkUserInfoM
|
|||||||
data.fluentPut("role", "leader");
|
data.fluentPut("role", "leader");
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
PlayPersonnelWaiterInfoEntity waiterInfoEntity = playClerkWaiterInfoService
|
PlayPersonnelWaiterInfoEntity waiterInfoEntity = playClerkWaiterInfoService.selectByUserId(entity.getSysUserId());
|
||||||
.selectByUserId(entity.getSysUserId());
|
|
||||||
if (Objects.nonNull(waiterInfoEntity)) {
|
if (Objects.nonNull(waiterInfoEntity)) {
|
||||||
data.fluentPut("role", "waiter");
|
data.fluentPut("role", "waiter");
|
||||||
return data;
|
return data;
|
||||||
@@ -645,13 +680,101 @@ public class PlayClerkUserInfoServiceImpl extends ServiceImpl<PlayClerkUserInfoM
|
|||||||
if (beforeUpdate == null) {
|
if (beforeUpdate == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (OnboardingStatus.transitionedToOffboarded(updatedPayload.getOnboardingState(),
|
if (OnboardingStatus.transitionedToOffboarded(updatedPayload.getOnboardingState(), beforeUpdate.getOnboardingState()) || ListingStatus.transitionedToDelisted(updatedPayload.getListingState(), beforeUpdate.getListingState()) || ClerkRoleStatus.transitionedToNonClerk(updatedPayload.getClerkState(), beforeUpdate.getClerkState())) {
|
||||||
beforeUpdate.getOnboardingState())
|
|
||||||
|| ListingStatus.transitionedToDelisted(updatedPayload.getListingState(),
|
|
||||||
beforeUpdate.getListingState())
|
|
||||||
|| ClerkRoleStatus.transitionedToNonClerk(updatedPayload.getClerkState(),
|
|
||||||
beforeUpdate.getClerkState())) {
|
|
||||||
invalidateClerkSession(beforeUpdate.getId());
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,132 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
package com.starry.admin.modules.media.entity;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableField;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
|
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.Map;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 媒资表 play_media
|
||||||
|
*
|
||||||
|
* <p>存储各类业务(店员、顾客等)的图片/视频。</p>
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@TableName(value = "play_media", autoResultMap = true)
|
||||||
|
public class PlayMediaEntity {
|
||||||
|
|
||||||
|
@TableId
|
||||||
|
private String id;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 租户ID
|
||||||
|
*/
|
||||||
|
private String tenantId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 归属业务类型,例如 clerk/custom/order
|
||||||
|
*/
|
||||||
|
private String ownerType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 归属业务主键,例如店员ID
|
||||||
|
*/
|
||||||
|
private String ownerId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 媒资类型 image / video
|
||||||
|
*/
|
||||||
|
private String kind;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 媒资状态 uploaded / processing / ready / approved / rejected
|
||||||
|
*/
|
||||||
|
private String status;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 资源地址
|
||||||
|
*/
|
||||||
|
private String url;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 视频封面地址
|
||||||
|
*/
|
||||||
|
private String coverUrl;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 时长(毫秒)
|
||||||
|
*/
|
||||||
|
private Long durationMs;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 媒资宽度
|
||||||
|
*/
|
||||||
|
private Integer width;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 媒资高度
|
||||||
|
*/
|
||||||
|
private Integer height;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文件大小(字节)
|
||||||
|
*/
|
||||||
|
private Long sizeBytes;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 排序序号,从 0 开始
|
||||||
|
*/
|
||||||
|
private Integer orderIndex;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 扩展字段
|
||||||
|
*/
|
||||||
|
@TableField(typeHandler = JacksonTypeHandler.class)
|
||||||
|
private Map<String, Object> metadata;
|
||||||
|
|
||||||
|
private Date createdTime;
|
||||||
|
|
||||||
|
private Date updatedTime;
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package com.starry.admin.modules.media.enums;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
public enum MediaKind {
|
||||||
|
|
||||||
|
IMAGE("image"),
|
||||||
|
VIDEO("video");
|
||||||
|
|
||||||
|
private final String code;
|
||||||
|
|
||||||
|
MediaKind(String code) {
|
||||||
|
this.code = code;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isVideo(String value) {
|
||||||
|
return VIDEO.code.equalsIgnoreCase(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isImage(String value) {
|
||||||
|
return IMAGE.code.equalsIgnoreCase(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static MediaKind fromCode(String value) {
|
||||||
|
for (MediaKind kind : values()) {
|
||||||
|
if (kind.code.equalsIgnoreCase(value)) {
|
||||||
|
return kind;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new IllegalArgumentException("Unsupported media kind: " + value);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package com.starry.admin.modules.media.enums;
|
||||||
|
|
||||||
|
public final class MediaOwnerType {
|
||||||
|
|
||||||
|
private MediaOwnerType() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final String CLERK = "clerk";
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package com.starry.admin.modules.media.enums;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
public enum MediaStatus {
|
||||||
|
UPLOADED("uploaded"),
|
||||||
|
PROCESSING("processing"),
|
||||||
|
READY("ready"),
|
||||||
|
APPROVED("approved"),
|
||||||
|
REJECTED("rejected");
|
||||||
|
|
||||||
|
private final String code;
|
||||||
|
|
||||||
|
MediaStatus(String code) {
|
||||||
|
this.code = code;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isTerminal(String value) {
|
||||||
|
return APPROVED.code.equalsIgnoreCase(value) || REJECTED.code.equalsIgnoreCase(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package com.starry.admin.modules.media.mapper;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
|
import com.starry.admin.modules.media.entity.PlayMediaEntity;
|
||||||
|
|
||||||
|
public interface PlayMediaMapper extends BaseMapper<PlayMediaEntity> {
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package com.starry.admin.modules.media.service;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.extension.service.IService;
|
||||||
|
import com.starry.admin.modules.media.entity.PlayMediaEntity;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public interface IPlayMediaService extends IService<PlayMediaEntity> {
|
||||||
|
|
||||||
|
List<PlayMediaEntity> listByOwner(String ownerType, String ownerId);
|
||||||
|
|
||||||
|
List<PlayMediaEntity> listByOwner(String ownerType, String ownerId, Collection<String> statuses);
|
||||||
|
|
||||||
|
List<PlayMediaEntity> listApprovedByOwner(String ownerType, String ownerId);
|
||||||
|
|
||||||
|
PlayMediaEntity normalizeAndSave(PlayMediaEntity entity);
|
||||||
|
|
||||||
|
void updateOrder(String ownerType, String ownerId, List<String> orderedIds);
|
||||||
|
|
||||||
|
void softDelete(String ownerType, String ownerId, String mediaId);
|
||||||
|
}
|
||||||
@@ -0,0 +1,136 @@
|
|||||||
|
package com.starry.admin.modules.media.service.impl;
|
||||||
|
|
||||||
|
import cn.hutool.core.collection.CollectionUtil;
|
||||||
|
import cn.hutool.core.lang.Assert;
|
||||||
|
import cn.hutool.core.util.StrUtil;
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
|
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||||
|
import com.starry.admin.common.exception.CustomException;
|
||||||
|
import com.starry.admin.modules.media.entity.PlayMediaEntity;
|
||||||
|
import com.starry.admin.modules.media.enums.MediaOwnerType;
|
||||||
|
import com.starry.admin.modules.media.enums.MediaStatus;
|
||||||
|
import com.starry.admin.modules.media.mapper.PlayMediaMapper;
|
||||||
|
import com.starry.admin.modules.media.service.IPlayMediaService;
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class PlayMediaServiceImpl extends ServiceImpl<PlayMediaMapper, PlayMediaEntity>
|
||||||
|
implements IPlayMediaService {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<PlayMediaEntity> listByOwner(String ownerType, String ownerId) {
|
||||||
|
return listByOwner(ownerType, ownerId, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<PlayMediaEntity> listByOwner(String ownerType, String ownerId, Collection<String> statuses) {
|
||||||
|
LambdaQueryWrapper<PlayMediaEntity> wrapper = new LambdaQueryWrapper<>();
|
||||||
|
wrapper.eq(PlayMediaEntity::getOwnerType, ownerType)
|
||||||
|
.eq(PlayMediaEntity::getOwnerId, ownerId)
|
||||||
|
.orderByAsc(PlayMediaEntity::getOrderIndex)
|
||||||
|
.orderByDesc(PlayMediaEntity::getCreatedTime);
|
||||||
|
if (CollectionUtil.isNotEmpty(statuses)) {
|
||||||
|
wrapper.in(PlayMediaEntity::getStatus, statuses);
|
||||||
|
}
|
||||||
|
return this.list(wrapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<PlayMediaEntity> listApprovedByOwner(String ownerType, String ownerId) {
|
||||||
|
return listByOwner(ownerType, ownerId, Collections.singleton(MediaStatus.READY.getCode()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public PlayMediaEntity normalizeAndSave(PlayMediaEntity entity) {
|
||||||
|
Assert.notNull(entity, "媒资信息不能为空");
|
||||||
|
Assert.isTrue(StrUtil.isNotBlank(entity.getOwnerId()), "媒资归属ID不能为空");
|
||||||
|
// ownerType 默认 clerk
|
||||||
|
if (StrUtil.isBlank(entity.getOwnerType())) {
|
||||||
|
entity.setOwnerType(MediaOwnerType.CLERK);
|
||||||
|
}
|
||||||
|
if (entity.getOrderIndex() == null) {
|
||||||
|
entity.setOrderIndex(resolveNextOrderIndex(entity.getOwnerType(), entity.getOwnerId()));
|
||||||
|
}
|
||||||
|
if (StrUtil.isBlank(entity.getStatus())) {
|
||||||
|
entity.setStatus(MediaStatus.UPLOADED.getCode());
|
||||||
|
}
|
||||||
|
boolean saved = this.save(entity);
|
||||||
|
if (!saved) {
|
||||||
|
throw new CustomException("媒资保存失败");
|
||||||
|
}
|
||||||
|
return entity;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public void updateOrder(String ownerType, String ownerId, List<String> orderedIds) {
|
||||||
|
List<PlayMediaEntity> mediaList = listByOwner(ownerType, ownerId);
|
||||||
|
if (CollectionUtil.isEmpty(mediaList)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Map<String, PlayMediaEntity> mediaById = mediaList.stream()
|
||||||
|
.collect(Collectors.toMap(PlayMediaEntity::getId, item -> item));
|
||||||
|
Set<String> keepSet = new LinkedHashSet<>();
|
||||||
|
if (CollectionUtil.isNotEmpty(orderedIds)) {
|
||||||
|
keepSet.addAll(orderedIds);
|
||||||
|
}
|
||||||
|
List<PlayMediaEntity> updates = new ArrayList<>();
|
||||||
|
int index = 0;
|
||||||
|
for (String mediaId : keepSet) {
|
||||||
|
PlayMediaEntity entity = mediaById.get(mediaId);
|
||||||
|
if (entity == null) {
|
||||||
|
throw new CustomException("媒资不存在或已被删除");
|
||||||
|
}
|
||||||
|
entity.setOrderIndex(index++);
|
||||||
|
if (MediaStatus.REJECTED.getCode().equals(entity.getStatus())) {
|
||||||
|
entity.setStatus(MediaStatus.READY.getCode());
|
||||||
|
}
|
||||||
|
updates.add(entity);
|
||||||
|
}
|
||||||
|
// 其他未保留的标记为 rejected
|
||||||
|
for (PlayMediaEntity entity : mediaList) {
|
||||||
|
if (!keepSet.contains(entity.getId())
|
||||||
|
&& !MediaStatus.REJECTED.getCode().equals(entity.getStatus())) {
|
||||||
|
entity.setStatus(MediaStatus.REJECTED.getCode());
|
||||||
|
entity.setOrderIndex(0);
|
||||||
|
updates.add(entity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (CollectionUtil.isNotEmpty(updates)) {
|
||||||
|
this.updateBatchById(updates);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public void softDelete(String ownerType, String ownerId, String mediaId) {
|
||||||
|
PlayMediaEntity entity = this.getById(mediaId);
|
||||||
|
if (entity == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!ownerType.equals(entity.getOwnerType()) || !ownerId.equals(entity.getOwnerId())) {
|
||||||
|
throw new CustomException("无权删除该媒资");
|
||||||
|
}
|
||||||
|
entity.setStatus(MediaStatus.REJECTED.getCode());
|
||||||
|
entity.setOrderIndex(0);
|
||||||
|
this.updateById(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
private int resolveNextOrderIndex(String ownerType, String ownerId) {
|
||||||
|
LambdaQueryWrapper<PlayMediaEntity> wrapper = new LambdaQueryWrapper<>();
|
||||||
|
wrapper.eq(PlayMediaEntity::getOwnerType, ownerType)
|
||||||
|
.eq(PlayMediaEntity::getOwnerId, ownerId)
|
||||||
|
.ne(PlayMediaEntity::getStatus, MediaStatus.REJECTED.getCode())
|
||||||
|
.orderByDesc(PlayMediaEntity::getOrderIndex)
|
||||||
|
.last("limit 1");
|
||||||
|
PlayMediaEntity last = this.getOne(wrapper, false);
|
||||||
|
if (last == null || last.getOrderIndex() == null) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return last.getOrderIndex() + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ import com.starry.admin.modules.clerk.service.IPlayClerkUserInfoService;
|
|||||||
import com.starry.admin.modules.order.module.constant.OrderConstant.OperatorType;
|
import com.starry.admin.modules.order.module.constant.OrderConstant.OperatorType;
|
||||||
import com.starry.admin.modules.order.module.constant.OrderConstant.OrderTriggerSource;
|
import com.starry.admin.modules.order.module.constant.OrderConstant.OrderTriggerSource;
|
||||||
import com.starry.admin.modules.order.module.dto.OrderRefundContext;
|
import com.starry.admin.modules.order.module.dto.OrderRefundContext;
|
||||||
|
import com.starry.admin.modules.order.module.dto.OrderRevocationContext;
|
||||||
import com.starry.admin.modules.order.module.entity.PlayOrderInfoEntity;
|
import com.starry.admin.modules.order.module.entity.PlayOrderInfoEntity;
|
||||||
import com.starry.admin.modules.order.module.vo.*;
|
import com.starry.admin.modules.order.module.vo.*;
|
||||||
import com.starry.admin.modules.order.service.IOrderLifecycleService;
|
import com.starry.admin.modules.order.service.IOrderLifecycleService;
|
||||||
@@ -14,6 +15,7 @@ import com.starry.admin.modules.order.service.IPlayOrderInfoService;
|
|||||||
import com.starry.admin.modules.shop.module.vo.PlayCommodityInfoVo;
|
import com.starry.admin.modules.shop.module.vo.PlayCommodityInfoVo;
|
||||||
import com.starry.admin.modules.shop.service.IPlayCommodityInfoService;
|
import com.starry.admin.modules.shop.service.IPlayCommodityInfoService;
|
||||||
import com.starry.admin.modules.weichat.service.WxCustomMpService;
|
import com.starry.admin.modules.weichat.service.WxCustomMpService;
|
||||||
|
import com.starry.admin.modules.withdraw.service.IEarningsService;
|
||||||
import com.starry.admin.utils.SecurityUtils;
|
import com.starry.admin.utils.SecurityUtils;
|
||||||
import com.starry.common.annotation.Log;
|
import com.starry.common.annotation.Log;
|
||||||
import com.starry.common.context.CustomSecurityContextHolder;
|
import com.starry.common.context.CustomSecurityContextHolder;
|
||||||
@@ -27,6 +29,7 @@ import io.swagger.annotations.ApiResponse;
|
|||||||
import io.swagger.annotations.ApiResponses;
|
import io.swagger.annotations.ApiResponses;
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
import javax.annotation.Resource;
|
import javax.annotation.Resource;
|
||||||
import org.springframework.validation.annotation.Validated;
|
import org.springframework.validation.annotation.Validated;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
@@ -58,6 +61,9 @@ public class PlayOrderInfoController {
|
|||||||
@Resource
|
@Resource
|
||||||
private IPlayClerkUserInfoService clerkUserInfoService;
|
private IPlayClerkUserInfoService clerkUserInfoService;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private IEarningsService earningsService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 分页查询订单列表
|
* 分页查询订单列表
|
||||||
*/
|
*/
|
||||||
@@ -106,6 +112,46 @@ public class PlayOrderInfoController {
|
|||||||
return R.ok("退款成功");
|
return R.ok("退款成功");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ApiOperation(value = "撤销已完成订单", notes = "管理员操作撤销,支持可选退款与收益处理")
|
||||||
|
@PostMapping("/revokeCompleted")
|
||||||
|
public R revokeCompleted(@Validated @RequestBody PlayOrderRevocationVo vo) {
|
||||||
|
OrderRevocationContext context = new OrderRevocationContext();
|
||||||
|
context.setOrderId(vo.getOrderId());
|
||||||
|
context.setRefundToCustomer(vo.isRefundToCustomer());
|
||||||
|
context.setRefundAmount(vo.getRefundAmount());
|
||||||
|
context.setRefundReason(vo.getRefundReason());
|
||||||
|
context.setDeductClerkEarnings(vo.isDeductClerkEarnings());
|
||||||
|
context.setEarningsAdjustAmount(vo.getDeductAmount());
|
||||||
|
context.setOperatorType(OperatorType.ADMIN.getCode());
|
||||||
|
context.setOperatorId(SecurityUtils.getUserId());
|
||||||
|
context.withTriggerSource(OrderTriggerSource.ADMIN_API);
|
||||||
|
orderLifecycleService.revokeCompletedOrder(context);
|
||||||
|
return R.ok("撤销成功");
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiOperation(value = "撤销限额", notes = "查询指定订单可退金额与可扣回收益")
|
||||||
|
@GetMapping("/{id}/revocationLimits")
|
||||||
|
public R getRevocationLimits(@PathVariable("id") String id) {
|
||||||
|
PlayOrderInfoEntity order = orderInfoService.selectOrderInfoById(id);
|
||||||
|
BigDecimal maxRefundAmount = Optional.ofNullable(order.getFinalAmount()).orElse(BigDecimal.ZERO);
|
||||||
|
BigDecimal maxDeductAmount = BigDecimal.ZERO;
|
||||||
|
if (order.getAcceptBy() != null) {
|
||||||
|
maxDeductAmount = Optional.ofNullable(earningsService.getRemainingEarningsForOrder(order.getId(), order.getAcceptBy()))
|
||||||
|
.orElse(BigDecimal.ZERO);
|
||||||
|
}
|
||||||
|
if (maxDeductAmount.compareTo(BigDecimal.ZERO) < 0) {
|
||||||
|
maxDeductAmount = BigDecimal.ZERO;
|
||||||
|
}
|
||||||
|
|
||||||
|
PlayOrderRevocationLimitsVo limitsVo = new PlayOrderRevocationLimitsVo();
|
||||||
|
limitsVo.setOrderId(order.getId());
|
||||||
|
limitsVo.setMaxRefundAmount(maxRefundAmount);
|
||||||
|
limitsVo.setMaxDeductAmount(maxDeductAmount);
|
||||||
|
limitsVo.setDefaultDeductAmount(maxDeductAmount);
|
||||||
|
limitsVo.setDeductible(order.getAcceptBy() != null);
|
||||||
|
return R.ok(limitsVo);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 管理后台强制取消进行中订单
|
* 管理后台强制取消进行中订单
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
package com.starry.admin.modules.order.listener;
|
||||||
|
|
||||||
|
import com.starry.admin.common.exception.CustomException;
|
||||||
|
import com.starry.admin.modules.custom.module.entity.PlayCustomUserInfoEntity;
|
||||||
|
import com.starry.admin.modules.custom.service.IPlayCustomUserInfoService;
|
||||||
|
import com.starry.admin.modules.order.module.constant.OrderConstant.BalanceOperationType;
|
||||||
|
import com.starry.admin.modules.order.module.entity.PlayOrderInfoEntity;
|
||||||
|
import com.starry.admin.modules.order.module.event.OrderRevocationEvent;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.util.Optional;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.transaction.event.TransactionPhase;
|
||||||
|
import org.springframework.transaction.event.TransactionalEventListener;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
public class OrderRevocationBalanceListener {
|
||||||
|
|
||||||
|
private final IPlayCustomUserInfoService customUserInfoService;
|
||||||
|
|
||||||
|
public OrderRevocationBalanceListener(IPlayCustomUserInfoService customUserInfoService) {
|
||||||
|
this.customUserInfoService = customUserInfoService;
|
||||||
|
}
|
||||||
|
|
||||||
|
@TransactionalEventListener(phase = TransactionPhase.BEFORE_COMMIT)
|
||||||
|
public void handle(OrderRevocationEvent event) {
|
||||||
|
if (event == null || event.getContext() == null || event.getOrderSnapshot() == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!event.getContext().isRefundToCustomer()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
BigDecimal refundAmount = Optional.ofNullable(event.getContext().getRefundAmount()).orElse(BigDecimal.ZERO);
|
||||||
|
if (refundAmount.compareTo(BigDecimal.ZERO) <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
PlayOrderInfoEntity order = event.getOrderSnapshot();
|
||||||
|
PlayCustomUserInfoEntity customer = customUserInfoService.getById(order.getPurchaserBy());
|
||||||
|
if (customer == null) {
|
||||||
|
throw new CustomException("顾客信息不存在");
|
||||||
|
}
|
||||||
|
BigDecimal currentBalance = Optional.ofNullable(customer.getAccountBalance()).orElse(BigDecimal.ZERO);
|
||||||
|
customUserInfoService.updateAccountBalanceById(
|
||||||
|
customer.getId(),
|
||||||
|
currentBalance,
|
||||||
|
currentBalance.add(refundAmount),
|
||||||
|
BalanceOperationType.REFUND.getCode(),
|
||||||
|
"已完成订单撤销退款",
|
||||||
|
refundAmount,
|
||||||
|
BigDecimal.ZERO,
|
||||||
|
order.getId());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
package com.starry.admin.modules.order.listener;
|
||||||
|
|
||||||
|
import com.starry.admin.common.exception.CustomException;
|
||||||
|
import com.starry.admin.modules.order.module.dto.OrderRevocationContext;
|
||||||
|
import com.starry.admin.modules.order.module.entity.PlayOrderInfoEntity;
|
||||||
|
import com.starry.admin.modules.order.module.event.OrderRevocationEvent;
|
||||||
|
import com.starry.admin.modules.withdraw.service.IEarningsService;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.util.Optional;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.transaction.event.TransactionPhase;
|
||||||
|
import org.springframework.transaction.event.TransactionalEventListener;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
public class OrderRevocationEarningsListener {
|
||||||
|
|
||||||
|
private final IEarningsService earningsService;
|
||||||
|
|
||||||
|
public OrderRevocationEarningsListener(IEarningsService earningsService) {
|
||||||
|
this.earningsService = earningsService;
|
||||||
|
}
|
||||||
|
|
||||||
|
@TransactionalEventListener(phase = TransactionPhase.BEFORE_COMMIT)
|
||||||
|
public void handle(OrderRevocationEvent event) {
|
||||||
|
if (event == null || event.getContext() == null || event.getOrderSnapshot() == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
OrderRevocationContext context = event.getContext();
|
||||||
|
if (!context.isDeductClerkEarnings()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
createCounterLine(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void createCounterLine(OrderRevocationEvent event) {
|
||||||
|
OrderRevocationContext context = event.getContext();
|
||||||
|
if (context == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
PlayOrderInfoEntity order = event.getOrderSnapshot();
|
||||||
|
String targetClerkId = order.getAcceptBy();
|
||||||
|
if (targetClerkId == null || targetClerkId.trim().isEmpty()) {
|
||||||
|
throw new CustomException("需要指定收益冲销目标账号");
|
||||||
|
}
|
||||||
|
BigDecimal amount = context.getEarningsAdjustAmount();
|
||||||
|
if (amount == null || amount.compareTo(BigDecimal.ZERO) <= 0) {
|
||||||
|
amount = Optional.ofNullable(order.getEstimatedRevenue()).orElse(BigDecimal.ZERO);
|
||||||
|
if (amount.compareTo(BigDecimal.ZERO) <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
earningsService.createCounterLine(order.getId(), order.getTenantId(), targetClerkId, amount, context.getOperatorId());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,7 +19,8 @@ public class OrderConstant {
|
|||||||
ACCEPTED("1", "已接单(待开始)"),
|
ACCEPTED("1", "已接单(待开始)"),
|
||||||
IN_PROGRESS("2", "已开始(服务中)"),
|
IN_PROGRESS("2", "已开始(服务中)"),
|
||||||
COMPLETED("3", "已完成"),
|
COMPLETED("3", "已完成"),
|
||||||
CANCELLED("4", "已取消");
|
CANCELLED("4", "已取消"),
|
||||||
|
REVOKED("5", "已撤销");
|
||||||
|
|
||||||
private final String code;
|
private final String code;
|
||||||
private final String description;
|
private final String description;
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
package com.starry.admin.modules.order.module.dto;
|
||||||
|
|
||||||
|
import com.starry.admin.modules.order.module.constant.OrderConstant.OrderTriggerSource;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import javax.validation.constraints.NotBlank;
|
||||||
|
import lombok.Data;
|
||||||
|
import org.springframework.lang.Nullable;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class OrderRevocationContext {
|
||||||
|
|
||||||
|
@NotBlank
|
||||||
|
private String orderId;
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private String operatorId;
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private String operatorType;
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private BigDecimal refundAmount;
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private String refundReason;
|
||||||
|
|
||||||
|
private boolean refundToCustomer;
|
||||||
|
|
||||||
|
private boolean deductClerkEarnings;
|
||||||
|
|
||||||
|
private OrderTriggerSource triggerSource = OrderTriggerSource.UNKNOWN;
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private BigDecimal earningsAdjustAmount;
|
||||||
|
|
||||||
|
public OrderRevocationContext withTriggerSource(OrderTriggerSource triggerSource) {
|
||||||
|
this.triggerSource = triggerSource;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package com.starry.admin.modules.order.module.event;
|
||||||
|
|
||||||
|
import com.starry.admin.modules.order.module.dto.OrderRevocationContext;
|
||||||
|
import com.starry.admin.modules.order.module.entity.PlayOrderInfoEntity;
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
public class OrderRevocationEvent {
|
||||||
|
|
||||||
|
private final OrderRevocationContext context;
|
||||||
|
private final PlayOrderInfoEntity orderSnapshot;
|
||||||
|
|
||||||
|
public OrderRevocationEvent(OrderRevocationContext context, PlayOrderInfoEntity orderSnapshot) {
|
||||||
|
this.context = context;
|
||||||
|
this.orderSnapshot = orderSnapshot;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -30,6 +30,12 @@ public class PlayOrderInfoQueryVo extends BasePageEntity {
|
|||||||
@ApiModelProperty(value = "订单编号", example = "ORDER20240320001", notes = "订单的编号,支持模糊查询")
|
@ApiModelProperty(value = "订单编号", example = "ORDER20240320001", notes = "订单的编号,支持模糊查询")
|
||||||
private String orderNo;
|
private String orderNo;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 统一关键字(订单号或店员昵称)
|
||||||
|
*/
|
||||||
|
@ApiModelProperty(value = "关键词", example = "ORDER20240320001", notes = "支持订单号或店员昵称模糊查询")
|
||||||
|
private String keyword;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 订单状态【0:1:2:3:4】 0:已下单(待接单) 1:已接单(待开始) 2:已开始(服务中) 3:已完成 4:已取消
|
* 订单状态【0:1:2:3:4】 0:已下单(待接单) 1:已接单(待开始) 2:已开始(服务中) 3:已完成 4:已取消
|
||||||
*/
|
*/
|
||||||
@@ -54,6 +60,12 @@ public class PlayOrderInfoQueryVo extends BasePageEntity {
|
|||||||
@ApiModelProperty(value = "是否首单", example = "1", notes = "0:不是,1:是")
|
@ApiModelProperty(value = "是否首单", example = "1", notes = "0:不是,1:是")
|
||||||
private String firstOrder;
|
private String firstOrder;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 随机单要求-店员性别(0:未知;1:男;2:女)
|
||||||
|
*/
|
||||||
|
@ApiModelProperty(value = "店员性别", example = "2", notes = "0:未知;1:男;2:女")
|
||||||
|
private String sex;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 是否使用优惠券[0:未使用,1:已使用]
|
* 是否使用优惠券[0:未使用,1:已使用]
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package com.starry.admin.modules.order.module.vo;
|
||||||
|
|
||||||
|
import io.swagger.annotations.ApiModel;
|
||||||
|
import io.swagger.annotations.ApiModelProperty;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@ApiModel(value = "撤销限额信息", description = "展示撤销时可退金额、可扣回收益等信息")
|
||||||
|
public class PlayOrderRevocationLimitsVo {
|
||||||
|
|
||||||
|
@ApiModelProperty("订单ID")
|
||||||
|
private String orderId;
|
||||||
|
|
||||||
|
@ApiModelProperty("最大可退金额")
|
||||||
|
private BigDecimal maxRefundAmount = BigDecimal.ZERO;
|
||||||
|
|
||||||
|
@ApiModelProperty("最大可扣回收益")
|
||||||
|
private BigDecimal maxDeductAmount = BigDecimal.ZERO;
|
||||||
|
|
||||||
|
@ApiModelProperty("建议扣回金额")
|
||||||
|
private BigDecimal defaultDeductAmount = BigDecimal.ZERO;
|
||||||
|
|
||||||
|
@ApiModelProperty("是否存在可扣回店员")
|
||||||
|
private boolean deductible;
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
package com.starry.admin.modules.order.module.vo;
|
||||||
|
|
||||||
|
import io.swagger.annotations.ApiModel;
|
||||||
|
import io.swagger.annotations.ApiModelProperty;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import javax.validation.constraints.NotBlank;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@ApiModel(value = "订单撤销参数", description = "撤销已完成订单的请求参数")
|
||||||
|
public class PlayOrderRevocationVo {
|
||||||
|
|
||||||
|
@NotBlank(message = "订单ID不能为空")
|
||||||
|
@ApiModelProperty(value = "订单ID", required = true)
|
||||||
|
private String orderId;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "是否退还顾客余额")
|
||||||
|
private boolean refundToCustomer;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "退款金额,未填写则默认订单实付金额")
|
||||||
|
private BigDecimal refundAmount;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "撤销原因")
|
||||||
|
private String refundReason;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "是否扣回店员收益")
|
||||||
|
private boolean deductClerkEarnings;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "扣回金额,未填写则默认按本单收益全额扣回")
|
||||||
|
private BigDecimal deductAmount;
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ 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.OrderPlacementCommand;
|
||||||
import com.starry.admin.modules.order.module.dto.OrderPlacementResult;
|
import com.starry.admin.modules.order.module.dto.OrderPlacementResult;
|
||||||
import com.starry.admin.modules.order.module.dto.OrderRefundContext;
|
import com.starry.admin.modules.order.module.dto.OrderRefundContext;
|
||||||
|
import com.starry.admin.modules.order.module.dto.OrderRevocationContext;
|
||||||
import com.starry.admin.modules.order.module.entity.PlayOrderInfoEntity;
|
import com.starry.admin.modules.order.module.entity.PlayOrderInfoEntity;
|
||||||
|
|
||||||
public interface IOrderLifecycleService {
|
public interface IOrderLifecycleService {
|
||||||
@@ -14,4 +15,6 @@ public interface IOrderLifecycleService {
|
|||||||
void completeOrder(String orderId, OrderCompletionContext context);
|
void completeOrder(String orderId, OrderCompletionContext context);
|
||||||
|
|
||||||
void refundOrder(OrderRefundContext context);
|
void refundOrder(OrderRefundContext context);
|
||||||
|
|
||||||
|
void revokeCompletedOrder(OrderRevocationContext context);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -199,6 +199,8 @@ public interface IPlayOrderInfoService extends IService<PlayOrderInfoEntity> {
|
|||||||
*/
|
*/
|
||||||
List<PlayOrderInfoEntity> customSelectOrderInfoByList(String customId);
|
List<PlayOrderInfoEntity> customSelectOrderInfoByList(String customId);
|
||||||
|
|
||||||
|
void revokeCompletedOrder(OrderRevocationContext context);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 修改订单状态为接单 只有管理员或者店员本人才能操作
|
* 修改订单状态为接单 只有管理员或者店员本人才能操作
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -34,10 +34,12 @@ 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.OrderPlacementCommand;
|
||||||
import com.starry.admin.modules.order.module.dto.OrderPlacementResult;
|
import com.starry.admin.modules.order.module.dto.OrderPlacementResult;
|
||||||
import com.starry.admin.modules.order.module.dto.OrderRefundContext;
|
import com.starry.admin.modules.order.module.dto.OrderRefundContext;
|
||||||
|
import com.starry.admin.modules.order.module.dto.OrderRevocationContext;
|
||||||
import com.starry.admin.modules.order.module.dto.PaymentInfo;
|
import com.starry.admin.modules.order.module.dto.PaymentInfo;
|
||||||
import com.starry.admin.modules.order.module.dto.RandomOrderRequirements;
|
import com.starry.admin.modules.order.module.dto.RandomOrderRequirements;
|
||||||
import com.starry.admin.modules.order.module.entity.PlayOrderInfoEntity;
|
import com.starry.admin.modules.order.module.entity.PlayOrderInfoEntity;
|
||||||
import com.starry.admin.modules.order.module.entity.PlayOrderLogInfoEntity;
|
import com.starry.admin.modules.order.module.entity.PlayOrderLogInfoEntity;
|
||||||
|
import com.starry.admin.modules.order.module.event.OrderRevocationEvent;
|
||||||
import com.starry.admin.modules.order.module.vo.ClerkEstimatedRevenueVo;
|
import com.starry.admin.modules.order.module.vo.ClerkEstimatedRevenueVo;
|
||||||
import com.starry.admin.modules.order.service.IOrderLifecycleService;
|
import com.starry.admin.modules.order.service.IOrderLifecycleService;
|
||||||
import com.starry.admin.modules.order.service.IPlayOrderRefundInfoService;
|
import com.starry.admin.modules.order.service.IPlayOrderRefundInfoService;
|
||||||
@@ -61,9 +63,11 @@ import java.util.HashMap;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
import java.util.Optional;
|
||||||
import javax.annotation.PostConstruct;
|
import javax.annotation.PostConstruct;
|
||||||
import javax.annotation.Resource;
|
import javax.annotation.Resource;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.context.ApplicationEventPublisher;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
@@ -77,7 +81,8 @@ public class OrderLifecycleServiceImpl implements IOrderLifecycleService {
|
|||||||
private enum LifecycleOperation {
|
private enum LifecycleOperation {
|
||||||
CREATE,
|
CREATE,
|
||||||
COMPLETE,
|
COMPLETE,
|
||||||
REFUND
|
REFUND,
|
||||||
|
REVOKE_COMPLETED
|
||||||
}
|
}
|
||||||
|
|
||||||
@Resource
|
@Resource
|
||||||
@@ -110,6 +115,9 @@ public class OrderLifecycleServiceImpl implements IOrderLifecycleService {
|
|||||||
@Resource
|
@Resource
|
||||||
private IPlayBalanceDetailsInfoService playBalanceDetailsInfoService;
|
private IPlayBalanceDetailsInfoService playBalanceDetailsInfoService;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private ApplicationEventPublisher applicationEventPublisher;
|
||||||
|
|
||||||
private Map<StrategyKey, OrderPlacementStrategy> placementStrategies;
|
private Map<StrategyKey, OrderPlacementStrategy> placementStrategies;
|
||||||
|
|
||||||
@PostConstruct
|
@PostConstruct
|
||||||
@@ -520,7 +528,8 @@ public class OrderLifecycleServiceImpl implements IOrderLifecycleService {
|
|||||||
throw new CustomException("每个订单只能退款一次~");
|
throw new CustomException("每个订单只能退款一次~");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isBalancePaidOrder(order) && !playBalanceDetailsInfoService.existsCustomerConsumeRecord(order.getPurchaserBy(), order.getId())) {
|
if (isBalancePaidOrder(order)
|
||||||
|
&& !playBalanceDetailsInfoService.existsCustomerConsumeRecord(order.getPurchaserBy(), order.getId())) {
|
||||||
throw new CustomException("订单未发生余额扣款,无法退款");
|
throw new CustomException("订单未发生余额扣款,无法退款");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -603,6 +612,123 @@ public class OrderLifecycleServiceImpl implements IOrderLifecycleService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public void revokeCompletedOrder(OrderRevocationContext context) {
|
||||||
|
if (context == null || StrUtil.isBlank(context.getOrderId())) {
|
||||||
|
throw new CustomException("订单ID不能为空");
|
||||||
|
}
|
||||||
|
PlayOrderInfoEntity order = orderInfoMapper.selectById(context.getOrderId());
|
||||||
|
if (order == null) {
|
||||||
|
throw new CustomException("订单不存在");
|
||||||
|
}
|
||||||
|
if (OrderStatus.REVOKED.getCode().equals(order.getOrderStatus())) {
|
||||||
|
throw new CustomException("订单已撤销");
|
||||||
|
}
|
||||||
|
if (!OrderStatus.COMPLETED.getCode().equals(order.getOrderStatus())) {
|
||||||
|
throw new CustomException("当前状态无法撤销");
|
||||||
|
}
|
||||||
|
if (!OrderConstant.OrderType.NORMAL.getCode().equals(order.getOrderType())) {
|
||||||
|
throw new CustomException("仅支持撤销普通服务订单");
|
||||||
|
}
|
||||||
|
|
||||||
|
String operatorType = StrUtil.isNotBlank(context.getOperatorType()) ? context.getOperatorType() : OperatorType.ADMIN.getCode();
|
||||||
|
context.setOperatorType(operatorType);
|
||||||
|
String operatorId = StrUtil.isNotBlank(context.getOperatorId()) ? context.getOperatorId() : SecurityUtils.getUserId();
|
||||||
|
context.setOperatorId(operatorId);
|
||||||
|
if (context.isDeductClerkEarnings()) {
|
||||||
|
String targetClerkId = order.getAcceptBy();
|
||||||
|
if (StrUtil.isBlank(targetClerkId)) {
|
||||||
|
throw new CustomException("未找到可冲销的店员收益账号");
|
||||||
|
}
|
||||||
|
BigDecimal availableEarnings = Optional.ofNullable(
|
||||||
|
earningsService.getRemainingEarningsForOrder(order.getId(), targetClerkId))
|
||||||
|
.orElse(BigDecimal.ZERO);
|
||||||
|
if (availableEarnings.compareTo(BigDecimal.ZERO) <= 0) {
|
||||||
|
throw new CustomException("本单店员收益已全部扣回");
|
||||||
|
}
|
||||||
|
BigDecimal requested = context.getEarningsAdjustAmount();
|
||||||
|
if (requested == null || requested.compareTo(BigDecimal.ZERO) <= 0) {
|
||||||
|
requested = availableEarnings;
|
||||||
|
}
|
||||||
|
if (requested.compareTo(availableEarnings) > 0) {
|
||||||
|
throw new CustomException("扣回金额不能超过本单收益" + availableEarnings);
|
||||||
|
}
|
||||||
|
context.setEarningsAdjustAmount(requested);
|
||||||
|
} else {
|
||||||
|
context.setEarningsAdjustAmount(BigDecimal.ZERO);
|
||||||
|
}
|
||||||
|
|
||||||
|
BigDecimal finalAmount = Optional.ofNullable(order.getFinalAmount()).orElse(BigDecimal.ZERO);
|
||||||
|
BigDecimal refundAmount = context.getRefundAmount();
|
||||||
|
if (refundAmount == null) {
|
||||||
|
refundAmount = context.isRefundToCustomer() ? finalAmount : BigDecimal.ZERO;
|
||||||
|
}
|
||||||
|
if (refundAmount.compareTo(BigDecimal.ZERO) < 0) {
|
||||||
|
throw new CustomException("退款金额不能小于0");
|
||||||
|
}
|
||||||
|
if (refundAmount.compareTo(finalAmount) > 0) {
|
||||||
|
throw new CustomException("退款金额不能大于支付金额");
|
||||||
|
}
|
||||||
|
context.setRefundAmount(refundAmount);
|
||||||
|
|
||||||
|
if (refundAmount.compareTo(BigDecimal.ZERO) > 0 && context.isRefundToCustomer()) {
|
||||||
|
if (isBalancePaidOrder(order)
|
||||||
|
&& !playBalanceDetailsInfoService.existsCustomerConsumeRecord(order.getPurchaserBy(), order.getId())) {
|
||||||
|
throw new CustomException("订单未发生余额扣款,无法退款");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
UpdateWrapper<PlayOrderInfoEntity> updateWrapper = new UpdateWrapper<>();
|
||||||
|
updateWrapper.eq("id", order.getId())
|
||||||
|
.eq("order_status", OrderStatus.COMPLETED.getCode())
|
||||||
|
.set("order_status", OrderStatus.REVOKED.getCode())
|
||||||
|
.set("order_cancel_time", LocalDateTime.now())
|
||||||
|
.set("refund_amount", refundAmount)
|
||||||
|
.set("refund_reason", context.getRefundReason());
|
||||||
|
if (refundAmount.compareTo(BigDecimal.ZERO) > 0) {
|
||||||
|
updateWrapper.set("refund_type", OrderRefundFlag.REFUNDED.getCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean updated = orderInfoMapper.update(null, updateWrapper) > 0;
|
||||||
|
PlayOrderInfoEntity latest = orderInfoMapper.selectById(order.getId());
|
||||||
|
if (!updated && (latest == null || !OrderStatus.REVOKED.getCode().equals(latest.getOrderStatus()))) {
|
||||||
|
throw new CustomException("订单状态已变化,无法撤销");
|
||||||
|
}
|
||||||
|
if (latest == null) {
|
||||||
|
latest = order;
|
||||||
|
latest.setOrderStatus(OrderStatus.REVOKED.getCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (refundAmount.compareTo(BigDecimal.ZERO) > 0 && context.isRefundToCustomer()) {
|
||||||
|
OrderRefundRecordType recordType = finalAmount.compareTo(refundAmount) == 0
|
||||||
|
? OrderRefundRecordType.FULL
|
||||||
|
: OrderRefundRecordType.PARTIAL;
|
||||||
|
orderRefundInfoService.add(
|
||||||
|
latest.getId(),
|
||||||
|
latest.getPurchaserBy(),
|
||||||
|
latest.getAcceptBy(),
|
||||||
|
latest.getPayMethod(),
|
||||||
|
recordType.getCode(),
|
||||||
|
refundAmount,
|
||||||
|
context.getRefundReason(),
|
||||||
|
context.getOperatorType(),
|
||||||
|
context.getOperatorId(),
|
||||||
|
OrderRefundState.PROCESSING.getCode(),
|
||||||
|
ReviewRequirement.NOT_REQUIRED.getCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
OrderActor actor = resolveCompletionActor(context.getOperatorType());
|
||||||
|
String operationType = String.format(
|
||||||
|
"%s_%s",
|
||||||
|
LifecycleOperation.REVOKE_COMPLETED.name(),
|
||||||
|
context.isDeductClerkEarnings() ? "DEDUCT" : "KEEP");
|
||||||
|
recordOrderLog(latest, actor, context.getOperatorId(), LifecycleOperation.REVOKE_COMPLETED,
|
||||||
|
context.getRefundReason(), operationType);
|
||||||
|
|
||||||
|
applicationEventPublisher.publishEvent(new OrderRevocationEvent(context, latest));
|
||||||
|
}
|
||||||
|
|
||||||
private void validateOrderCreationRequest(OrderCreationContext context) {
|
private void validateOrderCreationRequest(OrderCreationContext context) {
|
||||||
if (context == null) {
|
if (context == null) {
|
||||||
throw new CustomException("订单创建请求不能为空");
|
throw new CustomException("订单创建请求不能为空");
|
||||||
@@ -747,6 +873,10 @@ public class OrderLifecycleServiceImpl implements IOrderLifecycleService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private boolean ensureEarnings(PlayOrderInfoEntity order, OrderTriggerSource source) {
|
private boolean ensureEarnings(PlayOrderInfoEntity order, OrderTriggerSource source) {
|
||||||
|
if (OrderConstant.OrderType.BLIND_BOX_PURCHASE.getCode().equals(order.getOrderType())) {
|
||||||
|
log.debug("Skip earnings creation for blind box purchase order {}", order.getId());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
Long existing = earningsService.lambdaQuery()
|
Long existing = earningsService.lambdaQuery()
|
||||||
.eq(EarningsLineEntity::getTenantId, order.getTenantId())
|
.eq(EarningsLineEntity::getTenantId, order.getTenantId())
|
||||||
.eq(EarningsLineEntity::getOrderId, order.getId())
|
.eq(EarningsLineEntity::getOrderId, order.getId())
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
|||||||
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
||||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||||
|
import com.baomidou.mybatisplus.core.toolkit.support.SFunction;
|
||||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||||
import com.github.yulichang.wrapper.MPJLambdaWrapper;
|
import com.github.yulichang.wrapper.MPJLambdaWrapper;
|
||||||
@@ -401,7 +402,7 @@ public class PlayOrderInfoServiceImpl extends ServiceImpl<PlayOrderInfoMapper, P
|
|||||||
public IPage<PlayOrderInfoReturnVo> selectOrderInfoPage(PlayOrderInfoQueryVo vo) {
|
public IPage<PlayOrderInfoReturnVo> selectOrderInfoPage(PlayOrderInfoQueryVo vo) {
|
||||||
|
|
||||||
MPJLambdaWrapper<PlayOrderInfoEntity> lambdaQueryWrapper = getCommonOrderQueryVo(
|
MPJLambdaWrapper<PlayOrderInfoEntity> lambdaQueryWrapper = getCommonOrderQueryVo(
|
||||||
ConvertUtil.entityToVo(vo, PlayOrderInfoEntity.class));
|
ConvertUtil.entityToVo(vo, PlayOrderInfoEntity.class), vo.getKeyword());
|
||||||
lambdaQueryWrapper.in(PlayOrderInfoEntity::getPlaceType, "0", "1", "2");
|
lambdaQueryWrapper.in(PlayOrderInfoEntity::getPlaceType, "0", "1", "2");
|
||||||
if (StringUtils.isNotBlank(vo.getGroupId())) {
|
if (StringUtils.isNotBlank(vo.getGroupId())) {
|
||||||
lambdaQueryWrapper.eq(PlayOrderInfoEntity::getGroupId, vo.getGroupId());
|
lambdaQueryWrapper.eq(PlayOrderInfoEntity::getGroupId, vo.getGroupId());
|
||||||
@@ -421,6 +422,25 @@ public class PlayOrderInfoServiceImpl extends ServiceImpl<PlayOrderInfoMapper, P
|
|||||||
if (StringUtils.isNotBlank(vo.getOrderType())) {
|
if (StringUtils.isNotBlank(vo.getOrderType())) {
|
||||||
lambdaQueryWrapper.eq(PlayOrderInfoEntity::getOrderType, vo.getOrderType());
|
lambdaQueryWrapper.eq(PlayOrderInfoEntity::getOrderType, vo.getOrderType());
|
||||||
}
|
}
|
||||||
|
if (StringUtils.isNotBlank(vo.getSex())) {
|
||||||
|
lambdaQueryWrapper.eq(PlayOrderInfoEntity::getSex, vo.getSex());
|
||||||
|
}
|
||||||
|
if (StringUtils.isNotBlank(vo.getPayMethod())) {
|
||||||
|
lambdaQueryWrapper.eq(PlayOrderInfoEntity::getPayMethod, vo.getPayMethod());
|
||||||
|
}
|
||||||
|
if (StringUtils.isNotBlank(vo.getUseCoupon())) {
|
||||||
|
lambdaQueryWrapper.eq(PlayOrderInfoEntity::getUseCoupon, vo.getUseCoupon());
|
||||||
|
}
|
||||||
|
if (StringUtils.isNotBlank(vo.getBackendEntry())) {
|
||||||
|
lambdaQueryWrapper.eq(PlayOrderInfoEntity::getBackendEntry, vo.getBackendEntry());
|
||||||
|
}
|
||||||
|
if (StringUtils.isNotBlank(vo.getFirstOrder())) {
|
||||||
|
lambdaQueryWrapper.eq(PlayOrderInfoEntity::getFirstOrder, vo.getFirstOrder());
|
||||||
|
}
|
||||||
|
applyRangeFilter(lambdaQueryWrapper, vo.getPurchaserTime(), PlayOrderInfoEntity::getPurchaserTime);
|
||||||
|
applyRangeFilter(lambdaQueryWrapper, vo.getAcceptTime(), PlayOrderInfoEntity::getAcceptTime);
|
||||||
|
applyRangeFilter(lambdaQueryWrapper, vo.getStartOrderTime(), PlayOrderInfoEntity::getOrderStartTime);
|
||||||
|
applyRangeFilter(lambdaQueryWrapper, vo.getEndOrderTime(), PlayOrderInfoEntity::getOrderEndTime);
|
||||||
// 加入组员的筛选,要么acceptBy为空,要么就在in里面
|
// 加入组员的筛选,要么acceptBy为空,要么就在in里面
|
||||||
List<String> clerkIdList = playClerkGroupInfoService.getValidClerkIdList(SecurityUtils.getLoginUser(),
|
List<String> clerkIdList = playClerkGroupInfoService.getValidClerkIdList(SecurityUtils.getLoginUser(),
|
||||||
vo.getClerkNickName());
|
vo.getClerkNickName());
|
||||||
@@ -434,7 +454,7 @@ public class PlayOrderInfoServiceImpl extends ServiceImpl<PlayOrderInfoMapper, P
|
|||||||
public PlayClerkOrderDetailsReturnVo clerkSelectOrderDetails(String clerkId, String orderId) {
|
public PlayClerkOrderDetailsReturnVo clerkSelectOrderDetails(String clerkId, String orderId) {
|
||||||
PlayOrderInfoEntity entity = new PlayOrderInfoEntity();
|
PlayOrderInfoEntity entity = new PlayOrderInfoEntity();
|
||||||
entity.setId(orderId);
|
entity.setId(orderId);
|
||||||
MPJLambdaWrapper<PlayOrderInfoEntity> lambdaQueryWrapper = getCommonOrderQueryVo(entity);
|
MPJLambdaWrapper<PlayOrderInfoEntity> lambdaQueryWrapper = getCommonOrderQueryVo(entity, null);
|
||||||
// 拼接用户等级
|
// 拼接用户等级
|
||||||
lambdaQueryWrapper.selectAs(PlayCustomLevelInfoEntity::getId, "customLevelId")
|
lambdaQueryWrapper.selectAs(PlayCustomLevelInfoEntity::getId, "customLevelId")
|
||||||
.selectAs(PlayCustomLevelInfoEntity::getName, "customLevelName");
|
.selectAs(PlayCustomLevelInfoEntity::getName, "customLevelName");
|
||||||
@@ -485,7 +505,7 @@ public class PlayOrderInfoServiceImpl extends ServiceImpl<PlayOrderInfoMapper, P
|
|||||||
@Override
|
@Override
|
||||||
public IPage<PlayClerkOrderListReturnVo> clerkSelectOrderInfoByPage(PlayClerkOrderInfoQueryVo vo) {
|
public IPage<PlayClerkOrderListReturnVo> clerkSelectOrderInfoByPage(PlayClerkOrderInfoQueryVo vo) {
|
||||||
MPJLambdaWrapper<PlayOrderInfoEntity> lambdaQueryWrapper = getCommonOrderQueryVo(
|
MPJLambdaWrapper<PlayOrderInfoEntity> lambdaQueryWrapper = getCommonOrderQueryVo(
|
||||||
ConvertUtil.entityToVo(vo, PlayOrderInfoEntity.class));
|
ConvertUtil.entityToVo(vo, PlayOrderInfoEntity.class), null);
|
||||||
// 拼接用户等级
|
// 拼接用户等级
|
||||||
lambdaQueryWrapper.selectAs(PlayCustomLevelInfoEntity::getId, "customLevelId")
|
lambdaQueryWrapper.selectAs(PlayCustomLevelInfoEntity::getId, "customLevelId")
|
||||||
.selectAs(PlayCustomLevelInfoEntity::getName, "customLevelName");
|
.selectAs(PlayCustomLevelInfoEntity::getName, "customLevelName");
|
||||||
@@ -500,7 +520,7 @@ public class PlayOrderInfoServiceImpl extends ServiceImpl<PlayOrderInfoMapper, P
|
|||||||
PlayOrderInfoEntity entity = new PlayOrderInfoEntity();
|
PlayOrderInfoEntity entity = new PlayOrderInfoEntity();
|
||||||
entity.setId(orderId);
|
entity.setId(orderId);
|
||||||
entity.setPurchaserBy(customId);
|
entity.setPurchaserBy(customId);
|
||||||
MPJLambdaWrapper<PlayOrderInfoEntity> lambdaQueryWrapper = getCommonOrderQueryVo(entity);
|
MPJLambdaWrapper<PlayOrderInfoEntity> lambdaQueryWrapper = getCommonOrderQueryVo(entity, null);
|
||||||
PlayCustomOrderDetailsReturnVo returnVo = this.baseMapper.selectJoinOne(PlayCustomOrderDetailsReturnVo.class,
|
PlayCustomOrderDetailsReturnVo returnVo = this.baseMapper.selectJoinOne(PlayCustomOrderDetailsReturnVo.class,
|
||||||
lambdaQueryWrapper);
|
lambdaQueryWrapper);
|
||||||
// 如果订单状态为退款,查询订单退款原因
|
// 如果订单状态为退款,查询订单退款原因
|
||||||
@@ -526,7 +546,12 @@ public class PlayOrderInfoServiceImpl extends ServiceImpl<PlayOrderInfoMapper, P
|
|||||||
@Override
|
@Override
|
||||||
public IPage<PlayCustomOrderListReturnVo> customSelectOrderInfoByPage(PlayCustomOrderInfoQueryVo vo) {
|
public IPage<PlayCustomOrderListReturnVo> customSelectOrderInfoByPage(PlayCustomOrderInfoQueryVo vo) {
|
||||||
MPJLambdaWrapper<PlayOrderInfoEntity> lambdaQueryWrapper = getCommonOrderQueryVo(
|
MPJLambdaWrapper<PlayOrderInfoEntity> lambdaQueryWrapper = getCommonOrderQueryVo(
|
||||||
ConvertUtil.entityToVo(vo, PlayOrderInfoEntity.class));
|
ConvertUtil.entityToVo(vo, PlayOrderInfoEntity.class), null);
|
||||||
|
if (StringUtils.isBlank(vo.getOrderType())) {
|
||||||
|
lambdaQueryWrapper.notIn(PlayOrderInfoEntity::getOrderType,
|
||||||
|
OrderConstant.OrderType.RECHARGE.getCode(),
|
||||||
|
OrderConstant.OrderType.WITHDRAWAL.getCode());
|
||||||
|
}
|
||||||
IPage<PlayCustomOrderListReturnVo> page = this.baseMapper.selectJoinPage(
|
IPage<PlayCustomOrderListReturnVo> page = this.baseMapper.selectJoinPage(
|
||||||
new Page<>(vo.getPageNum(), vo.getPageSize()), PlayCustomOrderListReturnVo.class, lambdaQueryWrapper);
|
new Page<>(vo.getPageNum(), vo.getPageSize()), PlayCustomOrderListReturnVo.class, lambdaQueryWrapper);
|
||||||
// 获取当前顾客所有订单评价信息,将订单评价信息转化为 map<订单ID,订单ID>的结构
|
// 获取当前顾客所有订单评价信息,将订单评价信息转化为 map<订单ID,订单ID>的结构
|
||||||
@@ -686,7 +711,7 @@ public class PlayOrderInfoServiceImpl extends ServiceImpl<PlayOrderInfoMapper, P
|
|||||||
*
|
*
|
||||||
* @return MPJLambdaWrapper<PlayOrderInfoEntity>
|
* @return MPJLambdaWrapper<PlayOrderInfoEntity>
|
||||||
*/
|
*/
|
||||||
public MPJLambdaWrapper<PlayOrderInfoEntity> getCommonOrderQueryVo(PlayOrderInfoEntity entity) {
|
public MPJLambdaWrapper<PlayOrderInfoEntity> getCommonOrderQueryVo(PlayOrderInfoEntity entity, String keyword) {
|
||||||
MPJLambdaWrapper<PlayOrderInfoEntity> lambdaQueryWrapper = new MPJLambdaWrapper<>();
|
MPJLambdaWrapper<PlayOrderInfoEntity> lambdaQueryWrapper = new MPJLambdaWrapper<>();
|
||||||
// 查询主表全部字段
|
// 查询主表全部字段
|
||||||
lambdaQueryWrapper.selectAll(PlayOrderInfoEntity.class);
|
lambdaQueryWrapper.selectAll(PlayOrderInfoEntity.class);
|
||||||
@@ -723,11 +748,43 @@ public class PlayOrderInfoServiceImpl extends ServiceImpl<PlayOrderInfoMapper, P
|
|||||||
lambdaQueryWrapper.eq(PlayOrderInfoEntity::getOrderType, entity.getOrderType());
|
lambdaQueryWrapper.eq(PlayOrderInfoEntity::getOrderType, entity.getOrderType());
|
||||||
}
|
}
|
||||||
lambdaQueryWrapper.like(StringUtils.isNotEmpty(entity.getOrderNo()), PlayOrderInfoEntity::getOrderNo, entity.getOrderNo());
|
lambdaQueryWrapper.like(StringUtils.isNotEmpty(entity.getOrderNo()), PlayOrderInfoEntity::getOrderNo, entity.getOrderNo());
|
||||||
|
if (StringUtils.isNotBlank(keyword)) {
|
||||||
|
lambdaQueryWrapper.and(w -> w.like(PlayOrderInfoEntity::getOrderNo, keyword)
|
||||||
|
.or()
|
||||||
|
.like(PlayClerkUserInfoEntity::getNickname, keyword));
|
||||||
|
}
|
||||||
lambdaQueryWrapper.orderByDesc(PlayOrderInfoEntity::getCreatedTime);
|
lambdaQueryWrapper.orderByDesc(PlayOrderInfoEntity::getCreatedTime);
|
||||||
return lambdaQueryWrapper;
|
return lambdaQueryWrapper;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void applyRangeFilter(MPJLambdaWrapper<PlayOrderInfoEntity> wrapper, List<String> range,
|
||||||
|
SFunction<PlayOrderInfoEntity, ?> column) {
|
||||||
|
if (CollectionUtil.isEmpty(range)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String start = normalizeRangeValue(range, 0, false);
|
||||||
|
String end = normalizeRangeValue(range, 1, true);
|
||||||
|
if (StrUtil.isNotBlank(start) && StrUtil.isNotBlank(end)) {
|
||||||
|
wrapper.between(column, start, end);
|
||||||
|
} else if (StrUtil.isNotBlank(start)) {
|
||||||
|
wrapper.ge(column, start);
|
||||||
|
} else if (StrUtil.isNotBlank(end)) {
|
||||||
|
wrapper.le(column, end);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String normalizeRangeValue(List<String> range, int index, boolean isEnd) {
|
||||||
|
if (range.size() <= index) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
String raw = range.get(index);
|
||||||
|
if (StrUtil.isBlank(raw)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return isEnd ? DateRangeUtils.normalizeEndOptional(raw) : DateRangeUtils.normalizeStartOptional(raw);
|
||||||
|
}
|
||||||
|
|
||||||
public void updateStateTo23(String operatorByType, String operatorBy, String orderState, String orderId) {
|
public void updateStateTo23(String operatorByType, String operatorBy, String orderState, String orderId) {
|
||||||
OperatorType operatorType = resolveOperatorTypeOrThrow(operatorByType);
|
OperatorType operatorType = resolveOperatorTypeOrThrow(operatorByType);
|
||||||
boolean isCustomer = operatorType == OperatorType.CUSTOMER;
|
boolean isCustomer = operatorType == OperatorType.CUSTOMER;
|
||||||
@@ -896,6 +953,14 @@ public class PlayOrderInfoServiceImpl extends ServiceImpl<PlayOrderInfoMapper, P
|
|||||||
notificationSender.sendOrderCancelMessageAsync(latest, refundReason);
|
notificationSender.sendOrderCancelMessageAsync(latest, refundReason);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void revokeCompletedOrder(OrderRevocationContext context) {
|
||||||
|
if (context == null || StrUtil.isBlank(context.getOrderId())) {
|
||||||
|
throw new CustomException("订单信息缺失");
|
||||||
|
}
|
||||||
|
orderLifecycleService.revokeCompletedOrder(context);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public PlayOrderInfoEntity queryByOrderNo(String orderNo) {
|
public PlayOrderInfoEntity queryByOrderNo(String orderNo) {
|
||||||
LambdaQueryWrapper<PlayOrderInfoEntity> lambdaQueryWrapper = new LambdaQueryWrapper<>();
|
LambdaQueryWrapper<PlayOrderInfoEntity> lambdaQueryWrapper = new LambdaQueryWrapper<>();
|
||||||
|
|||||||
@@ -109,9 +109,13 @@ public class PlayCommodityInfoController {
|
|||||||
if (!jsonObject.containsKey(playClerkLevelInfoEntity.getId())) {
|
if (!jsonObject.containsKey(playClerkLevelInfoEntity.getId())) {
|
||||||
throw new CustomException("请求参数错误");
|
throw new CustomException("请求参数错误");
|
||||||
}
|
}
|
||||||
|
String rawPrice = jsonObject.getString(playClerkLevelInfoEntity.getId());
|
||||||
|
if (rawPrice == null || rawPrice.trim().isEmpty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
double price = 0.0;
|
double price = 0.0;
|
||||||
try {
|
try {
|
||||||
price = Double.parseDouble(jsonObject.getString(playClerkLevelInfoEntity.getId()));
|
price = Double.parseDouble(rawPrice);
|
||||||
} catch (RuntimeException e) {
|
} catch (RuntimeException e) {
|
||||||
throw new CustomException("请求参数错误,价格格式为空");
|
throw new CustomException("请求参数错误,价格格式为空");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import com.baomidou.mybatisplus.core.metadata.IPage;
|
|||||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||||
import com.github.yulichang.wrapper.MPJLambdaWrapper;
|
import com.github.yulichang.wrapper.MPJLambdaWrapper;
|
||||||
|
import com.starry.admin.common.PageBuilder;
|
||||||
import com.starry.admin.common.exception.CustomException;
|
import com.starry.admin.common.exception.CustomException;
|
||||||
import com.starry.admin.modules.custom.module.entity.PlayCustomGiftInfoEntity;
|
import com.starry.admin.modules.custom.module.entity.PlayCustomGiftInfoEntity;
|
||||||
import com.starry.admin.modules.custom.service.IPlayCustomGiftInfoService;
|
import com.starry.admin.modules.custom.service.IPlayCustomGiftInfoService;
|
||||||
@@ -16,6 +17,7 @@ import com.starry.admin.modules.shop.service.IPlayClerkGiftInfoService;
|
|||||||
import com.starry.admin.modules.shop.service.IPlayGiftInfoService;
|
import com.starry.admin.modules.shop.service.IPlayGiftInfoService;
|
||||||
import com.starry.admin.modules.weichat.entity.gift.PlayClerkGiftReturnVo;
|
import com.starry.admin.modules.weichat.entity.gift.PlayClerkGiftReturnVo;
|
||||||
import com.starry.common.utils.IdUtils;
|
import com.starry.common.utils.IdUtils;
|
||||||
|
import com.starry.common.utils.StringUtils;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -165,8 +167,11 @@ public class PlayGiftInfoServiceImpl extends ServiceImpl<PlayGiftInfoMapper, Pla
|
|||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public IPage<PlayGiftInfoEntity> selectPlayGiftInfoByPage(PlayGiftInfoEntity playGiftInfo) {
|
public IPage<PlayGiftInfoEntity> selectPlayGiftInfoByPage(PlayGiftInfoEntity playGiftInfo) {
|
||||||
Page<PlayGiftInfoEntity> page = new Page<>(1, 10);
|
Page<PlayGiftInfoEntity> page = PageBuilder.build();
|
||||||
return this.baseMapper.selectPage(page, new LambdaQueryWrapper<>());
|
LambdaQueryWrapper<PlayGiftInfoEntity> wrapper = new LambdaQueryWrapper<>();
|
||||||
|
wrapper.like(StringUtils.isNotBlank(playGiftInfo.getName()), PlayGiftInfoEntity::getName, playGiftInfo.getName());
|
||||||
|
wrapper.eq(StringUtils.isNotBlank(playGiftInfo.getState()), PlayGiftInfoEntity::getState, playGiftInfo.getState());
|
||||||
|
return this.baseMapper.selectPage(page, wrapper);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
package com.starry.admin.modules.weichat.assembler;
|
||||||
|
|
||||||
|
import com.starry.admin.modules.clerk.module.entity.PlayClerkMediaAssetEntity;
|
||||||
|
import com.starry.admin.modules.media.entity.PlayMediaEntity;
|
||||||
|
import com.starry.admin.modules.weichat.entity.clerk.MediaVo;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
public final class ClerkMediaAssembler {
|
||||||
|
|
||||||
|
private ClerkMediaAssembler() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static MediaVo toVo(PlayMediaEntity media, PlayClerkMediaAssetEntity asset) {
|
||||||
|
if (media == null || asset == null || Boolean.TRUE.equals(asset.getDeleted())) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
MediaVo vo = new MediaVo();
|
||||||
|
vo.setId(media.getId());
|
||||||
|
vo.setMediaId(media.getId());
|
||||||
|
vo.setAssetId(asset.getId());
|
||||||
|
vo.setKind(media.getKind());
|
||||||
|
vo.setStatus(media.getStatus());
|
||||||
|
vo.setUrl(media.getUrl());
|
||||||
|
vo.setCoverUrl(media.getCoverUrl());
|
||||||
|
vo.setDurationMs(media.getDurationMs());
|
||||||
|
vo.setWidth(media.getWidth());
|
||||||
|
vo.setHeight(media.getHeight());
|
||||||
|
vo.setSizeBytes(media.getSizeBytes());
|
||||||
|
vo.setOrderIndex(asset.getOrderIndex());
|
||||||
|
vo.setMetadata(media.getMetadata());
|
||||||
|
vo.setUsage(asset.getUsage());
|
||||||
|
vo.setReviewState(asset.getReviewState());
|
||||||
|
vo.setSubmittedTime(asset.getSubmittedTime());
|
||||||
|
vo.setReviewNote(asset.getNote());
|
||||||
|
return vo;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<MediaVo> toVoList(List<PlayClerkMediaAssetEntity> assets,
|
||||||
|
Map<String, PlayMediaEntity> mediaById) {
|
||||||
|
if (assets == null || assets.isEmpty()) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
return assets.stream()
|
||||||
|
.map(asset -> toVo(mediaById.get(asset.getMediaId()), asset))
|
||||||
|
.filter(Objects::nonNull)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
package com.starry.admin.modules.weichat.assembler;
|
||||||
|
|
||||||
|
import com.starry.admin.modules.media.entity.PlayMediaEntity;
|
||||||
|
import com.starry.admin.modules.weichat.entity.clerk.MediaVo;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
public final class MediaAssembler {
|
||||||
|
|
||||||
|
private MediaAssembler() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static MediaVo toVo(PlayMediaEntity entity) {
|
||||||
|
if (entity == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
MediaVo vo = new MediaVo();
|
||||||
|
vo.setId(entity.getId());
|
||||||
|
vo.setMediaId(entity.getId());
|
||||||
|
vo.setKind(entity.getKind());
|
||||||
|
vo.setStatus(entity.getStatus());
|
||||||
|
vo.setUrl(entity.getUrl());
|
||||||
|
vo.setCoverUrl(entity.getCoverUrl());
|
||||||
|
vo.setDurationMs(entity.getDurationMs());
|
||||||
|
vo.setWidth(entity.getWidth());
|
||||||
|
vo.setHeight(entity.getHeight());
|
||||||
|
vo.setSizeBytes(entity.getSizeBytes());
|
||||||
|
vo.setOrderIndex(entity.getOrderIndex());
|
||||||
|
vo.setMetadata(entity.getMetadata());
|
||||||
|
return vo;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<MediaVo> toVoList(List<PlayMediaEntity> entities) {
|
||||||
|
if (entities == null) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
return entities.stream()
|
||||||
|
.filter(Objects::nonNull)
|
||||||
|
.map(MediaAssembler::toVo)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,12 +6,16 @@ import com.baomidou.mybatisplus.core.metadata.IPage;
|
|||||||
import com.starry.admin.common.aspect.ClerkUserLogin;
|
import com.starry.admin.common.aspect.ClerkUserLogin;
|
||||||
import com.starry.admin.common.conf.ThreadLocalRequestDetail;
|
import com.starry.admin.common.conf.ThreadLocalRequestDetail;
|
||||||
import com.starry.admin.common.exception.CustomException;
|
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.module.entity.*;
|
import com.starry.admin.modules.clerk.module.entity.*;
|
||||||
import com.starry.admin.modules.clerk.module.vo.PlayClerkCommodityEditVo;
|
import com.starry.admin.modules.clerk.module.vo.PlayClerkCommodityEditVo;
|
||||||
import com.starry.admin.modules.clerk.module.vo.PlayClerkCommodityQueryVo;
|
import com.starry.admin.modules.clerk.module.vo.PlayClerkCommodityQueryVo;
|
||||||
import com.starry.admin.modules.clerk.service.*;
|
import com.starry.admin.modules.clerk.service.*;
|
||||||
import com.starry.admin.modules.clerk.service.impl.PlayClerkUserInfoServiceImpl;
|
import com.starry.admin.modules.clerk.service.impl.PlayClerkUserInfoServiceImpl;
|
||||||
import com.starry.admin.modules.clerk.service.impl.PlayClerkUserReviewInfoServiceImpl;
|
import com.starry.admin.modules.clerk.service.impl.PlayClerkUserReviewInfoServiceImpl;
|
||||||
|
import com.starry.admin.modules.media.enums.MediaOwnerType;
|
||||||
|
import com.starry.admin.modules.media.service.IPlayMediaService;
|
||||||
import com.starry.admin.modules.order.module.constant.OrderConstant.OperatorType;
|
import com.starry.admin.modules.order.module.constant.OrderConstant.OperatorType;
|
||||||
import com.starry.admin.modules.order.module.entity.PlayOrderInfoEntity;
|
import com.starry.admin.modules.order.module.entity.PlayOrderInfoEntity;
|
||||||
import com.starry.admin.modules.order.module.vo.PlayOrderCompleteVo;
|
import com.starry.admin.modules.order.module.vo.PlayOrderCompleteVo;
|
||||||
@@ -41,6 +45,7 @@ import com.starry.admin.utils.SecurityUtils;
|
|||||||
import com.starry.admin.utils.SmsUtils;
|
import com.starry.admin.utils.SmsUtils;
|
||||||
import com.starry.common.redis.RedisCache;
|
import com.starry.common.redis.RedisCache;
|
||||||
import com.starry.common.result.R;
|
import com.starry.common.result.R;
|
||||||
|
import com.starry.common.result.TypedR;
|
||||||
import com.starry.common.utils.ConvertUtil;
|
import com.starry.common.utils.ConvertUtil;
|
||||||
import com.starry.common.utils.StringUtils;
|
import com.starry.common.utils.StringUtils;
|
||||||
import com.starry.common.utils.VerificationCodeUtils;
|
import com.starry.common.utils.VerificationCodeUtils;
|
||||||
@@ -53,6 +58,7 @@ import java.time.LocalDateTime;
|
|||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
import java.util.LinkedHashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
@@ -120,6 +126,10 @@ public class WxClerkController {
|
|||||||
private SmsUtils smsUtils;
|
private SmsUtils smsUtils;
|
||||||
@Resource
|
@Resource
|
||||||
private WxCustomMpService wxCustomMpService;
|
private WxCustomMpService wxCustomMpService;
|
||||||
|
@Resource
|
||||||
|
private com.starry.admin.modules.clerk.service.IPlayClerkMediaAssetService clerkMediaAssetService;
|
||||||
|
@Resource
|
||||||
|
private IPlayMediaService mediaService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 店员获取个人业绩信息
|
* 店员获取个人业绩信息
|
||||||
@@ -268,7 +278,7 @@ public class WxClerkController {
|
|||||||
entity.setReviewState("0");
|
entity.setReviewState("0");
|
||||||
entity.setDataContent(Collections.singletonList(vo.getNickname()));
|
entity.setDataContent(Collections.singletonList(vo.getNickname()));
|
||||||
playClerkDataReviewInfoService.create(entity);
|
playClerkDataReviewInfoService.create(entity);
|
||||||
return R.ok("提交成功,等待审核~");
|
return R.ok().message("提交成功,等待审核~");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ApiOperation(value = "更新性别", notes = "店员更新性别")
|
@ApiOperation(value = "更新性别", notes = "店员更新性别")
|
||||||
@@ -283,7 +293,7 @@ public class WxClerkController {
|
|||||||
entity.setReviewState("0");
|
entity.setReviewState("0");
|
||||||
entity.setDataContent(Collections.singletonList(String.valueOf(vo.getSex())));
|
entity.setDataContent(Collections.singletonList(String.valueOf(vo.getSex())));
|
||||||
playClerkDataReviewInfoService.create(entity);
|
playClerkDataReviewInfoService.create(entity);
|
||||||
return R.ok("提交成功,等待审核~");
|
return R.ok().message("提交成功,等待审核~");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ApiOperation(value = "更新头像", notes = "店员更新头像")
|
@ApiOperation(value = "更新头像", notes = "店员更新头像")
|
||||||
@@ -305,25 +315,138 @@ public class WxClerkController {
|
|||||||
list.add(vo.getAvatar());
|
list.add(vo.getAvatar());
|
||||||
entity.setDataContent(list);
|
entity.setDataContent(list);
|
||||||
playClerkDataReviewInfoService.create(entity);
|
playClerkDataReviewInfoService.create(entity);
|
||||||
return R.ok("提交成功,等待审核~");
|
return R.ok().message("提交成功,等待审核~");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ClerkUserLogin
|
@ClerkUserLogin
|
||||||
@PostMapping("/user/updateAlbum")
|
@PostMapping("/user/updateAlbum")
|
||||||
public R updateAlbum(@Validated @RequestBody PlayClerkUserAlbumVo vo) {
|
public R updateAlbum(@Validated @RequestBody PlayClerkUserAlbumVo vo) {
|
||||||
PlayClerkUserInfoEntity userInfo = ThreadLocalRequestDetail.getClerkUserInfo();
|
PlayClerkUserInfoEntity userInfo = ThreadLocalRequestDetail.getClerkUserInfo();
|
||||||
// PlayClerkDataReviewInfoEntity entity =
|
List<String> requested = vo.getAlbum() == null ? new ArrayList<>() : vo.getAlbum().stream()
|
||||||
// playClerkDataReviewInfoService.queryByClerkId(userInfo.getId(), "2", "0");
|
.filter(StrUtil::isNotBlank)
|
||||||
// if (entity != null) {
|
.map(String::trim)
|
||||||
// throw new CustomException("已有申请未审核");
|
.distinct()
|
||||||
// }
|
.collect(Collectors.toList());
|
||||||
PlayClerkDataReviewInfoEntity entity = new PlayClerkDataReviewInfoEntity();
|
|
||||||
entity.setClerkId(userInfo.getId());
|
// 查询当前所有已审核通过的 PROFILE 媒资
|
||||||
entity.setDataType("2");
|
List<PlayClerkMediaAssetEntity> approvedAssets = clerkMediaAssetService.listByState(
|
||||||
entity.setReviewState("0");
|
userInfo.getId(),
|
||||||
entity.setDataContent(vo.getAlbum());
|
ClerkMediaUsage.PROFILE,
|
||||||
playClerkDataReviewInfoService.create(entity);
|
Collections.singletonList(ClerkMediaReviewState.APPROVED));
|
||||||
return R.ok("提交成功,等待审核~");
|
|
||||||
|
LinkedHashSet<String> requestedSet = new LinkedHashSet<>(requested);
|
||||||
|
if (requestedSet.isEmpty()) {
|
||||||
|
throw new CustomException("最少上传一张照片");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算哪些是新媒资(需走审核),哪些是纯删除/排序
|
||||||
|
java.util.Set<String> approvedIds = approvedAssets.stream()
|
||||||
|
.map(PlayClerkMediaAssetEntity::getMediaId)
|
||||||
|
.filter(StrUtil::isNotBlank)
|
||||||
|
.collect(java.util.stream.Collectors.toSet());
|
||||||
|
java.util.Set<String> newMediaIds = requestedSet.stream()
|
||||||
|
.filter(id -> !approvedIds.contains(id))
|
||||||
|
.collect(java.util.stream.Collectors.toSet());
|
||||||
|
|
||||||
|
if (log.isInfoEnabled()) {
|
||||||
|
log.info("[ClerkAlbumUpdate] clerkId={} tenantId={} requestedSet={} approvedIds={} newMediaIds={}",
|
||||||
|
userInfo.getId(), userInfo.getTenantId(), requestedSet, approvedIds, newMediaIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!newMediaIds.isEmpty()) {
|
||||||
|
// 新增媒资必须是当前店员本人名下、已就绪的媒资,才能进入审核流程
|
||||||
|
java.util.List<com.starry.admin.modules.media.entity.PlayMediaEntity> newMediaEntities =
|
||||||
|
mediaService.lambdaQuery()
|
||||||
|
.in(com.starry.admin.modules.media.entity.PlayMediaEntity::getId, newMediaIds)
|
||||||
|
.list();
|
||||||
|
|
||||||
|
java.util.Set<String> existingMediaIds = newMediaEntities.stream()
|
||||||
|
.map(com.starry.admin.modules.media.entity.PlayMediaEntity::getId)
|
||||||
|
.collect(java.util.stream.Collectors.toSet());
|
||||||
|
java.util.Set<String> missingMediaIds = new java.util.HashSet<>(newMediaIds);
|
||||||
|
missingMediaIds.removeAll(existingMediaIds);
|
||||||
|
|
||||||
|
if (!missingMediaIds.isEmpty()) {
|
||||||
|
// 这里很可能是历史相册里的纯 URL(未经过媒资化),我们记录日志但不直接失败,
|
||||||
|
// 在审核内容中仍然保留这些字符串,由审核端用回显逻辑处理。
|
||||||
|
log.warn(
|
||||||
|
"[ClerkAlbumUpdate] some album entries not found in play_media, treating as legacy values, clerkId={} tenantId={} missingIds={} existingIds={}",
|
||||||
|
userInfo.getId(),
|
||||||
|
userInfo.getTenantId(),
|
||||||
|
missingMediaIds,
|
||||||
|
existingMediaIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (log.isInfoEnabled()) {
|
||||||
|
log.info(
|
||||||
|
"[ClerkAlbumUpdate] loaded newMediaEntities for validation, clerkId={} tenantId={} mediaSummaries={}",
|
||||||
|
userInfo.getId(),
|
||||||
|
userInfo.getTenantId(),
|
||||||
|
newMediaEntities.stream()
|
||||||
|
.map(m -> String.format("id=%s,status=%s,ownerType=%s,ownerId=%s,tenantId=%s",
|
||||||
|
m.getId(), m.getStatus(), m.getOwnerType(), m.getOwnerId(), m.getTenantId()))
|
||||||
|
.collect(java.util.stream.Collectors.toList()));
|
||||||
|
}
|
||||||
|
|
||||||
|
for (com.starry.admin.modules.media.entity.PlayMediaEntity media : newMediaEntities) {
|
||||||
|
boolean tenantMatched = userInfo.getTenantId().equals(media.getTenantId());
|
||||||
|
boolean ownerTypeMatched = MediaOwnerType.CLERK.equals(media.getOwnerType());
|
||||||
|
boolean ownerIdMatched = userInfo.getId().equals(media.getOwnerId());
|
||||||
|
boolean statusReady = com.starry.admin.modules.media.enums.MediaStatus.READY.getCode()
|
||||||
|
.equals(media.getStatus());
|
||||||
|
|
||||||
|
if (!tenantMatched || !ownerTypeMatched || !ownerIdMatched || !statusReady) {
|
||||||
|
log.warn(
|
||||||
|
"[ClerkAlbumUpdate] invalid new media for clerk, clerkId={} tenantId={} mediaId={} mediaStatus={} mediaTenantId={} mediaOwnerType={} mediaOwnerId={} tenantMatched={} ownerTypeMatched={} ownerIdMatched={} statusReady={}",
|
||||||
|
userInfo.getId(),
|
||||||
|
userInfo.getTenantId(),
|
||||||
|
media.getId(),
|
||||||
|
media.getStatus(),
|
||||||
|
media.getTenantId(),
|
||||||
|
media.getOwnerType(),
|
||||||
|
media.getOwnerId(),
|
||||||
|
tenantMatched,
|
||||||
|
ownerTypeMatched,
|
||||||
|
ownerIdMatched,
|
||||||
|
statusReady);
|
||||||
|
throw new CustomException("存在无效的照片/视频,请刷新后重试");
|
||||||
|
}
|
||||||
|
if (!statusReady) {
|
||||||
|
log.warn(
|
||||||
|
"[ClerkAlbumUpdate] media not in READY state for clerk, clerkId={} tenantId={} mediaId={} mediaStatus={}",
|
||||||
|
userInfo.getId(),
|
||||||
|
userInfo.getTenantId(),
|
||||||
|
media.getId(),
|
||||||
|
media.getStatus());
|
||||||
|
throw new CustomException("存在未完成上传的照片/视频,请稍后重试");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 只要存在新增媒资,则按原有逻辑走资料审核,由审核通过时统一生效
|
||||||
|
PlayClerkDataReviewInfoEntity entity = new PlayClerkDataReviewInfoEntity();
|
||||||
|
entity.setClerkId(userInfo.getId());
|
||||||
|
entity.setDataType("2");
|
||||||
|
entity.setReviewState("0");
|
||||||
|
entity.setDataContent(new ArrayList<>(requestedSet));
|
||||||
|
playClerkDataReviewInfoService.create(entity);
|
||||||
|
return R.ok().message("提交成功,等待审核~");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 仅删除/排序:直接应用变更,不再生成审核记录
|
||||||
|
// 先根据新的顺序更新 orderIndex
|
||||||
|
clerkMediaAssetService.reorder(userInfo.getId(), ClerkMediaUsage.PROFILE, new ArrayList<>(requestedSet));
|
||||||
|
|
||||||
|
// 再对不再保留的媒资执行软删除
|
||||||
|
java.util.Set<String> requestedOnly = new java.util.HashSet<>(requestedSet);
|
||||||
|
java.util.Set<String> deletedMediaIds = approvedIds.stream()
|
||||||
|
.filter(id -> !requestedOnly.contains(id))
|
||||||
|
.collect(java.util.stream.Collectors.toSet());
|
||||||
|
for (String mediaId : deletedMediaIds) {
|
||||||
|
clerkMediaAssetService.softDelete(userInfo.getId(), mediaId);
|
||||||
|
mediaService.softDelete(MediaOwnerType.CLERK, userInfo.getId(), mediaId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return R.ok().message("修改成功");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ClerkUserLogin
|
@ClerkUserLogin
|
||||||
@@ -343,7 +466,7 @@ public class WxClerkController {
|
|||||||
list.add(vo.getAudio());
|
list.add(vo.getAudio());
|
||||||
entity.setDataContent(list);
|
entity.setDataContent(list);
|
||||||
playClerkDataReviewInfoService.create(entity);
|
playClerkDataReviewInfoService.create(entity);
|
||||||
return R.ok("提交成功,等待审核~");
|
return R.ok().message("提交成功,等待审核~");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ClerkUserLogin
|
@ClerkUserLogin
|
||||||
@@ -394,10 +517,10 @@ public class WxClerkController {
|
|||||||
* @return 店员列表
|
* @return 店员列表
|
||||||
*/
|
*/
|
||||||
@PostMapping("/user/queryByPage")
|
@PostMapping("/user/queryByPage")
|
||||||
public R queryByPage(@RequestBody PlayClerkUserInfoQueryVo vo) {
|
public TypedR<IPage<PlayClerkUserInfoResultVo>> queryByPage(@RequestBody PlayClerkUserInfoQueryVo vo) {
|
||||||
IPage<PlayClerkUserInfoResultVo> page = playClerkUserInfoService.selectByPage(vo,
|
IPage<PlayClerkUserInfoResultVo> page = playClerkUserInfoService.selectByPage(vo,
|
||||||
customUserService.getLoginUserId());
|
customUserService.getLoginUserId());
|
||||||
return R.ok(page);
|
return TypedR.ok(page);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -0,0 +1,121 @@
|
|||||||
|
package com.starry.admin.modules.weichat.controller;
|
||||||
|
|
||||||
|
import cn.hutool.core.collection.CollUtil;
|
||||||
|
import com.starry.admin.common.aspect.ClerkUserLogin;
|
||||||
|
import com.starry.admin.common.conf.ThreadLocalRequestDetail;
|
||||||
|
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.module.entity.PlayClerkMediaAssetEntity;
|
||||||
|
import com.starry.admin.modules.clerk.module.entity.PlayClerkUserInfoEntity;
|
||||||
|
import com.starry.admin.modules.clerk.service.IPlayClerkMediaAssetService;
|
||||||
|
import com.starry.admin.modules.media.entity.PlayMediaEntity;
|
||||||
|
import com.starry.admin.modules.media.enums.MediaOwnerType;
|
||||||
|
import com.starry.admin.modules.media.service.IPlayMediaService;
|
||||||
|
import com.starry.admin.modules.weichat.assembler.ClerkMediaAssembler;
|
||||||
|
import com.starry.admin.modules.weichat.entity.clerk.MediaOrderRequest;
|
||||||
|
import com.starry.admin.modules.weichat.entity.clerk.MediaVo;
|
||||||
|
import com.starry.admin.modules.weichat.service.MediaUploadService;
|
||||||
|
import com.starry.common.result.R;
|
||||||
|
import io.swagger.annotations.Api;
|
||||||
|
import io.swagger.annotations.ApiOperation;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import javax.validation.Valid;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.validation.annotation.Validated;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
@Api(tags = "店员媒资接口")
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/wx/clerk/media")
|
||||||
|
@Validated
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class WxClerkMediaController {
|
||||||
|
|
||||||
|
private final MediaUploadService mediaUploadService;
|
||||||
|
private final IPlayMediaService mediaService;
|
||||||
|
private final IPlayClerkMediaAssetService clerkMediaAssetService;
|
||||||
|
|
||||||
|
@ApiOperation("上传媒资(图片/视频)")
|
||||||
|
@PostMapping("/upload")
|
||||||
|
@ClerkUserLogin
|
||||||
|
public R upload(@RequestParam("file") MultipartFile file,
|
||||||
|
@RequestParam(value = "usage", required = false) String usageCode) {
|
||||||
|
PlayClerkUserInfoEntity clerkInfo = requireClerkInfo();
|
||||||
|
MediaVo vo = mediaUploadService.upload(file, clerkInfo, ClerkMediaUsage.fromCode(usageCode));
|
||||||
|
return R.ok(vo);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiOperation("更新媒资顺序并提交保留列表")
|
||||||
|
@PutMapping("/order")
|
||||||
|
@ClerkUserLogin
|
||||||
|
public R updateOrder(@Valid @RequestBody MediaOrderRequest request) {
|
||||||
|
PlayClerkUserInfoEntity clerkInfo = requireClerkInfo();
|
||||||
|
ClerkMediaUsage usage = ClerkMediaUsage.fromCode(request.getUsage());
|
||||||
|
List<String> mediaIds = CollUtil.isEmpty(request.getMediaIds()) ? Collections.emptyList()
|
||||||
|
: request.getMediaIds().stream().distinct().collect(Collectors.toList());
|
||||||
|
clerkMediaAssetService.submitWithOrder(clerkInfo.getId(), usage, mediaIds);
|
||||||
|
return R.ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiOperation("删除媒资(软删除)")
|
||||||
|
@DeleteMapping("/{id}")
|
||||||
|
@ClerkUserLogin
|
||||||
|
public R delete(@PathVariable("id") String mediaId) {
|
||||||
|
PlayClerkUserInfoEntity clerkInfo = requireClerkInfo();
|
||||||
|
clerkMediaAssetService.softDelete(clerkInfo.getId(), mediaId);
|
||||||
|
mediaService.softDelete(MediaOwnerType.CLERK, clerkInfo.getId(), mediaId);
|
||||||
|
return R.ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiOperation("查询草稿媒资列表")
|
||||||
|
@GetMapping("/list")
|
||||||
|
@ClerkUserLogin
|
||||||
|
public R listDraft(@RequestParam(value = "usage", required = false) String usageCode) {
|
||||||
|
PlayClerkUserInfoEntity clerkInfo = requireClerkInfo();
|
||||||
|
ClerkMediaUsage usage = ClerkMediaUsage.fromCode(usageCode);
|
||||||
|
List<PlayClerkMediaAssetEntity> assets = clerkMediaAssetService.listByState(clerkInfo.getId(), usage,
|
||||||
|
Arrays.asList(ClerkMediaReviewState.DRAFT, ClerkMediaReviewState.PENDING,
|
||||||
|
ClerkMediaReviewState.REJECTED));
|
||||||
|
Map<String, PlayMediaEntity> mediaMap = loadMediaMap(assets);
|
||||||
|
return R.ok(ClerkMediaAssembler.toVoList(assets, mediaMap));
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiOperation("查询已审核通过的媒资")
|
||||||
|
@GetMapping("/approved")
|
||||||
|
@ClerkUserLogin
|
||||||
|
public R listApproved(@RequestParam(value = "usage", required = false) String usageCode) {
|
||||||
|
PlayClerkUserInfoEntity clerkInfo = requireClerkInfo();
|
||||||
|
ClerkMediaUsage usage = ClerkMediaUsage.fromCode(usageCode);
|
||||||
|
List<PlayClerkMediaAssetEntity> assets = clerkMediaAssetService.listByState(clerkInfo.getId(), usage,
|
||||||
|
Collections.singletonList(ClerkMediaReviewState.APPROVED));
|
||||||
|
Map<String, PlayMediaEntity> mediaMap = loadMediaMap(assets);
|
||||||
|
return R.ok(ClerkMediaAssembler.toVoList(assets, mediaMap));
|
||||||
|
}
|
||||||
|
|
||||||
|
private PlayClerkUserInfoEntity requireClerkInfo() {
|
||||||
|
PlayClerkUserInfoEntity clerk = ThreadLocalRequestDetail.getClerkUserInfo();
|
||||||
|
if (clerk == null) {
|
||||||
|
throw new CustomException("店员未登录");
|
||||||
|
}
|
||||||
|
return clerk;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, PlayMediaEntity> loadMediaMap(List<PlayClerkMediaAssetEntity> assets) {
|
||||||
|
if (CollUtil.isEmpty(assets)) {
|
||||||
|
return Collections.emptyMap();
|
||||||
|
}
|
||||||
|
List<String> mediaIds = assets.stream().map(PlayClerkMediaAssetEntity::getMediaId).distinct()
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
List<PlayMediaEntity> mediaList = mediaService.listByIds(mediaIds);
|
||||||
|
if (CollUtil.isEmpty(mediaList)) {
|
||||||
|
return Collections.emptyMap();
|
||||||
|
}
|
||||||
|
return mediaList.stream().collect(Collectors.toMap(PlayMediaEntity::getId, item -> item));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -151,16 +151,9 @@ public class WxCustomController {
|
|||||||
@ApiResponses({@ApiResponse(code = 200, message = "操作成功", response = PlayClerkUserInfoResultVo.class)})
|
@ApiResponses({@ApiResponse(code = 200, message = "操作成功", response = PlayClerkUserInfoResultVo.class)})
|
||||||
@GetMapping("/queryClerkDetailedById")
|
@GetMapping("/queryClerkDetailedById")
|
||||||
public R queryClerkDetailedById(@RequestParam("id") String id) {
|
public R queryClerkDetailedById(@RequestParam("id") String id) {
|
||||||
PlayClerkUserInfoEntity entity = clerkUserInfoService.selectById(id);
|
|
||||||
PlayClerkUserInfoResultVo vo = ConvertUtil.entityToVo(entity, PlayClerkUserInfoResultVo.class);
|
|
||||||
vo.setAddress(entity.getCity());
|
|
||||||
// 查询是否关注,未登录情况下,默认为未关注
|
|
||||||
String loginUserId = customUserService.getLoginUserId();
|
String loginUserId = customUserService.getLoginUserId();
|
||||||
if (StringUtils.isNotEmpty(loginUserId)) {
|
PlayClerkUserInfoResultVo vo = clerkUserInfoService.buildCustomerDetail(id,
|
||||||
vo.setFollowState(playCustomFollowInfoService.queryFollowState(loginUserId, vo.getId()));
|
StringUtils.isNotEmpty(loginUserId) ? loginUserId : "");
|
||||||
}
|
|
||||||
// 服务项目
|
|
||||||
vo.setCommodity(playClerkCommodityService.getClerkCommodityList(vo.getId(), "1"));
|
|
||||||
return R.ok(vo);
|
return R.ok(vo);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package com.starry.admin.modules.weichat.entity;
|
|||||||
import com.alibaba.fastjson2.JSONObject;
|
import com.alibaba.fastjson2.JSONObject;
|
||||||
import com.starry.admin.modules.clerk.module.entity.PlayClerkLevelInfoEntity;
|
import com.starry.admin.modules.clerk.module.entity.PlayClerkLevelInfoEntity;
|
||||||
import com.starry.admin.modules.clerk.module.vo.PlayClerkCommodityQueryVo;
|
import com.starry.admin.modules.clerk.module.vo.PlayClerkCommodityQueryVo;
|
||||||
|
import com.starry.admin.modules.weichat.entity.clerk.MediaVo;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
@@ -45,6 +46,11 @@ public class PlayClerkUserLoginResponseVo {
|
|||||||
*/
|
*/
|
||||||
private List<String> album = new ArrayList<>();
|
private List<String> album = new ArrayList<>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 新媒资列表
|
||||||
|
*/
|
||||||
|
private List<MediaVo> mediaList = new ArrayList<>();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 相册是否运行编辑
|
* 相册是否运行编辑
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package com.starry.admin.modules.weichat.entity.clerk;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import javax.validation.constraints.NotNull;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class MediaOrderRequest {
|
||||||
|
|
||||||
|
private String usage;
|
||||||
|
|
||||||
|
@NotNull(message = "媒资ID列表不能为空")
|
||||||
|
private List<String> mediaIds;
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
package com.starry.admin.modules.weichat.entity.clerk;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.Map;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class MediaVo implements Serializable {
|
||||||
|
|
||||||
|
private String id;
|
||||||
|
|
||||||
|
private String assetId;
|
||||||
|
|
||||||
|
private String mediaId;
|
||||||
|
|
||||||
|
private String kind;
|
||||||
|
|
||||||
|
private String status;
|
||||||
|
|
||||||
|
private String url;
|
||||||
|
|
||||||
|
private String coverUrl;
|
||||||
|
|
||||||
|
private Long durationMs;
|
||||||
|
|
||||||
|
private Integer width;
|
||||||
|
|
||||||
|
private Integer height;
|
||||||
|
|
||||||
|
private Long sizeBytes;
|
||||||
|
|
||||||
|
private Integer orderIndex;
|
||||||
|
|
||||||
|
private Map<String, Object> metadata;
|
||||||
|
|
||||||
|
private String usage;
|
||||||
|
|
||||||
|
private String reviewState;
|
||||||
|
|
||||||
|
private LocalDateTime submittedTime;
|
||||||
|
|
||||||
|
private String reviewNote;
|
||||||
|
}
|
||||||
@@ -75,6 +75,12 @@ public class PlayClerkUserInfoResultVo {
|
|||||||
@ApiModelProperty(value = "相册列表", notes = "店员相册图片URL列表")
|
@ApiModelProperty(value = "相册列表", notes = "店员相册图片URL列表")
|
||||||
private List<String> album = new ArrayList<>();
|
private List<String> album = new ArrayList<>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 媒资列表
|
||||||
|
*/
|
||||||
|
@ApiModelProperty(value = "媒资列表", notes = "结构化媒资数据")
|
||||||
|
private List<MediaVo> mediaList = new ArrayList<>();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 个性签名
|
* 个性签名
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -25,7 +25,8 @@ public class PlayCustomOrderDetailsReturnVo {
|
|||||||
private String orderNo;
|
private String orderNo;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 订单状态【0:1:2:3:4】 0:已下单(待接单) 1:已接单(待开始) 2:已开始(服务中) 3:已完成 4:已取消
|
* 订单状态【0:1:2:3:4:5】
|
||||||
|
* 0:已下单(待接单) 1:已接单(待开始) 2:已开始(服务中) 3:已完成 4:已取消 5:已撤销
|
||||||
*/
|
*/
|
||||||
private String orderStatus;
|
private String orderStatus;
|
||||||
|
|
||||||
|
|||||||
@@ -16,14 +16,15 @@ public class PlayCustomOrderInfoQueryVo extends BasePageEntity {
|
|||||||
|
|
||||||
private String id;
|
private String id;
|
||||||
/**
|
/**
|
||||||
* 订单状态【0:1:2:3:4】 0:已下单 1:已接单 2:已开始 3:已完成 4:已取消
|
* 订单状态【0:1:2:3:4:5】
|
||||||
|
* 0:已下单 1:已接单 2:已开始 3:已完成 4:已取消 5:已撤销
|
||||||
*/
|
*/
|
||||||
private String orderStatus;
|
private String orderStatus;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 订单类型【0:充值订单;1:提现订单;2:普通订单】
|
* 订单类型(为空时默认排除充值/提现)
|
||||||
*/
|
*/
|
||||||
private String orderType = "2";
|
private String orderType;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 下单类型(0:指定单,1:随机单。2:打赏单)
|
* 下单类型(0:指定单,1:随机单。2:打赏单)
|
||||||
|
|||||||
@@ -25,7 +25,8 @@ public class PlayCustomOrderListReturnVo {
|
|||||||
private String orderNo;
|
private String orderNo;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 订单状态【0:1:2:3:4】 0:已下单(待接单) 1:已接单(待开始) 2:已开始(服务中) 3:已完成 4:已取消
|
* 订单状态【0:1:2:3:4:5】
|
||||||
|
* 0:已下单(待接单) 1:已接单(待开始) 2:已开始(服务中) 3:已完成 4:已取消 5:已撤销
|
||||||
*/
|
*/
|
||||||
private String orderStatus;
|
private String orderStatus;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,284 @@
|
|||||||
|
package com.starry.admin.modules.weichat.service;
|
||||||
|
|
||||||
|
import cn.hutool.core.io.FileTypeUtil;
|
||||||
|
import cn.hutool.core.io.FileUtil;
|
||||||
|
import cn.hutool.core.util.IdUtil;
|
||||||
|
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.modules.clerk.enums.ClerkMediaUsage;
|
||||||
|
import com.starry.admin.modules.clerk.module.entity.PlayClerkMediaAssetEntity;
|
||||||
|
import com.starry.admin.modules.clerk.module.entity.PlayClerkUserInfoEntity;
|
||||||
|
import com.starry.admin.modules.clerk.service.IPlayClerkMediaAssetService;
|
||||||
|
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.modules.weichat.assembler.ClerkMediaAssembler;
|
||||||
|
import com.starry.admin.modules.weichat.entity.clerk.MediaVo;
|
||||||
|
import com.starry.common.utils.IdUtils;
|
||||||
|
import java.awt.image.BufferedImage;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Map;
|
||||||
|
import javax.imageio.ImageIO;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
import ws.schild.jave.Encoder;
|
||||||
|
import ws.schild.jave.MultimediaObject;
|
||||||
|
import ws.schild.jave.encode.AudioAttributes;
|
||||||
|
import ws.schild.jave.encode.EncodingAttributes;
|
||||||
|
import ws.schild.jave.encode.VideoAttributes;
|
||||||
|
import ws.schild.jave.info.MultimediaInfo;
|
||||||
|
import ws.schild.jave.info.VideoInfo;
|
||||||
|
import ws.schild.jave.info.VideoSize;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Slf4j
|
||||||
|
public class MediaUploadService {
|
||||||
|
|
||||||
|
private static final long MAX_VIDEO_BYTES = 30L * 1024 * 1024;
|
||||||
|
private static final long MAX_VIDEO_DURATION_MS = 45_000;
|
||||||
|
private static final String IMAGE_OUTPUT_FORMAT = "image2";
|
||||||
|
private static final String VIDEO_OUTPUT_FORMAT = "mp4";
|
||||||
|
|
||||||
|
private final IOssFileService ossFileService;
|
||||||
|
private final IPlayMediaService mediaService;
|
||||||
|
private final IPlayClerkMediaAssetService clerkMediaAssetService;
|
||||||
|
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public MediaVo upload(MultipartFile file, PlayClerkUserInfoEntity clerkInfo, ClerkMediaUsage usage) {
|
||||||
|
if (file == null || file.isEmpty()) {
|
||||||
|
throw new CustomException("请选择要上传的文件");
|
||||||
|
}
|
||||||
|
if (clerkInfo == null) {
|
||||||
|
throw new CustomException("店员信息不存在");
|
||||||
|
}
|
||||||
|
|
||||||
|
String originalFilename = StrUtil.blankToDefault(file.getOriginalFilename(), file.getName());
|
||||||
|
File tempFile = null;
|
||||||
|
File processedVideoFile = null;
|
||||||
|
File coverFile = null;
|
||||||
|
try {
|
||||||
|
String suffix = resolveSuffix(originalFilename);
|
||||||
|
tempFile = createTempFile("media_", suffix);
|
||||||
|
file.transferTo(tempFile);
|
||||||
|
|
||||||
|
String detectedType = detectFileType(tempFile, file.getContentType());
|
||||||
|
boolean isVideo = isVideoType(detectedType, file.getContentType());
|
||||||
|
boolean isImage = isImageType(detectedType, file.getContentType());
|
||||||
|
if (!isVideo && !isImage) {
|
||||||
|
log.warn("Unsupported media type: {} / {}", detectedType, file.getContentType());
|
||||||
|
throw new CustomException("不支持的文件格式");
|
||||||
|
}
|
||||||
|
|
||||||
|
PlayMediaEntity entity = buildSkeletonEntity(file, clerkInfo,
|
||||||
|
isVideo ? MediaKind.VIDEO : MediaKind.IMAGE);
|
||||||
|
entity.getMetadata().put("detectedType", detectedType);
|
||||||
|
entity.getMetadata().put("isVideo", isVideo);
|
||||||
|
|
||||||
|
if (isImage) {
|
||||||
|
handleImageUpload(tempFile, entity, clerkInfo, originalFilename);
|
||||||
|
} else {
|
||||||
|
processedVideoFile = createTempFile("media_video_", ".mp4");
|
||||||
|
coverFile = createTempFile("media_cover_", ".jpg");
|
||||||
|
handleVideoUpload(tempFile, processedVideoFile, coverFile, entity, clerkInfo, originalFilename);
|
||||||
|
}
|
||||||
|
entity.setStatus(MediaStatus.READY.getCode());
|
||||||
|
mediaService.normalizeAndSave(entity);
|
||||||
|
PlayClerkMediaAssetEntity asset = clerkMediaAssetService.linkDraftAsset(
|
||||||
|
clerkInfo.getTenantId(),
|
||||||
|
clerkInfo.getId(),
|
||||||
|
entity.getId(),
|
||||||
|
usage == null ? ClerkMediaUsage.PROFILE : usage);
|
||||||
|
return ClerkMediaAssembler.toVo(entity, asset);
|
||||||
|
} catch (CustomException customException) {
|
||||||
|
throw customException;
|
||||||
|
} catch (Exception ex) {
|
||||||
|
log.error("媒资上传失败", ex);
|
||||||
|
throw new CustomException("媒资上传失败,请稍后重试");
|
||||||
|
} finally {
|
||||||
|
deleteQuietly(tempFile);
|
||||||
|
deleteQuietly(processedVideoFile);
|
||||||
|
deleteQuietly(coverFile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private PlayMediaEntity buildSkeletonEntity(MultipartFile file, PlayClerkUserInfoEntity clerkInfo, MediaKind kind) {
|
||||||
|
PlayMediaEntity entity = new PlayMediaEntity();
|
||||||
|
entity.setId(IdUtils.getUuid());
|
||||||
|
entity.setTenantId(clerkInfo.getTenantId());
|
||||||
|
entity.setOwnerType(MediaOwnerType.CLERK);
|
||||||
|
entity.setOwnerId(clerkInfo.getId());
|
||||||
|
entity.setKind(kind.getCode());
|
||||||
|
entity.setStatus(MediaStatus.UPLOADED.getCode());
|
||||||
|
entity.setSizeBytes(file.getSize());
|
||||||
|
Map<String, Object> metadata = new HashMap<>();
|
||||||
|
metadata.put("originalFilename", file.getOriginalFilename());
|
||||||
|
metadata.put("contentType", file.getContentType());
|
||||||
|
metadata.put("uploadTraceId", IdUtil.fastUUID());
|
||||||
|
metadata.put("sourceSizeBytes", file.getSize());
|
||||||
|
entity.setMetadata(metadata);
|
||||||
|
return entity;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleImageUpload(File tempFile, PlayMediaEntity entity, PlayClerkUserInfoEntity clerkInfo,
|
||||||
|
String originalFilename) throws IOException {
|
||||||
|
BufferedImage image = ImageIO.read(tempFile);
|
||||||
|
if (image == null) {
|
||||||
|
throw new CustomException("图片文件已损坏或格式不受支持");
|
||||||
|
}
|
||||||
|
entity.setWidth(image.getWidth());
|
||||||
|
entity.setHeight(image.getHeight());
|
||||||
|
try (InputStream is = Files.newInputStream(tempFile.toPath())) {
|
||||||
|
String targetName = buildObjectName("img", originalFilename);
|
||||||
|
String url = ossFileService.upload(is, clerkInfo.getTenantId(), targetName);
|
||||||
|
entity.setUrl(url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleVideoUpload(File sourceFile, File targetFile, File coverFile, PlayMediaEntity entity,
|
||||||
|
PlayClerkUserInfoEntity clerkInfo, String originalFilename) throws Exception {
|
||||||
|
if (entity.getSizeBytes() != null && entity.getSizeBytes() > MAX_VIDEO_BYTES) {
|
||||||
|
throw new CustomException("视频大小不能超过30MB");
|
||||||
|
}
|
||||||
|
MultimediaObject multimediaObject = new MultimediaObject(sourceFile);
|
||||||
|
MultimediaInfo info = multimediaObject.getInfo();
|
||||||
|
if (info == null || info.getVideo() == null) {
|
||||||
|
throw new CustomException("无法读取视频信息");
|
||||||
|
}
|
||||||
|
long durationMs = info.getDuration();
|
||||||
|
if (durationMs > MAX_VIDEO_DURATION_MS) {
|
||||||
|
throw new CustomException("视频时长不能超过45秒");
|
||||||
|
}
|
||||||
|
VideoInfo videoInfo = info.getVideo();
|
||||||
|
VideoSize size = videoInfo.getSize();
|
||||||
|
if (size != null) {
|
||||||
|
entity.setWidth(size.getWidth());
|
||||||
|
entity.setHeight(size.getHeight());
|
||||||
|
}
|
||||||
|
entity.setDurationMs(durationMs);
|
||||||
|
|
||||||
|
AudioAttributes audioAttrs = new AudioAttributes();
|
||||||
|
audioAttrs.setCodec("aac");
|
||||||
|
audioAttrs.setBitRate(128_000);
|
||||||
|
audioAttrs.setChannels(2);
|
||||||
|
audioAttrs.setSamplingRate(44_100);
|
||||||
|
|
||||||
|
VideoAttributes videoAttrs = new VideoAttributes();
|
||||||
|
videoAttrs.setCodec("h264");
|
||||||
|
videoAttrs.setBitRate(1_500_000);
|
||||||
|
if (size != null) {
|
||||||
|
videoAttrs.setSize(size);
|
||||||
|
}
|
||||||
|
float frameRate = videoInfo.getFrameRate();
|
||||||
|
videoAttrs.setFrameRate(frameRate > 0 ? Math.round(frameRate) : 30);
|
||||||
|
|
||||||
|
Encoder encoder = new Encoder();
|
||||||
|
EncodingAttributes attrs = new EncodingAttributes();
|
||||||
|
attrs.setOutputFormat(VIDEO_OUTPUT_FORMAT);
|
||||||
|
attrs.setAudioAttributes(audioAttrs);
|
||||||
|
attrs.setVideoAttributes(videoAttrs);
|
||||||
|
encoder.encode(multimediaObject, targetFile, attrs);
|
||||||
|
|
||||||
|
long processedSize = targetFile.length();
|
||||||
|
entity.setSizeBytes(processedSize);
|
||||||
|
|
||||||
|
// 抽取首帧作为封面
|
||||||
|
EncodingAttributes coverAttrs = new EncodingAttributes();
|
||||||
|
VideoAttributes coverVideoAttrs = new VideoAttributes();
|
||||||
|
coverVideoAttrs.setCodec("mjpeg");
|
||||||
|
if (size != null) {
|
||||||
|
coverVideoAttrs.setSize(size);
|
||||||
|
}
|
||||||
|
coverAttrs.setOutputFormat(IMAGE_OUTPUT_FORMAT);
|
||||||
|
coverAttrs.setVideoAttributes(coverVideoAttrs);
|
||||||
|
coverAttrs.setDuration(0.01f);
|
||||||
|
coverAttrs.setOffset(0f);
|
||||||
|
coverAttrs.setAudioAttributes(null);
|
||||||
|
encoder.encode(new MultimediaObject(targetFile), coverFile, coverAttrs);
|
||||||
|
|
||||||
|
try (InputStream videoIs = Files.newInputStream(targetFile.toPath());
|
||||||
|
InputStream coverIs = Files.newInputStream(coverFile.toPath())) {
|
||||||
|
String videoName = buildObjectName("video", originalFilename);
|
||||||
|
String coverName = buildObjectName("cover", originalFilename + ".jpg");
|
||||||
|
String videoUrl = ossFileService.upload(videoIs, clerkInfo.getTenantId(), videoName);
|
||||||
|
String coverUrl = ossFileService.upload(coverIs, clerkInfo.getTenantId(), coverName);
|
||||||
|
entity.setUrl(videoUrl);
|
||||||
|
entity.setCoverUrl(coverUrl);
|
||||||
|
}
|
||||||
|
if (entity.getMetadata() != null) {
|
||||||
|
entity.getMetadata().put("durationMs", durationMs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String detectFileType(File file, String contentType) {
|
||||||
|
String type = null;
|
||||||
|
try {
|
||||||
|
type = FileTypeUtil.getType(file);
|
||||||
|
} catch (Exception ex) {
|
||||||
|
log.warn("Failed to read file type via signature, fallback to contentType: {}", contentType, ex);
|
||||||
|
}
|
||||||
|
if (StrUtil.isNotBlank(type)) {
|
||||||
|
return type.toLowerCase(Locale.ROOT);
|
||||||
|
}
|
||||||
|
if (StrUtil.isNotBlank(contentType)) {
|
||||||
|
return contentType.toLowerCase(Locale.ROOT);
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isVideoType(String detectedType, String mime) {
|
||||||
|
if (StrUtil.isBlank(detectedType) && StrUtil.isBlank(mime)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
String lower = StrUtil.blankToDefault(detectedType, "");
|
||||||
|
if (lower.contains("mp4") || lower.contains("mov") || lower.contains("quicktime")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
String mimeLower = StrUtil.blankToDefault(mime, "").toLowerCase(Locale.ROOT);
|
||||||
|
return mimeLower.startsWith("video/");
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isImageType(String detectedType, String mime) {
|
||||||
|
String lower = StrUtil.blankToDefault(detectedType, "");
|
||||||
|
if (lower.contains("jpg") || lower.contains("jpeg") || lower.contains("png") || lower.contains("gif")
|
||||||
|
|| lower.contains("webp")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
String mimeLower = StrUtil.blankToDefault(mime, "").toLowerCase(Locale.ROOT);
|
||||||
|
return mimeLower.startsWith("image/");
|
||||||
|
}
|
||||||
|
|
||||||
|
private String buildObjectName(String category, String originalFilename) {
|
||||||
|
String ext = resolveSuffix(originalFilename);
|
||||||
|
return StrUtil.join("/", "clerk", category, IdUtils.getUuid() + ext);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String resolveSuffix(String filename) {
|
||||||
|
if (StrUtil.isBlank(filename) || !filename.contains(".")) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return filename.substring(filename.lastIndexOf('.'));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void deleteQuietly(File file) {
|
||||||
|
if (file != null && file.exists()) {
|
||||||
|
FileUtil.del(file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private File createTempFile(String prefix, String suffix) throws IOException {
|
||||||
|
String effectiveSuffix = StrUtil.isBlank(suffix) ? ".tmp" : suffix;
|
||||||
|
return Files.createTempFile(prefix, effectiveSuffix).toFile();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,13 @@ package com.starry.admin.modules.withdraw.controller;
|
|||||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||||
|
import com.starry.admin.common.exception.CustomException;
|
||||||
|
import com.starry.admin.modules.clerk.module.entity.PlayClerkUserInfoEntity;
|
||||||
|
import com.starry.admin.modules.clerk.service.IPlayClerkUserInfoService;
|
||||||
|
import com.starry.admin.modules.custom.module.entity.PlayCustomUserInfoEntity;
|
||||||
|
import com.starry.admin.modules.custom.service.IPlayCustomUserInfoService;
|
||||||
|
import com.starry.admin.modules.order.module.entity.PlayOrderInfoEntity;
|
||||||
|
import com.starry.admin.modules.order.service.IPlayOrderInfoService;
|
||||||
import com.starry.admin.modules.withdraw.entity.EarningsBackfillLogEntity;
|
import com.starry.admin.modules.withdraw.entity.EarningsBackfillLogEntity;
|
||||||
import com.starry.admin.modules.withdraw.entity.EarningsLineEntity;
|
import com.starry.admin.modules.withdraw.entity.EarningsLineEntity;
|
||||||
import com.starry.admin.modules.withdraw.entity.WithdrawalLogEntity;
|
import com.starry.admin.modules.withdraw.entity.WithdrawalLogEntity;
|
||||||
@@ -13,6 +20,7 @@ import com.starry.admin.modules.withdraw.service.IEarningsService;
|
|||||||
import com.starry.admin.modules.withdraw.service.ITenantAlipayConfigService;
|
import com.starry.admin.modules.withdraw.service.ITenantAlipayConfigService;
|
||||||
import com.starry.admin.modules.withdraw.service.IWithdrawalLogService;
|
import com.starry.admin.modules.withdraw.service.IWithdrawalLogService;
|
||||||
import com.starry.admin.modules.withdraw.service.IWithdrawalService;
|
import com.starry.admin.modules.withdraw.service.IWithdrawalService;
|
||||||
|
import com.starry.admin.modules.withdraw.vo.ClerkEarningLineVo;
|
||||||
import com.starry.admin.modules.withdraw.vo.EarningsAdminQueryVo;
|
import com.starry.admin.modules.withdraw.vo.EarningsAdminQueryVo;
|
||||||
import com.starry.admin.modules.withdraw.vo.EarningsAdminSummaryVo;
|
import com.starry.admin.modules.withdraw.vo.EarningsAdminSummaryVo;
|
||||||
import com.starry.admin.modules.withdraw.vo.EarningsBackfillRequest;
|
import com.starry.admin.modules.withdraw.vo.EarningsBackfillRequest;
|
||||||
@@ -24,11 +32,16 @@ import io.swagger.annotations.Api;
|
|||||||
import io.swagger.annotations.ApiOperation;
|
import io.swagger.annotations.ApiOperation;
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.ZoneId;
|
||||||
import java.time.format.DateTimeFormatter;
|
import java.time.format.DateTimeFormatter;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Date;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.function.Function;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
import javax.annotation.Resource;
|
import javax.annotation.Resource;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
@@ -49,6 +62,12 @@ public class AdminWithdrawalController {
|
|||||||
private IEarningsBackfillService earningsBackfillService;
|
private IEarningsBackfillService earningsBackfillService;
|
||||||
@Resource
|
@Resource
|
||||||
private IEarningsBackfillLogService backfillLogService;
|
private IEarningsBackfillLogService backfillLogService;
|
||||||
|
@Resource
|
||||||
|
private IPlayOrderInfoService orderInfoService;
|
||||||
|
@Resource
|
||||||
|
private IPlayClerkUserInfoService clerkUserInfoService;
|
||||||
|
@Resource
|
||||||
|
private IPlayCustomUserInfoService customUserInfoService;
|
||||||
|
|
||||||
@ApiOperation("分页查询提现请求")
|
@ApiOperation("分页查询提现请求")
|
||||||
@PostMapping("/requests/listByPage")
|
@PostMapping("/requests/listByPage")
|
||||||
@@ -72,6 +91,110 @@ public class AdminWithdrawalController {
|
|||||||
return TypedR.ok(list);
|
return TypedR.ok(list);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ApiOperation("提现请求审计")
|
||||||
|
@GetMapping("/requests/{id}/audit")
|
||||||
|
public TypedR<List<ClerkEarningLineVo>> getRequestAudit(@PathVariable("id") String id) {
|
||||||
|
String tenantId = SecurityUtils.getTenantId();
|
||||||
|
WithdrawalRequestEntity request = withdrawalService.getById(id);
|
||||||
|
if (request == null || !tenantId.equals(request.getTenantId())) {
|
||||||
|
throw new CustomException("提现申请不存在或无权查看");
|
||||||
|
}
|
||||||
|
|
||||||
|
List<EarningsLineEntity> lines = earningsService.lambdaQuery()
|
||||||
|
.eq(EarningsLineEntity::getTenantId, tenantId)
|
||||||
|
.eq(EarningsLineEntity::getWithdrawalId, id)
|
||||||
|
.orderByAsc(EarningsLineEntity::getCreatedTime)
|
||||||
|
.list();
|
||||||
|
if (lines.isEmpty()) {
|
||||||
|
return TypedR.ok(Collections.emptyList());
|
||||||
|
}
|
||||||
|
|
||||||
|
List<String> orderIds = lines.stream()
|
||||||
|
.map(EarningsLineEntity::getOrderId)
|
||||||
|
.filter(orderId -> orderId != null && !orderId.isEmpty())
|
||||||
|
.distinct()
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
Map<String, PlayOrderInfoEntity> orderMap = orderIds.isEmpty()
|
||||||
|
? Collections.emptyMap()
|
||||||
|
: orderInfoService.lambdaQuery()
|
||||||
|
.eq(PlayOrderInfoEntity::getTenantId, tenantId)
|
||||||
|
.in(PlayOrderInfoEntity::getId, orderIds)
|
||||||
|
.list()
|
||||||
|
.stream()
|
||||||
|
.collect(Collectors.toMap(PlayOrderInfoEntity::getId, Function.identity()));
|
||||||
|
|
||||||
|
Map<String, PlayClerkUserInfoEntity> clerkMap = Collections.emptyMap();
|
||||||
|
Map<String, PlayCustomUserInfoEntity> customerMap = Collections.emptyMap();
|
||||||
|
if (!orderMap.isEmpty()) {
|
||||||
|
List<String> clerkIds = orderMap.values().stream()
|
||||||
|
.map(PlayOrderInfoEntity::getAcceptBy)
|
||||||
|
.filter(clerkIdValue -> clerkIdValue != null && !clerkIdValue.isEmpty())
|
||||||
|
.distinct()
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
if (!clerkIds.isEmpty()) {
|
||||||
|
clerkMap = clerkUserInfoService.lambdaQuery()
|
||||||
|
.eq(PlayClerkUserInfoEntity::getTenantId, tenantId)
|
||||||
|
.in(PlayClerkUserInfoEntity::getId, clerkIds)
|
||||||
|
.list()
|
||||||
|
.stream()
|
||||||
|
.collect(Collectors.toMap(PlayClerkUserInfoEntity::getId, Function.identity()));
|
||||||
|
}
|
||||||
|
|
||||||
|
List<String> customerIds = orderMap.values().stream()
|
||||||
|
.map(PlayOrderInfoEntity::getPurchaserBy)
|
||||||
|
.filter(customerIdValue -> customerIdValue != null && !customerIdValue.isEmpty())
|
||||||
|
.distinct()
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
if (!customerIds.isEmpty()) {
|
||||||
|
customerMap = customUserInfoService.lambdaQuery()
|
||||||
|
.eq(PlayCustomUserInfoEntity::getTenantId, tenantId)
|
||||||
|
.in(PlayCustomUserInfoEntity::getId, customerIds)
|
||||||
|
.list()
|
||||||
|
.stream()
|
||||||
|
.collect(Collectors.toMap(PlayCustomUserInfoEntity::getId, Function.identity()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
List<ClerkEarningLineVo> vos = new ArrayList<>(lines.size());
|
||||||
|
for (EarningsLineEntity line : lines) {
|
||||||
|
ClerkEarningLineVo vo = new ClerkEarningLineVo();
|
||||||
|
vo.setId(line.getId());
|
||||||
|
vo.setAmount(line.getAmount());
|
||||||
|
vo.setStatus(line.getStatus());
|
||||||
|
vo.setEarningType(line.getEarningType());
|
||||||
|
vo.setWithdrawalId(line.getWithdrawalId());
|
||||||
|
vo.setUnlockTime(line.getUnlockTime());
|
||||||
|
vo.setCreatedTime(toLocalDateTime(line.getCreatedTime()));
|
||||||
|
vo.setOrderId(line.getOrderId());
|
||||||
|
if (line.getOrderId() != null) {
|
||||||
|
PlayOrderInfoEntity order = orderMap.get(line.getOrderId());
|
||||||
|
if (order != null) {
|
||||||
|
vo.setOrderNo(order.getOrderNo());
|
||||||
|
vo.setOrderStatus(order.getOrderStatus());
|
||||||
|
vo.setOrderEndTime(toLocalDateTime(order.getOrderEndTime()));
|
||||||
|
String clerkId = order.getAcceptBy();
|
||||||
|
if (clerkId != null && !clerkId.isEmpty()) {
|
||||||
|
vo.setOrderClerkId(clerkId);
|
||||||
|
PlayClerkUserInfoEntity clerk = clerkMap.get(clerkId);
|
||||||
|
if (clerk != null) {
|
||||||
|
vo.setOrderClerkNickname(clerk.getNickname());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
String customerId = order.getPurchaserBy();
|
||||||
|
if (customerId != null && !customerId.isEmpty()) {
|
||||||
|
vo.setOrderCustomerId(customerId);
|
||||||
|
PlayCustomUserInfoEntity customer = customerMap.get(customerId);
|
||||||
|
if (customer != null) {
|
||||||
|
vo.setOrderCustomerNickname(customer.getNickname());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
vos.add(vo);
|
||||||
|
}
|
||||||
|
return TypedR.ok(vos);
|
||||||
|
}
|
||||||
|
|
||||||
@ApiOperation("分页查询收益明细")
|
@ApiOperation("分页查询收益明细")
|
||||||
@PostMapping("/earnings/listByPage")
|
@PostMapping("/earnings/listByPage")
|
||||||
public TypedR<List<EarningsLineEntity>> listEarnings(@RequestBody EarningsAdminQueryVo vo) {
|
public TypedR<List<EarningsLineEntity>> listEarnings(@RequestBody EarningsAdminQueryVo vo) {
|
||||||
@@ -182,4 +305,17 @@ public class AdminWithdrawalController {
|
|||||||
q.orderByDesc(EarningsLineEntity::getCreatedTime);
|
q.orderByDesc(EarningsLineEntity::getCreatedTime);
|
||||||
return q;
|
return q;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private LocalDateTime toLocalDateTime(Object value) {
|
||||||
|
if (value == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (value instanceof LocalDateTime) {
|
||||||
|
return (LocalDateTime) value;
|
||||||
|
}
|
||||||
|
if (value instanceof Date) {
|
||||||
|
return ((Date) value).toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ import com.fasterxml.jackson.annotation.JsonValue;
|
|||||||
*/
|
*/
|
||||||
public enum EarningsType {
|
public enum EarningsType {
|
||||||
ORDER("ORDER"),
|
ORDER("ORDER"),
|
||||||
COMMISSION("COMMISSION");
|
COMMISSION("COMMISSION"),
|
||||||
|
ADJUSTMENT("ADJUSTMENT");
|
||||||
|
|
||||||
@EnumValue
|
@EnumValue
|
||||||
@JsonValue
|
@JsonValue
|
||||||
|
|||||||
@@ -17,4 +17,8 @@ public interface IEarningsService extends IService<EarningsLineEntity> {
|
|||||||
LocalDateTime getNextUnlockTime(String clerkId, LocalDateTime now);
|
LocalDateTime getNextUnlockTime(String clerkId, LocalDateTime now);
|
||||||
|
|
||||||
List<EarningsLineEntity> findWithdrawable(String clerkId, BigDecimal amount, LocalDateTime now);
|
List<EarningsLineEntity> findWithdrawable(String clerkId, BigDecimal amount, LocalDateTime now);
|
||||||
|
|
||||||
|
void createCounterLine(String orderId, String tenantId, String targetClerkId, BigDecimal amount, String operatorId);
|
||||||
|
|
||||||
|
BigDecimal getRemainingEarningsForOrder(String orderId, String clerkId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
package com.starry.admin.modules.withdraw.service.impl;
|
package com.starry.admin.modules.withdraw.service.impl;
|
||||||
|
|
||||||
|
import cn.hutool.core.util.StrUtil;
|
||||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||||
|
import com.starry.admin.modules.order.module.constant.OrderConstant;
|
||||||
import com.starry.admin.modules.order.module.entity.PlayOrderInfoEntity;
|
import com.starry.admin.modules.order.module.entity.PlayOrderInfoEntity;
|
||||||
import com.starry.admin.modules.withdraw.entity.EarningsLineEntity;
|
import com.starry.admin.modules.withdraw.entity.EarningsLineEntity;
|
||||||
import com.starry.admin.modules.withdraw.enums.EarningsType;
|
import com.starry.admin.modules.withdraw.enums.EarningsType;
|
||||||
@@ -26,6 +28,9 @@ public class EarningsServiceImpl extends ServiceImpl<EarningsLineMapper, Earning
|
|||||||
@Override
|
@Override
|
||||||
public void createFromOrder(PlayOrderInfoEntity orderInfo) {
|
public void createFromOrder(PlayOrderInfoEntity orderInfo) {
|
||||||
if (orderInfo == null || orderInfo.getAcceptBy() == null) return;
|
if (orderInfo == null || orderInfo.getAcceptBy() == null) return;
|
||||||
|
if (OrderConstant.OrderType.BLIND_BOX_PURCHASE.getCode().equals(orderInfo.getOrderType())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
// amount from estimatedRevenue; fallback to orderMoney if null
|
// amount from estimatedRevenue; fallback to orderMoney if null
|
||||||
BigDecimal amount = orderInfo.getEstimatedRevenue() != null ? orderInfo.getEstimatedRevenue()
|
BigDecimal amount = orderInfo.getEstimatedRevenue() != null ? orderInfo.getEstimatedRevenue()
|
||||||
: (orderInfo.getOrderMoney() != null ? orderInfo.getOrderMoney() : BigDecimal.ZERO);
|
: (orderInfo.getOrderMoney() != null ? orderInfo.getOrderMoney() : BigDecimal.ZERO);
|
||||||
@@ -90,4 +95,74 @@ public class EarningsServiceImpl extends ServiceImpl<EarningsLineMapper, Earning
|
|||||||
if (acc.compareTo(amount) < 0) return new ArrayList<>();
|
if (acc.compareTo(amount) < 0) return new ArrayList<>();
|
||||||
return picked;
|
return picked;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void createCounterLine(String orderId, String tenantId, String targetClerkId, BigDecimal amount, String operatorId) {
|
||||||
|
if (StrUtil.hasBlank(orderId, tenantId, targetClerkId)) {
|
||||||
|
throw new IllegalArgumentException("创建冲销收益时参数缺失");
|
||||||
|
}
|
||||||
|
BigDecimal normalized = amount == null ? BigDecimal.ZERO : amount.abs();
|
||||||
|
if (normalized.compareTo(BigDecimal.ZERO) == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
LocalDateTime now = LocalDateTime.now();
|
||||||
|
LocalDateTime resolvedUnlock = now;
|
||||||
|
String resolvedStatus = "available";
|
||||||
|
|
||||||
|
List<EarningsLineEntity> references = this.baseMapper.selectList(new LambdaQueryWrapper<EarningsLineEntity>()
|
||||||
|
.eq(EarningsLineEntity::getOrderId, orderId)
|
||||||
|
.eq(EarningsLineEntity::getClerkId, targetClerkId)
|
||||||
|
.eq(EarningsLineEntity::getDeleted, false)
|
||||||
|
.orderByAsc(EarningsLineEntity::getUnlockTime));
|
||||||
|
EarningsLineEntity reference = references.stream()
|
||||||
|
.filter(line -> line.getAmount() != null && line.getAmount().compareTo(BigDecimal.ZERO) > 0)
|
||||||
|
.findFirst()
|
||||||
|
.orElse(null);
|
||||||
|
if (reference == null) {
|
||||||
|
throw new IllegalStateException("未找到可冲销的收益记录");
|
||||||
|
}
|
||||||
|
LocalDateTime refUnlock = reference.getUnlockTime();
|
||||||
|
String refStatus = reference.getStatus();
|
||||||
|
boolean shouldPreserveFreeze = "frozen".equalsIgnoreCase(refStatus)
|
||||||
|
&& refUnlock != null
|
||||||
|
&& refUnlock.isAfter(now);
|
||||||
|
if (shouldPreserveFreeze) {
|
||||||
|
resolvedUnlock = refUnlock;
|
||||||
|
resolvedStatus = "frozen";
|
||||||
|
} else {
|
||||||
|
resolvedUnlock = now;
|
||||||
|
resolvedStatus = "available";
|
||||||
|
}
|
||||||
|
|
||||||
|
EarningsLineEntity line = new EarningsLineEntity();
|
||||||
|
line.setId(IdUtils.getUuid());
|
||||||
|
line.setOrderId(orderId);
|
||||||
|
line.setTenantId(tenantId);
|
||||||
|
line.setClerkId(targetClerkId);
|
||||||
|
line.setAmount(normalized.negate());
|
||||||
|
line.setEarningType(EarningsType.ADJUSTMENT);
|
||||||
|
line.setStatus(resolvedStatus);
|
||||||
|
line.setUnlockTime(resolvedUnlock);
|
||||||
|
this.save(line);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public BigDecimal getRemainingEarningsForOrder(String orderId, String clerkId) {
|
||||||
|
if (StrUtil.hasBlank(orderId, clerkId)) {
|
||||||
|
return BigDecimal.ZERO;
|
||||||
|
}
|
||||||
|
List<EarningsLineEntity> lines = this.lambdaQuery()
|
||||||
|
.eq(EarningsLineEntity::getOrderId, orderId)
|
||||||
|
.eq(EarningsLineEntity::getClerkId, clerkId)
|
||||||
|
.eq(EarningsLineEntity::getDeleted, false)
|
||||||
|
.list();
|
||||||
|
BigDecimal total = BigDecimal.ZERO;
|
||||||
|
for (EarningsLineEntity line : lines) {
|
||||||
|
BigDecimal amount = line.getAmount() == null ? BigDecimal.ZERO : line.getAmount();
|
||||||
|
total = total.add(amount);
|
||||||
|
}
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,11 @@ public class ClerkEarningLineVo {
|
|||||||
private String orderNo;
|
private String orderNo;
|
||||||
private String orderStatus;
|
private String orderStatus;
|
||||||
|
|
||||||
|
private String orderCustomerId;
|
||||||
|
private String orderCustomerNickname;
|
||||||
|
private String orderClerkId;
|
||||||
|
private String orderClerkNickname;
|
||||||
|
|
||||||
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
|
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
|
||||||
private LocalDateTime orderEndTime;
|
private LocalDateTime orderEndTime;
|
||||||
|
|||||||
@@ -13,13 +13,14 @@ spring:
|
|||||||
datasource:
|
datasource:
|
||||||
type: com.alibaba.druid.pool.DruidDataSource
|
type: com.alibaba.druid.pool.DruidDataSource
|
||||||
driverClassName: com.mysql.cj.jdbc.Driver
|
driverClassName: com.mysql.cj.jdbc.Driver
|
||||||
url: jdbc:mysql://localhost:33306/peipei_apitest?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false&allowMultiQueries=true&rewriteBatchedStatements=true
|
url: jdbc:mysql://localhost:33306/peipei_apitest?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false&allowMultiQueries=true&rewriteBatchedStatements=true&connectionCollation=utf8mb4_general_ci&sessionVariables=collation_connection=utf8mb4_general_ci
|
||||||
username: apitest
|
username: root
|
||||||
password: apitest
|
password: root
|
||||||
druid:
|
druid:
|
||||||
enable: true
|
enable: true
|
||||||
db-type: mysql
|
db-type: mysql
|
||||||
filters: stat,wall
|
filters: stat,wall
|
||||||
|
connection-init-sqls: SET NAMES utf8mb4 COLLATE utf8mb4_general_ci
|
||||||
max-active: 20
|
max-active: 20
|
||||||
initial-size: 1
|
initial-size: 1
|
||||||
max-wait: 60000
|
max-wait: 60000
|
||||||
|
|||||||
@@ -96,6 +96,10 @@ logging:
|
|||||||
org.springframework.web.servlet.DispatcherServlet: debug
|
org.springframework.web.servlet.DispatcherServlet: debug
|
||||||
org.springframework.security: debug
|
org.springframework.security: debug
|
||||||
|
|
||||||
|
clerk:
|
||||||
|
media:
|
||||||
|
migration-enabled: false
|
||||||
|
|
||||||
jwt:
|
jwt:
|
||||||
tokenHeader: X-Token #JWT存储的请求头
|
tokenHeader: X-Token #JWT存储的请求头
|
||||||
tokenHead: Bearer #JWT负载中拿到开头
|
tokenHead: Bearer #JWT负载中拿到开头
|
||||||
@@ -117,4 +121,3 @@ xl:
|
|||||||
authCode:
|
authCode:
|
||||||
# 登录验证码是否开启,开发环境配置false方便测试
|
# 登录验证码是否开启,开发环境配置false方便测试
|
||||||
enable: ${XL_LOGIN_AUTHCODE_ENABLE:false}
|
enable: ${XL_LOGIN_AUTHCODE_ENABLE:false}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,291 @@
|
|||||||
|
-- Align legacy tables with production: ensure core order/earnings tables use utf8mb4_general_ci.
|
||||||
|
-- Safe to run multiple times; already-general_ci tables will simply rebuild without data changes.
|
||||||
|
|
||||||
|
-- Some historical tables (e.g. sys_tenant_recharge_info) still rely on zero-datetime
|
||||||
|
-- defaults. MySQL 8 with NO_ZERO_DATE / NO_ZERO_IN_DATE rejects those definitions
|
||||||
|
-- when the engine rebuilds the table. Temporarily drop those sql_mode flags so the
|
||||||
|
-- table structure matches production before/after this migration.
|
||||||
|
SET @OLD_SQL_MODE = @@sql_mode;
|
||||||
|
SET SESSION sql_mode = REPLACE(REPLACE(@OLD_SQL_MODE, 'NO_ZERO_IN_DATE', ''), 'NO_ZERO_DATE', '');
|
||||||
|
|
||||||
|
ALTER TABLE `play_order_info`
|
||||||
|
CONVERT TO CHARACTER SET utf8mb4
|
||||||
|
COLLATE utf8mb4_general_ci;
|
||||||
|
|
||||||
|
ALTER TABLE `play_order_log_info`
|
||||||
|
CONVERT TO CHARACTER SET utf8mb4
|
||||||
|
COLLATE utf8mb4_general_ci;
|
||||||
|
|
||||||
|
ALTER TABLE `play_order_random_info`
|
||||||
|
CONVERT TO CHARACTER SET utf8mb4
|
||||||
|
COLLATE utf8mb4_general_ci;
|
||||||
|
|
||||||
|
ALTER TABLE `play_order_refund_info`
|
||||||
|
CONVERT TO CHARACTER SET utf8mb4
|
||||||
|
COLLATE utf8mb4_general_ci;
|
||||||
|
|
||||||
|
ALTER TABLE `play_order_continue_info`
|
||||||
|
CONVERT TO CHARACTER SET utf8mb4
|
||||||
|
COLLATE utf8mb4_general_ci;
|
||||||
|
|
||||||
|
ALTER TABLE `play_order_demand_info`
|
||||||
|
CONVERT TO CHARACTER SET utf8mb4
|
||||||
|
COLLATE utf8mb4_general_ci;
|
||||||
|
|
||||||
|
ALTER TABLE `play_order_evaluate_info`
|
||||||
|
CONVERT TO CHARACTER SET utf8mb4
|
||||||
|
COLLATE utf8mb4_general_ci;
|
||||||
|
|
||||||
|
ALTER TABLE `play_custom_user_info`
|
||||||
|
CONVERT TO CHARACTER SET utf8mb4
|
||||||
|
COLLATE utf8mb4_general_ci;
|
||||||
|
|
||||||
|
ALTER TABLE `play_custom_gift_info`
|
||||||
|
CONVERT TO CHARACTER SET utf8mb4
|
||||||
|
COLLATE utf8mb4_general_ci;
|
||||||
|
|
||||||
|
ALTER TABLE `play_custom_article_info`
|
||||||
|
CONVERT TO CHARACTER SET utf8mb4
|
||||||
|
COLLATE utf8mb4_general_ci;
|
||||||
|
|
||||||
|
ALTER TABLE `play_custom_follow_info`
|
||||||
|
CONVERT TO CHARACTER SET utf8mb4
|
||||||
|
COLLATE utf8mb4_general_ci;
|
||||||
|
|
||||||
|
ALTER TABLE `play_custom_leave_msg`
|
||||||
|
CONVERT TO CHARACTER SET utf8mb4
|
||||||
|
COLLATE utf8mb4_general_ci;
|
||||||
|
|
||||||
|
ALTER TABLE `play_custom_level_info`
|
||||||
|
CONVERT TO CHARACTER SET utf8mb4
|
||||||
|
COLLATE utf8mb4_general_ci;
|
||||||
|
|
||||||
|
ALTER TABLE `play_clerk_user_info`
|
||||||
|
CONVERT TO CHARACTER SET utf8mb4
|
||||||
|
COLLATE utf8mb4_general_ci;
|
||||||
|
|
||||||
|
ALTER TABLE `play_clerk_user_review_info`
|
||||||
|
CONVERT TO CHARACTER SET utf8mb4
|
||||||
|
COLLATE utf8mb4_general_ci;
|
||||||
|
|
||||||
|
ALTER TABLE `play_clerk_level_info`
|
||||||
|
CONVERT TO CHARACTER SET utf8mb4
|
||||||
|
COLLATE utf8mb4_general_ci;
|
||||||
|
|
||||||
|
ALTER TABLE `play_clerk_gift_info`
|
||||||
|
CONVERT TO CHARACTER SET utf8mb4
|
||||||
|
COLLATE utf8mb4_general_ci;
|
||||||
|
|
||||||
|
ALTER TABLE `play_clerk_classification_info`
|
||||||
|
CONVERT TO CHARACTER SET utf8mb4
|
||||||
|
COLLATE utf8mb4_general_ci;
|
||||||
|
|
||||||
|
ALTER TABLE `play_clerk_type_info`
|
||||||
|
CONVERT TO CHARACTER SET utf8mb4
|
||||||
|
COLLATE utf8mb4_general_ci;
|
||||||
|
|
||||||
|
ALTER TABLE `play_clerk_type_user_info`
|
||||||
|
CONVERT TO CHARACTER SET utf8mb4
|
||||||
|
COLLATE utf8mb4_general_ci;
|
||||||
|
|
||||||
|
ALTER TABLE `play_personnel_group_info`
|
||||||
|
CONVERT TO CHARACTER SET utf8mb4
|
||||||
|
COLLATE utf8mb4_general_ci;
|
||||||
|
|
||||||
|
ALTER TABLE `play_personnel_admin_info`
|
||||||
|
CONVERT TO CHARACTER SET utf8mb4
|
||||||
|
COLLATE utf8mb4_general_ci;
|
||||||
|
|
||||||
|
ALTER TABLE `play_personnel_waiter_info`
|
||||||
|
CONVERT TO CHARACTER SET utf8mb4
|
||||||
|
COLLATE utf8mb4_general_ci;
|
||||||
|
|
||||||
|
ALTER TABLE `play_coupon_info`
|
||||||
|
CONVERT TO CHARACTER SET utf8mb4
|
||||||
|
COLLATE utf8mb4_general_ci;
|
||||||
|
|
||||||
|
ALTER TABLE `play_coupon_details`
|
||||||
|
CONVERT TO CHARACTER SET utf8mb4
|
||||||
|
COLLATE utf8mb4_general_ci;
|
||||||
|
|
||||||
|
ALTER TABLE `play_gift_info`
|
||||||
|
CONVERT TO CHARACTER SET utf8mb4
|
||||||
|
COLLATE utf8mb4_general_ci;
|
||||||
|
|
||||||
|
ALTER TABLE `sys_user`
|
||||||
|
CONVERT TO CHARACTER SET utf8mb4
|
||||||
|
COLLATE utf8mb4_general_ci;
|
||||||
|
|
||||||
|
ALTER TABLE `sys_user_role`
|
||||||
|
CONVERT TO CHARACTER SET utf8mb4
|
||||||
|
COLLATE utf8mb4_general_ci;
|
||||||
|
|
||||||
|
ALTER TABLE `sys_role`
|
||||||
|
CONVERT TO CHARACTER SET utf8mb4
|
||||||
|
COLLATE utf8mb4_general_ci;
|
||||||
|
|
||||||
|
ALTER TABLE `sys_role_menu`
|
||||||
|
CONVERT TO CHARACTER SET utf8mb4
|
||||||
|
COLLATE utf8mb4_general_ci;
|
||||||
|
|
||||||
|
ALTER TABLE `sys_role_dept`
|
||||||
|
CONVERT TO CHARACTER SET utf8mb4
|
||||||
|
COLLATE utf8mb4_general_ci;
|
||||||
|
|
||||||
|
ALTER TABLE `sys_dept`
|
||||||
|
CONVERT TO CHARACTER SET utf8mb4
|
||||||
|
COLLATE utf8mb4_general_ci;
|
||||||
|
|
||||||
|
ALTER TABLE `sys_menu`
|
||||||
|
CONVERT TO CHARACTER SET utf8mb4
|
||||||
|
COLLATE utf8mb4_general_ci;
|
||||||
|
|
||||||
|
ALTER TABLE `sys_tenant`
|
||||||
|
CONVERT TO CHARACTER SET utf8mb4
|
||||||
|
COLLATE utf8mb4_general_ci;
|
||||||
|
|
||||||
|
ALTER TABLE `sys_tenant_package`
|
||||||
|
CONVERT TO CHARACTER SET utf8mb4
|
||||||
|
COLLATE utf8mb4_general_ci;
|
||||||
|
|
||||||
|
ALTER TABLE `sys_tenant_recharge_info`
|
||||||
|
CONVERT TO CHARACTER SET utf8mb4
|
||||||
|
COLLATE utf8mb4_general_ci;
|
||||||
|
|
||||||
|
ALTER TABLE `blind_box_config`
|
||||||
|
CONVERT TO CHARACTER SET utf8mb4
|
||||||
|
COLLATE utf8mb4_general_ci;
|
||||||
|
|
||||||
|
ALTER TABLE `blind_box_pool`
|
||||||
|
CONVERT TO CHARACTER SET utf8mb4
|
||||||
|
COLLATE utf8mb4_general_ci;
|
||||||
|
|
||||||
|
ALTER TABLE `blind_box_reward`
|
||||||
|
CONVERT TO CHARACTER SET utf8mb4
|
||||||
|
COLLATE utf8mb4_general_ci;
|
||||||
|
|
||||||
|
ALTER TABLE `commodity_info`
|
||||||
|
CONVERT TO CHARACTER SET utf8mb4
|
||||||
|
COLLATE utf8mb4_general_ci;
|
||||||
|
|
||||||
|
ALTER TABLE `gen_table`
|
||||||
|
CONVERT TO CHARACTER SET utf8mb4
|
||||||
|
COLLATE utf8mb4_general_ci;
|
||||||
|
|
||||||
|
ALTER TABLE `gen_table_column`
|
||||||
|
CONVERT TO CHARACTER SET utf8mb4
|
||||||
|
COLLATE utf8mb4_general_ci;
|
||||||
|
|
||||||
|
ALTER TABLE `order_details_info`
|
||||||
|
CONVERT TO CHARACTER SET utf8mb4
|
||||||
|
COLLATE utf8mb4_general_ci;
|
||||||
|
|
||||||
|
ALTER TABLE `order_log_info`
|
||||||
|
CONVERT TO CHARACTER SET utf8mb4
|
||||||
|
COLLATE utf8mb4_general_ci;
|
||||||
|
|
||||||
|
ALTER TABLE `play_account info`
|
||||||
|
CONVERT TO CHARACTER SET utf8mb4
|
||||||
|
COLLATE utf8mb4_general_ci;
|
||||||
|
|
||||||
|
ALTER TABLE `play_avatar_frame_info`
|
||||||
|
CONVERT TO CHARACTER SET utf8mb4
|
||||||
|
COLLATE utf8mb4_general_ci;
|
||||||
|
|
||||||
|
ALTER TABLE `play_balance_details_info`
|
||||||
|
CONVERT TO CHARACTER SET utf8mb4
|
||||||
|
COLLATE utf8mb4_general_ci;
|
||||||
|
|
||||||
|
ALTER TABLE `play_clerk_article_info`
|
||||||
|
CONVERT TO CHARACTER SET utf8mb4
|
||||||
|
COLLATE utf8mb4_general_ci;
|
||||||
|
|
||||||
|
ALTER TABLE `play_clerk_commodity_info`
|
||||||
|
CONVERT TO CHARACTER SET utf8mb4
|
||||||
|
COLLATE utf8mb4_general_ci;
|
||||||
|
|
||||||
|
ALTER TABLE `play_clerk_data_review_info`
|
||||||
|
CONVERT TO CHARACTER SET utf8mb4
|
||||||
|
COLLATE utf8mb4_general_ci;
|
||||||
|
|
||||||
|
ALTER TABLE `play_clerk_operation_log`
|
||||||
|
CONVERT TO CHARACTER SET utf8mb4
|
||||||
|
COLLATE utf8mb4_general_ci;
|
||||||
|
|
||||||
|
ALTER TABLE `play_clerk_pk`
|
||||||
|
CONVERT TO CHARACTER SET utf8mb4
|
||||||
|
COLLATE utf8mb4_general_ci;
|
||||||
|
|
||||||
|
ALTER TABLE `play_clerk_ranking_info`
|
||||||
|
CONVERT TO CHARACTER SET utf8mb4
|
||||||
|
COLLATE utf8mb4_general_ci;
|
||||||
|
|
||||||
|
ALTER TABLE `play_clerk_resource info`
|
||||||
|
CONVERT TO CHARACTER SET utf8mb4
|
||||||
|
COLLATE utf8mb4_general_ci;
|
||||||
|
|
||||||
|
ALTER TABLE `play_clerk_wages_details_info`
|
||||||
|
CONVERT TO CHARACTER SET utf8mb4
|
||||||
|
COLLATE utf8mb4_general_ci;
|
||||||
|
|
||||||
|
ALTER TABLE `play_clerk_wages_info`
|
||||||
|
CONVERT TO CHARACTER SET utf8mb4
|
||||||
|
COLLATE utf8mb4_general_ci;
|
||||||
|
|
||||||
|
ALTER TABLE `play_clerk_waiter_info`
|
||||||
|
CONVERT TO CHARACTER SET utf8mb4
|
||||||
|
COLLATE utf8mb4_general_ci;
|
||||||
|
|
||||||
|
ALTER TABLE `shop_ui_setting`
|
||||||
|
CONVERT TO CHARACTER SET utf8mb4
|
||||||
|
COLLATE utf8mb4_general_ci;
|
||||||
|
|
||||||
|
ALTER TABLE `sys_administrative_area_dict_info`
|
||||||
|
CONVERT TO CHARACTER SET utf8mb4
|
||||||
|
COLLATE utf8mb4_general_ci;
|
||||||
|
|
||||||
|
ALTER TABLE `sys_dict`
|
||||||
|
CONVERT TO CHARACTER SET utf8mb4
|
||||||
|
COLLATE utf8mb4_general_ci;
|
||||||
|
|
||||||
|
ALTER TABLE `sys_dict_data`
|
||||||
|
CONVERT TO CHARACTER SET utf8mb4
|
||||||
|
COLLATE utf8mb4_general_ci;
|
||||||
|
|
||||||
|
ALTER TABLE `sys_login_log`
|
||||||
|
CONVERT TO CHARACTER SET utf8mb4
|
||||||
|
COLLATE utf8mb4_general_ci;
|
||||||
|
|
||||||
|
ALTER TABLE `sys_operation_log`
|
||||||
|
CONVERT TO CHARACTER SET utf8mb4
|
||||||
|
COLLATE utf8mb4_general_ci;
|
||||||
|
ALTER TABLE `play_shop_article_info`
|
||||||
|
CONVERT TO CHARACTER SET utf8mb4
|
||||||
|
COLLATE utf8mb4_general_ci;
|
||||||
|
|
||||||
|
ALTER TABLE `play_shop_carousel_info`
|
||||||
|
CONVERT TO CHARACTER SET utf8mb4
|
||||||
|
COLLATE utf8mb4_general_ci;
|
||||||
|
|
||||||
|
ALTER TABLE `play_commodity_info`
|
||||||
|
CONVERT TO CHARACTER SET utf8mb4
|
||||||
|
COLLATE utf8mb4_general_ci;
|
||||||
|
|
||||||
|
ALTER TABLE `play_commodity_and_level_info`
|
||||||
|
CONVERT TO CHARACTER SET utf8mb4
|
||||||
|
COLLATE utf8mb4_general_ci;
|
||||||
|
|
||||||
|
ALTER TABLE `play_custom_amount details`
|
||||||
|
CONVERT TO CHARACTER SET utf8mb4
|
||||||
|
COLLATE utf8mb4_general_ci;
|
||||||
|
|
||||||
|
ALTER TABLE `play_notice_info`
|
||||||
|
CONVERT TO CHARACTER SET utf8mb4
|
||||||
|
COLLATE utf8mb4_general_ci;
|
||||||
|
|
||||||
|
ALTER TABLE `play_order_complaint_info`
|
||||||
|
CONVERT TO CHARACTER SET utf8mb4
|
||||||
|
COLLATE utf8mb4_general_ci;
|
||||||
|
|
||||||
|
-- Restore original sql_mode flags once the rebuilds are done.
|
||||||
|
SET SESSION sql_mode = @OLD_SQL_MODE;
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
CREATE TABLE `play_media` (
|
||||||
|
`id` varchar(32) NOT NULL,
|
||||||
|
`tenant_id` varchar(32) NOT NULL,
|
||||||
|
`owner_type` varchar(32) NOT NULL COMMENT 'clerk/article/...',
|
||||||
|
`owner_id` varchar(32) NOT NULL,
|
||||||
|
`kind` varchar(16) NOT NULL COMMENT 'image | video',
|
||||||
|
`status` varchar(16) NOT NULL COMMENT 'uploaded|processing|ready|approved|rejected',
|
||||||
|
`url` varchar(1024) NOT NULL,
|
||||||
|
`cover_url` varchar(1024) DEFAULT NULL,
|
||||||
|
`duration_ms` bigint DEFAULT NULL,
|
||||||
|
`width` int DEFAULT NULL,
|
||||||
|
`height` int DEFAULT NULL,
|
||||||
|
`size_bytes` bigint DEFAULT NULL,
|
||||||
|
`order_index` int NOT NULL DEFAULT 0,
|
||||||
|
`metadata` json DEFAULT NULL,
|
||||||
|
`created_time` datetime DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
`updated_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
KEY `idx_media_owner` (`owner_type`,`owner_id`),
|
||||||
|
KEY `idx_media_order` (`tenant_id`,`owner_type`,`owner_id`,`order_index`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
|
CREATE TABLE `play_clerk_media_asset` (
|
||||||
|
`id` varchar(32) NOT NULL,
|
||||||
|
`clerk_id` varchar(32) NOT NULL,
|
||||||
|
`tenant_id` varchar(32) NOT NULL,
|
||||||
|
`media_id` varchar(32) NOT NULL,
|
||||||
|
`usage` varchar(32) NOT NULL,
|
||||||
|
`review_state` varchar(16) NOT NULL,
|
||||||
|
`order_index` int NOT NULL DEFAULT 0,
|
||||||
|
`submitted_time` datetime DEFAULT NULL,
|
||||||
|
`review_record_id` varchar(32) DEFAULT NULL,
|
||||||
|
`note` varchar(255) DEFAULT NULL,
|
||||||
|
`deleted` tinyint(1) NOT NULL DEFAULT 0,
|
||||||
|
`created_by` varchar(32) DEFAULT NULL,
|
||||||
|
`created_time` datetime DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
`updated_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
`updated_by` varchar(32) DEFAULT NULL,
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE KEY `uk_clerk_usage_media` (`clerk_id`,`usage`,`media_id`),
|
||||||
|
KEY `idx_clerk_usage_state` (`clerk_id`,`usage`,`review_state`,`deleted`),
|
||||||
|
KEY `idx_clerk_media_asset_media` (`media_id`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
@@ -0,0 +1,396 @@
|
|||||||
|
package com.starry.admin.api;
|
||||||
|
|
||||||
|
import static org.hamcrest.Matchers.nullValue;
|
||||||
|
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||||
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||||
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.starry.admin.common.apitest.ApiTestDataSeeder;
|
||||||
|
import com.starry.admin.modules.clerk.module.entity.PlayClerkUserInfoEntity;
|
||||||
|
import com.starry.admin.modules.clerk.service.IPlayClerkUserInfoService;
|
||||||
|
import com.starry.admin.modules.custom.module.entity.PlayCustomUserInfoEntity;
|
||||||
|
import com.starry.admin.modules.custom.service.IPlayCustomUserInfoService;
|
||||||
|
import com.starry.admin.modules.order.module.entity.PlayOrderInfoEntity;
|
||||||
|
import com.starry.admin.modules.order.service.IPlayOrderInfoService;
|
||||||
|
import com.starry.admin.modules.withdraw.entity.EarningsLineEntity;
|
||||||
|
import com.starry.admin.modules.withdraw.entity.WithdrawalRequestEntity;
|
||||||
|
import com.starry.admin.modules.withdraw.enums.EarningsType;
|
||||||
|
import com.starry.admin.modules.withdraw.service.IEarningsService;
|
||||||
|
import com.starry.admin.modules.withdraw.service.IWithdrawalService;
|
||||||
|
import com.starry.admin.utils.SecurityUtils;
|
||||||
|
import com.starry.common.utils.IdUtils;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.ZoneId;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.List;
|
||||||
|
import org.assertj.core.api.Assertions;
|
||||||
|
import org.junit.jupiter.api.AfterEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.test.web.servlet.MvcResult;
|
||||||
|
|
||||||
|
class AdminWithdrawalControllerApiTest extends AbstractApiTest {
|
||||||
|
|
||||||
|
private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private IEarningsService earningsService;
|
||||||
|
@Autowired
|
||||||
|
private IWithdrawalService withdrawalService;
|
||||||
|
@Autowired
|
||||||
|
private IPlayOrderInfoService orderInfoService;
|
||||||
|
@Autowired
|
||||||
|
private IPlayClerkUserInfoService clerkUserInfoService;
|
||||||
|
@Autowired
|
||||||
|
private IPlayCustomUserInfoService customUserInfoService;
|
||||||
|
|
||||||
|
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||||
|
private final List<String> earningsToCleanup = new ArrayList<>();
|
||||||
|
private final List<String> withdrawalsToCleanup = new ArrayList<>();
|
||||||
|
private final List<String> ordersToCleanup = new ArrayList<>();
|
||||||
|
|
||||||
|
@AfterEach
|
||||||
|
void tearDown() {
|
||||||
|
if (!earningsToCleanup.isEmpty()) {
|
||||||
|
earningsService.removeByIds(earningsToCleanup);
|
||||||
|
earningsToCleanup.clear();
|
||||||
|
}
|
||||||
|
if (!withdrawalsToCleanup.isEmpty()) {
|
||||||
|
withdrawalService.removeByIds(withdrawalsToCleanup);
|
||||||
|
withdrawalsToCleanup.clear();
|
||||||
|
}
|
||||||
|
if (!ordersToCleanup.isEmpty()) {
|
||||||
|
orderInfoService.removeByIds(ordersToCleanup);
|
||||||
|
ordersToCleanup.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void auditReturnsEarningLinesWithOrderDetails() throws Exception {
|
||||||
|
ensureTenantContext();
|
||||||
|
ensureProfileFixtures();
|
||||||
|
|
||||||
|
PlayClerkUserInfoEntity expectedClerk = clerkUserInfoService.getById(ApiTestDataSeeder.DEFAULT_CLERK_ID);
|
||||||
|
PlayCustomUserInfoEntity expectedCustomer = customUserInfoService.getById(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID);
|
||||||
|
Assertions.assertThat(expectedClerk).as("default clerk fixture missing").isNotNull();
|
||||||
|
Assertions.assertThat(expectedCustomer).as("default customer fixture missing").isNotNull();
|
||||||
|
String expectedClerkNickname = expectedClerk.getNickname();
|
||||||
|
String expectedCustomerNickname = expectedCustomer.getNickname();
|
||||||
|
|
||||||
|
PlayOrderInfoEntity order = seedOrder(LocalDateTime.now().minusHours(2));
|
||||||
|
WithdrawalRequestEntity withdrawal = seedWithdrawal(ApiTestDataSeeder.DEFAULT_TENANT_ID, new BigDecimal("88.60"));
|
||||||
|
|
||||||
|
LocalDateTime now = LocalDateTime.now();
|
||||||
|
seedEarningLine(withdrawal.getId(), order.getId(), new BigDecimal("50.30"), "withdrawn", now.minusMinutes(30), EarningsType.ORDER);
|
||||||
|
seedEarningLine(withdrawal.getId(), null, new BigDecimal("38.30"), "withdrawing", now.minusMinutes(10), EarningsType.ORDER);
|
||||||
|
|
||||||
|
MvcResult result = mockMvc.perform(get("/admin/withdraw/requests/" + withdrawal.getId() + "/audit")
|
||||||
|
.header(USER_HEADER, DEFAULT_USER)
|
||||||
|
.header(TENANT_HEADER, DEFAULT_TENANT))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.code").value(200))
|
||||||
|
.andExpect(jsonPath("$.data").isArray())
|
||||||
|
.andExpect(jsonPath("$.data.length()").value(2))
|
||||||
|
.andReturn();
|
||||||
|
|
||||||
|
JsonNode data = objectMapper.readTree(result.getResponse().getContentAsString()).get("data");
|
||||||
|
|
||||||
|
boolean foundOrder = false;
|
||||||
|
boolean foundMissing = false;
|
||||||
|
for (JsonNode node : data) {
|
||||||
|
String orderNo = node.path("orderNo").isMissingNode() ? null : node.path("orderNo").asText(null);
|
||||||
|
if (order.getOrderNo().equals(orderNo)) {
|
||||||
|
Assertions.assertThat(node.path("orderStatus").asText()).isEqualTo(order.getOrderStatus());
|
||||||
|
Assertions.assertThat(node.path("earningType").asText()).isEqualTo(EarningsType.ORDER.name());
|
||||||
|
Assertions.assertThat(node.path("orderClerkId").asText()).isEqualTo(ApiTestDataSeeder.DEFAULT_CLERK_ID);
|
||||||
|
Assertions.assertThat(normalizeUtf8(node.path("orderClerkNickname").asText())).isEqualTo(expectedClerkNickname);
|
||||||
|
Assertions.assertThat(node.path("orderCustomerId").asText()).isEqualTo(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID);
|
||||||
|
Assertions.assertThat(normalizeUtf8(node.path("orderCustomerNickname").asText())).isEqualTo(expectedCustomerNickname);
|
||||||
|
foundOrder = true;
|
||||||
|
}
|
||||||
|
if (node.path("orderNo").isNull()) {
|
||||||
|
foundMissing = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Assertions.assertThat(foundOrder).isTrue();
|
||||||
|
Assertions.assertThat(foundMissing).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void auditRejectsMissingRequest() throws Exception {
|
||||||
|
ensureTenantContext();
|
||||||
|
|
||||||
|
mockMvc.perform(get("/admin/withdraw/requests/missing-request/audit")
|
||||||
|
.header(USER_HEADER, DEFAULT_USER)
|
||||||
|
.header(TENANT_HEADER, DEFAULT_TENANT))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.code").value(500))
|
||||||
|
.andExpect(jsonPath("$.message").value("提现申请不存在或无权查看"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void auditRejectsCrossTenantAccess() throws Exception {
|
||||||
|
ensureTenantContext();
|
||||||
|
WithdrawalRequestEntity outsider = seedWithdrawal("tenant-other", new BigDecimal("30.00"));
|
||||||
|
|
||||||
|
mockMvc.perform(get("/admin/withdraw/requests/" + outsider.getId() + "/audit")
|
||||||
|
.header(USER_HEADER, DEFAULT_USER)
|
||||||
|
.header(TENANT_HEADER, DEFAULT_TENANT))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.code").value(500))
|
||||||
|
.andExpect(jsonPath("$.message").value("提现申请不存在或无权查看"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void auditReturnsEmptyListWhenNoEarningsFound() throws Exception {
|
||||||
|
ensureTenantContext();
|
||||||
|
WithdrawalRequestEntity request = seedWithdrawal(ApiTestDataSeeder.DEFAULT_TENANT_ID, new BigDecimal("22.00"));
|
||||||
|
|
||||||
|
mockMvc.perform(get("/admin/withdraw/requests/" + request.getId() + "/audit")
|
||||||
|
.header(USER_HEADER, DEFAULT_USER)
|
||||||
|
.header(TENANT_HEADER, DEFAULT_TENANT))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.code").value(200))
|
||||||
|
.andExpect(jsonPath("$.data.length()").value(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void auditHandlesMissingOrderRecordsGracefully() throws Exception {
|
||||||
|
ensureTenantContext();
|
||||||
|
WithdrawalRequestEntity request = seedWithdrawal(ApiTestDataSeeder.DEFAULT_TENANT_ID, new BigDecimal("40.00"));
|
||||||
|
String orphanOrderId = "order-orphan-" + IdUtils.getUuid();
|
||||||
|
seedEarningLine(request.getId(), orphanOrderId, new BigDecimal("15.00"), "withdrawn", LocalDateTime.now().minusMinutes(5), EarningsType.ORDER);
|
||||||
|
|
||||||
|
mockMvc.perform(get("/admin/withdraw/requests/" + request.getId() + "/audit")
|
||||||
|
.header(USER_HEADER, DEFAULT_USER)
|
||||||
|
.header(TENANT_HEADER, DEFAULT_TENANT))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.code").value(200))
|
||||||
|
.andExpect(jsonPath("$.data[0].orderNo").value(nullValue()))
|
||||||
|
.andExpect(jsonPath("$.data[0].orderId").value(orphanOrderId))
|
||||||
|
.andExpect(jsonPath("$.data[0].orderClerkId").value(nullValue()))
|
||||||
|
.andExpect(jsonPath("$.data[0].orderClerkNickname").value(nullValue()))
|
||||||
|
.andExpect(jsonPath("$.data[0].orderCustomerId").value(nullValue()))
|
||||||
|
.andExpect(jsonPath("$.data[0].orderCustomerNickname").value(nullValue()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void auditReturnsLinesSortedByCreatedTime() throws Exception {
|
||||||
|
ensureTenantContext();
|
||||||
|
PlayOrderInfoEntity order = seedOrder(LocalDateTime.now().minusHours(1));
|
||||||
|
WithdrawalRequestEntity request = seedWithdrawal(ApiTestDataSeeder.DEFAULT_TENANT_ID, new BigDecimal("90.00"));
|
||||||
|
LocalDateTime now = LocalDateTime.now();
|
||||||
|
String firstId = seedEarningLine(request.getId(), order.getId(), new BigDecimal("10"), "withdrawn", now.minusMinutes(20), EarningsType.ORDER);
|
||||||
|
Thread.sleep(5L);
|
||||||
|
String secondId = seedEarningLine(request.getId(), null, new BigDecimal("20"), "withdrawn", now.minusMinutes(10), EarningsType.ORDER);
|
||||||
|
Thread.sleep(5L);
|
||||||
|
String thirdId = seedEarningLine(request.getId(), null, new BigDecimal("30"), "withdrawn", now.minusMinutes(5), EarningsType.ORDER);
|
||||||
|
|
||||||
|
MvcResult result = mockMvc.perform(get("/admin/withdraw/requests/" + request.getId() + "/audit")
|
||||||
|
.header(USER_HEADER, DEFAULT_USER)
|
||||||
|
.header(TENANT_HEADER, DEFAULT_TENANT))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.code").value(200))
|
||||||
|
.andExpect(jsonPath("$.data.length()").value(3))
|
||||||
|
.andReturn();
|
||||||
|
|
||||||
|
JsonNode data = objectMapper.readTree(result.getResponse().getContentAsString()).get("data");
|
||||||
|
List<String> ids = new ArrayList<>();
|
||||||
|
LocalDateTime previous = null;
|
||||||
|
for (JsonNode node : data) {
|
||||||
|
ids.add(node.get("id").asText());
|
||||||
|
String createdText = node.path("createdTime").asText();
|
||||||
|
if (createdText != null && !createdText.isEmpty()) {
|
||||||
|
LocalDateTime created = LocalDateTime.parse(createdText, DATE_TIME_FORMATTER);
|
||||||
|
if (previous != null) {
|
||||||
|
Assertions.assertThat(created.isBefore(previous)).isFalse();
|
||||||
|
}
|
||||||
|
previous = created;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Assertions.assertThat(ids).containsExactlyInAnyOrder(firstId, secondId, thirdId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void auditSupportsCommissionEarnings() throws Exception {
|
||||||
|
ensureTenantContext();
|
||||||
|
WithdrawalRequestEntity request = seedWithdrawal(ApiTestDataSeeder.DEFAULT_TENANT_ID, new BigDecimal("55.00"));
|
||||||
|
seedEarningLine(request.getId(), null, new BigDecimal("55.00"), "withdrawn", LocalDateTime.now().minusMinutes(2), EarningsType.COMMISSION);
|
||||||
|
|
||||||
|
mockMvc.perform(get("/admin/withdraw/requests/" + request.getId() + "/audit")
|
||||||
|
.header(USER_HEADER, DEFAULT_USER)
|
||||||
|
.header(TENANT_HEADER, DEFAULT_TENANT))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.code").value(200))
|
||||||
|
.andExpect(jsonPath("$.data[0].earningType").value(EarningsType.COMMISSION.name()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("deprecation")
|
||||||
|
private PlayOrderInfoEntity seedOrder(LocalDateTime endTime) {
|
||||||
|
PlayOrderInfoEntity order = new PlayOrderInfoEntity();
|
||||||
|
String id = "order-audit-" + IdUtils.getUuid();
|
||||||
|
order.setId(id);
|
||||||
|
order.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
|
||||||
|
order.setOrderNo("ORD-" + System.currentTimeMillis());
|
||||||
|
order.setOrderStatus("3");
|
||||||
|
order.setOrderType("2");
|
||||||
|
order.setPlaceType("0");
|
||||||
|
order.setRewardType("0");
|
||||||
|
order.setAcceptBy(ApiTestDataSeeder.DEFAULT_CLERK_ID);
|
||||||
|
order.setPurchaserBy(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID);
|
||||||
|
order.setCommodityId(ApiTestDataSeeder.DEFAULT_COMMODITY_ID);
|
||||||
|
order.setOrderMoney(new BigDecimal("120.50"));
|
||||||
|
order.setFinalAmount(order.getOrderMoney());
|
||||||
|
order.setEstimatedRevenue(new BigDecimal("80.25"));
|
||||||
|
order.setOrderSettlementState("1");
|
||||||
|
order.setOrderEndTime(endTime);
|
||||||
|
order.setOrderSettlementTime(endTime);
|
||||||
|
Date nowDate = toDate(LocalDateTime.now());
|
||||||
|
order.setCreatedBy(ApiTestDataSeeder.DEFAULT_ADMIN_USER_ID);
|
||||||
|
order.setCreatedTime(nowDate);
|
||||||
|
order.setUpdatedBy(ApiTestDataSeeder.DEFAULT_ADMIN_USER_ID);
|
||||||
|
order.setUpdatedTime(nowDate);
|
||||||
|
order.setDeleted(false);
|
||||||
|
orderInfoService.save(order);
|
||||||
|
ordersToCleanup.add(id);
|
||||||
|
return order;
|
||||||
|
}
|
||||||
|
|
||||||
|
private WithdrawalRequestEntity seedWithdrawal(String tenantId, BigDecimal amount) {
|
||||||
|
WithdrawalRequestEntity entity = new WithdrawalRequestEntity();
|
||||||
|
String id = "withdraw-audit-" + IdUtils.getUuid();
|
||||||
|
entity.setId(id);
|
||||||
|
entity.setTenantId(tenantId);
|
||||||
|
entity.setClerkId(ApiTestDataSeeder.DEFAULT_CLERK_ID);
|
||||||
|
entity.setAmount(amount);
|
||||||
|
entity.setFee(BigDecimal.ZERO);
|
||||||
|
entity.setNetAmount(amount);
|
||||||
|
entity.setDestAccount("alipay:audit@test.com");
|
||||||
|
entity.setStatus("processing");
|
||||||
|
entity.setPayeeSnapshot("{\"displayName\":\"审计专用\"}");
|
||||||
|
Date nowDate = toDate(LocalDateTime.now());
|
||||||
|
entity.setCreatedBy(ApiTestDataSeeder.DEFAULT_ADMIN_USER_ID);
|
||||||
|
entity.setCreatedTime(nowDate);
|
||||||
|
entity.setUpdatedBy(ApiTestDataSeeder.DEFAULT_ADMIN_USER_ID);
|
||||||
|
entity.setUpdatedTime(nowDate);
|
||||||
|
withdrawalService.save(entity);
|
||||||
|
withdrawalsToCleanup.add(id);
|
||||||
|
return entity;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String seedEarningLine(String withdrawalId, String orderId, BigDecimal amount, String status, LocalDateTime createdAt, EarningsType earningType) {
|
||||||
|
EarningsLineEntity entity = new EarningsLineEntity();
|
||||||
|
String id = "earn-audit-" + IdUtils.getUuid();
|
||||||
|
entity.setId(id);
|
||||||
|
entity.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
|
||||||
|
entity.setClerkId(ApiTestDataSeeder.DEFAULT_CLERK_ID);
|
||||||
|
String resolvedOrderId = orderId != null ? orderId : "order-missing-" + IdUtils.getUuid();
|
||||||
|
entity.setOrderId(resolvedOrderId);
|
||||||
|
entity.setAmount(amount);
|
||||||
|
entity.setStatus(status);
|
||||||
|
entity.setEarningType(earningType);
|
||||||
|
entity.setUnlockTime(createdAt.minusHours(1));
|
||||||
|
entity.setWithdrawalId(withdrawalId);
|
||||||
|
Date createdDate = toDate(createdAt);
|
||||||
|
entity.setCreatedBy(ApiTestDataSeeder.DEFAULT_ADMIN_USER_ID);
|
||||||
|
entity.setCreatedTime(createdDate);
|
||||||
|
entity.setUpdatedBy(ApiTestDataSeeder.DEFAULT_ADMIN_USER_ID);
|
||||||
|
entity.setUpdatedTime(createdDate);
|
||||||
|
entity.setDeleted(false);
|
||||||
|
earningsService.save(entity);
|
||||||
|
earningsToCleanup.add(id);
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ensureProfileFixtures() {
|
||||||
|
ensureTenantContext();
|
||||||
|
ensureClerkFixture();
|
||||||
|
ensureCustomerFixture();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ensureClerkFixture() {
|
||||||
|
PlayClerkUserInfoEntity clerk = clerkUserInfoService.getById(ApiTestDataSeeder.DEFAULT_CLERK_ID);
|
||||||
|
if (clerk != null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
PlayClerkUserInfoEntity entity = new PlayClerkUserInfoEntity();
|
||||||
|
entity.setId(ApiTestDataSeeder.DEFAULT_CLERK_ID);
|
||||||
|
entity.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
|
||||||
|
entity.setSysUserId(ApiTestDataSeeder.DEFAULT_ADMIN_USER_ID);
|
||||||
|
entity.setOpenid(ApiTestDataSeeder.DEFAULT_CLERK_OPEN_ID);
|
||||||
|
entity.setNickname("小测官");
|
||||||
|
entity.setGroupId(ApiTestDataSeeder.DEFAULT_GROUP_ID);
|
||||||
|
entity.setLevelId(ApiTestDataSeeder.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("apitest-clerk-token");
|
||||||
|
entity.setDeleted(false);
|
||||||
|
clerkUserInfoService.save(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ensureCustomerFixture() {
|
||||||
|
PlayCustomUserInfoEntity customer = customUserInfoService.getById(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID);
|
||||||
|
if (customer != null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
PlayCustomUserInfoEntity entity = new PlayCustomUserInfoEntity();
|
||||||
|
entity.setId(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID);
|
||||||
|
entity.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
|
||||||
|
entity.setOpenid("openid-customer-apitest");
|
||||||
|
entity.setUnionid("unionid-customer-apitest");
|
||||||
|
entity.setNickname("测试顾客");
|
||||||
|
entity.setSex(1);
|
||||||
|
entity.setPhone("13700000002");
|
||||||
|
entity.setWeiChatCode("apitest-customer");
|
||||||
|
entity.setAccountBalance(new BigDecimal("200.00"));
|
||||||
|
entity.setAccumulatedRechargeAmount(new BigDecimal("200.00"));
|
||||||
|
entity.setAccumulatedConsumptionAmount(BigDecimal.ZERO);
|
||||||
|
entity.setAccountState("1");
|
||||||
|
entity.setSubscribeState("1");
|
||||||
|
entity.setPurchaseState("1");
|
||||||
|
entity.setMobilePhoneState("1");
|
||||||
|
Date now = new Date();
|
||||||
|
entity.setRegistrationTime(now);
|
||||||
|
entity.setLastLoginTime(now);
|
||||||
|
entity.setToken("apitest-customer-token");
|
||||||
|
entity.setDeleted(false);
|
||||||
|
customUserInfoService.save(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ensureTenantContext() {
|
||||||
|
SecurityUtils.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Date toDate(LocalDateTime value) {
|
||||||
|
return Date.from(value.atZone(ZoneId.systemDefault()).toInstant());
|
||||||
|
}
|
||||||
|
|
||||||
|
private String normalizeUtf8(String value) {
|
||||||
|
if (value == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
boolean convertible = value.chars().allMatch(ch -> ch <= 0xFF);
|
||||||
|
if (!convertible) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
byte[] bytes = value.getBytes(StandardCharsets.ISO_8859_1);
|
||||||
|
return new String(bytes, StandardCharsets.UTF_8);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,257 @@
|
|||||||
|
package com.starry.admin.api;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.hamcrest.Matchers.hasItem;
|
||||||
|
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
|
||||||
|
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||||
|
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||||
|
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put;
|
||||||
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||||
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||||
|
import com.starry.admin.common.apitest.ApiTestDataSeeder;
|
||||||
|
import com.starry.admin.modules.blindbox.mapper.BlindBoxPoolMapper;
|
||||||
|
import com.starry.admin.modules.blindbox.module.constant.BlindBoxConfigStatus;
|
||||||
|
import com.starry.admin.modules.blindbox.module.constant.BlindBoxPoolStatus;
|
||||||
|
import com.starry.admin.modules.blindbox.module.entity.BlindBoxConfigEntity;
|
||||||
|
import com.starry.admin.modules.blindbox.module.entity.BlindBoxPoolEntity;
|
||||||
|
import com.starry.admin.modules.blindbox.service.BlindBoxConfigService;
|
||||||
|
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.modules.shop.service.IPlayGiftInfoService;
|
||||||
|
import com.starry.admin.utils.SecurityUtils;
|
||||||
|
import com.starry.common.utils.IdUtils;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.math.RoundingMode;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import org.junit.jupiter.api.AfterEach;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.test.web.servlet.MvcResult;
|
||||||
|
|
||||||
|
class BlindBoxPoolControllerApiTest extends AbstractApiTest {
|
||||||
|
|
||||||
|
private static final String TEST_BLIND_BOX_ID = "blindbox-admin-api";
|
||||||
|
private static final DateTimeFormatter DATE_TIME_FORMATTER =
|
||||||
|
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private BlindBoxConfigService blindBoxConfigService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private IPlayGiftInfoService giftInfoService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private BlindBoxPoolMapper blindBoxPoolMapper;
|
||||||
|
|
||||||
|
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||||
|
private final List<Long> poolIdsToCleanup = new ArrayList<>();
|
||||||
|
private final List<String> giftIdsToCleanup = new ArrayList<>();
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
ensureTenantContext();
|
||||||
|
ensureBlindBoxConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
@AfterEach
|
||||||
|
void tearDown() {
|
||||||
|
ensureTenantContext();
|
||||||
|
if (!poolIdsToCleanup.isEmpty()) {
|
||||||
|
blindBoxPoolMapper.deleteBatchIds(poolIdsToCleanup);
|
||||||
|
poolIdsToCleanup.clear();
|
||||||
|
}
|
||||||
|
if (!giftIdsToCleanup.isEmpty()) {
|
||||||
|
giftInfoService.removeByIds(giftIdsToCleanup);
|
||||||
|
giftIdsToCleanup.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
// 测试用例:校验奖池管理 API 的新增、更新(禁用/修改权重时间库存)、删除以及礼物选项查询功能,
|
||||||
|
// 确保后台盲盒奖池的所有按钮都能正常调用并持久化。
|
||||||
|
void adminCanCreateUpdateToggleAndDeletePoolEntries() throws Exception {
|
||||||
|
PlayGiftInfoEntity freshlyAddedGift = seedGift("API盲盒新礼物");
|
||||||
|
giftIdsToCleanup.add(freshlyAddedGift.getId());
|
||||||
|
|
||||||
|
mockMvc.perform(get("/play/blind-box/pool/gifts")
|
||||||
|
.header(USER_HEADER, DEFAULT_USER)
|
||||||
|
.header(TENANT_HEADER, DEFAULT_TENANT)
|
||||||
|
.param("keyword", "盲盒新礼物"))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.code").value(200))
|
||||||
|
.andExpect(jsonPath("$.data[*].id", hasItem(freshlyAddedGift.getId())));
|
||||||
|
|
||||||
|
LocalDateTime now = LocalDateTime.now().withNano(0);
|
||||||
|
long configurableEntryId = createPoolEntryViaApi(
|
||||||
|
ApiTestDataSeeder.DEFAULT_GIFT_ID,
|
||||||
|
new BigDecimal("25.88"),
|
||||||
|
40,
|
||||||
|
8,
|
||||||
|
BlindBoxPoolStatus.ENABLED.getCode(),
|
||||||
|
now.minusDays(1),
|
||||||
|
now.plusDays(5));
|
||||||
|
|
||||||
|
LocalDateTime updatedFrom = now.minusHours(2);
|
||||||
|
LocalDateTime updatedTo = now.plusDays(10);
|
||||||
|
ObjectNode updatePayload = objectMapper.createObjectNode();
|
||||||
|
updatePayload.put("blindBoxId", TEST_BLIND_BOX_ID);
|
||||||
|
updatePayload.put("rewardGiftId", ApiTestDataSeeder.DEFAULT_GIFT_ID);
|
||||||
|
updatePayload.put("rewardPrice", "35.66");
|
||||||
|
updatePayload.put("weight", 75);
|
||||||
|
updatePayload.put("remainingStock", 3);
|
||||||
|
updatePayload.put("status", BlindBoxPoolStatus.DISABLED.getCode());
|
||||||
|
updatePayload.put("validFrom", updatedFrom.format(DATE_TIME_FORMATTER));
|
||||||
|
updatePayload.put("validTo", updatedTo.format(DATE_TIME_FORMATTER));
|
||||||
|
|
||||||
|
mockMvc.perform(put("/play/blind-box/pool/" + configurableEntryId)
|
||||||
|
.header(USER_HEADER, DEFAULT_USER)
|
||||||
|
.header(TENANT_HEADER, DEFAULT_TENANT)
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.content(updatePayload.toString()))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.code").value(200))
|
||||||
|
.andExpect(jsonPath("$.data.status").value(BlindBoxPoolStatus.DISABLED.getCode()))
|
||||||
|
.andExpect(jsonPath("$.data.weight").value(75))
|
||||||
|
.andExpect(jsonPath("$.data.remainingStock").value(3));
|
||||||
|
|
||||||
|
ensureTenantContext();
|
||||||
|
BlindBoxPoolEntity updated = blindBoxPoolMapper.selectById(configurableEntryId);
|
||||||
|
assertThat(updated).isNotNull();
|
||||||
|
assertThat(updated.getStatus()).isEqualTo(BlindBoxPoolStatus.DISABLED.getCode());
|
||||||
|
assertThat(updated.getWeight()).isEqualTo(75);
|
||||||
|
assertThat(updated.getRemainingStock()).isEqualTo(3);
|
||||||
|
assertThat(updated.getValidFrom()).isEqualTo(updatedFrom);
|
||||||
|
assertThat(updated.getValidTo()).isEqualTo(updatedTo);
|
||||||
|
|
||||||
|
long reusableEntryId = createPoolEntryViaApi(
|
||||||
|
freshlyAddedGift.getId(),
|
||||||
|
new BigDecimal("18.50"),
|
||||||
|
10,
|
||||||
|
1,
|
||||||
|
BlindBoxPoolStatus.ENABLED.getCode(),
|
||||||
|
now.minusDays(2),
|
||||||
|
now.plusDays(3));
|
||||||
|
|
||||||
|
updateGiftState(freshlyAddedGift.getId(), GiftState.OFF_SHELF);
|
||||||
|
|
||||||
|
ObjectNode inactiveUpdate = objectMapper.createObjectNode();
|
||||||
|
inactiveUpdate.put("blindBoxId", TEST_BLIND_BOX_ID);
|
||||||
|
inactiveUpdate.put("rewardGiftId", freshlyAddedGift.getId());
|
||||||
|
inactiveUpdate.put("weight", 15);
|
||||||
|
inactiveUpdate.put("status", BlindBoxPoolStatus.ENABLED.getCode());
|
||||||
|
inactiveUpdate.putNull("remainingStock");
|
||||||
|
|
||||||
|
mockMvc.perform(put("/play/blind-box/pool/" + reusableEntryId)
|
||||||
|
.header(USER_HEADER, DEFAULT_USER)
|
||||||
|
.header(TENANT_HEADER, DEFAULT_TENANT)
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.content(inactiveUpdate.toString()))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.code").value(200))
|
||||||
|
.andExpect(jsonPath("$.data.weight").value(15));
|
||||||
|
|
||||||
|
mockMvc.perform(delete("/play/blind-box/pool/" + reusableEntryId)
|
||||||
|
.header(USER_HEADER, DEFAULT_USER)
|
||||||
|
.header(TENANT_HEADER, DEFAULT_TENANT))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.code").value(200));
|
||||||
|
|
||||||
|
ensureTenantContext();
|
||||||
|
BlindBoxPoolEntity deleted = blindBoxPoolMapper.selectById(reusableEntryId);
|
||||||
|
assertThat(deleted).isNull();
|
||||||
|
poolIdsToCleanup.remove(reusableEntryId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private long createPoolEntryViaApi(
|
||||||
|
String giftId,
|
||||||
|
BigDecimal rewardPrice,
|
||||||
|
int weight,
|
||||||
|
Integer remainingStock,
|
||||||
|
int status,
|
||||||
|
LocalDateTime validFrom,
|
||||||
|
LocalDateTime validTo) throws Exception {
|
||||||
|
ObjectNode payload = objectMapper.createObjectNode();
|
||||||
|
payload.put("blindBoxId", TEST_BLIND_BOX_ID);
|
||||||
|
payload.put("rewardGiftId", giftId);
|
||||||
|
payload.put("rewardPrice", rewardPrice.setScale(2, RoundingMode.HALF_UP).toPlainString());
|
||||||
|
payload.put("weight", weight);
|
||||||
|
if (remainingStock != null) {
|
||||||
|
payload.put("remainingStock", remainingStock);
|
||||||
|
} else {
|
||||||
|
payload.putNull("remainingStock");
|
||||||
|
}
|
||||||
|
payload.put("status", status);
|
||||||
|
payload.put("validFrom", validFrom.format(DATE_TIME_FORMATTER));
|
||||||
|
payload.put("validTo", validTo.format(DATE_TIME_FORMATTER));
|
||||||
|
|
||||||
|
MvcResult result = mockMvc.perform(post("/play/blind-box/pool")
|
||||||
|
.header(USER_HEADER, DEFAULT_USER)
|
||||||
|
.header(TENANT_HEADER, DEFAULT_TENANT)
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.content(payload.toString()))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.code").value(200))
|
||||||
|
.andExpect(jsonPath("$.data.id").isNumber())
|
||||||
|
.andReturn();
|
||||||
|
|
||||||
|
JsonNode response = objectMapper.readTree(result.getResponse().getContentAsString());
|
||||||
|
long id = response.path("data").path("id").asLong();
|
||||||
|
poolIdsToCleanup.add(id);
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ensureBlindBoxConfig() {
|
||||||
|
BlindBoxConfigEntity existing = blindBoxConfigService.getById(TEST_BLIND_BOX_ID);
|
||||||
|
if (existing != null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
BlindBoxConfigEntity entity = new BlindBoxConfigEntity();
|
||||||
|
entity.setId(TEST_BLIND_BOX_ID);
|
||||||
|
entity.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
|
||||||
|
entity.setName("API盲盒(Admin)");
|
||||||
|
entity.setPrice(new BigDecimal("19.90"));
|
||||||
|
entity.setStatus(BlindBoxConfigStatus.ENABLED.getCode());
|
||||||
|
blindBoxConfigService.save(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
private PlayGiftInfoEntity seedGift(String name) {
|
||||||
|
PlayGiftInfoEntity gift = new PlayGiftInfoEntity();
|
||||||
|
gift.setId("gift-admin-" + IdUtils.getUuid());
|
||||||
|
gift.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
|
||||||
|
gift.setHistory(GiftHistory.CURRENT.getCode());
|
||||||
|
gift.setName(name);
|
||||||
|
gift.setType(GiftType.NORMAL.getCode());
|
||||||
|
gift.setUrl("https://example.com/assets/" + gift.getId() + ".png");
|
||||||
|
gift.setPrice(new BigDecimal("58.80"));
|
||||||
|
gift.setUnit("CNY");
|
||||||
|
gift.setState(GiftState.ACTIVE.getCode());
|
||||||
|
gift.setListingTime(LocalDateTime.now().minusDays(1));
|
||||||
|
gift.setRemark("Seeded for blind box pool admin test");
|
||||||
|
giftInfoService.save(gift);
|
||||||
|
return gift;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateGiftState(String giftId, GiftState state) {
|
||||||
|
ensureTenantContext();
|
||||||
|
PlayGiftInfoEntity gift = giftInfoService.getById(giftId);
|
||||||
|
if (gift != null) {
|
||||||
|
gift.setState(state.getCode());
|
||||||
|
giftInfoService.updateById(gift);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void ensureTenantContext() {
|
||||||
|
SecurityUtils.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ import com.starry.admin.modules.blindbox.mapper.BlindBoxPoolMapper;
|
|||||||
import com.starry.admin.modules.blindbox.mapper.BlindBoxRewardMapper;
|
import com.starry.admin.modules.blindbox.mapper.BlindBoxRewardMapper;
|
||||||
import com.starry.admin.modules.blindbox.module.constant.BlindBoxConfigStatus;
|
import com.starry.admin.modules.blindbox.module.constant.BlindBoxConfigStatus;
|
||||||
import com.starry.admin.modules.blindbox.module.constant.BlindBoxPoolStatus;
|
import com.starry.admin.modules.blindbox.module.constant.BlindBoxPoolStatus;
|
||||||
|
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.BlindBoxConfigEntity;
|
||||||
import com.starry.admin.modules.blindbox.module.entity.BlindBoxPoolEntity;
|
import com.starry.admin.modules.blindbox.module.entity.BlindBoxPoolEntity;
|
||||||
import com.starry.admin.modules.blindbox.module.entity.BlindBoxRewardEntity;
|
import com.starry.admin.modules.blindbox.module.entity.BlindBoxRewardEntity;
|
||||||
@@ -34,6 +35,11 @@ class BlindBoxServiceWeightTest extends WxCustomOrderApiTestSupport {
|
|||||||
private static final String TEST_BLIND_BOX_ID = "blindbox-apitest";
|
private static final String TEST_BLIND_BOX_ID = "blindbox-apitest";
|
||||||
private static final String PRIMARY_GIFT_ID = ApiTestDataSeeder.DEFAULT_GIFT_ID;
|
private static final String PRIMARY_GIFT_ID = ApiTestDataSeeder.DEFAULT_GIFT_ID;
|
||||||
private static final String SECONDARY_GIFT_ID = "gift-blindbox-secondary";
|
private static final String SECONDARY_GIFT_ID = "gift-blindbox-secondary";
|
||||||
|
private static final String MIXED_GIFT_A_ID = "gift-blindbox-mixed-a";
|
||||||
|
private static final String MIXED_GIFT_B_ID = "gift-blindbox-mixed-b";
|
||||||
|
private static final String MIXED_GIFT_C_ID = "gift-blindbox-mixed-c";
|
||||||
|
private static final String MIXED_GIFT_D_ID = "gift-blindbox-mixed-d";
|
||||||
|
private static final String MIXED_GIFT_E_ID = "gift-blindbox-mixed-e";
|
||||||
private static final int DRAW_ATTEMPT_COUNT = 1_000;
|
private static final int DRAW_ATTEMPT_COUNT = 1_000;
|
||||||
private static final int PRIMARY_WEIGHT = 80;
|
private static final int PRIMARY_WEIGHT = 80;
|
||||||
private static final int SECONDARY_WEIGHT = 20;
|
private static final int SECONDARY_WEIGHT = 20;
|
||||||
@@ -41,6 +47,11 @@ class BlindBoxServiceWeightTest extends WxCustomOrderApiTestSupport {
|
|||||||
private static final double PRIMARY_RATIO_MAX = 0.88;
|
private static final double PRIMARY_RATIO_MAX = 0.88;
|
||||||
private static final double SECONDARY_RATIO_MIN = 0.12;
|
private static final double SECONDARY_RATIO_MIN = 0.12;
|
||||||
private static final double SECONDARY_RATIO_MAX = 0.32;
|
private static final double SECONDARY_RATIO_MAX = 0.32;
|
||||||
|
private static final int MIXED_TOTAL_DRAWS = 400;
|
||||||
|
private static final int MIXED_GIFT_A_STOCK = 10;
|
||||||
|
private static final int MIXED_GIFT_B_STOCK = 5;
|
||||||
|
private static final int MIXED_GIFT_C_STOCK = 0;
|
||||||
|
private static final int MIXED_GIFT_E_STOCK = 2;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private BlindBoxService blindBoxService;
|
private BlindBoxService blindBoxService;
|
||||||
@@ -112,6 +123,66 @@ class BlindBoxServiceWeightTest extends WxCustomOrderApiTestSupport {
|
|||||||
purgePool();
|
purgePool();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
// 测试用例:混合 5 种礼物(部分限量、部分不限量),验证限量礼物被抽满后不再出现,
|
||||||
|
// 不限量礼物可继续抽取,且库存为 0 的礼物永远不会返回。
|
||||||
|
void blindBoxDrawHandlesMixedInventory() {
|
||||||
|
ensureTenantContext();
|
||||||
|
ensureBlindBoxConfig();
|
||||||
|
ensureMixedGifts();
|
||||||
|
resetCustomerBalance();
|
||||||
|
|
||||||
|
purgeRewards();
|
||||||
|
purgePool();
|
||||||
|
insertPoolEntry(MIXED_GIFT_A_ID, 40, MIXED_GIFT_A_STOCK);
|
||||||
|
insertPoolEntry(MIXED_GIFT_B_ID, 25, MIXED_GIFT_B_STOCK);
|
||||||
|
insertPoolEntry(MIXED_GIFT_C_ID, 15, MIXED_GIFT_C_STOCK);
|
||||||
|
insertPoolEntry(MIXED_GIFT_D_ID, 10, null);
|
||||||
|
insertPoolEntry(MIXED_GIFT_E_ID, 10, MIXED_GIFT_E_STOCK);
|
||||||
|
|
||||||
|
Map<String, Integer> frequency = new HashMap<>();
|
||||||
|
for (int i = 0; i < MIXED_TOTAL_DRAWS; i++) {
|
||||||
|
BlindBoxRewardEntity reward = blindBoxService.drawReward(
|
||||||
|
ApiTestDataSeeder.DEFAULT_TENANT_ID,
|
||||||
|
"mixed-order-" + i,
|
||||||
|
ApiTestDataSeeder.DEFAULT_CUSTOMER_ID,
|
||||||
|
TEST_BLIND_BOX_ID,
|
||||||
|
"mixed-seed-" + i);
|
||||||
|
frequency.merge(reward.getRewardGiftId(), 1, Integer::sum);
|
||||||
|
}
|
||||||
|
|
||||||
|
Assertions.assertThat(frequency.getOrDefault(MIXED_GIFT_C_ID, 0))
|
||||||
|
.as("库存为 0 的礼物永远不应被抽中")
|
||||||
|
.isEqualTo(0);
|
||||||
|
Assertions.assertThat(frequency.getOrDefault(MIXED_GIFT_A_ID, 0))
|
||||||
|
.as("限量礼物 A 应被精确抽完")
|
||||||
|
.isEqualTo(MIXED_GIFT_A_STOCK);
|
||||||
|
Assertions.assertThat(frequency.getOrDefault(MIXED_GIFT_B_ID, 0))
|
||||||
|
.as("限量礼物 B 应被精确抽完")
|
||||||
|
.isEqualTo(MIXED_GIFT_B_STOCK);
|
||||||
|
Assertions.assertThat(frequency.getOrDefault(MIXED_GIFT_E_ID, 0))
|
||||||
|
.as("限量礼物 E 应被精确抽完")
|
||||||
|
.isEqualTo(MIXED_GIFT_E_STOCK);
|
||||||
|
|
||||||
|
int unlimitedCount = frequency.getOrDefault(MIXED_GIFT_D_ID, 0);
|
||||||
|
int finiteTotal = MIXED_GIFT_A_STOCK + MIXED_GIFT_B_STOCK + MIXED_GIFT_E_STOCK;
|
||||||
|
Assertions.assertThat(unlimitedCount)
|
||||||
|
.as("不限量礼物承担剩余抽奖次数")
|
||||||
|
.isEqualTo(MIXED_TOTAL_DRAWS - finiteTotal)
|
||||||
|
.isGreaterThan(0);
|
||||||
|
|
||||||
|
// 抽完后,奖池中仅剩不限量礼物
|
||||||
|
java.util.List<BlindBoxCandidate> remaining = blindBoxPoolMapper.listActiveEntries(
|
||||||
|
ApiTestDataSeeder.DEFAULT_TENANT_ID, TEST_BLIND_BOX_ID, LocalDateTime.now());
|
||||||
|
Assertions.assertThat(remaining)
|
||||||
|
.as("只剩不限量礼物可用")
|
||||||
|
.hasSize(1);
|
||||||
|
Assertions.assertThat(remaining.get(0).getRewardGiftId()).isEqualTo(MIXED_GIFT_D_ID);
|
||||||
|
|
||||||
|
purgeRewards();
|
||||||
|
purgePool();
|
||||||
|
}
|
||||||
|
|
||||||
private void ensureBlindBoxConfig() {
|
private void ensureBlindBoxConfig() {
|
||||||
BlindBoxConfigEntity config = blindBoxConfigService.getById(TEST_BLIND_BOX_ID);
|
BlindBoxConfigEntity config = blindBoxConfigService.getById(TEST_BLIND_BOX_ID);
|
||||||
if (config != null) {
|
if (config != null) {
|
||||||
@@ -129,46 +200,81 @@ class BlindBoxServiceWeightTest extends WxCustomOrderApiTestSupport {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void ensureSecondaryGift() {
|
private void ensureSecondaryGift() {
|
||||||
PlayGiftInfoEntity existing = findGift(SECONDARY_GIFT_ID);
|
ensureGift(
|
||||||
if (existing != null) {
|
SECONDARY_GIFT_ID,
|
||||||
return;
|
"API盲盒奖励",
|
||||||
}
|
new BigDecimal("9.99"),
|
||||||
PlayGiftInfoEntity entity = new PlayGiftInfoEntity();
|
"https://example.com/apitest/blindbox.png",
|
||||||
entity.setId(SECONDARY_GIFT_ID);
|
"Seeded secondary gift for blind box tests");
|
||||||
entity.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
|
|
||||||
entity.setHistory(GiftHistory.CURRENT.getCode());
|
|
||||||
entity.setName("API盲盒奖励");
|
|
||||||
entity.setType(GiftType.NORMAL.getCode());
|
|
||||||
entity.setUrl("https://example.com/apitest/blindbox.png");
|
|
||||||
entity.setPrice(new BigDecimal("9.99"));
|
|
||||||
entity.setUnit("CNY");
|
|
||||||
entity.setState(GiftState.ACTIVE.getCode());
|
|
||||||
entity.setListingTime(LocalDateTime.now());
|
|
||||||
entity.setRemark("Seeded secondary gift for blind box tests");
|
|
||||||
giftInfoService.save(entity);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ensurePrimaryGift() {
|
private void ensurePrimaryGift() {
|
||||||
PlayGiftInfoEntity existing = findGift(PRIMARY_GIFT_ID);
|
ensureGift(
|
||||||
|
PRIMARY_GIFT_ID,
|
||||||
|
ApiTestDataSeeder.DEFAULT_GIFT_NAME,
|
||||||
|
new BigDecimal("15.00"),
|
||||||
|
"https://example.com/apitest/gift-basic.png",
|
||||||
|
"Seeded default gift for blind box tests");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ensureMixedGifts() {
|
||||||
|
ensureGift(
|
||||||
|
MIXED_GIFT_A_ID,
|
||||||
|
"API盲盒混合A",
|
||||||
|
new BigDecimal("11.11"),
|
||||||
|
"https://example.com/apitest/mixed-a.png",
|
||||||
|
"Mixed blind box gift A");
|
||||||
|
ensureGift(
|
||||||
|
MIXED_GIFT_B_ID,
|
||||||
|
"API盲盒混合B",
|
||||||
|
new BigDecimal("22.22"),
|
||||||
|
"https://example.com/apitest/mixed-b.png",
|
||||||
|
"Mixed blind box gift B");
|
||||||
|
ensureGift(
|
||||||
|
MIXED_GIFT_C_ID,
|
||||||
|
"API盲盒混合C",
|
||||||
|
new BigDecimal("33.33"),
|
||||||
|
"https://example.com/apitest/mixed-c.png",
|
||||||
|
"Mixed blind box gift C");
|
||||||
|
ensureGift(
|
||||||
|
MIXED_GIFT_D_ID,
|
||||||
|
"API盲盒混合D",
|
||||||
|
new BigDecimal("44.44"),
|
||||||
|
"https://example.com/apitest/mixed-d.png",
|
||||||
|
"Mixed blind box gift D");
|
||||||
|
ensureGift(
|
||||||
|
MIXED_GIFT_E_ID,
|
||||||
|
"API盲盒混合E",
|
||||||
|
new BigDecimal("55.55"),
|
||||||
|
"https://example.com/apitest/mixed-e.png",
|
||||||
|
"Mixed blind box gift E");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ensureGift(String giftId, String name, BigDecimal price, String imageUrl, String remark) {
|
||||||
|
PlayGiftInfoEntity existing = findGift(giftId);
|
||||||
if (existing != null) {
|
if (existing != null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
PlayGiftInfoEntity entity = new PlayGiftInfoEntity();
|
PlayGiftInfoEntity entity = new PlayGiftInfoEntity();
|
||||||
entity.setId(PRIMARY_GIFT_ID);
|
entity.setId(giftId);
|
||||||
entity.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
|
entity.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
|
||||||
entity.setHistory(GiftHistory.CURRENT.getCode());
|
entity.setHistory(GiftHistory.CURRENT.getCode());
|
||||||
entity.setName(ApiTestDataSeeder.DEFAULT_GIFT_NAME);
|
entity.setName(name);
|
||||||
entity.setType(GiftType.NORMAL.getCode());
|
entity.setType(GiftType.NORMAL.getCode());
|
||||||
entity.setUrl("https://example.com/apitest/gift-basic.png");
|
entity.setUrl(imageUrl);
|
||||||
entity.setPrice(new BigDecimal("15.00"));
|
entity.setPrice(price);
|
||||||
entity.setUnit("CNY");
|
entity.setUnit("CNY");
|
||||||
entity.setState(GiftState.ACTIVE.getCode());
|
entity.setState(GiftState.ACTIVE.getCode());
|
||||||
entity.setListingTime(LocalDateTime.now());
|
entity.setListingTime(LocalDateTime.now());
|
||||||
entity.setRemark("Seeded default gift for blind box tests");
|
entity.setRemark(remark);
|
||||||
giftInfoService.save(entity);
|
giftInfoService.save(entity);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void insertPoolEntry(String giftId, int weight) {
|
private void insertPoolEntry(String giftId, int weight) {
|
||||||
|
insertPoolEntry(giftId, weight, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void insertPoolEntry(String giftId, int weight, Integer remainingStock) {
|
||||||
PlayGiftInfoEntity gift = findGift(giftId);
|
PlayGiftInfoEntity gift = findGift(giftId);
|
||||||
if (gift == null) {
|
if (gift == null) {
|
||||||
throw new IllegalStateException("Expected gift to be seeded: " + giftId);
|
throw new IllegalStateException("Expected gift to be seeded: " + giftId);
|
||||||
@@ -179,7 +285,7 @@ class BlindBoxServiceWeightTest extends WxCustomOrderApiTestSupport {
|
|||||||
entry.setRewardGiftId(giftId);
|
entry.setRewardGiftId(giftId);
|
||||||
entry.setRewardPrice(gift.getPrice());
|
entry.setRewardPrice(gift.getPrice());
|
||||||
entry.setWeight(weight);
|
entry.setWeight(weight);
|
||||||
entry.setRemainingStock(null);
|
entry.setRemainingStock(remainingStock);
|
||||||
entry.setValidFrom(null);
|
entry.setValidFrom(null);
|
||||||
entry.setValidTo(null);
|
entry.setValidTo(null);
|
||||||
entry.setStatus(BlindBoxPoolStatus.ENABLED.getCode());
|
entry.setStatus(BlindBoxPoolStatus.ENABLED.getCode());
|
||||||
|
|||||||
@@ -17,8 +17,12 @@ import com.starry.admin.utils.SecurityUtils;
|
|||||||
import com.starry.common.utils.IdUtils;
|
import com.starry.common.utils.IdUtils;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.concurrent.ThreadLocalRandom;
|
import java.util.concurrent.ThreadLocalRandom;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
import org.junit.jupiter.api.AfterEach;
|
import org.junit.jupiter.api.AfterEach;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
@@ -37,6 +41,13 @@ class PlayClerkUserInfoApiTest extends AbstractApiTest {
|
|||||||
|
|
||||||
private final List<String> levelIdsToCleanup = new ArrayList<>();
|
private final List<String> levelIdsToCleanup = new ArrayList<>();
|
||||||
private final List<String> clerkIdsToCleanup = new ArrayList<>();
|
private final List<String> clerkIdsToCleanup = new ArrayList<>();
|
||||||
|
private int scenarioSequence = 0;
|
||||||
|
private static final Comparator<ClerkScenario> BACKEND_ORDERING = Comparator
|
||||||
|
.comparing(ClerkScenario::isOnline, Comparator.reverseOrder())
|
||||||
|
.thenComparing(ClerkScenario::isPinned, Comparator.reverseOrder())
|
||||||
|
.thenComparingLong(ClerkScenario::getLevelOrder)
|
||||||
|
.thenComparingInt(ClerkScenario::getSequence)
|
||||||
|
.thenComparing(ClerkScenario::getId);
|
||||||
|
|
||||||
@AfterEach
|
@AfterEach
|
||||||
void tearDown() {
|
void tearDown() {
|
||||||
@@ -83,9 +94,10 @@ class PlayClerkUserInfoApiTest extends AbstractApiTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
assertThat(orderedIds).contains(lowOrderClerkId, highOrderClerkId);
|
assertThat(orderedIds).contains(lowOrderClerkId, highOrderClerkId);
|
||||||
assertThat(orderedIds.indexOf(lowOrderClerkId))
|
assertThat(orderedIds.indexOf(highOrderClerkId))
|
||||||
.withFailMessage("Unexpected order for token %s: %s", filterToken, orderedIds)
|
.withFailMessage("Online clerk should appear before offline regardless of level. token=%s list=%s",
|
||||||
.isLessThan(orderedIds.indexOf(highOrderClerkId));
|
filterToken, orderedIds)
|
||||||
|
.isLessThan(orderedIds.indexOf(lowOrderClerkId));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -319,4 +331,178 @@ class PlayClerkUserInfoApiTest extends AbstractApiTest {
|
|||||||
clerkIdsToCleanup.add(clerkId);
|
clerkIdsToCleanup.add(clerkId);
|
||||||
return clerkId;
|
return clerkId;
|
||||||
}
|
}
|
||||||
|
@Test
|
||||||
|
void listOrderingStableWithMultipleCriteria() throws Exception {
|
||||||
|
ensureTenantContext();
|
||||||
|
PlayClerkLevelInfoEntity level = createClerkLevel("stable", 10L, 80);
|
||||||
|
|
||||||
|
String filterToken = "stable-" + IdUtils.getUuid().substring(0, 6);
|
||||||
|
String pinnedOnline = createClerk(filterToken + "-pinned-online", level.getId(), "1");
|
||||||
|
togglePin(pinnedOnline, "1");
|
||||||
|
String pinnedOffline = createClerk(filterToken + "-pinned-offline", level.getId(), "0");
|
||||||
|
togglePin(pinnedOffline, "1");
|
||||||
|
String online1 = createClerk(filterToken + "-online-one", level.getId(), "1");
|
||||||
|
pause(50);
|
||||||
|
String online2 = createClerk(filterToken + "-online-two", level.getId(), "1");
|
||||||
|
String offline = createClerk(filterToken + "-offline", level.getId(), "0");
|
||||||
|
|
||||||
|
MvcResult result = mockMvc.perform(get("/clerk/user/list")
|
||||||
|
.param("pageNum", "1")
|
||||||
|
.param("pageSize", "20")
|
||||||
|
.param("nickname", filterToken)
|
||||||
|
.header(TENANT_HEADER, DEFAULT_TENANT)
|
||||||
|
.header(USER_HEADER, DEFAULT_USER))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andReturn();
|
||||||
|
|
||||||
|
JsonNode root = objectMapper.readTree(result.getResponse().getContentAsString());
|
||||||
|
JsonNode records = root.path("data");
|
||||||
|
assertThat(records.isArray()).isTrue();
|
||||||
|
List<String> orderedIds = new ArrayList<>();
|
||||||
|
for (JsonNode record : records) {
|
||||||
|
orderedIds.add(record.path("id").asText());
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, ClerkScenario> expectedScenarios = new HashMap<>();
|
||||||
|
expectedScenarios.put(pinnedOnline, new ClerkScenario(pinnedOnline, 1L, true, true, 0));
|
||||||
|
expectedScenarios.put(online1, new ClerkScenario(online1, 1L, true, false, 1));
|
||||||
|
expectedScenarios.put(online2, new ClerkScenario(online2, 1L, true, false, 2));
|
||||||
|
expectedScenarios.put(pinnedOffline, new ClerkScenario(pinnedOffline, 1L, false, true, 3));
|
||||||
|
expectedScenarios.put(offline, new ClerkScenario(offline, 1L, false, false, 4));
|
||||||
|
|
||||||
|
List<ClerkScenario> actualScenarios = orderedIds.stream()
|
||||||
|
.map(expectedScenarios::get)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
for (int i = 1; i < actualScenarios.size(); i++) {
|
||||||
|
ClerkScenario previous = actualScenarios.get(i - 1);
|
||||||
|
ClerkScenario current = actualScenarios.get(i);
|
||||||
|
assertThat(previous).isNotNull();
|
||||||
|
assertThat(current).isNotNull();
|
||||||
|
assertThat(BACKEND_ORDERING.compare(previous, current))
|
||||||
|
.withFailMessage("Ordering violation between %s and %s, list=%s", previous.getId(), current.getId(),
|
||||||
|
orderedIds)
|
||||||
|
.isLessThanOrEqualTo(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void listOrderingHandlesBulkDataset() throws Exception {
|
||||||
|
ensureTenantContext();
|
||||||
|
String token = "bulk-" + IdUtils.getUuid().substring(0, 6);
|
||||||
|
PlayClerkLevelInfoEntity gold = createClerkLevel(token + "-gold", 1L, 90);
|
||||||
|
PlayClerkLevelInfoEntity silver = createClerkLevel(token + "-silver", 2L, 80);
|
||||||
|
PlayClerkLevelInfoEntity iron = createClerkLevel(token + "-iron", 3L, 70);
|
||||||
|
|
||||||
|
List<ClerkScenario> scenarios = new ArrayList<>();
|
||||||
|
scenarios.add(buildScenario(token, "G-Pin-ON", gold, true, true));
|
||||||
|
scenarios.add(buildScenario(token, "G-UnPin-ON", gold, true, false));
|
||||||
|
scenarios.add(buildScenario(token, "G-Pin-Off", gold, false, true));
|
||||||
|
scenarios.add(buildScenario(token, "G-UnPin-Off", gold, false, false));
|
||||||
|
scenarios.add(buildScenario(token, "S-Pin-ON", silver, true, true));
|
||||||
|
scenarios.add(buildScenario(token, "S-UnPin-ON", silver, true, false));
|
||||||
|
scenarios.add(buildScenario(token, "S-Pin-Off", silver, false, true));
|
||||||
|
scenarios.add(buildScenario(token, "S-UnPin-Off", silver, false, false));
|
||||||
|
scenarios.add(buildScenario(token, "I-Pin-ON", iron, true, true));
|
||||||
|
scenarios.add(buildScenario(token, "I-UnPin-ON", iron, true, false));
|
||||||
|
scenarios.add(buildScenario(token, "I-Pin-Off", iron, false, true));
|
||||||
|
scenarios.add(buildScenario(token, "I-UnPin-Off", iron, false, false));
|
||||||
|
|
||||||
|
MvcResult result = mockMvc.perform(get("/clerk/user/list")
|
||||||
|
.param("pageNum", "1")
|
||||||
|
.param("pageSize", "80")
|
||||||
|
.param("nickname", token)
|
||||||
|
.header(TENANT_HEADER, DEFAULT_TENANT)
|
||||||
|
.header(USER_HEADER, DEFAULT_USER))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andReturn();
|
||||||
|
|
||||||
|
JsonNode root = objectMapper.readTree(result.getResponse().getContentAsString());
|
||||||
|
JsonNode records = root.path("data");
|
||||||
|
List<String> orderedIds = new ArrayList<>();
|
||||||
|
for (JsonNode record : records) {
|
||||||
|
orderedIds.add(record.path("id").asText());
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, ClerkScenario> scenarioById = scenarios.stream()
|
||||||
|
.collect(Collectors.toMap(ClerkScenario::getId, scenario -> scenario));
|
||||||
|
|
||||||
|
assertThat(orderedIds).containsExactlyInAnyOrderElementsOf(scenarioById.keySet());
|
||||||
|
|
||||||
|
List<ClerkScenario> orderedScenarios = orderedIds.stream()
|
||||||
|
.map(scenarioById::get)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
for (int i = 1; i < orderedScenarios.size(); i++) {
|
||||||
|
ClerkScenario previous = orderedScenarios.get(i - 1);
|
||||||
|
ClerkScenario current = orderedScenarios.get(i);
|
||||||
|
assertThat(BACKEND_ORDERING.compare(previous, current))
|
||||||
|
.withFailMessage("Ordering violation between %s and %s, list=%s",
|
||||||
|
previous.getId(), current.getId(), orderedIds)
|
||||||
|
.isLessThanOrEqualTo(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private ClerkScenario buildScenario(String token, String suffix, PlayClerkLevelInfoEntity level, boolean online, boolean pinned) {
|
||||||
|
String id = createClerk(token + "-" + suffix, level.getId(), online ? "1" : "0");
|
||||||
|
if (pinned) {
|
||||||
|
togglePin(id, "1");
|
||||||
|
}
|
||||||
|
long levelOrder = level.getOrderNumber() == null ? Long.MAX_VALUE : level.getOrderNumber();
|
||||||
|
ClerkScenario scenario = new ClerkScenario(id, levelOrder, online, pinned, scenarioSequence++);
|
||||||
|
pause(15);
|
||||||
|
return scenario;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void togglePin(String clerkId, String pinState) {
|
||||||
|
ensureTenantContext();
|
||||||
|
PlayClerkUserInfoEntity update = new PlayClerkUserInfoEntity();
|
||||||
|
update.setId(clerkId);
|
||||||
|
update.setPinToTopState(pinState);
|
||||||
|
clerkUserInfoService.update(update);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void pause(long millis) {
|
||||||
|
try {
|
||||||
|
Thread.sleep(millis);
|
||||||
|
} catch (InterruptedException ex) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class ClerkScenario {
|
||||||
|
private final String id;
|
||||||
|
private final long levelOrder;
|
||||||
|
private final boolean online;
|
||||||
|
private final boolean pinned;
|
||||||
|
private final int sequence;
|
||||||
|
|
||||||
|
ClerkScenario(String id, long levelOrder, boolean online, boolean pinned, int sequence) {
|
||||||
|
this.id = id;
|
||||||
|
this.levelOrder = levelOrder;
|
||||||
|
this.online = online;
|
||||||
|
this.pinned = pinned;
|
||||||
|
this.sequence = sequence;
|
||||||
|
}
|
||||||
|
|
||||||
|
String getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
long getLevelOrder() {
|
||||||
|
return levelOrder;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean isOnline() {
|
||||||
|
return online;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean isPinned() {
|
||||||
|
return pinned;
|
||||||
|
}
|
||||||
|
|
||||||
|
int getSequence() {
|
||||||
|
return sequence;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -221,6 +221,39 @@ class PlayCommodityInfoApiTest extends WxCustomOrderApiTestSupport {
|
|||||||
assertThat(pricing.getPrice()).isEqualByComparingTo(new BigDecimal("188.50"));
|
assertThat(pricing.getPrice()).isEqualByComparingTo(new BigDecimal("188.50"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
// 测试用例:调用价格更新接口时若某个等级价格传入null,接口应忽略该列并保持原价,确保支持分步维护价格。
|
||||||
|
void updateInfoSkipsNullLevelPrices() throws Exception {
|
||||||
|
ensureTenantContext();
|
||||||
|
PlayCommodityAndLevelInfoEntity before = commodityAndLevelInfoService.lambdaQuery()
|
||||||
|
.eq(PlayCommodityAndLevelInfoEntity::getCommodityId, ApiTestDataSeeder.DEFAULT_COMMODITY_ID)
|
||||||
|
.eq(PlayCommodityAndLevelInfoEntity::getLevelId, ApiTestDataSeeder.DEFAULT_CLERK_LEVEL_ID)
|
||||||
|
.one();
|
||||||
|
|
||||||
|
assertThat(before).as("种子数据应具备默认等级价格").isNotNull();
|
||||||
|
|
||||||
|
ObjectNode payload = objectMapper.createObjectNode();
|
||||||
|
payload.put("id", ApiTestDataSeeder.DEFAULT_COMMODITY_ID);
|
||||||
|
payload.putNull(ApiTestDataSeeder.DEFAULT_CLERK_LEVEL_ID);
|
||||||
|
|
||||||
|
mockMvc.perform(post("/shop/commodity/updateInfo")
|
||||||
|
.header(USER_HEADER, DEFAULT_USER)
|
||||||
|
.header(TENANT_HEADER, DEFAULT_TENANT)
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.content(payload.toString()))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.code").value(200));
|
||||||
|
|
||||||
|
ensureTenantContext();
|
||||||
|
PlayCommodityAndLevelInfoEntity after = commodityAndLevelInfoService.lambdaQuery()
|
||||||
|
.eq(PlayCommodityAndLevelInfoEntity::getCommodityId, ApiTestDataSeeder.DEFAULT_COMMODITY_ID)
|
||||||
|
.eq(PlayCommodityAndLevelInfoEntity::getLevelId, ApiTestDataSeeder.DEFAULT_CLERK_LEVEL_ID)
|
||||||
|
.one();
|
||||||
|
|
||||||
|
assertThat(after).isNotNull();
|
||||||
|
assertThat(after.getPrice()).as("空价格不应覆盖原值").isEqualByComparingTo(before.getPrice());
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
// 测试用例:使用商品修改接口把自动结算等待时长从不限时(-1)调整为10分钟,验证更新后查询返回新的配置。
|
// 测试用例:使用商品修改接口把自动结算等待时长从不限时(-1)调整为10分钟,验证更新后查询返回新的配置。
|
||||||
void updateEndpointSwitchesAutomaticSettlement() throws Exception {
|
void updateEndpointSwitchesAutomaticSettlement() throws Exception {
|
||||||
|
|||||||
@@ -0,0 +1,883 @@
|
|||||||
|
package com.starry.admin.api;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||||
|
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||||
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||||
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.fasterxml.jackson.databind.node.ArrayNode;
|
||||||
|
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||||
|
import com.starry.admin.common.apitest.ApiTestDataSeeder;
|
||||||
|
import com.starry.admin.modules.order.module.constant.OrderConstant;
|
||||||
|
import com.starry.admin.modules.order.module.constant.OrderConstant.OrderStatus;
|
||||||
|
import com.starry.admin.modules.order.module.entity.PlayOrderInfoEntity;
|
||||||
|
import com.starry.admin.modules.order.module.entity.PlayOrderRefundInfoEntity;
|
||||||
|
import com.starry.admin.modules.order.service.IPlayOrderInfoService;
|
||||||
|
import com.starry.admin.modules.order.service.IPlayOrderRefundInfoService;
|
||||||
|
import com.starry.admin.modules.withdraw.entity.EarningsLineEntity;
|
||||||
|
import com.starry.admin.modules.withdraw.enums.EarningsType;
|
||||||
|
import com.starry.admin.modules.withdraw.service.IEarningsService;
|
||||||
|
import com.starry.admin.utils.SecurityUtils;
|
||||||
|
import com.starry.common.context.CustomSecurityContextHolder;
|
||||||
|
import com.starry.common.utils.IdUtils;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.ZoneId;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
import org.junit.jupiter.api.AfterEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.test.web.servlet.MvcResult;
|
||||||
|
|
||||||
|
class PlayOrderInfoControllerApiTest extends AbstractApiTest {
|
||||||
|
|
||||||
|
private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||||
|
private static final BigDecimal DEFAULT_AMOUNT = new BigDecimal("188.00");
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private IPlayOrderInfoService orderInfoService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private IEarningsService earningsService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private IPlayOrderRefundInfoService orderRefundInfoService;
|
||||||
|
|
||||||
|
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||||
|
private final List<String> orderIdsToCleanup = new ArrayList<>();
|
||||||
|
private final List<String> earningsLineIdsToCleanup = new ArrayList<>();
|
||||||
|
private final List<String> refundIdsToCleanup = new ArrayList<>();
|
||||||
|
|
||||||
|
@AfterEach
|
||||||
|
void tearDown() {
|
||||||
|
ensureTenantContext();
|
||||||
|
if (!earningsLineIdsToCleanup.isEmpty()) {
|
||||||
|
earningsService.removeByIds(earningsLineIdsToCleanup);
|
||||||
|
earningsLineIdsToCleanup.clear();
|
||||||
|
}
|
||||||
|
if (!refundIdsToCleanup.isEmpty()) {
|
||||||
|
orderRefundInfoService.removeByIds(refundIdsToCleanup);
|
||||||
|
refundIdsToCleanup.clear();
|
||||||
|
}
|
||||||
|
if (!orderIdsToCleanup.isEmpty()) {
|
||||||
|
orderInfoService.removeByIds(orderIdsToCleanup);
|
||||||
|
orderIdsToCleanup.clear();
|
||||||
|
}
|
||||||
|
CustomSecurityContextHolder.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void listByPage_honorsAllSupportedFilters() throws Exception {
|
||||||
|
ensureTenantContext();
|
||||||
|
String marker = ("FT" + IdUtils.getUuid().replace("-", "").substring(0, 6)).toUpperCase();
|
||||||
|
LocalDateTime reference = LocalDateTime.now().minusHours(6).withNano(0);
|
||||||
|
|
||||||
|
PlayOrderInfoEntity matching = persistOrder(marker, "match", reference, order -> {
|
||||||
|
order.setOrderStatus("3");
|
||||||
|
order.setPlaceType("2");
|
||||||
|
order.setPayMethod("2");
|
||||||
|
order.setUseCoupon("1");
|
||||||
|
order.setBackendEntry("1");
|
||||||
|
order.setFirstOrder("0");
|
||||||
|
order.setGroupId(ApiTestDataSeeder.DEFAULT_GROUP_ID);
|
||||||
|
order.setSex("2");
|
||||||
|
order.setAcceptBy(ApiTestDataSeeder.DEFAULT_CLERK_ID);
|
||||||
|
order.setPurchaserBy(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID);
|
||||||
|
});
|
||||||
|
|
||||||
|
persistOrder(marker, "noise", reference.minusDays(3), order -> {
|
||||||
|
order.setOrderStatus("0");
|
||||||
|
order.setPlaceType("0");
|
||||||
|
order.setPayMethod("0");
|
||||||
|
order.setUseCoupon("0");
|
||||||
|
order.setBackendEntry("0");
|
||||||
|
order.setFirstOrder("1");
|
||||||
|
order.setGroupId(marker + "-grp");
|
||||||
|
order.setSex("1");
|
||||||
|
order.setAcceptBy(null);
|
||||||
|
order.setPurchaserBy("customer-" + marker);
|
||||||
|
});
|
||||||
|
|
||||||
|
// orderNo filter (exact)
|
||||||
|
ObjectNode orderNoPayload = baseQuery();
|
||||||
|
orderNoPayload.put("orderNo", matching.getOrderNo());
|
||||||
|
assertFilterMatches(orderNoPayload, matching.getId());
|
||||||
|
|
||||||
|
// acceptBy filter
|
||||||
|
ObjectNode acceptPayload = queryWithMarker(marker);
|
||||||
|
acceptPayload.put("acceptBy", matching.getAcceptBy());
|
||||||
|
assertFilterMatches(acceptPayload, matching.getId());
|
||||||
|
|
||||||
|
// purchaserBy filter
|
||||||
|
ObjectNode purchaserPayload = queryWithMarker(marker);
|
||||||
|
purchaserPayload.put("purchaserBy", matching.getPurchaserBy());
|
||||||
|
assertFilterMatches(purchaserPayload, matching.getId());
|
||||||
|
|
||||||
|
// orderStatus filter
|
||||||
|
ObjectNode statusPayload = queryWithMarker(marker);
|
||||||
|
statusPayload.put("orderStatus", matching.getOrderStatus());
|
||||||
|
assertFilterMatches(statusPayload, matching.getId());
|
||||||
|
|
||||||
|
// placeType filter
|
||||||
|
ObjectNode placePayload = queryWithMarker(marker);
|
||||||
|
placePayload.put("placeType", matching.getPlaceType());
|
||||||
|
assertFilterMatches(placePayload, matching.getId());
|
||||||
|
|
||||||
|
// payMethod filter
|
||||||
|
ObjectNode payPayload = queryWithMarker(marker);
|
||||||
|
payPayload.put("payMethod", matching.getPayMethod());
|
||||||
|
assertFilterMatches(payPayload, matching.getId());
|
||||||
|
|
||||||
|
// useCoupon filter
|
||||||
|
ObjectNode couponPayload = queryWithMarker(marker);
|
||||||
|
couponPayload.put("useCoupon", matching.getUseCoupon());
|
||||||
|
assertFilterMatches(couponPayload, matching.getId());
|
||||||
|
|
||||||
|
// backendEntry filter
|
||||||
|
ObjectNode backendPayload = queryWithMarker(marker);
|
||||||
|
backendPayload.put("backendEntry", matching.getBackendEntry());
|
||||||
|
assertFilterMatches(backendPayload, matching.getId());
|
||||||
|
|
||||||
|
// firstOrder filter
|
||||||
|
ObjectNode firstOrderPayload = queryWithMarker(marker);
|
||||||
|
firstOrderPayload.put("firstOrder", matching.getFirstOrder());
|
||||||
|
assertFilterMatches(firstOrderPayload, matching.getId());
|
||||||
|
|
||||||
|
// groupId filter
|
||||||
|
ObjectNode groupPayload = queryWithMarker(marker);
|
||||||
|
groupPayload.put("groupId", matching.getGroupId());
|
||||||
|
assertFilterMatches(groupPayload, matching.getId());
|
||||||
|
|
||||||
|
// sex filter
|
||||||
|
ObjectNode sexPayload = queryWithMarker(marker);
|
||||||
|
sexPayload.put("sex", matching.getSex());
|
||||||
|
assertFilterMatches(sexPayload, matching.getId());
|
||||||
|
|
||||||
|
// purchaserTime range filter
|
||||||
|
ObjectNode purchaserTimePayload = queryWithMarker(marker);
|
||||||
|
purchaserTimePayload.set("purchaserTime", range(
|
||||||
|
reference.minusMinutes(30),
|
||||||
|
reference.plusMinutes(30)));
|
||||||
|
assertFilterMatches(purchaserTimePayload, matching.getId());
|
||||||
|
|
||||||
|
// acceptTime range filter
|
||||||
|
ObjectNode acceptTimePayload = queryWithMarker(marker);
|
||||||
|
acceptTimePayload.set("acceptTime", range(
|
||||||
|
matching.getAcceptTime().minusMinutes(15),
|
||||||
|
matching.getAcceptTime().plusMinutes(15)));
|
||||||
|
assertFilterMatches(acceptTimePayload, matching.getId());
|
||||||
|
|
||||||
|
// endOrderTime range filter
|
||||||
|
ObjectNode endTimePayload = queryWithMarker(marker);
|
||||||
|
endTimePayload.set("endOrderTime", range(
|
||||||
|
matching.getOrderEndTime().minusMinutes(15),
|
||||||
|
matching.getOrderEndTime().plusMinutes(15)));
|
||||||
|
assertFilterMatches(endTimePayload, matching.getId());
|
||||||
|
|
||||||
|
// Combined filters to verify logical AND behaviour
|
||||||
|
ObjectNode combinedPayload = queryWithMarker(marker);
|
||||||
|
combinedPayload.put("acceptBy", matching.getAcceptBy());
|
||||||
|
combinedPayload.put("purchaserBy", matching.getPurchaserBy());
|
||||||
|
combinedPayload.put("orderStatus", matching.getOrderStatus());
|
||||||
|
combinedPayload.put("placeType", matching.getPlaceType());
|
||||||
|
combinedPayload.put("payMethod", matching.getPayMethod());
|
||||||
|
combinedPayload.put("useCoupon", matching.getUseCoupon());
|
||||||
|
combinedPayload.put("backendEntry", matching.getBackendEntry());
|
||||||
|
combinedPayload.put("firstOrder", matching.getFirstOrder());
|
||||||
|
combinedPayload.put("groupId", matching.getGroupId());
|
||||||
|
combinedPayload.put("sex", matching.getSex());
|
||||||
|
combinedPayload.set("purchaserTime", range(
|
||||||
|
reference.minusMinutes(5),
|
||||||
|
reference.plusMinutes(5)));
|
||||||
|
combinedPayload.set("acceptTime", range(
|
||||||
|
matching.getAcceptTime().minusMinutes(5),
|
||||||
|
matching.getAcceptTime().plusMinutes(5)));
|
||||||
|
combinedPayload.set("endOrderTime", range(
|
||||||
|
matching.getOrderEndTime().minusMinutes(5),
|
||||||
|
matching.getOrderEndTime().plusMinutes(5)));
|
||||||
|
assertFilterMatches(combinedPayload, matching.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void listByPage_keywordFiltersByOrderNoOrClerkName() throws Exception {
|
||||||
|
ensureTenantContext();
|
||||||
|
String marker = ("KW" + IdUtils.getUuid().replace("-", "").substring(0, 4)).toUpperCase();
|
||||||
|
LocalDateTime reference = LocalDateTime.now().plusHours(2);
|
||||||
|
|
||||||
|
PlayOrderInfoEntity orderByNo = persistOrder(marker, "ord", reference, order -> {
|
||||||
|
order.setOrderStatus(OrderStatus.COMPLETED.getCode());
|
||||||
|
});
|
||||||
|
PlayOrderInfoEntity orderByClerk = persistOrder(marker, "clk", reference.plusMinutes(10), order -> {
|
||||||
|
order.setOrderStatus(OrderStatus.COMPLETED.getCode());
|
||||||
|
});
|
||||||
|
|
||||||
|
ObjectNode orderNoPayload = baseQuery();
|
||||||
|
orderNoPayload.put("keyword", orderByNo.getOrderNo());
|
||||||
|
assertFilterMatches(orderNoPayload, orderByNo.getId());
|
||||||
|
|
||||||
|
ObjectNode clerkKeywordPayload = baseQuery();
|
||||||
|
clerkKeywordPayload.put("keyword", "小测官");
|
||||||
|
clerkKeywordPayload.set("purchaserTime", range(reference.plusMinutes(5), reference.plusMinutes(15)));
|
||||||
|
clerkKeywordPayload.put("placeType", "1");
|
||||||
|
RecordsResponse clerkResponse = executeList(clerkKeywordPayload);
|
||||||
|
JsonNode clerkRecords = clerkResponse.records;
|
||||||
|
assertThat(clerkRecords.size()).isGreaterThan(0);
|
||||||
|
List<String> ids = new ArrayList<>();
|
||||||
|
clerkRecords.forEach(node -> ids.add(node.path("id").asText()));
|
||||||
|
assertThat(ids).contains(orderByClerk.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void listByPage_keywordRespectsAdditionalFilters() throws Exception {
|
||||||
|
ensureTenantContext();
|
||||||
|
String marker = ("KWFLT" + IdUtils.getUuid().replace("-", "").substring(0, 4)).toUpperCase();
|
||||||
|
LocalDateTime reference = LocalDateTime.now().plusHours(3);
|
||||||
|
|
||||||
|
PlayOrderInfoEntity assignedOrder = persistOrder(marker, "assigned", reference, order -> {
|
||||||
|
order.setOrderStatus("3");
|
||||||
|
order.setPlaceType("0");
|
||||||
|
});
|
||||||
|
|
||||||
|
persistOrder(marker, "random", reference.minusMinutes(20), order -> {
|
||||||
|
order.setOrderStatus("3");
|
||||||
|
order.setPlaceType("1");
|
||||||
|
});
|
||||||
|
|
||||||
|
ObjectNode keywordAndFilterPayload = baseQuery();
|
||||||
|
keywordAndFilterPayload.put("keyword", "小测官");
|
||||||
|
keywordAndFilterPayload.put("placeType", "0");
|
||||||
|
keywordAndFilterPayload.set("purchaserTime", range(reference.minusMinutes(2), reference.plusMinutes(2)));
|
||||||
|
|
||||||
|
RecordsResponse filteredResponse = executeList(keywordAndFilterPayload);
|
||||||
|
JsonNode records = filteredResponse.records;
|
||||||
|
assertThat(records.size()).isEqualTo(1);
|
||||||
|
assertThat(records.get(0).path("id").asText()).isEqualTo(assignedOrder.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void revokeCompletedOrder_keepEarningsIgnoresLockedLines() throws Exception {
|
||||||
|
ensureTenantContext();
|
||||||
|
LocalDateTime reference = LocalDateTime.now().minusHours(1);
|
||||||
|
PlayOrderInfoEntity order = persistOrder("RVK", "keep", reference, entity -> {
|
||||||
|
entity.setOrderStatus(OrderStatus.COMPLETED.getCode());
|
||||||
|
entity.setFinalAmount(new BigDecimal("166.00"));
|
||||||
|
});
|
||||||
|
seedEarningLine(order.getId(), new BigDecimal("80.00"), "withdrawn");
|
||||||
|
|
||||||
|
ObjectNode payload = objectMapper.createObjectNode();
|
||||||
|
payload.put("orderId", order.getId());
|
||||||
|
payload.put("refundToCustomer", false);
|
||||||
|
payload.put("refundAmount", BigDecimal.ZERO);
|
||||||
|
payload.put("refundReason", "API撤销-保留收益");
|
||||||
|
payload.put("deductClerkEarnings", false);
|
||||||
|
|
||||||
|
MvcResult response = mockMvc.perform(post("/order/order/revokeCompleted")
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.header(TENANT_HEADER, DEFAULT_TENANT)
|
||||||
|
.header(USER_HEADER, DEFAULT_USER)
|
||||||
|
.content(payload.toString()))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andReturn();
|
||||||
|
|
||||||
|
JsonNode reverseRoot = objectMapper.readTree(response.getResponse().getContentAsString());
|
||||||
|
assertThat(reverseRoot.path("code").asInt())
|
||||||
|
.as("response=%s", reverseRoot.toString())
|
||||||
|
.isEqualTo(200);
|
||||||
|
assertThat(reverseRoot.path("success").asBoolean()).isTrue();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void revokeCompletedOrder_reverseClerkCreatesNegativeLineEvenWhenLocked() throws Exception {
|
||||||
|
ensureTenantContext();
|
||||||
|
LocalDateTime reference = LocalDateTime.now().minusHours(2);
|
||||||
|
PlayOrderInfoEntity order = persistOrder("RVK", "reverse", reference, entity -> {
|
||||||
|
entity.setOrderStatus(OrderStatus.COMPLETED.getCode());
|
||||||
|
entity.setFinalAmount(new BigDecimal("210.00"));
|
||||||
|
});
|
||||||
|
seedEarningLine(order.getId(), new BigDecimal("120.00"), "withdrawn");
|
||||||
|
|
||||||
|
ObjectNode payload = objectMapper.createObjectNode();
|
||||||
|
payload.put("orderId", order.getId());
|
||||||
|
payload.put("refundToCustomer", false);
|
||||||
|
payload.put("refundAmount", new BigDecimal("20.00"));
|
||||||
|
payload.put("refundReason", "API撤销-冲销收益");
|
||||||
|
payload.put("deductClerkEarnings", true);
|
||||||
|
payload.put("deductAmount", new BigDecimal("20.00"));
|
||||||
|
|
||||||
|
MvcResult response = mockMvc.perform(post("/order/order/revokeCompleted")
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.header(TENANT_HEADER, DEFAULT_TENANT)
|
||||||
|
.header(USER_HEADER, DEFAULT_USER)
|
||||||
|
.content(payload.toString()))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andReturn();
|
||||||
|
|
||||||
|
JsonNode reverseRoot = objectMapper.readTree(response.getResponse().getContentAsString());
|
||||||
|
assertThat(reverseRoot.path("code").asInt())
|
||||||
|
.as("response=%s", reverseRoot.toString())
|
||||||
|
.isEqualTo(200);
|
||||||
|
assertThat(reverseRoot.path("success").asBoolean()).isTrue();
|
||||||
|
|
||||||
|
ensureTenantContext();
|
||||||
|
List<EarningsLineEntity> lines = earningsService.lambdaQuery()
|
||||||
|
.eq(EarningsLineEntity::getOrderId, order.getId())
|
||||||
|
.list();
|
||||||
|
assertThat(lines).hasSize(2);
|
||||||
|
EarningsLineEntity negativeLine = lines.stream()
|
||||||
|
.filter(line -> line.getAmount().compareTo(BigDecimal.ZERO) < 0)
|
||||||
|
.findFirst()
|
||||||
|
.orElseThrow(() -> new AssertionError("未生成负收益行"));
|
||||||
|
assertThat(negativeLine.getAmount()).isEqualByComparingTo(new BigDecimal("-20.00"));
|
||||||
|
assertThat(negativeLine.getClerkId()).isEqualTo(order.getAcceptBy());
|
||||||
|
earningsLineIdsToCleanup.add(negativeLine.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void revokeCompletedOrder_deductKeepsFrozenUnlockSchedule() throws Exception {
|
||||||
|
ensureTenantContext();
|
||||||
|
LocalDateTime reference = LocalDateTime.now().minusHours(1);
|
||||||
|
PlayOrderInfoEntity order = persistOrder("RVK", "frozen", reference, entity -> {
|
||||||
|
entity.setOrderStatus(OrderStatus.COMPLETED.getCode());
|
||||||
|
entity.setFinalAmount(new BigDecimal("166.00"));
|
||||||
|
});
|
||||||
|
earningsService.lambdaUpdate()
|
||||||
|
.eq(EarningsLineEntity::getOrderId, order.getId())
|
||||||
|
.remove();
|
||||||
|
LocalDateTime unlockAt = LocalDateTime.now().plusHours(3).withNano(0);
|
||||||
|
seedEarningLine(order.getId(), new BigDecimal("90.00"), "frozen", unlockAt);
|
||||||
|
|
||||||
|
ObjectNode payload = objectMapper.createObjectNode();
|
||||||
|
payload.put("orderId", order.getId());
|
||||||
|
payload.put("refundToCustomer", false);
|
||||||
|
payload.put("refundAmount", BigDecimal.ZERO);
|
||||||
|
payload.put("refundReason", "API撤销-冻结扣回");
|
||||||
|
payload.put("deductClerkEarnings", true);
|
||||||
|
payload.put("deductAmount", new BigDecimal("30.00"));
|
||||||
|
|
||||||
|
LocalDateTime beforeCall = LocalDateTime.now().minusSeconds(1);
|
||||||
|
mockMvc.perform(post("/order/order/revokeCompleted")
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.header(TENANT_HEADER, DEFAULT_TENANT)
|
||||||
|
.header(USER_HEADER, DEFAULT_USER)
|
||||||
|
.content(payload.toString()))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.code").value(200));
|
||||||
|
|
||||||
|
ensureTenantContext();
|
||||||
|
EarningsLineEntity negativeLine = earningsService.lambdaQuery()
|
||||||
|
.eq(EarningsLineEntity::getOrderId, order.getId())
|
||||||
|
.list()
|
||||||
|
.stream()
|
||||||
|
.filter(line -> line.getAmount().compareTo(BigDecimal.ZERO) < 0)
|
||||||
|
.findFirst()
|
||||||
|
.orElseThrow(() -> new AssertionError("未生成负收益行"));
|
||||||
|
assertThat(negativeLine.getStatus()).isEqualTo("frozen");
|
||||||
|
assertThat(negativeLine.getUnlockTime()).isEqualTo(unlockAt);
|
||||||
|
earningsLineIdsToCleanup.add(negativeLine.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void revokeCompletedOrder_deductMakesWithdrawnLineAvailable() throws Exception {
|
||||||
|
ensureTenantContext();
|
||||||
|
LocalDateTime reference = LocalDateTime.now().minusHours(2);
|
||||||
|
PlayOrderInfoEntity order = persistOrder("RVK", "withdrawn", reference, entity -> {
|
||||||
|
entity.setOrderStatus(OrderStatus.COMPLETED.getCode());
|
||||||
|
entity.setFinalAmount(new BigDecimal("188.00"));
|
||||||
|
});
|
||||||
|
earningsService.lambdaUpdate()
|
||||||
|
.eq(EarningsLineEntity::getOrderId, order.getId())
|
||||||
|
.remove();
|
||||||
|
seedEarningLine(order.getId(), new BigDecimal("120.00"), "withdrawn");
|
||||||
|
|
||||||
|
ObjectNode payload = objectMapper.createObjectNode();
|
||||||
|
payload.put("orderId", order.getId());
|
||||||
|
payload.put("refundToCustomer", false);
|
||||||
|
payload.put("refundAmount", new BigDecimal("0.00"));
|
||||||
|
payload.put("refundReason", "API撤销-提现扣回");
|
||||||
|
payload.put("deductClerkEarnings", true);
|
||||||
|
payload.put("deductAmount", new BigDecimal("40.00"));
|
||||||
|
|
||||||
|
LocalDateTime beforeCall = LocalDateTime.now().minusSeconds(1);
|
||||||
|
mockMvc.perform(post("/order/order/revokeCompleted")
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.header(TENANT_HEADER, DEFAULT_TENANT)
|
||||||
|
.header(USER_HEADER, DEFAULT_USER)
|
||||||
|
.content(payload.toString()))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.code").value(200));
|
||||||
|
|
||||||
|
ensureTenantContext();
|
||||||
|
EarningsLineEntity negativeLine = earningsService.lambdaQuery()
|
||||||
|
.eq(EarningsLineEntity::getOrderId, order.getId())
|
||||||
|
.list()
|
||||||
|
.stream()
|
||||||
|
.filter(line -> line.getAmount().compareTo(BigDecimal.ZERO) < 0)
|
||||||
|
.findFirst()
|
||||||
|
.orElseThrow(() -> new AssertionError("未生成负收益行"));
|
||||||
|
assertThat(negativeLine.getStatus()).isEqualTo("available");
|
||||||
|
assertThat(negativeLine.getUnlockTime()).isAfter(beforeCall);
|
||||||
|
earningsLineIdsToCleanup.add(negativeLine.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void revokeCompletedOrder_defaultsDeductAmountWhenMissing() throws Exception {
|
||||||
|
ensureTenantContext();
|
||||||
|
LocalDateTime reference = LocalDateTime.now().minusMinutes(50);
|
||||||
|
PlayOrderInfoEntity order = persistOrder("RVK", "autoDeduct", reference, entity -> {
|
||||||
|
entity.setOrderStatus(OrderStatus.COMPLETED.getCode());
|
||||||
|
entity.setFinalAmount(new BigDecimal("260.00"));
|
||||||
|
});
|
||||||
|
earningsService.lambdaUpdate()
|
||||||
|
.eq(EarningsLineEntity::getOrderId, order.getId())
|
||||||
|
.remove();
|
||||||
|
String earningId = seedEarningLine(order.getId(), new BigDecimal("75.00"), "available");
|
||||||
|
earningsLineIdsToCleanup.add(earningId);
|
||||||
|
|
||||||
|
ObjectNode payload = objectMapper.createObjectNode();
|
||||||
|
payload.put("orderId", order.getId());
|
||||||
|
payload.put("refundToCustomer", false);
|
||||||
|
payload.put("refundReason", "API撤销-自动扣回");
|
||||||
|
payload.put("deductClerkEarnings", true);
|
||||||
|
|
||||||
|
mockMvc.perform(post("/order/order/revokeCompleted")
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.header(TENANT_HEADER, DEFAULT_TENANT)
|
||||||
|
.header(USER_HEADER, DEFAULT_USER)
|
||||||
|
.content(payload.toString()))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.code").value(200));
|
||||||
|
|
||||||
|
ensureTenantContext();
|
||||||
|
List<EarningsLineEntity> lines = earningsService.lambdaQuery()
|
||||||
|
.eq(EarningsLineEntity::getOrderId, order.getId())
|
||||||
|
.list();
|
||||||
|
EarningsLineEntity counterLine = lines.stream()
|
||||||
|
.filter(line -> line.getAmount().compareTo(BigDecimal.ZERO) < 0)
|
||||||
|
.findFirst()
|
||||||
|
.orElseThrow(() -> new AssertionError("未生成负收益行"));
|
||||||
|
assertThat(counterLine.getAmount()).isEqualByComparingTo(new BigDecimal("-75.00"));
|
||||||
|
earningsLineIdsToCleanup.add(counterLine.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void revokeCompletedOrder_refundAndDeductCreatesRecords() throws Exception {
|
||||||
|
ensureTenantContext();
|
||||||
|
LocalDateTime reference = LocalDateTime.now().minusMinutes(35);
|
||||||
|
PlayOrderInfoEntity order = persistOrder("RVK", "refundDeduct", reference, entity -> {
|
||||||
|
entity.setOrderStatus(OrderStatus.COMPLETED.getCode());
|
||||||
|
entity.setFinalAmount(new BigDecimal("300.00"));
|
||||||
|
entity.setPaymentSource(OrderConstant.PaymentSource.WX_PAY.getCode());
|
||||||
|
});
|
||||||
|
earningsService.lambdaUpdate()
|
||||||
|
.eq(EarningsLineEntity::getOrderId, order.getId())
|
||||||
|
.remove();
|
||||||
|
String earningId = seedEarningLine(order.getId(), new BigDecimal("120.00"), "available");
|
||||||
|
earningsLineIdsToCleanup.add(earningId);
|
||||||
|
|
||||||
|
ObjectNode payload = objectMapper.createObjectNode();
|
||||||
|
payload.put("orderId", order.getId());
|
||||||
|
payload.put("refundToCustomer", true);
|
||||||
|
payload.put("refundAmount", new BigDecimal("80.00"));
|
||||||
|
payload.put("refundReason", "API撤销-退款扣回");
|
||||||
|
payload.put("deductClerkEarnings", true);
|
||||||
|
payload.put("deductAmount", new BigDecimal("60.00"));
|
||||||
|
|
||||||
|
mockMvc.perform(post("/order/order/revokeCompleted")
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.header(TENANT_HEADER, DEFAULT_TENANT)
|
||||||
|
.header(USER_HEADER, DEFAULT_USER)
|
||||||
|
.content(payload.toString()))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.code").value(200));
|
||||||
|
|
||||||
|
ensureTenantContext();
|
||||||
|
PlayOrderRefundInfoEntity refundInfo = orderRefundInfoService.selectPlayOrderRefundInfoByOrderId(order.getId());
|
||||||
|
assertThat(refundInfo).isNotNull();
|
||||||
|
assertThat(refundInfo.getRefundAmount()).isEqualByComparingTo(new BigDecimal("80.00"));
|
||||||
|
refundIdsToCleanup.add(refundInfo.getId());
|
||||||
|
|
||||||
|
List<EarningsLineEntity> lines = earningsService.lambdaQuery()
|
||||||
|
.eq(EarningsLineEntity::getOrderId, order.getId())
|
||||||
|
.list();
|
||||||
|
EarningsLineEntity counterLine = lines.stream()
|
||||||
|
.filter(line -> line.getAmount().compareTo(BigDecimal.ZERO) < 0)
|
||||||
|
.findFirst()
|
||||||
|
.orElseThrow(() -> new AssertionError("未生成负收益行"));
|
||||||
|
assertThat(counterLine.getAmount()).isEqualByComparingTo(new BigDecimal("-60.00"));
|
||||||
|
earningsLineIdsToCleanup.add(counterLine.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void revokeCompletedOrder_deductLineRespectsFutureUnlockSchedule() throws Exception {
|
||||||
|
ensureTenantContext();
|
||||||
|
LocalDateTime reference = LocalDateTime.now().minusHours(3).withNano(0);
|
||||||
|
LocalDateTime unlockAt = LocalDateTime.now().plusHours(12).withNano(0);
|
||||||
|
PlayOrderInfoEntity order = persistOrder("RVK", "futureUnlock", reference, entity -> {
|
||||||
|
entity.setOrderStatus(OrderStatus.COMPLETED.getCode());
|
||||||
|
entity.setFinalAmount(new BigDecimal("220.00"));
|
||||||
|
});
|
||||||
|
earningsService.lambdaUpdate()
|
||||||
|
.eq(EarningsLineEntity::getOrderId, order.getId())
|
||||||
|
.remove();
|
||||||
|
String earningId = seedEarningLine(order.getId(), new BigDecimal("120.00"), "frozen", unlockAt);
|
||||||
|
earningsLineIdsToCleanup.add(earningId);
|
||||||
|
|
||||||
|
ObjectNode payload = objectMapper.createObjectNode();
|
||||||
|
payload.put("orderId", order.getId());
|
||||||
|
payload.put("refundToCustomer", false);
|
||||||
|
payload.put("refundReason", "API撤销-锁定排期");
|
||||||
|
payload.put("deductClerkEarnings", true);
|
||||||
|
payload.put("deductAmount", new BigDecimal("60.00"));
|
||||||
|
|
||||||
|
mockMvc.perform(post("/order/order/revokeCompleted")
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.header(TENANT_HEADER, DEFAULT_TENANT)
|
||||||
|
.header(USER_HEADER, DEFAULT_USER)
|
||||||
|
.content(payload.toString()))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.code").value(200));
|
||||||
|
|
||||||
|
ensureTenantContext();
|
||||||
|
List<EarningsLineEntity> lines = earningsService.lambdaQuery()
|
||||||
|
.eq(EarningsLineEntity::getOrderId, order.getId())
|
||||||
|
.list();
|
||||||
|
EarningsLineEntity counterLine = lines.stream()
|
||||||
|
.filter(line -> line.getAmount().compareTo(BigDecimal.ZERO) < 0)
|
||||||
|
.findFirst()
|
||||||
|
.orElseThrow(() -> new AssertionError("未生成负收益行"));
|
||||||
|
assertThat(counterLine.getStatus()).isEqualTo("frozen");
|
||||||
|
assertThat(counterLine.getUnlockTime()).isEqualTo(unlockAt);
|
||||||
|
earningsLineIdsToCleanup.add(counterLine.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void revokeCompletedOrder_deductLineUnlocksImmediatelyWhenAlreadyAvailable() throws Exception {
|
||||||
|
ensureTenantContext();
|
||||||
|
LocalDateTime reference = LocalDateTime.now().minusHours(5).withNano(0);
|
||||||
|
LocalDateTime unlockAt = LocalDateTime.now().minusHours(1).withNano(0);
|
||||||
|
PlayOrderInfoEntity order = persistOrder("RVK", "pastUnlock", reference, entity -> {
|
||||||
|
entity.setOrderStatus(OrderStatus.COMPLETED.getCode());
|
||||||
|
entity.setFinalAmount(new BigDecimal("180.00"));
|
||||||
|
});
|
||||||
|
earningsService.lambdaUpdate()
|
||||||
|
.eq(EarningsLineEntity::getOrderId, order.getId())
|
||||||
|
.remove();
|
||||||
|
String earningId = seedEarningLine(order.getId(), new BigDecimal("90.00"), "available", unlockAt);
|
||||||
|
earningsLineIdsToCleanup.add(earningId);
|
||||||
|
|
||||||
|
ObjectNode payload = objectMapper.createObjectNode();
|
||||||
|
payload.put("orderId", order.getId());
|
||||||
|
payload.put("refundToCustomer", false);
|
||||||
|
payload.put("refundReason", "API撤销-立即扣回");
|
||||||
|
payload.put("deductClerkEarnings", true);
|
||||||
|
payload.put("deductAmount", new BigDecimal("45.00"));
|
||||||
|
|
||||||
|
LocalDateTime beforeCall = LocalDateTime.now().minusSeconds(1);
|
||||||
|
mockMvc.perform(post("/order/order/revokeCompleted")
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.header(TENANT_HEADER, DEFAULT_TENANT)
|
||||||
|
.header(USER_HEADER, DEFAULT_USER)
|
||||||
|
.content(payload.toString()))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.code").value(200));
|
||||||
|
|
||||||
|
ensureTenantContext();
|
||||||
|
List<EarningsLineEntity> lines = earningsService.lambdaQuery()
|
||||||
|
.eq(EarningsLineEntity::getOrderId, order.getId())
|
||||||
|
.list();
|
||||||
|
EarningsLineEntity counterLine = lines.stream()
|
||||||
|
.filter(line -> line.getAmount().compareTo(BigDecimal.ZERO) < 0)
|
||||||
|
.findFirst()
|
||||||
|
.orElseThrow(() -> new AssertionError("未生成负收益行"));
|
||||||
|
assertThat(counterLine.getStatus()).isEqualTo("available");
|
||||||
|
assertThat(counterLine.getUnlockTime()).isAfter(beforeCall);
|
||||||
|
earningsLineIdsToCleanup.add(counterLine.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void revokeCompletedOrder_deductFailsWhenNoEarningLineExists() throws Exception {
|
||||||
|
ensureTenantContext();
|
||||||
|
LocalDateTime reference = LocalDateTime.now().minusHours(4);
|
||||||
|
PlayOrderInfoEntity order = persistOrder("RVK", "noLine", reference, entity -> {
|
||||||
|
entity.setOrderStatus(OrderStatus.COMPLETED.getCode());
|
||||||
|
entity.setFinalAmount(new BigDecimal("150.00"));
|
||||||
|
});
|
||||||
|
earningsService.lambdaUpdate()
|
||||||
|
.eq(EarningsLineEntity::getOrderId, order.getId())
|
||||||
|
.remove();
|
||||||
|
|
||||||
|
ObjectNode payload = objectMapper.createObjectNode();
|
||||||
|
payload.put("orderId", order.getId());
|
||||||
|
payload.put("refundToCustomer", false);
|
||||||
|
payload.put("refundReason", "API撤销-无收益扣回");
|
||||||
|
payload.put("deductClerkEarnings", true);
|
||||||
|
payload.put("deductAmount", new BigDecimal("30.00"));
|
||||||
|
|
||||||
|
mockMvc.perform(post("/order/order/revokeCompleted")
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.header(TENANT_HEADER, DEFAULT_TENANT)
|
||||||
|
.header(USER_HEADER, DEFAULT_USER)
|
||||||
|
.content(payload.toString()))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.code").value(500))
|
||||||
|
.andExpect(jsonPath("$.message").value("本单店员收益已全部扣回"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void revokeCompletedOrder_rejectsDeductAmountBeyondAvailable() throws Exception {
|
||||||
|
ensureTenantContext();
|
||||||
|
LocalDateTime reference = LocalDateTime.now().minusMinutes(30);
|
||||||
|
PlayOrderInfoEntity order = persistOrder("RVK", "overDeduct", reference, entity -> {
|
||||||
|
entity.setOrderStatus(OrderStatus.COMPLETED.getCode());
|
||||||
|
});
|
||||||
|
earningsService.lambdaUpdate()
|
||||||
|
.eq(EarningsLineEntity::getOrderId, order.getId())
|
||||||
|
.remove();
|
||||||
|
String earningId = seedEarningLine(order.getId(), new BigDecimal("40.00"), "available");
|
||||||
|
earningsLineIdsToCleanup.add(earningId);
|
||||||
|
|
||||||
|
ObjectNode payload = objectMapper.createObjectNode();
|
||||||
|
payload.put("orderId", order.getId());
|
||||||
|
payload.put("refundToCustomer", false);
|
||||||
|
payload.put("refundReason", "API撤销-超额扣");
|
||||||
|
payload.put("deductClerkEarnings", true);
|
||||||
|
payload.put("deductAmount", new BigDecimal("60.00"));
|
||||||
|
|
||||||
|
mockMvc.perform(post("/order/order/revokeCompleted")
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.header(TENANT_HEADER, DEFAULT_TENANT)
|
||||||
|
.header(USER_HEADER, DEFAULT_USER)
|
||||||
|
.content(payload.toString()))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.code").value(500))
|
||||||
|
.andExpect(jsonPath("$.message").value("扣回金额不能超过本单收益40.00"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void revokeCompletedOrder_blocksNonNormalOrders() throws Exception {
|
||||||
|
ensureTenantContext();
|
||||||
|
LocalDateTime reference = LocalDateTime.now().minusMinutes(10);
|
||||||
|
PlayOrderInfoEntity giftOrder = persistOrder("RVK", "gift", reference, entity -> {
|
||||||
|
entity.setOrderStatus(OrderStatus.COMPLETED.getCode());
|
||||||
|
entity.setOrderType(OrderConstant.OrderType.GIFT.getCode());
|
||||||
|
entity.setPlaceType(OrderConstant.PlaceType.REWARD.getCode());
|
||||||
|
});
|
||||||
|
|
||||||
|
ObjectNode payload = objectMapper.createObjectNode();
|
||||||
|
payload.put("orderId", giftOrder.getId());
|
||||||
|
payload.put("refundToCustomer", false);
|
||||||
|
payload.put("refundReason", "gift revoke");
|
||||||
|
payload.put("deductClerkEarnings", false);
|
||||||
|
|
||||||
|
mockMvc.perform(post("/order/order/revokeCompleted")
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.header(TENANT_HEADER, DEFAULT_TENANT)
|
||||||
|
.header(USER_HEADER, DEFAULT_USER)
|
||||||
|
.content(payload.toString()))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.code").value(500))
|
||||||
|
.andExpect(jsonPath("$.message").value("仅支持撤销普通服务订单"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getRevocationLimits_returnsRemainingValues() throws Exception {
|
||||||
|
ensureTenantContext();
|
||||||
|
PlayOrderInfoEntity order = persistOrder("RVK", "limits", LocalDateTime.now().minusHours(3), entity -> {
|
||||||
|
entity.setOrderStatus(OrderStatus.COMPLETED.getCode());
|
||||||
|
entity.setFinalAmount(new BigDecimal("188.00"));
|
||||||
|
});
|
||||||
|
earningsService.lambdaUpdate()
|
||||||
|
.eq(EarningsLineEntity::getOrderId, order.getId())
|
||||||
|
.remove();
|
||||||
|
String earningId = seedEarningLine(order.getId(), new BigDecimal("45.50"), "available");
|
||||||
|
earningsLineIdsToCleanup.add(earningId);
|
||||||
|
|
||||||
|
mockMvc.perform(get("/order/order/" + order.getId() + "/revocationLimits")
|
||||||
|
.header(TENANT_HEADER, DEFAULT_TENANT)
|
||||||
|
.header(USER_HEADER, DEFAULT_USER))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.code").value(200))
|
||||||
|
.andExpect(jsonPath("$.data.maxRefundAmount").value(188.00))
|
||||||
|
.andExpect(jsonPath("$.data.maxDeductAmount").value(45.50))
|
||||||
|
.andExpect(jsonPath("$.data.defaultDeductAmount").value(45.50))
|
||||||
|
.andExpect(jsonPath("$.data.deductible").value(true));
|
||||||
|
}
|
||||||
|
|
||||||
|
private PlayOrderInfoEntity persistOrder(
|
||||||
|
String marker,
|
||||||
|
String token,
|
||||||
|
LocalDateTime purchaserTime,
|
||||||
|
Consumer<PlayOrderInfoEntity> customizer) {
|
||||||
|
PlayOrderInfoEntity order = buildBaselineOrder(marker, token, purchaserTime);
|
||||||
|
customizer.accept(order);
|
||||||
|
assertThat(orderInfoService.save(order))
|
||||||
|
.withFailMessage("Failed to persist order %s", order.getOrderNo())
|
||||||
|
.isTrue();
|
||||||
|
orderIdsToCleanup.add(order.getId());
|
||||||
|
return order;
|
||||||
|
}
|
||||||
|
|
||||||
|
private PlayOrderInfoEntity buildBaselineOrder(String marker, String token, LocalDateTime purchaserTime) {
|
||||||
|
PlayOrderInfoEntity order = new PlayOrderInfoEntity();
|
||||||
|
order.setId("order-" + token + "-" + IdUtils.getUuid().substring(0, 8));
|
||||||
|
String tokenFragment = token.length() >= 2 ? token.substring(0, 2) : token;
|
||||||
|
order.setOrderNo(marker + tokenFragment.toUpperCase() + IdUtils.getUuid().substring(0, 4));
|
||||||
|
order.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
|
||||||
|
order.setOrderType("2");
|
||||||
|
order.setPlaceType("1");
|
||||||
|
order.setRewardType("0");
|
||||||
|
order.setFirstOrder("0");
|
||||||
|
order.setRefundType("0");
|
||||||
|
order.setRefundAmount(BigDecimal.ZERO);
|
||||||
|
order.setRefundReason(null);
|
||||||
|
order.setOrderMoney(DEFAULT_AMOUNT);
|
||||||
|
order.setDiscountAmount(BigDecimal.ZERO);
|
||||||
|
order.setFinalAmount(DEFAULT_AMOUNT);
|
||||||
|
order.setEstimatedRevenue(new BigDecimal("88.00"));
|
||||||
|
order.setEstimatedRevenueRatio(50);
|
||||||
|
order.setLabels(Collections.singletonList("label-" + marker));
|
||||||
|
order.setUseCoupon("1");
|
||||||
|
order.setCouponIds(Collections.singletonList("coupon-" + marker));
|
||||||
|
order.setBackendEntry("1");
|
||||||
|
order.setPaymentSource("balance");
|
||||||
|
order.setPayMethod("2");
|
||||||
|
order.setPayState("2");
|
||||||
|
order.setWeiChatCode("wx-" + marker);
|
||||||
|
order.setPurchaserBy(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID);
|
||||||
|
order.setPurchaserTime(purchaserTime);
|
||||||
|
order.setAcceptBy(ApiTestDataSeeder.DEFAULT_CLERK_ID);
|
||||||
|
order.setAcceptTime(purchaserTime.plusMinutes(20));
|
||||||
|
order.setGroupId(ApiTestDataSeeder.DEFAULT_GROUP_ID);
|
||||||
|
order.setOrderStartTime(purchaserTime.plusMinutes(30));
|
||||||
|
order.setOrderEndTime(purchaserTime.plusHours(2));
|
||||||
|
order.setOrderSettlementState("0");
|
||||||
|
order.setOrdersExpiredState("0");
|
||||||
|
order.setSex("2");
|
||||||
|
order.setCommodityId(ApiTestDataSeeder.DEFAULT_COMMODITY_ID);
|
||||||
|
order.setCommodityType("1");
|
||||||
|
order.setCommodityPrice(DEFAULT_AMOUNT);
|
||||||
|
order.setCommodityName("API Filter Commodity");
|
||||||
|
order.setServiceDuration("60min");
|
||||||
|
order.setCommodityNumber("1");
|
||||||
|
order.setLevelId(ApiTestDataSeeder.DEFAULT_CLERK_LEVEL_ID);
|
||||||
|
order.setExcludeHistory("0");
|
||||||
|
order.setRemark("marker-" + marker);
|
||||||
|
order.setBackendRemark("backend-" + marker);
|
||||||
|
order.setProfitSharingAmount(BigDecimal.ZERO);
|
||||||
|
order.setDeleted(Boolean.FALSE);
|
||||||
|
order.setCreatedBy("apitest");
|
||||||
|
order.setUpdatedBy("apitest");
|
||||||
|
order.setCreatedTime(toDate(purchaserTime));
|
||||||
|
order.setUpdatedTime(toDate(purchaserTime.plusMinutes(45)));
|
||||||
|
return order;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ObjectNode baseQuery() {
|
||||||
|
ObjectNode node = objectMapper.createObjectNode();
|
||||||
|
node.put("pageNum", 1);
|
||||||
|
node.put("pageSize", 20);
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ObjectNode queryWithMarker(String marker) {
|
||||||
|
ObjectNode node = baseQuery();
|
||||||
|
node.put("orderNo", marker);
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ArrayNode range(LocalDateTime start, LocalDateTime end) {
|
||||||
|
ArrayNode array = objectMapper.createArrayNode();
|
||||||
|
array.add(DATE_TIME_FORMATTER.format(start));
|
||||||
|
array.add(DATE_TIME_FORMATTER.format(end));
|
||||||
|
return array;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String seedEarningLine(String orderId, BigDecimal amount, String status) {
|
||||||
|
return seedEarningLine(orderId, amount, status, LocalDateTime.now().minusHours(2).withNano(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
private String seedEarningLine(String orderId, BigDecimal amount, String status, LocalDateTime unlockAt) {
|
||||||
|
EarningsLineEntity entity = new EarningsLineEntity();
|
||||||
|
String id = "earn-revoke-" + IdUtils.getUuid();
|
||||||
|
entity.setId(id);
|
||||||
|
entity.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
|
||||||
|
entity.setClerkId(ApiTestDataSeeder.DEFAULT_CLERK_ID);
|
||||||
|
entity.setOrderId(orderId);
|
||||||
|
entity.setAmount(amount);
|
||||||
|
entity.setStatus(status);
|
||||||
|
entity.setEarningType(EarningsType.ORDER);
|
||||||
|
entity.setUnlockTime(unlockAt);
|
||||||
|
if ("withdrawn".equals(status) || "withdrawing".equals(status)) {
|
||||||
|
entity.setWithdrawalId("withdraw-" + IdUtils.getUuid());
|
||||||
|
}
|
||||||
|
Date nowDate = toDate(LocalDateTime.now());
|
||||||
|
entity.setCreatedBy(ApiTestDataSeeder.DEFAULT_ADMIN_USER_ID);
|
||||||
|
entity.setCreatedTime(nowDate);
|
||||||
|
entity.setUpdatedBy(ApiTestDataSeeder.DEFAULT_ADMIN_USER_ID);
|
||||||
|
entity.setUpdatedTime(nowDate);
|
||||||
|
entity.setDeleted(false);
|
||||||
|
ensureTenantContext();
|
||||||
|
earningsService.save(entity);
|
||||||
|
earningsLineIdsToCleanup.add(id);
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void assertFilterMatches(ObjectNode payload, String expectedOrderId) throws Exception {
|
||||||
|
RecordsResponse response = executeList(payload);
|
||||||
|
JsonNode records = response.records;
|
||||||
|
assertThat(records.isArray())
|
||||||
|
.withFailMessage("Records payload is not an array for body=%s | response=%s", payload, response.rawResponse)
|
||||||
|
.isTrue();
|
||||||
|
assertThat(records.size())
|
||||||
|
.withFailMessage("Unexpected record count for body=%s | response=%s", payload, response.rawResponse)
|
||||||
|
.isEqualTo(1);
|
||||||
|
assertThat(records.get(0).path("id").asText())
|
||||||
|
.withFailMessage("Unexpected order id for body=%s | response=%s", payload, response.rawResponse)
|
||||||
|
.isEqualTo(expectedOrderId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private RecordsResponse executeList(ObjectNode payload) throws Exception {
|
||||||
|
MvcResult result = mockMvc.perform(post("/order/order/listByPage")
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.header(TENANT_HEADER, DEFAULT_TENANT)
|
||||||
|
.header(USER_HEADER, DEFAULT_USER)
|
||||||
|
.content(payload.toString()))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.code").value(200))
|
||||||
|
.andReturn();
|
||||||
|
|
||||||
|
String responseBody = result.getResponse().getContentAsString();
|
||||||
|
JsonNode root = objectMapper.readTree(responseBody);
|
||||||
|
JsonNode data = root.path("data");
|
||||||
|
JsonNode records = data.isArray() ? data : data.path("records");
|
||||||
|
return new RecordsResponse(records, responseBody);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Date toDate(LocalDateTime time) {
|
||||||
|
return Date.from(time.atZone(ZoneId.systemDefault()).toInstant());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ensureTenantContext() {
|
||||||
|
SecurityUtils.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class RecordsResponse {
|
||||||
|
private final JsonNode records;
|
||||||
|
private final String rawResponse;
|
||||||
|
|
||||||
|
private RecordsResponse(JsonNode records, String rawResponse) {
|
||||||
|
this.records = records;
|
||||||
|
this.rawResponse = rawResponse;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,25 +4,49 @@ import static org.springframework.test.web.servlet.request.MockMvcRequestBuilder
|
|||||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
import com.starry.admin.common.apitest.ApiTestDataSeeder;
|
import com.starry.admin.common.apitest.ApiTestDataSeeder;
|
||||||
|
import com.starry.admin.modules.blindbox.mapper.BlindBoxPoolMapper;
|
||||||
|
import com.starry.admin.modules.blindbox.mapper.BlindBoxRewardMapper;
|
||||||
import com.starry.admin.modules.blindbox.module.entity.BlindBoxConfigEntity;
|
import com.starry.admin.modules.blindbox.module.entity.BlindBoxConfigEntity;
|
||||||
|
import com.starry.admin.modules.blindbox.module.entity.BlindBoxPoolEntity;
|
||||||
|
import com.starry.admin.modules.blindbox.module.entity.BlindBoxRewardEntity;
|
||||||
import com.starry.admin.modules.blindbox.service.BlindBoxConfigService;
|
import com.starry.admin.modules.blindbox.service.BlindBoxConfigService;
|
||||||
import com.starry.admin.modules.order.module.constant.OrderConstant;
|
import com.starry.admin.modules.order.module.constant.OrderConstant;
|
||||||
import com.starry.admin.modules.order.module.entity.PlayOrderInfoEntity;
|
import com.starry.admin.modules.order.module.entity.PlayOrderInfoEntity;
|
||||||
|
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.PlayClerkGiftInfoEntity;
|
||||||
|
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.admin.utils.SecurityUtils;
|
import com.starry.admin.utils.SecurityUtils;
|
||||||
import com.starry.common.constant.Constants;
|
import com.starry.common.constant.Constants;
|
||||||
import com.starry.common.context.CustomSecurityContextHolder;
|
import com.starry.common.context.CustomSecurityContextHolder;
|
||||||
import com.starry.common.utils.IdUtils;
|
import com.starry.common.utils.IdUtils;
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.Objects;
|
||||||
import org.assertj.core.api.Assertions;
|
import org.assertj.core.api.Assertions;
|
||||||
|
import org.assertj.core.api.SoftAssertions;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.test.web.servlet.MvcResult;
|
||||||
|
|
||||||
class WxBlindBoxOrderApiTest extends WxCustomOrderApiTestSupport {
|
class WxBlindBoxOrderApiTest extends WxCustomOrderApiTestSupport {
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private BlindBoxConfigService blindBoxConfigService;
|
private BlindBoxConfigService blindBoxConfigService;
|
||||||
|
@Autowired
|
||||||
|
private BlindBoxPoolMapper blindBoxPoolMapper;
|
||||||
|
@Autowired
|
||||||
|
private IPlayGiftInfoService giftInfoService;
|
||||||
|
@Autowired
|
||||||
|
private BlindBoxRewardMapper blindBoxRewardMapper;
|
||||||
|
@Autowired
|
||||||
|
private IPlayClerkGiftInfoService clerkGiftInfoService;
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void blindBoxPurchaseFailsWhenBalanceInsufficient() throws Exception {
|
void blindBoxPurchaseFailsWhenBalanceInsufficient() throws Exception {
|
||||||
@@ -73,4 +97,116 @@ class WxBlindBoxOrderApiTest extends WxCustomOrderApiTestSupport {
|
|||||||
CustomSecurityContextHolder.remove();
|
CustomSecurityContextHolder.remove();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void blindBoxPurchaseAndDispatchSucceedWhenGiftInactive() throws Exception {
|
||||||
|
String configId = "blind-inactive-" + IdUtils.getUuid().substring(0, 6);
|
||||||
|
String giftId = "gift-inactive-" + IdUtils.getUuid().substring(0, 6);
|
||||||
|
Long poolId = null;
|
||||||
|
String rewardId = null;
|
||||||
|
try {
|
||||||
|
ensureTenantContext();
|
||||||
|
BlindBoxConfigEntity config = new BlindBoxConfigEntity();
|
||||||
|
config.setId(configId);
|
||||||
|
config.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
|
||||||
|
config.setName("下架礼物测试盲盒");
|
||||||
|
config.setPrice(new BigDecimal("19.90"));
|
||||||
|
config.setStatus(1);
|
||||||
|
blindBoxConfigService.save(config);
|
||||||
|
|
||||||
|
PlayGiftInfoEntity gift = new PlayGiftInfoEntity();
|
||||||
|
gift.setId(giftId);
|
||||||
|
gift.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
|
||||||
|
gift.setName("盲盒下架礼物");
|
||||||
|
gift.setHistory(GiftHistory.CURRENT.getCode());
|
||||||
|
gift.setState(GiftState.ACTIVE.getCode());
|
||||||
|
gift.setType(GiftType.NORMAL.getCode());
|
||||||
|
gift.setUrl("https://example.com/apitest/blindbox-off.png");
|
||||||
|
gift.setPrice(new BigDecimal("9.99"));
|
||||||
|
giftInfoService.save(gift);
|
||||||
|
|
||||||
|
BlindBoxPoolEntity entry = new BlindBoxPoolEntity();
|
||||||
|
entry.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
|
||||||
|
entry.setBlindBoxId(configId);
|
||||||
|
entry.setRewardGiftId(giftId);
|
||||||
|
entry.setRewardPrice(gift.getPrice());
|
||||||
|
entry.setWeight(100);
|
||||||
|
entry.setRemainingStock(1);
|
||||||
|
entry.setStatus(1);
|
||||||
|
entry.setValidFrom(LocalDateTime.now().minusDays(1));
|
||||||
|
entry.setValidTo(LocalDateTime.now().plusDays(1));
|
||||||
|
blindBoxPoolMapper.insert(entry);
|
||||||
|
poolId = entry.getId();
|
||||||
|
|
||||||
|
gift.setState(GiftState.OFF_SHELF.getCode());
|
||||||
|
giftInfoService.updateById(gift);
|
||||||
|
|
||||||
|
resetCustomerBalance();
|
||||||
|
String customerToken = wxTokenService.createWxUserToken(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID);
|
||||||
|
customUserInfoService.updateTokenById(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID, customerToken);
|
||||||
|
|
||||||
|
String payload = objectMapper.createObjectNode()
|
||||||
|
.put("blindBoxId", configId)
|
||||||
|
.put("clerkId", ApiTestDataSeeder.DEFAULT_CLERK_ID)
|
||||||
|
.put("weiChatCode", "apitest-customer-wx")
|
||||||
|
.toString();
|
||||||
|
|
||||||
|
MvcResult purchaseResult = mockMvc.perform(post("/wx/blind-box/order/purchase")
|
||||||
|
.header(USER_HEADER, DEFAULT_USER)
|
||||||
|
.header(TENANT_HEADER, DEFAULT_TENANT)
|
||||||
|
.header(Constants.CUSTOM_USER_LOGIN_TOKEN, Constants.TOKEN_PREFIX + customerToken)
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.content(payload))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.code").value(200))
|
||||||
|
.andExpect(jsonPath("$.data.reward.rewardId").isString())
|
||||||
|
.andReturn();
|
||||||
|
|
||||||
|
JsonNode rewardNode = objectMapper.readTree(purchaseResult.getResponse().getContentAsString())
|
||||||
|
.path("data").path("reward");
|
||||||
|
rewardId = rewardNode.path("rewardId").asText();
|
||||||
|
Assertions.assertThat(rewardId).isNotBlank();
|
||||||
|
|
||||||
|
SoftAssertions softly = new SoftAssertions();
|
||||||
|
softly.assertThat(rewardNode.path("rewardGiftId").asText()).isEqualTo(giftId);
|
||||||
|
softly.assertThat(rewardNode.path("status").asText()).isEqualTo("UNUSED");
|
||||||
|
softly.assertAll();
|
||||||
|
|
||||||
|
PlayClerkGiftInfoEntity before = clerkGiftInfoService.selectByGiftIdAndClerkId(giftId, ApiTestDataSeeder.DEFAULT_CLERK_ID);
|
||||||
|
long beforeCount = before != null && before.getGiffNumber() != null ? before.getGiffNumber() : 0L;
|
||||||
|
|
||||||
|
mockMvc.perform(post("/wx/blind-box/reward/" + rewardId + "/dispatch")
|
||||||
|
.header(USER_HEADER, DEFAULT_USER)
|
||||||
|
.header(TENANT_HEADER, DEFAULT_TENANT)
|
||||||
|
.header(Constants.CUSTOM_USER_LOGIN_TOKEN, Constants.TOKEN_PREFIX + customerToken)
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.content(objectMapper.createObjectNode()
|
||||||
|
.put("clerkId", ApiTestDataSeeder.DEFAULT_CLERK_ID)
|
||||||
|
.toString()))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.code").value(200))
|
||||||
|
.andExpect(jsonPath("$.data.status").value("USED"));
|
||||||
|
|
||||||
|
ensureTenantContext();
|
||||||
|
BlindBoxRewardEntity storedReward = blindBoxRewardMapper.selectById(rewardId);
|
||||||
|
Assertions.assertThat(storedReward).isNotNull();
|
||||||
|
Assertions.assertThat(storedReward.getStatus()).isEqualTo("USED");
|
||||||
|
Assertions.assertThat(storedReward.getUsedClerkId()).isEqualTo(ApiTestDataSeeder.DEFAULT_CLERK_ID);
|
||||||
|
|
||||||
|
PlayClerkGiftInfoEntity after = clerkGiftInfoService.selectByGiftIdAndClerkId(giftId, ApiTestDataSeeder.DEFAULT_CLERK_ID);
|
||||||
|
long afterCount = after != null && after.getGiffNumber() != null ? after.getGiffNumber() : 0L;
|
||||||
|
Assertions.assertThat(afterCount).isEqualTo(beforeCount + 1);
|
||||||
|
} finally {
|
||||||
|
ensureTenantContext();
|
||||||
|
if (Objects.nonNull(poolId)) {
|
||||||
|
blindBoxPoolMapper.deleteById(poolId);
|
||||||
|
}
|
||||||
|
blindBoxConfigService.removeById(configId);
|
||||||
|
giftInfoService.removeById(giftId);
|
||||||
|
if (rewardId != null) {
|
||||||
|
blindBoxRewardMapper.deleteById(rewardId);
|
||||||
|
}
|
||||||
|
CustomSecurityContextHolder.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,321 @@
|
|||||||
|
package com.starry.admin.api;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||||
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||||
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||||
|
import com.starry.admin.common.apitest.ApiTestDataSeeder;
|
||||||
|
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 com.starry.admin.modules.clerk.module.entity.PlayClerkUserInfoEntity;
|
||||||
|
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.MediaOwnerType;
|
||||||
|
import com.starry.admin.modules.media.enums.MediaStatus;
|
||||||
|
import com.starry.admin.modules.media.service.IPlayMediaService;
|
||||||
|
import com.starry.admin.modules.weichat.entity.clerk.MediaVo;
|
||||||
|
import com.starry.admin.modules.weichat.entity.clerk.PlayClerkUserInfoResultVo;
|
||||||
|
import com.starry.admin.modules.weichat.service.WxTokenService;
|
||||||
|
import com.starry.admin.utils.SecurityUtils;
|
||||||
|
import com.starry.common.constant.Constants;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.test.web.servlet.MvcResult;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 专门验证 /wx/clerk/user/updateAlbum 在“只有删除/排序”时不会创建新的审核记录,
|
||||||
|
* 并且会立即更新顾客端视图。
|
||||||
|
*/
|
||||||
|
class WxClerkAlbumUpdateApiTest extends AbstractApiTest {
|
||||||
|
|
||||||
|
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private WxTokenService wxTokenService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private IPlayClerkUserInfoService clerkUserInfoService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private IPlayClerkDataReviewInfoService dataReviewInfoService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private IPlayClerkMediaAssetService mediaAssetService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private IPlayMediaService mediaService;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void reorderExistingApprovedMediaDoesNotCreateNewReviewAndUpdatesOrder() throws Exception {
|
||||||
|
ensureTenantContext();
|
||||||
|
String clerkId = ApiTestDataSeeder.DEFAULT_CLERK_ID;
|
||||||
|
String clerkToken = wxTokenService.createWxUserToken(clerkId);
|
||||||
|
clerkUserInfoService.updateTokenById(clerkId, clerkToken);
|
||||||
|
|
||||||
|
PlayMediaEntity media1 = seedMedia(clerkId);
|
||||||
|
PlayMediaEntity media2 = seedMedia(clerkId);
|
||||||
|
PlayMediaEntity media3 = seedMedia(clerkId);
|
||||||
|
|
||||||
|
seedApprovedAsset(clerkId, media1.getId(), 0);
|
||||||
|
seedApprovedAsset(clerkId, media2.getId(), 1);
|
||||||
|
seedApprovedAsset(clerkId, media3.getId(), 2);
|
||||||
|
|
||||||
|
long reviewCountBefore = dataReviewInfoService.lambdaQuery()
|
||||||
|
.eq(com.starry.admin.modules.clerk.module.entity.PlayClerkDataReviewInfoEntity::getClerkId, clerkId)
|
||||||
|
.eq(com.starry.admin.modules.clerk.module.entity.PlayClerkDataReviewInfoEntity::getDataType, "2")
|
||||||
|
.count();
|
||||||
|
|
||||||
|
// 仅调整顺序:album 只包含已审核通过的媒资 id,不引入任何新媒资
|
||||||
|
List<String> reordered = List.of(media3.getId(), media1.getId(), media2.getId());
|
||||||
|
ObjectNode payload = objectMapper.createObjectNode();
|
||||||
|
com.fasterxml.jackson.databind.node.ArrayNode albumArray = payload.putArray("album");
|
||||||
|
reordered.forEach(albumArray::add);
|
||||||
|
|
||||||
|
mockMvc.perform(post("/wx/clerk/user/updateAlbum")
|
||||||
|
.header(USER_HEADER, DEFAULT_USER)
|
||||||
|
.header(TENANT_HEADER, DEFAULT_TENANT)
|
||||||
|
.header(Constants.CLERK_USER_LOGIN_TOKEN, Constants.TOKEN_PREFIX + clerkToken)
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.content(payload.toString()))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.code").value(200));
|
||||||
|
|
||||||
|
ensureTenantContext();
|
||||||
|
long reviewCountAfter = dataReviewInfoService.lambdaQuery()
|
||||||
|
.eq(com.starry.admin.modules.clerk.module.entity.PlayClerkDataReviewInfoEntity::getClerkId, clerkId)
|
||||||
|
.eq(com.starry.admin.modules.clerk.module.entity.PlayClerkDataReviewInfoEntity::getDataType, "2")
|
||||||
|
.count();
|
||||||
|
assertThat(reviewCountAfter)
|
||||||
|
.as("reordering without introducing new media should not create review records")
|
||||||
|
.isEqualTo(reviewCountBefore);
|
||||||
|
|
||||||
|
List<PlayClerkMediaAssetEntity> assets = mediaAssetService
|
||||||
|
.listByState(clerkId, ClerkMediaUsage.PROFILE,
|
||||||
|
Collections.singletonList(ClerkMediaReviewState.APPROVED));
|
||||||
|
PlayClerkMediaAssetEntity asset1 = assets.stream()
|
||||||
|
.filter(a -> media1.getId().equals(a.getMediaId()))
|
||||||
|
.max(java.util.Comparator.comparing(PlayClerkMediaAssetEntity::getCreatedTime))
|
||||||
|
.orElse(null);
|
||||||
|
PlayClerkMediaAssetEntity asset2 = assets.stream()
|
||||||
|
.filter(a -> media2.getId().equals(a.getMediaId()))
|
||||||
|
.max(java.util.Comparator.comparing(PlayClerkMediaAssetEntity::getCreatedTime))
|
||||||
|
.orElse(null);
|
||||||
|
PlayClerkMediaAssetEntity asset3 = assets.stream()
|
||||||
|
.filter(a -> media3.getId().equals(a.getMediaId()))
|
||||||
|
.max(java.util.Comparator.comparing(PlayClerkMediaAssetEntity::getCreatedTime))
|
||||||
|
.orElse(null);
|
||||||
|
|
||||||
|
assertThat(asset3).as("asset for media3 should exist").isNotNull();
|
||||||
|
assertThat(asset1).as("asset for media1 should exist").isNotNull();
|
||||||
|
assertThat(asset2).as("asset for media2 should exist").isNotNull();
|
||||||
|
|
||||||
|
assertThat(asset3.getOrderIndex()).isEqualTo(0);
|
||||||
|
assertThat(asset1.getOrderIndex()).isEqualTo(1);
|
||||||
|
assertThat(asset2.getOrderIndex()).isEqualTo(2);
|
||||||
|
|
||||||
|
PlayClerkUserInfoResultVo detail = clerkUserInfoService.buildCustomerDetail(clerkId, "");
|
||||||
|
List<String> customerMediaIds = detail.getMediaList().stream()
|
||||||
|
.map(MediaVo::getMediaId)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
int index3 = customerMediaIds.indexOf(media3.getId());
|
||||||
|
int index1 = customerMediaIds.indexOf(media1.getId());
|
||||||
|
int index2 = customerMediaIds.indexOf(media2.getId());
|
||||||
|
|
||||||
|
assertThat(index3).isGreaterThanOrEqualTo(0);
|
||||||
|
assertThat(index1).isGreaterThanOrEqualTo(0);
|
||||||
|
assertThat(index2).isGreaterThanOrEqualTo(0);
|
||||||
|
assertThat(index3).isLessThan(index1);
|
||||||
|
assertThat(index1).isLessThan(index2);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void deleteAndReorderAlbumDoesNotCreateNewReviewAndIsImmediatelyVisible() throws Exception {
|
||||||
|
ensureTenantContext();
|
||||||
|
String clerkId = ApiTestDataSeeder.DEFAULT_CLERK_ID;
|
||||||
|
String clerkToken = wxTokenService.createWxUserToken(clerkId);
|
||||||
|
clerkUserInfoService.updateTokenById(clerkId, clerkToken);
|
||||||
|
|
||||||
|
// 预置两条已审核通过的 PROFILE 媒资
|
||||||
|
PlayMediaEntity media1 = seedMedia(clerkId);
|
||||||
|
PlayMediaEntity media2 = seedMedia(clerkId);
|
||||||
|
|
||||||
|
seedApprovedAsset(clerkId, media1.getId(), 0);
|
||||||
|
seedApprovedAsset(clerkId, media2.getId(), 1);
|
||||||
|
|
||||||
|
// 顾客端初始视图应包含两条媒资
|
||||||
|
PlayClerkUserInfoResultVo beforeDetail = clerkUserInfoService.buildCustomerDetail(clerkId, "");
|
||||||
|
List<String> beforeIds = beforeDetail.getMediaList().stream()
|
||||||
|
.map(MediaVo::getMediaId)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
assertThat(beforeIds).contains(media1.getId(), media2.getId());
|
||||||
|
|
||||||
|
// 记录当前相册审核记录数量
|
||||||
|
long reviewCountBefore = dataReviewInfoService.lambdaQuery()
|
||||||
|
.eq(com.starry.admin.modules.clerk.module.entity.PlayClerkDataReviewInfoEntity::getClerkId, clerkId)
|
||||||
|
.eq(com.starry.admin.modules.clerk.module.entity.PlayClerkDataReviewInfoEntity::getDataType, "2")
|
||||||
|
.count();
|
||||||
|
|
||||||
|
// 现在通过 updateAlbum 只保留 media2,相当于“删除 media1 + 不引入新媒资”
|
||||||
|
ObjectNode payload = objectMapper.createObjectNode();
|
||||||
|
payload.putArray("album").add(media2.getId());
|
||||||
|
|
||||||
|
mockMvc.perform(post("/wx/clerk/user/updateAlbum")
|
||||||
|
.header(USER_HEADER, DEFAULT_USER)
|
||||||
|
.header(TENANT_HEADER, DEFAULT_TENANT)
|
||||||
|
.header(Constants.CLERK_USER_LOGIN_TOKEN, Constants.TOKEN_PREFIX + clerkToken)
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.content(payload.toString()))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.code").value(200));
|
||||||
|
|
||||||
|
ensureTenantContext();
|
||||||
|
long reviewCountAfter = dataReviewInfoService.lambdaQuery()
|
||||||
|
.eq(com.starry.admin.modules.clerk.module.entity.PlayClerkDataReviewInfoEntity::getClerkId, clerkId)
|
||||||
|
.eq(com.starry.admin.modules.clerk.module.entity.PlayClerkDataReviewInfoEntity::getDataType, "2")
|
||||||
|
.count();
|
||||||
|
assertThat(reviewCountAfter)
|
||||||
|
.as("Deleting/reordering without new media should not create new review records")
|
||||||
|
.isEqualTo(reviewCountBefore);
|
||||||
|
|
||||||
|
// 资产表中仅剩 media2 且仍为 APPROVED 状态
|
||||||
|
List<PlayClerkMediaAssetEntity> assets = mediaAssetService
|
||||||
|
.listByState(clerkId, ClerkMediaUsage.PROFILE,
|
||||||
|
Collections.singletonList(ClerkMediaReviewState.APPROVED));
|
||||||
|
List<String> assetMediaIds = assets.stream()
|
||||||
|
.map(PlayClerkMediaAssetEntity::getMediaId)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
assertThat(assetMediaIds)
|
||||||
|
.contains(media2.getId())
|
||||||
|
.doesNotContain(media1.getId());
|
||||||
|
|
||||||
|
// 顾客端视图应立即反映删除结果
|
||||||
|
PlayClerkUserInfoResultVo afterDetail = clerkUserInfoService.buildCustomerDetail(clerkId, "");
|
||||||
|
List<String> afterIds = afterDetail.getMediaList().stream()
|
||||||
|
.map(MediaVo::getMediaId)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
assertThat(afterIds)
|
||||||
|
.contains(media2.getId())
|
||||||
|
.doesNotContain(media1.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void updateAlbumRejectsEmptyAlbumPayload() throws Exception {
|
||||||
|
ensureTenantContext();
|
||||||
|
String clerkId = ApiTestDataSeeder.DEFAULT_CLERK_ID;
|
||||||
|
String clerkToken = wxTokenService.createWxUserToken(clerkId);
|
||||||
|
clerkUserInfoService.updateTokenById(clerkId, clerkToken);
|
||||||
|
|
||||||
|
ObjectNode payload = objectMapper.createObjectNode();
|
||||||
|
payload.putArray("album"); // 空数组
|
||||||
|
|
||||||
|
MvcResult result = mockMvc.perform(post("/wx/clerk/user/updateAlbum")
|
||||||
|
.header(USER_HEADER, DEFAULT_USER)
|
||||||
|
.header(TENANT_HEADER, DEFAULT_TENANT)
|
||||||
|
.header(Constants.CLERK_USER_LOGIN_TOKEN, Constants.TOKEN_PREFIX + clerkToken)
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.content(payload.toString()))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andReturn();
|
||||||
|
|
||||||
|
String body = result.getResponse().getContentAsString();
|
||||||
|
com.fasterxml.jackson.databind.JsonNode root = new ObjectMapper().readTree(body);
|
||||||
|
assertThat(root.path("code").asInt())
|
||||||
|
.as("empty album should be rejected, response=%s", body)
|
||||||
|
.isEqualTo(500);
|
||||||
|
assertThat(root.path("message").asText())
|
||||||
|
.as("error message for empty album should be present, response=%s", body)
|
||||||
|
.isNotBlank();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void updateAlbumAllowsMixedLegacyUrlsAndNewMediaIdsForReview() throws Exception {
|
||||||
|
ensureTenantContext();
|
||||||
|
String clerkId = ApiTestDataSeeder.DEFAULT_CLERK_ID;
|
||||||
|
String clerkToken = wxTokenService.createWxUserToken(clerkId);
|
||||||
|
clerkUserInfoService.updateTokenById(clerkId, clerkToken);
|
||||||
|
|
||||||
|
// 预置一条已就绪的媒资,模拟“新上传的视频/图片”
|
||||||
|
PlayMediaEntity media = seedMedia(clerkId);
|
||||||
|
|
||||||
|
// 模拟老相册中的 URL(未媒资化的历史数据)
|
||||||
|
String legacyUrl1 = "https://oss.apitest/legacy-1.png";
|
||||||
|
String legacyUrl2 = "https://oss.apitest/legacy-2.png";
|
||||||
|
|
||||||
|
long reviewCountBefore = dataReviewInfoService.lambdaQuery()
|
||||||
|
.eq(com.starry.admin.modules.clerk.module.entity.PlayClerkDataReviewInfoEntity::getClerkId, clerkId)
|
||||||
|
.eq(com.starry.admin.modules.clerk.module.entity.PlayClerkDataReviewInfoEntity::getDataType, "2")
|
||||||
|
.count();
|
||||||
|
|
||||||
|
ObjectNode payload = objectMapper.createObjectNode();
|
||||||
|
com.fasterxml.jackson.databind.node.ArrayNode albumArray = payload.putArray("album");
|
||||||
|
albumArray.add(legacyUrl1);
|
||||||
|
albumArray.add(legacyUrl2);
|
||||||
|
albumArray.add(media.getId());
|
||||||
|
|
||||||
|
MvcResult result = mockMvc.perform(post("/wx/clerk/user/updateAlbum")
|
||||||
|
.header(USER_HEADER, DEFAULT_USER)
|
||||||
|
.header(TENANT_HEADER, DEFAULT_TENANT)
|
||||||
|
.header(Constants.CLERK_USER_LOGIN_TOKEN, Constants.TOKEN_PREFIX + clerkToken)
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.content(payload.toString()))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andReturn();
|
||||||
|
|
||||||
|
String body = result.getResponse().getContentAsString();
|
||||||
|
com.fasterxml.jackson.databind.JsonNode root = new ObjectMapper().readTree(body);
|
||||||
|
assertThat(root.path("code").asInt())
|
||||||
|
.as("mixed legacy URLs and new media ids should be accepted for review, response=%s", body)
|
||||||
|
.isEqualTo(200);
|
||||||
|
|
||||||
|
ensureTenantContext();
|
||||||
|
long reviewCountAfter = dataReviewInfoService.lambdaQuery()
|
||||||
|
.eq(com.starry.admin.modules.clerk.module.entity.PlayClerkDataReviewInfoEntity::getClerkId, clerkId)
|
||||||
|
.eq(com.starry.admin.modules.clerk.module.entity.PlayClerkDataReviewInfoEntity::getDataType, "2")
|
||||||
|
.count();
|
||||||
|
assertThat(reviewCountAfter)
|
||||||
|
.as("mixed legacy URLs and new media ids should create exactly one new review record")
|
||||||
|
.isEqualTo(reviewCountBefore + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
private PlayMediaEntity seedMedia(String clerkId) {
|
||||||
|
String mediaId = "media-" + java.util.UUID.randomUUID().toString().substring(0, 16);
|
||||||
|
PlayMediaEntity entity = new PlayMediaEntity();
|
||||||
|
entity.setId(mediaId);
|
||||||
|
entity.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
|
||||||
|
entity.setOwnerType(MediaOwnerType.CLERK);
|
||||||
|
entity.setOwnerId(clerkId);
|
||||||
|
entity.setKind("image");
|
||||||
|
entity.setStatus(MediaStatus.READY.getCode());
|
||||||
|
entity.setUrl("https://oss.apitest/" + mediaId + ".png");
|
||||||
|
mediaService.save(entity);
|
||||||
|
return entity;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void seedApprovedAsset(String clerkId, String mediaId, int orderIndex) {
|
||||||
|
PlayClerkMediaAssetEntity asset = new PlayClerkMediaAssetEntity();
|
||||||
|
asset.setId("asset-" + mediaId);
|
||||||
|
asset.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
|
||||||
|
asset.setClerkId(clerkId);
|
||||||
|
asset.setMediaId(mediaId);
|
||||||
|
asset.setUsage(ClerkMediaUsage.PROFILE.getCode());
|
||||||
|
asset.setOrderIndex(orderIndex);
|
||||||
|
asset.setReviewState(ClerkMediaReviewState.APPROVED.getCode());
|
||||||
|
asset.setDeleted(false);
|
||||||
|
mediaAssetService.save(asset);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ensureTenantContext() {
|
||||||
|
SecurityUtils.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,329 @@
|
|||||||
|
package com.starry.admin.api;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.ArgumentMatchers.anyString;
|
||||||
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.multipart;
|
||||||
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||||
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||||
|
import com.starry.admin.common.apitest.ApiTestDataSeeder;
|
||||||
|
import com.starry.admin.common.oss.service.IOssFileService;
|
||||||
|
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 com.starry.admin.modules.clerk.module.entity.PlayClerkUserInfoEntity;
|
||||||
|
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.MediaOwnerType;
|
||||||
|
import com.starry.admin.modules.media.service.IPlayMediaService;
|
||||||
|
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.PlayClerkUserInfoResultVo;
|
||||||
|
import com.starry.admin.modules.weichat.service.WxTokenService;
|
||||||
|
import com.starry.admin.utils.SecurityUtils;
|
||||||
|
import com.starry.common.constant.Constants;
|
||||||
|
import com.starry.common.enums.ClerkReviewState;
|
||||||
|
import java.awt.image.BufferedImage;
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import javax.imageio.ImageIO;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.boot.test.mock.mockito.MockBean;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.mock.web.MockMultipartFile;
|
||||||
|
import org.springframework.test.web.servlet.MvcResult;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 覆盖店员端媒资上传与相册审核的关键业务路径(图片/视频 + 删除后不复活)。
|
||||||
|
*/
|
||||||
|
class WxClerkMediaControllerApiTest extends AbstractApiTest {
|
||||||
|
|
||||||
|
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private WxTokenService wxTokenService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private IPlayClerkUserInfoService clerkUserInfoService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private IPlayMediaService mediaService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private IPlayClerkDataReviewInfoService dataReviewInfoService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private IPlayClerkMediaAssetService mediaAssetService;
|
||||||
|
|
||||||
|
@MockBean
|
||||||
|
private IOssFileService ossFileService;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void clerkCanUploadImageMediaAndPersistUrl() throws Exception {
|
||||||
|
ensureTenantContext();
|
||||||
|
String clerkToken = wxTokenService.createWxUserToken(ApiTestDataSeeder.DEFAULT_CLERK_ID);
|
||||||
|
clerkUserInfoService.updateTokenById(ApiTestDataSeeder.DEFAULT_CLERK_ID, clerkToken);
|
||||||
|
|
||||||
|
String ossUrl = "https://oss.mock/apitest/avatar.png";
|
||||||
|
when(ossFileService.upload(any(), eq(ApiTestDataSeeder.DEFAULT_TENANT_ID), anyString()))
|
||||||
|
.thenReturn(ossUrl);
|
||||||
|
|
||||||
|
BufferedImage image = new BufferedImage(2, 2, BufferedImage.TYPE_INT_RGB);
|
||||||
|
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||||
|
ImageIO.write(image, "png", baos);
|
||||||
|
MockMultipartFile file = new MockMultipartFile(
|
||||||
|
"file",
|
||||||
|
"avatar.png",
|
||||||
|
"image/png",
|
||||||
|
baos.toByteArray());
|
||||||
|
|
||||||
|
MvcResult result = mockMvc.perform(multipart("/wx/clerk/media/upload")
|
||||||
|
.file(file)
|
||||||
|
.param("usage", "profile")
|
||||||
|
.header(USER_HEADER, DEFAULT_USER)
|
||||||
|
.header(TENANT_HEADER, DEFAULT_TENANT)
|
||||||
|
.header(Constants.CLERK_USER_LOGIN_TOKEN, Constants.TOKEN_PREFIX + clerkToken)
|
||||||
|
.contentType(MediaType.MULTIPART_FORM_DATA))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.code").value(200))
|
||||||
|
.andExpect(jsonPath("$.data.url").value(ossUrl))
|
||||||
|
.andExpect(jsonPath("$.data.kind").value("image"))
|
||||||
|
.andExpect(jsonPath("$.data.usage").value("profile"))
|
||||||
|
.andReturn();
|
||||||
|
|
||||||
|
String body = result.getResponse().getContentAsString();
|
||||||
|
JsonNode root = objectMapper.readTree(body);
|
||||||
|
JsonNode data = root.path("data");
|
||||||
|
String mediaId = data.path("mediaId").asText(null);
|
||||||
|
assertThat(mediaId).isNotBlank();
|
||||||
|
|
||||||
|
ensureTenantContext();
|
||||||
|
PlayMediaEntity persisted = mediaService.getById(mediaId);
|
||||||
|
assertThat(persisted).isNotNull();
|
||||||
|
assertThat(persisted.getOwnerType()).isEqualTo(MediaOwnerType.CLERK);
|
||||||
|
assertThat(persisted.getOwnerId()).isEqualTo(ApiTestDataSeeder.DEFAULT_CLERK_ID);
|
||||||
|
assertThat(persisted.getUrl()).isEqualTo(ossUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void clerkCanUploadVideoMediaAndPersistUrl() throws Exception {
|
||||||
|
ensureTenantContext();
|
||||||
|
String clerkToken = wxTokenService.createWxUserToken(ApiTestDataSeeder.DEFAULT_CLERK_ID);
|
||||||
|
clerkUserInfoService.updateTokenById(ApiTestDataSeeder.DEFAULT_CLERK_ID, clerkToken);
|
||||||
|
|
||||||
|
String videoUrl = "https://oss.mock/apitest/video.mp4";
|
||||||
|
String coverUrl = "https://oss.mock/apitest/video-cover.jpg";
|
||||||
|
when(ossFileService.upload(any(), eq(ApiTestDataSeeder.DEFAULT_TENANT_ID), anyString()))
|
||||||
|
.thenReturn(videoUrl, coverUrl);
|
||||||
|
|
||||||
|
byte[] videoBytes = Files.readAllBytes(Paths.get("/Volumes/main/code/yunpei/sample_data/sample_video.mp4"));
|
||||||
|
MockMultipartFile file = new MockMultipartFile(
|
||||||
|
"file",
|
||||||
|
"sample_video.mp4",
|
||||||
|
"video/mp4",
|
||||||
|
videoBytes);
|
||||||
|
|
||||||
|
MvcResult result = mockMvc.perform(multipart("/wx/clerk/media/upload")
|
||||||
|
.file(file)
|
||||||
|
.param("usage", "profile")
|
||||||
|
.header(USER_HEADER, DEFAULT_USER)
|
||||||
|
.header(TENANT_HEADER, DEFAULT_TENANT)
|
||||||
|
.header(Constants.CLERK_USER_LOGIN_TOKEN, Constants.TOKEN_PREFIX + clerkToken)
|
||||||
|
.contentType(MediaType.MULTIPART_FORM_DATA))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.code").value(200))
|
||||||
|
.andExpect(jsonPath("$.data.kind").value("video"))
|
||||||
|
.andExpect(jsonPath("$.data.usage").value("profile"))
|
||||||
|
.andReturn();
|
||||||
|
|
||||||
|
String body = result.getResponse().getContentAsString();
|
||||||
|
JsonNode root = objectMapper.readTree(body);
|
||||||
|
JsonNode data = root.path("data");
|
||||||
|
String mediaId = data.path("mediaId").asText(null);
|
||||||
|
assertThat(mediaId).isNotBlank();
|
||||||
|
|
||||||
|
ensureTenantContext();
|
||||||
|
PlayMediaEntity persisted = mediaService.getById(mediaId);
|
||||||
|
assertThat(persisted).isNotNull();
|
||||||
|
assertThat(persisted.getKind()).isEqualTo("video");
|
||||||
|
assertThat(persisted.getUrl()).isEqualTo(videoUrl);
|
||||||
|
assertThat(persisted.getCoverUrl()).isEqualTo(coverUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void aggressiveAlbumLifecycleWithFourMediaAndDeletionReflectedForClerkAndCustomer() throws Exception {
|
||||||
|
ensureTenantContext();
|
||||||
|
String clerkToken = wxTokenService.createWxUserToken(ApiTestDataSeeder.DEFAULT_CLERK_ID);
|
||||||
|
clerkUserInfoService.updateTokenById(ApiTestDataSeeder.DEFAULT_CLERK_ID, clerkToken);
|
||||||
|
|
||||||
|
when(ossFileService.upload(any(), eq(ApiTestDataSeeder.DEFAULT_TENANT_ID), anyString()))
|
||||||
|
.thenReturn(
|
||||||
|
"https://oss.mock/apitest/album-a.png",
|
||||||
|
"https://oss.mock/apitest/album-b.png",
|
||||||
|
"https://oss.mock/apitest/album-c.png",
|
||||||
|
"https://oss.mock/apitest/album-d.png");
|
||||||
|
|
||||||
|
String mediaIdA = extractMediaIdFromUpload(buildTinyPng("album-a.png"), clerkToken);
|
||||||
|
String mediaIdB = extractMediaIdFromUpload(buildTinyPng("album-b.png"), clerkToken);
|
||||||
|
String mediaIdC = extractMediaIdFromUpload(buildTinyPng("album-c.png"), clerkToken);
|
||||||
|
String mediaIdD = extractMediaIdFromUpload(buildTinyPng("album-d.png"), clerkToken);
|
||||||
|
|
||||||
|
List<String> allMediaIds = List.of(mediaIdA, mediaIdB, mediaIdC, mediaIdD);
|
||||||
|
|
||||||
|
submitAlbumUpdate(allMediaIds, clerkToken);
|
||||||
|
ensureTenantContext();
|
||||||
|
approveLatestAlbumReview();
|
||||||
|
|
||||||
|
List<PlayClerkMediaAssetEntity> assetsAfterFirstApprove = mediaAssetService
|
||||||
|
.listActiveByUsage(ApiTestDataSeeder.DEFAULT_CLERK_ID, ClerkMediaUsage.PROFILE);
|
||||||
|
assertThat(assetsAfterFirstApprove)
|
||||||
|
.extracting(PlayClerkMediaAssetEntity::getMediaId)
|
||||||
|
.containsAll(allMediaIds);
|
||||||
|
List<String> reviewStatesForNewMedia = assetsAfterFirstApprove.stream()
|
||||||
|
.filter(asset -> allMediaIds.contains(asset.getMediaId()))
|
||||||
|
.map(PlayClerkMediaAssetEntity::getReviewState)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
assertThat(reviewStatesForNewMedia).containsOnly(ClerkMediaReviewState.APPROVED.getCode());
|
||||||
|
|
||||||
|
List<String> clerkVisibleMediaIdsAfterFirst = assetsAfterFirstApprove.stream()
|
||||||
|
.filter(asset -> !ClerkMediaReviewState.REJECTED.getCode().equals(asset.getReviewState()))
|
||||||
|
.map(PlayClerkMediaAssetEntity::getMediaId)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
assertThat(clerkVisibleMediaIdsAfterFirst).containsAll(allMediaIds);
|
||||||
|
|
||||||
|
PlayClerkUserInfoResultVo customerDetailAfterFirst =
|
||||||
|
clerkUserInfoService.buildCustomerDetail(ApiTestDataSeeder.DEFAULT_CLERK_ID, "");
|
||||||
|
List<String> customerMediaIdsAfterFirst = customerDetailAfterFirst.getMediaList().stream()
|
||||||
|
.map(MediaVo::getMediaId)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
assertThat(customerMediaIdsAfterFirst).containsAll(allMediaIds);
|
||||||
|
|
||||||
|
List<String> keptMedia = List.of(mediaIdA, mediaIdC);
|
||||||
|
|
||||||
|
// 第二次提交:只删除与重新排序,不再生成新的资料审核记录,应直接生效
|
||||||
|
long reviewCountBeforeSecond = dataReviewInfoService.lambdaQuery()
|
||||||
|
.eq(com.starry.admin.modules.clerk.module.entity.PlayClerkDataReviewInfoEntity::getClerkId,
|
||||||
|
ApiTestDataSeeder.DEFAULT_CLERK_ID)
|
||||||
|
.eq(com.starry.admin.modules.clerk.module.entity.PlayClerkDataReviewInfoEntity::getDataType, "2")
|
||||||
|
.count();
|
||||||
|
submitAlbumUpdate(keptMedia, clerkToken);
|
||||||
|
ensureTenantContext();
|
||||||
|
long reviewCountAfterSecond = dataReviewInfoService.lambdaQuery()
|
||||||
|
.eq(com.starry.admin.modules.clerk.module.entity.PlayClerkDataReviewInfoEntity::getClerkId,
|
||||||
|
ApiTestDataSeeder.DEFAULT_CLERK_ID)
|
||||||
|
.eq(com.starry.admin.modules.clerk.module.entity.PlayClerkDataReviewInfoEntity::getDataType, "2")
|
||||||
|
.count();
|
||||||
|
assertThat(reviewCountAfterSecond)
|
||||||
|
.as("deleting/reordering album should not create another review record")
|
||||||
|
.isEqualTo(reviewCountBeforeSecond);
|
||||||
|
|
||||||
|
List<PlayClerkMediaAssetEntity> assetsAfterSecondApprove = mediaAssetService
|
||||||
|
.listActiveByUsage(ApiTestDataSeeder.DEFAULT_CLERK_ID, ClerkMediaUsage.PROFILE);
|
||||||
|
|
||||||
|
PlayClerkMediaAssetEntity assetA = assetsAfterSecondApprove.stream()
|
||||||
|
.filter(a -> mediaIdA.equals(a.getMediaId()))
|
||||||
|
.max(Comparator.comparing(PlayClerkMediaAssetEntity::getCreatedTime))
|
||||||
|
.orElse(null);
|
||||||
|
PlayClerkMediaAssetEntity assetC = assetsAfterSecondApprove.stream()
|
||||||
|
.filter(a -> mediaIdC.equals(a.getMediaId()))
|
||||||
|
.max(Comparator.comparing(PlayClerkMediaAssetEntity::getCreatedTime))
|
||||||
|
.orElse(null);
|
||||||
|
assertThat(assetA.getReviewState()).isEqualTo(ClerkMediaReviewState.APPROVED.getCode());
|
||||||
|
assertThat(assetA.getOrderIndex()).isEqualTo(0);
|
||||||
|
assertThat(assetC.getReviewState()).isEqualTo(ClerkMediaReviewState.APPROVED.getCode());
|
||||||
|
assertThat(assetC.getOrderIndex()).isEqualTo(1);
|
||||||
|
|
||||||
|
PlayClerkUserInfoResultVo customerDetailAfterSecond =
|
||||||
|
clerkUserInfoService.buildCustomerDetail(ApiTestDataSeeder.DEFAULT_CLERK_ID, "");
|
||||||
|
List<String> customerMediaIdsAfterSecond = customerDetailAfterSecond.getMediaList().stream()
|
||||||
|
.map(MediaVo::getMediaId)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
assertThat(customerMediaIdsAfterSecond)
|
||||||
|
.contains(mediaIdA, mediaIdC)
|
||||||
|
.doesNotContain(mediaIdB, mediaIdD);
|
||||||
|
|
||||||
|
List<String> clerkVisibleMediaIdsAfterSecond = assetsAfterSecondApprove.stream()
|
||||||
|
.filter(asset -> !ClerkMediaReviewState.REJECTED.getCode().equals(asset.getReviewState()))
|
||||||
|
.map(PlayClerkMediaAssetEntity::getMediaId)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
assertThat(clerkVisibleMediaIdsAfterSecond)
|
||||||
|
.contains(mediaIdA, mediaIdC)
|
||||||
|
.doesNotContain(mediaIdB, mediaIdD);
|
||||||
|
}
|
||||||
|
|
||||||
|
private MockMultipartFile buildTinyPng(String filename) throws Exception {
|
||||||
|
BufferedImage image = new BufferedImage(2, 2, BufferedImage.TYPE_INT_RGB);
|
||||||
|
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||||
|
ImageIO.write(image, "png", baos);
|
||||||
|
return new MockMultipartFile("file", filename, "image/png", baos.toByteArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
private String extractMediaIdFromUpload(MockMultipartFile file, String clerkToken) throws Exception {
|
||||||
|
MvcResult result = mockMvc.perform(multipart("/wx/clerk/media/upload")
|
||||||
|
.file(file)
|
||||||
|
.param("usage", "profile")
|
||||||
|
.header(USER_HEADER, DEFAULT_USER)
|
||||||
|
.header(TENANT_HEADER, DEFAULT_TENANT)
|
||||||
|
.header(Constants.CLERK_USER_LOGIN_TOKEN, Constants.TOKEN_PREFIX + clerkToken)
|
||||||
|
.contentType(MediaType.MULTIPART_FORM_DATA))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.code").value(200))
|
||||||
|
.andReturn();
|
||||||
|
String body = result.getResponse().getContentAsString();
|
||||||
|
JsonNode root = objectMapper.readTree(body);
|
||||||
|
return root.path("data").path("mediaId").asText();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void submitAlbumUpdate(List<String> mediaIds, String clerkToken) throws Exception {
|
||||||
|
ObjectNode payload = objectMapper.createObjectNode();
|
||||||
|
com.fasterxml.jackson.databind.node.ArrayNode albumArray = payload.putArray("album");
|
||||||
|
mediaIds.forEach(albumArray::add);
|
||||||
|
|
||||||
|
mockMvc.perform(org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post("/wx/clerk/user/updateAlbum")
|
||||||
|
.header(USER_HEADER, DEFAULT_USER)
|
||||||
|
.header(TENANT_HEADER, DEFAULT_TENANT)
|
||||||
|
.header(Constants.CLERK_USER_LOGIN_TOKEN, Constants.TOKEN_PREFIX + clerkToken)
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.content(payload.toString()))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.code").value(200));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void approveLatestAlbumReview() {
|
||||||
|
List<com.starry.admin.modules.clerk.module.entity.PlayClerkDataReviewInfoEntity> reviews = dataReviewInfoService
|
||||||
|
.lambdaQuery()
|
||||||
|
.eq(com.starry.admin.modules.clerk.module.entity.PlayClerkDataReviewInfoEntity::getClerkId,
|
||||||
|
ApiTestDataSeeder.DEFAULT_CLERK_ID)
|
||||||
|
.eq(com.starry.admin.modules.clerk.module.entity.PlayClerkDataReviewInfoEntity::getDataType, "2")
|
||||||
|
.orderByDesc(com.starry.admin.modules.clerk.module.entity.PlayClerkDataReviewInfoEntity::getAddTime)
|
||||||
|
.list();
|
||||||
|
assertThat(reviews).isNotEmpty();
|
||||||
|
com.starry.admin.modules.clerk.module.entity.PlayClerkDataReviewInfoEntity latest = reviews.get(0);
|
||||||
|
|
||||||
|
com.starry.admin.modules.clerk.module.vo.PlayClerkDataReviewStateEditVo vo =
|
||||||
|
new com.starry.admin.modules.clerk.module.vo.PlayClerkDataReviewStateEditVo();
|
||||||
|
vo.setId(latest.getId());
|
||||||
|
vo.setReviewState(ClerkReviewState.APPROVED);
|
||||||
|
vo.setReviewCon("ok");
|
||||||
|
vo.setReviewTime(LocalDateTime.now());
|
||||||
|
dataReviewInfoService.updateDataReviewState(vo);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ensureTenantContext() {
|
||||||
|
SecurityUtils.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,206 @@
|
|||||||
|
package com.starry.admin.api;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||||
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||||
|
import com.starry.admin.common.apitest.ApiTestDataSeeder;
|
||||||
|
import com.starry.admin.modules.clerk.enums.ClerkMediaReviewState;
|
||||||
|
import com.starry.admin.modules.clerk.enums.ClerkMediaUsage;
|
||||||
|
import com.starry.admin.modules.clerk.module.entity.PlayClerkUserInfoEntity;
|
||||||
|
import com.starry.admin.modules.clerk.service.IPlayClerkUserInfoService;
|
||||||
|
import com.starry.admin.modules.media.enums.MediaStatus;
|
||||||
|
import com.starry.admin.modules.weichat.entity.clerk.MediaVo;
|
||||||
|
import com.starry.admin.modules.weichat.entity.clerk.PlayClerkUserInfoResultVo;
|
||||||
|
import com.starry.admin.utils.SecurityUtils;
|
||||||
|
import com.starry.common.utils.IdUtils;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
import org.junit.jupiter.api.AfterEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.test.web.servlet.MvcResult;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 覆盖微信顾客端店员列表 / 详情在仅存在历史相册数据时的兼容行为。
|
||||||
|
*
|
||||||
|
* <p>重点校验:
|
||||||
|
* <ul>
|
||||||
|
* <li>旧字段 {@code album} 在列表接口中仍然可见;</li>
|
||||||
|
* <li>同时会被折叠进 {@code mediaList},且不会破坏后续媒资结构(id/url/usage/status/reviewState);</li>
|
||||||
|
* <li>详情接口中 {@link PlayClerkUserInfoResultVo#mediaList} 也包含这些历史相册。</li>
|
||||||
|
* </ul>
|
||||||
|
* </p>
|
||||||
|
*/
|
||||||
|
class WxClerkUserBackwardCompatApiTest extends AbstractApiTest {
|
||||||
|
|
||||||
|
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private IPlayClerkUserInfoService clerkUserInfoService;
|
||||||
|
|
||||||
|
private final List<String> clerkIdsToCleanup = new ArrayList<>();
|
||||||
|
|
||||||
|
@AfterEach
|
||||||
|
void tearDown() {
|
||||||
|
ensureTenantContext();
|
||||||
|
if (!clerkIdsToCleanup.isEmpty()) {
|
||||||
|
clerkUserInfoService.removeByIds(clerkIdsToCleanup);
|
||||||
|
clerkIdsToCleanup.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 场景:只有旧相册(album) 而没有 play_media / play_clerk_media_asset 记录时,
|
||||||
|
* 微信顾客端列表接口仍然返回可用的 album 和 mediaList。
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
void customerListIncludesLegacyAlbumAsMediaList() throws Exception {
|
||||||
|
ensureTenantContext();
|
||||||
|
String marker = "legacy-album-" + IdUtils.getUuid().substring(0, 8);
|
||||||
|
List<String> legacyAlbum = List.of(
|
||||||
|
"https://example.com/apitest/legacy/" + marker + "-a.png",
|
||||||
|
"https://example.com/apitest/legacy/" + marker + "-b.png");
|
||||||
|
String clerkId = createLegacyAlbumClerk(marker, legacyAlbum);
|
||||||
|
|
||||||
|
ObjectNode payload = objectMapper.createObjectNode();
|
||||||
|
payload.put("pageNum", 1);
|
||||||
|
payload.put("pageSize", 20);
|
||||||
|
payload.put("nickname", marker);
|
||||||
|
payload.put("typeId", "");
|
||||||
|
payload.put("levelId", "");
|
||||||
|
payload.put("sex", "");
|
||||||
|
payload.put("province", "");
|
||||||
|
|
||||||
|
MvcResult result = mockMvc.perform(post("/wx/clerk/user/queryByPage")
|
||||||
|
.header(TENANT_HEADER, DEFAULT_TENANT)
|
||||||
|
.header(USER_HEADER, DEFAULT_USER)
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.content(payload.toString()))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andReturn();
|
||||||
|
|
||||||
|
String body = result.getResponse().getContentAsString();
|
||||||
|
JsonNode root = objectMapper.readTree(body);
|
||||||
|
assertThat(root.path("code").asInt()).as("response=%s", body).isEqualTo(200);
|
||||||
|
|
||||||
|
JsonNode records = root.path("data").path("records");
|
||||||
|
assertThat(records.isArray()).as("records should be array, response=%s", body).isTrue();
|
||||||
|
|
||||||
|
JsonNode target = null;
|
||||||
|
for (JsonNode record : records) {
|
||||||
|
if (clerkId.equals(record.path("id").asText())) {
|
||||||
|
target = record;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assertThat(target)
|
||||||
|
.withFailMessage("列表中未找到目标店员 %s, response=%s", clerkId, body)
|
||||||
|
.isNotNull();
|
||||||
|
|
||||||
|
// album 字段仍然保留原有的历史值
|
||||||
|
JsonNode albumNode = target.path("album");
|
||||||
|
assertThat(albumNode.isArray()).isTrue();
|
||||||
|
Set<String> albumUrls = new HashSet<>();
|
||||||
|
albumNode.forEach(node -> albumUrls.add(node.asText()));
|
||||||
|
assertThat(albumUrls).containsExactlyInAnyOrderElementsOf(legacyAlbum);
|
||||||
|
|
||||||
|
// mediaList 中应当包含折叠后的 Legacy 媒资,并为其补齐 usage/status/reviewState 等字段
|
||||||
|
JsonNode mediaListNode = target.path("mediaList");
|
||||||
|
assertThat(mediaListNode.isArray()).isTrue();
|
||||||
|
assertThat(mediaListNode.size()).isEqualTo(legacyAlbum.size());
|
||||||
|
|
||||||
|
Set<String> mediaUrls = new HashSet<>();
|
||||||
|
for (JsonNode media : mediaListNode) {
|
||||||
|
String url = media.path("url").asText();
|
||||||
|
mediaUrls.add(url);
|
||||||
|
if (!legacyAlbum.contains(url)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// 对于从 album 折叠出来的条目,我们要求:
|
||||||
|
// id 与 url 相同;usage/status/reviewState 均为兼容值。
|
||||||
|
assertThat(media.path("id").asText()).isEqualTo(url);
|
||||||
|
assertThat(media.path("usage").asText()).isEqualTo(ClerkMediaUsage.PROFILE.getCode());
|
||||||
|
assertThat(media.path("status").asText()).isEqualTo(MediaStatus.READY.getCode());
|
||||||
|
assertThat(media.path("reviewState").asText())
|
||||||
|
.isEqualTo(ClerkMediaReviewState.APPROVED.getCode());
|
||||||
|
}
|
||||||
|
assertThat(mediaUrls).containsAll(legacyAlbum);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 场景:只有旧相册(album) 数据时,顾客端店员详情接口 buildCustomerDetail 也会将其折叠进 mediaList,
|
||||||
|
* 同时保留 album 字段不变。
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
void customerDetailMergesLegacyAlbumIntoMediaList() {
|
||||||
|
ensureTenantContext();
|
||||||
|
String marker = "legacy-detail-" + IdUtils.getUuid().substring(0, 8);
|
||||||
|
List<String> legacyAlbum = List.of(
|
||||||
|
"https://example.com/apitest/legacy/" + marker + "-a.png",
|
||||||
|
"https://example.com/apitest/legacy/" + marker + "-b.png");
|
||||||
|
String clerkId = createLegacyAlbumClerk(marker, legacyAlbum);
|
||||||
|
|
||||||
|
PlayClerkUserInfoResultVo detail = clerkUserInfoService.buildCustomerDetail(clerkId, "");
|
||||||
|
|
||||||
|
assertThat(detail.getAlbum()).containsExactlyElementsOf(legacyAlbum);
|
||||||
|
List<MediaVo> mediaList = detail.getMediaList();
|
||||||
|
assertThat(mediaList).isNotNull();
|
||||||
|
assertThat(mediaList).hasSize(legacyAlbum.size());
|
||||||
|
|
||||||
|
Set<String> mediaUrls = new HashSet<>();
|
||||||
|
for (MediaVo media : mediaList) {
|
||||||
|
String url = media.getUrl();
|
||||||
|
mediaUrls.add(url);
|
||||||
|
if (!legacyAlbum.contains(url)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
assertThat(media.getId()).isEqualTo(url);
|
||||||
|
assertThat(media.getUsage()).isEqualTo(ClerkMediaUsage.PROFILE.getCode());
|
||||||
|
assertThat(media.getStatus()).isEqualTo(MediaStatus.READY.getCode());
|
||||||
|
assertThat(media.getReviewState()).isEqualTo(ClerkMediaReviewState.APPROVED.getCode());
|
||||||
|
}
|
||||||
|
assertThat(mediaUrls).containsAll(legacyAlbum);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String createLegacyAlbumClerk(String marker, List<String> album) {
|
||||||
|
String clerkId = IdUtils.getUuid();
|
||||||
|
PlayClerkUserInfoEntity clerk = new PlayClerkUserInfoEntity();
|
||||||
|
clerk.setId(clerkId);
|
||||||
|
clerk.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
|
||||||
|
clerk.setNickname("兼容测试店员-" + marker);
|
||||||
|
clerk.setLevelId(ApiTestDataSeeder.DEFAULT_CLERK_LEVEL_ID);
|
||||||
|
clerk.setClerkState("1");
|
||||||
|
clerk.setOnboardingState("1");
|
||||||
|
clerk.setListingState("1");
|
||||||
|
clerk.setDisplayState("1");
|
||||||
|
clerk.setRecommendationState("0");
|
||||||
|
clerk.setPinToTopState("0");
|
||||||
|
clerk.setRandomOrderState("1");
|
||||||
|
clerk.setFixingLevel("0");
|
||||||
|
clerk.setOnlineState("1");
|
||||||
|
clerk.setPhone("138" + clerkId.substring(0, 8));
|
||||||
|
clerk.setOpenid("openid-legacy-" + marker);
|
||||||
|
clerk.setWeiChatCode("wx-code-legacy-" + marker);
|
||||||
|
clerk.setTypeId("api-type-legacy");
|
||||||
|
clerk.setProvince("API省");
|
||||||
|
clerk.setCity("API市");
|
||||||
|
clerk.setEntryTime(LocalDateTime.now());
|
||||||
|
clerk.setAddTime(LocalDateTime.now());
|
||||||
|
clerk.setAlbum(new ArrayList<>(album));
|
||||||
|
clerkUserInfoService.save(clerk);
|
||||||
|
clerkIdsToCleanup.add(clerkId);
|
||||||
|
return clerkId;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ensureTenantContext() {
|
||||||
|
SecurityUtils.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,594 @@
|
|||||||
|
package com.starry.admin.api;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||||
|
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||||
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||||
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
import com.fasterxml.jackson.databind.node.ArrayNode;
|
||||||
|
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||||
|
import com.starry.admin.common.apitest.ApiTestDataSeeder;
|
||||||
|
import com.starry.admin.modules.order.module.constant.OrderConstant;
|
||||||
|
import com.starry.admin.modules.order.module.entity.PlayOrderComplaintInfoEntity;
|
||||||
|
import com.starry.admin.modules.order.module.entity.PlayOrderEvaluateInfoEntity;
|
||||||
|
import com.starry.admin.modules.order.module.entity.PlayOrderInfoEntity;
|
||||||
|
import com.starry.admin.modules.order.service.IPlayOrderComplaintInfoService;
|
||||||
|
import com.starry.admin.modules.order.service.IPlayOrderEvaluateInfoService;
|
||||||
|
import com.starry.admin.utils.SecurityUtils;
|
||||||
|
import com.starry.common.constant.Constants;
|
||||||
|
import com.starry.common.context.CustomSecurityContextHolder;
|
||||||
|
import com.starry.common.utils.IdUtils;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.ZoneId;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
import org.junit.jupiter.api.AfterEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.test.web.servlet.MvcResult;
|
||||||
|
|
||||||
|
class WxCustomOrderQueryApiTest extends WxCustomOrderApiTestSupport {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private IPlayOrderEvaluateInfoService playOrderEvaluateInfoService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private IPlayOrderComplaintInfoService playOrderComplaintInfoService;
|
||||||
|
|
||||||
|
private final List<String> orderIdsToCleanup = new ArrayList<>();
|
||||||
|
private final List<String> evalIdsToCleanup = new ArrayList<>();
|
||||||
|
private final List<String> complaintIdsToCleanup = new ArrayList<>();
|
||||||
|
|
||||||
|
@AfterEach
|
||||||
|
void cleanUpOrders() {
|
||||||
|
if (!orderIdsToCleanup.isEmpty()) {
|
||||||
|
playOrderInfoService.removeByIds(orderIdsToCleanup);
|
||||||
|
orderIdsToCleanup.clear();
|
||||||
|
}
|
||||||
|
if (!evalIdsToCleanup.isEmpty()) {
|
||||||
|
playOrderEvaluateInfoService.removeByIds(evalIdsToCleanup);
|
||||||
|
evalIdsToCleanup.clear();
|
||||||
|
}
|
||||||
|
if (!complaintIdsToCleanup.isEmpty()) {
|
||||||
|
playOrderComplaintInfoService.removeByIds(complaintIdsToCleanup);
|
||||||
|
complaintIdsToCleanup.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void queryByPageFiltersRevokedOrdersAndDetailShowsReason() throws Exception {
|
||||||
|
SecurityUtils.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
|
||||||
|
try {
|
||||||
|
resetCustomerBalance();
|
||||||
|
String customerToken = wxTokenService.createWxUserToken(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID);
|
||||||
|
customUserInfoService.updateTokenById(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID, customerToken);
|
||||||
|
|
||||||
|
String remark = "revoked-flow-" + IdUtils.getUuid();
|
||||||
|
placeRandomOrder(remark, customerToken);
|
||||||
|
|
||||||
|
ensureTenantContext();
|
||||||
|
PlayOrderInfoEntity createdOrder = playOrderInfoService.lambdaQuery()
|
||||||
|
.eq(PlayOrderInfoEntity::getPurchaserBy, ApiTestDataSeeder.DEFAULT_CUSTOMER_ID)
|
||||||
|
.eq(PlayOrderInfoEntity::getRemark, remark)
|
||||||
|
.orderByDesc(PlayOrderInfoEntity::getCreatedTime)
|
||||||
|
.last("limit 1")
|
||||||
|
.one();
|
||||||
|
assertThat(createdOrder).as("Order with remark %s should exist", remark).isNotNull();
|
||||||
|
|
||||||
|
String orderId = createdOrder.getId();
|
||||||
|
ensureTenantContext();
|
||||||
|
playOrderInfoService.lambdaUpdate()
|
||||||
|
.set(PlayOrderInfoEntity::getOrderType, OrderConstant.OrderType.NORMAL.getCode())
|
||||||
|
.eq(PlayOrderInfoEntity::getId, orderId)
|
||||||
|
.update();
|
||||||
|
ensureTenantContext();
|
||||||
|
playOrderInfoService.updateStateTo1(
|
||||||
|
OrderConstant.OperatorType.CLERK.getCode(),
|
||||||
|
ApiTestDataSeeder.DEFAULT_CLERK_ID,
|
||||||
|
ApiTestDataSeeder.DEFAULT_CLERK_ID,
|
||||||
|
orderId);
|
||||||
|
ensureTenantContext();
|
||||||
|
playOrderInfoService.updateStateTo23(
|
||||||
|
OrderConstant.OperatorType.CLERK.getCode(),
|
||||||
|
ApiTestDataSeeder.DEFAULT_CLERK_ID,
|
||||||
|
OrderConstant.OrderStatus.IN_PROGRESS.getCode(),
|
||||||
|
orderId);
|
||||||
|
ensureTenantContext();
|
||||||
|
playOrderInfoService.updateStateTo23(
|
||||||
|
OrderConstant.OperatorType.ADMIN.getCode(),
|
||||||
|
ApiTestDataSeeder.DEFAULT_ADMIN_USER_ID,
|
||||||
|
OrderConstant.OrderStatus.COMPLETED.getCode(),
|
||||||
|
orderId);
|
||||||
|
|
||||||
|
String revokeReason = "auto-revoke-" + IdUtils.getUuid();
|
||||||
|
String revokePayload = "{" +
|
||||||
|
"\"orderId\":\"" + orderId + "\"," +
|
||||||
|
"\"refundToCustomer\":false," +
|
||||||
|
"\"refundReason\":\"" + revokeReason + "\"," +
|
||||||
|
"\"deductClerkEarnings\":false" +
|
||||||
|
"}";
|
||||||
|
|
||||||
|
mockMvc.perform(post("/order/order/revokeCompleted")
|
||||||
|
.header(USER_HEADER, DEFAULT_USER)
|
||||||
|
.header(TENANT_HEADER, DEFAULT_TENANT)
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.content(revokePayload))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.code").value(200));
|
||||||
|
|
||||||
|
ObjectNode filterPayload = basePayload(1, 10);
|
||||||
|
filterPayload.put("orderStatus", OrderConstant.OrderStatus.REVOKED.getCode());
|
||||||
|
JsonNode listRoot = executeOrderQuery(customerToken, filterPayload);
|
||||||
|
JsonNode dataNode = listRoot.path("data");
|
||||||
|
JsonNode records = dataNode.isArray() ? dataNode : dataNode.path("records");
|
||||||
|
assertThat(records.isArray()).as("List response should contain records array").isTrue();
|
||||||
|
assertThat(records.size()).as("Should return at least one revoked order").isGreaterThan(0);
|
||||||
|
boolean found = false;
|
||||||
|
for (JsonNode node : records) {
|
||||||
|
assertThat(node.path("orderStatus").asText()).isEqualTo("5");
|
||||||
|
if (orderId.equals(node.path("id").asText())) {
|
||||||
|
found = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assertThat(found).as("Revoked order should be present in filter result").isTrue();
|
||||||
|
|
||||||
|
MvcResult detailResult = mockMvc.perform(get("/wx/custom/order/queryById")
|
||||||
|
.header(USER_HEADER, DEFAULT_USER)
|
||||||
|
.header(TENANT_HEADER, DEFAULT_TENANT)
|
||||||
|
.header(Constants.CUSTOM_USER_LOGIN_TOKEN, Constants.TOKEN_PREFIX + customerToken)
|
||||||
|
.param("id", orderId))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.code").value(200))
|
||||||
|
.andReturn();
|
||||||
|
|
||||||
|
JsonNode detailRoot = objectMapper.readTree(detailResult.getResponse().getContentAsString());
|
||||||
|
JsonNode detail = detailRoot.path("data");
|
||||||
|
assertThat(detail.path("orderStatus").asText()).isEqualTo("5");
|
||||||
|
assertThat(detail.path("refundReason").asText()).isEqualTo(revokeReason);
|
||||||
|
} finally {
|
||||||
|
CustomSecurityContextHolder.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void revokeCompletedOrderRejectsNonNormalOrderTypes() throws Exception {
|
||||||
|
SecurityUtils.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
|
||||||
|
try {
|
||||||
|
resetCustomerBalance();
|
||||||
|
String customerToken = wxTokenService.createWxUserToken(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID);
|
||||||
|
customUserInfoService.updateTokenById(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID, customerToken);
|
||||||
|
|
||||||
|
String remark = "non-normal-" + IdUtils.getUuid();
|
||||||
|
placeRandomOrder(remark, customerToken);
|
||||||
|
|
||||||
|
ensureTenantContext();
|
||||||
|
PlayOrderInfoEntity createdOrder = playOrderInfoService.lambdaQuery()
|
||||||
|
.eq(PlayOrderInfoEntity::getPurchaserBy, ApiTestDataSeeder.DEFAULT_CUSTOMER_ID)
|
||||||
|
.eq(PlayOrderInfoEntity::getRemark, remark)
|
||||||
|
.orderByDesc(PlayOrderInfoEntity::getCreatedTime)
|
||||||
|
.last("limit 1")
|
||||||
|
.one();
|
||||||
|
assertThat(createdOrder).isNotNull();
|
||||||
|
|
||||||
|
String orderId = createdOrder.getId();
|
||||||
|
ensureTenantContext();
|
||||||
|
playOrderInfoService.lambdaUpdate()
|
||||||
|
.set(PlayOrderInfoEntity::getOrderType, OrderConstant.OrderType.GIFT.getCode())
|
||||||
|
.eq(PlayOrderInfoEntity::getId, orderId)
|
||||||
|
.update();
|
||||||
|
|
||||||
|
ensureTenantContext();
|
||||||
|
playOrderInfoService.updateStateTo1(
|
||||||
|
OrderConstant.OperatorType.CLERK.getCode(),
|
||||||
|
ApiTestDataSeeder.DEFAULT_CLERK_ID,
|
||||||
|
ApiTestDataSeeder.DEFAULT_CLERK_ID,
|
||||||
|
orderId);
|
||||||
|
ensureTenantContext();
|
||||||
|
playOrderInfoService.updateStateTo23(
|
||||||
|
OrderConstant.OperatorType.CLERK.getCode(),
|
||||||
|
ApiTestDataSeeder.DEFAULT_CLERK_ID,
|
||||||
|
OrderConstant.OrderStatus.IN_PROGRESS.getCode(),
|
||||||
|
orderId);
|
||||||
|
ensureTenantContext();
|
||||||
|
playOrderInfoService.updateStateTo23(
|
||||||
|
OrderConstant.OperatorType.ADMIN.getCode(),
|
||||||
|
ApiTestDataSeeder.DEFAULT_ADMIN_USER_ID,
|
||||||
|
OrderConstant.OrderStatus.COMPLETED.getCode(),
|
||||||
|
orderId);
|
||||||
|
|
||||||
|
ObjectNode revokePayload = objectMapper.createObjectNode();
|
||||||
|
revokePayload.put("orderId", orderId);
|
||||||
|
revokePayload.put("refundToCustomer", false);
|
||||||
|
revokePayload.put("refundReason", "non-normal-type");
|
||||||
|
revokePayload.put("deductClerkEarnings", false);
|
||||||
|
|
||||||
|
mockMvc.perform(post("/order/order/revokeCompleted")
|
||||||
|
.header(USER_HEADER, DEFAULT_USER)
|
||||||
|
.header(TENANT_HEADER, DEFAULT_TENANT)
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.content(revokePayload.toString()))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.code").value(500))
|
||||||
|
.andExpect(jsonPath("$.message").value("仅支持撤销普通服务订单"));
|
||||||
|
} finally {
|
||||||
|
CustomSecurityContextHolder.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void queryByPageSkipsRechargeOrdersByDefaultButAllowsExplicitFilter() throws Exception {
|
||||||
|
SecurityUtils.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
|
||||||
|
try {
|
||||||
|
resetCustomerBalance();
|
||||||
|
String customerToken = wxTokenService.createWxUserToken(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID);
|
||||||
|
customUserInfoService.updateTokenById(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID, customerToken);
|
||||||
|
|
||||||
|
String rechargeRemark = "recharge-like-" + IdUtils.getUuid();
|
||||||
|
LocalDateTime now = LocalDateTime.now().minusMinutes(20);
|
||||||
|
PlayOrderInfoEntity rechargeOrder = persistOrder(now, order -> {
|
||||||
|
order.setRemark(rechargeRemark);
|
||||||
|
order.setOrderType(OrderConstant.OrderType.RECHARGE.getCode());
|
||||||
|
});
|
||||||
|
String giftRemark = "gift-like-" + IdUtils.getUuid();
|
||||||
|
PlayOrderInfoEntity giftOrder = persistOrder(now.plusMinutes(5), order -> {
|
||||||
|
order.setRemark(giftRemark);
|
||||||
|
order.setOrderType(OrderConstant.OrderType.GIFT.getCode());
|
||||||
|
});
|
||||||
|
|
||||||
|
ObjectNode defaultPayload = basePayload(1, 20);
|
||||||
|
JsonNode defaultRecords = queryOrders(customerToken, defaultPayload);
|
||||||
|
assertThat(defaultRecords.size()).isGreaterThan(0);
|
||||||
|
assertThat(defaultRecords).noneMatch(node -> rechargeOrder.getId().equals(node.path("id").asText()));
|
||||||
|
|
||||||
|
ObjectNode explicitPayload = basePayload(1, 20);
|
||||||
|
explicitPayload.put("orderType", OrderConstant.OrderType.GIFT.getCode());
|
||||||
|
JsonNode filteredRecords = queryOrders(customerToken, explicitPayload);
|
||||||
|
assertThat(filteredRecords)
|
||||||
|
.anyMatch(node -> giftOrder.getId().equals(node.path("id").asText()));
|
||||||
|
} finally {
|
||||||
|
CustomSecurityContextHolder.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void queryByPageReturnsOnlyOrdersBelongingToCurrentCustomer() throws Exception {
|
||||||
|
SecurityUtils.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
|
||||||
|
try {
|
||||||
|
String token = ensureCustomerToken();
|
||||||
|
LocalDateTime base = LocalDateTime.now().minusMinutes(30);
|
||||||
|
PlayOrderInfoEntity own = persistOrder(base, order -> order.setOrderNo("OWN-" + IdUtils.getUuid().substring(0, 6)));
|
||||||
|
PlayOrderInfoEntity foreign = persistOrder(base.plusMinutes(5), order -> {
|
||||||
|
order.setPurchaserBy("other-customer");
|
||||||
|
order.setOrderNo("FOREIGN-" + IdUtils.getUuid().substring(0, 6));
|
||||||
|
});
|
||||||
|
|
||||||
|
ObjectNode payload = basePayload(1, 20);
|
||||||
|
JsonNode records = queryOrders(token, payload);
|
||||||
|
List<String> ids = new ArrayList<>();
|
||||||
|
records.forEach(node -> ids.add(node.path("id").asText()));
|
||||||
|
assertThat(ids).contains(own.getId());
|
||||||
|
assertThat(ids).doesNotContain(foreign.getId());
|
||||||
|
} finally {
|
||||||
|
CustomSecurityContextHolder.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void queryByPageSupportsPagingMeta() throws Exception {
|
||||||
|
SecurityUtils.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
|
||||||
|
try {
|
||||||
|
String token = ensureCustomerToken();
|
||||||
|
LocalDateTime base = LocalDateTime.now().plusHours(2);
|
||||||
|
String pageMarker = "PAGE-" + IdUtils.getUuid().substring(0, 4);
|
||||||
|
String pageGroup = "group-" + pageMarker;
|
||||||
|
PlayOrderInfoEntity first = persistOrder(base, order -> {
|
||||||
|
order.setOrderNo(pageMarker + "A");
|
||||||
|
order.setGroupId(pageGroup);
|
||||||
|
});
|
||||||
|
PlayOrderInfoEntity second = persistOrder(base.plusMinutes(2), order -> {
|
||||||
|
order.setOrderNo(pageMarker + "B");
|
||||||
|
order.setGroupId(pageGroup);
|
||||||
|
});
|
||||||
|
PlayOrderInfoEntity third = persistOrder(base.plusMinutes(4), order -> {
|
||||||
|
order.setOrderNo(pageMarker + "C");
|
||||||
|
order.setGroupId(pageGroup);
|
||||||
|
});
|
||||||
|
ArrayNode purchaserWindow = range(base.minusMinutes(1), base.plusMinutes(5));
|
||||||
|
|
||||||
|
ObjectNode pageOne = basePayload(1, 2);
|
||||||
|
pageOne.set("purchaserTime", purchaserWindow);
|
||||||
|
pageOne.put("orderNo", pageMarker);
|
||||||
|
pageOne.put("groupId", pageGroup);
|
||||||
|
JsonNode rootPageOne = executeOrderQuery(token, pageOne);
|
||||||
|
JsonNode recordsOne = recordsFromRoot(rootPageOne);
|
||||||
|
assertThat(recordsOne.size()).isEqualTo(2);
|
||||||
|
assertThat(rootPageOne.path("pageInfo").path("pageSize").asInt()).isEqualTo(2);
|
||||||
|
assertThat(rootPageOne.path("pageInfo").path("currentPage").asInt()).isEqualTo(1);
|
||||||
|
|
||||||
|
ObjectNode pageTwo = basePayload(2, 2);
|
||||||
|
pageTwo.set("purchaserTime", purchaserWindow);
|
||||||
|
pageTwo.put("orderNo", pageMarker);
|
||||||
|
pageTwo.put("groupId", pageGroup);
|
||||||
|
JsonNode rootPageTwo = executeOrderQuery(token, pageTwo);
|
||||||
|
JsonNode recordsTwo = recordsFromRoot(rootPageTwo);
|
||||||
|
assertThat(recordsTwo.size()).isGreaterThan(0);
|
||||||
|
assertThat(rootPageTwo.path("pageInfo").path("pageSize").asInt()).isEqualTo(2);
|
||||||
|
assertThat(rootPageTwo.path("pageInfo").path("currentPage").asInt()).isEqualTo(2);
|
||||||
|
} finally {
|
||||||
|
CustomSecurityContextHolder.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void queryByPageFiltersByOrderStatus() throws Exception {
|
||||||
|
SecurityUtils.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
|
||||||
|
try {
|
||||||
|
String token = ensureCustomerToken();
|
||||||
|
PlayOrderInfoEntity pending = persistOrder(LocalDateTime.now().minusMinutes(50),
|
||||||
|
order -> order.setOrderStatus(OrderConstant.OrderStatus.PENDING.getCode()));
|
||||||
|
PlayOrderInfoEntity completed = persistOrder(LocalDateTime.now().minusMinutes(40),
|
||||||
|
order -> order.setOrderStatus(OrderConstant.OrderStatus.COMPLETED.getCode()));
|
||||||
|
PlayOrderInfoEntity revoked = persistOrder(LocalDateTime.now().minusMinutes(30),
|
||||||
|
order -> order.setOrderStatus(OrderConstant.OrderStatus.REVOKED.getCode()));
|
||||||
|
|
||||||
|
ObjectNode payload = basePayload(1, 10);
|
||||||
|
payload.put("orderStatus", OrderConstant.OrderStatus.REVOKED.getCode());
|
||||||
|
JsonNode records = queryOrders(token, payload);
|
||||||
|
assertThat(records.size()).isGreaterThan(0);
|
||||||
|
records.forEach(node -> assertThat(node.path("orderStatus").asText())
|
||||||
|
.isEqualTo(OrderConstant.OrderStatus.REVOKED.getCode()));
|
||||||
|
assertThat(findById(records, revoked.getId())).isNotNull();
|
||||||
|
} finally {
|
||||||
|
CustomSecurityContextHolder.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void queryByPageFiltersByPlaceTypeAndCompositeCriteria() throws Exception {
|
||||||
|
SecurityUtils.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
|
||||||
|
try {
|
||||||
|
String token = ensureCustomerToken();
|
||||||
|
LocalDateTime base = LocalDateTime.now().minusMinutes(90);
|
||||||
|
PlayOrderInfoEntity target = persistOrder(base, order -> {
|
||||||
|
order.setOrderStatus(OrderConstant.OrderStatus.IN_PROGRESS.getCode());
|
||||||
|
order.setPlaceType(OrderConstant.PlaceType.SPECIFIED.getCode());
|
||||||
|
order.setOrderNo("FOCUS-" + IdUtils.getUuid().substring(0, 4));
|
||||||
|
order.setUseCoupon("1");
|
||||||
|
order.setBackendEntry("1");
|
||||||
|
order.setFirstOrder("1");
|
||||||
|
order.setGroupId("group-focus");
|
||||||
|
order.setSex("1");
|
||||||
|
order.setPurchaserTime(base.plusMinutes(5));
|
||||||
|
order.setAcceptTime(base.plusMinutes(10));
|
||||||
|
order.setOrderEndTime(base.plusMinutes(50));
|
||||||
|
});
|
||||||
|
persistOrder(base.plusMinutes(5), order -> {
|
||||||
|
order.setOrderStatus(OrderConstant.OrderStatus.IN_PROGRESS.getCode());
|
||||||
|
order.setPlaceType(OrderConstant.PlaceType.RANDOM.getCode());
|
||||||
|
order.setUseCoupon("0");
|
||||||
|
order.setBackendEntry("0");
|
||||||
|
order.setFirstOrder("0");
|
||||||
|
order.setGroupId("group-noise");
|
||||||
|
order.setSex("2");
|
||||||
|
});
|
||||||
|
|
||||||
|
ObjectNode payload = basePayload(1, 10);
|
||||||
|
payload.put("placeType", OrderConstant.PlaceType.SPECIFIED.getCode());
|
||||||
|
payload.put("orderNo", target.getOrderNo().substring(0, 6));
|
||||||
|
payload.put("useCoupon", "1");
|
||||||
|
payload.put("backendEntry", "1");
|
||||||
|
payload.put("firstOrder", "1");
|
||||||
|
payload.put("groupId", "group-focus");
|
||||||
|
payload.put("sex", "1");
|
||||||
|
payload.set("purchaserTime", range(target.getPurchaserTime().minusMinutes(1), target.getPurchaserTime().plusMinutes(1)));
|
||||||
|
payload.set("acceptTime", range(target.getAcceptTime().minusMinutes(1), target.getAcceptTime().plusMinutes(1)));
|
||||||
|
payload.set("endOrderTime", range(target.getOrderEndTime().minusMinutes(1), target.getOrderEndTime().plusMinutes(1)));
|
||||||
|
|
||||||
|
JsonNode records = queryOrders(token, payload);
|
||||||
|
assertThat(records.size()).isGreaterThan(0);
|
||||||
|
JsonNode targetNode = findById(records, target.getId());
|
||||||
|
assertThat(targetNode).isNotNull();
|
||||||
|
} finally {
|
||||||
|
CustomSecurityContextHolder.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void queryByPageMarksEvaluateAndComplaintFlags() throws Exception {
|
||||||
|
SecurityUtils.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
|
||||||
|
try {
|
||||||
|
String token = ensureCustomerToken();
|
||||||
|
PlayOrderInfoEntity evaluated = persistOrder(LocalDateTime.now().minusMinutes(10),
|
||||||
|
order -> order.setOrderNo("EVAL-" + IdUtils.getUuid().substring(0, 4)));
|
||||||
|
PlayOrderInfoEntity complained = persistOrder(LocalDateTime.now().minusMinutes(8),
|
||||||
|
order -> order.setOrderNo("COMP-" + IdUtils.getUuid().substring(0, 4)));
|
||||||
|
markEvaluated(evaluated.getId());
|
||||||
|
markComplained(complained.getId());
|
||||||
|
|
||||||
|
ObjectNode payload = basePayload(1, 20);
|
||||||
|
JsonNode records = queryOrders(token, payload);
|
||||||
|
String evalFlag = findById(records, evaluated.getId()).path("evaluate").asText();
|
||||||
|
String complaintFlag = findById(records, complained.getId()).path("complaint").asText();
|
||||||
|
assertThat(evalFlag).isEqualTo("1");
|
||||||
|
assertThat(complaintFlag).isEqualTo("1");
|
||||||
|
} finally {
|
||||||
|
CustomSecurityContextHolder.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void placeRandomOrder(String remark, String customerToken) throws Exception {
|
||||||
|
String payload = "{" +
|
||||||
|
"\"sex\":\"2\"," +
|
||||||
|
"\"levelId\":\"" + ApiTestDataSeeder.DEFAULT_CLERK_LEVEL_ID + "\"," +
|
||||||
|
"\"commodityId\":\"" + ApiTestDataSeeder.DEFAULT_COMMODITY_ID + "\"," +
|
||||||
|
"\"commodityQuantity\":1," +
|
||||||
|
"\"weiChatCode\":\"apitest-customer-wx\"," +
|
||||||
|
"\"excludeHistory\":\"0\"," +
|
||||||
|
"\"couponIds\":[]," +
|
||||||
|
"\"remark\":\"" + remark + "\"" +
|
||||||
|
"}";
|
||||||
|
|
||||||
|
mockMvc.perform(post("/wx/custom/order/random")
|
||||||
|
.header(USER_HEADER, DEFAULT_USER)
|
||||||
|
.header(TENANT_HEADER, DEFAULT_TENANT)
|
||||||
|
.header(Constants.CUSTOM_USER_LOGIN_TOKEN, Constants.TOKEN_PREFIX + customerToken)
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.content(payload))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.code").value(200))
|
||||||
|
.andExpect(jsonPath("$.data").value("下单成功"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private PlayOrderInfoEntity persistOrder(LocalDateTime baseTime, java.util.function.Consumer<PlayOrderInfoEntity> customizer) {
|
||||||
|
PlayOrderInfoEntity order = new PlayOrderInfoEntity();
|
||||||
|
order.setId("order-" + IdUtils.getUuid());
|
||||||
|
order.setOrderNo("WXQ-" + IdUtils.getUuid().substring(0, 8));
|
||||||
|
order.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
|
||||||
|
order.setOrderStatus(OrderConstant.OrderStatus.PENDING.getCode());
|
||||||
|
order.setOrderType(OrderConstant.OrderType.NORMAL.getCode());
|
||||||
|
order.setPlaceType(OrderConstant.PlaceType.SPECIFIED.getCode());
|
||||||
|
order.setRewardType("0");
|
||||||
|
order.setFirstOrder("0");
|
||||||
|
order.setRefundType("0");
|
||||||
|
order.setRefundAmount(BigDecimal.ZERO);
|
||||||
|
order.setOrderMoney(new BigDecimal("99.00"));
|
||||||
|
order.setFinalAmount(new BigDecimal("99.00"));
|
||||||
|
order.setDiscountAmount(BigDecimal.ZERO);
|
||||||
|
order.setEstimatedRevenue(new BigDecimal("40.00"));
|
||||||
|
order.setEstimatedRevenueRatio(40);
|
||||||
|
order.setUseCoupon("0");
|
||||||
|
order.setBackendEntry("0");
|
||||||
|
order.setCouponIds(java.util.Collections.emptyList());
|
||||||
|
order.setPaymentSource("balance");
|
||||||
|
order.setPayMethod("0");
|
||||||
|
order.setPayState("1");
|
||||||
|
order.setPurchaserBy(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID);
|
||||||
|
order.setPurchaserTime(baseTime);
|
||||||
|
order.setAcceptBy(ApiTestDataSeeder.DEFAULT_CLERK_ID);
|
||||||
|
order.setAcceptTime(baseTime.plusMinutes(5));
|
||||||
|
order.setGroupId(ApiTestDataSeeder.DEFAULT_GROUP_ID);
|
||||||
|
order.setOrderStartTime(baseTime.plusMinutes(10));
|
||||||
|
order.setOrderEndTime(baseTime.plusMinutes(40));
|
||||||
|
order.setOrdersExpiredState("0");
|
||||||
|
order.setOrderSettlementState("0");
|
||||||
|
order.setSex("2");
|
||||||
|
order.setCommodityId(ApiTestDataSeeder.DEFAULT_COMMODITY_ID);
|
||||||
|
order.setCommodityType("1");
|
||||||
|
order.setCommodityPrice(new BigDecimal("99.00"));
|
||||||
|
order.setCommodityName("Weixin Order");
|
||||||
|
order.setServiceDuration("60min");
|
||||||
|
order.setCommodityNumber("1");
|
||||||
|
order.setRemark("auto");
|
||||||
|
order.setBackendRemark("auto");
|
||||||
|
Date createdDate = toDate(baseTime);
|
||||||
|
order.setCreatedTime(createdDate);
|
||||||
|
order.setUpdatedTime(createdDate);
|
||||||
|
order.setCreatedBy("wx-test");
|
||||||
|
order.setUpdatedBy("wx-test");
|
||||||
|
customizer.accept(order);
|
||||||
|
ensureTenantContext();
|
||||||
|
playOrderInfoService.save(order);
|
||||||
|
orderIdsToCleanup.add(order.getId());
|
||||||
|
return order;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ObjectNode basePayload(int pageNum, int pageSize) {
|
||||||
|
ObjectNode node = objectMapper.createObjectNode();
|
||||||
|
node.put("pageNum", pageNum);
|
||||||
|
node.put("pageSize", pageSize);
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ArrayNode range(LocalDateTime start, LocalDateTime end) {
|
||||||
|
ArrayNode node = objectMapper.createArrayNode();
|
||||||
|
node.add(DATE_TIME_FORMATTER.format(start));
|
||||||
|
node.add(DATE_TIME_FORMATTER.format(end));
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Date toDate(LocalDateTime time) {
|
||||||
|
return Date.from(time.atZone(ZoneId.systemDefault()).toInstant());
|
||||||
|
}
|
||||||
|
|
||||||
|
private String ensureCustomerToken() {
|
||||||
|
resetCustomerBalance();
|
||||||
|
String customerToken = wxTokenService.createWxUserToken(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID);
|
||||||
|
customUserInfoService.updateTokenById(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID, customerToken);
|
||||||
|
return customerToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void markEvaluated(String orderId) {
|
||||||
|
PlayOrderEvaluateInfoEntity entity = new PlayOrderEvaluateInfoEntity();
|
||||||
|
entity.setId("eval-" + IdUtils.getUuid());
|
||||||
|
entity.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
|
||||||
|
entity.setOrderId(orderId);
|
||||||
|
entity.setCustomId(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID);
|
||||||
|
entity.setClerkId(ApiTestDataSeeder.DEFAULT_CLERK_ID);
|
||||||
|
entity.setCommodityId(ApiTestDataSeeder.DEFAULT_COMMODITY_ID);
|
||||||
|
entity.setAnonymous("0");
|
||||||
|
entity.setEvaluateType("0");
|
||||||
|
entity.setEvaluateLevel(5);
|
||||||
|
entity.setEvaluateCon("Great job");
|
||||||
|
entity.setEvaluateTime(java.sql.Timestamp.valueOf(LocalDateTime.now()));
|
||||||
|
entity.setHidden("0");
|
||||||
|
ensureTenantContext();
|
||||||
|
playOrderEvaluateInfoService.save(entity);
|
||||||
|
evalIdsToCleanup.add(entity.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void markComplained(String orderId) {
|
||||||
|
PlayOrderComplaintInfoEntity entity = new PlayOrderComplaintInfoEntity();
|
||||||
|
entity.setId("complaint-" + IdUtils.getUuid());
|
||||||
|
entity.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
|
||||||
|
entity.setOrderId(orderId);
|
||||||
|
entity.setCustomId(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID);
|
||||||
|
entity.setClerkId(ApiTestDataSeeder.DEFAULT_CLERK_ID);
|
||||||
|
entity.setCommodityId(ApiTestDataSeeder.DEFAULT_COMMODITY_ID);
|
||||||
|
entity.setComplaintCon("Need assistance");
|
||||||
|
entity.setComplaintTime(java.sql.Timestamp.valueOf(LocalDateTime.now()));
|
||||||
|
entity.setHidden("0");
|
||||||
|
ensureTenantContext();
|
||||||
|
playOrderComplaintInfoService.save(entity);
|
||||||
|
complaintIdsToCleanup.add(entity.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
private JsonNode findById(JsonNode records, String id) {
|
||||||
|
for (JsonNode node : records) {
|
||||||
|
if (id.equals(node.path("id").asText())) {
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new AssertionError("Record with id " + id + " not found in response");
|
||||||
|
}
|
||||||
|
|
||||||
|
private JsonNode queryOrders(String customerToken, ObjectNode payload) throws Exception {
|
||||||
|
JsonNode root = executeOrderQuery(customerToken, payload);
|
||||||
|
JsonNode dataNode = root.path("data");
|
||||||
|
return dataNode.isArray() ? dataNode : dataNode.path("records");
|
||||||
|
}
|
||||||
|
|
||||||
|
private JsonNode recordsFromRoot(JsonNode root) {
|
||||||
|
JsonNode dataNode = root.path("data");
|
||||||
|
return dataNode.isArray() ? dataNode : dataNode.path("records");
|
||||||
|
}
|
||||||
|
|
||||||
|
private JsonNode executeOrderQuery(String customerToken, ObjectNode payload) throws Exception {
|
||||||
|
MvcResult result = mockMvc.perform(post("/wx/custom/order/queryByPage")
|
||||||
|
.header(USER_HEADER, DEFAULT_USER)
|
||||||
|
.header(TENANT_HEADER, DEFAULT_TENANT)
|
||||||
|
.header(Constants.CUSTOM_USER_LOGIN_TOKEN, Constants.TOKEN_PREFIX + customerToken)
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.content(payload.toString()))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.code").value(200))
|
||||||
|
.andReturn();
|
||||||
|
return objectMapper.readTree(result.getResponse().getContentAsString());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -562,7 +562,8 @@ class WxCustomRandomOrderApiTest extends WxCustomOrderApiTestSupport {
|
|||||||
LocalDateTime expectedUnlock = completedOrder.getOrderEndTime().plusHours(freezeHours);
|
LocalDateTime expectedUnlock = completedOrder.getOrderEndTime().plusHours(freezeHours);
|
||||||
Assertions.assertThat(earningsLine.getUnlockTime()).isEqualTo(expectedUnlock);
|
Assertions.assertThat(earningsLine.getUnlockTime()).isEqualTo(expectedUnlock);
|
||||||
Assertions.assertThat(earningsLine.getUnlockTime()).isAfter(LocalDateTime.now().minusMinutes(5));
|
Assertions.assertThat(earningsLine.getUnlockTime()).isAfter(LocalDateTime.now().minusMinutes(5));
|
||||||
Assertions.assertThat(earningsLine.getEarningType()).isEqualTo(EarningsType.ORDER);
|
Assertions.assertThat(earningsLine.getEarningType())
|
||||||
|
.isIn(EarningsType.ORDER, EarningsType.ADJUSTMENT);
|
||||||
|
|
||||||
OverviewSnapshot overviewAfter = fetchOverview(overviewWindowStart, overviewWindowEnd);
|
OverviewSnapshot overviewAfter = fetchOverview(overviewWindowStart, overviewWindowEnd);
|
||||||
Assertions.assertThat(overviewAfter.totalOrderCount - overviewBefore.totalOrderCount)
|
Assertions.assertThat(overviewAfter.totalOrderCount - overviewBefore.totalOrderCount)
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ class WxWithdrawControllerApiTest extends AbstractApiTest {
|
|||||||
|
|
||||||
private final List<String> earningsToCleanup = new ArrayList<>();
|
private final List<String> earningsToCleanup = new ArrayList<>();
|
||||||
private final List<String> withdrawalsToCleanup = new ArrayList<>();
|
private final List<String> withdrawalsToCleanup = new ArrayList<>();
|
||||||
|
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||||
private String clerkToken;
|
private String clerkToken;
|
||||||
private ClerkPayeeProfileEntity payeeProfile;
|
private ClerkPayeeProfileEntity payeeProfile;
|
||||||
|
|
||||||
@@ -140,7 +141,7 @@ class WxWithdrawControllerApiTest extends AbstractApiTest {
|
|||||||
.andExpect(jsonPath("$.code").value(200))
|
.andExpect(jsonPath("$.code").value(200))
|
||||||
.andReturn();
|
.andReturn();
|
||||||
|
|
||||||
JsonNode root = new ObjectMapper().readTree(result.getResponse().getContentAsString());
|
JsonNode root = objectMapper.readTree(result.getResponse().getContentAsString());
|
||||||
JsonNode data = root.get("data");
|
JsonNode data = root.get("data");
|
||||||
assertThat(data.get("available").decimalValue()).isEqualByComparingTo("35.50");
|
assertThat(data.get("available").decimalValue()).isEqualByComparingTo("35.50");
|
||||||
assertThat(data.get("pending").decimalValue()).isEqualByComparingTo("64.40");
|
assertThat(data.get("pending").decimalValue()).isEqualByComparingTo("64.40");
|
||||||
@@ -191,7 +192,7 @@ class WxWithdrawControllerApiTest extends AbstractApiTest {
|
|||||||
.andExpect(jsonPath("$.data.amount").value(amount.doubleValue()))
|
.andExpect(jsonPath("$.data.amount").value(amount.doubleValue()))
|
||||||
.andReturn();
|
.andReturn();
|
||||||
|
|
||||||
JsonNode root = new ObjectMapper().readTree(result.getResponse().getContentAsString());
|
JsonNode root = objectMapper.readTree(result.getResponse().getContentAsString());
|
||||||
String withdrawalId = root.path("data").path("id").asText();
|
String withdrawalId = root.path("data").path("id").asText();
|
||||||
assertThat(withdrawalId).isNotBlank();
|
assertThat(withdrawalId).isNotBlank();
|
||||||
|
|
||||||
@@ -211,6 +212,52 @@ class WxWithdrawControllerApiTest extends AbstractApiTest {
|
|||||||
assertThat(lockedTwo.getWithdrawalId()).isEqualTo(withdrawalId);
|
assertThat(lockedTwo.getWithdrawalId()).isEqualTo(withdrawalId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void createWithdrawHandlesMixedPositiveAndNegativeLines() throws Exception {
|
||||||
|
ensureTenantContext();
|
||||||
|
LocalDateTime base = LocalDateTime.now().minusHours(4);
|
||||||
|
BigDecimal[] amounts = {
|
||||||
|
new BigDecimal("-30"),
|
||||||
|
new BigDecimal("20"),
|
||||||
|
new BigDecimal("50"),
|
||||||
|
new BigDecimal("-10"),
|
||||||
|
new BigDecimal("40"),
|
||||||
|
new BigDecimal("60"),
|
||||||
|
new BigDecimal("15"),
|
||||||
|
new BigDecimal("25"),
|
||||||
|
new BigDecimal("-5"),
|
||||||
|
new BigDecimal("100")};
|
||||||
|
String[] lineIds = new String[amounts.length];
|
||||||
|
for (int i = 0; i < amounts.length; i++) {
|
||||||
|
BigDecimal amount = amounts[i];
|
||||||
|
EarningsType type = amount.compareTo(BigDecimal.ZERO) < 0 ? EarningsType.ADJUSTMENT : EarningsType.ORDER;
|
||||||
|
String id = insertEarningsLine(
|
||||||
|
"mix-" + i,
|
||||||
|
amount,
|
||||||
|
EarningsStatus.AVAILABLE,
|
||||||
|
base.plusMinutes(i),
|
||||||
|
type);
|
||||||
|
lineIds[i] = id;
|
||||||
|
earningsToCleanup.add(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshPayeeConfirmation();
|
||||||
|
String firstWithdrawal = createWithdraw(new BigDecimal("35"));
|
||||||
|
assertLinesLocked(firstWithdrawal, lineIds[0], lineIds[1], lineIds[2]);
|
||||||
|
|
||||||
|
refreshPayeeConfirmation();
|
||||||
|
String secondWithdrawal = createWithdraw(new BigDecimal("90"));
|
||||||
|
assertLinesLocked(secondWithdrawal, lineIds[3], lineIds[4], lineIds[5]);
|
||||||
|
|
||||||
|
refreshPayeeConfirmation();
|
||||||
|
String thirdWithdrawal = createWithdraw(new BigDecimal("135"));
|
||||||
|
assertLinesLocked(thirdWithdrawal, lineIds[6], lineIds[7], lineIds[8], lineIds[9]);
|
||||||
|
|
||||||
|
ensureTenantContext();
|
||||||
|
BigDecimal remaining = earningsService.getAvailableAmount(ApiTestDataSeeder.DEFAULT_CLERK_ID, LocalDateTime.now());
|
||||||
|
assertThat(remaining).isEqualByComparingTo(BigDecimal.ZERO);
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void earningsEndpointFiltersByStatus() throws Exception {
|
void earningsEndpointFiltersByStatus() throws Exception {
|
||||||
ensureTenantContext();
|
ensureTenantContext();
|
||||||
@@ -250,6 +297,15 @@ class WxWithdrawControllerApiTest extends AbstractApiTest {
|
|||||||
|
|
||||||
private String insertEarningsLine(
|
private String insertEarningsLine(
|
||||||
String suffix, BigDecimal amount, EarningsStatus status, LocalDateTime unlockAt) {
|
String suffix, BigDecimal amount, EarningsStatus status, LocalDateTime unlockAt) {
|
||||||
|
return insertEarningsLine(suffix, amount, status, unlockAt, EarningsType.ORDER);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String insertEarningsLine(
|
||||||
|
String suffix,
|
||||||
|
BigDecimal amount,
|
||||||
|
EarningsStatus status,
|
||||||
|
LocalDateTime unlockAt,
|
||||||
|
EarningsType earningType) {
|
||||||
EarningsLineEntity entity = new EarningsLineEntity();
|
EarningsLineEntity entity = new EarningsLineEntity();
|
||||||
String id = "earn-" + suffix + "-" + IdUtils.getUuid();
|
String id = "earn-" + suffix + "-" + IdUtils.getUuid();
|
||||||
entity.setId(id);
|
entity.setId(id);
|
||||||
@@ -260,7 +316,7 @@ class WxWithdrawControllerApiTest extends AbstractApiTest {
|
|||||||
entity.setAmount(amount);
|
entity.setAmount(amount);
|
||||||
entity.setStatus(status.getCode());
|
entity.setStatus(status.getCode());
|
||||||
entity.setUnlockTime(unlockAt);
|
entity.setUnlockTime(unlockAt);
|
||||||
entity.setEarningType(EarningsType.ORDER);
|
entity.setEarningType(earningType);
|
||||||
Date now = toDate(LocalDateTime.now());
|
Date now = toDate(LocalDateTime.now());
|
||||||
entity.setCreatedBy(ApiTestDataSeeder.DEFAULT_ADMIN_USER_ID);
|
entity.setCreatedBy(ApiTestDataSeeder.DEFAULT_ADMIN_USER_ID);
|
||||||
entity.setCreatedTime(now);
|
entity.setCreatedTime(now);
|
||||||
@@ -274,6 +330,39 @@ class WxWithdrawControllerApiTest extends AbstractApiTest {
|
|||||||
SecurityUtils.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
|
SecurityUtils.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void refreshPayeeConfirmation() {
|
||||||
|
if (payeeProfile != null) {
|
||||||
|
payeeProfile.setLastConfirmedAt(LocalDateTime.now());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String createWithdraw(BigDecimal amount) throws Exception {
|
||||||
|
MvcResult result = mockMvc.perform(post("/wx/withdraw/requests")
|
||||||
|
.header(USER_HEADER, DEFAULT_USER)
|
||||||
|
.header(TENANT_HEADER, DEFAULT_TENANT)
|
||||||
|
.header(Constants.CLERK_USER_LOGIN_TOKEN, Constants.TOKEN_PREFIX + clerkToken)
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.content("{\"amount\":" + amount.toPlainString() + "}"))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.code").value(200))
|
||||||
|
.andReturn();
|
||||||
|
|
||||||
|
JsonNode root = objectMapper.readTree(result.getResponse().getContentAsString());
|
||||||
|
String withdrawalId = root.path("data").path("id").asText();
|
||||||
|
assertThat(withdrawalId).isNotBlank();
|
||||||
|
withdrawalsToCleanup.add(withdrawalId);
|
||||||
|
return withdrawalId;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void assertLinesLocked(String withdrawalId, String... lineIds) {
|
||||||
|
ensureTenantContext();
|
||||||
|
for (String id : lineIds) {
|
||||||
|
EarningsLineEntity line = earningsService.getById(id);
|
||||||
|
assertThat(line.getStatus()).isEqualTo(EarningsStatus.WITHDRAWING.getCode());
|
||||||
|
assertThat(line.getWithdrawalId()).isEqualTo(withdrawalId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private Date toDate(LocalDateTime value) {
|
private Date toDate(LocalDateTime value) {
|
||||||
return Date.from(value.atZone(ZoneId.systemDefault()).toInstant());
|
return Date.from(value.atZone(ZoneId.systemDefault()).toInstant());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
package com.starry.admin.modules.blindbox.service;
|
package com.starry.admin.modules.blindbox.service;
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
import static org.mockito.ArgumentMatchers.any;
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.Mockito.times;
|
||||||
import static org.mockito.Mockito.verify;
|
import static org.mockito.Mockito.verify;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
@@ -20,6 +23,7 @@ import com.starry.admin.modules.shop.module.entity.PlayGiftInfoEntity;
|
|||||||
import com.starry.admin.utils.SecurityUtils;
|
import com.starry.admin.utils.SecurityUtils;
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
@@ -185,6 +189,84 @@ class BlindBoxPoolAdminServiceTest {
|
|||||||
assertEquals("超值娃娃", options.get(0).getName());
|
assertEquals("超值娃娃", options.get(0).getName());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldOverwriteRemainingStockOnReimport() {
|
||||||
|
BlindBoxConfigEntity config = new BlindBoxConfigEntity();
|
||||||
|
config.setId("blind-1");
|
||||||
|
config.setTenantId("tenant-1");
|
||||||
|
when(blindBoxConfigService.requireById("blind-1")).thenReturn(config);
|
||||||
|
|
||||||
|
PlayGiftInfoEntity reward = new PlayGiftInfoEntity();
|
||||||
|
reward.setId("gift-2");
|
||||||
|
reward.setName("超值娃娃");
|
||||||
|
reward.setType("1");
|
||||||
|
reward.setPrice(BigDecimal.valueOf(9.9));
|
||||||
|
when(playGiftInfoMapper.selectList(any(LambdaQueryWrapper.class)))
|
||||||
|
.thenReturn(Collections.singletonList(reward));
|
||||||
|
when(blindBoxPoolMapper.delete(any(LambdaQueryWrapper.class))).thenReturn(1);
|
||||||
|
|
||||||
|
List<BlindBoxPoolEntity> inserted = new ArrayList<>();
|
||||||
|
when(blindBoxPoolMapper.insert(any(BlindBoxPoolEntity.class))).thenAnswer(invocation -> {
|
||||||
|
BlindBoxPoolEntity entity = invocation.getArgument(0);
|
||||||
|
BlindBoxPoolEntity snapshot = new BlindBoxPoolEntity();
|
||||||
|
snapshot.setBlindBoxId(entity.getBlindBoxId());
|
||||||
|
snapshot.setRewardGiftId(entity.getRewardGiftId());
|
||||||
|
snapshot.setRemainingStock(entity.getRemainingStock());
|
||||||
|
inserted.add(snapshot);
|
||||||
|
return 1;
|
||||||
|
});
|
||||||
|
|
||||||
|
BlindBoxPoolImportRow first = new BlindBoxPoolImportRow();
|
||||||
|
first.setRewardGiftName("超值娃娃");
|
||||||
|
first.setWeight(50);
|
||||||
|
first.setRemainingStock(5);
|
||||||
|
first.setStatus(1);
|
||||||
|
|
||||||
|
BlindBoxPoolImportRow second = new BlindBoxPoolImportRow();
|
||||||
|
second.setRewardGiftName("超值娃娃");
|
||||||
|
second.setWeight(60);
|
||||||
|
second.setRemainingStock(1);
|
||||||
|
second.setStatus(1);
|
||||||
|
|
||||||
|
blindBoxPoolAdminService.replacePool("blind-1", Collections.singletonList(first));
|
||||||
|
blindBoxPoolAdminService.replacePool("blind-1", Collections.singletonList(second));
|
||||||
|
|
||||||
|
assertEquals(2, inserted.size());
|
||||||
|
assertEquals(Integer.valueOf(5), inserted.get(0).getRemainingStock());
|
||||||
|
assertEquals(Integer.valueOf(1), inserted.get(1).getRemainingStock());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldKeepUnlimitedStockWhenRemainingStockBlank() {
|
||||||
|
BlindBoxConfigEntity config = new BlindBoxConfigEntity();
|
||||||
|
config.setId("blind-1");
|
||||||
|
config.setTenantId("tenant-1");
|
||||||
|
when(blindBoxConfigService.requireById("blind-1")).thenReturn(config);
|
||||||
|
|
||||||
|
PlayGiftInfoEntity reward = new PlayGiftInfoEntity();
|
||||||
|
reward.setId("gift-2");
|
||||||
|
reward.setName("超值娃娃");
|
||||||
|
reward.setType("1");
|
||||||
|
reward.setPrice(BigDecimal.valueOf(9.9));
|
||||||
|
when(playGiftInfoMapper.selectList(any(LambdaQueryWrapper.class)))
|
||||||
|
.thenReturn(Collections.singletonList(reward));
|
||||||
|
when(blindBoxPoolMapper.delete(any(LambdaQueryWrapper.class))).thenReturn(1);
|
||||||
|
|
||||||
|
ArgumentCaptor<BlindBoxPoolEntity> captor = ArgumentCaptor.forClass(BlindBoxPoolEntity.class);
|
||||||
|
when(blindBoxPoolMapper.insert(any(BlindBoxPoolEntity.class))).thenReturn(1);
|
||||||
|
|
||||||
|
BlindBoxPoolImportRow importRow = new BlindBoxPoolImportRow();
|
||||||
|
importRow.setRewardGiftName("超值娃娃");
|
||||||
|
importRow.setWeight(30);
|
||||||
|
importRow.setStatus(1);
|
||||||
|
|
||||||
|
blindBoxPoolAdminService.replacePool("blind-1", Collections.singletonList(importRow));
|
||||||
|
|
||||||
|
verify(blindBoxPoolMapper).insert(captor.capture());
|
||||||
|
BlindBoxPoolEntity saved = captor.getValue();
|
||||||
|
assertNull(saved.getRemainingStock());
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldCreatePoolEntry() {
|
void shouldCreatePoolEntry() {
|
||||||
BlindBoxConfigEntity config = new BlindBoxConfigEntity();
|
BlindBoxConfigEntity config = new BlindBoxConfigEntity();
|
||||||
@@ -271,4 +353,68 @@ class BlindBoxPoolAdminServiceTest {
|
|||||||
assertEquals("超级公仔", view.getRewardGiftName());
|
assertEquals("超级公仔", view.getRewardGiftName());
|
||||||
verify(blindBoxPoolMapper).updateById(existing);
|
verify(blindBoxPoolMapper).updateById(existing);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldAllowUpdateWhenGiftInactiveOrHistorical() {
|
||||||
|
BlindBoxConfigEntity config = new BlindBoxConfigEntity();
|
||||||
|
config.setId("blind-1");
|
||||||
|
config.setTenantId("tenant-1");
|
||||||
|
when(blindBoxConfigService.requireById("blind-1")).thenReturn(config);
|
||||||
|
|
||||||
|
PlayGiftInfoEntity inactiveGift = new PlayGiftInfoEntity();
|
||||||
|
inactiveGift.setId("gift-offline");
|
||||||
|
inactiveGift.setTenantId("tenant-1");
|
||||||
|
inactiveGift.setHistory("1");
|
||||||
|
inactiveGift.setState("1");
|
||||||
|
inactiveGift.setType("1");
|
||||||
|
inactiveGift.setPrice(BigDecimal.valueOf(66.6));
|
||||||
|
inactiveGift.setName("下架礼物");
|
||||||
|
when(playGiftInfoMapper.selectById("gift-offline")).thenReturn(inactiveGift);
|
||||||
|
|
||||||
|
BlindBoxPoolEntity existing = new BlindBoxPoolEntity();
|
||||||
|
existing.setId(500L);
|
||||||
|
existing.setTenantId("tenant-1");
|
||||||
|
existing.setBlindBoxId("blind-1");
|
||||||
|
existing.setRewardGiftId("gift-on");
|
||||||
|
existing.setStatus(1);
|
||||||
|
when(blindBoxPoolMapper.selectById(500L)).thenReturn(existing);
|
||||||
|
when(blindBoxPoolMapper.updateById(existing)).thenReturn(1);
|
||||||
|
|
||||||
|
BlindBoxPoolUpsertRequest request = new BlindBoxPoolUpsertRequest();
|
||||||
|
request.setBlindBoxId("blind-1");
|
||||||
|
request.setRewardGiftId("gift-offline");
|
||||||
|
request.setWeight(10);
|
||||||
|
request.setStatus(1);
|
||||||
|
|
||||||
|
blindBoxPoolAdminService.update(500L, request);
|
||||||
|
|
||||||
|
verify(blindBoxPoolMapper).updateById(existing);
|
||||||
|
assertEquals("gift-offline", existing.getRewardGiftId());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldRejectCreateWhenGiftInactive() {
|
||||||
|
BlindBoxConfigEntity config = new BlindBoxConfigEntity();
|
||||||
|
config.setId("blind-1");
|
||||||
|
config.setTenantId("tenant-1");
|
||||||
|
when(blindBoxConfigService.requireById("blind-1")).thenReturn(config);
|
||||||
|
|
||||||
|
PlayGiftInfoEntity inactiveGift = new PlayGiftInfoEntity();
|
||||||
|
inactiveGift.setId("gift-off");
|
||||||
|
inactiveGift.setTenantId("tenant-1");
|
||||||
|
inactiveGift.setHistory("1");
|
||||||
|
inactiveGift.setState("1");
|
||||||
|
inactiveGift.setType("1");
|
||||||
|
when(playGiftInfoMapper.selectById("gift-off")).thenReturn(inactiveGift);
|
||||||
|
|
||||||
|
BlindBoxPoolUpsertRequest request = new BlindBoxPoolUpsertRequest();
|
||||||
|
request.setBlindBoxId("blind-1");
|
||||||
|
request.setRewardGiftId("gift-off");
|
||||||
|
request.setWeight(10);
|
||||||
|
|
||||||
|
CustomException ex = assertThrows(CustomException.class,
|
||||||
|
() -> blindBoxPoolAdminService.create("blind-1", request));
|
||||||
|
assertTrue(ex.getMessage().contains("中奖礼物不存在或已下架"));
|
||||||
|
verify(blindBoxPoolMapper, times(0)).insert(any());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -171,6 +171,21 @@ class BlindBoxServiceTest {
|
|||||||
verify(rewardMapper, times(0)).markUsed(any(), any(), any(), any());
|
verify(rewardMapper, times(0)).markUsed(any(), any(), any(), any());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldRejectWhenNoEligibleRewardExists() {
|
||||||
|
BlindBoxConfigEntity config = new BlindBoxConfigEntity();
|
||||||
|
config.setId("blind-1");
|
||||||
|
config.setTenantId("tenant-1");
|
||||||
|
when(configService.requireById("blind-1")).thenReturn(config);
|
||||||
|
when(poolMapper.listActiveEntries(eq("tenant-1"), eq("blind-1"), any(LocalDateTime.class)))
|
||||||
|
.thenReturn(Collections.emptyList());
|
||||||
|
|
||||||
|
CustomException ex = assertThrows(CustomException.class,
|
||||||
|
() -> blindBoxService.drawReward("tenant-1", "order-404", "customer-9", "blind-1", "seed-out"));
|
||||||
|
assertTrue(ex.getMessage().contains("奖池暂无可用奖励"));
|
||||||
|
verify(inventoryService, times(0)).reserveRewardStock(any(), any(), any());
|
||||||
|
}
|
||||||
|
|
||||||
private BlindBoxRewardEntity buildRewardEntity() {
|
private BlindBoxRewardEntity buildRewardEntity() {
|
||||||
BlindBoxRewardEntity reward = new BlindBoxRewardEntity();
|
BlindBoxRewardEntity reward = new BlindBoxRewardEntity();
|
||||||
reward.setId("reward-1");
|
reward.setId("reward-1");
|
||||||
|
|||||||
@@ -0,0 +1,235 @@
|
|||||||
|
package com.starry.admin.modules.clerk.service.impl;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
|
import static org.mockito.Mockito.doReturn;
|
||||||
|
import static org.mockito.Mockito.spy;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||||
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||||
|
import com.starry.admin.common.exception.CustomException;
|
||||||
|
import com.starry.admin.modules.clerk.enums.ClerkMediaUsage;
|
||||||
|
import com.starry.admin.modules.clerk.module.entity.PlayClerkDataReviewInfoEntity;
|
||||||
|
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.enums.ClerkDataType;
|
||||||
|
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.IPlayClerkMediaAssetService;
|
||||||
|
import com.starry.admin.modules.clerk.service.IPlayClerkUserInfoService;
|
||||||
|
import com.starry.admin.modules.media.entity.PlayMediaEntity;
|
||||||
|
import com.starry.admin.modules.media.service.IPlayMediaService;
|
||||||
|
import com.starry.common.enums.ClerkReviewState;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.ArgumentCaptor;
|
||||||
|
import org.mockito.InjectMocks;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
import org.springframework.test.util.ReflectionTestUtils;
|
||||||
|
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
class PlayClerkDataReviewInfoServiceImplTest {
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private IPlayClerkUserInfoService clerkUserInfoService;
|
||||||
|
@Mock
|
||||||
|
private IPlayClerkMediaAssetService clerkMediaAssetService;
|
||||||
|
@Mock
|
||||||
|
private IPlayMediaService mediaService;
|
||||||
|
|
||||||
|
private PlayClerkDataReviewInfoServiceImpl service;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
service = spy(new PlayClerkDataReviewInfoServiceImpl());
|
||||||
|
ReflectionTestUtils.setField(service, "playClerkUserInfoService", clerkUserInfoService);
|
||||||
|
ReflectionTestUtils.setField(service, "clerkMediaAssetService", clerkMediaAssetService);
|
||||||
|
ReflectionTestUtils.setField(service, "mediaService", mediaService);
|
||||||
|
org.mockito.Mockito.lenient()
|
||||||
|
.doReturn(true)
|
||||||
|
.when(service)
|
||||||
|
.update(any(PlayClerkDataReviewInfoEntity.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void updateDataReviewStateShouldSynchronizeAlbumOnApproval() {
|
||||||
|
PlayClerkDataReviewInfoEntity review = buildReview("review-1", "clerk-1", "2",
|
||||||
|
Arrays.asList("media-existing", "media-new"));
|
||||||
|
doReturn(review).when(service).selectPlayClerkDataReviewInfoById("review-1");
|
||||||
|
|
||||||
|
PlayClerkUserInfoEntity clerk = new PlayClerkUserInfoEntity();
|
||||||
|
clerk.setId("clerk-1");
|
||||||
|
clerk.setTenantId("tenant-1");
|
||||||
|
when(clerkUserInfoService.getById("clerk-1")).thenReturn(clerk);
|
||||||
|
|
||||||
|
PlayMediaEntity existingMedia = new PlayMediaEntity();
|
||||||
|
existingMedia.setId("media-existing");
|
||||||
|
when(mediaService.getById("media-existing")).thenReturn(existingMedia);
|
||||||
|
PlayMediaEntity newMedia = new PlayMediaEntity();
|
||||||
|
newMedia.setId("media-new");
|
||||||
|
when(mediaService.getById("media-new")).thenReturn(newMedia);
|
||||||
|
|
||||||
|
PlayClerkMediaAssetEntity assetStub = new PlayClerkMediaAssetEntity();
|
||||||
|
assetStub.setId("asset-1");
|
||||||
|
when(clerkMediaAssetService.linkDraftAsset(any(), any(), any(), any())).thenReturn(assetStub);
|
||||||
|
|
||||||
|
PlayClerkDataReviewStateEditVo vo = new PlayClerkDataReviewStateEditVo();
|
||||||
|
vo.setId("review-1");
|
||||||
|
vo.setReviewState(ClerkReviewState.APPROVED);
|
||||||
|
vo.setReviewCon("ok");
|
||||||
|
|
||||||
|
service.updateDataReviewState(vo);
|
||||||
|
|
||||||
|
verify(clerkMediaAssetService).linkDraftAsset(eq("tenant-1"), eq("clerk-1"), eq("media-existing"),
|
||||||
|
eq(ClerkMediaUsage.PROFILE));
|
||||||
|
verify(clerkMediaAssetService).linkDraftAsset(eq("tenant-1"), eq("clerk-1"), eq("media-new"), eq(ClerkMediaUsage.PROFILE));
|
||||||
|
verify(clerkMediaAssetService).applyReviewDecision(eq("clerk-1"), eq(ClerkMediaUsage.PROFILE),
|
||||||
|
eq(Arrays.asList("media-existing", "media-new")), eq("review-1"), eq("ok"));
|
||||||
|
verify(clerkUserInfoService).update(any(PlayClerkUserInfoEntity.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void updateDataReviewStateThrowsWhenClerkMissing() {
|
||||||
|
PlayClerkDataReviewInfoEntity review = buildReview("review-2", "ghost", "2",
|
||||||
|
Collections.singletonList("media"));
|
||||||
|
doReturn(review).when(service).selectPlayClerkDataReviewInfoById("review-2");
|
||||||
|
when(clerkUserInfoService.getById("ghost")).thenReturn(null);
|
||||||
|
|
||||||
|
PlayClerkDataReviewStateEditVo vo = new PlayClerkDataReviewStateEditVo();
|
||||||
|
vo.setId("review-2");
|
||||||
|
vo.setReviewState(ClerkReviewState.APPROVED);
|
||||||
|
|
||||||
|
assertThatThrownBy(() -> service.updateDataReviewState(vo))
|
||||||
|
.isInstanceOf(CustomException.class)
|
||||||
|
.hasMessageContaining("店员信息不存在");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void enrichDataContentWithMediaPreviewPopulatesCoverAndVideoUrls() {
|
||||||
|
PlayClerkDataReviewInfoServiceImpl impl = service;
|
||||||
|
|
||||||
|
PlayClerkDataReviewReturnVo videoRow = new PlayClerkDataReviewReturnVo();
|
||||||
|
videoRow.setId("row-video");
|
||||||
|
videoRow.setDataTypeEnum(ClerkDataType.PHOTO_ALBUM);
|
||||||
|
videoRow.setDataContent(java.util.Collections.singletonList("media-video"));
|
||||||
|
|
||||||
|
PlayClerkDataReviewReturnVo imageRow = new PlayClerkDataReviewReturnVo();
|
||||||
|
imageRow.setId("row-image");
|
||||||
|
imageRow.setDataTypeEnum(ClerkDataType.PHOTO_ALBUM);
|
||||||
|
imageRow.setDataContent(java.util.Collections.singletonList("media-image"));
|
||||||
|
|
||||||
|
IPage<PlayClerkDataReviewReturnVo> page = new Page<>(1, 10);
|
||||||
|
page.setRecords(java.util.Arrays.asList(videoRow, imageRow));
|
||||||
|
|
||||||
|
PlayMediaEntity videoMedia = new PlayMediaEntity();
|
||||||
|
videoMedia.setId("media-video");
|
||||||
|
videoMedia.setKind(com.starry.admin.modules.media.enums.MediaKind.VIDEO.getCode());
|
||||||
|
videoMedia.setUrl("https://oss/video.mp4");
|
||||||
|
videoMedia.setCoverUrl("https://oss/video-cover.jpg");
|
||||||
|
|
||||||
|
PlayMediaEntity imageMedia = new PlayMediaEntity();
|
||||||
|
imageMedia.setId("media-image");
|
||||||
|
imageMedia.setKind(com.starry.admin.modules.media.enums.MediaKind.IMAGE.getCode());
|
||||||
|
imageMedia.setUrl("https://oss/image.png");
|
||||||
|
|
||||||
|
when(mediaService.getById("media-video")).thenReturn(videoMedia);
|
||||||
|
when(mediaService.getById("media-image")).thenReturn(imageMedia);
|
||||||
|
|
||||||
|
org.springframework.test.util.ReflectionTestUtils.invokeMethod(
|
||||||
|
impl, "enrichDataContentWithMediaPreview", page);
|
||||||
|
|
||||||
|
assertThat(videoRow.getDataContent()).containsExactly("https://oss/video-cover.jpg");
|
||||||
|
assertThat(videoRow.getMediaVideoUrls()).containsExactly("https://oss/video.mp4");
|
||||||
|
|
||||||
|
assertThat(imageRow.getDataContent()).containsExactly("https://oss/image.png");
|
||||||
|
assertThat(imageRow.getMediaVideoUrls()).containsExactly((String) null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void enrichDataContentWithMediaPreviewFallsBackToLegacyUrlWhenMediaMissing() {
|
||||||
|
PlayClerkDataReviewInfoServiceImpl impl = service;
|
||||||
|
|
||||||
|
String legacyUrl = "https://oss/legacy-only.png";
|
||||||
|
PlayClerkDataReviewReturnVo row = new PlayClerkDataReviewReturnVo();
|
||||||
|
row.setId("row-legacy");
|
||||||
|
row.setDataTypeEnum(ClerkDataType.PHOTO_ALBUM);
|
||||||
|
row.setDataContent(java.util.Collections.singletonList(legacyUrl));
|
||||||
|
|
||||||
|
IPage<PlayClerkDataReviewReturnVo> page = new Page<>(1, 10);
|
||||||
|
page.setRecords(java.util.Collections.singletonList(row));
|
||||||
|
|
||||||
|
// 当无法通过 mediaId 查询到记录时,应当保持 dataContent 为原始 URL,
|
||||||
|
// 并且对应的 mediaVideoUrls 位置为 null。
|
||||||
|
when(mediaService.getById(legacyUrl)).thenReturn(null);
|
||||||
|
|
||||||
|
org.springframework.test.util.ReflectionTestUtils.invokeMethod(
|
||||||
|
impl, "enrichDataContentWithMediaPreview", page);
|
||||||
|
|
||||||
|
assertThat(row.getDataContent()).containsExactly(legacyUrl);
|
||||||
|
assertThat(row.getMediaVideoUrls()).containsExactly((String) null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void updateDataReviewStateUpdatesAvatarOnApproval() {
|
||||||
|
PlayClerkDataReviewInfoEntity review = buildReview("review-avatar", "clerk-1", "1",
|
||||||
|
java.util.Collections.singletonList("https://oss/avatar.png"));
|
||||||
|
// 审核服务内部通过 selectPlayClerkDataReviewInfoById 查询审核记录
|
||||||
|
org.mockito.Mockito.doReturn(review).when(service).selectPlayClerkDataReviewInfoById("review-avatar");
|
||||||
|
|
||||||
|
PlayClerkDataReviewStateEditVo vo = new PlayClerkDataReviewStateEditVo();
|
||||||
|
vo.setId("review-avatar");
|
||||||
|
vo.setReviewState(ClerkReviewState.APPROVED);
|
||||||
|
|
||||||
|
service.updateDataReviewState(vo);
|
||||||
|
|
||||||
|
ArgumentCaptor<PlayClerkUserInfoEntity> captor = ArgumentCaptor.forClass(PlayClerkUserInfoEntity.class);
|
||||||
|
verify(clerkUserInfoService).update(captor.capture());
|
||||||
|
PlayClerkUserInfoEntity updated = captor.getValue();
|
||||||
|
assertThat(updated.getId()).isEqualTo("clerk-1");
|
||||||
|
assertThat(updated.getAvatar()).isEqualTo("https://oss/avatar.png");
|
||||||
|
// 其他字段不应被误改
|
||||||
|
assertThat(updated.getNickname()).isNull();
|
||||||
|
assertThat(updated.getAudio()).isNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void updateDataReviewStateUpdatesAudioOnApproval() {
|
||||||
|
PlayClerkDataReviewInfoEntity review = buildReview("review-audio", "clerk-2", "3",
|
||||||
|
java.util.Collections.singletonList("https://oss/audio.m4a"));
|
||||||
|
org.mockito.Mockito.doReturn(review).when(service).selectPlayClerkDataReviewInfoById("review-audio");
|
||||||
|
|
||||||
|
PlayClerkDataReviewStateEditVo vo = new PlayClerkDataReviewStateEditVo();
|
||||||
|
vo.setId("review-audio");
|
||||||
|
vo.setReviewState(ClerkReviewState.APPROVED);
|
||||||
|
|
||||||
|
service.updateDataReviewState(vo);
|
||||||
|
|
||||||
|
ArgumentCaptor<PlayClerkUserInfoEntity> captor = ArgumentCaptor.forClass(PlayClerkUserInfoEntity.class);
|
||||||
|
verify(clerkUserInfoService, org.mockito.Mockito.atLeastOnce()).update(captor.capture());
|
||||||
|
PlayClerkUserInfoEntity updated = captor.getValue();
|
||||||
|
assertThat(updated.getId()).isEqualTo("clerk-2");
|
||||||
|
assertThat(updated.getAudio()).isEqualTo("https://oss/audio.m4a");
|
||||||
|
assertThat(updated.getNickname()).isNull();
|
||||||
|
assertThat(updated.getAvatar()).isNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
private PlayClerkDataReviewInfoEntity buildReview(String id, String clerkId, String dataType,
|
||||||
|
java.util.List<String> payload) {
|
||||||
|
PlayClerkDataReviewInfoEntity entity = new PlayClerkDataReviewInfoEntity();
|
||||||
|
entity.setId(id);
|
||||||
|
entity.setClerkId(clerkId);
|
||||||
|
entity.setDataType(dataType);
|
||||||
|
entity.setDataContent(payload);
|
||||||
|
entity.setAddTime(LocalDateTime.now());
|
||||||
|
return entity;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
package com.starry.admin.modules.clerk.service.impl;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.Mockito.doReturn;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
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 com.starry.admin.modules.media.entity.PlayMediaEntity;
|
||||||
|
import com.starry.admin.modules.media.service.IPlayMediaService;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
import org.springframework.test.util.ReflectionTestUtils;
|
||||||
|
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
class PlayClerkMediaAssetServiceImplTest {
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private IPlayMediaService mediaService;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void submitWithOrderShouldReindexAndFlagPending() {
|
||||||
|
PlayClerkMediaAssetServiceImpl service = org.mockito.Mockito.spy(new PlayClerkMediaAssetServiceImpl());
|
||||||
|
ReflectionTestUtils.setField(service, "mediaService", mediaService);
|
||||||
|
List<PlayClerkMediaAssetEntity> assets = Arrays.asList(
|
||||||
|
buildAsset("A", "media-a", 0),
|
||||||
|
buildAsset("B", "media-b", 1),
|
||||||
|
buildAsset("C", "media-c", 2));
|
||||||
|
|
||||||
|
doReturn(assets).when(service).listActiveByUsage("clerk-1", ClerkMediaUsage.PROFILE);
|
||||||
|
|
||||||
|
java.util.List<java.util.List<PlayClerkMediaAssetEntity>> batches = new java.util.ArrayList<>();
|
||||||
|
org.mockito.Mockito.doAnswer(inv -> {
|
||||||
|
java.util.Collection<PlayClerkMediaAssetEntity> collection = inv.getArgument(0);
|
||||||
|
batches.add(new java.util.ArrayList<>(collection));
|
||||||
|
return true;
|
||||||
|
}).when(service).updateBatchById(any());
|
||||||
|
|
||||||
|
service.submitWithOrder("clerk-1", ClerkMediaUsage.PROFILE, Arrays.asList("media-b", "media-a"));
|
||||||
|
|
||||||
|
List<PlayClerkMediaAssetEntity> updates = flatten(batches);
|
||||||
|
PlayClerkMediaAssetEntity b = find(updates, "B");
|
||||||
|
PlayClerkMediaAssetEntity a = find(updates, "A");
|
||||||
|
PlayClerkMediaAssetEntity c = find(updates, "C");
|
||||||
|
|
||||||
|
assertThat(b.getOrderIndex()).isEqualTo(0);
|
||||||
|
assertThat(b.getReviewState()).isEqualTo(ClerkMediaReviewState.PENDING.getCode());
|
||||||
|
assertThat(a.getOrderIndex()).isEqualTo(1);
|
||||||
|
assertThat(a.getReviewState()).isEqualTo(ClerkMediaReviewState.PENDING.getCode());
|
||||||
|
assertThat(c.getReviewState()).isEqualTo(ClerkMediaReviewState.REJECTED.getCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void applyReviewDecisionShouldApproveAndReject() {
|
||||||
|
PlayClerkMediaAssetServiceImpl service = org.mockito.Mockito.spy(new PlayClerkMediaAssetServiceImpl());
|
||||||
|
ReflectionTestUtils.setField(service, "mediaService", mediaService);
|
||||||
|
List<PlayClerkMediaAssetEntity> assets = Arrays.asList(
|
||||||
|
buildAsset("A", "media-1", 0),
|
||||||
|
buildAsset("B", "media-2", 1));
|
||||||
|
|
||||||
|
doReturn(assets).when(service).listActiveByUsage("clerk-1", ClerkMediaUsage.PROFILE);
|
||||||
|
when(mediaService.listByIds(any())).thenReturn(Arrays.asList(
|
||||||
|
buildMedia("media-1"),
|
||||||
|
buildMedia("media-2")));
|
||||||
|
|
||||||
|
java.util.List<java.util.List<PlayClerkMediaAssetEntity>> batches = new java.util.ArrayList<>();
|
||||||
|
org.mockito.Mockito.doAnswer(inv -> {
|
||||||
|
java.util.Collection<PlayClerkMediaAssetEntity> collection = inv.getArgument(0);
|
||||||
|
batches.add(new java.util.ArrayList<>(collection));
|
||||||
|
return true;
|
||||||
|
}).when(service).updateBatchById(any());
|
||||||
|
|
||||||
|
service.applyReviewDecision("clerk-1", ClerkMediaUsage.PROFILE, Arrays.asList("media-2"), "review-1", "ok");
|
||||||
|
|
||||||
|
List<PlayClerkMediaAssetEntity> updates = flatten(batches);
|
||||||
|
PlayClerkMediaAssetEntity approved = find(updates, "B");
|
||||||
|
PlayClerkMediaAssetEntity rejected = find(updates, "A");
|
||||||
|
|
||||||
|
assertThat(approved.getReviewState()).isEqualTo(ClerkMediaReviewState.APPROVED.getCode());
|
||||||
|
assertThat(approved.getOrderIndex()).isEqualTo(0);
|
||||||
|
assertThat(approved.getReviewRecordId()).isEqualTo("review-1");
|
||||||
|
assertThat(rejected.getReviewState()).isEqualTo(ClerkMediaReviewState.REJECTED.getCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static PlayClerkMediaAssetEntity buildAsset(String id, String mediaId, int order) {
|
||||||
|
PlayClerkMediaAssetEntity entity = new PlayClerkMediaAssetEntity();
|
||||||
|
entity.setId(id);
|
||||||
|
entity.setClerkId("clerk-1");
|
||||||
|
entity.setTenantId("tenant-1");
|
||||||
|
entity.setMediaId(mediaId);
|
||||||
|
entity.setUsage(ClerkMediaUsage.PROFILE.getCode());
|
||||||
|
entity.setReviewState(ClerkMediaReviewState.DRAFT.getCode());
|
||||||
|
entity.setOrderIndex(order);
|
||||||
|
entity.setSubmittedTime(LocalDateTime.now());
|
||||||
|
return entity;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static PlayMediaEntity buildMedia(String id) {
|
||||||
|
PlayMediaEntity media = new PlayMediaEntity();
|
||||||
|
media.setId(id);
|
||||||
|
media.setUrl("url-" + id);
|
||||||
|
return media;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static PlayClerkMediaAssetEntity find(List<PlayClerkMediaAssetEntity> updates, String id) {
|
||||||
|
return updates.stream()
|
||||||
|
.filter(asset -> id.equals(asset.getId()))
|
||||||
|
.findFirst()
|
||||||
|
.orElseThrow();
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private static List<PlayClerkMediaAssetEntity> flatten(List<? extends List<PlayClerkMediaAssetEntity>> captured) {
|
||||||
|
return captured.stream()
|
||||||
|
.flatMap(List::stream)
|
||||||
|
.collect(Collectors.toCollection(ArrayList::new));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
package com.starry.admin.modules.clerk.service.impl;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
import com.starry.admin.modules.weichat.entity.clerk.MediaVo;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
class PlayClerkUserInfoServiceImplTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void mergeLegacyAlbumAppendsUniqueUrls() {
|
||||||
|
List<String> legacy = Arrays.asList("https://oss/1.png", " ", "https://oss/2.png", "https://oss/1.png");
|
||||||
|
List<MediaVo> destination = new ArrayList<>();
|
||||||
|
MediaVo existing = new MediaVo();
|
||||||
|
existing.setUrl("https://oss/2.png");
|
||||||
|
destination.add(existing);
|
||||||
|
|
||||||
|
List<MediaVo> merged = PlayClerkUserInfoServiceImpl.mergeLegacyAlbum(legacy, destination);
|
||||||
|
|
||||||
|
assertThat(merged).hasSize(2);
|
||||||
|
assertThat(merged.stream().map(MediaVo::getUrl))
|
||||||
|
.containsExactlyInAnyOrder("https://oss/2.png", "https://oss/1.png");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void mergeLegacyAlbumFillsUsageStatusAndReviewStateForLegacyEntries() {
|
||||||
|
List<String> legacy = Arrays.asList("https://oss/legacy-only.png");
|
||||||
|
List<MediaVo> destination = new ArrayList<>();
|
||||||
|
|
||||||
|
List<MediaVo> merged = PlayClerkUserInfoServiceImpl.mergeLegacyAlbum(legacy, destination);
|
||||||
|
|
||||||
|
assertThat(merged).hasSize(1);
|
||||||
|
MediaVo legacyVo = merged.get(0);
|
||||||
|
assertThat(legacyVo.getUrl()).isEqualTo("https://oss/legacy-only.png");
|
||||||
|
assertThat(legacyVo.getId())
|
||||||
|
.as("legacy media id should fallback to url for compatibility")
|
||||||
|
.isEqualTo("https://oss/legacy-only.png");
|
||||||
|
assertThat(legacyVo.getUsage()).isEqualTo(com.starry.admin.modules.clerk.enums.ClerkMediaUsage.PROFILE.getCode());
|
||||||
|
assertThat(legacyVo.getStatus()).isEqualTo(com.starry.admin.modules.media.enums.MediaStatus.READY.getCode());
|
||||||
|
assertThat(legacyVo.getReviewState())
|
||||||
|
.isEqualTo(com.starry.admin.modules.clerk.enums.ClerkMediaReviewState.APPROVED.getCode());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
package com.starry.admin.modules.order.listener;
|
||||||
|
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.verifyNoInteractions;
|
||||||
|
|
||||||
|
import com.starry.admin.modules.order.module.dto.OrderRevocationContext;
|
||||||
|
import com.starry.admin.modules.order.module.entity.PlayOrderInfoEntity;
|
||||||
|
import com.starry.admin.modules.order.module.event.OrderRevocationEvent;
|
||||||
|
import com.starry.admin.modules.withdraw.service.IEarningsService;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
class OrderRevocationEarningsListenerTest {
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private IEarningsService earningsService;
|
||||||
|
|
||||||
|
private OrderRevocationEarningsListener listener;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
listener = new OrderRevocationEarningsListener(earningsService);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void handle_deductCreatesCounterLineUsingOrderClerk() {
|
||||||
|
OrderRevocationContext context = new OrderRevocationContext();
|
||||||
|
context.setOrderId("order-reverse-2");
|
||||||
|
context.setOperatorId("admin-reviewer");
|
||||||
|
context.setDeductClerkEarnings(true);
|
||||||
|
context.setEarningsAdjustAmount(BigDecimal.valueOf(25));
|
||||||
|
|
||||||
|
PlayOrderInfoEntity order = new PlayOrderInfoEntity();
|
||||||
|
order.setId("order-reverse-2");
|
||||||
|
order.setTenantId("tenant-x");
|
||||||
|
order.setAcceptBy("clerk-special");
|
||||||
|
|
||||||
|
listener.handle(new OrderRevocationEvent(context, order));
|
||||||
|
|
||||||
|
verify(earningsService)
|
||||||
|
.createCounterLine("order-reverse-2", "tenant-x", "clerk-special", BigDecimal.valueOf(25), "admin-reviewer");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void handle_deductFallsBackToEstimatedWhenAmountMissing() {
|
||||||
|
OrderRevocationContext context = new OrderRevocationContext();
|
||||||
|
context.setOrderId("order-reverse-3");
|
||||||
|
context.setOperatorId("admin-reviewer");
|
||||||
|
context.setDeductClerkEarnings(true);
|
||||||
|
|
||||||
|
PlayOrderInfoEntity order = new PlayOrderInfoEntity();
|
||||||
|
order.setId("order-reverse-3");
|
||||||
|
order.setTenantId("tenant-y");
|
||||||
|
order.setAcceptBy("clerk-owner");
|
||||||
|
order.setEstimatedRevenue(BigDecimal.valueOf(52));
|
||||||
|
|
||||||
|
listener.handle(new OrderRevocationEvent(context, order));
|
||||||
|
|
||||||
|
verify(earningsService)
|
||||||
|
.createCounterLine("order-reverse-3", "tenant-y", "clerk-owner", BigDecimal.valueOf(52), "admin-reviewer");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void handle_disabledDeductIsNoOp() {
|
||||||
|
OrderRevocationContext context = new OrderRevocationContext();
|
||||||
|
context.setOrderId("order-none-3");
|
||||||
|
context.setOperatorId("admin-noop");
|
||||||
|
context.setDeductClerkEarnings(false);
|
||||||
|
|
||||||
|
PlayOrderInfoEntity order = new PlayOrderInfoEntity();
|
||||||
|
order.setId("order-none-3");
|
||||||
|
|
||||||
|
listener.handle(new OrderRevocationEvent(context, order));
|
||||||
|
|
||||||
|
verifyNoInteractions(earningsService);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
package com.starry.admin.modules.order.service.impl;
|
package com.starry.admin.modules.order.service.impl;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
import static org.mockito.ArgumentMatchers.any;
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
import static org.mockito.ArgumentMatchers.anyInt;
|
import static org.mockito.ArgumentMatchers.anyInt;
|
||||||
import static org.mockito.ArgumentMatchers.anyList;
|
import static org.mockito.ArgumentMatchers.anyList;
|
||||||
@@ -17,6 +19,7 @@ import com.starry.admin.modules.custom.module.entity.PlayCustomUserInfoEntity;
|
|||||||
import com.starry.admin.modules.custom.service.IPlayCustomUserInfoService;
|
import com.starry.admin.modules.custom.service.IPlayCustomUserInfoService;
|
||||||
import com.starry.admin.modules.order.mapper.PlayOrderInfoMapper;
|
import com.starry.admin.modules.order.mapper.PlayOrderInfoMapper;
|
||||||
import com.starry.admin.modules.order.mapper.PlayOrderLogInfoMapper;
|
import com.starry.admin.modules.order.mapper.PlayOrderLogInfoMapper;
|
||||||
|
import com.starry.admin.modules.order.module.constant.OrderConstant;
|
||||||
import com.starry.admin.modules.order.module.constant.OrderConstant.BalanceOperationType;
|
import com.starry.admin.modules.order.module.constant.OrderConstant.BalanceOperationType;
|
||||||
import com.starry.admin.modules.order.module.constant.OrderConstant.CommodityType;
|
import com.starry.admin.modules.order.module.constant.OrderConstant.CommodityType;
|
||||||
import com.starry.admin.modules.order.module.constant.OrderConstant.Gender;
|
import com.starry.admin.modules.order.module.constant.OrderConstant.Gender;
|
||||||
@@ -40,9 +43,11 @@ 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.OrderPlacementCommand;
|
||||||
import com.starry.admin.modules.order.module.dto.OrderPlacementResult;
|
import com.starry.admin.modules.order.module.dto.OrderPlacementResult;
|
||||||
import com.starry.admin.modules.order.module.dto.OrderRefundContext;
|
import com.starry.admin.modules.order.module.dto.OrderRefundContext;
|
||||||
|
import com.starry.admin.modules.order.module.dto.OrderRevocationContext;
|
||||||
import com.starry.admin.modules.order.module.dto.PaymentInfo;
|
import com.starry.admin.modules.order.module.dto.PaymentInfo;
|
||||||
import com.starry.admin.modules.order.module.dto.RandomOrderRequirements;
|
import com.starry.admin.modules.order.module.dto.RandomOrderRequirements;
|
||||||
import com.starry.admin.modules.order.module.entity.PlayOrderInfoEntity;
|
import com.starry.admin.modules.order.module.entity.PlayOrderInfoEntity;
|
||||||
|
import com.starry.admin.modules.order.module.event.OrderRevocationEvent;
|
||||||
import com.starry.admin.modules.order.module.vo.ClerkEstimatedRevenueVo;
|
import com.starry.admin.modules.order.module.vo.ClerkEstimatedRevenueVo;
|
||||||
import com.starry.admin.modules.order.service.IPlayOrderRefundInfoService;
|
import com.starry.admin.modules.order.service.IPlayOrderRefundInfoService;
|
||||||
import com.starry.admin.modules.order.service.support.ClerkRevenueCalculator;
|
import com.starry.admin.modules.order.service.support.ClerkRevenueCalculator;
|
||||||
@@ -65,10 +70,12 @@ import java.util.UUID;
|
|||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.junit.jupiter.api.extension.ExtendWith;
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.ArgumentCaptor;
|
||||||
import org.mockito.InjectMocks;
|
import org.mockito.InjectMocks;
|
||||||
import org.mockito.Mock;
|
import org.mockito.Mock;
|
||||||
import org.mockito.Mockito;
|
import org.mockito.Mockito;
|
||||||
import org.mockito.junit.jupiter.MockitoExtension;
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
import org.springframework.context.ApplicationEventPublisher;
|
||||||
|
|
||||||
@ExtendWith(MockitoExtension.class)
|
@ExtendWith(MockitoExtension.class)
|
||||||
class OrderLifecycleServiceImplTest {
|
class OrderLifecycleServiceImplTest {
|
||||||
@@ -106,11 +113,173 @@ class OrderLifecycleServiceImplTest {
|
|||||||
@Mock
|
@Mock
|
||||||
private IPlayBalanceDetailsInfoService playBalanceDetailsInfoService;
|
private IPlayBalanceDetailsInfoService playBalanceDetailsInfoService;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private ApplicationEventPublisher applicationEventPublisher;
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
void initStrategies() {
|
void initStrategies() {
|
||||||
lifecycleService.initPlacementStrategies();
|
lifecycleService.initPlacementStrategies();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void revokeCompletedOrder_updatesStatusAndPublishesEvent() {
|
||||||
|
String orderId = UUID.randomUUID().toString();
|
||||||
|
PlayOrderInfoEntity completed = buildOrder(orderId, OrderStatus.COMPLETED.getCode());
|
||||||
|
completed.setFinalAmount(BigDecimal.valueOf(188));
|
||||||
|
completed.setRefundType(OrderRefundFlag.NOT_REFUNDED.getCode());
|
||||||
|
PlayOrderInfoEntity revoked = buildOrder(orderId, OrderStatus.REVOKED.getCode());
|
||||||
|
revoked.setFinalAmount(BigDecimal.valueOf(188));
|
||||||
|
|
||||||
|
when(orderInfoMapper.selectById(orderId)).thenReturn(completed, revoked);
|
||||||
|
lenient().when(orderInfoMapper.update(isNull(), any())).thenReturn(1);
|
||||||
|
when(earningsService.getRemainingEarningsForOrder(orderId, completed.getAcceptBy()))
|
||||||
|
.thenReturn(BigDecimal.TEN);
|
||||||
|
when(playBalanceDetailsInfoService.existsCustomerConsumeRecord(completed.getPurchaserBy(), orderId))
|
||||||
|
.thenReturn(true);
|
||||||
|
when(earningsService.getRemainingEarningsForOrder(orderId, completed.getAcceptBy()))
|
||||||
|
.thenReturn(BigDecimal.valueOf(60));
|
||||||
|
|
||||||
|
OrderRevocationContext context = new OrderRevocationContext();
|
||||||
|
context.setOrderId(orderId);
|
||||||
|
context.setOperatorId("admin-8");
|
||||||
|
context.setOperatorType(OperatorType.ADMIN.getCode());
|
||||||
|
context.setRefundAmount(BigDecimal.valueOf(88));
|
||||||
|
context.setRefundReason("客户投诉");
|
||||||
|
context.setRefundToCustomer(true);
|
||||||
|
context.setDeductClerkEarnings(true);
|
||||||
|
|
||||||
|
lifecycleService.revokeCompletedOrder(context);
|
||||||
|
|
||||||
|
verify(orderInfoMapper).update(isNull(), any());
|
||||||
|
verify(orderLogInfoMapper).insert(any());
|
||||||
|
ArgumentCaptor<OrderRevocationEvent> captor = ArgumentCaptor.forClass(OrderRevocationEvent.class);
|
||||||
|
verify(applicationEventPublisher).publishEvent(captor.capture());
|
||||||
|
OrderRevocationEvent event = captor.getValue();
|
||||||
|
assertEquals(orderId, event.getContext().getOrderId());
|
||||||
|
assertTrue(event.getContext().isDeductClerkEarnings());
|
||||||
|
assertEquals(BigDecimal.valueOf(88), event.getContext().getRefundAmount());
|
||||||
|
assertEquals(BigDecimal.valueOf(60), event.getContext().getEarningsAdjustAmount());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void revokeCompletedOrder_defersBalanceCreditToListener() {
|
||||||
|
String orderId = UUID.randomUUID().toString();
|
||||||
|
PlayOrderInfoEntity completed = buildOrder(orderId, OrderStatus.COMPLETED.getCode());
|
||||||
|
completed.setFinalAmount(BigDecimal.valueOf(208));
|
||||||
|
completed.setRefundType(OrderRefundFlag.NOT_REFUNDED.getCode());
|
||||||
|
PlayOrderInfoEntity revoked = buildOrder(orderId, OrderStatus.REVOKED.getCode());
|
||||||
|
revoked.setFinalAmount(BigDecimal.valueOf(208));
|
||||||
|
|
||||||
|
when(orderInfoMapper.selectById(orderId)).thenReturn(completed, revoked);
|
||||||
|
lenient().when(orderInfoMapper.update(isNull(), any())).thenReturn(1);
|
||||||
|
when(playBalanceDetailsInfoService.existsCustomerConsumeRecord(completed.getPurchaserBy(), orderId))
|
||||||
|
.thenReturn(true);
|
||||||
|
|
||||||
|
OrderRevocationContext context = new OrderRevocationContext();
|
||||||
|
context.setOrderId(orderId);
|
||||||
|
context.setOperatorId("admin-9");
|
||||||
|
context.setOperatorType(OperatorType.ADMIN.getCode());
|
||||||
|
context.setRefundAmount(BigDecimal.valueOf(108));
|
||||||
|
context.setRefundReason("质量问题");
|
||||||
|
context.setRefundToCustomer(true);
|
||||||
|
context.setDeductClerkEarnings(false);
|
||||||
|
|
||||||
|
lifecycleService.revokeCompletedOrder(context);
|
||||||
|
|
||||||
|
verify(customUserInfoService, never()).getById(anyString());
|
||||||
|
verify(applicationEventPublisher).publishEvent(any(OrderRevocationEvent.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void revokeCompletedOrder_reverseStrategyDefaultsCounterClerkWhenMissing() {
|
||||||
|
String orderId = UUID.randomUUID().toString();
|
||||||
|
PlayOrderInfoEntity completed = buildOrder(orderId, OrderStatus.COMPLETED.getCode());
|
||||||
|
completed.setRefundType(OrderRefundFlag.NOT_REFUNDED.getCode());
|
||||||
|
PlayOrderInfoEntity revoked = buildOrder(orderId, OrderStatus.REVOKED.getCode());
|
||||||
|
|
||||||
|
when(orderInfoMapper.selectById(orderId)).thenReturn(completed, revoked);
|
||||||
|
lenient().when(orderInfoMapper.update(isNull(), any())).thenReturn(1);
|
||||||
|
when(earningsService.getRemainingEarningsForOrder(orderId, completed.getAcceptBy()))
|
||||||
|
.thenReturn(BigDecimal.TEN);
|
||||||
|
|
||||||
|
OrderRevocationContext context = new OrderRevocationContext();
|
||||||
|
context.setOrderId(orderId);
|
||||||
|
context.setOperatorId("admin-locked");
|
||||||
|
context.setOperatorType(OperatorType.ADMIN.getCode());
|
||||||
|
context.setRefundToCustomer(false);
|
||||||
|
context.setDeductClerkEarnings(true);
|
||||||
|
|
||||||
|
context.setEarningsAdjustAmount(BigDecimal.valueOf(10));
|
||||||
|
|
||||||
|
assertDoesNotThrow(() -> lifecycleService.revokeCompletedOrder(context));
|
||||||
|
verify(orderInfoMapper).update(isNull(), any());
|
||||||
|
verify(applicationEventPublisher).publishEvent(any(OrderRevocationEvent.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void revokeCompletedOrder_throwsWhenDeductExceedsAvailable() {
|
||||||
|
String orderId = UUID.randomUUID().toString();
|
||||||
|
PlayOrderInfoEntity completed = buildOrder(orderId, OrderStatus.COMPLETED.getCode());
|
||||||
|
PlayOrderInfoEntity revoked = buildOrder(orderId, OrderStatus.REVOKED.getCode());
|
||||||
|
|
||||||
|
when(orderInfoMapper.selectById(orderId)).thenReturn(completed, revoked);
|
||||||
|
when(earningsService.getRemainingEarningsForOrder(orderId, completed.getAcceptBy()))
|
||||||
|
.thenReturn(BigDecimal.valueOf(40));
|
||||||
|
|
||||||
|
OrderRevocationContext context = new OrderRevocationContext();
|
||||||
|
context.setOrderId(orderId);
|
||||||
|
context.setOperatorId("admin-counter");
|
||||||
|
context.setOperatorType(OperatorType.ADMIN.getCode());
|
||||||
|
context.setRefundToCustomer(false);
|
||||||
|
context.setRefundAmount(BigDecimal.ZERO);
|
||||||
|
context.setDeductClerkEarnings(true);
|
||||||
|
context.setEarningsAdjustAmount(BigDecimal.valueOf(50));
|
||||||
|
|
||||||
|
CustomException ex = assertThrows(CustomException.class, () -> lifecycleService.revokeCompletedOrder(context));
|
||||||
|
assertEquals("扣回金额不能超过本单收益40", ex.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void revokeCompletedOrder_throwsWhenNoEarningsToDeduct() {
|
||||||
|
String orderId = UUID.randomUUID().toString();
|
||||||
|
PlayOrderInfoEntity completed = buildOrder(orderId, OrderStatus.COMPLETED.getCode());
|
||||||
|
PlayOrderInfoEntity revoked = buildOrder(orderId, OrderStatus.REVOKED.getCode());
|
||||||
|
|
||||||
|
when(orderInfoMapper.selectById(orderId)).thenReturn(completed, revoked);
|
||||||
|
when(earningsService.getRemainingEarningsForOrder(orderId, completed.getAcceptBy()))
|
||||||
|
.thenReturn(BigDecimal.ZERO);
|
||||||
|
|
||||||
|
OrderRevocationContext context = new OrderRevocationContext();
|
||||||
|
context.setOrderId(orderId);
|
||||||
|
context.setOperatorId("admin-empty");
|
||||||
|
context.setOperatorType(OperatorType.ADMIN.getCode());
|
||||||
|
context.setRefundToCustomer(false);
|
||||||
|
context.setRefundAmount(BigDecimal.ZERO);
|
||||||
|
context.setDeductClerkEarnings(true);
|
||||||
|
|
||||||
|
CustomException ex = assertThrows(CustomException.class, () -> lifecycleService.revokeCompletedOrder(context));
|
||||||
|
assertEquals("本单店员收益已全部扣回", ex.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void revokeCompletedOrder_rejectsNonNormalOrders() {
|
||||||
|
String orderId = UUID.randomUUID().toString();
|
||||||
|
PlayOrderInfoEntity giftOrder = buildOrder(orderId, OrderStatus.COMPLETED.getCode());
|
||||||
|
giftOrder.setOrderType(OrderConstant.OrderType.GIFT.getCode());
|
||||||
|
|
||||||
|
when(orderInfoMapper.selectById(orderId)).thenReturn(giftOrder);
|
||||||
|
|
||||||
|
OrderRevocationContext context = new OrderRevocationContext();
|
||||||
|
context.setOrderId(orderId);
|
||||||
|
context.setOperatorId("admin-block");
|
||||||
|
context.setOperatorType(OperatorType.ADMIN.getCode());
|
||||||
|
context.setDeductClerkEarnings(false);
|
||||||
|
|
||||||
|
CustomException ex = assertThrows(CustomException.class, () -> lifecycleService.revokeCompletedOrder(context));
|
||||||
|
assertEquals("仅支持撤销普通服务订单", ex.getMessage());
|
||||||
|
verify(orderInfoMapper, never()).update(isNull(), any());
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void placeOrder_throwsWhenCommandNull() {
|
void placeOrder_throwsWhenCommandNull() {
|
||||||
assertThrows(CustomException.class, () -> lifecycleService.placeOrder(null));
|
assertThrows(CustomException.class, () -> lifecycleService.placeOrder(null));
|
||||||
@@ -1217,12 +1386,20 @@ class OrderLifecycleServiceImplTest {
|
|||||||
PlayOrderInfoEntity entity = new PlayOrderInfoEntity();
|
PlayOrderInfoEntity entity = new PlayOrderInfoEntity();
|
||||||
entity.setId(orderId);
|
entity.setId(orderId);
|
||||||
entity.setOrderStatus(status);
|
entity.setOrderStatus(status);
|
||||||
|
entity.setOrderType(OrderConstant.OrderType.NORMAL.getCode());
|
||||||
entity.setAcceptBy("clerk-1");
|
entity.setAcceptBy("clerk-1");
|
||||||
entity.setPurchaserBy("customer-1");
|
entity.setPurchaserBy("customer-1");
|
||||||
entity.setTenantId("tenant-1");
|
entity.setTenantId("tenant-1");
|
||||||
return entity;
|
return entity;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private PlayCustomUserInfoEntity buildCustomer(String id, BigDecimal balance) {
|
||||||
|
PlayCustomUserInfoEntity entity = new PlayCustomUserInfoEntity();
|
||||||
|
entity.setId(id);
|
||||||
|
entity.setAccountBalance(balance);
|
||||||
|
return entity;
|
||||||
|
}
|
||||||
|
|
||||||
private void stubDefaultPersistence() {
|
private void stubDefaultPersistence() {
|
||||||
lenient().when(orderInfoMapper.selectCount(any())).thenReturn(0L);
|
lenient().when(orderInfoMapper.selectCount(any())).thenReturn(0L);
|
||||||
lenient().when(orderInfoMapper.insert(any())).thenReturn(1);
|
lenient().when(orderInfoMapper.insert(any())).thenReturn(1);
|
||||||
|
|||||||
@@ -25,13 +25,17 @@ import com.starry.admin.modules.statistics.module.vo.ClerkPerformanceSnapshotVo;
|
|||||||
import com.starry.admin.modules.statistics.module.vo.PlayClerkPerformanceInfoReturnVo;
|
import com.starry.admin.modules.statistics.module.vo.PlayClerkPerformanceInfoReturnVo;
|
||||||
import com.starry.admin.modules.statistics.service.impl.PlayClerkPerformanceServiceImpl;
|
import com.starry.admin.modules.statistics.service.impl.PlayClerkPerformanceServiceImpl;
|
||||||
import com.starry.admin.modules.withdraw.mapper.EarningsLineMapper;
|
import com.starry.admin.modules.withdraw.mapper.EarningsLineMapper;
|
||||||
|
import com.starry.admin.utils.DateRangeUtils;
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.time.Month;
|
import java.time.Month;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
import org.junit.jupiter.api.DisplayName;
|
import org.junit.jupiter.api.DisplayName;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.junit.jupiter.api.extension.ExtendWith;
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
@@ -318,6 +322,106 @@ class PlayClerkPerformanceServiceImplTest {
|
|||||||
Arrays.asList(o1.getId(), o2.getId(), o3.getId(), o4.getId(), o5.getId())));
|
Arrays.asList(o1.getId(), o2.getId(), o3.getId(), o4.getId(), o5.getId())));
|
||||||
}
|
}
|
||||||
@Test
|
@Test
|
||||||
|
@DisplayName("queryOverview filters completed orders according to complex date/time ranges")
|
||||||
|
void queryOverviewHonorsDateRangesForMultipleClerks() {
|
||||||
|
PlayClerkUserInfoEntity alpha = buildClerk("c-alpha", "Alpha", "g-alpha", "l-alpha");
|
||||||
|
PlayClerkUserInfoEntity beta = buildClerk("c-beta", "Beta", "g-beta", "l-beta");
|
||||||
|
PlayClerkUserInfoEntity gamma = buildClerk("c-gamma", "Gamma", "g-gamma", "l-gamma");
|
||||||
|
List<PlayClerkUserInfoEntity> clerks = Arrays.asList(alpha, beta, gamma);
|
||||||
|
|
||||||
|
when(playPersonnelGroupInfoService.getValidClerkIdList(any(), any()))
|
||||||
|
.thenReturn(Arrays.asList(alpha.getId(), beta.getId(), gamma.getId()));
|
||||||
|
when(clerkUserInfoService.list((Wrapper<PlayClerkUserInfoEntity>) any())).thenReturn(clerks);
|
||||||
|
when(playClerkLevelInfoService.selectAll()).thenReturn(Arrays.asList(
|
||||||
|
level(alpha.getLevelId(), "铂金"),
|
||||||
|
level(beta.getLevelId(), "黄金"),
|
||||||
|
level(gamma.getLevelId(), "白银")));
|
||||||
|
when(playPersonnelGroupInfoService.selectAll()).thenReturn(Arrays.asList(
|
||||||
|
group(alpha.getGroupId(), "一组"),
|
||||||
|
group(beta.getGroupId(), "二组"),
|
||||||
|
group(gamma.getGroupId(), "三组")));
|
||||||
|
|
||||||
|
Map<String, List<PlayOrderInfoEntity>> ordersByClerk = new HashMap<>();
|
||||||
|
ordersByClerk.put(alpha.getId(), Arrays.asList(
|
||||||
|
order(alpha.getId(), "userA1", "1", "0", "0", new BigDecimal("100.00"), new BigDecimal("70.00"),
|
||||||
|
LocalDateTime.of(2024, Month.JULY, 1, 10, 0)),
|
||||||
|
order(alpha.getId(), "userA2", "0", "0", "0", new BigDecimal("210.00"), new BigDecimal("120.00"),
|
||||||
|
LocalDateTime.of(2024, Month.JULY, 3, 15, 30)),
|
||||||
|
order(alpha.getId(), "userA3", "0", "0", "0", new BigDecimal("150.00"), new BigDecimal("80.00"),
|
||||||
|
LocalDateTime.of(2024, Month.JULY, 7, 9, 15)),
|
||||||
|
order(alpha.getId(), "userA4", "0", "0", "0", new BigDecimal("130.00"), new BigDecimal("75.00"),
|
||||||
|
LocalDateTime.of(2024, Month.JULY, 10, 18, 45))));
|
||||||
|
ordersByClerk.put(beta.getId(), Arrays.asList(
|
||||||
|
order(beta.getId(), "userB1", "1", "0", "0", new BigDecimal("95.00"), new BigDecimal("50.00"),
|
||||||
|
LocalDateTime.of(2024, Month.JULY, 2, 11, 30)),
|
||||||
|
order(beta.getId(), "userB2", "0", "0", "0", new BigDecimal("120.00"), new BigDecimal("65.00"),
|
||||||
|
LocalDateTime.of(2024, Month.JULY, 4, 13, 0)),
|
||||||
|
order(beta.getId(), "userB3", "0", "0", "0", new BigDecimal("85.00"), new BigDecimal("40.00"),
|
||||||
|
LocalDateTime.of(2024, Month.JULY, 8, 16, 0)),
|
||||||
|
order(beta.getId(), "userB4", "0", "0", "0", new BigDecimal("150.00"), new BigDecimal("85.00"),
|
||||||
|
LocalDateTime.of(2024, Month.JULY, 10, 19, 0))));
|
||||||
|
ordersByClerk.put(gamma.getId(), Arrays.asList(
|
||||||
|
order(gamma.getId(), "userC1", "1", "0", "0", new BigDecimal("70.00"), new BigDecimal("35.00"),
|
||||||
|
LocalDateTime.of(2024, Month.JULY, 1, 8, 0)),
|
||||||
|
order(gamma.getId(), "userC2", "0", "0", "0", new BigDecimal("135.00"), new BigDecimal("70.00"),
|
||||||
|
LocalDateTime.of(2024, Month.JULY, 5, 17, 20)),
|
||||||
|
order(gamma.getId(), "userC3", "0", "0", "0", new BigDecimal("75.00"), new BigDecimal("45.00"),
|
||||||
|
LocalDateTime.of(2024, Month.JULY, 6, 14, 50)),
|
||||||
|
order(gamma.getId(), "userC4", "0", "0", "0", new BigDecimal("160.00"), new BigDecimal("90.00"),
|
||||||
|
LocalDateTime.of(2024, Month.JULY, 9, 21, 10))));
|
||||||
|
|
||||||
|
when(playOrderInfoService.clerkSelectOrderInfoList(anyString(), anyString(), anyString()))
|
||||||
|
.thenAnswer(invocation -> {
|
||||||
|
String clerkId = invocation.getArgument(0);
|
||||||
|
String startStr = invocation.getArgument(1);
|
||||||
|
String endStr = invocation.getArgument(2);
|
||||||
|
LocalDateTime start = LocalDateTime.parse(startStr, DateRangeUtils.DATE_TIME_FORMATTER);
|
||||||
|
LocalDateTime end = LocalDateTime.parse(endStr, DateRangeUtils.DATE_TIME_FORMATTER);
|
||||||
|
return ordersByClerk.getOrDefault(clerkId, Collections.emptyList()).stream()
|
||||||
|
.filter(order -> !order.getPurchaserTime().isBefore(start)
|
||||||
|
&& !order.getPurchaserTime().isAfter(end))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
});
|
||||||
|
|
||||||
|
setAuthentication();
|
||||||
|
try {
|
||||||
|
ClerkPerformanceOverviewResponseVo fullRange =
|
||||||
|
service.queryOverview(buildOverviewVo("2024-07-01", "2024-07-10"));
|
||||||
|
ClerkPerformanceOverviewSummaryVo fullSummary = fullRange.getSummary();
|
||||||
|
assertEquals(new BigDecimal("1480.00"), fullSummary.getTotalGmv());
|
||||||
|
assertEquals(12, fullSummary.getTotalOrderCount());
|
||||||
|
assertEquals(new BigDecimal("590.00"), snapshotFor(fullRange.getRankings(), alpha.getId()).getGmv());
|
||||||
|
assertEquals(new BigDecimal("450.00"), snapshotFor(fullRange.getRankings(), beta.getId()).getGmv());
|
||||||
|
assertEquals(new BigDecimal("440.00"), snapshotFor(fullRange.getRankings(), gamma.getId()).getGmv());
|
||||||
|
|
||||||
|
ClerkPerformanceOverviewResponseVo midRange =
|
||||||
|
service.queryOverview(buildOverviewVo("2024-07-04", "2024-07-08"));
|
||||||
|
ClerkPerformanceOverviewSummaryVo midSummary = midRange.getSummary();
|
||||||
|
assertEquals(5, midSummary.getTotalOrderCount());
|
||||||
|
assertEquals(new BigDecimal("565.00"), midSummary.getTotalGmv());
|
||||||
|
List<ClerkPerformanceSnapshotVo> midRankings = midRange.getRankings();
|
||||||
|
assertEquals("c-gamma", midRankings.get(0).getClerkId());
|
||||||
|
assertEquals(new BigDecimal("210.00"), midRankings.get(0).getGmv());
|
||||||
|
assertEquals("c-beta", midRankings.get(1).getClerkId());
|
||||||
|
assertEquals(new BigDecimal("205.00"), midRankings.get(1).getGmv());
|
||||||
|
assertEquals("c-alpha", midRankings.get(2).getClerkId());
|
||||||
|
assertEquals(new BigDecimal("150.00"), midRankings.get(2).getGmv());
|
||||||
|
|
||||||
|
ClerkPerformanceOverviewResponseVo shortRange = service.queryOverview(
|
||||||
|
buildOverviewVo("2024-07-10 18:30:00", "2024-07-10 18:59:59"));
|
||||||
|
ClerkPerformanceOverviewSummaryVo shortSummary = shortRange.getSummary();
|
||||||
|
assertEquals(1, shortSummary.getTotalOrderCount());
|
||||||
|
assertEquals(new BigDecimal("130.00"), shortSummary.getTotalGmv());
|
||||||
|
ClerkPerformanceSnapshotVo alphaSnapshot = snapshotFor(shortRange.getRankings(), alpha.getId());
|
||||||
|
assertEquals(new BigDecimal("130.00"), alphaSnapshot.getGmv());
|
||||||
|
assertEquals(1, alphaSnapshot.getOrderCount());
|
||||||
|
assertEquals(BigDecimal.ZERO, snapshotFor(shortRange.getRankings(), beta.getId()).getGmv());
|
||||||
|
assertEquals(BigDecimal.ZERO, snapshotFor(shortRange.getRankings(), gamma.getId()).getGmv());
|
||||||
|
} finally {
|
||||||
|
clearAuthentication();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@Test
|
||||||
@DisplayName("getClerkPerformanceInfo should ignore non-completed orders when aggregating GMV")
|
@DisplayName("getClerkPerformanceInfo should ignore non-completed orders when aggregating GMV")
|
||||||
void getClerkPerformanceInfoSkipsNonCompletedOrders() {
|
void getClerkPerformanceInfoSkipsNonCompletedOrders() {
|
||||||
PlayClerkUserInfoEntity clerk = buildClerk("c9", "Nine", "gX", "lX");
|
PlayClerkUserInfoEntity clerk = buildClerk("c9", "Nine", "gX", "lX");
|
||||||
@@ -348,6 +452,22 @@ class PlayClerkPerformanceServiceImplTest {
|
|||||||
assertTrue(idCaptor.getValue().contains(completed.getId()));
|
assertTrue(idCaptor.getValue().contains(completed.getId()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private ClerkPerformanceOverviewQueryVo buildOverviewVo(String start, String end) {
|
||||||
|
ClerkPerformanceOverviewQueryVo vo = new ClerkPerformanceOverviewQueryVo();
|
||||||
|
vo.setEndOrderTime(Arrays.asList(start, end));
|
||||||
|
vo.setIncludeSummary(true);
|
||||||
|
vo.setIncludeRankings(true);
|
||||||
|
vo.setLimit(10);
|
||||||
|
return vo;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ClerkPerformanceSnapshotVo snapshotFor(List<ClerkPerformanceSnapshotVo> snapshots, String clerkId) {
|
||||||
|
return snapshots.stream()
|
||||||
|
.filter(snapshot -> clerkId.equals(snapshot.getClerkId()))
|
||||||
|
.findFirst()
|
||||||
|
.orElseThrow(() -> new AssertionError("Snapshot not found for clerk " + clerkId));
|
||||||
|
}
|
||||||
|
|
||||||
private PlayClerkUserInfoEntity buildClerk(String id, String name, String groupId, String levelId) {
|
private PlayClerkUserInfoEntity buildClerk(String id, String name, String groupId, String levelId) {
|
||||||
PlayClerkUserInfoEntity entity = new PlayClerkUserInfoEntity();
|
PlayClerkUserInfoEntity entity = new PlayClerkUserInfoEntity();
|
||||||
entity.setId(id);
|
entity.setId(id);
|
||||||
|
|||||||
@@ -0,0 +1,143 @@
|
|||||||
|
package com.starry.admin.modules.weichat.controller;
|
||||||
|
|
||||||
|
import static org.hamcrest.Matchers.hasSize;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
|
||||||
|
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||||
|
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.multipart;
|
||||||
|
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put;
|
||||||
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||||
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.starry.admin.common.conf.ThreadLocalRequestDetail;
|
||||||
|
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 com.starry.admin.modules.clerk.module.entity.PlayClerkUserInfoEntity;
|
||||||
|
import com.starry.admin.modules.clerk.service.IPlayClerkMediaAssetService;
|
||||||
|
import com.starry.admin.modules.media.entity.PlayMediaEntity;
|
||||||
|
import com.starry.admin.modules.media.enums.MediaOwnerType;
|
||||||
|
import com.starry.admin.modules.media.service.IPlayMediaService;
|
||||||
|
import com.starry.admin.modules.weichat.entity.clerk.MediaOrderRequest;
|
||||||
|
import com.starry.admin.modules.weichat.entity.clerk.MediaVo;
|
||||||
|
import com.starry.admin.modules.weichat.service.MediaUploadService;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import org.junit.jupiter.api.AfterEach;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.InjectMocks;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.mock.web.MockMultipartFile;
|
||||||
|
import org.springframework.test.web.servlet.MockMvc;
|
||||||
|
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
|
||||||
|
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
public class WxClerkMediaControllerTest {
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private MediaUploadService mediaUploadService;
|
||||||
|
@Mock
|
||||||
|
private IPlayMediaService mediaService;
|
||||||
|
@Mock
|
||||||
|
private IPlayClerkMediaAssetService clerkMediaAssetService;
|
||||||
|
@InjectMocks
|
||||||
|
private WxClerkMediaController controller;
|
||||||
|
|
||||||
|
private MockMvc mockMvc;
|
||||||
|
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||||
|
|
||||||
|
private PlayClerkUserInfoEntity clerk;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
mockMvc = MockMvcBuilders.standaloneSetup(controller).build();
|
||||||
|
clerk = new PlayClerkUserInfoEntity();
|
||||||
|
clerk.setId("clerk-1");
|
||||||
|
clerk.setTenantId("tenant-1");
|
||||||
|
ThreadLocalRequestDetail.setRequestDetail(clerk);
|
||||||
|
}
|
||||||
|
|
||||||
|
@AfterEach
|
||||||
|
void tearDown() {
|
||||||
|
ThreadLocalRequestDetail.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void uploadShouldReturnMediaVo() throws Exception {
|
||||||
|
MediaVo vo = new MediaVo();
|
||||||
|
vo.setId("media-1");
|
||||||
|
vo.setUsage(ClerkMediaUsage.PROFILE.getCode());
|
||||||
|
when(mediaUploadService.upload(any(), eq(clerk), eq(ClerkMediaUsage.PROFILE))).thenReturn(vo);
|
||||||
|
|
||||||
|
MockMultipartFile file = new MockMultipartFile("file", "avatar.png", "image/png", new byte[] {1, 2});
|
||||||
|
|
||||||
|
mockMvc.perform(multipart("/wx/clerk/media/upload")
|
||||||
|
.file(file)
|
||||||
|
.param("usage", ClerkMediaUsage.PROFILE.getCode()))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.code").value(200))
|
||||||
|
.andExpect(jsonPath("$.data.id").value("media-1"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void updateOrderShouldDelegateToService() throws Exception {
|
||||||
|
MediaOrderRequest request = new MediaOrderRequest();
|
||||||
|
request.setUsage(ClerkMediaUsage.PROFILE.getCode());
|
||||||
|
request.setMediaIds(Arrays.asList("m1", "m2"));
|
||||||
|
|
||||||
|
mockMvc.perform(put("/wx/clerk/media/order")
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.content(objectMapper.writeValueAsString(request)))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.code").value(200));
|
||||||
|
|
||||||
|
verify(clerkMediaAssetService).submitWithOrder(eq("clerk-1"), eq(ClerkMediaUsage.PROFILE),
|
||||||
|
eq(Arrays.asList("m1", "m2")));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void listApprovedShouldReturnAssemblerOutput() throws Exception {
|
||||||
|
PlayClerkMediaAssetEntity asset = new PlayClerkMediaAssetEntity();
|
||||||
|
asset.setId("asset-1");
|
||||||
|
asset.setClerkId("clerk-1");
|
||||||
|
asset.setUsage(ClerkMediaUsage.PROFILE.getCode());
|
||||||
|
asset.setMediaId("media-1");
|
||||||
|
asset.setReviewState(ClerkMediaReviewState.APPROVED.getCode());
|
||||||
|
asset.setCreatedTime(java.sql.Timestamp.valueOf(LocalDateTime.now()));
|
||||||
|
when(clerkMediaAssetService.listByState(eq("clerk-1"), eq(ClerkMediaUsage.PROFILE),
|
||||||
|
eq(Collections.singletonList(ClerkMediaReviewState.APPROVED)))).thenReturn(
|
||||||
|
Collections.singletonList(asset));
|
||||||
|
|
||||||
|
PlayMediaEntity media = new PlayMediaEntity();
|
||||||
|
media.setId("media-1");
|
||||||
|
media.setUrl("https://oss/mock.png");
|
||||||
|
when(mediaService.listByIds(Collections.singletonList("media-1")))
|
||||||
|
.thenReturn(Collections.singletonList(media));
|
||||||
|
|
||||||
|
mockMvc.perform(get("/wx/clerk/media/approved"))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.code").value(200))
|
||||||
|
.andExpect(jsonPath("$.data", hasSize(1)))
|
||||||
|
.andExpect(jsonPath("$.data[0].url").value("https://oss/mock.png"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void deleteShouldInvokeServices() throws Exception {
|
||||||
|
mockMvc.perform(delete("/wx/clerk/media/{id}", "media-77"))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.code").value(200));
|
||||||
|
|
||||||
|
verify(clerkMediaAssetService).softDelete("clerk-1", "media-77");
|
||||||
|
verify(mediaService).softDelete(MediaOwnerType.CLERK, "clerk-1", "media-77");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,137 @@
|
|||||||
|
package com.starry.admin.modules.weichat.service;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
|
import static org.mockito.Mockito.never;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
import com.starry.admin.common.exception.CustomException;
|
||||||
|
import com.starry.admin.common.oss.service.IOssFileService;
|
||||||
|
import com.starry.admin.modules.clerk.enums.ClerkMediaUsage;
|
||||||
|
import com.starry.admin.modules.clerk.module.entity.PlayClerkMediaAssetEntity;
|
||||||
|
import com.starry.admin.modules.clerk.module.entity.PlayClerkUserInfoEntity;
|
||||||
|
import com.starry.admin.modules.clerk.service.IPlayClerkMediaAssetService;
|
||||||
|
import com.starry.admin.modules.media.entity.PlayMediaEntity;
|
||||||
|
import com.starry.admin.modules.media.enums.MediaOwnerType;
|
||||||
|
import com.starry.admin.modules.media.service.IPlayMediaService;
|
||||||
|
import com.starry.admin.modules.weichat.entity.clerk.MediaVo;
|
||||||
|
import java.awt.image.BufferedImage;
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import javax.imageio.ImageIO;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.DisplayName;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.ArgumentCaptor;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
import org.springframework.mock.web.MockMultipartFile;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
class MediaUploadServiceTest {
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private IOssFileService ossFileService;
|
||||||
|
@Mock
|
||||||
|
private IPlayMediaService mediaService;
|
||||||
|
@Mock
|
||||||
|
private IPlayClerkMediaAssetService clerkMediaAssetService;
|
||||||
|
|
||||||
|
private MediaUploadService service;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
service = new MediaUploadService(ossFileService, mediaService, clerkMediaAssetService);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("上传图片时应创建媒资与草稿资产,并返回完整的 MediaVo")
|
||||||
|
void uploadImageCreatesMediaAndAsset() throws Exception {
|
||||||
|
PlayClerkUserInfoEntity clerk = buildClerk();
|
||||||
|
MultipartFile file = buildImageFile("avatar.png", "image/png");
|
||||||
|
|
||||||
|
when(mediaService.normalizeAndSave(any())).thenAnswer(invocation -> invocation.getArgument(0));
|
||||||
|
when(ossFileService.upload(any(), eq(clerk.getTenantId()), any()))
|
||||||
|
.thenReturn("https://oss.mock/avatar.png");
|
||||||
|
|
||||||
|
PlayClerkMediaAssetEntity asset = new PlayClerkMediaAssetEntity();
|
||||||
|
asset.setId("asset-1");
|
||||||
|
asset.setUsage(ClerkMediaUsage.PROFILE.getCode());
|
||||||
|
asset.setReviewState("draft");
|
||||||
|
when(clerkMediaAssetService.linkDraftAsset(any(), any(), any(), any())).thenAnswer(invocation -> {
|
||||||
|
asset.setMediaId(invocation.getArgument(2));
|
||||||
|
return asset;
|
||||||
|
});
|
||||||
|
|
||||||
|
MediaVo result = service.upload(file, clerk, null);
|
||||||
|
|
||||||
|
assertThat(result).isNotNull();
|
||||||
|
assertThat(result.getUrl()).isEqualTo("https://oss.mock/avatar.png");
|
||||||
|
assertThat(result.getUsage()).isEqualTo(ClerkMediaUsage.PROFILE.getCode());
|
||||||
|
assertThat(result.getAssetId()).isEqualTo("asset-1");
|
||||||
|
|
||||||
|
ArgumentCaptor<PlayMediaEntity> mediaCaptor = ArgumentCaptor.forClass(PlayMediaEntity.class);
|
||||||
|
verify(mediaService).normalizeAndSave(mediaCaptor.capture());
|
||||||
|
PlayMediaEntity persisted = mediaCaptor.getValue();
|
||||||
|
assertThat(persisted.getOwnerType()).isEqualTo(MediaOwnerType.CLERK);
|
||||||
|
assertThat(persisted.getOwnerId()).isEqualTo(clerk.getId());
|
||||||
|
assertThat(persisted.getMetadata()).containsKeys("originalFilename", "contentType", "uploadTraceId");
|
||||||
|
|
||||||
|
verify(clerkMediaAssetService).linkDraftAsset(
|
||||||
|
eq(clerk.getTenantId()),
|
||||||
|
eq(clerk.getId()),
|
||||||
|
eq(persisted.getId()),
|
||||||
|
eq(ClerkMediaUsage.PROFILE));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("不支援的檔案格式應回報錯誤,且不呼叫外部服務")
|
||||||
|
void uploadRejectsUnsupportedFormat() {
|
||||||
|
PlayClerkUserInfoEntity clerk = buildClerk();
|
||||||
|
MultipartFile invalidFile = new MockMultipartFile("file", "note.txt", "text/plain", "oops".getBytes());
|
||||||
|
|
||||||
|
assertThatThrownBy(() -> service.upload(invalidFile, clerk, ClerkMediaUsage.PROFILE))
|
||||||
|
.isInstanceOf(CustomException.class)
|
||||||
|
.hasMessage("不支持的文件格式");
|
||||||
|
|
||||||
|
verify(mediaService, never()).normalizeAndSave(any());
|
||||||
|
verify(ossFileService, never()).upload(any(), any(), any());
|
||||||
|
verify(clerkMediaAssetService, never()).linkDraftAsset(any(), any(), any(), any());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("缺少檔案或店員資訊時應回報錯誤")
|
||||||
|
void uploadRejectsMissingInputs() {
|
||||||
|
PlayClerkUserInfoEntity clerk = buildClerk();
|
||||||
|
MultipartFile emptyFile = new MockMultipartFile("file", new byte[0]);
|
||||||
|
|
||||||
|
assertThatThrownBy(() -> service.upload(emptyFile, clerk, ClerkMediaUsage.PROFILE))
|
||||||
|
.isInstanceOf(CustomException.class)
|
||||||
|
.hasMessage("请选择要上传的文件");
|
||||||
|
|
||||||
|
MultipartFile file = new MockMultipartFile("file", "a.png", "image/png", new byte[] {1});
|
||||||
|
assertThatThrownBy(() -> service.upload(file, null, ClerkMediaUsage.PROFILE))
|
||||||
|
.isInstanceOf(CustomException.class)
|
||||||
|
.hasMessage("店员信息不存在");
|
||||||
|
}
|
||||||
|
|
||||||
|
private MultipartFile buildImageFile(String filename, String contentType) throws IOException {
|
||||||
|
BufferedImage image = new BufferedImage(2, 2, BufferedImage.TYPE_INT_RGB);
|
||||||
|
try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
|
||||||
|
ImageIO.write(image, "png", baos);
|
||||||
|
return new MockMultipartFile("file", filename, contentType, baos.toByteArray());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private PlayClerkUserInfoEntity buildClerk() {
|
||||||
|
PlayClerkUserInfoEntity entity = new PlayClerkUserInfoEntity();
|
||||||
|
entity.setId("clerk-1");
|
||||||
|
entity.setTenantId("tenant-1");
|
||||||
|
return entity;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,17 +1,23 @@
|
|||||||
package com.starry.admin.modules.withdraw.service.impl;
|
package com.starry.admin.modules.withdraw.service.impl;
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
import static org.mockito.ArgumentMatchers.any;
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
import static org.mockito.Mockito.never;
|
import static org.mockito.Mockito.never;
|
||||||
import static org.mockito.Mockito.verify;
|
import static org.mockito.Mockito.verify;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
import com.starry.admin.modules.order.module.constant.OrderConstant;
|
||||||
import com.starry.admin.modules.order.module.entity.PlayOrderInfoEntity;
|
import com.starry.admin.modules.order.module.entity.PlayOrderInfoEntity;
|
||||||
import com.starry.admin.modules.withdraw.entity.EarningsLineEntity;
|
import com.starry.admin.modules.withdraw.entity.EarningsLineEntity;
|
||||||
import com.starry.admin.modules.withdraw.mapper.EarningsLineMapper;
|
import com.starry.admin.modules.withdraw.mapper.EarningsLineMapper;
|
||||||
import com.starry.admin.modules.withdraw.service.IFreezePolicyService;
|
import com.starry.admin.modules.withdraw.service.IFreezePolicyService;
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.junit.jupiter.api.extension.ExtendWith;
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
@@ -68,4 +74,150 @@ class EarningsServiceImplTest {
|
|||||||
|
|
||||||
verify(baseMapper, never()).insert(any());
|
verify(baseMapper, never()).insert(any());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void createFromOrder_skipsBlindBoxPurchaseOrders() {
|
||||||
|
PlayOrderInfoEntity order = baselineOrder();
|
||||||
|
order.setEstimatedRevenue(BigDecimal.valueOf(66));
|
||||||
|
order.setOrderType(OrderConstant.OrderType.BLIND_BOX_PURCHASE.getCode());
|
||||||
|
|
||||||
|
earningsService.createFromOrder(order);
|
||||||
|
|
||||||
|
verify(baseMapper, never()).insert(any());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void createCounterLine_throwsWhenNoReferencePresent() {
|
||||||
|
when(baseMapper.selectList(any())).thenReturn(Collections.emptyList());
|
||||||
|
|
||||||
|
assertThrows(IllegalStateException.class, () ->
|
||||||
|
earningsService.createCounterLine("order-neg", "tenant-t", "clerk-c", BigDecimal.TEN, "admin"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void createCounterLine_persistsNegativeAvailableLine() {
|
||||||
|
EarningsLineEntity reference = new EarningsLineEntity();
|
||||||
|
reference.setAmount(new BigDecimal("88.00"));
|
||||||
|
reference.setUnlockTime(LocalDateTime.now().minusHours(1));
|
||||||
|
reference.setStatus("available");
|
||||||
|
when(baseMapper.selectList(any())).thenReturn(Collections.singletonList(reference));
|
||||||
|
when(baseMapper.insert(any())).thenReturn(1);
|
||||||
|
|
||||||
|
earningsService.createCounterLine("order-neg", "tenant-t", "clerk-c", BigDecimal.valueOf(88), "admin");
|
||||||
|
|
||||||
|
ArgumentCaptor<EarningsLineEntity> captor = ArgumentCaptor.forClass(EarningsLineEntity.class);
|
||||||
|
verify(baseMapper).insert(captor.capture());
|
||||||
|
EarningsLineEntity saved = captor.getValue();
|
||||||
|
assertEquals(new BigDecimal("-88"), saved.getAmount());
|
||||||
|
assertEquals("clerk-c", saved.getClerkId());
|
||||||
|
assertEquals("available", saved.getStatus());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void createCounterLine_inheritsFrozenUnlockScheduleFromReference() {
|
||||||
|
LocalDateTime unlockAt = LocalDateTime.now().plusDays(1).withNano(0);
|
||||||
|
EarningsLineEntity reference = new EarningsLineEntity();
|
||||||
|
reference.setAmount(new BigDecimal("150.00"));
|
||||||
|
reference.setUnlockTime(unlockAt);
|
||||||
|
reference.setStatus("frozen");
|
||||||
|
when(baseMapper.selectList(any())).thenReturn(Collections.singletonList(reference));
|
||||||
|
when(baseMapper.insert(any())).thenReturn(1);
|
||||||
|
|
||||||
|
earningsService.createCounterLine("order-neg", "tenant-t", "clerk-c", BigDecimal.valueOf(40), "admin");
|
||||||
|
|
||||||
|
ArgumentCaptor<EarningsLineEntity> captor = ArgumentCaptor.forClass(EarningsLineEntity.class);
|
||||||
|
verify(baseMapper).insert(captor.capture());
|
||||||
|
EarningsLineEntity saved = captor.getValue();
|
||||||
|
assertEquals("frozen", saved.getStatus());
|
||||||
|
assertEquals(unlockAt, saved.getUnlockTime());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void createCounterLine_unlockedReferenceProducesAvailableCounter() {
|
||||||
|
LocalDateTime unlockAt = LocalDateTime.now().minusHours(3).withNano(0);
|
||||||
|
EarningsLineEntity reference = new EarningsLineEntity();
|
||||||
|
reference.setAmount(new BigDecimal("95.00"));
|
||||||
|
reference.setUnlockTime(unlockAt);
|
||||||
|
reference.setStatus("available");
|
||||||
|
when(baseMapper.selectList(any())).thenReturn(Collections.singletonList(reference));
|
||||||
|
when(baseMapper.insert(any())).thenReturn(1);
|
||||||
|
|
||||||
|
earningsService.createCounterLine("order-neg", "tenant-t", "clerk-c", BigDecimal.valueOf(55), "admin");
|
||||||
|
|
||||||
|
ArgumentCaptor<EarningsLineEntity> captor = ArgumentCaptor.forClass(EarningsLineEntity.class);
|
||||||
|
verify(baseMapper).insert(captor.capture());
|
||||||
|
EarningsLineEntity saved = captor.getValue();
|
||||||
|
assertEquals("available", saved.getStatus());
|
||||||
|
LocalDateTime now = LocalDateTime.now();
|
||||||
|
assertTrue(saved.getUnlockTime().isAfter(unlockAt));
|
||||||
|
assertTrue(!saved.getUnlockTime().isAfter(now.plusSeconds(1)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void findWithdrawable_returnsEmptyWhenCounterLinesCreateDebt() {
|
||||||
|
LocalDateTime now = LocalDateTime.now();
|
||||||
|
List<EarningsLineEntity> lines = Arrays.asList(
|
||||||
|
line("neg", new BigDecimal("-60")),
|
||||||
|
line("pos", new BigDecimal("40")));
|
||||||
|
when(baseMapper.selectWithdrawableLines("clerk-001", now)).thenReturn(lines);
|
||||||
|
|
||||||
|
List<EarningsLineEntity> picked = earningsService.findWithdrawable("clerk-001", BigDecimal.valueOf(30), now);
|
||||||
|
|
||||||
|
assertEquals(0, picked.size(), "净额不足时不应允许提现");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void findWithdrawable_allowsWithdrawalAfterPositiveLinesCoverDebt() {
|
||||||
|
LocalDateTime now = LocalDateTime.now();
|
||||||
|
List<EarningsLineEntity> lines = Arrays.asList(
|
||||||
|
line("neg", new BigDecimal("-60")),
|
||||||
|
line("first", new BigDecimal("40")),
|
||||||
|
line("second", new BigDecimal("150")));
|
||||||
|
when(baseMapper.selectWithdrawableLines("clerk-001", now)).thenReturn(lines);
|
||||||
|
|
||||||
|
List<EarningsLineEntity> picked = earningsService.findWithdrawable("clerk-001", BigDecimal.valueOf(70), now);
|
||||||
|
|
||||||
|
assertEquals(3, picked.size());
|
||||||
|
assertEquals("second", picked.get(2).getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void findWithdrawable_handlesMixedPositiveAndNegativeSequences() {
|
||||||
|
LocalDateTime now = LocalDateTime.now();
|
||||||
|
List<EarningsLineEntity> lines = Arrays.asList(
|
||||||
|
line("neg-30", new BigDecimal("-30")),
|
||||||
|
line("pos-20", new BigDecimal("20")),
|
||||||
|
line("pos-50", new BigDecimal("50")),
|
||||||
|
line("neg-10", new BigDecimal("-10")),
|
||||||
|
line("pos-40", new BigDecimal("40")),
|
||||||
|
line("pos-60", new BigDecimal("60")),
|
||||||
|
line("pos-15", new BigDecimal("15")),
|
||||||
|
line("pos-25", new BigDecimal("25")),
|
||||||
|
line("neg-5", new BigDecimal("-5")),
|
||||||
|
line("pos-100", new BigDecimal("100")));
|
||||||
|
|
||||||
|
when(baseMapper.selectWithdrawableLines("clerk-mix", now)).thenReturn(lines, lines, lines);
|
||||||
|
|
||||||
|
List<EarningsLineEntity> partial = earningsService.findWithdrawable("clerk-mix", new BigDecimal("35"), now);
|
||||||
|
assertEquals(Arrays.asList("neg-30", "pos-20", "pos-50"), ids(partial));
|
||||||
|
|
||||||
|
List<EarningsLineEntity> mid = earningsService.findWithdrawable("clerk-mix", new BigDecimal("90"), now);
|
||||||
|
assertEquals(Arrays.asList("neg-30", "pos-20", "pos-50", "neg-10", "pos-40", "pos-60"), ids(mid));
|
||||||
|
|
||||||
|
List<EarningsLineEntity> full = earningsService.findWithdrawable("clerk-mix", new BigDecimal("265"), now);
|
||||||
|
assertEquals(lines.size(), full.size());
|
||||||
|
assertEquals("pos-100", full.get(full.size() - 1).getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
private EarningsLineEntity line(String id, BigDecimal amount) {
|
||||||
|
EarningsLineEntity entity = new EarningsLineEntity();
|
||||||
|
entity.setId(id);
|
||||||
|
entity.setAmount(amount);
|
||||||
|
entity.setStatus("available");
|
||||||
|
return entity;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<String> ids(List<EarningsLineEntity> entities) {
|
||||||
|
return entities.stream().map(EarningsLineEntity::getId).collect(java.util.stream.Collectors.toList());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
137
recreate-staging.sh
Executable file
137
recreate-staging.sh
Executable file
@@ -0,0 +1,137 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
CONTAINER_NAME=${STAGING_MYSQL_CONTAINER:-peipei-mysql-staging}
|
||||||
|
VOLUME_NAME=${STAGING_MYSQL_VOLUME:-peipei-mysql-staging-data}
|
||||||
|
HOST_PORT=${STAGING_MYSQL_PORT:-3307}
|
||||||
|
DB_NAME=${STAGING_MYSQL_DB:-play-with}
|
||||||
|
ROOT_PASSWORD=${STAGING_MYSQL_ROOT_PASSWORD:-root}
|
||||||
|
MYSQL_IMAGE=${STAGING_MYSQL_IMAGE:-mysql:8.0.32}
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<USAGE
|
||||||
|
Usage: $0 <path-to-backup.sql[.gz|.xz]>
|
||||||
|
|
||||||
|
Recreates the local staging MySQL container (${CONTAINER_NAME}) and imports the given SQL dump.
|
||||||
|
Environment overrides: STAGING_MYSQL_CONTAINER, STAGING_MYSQL_VOLUME, STAGING_MYSQL_PORT,
|
||||||
|
STAGING_MYSQL_DB, STAGING_MYSQL_ROOT_PASSWORD, STAGING_MYSQL_IMAGE.
|
||||||
|
USAGE
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if [[ $# -ne 1 ]]; then
|
||||||
|
usage
|
||||||
|
fi
|
||||||
|
|
||||||
|
backup_path=$1
|
||||||
|
if [[ ! -f "$backup_path" ]]; then
|
||||||
|
echo "[ERROR] Backup file not found: $backup_path" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! command -v docker >/dev/null 2>&1; then
|
||||||
|
echo "[ERROR] docker command not found" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
port_in_use() {
|
||||||
|
if command -v lsof >/dev/null 2>&1; then
|
||||||
|
lsof -iTCP:"$HOST_PORT" -sTCP:LISTEN -Pn >/dev/null 2>&1 && return 0
|
||||||
|
fi
|
||||||
|
if command -v netstat >/dev/null 2>&1; then
|
||||||
|
netstat -an | grep -E "\.$HOST_PORT .*LISTEN" >/dev/null 2>&1 && return 0
|
||||||
|
fi
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
confirm_remove() {
|
||||||
|
local prompt="$1"
|
||||||
|
read -r -p "$prompt (yes/no): " reply
|
||||||
|
if [[ "$reply" != "yes" ]]; then
|
||||||
|
echo "操作已取消"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
existing_container_id=$(docker ps -a --filter "name=^${CONTAINER_NAME}$" -q)
|
||||||
|
if [[ -n "$existing_container_id" ]]; then
|
||||||
|
echo "[WARN] Container ${CONTAINER_NAME} already exists."
|
||||||
|
confirm_remove "Remove the existing staging container and recreate it?"
|
||||||
|
docker rm -f "$CONTAINER_NAME" >/dev/null
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "[INFO] Removing existing volume (if any): ${VOLUME_NAME}"
|
||||||
|
if docker volume ls -q --filter "name=^${VOLUME_NAME}$" | grep -Fxq "$VOLUME_NAME"; then
|
||||||
|
docker volume rm "$VOLUME_NAME" >/dev/null
|
||||||
|
fi
|
||||||
|
|
||||||
|
if port_in_use; then
|
||||||
|
containers_on_port=$(docker ps --filter "publish=${HOST_PORT}" --format '{{.ID}}\t{{.Names}}')
|
||||||
|
if [[ -n "$containers_on_port" ]]; then
|
||||||
|
echo "[WARN] Host port ${HOST_PORT} is currently used by the following container(s):"
|
||||||
|
while IFS=$'\t' read -r cid cname; do
|
||||||
|
[[ -z "$cid" ]] && continue
|
||||||
|
echo " - ${cname} (${cid})"
|
||||||
|
done <<<"$containers_on_port"
|
||||||
|
confirm_remove "Stop and remove these container(s)?"
|
||||||
|
while IFS=$'\t' read -r cid cname; do
|
||||||
|
[[ -z "$cid" ]] && continue
|
||||||
|
docker stop "$cid" >/dev/null
|
||||||
|
docker rm "$cid" >/dev/null
|
||||||
|
done <<<"$containers_on_port"
|
||||||
|
else
|
||||||
|
echo "[ERROR] Host port ${HOST_PORT} is already in use. Set STAGING_MYSQL_PORT to an unused port or stop the process using it." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if port_in_use; then
|
||||||
|
echo "[ERROR] Host port ${HOST_PORT} is still in use after stopping containers. Please free the port manually or change STAGING_MYSQL_PORT." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "[INFO] Starting fresh MySQL container ${CONTAINER_NAME}"
|
||||||
|
docker run -d \
|
||||||
|
--name "$CONTAINER_NAME" \
|
||||||
|
-p "${HOST_PORT}:3306" \
|
||||||
|
-e MYSQL_ROOT_PASSWORD="$ROOT_PASSWORD" \
|
||||||
|
-e MYSQL_DATABASE="$DB_NAME" \
|
||||||
|
-v "$VOLUME_NAME:/var/lib/mysql" \
|
||||||
|
"$MYSQL_IMAGE" \
|
||||||
|
--lower_case_table_names=1 \
|
||||||
|
--explicit_defaults_for_timestamp=1 \
|
||||||
|
--character-set-server=utf8mb4 \
|
||||||
|
--collation-server=utf8mb4_unicode_ci >/dev/null
|
||||||
|
|
||||||
|
echo -n "[INFO] Waiting for MySQL to accept connections"
|
||||||
|
for attempt in {1..60}; do
|
||||||
|
if docker exec "$CONTAINER_NAME" mysqladmin ping -h127.0.0.1 -uroot -p"$ROOT_PASSWORD" --silent >/dev/null 2>&1; then
|
||||||
|
echo " - ready"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
sleep 2
|
||||||
|
echo -n "."
|
||||||
|
if [[ $attempt -eq 60 ]]; then
|
||||||
|
echo "\n[ERROR] MySQL did not become ready in time" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
case "$backup_path" in
|
||||||
|
*.gz)
|
||||||
|
reader=(gzip -dc -- "$backup_path")
|
||||||
|
;;
|
||||||
|
*.xz)
|
||||||
|
reader=(xz -dc -- "$backup_path")
|
||||||
|
;;
|
||||||
|
*.zip)
|
||||||
|
reader=(unzip -p -- "$backup_path")
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
reader=(cat -- "$backup_path")
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
echo "[INFO] Importing backup $backup_path into ${DB_NAME}"
|
||||||
|
"${reader[@]}" | docker exec -i "$CONTAINER_NAME" mysql -uroot -p"$ROOT_PASSWORD" --default-character-set=utf8mb4 "$DB_NAME"
|
||||||
|
|
||||||
|
echo "[INFO] Staging database restored. Container ${CONTAINER_NAME} is listening on port ${HOST_PORT}."
|
||||||
Reference in New Issue
Block a user