7.17 尝试部署-1

This commit is contained in:
zhangsan 2025-07-17 18:30:14 +08:00
parent 3df396437b
commit 1b1729d7f8
10 changed files with 252 additions and 85 deletions

View File

@ -92,3 +92,27 @@ body {
.pulse {
animation: pulse 2s infinite;
}
/* ==================== 无痕登录按钮 ==================== */
.stealth-login-btn {
margin-top: 25px;
padding: 12px 24px;
background-color: #007bff; /* 蓝底 */
color: #fff; /* 白字 */
border: none;
border-radius: 8px;
font-size: 16px;
font-weight: 500;
cursor: pointer;
box-shadow: 0 4px 12px rgba(0, 123, 255, 0.25);
transition: background-color 0.25s ease, transform 0.25s ease;
}
.stealth-login-btn:hover {
background-color: #0056b3; /* 深一点的蓝 */
transform: translateY(-2px);
}
.stealth-login-btn:active {
transform: translateY(0);
}

View File

@ -90,28 +90,47 @@ document.addEventListener('DOMContentLoaded', () => {
initCountdown();
}
function obfuscateUserId(userId) {
if (userId.length < 4) {
if (userId.length <= 1) {
return userId;
}
const first = userId.charAt(0);
const last = userId.charAt(userId.length - 1);
const middle = '*'.repeat(userId.length - 2);
return first + middle + last;
} else {
const start = userId.slice(0, 2);
const end = userId.slice(-1);
const middle = '*'.repeat(userId.length - 4);
return start + middle + end;
}
}
// 把 teamId / activityId 写到 user-info 的 dataset 上,便于点击时读取
function makeItem(team, price) {
const { userId, targetCount, lockCount,
validTimeCountdown, teamId, activityId: tActId } = team;
const { userId, targetCount, lockCount, validTimeCountdown } = team;
// ① 先对 userId 脱敏
const maskedId = obfuscateUserId(userId);
// ② 继续原本逻辑
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"
data-teamid="${teamId}"
data-activityid="${tActId}">
<div class="user-name">${userId}</div>
<div class="user-status">
仅剩${leftNum}人成团
<span class="countdown">${timeText}</span>
</div>
<div class="user-avatar"><i class="fas fa-user"></i></div>
<div class="user-info">
<div class="user-name">${maskedId}</div>
<div class="user-status">
仅剩 ${leftNum} 人成团
<span class="countdown">${timeText}</span>
</div>
<button class="buy-btn" data-price="${price}">参与拼团</button>
`;
</div>
<button class="buy-btn" data-price="${price}">参与拼团</button>
`;
return div;
}

View File

@ -1,7 +1,7 @@
/* -------------------- 配置 -------------------- */
const sPayMallUrl = "http://127.0.0.1:8092";
/* -------------------- 工具函数 -------------------- */
/* -------------------- Cookie 工具 -------------------- */
function setCookie(name, value, days) {
const date = new Date();
date.setTime(date.getTime() + days * 24 * 60 * 60 * 1000);
@ -10,36 +10,61 @@ function setCookie(name, value, days) {
/* -------------------- 主逻辑 -------------------- */
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;
// ① 异步加载 FingerprintJS 并计算浏览器指纹
import("https://openfpcdn.io/fingerprintjs/v4")
.then(FingerprintJS => FingerprintJS.load())
.then(fp => fp.get())
.then(result => {
const sceneStr = result.visitorId.toUpperCase(); // 作为场景值
/* ---------- 无痕登录按钮 ---------- */
const stealthBtn = document.getElementById("stealth-login-btn");
if (stealthBtn) {
stealthBtn.addEventListener("click", () => {
/**
* 建议此处可调用后端 auto_login?sceneStr=xxx 接口
* 下面仅示例直接把指纹写 cookie 后跳首页
*/
setCookie("loginToken", sceneStr, 30);
window.location.href = "./index.html";
});
}
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");
/* ---------- 获取二维码 ticket ---------- */
fetch(`${sPayMallUrl}/api/v1/login/weixin_qrcode_ticket_scene?sceneStr=${sceneStr}`)
.then(res => res.json())
.then(data => {
if (data.code !== "0000") {
console.error("获取二维码 ticket 失败:", data.info);
return;
}
// 2) 轮询确认登录
const intervalId = setInterval(() => checkLoginStatus(ticket, intervalId), 3000);
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");
// ② 开始轮询检查登录状态
const intervalId = setInterval(() => {
checkLoginStatus(ticket, sceneStr, intervalId);
}, 3000); // 每 3 秒检查一次
})
.catch(err => console.error("请求失败:", err));
})
.catch(err => console.error("请求失败:", err));
.catch(err => console.error("FingerprintJS 加载失败:", err));
});
/* -------------------- 轮询检查登录 -------------------- */
function checkLoginStatus(ticket, intervalId) {
fetch(`${sPayMallUrl}/api/v1/login/check_login?ticket=${ticket}`)
function checkLoginStatus(ticket, sceneStr, intervalId) {
fetch(`${sPayMallUrl}/api/v1/login/check_login_scene?ticket=${ticket}&sceneStr=${sceneStr}`)
.then(res => res.json())
.then(data => {
if (data.code === "0000") {
// 登录成功,停轮询
console.info("login success");
clearInterval(intervalId);
// 把 token 写入 cookie30 天)
// 把后端返回的登录凭证写入 cookie30 天)
setCookie("loginToken", data.data, 30);
// 跳转首页

View File

@ -2,10 +2,11 @@
<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" />
<meta name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<title>S Pay Mall 商城登录</title>
<!-- 独立样式文件 -->
<!-- 独立样式文件(可在这里或 login.css 里为 .stealth-login-btn 增加样式) -->
<link rel="stylesheet" href="css/login.css" />
</head>
<body>
@ -15,16 +16,24 @@
<div class="qr-code">
<!-- 占位图先显示,加载成功后替换 -->
<img id="qr-code-img" src="images/placeholder.png" alt="微信二维码" class="pulse" />
<img id="qr-code-img"
src="images/placeholder.png"
alt="微信二维码"
class="pulse" />
</div>
<p class="instructions">
请使用微信扫描二维码登录<br />
扫码后自动登录商城
</p>
<!-- 新增:无痕登录(浏览器指纹)按钮 -->
<button id="stealth-login-btn" class="stealth-login-btn">
无痕登录(浏览器指纹)
</button>
</div>
<!-- 独立脚本文件 -->
<!-- 业务脚本 -->
<script src="js/login.js"></script>
</body>
</html>

View File

@ -0,0 +1,26 @@
version: '3.8'
networks:
group-buy-network:
external: true
services:
pay-mall:
build:
context: .
dockerfile: Dockerfile
image: smile/pay-mall:latest
container_name: pay-mall
restart: on-failure
depends_on:
mysql:
condition: service_healthy
redis:
condition: service_healthy
ports:
- '8092:8092'
environment:
- TZ=Asia/Shanghai
- SPRING_PROFILES_ACTIVE=prod
networks:
- group-buy-network

View File

@ -1,17 +1,45 @@
# 基础镜像
FROM openjdk:8-jre-slim
# —— 第一阶段Maven 构建 ——
FROM maven:3.8.7-eclipse-temurin-17-alpine AS builder
WORKDIR /workspace
# 作者
MAINTAINER smile
# 把项目级 settings.xml 复制到容器里
COPY .mvn/settings.xml /root/.m2/settings.xml
# 配置
ENV PARAMS=""
# 1. 先只拷贝父 POM 及各模块的 pom.xml加速依赖下载
COPY pom.xml ./pom.xml
COPY pay-mall-api/pom.xml ./pay-mall-api/pom.xml
COPY pay-mall-domain/pom.xml ./pay-mall-domain/pom.xml
COPY pay-mall-infrastructure/pom.xml ./pay-mall-infrastructure/pom.xml
COPY pay-mall-trigger/pom.xml ./pay-mall-trigger/pom.xml
COPY pay-mall-types/pom.xml ./pay-mall-types/pom.xml
COPY pay-mall-app/pom.xml ./pay-mall-app/pom.xml
# 时区
ENV TZ=PRC
# 离线下载所有依赖
RUN mvn dependency:go-offline -B
# 2. 拷贝所有源码
COPY . .
# 3. 只打包 main 应用模块(连带编译它依赖的模块),跳过测试,加速构建
RUN mvn \
-f pom.xml clean package \
-pl pay-mall-app -am \
-DskipTests -B
# —— 第二阶段:运行时镜像 ——
FROM openjdk:17-jdk-slim
LABEL maintainer="smile"
# 可选:设置时区
ENV TZ=Asia/Shanghai
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
# 添加应用
ADD target/pay-mall-app.jar /pay-mall-app.jar
# 把构建产物拷过来
COPY --from=builder \
/workspace/pay-mall-app/target/pay-mall-app.jar \
app.jar
ENTRYPOINT ["sh","-c","java -jar $JAVA_OPTS /pay-mall-app.jar $PARAMS"]
# 暴露端口,按需改
EXPOSE 8092
ENTRYPOINT ["java", "-jar", "app.jar"]

View File

@ -1,41 +1,63 @@
# src/main/resources/application-prod.yml
server:
port: 8092
# 线程池配置
thread:
pool:
executor:
config:
core-pool-size: 20
max-pool-size: 50
keep-alive-time: 5000
block-queue-size: 5000
policy: CallerRunsPolicy
app:
config:
api-version: v1
cross-origin: '*' # 如果生产环境需要限域名,这里再改
group-buy-market:
# 从 127.0.0.1:8091 → group-buying-sys:8091容器名
api-url: http://group-buying-sys:8091
# 回调给自己,通常要是公网可访问的域名或网关地址
# 如果 pay-mall 服务外部也同 host:8092可以写
notify-url: http://124.71.159.195:8092/api/v1/alipay/group_buy_notify
source: s01
chanel: c01
# 数据库配置
#spring:
# datasource:
# username: root
# password: 123456
# url: jdbc:mysql://127.0.0.1:3306/pay-mall?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:
# 从本地 127.0.0.1:13306/pay-mall 改为容器 mysql:3306/pay-mall
url: jdbc:mysql://mysql:3306/pay-mall?useUnicode=true&characterEncoding=utf8&autoReconnect=true&zeroDateTimeBehavior=convertToNull&serverTimezone=Asia/Shanghai&useSSL=false
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
hikari:
pool-name: PayMall_HikariCP
minimum-idle: 15
maximum-pool-size: 25
idle-timeout: 180000
max-lifetime: 1800000
connection-timeout: 30000
connection-test-query: SELECT 1
type: com.zaxxer.hikari.HikariDataSource
#mybatis:
# mapper-locations: classpath:/mybatis/mapper/*.xml
# config-location: classpath:/mybatis/config/mybatis-config.xml
# 如果你想在 application.yml 里集中管理,也可以在 prod profile 下覆盖:
# spring.redis.host: redis
# spring.redis.port: 6379
mybatis:
mapper-locations: classpath:/mybatis/mapper/*.xml
config-location: classpath:/mybatis/config/mybatis-config.xml
weixin:
config:
original-id: ${paymall.wechat.original-id}
token: ${paymall.wechat.token}
app-id: ${paymall.wechat.app-id}
app-secret: ${paymall.wechat.app-secret}
template_id: ${paymall.wechat.template-id}
alipay:
enabled: true
app_id: ${paymall.alipay.app-id}
merchant_private_key: ${paymall.alipay.merchant-private-key}
alipay_public_key: ${paymall.alipay.alipay-public-key}
notify_url: ${paymall.alipay.notify-url}
return_url: ${paymall.alipay.return-url}
gateway_url: ${paymall.alipay.gateway-url}
# 日志
logging:
level:
root: info
config: classpath:logback-spring.xml
config: classpath:logback-spring.xml

View File

@ -1,3 +1,3 @@
spring:
profiles:
active: dev,local
active: prod,local

View File

@ -1,4 +1,5 @@
package edu.whut.infrastructure.adapter.port;
import cn.hutool.core.util.IdUtil;
import com.google.common.cache.Cache;
import edu.whut.domain.auth.adapter.port.ILoginPort;
import edu.whut.infrastructure.gateway.IWeixinApiService;
@ -32,11 +33,15 @@ public class LoginPort implements ILoginPort {
private final IWeixinApiService weixinApiService;
/**
* 生成二维码登录凭证 ticket
* 获取或刷新 access_token 调用微信接口创建带参数的二维码返回 ticket
* 生成带业务标识的二维码登录凭证 ticket
*
* @param sceneStr 本次登录会话的唯一标识字符串场景会被微信原样返回到回调的 EventKey 字段
* 用于在回调时直接定位到对应的登录请求无需再额外映射 ticket
* @return 本次二维码请求的 ticket用于前端换取二维码图片
* @throws IOException
*/
@Override
public String createQrCodeTicket() throws IOException {
public String createQrCodeTicket(String sceneStr) throws IOException {
// 1. 获取 access_token优先从缓存读取缓存失效时调用微信接口获取并更新缓存
String accessToken = weixinAccessToken.getIfPresent(appid);
if (null == accessToken) {
@ -50,10 +55,10 @@ public class LoginPort implements ILoginPort {
// 2. 构造二维码请求对象设置过期时间及业务参数
WeixinQrCodeRequestDTO weixinQrCodeReq = WeixinQrCodeRequestDTO.builder()
.expire_seconds(2592000)
.action_name(WeixinQrCodeRequestDTO.ActionNameTypeVO.QR_SCENE.getCode())
.action_name(WeixinQrCodeRequestDTO.ActionNameTypeVO.QR_STR_SCENE.getCode())
.action_info(WeixinQrCodeRequestDTO.ActionInfo.builder()
.scene(WeixinQrCodeRequestDTO.ActionInfo.Scene.builder()
.scene_id(100601)
.scene_str(sceneStr)
.build())
.build())
.build();
@ -65,6 +70,12 @@ public class LoginPort implements ILoginPort {
return weixinQrCodeRes.getTicket();
}
@Override
public String createQrCodeTicket() throws IOException {
String sceneStr = IdUtil.getSnowflake().nextIdStr();
return createQrCodeTicket(sceneStr);
}
/**
* 发送登录成功模板消息
* 获取或刷新 access_token 封装模板数据调用微信接口发送模板消息

View File

@ -41,6 +41,9 @@ public class LoginController implements IAuthService {
}
}
/**
* 生成并返回一个带浏览器指纹的微信扫码登录的凭证ticket
*/
@GetMapping("/weixin_qrcode_ticket_scene")
@Override
public Response<String> weixinQrCodeTicket(@RequestParam String sceneStr) {