资讯

精准传达 • 有效沟通

从品牌网站建设到网络营销策划,从策略到执行的一站式服务

PostgreSQL怎么实现用户自定义

本篇内容介绍了“PostgreSQL怎么实现用户自定义”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!

站在用户的角度思考问题,与客户深入沟通,找到仓山网站设计与仓山网站推广的解决方案,凭借多年的经验,让设计与互联网技术结合,创造个性化、用户体验好的作品,建站类型包括:成都做网站、网站设计、外贸营销网站建设、企业官网、英文网站、手机端网站、网站推广、国际域名空间、虚拟主机、企业邮箱。业务覆盖仓山地区。


Bison输入文件的组成:

%{
Declarations
%}
Definitions
%%
Productions
%%
User subroutines

一、User subroutines

用户自定义例程会原封不动的拷贝到gram.c文件中,详细请参考gram.c文件.

二、源码

下面是gram.y用户自定义部分的源码

/*
 * The signature of this function is required by bison.  However, we
 * ignore the passed yylloc and instead use the last token position
 * available from the scanner.
 */
static void
base_yyerror(YYLTYPE *yylloc, core_yyscan_t yyscanner, const char *msg)
{
    parser_yyerror(msg);
}
static RawStmt *
makeRawStmt(Node *stmt, int stmt_location)
{
    RawStmt    *rs = makeNode(RawStmt);
    rs->stmt = stmt;
    rs->stmt_location = stmt_location;
    rs->stmt_len = 0;            /* might get changed later */
    return rs;
}
/* Adjust a RawStmt to reflect that it doesn't run to the end of the string */
static void
updateRawStmtEnd(RawStmt *rs, int end_location)
{
    /*
     * If we already set the length, don't change it.  This is for situations
     * like "select foo ;; select bar" where the same statement will be last
     * in the string for more than one semicolon.
     */
    if (rs->stmt_len > 0)
        return;
    /* OK, update length of RawStmt */
    rs->stmt_len = end_location - rs->stmt_location;
}
static Node *
makeColumnRef(char *colname, List *indirection,
              int location, core_yyscan_t yyscanner)
{
    /*
     * Generate a ColumnRef node, with an A_Indirection node added if there
     * is any subscripting in the specified indirection list.  However,
     * any field selection at the start of the indirection list must be
     * transposed into the "fields" part of the ColumnRef node.
     */
    ColumnRef  *c = makeNode(ColumnRef);
    int        nfields = 0;
    ListCell *l;
    c->location = location;
    foreach(l, indirection)
    {
        if (IsA(lfirst(l), A_Indices))
        {
            A_Indirection *i = makeNode(A_Indirection);
            if (nfields == 0)
            {
                /* easy case - all indirection goes to A_Indirection */
                c->fields = list_make1(makeString(colname));
                i->indirection = check_indirection(indirection, yyscanner);
            }
            else
            {
                /* got to split the list in two */
                i->indirection = check_indirection(list_copy_tail(indirection,
                                                                  nfields),
                                                   yyscanner);
                indirection = list_truncate(indirection, nfields);
                c->fields = lcons(makeString(colname), indirection);
            }
            i->arg = (Node *) c;
            return (Node *) i;
        }
        else if (IsA(lfirst(l), A_Star))
        {
            /* We only allow '*' at the end of a ColumnRef */
            if (lnext(l) != NULL)
                parser_yyerror("improper use of \"*\"");
        }
        nfields++;
    }
    /* No subscripting, so all indirection gets added to field list */
    c->fields = lcons(makeString(colname), indirection);
    return (Node *) c;
}
static Node *
makeTypeCast(Node *arg, TypeName *typename, int location)
{
    TypeCast *n = makeNode(TypeCast);
    n->arg = arg;
    n->typeName = typename;
    n->location = location;
    return (Node *) n;
}
static Node *
makeStringConst(char *str, int location)
{
    A_Const *n = makeNode(A_Const);
    n->val.type = T_String;
    n->val.val.str = str;
    n->location = location;
    return (Node *)n;
}
static Node *
makeStringConstCast(char *str, int location, TypeName *typename)
{
    Node *s = makeStringConst(str, location);
    return makeTypeCast(s, typename, -1);
}
static Node *
makeIntConst(int val, int location)
{
    A_Const *n = makeNode(A_Const);
    n->val.type = T_Integer;
    n->val.val.ival = val;
    n->location = location;
    return (Node *)n;
}
static Node *
makeFloatConst(char *str, int location)
{
    A_Const *n = makeNode(A_Const);
    n->val.type = T_Float;
    n->val.val.str = str;
    n->location = location;
    return (Node *)n;
}
static Node *
makeBitStringConst(char *str, int location)
{
    A_Const *n = makeNode(A_Const);
    n->val.type = T_BitString;
    n->val.val.str = str;
    n->location = location;
    return (Node *)n;
}
static Node *
makeNullAConst(int location)
{
    A_Const *n = makeNode(A_Const);
    n->val.type = T_Null;
    n->location = location;
    return (Node *)n;
}
static Node *
makeAConst(Value *v, int location)
{
    Node *n;
    switch (v->type)
    {
        case T_Float:
            n = makeFloatConst(v->val.str, location);
            break;
        case T_Integer:
            n = makeIntConst(v->val.ival, location);
            break;
        case T_String:
        default:
            n = makeStringConst(v->val.str, location);
            break;
    }
    return n;
}
/* makeBoolAConst()
 * Create an A_Const string node and put it inside a boolean cast.
 */
