diff --git a/docs/dev-ops/mysql/sql/0716paymall.sql b/docs/dev-ops/mysql/sql/0716paymall.sql index 4edc432..85d7701 100644 --- a/docs/dev-ops/mysql/sql/0716paymall.sql +++ b/docs/dev-ops/mysql/sql/0716paymall.sql @@ -11,7 +11,7 @@ Target Server Version : 80042 File Encoding : 65001 - Date: 16/07/2025 13:59:35 + Date: 16/07/2025 17:08:44 */ SET NAMES utf8mb4; @@ -40,13 +40,16 @@ CREATE TABLE `pay_order` ( PRIMARY KEY (`id`) USING BTREE, UNIQUE INDEX `uq_order_id`(`order_id` ASC) USING BTREE, INDEX `idx_user_id_product_id`(`user_id` ASC, `product_id` ASC) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 11 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = DYNAMIC; +) ENGINE = InnoDB AUTO_INCREMENT = 35 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = DYNAMIC; -- ---------------------------- -- Records of pay_order -- ---------------------------- -INSERT INTO `pay_order` VALUES (31, 'smile01', '9890001', 'MyBatisBook', '376456387082', '2025-07-16 13:47:35', 100.00, 'DEAL_DONE', '
\n\n\n
\n', '2025-07-16 13:50:29', 1, 20.00, 80.00, '2025-07-16 13:47:35', '2025-07-16 13:58:39'); -INSERT INTO `pay_order` VALUES (32, 'smile02', '9890001', 'MyBatisBook', '503529040337', '2025-07-16 13:55:51', 100.00, 'DEAL_DONE', '
\n\n\n
\n', '2025-07-16 13:56:47', 1, 20.00, 80.00, '2025-07-16 13:55:50', '2025-07-16 13:58:39'); -INSERT INTO `pay_order` VALUES (33, 'smile03', '9890001', 'MyBatisBook', '274640446882', '2025-07-16 13:57:47', 100.00, 'DEAL_DONE', '
\n\n\n
\n', '2025-07-16 13:58:39', 1, 20.00, 80.00, '2025-07-16 13:57:47', '2025-07-16 13:58:40'); +INSERT INTO `pay_order` VALUES (36, 'opEtBvq6go0co-HQC5DSHkKfkdds', '9890001', 'MyBatisBook', '498873906342', '2025-07-16 15:53:11', 100.00, 'DEAL_DONE', '
\n\n\n
\n', '2025-07-16 15:54:03', 1, 20.00, 80.00, '2025-07-16 15:53:11', '2025-07-16 16:44:47'); +INSERT INTO `pay_order` VALUES (39, 'smile01', '9890001', 'MyBatisBook', '369511405849', '2025-07-16 16:28:58', 100.00, 'PAY_SUCCESS', '
\n\n\n
\n', '2025-07-16 16:30:33', 1, 20.00, 80.00, '2025-07-16 16:28:57', '2025-07-16 16:30:33'); +INSERT INTO `pay_order` VALUES (40, 'smile01', '9890001', 'MyBatisBook', '698481154046', '2025-07-16 16:34:25', 100.00, 'DEAL_DONE', '
\n\n\n
\n', '2025-07-16 16:35:36', 0, 0.00, 100.00, '2025-07-16 16:34:24', '2025-07-16 16:35:36'); +INSERT INTO `pay_order` VALUES (41, 'smile02', '9890001', 'MyBatisBook', '541129857040', '2025-07-16 16:36:49', 100.00, 'DEAL_DONE', '
\n\n\n
\n', '2025-07-16 16:38:05', 1, 20.00, 80.00, '2025-07-16 16:36:49', '2025-07-16 16:44:47'); +INSERT INTO `pay_order` VALUES (42, 'smile04', '9890001', 'MyBatisBook', '381698719384', '2025-07-16 16:43:47', 100.00, 'DEAL_DONE', '
\n\n\n
\n', '2025-07-16 16:44:47', 1, 20.00, 80.00, '2025-07-16 16:43:46', '2025-07-16 16:44:47'); +INSERT INTO `pay_order` VALUES (45, 'smile04', '9890001', 'MyBatisBook', '380924838419', '2025-07-16 16:54:06', 100.00, 'DEAL_DONE', '
\n\n\n
\n', '2025-07-16 16:55:14', 0, 0.00, 100.00, '2025-07-16 16:54:05', '2025-07-16 16:55:14'); SET FOREIGN_KEY_CHECKS = 1; diff --git a/docs/dev-ops/nginx/html/css/index.css b/docs/dev-ops/nginx/html/css/index.css new file mode 100644 index 0000000..7ec93be --- /dev/null +++ b/docs/dev-ops/nginx/html/css/index.css @@ -0,0 +1,147 @@ +/* ========== 全局 ========== */ +*{ + margin:0;padding:0;box-sizing:border-box; + font-family:'PingFang SC','Helvetica Neue',Arial,sans-serif; +} +body{ + background:#f5f5f5; + color:#333; + max-width:500px; + margin:0 auto; + position:relative; + padding-bottom:30px; +} + +/* ========== 轮播图 ========== */ +.swiper-container{ + width:100%;height:375px;position:relative;overflow:hidden; +} +.swiper-wrapper{display:flex;transition:transform .3s;} +.swiper-slide{flex:0 0 100%;height:375px;} +.swiper-slide img{width:100%;height:100%;object-fit:contain;background:#fff;} + +.swiper-pagination{ + position:absolute;bottom:10px;left:50%; + transform:translateX(-50%);display:flex;gap:6px; +} +.swiper-dot{ + width:8px;height:8px;border-radius:50%; + background:rgba(255,255,255,.5);transition:all .3s; +} +.swiper-dot.active{background:#ff5000;width:16px;border-radius:4px;} + +/* ========== 商品信息 ========== */ +.product-info{background:#fff;padding:15px;margin-bottom:10px;} +.price-row{display:flex;align-items:center;margin-bottom:12px;} + +.current-price{color:#ff5000;font-size:28px;font-weight:bold;} +.current-price::before{content:"¥";font-size:18px;} + +.original-price{ + color:#999;font-size:16px;text-decoration:line-through;margin-left:8px; +} +.original-price::before{content:"¥";} +.title{font-size:18px;font-weight:bold;line-height:1.4;margin-bottom:10px;} + +/* 促销行 */ +.promo-row{display:flex;align-items:center;gap:6px;margin-top:6px;} +.promo-tag{ + flex-shrink:0; + display:inline-block; + background:linear-gradient(90deg,#ff2c2c,#ff6b22); + color:#fff;font-size:12px;padding:2px 6px;border-radius:2px; +} +.promo-box{ + display:inline-block;font-size:13px;padding:2px 6px; + border-radius:4px;font-weight:600;line-height:1.2;white-space:nowrap; +} +.promo-box.drop, +.promo-box.sold{ + background:linear-gradient(90deg,#ff7e00,#ff5000); + color:#fff; +} + +/* ========== 拼单列表 ========== */ +.group-buying{background:#fff;padding:15px;margin-bottom:10px;position:relative;overflow:hidden;} +.section-title{ + font-size:16px;font-weight:bold;margin-bottom:12px;position:relative;padding-left:10px; +} +.section-title::before{ + content:"";position:absolute;left:0;top:50%;transform:translateY(-50%); + width:3px;height:16px;background:#ff5000;border-radius:2px; +} + +.group-users{height:120px;position:relative;overflow:hidden;} +.user-list{position:absolute;top:0;left:0;width:100%;transition:transform .5s ease;} + +.user-item{ + display:flex;align-items:center;padding:8px 0;border-bottom:1px solid #f5f5f5; +} +.user-item:last-child{border-bottom:none;} + +.user-avatar{ + width:40px;height:40px;border-radius:50%;background:#f5f5f5; + display:flex;align-items:center;justify-content:center;margin-right:10px; + color:#999;font-size:20px; +} + +.user-info{flex:1;} +.user-name{font-size:15px;font-weight:bold;margin-bottom:4px;} +.user-status{font-size:13px;color:#666;} + +.countdown{ + display:inline-block;background:#ff5000;color:#fff; + padding:1px 4px;border-radius:2px;margin-left:5px; +} + +.buy-btn{ + background:linear-gradient(90deg,#ff2c2c,#ff6b22); + color:#fff;border:none;border-radius:4px;padding:6px 15px; + font-size:14px;font-weight:bold;cursor:pointer; +} + +/* ========== 底部操作栏 ========== */ +.action-bar{ + position:fixed;inset-inline:0;bottom:0;max-width:500px;margin:0 auto; + background:#fff;display:flex;height:60px; + box-shadow:0 -2px 10px rgba(0,0,0,.1);z-index:100; +} +.action-btn{ + flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center; + font-size:12px;color:#666; +} +.action-btn i{font-size:20px;margin-bottom:4px;} + +.purchase-btn{flex:2;display:flex;} +.btn-single, +.btn-group{ + flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center; + gap:2px;border:none;cursor:pointer; +} +.btn-single{background:#ff9500;color:#fff;} +.btn-group {background:#ff5000;color:#fff;} + +.btn-price{font-size:18px;font-weight:700;line-height:1;} +.btn-label{font-size:12px;line-height:1;} + +/* ========== 支付弹窗 ========== */ +.payment-overlay{ + position:fixed;inset:0;z-index:9999; + background:rgba(0,0,0,.55);display:flex;align-items:center;justify-content:center; + backdrop-filter:blur(2px); +} +.payment-modal{ + width:300px;max-width:90vw;padding:26px 28px 30px;background:#fff; + border-radius:12px;box-shadow:0 12px 30px rgba(0,0,0,.18); + font-size:14px;line-height:1.45;text-align:center; +} +.payment-modal h3{font-size:18px;margin:0 0 14px;color:#333;} +.payment-modal p{margin:6px 0;color:#555;word-break:break-all;} +.payment-modal .copyable{color:#ff5000;cursor:pointer;text-decoration:underline;} + +.modal-buttons{margin-top:22px;display:flex;gap:12px;} +.modal-buttons button{ + flex:1;padding:8px 0;border-radius:6px;font-size:14px;cursor:pointer;border:none; +} +.confirm-btn{background:#ff5000;color:#fff;} +.cancel-btn{background:#f2f3f5;color:#333;} diff --git a/docs/dev-ops/nginx/html/css/login.css b/docs/dev-ops/nginx/html/css/login.css new file mode 100644 index 0000000..6a08fb1 --- /dev/null +++ b/docs/dev-ops/nginx/html/css/login.css @@ -0,0 +1,94 @@ +/* ==================== Reset & 基础 ==================== */ +* { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +body { + font-family: "Helvetica Neue", Arial, sans-serif; + background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%); + display: flex; + justify-content: center; + align-items: center; + height: 100vh; +} + +/* ==================== 登录卡片 ==================== */ +.login-container { + background-color: #fff; + padding: 40px; + border-radius: 20px; + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1); + text-align: center; + max-width: 400px; + width: 90%; + transition: all 0.3s ease; +} + +.login-container:hover { + transform: translateY(-5px); + box-shadow: 0 15px 35px rgba(0, 0, 0, 0.15); +} + +.login-container h1 { + margin-bottom: 30px; + color: #333; + font-size: 28px; + font-weight: 600; +} + +/* ==================== Logo ==================== */ +.logo { + width: 80px; + height: 80px; + margin-bottom: 20px; +} + +/* ==================== 二维码 ==================== */ +.qr-code { + margin: 30px 0; + position: relative; +} + +.qr-code img { + width: 200px; + height: 200px; + border-radius: 10px; + transition: all 0.3s ease; +} + +.qr-code::before { + content: ""; + position: absolute; + inset: -5px; + background: linear-gradient(45deg, #12c2e9, #c471ed, #f64f59); + z-index: -1; + filter: blur(20px); + border-radius: 15px; + opacity: 0; + transition: opacity 0.3s ease; +} + +.qr-code:hover::before { + opacity: 1; +} + +/* ==================== 说明文字 ==================== */ +.instructions { + color: #666; + font-size: 16px; + margin-top: 20px; + line-height: 1.5; +} + +/* ==================== 动画 ==================== */ +@keyframes pulse { + 0% { transform: scale(1); } + 50% { transform: scale(1.05); } + 100% { transform: scale(1); } +} + +.pulse { + animation: pulse 2s infinite; +} diff --git a/docs/dev-ops/nginx/html/form.html b/docs/dev-ops/nginx/html/form.html deleted file mode 100644 index 71fe6ea..0000000 --- a/docs/dev-ops/nginx/html/form.html +++ /dev/null @@ -1,5 +0,0 @@ -
- - -
- \ No newline at end of file diff --git a/docs/dev-ops/nginx/html/images/goods_info1.png b/docs/dev-ops/nginx/html/images/goods_info1.png new file mode 100644 index 0000000..4e80f6a Binary files /dev/null and b/docs/dev-ops/nginx/html/images/goods_info1.png differ diff --git a/docs/dev-ops/nginx/html/images/goods_info2.png b/docs/dev-ops/nginx/html/images/goods_info2.png new file mode 100644 index 0000000..56fe32d Binary files /dev/null and b/docs/dev-ops/nginx/html/images/goods_info2.png differ diff --git a/docs/dev-ops/nginx/html/images/goods_info3.png b/docs/dev-ops/nginx/html/images/goods_info3.png new file mode 100644 index 0000000..57429a9 Binary files /dev/null and b/docs/dev-ops/nginx/html/images/goods_info3.png differ diff --git a/docs/dev-ops/nginx/html/images/keyboard-001.jpg b/docs/dev-ops/nginx/html/images/keyboard-001.jpg deleted file mode 100644 index 7bc43e1..0000000 Binary files a/docs/dev-ops/nginx/html/images/keyboard-001.jpg and /dev/null differ diff --git a/docs/dev-ops/nginx/html/images/logo.png b/docs/dev-ops/nginx/html/images/logo.png new file mode 100644 index 0000000..e556332 Binary files /dev/null and b/docs/dev-ops/nginx/html/images/logo.png differ diff --git a/docs/dev-ops/nginx/html/images/qrcode.png b/docs/dev-ops/nginx/html/images/qrcode.png new file mode 100644 index 0000000..79f2351 Binary files /dev/null and b/docs/dev-ops/nginx/html/images/qrcode.png differ diff --git a/docs/dev-ops/nginx/html/index.html b/docs/dev-ops/nginx/html/index.html index d941634..4413608 100644 --- a/docs/dev-ops/nginx/html/index.html +++ b/docs/dev-ops/nginx/html/index.html @@ -1,123 +1,92 @@ - + - - - 商品下单支付页 - - - -
-
-

程序员 - 同款机械键盘

+ + + 手写 MyBatis:渐进式源码实践 - 拼多多 - -

价格:¥1.68

+ + + + + + + +
+
+
+
+
- - +
- +
+
+
+
+ + +
+
首页
+
收藏
+
购物车
+ +
+ + +
+
+ + + + + + - diff --git a/docs/dev-ops/nginx/html/js/index.js b/docs/dev-ops/nginx/html/js/index.js new file mode 100644 index 0000000..691df20 --- /dev/null +++ b/docs/dev-ops/nginx/html/js/index.js @@ -0,0 +1,347 @@ + +document.addEventListener('DOMContentLoaded', () => { + + /* ========== 通用工具 ========== */ + const getCookie = k => + document.cookie + .split(';') + .map(c => c.trim()) + .find(c => c.startsWith(k + '='))?.split('=')[1] || null; + + const $ = id => document.getElementById(id); + + /* ----------- 0. DOM 快捷引用 ----------- */ + const currentPrice = $('currentPrice'); + const originalPrice = $('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 CFG_API = 'http://127.0.0.1:8091/api/v1/gbm/index/query_group_buy_market_config'; + + // 登录检查 + const loginToken = getCookie('loginToken'); + if (!loginToken) { location.href = 'login.html'; return; } + const username = loginToken; + + // 保存活动 id(开团 / 参团时要用) + let activityId = null; + + const POST_BODY = { + userId : username, + source : 's01', + channel: 'c01', + goodsId: '9890001' + }; + + fetch(CFG_API, { + 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; } + + activityId = data.activityId; + renderGoods(data.goods); + renderStatistic(data.teamStatistic); + renderTeams(data.teamList, data.goods?.payPrice); + }) + .catch(console.error); + + /* ------------- 渲染商品信息 ------------- */ + function renderGoods(g = {}) { + const { originalPrice: op = 0, payPrice = 0, deductionPrice = 0 } = g; + currentPrice.textContent = payPrice; + originalPrice.textContent = op; + dropPrice.textContent = `直降 ¥${deductionPrice}`; + + singlePriceSpan.textContent = `¥${op}`; + groupPriceSpan.textContent = `¥${payPrice}`; + btnSingle.dataset.price = op; + btnGroup.dataset.price = payPrice; + } + + /* ------------- 渲染统计信息 ------------- */ + function renderStatistic(stat = {}) { + const { allTeamUserCount = 0 } = stat; + groupTitle.textContent = `${allTeamUserCount}人在抢,参与可立即拼成`; + soldBox.textContent = `${allTeamUserCount}人再抢`; + } + + /* ------------- 渲染拼团列表 ------------- */ + function renderTeams(list = [], groupPrice = 0) { + if (!list?.length) { + groupTitle.textContent = '小伙伴,赶紧去开团吧,做村里最靓的仔。'; + return; + } + userList.innerHTML = ''; + list.forEach(t => userList.appendChild(makeItem(t, groupPrice))); + initUserMarquee(); + initCountdown(); + } + + // 把 teamId / activityId 写到 user-info 的 dataset 上,便于点击时读取 + function makeItem(team, price) { + const { userId, targetCount, lockCount, + validTimeCountdown, teamId, activityId: tActId } = 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 = ` +
+
+
${userId}
+
+ 仅剩${leftNum}人成团 + ${timeText} +
+
+ + `; + 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) 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 PAY_MALL_URL = 'http://127.0.0.1:8092'; + const CREATE_PAY_API = `${PAY_MALL_URL}/api/v1/alipay/create_pay_order`; + + // 4.1 支付确认弹窗 + function showPaymentConfirm(price){ + if(document.querySelector('.payment-overlay')) return; + + const tpl = document.getElementById('tpl-payment'); + const overlay = tpl.content.firstElementChild.cloneNode(true); + + overlay.querySelector('#priceText').textContent = `¥${price}`; + overlay.querySelector('.copyable').onclick = function(){ + navigator.clipboard.writeText(this.dataset.copy) + .then(()=>alert('买家账号已复制到剪贴板')); + }; + overlay.querySelector('.cancel-btn').onclick = ()=>{ + document.querySelectorAll('form').forEach(f=>f.remove()); + overlay.remove(); + }; + overlay.querySelector('.confirm-btn').onclick = ()=>{ + document.querySelector('form')?.submit(); + overlay.remove(); + }; + overlay.addEventListener('click',e=>{ + if(e.target===overlay) overlay.querySelector('.cancel-btn').click(); + }); + + document.body.appendChild(overlay); + } + + /* ---------- 4.2 单独购买 ---------- */ + btnSingle.addEventListener('click', () => { + if (!getCookie('loginToken')) { location.href = 'login.html'; return; } + + fetch(CREATE_PAY_API, { + method : 'POST', + headers: { 'Content-Type': 'application/json' }, + body : JSON.stringify({ + userId : username, + productId : POST_BODY.goodsId, + marketType: 0 + }) + }) + .then(r=>r.json()) + .then(json=>{ + if (json.code!=='0000') return alert(json.info||'下单失败'); + document.querySelectorAll('form').forEach(f=>f.remove()); + document.body.insertAdjacentHTML('beforeend', json.data); + showPaymentConfirm(btnSingle.dataset.price); + }) + .catch(console.error); + }); + + /* ---------- 4.3 开团购买 ---------- */ + btnGroup.addEventListener('click', () => { + if (!getCookie('loginToken')) { location.href = 'login.html'; return; } + + fetch(CREATE_PAY_API, { + method : 'POST', + headers: { 'Content-Type': 'application/json' }, + body : JSON.stringify({ + userId : username, + productId : POST_BODY.goodsId, + marketType: 1, + activityId: activityId + }) + }) + .then(r=>r.json()) + .then(json=>{ + if (json.code!=='0000') return alert(json.info||'下单失败'); + document.querySelectorAll('form').forEach(f=>f.remove()); + document.body.insertAdjacentHTML('beforeend', json.data); + showPaymentConfirm(btnGroup.dataset.price); + }) + .catch(console.error); + }); + + /* ---------- 4.4 参与拼团( buy-btn )---------- */ + document.body.addEventListener('click', e => { + const joinBtn = e.target.closest('.buy-btn'); + if (!joinBtn) return; + + if (!getCookie('loginToken')) { location.href = 'login.html'; return; } + + // 获取 teamId、activityId + const userInfo = joinBtn.closest('.user-item')?.querySelector('.user-info'); + const teamId = userInfo?.dataset.teamid; + const actId = userInfo?.dataset.activityid || activityId; + + if (!teamId) { return alert('拼团信息已失效,请刷新页面'); } + + fetch(CREATE_PAY_API, { + method : 'POST', + headers: { 'Content-Type': 'application/json' }, + body : JSON.stringify({ + userId : username, + productId : POST_BODY.goodsId, + teamId : teamId, + activityId: actId, + marketType: 1 // 参团 + }) + }) + .then(r=>r.json()) + .then(json=>{ + if (json.code!=='0000') return alert(json.info||'参团失败'); + document.querySelectorAll('form').forEach(f=>f.remove()); + document.body.insertAdjacentHTML('beforeend', json.data); + showPaymentConfirm(joinBtn.dataset.price); + }) + .catch(console.error); + }); + + /* ===================================================== + * 5. 顶部横向轮播(原逻辑保留) + * =================================================== */ + const wrapper = document.querySelector('.swiper-wrapper'); + const slides = [...wrapper.children]; + const pagination = document.querySelector('.swiper-pagination'); + const count = slides.length; + + 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.onclick = () => goTo(i); + pagination.appendChild(dot); + } + const dots = pagination.children; + + const goTo = i => { + current = (i+count)%count; + wrapper.style.transition = 'transform .3s ease'; + wrapper.style.transform = `translateX(-${current*100}%)`; + [...dots].forEach((d,j)=>d.classList.toggle('active',j===current)); + }; + 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); + + 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(); + } +}); diff --git a/docs/dev-ops/nginx/html/js/login.js b/docs/dev-ops/nginx/html/js/login.js new file mode 100644 index 0000000..501f585 --- /dev/null +++ b/docs/dev-ops/nginx/html/js/login.js @@ -0,0 +1,52 @@ +/* -------------------- 配置 -------------------- */ +const sPayMallUrl = "http://127.0.0.1:8092"; + +/* -------------------- 工具函数 -------------------- */ +function setCookie(name, value, days) { + const date = new Date(); + date.setTime(date.getTime() + days * 24 * 60 * 60 * 1000); + document.cookie = `${name}=${value};expires=${date.toUTCString()};path=/`; +} + +/* -------------------- 主逻辑 -------------------- */ +document.addEventListener("DOMContentLoaded", () => { + // 1) 先拿二维码 ticket + fetch(`${sPayMallUrl}/api/v1/login/weixin_qrcode_ticket`) + .then(res => res.json()) + .then(data => { + if (data.code !== "0000") { + console.error("获取二维码 ticket 失败:", data.info); + return; + } + + const ticket = data.data; + const qrCodeImg = document.getElementById("qr-code-img"); + qrCodeImg.src = `https://mp.weixin.qq.com/cgi-bin/showqrcode?ticket=${ticket}`; + qrCodeImg.classList.remove("pulse"); + + // 2) 轮询确认登录 + const intervalId = setInterval(() => checkLoginStatus(ticket, intervalId), 3000); + }) + .catch(err => console.error("请求失败:", err)); +}); + +/* -------------------- 轮询检查登录 -------------------- */ +function checkLoginStatus(ticket, intervalId) { + fetch(`${sPayMallUrl}/api/v1/login/check_login?ticket=${ticket}`) + .then(res => res.json()) + .then(data => { + if (data.code === "0000") { + // 登录成功,停轮询 + clearInterval(intervalId); + + // 把 token 写入 cookie(30 天) + setCookie("loginToken", data.data, 30); + + // 跳转首页 + window.location.href = "./index.html"; + } else { + console.info("login wait"); + } + }) + .catch(err => console.error("请求失败:", err)); +} diff --git a/docs/dev-ops/nginx/html/login.html b/docs/dev-ops/nginx/html/login.html index a393a3f..488b2c1 100644 --- a/docs/dev-ops/nginx/html/login.html +++ b/docs/dev-ops/nginx/html/login.html @@ -1,105 +1,30 @@ - - - S Pay Mall 商城登录页 - + + + S Pay Mall 商城登录 + + +
-

S Pay Mall 商城登录页

+ +

欢迎登录 - 拼团团

+
- 微信二维码 + + 微信二维码
-

请使用微信扫描二维码登录

+ +

+ 请使用微信扫描二维码登录
+ 扫码后自动登录商城 +

- + + diff --git a/pay-mall-domain/src/main/java/edu/whut/domain/order/service/AbstractOrderService.java b/pay-mall-domain/src/main/java/edu/whut/domain/order/service/AbstractOrderService.java index 0d38bad..b33d0f9 100644 --- a/pay-mall-domain/src/main/java/edu/whut/domain/order/service/AbstractOrderService.java +++ b/pay-mall-domain/src/main/java/edu/whut/domain/order/service/AbstractOrderService.java @@ -27,11 +27,12 @@ public abstract class AbstractOrderService implements IOrderService { */ @Override public PayOrderEntity createOrder(ShopCartEntity shopCartEntity) throws Exception { - // 1. 查询当前用户是否存在掉单和未支付订单 + // 1. 查询当前用户是否存在掉单或未支付订单 OrderEntity unpaidOrderEntity = repository.queryUnPayOrder(shopCartEntity); - // 如果已有未支付订单且状态为支付等待,则直接复用 - if (unpaidOrderEntity != null && OrderStatusVO.PAY_WAIT.equals(unpaidOrderEntity.getOrderStatusVO())) { + // 如果已有未支付订单且状态为“等待支付”,直接复用该订单 + if (unpaidOrderEntity != null + && OrderStatusVO.PAY_WAIT.equals(unpaidOrderEntity.getOrderStatusVO())) { log.info("创建订单-存在,已存在未支付订单。userId:{} productId:{} orderId:{}", shopCartEntity.getUserId(), shopCartEntity.getProductId(), unpaidOrderEntity.getOrderId()); return PayOrderEntity.builder() @@ -39,41 +40,64 @@ public abstract class AbstractOrderService implements IOrderService { .payUrl(unpaidOrderEntity.getPayUrl()) .build(); - // 如果已有订单仅创建了记录但未生成支付单,则生成支付单 - } else if (unpaidOrderEntity != null && OrderStatusVO.CREATE.equals(unpaidOrderEntity.getOrderStatusVO())) { + // 如果已有订单仅创建了记录但未生成支付单,则进入支付单生成流程 + } else if (unpaidOrderEntity != null + && OrderStatusVO.CREATE.equals(unpaidOrderEntity.getOrderStatusVO())) { log.info("创建订单-存在,存在未创建支付单订单,创建支付单开始 userId:{} productId:{} orderId:{}", shopCartEntity.getUserId(), shopCartEntity.getProductId(), unpaidOrderEntity.getOrderId()); + Integer marketType = unpaidOrderEntity.getMarketType(); BigDecimal marketDeductionAmount = unpaidOrderEntity.getMarketDeductionAmount(); PayOrderEntity payOrderEntity; - if (MarketTypeVO.GROUP_BUY_MARKET.getCode().equals(marketType) && marketDeductionAmount == null) { + // === marketType 分支处理 === + if (MarketTypeVO.GROUP_BUY_MARKET.getCode().equals(marketType) + && marketDeductionAmount == null) { + // 1) 是拼团类型,且还未计算优惠(deductionAmount == null)时 + // 先锁定优惠(如拼团满减),获取 discount 对象 MarketPayDiscountEntity discount = lockMarketPayOrder( - shopCartEntity.getUserId(), shopCartEntity.getTeamId(), - shopCartEntity.getActivityId(), shopCartEntity.getProductId(), + shopCartEntity.getUserId(), + shopCartEntity.getTeamId(), + shopCartEntity.getActivityId(), + shopCartEntity.getProductId(), unpaidOrderEntity.getOrderId()); + // 2) 再发起预支付,传入优惠后金额 payOrderEntity = doPrepayOrder( - shopCartEntity.getUserId(), shopCartEntity.getProductId(), - unpaidOrderEntity.getProductName(), unpaidOrderEntity.getOrderId(), - unpaidOrderEntity.getTotalAmount(), discount); + shopCartEntity.getUserId(), + shopCartEntity.getProductId(), + unpaidOrderEntity.getProductName(), + unpaidOrderEntity.getOrderId(), + unpaidOrderEntity.getTotalAmount(), + discount); + } else if (MarketTypeVO.GROUP_BUY_MARKET.getCode().equals(marketType)) { + // 是拼团类型,但已经计算过优惠,直接使用 payAmount(已扣优惠) payOrderEntity = doPrepayOrder( - shopCartEntity.getUserId(), shopCartEntity.getProductId(), - unpaidOrderEntity.getProductName(), unpaidOrderEntity.getOrderId(), + shopCartEntity.getUserId(), + shopCartEntity.getProductId(), + unpaidOrderEntity.getProductName(), + unpaidOrderEntity.getOrderId(), unpaidOrderEntity.getPayAmount()); + } else { + // 其他类型(如单独购买或普通下单),按总价全额下单 payOrderEntity = doPrepayOrder( - shopCartEntity.getUserId(), shopCartEntity.getProductId(), - unpaidOrderEntity.getProductName(), unpaidOrderEntity.getOrderId(), + shopCartEntity.getUserId(), + shopCartEntity.getProductId(), + unpaidOrderEntity.getProductName(), + unpaidOrderEntity.getOrderId(), unpaidOrderEntity.getTotalAmount()); } + // 返回已生成支付单的订单信息 return PayOrderEntity.builder() .orderId(payOrderEntity.getOrderId()) .payUrl(payOrderEntity.getPayUrl()) .build(); } + // ———— 以下为“全新下单”流程 ———— + // 2. 查询商品信息 ProductEntity productEntity = port.queryProductByProductId(shopCartEntity.getProductId()); @@ -89,16 +113,18 @@ public abstract class AbstractOrderService implements IOrderService { .build(); this.doSaveOrder(orderAggregate); - // 4. 如果是拼团,发起营销锁单 + // 4. 如果是拼团(marketType == GROUP_BUY_MARKET),先锁定营销优惠 MarketPayDiscountEntity marketPayDiscountEntity = null; if (MarketTypeVO.GROUP_BUY_MARKET.equals(shopCartEntity.getMarketTypeVO())) { - marketPayDiscountEntity = this.lockMarketPayOrder( - shopCartEntity.getUserId(), shopCartEntity.getTeamId(), - shopCartEntity.getActivityId(), shopCartEntity.getProductId(), + marketPayDiscountEntity = this.lockMarketPayOrder( //调用拼团交易系统的锁单逻辑 + shopCartEntity.getUserId(), + shopCartEntity.getTeamId(), + shopCartEntity.getActivityId(), + shopCartEntity.getProductId(), orderEntity.getOrderId()); } - // 5. 创建支付订单 + // 5. 创建支付订单(预支付),如果有拼团优惠则传入 discount,否则按原价下单 PayOrderEntity payOrderEntity = doPrepayOrder( shopCartEntity.getUserId(), productEntity.getProductId(), @@ -110,12 +136,14 @@ public abstract class AbstractOrderService implements IOrderService { log.info("创建订单-完成,生成支付单。userId: {} orderId: {} payUrl: {}", shopCartEntity.getUserId(), orderEntity.getOrderId(), payOrderEntity.getPayUrl()); + // 返回新订单的支付链接 return PayOrderEntity.builder() .orderId(orderEntity.getOrderId()) .payUrl(payOrderEntity.getPayUrl()) .build(); } + /** * 保存订单 */ diff --git a/pay-mall-domain/src/main/java/edu/whut/domain/order/service/OrderService.java b/pay-mall-domain/src/main/java/edu/whut/domain/order/service/OrderService.java index 0bf78a1..bf217a8 100644 --- a/pay-mall-domain/src/main/java/edu/whut/domain/order/service/OrderService.java +++ b/pay-mall-domain/src/main/java/edu/whut/domain/order/service/OrderService.java @@ -56,7 +56,7 @@ public class OrderService extends AbstractOrderService{ @Override protected PayOrderEntity doPrepayOrder(String userId, String productId, String productName, String orderId, BigDecimal totalAmount, MarketPayDiscountEntity marketPayDiscountEntity) throws AlipayApiException { - // 支付金额 + // 支付金额,如果走拼团流程就取PayPrice,否则就是totalAmount BigDecimal payAmount = null == marketPayDiscountEntity ? totalAmount : marketPayDiscountEntity.getPayPrice(); AlipayTradePagePayRequest request = new AlipayTradePagePayRequest(); diff --git a/pay-mall-trigger/src/main/java/edu/whut/trigger/http/AliPayController.java b/pay-mall-trigger/src/main/java/edu/whut/trigger/http/AliPayController.java index 594178b..10cdb6c 100644 --- a/pay-mall-trigger/src/main/java/edu/whut/trigger/http/AliPayController.java +++ b/pay-mall-trigger/src/main/java/edu/whut/trigger/http/AliPayController.java @@ -54,6 +54,7 @@ public class AliPayController implements IPayService { .productId(productId) .teamId(teamId) .marketTypeVO(MarketTypeVO.valueOf(marketType)) + .activityId(createPayRequestDTO.getActivityId()) .build()); log.info("商品下单,根据商品ID创建支付单完成 userId:{} productId:{} orderId:{}", userId, productId, payOrderEntity.getOrderId()); diff --git a/pay-mall-trigger/src/main/java/edu/whut/trigger/job/NoPayNotifyOrderJob.java b/pay-mall-trigger/src/main/java/edu/whut/trigger/job/NoPayNotifyOrderJob.java index 4d60eca..0970ebb 100644 --- a/pay-mall-trigger/src/main/java/edu/whut/trigger/job/NoPayNotifyOrderJob.java +++ b/pay-mall-trigger/src/main/java/edu/whut/trigger/job/NoPayNotifyOrderJob.java @@ -25,12 +25,13 @@ public class NoPayNotifyOrderJob { private final AlipayClient alipayClient; /** - * 每 3 秒执行一次,扫描超过1分钟未收到回调的待支付订单 + * 每 10 秒执行一次,扫描超过1分钟未收到回调的待支付订单 */ - @Scheduled(cron = "0/3 * * * * ?") + @Scheduled(cron = "0/10 * * * * ?") public void exec() { try { log.info("任务:检测未接收到或未正确处理的支付回调通知"); + //查找所有STATUS为PAY_WAIT的订单 List orderIds = orderService.queryNoPayNotifyOrder(); if (null == orderIds || orderIds.isEmpty()) return; diff --git a/pay-mall-trigger/src/main/java/edu/whut/trigger/job/TimeoutCloseOrderJob.java b/pay-mall-trigger/src/main/java/edu/whut/trigger/job/TimeoutCloseOrderJob.java index 6a59e63..839f01c 100644 --- a/pay-mall-trigger/src/main/java/edu/whut/trigger/job/TimeoutCloseOrderJob.java +++ b/pay-mall-trigger/src/main/java/edu/whut/trigger/job/TimeoutCloseOrderJob.java @@ -18,9 +18,9 @@ public class TimeoutCloseOrderJob { private IOrderService orderService; /** - * 每10分钟执行一次,扫描超时未支付订单 + * 每 15 分钟执行一次,扫描超时未支付订单 */ - @Scheduled(cron = "0 0/30 * * * ?") + @Scheduled(cron = "0 0/15 * * * ?") public void exec() { try { log.info("任务;超时30分钟订单关闭"); @@ -31,11 +31,12 @@ public class TimeoutCloseOrderJob { } // 遍历订单,逐一关闭并记录结果 for (String orderId : orderIds) { + // 设置 STATUS为CLOSE boolean status = orderService.changeOrderClose(orderId); - log.info("定时任务,超时30分钟订单关闭 orderId: {} status:{}", orderId, status); + log.info("定时任务,超时15分钟订单关闭 orderId: {} status:{}", orderId, status); } } catch (Exception e) { - log.error("定时任务,超时30分钟订单关闭失败", e); + log.error("定时任务,超时15分钟订单关闭失败", e); } } diff --git a/pay-mall-types/src/main/java/edu/whut/types/weixin/MessageTextEntity.java b/pay-mall-types/src/main/java/edu/whut/types/weixin/MessageTextEntity.java index 75d9a44..3b31f6e 100644 --- a/pay-mall-types/src/main/java/edu/whut/types/weixin/MessageTextEntity.java +++ b/pay-mall-types/src/main/java/edu/whut/types/weixin/MessageTextEntity.java @@ -26,6 +26,9 @@ public class MessageTextEntity { @XStreamAlias("MsgId") private String msgId; + @XStreamAlias("MsgID") + private String msgID; + @XStreamAlias("Status") private String status; @@ -84,6 +87,14 @@ public class MessageTextEntity { this.msgId = msgId; } + public String getMsgID() { + return msgID; + } + + public void setMsgID(String msgID) { + this.msgID = msgID; + } + public String getStatus() { return status; }