用python开发一个记账的微信公众号

简单介绍

因为本人平常有记账的习惯,所以突发奇想,想做一个可以记账的公众号。
先贴上公众号的二维码,有兴趣的朋友可以尝试一下。
超级记账小能手
具体使用方法:
1、消费记录
可以把一些日常消费按照一个格式发送给公众号,格式为"时间 消费项目 金额"或"消费项目 金额",时间和消费项目、消费项目和消费金额之间一定要加空格,例如"2.17 早餐 6"、“午餐 15”,时间格式目前只支持"月份.日期" ,如果不加时间,则默认记录在当天的消费中;消费金额以元为单位。发送消费记录到公众号之后,公众号就会帮你自己动记录。也可以一次发送多个消费记录,以换行分隔,例如下图
使用方法
2、统计消费金额
发送"统计"到公众后,公众号就会返回当月消费、当天消费、当月日均消费的情况,具体见上图。
3、将消费记录生成csv文件、日均消费趋势图
发送"表格"到公众后,公众号会返回一个链接,打开链接可以下载一个包含当月所有消费的csv文件,见下图。
发送"图表"到公众后,公众号会返回一个链接,打开链接可以看到当月的一个日均消费趋势情况,见下图。
表格和图表
表格
图表
4、撤销
发送"撤销"到公众号,可以撤销发送的上一条消费记录。*

实现方法

整个服务是通过 flask 来实现的,就是在微信公众号绑定的ip服务器上开启一个 flask 服务,来接收用户发来的信息,检查信息中是否包含之前预设好的一些关键词,来调用相应的函数实现服务。
用到的第三方库:flask、wechatpy、pymysql、DBUtils、pyecharts。
个人感觉有些逻辑实现的还是太复杂了,欢迎朋友们指出不足的地方,谢谢!

完整代码请移步:https://github.com/a596480606/wechat_public

main.py

from flask import Flask, request
from wechatpy import parse_message
from table_funs import get_day_data, gen_day_table
from wechatpy.replies import TextReply
from tools import parse_items, get_today_date, get_consume, get_avg_consume, gen_table, Mymysql, RegexConverter

server_ip = "your server ip"

# 创建 flask 应用
app = Flask(__name__)
app.url_map.converters['re'] = RegexConverter   # 通过正则匹配路由

# 暂时存放用户对象的字典,后期可替换成redis
USERS = {}

# 实例化数据库对象
DB = Mymysql()


@app.route("/<re('.?'):url>", methods=["GET", "POST"])
def index(url):
    global USERS
    print("收到请求!",url)

    # 解析收到的用户数据
    data = parse_message(request.data)
    print("data:", data)

    if not data:
        return "无效的请求!"

    # 提取出用户发送给公众号的信息和用户名
    msg = data.content
    user = data.source.replace("@", "_").replace("-","_")

    # 如果用户不在USERS中,则为其新建一张表
    if user not in USERS:
        USERS[user] = {}
        DB.create(user)

    # 用户发送的信息中包含统计
    if "统计" in msg:
        today = get_today_date()            # 获取当天日期
        all_consume = DB.select(user)       # 获取用户数据中的所有记录
        # 统计出日消费和月总消费
        day_consume, month_consume = get_consume(all_consume,  today)
        avg_consume = get_avg_consume(month_consume)
        reply_text = "今日消费:" + str(day_consume) + "元\n当月消费:" + str(month_consume) + "元\n日均消费:" + str(avg_consume) + "元"
        return TextReply(content=reply_text, message=data).render()

    # 用户发送的信息中包含表格
    elif "表格" in msg:
        all_consume = DB.select(user)          # 获取数据库中关于用户的所有记录
        path = f"http://{server_ip}:8015/" + gen_table(user, all_consume)
        reply_text = path
        return TextReply(content=reply_text, message=data).render()

    # 用户发送的信息中包含撤销
    elif "撤销" in msg:
        res = DB.get_last_record(user)          # 获取数据库中关于用户的最后一条记录
        if not res:
            return TextReply(content="暂无数据可撤销!", message=data).render()
        last_record = eval(res)
        reply_text = f"已撤销  {' '.join(last_record)} 的记录!"
        DB.drop_a_record(user)
        return TextReply(content=reply_text, message=data).render()

    # 用户发送的信息中包含图表
    elif '图表' in msg:
        dates, values = get_day_data(user, DB)
        gen_day_table(user,dates,values)
        url = f"http://{server_ip}:8015/table/{user}"
        return TextReply(content=url, message=data).render()

    # 切割提取用户发送的消费信息
    try:
        items = [item for item in msg.split("\n")]
        items = [[xs for xs in s.split(" ") if xs] for s in items if s]

    except:
        print("split error")
        reply_text = "数据格式不正确!"
        return TextReply(content=reply_text, message=data).render()

    # 向数据库插入记录成功的数据,并返回记录失败的数据
    results = "以下数据记录失败:\n"
    for num, item in enumerate(items):
        res = parse_items(item, user, DB)
        print(res)
        if res != "已记录!":
            results += "  ".join(item) + "\n"

    if results == "以下数据记录失败:\n":
        reply_text = "记录成功!"
    else:
        reply_text = results

    xml = TextReply(content=reply_text, message=data).render()
    
    return xml