static Node *
makeBoolAConst(bool state, int location)
{
    A_Const *n = makeNode(A_Const);
    n->val.type = T_String;
    n->val.val.str = (state ? "t" : "f");
    n->location = location;
    return makeTypeCast((Node *)n, SystemTypeName("bool"), -1);
}
/* makeRoleSpec
 * Create a RoleSpec with the given type
 */
static RoleSpec *
makeRoleSpec(RoleSpecType type, int location)
{
    RoleSpec *spec = makeNode(RoleSpec);
    spec->roletype = type;
    spec->location = location;
    return spec;
}
/* check_qualified_name --- check the result of qualified_name production
 *
 * It's easiest to let the grammar production for qualified_name allow
 * subscripts and '*', which we then must reject here.
 */
static void
check_qualified_name(List *names, core_yyscan_t yyscanner)
{
    ListCell   *i;
    foreach(i, names)
    {
        if (!IsA(lfirst(i), String))
            parser_yyerror("syntax error");
    }
}
/* check_func_name --- check the result of func_name production
 *
 * It's easiest to let the grammar production for func_name allow subscripts
 * and '*', which we then must reject here.
 */
static List *
check_func_name(List *names, core_yyscan_t yyscanner)
{
    ListCell   *i;
    foreach(i, names)
    {
        if (!IsA(lfirst(i), String))
            parser_yyerror("syntax error");
    }
    return names;
}
/* check_indirection --- check the result of indirection production
 *
 * We only allow '*' at the end of the list, but it's hard to enforce that
 * in the grammar, so do it here.
 */
static List *
check_indirection(List *indirection, core_yyscan_t yyscanner)
{
    ListCell *l;
    foreach(l, indirection)
    {
        if (IsA(lfirst(l), A_Star))
        {
            if (lnext(l) != NULL)
                parser_yyerror("improper use of \"*\"");
        }
    }
    return indirection;
}
/* extractArgTypes()
 * Given a list of FunctionParameter nodes, extract a list of just the
 * argument types (TypeNames) for input parameters only.  This is what
 * is needed to look up an existing function, which is what is wanted by
 * the productions that use this call.
 */
static List *
extractArgTypes(List *parameters)
{
    List       *result = NIL;
    ListCell   *i;
    foreach(i, parameters)
    {
        FunctionParameter *p = (FunctionParameter *) lfirst(i);
        if (p->mode != FUNC_PARAM_OUT && p->mode != FUNC_PARAM_TABLE)
            result = lappend(result, p->argType);
    }
    return result;
}
/* extractAggrArgTypes()
 * As above, but work from the output of the aggr_args production.
 */
static List *
extractAggrArgTypes(List *aggrargs)
{
    Assert(list_length(aggrargs) == 2);
    return extractArgTypes((List *) linitial(aggrargs));
}
/* makeOrderedSetArgs()
 * Build the result of the aggr_args production (which see the comments for).
 * This handles only the case where both given lists are nonempty, so that
 * we have to deal with multiple VARIADIC arguments.
 */
