7.17 尝试部署-1
This commit is contained in:
parent
3df396437b
commit
1b1729d7f8
@ -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);
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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 写入 cookie(30 天)
|
||||
// 把后端返回的登录凭证写入 cookie(30 天)
|
||||
setCookie("loginToken", data.data, 30);
|
||||
|
||||
// 跳转首页
|
||||
|
@ -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>
|
||||
|
26
docs/tag/tagv1.0/docker-compose-app-v2.0.yml
Normal file
26
docs/tag/tagv1.0/docker-compose-app-v2.0.yml
Normal 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
|
@ -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"]
|
||||
|
@ -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 #空闲连接存活最大时间,默认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:
|
||||
# 从本地 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
|
||||
|
@ -1,3 +1,3 @@
|
||||
spring:
|
||||
profiles:
|
||||
active: dev,local
|
||||
active: prod,local
|
||||
|
@ -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 后,封装模板数据,调用微信接口发送模板消息。
|
||||
|
@ -41,6 +41,9 @@ public class LoginController implements IAuthService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成并返回一个带浏览器指纹的微信扫码登录的凭证(ticket)。
|
||||
*/
|
||||
@GetMapping("/weixin_qrcode_ticket_scene")
|
||||
@Override
|
||||
public Response<String> weixinQrCodeTicket(@RequestParam String sceneStr) {
|
||||
|
Loading…
x
Reference in New Issue
Block a user