设计背景

小区物业费用缴纳,公寓租金收取,出租屋租金收取等场景,大部分都是靠人工收取,而广大业主和住户可能时间或者工作繁忙,不能到现场付款,那么设计一个这样的小程序,方便物业公司进行账务收款,账务统计,节省人力物力成本,不论在家中或者外地,都可以通过小程序直接缴费,不受时间、地点约束,操作简单,方便快捷

概要设计

image.png

数据字典


SheetModel.DB_STRUCTURE = {
    _pid: 'string|true',
    SHEET_ID: 'string|true',

    SHEET_TITLE: 'string|true|comment=标题',
    SHEET_STATUS: 'int|true|default=1|comment=状态 0=未启用,1=使用中',

    SHEET_CATE_ID: 'string|true|default=0|comment=分类',
    SHEET_CATE_NAME: 'string|false|comment=分类冗余',

    SHEET_ORDER: 'int|true|default=9999',
    SHEET_VOUCH: 'int|true|default=0',

    SHEET_FORMS: 'array|true|default=[]',
    SHEET_OBJ: 'object|true|default={}',

    SHEET_QR: 'string|false',
    SHEET_VIEW_CNT: 'int|true|default=0',


    SHEET_CNT: 'int|true|default=0',
    SHEET_PAY_CNT: 'int|true|default=0',
    SHEET_WAIT_CNT: 'int|true|default=0',
    SHEET_NO_CNT: 'int|true|default=0',

    SHEET_FEE: 'int|true|default=0',
    SHEET_PAY_FEE: 'int|true|default=0',
    SHEET_WAIT_FEE: 'int|true|default=0',

    SHEET_ADD_TIME: 'int|true',
    SHEET_EDIT_TIME: 'int|true',
    SHEET_ADD_IP: 'string|false',
    SHEET_EDIT_IP: 'string|false',
};
SheetDataModel.DB_STRUCTURE = {
    _pid: 'string|true',
    SHEET_DATA_ID: 'string|true', 
    SHEET_DATA_SHEET_ID: 'string|true|comment=FK',
    SHEET_DATA_SHEET_TITLE: 'string|false',


    SHEET_DATA_PAY_TRADE_NO: 'string|false|comment=商家订单号 32位',
    SHEET_DATA_PAY_STATUS: 'int|true|default=0|comment=支付状态 0=未支付 1=已支付 99=无需支付',
    SHEET_DATA_PAY_FEE: 'int|true|default=0|comment=已支付费用 分',
    SHEET_DATA_PAY_TIME: 'int|true|default=0|comment=支付时间',

    SHEET_DATA_NAME: 'string|false|姓名',
    SHEET_DATA_MOBILE: 'string|false|手机号',
    SHEET_DATA_FEE: 'int|true|default=0|comment=需支付费用 分',

    SHEET_DATA_FORMS: 'array|true|default=[]',
    SHEET_DATA_OBJ: 'object|true|default={}',

    SHEET_DATA_ADD_TIME: 'int|true',
    SHEET_DATA_EDIT_TIME: 'int|true',
    SHEET_DATA_ADD_IP: 'string|false',
    SHEET_DATA_EDIT_IP: 'string|false',
};