static List *
makeOrderedSetArgs(List *directargs, List *orderedargs,
                   core_yyscan_t yyscanner)
{
    FunctionParameter *lastd = (FunctionParameter *) llast(directargs);
    int            ndirectargs;
    /* No restriction unless last direct arg is VARIADIC */
    if (lastd->mode == FUNC_PARAM_VARIADIC)
    {
        FunctionParameter *firsto = (FunctionParameter *) linitial(orderedargs);
        /*
         * We ignore the names, though the aggr_arg production allows them;
         * it doesn't allow default values, so those need not be checked.
         */
        if (list_length(orderedargs) != 1 ||
            firsto->mode != FUNC_PARAM_VARIADIC ||
            !equal(lastd->argType, firsto->argType))
            ereport(ERROR,
                    (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
                     errmsg("an ordered-set aggregate with a VARIADIC direct argument must have one VARIADIC aggregated argument of the same data type"),
                     parser_errposition(exprLocation((Node *) firsto))));
        /* OK, drop the duplicate VARIADIC argument from the internal form */
        orderedargs = NIL;
    }
    /* don't merge into the next line, as list_concat changes directargs */
    ndirectargs = list_length(directargs);
    return list_make2(list_concat(directargs, orderedargs),
                      makeInteger(ndirectargs));
}
/* insertSelectOptions()
 * Insert ORDER BY, etc into an already-constructed SelectStmt.
 *
 * This routine is just to avoid duplicating code in SelectStmt productions.
 */
static void
insertSelectOptions(SelectStmt *stmt,
                    List *sortClause, List *lockingClause,
                    Node *limitOffset, Node *limitCount,
                    WithClause *withClause,
                    core_yyscan_t yyscanner)
{
    Assert(IsA(stmt, SelectStmt));
    /*
     * Tests here are to reject constructs like
     *    (SELECT foo ORDER BY bar) ORDER BY baz
     */
    if (sortClause)
    {
        if (stmt->sortClause)
            ereport(ERROR,
                    (errcode(ERRCODE_SYNTAX_ERROR),
                     errmsg("multiple ORDER BY clauses not allowed"),
                     parser_errposition(exprLocation((Node *) sortClause))));
        stmt->sortClause = sortClause;
    }
    /* We can handle multiple locking clauses, though */
    stmt->lockingClause = list_concat(stmt->lockingClause, lockingClause);
    if (limitOffset)
    {
        if (stmt->limitOffset)
            ereport(ERROR,
                    (errcode(ERRCODE_SYNTAX_ERROR),
                     errmsg("multiple OFFSET clauses not allowed"),
                     parser_errposition(exprLocation(limitOffset))));
        stmt->limitOffset = limitOffset;
    }
    if (limitCount)
    {
        if (stmt->limitCount)
            ereport(ERROR,
                    (errcode(ERRCODE_SYNTAX_ERROR),
                     errmsg("multiple LIMIT clauses not allowed"),
                     parser_errposition(exprLocation(limitCount))));
        stmt->limitCount = limitCount;
    }
    if (withClause)
    {
        if (stmt->withClause)
            ereport(ERROR,
                    (errcode(ERRCODE_SYNTAX_ERROR),
                     errmsg("multiple WITH clauses not allowed"),
                     parser_errposition(exprLocation((Node *) withClause))));
        stmt->withClause = withClause;
    }
}
static Node *
makeSetOp(SetOperation op, bool all, Node *larg, Node *rarg)
{
    SelectStmt *n = makeNode(SelectStmt);
    n->op = op;
    n->all = all;
    n->larg = (SelectStmt *) larg;
    n->rarg = (SelectStmt *) rarg;
    return (Node *) n;
}
/* SystemFuncName()
 * Build a properly-qualified reference to a built-in function.
 */
List *
SystemFuncName(char *name)
{
    return list_make2(makeString("pg_catalog"), makeString(name));
}
/* SystemTypeName()
 * Build a properly-qualified reference to a built-in type.
 *
 * typmod is defaulted, but may be changed afterwards by caller.
 * Likewise for the location.
 */