if __name__ == '__main__':
    app.run(host="0.0.0.0", port=80)

主要的工具函数
tools.py

"""
    main.py 用到的函数集合
"""
import csv
import time
import pymysql
import datetime
from DBUtils.PooledDB import PooledDB
from werkzeug.routing import BaseConverter


class RegexConverter(BaseConverter):
    def __init__(self, url_map, *args):
        super(RegexConverter, self).__init__(url_map)
        # 将接受的第1个参数当作匹配规则进行保存
        self.regex = args[0]

def parse_items(item, user, DB):
    """
        处理用户消费信息的函数
    :param item: 消费记录
    :param user: 用户名
    :param DB:  数据库
    :return:
    """
    if len(item) == 2:
        today = get_today_date()

        if item[0].split(".")[0].isdigit():
            reply_text = "格式错误!请输入例如:早餐 12"
            return reply_text

        if is_number(item[1]):
            item.insert(0, today)
            DB.insert(user,str(item))
            reply_text = "已记录!"
        else:
            reply_text = "格式错误!请输入例如:早餐 12"

    elif len(item) == 3:
        today = format_date(item[0])

        if not today:
            reply_text = "日期格式不正确!请输入例如:12.2 或 12.02"

            return reply_text

        if is_number(item[2]):
            reply_text = "已记录!"
            item[0] = today
            DB.insert(user,str(item))
        else:
            reply_text = "格式错误!请输入例如:早餐 12"
    else:
        reply_text = "暂不支持其他消息,请见谅!"

    return reply_text


def get_today_date(detail=False):
    """detail 日期细节,默认False,返回到日, 为 s 返回 到时分秒"""
    if not detail:
        t = time.strftime("%Y%m%d", time.localtime())
        return t

    if detail == "s":
        t = time.strftime("%Y%m%d%H%M%S", time.localtime())
        return t


def time_to_timestmap(t):
    """把 类似 20191122 格式的时间转为时间戳"""
    new_t = time.mktime(time.strptime(t, "%Y%m%d"))
    return round(new_t)


def get_current_month():
    """返回当前月份的第一天的日期 20191201"""
    year = datetime.datetime.today().year
    month = datetime.datetime.today().month
    tomonth = str(year) + str(month) + "01"
    return tomonth


def get_consume(all_consume, today):
    """
        统计用户消费的函数
    :param all_consume: 数据库中关于用户的所有记录
    :param today: 当天日期
    :return: 日总消费,月总消费
    """
    tomonth = get_current_month()       # 当月第一天的日期
    month_consume = 0                   # 月总消费
    today_consume = 0                   # 日总消费
    # 遍历数据库中关于用户的所有记录
    for consume in all_consume:
        record = eval(consume[0])
        consume_day = record[0]
        fee = float(record[2])
        if consume_day == today:
            today_consume += fee
        if time_to_timestmap(consume_day) >= time_to_timestmap(tomonth):
            month_consume += float(fee)
    return round(today_consume,2), round(month_consume,2)


