完成模块2-2.1团课预约

This commit is contained in:
2026-06-01 19:28:06 +08:00
parent a8c7a4061e
commit 03991319fd
37 changed files with 1911 additions and 270 deletions
@@ -27,7 +27,8 @@ CREATE TABLE IF NOT EXISTS group_course (
CREATE TABLE IF NOT EXISTS group_course_booking (
id BIGSERIAL PRIMARY KEY,
course_id BIGINT NOT NULL,
user_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,
@@ -60,7 +61,8 @@ 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.user_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 '取消时间';
@@ -1,19 +1,28 @@
-- 测试数据1: 进行中的瑜伽课程 (已有部分学员)
-- 场景1: 0人预约,可预约(正常状态,6月15日,距开始还有14天)
INSERT INTO group_course (course_name, coach_id, course_type, start_time, end_time, max_members, current_members, status, location, cover_image, description, create_by, created_at, updated_at) VALUES
('清晨流瑜伽', 101, 1, '2026-05-10 09:00:00', '2026-05-10 10:30:00', 15, 8, '1', 'A座3楼瑜伽教室', '/images/yoga_flow.jpg', '适合有一定基础的学员,通过流畅的体式连接呼吸,唤醒身体能量', 'admin', '2026-05-01 10:00:00', '2026-05-01 10:00:00');
('极速燃脂单车', 104, 2, '2026-06-15 19:30:00', '2026-06-15 20:20:00', 25, 0, 0, '单车房', '/images/spinning.jpg', '跟随音乐节奏变换阻力和速度,体验爬坡与冲刺的快感,一节课消耗800大卡', 'admin', '2026-06-01 11:00:00', '2026-06-01 11:00:00');
-- 测试数据2: 即将开始的搏击课 (几乎满员)
-- 场景2: 已有人预约,可预约(正常状态,6月12日,5/15人)
INSERT INTO group_course (course_name, coach_id, course_type, start_time, end_time, max_members, current_members, status, location, cover_image, description, create_by, created_at, updated_at) VALUES
('燃脂搏击', 102, 2, '2026-05-12 18:30:00', '2026-05-12 19:30:00', 20, 19, '1', '综合训练区', '/images/kickboxing.jpg', '高强度间歇训练,配合音乐快速燃脂,释放压力', 'coach_zhang', '2026-05-02 14:30:00', '2026-05-02 14:30:00');
('清晨流瑜伽', 101, 1, '2026-06-12 09:00:00', '2026-06-12 10:30:00', 15, 5, 0, 'A座3楼瑜伽教室', '/images/yoga_flow.jpg', '适合有一定基础的学员,通过流畅的体式连接呼吸,唤醒身体能量', 'admin', '2026-06-01 10:00:00', '2026-06-01 10:00:00');
-- 测试数据3: 已结束的私教小团课 (课程号已满)
-- 场景3: 满员,不可预约(正常状态但已满员,6月10日,20/20人)
INSERT INTO group_course (course_name, coach_id, course_type, start_time, end_time, max_members, current_members, status, location, cover_image, description, create_by, created_at, updated_at) VALUES
('蜜桃臀塑造', 103, 3, '2026-04-25 19:00:00', '2026-04-25 20:00:00', 10, 10, '2', '私教专', '/images/glute.jpg', '针对性训练臀部肌肉群,打造完美臀线。小班教学,动作一对一纠正', 'coach_li', '2026-04-20 09:15:00', '2026-04-20 09:15:00');
('燃脂搏击', 102, 2, '2026-06-10 18:30:00', '2026-06-10 19:30:00', 20, 20, 0, '综合训练', '/images/kickboxing.jpg', '高强度间歇训练,配合音乐快速燃脂,释放压力。名额已满,无法预约', 'coach_zhang', '2026-06-01 14:30:00', '2026-06-01 14:30:00');
-- 测试数据4: 即将开始的动感单车 (名额充足,尚未有人报名)
-- 场景4: 超出可预约时间,不可预约(正常状态但距开始不足30分钟)
-- 当前时间: 2026-06-01 15:00,课程开始: 2026-06-01 15:20
INSERT INTO group_course (course_name, coach_id, course_type, start_time, end_time, max_members, current_members, status, location, cover_image, description, create_by, created_at, updated_at) VALUES
('极速燃脂单车', 104, 2, '2026-05-15 19:30:00', '2026-05-15 20:20:00', 25, 0, '0', '单车房', '/images/spinning.jpg', '跟随音乐节奏变换阻力和速度,体验爬坡与冲刺的快感,一节课消耗800大卡', 'admin', '2026-05-06 11:00:00', '2026-05-06 11:00:00');
('哈他瑜伽', 101, 1, '2026-06-01 15:20:00', '2026-06-01 16:50:00', 12, 3, 0, '瑜伽教室B', '/images/hatha_yoga.jpg', '基础哈他瑜伽,适合所有级别。距开始不足30分钟,已停止预约', 'coach_li', '2026-06-01 08:00:00', '2026-06-01 08:00:00');
-- 测试数据5: 已删除/作废的课程 (deleted_at 不为空)
INSERT INTO group_course (course_name, coach_id, course_type, start_time, end_time, max_members, current_members, status, location, cover_image, description, create_by, created_at, updated_at, deleted_at) VALUES
('周末冥想修复', 101, 1, '2026-05-03 15:00:00', '2026-05-03 16:00:00', 12, 3, '3', '冥想室', '/images/meditation.jpg', '通过呼吸和正念冥想,深度放松身心,缓解一周疲劳', 'coach_wang', '2026-04-28 08:00:00', '2026-04-28 08:00:00', '2026-04-29 16:20:00');
-- 场景5: 课程已取消,不可预约(status=1)
INSERT INTO group_course (course_name, coach_id, course_type, start_time, end_time, max_members, current_members, status, location, cover_image, description, create_by, created_at, updated_at) VALUES
('周末冥想修复', 101, 1, '2026-06-20 15:00:00', '2026-06-20 16:00:00', 12, 3, 1, '冥想室', '/images/meditation.jpg', '通过呼吸和正念冥想,深度放松身心。该课程已被取消', 'coach_wang', '2026-05-28 08:00:00', '2026-05-28 08:00:00');
-- 场景6: 课程已结束,不可预约(status=2)
INSERT INTO group_course (course_name, coach_id, course_type, start_time, end_time, max_members, current_members, status, location, cover_image, description, create_by, created_at, updated_at) VALUES
('蜜桃臀塑造', 103, 3, '2026-05-30 19:00:00', '2026-05-30 20:00:00', 10, 8, 2, '私教专区', '/images/glute.jpg', '针对性训练臀部肌肉群,课程已于5月30日结束,无法预约。', 'coach_li', '2026-05-20 09:15:00', '2026-05-20 09:15:00');
-- 场景7(可选): 已结束但未满员的课程(status=2,即使有空位也不可预约)
INSERT INTO group_course (course_name, coach_id, course_type, start_time, end_time, max_members, current_members, status, location, cover_image, description, create_by, created_at, updated_at) VALUES
('午间冥想放松', 101, 1, '2026-05-31 12:00:00', '2026-05-31 13:00:00', 15, 6, 2, '冥想室', '/images/meditation_noon.jpg', '午间冥想课程,已于5月31日结束。', 'admin', '2026-05-25 09:00:00', '2026-05-25 09:00:00');
@@ -0,0 +1,281 @@
-- ============================================
-- 1. member_user 表(会员表)
-- ============================================
-- Step 1: 删除已存在的表(如果需要重建)
-- DROP TABLE IF EXISTS member_user CASCADE;
-- Step 2: 创建 member_user 表
CREATE TABLE IF NOT EXISTS member_user (
-- ========== 主键和基础字段(来自BaseEntity==========
id BIGSERIAL PRIMARY KEY, -- 主键ID,自增
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), -- 手机号(AES加密存储)
gender INTEGER DEFAULT 0, -- 性别:0-未知,1-男,2-女
birthday DATE, -- 生日
address VARCHAR(500), -- 地址
avatar VARCHAR(500), -- 头像URL
subscribed BOOLEAN DEFAULT FALSE, -- 是否关注服务号
last_login_at TIMESTAMP, -- 最后登录时间
-- ========== 微信相关字段 ==========
union_id VARCHAR(100), -- 微信UnionID(跨应用唯一标识)
miniapp_open_id VARCHAR(100), -- 小程序OpenID
official_open_id VARCHAR(100), -- 服务号OpenID
-- ========== 软删除字段 ==========
is_deleted BOOLEAN DEFAULT FALSE -- 是否删除(软删除标记)
);
-- Step 3: 创建索引
-- 会员编号索引(唯一索引,加速查询)
CREATE UNIQUE INDEX IF NOT EXISTS idx_member_user_member_no ON member_user(member_no);
-- UnionID索引(加速跨平台用户查找)
CREATE INDEX IF NOT EXISTS idx_member_user_union_id ON member_user(union_id);
-- 小程序OpenID索引(加速小程序登录查询)
CREATE INDEX IF NOT EXISTS idx_member_user_miniapp_openid ON member_user(miniapp_open_id);
-- 服务号OpenID索引(加速服务号事件处理)
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);
-- Step 4: 添加注释
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 表(微信用户表)
-- ============================================
-- Step 1: 删除已存在的表(如果需要重建)
-- DROP TABLE IF EXISTS wechat_user CASCADE;
-- Step 2: 创建 wechat_user 表
CREATE TABLE IF NOT EXISTS wechat_user (
-- ========== 主键和基础字段(来自BaseEntity==========
id BIGSERIAL PRIMARY KEY, -- 主键ID,自增
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, -- 创建时间
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, -- 更新时间
-- ========== 关联字段 ==========
member_id BIGINT NOT NULL, -- 会员ID(外键)
-- ========== 微信标识字段 ==========
union_id VARCHAR(100), -- 微信UnionID
miniapp_openid VARCHAR(100), -- 小程序OpenID
mp_openid VARCHAR(100), -- 服务号OpenID
-- ========== 关注状态字段 ==========
is_subscribed BOOLEAN DEFAULT FALSE, -- 是否关注服务号
subscribe_time TIMESTAMP, -- 首次关注时间
unsubscribe_time TIMESTAMP -- 最后一次取消关注时间
);
-- Step 3: 创建外键约束
ALTER TABLE wechat_user
ADD CONSTRAINT fk_wechat_user_member
FOREIGN KEY (member_id) REFERENCES member_user(id) ON DELETE CASCADE;
-- Step 4: 创建索引
-- UnionID索引(加速跨平台用户查找)
CREATE INDEX IF NOT EXISTS idx_wechat_user_union_id ON wechat_user(union_id);
-- 小程序OpenID索引(加速小程序登录查询)
CREATE INDEX IF NOT EXISTS idx_wechat_user_miniapp_openid ON wechat_user(miniapp_openid);
-- 服务号OpenID索引(加速服务号事件处理)
CREATE INDEX IF NOT EXISTS idx_wechat_user_mp_openid ON wechat_user(mp_openid);
-- 会员ID索引(加速关联查询)
CREATE INDEX IF NOT EXISTS idx_wechat_user_member_id ON wechat_user(member_id);
-- Step 5: 添加注释
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,15 @@
-- ============================================
-- 为团课预约记录表添加课程冗余字段
-- 用于保存预约时的课程快照信息
-- ============================================
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 '上课地点(冗余字段,保存预约时的课程快照)';