TypeName *
SystemTypeName(char *name)
{
    return makeTypeNameFromNameList(list_make2(makeString("pg_catalog"),
                                               makeString(name)));
}
/* doNegate()
 * Handle negation of a numeric constant.
 *
 * Formerly, we did this here because the optimizer couldn't cope with
 * indexquals that looked like "var = -4" --- it wants "var = const"
 * and a unary minus operator applied to a constant didn't qualify.
 * As of Postgres 7.0, that problem doesn't exist anymore because there
 * is a constant-subexpression simplifier in the optimizer.  However,
 * there's still a good reason for doing this here, which is that we can
 * postpone committing to a particular internal representation for simple
 * negative constants.    It's better to leave "-123.456" in string form
 * until we know what the desired type is.
 */
static Node *
doNegate(Node *n, int location)
{
    if (IsA(n, A_Const))
    {
        A_Const *con = (A_Const *)n;
        /* report the constant's location as that of the '-' sign */
        con->location = location;
        if (con->val.type == T_Integer)
        {
            con->val.val.ival = -con->val.val.ival;
            return n;
        }
        if (con->val.type == T_Float)
        {
            doNegateFloat(&con->val);
            return n;
        }
    }
    return (Node *) makeSimpleA_Expr(AEXPR_OP, "-", NULL, n, location);
}
static void
doNegateFloat(Value *v)
{
    char   *oldval = v->val.str;
    Assert(IsA(v, Float));
    if (*oldval == '+')
        oldval++;
    if (*oldval == '-')
        v->val.str = oldval+1;    /* just strip the '-' */
    else
        v->val.str = psprintf("-%s", oldval);
}
static Node *
makeAndExpr(Node *lexpr, Node *rexpr, int location)
{
    Node       *lexp = lexpr;
    /* Look through AEXPR_PAREN nodes so they don't affect flattening */
    while (IsA(lexp, A_Expr) &&
           ((A_Expr *) lexp)->kind == AEXPR_PAREN)
        lexp = ((A_Expr *) lexp)->lexpr;
    /* Flatten "a AND b AND c ..." to a single BoolExpr on sight */
    if (IsA(lexp, BoolExpr))
    {
        BoolExpr *blexpr = (BoolExpr *) lexp;
        if (blexpr->boolop == AND_EXPR)
        {
            blexpr->args = lappend(blexpr->args, rexpr);
            return (Node *) blexpr;
        }
    }
    return (Node *) makeBoolExpr(AND_EXPR, list_make2(lexpr, rexpr), location);
}
static Node *
makeOrExpr(Node *lexpr, Node *rexpr, int location)
{
    Node       *lexp = lexpr;
    /* Look through AEXPR_PAREN nodes so they don't affect flattening */
    while (IsA(lexp, A_Expr) &&
           ((A_Expr *) lexp)->kind == AEXPR_PAREN)
        lexp = ((A_Expr *) lexp)->lexpr;
    /* Flatten "a OR b OR c ..." to a single BoolExpr on sight */
    if (IsA(lexp, BoolExpr))
    {
        BoolExpr *blexpr = (BoolExpr *) lexp;
        if (blexpr->boolop == OR_EXPR)
        {
            blexpr->args = lappend(blexpr->args, rexpr);
            return (Node *) blexpr;
        }
    }
    return (Node *) makeBoolExpr(OR_EXPR, list_make2(lexpr, rexpr), location);
}
static Node *
makeNotExpr(Node *expr, int location)
{
    return (Node *) makeBoolExpr(NOT_EXPR, list_make1(expr), location);
}
static Node *
makeAArrayExpr(List *elements, int location)
{
    A_ArrayExpr *n = makeNode(A_ArrayExpr);
    n->elements = elements;
    n->location = location;
    return (Node *) n;
}
static Node *
makeSQLValueFunction(SQLValueFunctionOp op, int32 typmod, int location)
{
    SQLValueFunction *svf = makeNode(SQLValueFunction);
    svf->op = op;
    /* svf->type will be filled during parse anaXXXis */
    svf->typmod = typmod;
    svf->location = location;
    return (Node *) svf;
}
static Node *
makeXmlExpr(XmlExprOp op, char *name, List *named_args, List *args,
            int location)
{
    XmlExpr        *x = makeNode(XmlExpr);
    x->op = op;
    x->name = name;
    /*
     * named_args is a list of ResTarget; it'll be split apart into separate
     * expression and name lists in transformXmlExpr().
     */
    x->named_args = named_args;
    x->arg_names = NIL;
    x->args = args;
    /* xmloption, if relevant, must be filled in by caller */
    /* type and typmod will be filled in during parse anaXXXis */
    x->type = InvalidOid;            /* marks the node as not analyzed */
    x->location = location;
    return (Node *) x;
}
/*
 * Merge the input and output parameters of a table function.
 */