关键实现


    async getMySheetChartList(userId, time = 180) {
        let where = {};
        if (!config.IS_DEMO) {
            let user = await UserModel.getOne({ USER_MINI_OPENID: userId });
            if (!user) return null;

            where = {
                SHEET_DATA_PAY_STATUS: 1,
                SHEET_DATA_NAME: user.USER_NAME,
                SHEET_DATA_MOBILE: user.USER_MOBILE,
                'sheet.SHEET_STATUS': SheetModel.STATUS.COMM
            };
        }
        else {
            where = {
                SHEET_DATA_PAY_STATUS: 1,
                SHEET_DATA_NAME: 'Tom',
                SHEET_DATA_MOBILE: '14600000000',
                'sheet.SHEET_STATUS': SheetModel.STATUS.COMM
            };
        }


        time = Number(time);
        time = this._timestamp - time * 86400 * 1000;

        where.SHEET_DATA_PAY_TIME = ['>=', time];

        let fields = 'SHEET_DATA_PAY_TIME,SHEET_DATA_FORMS,SHEET_DATA_FEE,sheet.SHEET_TITLE ';

        let orderBy = {
            'SHEET_DATA_PAY_TIME': 'desc',
            'SHEET_DATA_ADD_TIME': 'desc'
        };

        let joinParams = {
            from: SheetModel.CL,
            localField: 'SHEET_DATA_SHEET_ID',
            foreignField: '_id',
            as: 'sheet',
        };

        let list = await SheetDataModel.getListJoin(joinParams, where, fields, orderBy, 1, 200, false);
        list = list.list;

        if (list.length == 0) return null;

        let categories = [];
        let data = [];

        for (let k = 0; k < list.length; k++) {
            data.push(Number(list[k].SHEET_DATA_FEE / 100));
            categories.push(list[k].sheet.SHEET_TITLE.substr(0, 15));

            list[k].SHEET_DATA_PAY_TIME = timeUtil.timestamp2Time(list[k].SHEET_DATA_PAY_TIME);
        }
        return { categories, data, list: list.reverse() };

    }

    // 取得账单详情
    async getMySheetDataDetail(userId, sheetDataId) {
        let where = {};

        if (!config.IS_DEMO) {
            let user = await UserModel.getOne({ USER_MINI_OPENID: userId, USER_STATUS: UserModel.STATUS.COMM });
            if (!user) this.AppError('用户不存在或者状态异常');

            where = {
                _id: sheetDataId,
                SHEET_DATA_NAME: user.USER_NAME,
                SHEET_DATA_MOBILE: user.USER_MOBILE,
            }
        }
        else {
            where = {
                _id: sheetDataId,
                SHEET_DATA_NAME: 'Tom',
                SHEET_DATA_MOBILE: '14600000000',
            }
        }

        let sheetData = await SheetDataModel.getOne(where);
        if (!sheetData) return null;

        let sheet = await SheetModel.getOne({ _id: sheetData.SHEET_DATA_SHEET_ID, SHEET_STATUS: SheetModel.STATUS.COMM });
        if (!sheet) this.AppError('支付项目不存在');

        sheetData.sheet = sheet;
        return sheetData;
    }

    /** 取得我的分页列表 */
    async getMySheetDataList(userId, {
        search, // 搜索条件
        sortType, // 搜索菜单
        sortVal, // 搜索菜单
        orderBy, // 排序 
        page,
        size = 30,
        isTotal = true,
        oldTotal
    }) {


        orderBy = orderBy || {
            'SHEET_DATA_ADD_TIME': 'desc'
        };
        let fields = 'SHEET_DATA_PAY_TIME,SHEET_DATA_ADD_TIME,SHEET_DATA_FEE,SHEET_DATA_FORMS,SHEET_DATA_PAY_FEE,SHEET_DATA_PAY_STATUS,sheet.SHEET_TITLE,sheet.SHEET_OBJ';

        let where = {};
        if (!config.IS_DEMO) {
            let user = await UserModel.getOne({ USER_MINI_OPENID: userId });
            if (!user) return null;
            where = {
                SHEET_DATA_NAME: user.USER_NAME,
                SHEET_DATA_MOBILE: user.USER_MOBILE,
                'sheet.SHEET_STATUS': SheetModel.STATUS.COMM
            };
        }
        else {
            where = {
                SHEET_DATA_NAME: 'Tom',
                SHEET_DATA_MOBILE: '14600000000',
                'sheet.SHEET_STATUS': SheetModel.STATUS.COMM
            };
        }

        if (util.isDefined(search) && search) {
            where['SHEET_DATA_SHEET_TITLE'] = {
                $regex: '.*' + search,
                $options: 'i'
            };
        } else if (sortType) {
            // 搜索菜单
            switch (sortType) {
                case 'status': {
                    where['SHEET_DATA_PAY_STATUS'] = Number(sortVal);
                    break;
                }
                case 'sort': { //按时间倒序
                    orderBy = this.fmtOrderBySort(sortVal, 'SHEET_DATA_ADD_TIME');
                    break;
                }

            }
        }

        let joinParams = {
            from: SheetModel.CL,
            localField: 'SHEET_DATA_SHEET_ID',
            foreignField: '_id',
            as: 'sheet',
        };

        let result = await SheetDataModel.getListJoin(joinParams, where, fields, orderBy, page, size, isTotal, oldTotal);

        return result;
    }

    async statSheetData(sheetId) {
        let where = {
            SHEET_DATA_SHEET_ID: sheetId
        }

        // 总数
        let cnt = await SheetDataModel.count(where);


        // 总费用
        let fee = await SheetDataModel.sum(where, 'SHEET_DATA_FEE');


        // 已支付记录
        let wherePayCnt = {
            SHEET_DATA_SHEET_ID: sheetId,
            SHEET_DATA_PAY_STATUS: 1,
        }
        let payCnt = await SheetDataModel.count(wherePayCnt);

        // 无须支付
        let whereNoCnt = {
            SHEET_DATA_SHEET_ID: sheetId,
            SHEET_DATA_PAY_STATUS: 99,
        }
        let noCnt = await SheetDataModel.count(whereNoCnt);

        // 已支付金额
        let wherePayFee = {
            SHEET_DATA_SHEET_ID: sheetId,
            SHEET_DATA_PAY_STATUS: 1,
        }
        let payFee = await SheetDataModel.sum(wherePayFee, 'SHEET_DATA_PAY_FEE');

        let waitCnt = cnt - payCnt - noCnt;
        let waitFee = fee - payFee;
        let data = {
            SHEET_CNT: cnt,
            SHEET_PAY_CNT: payCnt,
            SHEET_WAIT_CNT: waitCnt,
            SHEET_NO_CNT: noCnt,

            SHEET_FEE: fee,
            SHEET_PAY_FEE: payFee,
            SHEET_WAIT_FEE: waitFee
        }
        await SheetModel.edit(sheetId, data);
        return { cnt, payCnt, waitCnt, noCnt, fee, payFee, waitFee };
    }

    // 修正本地订单状态
    async fixSheetDataPay(tradeNo, sheetId) {
        if (!tradeNo) {
            // 无支付号空单
            let data = {
                SHEET_DATA_PAY_STATUS: 0,
                SHEET_DATA_PAY_TRADE_NO: '',
                SHEET_DATA_PAY_FEE: 0,
                SHEET_DATA_PAY_TIME: 0,
            }

            await SheetDataModel.edit({ SHEET_DATA_PAY_TRADE_NO: tradeNo }, data);

            // 重新统计
            this.statSheetData(sheetId);

            return false;
        }

        let payService = new PayService();
        if (!await payService.fixPayResult(tradeNo)) {
            // 关闭未支付单
            payService.closePay(tradeNo);

            // 未支付
            let data = {
                SHEET_DATA_PAY_STATUS: 0,
                SHEET_DATA_PAY_TRADE_NO: '',
                SHEET_DATA_PAY_FEE: 0,
                SHEET_DATA_PAY_TIME: 0,
            }

            await SheetDataModel.edit({ SHEET_DATA_PAY_TRADE_NO: tradeNo }, data);

            // 重新统计
            this.statSheetData(sheetId);

            return false;
        }

        // 已支付
        let pay = await PayModel.getOne({ PAY_TRADE_NO: tradeNo });
        if (!pay) this.AppError('支付流水异常,请核查');

        // 更新支付信息
        let data = {
            SHEET_DATA_PAY_STATUS: 1,
            SHEET_DATA_PAY_TRADE_NO: tradeNo,
            SHEET_DATA_PAY_FEE: pay.PAY_TOTAL_FEE,
            SHEET_DATA_PAY_TIME: pay.PAY_END_TIME,
        }
        await SheetDataModel.edit({ SHEET_DATA_PAY_TRADE_NO: tradeNo }, data);


        // 重新统计
        this.statSheetData(sheetId);
        return true;
    }


    // 预支付
    async prepay(userId, sheetDataId) {

        this.AppError('[物业缴费]该功能暂不开放,如有需要请加作者微信:cclinux0730');
    }

    // 查询支付结果
    async queryPayResult(sheetDataId) {
        let sheetData = await SheetDataModel.getOne(sheetDataId);
        if (!sheetData) return { status: 0 };

        return {
            status: sheetData.SHEET_DATA_PAY_STATUS
        }
    }

住户端 UI设计

image.png
image.png
image.png
image.png
image.png
image.png

管理端UI设计

image.png
image.png
image.png
image.png
image.png
image.png
image.png
image.png

源码

github源码下载


CC同学呀
27 声望13 粉丝

鹅厂程序猿一枚,交流v: cclinux0730