完善业务闭环,实现对轮播图的管理,优化数据库表文件

This commit is contained in:
2026-07-18 16:21:40 +08:00
parent feaf014b4a
commit 97a5eff678
78 changed files with 3210 additions and 100 deletions
@@ -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());
@@ -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)
@@ -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());
@@ -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);
}
@@ -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;
}
}
@@ -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);
}
}
@@ -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 '删除时间(软删除)';
@@ -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 '二维码图片路径';
@@ -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 '软删除时间';
@@ -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 '更新时间';
@@ -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 '支付时间';
@@ -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;
@@ -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));
@@ -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;
@@ -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-失败';
@@ -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 '删除时间(软删除)';
@@ -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 '上课地点(冗余字段,保存预约时的课程快照)';
@@ -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 '记录更新时间';
@@ -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 '删除时间(软删除)';
@@ -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 '删除时间(软删除)';
@@ -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");
}
@@ -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}
@@ -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;
}
}
@@ -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);
}
@@ -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);
}
@@ -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();
}
}
@@ -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;
}
}
@@ -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()
+16
View File
@@ -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
}
+2 -2
View File
@@ -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 })
}
/** 获取会员的预约列表 */
+2
View File
@@ -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
}
}
+25 -9
View File
@@ -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) || '签到失败,请重试'
+122 -50
View File
@@ -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">&#9963;</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">&#9733;</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">&#128339;</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">&#128205;</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"(已预约)的课程IDstatus=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; }
+20
View File
@@ -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
+33
View File
@@ -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}`),
}
+6
View File
@@ -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',
+2
View File
@@ -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)
+29
View File
@@ -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 '归档时间';