static List *
mergeTableFuncParameters(List *func_args, List *columns)
{
    ListCell   *lc;
    /* Explicit OUT and INOUT parameters shouldn't be used in this syntax */
    foreach(lc, func_args)
    {
        FunctionParameter *p = (FunctionParameter *) lfirst(lc);
        if (p->mode != FUNC_PARAM_IN && p->mode != FUNC_PARAM_VARIADIC)
            ereport(ERROR,
                    (errcode(ERRCODE_SYNTAX_ERROR),
                     errmsg("OUT and INOUT arguments aren't allowed in TABLE functions")));
    }
    return list_concat(func_args, columns);
}
/*
 * Determine return type of a TABLE function.  A single result column
 * returns setof that column's type; otherwise return setof record.
 */
static TypeName *
TableFuncTypeName(List *columns)
{
    TypeName *result;
    if (list_length(columns) == 1)
    {
        FunctionParameter *p = (FunctionParameter *) linitial(columns);
        result = copyObject(p->argType);
    }
    else
        result = SystemTypeName("record");
    result->setof = true;
    return result;
}
/*
 * Convert a list of (dotted) names to a RangeVar (like
 * makeRangeVarFromNameList, but with position support).  The
 * "AnyName" refers to the any_name production in the grammar.
 */
static RangeVar *
makeRangeVarFromAnyName(List *names, int position, core_yyscan_t yyscanner)
{
    RangeVar *r = makeNode(RangeVar);
    switch (list_length(names))
    {
        case 1:
            r->catalogname = NULL;
            r->schemaname = NULL;
            r->relname = strVal(linitial(names));
            break;
        case 2:
            r->catalogname = NULL;
            r->schemaname = strVal(linitial(names));
            r->relname = strVal(lsecond(names));
            break;
        case 3:
            r->catalogname = strVal(linitial(names));
            r->schemaname = strVal(lsecond(names));
            r->relname = strVal(lthird(names));
            break;
        default:
            ereport(ERROR,
                    (errcode(ERRCODE_SYNTAX_ERROR),
                     errmsg("improper qualified name (too many dotted names): %s",
                            NameListToString(names)),
                     parser_errposition(position)));
            break;
    }
    r->relpersistence = RELPERSISTENCE_PERMANENT;
    r->location = position;
    return r;
}
/* Separate Constraint nodes from COLLATE clauses in a ColQualList */
static void
SplitColQualList(List *qualList,
                 List **constraintList, CollateClause **collClause,
                 core_yyscan_t yyscanner)
{
    ListCell   *cell;
    ListCell   *prev;
    ListCell   *next;
    *collClause = NULL;
    prev = NULL;
    for (cell = list_head(qualList); cell; cell = next)
    {
        Node   *n = (Node *) lfirst(cell);
        next = lnext(cell);
        if (IsA(n, Constraint))
        {
            /* keep it in list */
            prev = cell;
            continue;
        }
        if (IsA(n, CollateClause))
        {
            CollateClause *c = (CollateClause *) n;
            if (*collClause)
                ereport(ERROR,
                        (errcode(ERRCODE_SYNTAX_ERROR),
                         errmsg("multiple COLLATE clauses not allowed"),
                         parser_errposition(c->location)));
            *collClause = c;
        }
        else
            elog(ERROR, "unexpected node type %d", (int) n->type);
        /* remove non-Constraint nodes from qualList */
        qualList = list_delete_cell(qualList, cell, prev);
    }
    *constraintList = qualList;
}
/*
 * Process result of ConstraintAttributeSpec, and set appropriate bool flags
 * in the output command node.  Pass NULL for any flags the particular
 * command doesn't support.
 */
