完善业务闭环,实现对轮播图的管理,优化数据库表文件
This commit is contained in:
+12
-9
@@ -89,13 +89,14 @@ public class GroupCourseService implements IGroupCourseService {
|
||||
public Mono<GroupCourseDetail> findDetailById(Long id) {
|
||||
String cacheKey = CACHE_KEY_DETAIL_PREFIX + id;
|
||||
|
||||
return redisUtil.get(cacheKey, String.class)
|
||||
Mono<String> cachedMono = redisUtil.get(cacheKey, String.class);
|
||||
return cachedMono
|
||||
.flatMap(cachedJson -> {
|
||||
if (cachedJson != null) {
|
||||
if (cachedJson != null && !cachedJson.isEmpty()) {
|
||||
try {
|
||||
GroupCourseDetail detail = objectMapper.readValue(cachedJson, GroupCourseDetail.class);
|
||||
logger.info("缓存命中 - findDetailById: id={}", id);
|
||||
return Mono.just(detail);
|
||||
return Mono.<GroupCourseDetail>just(detail);
|
||||
} catch (JsonProcessingException e) {
|
||||
logger.warn("缓存解析失败,删除缓存 - id: {}, error: {}", id, e.getMessage());
|
||||
return redisUtil.delete(cacheKey).then(Mono.empty());
|
||||
@@ -176,13 +177,14 @@ public class GroupCourseService implements IGroupCourseService {
|
||||
public Mono<GroupCourse> findById(Long id) {
|
||||
String cacheKey = CACHE_KEY_ID_PREFIX + id;
|
||||
|
||||
return redisUtil.get(cacheKey, String.class)
|
||||
Mono<String> cachedMono = redisUtil.get(cacheKey, String.class);
|
||||
return cachedMono
|
||||
.flatMap(cachedJson -> {
|
||||
if (cachedJson != null) {
|
||||
if (cachedJson != null && !cachedJson.isEmpty()) {
|
||||
try {
|
||||
GroupCourse groupCourse = objectMapper.readValue(cachedJson, GroupCourse.class);
|
||||
logger.info("缓存命中 - findById: id={}", id);
|
||||
return Mono.just(groupCourse);
|
||||
return Mono.<GroupCourse>just(groupCourse);
|
||||
} catch (JsonProcessingException e) {
|
||||
logger.warn("缓存解析失败,删除缓存 - id: {}, error: {}", id, e.getMessage());
|
||||
return redisUtil.delete(cacheKey).then(Mono.empty());
|
||||
@@ -232,14 +234,15 @@ public class GroupCourseService implements IGroupCourseService {
|
||||
|
||||
String cacheKey = CACHE_KEY_PREFIX + page + ":" + size + ":" + includeDeleted + ":" + sort + ":" + order + ":" + keyword + ":" + status;
|
||||
|
||||
return redisUtil.get(cacheKey, String.class)
|
||||
Mono<String> cachedMono = redisUtil.get(cacheKey, String.class);
|
||||
return cachedMono
|
||||
.flatMap(cachedJson -> {
|
||||
if (cachedJson != null) {
|
||||
if (cachedJson != null && !cachedJson.isEmpty()) {
|
||||
try {
|
||||
PageResponse<GroupCourse> pageResponse = objectMapper.readValue(cachedJson,
|
||||
objectMapper.getTypeFactory().constructParametricType(PageResponse.class, GroupCourse.class));
|
||||
logger.info("缓存命中 - findByPage: key={}", cacheKey);
|
||||
return Mono.just(pageResponse);
|
||||
return Mono.<PageResponse<GroupCourse>>just(pageResponse);
|
||||
} catch (JsonProcessingException e) {
|
||||
logger.warn("缓存解析失败,删除缓存 - key: {}, error: {}", cacheKey, e.getMessage());
|
||||
return redisUtil.delete(cacheKey).then(Mono.empty());
|
||||
|
||||
+10
@@ -16,6 +16,7 @@ import cn.novalon.gym.manage.member.handler.MemberCardTransactionHandler;
|
||||
import cn.novalon.gym.manage.member.handler.MemberHandler;
|
||||
import cn.novalon.gym.manage.member.handler.MemberStoredCardHandler;
|
||||
import cn.novalon.gym.manage.member.handler.WechatAuthHandler;
|
||||
import cn.novalon.gym.manage.notify.handler.BannerHandler;
|
||||
import cn.novalon.gym.manage.notify.handler.SysNoticeHandler;
|
||||
import cn.novalon.gym.manage.notify.handler.SysUserMessageHandler;
|
||||
import cn.novalon.gym.manage.payment.handler.PaymentHandler;
|
||||
@@ -63,6 +64,7 @@ public class SystemRouter {
|
||||
SysAuthHandler authHandler,
|
||||
StatsHandler statsHandler,
|
||||
SysDictHandler dictHandler,
|
||||
BannerHandler bannerHandler,
|
||||
SysNoticeHandler noticeHandler,
|
||||
SysUserMessageHandler messageHandler,
|
||||
SysFileHandler fileHandler,
|
||||
@@ -188,6 +190,14 @@ public class SystemRouter {
|
||||
.PUT("/api/dict/data/{id}", dictHandler::updateDictData)
|
||||
.DELETE("/api/dict/data/{id}", dictHandler::deleteDictData)
|
||||
|
||||
// ========== 轮播图路由 ==========
|
||||
.GET("/api/banners", bannerHandler::getAllBanners)
|
||||
.GET("/api/banners/active", bannerHandler::getActiveBanners)
|
||||
.GET("/api/banners/{id}", bannerHandler::getBannerById)
|
||||
.POST("/api/banners", bannerHandler::createBanner)
|
||||
.PUT("/api/banners/{id}", bannerHandler::updateBanner)
|
||||
.DELETE("/api/banners/{id}", bannerHandler::deleteBanner)
|
||||
|
||||
// ========== 公告路由 ==========
|
||||
.GET("/api/notices", noticeHandler::getAllNotices)
|
||||
.GET("/api/notices/{id}", noticeHandler::getNoticeById)
|
||||
|
||||
+1
-1
@@ -43,7 +43,7 @@ public class RedisUtil {
|
||||
public <T> Mono<T> get(String key, Class<T> clazz) {
|
||||
return reactiveRedisTemplate.opsForValue().get(key)
|
||||
.filter(clazz::isInstance)
|
||||
.cast(clazz)
|
||||
.map(clazz::cast)
|
||||
.onErrorResume(e -> {
|
||||
// 旧缓存数据格式不兼容时静默跳过,后续逻辑会 fallback 到 DB 查询
|
||||
log.warn("读取 Redis 缓存失败(可能为旧格式): key={}, error={}", key, e.getMessage());
|
||||
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
package cn.novalon.gym.manage.db.converter;
|
||||
|
||||
import cn.novalon.gym.manage.notify.core.domain.Banner;
|
||||
import cn.novalon.gym.manage.db.entity.BannerEntity;
|
||||
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 轮播图实体转换器
|
||||
*
|
||||
* @author 张翔
|
||||
* @date 2026-07-18
|
||||
*/
|
||||
@Component
|
||||
public class BannerConverter {
|
||||
|
||||
public Banner toDomain(BannerEntity entity) {
|
||||
if (entity == null) {
|
||||
return null;
|
||||
}
|
||||
Banner domain = new Banner();
|
||||
domain.setId(entity.getId());
|
||||
domain.setImageUrl(entity.getImageUrl());
|
||||
domain.setTitle(entity.getTitle());
|
||||
domain.setSubtitle(entity.getSubtitle());
|
||||
domain.setSortOrder(entity.getSortOrder());
|
||||
domain.setIsActive(entity.getIsActive());
|
||||
domain.setLinkUrl(entity.getLinkUrl());
|
||||
domain.setCreatedAt(entity.getCreatedAt());
|
||||
domain.setUpdatedAt(entity.getUpdatedAt());
|
||||
domain.setDeletedAt(entity.getDeletedAt());
|
||||
return domain;
|
||||
}
|
||||
|
||||
public BannerEntity toEntity(Banner domain) {
|
||||
if (domain == null) {
|
||||
return null;
|
||||
}
|
||||
BannerEntity entity = new BannerEntity();
|
||||
entity.setId(domain.getId());
|
||||
entity.setImageUrl(domain.getImageUrl());
|
||||
entity.setTitle(domain.getTitle());
|
||||
entity.setSubtitle(domain.getSubtitle());
|
||||
entity.setSortOrder(domain.getSortOrder());
|
||||
entity.setIsActive(domain.getIsActive());
|
||||
entity.setLinkUrl(domain.getLinkUrl());
|
||||
entity.setCreatedAt(domain.getCreatedAt());
|
||||
entity.setUpdatedAt(domain.getUpdatedAt());
|
||||
entity.setDeletedAt(domain.getDeletedAt());
|
||||
return entity;
|
||||
}
|
||||
|
||||
public List<Banner> toDomainList(List<BannerEntity> entities) {
|
||||
if (entities == null) {
|
||||
return null;
|
||||
}
|
||||
return entities.stream()
|
||||
.map(this::toDomain)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
public List<BannerEntity> toEntityList(List<Banner> domains) {
|
||||
if (domains == null) {
|
||||
return null;
|
||||
}
|
||||
return domains.stream()
|
||||
.map(this::toEntity)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package cn.novalon.gym.manage.db.dao;
|
||||
|
||||
import cn.novalon.gym.manage.db.entity.BannerEntity;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.data.r2dbc.repository.Query;
|
||||
import org.springframework.data.r2dbc.repository.R2dbcRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
@Repository
|
||||
public interface BannerDao extends R2dbcRepository<BannerEntity, Long> {
|
||||
|
||||
Flux<BannerEntity> findByDeletedAtIsNull();
|
||||
|
||||
Flux<BannerEntity> findByDeletedAtIsNull(Sort sort);
|
||||
|
||||
@Query("SELECT * FROM banner WHERE deleted_at IS NULL ORDER BY sort_order ASC")
|
||||
Flux<BannerEntity> findActiveBanners();
|
||||
|
||||
Mono<Void> deleteByIdAndDeletedAtIsNull(Long id);
|
||||
}
|
||||
+127
@@ -0,0 +1,127 @@
|
||||
package cn.novalon.gym.manage.db.entity;
|
||||
|
||||
import org.springframework.data.annotation.Id;
|
||||
import org.springframework.data.relational.core.mapping.Column;
|
||||
import org.springframework.data.relational.core.mapping.Table;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 轮播图数据库实体类
|
||||
*
|
||||
* @author 张翔
|
||||
* @date 2026-07-18
|
||||
*/
|
||||
@Table("banner")
|
||||
public class BannerEntity {
|
||||
|
||||
@Id
|
||||
private Long id;
|
||||
|
||||
@Column("image_url")
|
||||
private String imageUrl;
|
||||
|
||||
@Column("title")
|
||||
private String title;
|
||||
|
||||
@Column("subtitle")
|
||||
private String subtitle;
|
||||
|
||||
@Column("sort_order")
|
||||
private Integer sortOrder;
|
||||
|
||||
@Column("is_active")
|
||||
private String isActive;
|
||||
|
||||
@Column("link_url")
|
||||
private String linkUrl;
|
||||
|
||||
@Column("created_at")
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@Column("updated_at")
|
||||
private LocalDateTime updatedAt;
|
||||
|
||||
@Column("deleted_at")
|
||||
private LocalDateTime deletedAt;
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getImageUrl() {
|
||||
return imageUrl;
|
||||
}
|
||||
|
||||
public void setImageUrl(String imageUrl) {
|
||||
this.imageUrl = imageUrl;
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
return title;
|
||||
}
|
||||
|
||||
public void setTitle(String title) {
|
||||
this.title = title;
|
||||
}
|
||||
|
||||
public String getSubtitle() {
|
||||
return subtitle;
|
||||
}
|
||||
|
||||
public void setSubtitle(String subtitle) {
|
||||
this.subtitle = subtitle;
|
||||
}
|
||||
|
||||
public Integer getSortOrder() {
|
||||
return sortOrder;
|
||||
}
|
||||
|
||||
public void setSortOrder(Integer sortOrder) {
|
||||
this.sortOrder = sortOrder;
|
||||
}
|
||||
|
||||
public String getIsActive() {
|
||||
return isActive;
|
||||
}
|
||||
|
||||
public void setIsActive(String isActive) {
|
||||
this.isActive = isActive;
|
||||
}
|
||||
|
||||
public String getLinkUrl() {
|
||||
return linkUrl;
|
||||
}
|
||||
|
||||
public void setLinkUrl(String linkUrl) {
|
||||
this.linkUrl = linkUrl;
|
||||
}
|
||||
|
||||
public LocalDateTime getCreatedAt() {
|
||||
return createdAt;
|
||||
}
|
||||
|
||||
public void setCreatedAt(LocalDateTime createdAt) {
|
||||
this.createdAt = createdAt;
|
||||
}
|
||||
|
||||
public LocalDateTime getUpdatedAt() {
|
||||
return updatedAt;
|
||||
}
|
||||
|
||||
public void setUpdatedAt(LocalDateTime updatedAt) {
|
||||
this.updatedAt = updatedAt;
|
||||
}
|
||||
|
||||
public LocalDateTime getDeletedAt() {
|
||||
return deletedAt;
|
||||
}
|
||||
|
||||
public void setDeletedAt(LocalDateTime deletedAt) {
|
||||
this.deletedAt = deletedAt;
|
||||
}
|
||||
}
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
package cn.novalon.gym.manage.db.repository;
|
||||
|
||||
import cn.novalon.gym.manage.notify.core.domain.Banner;
|
||||
import cn.novalon.gym.manage.notify.core.repository.IBannerRepository;
|
||||
import cn.novalon.gym.manage.db.converter.BannerConverter;
|
||||
import cn.novalon.gym.manage.db.dao.BannerDao;
|
||||
import cn.novalon.gym.manage.db.entity.BannerEntity;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.stereotype.Repository;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
/**
|
||||
* 轮播图仓储实现类
|
||||
*
|
||||
* @author 张翔
|
||||
* @date 2026-07-18
|
||||
*/
|
||||
@Repository
|
||||
public class BannerRepository implements IBannerRepository {
|
||||
|
||||
private final BannerDao bannerDao;
|
||||
private final BannerConverter bannerConverter;
|
||||
|
||||
public BannerRepository(BannerDao bannerDao, BannerConverter bannerConverter) {
|
||||
this.bannerDao = bannerDao;
|
||||
this.bannerConverter = bannerConverter;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Flux<Banner> findByDeletedAtIsNull() {
|
||||
return bannerDao.findByDeletedAtIsNull(Sort.by(Sort.Direction.ASC, "sortOrder"))
|
||||
.map(bannerConverter::toDomain);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Flux<Banner> findActiveBanners() {
|
||||
return bannerDao.findActiveBanners()
|
||||
.map(bannerConverter::toDomain);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Banner> findById(Long id) {
|
||||
return bannerDao.findById(id)
|
||||
.map(bannerConverter::toDomain);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Banner> save(Banner banner) {
|
||||
BannerEntity entity = bannerConverter.toEntity(banner);
|
||||
return bannerDao.save(entity)
|
||||
.map(bannerConverter::toDomain);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> deleteByIdAndDeletedAtIsNull(Long id) {
|
||||
return bannerDao.deleteByIdAndDeletedAtIsNull(id);
|
||||
}
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
-- ============================================
|
||||
-- 团课推荐表
|
||||
-- 版本: V10
|
||||
-- 描述: 创建团课推荐表,用于首页推荐团课展示
|
||||
-- ============================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS group_course_recommend (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
course_id BIGINT NOT NULL,
|
||||
recommend_title VARCHAR(200),
|
||||
recommend_content TEXT,
|
||||
recommend_reason VARCHAR(500),
|
||||
priority INTEGER DEFAULT 0,
|
||||
is_active BOOLEAN DEFAULT TRUE,
|
||||
create_by VARCHAR(50),
|
||||
update_by VARCHAR(50),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
deleted_at TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_group_course_recommend_course_id ON group_course_recommend(course_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_group_course_recommend_priority ON group_course_recommend(priority);
|
||||
CREATE INDEX IF NOT EXISTS idx_group_course_recommend_is_active ON group_course_recommend(is_active);
|
||||
|
||||
COMMENT ON TABLE group_course_recommend IS '团课推荐表';
|
||||
COMMENT ON COLUMN group_course_recommend.id IS '主键ID';
|
||||
COMMENT ON COLUMN group_course_recommend.course_id IS '团课ID(关联group_course.id)';
|
||||
COMMENT ON COLUMN group_course_recommend.recommend_title IS '推荐标题';
|
||||
COMMENT ON COLUMN group_course_recommend.recommend_content IS '推荐内容';
|
||||
COMMENT ON COLUMN group_course_recommend.recommend_reason IS '推荐理由';
|
||||
COMMENT ON COLUMN group_course_recommend.priority IS '优先级(数字越大优先级越高)';
|
||||
COMMENT ON COLUMN group_course_recommend.is_active IS '是否启用';
|
||||
COMMENT ON COLUMN group_course_recommend.create_by IS '创建人';
|
||||
COMMENT ON COLUMN group_course_recommend.update_by IS '更新人';
|
||||
COMMENT ON COLUMN group_course_recommend.created_at IS '创建时间';
|
||||
COMMENT ON COLUMN group_course_recommend.updated_at IS '更新时间';
|
||||
COMMENT ON COLUMN group_course_recommend.deleted_at IS '删除时间(软删除)';
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
-- ============================================
|
||||
-- 为团课表添加二维码路径字段
|
||||
-- 版本: V11
|
||||
-- 描述: 添加团课二维码路径,用于扫码签到
|
||||
-- ============================================
|
||||
|
||||
ALTER TABLE group_course ADD COLUMN IF NOT EXISTS qr_code_path VARCHAR(500);
|
||||
|
||||
COMMENT ON COLUMN group_course.qr_code_path IS '二维码图片路径';
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
-- ============================================
|
||||
-- 为 member_user 表添加 deleted_at 字段
|
||||
-- 版本: V12
|
||||
-- 描述: 补充软删除时间字段,与BaseEntity映射对齐
|
||||
-- ============================================
|
||||
|
||||
ALTER TABLE member_user
|
||||
ADD COLUMN IF NOT EXISTS deleted_at TIMESTAMP;
|
||||
|
||||
COMMENT ON COLUMN member_user.deleted_at IS '软删除时间';
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
-- ============================================
|
||||
-- 为 member_card_transactions 表添加 updated_at 字段
|
||||
-- 版本: V13
|
||||
-- 描述: 补充更新时间字段,与BaseEntity映射对齐
|
||||
-- ============================================
|
||||
|
||||
ALTER TABLE member_card_transactions
|
||||
ADD COLUMN IF NOT EXISTS updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP;
|
||||
|
||||
COMMENT ON COLUMN member_card_transactions.updated_at IS '更新时间';
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
-- ============================================
|
||||
-- 支付订单表
|
||||
-- 版本: V14
|
||||
-- 描述: 创建支付订单表,用于记录支付交易信息
|
||||
-- ============================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS payment_order (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
order_no VARCHAR(64) NOT NULL UNIQUE,
|
||||
member_id BIGINT,
|
||||
order_type VARCHAR(32),
|
||||
goods_desc VARCHAR(256),
|
||||
trans_amt DECIMAL(12, 2),
|
||||
trade_type VARCHAR(32),
|
||||
pay_status VARCHAR(16) DEFAULT 'PENDING',
|
||||
huifu_id VARCHAR(64),
|
||||
req_seq_id VARCHAR(64),
|
||||
req_date VARCHAR(16),
|
||||
hf_seq_id VARCHAR(64),
|
||||
pay_info TEXT,
|
||||
qr_code TEXT,
|
||||
pay_url TEXT,
|
||||
remark VARCHAR(512),
|
||||
notify_url VARCHAR(512),
|
||||
pay_time TIMESTAMP,
|
||||
expire_time TIMESTAMP,
|
||||
error_code VARCHAR(64),
|
||||
error_msg VARCHAR(512),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
deleted_at TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_payment_order_order_no ON payment_order(order_no);
|
||||
CREATE INDEX IF NOT EXISTS idx_payment_order_member_id ON payment_order(member_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_payment_order_req_seq_id ON payment_order(req_seq_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_payment_order_status ON payment_order(pay_status);
|
||||
CREATE INDEX IF NOT EXISTS idx_payment_order_expire_time ON payment_order(expire_time);
|
||||
|
||||
COMMENT ON TABLE payment_order IS '支付订单表';
|
||||
COMMENT ON COLUMN payment_order.order_no IS '订单号';
|
||||
COMMENT ON COLUMN payment_order.member_id IS '会员ID';
|
||||
COMMENT ON COLUMN payment_order.order_type IS '订单类型';
|
||||
COMMENT ON COLUMN payment_order.goods_desc IS '商品描述';
|
||||
COMMENT ON COLUMN payment_order.trans_amt IS '交易金额';
|
||||
COMMENT ON COLUMN payment_order.trade_type IS '交易类型';
|
||||
COMMENT ON COLUMN payment_order.pay_status IS '支付状态:PENDING-待支付,SUCCESS-成功,FAILED-失败';
|
||||
COMMENT ON COLUMN payment_order.huifu_id IS '汇付商户ID';
|
||||
COMMENT ON COLUMN payment_order.req_seq_id IS '请求流水号';
|
||||
COMMENT ON COLUMN payment_order.req_date IS '请求日期';
|
||||
COMMENT ON COLUMN payment_order.hf_seq_id IS '汇付流水号';
|
||||
COMMENT ON COLUMN payment_order.pay_info IS '支付信息';
|
||||
COMMENT ON COLUMN payment_order.qr_code IS '二维码';
|
||||
COMMENT ON COLUMN payment_order.pay_url IS '支付链接';
|
||||
COMMENT ON COLUMN payment_order.remark IS '备注';
|
||||
COMMENT ON COLUMN payment_order.notify_url IS '通知地址';
|
||||
COMMENT ON COLUMN payment_order.pay_time IS '支付时间';
|
||||
COMMENT ON COLUMN payment_order.expire_time IS '过期时间';
|
||||
COMMENT ON COLUMN payment_order.error_code IS '错误码';
|
||||
COMMENT ON COLUMN payment_order.error_msg IS '错误信息';
|
||||
COMMENT ON COLUMN payment_order.created_at IS '创建时间';
|
||||
COMMENT ON COLUMN payment_order.updated_at IS '更新时间';
|
||||
COMMENT ON COLUMN payment_order.deleted_at IS '删除时间';
|
||||
@@ -0,0 +1,9 @@
|
||||
-- ============================================
|
||||
-- 支付订单表添加 party_order_id 字段
|
||||
-- 版本: V15
|
||||
-- 描述: 添加汇付支付返回的商户订单号
|
||||
-- ============================================
|
||||
|
||||
ALTER TABLE payment_order ADD COLUMN IF NOT EXISTS party_order_id VARCHAR(64);
|
||||
|
||||
COMMENT ON COLUMN payment_order.party_order_id IS '汇付商户订单号';
|
||||
@@ -0,0 +1,58 @@
|
||||
-- ============================================
|
||||
-- 储值卡相关表
|
||||
-- 版本: V16
|
||||
-- 描述: 创建储值卡表和充值记录表,每个会员只有一张储值卡
|
||||
-- ============================================
|
||||
|
||||
-- 储值卡表(包含支付密码)
|
||||
CREATE TABLE IF NOT EXISTS member_stored_card (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
member_id BIGINT NOT NULL,
|
||||
balance DECIMAL(10, 2) DEFAULT 0.00,
|
||||
total_recharge DECIMAL(10, 2) DEFAULT 0.00,
|
||||
total_consume DECIMAL(10, 2) DEFAULT 0.00,
|
||||
pay_password VARCHAR(255),
|
||||
is_pay_password_set BOOLEAN DEFAULT FALSE,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
deleted_at TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_member_stored_card_member_id ON member_stored_card(member_id) WHERE deleted_at IS NULL;
|
||||
|
||||
COMMENT ON TABLE member_stored_card IS '储值卡表,每个会员只有一张储值卡,包含支付密码';
|
||||
COMMENT ON COLUMN member_stored_card.member_id IS '会员用户ID';
|
||||
COMMENT ON COLUMN member_stored_card.balance IS '储值卡余额';
|
||||
COMMENT ON COLUMN member_stored_card.total_recharge IS '累计充值金额';
|
||||
COMMENT ON COLUMN member_stored_card.total_consume IS '累计消费金额';
|
||||
COMMENT ON COLUMN member_stored_card.pay_password IS '支付密码(BCrypt加密)';
|
||||
COMMENT ON COLUMN member_stored_card.is_pay_password_set IS '是否已设置支付密码';
|
||||
|
||||
-- 储值卡充值记录表
|
||||
CREATE TABLE IF NOT EXISTS member_stored_card_recharge (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
member_id BIGINT NOT NULL,
|
||||
order_no VARCHAR(64) NOT NULL,
|
||||
recharge_amount DECIMAL(10, 2) DEFAULT 0.00,
|
||||
bonus_amount DECIMAL(10, 2) DEFAULT 0.00,
|
||||
total_amount DECIMAL(10, 2) DEFAULT 0.00,
|
||||
pay_amount DECIMAL(10, 2) DEFAULT 0.00,
|
||||
status VARCHAR(32) DEFAULT 'PENDING',
|
||||
pay_time TIMESTAMP,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
deleted_at TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_stored_card_recharge_member_id ON member_stored_card_recharge(member_id) WHERE deleted_at IS NULL;
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_stored_card_recharge_order_no ON member_stored_card_recharge(order_no) WHERE deleted_at IS NULL;
|
||||
|
||||
COMMENT ON TABLE member_stored_card_recharge IS '储值卡充值记录表';
|
||||
COMMENT ON COLUMN member_stored_card_recharge.member_id IS '会员用户ID';
|
||||
COMMENT ON COLUMN member_stored_card_recharge.order_no IS '订单号';
|
||||
COMMENT ON COLUMN member_stored_card_recharge.recharge_amount IS '充值金额';
|
||||
COMMENT ON COLUMN member_stored_card_recharge.bonus_amount IS '赠送金额';
|
||||
COMMENT ON COLUMN member_stored_card_recharge.total_amount IS '到账总金额(充值+赠送)';
|
||||
COMMENT ON COLUMN member_stored_card_recharge.pay_amount IS '实际支付金额';
|
||||
COMMENT ON COLUMN member_stored_card_recharge.status IS '充值状态:PENDING-待支付,SUCCESS-成功,FAILED-失败';
|
||||
COMMENT ON COLUMN member_stored_card_recharge.pay_time IS '支付时间';
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
-- ============================================
|
||||
-- 将 group_course_booking 表的 member_card_id 改为可空
|
||||
-- 版本: V17
|
||||
-- 描述: 预约团课不再需要会员卡,member_card_id 允许为空
|
||||
-- ============================================
|
||||
|
||||
ALTER TABLE group_course_booking
|
||||
ALTER COLUMN member_card_id DROP NOT NULL;
|
||||
+209
@@ -0,0 +1,209 @@
|
||||
-- ============================================
|
||||
-- 团课测试数据
|
||||
-- 版本: V18
|
||||
-- 描述: 插入团课标签、类型、关联关系及课程数据
|
||||
-- 5个团课类型,14个标签,每类型3个标签,每类型5个团课
|
||||
-- ============================================
|
||||
|
||||
-- ============================================
|
||||
-- 1. 团课标签数据(14个)
|
||||
-- ============================================
|
||||
|
||||
INSERT INTO course_label (label_name, color, description) VALUES
|
||||
('适合新手', '#52c41a', '适合健身初学者,动作简单易学'),
|
||||
('中级过渡', '#faad14', '适合有一定基础的学员,逐步提升难度'),
|
||||
('高级进阶', '#f5222d', '适合高级学员,强度和技巧要求较高'),
|
||||
('减脂塑形', '#722ed1', '高效燃烧卡路里,助力减脂和体型塑造'),
|
||||
('增肌强化', '#13c2c2', '注重肌肉力量和围度增长'),
|
||||
('柔韧性训练', '#eb2f96', '提升身体柔韧性和关节活动度'),
|
||||
('核心训练', '#1890ff', '强化核心肌群,改善身体稳定性'),
|
||||
('心肺训练', '#fa8c16', '提升心肺功能和耐力水平'),
|
||||
('低冲击', '#52c41a', '低冲击运动,对关节友好'),
|
||||
('高强度', '#f5222d', '高强度训练,短时间内最大化训练效果'),
|
||||
('团体互动', '#722ed1', '注重团队协作和互动氛围'),
|
||||
('私教推荐', '#13c2c2', '私教推荐的高效训练课程'),
|
||||
('热门课程', '#ff1493', '人气较高,广受学员欢迎'),
|
||||
('新课上线', '#00ced1', '新上线课程,欢迎尝鲜体验')
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
|
||||
-- ============================================
|
||||
-- 2. 团课类型数据(5个)
|
||||
-- ============================================
|
||||
|
||||
INSERT INTO group_course_type (type_name, base_difficulty, description, category) VALUES
|
||||
('瑜伽', 3, '通过呼吸调控与体式练习,提升身体柔韧性、力量与心灵平静,适合各年龄段人群', '柔韧与平衡类'),
|
||||
('动感单车', 5, '伴随动感音乐节奏进行室内骑行,高效燃烧卡路里,释放压力,体验极速快感', '基础有氧与热身'),
|
||||
('HIIT训练', 8, '高强度间歇训练,通过短时爆发动作与间歇休息交替,短时间内达到最大燃脂效果', '高强度与爆发力'),
|
||||
('普拉提', 4, '注重核心肌群控制、脊柱对齐和呼吸配合,有效改善体态,预防运动损伤', '柔韧与平衡类'),
|
||||
('搏击操', 6, '融合拳击、踢腿、格斗动作的有氧运动,宣泄压力同时高效塑形,增强协调性', '高强度与爆发力')
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
|
||||
-- ============================================
|
||||
-- 3. 类型-标签关联数据(每类型3个标签,共15条)
|
||||
-- ============================================
|
||||
|
||||
INSERT INTO course_type_label (type_id, label_id)
|
||||
SELECT gt.id, cl.id FROM group_course_type gt, course_label cl
|
||||
WHERE gt.type_name = '瑜伽' AND cl.label_name = '柔韧性训练'
|
||||
OR gt.type_name = '瑜伽' AND cl.label_name = '低冲击'
|
||||
OR gt.type_name = '瑜伽' AND cl.label_name = '适合新手'
|
||||
OR gt.type_name = '动感单车' AND cl.label_name = '心肺训练'
|
||||
OR gt.type_name = '动感单车' AND cl.label_name = '热门课程'
|
||||
OR gt.type_name = '动感单车' AND cl.label_name = '中级过渡'
|
||||
OR gt.type_name = 'HIIT训练' AND cl.label_name = '高强度'
|
||||
OR gt.type_name = 'HIIT训练' AND cl.label_name = '减脂塑形'
|
||||
OR gt.type_name = 'HIIT训练' AND cl.label_name = '高级进阶'
|
||||
OR gt.type_name = '普拉提' AND cl.label_name = '核心训练'
|
||||
OR gt.type_name = '普拉提' AND cl.label_name = '柔韧性训练'
|
||||
OR gt.type_name = '普拉提' AND cl.label_name = '适合新手'
|
||||
OR gt.type_name = '搏击操' AND cl.label_name = '高强度'
|
||||
OR gt.type_name = '搏击操' AND cl.label_name = '减脂塑形'
|
||||
OR gt.type_name = '搏击操' AND cl.label_name = '心肺训练'
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
|
||||
-- ============================================
|
||||
-- 4. 团课课程数据(5个类型 × 5个课程 = 25个)
|
||||
-- start_time / end_time 分布在 2026-07-20 至 2026-08-02
|
||||
-- ============================================
|
||||
|
||||
-- ---------- 瑜伽课程(5个)----------
|
||||
|
||||
INSERT INTO group_course (course_name, course_type, start_time, end_time, max_members, current_members, status, location, cover_image, description, stored_value_amount, create_by)
|
||||
SELECT '哈他瑜伽入门', gt.id, '2026-07-20 08:00:00'::TIMESTAMP, '2026-07-20 09:00:00'::TIMESTAMP, 25, 12, '0', '瑜伽室A', '/static/course/yoga_hatha.jpg', '经典哈他瑜伽,从基础体式入手,配合呼吸引导,帮助初学者建立正确的瑜伽练习基础,感受身心的和谐统一。', 88.00, 'system'
|
||||
FROM group_course_type gt WHERE gt.type_name = '瑜伽';
|
||||
|
||||
INSERT INTO group_course (course_name, course_type, start_time, end_time, max_members, current_members, status, location, cover_image, description, stored_value_amount, create_by)
|
||||
SELECT '流瑜伽', gt.id, '2026-07-22 10:00:00'::TIMESTAMP, '2026-07-22 11:00:00'::TIMESTAMP, 20, 18, '0', '瑜伽室A', '/static/course/yoga_flow.jpg', '体式之间流畅串联,如行云流水般一气呵成。以拜日式为根基,结合站姿、平衡和扭转,提升力量与柔韧的整合能力。', 98.00, 'system'
|
||||
FROM group_course_type gt WHERE gt.type_name = '瑜伽';
|
||||
|
||||
INSERT INTO group_course (course_name, course_type, start_time, end_time, max_members, current_members, status, location, cover_image, description, stored_value_amount, create_by)
|
||||
SELECT '阴瑜伽', gt.id, '2026-07-24 18:00:00'::TIMESTAMP, '2026-07-24 19:15:00'::TIMESTAMP, 20, 8, '0', '瑜伽室B', '/static/course/yoga_yin.jpg', '阴瑜伽以长时间保持体式为特点,深度伸展筋膜和结缔组织,帮助你释放身体深层紧张,缓解一周的工作疲劳。适合所有级别。', 88.00, 'system'
|
||||
FROM group_course_type gt WHERE gt.type_name = '瑜伽';
|
||||
|
||||
INSERT INTO group_course (course_name, course_type, start_time, end_time, max_members, current_members, status, location, cover_image, description, stored_value_amount, create_by)
|
||||
SELECT '阿斯汤加瑜伽', gt.id, '2026-07-21 07:00:00'::TIMESTAMP, '2026-07-21 08:30:00'::TIMESTAMP, 15, 10, '0', '瑜伽室A', '/static/course/yoga_ashtanga.jpg', '阿斯汤加瑜伽以固定的体式序列和独特的呼吸法为核心,节奏紧凑,强度较高,能有效提升力量、柔韧和专注力。建议有一定瑜伽基础者参加。', 128.00, 'system'
|
||||
FROM group_course_type gt WHERE gt.type_name = '瑜伽';
|
||||
|
||||
INSERT INTO group_course (course_name, course_type, start_time, end_time, max_members, current_members, status, location, cover_image, description, stored_value_amount, create_by)
|
||||
SELECT '空中瑜伽', gt.id, '2026-07-23 19:00:00'::TIMESTAMP, '2026-07-23 20:00:00'::TIMESTAMP, 12, 12, '0', '瑜伽室B', '/static/course/yoga_aerial.jpg', '利用悬挂的瑜伽吊床完成各种体式,借助重力让脊柱自然延展,体验"飞翔"般的自由感。深受女性学员喜爱,名额有限请提前预约。', 128.00, 'system'
|
||||
FROM group_course_type gt WHERE gt.type_name = '瑜伽';
|
||||
|
||||
|
||||
-- ---------- 动感单车课程(5个)----------
|
||||
|
||||
INSERT INTO group_course (course_name, course_type, start_time, end_time, max_members, current_members, status, location, cover_image, description, stored_value_amount, create_by)
|
||||
SELECT '初级燃脂骑行', gt.id, '2026-07-20 19:00:00'::TIMESTAMP, '2026-07-20 19:45:00'::TIMESTAMP, 30, 22, '0', '单车房', '/static/course/spin_beginner.jpg', '适合新手的入门级骑行课程,教练将带领你掌握正确的骑行姿势和阻力调节技巧。在动感音乐中轻松燃烧300-400卡路里。', 58.00, 'system'
|
||||
FROM group_course_type gt WHERE gt.type_name = '动感单车';
|
||||
|
||||
INSERT INTO group_course (course_name, course_type, start_time, end_time, max_members, current_members, status, location, cover_image, description, stored_value_amount, create_by)
|
||||
SELECT '节奏爬坡', gt.id, '2026-07-22 18:30:00'::TIMESTAMP, '2026-07-22 19:15:00'::TIMESTAMP, 30, 15, '0', '单车房', '/static/course/spin_climb.jpg', '模拟山路爬坡骑行,通过逐渐增加阻力来挑战你的耐力和意志力。每一段爬坡后都有短暂的平路冲刺,节奏感十足,大汗淋漓的畅快体验。', 68.00, 'system'
|
||||
FROM group_course_type gt WHERE gt.type_name = '动感单车';
|
||||
|
||||
INSERT INTO group_course (course_name, course_type, start_time, end_time, max_members, current_members, status, location, cover_image, description, stored_value_amount, create_by)
|
||||
SELECT '极速冲刺', gt.id, '2026-07-24 20:00:00'::TIMESTAMP, '2026-07-24 20:45:00'::TIMESTAMP, 25, 25, '0', '单车房', '/static/course/spin_sprint.jpg', '高强度冲刺为主题的骑行课,多组30秒极限冲刺搭配短暂恢复,彻底引爆你的心肺极限。周五夜间的爆款课程,约满速度极快!', 68.00, 'system'
|
||||
FROM group_course_type gt WHERE gt.type_name = '动感单车';
|
||||
|
||||
INSERT INTO group_course (course_name, course_type, start_time, end_time, max_members, current_members, status, location, cover_image, description, stored_value_amount, create_by)
|
||||
SELECT '耐力骑行', gt.id, '2026-07-25 10:00:00'::TIMESTAMP, '2026-07-25 11:00:00'::TIMESTAMP, 30, 18, '0', '单车房', '/static/course/spin_endurance.jpg', '60分钟中低强度耐力骑行,适合周末早晨唤醒身体。在稳定的节奏中持续燃脂,配以舒缓的收尾拉伸,开启元气满满的周末。', 58.00, 'system'
|
||||
FROM group_course_type gt WHERE gt.type_name = '动感单车';
|
||||
|
||||
INSERT INTO group_course (course_name, course_type, start_time, end_time, max_members, current_members, status, location, cover_image, description, stored_value_amount, create_by)
|
||||
SELECT '音乐主题骑行', gt.id, '2026-07-21 19:30:00'::TIMESTAMP, '2026-07-21 20:15:00'::TIMESTAMP, 30, 20, '0', '单车房', '/static/course/spin_music.jpg', '以热门流行音乐为主题的骑行派对!每首歌曲对应一套骑行组合,跟着节拍加速、爬坡、冲刺,在音乐中忘记疲惫。', 68.00, 'system'
|
||||
FROM group_course_type gt WHERE gt.type_name = '动感单车';
|
||||
|
||||
|
||||
-- ---------- HIIT训练课程(5个)----------
|
||||
|
||||
INSERT INTO group_course (course_name, course_type, start_time, end_time, max_members, current_members, status, location, cover_image, description, stored_value_amount, create_by)
|
||||
SELECT 'Tabata燃脂', gt.id, '2026-07-20 07:00:00'::TIMESTAMP, '2026-07-20 07:30:00'::TIMESTAMP, 15, 8, '0', '多功能训练区', '/static/course/hiit_tabata.jpg', '经典的Tabata模式:20秒全力运动 + 10秒休息,循环8轮共4分钟,搭配热身和拉伸,30分钟全身燃爆。早晨空腹训练燃脂效果加倍!', 98.00, 'system'
|
||||
FROM group_course_type gt WHERE gt.type_name = 'HIIT训练';
|
||||
|
||||
INSERT INTO group_course (course_name, course_type, start_time, end_time, max_members, current_members, status, location, cover_image, description, stored_value_amount, create_by)
|
||||
SELECT '全身循环训练', gt.id, '2026-07-22 12:00:00'::TIMESTAMP, '2026-07-22 12:45:00'::TIMESTAMP, 15, 12, '0', '多功能训练区', '/static/course/hiit_circuit.jpg', '午间燃脂利器!8个动作站点轮流进行,涵盖深蹲跳、波比跳、壶铃摇摆、登山跑等,每个动作45秒,休息15秒,3轮循环让你全身湿透。', 98.00, 'system'
|
||||
FROM group_course_type gt WHERE gt.type_name = 'HIIT训练';
|
||||
|
||||
INSERT INTO group_course (course_name, course_type, start_time, end_time, max_members, current_members, status, location, cover_image, description, stored_value_amount, create_by)
|
||||
SELECT '核心爆发力', gt.id, '2026-07-24 17:30:00'::TIMESTAMP, '2026-07-24 18:15:00'::TIMESTAMP, 12, 5, '0', '多功能训练区', '/static/course/hiit_core.jpg', '专注于核心肌群的HIIT训练,结合药球砸地、悬垂举腿、俄罗斯转体和平板支撑变式,打造钢铁般的核心力量,提升所有运动表现。', 108.00, 'system'
|
||||
FROM group_course_type gt WHERE gt.type_name = 'HIIT训练';
|
||||
|
||||
INSERT INTO group_course (course_name, course_type, start_time, end_time, max_members, current_members, status, location, cover_image, description, stored_value_amount, create_by)
|
||||
SELECT '战绳挑战', gt.id, '2026-07-25 08:00:00'::TIMESTAMP, '2026-07-25 08:45:00'::TIMESTAMP, 10, 10, '0', '户外训练区', '/static/course/hiit_battle.jpg', '风靡全球的战绳训练!通过波浪、猛击、旋转等动作模式激活全身肌群,30秒一组高强度间歇,燃脂同时雕刻上肢线条。名额有限,已约满!', 128.00, 'system'
|
||||
FROM group_course_type gt WHERE gt.type_name = 'HIIT训练';
|
||||
|
||||
INSERT INTO group_course (course_name, course_type, start_time, end_time, max_members, current_members, status, location, cover_image, description, stored_value_amount, create_by)
|
||||
SELECT '药球HIIT', gt.id, '2026-07-26 10:00:00'::TIMESTAMP, '2026-07-26 10:45:00'::TIMESTAMP, 12, 6, '0', '多功能训练区', '/static/course/hiit_medball.jpg', '以药球为主要工具的HIIT训练。药球砸地、旋转投掷、过头抛接等动作兼具力量和爆发力训练,动作多样不枯燥,周日上午的爆汗之选。', 108.00, 'system'
|
||||
FROM group_course_type gt WHERE gt.type_name = 'HIIT训练';
|
||||
|
||||
|
||||
-- ---------- 普拉提课程(5个)----------
|
||||
|
||||
INSERT INTO group_course (course_name, course_type, start_time, end_time, max_members, current_members, status, location, cover_image, description, stored_value_amount, create_by)
|
||||
SELECT '垫上普拉提', gt.id, '2026-07-21 10:00:00'::TIMESTAMP, '2026-07-21 11:00:00'::TIMESTAMP, 20, 14, '0', '普拉提室', '/static/course/pilates_mat.jpg', '经典垫上普拉提,通过精准的动作控制激活深层核心肌群。从呼吸到骨盆稳定,从脊柱逐节卷动到四肢协调,一节课改善你的身体感知。', 108.00, 'system'
|
||||
FROM group_course_type gt WHERE gt.type_name = '普拉提';
|
||||
|
||||
INSERT INTO group_course (course_name, course_type, start_time, end_time, max_members, current_members, status, location, cover_image, description, stored_value_amount, create_by)
|
||||
SELECT '器械普拉提', gt.id, '2026-07-23 09:00:00'::TIMESTAMP, '2026-07-23 10:00:00'::TIMESTAMP, 10, 8, '0', '普拉提室', '/static/course/pilates_reformer.jpg', '使用专业普拉提核心床(Reformer)进行训练,弹簧阻力提供精准的负荷控制,适合需要针对性改善体态、康复训练或追求高效塑形的学员。', 158.00, 'system'
|
||||
FROM group_course_type gt WHERE gt.type_name = '普拉提';
|
||||
|
||||
INSERT INTO group_course (course_name, course_type, start_time, end_time, max_members, current_members, status, location, cover_image, description, stored_value_amount, create_by)
|
||||
SELECT '核心塑形', gt.id, '2026-07-25 14:00:00'::TIMESTAMP, '2026-07-25 15:00:00'::TIMESTAMP, 15, 9, '0', '普拉提室', '/static/course/pilates_sculpt.jpg', '专注于腹部、腰部和臀部的普拉提塑形课程。百次拍击、剪刀腿、肩桥等经典动作持续刺激目标肌群,打造紧致核心线条。', 128.00, 'system'
|
||||
FROM group_course_type gt WHERE gt.type_name = '普拉提';
|
||||
|
||||
INSERT INTO group_course (course_name, course_type, start_time, end_time, max_members, current_members, status, location, cover_image, description, stored_value_amount, create_by)
|
||||
SELECT '脊柱健康', gt.id, '2026-07-20 16:00:00'::TIMESTAMP, '2026-07-20 17:00:00'::TIMESTAMP, 15, 6, '0', '普拉提室', '/static/course/pilates_spine.jpg', '专为久坐办公人群设计,通过普拉提脊柱逐节运动改善驼背、圆肩等不良体态。融合猫牛式、脊柱旋转和游泳式,给你的脊柱一次深度保养。', 108.00, 'system'
|
||||
FROM group_course_type gt WHERE gt.type_name = '普拉提';
|
||||
|
||||
INSERT INTO group_course (course_name, course_type, start_time, end_time, max_members, current_members, status, location, cover_image, description, stored_value_amount, create_by)
|
||||
SELECT '产后恢复普拉提', gt.id, '2026-07-22 11:00:00'::TIMESTAMP, '2026-07-22 12:00:00'::TIMESTAMP, 10, 4, '0', '普拉提室', '/static/course/pilates_postnatal.jpg', '专为产后妈妈设计的温和修复课程,重点激活盆底肌和腹横肌,循序渐进恢复核心力量。小班教学,教练一对一关注每位学员的姿势质量。', 158.00, 'system'
|
||||
FROM group_course_type gt WHERE gt.type_name = '普拉提';
|
||||
|
||||
|
||||
-- ---------- 搏击操课程(5个)----------
|
||||
|
||||
INSERT INTO group_course (course_name, course_type, start_time, end_time, max_members, current_members, status, location, cover_image, description, stored_value_amount, create_by)
|
||||
SELECT '有氧搏击基础', gt.id, '2026-07-21 18:00:00'::TIMESTAMP, '2026-07-21 19:00:00'::TIMESTAMP, 25, 20, '0', '操厅A', '/static/course/boxing_basic.jpg', '零基础友好的搏击操入门课。从直拳、摆拳、勾拳到前踢、侧踹,教练分步讲解每个动作的要领,配合动感音乐串联成完整的搏击组合。', 68.00, 'system'
|
||||
FROM group_course_type gt WHERE gt.type_name = '搏击操';
|
||||
|
||||
INSERT INTO group_course (course_name, course_type, start_time, end_time, max_members, current_members, status, location, cover_image, description, stored_value_amount, create_by)
|
||||
SELECT 'Kickboxing进阶', gt.id, '2026-07-23 19:00:00'::TIMESTAMP, '2026-07-23 20:00:00'::TIMESTAMP, 20, 15, '0', '操厅A', '/static/course/boxing_kickboxing.jpg', '融合踢拳技术的进阶搏击课程,增加连击组合、闪躲步法和膝肘技术。强度较基础课明显提升,汗如雨下的同时释放工作压力,宣泄效果一流。', 88.00, 'system'
|
||||
FROM group_course_type gt WHERE gt.type_name = '搏击操';
|
||||
|
||||
INSERT INTO group_course (course_name, course_type, start_time, end_time, max_members, current_members, status, location, cover_image, description, stored_value_amount, create_by)
|
||||
SELECT '综合格斗体能', gt.id, '2026-07-25 16:00:00'::TIMESTAMP, '2026-07-25 17:00:00'::TIMESTAMP, 20, 12, '0', '操厅A', '/static/course/boxing_mma.jpg', '融合拳击、泰拳、摔跤元素的综合体能训练。沙袋击打、地面对抗和爆发力训练交替进行,全面提升力量、速度和耐力。周六下午的硬核之选!', 98.00, 'system'
|
||||
FROM group_course_type gt WHERE gt.type_name = '搏击操';
|
||||
|
||||
INSERT INTO group_course (course_name, course_type, start_time, end_time, max_members, current_members, status, location, cover_image, description, stored_value_amount, create_by)
|
||||
SELECT '燃脂拳击', gt.id, '2026-07-20 12:00:00'::TIMESTAMP, '2026-07-20 13:00:00'::TIMESTAMP, 25, 16, '0', '操厅A', '/static/course/boxing_fatburn.jpg', '午间燃脂拳击课。空击组合与沙袋击打间歇进行,心率维持在高燃脂区间。一节课可消耗500-700卡路里,中午来打拳比喝咖啡更提神!', 68.00, 'system'
|
||||
FROM group_course_type gt WHERE gt.type_name = '搏击操';
|
||||
|
||||
INSERT INTO group_course (course_name, course_type, start_time, end_time, max_members, current_members, status, location, cover_image, description, stored_value_amount, create_by)
|
||||
SELECT '泰拳基础', gt.id, '2026-07-26 15:00:00'::TIMESTAMP, '2026-07-26 16:00:00'::TIMESTAMP, 15, 7, '0', '操厅A', '/static/course/boxing_muaythai.jpg', '学习泰拳的八肢艺术(双拳、双腿、双膝、双肘),从基本站架到肘膝组合,感受泰拳的刚猛魅力。小班教学保证动作纠正质量。', 98.00, 'system'
|
||||
FROM group_course_type gt WHERE gt.type_name = '搏击操';
|
||||
|
||||
|
||||
-- ============================================
|
||||
-- 5. 团课推荐数据(每类型推荐1个热门课程,共5条)
|
||||
-- ============================================
|
||||
|
||||
INSERT INTO group_course_recommend (course_id, recommend_title, recommend_content, recommend_reason, priority, is_active)
|
||||
SELECT gc.id, '清晨之选 - 哈他瑜伽入门', '周一早晨8点,用温和的瑜伽唤醒身体,开启高效一周', '最受欢迎的入门瑜伽课,教练耐心细致', 5, TRUE
|
||||
FROM group_course gc WHERE gc.course_name = '哈他瑜伽入门';
|
||||
|
||||
INSERT INTO group_course_recommend (course_id, recommend_title, recommend_content, recommend_reason, priority, is_active)
|
||||
SELECT gc.id, '周五狂欢 - 极速冲刺', '周五晚8点,用极速骑行释放一周的疲惫与压力', '单车房王牌课程,周周爆满,燃烧500+卡路里', 4, TRUE
|
||||
FROM group_course gc WHERE gc.course_name = '极速冲刺';
|
||||
|
||||
INSERT INTO group_course_recommend (course_id, recommend_title, recommend_content, recommend_reason, priority, is_active)
|
||||
SELECT gc.id, '午间爆汗 - 全身循环训练', '午休45分钟,8个站点循环,高效利用碎片时间燃脂', '午间最受欢迎的HIIT课,适合忙碌的上班族', 3, TRUE
|
||||
FROM group_course gc WHERE gc.course_name = '全身循环训练';
|
||||
|
||||
INSERT INTO group_course_recommend (course_id, recommend_title, recommend_content, recommend_reason, priority, is_active)
|
||||
SELECT gc.id, '久坐救星 - 脊柱健康普拉提', '专为久坐族设计的脊柱保养课,改善驼背圆肩', '全网好评,学员反馈"一节课腰就不疼了"', 2, TRUE
|
||||
FROM group_course gc WHERE gc.course_name = '脊柱健康';
|
||||
|
||||
INSERT INTO group_course_recommend (course_id, recommend_title, recommend_content, recommend_reason, priority, is_active)
|
||||
SELECT gc.id, '释压首选 - 有氧搏击基础', '零基础也能打!在拳拳到肉的快感中忘记烦恼', '新手友好,发泄压力效果拔群,超过200名学员推荐', 1, TRUE
|
||||
FROM group_course gc WHERE gc.course_name = '有氧搏击基础';
|
||||
@@ -0,0 +1,315 @@
|
||||
-- ============================================
|
||||
-- V19: 系统初始数据(角色/用户/权限/菜单/字典/配置)
|
||||
-- 来源:合并原 V2 + V22 + V28 + V29 + V30 + V31
|
||||
-- ============================================
|
||||
|
||||
-- ============================================
|
||||
-- 1. 角色数据
|
||||
-- ============================================
|
||||
INSERT INTO sys_role (id, role_name, role_key, role_sort, status, create_by, update_by, created_at, updated_at)
|
||||
VALUES
|
||||
(1, '超级管理员', 'admin', 1, 1, 'system', 'system', NOW(), NOW()),
|
||||
(2, '测试管理员', 'test_admin', 2, 1, 'system', 'system', NOW(), NOW()),
|
||||
(3, '普通用户', 'normal_user', 3, 1, 'system', 'system', NOW(), NOW()),
|
||||
(4, '访客', 'guest', 4, 1, 'system', 'system', NOW(), NOW());
|
||||
|
||||
SELECT setval('sys_role_id_seq', 4);
|
||||
|
||||
-- ============================================
|
||||
-- 2. 用户数据(密码均为 Test@123)
|
||||
-- ============================================
|
||||
INSERT INTO sys_user (id, username, password, email, phone, nickname, status, create_by, update_by, created_at, updated_at)
|
||||
VALUES
|
||||
(1, 'admin', '$2a$12$nZ1EMUpZQljbnEdIKzH72eHlDJKUmHmHppnTTVth/SlHs5VpSAr8C', 'admin@novalon.com', '13800138000', '超级管理员', 1, 'system', 'system', NOW(), NOW()),
|
||||
(2, 'user', '$2a$12$nZ1EMUpZQljbnEdIKzH72eHlDJKUmHmHppnTTVth/SlHs5VpSAr8C', 'user@novalon.com', '13800138001', '普通用户', 1, 'system', 'system', NOW(), NOW()),
|
||||
(10,'e2e_test_user', '$2a$12$nZ1EMUpZQljbnEdIKzH72eHlDJKUmHmHppnTTVth/SlHs5VpSAr8C', 'e2e@test.com', '13900139000', 'E2E测试用户', 1, 'system', 'system', NOW(), NOW())
|
||||
ON CONFLICT (username) DO UPDATE SET
|
||||
password = EXCLUDED.password,
|
||||
status = EXCLUDED.status;
|
||||
|
||||
SELECT setval('sys_user_id_seq', 10);
|
||||
|
||||
-- ============================================
|
||||
-- 3. 用户角色关联
|
||||
-- ============================================
|
||||
INSERT INTO user_role (user_id, role_id, created_by, created_at)
|
||||
VALUES
|
||||
(1, 1, 'system', NOW()),
|
||||
(2, 3, 'system', NOW()),
|
||||
(10, 1, 'system', NOW())
|
||||
ON CONFLICT (user_id, role_id) DO NOTHING;
|
||||
|
||||
-- ============================================
|
||||
-- 4. 权限数据
|
||||
-- ============================================
|
||||
|
||||
-- 4.1 系统管理权限
|
||||
INSERT INTO sys_permission (permission_name, permission_code, resource, action, description, status, create_by, update_by, created_at, updated_at) VALUES
|
||||
('用户查看', 'system:user:view', '/api/users', 'GET', '查看用户列表', 1, 'system', 'system', NOW(), NOW()),
|
||||
('用户创建', 'system:user:create', '/api/users', 'POST', '创建用户', 1, 'system', 'system', NOW(), NOW()),
|
||||
('用户编辑', 'system:user:edit', '/api/users', 'PUT', '编辑用户', 1, 'system', 'system', NOW(), NOW()),
|
||||
('用户删除', 'system:user:delete', '/api/users', 'DELETE', '删除用户', 1, 'system', 'system', NOW(), NOW()),
|
||||
('角色查看', 'system:role:view', '/api/roles', 'GET', '查看角色列表', 1, 'system', 'system', NOW(), NOW()),
|
||||
('角色创建', 'system:role:create', '/api/roles', 'POST', '创建角色', 1, 'system', 'system', NOW(), NOW()),
|
||||
('角色编辑', 'system:role:edit', '/api/roles', 'PUT', '编辑角色', 1, 'system', 'system', NOW(), NOW()),
|
||||
('角色删除', 'system:role:delete', '/api/roles', 'DELETE', '删除角色', 1, 'system', 'system', NOW(), NOW()),
|
||||
('角色分配权限', 'system:role:assign', '/api/roles/*/permissions', 'POST', '为角色分配权限', 1, 'system', 'system', NOW(), NOW()),
|
||||
('权限查看', 'system:permission:view', '/api/permissions', 'GET', '查看权限列表', 1, 'system', 'system', NOW(), NOW()),
|
||||
('权限创建', 'system:permission:create', '/api/permissions', 'POST', '创建权限', 1, 'system', 'system', NOW(), NOW()),
|
||||
('权限编辑', 'system:permission:edit', '/api/permissions', 'PUT', '编辑权限', 1, 'system', 'system', NOW(), NOW()),
|
||||
('权限删除', 'system:permission:delete', '/api/permissions', 'DELETE', '删除权限', 1, 'system', 'system', NOW(), NOW()),
|
||||
('菜单查看', 'system:menu:view', '/api/menus', 'GET', '查看菜单列表', 1, 'system', 'system', NOW(), NOW()),
|
||||
('菜单创建', 'system:menu:create', '/api/menus', 'POST', '创建菜单', 1, 'system', 'system', NOW(), NOW()),
|
||||
('菜单编辑', 'system:menu:edit', '/api/menus', 'PUT', '编辑菜单', 1, 'system', 'system', NOW(), NOW()),
|
||||
('菜单删除', 'system:menu:delete', '/api/menus', 'DELETE', '删除菜单', 1, 'system', 'system', NOW(), NOW()),
|
||||
('字典查看', 'system:dict:view', '/api/dict', 'GET', '查看字典列表', 1, 'system', 'system', NOW(), NOW()),
|
||||
('字典创建', 'system:dict:create', '/api/dict', 'POST', '创建字典', 1, 'system', 'system', NOW(), NOW()),
|
||||
('字典编辑', 'system:dict:edit', '/api/dict', 'PUT', '编辑字典', 1, 'system', 'system', NOW(), NOW()),
|
||||
('字典删除', 'system:dict:delete', '/api/dict', 'DELETE', '删除字典', 1, 'system', 'system', NOW(), NOW()),
|
||||
('配置查看', 'system:config:view', '/api/config', 'GET', '查看系统配置', 1, 'system', 'system', NOW(), NOW()),
|
||||
('配置创建', 'system:config:create', '/api/config', 'POST', '创建系统配置', 1, 'system', 'system', NOW(), NOW()),
|
||||
('配置编辑', 'system:config:edit', '/api/config', 'PUT', '编辑系统配置', 1, 'system', 'system', NOW(), NOW()),
|
||||
('配置删除', 'system:config:delete', '/api/config', 'DELETE', '删除系统配置', 1, 'system', 'system', NOW(), NOW()),
|
||||
('日志查看', 'system:log:view', '/api/logs', 'GET', '查看日志', 1, 'system', 'system', NOW(), NOW()),
|
||||
('文件上传', 'system:file:upload', '/api/files/upload', 'POST', '上传文件', 1, 'system', 'system', NOW(), NOW()),
|
||||
('文件下载', 'system:file:download', '/api/files/download', 'GET', '下载文件', 1, 'system', 'system', NOW(), NOW()),
|
||||
('文件删除', 'system:file:delete', '/api/files', 'DELETE', '删除文件', 1, 'system', 'system', NOW(), NOW()),
|
||||
('公告查看', 'system:notice:view', '/api/notices', 'GET', '查看公告', 1, 'system', 'system', NOW(), NOW()),
|
||||
('公告创建', 'system:notice:create', '/api/notices', 'POST', '创建公告', 1, 'system', 'system', NOW(), NOW()),
|
||||
('公告编辑', 'system:notice:edit', '/api/notices', 'PUT', '编辑公告', 1, 'system', 'system', NOW(), NOW()),
|
||||
('公告删除', 'system:notice:delete', '/api/notices', 'DELETE', '删除公告', 1, 'system', 'system', NOW(), NOW());
|
||||
|
||||
-- 4.2 业务模块权限
|
||||
INSERT INTO sys_permission (permission_name, permission_code, resource, action, description, status, create_by, update_by, created_at, updated_at) VALUES
|
||||
-- 会员管理
|
||||
('会员查看', 'business:member:view', '/api/admin/members', 'GET', '查看会员列表及详情', 1, 'system', 'system', NOW(), NOW()),
|
||||
('会员编辑', 'business:member:edit', '/api/admin/member', 'PUT', '编辑会员信息', 1, 'system', 'system', NOW(), NOW()),
|
||||
('会员手机修改', 'business:member:phone', '/api/admin/member/phone', 'POST', '管理员修改会员手机号', 1, 'system', 'system', NOW(), NOW()),
|
||||
-- 会员卡管理
|
||||
('会员卡查看', 'business:memberCard:view', '/api/member-cards', 'GET', '查看会员卡列表及详情', 1, 'system', 'system', NOW(), NOW()),
|
||||
('会员卡创建', 'business:memberCard:create', '/api/member-cards', 'POST', '创建会员卡类型', 1, 'system', 'system', NOW(), NOW()),
|
||||
('会员卡编辑', 'business:memberCard:edit', '/api/member-cards', 'PUT', '编辑会员卡类型', 1, 'system', 'system', NOW(), NOW()),
|
||||
('会员卡删除', 'business:memberCard:delete', '/api/member-cards', 'DELETE', '删除会员卡类型', 1, 'system', 'system', NOW(), NOW()),
|
||||
('会员卡购买', 'business:memberCard:purchase', '/api/member-card-records/purchase', 'POST', '为会员购买/绑定会员卡', 1, 'system', 'system', NOW(), NOW()),
|
||||
('会员卡退卡', 'business:memberCard:refund', '/api/member-card-records/refund', 'POST', '会员卡退卡/解绑', 1, 'system', 'system', NOW(), NOW()),
|
||||
-- 团课管理
|
||||
('团课查看', 'business:groupCourse:view', '/api/groupCourse', 'GET', '查看团课列表及详情', 1, 'system', 'system', NOW(), NOW()),
|
||||
('团课创建', 'business:groupCourse:create', '/api/groupCourse', 'POST', '创建团课', 1, 'system', 'system', NOW(), NOW()),
|
||||
('团课编辑', 'business:groupCourse:edit', '/api/groupCourse', 'PUT', '编辑团课信息', 1, 'system', 'system', NOW(), NOW()),
|
||||
('团课删除', 'business:groupCourse:delete', '/api/groupCourse', 'DELETE', '删除团课', 1, 'system', 'system', NOW(), NOW()),
|
||||
('团课取消', 'business:groupCourse:cancel', '/api/groupCourse/cancel', 'POST', '取消团课', 1, 'system', 'system', NOW(), NOW()),
|
||||
-- 团课类型管理
|
||||
('团课类型查看', 'business:groupCourseType:view', '/api/groupCourse/types', 'GET', '查看团课类型列表', 1, 'system', 'system', NOW(), NOW()),
|
||||
('团课类型创建', 'business:groupCourseType:create', '/api/groupCourse/types', 'POST', '创建团课类型', 1, 'system', 'system', NOW(), NOW()),
|
||||
('团课类型编辑', 'business:groupCourseType:edit', '/api/groupCourse/types', 'PUT', '编辑团课类型', 1, 'system', 'system', NOW(), NOW()),
|
||||
('团课类型删除', 'business:groupCourseType:delete', '/api/groupCourse/types', 'DELETE', '删除团课类型', 1, 'system', 'system', NOW(), NOW()),
|
||||
-- 团课推荐管理
|
||||
('团课推荐查看', 'business:groupCourseRecommend:view', '/api/groupCourse/recommend', 'GET', '查看推荐团课列表', 1, 'system', 'system', NOW(), NOW()),
|
||||
('团课推荐创建', 'business:groupCourseRecommend:create','/api/groupCourse/recommend', 'POST', '创建推荐团课', 1, 'system', 'system', NOW(), NOW()),
|
||||
('团课推荐编辑', 'business:groupCourseRecommend:edit', '/api/groupCourse/recommend', 'PUT', '编辑推荐团课', 1, 'system', 'system', NOW(), NOW()),
|
||||
('团课推荐删除', 'business:groupCourseRecommend:delete','/api/groupCourse/recommend', 'DELETE', '删除推荐团课', 1, 'system', 'system', NOW(), NOW()),
|
||||
-- 团课预约管理
|
||||
('团课预约查看', 'business:groupCourseBooking:view', '/api/groupCourse/bookings', 'GET', '查看团课预约记录', 1, 'system', 'system', NOW(), NOW()),
|
||||
('团课预约创建', 'business:groupCourseBooking:create', '/api/groupCourse/book', 'POST', '预约团课', 1, 'system', 'system', NOW(), NOW()),
|
||||
('团课预约取消', 'business:groupCourseBooking:cancel', '/api/groupCourse/booking/cancel', 'POST', '取消团课预约', 1, 'system', 'system', NOW(), NOW()),
|
||||
-- 签到管理
|
||||
('签到查看', 'business:checkIn:view', '/api/checkIn/records', 'GET', '查看签到记录', 1, 'system', 'system', NOW(), NOW()),
|
||||
('签到操作', 'business:checkIn:create', '/api/checkIn', 'POST', '执行签到操作', 1, 'system', 'system', NOW(), NOW()),
|
||||
('签到导出', 'business:checkIn:export', '/api/checkIn/records/export', 'GET', '导出签到记录', 1, 'system', 'system', NOW(), NOW()),
|
||||
-- 数据统计
|
||||
('数据统计查看', 'business:dataCount:view', '/api/datacount', 'GET', '查看数据统计报表', 1, 'system', 'system', NOW(), NOW()),
|
||||
('数据统计导出', 'business:dataCount:export', '/api/datacount/export', 'GET', '导出统计报表', 1, 'system', 'system', NOW(), NOW()),
|
||||
-- 员工管理
|
||||
('员工查看', 'business:employee:view', '/api/employees/page', 'GET', '查看员工列表', 1, 'system', 'system', NOW(), NOW()),
|
||||
('员工创建', 'business:employee:create', '/api/users', 'POST', '创建员工账号', 1, 'system', 'system', NOW(), NOW()),
|
||||
('员工编辑', 'business:employee:edit', '/api/users', 'PUT', '编辑员工信息', 1, 'system', 'system', NOW(), NOW()),
|
||||
('员工删除', 'business:employee:delete', '/api/users', 'DELETE', '删除/禁用员工账号', 1, 'system', 'system', NOW(), NOW()),
|
||||
('员工分配角色', 'business:employee:assignRole', '/api/users/roles', 'POST', '为员工分配角色', 1, 'system', 'system', NOW(), NOW());
|
||||
|
||||
-- ============================================
|
||||
-- 5. 为超级管理员角色分配所有权限
|
||||
-- ============================================
|
||||
INSERT INTO sys_role_permission (role_id, permission_id, create_by, update_by, created_at, updated_at)
|
||||
SELECT 1, id, 'system', 'system', NOW(), NOW() FROM sys_permission WHERE status = 1
|
||||
AND id NOT IN (SELECT permission_id FROM sys_role_permission WHERE role_id = 1);
|
||||
|
||||
-- ============================================
|
||||
-- 6. 菜单数据
|
||||
-- ============================================
|
||||
|
||||
-- 6.1 一级菜单
|
||||
INSERT INTO sys_menu (id, menu_name, parent_id, order_num, menu_type, perms, component, status, created_at, updated_at) VALUES
|
||||
(1, '系统管理', 0, 1, 'M', NULL, NULL, 1, NOW(), NOW()),
|
||||
(2, '审计日志', 0, 2, 'M', NULL, NULL, 1, NOW(), NOW()),
|
||||
(3, '系统监控', 0, 3, 'M', NULL, NULL, 1, NOW(), NOW()),
|
||||
(400, '团课管理', 0, 4, 'M', NULL, NULL, 1, NOW(), NOW()),
|
||||
(500, '会员管理', 0, 5, 'M', NULL, NULL, 1, NOW(), NOW()),
|
||||
(502, '会员卡管理', 0, 6, 'M', NULL, NULL, 1, NOW(), NOW()),
|
||||
(504, '数据统计', 0, 7, 'M', NULL, NULL, 1, NOW(), NOW());
|
||||
|
||||
-- 6.2 系统管理 - 子菜单
|
||||
INSERT INTO sys_menu (id, menu_name, parent_id, order_num, menu_type, perms, component, status, created_at, updated_at) VALUES
|
||||
(11, '用户管理', 1, 1, 'C', 'system:user:list', 'system/user/index', 1, NOW(), NOW()),
|
||||
(12, '角色管理', 1, 2, 'C', 'system:role:list', 'system/role/index', 1, NOW(), NOW()),
|
||||
(13, '菜单管理', 1, 3, 'C', 'system:menu:list', 'system/menu/index', 1, NOW(), NOW()),
|
||||
(14, '部门管理', 1, 4, 'C', 'system:dept:list', 'system/dept/index', 1, NOW(), NOW()),
|
||||
(15, '字典管理', 1, 5, 'C', 'system:dict:list', 'system/dict/index', 1, NOW(), NOW()),
|
||||
(16, '参数管理', 1, 6, 'C', 'system:config:list', 'system/config/index', 1, NOW(), NOW()),
|
||||
(17, '通知公告', 1, 7, 'C', 'system:notice:list', 'system/notice/index', 1, NOW(), NOW()),
|
||||
(18, '文件管理', 1, 8, 'C', 'system:file:list', 'system/file/index', 1, NOW(), NOW()),
|
||||
(19, '轮播图管理', 0, 8, 'C', 'system:banner:list', 'banner/index', 1, NOW(), NOW());
|
||||
|
||||
-- 6.3 系统管理 - 按钮权限
|
||||
INSERT INTO sys_menu (id, menu_name, parent_id, order_num, menu_type, perms, component, status, created_at, updated_at) VALUES
|
||||
-- 用户管理按钮
|
||||
(111, '用户查询', 11, 1, 'F', 'system:user:query', NULL, 1, NOW(), NOW()),
|
||||
(112, '用户新增', 11, 2, 'F', 'system:user:add', NULL, 1, NOW(), NOW()),
|
||||
(113, '用户修改', 11, 3, 'F', 'system:user:edit', NULL, 1, NOW(), NOW()),
|
||||
(114, '用户删除', 11, 4, 'F', 'system:user:remove', NULL, 1, NOW(), NOW()),
|
||||
(115, '用户导出', 11, 5, 'F', 'system:user:export', NULL, 1, NOW(), NOW()),
|
||||
(116, '用户导入', 11, 6, 'F', 'system:user:import', NULL, 1, NOW(), NOW()),
|
||||
(117, '重置密码', 11, 7, 'F', 'system:user:resetPwd', NULL, 1, NOW(), NOW()),
|
||||
-- 角色管理按钮
|
||||
(121, '角色查询', 12, 1, 'F', 'system:role:query', NULL, 1, NOW(), NOW()),
|
||||
(122, '角色新增', 12, 2, 'F', 'system:role:add', NULL, 1, NOW(), NOW()),
|
||||
(123, '角色修改', 12, 3, 'F', 'system:role:edit', NULL, 1, NOW(), NOW()),
|
||||
(124, '角色删除', 12, 4, 'F', 'system:role:remove', NULL, 1, NOW(), NOW()),
|
||||
(125, '角色导出', 12, 5, 'F', 'system:role:export', NULL, 1, NOW(), NOW()),
|
||||
-- 菜单管理按钮
|
||||
(131, '菜单查询', 13, 1, 'F', 'system:menu:query', NULL, 1, NOW(), NOW()),
|
||||
(132, '菜单新增', 13, 2, 'F', 'system:menu:add', NULL, 1, NOW(), NOW()),
|
||||
(133, '菜单修改', 13, 3, 'F', 'system:menu:edit', NULL, 1, NOW(), NOW()),
|
||||
(134, '菜单删除', 13, 4, 'F', 'system:menu:remove', NULL, 1, NOW(), NOW());
|
||||
|
||||
-- 6.4 审计日志 - 子菜单
|
||||
INSERT INTO sys_menu (id, menu_name, parent_id, order_num, menu_type, perms, component, status, created_at, updated_at) VALUES
|
||||
(21, '操作日志', 2, 1, 'C', 'audit:operation:list', 'audit/operation/index', 1, NOW(), NOW()),
|
||||
(22, '登录日志', 2, 2, 'C', 'audit:login:list', 'audit/login/index', 1, NOW(), NOW()),
|
||||
(23, '异常日志', 2, 3, 'C', 'audit:exception:list', 'audit/exception/index', 1, NOW(), NOW());
|
||||
|
||||
-- 6.5 审计日志 - 按钮权限
|
||||
INSERT INTO sys_menu (id, menu_name, parent_id, order_num, menu_type, perms, component, status, created_at, updated_at) VALUES
|
||||
(211, '操作查询', 21, 1, 'F', 'audit:operation:query', NULL, 1, NOW(), NOW()),
|
||||
(212, '操作删除', 21, 2, 'F', 'audit:operation:remove', NULL, 1, NOW(), NOW()),
|
||||
(213, '操作导出', 21, 3, 'F', 'audit:operation:export', NULL, 1, NOW(), NOW()),
|
||||
(221, '登录查询', 22, 1, 'F', 'audit:login:query', NULL, 1, NOW(), NOW()),
|
||||
(222, '登录删除', 22, 2, 'F', 'audit:login:remove', NULL, 1, NOW(), NOW()),
|
||||
(223, '登录导出', 22, 3, 'F', 'audit:login:export', NULL, 1, NOW(), NOW()),
|
||||
(231, '异常查询', 23, 1, 'F', 'audit:exception:query', NULL, 1, NOW(), NOW()),
|
||||
(232, '异常删除', 23, 2, 'F', 'audit:exception:remove', NULL, 1, NOW(), NOW()),
|
||||
(233, '异常导出', 23, 3, 'F', 'audit:exception:export', NULL, 1, NOW(), NOW());
|
||||
|
||||
-- 6.6 系统监控 - 子菜单
|
||||
INSERT INTO sys_menu (id, menu_name, parent_id, order_num, menu_type, perms, component, status, created_at, updated_at) VALUES
|
||||
(31, '在线用户', 3, 1, 'C', 'monitor:online:list', 'monitor/online/index', 1, NOW(), NOW()),
|
||||
(32, '定时任务', 3, 2, 'C', 'monitor:job:list', 'monitor/job/index', 1, NOW(), NOW()),
|
||||
(33, '数据监控', 3, 3, 'C', 'monitor:data:list', 'monitor/data/index', 1, NOW(), NOW()),
|
||||
(34, '服务监控', 3, 4, 'C', 'monitor:server:list', 'monitor/server/index', 1, NOW(), NOW()),
|
||||
(35, '缓存监控', 3, 5, 'C', 'monitor:cache:list', 'monitor/cache/index', 1, NOW(), NOW());
|
||||
|
||||
-- 6.7 系统监控 - 按钮权限
|
||||
INSERT INTO sys_menu (id, menu_name, parent_id, order_num, menu_type, perms, component, status, created_at, updated_at) VALUES
|
||||
(311, '在线查询', 31, 1, 'F', 'monitor:online:query', NULL, 1, NOW(), NOW()),
|
||||
(312, '在线强退', 31, 2, 'F', 'monitor:online:forceLogout', NULL, 1, NOW(), NOW()),
|
||||
(321, '任务查询', 32, 1, 'F', 'monitor:job:query', NULL, 1, NOW(), NOW()),
|
||||
(322, '任务新增', 32, 2, 'F', 'monitor:job:add', NULL, 1, NOW(), NOW()),
|
||||
(323, '任务修改', 32, 3, 'F', 'monitor:job:edit', NULL, 1, NOW(), NOW()),
|
||||
(324, '任务删除', 32, 4, 'F', 'monitor:job:remove', NULL, 1, NOW(), NOW()),
|
||||
(325, '任务执行', 32, 5, 'F', 'monitor:job:execute', NULL, 1, NOW(), NOW());
|
||||
|
||||
-- 6.8 团课管理 - 子菜单
|
||||
INSERT INTO sys_menu (id, menu_name, parent_id, order_num, menu_type, perms, component, status, created_at, updated_at) VALUES
|
||||
(401, '团课管理', 400, 1, 'C', 'business:groupCourse:list', 'gymCourse/course/index', 1, NOW(), NOW()),
|
||||
(402, '团课类型', 400, 2, 'C', 'business:groupCourseType:list', 'gymCourse/type/index', 1, NOW(), NOW()),
|
||||
(403, '类型标签', 400, 3, 'C', 'business:groupCourseLabel:list', 'gymCourse/label/index', 1, NOW(), NOW()),
|
||||
(404, '团课推荐', 400, 4, 'C', 'business:groupCourseRecommend:list', 'gymCourse/recommend/index', 1, NOW(), NOW());
|
||||
|
||||
-- 6.9 团课管理 - 按钮权限
|
||||
INSERT INTO sys_menu (id, menu_name, parent_id, order_num, menu_type, perms, component, status, created_at, updated_at) VALUES
|
||||
(4011, '团课查询', 401, 1, 'F', 'business:groupCourse:query', NULL, 1, NOW(), NOW()),
|
||||
(4012, '团课新增', 401, 2, 'F', 'business:groupCourse:add', NULL, 1, NOW(), NOW()),
|
||||
(4013, '团课修改', 401, 3, 'F', 'business:groupCourse:edit', NULL, 1, NOW(), NOW()),
|
||||
(4014, '团课删除', 401, 4, 'F', 'business:groupCourse:remove', NULL, 1, NOW(), NOW()),
|
||||
(4015, '团课取消', 401, 5, 'F', 'business:groupCourse:cancel', NULL, 1, NOW(), NOW()),
|
||||
(4021, '类型查询', 402, 1, 'F', 'business:groupCourseType:query', NULL, 1, NOW(), NOW()),
|
||||
(4022, '类型新增', 402, 2, 'F', 'business:groupCourseType:add', NULL, 1, NOW(), NOW()),
|
||||
(4023, '类型修改', 402, 3, 'F', 'business:groupCourseType:edit', NULL, 1, NOW(), NOW()),
|
||||
(4024, '类型删除', 402, 4, 'F', 'business:groupCourseType:remove', NULL, 1, NOW(), NOW()),
|
||||
(4031, '标签查询', 403, 1, 'F', 'business:groupCourseLabel:query', NULL, 1, NOW(), NOW()),
|
||||
(4032, '标签新增', 403, 2, 'F', 'business:groupCourseLabel:add', NULL, 1, NOW(), NOW()),
|
||||
(4033, '标签修改', 403, 3, 'F', 'business:groupCourseLabel:edit', NULL, 1, NOW(), NOW()),
|
||||
(4034, '标签删除', 403, 4, 'F', 'business:groupCourseLabel:remove', NULL, 1, NOW(), NOW()),
|
||||
(4041, '推荐查询', 404, 1, 'F', 'business:groupCourseRecommend:query', NULL, 1, NOW(), NOW()),
|
||||
(4042, '推荐新增', 404, 2, 'F', 'business:groupCourseRecommend:add', NULL, 1, NOW(), NOW()),
|
||||
(4043, '推荐修改', 404, 3, 'F', 'business:groupCourseRecommend:edit', NULL, 1, NOW(), NOW()),
|
||||
(4044, '推荐删除', 404, 4, 'F', 'business:groupCourseRecommend:remove', NULL, 1, NOW(), NOW());
|
||||
|
||||
-- 6.10 会员管理 - 子菜单
|
||||
INSERT INTO sys_menu (id, menu_name, parent_id, order_num, menu_type, perms, component, status, created_at, updated_at) VALUES
|
||||
(501, '会员管理', 500, 1, 'C', 'business:member:list', 'member/member/index', 1, NOW(), NOW()),
|
||||
(503, '会员卡管理', 502, 1, 'C', 'business:memberCard:list', 'member/card/index', 1, NOW(), NOW()),
|
||||
(505, '数据统计看板', 504, 1, 'C', 'business:statistics:view', 'statistics/dashboard/index', 1, NOW(), NOW());
|
||||
|
||||
-- 6.11 会员管理 - 按钮权限
|
||||
INSERT INTO sys_menu (id, menu_name, parent_id, order_num, menu_type, perms, component, status, created_at, updated_at) VALUES
|
||||
(5011, '会员查询', 501, 1, 'F', 'business:member:query', NULL, 1, NOW(), NOW()),
|
||||
(5012, '会员新增', 501, 2, 'F', 'business:member:add', NULL, 1, NOW(), NOW()),
|
||||
(5013, '会员修改', 501, 3, 'F', 'business:member:edit', NULL, 1, NOW(), NOW()),
|
||||
(5014, '会员删除', 501, 4, 'F', 'business:member:remove', NULL, 1, NOW(), NOW()),
|
||||
(5015, '会员详情', 501, 5, 'F', 'business:member:detail', NULL, 1, NOW(), NOW()),
|
||||
(5031, '会员卡查询', 503, 1, 'F', 'business:memberCard:query', NULL, 1, NOW(), NOW()),
|
||||
(5032, '会员卡新增', 503, 2, 'F', 'business:memberCard:add', NULL, 1, NOW(), NOW()),
|
||||
(5033, '会员卡修改', 503, 3, 'F', 'business:memberCard:edit', NULL, 1, NOW(), NOW()),
|
||||
(5034, '会员卡删除', 503, 4, 'F', 'business:memberCard:remove', NULL, 1, NOW(), NOW()),
|
||||
(5051, '数据统计导出', 505, 1, 'F', 'business:statistics:export', NULL, 1, NOW(), NOW());
|
||||
|
||||
-- ============================================
|
||||
-- 7. 字典数据
|
||||
-- ============================================
|
||||
|
||||
-- 7.1 字典类型
|
||||
INSERT INTO sys_dict_type (dict_name, dict_type, status, remark, create_by, update_by, created_at, updated_at)
|
||||
VALUES
|
||||
('用户状态', 'user_status', '0', '用户状态列表', 'system', 'system', NOW(), NOW()),
|
||||
('菜单状态', 'menu_status', '0', '菜单状态列表', 'system', 'system', NOW(), NOW()),
|
||||
('角色状态', 'role_status', '0', '角色状态列表', 'system', 'system', NOW(), NOW()),
|
||||
('系统开关', 'sys_normal_disable', '0', '系统开关列表', 'system', 'system', NOW(), NOW())
|
||||
ON CONFLICT (dict_type) DO NOTHING;
|
||||
|
||||
-- 7.2 字典数据
|
||||
INSERT INTO sys_dict_data (dict_sort, dict_label, dict_value, dict_type, css_class, list_class, is_default, status, create_by, update_by, created_at, updated_at)
|
||||
VALUES
|
||||
(1, '正常', '1', 'user_status', '', 'primary', 'Y', '0', 'system', 'system', NOW(), NOW()),
|
||||
(2, '停用', '0', 'user_status', '', 'danger', 'N', '0', 'system', 'system', NOW(), NOW()),
|
||||
(1, '正常', '0', 'menu_status', '', 'primary', 'Y', '0', 'system', 'system', NOW(), NOW()),
|
||||
(2, '停用', '1', 'menu_status', '', 'danger', 'N', '0', 'system', 'system', NOW(), NOW()),
|
||||
(1, '正常', '0', 'role_status', '', 'primary', 'Y', '0', 'system', 'system', NOW(), NOW()),
|
||||
(2, '停用', '1', 'role_status', '', 'danger', 'N', '0', 'system', 'system', NOW(), NOW()),
|
||||
(1, '正常', '0', 'sys_normal_disable', '', 'primary', 'Y', '0', 'system', 'system', NOW(), NOW()),
|
||||
(2, '停用', '1', 'sys_normal_disable', '', 'danger', 'N', '0', 'system', 'system', NOW(), NOW());
|
||||
|
||||
-- ============================================
|
||||
-- 8. 系统配置
|
||||
-- ============================================
|
||||
INSERT INTO sys_config (config_name, config_key, config_value, config_type, create_by, update_by, created_at, updated_at)
|
||||
VALUES
|
||||
('用户管理-用户初始密码', 'sys.user.initPassword', '123456', 'Y', 'system', 'system', NOW(), NOW()),
|
||||
('主框架页-默认皮肤样式名称', 'sys.index.skinName', 'skin-blue','Y', 'system', 'system', NOW(), NOW()),
|
||||
('用户自助-验证码开关', 'sys.account.captchaEnabled', 'true', 'Y', 'system', 'system', NOW(), NOW()),
|
||||
('用户自助-是否开启用户注册功能', 'sys.account.registerUser', 'false', 'Y', 'system', 'system', NOW(), NOW()),
|
||||
('账号自助-密码验证码', 'sys.account.pwdCaptchaEnabled','true', 'Y', 'system', 'system', NOW(), NOW()),
|
||||
('团课-取消预约免手续费次数', 'group_course.cancel_free_limit','3', 'Y', 'system', 'system', NOW(), NOW())
|
||||
ON CONFLICT (config_key) DO NOTHING;
|
||||
|
||||
-- ============================================
|
||||
-- 9. 重置所有序列值
|
||||
-- ============================================
|
||||
SELECT setval('sys_user_id_seq', (SELECT COALESCE(MAX(id), 1) FROM sys_user));
|
||||
SELECT setval('sys_role_id_seq', (SELECT COALESCE(MAX(id), 1) FROM sys_role));
|
||||
SELECT setval('user_role_id_seq', (SELECT COALESCE(MAX(id), 1) FROM user_role));
|
||||
SELECT setval('sys_permission_id_seq', (SELECT COALESCE(MAX(id), 1) FROM sys_permission));
|
||||
SELECT setval('sys_role_permission_id_seq', (SELECT COALESCE(MAX(id), 1) FROM sys_role_permission));
|
||||
SELECT setval('sys_menu_id_seq', (SELECT COALESCE(MAX(id), 1) FROM sys_menu));
|
||||
SELECT setval('sys_dict_type_id_seq', (SELECT COALESCE(MAX(id), 1) FROM sys_dict_type));
|
||||
SELECT setval('sys_dict_data_id_seq', (SELECT COALESCE(MAX(id), 1) FROM sys_dict_data));
|
||||
SELECT setval('sys_config_id_seq', (SELECT COALESCE(MAX(id), 1) FROM sys_config));
|
||||
+25
-14
@@ -1,6 +1,8 @@
|
||||
-- ============================================
|
||||
-- Novalon管理系统数据库初始化脚本
|
||||
-- 版本: V1
|
||||
-- 描述: 创建所有核心表结构(合并版)
|
||||
-- 描述: 创建所有核心系统表结构
|
||||
-- ============================================
|
||||
|
||||
-- ============================================
|
||||
-- 用户与角色相关表
|
||||
@@ -140,7 +142,7 @@ CREATE TABLE IF NOT EXISTS sys_dict_data (
|
||||
deleted_at TIMESTAMP
|
||||
);
|
||||
|
||||
-- 字典表(通用字典)
|
||||
-- 通用字典表
|
||||
CREATE TABLE IF NOT EXISTS sys_dictionary (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
type VARCHAR(100) NOT NULL,
|
||||
@@ -165,7 +167,7 @@ CREATE TABLE IF NOT EXISTS sys_config (
|
||||
config_name VARCHAR(100) NOT NULL,
|
||||
config_key VARCHAR(100) NOT NULL UNIQUE,
|
||||
config_value VARCHAR(500) NOT NULL,
|
||||
config_type VARCHAR(1) DEFAULT 'N',
|
||||
config_type VARCHAR(1) DEFAULT 'Y',
|
||||
create_by VARCHAR(50),
|
||||
update_by VARCHAR(50),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
@@ -355,15 +357,6 @@ COMMENT ON TABLE sys_dict_data IS '字典数据表';
|
||||
COMMENT ON TABLE sys_dictionary IS '通用字典表';
|
||||
COMMENT ON TABLE sys_config IS '系统配置表';
|
||||
COMMENT ON TABLE sys_login_log IS '登录日志表';
|
||||
COMMENT ON TABLE sys_exception_log IS '异常日志表';
|
||||
COMMENT ON TABLE operation_log IS '操作日志表';
|
||||
COMMENT ON TABLE audit_log IS '审计日志表';
|
||||
COMMENT ON TABLE audit_log_archive IS '审计日志归档表';
|
||||
COMMENT ON TABLE sys_notice IS '系统公告表';
|
||||
COMMENT ON TABLE sys_user_message IS '用户消息表';
|
||||
COMMENT ON TABLE sys_file IS '文件管理表';
|
||||
COMMENT ON TABLE oauth2_client IS 'OAuth2客户端表';
|
||||
|
||||
COMMENT ON TABLE sys_exception_log IS '异常日志表';
|
||||
COMMENT ON COLUMN sys_exception_log.id IS '主键ID';
|
||||
COMMENT ON COLUMN sys_exception_log.username IS '操作用户';
|
||||
@@ -375,7 +368,22 @@ COMMENT ON COLUMN sys_exception_log.exception_msg IS '异常消息';
|
||||
COMMENT ON COLUMN sys_exception_log.exception_stack IS '异常堆栈';
|
||||
COMMENT ON COLUMN sys_exception_log.ip IS 'IP地址';
|
||||
COMMENT ON COLUMN sys_exception_log.create_time IS '创建时间';
|
||||
|
||||
COMMENT ON TABLE operation_log IS '操作日志表';
|
||||
COMMENT ON COLUMN operation_log.id IS '主键ID';
|
||||
COMMENT ON COLUMN operation_log.username IS '操作用户';
|
||||
COMMENT ON COLUMN operation_log.operation IS '操作描述';
|
||||
COMMENT ON COLUMN operation_log.method IS '请求方法';
|
||||
COMMENT ON COLUMN operation_log.params IS '请求参数';
|
||||
COMMENT ON COLUMN operation_log.result IS '操作结果';
|
||||
COMMENT ON COLUMN operation_log.ip IS 'IP地址';
|
||||
COMMENT ON COLUMN operation_log.duration IS '执行时长(毫秒)';
|
||||
COMMENT ON COLUMN operation_log.status IS '操作状态(0成功 1失败)';
|
||||
COMMENT ON COLUMN operation_log.error_msg IS '错误消息';
|
||||
COMMENT ON COLUMN operation_log.create_by IS '创建人';
|
||||
COMMENT ON COLUMN operation_log.update_by IS '更新人';
|
||||
COMMENT ON COLUMN operation_log.created_at IS '创建时间';
|
||||
COMMENT ON COLUMN operation_log.updated_at IS '更新时间';
|
||||
COMMENT ON COLUMN operation_log.deleted_at IS '删除时间';
|
||||
COMMENT ON TABLE audit_log IS '审计日志表';
|
||||
COMMENT ON COLUMN audit_log.id IS '主键ID';
|
||||
COMMENT ON COLUMN audit_log.entity_type IS '实体类型(如User, Role等)';
|
||||
@@ -389,7 +397,6 @@ COMMENT ON COLUMN audit_log.changed_fields IS '变更字段列表';
|
||||
COMMENT ON COLUMN audit_log.ip_address IS 'IP地址';
|
||||
COMMENT ON COLUMN audit_log.description IS '操作描述';
|
||||
COMMENT ON COLUMN audit_log.created_at IS '记录创建时间';
|
||||
|
||||
COMMENT ON TABLE audit_log_archive IS '审计日志归档表';
|
||||
COMMENT ON COLUMN audit_log_archive.id IS '主键ID';
|
||||
COMMENT ON COLUMN audit_log_archive.entity_type IS '实体类型(如User, Role等)';
|
||||
@@ -405,3 +412,7 @@ COMMENT ON COLUMN audit_log_archive.user_agent IS '用户代理';
|
||||
COMMENT ON COLUMN audit_log_archive.description IS '操作描述';
|
||||
COMMENT ON COLUMN audit_log_archive.created_at IS '记录创建时间';
|
||||
COMMENT ON COLUMN audit_log_archive.archived_at IS '归档时间';
|
||||
COMMENT ON TABLE sys_notice IS '系统公告表';
|
||||
COMMENT ON TABLE sys_user_message IS '用户消息表';
|
||||
COMMENT ON TABLE sys_file IS '文件管理表';
|
||||
COMMENT ON TABLE oauth2_client IS 'OAuth2客户端表';
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
-- ============================================
|
||||
-- V20: 新增轮播图管理菜单
|
||||
-- 描述: 为首页轮播图管理功能创建菜单项
|
||||
-- ============================================
|
||||
|
||||
-- 轮播图管理子菜单(页面),归属系统管理目录
|
||||
INSERT INTO sys_menu (id, menu_name, parent_id, order_num, menu_type, perms, component, status, created_at, updated_at) VALUES
|
||||
(19, '轮播图管理', 0, 8, 'C', 'system:banner:list', 'banner/index', 1, NOW(), NOW());
|
||||
|
||||
-- 轮播图管理按钮权限
|
||||
INSERT INTO sys_menu (id, menu_name, parent_id, order_num, menu_type, perms, component, status, created_at, updated_at) VALUES
|
||||
(191, '轮播图查询', 19, 1, 'F', 'system:banner:query', NULL, 1, NOW(), NOW()),
|
||||
(192, '轮播图新增', 19, 2, 'F', 'system:banner:add', NULL, 1, NOW(), NOW()),
|
||||
(193, '轮播图修改', 19, 3, 'F', 'system:banner:edit', NULL, 1, NOW(), NOW()),
|
||||
(194, '轮播图删除', 19, 4, 'F', 'system:banner:remove', NULL, 1, NOW(), NOW());
|
||||
|
||||
-- 重置菜单序列
|
||||
SELECT setval('sys_menu_id_seq', (SELECT COALESCE(MAX(id), 1) FROM sys_menu));
|
||||
@@ -0,0 +1,20 @@
|
||||
-- ============================================
|
||||
-- V21: 创建轮播图表并更新菜单为顶级菜单
|
||||
-- ============================================
|
||||
|
||||
-- 创建 banner 表
|
||||
CREATE TABLE IF NOT EXISTS banner (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
image_url VARCHAR(512) NOT NULL,
|
||||
title VARCHAR(100) NOT NULL,
|
||||
subtitle TEXT,
|
||||
sort_order INT DEFAULT 0,
|
||||
is_active VARCHAR(1) DEFAULT '1',
|
||||
link_url VARCHAR(512),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
deleted_at TIMESTAMP
|
||||
);
|
||||
|
||||
-- 将轮播图管理菜单从系统管理子菜单调整为顶级菜单(parent_id=0, sort=8)
|
||||
UPDATE sys_menu SET parent_id = 0, order_num = 8 WHERE id = 19;
|
||||
@@ -0,0 +1,141 @@
|
||||
-- ============================================
|
||||
-- Novalon管理系统索引优化脚本
|
||||
-- 版本: V2
|
||||
-- 描述: 为核心系统表创建必要的索引以提升查询性能
|
||||
-- ============================================
|
||||
|
||||
-- ============================================
|
||||
-- 用户与角色表索引
|
||||
-- ============================================
|
||||
|
||||
-- 用户表索引
|
||||
CREATE INDEX IF NOT EXISTS idx_users_username ON sys_user(username);
|
||||
CREATE INDEX IF NOT EXISTS idx_users_email ON sys_user(email);
|
||||
CREATE INDEX IF NOT EXISTS idx_users_status ON sys_user(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_users_deleted_at ON sys_user(deleted_at);
|
||||
|
||||
-- 角色表索引
|
||||
CREATE INDEX IF NOT EXISTS idx_roles_role_key ON sys_role(role_key);
|
||||
CREATE INDEX IF NOT EXISTS idx_roles_status ON sys_role(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_roles_deleted_at ON sys_role(deleted_at);
|
||||
|
||||
-- 用户角色关联表索引
|
||||
CREATE INDEX IF NOT EXISTS idx_user_role_user_id ON user_role(user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_user_role_role_id ON user_role(role_id);
|
||||
|
||||
-- ============================================
|
||||
-- 权限表索引
|
||||
-- ============================================
|
||||
|
||||
-- 权限表索引
|
||||
CREATE INDEX IF NOT EXISTS idx_permission_code ON sys_permission(permission_code);
|
||||
CREATE INDEX IF NOT EXISTS idx_permission_resource ON sys_permission(resource);
|
||||
CREATE INDEX IF NOT EXISTS idx_permission_status ON sys_permission(status);
|
||||
|
||||
-- 角色权限关联表索引
|
||||
CREATE INDEX IF NOT EXISTS idx_role_permission_role_id ON sys_role_permission(role_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_role_permission_permission_id ON sys_role_permission(permission_id);
|
||||
|
||||
-- ============================================
|
||||
-- 菜单表索引
|
||||
-- ============================================
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_sys_menu_parent_id ON sys_menu(parent_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_sys_menu_status ON sys_menu(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_sys_menu_deleted_at ON sys_menu(deleted_at);
|
||||
|
||||
-- ============================================
|
||||
-- 字典表索引
|
||||
-- ============================================
|
||||
|
||||
-- 字典类型表索引
|
||||
CREATE INDEX IF NOT EXISTS idx_sys_dict_type_dict_type ON sys_dict_type(dict_type);
|
||||
CREATE INDEX IF NOT EXISTS idx_sys_dict_type_status ON sys_dict_type(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_sys_dict_type_deleted_at ON sys_dict_type(deleted_at);
|
||||
|
||||
-- 字典数据表索引
|
||||
CREATE INDEX IF NOT EXISTS idx_sys_dict_data_dict_type ON sys_dict_data(dict_type);
|
||||
CREATE INDEX IF NOT EXISTS idx_sys_dict_data_dict_value ON sys_dict_data(dict_value);
|
||||
CREATE INDEX IF NOT EXISTS idx_sys_dict_data_status ON sys_dict_data(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_sys_dict_data_deleted_at ON sys_dict_data(deleted_at);
|
||||
|
||||
-- 通用字典表索引
|
||||
CREATE INDEX IF NOT EXISTS idx_sys_dictionary_type ON sys_dictionary(type);
|
||||
CREATE INDEX IF NOT EXISTS idx_sys_dictionary_type_code ON sys_dictionary(type, code);
|
||||
CREATE INDEX IF NOT EXISTS idx_sys_dictionary_deleted_at ON sys_dictionary(deleted_at);
|
||||
|
||||
-- ============================================
|
||||
-- 系统配置表索引
|
||||
-- ============================================
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_sys_config_config_key ON sys_config(config_key);
|
||||
CREATE INDEX IF NOT EXISTS idx_sys_config_config_type ON sys_config(config_type);
|
||||
CREATE INDEX IF NOT EXISTS idx_sys_config_deleted_at ON sys_config(deleted_at);
|
||||
|
||||
-- ============================================
|
||||
-- 日志表索引
|
||||
-- ============================================
|
||||
|
||||
-- 登录日志表索引
|
||||
CREATE INDEX IF NOT EXISTS idx_sys_login_log_username ON sys_login_log(username);
|
||||
CREATE INDEX IF NOT EXISTS idx_sys_login_log_ip ON sys_login_log(ip);
|
||||
CREATE INDEX IF NOT EXISTS idx_sys_login_log_status ON sys_login_log(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_sys_login_log_login_time ON sys_login_log(login_time);
|
||||
|
||||
-- 异常日志表索引
|
||||
CREATE INDEX IF NOT EXISTS idx_sys_exception_log_username ON sys_exception_log(username);
|
||||
CREATE INDEX IF NOT EXISTS idx_sys_exception_log_exception_name ON sys_exception_log(exception_name);
|
||||
CREATE INDEX IF NOT EXISTS idx_sys_exception_log_create_time ON sys_exception_log(create_time);
|
||||
|
||||
-- 操作日志表索引
|
||||
CREATE INDEX IF NOT EXISTS idx_operation_log_username ON operation_log(username);
|
||||
CREATE INDEX IF NOT EXISTS idx_operation_log_operation ON operation_log(operation);
|
||||
CREATE INDEX IF NOT EXISTS idx_operation_log_created_at ON operation_log(created_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_operation_log_status ON operation_log(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_operation_log_deleted_at ON operation_log(deleted_at);
|
||||
|
||||
-- 审计日志表索引
|
||||
CREATE INDEX IF NOT EXISTS idx_audit_log_entity_type ON audit_log(entity_type);
|
||||
CREATE INDEX IF NOT EXISTS idx_audit_log_entity_id ON audit_log(entity_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_audit_log_operation_type ON audit_log(operation_type);
|
||||
CREATE INDEX IF NOT EXISTS idx_audit_log_operator ON audit_log(operator);
|
||||
CREATE INDEX IF NOT EXISTS idx_audit_log_operation_time ON audit_log(operation_time);
|
||||
CREATE INDEX IF NOT EXISTS idx_audit_log_entity ON audit_log(entity_type, entity_id);
|
||||
|
||||
-- 审计日志归档表索引
|
||||
CREATE INDEX IF NOT EXISTS idx_audit_log_archive_entity_type ON audit_log_archive(entity_type);
|
||||
CREATE INDEX IF NOT EXISTS idx_audit_log_archive_entity_id ON audit_log_archive(entity_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_audit_log_archive_operation_type ON audit_log_archive(operation_type);
|
||||
CREATE INDEX IF NOT EXISTS idx_audit_log_archive_operator ON audit_log_archive(operator);
|
||||
CREATE INDEX IF NOT EXISTS idx_audit_log_archive_operation_time ON audit_log_archive(operation_time);
|
||||
CREATE INDEX IF NOT EXISTS idx_audit_log_archive_archived_at ON audit_log_archive(archived_at);
|
||||
|
||||
-- ============================================
|
||||
-- 通知与消息表索引
|
||||
-- ============================================
|
||||
|
||||
-- 系统公告表索引
|
||||
CREATE INDEX IF NOT EXISTS idx_sys_notice_notice_type ON sys_notice(notice_type);
|
||||
CREATE INDEX IF NOT EXISTS idx_sys_notice_status ON sys_notice(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_sys_notice_deleted_at ON sys_notice(deleted_at);
|
||||
|
||||
-- 用户消息表索引
|
||||
CREATE INDEX IF NOT EXISTS idx_sys_user_message_user_id ON sys_user_message(user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_sys_user_message_notice_id ON sys_user_message(notice_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_sys_user_message_is_read ON sys_user_message(is_read);
|
||||
CREATE INDEX IF NOT EXISTS idx_sys_user_message_deleted_at ON sys_user_message(deleted_at);
|
||||
|
||||
-- ============================================
|
||||
-- 文件管理表索引
|
||||
-- ============================================
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_sys_file_file_type ON sys_file(file_type);
|
||||
CREATE INDEX IF NOT EXISTS idx_sys_file_deleted_at ON sys_file(deleted_at);
|
||||
|
||||
-- ============================================
|
||||
-- OAuth2客户端表索引
|
||||
-- ============================================
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_oauth2_client_client_id ON oauth2_client(client_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_oauth2_client_enabled ON oauth2_client(enabled);
|
||||
CREATE INDEX IF NOT EXISTS idx_oauth2_client_deleted_at ON oauth2_client(deleted_at);
|
||||
@@ -0,0 +1,15 @@
|
||||
-- ============================================
|
||||
-- Novalon管理系统权限授予脚本
|
||||
-- 版本: V3
|
||||
-- 描述: 为novalon用户授予所有表的访问权限
|
||||
-- ============================================
|
||||
|
||||
-- 授予所有表的SELECT, INSERT, UPDATE, DELETE权限
|
||||
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO novalon;
|
||||
|
||||
-- 授予所有序列的使用权限
|
||||
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO novalon;
|
||||
|
||||
-- 设置默认权限,使未来创建的表自动授予novalon用户权限
|
||||
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO novalon;
|
||||
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT USAGE, SELECT ON SEQUENCES TO novalon;
|
||||
+238
@@ -0,0 +1,238 @@
|
||||
-- ============================================
|
||||
-- 会员相关表
|
||||
-- 版本: V4
|
||||
-- 描述: 创建会员、微信用户、会员卡类型、会员卡记录、交易流水、退款申请等表
|
||||
-- ============================================
|
||||
|
||||
-- ============================================
|
||||
-- 1. member_user 表(会员表)
|
||||
-- ============================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS member_user (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
member_no VARCHAR(50) NOT NULL UNIQUE,
|
||||
nickname VARCHAR(100),
|
||||
phone VARCHAR(255),
|
||||
gender INTEGER DEFAULT 0,
|
||||
birthday DATE,
|
||||
address VARCHAR(500),
|
||||
avatar VARCHAR(500),
|
||||
subscribed BOOLEAN DEFAULT FALSE,
|
||||
last_login_at TIMESTAMP,
|
||||
|
||||
union_id VARCHAR(100),
|
||||
miniapp_open_id VARCHAR(100),
|
||||
official_open_id VARCHAR(100),
|
||||
|
||||
is_deleted BOOLEAN DEFAULT FALSE
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_member_user_member_no ON member_user(member_no);
|
||||
CREATE INDEX IF NOT EXISTS idx_member_user_union_id ON member_user(union_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_member_user_miniapp_openid ON member_user(miniapp_open_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_member_user_official_openid ON member_user(official_open_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_member_user_phone ON member_user(phone);
|
||||
CREATE INDEX IF NOT EXISTS idx_member_user_is_deleted ON member_user(is_deleted);
|
||||
|
||||
COMMENT ON TABLE member_user IS '会员表';
|
||||
COMMENT ON COLUMN member_user.id IS '主键ID';
|
||||
COMMENT ON COLUMN member_user.created_at IS '创建时间';
|
||||
COMMENT ON COLUMN member_user.updated_at IS '更新时间';
|
||||
COMMENT ON COLUMN member_user.member_no IS '会员编号(唯一,格式:MEM + 8位随机字符)';
|
||||
COMMENT ON COLUMN member_user.nickname IS '昵称';
|
||||
COMMENT ON COLUMN member_user.phone IS '手机号(AES-128-CBC加密存储)';
|
||||
COMMENT ON COLUMN member_user.gender IS '性别:0-未知,1-男,2-女';
|
||||
COMMENT ON COLUMN member_user.birthday IS '生日';
|
||||
COMMENT ON COLUMN member_user.address IS '地址';
|
||||
COMMENT ON COLUMN member_user.avatar IS '头像URL';
|
||||
COMMENT ON COLUMN member_user.subscribed IS '是否关注服务号:true-已关注,false-未关注';
|
||||
COMMENT ON COLUMN member_user.last_login_at IS '最后登录时间';
|
||||
COMMENT ON COLUMN member_user.union_id IS '微信UnionID(用户在开放平台的唯一标识,跨应用相同)';
|
||||
COMMENT ON COLUMN member_user.miniapp_open_id IS '小程序OpenID(用户在当前小程序的唯一标识)';
|
||||
COMMENT ON COLUMN member_user.official_open_id IS '服务号OpenID(用户在当前服务号的唯一标识)';
|
||||
COMMENT ON COLUMN member_user.is_deleted IS '是否删除(软删除标记):false-正常,true-已删除';
|
||||
|
||||
-- ============================================
|
||||
-- 2. wechat_user 表(微信用户表)
|
||||
-- ============================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS wechat_user (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
member_id BIGINT NOT NULL,
|
||||
|
||||
union_id VARCHAR(100),
|
||||
miniapp_openid VARCHAR(100),
|
||||
mp_openid VARCHAR(100),
|
||||
|
||||
is_subscribed BOOLEAN DEFAULT FALSE,
|
||||
subscribe_time TIMESTAMP,
|
||||
unsubscribe_time TIMESTAMP
|
||||
);
|
||||
|
||||
ALTER TABLE wechat_user
|
||||
ADD CONSTRAINT fk_wechat_user_member
|
||||
FOREIGN KEY (member_id) REFERENCES member_user(id) ON DELETE CASCADE;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_wechat_user_union_id ON wechat_user(union_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_wechat_user_miniapp_openid ON wechat_user(miniapp_openid);
|
||||
CREATE INDEX IF NOT EXISTS idx_wechat_user_mp_openid ON wechat_user(mp_openid);
|
||||
CREATE INDEX IF NOT EXISTS idx_wechat_user_member_id ON wechat_user(member_id);
|
||||
|
||||
COMMENT ON TABLE wechat_user IS '微信用户表';
|
||||
COMMENT ON COLUMN wechat_user.id IS '主键ID';
|
||||
COMMENT ON COLUMN wechat_user.created_at IS '创建时间';
|
||||
COMMENT ON COLUMN wechat_user.updated_at IS '更新时间';
|
||||
COMMENT ON COLUMN wechat_user.member_id IS '会员ID(关联 member_user 表的 id 字段)';
|
||||
COMMENT ON COLUMN wechat_user.union_id IS '微信UnionID(用户在开放平台的唯一标识)';
|
||||
COMMENT ON COLUMN wechat_user.miniapp_openid IS '小程序OpenID(用户在当前小程序的唯一标识)';
|
||||
COMMENT ON COLUMN wechat_user.mp_openid IS '服务号OpenID(用户在当前服务号的唯一标识)';
|
||||
COMMENT ON COLUMN wechat_user.is_subscribed IS '是否关注服务号:true-已关注,false-未关注';
|
||||
COMMENT ON COLUMN wechat_user.subscribe_time IS '首次关注时间';
|
||||
COMMENT ON COLUMN wechat_user.unsubscribe_time IS '最后一次取消关注时间';
|
||||
|
||||
-- ============================================
|
||||
-- 3. member_card 表(会员卡类型表)
|
||||
-- ============================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS member_card (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
deleted_at TIMESTAMP,
|
||||
|
||||
member_card_id BIGSERIAL,
|
||||
member_card_name VARCHAR(100) NOT NULL,
|
||||
member_card_type VARCHAR(20) NOT NULL,
|
||||
member_card_price DECIMAL(10, 2) NOT NULL,
|
||||
member_card_validity_days INTEGER,
|
||||
member_card_total_times INTEGER,
|
||||
member_card_amount DECIMAL(10, 2),
|
||||
member_card_status INTEGER DEFAULT 1 NOT NULL,
|
||||
extra_config TEXT DEFAULT '{}'
|
||||
);
|
||||
|
||||
COMMENT ON TABLE member_card IS '会员卡类型表';
|
||||
COMMENT ON COLUMN member_card.member_card_id IS '会员卡ID';
|
||||
COMMENT ON COLUMN member_card.member_card_name IS '会员卡名称';
|
||||
COMMENT ON COLUMN member_card.member_card_type IS '会员卡类型:TIME_CARD-时长卡, COUNT_CARD-次卡, STORED_VALUE_CARD-储值卡';
|
||||
COMMENT ON COLUMN member_card.member_card_price IS '会员卡价格';
|
||||
COMMENT ON COLUMN member_card.member_card_validity_days IS '有效天数(时长卡用)';
|
||||
COMMENT ON COLUMN member_card.member_card_total_times IS '总次数(次卡用)';
|
||||
COMMENT ON COLUMN member_card.member_card_amount IS '面额(储值卡用)';
|
||||
COMMENT ON COLUMN member_card.member_card_status IS '状态:0-下架, 1-上架';
|
||||
COMMENT ON COLUMN member_card.extra_config IS '扩展配置(JSON格式)';
|
||||
|
||||
-- ============================================
|
||||
-- 4. member_card_record 表(会员卡记录表)
|
||||
-- ============================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS member_card_record (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
deleted_at TIMESTAMP,
|
||||
|
||||
member_card_record_id BIGSERIAL,
|
||||
member_id BIGINT NOT NULL,
|
||||
member_card_id BIGINT NOT NULL,
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE',
|
||||
remaining_times INTEGER DEFAULT 0,
|
||||
remaining_amount DECIMAL(10, 2) DEFAULT 0.00,
|
||||
expire_time TIMESTAMP,
|
||||
source_order_id BIGINT,
|
||||
purchase_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
version INTEGER DEFAULT 0 NOT NULL,
|
||||
card_composition TEXT
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_member_card_record_member_id ON member_card_record(member_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_member_card_record_status ON member_card_record(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_member_card_record_expire_time ON member_card_record(expire_time);
|
||||
CREATE INDEX IF NOT EXISTS idx_member_card_record_member_status ON member_card_record(member_id, status);
|
||||
CREATE INDEX IF NOT EXISTS idx_member_card_record_status_expire ON member_card_record(status, expire_time)
|
||||
WHERE status = 'ACTIVE';
|
||||
|
||||
COMMENT ON TABLE member_card_record IS '会员卡记录表(会员持有的卡)';
|
||||
COMMENT ON COLUMN member_card_record.member_card_record_id IS '会员卡记录ID';
|
||||
COMMENT ON COLUMN member_card_record.member_id IS '会员ID';
|
||||
COMMENT ON COLUMN member_card_record.member_card_id IS '会员卡类型ID';
|
||||
COMMENT ON COLUMN member_card_record.status IS '状态:ACTIVE-有效, USED_UP-用完, EXPIRED-过期, REFUNDED-已退款';
|
||||
COMMENT ON COLUMN member_card_record.remaining_times IS '剩余次数';
|
||||
COMMENT ON COLUMN member_card_record.remaining_amount IS '剩余金额';
|
||||
COMMENT ON COLUMN member_card_record.expire_time IS '到期时间';
|
||||
COMMENT ON COLUMN member_card_record.source_order_id IS '来源订单ID';
|
||||
COMMENT ON COLUMN member_card_record.purchase_time IS '购买时间';
|
||||
COMMENT ON COLUMN member_card_record.version IS '乐观锁版本号';
|
||||
COMMENT ON COLUMN member_card_record.card_composition IS '卡片组成(JSON格式,用于组合卡)';
|
||||
|
||||
-- ============================================
|
||||
-- 5. member_card_transactions 表(会员卡交易流水表)
|
||||
-- ============================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS member_card_transactions (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
member_card_record_id BIGINT NOT NULL,
|
||||
member_id BIGINT NOT NULL,
|
||||
member_card_id BIGINT NOT NULL,
|
||||
operation_type VARCHAR(20) NOT NULL,
|
||||
change_amount INTEGER DEFAULT 0,
|
||||
change_balance DECIMAL(10, 2) DEFAULT 0.00,
|
||||
after_remaining_count INTEGER DEFAULT 0,
|
||||
after_remaining_balance DECIMAL(10, 2) DEFAULT 0.00,
|
||||
related_biz_type VARCHAR(20),
|
||||
source_order_id BIGINT,
|
||||
remark VARCHAR(500),
|
||||
is_archived BOOLEAN DEFAULT FALSE,
|
||||
archived_at TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_member_card_transactions_member_id ON member_card_transactions(member_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_member_card_transactions_record_id ON member_card_transactions(member_card_record_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_member_card_transactions_created_at ON member_card_transactions(created_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_member_card_transactions_member_type_time
|
||||
ON member_card_transactions(member_id, operation_type, created_at);
|
||||
|
||||
COMMENT ON TABLE member_card_transactions IS '会员卡交易流水表';
|
||||
COMMENT ON COLUMN member_card_transactions.operation_type IS '操作类型:PURCHASE-购买, DEDUCT-扣次/扣费, RENEW-续费, REFUND-退款, EXPIRE-过期';
|
||||
COMMENT ON COLUMN member_card_transactions.change_amount IS '变动次数';
|
||||
COMMENT ON COLUMN member_card_transactions.change_balance IS '变动金额';
|
||||
COMMENT ON COLUMN member_card_transactions.after_remaining_count IS '变动后剩余次数';
|
||||
COMMENT ON COLUMN member_card_transactions.after_remaining_balance IS '变动后剩余金额';
|
||||
COMMENT ON COLUMN member_card_transactions.related_biz_type IS '关联业务类型:GROUP_CLASS-团课, PT_CLASS-私教, CHECK_IN-签到';
|
||||
COMMENT ON COLUMN member_card_transactions.is_archived IS '是否已归档';
|
||||
COMMENT ON COLUMN member_card_transactions.archived_at IS '归档时间';
|
||||
|
||||
-- ============================================
|
||||
-- 6. refund_application 表(退款申请表)
|
||||
-- ============================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS refund_application (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
deleted_at TIMESTAMP,
|
||||
|
||||
record_id BIGINT NOT NULL,
|
||||
member_id BIGINT NOT NULL,
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'PENDING',
|
||||
reason VARCHAR(500),
|
||||
apply_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
audit_time TIMESTAMP,
|
||||
auditor_id BIGINT,
|
||||
audit_remark VARCHAR(500),
|
||||
refund_amount DECIMAL(10, 2)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_refund_application_record_id ON refund_application(record_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_refund_application_status ON refund_application(status);
|
||||
|
||||
COMMENT ON TABLE refund_application IS '退款申请表';
|
||||
COMMENT ON COLUMN refund_application.status IS '状态:PENDING-待审核, APPROVED-已批准, REJECTED-已拒绝, PROCESSING-处理中, SUCCESS-成功, FAILED-失败';
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
-- ============================================
|
||||
-- 团课相关表
|
||||
-- 版本: V5
|
||||
-- 描述: 创建团课课程表和团课预约记录表
|
||||
-- ============================================
|
||||
|
||||
-- 团课课程表
|
||||
CREATE TABLE IF NOT EXISTS group_course (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
course_name VARCHAR(100) NOT NULL,
|
||||
coach_id BIGINT,
|
||||
course_type BIGINT,
|
||||
start_time TIMESTAMP NOT NULL,
|
||||
end_time TIMESTAMP NOT NULL,
|
||||
max_members INTEGER DEFAULT 20,
|
||||
current_members INTEGER DEFAULT 0,
|
||||
status VARCHAR(1) DEFAULT '0',
|
||||
actual_start_time TIMESTAMP,
|
||||
actual_end_time TIMESTAMP,
|
||||
location VARCHAR(255),
|
||||
cover_image VARCHAR(500),
|
||||
description TEXT,
|
||||
stored_value_amount DECIMAL(10,2) DEFAULT 0,
|
||||
create_by VARCHAR(50),
|
||||
update_by VARCHAR(50),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
deleted_at TIMESTAMP
|
||||
);
|
||||
|
||||
-- 团课预约记录表
|
||||
CREATE TABLE IF NOT EXISTS group_course_booking (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
course_id BIGINT NOT NULL,
|
||||
member_id BIGINT NOT NULL,
|
||||
member_card_id BIGINT NOT NULL,
|
||||
booking_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
status VARCHAR(1) DEFAULT '0',
|
||||
cancel_time TIMESTAMP,
|
||||
create_by VARCHAR(50),
|
||||
update_by VARCHAR(50),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
deleted_at TIMESTAMP
|
||||
);
|
||||
|
||||
COMMENT ON TABLE group_course IS '团课课程表';
|
||||
COMMENT ON COLUMN group_course.id IS '主键ID';
|
||||
COMMENT ON COLUMN group_course.course_name IS '课程名称';
|
||||
COMMENT ON COLUMN group_course.coach_id IS '教练ID(关联sys_user)';
|
||||
COMMENT ON COLUMN group_course.course_type IS '课程类型(如瑜伽/普拉提/动感单车)';
|
||||
COMMENT ON COLUMN group_course.start_time IS '开始时间';
|
||||
COMMENT ON COLUMN group_course.end_time IS '结束时间';
|
||||
COMMENT ON COLUMN group_course.max_members IS '最大参与人数';
|
||||
COMMENT ON COLUMN group_course.current_members IS '当前参与人数';
|
||||
COMMENT ON COLUMN group_course.status IS '状态(0正常 1已取消 2已结束 3进行中 4超时)';
|
||||
COMMENT ON COLUMN group_course.actual_start_time IS '实际开课时间(教练点击开课时记录)';
|
||||
COMMENT ON COLUMN group_course.actual_end_time IS '实际结课时间(教练点击结课时记录)';
|
||||
COMMENT ON COLUMN group_course.location IS '上课地点';
|
||||
COMMENT ON COLUMN group_course.cover_image IS '封面图URL';
|
||||
COMMENT ON COLUMN group_course.description IS '课程描述';
|
||||
COMMENT ON COLUMN group_course.stored_value_amount IS '储值卡额度(消耗金额)';
|
||||
COMMENT ON COLUMN group_course.create_by IS '创建人';
|
||||
COMMENT ON COLUMN group_course.update_by IS '更新人';
|
||||
COMMENT ON COLUMN group_course.created_at IS '创建时间';
|
||||
COMMENT ON COLUMN group_course.updated_at IS '更新时间';
|
||||
COMMENT ON COLUMN group_course.deleted_at IS '删除时间(软删除)';
|
||||
|
||||
COMMENT ON TABLE group_course_booking IS '团课预约记录表';
|
||||
COMMENT ON COLUMN group_course_booking.id IS '主键ID';
|
||||
COMMENT ON COLUMN group_course_booking.course_id IS '团课ID';
|
||||
COMMENT ON COLUMN group_course_booking.member_id IS '用户ID';
|
||||
COMMENT ON COLUMN group_course_booking.member_card_id IS '会员卡ID';
|
||||
COMMENT ON COLUMN group_course_booking.booking_time IS '预约时间';
|
||||
COMMENT ON COLUMN group_course_booking.status IS '状态(0已预约 1已取消 2已出席 3缺席)';
|
||||
COMMENT ON COLUMN group_course_booking.cancel_time IS '取消时间';
|
||||
COMMENT ON COLUMN group_course_booking.create_by IS '创建人';
|
||||
COMMENT ON COLUMN group_course_booking.update_by IS '更新人';
|
||||
COMMENT ON COLUMN group_course_booking.created_at IS '创建时间';
|
||||
COMMENT ON COLUMN group_course_booking.updated_at IS '更新时间';
|
||||
COMMENT ON COLUMN group_course_booking.deleted_at IS '删除时间(软删除)';
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
-- ============================================
|
||||
-- 为团课预约记录表添加课程冗余字段
|
||||
-- 版本: V6
|
||||
-- 描述: 保存预约时的课程快照信息
|
||||
-- ============================================
|
||||
|
||||
ALTER TABLE group_course_booking
|
||||
ADD COLUMN IF NOT EXISTS course_name VARCHAR(100),
|
||||
ADD COLUMN IF NOT EXISTS course_start_time TIMESTAMP,
|
||||
ADD COLUMN IF NOT EXISTS course_end_time TIMESTAMP,
|
||||
ADD COLUMN IF NOT EXISTS location VARCHAR(255);
|
||||
|
||||
COMMENT ON COLUMN group_course_booking.course_name IS '课程名称(冗余字段,保存预约时的课程快照)';
|
||||
COMMENT ON COLUMN group_course_booking.course_start_time IS '课程开始时间(冗余字段,保存预约时的课程快照)';
|
||||
COMMENT ON COLUMN group_course_booking.course_end_time IS '课程结束时间(冗余字段,保存预约时的课程快照)';
|
||||
COMMENT ON COLUMN group_course_booking.location IS '上课地点(冗余字段,保存预约时的课程快照)';
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
-- ============================================
|
||||
-- 会员到店签到记录表
|
||||
-- 版本: V7
|
||||
-- 描述: 创建sign_in_record表,用于记录会员签到信息
|
||||
-- ============================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS sign_in_record (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
member_id BIGINT NOT NULL,
|
||||
member_card_id BIGINT,
|
||||
sign_in_time TIMESTAMP NOT NULL,
|
||||
sign_in_type VARCHAR(20) NOT NULL,
|
||||
sign_in_status VARCHAR(20) NOT NULL DEFAULT 'SUCCESS',
|
||||
verification_details TEXT,
|
||||
fail_reason VARCHAR(500),
|
||||
operator_id BIGINT,
|
||||
operator_name VARCHAR(100),
|
||||
device_info VARCHAR(200),
|
||||
ip_address VARCHAR(50),
|
||||
source VARCHAR(20) NOT NULL,
|
||||
is_delete BOOLEAN DEFAULT FALSE,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_sign_in_record_member_id ON sign_in_record(member_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_sign_in_record_sign_in_time ON sign_in_record(sign_in_time);
|
||||
CREATE INDEX IF NOT EXISTS idx_sign_in_record_sign_in_status ON sign_in_record(sign_in_status);
|
||||
CREATE INDEX IF NOT EXISTS idx_sign_in_record_member_card_id ON sign_in_record(member_card_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_sign_in_record_operator_id ON sign_in_record(operator_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_sign_in_record_source ON sign_in_record(source);
|
||||
CREATE INDEX IF NOT EXISTS idx_sign_in_record_is_delete ON sign_in_record(is_delete);
|
||||
CREATE INDEX IF NOT EXISTS idx_sign_in_record_member_time ON sign_in_record(member_id, sign_in_time);
|
||||
CREATE INDEX IF NOT EXISTS idx_sign_in_record_status_time ON sign_in_record(sign_in_status, sign_in_time);
|
||||
|
||||
COMMENT ON TABLE sign_in_record IS '会员到店签到记录表';
|
||||
COMMENT ON COLUMN sign_in_record.id IS '自增主键';
|
||||
COMMENT ON COLUMN sign_in_record.member_id IS '会员ID,关联member表';
|
||||
COMMENT ON COLUMN sign_in_record.member_card_id IS '签到时使用的会员卡ID';
|
||||
COMMENT ON COLUMN sign_in_record.sign_in_time IS '签到入场时间';
|
||||
COMMENT ON COLUMN sign_in_record.sign_in_type IS '签到方式:QR_CODE-扫码签到,MANUAL-手动签到,FACE-人脸识别';
|
||||
COMMENT ON COLUMN sign_in_record.sign_in_status IS '签到状态:SUCCESS-成功,FAILED-失败';
|
||||
COMMENT ON COLUMN sign_in_record.verification_details IS 'JSON格式,存储会员卡验证时的快照数据';
|
||||
COMMENT ON COLUMN sign_in_record.fail_reason IS '失败时的具体原因文案';
|
||||
COMMENT ON COLUMN sign_in_record.operator_id IS '操作人ID(前台人员),自助签到时为NULL';
|
||||
COMMENT ON COLUMN sign_in_record.operator_name IS '操作人姓名冗余';
|
||||
COMMENT ON COLUMN sign_in_record.device_info IS '签到设备标识或型号';
|
||||
COMMENT ON COLUMN sign_in_record.ip_address IS '客户端IP地址';
|
||||
COMMENT ON COLUMN sign_in_record.source IS '签到来源:MINI_PROGRAM-小程序扫码,PC_BACKEND-后台管理端';
|
||||
COMMENT ON COLUMN sign_in_record.is_delete IS '软删除标识:false-未删除,true-已删除';
|
||||
COMMENT ON COLUMN sign_in_record.created_at IS '记录创建时间';
|
||||
COMMENT ON COLUMN sign_in_record.updated_at IS '记录更新时间';
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
-- ============================================
|
||||
-- 团课类型表
|
||||
-- 版本: V8
|
||||
-- 描述: 创建团课类型表,用于分类管理团课
|
||||
-- ============================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS group_course_type (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
type_name VARCHAR(100) NOT NULL,
|
||||
base_difficulty INTEGER DEFAULT 1,
|
||||
description TEXT,
|
||||
category VARCHAR(50),
|
||||
create_by VARCHAR(50),
|
||||
update_by VARCHAR(50),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
deleted_at TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_group_course_type_type_name ON group_course_type(type_name);
|
||||
CREATE INDEX IF NOT EXISTS idx_group_course_type_category ON group_course_type(category);
|
||||
|
||||
COMMENT ON TABLE group_course_type IS '团课类型表';
|
||||
COMMENT ON COLUMN group_course_type.id IS '主键ID';
|
||||
COMMENT ON COLUMN group_course_type.type_name IS '类型名称';
|
||||
COMMENT ON COLUMN group_course_type.base_difficulty IS '基础难度(1-10)';
|
||||
COMMENT ON COLUMN group_course_type.description IS '类型描述';
|
||||
COMMENT ON COLUMN group_course_type.category IS '分类(如:有氧、力量、柔韧等)';
|
||||
COMMENT ON COLUMN group_course_type.create_by IS '创建人';
|
||||
COMMENT ON COLUMN group_course_type.update_by IS '更新人';
|
||||
COMMENT ON COLUMN group_course_type.created_at IS '创建时间';
|
||||
COMMENT ON COLUMN group_course_type.updated_at IS '更新时间';
|
||||
COMMENT ON COLUMN group_course_type.deleted_at IS '删除时间(软删除)';
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
-- ============================================
|
||||
-- 团课标签相关表
|
||||
-- 版本: V9
|
||||
-- 描述: 创建团课标签表和类型-标签关联表
|
||||
-- ============================================
|
||||
|
||||
-- 团课标签表
|
||||
CREATE TABLE IF NOT EXISTS course_label (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
label_name VARCHAR(50) NOT NULL,
|
||||
color VARCHAR(20) DEFAULT '#1890ff',
|
||||
description VARCHAR(200),
|
||||
create_by VARCHAR(50),
|
||||
update_by VARCHAR(50),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
deleted_at TIMESTAMP
|
||||
);
|
||||
|
||||
-- 团课类型-标签关联表
|
||||
CREATE TABLE IF NOT EXISTS course_type_label (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
type_id BIGINT NOT NULL,
|
||||
label_id BIGINT NOT NULL,
|
||||
create_by VARCHAR(50),
|
||||
update_by VARCHAR(50),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
deleted_at TIMESTAMP,
|
||||
UNIQUE (type_id, label_id)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_course_label_label_name ON course_label(label_name);
|
||||
CREATE INDEX IF NOT EXISTS idx_course_type_label_type_id ON course_type_label(type_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_course_type_label_label_id ON course_type_label(label_id);
|
||||
|
||||
COMMENT ON TABLE course_label IS '团课标签表';
|
||||
COMMENT ON COLUMN course_label.id IS '主键ID';
|
||||
COMMENT ON COLUMN course_label.label_name IS '标签名称';
|
||||
COMMENT ON COLUMN course_label.color IS '标签颜色(十六进制)';
|
||||
COMMENT ON COLUMN course_label.description IS '标签描述';
|
||||
COMMENT ON COLUMN course_label.create_by IS '创建人';
|
||||
COMMENT ON COLUMN course_label.update_by IS '更新人';
|
||||
COMMENT ON COLUMN course_label.created_at IS '创建时间';
|
||||
COMMENT ON COLUMN course_label.updated_at IS '更新时间';
|
||||
COMMENT ON COLUMN course_label.deleted_at IS '删除时间(软删除)';
|
||||
|
||||
COMMENT ON TABLE course_type_label IS '团课类型-标签关联表';
|
||||
COMMENT ON COLUMN course_type_label.id IS '主键ID';
|
||||
COMMENT ON COLUMN course_type_label.type_id IS '团课类型ID';
|
||||
COMMENT ON COLUMN course_type_label.label_id IS '标签ID';
|
||||
COMMENT ON COLUMN course_type_label.create_by IS '创建人';
|
||||
COMMENT ON COLUMN course_type_label.update_by IS '更新人';
|
||||
COMMENT ON COLUMN course_type_label.created_at IS '创建时间';
|
||||
COMMENT ON COLUMN course_type_label.updated_at IS '更新时间';
|
||||
COMMENT ON COLUMN course_type_label.deleted_at IS '删除时间(软删除)';
|
||||
+1
@@ -64,6 +64,7 @@ public class JwtAuthenticationFilter extends AbstractGatewayFilterFactory<JwtAut
|
||||
path.equals("/api/groupCourse/page") ||
|
||||
path.startsWith("/api/checkIn") ||
|
||||
path.startsWith("/api/payment") ||
|
||||
path.startsWith("/api/files/") ||
|
||||
path.startsWith("/actuator/info");
|
||||
}
|
||||
|
||||
|
||||
+2
-1
@@ -70,7 +70,8 @@ public class RbacAuthorizationFilter extends AbstractGatewayFilterFactory<RbacAu
|
||||
path.startsWith("/api/member-cards/") ||
|
||||
path.startsWith("/api/stored-card/") ||
|
||||
path.startsWith("/api/pay-password/") ||
|
||||
path.startsWith("/api/datacount/");
|
||||
path.startsWith("/api/datacount/") ||
|
||||
path.startsWith("/api/files/");
|
||||
}
|
||||
|
||||
public static class Config {
|
||||
|
||||
@@ -64,7 +64,7 @@ signature:
|
||||
max-age-minutes: ${SIGNATURE_MAX_AGE_MINUTES:5}
|
||||
nonce-cache-size: ${SIGNATURE_NONCE_CACHE_SIZE:10000}
|
||||
whitelist:
|
||||
paths: ${SIGNATURE_WHITELIST_PATHS:/actuator/health,/actuator/info,/api/auth/login,/api/auth/register,/api/member/auth/miniapp/login,/api/member/auth/mp/callback,/api/groupCourse/**,/api/checkIn/**,/api/member/**,/api/member-cards/**,/api/stored-card/**,/api/pay-password/**,/api/datacount/**}
|
||||
paths: ${SIGNATURE_WHITELIST_PATHS:/actuator/health,/actuator/info,/api/auth/login,/api/auth/register,/api/member/auth/miniapp/login,/api/member/auth/mp/callback,/api/groupCourse/**,/api/checkIn/**,/api/member/**,/api/member-cards/**,/api/stored-card/**,/api/pay-password/**,/api/datacount/**,/api/files/**}
|
||||
|
||||
resilience:
|
||||
enabled: ${RESILIENCE_ENABLED:true}
|
||||
|
||||
+97
@@ -0,0 +1,97 @@
|
||||
package cn.novalon.gym.manage.notify.core.domain;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
public class Banner {
|
||||
|
||||
private Long id;
|
||||
private String imageUrl;
|
||||
private String title;
|
||||
private String subtitle;
|
||||
private Integer sortOrder;
|
||||
private String isActive;
|
||||
private String linkUrl;
|
||||
private LocalDateTime createdAt;
|
||||
private LocalDateTime updatedAt;
|
||||
private LocalDateTime deletedAt;
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getImageUrl() {
|
||||
return imageUrl;
|
||||
}
|
||||
|
||||
public void setImageUrl(String imageUrl) {
|
||||
this.imageUrl = imageUrl;
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
return title;
|
||||
}
|
||||
|
||||
public void setTitle(String title) {
|
||||
this.title = title;
|
||||
}
|
||||
|
||||
public String getSubtitle() {
|
||||
return subtitle;
|
||||
}
|
||||
|
||||
public void setSubtitle(String subtitle) {
|
||||
this.subtitle = subtitle;
|
||||
}
|
||||
|
||||
public Integer getSortOrder() {
|
||||
return sortOrder;
|
||||
}
|
||||
|
||||
public void setSortOrder(Integer sortOrder) {
|
||||
this.sortOrder = sortOrder;
|
||||
}
|
||||
|
||||
public String getIsActive() {
|
||||
return isActive;
|
||||
}
|
||||
|
||||
public void setIsActive(String isActive) {
|
||||
this.isActive = isActive;
|
||||
}
|
||||
|
||||
public String getLinkUrl() {
|
||||
return linkUrl;
|
||||
}
|
||||
|
||||
public void setLinkUrl(String linkUrl) {
|
||||
this.linkUrl = linkUrl;
|
||||
}
|
||||
|
||||
public LocalDateTime getCreatedAt() {
|
||||
return createdAt;
|
||||
}
|
||||
|
||||
public void setCreatedAt(LocalDateTime createdAt) {
|
||||
this.createdAt = createdAt;
|
||||
}
|
||||
|
||||
public LocalDateTime getUpdatedAt() {
|
||||
return updatedAt;
|
||||
}
|
||||
|
||||
public void setUpdatedAt(LocalDateTime updatedAt) {
|
||||
this.updatedAt = updatedAt;
|
||||
}
|
||||
|
||||
public LocalDateTime getDeletedAt() {
|
||||
return deletedAt;
|
||||
}
|
||||
|
||||
public void setDeletedAt(LocalDateTime deletedAt) {
|
||||
this.deletedAt = deletedAt;
|
||||
}
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
package cn.novalon.gym.manage.notify.core.repository;
|
||||
|
||||
import cn.novalon.gym.manage.notify.core.domain.Banner;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
public interface IBannerRepository {
|
||||
|
||||
Flux<Banner> findByDeletedAtIsNull();
|
||||
|
||||
Flux<Banner> findActiveBanners();
|
||||
|
||||
Mono<Banner> findById(Long id);
|
||||
|
||||
Mono<Banner> save(Banner banner);
|
||||
|
||||
Mono<Void> deleteByIdAndDeletedAtIsNull(Long id);
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
package cn.novalon.gym.manage.notify.core.service;
|
||||
|
||||
import cn.novalon.gym.manage.notify.core.domain.Banner;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
public interface IBannerService {
|
||||
|
||||
Flux<Banner> getAllBanners();
|
||||
|
||||
Flux<Banner> getActiveBanners();
|
||||
|
||||
Mono<Banner> getBannerById(Long id);
|
||||
|
||||
Mono<Banner> createBanner(Banner banner);
|
||||
|
||||
Mono<Banner> updateBanner(Long id, Banner banner);
|
||||
|
||||
Mono<Void> deleteBanner(Long id);
|
||||
}
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
package cn.novalon.gym.manage.notify.core.service.impl;
|
||||
|
||||
import cn.novalon.gym.manage.notify.core.domain.Banner;
|
||||
import cn.novalon.gym.manage.notify.core.repository.IBannerRepository;
|
||||
import cn.novalon.gym.manage.notify.core.service.IBannerService;
|
||||
import org.springframework.stereotype.Service;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Service
|
||||
public class BannerServiceImpl implements IBannerService {
|
||||
|
||||
private final IBannerRepository bannerRepository;
|
||||
|
||||
public BannerServiceImpl(IBannerRepository bannerRepository) {
|
||||
this.bannerRepository = bannerRepository;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Flux<Banner> getAllBanners() {
|
||||
return bannerRepository.findByDeletedAtIsNull();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Flux<Banner> getActiveBanners() {
|
||||
return bannerRepository.findActiveBanners();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Banner> getBannerById(Long id) {
|
||||
return bannerRepository.findById(id)
|
||||
.filter(banner -> banner.getDeletedAt() == null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Banner> createBanner(Banner banner) {
|
||||
banner.setCreatedAt(LocalDateTime.now());
|
||||
return bannerRepository.save(banner);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Banner> updateBanner(Long id, Banner banner) {
|
||||
return bannerRepository.findById(id)
|
||||
.flatMap(existing -> {
|
||||
if (banner.getImageUrl() != null) {
|
||||
existing.setImageUrl(banner.getImageUrl());
|
||||
}
|
||||
if (banner.getTitle() != null) {
|
||||
existing.setTitle(banner.getTitle());
|
||||
}
|
||||
if (banner.getSubtitle() != null) {
|
||||
existing.setSubtitle(banner.getSubtitle());
|
||||
}
|
||||
if (banner.getSortOrder() != null) {
|
||||
existing.setSortOrder(banner.getSortOrder());
|
||||
}
|
||||
if (banner.getIsActive() != null) {
|
||||
existing.setIsActive(banner.getIsActive());
|
||||
}
|
||||
if (banner.getLinkUrl() != null) {
|
||||
existing.setLinkUrl(banner.getLinkUrl());
|
||||
}
|
||||
existing.setUpdatedAt(LocalDateTime.now());
|
||||
return bannerRepository.save(existing);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> deleteBanner(Long id) {
|
||||
return bannerRepository.findById(id)
|
||||
.filter(banner -> banner.getDeletedAt() == null)
|
||||
.flatMap(banner -> {
|
||||
banner.setDeletedAt(LocalDateTime.now());
|
||||
return bannerRepository.save(banner);
|
||||
})
|
||||
.then();
|
||||
}
|
||||
}
|
||||
+120
@@ -0,0 +1,120 @@
|
||||
package cn.novalon.gym.manage.notify.handler;
|
||||
|
||||
import cn.novalon.gym.manage.notify.core.domain.Banner;
|
||||
import cn.novalon.gym.manage.notify.core.service.IBannerService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.reactive.function.server.ServerRequest;
|
||||
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Map;
|
||||
|
||||
@Component
|
||||
@Tag(name = "轮播图管理", description = "首页轮播图相关操作")
|
||||
public class BannerHandler {
|
||||
|
||||
private final IBannerService bannerService;
|
||||
|
||||
public BannerHandler(IBannerService bannerService) {
|
||||
this.bannerService = bannerService;
|
||||
}
|
||||
|
||||
@Operation(summary = "获取所有轮播图", description = "获取系统中所有轮播图列表(按排序升序)")
|
||||
public Mono<ServerResponse> getAllBanners(ServerRequest request) {
|
||||
Flux<Banner> banners = bannerService.getAllBanners();
|
||||
return ServerResponse.ok().body(banners, Banner.class);
|
||||
}
|
||||
|
||||
@Operation(summary = "获取启用的轮播图", description = "获取所有启用状态的轮播图(按排序升序),供外部页面调用")
|
||||
public Mono<ServerResponse> getActiveBanners(ServerRequest request) {
|
||||
Flux<Banner> banners = bannerService.getActiveBanners();
|
||||
return ServerResponse.ok().body(banners, Banner.class);
|
||||
}
|
||||
|
||||
@Operation(summary = "根据ID获取轮播图", description = "根据轮播图ID获取详细信息")
|
||||
public Mono<ServerResponse> getBannerById(ServerRequest request) {
|
||||
Long id = Long.parseLong(request.pathVariable("id"));
|
||||
return bannerService.getBannerById(id)
|
||||
.flatMap(banner -> ServerResponse.ok().bodyValue(banner))
|
||||
.switchIfEmpty(ServerResponse.notFound().build());
|
||||
}
|
||||
|
||||
@Operation(summary = "创建轮播图", description = "创建新轮播图")
|
||||
public Mono<ServerResponse> createBanner(ServerRequest request) {
|
||||
return request.bodyToMono(Map.class)
|
||||
.map(this::mapToBanner)
|
||||
.filter(banner -> banner.getImageUrl() != null && !banner.getImageUrl().trim().isEmpty())
|
||||
.switchIfEmpty(Mono.error(new IllegalArgumentException("轮播图图片地址不能为空")))
|
||||
.filter(banner -> banner.getTitle() != null && !banner.getTitle().trim().isEmpty())
|
||||
.switchIfEmpty(Mono.error(new IllegalArgumentException("轮播图标题不能为空")))
|
||||
.flatMap(banner -> {
|
||||
if (banner.getSortOrder() == null) {
|
||||
banner.setSortOrder(0);
|
||||
}
|
||||
if (banner.getIsActive() == null) {
|
||||
banner.setIsActive("1");
|
||||
}
|
||||
return bannerService.createBanner(banner);
|
||||
})
|
||||
.flatMap(banner -> ServerResponse.ok().bodyValue(banner))
|
||||
.onErrorResume(IllegalArgumentException.class, ex ->
|
||||
ServerResponse.badRequest().bodyValue(Map.of(
|
||||
"code", 400,
|
||||
"message", ex.getMessage(),
|
||||
"timestamp", LocalDateTime.now()
|
||||
))
|
||||
);
|
||||
}
|
||||
|
||||
@Operation(summary = "更新轮播图", description = "更新轮播图信息")
|
||||
public Mono<ServerResponse> updateBanner(ServerRequest request) {
|
||||
Long id = Long.parseLong(request.pathVariable("id"));
|
||||
return request.bodyToMono(Map.class)
|
||||
.map(this::mapToBanner)
|
||||
.flatMap(banner -> bannerService.updateBanner(id, banner))
|
||||
.flatMap(banner -> ServerResponse.ok().bodyValue(banner))
|
||||
.switchIfEmpty(ServerResponse.notFound().build());
|
||||
}
|
||||
|
||||
@Operation(summary = "删除轮播图", description = "删除指定轮播图(软删除)")
|
||||
public Mono<ServerResponse> deleteBanner(ServerRequest request) {
|
||||
Long id = Long.parseLong(request.pathVariable("id"));
|
||||
return bannerService.getBannerById(id)
|
||||
.filter(banner -> banner.getDeletedAt() == null)
|
||||
.flatMap(banner -> bannerService.deleteBanner(id)
|
||||
.then(ServerResponse.noContent().build()))
|
||||
.switchIfEmpty(ServerResponse.notFound().build());
|
||||
}
|
||||
|
||||
/**
|
||||
* 将前端传来的 Map 转换为 Banner 领域对象
|
||||
* 兼容 isActive 以 boolean 或 string 形式传入
|
||||
*/
|
||||
private Banner mapToBanner(Map<String, Object> body) {
|
||||
Banner banner = new Banner();
|
||||
banner.setImageUrl((String) body.get("imageUrl"));
|
||||
banner.setTitle((String) body.get("title"));
|
||||
banner.setSubtitle((String) body.get("subtitle"));
|
||||
banner.setLinkUrl((String) body.get("linkUrl"));
|
||||
|
||||
// sortOrder 处理
|
||||
Object sortOrderObj = body.get("sortOrder");
|
||||
if (sortOrderObj instanceof Number) {
|
||||
banner.setSortOrder(((Number) sortOrderObj).intValue());
|
||||
}
|
||||
|
||||
// isActive 兼容 boolean 和 string
|
||||
Object isActiveObj = body.get("isActive");
|
||||
if (isActiveObj instanceof Boolean) {
|
||||
banner.setIsActive((Boolean) isActiveObj ? "1" : "0");
|
||||
} else if (isActiveObj instanceof String) {
|
||||
banner.setIsActive((String) isActiveObj);
|
||||
}
|
||||
|
||||
return banner;
|
||||
}
|
||||
}
|
||||
+2
-1
@@ -65,7 +65,8 @@ public class SecurityConfig {
|
||||
.pathMatchers("/api/payment/create").permitAll()
|
||||
.pathMatchers("/api/payment/query").permitAll()
|
||||
.pathMatchers("/api/payment/notify").permitAll()
|
||||
.pathMatchers("/api/payment/refund").permitAll();
|
||||
.pathMatchers("/api/payment/refund").permitAll()
|
||||
.pathMatchers("/api/files/**").permitAll();
|
||||
|
||||
if (isDevOrTest) {
|
||||
spec.pathMatchers("/swagger-ui.html").permitAll()
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
const http = require('../utils/request')
|
||||
|
||||
/** 获取启用的轮播图列表(按 sortOrder 升序) */
|
||||
function getActiveBanners() {
|
||||
return http.get('/banners/active').then(res => {
|
||||
console.log('[Banner API] 响应数据:', JSON.stringify(res))
|
||||
return res
|
||||
}).catch(err => {
|
||||
console.error('[Banner API] 请求失败:', err)
|
||||
throw err
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getActiveBanners
|
||||
}
|
||||
@@ -6,8 +6,8 @@ function bookCourse(params) {
|
||||
}
|
||||
|
||||
/** 取消预约 */
|
||||
function cancelBooking(bookingId) {
|
||||
return http.post('/groupCourse/booking/' + bookingId + '/cancel')
|
||||
function cancelBooking(bookingId, memberId) {
|
||||
return http.post('/groupCourse/booking/' + bookingId + '/cancel', { memberId })
|
||||
}
|
||||
|
||||
/** 获取会员的预约列表 */
|
||||
|
||||
@@ -58,6 +58,8 @@
|
||||
"selectedColor": "#00E676",
|
||||
"backgroundColor": "#1A1A1A",
|
||||
"borderStyle": "black",
|
||||
"iconWidth": "24px",
|
||||
"iconHeight": "24px",
|
||||
"list": [
|
||||
{
|
||||
"pagePath": "pages/index/index",
|
||||
|
||||
@@ -128,6 +128,7 @@
|
||||
const courseApi = require('../../api/course')
|
||||
const bookingApi = require('../../api/booking')
|
||||
const store = require('../../store/index')
|
||||
const { resolveCoverUrl } = require('../../utils/request')
|
||||
|
||||
export default {
|
||||
data() {
|
||||
@@ -279,7 +280,7 @@
|
||||
storedValueAmount: detail.storedValueAmount != null ? detail.storedValueAmount : 0,
|
||||
statusText: statusText,
|
||||
statusClass: statusClass,
|
||||
coverImage: detail.coverImage || '',
|
||||
coverImage: resolveCoverUrl(detail.coverImage),
|
||||
coverError: false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,6 +74,8 @@
|
||||
const store = require('../../store/index')
|
||||
const memberApi = require('../../api/member')
|
||||
const courseApi = require('../../api/course')
|
||||
const bannerApi = require('../../api/banner')
|
||||
const { resolveCoverUrl } = require('../../utils/request')
|
||||
|
||||
export default {
|
||||
data() {
|
||||
@@ -88,11 +90,7 @@
|
||||
totalSignDays: 0,
|
||||
pendingBookings: 0
|
||||
},
|
||||
banners: [
|
||||
{ imageUrl: 'https://img.zcool.cn/community/01f5c65e5d9c6ca801216518e1d2e0.jpg@1280w_1l_2o_100sh.jpg', title: '夏季燃脂计划', subtitle: 'HIIT + 动感单车,高效燃脂 7 天挑战' },
|
||||
{ imageUrl: 'https://img.zcool.cn/community/01a5fe5e5d9c4ba80121985ac87c5f.jpg@1280w_1l_2o_100sh.jpg', title: '瑜伽月卡特惠', subtitle: '新人专享 5 折,流瑜伽 / 普拉提任选' },
|
||||
{ imageUrl: 'https://img.zcool.cn/community/0177c85e5d9c84a8012165182ef63f.jpg@1280w_1l_2o_100sh.jpg', title: '私教一对一体验', subtitle: '免费体测 + 定制训练计划,限 30 名' }
|
||||
],
|
||||
banners: [],
|
||||
recommendCourses: []
|
||||
}
|
||||
},
|
||||
@@ -128,10 +126,11 @@
|
||||
async loadData() {
|
||||
this.loading = true
|
||||
try {
|
||||
// 并行获取会员信息和推荐课程
|
||||
const [memberResult, recommendResult] = await Promise.allSettled([
|
||||
// 并行获取会员信息、推荐课程和轮播图
|
||||
const [memberResult, recommendResult, bannerResult] = await Promise.allSettled([
|
||||
memberApi.getMemberInfo(),
|
||||
courseApi.getActiveRecommendations()
|
||||
courseApi.getActiveRecommendations(),
|
||||
bannerApi.getActiveBanners()
|
||||
])
|
||||
|
||||
// 处理会员信息
|
||||
@@ -153,13 +152,30 @@
|
||||
return {
|
||||
id: gc.id || r.courseId,
|
||||
courseName: gc.courseName || '推荐课程',
|
||||
imageUrl: gc.coverImage || '',
|
||||
imageUrl: resolveCoverUrl(gc.coverImage),
|
||||
recommendReason: r.recommendReason || '',
|
||||
priceLabel: (gc.storedValueAmount > 0 ? '¥' + gc.storedValueAmount : '免费'),
|
||||
bookedCount: gc.currentMembers || 0
|
||||
}
|
||||
})
|
||||
}
|
||||
// 处理轮播图
|
||||
if (bannerResult.status === 'fulfilled') {
|
||||
const rawBanners = bannerResult.value || []
|
||||
console.log('[Index] 获取到轮播图原始数据:', rawBanners.length, '条')
|
||||
this.banners = rawBanners.map(b => {
|
||||
const resolvedUrl = resolveCoverUrl(b.imageUrl)
|
||||
console.log('[Index] Banner id=' + b.id + ' imageUrl原始=' + b.imageUrl + ' → 解析后=' + resolvedUrl)
|
||||
return {
|
||||
imageUrl: resolvedUrl,
|
||||
title: b.title || '',
|
||||
subtitle: b.subtitle || ''
|
||||
}
|
||||
})
|
||||
console.log('[Index] 最终 banners:', JSON.stringify(this.banners))
|
||||
} else {
|
||||
console.error('[Index] 轮播图请求失败:', bannerResult.reason)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('首页数据加载失败:', e)
|
||||
} finally {
|
||||
|
||||
@@ -179,9 +179,15 @@
|
||||
if (!res.confirm) return
|
||||
this.cancelling = item.id
|
||||
try {
|
||||
await bookingApi.cancelBooking(item.id)
|
||||
const memberInfo = store.getMemberInfo()
|
||||
const memberId = memberInfo ? (memberInfo.memberId || memberInfo.id) : null
|
||||
if (!memberId) {
|
||||
uni.showToast({ title: '请先登录', icon: 'none' })
|
||||
return
|
||||
}
|
||||
await bookingApi.cancelBooking(item.id, memberId)
|
||||
await this.loadBookings()
|
||||
uni.showToast({ title: '已取消', icon: 'success' })
|
||||
this.loadBookings()
|
||||
} catch (e) {
|
||||
console.error('取消预约失败:', e)
|
||||
const msg = (e.data && e.data.message) || '取消失败,请重试'
|
||||
@@ -245,8 +251,8 @@
|
||||
return
|
||||
}
|
||||
await bookingApi.signIn(Number(scannedCourseId), memberId)
|
||||
await this.loadBookings()
|
||||
uni.showToast({ title: '签到成功', icon: 'success' })
|
||||
this.loadBookings()
|
||||
} catch (e) {
|
||||
console.error('签到失败:', e)
|
||||
const msg = (e.data && e.data.message) || '签到失败,请重试'
|
||||
|
||||
@@ -74,40 +74,46 @@
|
||||
<view class="course-cover">
|
||||
<view class="cover-skeleton" v-if="course.coverImage && !course.coverLoaded && !course.coverError"></view>
|
||||
<image v-if="course.coverImage" class="cover-img" :class="{ loaded: course.coverLoaded }" :src="course.coverImage" mode="aspectFill" @load="onCoverLoad(course)" @error="onCoverError(course)"></image>
|
||||
<text class="cover-text" v-if="(!course.coverImage || course.coverError) && course.typeName">{{ course.typeName.slice(0, 2) }}</text>
|
||||
<view class="cover-placeholder" v-if="(!course.coverImage || course.coverError)">
|
||||
<text class="cover-icon">⛫</text>
|
||||
<text class="cover-type-name">{{ course.typeName }}</text>
|
||||
</view>
|
||||
<view class="cover-overlay">
|
||||
<view class="cover-badge diff-badge" :class="'diff-' + course.difficultyLevel">
|
||||
<text class="diff-badge-star" v-for="s in course.difficultyStars" :key="s">★</text>
|
||||
<text class="diff-badge-label">{{ course.difficultyLabel }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="course-body">
|
||||
<view class="course-top">
|
||||
<text class="course-name">{{ course.courseName }}</text>
|
||||
<view class="course-tags">
|
||||
<text class="course-category">{{ course.category }}</text>
|
||||
<text class="course-difficulty" :class="'diff-' + course.difficultyLevel">{{ course.difficultyLabel }}</text>
|
||||
</view>
|
||||
<text class="course-price" :class="{ free: course.storedValueAmount === 0 }">
|
||||
{{ course.storedValueAmount > 0 ? '¥' + course.storedValueAmount + '/次' : '免费' }}
|
||||
</text>
|
||||
</view>
|
||||
<view class="course-labels" v-if="course.labels && course.labels.length">
|
||||
<text v-for="(label, li) in course.labels" :key="li" class="course-label"
|
||||
:style="{ background: label.color + '1a', color: label.color }">{{ label.labelName }}</text>
|
||||
</view>
|
||||
|
||||
<view class="course-meta">
|
||||
<text class="meta-icon">🕓</text>
|
||||
<text class="course-date">{{ course.shortDate }}</text>
|
||||
<text class="course-time">{{ course.shortTime }} - {{ course.endShortTime }}</text>
|
||||
<text class="course-duration" v-if="course.duration">{{ course.duration }}</text>
|
||||
</view>
|
||||
<view class="course-meta">
|
||||
<text class="meta-icon">📍</text>
|
||||
<text class="course-location">{{ course.location }}</text>
|
||||
</view>
|
||||
<view class="course-meta">
|
||||
<view class="course-bottom">
|
||||
<text class="course-capacity">已预约 {{ course.currentMembers }}/{{ course.maxMembers }}人</text>
|
||||
<button class="btn-book" :class="{ disabled: !course.canBook }" :disabled="!course.canBook"
|
||||
@click.stop="bookCourse(course)">
|
||||
{{ course.canBook ? '立即预约' : course.statusLabel }}
|
||||
</button>
|
||||
</view>
|
||||
<view class="course-labels" v-if="course.labels">
|
||||
<text v-for="(label, li) in course.labels" :key="li" class="course-label"
|
||||
:style="{ background: label.color + '1a', color: label.color }">{{ label.labelName }}</text>
|
||||
</view>
|
||||
<text class="course-price" :class="{ free: course.storedValueAmount === 0 }">
|
||||
{{ course.storedValueAmount > 0 ? '¥' + course.storedValueAmount + '/次' : '免费' }}
|
||||
</text>
|
||||
<view class="course-bottom">
|
||||
<button class="btn-book" :class="{ disabled: !course.canBook }" :disabled="!course.canBook"
|
||||
@click.stop="bookCourse(course)">
|
||||
{{ course.canBook ? '立即预约' : course.statusLabel }}
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -127,6 +133,7 @@
|
||||
const courseApi = require('../../api/course')
|
||||
const bookingApi = require('../../api/booking')
|
||||
const store = require('../../store/index')
|
||||
const { resolveCoverUrl } = require('../../utils/request')
|
||||
|
||||
export default {
|
||||
data() {
|
||||
@@ -153,7 +160,8 @@
|
||||
{ label: '难度', value: 'difficulty' },
|
||||
{ label: '热度', value: 'popular' }
|
||||
],
|
||||
courses: []
|
||||
courses: [],
|
||||
activeBookedCourseIds: new Set() // 用户当前已预约(status=0)的课程ID集合
|
||||
}
|
||||
},
|
||||
onLoad() {
|
||||
@@ -172,8 +180,11 @@
|
||||
this.loadCourses()
|
||||
this.loadTypes()
|
||||
},
|
||||
onShow() {
|
||||
this.loadActiveBookings()
|
||||
},
|
||||
onPullDownRefresh() {
|
||||
Promise.all([this.loadCourses(), this.loadTypes()])
|
||||
Promise.all([this.loadCourses(), this.loadTypes(), this.loadActiveBookings()])
|
||||
.finally(() => { uni.stopPullDownRefresh() })
|
||||
},
|
||||
computed: {
|
||||
@@ -191,7 +202,7 @@
|
||||
},
|
||||
filteredCourses() {
|
||||
let list = this.courses.map(c => {
|
||||
const bd = c.baseDifficulty || c.calculatedDifficulty || 5
|
||||
const bd = this.getTypeDifficulty(c.courseType)
|
||||
const st = c.startTime || ''
|
||||
const et = c.endTime || ''
|
||||
// 兼容 "2026-07-15T16:45:00" 和 "2026-07-15 16:45:00" 两种格式
|
||||
@@ -226,14 +237,22 @@
|
||||
else if (statusStr !== 0) statusLabel = '已关闭'
|
||||
if (!canBook && statusStr === 0 && isFull) statusLabel = '已约满'
|
||||
|
||||
const diffLevel = bd <= 3 ? 'low' : bd <= 6 ? 'mid' : 'high'
|
||||
const diffLabel = bd <= 3 ? '入门' : bd <= 6 ? '中等' : '困难'
|
||||
const diffColor = diffLevel === 'low' ? '#00E676' : diffLevel === 'mid' ? '#FFA502' : '#FF5252'
|
||||
const diffStars = Math.min(Math.max(Math.ceil(bd / 2), 1), 5)
|
||||
|
||||
return {
|
||||
...c,
|
||||
currentMembers: curMembers,
|
||||
maxMembers: maxMems,
|
||||
isFull: isFull,
|
||||
canBook: canBook,
|
||||
difficultyLevel: bd <= 3 ? 'low' : bd <= 6 ? 'mid' : 'high',
|
||||
difficultyLabel: bd <= 3 ? '入门' : bd <= 6 ? '中等' : '困难',
|
||||
difficultyLevel: diffLevel,
|
||||
difficultyLabel: diffLabel,
|
||||
difficultyColor: diffColor,
|
||||
difficultyStars: diffStars,
|
||||
difficultyPercent: Math.round(bd * 10),
|
||||
baseDifficulty: bd,
|
||||
shortDate: md + '-' + dd,
|
||||
shortTime: hh + ':' + mm,
|
||||
@@ -247,8 +266,8 @@
|
||||
status: statusStr,
|
||||
statusLabel: statusLabel,
|
||||
storedValueAmount: c.storedValueAmount != null ? c.storedValueAmount : 0,
|
||||
labels: c.labels || [],
|
||||
coverImage: c.coverImage || ''
|
||||
labels: this.getTypeLabels(c.courseType),
|
||||
coverImage: resolveCoverUrl(c.coverImage),
|
||||
}
|
||||
})
|
||||
if (this.searchKeyword) {
|
||||
@@ -280,12 +299,26 @@
|
||||
return ta < tb ? -1 : ta > tb ? 1 : 0
|
||||
})
|
||||
}
|
||||
// 只显示可预约的团课
|
||||
list = list.filter(c => c.canBook)
|
||||
// 只显示可预约的团课,且排除用户已主动预约的课程
|
||||
list = list.filter(c => c.canBook && !this.activeBookedCourseIds.has(c.id))
|
||||
return list
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// 从已加载的课程类型中匹配标签
|
||||
getTypeLabels(courseType) {
|
||||
const type = this.getTypeInfo(courseType)
|
||||
return type && type.labels ? type.labels : []
|
||||
},
|
||||
// 从已加载的课程类型中匹配难度
|
||||
getTypeDifficulty(courseType) {
|
||||
const type = this.getTypeInfo(courseType)
|
||||
return type && type.baseDifficulty != null ? type.baseDifficulty : 5
|
||||
},
|
||||
// 查找课程类型信息
|
||||
getTypeInfo(courseType) {
|
||||
return this.courseTypes.find(t => String(t.id) === String(courseType))
|
||||
},
|
||||
// 判断某行是否可见:展开时全部可见,折叠时只有 visibleRowStart 行可见
|
||||
isRowVisible(rowIndex) {
|
||||
if (this.typesExpanded) return true
|
||||
@@ -313,6 +346,24 @@
|
||||
console.error('加载课程类型失败:', e)
|
||||
}
|
||||
},
|
||||
async loadActiveBookings() {
|
||||
try {
|
||||
const memberInfo = store.getMemberInfo()
|
||||
const memberId = memberInfo ? (memberInfo.memberId || memberInfo.id) : null
|
||||
if (!memberId) {
|
||||
this.activeBookedCourseIds = new Set()
|
||||
return
|
||||
}
|
||||
const list = await bookingApi.getMemberBookings(memberId)
|
||||
// 只记录状态为"0"(已预约)的课程ID,status=1(已取消)、2(已出席)、3(缺席)不挡
|
||||
const bookings = Array.isArray(list) ? list : (list.content || [])
|
||||
this.activeBookedCourseIds = new Set(
|
||||
bookings.filter(b => String(b.status) === '0').map(b => b.courseId)
|
||||
)
|
||||
} catch (e) {
|
||||
console.error('加载预约记录失败:', e)
|
||||
}
|
||||
},
|
||||
onSearchInput() {},
|
||||
clearSearch() { this.searchKeyword = '' },
|
||||
onCoverLoad(course) { course.coverLoaded = true },
|
||||
@@ -408,35 +459,56 @@
|
||||
.sort-item.active { color: #00C853; font-weight: 600; }
|
||||
|
||||
.course-list-inner { padding: 12px 16px 0; }
|
||||
.course-item { margin-bottom: 12px; border-radius: 20px; box-shadow: 0 4px 12px rgba(0,0,0,0.06); overflow: hidden; }
|
||||
.course-card { background: #FFFFFF; border-radius: 20px; padding: 14px; display: flex; }
|
||||
.course-cover { width: 72px; height: 72px; border-radius: 12px; background: rgba(0,230,118,0.12); display: flex; align-items: center; justify-content: center; flex-shrink: 0; margin-right: 14px; overflow: hidden; position: relative; }
|
||||
.cover-skeleton { position: absolute; inset: 0; background: linear-gradient(90deg, #E8E8E8 25%, #F0F0F0 50%, #E8E8E8 75%); background-size: 200% 100%; animation: shimmer 1.5s infinite; z-index: 1; }
|
||||
.course-item { margin-bottom: 14px; border-radius: 16px; box-shadow: 0 4px 16px rgba(0,0,0,0.07); overflow: hidden; }
|
||||
.course-card { background: #FFFFFF; border-radius: 16px; display: flex; flex-direction: column; }
|
||||
|
||||
/* ---- 封面区域 ---- */
|
||||
.course-cover { width: 100%; height: 170px; background: linear-gradient(135deg, #E8F5E9 0%, #C8E6C9 100%); display: flex; align-items: center; justify-content: center; position: relative; overflow: hidden; }
|
||||
.cover-skeleton { position: absolute; inset: 0; background: linear-gradient(90deg, #E8E8E8 25%, #F5F5F5 50%, #E8E8E8 75%); background-size: 200% 100%; animation: shimmer 1.5s infinite; z-index: 1; }
|
||||
@keyframes shimmer { 0% { background-position: 200% 0; } 100% { background-position: -200% 0; } }
|
||||
.cover-img { position: absolute; inset: 0; width: 100%; height: 100%; opacity: 0; transition: opacity 0.3s; z-index: 0; }
|
||||
.cover-img.loaded { opacity: 1; z-index: 2; }
|
||||
.cover-text { font-size: 22px; font-weight: 700; color: #00C853; }
|
||||
.course-body { flex: 1; display: flex; flex-direction: column; min-width: 0; }
|
||||
.course-top { display: flex; justify-content: space-between; align-items: flex-start; }
|
||||
.course-name { font-size: 16px; font-weight: 700; color: #1E1E1E; flex: 1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.course-tags { display: flex; flex-shrink: 0; margin-left: 6px; }
|
||||
.course-category { font-size: 11px; color: #00C853; background: rgba(0,230,118,0.12); padding: 1px 8px; border-radius: 20px; font-weight: 500; margin-right: 4px; }
|
||||
.course-difficulty { padding: 1px 8px; border-radius: 20px; font-size: 11px; font-weight: 600; }
|
||||
.diff-low { background: rgba(0,230,118,0.15); color: #00C853; }
|
||||
.diff-mid { background: rgba(255,165,2,0.15); color: #FFA502; }
|
||||
.diff-high { background: rgba(255,82,82,0.15); color: #FF5252; }
|
||||
.course-meta { display: flex; align-items: center; font-size: 12px; color: #7A7E84; margin-top: 4px; }
|
||||
|
||||
/* 无封面时的占位 */
|
||||
.cover-placeholder { position: absolute; inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; z-index: 0; }
|
||||
.cover-icon { font-size: 36px; margin-bottom: 4px; }
|
||||
.cover-type-name { font-size: 16px; font-weight: 700; color: #00C853; }
|
||||
|
||||
/* 封面覆盖层 - 难度角标 */
|
||||
.cover-overlay { position: absolute; top: 10px; right: 10px; z-index: 5; }
|
||||
.diff-badge { display: flex; align-items: center; padding: 4px 10px; border-radius: 20px; background: rgba(0,0,0,0.55); backdrop-filter: blur(4px); }
|
||||
.diff-badge-star { font-size: 11px; margin-right: 1px; }
|
||||
.diff-badge-label { font-size: 11px; font-weight: 600; margin-left: 4px; }
|
||||
.diff-badge.diff-low .diff-badge-star { color: #00E676; }
|
||||
.diff-badge.diff-low .diff-badge-label { color: #69F0AE; }
|
||||
.diff-badge.diff-mid .diff-badge-star { color: #FFA502; }
|
||||
.diff-badge.diff-mid .diff-badge-label { color: #FFBE4D; }
|
||||
.diff-badge.diff-high .diff-badge-star { color: #FF5252; }
|
||||
.diff-badge.diff-high .diff-badge-label { color: #FF8A80; }
|
||||
|
||||
/* ---- 卡片内容 ---- */
|
||||
.course-body { padding: 14px 16px 16px; display: flex; flex-direction: column; }
|
||||
.course-top { display: flex; justify-content: space-between; align-items: center; }
|
||||
.course-name { font-size: 17px; font-weight: 700; color: #1E1E1E; flex: 1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.course-price { font-size: 20px; font-weight: 700; color: #00C853; flex-shrink: 0; margin-left: 8px; }
|
||||
.course-price.free { color: #00C853; }
|
||||
|
||||
/* 标签 */
|
||||
.course-labels { display: flex; flex-wrap: wrap; margin-top: 8px; }
|
||||
.course-label { font-size: 11px; padding: 2px 8px; border-radius: 10px; margin-right: 6px; margin-bottom: 4px; }
|
||||
|
||||
/* 元数据行 */
|
||||
.course-meta { display: flex; align-items: center; font-size: 13px; color: #7A7E84; margin-top: 6px; }
|
||||
.meta-icon { font-size: 12px; margin-right: 4px; flex-shrink: 0; }
|
||||
.course-date { flex-shrink: 0; }
|
||||
.course-time { margin-left: 8px; }
|
||||
.course-duration { margin-left: 8px; color: #00C853; font-weight: 500; }
|
||||
.course-location { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.course-capacity { color: #7A7E84; }
|
||||
.course-labels { display: flex; flex-wrap: wrap; margin-top: 6px; }
|
||||
.course-label { font-size: 10px; padding: 1px 6px; border-radius: 10px; margin-right: 4px; margin-bottom: 2px; }
|
||||
.course-bottom { text-align: right; margin-top: 6px; }
|
||||
.course-price { font-size: 18px; font-weight: 700; color: #00C853; display: block; margin-top: 6px; }
|
||||
.course-price.free { color: #00C853; }
|
||||
.btn-book { background: #00E676; color: #1A1A1A; border: none; padding: 6px 16px; border-radius: 40px; font-weight: 700; font-size: 13px; line-height: 1.4; box-shadow: 0 2px 8px rgba(0,230,118,0.3); }
|
||||
|
||||
/* 底部预约行 */
|
||||
.course-bottom { display: flex; align-items: center; margin-top: 12px; padding-top: 10px; border-top: 1px solid #F0F0F0; }
|
||||
.course-capacity { font-size: 13px; color: #7A7E84; flex: 1; }
|
||||
.btn-book { background: #00E676; color: #1A1A1A; border: none; padding: 10px 28px; border-radius: 40px; font-weight: 700; font-size: 15px; line-height: 1.2; flex-shrink: 0; margin-left: auto; text-align: center; box-shadow: 0 2px 8px rgba(0,230,118,0.3); }
|
||||
.btn-book::after { border: none; }
|
||||
.btn-book.disabled { background: #E0E0E0; color: #BDBDBD; box-shadow: none; }
|
||||
|
||||
|
||||
@@ -60,4 +60,24 @@ http.interceptors.response.use(
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* 将 coverImage 字段值解析为完整图片 URL
|
||||
* - /api/files/{id}/preview → 拼接 baseURL 后返回
|
||||
* - 纯数字 → 构造 /files/{id}/preview 后拼接 baseURL
|
||||
* - 其他 → 原样返回
|
||||
*/
|
||||
function resolveCoverUrl(coverImage) {
|
||||
if (!coverImage) return ''
|
||||
// 新格式:/api/files/{id}/preview
|
||||
if (/^\/api\/files\/\d+\/preview$/.test(coverImage)) {
|
||||
return BASE_URL + coverImage.substring(4) // 去掉 /api
|
||||
}
|
||||
// 旧格式:纯数字 ID
|
||||
if (/^\d+$/.test(coverImage)) {
|
||||
return BASE_URL + '/files/' + coverImage + '/preview'
|
||||
}
|
||||
return coverImage
|
||||
}
|
||||
|
||||
module.exports = http
|
||||
module.exports.resolveCoverUrl = resolveCoverUrl
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
export interface Banner {
|
||||
id?: number
|
||||
imageUrl: string
|
||||
title: string
|
||||
subtitle?: string
|
||||
sortOrder?: number
|
||||
isActive?: boolean
|
||||
linkUrl?: string
|
||||
createdAt?: string
|
||||
updatedAt?: string
|
||||
}
|
||||
|
||||
export const bannerApi = {
|
||||
getAll: () =>
|
||||
request.get<Banner[]>('/banners'),
|
||||
|
||||
getActive: () =>
|
||||
request.get<Banner[]>('/banners/active'),
|
||||
|
||||
getById: (id: number) =>
|
||||
request.get<Banner>(`/banners/${id}`),
|
||||
|
||||
create: (data: Banner) =>
|
||||
request.post<Banner>('/banners', data),
|
||||
|
||||
update: (id: number, data: Banner) =>
|
||||
request.put<Banner>(`/banners/${id}`, data),
|
||||
|
||||
delete: (id: number) =>
|
||||
request.delete<void>(`/banners/${id}`),
|
||||
}
|
||||
@@ -76,6 +76,12 @@ const routes: RouteRecordRaw[] = [
|
||||
component: () => import('@/views/notify/NoticeManagement.vue'),
|
||||
meta: { title: '通知公告' }
|
||||
},
|
||||
{
|
||||
path: 'banners',
|
||||
name: 'BannerManagement',
|
||||
component: () => import('@/views/banner/BannerManagement.vue'),
|
||||
meta: { title: '轮播图管理' }
|
||||
},
|
||||
{
|
||||
path: 'loginlog',
|
||||
name: 'LoginLog',
|
||||
|
||||
@@ -45,6 +45,7 @@ function transformMenuData(backendMenus: BackendMenuItem[]): MenuItem[] {
|
||||
'member/member/index': '/members',
|
||||
'member/card/index': '/member-cards',
|
||||
'statistics/dashboard/index': '/statistics',
|
||||
'banner/index': '/banners',
|
||||
}
|
||||
|
||||
const filteredMenus = backendMenus.filter(menu => menu.menuType !== 'F')
|
||||
@@ -107,6 +108,7 @@ function getMenuIcon(menuName: string): string {
|
||||
'会员管理': 'Avatar',
|
||||
'会员卡管理': 'CreditCard',
|
||||
'数据统计': 'DataAnalysis',
|
||||
'轮播图管理': 'Picture',
|
||||
}
|
||||
return iconMap[menuName] || 'Document'
|
||||
}
|
||||
|
||||
@@ -0,0 +1,454 @@
|
||||
<template>
|
||||
<div class="banner-management">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="card-title">
|
||||
<span>轮播图管理</span>
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="handleAdd"
|
||||
>
|
||||
新增轮播图
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="dataSource"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-table-column
|
||||
prop="id"
|
||||
label="ID"
|
||||
width="80"
|
||||
/>
|
||||
<el-table-column
|
||||
label="预览图"
|
||||
width="120"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<div class="cover-cell">
|
||||
<img
|
||||
v-if="coverCache[row.imageUrl]"
|
||||
:src="coverCache[row.imageUrl]"
|
||||
class="cover-thumb"
|
||||
@click="previewImage(row)"
|
||||
@error="onCoverError(row)"
|
||||
/>
|
||||
<span v-else class="cover-no-data">No data</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="title"
|
||||
label="标题"
|
||||
min-width="140"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<el-table-column
|
||||
prop="subtitle"
|
||||
label="副标题"
|
||||
min-width="180"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<el-table-column
|
||||
prop="sortOrder"
|
||||
label="排序"
|
||||
width="80"
|
||||
align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
label="状态"
|
||||
width="80"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-tag
|
||||
:type="row.isActive ? 'success' : 'danger'"
|
||||
effect="dark"
|
||||
style="font-weight: 500; font-size: 14px;"
|
||||
>
|
||||
{{ row.isActive ? '启用' : '停用' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="createdAt"
|
||||
label="创建时间"
|
||||
width="170"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
{{ formatDateTime(row.createdAt) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
label="操作"
|
||||
width="150"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-button
|
||||
type="primary"
|
||||
link
|
||||
size="small"
|
||||
@click="handleEdit(row)"
|
||||
>
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button
|
||||
type="danger"
|
||||
link
|
||||
size="small"
|
||||
@click="handleDelete(row)"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
|
||||
<!-- 图片预览弹窗 -->
|
||||
<el-dialog v-model="previewVisible" title="图片预览" width="700px">
|
||||
<img
|
||||
v-if="previewUrl"
|
||||
:src="previewUrl"
|
||||
style="width: 100%; max-height: 500px; object-fit: contain"
|
||||
/>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog
|
||||
v-model="modalVisible"
|
||||
:title="modalTitle"
|
||||
width="550px"
|
||||
>
|
||||
<el-form
|
||||
ref="formRef"
|
||||
:model="formState"
|
||||
:rules="formRules"
|
||||
label-width="80px"
|
||||
>
|
||||
<el-form-item label="轮播图" prop="imageUrl">
|
||||
<el-upload
|
||||
class="banner-upload"
|
||||
:http-request="handleUpload"
|
||||
:show-file-list="false"
|
||||
:before-upload="beforeUpload"
|
||||
accept="image/*"
|
||||
>
|
||||
<el-icon class="banner-upload-icon"><Plus /></el-icon>
|
||||
</el-upload>
|
||||
<div v-if="coverPreviewSrc" class="banner-preview-wrap">
|
||||
<img :src="coverPreviewSrc" class="banner-preview-img" />
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="标题" prop="title">
|
||||
<el-input v-model="formState.title" placeholder="请输入轮播图标题" />
|
||||
</el-form-item>
|
||||
<el-form-item label="副标题">
|
||||
<el-input
|
||||
v-model="formState.subtitle"
|
||||
type="textarea"
|
||||
:rows="2"
|
||||
placeholder="请输入轮播图副标题"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="跳转链接">
|
||||
<el-input v-model="formState.linkUrl" placeholder="选填,点击轮播图跳转地址" />
|
||||
</el-form-item>
|
||||
<el-form-item label="排序" prop="sortOrder">
|
||||
<el-input-number v-model="formState.sortOrder" :min="0" :max="999" style="width: 100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="状态">
|
||||
<el-switch
|
||||
v-model="formState.isActive"
|
||||
active-text="启用"
|
||||
inactive-text="停用"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="modalVisible = false">
|
||||
取消
|
||||
</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="handleModalOk"
|
||||
>
|
||||
确定
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, watch, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Plus } from '@element-plus/icons-vue'
|
||||
import type { UploadRequestOptions } from 'element-plus'
|
||||
import request from '@/utils/request'
|
||||
import { bannerApi, type Banner } from '@/api/banner.api'
|
||||
import { formatDateTime } from '@/utils/dateFormat'
|
||||
|
||||
const loading = ref(false)
|
||||
const dataSource = ref<Banner[]>([])
|
||||
const modalVisible = ref(false)
|
||||
const modalTitle = ref('')
|
||||
const formRef = ref()
|
||||
const coverPreviewSrc = ref('')
|
||||
const previewVisible = ref(false)
|
||||
const previewUrl = ref('')
|
||||
|
||||
const formState = reactive<Banner>({
|
||||
imageUrl: '',
|
||||
title: '',
|
||||
subtitle: '',
|
||||
sortOrder: 0,
|
||||
isActive: true,
|
||||
linkUrl: '',
|
||||
})
|
||||
|
||||
const formRules = {
|
||||
imageUrl: [{ required: true, message: '请上传轮播图', trigger: 'change' }],
|
||||
title: [
|
||||
{ required: true, message: '请输入标题', trigger: 'blur' },
|
||||
{ max: 100, message: '标题长度不能超过100个字符', trigger: 'blur' },
|
||||
],
|
||||
sortOrder: [{ required: true, message: '请设置排序', trigger: 'blur' }],
|
||||
}
|
||||
|
||||
// ========== 图片缓存(表格 + 表单预览) ==========
|
||||
const coverCache = reactive<Record<string, string>>({})
|
||||
|
||||
/** 从 imageUrl 中提取文件 ID(/api/files/{id}/preview → id) */
|
||||
const extractFileId = (url: string | undefined): string | null => {
|
||||
if (!url) return null
|
||||
const match = url.match(/\/api\/files\/(\d+)\/preview$/)
|
||||
if (match) return match[1]
|
||||
if (/^\d+$/.test(url)) return url
|
||||
return null
|
||||
}
|
||||
|
||||
/** 加载表格中某行的封面图(完全对照 GroupCourseManagement 的 loadRowCover) */
|
||||
const loadRowCover = async (imageUrl: string | undefined) => {
|
||||
if (!imageUrl) return
|
||||
if (coverCache[imageUrl]) return
|
||||
const fileId = extractFileId(imageUrl)
|
||||
if (fileId) {
|
||||
try {
|
||||
const blob: any = await request.get(`/files/${fileId}/preview`, { responseType: 'blob' })
|
||||
coverCache[imageUrl] = URL.createObjectURL(blob)
|
||||
} catch {
|
||||
// leave empty, template will show "No data"
|
||||
}
|
||||
} else {
|
||||
// 外部 URL 直接使用
|
||||
coverCache[imageUrl] = imageUrl
|
||||
}
|
||||
}
|
||||
|
||||
const onCoverError = (row: Banner) => {
|
||||
if (row.imageUrl) {
|
||||
delete coverCache[row.imageUrl]
|
||||
}
|
||||
}
|
||||
|
||||
/** 加载表单中的封面预览(完全对照 GroupCourseManagement 的 loadCoverPreview) */
|
||||
const loadCoverPreview = async (val: string | undefined) => {
|
||||
if (!val) {
|
||||
coverPreviewSrc.value = ''
|
||||
return
|
||||
}
|
||||
const fileId = extractFileId(val)
|
||||
if (fileId) {
|
||||
try {
|
||||
const blob: any = await request.get(`/files/${fileId}/preview`, { responseType: 'blob' })
|
||||
const oldUrl = coverPreviewSrc.value
|
||||
if (oldUrl) URL.revokeObjectURL(oldUrl)
|
||||
coverPreviewSrc.value = URL.createObjectURL(blob)
|
||||
} catch {
|
||||
coverPreviewSrc.value = ''
|
||||
}
|
||||
} else {
|
||||
coverPreviewSrc.value = val
|
||||
}
|
||||
}
|
||||
|
||||
// 编辑时监听 imageUrl 变化自动加载预览
|
||||
watch(() => formState.imageUrl, (val) => loadCoverPreview(val), { immediate: true })
|
||||
|
||||
// ========== 数据加载 ==========
|
||||
|
||||
const fetchData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await bannerApi.getAll()
|
||||
dataSource.value = (res as Banner[] || [])
|
||||
dataSource.value.forEach(row => loadRowCover(row.imageUrl))
|
||||
} catch {
|
||||
ElMessage.error('加载数据失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 点击预览表格中的图片 */
|
||||
const previewImage = (row: Banner) => {
|
||||
previewUrl.value = row.imageUrl ? (coverCache[row.imageUrl] || row.imageUrl) : ''
|
||||
previewVisible.value = true
|
||||
}
|
||||
|
||||
// ========== 表单操作 ==========
|
||||
|
||||
const handleAdd = () => {
|
||||
modalTitle.value = '新增轮播图'
|
||||
Object.assign(formState, {
|
||||
id: undefined,
|
||||
imageUrl: '',
|
||||
title: '',
|
||||
subtitle: '',
|
||||
sortOrder: 0,
|
||||
isActive: true,
|
||||
linkUrl: '',
|
||||
})
|
||||
modalVisible.value = true
|
||||
}
|
||||
|
||||
const handleEdit = (row: Banner) => {
|
||||
modalTitle.value = '编辑轮播图'
|
||||
Object.assign(formState, { ...row })
|
||||
modalVisible.value = true
|
||||
}
|
||||
|
||||
const handleDelete = async (row: Banner) => {
|
||||
try {
|
||||
await ElMessageBox.confirm('确定要删除该轮播图吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
})
|
||||
await bannerApi.delete(row.id!)
|
||||
ElMessage.success('删除成功')
|
||||
fetchData()
|
||||
} catch (error) {
|
||||
// 取消操作不提示
|
||||
}
|
||||
}
|
||||
|
||||
// ========== 图片上传 ==========
|
||||
|
||||
const beforeUpload = (file: File) => {
|
||||
const isImage = file.type.startsWith('image/')
|
||||
if (!isImage) {
|
||||
ElMessage.error('只能上传图片文件')
|
||||
return false
|
||||
}
|
||||
const isLt5M = file.size / 1024 / 1024 < 5
|
||||
if (!isLt5M) {
|
||||
ElMessage.error('图片大小不能超过 5MB')
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* 自定义上传(完全对照 GroupCourseManagement 的 uploadCover)
|
||||
* - 存储 imageUrl 为 /api/files/{id}/preview
|
||||
* - 上传成功后立即通过 loadCoverPreview 获取 blob 预览
|
||||
*/
|
||||
const handleUpload = async (options: UploadRequestOptions) => {
|
||||
const formData = new FormData()
|
||||
formData.append('file', options.file)
|
||||
try {
|
||||
const res: any = await request.post('/files/upload', formData, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' },
|
||||
})
|
||||
if (res && res.id) {
|
||||
formState.imageUrl = `/api/files/${res.id}/preview`
|
||||
loadCoverPreview(formState.imageUrl)
|
||||
}
|
||||
ElMessage.success('封面上传成功')
|
||||
} catch {
|
||||
ElMessage.error('封面上传失败')
|
||||
}
|
||||
}
|
||||
|
||||
const handleModalOk = async () => {
|
||||
if (!formRef.value) return
|
||||
try {
|
||||
await formRef.value.validate()
|
||||
if (formState.id) {
|
||||
await bannerApi.update(formState.id, formState)
|
||||
} else {
|
||||
await bannerApi.create(formState)
|
||||
}
|
||||
ElMessage.success('操作成功')
|
||||
modalVisible.value = false
|
||||
fetchData()
|
||||
} catch {
|
||||
ElMessage.error('操作失败')
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => fetchData())
|
||||
</script>
|
||||
|
||||
<style scoped lang="css">
|
||||
.banner-management .card-title {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* 表格缩略图 */
|
||||
.cover-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.cover-thumb {
|
||||
width: 64px;
|
||||
height: 48px;
|
||||
object-fit: cover;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.cover-no-data {
|
||||
color: #c0c4cc;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* 上传区域 */
|
||||
.banner-upload :deep(.el-upload) {
|
||||
border: 1px dashed #d9d9d9;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
width: 200px;
|
||||
height: 120px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: border-color 0.3s;
|
||||
}
|
||||
|
||||
.banner-upload :deep(.el-upload:hover) {
|
||||
border-color: #409eff;
|
||||
}
|
||||
|
||||
.banner-upload-icon {
|
||||
font-size: 28px;
|
||||
color: #8c939d;
|
||||
}
|
||||
|
||||
.banner-preview-wrap {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.banner-preview-img {
|
||||
width: 200px;
|
||||
height: 120px;
|
||||
object-fit: cover;
|
||||
border-radius: 6px;
|
||||
}
|
||||
</style>
|
||||
@@ -312,10 +312,12 @@ const loadRowCover = async (coverImage: string | undefined) => {
|
||||
if (!coverImage) return
|
||||
// already loaded
|
||||
if (coverCache[coverImage]) return
|
||||
// if it's a numeric file ID, fetch via preview API
|
||||
if (/^\d+$/.test(coverImage)) {
|
||||
// Extract file ID from either /api/files/{id}/preview format or pure numeric string
|
||||
const match = coverImage.match(/\/api\/files\/(\d+)\/preview$/)
|
||||
const fileId = match ? match[1] : (/^\d+$/.test(coverImage) ? coverImage : null)
|
||||
if (fileId) {
|
||||
try {
|
||||
const blob: any = await request.get(`/files/${coverImage}/preview`, { responseType: 'blob' })
|
||||
const blob: any = await request.get(`/files/${fileId}/preview`, { responseType: 'blob' })
|
||||
coverCache[coverImage] = URL.createObjectURL(blob)
|
||||
} catch {
|
||||
// leave empty, template will show "No data"
|
||||
@@ -413,9 +415,9 @@ const uploadCover = async (options: any) => {
|
||||
})
|
||||
// res is the SysFile object (axios interceptor unwrapped response.data)
|
||||
if (res && res.id) {
|
||||
formState.coverImage = String(res.id)
|
||||
formState.coverImage = `/api/files/${res.id}/preview`
|
||||
// immediately fetch preview for the newly uploaded file
|
||||
loadCoverPreview(String(res.id))
|
||||
loadCoverPreview(formState.coverImage)
|
||||
}
|
||||
ElMessage.success('封面上传成功')
|
||||
} catch {
|
||||
@@ -499,10 +501,12 @@ const loadCoverPreview = async (val: string) => {
|
||||
coverPreviewSrc.value = ''
|
||||
return
|
||||
}
|
||||
// If it looks like a numeric file ID, fetch via preview API
|
||||
if (/^\d+$/.test(val)) {
|
||||
// Extract file ID from either /api/files/{id}/preview format or pure numeric string
|
||||
const match = val.match(/\/api\/files\/(\d+)\/preview$/)
|
||||
const fileId = match ? match[1] : (/^\d+$/.test(val) ? val : null)
|
||||
if (fileId) {
|
||||
try {
|
||||
const blob: any = await request.get(`/files/${val}/preview`, { responseType: 'blob' })
|
||||
const blob: any = await request.get(`/files/${fileId}/preview`, { responseType: 'blob' })
|
||||
const oldUrl = coverPreviewSrc.value
|
||||
if (oldUrl) URL.revokeObjectURL(oldUrl)
|
||||
coverPreviewSrc.value = URL.createObjectURL(blob)
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
-- ============================================
|
||||
-- 轮播图管理 - 菜单数据插入(可独立执行)
|
||||
-- 使用方法: 直接在数据库中运行此 SQL
|
||||
-- ============================================
|
||||
|
||||
-- 轮播图管理菜单项(顶级菜单 parent_id=0)
|
||||
INSERT INTO sys_menu (id, menu_name, parent_id, order_num, menu_type, perms, component, status, created_at, updated_at)
|
||||
SELECT 19, '轮播图管理', 0, 8, 'C', 'system:banner:list', 'banner/index', 1, NOW(), NOW()
|
||||
WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE id = 19);
|
||||
|
||||
-- 按钮权限
|
||||
INSERT INTO sys_menu (id, menu_name, parent_id, order_num, menu_type, perms, component, status, created_at, updated_at)
|
||||
SELECT 191, '轮播图查询', 19, 1, 'F', 'system:banner:query', NULL, 1, NOW(), NOW()
|
||||
WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE id = 191);
|
||||
|
||||
INSERT INTO sys_menu (id, menu_name, parent_id, order_num, menu_type, perms, component, status, created_at, updated_at)
|
||||
SELECT 192, '轮播图新增', 19, 2, 'F', 'system:banner:add', NULL, 1, NOW(), NOW()
|
||||
WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE id = 192);
|
||||
|
||||
INSERT INTO sys_menu (id, menu_name, parent_id, order_num, menu_type, perms, component, status, created_at, updated_at)
|
||||
SELECT 193, '轮播图修改', 19, 3, 'F', 'system:banner:edit', NULL, 1, NOW(), NOW()
|
||||
WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE id = 193);
|
||||
|
||||
INSERT INTO sys_menu (id, menu_name, parent_id, order_num, menu_type, perms, component, status, created_at, updated_at)
|
||||
SELECT 194, '轮播图删除', 19, 4, 'F', 'system:banner:remove', NULL, 1, NOW(), NOW()
|
||||
WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE id = 194);
|
||||
|
||||
-- 重置序列
|
||||
SELECT setval('sys_menu_id_seq', (SELECT COALESCE(MAX(id), 1) FROM sys_menu));
|
||||
@@ -0,0 +1,407 @@
|
||||
-- Novalon管理系统数据库初始化脚本
|
||||
-- 版本: V1
|
||||
-- 描述: 创建所有核心表结构(合并版)
|
||||
|
||||
-- ============================================
|
||||
-- 用户与角色相关表
|
||||
-- ============================================
|
||||
|
||||
-- 用户表
|
||||
CREATE TABLE IF NOT EXISTS sys_user (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
username VARCHAR(50) NOT NULL UNIQUE,
|
||||
password VARCHAR(255) NOT NULL,
|
||||
email VARCHAR(100),
|
||||
phone VARCHAR(20),
|
||||
nickname VARCHAR(100),
|
||||
status INTEGER DEFAULT 1,
|
||||
role_id BIGINT,
|
||||
create_by VARCHAR(50),
|
||||
update_by VARCHAR(50),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
deleted_at TIMESTAMP
|
||||
);
|
||||
|
||||
-- 角色表
|
||||
CREATE TABLE IF NOT EXISTS sys_role (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
role_name VARCHAR(100) NOT NULL,
|
||||
role_key VARCHAR(100) NOT NULL UNIQUE,
|
||||
role_sort INTEGER DEFAULT 0,
|
||||
status INTEGER DEFAULT 1,
|
||||
create_by VARCHAR(50),
|
||||
update_by VARCHAR(50),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
deleted_at TIMESTAMP
|
||||
);
|
||||
|
||||
-- 用户角色关联表(支持多对多关系)
|
||||
CREATE TABLE IF NOT EXISTS user_role (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
user_id BIGINT NOT NULL,
|
||||
role_id BIGINT NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
created_by VARCHAR(50),
|
||||
CONSTRAINT fk_user_role_user FOREIGN KEY (user_id) REFERENCES sys_user(id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_user_role_role FOREIGN KEY (role_id) REFERENCES sys_role(id) ON DELETE CASCADE,
|
||||
CONSTRAINT uk_user_role UNIQUE (user_id, role_id)
|
||||
);
|
||||
|
||||
-- ============================================
|
||||
-- 权限相关表
|
||||
-- ============================================
|
||||
|
||||
-- 权限表
|
||||
CREATE TABLE IF NOT EXISTS sys_permission (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
permission_name VARCHAR(100) NOT NULL,
|
||||
permission_code VARCHAR(100) NOT NULL UNIQUE,
|
||||
resource VARCHAR(200) NOT NULL,
|
||||
action VARCHAR(50) NOT NULL,
|
||||
description VARCHAR(500),
|
||||
status INTEGER DEFAULT 1,
|
||||
create_by VARCHAR(50),
|
||||
update_by VARCHAR(50),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
deleted_at TIMESTAMP
|
||||
);
|
||||
|
||||
-- 角色权限关联表
|
||||
CREATE TABLE IF NOT EXISTS sys_role_permission (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
role_id BIGINT NOT NULL,
|
||||
permission_id BIGINT NOT NULL,
|
||||
create_by VARCHAR(50),
|
||||
update_by VARCHAR(50),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (role_id) REFERENCES sys_role(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (permission_id) REFERENCES sys_permission(id) ON DELETE CASCADE,
|
||||
UNIQUE (role_id, permission_id)
|
||||
);
|
||||
|
||||
-- ============================================
|
||||
-- 菜单相关表
|
||||
-- ============================================
|
||||
|
||||
-- 菜单表
|
||||
CREATE TABLE IF NOT EXISTS sys_menu (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
menu_name VARCHAR(50) NOT NULL,
|
||||
parent_id BIGINT DEFAULT 0,
|
||||
order_num INTEGER DEFAULT 0,
|
||||
menu_type VARCHAR(1) DEFAULT 'C',
|
||||
perms VARCHAR(100),
|
||||
component VARCHAR(200),
|
||||
status INTEGER DEFAULT 1,
|
||||
create_by VARCHAR(50),
|
||||
update_by VARCHAR(50),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
deleted_at TIMESTAMP
|
||||
);
|
||||
|
||||
-- ============================================
|
||||
-- 字典相关表
|
||||
-- ============================================
|
||||
|
||||
-- 字典类型表
|
||||
CREATE TABLE IF NOT EXISTS sys_dict_type (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
dict_name VARCHAR(100) NOT NULL,
|
||||
dict_type VARCHAR(100) NOT NULL UNIQUE,
|
||||
status VARCHAR(1) DEFAULT '0',
|
||||
remark VARCHAR(500),
|
||||
create_by VARCHAR(50),
|
||||
update_by VARCHAR(50),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
deleted_at TIMESTAMP
|
||||
);
|
||||
|
||||
-- 字典数据表
|
||||
CREATE TABLE IF NOT EXISTS sys_dict_data (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
dict_sort INTEGER DEFAULT 0,
|
||||
dict_label VARCHAR(100) NOT NULL,
|
||||
dict_value VARCHAR(100) NOT NULL,
|
||||
dict_type VARCHAR(100) NOT NULL,
|
||||
css_class VARCHAR(100),
|
||||
list_class VARCHAR(100),
|
||||
is_default VARCHAR(1) DEFAULT 'N',
|
||||
status VARCHAR(1) DEFAULT '0',
|
||||
create_by VARCHAR(50),
|
||||
update_by VARCHAR(50),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
deleted_at TIMESTAMP
|
||||
);
|
||||
|
||||
-- 字典表(通用字典)
|
||||
CREATE TABLE IF NOT EXISTS sys_dictionary (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
type VARCHAR(100) NOT NULL,
|
||||
code VARCHAR(100) NOT NULL,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
value VARCHAR(500),
|
||||
remark VARCHAR(500),
|
||||
sort INTEGER DEFAULT 0,
|
||||
create_by VARCHAR(50),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
deleted_at TIMESTAMP
|
||||
);
|
||||
|
||||
-- ============================================
|
||||
-- 系统配置表
|
||||
-- ============================================
|
||||
|
||||
-- 系统配置表
|
||||
CREATE TABLE IF NOT EXISTS sys_config (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
config_name VARCHAR(100) NOT NULL,
|
||||
config_key VARCHAR(100) NOT NULL UNIQUE,
|
||||
config_value VARCHAR(500) NOT NULL,
|
||||
config_type VARCHAR(1) DEFAULT 'N',
|
||||
create_by VARCHAR(50),
|
||||
update_by VARCHAR(50),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
deleted_at TIMESTAMP
|
||||
);
|
||||
|
||||
-- ============================================
|
||||
-- 日志相关表
|
||||
-- ============================================
|
||||
|
||||
-- 登录日志表
|
||||
CREATE TABLE IF NOT EXISTS sys_login_log (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
username VARCHAR(50),
|
||||
ip VARCHAR(50),
|
||||
location VARCHAR(255),
|
||||
browser VARCHAR(50),
|
||||
os VARCHAR(50),
|
||||
status VARCHAR(1),
|
||||
message VARCHAR(255),
|
||||
login_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- 异常日志表
|
||||
CREATE TABLE IF NOT EXISTS sys_exception_log (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
username VARCHAR(50),
|
||||
title VARCHAR(100),
|
||||
exception_name VARCHAR(100),
|
||||
method_name VARCHAR(255),
|
||||
method_params TEXT,
|
||||
exception_msg TEXT,
|
||||
exception_stack TEXT,
|
||||
ip VARCHAR(50),
|
||||
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- 操作日志表
|
||||
CREATE TABLE IF NOT EXISTS operation_log (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
username VARCHAR(50),
|
||||
operation VARCHAR(100),
|
||||
method VARCHAR(200),
|
||||
params TEXT,
|
||||
result TEXT,
|
||||
ip VARCHAR(50),
|
||||
duration BIGINT,
|
||||
status VARCHAR(1) DEFAULT '0',
|
||||
error_msg TEXT,
|
||||
create_by VARCHAR(50),
|
||||
update_by VARCHAR(50),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
deleted_at TIMESTAMP
|
||||
);
|
||||
|
||||
-- 审计日志表
|
||||
CREATE TABLE IF NOT EXISTS audit_log (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
entity_type VARCHAR(100) NOT NULL,
|
||||
entity_id BIGINT,
|
||||
operation_type VARCHAR(20) NOT NULL,
|
||||
operator VARCHAR(100),
|
||||
operation_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
before_data JSONB,
|
||||
after_data JSONB,
|
||||
changed_fields TEXT[],
|
||||
ip_address VARCHAR(50),
|
||||
user_agent TEXT,
|
||||
description TEXT,
|
||||
create_by VARCHAR(50),
|
||||
update_by VARCHAR(50),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
deleted_at TIMESTAMP
|
||||
);
|
||||
|
||||
-- 审计日志归档表
|
||||
CREATE TABLE IF NOT EXISTS audit_log_archive (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
entity_type VARCHAR(100) NOT NULL,
|
||||
entity_id BIGINT,
|
||||
operation_type VARCHAR(20) NOT NULL,
|
||||
operator VARCHAR(100),
|
||||
operation_time TIMESTAMP,
|
||||
before_data JSONB,
|
||||
after_data JSONB,
|
||||
changed_fields TEXT[],
|
||||
ip_address VARCHAR(50),
|
||||
user_agent TEXT,
|
||||
description TEXT,
|
||||
created_at TIMESTAMP,
|
||||
archived_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- ============================================
|
||||
-- 通知与消息表
|
||||
-- ============================================
|
||||
|
||||
-- 系统公告表
|
||||
CREATE TABLE IF NOT EXISTS sys_notice (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
notice_title VARCHAR(50) NOT NULL,
|
||||
notice_type VARCHAR(1) NOT NULL,
|
||||
notice_content TEXT,
|
||||
status VARCHAR(1) DEFAULT '0',
|
||||
create_by VARCHAR(50),
|
||||
update_by VARCHAR(50),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
deleted_at TIMESTAMP
|
||||
);
|
||||
|
||||
-- 用户消息表
|
||||
CREATE TABLE IF NOT EXISTS sys_user_message (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
user_id BIGINT NOT NULL,
|
||||
notice_id BIGINT,
|
||||
message_title VARCHAR(255),
|
||||
message_content TEXT,
|
||||
is_read VARCHAR(1) DEFAULT '0',
|
||||
read_time TIMESTAMP,
|
||||
create_by VARCHAR(50),
|
||||
update_by VARCHAR(50),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
deleted_at TIMESTAMP
|
||||
);
|
||||
|
||||
-- ============================================
|
||||
-- 文件管理表
|
||||
-- ============================================
|
||||
|
||||
-- 文件管理表
|
||||
CREATE TABLE IF NOT EXISTS sys_file (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
file_name VARCHAR(255) NOT NULL,
|
||||
file_path VARCHAR(500) NOT NULL,
|
||||
file_size BIGINT,
|
||||
file_type VARCHAR(100),
|
||||
file_extension VARCHAR(10),
|
||||
storage_type VARCHAR(50),
|
||||
create_by VARCHAR(50),
|
||||
update_by VARCHAR(50),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
deleted_at TIMESTAMP
|
||||
);
|
||||
|
||||
-- ============================================
|
||||
-- OAuth2相关表
|
||||
-- ============================================
|
||||
|
||||
-- OAuth2客户端表
|
||||
CREATE TABLE IF NOT EXISTS oauth2_client (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
client_id VARCHAR(100) NOT NULL UNIQUE,
|
||||
client_secret VARCHAR(255) NOT NULL,
|
||||
client_name VARCHAR(100),
|
||||
web_server_redirect_uri VARCHAR(500),
|
||||
scope VARCHAR(500),
|
||||
authorized_grant_types VARCHAR(500),
|
||||
access_token_validity_seconds INTEGER,
|
||||
refresh_token_validity_seconds INTEGER,
|
||||
auto_approve VARCHAR(1) DEFAULT 'false',
|
||||
enabled VARCHAR(1) DEFAULT 'true',
|
||||
create_by VARCHAR(50),
|
||||
update_by VARCHAR(50),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
deleted_at TIMESTAMP
|
||||
);
|
||||
|
||||
-- ============================================
|
||||
-- 表注释
|
||||
-- ============================================
|
||||
|
||||
COMMENT ON TABLE sys_user IS '系统用户表';
|
||||
COMMENT ON TABLE sys_role IS '系统角色表';
|
||||
COMMENT ON TABLE user_role IS '用户角色关联表';
|
||||
COMMENT ON TABLE sys_permission IS '系统权限表';
|
||||
COMMENT ON TABLE sys_role_permission IS '角色权限关联表';
|
||||
COMMENT ON TABLE sys_menu IS '系统菜单表';
|
||||
COMMENT ON TABLE sys_dict_type IS '字典类型表';
|
||||
COMMENT ON TABLE sys_dict_data IS '字典数据表';
|
||||
COMMENT ON TABLE sys_dictionary IS '通用字典表';
|
||||
COMMENT ON TABLE sys_config IS '系统配置表';
|
||||
COMMENT ON TABLE sys_login_log IS '登录日志表';
|
||||
COMMENT ON TABLE sys_exception_log IS '异常日志表';
|
||||
COMMENT ON TABLE operation_log IS '操作日志表';
|
||||
COMMENT ON TABLE audit_log IS '审计日志表';
|
||||
COMMENT ON TABLE audit_log_archive IS '审计日志归档表';
|
||||
COMMENT ON TABLE sys_notice IS '系统公告表';
|
||||
COMMENT ON TABLE sys_user_message IS '用户消息表';
|
||||
COMMENT ON TABLE sys_file IS '文件管理表';
|
||||
COMMENT ON TABLE oauth2_client IS 'OAuth2客户端表';
|
||||
|
||||
COMMENT ON TABLE sys_exception_log IS '异常日志表';
|
||||
COMMENT ON COLUMN sys_exception_log.id IS '主键ID';
|
||||
COMMENT ON COLUMN sys_exception_log.username IS '操作用户';
|
||||
COMMENT ON COLUMN sys_exception_log.title IS '异常标题';
|
||||
COMMENT ON COLUMN sys_exception_log.exception_name IS '异常名称';
|
||||
COMMENT ON COLUMN sys_exception_log.method_name IS '方法名称';
|
||||
COMMENT ON COLUMN sys_exception_log.method_params IS '方法参数';
|
||||
COMMENT ON COLUMN sys_exception_log.exception_msg IS '异常消息';
|
||||
COMMENT ON COLUMN sys_exception_log.exception_stack IS '异常堆栈';
|
||||
COMMENT ON COLUMN sys_exception_log.ip IS 'IP地址';
|
||||
COMMENT ON COLUMN sys_exception_log.create_time IS '创建时间';
|
||||
|
||||
COMMENT ON TABLE audit_log IS '审计日志表';
|
||||
COMMENT ON COLUMN audit_log.id IS '主键ID';
|
||||
COMMENT ON COLUMN audit_log.entity_type IS '实体类型(如User, Role等)';
|
||||
COMMENT ON COLUMN audit_log.entity_id IS '实体ID';
|
||||
COMMENT ON COLUMN audit_log.operation_type IS '操作类型(CREATE, UPDATE, DELETE)';
|
||||
COMMENT ON COLUMN audit_log.operator IS '操作人';
|
||||
COMMENT ON COLUMN audit_log.operation_time IS '操作时间';
|
||||
COMMENT ON COLUMN audit_log.before_data IS '变更前数据(JSON格式)';
|
||||
COMMENT ON COLUMN audit_log.after_data IS '变更后数据(JSON格式)';
|
||||
COMMENT ON COLUMN audit_log.changed_fields IS '变更字段列表';
|
||||
COMMENT ON COLUMN audit_log.ip_address IS 'IP地址';
|
||||
COMMENT ON COLUMN audit_log.description IS '操作描述';
|
||||
COMMENT ON COLUMN audit_log.created_at IS '记录创建时间';
|
||||
|
||||
COMMENT ON TABLE audit_log_archive IS '审计日志归档表';
|
||||
COMMENT ON COLUMN audit_log_archive.id IS '主键ID';
|
||||
COMMENT ON COLUMN audit_log_archive.entity_type IS '实体类型(如User, Role等)';
|
||||
COMMENT ON COLUMN audit_log_archive.entity_id IS '实体ID';
|
||||
COMMENT ON COLUMN audit_log_archive.operation_type IS '操作类型(CREATE, UPDATE, DELETE)';
|
||||
COMMENT ON COLUMN audit_log_archive.operator IS '操作人';
|
||||
COMMENT ON COLUMN audit_log_archive.operation_time IS '操作时间';
|
||||
COMMENT ON COLUMN audit_log_archive.before_data IS '变更前数据(JSON格式)';
|
||||
COMMENT ON COLUMN audit_log_archive.after_data IS '变更后数据(JSON格式)';
|
||||
COMMENT ON COLUMN audit_log_archive.changed_fields IS '变更字段列表';
|
||||
COMMENT ON COLUMN audit_log_archive.ip_address IS 'IP地址';
|
||||
COMMENT ON COLUMN audit_log_archive.user_agent IS '用户代理';
|
||||
COMMENT ON COLUMN audit_log_archive.description IS '操作描述';
|
||||
COMMENT ON COLUMN audit_log_archive.created_at IS '记录创建时间';
|
||||
COMMENT ON COLUMN audit_log_archive.archived_at IS '归档时间';
|
||||
Reference in New Issue
Block a user