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

Commit 568cbd03 authored by wangyifan's avatar wangyifan

admin后台管理申请、专题功能;删除goblin_artist_auth表

parent c8e94dc0
...@@ -24,24 +24,6 @@ CREATE TABLE `goblin_artist_auth_apply` ( ...@@ -24,24 +24,6 @@ CREATE TABLE `goblin_artist_auth_apply` (
KEY `idx_created_at` (`created_at`) KEY `idx_created_at` (`created_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='艺人关联申请表'; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='艺人关联申请表';
-- 店铺-艺人授权关系表
CREATE TABLE `goblin_artist_auth` (
`mid` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '自增主键',
`auth_id` varchar(64) NOT NULL COMMENT '授权ID',
`store_id` varchar(64) NOT NULL COMMENT '店铺ID',
`artist_id` varchar(64) NOT NULL COMMENT '艺人ID',
`apply_id` varchar(64) NOT NULL COMMENT '来源申请ID',
`expire_at` datetime NOT NULL COMMENT '授权截止时间',
`status` tinyint(4) NOT NULL DEFAULT 1 COMMENT '1有效 0失效',
`created_at` datetime NOT NULL COMMENT '创建时间',
`updated_at` datetime NOT NULL COMMENT '更新时间',
PRIMARY KEY (`mid`),
UNIQUE KEY `uk_auth_id` (`auth_id`),
UNIQUE KEY `uk_store_artist` (`store_id`, `artist_id`),
KEY `idx_expire_at` (`expire_at`),
KEY `idx_store_status` (`store_id`, `status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='店铺艺人授权关系表';
-- 艺人专题表 -- 艺人专题表
CREATE TABLE `goblin_artist_topic` ( CREATE TABLE `goblin_artist_topic` (
`mid` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '自增主键', `mid` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '自增主键',
......
...@@ -476,33 +476,9 @@ public class GoblinRedisConst { ...@@ -476,33 +476,9 @@ public class GoblinRedisConst {
/* ----------------------------------------------------------------- */ /* ----------------------------------------------------------------- */
/** /**
* 艺人专题信息 * 艺人专题信息(详情缓存)
* {goblin:artist:topic:{store_id}_{topic_id}, GoblinArtistTopicVo} * {goblin:artist:topic:{topic_id}, GoblinArtistTopicVo}
*/ */
public static final String ARTIST_TOPIC = PREFIX.concat("artist:topic:"); public static final String ARTIST_TOPIC = PREFIX.concat("artist:topic:");
/**
* 店铺艺人专题列表
* {goblin:artist:topic:list:{store_id}, List<GoblinArtistTopicVo>}
*/
public static final String ARTIST_TOPIC_LIST = PREFIX.concat("artist:topic:list:");
/**
* 艺人授权关系
* {goblin:artist:auth:{store_id}_{artist_id}, GoblinArtistAuthVo}
*/
public static final String ARTIST_AUTH = PREFIX.concat("artist:auth:");
/**
* 店铺艺人授权列表
* {goblin:artist:auth:list:{store_id}, List<GoblinArtistAuthVo>}
*/
public static final String ARTIST_AUTH_LIST = PREFIX.concat("artist:auth:list:");
/**
* 艺人关联申请
* {goblin:artist:apply:{store_id}_{apply_id}, GoblinArtistAuthApplyVo}
*/
public static final String ARTIST_APPLY = PREFIX.concat("artist:apply:");
} }
package com.liquidnet.service.goblin.dto.vo;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDateTime;
@Data
public class GoblinArtistAuthVo implements Serializable {
private static final long serialVersionUID = 1L;
private String authId;
private String storeId;
private String artistId;
private String applyId;
private LocalDateTime expireAt;
private Integer status;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
}
package com.liquidnet.client.admin.web.controller.zhengzai.goblin.artist;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.liquidnet.service.goblin.entity.GoblinStoreInfo;
import com.liquidnet.service.goblin.mapper.GoblinStoreInfoMapper;
import com.liquidnet.service.kylin.entity.KylinArtist;
import com.liquidnet.service.kylin.mapper.KylinArtistMapper;
import org.apache.commons.lang3.StringUtils;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;
/**
* Admin 艺人模块共享工具方法
*/
final class ArtistAdminHelper {
private ArtistAdminHelper() {}
static final DateTimeFormatter FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
/** 按店铺名模糊查店铺ID,无输入返回 null */
static List<String> findStoreIds(GoblinStoreInfoMapper mapper, String storeName) {
if (StringUtils.isBlank(storeName)) return null;
return mapper.selectList(
new LambdaQueryWrapper<GoblinStoreInfo>()
.like(GoblinStoreInfo::getStoreName, storeName)
.select(GoblinStoreInfo::getStoreId))
.stream().map(GoblinStoreInfo::getStoreId).collect(Collectors.toList());
}
/** 按艺人名模糊查艺人ID,无输入返回 null */
static List<String> findArtistIds(KylinArtistMapper mapper, String artistName) {
if (StringUtils.isBlank(artistName)) return null;
return mapper.selectList(
new LambdaQueryWrapper<KylinArtist>()
.like(KylinArtist::getArtistName, artistName)
.select(KylinArtist::getArtistId))
.stream().map(KylinArtist::getArtistId).collect(Collectors.toList());
}
/** 批量查店铺名 */
static Map<String, String> storeNameMap(GoblinStoreInfoMapper mapper, Set<String> ids) {
if (ids.isEmpty()) return Collections.emptyMap();
return mapper.selectList(new LambdaQueryWrapper<GoblinStoreInfo>()
.in(GoblinStoreInfo::getStoreId, ids))
.stream().collect(Collectors.toMap(GoblinStoreInfo::getStoreId, GoblinStoreInfo::getStoreName, (a, b) -> a));
}
/** 批量查艺人名 */
static Map<String, String> artistNameMap(KylinArtistMapper mapper, Set<String> ids) {
if (ids.isEmpty()) return Collections.emptyMap();
return mapper.selectList(new LambdaQueryWrapper<KylinArtist>()
.in(KylinArtist::getArtistId, ids))
.stream().collect(Collectors.toMap(KylinArtist::getArtistId, KylinArtist::getArtistName, (a, b) -> a));
}
/** 从实体列表中提取 ID 集合 */
static <T> Set<String> idsOf(List<T> list, Function<T, String> getter) {
return list.stream().map(getter).filter(Objects::nonNull).collect(Collectors.toSet());
}
/** 时间格式化为 yyyy-MM-dd HH:mm:ss */
static String formatTime(LocalDateTime time) {
return time != null ? time.format(FMT) : null;
}
}
package com.liquidnet.client.admin.web.controller.zhengzai.goblin.artist;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.fasterxml.jackson.core.type.TypeReference;
import com.liquidnet.commons.lang.util.JsonUtils;
import com.liquidnet.client.admin.common.annotation.Log;
import com.liquidnet.client.admin.common.core.controller.BaseController;
import com.liquidnet.client.admin.common.core.domain.AjaxResult;
import com.liquidnet.client.admin.common.core.page.TableDataInfo;
import com.liquidnet.client.admin.common.enums.BusinessType;
import com.liquidnet.client.admin.common.utils.ShiroUtils;
import com.liquidnet.client.admin.zhengzai.goblin.service.IGoblinArtistAuthAdminService;
import com.liquidnet.service.goblin.entity.GoblinArtistAuthApply;
import com.liquidnet.service.goblin.entity.GoblinStoreInfo;
import com.liquidnet.service.goblin.mapper.GoblinStoreInfoMapper;
import com.liquidnet.service.kylin.entity.KylinArtist;
import com.liquidnet.service.kylin.mapper.KylinArtistMapper;
import org.apache.commons.lang3.StringUtils;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap;
import org.springframework.web.bind.annotation.*;
import java.time.LocalDateTime;
import java.util.*;
import java.util.stream.Collectors;
import static com.liquidnet.client.admin.web.controller.zhengzai.goblin.artist.ArtistAdminHelper.*;
@Controller
@RequestMapping("goblin/artist/auth")
public class GoblinArtistAuthAdminController extends BaseController {
private final String prefix = "zhengzai/goblin/artist/auth";
@Autowired
private IGoblinArtistAuthAdminService authAdminService;
@Autowired
private KylinArtistMapper kylinArtistMapper;
@Autowired
private GoblinStoreInfoMapper storeInfoMapper;
@GetMapping()
public String index() {
return prefix + "/auth";
}
@RequiresPermissions("goblin:artist:auth:list")
@RequestMapping("/list")
@ResponseBody
public TableDataInfo list(GoblinArtistAuthApply apply,
@RequestParam(required = false) String artistName,
@RequestParam(required = false) String storeName) {
LambdaQueryWrapper<GoblinArtistAuthApply> qw = Wrappers.lambdaQuery(GoblinArtistAuthApply.class);
if (apply.getStatus() != null) {
qw.eq(GoblinArtistAuthApply::getStatus, apply.getStatus());
}
List<String> sids = findStoreIds(storeInfoMapper, storeName);
if (sids != null) {
if (sids.isEmpty()) return getDataTable(Collections.emptyList());
qw.in(GoblinArtistAuthApply::getStoreId, sids);
}
List<String> aids = findArtistIds(kylinArtistMapper, artistName);
if (aids != null) {
if (aids.isEmpty()) return getDataTable(Collections.emptyList());
qw.in(GoblinArtistAuthApply::getArtistId, aids);
}
qw.orderByDesc(GoblinArtistAuthApply::getCreatedAt);
startPage();
return getDataTable(withNames(authAdminService.list(qw)));
}
private List<Map<String, Object>> withNames(List<GoblinArtistAuthApply> list) {
if (list.isEmpty()) return Collections.emptyList();
Map<String, String> storeNames = storeNameMap(storeInfoMapper, idsOf(list, GoblinArtistAuthApply::getStoreId));
Map<String, String> artistNames = artistNameMap(kylinArtistMapper, idsOf(list, GoblinArtistAuthApply::getArtistId));
return list.stream().map(item -> {
Map<String, Object> row = new LinkedHashMap<>();
row.put("applyId", item.getApplyId());
row.put("storeId", item.getStoreId());
row.put("storeName", storeNames.get(item.getStoreId()));
row.put("artistId", item.getArtistId());
row.put("artistName", artistNames.get(item.getArtistId()));
row.put("status", item.getStatus());
row.put("createdAt", formatTime(item.getCreatedAt()));
row.put("reviewTime", formatTime(item.getReviewTime()));
row.put("reviewerName", item.getReviewerName());
row.put("authExpireAt", formatTime(item.getAuthExpireAt()));
return row;
}).collect(Collectors.toList());
}
@RequiresPermissions("goblin:artist:auth:info")
@GetMapping("detail/{applyId}")
@ResponseBody
public AjaxResult detail(@PathVariable String applyId) {
Map<String, Object> data = buildDetail(applyId);
if (data == null) return AjaxResult.error("申请不存在");
return AjaxResult.success(data);
}
@RequiresPermissions("goblin:artist:auth:info")
@GetMapping("detailView/{applyId}")
public String detailView(@PathVariable String applyId, ModelMap mmap) {
Map<String, Object> data = buildDetail(applyId);
if (data == null) return prefix + "/auth";
mmap.put("apply", data);
return prefix + "/detail";
}
private Map<String, Object> buildDetail(String applyId) {
GoblinArtistAuthApply apply = authAdminService.getOne(
new LambdaQueryWrapper<GoblinArtistAuthApply>()
.eq(GoblinArtistAuthApply::getApplyId, applyId).last("limit 1"));
if (apply == null) return null;
KylinArtist artist = kylinArtistMapper.selectOne(
new LambdaQueryWrapper<KylinArtist>()
.eq(KylinArtist::getArtistId, apply.getArtistId()).last("limit 1"));
GoblinStoreInfo store = storeInfoMapper.selectOne(
new LambdaQueryWrapper<GoblinStoreInfo>()
.eq(GoblinStoreInfo::getStoreId, apply.getStoreId()).last("limit 1"));
Map<String, Object> data = new LinkedHashMap<>();
data.put("applyId", apply.getApplyId());
data.put("status", apply.getStatus());
data.put("createdAt", apply.getCreatedAt());
data.put("applyTime", formatTime(apply.getCreatedAt()));
data.put("artistId", apply.getArtistId());
data.put("artistName", artist != null ? artist.getArtistName() : null);
data.put("storeId", apply.getStoreId());
data.put("storeName", store != null ? store.getStoreName() : null);
data.put("reviewerName", apply.getReviewerName());
data.put("reviewTime", formatTime(apply.getReviewTime()));
data.put("reviewRemark", apply.getReviewRemark());
data.put("rejectReason", apply.getRejectReason());
data.put("authExpireAt", formatTime(apply.getAuthExpireAt()));
List<String> urls = new ArrayList<>();
if (StringUtils.isNotBlank(apply.getAttachmentUrls())) {
try {
urls = JsonUtils.fromJson(apply.getAttachmentUrls(), new TypeReference<List<String>>() {});
} catch (Exception ignored) {}
}
data.put("attachmentUrls", urls != null ? urls : Collections.emptyList());
return data;
}
@Log(title = "艺人授权:通过", businessType = BusinessType.UPDATE)
@RequiresPermissions("goblin:artist:auth:approve")
@PostMapping("approve")
@ResponseBody
public AjaxResult approve(@RequestParam String applyId,
@RequestParam @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime expireAt,
@RequestParam(required = false) String remark) {
String reviewerName = ShiroUtils.getLoginName();
String reviewerId = ShiroUtils.getUserId().toString();
return toAjax(authAdminService.approve(applyId, expireAt, remark, reviewerId, reviewerName));
}
@Log(title = "艺人授权:驳回", businessType = BusinessType.UPDATE)
@RequiresPermissions("goblin:artist:auth:reject")
@PostMapping("reject")
@ResponseBody
public AjaxResult reject(@RequestParam String applyId, @RequestParam String reason) {
String reviewerName = ShiroUtils.getLoginName();
String reviewerId = ShiroUtils.getUserId().toString();
return toAjax(authAdminService.reject(applyId, reason, reviewerId, reviewerName));
}
}
package com.liquidnet.client.admin.web.controller.zhengzai.goblin.artist;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.liquidnet.client.admin.common.annotation.Log;
import com.liquidnet.client.admin.common.core.controller.BaseController;
import com.liquidnet.client.admin.common.core.domain.AjaxResult;
import com.liquidnet.client.admin.common.core.page.TableDataInfo;
import com.liquidnet.client.admin.common.enums.BusinessType;
import com.liquidnet.client.admin.zhengzai.goblin.service.IGoblinArtistTopicAdminService;
import com.liquidnet.service.goblin.entity.GoblinArtistTopic;
import com.liquidnet.service.goblin.entity.GoblinStoreInfo;
import com.liquidnet.service.goblin.mapper.GoblinStoreInfoMapper;
import com.liquidnet.service.kylin.entity.KylinArtist;
import com.liquidnet.service.kylin.mapper.KylinArtistMapper;
import org.apache.commons.lang3.StringUtils;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap;
import org.springframework.web.bind.annotation.*;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.stream.Collectors;
import static com.liquidnet.client.admin.web.controller.zhengzai.goblin.artist.ArtistAdminHelper.*;
@Controller
@RequestMapping("goblin/artist/topic")
public class GoblinArtistTopicAdminController extends BaseController {
private static final DateTimeFormatter DATE_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd");
private final String prefix = "zhengzai/goblin/artist/topic";
@Autowired
private IGoblinArtistTopicAdminService topicAdminService;
@Autowired
private KylinArtistMapper kylinArtistMapper;
@Autowired
private GoblinStoreInfoMapper storeInfoMapper;
@GetMapping()
public String index() {
return prefix + "/topic";
}
@RequiresPermissions("goblin:artist:topic:list")
@RequestMapping("/list")
@ResponseBody
public TableDataInfo list(GoblinArtistTopic topic,
@RequestParam(required = false) String artistName,
@RequestParam(required = false) String storeName) {
LambdaQueryWrapper<GoblinArtistTopic> qw = Wrappers.lambdaQuery(GoblinArtistTopic.class);
qw.eq(GoblinArtistTopic::getDelFlg, 0);
if (topic.getStatus() != null) {
qw.eq(GoblinArtistTopic::getStatus, topic.getStatus());
}
if (StringUtils.isNotBlank(topic.getTopicName())) {
qw.like(GoblinArtistTopic::getTopicName, topic.getTopicName());
}
List<String> sids = findStoreIds(storeInfoMapper, storeName);
if (sids != null) {
if (sids.isEmpty()) return getDataTable(Collections.emptyList());
qw.in(GoblinArtistTopic::getStoreId, sids);
}
List<String> aids = findArtistIds(kylinArtistMapper, artistName);
if (aids != null) {
if (aids.isEmpty()) return getDataTable(Collections.emptyList());
qw.in(GoblinArtistTopic::getArtistId, aids);
}
qw.orderByAsc(GoblinArtistTopic::getSort);
qw.orderByDesc(GoblinArtistTopic::getCreatedAt);
startPage();
return getDataTable(withNames(topicAdminService.list(qw)));
}
private List<Map<String, Object>> withNames(List<GoblinArtistTopic> list) {
if (list.isEmpty()) return Collections.emptyList();
Map<String, String> storeNames = storeNameMap(storeInfoMapper, idsOf(list, GoblinArtistTopic::getStoreId));
Map<String, String> artistNames = artistNameMap(kylinArtistMapper, idsOf(list, GoblinArtistTopic::getArtistId));
Map<String, Integer> goodsCounts = topicAdminService.goodsCountMap(idsOf(list, GoblinArtistTopic::getTopicId));
return list.stream().map(item -> {
Map<String, Object> row = new LinkedHashMap<>();
row.put("topicId", item.getTopicId());
row.put("topicName", item.getTopicName());
row.put("artistId", item.getArtistId());
row.put("artistName", artistNames.get(item.getArtistId()));
row.put("storeId", item.getStoreId());
row.put("storeName", storeNames.get(item.getStoreId()));
row.put("status", item.getStatus());
row.put("sort", item.getSort());
row.put("recommendPosition", item.getRecommendPosition());
row.put("goodsCount", goodsCounts.getOrDefault(item.getTopicId(), 0));
row.put("createdAt", formatTime(item.getCreatedAt()));
row.put("onSaleStart", formatDate(item.getOnSaleStart()));
row.put("onSaleEnd", formatDate(item.getOnSaleEnd()));
return row;
}).collect(Collectors.toList());
}
private static String formatDate(LocalDateTime time) {
return time != null ? time.format(DATE_FMT) : null;
}
@RequiresPermissions("goblin:artist:topic:info")
@GetMapping("detail/{topicId}")
public String detail(@PathVariable String topicId, ModelMap mmap) {
Map<String, Object> topic = topicAdminService.topicDetail(topicId);
if (topic == null) {
return prefix + "/topic";
}
mmap.put("topic", formatDetailTime(topic));
String artistId = (String) topic.get("artistId");
if (StringUtils.isNotBlank(artistId)) {
KylinArtist artist = kylinArtistMapper.selectOne(
new LambdaQueryWrapper<KylinArtist>()
.eq(KylinArtist::getArtistId, artistId).last("limit 1"));
mmap.put("artist", artist);
}
String storeId = (String) topic.get("storeId");
if (StringUtils.isNotBlank(storeId)) {
mmap.put("store", storeInfoMapper.selectOne(
new LambdaQueryWrapper<GoblinStoreInfo>()
.eq(GoblinStoreInfo::getStoreId, storeId).last("limit 1")));
}
return prefix + "/detail";
}
private Map<String, Object> formatDetailTime(Map<String, Object> topic) {
Map<String, Object> copy = new LinkedHashMap<>(topic);
copy.put("createdAt", formatTime((LocalDateTime) copy.get("createdAt")));
copy.put("onSaleStart", formatTime((LocalDateTime) copy.get("onSaleStart")));
copy.put("onSaleEnd", formatTime((LocalDateTime) copy.get("onSaleEnd")));
return copy;
}
@Log(title = "艺人专题:设置排序", businessType = BusinessType.UPDATE)
@RequiresPermissions("goblin:artist:topic:sort")
@PostMapping("sort")
@ResponseBody
public AjaxResult setSort(@RequestParam String topicId, @RequestParam int sort) {
return toAjax(topicAdminService.setSort(topicId, sort));
}
@Log(title = "艺人专题:设置推荐位", businessType = BusinessType.UPDATE)
@RequiresPermissions("goblin:artist:topic:recommend")
@PostMapping("recommend")
@ResponseBody
public AjaxResult setRecommend(@RequestParam String topicId, @RequestParam int position) {
return toAjax(topicAdminService.setRecommend(topicId, position));
}
@Log(title = "艺人专题:上下架", businessType = BusinessType.UPDATE)
@RequiresPermissions("goblin:artist:topic:status")
@PostMapping("status")
@ResponseBody
public AjaxResult setStatus(@RequestParam String topicId, @RequestParam int status) {
return toAjax(topicAdminService.setStatus(topicId, status));
}
@Log(title = "艺人专题:删除", businessType = BusinessType.DELETE)
@RequiresPermissions("goblin:artist:topic:delete")
@PostMapping("delete")
@ResponseBody
public AjaxResult deleteTopic(@RequestParam String topicId) {
return toAjax(topicAdminService.deleteTopic(topicId));
}
}
<!DOCTYPE html>
<html lang="zh" xmlns:th="http://www.thymeleaf.org" xmlns:shiro="http://www.pollix.at/thymeleaf/shiro">
<head>
<th:block th:include="include :: header('艺人授权审核')" />
<style>
/* 状态标签样式 (匹配图1) */
.badge-status {
display: inline-block;
padding: 3px 12px;
border-radius: 12px;
font-size: 12px;
font-weight: 500;
line-height: 1.4;
}
.badge-status-warning {
background-color: #fef8e7;
color: #d99b00;
}
.badge-status-success {
background-color: #e6f7eb;
color: #52c41a;
}
.badge-status-danger {
background-color: #fff2f0;
color: #ff4d4f;
}
.badge-status-gray {
background-color: #f5f5f5;
color: #8c8c8c;
}
/* 操作按钮样式 (匹配图1) */
.action-cell {
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
}
.btn-act {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 2px 10px;
font-size: 12px;
border-radius: 4px;
cursor: pointer;
text-decoration: none !important;
transition: all 0.2s;
}
.btn-act i {
margin-right: 3px;
}
.btn-act-detail {
background-color: #e6f7ff;
color: #1890ff;
border: 1px solid #91d5ff;
}
.btn-act-detail:hover {
background-color: #bae7ff;
color: #096dd9;
}
.btn-act-approve {
background-color: #e6f7eb;
color: #52c41a;
border: 1px solid #b7eb8f;
}
.btn-act-approve:hover {
background-color: #d9f7be;
color: #389e0d;
}
.btn-act-reject {
background-color: #fff2f0;
color: #ff4d4f;
border: 1px solid #ffccc7;
}
.btn-act-reject:hover {
background-color: #ffedd5;
color: #cf1322;
}
/* 弹框自定义样式 (匹配图2图3) */
.custom-modal-body {
padding: 20px 24px;
background: #fff;
}
.modal-form-item {
margin-bottom: 18px;
}
.modal-label {
display: block;
font-size: 14px;
font-weight: bold;
color: #262626;
margin-bottom: 8px;
}
.required-star {
color: #ff4d4f;
margin-right: 4px;
}
.date-input-wrap {
position: relative;
}
.date-input-wrap .modal-input {
width: 100%;
height: 38px;
padding: 6px 36px 6px 12px;
border: 1px solid #d9d9d9;
border-radius: 6px;
background-color: #fff;
cursor: pointer;
font-size: 14px;
}
.date-input-wrap .date-icon {
position: absolute;
right: 12px;
top: 11px;
color: #8c8c8c;
pointer-events: none;
}
.modal-textarea {
width: 100%;
border: 1px solid #d9d9d9;
border-radius: 6px;
padding: 8px 12px;
font-size: 14px;
resize: vertical;
}
.modal-textarea::placeholder, .modal-input::placeholder {
color: #bfbfbf;
}
.modal-footer-btns {
display: flex;
justify-content: flex-end;
gap: 12px;
margin-top: 24px;
padding-top: 16px;
border-top: 1px solid #f0f0f0;
}
.btn-modal-cancel {
background: #e6e6e6;
color: #333;
border: none;
padding: 7px 22px;
border-radius: 4px;
font-size: 14px;
cursor: pointer;
transition: background 0.2s;
}
.btn-modal-cancel:hover {
background: #d9d9d9;
}
.btn-modal-approve {
background: #1890ff;
color: #fff;
border: none;
padding: 7px 22px;
border-radius: 4px;
font-size: 14px;
cursor: pointer;
transition: background 0.2s;
}
.btn-modal-approve:hover {
background: #40a9ff;
}
.btn-modal-reject {
background: #ff4d4f;
color: #fff;
border: none;
padding: 7px 22px;
border-radius: 4px;
font-size: 14px;
cursor: pointer;
transition: background 0.2s;
}
.btn-modal-reject:hover {
background: #ff7875;
}
</style>
</head>
<body class="gray-bg">
<div class="container-div">
<div class="row">
<div class="col-sm-12 search-collapse">
<form id="role-form">
<div class="select-list">
<ul>
<li>审核状态:<select name="status">
<option value="">全部</option>
<option value="0">待审核</option>
<option value="1">已通过</option>
<option value="2">已拒绝</option>
<option value="3">已撤销</option>
</select></li>
<li>艺人名称:<input type="text" name="artistName" placeholder="请输入艺人名称"/></li>
<li>店铺名称:<input type="text" name="storeName" placeholder="请输入店铺名称"/></li>
<li>
<a class="btn btn-primary btn-rounded btn-sm" onclick="$.table.search()"><i class="fa fa-search"></i> 搜索</a>
<a class="btn btn-warning btn-rounded btn-sm" onclick="$.form.reset()"><i class="fa fa-refresh"></i> 重置</a>
</li>
</ul>
</div>
</form>
</div>
<div class="col-sm-12 select-table table-striped">
<table id="bootstrap-table"></table>
</div>
</div>
<!-- 驳回弹框 (图3) -->
<div id="addClassify" style="display:none;" class="custom-modal-body">
<div class="modal-form-item">
<label class="modal-label"><span class="required-star">*</span> 驳回原因</label>
<textarea id="reason" class="modal-textarea" rows="3" placeholder="请输入驳回原因"></textarea>
</div>
<div class="modal-footer-btns">
<button type="button" class="btn-modal-cancel" onclick="closeModal()">取 消</button>
<button type="button" class="btn-modal-reject" onclick="yes()">确认驳回</button>
</div>
</div>
<!-- 通过弹框 (图2) -->
<div id="approveModal" style="display:none;" class="custom-modal-body">
<div class="modal-form-item">
<label class="modal-label"><span class="required-star">*</span> 授权截止时间</label>
<div class="date-input-wrap">
<input type="text" id="expireAt" class="modal-input" placeholder="年 / 月 / 日 --:--" readonly>
<i class="fa fa-calendar date-icon"></i>
</div>
</div>
<div class="modal-form-item">
<label class="modal-label">备注</label>
<textarea id="remark" class="modal-textarea" rows="3" placeholder="请输入备注信息(选填)"></textarea>
</div>
<div class="modal-footer-btns">
<button type="button" class="btn-modal-cancel" onclick="closeModal()">取 消</button>
<button type="button" class="btn-modal-approve" onclick="approveYes()">确认通过</button>
</div>
</div>
</div>
<th:block th:include="include :: footer" />
<script th:inline="javascript">
var prefix = ctx + "goblin/artist/auth";
var currentApplyId = '';
var infoFlg = [[${@permission.hasPermi('goblin:artist:auth:info')}]];
function statusHtml(v) {
if (v == 0) {
return '<span class="badge-status badge-status-warning">待审核</span>';
} else if (v == 1) {
return '<span class="badge-status badge-status-success">已通过</span>';
} else if (v == 2) {
return '<span class="badge-status badge-status-danger">已驳回</span>';
} else {
return '<span class="badge-status badge-status-gray">已撤销</span>';
}
}
function timeHtml(v) {
if (!v) return '-';
var s = String(v).replace('T', ' ');
if (s.length > 19) { s = s.substring(0, 19); }
return s;
}
$(function() {
var options = {
url: prefix + "/list",
detailUrl: prefix + "/detailView/{id}",
modalName: "授权申请",
sortName: "createdAt",
sortOrder: "desc",
columns: [
{ field: 'applyId', title: '申请ID' },
{ field: 'storeName', title: '申请商家', formatter: function(v, row) { return v || row.storeId || '-'; }},
{ field: 'artistName', title: '关联艺人', formatter: function(v, row) { return v || row.artistId || '-'; }},
{ field: 'createdAt', title: '申请时间', formatter: function(v) { return timeHtml(v); }},
{ field: 'status', title: '审批状态', align: 'center', formatter: function(v) { return statusHtml(v); }},
{ field: 'reviewerName', title: '审批人', formatter: function(v) { return v || '-'; }},
{ field: 'reviewTime', title: '审批时间', formatter: function(v) { return timeHtml(v); }},
{ field: 'authExpireAt', title: '授权截止时间', formatter: function(v) { return timeHtml(v); }},
{ title: '操作', align: 'center', formatter: function(v, row) {
var html = '<div class="action-cell">';
html += '<a class="btn-act btn-act-detail ' + infoFlg + '" onclick="$.operate.detail(\'' + row.applyId + '\')"><i class="fa fa-eye"></i> 详情</a>';
if (row.status == 0) {
html += '<a class="btn-act btn-act-approve ' + infoFlg + '" onclick="auditApprove(\'' + row.applyId + '\')"><i class="fa fa-check"></i> 通过</a>';
html += '<a class="btn-act btn-act-reject ' + infoFlg + '" onclick="auditReject(\'' + row.applyId + '\')"><i class="fa fa-times"></i> 驳回</a>';
}
html += '</div>';
return html;
}}
]
};
$.table.init(options);
});
function auditApprove(applyId) {
currentApplyId = applyId;
$("#expireAt").val('');
$("#remark").val('');
var initPicker = function() {
var datePicker = (typeof layui !== 'undefined' && layui.laydate) ? layui.laydate : (typeof window.laydate !== 'undefined' ? window.laydate : null);
if (datePicker) {
datePicker.render({ elem: '#expireAt', type: 'datetime', format: 'yyyy-MM-dd HH:mm:ss', min: 0, trigger: 'click' });
}
};
if (typeof layui !== 'undefined') {
layui.use('laydate', function() { initPicker(); });
} else {
initPicker();
}
layer.open({
type: 1,
title: '审批通过',
area: ['480px', 'auto'],
shade: 0.3,
content: $('#approveModal')
});
}
function approveYes() {
var expireAt = $("#expireAt").val();
if (!expireAt) { layer.msg('请选择授权截止时间'); return; }
$.ajax({
type: 'post',
url: prefix + "/approve",
data: { applyId: currentApplyId, expireAt: expireAt, remark: $("#remark").val() },
success: function() {
layer.closeAll();
$.table.refresh();
}
});
}
function auditReject(applyId) {
currentApplyId = applyId;
$("#reason").val('');
layer.open({
type: 1,
title: '审批驳回',
area: ['480px', 'auto'],
shade: 0.3,
content: $('#addClassify')
});
}
function yes() {
var reason = $("#reason").val();
if (!reason) { layer.msg('请填写驳回原因'); return; }
$.ajax({
type: 'post',
url: prefix + "/reject",
data: { applyId: currentApplyId, reason: reason },
success: function() {
layer.closeAll();
$.table.refresh();
}
});
}
function closeModal() { layer.closeAll(); }
</script>
</body>
</html>
<!DOCTYPE html>
<html lang="zh" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>授权申请详情</title>
<link th:href="@{/css/bootstrap.min.css}" rel="stylesheet"/>
<link th:href="@{/css/font-awesome.min.css}" rel="stylesheet"/>
<link th:href="@{/css/animate.css}" rel="stylesheet"/>
<link th:href="@{/css/style.css?v=20200903}" rel="stylesheet"/>
<link th:href="@{/ruoyi/css/ry-ui.css?v=4.6.1}" rel="stylesheet"/>
<style>
:root {
--paper: #F7F8FA;
--ink: #1F2937;
--ink-dim: #6B7280;
--line: #E5E7EB;
--blue: #2563EB;
--amber: #D97706;
--green: #0F766E;
--red: #DC2626;
--gray: #6B7280;
}
body {
background: var(--paper);
font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", "Microsoft YaHei", "Segoe UI", sans-serif;
color: var(--ink);
}
.mono {
font-family: ui-monospace, SFMono-Regular, "JetBrains Mono", Consolas, "Courier New", monospace;
letter-spacing: .02em;
}
.auth-page { max-width: 680px; margin: 0 auto; padding: 28px 24px 48px; }
/* ---------- 区块卡片 ---------- */
.auth-card {
background: #fff; border: 1px solid var(--line); border-radius: 10px;
padding: 8px 24px 12px; margin-bottom: 18px;
box-shadow: 0 1px 2px rgba(31,41,55,.04);
}
.auth-card-title {
font-size: 14px; font-weight: 700; color: var(--ink);
padding: 16px 0 12px; border-bottom: 1px solid var(--line); margin-bottom: 4px;
}
/* 每条一行 */
.auth-row {
display: flex; align-items: flex-start; gap: 16px;
padding: 10px 0; border-bottom: 1px solid #F1F2F4;
}
.auth-row:last-child { border-bottom: none; }
.auth-row-label {
width: 88px; flex-shrink: 0; font-size: 13px; color: var(--ink-dim);
padding-top: 1px;
}
.auth-row-value {
flex: 1; font-size: 14px; color: var(--ink); word-break: break-all; line-height: 1.5;
}
.auth-row-value.placeholder { color: #9CA3AF; }
/* 状态徽章 */
.auth-badge {
display: inline-block; padding: 2px 12px; border-radius: 999px;
font-size: 12px; font-weight: 600; line-height: 1.6;
}
.badge-pending { background: #FFF3E0; color: #B45309; }
.badge-approved { background: #E6F6F3; color: #0F766E; }
.badge-rejected { background: #FDE8E8; color: #C53030; }
.badge-revoked { background: #F3F4F6; color: #6B7280; }
/* ---------- 附件画廊(横向排列) ---------- */
.auth-gallery { display: flex; flex-wrap: wrap; gap: 12px; padding: 16px 0 20px; }
.auth-attach {
width: 96px; height: 96px; border: 1px solid var(--line); border-radius: 8px;
overflow: hidden; cursor: zoom-in; background: #fff;
display: flex; align-items: center; justify-content: center;
transition: border-color .2s ease, transform .2s ease, box-shadow .2s ease;
}
.auth-attach:hover { border-color: var(--blue); transform: translateY(-2px); box-shadow: 0 4px 12px rgba(37,99,235,.12); }
.auth-attach img { max-width: 100%; max-height: 100%; object-fit: contain; }
.auth-attach-ico { font-size: 26px; color: #C4C9D1; }
.auth-empty {
padding: 24px 16px; text-align: center; color: #9CA3AF; font-size: 13px;
border: 1px dashed var(--line); border-radius: 8px; background: #FAFBFC; margin-bottom: 20px;
}
.auth-empty i { display: block; font-size: 26px; margin-bottom: 6px; }
/* ---------- 图片放大灯箱 ---------- */
.auth-lightbox {
display: none; position: fixed; inset: 0; z-index: 99999;
background: rgba(17,24,39,.88); align-items: center; justify-content: center;
}
.auth-lightbox.active { display: flex; }
.auth-lightbox img {
max-width: 92vw; max-height: 92vh; object-fit: contain;
border-radius: 8px; box-shadow: 0 8px 40px rgba(0,0,0,.5);
}
.auth-lightbox-close {
position: fixed; top: 20px; right: 28px; font-size: 36px; color: #fff;
cursor: pointer; line-height: 1; user-select: none; opacity: .85;
}
.auth-lightbox-close:hover { opacity: 1; }
@media (max-width: 640px) {
.auth-page { padding: 20px 14px 32px; }
.auth-card { padding: 8px 16px 12px; }
}
@media (prefers-reduced-motion: reduce) {
.auth-attach { transition: none; }
}
</style>
</head>
<body class="white-bg">
<div class="auth-page" th:object="${apply}">
<!-- 基本信息 -->
<div class="auth-card">
<div class="auth-card-title">基本信息</div>
<div class="auth-row">
<div class="auth-row-label">申请ID:</div>
<div class="auth-row-value mono" th:text="*{applyId} ?: '--'">A202601010001</div>
</div>
<div class="auth-row">
<div class="auth-row-label">申请商家:</div>
<div class="auth-row-value" th:text="*{storeName} ?: '--'">摩登天空</div>
</div>
<div class="auth-row">
<div class="auth-row-label">关联艺人:</div>
<div class="auth-row-value" th:text="*{artistName} ?: '-'">陈粒</div>
</div>
<div class="auth-row">
<div class="auth-row-label">申请时间:</div>
<div class="auth-row-value mono">
<span th:if="*{applyTime}" th:text="*{applyTime}">2026-01-01 10:00:00</span>
<span th:if="!*{applyTime}">--</span>
</div>
</div>
</div>
<!-- 附件信息 -->
<div class="auth-card" th:if="*{attachmentUrls != null and !attachmentUrls.isEmpty()}">
<div class="auth-card-title">附件信息</div>
<div class="auth-gallery">
<div class="auth-attach" th:each="url : ${apply.attachmentUrls}" th:title="点击放大查看">
<img th:src="${url}" alt="附件材料"
onerror="this.style.display='none';this.parentElement.innerHTML='<i class=&quot;fa fa-file-image-o auth-attach-ico&quot;></i>'"
onclick="authLightbox.open(this.src)"/>
</div>
</div>
</div>
<div class="auth-empty" th:if="*{attachmentUrls == null or attachmentUrls.isEmpty()}">
<i class="fa fa-file-o"></i>
暂无附件材料
</div>
<!-- 审批信息 -->
<div class="auth-card">
<div class="auth-card-title">审批信息</div>
<div class="auth-row">
<div class="auth-row-label">申请状态:</div>
<div class="auth-row-value">
<span class="auth-badge" th:classappend="*{status == 0 ? 'badge-pending' : (status == 1 ? 'badge-approved' : (status == 2 ? 'badge-rejected' : 'badge-revoked'))}">
<th:block th:if="*{status == 0}">待审核</th:block>
<th:block th:if="*{status == 1}">已通过</th:block>
<th:block th:if="*{status == 2}">已拒绝</th:block>
<th:block th:if="*{status == 3}">已撤销</th:block>
</span>
</div>
</div>
<div class="auth-row">
<div class="auth-row-label">审批人:</div>
<div class="auth-row-value" th:text="*{reviewerName} ?: '--'">admin</div>
</div>
<div class="auth-row">
<div class="auth-row-label">审批时间:</div>
<div class="auth-row-value mono">
<span th:if="*{reviewTime}" th:text="*{reviewTime}">2026-01-02 09:30:00</span>
<span th:if="!*{reviewTime}">--</span>
</div>
</div>
<div class="auth-row">
<div class="auth-row-label">授权截止:</div>
<div class="auth-row-value mono">
<span th:if="*{authExpireAt}" th:text="*{authExpireAt}">2027-01-01 00:00:00</span>
<span th:if="!*{authExpireAt}">--</span>
</div>
</div>
<div class="auth-row" th:if="*{reviewRemark}">
<div class="auth-row-label">备注:</div>
<div class="auth-row-value" th:text="*{reviewRemark}">通过</div>
</div>
<div class="auth-row" th:if="*{rejectReason}">
<div class="auth-row-label">驳回原因:</div>
<div class="auth-row-value" th:text="*{rejectReason}">材料不完整</div>
</div>
</div>
</div>
<!-- 图片放大灯箱 -->
<div id="auth-lightbox" class="auth-lightbox" onclick="authLightbox.close()">
<span class="auth-lightbox-close" onclick="authLightbox.close()">&times;</span>
<img id="auth-lightbox-img" src="" alt="" onclick="event.stopPropagation()">
</div>
<script>
var authLightbox = {
open: function (src) {
if (!src) return;
var lb = document.getElementById('auth-lightbox');
document.getElementById('auth-lightbox-img').src = src;
lb.classList.add('active');
document.body.style.overflow = 'hidden';
},
close: function () {
var lb = document.getElementById('auth-lightbox');
lb.classList.remove('active');
document.getElementById('auth-lightbox-img').src = '';
document.body.style.overflow = '';
}
};
document.addEventListener('keydown', function (e) {
if (e.key === 'Escape') { authLightbox.close(); }
});
</script>
</body>
</html>
<!DOCTYPE html>
<html lang="zh" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="utf-8">
<title>艺人专题详情</title>
<link th:href="@{/css/bootstrap.min.css}" rel="stylesheet"/>
<style>
body {
background-color: #fff;
color: #333;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
font-size: 14px;
padding: 24px 32px;
margin: 0;
}
.detail-container {
padding-bottom: 60px;
}
.detail-section {
margin-bottom: 24px;
}
.detail-section-title {
font-size: 15px;
font-weight: bold;
color: #262626;
padding-bottom: 10px;
margin-bottom: 16px;
border-bottom: 1px solid #f0f0f0;
}
.detail-list {
display: flex;
flex-direction: column;
gap: 14px;
}
.detail-item {
display: flex;
align-items: center;
line-height: 1.5;
}
.detail-item .label-text {
color: #8c8c8c;
width: 85px;
flex-shrink: 0;
white-space: nowrap;
}
.detail-item .value-text {
color: #262626;
margin-left: 12px;
word-break: break-all;
}
.goods-table {
width: 100%;
border-collapse: collapse;
margin-top: 12px;
}
.goods-table th, .goods-table td {
padding: 12px 16px;
text-align: left;
vertical-align: middle;
border-bottom: 1px solid #f0f0f0;
}
.goods-table th {
background: #fafafa;
font-weight: bold;
color: #262626;
border-top: none;
border-left: none;
border-right: none;
}
.goods-cover-wrap {
width: 52px;
height: 52px;
}
.goods-cover {
width: 52px;
height: 52px;
object-fit: cover;
border-radius: 4px;
display: block;
}
.goods-cover-placeholder {
width: 52px;
height: 52px;
border-radius: 4px;
background: #5c62d6;
color: #fff;
font-size: 12px;
font-weight: bold;
display: flex;
align-items: center;
justify-content: center;
}
.goods-name {
color: #262626;
}
.goods-spec {
color: #595959;
}
.goods-price {
color: #ff4d4f;
font-weight: bold;
}
.detail-empty {
color: #8c8c8c;
text-align: center;
padding: 32px 0;
font-size: 14px;
}
.detail-footer {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: #fff;
padding: 12px 24px;
text-align: right;
border-top: 1px solid #f0f0f0;
z-index: 10;
}
.btn-close {
background: #e0e0e0;
border: none;
color: #333;
font-size: 14px;
padding: 6px 22px;
border-radius: 4px;
cursor: pointer;
outline: none;
transition: background 0.2s;
}
.btn-close:hover {
background: #d4d4d4;
}
</style>
</head>
<body>
<div class="detail-container" th:object="${topic}">
<!-- 基本信息 -->
<div class="detail-section">
<div class="detail-section-title">基本信息</div>
<div class="detail-list">
<div class="detail-item">
<span class="label-text">专题名称:</span>
<span class="value-text" th:text="*{topicName} ?: '-'">-</span>
</div>
<div class="detail-item">
<span class="label-text">所属商家:</span>
<span class="value-text" th:text="${store != null ? store.storeName : '-'}">-</span>
</div>
<div class="detail-item">
<span class="label-text">关联艺人:</span>
<span class="value-text" th:text="${artist != null ? artist.artistName : '-'}">-</span>
</div>
<div class="detail-item">
<span class="label-text">创建时间:</span>
<span class="value-text" th:text="*{createdAt} ?: '-'">-</span>
</div>
</div>
</div>
<!-- 上架时间 -->
<div class="detail-section">
<div class="detail-section-title">上架时间</div>
<div class="detail-list">
<div class="detail-item">
<span class="label-text">开始时间:</span>
<span class="value-text" th:text="*{onSaleStart} ?: '-'">-</span>
</div>
<div class="detail-item">
<span class="label-text">结束时间:</span>
<span class="value-text" th:text="*{onSaleEnd} ?: '-'">-</span>
</div>
</div>
</div>
<!-- 关联商品 -->
<div class="detail-section">
<div class="detail-section-title" th:text="|关联商品(共 ${topic.goods != null ? #lists.size(topic.goods) : 0} 个)|">关联商品(共0个)</div>
<table class="goods-table" th:if="${topic.goods != null and !#lists.isEmpty(topic.goods)}">
<thead>
<tr>
<th style="width: 90px;">商品头图</th>
<th>商品名称</th>
<th>商品规格</th>
<th style="width: 120px;">商品价格</th>
</tr>
</thead>
<tbody>
<tr th:each="good : ${topic.goods}">
<td>
<div class="goods-cover-wrap">
<img class="goods-cover" th:if="${good.coverPic != null and !#strings.isEmpty(good.coverPic)}"
th:src="${good.coverPic}" alt="商品图"
onerror="this.style.display='none'; this.nextElementSibling.style.display='flex';"/>
<div class="goods-cover-placeholder"
th:style="${good.coverPic != null and !#strings.isEmpty(good.coverPic)} ? 'display:none;' : 'display:flex;'">
商品图
</div>
</div>
</td>
<td class="goods-name" th:text="${good.name} ?: '-'">-</td>
<td class="goods-spec" th:text="${good.spec} ?: '-'">-</td>
<td class="goods-price"
th:text="${good.sellPrice != null and !#strings.isEmpty(good.sellPrice) ? '¥' + good.sellPrice : '-'}">-</td>
</tr>
</tbody>
</table>
<div class="detail-empty" th:if="${topic.goods == null or #lists.isEmpty(topic.goods)}">暂未配置商品</div>
</div>
<!-- 底部关闭按钮 -->
<div class="detail-footer">
<button type="button" class="btn-close" onclick="closeDialog()">关 闭</button>
</div>
</div>
<script>
function closeDialog() {
var index = parent.layer.getFrameIndex(window.name);
parent.layer.close(index);
}
</script>
</body>
</html>
<!DOCTYPE html>
<html lang="zh" xmlns:th="http://www.thymeleaf.org">
<head>
<th:block th:include="include :: header('艺人专题管理')" />
<style>
/* 状态与推荐 标签样式 (匹配图2) */
.badge-status {
display: inline-block;
padding: 3px 12px;
border-radius: 12px;
font-size: 12px;
font-weight: 500;
line-height: 1.4;
}
.badge-status-success {
background-color: #e6f7eb;
color: #52c41a;
}
.badge-status-warning {
background-color: #fef8e7;
color: #d99b00;
}
.badge-status-gray {
background-color: #f5f5f5;
color: #8c8c8c;
}
.badge-rec {
display: inline-block;
padding: 3px 10px;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
line-height: 1.4;
}
.badge-rec-active {
background-color: #e6f7ff;
color: #1890ff;
}
.badge-rec-none {
background-color: #f5f5f5;
color: #8c8c8c;
}
/* 操作按钮样式 (匹配图2) */
.action-cell {
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
}
.action-row-top {
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
}
.btn-act {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 2px 8px;
font-size: 12px;
border-radius: 4px;
cursor: pointer;
text-decoration: none !important;
transition: all 0.2s;
}
.btn-act i {
margin-right: 3px;
}
.btn-act-detail {
background-color: #e6f7ff;
color: #1890ff;
border: 1px solid #91d5ff;
}
.btn-act-detail:hover {
background-color: #bae7ff;
color: #096dd9;
}
.btn-act-offsale {
background-color: #fff7e6;
color: #fa8c16;
border: 1px solid #ffd591;
}
.btn-act-offsale:hover {
background-color: #ffe7ba;
color: #d46b08;
}
.btn-act-rec {
background-color: #ffffff;
color: #595959;
border: 1px solid #d9d9d9;
padding: 2px 10px;
}
.btn-act-rec:hover {
border-color: #40a9ff;
color: #40a9ff;
}
/* 推荐设置下拉菜单 */
.rec-menu {
position: absolute; z-index: 9999; display: none;
min-width: 110px; padding: 5px 0;
background: #fff; border: 1px solid #e4e7ed; border-radius: 4px;
box-shadow: 0 2px 12px rgba(0, 0, 0, .12);
}
.rec-menu a {
display: block; padding: 6px 16px; font-size: 12px;
color: #606266; white-space: nowrap; cursor: pointer;
text-decoration: none !important;
}
.rec-menu a:hover { background: #f5f7fa; color: #1890ff; }
.rec-menu a.active { color: #1890ff; font-weight: bold; }
</style>
</head>
<body class="gray-bg">
<div class="container-div">
<div class="row">
<div class="col-sm-12 search-collapse">
<form id="role-form">
<div class="select-list">
<ul>
<li>专题名称:<input type="text" name="topicName" placeholder="请输入专题名称"/></li>
<li>关联艺人:<input type="text" name="artistName" placeholder="请输入艺人名称"/></li>
<li>专题状态:<select name="status">
<option value="">全部</option>
<option value="0">待上架</option>
<option value="1">上架中</option>
<option value="2">已下架</option>
</select></li>
<li>所属商家:<input type="text" name="storeName" placeholder="请输入商家名称"/></li>
<li>
<a class="btn btn-primary btn-rounded btn-sm" onclick="$.table.search()"><i class="fa fa-search"></i> 搜索</a>
<a class="btn btn-warning btn-rounded btn-sm" onclick="$.form.reset()"><i class="fa fa-refresh"></i> 重置</a>
</li>
</ul>
</div>
</form>
</div>
<div class="col-sm-12 select-table table-striped">
<table id="bootstrap-table"></table>
</div>
</div>
</div>
<th:block th:include="include :: footer" />
<script th:inline="javascript">
var prefix = ctx + "goblin/artist/topic";
var recommendFlg = [[${@permission.hasPermi('goblin:artist:topic:recommend')}]];
var statusFlg = [[${@permission.hasPermi('goblin:artist:topic:status')}]];
var infoFlg = [[${@permission.hasPermi('goblin:artist:topic:info')}]];
function statusHtml(v) {
if (v == 1) {
return '<span class="badge-status badge-status-success">上架中</span>';
} else if (v == 0) {
return '<span class="badge-status badge-status-warning">待上架</span>';
} else {
return '<span class="badge-status badge-status-gray">已下架</span>';
}
}
function recommendHtml(v) {
return v ? '<span class="badge-rec badge-rec-active">推荐' + v + '号位</span>'
: '<span class="badge-rec badge-rec-none">无推荐</span>';
}
function saleTimeHtml(row) {
if (!row.onSaleStart && !row.onSaleEnd) return '-';
return (row.onSaleStart || '--') + ' 至 ' + (row.onSaleEnd || '--');
}
$(function() {
$.table.init({
url: prefix + "/list",
sortName: "sort",
sortOrder: "asc",
columns: [
{ field: 'topicId', title: '专题ID' },
{ field: 'topicName', title: '专题名称' },
{ field: 'artistName', title: '关联艺人', formatter: function(v, row) { return v || row.artistId || '-'; }},
{ field: 'goodsCount', title: '商品数' },
{ field: 'storeName', title: '所属商家', formatter: function(v, row) { return v || row.storeId || '-'; }},
{ field: 'createdAt', title: '创建时间' },
{ field: 'onSaleStart', title: '上架时间', formatter: function(v, row) { return saleTimeHtml(row); }},
{ field: 'status', title: '专题状态', align: 'center', formatter: function(v) { return statusHtml(v); }},
{ field: 'recommendPosition', title: '推荐状态', align: 'center', formatter: function(v) { return recommendHtml(v); }},
{ title: '操作', align: 'center', formatter: function(v, row) {
var html = '<div class="action-cell">';
html += '<div class="action-row-top">';
html += '<a class="btn-act btn-act-detail ' + infoFlg + '" onclick="openDetail(\'' + row.topicId + '\')"><i class="fa fa-eye"></i> 详情</a>';
if (row.status == 1) {
html += '<a class="btn-act btn-act-offsale ' + statusFlg + '" onclick="changeStatus(\'' + row.topicId + '\',2)"><i class="fa fa-arrow-down"></i> 下架</a>';
}
html += '</div>';
html += '<a class="btn-act btn-act-rec ' + recommendFlg + '" onclick="openRecMenu(event, \'' + row.topicId + '\',' + (row.recommendPosition || 0) + ')">推荐设置 <i class="fa fa-caret-down"></i></a>';
html += '</div>';
return html;
}}
]
});
});
// ---------- 详情弹框 ----------
function openDetail(topicId) {
layer.open({
type: 2,
title: '艺人专题详情',
area: ['680px', '600px'],
shade: 0.3,
content: prefix + "/detail/" + topicId
});
}
// ---------- 下架 ----------
function changeStatus(topicId, status) {
$.modal.confirm("确认要将该专题下架吗?", function() {
$.ajax({
type: 'post', url: prefix + "/status",
data: { topicId: topicId, status: status },
success: function() { $.table.refresh(); }
});
});
}
// ---------- 推荐设置下拉菜单 ----------
var recPositions = [1, 2, 3, 4, 5];
var $recMenu = null;
function openRecMenu(event, topicId, curPos) {
event.stopPropagation();
closeRecMenu();
var html = '<div class="rec-menu">';
recPositions.forEach(function(pos) {
html += '<a data-val="' + pos + '"' + (pos == curPos ? ' class="active"' : '') + '>推荐' + pos + '号位</a>';
});
html += '<a data-val="0"' + (curPos == 0 ? ' class="active"' : '') + '>无推荐</a></div>';
$recMenu = $(html).appendTo('body');
var $btn = $(event.currentTarget);
$recMenu.css({
top: $btn.offset().top + $btn.outerHeight() + 4,
left: $btn.offset().left
}).show();
$recMenu.on('click', 'a', function() {
setRecommend(topicId, $(this).data('val'));
});
$(document).on('click.recMenu', closeRecMenu);
}
function closeRecMenu() {
if ($recMenu) { $recMenu.remove(); $recMenu = null; }
$(document).off('click.recMenu');
}
function setRecommend(topicId, position) {
closeRecMenu();
$.ajax({
type: 'post', url: prefix + "/recommend",
data: { topicId: topicId, position: position },
success: function() { $.table.refresh(); }
});
}
</script>
</body>
</html>
package com.liquidnet.client.admin.zhengzai.goblin.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.liquidnet.service.goblin.entity.GoblinArtistAuthApply;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
/**
* Admin 艺人授权审核
*/
public interface IGoblinArtistAuthAdminService extends IService<GoblinArtistAuthApply> {
/**
* 审核通过:更新申请表(status=1 + authExpireAt),同步写 MongoDB
*/
boolean approve(String applyId, LocalDateTime expireAt, String remark, String reviewerId, String reviewerName);
/**
* 审核驳回:更新申请表(status=2 + rejectReason),同步写 MongoDB
*/
boolean reject(String applyId, String reason, String reviewerId, String reviewerName);
/**
* 授权列表(MongoDB 查询,返回分页结果)
*/
List<Map<String, Object>> authList(int pageNum, int pageSize);
}
package com.liquidnet.client.admin.zhengzai.goblin.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.liquidnet.service.goblin.entity.GoblinArtistTopic;
import java.util.Collection;
import java.util.Map;
/**
* Admin 艺人专题管理
*/
public interface IGoblinArtistTopicAdminService extends IService<GoblinArtistTopic> {
/**
* 设置排序,同步写 MongoDB + MySQL
*/
boolean setSort(String topicId, int sort);
/**
* 设置推荐位,0~5,同步写 MongoDB + MySQL
*/
boolean setRecommend(String topicId, int position);
/**
* 上下架,同步写 MongoDB + MySQL
*/
boolean setStatus(String topicId, int status);
/**
* 逻辑删除,同步写 MongoDB + MySQL
*/
boolean deleteTopic(String topicId);
/**
* 专题详情(MySQL 读取:专题主表 + 商品关联表 + 商品/SKU 表,goods 附带商品规格)
*/
Map<String, Object> topicDetail(String topicId);
/**
* 批量查询专题的商品数量(MySQL 商品关联表)
*/
Map<String, Integer> goodsCountMap(Collection<String> topicIds);
}
package com.liquidnet.client.admin.zhengzai.goblin.service.impl;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.liquidnet.client.admin.zhengzai.goblin.service.IGoblinArtistAuthAdminService;
import com.liquidnet.service.goblin.constant.GoblinArtistConst;
import com.liquidnet.service.goblin.dto.vo.GoblinArtistAuthApplyVo;
import com.liquidnet.service.goblin.entity.GoblinArtistAuthApply;
import com.liquidnet.service.goblin.mapper.GoblinArtistAuthApplyMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.core.query.Update;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
@Slf4j
@Service
public class GoblinArtistAuthAdminServiceImpl
extends ServiceImpl<GoblinArtistAuthApplyMapper, GoblinArtistAuthApply>
implements IGoblinArtistAuthAdminService {
@Autowired
private MongoTemplate mongoTemplate;
@Override
@Transactional
public boolean approve(String applyId, LocalDateTime expireAt, String remark,
String reviewerId, String reviewerName) {
LocalDateTime now = LocalDateTime.now();
LambdaUpdateWrapper<GoblinArtistAuthApply> applyWrapper = Wrappers.lambdaUpdate(GoblinArtistAuthApply.class);
applyWrapper.eq(GoblinArtistAuthApply::getApplyId, applyId);
applyWrapper.set(GoblinArtistAuthApply::getStatus, GoblinArtistConst.APPLY_APPROVED);
applyWrapper.set(GoblinArtistAuthApply::getReviewerId, reviewerId);
applyWrapper.set(GoblinArtistAuthApply::getReviewerName, reviewerName);
applyWrapper.set(GoblinArtistAuthApply::getReviewTime, now);
applyWrapper.set(GoblinArtistAuthApply::getReviewRemark, remark);
applyWrapper.set(GoblinArtistAuthApply::getAuthExpireAt, expireAt);
applyWrapper.set(GoblinArtistAuthApply::getUpdatedAt, now);
if (!this.update(applyWrapper)) {
return false;
}
syncMongoApprove(applyId, expireAt, reviewerId, reviewerName, remark, now);
return true;
}
@Override
@Transactional
public boolean reject(String applyId, String reason, String reviewerId, String reviewerName) {
LocalDateTime now = LocalDateTime.now();
LambdaUpdateWrapper<GoblinArtistAuthApply> applyWrapper = Wrappers.lambdaUpdate(GoblinArtistAuthApply.class);
applyWrapper.eq(GoblinArtistAuthApply::getApplyId, applyId);
applyWrapper.set(GoblinArtistAuthApply::getStatus, GoblinArtistConst.APPLY_REJECTED);
applyWrapper.set(GoblinArtistAuthApply::getReviewerId, reviewerId);
applyWrapper.set(GoblinArtistAuthApply::getReviewerName, reviewerName);
applyWrapper.set(GoblinArtistAuthApply::getReviewTime, now);
applyWrapper.set(GoblinArtistAuthApply::getRejectReason, reason);
applyWrapper.set(GoblinArtistAuthApply::getUpdatedAt, now);
if (!this.update(applyWrapper)) {
return false;
}
syncMongoReject(applyId, reason, reviewerId, reviewerName, now);
return true;
}
@Override
public List<Map<String, Object>> authList(int pageNum, int pageSize) {
List<Map<String, Object>> result = new ArrayList<>();
return result;
}
private void syncMongoApprove(String applyId, LocalDateTime expireAt,
String reviewerId, String reviewerName,
String remark, LocalDateTime now) {
Query query = Query.query(Criteria.where("applyId").is(applyId));
Update update = Update.update("status", GoblinArtistConst.APPLY_APPROVED)
.set("reviewerId", reviewerId)
.set("reviewerName", reviewerName)
.set("reviewTime", now)
.set("reviewRemark", remark)
.set("authExpireAt", expireAt)
.set("updatedAt", now);
mongoTemplate.updateFirst(query, update, GoblinArtistAuthApplyVo.class.getSimpleName());
}
private void syncMongoReject(String applyId, String reason,
String reviewerId, String reviewerName, LocalDateTime now) {
Query query = Query.query(Criteria.where("applyId").is(applyId));
Update update = Update.update("status", GoblinArtistConst.APPLY_REJECTED)
.set("reviewerId", reviewerId)
.set("reviewerName", reviewerName)
.set("reviewTime", now)
.set("rejectReason", reason)
.set("updatedAt", now);
mongoTemplate.updateFirst(query, update, GoblinArtistAuthApplyVo.class.getSimpleName());
}
}
package com.liquidnet.client.admin.zhengzai.goblin.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.liquidnet.client.admin.zhengzai.goblin.service.IGoblinArtistTopicAdminService;
import com.liquidnet.common.cache.redis.util.RedisDataSourceUtil;
import com.liquidnet.service.goblin.constant.GoblinArtistConst;
import com.liquidnet.service.goblin.constant.GoblinRedisConst;
import com.liquidnet.service.goblin.dto.vo.GoblinArtistTopicVo;
import com.liquidnet.service.goblin.entity.GoblinArtistTopic;
import com.liquidnet.service.goblin.entity.GoblinArtistTopicGoods;
import com.liquidnet.service.goblin.entity.GoblinGoods;
import com.liquidnet.service.goblin.entity.GoblinGoodsSku;
import com.liquidnet.service.goblin.mapper.GoblinArtistTopicGoodsMapper;
import com.liquidnet.service.goblin.mapper.GoblinArtistTopicMapper;
import com.liquidnet.service.goblin.mapper.GoblinGoodsMapper;
import com.liquidnet.service.goblin.mapper.GoblinGoodsSkuMapper;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.core.query.Update;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.*;
import java.util.stream.Collectors;
@Slf4j
@Service
public class GoblinArtistTopicAdminServiceImpl
extends ServiceImpl<GoblinArtistTopicMapper, GoblinArtistTopic>
implements IGoblinArtistTopicAdminService {
@Autowired
private MongoTemplate mongoTemplate;
@Autowired
private RedisDataSourceUtil redisDataSourceUtil;
@Autowired
private GoblinArtistTopicGoodsMapper topicGoodsMapper;
@Autowired
private GoblinGoodsMapper goodsMapper;
@Autowired
private GoblinGoodsSkuMapper goodsSkuMapper;
@Override
@Transactional
public boolean setSort(String topicId, int sort) {
LocalDateTime now = LocalDateTime.now();
this.update(new LambdaUpdateWrapper<GoblinArtistTopic>()
.eq(GoblinArtistTopic::getTopicId, topicId)
.set(GoblinArtistTopic::getSort, sort)
.set(GoblinArtistTopic::getUpdatedAt, now));
mongoTemplate.updateFirst(Query.query(Criteria.where("topicId").is(topicId)),
Update.update("sort", sort).set("updatedAt", now),
GoblinArtistTopicVo.class.getSimpleName());
redisDataSourceUtil.getRedisGoblinUtil().del(GoblinRedisConst.ARTIST_TOPIC + topicId);
return true;
}
@Override
@Transactional
public boolean setRecommend(String topicId, int position) {
LocalDateTime now = LocalDateTime.now();
if (position > 0) {
mongoTemplate.updateMulti(
Query.query(Criteria.where("recommendPosition").is(position)),
Update.update("recommendPosition", 0),
GoblinArtistTopicVo.class.getSimpleName());
this.update(new LambdaUpdateWrapper<GoblinArtistTopic>()
.eq(GoblinArtistTopic::getRecommendPosition, position)
.set(GoblinArtistTopic::getRecommendPosition, 0)
.set(GoblinArtistTopic::getUpdatedAt, now));
}
this.update(new LambdaUpdateWrapper<GoblinArtistTopic>()
.eq(GoblinArtistTopic::getTopicId, topicId)
.set(GoblinArtistTopic::getRecommendPosition, position)
.set(GoblinArtistTopic::getUpdatedAt, now));
mongoTemplate.updateFirst(Query.query(Criteria.where("topicId").is(topicId)),
Update.update("recommendPosition", position).set("updatedAt", now),
GoblinArtistTopicVo.class.getSimpleName());
redisDataSourceUtil.getRedisGoblinUtil().del(GoblinRedisConst.ARTIST_TOPIC + topicId);
return true;
}
@Override
@Transactional
public boolean setStatus(String topicId, int status) {
LocalDateTime now = LocalDateTime.now();
this.update(new LambdaUpdateWrapper<GoblinArtistTopic>()
.eq(GoblinArtistTopic::getTopicId, topicId)
.set(GoblinArtistTopic::getStatus, status)
.set(GoblinArtistTopic::getUpdatedAt, now));
mongoTemplate.updateFirst(Query.query(Criteria.where("topicId").is(topicId)),
Update.update("status", status).set("updatedAt", now),
GoblinArtistTopicVo.class.getSimpleName());
redisDataSourceUtil.getRedisGoblinUtil().del(GoblinRedisConst.ARTIST_TOPIC + topicId);
return true;
}
@Override
@Transactional
public boolean deleteTopic(String topicId) {
LocalDateTime now = LocalDateTime.now();
this.update(new LambdaUpdateWrapper<GoblinArtistTopic>()
.eq(GoblinArtistTopic::getTopicId, topicId)
.set(GoblinArtistTopic::getDelFlg, GoblinArtistConst.DEL_YES)
.set(GoblinArtistTopic::getUpdatedAt, now));
mongoTemplate.updateFirst(Query.query(Criteria.where("topicId").is(topicId)),
Update.update("delFlg", GoblinArtistConst.DEL_YES).set("updatedAt", now),
GoblinArtistTopicVo.class.getSimpleName());
redisDataSourceUtil.getRedisGoblinUtil().del(GoblinRedisConst.ARTIST_TOPIC + topicId);
return true;
}
@Override
public Map<String, Object> topicDetail(String topicId) {
GoblinArtistTopic topic = this.getOne(new LambdaQueryWrapper<GoblinArtistTopic>()
.eq(GoblinArtistTopic::getTopicId, topicId)
.eq(GoblinArtistTopic::getDelFlg, GoblinArtistConst.DEL_NO));
if (topic == null) {
return null;
}
List<GoblinArtistTopicGoods> relations = topicGoodsMapper.selectList(
new LambdaQueryWrapper<GoblinArtistTopicGoods>()
.eq(GoblinArtistTopicGoods::getTopicId, topicId)
.orderByAsc(GoblinArtistTopicGoods::getSort));
Map<String, Object> result = new LinkedHashMap<>();
result.put("topicId", topic.getTopicId());
result.put("storeId", topic.getStoreId());
result.put("topicName", topic.getTopicName());
result.put("artistId", topic.getArtistId());
result.put("templateType", topic.getTemplateType());
result.put("bannerUrl", topic.getBannerUrl());
result.put("bannerHeight", topic.getBannerHeight());
result.put("onSaleStart", topic.getOnSaleStart());
result.put("onSaleEnd", topic.getOnSaleEnd());
result.put("status", topic.getStatus());
result.put("sort", topic.getSort());
result.put("recommendPosition", topic.getRecommendPosition());
result.put("createdAt", topic.getCreatedAt());
result.put("updatedAt", topic.getUpdatedAt());
result.put("goods", buildGoods(relations));
return result;
}
/**
* 按关联表拼商品明细:名称/封面/价格取商品表,规格取 SKU 表(一个商品可能有多个规格,顿号分隔)
*/
private List<Map<String, Object>> buildGoods(List<GoblinArtistTopicGoods> relations) {
List<Map<String, Object>> list = new ArrayList<>();
if (relations.isEmpty()) {
return list;
}
Set<String> spuIds = relations.stream()
.map(GoblinArtistTopicGoods::getSpuId)
.filter(StringUtils::isNotBlank)
.collect(Collectors.toSet());
Map<String, GoblinGoods> goodsMap = goodsMapper.selectList(
new LambdaQueryWrapper<GoblinGoods>().in(GoblinGoods::getSpuId, spuIds))
.stream().collect(Collectors.toMap(GoblinGoods::getSpuId, g -> g, (a, b) -> a));
List<GoblinGoodsSku> skus = goodsSkuMapper.selectList(
new LambdaQueryWrapper<GoblinGoodsSku>()
.in(GoblinGoodsSku::getSpuId, spuIds)
.eq(GoblinGoodsSku::getDelFlg, "0"));
Map<String, String> specMap = skus.stream()
.filter(sku -> StringUtils.isNotBlank(sku.getName()))
.collect(Collectors.groupingBy(GoblinGoodsSku::getSpuId,
Collectors.mapping(GoblinGoodsSku::getName, Collectors.joining("、"))));
for (GoblinArtistTopicGoods relation : relations) {
GoblinGoods goods = goodsMap.get(relation.getSpuId());
Map<String, Object> good = new LinkedHashMap<>();
good.put("spuId", relation.getSpuId());
good.put("sort", relation.getSort());
good.put("name", goods == null ? null : goods.getName());
good.put("coverPic", goods == null ? null : goods.getCoverPic());
good.put("sellPrice", formatGoodsPrice(goods, skus, relation.getSpuId()));
good.put("spec", specMap.get(relation.getSpuId()));
list.add(good);
}
return list;
}
/**
* 计算并格式化商品价格:优先用 priceGe 和 priceLe。若相同取一个,若不同展示区间。
*/
private String formatGoodsPrice(GoblinGoods goods, List<GoblinGoodsSku> skus, String spuId) {
BigDecimal ge = goods != null ? goods.getPriceGe() : null;
BigDecimal le = goods != null ? goods.getPriceLe() : null;
if (ge != null && le != null) {
if (ge.compareTo(le) == 0) {
return ge.setScale(2, java.math.RoundingMode.HALF_UP).toString();
} else {
return ge.setScale(2, java.math.RoundingMode.HALF_UP).toString() + " ~ " + le.setScale(2, java.math.RoundingMode.HALF_UP).toString();
}
}
if (ge != null) {
return ge.setScale(2, java.math.RoundingMode.HALF_UP).toString();
}
if (le != null) {
return le.setScale(2, java.math.RoundingMode.HALF_UP).toString();
}
if (goods != null && goods.getSellPrice() != null) {
return goods.getSellPrice().setScale(2, java.math.RoundingMode.HALF_UP).toString();
}
// 兜底查 SKU 价格
List<BigDecimal> skuPrices = skus.stream()
.filter(s -> StringUtils.equals(s.getSpuId(), spuId))
.map(s -> s.getPrice() != null ? s.getPrice() : s.getSellPrice())
.filter(Objects::nonNull)
.collect(Collectors.toList());
if (!skuPrices.isEmpty()) {
BigDecimal min = Collections.min(skuPrices);
BigDecimal max = Collections.max(skuPrices);
if (min.compareTo(max) == 0) {
return min.setScale(2, java.math.RoundingMode.HALF_UP).toString();
} else {
return min.setScale(2, java.math.RoundingMode.HALF_UP).toString() + " ~ " + max.setScale(2, java.math.RoundingMode.HALF_UP).toString();
}
}
return null;
}
@Override
public Map<String, Integer> goodsCountMap(Collection<String> topicIds) {
if (CollectionUtils.isEmpty(topicIds)) {
return Collections.emptyMap();
}
List<GoblinArtistTopicGoods> relations = topicGoodsMapper.selectList(
new LambdaQueryWrapper<GoblinArtistTopicGoods>()
.in(GoblinArtistTopicGoods::getTopicId, topicIds));
return relations.stream().collect(Collectors.groupingBy(
GoblinArtistTopicGoods::getTopicId, Collectors.summingInt(r -> 1)));
}
}
package com.liquidnet.service.goblin.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* 店铺-艺人授权关系
*/
@Data
@EqualsAndHashCode(callSuper = false)
public class GoblinArtistAuth implements Serializable {
private static final long serialVersionUID = 1L;
@TableId(value = "mid", type = IdType.AUTO)
private Long mid;
private String authId;
private String storeId;
private String artistId;
private String applyId;
private LocalDateTime expireAt;
/** 1有效 0失效 */
private Integer status;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
}
package com.liquidnet.service.goblin.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.liquidnet.service.goblin.entity.GoblinArtistAuth;
public interface GoblinArtistAuthMapper extends BaseMapper<GoblinArtistAuth> {
}
...@@ -24,7 +24,6 @@ import com.liquidnet.service.goblin.dto.manage.vo.GoblinStoreMgtArtistTopicDetai ...@@ -24,7 +24,6 @@ import com.liquidnet.service.goblin.dto.manage.vo.GoblinStoreMgtArtistTopicDetai
import com.liquidnet.service.goblin.dto.manage.vo.GoblinStoreMgtArtistTopicGoodsSearchItemVo; import com.liquidnet.service.goblin.dto.manage.vo.GoblinStoreMgtArtistTopicGoodsSearchItemVo;
import com.liquidnet.service.goblin.dto.manage.vo.GoblinStoreMgtArtistTopicPageVo; import com.liquidnet.service.goblin.dto.manage.vo.GoblinStoreMgtArtistTopicPageVo;
import com.liquidnet.service.goblin.dto.vo.GoblinArtistAuthApplyVo; import com.liquidnet.service.goblin.dto.vo.GoblinArtistAuthApplyVo;
import com.liquidnet.service.goblin.dto.vo.GoblinArtistAuthVo;
import com.liquidnet.service.goblin.dto.vo.GoblinArtistTopicVo; import com.liquidnet.service.goblin.dto.vo.GoblinArtistTopicVo;
import com.liquidnet.service.goblin.dto.vo.GoblinGoodsInfoVo; import com.liquidnet.service.goblin.dto.vo.GoblinGoodsInfoVo;
import com.liquidnet.service.goblin.dto.vo.GoblinStoreInfoVo; import com.liquidnet.service.goblin.dto.vo.GoblinStoreInfoVo;
...@@ -51,6 +50,7 @@ import java.time.LocalDateTime; ...@@ -51,6 +50,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.LinkedHashMap;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
...@@ -259,23 +259,30 @@ public class GoblinStoreMgtArtistServiceImpl implements IGoblinStoreMgtArtistSer ...@@ -259,23 +259,30 @@ public class GoblinStoreMgtArtistServiceImpl implements IGoblinStoreMgtArtistSer
return ResponseDto.failure(storeResp.getMessage()); return ResponseDto.failure(storeResp.getMessage());
} }
String storeId = storeResp.getData().getStoreId(); String storeId = storeResp.getData().getStoreId();
List<GoblinArtistAuthVo> auths = loadAuthsByStore(storeId);
LocalDateTime now = LocalDateTime.now(); LocalDateTime now = LocalDateTime.now();
auths = auths.stream()
.filter(a -> Objects.equals(a.getStatus(), GoblinArtistConst.AUTH_VALID) List<GoblinArtistAuthApplyVo> all = loadAppliesByStore(storeId);
&& a.getExpireAt() != null && a.getExpireAt().isAfter(now)) List<GoblinArtistAuthApplyVo> approved = all.stream()
.filter(a -> Objects.equals(a.getStatus(), GoblinArtistConst.APPLY_APPROVED)
&& a.getAuthExpireAt() != null && a.getAuthExpireAt().isAfter(now))
.collect(Collectors.toList()); .collect(Collectors.toList());
Map<String, KylinArtist> artistMap = loadArtistMap(auths.stream() Map<String, GoblinArtistAuthApplyVo> authByArtist = new LinkedHashMap<>();
.map(GoblinArtistAuthVo::getArtistId).collect(Collectors.toSet())); for (GoblinArtistAuthApplyVo a : approved) {
GoblinArtistAuthApplyVo exist = authByArtist.get(a.getArtistId());
if (exist == null || a.getAuthExpireAt().isAfter(exist.getAuthExpireAt())) {
authByArtist.put(a.getArtistId(), a);
}
}
Map<String, KylinArtist> artistMap = loadArtistMap(authByArtist.keySet());
GoblinStoreMgtArtistAuthListVo result = new GoblinStoreMgtArtistAuthListVo(); GoblinStoreMgtArtistAuthListVo result = new GoblinStoreMgtArtistAuthListVo();
for (GoblinArtistAuthVo auth : auths) { for (GoblinArtistAuthApplyVo apply : authByArtist.values()) {
GoblinStoreMgtArtistAuthListVo.Item item = new GoblinStoreMgtArtistAuthListVo.Item(); GoblinStoreMgtArtistAuthListVo.Item item = new GoblinStoreMgtArtistAuthListVo.Item();
item.setArtistId(auth.getArtistId()); item.setArtistId(apply.getArtistId());
item.setExpireAt(auth.getExpireAt()); item.setExpireAt(apply.getAuthExpireAt());
KylinArtist artist = artistMap.get(auth.getArtistId()); KylinArtist artist = artistMap.get(apply.getArtistId());
if (artist != null) { if (artist != null) {
item.setArtistName(artist.getArtistName()); item.setArtistName(artist.getArtistName());
item.setAvatarUrl(artist.getAvatarUrl()); item.setAvatarUrl(artist.getAvatarUrl());
...@@ -450,7 +457,7 @@ public class GoblinStoreMgtArtistServiceImpl implements IGoblinStoreMgtArtistSer ...@@ -450,7 +457,7 @@ public class GoblinStoreMgtArtistServiceImpl implements IGoblinStoreMgtArtistSer
topicVo.setGoods(goodsItems); topicVo.setGoods(goodsItems);
mongoTemplate.insert(topicVo, GoblinArtistTopicVo.class.getSimpleName()); mongoTemplate.insert(topicVo, GoblinArtistTopicVo.class.getSimpleName());
String topicKey = GoblinRedisConst.ARTIST_TOPIC + storeId + "_" + topicId; String topicKey = GoblinRedisConst.ARTIST_TOPIC + topicId;
redisUtils.set(topicKey, topicVo, TOPIC_TTL); redisUtils.set(topicKey, topicVo, TOPIC_TTL);
queueTopicInsertMysql(topicVo, now); queueTopicInsertMysql(topicVo, now);
...@@ -513,7 +520,7 @@ public class GoblinStoreMgtArtistServiceImpl implements IGoblinStoreMgtArtistSer ...@@ -513,7 +520,7 @@ public class GoblinStoreMgtArtistServiceImpl implements IGoblinStoreMgtArtistSer
update.set("updatedAt", now); update.set("updatedAt", now);
mongoTemplate.updateFirst(query, update, GoblinArtistTopicVo.class.getSimpleName()); mongoTemplate.updateFirst(query, update, GoblinArtistTopicVo.class.getSimpleName());
String topicKey = GoblinRedisConst.ARTIST_TOPIC + storeId + "_" + params.getTopicId(); String topicKey = GoblinRedisConst.ARTIST_TOPIC + params.getTopicId();
redisUtils.del(topicKey); redisUtils.del(topicKey);
queueTopicUpdateMysql(params.getTopicId(), params, now); queueTopicUpdateMysql(params.getTopicId(), params, now);
...@@ -540,7 +547,7 @@ public class GoblinStoreMgtArtistServiceImpl implements IGoblinStoreMgtArtistSer ...@@ -540,7 +547,7 @@ public class GoblinStoreMgtArtistServiceImpl implements IGoblinStoreMgtArtistSer
new Update().set("delFlg", GoblinArtistConst.DEL_YES).set("updatedAt", now), new Update().set("delFlg", GoblinArtistConst.DEL_YES).set("updatedAt", now),
GoblinArtistTopicVo.class.getSimpleName()); GoblinArtistTopicVo.class.getSimpleName());
String topicKey = GoblinRedisConst.ARTIST_TOPIC + storeId + "_" + params.getTopicId(); String topicKey = GoblinRedisConst.ARTIST_TOPIC + params.getTopicId();
redisUtils.del(topicKey); redisUtils.del(topicKey);
LinkedList<String> sqls = CollectionUtil.linkedListString(); LinkedList<String> sqls = CollectionUtil.linkedListString();
...@@ -578,7 +585,7 @@ public class GoblinStoreMgtArtistServiceImpl implements IGoblinStoreMgtArtistSer ...@@ -578,7 +585,7 @@ public class GoblinStoreMgtArtistServiceImpl implements IGoblinStoreMgtArtistSer
new Update().set("status", status).set("updatedAt", now), new Update().set("status", status).set("updatedAt", now),
GoblinArtistTopicVo.class.getSimpleName()); GoblinArtistTopicVo.class.getSimpleName());
String topicKey = GoblinRedisConst.ARTIST_TOPIC + storeId + "_" + params.getTopicId(); String topicKey = GoblinRedisConst.ARTIST_TOPIC + params.getTopicId();
redisUtils.del(topicKey); redisUtils.del(topicKey);
LinkedList<String> sqls = CollectionUtil.linkedListString(); LinkedList<String> sqls = CollectionUtil.linkedListString();
...@@ -631,12 +638,13 @@ public class GoblinStoreMgtArtistServiceImpl implements IGoblinStoreMgtArtistSer ...@@ -631,12 +638,13 @@ public class GoblinStoreMgtArtistServiceImpl implements IGoblinStoreMgtArtistSer
return ResponseDto.success(null); return ResponseDto.success(null);
} }
LocalDateTime now = LocalDateTime.now(); LocalDateTime now = LocalDateTime.now();
List<GoblinArtistAuthVo> auths = loadAuthsByStore(storeId); long count = mongoTemplate.count(
boolean valid = auths.stream().anyMatch(a -> new Query(Criteria.where("storeId").is(storeId)
Objects.equals(a.getArtistId(), artistId) .and("artistId").is(artistId)
&& Objects.equals(a.getStatus(), GoblinArtistConst.AUTH_VALID) .and("status").is(GoblinArtistConst.APPLY_APPROVED)
&& a.getExpireAt() != null && a.getExpireAt().isAfter(now)); .and("authExpireAt").gt(now)),
if (!valid) { GoblinArtistAuthApplyVo.class.getSimpleName());
if (count == 0) {
return ResponseDto.failure("未获得该艺人有效授权"); return ResponseDto.failure("未获得该艺人有效授权");
} }
return ResponseDto.success(null); return ResponseDto.success(null);
...@@ -667,13 +675,6 @@ public class GoblinStoreMgtArtistServiceImpl implements IGoblinStoreMgtArtistSer ...@@ -667,13 +675,6 @@ public class GoblinStoreMgtArtistServiceImpl implements IGoblinStoreMgtArtistSer
return applies == null ? Collections.emptyList() : applies; return applies == null ? Collections.emptyList() : applies;
} }
private List<GoblinArtistAuthVo> loadAuthsByStore(String storeId) {
List<GoblinArtistAuthVo> auths = mongoTemplate.find(
new Query(Criteria.where("storeId").is(storeId)),
GoblinArtistAuthVo.class, GoblinArtistAuthVo.class.getSimpleName());
return auths == null ? Collections.emptyList() : auths;
}
private List<GoblinArtistTopicVo> loadTopicsByStore(String storeId) { private List<GoblinArtistTopicVo> loadTopicsByStore(String storeId) {
List<GoblinArtistTopicVo> topics = mongoTemplate.find( List<GoblinArtistTopicVo> topics = mongoTemplate.find(
new Query(Criteria.where("storeId").is(storeId).and("delFlg").is(GoblinArtistConst.DEL_NO)), new Query(Criteria.where("storeId").is(storeId).and("delFlg").is(GoblinArtistConst.DEL_NO)),
...@@ -682,7 +683,7 @@ public class GoblinStoreMgtArtistServiceImpl implements IGoblinStoreMgtArtistSer ...@@ -682,7 +683,7 @@ public class GoblinStoreMgtArtistServiceImpl implements IGoblinStoreMgtArtistSer
} }
private GoblinArtistTopicVo loadTopic(String storeId, String topicId) { private GoblinArtistTopicVo loadTopic(String storeId, String topicId) {
String redisKey = GoblinRedisConst.ARTIST_TOPIC + storeId + "_" + topicId; String redisKey = GoblinRedisConst.ARTIST_TOPIC + topicId;
Object cached = redisUtils.get(redisKey); Object cached = redisUtils.get(redisKey);
if (cached instanceof GoblinArtistTopicVo) { if (cached instanceof GoblinArtistTopicVo) {
GoblinArtistTopicVo topic = (GoblinArtistTopicVo) cached; GoblinArtistTopicVo topic = (GoblinArtistTopicVo) cached;
......
...@@ -211,9 +211,6 @@ goblin_artist_auth_apply.insert=INSERT INTO goblin_artist_auth_apply (apply_id,s ...@@ -211,9 +211,6 @@ goblin_artist_auth_apply.insert=INSERT INTO goblin_artist_auth_apply (apply_id,s
goblin_artist_auth_apply.revoke=UPDATE goblin_artist_auth_apply SET status=3,updated_at=? WHERE apply_id=? goblin_artist_auth_apply.revoke=UPDATE goblin_artist_auth_apply SET status=3,updated_at=? WHERE apply_id=?
goblin_artist_auth_apply.approve=UPDATE goblin_artist_auth_apply SET status=1,reviewer_id=?,reviewer_name=?,review_time=?,review_remark=?,auth_expire_at=?,updated_at=? WHERE apply_id=? goblin_artist_auth_apply.approve=UPDATE goblin_artist_auth_apply SET status=1,reviewer_id=?,reviewer_name=?,review_time=?,review_remark=?,auth_expire_at=?,updated_at=? WHERE apply_id=?
goblin_artist_auth_apply.reject=UPDATE goblin_artist_auth_apply SET status=2,reviewer_id=?,reviewer_name=?,review_time=?,reject_reason=?,updated_at=? WHERE apply_id=? goblin_artist_auth_apply.reject=UPDATE goblin_artist_auth_apply SET status=2,reviewer_id=?,reviewer_name=?,review_time=?,reject_reason=?,updated_at=? WHERE apply_id=?
#---- \u827A\u4EBA\u6388\u6743\u5173\u7CFB
goblin_artist_auth.insert=INSERT INTO goblin_artist_auth (auth_id,store_id,artist_id,apply_id,expire_at,status,created_at,updated_at)VALUES(?,?,?,?,?,?,?,?)
goblin_artist_auth.update_expire=UPDATE goblin_artist_auth SET expire_at=?,updated_at=? WHERE store_id=? AND artist_id=? AND status=1
#---- \u827A\u4EBA\u4E13\u9898 #---- \u827A\u4EBA\u4E13\u9898
goblin_artist_topic.insert=INSERT INTO goblin_artist_topic (topic_id,store_id,topic_name,artist_id,template_type,banner_url,banner_height,on_sale_start,on_sale_end,status,sort,recommend_position,del_flg,created_at,updated_at)VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) goblin_artist_topic.insert=INSERT INTO goblin_artist_topic (topic_id,store_id,topic_name,artist_id,template_type,banner_url,banner_height,on_sale_start,on_sale_end,status,sort,recommend_position,del_flg,created_at,updated_at)VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
goblin_artist_topic.update=UPDATE goblin_artist_topic SET topic_name=?,artist_id=?,template_type=?,banner_url=?,banner_height=?,on_sale_start=?,on_sale_end=?,updated_at=? WHERE topic_id=? goblin_artist_topic.update=UPDATE goblin_artist_topic SET topic_name=?,artist_id=?,template_type=?,banner_url=?,banner_height=?,on_sale_start=?,on_sale_end=?,updated_at=? WHERE topic_id=?
......
...@@ -162,7 +162,7 @@ public class GoblinStoreMgtArtistServiceImplTest { ...@@ -162,7 +162,7 @@ public class GoblinStoreMgtArtistServiceImplTest {
GoblinStoreMgtArtistApplySubmitParams submitParams = new GoblinStoreMgtArtistApplySubmitParams(); GoblinStoreMgtArtistApplySubmitParams submitParams = new GoblinStoreMgtArtistApplySubmitParams();
submitParams.setArtistId(artistId); submitParams.setArtistId(artistId);
submitParams.setAttachmentUrls(new ArrayList<>()); submitParams.setAttachmentUrls(new ArrayList<>());
submitParams.getAttachmentUrls().add("https://example.com/test.jpg"); submitParams.getAttachmentUrls().add("https://img.zhengzai.tv/other/2022/07/18/b519c4ff8e4c4ea280898acfb68e6048.jpeg");
ResponseDto<String> submitResp = artistService.submitApply(submitParams); ResponseDto<String> submitResp = artistService.submitApply(submitParams);
log.info("submitApply: success={} msg={} data={}", log.info("submitApply: success={} msg={} data={}",
...@@ -206,7 +206,7 @@ public class GoblinStoreMgtArtistServiceImplTest { ...@@ -206,7 +206,7 @@ public class GoblinStoreMgtArtistServiceImplTest {
*/ */
@Test @Test
public void applyList_filterByStatus() { public void applyList_filterByStatus() {
ResponseDto<PagedResult<GoblinStoreMgtArtistApplyListItemVo>> resp = artistService.applyList(GoblinArtistConst.APPLY_PENDING, 1, 10); ResponseDto<PagedResult<GoblinStoreMgtArtistApplyListItemVo>> resp = artistService.applyList(GoblinArtistConst.APPLY_APPROVED, 1, 10);
Assert.assertTrue("applyList status=0: " + resp.getMessage(), resp.isSuccess()); Assert.assertTrue("applyList status=0: " + resp.getMessage(), resp.isSuccess());
log.info("applyList status=0 total={}", resp.getData().getTotal()); log.info("applyList status=0 total={}", resp.getData().getTotal());
} }
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment