7.8 调整前端数据请求,动态加载后端数据;构建镜像部署云服务器

This commit is contained in:
zhangsan 2025-07-08 15:44:31 +08:00
parent 6ebd80a6c8
commit 88f74950bc
11 changed files with 471 additions and 334 deletions

View File

@ -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

View File

@ -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" ]

View File

@ -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
View 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

View File

@ -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">直降&nbsp;¥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>

View File

@ -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);
});

View File

@ -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/>

View File

@ -2,7 +2,7 @@
FROM openjdk:8-jre-slim
# 作者
MAINTAINER xiaofuge
MAINTAINER zy
# 配置
ENV PARAMS=""

View File

@ -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

View File

@ -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 #空闲连接存活最大时间默认60000010分钟
# 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

View File

@ -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 -> {