7.8 调整前端数据请求,动态加载后端数据;构建镜像部署云服务器
This commit is contained in:
parent
6ebd80a6c8
commit
88f74950bc
@ -1,27 +1,132 @@
|
||||
# /usr/local/bin/docker-compose -f /docs/dev-ops/environment/environment-docker-compose-2.4.yml up -d
|
||||
version: '3.8'
|
||||
# docker-compose -f docker-compose-app.yml up -d
|
||||
# 你需要修改system为你自身系统的仓库名
|
||||
services:
|
||||
group-buying-sys:
|
||||
image: system/group-buying-sys:1.0-SNAPSHOT
|
||||
container_name: group-buying-sys
|
||||
restart: on-failure
|
||||
ports:
|
||||
- "8091:8091"
|
||||
environment:
|
||||
- TZ=PRC
|
||||
- SERVER_PORT=8091
|
||||
volumes:
|
||||
- ./log:/data/log
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-size: "10m"
|
||||
max-file: "3"
|
||||
networks:
|
||||
- my-network
|
||||
|
||||
networks:
|
||||
my-network:
|
||||
driver: bridge
|
||||
|
||||
services:
|
||||
# 1. 前端
|
||||
group-buy-market-front:
|
||||
image: nginx:alpine
|
||||
container_name: group-buy-market-front
|
||||
restart: always
|
||||
ports:
|
||||
- '80:80'
|
||||
- '443:443'
|
||||
volumes:
|
||||
- ./nginx/html:/usr/share/nginx/html
|
||||
privileged: true
|
||||
networks:
|
||||
- my-network
|
||||
|
||||
# 2. MySQL
|
||||
mysql:
|
||||
image: mysql:8.0
|
||||
container_name: mysql
|
||||
command: --default-authentication-plugin=mysql_native_password
|
||||
restart: always
|
||||
environment:
|
||||
TZ: Asia/Shanghai
|
||||
MYSQL_ROOT_PASSWORD: 123456
|
||||
ports:
|
||||
- '13306:3306' # 宿主机访问用 13306
|
||||
volumes:
|
||||
- ./mysql/my.cnf:/etc/mysql/conf.d/mysql.cnf:ro
|
||||
- ./mysql/sql:/docker-entrypoint-initdb.d
|
||||
healthcheck:
|
||||
test: ['CMD', 'mysqladmin', 'ping', '-h', 'localhost']
|
||||
interval: 5s
|
||||
timeout: 10s
|
||||
retries: 10
|
||||
start_period: 15s
|
||||
networks:
|
||||
- my-network
|
||||
|
||||
# 3. Redis
|
||||
redis:
|
||||
image: redis:6.2
|
||||
container_name: redis
|
||||
restart: always
|
||||
hostname: redis
|
||||
ports:
|
||||
- '16379:6379' # 宿主机访问用 16379
|
||||
volumes:
|
||||
- ./redis/redis.conf:/usr/local/etc/redis/redis.conf
|
||||
command: redis-server /usr/local/etc/redis/redis.conf
|
||||
healthcheck:
|
||||
test: ['CMD', 'redis-cli', 'ping']
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
networks:
|
||||
- my-network
|
||||
|
||||
# 4. Java 后端
|
||||
group-buying-sys:
|
||||
image: smile/group-buying-sys
|
||||
container_name: group-buying-sys
|
||||
restart: on-failure
|
||||
depends_on:
|
||||
mysql:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
condition: service_healthy
|
||||
ports:
|
||||
- '8091:8091'
|
||||
environment:
|
||||
# 时区 & 端口
|
||||
- TZ=PRC
|
||||
- SERVER_PORT=8091
|
||||
# —— MySQL ——
|
||||
- SPRING_DATASOURCE_USERNAME=root
|
||||
- SPRING_DATASOURCE_PASSWORD=123456
|
||||
- SPRING_DATASOURCE_URL=jdbc:mysql://mysql:3306/big_market?useUnicode=true&characterEncoding=utf8&autoReconnect=true&serverTimezone=Asia/Shanghai&useSSL=false
|
||||
- SPRING_DATASOURCE_DRIVER_CLASS_NAME=com.mysql.cj.jdbc.Driver
|
||||
- SPRING_HIKARI_POOL_NAME=Retail_HikariCP
|
||||
# —— Redis ——
|
||||
- REDIS_SDK_CONFIG_HOST=redis
|
||||
- REDIS_SDK_CONFIG_PORT=6379
|
||||
volumes:
|
||||
- ./log:/data/log
|
||||
logging:
|
||||
driver: json-file
|
||||
options:
|
||||
max-size: '10m'
|
||||
max-file: '3'
|
||||
networks:
|
||||
- my-network
|
||||
|
||||
# 5. phpMyAdmin
|
||||
phpmyadmin:
|
||||
image: phpmyadmin:5.2.1
|
||||
container_name: phpmyadmin
|
||||
hostname: phpmyadmin
|
||||
depends_on:
|
||||
mysql:
|
||||
condition: service_healthy
|
||||
ports:
|
||||
- '8899:80'
|
||||
environment:
|
||||
- PMA_HOST=mysql
|
||||
- PMA_PORT=3306
|
||||
- MYSQL_ROOT_PASSWORD=123456
|
||||
networks:
|
||||
- my-network
|
||||
|
||||
# 6. Redis Commander
|
||||
redis-admin:
|
||||
image: spryker/redis-commander:0.8.0
|
||||
container_name: redis-admin
|
||||
hostname: redis-commander
|
||||
restart: always
|
||||
depends_on:
|
||||
redis:
|
||||
condition: service_healthy
|
||||
ports:
|
||||
- '8081:8081'
|
||||
environment:
|
||||
- REDIS_HOSTS=local:redis:6379
|
||||
- HTTP_USER=admin
|
||||
- HTTP_PASSWORD=admin
|
||||
networks:
|
||||
- my-network
|
||||
|
@ -12,6 +12,7 @@ services:
|
||||
ports:
|
||||
- "13306:3306"
|
||||
volumes:
|
||||
- ./mysql/my.cnf:/etc/mysql/conf.d/mysql.cnf:ro
|
||||
- ./mysql/sql:/docker-entrypoint-initdb.d
|
||||
healthcheck:
|
||||
test: [ "CMD", "mysqladmin" ,"ping", "-h", "localhost" ]
|
||||
|
@ -1,4 +1,4 @@
|
||||
version: '3.9'
|
||||
version: '3.8'
|
||||
services:
|
||||
mysql:
|
||||
image: mysql:8.0
|
||||
@ -11,6 +11,7 @@ services:
|
||||
ports:
|
||||
- "13306:3306"
|
||||
volumes:
|
||||
- ./mysql/my.cnf:/etc/mysql/conf.d/mysql.cnf:ro
|
||||
- ./mysql/sql:/docker-entrypoint-initdb.d
|
||||
healthcheck:
|
||||
test: [ "CMD", "mysqladmin" ,"ping", "-h", "localhost" ]
|
||||
|
24
docs/dev-ops/mysql/my.cnf
Normal file
24
docs/dev-ops/mysql/my.cnf
Normal file
@ -0,0 +1,24 @@
|
||||
[client]
|
||||
port = 3306
|
||||
default-character-set = utf8mb4
|
||||
|
||||
[mysqld]
|
||||
user = mysql
|
||||
port = 3306
|
||||
sql_mode = NO_ENGINE_SUBSTITUTION,STRICT_TRANS_TABLES
|
||||
|
||||
default-storage-engine = InnoDB
|
||||
default-authentication-plugin = mysql_native_password
|
||||
character-set-server = utf8mb4
|
||||
collation-server = utf8mb4_unicode_ci
|
||||
init_connect = 'SET NAMES utf8mb4'
|
||||
|
||||
slow_query_log
|
||||
#long_query_time = 3
|
||||
slow-query-log-file = /var/log/mysql/mysql.slow.log
|
||||
log-error = /var/log/mysql/mysql.error.log
|
||||
|
||||
default-time-zone = '+8:00'
|
||||
|
||||
[mysql]
|
||||
default-character-set = utf8mb4
|
@ -1,140 +1,83 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||
<title>手写MyBatis:渐进式源码实践 - 拼多多</title>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||
<link rel="stylesheet" href="css/index.css">
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||
<title>手写 MyBatis:渐进式源码实践 - 拼多多</title>
|
||||
|
||||
<!-- 现成样式 -->
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" />
|
||||
<link rel="stylesheet" href="css/index.css" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<!-- 顶部轮播图 -->
|
||||
<div class="swiper-container">
|
||||
<div class="swiper-wrapper">
|
||||
<div class="swiper-slide"><img src="images/goods_info2.png"></div>
|
||||
<div class="swiper-slide"><img src="images/goods_info3.png"></div>
|
||||
<div class="swiper-slide"><img src="images/goods_info1.png"></div>
|
||||
<div class="swiper-slide"><img src="images/goods_info2.png" /></div>
|
||||
<div class="swiper-slide"><img src="images/goods_info3.png" /></div>
|
||||
<div class="swiper-slide"><img src="images/goods_info1.png" /></div>
|
||||
</div>
|
||||
<div class="swiper-pagination"></div>
|
||||
</div>
|
||||
|
||||
<!-- 商品信息区域 -->
|
||||
<!-- 商品信息 -->
|
||||
<div class="product-info">
|
||||
<div class="price-row">
|
||||
<div class="current-price">80</div>
|
||||
<div class="original-price">100</div>
|
||||
<div class="current-price" id="currentPrice"></div>
|
||||
<div class="original-price" id="originalPrice"></div>
|
||||
</div>
|
||||
<div class="title">手写MyBatis:渐进式源码实践(全彩)</div>
|
||||
|
||||
<!-- 标题写死即可,如需从接口取也可在 JS 中替换 -->
|
||||
<div class="title" id="goodsTitle">手写 MyBatis:渐进式源码实践(全彩)</div>
|
||||
|
||||
<div class="promo-row">
|
||||
<span class="promo-tag">大促优惠</span>
|
||||
<!-- 直降价 -->
|
||||
<span class="promo-box drop">直降 ¥60</span>
|
||||
<!-- 已抢件数,由 JS 动态写入 -->
|
||||
<span class="promo-box drop" id="dropPrice"></span>
|
||||
<span class="promo-box sold" id="soldBox"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 拼单区域 - 修改了高度 -->
|
||||
<!-- 拼单区域 -->
|
||||
<div class="group-buying">
|
||||
<div class="section-title" id="groupTitle"></div>
|
||||
|
||||
<div class="group-users">
|
||||
<div class="user-list" id="userList">
|
||||
<div class="user-item">
|
||||
<div class="user-avatar">
|
||||
<i class="fas fa-user"></i>
|
||||
</div>
|
||||
<div class="user-info">
|
||||
<div class="user-name">宇哥</div>
|
||||
<div class="user-status">拼单即将结束 <span class="countdown">00:05:49</span></div>
|
||||
</div>
|
||||
<button class="buy-btn" data-price="80">参与拼团</button>
|
||||
</div>
|
||||
<div class="user-item">
|
||||
<div class="user-avatar">
|
||||
<i class="fas fa-user"></i>
|
||||
</div>
|
||||
<div class="user-info">
|
||||
<div class="user-name">李二狗</div>
|
||||
<div class="user-status">拼单即将结束 <span class="countdown">00:05:49</span></div>
|
||||
</div>
|
||||
<button class="buy-btn" data-price="80">参与拼团</button>
|
||||
</div>
|
||||
<div class="user-item">
|
||||
<div class="user-avatar">
|
||||
<i class="fas fa-user"></i>
|
||||
</div>
|
||||
<div class="user-info">
|
||||
<div class="user-name">张全蛋</div>
|
||||
<div class="user-status">拼单即将结束 <span class="countdown">00:02:30</span></div>
|
||||
</div>
|
||||
<button class="buy-btn" data-price="80">参与拼团</button>
|
||||
</div>
|
||||
<div class="user-item">
|
||||
<div class="user-avatar">
|
||||
<i class="fas fa-user"></i>
|
||||
</div>
|
||||
<div class="user-info">
|
||||
<div class="user-name">王翠花</div>
|
||||
<div class="user-status">拼单即将结束 <span class="countdown">00:01:15</span></div>
|
||||
</div>
|
||||
<button class="buy-btn" data-price="80">参与拼团</button>
|
||||
</div>
|
||||
<!-- 添加更多用户项确保轮播效果 -->
|
||||
<div class="user-item">
|
||||
<div class="user-avatar">
|
||||
<i class="fas fa-user"></i>
|
||||
</div>
|
||||
<div class="user-info">
|
||||
<div class="user-name">刘大壮</div>
|
||||
<div class="user-status">拼单即将结束 <span class="countdown">00:03:45</span></div>
|
||||
</div>
|
||||
<button class="buy-btn" data-price="80">参与拼团</button>
|
||||
</div>
|
||||
<div class="user-item">
|
||||
<div class="user-avatar">
|
||||
<i class="fas fa-user"></i>
|
||||
</div>
|
||||
<div class="user-info">
|
||||
<div class="user-name">赵小敏</div>
|
||||
<div class="user-status" data-price="80">拼单即将结束 <span class="countdown">00:04:20</span></div>
|
||||
</div>
|
||||
<button class="buy-btn">参与拼团</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 列表由 JS 动态注入 -->
|
||||
<div class="user-list" id="userList"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 底部操作栏 -->
|
||||
<div class="action-bar">
|
||||
<div class="action-btn">
|
||||
<i class="fas fa-home"></i>
|
||||
<span>首页</span>
|
||||
<i class="fas fa-home"></i><span>首页</span>
|
||||
</div>
|
||||
<div class="action-btn">
|
||||
<i class="fas fa-heart"></i>
|
||||
<span>收藏</span>
|
||||
<i class="fas fa-heart"></i><span>收藏</span>
|
||||
</div>
|
||||
<div class="action-btn">
|
||||
<i class="fas fa-shopping-cart"></i>
|
||||
<span>购物车</span>
|
||||
<i class="fas fa-shopping-cart"></i><span>购物车</span>
|
||||
</div>
|
||||
|
||||
<div class="purchase-btn">
|
||||
<button class="btn-single" data-price="100">
|
||||
<span class="btn-price">¥100</span>
|
||||
<button class="btn-single" id="btnSingle" data-price="">
|
||||
<span class="btn-price" id="singlePrice"></span>
|
||||
<span class="btn-label">单独购买</span>
|
||||
</button>
|
||||
<button class="btn-group" data-price="80">
|
||||
<span class="btn-price">¥80</span>
|
||||
<button class="btn-group" id="btnGroup" data-price="">
|
||||
<span class="btn-price" id="groupPrice"></span>
|
||||
<span class="btn-label">开团购买</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 支付弹窗(默认隐藏) -->
|
||||
|
||||
<!-- 支付弹窗 -->
|
||||
<div id="paymentModal" class="pay-mask">
|
||||
<div class="pay-box">
|
||||
<h2 class="pay-title">请扫码支付</h2>
|
||||
<p class="pay-amount" id="paymentAmount"></p>
|
||||
<img src="images/qrcode.png" alt="支付二维码" class="qr-code">
|
||||
<img src="images/qrcode.png" alt="支付二维码" class="qr-code" />
|
||||
<div class="pay-btns">
|
||||
<button id="cancelPayment" class="btn-secondary">取消支付</button>
|
||||
<button id="completePayment" class="btn-primary">支付完成</button>
|
||||
@ -142,6 +85,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 逻辑脚本 -->
|
||||
<script src="js/index.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
|
@ -1,205 +1,253 @@
|
||||
// index.js (改进版)
|
||||
// 功能:
|
||||
// 1. 解决用户列表竖向轮播在无缝跳转时出现的卡顿/闪动问题
|
||||
// 2. 为每条拼单信息增加实时倒计时,秒级更新
|
||||
/* -------------------------------------------------------
|
||||
* Author : 你
|
||||
* Desc : 改进版拼团页面脚本(userId 从 cookie 里读)
|
||||
* ----------------------------------------------------- */
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
/* ---------- 顶部横向轮播 ---------- */
|
||||
/* ========== 通用工具 ========== */
|
||||
const getCookie = (k) =>
|
||||
document.cookie
|
||||
.split(';')
|
||||
.map((c) => c.trim())
|
||||
.find((c) => c.startsWith(k + '='))?.split('=')[1] || null;
|
||||
|
||||
/* ----------- 0. DOM 快捷引用 ----------- */
|
||||
const $ = (id) => document.getElementById(id);
|
||||
const currentPrice = $('currentPrice');
|
||||
const originalPriceElem = $('originalPrice');
|
||||
const dropPrice = $('dropPrice');
|
||||
const soldBox = $('soldBox');
|
||||
const groupTitle = $('groupTitle');
|
||||
const userList = $('userList');
|
||||
const singlePriceSpan = $('singlePrice');
|
||||
const groupPriceSpan = $('groupPrice');
|
||||
const btnSingle = $('btnSingle');
|
||||
const btnGroup = $('btnGroup');
|
||||
|
||||
/* =====================================================
|
||||
* 1. 取接口数据并渲染
|
||||
* =================================================== */
|
||||
const API_URL = 'http://127.0.0.1:8091/api/v1/gbm/index/query_group_buy_market_config';
|
||||
|
||||
// 读取 cookie 中的 username 当作 userId
|
||||
const username = getCookie('username');
|
||||
|
||||
// 如果没登录,直接跳去登录页,免得后面接口 401/判空
|
||||
if (!username) {
|
||||
location.href = 'login.html';
|
||||
return;
|
||||
}
|
||||
|
||||
const POST_BODY = {
|
||||
userId : username, // 不再写死
|
||||
source : 's01',
|
||||
channel: 'c01',
|
||||
goodsId: '9890001'
|
||||
};
|
||||
|
||||
fetch(API_URL, {
|
||||
method : 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body : JSON.stringify(POST_BODY),
|
||||
})
|
||||
.then((r) => r.json())
|
||||
.then(({ code, info, data }) => {
|
||||
if (code !== '0000' || !data) {
|
||||
console.error('接口异常:', info);
|
||||
return;
|
||||
}
|
||||
renderGoods(data.goods);
|
||||
renderStatistic(data.teamStatistic);
|
||||
renderTeams(data.teamList, data.goods?.payPrice);
|
||||
})
|
||||
.catch((e) => console.error('接口请求失败:', e));
|
||||
|
||||
/* ------------- 渲染商品信息 ------------- */
|
||||
function renderGoods(g = {}) {
|
||||
const { originalPrice = 0, payPrice = 0, deductionPrice = 0 } = g;
|
||||
currentPrice.textContent = payPrice;
|
||||
originalPriceElem.textContent = originalPrice;
|
||||
dropPrice.textContent = `直降 ¥${deductionPrice}`;
|
||||
|
||||
singlePriceSpan.textContent = `¥${originalPrice}`;
|
||||
groupPriceSpan.textContent = `¥${payPrice}`;
|
||||
btnSingle.dataset.price = originalPrice;
|
||||
btnGroup.dataset.price = payPrice;
|
||||
}
|
||||
|
||||
/* ------------- 渲染统计信息 ------------- */
|
||||
function renderStatistic(stat = {}) {
|
||||
const { allTeamUserCount = 0 } = stat;
|
||||
groupTitle.textContent = `${allTeamUserCount}人在抢,参与可立即拼成`;
|
||||
soldBox.textContent = `${allTeamUserCount}人再抢`;
|
||||
}
|
||||
|
||||
/* ------------- 渲染拼团列表 ------------- */
|
||||
function renderTeams(list = [], groupPrice = 0) {
|
||||
if (!list || list.length === 0) {
|
||||
groupTitle.textContent = '小伙伴,赶紧去开团吧,做村里最靓的仔。';
|
||||
return;
|
||||
}
|
||||
userList.innerHTML = '';
|
||||
list.forEach((t) => userList.appendChild(makeItem(t, groupPrice)));
|
||||
initUserMarquee();
|
||||
initCountdown();
|
||||
}
|
||||
|
||||
function makeItem(team, price) {
|
||||
const { userId, targetCount, lockCount, validTimeCountdown } = team;
|
||||
const leftNum = Math.max(targetCount - lockCount, 0);
|
||||
const timeText = validTimeCountdown || '00:00:00';
|
||||
|
||||
const div = document.createElement('div');
|
||||
div.className = 'user-item';
|
||||
div.innerHTML = `
|
||||
<div class="user-avatar"><i class="fas fa-user"></i></div>
|
||||
<div class="user-info">
|
||||
<div class="user-name">${userId}</div>
|
||||
<div class="user-status">
|
||||
仅剩${leftNum}人成团
|
||||
<span class="countdown">${timeText}</span>
|
||||
</div>
|
||||
</div>
|
||||
<button class="buy-btn" data-price="${price}">参与拼团</button>
|
||||
`;
|
||||
return div;
|
||||
}
|
||||
|
||||
/* =====================================================
|
||||
* 2. 拼单列表纵向轮播
|
||||
* =================================================== */
|
||||
function initUserMarquee() {
|
||||
const items = userList.querySelectorAll('.user-item');
|
||||
if (items.length <= 1) return;
|
||||
|
||||
const itemH = items[0].offsetHeight;
|
||||
userList.appendChild(items[0].cloneNode(true)); // 无缝衔接
|
||||
|
||||
let idx = 0;
|
||||
userList.addEventListener('transitionend', () => {
|
||||
if (idx >= items.length) {
|
||||
userList.style.transition = 'none';
|
||||
userList.style.transform = 'translateY(0)';
|
||||
idx = 0;
|
||||
void userList.offsetWidth;
|
||||
}
|
||||
});
|
||||
|
||||
setInterval(() => {
|
||||
idx++;
|
||||
userList.style.transition = 'transform .5s ease';
|
||||
userList.style.transform = `translateY(${-idx * itemH}px)`;
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
/* =====================================================
|
||||
* 3. 倒计时
|
||||
* =================================================== */
|
||||
let countdownData = [];
|
||||
|
||||
function initCountdown() {
|
||||
const els = document.querySelectorAll('.countdown');
|
||||
countdownData = Array.from(els).map((el) => ({
|
||||
el,
|
||||
remain: toSec(el.textContent.trim()),
|
||||
}));
|
||||
setInterval(tick, 1000);
|
||||
}
|
||||
|
||||
const toSec = (t) => {
|
||||
if (!t.includes(':')) return 0;
|
||||
const [h = '00', m = '00', s = '00'] = t.split(':');
|
||||
return +h * 3600 + +m * 60 + +s;
|
||||
};
|
||||
const fmt = (n) => String(n).padStart(2, '0');
|
||||
const format = (s) => `${fmt(s / 3600 | 0)}:${fmt((s % 3600) / 60 | 0)}:${fmt(s % 60)}`;
|
||||
|
||||
function tick() {
|
||||
countdownData.forEach((c) => {
|
||||
if (c.remain > 0) {
|
||||
c.remain--;
|
||||
c.el.textContent = format(c.remain);
|
||||
if (c.remain === 0) expire(c.el);
|
||||
}
|
||||
});
|
||||
}
|
||||
function expire(el) {
|
||||
el.textContent = '00:00:00';
|
||||
const item = el.closest('.user-item');
|
||||
item?.classList.add('expired');
|
||||
item?.querySelector('.buy-btn')?.setAttribute('disabled', 'disabled');
|
||||
}
|
||||
|
||||
/* =====================================================
|
||||
* 4. 支付弹窗(事件委托)
|
||||
* =================================================== */
|
||||
const modal = $('paymentModal');
|
||||
const amountText = $('paymentAmount');
|
||||
const cancelPayment = $('cancelPayment');
|
||||
const completePayment= $('completePayment');
|
||||
|
||||
document.body.addEventListener('click', (e) => {
|
||||
const btn = e.target.closest('.buy-btn, .btn-single, .btn-group');
|
||||
if (!btn) return;
|
||||
|
||||
// 再次确认 cookie,防止手动删 cookie
|
||||
if (!getCookie('username')) {
|
||||
location.href = 'login.html';
|
||||
return;
|
||||
}
|
||||
|
||||
amountText.textContent = `支付金额 ¥${btn.dataset.price || 0}`;
|
||||
modal.style.display = 'flex';
|
||||
});
|
||||
|
||||
cancelPayment.onclick = () => modal.style.display = 'none';
|
||||
completePayment.onclick= () => { alert('支付成功!'); modal.style.display = 'none'; };
|
||||
modal.addEventListener('click', (e) => { if (e.target === modal) modal.style.display = 'none'; });
|
||||
|
||||
/* =====================================================
|
||||
* 5. 顶部横向轮播(原逻辑保留)
|
||||
* =================================================== */
|
||||
const wrapper = document.querySelector('.swiper-wrapper');
|
||||
const slides = [...wrapper.children];
|
||||
const pagination = document.querySelector('.swiper-pagination');
|
||||
const count = slides.length;
|
||||
|
||||
let current = 0; // 当前索引
|
||||
let startX = 0; // 手势起点
|
||||
let dragging = false; // 拖动状态
|
||||
let timer = null; // 自动轮播计时器
|
||||
|
||||
/* --- 1. 生成分页小圆点 --- */
|
||||
for(let i=0;i<count;i++){
|
||||
let current = 0, startX = 0, dragging = false, timer;
|
||||
for (let i = 0; i < count; i++) {
|
||||
const dot = document.createElement('div');
|
||||
dot.className = 'swiper-dot' + (i===0?' active':'');
|
||||
dot.addEventListener('click',()=>goTo(i));
|
||||
dot.className = 'swiper-dot' + (i === 0 ? ' active' : '');
|
||||
dot.onclick = () => goTo(i);
|
||||
pagination.appendChild(dot);
|
||||
}
|
||||
const dots = pagination.children;
|
||||
|
||||
/* --- 2. 切换核心 --- */
|
||||
function goTo(index){
|
||||
current = (index + count) % count; // 防越界
|
||||
const goTo = (i) => {
|
||||
current = (i + count) % count;
|
||||
wrapper.style.transition = 'transform .3s ease';
|
||||
wrapper.style.transform = `translateX(-${current*100}%)`;
|
||||
[...dots].forEach((d,i)=>d.classList.toggle('active',i===current));
|
||||
}
|
||||
|
||||
/* --- 3. 自动轮播 --- */
|
||||
function startAuto(){
|
||||
timer = setInterval(()=>goTo(current+1),3000);
|
||||
}
|
||||
function stopAuto(){
|
||||
clearInterval(timer);
|
||||
}
|
||||
startAuto();
|
||||
|
||||
/* --- 4. 手势/鼠标拖动 --- */
|
||||
const getX = e => e.touches ? e.touches[0].clientX : e.clientX;
|
||||
|
||||
wrapper.addEventListener('pointerdown',e=>{
|
||||
stopAuto();
|
||||
dragging = true;
|
||||
startX = getX(e);
|
||||
wrapper.style.transition = 'none'; // 跟手拖动
|
||||
});
|
||||
|
||||
wrapper.addEventListener('pointermove',e=>{
|
||||
if(!dragging) return;
|
||||
const diff = getX(e) - startX; // 像素位移
|
||||
wrapper.style.transform =
|
||||
`translateX(calc(${-current*100}% + ${diff}px))`;
|
||||
});
|
||||
|
||||
const endSwipe = e=>{
|
||||
if(!dragging) return;
|
||||
dragging = false;
|
||||
const diff = getX(e) - startX;
|
||||
const limit = wrapper.offsetWidth * 0.15; // 15% 宽度阈值
|
||||
if(diff > limit) goTo(current-1);
|
||||
else if(diff < -limit) goTo(current+1);
|
||||
else goTo(current); // 回弹
|
||||
startAuto();
|
||||
wrapper.style.transform = `translateX(-${current * 100}%)`;
|
||||
[...dots].forEach((d, j) => d.classList.toggle('active', j === current));
|
||||
};
|
||||
wrapper.addEventListener('pointerup', endSwipe);
|
||||
wrapper.addEventListener('pointercancel', endSwipe);
|
||||
wrapper.addEventListener('pointerleave', endSwipe);
|
||||
const auto = () => { timer = setInterval(() => goTo(current + 1), 3000); };
|
||||
const stop = () => clearInterval(timer);
|
||||
auto();
|
||||
|
||||
const getX = (e) => (e.touches ? e.touches[0].clientX : e.clientX);
|
||||
wrapper.addEventListener('pointerdown', (e) => { stop(); dragging = true; startX = getX(e); wrapper.style.transition = 'none'; });
|
||||
wrapper.addEventListener('pointermove', (e) => { if (!dragging) return; const diff = getX(e) - startX; wrapper.style.transform = `translateX(calc(${-current * 100}% + ${diff}px))`; });
|
||||
wrapper.addEventListener('pointerup', endSwipe);
|
||||
wrapper.addEventListener('pointercancel',endSwipe);
|
||||
wrapper.addEventListener('pointerleave', endSwipe);
|
||||
|
||||
/* --------- 动态生成“xx人在抢,参与可立即拼成” --------- */
|
||||
const leftNum = Math.floor(Math.random() * 101) + 100; // 100 ~ 200
|
||||
const groupTitle = document.getElementById('groupTitle');
|
||||
if (groupTitle) groupTitle.textContent = `${leftNum}人在抢,参与可立即拼成`;
|
||||
|
||||
/* ---------- 拼单用户纵向轮播 ---------- */
|
||||
const userList = document.getElementById('userList');
|
||||
const userItems = userList.querySelectorAll('.user-item');
|
||||
const itemHeight = userItems[0].offsetHeight;
|
||||
let userIndex = 0;
|
||||
const originalCount = userItems.length;
|
||||
|
||||
// 克隆第一条放到末尾,实现无缝衔接
|
||||
userList.appendChild(userItems[0].cloneNode(true));
|
||||
|
||||
userList.addEventListener('transitionend', () => {
|
||||
if (userIndex >= originalCount) {
|
||||
// 闪电跳回首条,关闭过渡以避免闪屏
|
||||
userList.style.transition = 'none';
|
||||
userList.style.transform = 'translateY(0)';
|
||||
userIndex = 0;
|
||||
// 强制回流,保证下次 transition 生效
|
||||
void userList.offsetWidth;
|
||||
}
|
||||
});
|
||||
|
||||
function rotateUsers() {
|
||||
userIndex++;
|
||||
userList.style.transition = 'transform 0.5s ease';
|
||||
userList.style.transform = `translateY(${-userIndex * itemHeight}px)`;
|
||||
function endSwipe(e) {
|
||||
if (!dragging) return;
|
||||
dragging = false;
|
||||
const diff = getX(e) - startX;
|
||||
const limit = wrapper.offsetWidth * 0.15;
|
||||
if (diff > limit) goTo(current - 1);
|
||||
else if (diff < -limit) goTo(current + 1);
|
||||
else goTo(current);
|
||||
auto();
|
||||
}
|
||||
|
||||
// 每 3 秒滚动一次
|
||||
setInterval(rotateUsers, 3000);
|
||||
// 页面加载后立即滚动一次,保证视觉一致
|
||||
rotateUsers();
|
||||
|
||||
/* ---------- 拼单倒计时 ---------- */
|
||||
const countdownEls = document.querySelectorAll('.countdown');
|
||||
// 预处理:把初始文本转为秒数
|
||||
const countdownData = Array.from(countdownEls).map((el) => ({
|
||||
el,
|
||||
remain: parseTime(el.textContent.trim()),
|
||||
}));
|
||||
|
||||
function parseTime(t) {
|
||||
const [h = '00', m = '00', s = '00'] = t.split(':');
|
||||
return Number(h) * 3600 + Number(m) * 60 + Number(s);
|
||||
}
|
||||
function formatTime(sec) {
|
||||
const h = String(Math.floor(sec / 3600)).padStart(2, '0');
|
||||
const m = String(Math.floor((sec % 3600) / 60)).padStart(2, '0');
|
||||
const s = String(sec % 60).padStart(2, '0');
|
||||
return `${h}:${m}:${s}`;
|
||||
}
|
||||
|
||||
function updateCountdown() {
|
||||
countdownData.forEach((c) => {
|
||||
if (c.remain > 0) {
|
||||
c.remain -= 1;
|
||||
c.el.textContent = formatTime(c.remain);
|
||||
} else {
|
||||
c.el.textContent = '00:00:00';
|
||||
// 可选:到点后给整条加灰色样式,并禁用按钮
|
||||
const item = c.el.closest('.user-item');
|
||||
item?.classList.add('expired');
|
||||
item?.querySelector('.buy-btn')?.setAttribute('disabled', 'disabled');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/* --------- 动态填充“已抢 xxx 件” --------- */
|
||||
const soldBox = document.getElementById('soldBox');
|
||||
if (soldBox){
|
||||
const soldNum = Math.floor(Math.random()*101)+200; // 200~300
|
||||
soldBox.textContent = `已抢 ${soldNum} 件`;
|
||||
}
|
||||
|
||||
/* --------- 给每条拼单状态前加 “仅剩 x 人成团” --------- */
|
||||
document.querySelectorAll('.user-status').forEach(statusEl=>{
|
||||
const x = Math.floor(Math.random()*3)+1; // 1 ~ 3 随机整数
|
||||
const span = document.createElement('span');
|
||||
span.className = 'left-num';
|
||||
span.textContent = `仅剩${x}人成团,`; // 注意带逗号或空格
|
||||
statusEl.prepend(span);
|
||||
});
|
||||
|
||||
/* ============= 支付 & 登录判断 ============= */
|
||||
const modal = document.getElementById('paymentModal');
|
||||
const paymentAmount = document.getElementById('paymentAmount');
|
||||
const cancelPayment = document.getElementById('cancelPayment');
|
||||
const completePayment= document.getElementById('completePayment');
|
||||
|
||||
/* 把 3 类按钮统一选出来 */
|
||||
[...document.querySelectorAll('.buy-btn, .btn-single, .btn-group')].forEach(btn=>{
|
||||
btn.addEventListener('click',()=>{
|
||||
/* 简单读取 cookie 判断是否登录 */
|
||||
if(!getCookie('username')){
|
||||
window.location.href='login.html'; // 跳转到登录页
|
||||
return;
|
||||
}
|
||||
/* 已登录:弹出支付弹窗 */
|
||||
const price = btn.dataset.price || '0';
|
||||
paymentAmount.textContent = `支付金额 ¥${price}`;
|
||||
modal.style.display='flex';
|
||||
});
|
||||
});
|
||||
|
||||
/* 取消/完成支付 */
|
||||
cancelPayment.addEventListener('click', ()=>modal.style.display='none');
|
||||
completePayment.addEventListener('click', ()=>{
|
||||
alert('支付成功!');
|
||||
modal.style.display='none';
|
||||
});
|
||||
|
||||
/* 读取 cookie 工具函数 */
|
||||
function getCookie(name){
|
||||
return document.cookie.split(';').map(c=>c.trim())
|
||||
.find(c=>c.startsWith(name+'='))?.split('=')[1] || null;
|
||||
}
|
||||
|
||||
/* 点击遮罩空白关闭弹窗 */
|
||||
modal.addEventListener('click', e=>{
|
||||
if(e.target===modal) modal.style.display='none';
|
||||
});
|
||||
|
||||
|
||||
// 每秒刷新一次倒计时
|
||||
setInterval(updateCountdown, 1000);
|
||||
});
|
||||
|
@ -3,13 +3,13 @@
|
||||
<head>
|
||||
<meta charset="UTF-8"/>
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0,maximum-scale=1.0,user-scalable=no"/>
|
||||
<title>欢迎登录 - 小傅哥拼团</title>
|
||||
<title>欢迎登录 - 宇哥拼团</title>
|
||||
<link rel="stylesheet" href="css/login.css"/>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<form id="loginForm" class="login-form">
|
||||
<h2>欢迎登录 - 小傅哥拼团</h2>
|
||||
<h2>欢迎登录 - 宇哥拼团</h2>
|
||||
|
||||
<div class="input-group">
|
||||
<input type="text" id="username" required/>
|
||||
|
@ -2,7 +2,7 @@
|
||||
FROM openjdk:8-jre-slim
|
||||
|
||||
# 作者
|
||||
MAINTAINER xiaofuge
|
||||
MAINTAINER zy
|
||||
|
||||
# 配置
|
||||
ENV PARAMS=""
|
||||
|
@ -1,6 +1,5 @@
|
||||
|
||||
# 普通镜像构建,随系统版本构建 amd/arm
|
||||
docker build -t system/group-buying-sys-app:1.0-SNAPSHOT -f ./Dockerfile .
|
||||
docker build -t smile/group-buying-sys-app -f ./Dockerfile .
|
||||
|
||||
# 兼容 amd、arm 构建镜像
|
||||
# docker buildx build --load --platform liunx/amd64,linux/arm64 -t xiaofuge/xfg-frame-archetype-app:1.0 -f ./Dockerfile . --push
|
@ -1,7 +1,7 @@
|
||||
server:
|
||||
port: 8091
|
||||
|
||||
# 线程池配置
|
||||
# ---------- 线程池 ----------
|
||||
thread:
|
||||
pool:
|
||||
executor:
|
||||
@ -12,30 +12,45 @@ thread:
|
||||
block-queue-size: 5000
|
||||
policy: CallerRunsPolicy
|
||||
|
||||
# 数据库配置
|
||||
#spring:
|
||||
# datasource:
|
||||
# username: root
|
||||
# password: 123456
|
||||
# url: jdbc:mysql://127.0.0.1:3306/xfg_frame_archetype?useUnicode=true&characterEncoding=utf8&autoReconnect=true&zeroDateTimeBehavior=convertToNull&serverTimezone=UTC&useSSL=true
|
||||
# driver-class-name: com.mysql.cj.jdbc.Driver
|
||||
# hikari:
|
||||
# pool-name: Retail_HikariCP
|
||||
# minimum-idle: 15 #最小空闲连接数量
|
||||
# idle-timeout: 180000 #空闲连接存活最大时间,默认600000(10分钟)
|
||||
# maximum-pool-size: 25 #连接池最大连接数,默认是10
|
||||
# auto-commit: true #此属性控制从池返回的连接的默认自动提交行为,默认值:true
|
||||
# max-lifetime: 1800000 #此属性控制池中连接的最长生命周期,值0表示无限生命周期,默认1800000即30分钟
|
||||
# connection-timeout: 30000 #数据库连接超时时间,默认30秒,即30000
|
||||
# connection-test-query: SELECT 1
|
||||
# type: com.zaxxer.hikari.HikariDataSource
|
||||
# ---------- 数据源 ----------
|
||||
spring:
|
||||
datasource:
|
||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||
url: jdbc:mysql://mysql:3306/big_market?useUnicode=true&characterEncoding=utf8&autoReconnect=true&serverTimezone=Asia/Shanghai&useSSL=false
|
||||
username: root
|
||||
password: 123456
|
||||
hikari:
|
||||
pool-name: Retail_HikariCP
|
||||
minimum-idle: 15 # 最小空闲连接
|
||||
idle-timeout: 180000 # 空闲连接存活(ms)
|
||||
maximum-pool-size: 25 # 最大连接
|
||||
auto-commit: true
|
||||
max-lifetime: 1800000 # 连接最大生命周期(ms)
|
||||
connection-timeout: 30000
|
||||
connection-test-query: SELECT 1
|
||||
|
||||
#mybatis:
|
||||
# mapper-locations: classpath:/mybatis/mapper/*.xml
|
||||
# config-location: classpath:/mybatis/config/mybatis-config.xml
|
||||
# ---------- MyBatis ----------
|
||||
mybatis:
|
||||
mapper-locations: classpath:/mybatis/mapper/*.xml
|
||||
config-location: classpath:/mybatis/config/mybatis-config.xml
|
||||
|
||||
# 日志
|
||||
# ---------- Redis ----------
|
||||
redis:
|
||||
sdk:
|
||||
config:
|
||||
host: redis # 使用服务名
|
||||
port: 6379
|
||||
pool-size: 10
|
||||
min-idle-size: 5
|
||||
idle-timeout: 30000
|
||||
connect-timeout: 5000
|
||||
retry-attempts: 3
|
||||
retry-interval: 1000
|
||||
ping-interval: 60000
|
||||
keep-alive: true
|
||||
|
||||
# ---------- 日志 ----------
|
||||
logging:
|
||||
level:
|
||||
root: info
|
||||
config: classpath:logback-spring.xml
|
||||
config: classpath:logback-spring.xml
|
||||
|
@ -37,8 +37,8 @@ public class MarketIndexController implements IMarketIndexService {
|
||||
|
||||
log.info("查询拼团营销配置开始: userId={} goodsId={}", req.getUserId(), req.getGoodsId());
|
||||
|
||||
// —— 1. 参数校验 ——
|
||||
if (StringUtils.isAnyBlank(req.getUserId(), req.getSource(), req.getChannel(), req.getGoodsId())) {
|
||||
// —— 1. 参数校验 —— req.getUserId()可为空
|
||||
if (StringUtils.isAnyBlank(req.getSource(), req.getChannel(), req.getGoodsId())) {
|
||||
return Response.<GoodsMarketResponseDTO>builder()
|
||||
.code(ResponseCode.ILLEGAL_PARAMETER.getCode())
|
||||
.info(ResponseCode.ILLEGAL_PARAMETER.getInfo())
|
||||
@ -60,7 +60,7 @@ public class MarketIndexController implements IMarketIndexService {
|
||||
//获取拼团展示列表
|
||||
List<UserGroupBuyOrderDetailEntity> details = indexGroupBuyMarketService.queryInProgressUserGroupBuyOrderDetailList(
|
||||
trial.getGroupBuyActivityDiscountVO().getActivityId(),
|
||||
req.getUserId(), 1, 2);
|
||||
req.getUserId(), 1, 4);
|
||||
|
||||
List<GoodsMarketResponseDTO.Team> teams = details.stream()
|
||||
.map(d -> {
|
||||
|
Loading…
x
Reference in New Issue
Block a user