PAYATHON 2026

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 要写入的字段,让字段与参数一一对应,同时确保表结构准确反映实际业务数据。

备注:内容仅供参考。