def get_avg_consume(month_consume):
    """生成月平均消费的函数"""
    day = datetime.datetime.today().day
    avg_consume = round(month_consume/day,2)
    return avg_consume


def format_date(s):
    year = str(datetime.datetime.today().year)
    if "." in s:
        sl = s.split(".")
    else:
        return False
    if len(sl) != 2:
        return False
    month, day = sl
    if month.isdigit() and day.isdigit():
        month, day = int(month),  int(day)
    else:
        return False
    if 0 < month < 10:
        month = "0" + str(month)
    elif 10 <= month < 13:
        month = str(month)
    else:
        return False

    every_month_last_day = 31
    if int(month) in [1,3,5,7,8,10,12]:
        every_month_last_day = 32

    if 0 < day < 10:
        day = "0" + str(day)
    elif 10 <= day < every_month_last_day:
        day = str(day)
    else:
        return False
    return year + month + day


def is_number(string):
    try:
        float(string)
    except ValueError:
        return False
    return string


def gen_table(user, all_consume):
    """
        生产用户消费表格的函数
    :param user:    用户名
    :param all_consume: 获取数据库中关于用户的所有记录
    :return: 用户消费表格的路径
    """
    path = f"{user}_details.csv"
    with open(f"./forms/{user}_details.csv", "w", encoding="utf8", newline="" ) as f:
        csvf = csv.writer(f, delimiter=",")
        for consume in all_consume:
            record = eval(consume[0])
            csvf.writerow(record)
    return path



class Mymysql:
    """
        mysql数据库类
    """
    def __init__(self):
        # 通过 PooledDB 生成一个连接数最大为10的连接池
        self.pool = PooledDB(pymysql, 10, host="127.0.0.1", port=3306, user="root", password="123456", db="accounts")

    def create(self, user):
        self.conn = self.pool.connection()
        self.cursor = self.conn.cursor()
        # self.conn.cursor.execute("""DROP TABLE IF EXISTS {};""".format(user))
        sql = """create table IF NOT EXISTS {}( content varchar(50), id INT(20) AUTO_INCREMENT, primary key(id));""".format(user)
        print(sql)
        self.conn.ping(reconnect=True)
        self.cursor.execute(sql)
        self.cursor.close()
        self.conn.close()

    def insert(self, user, value):
        self.conn = self.pool.connection()
        self.cursor = self.conn.cursor()
        sql = """insert into {}(content) value ("{}");""".format(user,value)
        print(sql)
        self.conn.ping(reconnect=True)
        self.cursor.execute(sql)
        self.conn.commit()
        self.cursor.close()
        self.conn.close()

    def get_last_record(self, user):
        self.conn = self.pool.connection()
        self.cursor = self.conn.cursor()
        sql = """select * from {} order by id desc limit 1;""".format(user)
        print(sql)
        self.conn.ping(reconnect=True)
        self.cursor.execute(sql)
        try:
            res = self.cursor.fetchall()[0][0]
        except IndexError:
            return None
        print(type(res))
        print(res)
        self.cursor.close()
        self.conn.close()
        return res

    def drop_a_record(self, user):
        self.conn = self.pool.connection()
        self.cursor = self.conn.cursor()
        # sql = """delete from {} where id = (select id from {} Limit (count-1),1);""".format(user,user)
        sql = """delete from {}  order by id desc limit 1;""".format(user)
        print(sql)
        self.conn.ping(reconnect=True)
        self.cursor.execute(sql)
        self.conn.commit()
        self.cursor.close()
        self.conn.close()

    def select(self,user):
        self.conn = self.pool.connection()
        self.cursor = self.conn.cursor()
        sql = """select * from {};""".format(user)
        print(sql)
        self.conn.ping(reconnect=True)
        self.cursor.execute(sql)
        try:
            res = self.cursor.fetchall()
        except IndexError:
            return None
        self.cursor.close()
        self.conn.close()
        return res
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值