环境:原有进销存系统,使用的数据库为sqlserver。网页端操作时只能逐条输入保存数据。使用python编写程序实现批量导入excel条目,增加效率。
创建sql用户
使用ssms登录原有数据库,创建一个账号供外部登录用,给该用户读写权限




随后一直点确定即可 使用Navicat等软件登录数据库,测试用户创建是否成功
数据表结构分析

结论:进销存信息都在SS_InOutProduct这张表里,写入数据时只需要针对这一张表进行操作
查看需要用到的表头
整理成excel模板

python程序
DB_CONFIG填写上面创建的用户和数据库信息,数据表为SS_SaleReport
from flask import Flask, request, render_template
import pandas as pd
import pyodbc
import os
app = Flask(__name__)
# ---------------- 上传文件夹 ----------------
UPLOAD_FOLDER = "/root/sales/uploads"
os.makedirs(UPLOAD_FOLDER, exist_ok=True) # 创建目录
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
# ---------------- 数据库配置 ----------------
DB_CONFIG = {
"server": "10.0.0.252",
"database": "SS_SaleReport",
"username": "user01",
"password": "Xhnetwork@123"
}
def get_conn():
conn_str = (
"DRIVER={ODBC Driver 17 for SQL Server};"
f"SERVER={DB_CONFIG['server']};"
f"DATABASE={DB_CONFIG['database']};"
f"UID={DB_CONFIG['username']};"
f"PWD={DB_CONFIG['password']}"
)
return pyodbc.connect(conn_str, autocommit=True)
# ---------------- 辅助函数 ----------------
def safe_float(value):
try:
if pd.isna(value):
return None
return float(value)
except:
return None
def is_valid_row(row):
try:
float_cols = ['InCount', 'InPrice', 'OutCount', 'OutPrice', 'LaborCost', 'EarnMoney']
for col in float_cols:
val = row.get(col, None)
if pd.isna(val) or val == '':
continue
float(val)
date_cols = ['InProductDate', 'OutProduceDate']
for col in date_cols:
val = row.get(col, None)
if pd.isna(val) or val == '':
continue
pd.to_datetime(val)
text_cols = ['InOrderNum', 'ProductType', 'InUnit', 'OutOrderNum', 'OutUnit', 'SalesMan']
for col in text_cols:
val = row.get(col, None)
if pd.isna(val) or str(val).strip() == '':
raise ValueError(f"{col} 不能为空")
return True
except:
return False
# ---------------- 上传 Excel ----------------
@app.route("/", methods=["GET", "POST"])
def upload_file():
if request.method == "POST":
file = request.files.get("file")
if not file or not file.filename.endswith(".xlsx"):
return "请上传 .xlsx 文件"
# 保存到可写目录
file_path = os.path.join(app.config['UPLOAD_FOLDER'], file.filename)
file.save(file_path)
try:
df = pd.read_excel(file_path, header=1, engine='openpyxl')
except Exception as e:
return f"读取 Excel 文件失败: {e}"
try:
conn = get_conn()
cursor = conn.cursor()
except Exception as e:
return f"数据库连接失败: {e}"
inserted = 0
failed_rows = []
for index, row in df.iterrows():
if not is_valid_row(row):
failed_rows.append((index + 3, "数据无效"))
continue
try:
cursor.execute("""
INSERT INTO SS_InOutProduct
(InProductDate, InOrderNum, ProductType, InCount, InPrice, InUnit,
OutProduceDate, OutPrice, OutCount, OutOrderNum, OutUnit,
LaborCost, SalesMan, EarnMoney)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
row['InProductDate'], row['InOrderNum'], row['ProductType'], safe_float(row['InCount']),
safe_float(row['InPrice']), row['InUnit'], row['OutProduceDate'], safe_float(row['OutPrice']),
safe_float(row['OutCount']), row['OutOrderNum'], row['OutUnit'], safe_float(row['LaborCost']),
row['SalesMan'], safe_float(row['EarnMoney'])
)
inserted += 1
except Exception as e:
failed_rows.append((index + 3, str(e)))
cursor.close()
conn.close()
result = f"成功导入 {inserted} 条记录!"
if failed_rows:
result += "<br>失败行:<br>" + "<br>".join([f"第{r[0]}行: {r[1]}" for r in failed_rows])
return result
return render_template("form.html")
# ---------------- 启动 ----------------
if __name__ == "__main__":
app.run(host="0.0.0.0", port=5000, debug=False)
再写一个前端页面
<!doctype html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>库存管理系统 | 登录</title>
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@300;400;500;700&display=swap" rel="stylesheet">
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: 'Noto Sans SC', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
}
body {
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 20px;
}
/* 登录表单样式 */
.login-container {
background: rgba(255, 255, 255, 0.15);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
border-radius: 16px;
padding: 2.5rem;
width: 100%;
max-width: 400px;
box-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.37);
border: 1px solid rgba(255, 255, 255, 0.18);
}
.login-title {
color: white;
text-align: center;
margin-bottom: 2rem;
font-size: 1.8rem;
font-weight: 600;
}
.form-group {
margin-bottom: 1.5rem;
}
.form-group label {
display: block;
color: rgba(255, 255, 255, 0.9);
margin-bottom: 0.5rem;
font-size: 0.95rem;
}
.form-control {
width: 100%;
padding: 0.8rem 1rem;
border: 1px solid rgba(255, 255, 255, 0.3);
background: rgba(255, 255, 255, 0.1);
border-radius: 8px;
color: white;
font-size: 1rem;
transition: all 0.3s ease;
}
.form-control:focus {
outline: none;
border-color: rgba(255, 255, 255, 0.6);
background: rgba(255, 255, 255, 0.15);
}
.form-control::placeholder {
color: rgba(255, 255, 255, 0.6);
}
.btn-login {
width: 100%;
padding: 0.9rem;
background: #4f46e5;
color: white;
border: none;
border-radius: 8px;
font-size: 1rem;
font-weight: 500;
cursor: pointer;
transition: all 0.3s ease;
margin-top: 0.5rem;
}
.btn-login:hover {
background: #4338ca;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.error-message {
color: #ff6b6b;
font-size: 0.9rem;
margin-top: 0.5rem;
text-align: center;
min-height: 1.2rem;
}
/* 主内容区域 - 默认隐藏 */
.main-content {
display: none;
width: 100%;
max-width: 800px;
margin: 0 auto;
padding: 0 20px;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
body {
min-height: 100vh;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
display: flex;
justify-content: center;
align-items: center;
padding: 20px;
}
.container {
background: var(--glass-bg);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
border-radius: 16px;
border: 1px solid var(--glass-border);
box-shadow: var(--glass-shadow);
padding: 2.5rem;
width: 100%;
max-width: 480px;
margin: 0 auto;
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
color: white;
}
h1 {
margin-bottom: 1.5rem;
font-weight: 600;
font-size: 1.875rem;
}
.upload-area {
border: 2px dashed rgba(255, 255, 255, 0.5);
border-radius: 12px;
padding: 2.5rem 1.5rem;
margin-bottom: 1.5rem;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 200px;
}
.upload-area:hover {
border-color: white;
background: rgba(255, 255, 255, 0.1);
}
.upload-icon {
font-size: 4rem;
margin-bottom: 1.25rem;
color: rgba(255, 255, 255, 0.9);
line-height: 1;
}
.upload-text {
margin-bottom: 1rem;
color: rgba(255, 255, 255, 0.9);
}
.file-input {
display: none;
}
.file-name {
margin-top: 1rem;
font-size: 0.875rem;
color: rgba(255, 255, 255, 0.8);
word-break: break-all;
}
.submit-btn {
background: var(--primary-color);
color: white;
border: none;
padding: 0.75rem 2rem;
border-radius: 8px;
font-size: 1rem;
font-weight: 500;
cursor: pointer;
transition: all 0.3s ease;
width: 100%;
max-width: 200px;
margin: 0 auto;
display: block;
backdrop-filter: blur(5px);
}
.submit-btn:hover {
background: var(--primary-hover);
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.submit-btn:disabled {
opacity: 0.7;
cursor: not-allowed;
transform: none;
box-shadow: none;
}
/* 弹窗样式 */
.modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
backdrop-filter: blur(5px);
z-index: 1000;
justify-content: center;
align-items: center;
}
.modal-content {
background: var(--glass-bg);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
border: 1px solid var(--glass-border);
border-radius: 12px;
padding: 2rem;
max-width: 400px;
width: 90%;
text-align: center;
color: white;
box-shadow: var(--glass-shadow);
}
.modal-buttons {
display: flex;
justify-content: center;
gap: 1rem;
margin-top: 1.5rem;
}
.modal-btn {
padding: 0.5rem 1.5rem;
border: none;
border-radius: 6px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
}
.confirm-btn {
background: var(--primary-color);
color: white;
}
.confirm-btn:hover {
background: var(--primary-hover);
transform: translateY(-1px);
}
.cancel-btn {
background: rgba(255, 255, 255, 0.1);
color: white;
}
.cancel-btn:hover {
background: rgba(255, 255, 255, 0.2);
}
/* 进度条样式 */
.progress-container {
display: none;
width: 100%;
background: rgba(255, 255, 255, 0.1);
border-radius: 10px;
margin: 1.5rem 0;
overflow: hidden;
}
.progress-bar {
height: 10px;
background: var(--primary-color);
width: 0%;
border-radius: 10px;
transition: width 0.3s ease;
}
.progress-text {
margin-top: 0.5rem;
font-size: 0.875rem;
color: rgba(255, 255, 255, 0.9);
text-align: left;
line-height: 1.5;
}
.success {
color: #4ade80;
}
.error {
color: #f87171;
}
</style>
</head>
<body>
<div id="app">
<!-- 登录表单 -->
<div class="login-container" id="loginContainer">
<h1 class="login-title">库存管理系统</h1>
<form id="loginForm">
<div class="form-group">
<label for="username">用户名</label>
<input type="text" id="username" class="form-control" placeholder="请输入用户名" required>
</div>
<div class="form-group">
<label for="password">密码</label>
<input type="password" id="password" class="form-control" placeholder="请输入密码" required>
</div>
<div class="error-message" id="errorMessage"></div>
<button type="submit" class="btn-login">登 录</button>
</form>
</div>
<!-- 主内容区域 -->
<div class="main-content" id="mainContent" style="display: none;">
<div id="confirmModal" class="modal">
<div class="modal-content">
<h3>确认上传</h3>
<p>确定要上传 <span id="confirmFileName"></span> 吗?</p>
<div class="modal-buttons">
<button id="cancelUpload" class="modal-btn cancel-btn">取消</button>
<button id="confirmUpload" class="modal-btn confirm-btn">确认上传</button>
</div>
</div>
</div>
<div class="container">
<h1>库存管理系统</h1>
<form id="uploadForm" action="/" method="post" enctype="multipart/form-data">
<label class="upload-area" id="dropZone">
<div class="upload-icon" style="font-size: 3.5rem;">📁</div>
<div class="upload-text">点击或拖拽文件到此处上传</div>
<div style="margin-top: 0.5rem; font-size: 0.9rem; opacity: 0.9;">支持 .xlsx 格式</div>
<input type="file" id="fileInput" name="file" accept=".xlsx" class="file-input" required>
<div id="fileName" class="file-name"></div>
</label>
<!-- 上传进度条 -->
<div id="progressContainer" class="progress-container">
<div id="progressBar" class="progress-bar"></div>
<div id="progressText" class="progress-text">上传中: 0%</div>
</div>
<button type="button" class="submit-btn" id="submitBtn" disabled>上传文件</button>
</form>
</div>
</div>
<script>
// 登录功能
document.addEventListener('DOMContentLoaded', function() {
const loginForm = document.getElementById('loginForm');
const loginContainer = document.getElementById('loginContainer');
const mainContent = document.getElementById('mainContent');
const errorMessage = document.getElementById('errorMessage');
// 正确的凭据
const CORRECT_USERNAME = '李艳';
const CORRECT_PASSWORD = '111111';
// 检查是否已经登录
if (localStorage.getItem('isLoggedIn') === 'true') {
if (loginContainer) loginContainer.style.display = 'none';
if (mainContent) mainContent.style.display = 'block';
document.title = '库存管理系统 | 文件上传';
return;
} else {
if (mainContent) mainContent.style.display = 'none';
}
// 登录表单提交
if (loginForm) {
loginForm.addEventListener('submit', function(e) {
e.preventDefault();
const username = document.getElementById('username')?.value.trim() || '';
const password = document.getElementById('password')?.value || '';
// 验证凭据
if (username === CORRECT_USERNAME && password === CORRECT_PASSWORD) {
// 登录成功
localStorage.setItem('isLoggedIn', 'true');
if (loginContainer) loginContainer.style.display = 'none';
if (mainContent) mainContent.style.display = 'block';
document.title = '库存管理系统 | 文件上传';
} else {
// 登录失败
if (errorMessage) {
errorMessage.textContent = '用户名或密码错误';
const passwordInput = document.getElementById('password');
if (passwordInput) passwordInput.value = '';
setTimeout(() => {
errorMessage.textContent = '';
}, 3000);
}
}
});
}
const dropZone = document.getElementById('dropZone');
const fileInput = document.getElementById('fileInput');
const fileName = document.getElementById('fileName');
const submitBtn = document.getElementById('submitBtn');
const form = document.getElementById('uploadForm');
// 处理拖放
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
dropZone.addEventListener(eventName, preventDefaults, false);
});
function preventDefaults(e) {
e.preventDefault();
e.stopPropagation();
}
['dragenter', 'dragover'].forEach(eventName => {
dropZone.addEventListener(eventName, highlight, false);
});
['dragleave', 'drop'].forEach(eventName => {
dropZone.addEventListener(eventName, unhighlight, false);
});
function highlight() {
dropZone.style.borderColor = 'white';
dropZone.style.background = 'rgba(255, 255, 255, 0.15)';
}
function unhighlight() {
dropZone.style.borderColor = 'rgba(255, 255, 255, 0.5)';
dropZone.style.background = 'transparent';
}
// 初始化文件输入事件
fileInput.addEventListener('change', function(e) {
e.stopPropagation();
if (this.files && this.files[0]) {
handleFileSelection(this.files[0]);
}
}, false);
// 处理拖放区域点击
dropZone.addEventListener('click', () => fileInput.click());
// 处理文件拖放
dropZone.addEventListener('dragover', function(e) {
e.preventDefault();
e.stopPropagation();
highlight();
});
dropZone.addEventListener('dragleave', function(e) {
e.preventDefault();
e.stopPropagation();
unhighlight();
});
dropZone.addEventListener('drop', function(e) {
e.preventDefault();
e.stopPropagation();
unhighlight();
const files = e.dataTransfer.files;
if (files.length > 0) {
const file = files[0];
// 更新文件输入框
const dataTransfer = new DataTransfer();
dataTransfer.items.add(file);
fileInput.files = dataTransfer.files;
// 处理文件
handleFileSelection(file);
}
});
// 获取弹窗和按钮元素
const modal = document.getElementById('confirmModal');
const confirmBtn = document.getElementById('confirmUpload');
const cancelBtn = document.getElementById('cancelUpload');
const confirmFileName = document.getElementById('confirmFileName');
const progressContainer = document.getElementById('progressContainer');
const progressBar = document.getElementById('progressBar');
const progressText = document.getElementById('progressText');
let selectedFile = null;
// 修改提交按钮类型为button,防止直接提交表单
submitBtn.type = 'button';
// 处理文件选择
function handleFileSelection(file) {
if (!file) return false;
if (!file.name.endsWith('.xlsx')) {
alert('请上传 .xlsx 格式的文件');
fileInput.value = ''; // 清空输入框
fileName.textContent = '';
submitBtn.disabled = true;
return false;
}
// 更新UI
fileName.textContent = `已选择: ${file.name}`;
submitBtn.disabled = false;
// 显示确认弹窗
confirmFileName.textContent = file.name;
modal.style.display = 'flex';
return true;
}
// 点击上传按钮时处理
submitBtn.addEventListener('click', function() {
// 如果已经在上传中,则不再处理点击
if (submitBtn.disabled) return;
// 检查是否选择了文件
if (!fileInput.files || !fileInput.files[0]) {
// 如果没有选择文件,触发文件选择
fileInput.click();
} else {
// 如果已经选择了文件,直接显示确认弹窗
confirmFileName.textContent = fileInput.files[0].name;
modal.style.display = 'flex';
}
});
// 文件选择变化时处理
fileInput.addEventListener('change', function() {
if (this.files && this.files[0]) {
handleFileSelection(this.files[0]);
}
});
// 确认上传
confirmBtn.addEventListener('click', async function() {
modal.style.display = 'none';
submitBtn.disabled = true;
submitBtn.textContent = '上传中...';
// 显示进度条
progressContainer.style.display = 'block';
progressBar.style.width = '0%';
progressText.textContent = '上传中: 0%';
try {
// 创建FormData对象
const formData = new FormData();
formData.append('file', fileInput.files[0]);
// 使用fetch API进行上传,支持进度跟踪
const xhr = new XMLHttpRequest();
// 设置请求头
xhr.open('POST', '/', true);
xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
// 进度事件处理
xhr.upload.addEventListener('progress', function(e) {
if (e.lengthComputable) {
const percentComplete = Math.round((e.loaded / e.total) * 100);
progressBar.style.width = percentComplete + '%';
progressText.textContent = `上传中: ${percentComplete}%`;
}
});
// 加载完成处理
xhr.onload = function() {
if (xhr.status >= 200 && xhr.status < 300) {
// 上传成功
progressBar.style.width = '100%';
progressText.textContent = '上传完成!';
// 立即更新按钮状态
submitBtn.disabled = false;
submitBtn.textContent = '上传文件';
// 检查是否是重定向响应
const contentType = xhr.getResponseHeader('Content-Type') || '';
try {
// 尝试解析JSON响应
if (contentType.includes('application/json') && xhr.responseText) {
const response = JSON.parse(xhr.responseText);
let resultHtml = '上传完成!<br>';
if (response.success !== undefined) {
resultHtml += `成功: ${response.success}条<br>`;
}
if (response.failed !== undefined) {
resultHtml += `失败: ${response.failed}条`;
if (response.errors && response.errors.length > 0) {
resultHtml += '<br>错误信息:<br>' + response.errors.join('<br>');
}
} else if (response.message) {
resultHtml += response.message;
}
progressText.innerHTML = resultHtml;
} else {
// 非JSON响应,直接显示文本
if (xhr.status >= 300 && xhr.status < 400) {
// 如果是重定向响应,2秒后刷新页面
setTimeout(() => {
window.location.reload();
}, 2000);
} else if (xhr.responseText) {
// 显示服务器返回的文本
progressText.innerHTML = `上传完成!<br>${xhr.responseText}`;
} else {
progressText.textContent = '上传完成!';
}
}
} catch (e) {
console.error('处理响应时出错:', e);
progressText.textContent = '上传完成!';
}
// 10秒后重置UI元素(让用户有足够时间查看上传结果)
setTimeout(() => {
form.reset();
progressContainer.style.display = 'none';
fileName.textContent = '';
submitBtn.disabled = true;
}, 10000);
} else {
// 上传失败
let errorMsg = `上传失败: ${xhr.status} ${xhr.statusText}`;
try {
const errorResponse = JSON.parse(xhr.responseText);
if (errorResponse.error) {
errorMsg = errorResponse.error;
}
} catch (e) {
// 忽略JSON解析错误
}
progressText.textContent = errorMsg;
progressBar.style.width = '0%';
submitBtn.disabled = false;
submitBtn.textContent = '重新上传';
}
};
// 错误处理
xhr.onerror = function() {
progressText.textContent = '网络错误,请检查连接';
progressBar.style.width = '0%';
submitBtn.disabled = false;
submitBtn.textContent = '重新上传';
};
// 发送请求
xhr.send(formData);
} catch (error) {
console.error('上传出错:', error);
progressText.textContent = error.message || '上传失败,请重试';
submitBtn.disabled = false;
submitBtn.textContent = '重新上传';
}
});
// 取消上传
cancelBtn.addEventListener('click', function() {
modal.style.display = 'none';
});
// 点击弹窗外部关闭
window.addEventListener('click', function(e) {
if (e.target === modal) {
modal.style.display = 'none';
}
});
}); // 关闭DOMContentLoaded事件监听器
</script>
</body>
</html>在linux上运行python程序 我这里是centos7,并设置开机自启
Linux 安装依赖
sudo yum install python3-devel gcc unixODBC-devel -y
pip3 install flask pandas pyodbc openpyxl
pip3 install xlrd
创建上传目录:
mkdir -p /root/sales/uploads
chmod 777 /root/sales/uploads
后台运行程序:
nohup python3 /root/sales/sales.py > /root/sales/sales.log 2>&1 &
创建 Python 脚本的 systemd 服务文件
sudo nano /etc/systemd/system/sales.service
[Unit]
Description=Sales Python Service
After=network.target
[Service]
Type=simple
User=root
WorkingDirectory=/root/sales
ExecStart=/usr/bin/python3 /root/sales/sales.py
Restart=always
RestartSec=5
StandardOutput=file:/root/sales/sales.log
StandardError=file:/root/sales/sales.err
[Install]
WantedBy=multi-user.target
重新加载 systemd
sudo systemctl daemon-reload
设置开机自启
sudo systemctl enable sales.service
启动服务
sudo systemctl start sales.service
查看状态和日志
sudo systemctl status sales.service
cat /root/sales/sales.log
cat /root/sales/sales.err测试在内网浏览器访问http://10.0.0.131:5000 (如无法访问端口请优先检查防火墙设置)

服务正常启用,至此批量导入的程序已全部部署完成
优化
为了在用户端更便于使用。省去手动访问
http://10.0.0.131:5000来使用,需要把页面以超链接的方式嵌入到现有进销存系统中。现有进销存页面,在下面增加一个导入按钮,超链接到
http://10.0.0.131:5000
根据url查看需要修改的页面为
SS_BusinessManage.aspx
在网站根目录找到该文件
链接的页面是
SS_InOutProduct.aspx
找到该页面新增一个导入按钮
<asp:HyperLink ID="hlkOpen" runat="server" Text="导 入" NavigateUrl="http://10.0.0.131:5000/" Target="_blank" CssClass="ButtonCss80"> </asp:HyperLink>保存并
iisreset重启IIS服务
添加完成
点击后跳转到
http://10.0.0.131:5000







