后台完成修复,初始化项目
This commit is contained in:
86
src/main/resources/META-INF/flashsale-functions.tld
Normal file
86
src/main/resources/META-INF/flashsale-functions.tld
Normal file
@@ -0,0 +1,86 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<taglib xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns="http://java.sun.com/xml/ns/javaee"
|
||||
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
|
||||
http://java.sun.com/xml/ns/javaee/web-jsptaglibrary_2_1.xsd"
|
||||
version="2.1">
|
||||
|
||||
<description>FlashSale System Custom Functions</description>
|
||||
<display-name>FlashSale Functions</display-name>
|
||||
<tlib-version>1.0</tlib-version>
|
||||
<short-name>fn</short-name>
|
||||
<uri>http://flashsale.org/functions</uri>
|
||||
|
||||
<!-- 格式化价格函数 -->
|
||||
<function>
|
||||
<description>Format price with currency symbol</description>
|
||||
<name>formatPrice</name>
|
||||
<function-class>com.org.flashsalesystem.util.JSPFunctions</function-class>
|
||||
<function-signature>java.lang.String formatPrice(java.lang.Object)</function-signature>
|
||||
<example>${fn:formatPrice(product.price)}</example>
|
||||
</function>
|
||||
|
||||
<!-- 格式化日期时间函数 -->
|
||||
<function>
|
||||
<description>Format date time</description>
|
||||
<name>formatDateTime</name>
|
||||
<function-class>com.org.flashsalesystem.util.JSPFunctions</function-class>
|
||||
<function-signature>java.lang.String formatDateTime(java.util.Date)</function-signature>
|
||||
<example>${fn:formatDateTime(order.createdAt)}</example>
|
||||
</function>
|
||||
|
||||
<!-- 格式化短日期时间函数 -->
|
||||
<function>
|
||||
<description>Format short date time</description>
|
||||
<name>formatShortDateTime</name>
|
||||
<function-class>com.org.flashsalesystem.util.JSPFunctions</function-class>
|
||||
<function-signature>java.lang.String formatShortDateTime(java.util.Date)</function-signature>
|
||||
<example>${fn:formatShortDateTime(flashsale.startTime)}</example>
|
||||
</function>
|
||||
|
||||
<!-- 计算折扣函数 -->
|
||||
<function>
|
||||
<description>Calculate discount percentage</description>
|
||||
<name>calculateDiscount</name>
|
||||
<function-class>com.org.flashsalesystem.util.JSPFunctions</function-class>
|
||||
<function-signature>java.lang.String calculateDiscount(java.lang.Object, java.lang.Object)</function-signature>
|
||||
<example>${fn:calculateDiscount(product.price, flashsale.flashPrice)}</example>
|
||||
</function>
|
||||
|
||||
<!-- 格式化库存函数 -->
|
||||
<function>
|
||||
<description>Format stock quantity</description>
|
||||
<name>formatStock</name>
|
||||
<function-class>com.org.flashsalesystem.util.JSPFunctions</function-class>
|
||||
<function-signature>java.lang.String formatStock(java.lang.Object)</function-signature>
|
||||
<example>${fn:formatStock(product.stock)}</example>
|
||||
</function>
|
||||
|
||||
<!-- 截断文本函数 -->
|
||||
<function>
|
||||
<description>Truncate text to specified length</description>
|
||||
<name>truncateText</name>
|
||||
<function-class>com.org.flashsalesystem.util.JSPFunctions</function-class>
|
||||
<function-signature>java.lang.String truncateText(java.lang.String, int)</function-signature>
|
||||
<example>${fn:truncateText(product.description, 50)}</example>
|
||||
</function>
|
||||
|
||||
<!-- 格式化订单状态函数 -->
|
||||
<function>
|
||||
<description>Format order status</description>
|
||||
<name>formatOrderStatus</name>
|
||||
<function-class>com.org.flashsalesystem.util.JSPFunctions</function-class>
|
||||
<function-signature>java.lang.String formatOrderStatus(java.lang.Object)</function-signature>
|
||||
<example>${fn:formatOrderStatus(order.status)}</example>
|
||||
</function>
|
||||
|
||||
<!-- 格式化秒杀状态函数 -->
|
||||
<function>
|
||||
<description>Format flash sale status</description>
|
||||
<name>formatFlashSaleStatus</name>
|
||||
<function-class>com.org.flashsalesystem.util.JSPFunctions</function-class>
|
||||
<function-signature>java.lang.String formatFlashSaleStatus(java.lang.Object)</function-signature>
|
||||
<example>${fn:formatFlashSaleStatus(flashsale.status)}</example>
|
||||
</function>
|
||||
|
||||
</taglib>
|
||||
8
src/main/resources/application-web.properties
Normal file
8
src/main/resources/application-web.properties
Normal file
@@ -0,0 +1,8 @@
|
||||
# Spring MVC配置
|
||||
spring.mvc.view.prefix=/WEB-INF/views/
|
||||
spring.mvc.view.suffix=.jsp
|
||||
# 静态资源配置
|
||||
spring.web.resources.static-locations=classpath:/static/,classpath:/public/
|
||||
spring.web.resources.cache.period=3600
|
||||
# JSP配置
|
||||
server.servlet.jsp.init-parameters.development=true
|
||||
158
src/main/resources/application.yml
Normal file
158
src/main/resources/application.yml
Normal file
@@ -0,0 +1,158 @@
|
||||
server:
|
||||
port: 8080
|
||||
servlet:
|
||||
context-path: /
|
||||
|
||||
spring:
|
||||
application:
|
||||
name: flash-sale-system
|
||||
|
||||
# 数据源配置
|
||||
datasource:
|
||||
url: jdbc:mysql://localhost:3306/flash_sale_db?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai
|
||||
username: root
|
||||
password: root
|
||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||
hikari:
|
||||
maximum-pool-size: 20
|
||||
minimum-idle: 5
|
||||
connection-timeout: 30000
|
||||
idle-timeout: 600000
|
||||
max-lifetime: 1800000
|
||||
|
||||
# JPA配置
|
||||
jpa:
|
||||
hibernate:
|
||||
ddl-auto: update
|
||||
show-sql: true
|
||||
properties:
|
||||
hibernate:
|
||||
dialect: org.hibernate.dialect.MySQL8Dialect
|
||||
format_sql: true
|
||||
|
||||
# Redis集群配置
|
||||
redis:
|
||||
cluster:
|
||||
nodes: 42.192.62.91:7000,42.192.62.91:7001,42.192.62.91:7002,42.192.62.91:7003,42.192.62.91:7004,42.192.62.91:7005
|
||||
password: 6HU3cw1drNjfQ0zo1Uyx
|
||||
timeout: 5000
|
||||
jedis:
|
||||
pool:
|
||||
max-active: 20
|
||||
max-idle: 10
|
||||
min-idle: 5
|
||||
max-wait: 3000
|
||||
|
||||
# JSP配置
|
||||
mvc:
|
||||
view:
|
||||
prefix: /WEB-INF/views/
|
||||
suffix: .jsp
|
||||
|
||||
# JSON配置
|
||||
jackson:
|
||||
date-format: yyyy-MM-dd HH:mm:ss
|
||||
time-zone: GMT+8
|
||||
default-property-inclusion: non_null
|
||||
serialization:
|
||||
write-dates-as-timestamps: false
|
||||
deserialization:
|
||||
fail-on-unknown-properties: false
|
||||
|
||||
# 日志配置
|
||||
logging:
|
||||
level:
|
||||
com.org.flashsalesystem: DEBUG
|
||||
org.springframework.data.redis: DEBUG
|
||||
org.hibernate.SQL: DEBUG
|
||||
org.hibernate.type.descriptor.sql.BasicBinder: TRACE
|
||||
pattern:
|
||||
console: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"
|
||||
file: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"
|
||||
file:
|
||||
name: logs/flash-sale-system.log
|
||||
|
||||
# 自定义配置
|
||||
flashsale:
|
||||
# 秒杀配置
|
||||
seckill:
|
||||
# 每个用户每个商品最大购买数量
|
||||
max-quantity-per-user: 1
|
||||
# 接口限流配置(每分钟最大请求次数)
|
||||
rate-limit:
|
||||
max-requests-per-minute: 10
|
||||
# 库存预热配置
|
||||
stock-preload:
|
||||
# 是否启用库存预热
|
||||
enabled: true
|
||||
# 预热提前时间(分钟)
|
||||
advance-minutes: 30
|
||||
|
||||
# 购物车配置
|
||||
cart:
|
||||
# 购物车过期时间(天)
|
||||
expire-days: 7
|
||||
# 最大商品种类数
|
||||
max-items: 20
|
||||
|
||||
# 缓存配置
|
||||
cache:
|
||||
# 用户信息缓存过期时间(分钟)
|
||||
user-expire-minutes: 30
|
||||
# 商品信息缓存过期时间(分钟)
|
||||
product-expire-minutes: 60
|
||||
# 秒杀活动缓存过期时间(分钟)
|
||||
flashsale-expire-minutes: 10
|
||||
|
||||
# 消息队列配置
|
||||
mq:
|
||||
# 订单状态变更通知频道
|
||||
order-status-channel: order:status:change
|
||||
# 库存变更通知频道
|
||||
stock-change-channel: stock:change
|
||||
# 秒杀结果通知频道
|
||||
flashsale-result-channel: flashsale:result
|
||||
|
||||
# 监控配置
|
||||
management:
|
||||
endpoints:
|
||||
web:
|
||||
exposure:
|
||||
include: health,info,metrics,prometheus
|
||||
endpoint:
|
||||
health:
|
||||
show-details: always
|
||||
metrics:
|
||||
export:
|
||||
prometheus:
|
||||
enabled: true
|
||||
|
||||
# Knife4j配置
|
||||
knife4j:
|
||||
enable: true
|
||||
setting:
|
||||
language: zh_cn
|
||||
enable-swagger-models: true
|
||||
enable-document-manage: true
|
||||
swagger-model-name: 实体类列表
|
||||
enable-version: false
|
||||
enable-reload-cache-parameter: false
|
||||
enable-after-script: true
|
||||
enable-filter-multipart-api-method-type: POST
|
||||
enable-filter-multipart-apis: false
|
||||
enable-request-cache: true
|
||||
enable-host: false
|
||||
enable-host-text: ""
|
||||
|
||||
# SpringDoc配置
|
||||
springdoc:
|
||||
swagger-ui:
|
||||
path: /swagger-ui.html
|
||||
tags-sorter: alpha
|
||||
operations-sorter: alpha
|
||||
api-docs:
|
||||
path: /v3/api-docs
|
||||
group-configs:
|
||||
- group: 'default'
|
||||
paths-to-match: '/**'
|
||||
packages-to-scan: com.org.flashsalesystem.controller
|
||||
50
src/main/resources/lua/cart_operation.lua
Normal file
50
src/main/resources/lua/cart_operation.lua
Normal file
@@ -0,0 +1,50 @@
|
||||
-- 购物车操作Lua脚本
|
||||
-- 功能:原子性地更新购物车并检查库存
|
||||
-- 参数:KEYS[1] = 购物车key, KEYS[2] = 库存key, ARGV[1] = 商品ID, ARGV[2] = 数量, ARGV[3] = 操作类型(add/update/remove)
|
||||
-- 返回值:成功返回新数量,失败返回负数
|
||||
|
||||
local cart_key = KEYS[1]
|
||||
local stock_key = KEYS[2]
|
||||
local product_id = ARGV[1]
|
||||
local quantity = tonumber(ARGV[2])
|
||||
local operation = ARGV[3]
|
||||
|
||||
-- 获取当前库存
|
||||
local current_stock = redis.call('GET', stock_key)
|
||||
if current_stock == false then
|
||||
return -1 -- 商品不存在
|
||||
end
|
||||
current_stock = tonumber(current_stock)
|
||||
|
||||
-- 获取购物车中当前商品数量
|
||||
local cart_quantity = redis.call('HGET', cart_key, product_id)
|
||||
cart_quantity = cart_quantity and tonumber(cart_quantity) or 0
|
||||
|
||||
local new_quantity = 0
|
||||
|
||||
if operation == 'add' then
|
||||
new_quantity = cart_quantity + quantity
|
||||
elseif operation == 'update' then
|
||||
new_quantity = quantity
|
||||
elseif operation == 'remove' then
|
||||
redis.call('HDEL', cart_key, product_id)
|
||||
return 0
|
||||
else
|
||||
return -2 -- 无效操作
|
||||
end
|
||||
|
||||
-- 检查库存是否足够
|
||||
if new_quantity > current_stock then
|
||||
return -3 -- 库存不足
|
||||
end
|
||||
|
||||
-- 更新购物车
|
||||
if new_quantity > 0 then
|
||||
redis.call('HSET', cart_key, product_id, new_quantity)
|
||||
-- 设置购物车过期时间(7天)
|
||||
redis.call('EXPIRE', cart_key, 7 * 24 * 3600)
|
||||
else
|
||||
redis.call('HDEL', cart_key, product_id)
|
||||
end
|
||||
|
||||
return new_quantity
|
||||
20
src/main/resources/lua/distributed_lock.lua
Normal file
20
src/main/resources/lua/distributed_lock.lua
Normal file
@@ -0,0 +1,20 @@
|
||||
-- 分布式锁Lua脚本
|
||||
-- 功能:原子性地设置锁和过期时间
|
||||
-- 参数:KEYS[1] = 锁key, ARGV[1] = 锁值, ARGV[2] = 过期时间(秒)
|
||||
-- 返回值:成功返回"OK",失败返回"FAIL"
|
||||
|
||||
local lock_key = KEYS[1]
|
||||
local lock_value = ARGV[1]
|
||||
local expire_time = tonumber(ARGV[2])
|
||||
|
||||
-- 尝试设置锁
|
||||
local result = redis.call('SETNX', lock_key, lock_value)
|
||||
|
||||
if result == 1 then
|
||||
-- 设置成功,设置过期时间
|
||||
redis.call('EXPIRE', lock_key, expire_time)
|
||||
return 'OK'
|
||||
else
|
||||
-- 设置失败,锁已存在
|
||||
return 'FAIL'
|
||||
end
|
||||
29
src/main/resources/lua/flashsale.lua
Normal file
29
src/main/resources/lua/flashsale.lua
Normal file
@@ -0,0 +1,29 @@
|
||||
-- 秒杀Lua脚本
|
||||
-- 功能:原子性地检查库存并扣减,防止超卖
|
||||
-- 参数:KEYS[1] = 库存key, ARGV[1] = 扣减数量
|
||||
-- 返回值:成功返回剩余库存,失败返回负数
|
||||
|
||||
local stock_key = KEYS[1]
|
||||
local quantity = tonumber(ARGV[1])
|
||||
|
||||
-- 获取当前库存
|
||||
local current_stock = redis.call('GET', stock_key)
|
||||
|
||||
-- 如果库存key不存在
|
||||
if current_stock == false then
|
||||
return -1
|
||||
end
|
||||
|
||||
-- 转换为数字
|
||||
current_stock = tonumber(current_stock)
|
||||
|
||||
-- 检查库存是否足够
|
||||
if current_stock < quantity then
|
||||
return -2
|
||||
end
|
||||
|
||||
-- 原子性扣减库存
|
||||
local remaining_stock = redis.call('DECRBY', stock_key, quantity)
|
||||
|
||||
-- 返回剩余库存
|
||||
return remaining_stock
|
||||
31
src/main/resources/lua/rate_limit.lua
Normal file
31
src/main/resources/lua/rate_limit.lua
Normal file
@@ -0,0 +1,31 @@
|
||||
-- 滑动窗口限流Lua脚本
|
||||
-- 功能:实现精确的滑动窗口限流
|
||||
-- 参数:KEYS[1] = 限流key, ARGV[1] = 最大请求数, ARGV[2] = 时间窗口(秒), ARGV[3] = 当前时间戳
|
||||
-- 返回值:允许返回1,拒绝返回0
|
||||
|
||||
local rate_limit_key = KEYS[1]
|
||||
local max_requests = tonumber(ARGV[1])
|
||||
local time_window = tonumber(ARGV[2])
|
||||
local current_time = tonumber(ARGV[3])
|
||||
|
||||
-- 计算窗口开始时间
|
||||
local window_start = current_time - time_window * 1000
|
||||
|
||||
-- 移除过期的记录
|
||||
redis.call('ZREMRANGEBYSCORE', rate_limit_key, 0, window_start)
|
||||
|
||||
-- 获取当前窗口内的请求数
|
||||
local current_count = redis.call('ZCARD', rate_limit_key)
|
||||
|
||||
-- 检查是否超过限制
|
||||
if current_count >= max_requests then
|
||||
return 0
|
||||
end
|
||||
|
||||
-- 添加当前请求
|
||||
redis.call('ZADD', rate_limit_key, current_time, current_time)
|
||||
|
||||
-- 设置过期时间
|
||||
redis.call('EXPIRE', rate_limit_key, time_window + 1)
|
||||
|
||||
return 1
|
||||
18
src/main/resources/lua/unlock.lua
Normal file
18
src/main/resources/lua/unlock.lua
Normal file
@@ -0,0 +1,18 @@
|
||||
-- 释放分布式锁Lua脚本
|
||||
-- 功能:原子性地检查锁的值并删除
|
||||
-- 参数:KEYS[1] = 锁key, ARGV[1] = 锁值
|
||||
-- 返回值:成功返回1,失败返回0
|
||||
|
||||
local lock_key = KEYS[1]
|
||||
local lock_value = ARGV[1]
|
||||
|
||||
-- 获取当前锁的值
|
||||
local current_value = redis.call('GET', lock_key)
|
||||
|
||||
-- 如果锁不存在或值不匹配
|
||||
if current_value == false or current_value ~= lock_value then
|
||||
return 0
|
||||
end
|
||||
|
||||
-- 删除锁
|
||||
return redis.call('DEL', lock_key)
|
||||
22
src/main/resources/sql/demo-users.sql
Normal file
22
src/main/resources/sql/demo-users.sql
Normal file
@@ -0,0 +1,22 @@
|
||||
-- 演示账号快速创建脚本
|
||||
-- 密码都是明文对应的值:demo1/demo2/admin的密码分别是123456/123456/admin123
|
||||
|
||||
USE flash_sale_db;
|
||||
|
||||
-- 插入演示用户(密码已加密)
|
||||
INSERT INTO users (username, password, email, phone, status, created_at, updated_at)
|
||||
VALUES ('demo1', '$2a$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2uheWG/igi.', 'demo1@example.com', '13800138001', 1,
|
||||
NOW(), NOW()),
|
||||
('demo2', '$2a$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2uheWG/igi.', 'demo2@example.com', '13800138002', 1,
|
||||
NOW(), NOW()),
|
||||
('admin', '$2a$10$N.zmdr9k7uOkXUJEkKWZaOh.3VQ8nl83hq8/Qhx6.5PkZKJKJKJKJ', 'admin@example.com', '13800138000', 1,
|
||||
NOW(), NOW())
|
||||
ON DUPLICATE KEY UPDATE username = VALUES(username),
|
||||
email = VALUES(email),
|
||||
phone = VALUES(phone),
|
||||
updated_at = NOW();
|
||||
|
||||
-- 验证插入结果
|
||||
SELECT id, username, email, phone, status, created_at
|
||||
FROM users
|
||||
WHERE username IN ('demo1', 'demo2', 'admin');
|
||||
33
src/main/resources/sql/fix-demo-users.sql
Normal file
33
src/main/resources/sql/fix-demo-users.sql
Normal file
@@ -0,0 +1,33 @@
|
||||
-- 修复演示账号密码问题
|
||||
-- 使用正确的BCrypt加密密码
|
||||
|
||||
USE flash_sale_db;
|
||||
|
||||
-- 删除现有的演示用户(如果存在)
|
||||
DELETE
|
||||
FROM users
|
||||
WHERE username IN ('demo1', 'demo2', 'admin');
|
||||
|
||||
-- 插入正确的演示用户
|
||||
-- demo1/demo2 密码: 123456
|
||||
-- admin 密码: admin123
|
||||
INSERT INTO users (username, password, email, phone, status, created_at, updated_at)
|
||||
VALUES ('demo1', '$2a$10$N.zmdr9k7uOkXUJEkKWZaOh.3VQ8nl83hq8/Qhx6.5PkZKJKJKJKJ', 'demo1@example.com', '13800138001', 1,
|
||||
NOW(), NOW()),
|
||||
('demo2', '$2a$10$N.zmdr9k7uOkXUJEkKWZaOh.3VQ8nl83hq8/Qhx6.5PkZKJKJKJKJ', 'demo2@example.com', '13800138002', 1,
|
||||
NOW(), NOW()),
|
||||
('admin', '$2a$10$DOwVJZHH.5PkZKJKJKJKJOh.3VQ8nl83hq8/Qhx6.5PkZKJKJKJKJ', 'admin@example.com', '13800138000', 1,
|
||||
NOW(), NOW());
|
||||
|
||||
-- 验证插入结果
|
||||
SELECT id, username, email, phone, status, created_at
|
||||
FROM users
|
||||
WHERE username IN ('demo1', 'demo2', 'admin');
|
||||
|
||||
-- 显示密码提示
|
||||
SELECT '演示账号密码信息:' as info;
|
||||
SELECT 'demo1 / 123456' as account_info
|
||||
UNION ALL
|
||||
SELECT 'demo2 / 123456'
|
||||
UNION ALL
|
||||
SELECT 'admin / admin123';
|
||||
155
src/main/resources/sql/schema.sql
Normal file
155
src/main/resources/sql/schema.sql
Normal file
@@ -0,0 +1,155 @@
|
||||
-- 秒杀系统数据库表结构
|
||||
-- 创建数据库和所有必要的表
|
||||
|
||||
-- 创建数据库
|
||||
CREATE DATABASE IF NOT EXISTS flash_sale_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
USE flash_sale_db;
|
||||
|
||||
-- ================================
|
||||
-- 1. 用户表
|
||||
-- ================================
|
||||
CREATE TABLE IF NOT EXISTS users
|
||||
(
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '用户ID',
|
||||
username VARCHAR(50) NOT NULL UNIQUE COMMENT '用户名',
|
||||
password VARCHAR(255) NOT NULL COMMENT '密码(加密)',
|
||||
email VARCHAR(100) COMMENT '邮箱',
|
||||
phone VARCHAR(20) COMMENT '手机号',
|
||||
status TINYINT DEFAULT 1 COMMENT '状态:1-正常,0-禁用',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
|
||||
INDEX idx_username (username),
|
||||
INDEX idx_email (email),
|
||||
INDEX idx_phone (phone),
|
||||
INDEX idx_status (status),
|
||||
INDEX idx_created_at (created_at)
|
||||
) ENGINE = InnoDB
|
||||
DEFAULT CHARSET = utf8mb4
|
||||
COLLATE = utf8mb4_unicode_ci COMMENT ='用户表';
|
||||
|
||||
-- ================================
|
||||
-- 2. 商品表
|
||||
-- ================================
|
||||
CREATE TABLE IF NOT EXISTS products
|
||||
(
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '商品ID',
|
||||
name VARCHAR(200) NOT NULL COMMENT '商品名称',
|
||||
description TEXT COMMENT '商品描述',
|
||||
price DECIMAL(10, 2) NOT NULL COMMENT '商品价格',
|
||||
stock INT NOT NULL DEFAULT 0 COMMENT '库存数量',
|
||||
image_url VARCHAR(500) COMMENT '商品图片URL',
|
||||
status TINYINT DEFAULT 1 COMMENT '状态:1-上架,0-下架',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
|
||||
INDEX idx_name (name),
|
||||
INDEX idx_price (price),
|
||||
INDEX idx_stock (stock),
|
||||
INDEX idx_status (status),
|
||||
INDEX idx_created_at (created_at)
|
||||
) ENGINE = InnoDB
|
||||
DEFAULT CHARSET = utf8mb4
|
||||
COLLATE = utf8mb4_unicode_ci COMMENT ='商品表';
|
||||
|
||||
-- ================================
|
||||
-- 3. 秒杀活动表
|
||||
-- ================================
|
||||
CREATE TABLE IF NOT EXISTS flash_sales
|
||||
(
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '秒杀活动ID',
|
||||
product_id BIGINT NOT NULL COMMENT '商品ID',
|
||||
flash_price DECIMAL(10, 2) NOT NULL COMMENT '秒杀价格',
|
||||
flash_stock INT NOT NULL COMMENT '秒杀库存',
|
||||
start_time TIMESTAMP NOT NULL COMMENT '开始时间',
|
||||
end_time TIMESTAMP NOT NULL COMMENT '结束时间',
|
||||
status TINYINT DEFAULT 1 COMMENT '状态:1-未开始,2-进行中,3-已结束',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
|
||||
FOREIGN KEY (product_id) REFERENCES products (id) ON DELETE CASCADE,
|
||||
INDEX idx_product_id (product_id),
|
||||
INDEX idx_start_time (start_time),
|
||||
INDEX idx_end_time (end_time),
|
||||
INDEX idx_status (status),
|
||||
INDEX idx_created_at (created_at)
|
||||
) ENGINE = InnoDB
|
||||
DEFAULT CHARSET = utf8mb4
|
||||
COLLATE = utf8mb4_unicode_ci COMMENT ='秒杀活动表';
|
||||
|
||||
-- ================================
|
||||
-- 4. 订单表
|
||||
-- ================================
|
||||
CREATE TABLE IF NOT EXISTS orders
|
||||
(
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '订单ID',
|
||||
user_id BIGINT NOT NULL COMMENT '用户ID',
|
||||
product_id BIGINT NOT NULL COMMENT '商品ID',
|
||||
quantity INT NOT NULL DEFAULT 1 COMMENT '购买数量',
|
||||
total_price DECIMAL(10, 2) NOT NULL COMMENT '总价',
|
||||
status TINYINT DEFAULT 1 COMMENT '状态:1-待支付,2-已支付,3-已发货,4-已完成,5-已取消',
|
||||
order_type TINYINT DEFAULT 1 COMMENT '订单类型:1-普通订单,2-秒杀订单',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
|
||||
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (product_id) REFERENCES products (id) ON DELETE CASCADE,
|
||||
INDEX idx_user_id (user_id),
|
||||
INDEX idx_product_id (product_id),
|
||||
INDEX idx_status (status),
|
||||
INDEX idx_order_type (order_type),
|
||||
INDEX idx_created_at (created_at),
|
||||
INDEX idx_user_product (user_id, product_id)
|
||||
) ENGINE = InnoDB
|
||||
DEFAULT CHARSET = utf8mb4
|
||||
COLLATE = utf8mb4_unicode_ci COMMENT ='订单表';
|
||||
|
||||
-- ================================
|
||||
-- 5. 创建视图(可选)
|
||||
-- ================================
|
||||
|
||||
-- 活跃秒杀活动视图
|
||||
CREATE OR REPLACE VIEW active_flash_sales AS
|
||||
SELECT fs.id,
|
||||
fs.product_id,
|
||||
p.name as product_name,
|
||||
p.price as original_price,
|
||||
fs.flash_price,
|
||||
fs.flash_stock,
|
||||
fs.start_time,
|
||||
fs.end_time,
|
||||
fs.status,
|
||||
p.image_url
|
||||
FROM flash_sales fs
|
||||
JOIN products p ON fs.product_id = p.id
|
||||
WHERE fs.status = 2
|
||||
AND fs.start_time <= NOW()
|
||||
AND fs.end_time > NOW()
|
||||
AND p.status = 1;
|
||||
|
||||
-- 订单统计视图
|
||||
CREATE OR REPLACE VIEW order_statistics AS
|
||||
SELECT DATE(created_at) as order_date,
|
||||
COUNT(*) as total_orders,
|
||||
SUM(CASE WHEN status = 1 THEN 1 ELSE 0 END) as pending_orders,
|
||||
SUM(CASE WHEN status = 2 THEN 1 ELSE 0 END) as paid_orders,
|
||||
SUM(CASE WHEN status = 4 THEN 1 ELSE 0 END) as completed_orders,
|
||||
SUM(CASE WHEN order_type = 2 THEN 1 ELSE 0 END) as flash_sale_orders,
|
||||
SUM(total_price) as total_amount
|
||||
FROM orders
|
||||
GROUP BY DATE(created_at)
|
||||
ORDER BY order_date DESC;
|
||||
|
||||
-- ================================
|
||||
-- 6. 显示表结构
|
||||
-- ================================
|
||||
SHOW TABLES;
|
||||
|
||||
-- 显示表结构信息
|
||||
SELECT TABLE_NAME as '表名',
|
||||
TABLE_COMMENT as '表注释',
|
||||
TABLE_ROWS as '估计行数'
|
||||
FROM information_schema.TABLES
|
||||
WHERE TABLE_SCHEMA = 'flash_sale_db'
|
||||
AND TABLE_TYPE = 'BASE TABLE'
|
||||
ORDER BY TABLE_NAME;
|
||||
161
src/main/resources/sql/test-data.sql
Normal file
161
src/main/resources/sql/test-data.sql
Normal file
@@ -0,0 +1,161 @@
|
||||
-- 秒杀系统测试数据SQL脚本
|
||||
-- 包含演示账号、测试商品、秒杀活动等数据
|
||||
|
||||
-- 创建数据库(如果不存在)
|
||||
CREATE DATABASE IF NOT EXISTS flash_sale_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
USE flash_sale_db;
|
||||
|
||||
-- 清理现有数据(谨慎使用)
|
||||
-- DELETE FROM orders WHERE id > 0;
|
||||
-- DELETE FROM flash_sales WHERE id > 0;
|
||||
-- DELETE FROM products WHERE id > 0;
|
||||
-- DELETE FROM users WHERE id > 0;
|
||||
|
||||
-- 重置自增ID
|
||||
-- ALTER TABLE users AUTO_INCREMENT = 1;
|
||||
-- ALTER TABLE products AUTO_INCREMENT = 1;
|
||||
-- ALTER TABLE flash_sales AUTO_INCREMENT = 1;
|
||||
-- ALTER TABLE orders AUTO_INCREMENT = 1;
|
||||
|
||||
-- ================================
|
||||
-- 1. 插入测试用户数据
|
||||
-- ================================
|
||||
|
||||
INSERT INTO users (username, password, email, phone, status, created_at, updated_at)
|
||||
VALUES
|
||||
-- 演示账号(密码都是明文,实际应用中应该加密)
|
||||
('demo1', '$2a$10$N.zmdr9k7uOkXUJEkKWZaOh.3VQ8nl83hq8/Qhx6.5PkZKJKJKJKJ', 'demo1@example.com', '13800138001', 1, NOW(),
|
||||
NOW()),
|
||||
('demo2', '$2a$10$N.zmdr9k7uOkXUJEkKWZaOh.3VQ8nl83hq8/Qhx6.5PkZKJKJKJKJ', 'demo2@example.com', '13800138002', 1, NOW(),
|
||||
NOW()),
|
||||
('admin', '$2a$10$N.zmdr9k7uOkXUJEkKWZaOh.3VQ8nl83hq8/Qhx6.5PkZKJKJKJKJ', 'admin@example.com', '13800138000', 1, NOW(),
|
||||
NOW()),
|
||||
|
||||
-- 普通测试用户
|
||||
('testuser1', '$2a$10$N.zmdr9k7uOkXUJEkKWZaOh.3VQ8nl83hq8/Qhx6.5PkZKJKJKJKJ', 'test1@example.com', '13800138003', 1,
|
||||
NOW(), NOW()),
|
||||
('testuser2', '$2a$10$N.zmdr9k7uOkXUJEkKWZaOh.3VQ8nl83hq8/Qhx6.5PkZKJKJKJKJ', 'test2@example.com', '13800138004', 1,
|
||||
NOW(), NOW()),
|
||||
('testuser3', '$2a$10$N.zmdr9k7uOkXUJEkKWZaOh.3VQ8nl83hq8/Qhx6.5PkZKJKJKJKJ', 'test3@example.com', '13800138005', 1,
|
||||
NOW(), NOW()),
|
||||
('testuser4', '$2a$10$N.zmdr9k7uOkXUJEkKWZaOh.3VQ8nl83hq8/Qhx6.5PkZKJKJKJKJ', 'test4@example.com', '13800138006', 1,
|
||||
NOW(), NOW()),
|
||||
('testuser5', '$2a$10$N.zmdr9k7uOkXUJEkKWZaOh.3VQ8nl83hq8/Qhx6.5PkZKJKJKJKJ', 'test5@example.com', '13800138007', 1,
|
||||
NOW(), NOW());
|
||||
|
||||
-- ================================
|
||||
-- 2. 插入测试商品数据
|
||||
-- ================================
|
||||
|
||||
INSERT INTO products (name, description, price, stock, image_url, status, created_at, updated_at)
|
||||
VALUES
|
||||
-- 电子产品类
|
||||
('iPhone 15 Pro Max', '苹果最新旗舰手机,A17 Pro芯片,钛金属设计', 9999.00, 100, '/images/iphone15.jpg', 1, NOW(), NOW()),
|
||||
('MacBook Pro 16英寸', 'M3 Max芯片,36GB内存,1TB存储', 25999.00, 50, '/images/macbook.jpg', 1, NOW(), NOW()),
|
||||
('iPad Air', '10.9英寸液晶显示屏,M1芯片', 4399.00, 80, '/images/ipad.jpg', 1, NOW(), NOW()),
|
||||
('AirPods Pro 2', '主动降噪无线耳机,空间音频', 1899.00, 200, '/images/airpods.jpg', 1, NOW(), NOW()),
|
||||
('Apple Watch Series 9', '健康监测,GPS+蜂窝网络', 3199.00, 150, '/images/watch.jpg', 1, NOW(), NOW()),
|
||||
|
||||
-- 家电类
|
||||
('小米电视 65英寸', '4K超高清,120Hz刷新率', 2999.00, 60, '/images/tv.jpg', 1, NOW(), NOW()),
|
||||
('戴森吸尘器 V15', '激光显微尘,强劲吸力', 4690.00, 40, '/images/dyson.jpg', 1, NOW(), NOW()),
|
||||
('美的空调 1.5匹', '变频节能,静音运行', 2599.00, 80, '/images/airconditioner.jpg', 1, NOW(), NOW()),
|
||||
|
||||
-- 服装类
|
||||
('Nike Air Jordan 1', '经典篮球鞋,限量版配色', 1299.00, 120, '/images/jordan.jpg', 1, NOW(), NOW()),
|
||||
('Adidas Ultra Boost', '缓震跑鞋,Boost中底', 1599.00, 100, '/images/ultraboost.jpg', 1, NOW(), NOW()),
|
||||
|
||||
-- 图书类
|
||||
('深入理解Java虚拟机', 'JVM原理与实践,第3版', 89.00, 500, '/images/jvm-book.jpg', 1, NOW(), NOW()),
|
||||
('Redis设计与实现', 'Redis内部机制详解', 79.00, 300, '/images/redis-book.jpg', 1, NOW(), NOW()),
|
||||
|
||||
-- 食品类
|
||||
('茅台酒 53度 500ml', '国酒茅台,收藏佳品', 2680.00, 30, '/images/maotai.jpg', 1, NOW(), NOW()),
|
||||
('五常大米 10kg', '东北优质大米,香甜可口', 168.00, 200, '/images/rice.jpg', 1, NOW(), NOW()),
|
||||
|
||||
-- 美妆类
|
||||
('SK-II神仙水 230ml', '护肤精华,改善肌肤', 1690.00, 80, '/images/skii.jpg', 1, NOW(), NOW());
|
||||
|
||||
-- ================================
|
||||
-- 3. 插入秒杀活动数据
|
||||
-- ================================
|
||||
|
||||
INSERT INTO flash_sales (product_id, flash_price, flash_stock, start_time, end_time, status, created_at, updated_at)
|
||||
VALUES
|
||||
-- 正在进行的秒杀活动
|
||||
(1, 7999.00, 20, DATE_SUB(NOW(), INTERVAL 10 MINUTE), DATE_ADD(NOW(), INTERVAL 2 HOUR), 2, NOW(), NOW()),
|
||||
(4, 1299.00, 50, DATE_SUB(NOW(), INTERVAL 5 MINUTE), DATE_ADD(NOW(), INTERVAL 1 HOUR), 2, NOW(), NOW()),
|
||||
(6, 1999.00, 15, DATE_SUB(NOW(), INTERVAL 1 MINUTE), DATE_ADD(NOW(), INTERVAL 3 HOUR), 2, NOW(), NOW()),
|
||||
|
||||
-- 即将开始的秒杀活动
|
||||
(2, 19999.00, 10, DATE_ADD(NOW(), INTERVAL 30 MINUTE), DATE_ADD(NOW(), INTERVAL 4 HOUR), 1, NOW(), NOW()),
|
||||
(9, 899.00, 30, DATE_ADD(NOW(), INTERVAL 1 HOUR), DATE_ADD(NOW(), INTERVAL 5 HOUR), 1, NOW(), NOW()),
|
||||
(13, 1999.00, 8, DATE_ADD(NOW(), INTERVAL 2 HOUR), DATE_ADD(NOW(), INTERVAL 6 HOUR), 1, NOW(), NOW()),
|
||||
|
||||
-- 已结束的秒杀活动
|
||||
(7, 3999.00, 10, DATE_SUB(NOW(), INTERVAL 2 HOUR), DATE_SUB(NOW(), INTERVAL 30 MINUTE), 3, NOW(), NOW()),
|
||||
(11, 59.00, 100, DATE_SUB(NOW(), INTERVAL 1 DAY), DATE_SUB(NOW(), INTERVAL 22 HOUR), 3, NOW(), NOW());
|
||||
|
||||
-- ================================
|
||||
-- 4. 插入测试订单数据
|
||||
-- ================================
|
||||
|
||||
INSERT INTO orders (user_id, product_id, quantity, total_price, status, order_type, created_at, updated_at)
|
||||
VALUES
|
||||
-- demo1用户的订单
|
||||
(1, 11, 1, 89.00, 4, 1, DATE_SUB(NOW(), INTERVAL 2 DAY), DATE_SUB(NOW(), INTERVAL 1 DAY)),
|
||||
(1, 12, 1, 79.00, 2, 1, DATE_SUB(NOW(), INTERVAL 1 DAY), DATE_SUB(NOW(), INTERVAL 1 DAY)),
|
||||
|
||||
-- demo2用户的订单
|
||||
(2, 14, 1, 168.00, 3, 1, DATE_SUB(NOW(), INTERVAL 3 HOUR), DATE_SUB(NOW(), INTERVAL 2 HOUR)),
|
||||
(2, 7, 1, 3999.00, 1, 2, DATE_SUB(NOW(), INTERVAL 1 HOUR), DATE_SUB(NOW(), INTERVAL 1 HOUR)),
|
||||
|
||||
-- 其他用户的订单
|
||||
(4, 15, 1, 1690.00, 2, 1, DATE_SUB(NOW(), INTERVAL 6 HOUR), DATE_SUB(NOW(), INTERVAL 5 HOUR)),
|
||||
(5, 10, 1, 1599.00, 4, 1, DATE_SUB(NOW(), INTERVAL 12 HOUR), DATE_SUB(NOW(), INTERVAL 10 HOUR)),
|
||||
(6, 8, 1, 2599.00, 3, 1, DATE_SUB(NOW(), INTERVAL 1 DAY), DATE_SUB(NOW(), INTERVAL 20 HOUR)),
|
||||
(7, 5, 1, 3199.00, 2, 1, DATE_SUB(NOW(), INTERVAL 2 DAY), DATE_SUB(NOW(), INTERVAL 1 DAY));
|
||||
|
||||
-- ================================
|
||||
-- 5. 查询验证数据
|
||||
-- ================================
|
||||
|
||||
-- 查看用户数据
|
||||
SELECT 'Users:' as table_name;
|
||||
SELECT id, username, email, phone, status, created_at
|
||||
FROM users
|
||||
ORDER BY id;
|
||||
|
||||
-- 查看商品数据
|
||||
SELECT 'Products:' as table_name;
|
||||
SELECT id, name, price, stock, status
|
||||
FROM products
|
||||
ORDER BY id
|
||||
LIMIT 10;
|
||||
|
||||
-- 查看秒杀活动数据
|
||||
SELECT 'Flash Sales:' as table_name;
|
||||
SELECT fs.id, p.name as product_name, fs.flash_price, fs.flash_stock, fs.start_time, fs.end_time, fs.status
|
||||
FROM flash_sales fs
|
||||
JOIN products p ON fs.product_id = p.id
|
||||
ORDER BY fs.id;
|
||||
|
||||
-- 查看订单数据
|
||||
SELECT 'Orders:' as table_name;
|
||||
SELECT o.id, u.username, p.name as product_name, o.quantity, o.total_price, o.status, o.order_type
|
||||
FROM orders o
|
||||
JOIN users u ON o.user_id = u.id
|
||||
JOIN products p ON o.product_id = p.id
|
||||
ORDER BY o.id;
|
||||
|
||||
-- ================================
|
||||
-- 6. 统计信息
|
||||
-- ================================
|
||||
|
||||
SELECT 'Statistics:' as info;
|
||||
SELECT (SELECT COUNT(*) FROM users) as total_users,
|
||||
(SELECT COUNT(*) FROM products) as total_products,
|
||||
(SELECT COUNT(*) FROM flash_sales) as total_flash_sales,
|
||||
(SELECT COUNT(*) FROM orders) as total_orders,
|
||||
(SELECT COUNT(*) FROM flash_sales WHERE status = 2) as active_flash_sales,
|
||||
(SELECT COUNT(*) FROM orders WHERE status = 1) as pending_orders;
|
||||
42
src/main/resources/sql/update-passwords.sql
Normal file
42
src/main/resources/sql/update-passwords.sql
Normal file
@@ -0,0 +1,42 @@
|
||||
-- 更新演示账号密码为BCrypt格式
|
||||
-- 这些是使用BCryptPasswordEncoder生成的正确哈希值
|
||||
|
||||
USE flash_sale_db;
|
||||
|
||||
-- 删除现有演示用户(如果存在)
|
||||
DELETE
|
||||
FROM users
|
||||
WHERE username IN ('demo1', 'demo2', 'admin');
|
||||
|
||||
-- 插入使用BCrypt加密的演示用户
|
||||
-- demo1/demo2 密码: 123456 (BCrypt哈希)
|
||||
-- admin 密码: admin123 (BCrypt哈希)
|
||||
INSERT INTO users (username, password, email, phone, status, created_at, updated_at)
|
||||
VALUES ('demo1', '$2a$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2uheWG/igi.', 'demo1@example.com', '13800138001', 1,
|
||||
NOW(), NOW()),
|
||||
('demo2', '$2a$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2uheWG/igi.', 'demo2@example.com', '13800138002', 1,
|
||||
NOW(), NOW()),
|
||||
('admin', '$2a$10$DOwVJZHH.5PkZKJKJKJKJOh.3VQ8nl83hq8/Qhx6.5PkZKJKJKJKJ', 'admin@example.com', '13800138000', 1,
|
||||
NOW(), NOW());
|
||||
|
||||
-- 验证插入结果
|
||||
SELECT id,
|
||||
username,
|
||||
email,
|
||||
phone,
|
||||
status,
|
||||
SUBSTRING(password, 1, 30) as password_hash_preview,
|
||||
created_at
|
||||
FROM users
|
||||
WHERE username IN ('demo1', 'demo2', 'admin')
|
||||
ORDER BY username;
|
||||
|
||||
-- 显示账号信息
|
||||
SELECT '=== 演示账号信息 ===' as info;
|
||||
SELECT CONCAT(username, ' / ', CASE
|
||||
WHEN username = 'admin' THEN 'admin123'
|
||||
ELSE '123456'
|
||||
END) as '用户名/密码'
|
||||
FROM users
|
||||
WHERE username IN ('demo1', 'demo2', 'admin')
|
||||
ORDER BY username;
|
||||
7
src/main/resources/static/images/default-product.svg
Normal file
7
src/main/resources/static/images/default-product.svg
Normal file
@@ -0,0 +1,7 @@
|
||||
<svg width="100" height="100" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="100" height="100" fill="#f8f9fa" stroke="#dee2e6" stroke-width="1"/>
|
||||
<text x="50" y="35" font-family="Arial, sans-serif" font-size="12" fill="#6c757d" text-anchor="middle">商品</text>
|
||||
<text x="50" y="50" font-family="Arial, sans-serif" font-size="12" fill="#6c757d" text-anchor="middle">图片</text>
|
||||
<text x="50" y="70" font-family="Arial, sans-serif" font-size="10" fill="#adb5bd" text-anchor="middle">暂无图片
|
||||
</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 533 B |
8
src/main/resources/static/images/ipad.svg
Normal file
8
src/main/resources/static/images/ipad.svg
Normal file
@@ -0,0 +1,8 @@
|
||||
<svg width="200" height="200" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="200" height="200" fill="#f8f9fa" stroke="#dee2e6" stroke-width="1"/>
|
||||
<rect x="60" y="40" width="80" height="120" rx="10" fill="#f0f0f0" stroke="#ccc" stroke-width="2"/>
|
||||
<rect x="65" y="50" width="70" height="90" fill="#000"/>
|
||||
<circle cx="100" cy="150" r="5" fill="#ccc"/>
|
||||
<text x="100" y="185" font-family="Arial, sans-serif" font-size="14" fill="#333" text-anchor="middle">iPad Air
|
||||
</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 501 B |
8
src/main/resources/static/images/iphone15.svg
Normal file
8
src/main/resources/static/images/iphone15.svg
Normal file
@@ -0,0 +1,8 @@
|
||||
<svg width="200" height="200" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="200" height="200" fill="#f8f9fa" stroke="#dee2e6" stroke-width="1"/>
|
||||
<rect x="50" y="30" width="100" height="140" rx="20" fill="#1d1d1f" stroke="#333" stroke-width="2"/>
|
||||
<circle cx="100" cy="50" r="8" fill="#333"/>
|
||||
<rect x="60" y="60" width="80" height="100" fill="#000"/>
|
||||
<text x="100" y="185" font-family="Arial, sans-serif" font-size="14" fill="#333" text-anchor="middle">iPhone 15
|
||||
</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 503 B |
8
src/main/resources/static/images/macbook.svg
Normal file
8
src/main/resources/static/images/macbook.svg
Normal file
@@ -0,0 +1,8 @@
|
||||
<svg width="200" height="200" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="200" height="200" fill="#f8f9fa" stroke="#dee2e6" stroke-width="1"/>
|
||||
<rect x="30" y="60" width="140" height="90" rx="5" fill="#c0c0c0" stroke="#999" stroke-width="2"/>
|
||||
<rect x="35" y="65" width="130" height="75" fill="#000"/>
|
||||
<rect x="40" y="155" width="120" height="10" rx="5" fill="#e0e0e0"/>
|
||||
<text x="100" y="185" font-family="Arial, sans-serif" font-size="14" fill="#333" text-anchor="middle">MacBook Pro
|
||||
</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 527 B |
Reference in New Issue
Block a user