Python 和 MySQL 电费计费系统如何修复 1136 错误
结论
1136 (21S01): Column count doesn't match value count at row 1 的意思是:newuser 表要接收的列数与 VALUES 中的值数量不一致。
当前语句传入了三个值:
insert into newuser values(username, password, confirmpasswd)
但 newuser 表未必正好有三列。处理这个错误时,应先确认表的实际结构,并在 INSERT 中明确指定列名。确认密码只用于核对两次输入是否一致,不应存入数据库。SQL 也应使用参数化查询,不能通过 .format() 拼接用户输入。
先检查 newuser 表
在 MySQL 中执行:
DESCRIBE newuser;
也可以查看完整的建表语句:
SHOW CREATE TABLE newuser;
假设表结构如下:
CREATE TABLE newuser (
username VARCHAR(100) NOT NULL,
password VARCHAR(255) NOT NULL
);
这张表只有两列,而程序插入了三个值,因此会触发 1136 错误。
对应的 Python 代码应改为:
sql_insert = """
INSERT INTO newuser (username, password)
VALUES (%s, %s)
"""
c1.execute(sql_insert, (username, password))
conn.commit()
confirmpasswd 无需写入数据库:
username = input("Enter your username: ")
password = input("Enter your password: ")
confirmpasswd = input("Confirm your password: ")
if password != confirmpasswd:
print("The passwords do not match.")
else:
sql_insert = """
INSERT INTO newuser (username, password)
VALUES (%s, %s)
"""
c1.execute(sql_insert, (username, password))
conn.commit()
print("User created successfully.")
如果表中还有自增主键
更常见的表结构如下:
CREATE TABLE newuser (
id INT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(100) NOT NULL UNIQUE,
password VARCHAR(255) NOT NULL
);
此时表中虽然有三列,但 id 由 MySQL 自动生成。插入时仍然只需提供 username 和 password:
c1.execute(
"""
INSERT INTO newuser (username, password)
VALUES (%s, %s)
""",
(username, password)
)
conn.commit()
不要写成:
c1.execute(
"INSERT INTO newuser VALUES (%s, %s)",
(username, password)
)
未指定列名时,MySQL 会按照表中所有列的顺序匹配值,自增的 id 也包括在内。虽然可以显式传入 NULL:
INSERT INTO newuser VALUES (NULL, 'alice', 'password');
但这种写法依赖表的列顺序。以后只要表结构发生变化,就可能再次出错,因此不推荐使用。
是否应该修改表结构
只有业务确实需要新字段时,才应修改表结构。不要为了消除 1136 错误随意增加一列。
确认密码只是界面输入,用来检查用户是否输错,不是需要长期保存的数据。合理的表结构通常会保存:
- 用户 ID
- 用户名
- 密码摘要
- 可选的创建时间等业务字段
例如:
CREATE TABLE newuser (
id INT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(100) NOT NULL UNIQUE,
password_hash VARCHAR(255) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
如果现有表结构不符合这个设计,应先确认数据迁移会产生什么影响,再修改表,而不是添加 confirmpasswd 字段。
不要使用字符串拼接生成 SQL
原代码使用了以下写法:
info1 = "insert into newuser values('{}','{}','{}')".format(
username,
password,
confirmpasswd
)
这种写法容易引发引号和类型问题,也存在 SQL 注入风险。如果用户名或密码包含单引号,SQL 还可能直接执行失败。
mysql.connector 使用 %s 作为参数占位符:
c1.execute(
"SELECT * FROM newuser WHERE username = %s AND password = %s",
(username, password)
)
删除和查询也应采用相同方式:
c1.execute(
"DELETE FROM newuser WHERE username = %s",
(username,)
)
只有一个参数时,必须写成 (username,),末尾的逗号不能省略。
修正主菜单循环条件
下面的表达式不能用于匹配 YES、yes 或 Yes:
c = 'YES' or 'yes' or 'Yes'
while c == 'YES' or 'yes' or 'Yes':
非空字符串在布尔判断中都是真值,所以这个循环条件始终成立。
可以先把输入统一转换为小写:
continue_program = "yes"
while continue_program == "yes":
# 主菜单代码
continue_program = input(
"Do you want to continue? (yes or no): "
).strip().lower()
如果需要接受多个表示”是”的值,可以使用集合:
while continue_program.strip().lower() in {"yes", "y"}:
...
V 的循环也有同样的问题:
while V == 'YES' or "yes" or 'Yes':
应改为:
while V.strip().lower() in {"yes", "y"}:
删除操作被执行了两次
原代码的问题出在这里:
info6 = c1.execute("delete from Transaction ...")
c1.execute(info6)
cursor.execute() 会立即执行 SQL,返回值通常是 None。因此,后面的代码实际上相当于:
c1.execute(None)
这会引发新的错误。
每条 SQL 只应执行一次:
c1.execute(
"DELETE FROM `Transaction` WHERE accountno = %s",
(acc,)
)
c1.execute(
"DELETE FROM AddNewCustomer WHERE accountno = %s",
(acc,)
)
c1.execute(
"DELETE FROM newuser WHERE username = %s",
(use,)
)
conn.commit()
建议配合事务和异常处理,避免只删除部分数据:
try:
c1.execute(
"DELETE FROM `Transaction` WHERE accountno = %s",
(acc,)
)
c1.execute(
"DELETE FROM AddNewCustomer WHERE accountno = %s",
(acc,)
)
c1.execute(
"DELETE FROM newuser WHERE username = %s",
(use,)
)
conn.commit()
except sql.Error as exc:
conn.rollback()
print(f"Delete failed: {exc}")
如果表之间存在外键,需要根据外键关系确定删除顺序,或者在建表时合理设置 ON DELETE CASCADE。
AddNewCustomer 也应该明确列名
下面的语句同样依赖表中列的数量和顺序:
insert into AddNewCustomer values(...)
应根据真实表结构明确指定列名。例如:
c1.execute(
"""
INSERT INTO AddNewCustomer
(
accountno,
bankname,
bankbranch,
name,
address,
areacode,
phoneno,
email,
boxid
)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s)
""",
(
accountno,
bankname,
bankbranch,
name,
address,
areacode,
phoneno,
email,
boxid
)
)
conn.commit()
这里的列名必须与 DESCRIBE AddNewCustomer; 显示的实际字段一致。
电话号码和邮政编码通常更适合用字符串保存。电话号码可能包含 +、空格或前导零,而且不参与数学运算:
areacode = input("Enter your area PIN CODE: ").strip()
phoneno = input("Enter your PHONE NUMBER: ").strip()
对应的 MySQL 字段可以使用 VARCHAR。
查询不到交易记录时,paid 未定义
原代码如下:
for row in data3:
paid = row[6]
if paid == 'yes':
如果查询结果为空,循环不会执行,paid 也就没有被赋值。后续访问它时会出现 UnboundLocalError 或 NameError。
可以直接读取一条记录,并先检查记录是否存在:
c1.execute(
"""
SELECT paid
FROM `Transaction`
WHERE accountno = %s
""",
(accountno,)
)
row = c1.fetchone()
if row is None:
print("No transaction was found for this account.")
else:
paid = str(row[0]).strip().lower()
if paid == "yes":
print("You have already paid the bill.")
else:
print("The bill has not been paid.")
直接使用 SELECT paid,比执行 SELECT * 后再依赖 row[6] 更可靠。表中的字段一旦增加或调整,固定下标很容易读到错误的数据。
正确处理退出选项和无效输入
int(input(...)) 遇到非数字输入时会抛出 ValueError。可以先读取字符串,再判断具体选项:
choice = input("ENTER YOUR CHOICE: ").strip()
if choice == "1":
create_user()
elif choice == "2":
login()
elif choice == "3":
print("Goodbye.")
break
else:
print("Invalid choice. Please enter 1, 2, or 3.")
这样无需强制转换,也能正确处理退出选项。
一个可运行的用户注册和登录示例
下面的代码统一修复了注册、登录、循环和 SQL 参数问题。运行它的前提是 newuser 表包含 username 和 password_hash 字段:
import getpass
import hashlib
import mysql.connector as sql
def hash_password(password):
return hashlib.sha256(password.encode("utf-8")).hexdigest()
conn = sql.connect(
host="localhost",
user="root",
password="your-password",
database="EBS"
)
cursor = conn.cursor()
try:
while True:
print("\n*** ELECTRICITY BILLING SYSTEM ***")
print("1. NEW USER")
print("2. EXISTING USER")
print("3. EXIT")
choice = input("ENTER YOUR CHOICE: ").strip()
if choice == "1":
username = input("Enter your username: ").strip()
password = getpass.getpass("Enter your password: ")
confirm_password = getpass.getpass("Confirm your password: ")
if not username:
print("Username cannot be empty.")
continue
if password != confirm_password:
print("The passwords do not match.")
continue
password_hash = hash_password(password)
try:
cursor.execute(
"""
INSERT INTO newuser (username, password_hash)
VALUES (%s, %s)
""",
(username, password_hash)
)
conn.commit()
print("User created successfully.")
except sql.IntegrityError:
conn.rollback()
print("That username already exists.")
except sql.Error as exc:
conn.rollback()
print(f"Unable to create user: {exc}")
elif choice == "2":
username = input("Enter your username: ").strip()
password = getpass.getpass("Enter your password: ")
password_hash = hash_password(password)
cursor.execute(
"""
SELECT id
FROM newuser
WHERE username = %s AND password_hash = %s
""",
(username, password_hash)
)
user = cursor.fetchone()
if user is None:
print("Invalid username or password.")
else:
print("Login successful.")
elif choice == "3":
print("Goodbye.")
break
else:
print("Invalid choice. Please enter 1, 2, or 3.")
finally:
cursor.close()
conn.close()
这个 SHA-256 示例比明文保存密码更安全,但仍不适合正式的密码存储。它的计算速度太快,也不会自动处理盐值。生产系统应使用专门的密码哈希算法和库,例如 Argon2、bcrypt 或 PBKDF2,并为每个密码生成独立盐值。
还需要注意的设计问题
- 选择菜单不会自动创建 MySQL 表。通常应在应用初始化或数据库迁移阶段通过
CREATE TABLE建表,而不是在用户每次选择菜单后创建。 - 不要在公开代码中写入真实的数据库密码。连接凭据应通过环境变量或安全配置提供。
deadline = dt.date(2020, 1, 30)是一个固定且已经过去的日期。如果截止日期属于账单数据,应将其存入数据库,并按对应账单读取。random不适合生成必须唯一的账户号码。至少需要在数据库中添加UNIQUE约束,并在发生冲突时重新生成。更稳妥的方式是使用数据库自增 ID 或 UUID。Transaction容易与 SQL 中的事务概念混淆。如果保留这个表名,SQL 中最好使用反引号:`Transaction`。也可以改成billing_transaction等更清楚的名称。- 每次写入、更新或删除出现异常时,都应调用
rollback()。 - 数据库应通过主键、唯一约束、外键和
NOT NULL约束保证数据完整性,不能只依靠 Python 代码中的判断。
修复 1136 错误的重点不是机械地增加或删除一个值,而是明确列出 INSERT 要写入的字段,让字段与参数一一对应,同时确保表结构准确反映实际业务数据。
备注:内容仅供参考。