static void
processCASbits(int cas_bits, int location, const char *constrType,
               bool *deferrable, bool *initdeferred, bool *not_valid,
               bool *no_inherit, core_yyscan_t yyscanner)
{
    /* defaults */
    if (deferrable)
        *deferrable = false;
    if (initdeferred)
        *initdeferred = false;
    if (not_valid)
        *not_valid = false;
    if (cas_bits & (CAS_DEFERRABLE | CAS_INITIALLY_DEFERRED))
    {
        if (deferrable)
            *deferrable = true;
        else
            ereport(ERROR,
                    (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
                     /* translator: %s is CHECK, UNIQUE, or similar */
                     errmsg("%s constraints cannot be marked DEFERRABLE",
                            constrType),
                     parser_errposition(location)));
    }
    if (cas_bits & CAS_INITIALLY_DEFERRED)
    {
        if (initdeferred)
            *initdeferred = true;
        else
            ereport(ERROR,
                    (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
                     /* translator: %s is CHECK, UNIQUE, or similar */
                     errmsg("%s constraints cannot be marked DEFERRABLE",
                            constrType),
                     parser_errposition(location)));
    }
    if (cas_bits & CAS_NOT_VALID)
    {
        if (not_valid)
            *not_valid = true;
        else
            ereport(ERROR,
                    (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
                     /* translator: %s is CHECK, UNIQUE, or similar */
                     errmsg("%s constraints cannot be marked NOT VALID",
                            constrType),
                     parser_errposition(location)));
    }
    if (cas_bits & CAS_NO_INHERIT)
    {
        if (no_inherit)
            *no_inherit = true;
        else
            ereport(ERROR,
                    (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
                     /* translator: %s is CHECK, UNIQUE, or similar */
                     errmsg("%s constraints cannot be marked NO INHERIT",
                            constrType),
                     parser_errposition(location)));
    }
}
/*----------
 * Recursive view transformation
 *
 * Convert
 *
 *     CREATE RECURSIVE VIEW relname (aliases) AS query
 *
 * to
 *
 *     CREATE VIEW relname (aliases) AS
 *         WITH RECURSIVE relname (aliases) AS (query)
 *         SELECT aliases FROM relname
 *
 * Actually, just the WITH ... part, which is then inserted into the original
 * view definition as the query.
 * ----------
 */
static Node *
makeRecursiveViewSelect(char *relname, List *aliases, Node *query)
{
    SelectStmt *s = makeNode(SelectStmt);
    WithClause *w = makeNode(WithClause);
    CommonTableExpr *cte = makeNode(CommonTableExpr);
    List       *tl = NIL;
    ListCell   *lc;
    /* create common table expression */
    cte->ctename = relname;
    cte->aliascolnames = aliases;
    cte->ctequery = query;
    cte->location = -1;
    /* create WITH clause and attach CTE */
    w->recursive = true;
    w->ctes = list_make1(cte);
    w->location = -1;
    /* create target list for the new SELECT from the alias list of the
     * recursive view specification */
    foreach (lc, aliases)
    {
        ResTarget *rt = makeNode(ResTarget);
        rt->name = NULL;
        rt->indirection = NIL;
        rt->val = makeColumnRef(strVal(lfirst(lc)), NIL, -1, 0);
        rt->location = -1;
        tl = lappend(tl, rt);
    }
    /* create new SELECT combining WITH clause, target list, and fake FROM
     * clause */
    s->withClause = w;
    s->targetList = tl;
    s->fromClause = list_make1(makeRangeVar(NULL, relname, -1));
    return (Node *) s;
}
/* parser_init()
 * Initialize to parse one query string
 */
void
parser_init(base_yy_extra_type *yyext)
{
    yyext->parsetree = NIL;        /* in case grammar forgets to set it */
}

“PostgreSQL怎么实现用户自定义”的内容就介绍到这里了,感谢大家的阅读。如果想了解更多行业相关的知识可以关注创新互联网站,小编将为大家输出更多高质量的实用文章!


分享文章:PostgreSQL怎么实现用户自定义
分享地址:http://cdkjz.cn/article/ihjojo.html
多年建站经验

多一份参考,总有益处

联系快上网,免费获得专属《策划方案》及报价

咨询相关问题或预约面谈,可以通过以下方式与我们联系

大客户专线   成都:13518219792   座机:028-86922220